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:
@@ -26,9 +26,7 @@ class ParkFilterService:
|
||||
def __init__(self):
|
||||
self.cache_prefix = "park_filter"
|
||||
|
||||
def get_filter_counts(
|
||||
self, base_queryset: QuerySet | None = None
|
||||
) -> dict[str, Any]:
|
||||
def get_filter_counts(self, base_queryset: QuerySet | None = None) -> dict[str, Any]:
|
||||
"""
|
||||
Get counts for various filter options to show users what's available.
|
||||
|
||||
@@ -76,9 +74,7 @@ class ParkFilterService:
|
||||
).count(),
|
||||
}
|
||||
|
||||
def _get_top_operators(
|
||||
self, queryset: QuerySet, limit: int = 10
|
||||
) -> list[dict[str, Any]]:
|
||||
def _get_top_operators(self, queryset: QuerySet, limit: int = 10) -> list[dict[str, Any]]:
|
||||
"""Get the top operators by number of parks."""
|
||||
return list(
|
||||
queryset.values("operator__name", "operator__id")
|
||||
@@ -87,9 +83,7 @@ class ParkFilterService:
|
||||
.order_by("-park_count")[:limit]
|
||||
)
|
||||
|
||||
def _get_country_counts(
|
||||
self, queryset: QuerySet, limit: int = 10
|
||||
) -> list[dict[str, Any]]:
|
||||
def _get_country_counts(self, queryset: QuerySet, limit: int = 10) -> list[dict[str, Any]]:
|
||||
"""Get countries with the most parks."""
|
||||
return list(
|
||||
queryset.filter(location__country__isnull=False)
|
||||
@@ -123,21 +117,18 @@ class ParkFilterService:
|
||||
|
||||
if len(query) >= 2: # Only search for queries of 2+ characters
|
||||
# Park name suggestions
|
||||
park_names = Park.objects.filter(name__icontains=query).values_list(
|
||||
"name", flat=True
|
||||
)[:5]
|
||||
park_names = Park.objects.filter(name__icontains=query).values_list("name", flat=True)[:5]
|
||||
suggestions["parks"] = list(park_names)
|
||||
|
||||
# Operator suggestions
|
||||
operator_names = Company.objects.filter(
|
||||
roles__contains=["OPERATOR"], name__icontains=query
|
||||
).values_list("name", flat=True)[:5]
|
||||
operator_names = Company.objects.filter(roles__contains=["OPERATOR"], name__icontains=query).values_list(
|
||||
"name", flat=True
|
||||
)[:5]
|
||||
suggestions["operators"] = list(operator_names)
|
||||
|
||||
# Location suggestions (cities and countries)
|
||||
locations = Park.objects.filter(
|
||||
Q(location__city__icontains=query)
|
||||
| Q(location__country__icontains=query)
|
||||
Q(location__city__icontains=query) | Q(location__country__icontains=query)
|
||||
).values_list("location__city", "location__country")[:5]
|
||||
|
||||
location_suggestions = []
|
||||
@@ -264,14 +255,10 @@ class ParkFilterService:
|
||||
|
||||
# Apply location filters
|
||||
if filters.get("country_filter"):
|
||||
queryset = queryset.filter(
|
||||
location__country__icontains=filters["country_filter"]
|
||||
)
|
||||
queryset = queryset.filter(location__country__icontains=filters["country_filter"])
|
||||
|
||||
if filters.get("state_filter"):
|
||||
queryset = queryset.filter(
|
||||
location__state__icontains=filters["state_filter"]
|
||||
)
|
||||
queryset = queryset.filter(location__state__icontains=filters["state_filter"])
|
||||
|
||||
# Apply ordering
|
||||
if filters.get("ordering"):
|
||||
|
||||
@@ -21,8 +21,8 @@ class SmartParkLoader:
|
||||
"""
|
||||
|
||||
# Cache configuration
|
||||
CACHE_TIMEOUT = getattr(settings, 'HYBRID_FILTER_CACHE_TIMEOUT', 300) # 5 minutes
|
||||
CACHE_KEY_PREFIX = 'hybrid_parks'
|
||||
CACHE_TIMEOUT = getattr(settings, "HYBRID_FILTER_CACHE_TIMEOUT", 300) # 5 minutes
|
||||
CACHE_KEY_PREFIX = "hybrid_parks"
|
||||
|
||||
# Progressive loading thresholds
|
||||
INITIAL_LOAD_SIZE = 50
|
||||
@@ -34,17 +34,22 @@ class SmartParkLoader:
|
||||
|
||||
def _get_optimized_queryset(self) -> models.QuerySet:
|
||||
"""Get optimized base queryset with all necessary prefetches."""
|
||||
return Park.objects.select_related(
|
||||
'operator',
|
||||
'property_owner',
|
||||
'banner_image',
|
||||
'card_image',
|
||||
).prefetch_related(
|
||||
'location', # ParkLocation relationship
|
||||
).filter(
|
||||
# Only include operating and temporarily closed parks by default
|
||||
status__in=['OPERATING', 'CLOSED_TEMP']
|
||||
).order_by('name')
|
||||
return (
|
||||
Park.objects.select_related(
|
||||
"operator",
|
||||
"property_owner",
|
||||
"banner_image",
|
||||
"card_image",
|
||||
)
|
||||
.prefetch_related(
|
||||
"location", # ParkLocation relationship
|
||||
)
|
||||
.filter(
|
||||
# Only include operating and temporarily closed parks by default
|
||||
status__in=["OPERATING", "CLOSED_TEMP"]
|
||||
)
|
||||
.order_by("name")
|
||||
)
|
||||
|
||||
def get_initial_load(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""
|
||||
@@ -56,7 +61,7 @@ class SmartParkLoader:
|
||||
Returns:
|
||||
Dictionary containing parks data and metadata
|
||||
"""
|
||||
cache_key = self._generate_cache_key('initial', filters)
|
||||
cache_key = self._generate_cache_key("initial", filters)
|
||||
cached_result = cache.get(cache_key)
|
||||
|
||||
if cached_result:
|
||||
@@ -74,21 +79,21 @@ class SmartParkLoader:
|
||||
if total_count <= self.MAX_CLIENT_SIDE_RECORDS:
|
||||
# Load all data for client-side filtering
|
||||
parks = list(queryset.all())
|
||||
strategy = 'client_side'
|
||||
strategy = "client_side"
|
||||
has_more = False
|
||||
else:
|
||||
# Load initial batch for server-side pagination
|
||||
parks = list(queryset[:self.INITIAL_LOAD_SIZE])
|
||||
strategy = 'server_side'
|
||||
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,
|
||||
'strategy': strategy,
|
||||
'has_more': has_more,
|
||||
'next_offset': len(parks) if has_more else None,
|
||||
'filter_metadata': self._get_filter_metadata(queryset),
|
||||
"parks": parks,
|
||||
"total_count": total_count,
|
||||
"strategy": strategy,
|
||||
"has_more": has_more,
|
||||
"next_offset": len(parks) if has_more else None,
|
||||
"filter_metadata": self._get_filter_metadata(queryset),
|
||||
}
|
||||
|
||||
# Cache the result
|
||||
@@ -96,11 +101,7 @@ class SmartParkLoader:
|
||||
|
||||
return result
|
||||
|
||||
def get_progressive_load(
|
||||
self,
|
||||
offset: int,
|
||||
filters: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
def get_progressive_load(self, offset: int, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""
|
||||
Get next batch of parks for progressive loading.
|
||||
|
||||
@@ -111,7 +112,7 @@ class SmartParkLoader:
|
||||
Returns:
|
||||
Dictionary containing parks data and metadata
|
||||
"""
|
||||
cache_key = self._generate_cache_key(f'progressive_{offset}', filters)
|
||||
cache_key = self._generate_cache_key(f"progressive_{offset}", filters)
|
||||
cached_result = cache.get(cache_key)
|
||||
|
||||
if cached_result:
|
||||
@@ -131,10 +132,10 @@ class SmartParkLoader:
|
||||
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,
|
||||
"parks": parks,
|
||||
"total_count": total_count,
|
||||
"has_more": has_more,
|
||||
"next_offset": end_offset if has_more else None,
|
||||
}
|
||||
|
||||
# Cache the result
|
||||
@@ -152,7 +153,7 @@ class SmartParkLoader:
|
||||
Returns:
|
||||
Dictionary containing filter metadata
|
||||
"""
|
||||
cache_key = self._generate_cache_key('metadata', filters)
|
||||
cache_key = self._generate_cache_key("metadata", filters)
|
||||
cached_result = cache.get(cache_key)
|
||||
|
||||
if cached_result:
|
||||
@@ -174,72 +175,72 @@ class SmartParkLoader:
|
||||
"""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'])
|
||||
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'])
|
||||
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'])
|
||||
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'])
|
||||
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'])
|
||||
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'])
|
||||
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_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'])
|
||||
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_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'])
|
||||
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_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'])
|
||||
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_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'])
|
||||
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_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'])
|
||||
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'])
|
||||
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'])
|
||||
queryset = queryset.filter(operator__slug=filters["operator"])
|
||||
|
||||
# Search query
|
||||
if 'search' in filters and filters['search']:
|
||||
search_term = filters['search'].lower()
|
||||
if "search" in filters and filters["search"]:
|
||||
search_term = filters["search"].lower()
|
||||
queryset = queryset.filter(search_text__icontains=search_term)
|
||||
|
||||
return queryset
|
||||
@@ -249,150 +250,125 @@ class SmartParkLoader:
|
||||
|
||||
# Get distinct values for categorical filters with counts
|
||||
countries_data = list(
|
||||
queryset.values('location__country')
|
||||
queryset.values("location__country")
|
||||
.exclude(location__country__isnull=True)
|
||||
.annotate(count=models.Count('id'))
|
||||
.order_by('location__country')
|
||||
.annotate(count=models.Count("id"))
|
||||
.order_by("location__country")
|
||||
)
|
||||
|
||||
states_data = list(
|
||||
queryset.values('location__state')
|
||||
queryset.values("location__state")
|
||||
.exclude(location__state__isnull=True)
|
||||
.annotate(count=models.Count('id'))
|
||||
.order_by('location__state')
|
||||
.annotate(count=models.Count("id"))
|
||||
.order_by("location__state")
|
||||
)
|
||||
|
||||
park_types_data = list(
|
||||
queryset.values('park_type')
|
||||
queryset.values("park_type")
|
||||
.exclude(park_type__isnull=True)
|
||||
.annotate(count=models.Count('id'))
|
||||
.order_by('park_type')
|
||||
.annotate(count=models.Count("id"))
|
||||
.order_by("park_type")
|
||||
)
|
||||
|
||||
statuses_data = list(
|
||||
queryset.values('status')
|
||||
.annotate(count=models.Count('id'))
|
||||
.order_by('status')
|
||||
)
|
||||
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')
|
||||
queryset.select_related("operator")
|
||||
.values("operator__id", "operator__name", "operator__slug")
|
||||
.exclude(operator__isnull=True)
|
||||
.annotate(count=models.Count('id'))
|
||||
.order_by('operator__name')
|
||||
.annotate(count=models.Count("id"))
|
||||
.order_by("operator__name")
|
||||
)
|
||||
|
||||
# Convert to frontend-expected format with value/label/count
|
||||
countries = [
|
||||
{
|
||||
'value': item['location__country'],
|
||||
'label': item['location__country'],
|
||||
'count': item['count']
|
||||
}
|
||||
{"value": item["location__country"], "label": item["location__country"], "count": item["count"]}
|
||||
for item in countries_data
|
||||
]
|
||||
|
||||
states = [
|
||||
{
|
||||
'value': item['location__state'],
|
||||
'label': item['location__state'],
|
||||
'count': item['count']
|
||||
}
|
||||
{"value": item["location__state"], "label": item["location__state"], "count": item["count"]}
|
||||
for item in states_data
|
||||
]
|
||||
|
||||
park_types = [
|
||||
{
|
||||
'value': item['park_type'],
|
||||
'label': item['park_type'],
|
||||
'count': item['count']
|
||||
}
|
||||
for item in park_types_data
|
||||
{"value": item["park_type"], "label": item["park_type"], "count": item["count"]} for item in park_types_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
|
||||
]
|
||||
|
||||
operators = [
|
||||
{
|
||||
'value': item['operator__slug'],
|
||||
'label': item['operator__name'],
|
||||
'count': item['count']
|
||||
}
|
||||
{"value": item["operator__slug"], "label": item["operator__name"], "count": item["count"]}
|
||||
for item in operators_data
|
||||
]
|
||||
|
||||
# Get ranges for numerical filters
|
||||
aggregates = queryset.aggregate(
|
||||
opening_year_min=models.Min('opening_year'),
|
||||
opening_year_max=models.Max('opening_year'),
|
||||
size_min=models.Min('size_acres'),
|
||||
size_max=models.Max('size_acres'),
|
||||
rating_min=models.Min('average_rating'),
|
||||
rating_max=models.Max('average_rating'),
|
||||
ride_count_min=models.Min('ride_count'),
|
||||
ride_count_max=models.Max('ride_count'),
|
||||
coaster_count_min=models.Min('coaster_count'),
|
||||
coaster_count_max=models.Max('coaster_count'),
|
||||
opening_year_min=models.Min("opening_year"),
|
||||
opening_year_max=models.Max("opening_year"),
|
||||
size_min=models.Min("size_acres"),
|
||||
size_max=models.Max("size_acres"),
|
||||
rating_min=models.Min("average_rating"),
|
||||
rating_max=models.Max("average_rating"),
|
||||
ride_count_min=models.Min("ride_count"),
|
||||
ride_count_max=models.Max("ride_count"),
|
||||
coaster_count_min=models.Min("coaster_count"),
|
||||
coaster_count_max=models.Max("coaster_count"),
|
||||
)
|
||||
|
||||
return {
|
||||
'categorical': {
|
||||
'countries': countries,
|
||||
'states': states,
|
||||
'park_types': park_types,
|
||||
'statuses': statuses,
|
||||
'operators': operators,
|
||||
"categorical": {
|
||||
"countries": countries,
|
||||
"states": states,
|
||||
"park_types": park_types,
|
||||
"statuses": statuses,
|
||||
"operators": operators,
|
||||
},
|
||||
'ranges': {
|
||||
'opening_year': {
|
||||
'min': aggregates['opening_year_min'],
|
||||
'max': aggregates['opening_year_max'],
|
||||
'step': 1,
|
||||
'unit': 'year'
|
||||
"ranges": {
|
||||
"opening_year": {
|
||||
"min": aggregates["opening_year_min"],
|
||||
"max": aggregates["opening_year_max"],
|
||||
"step": 1,
|
||||
"unit": "year",
|
||||
},
|
||||
'size_acres': {
|
||||
'min': float(aggregates['size_min']) if aggregates['size_min'] else None,
|
||||
'max': float(aggregates['size_max']) if aggregates['size_max'] else None,
|
||||
'step': 1.0,
|
||||
'unit': 'acres'
|
||||
"size_acres": {
|
||||
"min": float(aggregates["size_min"]) if aggregates["size_min"] else None,
|
||||
"max": float(aggregates["size_max"]) if aggregates["size_max"] else None,
|
||||
"step": 1.0,
|
||||
"unit": "acres",
|
||||
},
|
||||
'average_rating': {
|
||||
'min': float(aggregates['rating_min']) if aggregates['rating_min'] else None,
|
||||
'max': float(aggregates['rating_max']) if aggregates['rating_max'] else None,
|
||||
'step': 0.1,
|
||||
'unit': 'stars'
|
||||
"average_rating": {
|
||||
"min": float(aggregates["rating_min"]) if aggregates["rating_min"] else None,
|
||||
"max": float(aggregates["rating_max"]) if aggregates["rating_max"] else None,
|
||||
"step": 0.1,
|
||||
"unit": "stars",
|
||||
},
|
||||
'ride_count': {
|
||||
'min': aggregates['ride_count_min'],
|
||||
'max': aggregates['ride_count_max'],
|
||||
'step': 1,
|
||||
'unit': 'rides'
|
||||
"ride_count": {
|
||||
"min": aggregates["ride_count_min"],
|
||||
"max": aggregates["ride_count_max"],
|
||||
"step": 1,
|
||||
"unit": "rides",
|
||||
},
|
||||
'coaster_count': {
|
||||
'min': aggregates['coaster_count_min'],
|
||||
'max': aggregates['coaster_count_max'],
|
||||
'step': 1,
|
||||
'unit': 'coasters'
|
||||
"coaster_count": {
|
||||
"min": aggregates["coaster_count_min"],
|
||||
"max": aggregates["coaster_count_max"],
|
||||
"step": 1,
|
||||
"unit": "coasters",
|
||||
},
|
||||
},
|
||||
'total_count': queryset.count(),
|
||||
"total_count": queryset.count(),
|
||||
}
|
||||
|
||||
def _get_status_label(self, status: str) -> str:
|
||||
"""Convert status code to human-readable label."""
|
||||
status_labels = {
|
||||
'OPERATING': 'Operating',
|
||||
'CLOSED_TEMP': 'Temporarily Closed',
|
||||
'CLOSED_PERM': 'Permanently Closed',
|
||||
'UNDER_CONSTRUCTION': 'Under Construction',
|
||||
"OPERATING": "Operating",
|
||||
"CLOSED_TEMP": "Temporarily Closed",
|
||||
"CLOSED_PERM": "Permanently Closed",
|
||||
"UNDER_CONSTRUCTION": "Under Construction",
|
||||
}
|
||||
if status in status_labels:
|
||||
return status_labels[status]
|
||||
@@ -405,23 +381,23 @@ class SmartParkLoader:
|
||||
|
||||
if filters:
|
||||
# Create a consistent string representation of filters
|
||||
filter_str = '_'.join(f"{k}:{v}" for k, v in sorted(filters.items()) if v)
|
||||
filter_str = "_".join(f"{k}:{v}" for k, v in sorted(filters.items()) if v)
|
||||
key_parts.append(filter_str)
|
||||
|
||||
return '_'.join(key_parts)
|
||||
return "_".join(key_parts)
|
||||
|
||||
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
|
||||
cache_keys = [
|
||||
self._generate_cache_key('initial', filters),
|
||||
self._generate_cache_key('metadata', filters),
|
||||
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_keys.append(self._generate_cache_key(f"progressive_{offset}", filters))
|
||||
|
||||
cache.delete_many(cache_keys)
|
||||
|
||||
|
||||
@@ -245,9 +245,7 @@ class ParkLocationService:
|
||||
return park_location
|
||||
|
||||
@classmethod
|
||||
def update_park_location(
|
||||
cls, park_location: ParkLocation, **updates
|
||||
) -> ParkLocation:
|
||||
def update_park_location(cls, park_location: ParkLocation, **updates) -> ParkLocation:
|
||||
"""
|
||||
Update park location with validation.
|
||||
|
||||
@@ -278,9 +276,7 @@ class ParkLocationService:
|
||||
return park_location
|
||||
|
||||
@classmethod
|
||||
def find_nearby_parks(
|
||||
cls, latitude: float, longitude: float, radius_km: float = 50
|
||||
) -> list[ParkLocation]:
|
||||
def find_nearby_parks(cls, latitude: float, longitude: float, radius_km: float = 50) -> list[ParkLocation]:
|
||||
"""
|
||||
Find parks near given coordinates using PostGIS.
|
||||
|
||||
@@ -298,9 +294,7 @@ class ParkLocationService:
|
||||
center_point = Point(longitude, latitude, srid=4326)
|
||||
|
||||
return list(
|
||||
ParkLocation.objects.filter(
|
||||
point__distance_lte=(center_point, Distance(km=radius_km))
|
||||
)
|
||||
ParkLocation.objects.filter(point__distance_lte=(center_point, Distance(km=radius_km)))
|
||||
.select_related("park", "park__operator")
|
||||
.order_by("point__distance")
|
||||
)
|
||||
@@ -349,9 +343,7 @@ class ParkLocationService:
|
||||
return park_location
|
||||
|
||||
@classmethod
|
||||
def _transform_osm_result(
|
||||
cls, osm_item: dict[str, Any]
|
||||
) -> dict[str, Any] | None:
|
||||
def _transform_osm_result(cls, osm_item: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""Transform OSM search result to our standard format."""
|
||||
try:
|
||||
address = osm_item.get("address", {})
|
||||
@@ -369,12 +361,7 @@ class ParkLocationService:
|
||||
or ""
|
||||
)
|
||||
|
||||
state = (
|
||||
address.get("state")
|
||||
or address.get("province")
|
||||
or address.get("region")
|
||||
or ""
|
||||
)
|
||||
state = address.get("state") or address.get("province") or address.get("region") or ""
|
||||
|
||||
country = address.get("country", "")
|
||||
postal_code = address.get("postcode", "")
|
||||
@@ -432,9 +419,7 @@ class ParkLocationService:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _transform_osm_reverse_result(
|
||||
cls, osm_result: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
def _transform_osm_reverse_result(cls, osm_result: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Transform OSM reverse geocoding result to our standard format."""
|
||||
address = osm_result.get("address", {})
|
||||
|
||||
@@ -443,20 +428,9 @@ class ParkLocationService:
|
||||
street_name = address.get("road", "")
|
||||
street_address = f"{street_number} {street_name}".strip()
|
||||
|
||||
city = (
|
||||
address.get("city")
|
||||
or address.get("town")
|
||||
or address.get("village")
|
||||
or address.get("municipality")
|
||||
or ""
|
||||
)
|
||||
city = address.get("city") or address.get("town") or address.get("village") or address.get("municipality") or ""
|
||||
|
||||
state = (
|
||||
address.get("state")
|
||||
or address.get("province")
|
||||
or address.get("region")
|
||||
or ""
|
||||
)
|
||||
state = address.get("state") or address.get("province") or address.get("region") or ""
|
||||
|
||||
country = address.get("country", "")
|
||||
postal_code = address.get("postcode", "")
|
||||
|
||||
@@ -79,9 +79,7 @@ class ParkMediaService:
|
||||
return photo
|
||||
|
||||
@staticmethod
|
||||
def get_park_photos(
|
||||
park: Park, approved_only: bool = True, primary_first: bool = True
|
||||
) -> list[ParkPhoto]:
|
||||
def get_park_photos(park: Park, approved_only: bool = True, primary_first: bool = True) -> list[ParkPhoto]:
|
||||
"""
|
||||
Get photos for a park.
|
||||
|
||||
@@ -190,9 +188,7 @@ class ParkMediaService:
|
||||
photo.image.delete(save=False)
|
||||
photo.delete()
|
||||
|
||||
logger.info(
|
||||
f"Photo {photo_id} deleted from park {park_slug} by user {deleted_by.username}"
|
||||
)
|
||||
logger.info(f"Photo {photo_id} deleted from park {park_slug} by user {deleted_by.username}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete photo {photo.pk}: {str(e)}")
|
||||
@@ -238,7 +234,5 @@ class ParkMediaService:
|
||||
if ParkMediaService.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
|
||||
|
||||
@@ -133,9 +133,7 @@ class ParkService:
|
||||
return park
|
||||
|
||||
@staticmethod
|
||||
def delete_park(
|
||||
*, park_id: int, deleted_by: Optional["AbstractUser"] = None
|
||||
) -> bool:
|
||||
def delete_park(*, park_id: int, deleted_by: Optional["AbstractUser"] = None) -> bool:
|
||||
"""
|
||||
Soft delete a park by setting status to DEMOLISHED.
|
||||
|
||||
@@ -219,9 +217,9 @@ class ParkService:
|
||||
)
|
||||
|
||||
# Calculate average rating
|
||||
avg_rating = ParkReview.objects.filter(
|
||||
park=park, is_published=True
|
||||
).aggregate(avg_rating=Avg("rating"))["avg_rating"]
|
||||
avg_rating = ParkReview.objects.filter(park=park, is_published=True).aggregate(avg_rating=Avg("rating"))[
|
||||
"avg_rating"
|
||||
]
|
||||
|
||||
# Update park fields
|
||||
park.ride_count = ride_stats["total_rides"] or 0
|
||||
|
||||
@@ -148,12 +148,8 @@ class RoadTripService:
|
||||
|
||||
# Configuration from Django settings
|
||||
self.cache_timeout = getattr(settings, "ROADTRIP_CACHE_TIMEOUT", 3600 * 24)
|
||||
self.route_cache_timeout = getattr(
|
||||
settings, "ROADTRIP_ROUTE_CACHE_TIMEOUT", 3600 * 6
|
||||
)
|
||||
self.user_agent = getattr(
|
||||
settings, "ROADTRIP_USER_AGENT", "ThrillWiki Road Trip Planner"
|
||||
)
|
||||
self.route_cache_timeout = getattr(settings, "ROADTRIP_ROUTE_CACHE_TIMEOUT", 3600 * 6)
|
||||
self.user_agent = getattr(settings, "ROADTRIP_USER_AGENT", "ThrillWiki Road Trip Planner")
|
||||
self.request_timeout = getattr(settings, "ROADTRIP_REQUEST_TIMEOUT", 10)
|
||||
self.max_retries = getattr(settings, "ROADTRIP_MAX_RETRIES", 3)
|
||||
self.backoff_factor = getattr(settings, "ROADTRIP_BACKOFF_FACTOR", 2)
|
||||
@@ -179,9 +175,7 @@ class RoadTripService:
|
||||
|
||||
for attempt in range(self.max_retries):
|
||||
try:
|
||||
response = self.session.get(
|
||||
url, params=params, timeout=self.request_timeout
|
||||
)
|
||||
response = self.session.get(url, params=params, timeout=self.request_timeout)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
@@ -192,9 +186,7 @@ class RoadTripService:
|
||||
wait_time = self.backoff_factor**attempt
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
raise OSMAPIException(
|
||||
f"Failed to make request after {self.max_retries} attempts: {e}"
|
||||
)
|
||||
raise OSMAPIException(f"Failed to make request after {self.max_retries} attempts: {e}") from e
|
||||
|
||||
def geocode_address(self, address: str) -> Coordinates | None:
|
||||
"""
|
||||
@@ -243,9 +235,7 @@ class RoadTripService:
|
||||
self.cache_timeout,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Geocoded '{address}' to {coords.latitude}, {coords.longitude}"
|
||||
)
|
||||
logger.info(f"Geocoded '{address}' to {coords.latitude}, {coords.longitude}")
|
||||
return coords
|
||||
else:
|
||||
logger.warning(f"No geocoding results for address: {address}")
|
||||
@@ -255,9 +245,7 @@ class RoadTripService:
|
||||
logger.error(f"Geocoding failed for '{address}': {e}")
|
||||
return None
|
||||
|
||||
def calculate_route(
|
||||
self, start_coords: Coordinates, end_coords: Coordinates
|
||||
) -> RouteInfo | None:
|
||||
def calculate_route(self, start_coords: Coordinates, end_coords: Coordinates) -> RouteInfo | None:
|
||||
"""
|
||||
Calculate route between two coordinate points using OSRM.
|
||||
|
||||
@@ -327,9 +315,7 @@ class RoadTripService:
|
||||
return route_info
|
||||
else:
|
||||
# Fallback to straight-line distance calculation
|
||||
logger.warning(
|
||||
"OSRM routing failed, falling back to straight-line distance"
|
||||
)
|
||||
logger.warning("OSRM routing failed, falling back to straight-line distance")
|
||||
return self._calculate_straight_line_route(start_coords, end_coords)
|
||||
|
||||
except Exception as e:
|
||||
@@ -337,9 +323,7 @@ class RoadTripService:
|
||||
# Fallback to straight-line distance
|
||||
return self._calculate_straight_line_route(start_coords, end_coords)
|
||||
|
||||
def _calculate_straight_line_route(
|
||||
self, start_coords: Coordinates, end_coords: Coordinates
|
||||
) -> RouteInfo:
|
||||
def _calculate_straight_line_route(self, start_coords: Coordinates, end_coords: Coordinates) -> RouteInfo:
|
||||
"""
|
||||
Calculate straight-line distance as fallback when routing fails.
|
||||
"""
|
||||
@@ -356,10 +340,7 @@ class RoadTripService:
|
||||
dlat = lat2 - lat1
|
||||
dlon = lon2 - lon1
|
||||
|
||||
a = (
|
||||
math.sin(dlat / 2) ** 2
|
||||
+ math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
||||
)
|
||||
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
|
||||
c = 2 * math.asin(math.sqrt(a))
|
||||
|
||||
# Earth's radius in kilometers
|
||||
@@ -376,9 +357,7 @@ class RoadTripService:
|
||||
geometry=None,
|
||||
)
|
||||
|
||||
def find_parks_along_route(
|
||||
self, start_park: "Park", end_park: "Park", max_detour_km: float = 50
|
||||
) -> list["Park"]:
|
||||
def find_parks_along_route(self, start_park: "Park", end_park: "Park", max_detour_km: float = 50) -> list["Park"]:
|
||||
"""
|
||||
Find parks along a route within specified detour distance.
|
||||
|
||||
@@ -443,9 +422,7 @@ class RoadTripService:
|
||||
|
||||
return parks_along_route
|
||||
|
||||
def _calculate_detour_distance(
|
||||
self, start: Coordinates, end: Coordinates, waypoint: Coordinates
|
||||
) -> float | None:
|
||||
def _calculate_detour_distance(self, start: Coordinates, end: Coordinates, waypoint: Coordinates) -> float | None:
|
||||
"""
|
||||
Calculate the detour distance when visiting a waypoint.
|
||||
"""
|
||||
@@ -508,9 +485,7 @@ class RoadTripService:
|
||||
|
||||
return best_trip
|
||||
|
||||
def _optimize_trip_nearest_neighbor(
|
||||
self, park_list: list["Park"]
|
||||
) -> RoadTrip | None:
|
||||
def _optimize_trip_nearest_neighbor(self, park_list: list["Park"]) -> RoadTrip | None:
|
||||
"""
|
||||
Optimize trip using nearest neighbor heuristic (for larger lists).
|
||||
"""
|
||||
@@ -536,9 +511,7 @@ class RoadTripService:
|
||||
if not park_coords:
|
||||
continue
|
||||
|
||||
route = self.calculate_route(
|
||||
Coordinates(*current_coords), Coordinates(*park_coords)
|
||||
)
|
||||
route = self.calculate_route(Coordinates(*current_coords), Coordinates(*park_coords))
|
||||
|
||||
if route and route.distance_km < min_distance:
|
||||
min_distance = route.distance_km
|
||||
@@ -553,9 +526,7 @@ class RoadTripService:
|
||||
|
||||
return self._create_trip_from_order(ordered_parks)
|
||||
|
||||
def _create_trip_from_order(
|
||||
self, ordered_parks: list["Park"]
|
||||
) -> RoadTrip | None:
|
||||
def _create_trip_from_order(self, ordered_parks: list["Park"]) -> RoadTrip | None:
|
||||
"""
|
||||
Create a RoadTrip object from an ordered list of parks.
|
||||
"""
|
||||
@@ -576,9 +547,7 @@ class RoadTripService:
|
||||
if not from_coords or not to_coords:
|
||||
continue
|
||||
|
||||
route = self.calculate_route(
|
||||
Coordinates(*from_coords), Coordinates(*to_coords)
|
||||
)
|
||||
route = self.calculate_route(Coordinates(*from_coords), Coordinates(*to_coords))
|
||||
|
||||
if route:
|
||||
legs.append(TripLeg(from_park=from_park, to_park=to_park, route=route))
|
||||
@@ -595,9 +564,7 @@ class RoadTripService:
|
||||
total_duration_minutes=total_duration,
|
||||
)
|
||||
|
||||
def get_park_distances(
|
||||
self, center_park: "Park", radius_km: float = 100
|
||||
) -> list[dict[str, Any]]:
|
||||
def get_park_distances(self, center_park: "Park", radius_km: float = 100) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Get all parks within radius of a center park with distances.
|
||||
|
||||
@@ -621,9 +588,7 @@ class RoadTripService:
|
||||
search_distance = Distance(km=radius_km)
|
||||
|
||||
nearby_parks = (
|
||||
Park.objects.filter(
|
||||
location__point__distance_lte=(center_point, search_distance)
|
||||
)
|
||||
Park.objects.filter(location__point__distance_lte=(center_point, search_distance))
|
||||
.exclude(id=center_park.id)
|
||||
.select_related("location")
|
||||
)
|
||||
@@ -635,9 +600,7 @@ class RoadTripService:
|
||||
if not park_coords:
|
||||
continue
|
||||
|
||||
route = self.calculate_route(
|
||||
Coordinates(*center_coords), Coordinates(*park_coords)
|
||||
)
|
||||
route = self.calculate_route(Coordinates(*center_coords), Coordinates(*park_coords))
|
||||
|
||||
if route:
|
||||
results.append(
|
||||
@@ -691,9 +654,7 @@ class RoadTripService:
|
||||
if coords:
|
||||
location.set_coordinates(coords.latitude, coords.longitude)
|
||||
location.save()
|
||||
logger.info(
|
||||
f"Geocoded park '{park.name}' to {coords.latitude}, {coords.longitude}"
|
||||
)
|
||||
logger.info(f"Geocoded park '{park.name}' to {coords.latitude}, {coords.longitude}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user