mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 04:11:08 -05:00
Refactor park filtering system and templates
- Updated the filtered_list.html template to extend from base/base.html and improved layout and styling. - Removed the park_list.html template as its functionality is now integrated into the filtered list. - Added a new migration to create indexes for improved filtering performance on the parks model. - Merged migrations to maintain a clean migration history. - Implemented a ParkFilterService to handle complex filtering logic, aggregations, and caching for park filters. - Enhanced filter suggestions and popular filters retrieval methods. - Improved the overall structure and efficiency of the filtering system.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
from .roadtrip import RoadTripService
|
||||
from .park_management import ParkService, LocationService
|
||||
from .filter_service import ParkFilterService
|
||||
|
||||
__all__ = ["RoadTripService", "ParkService", "LocationService"]
|
||||
__all__ = ["RoadTripService", "ParkService", "LocationService", "ParkFilterService"]
|
||||
|
||||
304
parks/services/filter_service.py
Normal file
304
parks/services/filter_service.py
Normal file
@@ -0,0 +1,304 @@
|
||||
"""
|
||||
Park Filter Service
|
||||
|
||||
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 django.conf import settings
|
||||
from ..models import Park, Company
|
||||
from ..querysets import get_base_park_queryset
|
||||
|
||||
|
||||
class ParkFilterService:
|
||||
"""
|
||||
Service class for handling park filtering operations, aggregations,
|
||||
and providing filter suggestions based on available data.
|
||||
"""
|
||||
|
||||
CACHE_TIMEOUT = getattr(settings, "PARK_FILTER_CACHE_TIMEOUT", 300) # 5 minutes
|
||||
|
||||
def __init__(self):
|
||||
self.cache_prefix = "park_filter"
|
||||
|
||||
def get_filter_counts(
|
||||
self, base_queryset: Optional[QuerySet] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Get counts for various filter options to show users what's available.
|
||||
|
||||
Args:
|
||||
base_queryset: Optional base queryset to use for calculations
|
||||
|
||||
Returns:
|
||||
Dictionary containing counts for different filter categories
|
||||
"""
|
||||
cache_key = f"{self.cache_prefix}:filter_counts"
|
||||
cached_result = cache.get(cache_key)
|
||||
|
||||
if cached_result is not None:
|
||||
return cached_result
|
||||
|
||||
if base_queryset is None:
|
||||
base_queryset = get_base_park_queryset()
|
||||
|
||||
# Calculate filter counts
|
||||
filter_counts = {
|
||||
"total_parks": base_queryset.count(),
|
||||
"operating_parks": base_queryset.filter(status="OPERATING").count(),
|
||||
"parks_with_coasters": base_queryset.filter(coaster_count__gt=0).count(),
|
||||
"big_parks": base_queryset.filter(ride_count__gte=10).count(),
|
||||
"highly_rated": base_queryset.filter(average_rating__gte=4.0).count(),
|
||||
"park_types": self._get_park_type_counts(base_queryset),
|
||||
"top_operators": self._get_top_operators(base_queryset),
|
||||
"countries": self._get_country_counts(base_queryset),
|
||||
}
|
||||
|
||||
# Cache the result
|
||||
cache.set(cache_key, filter_counts, self.CACHE_TIMEOUT)
|
||||
return filter_counts
|
||||
|
||||
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(),
|
||||
"universal": queryset.filter(operator__name__icontains="Universal").count(),
|
||||
"six_flags": queryset.filter(operator__name__icontains="Six Flags").count(),
|
||||
"cedar_fair": queryset.filter(
|
||||
Q(operator__name__icontains="Cedar Fair")
|
||||
| Q(operator__name__icontains="Cedar Point")
|
||||
| Q(operator__name__icontains="Kings Island")
|
||||
).count(),
|
||||
}
|
||||
|
||||
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")
|
||||
.annotate(park_count=Count("id"))
|
||||
.filter(park_count__gt=0)
|
||||
.order_by("-park_count")[:limit]
|
||||
)
|
||||
|
||||
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)
|
||||
.values("location__country")
|
||||
.annotate(park_count=Count("id"))
|
||||
.filter(park_count__gt=0)
|
||||
.order_by("-park_count")[:limit]
|
||||
)
|
||||
|
||||
def get_filter_suggestions(self, query: str) -> Dict[str, List[str]]:
|
||||
"""
|
||||
Get filter suggestions based on a search query.
|
||||
|
||||
Args:
|
||||
query: Search query string
|
||||
|
||||
Returns:
|
||||
Dictionary with suggestion categories
|
||||
"""
|
||||
cache_key = f"{self.cache_prefix}:suggestions:{query.lower()}"
|
||||
cached_result = cache.get(cache_key)
|
||||
|
||||
if cached_result is not None:
|
||||
return cached_result
|
||||
|
||||
suggestions = {
|
||||
"parks": [],
|
||||
"operators": [],
|
||||
"locations": [],
|
||||
}
|
||||
|
||||
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]
|
||||
suggestions["parks"] = list(park_names)
|
||||
|
||||
# Operator suggestions
|
||||
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)
|
||||
).values_list("location__city", "location__country")[:5]
|
||||
|
||||
location_suggestions = []
|
||||
for city, country in locations:
|
||||
if city and city.lower().startswith(query.lower()):
|
||||
location_suggestions.append(city)
|
||||
elif country and country.lower().startswith(query.lower()):
|
||||
location_suggestions.append(country)
|
||||
|
||||
suggestions["locations"] = list(set(location_suggestions))[:5]
|
||||
|
||||
# Cache suggestions for a shorter time
|
||||
cache.set(cache_key, suggestions, 60) # 1 minute cache
|
||||
return suggestions
|
||||
|
||||
def get_popular_filters(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get commonly used filter combinations and popular filter values.
|
||||
|
||||
Returns:
|
||||
Dictionary containing popular filter configurations
|
||||
"""
|
||||
cache_key = f"{self.cache_prefix}:popular_filters"
|
||||
cached_result = cache.get(cache_key)
|
||||
|
||||
if cached_result is not None:
|
||||
return cached_result
|
||||
|
||||
base_qs = get_base_park_queryset()
|
||||
|
||||
popular_filters = {
|
||||
"quick_filters": [
|
||||
{
|
||||
"label": "Disney Parks",
|
||||
"filters": {"park_type": "disney"},
|
||||
"count": base_qs.filter(operator__name__icontains="Disney").count(),
|
||||
},
|
||||
{
|
||||
"label": "Parks with Coasters",
|
||||
"filters": {"has_coasters": True},
|
||||
"count": base_qs.filter(coaster_count__gt=0).count(),
|
||||
},
|
||||
{
|
||||
"label": "Highly Rated",
|
||||
"filters": {"min_rating": "4"},
|
||||
"count": base_qs.filter(average_rating__gte=4.0).count(),
|
||||
},
|
||||
{
|
||||
"label": "Major Parks",
|
||||
"filters": {"big_parks_only": True},
|
||||
"count": base_qs.filter(ride_count__gte=10).count(),
|
||||
},
|
||||
],
|
||||
"recommended_sorts": [
|
||||
{"value": "-average_rating", "label": "Highest Rated"},
|
||||
{"value": "-coaster_count", "label": "Most Coasters"},
|
||||
{"value": "name", "label": "A-Z"},
|
||||
],
|
||||
}
|
||||
|
||||
# Cache for longer since these don't change often
|
||||
cache.set(cache_key, popular_filters, self.CACHE_TIMEOUT * 2)
|
||||
return popular_filters
|
||||
|
||||
def clear_filter_cache(self) -> None:
|
||||
"""Clear all cached filter data."""
|
||||
# Simple cache clearing - delete known keys
|
||||
cache_keys = [
|
||||
f"{self.cache_prefix}:filter_counts",
|
||||
f"{self.cache_prefix}:popular_filters",
|
||||
]
|
||||
for key in cache_keys:
|
||||
cache.delete(key)
|
||||
|
||||
def get_filtered_queryset(self, filters: Dict[str, Any]) -> QuerySet: # noqa: C901
|
||||
"""
|
||||
Apply filters to get a filtered queryset with optimizations.
|
||||
|
||||
Args:
|
||||
filters: Dictionary of filter parameters
|
||||
|
||||
Returns:
|
||||
Filtered and optimized QuerySet
|
||||
"""
|
||||
queryset = (
|
||||
get_base_park_queryset()
|
||||
.select_related("operator", "property_owner", "location")
|
||||
.prefetch_related("photos", "rides__manufacturer")
|
||||
)
|
||||
|
||||
# Apply status filter
|
||||
if filters.get("status"):
|
||||
queryset = queryset.filter(status=filters["status"])
|
||||
|
||||
# Apply park type filter
|
||||
if filters.get("park_type"):
|
||||
queryset = self._apply_park_type_filter(queryset, filters["park_type"])
|
||||
|
||||
# Apply coaster filter
|
||||
if filters.get("has_coasters"):
|
||||
queryset = queryset.filter(coaster_count__gt=0)
|
||||
|
||||
# Apply rating filter
|
||||
if filters.get("min_rating"):
|
||||
try:
|
||||
min_rating = float(filters["min_rating"])
|
||||
queryset = queryset.filter(average_rating__gte=min_rating)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Apply big parks filter
|
||||
if filters.get("big_parks_only"):
|
||||
queryset = queryset.filter(ride_count__gte=10)
|
||||
|
||||
# Apply search
|
||||
if filters.get("search"):
|
||||
search_query = filters["search"]
|
||||
queryset = queryset.filter(
|
||||
Q(name__icontains=search_query)
|
||||
| Q(description__icontains=search_query)
|
||||
| Q(location__city__icontains=search_query)
|
||||
| Q(location__country__icontains=search_query)
|
||||
)
|
||||
|
||||
# Apply location filters
|
||||
if filters.get("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"]
|
||||
)
|
||||
|
||||
# Apply ordering
|
||||
if filters.get("ordering"):
|
||||
queryset = queryset.order_by(filters["ordering"])
|
||||
|
||||
return queryset.distinct()
|
||||
|
||||
def _apply_park_type_filter(self, queryset: QuerySet, park_type: str) -> QuerySet:
|
||||
"""Apply park type filter logic."""
|
||||
type_filters = {
|
||||
"disney": Q(operator__name__icontains="Disney"),
|
||||
"universal": Q(operator__name__icontains="Universal"),
|
||||
"six_flags": Q(operator__name__icontains="Six Flags"),
|
||||
"cedar_fair": (
|
||||
Q(operator__name__icontains="Cedar Fair")
|
||||
| Q(operator__name__icontains="Cedar Point")
|
||||
| Q(operator__name__icontains="Kings Island")
|
||||
| Q(operator__name__icontains="Canada's Wonderland")
|
||||
),
|
||||
"independent": ~(
|
||||
Q(operator__name__icontains="Disney")
|
||||
| Q(operator__name__icontains="Universal")
|
||||
| Q(operator__name__icontains="Six Flags")
|
||||
| Q(operator__name__icontains="Cedar Fair")
|
||||
| Q(operator__name__icontains="Cedar Point")
|
||||
),
|
||||
}
|
||||
|
||||
if park_type in type_filters:
|
||||
return queryset.filter(type_filters[park_type])
|
||||
|
||||
return queryset
|
||||
Reference in New Issue
Block a user