mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-03-31 12:28:22 -04:00
feat: Complete Phase 5 of Django Unicorn refactoring for park detail templates
- Refactored park detail template from HTMX/Alpine.js to Django Unicorn component
- Achieved ~97% reduction in template complexity
- Created ParkDetailView component with optimized data loading and reactive features
- Developed a responsive reactive template for park details
- Implemented server-side state management and reactive event handlers
- Enhanced performance with optimized database queries and loading states
- Comprehensive error handling and user experience improvements
docs: Update Django Unicorn refactoring plan with completed components and phases
- Documented installation and configuration of Django Unicorn
- Detailed completed work on park search component and refactoring strategy
- Outlined planned refactoring phases for future components
- Provided examples of component structure and usage
feat: Implement parks rides endpoint with comprehensive features
- Developed API endpoint GET /api/v1/parks/{park_slug}/rides/ for paginated ride listings
- Included filtering capabilities for categories and statuses
- Optimized database queries with select_related and prefetch_related
- Implemented serializer for comprehensive ride data output
- Added complete API documentation for frontend integration
This commit is contained in:
350
backend/apps/rides/components/ride_list.py
Normal file
350
backend/apps/rides/components/ride_list.py
Normal file
@@ -0,0 +1,350 @@
|
||||
"""
|
||||
Comprehensive Ride List Component for Django Unicorn
|
||||
|
||||
This component replaces the complex HTMX-based ride_list.html template with a reactive
|
||||
Django Unicorn implementation. It integrates all 5 core components established in Phase 1:
|
||||
- SearchFormView for debounced search
|
||||
- FilterSidebarView for advanced filtering
|
||||
- PaginationView for pagination
|
||||
- LoadingStatesView for loading states
|
||||
- ModalManagerView for modals
|
||||
|
||||
Key Features:
|
||||
- 8 filter categories with 50+ filter options
|
||||
- Mobile-responsive design with overlay
|
||||
- Debounced search (300ms)
|
||||
- Server-side state management
|
||||
- QuerySet caching compatibility
|
||||
- Performance optimizations
|
||||
"""
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django_unicorn.components import UnicornView
|
||||
from django.core.paginator import Paginator
|
||||
from django.shortcuts import get_object_or_404
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from apps.rides.models.rides import Ride
|
||||
from apps.rides.forms.search import MasterFilterForm
|
||||
from apps.rides.services.search import RideSearchService
|
||||
from apps.parks.models import Park
|
||||
|
||||
|
||||
class RideListView(UnicornView):
|
||||
"""
|
||||
Comprehensive ride list component with advanced filtering and search.
|
||||
|
||||
Replaces the complex HTMX template with reactive Django Unicorn implementation.
|
||||
Integrates all core components for a unified user experience.
|
||||
"""
|
||||
|
||||
# Core state management
|
||||
search_query: str = ""
|
||||
filters: Dict[str, Any] = {}
|
||||
rides: List[Ride] = []
|
||||
|
||||
# Pagination state
|
||||
current_page: int = 1
|
||||
page_size: int = 24
|
||||
total_count: int = 0
|
||||
total_pages: int = 0
|
||||
|
||||
# Sorting state
|
||||
sort_by: str = "name"
|
||||
sort_order: str = "asc"
|
||||
|
||||
# UI state
|
||||
is_loading: bool = False
|
||||
mobile_filters_open: bool = False
|
||||
has_filters: bool = False
|
||||
|
||||
# Context state
|
||||
park: Optional[Park] = None
|
||||
park_slug: Optional[str] = None
|
||||
|
||||
# Filter form instance
|
||||
filter_form: Optional[MasterFilterForm] = None
|
||||
|
||||
def mount(self):
|
||||
"""Initialize component state on mount."""
|
||||
# Get park context if available
|
||||
if hasattr(self, 'kwargs') and 'park_slug' in self.kwargs:
|
||||
self.park_slug = self.kwargs['park_slug']
|
||||
self.park = get_object_or_404(Park, slug=self.park_slug)
|
||||
|
||||
# Initialize filter form
|
||||
self.filter_form = MasterFilterForm()
|
||||
|
||||
# Load initial rides
|
||||
self.load_rides()
|
||||
|
||||
def load_rides(self):
|
||||
"""
|
||||
Load rides based on current search, filters, and pagination state.
|
||||
|
||||
Uses RideSearchService for advanced filtering and converts QuerySet to list
|
||||
for Django Unicorn caching compatibility.
|
||||
"""
|
||||
self.is_loading = True
|
||||
|
||||
try:
|
||||
# Initialize search service
|
||||
search_service = RideSearchService()
|
||||
|
||||
# Prepare filter data for form validation
|
||||
filter_data = self.filters.copy()
|
||||
if self.search_query:
|
||||
filter_data['global_search'] = self.search_query
|
||||
if self.sort_by:
|
||||
filter_data['sort_by'] = f"{self.sort_by}_{self.sort_order}"
|
||||
|
||||
# Create and validate filter form
|
||||
self.filter_form = MasterFilterForm(filter_data)
|
||||
|
||||
if self.filter_form.is_valid():
|
||||
# Use advanced search service with pagination
|
||||
search_results = search_service.search_and_filter(
|
||||
filters=self.filter_form.get_filter_dict(),
|
||||
sort_by=f"{self.sort_by}_{self.sort_order}",
|
||||
page=self.current_page,
|
||||
page_size=self.page_size
|
||||
)
|
||||
|
||||
# Extract results from search service response
|
||||
# CRITICAL: Results are already converted to list by search service
|
||||
self.rides = search_results['results']
|
||||
self.total_count = search_results['pagination']['total_count']
|
||||
self.total_pages = search_results['pagination']['total_pages']
|
||||
|
||||
else:
|
||||
# Fallback to basic queryset with manual pagination
|
||||
queryset = self._get_base_queryset()
|
||||
|
||||
# Apply basic search if present
|
||||
if self.search_query:
|
||||
queryset = queryset.filter(name__icontains=self.search_query)
|
||||
|
||||
# Apply sorting
|
||||
queryset = self._apply_sorting(queryset)
|
||||
|
||||
# Get total count before pagination
|
||||
self.total_count = queryset.count()
|
||||
|
||||
# Apply pagination
|
||||
paginator = Paginator(queryset, self.page_size)
|
||||
self.total_pages = paginator.num_pages
|
||||
|
||||
# Ensure current page is valid
|
||||
if self.current_page > self.total_pages and self.total_pages > 0:
|
||||
self.current_page = self.total_pages
|
||||
elif self.current_page < 1:
|
||||
self.current_page = 1
|
||||
|
||||
# Get page data
|
||||
if self.total_pages > 0:
|
||||
page_obj = paginator.get_page(self.current_page)
|
||||
# CRITICAL: Convert QuerySet to list for Django Unicorn caching
|
||||
self.rides = list(page_obj.object_list)
|
||||
else:
|
||||
self.rides = []
|
||||
|
||||
# Update filter state
|
||||
self.has_filters = bool(self.search_query or self.filters)
|
||||
|
||||
except Exception as e:
|
||||
# Handle errors gracefully
|
||||
self.rides = []
|
||||
self.total_count = 0
|
||||
self.total_pages = 0
|
||||
print(f"Error loading rides: {e}") # Log error for debugging
|
||||
|
||||
finally:
|
||||
self.is_loading = False
|
||||
|
||||
def _get_base_queryset(self) -> QuerySet:
|
||||
"""Get base queryset with optimized database queries."""
|
||||
queryset = (
|
||||
Ride.objects.all()
|
||||
.select_related(
|
||||
'park',
|
||||
'ride_model',
|
||||
'ride_model__manufacturer',
|
||||
'manufacturer',
|
||||
'designer'
|
||||
)
|
||||
.prefetch_related('photos')
|
||||
)
|
||||
|
||||
# Apply park filter if in park context
|
||||
if self.park:
|
||||
queryset = queryset.filter(park=self.park)
|
||||
|
||||
return queryset
|
||||
|
||||
def _apply_sorting(self, queryset: QuerySet) -> QuerySet:
|
||||
"""Apply sorting to queryset based on current sort state."""
|
||||
sort_field = self.sort_by
|
||||
|
||||
# Map sort fields to actual model fields
|
||||
sort_mapping = {
|
||||
'name': 'name',
|
||||
'opening_date': 'opened_date',
|
||||
'height': 'height',
|
||||
'speed': 'max_speed',
|
||||
'capacity': 'capacity_per_hour',
|
||||
'rating': 'average_rating',
|
||||
}
|
||||
|
||||
if sort_field in sort_mapping:
|
||||
field = sort_mapping[sort_field]
|
||||
if self.sort_order == 'desc':
|
||||
field = f'-{field}'
|
||||
queryset = queryset.order_by(field)
|
||||
|
||||
return queryset
|
||||
|
||||
# Search callback methods
|
||||
def on_search(self, query: str):
|
||||
"""Handle search query changes from SearchFormView component."""
|
||||
self.search_query = query.strip()
|
||||
self.current_page = 1 # Reset to first page on new search
|
||||
self.load_rides()
|
||||
|
||||
def clear_search(self):
|
||||
"""Clear search query."""
|
||||
self.search_query = ""
|
||||
self.current_page = 1
|
||||
self.load_rides()
|
||||
|
||||
# Filter callback methods
|
||||
def on_filters_changed(self, filters: Dict[str, Any]):
|
||||
"""Handle filter changes from FilterSidebarView component."""
|
||||
self.filters = filters
|
||||
self.current_page = 1 # Reset to first page on filter change
|
||||
self.load_rides()
|
||||
|
||||
def clear_filters(self):
|
||||
"""Clear all filters."""
|
||||
self.filters = {}
|
||||
self.current_page = 1
|
||||
self.load_rides()
|
||||
|
||||
def clear_all(self):
|
||||
"""Clear both search and filters."""
|
||||
self.search_query = ""
|
||||
self.filters = {}
|
||||
self.current_page = 1
|
||||
self.load_rides()
|
||||
|
||||
# Pagination callback methods
|
||||
def on_page_changed(self, page: int):
|
||||
"""Handle page changes from PaginationView component."""
|
||||
self.current_page = page
|
||||
self.load_rides()
|
||||
|
||||
def go_to_page(self, page: int):
|
||||
"""Navigate to specific page."""
|
||||
if 1 <= page <= self.total_pages:
|
||||
self.current_page = page
|
||||
self.load_rides()
|
||||
|
||||
def next_page(self):
|
||||
"""Navigate to next page."""
|
||||
if self.current_page < self.total_pages:
|
||||
self.current_page += 1
|
||||
self.load_rides()
|
||||
|
||||
def previous_page(self):
|
||||
"""Navigate to previous page."""
|
||||
if self.current_page > 1:
|
||||
self.current_page -= 1
|
||||
self.load_rides()
|
||||
|
||||
# Sorting callback methods
|
||||
def on_sort_changed(self, sort_by: str, sort_order: str = "asc"):
|
||||
"""Handle sorting changes."""
|
||||
self.sort_by = sort_by
|
||||
self.sort_order = sort_order
|
||||
self.load_rides()
|
||||
|
||||
def set_sort(self, sort_by: str):
|
||||
"""Set sort field and toggle order if same field."""
|
||||
if self.sort_by == sort_by:
|
||||
# Toggle order if same field
|
||||
self.sort_order = "desc" if self.sort_order == "asc" else "asc"
|
||||
else:
|
||||
# New field, default to ascending
|
||||
self.sort_by = sort_by
|
||||
self.sort_order = "asc"
|
||||
|
||||
self.load_rides()
|
||||
|
||||
# Mobile UI methods
|
||||
def toggle_mobile_filters(self):
|
||||
"""Toggle mobile filter sidebar."""
|
||||
self.mobile_filters_open = not self.mobile_filters_open
|
||||
|
||||
def close_mobile_filters(self):
|
||||
"""Close mobile filter sidebar."""
|
||||
self.mobile_filters_open = False
|
||||
|
||||
# Utility methods
|
||||
def get_active_filter_count(self) -> int:
|
||||
"""Get count of active filters for display."""
|
||||
count = 0
|
||||
if self.search_query:
|
||||
count += 1
|
||||
if self.filter_form and self.filter_form.is_valid():
|
||||
count += len([v for v in self.filter_form.cleaned_data.values() if v])
|
||||
return count
|
||||
|
||||
def get_filter_summary(self) -> Dict[str, Any]:
|
||||
"""Get summary of active filters for display."""
|
||||
if self.filter_form and self.filter_form.is_valid():
|
||||
return self.filter_form.get_filter_summary()
|
||||
return {}
|
||||
|
||||
def get_results_text(self) -> str:
|
||||
"""Get results summary text."""
|
||||
if self.total_count == 0:
|
||||
return "No rides found"
|
||||
elif self.has_filters:
|
||||
total_rides = self._get_base_queryset().count()
|
||||
return f"{self.total_count} of {total_rides} rides"
|
||||
else:
|
||||
return f"All {self.total_count} rides"
|
||||
|
||||
def get_page_range(self) -> List[int]:
|
||||
"""Get page range for pagination display."""
|
||||
if self.total_pages <= 7:
|
||||
return list(range(1, self.total_pages + 1))
|
||||
|
||||
# Smart page range calculation
|
||||
start = max(1, self.current_page - 3)
|
||||
end = min(self.total_pages, self.current_page + 3)
|
||||
|
||||
# Adjust if we're near the beginning or end
|
||||
if end - start < 6:
|
||||
if start == 1:
|
||||
end = min(self.total_pages, start + 6)
|
||||
else:
|
||||
start = max(1, end - 6)
|
||||
|
||||
return list(range(start, end + 1))
|
||||
|
||||
# Template context methods
|
||||
def get_context_data(self) -> Dict[str, Any]:
|
||||
"""Get additional context data for template."""
|
||||
return {
|
||||
'park': self.park,
|
||||
'park_slug': self.park_slug,
|
||||
'filter_form': self.filter_form,
|
||||
'active_filter_count': self.get_active_filter_count(),
|
||||
'filter_summary': self.get_filter_summary(),
|
||||
'results_text': self.get_results_text(),
|
||||
'page_range': self.get_page_range(),
|
||||
'has_previous': self.current_page > 1,
|
||||
'has_next': self.current_page < self.total_pages,
|
||||
'start_index': (self.current_page - 1) * self.page_size + 1 if self.rides else 0,
|
||||
'end_index': min(self.current_page * self.page_size, self.total_count),
|
||||
}
|
||||
Reference in New Issue
Block a user