mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 02:31:08 -05:00
1829 lines
83 KiB
Python
1829 lines
83 KiB
Python
"""
|
|
Full-featured Rides API views for ThrillWiki API v1.
|
|
|
|
This module implements a "full fat" set of endpoints:
|
|
- List / Create: GET /rides/ POST /rides/
|
|
- Retrieve / Update / Delete: GET /rides/{pk}/ PATCH/PUT/DELETE
|
|
- Filter options: GET /rides/filter-options/
|
|
- Company search: GET /rides/search/companies/?q=...
|
|
- Ride model search: GET /rides/search-ride-models/?q=...
|
|
- Search suggestions: GET /rides/search-suggestions/?q=...
|
|
Notes:
|
|
- These views try to use real Django models if available. If the domain models/services
|
|
are not present, they return a clear 501 response explaining what to wire up.
|
|
"""
|
|
|
|
from typing import Any
|
|
import logging
|
|
|
|
from django.db import models
|
|
|
|
logger = logging.getLogger(__name__)
|
|
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, extend_schema_view, OpenApiParameter
|
|
from drf_spectacular.types import OpenApiTypes
|
|
|
|
# Reuse existing serializers where possible
|
|
from apps.api.v1.serializers.rides import (
|
|
RideListOutputSerializer,
|
|
RideDetailOutputSerializer,
|
|
RideCreateInputSerializer,
|
|
RideUpdateInputSerializer,
|
|
RideImageSettingsInputSerializer,
|
|
)
|
|
|
|
# Import hybrid filtering components
|
|
from apps.rides.services.hybrid_loader import SmartRideLoader
|
|
|
|
# Create smart loader instance
|
|
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
|
|
|
|
# Attempt to import ModelChoices to return filter options
|
|
try:
|
|
from apps.api.v1.serializers.shared import ModelChoices # type: ignore
|
|
|
|
HAVE_MODELCHOICES = True
|
|
except Exception:
|
|
ModelChoices = None # type: ignore
|
|
HAVE_MODELCHOICES = False
|
|
|
|
|
|
class StandardResultsSetPagination(PageNumberPagination):
|
|
page_size = 20
|
|
page_size_query_param = "page_size"
|
|
max_page_size = 1000
|
|
|
|
|
|
# --- Ride list & create -----------------------------------------------------
|
|
class RideListCreateAPIView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
@extend_schema(
|
|
summary="List rides with comprehensive filtering and pagination",
|
|
description="List rides with comprehensive filtering options including category, status, manufacturer, designer, ride model, and more.",
|
|
parameters=[
|
|
OpenApiParameter(
|
|
name="page",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Page number for pagination",
|
|
),
|
|
OpenApiParameter(
|
|
name="page_size",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Number of results per page (max 1000)",
|
|
),
|
|
OpenApiParameter(
|
|
name="search",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR,
|
|
description="Search in ride names and descriptions",
|
|
),
|
|
OpenApiParameter(
|
|
name="park_slug",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR,
|
|
description="Filter by park slug",
|
|
),
|
|
OpenApiParameter(
|
|
name="park_id",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Filter by park ID",
|
|
),
|
|
OpenApiParameter(
|
|
name="category",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR,
|
|
description="Filter by ride category (RC, DR, FR, WR, TR, OT). Multiple values supported: ?category=RC&category=DR",
|
|
),
|
|
OpenApiParameter(
|
|
name="status",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR,
|
|
description="Filter by ride status. Multiple values supported: ?status=OPERATING&status=CLOSED_TEMP",
|
|
),
|
|
OpenApiParameter(
|
|
name="manufacturer_id",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Filter by manufacturer company ID",
|
|
),
|
|
OpenApiParameter(
|
|
name="manufacturer_slug",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR,
|
|
description="Filter by manufacturer company slug",
|
|
),
|
|
OpenApiParameter(
|
|
name="designer_id",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Filter by designer company ID",
|
|
),
|
|
OpenApiParameter(
|
|
name="designer_slug",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR,
|
|
description="Filter by designer company slug",
|
|
),
|
|
OpenApiParameter(
|
|
name="ride_model_id",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Filter by specific ride model ID",
|
|
),
|
|
OpenApiParameter(
|
|
name="ride_model_slug",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR,
|
|
description="Filter by ride model slug (requires manufacturer_slug)",
|
|
),
|
|
OpenApiParameter(
|
|
name="roller_coaster_type",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR,
|
|
description="Filter roller coasters by type (SITDOWN, INVERTED, FLYING, etc.)",
|
|
),
|
|
OpenApiParameter(
|
|
name="track_material",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR,
|
|
description="Filter roller coasters by track material (STEEL, WOOD, HYBRID)",
|
|
),
|
|
OpenApiParameter(
|
|
name="propulsion_system",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR,
|
|
description="Filter roller coasters by propulsion system (CHAIN, LSM, HYDRAULIC, etc.)",
|
|
),
|
|
OpenApiParameter(
|
|
name="min_rating",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.NUMBER,
|
|
description="Filter by minimum average rating (1-10)",
|
|
),
|
|
OpenApiParameter(
|
|
name="max_rating",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.NUMBER,
|
|
description="Filter by maximum average rating (1-10)",
|
|
),
|
|
OpenApiParameter(
|
|
name="min_height_requirement",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Filter by minimum height requirement in inches",
|
|
),
|
|
OpenApiParameter(
|
|
name="max_height_requirement",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Filter by maximum height requirement in inches",
|
|
),
|
|
OpenApiParameter(
|
|
name="min_capacity",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Filter by minimum hourly capacity",
|
|
),
|
|
OpenApiParameter(
|
|
name="max_capacity",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Filter by maximum hourly capacity",
|
|
),
|
|
OpenApiParameter(
|
|
name="min_height_ft",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.NUMBER,
|
|
description="Filter roller coasters by minimum height in feet",
|
|
),
|
|
OpenApiParameter(
|
|
name="max_height_ft",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.NUMBER,
|
|
description="Filter roller coasters by maximum height in feet",
|
|
),
|
|
OpenApiParameter(
|
|
name="min_speed_mph",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.NUMBER,
|
|
description="Filter roller coasters by minimum speed in mph",
|
|
),
|
|
OpenApiParameter(
|
|
name="max_speed_mph",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.NUMBER,
|
|
description="Filter roller coasters by maximum speed in mph",
|
|
),
|
|
OpenApiParameter(
|
|
name="min_inversions",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Filter roller coasters by minimum number of inversions",
|
|
),
|
|
OpenApiParameter(
|
|
name="max_inversions",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Filter roller coasters by maximum number of inversions",
|
|
),
|
|
OpenApiParameter(
|
|
name="has_inversions",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.BOOL,
|
|
description="Filter roller coasters that have inversions (true) or don't have inversions (false)",
|
|
),
|
|
OpenApiParameter(
|
|
name="opening_year",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Filter by opening year",
|
|
),
|
|
OpenApiParameter(
|
|
name="min_opening_year",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Filter by minimum opening year",
|
|
),
|
|
OpenApiParameter(
|
|
name="max_opening_year",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.INT,
|
|
description="Filter by maximum opening year",
|
|
),
|
|
OpenApiParameter(
|
|
name="ordering",
|
|
location=OpenApiParameter.QUERY,
|
|
type=OpenApiTypes.STR,
|
|
description="Order results by field. Options: name, -name, opening_date, -opening_date, average_rating, -average_rating, capacity_per_hour, -capacity_per_hour, created_at, -created_at, height_ft, -height_ft, speed_mph, -speed_mph",
|
|
),
|
|
],
|
|
responses={200: RideListOutputSerializer(many=True)},
|
|
tags=["Rides"],
|
|
)
|
|
def get(self, request: Request) -> Response:
|
|
"""List rides with comprehensive filtering and pagination."""
|
|
if not MODELS_AVAILABLE:
|
|
return Response(
|
|
{
|
|
"detail": "Ride listing is not available because domain models are not imported. "
|
|
"Implement apps.rides.models.Ride (and related managers) to enable listing."
|
|
},
|
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
)
|
|
|
|
# Start with base queryset with optimized joins
|
|
qs = (
|
|
Ride.objects.all()
|
|
.select_related(
|
|
"park",
|
|
"manufacturer",
|
|
"designer",
|
|
"ride_model",
|
|
"ride_model__manufacturer",
|
|
)
|
|
.prefetch_related("coaster_stats")
|
|
) # type: ignore
|
|
|
|
# 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
|
|
|
|
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 = params.get("park_id")
|
|
if park_id:
|
|
try:
|
|
qs = qs.filter(park_id=int(park_id))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
return qs
|
|
|
|
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)
|
|
|
|
statuses = params.getlist("status")
|
|
if statuses:
|
|
qs = qs.filter(status__in=statuses)
|
|
|
|
return qs
|
|
|
|
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 = params.get("manufacturer_slug")
|
|
if manufacturer_slug:
|
|
qs = qs.filter(manufacturer__slug=manufacturer_slug)
|
|
|
|
designer_id = params.get("designer_id")
|
|
if designer_id:
|
|
try:
|
|
qs = qs.filter(designer_id=int(designer_id))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
designer_slug = params.get("designer_slug")
|
|
if designer_slug:
|
|
qs = qs.filter(designer__slug=designer_slug)
|
|
|
|
return qs
|
|
|
|
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 = 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
|
|
|
|
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 = params.get("max_rating")
|
|
if max_rating:
|
|
try:
|
|
qs = qs.filter(average_rating__lte=float(max_rating))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
return qs
|
|
|
|
def _apply_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 = 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
|
|
|
|
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 = params.get("max_capacity")
|
|
if max_capacity:
|
|
try:
|
|
qs = qs.filter(capacity_per_hour__lte=int(max_capacity))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
return qs
|
|
|
|
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 = params.get("min_opening_year")
|
|
if min_opening_year:
|
|
try:
|
|
qs = qs.filter(opening_date__year__gte=int(min_opening_year))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
max_opening_year = params.get("max_opening_year")
|
|
if max_opening_year:
|
|
try:
|
|
qs = qs.filter(opening_date__year__lte=int(max_opening_year))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
return qs
|
|
|
|
def _apply_roller_coaster_filters(self, qs, 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 = params.get("track_material")
|
|
if track_material:
|
|
qs = qs.filter(coaster_stats__track_material=track_material)
|
|
|
|
propulsion_system = params.get("propulsion_system")
|
|
if propulsion_system:
|
|
qs = qs.filter(coaster_stats__propulsion_system=propulsion_system)
|
|
|
|
# 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 = 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
|
|
|
|
# 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 = params.get("max_speed_mph")
|
|
if max_speed_mph:
|
|
try:
|
|
qs = qs.filter(coaster_stats__speed_mph__lte=float(max_speed_mph))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
# Inversion filters
|
|
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 = params.get("max_inversions")
|
|
if max_inversions:
|
|
try:
|
|
qs = qs.filter(coaster_stats__inversions__lte=int(max_inversions))
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
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
|
|
|
|
def _apply_ordering(self, qs, params):
|
|
"""Apply ordering to the queryset."""
|
|
ordering = params.get("ordering", "name")
|
|
valid_orderings = [
|
|
"name",
|
|
"-name",
|
|
"opening_date",
|
|
"-opening_date",
|
|
"average_rating",
|
|
"-average_rating",
|
|
"capacity_per_hour",
|
|
"-capacity_per_hour",
|
|
"created_at",
|
|
"-created_at",
|
|
"height_ft",
|
|
"-height_ft",
|
|
"speed_mph",
|
|
"-speed_mph",
|
|
]
|
|
|
|
if ordering in valid_orderings:
|
|
if ordering in ["height_ft", "-height_ft", "speed_mph", "-speed_mph"]:
|
|
# For coaster stats ordering, we need to join and order by the stats
|
|
ordering_field = ordering.replace(
|
|
"height_ft", "coaster_stats__height_ft"
|
|
).replace("speed_mph", "coaster_stats__speed_mph")
|
|
qs = qs.order_by(ordering_field)
|
|
else:
|
|
qs = qs.order_by(ordering)
|
|
|
|
return qs
|
|
|
|
@extend_schema(
|
|
summary="Create a new ride",
|
|
description="Create a new ride.",
|
|
responses={201: RideDetailOutputSerializer()},
|
|
tags=["Rides"],
|
|
)
|
|
def post(self, request: Request) -> Response:
|
|
"""Create a new ride."""
|
|
serializer_in = RideCreateInputSerializer(data=request.data)
|
|
serializer_in.is_valid(raise_exception=True)
|
|
|
|
if not MODELS_AVAILABLE:
|
|
return Response(
|
|
{
|
|
"detail": "Ride creation is not available because domain models are not imported. "
|
|
"Implement apps.rides.models.Ride and necessary create logic."
|
|
},
|
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
)
|
|
|
|
validated = serializer_in.validated_data
|
|
|
|
# Minimal create logic using model fields if available.
|
|
try:
|
|
park = Park.objects.get(id=validated["park_id"]) # type: ignore
|
|
except Park.DoesNotExist: # type: ignore
|
|
raise NotFound("Park not found")
|
|
|
|
ride = Ride.objects.create( # type: ignore
|
|
name=validated["name"],
|
|
description=validated.get("description", ""),
|
|
category=validated.get("category"),
|
|
status=validated.get("status"),
|
|
park=park,
|
|
park_area_id=validated.get("park_area_id"),
|
|
opening_date=validated.get("opening_date"),
|
|
closing_date=validated.get("closing_date"),
|
|
status_since=validated.get("status_since"),
|
|
min_height_in=validated.get("min_height_in"),
|
|
max_height_in=validated.get("max_height_in"),
|
|
capacity_per_hour=validated.get("capacity_per_hour"),
|
|
ride_duration_seconds=validated.get("ride_duration_seconds"),
|
|
)
|
|
|
|
# Optional foreign keys
|
|
if validated.get("manufacturer_id"):
|
|
try:
|
|
ride.manufacturer_id = validated["manufacturer_id"]
|
|
ride.save()
|
|
except Exception:
|
|
# ignore if foreign key constraints or models not present
|
|
pass
|
|
|
|
out_serializer = RideDetailOutputSerializer(ride, context={"request": request})
|
|
return Response(out_serializer.data, status=status.HTTP_201_CREATED)
|
|
|
|
|
|
# --- Ride retrieve / update / delete ---------------------------------------
|
|
@extend_schema(
|
|
summary="Retrieve, update or delete a ride",
|
|
responses={200: RideDetailOutputSerializer()},
|
|
tags=["Rides"],
|
|
)
|
|
class RideDetailAPIView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def _get_ride_or_404(self, pk: int) -> Any:
|
|
if not MODELS_AVAILABLE:
|
|
raise NotFound(
|
|
"Ride detail is not available because domain models are not imported. "
|
|
"Implement apps.rides.models.Ride to enable detail endpoints."
|
|
)
|
|
try:
|
|
return Ride.objects.select_related("park").get(pk=pk) # type: ignore
|
|
except Ride.DoesNotExist: # type: ignore
|
|
raise NotFound("Ride not found")
|
|
|
|
def get(self, request: Request, pk: int) -> Response:
|
|
ride = self._get_ride_or_404(pk)
|
|
serializer = RideDetailOutputSerializer(ride, context={"request": request})
|
|
return Response(serializer.data)
|
|
|
|
def patch(self, request: Request, pk: int) -> Response:
|
|
ride = self._get_ride_or_404(pk)
|
|
serializer_in = RideUpdateInputSerializer(data=request.data, partial=True)
|
|
serializer_in.is_valid(raise_exception=True)
|
|
if not MODELS_AVAILABLE:
|
|
return Response(
|
|
{
|
|
"detail": "Ride update is not available because domain models are not imported."
|
|
},
|
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
)
|
|
|
|
validated_data = serializer_in.validated_data
|
|
park_change_info = None
|
|
|
|
# Handle park change specially if park_id is being updated
|
|
if 'park_id' in validated_data:
|
|
new_park_id = validated_data.pop('park_id')
|
|
try:
|
|
new_park = Park.objects.get(id=new_park_id) # type: ignore
|
|
if new_park.id != ride.park_id:
|
|
# Use the move_to_park method for proper handling
|
|
park_change_info = ride.move_to_park(new_park)
|
|
except Park.DoesNotExist: # type: ignore
|
|
raise NotFound("Target park not found")
|
|
|
|
# Apply other field updates
|
|
for key, value in validated_data.items():
|
|
setattr(ride, key, value)
|
|
|
|
ride.save()
|
|
|
|
# Prepare response data
|
|
serializer = RideDetailOutputSerializer(ride, context={"request": request})
|
|
response_data = serializer.data
|
|
|
|
# Add park change information to response if applicable
|
|
if park_change_info:
|
|
response_data['park_change_info'] = park_change_info
|
|
|
|
return Response(response_data)
|
|
|
|
def put(self, request: Request, pk: int) -> Response:
|
|
# Full replace - reuse patch behavior for simplicity
|
|
return self.patch(request, pk)
|
|
|
|
def delete(self, request: Request, pk: int) -> Response:
|
|
if not MODELS_AVAILABLE:
|
|
return Response(
|
|
{
|
|
"detail": "Ride delete is not available because domain models are not imported."
|
|
},
|
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
|
)
|
|
ride = self._get_ride_or_404(pk)
|
|
ride.delete()
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
# --- Filter options ---------------------------------------------------------
|
|
@extend_schema(
|
|
summary="Get comprehensive filter options for rides",
|
|
description="Returns all available filter options for rides with complete read-only access to all possible ride model fields and attributes, including dynamic data from database.",
|
|
responses={200: OpenApiTypes.OBJECT},
|
|
tags=["Rides"],
|
|
)
|
|
class FilterOptionsAPIView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request: Request) -> Response:
|
|
"""Return comprehensive filter options with Rich Choice Objects metadata."""
|
|
# Import Rich Choice registry
|
|
from apps.core.choices.registry import get_choices
|
|
|
|
if not MODELS_AVAILABLE:
|
|
# 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')
|
|
propulsion_systems = get_choices('propulsion_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
|
|
]
|
|
|
|
propulsion_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 propulsion_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},
|
|
]
|
|
propulsion_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": categories_data,
|
|
"statuses": statuses_data,
|
|
"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"},
|
|
],
|
|
"propulsion_systems": [
|
|
{"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)"},
|
|
],
|
|
})
|
|
|
|
# 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')
|
|
propulsion_systems = get_choices('propulsion_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
|
|
]
|
|
|
|
propulsion_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 propulsion_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 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 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 = {
|
|
"rating": {
|
|
"min": float(ride_stats['min_rating'] or 1),
|
|
"max": float(ride_stats['max_rating'] or 10),
|
|
"step": 0.1,
|
|
"unit": "stars"
|
|
},
|
|
"height_requirement": {
|
|
"min": ride_stats['min_height_req'] or 30,
|
|
"max": ride_stats['max_height_req'] or 90,
|
|
"step": 1,
|
|
"unit": "inches"
|
|
},
|
|
"capacity": {
|
|
"min": ride_stats['min_capacity'] or 0,
|
|
"max": ride_stats['max_capacity'] or 5000,
|
|
"step": 50,
|
|
"unit": "riders/hour"
|
|
},
|
|
"ride_duration": {
|
|
"min": ride_stats['min_duration'] or 0,
|
|
"max": ride_stats['max_duration'] or 600,
|
|
"step": 10,
|
|
"unit": "seconds"
|
|
},
|
|
"height_ft": {
|
|
"min": float(coaster_stats['min_height_ft'] or 0),
|
|
"max": float(coaster_stats['max_height_ft'] or 500),
|
|
"step": 5,
|
|
"unit": "feet"
|
|
},
|
|
"length_ft": {
|
|
"min": float(coaster_stats['min_length_ft'] or 0),
|
|
"max": float(coaster_stats['max_length_ft'] or 10000),
|
|
"step": 100,
|
|
"unit": "feet"
|
|
},
|
|
"speed_mph": {
|
|
"min": float(coaster_stats['min_speed_mph'] or 0),
|
|
"max": float(coaster_stats['max_speed_mph'] or 150),
|
|
"step": 5,
|
|
"unit": "mph"
|
|
},
|
|
"inversions": {
|
|
"min": coaster_stats['min_inversions'] or 0,
|
|
"max": coaster_stats['max_inversions'] or 20,
|
|
"step": 1,
|
|
"unit": "inversions"
|
|
},
|
|
"ride_time": {
|
|
"min": coaster_stats['min_ride_time'] or 0,
|
|
"max": coaster_stats['max_ride_time'] or 600,
|
|
"step": 10,
|
|
"unit": "seconds"
|
|
},
|
|
"max_drop_height_ft": {
|
|
"min": float(coaster_stats['min_drop_height'] or 0),
|
|
"max": float(coaster_stats['max_drop_height'] or 500),
|
|
"step": 10,
|
|
"unit": "feet"
|
|
},
|
|
"trains_count": {
|
|
"min": coaster_stats['min_trains'] or 1,
|
|
"max": coaster_stats['max_trains'] or 10,
|
|
"step": 1,
|
|
"unit": "trains"
|
|
},
|
|
"cars_per_train": {
|
|
"min": coaster_stats['min_cars'] or 1,
|
|
"max": coaster_stats['max_cars'] or 20,
|
|
"step": 1,
|
|
"unit": "cars"
|
|
},
|
|
"seats_per_car": {
|
|
"min": coaster_stats['min_seats'] or 1,
|
|
"max": coaster_stats['max_seats'] or 8,
|
|
"step": 1,
|
|
"unit": "seats"
|
|
},
|
|
"opening_year": {
|
|
"min": ride_stats['min_year'] or 1800,
|
|
"max": ride_stats['max_year'] or 2030,
|
|
"step": 1,
|
|
"unit": "year"
|
|
},
|
|
}
|
|
|
|
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,
|
|
"propulsion_systems": propulsion_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"},
|
|
{"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) -----------------------------------------
|
|
@extend_schema(
|
|
summary="Search companies (manufacturers/designers) for autocomplete",
|
|
parameters=[
|
|
OpenApiParameter(
|
|
name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
|
)
|
|
],
|
|
responses={200: OpenApiTypes.OBJECT},
|
|
tags=["Rides"],
|
|
)
|
|
class CompanySearchAPIView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request: Request) -> Response:
|
|
q = request.query_params.get("q", "")
|
|
if not q:
|
|
return Response([], status=status.HTTP_200_OK)
|
|
|
|
if Company is None:
|
|
# Provide helpful placeholder structure
|
|
return Response(
|
|
[
|
|
{"id": 1, "name": "Rocky Mountain Construction", "slug": "rmc"},
|
|
{"id": 2, "name": "Bolliger & Mabillard", "slug": "b&m"},
|
|
]
|
|
)
|
|
|
|
qs = Company.objects.filter(name__icontains=q)[:20] # type: ignore
|
|
results = [
|
|
{"id": c.id, "name": c.name, "slug": getattr(c, "slug", "")} for c in qs
|
|
]
|
|
return Response(results)
|
|
|
|
|
|
# --- Ride model search (autocomplete) --------------------------------------
|
|
@extend_schema(
|
|
summary="Search ride models for autocomplete",
|
|
parameters=[
|
|
OpenApiParameter(
|
|
name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
|
)
|
|
],
|
|
tags=["Rides"],
|
|
)
|
|
class RideModelSearchAPIView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request: Request) -> Response:
|
|
q = request.query_params.get("q", "")
|
|
if not q:
|
|
return Response([], status=status.HTTP_200_OK)
|
|
|
|
if RideModel is None:
|
|
return Response(
|
|
[
|
|
{"id": 1, "name": "I-Box (RMC)", "category": "ROLLER_COASTER"},
|
|
{
|
|
"id": 2,
|
|
"name": "Hyper Coaster Model X",
|
|
"category": "ROLLER_COASTER",
|
|
},
|
|
]
|
|
)
|
|
|
|
qs = RideModel.objects.filter(name__icontains=q)[:20] # type: ignore
|
|
results = [
|
|
{"id": m.id, "name": m.name, "category": getattr(m, "category", "")}
|
|
for m in qs
|
|
]
|
|
return Response(results)
|
|
|
|
|
|
# --- Search suggestions -----------------------------------------------------
|
|
@extend_schema(
|
|
summary="Search suggestions for ride search box",
|
|
parameters=[
|
|
OpenApiParameter(
|
|
name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
|
)
|
|
],
|
|
tags=["Rides"],
|
|
)
|
|
class RideSearchSuggestionsAPIView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request: Request) -> Response:
|
|
q = request.query_params.get("q", "")
|
|
if not q:
|
|
return Response([], status=status.HTTP_200_OK)
|
|
|
|
# Very small suggestion implementation: look in ride names if available
|
|
if MODELS_AVAILABLE and Ride is not None:
|
|
qs = Ride.objects.filter(name__icontains=q).values_list("name", flat=True)[
|
|
:10
|
|
] # type: ignore
|
|
return Response([{"suggestion": name} for name in qs])
|
|
|
|
# Fallback suggestions
|
|
fallback = [
|
|
{"suggestion": f"{q} coaster"},
|
|
{"suggestion": f"{q} ride"},
|
|
{"suggestion": f"{q} park"},
|
|
]
|
|
return Response(fallback)
|
|
|
|
|
|
# --- Ride image settings ---------------------------------------------------
|
|
@extend_schema(
|
|
summary="Set ride banner and card images",
|
|
description="Set banner_image and card_image for a ride from existing ride photos",
|
|
request=RideImageSettingsInputSerializer,
|
|
responses={
|
|
200: RideDetailOutputSerializer,
|
|
400: OpenApiTypes.OBJECT,
|
|
404: OpenApiTypes.OBJECT,
|
|
},
|
|
tags=["Rides"],
|
|
)
|
|
class RideImageSettingsAPIView(APIView):
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def _get_ride_or_404(self, pk: int) -> Any:
|
|
if not MODELS_AVAILABLE:
|
|
raise NotFound("Ride models not available")
|
|
try:
|
|
return Ride.objects.get(pk=pk) # type: ignore
|
|
except Ride.DoesNotExist: # type: ignore
|
|
raise NotFound("Ride not found")
|
|
|
|
def patch(self, request: Request, pk: int) -> Response:
|
|
"""Set banner and card images for the ride."""
|
|
ride = self._get_ride_or_404(pk)
|
|
|
|
serializer = RideImageSettingsInputSerializer(data=request.data, partial=True)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
# Update the ride with the validated data
|
|
for field, value in serializer.validated_data.items():
|
|
setattr(ride, field, value)
|
|
|
|
ride.save()
|
|
|
|
# Return updated ride data
|
|
output_serializer = RideDetailOutputSerializer(
|
|
ride, context={"request": request}
|
|
)
|
|
return Response(output_serializer.data)
|
|
|
|
|
|
# --- Hybrid Filtering API Views --------------------------------------------
|
|
|
|
@extend_schema_view(
|
|
get=extend_schema(
|
|
summary="Get rides with hybrid filtering",
|
|
description="Retrieve rides with intelligent hybrid filtering strategy. Automatically chooses between client-side and server-side filtering based on data size.",
|
|
parameters=[
|
|
OpenApiParameter("category", OpenApiTypes.STR, description="Filter by ride category (comma-separated for multiple)"),
|
|
OpenApiParameter("status", OpenApiTypes.STR, description="Filter by ride status (comma-separated for multiple)"),
|
|
OpenApiParameter("park_slug", OpenApiTypes.STR, description="Filter by park slug"),
|
|
OpenApiParameter("park_id", OpenApiTypes.INT, description="Filter by park ID"),
|
|
OpenApiParameter("manufacturer", OpenApiTypes.STR, description="Filter by manufacturer slug (comma-separated for multiple)"),
|
|
OpenApiParameter("designer", OpenApiTypes.STR, description="Filter by designer slug (comma-separated for multiple)"),
|
|
OpenApiParameter("ride_model", OpenApiTypes.STR, description="Filter by ride model slug (comma-separated for multiple)"),
|
|
OpenApiParameter("opening_year_min", OpenApiTypes.INT, description="Minimum opening year"),
|
|
OpenApiParameter("opening_year_max", OpenApiTypes.INT, description="Maximum opening year"),
|
|
OpenApiParameter("rating_min", OpenApiTypes.NUMBER, description="Minimum average rating"),
|
|
OpenApiParameter("rating_max", OpenApiTypes.NUMBER, description="Maximum average rating"),
|
|
OpenApiParameter("height_requirement_min", OpenApiTypes.INT, description="Minimum height requirement in inches"),
|
|
OpenApiParameter("height_requirement_max", OpenApiTypes.INT, description="Maximum height requirement in inches"),
|
|
OpenApiParameter("capacity_min", OpenApiTypes.INT, description="Minimum hourly capacity"),
|
|
OpenApiParameter("capacity_max", OpenApiTypes.INT, description="Maximum hourly capacity"),
|
|
OpenApiParameter("roller_coaster_type", OpenApiTypes.STR, description="Filter by roller coaster type (comma-separated for multiple)"),
|
|
OpenApiParameter("track_material", OpenApiTypes.STR, description="Filter by track material (comma-separated for multiple)"),
|
|
OpenApiParameter("propulsion_system", OpenApiTypes.STR, description="Filter by propulsion system (comma-separated for multiple)"),
|
|
OpenApiParameter("height_ft_min", OpenApiTypes.NUMBER, description="Minimum roller coaster height in feet"),
|
|
OpenApiParameter("height_ft_max", OpenApiTypes.NUMBER, description="Maximum roller coaster height in feet"),
|
|
OpenApiParameter("speed_mph_min", OpenApiTypes.NUMBER, description="Minimum roller coaster speed in mph"),
|
|
OpenApiParameter("speed_mph_max", OpenApiTypes.NUMBER, description="Maximum roller coaster speed in mph"),
|
|
OpenApiParameter("inversions_min", OpenApiTypes.INT, description="Minimum number of inversions"),
|
|
OpenApiParameter("inversions_max", OpenApiTypes.INT, description="Maximum number of inversions"),
|
|
OpenApiParameter("has_inversions", OpenApiTypes.BOOL, description="Filter rides with inversions (true) or without (false)"),
|
|
OpenApiParameter("search", OpenApiTypes.STR, description="Search query for ride names, descriptions, parks, and related data"),
|
|
OpenApiParameter("offset", OpenApiTypes.INT, description="Offset for progressive loading (server-side pagination)"),
|
|
],
|
|
responses={
|
|
200: {
|
|
"description": "Rides data with hybrid filtering metadata",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"rides": {
|
|
"type": "array",
|
|
"items": {"$ref": "#/components/schemas/HybridRideSerializer"}
|
|
},
|
|
"total_count": {"type": "integer"},
|
|
"strategy": {
|
|
"type": "string",
|
|
"enum": ["client_side", "server_side"],
|
|
"description": "Filtering strategy used"
|
|
},
|
|
"has_more": {
|
|
"type": "boolean",
|
|
"description": "Whether more data is available for progressive loading"
|
|
},
|
|
"next_offset": {
|
|
"type": "integer",
|
|
"nullable": True,
|
|
"description": "Next offset for progressive loading"
|
|
},
|
|
"filter_metadata": {
|
|
"type": "object",
|
|
"description": "Available filter options and ranges"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
tags=["Rides"],
|
|
)
|
|
)
|
|
class HybridRideAPIView(APIView):
|
|
"""
|
|
Hybrid Ride API View with intelligent filtering strategy.
|
|
|
|
Automatically chooses between client-side and server-side filtering
|
|
based on data size and complexity. Provides progressive loading
|
|
for large datasets and complete data for smaller sets.
|
|
"""
|
|
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request):
|
|
"""Get rides with hybrid filtering strategy."""
|
|
try:
|
|
# Extract filters from query parameters
|
|
filters = self._extract_filters(request.query_params)
|
|
|
|
# Check if this is a progressive load request
|
|
offset = request.query_params.get('offset')
|
|
if offset is not None:
|
|
try:
|
|
offset = int(offset)
|
|
# Get progressive load data
|
|
data = smart_ride_loader.get_progressive_load(offset, filters)
|
|
except ValueError:
|
|
return Response(
|
|
{"error": "Invalid offset parameter"},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
else:
|
|
# Get initial load data
|
|
data = smart_ride_loader.get_initial_load(filters)
|
|
|
|
# Prepare response (rides are already serialized by the service)
|
|
response_data = {
|
|
'rides': data['rides'],
|
|
'total_count': data['total_count'],
|
|
'strategy': data.get('strategy', 'server_side'),
|
|
'has_more': data.get('has_more', False),
|
|
'next_offset': data.get('next_offset'),
|
|
}
|
|
|
|
# Include filter metadata for initial loads
|
|
if 'filter_metadata' in data:
|
|
response_data['filter_metadata'] = data['filter_metadata']
|
|
|
|
return Response(response_data, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in HybridRideAPIView: {e}")
|
|
return Response(
|
|
{"error": "Internal server error"},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
def _extract_filters(self, query_params):
|
|
"""Extract and parse filters from query parameters."""
|
|
filters = {}
|
|
|
|
# Handle comma-separated list parameters
|
|
list_params = ['category', 'status', 'manufacturer', 'designer', 'ride_model', 'roller_coaster_type', 'track_material', 'propulsion_system']
|
|
for param in list_params:
|
|
value = query_params.get(param)
|
|
if value:
|
|
filters[param] = [v.strip() for v in value.split(',') if v.strip()]
|
|
|
|
# Handle single value parameters
|
|
single_params = ['park_slug', 'park_id']
|
|
for param in single_params:
|
|
value = query_params.get(param)
|
|
if value:
|
|
if param == 'park_id':
|
|
try:
|
|
filters[param] = int(value)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
filters[param] = value
|
|
|
|
# Handle integer parameters
|
|
int_params = [
|
|
'opening_year_min', 'opening_year_max',
|
|
'height_requirement_min', 'height_requirement_max',
|
|
'capacity_min', 'capacity_max',
|
|
'inversions_min', 'inversions_max'
|
|
]
|
|
for param in int_params:
|
|
value = query_params.get(param)
|
|
if value:
|
|
try:
|
|
filters[param] = int(value)
|
|
except ValueError:
|
|
pass # Skip invalid integer values
|
|
|
|
# Handle float parameters
|
|
float_params = ['rating_min', 'rating_max', 'height_ft_min', 'height_ft_max', 'speed_mph_min', 'speed_mph_max']
|
|
for param in float_params:
|
|
value = query_params.get(param)
|
|
if value:
|
|
try:
|
|
filters[param] = float(value)
|
|
except ValueError:
|
|
pass # Skip invalid float values
|
|
|
|
# Handle boolean parameters
|
|
has_inversions = query_params.get('has_inversions')
|
|
if has_inversions is not None:
|
|
if has_inversions.lower() in ['true', '1', 'yes']:
|
|
filters['has_inversions'] = True
|
|
elif has_inversions.lower() in ['false', '0', 'no']:
|
|
filters['has_inversions'] = False
|
|
|
|
# Handle search parameter
|
|
search = query_params.get('search')
|
|
if search:
|
|
filters['search'] = search.strip()
|
|
|
|
return filters
|
|
|
|
|
|
@extend_schema_view(
|
|
get=extend_schema(
|
|
summary="Get ride filter metadata",
|
|
description="Get available filter options and ranges for rides filtering.",
|
|
parameters=[
|
|
OpenApiParameter("scoped", OpenApiTypes.BOOL, description="Whether to scope metadata to current filters"),
|
|
],
|
|
responses={
|
|
200: {
|
|
"description": "Filter metadata",
|
|
"content": {
|
|
"application/json": {
|
|
"schema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"categorical": {
|
|
"type": "object",
|
|
"properties": {
|
|
"categories": {"type": "array", "items": {"type": "string"}},
|
|
"statuses": {"type": "array", "items": {"type": "string"}},
|
|
"roller_coaster_types": {"type": "array", "items": {"type": "string"}},
|
|
"track_materials": {"type": "array", "items": {"type": "string"}},
|
|
"propulsion_systems": {"type": "array", "items": {"type": "string"}},
|
|
"parks": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string"},
|
|
"slug": {"type": "string"}
|
|
}
|
|
}
|
|
},
|
|
"manufacturers": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string"},
|
|
"slug": {"type": "string"}
|
|
}
|
|
}
|
|
},
|
|
"designers": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string"},
|
|
"slug": {"type": "string"}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"ranges": {
|
|
"type": "object",
|
|
"properties": {
|
|
"opening_year": {
|
|
"type": "object",
|
|
"properties": {
|
|
"min": {"type": "integer", "nullable": True},
|
|
"max": {"type": "integer", "nullable": True}
|
|
}
|
|
},
|
|
"rating": {
|
|
"type": "object",
|
|
"properties": {
|
|
"min": {"type": "number", "nullable": True},
|
|
"max": {"type": "number", "nullable": True}
|
|
}
|
|
},
|
|
"height_requirement": {
|
|
"type": "object",
|
|
"properties": {
|
|
"min": {"type": "integer", "nullable": True},
|
|
"max": {"type": "integer", "nullable": True}
|
|
}
|
|
},
|
|
"capacity": {
|
|
"type": "object",
|
|
"properties": {
|
|
"min": {"type": "integer", "nullable": True},
|
|
"max": {"type": "integer", "nullable": True}
|
|
}
|
|
},
|
|
"height_ft": {
|
|
"type": "object",
|
|
"properties": {
|
|
"min": {"type": "number", "nullable": True},
|
|
"max": {"type": "number", "nullable": True}
|
|
}
|
|
},
|
|
"speed_mph": {
|
|
"type": "object",
|
|
"properties": {
|
|
"min": {"type": "number", "nullable": True},
|
|
"max": {"type": "number", "nullable": True}
|
|
}
|
|
},
|
|
"inversions": {
|
|
"type": "object",
|
|
"properties": {
|
|
"min": {"type": "integer", "nullable": True},
|
|
"max": {"type": "integer", "nullable": True}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"total_count": {"type": "integer"}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
tags=["Rides"],
|
|
)
|
|
)
|
|
class RideFilterMetadataAPIView(APIView):
|
|
"""
|
|
API view for getting ride filter metadata.
|
|
|
|
Provides information about available filter options and ranges
|
|
to help build dynamic filter interfaces.
|
|
"""
|
|
|
|
permission_classes = [permissions.AllowAny]
|
|
|
|
def get(self, request):
|
|
"""Get ride filter metadata."""
|
|
try:
|
|
# Check if metadata should be scoped to current filters
|
|
scoped = request.query_params.get('scoped', '').lower() == 'true'
|
|
filters = None
|
|
|
|
if scoped:
|
|
filters = self._extract_filters(request.query_params)
|
|
|
|
# Get filter metadata
|
|
metadata = smart_ride_loader.get_filter_metadata(filters)
|
|
|
|
return Response(metadata, status=status.HTTP_200_OK)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in RideFilterMetadataAPIView: {e}")
|
|
return Response(
|
|
{"error": "Internal server error"},
|
|
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
|
)
|
|
|
|
def _extract_filters(self, query_params):
|
|
"""Extract and parse filters from query parameters."""
|
|
# Reuse the same filter extraction logic
|
|
view = HybridRideAPIView()
|
|
return view._extract_filters(query_params)
|