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

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

View File

@@ -0,0 +1,6 @@
"""
RideModel API package for ThrillWiki API v1.
This package provides comprehensive API endpoints for ride model management,
including CRUD operations, search, filtering, and nested resources.
"""

View File

@@ -0,0 +1,65 @@
"""
URL routes for RideModel domain (API v1).
This file exposes comprehensive endpoints for ride model management:
- Core CRUD operations for ride models
- Search and filtering capabilities
- Statistics and analytics
- Nested resources (variants, technical specs, photos)
"""
from django.urls import path
from .views import (
RideModelListCreateAPIView,
RideModelDetailAPIView,
RideModelSearchAPIView,
RideModelFilterOptionsAPIView,
RideModelStatsAPIView,
RideModelVariantListCreateAPIView,
RideModelVariantDetailAPIView,
RideModelTechnicalSpecListCreateAPIView,
RideModelTechnicalSpecDetailAPIView,
RideModelPhotoListCreateAPIView,
RideModelPhotoDetailAPIView,
)
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"),
# 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 (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"),
# 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"),
# 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"),
]

View File

@@ -0,0 +1,701 @@
"""
RideModel API views for ThrillWiki API v1.
This module implements comprehensive endpoints for ride model management:
- List / Create: GET /ride-models/ POST /ride-models/
- Retrieve / Update / Delete: GET /ride-models/{pk}/ PATCH/PUT/DELETE
- Filter options: GET /ride-models/filter-options/
- Search: GET /ride-models/search/?q=...
- Statistics: GET /ride-models/stats/
- Variants: CRUD operations for ride model variants
- Technical specs: CRUD operations for technical specifications
- Photos: CRUD operations for ride model photos
"""
from typing import Any
from datetime import datetime, timedelta
from rest_framework import status, permissions
from rest_framework.views import APIView
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from rest_framework.exceptions import NotFound, ValidationError
from drf_spectacular.utils import extend_schema, OpenApiParameter
from drf_spectacular.types import OpenApiTypes
from django.db.models import Q, Count
from django.utils import timezone
# Import serializers
from apps.api.v1.serializers.ride_models import (
RideModelListOutputSerializer,
RideModelDetailOutputSerializer,
RideModelCreateInputSerializer,
RideModelUpdateInputSerializer,
RideModelFilterInputSerializer,
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.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 Company
MODELS_AVAILABLE = True
except ImportError:
RideModel = None
RideModelVariant = None
RideModelPhoto = None
RideModelTechnicalSpec = None
Company = None
MODELS_AVAILABLE = False
class StandardResultsSetPagination(PageNumberPagination):
page_size = 20
page_size_query_param = "page_size"
max_page_size = 100
# === RIDE MODEL VIEWS ===
class RideModelListCreateAPIView(APIView):
permission_classes = [permissions.AllowAny]
@extend_schema(
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
),
OpenApiParameter(
name="page_size", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT
),
OpenApiParameter(
name="search", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
),
OpenApiParameter(
name="category", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
),
OpenApiParameter(
name="target_market", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
),
OpenApiParameter(
name="is_discontinued", location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL
),
],
responses={200: RideModelListOutputSerializer(many=True)},
tags=["Ride Models"],
)
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(
{
"detail": "Ride model listing is not available because domain models are not imported. "
"Implement apps.rides.models.RideModel to enable listing."
},
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")
qs = RideModel.objects.filter(manufacturer=manufacturer).select_related("manufacturer").prefetch_related("photos")
# Apply filters
filter_serializer = RideModelFilterInputSerializer(data=request.query_params)
if filter_serializer.is_valid():
filters = filter_serializer.validated_data
# Search filter
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)
)
# Category filter
if filters.get("category"):
qs = qs.filter(category__in=filters["category"])
# Manufacturer filters
if filters.get("manufacturer_id"):
qs = qs.filter(manufacturer_id=filters["manufacturer_id"])
if filters.get("manufacturer_slug"):
qs = qs.filter(manufacturer__slug=filters["manufacturer_slug"])
# Target market filter
if filters.get("target_market"):
qs = qs.filter(target_market__in=filters["target_market"])
# Discontinued filter
if filters.get("is_discontinued") is not None:
qs = qs.filter(is_discontinued=filters["is_discontinued"])
# Year filters
if filters.get("first_installation_year_min"):
qs = qs.filter(
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"])
# Installation count filter
if filters.get("min_installations"):
qs = qs.filter(total_installations__gte=filters["min_installations"])
# Height filters
if filters.get("min_height_ft"):
qs = qs.filter(
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"])
# Speed filters
if filters.get("min_speed_mph"):
qs = qs.filter(
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"])
# Ordering
ordering = filters.get("ordering", "manufacturer__name,name")
if ordering:
order_fields = ordering.split(",")
qs = qs.order_by(*order_fields)
paginator = StandardResultsSetPagination()
page = paginator.paginate_queryset(qs, request)
serializer = RideModelListOutputSerializer(
page, many=True, context={"request": request}
)
return paginator.get_paginated_response(serializer.data)
@extend_schema(
summary="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, manufacturer_slug: str) -> Response:
"""Create a new ride model for a specific manufacturer."""
if not MODELS_AVAILABLE:
return Response(
{
"detail": "Ride model creation is not available because domain models are not imported."
},
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
# Create ride model (use manufacturer from URL, not from request data)
ride_model = RideModel.objects.create(
name=validated["name"],
description=validated.get("description", ""),
category=validated.get("category", ""),
manufacturer=manufacturer,
typical_height_range_min_ft=validated.get("typical_height_range_min_ft"),
typical_height_range_max_ft=validated.get("typical_height_range_max_ft"),
typical_speed_range_min_mph=validated.get("typical_speed_range_min_mph"),
typical_speed_range_max_mph=validated.get("typical_speed_range_max_mph"),
typical_capacity_range_min=validated.get("typical_capacity_range_min"),
typical_capacity_range_max=validated.get("typical_capacity_range_max"),
track_type=validated.get("track_type", ""),
support_structure=validated.get("support_structure", ""),
train_configuration=validated.get("train_configuration", ""),
restraint_system=validated.get("restraint_system", ""),
first_installation_year=validated.get("first_installation_year"),
last_installation_year=validated.get("last_installation_year"),
is_discontinued=validated.get("is_discontinued", False),
notable_features=validated.get("notable_features", ""),
target_market=validated.get("target_market", ""),
)
out_serializer = RideModelDetailOutputSerializer(
ride_model, context={"request": request}
)
return Response(out_serializer.data, status=status.HTTP_201_CREATED)
class RideModelDetailAPIView(APIView):
permission_classes = [permissions.AllowAny]
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)
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, 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}
)
return Response(serializer.data)
@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, 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)
# Update fields
for field, value in serializer_in.validated_data.items():
if field == "manufacturer_id":
try:
manufacturer = Company.objects.get(id=value)
ride_model.manufacturer = manufacturer
except Company.DoesNotExist:
raise ValidationError({"manufacturer_id": "Manufacturer not found"})
else:
setattr(ride_model, field, value)
ride_model.save()
serializer = RideModelDetailOutputSerializer(
ride_model, context={"request": request}
)
return Response(serializer.data)
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)
@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, 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)
# === RIDE MODEL SEARCH AND FILTER OPTIONS ===
class RideModelSearchAPIView(APIView):
permission_classes = [permissions.AllowAny]
@extend_schema(
summary="Search ride models",
description="Search ride models by name, description, or manufacturer.",
parameters=[
OpenApiParameter(
name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, required=True
)
],
responses={200: RideModelListOutputSerializer(many=True)},
tags=["Ride Models"],
)
def get(self, request: Request) -> Response:
q = request.query_params.get("q", "")
if not q:
return Response([], status=status.HTTP_200_OK)
if not MODELS_AVAILABLE:
return Response(
[
{
"id": 1,
"name": "Hyper Coaster",
"manufacturer": {"name": "Bolliger & Mabillard"},
"category": "RC"
}
]
)
qs = RideModel.objects.filter(
Q(name__icontains=q) |
Q(description__icontains=q) |
Q(manufacturer__name__icontains=q)
).select_related("manufacturer")[:20]
results = [
{
"id": model.id,
"name": model.name,
"slug": model.slug,
"manufacturer": {
"id": model.manufacturer.id if model.manufacturer else None,
"name": model.manufacturer.name if model.manufacturer else None,
"slug": model.manufacturer.slug if model.manufacturer else None,
},
"category": model.category,
"target_market": model.target_market,
"is_discontinued": model.is_discontinued,
}
for model in qs
]
return Response(results)
class RideModelFilterOptionsAPIView(APIView):
permission_classes = [permissions.AllowAny]
@extend_schema(
summary="Get filter options for ride models",
description="Get available filter options for ride model filtering.",
responses={200: OpenApiTypes.OBJECT},
tags=["Ride Models"],
)
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"}],
})
# Get actual data from database
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()
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"),
],
})
# === RIDE MODEL STATISTICS ===
class RideModelStatsAPIView(APIView):
permission_classes = [permissions.AllowAny]
@extend_schema(
summary="Get ride model statistics",
description="Get comprehensive statistics about ride models.",
responses={200: RideModelStatsOutputSerializer()},
tags=["Ride Models"],
)
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,
})
# Calculate statistics
total_models = RideModel.objects.count()
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()
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"))
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"))
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]
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()
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 ===
class RideModelVariantListCreateAPIView(APIView):
permission_classes = [permissions.AllowAny]
@extend_schema(
summary="List variants for a ride model",
description="Get all variants for a specific ride model.",
responses={200: RideModelVariantOutputSerializer(many=True)},
tags=["Ride Model Variants"],
)
def get(self, request: Request, ride_model_pk: int) -> Response:
if not MODELS_AVAILABLE:
return Response([])
try:
ride_model = RideModel.objects.get(pk=ride_model_pk)
except RideModel.DoesNotExist:
raise NotFound("Ride model not found")
variants = RideModelVariant.objects.filter(ride_model=ride_model)
serializer = RideModelVariantOutputSerializer(variants, many=True)
return Response(serializer.data)
@extend_schema(
summary="Create a variant for a ride model",
description="Create a new variant for a specific ride model.",
request=RideModelVariantCreateInputSerializer,
responses={201: RideModelVariantOutputSerializer()},
tags=["Ride Model Variants"],
)
def post(self, request: Request, ride_model_pk: int) -> Response:
if not MODELS_AVAILABLE:
return Response(
{"detail": "Variants not available"},
status=status.HTTP_501_NOT_IMPLEMENTED
)
try:
ride_model = RideModel.objects.get(pk=ride_model_pk)
except RideModel.DoesNotExist:
raise NotFound("Ride model not found")
# Override ride_model_id in the data
data = request.data.copy()
data["ride_model_id"] = ride_model_pk
serializer_in = RideModelVariantCreateInputSerializer(data=data)
serializer_in.is_valid(raise_exception=True)
validated = serializer_in.validated_data
variant = RideModelVariant.objects.create(
ride_model=ride_model,
name=validated["name"],
description=validated.get("description", ""),
min_height_ft=validated.get("min_height_ft"),
max_height_ft=validated.get("max_height_ft"),
min_speed_mph=validated.get("min_speed_mph"),
max_speed_mph=validated.get("max_speed_mph"),
distinguishing_features=validated.get("distinguishing_features", ""),
)
serializer = RideModelVariantOutputSerializer(variant)
return Response(serializer.data, status=status.HTTP_201_CREATED)
class RideModelVariantDetailAPIView(APIView):
permission_classes = [permissions.AllowAny]
def _get_variant_or_404(self, ride_model_pk: int, pk: int) -> Any:
if not MODELS_AVAILABLE:
raise NotFound("Variants not available")
try:
return RideModelVariant.objects.get(ride_model_id=ride_model_pk, pk=pk)
except RideModelVariant.DoesNotExist:
raise NotFound("Variant not found")
@extend_schema(
summary="Get a ride model variant",
responses={200: RideModelVariantOutputSerializer()},
tags=["Ride Model Variants"],
)
def get(self, request: Request, ride_model_pk: int, pk: int) -> Response:
variant = self._get_variant_or_404(ride_model_pk, pk)
serializer = RideModelVariantOutputSerializer(variant)
return Response(serializer.data)
@extend_schema(
summary="Update a ride model variant",
request=RideModelVariantUpdateInputSerializer,
responses={200: RideModelVariantOutputSerializer()},
tags=["Ride Model Variants"],
)
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)
serializer_in.is_valid(raise_exception=True)
for field, value in serializer_in.validated_data.items():
setattr(variant, field, value)
variant.save()
serializer = RideModelVariantOutputSerializer(variant)
return Response(serializer.data)
@extend_schema(
summary="Delete a ride model variant",
responses={204: None},
tags=["Ride Model Variants"],
)
def delete(self, request: Request, ride_model_pk: int, pk: int) -> Response:
variant = self._get_variant_or_404(ride_model_pk, pk)
variant.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
# 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...