mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 04:31:09 -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"
|
||||
|
||||
urlpatterns = [
|
||||
# Core ride model endpoints
|
||||
# Core ride model endpoints - nested under manufacturer
|
||||
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("filter-options/", RideModelFilterOptionsAPIView.as_view(),
|
||||
name="ride-model-filter-options"),
|
||||
|
||||
# Statistics
|
||||
# Statistics (global, not manufacturer-specific)
|
||||
path("stats/", RideModelStatsAPIView.as_view(), name="ride-model-stats"),
|
||||
|
||||
# Ride model variants
|
||||
path("<int:ride_model_pk>/variants/",
|
||||
# Ride model variants - using slug-based lookup
|
||||
path("<slug:ride_model_slug>/variants/",
|
||||
RideModelVariantListCreateAPIView.as_view(),
|
||||
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(),
|
||||
name="ride-model-variant-detail"),
|
||||
|
||||
# Technical specifications
|
||||
path("<int:ride_model_pk>/technical-specs/",
|
||||
# Technical specifications - using slug-based lookup
|
||||
path("<slug:ride_model_slug>/technical-specs/",
|
||||
RideModelTechnicalSpecListCreateAPIView.as_view(),
|
||||
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(),
|
||||
name="ride-model-technical-spec-detail"),
|
||||
|
||||
# Photos
|
||||
path("<int:ride_model_pk>/photos/",
|
||||
# Photos - using slug-based lookup
|
||||
path("<slug:ride_model_slug>/photos/",
|
||||
RideModelPhotoListCreateAPIView.as_view(),
|
||||
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(),
|
||||
name="ride-model-photo-detail"),
|
||||
]
|
||||
@@ -81,6 +81,9 @@ class RideModelListCreateAPIView(APIView):
|
||||
summary="List ride models with filtering and pagination",
|
||||
description="List ride models with comprehensive filtering and pagination.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="manufacturer_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT
|
||||
),
|
||||
@@ -93,9 +96,6 @@ class RideModelListCreateAPIView(APIView):
|
||||
OpenApiParameter(
|
||||
name="category", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="manufacturer_id", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="target_market", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||
),
|
||||
@@ -106,8 +106,8 @@ class RideModelListCreateAPIView(APIView):
|
||||
responses={200: RideModelListOutputSerializer(many=True)},
|
||||
tags=["Ride Models"],
|
||||
)
|
||||
def get(self, request: Request) -> Response:
|
||||
"""List ride models with filtering and pagination."""
|
||||
def get(self, request: Request, manufacturer_slug: str) -> Response:
|
||||
"""List ride models for a specific manufacturer with filtering and pagination."""
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{
|
||||
@@ -117,7 +117,13 @@ class RideModelListCreateAPIView(APIView):
|
||||
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
|
||||
filter_serializer = RideModelFilterInputSerializer(data=request.query_params)
|
||||
@@ -194,13 +200,18 @@ class RideModelListCreateAPIView(APIView):
|
||||
|
||||
@extend_schema(
|
||||
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,
|
||||
responses={201: RideModelDetailOutputSerializer()},
|
||||
tags=["Ride Models"],
|
||||
)
|
||||
def post(self, request: Request) -> Response:
|
||||
"""Create a new ride model."""
|
||||
def post(self, request: Request, manufacturer_slug: str) -> Response:
|
||||
"""Create a new ride model for a specific manufacturer."""
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{
|
||||
@@ -209,17 +220,17 @@ class RideModelListCreateAPIView(APIView):
|
||||
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.is_valid(raise_exception=True)
|
||||
validated = serializer_in.validated_data
|
||||
|
||||
# Validate manufacturer exists
|
||||
try:
|
||||
manufacturer = Company.objects.get(id=validated["manufacturer_id"])
|
||||
except Company.DoesNotExist:
|
||||
raise NotFound("Manufacturer not found")
|
||||
|
||||
# Create ride model
|
||||
# Create ride model (use manufacturer from URL, not from request data)
|
||||
ride_model = RideModel.objects.create(
|
||||
name=validated["name"],
|
||||
description=validated.get("description", ""),
|
||||
@@ -251,24 +262,32 @@ class RideModelListCreateAPIView(APIView):
|
||||
class RideModelDetailAPIView(APIView):
|
||||
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:
|
||||
raise NotFound("Ride model models not available")
|
||||
try:
|
||||
return RideModel.objects.select_related("manufacturer").prefetch_related(
|
||||
"photos", "variants", "technical_specs"
|
||||
).get(pk=pk)
|
||||
).get(manufacturer__slug=manufacturer_slug, slug=ride_model_slug)
|
||||
except RideModel.DoesNotExist:
|
||||
raise NotFound("Ride model not found")
|
||||
|
||||
@extend_schema(
|
||||
summary="Retrieve a 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()},
|
||||
tags=["Ride Models"],
|
||||
)
|
||||
def get(self, request: Request, pk: int) -> Response:
|
||||
ride_model = self._get_ride_model_or_404(pk)
|
||||
def get(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||
ride_model = self._get_ride_model_or_404(manufacturer_slug, ride_model_slug)
|
||||
serializer = RideModelDetailOutputSerializer(
|
||||
ride_model, context={"request": request}
|
||||
)
|
||||
@@ -277,12 +296,20 @@ class RideModelDetailAPIView(APIView):
|
||||
@extend_schema(
|
||||
summary="Update a ride model",
|
||||
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,
|
||||
responses={200: RideModelDetailOutputSerializer()},
|
||||
tags=["Ride Models"],
|
||||
)
|
||||
def patch(self, request: Request, pk: int) -> Response:
|
||||
ride_model = self._get_ride_model_or_404(pk)
|
||||
def patch(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||
ride_model = self._get_ride_model_or_404(manufacturer_slug, ride_model_slug)
|
||||
serializer_in = RideModelUpdateInputSerializer(data=request.data, partial=True)
|
||||
serializer_in.is_valid(raise_exception=True)
|
||||
|
||||
@@ -304,18 +331,26 @@ class RideModelDetailAPIView(APIView):
|
||||
)
|
||||
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
|
||||
return self.patch(request, pk)
|
||||
return self.patch(request, manufacturer_slug, ride_model_slug)
|
||||
|
||||
@extend_schema(
|
||||
summary="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},
|
||||
tags=["Ride Models"],
|
||||
)
|
||||
def delete(self, request: Request, pk: int) -> Response:
|
||||
ride_model = self._get_ride_model_or_404(pk)
|
||||
def delete(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||
ride_model = self._get_ride_model_or_404(manufacturer_slug, ride_model_slug)
|
||||
ride_model.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@@ -49,6 +49,9 @@ urlpatterns = [
|
||||
RideSearchSuggestionsAPIView.as_view(),
|
||||
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
|
||||
path("<int:pk>/", RideDetailAPIView.as_view(), name="ride-detail"),
|
||||
# Ride image settings endpoint
|
||||
|
||||
@@ -15,6 +15,7 @@ Notes:
|
||||
|
||||
from typing import Any
|
||||
|
||||
from django.db import models
|
||||
from rest_framework import status, permissions
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.request import Request
|
||||
@@ -68,27 +69,147 @@ class RideListCreateAPIView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
@extend_schema(
|
||||
summary="List rides with filtering and pagination",
|
||||
description="List rides with basic filtering and pagination.",
|
||||
summary="List rides with comprehensive filtering and pagination",
|
||||
description="List rides with comprehensive filtering options including category, status, manufacturer, designer, ride model, and more.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT
|
||||
name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT,
|
||||
description="Page number for pagination"
|
||||
),
|
||||
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(
|
||||
name="search", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||
name="search", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR,
|
||||
description="Search in ride names and descriptions"
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="park_slug", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||
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)},
|
||||
tags=["Rides"],
|
||||
)
|
||||
def get(self, request: Request) -> Response:
|
||||
"""List rides with basic filtering and pagination."""
|
||||
"""List rides with comprehensive filtering and pagination."""
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{
|
||||
@@ -98,16 +219,230 @@ class RideListCreateAPIView(APIView):
|
||||
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
|
||||
q = request.query_params.get("search")
|
||||
if q:
|
||||
qs = qs.filter(name__icontains=q) # simplistic search
|
||||
# Text search
|
||||
search = request.query_params.get("search")
|
||||
if 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")
|
||||
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()
|
||||
page = paginator.paginate_queryset(qs, request)
|
||||
@@ -234,7 +569,8 @@ class RideDetailAPIView(APIView):
|
||||
|
||||
# --- Filter options ---------------------------------------------------------
|
||||
@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},
|
||||
tags=["Rides"],
|
||||
)
|
||||
@@ -242,7 +578,7 @@ class FilterOptionsAPIView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
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
|
||||
if HAVE_MODELCHOICES and ModelChoices is not None:
|
||||
try:
|
||||
@@ -250,13 +586,41 @@ class FilterOptionsAPIView(APIView):
|
||||
"categories": ModelChoices.get_ride_category_choices(),
|
||||
"statuses": ModelChoices.get_ride_status_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": [
|
||||
"name",
|
||||
"-name",
|
||||
"opening_date",
|
||||
"-opening_date",
|
||||
"average_rating",
|
||||
"-average_rating",
|
||||
{"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"},
|
||||
],
|
||||
}
|
||||
return Response(data)
|
||||
@@ -264,12 +628,82 @@ class FilterOptionsAPIView(APIView):
|
||||
# fallthrough to fallback
|
||||
pass
|
||||
|
||||
# Fallback minimal options
|
||||
# Comprehensive fallback options
|
||||
return Response(
|
||||
{
|
||||
"categories": ["ROLLER_COASTER", "WATER_RIDE", "FLAT"],
|
||||
"statuses": ["OPERATING", "CLOSED", "MAINTENANCE"],
|
||||
"ordering_options": ["name", "-name", "opening_date", "-opening_date"],
|
||||
"categories": [
|
||||
("RC", "Roller Coaster"),
|
||||
("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
|
||||
path("parks/", include("apps.api.v1.parks.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("history/", include("apps.api.v1.history.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")
|
||||
slug = models.SlugField(max_length=255, unique=True,
|
||||
help_text="URL-friendly identifier")
|
||||
slug = models.SlugField(max_length=255,
|
||||
help_text="URL-friendly identifier (unique within manufacturer)")
|
||||
manufacturer = models.ForeignKey(
|
||||
Company,
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -152,7 +152,10 @@ class RideModel(TrackedModel):
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
ordering = ["manufacturer__name", "name"]
|
||||
unique_together = ["manufacturer", "name"]
|
||||
unique_together = [
|
||||
["manufacturer", "name"],
|
||||
["manufacturer", "slug"]
|
||||
]
|
||||
constraints = [
|
||||
# Height range validation
|
||||
models.CheckConstraint(
|
||||
@@ -198,13 +201,16 @@ class RideModel(TrackedModel):
|
||||
def save(self, *args, **kwargs) -> None:
|
||||
if not self.slug:
|
||||
from django.utils.text import slugify
|
||||
base_slug = slugify(
|
||||
f"{self.manufacturer.name if self.manufacturer else ''} {self.name}")
|
||||
# Only use the ride model name for the slug, not manufacturer
|
||||
base_slug = slugify(self.name)
|
||||
self.slug = base_slug
|
||||
|
||||
# Ensure uniqueness
|
||||
# Ensure uniqueness within the same manufacturer
|
||||
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}"
|
||||
counter += 1
|
||||
|
||||
|
||||
@@ -1,15 +1,48 @@
|
||||
c# Active Context
|
||||
|
||||
## 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: 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: Comprehensive Rides Filtering System**: Successfully implemented comprehensive filtering capabilities for rides API with 25+ filter parameters and enhanced filter options endpoint
|
||||
- **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
|
||||
- **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
|
||||
- **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
|
||||
**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:**
|
||||
- **Implemented**: Complete Cloudflare Images integration for rides and parks models
|
||||
- **Files Created/Modified**:
|
||||
@@ -39,6 +72,31 @@ c# Active Context
|
||||
- `backend/apps/api/v1/serializers/maps.py` - Comprehensive map serializers
|
||||
- `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:**
|
||||
- **Stats Endpoint**: GET `/api/v1/stats/` - Returns comprehensive platform statistics
|
||||
- **Maps Endpoints**:
|
||||
@@ -56,6 +114,17 @@ c# Active Context
|
||||
|
||||
## 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
|
||||
- `backend/apps/rides/models/media.py` - RidePhoto 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/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
|
||||
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
|
||||
- Add signed URLs for private images
|
||||
- Implement batch upload capabilities
|
||||
- Add image analytics integration
|
||||
2. **Maps API Enhancements**:
|
||||
3. **Maps API Enhancements**:
|
||||
- Implement clustering algorithm for high-density areas
|
||||
- Add nearby locations functionality
|
||||
- Implement relevance scoring for search results
|
||||
- Add cache statistics tracking
|
||||
- Add admin permission checks for cache management endpoints
|
||||
3. **Stats API Enhancements**:
|
||||
4. **Stats API Enhancements**:
|
||||
- Consider adding more granular statistics if needed
|
||||
- Monitor cache performance and adjust cache duration if necessary
|
||||
- Add unit tests for the stats endpoint
|
||||
- Consider adding filtering or query parameters for specific stat categories
|
||||
4. **Testing**: Add comprehensive unit tests for all endpoints
|
||||
5. **Performance**: Monitor and optimize database queries for large datasets
|
||||
5. **Testing**: Add comprehensive unit tests for all endpoints
|
||||
6. **Performance**: Monitor and optimize database queries for large datasets
|
||||
|
||||
## Current Development State
|
||||
- Django backend with comprehensive stats API
|
||||
@@ -102,6 +194,21 @@ c# Active Context
|
||||
- All middleware issues resolved
|
||||
|
||||
## 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
|
||||
- **Models**: RidePhoto and ParkPhoto using CloudflareImagesField
|
||||
- **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