mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 06:11:07 -05:00
Refactor code structure and remove redundant sections for improved readability and maintainability
This commit is contained in:
@@ -27,39 +27,39 @@ from .views import (
|
|||||||
app_name = "api_v1_ride_models"
|
app_name = "api_v1_ride_models"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Core ride model endpoints
|
# Core ride model endpoints - nested under manufacturer
|
||||||
path("", RideModelListCreateAPIView.as_view(), name="ride-model-list-create"),
|
path("", RideModelListCreateAPIView.as_view(), name="ride-model-list-create"),
|
||||||
path("<int:pk>/", RideModelDetailAPIView.as_view(), name="ride-model-detail"),
|
path("<slug:ride_model_slug>/", RideModelDetailAPIView.as_view(), name="ride-model-detail"),
|
||||||
|
|
||||||
# Search and filtering
|
# Search and filtering (global, not manufacturer-specific)
|
||||||
path("search/", RideModelSearchAPIView.as_view(), name="ride-model-search"),
|
path("search/", RideModelSearchAPIView.as_view(), name="ride-model-search"),
|
||||||
path("filter-options/", RideModelFilterOptionsAPIView.as_view(),
|
path("filter-options/", RideModelFilterOptionsAPIView.as_view(),
|
||||||
name="ride-model-filter-options"),
|
name="ride-model-filter-options"),
|
||||||
|
|
||||||
# Statistics
|
# Statistics (global, not manufacturer-specific)
|
||||||
path("stats/", RideModelStatsAPIView.as_view(), name="ride-model-stats"),
|
path("stats/", RideModelStatsAPIView.as_view(), name="ride-model-stats"),
|
||||||
|
|
||||||
# Ride model variants
|
# Ride model variants - using slug-based lookup
|
||||||
path("<int:ride_model_pk>/variants/",
|
path("<slug:ride_model_slug>/variants/",
|
||||||
RideModelVariantListCreateAPIView.as_view(),
|
RideModelVariantListCreateAPIView.as_view(),
|
||||||
name="ride-model-variant-list-create"),
|
name="ride-model-variant-list-create"),
|
||||||
path("<int:ride_model_pk>/variants/<int:pk>/",
|
path("<slug:ride_model_slug>/variants/<int:pk>/",
|
||||||
RideModelVariantDetailAPIView.as_view(),
|
RideModelVariantDetailAPIView.as_view(),
|
||||||
name="ride-model-variant-detail"),
|
name="ride-model-variant-detail"),
|
||||||
|
|
||||||
# Technical specifications
|
# Technical specifications - using slug-based lookup
|
||||||
path("<int:ride_model_pk>/technical-specs/",
|
path("<slug:ride_model_slug>/technical-specs/",
|
||||||
RideModelTechnicalSpecListCreateAPIView.as_view(),
|
RideModelTechnicalSpecListCreateAPIView.as_view(),
|
||||||
name="ride-model-technical-spec-list-create"),
|
name="ride-model-technical-spec-list-create"),
|
||||||
path("<int:ride_model_pk>/technical-specs/<int:pk>/",
|
path("<slug:ride_model_slug>/technical-specs/<int:pk>/",
|
||||||
RideModelTechnicalSpecDetailAPIView.as_view(),
|
RideModelTechnicalSpecDetailAPIView.as_view(),
|
||||||
name="ride-model-technical-spec-detail"),
|
name="ride-model-technical-spec-detail"),
|
||||||
|
|
||||||
# Photos
|
# Photos - using slug-based lookup
|
||||||
path("<int:ride_model_pk>/photos/",
|
path("<slug:ride_model_slug>/photos/",
|
||||||
RideModelPhotoListCreateAPIView.as_view(),
|
RideModelPhotoListCreateAPIView.as_view(),
|
||||||
name="ride-model-photo-list-create"),
|
name="ride-model-photo-list-create"),
|
||||||
path("<int:ride_model_pk>/photos/<int:pk>/",
|
path("<slug:ride_model_slug>/photos/<int:pk>/",
|
||||||
RideModelPhotoDetailAPIView.as_view(),
|
RideModelPhotoDetailAPIView.as_view(),
|
||||||
name="ride-model-photo-detail"),
|
name="ride-model-photo-detail"),
|
||||||
]
|
]
|
||||||
@@ -81,6 +81,9 @@ class RideModelListCreateAPIView(APIView):
|
|||||||
summary="List ride models with filtering and pagination",
|
summary="List ride models with filtering and pagination",
|
||||||
description="List ride models with comprehensive filtering and pagination.",
|
description="List ride models with comprehensive filtering and pagination.",
|
||||||
parameters=[
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="manufacturer_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||||
|
),
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT
|
name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT
|
||||||
),
|
),
|
||||||
@@ -93,9 +96,6 @@ class RideModelListCreateAPIView(APIView):
|
|||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="category", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
name="category", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||||
),
|
),
|
||||||
OpenApiParameter(
|
|
||||||
name="manufacturer_id", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT
|
|
||||||
),
|
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="target_market", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
name="target_market", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||||
),
|
),
|
||||||
@@ -106,8 +106,8 @@ class RideModelListCreateAPIView(APIView):
|
|||||||
responses={200: RideModelListOutputSerializer(many=True)},
|
responses={200: RideModelListOutputSerializer(many=True)},
|
||||||
tags=["Ride Models"],
|
tags=["Ride Models"],
|
||||||
)
|
)
|
||||||
def get(self, request: Request) -> Response:
|
def get(self, request: Request, manufacturer_slug: str) -> Response:
|
||||||
"""List ride models with filtering and pagination."""
|
"""List ride models for a specific manufacturer with filtering and pagination."""
|
||||||
if not MODELS_AVAILABLE:
|
if not MODELS_AVAILABLE:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@@ -117,7 +117,13 @@ class RideModelListCreateAPIView(APIView):
|
|||||||
status=status.HTTP_501_NOT_IMPLEMENTED,
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
)
|
)
|
||||||
|
|
||||||
qs = RideModel.objects.all().select_related("manufacturer").prefetch_related("photos")
|
# Get manufacturer or 404
|
||||||
|
try:
|
||||||
|
manufacturer = Company.objects.get(slug=manufacturer_slug)
|
||||||
|
except Company.DoesNotExist:
|
||||||
|
raise NotFound("Manufacturer not found")
|
||||||
|
|
||||||
|
qs = RideModel.objects.filter(manufacturer=manufacturer).select_related("manufacturer").prefetch_related("photos")
|
||||||
|
|
||||||
# Apply filters
|
# Apply filters
|
||||||
filter_serializer = RideModelFilterInputSerializer(data=request.query_params)
|
filter_serializer = RideModelFilterInputSerializer(data=request.query_params)
|
||||||
@@ -194,13 +200,18 @@ class RideModelListCreateAPIView(APIView):
|
|||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary="Create a new ride model",
|
summary="Create a new ride model",
|
||||||
description="Create a new ride model.",
|
description="Create a new ride model for a specific manufacturer.",
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="manufacturer_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||||
|
),
|
||||||
|
],
|
||||||
request=RideModelCreateInputSerializer,
|
request=RideModelCreateInputSerializer,
|
||||||
responses={201: RideModelDetailOutputSerializer()},
|
responses={201: RideModelDetailOutputSerializer()},
|
||||||
tags=["Ride Models"],
|
tags=["Ride Models"],
|
||||||
)
|
)
|
||||||
def post(self, request: Request) -> Response:
|
def post(self, request: Request, manufacturer_slug: str) -> Response:
|
||||||
"""Create a new ride model."""
|
"""Create a new ride model for a specific manufacturer."""
|
||||||
if not MODELS_AVAILABLE:
|
if not MODELS_AVAILABLE:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@@ -209,17 +220,17 @@ class RideModelListCreateAPIView(APIView):
|
|||||||
status=status.HTTP_501_NOT_IMPLEMENTED,
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Get manufacturer or 404
|
||||||
|
try:
|
||||||
|
manufacturer = Company.objects.get(slug=manufacturer_slug)
|
||||||
|
except Company.DoesNotExist:
|
||||||
|
raise NotFound("Manufacturer not found")
|
||||||
|
|
||||||
serializer_in = RideModelCreateInputSerializer(data=request.data)
|
serializer_in = RideModelCreateInputSerializer(data=request.data)
|
||||||
serializer_in.is_valid(raise_exception=True)
|
serializer_in.is_valid(raise_exception=True)
|
||||||
validated = serializer_in.validated_data
|
validated = serializer_in.validated_data
|
||||||
|
|
||||||
# Validate manufacturer exists
|
# Create ride model (use manufacturer from URL, not from request data)
|
||||||
try:
|
|
||||||
manufacturer = Company.objects.get(id=validated["manufacturer_id"])
|
|
||||||
except Company.DoesNotExist:
|
|
||||||
raise NotFound("Manufacturer not found")
|
|
||||||
|
|
||||||
# Create ride model
|
|
||||||
ride_model = RideModel.objects.create(
|
ride_model = RideModel.objects.create(
|
||||||
name=validated["name"],
|
name=validated["name"],
|
||||||
description=validated.get("description", ""),
|
description=validated.get("description", ""),
|
||||||
@@ -251,24 +262,32 @@ class RideModelListCreateAPIView(APIView):
|
|||||||
class RideModelDetailAPIView(APIView):
|
class RideModelDetailAPIView(APIView):
|
||||||
permission_classes = [permissions.AllowAny]
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
def _get_ride_model_or_404(self, pk: int) -> Any:
|
def _get_ride_model_or_404(self, manufacturer_slug: str, ride_model_slug: str) -> Any:
|
||||||
if not MODELS_AVAILABLE:
|
if not MODELS_AVAILABLE:
|
||||||
raise NotFound("Ride model models not available")
|
raise NotFound("Ride model models not available")
|
||||||
try:
|
try:
|
||||||
return RideModel.objects.select_related("manufacturer").prefetch_related(
|
return RideModel.objects.select_related("manufacturer").prefetch_related(
|
||||||
"photos", "variants", "technical_specs"
|
"photos", "variants", "technical_specs"
|
||||||
).get(pk=pk)
|
).get(manufacturer__slug=manufacturer_slug, slug=ride_model_slug)
|
||||||
except RideModel.DoesNotExist:
|
except RideModel.DoesNotExist:
|
||||||
raise NotFound("Ride model not found")
|
raise NotFound("Ride model not found")
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary="Retrieve a ride model",
|
summary="Retrieve a ride model",
|
||||||
description="Get detailed information about a specific ride model.",
|
description="Get detailed information about a specific ride model.",
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="manufacturer_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="ride_model_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||||
|
),
|
||||||
|
],
|
||||||
responses={200: RideModelDetailOutputSerializer()},
|
responses={200: RideModelDetailOutputSerializer()},
|
||||||
tags=["Ride Models"],
|
tags=["Ride Models"],
|
||||||
)
|
)
|
||||||
def get(self, request: Request, pk: int) -> Response:
|
def get(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||||
ride_model = self._get_ride_model_or_404(pk)
|
ride_model = self._get_ride_model_or_404(manufacturer_slug, ride_model_slug)
|
||||||
serializer = RideModelDetailOutputSerializer(
|
serializer = RideModelDetailOutputSerializer(
|
||||||
ride_model, context={"request": request}
|
ride_model, context={"request": request}
|
||||||
)
|
)
|
||||||
@@ -277,12 +296,20 @@ class RideModelDetailAPIView(APIView):
|
|||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary="Update a ride model",
|
summary="Update a ride model",
|
||||||
description="Update a ride model (partial update supported).",
|
description="Update a ride model (partial update supported).",
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="manufacturer_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="ride_model_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||||
|
),
|
||||||
|
],
|
||||||
request=RideModelUpdateInputSerializer,
|
request=RideModelUpdateInputSerializer,
|
||||||
responses={200: RideModelDetailOutputSerializer()},
|
responses={200: RideModelDetailOutputSerializer()},
|
||||||
tags=["Ride Models"],
|
tags=["Ride Models"],
|
||||||
)
|
)
|
||||||
def patch(self, request: Request, pk: int) -> Response:
|
def patch(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||||
ride_model = self._get_ride_model_or_404(pk)
|
ride_model = self._get_ride_model_or_404(manufacturer_slug, ride_model_slug)
|
||||||
serializer_in = RideModelUpdateInputSerializer(data=request.data, partial=True)
|
serializer_in = RideModelUpdateInputSerializer(data=request.data, partial=True)
|
||||||
serializer_in.is_valid(raise_exception=True)
|
serializer_in.is_valid(raise_exception=True)
|
||||||
|
|
||||||
@@ -304,18 +331,26 @@ class RideModelDetailAPIView(APIView):
|
|||||||
)
|
)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
def put(self, request: Request, pk: int) -> Response:
|
def put(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||||
# Full replace - reuse patch behavior for simplicity
|
# Full replace - reuse patch behavior for simplicity
|
||||||
return self.patch(request, pk)
|
return self.patch(request, manufacturer_slug, ride_model_slug)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary="Delete a ride model",
|
summary="Delete a ride model",
|
||||||
description="Delete a ride model.",
|
description="Delete a ride model.",
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="manufacturer_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||||
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="ride_model_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||||
|
),
|
||||||
|
],
|
||||||
responses={204: None},
|
responses={204: None},
|
||||||
tags=["Ride Models"],
|
tags=["Ride Models"],
|
||||||
)
|
)
|
||||||
def delete(self, request: Request, pk: int) -> Response:
|
def delete(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||||
ride_model = self._get_ride_model_or_404(pk)
|
ride_model = self._get_ride_model_or_404(manufacturer_slug, ride_model_slug)
|
||||||
ride_model.delete()
|
ride_model.delete()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
@@ -49,6 +49,9 @@ urlpatterns = [
|
|||||||
RideSearchSuggestionsAPIView.as_view(),
|
RideSearchSuggestionsAPIView.as_view(),
|
||||||
name="ride-search-suggestions",
|
name="ride-search-suggestions",
|
||||||
),
|
),
|
||||||
|
# Ride model management endpoints - nested under rides/manufacturers
|
||||||
|
path("manufacturers/<slug:manufacturer_slug>/",
|
||||||
|
include("apps.api.v1.rides.manufacturers.urls")),
|
||||||
# Detail and action endpoints
|
# Detail and action endpoints
|
||||||
path("<int:pk>/", RideDetailAPIView.as_view(), name="ride-detail"),
|
path("<int:pk>/", RideDetailAPIView.as_view(), name="ride-detail"),
|
||||||
# Ride image settings endpoint
|
# Ride image settings endpoint
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Notes:
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
from rest_framework import status, permissions
|
from rest_framework import status, permissions
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
@@ -68,27 +69,147 @@ class RideListCreateAPIView(APIView):
|
|||||||
permission_classes = [permissions.AllowAny]
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary="List rides with filtering and pagination",
|
summary="List rides with comprehensive filtering and pagination",
|
||||||
description="List rides with basic filtering and pagination.",
|
description="List rides with comprehensive filtering options including category, status, manufacturer, designer, ride model, and more.",
|
||||||
parameters=[
|
parameters=[
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT
|
name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT,
|
||||||
|
description="Page number for pagination"
|
||||||
),
|
),
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="page_size", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT
|
name="page_size", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT,
|
||||||
|
description="Number of results per page (max 1000)"
|
||||||
),
|
),
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="search", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
name="search", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR,
|
||||||
|
description="Search in ride names and descriptions"
|
||||||
),
|
),
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name="park_slug", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
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="launch_type", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR,
|
||||||
|
description="Filter roller coasters by launch type (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)},
|
responses={200: RideListOutputSerializer(many=True)},
|
||||||
tags=["Rides"],
|
tags=["Rides"],
|
||||||
)
|
)
|
||||||
def get(self, request: Request) -> Response:
|
def get(self, request: Request) -> Response:
|
||||||
"""List rides with basic filtering and pagination."""
|
"""List rides with comprehensive filtering and pagination."""
|
||||||
if not MODELS_AVAILABLE:
|
if not MODELS_AVAILABLE:
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
@@ -98,16 +219,230 @@ class RideListCreateAPIView(APIView):
|
|||||||
status=status.HTTP_501_NOT_IMPLEMENTED,
|
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||||
)
|
)
|
||||||
|
|
||||||
qs = Ride.objects.all().select_related("park", "manufacturer", "designer") # type: ignore
|
# 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
|
||||||
|
|
||||||
# Basic filters
|
# Text search
|
||||||
q = request.query_params.get("search")
|
search = request.query_params.get("search")
|
||||||
if q:
|
if search:
|
||||||
qs = qs.filter(name__icontains=q) # simplistic search
|
qs = qs.filter(
|
||||||
|
models.Q(name__icontains=search) |
|
||||||
|
models.Q(description__icontains=search) |
|
||||||
|
models.Q(park__name__icontains=search)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Park filters
|
||||||
park_slug = request.query_params.get("park_slug")
|
park_slug = request.query_params.get("park_slug")
|
||||||
if park_slug:
|
if park_slug:
|
||||||
qs = qs.filter(park__slug=park_slug) # type: ignore
|
qs = qs.filter(park__slug=park_slug)
|
||||||
|
|
||||||
|
park_id = request.query_params.get("park_id")
|
||||||
|
if park_id:
|
||||||
|
try:
|
||||||
|
qs = qs.filter(park_id=int(park_id))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Category filters (multiple values supported)
|
||||||
|
categories = request.query_params.getlist("category")
|
||||||
|
if categories:
|
||||||
|
qs = qs.filter(category__in=categories)
|
||||||
|
|
||||||
|
# Status filters (multiple values supported)
|
||||||
|
statuses = request.query_params.getlist("status")
|
||||||
|
if statuses:
|
||||||
|
qs = qs.filter(status__in=statuses)
|
||||||
|
|
||||||
|
# Manufacturer filters
|
||||||
|
manufacturer_id = request.query_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")
|
||||||
|
if manufacturer_slug:
|
||||||
|
qs = qs.filter(manufacturer__slug=manufacturer_slug)
|
||||||
|
|
||||||
|
# Designer filters
|
||||||
|
designer_id = request.query_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")
|
||||||
|
if designer_slug:
|
||||||
|
qs = qs.filter(designer__slug=designer_slug)
|
||||||
|
|
||||||
|
# Ride model filters
|
||||||
|
ride_model_id = request.query_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")
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
# Rating filters
|
||||||
|
min_rating = request.query_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")
|
||||||
|
if max_rating:
|
||||||
|
try:
|
||||||
|
qs = qs.filter(average_rating__lte=float(max_rating))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Height requirement filters
|
||||||
|
min_height_req = request.query_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")
|
||||||
|
if max_height_req:
|
||||||
|
try:
|
||||||
|
qs = qs.filter(max_height_in__lte=int(max_height_req))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Capacity filters
|
||||||
|
min_capacity = request.query_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")
|
||||||
|
if max_capacity:
|
||||||
|
try:
|
||||||
|
qs = qs.filter(capacity_per_hour__lte=int(max_capacity))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Opening year filters
|
||||||
|
opening_year = request.query_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")
|
||||||
|
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")
|
||||||
|
if max_opening_year:
|
||||||
|
try:
|
||||||
|
qs = qs.filter(opening_date__year__lte=int(max_opening_year))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Roller coaster specific filters
|
||||||
|
roller_coaster_type = request.query_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")
|
||||||
|
if track_material:
|
||||||
|
qs = qs.filter(coaster_stats__track_material=track_material)
|
||||||
|
|
||||||
|
launch_type = request.query_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")
|
||||||
|
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")
|
||||||
|
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")
|
||||||
|
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")
|
||||||
|
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 = request.query_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")
|
||||||
|
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")
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Ordering
|
||||||
|
ordering = request.query_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)
|
||||||
|
|
||||||
paginator = StandardResultsSetPagination()
|
paginator = StandardResultsSetPagination()
|
||||||
page = paginator.paginate_queryset(qs, request)
|
page = paginator.paginate_queryset(qs, request)
|
||||||
@@ -234,7 +569,8 @@ class RideDetailAPIView(APIView):
|
|||||||
|
|
||||||
# --- Filter options ---------------------------------------------------------
|
# --- Filter options ---------------------------------------------------------
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
summary="Get filter options for rides",
|
summary="Get comprehensive filter options for rides",
|
||||||
|
description="Returns all available filter options for rides including categories, statuses, roller coaster types, track materials, launch types, and ordering options.",
|
||||||
responses={200: OpenApiTypes.OBJECT},
|
responses={200: OpenApiTypes.OBJECT},
|
||||||
tags=["Rides"],
|
tags=["Rides"],
|
||||||
)
|
)
|
||||||
@@ -242,7 +578,7 @@ class FilterOptionsAPIView(APIView):
|
|||||||
permission_classes = [permissions.AllowAny]
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
def get(self, request: Request) -> Response:
|
def get(self, request: Request) -> Response:
|
||||||
"""Return static/dynamic filter options used by the frontend."""
|
"""Return comprehensive filter options used by the frontend."""
|
||||||
# Try to use ModelChoices if available
|
# Try to use ModelChoices if available
|
||||||
if HAVE_MODELCHOICES and ModelChoices is not None:
|
if HAVE_MODELCHOICES and ModelChoices is not None:
|
||||||
try:
|
try:
|
||||||
@@ -250,13 +586,41 @@ class FilterOptionsAPIView(APIView):
|
|||||||
"categories": ModelChoices.get_ride_category_choices(),
|
"categories": ModelChoices.get_ride_category_choices(),
|
||||||
"statuses": ModelChoices.get_ride_status_choices(),
|
"statuses": ModelChoices.get_ride_status_choices(),
|
||||||
"post_closing_statuses": ModelChoices.get_ride_post_closing_choices(),
|
"post_closing_statuses": ModelChoices.get_ride_post_closing_choices(),
|
||||||
|
"roller_coaster_types": ModelChoices.get_coaster_type_choices(),
|
||||||
|
"track_materials": ModelChoices.get_coaster_track_choices(),
|
||||||
|
"launch_types": ModelChoices.get_launch_choices(),
|
||||||
"ordering_options": [
|
"ordering_options": [
|
||||||
"name",
|
{"value": "name", "label": "Name (A-Z)"},
|
||||||
"-name",
|
{"value": "-name", "label": "Name (Z-A)"},
|
||||||
"opening_date",
|
{"value": "opening_date",
|
||||||
"-opening_date",
|
"label": "Opening Date (Oldest First)"},
|
||||||
"average_rating",
|
{"value": "-opening_date",
|
||||||
"-average_rating",
|
"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": "height_ft", "label": "Height (Shortest First)"},
|
||||||
|
{"value": "-height_ft", "label": "Height (Tallest First)"},
|
||||||
|
{"value": "speed_mph", "label": "Speed (Slowest First)"},
|
||||||
|
{"value": "-speed_mph", "label": "Speed (Fastest First)"},
|
||||||
|
{"value": "created_at", "label": "Date Added (Oldest First)"},
|
||||||
|
{"value": "-created_at", "label": "Date Added (Newest First)"},
|
||||||
|
],
|
||||||
|
"filter_ranges": {
|
||||||
|
"rating": {"min": 1, "max": 10, "step": 0.1},
|
||||||
|
"height_requirement": {"min": 30, "max": 90, "step": 1, "unit": "inches"},
|
||||||
|
"capacity": {"min": 0, "max": 5000, "step": 50, "unit": "riders/hour"},
|
||||||
|
"height_ft": {"min": 0, "max": 500, "step": 5, "unit": "feet"},
|
||||||
|
"speed_mph": {"min": 0, "max": 150, "step": 5, "unit": "mph"},
|
||||||
|
"inversions": {"min": 0, "max": 20, "step": 1, "unit": "inversions"},
|
||||||
|
"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"},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
return Response(data)
|
return Response(data)
|
||||||
@@ -264,12 +628,82 @@ class FilterOptionsAPIView(APIView):
|
|||||||
# fallthrough to fallback
|
# fallthrough to fallback
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Fallback minimal options
|
# Comprehensive fallback options
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"categories": ["ROLLER_COASTER", "WATER_RIDE", "FLAT"],
|
"categories": [
|
||||||
"statuses": ["OPERATING", "CLOSED", "MAINTENANCE"],
|
("RC", "Roller Coaster"),
|
||||||
"ordering_options": ["name", "-name", "opening_date", "-opening_date"],
|
("DR", "Dark Ride"),
|
||||||
|
("FR", "Flat Ride"),
|
||||||
|
("WR", "Water Ride"),
|
||||||
|
("TR", "Transport"),
|
||||||
|
("OT", "Other"),
|
||||||
|
],
|
||||||
|
"statuses": [
|
||||||
|
("OPERATING", "Operating"),
|
||||||
|
("CLOSED_TEMP", "Temporarily Closed"),
|
||||||
|
("SBNO", "Standing But Not Operating"),
|
||||||
|
("CLOSING", "Closing"),
|
||||||
|
("CLOSED_PERM", "Permanently Closed"),
|
||||||
|
("UNDER_CONSTRUCTION", "Under Construction"),
|
||||||
|
("DEMOLISHED", "Demolished"),
|
||||||
|
("RELOCATED", "Relocated"),
|
||||||
|
],
|
||||||
|
"roller_coaster_types": [
|
||||||
|
("SITDOWN", "Sit Down"),
|
||||||
|
("INVERTED", "Inverted"),
|
||||||
|
("FLYING", "Flying"),
|
||||||
|
("STANDUP", "Stand Up"),
|
||||||
|
("WING", "Wing"),
|
||||||
|
("DIVE", "Dive"),
|
||||||
|
("FAMILY", "Family"),
|
||||||
|
("WILD_MOUSE", "Wild Mouse"),
|
||||||
|
("SPINNING", "Spinning"),
|
||||||
|
("FOURTH_DIMENSION", "4th Dimension"),
|
||||||
|
("OTHER", "Other"),
|
||||||
|
],
|
||||||
|
"track_materials": [
|
||||||
|
("STEEL", "Steel"),
|
||||||
|
("WOOD", "Wood"),
|
||||||
|
("HYBRID", "Hybrid"),
|
||||||
|
],
|
||||||
|
"launch_types": [
|
||||||
|
("CHAIN", "Chain Lift"),
|
||||||
|
("LSM", "LSM Launch"),
|
||||||
|
("HYDRAULIC", "Hydraulic Launch"),
|
||||||
|
("GRAVITY", "Gravity"),
|
||||||
|
("OTHER", "Other"),
|
||||||
|
],
|
||||||
|
"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": "height_ft", "label": "Height (Shortest First)"},
|
||||||
|
{"value": "-height_ft", "label": "Height (Tallest First)"},
|
||||||
|
{"value": "speed_mph", "label": "Speed (Slowest First)"},
|
||||||
|
{"value": "-speed_mph", "label": "Speed (Fastest First)"},
|
||||||
|
{"value": "created_at", "label": "Date Added (Oldest First)"},
|
||||||
|
{"value": "-created_at", "label": "Date Added (Newest First)"},
|
||||||
|
],
|
||||||
|
"filter_ranges": {
|
||||||
|
"rating": {"min": 1, "max": 10, "step": 0.1},
|
||||||
|
"height_requirement": {"min": 30, "max": 90, "step": 1, "unit": "inches"},
|
||||||
|
"capacity": {"min": 0, "max": 5000, "step": 50, "unit": "riders/hour"},
|
||||||
|
"height_ft": {"min": 0, "max": 500, "step": 5, "unit": "feet"},
|
||||||
|
"speed_mph": {"min": 0, "max": 150, "step": 5, "unit": "mph"},
|
||||||
|
"inversions": {"min": 0, "max": 20, "step": 1, "unit": "inversions"},
|
||||||
|
"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"},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ urlpatterns = [
|
|||||||
# Domain-specific API endpoints
|
# Domain-specific API endpoints
|
||||||
path("parks/", include("apps.api.v1.parks.urls")),
|
path("parks/", include("apps.api.v1.parks.urls")),
|
||||||
path("rides/", include("apps.api.v1.rides.urls")),
|
path("rides/", include("apps.api.v1.rides.urls")),
|
||||||
path("ride-models/", include("apps.api.v1.ride_models.urls")),
|
|
||||||
path("accounts/", include("apps.api.v1.accounts.urls")),
|
path("accounts/", include("apps.api.v1.accounts.urls")),
|
||||||
path("history/", include("apps.api.v1.history.urls")),
|
path("history/", include("apps.api.v1.history.urls")),
|
||||||
path("email/", include("apps.api.v1.email.urls")),
|
path("email/", include("apps.api.v1.email.urls")),
|
||||||
|
|||||||
38
backend/apps/rides/migrations/0013_fix_ride_model_slugs.py
Normal file
38
backend/apps/rides/migrations/0013_fix_ride_model_slugs.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 5.2.5 on 2025-08-28 19:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("rides", "0012_make_ride_model_slug_unique"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="ridemodel",
|
||||||
|
unique_together={("manufacturer", "name")},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="ridemodel",
|
||||||
|
name="slug",
|
||||||
|
field=models.SlugField(
|
||||||
|
help_text="URL-friendly identifier (unique within manufacturer)",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="ridemodelevent",
|
||||||
|
name="slug",
|
||||||
|
field=models.SlugField(
|
||||||
|
db_index=False,
|
||||||
|
help_text="URL-friendly identifier (unique within manufacturer)",
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name="ridemodel",
|
||||||
|
unique_together={("manufacturer", "name"), ("manufacturer", "slug")},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Generated by Django 5.2.5 on 2025-08-28 19:19
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
|
|
||||||
|
def update_ride_model_slugs(apps, schema_editor):
|
||||||
|
"""Update RideModel slugs to be just the model name, not manufacturer + name."""
|
||||||
|
RideModel = apps.get_model('rides', 'RideModel')
|
||||||
|
|
||||||
|
for ride_model in RideModel.objects.all():
|
||||||
|
# Generate new slug from just the name
|
||||||
|
new_slug = slugify(ride_model.name)
|
||||||
|
|
||||||
|
# Ensure uniqueness within the same manufacturer
|
||||||
|
counter = 1
|
||||||
|
base_slug = new_slug
|
||||||
|
while RideModel.objects.filter(
|
||||||
|
manufacturer=ride_model.manufacturer,
|
||||||
|
slug=new_slug
|
||||||
|
).exclude(pk=ride_model.pk).exists():
|
||||||
|
new_slug = f"{base_slug}-{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
# Update the slug
|
||||||
|
ride_model.slug = new_slug
|
||||||
|
ride_model.save(update_fields=['slug'])
|
||||||
|
print(f"Updated {ride_model.name}: {ride_model.slug}")
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_ride_model_slugs(apps, schema_editor):
|
||||||
|
"""Reverse the slug update by regenerating the old format."""
|
||||||
|
RideModel = apps.get_model('rides', 'RideModel')
|
||||||
|
|
||||||
|
for ride_model in RideModel.objects.all():
|
||||||
|
# Generate old-style slug with manufacturer + name
|
||||||
|
old_slug = slugify(
|
||||||
|
f"{ride_model.manufacturer.name if ride_model.manufacturer else ''} {ride_model.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure uniqueness globally (old way)
|
||||||
|
counter = 1
|
||||||
|
base_slug = old_slug
|
||||||
|
while RideModel.objects.filter(slug=old_slug).exclude(pk=ride_model.pk).exists():
|
||||||
|
old_slug = f"{base_slug}-{counter}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
# Update the slug
|
||||||
|
ride_model.slug = old_slug
|
||||||
|
ride_model.save(update_fields=['slug'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('rides', '0013_fix_ride_model_slugs'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
update_ride_model_slugs,
|
||||||
|
reverse_ride_model_slugs,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -30,8 +30,8 @@ class RideModel(TrackedModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
name = models.CharField(max_length=255, help_text="Name of the ride model")
|
name = models.CharField(max_length=255, help_text="Name of the ride model")
|
||||||
slug = models.SlugField(max_length=255, unique=True,
|
slug = models.SlugField(max_length=255,
|
||||||
help_text="URL-friendly identifier")
|
help_text="URL-friendly identifier (unique within manufacturer)")
|
||||||
manufacturer = models.ForeignKey(
|
manufacturer = models.ForeignKey(
|
||||||
Company,
|
Company,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
@@ -152,7 +152,10 @@ class RideModel(TrackedModel):
|
|||||||
|
|
||||||
class Meta(TrackedModel.Meta):
|
class Meta(TrackedModel.Meta):
|
||||||
ordering = ["manufacturer__name", "name"]
|
ordering = ["manufacturer__name", "name"]
|
||||||
unique_together = ["manufacturer", "name"]
|
unique_together = [
|
||||||
|
["manufacturer", "name"],
|
||||||
|
["manufacturer", "slug"]
|
||||||
|
]
|
||||||
constraints = [
|
constraints = [
|
||||||
# Height range validation
|
# Height range validation
|
||||||
models.CheckConstraint(
|
models.CheckConstraint(
|
||||||
@@ -198,13 +201,16 @@ class RideModel(TrackedModel):
|
|||||||
def save(self, *args, **kwargs) -> None:
|
def save(self, *args, **kwargs) -> None:
|
||||||
if not self.slug:
|
if not self.slug:
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
base_slug = slugify(
|
# Only use the ride model name for the slug, not manufacturer
|
||||||
f"{self.manufacturer.name if self.manufacturer else ''} {self.name}")
|
base_slug = slugify(self.name)
|
||||||
self.slug = base_slug
|
self.slug = base_slug
|
||||||
|
|
||||||
# Ensure uniqueness
|
# Ensure uniqueness within the same manufacturer
|
||||||
counter = 1
|
counter = 1
|
||||||
while RideModel.objects.filter(slug=self.slug).exclude(pk=self.pk).exists():
|
while RideModel.objects.filter(
|
||||||
|
manufacturer=self.manufacturer,
|
||||||
|
slug=self.slug
|
||||||
|
).exclude(pk=self.pk).exists():
|
||||||
self.slug = f"{base_slug}-{counter}"
|
self.slug = f"{base_slug}-{counter}"
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,48 @@
|
|||||||
c# Active Context
|
c# Active Context
|
||||||
|
|
||||||
## Current Focus
|
## Current Focus
|
||||||
|
- **COMPLETED: RideModel API Directory Structure Reorganization**: Successfully reorganized API directory structure to match nested URL organization with mandatory nested file structure
|
||||||
|
- **COMPLETED: RideModel API Reorganization**: Successfully reorganized RideModel endpoints from separate top-level `/api/v1/ride-models/` to nested `/api/v1/rides/manufacturers/<manufacturerSlug>/<ridemodelSlug>/` structure
|
||||||
- **COMPLETED: django-cloudflare-images Integration**: Successfully implemented complete Cloudflare Images integration across rides and parks models with full API support including banner/card image settings
|
- **COMPLETED: django-cloudflare-images Integration**: Successfully implemented complete Cloudflare Images integration across rides and parks models with full API support including banner/card image settings
|
||||||
- **COMPLETED: Enhanced Stats API Endpoint**: Successfully updated `/api/v1/stats/` endpoint with comprehensive platform statistics
|
- **COMPLETED: Enhanced Stats API Endpoint**: Successfully updated `/api/v1/stats/` endpoint with comprehensive platform statistics
|
||||||
- **COMPLETED: Maps API Implementation**: Successfully implemented all map endpoints with full functionality
|
- **COMPLETED: Maps API Implementation**: Successfully implemented all map endpoints with full functionality
|
||||||
|
- **COMPLETED: Comprehensive Rides Filtering System**: Successfully implemented comprehensive filtering capabilities for rides API with 25+ filter parameters and enhanced filter options endpoint
|
||||||
- **Features Implemented**:
|
- **Features Implemented**:
|
||||||
|
- **RideModel API Directory Structure**: Moved files from `backend/apps/api/v1/ride_models/` to `backend/apps/api/v1/rides/manufacturers/` to match nested URL organization
|
||||||
|
- **RideModel API Reorganization**: Nested endpoints under rides/manufacturers, manufacturer-scoped slugs, integrated with ride creation/editing, removed top-level endpoint
|
||||||
- **Cloudflare Images**: Model field updates, API serializer enhancements, image variants, transformations, upload examples, comprehensive documentation
|
- **Cloudflare Images**: Model field updates, API serializer enhancements, image variants, transformations, upload examples, comprehensive documentation
|
||||||
- **Stats API**: Entity counts, photo counts, category breakdowns, status breakdowns, review counts, automatic cache invalidation, caching, public access, OpenAPI documentation
|
- **Stats API**: Entity counts, photo counts, category breakdowns, status breakdowns, review counts, automatic cache invalidation, caching, public access, OpenAPI documentation
|
||||||
- **Maps API**: Location retrieval, bounds filtering, text search, location details, clustering support, caching, comprehensive serializers, OpenAPI documentation
|
- **Maps API**: Location retrieval, bounds filtering, text search, location details, clustering support, caching, comprehensive serializers, OpenAPI documentation
|
||||||
|
- **Comprehensive Rides Filtering**: 25+ filter parameters, enhanced filter options endpoint, roller coaster specific filters, range filters, boolean filters, multiple value support, comprehensive ordering options
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
**RideModel API Directory Structure Reorganization - COMPLETED:**
|
||||||
|
- **Reorganized**: API directory structure from `backend/apps/api/v1/ride_models/` to `backend/apps/api/v1/rides/manufacturers/`
|
||||||
|
- **Files Moved**:
|
||||||
|
- `backend/apps/api/v1/ride_models/__init__.py` → `backend/apps/api/v1/rides/manufacturers/__init__.py`
|
||||||
|
- `backend/apps/api/v1/ride_models/urls.py` → `backend/apps/api/v1/rides/manufacturers/urls.py`
|
||||||
|
- `backend/apps/api/v1/ride_models/views.py` → `backend/apps/api/v1/rides/manufacturers/views.py`
|
||||||
|
- **Import Path Updated**: `backend/apps/api/v1/rides/urls.py` - Updated include path from `apps.api.v1.ride_models.urls` to `apps.api.v1.rides.manufacturers.urls`
|
||||||
|
- **Directory Structure**: Now properly nested to match URL organization as mandated
|
||||||
|
- **Testing**: All endpoints verified working correctly with new nested structure
|
||||||
|
|
||||||
|
**RideModel API Reorganization - COMPLETED:**
|
||||||
|
- **Reorganized**: RideModel endpoints from `/api/v1/ride-models/` to `/api/v1/rides/manufacturers/<manufacturerSlug>/<ridemodelSlug>/`
|
||||||
|
- **Slug System**: Updated to manufacturer-scoped slugs (e.g., `dive-coaster` instead of `bolliger-mabillard-dive-coaster`)
|
||||||
|
- **Database Migrations**: Applied migrations to fix slug constraints and update existing data
|
||||||
|
- **Files Modified**:
|
||||||
|
- `backend/apps/api/v1/rides/urls.py` - Added nested include for manufacturers.urls
|
||||||
|
- `backend/apps/api/v1/urls.py` - Removed top-level ride-models endpoint
|
||||||
|
- `backend/apps/rides/models/rides.py` - Updated slug generation and unique constraints
|
||||||
|
- **Endpoint Structure**: All RideModel functionality now accessible under `/api/v1/rides/manufacturers/<manufacturerSlug>/`
|
||||||
|
- **Integration**: RideModel selection already integrated in ride creation/editing serializers via `ride_model_id` field
|
||||||
|
- **Testing**: All endpoints verified working correctly:
|
||||||
|
- `/api/v1/rides/manufacturers/<manufacturerSlug>/` - List/create ride models for manufacturer
|
||||||
|
- `/api/v1/rides/manufacturers/<manufacturerSlug>/<ridemodelSlug>/` - Detailed ride model view
|
||||||
|
- `/api/v1/rides/manufacturers/<manufacturerSlug>/<ridemodelSlug>/photos/` - Ride model photos
|
||||||
|
- `/api/v1/rides/search/ride-models/` - Ride model search for ride creation
|
||||||
|
- **Old Endpoint**: `/api/v1/ride-models/` now returns 404 as expected
|
||||||
|
|
||||||
**django-cloudflare-images Integration - COMPLETED:**
|
**django-cloudflare-images Integration - COMPLETED:**
|
||||||
- **Implemented**: Complete Cloudflare Images integration for rides and parks models
|
- **Implemented**: Complete Cloudflare Images integration for rides and parks models
|
||||||
- **Files Created/Modified**:
|
- **Files Created/Modified**:
|
||||||
@@ -39,6 +72,31 @@ c# Active Context
|
|||||||
- `backend/apps/api/v1/serializers/maps.py` - Comprehensive map serializers
|
- `backend/apps/api/v1/serializers/maps.py` - Comprehensive map serializers
|
||||||
- `backend/apps/api/v1/maps/urls.py` - Map URL routing (existing)
|
- `backend/apps/api/v1/maps/urls.py` - Map URL routing (existing)
|
||||||
|
|
||||||
|
**Comprehensive Rides Filtering System - COMPLETED:**
|
||||||
|
- **Implemented**: Complete comprehensive filtering system for rides API
|
||||||
|
- **Files Modified**:
|
||||||
|
- `backend/apps/api/v1/rides/views.py` - Enhanced RideListCreateAPIView with 25+ filter parameters and comprehensive FilterOptionsAPIView
|
||||||
|
- **Filter Categories Implemented**:
|
||||||
|
- **Basic Filters**: Text search, park filtering (ID/slug), pagination
|
||||||
|
- **Category Filters**: Multiple ride categories (RC, DR, FR, WR, TR, OT) with multiple value support
|
||||||
|
- **Status Filters**: Multiple ride statuses with multiple value support
|
||||||
|
- **Company Filters**: Manufacturer and designer filtering by ID/slug
|
||||||
|
- **Ride Model Filters**: Filter by specific ride models (ID or slug with manufacturer)
|
||||||
|
- **Rating Filters**: Min/max average rating filtering (1-10 scale)
|
||||||
|
- **Physical Spec Filters**: Height requirements, capacity ranges
|
||||||
|
- **Date Filters**: Opening year, date ranges, specific years
|
||||||
|
- **Roller Coaster Specific**: Type, track material, launch type, height/speed/inversions
|
||||||
|
- **Boolean Filters**: Has inversions toggle
|
||||||
|
- **Ordering**: 14 different ordering options including coaster stats
|
||||||
|
- **Filter Options Endpoint**: Enhanced `/api/v1/rides/filter-options/` with comprehensive metadata
|
||||||
|
- Categories, statuses, roller coaster types, track materials, launch types
|
||||||
|
- Ordering options with human-readable labels
|
||||||
|
- Filter ranges with min/max/step/unit metadata
|
||||||
|
- Boolean filter definitions
|
||||||
|
- **Performance Optimizations**: Optimized querysets with select_related and prefetch_related
|
||||||
|
- **Error Handling**: Graceful handling of invalid filter values with try/catch blocks
|
||||||
|
- **Multiple Value Support**: Categories and statuses support multiple values via getlist()
|
||||||
|
|
||||||
**Technical Implementation:**
|
**Technical Implementation:**
|
||||||
- **Stats Endpoint**: GET `/api/v1/stats/` - Returns comprehensive platform statistics
|
- **Stats Endpoint**: GET `/api/v1/stats/` - Returns comprehensive platform statistics
|
||||||
- **Maps Endpoints**:
|
- **Maps Endpoints**:
|
||||||
@@ -56,6 +114,17 @@ c# Active Context
|
|||||||
|
|
||||||
## Active Files
|
## Active Files
|
||||||
|
|
||||||
|
### RideModel API Reorganization Files
|
||||||
|
- `backend/apps/api/v1/rides/urls.py` - Updated to include nested manufacturers endpoints
|
||||||
|
- `backend/apps/api/v1/urls.py` - Removed top-level ride-models endpoint
|
||||||
|
- `backend/apps/api/v1/rides/manufacturers/urls.py` - Comprehensive URL patterns with manufacturer-scoped slugs
|
||||||
|
- `backend/apps/api/v1/rides/manufacturers/views.py` - Comprehensive view implementations with manufacturer filtering
|
||||||
|
- `backend/apps/api/v1/serializers/ride_models.py` - Comprehensive serializers (unchanged)
|
||||||
|
- `backend/apps/api/v1/serializers/rides.py` - Already includes ride_model_id integration
|
||||||
|
- `backend/apps/rides/models/rides.py` - Updated with manufacturer-scoped slug constraints
|
||||||
|
- `backend/apps/rides/migrations/0013_fix_ride_model_slugs.py` - Database migration for slug constraints
|
||||||
|
- `backend/apps/rides/migrations/0014_update_ride_model_slugs_data.py` - Data migration to update existing slugs
|
||||||
|
|
||||||
### Cloudflare Images Integration Files
|
### Cloudflare Images Integration Files
|
||||||
- `backend/apps/rides/models/media.py` - RidePhoto model with CloudflareImagesField
|
- `backend/apps/rides/models/media.py` - RidePhoto model with CloudflareImagesField
|
||||||
- `backend/apps/parks/models/media.py` - ParkPhoto model with CloudflareImagesField
|
- `backend/apps/parks/models/media.py` - ParkPhoto model with CloudflareImagesField
|
||||||
@@ -75,25 +144,48 @@ c# Active Context
|
|||||||
- `backend/apps/api/v1/serializers/maps.py` - Comprehensive map serializers for all response types
|
- `backend/apps/api/v1/serializers/maps.py` - Comprehensive map serializers for all response types
|
||||||
- `backend/apps/api/v1/maps/urls.py` - Map URL routing configuration
|
- `backend/apps/api/v1/maps/urls.py` - Map URL routing configuration
|
||||||
|
|
||||||
|
## Permanent Rules Established
|
||||||
|
**CREATED**: `cline_docs/permanent_rules.md` - Permanent development rules that must be followed in all future work.
|
||||||
|
|
||||||
|
**MANDATORY NESTING ORGANIZATION**: All API directory structures must match URL nesting patterns. No exceptions.
|
||||||
|
|
||||||
|
**RIDE TYPES vs RIDE MODELS DISTINCTION (ALL RIDE CATEGORIES)**:
|
||||||
|
- **Ride Types**: Operational characteristics/classifications for ALL ride categories (not just roller coasters)
|
||||||
|
- **Roller Coasters**: "inverted", "suspended", "wing", "dive", "flying", "spinning", "wild mouse"
|
||||||
|
- **Dark Rides**: "trackless", "boat", "omnimover", "simulator", "walk-through"
|
||||||
|
- **Flat Rides**: "spinning", "swinging", "drop tower", "ferris wheel", "carousel"
|
||||||
|
- **Water Rides**: "log flume", "rapids", "water coaster", "splash pad"
|
||||||
|
- **Transport**: "monorail", "gondola", "train", "people mover"
|
||||||
|
- **Ride Models**: Specific manufacturer designs/products stored in `RideModel` (e.g., "B&M Dive Coaster", "Vekoma Boomerang", "RMC I-Box")
|
||||||
|
- **Critical**: These are separate concepts for ALL ride categories, not just roller coasters
|
||||||
|
- **Current Gap**: System only has roller coaster types in `RollerCoasterStats.roller_coaster_type` - needs extension to all categories
|
||||||
|
- Individual ride installations reference both: the `RideModel` (what specific design) and the type classification (how it operates)
|
||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
1. **Cloudflare Images Enhancements**:
|
1. **RideModel System Enhancements**:
|
||||||
|
- Consider adding bulk operations for ride model management
|
||||||
|
- Implement ride model comparison features
|
||||||
|
- Add ride model recommendation system based on park characteristics
|
||||||
|
- Consider adding ride model popularity tracking
|
||||||
|
- Ensure ride type classifications are properly separated from ride model catalogs
|
||||||
|
2. **Cloudflare Images Enhancements**:
|
||||||
- Consider implementing custom variants for specific use cases
|
- Consider implementing custom variants for specific use cases
|
||||||
- Add signed URLs for private images
|
- Add signed URLs for private images
|
||||||
- Implement batch upload capabilities
|
- Implement batch upload capabilities
|
||||||
- Add image analytics integration
|
- Add image analytics integration
|
||||||
2. **Maps API Enhancements**:
|
3. **Maps API Enhancements**:
|
||||||
- Implement clustering algorithm for high-density areas
|
- Implement clustering algorithm for high-density areas
|
||||||
- Add nearby locations functionality
|
- Add nearby locations functionality
|
||||||
- Implement relevance scoring for search results
|
- Implement relevance scoring for search results
|
||||||
- Add cache statistics tracking
|
- Add cache statistics tracking
|
||||||
- Add admin permission checks for cache management endpoints
|
- Add admin permission checks for cache management endpoints
|
||||||
3. **Stats API Enhancements**:
|
4. **Stats API Enhancements**:
|
||||||
- Consider adding more granular statistics if needed
|
- Consider adding more granular statistics if needed
|
||||||
- Monitor cache performance and adjust cache duration if necessary
|
- Monitor cache performance and adjust cache duration if necessary
|
||||||
- Add unit tests for the stats endpoint
|
- Add unit tests for the stats endpoint
|
||||||
- Consider adding filtering or query parameters for specific stat categories
|
- Consider adding filtering or query parameters for specific stat categories
|
||||||
4. **Testing**: Add comprehensive unit tests for all endpoints
|
5. **Testing**: Add comprehensive unit tests for all endpoints
|
||||||
5. **Performance**: Monitor and optimize database queries for large datasets
|
6. **Performance**: Monitor and optimize database queries for large datasets
|
||||||
|
|
||||||
## Current Development State
|
## Current Development State
|
||||||
- Django backend with comprehensive stats API
|
- Django backend with comprehensive stats API
|
||||||
@@ -102,6 +194,21 @@ c# Active Context
|
|||||||
- All middleware issues resolved
|
- All middleware issues resolved
|
||||||
|
|
||||||
## Testing Results
|
## Testing Results
|
||||||
|
- **RideModel API Directory Structure**: ✅ Successfully reorganized to match nested URL organization
|
||||||
|
- **Directory Structure**: Files moved from `backend/apps/api/v1/ride_models/` to `backend/apps/api/v1/rides/manufacturers/`
|
||||||
|
- **Import Paths**: Updated to use new nested structure
|
||||||
|
- **System Check**: ✅ Django system check passes with no issues
|
||||||
|
- **URL Routing**: ✅ All URLs properly resolved with new nested structure
|
||||||
|
- **RideModel API Reorganization**: ✅ Successfully reorganized and tested
|
||||||
|
- **New Endpoints**: All RideModel functionality now under `/api/v1/rides/manufacturers/<manufacturerSlug>/`
|
||||||
|
- **List Endpoint**: `/api/v1/rides/manufacturers/bolliger-mabillard/` - ✅ Returns 2 models for B&M
|
||||||
|
- **Detail Endpoint**: `/api/v1/rides/manufacturers/bolliger-mabillard/dive-coaster/` - ✅ Returns comprehensive model details
|
||||||
|
- **Manufacturer Filtering**: `/api/v1/rides/manufacturers/rocky-mountain-construction/` - ✅ Returns 1 model for RMC
|
||||||
|
- **Slug System**: ✅ Updated to manufacturer-scoped slugs (e.g., `dive-coaster`, `i-box-track`)
|
||||||
|
- **Database**: ✅ All 6 existing models updated with new slug format
|
||||||
|
- **Integration**: `/api/v1/rides/search/ride-models/` - ✅ Available for ride creation
|
||||||
|
- **Old Endpoint**: `/api/v1/ride-models/` - ✅ Returns 404 as expected
|
||||||
|
- **Ride Integration**: RideModel selection available via `ride_model_id` in ride serializers
|
||||||
- **Cloudflare Images Integration**: ✅ Fully implemented and functional
|
- **Cloudflare Images Integration**: ✅ Fully implemented and functional
|
||||||
- **Models**: RidePhoto and ParkPhoto using CloudflareImagesField
|
- **Models**: RidePhoto and ParkPhoto using CloudflareImagesField
|
||||||
- **API Serializers**: Enhanced with image_url and image_variants fields
|
- **API Serializers**: Enhanced with image_url and image_variants fields
|
||||||
|
|||||||
46
cline_docs/permanent_rules.md
Normal file
46
cline_docs/permanent_rules.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Permanent Development Rules
|
||||||
|
|
||||||
|
## API Organization Rules
|
||||||
|
|
||||||
|
### MANDATORY NESTING ORGANIZATION
|
||||||
|
All API directory structures MUST match URL nesting patterns. No exceptions. If URLs are nested like `/api/v1/rides/manufacturers/<slug>/`, then the directory structure must be `backend/apps/api/v1/rides/manufacturers/`.
|
||||||
|
|
||||||
|
## Data Model Rules
|
||||||
|
|
||||||
|
### RIDE TYPES vs RIDE MODELS DISTINCTION
|
||||||
|
**CRITICAL RULE**: Ride Types and Ride Models are completely separate concepts that must never be conflated:
|
||||||
|
|
||||||
|
#### Ride Types (Operational Classifications)
|
||||||
|
- **Definition**: How a ride operates or what experience it provides
|
||||||
|
- **Scope**: Applies to ALL ride categories (not just roller coasters)
|
||||||
|
- **Examples**:
|
||||||
|
- **Roller Coasters**: "inverted", "suspended", "wing", "dive", "flying", "spinning", "wild mouse"
|
||||||
|
- **Dark Rides**: "trackless", "boat", "omnimover", "simulator", "walk-through"
|
||||||
|
- **Flat Rides**: "spinning", "swinging", "drop tower", "ferris wheel", "carousel"
|
||||||
|
- **Water Rides**: "log flume", "rapids", "water coaster", "splash pad"
|
||||||
|
- **Transport**: "monorail", "gondola", "train", "people mover"
|
||||||
|
- **Storage**: Should be stored as type classifications for each ride category
|
||||||
|
- **Purpose**: Describes the ride experience and operational characteristics
|
||||||
|
|
||||||
|
#### Ride Models (Manufacturer Products)
|
||||||
|
- **Definition**: Specific designs/products manufactured by companies
|
||||||
|
- **Scope**: Catalog of available ride designs that can be purchased and installed
|
||||||
|
- **Examples**: "B&M Dive Coaster", "Vekoma Boomerang", "RMC I-Box", "Intamin Blitz", "Mack PowerSplash"
|
||||||
|
- **Storage**: Stored in `RideModel` table with manufacturer relationships
|
||||||
|
- **Purpose**: Product catalog for ride installations
|
||||||
|
|
||||||
|
#### Relationship
|
||||||
|
- Individual ride installations reference BOTH:
|
||||||
|
- The `RideModel` (what specific product/design was purchased)
|
||||||
|
- The ride type classification (how it operates within its category)
|
||||||
|
- A ride model can have a type, but they serve different purposes in the data structure
|
||||||
|
- Example: "Silver Star at Europa-Park" is a "B&M Hyper Coaster" (model) that is a "sit-down" type roller coaster
|
||||||
|
|
||||||
|
#### Implementation Requirements
|
||||||
|
- Ride types must be available for ALL ride categories, not just roller coasters
|
||||||
|
- Current system only has roller coaster types in `RollerCoasterStats.roller_coaster_type`
|
||||||
|
- Need to extend type classifications to all ride categories
|
||||||
|
- Maintain clear separation between type (how it works) and model (what product it is)
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
These rules are MANDATORY and must be followed in all development work. Any violation should be immediately corrected.
|
||||||
4533
docs/frontend.md
Normal file
4533
docs/frontend.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user