mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 10:51:09 -05:00
Add comprehensive API documentation for ThrillWiki integration and features
- Introduced Next.js integration guide for ThrillWiki API, detailing authentication, core domain APIs, data structures, and implementation patterns. - Documented the migration to Rich Choice Objects, highlighting changes for frontend developers and enhanced metadata availability. - Fixed the missing `get_by_slug` method in the Ride model, ensuring proper functionality of ride detail endpoints. - Created a test script to verify manufacturer syncing with ride models, ensuring data integrity across related models.
This commit is contained in:
@@ -7,7 +7,7 @@ TypeScript interfaces, providing immediate feedback during development.
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any
|
||||
from django.conf import settings
|
||||
from django.http import JsonResponse
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
||||
306
backend/apps/api/v1/parks/park_rides_views.py
Normal file
306
backend/apps/api/v1/parks/park_rides_views.py
Normal file
@@ -0,0 +1,306 @@
|
||||
"""
|
||||
Park Rides API views for ThrillWiki API v1.
|
||||
|
||||
This module implements endpoints for accessing rides within specific parks:
|
||||
- GET /parks/{park_slug}/rides/ - List rides at a park with pagination and filtering
|
||||
- GET /parks/{park_slug}/rides/{ride_slug}/ - Get specific ride details within park context
|
||||
"""
|
||||
|
||||
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
|
||||
from apps.rides.models import Ride
|
||||
MODELS_AVAILABLE = True
|
||||
except Exception:
|
||||
Park = None # type: ignore
|
||||
Ride = None # type: ignore
|
||||
MODELS_AVAILABLE = False
|
||||
|
||||
# Import serializers
|
||||
try:
|
||||
from apps.api.v1.serializers.rides import RideListOutputSerializer, RideDetailOutputSerializer
|
||||
from apps.api.v1.serializers.parks import ParkDetailOutputSerializer
|
||||
SERIALIZERS_AVAILABLE = True
|
||||
except Exception:
|
||||
SERIALIZERS_AVAILABLE = False
|
||||
|
||||
|
||||
class StandardResultsSetPagination(PageNumberPagination):
|
||||
page_size = 20
|
||||
page_size_query_param = "page_size"
|
||||
max_page_size = 100
|
||||
|
||||
|
||||
class ParkRidesListAPIView(APIView):
|
||||
"""List rides at a specific park with pagination and filtering."""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
summary="List rides at a specific park",
|
||||
description="Get paginated list of rides at a specific park with filtering options",
|
||||
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 (max 100)"),
|
||||
|
||||
# Filtering
|
||||
OpenApiParameter(name="category", location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR, description="Filter by ride category"),
|
||||
OpenApiParameter(name="status", location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR, description="Filter by operational status"),
|
||||
OpenApiParameter(name="search", location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR, description="Search rides by name"),
|
||||
|
||||
# Ordering
|
||||
OpenApiParameter(name="ordering", location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR, description="Order results by field"),
|
||||
],
|
||||
responses={
|
||||
200: OpenApiTypes.OBJECT,
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
tags=["Parks", "Rides"],
|
||||
)
|
||||
def get(self, request: Request, park_slug: str) -> Response:
|
||||
"""List rides at a specific park."""
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{"detail": "Park and ride models not available."},
|
||||
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
)
|
||||
|
||||
# Get the park
|
||||
try:
|
||||
park, is_historical = Park.get_by_slug(park_slug)
|
||||
except Park.DoesNotExist:
|
||||
raise NotFound("Park not found")
|
||||
|
||||
# Get rides for this park
|
||||
qs = Ride.objects.filter(park=park).select_related(
|
||||
"manufacturer", "designer", "ride_model", "park_area"
|
||||
).prefetch_related("photos")
|
||||
|
||||
# Apply 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 = RideListOutputSerializer(
|
||||
page, many=True, context={"request": request, "park": park}
|
||||
)
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
else:
|
||||
# Fallback serialization
|
||||
serializer_data = [
|
||||
{
|
||||
"id": ride.id,
|
||||
"name": ride.name,
|
||||
"slug": ride.slug,
|
||||
"category": getattr(ride, "category", ""),
|
||||
"status": getattr(ride, "status", ""),
|
||||
"manufacturer": {
|
||||
"name": ride.manufacturer.name if ride.manufacturer else "",
|
||||
"slug": getattr(ride.manufacturer, "slug", "") if ride.manufacturer else "",
|
||||
},
|
||||
}
|
||||
for ride in page
|
||||
]
|
||||
return paginator.get_paginated_response(serializer_data)
|
||||
|
||||
def _apply_filters(self, qs: QuerySet, params: dict) -> QuerySet:
|
||||
"""Apply filtering to the rides queryset."""
|
||||
# Category filter
|
||||
category = params.get("category")
|
||||
if category:
|
||||
qs = qs.filter(category=category)
|
||||
|
||||
# Status filter
|
||||
status_filter = params.get("status")
|
||||
if status_filter:
|
||||
qs = qs.filter(status=status_filter)
|
||||
|
||||
# Search filter
|
||||
search = params.get("search")
|
||||
if search:
|
||||
qs = qs.filter(
|
||||
Q(name__icontains=search) |
|
||||
Q(description__icontains=search) |
|
||||
Q(manufacturer__name__icontains=search)
|
||||
)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class ParkRideDetailAPIView(APIView):
|
||||
"""Get specific ride details within park context."""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
summary="Get ride details within park context",
|
||||
description="Get comprehensive details for a specific ride at a specific park",
|
||||
responses={
|
||||
200: OpenApiTypes.OBJECT,
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
tags=["Parks", "Rides"],
|
||||
)
|
||||
def get(self, request: Request, park_slug: str, ride_slug: str) -> Response:
|
||||
"""Get ride details within park context."""
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{"detail": "Park and ride models not available."},
|
||||
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
)
|
||||
|
||||
# Get the park
|
||||
try:
|
||||
park, is_historical = Park.get_by_slug(park_slug)
|
||||
except Park.DoesNotExist:
|
||||
raise NotFound("Park not found")
|
||||
|
||||
# Get the ride
|
||||
try:
|
||||
ride, is_historical = Ride.get_by_slug(ride_slug, park=park)
|
||||
except Ride.DoesNotExist:
|
||||
raise NotFound("Ride not found at this park")
|
||||
|
||||
# Ensure ride belongs to this park
|
||||
if ride.park_id != park.id:
|
||||
raise NotFound("Ride not found at this park")
|
||||
|
||||
if SERIALIZERS_AVAILABLE:
|
||||
serializer = RideDetailOutputSerializer(
|
||||
ride, context={"request": request, "park": park}
|
||||
)
|
||||
return Response(serializer.data)
|
||||
else:
|
||||
# Fallback serialization
|
||||
return Response({
|
||||
"id": ride.id,
|
||||
"name": ride.name,
|
||||
"slug": ride.slug,
|
||||
"description": getattr(ride, "description", ""),
|
||||
"category": getattr(ride, "category", ""),
|
||||
"status": getattr(ride, "status", ""),
|
||||
"park": {
|
||||
"id": park.id,
|
||||
"name": park.name,
|
||||
"slug": park.slug,
|
||||
},
|
||||
"manufacturer": {
|
||||
"name": ride.manufacturer.name if ride.manufacturer else "",
|
||||
"slug": getattr(ride.manufacturer, "slug", "") if ride.manufacturer else "",
|
||||
} if ride.manufacturer else None,
|
||||
})
|
||||
|
||||
|
||||
class ParkComprehensiveDetailAPIView(APIView):
|
||||
"""Get comprehensive park details including summary of rides."""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
summary="Get comprehensive park details with rides summary",
|
||||
description="Get complete park details including a summary of rides (first 10) and link to full rides list",
|
||||
responses={
|
||||
200: OpenApiTypes.OBJECT,
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
tags=["Parks"],
|
||||
)
|
||||
def get(self, request: Request, park_slug: str) -> Response:
|
||||
"""Get comprehensive park details with rides summary."""
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{"detail": "Park and ride models not available."},
|
||||
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
)
|
||||
|
||||
# Get the park
|
||||
try:
|
||||
park, is_historical = Park.get_by_slug(park_slug)
|
||||
except Park.DoesNotExist:
|
||||
raise NotFound("Park not found")
|
||||
|
||||
# Get park with full related data
|
||||
park = Park.objects.select_related(
|
||||
"operator", "property_owner", "location"
|
||||
).prefetch_related(
|
||||
"areas", "rides", "photos"
|
||||
).get(pk=park.pk)
|
||||
|
||||
# Get a sample of rides (first 10) for preview
|
||||
rides_sample = Ride.objects.filter(park=park).select_related(
|
||||
"manufacturer", "designer", "ride_model"
|
||||
)[:10]
|
||||
|
||||
if SERIALIZERS_AVAILABLE:
|
||||
# Get full park details
|
||||
park_serializer = ParkDetailOutputSerializer(
|
||||
park, context={"request": request}
|
||||
)
|
||||
park_data = park_serializer.data
|
||||
|
||||
# Add rides summary
|
||||
rides_serializer = RideListOutputSerializer(
|
||||
rides_sample, many=True, context={"request": request, "park": park}
|
||||
)
|
||||
|
||||
# Enhance response with rides data
|
||||
park_data["rides_summary"] = {
|
||||
"total_count": park.ride_count or 0,
|
||||
"sample": rides_serializer.data,
|
||||
"full_list_url": f"/api/v1/parks/{park_slug}/rides/",
|
||||
}
|
||||
|
||||
return Response(park_data)
|
||||
else:
|
||||
# Fallback serialization
|
||||
return Response({
|
||||
"id": park.id,
|
||||
"name": park.name,
|
||||
"slug": park.slug,
|
||||
"description": getattr(park, "description", ""),
|
||||
"location": str(getattr(park, "location", "")),
|
||||
"operator": getattr(park.operator, "name", "") if hasattr(park, "operator") else "",
|
||||
"ride_count": getattr(park, "ride_count", 0),
|
||||
"rides_summary": {
|
||||
"total_count": getattr(park, "ride_count", 0),
|
||||
"sample": [
|
||||
{
|
||||
"id": ride.id,
|
||||
"name": ride.name,
|
||||
"slug": ride.slug,
|
||||
"category": getattr(ride, "category", ""),
|
||||
}
|
||||
for ride in rides_sample
|
||||
],
|
||||
"full_list_url": f"/api/v1/parks/{park_slug}/rides/",
|
||||
},
|
||||
})
|
||||
@@ -216,8 +216,18 @@ class ParkListCreateAPIView(APIView):
|
||||
|
||||
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
|
||||
|
||||
# Search filter
|
||||
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(
|
||||
@@ -227,53 +237,54 @@ class ParkListCreateAPIView(APIView):
|
||||
Q(location__state__icontains=search) |
|
||||
Q(location__country__icontains=search)
|
||||
)
|
||||
return qs
|
||||
|
||||
# Location filters (only available fields)
|
||||
country = params.get("country")
|
||||
if country:
|
||||
qs = qs.filter(location__country__iexact=country)
|
||||
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
|
||||
|
||||
state = params.get("state")
|
||||
if state:
|
||||
qs = qs.filter(location__state__iexact=state)
|
||||
|
||||
city = params.get("city")
|
||||
if city:
|
||||
qs = qs.filter(location__city__iexact=city)
|
||||
|
||||
# Continent filter (now available field)
|
||||
continent = params.get("continent")
|
||||
if continent:
|
||||
qs = qs.filter(location__continent__iexact=continent)
|
||||
|
||||
# Park type filter (now available field)
|
||||
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 (available field)
|
||||
status_filter = params.get("status")
|
||||
if status_filter:
|
||||
qs = qs.filter(status=status_filter)
|
||||
|
||||
return qs
|
||||
|
||||
# Company filters (available fields)
|
||||
operator_id = params.get("operator_id")
|
||||
if operator_id:
|
||||
qs = qs.filter(operator_id=operator_id)
|
||||
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
|
||||
|
||||
operator_slug = params.get("operator_slug")
|
||||
if operator_slug:
|
||||
qs = qs.filter(operator__slug=operator_slug)
|
||||
|
||||
property_owner_id = params.get("property_owner_id")
|
||||
if property_owner_id:
|
||||
qs = qs.filter(property_owner_id=property_owner_id)
|
||||
|
||||
property_owner_slug = params.get("property_owner_slug")
|
||||
if property_owner_slug:
|
||||
qs = qs.filter(property_owner__slug=property_owner_slug)
|
||||
|
||||
# Rating filters (available field)
|
||||
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:
|
||||
@@ -287,8 +298,11 @@ class ParkListCreateAPIView(APIView):
|
||||
qs = qs.filter(average_rating__lte=float(max_rating))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return qs
|
||||
|
||||
# Ride count filters (available field)
|
||||
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:
|
||||
@@ -302,8 +316,11 @@ class ParkListCreateAPIView(APIView):
|
||||
qs = qs.filter(ride_count__lte=int(max_ride_count))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return qs
|
||||
|
||||
# Opening year filters (available field)
|
||||
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:
|
||||
@@ -324,8 +341,11 @@ class ParkListCreateAPIView(APIView):
|
||||
qs = qs.filter(opening_date__year__lte=int(max_opening_year))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return qs
|
||||
|
||||
# Roller coaster filters (using coaster_count field)
|
||||
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']:
|
||||
@@ -346,7 +366,7 @@ class ParkListCreateAPIView(APIView):
|
||||
qs = qs.filter(coaster_count__lte=int(max_roller_coaster_count))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
|
||||
return qs
|
||||
|
||||
@extend_schema(
|
||||
@@ -575,32 +595,49 @@ class FilterOptionsAPIView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Return comprehensive filter options with all possible park model fields and attributes."""
|
||||
if not MODELS_AVAILABLE:
|
||||
# Fallback comprehensive options with all possible fields
|
||||
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"},
|
||||
],
|
||||
"""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",
|
||||
@@ -665,18 +702,37 @@ class FilterOptionsAPIView(APIView):
|
||||
],
|
||||
})
|
||||
|
||||
# Try to get dynamic options from database
|
||||
# Try to get dynamic options from database using Rich Choice Objects
|
||||
try:
|
||||
# Get all park types from model choices
|
||||
park_types = [
|
||||
{"value": choice[0], "label": choice[1]}
|
||||
for choice in Park.PARK_TYPE_CHOICES
|
||||
# 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
|
||||
]
|
||||
|
||||
# Get all statuses from model choices
|
||||
statuses = [
|
||||
{"value": choice[0], "label": choice[1]}
|
||||
for choice in Park.STATUS_CHOICES
|
||||
|
||||
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
|
||||
@@ -773,8 +829,8 @@ class FilterOptionsAPIView(APIView):
|
||||
}
|
||||
|
||||
return Response({
|
||||
"park_types": park_types,
|
||||
"statuses": statuses,
|
||||
"park_types": park_types_data,
|
||||
"statuses": statuses_data,
|
||||
"continents": continents,
|
||||
"countries": countries,
|
||||
"states": states,
|
||||
|
||||
@@ -17,6 +17,11 @@ from .park_views import (
|
||||
ParkSearchSuggestionsAPIView,
|
||||
ParkImageSettingsAPIView,
|
||||
)
|
||||
from .park_rides_views import (
|
||||
ParkRidesListAPIView,
|
||||
ParkRideDetailAPIView,
|
||||
ParkComprehensiveDetailAPIView,
|
||||
)
|
||||
from .views import ParkPhotoViewSet, HybridParkAPIView, ParkFilterMetadataAPIView
|
||||
|
||||
# Create router for nested photo endpoints
|
||||
@@ -48,6 +53,14 @@ urlpatterns = [
|
||||
),
|
||||
# Detail and action endpoints - supports both ID and slug
|
||||
path("<str:pk>/", ParkDetailAPIView.as_view(), name="park-detail"),
|
||||
|
||||
# Park rides endpoints
|
||||
path("<str:park_slug>/rides/", ParkRidesListAPIView.as_view(), name="park-rides-list"),
|
||||
path("<str:park_slug>/rides/<str:ride_slug>/", ParkRideDetailAPIView.as_view(), name="park-ride-detail"),
|
||||
|
||||
# Comprehensive park detail endpoint with rides summary
|
||||
path("<str:park_slug>/detail/", ParkComprehensiveDetailAPIView.as_view(), name="park-comprehensive-detail"),
|
||||
|
||||
# Park image settings endpoint
|
||||
path(
|
||||
"<int:pk>/image-settings/",
|
||||
|
||||
@@ -483,15 +483,111 @@ class RideModelFilterOptionsAPIView(APIView):
|
||||
tags=["Ride Models"],
|
||||
)
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Return filter options for ride models."""
|
||||
"""Return filter options for ride models with Rich Choice Objects metadata."""
|
||||
# Import Rich Choice registry
|
||||
from apps.core.choices.registry import get_choices
|
||||
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{
|
||||
"categories": [("RC", "Roller Coaster"), ("FR", "Flat Ride")],
|
||||
"target_markets": [("THRILL", "Thrill"), ("FAMILY", "Family")],
|
||||
"manufacturers": [{"id": 1, "name": "Bolliger & Mabillard"}],
|
||||
}
|
||||
)
|
||||
# Use Rich Choice Objects for fallback options
|
||||
try:
|
||||
# Get rich choice objects from registry
|
||||
categories = get_choices('categories', 'rides')
|
||||
target_markets = get_choices('target_markets', 'rides')
|
||||
|
||||
# Convert Rich Choice Objects to frontend format with metadata
|
||||
categories_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 categories
|
||||
]
|
||||
|
||||
target_markets_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 target_markets
|
||||
]
|
||||
|
||||
except Exception:
|
||||
# Ultimate fallback with basic structure
|
||||
categories_data = [
|
||||
{"value": "RC", "label": "Roller Coaster", "description": "High-speed thrill rides with tracks", "color": "red", "icon": "roller-coaster", "css_class": "bg-red-100 text-red-800", "sort_order": 1},
|
||||
{"value": "DR", "label": "Dark Ride", "description": "Indoor themed experiences", "color": "purple", "icon": "dark-ride", "css_class": "bg-purple-100 text-purple-800", "sort_order": 2},
|
||||
{"value": "FR", "label": "Flat Ride", "description": "Spinning and rotating attractions", "color": "blue", "icon": "flat-ride", "css_class": "bg-blue-100 text-blue-800", "sort_order": 3},
|
||||
{"value": "WR", "label": "Water Ride", "description": "Water-based attractions and slides", "color": "cyan", "icon": "water-ride", "css_class": "bg-cyan-100 text-cyan-800", "sort_order": 4},
|
||||
{"value": "TR", "label": "Transport", "description": "Transportation systems within parks", "color": "green", "icon": "transport", "css_class": "bg-green-100 text-green-800", "sort_order": 5},
|
||||
{"value": "OT", "label": "Other", "description": "Miscellaneous attractions", "color": "gray", "icon": "other", "css_class": "bg-gray-100 text-gray-800", "sort_order": 6},
|
||||
]
|
||||
target_markets_data = [
|
||||
{"value": "FAMILY", "label": "Family", "description": "Suitable for all family members", "color": "green", "icon": "family", "css_class": "bg-green-100 text-green-800", "sort_order": 1},
|
||||
{"value": "THRILL", "label": "Thrill", "description": "High-intensity thrill experience", "color": "orange", "icon": "thrill", "css_class": "bg-orange-100 text-orange-800", "sort_order": 2},
|
||||
{"value": "EXTREME", "label": "Extreme", "description": "Maximum intensity experience", "color": "red", "icon": "extreme", "css_class": "bg-red-100 text-red-800", "sort_order": 3},
|
||||
{"value": "KIDDIE", "label": "Kiddie", "description": "Designed for young children", "color": "pink", "icon": "kiddie", "css_class": "bg-pink-100 text-pink-800", "sort_order": 4},
|
||||
{"value": "ALL_AGES", "label": "All Ages", "description": "Enjoyable for all age groups", "color": "blue", "icon": "all-ages", "css_class": "bg-blue-100 text-blue-800", "sort_order": 5},
|
||||
]
|
||||
|
||||
return Response({
|
||||
"categories": categories_data,
|
||||
"target_markets": target_markets_data,
|
||||
"manufacturers": [{"id": 1, "name": "Bolliger & Mabillard", "slug": "bolliger-mabillard"}],
|
||||
"ordering_options": [
|
||||
{"value": "name", "label": "Name A-Z"},
|
||||
{"value": "-name", "label": "Name Z-A"},
|
||||
{"value": "manufacturer__name", "label": "Manufacturer A-Z"},
|
||||
{"value": "-manufacturer__name", "label": "Manufacturer Z-A"},
|
||||
{"value": "first_installation_year", "label": "Oldest First"},
|
||||
{"value": "-first_installation_year", "label": "Newest First"},
|
||||
{"value": "total_installations", "label": "Fewest Installations"},
|
||||
{"value": "-total_installations", "label": "Most Installations"},
|
||||
],
|
||||
})
|
||||
|
||||
# Get static choice definitions from Rich Choice Objects (primary source)
|
||||
# Get dynamic data from database queries
|
||||
|
||||
# Get rich choice objects from registry
|
||||
categories = get_choices('categories', 'rides')
|
||||
target_markets = get_choices('target_markets', 'rides')
|
||||
|
||||
# Convert Rich Choice Objects to frontend format with metadata
|
||||
categories_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 categories
|
||||
]
|
||||
|
||||
target_markets_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 target_markets
|
||||
]
|
||||
|
||||
# Get actual data from database
|
||||
manufacturers = (
|
||||
@@ -502,48 +598,22 @@ class RideModelFilterOptionsAPIView(APIView):
|
||||
.values("id", "name", "slug")
|
||||
)
|
||||
|
||||
(
|
||||
RideModel.objects.exclude(category="")
|
||||
.values_list("category", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
return Response({
|
||||
"categories": categories_data,
|
||||
"target_markets": target_markets_data,
|
||||
"manufacturers": list(manufacturers),
|
||||
"ordering_options": [
|
||||
{"value": "name", "label": "Name A-Z"},
|
||||
{"value": "-name", "label": "Name Z-A"},
|
||||
{"value": "manufacturer__name", "label": "Manufacturer A-Z"},
|
||||
{"value": "-manufacturer__name", "label": "Manufacturer Z-A"},
|
||||
{"value": "first_installation_year", "label": "Oldest First"},
|
||||
{"value": "-first_installation_year", "label": "Newest First"},
|
||||
{"value": "total_installations", "label": "Fewest Installations"},
|
||||
{"value": "-total_installations", "label": "Most Installations"},
|
||||
],
|
||||
})
|
||||
|
||||
(
|
||||
RideModel.objects.exclude(target_market="")
|
||||
.values_list("target_market", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"categories": [
|
||||
("RC", "Roller Coaster"),
|
||||
("DR", "Dark Ride"),
|
||||
("FR", "Flat Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("TR", "Transport"),
|
||||
("OT", "Other"),
|
||||
],
|
||||
"target_markets": [
|
||||
("FAMILY", "Family"),
|
||||
("THRILL", "Thrill"),
|
||||
("EXTREME", "Extreme"),
|
||||
("KIDDIE", "Kiddie"),
|
||||
("ALL_AGES", "All Ages"),
|
||||
],
|
||||
"manufacturers": list(manufacturers),
|
||||
"ordering_options": [
|
||||
("name", "Name A-Z"),
|
||||
("-name", "Name Z-A"),
|
||||
("manufacturer__name", "Manufacturer A-Z"),
|
||||
("-manufacturer__name", "Manufacturer Z-A"),
|
||||
("first_installation_year", "Oldest First"),
|
||||
("-first_installation_year", "Newest First"),
|
||||
("total_installations", "Fewest Installations"),
|
||||
("-total_installations", "Most Installations"),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# === RIDE MODEL STATISTICS ===
|
||||
|
||||
@@ -13,7 +13,7 @@ Notes:
|
||||
are not present, they return a clear 501 response explaining what to wire up.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
@@ -38,7 +38,6 @@ from apps.api.v1.serializers.rides import (
|
||||
)
|
||||
|
||||
# Import hybrid filtering components
|
||||
from apps.api.v1.rides.serializers import HybridRideSerializer
|
||||
from apps.rides.services.hybrid_loader import SmartRideLoader
|
||||
|
||||
# Create smart loader instance
|
||||
@@ -47,12 +46,14 @@ smart_ride_loader = SmartRideLoader()
|
||||
# Attempt to import model-level helpers; fall back gracefully if not present.
|
||||
try:
|
||||
from apps.rides.models import Ride, RideModel
|
||||
from apps.rides.models.rides import RollerCoasterStats
|
||||
from apps.parks.models import Park, Company
|
||||
|
||||
MODELS_AVAILABLE = True
|
||||
except Exception:
|
||||
Ride = None # type: ignore
|
||||
RideModel = None # type: ignore
|
||||
RollerCoasterStats = None # type: ignore
|
||||
Company = None # type: ignore
|
||||
Park = None # type: ignore
|
||||
MODELS_AVAILABLE = False
|
||||
@@ -307,181 +308,233 @@ class RideListCreateAPIView(APIView):
|
||||
.prefetch_related("coaster_stats")
|
||||
) # type: ignore
|
||||
|
||||
# Text search
|
||||
search = request.query_params.get("search")
|
||||
# Apply comprehensive filtering
|
||||
qs = self._apply_filters(qs, request.query_params)
|
||||
|
||||
# Apply ordering
|
||||
qs = self._apply_ordering(qs, request.query_params)
|
||||
|
||||
paginator = StandardResultsSetPagination()
|
||||
page = paginator.paginate_queryset(qs, request)
|
||||
serializer = RideListOutputSerializer(
|
||||
page, many=True, context={"request": request}
|
||||
)
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
def _apply_filters(self, qs, params):
|
||||
"""Apply all filtering to the queryset."""
|
||||
qs = self._apply_search_filters(qs, params)
|
||||
qs = self._apply_park_filters(qs, params)
|
||||
qs = self._apply_category_status_filters(qs, params)
|
||||
qs = self._apply_company_filters(qs, params)
|
||||
qs = self._apply_ride_model_filters(qs, params)
|
||||
qs = self._apply_rating_filters(qs, params)
|
||||
qs = self._apply_height_requirement_filters(qs, params)
|
||||
qs = self._apply_capacity_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, params):
|
||||
"""Apply text search filtering."""
|
||||
search = params.get("search")
|
||||
if search:
|
||||
qs = qs.filter(
|
||||
models.Q(name__icontains=search)
|
||||
| models.Q(description__icontains=search)
|
||||
| models.Q(park__name__icontains=search)
|
||||
)
|
||||
return qs
|
||||
|
||||
# Park filters
|
||||
park_slug = request.query_params.get("park_slug")
|
||||
def _apply_park_filters(self, qs, params):
|
||||
"""Apply park-related filtering."""
|
||||
park_slug = params.get("park_slug")
|
||||
if park_slug:
|
||||
qs = qs.filter(park__slug=park_slug)
|
||||
|
||||
park_id = request.query_params.get("park_id")
|
||||
park_id = params.get("park_id")
|
||||
if park_id:
|
||||
try:
|
||||
qs = qs.filter(park_id=int(park_id))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return qs
|
||||
|
||||
# Category filters (multiple values supported)
|
||||
categories = request.query_params.getlist("category")
|
||||
def _apply_category_status_filters(self, qs, params):
|
||||
"""Apply category and status filtering."""
|
||||
categories = params.getlist("category")
|
||||
if categories:
|
||||
qs = qs.filter(category__in=categories)
|
||||
|
||||
# Status filters (multiple values supported)
|
||||
statuses = request.query_params.getlist("status")
|
||||
statuses = params.getlist("status")
|
||||
if statuses:
|
||||
qs = qs.filter(status__in=statuses)
|
||||
|
||||
return qs
|
||||
|
||||
# Manufacturer filters
|
||||
manufacturer_id = request.query_params.get("manufacturer_id")
|
||||
def _apply_company_filters(self, qs, params):
|
||||
"""Apply manufacturer and designer filtering."""
|
||||
manufacturer_id = params.get("manufacturer_id")
|
||||
if manufacturer_id:
|
||||
try:
|
||||
qs = qs.filter(manufacturer_id=int(manufacturer_id))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
manufacturer_slug = request.query_params.get("manufacturer_slug")
|
||||
manufacturer_slug = params.get("manufacturer_slug")
|
||||
if manufacturer_slug:
|
||||
qs = qs.filter(manufacturer__slug=manufacturer_slug)
|
||||
|
||||
# Designer filters
|
||||
designer_id = request.query_params.get("designer_id")
|
||||
designer_id = params.get("designer_id")
|
||||
if designer_id:
|
||||
try:
|
||||
qs = qs.filter(designer_id=int(designer_id))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
designer_slug = request.query_params.get("designer_slug")
|
||||
designer_slug = params.get("designer_slug")
|
||||
if designer_slug:
|
||||
qs = qs.filter(designer__slug=designer_slug)
|
||||
|
||||
return qs
|
||||
|
||||
# Ride model filters
|
||||
ride_model_id = request.query_params.get("ride_model_id")
|
||||
def _apply_ride_model_filters(self, qs, params):
|
||||
"""Apply ride model filtering."""
|
||||
ride_model_id = params.get("ride_model_id")
|
||||
if ride_model_id:
|
||||
try:
|
||||
qs = qs.filter(ride_model_id=int(ride_model_id))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
ride_model_slug = request.query_params.get("ride_model_slug")
|
||||
manufacturer_slug_for_model = request.query_params.get("manufacturer_slug")
|
||||
ride_model_slug = params.get("ride_model_slug")
|
||||
manufacturer_slug_for_model = params.get("manufacturer_slug")
|
||||
if ride_model_slug and manufacturer_slug_for_model:
|
||||
qs = qs.filter(
|
||||
ride_model__slug=ride_model_slug,
|
||||
ride_model__manufacturer__slug=manufacturer_slug_for_model,
|
||||
)
|
||||
|
||||
return qs
|
||||
|
||||
# Rating filters
|
||||
min_rating = request.query_params.get("min_rating")
|
||||
def _apply_rating_filters(self, qs, params):
|
||||
"""Apply rating-based filtering."""
|
||||
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 = request.query_params.get("max_rating")
|
||||
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
|
||||
|
||||
# Height requirement filters
|
||||
min_height_req = request.query_params.get("min_height_requirement")
|
||||
def _apply_height_requirement_filters(self, qs, params):
|
||||
"""Apply height requirement filtering."""
|
||||
min_height_req = params.get("min_height_requirement")
|
||||
if min_height_req:
|
||||
try:
|
||||
qs = qs.filter(min_height_in__gte=int(min_height_req))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
max_height_req = request.query_params.get("max_height_requirement")
|
||||
max_height_req = params.get("max_height_requirement")
|
||||
if max_height_req:
|
||||
try:
|
||||
qs = qs.filter(max_height_in__lte=int(max_height_req))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return qs
|
||||
|
||||
# Capacity filters
|
||||
min_capacity = request.query_params.get("min_capacity")
|
||||
def _apply_capacity_filters(self, qs, params):
|
||||
"""Apply capacity filtering."""
|
||||
min_capacity = params.get("min_capacity")
|
||||
if min_capacity:
|
||||
try:
|
||||
qs = qs.filter(capacity_per_hour__gte=int(min_capacity))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
max_capacity = request.query_params.get("max_capacity")
|
||||
max_capacity = params.get("max_capacity")
|
||||
if max_capacity:
|
||||
try:
|
||||
qs = qs.filter(capacity_per_hour__lte=int(max_capacity))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return qs
|
||||
|
||||
# Opening year filters
|
||||
opening_year = request.query_params.get("opening_year")
|
||||
def _apply_opening_year_filters(self, qs, params):
|
||||
"""Apply opening year filtering."""
|
||||
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 = request.query_params.get("min_opening_year")
|
||||
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 = request.query_params.get("max_opening_year")
|
||||
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
|
||||
|
||||
# Roller coaster specific filters
|
||||
roller_coaster_type = request.query_params.get("roller_coaster_type")
|
||||
def _apply_roller_coaster_filters(self, qs, params):
|
||||
"""Apply roller coaster specific filtering."""
|
||||
roller_coaster_type = params.get("roller_coaster_type")
|
||||
if roller_coaster_type:
|
||||
qs = qs.filter(coaster_stats__roller_coaster_type=roller_coaster_type)
|
||||
|
||||
track_material = request.query_params.get("track_material")
|
||||
track_material = params.get("track_material")
|
||||
if track_material:
|
||||
qs = qs.filter(coaster_stats__track_material=track_material)
|
||||
|
||||
launch_type = request.query_params.get("launch_type")
|
||||
launch_type = params.get("launch_type")
|
||||
if launch_type:
|
||||
qs = qs.filter(coaster_stats__launch_type=launch_type)
|
||||
|
||||
# Roller coaster height filters
|
||||
min_height_ft = request.query_params.get("min_height_ft")
|
||||
# Height filters
|
||||
min_height_ft = params.get("min_height_ft")
|
||||
if min_height_ft:
|
||||
try:
|
||||
qs = qs.filter(coaster_stats__height_ft__gte=float(min_height_ft))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
max_height_ft = request.query_params.get("max_height_ft")
|
||||
max_height_ft = params.get("max_height_ft")
|
||||
if max_height_ft:
|
||||
try:
|
||||
qs = qs.filter(coaster_stats__height_ft__lte=float(max_height_ft))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Roller coaster speed filters
|
||||
min_speed_mph = request.query_params.get("min_speed_mph")
|
||||
# Speed filters
|
||||
min_speed_mph = params.get("min_speed_mph")
|
||||
if min_speed_mph:
|
||||
try:
|
||||
qs = qs.filter(coaster_stats__speed_mph__gte=float(min_speed_mph))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
max_speed_mph = request.query_params.get("max_speed_mph")
|
||||
max_speed_mph = params.get("max_speed_mph")
|
||||
if max_speed_mph:
|
||||
try:
|
||||
qs = qs.filter(coaster_stats__speed_mph__lte=float(max_speed_mph))
|
||||
@@ -489,29 +542,32 @@ class RideListCreateAPIView(APIView):
|
||||
pass
|
||||
|
||||
# Inversion filters
|
||||
min_inversions = request.query_params.get("min_inversions")
|
||||
min_inversions = params.get("min_inversions")
|
||||
if min_inversions:
|
||||
try:
|
||||
qs = qs.filter(coaster_stats__inversions__gte=int(min_inversions))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
max_inversions = request.query_params.get("max_inversions")
|
||||
max_inversions = params.get("max_inversions")
|
||||
if max_inversions:
|
||||
try:
|
||||
qs = qs.filter(coaster_stats__inversions__lte=int(max_inversions))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
has_inversions = request.query_params.get("has_inversions")
|
||||
has_inversions = params.get("has_inversions")
|
||||
if has_inversions is not None:
|
||||
if has_inversions.lower() in ["true", "1", "yes"]:
|
||||
qs = qs.filter(coaster_stats__inversions__gt=0)
|
||||
elif has_inversions.lower() in ["false", "0", "no"]:
|
||||
qs = qs.filter(coaster_stats__inversions=0)
|
||||
|
||||
return qs
|
||||
|
||||
# Ordering
|
||||
ordering = request.query_params.get("ordering", "name")
|
||||
def _apply_ordering(self, qs, params):
|
||||
"""Apply ordering to the queryset."""
|
||||
ordering = params.get("ordering", "name")
|
||||
valid_orderings = [
|
||||
"name",
|
||||
"-name",
|
||||
@@ -538,13 +594,8 @@ class RideListCreateAPIView(APIView):
|
||||
qs = qs.order_by(ordering_field)
|
||||
else:
|
||||
qs = qs.order_by(ordering)
|
||||
|
||||
paginator = StandardResultsSetPagination()
|
||||
page = paginator.paginate_queryset(qs, request)
|
||||
serializer = RideListOutputSerializer(
|
||||
page, many=True, context={"request": request}
|
||||
)
|
||||
return paginator.get_paginated_response(serializer.data)
|
||||
|
||||
return qs
|
||||
|
||||
@extend_schema(
|
||||
summary="Create a new ride",
|
||||
@@ -698,28 +749,169 @@ class FilterOptionsAPIView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Return comprehensive filter options with all possible ride model fields and attributes."""
|
||||
"""Return comprehensive filter options with Rich Choice Objects metadata."""
|
||||
# Import Rich Choice registry
|
||||
from apps.core.choices.registry import get_choices
|
||||
|
||||
if not MODELS_AVAILABLE:
|
||||
# Comprehensive fallback options with all possible fields
|
||||
# Use Rich Choice Objects for fallback options
|
||||
try:
|
||||
# Get rich choice objects from registry
|
||||
categories = get_choices('categories', 'rides')
|
||||
statuses = get_choices('statuses', 'rides')
|
||||
post_closing_statuses = get_choices('post_closing_statuses', 'rides')
|
||||
track_materials = get_choices('track_materials', 'rides')
|
||||
coaster_types = get_choices('coaster_types', 'rides')
|
||||
launch_systems = get_choices('launch_systems', 'rides')
|
||||
target_markets = get_choices('target_markets', 'rides')
|
||||
|
||||
# Convert Rich Choice Objects to frontend format with metadata
|
||||
categories_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 categories
|
||||
]
|
||||
|
||||
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
|
||||
]
|
||||
|
||||
post_closing_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 post_closing_statuses
|
||||
]
|
||||
|
||||
track_materials_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 track_materials
|
||||
]
|
||||
|
||||
coaster_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 coaster_types
|
||||
]
|
||||
|
||||
launch_systems_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 launch_systems
|
||||
]
|
||||
|
||||
target_markets_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 target_markets
|
||||
]
|
||||
|
||||
except Exception:
|
||||
# Ultimate fallback with basic structure
|
||||
categories_data = [
|
||||
{"value": "RC", "label": "Roller Coaster", "description": "High-speed thrill rides with tracks", "color": "red", "icon": "roller-coaster", "css_class": "bg-red-100 text-red-800", "sort_order": 1},
|
||||
{"value": "DR", "label": "Dark Ride", "description": "Indoor themed experiences", "color": "purple", "icon": "dark-ride", "css_class": "bg-purple-100 text-purple-800", "sort_order": 2},
|
||||
{"value": "FR", "label": "Flat Ride", "description": "Spinning and rotating attractions", "color": "blue", "icon": "flat-ride", "css_class": "bg-blue-100 text-blue-800", "sort_order": 3},
|
||||
{"value": "WR", "label": "Water Ride", "description": "Water-based attractions and slides", "color": "cyan", "icon": "water-ride", "css_class": "bg-cyan-100 text-cyan-800", "sort_order": 4},
|
||||
{"value": "TR", "label": "Transport", "description": "Transportation systems within parks", "color": "green", "icon": "transport", "css_class": "bg-green-100 text-green-800", "sort_order": 5},
|
||||
{"value": "OT", "label": "Other", "description": "Miscellaneous attractions", "color": "gray", "icon": "other", "css_class": "bg-gray-100 text-gray-800", "sort_order": 6},
|
||||
]
|
||||
statuses_data = [
|
||||
{"value": "OPERATING", "label": "Operating", "description": "Ride is currently open and operating", "color": "green", "icon": "check-circle", "css_class": "bg-green-100 text-green-800", "sort_order": 1},
|
||||
{"value": "CLOSED_TEMP", "label": "Temporarily Closed", "description": "Ride is temporarily closed for maintenance", "color": "yellow", "icon": "pause-circle", "css_class": "bg-yellow-100 text-yellow-800", "sort_order": 2},
|
||||
{"value": "SBNO", "label": "Standing But Not Operating", "description": "Ride exists but is not operational", "color": "orange", "icon": "stop-circle", "css_class": "bg-orange-100 text-orange-800", "sort_order": 3},
|
||||
{"value": "CLOSING", "label": "Closing", "description": "Ride is scheduled to close permanently", "color": "red", "icon": "x-circle", "css_class": "bg-red-100 text-red-800", "sort_order": 4},
|
||||
{"value": "CLOSED_PERM", "label": "Permanently Closed", "description": "Ride has been permanently closed", "color": "red", "icon": "x-circle", "css_class": "bg-red-100 text-red-800", "sort_order": 5},
|
||||
{"value": "UNDER_CONSTRUCTION", "label": "Under Construction", "description": "Ride is currently being built", "color": "blue", "icon": "tool", "css_class": "bg-blue-100 text-blue-800", "sort_order": 6},
|
||||
{"value": "DEMOLISHED", "label": "Demolished", "description": "Ride has been completely removed", "color": "gray", "icon": "trash", "css_class": "bg-gray-100 text-gray-800", "sort_order": 7},
|
||||
{"value": "RELOCATED", "label": "Relocated", "description": "Ride has been moved to another location", "color": "purple", "icon": "arrow-right", "css_class": "bg-purple-100 text-purple-800", "sort_order": 8},
|
||||
]
|
||||
post_closing_statuses_data = [
|
||||
{"value": "SBNO", "label": "Standing But Not Operating", "description": "Ride exists but is not operational", "color": "orange", "icon": "stop-circle", "css_class": "bg-orange-100 text-orange-800", "sort_order": 1},
|
||||
{"value": "CLOSED_PERM", "label": "Permanently Closed", "description": "Ride has been permanently closed", "color": "red", "icon": "x-circle", "css_class": "bg-red-100 text-red-800", "sort_order": 2},
|
||||
]
|
||||
track_materials_data = [
|
||||
{"value": "STEEL", "label": "Steel", "description": "Modern steel track construction", "color": "gray", "icon": "steel", "css_class": "bg-gray-100 text-gray-800", "sort_order": 1},
|
||||
{"value": "WOOD", "label": "Wood", "description": "Traditional wooden track construction", "color": "amber", "icon": "wood", "css_class": "bg-amber-100 text-amber-800", "sort_order": 2},
|
||||
{"value": "HYBRID", "label": "Hybrid", "description": "Steel track on wooden structure", "color": "orange", "icon": "hybrid", "css_class": "bg-orange-100 text-orange-800", "sort_order": 3},
|
||||
]
|
||||
coaster_types_data = [
|
||||
{"value": "SITDOWN", "label": "Sit Down", "description": "Traditional seated roller coaster", "color": "blue", "icon": "sitdown", "css_class": "bg-blue-100 text-blue-800", "sort_order": 1},
|
||||
{"value": "INVERTED", "label": "Inverted", "description": "Track above riders, feet dangle", "color": "purple", "icon": "inverted", "css_class": "bg-purple-100 text-purple-800", "sort_order": 2},
|
||||
{"value": "FLYING", "label": "Flying", "description": "Riders positioned face-down", "color": "sky", "icon": "flying", "css_class": "bg-sky-100 text-sky-800", "sort_order": 3},
|
||||
{"value": "STANDUP", "label": "Stand Up", "description": "Riders stand during the ride", "color": "green", "icon": "standup", "css_class": "bg-green-100 text-green-800", "sort_order": 4},
|
||||
{"value": "WING", "label": "Wing", "description": "Seats extend beyond track sides", "color": "indigo", "icon": "wing", "css_class": "bg-indigo-100 text-indigo-800", "sort_order": 5},
|
||||
{"value": "DIVE", "label": "Dive", "description": "Features steep vertical drops", "color": "red", "icon": "dive", "css_class": "bg-red-100 text-red-800", "sort_order": 6},
|
||||
]
|
||||
launch_systems_data = [
|
||||
{"value": "CHAIN", "label": "Chain Lift", "description": "Traditional chain lift hill", "color": "gray", "icon": "chain", "css_class": "bg-gray-100 text-gray-800", "sort_order": 1},
|
||||
{"value": "LSM", "label": "LSM Launch", "description": "Linear synchronous motor launch", "color": "blue", "icon": "lightning", "css_class": "bg-blue-100 text-blue-800", "sort_order": 2},
|
||||
{"value": "HYDRAULIC", "label": "Hydraulic Launch", "description": "High-pressure hydraulic launch", "color": "red", "icon": "hydraulic", "css_class": "bg-red-100 text-red-800", "sort_order": 3},
|
||||
{"value": "GRAVITY", "label": "Gravity", "description": "Gravity-powered ride", "color": "green", "icon": "gravity", "css_class": "bg-green-100 text-green-800", "sort_order": 4},
|
||||
]
|
||||
target_markets_data = [
|
||||
{"value": "FAMILY", "label": "Family", "description": "Suitable for all family members", "color": "green", "icon": "family", "css_class": "bg-green-100 text-green-800", "sort_order": 1},
|
||||
{"value": "THRILL", "label": "Thrill", "description": "High-intensity thrill experience", "color": "orange", "icon": "thrill", "css_class": "bg-orange-100 text-orange-800", "sort_order": 2},
|
||||
{"value": "EXTREME", "label": "Extreme", "description": "Maximum intensity experience", "color": "red", "icon": "extreme", "css_class": "bg-red-100 text-red-800", "sort_order": 3},
|
||||
{"value": "KIDDIE", "label": "Kiddie", "description": "Designed for young children", "color": "pink", "icon": "kiddie", "css_class": "bg-pink-100 text-pink-800", "sort_order": 4},
|
||||
{"value": "ALL_AGES", "label": "All Ages", "description": "Enjoyable for all age groups", "color": "blue", "icon": "all-ages", "css_class": "bg-blue-100 text-blue-800", "sort_order": 5},
|
||||
]
|
||||
|
||||
# Comprehensive fallback options with Rich Choice Objects metadata
|
||||
return Response({
|
||||
"categories": [
|
||||
{"value": "RC", "label": "Roller Coaster"},
|
||||
{"value": "DR", "label": "Dark Ride"},
|
||||
{"value": "FR", "label": "Flat Ride"},
|
||||
{"value": "WR", "label": "Water Ride"},
|
||||
{"value": "TR", "label": "Transport"},
|
||||
{"value": "OT", "label": "Other"},
|
||||
],
|
||||
"statuses": [
|
||||
{"value": "OPERATING", "label": "Operating"},
|
||||
{"value": "CLOSED_TEMP", "label": "Temporarily Closed"},
|
||||
{"value": "SBNO", "label": "Standing But Not Operating"},
|
||||
{"value": "CLOSING", "label": "Closing"},
|
||||
{"value": "CLOSED_PERM", "label": "Permanently Closed"},
|
||||
{"value": "UNDER_CONSTRUCTION", "label": "Under Construction"},
|
||||
{"value": "DEMOLISHED", "label": "Demolished"},
|
||||
{"value": "RELOCATED", "label": "Relocated"},
|
||||
],
|
||||
"categories": categories_data,
|
||||
"statuses": statuses_data,
|
||||
"post_closing_statuses": [
|
||||
{"value": "SBNO", "label": "Standing But Not Operating"},
|
||||
{"value": "CLOSED_PERM", "label": "Permanently Closed"},
|
||||
@@ -818,119 +1010,178 @@ class FilterOptionsAPIView(APIView):
|
||||
],
|
||||
})
|
||||
|
||||
# Try to get dynamic options from database
|
||||
try:
|
||||
# Get all ride categories from model choices
|
||||
categories = [
|
||||
{"value": choice[0], "label": choice[1]}
|
||||
for choice in Ride.CATEGORY_CHOICES if choice[0] # Skip empty choice
|
||||
]
|
||||
# Get static choice definitions from Rich Choice Objects (primary source)
|
||||
# Get dynamic data from database queries
|
||||
|
||||
# Get rich choice objects from registry
|
||||
categories = get_choices('categories', 'rides')
|
||||
statuses = get_choices('statuses', 'rides')
|
||||
post_closing_statuses = get_choices('post_closing_statuses', 'rides')
|
||||
track_materials = get_choices('track_materials', 'rides')
|
||||
coaster_types = get_choices('coaster_types', 'rides')
|
||||
launch_systems = get_choices('launch_systems', 'rides')
|
||||
target_markets = get_choices('target_markets', 'rides')
|
||||
|
||||
# Convert Rich Choice Objects to frontend format with metadata
|
||||
categories_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 categories
|
||||
]
|
||||
|
||||
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
|
||||
]
|
||||
|
||||
post_closing_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 post_closing_statuses
|
||||
]
|
||||
|
||||
track_materials_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 track_materials
|
||||
]
|
||||
|
||||
coaster_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 coaster_types
|
||||
]
|
||||
|
||||
launch_systems_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 launch_systems
|
||||
]
|
||||
|
||||
target_markets_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 target_markets
|
||||
]
|
||||
|
||||
# Get all ride statuses from model choices
|
||||
statuses = [
|
||||
{"value": choice[0], "label": choice[1]}
|
||||
for choice in Ride.STATUS_CHOICES if choice[0] # Skip empty choice
|
||||
]
|
||||
# Get parks data from database
|
||||
parks = list(Ride.objects.exclude(
|
||||
park__isnull=True
|
||||
).select_related('park').values(
|
||||
'park__id', 'park__name', 'park__slug'
|
||||
).distinct().order_by('park__name'))
|
||||
|
||||
# Get post-closing statuses from model choices
|
||||
post_closing_statuses = [
|
||||
{"value": choice[0], "label": choice[1]}
|
||||
for choice in Ride.POST_CLOSING_STATUS_CHOICES
|
||||
]
|
||||
# Get park areas data from database
|
||||
park_areas = list(Ride.objects.exclude(
|
||||
park_area__isnull=True
|
||||
).select_related('park_area').values(
|
||||
'park_area__id', 'park_area__name', 'park_area__slug'
|
||||
).distinct().order_by('park_area__name'))
|
||||
|
||||
# Get roller coaster types from model choices
|
||||
from apps.rides.models.rides import RollerCoasterStats
|
||||
roller_coaster_types = [
|
||||
{"value": choice[0], "label": choice[1]}
|
||||
for choice in RollerCoasterStats.COASTER_TYPE_CHOICES
|
||||
]
|
||||
# Get manufacturers (companies with MANUFACTURER role)
|
||||
manufacturers = list(Company.objects.filter(
|
||||
roles__contains=['MANUFACTURER']
|
||||
).values('id', 'name', 'slug').order_by('name'))
|
||||
|
||||
# Get track materials from model choices
|
||||
track_materials = [
|
||||
{"value": choice[0], "label": choice[1]}
|
||||
for choice in RollerCoasterStats.TRACK_MATERIAL_CHOICES
|
||||
]
|
||||
# Get designers (companies with DESIGNER role)
|
||||
designers = list(Company.objects.filter(
|
||||
roles__contains=['DESIGNER']
|
||||
).values('id', 'name', 'slug').order_by('name'))
|
||||
|
||||
# Get launch types from model choices
|
||||
launch_types = [
|
||||
{"value": choice[0], "label": choice[1]}
|
||||
for choice in RollerCoasterStats.LAUNCH_CHOICES
|
||||
]
|
||||
# Get ride models data from database
|
||||
ride_models = list(RideModel.objects.select_related(
|
||||
'manufacturer'
|
||||
).values(
|
||||
'id', 'name', 'slug', 'manufacturer__name', 'manufacturer__slug', 'category'
|
||||
).order_by('manufacturer__name', 'name'))
|
||||
|
||||
# Get ride model target markets from model choices
|
||||
ride_model_target_markets = [
|
||||
{"value": choice[0], "label": choice[1]}
|
||||
for choice in RideModel._meta.get_field('target_market').choices
|
||||
]
|
||||
# Calculate ranges from actual data
|
||||
ride_stats = Ride.objects.aggregate(
|
||||
min_rating=models.Min('average_rating'),
|
||||
max_rating=models.Max('average_rating'),
|
||||
min_height_req=models.Min('min_height_in'),
|
||||
max_height_req=models.Max('max_height_in'),
|
||||
min_capacity=models.Min('capacity_per_hour'),
|
||||
max_capacity=models.Max('capacity_per_hour'),
|
||||
min_duration=models.Min('ride_duration_seconds'),
|
||||
max_duration=models.Max('ride_duration_seconds'),
|
||||
min_year=models.Min('opening_date__year'),
|
||||
max_year=models.Max('opening_date__year'),
|
||||
)
|
||||
|
||||
# Get parks data from database
|
||||
parks = list(Ride.objects.exclude(
|
||||
park__isnull=True
|
||||
).select_related('park').values(
|
||||
'park__id', 'park__name', 'park__slug'
|
||||
).distinct().order_by('park__name'))
|
||||
# Calculate roller coaster specific ranges
|
||||
coaster_stats = RollerCoasterStats.objects.aggregate(
|
||||
min_height_ft=models.Min('height_ft'),
|
||||
max_height_ft=models.Max('height_ft'),
|
||||
min_length_ft=models.Min('length_ft'),
|
||||
max_length_ft=models.Max('length_ft'),
|
||||
min_speed_mph=models.Min('speed_mph'),
|
||||
max_speed_mph=models.Max('speed_mph'),
|
||||
min_inversions=models.Min('inversions'),
|
||||
max_inversions=models.Max('inversions'),
|
||||
min_ride_time=models.Min('ride_time_seconds'),
|
||||
max_ride_time=models.Max('ride_time_seconds'),
|
||||
min_drop_height=models.Min('max_drop_height_ft'),
|
||||
max_drop_height=models.Max('max_drop_height_ft'),
|
||||
min_trains=models.Min('trains_count'),
|
||||
max_trains=models.Max('trains_count'),
|
||||
min_cars=models.Min('cars_per_train'),
|
||||
max_cars=models.Max('cars_per_train'),
|
||||
min_seats=models.Min('seats_per_car'),
|
||||
max_seats=models.Max('seats_per_car'),
|
||||
)
|
||||
|
||||
# Get park areas data from database
|
||||
park_areas = list(Ride.objects.exclude(
|
||||
park_area__isnull=True
|
||||
).select_related('park_area').values(
|
||||
'park_area__id', 'park_area__name', 'park_area__slug'
|
||||
).distinct().order_by('park_area__name'))
|
||||
|
||||
# Get manufacturers (companies with MANUFACTURER role)
|
||||
manufacturers = list(Company.objects.filter(
|
||||
roles__contains=['MANUFACTURER']
|
||||
).values('id', 'name', 'slug').order_by('name'))
|
||||
|
||||
# Get designers (companies with DESIGNER role)
|
||||
designers = list(Company.objects.filter(
|
||||
roles__contains=['DESIGNER']
|
||||
).values('id', 'name', 'slug').order_by('name'))
|
||||
|
||||
# Get ride models data from database
|
||||
ride_models = list(RideModel.objects.select_related(
|
||||
'manufacturer'
|
||||
).values(
|
||||
'id', 'name', 'slug', 'manufacturer__name', 'manufacturer__slug', 'category'
|
||||
).order_by('manufacturer__name', 'name'))
|
||||
|
||||
# Calculate ranges from actual data
|
||||
ride_stats = Ride.objects.aggregate(
|
||||
min_rating=models.Min('average_rating'),
|
||||
max_rating=models.Max('average_rating'),
|
||||
min_height_req=models.Min('min_height_in'),
|
||||
max_height_req=models.Max('max_height_in'),
|
||||
min_capacity=models.Min('capacity_per_hour'),
|
||||
max_capacity=models.Max('capacity_per_hour'),
|
||||
min_duration=models.Min('ride_duration_seconds'),
|
||||
max_duration=models.Max('ride_duration_seconds'),
|
||||
min_year=models.Min('opening_date__year'),
|
||||
max_year=models.Max('opening_date__year'),
|
||||
)
|
||||
|
||||
# Calculate roller coaster specific ranges
|
||||
coaster_stats = RollerCoasterStats.objects.aggregate(
|
||||
min_height_ft=models.Min('height_ft'),
|
||||
max_height_ft=models.Max('height_ft'),
|
||||
min_length_ft=models.Min('length_ft'),
|
||||
max_length_ft=models.Max('length_ft'),
|
||||
min_speed_mph=models.Min('speed_mph'),
|
||||
max_speed_mph=models.Max('speed_mph'),
|
||||
min_inversions=models.Min('inversions'),
|
||||
max_inversions=models.Max('inversions'),
|
||||
min_ride_time=models.Min('ride_time_seconds'),
|
||||
max_ride_time=models.Max('ride_time_seconds'),
|
||||
min_drop_height=models.Min('max_drop_height_ft'),
|
||||
max_drop_height=models.Max('max_drop_height_ft'),
|
||||
min_trains=models.Min('trains_count'),
|
||||
max_trains=models.Max('trains_count'),
|
||||
min_cars=models.Min('cars_per_train'),
|
||||
max_cars=models.Max('cars_per_train'),
|
||||
min_seats=models.Min('seats_per_car'),
|
||||
max_seats=models.Max('seats_per_car'),
|
||||
)
|
||||
|
||||
ranges = {
|
||||
ranges = {
|
||||
"rating": {
|
||||
"min": float(ride_stats['min_rating'] or 1),
|
||||
"max": float(ride_stats['max_rating'] or 10),
|
||||
@@ -1017,24 +1268,24 @@ class FilterOptionsAPIView(APIView):
|
||||
},
|
||||
}
|
||||
|
||||
return Response({
|
||||
"categories": categories,
|
||||
"statuses": statuses,
|
||||
"post_closing_statuses": post_closing_statuses,
|
||||
"roller_coaster_types": roller_coaster_types,
|
||||
"track_materials": track_materials,
|
||||
"launch_types": launch_types,
|
||||
"ride_model_target_markets": ride_model_target_markets,
|
||||
"parks": parks,
|
||||
"park_areas": park_areas,
|
||||
"manufacturers": manufacturers,
|
||||
"designers": designers,
|
||||
"ride_models": ride_models,
|
||||
"ranges": ranges,
|
||||
"boolean_filters": [
|
||||
{"key": "has_inversions", "label": "Has Inversions",
|
||||
"description": "Filter roller coasters with or without inversions"},
|
||||
{"key": "has_coordinates", "label": "Has Location Coordinates",
|
||||
return Response({
|
||||
"categories": categories_data,
|
||||
"statuses": statuses_data,
|
||||
"post_closing_statuses": post_closing_statuses_data,
|
||||
"roller_coaster_types": coaster_types_data,
|
||||
"track_materials": track_materials_data,
|
||||
"launch_types": launch_systems_data,
|
||||
"ride_model_target_markets": target_markets_data,
|
||||
"parks": parks,
|
||||
"park_areas": park_areas,
|
||||
"manufacturers": manufacturers,
|
||||
"designers": designers,
|
||||
"ride_models": ride_models,
|
||||
"ranges": ranges,
|
||||
"boolean_filters": [
|
||||
{"key": "has_inversions", "label": "Has Inversions",
|
||||
"description": "Filter roller coasters with or without inversions"},
|
||||
{"key": "has_coordinates", "label": "Has Location Coordinates",
|
||||
"description": "Filter rides with GPS coordinates"},
|
||||
{"key": "has_ride_model", "label": "Has Ride Model",
|
||||
"description": "Filter rides with specified ride model"},
|
||||
@@ -1072,124 +1323,6 @@ class FilterOptionsAPIView(APIView):
|
||||
],
|
||||
})
|
||||
|
||||
except Exception:
|
||||
# Fallback to static options if database query fails
|
||||
return Response({
|
||||
"categories": [
|
||||
{"value": "RC", "label": "Roller Coaster"},
|
||||
{"value": "DR", "label": "Dark Ride"},
|
||||
{"value": "FR", "label": "Flat Ride"},
|
||||
{"value": "WR", "label": "Water Ride"},
|
||||
{"value": "TR", "label": "Transport"},
|
||||
{"value": "OT", "label": "Other"},
|
||||
],
|
||||
"statuses": [
|
||||
{"value": "OPERATING", "label": "Operating"},
|
||||
{"value": "CLOSED_TEMP", "label": "Temporarily Closed"},
|
||||
{"value": "SBNO", "label": "Standing But Not Operating"},
|
||||
{"value": "CLOSING", "label": "Closing"},
|
||||
{"value": "CLOSED_PERM", "label": "Permanently Closed"},
|
||||
{"value": "UNDER_CONSTRUCTION", "label": "Under Construction"},
|
||||
{"value": "DEMOLISHED", "label": "Demolished"},
|
||||
{"value": "RELOCATED", "label": "Relocated"},
|
||||
],
|
||||
"post_closing_statuses": [
|
||||
{"value": "SBNO", "label": "Standing But Not Operating"},
|
||||
{"value": "CLOSED_PERM", "label": "Permanently Closed"},
|
||||
],
|
||||
"roller_coaster_types": [
|
||||
{"value": "SITDOWN", "label": "Sit Down"},
|
||||
{"value": "INVERTED", "label": "Inverted"},
|
||||
{"value": "FLYING", "label": "Flying"},
|
||||
{"value": "STANDUP", "label": "Stand Up"},
|
||||
{"value": "WING", "label": "Wing"},
|
||||
{"value": "DIVE", "label": "Dive"},
|
||||
{"value": "FAMILY", "label": "Family"},
|
||||
{"value": "WILD_MOUSE", "label": "Wild Mouse"},
|
||||
{"value": "SPINNING", "label": "Spinning"},
|
||||
{"value": "FOURTH_DIMENSION", "label": "4th Dimension"},
|
||||
{"value": "OTHER", "label": "Other"},
|
||||
],
|
||||
"track_materials": [
|
||||
{"value": "STEEL", "label": "Steel"},
|
||||
{"value": "WOOD", "label": "Wood"},
|
||||
{"value": "HYBRID", "label": "Hybrid"},
|
||||
],
|
||||
"launch_types": [
|
||||
{"value": "CHAIN", "label": "Chain Lift"},
|
||||
{"value": "LSM", "label": "LSM Launch"},
|
||||
{"value": "HYDRAULIC", "label": "Hydraulic Launch"},
|
||||
{"value": "GRAVITY", "label": "Gravity"},
|
||||
{"value": "OTHER", "label": "Other"},
|
||||
],
|
||||
"ride_model_target_markets": [
|
||||
{"value": "FAMILY", "label": "Family"},
|
||||
{"value": "THRILL", "label": "Thrill"},
|
||||
{"value": "EXTREME", "label": "Extreme"},
|
||||
{"value": "KIDDIE", "label": "Kiddie"},
|
||||
{"value": "ALL_AGES", "label": "All Ages"},
|
||||
],
|
||||
"parks": [],
|
||||
"park_areas": [],
|
||||
"manufacturers": [],
|
||||
"designers": [],
|
||||
"ride_models": [],
|
||||
"ranges": {
|
||||
"rating": {"min": 1, "max": 10, "step": 0.1, "unit": "stars"},
|
||||
"height_requirement": {"min": 30, "max": 90, "step": 1, "unit": "inches"},
|
||||
"capacity": {"min": 0, "max": 5000, "step": 50, "unit": "riders/hour"},
|
||||
"ride_duration": {"min": 0, "max": 600, "step": 10, "unit": "seconds"},
|
||||
"height_ft": {"min": 0, "max": 500, "step": 5, "unit": "feet"},
|
||||
"length_ft": {"min": 0, "max": 10000, "step": 100, "unit": "feet"},
|
||||
"speed_mph": {"min": 0, "max": 150, "step": 5, "unit": "mph"},
|
||||
"inversions": {"min": 0, "max": 20, "step": 1, "unit": "inversions"},
|
||||
"ride_time": {"min": 0, "max": 600, "step": 10, "unit": "seconds"},
|
||||
"max_drop_height_ft": {"min": 0, "max": 500, "step": 10, "unit": "feet"},
|
||||
"trains_count": {"min": 1, "max": 10, "step": 1, "unit": "trains"},
|
||||
"cars_per_train": {"min": 1, "max": 20, "step": 1, "unit": "cars"},
|
||||
"seats_per_car": {"min": 1, "max": 8, "step": 1, "unit": "seats"},
|
||||
"opening_year": {"min": 1800, "max": 2030, "step": 1, "unit": "year"},
|
||||
},
|
||||
"boolean_filters": [
|
||||
{"key": "has_inversions", "label": "Has Inversions",
|
||||
"description": "Filter roller coasters with or without inversions"},
|
||||
{"key": "has_coordinates", "label": "Has Location Coordinates",
|
||||
"description": "Filter rides with GPS coordinates"},
|
||||
{"key": "has_ride_model", "label": "Has Ride Model",
|
||||
"description": "Filter rides with specified ride model"},
|
||||
{"key": "has_manufacturer", "label": "Has Manufacturer",
|
||||
"description": "Filter rides with specified manufacturer"},
|
||||
{"key": "has_designer", "label": "Has Designer",
|
||||
"description": "Filter rides with specified designer"},
|
||||
],
|
||||
"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": "average_rating", "label": "Rating (Lowest First)"},
|
||||
{"value": "-average_rating", "label": "Rating (Highest First)"},
|
||||
{"value": "capacity_per_hour", "label": "Capacity (Lowest First)"},
|
||||
{"value": "-capacity_per_hour",
|
||||
"label": "Capacity (Highest First)"},
|
||||
{"value": "ride_duration_seconds",
|
||||
"label": "Duration (Shortest First)"},
|
||||
{"value": "-ride_duration_seconds",
|
||||
"label": "Duration (Longest First)"},
|
||||
{"value": "height_ft", "label": "Height (Shortest First)"},
|
||||
{"value": "-height_ft", "label": "Height (Tallest First)"},
|
||||
{"value": "length_ft", "label": "Length (Shortest First)"},
|
||||
{"value": "-length_ft", "label": "Length (Longest First)"},
|
||||
{"value": "speed_mph", "label": "Speed (Slowest First)"},
|
||||
{"value": "-speed_mph", "label": "Speed (Fastest First)"},
|
||||
{"value": "inversions", "label": "Inversions (Fewest First)"},
|
||||
{"value": "-inversions", "label": "Inversions (Most First)"},
|
||||
{"value": "created_at", "label": "Date Added (Oldest First)"},
|
||||
{"value": "-created_at", "label": "Date Added (Newest First)"},
|
||||
{"value": "updated_at", "label": "Last Updated (Oldest First)"},
|
||||
{"value": "-updated_at", "label": "Last Updated (Newest First)"},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
# --- Company search (autocomplete) -----------------------------------------
|
||||
|
||||
@@ -18,6 +18,7 @@ from apps.accounts.models import (
|
||||
UserNotification,
|
||||
NotificationPreference,
|
||||
)
|
||||
from apps.core.choices.serializers import RichChoiceFieldSerializer
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
@@ -190,8 +191,10 @@ class CompleteUserSerializer(serializers.ModelSerializer):
|
||||
class UserPreferencesSerializer(serializers.Serializer):
|
||||
"""Serializer for user preferences and settings."""
|
||||
|
||||
theme_preference = serializers.ChoiceField(
|
||||
choices=User.ThemePreference.choices, help_text="User's theme preference"
|
||||
theme_preference = RichChoiceFieldSerializer(
|
||||
choice_group="theme_preferences",
|
||||
domain="accounts",
|
||||
help_text="User's theme preference"
|
||||
)
|
||||
email_notifications = serializers.BooleanField(
|
||||
default=True, help_text="Whether to receive email notifications"
|
||||
@@ -199,12 +202,9 @@ class UserPreferencesSerializer(serializers.Serializer):
|
||||
push_notifications = serializers.BooleanField(
|
||||
default=False, help_text="Whether to receive push notifications"
|
||||
)
|
||||
privacy_level = serializers.ChoiceField(
|
||||
choices=[
|
||||
("public", "Public"),
|
||||
("friends", "Friends Only"),
|
||||
("private", "Private"),
|
||||
],
|
||||
privacy_level = RichChoiceFieldSerializer(
|
||||
choice_group="privacy_levels",
|
||||
domain="accounts",
|
||||
default="public",
|
||||
help_text="Profile visibility level",
|
||||
)
|
||||
@@ -321,12 +321,9 @@ class NotificationSettingsSerializer(serializers.Serializer):
|
||||
class PrivacySettingsSerializer(serializers.Serializer):
|
||||
"""Serializer for privacy and visibility settings."""
|
||||
|
||||
profile_visibility = serializers.ChoiceField(
|
||||
choices=[
|
||||
("public", "Public"),
|
||||
("friends", "Friends Only"),
|
||||
("private", "Private"),
|
||||
],
|
||||
profile_visibility = RichChoiceFieldSerializer(
|
||||
choice_group="privacy_levels",
|
||||
domain="accounts",
|
||||
default="public",
|
||||
help_text="Overall profile visibility",
|
||||
)
|
||||
@@ -363,12 +360,9 @@ class PrivacySettingsSerializer(serializers.Serializer):
|
||||
search_visibility = serializers.BooleanField(
|
||||
default=True, help_text="Allow profile to appear in search results"
|
||||
)
|
||||
activity_visibility = serializers.ChoiceField(
|
||||
choices=[
|
||||
("public", "Public"),
|
||||
("friends", "Friends Only"),
|
||||
("private", "Private"),
|
||||
],
|
||||
activity_visibility = RichChoiceFieldSerializer(
|
||||
choice_group="privacy_levels",
|
||||
domain="accounts",
|
||||
default="friends",
|
||||
help_text="Who can see your activity feed",
|
||||
)
|
||||
|
||||
@@ -12,7 +12,8 @@ from drf_spectacular.utils import (
|
||||
OpenApiExample,
|
||||
)
|
||||
|
||||
from .shared import CATEGORY_CHOICES, ModelChoices
|
||||
from .shared import ModelChoices
|
||||
from apps.core.choices.serializers import RichChoiceFieldSerializer
|
||||
|
||||
|
||||
# === COMPANY SERIALIZERS ===
|
||||
@@ -111,7 +112,10 @@ class RideModelDetailOutputSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
description = serializers.CharField()
|
||||
category = serializers.CharField()
|
||||
category = RichChoiceFieldSerializer(
|
||||
choice_group="categories",
|
||||
domain="rides"
|
||||
)
|
||||
|
||||
# Manufacturer info
|
||||
manufacturer = serializers.SerializerMethodField()
|
||||
@@ -136,7 +140,7 @@ class RideModelCreateInputSerializer(serializers.Serializer):
|
||||
|
||||
name = serializers.CharField(max_length=255)
|
||||
description = serializers.CharField(allow_blank=True, default="")
|
||||
category = serializers.ChoiceField(choices=CATEGORY_CHOICES, required=False)
|
||||
category = serializers.ChoiceField(choices=ModelChoices.get_ride_category_choices(), required=False)
|
||||
manufacturer_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
|
||||
@@ -145,5 +149,5 @@ class RideModelUpdateInputSerializer(serializers.Serializer):
|
||||
|
||||
name = serializers.CharField(max_length=255, required=False)
|
||||
description = serializers.CharField(allow_blank=True, required=False)
|
||||
category = serializers.ChoiceField(choices=CATEGORY_CHOICES, required=False)
|
||||
category = serializers.ChoiceField(choices=ModelChoices.get_ride_category_choices(), required=False)
|
||||
manufacturer_id = serializers.IntegerField(required=False, allow_null=True)
|
||||
|
||||
@@ -9,6 +9,8 @@ from rest_framework import serializers
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema_field,
|
||||
)
|
||||
from .shared import ModelChoices
|
||||
from apps.core.choices.serializers import RichChoiceFieldSerializer
|
||||
|
||||
|
||||
# === STATISTICS SERIALIZERS ===
|
||||
@@ -90,7 +92,10 @@ class ParkReviewOutputSerializer(serializers.Serializer):
|
||||
class HealthCheckOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for health check responses."""
|
||||
|
||||
status = serializers.ChoiceField(choices=["healthy", "unhealthy"])
|
||||
status = RichChoiceFieldSerializer(
|
||||
choice_group="health_statuses",
|
||||
domain="core"
|
||||
)
|
||||
timestamp = serializers.DateTimeField()
|
||||
version = serializers.CharField()
|
||||
environment = serializers.CharField()
|
||||
@@ -111,6 +116,9 @@ class PerformanceMetricsOutputSerializer(serializers.Serializer):
|
||||
class SimpleHealthOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for simple health check."""
|
||||
|
||||
status = serializers.ChoiceField(choices=["ok", "error"])
|
||||
status = RichChoiceFieldSerializer(
|
||||
choice_group="simple_health_statuses",
|
||||
domain="core"
|
||||
)
|
||||
timestamp = serializers.DateTimeField()
|
||||
error = serializers.CharField(required=False)
|
||||
|
||||
@@ -15,6 +15,7 @@ from config.django import base as settings
|
||||
|
||||
from .shared import LocationOutputSerializer, CompanyOutputSerializer, ModelChoices
|
||||
from apps.core.services.media_url_service import MediaURLService
|
||||
from apps.core.choices.serializers import RichChoiceFieldSerializer
|
||||
|
||||
|
||||
# === PARK SERIALIZERS ===
|
||||
@@ -51,7 +52,10 @@ class ParkListOutputSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
status = RichChoiceFieldSerializer(
|
||||
choice_group="statuses",
|
||||
domain="parks"
|
||||
)
|
||||
description = serializers.CharField()
|
||||
|
||||
# Statistics
|
||||
@@ -141,7 +145,10 @@ class ParkDetailOutputSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
status = RichChoiceFieldSerializer(
|
||||
choice_group="statuses",
|
||||
domain="parks"
|
||||
)
|
||||
description = serializers.CharField()
|
||||
|
||||
# Details
|
||||
|
||||
@@ -14,6 +14,7 @@ from drf_spectacular.utils import (
|
||||
from config.django import base as settings
|
||||
|
||||
from .shared import ModelChoices
|
||||
from apps.core.choices.serializers import RichChoiceFieldSerializer
|
||||
|
||||
# Use dynamic imports to avoid circular import issues
|
||||
|
||||
@@ -132,14 +133,20 @@ class RideModelListOutputSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
category = serializers.CharField()
|
||||
category = RichChoiceFieldSerializer(
|
||||
choice_group="categories",
|
||||
domain="rides"
|
||||
)
|
||||
description = serializers.CharField()
|
||||
|
||||
# Manufacturer info
|
||||
manufacturer = RideModelManufacturerOutputSerializer(allow_null=True)
|
||||
|
||||
# Market info
|
||||
target_market = serializers.CharField()
|
||||
target_market = RichChoiceFieldSerializer(
|
||||
choice_group="target_markets",
|
||||
domain="rides"
|
||||
)
|
||||
is_discontinued = serializers.BooleanField()
|
||||
total_installations = serializers.IntegerField()
|
||||
first_installation_year = serializers.IntegerField(allow_null=True)
|
||||
@@ -386,15 +393,9 @@ class RideModelCreateInputSerializer(serializers.Serializer):
|
||||
# Design features
|
||||
notable_features = serializers.CharField(allow_blank=True, default="")
|
||||
target_market = serializers.ChoiceField(
|
||||
choices=[
|
||||
("FAMILY", "Family"),
|
||||
("THRILL", "Thrill"),
|
||||
("EXTREME", "Extreme"),
|
||||
("KIDDIE", "Kiddie"),
|
||||
("ALL_AGES", "All Ages"),
|
||||
],
|
||||
choices=ModelChoices.get_target_market_choices(),
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
default="",
|
||||
)
|
||||
|
||||
def validate(self, attrs):
|
||||
@@ -496,13 +497,7 @@ class RideModelUpdateInputSerializer(serializers.Serializer):
|
||||
# Design features
|
||||
notable_features = serializers.CharField(allow_blank=True, required=False)
|
||||
target_market = serializers.ChoiceField(
|
||||
choices=[
|
||||
("FAMILY", "Family"),
|
||||
("THRILL", "Thrill"),
|
||||
("EXTREME", "Extreme"),
|
||||
("KIDDIE", "Kiddie"),
|
||||
("ALL_AGES", "All Ages"),
|
||||
],
|
||||
choices=ModelChoices.get_target_market_choices(),
|
||||
allow_blank=True,
|
||||
required=False,
|
||||
)
|
||||
@@ -565,13 +560,7 @@ class RideModelFilterInputSerializer(serializers.Serializer):
|
||||
|
||||
# Market filter
|
||||
target_market = serializers.MultipleChoiceField(
|
||||
choices=[
|
||||
("FAMILY", "Family"),
|
||||
("THRILL", "Thrill"),
|
||||
("EXTREME", "Extreme"),
|
||||
("KIDDIE", "Kiddie"),
|
||||
("ALL_AGES", "All Ages"),
|
||||
],
|
||||
choices=ModelChoices.get_target_market_choices(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
@@ -724,16 +713,7 @@ class RideModelTechnicalSpecCreateInputSerializer(serializers.Serializer):
|
||||
|
||||
ride_model_id = serializers.IntegerField()
|
||||
spec_category = serializers.ChoiceField(
|
||||
choices=[
|
||||
("DIMENSIONS", "Dimensions"),
|
||||
("PERFORMANCE", "Performance"),
|
||||
("CAPACITY", "Capacity"),
|
||||
("SAFETY", "Safety Features"),
|
||||
("ELECTRICAL", "Electrical Requirements"),
|
||||
("FOUNDATION", "Foundation Requirements"),
|
||||
("MAINTENANCE", "Maintenance"),
|
||||
("OTHER", "Other"),
|
||||
]
|
||||
choices=ModelChoices.get_technical_spec_category_choices()
|
||||
)
|
||||
spec_name = serializers.CharField(max_length=100)
|
||||
spec_value = serializers.CharField(max_length=255)
|
||||
@@ -745,16 +725,7 @@ class RideModelTechnicalSpecUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating ride model technical specifications."""
|
||||
|
||||
spec_category = serializers.ChoiceField(
|
||||
choices=[
|
||||
("DIMENSIONS", "Dimensions"),
|
||||
("PERFORMANCE", "Performance"),
|
||||
("CAPACITY", "Capacity"),
|
||||
("SAFETY", "Safety Features"),
|
||||
("ELECTRICAL", "Electrical Requirements"),
|
||||
("FOUNDATION", "Foundation Requirements"),
|
||||
("MAINTENANCE", "Maintenance"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
choices=ModelChoices.get_technical_spec_category_choices(),
|
||||
required=False,
|
||||
)
|
||||
spec_name = serializers.CharField(max_length=100, required=False)
|
||||
@@ -774,13 +745,7 @@ class RideModelPhotoCreateInputSerializer(serializers.Serializer):
|
||||
caption = serializers.CharField(max_length=500, allow_blank=True, default="")
|
||||
alt_text = serializers.CharField(max_length=255, allow_blank=True, default="")
|
||||
photo_type = serializers.ChoiceField(
|
||||
choices=[
|
||||
("PROMOTIONAL", "Promotional"),
|
||||
("TECHNICAL", "Technical Drawing"),
|
||||
("INSTALLATION", "Installation Example"),
|
||||
("RENDERING", "3D Rendering"),
|
||||
("CATALOG", "Catalog Image"),
|
||||
],
|
||||
choices=ModelChoices.get_photo_type_choices(),
|
||||
default="PROMOTIONAL",
|
||||
)
|
||||
is_primary = serializers.BooleanField(default=False)
|
||||
@@ -795,13 +760,7 @@ class RideModelPhotoUpdateInputSerializer(serializers.Serializer):
|
||||
caption = serializers.CharField(max_length=500, allow_blank=True, required=False)
|
||||
alt_text = serializers.CharField(max_length=255, allow_blank=True, required=False)
|
||||
photo_type = serializers.ChoiceField(
|
||||
choices=[
|
||||
("PROMOTIONAL", "Promotional"),
|
||||
("TECHNICAL", "Technical Drawing"),
|
||||
("INSTALLATION", "Installation Example"),
|
||||
("RENDERING", "3D Rendering"),
|
||||
("CATALOG", "Catalog Image"),
|
||||
],
|
||||
choices=ModelChoices.get_photo_type_choices(),
|
||||
required=False,
|
||||
)
|
||||
is_primary = serializers.BooleanField(required=False)
|
||||
|
||||
@@ -13,6 +13,7 @@ from drf_spectacular.utils import (
|
||||
)
|
||||
from config.django import base as settings
|
||||
from .shared import ModelChoices
|
||||
from apps.core.choices.serializers import RichChoiceFieldSerializer
|
||||
|
||||
|
||||
# === RIDE SERIALIZERS ===
|
||||
@@ -24,6 +25,12 @@ class RideParkOutputSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
url = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.URLField())
|
||||
def get_url(self, obj) -> str:
|
||||
"""Generate the frontend URL for this park."""
|
||||
return f"{settings.FRONTEND_DOMAIN}/parks/{obj.slug}/"
|
||||
|
||||
|
||||
class RideModelOutputSerializer(serializers.Serializer):
|
||||
@@ -73,8 +80,14 @@ class RideListOutputSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
category = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
category = RichChoiceFieldSerializer(
|
||||
choice_group="categories",
|
||||
domain="rides"
|
||||
)
|
||||
status = RichChoiceFieldSerializer(
|
||||
choice_group="statuses",
|
||||
domain="rides"
|
||||
)
|
||||
description = serializers.CharField()
|
||||
|
||||
# Park info
|
||||
@@ -164,9 +177,19 @@ class RideDetailOutputSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
category = serializers.CharField()
|
||||
status = serializers.CharField()
|
||||
post_closing_status = serializers.CharField(allow_null=True)
|
||||
category = RichChoiceFieldSerializer(
|
||||
choice_group="categories",
|
||||
domain="rides"
|
||||
)
|
||||
status = RichChoiceFieldSerializer(
|
||||
choice_group="statuses",
|
||||
domain="rides"
|
||||
)
|
||||
post_closing_status = RichChoiceFieldSerializer(
|
||||
choice_group="post_closing_statuses",
|
||||
domain="rides",
|
||||
allow_null=True
|
||||
)
|
||||
description = serializers.CharField()
|
||||
|
||||
# Park info
|
||||
@@ -449,10 +472,10 @@ class RideCreateInputSerializer(serializers.Serializer):
|
||||
|
||||
name = serializers.CharField(max_length=255)
|
||||
description = serializers.CharField(allow_blank=True, default="")
|
||||
category = serializers.ChoiceField(choices=[]) # Choices set dynamically
|
||||
category = serializers.ChoiceField(choices=ModelChoices.get_ride_category_choices())
|
||||
status = serializers.ChoiceField(
|
||||
choices=[], default="OPERATING"
|
||||
) # Choices set dynamically
|
||||
choices=ModelChoices.get_ride_status_choices(), default="OPERATING"
|
||||
)
|
||||
|
||||
# Required park
|
||||
park_id = serializers.IntegerField()
|
||||
@@ -531,11 +554,11 @@ class RideUpdateInputSerializer(serializers.Serializer):
|
||||
name = serializers.CharField(max_length=255, required=False)
|
||||
description = serializers.CharField(allow_blank=True, required=False)
|
||||
category = serializers.ChoiceField(
|
||||
choices=[], required=False
|
||||
) # Choices set dynamically
|
||||
choices=ModelChoices.get_ride_category_choices(), required=False
|
||||
)
|
||||
status = serializers.ChoiceField(
|
||||
choices=[], required=False
|
||||
) # Choices set dynamically
|
||||
choices=ModelChoices.get_ride_status_choices(), required=False
|
||||
)
|
||||
post_closing_status = serializers.ChoiceField(
|
||||
choices=ModelChoices.get_ride_post_closing_choices(),
|
||||
required=False,
|
||||
@@ -603,13 +626,13 @@ class RideFilterInputSerializer(serializers.Serializer):
|
||||
|
||||
# Category filter
|
||||
category = serializers.MultipleChoiceField(
|
||||
choices=[], required=False
|
||||
) # Choices set dynamically
|
||||
choices=ModelChoices.get_ride_category_choices(), required=False
|
||||
)
|
||||
|
||||
# Status filter
|
||||
status = serializers.MultipleChoiceField(
|
||||
choices=[],
|
||||
required=False, # Choices set dynamically
|
||||
choices=ModelChoices.get_ride_status_choices(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
# Park filter
|
||||
@@ -695,12 +718,21 @@ class RollerCoasterStatsOutputSerializer(serializers.Serializer):
|
||||
inversions = serializers.IntegerField()
|
||||
ride_time_seconds = serializers.IntegerField(allow_null=True)
|
||||
track_type = serializers.CharField()
|
||||
track_material = serializers.CharField()
|
||||
roller_coaster_type = serializers.CharField()
|
||||
track_material = RichChoiceFieldSerializer(
|
||||
choice_group="track_materials",
|
||||
domain="rides"
|
||||
)
|
||||
roller_coaster_type = RichChoiceFieldSerializer(
|
||||
choice_group="coaster_types",
|
||||
domain="rides"
|
||||
)
|
||||
max_drop_height_ft = serializers.DecimalField(
|
||||
max_digits=6, decimal_places=2, allow_null=True
|
||||
)
|
||||
launch_type = serializers.CharField()
|
||||
launch_type = RichChoiceFieldSerializer(
|
||||
choice_group="launch_systems",
|
||||
domain="rides"
|
||||
)
|
||||
train_style = serializers.CharField()
|
||||
trains_count = serializers.IntegerField(allow_null=True)
|
||||
cars_per_train = serializers.IntegerField(allow_null=True)
|
||||
|
||||
@@ -6,6 +6,8 @@ and other search functionality.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from ..shared import ModelChoices
|
||||
from apps.core.choices.serializers import RichChoiceFieldSerializer
|
||||
|
||||
|
||||
# === CORE ENTITY SEARCH SERIALIZERS ===
|
||||
@@ -16,7 +18,9 @@ class EntitySearchInputSerializer(serializers.Serializer):
|
||||
|
||||
query = serializers.CharField(max_length=255, help_text="Search query string")
|
||||
entity_types = serializers.ListField(
|
||||
child=serializers.ChoiceField(choices=["park", "ride", "company", "user"]),
|
||||
child=serializers.ChoiceField(
|
||||
choices=ModelChoices.get_entity_type_choices()
|
||||
),
|
||||
required=False,
|
||||
help_text="Types of entities to search for",
|
||||
)
|
||||
@@ -34,7 +38,10 @@ class EntitySearchResultSerializer(serializers.Serializer):
|
||||
id = serializers.IntegerField()
|
||||
name = serializers.CharField()
|
||||
slug = serializers.CharField()
|
||||
type = serializers.CharField()
|
||||
type = RichChoiceFieldSerializer(
|
||||
choice_group="entity_types",
|
||||
domain="core"
|
||||
)
|
||||
description = serializers.CharField()
|
||||
relevance_score = serializers.FloatField()
|
||||
|
||||
|
||||
@@ -147,7 +147,12 @@ class ModerationSubmissionSerializer(serializers.Serializer):
|
||||
"""Serializer for moderation submissions."""
|
||||
|
||||
submission_type = serializers.ChoiceField(
|
||||
choices=["EDIT", "PHOTO", "REVIEW"], help_text="Type of submission"
|
||||
choices=[
|
||||
("EDIT", "Edit Submission"),
|
||||
("PHOTO", "Photo Submission"),
|
||||
("REVIEW", "Review Submission"),
|
||||
],
|
||||
help_text="Type of submission"
|
||||
)
|
||||
content_type = serializers.CharField(help_text="Content type being modified")
|
||||
object_id = serializers.IntegerField(help_text="ID of object being modified")
|
||||
|
||||
@@ -9,7 +9,7 @@ for common data structures used throughout the API.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from typing import Dict, Any, List, Optional
|
||||
from typing import Dict, Any, List
|
||||
|
||||
|
||||
class FilterOptionSerializer(serializers.Serializer):
|
||||
@@ -316,107 +316,124 @@ class CompanyOutputSerializer(serializers.Serializer):
|
||||
)
|
||||
|
||||
|
||||
# Category choices for ride models
|
||||
CATEGORY_CHOICES = [
|
||||
('RC', 'Roller Coaster'),
|
||||
('DR', 'Dark Ride'),
|
||||
('FR', 'Flat Ride'),
|
||||
('WR', 'Water Ride'),
|
||||
('TR', 'Transport Ride'),
|
||||
]
|
||||
|
||||
|
||||
class ModelChoices:
|
||||
"""
|
||||
Utility class to provide model choices for serializers.
|
||||
This prevents circular imports while providing access to model choices.
|
||||
Utility class to provide model choices for serializers using Rich Choice Objects.
|
||||
This prevents circular imports while providing access to model choices from the registry.
|
||||
|
||||
NO FALLBACKS - All choices must be properly defined in Rich Choice Objects.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_park_status_choices():
|
||||
"""Get park status choices."""
|
||||
return [
|
||||
('OPERATING', 'Operating'),
|
||||
('CLOSED_TEMP', 'Temporarily Closed'),
|
||||
('CLOSED_PERM', 'Permanently Closed'),
|
||||
('UNDER_CONSTRUCTION', 'Under Construction'),
|
||||
('PLANNED', 'Planned'),
|
||||
]
|
||||
"""Get park status choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("statuses", "parks")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
@staticmethod
|
||||
def get_ride_status_choices():
|
||||
"""Get ride status choices."""
|
||||
return [
|
||||
('OPERATING', 'Operating'),
|
||||
('CLOSED_TEMP', 'Temporarily Closed'),
|
||||
('CLOSED_PERM', 'Permanently Closed'),
|
||||
('SBNO', 'Standing But Not Operating'),
|
||||
('UNDER_CONSTRUCTION', 'Under Construction'),
|
||||
('RELOCATED', 'Relocated'),
|
||||
('DEMOLISHED', 'Demolished'),
|
||||
]
|
||||
"""Get ride status choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("statuses", "rides")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
@staticmethod
|
||||
def get_company_role_choices():
|
||||
"""Get company role choices."""
|
||||
return [
|
||||
('MANUFACTURER', 'Manufacturer'),
|
||||
('OPERATOR', 'Operator'),
|
||||
('DESIGNER', 'Designer'),
|
||||
('PROPERTY_OWNER', 'Property Owner'),
|
||||
]
|
||||
"""Get company role choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
# Get rides domain company roles (MANUFACTURER, DESIGNER)
|
||||
rides_choices = get_choices("company_roles", "rides")
|
||||
# Get parks domain company roles (OPERATOR, PROPERTY_OWNER)
|
||||
parks_choices = get_choices("company_roles", "parks")
|
||||
all_choices = list(rides_choices) + list(parks_choices)
|
||||
return [(choice.value, choice.label) for choice in all_choices]
|
||||
|
||||
@staticmethod
|
||||
def get_ride_category_choices():
|
||||
"""Get ride category choices."""
|
||||
return CATEGORY_CHOICES
|
||||
"""Get ride category choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("categories", "rides")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
@staticmethod
|
||||
def get_ride_post_closing_choices():
|
||||
"""Get ride post-closing status choices."""
|
||||
return [
|
||||
('RELOCATED', 'Relocated'),
|
||||
('DEMOLISHED', 'Demolished'),
|
||||
('STORED', 'Stored'),
|
||||
('UNKNOWN', 'Unknown'),
|
||||
]
|
||||
"""Get ride post-closing status choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("post_closing_statuses", "rides")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
@staticmethod
|
||||
def get_coaster_track_choices():
|
||||
"""Get coaster track type choices."""
|
||||
return [
|
||||
('STEEL', 'Steel'),
|
||||
('WOOD', 'Wood'),
|
||||
('HYBRID', 'Hybrid'),
|
||||
]
|
||||
"""Get coaster track material choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("track_materials", "rides")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
@staticmethod
|
||||
def get_coaster_type_choices():
|
||||
"""Get coaster type choices."""
|
||||
return [
|
||||
('SIT_DOWN', 'Sit Down'),
|
||||
('INVERTED', 'Inverted'),
|
||||
('FLOORLESS', 'Floorless'),
|
||||
('FLYING', 'Flying'),
|
||||
('STAND_UP', 'Stand Up'),
|
||||
('SPINNING', 'Spinning'),
|
||||
('WING', 'Wing'),
|
||||
('DIVE', 'Dive'),
|
||||
('LAUNCHED', 'Launched'),
|
||||
]
|
||||
"""Get coaster type choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("coaster_types", "rides")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
@staticmethod
|
||||
def get_launch_choices():
|
||||
"""Get launch system choices."""
|
||||
return [
|
||||
('NONE', 'None'),
|
||||
('LIM', 'Linear Induction Motor'),
|
||||
('LSM', 'Linear Synchronous Motor'),
|
||||
('HYDRAULIC', 'Hydraulic'),
|
||||
('PNEUMATIC', 'Pneumatic'),
|
||||
('CABLE', 'Cable'),
|
||||
('FLYWHEEL', 'Flywheel'),
|
||||
]
|
||||
"""Get launch system choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("launch_systems", "rides")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
@staticmethod
|
||||
def get_photo_type_choices():
|
||||
"""Get photo type choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("photo_types", "rides")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
@staticmethod
|
||||
def get_spec_category_choices():
|
||||
"""Get technical specification category choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("spec_categories", "rides")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
@staticmethod
|
||||
def get_technical_spec_category_choices():
|
||||
"""Get technical specification category choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("spec_categories", "rides")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
@staticmethod
|
||||
def get_target_market_choices():
|
||||
"""Get target market choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("target_markets", "rides")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
@staticmethod
|
||||
def get_entity_type_choices():
|
||||
"""Get entity type choices for search functionality."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("entity_types", "core")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
@staticmethod
|
||||
def get_health_status_choices():
|
||||
"""Get health check status choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("health_statuses", "core")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
@staticmethod
|
||||
def get_simple_health_status_choices():
|
||||
"""Get simple health check status choices from Rich Choice registry."""
|
||||
from apps.core.choices.registry import get_choices
|
||||
choices = get_choices("simple_health_statuses", "core")
|
||||
return [(choice.value, choice.label) for choice in choices]
|
||||
|
||||
|
||||
class EntityReferenceSerializer(serializers.Serializer):
|
||||
@@ -593,12 +610,12 @@ def ensure_filter_option_format(options: List[Any]) -> List[Dict[str, Any]]:
|
||||
'count': option.get('count'),
|
||||
'selected': option.get('selected', False)
|
||||
}
|
||||
elif isinstance(option, (list, tuple)) and len(option) >= 2:
|
||||
# Tuple format: (value, label) or (value, label, count)
|
||||
elif hasattr(option, 'value') and hasattr(option, 'label'):
|
||||
# RichChoice object format
|
||||
standardized_option = {
|
||||
'value': str(option[0]),
|
||||
'label': str(option[1]),
|
||||
'count': option[2] if len(option) > 2 else None,
|
||||
'value': str(option.value),
|
||||
'label': str(option.label),
|
||||
'count': None,
|
||||
'selected': False
|
||||
}
|
||||
else:
|
||||
|
||||
@@ -5,12 +5,8 @@ These tests verify that API responses match frontend TypeScript interfaces exact
|
||||
preventing runtime errors and ensuring type safety.
|
||||
"""
|
||||
|
||||
import json
|
||||
from django.test import TestCase, Client
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework import status
|
||||
from typing import Dict, Any, List
|
||||
|
||||
from apps.parks.services.hybrid_loader import smart_park_loader
|
||||
from apps.rides.services.hybrid_loader import SmartRideLoader
|
||||
|
||||
@@ -14,9 +14,7 @@ from rest_framework.serializers import Serializer
|
||||
from django.conf import settings
|
||||
|
||||
from apps.api.v1.serializers.shared import (
|
||||
validate_filter_metadata_contract,
|
||||
ApiResponseSerializer,
|
||||
ErrorResponseSerializer
|
||||
validate_filter_metadata_contract
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -250,7 +250,10 @@ class StatsAPIView(APIView):
|
||||
"RELOCATED": "relocated_parks",
|
||||
}
|
||||
|
||||
status_name = status_names.get(status_code, f"status_{status_code.lower()}")
|
||||
if status_code in status_names:
|
||||
status_name = status_names[status_code]
|
||||
else:
|
||||
raise ValueError(f"Unknown park status: {status_code}")
|
||||
park_status_stats[status_name] = status_count
|
||||
|
||||
# Ride status counts
|
||||
|
||||
Reference in New Issue
Block a user