Files
thrillwiki_django_no_react/backend/apps/parks/views_roadtrip.py
pacnpal e4e36c7899 Add migrations for ParkPhoto and RidePhoto models with associated events
- Created ParkPhoto and ParkPhotoEvent models in the parks app, including fields for image, caption, alt text, and relationships to the Park model.
- Implemented triggers for insert and update operations on ParkPhoto to log changes in ParkPhotoEvent.
- Created RidePhoto and RidePhotoEvent models in the rides app, with similar structure and functionality as ParkPhoto.
- Added fields for photo type in RidePhoto and implemented corresponding triggers for logging changes.
- Established necessary indexes and unique constraints for both models to ensure data integrity and optimize queries.
2025-08-26 14:40:46 -04:00

481 lines
16 KiB
Python

"""
Road trip planning views for theme parks.
Provides interfaces for creating and managing multi-park road trips.
"""
import json
from typing import Dict, Any, List
from django.shortcuts import render
from django.http import JsonResponse, HttpRequest, HttpResponse
from django.views.generic import TemplateView, View
from django.urls import reverse
from .models import Park
from .services.roadtrip import RoadTripService
from apps.core.services.map_service import unified_map_service
from apps.core.services.data_structures import LocationType
JSON_DECODE_ERROR_MSG = "Invalid JSON data"
PARKS_ALONG_ROUTE_HTML = "parks/partials/parks_along_route.html"
class RoadTripViewMixin:
"""Mixin providing common functionality for road trip views."""
def __init__(self):
super().__init__()
self.roadtrip_service = RoadTripService()
def get_roadtrip_context(self) -> Dict[str, Any]:
"""Get common context data for road trip views."""
return {
"roadtrip_api_urls": {
"create_trip": "/roadtrip/create/",
"find_parks_along_route": "/roadtrip/htmx/parks-along-route/",
"geocode": "/roadtrip/htmx/geocode/",
},
"max_parks_per_trip": 10,
"default_detour_km": 50,
"enable_osm_integration": True,
}
class RoadTripPlannerView(RoadTripViewMixin, TemplateView):
"""
Main road trip planning interface.
URL: /roadtrip/
"""
template_name = "parks/roadtrip_planner.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(self.get_roadtrip_context(self.request))
# Get popular parks for suggestions
popular_parks = (
Park.objects.filter(status="OPERATING", location__isnull=False)
.select_related("location", "operator")
.order_by("-ride_count")[:20]
)
context.update(
{
"page_title": "Road Trip Planner",
"popular_parks": popular_parks,
"countries_with_parks": self._get_countries_with_parks(),
"enable_route_optimization": True,
"show_distance_estimates": True,
}
)
return context
def _get_countries_with_parks(self) -> List[str]:
"""Get list of countries that have theme parks."""
countries = (
Park.objects.filter(status="OPERATING", location__country__isnull=False)
.values_list("location__country", flat=True)
.distinct()
.order_by("location__country")
)
return list(countries)
class CreateTripView(RoadTripViewMixin, View):
"""
Generate optimized road trip routes.
URL: /roadtrip/create/
"""
def post(self, request: HttpRequest) -> HttpResponse:
"""Create a new road trip with optimized routing."""
try:
data = json.loads(request.body)
# Parse park IDs
park_ids = data.get("park_ids", [])
if not park_ids or len(park_ids) < 2:
return JsonResponse(
{
"status": "error",
"message": "At least 2 parks are required for a road trip",
},
status=400,
)
if len(park_ids) > 10:
return JsonResponse(
{
"status": "error",
"message": "Maximum 10 parks allowed per trip",
},
status=400,
)
# Get parks
parks = list(
Park.objects.filter(
id__in=park_ids, location__isnull=False
).select_related("location", "operator")
)
if len(parks) != len(park_ids):
return JsonResponse(
{
"status": "error",
"message": "Some parks could not be found or do not have location data",
},
status=400,
)
# Create optimized trip
trip = self.roadtrip_service.create_multi_park_trip(parks)
if not trip:
return JsonResponse(
{
"status": "error",
"message": "Could not create optimized route for the selected parks",
},
status=400,
)
# Convert trip to dict for JSON response
trip_data = {
"parks": [self._park_to_dict(park) for park in trip.parks],
"legs": [self._leg_to_dict(leg) for leg in trip.legs],
"total_distance_km": trip.total_distance_km,
"total_duration_minutes": trip.total_duration_minutes,
"formatted_total_distance": trip.formatted_total_distance,
"formatted_total_duration": trip.formatted_total_duration,
}
return JsonResponse(
{
"status": "success",
"data": trip_data,
"trip_url": reverse(
"parks:roadtrip_detail", kwargs={"trip_id": "temp"}
),
}
)
except json.JSONDecodeError:
return JsonResponse(
{"status": "error", "message": JSON_DECODE_ERROR_MSG},
status=400,
)
except Exception as e:
return JsonResponse(
{
"status": "error",
"message": f"Failed to create trip: {str(e)}",
},
status=500,
)
def _park_to_dict(self, park: Park) -> Dict[str, Any]:
"""Convert park instance to dictionary."""
return {
"id": park.id,
"name": park.name,
"slug": park.slug,
"formatted_location": getattr(park, "formatted_location", ""),
"coordinates": park.coordinates,
"operator": park.operator.name if park.operator else None,
"ride_count": getattr(park, "ride_count", 0),
"url": reverse("parks:park_detail", kwargs={"slug": park.slug}),
}
def _leg_to_dict(self, leg) -> Dict[str, Any]:
"""Convert trip leg to dictionary."""
return {
"from_park": self._park_to_dict(leg.from_park),
"to_park": self._park_to_dict(leg.to_park),
"distance_km": leg.route.distance_km,
"duration_minutes": leg.route.duration_minutes,
"formatted_distance": leg.route.formatted_distance,
"formatted_duration": leg.route.formatted_duration,
"geometry": leg.route.geometry,
}
class TripDetailView(RoadTripViewMixin, TemplateView):
"""
Show trip details and map.
URL: /roadtrip/<trip_id>/
"""
template_name = "parks/trip_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update(self.get_roadtrip_context(self.request))
# For now, this is a placeholder since we don't persist trips
# In a full implementation, you would retrieve the trip from database
trip_id = kwargs.get("trip_id")
context.update(
{
"page_title": f"Road Trip #{trip_id}",
"trip_id": trip_id,
"message": "Trip details would be loaded here. Currently trips are not persisted.",
}
)
return context
class FindParksAlongRouteView(RoadTripViewMixin, View):
"""
HTMX endpoint for route-based park discovery.
URL: /roadtrip/htmx/parks-along-route/
"""
def post(self, request: HttpRequest) -> HttpResponse:
"""Find parks along a route between two points."""
try:
data = json.loads(request.body)
start_park_id = data.get("start_park_id")
end_park_id = data.get("end_park_id")
max_detour_km = min(100, max(10, float(data.get("max_detour_km", 50))))
if not start_park_id or not end_park_id:
return render(
request,
PARKS_ALONG_ROUTE_HTML,
{"error": "Start and end parks are required"},
)
# Get start and end parks
try:
start_park = Park.objects.select_related("location").get(
id=start_park_id, location__isnull=False
)
end_park = Park.objects.select_related("location").get(
id=end_park_id, location__isnull=False
)
except Park.DoesNotExist:
return render(
request,
PARKS_ALONG_ROUTE_HTML,
{"error": "One or both parks could not be found"},
)
# Find parks along route
parks_along_route = self.roadtrip_service.find_parks_along_route(
start_park, end_park, max_detour_km
)
return render(
request,
PARKS_ALONG_ROUTE_HTML,
{
"parks": parks_along_route,
"start_park": start_park,
"end_park": end_park,
"max_detour_km": max_detour_km,
"count": len(parks_along_route),
},
)
except json.JSONDecodeError:
return render(
request,
PARKS_ALONG_ROUTE_HTML,
{"error": JSON_DECODE_ERROR_MSG},
)
except Exception as e:
return render(request, PARKS_ALONG_ROUTE_HTML, {"error": str(e)})
class GeocodeAddressView(RoadTripViewMixin, View):
"""
HTMX endpoint for geocoding addresses.
URL: /roadtrip/htmx/geocode/
"""
def post(self, request: HttpRequest) -> HttpResponse:
"""Geocode an address and find nearby parks."""
try:
data = json.loads(request.body)
address = data.get("address", "").strip()
if not address:
return JsonResponse(
{"status": "error", "message": "Address is required"},
status=400,
)
# Geocode the address
coordinates = self.roadtrip_service.geocode_address(address)
if not coordinates:
return JsonResponse(
{
"status": "error",
"message": "Could not geocode the provided address",
},
status=400,
)
# Find nearby parks
radius_km = min(200, max(10, float(data.get("radius_km", 100))))
# Use map service to find parks near coordinates
from core.services.data_structures import GeoBounds
# Create a bounding box around the coordinates
lat_delta = radius_km / 111.0 # Rough conversion: 1 degree ≈ 111km
lng_delta = radius_km / (111.0 * abs(coordinates.latitude / 90.0))
bounds = GeoBounds(
north=coordinates.latitude + lat_delta,
south=coordinates.latitude - lat_delta,
east=coordinates.longitude + lng_delta,
west=coordinates.longitude - lng_delta,
)
map_response = unified_map_service.get_locations_by_bounds(
north=bounds.north,
south=bounds.south,
east=bounds.east,
west=bounds.west,
location_types={LocationType.PARK},
)
return JsonResponse(
{
"status": "success",
"data": {
"coordinates": {
"latitude": coordinates.latitude,
"longitude": coordinates.longitude,
},
"address": address,
"nearby_parks": [
loc.to_dict() for loc in map_response.locations[:20]
],
"radius_km": radius_km,
},
}
)
except json.JSONDecodeError:
return JsonResponse(
{"status": "error", "message": JSON_DECODE_ERROR_MSG},
status=400,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
class ParkDistanceCalculatorView(RoadTripViewMixin, View):
"""
HTMX endpoint for calculating distances between parks.
URL: /roadtrip/htmx/distance/
"""
def post(self, request: HttpRequest) -> HttpResponse:
"""Calculate distance and duration between two parks."""
try:
data = json.loads(request.body)
park1_id = data.get("park1_id")
park2_id = data.get("park2_id")
if not park1_id or not park2_id:
return JsonResponse(
{
"status": "error",
"message": "Both park IDs are required",
},
status=400,
)
# Get parks
try:
park1 = Park.objects.select_related("location").get(
id=park1_id, location__isnull=False
)
park2 = Park.objects.select_related("location").get(
id=park2_id, location__isnull=False
)
except Park.DoesNotExist:
return JsonResponse(
{
"status": "error",
"message": "One or both parks could not be found",
},
status=400,
)
# Calculate route
coords1 = park1.coordinates
coords2 = park2.coordinates
if not coords1 or not coords2:
return JsonResponse(
{
"status": "error",
"message": "One or both parks do not have coordinate data",
},
status=400,
)
from services.roadtrip import Coordinates
route = self.roadtrip_service.calculate_route(
Coordinates(*coords1), Coordinates(*coords2)
)
if not route:
return JsonResponse(
{
"status": "error",
"message": "Could not calculate route between parks",
},
status=400,
)
return JsonResponse(
{
"status": "success",
"data": {
"distance_km": route.distance_km,
"duration_minutes": route.duration_minutes,
"formatted_distance": route.formatted_distance,
"formatted_duration": route.formatted_duration,
"park1": {
"name": park1.name,
"formatted_location": getattr(
park1, "formatted_location", ""
),
},
"park2": {
"name": park2.name,
"formatted_location": getattr(
park2, "formatted_location", ""
),
},
},
}
)
except json.JSONDecodeError:
return JsonResponse(
{"status": "error", "message": JSON_DECODE_ERROR_MSG},
status=400,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)