mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 06:11:07 -05:00
Enhance park search with autocomplete and improved filtering options
Introduce autocomplete for park searches, optimize park data fetching with select_related and prefetch_related, add new API endpoints for autocomplete and quick filters, and refactor the park list view to use new Django Cotton components for a more dynamic and user-friendly experience. Replit-Commit-Author: Agent Replit-Commit-Session-Id: c446bc9e-66df-438c-a86c-f53e6da13649 Replit-Commit-Checkpoint-Type: intermediate_checkpoint
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
from django.urls import path, include
|
||||
from . import views, views_search
|
||||
from . import views, views_search, views_autocomplete
|
||||
from apps.rides.views import ParkSingleCategoryListView
|
||||
from .views_roadtrip import (
|
||||
RoadTripPlannerView,
|
||||
@@ -30,6 +30,9 @@ urlpatterns = [
|
||||
path("areas/", views.get_park_areas, name="get_park_areas"),
|
||||
path("suggest_parks/", views_search.suggest_parks, name="suggest_parks"),
|
||||
path("search/", views.search_parks, name="search_parks"),
|
||||
# Enhanced search endpoints
|
||||
path("api/autocomplete/", views_autocomplete.ParkAutocompleteView.as_view(), name="park_autocomplete"),
|
||||
path("api/quick-filters/", views_autocomplete.QuickFilterSuggestionsView.as_view(), name="quick_filter_suggestions"),
|
||||
# Road trip planning URLs
|
||||
path("roadtrip/", RoadTripPlannerView.as_view(), name="roadtrip_planner"),
|
||||
path("roadtrip/create/", CreateTripView.as_view(), name="roadtrip_create"),
|
||||
|
||||
@@ -245,15 +245,56 @@ class ParkListView(HTMXFilterableMixin, ListView):
|
||||
return get_view_mode(self.request)
|
||||
|
||||
def get_queryset(self) -> QuerySet[Park]:
|
||||
"""Get optimized queryset with filter service"""
|
||||
"""Get optimized queryset with enhanced filtering and proper relations"""
|
||||
try:
|
||||
# Use filter service for optimized filtering
|
||||
filter_params = dict(self.request.GET.items())
|
||||
queryset = self.filter_service.get_filtered_queryset(filter_params)
|
||||
# Start with optimized base queryset
|
||||
queryset = (
|
||||
get_base_park_queryset()
|
||||
.select_related(
|
||||
'operator',
|
||||
'property_owner',
|
||||
'location',
|
||||
'banner_image',
|
||||
'card_image'
|
||||
)
|
||||
.prefetch_related(
|
||||
'photos',
|
||||
'rides__manufacturer',
|
||||
'areas'
|
||||
)
|
||||
)
|
||||
|
||||
# Also create filterset for form rendering
|
||||
self.filterset = self.filter_class(self.request.GET, queryset=queryset)
|
||||
# Use filter service for enhanced filtering
|
||||
filter_params = self._get_clean_filter_params()
|
||||
|
||||
# Apply ordering
|
||||
ordering = self.request.GET.get('ordering', 'name')
|
||||
if ordering:
|
||||
# Validate ordering to prevent SQL injection
|
||||
valid_orderings = [
|
||||
'name', '-name',
|
||||
'average_rating', '-average_rating',
|
||||
'coaster_count', '-coaster_count',
|
||||
'ride_count', '-ride_count',
|
||||
'opening_date', '-opening_date'
|
||||
]
|
||||
if ordering in valid_orderings:
|
||||
queryset = queryset.order_by(ordering)
|
||||
else:
|
||||
queryset = queryset.order_by('name') # Default fallback
|
||||
|
||||
# Apply other filters through service
|
||||
filtered_queryset = self.filter_service.get_filtered_queryset(filter_params)
|
||||
|
||||
# Combine with optimized queryset maintaining the optimizations
|
||||
final_queryset = queryset.filter(
|
||||
pk__in=filtered_queryset.values_list('pk', flat=True)
|
||||
)
|
||||
|
||||
# Create filterset for form rendering
|
||||
self.filterset = self.filter_class(self.request.GET, queryset=final_queryset)
|
||||
return self.filterset.qs
|
||||
|
||||
except Exception as e:
|
||||
messages.error(self.request, f"Error loading parks: {str(e)}")
|
||||
queryset = self.model.objects.none()
|
||||
@@ -275,6 +316,12 @@ class ParkListView(HTMXFilterableMixin, ListView):
|
||||
filter_counts = self.filter_service.get_filter_counts()
|
||||
popular_filters = self.filter_service.get_popular_filters()
|
||||
|
||||
# Calculate active filters for chips component
|
||||
active_filters = {}
|
||||
for key, value in self.request.GET.items():
|
||||
if key not in ['page', 'view_mode'] and value:
|
||||
active_filters[key] = value
|
||||
|
||||
context.update(
|
||||
{
|
||||
"view_mode": self.get_view_mode(),
|
||||
@@ -282,6 +329,9 @@ class ParkListView(HTMXFilterableMixin, ListView):
|
||||
"search_query": self.request.GET.get("search", ""),
|
||||
"filter_counts": filter_counts,
|
||||
"popular_filters": popular_filters,
|
||||
"active_filters": active_filters,
|
||||
"filter_count": len(active_filters),
|
||||
"current_ordering": self.request.GET.get("ordering", "name"),
|
||||
"total_results": (
|
||||
context.get("paginator").count
|
||||
if context.get("paginator")
|
||||
|
||||
178
apps/parks/views_autocomplete.py
Normal file
178
apps/parks/views_autocomplete.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""
|
||||
Park search autocomplete views for enhanced search functionality.
|
||||
Provides fast, cached autocomplete suggestions for park search.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any
|
||||
from django.http import JsonResponse
|
||||
from django.views import View
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Q
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_page
|
||||
|
||||
from .models import Park
|
||||
from .models.companies import Company
|
||||
from .services.filter_service import ParkFilterService
|
||||
|
||||
|
||||
class ParkAutocompleteView(View):
|
||||
"""
|
||||
Provides autocomplete suggestions for park search.
|
||||
Returns JSON with park names, operators, and location suggestions.
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
"""Handle GET request for autocomplete suggestions."""
|
||||
query = request.GET.get('q', '').strip()
|
||||
|
||||
if len(query) < 2:
|
||||
return JsonResponse({
|
||||
'suggestions': [],
|
||||
'message': 'Type at least 2 characters to search'
|
||||
})
|
||||
|
||||
# Check cache first
|
||||
cache_key = f"park_autocomplete:{query.lower()}"
|
||||
cached_result = cache.get(cache_key)
|
||||
|
||||
if cached_result:
|
||||
return JsonResponse(cached_result)
|
||||
|
||||
# Generate suggestions
|
||||
suggestions = self._get_suggestions(query)
|
||||
|
||||
# Cache results for 5 minutes
|
||||
result = {
|
||||
'suggestions': suggestions,
|
||||
'query': query
|
||||
}
|
||||
cache.set(cache_key, result, 300)
|
||||
|
||||
return JsonResponse(result)
|
||||
|
||||
def _get_suggestions(self, query: str) -> List[Dict[str, Any]]:
|
||||
"""Generate autocomplete suggestions based on query."""
|
||||
suggestions = []
|
||||
|
||||
# Park name suggestions (top 5)
|
||||
park_suggestions = self._get_park_suggestions(query)
|
||||
suggestions.extend(park_suggestions)
|
||||
|
||||
# Operator suggestions (top 3)
|
||||
operator_suggestions = self._get_operator_suggestions(query)
|
||||
suggestions.extend(operator_suggestions)
|
||||
|
||||
# Location suggestions (top 3)
|
||||
location_suggestions = self._get_location_suggestions(query)
|
||||
suggestions.extend(location_suggestions)
|
||||
|
||||
# Remove duplicates and limit results
|
||||
seen = set()
|
||||
unique_suggestions = []
|
||||
for suggestion in suggestions:
|
||||
key = suggestion['name'].lower()
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique_suggestions.append(suggestion)
|
||||
|
||||
return unique_suggestions[:10] # Limit to 10 suggestions
|
||||
|
||||
def _get_park_suggestions(self, query: str) -> List[Dict[str, Any]]:
|
||||
"""Get park name suggestions."""
|
||||
parks = Park.objects.filter(
|
||||
name__icontains=query,
|
||||
status='OPERATING'
|
||||
).select_related('operator').order_by('name')[:5]
|
||||
|
||||
suggestions = []
|
||||
for park in parks:
|
||||
suggestion = {
|
||||
'name': park.name,
|
||||
'type': 'park',
|
||||
'operator': park.operator.name if park.operator else None,
|
||||
'url': f'/parks/{park.slug}/' if park.slug else None
|
||||
}
|
||||
suggestions.append(suggestion)
|
||||
|
||||
return suggestions
|
||||
|
||||
def _get_operator_suggestions(self, query: str) -> List[Dict[str, Any]]:
|
||||
"""Get operator suggestions."""
|
||||
operators = Company.objects.filter(
|
||||
roles__contains=['OPERATOR'],
|
||||
name__icontains=query
|
||||
).order_by('name')[:3]
|
||||
|
||||
suggestions = []
|
||||
for operator in operators:
|
||||
suggestion = {
|
||||
'name': operator.name,
|
||||
'type': 'operator',
|
||||
'park_count': operator.operated_parks.filter(status='OPERATING').count()
|
||||
}
|
||||
suggestions.append(suggestion)
|
||||
|
||||
return suggestions
|
||||
|
||||
def _get_location_suggestions(self, query: str) -> List[Dict[str, Any]]:
|
||||
"""Get location (city/country) suggestions."""
|
||||
# Get unique cities
|
||||
city_parks = Park.objects.filter(
|
||||
location__city__icontains=query,
|
||||
status='OPERATING'
|
||||
).select_related('location').order_by('location__city').distinct()[:2]
|
||||
|
||||
# Get unique countries
|
||||
country_parks = Park.objects.filter(
|
||||
location__country__icontains=query,
|
||||
status='OPERATING'
|
||||
).select_related('location').order_by('location__country').distinct()[:2]
|
||||
|
||||
suggestions = []
|
||||
|
||||
# Add city suggestions
|
||||
for park in city_parks:
|
||||
if park.location and park.location.city:
|
||||
city_name = park.location.city
|
||||
if park.location.country:
|
||||
city_name += f", {park.location.country}"
|
||||
|
||||
suggestion = {
|
||||
'name': city_name,
|
||||
'type': 'location',
|
||||
'location_type': 'city'
|
||||
}
|
||||
suggestions.append(suggestion)
|
||||
|
||||
# Add country suggestions
|
||||
for park in country_parks:
|
||||
if park.location and park.location.country:
|
||||
suggestion = {
|
||||
'name': park.location.country,
|
||||
'type': 'location',
|
||||
'location_type': 'country'
|
||||
}
|
||||
suggestions.append(suggestion)
|
||||
|
||||
return suggestions
|
||||
|
||||
|
||||
@method_decorator(cache_page(60 * 5), name='dispatch') # Cache for 5 minutes
|
||||
class QuickFilterSuggestionsView(View):
|
||||
"""
|
||||
Provides quick filter suggestions and popular filters.
|
||||
Used for search dropdown quick actions.
|
||||
"""
|
||||
|
||||
def get(self, request):
|
||||
"""Handle GET request for quick filter suggestions."""
|
||||
filter_service = ParkFilterService()
|
||||
popular_filters = filter_service.get_popular_filters()
|
||||
filter_counts = filter_service.get_filter_counts()
|
||||
|
||||
return JsonResponse({
|
||||
'quick_filters': popular_filters.get('quick_filters', []),
|
||||
'filter_counts': filter_counts,
|
||||
'recommended_sorts': popular_filters.get('recommended_sorts', [])
|
||||
})
|
||||
Reference in New Issue
Block a user