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

@@ -0,0 +1,12 @@
"""URL routes for Company CRUD API."""
from django.urls import path
from .company_views import CompanyDetailAPIView, CompanyListCreateAPIView
app_name = "api_v1_companies"
urlpatterns = [
path("", CompanyListCreateAPIView.as_view(), name="company-list-create"),
path("<int:pk>/", CompanyDetailAPIView.as_view(), name="company-detail"),
]

View File

@@ -0,0 +1,167 @@
"""
Company API views for ThrillWiki API v1.
This module implements CRUD endpoints for company management:
- List / Create: GET /companies/ POST /companies/
- Retrieve / Update / Delete: GET /companies/{id}/ PATCH/PUT/DELETE
"""
from django.db.models import Q
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework import permissions, status
from rest_framework.exceptions import NotFound
from rest_framework.pagination import PageNumberPagination
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.api.v1.serializers.companies import (
CompanyCreateInputSerializer,
CompanyDetailOutputSerializer,
CompanyUpdateInputSerializer,
)
try:
from apps.rides.models.company import Company
MODELS_AVAILABLE = True
except ImportError:
Company = None
MODELS_AVAILABLE = False
class StandardResultsSetPagination(PageNumberPagination):
page_size = 20
page_size_query_param = "page_size"
max_page_size = 100
class CompanyListCreateAPIView(APIView):
"""List and create companies."""
permission_classes = [permissions.AllowAny]
@extend_schema(
summary="List all companies",
description="List companies with optional search and role filtering.",
parameters=[
OpenApiParameter(name="search", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR),
OpenApiParameter(name="role", location=OpenApiParameter.QUERY, type=OpenApiTypes.STR),
OpenApiParameter(name="page", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT),
OpenApiParameter(name="page_size", location=OpenApiParameter.QUERY, type=OpenApiTypes.INT),
],
responses={200: CompanyDetailOutputSerializer(many=True)},
tags=["Companies"],
)
def get(self, request: Request) -> Response:
if not MODELS_AVAILABLE:
return Response(
{"detail": "Company models not available"},
status=status.HTTP_501_NOT_IMPLEMENTED,
)
qs = Company.objects.all().order_by("name")
# Search filter
search = request.query_params.get("search", "")
if search:
qs = qs.filter(
Q(name__icontains=search) | Q(description__icontains=search)
)
# Role filter
role = request.query_params.get("role", "")
if role:
qs = qs.filter(roles__contains=[role])
paginator = StandardResultsSetPagination()
page = paginator.paginate_queryset(qs, request)
serializer = CompanyDetailOutputSerializer(page, many=True)
return paginator.get_paginated_response(serializer.data)
@extend_schema(
summary="Create a new company",
description="Create a new company with the given details.",
request=CompanyCreateInputSerializer,
responses={201: CompanyDetailOutputSerializer()},
tags=["Companies"],
)
def post(self, request: Request) -> Response:
if not MODELS_AVAILABLE:
return Response(
{"detail": "Company models not available"},
status=status.HTTP_501_NOT_IMPLEMENTED,
)
serializer_in = CompanyCreateInputSerializer(data=request.data)
serializer_in.is_valid(raise_exception=True)
validated = serializer_in.validated_data
company = Company.objects.create(
name=validated["name"],
roles=validated["roles"],
description=validated.get("description", ""),
website=validated.get("website", ""),
founded_date=validated.get("founded_date"),
)
serializer = CompanyDetailOutputSerializer(company)
return Response(serializer.data, status=status.HTTP_201_CREATED)
class CompanyDetailAPIView(APIView):
"""Retrieve, update, and delete a company."""
permission_classes = [permissions.AllowAny]
def _get_company_or_404(self, pk: int) -> "Company":
if not MODELS_AVAILABLE:
raise NotFound("Company models not available")
try:
return Company.objects.get(pk=pk)
except Company.DoesNotExist:
raise NotFound("Company not found")
@extend_schema(
summary="Retrieve a company",
description="Get detailed information about a specific company.",
responses={200: CompanyDetailOutputSerializer()},
tags=["Companies"],
)
def get(self, request: Request, pk: int) -> Response:
company = self._get_company_or_404(pk)
serializer = CompanyDetailOutputSerializer(company)
return Response(serializer.data)
@extend_schema(
summary="Update a company",
description="Update a company (partial update supported).",
request=CompanyUpdateInputSerializer,
responses={200: CompanyDetailOutputSerializer()},
tags=["Companies"],
)
def patch(self, request: Request, pk: int) -> Response:
company = self._get_company_or_404(pk)
serializer_in = CompanyUpdateInputSerializer(data=request.data, partial=True)
serializer_in.is_valid(raise_exception=True)
for field, value in serializer_in.validated_data.items():
setattr(company, field, value)
company.save()
serializer = CompanyDetailOutputSerializer(company)
return Response(serializer.data)
def put(self, request: Request, pk: int) -> Response:
return self.patch(request, pk)
@extend_schema(
summary="Delete a company",
description="Delete a company.",
responses={204: None},
tags=["Companies"],
)
def delete(self, request: Request, pk: int) -> Response:
company = self._get_company_or_404(pk)
company.delete()
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@@ -11,17 +11,17 @@ This file exposes comprehensive endpoints for ride model management:
from django.urls import path
from .views import (
RideModelListCreateAPIView,
RideModelDetailAPIView,
RideModelSearchAPIView,
RideModelFilterOptionsAPIView,
RideModelStatsAPIView,
RideModelVariantListCreateAPIView,
RideModelVariantDetailAPIView,
RideModelTechnicalSpecListCreateAPIView,
RideModelTechnicalSpecDetailAPIView,
RideModelPhotoListCreateAPIView,
RideModelListCreateAPIView,
RideModelPhotoDetailAPIView,
RideModelPhotoListCreateAPIView,
RideModelSearchAPIView,
RideModelStatsAPIView,
RideModelTechnicalSpecDetailAPIView,
RideModelTechnicalSpecListCreateAPIView,
RideModelVariantDetailAPIView,
RideModelVariantListCreateAPIView,
)
app_name = "api_v1_ride_models"

