mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 05:11:09 -05:00
Implement hybrid filtering strategy for parks and rides
- Added comprehensive documentation for hybrid filtering implementation, including architecture, API endpoints, performance characteristics, and usage examples. - Developed a hybrid pagination and client-side filtering recommendation, detailing server-side responsibilities and client-side logic. - Created a test script for hybrid filtering endpoints, covering various test cases including basic filtering, search functionality, pagination, and edge cases.
This commit is contained in:
@@ -13,16 +13,19 @@ Notes:
|
||||
are not present, they return a clear 501 response explaining what to wire up.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, Dict
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from rest_framework import status, permissions
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
from rest_framework.exceptions import NotFound
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
|
||||
# Reuse existing serializers where possible
|
||||
@@ -34,6 +37,13 @@ from apps.api.v1.serializers.rides import (
|
||||
RideImageSettingsInputSerializer,
|
||||
)
|
||||
|
||||
# Import hybrid filtering components
|
||||
from apps.api.v1.rides.serializers import HybridRideSerializer
|
||||
from apps.rides.services.hybrid_loader import SmartRideLoader
|
||||
|
||||
# Create smart loader instance
|
||||
smart_ride_loader = SmartRideLoader()
|
||||
|
||||
# Attempt to import model-level helpers; fall back gracefully if not present.
|
||||
try:
|
||||
from apps.rides.models import Ride, RideModel
|
||||
@@ -1332,4 +1342,354 @@ class RideImageSettingsAPIView(APIView):
|
||||
return Response(output_serializer.data)
|
||||
|
||||
|
||||
# --- Ride duplicate action --------------------------------------------------
|
||||
# --- Hybrid Filtering API Views --------------------------------------------
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary="Get rides with hybrid filtering",
|
||||
description="Retrieve rides with intelligent hybrid filtering strategy. Automatically chooses between client-side and server-side filtering based on data size.",
|
||||
parameters=[
|
||||
OpenApiParameter("category", OpenApiTypes.STR, description="Filter by ride category (comma-separated for multiple)"),
|
||||
OpenApiParameter("status", 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("manufacturer", OpenApiTypes.STR, description="Filter by manufacturer slug (comma-separated for multiple)"),
|
||||
OpenApiParameter("designer", OpenApiTypes.STR, description="Filter by designer slug (comma-separated for multiple)"),
|
||||
OpenApiParameter("ride_model", 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("height_requirement_min", OpenApiTypes.INT, description="Minimum height requirement in inches"),
|
||||
OpenApiParameter("height_requirement_max", 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("roller_coaster_type", OpenApiTypes.STR, description="Filter by roller coaster type (comma-separated for multiple)"),
|
||||
OpenApiParameter("track_material", OpenApiTypes.STR, description="Filter by track material (comma-separated for multiple)"),
|
||||
OpenApiParameter("launch_type", OpenApiTypes.STR, description="Filter by launch type (comma-separated for multiple)"),
|
||||
OpenApiParameter("height_ft_min", OpenApiTypes.NUMBER, description="Minimum roller coaster height in feet"),
|
||||
OpenApiParameter("height_ft_max", OpenApiTypes.NUMBER, description="Maximum roller coaster height in feet"),
|
||||
OpenApiParameter("speed_mph_min", OpenApiTypes.NUMBER, description="Minimum roller coaster speed in mph"),
|
||||
OpenApiParameter("speed_mph_max", OpenApiTypes.NUMBER, description="Maximum roller coaster speed in mph"),
|
||||
OpenApiParameter("inversions_min", OpenApiTypes.INT, description="Minimum number of inversions"),
|
||||
OpenApiParameter("inversions_max", OpenApiTypes.INT, description="Maximum number of inversions"),
|
||||
OpenApiParameter("has_inversions", OpenApiTypes.BOOL, description="Filter rides with inversions (true) or without (false)"),
|
||||
OpenApiParameter("search", OpenApiTypes.STR, description="Search query for ride names, descriptions, parks, and related data"),
|
||||
OpenApiParameter("offset", OpenApiTypes.INT, description="Offset for progressive loading (server-side pagination)"),
|
||||
],
|
||||
responses={
|
||||
200: {
|
||||
"description": "Rides data with hybrid filtering metadata",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"rides": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "#/components/schemas/HybridRideSerializer"}
|
||||
},
|
||||
"total_count": {"type": "integer"},
|
||||
"strategy": {
|
||||
"type": "string",
|
||||
"enum": ["client_side", "server_side"],
|
||||
"description": "Filtering strategy used"
|
||||
},
|
||||
"has_more": {
|
||||
"type": "boolean",
|
||||
"description": "Whether more data is available for progressive loading"
|
||||
},
|
||||
"next_offset": {
|
||||
"type": "integer",
|
||||
"nullable": True,
|
||||
"description": "Next offset for progressive loading"
|
||||
},
|
||||
"filter_metadata": {
|
||||
"type": "object",
|
||||
"description": "Available filter options and ranges"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
tags=["Rides"],
|
||||
)
|
||||
)
|
||||
class HybridRideAPIView(APIView):
|
||||
"""
|
||||
Hybrid Ride API View with intelligent filtering strategy.
|
||||
|
||||
Automatically chooses between client-side and server-side filtering
|
||||
based on data size and complexity. Provides progressive loading
|
||||
for large datasets and complete data for smaller sets.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
"""Get rides with hybrid filtering strategy."""
|
||||
try:
|
||||
# Extract filters from query parameters
|
||||
filters = self._extract_filters(request.query_params)
|
||||
|
||||
# Check if this is a progressive load request
|
||||
offset = request.query_params.get('offset')
|
||||
if offset is not None:
|
||||
try:
|
||||
offset = int(offset)
|
||||
# Get progressive load data
|
||||
data = smart_ride_loader.get_progressive_load(offset, filters)
|
||||
except ValueError:
|
||||
return Response(
|
||||
{"error": "Invalid offset parameter"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
else:
|
||||
# Get initial load data
|
||||
data = smart_ride_loader.get_initial_load(filters)
|
||||
|
||||
# Prepare response (rides are already serialized by the service)
|
||||
response_data = {
|
||||
'rides': data['rides'],
|
||||
'total_count': data['total_count'],
|
||||
'strategy': data.get('strategy', 'server_side'),
|
||||
'has_more': data.get('has_more', False),
|
||||
'next_offset': data.get('next_offset'),
|
||||
}
|
||||
|
||||
# Include filter metadata for initial loads
|
||||
if 'filter_metadata' in data:
|
||||
response_data['filter_metadata'] = data['filter_metadata']
|
||||
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in HybridRideAPIView: {e}")
|
||||
return Response(
|
||||
{"error": "Internal server error"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
def _extract_filters(self, query_params):
|
||||
"""Extract and parse filters from query parameters."""
|
||||
filters = {}
|
||||
|
||||
# Handle comma-separated list parameters
|
||||
list_params = ['category', 'status', 'manufacturer', 'designer', 'ride_model', 'roller_coaster_type', 'track_material', 'launch_type']
|
||||
for param in list_params:
|
||||
value = query_params.get(param)
|
||||
if value:
|
||||
filters[param] = [v.strip() for v in value.split(',') if v.strip()]
|
||||
|
||||
# Handle single value parameters
|
||||
single_params = ['park_slug', 'park_id']
|
||||
for param in single_params:
|
||||
value = query_params.get(param)
|
||||
if value:
|
||||
if param == 'park_id':
|
||||
try:
|
||||
filters[param] = int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
filters[param] = value
|
||||
|
||||
# Handle integer parameters
|
||||
int_params = [
|
||||
'opening_year_min', 'opening_year_max',
|
||||
'height_requirement_min', 'height_requirement_max',
|
||||
'capacity_min', 'capacity_max',
|
||||
'inversions_min', 'inversions_max'
|
||||
]
|
||||
for param in int_params:
|
||||
value = query_params.get(param)
|
||||
if value:
|
||||
try:
|
||||
filters[param] = int(value)
|
||||
except ValueError:
|
||||
pass # Skip invalid integer values
|
||||
|
||||
# Handle float parameters
|
||||
float_params = ['rating_min', 'rating_max', 'height_ft_min', 'height_ft_max', 'speed_mph_min', 'speed_mph_max']
|
||||
for param in float_params:
|
||||
value = query_params.get(param)
|
||||
if value:
|
||||
try:
|
||||
filters[param] = float(value)
|
||||
except ValueError:
|
||||
pass # Skip invalid float values
|
||||
|
||||
# Handle boolean parameters
|
||||
has_inversions = query_params.get('has_inversions')
|
||||
if has_inversions is not None:
|
||||
if has_inversions.lower() in ['true', '1', 'yes']:
|
||||
filters['has_inversions'] = True
|
||||
elif has_inversions.lower() in ['false', '0', 'no']:
|
||||
filters['has_inversions'] = False
|
||||
|
||||
# Handle search parameter
|
||||
search = query_params.get('search')
|
||||
if search:
|
||||
filters['search'] = search.strip()
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary="Get ride filter metadata",
|
||||
description="Get available filter options and ranges for rides filtering.",
|
||||
parameters=[
|
||||
OpenApiParameter("scoped", OpenApiTypes.BOOL, description="Whether to scope metadata to current filters"),
|
||||
],
|
||||
responses={
|
||||
200: {
|
||||
"description": "Filter metadata",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"categorical": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"categories": {"type": "array", "items": {"type": "string"}},
|
||||
"statuses": {"type": "array", "items": {"type": "string"}},
|
||||
"roller_coaster_types": {"type": "array", "items": {"type": "string"}},
|
||||
"track_materials": {"type": "array", "items": {"type": "string"}},
|
||||
"launch_types": {"type": "array", "items": {"type": "string"}},
|
||||
"parks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"slug": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"manufacturers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"slug": {"type": "string"}
|
||||
}
|
||||
}
|
||||
},
|
||||
"designers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"slug": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ranges": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"opening_year": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min": {"type": "integer", "nullable": True},
|
||||
"max": {"type": "integer", "nullable": True}
|
||||
}
|
||||
},
|
||||
"rating": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min": {"type": "number", "nullable": True},
|
||||
"max": {"type": "number", "nullable": True}
|
||||
}
|
||||
},
|
||||
"height_requirement": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min": {"type": "integer", "nullable": True},
|
||||
"max": {"type": "integer", "nullable": True}
|
||||
}
|
||||
},
|
||||
"capacity": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min": {"type": "integer", "nullable": True},
|
||||
"max": {"type": "integer", "nullable": True}
|
||||
}
|
||||
},
|
||||
"height_ft": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min": {"type": "number", "nullable": True},
|
||||
"max": {"type": "number", "nullable": True}
|
||||
}
|
||||
},
|
||||
"speed_mph": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min": {"type": "number", "nullable": True},
|
||||
"max": {"type": "number", "nullable": True}
|
||||
}
|
||||
},
|
||||
"inversions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"min": {"type": "integer", "nullable": True},
|
||||
"max": {"type": "integer", "nullable": True}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"total_count": {"type": "integer"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
tags=["Rides"],
|
||||
)
|
||||
)
|
||||
class RideFilterMetadataAPIView(APIView):
|
||||
"""
|
||||
API view for getting ride filter metadata.
|
||||
|
||||
Provides information about available filter options and ranges
|
||||
to help build dynamic filter interfaces.
|
||||
"""
|
||||
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get(self, request):
|
||||
"""Get ride filter metadata."""
|
||||
try:
|
||||
# Check if metadata should be scoped to current filters
|
||||
scoped = request.query_params.get('scoped', '').lower() == 'true'
|
||||
filters = None
|
||||
|
||||
if scoped:
|
||||
filters = self._extract_filters(request.query_params)
|
||||
|
||||
# Get filter metadata
|
||||
metadata = smart_ride_loader.get_filter_metadata(filters)
|
||||
|
||||
return Response(metadata, status=status.HTTP_200_OK)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in RideFilterMetadataAPIView: {e}")
|
||||
return Response(
|
||||
{"error": "Internal server error"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
def _extract_filters(self, query_params):
|
||||
"""Extract and parse filters from query parameters."""
|
||||
# Reuse the same filter extraction logic
|
||||
view = HybridRideAPIView()
|
||||
return view._extract_filters(query_params)
|
||||
|
||||
Reference in New Issue
Block a user