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,8 +1,8 @@
|
||||
from .roadtrip import RoadTripService
|
||||
from .park_management import ParkService
|
||||
from .location_service import ParkLocationService
|
||||
from .filter_service import ParkFilterService
|
||||
from .location_service import ParkLocationService
|
||||
from .media_service import ParkMediaService
|
||||
from .park_management import ParkService
|
||||
from .roadtrip import RoadTripService
|
||||
|
||||
__all__ = [
|
||||
"RoadTripService",
|
||||
|
||||
@@ -5,11 +5,13 @@ Provides filtering functionality, aggregations, and caching for park filters.
|
||||
This service handles complex filter logic and provides useful filter statistics.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
from django.db.models import QuerySet, Count, Q
|
||||
from django.core.cache import cache
|
||||
from typing import Any
|
||||
|
||||
from django.conf import settings
|
||||
from ..models import Park, Company
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Count, Q, QuerySet
|
||||
|
||||
from ..models import Company, Park
|
||||
from ..querysets import get_base_park_queryset
|
||||
|
||||
|
||||
@@ -25,8 +27,8 @@ class ParkFilterService:
|
||||
self.cache_prefix = "park_filter"
|
||||
|
||||
def get_filter_counts(
|
||||
self, base_queryset: Optional[QuerySet] = None
|
||||
) -> Dict[str, Any]:
|
||||
self, base_queryset: QuerySet | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Get counts for various filter options to show users what's available.
|
||||
|
||||
@@ -61,7 +63,7 @@ class ParkFilterService:
|
||||
cache.set(cache_key, filter_counts, self.CACHE_TIMEOUT)
|
||||
return filter_counts
|
||||
|
||||
def _get_park_type_counts(self, queryset: QuerySet) -> Dict[str, int]:
|
||||
def _get_park_type_counts(self, queryset: QuerySet) -> dict[str, int]:
|
||||
"""Get counts for different park types based on operator names."""
|
||||
return {
|
||||
"disney": queryset.filter(operator__name__icontains="Disney").count(),
|
||||
@@ -76,7 +78,7 @@ class ParkFilterService:
|
||||
|
||||
def _get_top_operators(
|
||||
self, queryset: QuerySet, limit: int = 10
|
||||
) -> List[Dict[str, Any]]:
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get the top operators by number of parks."""
|
||||
return list(
|
||||
queryset.values("operator__name", "operator__id")
|
||||
@@ -87,7 +89,7 @@ class ParkFilterService:
|
||||
|
||||
def _get_country_counts(
|
||||
self, queryset: QuerySet, limit: int = 10
|
||||
) -> List[Dict[str, Any]]:
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get countries with the most parks."""
|
||||
return list(
|
||||
queryset.filter(location__country__isnull=False)
|
||||
@@ -97,7 +99,7 @@ class ParkFilterService:
|
||||
.order_by("-park_count")[:limit]
|
||||
)
|
||||
|
||||
def get_filter_suggestions(self, query: str) -> Dict[str, List[str]]:
|
||||
def get_filter_suggestions(self, query: str) -> dict[str, list[str]]:
|
||||
"""
|
||||
Get filter suggestions based on a search query.
|
||||
|
||||
@@ -151,7 +153,7 @@ class ParkFilterService:
|
||||
cache.set(cache_key, suggestions, 60) # 1 minute cache
|
||||
return suggestions
|
||||
|
||||
def get_popular_filters(self) -> Dict[str, Any]:
|
||||
def get_popular_filters(self) -> dict[str, Any]:
|
||||
"""
|
||||
Get commonly used filter combinations and popular filter values.
|
||||
|
||||
@@ -210,7 +212,7 @@ class ParkFilterService:
|
||||
for key in cache_keys:
|
||||
cache.delete(key)
|
||||
|
||||
def get_filtered_queryset(self, filters: Dict[str, Any]) -> QuerySet: # noqa: C901
|
||||
def get_filtered_queryset(self, filters: dict[str, Any]) -> QuerySet: # noqa: C901
|
||||
"""
|
||||
Apply filters to get a filtered queryset with optimizations.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@ Parks-specific location services with OpenStreetMap integration.
|
||||
Handles geocoding, reverse geocoding, and location search for parks.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from typing import List, Dict, Any, Optional
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
import logging
|
||||
|
||||
from ..models import ParkLocation
|
||||
|
||||
@@ -23,7 +24,7 @@ class ParkLocationService:
|
||||
USER_AGENT = "ThrillWiki/1.0 (https://thrillwiki.com)"
|
||||
|
||||
@classmethod
|
||||
def search_locations(cls, query: str, limit: int = 10) -> Dict[str, Any]:
|
||||
def search_locations(cls, query: str, limit: int = 10) -> dict[str, Any]:
|
||||
"""
|
||||
Search for locations using OpenStreetMap Nominatim API.
|
||||
Optimized for finding theme parks and amusement parks.
|
||||
@@ -98,7 +99,7 @@ class ParkLocationService:
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def reverse_geocode(cls, latitude: float, longitude: float) -> Dict[str, Any]:
|
||||
def reverse_geocode(cls, latitude: float, longitude: float) -> dict[str, Any]:
|
||||
"""
|
||||
Reverse geocode coordinates to get location information using OSM.
|
||||
|
||||
@@ -159,7 +160,7 @@ class ParkLocationService:
|
||||
return {"error": "Reverse geocoding service temporarily unavailable"}
|
||||
|
||||
@classmethod
|
||||
def geocode_address(cls, address: str) -> Dict[str, Any]:
|
||||
def geocode_address(cls, address: str) -> dict[str, Any]:
|
||||
"""
|
||||
Geocode an address to get coordinates using OSM.
|
||||
|
||||
@@ -185,8 +186,8 @@ class ParkLocationService:
|
||||
cls,
|
||||
*,
|
||||
park,
|
||||
latitude: Optional[float] = None,
|
||||
longitude: Optional[float] = None,
|
||||
latitude: float | None = None,
|
||||
longitude: float | None = None,
|
||||
street_address: str = "",
|
||||
city: str = "",
|
||||
state: str = "",
|
||||
@@ -195,7 +196,7 @@ class ParkLocationService:
|
||||
highway_exit: str = "",
|
||||
parking_notes: str = "",
|
||||
seasonal_notes: str = "",
|
||||
osm_id: Optional[int] = None,
|
||||
osm_id: int | None = None,
|
||||
osm_type: str = "",
|
||||
) -> ParkLocation:
|
||||
"""
|
||||
@@ -279,7 +280,7 @@ class ParkLocationService:
|
||||
@classmethod
|
||||
def find_nearby_parks(
|
||||
cls, latitude: float, longitude: float, radius_km: float = 50
|
||||
) -> List[ParkLocation]:
|
||||
) -> list[ParkLocation]:
|
||||
"""
|
||||
Find parks near given coordinates using PostGIS.
|
||||
|
||||
@@ -349,8 +350,8 @@ class ParkLocationService:
|
||||
|
||||
@classmethod
|
||||
def _transform_osm_result(
|
||||
cls, osm_item: Dict[str, Any]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
cls, osm_item: dict[str, Any]
|
||||
) -> dict[str, Any] | None:
|
||||
"""Transform OSM search result to our standard format."""
|
||||
try:
|
||||
address = osm_item.get("address", {})
|
||||
@@ -432,8 +433,8 @@ class ParkLocationService:
|
||||
|
||||
@classmethod
|
||||
def _transform_osm_reverse_result(
|
||||
cls, osm_result: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
cls, osm_result: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Transform OSM reverse geocoding result to our standard format."""
|
||||
address = osm_result.get("address", {})
|
||||
|
||||
|
||||
@@ -5,11 +5,14 @@ This module provides media management functionality specific to parks.
|
||||
"""
|
||||
|
||||
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 Park, ParkPhoto
|
||||
|
||||
User = get_user_model()
|
||||
@@ -78,7 +81,7 @@ class ParkMediaService:
|
||||
@staticmethod
|
||||
def get_park_photos(
|
||||
park: Park, approved_only: bool = True, primary_first: bool = True
|
||||
) -> List[ParkPhoto]:
|
||||
) -> list[ParkPhoto]:
|
||||
"""
|
||||
Get photos for a park.
|
||||
|
||||
@@ -103,7 +106,7 @@ class ParkMediaService:
|
||||
return list(queryset)
|
||||
|
||||
@staticmethod
|
||||
def get_primary_photo(park: Park) -> Optional[ParkPhoto]:
|
||||
def get_primary_photo(park: Park) -> ParkPhoto | None:
|
||||
"""
|
||||
Get the primary photo for a park.
|
||||
|
||||
@@ -196,7 +199,7 @@ class ParkMediaService:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_photo_stats(park: Park) -> Dict[str, Any]:
|
||||
def get_photo_stats(park: Park) -> dict[str, Any]:
|
||||
"""
|
||||
Get photo statistics for a park.
|
||||
|
||||
@@ -217,7 +220,7 @@ class ParkMediaService:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def bulk_approve_photos(photos: List[ParkPhoto], approved_by: User) -> int:
|
||||
def bulk_approve_photos(photos: list[ParkPhoto], approved_by: User) -> int:
|
||||
"""
|
||||
Bulk approve multiple photos.
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@ Following Django styleguide pattern for business logic encapsulation.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, List, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
@@ -28,14 +29,14 @@ class ParkService:
|
||||
name: str,
|
||||
description: str = "",
|
||||
status: str = "OPERATING",
|
||||
operator_id: Optional[int] = None,
|
||||
property_owner_id: Optional[int] = None,
|
||||
opening_date: Optional[str] = None,
|
||||
closing_date: Optional[str] = None,
|
||||
operator_id: int | None = None,
|
||||
property_owner_id: int | None = None,
|
||||
opening_date: str | None = None,
|
||||
closing_date: str | None = None,
|
||||
operating_season: str = "",
|
||||
size_acres: Optional[float] = None,
|
||||
size_acres: float | None = None,
|
||||
website: str = "",
|
||||
location_data: Optional[Dict[str, Any]] = None,
|
||||
location_data: dict[str, Any] | None = None,
|
||||
created_by: Optional["AbstractUser"] = None,
|
||||
) -> Park:
|
||||
"""
|
||||
@@ -99,7 +100,7 @@ class ParkService:
|
||||
def update_park(
|
||||
*,
|
||||
park_id: int,
|
||||
updates: Dict[str, Any],
|
||||
updates: dict[str, Any],
|
||||
updated_by: Optional["AbstractUser"] = None,
|
||||
) -> Park:
|
||||
"""
|
||||
@@ -203,9 +204,10 @@ class ParkService:
|
||||
Returns:
|
||||
Updated Park instance with fresh statistics
|
||||
"""
|
||||
from apps.rides.models import Ride
|
||||
from django.db.models import Avg, Count
|
||||
|
||||
from apps.parks.models import ParkReview
|
||||
from django.db.models import Count, Avg
|
||||
from apps.rides.models import Ride
|
||||
|
||||
with transaction.atomic():
|
||||
park = Park.objects.select_for_update().get(id=park_id)
|
||||
@@ -235,11 +237,11 @@ class ParkService:
|
||||
@staticmethod
|
||||
def create_park_with_moderation(
|
||||
*,
|
||||
changes: Dict[str, Any],
|
||||
changes: dict[str, Any],
|
||||
submitter: "AbstractUser",
|
||||
reason: str = "",
|
||||
source: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Create a park through the moderation system.
|
||||
|
||||
@@ -267,11 +269,11 @@ class ParkService:
|
||||
def update_park_with_moderation(
|
||||
*,
|
||||
park: Park,
|
||||
changes: Dict[str, Any],
|
||||
changes: dict[str, Any],
|
||||
submitter: "AbstractUser",
|
||||
reason: str = "",
|
||||
source: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Update a park through the moderation system.
|
||||
|
||||
@@ -300,14 +302,14 @@ class ParkService:
|
||||
def create_or_update_location(
|
||||
*,
|
||||
park: Park,
|
||||
latitude: Optional[float],
|
||||
longitude: Optional[float],
|
||||
latitude: float | None,
|
||||
longitude: float | None,
|
||||
street_address: str = "",
|
||||
city: str = "",
|
||||
state: str = "",
|
||||
country: str = "USA",
|
||||
postal_code: str = "",
|
||||
) -> Optional[ParkLocation]:
|
||||
) -> ParkLocation | None:
|
||||
"""
|
||||
Create or update a park's location.
|
||||
|
||||
@@ -356,9 +358,9 @@ class ParkService:
|
||||
def upload_photos(
|
||||
*,
|
||||
park: Park,
|
||||
photos: List[UploadedFile],
|
||||
photos: list[UploadedFile],
|
||||
uploaded_by: "AbstractUser",
|
||||
) -> Dict[str, Any]:
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Upload multiple photos for a park.
|
||||
|
||||
@@ -370,10 +372,9 @@ class ParkService:
|
||||
Returns:
|
||||
Dictionary with uploaded_count and errors list
|
||||
"""
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
uploaded_count = 0
|
||||
errors: List[str] = []
|
||||
errors: list[str] = []
|
||||
|
||||
for photo_file in photos:
|
||||
try:
|
||||
@@ -396,11 +397,11 @@ class ParkService:
|
||||
@staticmethod
|
||||
def handle_park_creation_result(
|
||||
*,
|
||||
result: Dict[str, Any],
|
||||
form_data: Dict[str, Any],
|
||||
photos: List[UploadedFile],
|
||||
result: dict[str, Any],
|
||||
form_data: dict[str, Any],
|
||||
photos: list[UploadedFile],
|
||||
user: "AbstractUser",
|
||||
) -> Dict[str, Any]:
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Handle the result of park creation through moderation.
|
||||
|
||||
@@ -413,7 +414,7 @@ class ParkService:
|
||||
Returns:
|
||||
Dictionary with status, park (if created), uploaded_count, and errors
|
||||
"""
|
||||
response: Dict[str, Any] = {
|
||||
response: dict[str, Any] = {
|
||||
"status": result["status"],
|
||||
"park": None,
|
||||
"uploaded_count": 0,
|
||||
@@ -454,12 +455,12 @@ class ParkService:
|
||||
@staticmethod
|
||||
def handle_park_update_result(
|
||||
*,
|
||||
result: Dict[str, Any],
|
||||
result: dict[str, Any],
|
||||
park: Park,
|
||||
form_data: Dict[str, Any],
|
||||
photos: List[UploadedFile],
|
||||
form_data: dict[str, Any],
|
||||
photos: list[UploadedFile],
|
||||
user: "AbstractUser",
|
||||
) -> Dict[str, Any]:
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Handle the result of park update through moderation.
|
||||
|
||||
@@ -473,7 +474,7 @@ class ParkService:
|
||||
Returns:
|
||||
Dictionary with status, park, uploaded_count, and errors
|
||||
"""
|
||||
response: Dict[str, Any] = {
|
||||
response: dict[str, Any] = {
|
||||
"status": result["status"],
|
||||
"park": park,
|
||||
"uploaded_count": 0,
|
||||
|
||||
@@ -9,18 +9,19 @@ This service provides functionality for:
|
||||
- Proper rate limiting and caching
|
||||
"""
|
||||
|
||||
import time
|
||||
import math
|
||||
import logging
|
||||
import requests
|
||||
from typing import Dict, List, Optional, Any
|
||||
import math
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from itertools import permutations
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.contrib.gis.geos import Point
|
||||
from django.contrib.gis.measure import Distance
|
||||
from django.core.cache import cache
|
||||
|
||||
from apps.parks.models import Park
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -33,7 +34,7 @@ class Coordinates:
|
||||
latitude: float
|
||||
longitude: float
|
||||
|
||||
def to_list(self) -> List[float]:
|
||||
def to_list(self) -> list[float]:
|
||||
"""Return as [lat, lon] list."""
|
||||
return [self.latitude, self.longitude]
|
||||
|
||||
@@ -48,7 +49,7 @@ class RouteInfo:
|
||||
|
||||
distance_km: float
|
||||
duration_minutes: int
|
||||
geometry: Optional[str] = None # Encoded polyline
|
||||
geometry: str | None = None # Encoded polyline
|
||||
|
||||
@property
|
||||
def formatted_distance(self) -> str:
|
||||
@@ -79,7 +80,7 @@ class TripLeg:
|
||||
route: RouteInfo
|
||||
|
||||
@property
|
||||
def parks_along_route(self) -> List["Park"]:
|
||||
def parks_along_route(self) -> list["Park"]:
|
||||
"""Get parks along this route segment."""
|
||||
# This would be populated by find_parks_along_route
|
||||
return []
|
||||
@@ -89,8 +90,8 @@ class TripLeg:
|
||||
class RoadTrip:
|
||||
"""Complete road trip with multiple parks."""
|
||||
|
||||
parks: List["Park"]
|
||||
legs: List[TripLeg]
|
||||
parks: list["Park"]
|
||||
legs: list[TripLeg]
|
||||
total_distance_km: float
|
||||
total_duration_minutes: int
|
||||
|
||||
@@ -170,7 +171,7 @@ class RoadTripService:
|
||||
}
|
||||
)
|
||||
|
||||
def _make_request(self, url: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
def _make_request(self, url: str, params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
Make HTTP request with rate limiting, retries, and error handling.
|
||||
"""
|
||||
@@ -195,7 +196,7 @@ class RoadTripService:
|
||||
f"Failed to make request after {self.max_retries} attempts: {e}"
|
||||
)
|
||||
|
||||
def geocode_address(self, address: str) -> Optional[Coordinates]:
|
||||
def geocode_address(self, address: str) -> Coordinates | None:
|
||||
"""
|
||||
Convert address to coordinates using Nominatim geocoding service.
|
||||
|
||||
@@ -256,7 +257,7 @@ class RoadTripService:
|
||||
|
||||
def calculate_route(
|
||||
self, start_coords: Coordinates, end_coords: Coordinates
|
||||
) -> Optional[RouteInfo]:
|
||||
) -> RouteInfo | None:
|
||||
"""
|
||||
Calculate route between two coordinate points using OSRM.
|
||||
|
||||
@@ -377,7 +378,7 @@ class RoadTripService:
|
||||
|
||||
def find_parks_along_route(
|
||||
self, start_park: "Park", end_park: "Park", max_detour_km: float = 50
|
||||
) -> List["Park"]:
|
||||
) -> list["Park"]:
|
||||
"""
|
||||
Find parks along a route within specified detour distance.
|
||||
|
||||
@@ -444,7 +445,7 @@ class RoadTripService:
|
||||
|
||||
def _calculate_detour_distance(
|
||||
self, start: Coordinates, end: Coordinates, waypoint: Coordinates
|
||||
) -> Optional[float]:
|
||||
) -> float | None:
|
||||
"""
|
||||
Calculate the detour distance when visiting a waypoint.
|
||||
"""
|
||||
@@ -470,7 +471,7 @@ class RoadTripService:
|
||||
logger.error(f"Failed to calculate detour distance: {e}")
|
||||
return None
|
||||
|
||||
def create_multi_park_trip(self, park_list: List["Park"]) -> Optional[RoadTrip]:
|
||||
def create_multi_park_trip(self, park_list: list["Park"]) -> RoadTrip | None:
|
||||
"""
|
||||
Create optimized multi-park road trip using simple nearest neighbor heuristic.
|
||||
|
||||
@@ -489,7 +490,7 @@ class RoadTripService:
|
||||
else:
|
||||
return self._optimize_trip_nearest_neighbor(park_list)
|
||||
|
||||
def _optimize_trip_exhaustive(self, park_list: List["Park"]) -> Optional[RoadTrip]:
|
||||
def _optimize_trip_exhaustive(self, park_list: list["Park"]) -> RoadTrip | None:
|
||||
"""
|
||||
Find optimal route by testing all permutations (for small lists).
|
||||
"""
|
||||
@@ -508,8 +509,8 @@ class RoadTripService:
|
||||
return best_trip
|
||||
|
||||
def _optimize_trip_nearest_neighbor(
|
||||
self, park_list: List["Park"]
|
||||
) -> Optional[RoadTrip]:
|
||||
self, park_list: list["Park"]
|
||||
) -> RoadTrip | None:
|
||||
"""
|
||||
Optimize trip using nearest neighbor heuristic (for larger lists).
|
||||
"""
|
||||
@@ -553,8 +554,8 @@ class RoadTripService:
|
||||
return self._create_trip_from_order(ordered_parks)
|
||||
|
||||
def _create_trip_from_order(
|
||||
self, ordered_parks: List["Park"]
|
||||
) -> Optional[RoadTrip]:
|
||||
self, ordered_parks: list["Park"]
|
||||
) -> RoadTrip | None:
|
||||
"""
|
||||
Create a RoadTrip object from an ordered list of parks.
|
||||
"""
|
||||
@@ -596,7 +597,7 @@ class RoadTripService:
|
||||
|
||||
def get_park_distances(
|
||||
self, center_park: "Park", radius_km: float = 100
|
||||
) -> List[Dict[str, Any]]:
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get all parks within radius of a center park with distances.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user