mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-04-02 12:58:24 -04:00
- 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
311 lines
10 KiB
Python
311 lines
10 KiB
Python
from typing import List, Dict, Any, Optional
|
|
from django.contrib.auth.models import AnonymousUser
|
|
from django.db.models import QuerySet
|
|
from django.shortcuts import get_object_or_404
|
|
from django_unicorn.components import UnicornView
|
|
|
|
from apps.parks.models import Park
|
|
from apps.rides.models import Ride
|
|
from apps.media.models import Photo
|
|
|
|
|
|
class ParkDetailView(UnicornView):
|
|
"""
|
|
Django Unicorn component for park detail page.
|
|
|
|
Handles park information display, photo management, ride listings,
|
|
location mapping, and history tracking with reactive updates.
|
|
"""
|
|
|
|
# Core park data
|
|
park: Optional[Park] = None
|
|
park_slug: str = ""
|
|
|
|
# Section data (converted to lists for caching compatibility)
|
|
rides: List[Dict[str, Any]] = []
|
|
photos: List[Dict[str, Any]] = []
|
|
history_records: List[Dict[str, Any]] = []
|
|
|
|
# UI state management
|
|
show_photo_modal: bool = False
|
|
show_all_rides: bool = False
|
|
loading_photos: bool = False
|
|
loading_rides: bool = False
|
|
loading_history: bool = False
|
|
|
|
# Photo upload state
|
|
uploading_photo: bool = False
|
|
upload_error: str = ""
|
|
upload_success: str = ""
|
|
|
|
# Map state
|
|
show_map: bool = False
|
|
map_latitude: Optional[float] = None
|
|
map_longitude: Optional[float] = None
|
|
|
|
def mount(self):
|
|
"""Initialize component with park data."""
|
|
if self.park_slug:
|
|
self.load_park_data()
|
|
|
|
def load_park_data(self):
|
|
"""Load park and related data."""
|
|
try:
|
|
# Get park with related data
|
|
park_queryset = Park.objects.select_related(
|
|
'operator',
|
|
'property_owner'
|
|
).prefetch_related(
|
|
'photos',
|
|
'rides__ride_model__manufacturer',
|
|
'location'
|
|
)
|
|
|
|
self.park = get_object_or_404(park_queryset, slug=self.park_slug)
|
|
|
|
# Load sections
|
|
self.load_rides()
|
|
self.load_photos()
|
|
self.load_history()
|
|
self.load_map_data()
|
|
|
|
except Exception as e:
|
|
# Handle park not found or other errors
|
|
self.park = None
|
|
|
|
def load_rides(self):
|
|
"""Load park rides data."""
|
|
if not self.park:
|
|
self.rides = []
|
|
return
|
|
|
|
try:
|
|
self.loading_rides = True
|
|
|
|
# Get rides with related data
|
|
rides_queryset = self.park.rides.select_related(
|
|
'ride_model__manufacturer',
|
|
'park'
|
|
).prefetch_related(
|
|
'photos'
|
|
).order_by('name')
|
|
|
|
# Convert to list for caching compatibility
|
|
self.rides = []
|
|
for ride in rides_queryset:
|
|
ride_data = {
|
|
'id': ride.id,
|
|
'name': ride.name,
|
|
'slug': ride.slug,
|
|
'category': ride.category,
|
|
'category_display': ride.get_category_display(),
|
|
'status': ride.status,
|
|
'status_display': ride.get_status_display(),
|
|
'average_rating': ride.average_rating,
|
|
'url': ride.get_absolute_url(),
|
|
'has_photos': ride.photos.exists(),
|
|
'ride_model': {
|
|
'name': ride.ride_model.name if ride.ride_model else None,
|
|
'manufacturer': ride.ride_model.manufacturer.name if ride.ride_model and ride.ride_model.manufacturer else None,
|
|
} if ride.ride_model else None
|
|
}
|
|
self.rides.append(ride_data)
|
|
|
|
except Exception as e:
|
|
self.rides = []
|
|
finally:
|
|
self.loading_rides = False
|
|
|
|
def load_photos(self):
|
|
"""Load park photos data."""
|
|
if not self.park:
|
|
self.photos = []
|
|
return
|
|
|
|
try:
|
|
self.loading_photos = True
|
|
|
|
# Get photos with related data
|
|
photos_queryset = self.park.photos.select_related(
|
|
'uploaded_by'
|
|
).order_by('-created_at')
|
|
|
|
# Convert to list for caching compatibility
|
|
self.photos = []
|
|
for photo in photos_queryset:
|
|
photo_data = {
|
|
'id': photo.id,
|
|
'image_url': photo.image.url if photo.image else None,
|
|
'image_variants': getattr(photo.image, 'variants', []) if photo.image else [],
|
|
'caption': photo.caption or '',
|
|
'uploaded_by': photo.uploaded_by.username if photo.uploaded_by else 'Anonymous',
|
|
'created_at': photo.created_at,
|
|
'is_primary': getattr(photo, 'is_primary', False),
|
|
}
|
|
self.photos.append(photo_data)
|
|
|
|
except Exception as e:
|
|
self.photos = []
|
|
finally:
|
|
self.loading_photos = False
|
|
|
|
def load_history(self):
|
|
"""Load park history records."""
|
|
if not self.park:
|
|
self.history_records = []
|
|
return
|
|
|
|
try:
|
|
self.loading_history = True
|
|
|
|
# Get history records (using pghistory)
|
|
history_queryset = self.park.history.select_related(
|
|
'history_user'
|
|
).order_by('-history_date')[:10] # Last 10 changes
|
|
|
|
# Convert to list for caching compatibility
|
|
self.history_records = []
|
|
for record in history_queryset:
|
|
# Get changes from previous record
|
|
changes = {}
|
|
try:
|
|
if hasattr(record, 'diff_against_previous'):
|
|
diff = record.diff_against_previous()
|
|
if diff:
|
|
changes = {
|
|
field: {
|
|
'old': str(change.old) if change.old is not None else 'None',
|
|
'new': str(change.new) if change.new is not None else 'None'
|
|
}
|
|
for field, change in diff.items()
|
|
if field != 'updated_at' # Skip timestamp changes
|
|
}
|
|
except:
|
|
changes = {}
|
|
|
|
history_data = {
|
|
'id': record.history_id,
|
|
'date': record.history_date,
|
|
'user': record.history_user.username if record.history_user else 'System',
|
|
'changes': changes,
|
|
'has_changes': bool(changes)
|
|
}
|
|
self.history_records.append(history_data)
|
|
|
|
except Exception as e:
|
|
self.history_records = []
|
|
finally:
|
|
self.loading_history = False
|
|
|
|
def load_map_data(self):
|
|
"""Load map coordinates if location exists."""
|
|
if not self.park:
|
|
self.show_map = False
|
|
return
|
|
|
|
try:
|
|
location = self.park.location.first()
|
|
if location and location.point:
|
|
self.map_latitude = location.point.y
|
|
self.map_longitude = location.point.x
|
|
self.show_map = True
|
|
else:
|
|
self.show_map = False
|
|
except:
|
|
self.show_map = False
|
|
|
|
# UI Event Handlers
|
|
def toggle_photo_modal(self):
|
|
"""Toggle photo upload modal."""
|
|
self.show_photo_modal = not self.show_photo_modal
|
|
if self.show_photo_modal:
|
|
self.upload_error = ""
|
|
self.upload_success = ""
|
|
|
|
def close_photo_modal(self):
|
|
"""Close photo upload modal."""
|
|
self.show_photo_modal = False
|
|
self.upload_error = ""
|
|
self.upload_success = ""
|
|
|
|
def toggle_all_rides(self):
|
|
"""Toggle between showing limited rides vs all rides."""
|
|
self.show_all_rides = not self.show_all_rides
|
|
|
|
def refresh_photos(self):
|
|
"""Refresh photos after upload."""
|
|
self.load_photos()
|
|
self.upload_success = "Photo uploaded successfully!"
|
|
|
|
# Auto-hide success message after 3 seconds
|
|
# Note: In a real implementation, you might use JavaScript for this
|
|
|
|
def refresh_data(self):
|
|
"""Refresh all park data."""
|
|
self.load_park_data()
|
|
|
|
# Computed Properties
|
|
@property
|
|
def visible_rides(self) -> List[Dict[str, Any]]:
|
|
"""Get rides to display (limited or all)."""
|
|
if self.show_all_rides:
|
|
return self.rides
|
|
return self.rides[:6] # Show first 6 rides
|
|
|
|
@property
|
|
def has_more_rides(self) -> bool:
|
|
"""Check if there are more rides to show."""
|
|
return len(self.rides) > 6
|
|
|
|
@property
|
|
def park_stats(self) -> Dict[str, Any]:
|
|
"""Get park statistics for display."""
|
|
if not self.park:
|
|
return {}
|
|
|
|
return {
|
|
'total_rides': self.park.ride_count or len(self.rides),
|
|
'coaster_count': self.park.coaster_count or 0,
|
|
'average_rating': self.park.average_rating,
|
|
'status': self.park.get_status_display() if self.park else '',
|
|
'opening_date': self.park.opening_date if self.park else None,
|
|
'website': self.park.website if self.park else None,
|
|
'operator': {
|
|
'name': self.park.operator.name if self.park and self.park.operator else None,
|
|
'slug': self.park.operator.slug if self.park and self.park.operator else None,
|
|
},
|
|
'property_owner': {
|
|
'name': self.park.property_owner.name if self.park and self.park.property_owner else None,
|
|
'slug': self.park.property_owner.slug if self.park and self.park.property_owner else None,
|
|
} if self.park and self.park.property_owner and self.park.property_owner != self.park.operator else None
|
|
}
|
|
|
|
@property
|
|
def can_upload_photos(self) -> bool:
|
|
"""Check if user can upload photos."""
|
|
if isinstance(self.request.user, AnonymousUser):
|
|
return False
|
|
return self.request.user.has_perm('media.add_photo')
|
|
|
|
@property
|
|
def formatted_location(self) -> str:
|
|
"""Get formatted location string."""
|
|
if not self.park:
|
|
return ""
|
|
|
|
try:
|
|
location = self.park.location.first()
|
|
if location:
|
|
parts = []
|
|
if location.city:
|
|
parts.append(location.city)
|
|
if location.state:
|
|
parts.append(location.state)
|
|
if location.country:
|
|
parts.append(location.country)
|
|
return ", ".join(parts)
|
|
except:
|
|
pass
|
|
|
|
return ""
|