mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 11:31:07 -05:00
313 lines
12 KiB
Python
313 lines
12 KiB
Python
from django import forms
|
|
from decimal import Decimal, InvalidOperation, ROUND_DOWN
|
|
from autocomplete import AutocompleteWidget
|
|
|
|
from core.forms import BaseAutocomplete
|
|
from .models import Park
|
|
from .models.location import ParkLocation
|
|
from .querysets import get_base_park_queryset
|
|
|
|
|
|
class ParkAutocomplete(BaseAutocomplete):
|
|
"""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
|