mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-30 08:27:00 -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:
@@ -1,7 +1,7 @@
|
||||
from .location_service import RideLocationService
|
||||
from .media_service import RideMediaService
|
||||
# Import from the services_core module (was services.py, renamed to avoid package collision)
|
||||
from ..services_core import RideService
|
||||
from .location_service import RideLocationService
|
||||
from .media_service import RideMediaService
|
||||
|
||||
__all__ = ["RideLocationService", "RideMediaService", "RideService"]
|
||||
|
||||
|
||||
@@ -17,11 +17,12 @@ Architecture:
|
||||
- Hybrid: Combine both approaches based on data characteristics
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db import models
|
||||
from django.db.models import Q, Min, Max
|
||||
import logging
|
||||
from django.db.models import Max, Min, Q
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,34 +30,34 @@ 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: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
|
||||
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'
|
||||
@@ -65,63 +66,63 @@ class SmartRideLoader:
|
||||
- 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: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
|
||||
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: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
|
||||
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
|
||||
@@ -131,31 +132,31 @@ class SmartRideLoader:
|
||||
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: Optional[Dict[str, Any]] = None) -> int:
|
||||
|
||||
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: Optional[Dict[str, Any]],
|
||||
total_count: int) -> Dict[str, Any]:
|
||||
|
||||
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',
|
||||
@@ -168,11 +169,11 @@ class SmartRideLoader:
|
||||
).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,
|
||||
@@ -180,16 +181,16 @@ class SmartRideLoader:
|
||||
'has_more': False,
|
||||
'filter_metadata': self.get_filter_metadata(filters)
|
||||
}
|
||||
|
||||
def _get_server_side_data(self, filters: Optional[Dict[str, Any]],
|
||||
total_count: int) -> Dict[str, Any]:
|
||||
|
||||
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),
|
||||
@@ -197,11 +198,11 @@ class SmartRideLoader:
|
||||
'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: Optional[Dict[str, Any]]):
|
||||
|
||||
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',
|
||||
@@ -214,115 +215,115 @@ class SmartRideLoader:
|
||||
).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']:
|
||||
@@ -331,13 +332,13 @@ class SmartRideLoader:
|
||||
queryset = queryset.order_by(ordering_field)
|
||||
else:
|
||||
queryset = queryset.order_by(ordering)
|
||||
|
||||
|
||||
return queryset
|
||||
|
||||
def _serialize_rides(self, rides: List) -> List[Dict[str, Any]]:
|
||||
|
||||
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 = {
|
||||
@@ -360,7 +361,7 @@ class SmartRideLoader:
|
||||
'created_at': ride.created_at.isoformat(),
|
||||
'updated_at': ride.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
# Park data
|
||||
if ride.park:
|
||||
ride_data['park'] = {
|
||||
@@ -368,7 +369,7 @@ class SmartRideLoader:
|
||||
'name': ride.park.name,
|
||||
'slug': ride.park.slug,
|
||||
}
|
||||
|
||||
|
||||
# Park location data
|
||||
if hasattr(ride.park, 'location') and ride.park.location:
|
||||
ride_data['park']['location'] = {
|
||||
@@ -376,7 +377,7 @@ class SmartRideLoader:
|
||||
'state': ride.park.location.state,
|
||||
'country': ride.park.location.country,
|
||||
}
|
||||
|
||||
|
||||
# Park area data
|
||||
if ride.park_area:
|
||||
ride_data['park_area'] = {
|
||||
@@ -384,7 +385,7 @@ class SmartRideLoader:
|
||||
'name': ride.park_area.name,
|
||||
'slug': ride.park_area.slug,
|
||||
}
|
||||
|
||||
|
||||
# Company data
|
||||
if ride.manufacturer:
|
||||
ride_data['manufacturer'] = {
|
||||
@@ -392,14 +393,14 @@ class SmartRideLoader:
|
||||
'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'] = {
|
||||
@@ -408,14 +409,14 @@ class SmartRideLoader:
|
||||
'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
|
||||
@@ -435,70 +436,70 @@ class SmartRideLoader:
|
||||
'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: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
|
||||
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 = [
|
||||
{
|
||||
@@ -508,7 +509,7 @@ class SmartRideLoader:
|
||||
}
|
||||
for item in categories_data
|
||||
]
|
||||
|
||||
|
||||
statuses = [
|
||||
{
|
||||
'value': item['status'],
|
||||
@@ -517,7 +518,7 @@ class SmartRideLoader:
|
||||
}
|
||||
for item in statuses_data
|
||||
]
|
||||
|
||||
|
||||
roller_coaster_types = [
|
||||
{
|
||||
'value': item['roller_coaster_type'],
|
||||
@@ -526,7 +527,7 @@ class SmartRideLoader:
|
||||
}
|
||||
for item in rc_types_data
|
||||
]
|
||||
|
||||
|
||||
track_materials = [
|
||||
{
|
||||
'value': item['track_material'],
|
||||
@@ -535,7 +536,7 @@ class SmartRideLoader:
|
||||
}
|
||||
for item in track_materials_data
|
||||
]
|
||||
|
||||
|
||||
propulsion_systems = [
|
||||
{
|
||||
'value': item['propulsion_system'],
|
||||
@@ -544,7 +545,7 @@ class SmartRideLoader:
|
||||
}
|
||||
for item in propulsion_systems_data
|
||||
]
|
||||
|
||||
|
||||
# Convert other data to expected format
|
||||
parks = [
|
||||
{
|
||||
@@ -554,7 +555,7 @@ class SmartRideLoader:
|
||||
}
|
||||
for item in parks_data
|
||||
]
|
||||
|
||||
|
||||
park_areas = [
|
||||
{
|
||||
'value': str(item['park_area__id']),
|
||||
@@ -563,7 +564,7 @@ class SmartRideLoader:
|
||||
}
|
||||
for item in park_areas_data
|
||||
]
|
||||
|
||||
|
||||
manufacturers = [
|
||||
{
|
||||
'value': str(item['id']),
|
||||
@@ -572,7 +573,7 @@ class SmartRideLoader:
|
||||
}
|
||||
for item in manufacturers_data
|
||||
]
|
||||
|
||||
|
||||
designers = [
|
||||
{
|
||||
'value': str(item['id']),
|
||||
@@ -581,7 +582,7 @@ class SmartRideLoader:
|
||||
}
|
||||
for item in designers_data
|
||||
]
|
||||
|
||||
|
||||
ride_models = [
|
||||
{
|
||||
'value': str(item['id']),
|
||||
@@ -590,7 +591,7 @@ class SmartRideLoader:
|
||||
}
|
||||
for item in ride_models_data
|
||||
]
|
||||
|
||||
|
||||
# Calculate ranges from actual data
|
||||
ride_stats = Ride.objects.aggregate(
|
||||
min_rating=Min('average_rating'),
|
||||
@@ -604,7 +605,7 @@ class SmartRideLoader:
|
||||
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'),
|
||||
@@ -626,7 +627,7 @@ class SmartRideLoader:
|
||||
min_seats=Min('seats_per_car'),
|
||||
max_seats=Max('seats_per_car'),
|
||||
)
|
||||
|
||||
|
||||
return {
|
||||
'categorical': {
|
||||
'categories': categories,
|
||||
@@ -698,7 +699,7 @@ class SmartRideLoader:
|
||||
},
|
||||
'total_count': Ride.objects.count(),
|
||||
}
|
||||
|
||||
|
||||
def _get_category_label(self, category: str) -> str:
|
||||
"""Convert category code to human-readable label."""
|
||||
category_labels = {
|
||||
@@ -713,7 +714,7 @@ class SmartRideLoader:
|
||||
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 = {
|
||||
@@ -730,7 +731,7 @@ class SmartRideLoader:
|
||||
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 = {
|
||||
@@ -752,7 +753,7 @@ class SmartRideLoader:
|
||||
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 = {
|
||||
@@ -764,7 +765,7 @@ class SmartRideLoader:
|
||||
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 = {
|
||||
|
||||
@@ -3,10 +3,11 @@ Rides-specific location services with OpenStreetMap integration.
|
||||
Handles location management for individual rides within parks.
|
||||
"""
|
||||
|
||||
import requests
|
||||
from typing import List, Dict, Any, Optional
|
||||
from django.db import transaction
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from django.db import transaction
|
||||
|
||||
from ..models import RideLocation
|
||||
|
||||
@@ -27,8 +28,8 @@ class RideLocationService:
|
||||
cls,
|
||||
*,
|
||||
ride,
|
||||
latitude: Optional[float] = None,
|
||||
longitude: Optional[float] = None,
|
||||
latitude: float | None = None,
|
||||
longitude: float | None = None,
|
||||
park_area: str = "",
|
||||
notes: str = "",
|
||||
entrance_notes: str = "",
|
||||
@@ -101,7 +102,7 @@ class RideLocationService:
|
||||
return ride_location
|
||||
|
||||
@classmethod
|
||||
def find_rides_in_area(cls, park, park_area: str) -> List[RideLocation]:
|
||||
def find_rides_in_area(cls, park, park_area: str) -> list[RideLocation]:
|
||||
"""
|
||||
Find all rides in a specific park area.
|
||||
|
||||
@@ -121,7 +122,7 @@ class RideLocationService:
|
||||
@classmethod
|
||||
def find_nearby_rides(
|
||||
cls, latitude: float, longitude: float, park=None, radius_meters: float = 500
|
||||
) -> List[RideLocation]:
|
||||
) -> list[RideLocation]:
|
||||
"""
|
||||
Find rides near given coordinates using PostGIS.
|
||||
Useful for finding rides near a specific location within a park.
|
||||
@@ -153,7 +154,7 @@ class RideLocationService:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_ride_navigation_info(cls, ride_location: RideLocation) -> Dict[str, Any]:
|
||||
def get_ride_navigation_info(cls, ride_location: RideLocation) -> dict[str, Any]:
|
||||
"""
|
||||
Get comprehensive navigation information for a ride.
|
||||
|
||||
@@ -196,8 +197,8 @@ class RideLocationService:
|
||||
def estimate_ride_coordinates_from_park(
|
||||
cls,
|
||||
ride_location: RideLocation,
|
||||
area_offset_meters: Optional[Dict[str, List[float]]] = None,
|
||||
) -> Optional[List[float]]:
|
||||
area_offset_meters: dict[str, list[float]] | None = None,
|
||||
) -> list[float] | None:
|
||||
"""
|
||||
Estimate ride coordinates based on park location and area.
|
||||
Useful when exact ride coordinates are not available.
|
||||
@@ -332,7 +333,7 @@ class RideLocationService:
|
||||
return updated_count
|
||||
|
||||
@classmethod
|
||||
def generate_park_area_map(cls, park) -> Dict[str, List[str]]:
|
||||
def generate_park_area_map(cls, park) -> dict[str, list[str]]:
|
||||
"""
|
||||
Generate a map of park areas and the rides in each area.
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@ This module provides media management functionality specific to rides.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any
|
||||
from typing import Any
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.db import transaction
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from apps.core.services.media_service import MediaService
|
||||
|
||||
from ..models import Ride, RidePhoto
|
||||
|
||||
User = get_user_model()
|
||||
@@ -83,8 +86,8 @@ class RideMediaService:
|
||||
ride: Ride,
|
||||
approved_only: bool = True,
|
||||
primary_first: bool = True,
|
||||
photo_type: Optional[str] = None,
|
||||
) -> List[RidePhoto]:
|
||||
photo_type: str | None = None,
|
||||
) -> list[RidePhoto]:
|
||||
"""
|
||||
Get photos for a ride.
|
||||
|
||||
@@ -113,7 +116,7 @@ class RideMediaService:
|
||||
return list(queryset)
|
||||
|
||||
@staticmethod
|
||||
def get_primary_photo(ride: Ride) -> Optional[RidePhoto]:
|
||||
def get_primary_photo(ride: Ride) -> RidePhoto | None:
|
||||
"""
|
||||
Get the primary photo for a ride.
|
||||
|
||||
@@ -129,7 +132,7 @@ class RideMediaService:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_photos_by_type(ride: Ride, photo_type: str) -> List[RidePhoto]:
|
||||
def get_photos_by_type(ride: Ride, photo_type: str) -> list[RidePhoto]:
|
||||
"""
|
||||
Get photos of a specific type for a ride.
|
||||
|
||||
@@ -224,7 +227,7 @@ class RideMediaService:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_photo_stats(ride: Ride) -> Dict[str, Any]:
|
||||
def get_photo_stats(ride: Ride) -> dict[str, Any]:
|
||||
"""
|
||||
Get photo statistics for a ride.
|
||||
|
||||
@@ -251,7 +254,7 @@ class RideMediaService:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def bulk_approve_photos(photos: List[RidePhoto], approved_by: User) -> int:
|
||||
def bulk_approve_photos(photos: list[RidePhoto], approved_by: User) -> int:
|
||||
"""
|
||||
Bulk approve multiple photos.
|
||||
|
||||
@@ -275,7 +278,7 @@ class RideMediaService:
|
||||
return approved_count
|
||||
|
||||
@staticmethod
|
||||
def get_construction_timeline(ride: Ride) -> List[RidePhoto]:
|
||||
def get_construction_timeline(ride: Ride) -> list[RidePhoto]:
|
||||
"""
|
||||
Get construction photos ordered chronologically.
|
||||
|
||||
@@ -292,7 +295,7 @@ class RideMediaService:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_onride_photos(ride: Ride) -> List[RidePhoto]:
|
||||
def get_onride_photos(ride: Ride) -> list[RidePhoto]:
|
||||
"""
|
||||
Get on-ride photos for a ride.
|
||||
|
||||
|
||||
@@ -7,23 +7,21 @@ Rankings are determined by winning percentage in these comparisons.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
from decimal import Decimal
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import Avg, Count, Q
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.rides.models import (
|
||||
Ride,
|
||||
RideReview,
|
||||
RideRanking,
|
||||
RidePairComparison,
|
||||
RankingSnapshot,
|
||||
Ride,
|
||||
RidePairComparison,
|
||||
RideRanking,
|
||||
RideReview,
|
||||
)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -43,7 +41,7 @@ class RideRankingService:
|
||||
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
|
||||
self.calculation_version = "1.0"
|
||||
|
||||
def update_all_rankings(self, category: Optional[str] = None) -> Dict[str, any]:
|
||||
def update_all_rankings(self, category: str | None = None) -> dict[str, any]:
|
||||
"""
|
||||
Main entry point to update all ride rankings.
|
||||
|
||||
@@ -105,7 +103,7 @@ class RideRankingService:
|
||||
self.logger.error(f"Error updating rankings: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _get_eligible_rides(self, category: Optional[str] = None) -> List[Ride]:
|
||||
def _get_eligible_rides(self, category: str | None = None) -> list[Ride]:
|
||||
"""
|
||||
Get rides that are eligible for ranking.
|
||||
|
||||
@@ -127,8 +125,8 @@ class RideRankingService:
|
||||
return list(queryset.distinct())
|
||||
|
||||
def _calculate_all_comparisons(
|
||||
self, rides: List[Ride]
|
||||
) -> Dict[tuple[int, int], RidePairComparison]:
|
||||
self, rides: list[Ride]
|
||||
) -> dict[tuple[int, int], RidePairComparison]:
|
||||
"""
|
||||
Calculate pairwise comparisons for all ride pairs.
|
||||
|
||||
@@ -156,7 +154,7 @@ class RideRankingService:
|
||||
|
||||
def _calculate_pairwise_comparison(
|
||||
self, ride_a: Ride, ride_b: Ride
|
||||
) -> Optional[RidePairComparison]:
|
||||
) -> RidePairComparison | None:
|
||||
"""
|
||||
Calculate the pairwise comparison between two rides.
|
||||
|
||||
@@ -246,8 +244,8 @@ class RideRankingService:
|
||||
return comparison
|
||||
|
||||
def _calculate_rankings_from_comparisons(
|
||||
self, rides: List[Ride], comparisons: Dict[tuple[int, int], RidePairComparison]
|
||||
) -> List[Dict]:
|
||||
self, rides: list[Ride], comparisons: dict[tuple[int, int], RidePairComparison]
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Calculate final rankings from pairwise comparisons.
|
||||
|
||||
@@ -343,9 +341,9 @@ class RideRankingService:
|
||||
|
||||
def _apply_tiebreakers(
|
||||
self,
|
||||
rankings: List[Dict],
|
||||
comparisons: Dict[tuple[int, int], RidePairComparison],
|
||||
) -> List[Dict]:
|
||||
rankings: list[dict],
|
||||
comparisons: dict[tuple[int, int], RidePairComparison],
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Apply head-to-head tiebreaker for rides with identical winning percentages.
|
||||
|
||||
@@ -379,9 +377,9 @@ class RideRankingService:
|
||||
|
||||
def _sort_tied_group(
|
||||
self,
|
||||
tied_group: List[Dict],
|
||||
comparisons: Dict[tuple[int, int], RidePairComparison],
|
||||
) -> List[Dict]:
|
||||
tied_group: list[dict],
|
||||
comparisons: dict[tuple[int, int], RidePairComparison],
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Sort a group of tied rides using head-to-head comparisons.
|
||||
"""
|
||||
@@ -426,7 +424,7 @@ class RideRankingService:
|
||||
|
||||
return tied_group
|
||||
|
||||
def _save_rankings(self, rankings: List[Dict]):
|
||||
def _save_rankings(self, rankings: list[dict]):
|
||||
"""Save calculated rankings to the database."""
|
||||
for ranking_data in rankings:
|
||||
RideRanking.objects.update_or_create(
|
||||
@@ -445,7 +443,7 @@ class RideRankingService:
|
||||
},
|
||||
)
|
||||
|
||||
def _save_ranking_snapshots(self, rankings: List[Dict]):
|
||||
def _save_ranking_snapshots(self, rankings: list[dict]):
|
||||
"""Save ranking snapshots for historical tracking."""
|
||||
today = date.today()
|
||||
|
||||
@@ -471,7 +469,7 @@ class RideRankingService:
|
||||
if deleted_snapshots[0] > 0:
|
||||
self.logger.info(f"Deleted {deleted_snapshots[0]} old ranking snapshots")
|
||||
|
||||
def get_ride_ranking_details(self, ride: Ride) -> Optional[Dict]:
|
||||
def get_ride_ranking_details(self, ride: Ride) -> dict | None:
|
||||
"""
|
||||
Get detailed ranking information for a specific ride.
|
||||
|
||||
|
||||
@@ -9,19 +9,20 @@ This service implements the filtering design specified in:
|
||||
backend/docs/ride_filtering_design.md
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from django.contrib.postgres.search import (
|
||||
SearchVector,
|
||||
SearchQuery,
|
||||
SearchRank,
|
||||
SearchVector,
|
||||
TrigramSimilarity,
|
||||
)
|
||||
from django.db import models
|
||||
from django.db.models import Q, F, Value
|
||||
from django.db.models import F, Q, Value
|
||||
from django.db.models.functions import Greatest
|
||||
from typing import Dict, List, Optional, Any
|
||||
|
||||
from apps.rides.models import Ride
|
||||
from apps.parks.models import Park
|
||||
from apps.rides.models import Ride
|
||||
from apps.rides.models.company import Company
|
||||
|
||||
|
||||
@@ -104,11 +105,11 @@ class RideSearchService:
|
||||
|
||||
def search_and_filter(
|
||||
self,
|
||||
filters: Dict[str, Any],
|
||||
filters: dict[str, Any],
|
||||
sort_by: str = "relevance",
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> Dict[str, Any]:
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Main search and filter method that combines all capabilities.
|
||||
|
||||
@@ -219,7 +220,7 @@ class RideSearchService:
|
||||
return queryset, final_rank
|
||||
|
||||
def _apply_basic_info_filters(
|
||||
self, queryset, filters: Dict[str, Any]
|
||||
self, queryset, filters: dict[str, Any]
|
||||
) -> models.QuerySet:
|
||||
"""Apply basic information filters."""
|
||||
|
||||
@@ -267,7 +268,7 @@ class RideSearchService:
|
||||
|
||||
return queryset
|
||||
|
||||
def _apply_date_filters(self, queryset, filters: Dict[str, Any]) -> models.QuerySet:
|
||||
def _apply_date_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
|
||||
"""Apply date range filters."""
|
||||
|
||||
# Opening date range
|
||||
@@ -297,7 +298,7 @@ class RideSearchService:
|
||||
return queryset
|
||||
|
||||
def _apply_height_safety_filters(
|
||||
self, queryset, filters: Dict[str, Any]
|
||||
self, queryset, filters: dict[str, Any]
|
||||
) -> models.QuerySet:
|
||||
"""Apply height and safety requirement filters."""
|
||||
|
||||
@@ -320,7 +321,7 @@ class RideSearchService:
|
||||
return queryset
|
||||
|
||||
def _apply_performance_filters(
|
||||
self, queryset, filters: Dict[str, Any]
|
||||
self, queryset, filters: dict[str, Any]
|
||||
) -> models.QuerySet:
|
||||
"""Apply performance metric filters."""
|
||||
|
||||
@@ -355,7 +356,7 @@ class RideSearchService:
|
||||
return queryset
|
||||
|
||||
def _apply_relationship_filters(
|
||||
self, queryset, filters: Dict[str, Any]
|
||||
self, queryset, filters: dict[str, Any]
|
||||
) -> models.QuerySet:
|
||||
"""Apply relationship filters (manufacturer, designer, ride model)."""
|
||||
|
||||
@@ -398,7 +399,7 @@ class RideSearchService:
|
||||
return queryset
|
||||
|
||||
def _apply_roller_coaster_filters(
|
||||
self, queryset, filters: Dict[str, Any]
|
||||
self, queryset, filters: dict[str, Any]
|
||||
) -> models.QuerySet:
|
||||
"""Apply roller coaster specific filters."""
|
||||
queryset = self._apply_numeric_range_filter(
|
||||
@@ -448,7 +449,7 @@ class RideSearchService:
|
||||
def _apply_numeric_range_filter(
|
||||
self,
|
||||
queryset,
|
||||
filters: Dict[str, Any],
|
||||
filters: dict[str, Any],
|
||||
filter_key: str,
|
||||
field_name: str,
|
||||
) -> models.QuerySet:
|
||||
@@ -466,7 +467,7 @@ class RideSearchService:
|
||||
return queryset
|
||||
|
||||
def _apply_company_filters(
|
||||
self, queryset, filters: Dict[str, Any]
|
||||
self, queryset, filters: dict[str, Any]
|
||||
) -> models.QuerySet:
|
||||
"""Apply company-related filters."""
|
||||
|
||||
@@ -522,8 +523,8 @@ class RideSearchService:
|
||||
) # Always add name as secondary sort
|
||||
|
||||
def _add_search_highlights(
|
||||
self, results: List[Ride], search_term: str
|
||||
) -> List[Ride]:
|
||||
self, results: list[Ride], search_term: str
|
||||
) -> list[Ride]:
|
||||
"""Add search highlights to results using SearchHeadline."""
|
||||
|
||||
if not search_term or not results:
|
||||
@@ -536,12 +537,12 @@ class RideSearchService:
|
||||
# (note: highlights would need to be processed at query time)
|
||||
for ride in results:
|
||||
# Store highlighted versions as dynamic attributes (for template use)
|
||||
setattr(ride, "highlighted_name", ride.name)
|
||||
setattr(ride, "highlighted_description", ride.description)
|
||||
ride.highlighted_name = ride.name
|
||||
ride.highlighted_description = ride.description
|
||||
|
||||
return results
|
||||
|
||||
def _get_applied_filters_summary(self, filters: Dict[str, Any]) -> Dict[str, Any]:
|
||||
def _get_applied_filters_summary(self, filters: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Generate a summary of applied filters for the frontend."""
|
||||
|
||||
applied = {}
|
||||
@@ -602,7 +603,7 @@ class RideSearchService:
|
||||
|
||||
def get_search_suggestions(
|
||||
self, query: str, limit: int = 10
|
||||
) -> List[Dict[str, Any]]:
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get search suggestions for autocomplete functionality.
|
||||
"""
|
||||
@@ -672,8 +673,8 @@ class RideSearchService:
|
||||
return suggestions[:limit]
|
||||
|
||||
def get_filter_options(
|
||||
self, filter_type: str, context_filters: Optional[Dict[str, Any]] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
self, filter_type: str, context_filters: dict[str, Any] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get available options for a specific filter type.
|
||||
Optionally filter options based on current context.
|
||||
@@ -716,7 +717,7 @@ class RideSearchService:
|
||||
# Add more filter options as needed
|
||||
return []
|
||||
|
||||
def _apply_all_filters(self, queryset, filters: Dict[str, Any]) -> models.QuerySet:
|
||||
def _apply_all_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
|
||||
"""Apply all filters except search ranking."""
|
||||
|
||||
queryset = self._apply_basic_info_filters(queryset, filters)
|
||||
|
||||
@@ -3,9 +3,9 @@ Services for ride status transitions and management.
|
||||
Following Django styleguide pattern for business logic encapsulation.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from django.db import transaction
|
||||
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
from django.db import transaction
|
||||
|
||||
from apps.rides.models import Ride
|
||||
|
||||
@@ -14,7 +14,7 @@ class RideStatusService:
|
||||
"""Service for managing ride status transitions using FSM."""
|
||||
|
||||
@staticmethod
|
||||
def open_ride(*, ride_id: int, user: Optional[AbstractBaseUser] = None) -> Ride:
|
||||
def open_ride(*, ride_id: int, user: AbstractBaseUser | None = None) -> Ride:
|
||||
"""
|
||||
Open a ride for operation.
|
||||
|
||||
@@ -35,7 +35,7 @@ class RideStatusService:
|
||||
|
||||
@staticmethod
|
||||
def close_ride_temporarily(
|
||||
*, ride_id: int, user: Optional[AbstractBaseUser] = None
|
||||
*, ride_id: int, user: AbstractBaseUser | None = None
|
||||
) -> Ride:
|
||||
"""
|
||||
Temporarily close a ride.
|
||||
@@ -57,7 +57,7 @@ class RideStatusService:
|
||||
|
||||
@staticmethod
|
||||
def mark_ride_sbno(
|
||||
*, ride_id: int, user: Optional[AbstractBaseUser] = None
|
||||
*, ride_id: int, user: AbstractBaseUser | None = None
|
||||
) -> Ride:
|
||||
"""
|
||||
Mark a ride as SBNO (Standing But Not Operating).
|
||||
@@ -83,7 +83,7 @@ class RideStatusService:
|
||||
ride_id: int,
|
||||
closing_date,
|
||||
post_closing_status: str,
|
||||
user: Optional[AbstractBaseUser] = None,
|
||||
user: AbstractBaseUser | None = None,
|
||||
) -> Ride:
|
||||
"""
|
||||
Mark a ride as closing with a specific date and post-closing status.
|
||||
@@ -112,7 +112,7 @@ class RideStatusService:
|
||||
|
||||
@staticmethod
|
||||
def close_ride_permanently(
|
||||
*, ride_id: int, user: Optional[AbstractBaseUser] = None
|
||||
*, ride_id: int, user: AbstractBaseUser | None = None
|
||||
) -> Ride:
|
||||
"""
|
||||
Permanently close a ride.
|
||||
@@ -133,7 +133,7 @@ class RideStatusService:
|
||||
return ride
|
||||
|
||||
@staticmethod
|
||||
def demolish_ride(*, ride_id: int, user: Optional[AbstractBaseUser] = None) -> Ride:
|
||||
def demolish_ride(*, ride_id: int, user: AbstractBaseUser | None = None) -> Ride:
|
||||
"""
|
||||
Mark a ride as demolished.
|
||||
|
||||
@@ -153,7 +153,7 @@ class RideStatusService:
|
||||
return ride
|
||||
|
||||
@staticmethod
|
||||
def relocate_ride(*, ride_id: int, user: Optional[AbstractBaseUser] = None) -> Ride:
|
||||
def relocate_ride(*, ride_id: int, user: AbstractBaseUser | None = None) -> Ride:
|
||||
"""
|
||||
Mark a ride as relocated.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user