feat: Implement MFA authentication, add ride statistics model, and update various services, APIs, and tests across the application.

This commit is contained in:
pacnpal
2025-12-28 17:32:53 -05:00
parent aa56c46c27
commit c95f99ca10
452 changed files with 7948 additions and 6073 deletions

View File

@@ -23,12 +23,13 @@ Caching Strategy:
- RideSearchSuggestionsAPIView.get: 5 minutes (300s) - suggestions should be fresh
"""
import contextlib
import logging
from typing import Any
from django.db import models
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
from rest_framework import permissions, status
from rest_framework.exceptions import NotFound
from rest_framework.pagination import PageNumberPagination
@@ -53,9 +54,9 @@ smart_ride_loader = SmartRideLoader()
# Attempt to import model-level helpers; fall back gracefully if not present.
try:
from apps.parks.models import Company, Park
from apps.rides.models import Ride, RideModel
from apps.rides.models.rides import RollerCoasterStats
from apps.parks.models import Park, Company
MODELS_AVAILABLE = True
except Exception:
@@ -370,10 +371,8 @@ class RideListCreateAPIView(APIView):
park_id = params.get("park_id")
if park_id:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(park_id=int(park_id))
except (ValueError, TypeError):
pass
return qs
@@ -393,10 +392,8 @@ class RideListCreateAPIView(APIView):
"""Apply manufacturer and designer filtering."""
manufacturer_id = params.get("manufacturer_id")
if manufacturer_id:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(manufacturer_id=int(manufacturer_id))
except (ValueError, TypeError):
pass
manufacturer_slug = params.get("manufacturer_slug")
if manufacturer_slug:
@@ -404,10 +401,8 @@ class RideListCreateAPIView(APIView):
designer_id = params.get("designer_id")
if designer_id:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(designer_id=int(designer_id))
except (ValueError, TypeError):
pass
designer_slug = params.get("designer_slug")
if designer_slug:
@@ -419,10 +414,8 @@ class RideListCreateAPIView(APIView):
"""Apply ride model filtering."""
ride_model_id = params.get("ride_model_id")
if ride_model_id:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(ride_model_id=int(ride_model_id))
except (ValueError, TypeError):
pass
ride_model_slug = params.get("ride_model_slug")
manufacturer_slug_for_model = params.get("manufacturer_slug")
@@ -438,17 +431,13 @@ class RideListCreateAPIView(APIView):
"""Apply rating-based filtering."""
min_rating = params.get("min_rating")
if min_rating:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(average_rating__gte=float(min_rating))
except (ValueError, TypeError):
pass
max_rating = params.get("max_rating")
if max_rating:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(average_rating__lte=float(max_rating))
except (ValueError, TypeError):
pass
return qs
@@ -456,17 +445,13 @@ class RideListCreateAPIView(APIView):
"""Apply height requirement filtering."""
min_height_req = params.get("min_height_requirement")
if min_height_req:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(min_height_in__gte=int(min_height_req))
except (ValueError, TypeError):
pass
max_height_req = params.get("max_height_requirement")
if max_height_req:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(max_height_in__lte=int(max_height_req))
except (ValueError, TypeError):
pass
return qs
@@ -474,17 +459,13 @@ class RideListCreateAPIView(APIView):
"""Apply capacity filtering."""
min_capacity = params.get("min_capacity")
if min_capacity:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(capacity_per_hour__gte=int(min_capacity))
except (ValueError, TypeError):
pass
max_capacity = params.get("max_capacity")
if max_capacity:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(capacity_per_hour__lte=int(max_capacity))
except (ValueError, TypeError):
pass
return qs
@@ -492,24 +473,18 @@ class RideListCreateAPIView(APIView):
"""Apply opening year filtering."""
opening_year = params.get("opening_year")
if opening_year:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(opening_date__year=int(opening_year))
except (ValueError, TypeError):
pass
min_opening_year = params.get("min_opening_year")
if min_opening_year:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(opening_date__year__gte=int(min_opening_year))
except (ValueError, TypeError):
pass
max_opening_year = params.get("max_opening_year")
if max_opening_year:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(opening_date__year__lte=int(max_opening_year))
except (ValueError, TypeError):
pass
return qs
@@ -530,47 +505,35 @@ class RideListCreateAPIView(APIView):
# Height filters
min_height_ft = params.get("min_height_ft")
if min_height_ft:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(coaster_stats__height_ft__gte=float(min_height_ft))
except (ValueError, TypeError):
pass
max_height_ft = params.get("max_height_ft")
if max_height_ft:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(coaster_stats__height_ft__lte=float(max_height_ft))
except (ValueError, TypeError):
pass
# Speed filters
min_speed_mph = params.get("min_speed_mph")
if min_speed_mph:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(coaster_stats__speed_mph__gte=float(min_speed_mph))
except (ValueError, TypeError):
pass
max_speed_mph = params.get("max_speed_mph")
if max_speed_mph:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(coaster_stats__speed_mph__lte=float(max_speed_mph))
except (ValueError, TypeError):
pass
# Inversion filters
min_inversions = params.get("min_inversions")
if min_inversions:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(coaster_stats__inversions__gte=int(min_inversions))
except (ValueError, TypeError):
pass
max_inversions = params.get("max_inversions")
if max_inversions:
try:
with contextlib.suppress(ValueError, TypeError):
qs = qs.filter(coaster_stats__inversions__lte=int(max_inversions))
except (ValueError, TypeError):
pass
has_inversions = params.get("has_inversions")
if has_inversions is not None:
@@ -2176,10 +2139,8 @@ class HybridRideAPIView(APIView):
value = query_params.get(param)
if value:
if param == "park_id":
try:
with contextlib.suppress(ValueError):
filters[param] = int(value)
except ValueError:
pass
else:
filters[param] = value
@@ -2461,14 +2422,14 @@ class RideFilterMetadataAPIView(APIView):
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
)
companies = (
Company.objects.filter(roles__contains=[self.role])
.annotate(ride_count=Count("manufactured_rides" if self.role == "MANUFACTURER" else "designed_rides"))
@@ -2486,7 +2447,7 @@ class BaseCompanyListAPIView(APIView):
}
for c in companies
]
return Response({
"results": data,
"count": len(data)