mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-01-02 00:07:02 -05:00
763 lines
29 KiB
Python
763 lines
29 KiB
Python
"""
|
|
Smart Ride Loader for Hybrid Filtering Strategy
|
|
|
|
This service implements intelligent data loading for rides, automatically choosing
|
|
between client-side and server-side filtering based on data size and complexity.
|
|
|
|
Key Features:
|
|
- Automatic strategy selection (≤200 records = client-side, >200 = server-side)
|
|
- Progressive loading for large datasets
|
|
- Intelligent caching with automatic invalidation
|
|
- Comprehensive filter metadata generation
|
|
- Optimized database queries with strategic prefetching
|
|
|
|
Architecture:
|
|
- Client-side: Load all data once, filter in frontend
|
|
- Server-side: Apply filters in database, paginate results
|
|
- Hybrid: Combine both approaches based on data characteristics
|
|
"""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from django.core.cache import cache
|
|
from django.db import models
|
|
from django.db.models import Max, Min, Q
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SmartRideLoader:
|
|
"""
|
|
Intelligent ride data loader that chooses optimal filtering strategy.
|
|
|
|
Strategy Selection:
|
|
- ≤200 total records: Client-side filtering (load all data)
|
|
- >200 total records: Server-side filtering (database filtering + pagination)
|
|
|
|
Features:
|
|
- Progressive loading for large datasets
|
|
- 5-minute intelligent caching
|
|
- Comprehensive filter metadata
|
|
- Optimized queries with prefetch_related
|
|
"""
|
|
|
|
# Configuration constants
|
|
INITIAL_LOAD_SIZE = 50
|
|
PROGRESSIVE_LOAD_SIZE = 25
|
|
MAX_CLIENT_SIDE_RECORDS = 200
|
|
CACHE_TIMEOUT = 300 # 5 minutes
|
|
|
|
def __init__(self):
|
|
self.cache_prefix = "rides_hybrid_"
|
|
|
|
def get_initial_load(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
"""
|
|
Get initial data load with automatic strategy selection.
|
|
|
|
Args:
|
|
filters: Optional filter parameters
|
|
|
|
Returns:
|
|
Dict containing:
|
|
- strategy: 'client_side' or 'server_side'
|
|
- data: List of ride records
|
|
- total_count: Total number of records
|
|
- has_more: Whether more data is available
|
|
- filter_metadata: Available filter options
|
|
"""
|
|
|
|
# Get total count for strategy decision
|
|
total_count = self._get_total_count(filters)
|
|
|
|
# Choose strategy based on total count
|
|
if total_count <= self.MAX_CLIENT_SIDE_RECORDS:
|
|
return self._get_client_side_data(filters, total_count)
|
|
else:
|
|
return self._get_server_side_data(filters, total_count)
|
|
|
|
def get_progressive_load(self, offset: int, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
"""
|
|
Get additional data for progressive loading (server-side strategy only).
|
|
|
|
Args:
|
|
offset: Number of records to skip
|
|
filters: Filter parameters
|
|
|
|
Returns:
|
|
Dict containing additional ride records
|
|
"""
|
|
|
|
# Build queryset with filters
|
|
queryset = self._build_filtered_queryset(filters)
|
|
|
|
# Get total count for this filtered set
|
|
total_count = queryset.count()
|
|
|
|
# Get progressive batch
|
|
rides = list(queryset[offset : offset + self.PROGRESSIVE_LOAD_SIZE])
|
|
|
|
return {
|
|
"rides": self._serialize_rides(rides),
|
|
"total_count": total_count,
|
|
"has_more": len(rides) == self.PROGRESSIVE_LOAD_SIZE,
|
|
"next_offset": offset + len(rides) if len(rides) == self.PROGRESSIVE_LOAD_SIZE else None,
|
|
}
|
|
|
|
def get_filter_metadata(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
"""
|
|
Get comprehensive filter metadata for dynamic filter generation.
|
|
|
|
Args:
|
|
filters: Optional filters to scope the metadata
|
|
|
|
Returns:
|
|
Dict containing all available filter options and ranges
|
|
"""
|
|
cache_key = f"{self.cache_prefix}filter_metadata_{hash(str(filters))}"
|
|
metadata = cache.get(cache_key)
|
|
|
|
if metadata is None:
|
|
metadata = self._generate_filter_metadata(filters)
|
|
cache.set(cache_key, metadata, self.CACHE_TIMEOUT)
|
|
|
|
return metadata
|
|
|
|
def invalidate_cache(self) -> None:
|
|
"""Invalidate all cached data for rides."""
|
|
# Note: In production, you might want to use cache versioning
|
|
# or more sophisticated cache invalidation
|
|
cache_keys = [
|
|
f"{self.cache_prefix}client_side_all",
|
|
f"{self.cache_prefix}filter_metadata",
|
|
f"{self.cache_prefix}total_count",
|
|
]
|
|
|
|
for key in cache_keys:
|
|
cache.delete(key)
|
|
|
|
def _get_total_count(self, filters: dict[str, Any] | None = None) -> int:
|
|
"""Get total count of rides matching filters."""
|
|
cache_key = f"{self.cache_prefix}total_count_{hash(str(filters))}"
|
|
count = cache.get(cache_key)
|
|
|
|
if count is None:
|
|
queryset = self._build_filtered_queryset(filters)
|
|
count = queryset.count()
|
|
cache.set(cache_key, count, self.CACHE_TIMEOUT)
|
|
|
|
return count
|
|
|
|
def _get_client_side_data(self, filters: dict[str, Any] | None, total_count: int) -> dict[str, Any]:
|
|
"""Get all data for client-side filtering."""
|
|
cache_key = f"{self.cache_prefix}client_side_all"
|
|
cached_data = cache.get(cache_key)
|
|
|
|
if cached_data is None:
|
|
from apps.rides.models import Ride
|
|
|
|
# Load all rides with optimized query
|
|
queryset = (
|
|
Ride.objects.select_related(
|
|
"park",
|
|
"park__location",
|
|
"park_area",
|
|
"manufacturer",
|
|
"designer",
|
|
"ride_model",
|
|
"ride_model__manufacturer",
|
|
)
|
|
.prefetch_related("coaster_stats")
|
|
.order_by("name")
|
|
)
|
|
|
|
rides = list(queryset)
|
|
cached_data = self._serialize_rides(rides)
|
|
cache.set(cache_key, cached_data, self.CACHE_TIMEOUT)
|
|
|
|
return {
|
|
"strategy": "client_side",
|
|
"rides": cached_data,
|
|
"total_count": total_count,
|
|
"has_more": False,
|
|
"filter_metadata": self.get_filter_metadata(filters),
|
|
}
|
|
|
|
def _get_server_side_data(self, filters: dict[str, Any] | None, total_count: int) -> dict[str, Any]:
|
|
"""Get initial batch for server-side filtering."""
|
|
# Build filtered queryset
|
|
queryset = self._build_filtered_queryset(filters)
|
|
|
|
# Get initial batch
|
|
rides = list(queryset[: self.INITIAL_LOAD_SIZE])
|
|
|
|
return {
|
|
"strategy": "server_side",
|
|
"rides": self._serialize_rides(rides),
|
|
"total_count": total_count,
|
|
"has_more": len(rides) == self.INITIAL_LOAD_SIZE,
|
|
"next_offset": len(rides) if len(rides) == self.INITIAL_LOAD_SIZE else None,
|
|
}
|
|
|
|
def _build_filtered_queryset(self, filters: dict[str, Any] | None):
|
|
"""Build Django queryset with applied filters."""
|
|
from apps.rides.models import Ride
|
|
|
|
# Start with optimized base queryset
|
|
queryset = Ride.objects.select_related(
|
|
"park", "park__location", "park_area", "manufacturer", "designer", "ride_model", "ride_model__manufacturer"
|
|
).prefetch_related("coaster_stats")
|
|
|
|
if not filters:
|
|
return queryset.order_by("name")
|
|
|
|
# Apply filters
|
|
q_objects = Q()
|
|
|
|
# Text search using computed search_text field
|
|
if "search" in filters and filters["search"]:
|
|
search_term = filters["search"].lower()
|
|
q_objects &= Q(search_text__icontains=search_term)
|
|
|
|
# Park filters
|
|
if "park_slug" in filters and filters["park_slug"]:
|
|
q_objects &= Q(park__slug=filters["park_slug"])
|
|
|
|
if "park_id" in filters and filters["park_id"]:
|
|
q_objects &= Q(park_id=filters["park_id"])
|
|
|
|
# Category filters
|
|
if "category" in filters and filters["category"]:
|
|
q_objects &= Q(category__in=filters["category"])
|
|
|
|
# Status filters
|
|
if "status" in filters and filters["status"]:
|
|
q_objects &= Q(status__in=filters["status"])
|
|
|
|
# Company filters
|
|
if "manufacturer_ids" in filters and filters["manufacturer_ids"]:
|
|
q_objects &= Q(manufacturer_id__in=filters["manufacturer_ids"])
|
|
|
|
if "designer_ids" in filters and filters["designer_ids"]:
|
|
q_objects &= Q(designer_id__in=filters["designer_ids"])
|
|
|
|
# Ride model filters
|
|
if "ride_model_ids" in filters and filters["ride_model_ids"]:
|
|
q_objects &= Q(ride_model_id__in=filters["ride_model_ids"])
|
|
|
|
# Opening year filters using computed opening_year field
|
|
if "opening_year" in filters and filters["opening_year"]:
|
|
q_objects &= Q(opening_year=filters["opening_year"])
|
|
|
|
if "min_opening_year" in filters and filters["min_opening_year"]:
|
|
q_objects &= Q(opening_year__gte=filters["min_opening_year"])
|
|
|
|
if "max_opening_year" in filters and filters["max_opening_year"]:
|
|
q_objects &= Q(opening_year__lte=filters["max_opening_year"])
|
|
|
|
# Rating filters
|
|
if "min_rating" in filters and filters["min_rating"]:
|
|
q_objects &= Q(average_rating__gte=filters["min_rating"])
|
|
|
|
if "max_rating" in filters and filters["max_rating"]:
|
|
q_objects &= Q(average_rating__lte=filters["max_rating"])
|
|
|
|
# Height requirement filters
|
|
if "min_height_requirement" in filters and filters["min_height_requirement"]:
|
|
q_objects &= Q(min_height_in__gte=filters["min_height_requirement"])
|
|
|
|
if "max_height_requirement" in filters and filters["max_height_requirement"]:
|
|
q_objects &= Q(max_height_in__lte=filters["max_height_requirement"])
|
|
|
|
# Capacity filters
|
|
if "min_capacity" in filters and filters["min_capacity"]:
|
|
q_objects &= Q(capacity_per_hour__gte=filters["min_capacity"])
|
|
|
|
if "max_capacity" in filters and filters["max_capacity"]:
|
|
q_objects &= Q(capacity_per_hour__lte=filters["max_capacity"])
|
|
|
|
# Roller coaster specific filters
|
|
if "roller_coaster_type" in filters and filters["roller_coaster_type"]:
|
|
q_objects &= Q(coaster_stats__roller_coaster_type__in=filters["roller_coaster_type"])
|
|
|
|
if "track_material" in filters and filters["track_material"]:
|
|
q_objects &= Q(coaster_stats__track_material__in=filters["track_material"])
|
|
|
|
if "propulsion_system" in filters and filters["propulsion_system"]:
|
|
q_objects &= Q(coaster_stats__propulsion_system__in=filters["propulsion_system"])
|
|
|
|
# Roller coaster height filters
|
|
if "min_height_ft" in filters and filters["min_height_ft"]:
|
|
q_objects &= Q(coaster_stats__height_ft__gte=filters["min_height_ft"])
|
|
|
|
if "max_height_ft" in filters and filters["max_height_ft"]:
|
|
q_objects &= Q(coaster_stats__height_ft__lte=filters["max_height_ft"])
|
|
|
|
# Roller coaster speed filters
|
|
if "min_speed_mph" in filters and filters["min_speed_mph"]:
|
|
q_objects &= Q(coaster_stats__speed_mph__gte=filters["min_speed_mph"])
|
|
|
|
if "max_speed_mph" in filters and filters["max_speed_mph"]:
|
|
q_objects &= Q(coaster_stats__speed_mph__lte=filters["max_speed_mph"])
|
|
|
|
# Inversion filters
|
|
if "min_inversions" in filters and filters["min_inversions"]:
|
|
q_objects &= Q(coaster_stats__inversions__gte=filters["min_inversions"])
|
|
|
|
if "max_inversions" in filters and filters["max_inversions"]:
|
|
q_objects &= Q(coaster_stats__inversions__lte=filters["max_inversions"])
|
|
|
|
if "has_inversions" in filters and filters["has_inversions"] is not None:
|
|
if filters["has_inversions"]:
|
|
q_objects &= Q(coaster_stats__inversions__gt=0)
|
|
else:
|
|
q_objects &= Q(coaster_stats__inversions=0)
|
|
|
|
# Apply filters and ordering
|
|
queryset = queryset.filter(q_objects)
|
|
|
|
# Apply ordering
|
|
ordering = filters.get("ordering", "name")
|
|
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"
|
|
)
|
|
queryset = queryset.order_by(ordering_field)
|
|
else:
|
|
queryset = queryset.order_by(ordering)
|
|
|
|
return queryset
|
|
|
|
def _serialize_rides(self, rides: list) -> list[dict[str, Any]]:
|
|
"""Serialize ride objects to dictionaries."""
|
|
serialized = []
|
|
|
|
for ride in rides:
|
|
# Basic ride data
|
|
ride_data = {
|
|
"id": ride.id,
|
|
"name": ride.name,
|
|
"slug": ride.slug,
|
|
"description": ride.description,
|
|
"category": ride.category,
|
|
"status": ride.status,
|
|
"opening_date": ride.opening_date.isoformat() if ride.opening_date else None,
|
|
"closing_date": ride.closing_date.isoformat() if ride.closing_date else None,
|
|
"opening_year": ride.opening_year,
|
|
"min_height_in": ride.min_height_in,
|
|
"max_height_in": ride.max_height_in,
|
|
"capacity_per_hour": ride.capacity_per_hour,
|
|
"ride_duration_seconds": ride.ride_duration_seconds,
|
|
"average_rating": float(ride.average_rating) if ride.average_rating else None,
|
|
"url": ride.url,
|
|
"park_url": ride.park_url,
|
|
"created_at": ride.created_at.isoformat(),
|
|
"updated_at": ride.updated_at.isoformat(),
|
|
}
|
|
|
|
# Park data
|
|
if ride.park:
|
|
ride_data["park"] = {
|
|
"id": ride.park.id,
|
|
"name": ride.park.name,
|
|
"slug": ride.park.slug,
|
|
}
|
|
|
|
# Park location data
|
|
if hasattr(ride.park, "location") and ride.park.location:
|
|
ride_data["park"]["location"] = {
|
|
"city": ride.park.location.city,
|
|
"state": ride.park.location.state,
|
|
"country": ride.park.location.country,
|
|
}
|
|
|
|
# Park area data
|
|
if ride.park_area:
|
|
ride_data["park_area"] = {
|
|
"id": ride.park_area.id,
|
|
"name": ride.park_area.name,
|
|
"slug": ride.park_area.slug,
|
|
}
|
|
|
|
# Company data
|
|
if ride.manufacturer:
|
|
ride_data["manufacturer"] = {
|
|
"id": ride.manufacturer.id,
|
|
"name": ride.manufacturer.name,
|
|
"slug": ride.manufacturer.slug,
|
|
}
|
|
|
|
if ride.designer:
|
|
ride_data["designer"] = {
|
|
"id": ride.designer.id,
|
|
"name": ride.designer.name,
|
|
"slug": ride.designer.slug,
|
|
}
|
|
|
|
# Ride model data
|
|
if ride.ride_model:
|
|
ride_data["ride_model"] = {
|
|
"id": ride.ride_model.id,
|
|
"name": ride.ride_model.name,
|
|
"slug": ride.ride_model.slug,
|
|
"category": ride.ride_model.category,
|
|
}
|
|
|
|
if ride.ride_model.manufacturer:
|
|
ride_data["ride_model"]["manufacturer"] = {
|
|
"id": ride.ride_model.manufacturer.id,
|
|
"name": ride.ride_model.manufacturer.name,
|
|
"slug": ride.ride_model.manufacturer.slug,
|
|
}
|
|
|
|
# Roller coaster stats
|
|
if hasattr(ride, "coaster_stats") and ride.coaster_stats:
|
|
stats = ride.coaster_stats
|
|
ride_data["coaster_stats"] = {
|
|
"height_ft": float(stats.height_ft) if stats.height_ft else None,
|
|
"length_ft": float(stats.length_ft) if stats.length_ft else None,
|
|
"speed_mph": float(stats.speed_mph) if stats.speed_mph else None,
|
|
"inversions": stats.inversions,
|
|
"ride_time_seconds": stats.ride_time_seconds,
|
|
"track_type": stats.track_type,
|
|
"track_material": stats.track_material,
|
|
"roller_coaster_type": stats.roller_coaster_type,
|
|
"max_drop_height_ft": float(stats.max_drop_height_ft) if stats.max_drop_height_ft else None,
|
|
"propulsion_system": stats.propulsion_system,
|
|
"train_style": stats.train_style,
|
|
"trains_count": stats.trains_count,
|
|
"cars_per_train": stats.cars_per_train,
|
|
"seats_per_car": stats.seats_per_car,
|
|
}
|
|
|
|
serialized.append(ride_data)
|
|
|
|
return serialized
|
|
|
|
def _generate_filter_metadata(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
"""Generate comprehensive filter metadata."""
|
|
from apps.rides.models import Ride, RideModel
|
|
from apps.rides.models.company import Company
|
|
from apps.rides.models.rides import RollerCoasterStats
|
|
|
|
# Get unique values from database with counts
|
|
parks_data = list(
|
|
Ride.objects.exclude(park__isnull=True)
|
|
.select_related("park")
|
|
.values("park__id", "park__name", "park__slug")
|
|
.annotate(count=models.Count("id"))
|
|
.distinct()
|
|
.order_by("park__name")
|
|
)
|
|
|
|
park_areas_data = list(
|
|
Ride.objects.exclude(park_area__isnull=True)
|
|
.select_related("park_area")
|
|
.values("park_area__id", "park_area__name", "park_area__slug")
|
|
.annotate(count=models.Count("id"))
|
|
.distinct()
|
|
.order_by("park_area__name")
|
|
)
|
|
|
|
manufacturers_data = list(
|
|
Company.objects.filter(roles__contains=["MANUFACTURER"])
|
|
.values("id", "name", "slug")
|
|
.annotate(count=models.Count("manufactured_rides"))
|
|
.order_by("name")
|
|
)
|
|
|
|
designers_data = list(
|
|
Company.objects.filter(roles__contains=["DESIGNER"])
|
|
.values("id", "name", "slug")
|
|
.annotate(count=models.Count("designed_rides"))
|
|
.order_by("name")
|
|
)
|
|
|
|
ride_models_data = list(
|
|
RideModel.objects.select_related("manufacturer")
|
|
.values("id", "name", "slug", "manufacturer__name", "manufacturer__slug", "category")
|
|
.annotate(count=models.Count("rides"))
|
|
.order_by("manufacturer__name", "name")
|
|
)
|
|
|
|
# Get categories and statuses with counts
|
|
categories_data = list(Ride.objects.values("category").annotate(count=models.Count("id")).order_by("category"))
|
|
|
|
statuses_data = list(Ride.objects.values("status").annotate(count=models.Count("id")).order_by("status"))
|
|
|
|
# Get roller coaster specific data with counts
|
|
rc_types_data = list(
|
|
RollerCoasterStats.objects.values("roller_coaster_type")
|
|
.annotate(count=models.Count("ride"))
|
|
.exclude(roller_coaster_type__isnull=True)
|
|
.order_by("roller_coaster_type")
|
|
)
|
|
|
|
track_materials_data = list(
|
|
RollerCoasterStats.objects.values("track_material")
|
|
.annotate(count=models.Count("ride"))
|
|
.exclude(track_material__isnull=True)
|
|
.order_by("track_material")
|
|
)
|
|
|
|
propulsion_systems_data = list(
|
|
RollerCoasterStats.objects.values("propulsion_system")
|
|
.annotate(count=models.Count("ride"))
|
|
.exclude(propulsion_system__isnull=True)
|
|
.order_by("propulsion_system")
|
|
)
|
|
|
|
# Convert to frontend-expected format with value/label/count
|
|
categories = [
|
|
{"value": item["category"], "label": self._get_category_label(item["category"]), "count": item["count"]}
|
|
for item in categories_data
|
|
]
|
|
|
|
statuses = [
|
|
{"value": item["status"], "label": self._get_status_label(item["status"]), "count": item["count"]}
|
|
for item in statuses_data
|
|
]
|
|
|
|
roller_coaster_types = [
|
|
{
|
|
"value": item["roller_coaster_type"],
|
|
"label": self._get_rc_type_label(item["roller_coaster_type"]),
|
|
"count": item["count"],
|
|
}
|
|
for item in rc_types_data
|
|
]
|
|
|
|
track_materials = [
|
|
{
|
|
"value": item["track_material"],
|
|
"label": self._get_track_material_label(item["track_material"]),
|
|
"count": item["count"],
|
|
}
|
|
for item in track_materials_data
|
|
]
|
|
|
|
propulsion_systems = [
|
|
{
|
|
"value": item["propulsion_system"],
|
|
"label": self._get_propulsion_system_label(item["propulsion_system"]),
|
|
"count": item["count"],
|
|
}
|
|
for item in propulsion_systems_data
|
|
]
|
|
|
|
# Convert other data to expected format
|
|
parks = [
|
|
{"value": str(item["park__id"]), "label": item["park__name"], "count": item["count"]} for item in parks_data
|
|
]
|
|
|
|
park_areas = [
|
|
{"value": str(item["park_area__id"]), "label": item["park_area__name"], "count": item["count"]}
|
|
for item in park_areas_data
|
|
]
|
|
|
|
manufacturers = [
|
|
{"value": str(item["id"]), "label": item["name"], "count": item["count"]} for item in manufacturers_data
|
|
]
|
|
|
|
designers = [
|
|
{"value": str(item["id"]), "label": item["name"], "count": item["count"]} for item in designers_data
|
|
]
|
|
|
|
ride_models = [
|
|
{"value": str(item["id"]), "label": f"{item['manufacturer__name']} {item['name']}", "count": item["count"]}
|
|
for item in ride_models_data
|
|
]
|
|
|
|
# Calculate ranges from actual data
|
|
ride_stats = Ride.objects.aggregate(
|
|
min_rating=Min("average_rating"),
|
|
max_rating=Max("average_rating"),
|
|
min_height_req=Min("min_height_in"),
|
|
max_height_req=Max("max_height_in"),
|
|
min_capacity=Min("capacity_per_hour"),
|
|
max_capacity=Max("capacity_per_hour"),
|
|
min_duration=Min("ride_duration_seconds"),
|
|
max_duration=Max("ride_duration_seconds"),
|
|
min_year=Min("opening_year"),
|
|
max_year=Max("opening_year"),
|
|
)
|
|
|
|
# Calculate roller coaster specific ranges
|
|
coaster_stats = RollerCoasterStats.objects.aggregate(
|
|
min_height_ft=Min("height_ft"),
|
|
max_height_ft=Max("height_ft"),
|
|
min_length_ft=Min("length_ft"),
|
|
max_length_ft=Max("length_ft"),
|
|
min_speed_mph=Min("speed_mph"),
|
|
max_speed_mph=Max("speed_mph"),
|
|
min_inversions=Min("inversions"),
|
|
max_inversions=Max("inversions"),
|
|
min_ride_time=Min("ride_time_seconds"),
|
|
max_ride_time=Max("ride_time_seconds"),
|
|
min_drop_height=Min("max_drop_height_ft"),
|
|
max_drop_height=Max("max_drop_height_ft"),
|
|
min_trains=Min("trains_count"),
|
|
max_trains=Max("trains_count"),
|
|
min_cars=Min("cars_per_train"),
|
|
max_cars=Max("cars_per_train"),
|
|
min_seats=Min("seats_per_car"),
|
|
max_seats=Max("seats_per_car"),
|
|
)
|
|
|
|
return {
|
|
"categorical": {
|
|
"categories": categories,
|
|
"statuses": statuses,
|
|
"roller_coaster_types": roller_coaster_types,
|
|
"track_materials": track_materials,
|
|
"propulsion_systems": propulsion_systems,
|
|
"parks": parks,
|
|
"park_areas": park_areas,
|
|
"manufacturers": manufacturers,
|
|
"designers": designers,
|
|
"ride_models": ride_models,
|
|
},
|
|
"ranges": {
|
|
"rating": {
|
|
"min": float(ride_stats["min_rating"] or 1),
|
|
"max": float(ride_stats["max_rating"] or 10),
|
|
"step": 0.1,
|
|
"unit": "stars",
|
|
},
|
|
"height_requirement": {
|
|
"min": ride_stats["min_height_req"] or 30,
|
|
"max": ride_stats["max_height_req"] or 90,
|
|
"step": 1,
|
|
"unit": "inches",
|
|
},
|
|
"capacity": {
|
|
"min": ride_stats["min_capacity"] or 0,
|
|
"max": ride_stats["max_capacity"] or 5000,
|
|
"step": 50,
|
|
"unit": "riders/hour",
|
|
},
|
|
"ride_duration": {
|
|
"min": ride_stats["min_duration"] or 0,
|
|
"max": ride_stats["max_duration"] or 600,
|
|
"step": 10,
|
|
"unit": "seconds",
|
|
},
|
|
"opening_year": {
|
|
"min": ride_stats["min_year"] or 1800,
|
|
"max": ride_stats["max_year"] or 2030,
|
|
"step": 1,
|
|
"unit": "year",
|
|
},
|
|
"height_ft": {
|
|
"min": float(coaster_stats["min_height_ft"] or 0),
|
|
"max": float(coaster_stats["max_height_ft"] or 500),
|
|
"step": 5,
|
|
"unit": "feet",
|
|
},
|
|
"length_ft": {
|
|
"min": float(coaster_stats["min_length_ft"] or 0),
|
|
"max": float(coaster_stats["max_length_ft"] or 10000),
|
|
"step": 100,
|
|
"unit": "feet",
|
|
},
|
|
"speed_mph": {
|
|
"min": float(coaster_stats["min_speed_mph"] or 0),
|
|
"max": float(coaster_stats["max_speed_mph"] or 150),
|
|
"step": 5,
|
|
"unit": "mph",
|
|
},
|
|
"inversions": {
|
|
"min": coaster_stats["min_inversions"] or 0,
|
|
"max": coaster_stats["max_inversions"] or 20,
|
|
"step": 1,
|
|
"unit": "inversions",
|
|
},
|
|
},
|
|
"total_count": Ride.objects.count(),
|
|
}
|
|
|
|
def _get_category_label(self, category: str) -> str:
|
|
"""Convert category code to human-readable label."""
|
|
category_labels = {
|
|
"RC": "Roller Coaster",
|
|
"DR": "Dark Ride",
|
|
"FR": "Flat Ride",
|
|
"WR": "Water Ride",
|
|
"TR": "Transport Ride",
|
|
"OT": "Other",
|
|
}
|
|
if category in category_labels:
|
|
return category_labels[category]
|
|
else:
|
|
raise ValueError(f"Unknown ride category: {category}")
|
|
|
|
def _get_status_label(self, status: str) -> str:
|
|
"""Convert status code to human-readable label."""
|
|
status_labels = {
|
|
"OPERATING": "Operating",
|
|
"CLOSED_TEMP": "Temporarily Closed",
|
|
"SBNO": "Standing But Not Operating",
|
|
"CLOSING": "Closing Soon",
|
|
"CLOSED_PERM": "Permanently Closed",
|
|
"UNDER_CONSTRUCTION": "Under Construction",
|
|
"DEMOLISHED": "Demolished",
|
|
"RELOCATED": "Relocated",
|
|
}
|
|
if status in status_labels:
|
|
return status_labels[status]
|
|
else:
|
|
raise ValueError(f"Unknown ride status: {status}")
|
|
|
|
def _get_rc_type_label(self, rc_type: str) -> str:
|
|
"""Convert roller coaster type to human-readable label."""
|
|
rc_type_labels = {
|
|
"SITDOWN": "Sit Down",
|
|
"INVERTED": "Inverted",
|
|
"SUSPENDED": "Suspended",
|
|
"FLOORLESS": "Floorless",
|
|
"FLYING": "Flying",
|
|
"WING": "Wing",
|
|
"DIVE": "Dive",
|
|
"SPINNING": "Spinning",
|
|
"WILD_MOUSE": "Wild Mouse",
|
|
"BOBSLED": "Bobsled",
|
|
"PIPELINE": "Pipeline",
|
|
"FOURTH_DIMENSION": "4th Dimension",
|
|
"FAMILY": "Family",
|
|
}
|
|
if rc_type in rc_type_labels:
|
|
return rc_type_labels[rc_type]
|
|
else:
|
|
raise ValueError(f"Unknown roller coaster type: {rc_type}")
|
|
|
|
def _get_track_material_label(self, material: str) -> str:
|
|
"""Convert track material to human-readable label."""
|
|
material_labels = {
|
|
"STEEL": "Steel",
|
|
"WOOD": "Wood",
|
|
"HYBRID": "Hybrid (Steel/Wood)",
|
|
}
|
|
if material in material_labels:
|
|
return material_labels[material]
|
|
else:
|
|
raise ValueError(f"Unknown track material: {material}")
|
|
|
|
def _get_propulsion_system_label(self, propulsion_system: str) -> str:
|
|
"""Convert propulsion system to human-readable label."""
|
|
propulsion_labels = {
|
|
"CHAIN": "Chain Lift",
|
|
"LSM": "Linear Synchronous Motor",
|
|
"LIM": "Linear Induction Motor",
|
|
"HYDRAULIC": "Hydraulic Launch",
|
|
"PNEUMATIC": "Pneumatic Launch",
|
|
"CABLE": "Cable Lift",
|
|
"FLYWHEEL": "Flywheel Launch",
|
|
"GRAVITY": "Gravity",
|
|
"NONE": "No Propulsion System",
|
|
}
|
|
if propulsion_system in propulsion_labels:
|
|
return propulsion_labels[propulsion_system]
|
|
else:
|
|
raise ValueError(f"Unknown propulsion system: {propulsion_system}")
|