Refactor code structure and remove redundant sections for improved readability and maintainability

This commit is contained in:
pacnpal
2025-08-28 16:01:24 -04:00
parent 67db0aa46e
commit 02ac587216
12 changed files with 5342 additions and 77 deletions

View File

@@ -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"),
] ]

View File

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

View File

@@ -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

View File

@@ -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"},
],
} }
) )

View File

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

View 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")},
),
]

View File

@@ -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,
),
]

View File

@@ -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

View File

@@ -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

View 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

File diff suppressed because it is too large Load Diff