Files
thrillwiki_django_no_react/parks/forms.py
pacnpal b5bae44cb8 Add Road Trip Planner template with interactive map and trip management features
- Implemented a new HTML template for the Road Trip Planner.
- Integrated Leaflet.js for interactive mapping and routing.
- Added functionality for searching and selecting parks to include in a trip.
- Enabled drag-and-drop reordering of selected parks.
- Included trip optimization and route calculation features.
- Created a summary display for trip statistics.
- Added functionality to save trips and manage saved trips.
- Enhanced UI with responsive design and dark mode support.
2025-08-15 20:53:00 -04:00

313 lines
12 KiB
Python

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