""" 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 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, 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 with Rich Choice Objects metadata.""" # Import Rich Choice registry from apps.core.choices.registry import get_choices if not MODELS_AVAILABLE: # Use Rich Choice Objects for fallback options try: # Get rich choice objects from registry categories = get_choices('categories', 'rides') target_markets = get_choices('target_markets', 'rides') # Convert Rich Choice Objects to frontend format with metadata categories_data = [ { "value": choice.value, "label": choice.label, "description": choice.description, "color": choice.metadata.get('color'), "icon": choice.metadata.get('icon'), "css_class": choice.metadata.get('css_class'), "sort_order": choice.metadata.get('sort_order', 0) } for choice in categories ] target_markets_data = [ { "value": choice.value, "label": choice.label, "description": choice.description, "color": choice.metadata.get('color'), "icon": choice.metadata.get('icon'), "css_class": choice.metadata.get('css_class'), "sort_order": choice.metadata.get('sort_order', 0) } for choice in target_markets ] except Exception: # Ultimate fallback with basic structure categories_data = [ {"value": "RC", "label": "Roller Coaster", "description": "High-speed thrill rides with tracks", "color": "red", "icon": "roller-coaster", "css_class": "bg-red-100 text-red-800", "sort_order": 1}, {"value": "DR", "label": "Dark Ride", "description": "Indoor themed experiences", "color": "purple", "icon": "dark-ride", "css_class": "bg-purple-100 text-purple-800", "sort_order": 2}, {"value": "FR", "label": "Flat Ride", "description": "Spinning and rotating attractions", "color": "blue", "icon": "flat-ride", "css_class": "bg-blue-100 text-blue-800", "sort_order": 3}, {"value": "WR", "label": "Water Ride", "description": "Water-based attractions and slides", "color": "cyan", "icon": "water-ride", "css_class": "bg-cyan-100 text-cyan-800", "sort_order": 4}, {"value": "TR", "label": "Transport", "description": "Transportation systems within parks", "color": "green", "icon": "transport", "css_class": "bg-green-100 text-green-800", "sort_order": 5}, {"value": "OT", "label": "Other", "description": "Miscellaneous attractions", "color": "gray", "icon": "other", "css_class": "bg-gray-100 text-gray-800", "sort_order": 6}, ] target_markets_data = [ {"value": "FAMILY", "label": "Family", "description": "Suitable for all family members", "color": "green", "icon": "family", "css_class": "bg-green-100 text-green-800", "sort_order": 1}, {"value": "THRILL", "label": "Thrill", "description": "High-intensity thrill experience", "color": "orange", "icon": "thrill", "css_class": "bg-orange-100 text-orange-800", "sort_order": 2}, {"value": "EXTREME", "label": "Extreme", "description": "Maximum intensity experience", "color": "red", "icon": "extreme", "css_class": "bg-red-100 text-red-800", "sort_order": 3}, {"value": "KIDDIE", "label": "Kiddie", "description": "Designed for young children", "color": "pink", "icon": "kiddie", "css_class": "bg-pink-100 text-pink-800", "sort_order": 4}, {"value": "ALL_AGES", "label": "All Ages", "description": "Enjoyable for all age groups", "color": "blue", "icon": "all-ages", "css_class": "bg-blue-100 text-blue-800", "sort_order": 5}, ] return Response({ "categories": categories_data, "target_markets": target_markets_data, "manufacturers": [{"id": 1, "name": "Bolliger & Mabillard", "slug": "bolliger-mabillard"}], "ordering_options": [ {"value": "name", "label": "Name A-Z"}, {"value": "-name", "label": "Name Z-A"}, {"value": "manufacturer__name", "label": "Manufacturer A-Z"}, {"value": "-manufacturer__name", "label": "Manufacturer Z-A"}, {"value": "first_installation_year", "label": "Oldest First"}, {"value": "-first_installation_year", "label": "Newest First"}, {"value": "total_installations", "label": "Fewest Installations"}, {"value": "-total_installations", "label": "Most Installations"}, ], }) # Get static choice definitions from Rich Choice Objects (primary source) # Get dynamic data from database queries # Get rich choice objects from registry categories = get_choices('categories', 'rides') target_markets = get_choices('target_markets', 'rides') # Convert Rich Choice Objects to frontend format with metadata categories_data = [ { "value": choice.value, "label": choice.label, "description": choice.description, "color": choice.metadata.get('color'), "icon": choice.metadata.get('icon'), "css_class": choice.metadata.get('css_class'), "sort_order": choice.metadata.get('sort_order', 0) } for choice in categories ] target_markets_data = [ { "value": choice.value, "label": choice.label, "description": choice.description, "color": choice.metadata.get('color'), "icon": choice.metadata.get('icon'), "css_class": choice.metadata.get('css_class'), "sort_order": choice.metadata.get('sort_order', 0) } for choice in target_markets ] # Get actual data from database manufacturers = ( Company.objects.filter( roles__contains=["MANUFACTURER"], ride_models__isnull=False ) .distinct() .values("id", "name", "slug") ) return Response({ "categories": categories_data, "target_markets": target_markets_data, "manufacturers": list(manufacturers), "ordering_options": [ {"value": "name", "label": "Name A-Z"}, {"value": "-name", "label": "Name Z-A"}, {"value": "manufacturer__name", "label": "Manufacturer A-Z"}, {"value": "-manufacturer__name", "label": "Manufacturer Z-A"}, {"value": "first_installation_year", "label": "Oldest First"}, {"value": "-first_installation_year", "label": "Newest First"}, {"value": "total_installations", "label": "Fewest Installations"}, {"value": "-total_installations", "label": "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...