""" 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.""" CATEGORY_CHOICES = [ ("", "All Categories"), ("roller_coaster", "Roller Coaster"), ("water_ride", "Water Ride"), ("flat_ride", "Flat Ride"), ("dark_ride", "Dark Ride"), ("kiddie_ride", "Kiddie Ride"), ("transport", "Transport"), ("show", "Show"), ("other", "Other"), ] STATUS_CHOICES = [ ("", "All Statuses"), ("operating", "Operating"), ("closed_temporary", "Temporarily Closed"), ("closed_permanent", "Permanently Closed"), ("under_construction", "Under Construction"), ("announced", "Announced"), ("rumored", "Rumored"), ] category = forms.MultipleChoiceField( choices=CATEGORY_CHOICES[1:], # Exclude "All Categories" required=False, widget=forms.CheckboxSelectMultiple(attrs={"class": "grid grid-cols-2 gap-2"}), ) status = forms.MultipleChoiceField( choices=STATUS_CHOICES[1:], # Exclude "All Statuses" 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.""" TRACK_MATERIAL_CHOICES = [ ("steel", "Steel"), ("wood", "Wood"), ("hybrid", "Hybrid"), ] COASTER_TYPE_CHOICES = [ ("sit_down", "Sit Down"), ("inverted", "Inverted"), ("floorless", "Floorless"), ("flying", "Flying"), ("suspended", "Suspended"), ("stand_up", "Stand Up"), ("spinning", "Spinning"), ("launched", "Launched"), ("hypercoaster", "Hypercoaster"), ("giga_coaster", "Giga Coaster"), ("strata_coaster", "Strata Coaster"), ] LAUNCH_TYPE_CHOICES = [ ("none", "No Launch"), ("lim", "LIM (Linear Induction Motor)"), ("lsm", "LSM (Linear Synchronous Motor)"), ("hydraulic", "Hydraulic"), ("pneumatic", "Pneumatic"), ("cable", "Cable"), ("flywheel", "Flywheel"), ] 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=TRACK_MATERIAL_CHOICES, required=False, widget=forms.CheckboxSelectMultiple(attrs={"class": "space-y-2"}), ) coaster_type = forms.MultipleChoiceField( choices=COASTER_TYPE_CHOICES, required=False, widget=forms.CheckboxSelectMultiple( attrs={"class": "grid grid-cols-2 gap-2 max-h-48 overflow-y-auto"} ), ) launch_type = forms.MultipleChoiceField( choices=LAUNCH_TYPE_CHOICES, required=False, widget=forms.CheckboxSelectMultiple( attrs={"class": "space-y-2 max-h-48 overflow-y-auto"} ), ) class CompanyForm(BaseFilterForm): """Form for company-related filters.""" ROLE_CHOICES = [ ("MANUFACTURER", "Manufacturer"), ("DESIGNER", "Designer"), ("OPERATOR", "Operator"), ] manufacturer_roles = forms.MultipleChoiceField( choices=ROLE_CHOICES, required=False, widget=forms.CheckboxSelectMultiple(attrs={"class": "space-y-2"}), ) designer_roles = forms.MultipleChoiceField( choices=ROLE_CHOICES, 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.""" 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)"), ] sort_by = forms.ChoiceField( choices=SORT_CHOICES, 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_search_filters(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_active_filters_summary(self) -> Dict[str, Any]: """Get summary of active filters for display.""" active_filters = {} if not self.is_valid(): return active_filters # 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", "launch_type", ], "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