Refactor test utilities and enhance ASGI settings

- Cleaned up and standardized assertions in ApiTestMixin for API response validation.
- Updated ASGI settings to use os.environ for setting the DJANGO_SETTINGS_MODULE.
- Removed unused imports and improved formatting in settings.py.
- Refactored URL patterns in urls.py for better readability and organization.
- Enhanced view functions in views.py for consistency and clarity.
- Added .flake8 configuration for linting and style enforcement.
- Introduced type stubs for django-environ to improve type checking with Pylance.
This commit is contained in:
pacnpal
2025-08-20 19:51:59 -04:00
parent 69c07d1381
commit 66ed4347a9
230 changed files with 15094 additions and 11578 deletions

View File

@@ -4,24 +4,19 @@ Provides interfaces for creating and managing multi-park road trips.
"""
import json
from typing import Dict, Any, List, Optional
from django.shortcuts import render, get_object_or_404, redirect
from django.http import JsonResponse, HttpRequest, HttpResponse, Http404
from django.views.generic import TemplateView, View, DetailView
from django.views.decorators.http import require_http_methods
from django.utils.decorators import method_decorator
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ValidationError
from django.contrib import messages
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 django.db.models import Q
from .models import Park
from .services.roadtrip import RoadTripService
from core.services.map_service import unified_map_service
from core.services.data_structures import LocationType, MapFilters
JSON_DECODE_ERROR_MSG = 'Invalid JSON data'
PARKS_ALONG_ROUTE_HTML = 'parks/partials/parks_along_route.html'
JSON_DECODE_ERROR_MSG = "Invalid JSON data"
PARKS_ALONG_ROUTE_HTML = "parks/partials/parks_along_route.html"
class RoadTripViewMixin:
@@ -34,14 +29,14 @@ class RoadTripViewMixin:
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/',
"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,
"max_parks_per_trip": 10,
"default_detour_km": 50,
"enable_osm_integration": True,
}
@@ -51,34 +46,40 @@ class RoadTripPlannerView(RoadTripViewMixin, TemplateView):
URL: /roadtrip/
"""
template_name = 'parks/roadtrip_planner.html'
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]
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,
})
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')
countries = (
Park.objects.filter(status="OPERATING", location__country__isnull=False)
.values_list("location__country", flat=True)
.distinct()
.order_by("location__country")
)
return list(countries)
@@ -95,90 +96,110 @@ class CreateTripView(RoadTripViewMixin, View):
data = json.loads(request.body)
# Parse park IDs
park_ids = data.get('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)
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)
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'))
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)
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)
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,
"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'})
})
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)
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)
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}),
"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,
"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,
}
@@ -188,7 +209,8 @@ class TripDetailView(RoadTripViewMixin, TemplateView):
URL: /roadtrip/<trip_id>/
"""
template_name = 'parks/trip_detail.html'
template_name = "parks/trip_detail.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -196,13 +218,15 @@ class TripDetailView(RoadTripViewMixin, TemplateView):
# 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')
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.',
})
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
@@ -219,50 +243,57 @@ class FindParksAlongRouteView(RoadTripViewMixin, View):
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))))
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'
})
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(
start_park = Park.objects.select_related("location").get(
id=start_park_id, location__isnull=False
)
end_park = Park.objects.select_related('location').get(
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'
})
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)
})
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
})
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)
})
return render(request, PARKS_ALONG_ROUTE_HTML, {"error": str(e)})
class GeocodeAddressView(RoadTripViewMixin, View):
@@ -276,25 +307,28 @@ class GeocodeAddressView(RoadTripViewMixin, View):
"""Geocode an address and find nearby parks."""
try:
data = json.loads(request.body)
address = data.get('address', '').strip()
address = data.get("address", "").strip()
if not address:
return JsonResponse({
'status': 'error',
'message': 'Address is required'
}, status=400)
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)
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))))
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
@@ -307,42 +341,41 @@ class GeocodeAddressView(RoadTripViewMixin, View):
north=coordinates.latitude + lat_delta,
south=coordinates.latitude - lat_delta,
east=coordinates.longitude + lng_delta,
west=coordinates.longitude - lng_delta
west=coordinates.longitude - lng_delta,
)
filters = MapFilters(location_types={LocationType.PARK})
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}
location_types={LocationType.PARK},
)
return JsonResponse({
'status': 'success',
'data': {
'coordinates': {
'latitude': coordinates.latitude,
'longitude': coordinates.longitude
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,
},
'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)
return JsonResponse(
{"status": "error", "message": JSON_DECODE_ERROR_MSG},
status=400,
)
except Exception as e:
return JsonResponse({
'status': 'error',
'message': str(e)
}, status=500)
return JsonResponse({"status": "error", "message": str(e)}, status=500)
class ParkDistanceCalculatorView(RoadTripViewMixin, View):
@@ -357,77 +390,91 @@ class ParkDistanceCalculatorView(RoadTripViewMixin, View):
try:
data = json.loads(request.body)
park1_id = data.get('park1_id')
park2_id = data.get('park2_id')
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)
return JsonResponse(
{
"status": "error",
"message": "Both park IDs are required",
},
status=400,
)
# Get parks
try:
park1 = Park.objects.select_related('location').get(
park1 = Park.objects.select_related("location").get(
id=park1_id, location__isnull=False
)
park2 = Park.objects.select_related('location').get(
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)
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)
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)
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', '')
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", ""
),
},
},
'park2': {
'name': park2.name,
'formatted_location': getattr(park2, 'formatted_location', '')
}
}
})
)
except json.JSONDecodeError:
return JsonResponse({
'status': 'error',
'message': JSON_DECODE_ERROR_MSG
}, status=400)
return JsonResponse(
{"status": "error", "message": JSON_DECODE_ERROR_MSG},
status=400,
)
except Exception as e:
return JsonResponse({
'status': 'error',
'message': str(e)
}, status=500)
return JsonResponse({"status": "error", "message": str(e)}, status=500)