""" Full-featured Rides API views for ThrillWiki API v1. This module implements a "full fat" set of endpoints: - List / Create: GET /rides/ POST /rides/ - Retrieve / Update / Delete: GET /rides/{pk}/ PATCH/PUT/DELETE - Filter options: GET /rides/filter-options/ - Company search: GET /rides/search/companies/?q=... - Ride model search: GET /rides/search-ride-models/?q=... - Search suggestions: GET /rides/search-suggestions/?q=... Notes: - These views try to use real Django models if available. If the domain models/services are not present, they return a clear 501 response explaining what to wire up. """ from typing import Any from django.db import models from rest_framework import status, permissions from rest_framework.views import APIView from rest_framework.request import Request 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 # Reuse existing serializers where possible from apps.api.v1.serializers.rides import ( RideListOutputSerializer, RideDetailOutputSerializer, RideCreateInputSerializer, RideUpdateInputSerializer, RideImageSettingsInputSerializer, ) # Attempt to import model-level helpers; fall back gracefully if not present. try: from apps.rides.models import Ride, RideModel, Company as RideCompany # type: ignore from apps.parks.models import Park, Company as ParkCompany # type: ignore MODELS_AVAILABLE = True except Exception: Ride = None # type: ignore RideModel = None # type: ignore RideCompany = None # type: ignore Park = None # type: ignore ParkCompany = None # type: ignore MODELS_AVAILABLE = False # Attempt to import ModelChoices to return filter options try: from apps.api.v1.serializers.shared import ModelChoices # type: ignore HAVE_MODELCHOICES = True except Exception: ModelChoices = None # type: ignore HAVE_MODELCHOICES = False class StandardResultsSetPagination(PageNumberPagination): page_size = 20 page_size_query_param = "page_size" max_page_size = 1000 # --- Ride list & create ----------------------------------------------------- class RideListCreateAPIView(APIView): permission_classes = [permissions.AllowAny] @extend_schema( summary="List rides with comprehensive filtering and pagination", description="List rides with comprehensive filtering options including category, status, manufacturer, designer, ride model, and more.", parameters=[ OpenApiParameter( name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Page number for pagination" ), OpenApiParameter( 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" ), OpenApiParameter( name="park_slug", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description="Filter by park slug" ), OpenApiParameter( name="park_id", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Filter by park ID" ), OpenApiParameter( name="category", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description="Filter by ride category (RC, DR, FR, WR, TR, OT). Multiple values supported: ?category=RC&category=DR" ), OpenApiParameter( name="status", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description="Filter by ride status. Multiple values supported: ?status=OPERATING&status=CLOSED_TEMP" ), OpenApiParameter( name="manufacturer_id", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Filter by manufacturer company ID" ), OpenApiParameter( name="manufacturer_slug", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description="Filter by manufacturer company slug" ), OpenApiParameter( name="designer_id", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Filter by designer company ID" ), OpenApiParameter( name="designer_slug", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description="Filter by designer company slug" ), OpenApiParameter( name="ride_model_id", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Filter by specific ride model ID" ), OpenApiParameter( name="ride_model_slug", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description="Filter by ride model slug (requires manufacturer_slug)" ), OpenApiParameter( name="roller_coaster_type", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description="Filter roller coasters by type (SITDOWN, INVERTED, FLYING, etc.)" ), OpenApiParameter( name="track_material", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description="Filter roller coasters by track material (STEEL, WOOD, HYBRID)" ), OpenApiParameter( name="launch_type", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description="Filter roller coasters by launch type (CHAIN, LSM, HYDRAULIC, etc.)" ), OpenApiParameter( name="min_rating", location=OpenApiParameter.QUERY, type=OpenApiTypes.NUMBER, description="Filter by minimum average rating (1-10)" ), OpenApiParameter( name="max_rating", location=OpenApiParameter.QUERY, type=OpenApiTypes.NUMBER, description="Filter by maximum average rating (1-10)" ), OpenApiParameter( name="min_height_requirement", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Filter by minimum height requirement in inches" ), OpenApiParameter( name="max_height_requirement", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Filter by maximum height requirement in inches" ), OpenApiParameter( name="min_capacity", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Filter by minimum hourly capacity" ), OpenApiParameter( name="max_capacity", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Filter by maximum hourly capacity" ), OpenApiParameter( name="min_height_ft", location=OpenApiParameter.QUERY, type=OpenApiTypes.NUMBER, description="Filter roller coasters by minimum height in feet" ), OpenApiParameter( name="max_height_ft", location=OpenApiParameter.QUERY, type=OpenApiTypes.NUMBER, description="Filter roller coasters by maximum height in feet" ), OpenApiParameter( name="min_speed_mph", location=OpenApiParameter.QUERY, type=OpenApiTypes.NUMBER, description="Filter roller coasters by minimum speed in mph" ), OpenApiParameter( name="max_speed_mph", location=OpenApiParameter.QUERY, type=OpenApiTypes.NUMBER, description="Filter roller coasters by maximum speed in mph" ), OpenApiParameter( name="min_inversions", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Filter roller coasters by minimum number of inversions" ), OpenApiParameter( name="max_inversions", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Filter roller coasters by maximum number of inversions" ), OpenApiParameter( name="has_inversions", location=OpenApiParameter.QUERY, type=OpenApiTypes.BOOL, description="Filter roller coasters that have inversions (true) or don't have inversions (false)" ), OpenApiParameter( name="opening_year", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Filter by opening year" ), OpenApiParameter( name="min_opening_year", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Filter by minimum opening year" ), OpenApiParameter( name="max_opening_year", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, description="Filter by maximum opening year" ), OpenApiParameter( name="ordering", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR, description="Order results by field. Options: name, -name, opening_date, -opening_date, average_rating, -average_rating, capacity_per_hour, -capacity_per_hour, created_at, -created_at, height_ft, -height_ft, speed_mph, -speed_mph" ), ], responses={200: RideListOutputSerializer(many=True)}, tags=["Rides"], ) def get(self, request: Request) -> Response: """List rides with comprehensive filtering and pagination.""" if not MODELS_AVAILABLE: return Response( { "detail": "Ride listing is not available because domain models are not imported. " "Implement apps.rides.models.Ride (and related managers) to enable listing." }, status=status.HTTP_501_NOT_IMPLEMENTED, ) # 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 # Text search search = request.query_params.get("search") if search: qs = qs.filter( models.Q(name__icontains=search) | models.Q(description__icontains=search) | models.Q(park__name__icontains=search) ) # Park filters park_slug = request.query_params.get("park_slug") if park_slug: qs = qs.filter(park__slug=park_slug) park_id = request.query_params.get("park_id") if park_id: try: qs = qs.filter(park_id=int(park_id)) except (ValueError, TypeError): pass # Category filters (multiple values supported) categories = request.query_params.getlist("category") if categories: qs = qs.filter(category__in=categories) # Status filters (multiple values supported) statuses = request.query_params.getlist("status") if statuses: qs = qs.filter(status__in=statuses) # Manufacturer filters manufacturer_id = request.query_params.get("manufacturer_id") if manufacturer_id: try: qs = qs.filter(manufacturer_id=int(manufacturer_id)) except (ValueError, TypeError): pass manufacturer_slug = request.query_params.get("manufacturer_slug") if manufacturer_slug: qs = qs.filter(manufacturer__slug=manufacturer_slug) # Designer filters designer_id = request.query_params.get("designer_id") if designer_id: try: qs = qs.filter(designer_id=int(designer_id)) except (ValueError, TypeError): pass designer_slug = request.query_params.get("designer_slug") if designer_slug: qs = qs.filter(designer__slug=designer_slug) # Ride model filters ride_model_id = request.query_params.get("ride_model_id") if ride_model_id: try: qs = qs.filter(ride_model_id=int(ride_model_id)) except (ValueError, TypeError): pass ride_model_slug = request.query_params.get("ride_model_slug") manufacturer_slug_for_model = request.query_params.get("manufacturer_slug") if ride_model_slug and manufacturer_slug_for_model: qs = qs.filter( ride_model__slug=ride_model_slug, ride_model__manufacturer__slug=manufacturer_slug_for_model ) # Rating filters min_rating = request.query_params.get("min_rating") if min_rating: try: qs = qs.filter(average_rating__gte=float(min_rating)) except (ValueError, TypeError): pass max_rating = request.query_params.get("max_rating") if max_rating: try: qs = qs.filter(average_rating__lte=float(max_rating)) except (ValueError, TypeError): pass # Height requirement filters min_height_req = request.query_params.get("min_height_requirement") if min_height_req: try: qs = qs.filter(min_height_in__gte=int(min_height_req)) except (ValueError, TypeError): pass max_height_req = request.query_params.get("max_height_requirement") if max_height_req: try: qs = qs.filter(max_height_in__lte=int(max_height_req)) except (ValueError, TypeError): pass # Capacity filters min_capacity = request.query_params.get("min_capacity") if min_capacity: try: qs = qs.filter(capacity_per_hour__gte=int(min_capacity)) except (ValueError, TypeError): pass max_capacity = request.query_params.get("max_capacity") if max_capacity: try: qs = qs.filter(capacity_per_hour__lte=int(max_capacity)) except (ValueError, TypeError): pass # Opening year filters opening_year = request.query_params.get("opening_year") if opening_year: try: qs = qs.filter(opening_date__year=int(opening_year)) except (ValueError, TypeError): pass min_opening_year = request.query_params.get("min_opening_year") if min_opening_year: try: qs = qs.filter(opening_date__year__gte=int(min_opening_year)) except (ValueError, TypeError): pass max_opening_year = request.query_params.get("max_opening_year") if max_opening_year: try: qs = qs.filter(opening_date__year__lte=int(max_opening_year)) except (ValueError, TypeError): pass # Roller coaster specific filters roller_coaster_type = request.query_params.get("roller_coaster_type") if roller_coaster_type: qs = qs.filter(coaster_stats__roller_coaster_type=roller_coaster_type) track_material = request.query_params.get("track_material") if track_material: qs = qs.filter(coaster_stats__track_material=track_material) launch_type = request.query_params.get("launch_type") if launch_type: qs = qs.filter(coaster_stats__launch_type=launch_type) # Roller coaster height filters min_height_ft = request.query_params.get("min_height_ft") if min_height_ft: try: qs = qs.filter(coaster_stats__height_ft__gte=float(min_height_ft)) except (ValueError, TypeError): pass max_height_ft = request.query_params.get("max_height_ft") if max_height_ft: try: qs = qs.filter(coaster_stats__height_ft__lte=float(max_height_ft)) except (ValueError, TypeError): pass # Roller coaster speed filters min_speed_mph = request.query_params.get("min_speed_mph") if min_speed_mph: try: qs = qs.filter(coaster_stats__speed_mph__gte=float(min_speed_mph)) except (ValueError, TypeError): pass max_speed_mph = request.query_params.get("max_speed_mph") if max_speed_mph: try: qs = qs.filter(coaster_stats__speed_mph__lte=float(max_speed_mph)) except (ValueError, TypeError): pass # Inversion filters min_inversions = request.query_params.get("min_inversions") if min_inversions: try: qs = qs.filter(coaster_stats__inversions__gte=int(min_inversions)) except (ValueError, TypeError): pass max_inversions = request.query_params.get("max_inversions") if max_inversions: try: qs = qs.filter(coaster_stats__inversions__lte=int(max_inversions)) except (ValueError, TypeError): pass has_inversions = request.query_params.get("has_inversions") if has_inversions is not None: if has_inversions.lower() in ['true', '1', 'yes']: qs = qs.filter(coaster_stats__inversions__gt=0) elif has_inversions.lower() in ['false', '0', 'no']: qs = qs.filter(coaster_stats__inversions=0) # Ordering ordering = request.query_params.get("ordering", "name") valid_orderings = [ "name", "-name", "opening_date", "-opening_date", "average_rating", "-average_rating", "capacity_per_hour", "-capacity_per_hour", "created_at", "-created_at", "height_ft", "-height_ft", "speed_mph", "-speed_mph" ] if ordering in valid_orderings: if ordering in ["height_ft", "-height_ft", "speed_mph", "-speed_mph"]: # For coaster stats ordering, we need to join and order by the stats ordering_field = ordering.replace("height_ft", "coaster_stats__height_ft").replace( "speed_mph", "coaster_stats__speed_mph") qs = qs.order_by(ordering_field) else: qs = qs.order_by(ordering) paginator = StandardResultsSetPagination() page = paginator.paginate_queryset(qs, request) serializer = RideListOutputSerializer( page, many=True, context={"request": request} ) return paginator.get_paginated_response(serializer.data) @extend_schema( summary="Create a new ride", description="Create a new ride.", responses={201: RideDetailOutputSerializer()}, tags=["Rides"], ) def post(self, request: Request) -> Response: """Create a new ride.""" serializer_in = RideCreateInputSerializer(data=request.data) serializer_in.is_valid(raise_exception=True) if not MODELS_AVAILABLE: return Response( { "detail": "Ride creation is not available because domain models are not imported. " "Implement apps.rides.models.Ride and necessary create logic." }, status=status.HTTP_501_NOT_IMPLEMENTED, ) validated = serializer_in.validated_data # Minimal create logic using model fields if available. try: park = Park.objects.get(id=validated["park_id"]) # type: ignore except Park.DoesNotExist: # type: ignore raise NotFound("Park not found") ride = Ride.objects.create( # type: ignore name=validated["name"], description=validated.get("description", ""), category=validated.get("category"), status=validated.get("status"), park=park, park_area_id=validated.get("park_area_id"), opening_date=validated.get("opening_date"), closing_date=validated.get("closing_date"), status_since=validated.get("status_since"), min_height_in=validated.get("min_height_in"), max_height_in=validated.get("max_height_in"), capacity_per_hour=validated.get("capacity_per_hour"), ride_duration_seconds=validated.get("ride_duration_seconds"), ) # Optional foreign keys if validated.get("manufacturer_id"): try: ride.manufacturer_id = validated["manufacturer_id"] ride.save() except Exception: # ignore if foreign key constraints or models not present pass out_serializer = RideDetailOutputSerializer(ride, context={"request": request}) return Response(out_serializer.data, status=status.HTTP_201_CREATED) # --- Ride retrieve / update / delete --------------------------------------- @extend_schema( summary="Retrieve, update or delete a ride", responses={200: RideDetailOutputSerializer()}, tags=["Rides"], ) class RideDetailAPIView(APIView): permission_classes = [permissions.AllowAny] def _get_ride_or_404(self, pk: int) -> Any: if not MODELS_AVAILABLE: raise NotFound( "Ride detail is not available because domain models are not imported. " "Implement apps.rides.models.Ride to enable detail endpoints." ) try: return Ride.objects.select_related("park").get(pk=pk) # type: ignore except Ride.DoesNotExist: # type: ignore raise NotFound("Ride not found") def get(self, request: Request, pk: int) -> Response: ride = self._get_ride_or_404(pk) serializer = RideDetailOutputSerializer(ride, context={"request": request}) return Response(serializer.data) def patch(self, request: Request, pk: int) -> Response: ride = self._get_ride_or_404(pk) serializer_in = RideUpdateInputSerializer(data=request.data, partial=True) serializer_in.is_valid(raise_exception=True) if not MODELS_AVAILABLE: return Response( { "detail": "Ride update is not available because domain models are not imported." }, status=status.HTTP_501_NOT_IMPLEMENTED, ) for key, value in serializer_in.validated_data.items(): setattr(ride, key, value) ride.save() serializer = RideDetailOutputSerializer(ride, context={"request": request}) return Response(serializer.data) def put(self, request: Request, pk: int) -> Response: # Full replace - reuse patch behavior for simplicity return self.patch(request, pk) def delete(self, request: Request, pk: int) -> Response: if not MODELS_AVAILABLE: return Response( { "detail": "Ride delete is not available because domain models are not imported." }, status=status.HTTP_501_NOT_IMPLEMENTED, ) ride = self._get_ride_or_404(pk) ride.delete() return Response(status=status.HTTP_204_NO_CONTENT) # --- Filter options --------------------------------------------------------- @extend_schema( summary="Get comprehensive filter options for rides", description="Returns all available filter options for rides including categories, statuses, roller coaster types, track materials, launch types, and ordering options.", responses={200: OpenApiTypes.OBJECT}, tags=["Rides"], ) class FilterOptionsAPIView(APIView): permission_classes = [permissions.AllowAny] def get(self, request: Request) -> Response: """Return comprehensive filter options used by the frontend.""" # Try to use ModelChoices if available if HAVE_MODELCHOICES and ModelChoices is not None: try: data = { "categories": ModelChoices.get_ride_category_choices(), "statuses": ModelChoices.get_ride_status_choices(), "post_closing_statuses": ModelChoices.get_ride_post_closing_choices(), "roller_coaster_types": ModelChoices.get_coaster_type_choices(), "track_materials": ModelChoices.get_coaster_track_choices(), "launch_types": ModelChoices.get_launch_choices(), "ordering_options": [ {"value": "name", "label": "Name (A-Z)"}, {"value": "-name", "label": "Name (Z-A)"}, {"value": "opening_date", "label": "Opening Date (Oldest First)"}, {"value": "-opening_date", "label": "Opening Date (Newest First)"}, {"value": "average_rating", "label": "Rating (Lowest First)"}, {"value": "-average_rating", "label": "Rating (Highest First)"}, {"value": "capacity_per_hour", "label": "Capacity (Lowest First)"}, {"value": "-capacity_per_hour", "label": "Capacity (Highest First)"}, {"value": "height_ft", "label": "Height (Shortest First)"}, {"value": "-height_ft", "label": "Height (Tallest First)"}, {"value": "speed_mph", "label": "Speed (Slowest First)"}, {"value": "-speed_mph", "label": "Speed (Fastest First)"}, {"value": "created_at", "label": "Date Added (Oldest First)"}, {"value": "-created_at", "label": "Date Added (Newest First)"}, ], "filter_ranges": { "rating": {"min": 1, "max": 10, "step": 0.1}, "height_requirement": {"min": 30, "max": 90, "step": 1, "unit": "inches"}, "capacity": {"min": 0, "max": 5000, "step": 50, "unit": "riders/hour"}, "height_ft": {"min": 0, "max": 500, "step": 5, "unit": "feet"}, "speed_mph": {"min": 0, "max": 150, "step": 5, "unit": "mph"}, "inversions": {"min": 0, "max": 20, "step": 1, "unit": "inversions"}, "opening_year": {"min": 1800, "max": 2030, "step": 1, "unit": "year"}, }, "boolean_filters": [ {"key": "has_inversions", "label": "Has Inversions", "description": "Filter roller coasters with or without inversions"}, ], } return Response(data) except Exception: # fallthrough to fallback pass # Comprehensive fallback options return Response( { "categories": [ ("RC", "Roller Coaster"), ("DR", "Dark Ride"), ("FR", "Flat Ride"), ("WR", "Water Ride"), ("TR", "Transport"), ("OT", "Other"), ], "statuses": [ ("OPERATING", "Operating"), ("CLOSED_TEMP", "Temporarily Closed"), ("SBNO", "Standing But Not Operating"), ("CLOSING", "Closing"), ("CLOSED_PERM", "Permanently Closed"), ("UNDER_CONSTRUCTION", "Under Construction"), ("DEMOLISHED", "Demolished"), ("RELOCATED", "Relocated"), ], "roller_coaster_types": [ ("SITDOWN", "Sit Down"), ("INVERTED", "Inverted"), ("FLYING", "Flying"), ("STANDUP", "Stand Up"), ("WING", "Wing"), ("DIVE", "Dive"), ("FAMILY", "Family"), ("WILD_MOUSE", "Wild Mouse"), ("SPINNING", "Spinning"), ("FOURTH_DIMENSION", "4th Dimension"), ("OTHER", "Other"), ], "track_materials": [ ("STEEL", "Steel"), ("WOOD", "Wood"), ("HYBRID", "Hybrid"), ], "launch_types": [ ("CHAIN", "Chain Lift"), ("LSM", "LSM Launch"), ("HYDRAULIC", "Hydraulic Launch"), ("GRAVITY", "Gravity"), ("OTHER", "Other"), ], "ordering_options": [ {"value": "name", "label": "Name (A-Z)"}, {"value": "-name", "label": "Name (Z-A)"}, {"value": "opening_date", "label": "Opening Date (Oldest First)"}, {"value": "-opening_date", "label": "Opening Date (Newest First)"}, {"value": "average_rating", "label": "Rating (Lowest First)"}, {"value": "-average_rating", "label": "Rating (Highest First)"}, {"value": "capacity_per_hour", "label": "Capacity (Lowest First)"}, {"value": "-capacity_per_hour", "label": "Capacity (Highest First)"}, {"value": "height_ft", "label": "Height (Shortest First)"}, {"value": "-height_ft", "label": "Height (Tallest First)"}, {"value": "speed_mph", "label": "Speed (Slowest First)"}, {"value": "-speed_mph", "label": "Speed (Fastest First)"}, {"value": "created_at", "label": "Date Added (Oldest First)"}, {"value": "-created_at", "label": "Date Added (Newest First)"}, ], "filter_ranges": { "rating": {"min": 1, "max": 10, "step": 0.1}, "height_requirement": {"min": 30, "max": 90, "step": 1, "unit": "inches"}, "capacity": {"min": 0, "max": 5000, "step": 50, "unit": "riders/hour"}, "height_ft": {"min": 0, "max": 500, "step": 5, "unit": "feet"}, "speed_mph": {"min": 0, "max": 150, "step": 5, "unit": "mph"}, "inversions": {"min": 0, "max": 20, "step": 1, "unit": "inversions"}, "opening_year": {"min": 1800, "max": 2030, "step": 1, "unit": "year"}, }, "boolean_filters": [ {"key": "has_inversions", "label": "Has Inversions", "description": "Filter roller coasters with or without inversions"}, ], } ) # --- Company search (autocomplete) ----------------------------------------- @extend_schema( summary="Search companies (manufacturers/designers) for autocomplete", parameters=[ OpenApiParameter( name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR ) ], responses={200: OpenApiTypes.OBJECT}, tags=["Rides"], ) class CompanySearchAPIView(APIView): permission_classes = [permissions.AllowAny] def get(self, request: Request) -> Response: q = request.query_params.get("q", "") if not q: return Response([], status=status.HTTP_200_OK) if RideCompany is None: # Provide helpful placeholder structure return Response( [ {"id": 1, "name": "Rocky Mountain Construction", "slug": "rmc"}, {"id": 2, "name": "Bolliger & Mabillard", "slug": "b&m"}, ] ) qs = RideCompany.objects.filter(name__icontains=q)[:20] # type: ignore results = [ {"id": c.id, "name": c.name, "slug": getattr(c, "slug", "")} for c in qs ] return Response(results) # --- Ride model search (autocomplete) -------------------------------------- @extend_schema( summary="Search ride models for autocomplete", parameters=[ OpenApiParameter( name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR ) ], tags=["Rides"], ) class RideModelSearchAPIView(APIView): permission_classes = [permissions.AllowAny] def get(self, request: Request) -> Response: q = request.query_params.get("q", "") if not q: return Response([], status=status.HTTP_200_OK) if RideModel is None: return Response( [ {"id": 1, "name": "I-Box (RMC)", "category": "ROLLER_COASTER"}, { "id": 2, "name": "Hyper Coaster Model X", "category": "ROLLER_COASTER", }, ] ) qs = RideModel.objects.filter(name__icontains=q)[:20] # type: ignore results = [ {"id": m.id, "name": m.name, "category": getattr(m, "category", "")} for m in qs ] return Response(results) # --- Search suggestions ----------------------------------------------------- @extend_schema( summary="Search suggestions for ride search box", parameters=[ OpenApiParameter( name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR ) ], tags=["Rides"], ) class RideSearchSuggestionsAPIView(APIView): permission_classes = [permissions.AllowAny] def get(self, request: Request) -> Response: q = request.query_params.get("q", "") if not q: return Response([], status=status.HTTP_200_OK) # Very small suggestion implementation: look in ride names if available if MODELS_AVAILABLE and Ride is not None: qs = Ride.objects.filter(name__icontains=q).values_list("name", flat=True)[ :10 ] # type: ignore return Response([{"suggestion": name} for name in qs]) # Fallback suggestions fallback = [ {"suggestion": f"{q} coaster"}, {"suggestion": f"{q} ride"}, {"suggestion": f"{q} park"}, ] return Response(fallback) # --- Ride image settings --------------------------------------------------- @extend_schema( summary="Set ride banner and card images", description="Set banner_image and card_image for a ride from existing ride photos", request=RideImageSettingsInputSerializer, responses={ 200: RideDetailOutputSerializer, 400: OpenApiTypes.OBJECT, 404: OpenApiTypes.OBJECT, }, tags=["Rides"], ) class RideImageSettingsAPIView(APIView): permission_classes = [permissions.AllowAny] def _get_ride_or_404(self, pk: int) -> Any: if not MODELS_AVAILABLE: raise NotFound("Ride models not available") try: return Ride.objects.get(pk=pk) # type: ignore except Ride.DoesNotExist: # type: ignore raise NotFound("Ride not found") def patch(self, request: Request, pk: int) -> Response: """Set banner and card images for the ride.""" ride = self._get_ride_or_404(pk) serializer = RideImageSettingsInputSerializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) # Update the ride with the validated data for field, value in serializer.validated_data.items(): setattr(ride, field, value) ride.save() # Return updated ride data output_serializer = RideDetailOutputSerializer( ride, context={"request": request}) return Response(output_serializer.data) # --- Ride duplicate action --------------------------------------------------