mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 12:31:22 -05:00
- Added migration 0011 to populate unique slugs for existing RideModel records based on manufacturer and model names. - Implemented logic to ensure slug uniqueness during population. - Added reverse migration to clear slugs if needed. feat(rides): enforce unique slugs for RideModel - Created migration 0012 to alter the slug field in RideModel to be unique. - Updated the slug field to include help text and a maximum length of 255 characters. docs: integrate Cloudflare Images into rides and parks models - Updated RidePhoto and ParkPhoto models to use CloudflareImagesField for image storage. - Enhanced API serializers for rides and parks to support Cloudflare Images, including new fields for image URLs and variants. - Provided comprehensive OpenAPI schema metadata for new fields. - Documented database migrations for the integration. - Detailed configuration settings for Cloudflare Images. - Updated API response formats to include Cloudflare Images URLs and variants. - Added examples for uploading photos via API and outlined testing procedures.
810 lines
34 KiB
Python
810 lines
34 KiB
Python
"""
|
|
Centralized map API views.
|
|
Migrated from apps.core.views.map_views
|
|
"""
|
|
|
|
import logging
|
|
from typing import Dict, List, Any, Optional
|
|
|
|
from django.http import HttpRequest
|
|
from django.db.models import Q
|
|
from django.core.cache import cache
|
|
from django.contrib.gis.geos import Polygon
|
|
from django.contrib.gis.db.models.functions import Distance
|
|
from django.contrib.gis.geos import Point
|
|
from rest_framework.views import APIView
|
|
from rest_framework.response import Response
|
|
from rest_framework import status
|
|
from rest_framework.permissions import AllowAny
|
|
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiExample
|
|
from drf_spectacular.types import OpenApiTypes
|
|
|
|
from apps.parks.models import Park, ParkLocation
|
|
from apps.rides.models import Ride
|
|
from ..serializers.maps import (
|
|
MapLocationSerializer,
|
|
MapLocationsResponseSerializer,
|
|
MapSearchResultSerializer,
|
|
MapSearchResponseSerializer,
|
|
MapLocationDetailSerializer,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@extend_schema_view(
|
|
get=extend_schema(
|
|
summary="Get map locations",
|
|
description="Get map locations with optional clustering and filtering.",
|
|
parameters=[
|
|
OpenApiParameter(
|
|
"north",
|
|
type=OpenApiTypes.NUMBER,
|
|
location=OpenApiParameter.QUERY,
|
|
required=False,
|
|
description="Northern latitude bound (-90 to 90). Used with south, east, west to define geographic bounds.",
|
|
examples=[OpenApiExample("Example", value=41.5)],
|
|
),
|
|
OpenApiParameter(
|
|
"south",
|
|
type=OpenApiTypes.NUMBER,
|
|
location=OpenApiParameter.QUERY,
|
|
required=False,
|
|
description="Southern latitude bound (-90 to 90). Must be less than north bound.",
|
|
examples=[OpenApiExample("Example", value=41.4)],
|
|
),
|
|
OpenApiParameter(
|
|
"east",
|
|
type=OpenApiTypes.NUMBER,
|
|
location=OpenApiParameter.QUERY,
|
|
required=False,
|
|
description="Eastern longitude bound (-180 to 180). Must be greater than west bound.",
|
|
examples=[OpenApiExample("Example", value=-82.6)],
|
|
),
|
|
OpenApiParameter(
|
|
"west",
|
|
type=OpenApiTypes.NUMBER,
|
|
location=OpenApiParameter.QUERY,
|
|
required=False,
|
|
description="Western longitude bound (-180 to 180). Used with other bounds for geographic filtering.",
|
|
examples=[OpenApiExample("Example", value=-82.8)],
|
|
),
|
|
OpenApiParameter(
|
|
"zoom",
|
|
type=OpenApiTypes.INT,
|
|
location=OpenApiParameter.QUERY,
|
|
required=False,
|
|
description="Map zoom level (1-20). Higher values show more detail. Used for clustering decisions.",
|
|
examples=[OpenApiExample("Example", value=10)],
|
|
),
|
|
OpenApiParameter(
|
|
"types",
|
|
type=OpenApiTypes.STR,
|
|
location=OpenApiParameter.QUERY,
|
|
required=False,
|
|
description="Comma-separated location types to include. Valid values: 'park', 'ride'. Default: 'park,ride'",
|
|
examples=[
|
|
OpenApiExample("All types", value="park,ride"),
|
|
OpenApiExample("Parks only", value="park"),
|
|
OpenApiExample("Rides only", value="ride")
|
|
],
|
|
),
|
|
OpenApiParameter(
|
|
"cluster",
|
|
type=OpenApiTypes.BOOL,
|
|
location=OpenApiParameter.QUERY,
|
|
required=False,
|
|
description="Enable location clustering for high-density areas. Default: false",
|
|
examples=[
|
|
OpenApiExample("Enable clustering", value=True),
|
|
OpenApiExample("Disable clustering", value=False)
|
|
],
|
|
),
|
|
OpenApiParameter(
|
|
"q",
|
|
type=OpenApiTypes.STR,
|
|
location=OpenApiParameter.QUERY,
|
|
required=False,
|
|
description="Text search query. Searches park/ride names, cities, and states.",
|
|
examples=[
|
|
OpenApiExample("Park name", value="Cedar Point"),
|
|
OpenApiExample("Ride type", value="roller coaster"),
|
|
OpenApiExample("Location", value="Ohio")
|
|
],
|
|
),
|
|
],
|
|
responses={
|
|
200: MapLocationsResponseSerializer,
|
|
400: OpenApiTypes.OBJECT,
|
|
500: OpenApiTypes.OBJECT,
|
|
},
|
|
tags=["Maps"],
|
|
),
|
|
)
|
|
class MapLocationsAPIView(APIView):
|
|
"""API endpoint for getting map locations with optional clustering."""
|
|
|
|
permission_classes = [AllowAny]
|
|
|
|
def get(self, request: HttpRequest) -> Response:
|
|
"""Get map locations with optional clustering and filtering."""
|
|
try:
|
|
# Parse query parameters
|
|
north = request.GET.get("north")
|
|
south = request.GET.get("south")
|
|
east = request.GET.get("east")
|
|
west = request.GET.get("west")
|
|
zoom = request.GET.get("zoom", 10)
|
|
types = request.GET.get("types", "park,ride").split(",")
|
|
cluster = request.GET.get("cluster", "false").lower() == "true"
|
|
query = request.GET.get("q", "").strip()
|
|
|
|
# Build cache key
|
|
cache_key = f"map_locations_{north}_{south}_{east}_{west}_{zoom}_{','.join(types)}_{cluster}_{query}"
|
|
cached_result = cache.get(cache_key)
|
|
if cached_result:
|
|
return Response(cached_result)
|
|
|
|
locations = []
|
|
total_count = 0
|
|
|
|
# Get parks if requested
|
|
if "park" in types:
|
|
parks_query = Park.objects.select_related("location", "operator").filter(
|
|
location__point__isnull=False
|
|
)
|
|
|
|
# Apply bounds filtering
|
|
if all([north, south, east, west]):
|
|
try:
|
|
bounds_polygon = Polygon.from_bbox((
|
|
float(west), float(south), float(east), float(north)
|
|
))
|
|
parks_query = parks_query.filter(
|
|
location__point__within=bounds_polygon)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# Apply text search
|
|
if query:
|
|
parks_query = parks_query.filter(
|
|
Q(name__icontains=query) |
|
|
Q(location__city__icontains=query) |
|
|
Q(location__state__icontains=query)
|
|
)
|
|
|
|
# Serialize parks
|
|
for park in parks_query[:100]: # Limit results
|
|
park_data = {
|
|
"id": park.id,
|
|
"type": "park",
|
|
"name": park.name,
|
|
"slug": park.slug,
|
|
"latitude": 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,
|
|
"status": park.status,
|
|
"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 "",
|
|
"country": park.location.country if hasattr(park, 'location') and park.location else "",
|
|
"formatted_address": park.location.formatted_address if hasattr(park, 'location') and park.location else "",
|
|
},
|
|
"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,
|
|
},
|
|
}
|
|
locations.append(park_data)
|
|
|
|
# Get rides if requested
|
|
if "ride" in types:
|
|
rides_query = Ride.objects.select_related("park__location", "manufacturer").filter(
|
|
park__location__point__isnull=False
|
|
)
|
|
|
|
# Apply bounds filtering
|
|
if all([north, south, east, west]):
|
|
try:
|
|
bounds_polygon = Polygon.from_bbox((
|
|
float(west), float(south), float(east), float(north)
|
|
))
|
|
rides_query = rides_query.filter(
|
|
park__location__point__within=bounds_polygon)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# Apply text search
|
|
if query:
|
|
rides_query = rides_query.filter(
|
|
Q(name__icontains=query) |
|
|
Q(park__name__icontains=query) |
|
|
Q(park__location__city__icontains=query)
|
|
)
|
|
|
|
# Serialize rides
|
|
for ride in rides_query[:100]: # Limit results
|
|
ride_data = {
|
|
"id": ride.id,
|
|
"type": "ride",
|
|
"name": ride.name,
|
|
"slug": ride.slug,
|
|
"latitude": ride.park.location.latitude if hasattr(ride.park, 'location') and ride.park.location else None,
|
|
"longitude": ride.park.location.longitude if hasattr(ride.park, 'location') and ride.park.location else None,
|
|
"status": ride.status,
|
|
"location": {
|
|
"city": ride.park.location.city if hasattr(ride.park, 'location') and ride.park.location else "",
|
|
"state": ride.park.location.state if hasattr(ride.park, 'location') and ride.park.location else "",
|
|
"country": ride.park.location.country if hasattr(ride.park, 'location') and ride.park.location else "",
|
|
"formatted_address": ride.park.location.formatted_address if hasattr(ride.park, 'location') and ride.park.location else "",
|
|
},
|
|
"stats": {
|
|
"category": ride.get_category_display() if ride.category else None,
|
|
"average_rating": float(ride.average_rating) if ride.average_rating else None,
|
|
"park_name": ride.park.name,
|
|
},
|
|
}
|
|
locations.append(ride_data)
|
|
|
|
total_count = len(locations)
|
|
|
|
# Calculate bounds from results
|
|
bounds = {}
|
|
if locations:
|
|
lats = [loc["latitude"] for loc in locations if loc["latitude"]]
|
|
lngs = [loc["longitude"] for loc in locations if loc["longitude"]]
|
|
if lats and lngs:
|
|
bounds = {
|
|
"north": max(lats),
|
|
"south": min(lats),
|
|
"east": max(lngs),
|
|
"west": min(lngs),
|
|
}
|
|
|
|
result = {
|
|
"status": "success",
|
|
"locations": locations,
|
|
"clusters": [], # TODO: Implement clustering
|
|
"bounds": bounds,
|
|
"total_count": total_count,
|
|
"clustered": cluster,
|
|
}
|
|
|
|
# Cache result for 5 minutes
|
|
cache.set(cache_key, result, 300)
|
|
|
|
return Response(result)
|
|
|
|
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=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
)
|
|
|
|
|
|
@extend_schema_view(
|
|
get=extend_schema(
|
|
summary="Get location details",
|
|
description="Get detailed information about a specific location.",
|
|
parameters=[
|
|
OpenApiParameter(
|
|
"location_type",
|
|
type=OpenApiTypes.STR,
|
|
location=OpenApiParameter.PATH,
|
|
required=True,
|
|
description="Type of location",
|
|
),
|
|
OpenApiParameter(
|
|
"location_id",
|
|
type=OpenApiTypes.INT,
|
|
location=OpenApiParameter.PATH,
|
|
required=True,
|
|
description="ID of the location",
|
|
),
|
|
],
|
|
responses={
|
|
200: MapLocationDetailSerializer,
|
|
400: OpenApiTypes.OBJECT,
|
|
404: OpenApiTypes.OBJECT,
|
|
500: OpenApiTypes.OBJECT,
|
|
},
|
|
tags=["Maps"],
|
|
),
|
|
)
|
|
class MapLocationDetailAPIView(APIView):
|
|
"""API endpoint for getting detailed information about a specific location."""
|
|
|
|
permission_classes = [AllowAny]
|
|
|
|
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)
|
|
except Park.DoesNotExist:
|
|
return Response(
|
|
{"status": "error", "message": "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)
|
|
except Ride.DoesNotExist:
|
|
return Response(
|
|
{"status": "error", "message": "Ride not found"},
|
|
status=status.HTTP_404_NOT_FOUND,
|
|
)
|
|
else:
|
|
return Response(
|
|
{"status": "error", "message": "Invalid location type"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
# Serialize the object
|
|
if location_type == "park":
|
|
data = {
|
|
"id": obj.id,
|
|
"type": "park",
|
|
"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,
|
|
"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 "",
|
|
"formatted_address": 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,
|
|
"size_acres": float(obj.size_acres) if obj.size_acres else None,
|
|
"opening_date": obj.opening_date.isoformat() if obj.opening_date else None,
|
|
},
|
|
"nearby_locations": [], # TODO: Implement nearby locations
|
|
}
|
|
else: # ride
|
|
data = {
|
|
"id": obj.id,
|
|
"type": "ride",
|
|
"name": obj.name,
|
|
"slug": obj.slug,
|
|
"description": obj.description,
|
|
"latitude": 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,
|
|
"status": obj.status,
|
|
"location": {
|
|
"street_address": obj.park.location.street_address 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 "",
|
|
"country": 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 "",
|
|
"formatted_address": obj.park.location.formatted_address if hasattr(obj.park, 'location') and obj.park.location else "",
|
|
},
|
|
"stats": {
|
|
"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,
|
|
},
|
|
"nearby_locations": [], # TODO: Implement nearby locations
|
|
}
|
|
|
|
return Response({
|
|
"status": "success",
|
|
"data": data,
|
|
})
|
|
|
|
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=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
)
|
|
|
|
|
|
@extend_schema_view(
|
|
get=extend_schema(
|
|
summary="Search map locations",
|
|
description="Search locations by text query with optional bounds filtering.",
|
|
parameters=[
|
|
OpenApiParameter(
|
|
"q",
|
|
type=OpenApiTypes.STR,
|
|
location=OpenApiParameter.QUERY,
|
|
required=True,
|
|
description="Search query",
|
|
),
|
|
OpenApiParameter(
|
|
"types",
|
|
type=OpenApiTypes.STR,
|
|
location=OpenApiParameter.QUERY,
|
|
required=False,
|
|
description="Comma-separated location types (park,ride)",
|
|
),
|
|
OpenApiParameter(
|
|
"page",
|
|
type=OpenApiTypes.INT,
|
|
location=OpenApiParameter.QUERY,
|
|
required=False,
|
|
description="Page number",
|
|
),
|
|
OpenApiParameter(
|
|
"page_size",
|
|
type=OpenApiTypes.INT,
|
|
location=OpenApiParameter.QUERY,
|
|
required=False,
|
|
description="Results per page",
|
|
),
|
|
],
|
|
responses={
|
|
200: MapSearchResponseSerializer,
|
|
400: OpenApiTypes.OBJECT,
|
|
500: OpenApiTypes.OBJECT,
|
|
},
|
|
tags=["Maps"],
|
|
),
|
|
)
|
|
class MapSearchAPIView(APIView):
|
|
"""API endpoint for searching locations by text query."""
|
|
|
|
permission_classes = [AllowAny]
|
|
|
|
def get(self, request: HttpRequest) -> Response:
|
|
"""Search locations by text query with pagination."""
|
|
try:
|
|
query = request.GET.get("q", "").strip()
|
|
if not query:
|
|
return Response(
|
|
{
|
|
"status": "error",
|
|
"message": "Search query 'q' parameter is required",
|
|
},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
types = request.GET.get("types", "park,ride").split(",")
|
|
page = int(request.GET.get("page", 1))
|
|
page_size = min(int(request.GET.get("page_size", 20)), 100)
|
|
|
|
results = []
|
|
total_count = 0
|
|
|
|
# Search parks
|
|
if "park" in types:
|
|
parks_query = Park.objects.select_related("location").filter(
|
|
Q(name__icontains=query) |
|
|
Q(location__city__icontains=query) |
|
|
Q(location__state__icontains=query)
|
|
).filter(location__point__isnull=False)
|
|
|
|
for park in parks_query[:50]: # Limit results
|
|
results.append({
|
|
"id": park.id,
|
|
"type": "park",
|
|
"name": park.name,
|
|
"slug": park.slug,
|
|
"latitude": 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,
|
|
"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 "",
|
|
"country": park.location.country if hasattr(park, 'location') and park.location else "",
|
|
},
|
|
"relevance_score": 1.0, # TODO: Implement relevance scoring
|
|
})
|
|
|
|
# Search rides
|
|
if "ride" in types:
|
|
rides_query = Ride.objects.select_related("park__location").filter(
|
|
Q(name__icontains=query) |
|
|
Q(park__name__icontains=query) |
|
|
Q(park__location__city__icontains=query)
|
|
).filter(park__location__point__isnull=False)
|
|
|
|
for ride in rides_query[:50]: # Limit results
|
|
results.append({
|
|
"id": ride.id,
|
|
"type": "ride",
|
|
"name": ride.name,
|
|
"slug": ride.slug,
|
|
"latitude": ride.park.location.latitude if hasattr(ride.park, 'location') and ride.park.location else None,
|
|
"longitude": ride.park.location.longitude if hasattr(ride.park, 'location') and ride.park.location else None,
|
|
"location": {
|
|
"city": ride.park.location.city if hasattr(ride.park, 'location') and ride.park.location else "",
|
|
"state": ride.park.location.state if hasattr(ride.park, 'location') and ride.park.location else "",
|
|
"country": ride.park.location.country if hasattr(ride.park, 'location') and ride.park.location else "",
|
|
},
|
|
"relevance_score": 1.0, # TODO: Implement relevance scoring
|
|
})
|
|
|
|
total_count = len(results)
|
|
|
|
# Apply pagination
|
|
start_idx = (page - 1) * page_size
|
|
end_idx = start_idx + page_size
|
|
paginated_results = results[start_idx:end_idx]
|
|
|
|
return Response({
|
|
"status": "success",
|
|
"results": paginated_results,
|
|
"query": query,
|
|
"total_count": total_count,
|
|
"page": page,
|
|
"page_size": page_size,
|
|
})
|
|
|
|
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=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
)
|
|
|
|
|
|
@extend_schema_view(
|
|
get=extend_schema(
|
|
summary="Get locations within bounds",
|
|
description="Get locations within specific geographic bounds.",
|
|
parameters=[
|
|
OpenApiParameter(
|
|
"north",
|
|
type=OpenApiTypes.NUMBER,
|
|
location=OpenApiParameter.QUERY,
|
|
required=True,
|
|
description="Northern latitude bound",
|
|
),
|
|
OpenApiParameter(
|
|
"south",
|
|
type=OpenApiTypes.NUMBER,
|
|
location=OpenApiParameter.QUERY,
|
|
required=True,
|
|
description="Southern latitude bound",
|
|
),
|
|
OpenApiParameter(
|
|
"east",
|
|
type=OpenApiTypes.NUMBER,
|
|
location=OpenApiParameter.QUERY,
|
|
required=True,
|
|
description="Eastern longitude bound",
|
|
),
|
|
OpenApiParameter(
|
|
"west",
|
|
type=OpenApiTypes.NUMBER,
|
|
location=OpenApiParameter.QUERY,
|
|
required=True,
|
|
description="Western longitude bound",
|
|
),
|
|
OpenApiParameter(
|
|
"types",
|
|
type=OpenApiTypes.STR,
|
|
location=OpenApiParameter.QUERY,
|
|
required=False,
|
|
description="Comma-separated location types (park,ride)",
|
|
),
|
|
],
|
|
responses={200: OpenApiTypes.OBJECT, 400: OpenApiTypes.OBJECT},
|
|
tags=["Maps"],
|
|
),
|
|
)
|
|
class MapBoundsAPIView(APIView):
|
|
"""API endpoint for getting locations within specific bounds."""
|
|
|
|
permission_classes = [AllowAny]
|
|
|
|
def get(self, request: HttpRequest) -> Response:
|
|
"""Get locations within specific geographic bounds."""
|
|
try:
|
|
# Parse required bounds parameters
|
|
try:
|
|
north = float(request.GET.get("north"))
|
|
south = float(request.GET.get("south"))
|
|
east = float(request.GET.get("east"))
|
|
west = float(request.GET.get("west"))
|
|
except (TypeError, ValueError):
|
|
return Response(
|
|
{"status": "error", "message": "Invalid bounds parameters"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
# Validate bounds
|
|
if north <= south:
|
|
return Response(
|
|
{"status": "error", "message": "North bound must be greater than south bound"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
if west >= east:
|
|
return Response(
|
|
{"status": "error", "message": "West bound must be less than east bound"},
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
|
|
types = request.GET.get("types", "park,ride").split(",")
|
|
locations = []
|
|
|
|
# Create bounds polygon
|
|
bounds_polygon = Polygon.from_bbox((west, south, east, north))
|
|
|
|
# Get parks within bounds
|
|
if "park" in types:
|
|
parks_query = Park.objects.select_related("location").filter(
|
|
location__point__within=bounds_polygon
|
|
)
|
|
|
|
for park in parks_query[:100]: # Limit results
|
|
locations.append({
|
|
"id": park.id,
|
|
"type": "park",
|
|
"name": park.name,
|
|
"slug": park.slug,
|
|
"latitude": 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,
|
|
"status": park.status,
|
|
})
|
|
|
|
# Get rides within bounds
|
|
if "ride" in types:
|
|
rides_query = Ride.objects.select_related("park__location").filter(
|
|
park__location__point__within=bounds_polygon
|
|
)
|
|
|
|
for ride in rides_query[:100]: # Limit results
|
|
locations.append({
|
|
"id": ride.id,
|
|
"type": "ride",
|
|
"name": ride.name,
|
|
"slug": ride.slug,
|
|
"latitude": ride.park.location.latitude if hasattr(ride.park, 'location') and ride.park.location else None,
|
|
"longitude": ride.park.location.longitude if hasattr(ride.park, 'location') and ride.park.location else None,
|
|
"status": ride.status,
|
|
})
|
|
|
|
return Response({
|
|
"status": "success",
|
|
"locations": locations,
|
|
"bounds": {
|
|
"north": north,
|
|
"south": south,
|
|
"east": east,
|
|
"west": west,
|
|
},
|
|
"total_count": len(locations),
|
|
})
|
|
|
|
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=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
)
|
|
|
|
|
|
@extend_schema_view(
|
|
get=extend_schema(
|
|
summary="Get map service statistics",
|
|
description="Get map service statistics and performance metrics.",
|
|
responses={200: OpenApiTypes.OBJECT},
|
|
tags=["Maps"],
|
|
),
|
|
)
|
|
class MapStatsAPIView(APIView):
|
|
"""API endpoint for getting map service statistics and health information."""
|
|
|
|
permission_classes = [AllowAny]
|
|
|
|
def get(self, request: HttpRequest) -> Response:
|
|
"""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()
|
|
total_locations = parks_with_location + rides_with_location
|
|
|
|
return Response({
|
|
"status": "success",
|
|
"data": {
|
|
"total_locations": total_locations,
|
|
"parks_with_location": parks_with_location,
|
|
"rides_with_location": rides_with_location,
|
|
"cache_hits": 0, # TODO: Implement cache statistics
|
|
"cache_misses": 0, # TODO: Implement cache statistics
|
|
},
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in MapStatsAPIView: {str(e)}", exc_info=True)
|
|
return Response(
|
|
{"error": f"Internal server error: {str(e)}"},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
)
|
|
|
|
|
|
@extend_schema_view(
|
|
delete=extend_schema(
|
|
summary="Clear map cache",
|
|
description="Clear all map cache (admin only).",
|
|
responses={200: OpenApiTypes.OBJECT},
|
|
tags=["Maps"],
|
|
),
|
|
post=extend_schema(
|
|
summary="Invalidate specific cache entries",
|
|
description="Invalidate specific cache entries.",
|
|
responses={200: OpenApiTypes.OBJECT},
|
|
tags=["Maps"],
|
|
),
|
|
)
|
|
class MapCacheAPIView(APIView):
|
|
"""API endpoint for cache management (admin only)."""
|
|
|
|
permission_classes = [AllowAny] # TODO: Add admin permission check
|
|
|
|
def delete(self, request: HttpRequest) -> Response:
|
|
"""Clear all map cache (admin only)."""
|
|
try:
|
|
# Clear all map-related cache keys
|
|
cache_keys = cache.keys("map_*")
|
|
if cache_keys:
|
|
cache.delete_many(cache_keys)
|
|
cleared_count = len(cache_keys)
|
|
else:
|
|
cleared_count = 0
|
|
|
|
return Response({
|
|
"status": "success",
|
|
"message": f"Map cache cleared successfully. Cleared {cleared_count} entries.",
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in MapCacheAPIView.delete: {str(e)}", exc_info=True)
|
|
return Response(
|
|
{"error": f"Internal server error: {str(e)}"},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
)
|
|
|
|
def post(self, request: HttpRequest) -> Response:
|
|
"""Invalidate specific cache entries."""
|
|
try:
|
|
# Get cache keys to invalidate from request data
|
|
cache_keys = request.data.get("cache_keys", [])
|
|
if cache_keys:
|
|
cache.delete_many(cache_keys)
|
|
invalidated_count = len(cache_keys)
|
|
else:
|
|
invalidated_count = 0
|
|
|
|
return Response({
|
|
"status": "success",
|
|
"message": f"Cache invalidated successfully. Invalidated {invalidated_count} entries.",
|
|
})
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in MapCacheAPIView.post: {str(e)}", exc_info=True)
|
|
return Response(
|
|
{"error": f"Internal server error: {str(e)}"},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
)
|
|
|
|
|
|
# Legacy compatibility aliases
|
|
MapLocationsView = MapLocationsAPIView
|
|
MapLocationDetailView = MapLocationDetailAPIView
|
|
MapSearchView = MapSearchAPIView
|
|
MapBoundsView = MapBoundsAPIView
|
|
MapStatsView = MapStatsAPIView
|
|
MapCacheView = MapCacheAPIView
|