mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-21 22:11:07 -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:
154
parks/views.py
154
parks/views.py
@@ -12,6 +12,7 @@ from core.views.views import SlugRedirectMixin
|
||||
from .filters import ParkFilter
|
||||
from .forms import ParkForm
|
||||
from .models import Park, ParkArea, ParkReview as Review
|
||||
from .services import ParkFilterService
|
||||
from django.http import (
|
||||
HttpResponseRedirect,
|
||||
HttpResponse,
|
||||
@@ -29,7 +30,7 @@ from decimal import InvalidOperation
|
||||
from django.views.generic import DetailView, ListView, CreateView, UpdateView
|
||||
import requests
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
from typing import Any, Optional, cast, Literal
|
||||
from typing import Any, Optional, cast, Literal, Dict
|
||||
|
||||
# Constants
|
||||
PARK_DETAIL_URL = "parks:park_detail"
|
||||
@@ -44,7 +45,7 @@ ViewMode = Literal["grid", "list"]
|
||||
|
||||
|
||||
def normalize_osm_result(result: dict) -> dict:
|
||||
"""Normalize OpenStreetMap result to a consistent format with enhanced address details"""
|
||||
"""Normalize OpenStreetMap result to a consistent format with enhanced address details""" # noqa: E501
|
||||
from .location_utils import get_english_name, normalize_coordinate
|
||||
|
||||
# Get address details
|
||||
@@ -229,6 +230,10 @@ class ParkListView(HTMXFilterableMixin, ListView):
|
||||
filter_class = ParkFilter
|
||||
paginate_by = 20
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.filter_service = ParkFilterService()
|
||||
|
||||
def get_template_names(self) -> list[str]:
|
||||
"""Return park_list_item.html for HTMX requests"""
|
||||
if self.request.htmx:
|
||||
@@ -240,35 +245,60 @@ class ParkListView(HTMXFilterableMixin, ListView):
|
||||
return get_view_mode(self.request)
|
||||
|
||||
def get_queryset(self) -> QuerySet[Park]:
|
||||
"""Get base queryset with annotations and apply filters"""
|
||||
"""Get optimized queryset with filter service"""
|
||||
try:
|
||||
queryset = get_base_park_queryset()
|
||||
# Use filter service for optimized filtering
|
||||
filter_params = dict(self.request.GET.items())
|
||||
queryset = self.filter_service.get_filtered_queryset(filter_params)
|
||||
|
||||
# Also create filterset for form rendering
|
||||
self.filterset = self.filter_class(self.request.GET, queryset=queryset)
|
||||
return self.filterset.qs
|
||||
except Exception as e:
|
||||
messages.error(self.request, f"Error loading parks: {str(e)}")
|
||||
queryset = self.model.objects.none()
|
||||
|
||||
# Always initialize filterset, even if queryset failed
|
||||
self.filterset = self.filter_class(self.request.GET, queryset=queryset)
|
||||
return self.filterset.qs
|
||||
self.filterset = self.filter_class(self.request.GET, queryset=queryset)
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||
"""Add view_mode and other context data"""
|
||||
"""Add enhanced context with filter stats and suggestions"""
|
||||
try:
|
||||
# Initialize filterset even if queryset fails
|
||||
# Initialize filterset if not exists
|
||||
if not hasattr(self, "filterset"):
|
||||
self.filterset = self.filter_class(
|
||||
self.request.GET, queryset=self.model.objects.none()
|
||||
)
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Add filter service data
|
||||
filter_counts = self.filter_service.get_filter_counts()
|
||||
popular_filters = self.filter_service.get_popular_filters()
|
||||
|
||||
context.update(
|
||||
{
|
||||
"view_mode": self.get_view_mode(),
|
||||
"is_search": bool(self.request.GET.get("search")),
|
||||
"search_query": self.request.GET.get("search", ""),
|
||||
"filter_counts": filter_counts,
|
||||
"popular_filters": popular_filters,
|
||||
"total_results": (
|
||||
context.get("paginator").count
|
||||
if context.get("paginator")
|
||||
else 0
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# Add filter suggestions for search queries
|
||||
search_query = self.request.GET.get("search", "")
|
||||
if search_query:
|
||||
context["filter_suggestions"] = (
|
||||
self.filter_service.get_filter_suggestions(search_query)
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
except Exception as e:
|
||||
messages.error(self.request, f"Error applying filters: {str(e)}")
|
||||
# Ensure filterset exists in error case
|
||||
@@ -284,6 +314,108 @@ class ParkListView(HTMXFilterableMixin, ListView):
|
||||
"search_query": self.request.GET.get("search", ""),
|
||||
}
|
||||
|
||||
def _get_clean_filter_params(self) -> Dict[str, Any]:
|
||||
"""Extract and clean filter parameters from request."""
|
||||
filter_params = {}
|
||||
|
||||
# Define valid filter fields
|
||||
valid_filters = {
|
||||
"status",
|
||||
"operator",
|
||||
"park_type",
|
||||
"has_coasters",
|
||||
"min_rating",
|
||||
"big_parks_only",
|
||||
"ordering",
|
||||
"search",
|
||||
}
|
||||
|
||||
for param, value in self.request.GET.items():
|
||||
if param in valid_filters and value:
|
||||
# Skip pagination parameter
|
||||
if param == "page":
|
||||
continue
|
||||
|
||||
# Clean and validate the value
|
||||
filter_params[param] = self._clean_filter_value(param, value)
|
||||
|
||||
return {k: v for k, v in filter_params.items() if v is not None}
|
||||
|
||||
def _clean_filter_value(self, param: str, value: str) -> Optional[Any]:
|
||||
"""Clean and validate a single filter value."""
|
||||
if param in ("has_coasters", "big_parks_only"):
|
||||
# Boolean filters
|
||||
return value.lower() in ("true", "1", "yes", "on")
|
||||
elif param == "min_rating":
|
||||
# Numeric filter
|
||||
try:
|
||||
rating = float(value)
|
||||
if 0 <= rating <= 5:
|
||||
return str(rating)
|
||||
except (ValueError, TypeError):
|
||||
pass # Skip invalid ratings
|
||||
return None
|
||||
elif param == "search":
|
||||
# Search filter
|
||||
clean_search = value.strip()
|
||||
return clean_search if clean_search else None
|
||||
else:
|
||||
# String filters
|
||||
return value.strip()
|
||||
|
||||
def _build_filter_query_string(self, filter_params: Dict[str, Any]) -> str:
|
||||
"""Build query string from filter parameters."""
|
||||
from urllib.parse import urlencode
|
||||
|
||||
# Convert boolean values to strings for URL
|
||||
url_params = {}
|
||||
for key, value in filter_params.items():
|
||||
if isinstance(value, bool):
|
||||
url_params[key] = "true" if value else "false"
|
||||
else:
|
||||
url_params[key] = str(value)
|
||||
|
||||
return urlencode(url_params)
|
||||
|
||||
def _get_pagination_urls(
|
||||
self, page_obj, filter_params: Dict[str, Any]
|
||||
) -> Dict[str, str]:
|
||||
"""Generate pagination URLs that preserve filter state."""
|
||||
|
||||
base_query = self._build_filter_query_string(filter_params)
|
||||
pagination_urls = {}
|
||||
|
||||
if page_obj.has_previous():
|
||||
prev_params = (
|
||||
f"{base_query}&page={page_obj.previous_page_number()}"
|
||||
if base_query
|
||||
else f"page={page_obj.previous_page_number()}"
|
||||
)
|
||||
pagination_urls["previous_url"] = f"?{prev_params}"
|
||||
|
||||
if page_obj.has_next():
|
||||
next_params = (
|
||||
f"{base_query}&page={page_obj.next_page_number()}"
|
||||
if base_query
|
||||
else f"page={page_obj.next_page_number()}"
|
||||
)
|
||||
pagination_urls["next_url"] = f"?{next_params}"
|
||||
|
||||
# First and last page URLs
|
||||
if page_obj.number > 1:
|
||||
first_params = f"{base_query}&page=1" if base_query else "page=1"
|
||||
pagination_urls["first_url"] = f"?{first_params}"
|
||||
|
||||
if page_obj.number < page_obj.paginator.num_pages:
|
||||
last_params = (
|
||||
f"{base_query}&page={page_obj.paginator.num_pages}"
|
||||
if base_query
|
||||
else f"page={page_obj.paginator.num_pages}"
|
||||
)
|
||||
pagination_urls["last_url"] = f"?{last_params}"
|
||||
|
||||
return pagination_urls
|
||||
|
||||
|
||||
def search_parks(request: HttpRequest) -> HttpResponse:
|
||||
"""Search parks and return results using park_list_item.html"""
|
||||
@@ -499,7 +631,7 @@ class ParkUpdateView(LoginRequiredMixin, UpdateView):
|
||||
Decimal("0.000001"), rounding=ROUND_DOWN
|
||||
)
|
||||
|
||||
def form_valid(self, form: ParkForm) -> HttpResponse:
|
||||
def form_valid(self, form: ParkForm) -> HttpResponse: # noqa: C901
|
||||
self.normalize_coordinates(form)
|
||||
changes = self.prepare_changes_data(form.cleaned_data)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user