mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 13:31:08 -05:00
- Created critical functionality audit report identifying 7 critical issues affecting production readiness. - Added design assessment report highlighting exceptional design quality and minor cosmetic fixes needed. - Documented non-authenticated features testing results confirming successful functionality and public access. - Implemented ride search form with autocomplete functionality and corresponding templates for search results. - Developed tests for ride autocomplete functionality, ensuring proper filtering and authentication checks.
474 lines
16 KiB
Python
474 lines
16 KiB
Python
from typing import Any, Dict, Optional, Tuple, Union, cast, Type
|
|
from django.views.generic import DetailView, ListView, CreateView, UpdateView
|
|
from django.shortcuts import get_object_or_404, render
|
|
from django.urls import reverse
|
|
from django.db.models import Q, Model
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.contrib import messages
|
|
from django.http import HttpRequest, HttpResponse, Http404
|
|
from django.db.models import Count
|
|
from .models import (
|
|
Ride, RollerCoasterStats, RideModel, RideEvent,
|
|
CATEGORY_CHOICES
|
|
)
|
|
from .forms import RideForm
|
|
from parks.models import Park
|
|
from core.views import SlugRedirectMixin
|
|
from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin
|
|
from moderation.models import EditSubmission
|
|
from companies.models import Manufacturer
|
|
from designers.models import Designer
|
|
|
|
|
|
class ParkContextRequired:
|
|
"""Mixin to require park context for views"""
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if 'park_slug' not in self.kwargs:
|
|
raise Http404("Park context is required")
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
def show_coaster_fields(request: HttpRequest) -> HttpResponse:
|
|
"""Show roller coaster specific fields based on category selection"""
|
|
category = request.GET.get('category')
|
|
if category != 'RC': # Only show for roller coasters
|
|
return HttpResponse('')
|
|
return render(request, "rides/partials/coaster_fields.html")
|
|
|
|
|
|
class RideDetailView(HistoryMixin, DetailView):
|
|
"""View for displaying ride details"""
|
|
model = Ride
|
|
template_name = 'rides/ride_detail.html'
|
|
slug_url_kwarg = 'ride_slug'
|
|
|
|
def get_queryset(self):
|
|
"""Get ride for the specific park if park_slug is provided"""
|
|
queryset = Ride.objects.all().select_related(
|
|
'park',
|
|
'ride_model',
|
|
'ride_model__manufacturer'
|
|
).prefetch_related('photos')
|
|
|
|
if 'park_slug' in self.kwargs:
|
|
queryset = queryset.filter(park__slug=self.kwargs['park_slug'])
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add context data"""
|
|
context = super().get_context_data(**kwargs)
|
|
if 'park_slug' in self.kwargs:
|
|
context['park_slug'] = self.kwargs['park_slug']
|
|
context['park'] = self.object.park
|
|
|
|
# Add history records
|
|
context['history'] = RideEvent.objects.filter(
|
|
pgh_obj_id=self.object.id
|
|
).order_by('-pgh_created_at')
|
|
|
|
return context
|
|
|
|
|
|
class RideCreateView(LoginRequiredMixin, ParkContextRequired, CreateView):
|
|
"""View for creating a new ride"""
|
|
model = Ride
|
|
form_class = RideForm
|
|
template_name = 'rides/ride_form.html'
|
|
|
|
def get_success_url(self):
|
|
"""Get URL to redirect to after successful creation"""
|
|
return reverse('parks:rides:ride_detail', kwargs={
|
|
'park_slug': self.park.slug,
|
|
'ride_slug': self.object.slug
|
|
})
|
|
|
|
def get_form_kwargs(self):
|
|
"""Pass park to the form"""
|
|
kwargs = super().get_form_kwargs()
|
|
self.park = get_object_or_404(Park, slug=self.kwargs['park_slug'])
|
|
kwargs['park'] = self.park
|
|
return kwargs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add park and park_slug to context"""
|
|
context = super().get_context_data(**kwargs)
|
|
context['park'] = self.park
|
|
context['park_slug'] = self.park.slug
|
|
context['is_edit'] = False
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
"""Handle form submission including new items"""
|
|
# Check for new manufacturer
|
|
manufacturer_name = form.cleaned_data.get('manufacturer_search')
|
|
if manufacturer_name and not form.cleaned_data.get('manufacturer'):
|
|
EditSubmission.objects.create(
|
|
user=self.request.user,
|
|
content_type=ContentType.objects.get_for_model(Manufacturer),
|
|
submission_type="CREATE",
|
|
changes={"name": manufacturer_name},
|
|
)
|
|
|
|
# Check for new designer
|
|
designer_name = form.cleaned_data.get('designer_search')
|
|
if designer_name and not form.cleaned_data.get('designer'):
|
|
EditSubmission.objects.create(
|
|
user=self.request.user,
|
|
content_type=ContentType.objects.get_for_model(Designer),
|
|
submission_type="CREATE",
|
|
changes={"name": designer_name},
|
|
)
|
|
|
|
# Check for new ride model
|
|
ride_model_name = form.cleaned_data.get('ride_model_search')
|
|
manufacturer = form.cleaned_data.get('manufacturer')
|
|
if ride_model_name and not form.cleaned_data.get('ride_model') and manufacturer:
|
|
EditSubmission.objects.create(
|
|
user=self.request.user,
|
|
content_type=ContentType.objects.get_for_model(RideModel),
|
|
submission_type="CREATE",
|
|
changes={
|
|
"name": ride_model_name,
|
|
"manufacturer": manufacturer.id
|
|
},
|
|
)
|
|
|
|
return super().form_valid(form)
|
|
|
|
|
|
class RideUpdateView(LoginRequiredMixin, ParkContextRequired, EditSubmissionMixin, UpdateView):
|
|
"""View for updating an existing ride"""
|
|
model = Ride
|
|
form_class = RideForm
|
|
template_name = 'rides/ride_form.html'
|
|
slug_url_kwarg = 'ride_slug'
|
|
|
|
def get_success_url(self):
|
|
"""Get URL to redirect to after successful update"""
|
|
return reverse('parks:rides:ride_detail', kwargs={
|
|
'park_slug': self.park.slug,
|
|
'ride_slug': self.object.slug
|
|
})
|
|
|
|
def get_queryset(self):
|
|
"""Get ride for the specific park"""
|
|
return Ride.objects.filter(park__slug=self.kwargs['park_slug'])
|
|
|
|
def get_form_kwargs(self):
|
|
"""Pass park to the form"""
|
|
kwargs = super().get_form_kwargs()
|
|
self.park = get_object_or_404(Park, slug=self.kwargs['park_slug'])
|
|
kwargs['park'] = self.park
|
|
return kwargs
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add park and park_slug to context"""
|
|
context = super().get_context_data(**kwargs)
|
|
context['park'] = self.park
|
|
context['park_slug'] = self.park.slug
|
|
context['is_edit'] = True
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
"""Handle form submission including new items"""
|
|
# Check for new manufacturer
|
|
manufacturer_name = form.cleaned_data.get('manufacturer_search')
|
|
if manufacturer_name and not form.cleaned_data.get('manufacturer'):
|
|
EditSubmission.objects.create(
|
|
user=self.request.user,
|
|
content_type=ContentType.objects.get_for_model(Manufacturer),
|
|
submission_type="CREATE",
|
|
changes={"name": manufacturer_name}
|
|
)
|
|
|
|
# Check for new designer
|
|
designer_name = form.cleaned_data.get('designer_search')
|
|
if designer_name and not form.cleaned_data.get('designer'):
|
|
EditSubmission.objects.create(
|
|
user=self.request.user,
|
|
content_type=ContentType.objects.get_for_model(Designer),
|
|
submission_type="CREATE",
|
|
changes={"name": designer_name}
|
|
)
|
|
|
|
# Check for new ride model
|
|
ride_model_name = form.cleaned_data.get('ride_model_search')
|
|
manufacturer = form.cleaned_data.get('manufacturer')
|
|
if ride_model_name and not form.cleaned_data.get('ride_model') and manufacturer:
|
|
EditSubmission.objects.create(
|
|
user=self.request.user,
|
|
content_type=ContentType.objects.get_for_model(RideModel),
|
|
submission_type="CREATE",
|
|
changes={
|
|
"name": ride_model_name,
|
|
"manufacturer": manufacturer.id
|
|
}
|
|
)
|
|
|
|
return super().form_valid(form)
|
|
|
|
|
|
class RideListView(ListView):
|
|
"""View for displaying a list of rides"""
|
|
model = Ride
|
|
template_name = 'rides/ride_list.html'
|
|
context_object_name = 'rides'
|
|
|
|
def get_queryset(self):
|
|
"""Get filtered rides based on search and filters"""
|
|
queryset = Ride.objects.all().select_related(
|
|
'park',
|
|
'ride_model',
|
|
'ride_model__manufacturer'
|
|
).prefetch_related('photos')
|
|
|
|
# Park filter
|
|
if 'park_slug' in self.kwargs:
|
|
self.park = get_object_or_404(Park, slug=self.kwargs['park_slug'])
|
|
queryset = queryset.filter(park=self.park)
|
|
|
|
# Search term handling
|
|
search = self.request.GET.get('q', '').strip()
|
|
if search:
|
|
# Split search terms for more flexible matching
|
|
search_terms = search.split()
|
|
search_query = Q()
|
|
|
|
for term in search_terms:
|
|
term_query = Q(
|
|
name__icontains=term
|
|
) | Q(
|
|
park__name__icontains=term
|
|
) | Q(
|
|
description__icontains=term
|
|
)
|
|
search_query &= term_query
|
|
|
|
queryset = queryset.filter(search_query)
|
|
|
|
# Category filter
|
|
category = self.request.GET.get('category')
|
|
if category and category != 'all':
|
|
queryset = queryset.filter(category=category)
|
|
|
|
# Operating status filter
|
|
if self.request.GET.get('operating') == 'true':
|
|
queryset = queryset.filter(status='operating')
|
|
|
|
return queryset
|
|
|
|
def get_template_names(self):
|
|
"""Return appropriate template based on request type"""
|
|
if self.request.htmx:
|
|
return ["rides/partials/ride_list_results.html"]
|
|
return [self.template_name]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add park and category choices to context"""
|
|
context = super().get_context_data(**kwargs)
|
|
if hasattr(self, 'park'):
|
|
context['park'] = self.park
|
|
context['park_slug'] = self.kwargs['park_slug']
|
|
context['category_choices'] = CATEGORY_CHOICES
|
|
return context
|
|
|
|
|
|
class SingleCategoryListView(ListView):
|
|
"""View for displaying rides of a specific category"""
|
|
model = Ride
|
|
template_name = 'rides/park_category_list.html'
|
|
context_object_name = 'rides'
|
|
|
|
def get_queryset(self):
|
|
"""Get rides filtered by category and optionally by park"""
|
|
category = self.kwargs.get('category')
|
|
queryset = Ride.objects.filter(
|
|
category=category
|
|
).select_related(
|
|
'park',
|
|
'ride_model',
|
|
'ride_model__manufacturer'
|
|
)
|
|
|
|
if 'park_slug' in self.kwargs:
|
|
self.park = get_object_or_404(Park, slug=self.kwargs['park_slug'])
|
|
queryset = queryset.filter(park=self.park)
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add park and category information to context"""
|
|
context = super().get_context_data(**kwargs)
|
|
if hasattr(self, 'park'):
|
|
context['park'] = self.park
|
|
context['park_slug'] = self.kwargs['park_slug']
|
|
context['category'] = dict(CATEGORY_CHOICES).get(
|
|
self.kwargs['category'])
|
|
return context
|
|
|
|
|
|
# Alias for parks app to maintain backward compatibility
|
|
ParkSingleCategoryListView = SingleCategoryListView
|
|
|
|
|
|
def search_manufacturers(request: HttpRequest) -> HttpResponse:
|
|
"""Search manufacturers and return results for HTMX"""
|
|
query = request.GET.get("q", "").strip()
|
|
|
|
# Show all manufacturers on click, filter on input
|
|
manufacturers = Manufacturer.objects.all().order_by("name")
|
|
if query:
|
|
manufacturers = manufacturers.filter(name__icontains=query)
|
|
manufacturers = manufacturers[:10]
|
|
|
|
return render(
|
|
request,
|
|
"rides/partials/manufacturer_search_results.html",
|
|
{"manufacturers": manufacturers, "search_term": query},
|
|
)
|
|
|
|
|
|
def search_designers(request: HttpRequest) -> HttpResponse:
|
|
"""Search designers and return results for HTMX"""
|
|
query = request.GET.get("q", "").strip()
|
|
|
|
# Show all designers on click, filter on input
|
|
designers = Designer.objects.all().order_by("name")
|
|
if query:
|
|
designers = designers.filter(name__icontains=query)
|
|
designers = designers[:10]
|
|
|
|
return render(
|
|
request,
|
|
"rides/partials/designer_search_results.html",
|
|
{"designers": designers, "search_term": query},
|
|
)
|
|
|
|
|
|
def search_ride_models(request: HttpRequest) -> HttpResponse:
|
|
"""Search ride models and return results for HTMX"""
|
|
query = request.GET.get("q", "").strip()
|
|
manufacturer_id = request.GET.get("manufacturer")
|
|
|
|
# Show all ride models on click, filter on input
|
|
ride_models = RideModel.objects.select_related(
|
|
"manufacturer").order_by("name")
|
|
if query:
|
|
ride_models = ride_models.filter(name__icontains=query)
|
|
if manufacturer_id:
|
|
ride_models = ride_models.filter(manufacturer_id=manufacturer_id)
|
|
ride_models = ride_models[:10]
|
|
|
|
return render(
|
|
request,
|
|
"rides/partials/ride_model_search_results.html",
|
|
{"ride_models": ride_models, "search_term": query,
|
|
"manufacturer_id": manufacturer_id},
|
|
)
|
|
|
|
|
|
def get_search_suggestions(request: HttpRequest) -> HttpResponse:
|
|
"""Get smart search suggestions for rides
|
|
|
|
Returns suggestions including:
|
|
- Common matching ride names
|
|
- Matching parks
|
|
- Matching categories
|
|
"""
|
|
query = request.GET.get('q', '').strip().lower()
|
|
suggestions = []
|
|
|
|
if query:
|
|
# Get common ride names
|
|
matching_names = Ride.objects.filter(
|
|
name__icontains=query
|
|
).values('name').annotate(
|
|
count=Count('id')
|
|
).order_by('-count')[:3]
|
|
|
|
for match in matching_names:
|
|
suggestions.append({
|
|
'type': 'ride',
|
|
'text': match['name'],
|
|
'count': match['count']
|
|
})
|
|
|
|
# Get matching parks
|
|
matching_parks = Park.objects.filter(
|
|
Q(name__icontains=query) |
|
|
Q(location__city__icontains=query)
|
|
)[:3]
|
|
|
|
for park in matching_parks:
|
|
suggestions.append({
|
|
'type': 'park',
|
|
'text': park.name,
|
|
'location': park.location.city if park.location else None
|
|
})
|
|
|
|
# Add category matches
|
|
for code, name in CATEGORY_CHOICES:
|
|
if query in name.lower():
|
|
ride_count = Ride.objects.filter(category=code).count()
|
|
suggestions.append({
|
|
'type': 'category',
|
|
'code': code,
|
|
'text': name,
|
|
'count': ride_count
|
|
})
|
|
|
|
return render(
|
|
request,
|
|
'rides/partials/search_suggestions.html',
|
|
{
|
|
'suggestions': suggestions,
|
|
'query': query
|
|
}
|
|
)
|
|
|
|
|
|
class RideSearchView(ListView):
|
|
"""View for ride search functionality with HTMX support."""
|
|
model = Ride
|
|
template_name = 'search/partials/ride_search_results.html'
|
|
context_object_name = 'rides'
|
|
paginate_by = 20
|
|
|
|
def get_queryset(self):
|
|
"""Get filtered rides based on search form."""
|
|
from search.forms import RideSearchForm
|
|
|
|
queryset = Ride.objects.select_related('park').order_by('name')
|
|
|
|
# Process search form
|
|
form = RideSearchForm(self.request.GET)
|
|
if form.is_valid():
|
|
ride = form.cleaned_data.get('ride')
|
|
if ride:
|
|
# If specific ride selected, return just that ride
|
|
queryset = queryset.filter(id=ride.id)
|
|
else:
|
|
# If no specific ride, filter by search term
|
|
search_term = self.request.GET.get('ride', '').strip()
|
|
if search_term:
|
|
queryset = queryset.filter(name__icontains=search_term)
|
|
|
|
return queryset
|
|
|
|
def get_template_names(self):
|
|
"""Return appropriate template based on request type."""
|
|
if self.request.htmx:
|
|
return ['search/partials/ride_search_results.html']
|
|
return ['search/ride_search.html']
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add search form to context."""
|
|
from search.forms import RideSearchForm
|
|
|
|
context = super().get_context_data(**kwargs)
|
|
context['search_form'] = RideSearchForm(self.request.GET)
|
|
return context
|