from django import forms from decimal import Decimal, InvalidOperation, ROUND_DOWN from autocomplete import AutocompleteWidget from django import forms from .models import Park from .models.location import ParkLocation from .querysets import get_base_park_queryset class ParkAutocomplete(forms.Form): """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 def get_search_results(self, search): """Return search results with related data.""" return (get_base_park_queryset() .filter(name__icontains=search) .select_related('operator', 'property_owner') .order_by('name')) def format_result(self, park): """Format each park result with status and location.""" location = park.formatted_location location_text = f" • {location}" if location else "" return { 'key': str(park.pk), 'label': park.name, 'extra': f"{park.get_status_display()}{location_text}" } 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