""" 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 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 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, ) # 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 filtering and pagination", description="List rides with basic filtering and pagination.", parameters=[ 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="park_slug", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR ), ], responses={200: RideListOutputSerializer(many=True)}, tags=["Rides"], ) def get(self, request: Request) -> Response: """List rides with basic 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, ) qs = Ride.objects.all().select_related("park", "manufacturer", "designer") # type: ignore # Basic filters q = request.query_params.get("search") if q: qs = qs.filter(name__icontains=q) # simplistic search park_slug = request.query_params.get("park_slug") if park_slug: qs = qs.filter(park__slug=park_slug) # type: ignore 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 filter options for rides", responses={200: OpenApiTypes.OBJECT}, tags=["Rides"], ) class FilterOptionsAPIView(APIView): permission_classes = [permissions.AllowAny] def get(self, request: Request) -> Response: """Return static/dynamic 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(), "ordering_options": [ "name", "-name", "opening_date", "-opening_date", "average_rating", "-average_rating", ], } return Response(data) except Exception: # fallthrough to fallback pass # Fallback minimal options return Response( { "categories": ["ROLLER_COASTER", "WATER_RIDE", "FLAT"], "statuses": ["OPERATING", "CLOSED", "MAINTENANCE"], "ordering_options": ["name", "-name", "opening_date", "-opening_date"], } ) # --- 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 duplicate action --------------------------------------------------