feat: Implement MFA authentication, add ride statistics model, and update various services, APIs, and tests across the application.

This commit is contained in:
pacnpal
2025-12-28 17:32:53 -05:00
parent aa56c46c27
commit c95f99ca10
452 changed files with 7948 additions and 6073 deletions

View File

@@ -5,10 +5,12 @@ This module provides intelligent data loading capabilities for the hybrid filter
optimizing database queries and implementing progressive loading strategies.
"""
from typing import Dict, Optional, Any
from django.db import models
from django.core.cache import cache
from typing import Any
from django.conf import settings
from django.core.cache import cache
from django.db import models
from apps.parks.models import Park
@@ -17,19 +19,19 @@ class SmartParkLoader:
Intelligent park data loader that optimizes queries based on filtering requirements.
Implements progressive loading and smart caching strategies.
"""
# Cache configuration
CACHE_TIMEOUT = getattr(settings, 'HYBRID_FILTER_CACHE_TIMEOUT', 300) # 5 minutes
CACHE_KEY_PREFIX = 'hybrid_parks'
# Progressive loading thresholds
INITIAL_LOAD_SIZE = 50
PROGRESSIVE_LOAD_SIZE = 25
MAX_CLIENT_SIDE_RECORDS = 200
def __init__(self):
self.base_queryset = self._get_optimized_queryset()
def _get_optimized_queryset(self) -> models.QuerySet:
"""Get optimized base queryset with all necessary prefetches."""
return Park.objects.select_related(
@@ -43,31 +45,31 @@ class SmartParkLoader:
# Only include operating and temporarily closed parks by default
status__in=['OPERATING', 'CLOSED_TEMP']
).order_by('name')
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 park data load with smart filtering decisions.
Args:
filters: Optional filters to apply
Returns:
Dictionary containing parks data and metadata
"""
cache_key = self._generate_cache_key('initial', filters)
cached_result = cache.get(cache_key)
if cached_result:
return cached_result
# Apply filters if provided
queryset = self.base_queryset
if filters:
queryset = self._apply_filters(queryset, filters)
# Get total count for pagination decisions
total_count = queryset.count()
# Determine loading strategy
if total_count <= self.MAX_CLIENT_SIDE_RECORDS:
# Load all data for client-side filtering
@@ -79,7 +81,7 @@ class SmartParkLoader:
parks = list(queryset[:self.INITIAL_LOAD_SIZE])
strategy = 'server_side'
has_more = total_count > self.INITIAL_LOAD_SIZE
result = {
'parks': parks,
'total_count': total_count,
@@ -88,163 +90,163 @@ class SmartParkLoader:
'next_offset': len(parks) if has_more else None,
'filter_metadata': self._get_filter_metadata(queryset),
}
# Cache the result
cache.set(cache_key, result, self.CACHE_TIMEOUT)
return result
def get_progressive_load(
self,
offset: int,
filters: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
self,
offset: int,
filters: dict[str, Any] | None = None
) -> dict[str, Any]:
"""
Get next batch of parks for progressive loading.
Args:
offset: Starting offset for the batch
filters: Optional filters to apply
Returns:
Dictionary containing parks data and metadata
"""
cache_key = self._generate_cache_key(f'progressive_{offset}', filters)
cached_result = cache.get(cache_key)
if cached_result:
return cached_result
# Apply filters if provided
queryset = self.base_queryset
if filters:
queryset = self._apply_filters(queryset, filters)
# Get the batch
end_offset = offset + self.PROGRESSIVE_LOAD_SIZE
parks = list(queryset[offset:end_offset])
# Check if there are more records
total_count = queryset.count()
has_more = end_offset < total_count
result = {
'parks': parks,
'total_count': total_count,
'has_more': has_more,
'next_offset': end_offset if has_more else None,
}
# Cache the result
cache.set(cache_key, result, self.CACHE_TIMEOUT)
return result
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 metadata about available filter options.
Args:
filters: Current filters to scope the metadata
Returns:
Dictionary containing filter metadata
"""
cache_key = self._generate_cache_key('metadata', filters)
cached_result = cache.get(cache_key)
if cached_result:
return cached_result
# Apply filters if provided
queryset = self.base_queryset
if filters:
queryset = self._apply_filters(queryset, filters)
result = self._get_filter_metadata(queryset)
# Cache the result
cache.set(cache_key, result, self.CACHE_TIMEOUT)
return result
def _apply_filters(self, queryset: models.QuerySet, filters: Dict[str, Any]) -> models.QuerySet:
def _apply_filters(self, queryset: models.QuerySet, filters: dict[str, Any]) -> models.QuerySet:
"""Apply filters to the queryset."""
# Status filter
if 'status' in filters and filters['status']:
if isinstance(filters['status'], list):
queryset = queryset.filter(status__in=filters['status'])
else:
queryset = queryset.filter(status=filters['status'])
# Park type filter
if 'park_type' in filters and filters['park_type']:
if isinstance(filters['park_type'], list):
queryset = queryset.filter(park_type__in=filters['park_type'])
else:
queryset = queryset.filter(park_type=filters['park_type'])
# Country filter
if 'country' in filters and filters['country']:
queryset = queryset.filter(location__country__in=filters['country'])
# State filter
if 'state' in filters and filters['state']:
queryset = queryset.filter(location__state__in=filters['state'])
# Opening year range
if 'opening_year_min' in filters and filters['opening_year_min']:
queryset = queryset.filter(opening_year__gte=filters['opening_year_min'])
if 'opening_year_max' in filters and filters['opening_year_max']:
queryset = queryset.filter(opening_year__lte=filters['opening_year_max'])
# Size range
if 'size_min' in filters and filters['size_min']:
queryset = queryset.filter(size_acres__gte=filters['size_min'])
if 'size_max' in filters and filters['size_max']:
queryset = queryset.filter(size_acres__lte=filters['size_max'])
# Rating range
if 'rating_min' in filters and filters['rating_min']:
queryset = queryset.filter(average_rating__gte=filters['rating_min'])
if 'rating_max' in filters and filters['rating_max']:
queryset = queryset.filter(average_rating__lte=filters['rating_max'])
# Ride count range
if 'ride_count_min' in filters and filters['ride_count_min']:
queryset = queryset.filter(ride_count__gte=filters['ride_count_min'])
if 'ride_count_max' in filters and filters['ride_count_max']:
queryset = queryset.filter(ride_count__lte=filters['ride_count_max'])
# Coaster count range
if 'coaster_count_min' in filters and filters['coaster_count_min']:
queryset = queryset.filter(coaster_count__gte=filters['coaster_count_min'])
if 'coaster_count_max' in filters and filters['coaster_count_max']:
queryset = queryset.filter(coaster_count__lte=filters['coaster_count_max'])
# Operator filter
if 'operator' in filters and filters['operator']:
if isinstance(filters['operator'], list):
queryset = queryset.filter(operator__slug__in=filters['operator'])
else:
queryset = queryset.filter(operator__slug=filters['operator'])
# Search query
if 'search' in filters and filters['search']:
search_term = filters['search'].lower()
queryset = queryset.filter(search_text__icontains=search_term)
return queryset
def _get_filter_metadata(self, queryset: models.QuerySet) -> Dict[str, Any]:
def _get_filter_metadata(self, queryset: models.QuerySet) -> dict[str, Any]:
"""Generate filter metadata from the current queryset."""
# Get distinct values for categorical filters with counts
countries_data = list(
queryset.values('location__country')
@@ -252,27 +254,27 @@ class SmartParkLoader:
.annotate(count=models.Count('id'))
.order_by('location__country')
)
states_data = list(
queryset.values('location__state')
.exclude(location__state__isnull=True)
.annotate(count=models.Count('id'))
.order_by('location__state')
)
park_types_data = list(
queryset.values('park_type')
.exclude(park_type__isnull=True)
.annotate(count=models.Count('id'))
.order_by('park_type')
)
statuses_data = list(
queryset.values('status')
.annotate(count=models.Count('id'))
.order_by('status')
)
operators_data = list(
queryset.select_related('operator')
.values('operator__id', 'operator__name', 'operator__slug')
@@ -280,7 +282,7 @@ class SmartParkLoader:
.annotate(count=models.Count('id'))
.order_by('operator__name')
)
# Convert to frontend-expected format with value/label/count
countries = [
{
@@ -290,7 +292,7 @@ class SmartParkLoader:
}
for item in countries_data
]
states = [
{
'value': item['location__state'],
@@ -299,7 +301,7 @@ class SmartParkLoader:
}
for item in states_data
]
park_types = [
{
'value': item['park_type'],
@@ -308,7 +310,7 @@ class SmartParkLoader:
}
for item in park_types_data
]
statuses = [
{
'value': item['status'],
@@ -317,7 +319,7 @@ class SmartParkLoader:
}
for item in statuses_data
]
operators = [
{
'value': item['operator__slug'],
@@ -326,7 +328,7 @@ class SmartParkLoader:
}
for item in operators_data
]
# Get ranges for numerical filters
aggregates = queryset.aggregate(
opening_year_min=models.Min('opening_year'),
@@ -340,7 +342,7 @@ class SmartParkLoader:
coaster_count_min=models.Min('coaster_count'),
coaster_count_max=models.Max('coaster_count'),
)
return {
'categorical': {
'countries': countries,
@@ -383,7 +385,7 @@ class SmartParkLoader:
},
'total_count': queryset.count(),
}
def _get_status_label(self, status: str) -> str:
"""Convert status code to human-readable label."""
status_labels = {
@@ -396,19 +398,19 @@ class SmartParkLoader:
return status_labels[status]
else:
raise ValueError(f"Unknown park status: {status}")
def _generate_cache_key(self, operation: str, filters: Optional[Dict[str, Any]] = None) -> str:
def _generate_cache_key(self, operation: str, filters: dict[str, Any] | None = None) -> str:
"""Generate cache key for the given operation and filters."""
key_parts = [self.CACHE_KEY_PREFIX, operation]
if filters:
# Create a consistent string representation of filters
filter_str = '_'.join(f"{k}:{v}" for k, v in sorted(filters.items()) if v)
key_parts.append(filter_str)
return '_'.join(key_parts)
def invalidate_cache(self, filters: Optional[Dict[str, Any]] = None) -> None:
def invalidate_cache(self, filters: dict[str, Any] | None = None) -> None:
"""Invalidate cached data for the given filters."""
# This is a simplified implementation
# In production, you might want to use cache versioning or tags
@@ -416,11 +418,11 @@ class SmartParkLoader:
self._generate_cache_key('initial', filters),
self._generate_cache_key('metadata', filters),
]
# Also invalidate progressive load caches
for offset in range(0, 1000, self.PROGRESSIVE_LOAD_SIZE):
cache_keys.append(self._generate_cache_key(f'progressive_{offset}', filters))
cache.delete_many(cache_keys)