from django import forms from decimal import Decimal, InvalidOperation, ROUND_DOWN from autocomplete.core import register from autocomplete.shortcuts import ModelAutocomplete from autocomplete.widgets import AutocompleteWidget from .models import Park from .models.location import ParkLocation @register class ParkAutocomplete(ModelAutocomplete): """Autocomplete for searching parks. Features: - Name-based search with partial matching - Prefetches related owner data - Applies standard park queryset filtering - Includes park status and location in results """ model = Park search_attrs = ["name"] # We'll match on park names class ParkSearchForm(forms.Form): """Form for searching parks with autocomplete.""" park = forms.ModelChoiceField( queryset=Park.objects.all(), required=False, widget=AutocompleteWidget( ac_class=ParkAutocomplete, attrs={ "class": ( "w-full border-gray-300 rounded-lg form-input " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ), "placeholder": "Search parks...", }, ), ) class ParkForm(forms.ModelForm): """Form for creating and updating Park objects with location support""" # Location fields latitude = forms.DecimalField( max_digits=9, decimal_places=6, required=False, widget=forms.HiddenInput(), ) longitude = forms.DecimalField( max_digits=10, decimal_places=6, required=False, widget=forms.HiddenInput(), ) street_address = forms.CharField( max_length=255, required=False, widget=forms.TextInput( attrs={ "class": ( "w-full border-gray-300 rounded-lg form-input " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ) } ), ) city = forms.CharField( max_length=255, required=False, widget=forms.TextInput( attrs={ "class": ( "w-full border-gray-300 rounded-lg form-input " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ) } ), ) state = forms.CharField( max_length=255, required=False, widget=forms.TextInput( attrs={ "class": ( "w-full border-gray-300 rounded-lg form-input " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ) } ), ) country = forms.CharField( max_length=255, required=False, widget=forms.TextInput( attrs={ "class": ( "w-full border-gray-300 rounded-lg form-input " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ) } ), ) postal_code = forms.CharField( max_length=20, required=False, widget=forms.TextInput( attrs={ "class": ( "w-full border-gray-300 rounded-lg form-input " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ) } ), ) class Meta: model = Park fields = [ "name", "description", "operator", "property_owner", "status", "opening_date", "closing_date", "operating_season", "size_acres", "website", # Location fields handled separately "latitude", "longitude", "street_address", "city", "state", "country", "postal_code", ] widgets = { "name": forms.TextInput( attrs={ "class": ( "w-full border-gray-300 rounded-lg form-input " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ) } ), "description": forms.Textarea( attrs={ "class": ( "w-full border-gray-300 rounded-lg form-textarea " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ), "rows": 2, } ), "operator": forms.Select( attrs={ "class": ( "w-full border-gray-300 rounded-lg form-select " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ) } ), "property_owner": forms.Select( attrs={ "class": ( "w-full border-gray-300 rounded-lg form-select " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ) } ), "status": forms.Select( attrs={ "class": ( "w-full border-gray-300 rounded-lg form-select " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ) } ), "opening_date": forms.DateInput( attrs={ "type": "date", "class": ( "w-full border-gray-300 rounded-lg form-input " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ), } ), "closing_date": forms.DateInput( attrs={ "type": "date", "class": ( "w-full border-gray-300 rounded-lg form-input " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ), } ), "operating_season": forms.TextInput( attrs={ "class": ( "w-full border-gray-300 rounded-lg form-input " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ), "placeholder": "e.g., Year-round, Summer only, etc.", } ), "size_acres": forms.NumberInput( attrs={ "class": ( "w-full border-gray-300 rounded-lg form-input " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ), "step": "0.01", "min": "0", } ), "website": forms.URLInput( attrs={ "class": ( "w-full border-gray-300 rounded-lg form-input " "dark:border-gray-600 dark:bg-gray-700 dark:text-white" ), "placeholder": "https://example.com", } ), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Pre-fill location fields if editing existing park if self.instance and self.instance.pk and self.instance.location.exists(): location = self.instance.location.first() self.fields["latitude"].initial = location.latitude self.fields["longitude"].initial = location.longitude self.fields["street_address"].initial = location.street_address self.fields["city"].initial = location.city self.fields["state"].initial = location.state self.fields["country"].initial = location.country self.fields["postal_code"].initial = location.postal_code def clean_latitude(self): latitude = self.cleaned_data.get("latitude") if latitude is not None: try: # Convert to Decimal for precise handling latitude = Decimal(str(latitude)) # Round to exactly 6 decimal places latitude = latitude.quantize(Decimal("0.000001"), rounding=ROUND_DOWN) # Validate range if latitude < -90 or latitude > 90: raise forms.ValidationError( "Latitude must be between -90 and 90 degrees." ) # Convert to string to preserve exact decimal places return str(latitude) except (InvalidOperation, TypeError) as e: raise forms.ValidationError("Invalid latitude value.") from e return latitude def clean_longitude(self): longitude = self.cleaned_data.get("longitude") if longitude is not None: try: # Convert to Decimal for precise handling longitude = Decimal(str(longitude)) # Round to exactly 6 decimal places longitude = longitude.quantize(Decimal("0.000001"), rounding=ROUND_DOWN) # Validate range if longitude < -180 or longitude > 180: raise forms.ValidationError( "Longitude must be between -180 and 180 degrees." ) # Convert to string to preserve exact decimal places return str(longitude) except (InvalidOperation, TypeError) as e: raise forms.ValidationError("Invalid longitude value.") from e return longitude def save(self, commit=True): park = super().save(commit=False) # Prepare location data location_data = { "name": park.name, "location_type": "park", "latitude": self.cleaned_data.get("latitude"), "longitude": self.cleaned_data.get("longitude"), "street_address": self.cleaned_data.get("street_address"), "city": self.cleaned_data.get("city"), "state": self.cleaned_data.get("state"), "country": self.cleaned_data.get("country"), "postal_code": self.cleaned_data.get("postal_code"), } # Handle location: update if exists, create if not try: park_location = park.location # Update existing location for key, value in location_data.items(): if key in ["latitude", "longitude"] and value: continue # Handle coordinates separately if hasattr(park_location, key): setattr(park_location, key, value) # Handle coordinates if provided if "latitude" in location_data and "longitude" in location_data: if location_data["latitude"] and location_data["longitude"]: park_location.set_coordinates( float(location_data["latitude"]), float(location_data["longitude"]), ) park_location.save() except ParkLocation.DoesNotExist: # Create new ParkLocation coordinates_data = {} if "latitude" in location_data and "longitude" in location_data: if location_data["latitude"] and location_data["longitude"]: coordinates_data = { "latitude": float(location_data["latitude"]), "longitude": float(location_data["longitude"]), } # Remove coordinate fields from location_data for creation creation_data = { k: v for k, v in location_data.items() if k not in ["latitude", "longitude"] } creation_data.setdefault("country", "USA") park_location = ParkLocation.objects.create(park=park, **creation_data) if coordinates_data: park_location.set_coordinates( coordinates_data["latitude"], coordinates_data["longitude"] ) park_location.save() if commit: park.save() return park