View File

@@ -12,40 +12,40 @@ This module implements comprehensive endpoints for ride model management:
- Photos: CRUD operations for ride model photos
"""
from typing import Any
from datetime import timedelta
from typing import Any
from rest_framework import status, permissions
from rest_framework.views import APIView
from django.db.models import Count, Q
from django.utils import timezone
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework import permissions, status
from rest_framework.exceptions import NotFound, ValidationError
from rest_framework.pagination import PageNumberPagination
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.pagination import PageNumberPagination
from rest_framework.exceptions import NotFound, ValidationError
from drf_spectacular.utils import extend_schema, OpenApiParameter
from drf_spectacular.types import OpenApiTypes
from django.db.models import Q, Count
from django.utils import timezone
from rest_framework.views import APIView
# Import serializers
from apps.api.v1.serializers.ride_models import (
RideModelListOutputSerializer,
RideModelDetailOutputSerializer,
RideModelCreateInputSerializer,
RideModelUpdateInputSerializer,
RideModelDetailOutputSerializer,
RideModelFilterInputSerializer,
RideModelVariantOutputSerializer,
RideModelVariantCreateInputSerializer,
RideModelVariantUpdateInputSerializer,
RideModelListOutputSerializer,
RideModelStatsOutputSerializer,
RideModelUpdateInputSerializer,
RideModelVariantCreateInputSerializer,
RideModelVariantOutputSerializer,
RideModelVariantUpdateInputSerializer,
)
# Attempt to import models; fall back gracefully if not present
try:
from apps.rides.models import (
RideModel,
RideModelVariant,
RideModelPhoto,
RideModelTechnicalSpec,
RideModelVariant,
)
from apps.rides.models.company import Company
@@ -54,12 +54,12 @@ except ImportError:
try:
# Try alternative import path
from apps.rides.models.rides import (
Company,
RideModel,
RideModelVariant,
RideModelPhoto,
RideModelTechnicalSpec,
RideModelVariant,
)
from apps.rides.models.rides import Company
MODELS_AVAILABLE = True
except ImportError:
@@ -486,14 +486,14 @@ class RideModelFilterOptionsAPIView(APIView):
"""Return filter options for ride models with Rich Choice Objects metadata."""
# Import Rich Choice registry
from apps.core.choices.registry import get_choices
if not MODELS_AVAILABLE:
# Use Rich Choice Objects for fallback options
try:
# Get rich choice objects from registry
categories = get_choices('categories', 'rides')
target_markets = get_choices('target_markets', 'rides')
# Convert Rich Choice Objects to frontend format with metadata
categories_data = [
{
@@ -507,7 +507,7 @@ class RideModelFilterOptionsAPIView(APIView):
}
for choice in categories
]
target_markets_data = [
{
"value": choice.value,
@@ -520,7 +520,7 @@ class RideModelFilterOptionsAPIView(APIView):
}
for choice in target_markets
]
except Exception:
# Ultimate fallback with basic structure
categories_data = [
@@ -538,7 +538,7 @@ class RideModelFilterOptionsAPIView(APIView):
{"value": "KIDDIE", "label": "Kiddie", "description": "Designed for young children", "color": "pink", "icon": "kiddie", "css_class": "bg-pink-100 text-pink-800", "sort_order": 4},
{"value": "ALL_AGES", "label": "All Ages", "description": "Enjoyable for all age groups", "color": "blue", "icon": "all-ages", "css_class": "bg-blue-100 text-blue-800", "sort_order": 5},
]
return Response({
"categories": categories_data,
"target_markets": target_markets_data,
@@ -557,11 +557,11 @@ class RideModelFilterOptionsAPIView(APIView):
# Get static choice definitions from Rich Choice Objects (primary source)
# Get dynamic data from database queries
# Get rich choice objects from registry
categories = get_choices('categories', 'rides')
target_markets = get_choices('target_markets', 'rides')
# Convert Rich Choice Objects to frontend format with metadata
categories_data = [
{
@@ -575,7 +575,7 @@ class RideModelFilterOptionsAPIView(APIView):
}
for choice in categories
]
target_markets_data = [
{
"value": choice.value,

View File

@@ -5,23 +5,25 @@ This module contains ride photo ViewSet following the parks pattern for domain c
Enhanced from centralized media API to provide domain-specific ride photo management.
"""
from .serializers import (
RidePhotoOutputSerializer,
RidePhotoCreateInputSerializer,
RidePhotoUpdateInputSerializer,
RidePhotoListOutputSerializer,
RidePhotoApprovalInputSerializer,
RidePhotoStatsOutputSerializer,
)
from typing import TYPE_CHECKING
from .serializers import (
RidePhotoApprovalInputSerializer,
RidePhotoCreateInputSerializer,
RidePhotoListOutputSerializer,
RidePhotoOutputSerializer,
RidePhotoStatsOutputSerializer,
RidePhotoUpdateInputSerializer,
)
if TYPE_CHECKING:
pass
import logging
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
from drf_spectacular.utils import extend_schema_view, extend_schema
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
@@ -29,9 +31,8 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from apps.rides.models import RidePhoto, Ride
from apps.rides.models import Ride, RidePhoto
from apps.rides.services.media_service import RideMediaService
from django.contrib.auth import get_user_model
UserModel = get_user_model()
@@ -460,9 +461,9 @@ class RidePhotoViewSet(ModelViewSet):
try:
# Import CloudflareImage model and service
from django.utils import timezone
from django_cloudflareimages_toolkit.models import CloudflareImage
from django_cloudflareimages_toolkit.services import CloudflareImagesService
from django.utils import timezone
# Always fetch the latest image data from Cloudflare API
try:

