feat: Implement initial schema and add various API, service, and management command enhancements across the application.

This commit is contained in:
pacnpal
2026-01-01 15:13:01 -05:00
parent c95f99ca10
commit b243b17af7
413 changed files with 11164 additions and 17433 deletions

View File

@@ -28,6 +28,7 @@ import logging
from typing import Any
from django.db import models
from django.db.models import Count
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
from rest_framework import permissions, status
@@ -333,9 +334,7 @@ class RideListCreateAPIView(APIView):
paginator = StandardResultsSetPagination()
page = paginator.paginate_queryset(qs, request)
serializer = RideListOutputSerializer(
page, many=True, context={"request": request}
)
serializer = RideListOutputSerializer(page, many=True, context={"request": request})
return paginator.get_paginated_response(serializer.data)
def _apply_filters(self, qs, params):
@@ -567,9 +566,9 @@ class RideListCreateAPIView(APIView):
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")
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)
@@ -602,7 +601,7 @@ class RideListCreateAPIView(APIView):
try:
park = Park.objects.get(id=validated["park_id"]) # type: ignore
except Park.DoesNotExist: # type: ignore
raise NotFound("Park not found")
raise NotFound("Park not found") from None
ride = Ride.objects.create( # type: ignore
name=validated["name"],
@@ -658,7 +657,7 @@ class RideDetailAPIView(APIView):
try:
return Ride.objects.select_related("park").get(pk=pk) # type: ignore
except Ride.DoesNotExist: # type: ignore
raise NotFound("Ride not found")
raise NotFound("Ride not found") from None
@cache_api_response(timeout=1800, key_prefix="ride_detail")
def get(self, request: Request, pk: int) -> Response:
@@ -672,9 +671,7 @@ class RideDetailAPIView(APIView):
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."
},
{"detail": "Ride update is not available because domain models are not imported."},
status=status.HTTP_501_NOT_IMPLEMENTED,
)
@@ -690,7 +687,7 @@ class RideDetailAPIView(APIView):
# Use the move_to_park method for proper handling
park_change_info = ride.move_to_park(new_park)
except Park.DoesNotExist: # type: ignore
raise NotFound("Target park not found")
raise NotFound("Target park not found") from None
# Apply other field updates
for key, value in validated_data.items():
@@ -715,9 +712,7 @@ class RideDetailAPIView(APIView):
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."
},
{"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)
@@ -1491,16 +1486,12 @@ class FilterOptionsAPIView(APIView):
# Get manufacturers (companies with MANUFACTURER role)
manufacturers = list(
Company.objects.filter(roles__contains=["MANUFACTURER"])
.values("id", "name", "slug")
.order_by("name")
Company.objects.filter(roles__contains=["MANUFACTURER"]).values("id", "name", "slug").order_by("name")
)
# Get designers (companies with DESIGNER role)
designers = list(
Company.objects.filter(roles__contains=["DESIGNER"])
.values("id", "name", "slug")
.order_by("name")
Company.objects.filter(roles__contains=["DESIGNER"]).values("id", "name", "slug").order_by("name")
)
# Get ride models data from database
@@ -1722,11 +1713,7 @@ class FilterOptionsAPIView(APIView):
# --- Company search (autocomplete) -----------------------------------------
@extend_schema(
summary="Search companies (manufacturers/designers) for autocomplete",
parameters=[
OpenApiParameter(
name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR
)
],
parameters=[OpenApiParameter(name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR)],
responses={200: OpenApiTypes.OBJECT},
tags=["Rides"],
)
@@ -1753,20 +1740,14 @@ class CompanySearchAPIView(APIView):
)
qs = Company.objects.filter(name__icontains=q)[:20] # type: ignore
results = [
{"id": c.id, "name": c.name, "slug": getattr(c, "slug", "")} for c in qs
]
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
)
],
parameters=[OpenApiParameter(name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR)],
tags=["Rides"],
)
class RideModelSearchAPIView(APIView):
@@ -1795,21 +1776,14 @@ class RideModelSearchAPIView(APIView):
)
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
]
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
)
],
parameters=[OpenApiParameter(name="q", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR)],
tags=["Rides"],
)
class RideSearchSuggestionsAPIView(APIView):
@@ -1827,9 +1801,7 @@ class RideSearchSuggestionsAPIView(APIView):
# 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
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
@@ -1862,7 +1834,7 @@ class RideImageSettingsAPIView(APIView):
try:
return Ride.objects.get(pk=pk) # type: ignore
except Ride.DoesNotExist: # type: ignore
raise NotFound("Ride not found")
raise NotFound("Ride not found") from None
def patch(self, request: Request, pk: int) -> Response:
"""Set banner and card images for the ride."""
@@ -1878,9 +1850,7 @@ class RideImageSettingsAPIView(APIView):
ride.save()
# Return updated ride data
output_serializer = RideDetailOutputSerializer(
ride, context={"request": request}
)
output_serializer = RideDetailOutputSerializer(ride, context={"request": request})
return Response(output_serializer.data)
@@ -1902,12 +1872,8 @@ class RideImageSettingsAPIView(APIView):
OpenApiTypes.STR,
description="Filter by ride status (comma-separated for multiple)",
),
OpenApiParameter(
"park_slug", OpenApiTypes.STR, description="Filter by park slug"
),
OpenApiParameter(
"park_id", OpenApiTypes.INT, description="Filter by park ID"
),
OpenApiParameter("park_slug", OpenApiTypes.STR, description="Filter by park slug"),
OpenApiParameter("park_id", OpenApiTypes.INT, description="Filter by park ID"),
OpenApiParameter(
"manufacturer",
OpenApiTypes.STR,
@@ -1923,18 +1889,10 @@ class RideImageSettingsAPIView(APIView):
OpenApiTypes.STR,
description="Filter by ride model slug (comma-separated for multiple)",
),
OpenApiParameter(
"opening_year_min", OpenApiTypes.INT, description="Minimum opening year"
),
OpenApiParameter(
"opening_year_max", OpenApiTypes.INT, description="Maximum opening year"
),
OpenApiParameter(
"rating_min", OpenApiTypes.NUMBER, description="Minimum average rating"
),
OpenApiParameter(
"rating_max", OpenApiTypes.NUMBER, description="Maximum average rating"
),
OpenApiParameter("opening_year_min", OpenApiTypes.INT, description="Minimum opening year"),
OpenApiParameter("opening_year_max", OpenApiTypes.INT, description="Maximum opening year"),
OpenApiParameter("rating_min", OpenApiTypes.NUMBER, description="Minimum average rating"),
OpenApiParameter("rating_max", OpenApiTypes.NUMBER, description="Maximum average rating"),
OpenApiParameter(
"height_requirement_min",
OpenApiTypes.INT,
@@ -1945,12 +1903,8 @@ class RideImageSettingsAPIView(APIView):
OpenApiTypes.INT,
description="Maximum height requirement in inches",
),
OpenApiParameter(
"capacity_min", OpenApiTypes.INT, description="Minimum hourly capacity"
),
OpenApiParameter(
"capacity_max", OpenApiTypes.INT, description="Maximum hourly capacity"
),
OpenApiParameter("capacity_min", OpenApiTypes.INT, description="Minimum hourly capacity"),
OpenApiParameter("capacity_max", OpenApiTypes.INT, description="Maximum hourly capacity"),
OpenApiParameter(
"roller_coaster_type",
OpenApiTypes.STR,
@@ -2022,9 +1976,7 @@ class RideImageSettingsAPIView(APIView):
"properties": {
"rides": {
"type": "array",
"items": {
"$ref": "#/components/schemas/HybridRideSerializer"
},
"items": {"$ref": "#/components/schemas/HybridRideSerializer"},
},
"total_count": {"type": "integer"},
"strategy": {
@@ -2084,7 +2036,7 @@ class HybridRideAPIView(APIView):
data = smart_ride_loader.get_progressive_load(offset, filters)
except ValueError:
return Response(
{"error": "Invalid offset parameter"},
{"detail": "Invalid offset parameter"},
status=status.HTTP_400_BAD_REQUEST,
)
else:
@@ -2109,7 +2061,7 @@ class HybridRideAPIView(APIView):
except Exception as e:
logger.error(f"Error in HybridRideAPIView: {e}")
return Response(
{"error": "Internal server error"},
{"detail": "Internal server error"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@@ -2158,7 +2110,7 @@ class HybridRideAPIView(APIView):
for param in int_params:
value = query_params.get(param)
if value:
try:
try: # noqa: SIM105
filters[param] = int(value)
except ValueError:
pass # Skip invalid integer values
@@ -2175,7 +2127,7 @@ class HybridRideAPIView(APIView):
for param in float_params:
value = query_params.get(param)
if value:
try:
try: # noqa: SIM105
filters[param] = float(value)
except ValueError:
pass # Skip invalid float values
@@ -2408,7 +2360,7 @@ class RideFilterMetadataAPIView(APIView):
except Exception as e:
logger.error(f"Error in RideFilterMetadataAPIView: {e}")
return Response(
{"error": "Internal server error"},
{"detail": "Internal server error"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
@@ -2417,18 +2369,18 @@ class RideFilterMetadataAPIView(APIView):
# Reuse the same filter extraction logic
view = HybridRideAPIView()
return view._extract_filters(query_params)
# === MANUFACTURER & DESIGNER LISTS ===
class BaseCompanyListAPIView(APIView):
permission_classes = [permissions.AllowAny]
role = None
def get(self, request: Request) -> Response:
if not MODELS_AVAILABLE:
return Response(
{"detail": "Models not available"},
status=status.HTTP_501_NOT_IMPLEMENTED
)
return Response({"detail": "Models not available"}, status=status.HTTP_501_NOT_IMPLEMENTED)
companies = (
Company.objects.filter(roles__contains=[self.role])
@@ -2448,10 +2400,8 @@ class BaseCompanyListAPIView(APIView):
for c in companies
]
return Response({
"results": data,
"count": len(data)
})
return Response({"results": data, "count": len(data)})
@extend_schema(
summary="List manufacturers",
@@ -2462,6 +2412,7 @@ class BaseCompanyListAPIView(APIView):
class ManufacturerListAPIView(BaseCompanyListAPIView):
role = "MANUFACTURER"
@extend_schema(
summary="List designers",
description="List all companies with DESIGNER role.",