""" 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)