View File

@@ -4,12 +4,13 @@ Ride media serializers for ThrillWiki API v1.
This module contains serializers for ride-specific media functionality.
"""
from rest_framework import serializers
from drf_spectacular.utils import (
OpenApiExample,
extend_schema_field,
extend_schema_serializer,
OpenApiExample,
)
from rest_framework import serializers
from apps.rides.models import Ride, RidePhoto
@@ -267,33 +268,33 @@ class HybridRideSerializer(serializers.ModelSerializer):
Enhanced serializer for hybrid filtering strategy.
Includes all filterable fields for client-side filtering.
"""
# Park fields
park_name = serializers.CharField(source="park.name", read_only=True)
park_slug = serializers.CharField(source="park.slug", read_only=True)
# Park location fields
park_city = serializers.SerializerMethodField()
park_state = serializers.SerializerMethodField()
park_country = serializers.SerializerMethodField()
# Park area fields
park_area_name = serializers.CharField(source="park_area.name", read_only=True, allow_null=True)
park_area_slug = serializers.CharField(source="park_area.slug", read_only=True, allow_null=True)
# Company fields
manufacturer_name = serializers.CharField(source="manufacturer.name", read_only=True, allow_null=True)
manufacturer_slug = serializers.CharField(source="manufacturer.slug", read_only=True, allow_null=True)
designer_name = serializers.CharField(source="designer.name", read_only=True, allow_null=True)
designer_slug = serializers.CharField(source="designer.slug", read_only=True, allow_null=True)
# Ride model fields
ride_model_name = serializers.CharField(source="ride_model.name", read_only=True, allow_null=True)
ride_model_slug = serializers.CharField(source="ride_model.slug", read_only=True, allow_null=True)
ride_model_category = serializers.CharField(source="ride_model.category", read_only=True, allow_null=True)
ride_model_manufacturer_name = serializers.CharField(source="ride_model.manufacturer.name", read_only=True, allow_null=True)
ride_model_manufacturer_slug = serializers.CharField(source="ride_model.manufacturer.slug", read_only=True, allow_null=True)
# Roller coaster stats fields
coaster_height_ft = serializers.SerializerMethodField()
coaster_length_ft = serializers.SerializerMethodField()
@@ -309,15 +310,15 @@ class HybridRideSerializer(serializers.ModelSerializer):
coaster_trains_count = serializers.SerializerMethodField()
coaster_cars_per_train = serializers.SerializerMethodField()
coaster_seats_per_car = serializers.SerializerMethodField()
# Image URLs for display
banner_image_url = serializers.SerializerMethodField()
card_image_url = serializers.SerializerMethodField()
# Computed fields for filtering
opening_year = serializers.IntegerField(read_only=True)
search_text = serializers.CharField(read_only=True)
@extend_schema_field(serializers.CharField(allow_null=True))
def get_park_city(self, obj):
"""Get city from park location."""
@@ -327,7 +328,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except AttributeError:
return None
@extend_schema_field(serializers.CharField(allow_null=True))
def get_park_state(self, obj):
"""Get state from park location."""
@@ -337,7 +338,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except AttributeError:
return None
@extend_schema_field(serializers.CharField(allow_null=True))
def get_park_country(self, obj):
"""Get country from park location."""
@@ -347,7 +348,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except AttributeError:
return None
@extend_schema_field(serializers.FloatField(allow_null=True))
def get_coaster_height_ft(self, obj):
"""Get roller coaster height."""
@@ -357,7 +358,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except (AttributeError, TypeError):
return None
@extend_schema_field(serializers.FloatField(allow_null=True))
def get_coaster_length_ft(self, obj):
"""Get roller coaster length."""
@@ -367,7 +368,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except (AttributeError, TypeError):
return None
@extend_schema_field(serializers.FloatField(allow_null=True))
def get_coaster_speed_mph(self, obj):
"""Get roller coaster speed."""
@@ -377,7 +378,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except (AttributeError, TypeError):
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_coaster_inversions(self, obj):
"""Get roller coaster inversions."""
@@ -387,7 +388,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_coaster_ride_time_seconds(self, obj):
"""Get roller coaster ride time."""
@@ -397,7 +398,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except AttributeError:
return None
@extend_schema_field(serializers.CharField(allow_null=True))
def get_coaster_track_type(self, obj):
"""Get roller coaster track type."""
@@ -407,7 +408,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except AttributeError:
return None
@extend_schema_field(serializers.CharField(allow_null=True))
def get_coaster_track_material(self, obj):
"""Get roller coaster track material."""
@@ -417,7 +418,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except AttributeError:
return None
@extend_schema_field(serializers.CharField(allow_null=True))
def get_coaster_roller_coaster_type(self, obj):
"""Get roller coaster type."""
@@ -427,7 +428,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except AttributeError:
return None
@extend_schema_field(serializers.FloatField(allow_null=True))
def get_coaster_max_drop_height_ft(self, obj):
"""Get roller coaster max drop height."""
@@ -437,7 +438,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except (AttributeError, TypeError):
return None
@extend_schema_field(serializers.CharField(allow_null=True))
def get_coaster_propulsion_system(self, obj):
"""Get roller coaster propulsion system."""
@@ -447,7 +448,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except AttributeError:
return None
@extend_schema_field(serializers.CharField(allow_null=True))
def get_coaster_train_style(self, obj):
"""Get roller coaster train style."""
@@ -457,7 +458,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_coaster_trains_count(self, obj):
"""Get roller coaster trains count."""
@@ -467,7 +468,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_coaster_cars_per_train(self, obj):
"""Get roller coaster cars per train."""
@@ -477,7 +478,7 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except AttributeError:
return None
@extend_schema_field(serializers.IntegerField(allow_null=True))
def get_coaster_seats_per_car(self, obj):
"""Get roller coaster seats per car."""
@@ -487,14 +488,14 @@ class HybridRideSerializer(serializers.ModelSerializer):
return None
except AttributeError:
return None
@extend_schema_field(serializers.URLField(allow_null=True))
def get_banner_image_url(self, obj):
"""Get banner image URL."""
if obj.banner_image and obj.banner_image.image:
return obj.banner_image.image.url
return None
@extend_schema_field(serializers.URLField(allow_null=True))
def get_card_image_url(self, obj):
"""Get card image URL."""
@@ -513,44 +514,44 @@ class HybridRideSerializer(serializers.ModelSerializer):
"category",
"status",
"post_closing_status",
# Dates and computed fields
"opening_date",
"closing_date",
"status_since",
"opening_year",
# Park fields
"park_name",
"park_slug",
"park_city",
"park_state",
"park_country",
# Park area fields
"park_area_name",
"park_area_slug",
# Company fields
"manufacturer_name",
"manufacturer_slug",
"designer_name",
"designer_slug",
# Ride model fields
"ride_model_name",
"ride_model_slug",
"ride_model_category",
"ride_model_manufacturer_name",
"ride_model_manufacturer_slug",
# Ride specifications
"min_height_in",
"max_height_in",
"capacity_per_hour",
"ride_duration_seconds",
"average_rating",
# Roller coaster stats
"coaster_height_ft",
"coaster_length_ft",
@@ -566,18 +567,18 @@ class HybridRideSerializer(serializers.ModelSerializer):
"coaster_trains_count",
"coaster_cars_per_train",
"coaster_seats_per_car",
# Images
"banner_image_url",
"card_image_url",
# URLs
"url",
"park_url",
# Computed fields for filtering
"search_text",
# Metadata
"created_at",
"updated_at",

