diff --git a/rides/views.py b/rides/views.py index a559b1fe..4362cb7e 100644 --- a/rides/views.py +++ b/rides/views.py @@ -1,17 +1,144 @@ -# Keep all imports and previous classes up to RideCreateView -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.contenttypes.models import ContentType -from django.shortcuts import get_object_or_404 +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.views.generic import UpdateView +from django.db.models import Q, Model +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.decorators import login_required +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 -from moderation.mixins import EditSubmissionMixin -from apps.parks.mixins.base import ParkContextRequired # type: ignore -from moderation.models import EditSubmission -from parks.models import Park -from rides.forms import RideForm # type: ignore -from .models import Ride, RideModel + + +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""" @@ -83,3 +210,129 @@ class RideUpdateView(LoginRequiredMixin, ParkContextRequired, EditSubmissionMixi ) 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 all rides or filter by 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: + 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 to context if park_slug is provided""" + context = super().get_context_data(**kwargs) + if hasattr(self, 'park'): + context['park'] = self.park + context['park_slug'] = self.kwargs['park_slug'] + 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 + + +@login_required +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}, + ) + + +@login_required +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}, + ) + + +@login_required +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}, + )