mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-31 05:47:04 -05:00
feat: Implement MFA authentication, add ride statistics model, and update various services, APIs, and tests across the application.
This commit is contained in:
12
backend/apps/api/v1/rides/company_urls.py
Normal file
12
backend/apps/api/v1/rides/company_urls.py
Normal 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"),
|
||||
]
|
||||
167
backend/apps/api/v1/rides/company_views.py
Normal file
167
backend/apps/api/v1/rides/company_views.py
Normal 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)
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user