mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 10:11:09 -05:00
feat: Enhance Park Detail Endpoint with Media URL Service Integration
- Updated ParkDetailOutputSerializer to utilize MediaURLService for generating Cloudflare URLs and friendly URLs for park photos. - Added support for multiple lookup methods (ID and slug) in the park detail endpoint. - Improved documentation for the park detail endpoint, including request properties and response structure. - Created MediaURLService for generating SEO-friendly URLs and handling Cloudflare image URLs. - Comprehensive updates to frontend documentation to reflect new endpoint capabilities and usage examples. - Added detailed park detail endpoint documentation, including request and response structures, field descriptions, and usage examples.
This commit is contained in:
@@ -12,6 +12,7 @@ Supports all 24 filtering parameters from frontend API documentation.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from django.db import models
|
||||
from django.db.models import Q, Count, Avg
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
@@ -401,20 +402,22 @@ class ParkListCreateAPIView(APIView):
|
||||
|
||||
# --- Park retrieve / update / delete ---------------------------------------
|
||||
@extend_schema(
|
||||
summary="Retrieve, update or delete a park",
|
||||
summary="Retrieve, update or delete a park by ID or slug",
|
||||
description="Retrieve full park details including location, photos, areas, rides, and company information. Supports both ID and slug-based lookup with historical slug support.",
|
||||
responses={
|
||||
200: (
|
||||
"ParkDetailOutputSerializer()"
|
||||
if SERIALIZERS_AVAILABLE
|
||||
else OpenApiTypes.OBJECT
|
||||
)
|
||||
),
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
tags=["Parks"],
|
||||
)
|
||||
class ParkDetailAPIView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def _get_park_or_404(self, pk: int) -> Any:
|
||||
def _get_park_or_404(self, identifier: str) -> Any:
|
||||
if not MODELS_AVAILABLE:
|
||||
raise NotFound(
|
||||
(
|
||||
@@ -423,13 +426,77 @@ class ParkDetailAPIView(APIView):
|
||||
"to enable detail endpoints."
|
||||
)
|
||||
)
|
||||
|
||||
# Try to parse as integer ID first
|
||||
try:
|
||||
# type: ignore
|
||||
return Park.objects.select_related("operator", "property_owner").get(pk=pk)
|
||||
except Park.DoesNotExist: # type: ignore
|
||||
raise NotFound("Park not found")
|
||||
pk = int(identifier)
|
||||
try:
|
||||
return Park.objects.select_related(
|
||||
"operator", "property_owner", "location"
|
||||
).prefetch_related(
|
||||
"areas", "rides", "photos"
|
||||
).get(pk=pk)
|
||||
except Park.DoesNotExist:
|
||||
raise NotFound("Park not found")
|
||||
except ValueError:
|
||||
# Not an integer, try slug lookup
|
||||
try:
|
||||
park, is_historical = Park.get_by_slug(identifier)
|
||||
# Ensure we have the full related data
|
||||
return Park.objects.select_related(
|
||||
"operator", "property_owner", "location"
|
||||
).prefetch_related(
|
||||
"areas", "rides", "photos"
|
||||
).get(pk=park.pk)
|
||||
except Park.DoesNotExist:
|
||||
raise NotFound("Park not found")
|
||||
|
||||
def get(self, request: Request, pk: int) -> Response:
|
||||
@extend_schema(
|
||||
summary="Get park full details",
|
||||
description="""
|
||||
Retrieve comprehensive park details including:
|
||||
|
||||
**Core Information:**
|
||||
- Basic park details (name, slug, description, status)
|
||||
- Opening/closing dates and operating season
|
||||
- Size in acres and website URL
|
||||
- Statistics (average rating, ride count, coaster count)
|
||||
|
||||
**Location Data:**
|
||||
- Full address with coordinates
|
||||
- City, state, country information
|
||||
- Formatted address string
|
||||
|
||||
**Company Information:**
|
||||
- Operating company details
|
||||
- Property owner information (if different)
|
||||
|
||||
**Media:**
|
||||
- All approved photos with Cloudflare variants
|
||||
- Primary photo designation
|
||||
- Banner and card image settings
|
||||
|
||||
**Related Content:**
|
||||
- Park areas/themed sections
|
||||
- Associated rides (summary)
|
||||
|
||||
**Lookup Methods:**
|
||||
- By ID: `/api/v1/parks/123/`
|
||||
- By current slug: `/api/v1/parks/cedar-point/`
|
||||
- By historical slug: `/api/v1/parks/old-cedar-point-name/`
|
||||
|
||||
**No Query Parameters Required** - This endpoint returns full details by default.
|
||||
""",
|
||||
responses={
|
||||
200: (
|
||||
"ParkDetailOutputSerializer()"
|
||||
if SERIALIZERS_AVAILABLE
|
||||
else OpenApiTypes.OBJECT
|
||||
),
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
)
|
||||
def get(self, request: Request, pk: str) -> Response:
|
||||
park = self._get_park_or_404(pk)
|
||||
if SERIALIZERS_AVAILABLE:
|
||||
serializer = ParkDetailOutputSerializer(park, context={"request": request})
|
||||
@@ -451,7 +518,7 @@ class ParkDetailAPIView(APIView):
|
||||
}
|
||||
)
|
||||
|
||||
def patch(self, request: Request, pk: int) -> Response:
|
||||
def patch(self, request: Request, pk: str) -> Response:
|
||||
park = self._get_park_or_404(pk)
|
||||
if not SERIALIZERS_AVAILABLE:
|
||||
return Response(
|
||||
@@ -478,11 +545,11 @@ class ParkDetailAPIView(APIView):
|
||||
serializer = ParkDetailOutputSerializer(park, context={"request": request})
|
||||
return Response(serializer.data)
|
||||
|
||||
def put(self, request: Request, pk: int) -> Response:
|
||||
def put(self, request: Request, pk: str) -> Response:
|
||||
# Full replace - reuse patch behavior for simplicity
|
||||
return self.patch(request, pk)
|
||||
|
||||
def delete(self, request: Request, pk: int) -> Response:
|
||||
def delete(self, request: Request, pk: str) -> Response:
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{
|
||||
@@ -508,9 +575,9 @@ class FilterOptionsAPIView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Return comprehensive filter options matching frontend API documentation."""
|
||||
"""Return comprehensive filter options with all possible park model fields and attributes."""
|
||||
if not MODELS_AVAILABLE:
|
||||
# Fallback comprehensive options
|
||||
# Fallback comprehensive options with all possible fields
|
||||
return Response({
|
||||
"park_types": [
|
||||
{"value": "THEME_PARK", "label": "Theme Park"},
|
||||
@@ -518,6 +585,21 @@ class FilterOptionsAPIView(APIView):
|
||||
{"value": "WATER_PARK", "label": "Water Park"},
|
||||
{"value": "FAMILY_ENTERTAINMENT_CENTER",
|
||||
"label": "Family Entertainment Center"},
|
||||
{"value": "CARNIVAL", "label": "Carnival"},
|
||||
{"value": "FAIR", "label": "Fair"},
|
||||
{"value": "PIER", "label": "Pier"},
|
||||
{"value": "BOARDWALK", "label": "Boardwalk"},
|
||||
{"value": "SAFARI_PARK", "label": "Safari Park"},
|
||||
{"value": "ZOO", "label": "Zoo"},
|
||||
{"value": "OTHER", "label": "Other"},
|
||||
],
|
||||
"statuses": [
|
||||
{"value": "OPERATING", "label": "Operating"},
|
||||
{"value": "CLOSED_TEMP", "label": "Temporarily Closed"},
|
||||
{"value": "CLOSED_PERM", "label": "Permanently Closed"},
|
||||
{"value": "UNDER_CONSTRUCTION", "label": "Under Construction"},
|
||||
{"value": "DEMOLISHED", "label": "Demolished"},
|
||||
{"value": "RELOCATED", "label": "Relocated"},
|
||||
],
|
||||
"continents": [
|
||||
"North America",
|
||||
@@ -546,6 +628,21 @@ class FilterOptionsAPIView(APIView):
|
||||
"Texas",
|
||||
"New York"
|
||||
],
|
||||
"cities": [
|
||||
"Orlando",
|
||||
"Los Angeles",
|
||||
"Cedar Point",
|
||||
"Sandusky"
|
||||
],
|
||||
"operators": [],
|
||||
"property_owners": [],
|
||||
"ranges": {
|
||||
"rating": {"min": 1, "max": 10, "step": 0.1, "unit": "stars"},
|
||||
"ride_count": {"min": 0, "max": 100, "step": 1, "unit": "rides"},
|
||||
"coaster_count": {"min": 0, "max": 50, "step": 1, "unit": "coasters"},
|
||||
"size_acres": {"min": 0, "max": 10000, "step": 1, "unit": "acres"},
|
||||
"opening_year": {"min": 1800, "max": 2030, "step": 1, "unit": "year"},
|
||||
},
|
||||
"ordering_options": [
|
||||
{"value": "name", "label": "Name (A-Z)"},
|
||||
{"value": "-name", "label": "Name (Z-A)"},
|
||||
@@ -553,18 +650,36 @@ class FilterOptionsAPIView(APIView):
|
||||
{"value": "-opening_date", "label": "Opening Date (Newest First)"},
|
||||
{"value": "ride_count", "label": "Ride Count (Low to High)"},
|
||||
{"value": "-ride_count", "label": "Ride Count (High to Low)"},
|
||||
{"value": "coaster_count", "label": "Coaster Count (Low to High)"},
|
||||
{"value": "-coaster_count", "label": "Coaster Count (High to Low)"},
|
||||
{"value": "average_rating", "label": "Rating (Low to High)"},
|
||||
{"value": "-average_rating", "label": "Rating (High to Low)"},
|
||||
{"value": "roller_coaster_count",
|
||||
"label": "Coaster Count (Low to High)"},
|
||||
{"value": "-roller_coaster_count",
|
||||
"label": "Coaster Count (High to Low)"},
|
||||
{"value": "size_acres", "label": "Size (Small to Large)"},
|
||||
{"value": "-size_acres", "label": "Size (Large to Small)"},
|
||||
{"value": "created_at",
|
||||
"label": "Added to Database (Oldest First)"},
|
||||
{"value": "-created_at",
|
||||
"label": "Added to Database (Newest First)"},
|
||||
{"value": "updated_at", "label": "Last Updated (Oldest First)"},
|
||||
{"value": "-updated_at", "label": "Last Updated (Newest First)"},
|
||||
],
|
||||
})
|
||||
|
||||
# Try to get dynamic options from database
|
||||
try:
|
||||
# Get continents from database (now available field)
|
||||
# Get all park types from model choices
|
||||
park_types = [
|
||||
{"value": choice[0], "label": choice[1]}
|
||||
for choice in Park.PARK_TYPE_CHOICES
|
||||
]
|
||||
|
||||
# Get all statuses from model choices
|
||||
statuses = [
|
||||
{"value": choice[0], "label": choice[1]}
|
||||
for choice in Park.STATUS_CHOICES
|
||||
]
|
||||
|
||||
# Get location data from database
|
||||
continents = list(Park.objects.exclude(
|
||||
location__continent__isnull=True
|
||||
).exclude(
|
||||
@@ -595,17 +710,78 @@ class FilterOptionsAPIView(APIView):
|
||||
location__state__exact=''
|
||||
).values_list('location__state', flat=True).distinct().order_by('location__state'))
|
||||
|
||||
# Get park types from model choices (now available field)
|
||||
park_types = [
|
||||
{"value": choice[0], "label": choice[1]}
|
||||
for choice in Park.PARK_TYPE_CHOICES
|
||||
]
|
||||
cities = list(Park.objects.exclude(
|
||||
location__city__isnull=True
|
||||
).exclude(
|
||||
location__city__exact=''
|
||||
).values_list('location__city', flat=True).distinct().order_by('location__city'))
|
||||
|
||||
# Get operators and property owners
|
||||
operators = list(Company.objects.filter(
|
||||
roles__contains=['OPERATOR']
|
||||
).values('id', 'name', 'slug').order_by('name'))
|
||||
|
||||
property_owners = list(Company.objects.filter(
|
||||
roles__contains=['PROPERTY_OWNER']
|
||||
).values('id', 'name', 'slug').order_by('name'))
|
||||
|
||||
# Calculate ranges from actual data
|
||||
park_stats = Park.objects.aggregate(
|
||||
min_rating=models.Min('average_rating'),
|
||||
max_rating=models.Max('average_rating'),
|
||||
min_ride_count=models.Min('ride_count'),
|
||||
max_ride_count=models.Max('ride_count'),
|
||||
min_coaster_count=models.Min('coaster_count'),
|
||||
max_coaster_count=models.Max('coaster_count'),
|
||||
min_size=models.Min('size_acres'),
|
||||
max_size=models.Max('size_acres'),
|
||||
min_year=models.Min('opening_date__year'),
|
||||
max_year=models.Max('opening_date__year'),
|
||||
)
|
||||
|
||||
ranges = {
|
||||
"rating": {
|
||||
"min": float(park_stats['min_rating'] or 1),
|
||||
"max": float(park_stats['max_rating'] or 10),
|
||||
"step": 0.1,
|
||||
"unit": "stars"
|
||||
},
|
||||
"ride_count": {
|
||||
"min": park_stats['min_ride_count'] or 0,
|
||||
"max": park_stats['max_ride_count'] or 100,
|
||||
"step": 1,
|
||||
"unit": "rides"
|
||||
},
|
||||
"coaster_count": {
|
||||
"min": park_stats['min_coaster_count'] or 0,
|
||||
"max": park_stats['max_coaster_count'] or 50,
|
||||
"step": 1,
|
||||
"unit": "coasters"
|
||||
},
|
||||
"size_acres": {
|
||||
"min": float(park_stats['min_size'] or 0),
|
||||
"max": float(park_stats['max_size'] or 10000),
|
||||
"step": 1,
|
||||
"unit": "acres"
|
||||
},
|
||||
"opening_year": {
|
||||
"min": park_stats['min_year'] or 1800,
|
||||
"max": park_stats['max_year'] or 2030,
|
||||
"step": 1,
|
||||
"unit": "year"
|
||||
},
|
||||
}
|
||||
|
||||
return Response({
|
||||
"park_types": park_types,
|
||||
"statuses": statuses,
|
||||
"continents": continents,
|
||||
"countries": countries,
|
||||
"states": states,
|
||||
"cities": cities,
|
||||
"operators": operators,
|
||||
"property_owners": property_owners,
|
||||
"ranges": ranges,
|
||||
"ordering_options": [
|
||||
{"value": "name", "label": "Name (A-Z)"},
|
||||
{"value": "-name", "label": "Name (Z-A)"},
|
||||
@@ -613,12 +789,18 @@ class FilterOptionsAPIView(APIView):
|
||||
{"value": "-opening_date", "label": "Opening Date (Newest First)"},
|
||||
{"value": "ride_count", "label": "Ride Count (Low to High)"},
|
||||
{"value": "-ride_count", "label": "Ride Count (High to Low)"},
|
||||
{"value": "coaster_count", "label": "Coaster Count (Low to High)"},
|
||||
{"value": "-coaster_count", "label": "Coaster Count (High to Low)"},
|
||||
{"value": "average_rating", "label": "Rating (Low to High)"},
|
||||
{"value": "-average_rating", "label": "Rating (High to Low)"},
|
||||
{"value": "roller_coaster_count",
|
||||
"label": "Coaster Count (Low to High)"},
|
||||
{"value": "-roller_coaster_count",
|
||||
"label": "Coaster Count (High to Low)"},
|
||||
{"value": "size_acres", "label": "Size (Small to Large)"},
|
||||
{"value": "-size_acres", "label": "Size (Large to Small)"},
|
||||
{"value": "created_at",
|
||||
"label": "Added to Database (Oldest First)"},
|
||||
{"value": "-created_at",
|
||||
"label": "Added to Database (Newest First)"},
|
||||
{"value": "updated_at", "label": "Last Updated (Oldest First)"},
|
||||
{"value": "-updated_at", "label": "Last Updated (Newest First)"},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -629,6 +811,23 @@ class FilterOptionsAPIView(APIView):
|
||||
{"value": "THEME_PARK", "label": "Theme Park"},
|
||||
{"value": "AMUSEMENT_PARK", "label": "Amusement Park"},
|
||||
{"value": "WATER_PARK", "label": "Water Park"},
|
||||
{"value": "FAMILY_ENTERTAINMENT_CENTER",
|
||||
"label": "Family Entertainment Center"},
|
||||
{"value": "CARNIVAL", "label": "Carnival"},
|
||||
{"value": "FAIR", "label": "Fair"},
|
||||
{"value": "PIER", "label": "Pier"},
|
||||
{"value": "BOARDWALK", "label": "Boardwalk"},
|
||||
{"value": "SAFARI_PARK", "label": "Safari Park"},
|
||||
{"value": "ZOO", "label": "Zoo"},
|
||||
{"value": "OTHER", "label": "Other"},
|
||||
],
|
||||
"statuses": [
|
||||
{"value": "OPERATING", "label": "Operating"},
|
||||
{"value": "CLOSED_TEMP", "label": "Temporarily Closed"},
|
||||
{"value": "CLOSED_PERM", "label": "Permanently Closed"},
|
||||
{"value": "UNDER_CONSTRUCTION", "label": "Under Construction"},
|
||||
{"value": "DEMOLISHED", "label": "Demolished"},
|
||||
{"value": "RELOCATED", "label": "Relocated"},
|
||||
],
|
||||
"continents": [
|
||||
"North America",
|
||||
@@ -652,6 +851,20 @@ class FilterOptionsAPIView(APIView):
|
||||
"Ohio",
|
||||
"Pennsylvania"
|
||||
],
|
||||
"cities": [
|
||||
"Orlando",
|
||||
"Los Angeles",
|
||||
"Cedar Point"
|
||||
],
|
||||
"operators": [],
|
||||
"property_owners": [],
|
||||
"ranges": {
|
||||
"rating": {"min": 1, "max": 10, "step": 0.1, "unit": "stars"},
|
||||
"ride_count": {"min": 0, "max": 100, "step": 1, "unit": "rides"},
|
||||
"coaster_count": {"min": 0, "max": 50, "step": 1, "unit": "coasters"},
|
||||
"size_acres": {"min": 0, "max": 10000, "step": 1, "unit": "acres"},
|
||||
"opening_year": {"min": 1800, "max": 2030, "step": 1, "unit": "year"},
|
||||
},
|
||||
"ordering_options": [
|
||||
{"value": "name", "label": "Name (A-Z)"},
|
||||
{"value": "-name", "label": "Name (Z-A)"},
|
||||
@@ -659,6 +872,10 @@ class FilterOptionsAPIView(APIView):
|
||||
{"value": "-opening_date", "label": "Opening Date (Newest First)"},
|
||||
{"value": "ride_count", "label": "Ride Count (Low to High)"},
|
||||
{"value": "-ride_count", "label": "Ride Count (High to Low)"},
|
||||
{"value": "coaster_count", "label": "Coaster Count (Low to High)"},
|
||||
{"value": "-coaster_count", "label": "Coaster Count (High to Low)"},
|
||||
{"value": "average_rating", "label": "Rating (Low to High)"},
|
||||
{"value": "-average_rating", "label": "Rating (High to Low)"},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ urlpatterns = [
|
||||
ParkSearchSuggestionsAPIView.as_view(),
|
||||
name="park-search-suggestions",
|
||||
),
|
||||
# Detail and action endpoints
|
||||
path("<int:pk>/", ParkDetailAPIView.as_view(), name="park-detail"),
|
||||
# Detail and action endpoints - supports both ID and slug
|
||||
path("<str:pk>/", ParkDetailAPIView.as_view(), name="park-detail"),
|
||||
# Park image settings endpoint
|
||||
path(
|
||||
"<int:pk>/image-settings/",
|
||||
|
||||
Reference in New Issue
Block a user