mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 02:11:08 -05:00
- Implemented PrimeProgress component with support for labels, helper text, and various styles (size, variant, color). - Created PrimeSelect component with dropdown functionality, custom templates, and validation states. - Developed PrimeSkeleton component for loading placeholders with different shapes and animations. - Updated index.ts to export new components for easy import. - Enhanced PrimeVueTest.vue to include tests for new components and their functionalities. - Introduced a custom ThrillWiki theme for PrimeVue with tailored color schemes and component styles. - Added ambient type declarations for various components to improve TypeScript support.
384 lines
14 KiB
Python
384 lines
14 KiB
Python
"""
|
|
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 --------------------------------------------------
|