View File

@@ -8,23 +8,23 @@ actions (bulk, publish, export, import, recommendations) should be added
to the views module when business logic is available.
"""
from django.urls import path, include
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .photo_views import RidePhotoViewSet
from .views import (
RideListCreateAPIView,
RideDetailAPIView,
FilterOptionsAPIView,
CompanySearchAPIView,
DesignerListAPIView,
FilterOptionsAPIView,
HybridRideAPIView,
ManufacturerListAPIView,
RideDetailAPIView,
RideFilterMetadataAPIView,
RideImageSettingsAPIView,
RideListCreateAPIView,
RideModelSearchAPIView,
RideSearchSuggestionsAPIView,
RideImageSettingsAPIView,
HybridRideAPIView,
RideFilterMetadataAPIView,
ManufacturerListAPIView,
DesignerListAPIView,
)
from .photo_views import RidePhotoViewSet
# Create router for nested photo endpoints
router = DefaultRouter()
@@ -35,11 +35,11 @@ app_name = "api_v1_rides"
urlpatterns = [
# Core list/create endpoints
path("", RideListCreateAPIView.as_view(), name="ride-list-create"),
# Hybrid filtering endpoints
path("hybrid/", HybridRideAPIView.as_view(), name="ride-hybrid-filtering"),
path("hybrid/filter-metadata/", RideFilterMetadataAPIView.as_view(), name="ride-hybrid-filter-metadata"),
# Filter options
path("filter-options/", FilterOptionsAPIView.as_view(), name="ride-filter-options"),
# Autocomplete / suggestion endpoints

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)