feat: Implement initial schema and add various API, service, and management command enhancements across the application.

This commit is contained in:
pacnpal
2026-01-01 15:13:01 -05:00
parent c95f99ca10
commit b243b17af7
413 changed files with 11164 additions and 17433 deletions

View File

@@ -162,16 +162,13 @@ class MapLocationsAPIView(APIView):
if not all([north, south, east, west]):
return None
try:
return Polygon.from_bbox(
(float(west), float(south), float(east), float(north))
)
return Polygon.from_bbox((float(west), float(south), float(east), float(north)))
except (ValueError, TypeError):
return None
def _serialize_park_location(self, park) -> dict:
"""Serialize park location data."""
location = park.location if hasattr(
park, "location") and park.location else None
location = park.location if hasattr(park, "location") and park.location else None
return {
"city": location.city if location else "",
"state": location.state if location else "",
@@ -181,8 +178,7 @@ class MapLocationsAPIView(APIView):
def _serialize_park_data(self, park) -> dict:
"""Serialize park data for map response."""
location = park.location if hasattr(
park, "location") and park.location else None
location = park.location if hasattr(park, "location") and park.location else None
return {
"id": park.id,
"type": "park",
@@ -195,9 +191,7 @@ class MapLocationsAPIView(APIView):
"stats": {
"coaster_count": park.coaster_count or 0,
"ride_count": park.ride_count or 0,
"average_rating": (
float(park.average_rating) if park.average_rating else None
),
"average_rating": (float(park.average_rating) if park.average_rating else None),
},
}
@@ -206,14 +200,10 @@ class MapLocationsAPIView(APIView):
if "park" not in params["types"]:
return []
parks_query = Park.objects.select_related(
"location", "operator"
).filter(location__point__isnull=False)
parks_query = Park.objects.select_related("location", "operator").filter(location__point__isnull=False)
# Apply bounds filtering
bounds_polygon = self._create_bounds_polygon(
params["north"], params["south"], params["east"], params["west"]
)
bounds_polygon = self._create_bounds_polygon(params["north"], params["south"], params["east"], params["west"])
if bounds_polygon:
parks_query = parks_query.filter(location__point__within=bounds_polygon)
@@ -229,11 +219,7 @@ class MapLocationsAPIView(APIView):
def _serialize_ride_location(self, ride) -> dict:
"""Serialize ride location data."""
location = (
ride.park.location
if hasattr(ride.park, "location") and ride.park.location
else None
)
location = ride.park.location if hasattr(ride.park, "location") and ride.park.location else None
return {
"city": location.city if location else "",
"state": location.state if location else "",
@@ -243,11 +229,7 @@ class MapLocationsAPIView(APIView):
def _serialize_ride_data(self, ride) -> dict:
"""Serialize ride data for map response."""
location = (
ride.park.location
if hasattr(ride.park, "location") and ride.park.location
else None
)
location = ride.park.location if hasattr(ride.park, "location") and ride.park.location else None
return {
"id": ride.id,
"type": "ride",
@@ -259,9 +241,7 @@ class MapLocationsAPIView(APIView):
"location": self._serialize_ride_location(ride),
"stats": {
"category": ride.get_category_display() if ride.category else None,
"average_rating": (
float(ride.average_rating) if ride.average_rating else None
),
"average_rating": (float(ride.average_rating) if ride.average_rating else None),
"park_name": ride.park.name,
},
}
@@ -271,17 +251,14 @@ class MapLocationsAPIView(APIView):
if "ride" not in params["types"]:
return []
rides_query = Ride.objects.select_related(
"park__location", "manufacturer"
).filter(park__location__point__isnull=False)
rides_query = Ride.objects.select_related("park__location", "manufacturer").filter(
park__location__point__isnull=False
)
# Apply bounds filtering
bounds_polygon = self._create_bounds_polygon(
params["north"], params["south"], params["east"], params["west"]
)
bounds_polygon = self._create_bounds_polygon(params["north"], params["south"], params["east"], params["west"])
if bounds_polygon:
rides_query = rides_query.filter(
park__location__point__within=bounds_polygon)
rides_query = rides_query.filter(park__location__point__within=bounds_polygon)
# Apply text search
if params["query"]:
@@ -335,7 +312,7 @@ class MapLocationsAPIView(APIView):
# Use EnhancedCacheService for improved caching with monitoring
cache_service = EnhancedCacheService()
cached_result = cache_service.get_cached_api_response('map_locations', params)
cached_result = cache_service.get_cached_api_response("map_locations", params)
if cached_result:
logger.debug(f"Cache hit for map_locations with key: {cache_key}")
return Response(cached_result)
@@ -349,7 +326,7 @@ class MapLocationsAPIView(APIView):
result = self._build_response(locations, params)
# Cache result for 5 minutes using EnhancedCacheService
cache_service.cache_api_response('map_locations', params, result, timeout=300)
cache_service.cache_api_response("map_locations", params, result, timeout=300)
logger.debug(f"Cached map_locations result for key: {cache_key}")
return Response(result)
@@ -357,7 +334,7 @@ class MapLocationsAPIView(APIView):
except Exception as e:
logger.error(f"Error in MapLocationsAPIView: {str(e)}", exc_info=True)
return Response(
{"status": "error", "message": "Failed to retrieve map locations"},
{"status": "error", "detail": "Failed to retrieve map locations"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@@ -401,34 +378,28 @@ class MapLocationDetailAPIView(APIView):
permission_classes = [AllowAny]
@cache_api_response(timeout=1800, key_prefix="map_detail")
def get(
self, request: HttpRequest, location_type: str, location_id: int
) -> Response:
def get(self, request: HttpRequest, location_type: str, location_id: int) -> Response:
"""Get detailed information for a specific location."""
try:
if location_type == "park":
try:
obj = Park.objects.select_related("location", "operator").get(
id=location_id
)
obj = Park.objects.select_related("location", "operator").get(id=location_id)
except Park.DoesNotExist:
return Response(
{"status": "error", "message": "Park not found"},
{"status": "error", "detail": "Park not found"},
status=status.HTTP_404_NOT_FOUND,
)
elif location_type == "ride":
try:
obj = Ride.objects.select_related(
"park__location", "manufacturer"
).get(id=location_id)
obj = Ride.objects.select_related("park__location", "manufacturer").get(id=location_id)
except Ride.DoesNotExist:
return Response(
{"status": "error", "message": "Ride not found"},
{"status": "error", "detail": "Ride not found"},
status=status.HTTP_404_NOT_FOUND,
)
else:
return Response(
{"status": "error", "message": "Invalid location type"},
{"status": "error", "detail": "Invalid location type"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -440,59 +411,27 @@ class MapLocationDetailAPIView(APIView):
"name": obj.name,
"slug": obj.slug,
"description": obj.description,
"latitude": (
obj.location.latitude
if hasattr(obj, "location") and obj.location
else None
),
"longitude": (
obj.location.longitude
if hasattr(obj, "location") and obj.location
else None
),
"latitude": (obj.location.latitude if hasattr(obj, "location") and obj.location else None),
"longitude": (obj.location.longitude if hasattr(obj, "location") and obj.location else None),
"status": obj.status,
"location": {
"street_address": (
obj.location.street_address
if hasattr(obj, "location") and obj.location
else ""
),
"city": (
obj.location.city
if hasattr(obj, "location") and obj.location
else ""
),
"state": (
obj.location.state
if hasattr(obj, "location") and obj.location
else ""
),
"country": (
obj.location.country
if hasattr(obj, "location") and obj.location
else ""
),
"postal_code": (
obj.location.postal_code
if hasattr(obj, "location") and obj.location
else ""
obj.location.street_address if hasattr(obj, "location") and obj.location else ""
),
"city": (obj.location.city if hasattr(obj, "location") and obj.location else ""),
"state": (obj.location.state if hasattr(obj, "location") and obj.location else ""),
"country": (obj.location.country if hasattr(obj, "location") and obj.location else ""),
"postal_code": (obj.location.postal_code if hasattr(obj, "location") and obj.location else ""),
"formatted_address": (
obj.location.formatted_address
if hasattr(obj, "location") and obj.location
else ""
obj.location.formatted_address if hasattr(obj, "location") and obj.location else ""
),
},
"stats": {
"coaster_count": obj.coaster_count or 0,
"ride_count": obj.ride_count or 0,
"average_rating": (
float(obj.average_rating) if obj.average_rating else None
),
"average_rating": (float(obj.average_rating) if obj.average_rating else None),
"size_acres": float(obj.size_acres) if obj.size_acres else None,
"opening_date": (
obj.opening_date.isoformat() if obj.opening_date else None
),
"opening_date": (obj.opening_date.isoformat() if obj.opening_date else None),
},
"nearby_locations": [], # See FUTURE_WORK.md - THRILLWIKI-107
}
@@ -504,14 +443,10 @@ class MapLocationDetailAPIView(APIView):
"slug": obj.slug,
"description": obj.description,
"latitude": (
obj.park.location.latitude
if hasattr(obj.park, "location") and obj.park.location
else None
obj.park.location.latitude if hasattr(obj.park, "location") and obj.park.location else None
),
"longitude": (
obj.park.location.longitude
if hasattr(obj.park, "location") and obj.park.location
else None
obj.park.location.longitude if hasattr(obj.park, "location") and obj.park.location else None
),
"status": obj.status,
"location": {
@@ -520,25 +455,15 @@ class MapLocationDetailAPIView(APIView):
if hasattr(obj.park, "location") and obj.park.location
else ""
),
"city": (
obj.park.location.city
if hasattr(obj.park, "location") and obj.park.location
else ""
),
"city": (obj.park.location.city if hasattr(obj.park, "location") and obj.park.location else ""),
"state": (
obj.park.location.state
if hasattr(obj.park, "location") and obj.park.location
else ""
obj.park.location.state if hasattr(obj.park, "location") and obj.park.location else ""
),
"country": (
obj.park.location.country
if hasattr(obj.park, "location") and obj.park.location
else ""
obj.park.location.country if hasattr(obj.park, "location") and obj.park.location else ""
),
"postal_code": (
obj.park.location.postal_code
if hasattr(obj.park, "location") and obj.park.location
else ""
obj.park.location.postal_code if hasattr(obj.park, "location") and obj.park.location else ""
),
"formatted_address": (
obj.park.location.formatted_address
@@ -547,19 +472,11 @@ class MapLocationDetailAPIView(APIView):
),
},
"stats": {
"category": (
obj.get_category_display() if obj.category else None
),
"average_rating": (
float(obj.average_rating) if obj.average_rating else None
),
"category": (obj.get_category_display() if obj.category else None),
"average_rating": (float(obj.average_rating) if obj.average_rating else None),
"park_name": obj.park.name,
"opening_date": (
obj.opening_date.isoformat() if obj.opening_date else None
),
"manufacturer": (
obj.manufacturer.name if obj.manufacturer else None
),
"opening_date": (obj.opening_date.isoformat() if obj.opening_date else None),
"manufacturer": (obj.manufacturer.name if obj.manufacturer else None),
},
"nearby_locations": [], # See FUTURE_WORK.md - THRILLWIKI-107
}
@@ -574,7 +491,7 @@ class MapLocationDetailAPIView(APIView):
except Exception as e:
logger.error(f"Error in MapLocationDetailAPIView: {str(e)}", exc_info=True)
return Response(
{"status": "error", "message": "Failed to retrieve location details"},
{"status": "error", "detail": "Failed to retrieve location details"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@@ -640,7 +557,7 @@ class MapSearchAPIView(APIView):
return Response(
{
"status": "error",
"message": "Search query 'q' parameter is required",
"detail": "Search query 'q' parameter is required",
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -672,30 +589,16 @@ class MapSearchAPIView(APIView):
"name": park.name,
"slug": park.slug,
"latitude": (
park.location.latitude
if hasattr(park, "location") and park.location
else None
park.location.latitude if hasattr(park, "location") and park.location else None
),
"longitude": (
park.location.longitude
if hasattr(park, "location") and park.location
else None
park.location.longitude if hasattr(park, "location") and park.location else None
),
"location": {
"city": (
park.location.city
if hasattr(park, "location") and park.location
else ""
),
"state": (
park.location.state
if hasattr(park, "location") and park.location
else ""
),
"city": (park.location.city if hasattr(park, "location") and park.location else ""),
"state": (park.location.state if hasattr(park, "location") and park.location else ""),
"country": (
park.location.country
if hasattr(park, "location") and park.location
else ""
park.location.country if hasattr(park, "location") and park.location else ""
),
},
"relevance_score": 1.0, # See FUTURE_WORK.md - THRILLWIKI-108
@@ -734,20 +637,17 @@ class MapSearchAPIView(APIView):
"location": {
"city": (
ride.park.location.city
if hasattr(ride.park, "location")
and ride.park.location
if hasattr(ride.park, "location") and ride.park.location
else ""
),
"state": (
ride.park.location.state
if hasattr(ride.park, "location")
and ride.park.location
if hasattr(ride.park, "location") and ride.park.location
else ""
),
"country": (
ride.park.location.country
if hasattr(ride.park, "location")
and ride.park.location
if hasattr(ride.park, "location") and ride.park.location
else ""
),
},
@@ -776,7 +676,7 @@ class MapSearchAPIView(APIView):
except Exception as e:
logger.error(f"Error in MapSearchAPIView: {str(e)}", exc_info=True)
return Response(
{"status": "error", "message": "Search failed due to internal error"},
{"status": "error", "detail": "Search failed due to internal error"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@@ -848,8 +748,7 @@ class MapBoundsAPIView(APIView):
if not all([north_str, south_str, east_str, west_str]):
return Response(
{"status": "error",
"message": "All bounds parameters (north, south, east, west) are required"},
{"status": "error", "detail": "All bounds parameters (north, south, east, west) are required"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -860,7 +759,7 @@ class MapBoundsAPIView(APIView):
west = float(west_str) if west_str else 0.0
except (TypeError, ValueError):
return Response(
{"status": "error", "message": "Invalid bounds parameters"},
{"status": "error", "detail": "Invalid bounds parameters"},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -869,7 +768,7 @@ class MapBoundsAPIView(APIView):
return Response(
{
"status": "error",
"message": "North bound must be greater than south bound",
"detail": "North bound must be greater than south bound",
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -878,7 +777,7 @@ class MapBoundsAPIView(APIView):
return Response(
{
"status": "error",
"message": "West bound must be less than east bound",
"detail": "West bound must be less than east bound",
},
status=status.HTTP_400_BAD_REQUEST,
)
@@ -891,9 +790,7 @@ class MapBoundsAPIView(APIView):
# Get parks within bounds
if "park" in types:
parks_query = Park.objects.select_related("location").filter(
location__point__within=bounds_polygon
)
parks_query = Park.objects.select_related("location").filter(location__point__within=bounds_polygon)
for park in parks_query[:100]: # Limit results
locations.append(
@@ -903,14 +800,10 @@ class MapBoundsAPIView(APIView):
"name": park.name,
"slug": park.slug,
"latitude": (
park.location.latitude
if hasattr(park, "location") and park.location
else None
park.location.latitude if hasattr(park, "location") and park.location else None
),
"longitude": (
park.location.longitude
if hasattr(park, "location") and park.location
else None
park.location.longitude if hasattr(park, "location") and park.location else None
),
"status": park.status,
}
@@ -960,7 +853,7 @@ class MapBoundsAPIView(APIView):
except Exception as e:
logger.error(f"Error in MapBoundsAPIView: {str(e)}", exc_info=True)
return Response(
{"status": "error", "message": "Failed to retrieve locations within bounds"},
{"status": "error", "detail": "Failed to retrieve locations within bounds"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@@ -987,18 +880,15 @@ class MapStatsAPIView(APIView):
"""Get map service statistics and performance metrics."""
try:
# Count locations with coordinates
parks_with_location = Park.objects.filter(
location__point__isnull=False
).count()
rides_with_location = Ride.objects.filter(
park__location__point__isnull=False
).count()
parks_with_location = Park.objects.filter(location__point__isnull=False).count()
rides_with_location = Ride.objects.filter(park__location__point__isnull=False).count()
total_locations = parks_with_location + rides_with_location
# Get cache statistics
from apps.core.services.enhanced_cache_service import CacheMonitor
cache_monitor = CacheMonitor()
cache_stats = cache_monitor.get_cache_statistics('map_locations')
cache_stats = cache_monitor.get_cache_statistics("map_locations")
return Response(
{
@@ -1006,17 +896,17 @@ class MapStatsAPIView(APIView):
"total_locations": total_locations,
"parks_with_location": parks_with_location,
"rides_with_location": rides_with_location,
"cache_hits": cache_stats.get('hits', 0),
"cache_misses": cache_stats.get('misses', 0),
"cache_hit_rate": cache_stats.get('hit_rate', 0.0),
"cache_size": cache_stats.get('size', 0),
"cache_hits": cache_stats.get("hits", 0),
"cache_misses": cache_stats.get("misses", 0),
"cache_hit_rate": cache_stats.get("hit_rate", 0.0),
"cache_size": cache_stats.get("size", 0),
}
)
except Exception as e:
logger.error(f"Error in MapStatsAPIView: {str(e)}", exc_info=True)
return Response(
{"status": "error", "message": "Failed to retrieve map statistics"},
{"status": "error", "detail": "Failed to retrieve map statistics"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@@ -1060,7 +950,7 @@ class MapCacheAPIView(APIView):
return Response(
{
"status": "success",
"message": f"Map cache cleared successfully. Cleared {cleared_count} entries.",
"detail": f"Map cache cleared successfully. Cleared {cleared_count} entries.",
"cleared_count": cleared_count,
}
)
@@ -1068,7 +958,7 @@ class MapCacheAPIView(APIView):
except Exception as e:
logger.error(f"Error in MapCacheAPIView.delete: {str(e)}", exc_info=True)
return Response(
{"status": "error", "message": "Failed to clear map cache"},
{"status": "error", "detail": "Failed to clear map cache"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@@ -1076,7 +966,7 @@ class MapCacheAPIView(APIView):
"""Invalidate specific cache entries."""
try:
# Get cache keys to invalidate from request data
request_data = getattr(request, 'data', {})
request_data = getattr(request, "data", {})
cache_keys = request_data.get("cache_keys", []) if request_data else []
if cache_keys:
@@ -1088,7 +978,7 @@ class MapCacheAPIView(APIView):
return Response(
{
"status": "success",
"message": f"Cache invalidated successfully. Invalidated {invalidated_count} entries.",
"detail": f"Cache invalidated successfully. Invalidated {invalidated_count} entries.",
"invalidated_count": invalidated_count,
}
)
@@ -1096,7 +986,7 @@ class MapCacheAPIView(APIView):
except Exception as e:
logger.error(f"Error in MapCacheAPIView.post: {str(e)}", exc_info=True)
return Response(
{"status": "error", "message": "Failed to invalidate cache"},
{"status": "error", "detail": "Failed to invalidate cache"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)