""" Ride Search and Filter Forms Forms for the comprehensive ride filtering system with 8 categories: 1. Search text 2. Basic info 3. Dates 4. Height/safety 5. Performance 6. Relationships 7. Roller coaster 8. Company Each form handles validation and provides clean data for the RideSearchService. """ from django import forms from django.core.exceptions import ValidationError from typing import Dict, Any from apps.parks.models import Park, ParkArea from apps.rides.models.company import Company from apps.rides.models.rides import RideModel class BaseFilterForm(forms.Form): """Base form with common functionality for all filter forms.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Add common CSS classes for field in self.fields.values(): field.widget.attrs.update({"class": "form-input"}) class SearchTextForm(BaseFilterForm): """Form for text-based search filters.""" global_search = forms.CharField( required=False, max_length=255, widget=forms.TextInput( attrs={ "placeholder": "Search rides, parks, manufacturers...", "class": ( "w-full px-4 py-2 border border-gray-300 " "dark:border-gray-600 rounded-lg focus:ring-2 " "focus:ring-blue-500 focus:border-transparent " "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" ), "autocomplete": "off", "hx-get": "", "hx-trigger": "input changed delay:300ms", "hx-target": "#search-results", "hx-indicator": "#search-loading", } ), ) name_search = forms.CharField( required=False, max_length=255, widget=forms.TextInput( attrs={ "placeholder": "Search by ride name...", "class": ( "w-full px-3 py-2 border border-gray-300 " "dark:border-gray-600 rounded-md focus:ring-2 " "focus:ring-blue-500 focus:border-transparent " "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" ), } ), ) description_search = forms.CharField( required=False, max_length=255, widget=forms.TextInput( attrs={ "placeholder": "Search in descriptions...", "class": ( "w-full px-3 py-2 border border-gray-300 " "dark:border-gray-600 rounded-md focus:ring-2 " "focus:ring-blue-500 focus:border-transparent " "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" ), } ), ) class BasicInfoForm(BaseFilterForm): """Form for basic ride information filters.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Get choices from Rich Choice registry from apps.core.choices.registry import get_choices # Get choices - let exceptions propagate if registry fails category_choices = [(choice.value, choice.label) for choice in get_choices("categories", "rides")] status_choices = [(choice.value, choice.label) for choice in get_choices("statuses", "rides")] # Update field choices dynamically self.fields['category'].choices = category_choices self.fields['status'].choices = status_choices category = forms.MultipleChoiceField( choices=[], # Will be populated in __init__ required=False, widget=forms.CheckboxSelectMultiple(attrs={"class": "grid grid-cols-2 gap-2"}), ) status = forms.MultipleChoiceField( choices=[], # Will be populated in __init__ required=False, widget=forms.CheckboxSelectMultiple(attrs={"class": "space-y-2"}), ) park = forms.ModelMultipleChoiceField( queryset=Park.objects.all(), required=False, widget=forms.CheckboxSelectMultiple( attrs={"class": "max-h-48 overflow-y-auto space-y-1"} ), ) park_area = forms.ModelMultipleChoiceField( queryset=ParkArea.objects.all(), required=False, widget=forms.CheckboxSelectMultiple( attrs={"class": "max-h-48 overflow-y-auto space-y-1"} ), ) class DateRangeWidget(forms.MultiWidget): """Custom widget for date range inputs.""" def __init__(self, attrs=None): widgets = [ forms.DateInput( attrs={ "type": "date", "class": ( "px-3 py-2 border border-gray-300 " "dark:border-gray-600 rounded-md focus:ring-2 " "focus:ring-blue-500 focus:border-transparent " "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" ), } ), forms.DateInput( attrs={ "type": "date", "class": ( "px-3 py-2 border border-gray-300 " "dark:border-gray-600 rounded-md focus:ring-2 " "focus:ring-blue-500 focus:border-transparent " "bg-white dark:bg-gray-800 text-gray-900 dark:text-white" ), } ), ] super().__init__(widgets, attrs) def decompress(self, value): if value: return [value.get("start"), value.get("end")] return [None, None] class DateRangeField(forms.MultiValueField): """Custom field for date ranges.""" def __init__(self, *args, **kwargs): fields = [forms.DateField(required=False), forms.DateField(required=False)] super().__init__(fields, *args, **kwargs) self.widget = DateRangeWidget() def compress(self, data_list): if data_list: start_date, end_date = data_list if start_date or end_date: return {"start": start_date, "end": end_date} return None def validate(self, value): super().validate(value) if value and value.get("start") and value.get("end"): if value["start"] > value["end"]: raise ValidationError("Start date must be before end date.") class DateFiltersForm(BaseFilterForm): """Form for date range filters.""" opening_date_range = DateRangeField(required=False, label="Opening Date Range") closing_date_range = DateRangeField(required=False, label="Closing Date Range") status_since_range = DateRangeField(required=False, label="Status Since Range") class NumberRangeWidget(forms.MultiWidget): """Custom widget for number range inputs.""" def __init__(self, attrs=None, min_val=None, max_val=None, step=None): widgets = [ forms.NumberInput( attrs={ "class": ( "px-3 py-2 border border-gray-300 dark:border-gray-600 " "rounded-md focus:ring-2 focus:ring-blue-500 " "focus:border-transparent bg-white dark:bg-gray-800 " "text-gray-900 dark:text-white" ), "placeholder": "Min", "min": min_val, "max": max_val, "step": step, } ), forms.NumberInput( attrs={ "class": ( "px-3 py-2 border border-gray-300 dark:border-gray-600 " "rounded-md focus:ring-2 focus:ring-blue-500 " "focus:border-transparent bg-white dark:bg-gray-800 " "text-gray-900 dark:text-white" ), "placeholder": "Max", "min": min_val, "max": max_val, "step": step, } ), ] super().__init__(widgets, attrs) def decompress(self, value): if value: return [value.get("min"), value.get("max")] return [None, None] class NumberRangeField(forms.MultiValueField): """Custom field for number ranges.""" def __init__(self, min_val=None, max_val=None, step=None, *args, **kwargs): fields = [ forms.FloatField(required=False, min_value=min_val, max_value=max_val), forms.FloatField(required=False, min_value=min_val, max_value=max_val), ] super().__init__(fields, *args, **kwargs) self.widget = NumberRangeWidget(min_val=min_val, max_val=max_val, step=step) def compress(self, data_list): if data_list: min_val, max_val = data_list if min_val is not None or max_val is not None: return {"min": min_val, "max": max_val} return None def validate(self, value): super().validate(value) if value and value.get("min") is not None and value.get("max") is not None: if value["min"] > value["max"]: raise ValidationError("Minimum value must be less than maximum value.") class HeightSafetyForm(BaseFilterForm): """Form for height and safety requirement filters.""" min_height_range = NumberRangeField( min_val=0, max_val=84, # 7 feet in inches step=1, required=False, label="Minimum Height (inches)", ) max_height_range = NumberRangeField( min_val=0, max_val=84, step=1, required=False, label="Maximum Height (inches)" ) class PerformanceForm(BaseFilterForm): """Form for performance metric filters.""" capacity_range = NumberRangeField( min_val=0, max_val=5000, step=50, required=False, label="Capacity per Hour" ) duration_range = NumberRangeField( min_val=0, max_val=1800, # 30 minutes in seconds step=10, required=False, label="Duration (seconds)", ) rating_range = NumberRangeField( min_val=0.0, max_val=10.0, step=0.1, required=False, label="Average Rating" ) class RelationshipsForm(BaseFilterForm): """Form for relationship filters (manufacturer, designer, ride model).""" manufacturer = forms.ModelMultipleChoiceField( queryset=Company.objects.filter(roles__contains=["MANUFACTURER"]), required=False, widget=forms.CheckboxSelectMultiple( attrs={"class": "max-h-48 overflow-y-auto space-y-1"} ), ) designer = forms.ModelMultipleChoiceField( queryset=Company.objects.filter(roles__contains=["DESIGNER"]), required=False, widget=forms.CheckboxSelectMultiple( attrs={"class": "max-h-48 overflow-y-auto space-y-1"} ), ) ride_model = forms.ModelMultipleChoiceField( queryset=RideModel.objects.all(), required=False, widget=forms.CheckboxSelectMultiple( attrs={"class": "max-h-48 overflow-y-auto space-y-1"} ), ) class RollerCoasterForm(BaseFilterForm): """Form for roller coaster specific filters.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Get choices from Rich Choice registry from apps.core.choices.registry import get_choices # Get choices - let exceptions propagate if registry fails track_material_choices = [(choice.value, choice.label) for choice in get_choices("track_materials", "rides")] coaster_type_choices = [(choice.value, choice.label) for choice in get_choices("coaster_types", "rides")] propulsion_system_choices = [(choice.value, choice.label) for choice in get_choices("propulsion_systems", "rides")] # Update field choices dynamically self.fields['track_material'].choices = track_material_choices self.fields['coaster_type'].choices = coaster_type_choices self.fields['propulsion_system'].choices = propulsion_system_choices height_ft_range = NumberRangeField( min_val=0, max_val=500, step=1, required=False, label="Height (feet)" ) length_ft_range = NumberRangeField( min_val=0, max_val=10000, step=10, required=False, label="Length (feet)" ) speed_mph_range = NumberRangeField( min_val=0, max_val=150, step=1, required=False, label="Speed (mph)" ) inversions_range = NumberRangeField( min_val=0, max_val=20, step=1, required=False, label="Number of Inversions" ) track_material = forms.MultipleChoiceField( choices=[], # Will be populated in __init__ required=False, widget=forms.CheckboxSelectMultiple(attrs={"class": "space-y-2"}), ) coaster_type = forms.MultipleChoiceField( choices=[], # Will be populated in __init__ required=False, widget=forms.CheckboxSelectMultiple( attrs={"class": "grid grid-cols-2 gap-2 max-h-48 overflow-y-auto"} ), ) propulsion_system = forms.MultipleChoiceField( choices=[], # Will be populated in __init__ required=False, widget=forms.CheckboxSelectMultiple( attrs={"class": "space-y-2 max-h-48 overflow-y-auto"} ), ) class CompanyForm(BaseFilterForm): """Form for company-related filters.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Get choices from Rich Choice registry from apps.core.choices.registry import get_choices # Get both rides and parks company roles - let exceptions propagate if registry fails rides_roles = [(choice.value, choice.label) for choice in get_choices("company_roles", "rides")] parks_roles = [(choice.value, choice.label) for choice in get_choices("company_roles", "parks")] role_choices = rides_roles + parks_roles # Update field choices dynamically self.fields['manufacturer_roles'].choices = role_choices self.fields['designer_roles'].choices = role_choices manufacturer_roles = forms.MultipleChoiceField( choices=[], # Will be populated in __init__ required=False, widget=forms.CheckboxSelectMultiple(attrs={"class": "space-y-2"}), ) designer_roles = forms.MultipleChoiceField( choices=[], # Will be populated in __init__ required=False, widget=forms.CheckboxSelectMultiple(attrs={"class": "space-y-2"}), ) founded_date_range = DateRangeField( required=False, label="Company Founded Date Range" ) class SortingForm(BaseFilterForm): """Form for sorting options.""" # Static sorting choices - these are UI-specific and don't need Rich Choice Objects def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Static sort choices for UI functionality sort_choices = [ ("relevance", "Relevance"), ("name_asc", "Name (A-Z)"), ("name_desc", "Name (Z-A)"), ("opening_date_asc", "Opening Date (Oldest)"), ("opening_date_desc", "Opening Date (Newest)"), ("rating_asc", "Rating (Lowest)"), ("rating_desc", "Rating (Highest)"), ("height_asc", "Height (Shortest)"), ("height_desc", "Height (Tallest)"), ("speed_asc", "Speed (Slowest)"), ("speed_desc", "Speed (Fastest)"), ("capacity_asc", "Capacity (Lowest)"), ("capacity_desc", "Capacity (Highest)"), ] self.fields['sort_by'].choices = sort_choices sort_by = forms.ChoiceField( choices=[], # Will be populated in __init__ required=False, initial="relevance", widget=forms.Select( attrs={ "class": ( "px-3 py-2 border border-gray-300 dark:border-gray-600 " "rounded-md focus:ring-2 focus:ring-blue-500 " "focus:border-transparent bg-white dark:bg-gray-800 " "text-gray-900 dark:text-white" ), "hx-get": "", "hx-trigger": "change", "hx-target": "#search-results", "hx-indicator": "#search-loading", } ), ) class MasterFilterForm(BaseFilterForm): """Master form that combines all filter categories.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Initialize all sub-forms self.search_text_form = SearchTextForm(*args, **kwargs) self.basic_info_form = BasicInfoForm(*args, **kwargs) self.date_filters_form = DateFiltersForm(*args, **kwargs) self.height_safety_form = HeightSafetyForm(*args, **kwargs) self.performance_form = PerformanceForm(*args, **kwargs) self.relationships_form = RelationshipsForm(*args, **kwargs) self.roller_coaster_form = RollerCoasterForm(*args, **kwargs) self.company_form = CompanyForm(*args, **kwargs) self.sorting_form = SortingForm(*args, **kwargs) # Add all fields to this form self.fields.update(self.search_text_form.fields) self.fields.update(self.basic_info_form.fields) self.fields.update(self.date_filters_form.fields) self.fields.update(self.height_safety_form.fields) self.fields.update(self.performance_form.fields) self.fields.update(self.relationships_form.fields) self.fields.update(self.roller_coaster_form.fields) self.fields.update(self.company_form.fields) self.fields.update(self.sorting_form.fields) def clean(self): cleaned_data = super().clean() # Validate all sub-forms for form in [ self.search_text_form, self.basic_info_form, self.date_filters_form, self.height_safety_form, self.performance_form, self.relationships_form, self.roller_coaster_form, self.company_form, self.sorting_form, ]: if not form.is_valid(): for field, errors in form.errors.items(): self.add_error(field, errors) return cleaned_data def get_filter_dict(self) -> Dict[str, Any]: """Convert form data to search service filter format.""" if not self.is_valid(): return {} filters = {} # Add all cleaned data to filters for field_name, value in self.cleaned_data.items(): if value: # Only include non-empty values filters[field_name] = value return filters def get_search_filters(self) -> Dict[str, Any]: """Alias for get_filter_dict for backward compatibility.""" return self.get_filter_dict() def get_filter_summary(self) -> Dict[str, Any]: """Get summary of active filters for display.""" active_filters = {} if not self.is_valid(): return active_filters def get_active_filters_summary(self) -> Dict[str, Any]: """Alias for get_filter_summary for backward compatibility.""" return self.get_filter_summary() def has_active_filters(self) -> bool: """Check if any filters are currently active.""" if not self.is_valid(): return False for field_name, value in self.cleaned_data.items(): if value: # If any field has a value, we have active filters return True return False # Group filters by category categories = { "Search": ["global_search", "name_search", "description_search"], "Basic Info": ["category", "status", "park", "park_area"], "Dates": ["opening_date_range", "closing_date_range", "status_since_range"], "Height & Safety": ["min_height_range", "max_height_range"], "Performance": ["capacity_range", "duration_range", "rating_range"], "Relationships": ["manufacturer", "designer", "ride_model"], "Roller Coaster": [ "height_ft_range", "length_ft_range", "speed_mph_range", "inversions_range", "track_material", "coaster_type", "propulsion_system", ], "Company": ["manufacturer_roles", "designer_roles", "founded_date_range"], } for category, field_names in categories.items(): category_filters = [] for field_name in field_names: value = self.cleaned_data.get(field_name) if value: category_filters.append( { "field": field_name, "value": value, "label": self.fields[field_name].label or field_name.replace("_", " ").title(), } ) if category_filters: active_filters[category] = category_filters return active_filters