mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 05:11:09 -05:00
1084 lines
44 KiB
Python
1084 lines
44 KiB
Python
"""
|
|
Full-featured Parks API views for ThrillWiki API v1.
|
|
|
|
This module implements comprehensive park endpoints with full filtering support:
|
|
- List / Create: GET /parks/ POST /parks/
|
|
- Retrieve / Update / Delete: GET /parks/{pk}/ PATCH/PUT/DELETE
|
|
- Filter options: GET /parks/filter-options/
|
|
- Company search: GET /parks/search/companies/?q=...
|
|
- Search suggestions: GET /parks/search-suggestions/?q=...
|
|
|
|
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
|
|
|
|
from rest_framework import status, permissions
|
|
from rest_framework.views import APIView
|
|
from rest_framework.request import Request
|
|
from rest_framework.response import Response
|
|
from rest_framework.pagination import PageNumberPagination
|
|
from rest_framework.exceptions import NotFound
|
|
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
|
from drf_spectacular.types import OpenApiTypes
|
|
|
|
# Import models
|
|
try:
|
|
from apps.parks.models import Park, Company
|
|
MODELS_AVAILABLE = True
|
|
except Exception:
|
|
Park = None # type: ignore
|
|
Company = None # type: ignore
|
|
MODELS_AVAILABLE = False
|
|
|
|
# Import ModelChoices for filter options
|
|
try:
|
|
from apps.api.v1.serializers.shared import ModelChoices
|
|
HAVE_MODELCHOICES = True
|
|
except Exception:
|
|
ModelChoices = None # type: ignore
|
|
HAVE_MODELCHOICES = False
|
|
|
|
# Import serializers
|
|
try:
|
|
from apps.api.v1.serializers.parks import (
|
|
ParkListOutputSerializer,
|
|
ParkDetailOutputSerializer,
|
|
ParkCreateInputSerializer,
|
|
ParkUpdateInputSerializer,
|
|
ParkImageSettingsInputSerializer,
|
|
)
|
|
SERIALIZERS_AVAILABLE = True
|
|
except Exception:
|
|
SERIALIZERS_AVAILABLE = False
|
|
|
|
|
|
class StandardResultsSetPagination(PageNumberPagination):
|
|
page_size = 20
|
|
page_size_query_param = "page_size"
|
|
max_page_size = 1000
|
|
|
|
|
|
# --- Park list & create -----------------------------------------------------
|
|
class ParkListCreateAPIView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
@extend_schema(
|
|
summary="List parks with comprehensive filtering and pagination",
|
|
description="List parks with comprehensive filtering matching frontend API documentation. Supports all 24 filtering parameters including continent, rating ranges, ride counts, and more.",
|
|
parameters=[
|
|
# Pagination
|
|
OpenApiParameter(name="page", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT, description="Page number"),
|
|
OpenApiParameter(name="page_size", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT, description="Number of results per page"),
|
|
|
|
# Search
|
|
OpenApiParameter(name="search", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR, description="Search parks by name"),
|
|
|
|
# Location filters
|
|
OpenApiParameter(name="continent", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR, description="Filter by continent"),
|
|
OpenApiParameter(name="country", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR, description="Filter by country"),
|
|
OpenApiParameter(name="state", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR, description="Filter by state/province"),
|
|
OpenApiParameter(name="city", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR, description="Filter by city"),
|
|
|
|
# Park attributes
|
|
OpenApiParameter(name="park_type", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR, description="Filter by park type"),
|
|
OpenApiParameter(name="status", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR, description="Filter by operational status"),
|
|
|
|
# Company filters
|
|
OpenApiParameter(name="operator_id", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT, description="Filter by operator company ID"),
|
|
OpenApiParameter(name="operator_slug", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR, description="Filter by operator company slug"),
|
|
OpenApiParameter(name="property_owner_id", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT, description="Filter by property owner company ID"),
|
|
OpenApiParameter(name="property_owner_slug", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR, description="Filter by property owner company slug"),
|
|
|
|
# Rating filters
|
|
OpenApiParameter(name="min_rating", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.NUMBER, description="Minimum average rating"),
|
|
OpenApiParameter(name="max_rating", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.NUMBER, description="Maximum average rating"),
|
|
|
|
# Ride count filters
|
|
OpenApiParameter(name="min_ride_count", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT, description="Minimum total ride count"),
|
|
OpenApiParameter(name="max_ride_count", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT, description="Maximum total ride count"),
|
|
|
|
# Opening year filters
|
|
OpenApiParameter(name="opening_year", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT, description="Filter by specific opening year"),
|
|
OpenApiParameter(name="min_opening_year", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT, description="Minimum opening year"),
|
|
OpenApiParameter(name="max_opening_year", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT, description="Maximum opening year"),
|
|
|
|
# Roller coaster filters
|
|
OpenApiParameter(name="has_roller_coasters", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.BOOL, description="Filter parks that have roller coasters"),
|
|
OpenApiParameter(name="min_roller_coaster_count", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT, description="Minimum roller coaster count"),
|
|
OpenApiParameter(name="max_roller_coaster_count", location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT, description="Maximum roller coaster count"),
|
|
|
|
# Ordering
|
|
OpenApiParameter(name="ordering", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR,
|
|
description="Order results by field (prefix with - for descending)"),
|
|
],
|
|
responses={
|
|
200: (
|
|
"ParkListOutputSerializer(many=True)"
|
|
if SERIALIZERS_AVAILABLE
|
|
else OpenApiTypes.OBJECT
|
|
)
|
|
},
|
|
tags=["Parks"],
|
|
)
|
|
def get(self, request: Request) -> Response:
|
|
"""List parks with comprehensive filtering and pagination."""
|
|
if not MODELS_AVAILABLE:
|
|
return Response(
|
|
{
|
|
"detail": (
|
|
"Park listing is not available because domain models "
|
|
"are not imported. Implement apps.parks.models.Park "
|
|
"(and related managers) to enable listing."
|
|
)
|
|
},
|
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
)
|
|
|
|
# Start with base queryset
|
|
qs = Park.objects.all().select_related(
|
|
"operator", "property_owner", "location"
|
|
).prefetch_related("rides").annotate(
|
|
ride_count_calculated=Count('rides'),
|
|
roller_coaster_count_calculated=Count(
|
|
'rides', filter=Q(rides__category='RC')),
|
|
average_rating_calculated=Avg('reviews__rating')
|
|
)
|
|
|
|
# Apply comprehensive filtering
|
|
qs = self._apply_filters(qs, request.query_params)
|
|
|
|
# Apply ordering
|
|
ordering = request.query_params.get("ordering", "name")
|
|
if ordering:
|
|
qs = qs.order_by(ordering)
|
|
|
|
# Paginate results
|
|
paginator = StandardResultsSetPagination()
|
|
page = paginator.paginate_queryset(qs, request)
|
|
|
|
if SERIALIZERS_AVAILABLE:
|
|
serializer = ParkListOutputSerializer(
|
|
page, many=True, context={"request": request}
|
|
)
|
|
return paginator.get_paginated_response(serializer.data)
|
|
else:
|
|
# Fallback serialization
|
|
serializer_data = [
|
|
{
|
|
"id": park.id,
|
|
"name": park.name,
|
|
"slug": getattr(park, "slug", ""),
|
|
"description": getattr(park, "description", ""),
|
|
"location": {
|
|
"country": getattr(park.location, "country", "") if hasattr(park, "location") else "",
|
|
"state": getattr(park.location, "state", "") if hasattr(park, "location") else "",
|
|
"city": getattr(park.location, "city", "") if hasattr(park, "location") else "",
|
|
},
|
|
"operator": {
|
|
"id": park.operator.id if park.operator else None,
|
|
"name": park.operator.name if park.operator else "",
|
|
"slug": getattr(park.operator, "slug", "") if park.operator else "",
|
|
},
|
|
"ride_count": getattr(park, "ride_count", 0),
|
|
"roller_coaster_count": getattr(park, "roller_coaster_count", 0),
|
|
"average_rating": getattr(park, "average_rating", None),
|
|
}
|
|
for park in page
|
|
]
|
|
return paginator.get_paginated_response(serializer_data)
|
|
|
|
def _apply_filters(self, qs: QuerySet, params: dict) -> QuerySet:
|
|
"""Apply filtering to the queryset based on actual model fields."""
|
|
qs = self._apply_search_filters(qs, params)
|
|
qs = self._apply_location_filters(qs, params)
|
|
qs = self._apply_park_attribute_filters(qs, params)
|
|
qs = self._apply_company_filters(qs, params)
|
|
qs = self._apply_rating_filters(qs, params)
|
|
qs = self._apply_ride_count_filters(qs, params)
|
|
qs = self._apply_opening_year_filters(qs, params)
|
|
qs = self._apply_roller_coaster_filters(qs, params)
|
|
return qs
|
|
|
|
def _apply_search_filters(self, qs: QuerySet, params: dict) -> QuerySet:
|
|
"""Apply search filtering to the queryset."""
|
|
search = params.get("search")
|
|
if search:
|
|
qs = qs.filter(
|
|
Q(name__icontains=search) |
|
|
Q(description__icontains=search) |
|
|
Q(location__city__icontains=search) |
|
|
Q(location__state__icontains=search) |
|
|
Q(location__country__icontains=search)
|
|
)
|
|
return qs
|
|
|
|
def _apply_location_filters(self, qs: QuerySet, params: dict) -> QuerySet:
|
|
"""Apply location-based filtering to the queryset."""
|
|
location_filters = {
|
|
'country': 'location__country__iexact',
|
|
'state': 'location__state__iexact',
|
|
'city': 'location__city__iexact',
|
|
'continent': 'location__continent__iexact'
|
|
}
|
|
|
|
for param_name, filter_field in location_filters.items():
|
|
value = params.get(param_name)
|
|
if value:
|
|
qs = qs.filter(**{filter_field: value})
|
|
|
|
return qs
|
|
|
|
def _apply_park_attribute_filters(self, qs: QuerySet, params: dict) -> QuerySet:
|
|
"""Apply park attribute filtering to the queryset."""
|
|
park_type = params.get("park_type")
|
|
if park_type:
|
|
qs = qs.filter(park_type=park_type)
|
|
|
|
status_filter = params.get("status")
|
|
if status_filter:
|
|
qs = qs.filter(status=status_filter)
|
|
|
|
return qs
|
|
|
|
def _apply_company_filters(self, qs: QuerySet, params: dict) -> QuerySet:
|
|
"""Apply company-related filtering to the queryset."""
|
|
company_filters = {
|
|
'operator_id': 'operator_id',
|
|
'operator_slug': 'operator__slug',
|
|
'property_owner_id': 'property_owner_id',
|
|
'property_owner_slug': 'property_owner__slug'
|
|
}
|
|
|
|
for param_name, filter_field in company_filters.items():
|
|
value = params.get(param_name)
|
|
if value:
|
|
qs = qs.filter(**{filter_field: value})
|
|
|
|
return qs
|
|
|
|
def _apply_rating_filters(self, qs: QuerySet, params: dict) -> QuerySet:
|
|
"""Apply rating-based filtering to the queryset."""
|
|
min_rating = params.get("min_rating")
|
|
if min_rating:
|
|
try:
|
|
qs = qs.filter(average_rating__gte=float(min_rating))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
max_rating = params.get("max_rating")
|
|
if max_rating:
|
|
try:
|
|
qs = qs.filter(average_rating__lte=float(max_rating))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
return qs
|
|
|
|
def _apply_ride_count_filters(self, qs: QuerySet, params: dict) -> QuerySet:
|
|
"""Apply ride count filtering to the queryset."""
|
|
min_ride_count = params.get("min_ride_count")
|
|
if min_ride_count:
|
|
try:
|
|
qs = qs.filter(ride_count__gte=int(min_ride_count))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
max_ride_count = params.get("max_ride_count")
|
|
if max_ride_count:
|
|
try:
|
|
qs = qs.filter(ride_count__lte=int(max_ride_count))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
return qs
|
|
|
|
def _apply_opening_year_filters(self, qs: QuerySet, params: dict) -> QuerySet:
|
|
"""Apply opening year filtering to the queryset."""
|
|
opening_year = params.get("opening_year")
|
|
if opening_year:
|
|
try:
|
|
qs = qs.filter(opening_date__year=int(opening_year))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
min_opening_year = params.get("min_opening_year")
|
|
if min_opening_year:
|
|
try:
|
|
qs = qs.filter(opening_date__year__gte=int(min_opening_year))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
max_opening_year = params.get("max_opening_year")
|
|
if max_opening_year:
|
|
try:
|
|
qs = qs.filter(opening_date__year__lte=int(max_opening_year))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
return qs
|
|
|
|
def _apply_roller_coaster_filters(self, qs: QuerySet, params: dict) -> QuerySet:
|
|
"""Apply roller coaster filtering to the queryset."""
|
|
has_roller_coasters = params.get("has_roller_coasters")
|
|
if has_roller_coasters is not None:
|
|
if has_roller_coasters.lower() in ['true', '1', 'yes']:
|
|
qs = qs.filter(coaster_count__gt=0)
|
|
elif has_roller_coasters.lower() in ['false', '0', 'no']:
|
|
qs = qs.filter(coaster_count=0)
|
|
|
|
min_roller_coaster_count = params.get("min_roller_coaster_count")
|
|
if min_roller_coaster_count:
|
|
try:
|
|
qs = qs.filter(coaster_count__gte=int(min_roller_coaster_count))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
max_roller_coaster_count = params.get("max_roller_coaster_count")
|
|
if max_roller_coaster_count:
|
|
try:
|
|
qs = qs.filter(coaster_count__lte=int(max_roller_coaster_count))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
return qs
|
|
|
|
@extend_schema(
|
|
summary="Create a new park",
|
|
description="Create a new park.",
|
|
responses={
|
|
201: (
|
|
"ParkDetailOutputSerializer()"
|
|
if SERIALIZERS_AVAILABLE
|
|
else OpenApiTypes.OBJECT
|
|
)
|
|
},
|
|
tags=["Parks"],
|
|
)
|
|
def post(self, request: Request) -> Response:
|
|
"""Create a new park."""
|
|
if not SERIALIZERS_AVAILABLE:
|
|
return Response(
|
|
{
|
|
"detail": "Park creation serializers not available. "
|
|
"Implement park serializers to enable creation."
|
|
},
|
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
)
|
|
|
|
serializer_in = ParkCreateInputSerializer(data=request.data)
|
|
serializer_in.is_valid(raise_exception=True)
|
|
|
|
if not MODELS_AVAILABLE:
|
|
return Response(
|
|
{
|
|
"detail": (
|
|
"Park creation is not available because domain models "
|
|
"are not imported. Implement apps.parks.models.Park "
|
|
"and necessary create logic."
|
|
)
|
|
},
|
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
)
|
|
|
|
validated = serializer_in.validated_data
|
|
|
|
# Minimal create logic using model fields if available.
|
|
park = Park.objects.create( # type: ignore
|
|
name=validated["name"],
|
|
description=validated.get("description", ""),
|
|
# Add other fields as needed based on Park model
|
|
)
|
|
|
|
out_serializer = ParkDetailOutputSerializer(park, context={"request": request})
|
|
return Response(out_serializer.data, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
# --- Park retrieve / update / delete ---------------------------------------
|
|
@extend_schema(
|
|
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, identifier: str) -> Any:
|
|
if not MODELS_AVAILABLE:
|
|
raise NotFound(
|
|
(
|
|
"Park detail is not available because domain models "
|
|
"are not imported. Implement apps.parks.models.Park "
|
|
"to enable detail endpoints."
|
|
)
|
|
)
|
|
|
|
# Try to parse as integer ID first
|
|
try:
|
|
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")
|
|
|
|
@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})
|
|
return Response(serializer.data)
|
|
else:
|
|
# Fallback serialization
|
|
return Response(
|
|
{
|
|
"id": park.id,
|
|
"name": park.name,
|
|
"slug": getattr(park, "slug", ""),
|
|
"description": getattr(park, "description", ""),
|
|
"location": str(getattr(park, "location", "")),
|
|
"operator": (
|
|
getattr(park.operator, "name", "")
|
|
if hasattr(park, "operator")
|
|
else ""
|
|
),
|
|
}
|
|
)
|
|
|
|
def patch(self, request: Request, pk: str) -> Response:
|
|
park = self._get_park_or_404(pk)
|
|
if not SERIALIZERS_AVAILABLE:
|
|
return Response(
|
|
{"detail": "Park update serializers not available."},
|
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
)
|
|
|
|
serializer_in = ParkUpdateInputSerializer(data=request.data, partial=True)
|
|
serializer_in.is_valid(raise_exception=True)
|
|
|
|
if not MODELS_AVAILABLE:
|
|
return Response(
|
|
{
|
|
"detail": (
|
|
"Park update is not available because domain models "
|
|
"are not imported."
|
|
)
|
|
},
|
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
)
|
|
for key, value in serializer_in.validated_data.items():
|
|
setattr(park, key, value)
|
|
park.save()
|
|
serializer = ParkDetailOutputSerializer(park, context={"request": request})
|
|
return Response(serializer.data)
|
|
|
|
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: str) -> Response:
|
|
if not MODELS_AVAILABLE:
|
|
return Response(
|
|
{
|
|
"detail": (
|
|
"Park delete is not available because domain models "
|
|
"are not imported."
|
|
)
|
|
},
|
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
)
|
|
park = self._get_park_or_404(pk)
|
|
park.delete()
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
# --- Filter options ---------------------------------------------------------
|
|
@extend_schema(
|
|
summary="Get comprehensive filter options for parks",
|
|
responses={200: OpenApiTypes.OBJECT},
|
|
tags=["Parks"],
|
|
)
|
|
class FilterOptionsAPIView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request: Request) -> Response:
|
|
"""Return comprehensive filter options with Rich Choice Objects metadata."""
|
|
# Import Rich Choice registry
|
|
from apps.core.choices.registry import get_choices
|
|
|
|
# Always get static choice definitions from Rich Choice Objects (primary source)
|
|
park_types = get_choices('types', 'parks')
|
|
statuses = get_choices('statuses', 'parks')
|
|
|
|
# Convert Rich Choice Objects to frontend format with metadata
|
|
park_types_data = [
|
|
{
|
|
"value": choice.value,
|
|
"label": choice.label,
|
|
"description": choice.description,
|
|
"color": choice.metadata.get('color'),
|
|
"icon": choice.metadata.get('icon'),
|
|
"css_class": choice.metadata.get('css_class'),
|
|
"sort_order": choice.metadata.get('sort_order', 0)
|
|
}
|
|
for choice in park_types
|
|
]
|
|
|
|
statuses_data = [
|
|
{
|
|
"value": choice.value,
|
|
"label": choice.label,
|
|
"description": choice.description,
|
|
"color": choice.metadata.get('color'),
|
|
"icon": choice.metadata.get('icon'),
|
|
"css_class": choice.metadata.get('css_class'),
|
|
"sort_order": choice.metadata.get('sort_order', 0)
|
|
}
|
|
for choice in statuses
|
|
]
|
|
|
|
# Get dynamic data from database if models are available
|
|
if MODELS_AVAILABLE:
|
|
# Add any dynamic data queries here
|
|
pass
|
|
|
|
return Response({
|
|
"park_types": park_types_data,
|
|
"statuses": statuses_data,
|
|
"continents": [
|
|
"North America",
|
|
"South America",
|
|
"Europe",
|
|
"Asia",
|
|
"Africa",
|
|
"Australia",
|
|
"Antarctica"
|
|
],
|
|
"countries": [
|
|
"United States",
|
|
"Canada",
|
|
"United Kingdom",
|
|
"Germany",
|
|
"France",
|
|
"Japan",
|
|
"Australia",
|
|
"Brazil"
|
|
],
|
|
"states": [
|
|
"California",
|
|
"Florida",
|
|
"Ohio",
|
|
"Pennsylvania",
|
|
"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)"},
|
|
{"value": "opening_date", "label": "Opening Date (Oldest First)"},
|
|
{"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": "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 using Rich Choice Objects
|
|
try:
|
|
# Get rich choice objects from registry
|
|
park_types = get_choices('types', 'parks')
|
|
statuses = get_choices('statuses', 'parks')
|
|
|
|
# Convert Rich Choice Objects to frontend format with metadata
|
|
park_types_data = [
|
|
{
|
|
"value": choice.value,
|
|
"label": choice.label,
|
|
"description": choice.description,
|
|
"color": choice.metadata.get('color'),
|
|
"icon": choice.metadata.get('icon'),
|
|
"css_class": choice.metadata.get('css_class'),
|
|
"sort_order": choice.metadata.get('sort_order', 0)
|
|
}
|
|
for choice in park_types
|
|
]
|
|
|
|
statuses_data = [
|
|
{
|
|
"value": choice.value,
|
|
"label": choice.label,
|
|
"description": choice.description,
|
|
"color": choice.metadata.get('color'),
|
|
"icon": choice.metadata.get('icon'),
|
|
"css_class": choice.metadata.get('css_class'),
|
|
"sort_order": choice.metadata.get('sort_order', 0)
|
|
}
|
|
for choice in statuses
|
|
]
|
|
|
|
# Get location data from database
|
|
continents = list(Park.objects.exclude(
|
|
location__continent__isnull=True
|
|
).exclude(
|
|
location__continent__exact=''
|
|
).values_list('location__continent', flat=True).distinct().order_by('location__continent'))
|
|
|
|
# Fallback to static list if no continents in database
|
|
if not continents:
|
|
continents = [
|
|
"North America",
|
|
"South America",
|
|
"Europe",
|
|
"Asia",
|
|
"Africa",
|
|
"Australia",
|
|
"Antarctica"
|
|
]
|
|
|
|
countries = list(Park.objects.exclude(
|
|
location__country__isnull=True
|
|
).exclude(
|
|
location__country__exact=''
|
|
).values_list('location__country', flat=True).distinct().order_by('location__country'))
|
|
|
|
states = list(Park.objects.exclude(
|
|
location__state__isnull=True
|
|
).exclude(
|
|
location__state__exact=''
|
|
).values_list('location__state', flat=True).distinct().order_by('location__state'))
|
|
|
|
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_data,
|
|
"statuses": statuses_data,
|
|
"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)"},
|
|
{"value": "opening_date", "label": "Opening Date (Oldest First)"},
|
|
{"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": "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)"},
|
|
],
|
|
})
|
|
|
|
except Exception:
|
|
# Fallback to static options if database query fails
|
|
return Response({
|
|
"park_types": [
|
|
{"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",
|
|
"South America",
|
|
"Europe",
|
|
"Asia",
|
|
"Africa",
|
|
"Australia"
|
|
],
|
|
"countries": [
|
|
"United States",
|
|
"Canada",
|
|
"United Kingdom",
|
|
"Germany",
|
|
"France",
|
|
"Japan"
|
|
],
|
|
"states": [
|
|
"California",
|
|
"Florida",
|
|
"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)"},
|
|
{"value": "opening_date", "label": "Opening Date (Oldest First)"},
|
|
{"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)"},
|
|
],
|
|
})
|
|
|
|
|
|
# --- Company search (autocomplete) -----------------------------------------
|
|
@extend_schema(
|
|
summary="Search companies (operators/property owners) for autocomplete",
|
|
parameters=[
|
|
OpenApiParameter(
|
|
name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description="Search query for company names"
|
|
)
|
|
],
|
|
responses={200: OpenApiTypes.OBJECT},
|
|
tags=["Parks"],
|
|
)
|
|
class CompanySearchAPIView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request: Request) -> Response:
|
|
q = request.query_params.get("q", "")
|
|
if not q:
|
|
return Response([], status=status.HTTP_200_OK)
|
|
|
|
if not MODELS_AVAILABLE or Company is None:
|
|
# Provide helpful placeholder structure
|
|
return Response([
|
|
{"id": 1, "name": "Six Flags Entertainment", "slug": "six-flags"},
|
|
{"id": 2, "name": "Cedar Fair", "slug": "cedar-fair"},
|
|
{"id": 3, "name": "Disney Parks", "slug": "disney"},
|
|
{"id": 4, "name": "Universal Parks & Resorts", "slug": "universal"},
|
|
{"id": 5, "name": "SeaWorld Parks & Entertainment", "slug": "seaworld"},
|
|
])
|
|
|
|
try:
|
|
# Search companies that can be operators or property owners
|
|
qs = Company.objects.filter(
|
|
Q(name__icontains=q) &
|
|
(Q(roles__contains=['OPERATOR']) | Q(
|
|
roles__contains=['PROPERTY_OWNER']))
|
|
).distinct()[:20]
|
|
|
|
results = [
|
|
{
|
|
"id": c.id,
|
|
"name": c.name,
|
|
"slug": getattr(c, "slug", ""),
|
|
"roles": getattr(c, "roles", [])
|
|
}
|
|
for c in qs
|
|
]
|
|
return Response(results)
|
|
except Exception:
|
|
# Fallback to placeholder data
|
|
return Response([
|
|
{"id": 1, "name": "Six Flags Entertainment", "slug": "six-flags"},
|
|
{"id": 2, "name": "Cedar Fair", "slug": "cedar-fair"},
|
|
{"id": 3, "name": "Disney Parks", "slug": "disney"},
|
|
])
|
|
|
|
|
|
# --- Search suggestions -----------------------------------------------------
|
|
@extend_schema(
|
|
summary="Search suggestions for park search box",
|
|
parameters=[
|
|
OpenApiParameter(
|
|
name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
|
)
|
|
],
|
|
tags=["Parks"],
|
|
)
|
|
class ParkSearchSuggestionsAPIView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request: Request) -> Response:
|
|
q = request.query_params.get("q", "")
|
|
if not q:
|
|
return Response([], status=status.HTTP_200_OK)
|
|
|
|
# Very small suggestion implementation: look in park names if available
|
|
if MODELS_AVAILABLE and Park is not None:
|
|
qs = Park.objects.filter(name__icontains=q).values_list("name", flat=True)[
|
|
:10
|
|
] # type: ignore
|
|
return Response([{"suggestion": name} for name in qs])
|
|
|
|
# Fallback suggestions
|
|
fallback = [
|
|
{"suggestion": f"{q} Park"},
|
|
{"suggestion": f"{q} Theme Park"},
|
|
{"suggestion": f"{q} Amusement Park"},
|
|
]
|
|
return Response(fallback)
|
|
|
|
|
|
# --- Park image settings ---------------------------------------------------
|
|
@extend_schema(
|
|
summary="Set park banner and card images",
|
|
description="Set banner_image and card_image for a park from existing park photos",
|
|
request=(
|
|
"ParkImageSettingsInputSerializer"
|
|
if SERIALIZERS_AVAILABLE
|
|
else OpenApiTypes.OBJECT
|
|
),
|
|
responses={
|
|
200: (
|
|
"ParkDetailOutputSerializer"
|
|
if SERIALIZERS_AVAILABLE
|
|
else OpenApiTypes.OBJECT
|
|
),
|
|
400: OpenApiTypes.OBJECT,
|
|
404: OpenApiTypes.OBJECT,
|
|
},
|
|
tags=["Parks"],
|
|
)
|
|
class ParkImageSettingsAPIView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def _get_park_or_404(self, pk: int) -> Any:
|
|
if not MODELS_AVAILABLE:
|
|
raise NotFound("Park models not available")
|
|
try:
|
|
return Park.objects.get(pk=pk) # type: ignore
|
|
except Park.DoesNotExist: # type: ignore
|
|
raise NotFound("Park not found")
|
|
|
|
def patch(self, request: Request, pk: int) -> Response:
|
|
"""Set banner and card images for the park."""
|
|
if not SERIALIZERS_AVAILABLE:
|
|
return Response(
|
|
{"detail": "Park image settings serializers not available."},
|
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
)
|
|
|
|
park = self._get_park_or_404(pk)
|
|
|
|
serializer = ParkImageSettingsInputSerializer(data=request.data, partial=True)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
# Update the park with the validated data
|
|
for field, value in serializer.validated_data.items():
|
|
setattr(park, field, value)
|
|
|
|
park.save()
|
|
|
|
# Return updated park data
|
|
output_serializer = ParkDetailOutputSerializer(
|
|
park, context={"request": request}
|
|
)
|
|
return Response(output_serializer.data)
|