mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 13:31:08 -05:00
Refactor API structure and add comprehensive user management features
- Restructure API v1 with improved serializers organization - Add user deletion requests and moderation queue system - Implement bulk moderation operations and permissions - Add user profile enhancements with display names and avatars - Expand ride and park API endpoints with better filtering - Add manufacturer API with detailed ride relationships - Improve authentication flows and error handling - Update frontend documentation and API specifications
This commit is contained in:
@@ -29,37 +29,51 @@ app_name = "api_v1_ride_models"
|
||||
urlpatterns = [
|
||||
# Core ride model endpoints - nested under manufacturer
|
||||
path("", RideModelListCreateAPIView.as_view(), name="ride-model-list-create"),
|
||||
path("<slug:ride_model_slug>/", RideModelDetailAPIView.as_view(), name="ride-model-detail"),
|
||||
|
||||
path(
|
||||
"<slug:ride_model_slug>/",
|
||||
RideModelDetailAPIView.as_view(),
|
||||
name="ride-model-detail",
|
||||
),
|
||||
# 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"),
|
||||
|
||||
path(
|
||||
"filter-options/",
|
||||
RideModelFilterOptionsAPIView.as_view(),
|
||||
name="ride-model-filter-options",
|
||||
),
|
||||
# Statistics (global, not manufacturer-specific)
|
||||
path("stats/", RideModelStatsAPIView.as_view(), name="ride-model-stats"),
|
||||
|
||||
# Ride model variants - using slug-based lookup
|
||||
path("<slug:ride_model_slug>/variants/",
|
||||
RideModelVariantListCreateAPIView.as_view(),
|
||||
name="ride-model-variant-list-create"),
|
||||
path("<slug:ride_model_slug>/variants/<int:pk>/",
|
||||
RideModelVariantDetailAPIView.as_view(),
|
||||
name="ride-model-variant-detail"),
|
||||
|
||||
path(
|
||||
"<slug:ride_model_slug>/variants/",
|
||||
RideModelVariantListCreateAPIView.as_view(),
|
||||
name="ride-model-variant-list-create",
|
||||
),
|
||||
path(
|
||||
"<slug:ride_model_slug>/variants/<int:pk>/",
|
||||
RideModelVariantDetailAPIView.as_view(),
|
||||
name="ride-model-variant-detail",
|
||||
),
|
||||
# Technical specifications - using slug-based lookup
|
||||
path("<slug:ride_model_slug>/technical-specs/",
|
||||
RideModelTechnicalSpecListCreateAPIView.as_view(),
|
||||
name="ride-model-technical-spec-list-create"),
|
||||
path("<slug:ride_model_slug>/technical-specs/<int:pk>/",
|
||||
RideModelTechnicalSpecDetailAPIView.as_view(),
|
||||
name="ride-model-technical-spec-detail"),
|
||||
|
||||
path(
|
||||
"<slug:ride_model_slug>/technical-specs/",
|
||||
RideModelTechnicalSpecListCreateAPIView.as_view(),
|
||||
name="ride-model-technical-spec-list-create",
|
||||
),
|
||||
path(
|
||||
"<slug:ride_model_slug>/technical-specs/<int:pk>/",
|
||||
RideModelTechnicalSpecDetailAPIView.as_view(),
|
||||
name="ride-model-technical-spec-detail",
|
||||
),
|
||||
# Photos - using slug-based lookup
|
||||
path("<slug:ride_model_slug>/photos/",
|
||||
RideModelPhotoListCreateAPIView.as_view(),
|
||||
name="ride-model-photo-list-create"),
|
||||
path("<slug:ride_model_slug>/photos/<int:pk>/",
|
||||
RideModelPhotoDetailAPIView.as_view(),
|
||||
name="ride-model-photo-detail"),
|
||||
path(
|
||||
"<slug:ride_model_slug>/photos/",
|
||||
RideModelPhotoListCreateAPIView.as_view(),
|
||||
name="ride-model-photo-list-create",
|
||||
),
|
||||
path(
|
||||
"<slug:ride_model_slug>/photos/<int:pk>/",
|
||||
RideModelPhotoDetailAPIView.as_view(),
|
||||
name="ride-model-photo-detail",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -13,7 +13,7 @@ This module implements comprehensive endpoints for ride model management:
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
|
||||
from rest_framework import status, permissions
|
||||
from rest_framework.views import APIView
|
||||
@@ -36,25 +36,31 @@ from apps.api.v1.serializers.ride_models import (
|
||||
RideModelVariantOutputSerializer,
|
||||
RideModelVariantCreateInputSerializer,
|
||||
RideModelVariantUpdateInputSerializer,
|
||||
RideModelTechnicalSpecOutputSerializer,
|
||||
RideModelTechnicalSpecCreateInputSerializer,
|
||||
RideModelTechnicalSpecUpdateInputSerializer,
|
||||
RideModelPhotoOutputSerializer,
|
||||
RideModelPhotoCreateInputSerializer,
|
||||
RideModelPhotoUpdateInputSerializer,
|
||||
RideModelStatsOutputSerializer,
|
||||
)
|
||||
|
||||
# Attempt to import models; fall back gracefully if not present
|
||||
try:
|
||||
from apps.rides.models import RideModel, RideModelVariant, RideModelPhoto, RideModelTechnicalSpec
|
||||
from apps.rides.models import (
|
||||
RideModel,
|
||||
RideModelVariant,
|
||||
RideModelPhoto,
|
||||
RideModelTechnicalSpec,
|
||||
)
|
||||
from apps.rides.models.company import Company
|
||||
|
||||
MODELS_AVAILABLE = True
|
||||
except ImportError:
|
||||
try:
|
||||
# Try alternative import path
|
||||
from apps.rides.models.rides import RideModel, RideModelVariant, RideModelPhoto, RideModelTechnicalSpec
|
||||
from apps.rides.models.rides import (
|
||||
RideModel,
|
||||
RideModelVariant,
|
||||
RideModelPhoto,
|
||||
RideModelTechnicalSpec,
|
||||
)
|
||||
from apps.rides.models.rides import Company
|
||||
|
||||
MODELS_AVAILABLE = True
|
||||
except ImportError:
|
||||
RideModel = None
|
||||
@@ -82,7 +88,10 @@ class RideModelListCreateAPIView(APIView):
|
||||
description="List ride models with comprehensive filtering and pagination.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="manufacturer_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||
name="manufacturer_slug",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.STR,
|
||||
required=True,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT
|
||||
@@ -97,10 +106,14 @@ class RideModelListCreateAPIView(APIView):
|
||||
name="category", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="target_market", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
|
||||
name="target_market",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="is_discontinued", location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL
|
||||
name="is_discontinued",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.BOOL,
|
||||
),
|
||||
],
|
||||
responses={200: RideModelListOutputSerializer(many=True)},
|
||||
@@ -123,7 +136,11 @@ class RideModelListCreateAPIView(APIView):
|
||||
except Company.DoesNotExist:
|
||||
raise NotFound("Manufacturer not found")
|
||||
|
||||
qs = RideModel.objects.filter(manufacturer=manufacturer).select_related("manufacturer").prefetch_related("photos")
|
||||
qs = (
|
||||
RideModel.objects.filter(manufacturer=manufacturer)
|
||||
.select_related("manufacturer")
|
||||
.prefetch_related("photos")
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
filter_serializer = RideModelFilterInputSerializer(data=request.query_params)
|
||||
@@ -134,9 +151,9 @@ class RideModelListCreateAPIView(APIView):
|
||||
if filters.get("search"):
|
||||
search_term = filters["search"]
|
||||
qs = qs.filter(
|
||||
Q(name__icontains=search_term) |
|
||||
Q(description__icontains=search_term) |
|
||||
Q(manufacturer__name__icontains=search_term)
|
||||
Q(name__icontains=search_term)
|
||||
| Q(description__icontains=search_term)
|
||||
| Q(manufacturer__name__icontains=search_term)
|
||||
)
|
||||
|
||||
# Category filter
|
||||
@@ -160,10 +177,12 @@ class RideModelListCreateAPIView(APIView):
|
||||
# Year filters
|
||||
if filters.get("first_installation_year_min"):
|
||||
qs = qs.filter(
|
||||
first_installation_year__gte=filters["first_installation_year_min"])
|
||||
first_installation_year__gte=filters["first_installation_year_min"]
|
||||
)
|
||||
if filters.get("first_installation_year_max"):
|
||||
qs = qs.filter(
|
||||
first_installation_year__lte=filters["first_installation_year_max"])
|
||||
first_installation_year__lte=filters["first_installation_year_max"]
|
||||
)
|
||||
|
||||
# Installation count filter
|
||||
if filters.get("min_installations"):
|
||||
@@ -172,18 +191,22 @@ class RideModelListCreateAPIView(APIView):
|
||||
# Height filters
|
||||
if filters.get("min_height_ft"):
|
||||
qs = qs.filter(
|
||||
typical_height_range_max_ft__gte=filters["min_height_ft"])
|
||||
typical_height_range_max_ft__gte=filters["min_height_ft"]
|
||||
)
|
||||
if filters.get("max_height_ft"):
|
||||
qs = qs.filter(
|
||||
typical_height_range_min_ft__lte=filters["max_height_ft"])
|
||||
typical_height_range_min_ft__lte=filters["max_height_ft"]
|
||||
)
|
||||
|
||||
# Speed filters
|
||||
if filters.get("min_speed_mph"):
|
||||
qs = qs.filter(
|
||||
typical_speed_range_max_mph__gte=filters["min_speed_mph"])
|
||||
typical_speed_range_max_mph__gte=filters["min_speed_mph"]
|
||||
)
|
||||
if filters.get("max_speed_mph"):
|
||||
qs = qs.filter(
|
||||
typical_speed_range_min_mph__lte=filters["max_speed_mph"])
|
||||
typical_speed_range_min_mph__lte=filters["max_speed_mph"]
|
||||
)
|
||||
|
||||
# Ordering
|
||||
ordering = filters.get("ordering", "manufacturer__name,name")
|
||||
@@ -203,7 +226,10 @@ class RideModelListCreateAPIView(APIView):
|
||||
description="Create a new ride model for a specific manufacturer.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="manufacturer_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||
name="manufacturer_slug",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.STR,
|
||||
required=True,
|
||||
),
|
||||
],
|
||||
request=RideModelCreateInputSerializer,
|
||||
@@ -262,13 +288,17 @@ class RideModelListCreateAPIView(APIView):
|
||||
class RideModelDetailAPIView(APIView):
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def _get_ride_model_or_404(self, manufacturer_slug: str, ride_model_slug: str) -> 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(manufacturer__slug=manufacturer_slug, slug=ride_model_slug)
|
||||
return (
|
||||
RideModel.objects.select_related("manufacturer")
|
||||
.prefetch_related("photos", "variants", "technical_specs")
|
||||
.get(manufacturer__slug=manufacturer_slug, slug=ride_model_slug)
|
||||
)
|
||||
except RideModel.DoesNotExist:
|
||||
raise NotFound("Ride model not found")
|
||||
|
||||
@@ -277,16 +307,24 @@ class RideModelDetailAPIView(APIView):
|
||||
description="Get detailed information about a specific ride model.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="manufacturer_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||
name="manufacturer_slug",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.STR,
|
||||
required=True,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="ride_model_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||
name="ride_model_slug",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.STR,
|
||||
required=True,
|
||||
),
|
||||
],
|
||||
responses={200: RideModelDetailOutputSerializer()},
|
||||
tags=["Ride Models"],
|
||||
)
|
||||
def get(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||
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}
|
||||
@@ -298,17 +336,25 @@ class RideModelDetailAPIView(APIView):
|
||||
description="Update a ride model (partial update supported).",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="manufacturer_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||
name="manufacturer_slug",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.STR,
|
||||
required=True,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="ride_model_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||
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, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||
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)
|
||||
@@ -331,7 +377,9 @@ class RideModelDetailAPIView(APIView):
|
||||
)
|
||||
return Response(serializer.data)
|
||||
|
||||
def put(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> 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, manufacturer_slug, ride_model_slug)
|
||||
|
||||
@@ -340,16 +388,24 @@ class RideModelDetailAPIView(APIView):
|
||||
description="Delete a ride model.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="manufacturer_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||
name="manufacturer_slug",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.STR,
|
||||
required=True,
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="ride_model_slug", location=OpenApiParameter.PATH, type=OpenApiTypes.STR, required=True
|
||||
name="ride_model_slug",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.STR,
|
||||
required=True,
|
||||
),
|
||||
],
|
||||
responses={204: None},
|
||||
tags=["Ride Models"],
|
||||
)
|
||||
def delete(self, request: Request, manufacturer_slug: str, ride_model_slug: str) -> Response:
|
||||
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)
|
||||
@@ -366,7 +422,10 @@ class RideModelSearchAPIView(APIView):
|
||||
description="Search ride models by name, description, or manufacturer.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=True
|
||||
name="q",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
required=True,
|
||||
)
|
||||
],
|
||||
responses={200: RideModelListOutputSerializer(many=True)},
|
||||
@@ -384,15 +443,15 @@ class RideModelSearchAPIView(APIView):
|
||||
"id": 1,
|
||||
"name": "Hyper Coaster",
|
||||
"manufacturer": {"name": "Bolliger & Mabillard"},
|
||||
"category": "RC"
|
||||
"category": "RC",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
qs = RideModel.objects.filter(
|
||||
Q(name__icontains=q) |
|
||||
Q(description__icontains=q) |
|
||||
Q(manufacturer__name__icontains=q)
|
||||
Q(name__icontains=q)
|
||||
| Q(description__icontains=q)
|
||||
| Q(manufacturer__name__icontains=q)
|
||||
).select_related("manufacturer")[:20]
|
||||
|
||||
results = [
|
||||
@@ -426,54 +485,65 @@ class RideModelFilterOptionsAPIView(APIView):
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Return filter options for ride models."""
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response({
|
||||
"categories": [("RC", "Roller Coaster"), ("FR", "Flat Ride")],
|
||||
"target_markets": [("THRILL", "Thrill"), ("FAMILY", "Family")],
|
||||
"manufacturers": [{"id": 1, "name": "Bolliger & Mabillard"}],
|
||||
})
|
||||
return Response(
|
||||
{
|
||||
"categories": [("RC", "Roller Coaster"), ("FR", "Flat Ride")],
|
||||
"target_markets": [("THRILL", "Thrill"), ("FAMILY", "Family")],
|
||||
"manufacturers": [{"id": 1, "name": "Bolliger & Mabillard"}],
|
||||
}
|
||||
)
|
||||
|
||||
# Get actual data from database
|
||||
manufacturers = Company.objects.filter(
|
||||
roles__contains=["MANUFACTURER"],
|
||||
ride_models__isnull=False
|
||||
).distinct().values("id", "name", "slug")
|
||||
manufacturers = (
|
||||
Company.objects.filter(
|
||||
roles__contains=["MANUFACTURER"], ride_models__isnull=False
|
||||
)
|
||||
.distinct()
|
||||
.values("id", "name", "slug")
|
||||
)
|
||||
|
||||
categories = RideModel.objects.exclude(category="").values_list(
|
||||
"category", flat=True
|
||||
).distinct()
|
||||
categories = (
|
||||
RideModel.objects.exclude(category="")
|
||||
.values_list("category", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
target_markets = RideModel.objects.exclude(target_market="").values_list(
|
||||
"target_market", flat=True
|
||||
).distinct()
|
||||
target_markets = (
|
||||
RideModel.objects.exclude(target_market="")
|
||||
.values_list("target_market", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
return Response({
|
||||
"categories": [
|
||||
("RC", "Roller Coaster"),
|
||||
("DR", "Dark Ride"),
|
||||
("FR", "Flat Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("TR", "Transport"),
|
||||
("OT", "Other"),
|
||||
],
|
||||
"target_markets": [
|
||||
("FAMILY", "Family"),
|
||||
("THRILL", "Thrill"),
|
||||
("EXTREME", "Extreme"),
|
||||
("KIDDIE", "Kiddie"),
|
||||
("ALL_AGES", "All Ages"),
|
||||
],
|
||||
"manufacturers": list(manufacturers),
|
||||
"ordering_options": [
|
||||
("name", "Name A-Z"),
|
||||
("-name", "Name Z-A"),
|
||||
("manufacturer__name", "Manufacturer A-Z"),
|
||||
("-manufacturer__name", "Manufacturer Z-A"),
|
||||
("first_installation_year", "Oldest First"),
|
||||
("-first_installation_year", "Newest First"),
|
||||
("total_installations", "Fewest Installations"),
|
||||
("-total_installations", "Most Installations"),
|
||||
],
|
||||
})
|
||||
return Response(
|
||||
{
|
||||
"categories": [
|
||||
("RC", "Roller Coaster"),
|
||||
("DR", "Dark Ride"),
|
||||
("FR", "Flat Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("TR", "Transport"),
|
||||
("OT", "Other"),
|
||||
],
|
||||
"target_markets": [
|
||||
("FAMILY", "Family"),
|
||||
("THRILL", "Thrill"),
|
||||
("EXTREME", "Extreme"),
|
||||
("KIDDIE", "Kiddie"),
|
||||
("ALL_AGES", "All Ages"),
|
||||
],
|
||||
"manufacturers": list(manufacturers),
|
||||
"ordering_options": [
|
||||
("name", "Name A-Z"),
|
||||
("-name", "Name Z-A"),
|
||||
("manufacturer__name", "Manufacturer A-Z"),
|
||||
("-manufacturer__name", "Manufacturer Z-A"),
|
||||
("first_installation_year", "Oldest First"),
|
||||
("-first_installation_year", "Newest First"),
|
||||
("total_installations", "Fewest Installations"),
|
||||
("-total_installations", "Most Installations"),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# === RIDE MODEL STATISTICS ===
|
||||
@@ -491,69 +561,84 @@ class RideModelStatsAPIView(APIView):
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Get ride model statistics."""
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response({
|
||||
"total_models": 50,
|
||||
"total_installations": 500,
|
||||
"active_manufacturers": 15,
|
||||
"discontinued_models": 10,
|
||||
"by_category": {"RC": 30, "FR": 15, "WR": 5},
|
||||
"by_target_market": {"THRILL": 25, "FAMILY": 20, "EXTREME": 5},
|
||||
"by_manufacturer": {"Bolliger & Mabillard": 8, "Intamin": 6},
|
||||
"recent_models": 3,
|
||||
})
|
||||
return Response(
|
||||
{
|
||||
"total_models": 50,
|
||||
"total_installations": 500,
|
||||
"active_manufacturers": 15,
|
||||
"discontinued_models": 10,
|
||||
"by_category": {"RC": 30, "FR": 15, "WR": 5},
|
||||
"by_target_market": {"THRILL": 25, "FAMILY": 20, "EXTREME": 5},
|
||||
"by_manufacturer": {"Bolliger & Mabillard": 8, "Intamin": 6},
|
||||
"recent_models": 3,
|
||||
}
|
||||
)
|
||||
|
||||
# Calculate statistics
|
||||
total_models = RideModel.objects.count()
|
||||
total_installations = RideModel.objects.aggregate(
|
||||
total=Count('rides')
|
||||
)['total'] or 0
|
||||
total_installations = (
|
||||
RideModel.objects.aggregate(total=Count("rides"))["total"] or 0
|
||||
)
|
||||
|
||||
active_manufacturers = Company.objects.filter(
|
||||
roles__contains=["MANUFACTURER"],
|
||||
ride_models__isnull=False
|
||||
).distinct().count()
|
||||
active_manufacturers = (
|
||||
Company.objects.filter(
|
||||
roles__contains=["MANUFACTURER"], ride_models__isnull=False
|
||||
)
|
||||
.distinct()
|
||||
.count()
|
||||
)
|
||||
|
||||
discontinued_models = RideModel.objects.filter(is_discontinued=True).count()
|
||||
|
||||
# Category breakdown
|
||||
by_category = {}
|
||||
category_counts = RideModel.objects.exclude(category="").values(
|
||||
"category"
|
||||
).annotate(count=Count("id"))
|
||||
category_counts = (
|
||||
RideModel.objects.exclude(category="")
|
||||
.values("category")
|
||||
.annotate(count=Count("id"))
|
||||
)
|
||||
for item in category_counts:
|
||||
by_category[item["category"]] = item["count"]
|
||||
|
||||
# Target market breakdown
|
||||
by_target_market = {}
|
||||
market_counts = RideModel.objects.exclude(target_market="").values(
|
||||
"target_market"
|
||||
).annotate(count=Count("id"))
|
||||
market_counts = (
|
||||
RideModel.objects.exclude(target_market="")
|
||||
.values("target_market")
|
||||
.annotate(count=Count("id"))
|
||||
)
|
||||
for item in market_counts:
|
||||
by_target_market[item["target_market"]] = item["count"]
|
||||
|
||||
# Manufacturer breakdown (top 10)
|
||||
by_manufacturer = {}
|
||||
manufacturer_counts = RideModel.objects.filter(
|
||||
manufacturer__isnull=False
|
||||
).values("manufacturer__name").annotate(count=Count("id")).order_by("-count")[:10]
|
||||
manufacturer_counts = (
|
||||
RideModel.objects.filter(manufacturer__isnull=False)
|
||||
.values("manufacturer__name")
|
||||
.annotate(count=Count("id"))
|
||||
.order_by("-count")[:10]
|
||||
)
|
||||
for item in manufacturer_counts:
|
||||
by_manufacturer[item["manufacturer__name"]] = item["count"]
|
||||
|
||||
# Recent models (last 30 days)
|
||||
thirty_days_ago = timezone.now() - timedelta(days=30)
|
||||
recent_models = RideModel.objects.filter(
|
||||
created_at__gte=thirty_days_ago).count()
|
||||
created_at__gte=thirty_days_ago
|
||||
).count()
|
||||
|
||||
return Response({
|
||||
"total_models": total_models,
|
||||
"total_installations": total_installations,
|
||||
"active_manufacturers": active_manufacturers,
|
||||
"discontinued_models": discontinued_models,
|
||||
"by_category": by_category,
|
||||
"by_target_market": by_target_market,
|
||||
"by_manufacturer": by_manufacturer,
|
||||
"recent_models": recent_models,
|
||||
})
|
||||
return Response(
|
||||
{
|
||||
"total_models": total_models,
|
||||
"total_installations": total_installations,
|
||||
"active_manufacturers": active_manufacturers,
|
||||
"discontinued_models": discontinued_models,
|
||||
"by_category": by_category,
|
||||
"by_target_market": by_target_market,
|
||||
"by_manufacturer": by_manufacturer,
|
||||
"recent_models": recent_models,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# === RIDE MODEL VARIANTS ===
|
||||
@@ -592,7 +677,7 @@ class RideModelVariantListCreateAPIView(APIView):
|
||||
if not MODELS_AVAILABLE:
|
||||
return Response(
|
||||
{"detail": "Variants not available"},
|
||||
status=status.HTTP_501_NOT_IMPLEMENTED
|
||||
status=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -653,7 +738,8 @@ class RideModelVariantDetailAPIView(APIView):
|
||||
def patch(self, request: Request, ride_model_pk: int, pk: int) -> Response:
|
||||
variant = self._get_variant_or_404(ride_model_pk, pk)
|
||||
serializer_in = RideModelVariantUpdateInputSerializer(
|
||||
data=request.data, partial=True)
|
||||
data=request.data, partial=True
|
||||
)
|
||||
serializer_in.is_valid(raise_exception=True)
|
||||
|
||||
for field, value in serializer_in.validated_data.items():
|
||||
@@ -677,25 +763,30 @@ class RideModelVariantDetailAPIView(APIView):
|
||||
# Note: Similar patterns would be implemented for RideModelTechnicalSpec and RideModelPhoto
|
||||
# For brevity, I'm including the class definitions but not the full implementations
|
||||
|
||||
|
||||
class RideModelTechnicalSpecListCreateAPIView(APIView):
|
||||
"""CRUD operations for ride model technical specifications."""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
# Implementation similar to variants...
|
||||
|
||||
|
||||
class RideModelTechnicalSpecDetailAPIView(APIView):
|
||||
"""CRUD operations for individual technical specifications."""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
# Implementation similar to variant detail...
|
||||
|
||||
|
||||
class RideModelPhotoListCreateAPIView(APIView):
|
||||
"""CRUD operations for ride model photos."""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
# Implementation similar to variants...
|
||||
|
||||
|
||||
class RideModelPhotoDetailAPIView(APIView):
|
||||
"""CRUD operations for individual ride model photos."""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
# Implementation similar to variant detail...
|
||||
|
||||
@@ -5,42 +5,46 @@ This module contains serializers for ride-specific media functionality.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer, OpenApiExample
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema_field,
|
||||
extend_schema_serializer,
|
||||
OpenApiExample,
|
||||
)
|
||||
from apps.rides.models import Ride, RidePhoto
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
name='Ride Photo with Cloudflare Images',
|
||||
summary='Complete ride photo response',
|
||||
description='Example response showing all fields including Cloudflare Images URLs and variants',
|
||||
name="Ride Photo with Cloudflare Images",
|
||||
summary="Complete ride photo response",
|
||||
description="Example response showing all fields including Cloudflare Images URLs and variants",
|
||||
value={
|
||||
'id': 123,
|
||||
'image': 'https://imagedelivery.net/account-hash/abc123def456/public',
|
||||
'image_url': 'https://imagedelivery.net/account-hash/abc123def456/public',
|
||||
'image_variants': {
|
||||
'thumbnail': 'https://imagedelivery.net/account-hash/abc123def456/thumbnail',
|
||||
'medium': 'https://imagedelivery.net/account-hash/abc123def456/medium',
|
||||
'large': 'https://imagedelivery.net/account-hash/abc123def456/large',
|
||||
'public': 'https://imagedelivery.net/account-hash/abc123def456/public'
|
||||
"id": 123,
|
||||
"image": "https://imagedelivery.net/account-hash/abc123def456/public",
|
||||
"image_url": "https://imagedelivery.net/account-hash/abc123def456/public",
|
||||
"image_variants": {
|
||||
"thumbnail": "https://imagedelivery.net/account-hash/abc123def456/thumbnail",
|
||||
"medium": "https://imagedelivery.net/account-hash/abc123def456/medium",
|
||||
"large": "https://imagedelivery.net/account-hash/abc123def456/large",
|
||||
"public": "https://imagedelivery.net/account-hash/abc123def456/public",
|
||||
},
|
||||
'caption': 'Amazing roller coaster photo',
|
||||
'alt_text': 'Steel roller coaster with multiple inversions',
|
||||
'is_primary': True,
|
||||
'is_approved': True,
|
||||
'photo_type': 'exterior',
|
||||
'created_at': '2023-01-01T12:00:00Z',
|
||||
'updated_at': '2023-01-01T12:00:00Z',
|
||||
'date_taken': '2023-01-01T10:00:00Z',
|
||||
'uploaded_by_username': 'photographer123',
|
||||
'file_size': 2048576,
|
||||
'dimensions': [1920, 1080],
|
||||
'ride_slug': 'steel-vengeance',
|
||||
'ride_name': 'Steel Vengeance',
|
||||
'park_slug': 'cedar-point',
|
||||
'park_name': 'Cedar Point'
|
||||
}
|
||||
"caption": "Amazing roller coaster photo",
|
||||
"alt_text": "Steel roller coaster with multiple inversions",
|
||||
"is_primary": True,
|
||||
"is_approved": True,
|
||||
"photo_type": "exterior",
|
||||
"created_at": "2023-01-01T12:00:00Z",
|
||||
"updated_at": "2023-01-01T12:00:00Z",
|
||||
"date_taken": "2023-01-01T10:00:00Z",
|
||||
"uploaded_by_username": "photographer123",
|
||||
"file_size": 2048576,
|
||||
"dimensions": [1920, 1080],
|
||||
"ride_slug": "steel-vengeance",
|
||||
"ride_name": "Steel Vengeance",
|
||||
"park_slug": "cedar-point",
|
||||
"park_name": "Cedar Point",
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -78,8 +82,7 @@ class RidePhotoOutputSerializer(serializers.ModelSerializer):
|
||||
|
||||
@extend_schema_field(
|
||||
serializers.URLField(
|
||||
help_text="Full URL to the Cloudflare Images asset",
|
||||
allow_null=True
|
||||
help_text="Full URL to the Cloudflare Images asset", allow_null=True
|
||||
)
|
||||
)
|
||||
def get_image_url(self, obj):
|
||||
@@ -91,7 +94,7 @@ class RidePhotoOutputSerializer(serializers.ModelSerializer):
|
||||
@extend_schema_field(
|
||||
serializers.DictField(
|
||||
child=serializers.URLField(),
|
||||
help_text="Available Cloudflare Images variants with their URLs"
|
||||
help_text="Available Cloudflare Images variants with their URLs",
|
||||
)
|
||||
)
|
||||
def get_image_variants(self, obj):
|
||||
@@ -101,10 +104,10 @@ class RidePhotoOutputSerializer(serializers.ModelSerializer):
|
||||
|
||||
# Common variants for ride photos
|
||||
variants = {
|
||||
'thumbnail': f"{obj.image.url}/thumbnail",
|
||||
'medium': f"{obj.image.url}/medium",
|
||||
'large': f"{obj.image.url}/large",
|
||||
'public': f"{obj.image.url}/public"
|
||||
"thumbnail": f"{obj.image.url}/thumbnail",
|
||||
"medium": f"{obj.image.url}/medium",
|
||||
"large": f"{obj.image.url}/large",
|
||||
"public": f"{obj.image.url}/public",
|
||||
}
|
||||
return variants
|
||||
|
||||
|
||||
@@ -50,13 +50,18 @@ urlpatterns = [
|
||||
name="ride-search-suggestions",
|
||||
),
|
||||
# Ride model management endpoints - nested under rides/manufacturers
|
||||
path("manufacturers/<slug:manufacturer_slug>/",
|
||||
include("apps.api.v1.rides.manufacturers.urls")),
|
||||
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
|
||||
path("<int:pk>/image-settings/", RideImageSettingsAPIView.as_view(),
|
||||
name="ride-image-settings"),
|
||||
path(
|
||||
"<int:pk>/image-settings/",
|
||||
RideImageSettingsAPIView.as_view(),
|
||||
name="ride-image-settings",
|
||||
),
|
||||
# Ride photo endpoints - domain-specific photo management
|
||||
path("<int:ride_pk>/photos/", include(router.urls)),
|
||||
]
|
||||
|
||||
@@ -21,7 +21,7 @@ from rest_framework.views import APIView
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
from rest_framework.exceptions import NotFound
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
|
||||
@@ -73,136 +73,202 @@ class RideListCreateAPIView(APIView):
|
||||
description="List rides with comprehensive filtering options including category, status, manufacturer, designer, ride model, and more.",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT,
|
||||
description="Page number for pagination"
|
||||
name="page",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.INT,
|
||||
description="Page number for pagination",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="page_size", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT,
|
||||
description="Number of results per page (max 1000)"
|
||||
name="page_size",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.INT,
|
||||
description="Number of results per page (max 1000)",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="search", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR,
|
||||
description="Search in ride names and descriptions"
|
||||
name="search",
|
||||
location=OpenApiParameter.QUERY,
|
||||
type=OpenApiTypes.STR,
|
||||
description="Search in ride names and descriptions",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="park_slug", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR,
|
||||
description="Filter by park slug"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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)"
|
||||
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.)"
|
||||
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)"
|
||||
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.)"
|
||||
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)"
|
||||
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)"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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)"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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)},
|
||||
@@ -220,17 +286,25 @@ class RideListCreateAPIView(APIView):
|
||||
)
|
||||
|
||||
# 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
|
||||
qs = (
|
||||
Ride.objects.all()
|
||||
.select_related(
|
||||
"park",
|
||||
"manufacturer",
|
||||
"designer",
|
||||
"ride_model",
|
||||
"ride_model__manufacturer",
|
||||
)
|
||||
.prefetch_related("coaster_stats")
|
||||
) # type: ignore
|
||||
|
||||
# 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)
|
||||
models.Q(name__icontains=search)
|
||||
| models.Q(description__icontains=search)
|
||||
| models.Q(park__name__icontains=search)
|
||||
)
|
||||
|
||||
# Park filters
|
||||
@@ -292,7 +366,7 @@ class RideListCreateAPIView(APIView):
|
||||
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
|
||||
ride_model__manufacturer__slug=manufacturer_slug_for_model,
|
||||
)
|
||||
|
||||
# Rating filters
|
||||
@@ -422,24 +496,36 @@ class RideListCreateAPIView(APIView):
|
||||
|
||||
has_inversions = request.query_params.get("has_inversions")
|
||||
if has_inversions is not None:
|
||||
if has_inversions.lower() in ['true', '1', 'yes']:
|
||||
if has_inversions.lower() in ["true", "1", "yes"]:
|
||||
qs = qs.filter(coaster_stats__inversions__gt=0)
|
||||
elif has_inversions.lower() in ['false', '0', 'no']:
|
||||
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"
|
||||
"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")
|
||||
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)
|
||||
@@ -592,16 +678,24 @@ class FilterOptionsAPIView(APIView):
|
||||
"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": "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": "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)"},
|
||||
@@ -611,16 +705,39 @@ class FilterOptionsAPIView(APIView):
|
||||
],
|
||||
"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_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"},
|
||||
"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"},
|
||||
{
|
||||
"key": "has_inversions",
|
||||
"label": "Has Inversions",
|
||||
"description": "Filter roller coasters with or without inversions",
|
||||
},
|
||||
],
|
||||
}
|
||||
return Response(data)
|
||||
@@ -682,8 +799,10 @@ class FilterOptionsAPIView(APIView):
|
||||
{"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": "-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)"},
|
||||
@@ -693,16 +812,39 @@ class FilterOptionsAPIView(APIView):
|
||||
],
|
||||
"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_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"},
|
||||
"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"},
|
||||
{
|
||||
"key": "has_inversions",
|
||||
"label": "Has Inversions",
|
||||
"description": "Filter roller coasters with or without inversions",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
@@ -853,7 +995,8 @@ class RideImageSettingsAPIView(APIView):
|
||||
|
||||
# Return updated ride data
|
||||
output_serializer = RideDetailOutputSerializer(
|
||||
ride, context={"request": request})
|
||||
ride, context={"request": request}
|
||||
)
|
||||
return Response(output_serializer.data)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user