mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-01-01 22:07:03 -05:00
feat: Implement initial schema and add various API, service, and management command enhancements across the application.
This commit is contained in:
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user