mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-01-02 01:47:04 -05:00
feat: Implement initial schema and add various API, service, and management command enhancements across the application.
This commit is contained in:
@@ -4,4 +4,3 @@ from .location_service import RideLocationService
|
||||
from .media_service import RideMediaService
|
||||
|
||||
__all__ = ["RideLocationService", "RideMediaService", "RideService"]
|
||||
|
||||
|
||||
@@ -95,13 +95,13 @@ class SmartRideLoader:
|
||||
total_count = queryset.count()
|
||||
|
||||
# Get progressive batch
|
||||
rides = list(queryset[offset:offset + self.PROGRESSIVE_LOAD_SIZE])
|
||||
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
|
||||
"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]:
|
||||
@@ -148,8 +148,7 @@ class SmartRideLoader:
|
||||
|
||||
return count
|
||||
|
||||
def _get_client_side_data(self, filters: dict[str, Any] | None,
|
||||
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)
|
||||
@@ -158,45 +157,46 @@ class SmartRideLoader:
|
||||
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')
|
||||
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)
|
||||
"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]:
|
||||
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])
|
||||
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
|
||||
"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):
|
||||
@@ -205,118 +205,110 @@ class SmartRideLoader:
|
||||
|
||||
# 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'
|
||||
)
|
||||
"park", "park__location", "park_area", "manufacturer", "designer", "ride_model", "ride_model__manufacturer"
|
||||
).prefetch_related("coaster_stats")
|
||||
|
||||
if not filters:
|
||||
return queryset.order_by('name')
|
||||
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()
|
||||
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_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'])
|
||||
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'])
|
||||
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'])
|
||||
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 "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'])
|
||||
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'])
|
||||
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 "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 "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'])
|
||||
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 "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'])
|
||||
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 "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'])
|
||||
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 "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'])
|
||||
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 "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 "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'])
|
||||
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 "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'])
|
||||
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 "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'])
|
||||
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 "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 "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']:
|
||||
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)
|
||||
@@ -325,10 +317,12 @@ class SmartRideLoader:
|
||||
queryset = queryset.filter(q_objects)
|
||||
|
||||
# Apply ordering
|
||||
ordering = filters.get('ordering', 'name')
|
||||
if ordering in ['height_ft', '-height_ft', 'speed_mph', '-speed_mph']:
|
||||
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')
|
||||
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)
|
||||
@@ -342,99 +336,99 @@ class SmartRideLoader:
|
||||
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(),
|
||||
"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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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_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,
|
||||
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,
|
||||
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:
|
||||
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,
|
||||
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)
|
||||
@@ -448,267 +442,250 @@ class SmartRideLoader:
|
||||
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'))
|
||||
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'))
|
||||
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'))
|
||||
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'))
|
||||
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'))
|
||||
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'))
|
||||
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'))
|
||||
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'))
|
||||
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'))
|
||||
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'))
|
||||
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']
|
||||
}
|
||||
{"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']
|
||||
}
|
||||
{"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']
|
||||
"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']
|
||||
"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']
|
||||
"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
|
||||
{"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']
|
||||
}
|
||||
{"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
|
||||
{"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
|
||||
{"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']
|
||||
}
|
||||
{"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'),
|
||||
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'),
|
||||
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,
|
||||
"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'
|
||||
"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'
|
||||
"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'
|
||||
"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'
|
||||
"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'
|
||||
"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'
|
||||
"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'
|
||||
"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'
|
||||
"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'
|
||||
"inversions": {
|
||||
"min": coaster_stats["min_inversions"] or 0,
|
||||
"max": coaster_stats["max_inversions"] or 20,
|
||||
"step": 1,
|
||||
"unit": "inversions",
|
||||
},
|
||||
},
|
||||
'total_count': Ride.objects.count(),
|
||||
"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',
|
||||
"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]
|
||||
@@ -718,14 +695,14 @@ class SmartRideLoader:
|
||||
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',
|
||||
"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]
|
||||
@@ -735,19 +712,19 @@ class SmartRideLoader:
|
||||
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',
|
||||
"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]
|
||||
@@ -757,9 +734,9 @@ class SmartRideLoader:
|
||||
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)',
|
||||
"STEEL": "Steel",
|
||||
"WOOD": "Wood",
|
||||
"HYBRID": "Hybrid (Steel/Wood)",
|
||||
}
|
||||
if material in material_labels:
|
||||
return material_labels[material]
|
||||
@@ -769,15 +746,15 @@ class SmartRideLoader:
|
||||
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',
|
||||
"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]
|
||||
|
||||
@@ -69,9 +69,7 @@ class RideLocationService:
|
||||
return ride_location
|
||||
|
||||
@classmethod
|
||||
def update_ride_location(
|
||||
cls, ride_location: RideLocation, **updates
|
||||
) -> RideLocation:
|
||||
def update_ride_location(cls, ride_location: RideLocation, **updates) -> RideLocation:
|
||||
"""
|
||||
Update ride location with validation.
|
||||
|
||||
@@ -149,9 +147,7 @@ class RideLocationService:
|
||||
if park:
|
||||
queryset = queryset.filter(ride__park=park)
|
||||
|
||||
return list(
|
||||
queryset.select_related("ride", "ride__park").order_by("point__distance")
|
||||
)
|
||||
return list(queryset.select_related("ride", "ride__park").order_by("point__distance"))
|
||||
|
||||
@classmethod
|
||||
def get_ride_navigation_info(cls, ride_location: RideLocation) -> dict[str, Any]:
|
||||
@@ -249,9 +245,7 @@ class RideLocationService:
|
||||
# Rough conversion: 1 degree latitude ≈ 111,000 meters
|
||||
# 1 degree longitude varies by latitude, but we'll use a rough approximation
|
||||
lat_offset = offset[0] / 111000 # North offset in degrees
|
||||
lon_offset = offset[1] / (
|
||||
111000 * abs(park_location.latitude) * 0.01
|
||||
) # East offset
|
||||
lon_offset = offset[1] / (111000 * abs(park_location.latitude) * 0.01) # East offset
|
||||
|
||||
estimated_lat = park_location.latitude + lat_offset
|
||||
estimated_lon = park_location.longitude + lon_offset
|
||||
@@ -277,9 +271,7 @@ class RideLocationService:
|
||||
return updated_count
|
||||
|
||||
# Get all rides in the park that don't have precise coordinates
|
||||
ride_locations = RideLocation.objects.filter(
|
||||
ride__park=park, point__isnull=True
|
||||
).select_related("ride")
|
||||
ride_locations = RideLocation.objects.filter(ride__park=park, point__isnull=True).select_related("ride")
|
||||
|
||||
for ride_location in ride_locations:
|
||||
# Try to search for the specific ride within the park area
|
||||
@@ -312,22 +304,15 @@ class RideLocationService:
|
||||
# Look for results that might be the ride
|
||||
for result in results:
|
||||
display_name = result.get("display_name", "").lower()
|
||||
if (
|
||||
ride_location.ride.name.lower() in display_name
|
||||
and park.name.lower() in display_name
|
||||
):
|
||||
if ride_location.ride.name.lower() in display_name and park.name.lower() in display_name:
|
||||
# Update the ride location
|
||||
ride_location.set_coordinates(
|
||||
float(result["lat"]), float(result["lon"])
|
||||
)
|
||||
ride_location.set_coordinates(float(result["lat"]), float(result["lon"]))
|
||||
ride_location.save()
|
||||
updated_count += 1
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Error updating ride location for {ride_location.ride.name}: {str(e)}"
|
||||
)
|
||||
logger.warning(f"Error updating ride location for {ride_location.ride.name}: {str(e)}")
|
||||
continue
|
||||
|
||||
return updated_count
|
||||
@@ -346,9 +331,7 @@ class RideLocationService:
|
||||
area_map = {}
|
||||
|
||||
ride_locations = (
|
||||
RideLocation.objects.filter(ride__park=park)
|
||||
.select_related("ride")
|
||||
.order_by("park_area", "ride__name")
|
||||
RideLocation.objects.filter(ride__park=park).select_related("ride").order_by("park_area", "ride__name")
|
||||
)
|
||||
|
||||
for ride_location in ride_locations:
|
||||
|
||||
@@ -143,11 +143,7 @@ class RideMediaService:
|
||||
Returns:
|
||||
List of RidePhoto instances
|
||||
"""
|
||||
return list(
|
||||
ride.photos.filter(photo_type=photo_type, is_approved=True).order_by(
|
||||
"-created_at"
|
||||
)
|
||||
)
|
||||
return list(ride.photos.filter(photo_type=photo_type, is_approved=True).order_by("-created_at"))
|
||||
|
||||
@staticmethod
|
||||
def set_primary_photo(ride: Ride, photo: RidePhoto) -> bool:
|
||||
@@ -218,9 +214,7 @@ class RideMediaService:
|
||||
photo.image.delete(save=False)
|
||||
photo.delete()
|
||||
|
||||
logger.info(
|
||||
f"Photo {photo_id} deleted from ride {ride_slug} by user {deleted_by.username}"
|
||||
)
|
||||
logger.info(f"Photo {photo_id} deleted from ride {ride_slug} by user {deleted_by.username}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete photo {photo.pk}: {str(e)}")
|
||||
@@ -272,9 +266,7 @@ class RideMediaService:
|
||||
if RideMediaService.approve_photo(photo, approved_by):
|
||||
approved_count += 1
|
||||
|
||||
logger.info(
|
||||
f"Bulk approved {approved_count} photos by user {approved_by.username}"
|
||||
)
|
||||
logger.info(f"Bulk approved {approved_count} photos by user {approved_by.username}")
|
||||
return approved_count
|
||||
|
||||
@staticmethod
|
||||
@@ -289,9 +281,7 @@ class RideMediaService:
|
||||
List of construction RidePhoto instances ordered by date taken
|
||||
"""
|
||||
return list(
|
||||
ride.photos.filter(photo_type="construction", is_approved=True).order_by(
|
||||
"date_taken", "created_at"
|
||||
)
|
||||
ride.photos.filter(photo_type="construction", is_approved=True).order_by("date_taken", "created_at")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -53,9 +53,7 @@ class RideRankingService:
|
||||
Dictionary with statistics about the ranking calculation
|
||||
"""
|
||||
start_time = timezone.now()
|
||||
self.logger.info(
|
||||
f"Starting ranking calculation for category: {category or 'ALL'}"
|
||||
)
|
||||
self.logger.info(f"Starting ranking calculation for category: {category or 'ALL'}")
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
@@ -87,9 +85,7 @@ class RideRankingService:
|
||||
self._cleanup_old_data()
|
||||
|
||||
duration = (timezone.now() - start_time).total_seconds()
|
||||
self.logger.info(
|
||||
f"Ranking calculation completed in {duration:.2f} seconds"
|
||||
)
|
||||
self.logger.info(f"Ranking calculation completed in {duration:.2f} seconds")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
@@ -113,9 +109,7 @@ class RideRankingService:
|
||||
"""
|
||||
queryset = (
|
||||
Ride.objects.filter(status="OPERATING", reviews__is_published=True)
|
||||
.annotate(
|
||||
review_count=Count("reviews", filter=Q(reviews__is_published=True))
|
||||
)
|
||||
.annotate(review_count=Count("reviews", filter=Q(reviews__is_published=True)))
|
||||
.filter(review_count__gt=0)
|
||||
)
|
||||
|
||||
@@ -124,9 +118,7 @@ class RideRankingService:
|
||||
|
||||
return list(queryset.distinct())
|
||||
|
||||
def _calculate_all_comparisons(
|
||||
self, rides: list[Ride]
|
||||
) -> dict[tuple[int, int], RidePairComparison]:
|
||||
def _calculate_all_comparisons(self, rides: list[Ride]) -> dict[tuple[int, int], RidePairComparison]:
|
||||
"""
|
||||
Calculate pairwise comparisons for all ride pairs.
|
||||
|
||||
@@ -146,15 +138,11 @@ class RideRankingService:
|
||||
|
||||
processed += 1
|
||||
if processed % 100 == 0:
|
||||
self.logger.debug(
|
||||
f"Processed {processed}/{total_pairs} comparisons"
|
||||
)
|
||||
self.logger.debug(f"Processed {processed}/{total_pairs} comparisons")
|
||||
|
||||
return comparisons
|
||||
|
||||
def _calculate_pairwise_comparison(
|
||||
self, ride_a: Ride, ride_b: Ride
|
||||
) -> RidePairComparison | None:
|
||||
def _calculate_pairwise_comparison(self, ride_a: Ride, ride_b: Ride) -> RidePairComparison | None:
|
||||
"""
|
||||
Calculate the pairwise comparison between two rides.
|
||||
|
||||
@@ -163,15 +151,11 @@ class RideRankingService:
|
||||
"""
|
||||
# Get mutual riders (users who have rated both rides)
|
||||
ride_a_reviewers = set(
|
||||
RideReview.objects.filter(ride=ride_a, is_published=True).values_list(
|
||||
"user_id", flat=True
|
||||
)
|
||||
RideReview.objects.filter(ride=ride_a, is_published=True).values_list("user_id", flat=True)
|
||||
)
|
||||
|
||||
ride_b_reviewers = set(
|
||||
RideReview.objects.filter(ride=ride_b, is_published=True).values_list(
|
||||
"user_id", flat=True
|
||||
)
|
||||
RideReview.objects.filter(ride=ride_b, is_published=True).values_list("user_id", flat=True)
|
||||
)
|
||||
|
||||
mutual_riders = ride_a_reviewers & ride_b_reviewers
|
||||
@@ -183,16 +167,12 @@ class RideRankingService:
|
||||
# Get ratings from mutual riders
|
||||
ride_a_ratings = {
|
||||
review.user_id: review.rating
|
||||
for review in RideReview.objects.filter(
|
||||
ride=ride_a, user_id__in=mutual_riders, is_published=True
|
||||
)
|
||||
for review in RideReview.objects.filter(ride=ride_a, user_id__in=mutual_riders, is_published=True)
|
||||
}
|
||||
|
||||
ride_b_ratings = {
|
||||
review.user_id: review.rating
|
||||
for review in RideReview.objects.filter(
|
||||
ride=ride_b, user_id__in=mutual_riders, is_published=True
|
||||
)
|
||||
for review in RideReview.objects.filter(ride=ride_b, user_id__in=mutual_riders, is_published=True)
|
||||
}
|
||||
|
||||
# Count wins and ties
|
||||
@@ -212,12 +192,8 @@ class RideRankingService:
|
||||
ties += 1
|
||||
|
||||
# Calculate average ratings from mutual riders
|
||||
ride_a_avg = (
|
||||
sum(ride_a_ratings.values()) / len(ride_a_ratings) if ride_a_ratings else 0
|
||||
)
|
||||
ride_b_avg = (
|
||||
sum(ride_b_ratings.values()) / len(ride_b_ratings) if ride_b_ratings else 0
|
||||
)
|
||||
ride_a_avg = sum(ride_a_ratings.values()) / len(ride_a_ratings) if ride_a_ratings else 0
|
||||
ride_b_avg = sum(ride_b_ratings.values()) / len(ride_b_ratings) if ride_b_ratings else 0
|
||||
|
||||
# Create or update comparison record
|
||||
comparison, created = RidePairComparison.objects.update_or_create(
|
||||
@@ -228,16 +204,8 @@ class RideRankingService:
|
||||
"ride_b_wins": ride_b_wins if ride_a.id < ride_b.id else ride_a_wins,
|
||||
"ties": ties,
|
||||
"mutual_riders_count": len(mutual_riders),
|
||||
"ride_a_avg_rating": (
|
||||
Decimal(str(ride_a_avg))
|
||||
if ride_a.id < ride_b.id
|
||||
else Decimal(str(ride_b_avg))
|
||||
),
|
||||
"ride_b_avg_rating": (
|
||||
Decimal(str(ride_b_avg))
|
||||
if ride_a.id < ride_b.id
|
||||
else Decimal(str(ride_a_avg))
|
||||
),
|
||||
"ride_a_avg_rating": (Decimal(str(ride_a_avg)) if ride_a.id < ride_b.id else Decimal(str(ride_b_avg))),
|
||||
"ride_b_avg_rating": (Decimal(str(ride_b_avg)) if ride_a.id < ride_b.id else Decimal(str(ride_a_avg))),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -294,16 +262,12 @@ class RideRankingService:
|
||||
# Calculate winning percentage (ties count as 0.5)
|
||||
total_comparisons = wins + losses + ties
|
||||
if total_comparisons > 0:
|
||||
winning_percentage = Decimal(
|
||||
str((wins + 0.5 * ties) / total_comparisons)
|
||||
)
|
||||
winning_percentage = Decimal(str((wins + 0.5 * ties) / total_comparisons))
|
||||
else:
|
||||
winning_percentage = Decimal("0.5")
|
||||
|
||||
# Get average rating and reviewer count
|
||||
ride_stats = RideReview.objects.filter(
|
||||
ride=ride, is_published=True
|
||||
).aggregate(
|
||||
ride_stats = RideReview.objects.filter(ride=ride, is_published=True).aggregate(
|
||||
avg_rating=Avg("rating"), reviewer_count=Count("user", distinct=True)
|
||||
)
|
||||
|
||||
@@ -356,11 +320,7 @@ class RideRankingService:
|
||||
tied_group = [rankings[i]]
|
||||
j = i + 1
|
||||
|
||||
while (
|
||||
j < len(rankings)
|
||||
and rankings[j]["winning_percentage"]
|
||||
== rankings[i]["winning_percentage"]
|
||||
):
|
||||
while j < len(rankings) and rankings[j]["winning_percentage"] == rankings[i]["winning_percentage"]:
|
||||
tied_group.append(rankings[j])
|
||||
j += 1
|
||||
|
||||
@@ -462,9 +422,7 @@ class RideRankingService:
|
||||
cutoff_date = timezone.now() - timezone.timedelta(days=days_to_keep)
|
||||
|
||||
# Delete old snapshots
|
||||
deleted_snapshots = RankingSnapshot.objects.filter(
|
||||
snapshot_date__lt=cutoff_date.date()
|
||||
).delete()
|
||||
deleted_snapshots = RankingSnapshot.objects.filter(snapshot_date__lt=cutoff_date.date()).delete()
|
||||
|
||||
if deleted_snapshots[0] > 0:
|
||||
self.logger.info(f"Deleted {deleted_snapshots[0]} old ranking snapshots")
|
||||
@@ -486,9 +444,7 @@ class RideRankingService:
|
||||
)
|
||||
|
||||
# Get ranking history
|
||||
history = RankingSnapshot.objects.filter(ride=ride).order_by(
|
||||
"-snapshot_date"
|
||||
)[:30]
|
||||
history = RankingSnapshot.objects.filter(ride=ride).order_by("-snapshot_date")[:30]
|
||||
|
||||
return {
|
||||
"current_rank": ranking.rank,
|
||||
@@ -501,32 +457,18 @@ class RideRankingService:
|
||||
"last_calculated": ranking.last_calculated,
|
||||
"head_to_head": [
|
||||
{
|
||||
"opponent": (
|
||||
comp.ride_b if comp.ride_a_id == ride.id else comp.ride_a
|
||||
),
|
||||
"opponent": (comp.ride_b if comp.ride_a_id == ride.id else comp.ride_a),
|
||||
"result": (
|
||||
"win"
|
||||
if (
|
||||
(
|
||||
comp.ride_a_id == ride.id
|
||||
and comp.ride_a_wins > comp.ride_b_wins
|
||||
)
|
||||
or (
|
||||
comp.ride_b_id == ride.id
|
||||
and comp.ride_b_wins > comp.ride_a_wins
|
||||
)
|
||||
(comp.ride_a_id == ride.id and comp.ride_a_wins > comp.ride_b_wins)
|
||||
or (comp.ride_b_id == ride.id and comp.ride_b_wins > comp.ride_a_wins)
|
||||
)
|
||||
else (
|
||||
"loss"
|
||||
if (
|
||||
(
|
||||
comp.ride_a_id == ride.id
|
||||
and comp.ride_a_wins < comp.ride_b_wins
|
||||
)
|
||||
or (
|
||||
comp.ride_b_id == ride.id
|
||||
and comp.ride_b_wins < comp.ride_a_wins
|
||||
)
|
||||
(comp.ride_a_id == ride.id and comp.ride_a_wins < comp.ride_b_wins)
|
||||
or (comp.ride_b_id == ride.id and comp.ride_b_wins < comp.ride_a_wins)
|
||||
)
|
||||
else "tie"
|
||||
)
|
||||
|
||||
@@ -127,9 +127,7 @@ class RideSearchService:
|
||||
|
||||
# Apply text search with ranking
|
||||
if filters.get("global_search"):
|
||||
queryset, search_rank = self._apply_full_text_search(
|
||||
queryset, filters["global_search"]
|
||||
)
|
||||
queryset, search_rank = self._apply_full_text_search(queryset, filters["global_search"])
|
||||
search_metadata["search_applied"] = True
|
||||
search_metadata["search_term"] = filters["global_search"]
|
||||
else:
|
||||
@@ -176,9 +174,7 @@ class RideSearchService:
|
||||
"applied_filters": self._get_applied_filters_summary(filters),
|
||||
}
|
||||
|
||||
def _apply_full_text_search(
|
||||
self, queryset, search_term: str
|
||||
) -> tuple[models.QuerySet, models.Expression]:
|
||||
def _apply_full_text_search(self, queryset, search_term: str) -> tuple[models.QuerySet, models.Expression]:
|
||||
"""
|
||||
Apply PostgreSQL full-text search with ranking and fuzzy matching.
|
||||
"""
|
||||
@@ -201,17 +197,14 @@ class RideSearchService:
|
||||
search_query = SearchQuery(search_term, config="english")
|
||||
|
||||
# Calculate search rank
|
||||
search_rank = SearchRank(
|
||||
search_vector, search_query, weights=self.SEARCH_RANK_WEIGHTS
|
||||
)
|
||||
search_rank = SearchRank(search_vector, search_query, weights=self.SEARCH_RANK_WEIGHTS)
|
||||
|
||||
# Apply trigram similarity for fuzzy matching on name
|
||||
trigram_similarity = TrigramSimilarity("name", search_term)
|
||||
|
||||
# Combine full-text search with trigram similarity
|
||||
queryset = queryset.annotate(trigram_similarity=trigram_similarity).filter(
|
||||
Q(search_vector=search_query)
|
||||
| Q(trigram_similarity__gte=self.TRIGRAM_SIMILARITY_THRESHOLD)
|
||||
Q(search_vector=search_query) | Q(trigram_similarity__gte=self.TRIGRAM_SIMILARITY_THRESHOLD)
|
||||
)
|
||||
|
||||
# Use the greatest of search rank and trigram similarity for final ranking
|
||||
@@ -219,36 +212,22 @@ class RideSearchService:
|
||||
|
||||
return queryset, final_rank
|
||||
|
||||
def _apply_basic_info_filters(
|
||||
self, queryset, filters: dict[str, Any]
|
||||
) -> models.QuerySet:
|
||||
def _apply_basic_info_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
|
||||
"""Apply basic information filters."""
|
||||
|
||||
# Category filter (multi-select)
|
||||
if filters.get("category"):
|
||||
categories = (
|
||||
filters["category"]
|
||||
if isinstance(filters["category"], list)
|
||||
else [filters["category"]]
|
||||
)
|
||||
categories = filters["category"] if isinstance(filters["category"], list) else [filters["category"]]
|
||||
queryset = queryset.filter(category__in=categories)
|
||||
|
||||
# Status filter (multi-select)
|
||||
if filters.get("status"):
|
||||
statuses = (
|
||||
filters["status"]
|
||||
if isinstance(filters["status"], list)
|
||||
else [filters["status"]]
|
||||
)
|
||||
statuses = filters["status"] if isinstance(filters["status"], list) else [filters["status"]]
|
||||
queryset = queryset.filter(status__in=statuses)
|
||||
|
||||
# Park filter (multi-select)
|
||||
if filters.get("park"):
|
||||
parks = (
|
||||
filters["park"]
|
||||
if isinstance(filters["park"], list)
|
||||
else [filters["park"]]
|
||||
)
|
||||
parks = filters["park"] if isinstance(filters["park"], list) else [filters["park"]]
|
||||
if isinstance(parks[0], str): # If slugs provided
|
||||
queryset = queryset.filter(park__slug__in=parks)
|
||||
else: # If IDs provided
|
||||
@@ -256,11 +235,7 @@ class RideSearchService:
|
||||
|
||||
# Park area filter (multi-select)
|
||||
if filters.get("park_area"):
|
||||
areas = (
|
||||
filters["park_area"]
|
||||
if isinstance(filters["park_area"], list)
|
||||
else [filters["park_area"]]
|
||||
)
|
||||
areas = filters["park_area"] if isinstance(filters["park_area"], list) else [filters["park_area"]]
|
||||
if isinstance(areas[0], str): # If slugs provided
|
||||
queryset = queryset.filter(park_area__slug__in=areas)
|
||||
else: # If IDs provided
|
||||
@@ -297,9 +272,7 @@ class RideSearchService:
|
||||
|
||||
return queryset
|
||||
|
||||
def _apply_height_safety_filters(
|
||||
self, queryset, filters: dict[str, Any]
|
||||
) -> models.QuerySet:
|
||||
def _apply_height_safety_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
|
||||
"""Apply height and safety requirement filters."""
|
||||
|
||||
# Minimum height range
|
||||
@@ -320,9 +293,7 @@ class RideSearchService:
|
||||
|
||||
return queryset
|
||||
|
||||
def _apply_performance_filters(
|
||||
self, queryset, filters: dict[str, Any]
|
||||
) -> models.QuerySet:
|
||||
def _apply_performance_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
|
||||
"""Apply performance metric filters."""
|
||||
|
||||
# Capacity range
|
||||
@@ -337,13 +308,9 @@ class RideSearchService:
|
||||
if filters.get("duration_range"):
|
||||
duration_range = filters["duration_range"]
|
||||
if duration_range.get("min") is not None:
|
||||
queryset = queryset.filter(
|
||||
ride_duration_seconds__gte=duration_range["min"]
|
||||
)
|
||||
queryset = queryset.filter(ride_duration_seconds__gte=duration_range["min"])
|
||||
if duration_range.get("max") is not None:
|
||||
queryset = queryset.filter(
|
||||
ride_duration_seconds__lte=duration_range["max"]
|
||||
)
|
||||
queryset = queryset.filter(ride_duration_seconds__lte=duration_range["max"])
|
||||
|
||||
# Rating range
|
||||
if filters.get("rating_range"):
|
||||
@@ -355,17 +322,13 @@ class RideSearchService:
|
||||
|
||||
return queryset
|
||||
|
||||
def _apply_relationship_filters(
|
||||
self, queryset, filters: dict[str, Any]
|
||||
) -> models.QuerySet:
|
||||
def _apply_relationship_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
|
||||
"""Apply relationship filters (manufacturer, designer, ride model)."""
|
||||
|
||||
# Manufacturer filter (multi-select)
|
||||
if filters.get("manufacturer"):
|
||||
manufacturers = (
|
||||
filters["manufacturer"]
|
||||
if isinstance(filters["manufacturer"], list)
|
||||
else [filters["manufacturer"]]
|
||||
filters["manufacturer"] if isinstance(filters["manufacturer"], list) else [filters["manufacturer"]]
|
||||
)
|
||||
if isinstance(manufacturers[0], str): # If slugs provided
|
||||
queryset = queryset.filter(manufacturer__slug__in=manufacturers)
|
||||
@@ -374,11 +337,7 @@ class RideSearchService:
|
||||
|
||||
# Designer filter (multi-select)
|
||||
if filters.get("designer"):
|
||||
designers = (
|
||||
filters["designer"]
|
||||
if isinstance(filters["designer"], list)
|
||||
else [filters["designer"]]
|
||||
)
|
||||
designers = filters["designer"] if isinstance(filters["designer"], list) else [filters["designer"]]
|
||||
if isinstance(designers[0], str): # If slugs provided
|
||||
queryset = queryset.filter(designer__slug__in=designers)
|
||||
else: # If IDs provided
|
||||
@@ -386,11 +345,7 @@ class RideSearchService:
|
||||
|
||||
# Ride model filter (multi-select)
|
||||
if filters.get("ride_model"):
|
||||
models_list = (
|
||||
filters["ride_model"]
|
||||
if isinstance(filters["ride_model"], list)
|
||||
else [filters["ride_model"]]
|
||||
)
|
||||
models_list = filters["ride_model"] if isinstance(filters["ride_model"], list) else [filters["ride_model"]]
|
||||
if isinstance(models_list[0], str): # If slugs provided
|
||||
queryset = queryset.filter(ride_model__slug__in=models_list)
|
||||
else: # If IDs provided
|
||||
@@ -398,9 +353,7 @@ class RideSearchService:
|
||||
|
||||
return queryset
|
||||
|
||||
def _apply_roller_coaster_filters(
|
||||
self, queryset, filters: dict[str, Any]
|
||||
) -> models.QuerySet:
|
||||
def _apply_roller_coaster_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
|
||||
"""Apply roller coaster specific filters."""
|
||||
queryset = self._apply_numeric_range_filter(
|
||||
queryset, filters, "height_ft_range", "rollercoasterstats__height_ft"
|
||||
@@ -426,14 +379,8 @@ class RideSearchService:
|
||||
|
||||
# Coaster type filter (multi-select)
|
||||
if filters.get("coaster_type"):
|
||||
types = (
|
||||
filters["coaster_type"]
|
||||
if isinstance(filters["coaster_type"], list)
|
||||
else [filters["coaster_type"]]
|
||||
)
|
||||
queryset = queryset.filter(
|
||||
rollercoasterstats__roller_coaster_type__in=types
|
||||
)
|
||||
types = filters["coaster_type"] if isinstance(filters["coaster_type"], list) else [filters["coaster_type"]]
|
||||
queryset = queryset.filter(rollercoasterstats__roller_coaster_type__in=types)
|
||||
|
||||
# Propulsion system filter (multi-select)
|
||||
if filters.get("propulsion_system"):
|
||||
@@ -457,18 +404,12 @@ class RideSearchService:
|
||||
if filters.get(filter_key):
|
||||
range_filter = filters[filter_key]
|
||||
if range_filter.get("min") is not None:
|
||||
queryset = queryset.filter(
|
||||
**{f"{field_name}__gte": range_filter["min"]}
|
||||
)
|
||||
queryset = queryset.filter(**{f"{field_name}__gte": range_filter["min"]})
|
||||
if range_filter.get("max") is not None:
|
||||
queryset = queryset.filter(
|
||||
**{f"{field_name}__lte": range_filter["max"]}
|
||||
)
|
||||
queryset = queryset.filter(**{f"{field_name}__lte": range_filter["max"]})
|
||||
return queryset
|
||||
|
||||
def _apply_company_filters(
|
||||
self, queryset, filters: dict[str, Any]
|
||||
) -> models.QuerySet:
|
||||
def _apply_company_filters(self, queryset, filters: dict[str, Any]) -> models.QuerySet:
|
||||
"""Apply company-related filters."""
|
||||
|
||||
# Manufacturer roles filter
|
||||
@@ -518,13 +459,9 @@ class RideSearchService:
|
||||
return queryset.order_by("-search_rank", "name")
|
||||
|
||||
# Apply the sorting
|
||||
return queryset.order_by(
|
||||
sort_field, "name"
|
||||
) # Always add name as secondary sort
|
||||
return queryset.order_by(sort_field, "name") # Always add name as secondary sort
|
||||
|
||||
def _add_search_highlights(
|
||||
self, results: list[Ride], search_term: str
|
||||
) -> list[Ride]:
|
||||
def _add_search_highlights(self, results: list[Ride], search_term: str) -> list[Ride]:
|
||||
"""Add search highlights to results using SearchHeadline."""
|
||||
|
||||
if not search_term or not results:
|
||||
@@ -601,9 +538,7 @@ class RideSearchService:
|
||||
else:
|
||||
raise ValueError(f"Unknown filter key: {filter_key}")
|
||||
|
||||
def get_search_suggestions(
|
||||
self, query: str, limit: int = 10
|
||||
) -> list[dict[str, Any]]:
|
||||
def get_search_suggestions(self, query: str, limit: int = 10) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get search suggestions for autocomplete functionality.
|
||||
"""
|
||||
@@ -686,17 +621,11 @@ class RideSearchService:
|
||||
# Apply context filters to narrow down options
|
||||
if context_filters:
|
||||
temp_filters = context_filters.copy()
|
||||
temp_filters.pop(
|
||||
filter_type, None
|
||||
) # Remove the filter we're getting options for
|
||||
temp_filters.pop(filter_type, None) # Remove the filter we're getting options for
|
||||
base_queryset = self._apply_all_filters(base_queryset, temp_filters)
|
||||
|
||||
if filter_type == "park":
|
||||
return list(
|
||||
base_queryset.values("park__name", "park__slug")
|
||||
.distinct()
|
||||
.order_by("park__name")
|
||||
)
|
||||
return list(base_queryset.values("park__name", "park__slug").distinct().order_by("park__name"))
|
||||
|
||||
elif filter_type == "manufacturer":
|
||||
return list(
|
||||
|
||||
@@ -3,7 +3,6 @@ Services for ride status transitions and management.
|
||||
Following Django styleguide pattern for business logic encapsulation.
|
||||
"""
|
||||
|
||||
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
from django.db import transaction
|
||||
|
||||
@@ -34,9 +33,7 @@ class RideStatusService:
|
||||
return ride
|
||||
|
||||
@staticmethod
|
||||
def close_ride_temporarily(
|
||||
*, ride_id: int, user: AbstractBaseUser | None = None
|
||||
) -> Ride:
|
||||
def close_ride_temporarily(*, ride_id: int, user: AbstractBaseUser | None = None) -> Ride:
|
||||
"""
|
||||
Temporarily close a ride.
|
||||
|
||||
@@ -56,9 +53,7 @@ class RideStatusService:
|
||||
return ride
|
||||
|
||||
@staticmethod
|
||||
def mark_ride_sbno(
|
||||
*, ride_id: int, user: AbstractBaseUser | None = None
|
||||
) -> Ride:
|
||||
def mark_ride_sbno(*, ride_id: int, user: AbstractBaseUser | None = None) -> Ride:
|
||||
"""
|
||||
Mark a ride as SBNO (Standing But Not Operating).
|
||||
|
||||
@@ -111,9 +106,7 @@ class RideStatusService:
|
||||
return ride
|
||||
|
||||
@staticmethod
|
||||
def close_ride_permanently(
|
||||
*, ride_id: int, user: AbstractBaseUser | None = None
|
||||
) -> Ride:
|
||||
def close_ride_permanently(*, ride_id: int, user: AbstractBaseUser | None = None) -> Ride:
|
||||
"""
|
||||
Permanently close a ride.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user