from typing import Any, Dict, Optional, Tuple, Union, cast from django.views.generic import DetailView, ListView, CreateView, UpdateView from django.shortcuts import get_object_or_404 from django.core.serializers.json import DjangoJSONEncoder from django.urls import reverse from django.db.models import Q from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType from django.contrib import messages from django.http import ( JsonResponse, HttpResponseRedirect, Http404, HttpRequest, HttpResponse, ) from django.db.models import Count from django.core.files.uploadedfile import UploadedFile from django.forms import ModelForm from .models import Ride, RollerCoasterStats 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 media.models import Photo from accounts.models import User def is_privileged_user(user: Any) -> bool: """Check if the user has privileged access. Args: user: The user to check Returns: bool: True if user has privileged or higher privileges """ return isinstance(user, User) and user.role in ["MODERATOR", "ADMIN", "SUPERUSER"] def handle_photo_uploads(request: HttpRequest, ride: Ride) -> int: """Handle photo uploads for a ride. Args: request: The HTTP request containing files ride: The ride to attach photos to Returns: int: Number of successfully uploaded photos """ uploaded_count = 0 photos = request.FILES.getlist("photos") for photo_file in photos: try: Photo.objects.create( image=photo_file, uploaded_by=request.user, content_type=ContentType.objects.get_for_model(Ride), object_id=ride.pk, ) uploaded_count += 1 except Exception as e: messages.error(request, f"Error uploading photo {photo_file.name}: {str(e)}") return uploaded_count def prepare_form_data(cleaned_data: Dict[str, Any], park: Park) -> Dict[str, Any]: """Prepare form data for submission. Args: cleaned_data: The form's cleaned data park: The park instance Returns: Dict[str, Any]: Processed form data ready for submission """ data = cleaned_data.copy() data["park"] = park.pk if data.get("park_area"): data["park_area"] = data["park_area"].pk if data.get("manufacturer"): data["manufacturer"] = data["manufacturer"].pk return data def handle_form_errors(request: HttpRequest, form: ModelForm) -> None: """Handle form validation errors by adding appropriate error messages. Args: request: The HTTP request form: The form containing validation errors """ messages.error( request, "Please correct the errors below. Required fields are marked with an asterisk (*).", ) for field, errors in form.errors.items(): for error in errors: messages.error(request, f"{field}: {error}") def create_edit_submission( request: HttpRequest, submission_type: str, changes: Dict[str, Any], object_id: Optional[int] = None, ) -> EditSubmission: """Create an EditSubmission object for ride changes. Args: request: The HTTP request submission_type: Type of submission (CREATE or EDIT) changes: The changes to be submitted object_id: Optional ID of the existing object for edits Returns: EditSubmission: The created submission object """ submission_data = { "user": request.user, "content_type": ContentType.objects.get_for_model(Ride), "submission_type": submission_type, "changes": changes, "reason": request.POST.get("reason", ""), "source": request.POST.get("source", ""), } if object_id is not None: submission_data["object_id"] = object_id return EditSubmission.objects.create(**submission_data) def handle_privileged_save( request: HttpRequest, form: RideForm, submission: EditSubmission ) -> Tuple[bool, str]: """Handle saving form and updating submission for privileged users. Args: request: The HTTP request form: The form to save submission: The edit submission to update Returns: Tuple[bool, str]: Success status and error message (empty string if successful) """ try: ride = form.save() if submission.submission_type == "CREATE": submission.object_id = ride.pk submission.status = "APPROVED" submission.handled_by = request.user submission.save() return True, "" except Exception as e: error_msg = ( f"Error {submission.submission_type.lower()}ing ride: {str(e)}. " "Please check your input and try again." ) return False, error_msg class SingleCategoryListView(ListView): model = Ride template_name = "rides/ride_category_list.html" context_object_name = "categories" def get_category_code(self) -> str: if category := self.kwargs.get("category"): return category raise Http404("Category not found") def get_queryset(self): category_code = self.get_category_code() category_name = dict(Ride.CATEGORY_CHOICES)[category_code] rides = ( Ride.objects.filter(category=category_code) .select_related("park", "manufacturer") .order_by("name") ) return {category_name: rides} if rides.exists() else {} def get_context_data(self, **kwargs) -> Dict[str, Any]: context = super().get_context_data(**kwargs) category_code = self.get_category_code() category_name = dict(Ride.CATEGORY_CHOICES)[category_code] context["title"] = f"All {category_name}s" context["category_code"] = category_code return context class ParkSingleCategoryListView(ListView): model = Ride template_name = "rides/ride_category_list.html" context_object_name = "categories" def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None: super().setup(request, *args, **kwargs) self.park = get_object_or_404(Park, slug=self.kwargs["park_slug"]) def get_category_code(self) -> str: if category := self.kwargs.get("category"): return category raise Http404("Category not found") def get_queryset(self): category_code = self.get_category_code() category_name = dict(Ride.CATEGORY_CHOICES)[category_code] rides = ( Ride.objects.filter(park=self.park, category=category_code) .select_related("manufacturer") .order_by("name") ) return {category_name: rides} if rides.exists() else {} def get_context_data(self, **kwargs) -> Dict[str, Any]: context = super().get_context_data(**kwargs) context["park"] = self.park category_code = self.get_category_code() category_name = dict(Ride.CATEGORY_CHOICES)[category_code] context["title"] = f"{category_name}s at {self.park.name}" context["category_code"] = category_code return context class RideCreateView(LoginRequiredMixin, CreateView): model = Ride form_class = RideForm template_name = "rides/ride_form.html" def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None: super().setup(request, *args, **kwargs) self.park = get_object_or_404(Park, slug=self.kwargs["park_slug"]) def get_form_kwargs(self) -> Dict[str, Any]: kwargs = super().get_form_kwargs() kwargs["park"] = self.park return kwargs def handle_submission( self, form: RideForm, cleaned_data: Dict[str, Any] ) -> HttpResponseRedirect: """Handle the form submission. Args: form: The form to process cleaned_data: The cleaned form data Returns: HttpResponseRedirect to appropriate URL """ submission = create_edit_submission(self.request, "CREATE", cleaned_data) if is_privileged_user(self.request.user): success, error_msg = handle_privileged_save(self.request, form, submission) if success: self.object = form.instance uploaded_count = handle_photo_uploads(self.request, self.object) messages.success( self.request, f"Successfully created {self.object.name} at {self.park.name}. " f"Added {uploaded_count} photo(s).", ) return HttpResponseRedirect(self.get_success_url()) else: if error_msg: # Only add error message if there is one messages.error(self.request, error_msg) return cast(HttpResponseRedirect, self.form_invalid(form)) messages.success( self.request, "Your ride submission has been sent for review. " "You will be notified when it is approved.", ) return HttpResponseRedirect( reverse("parks:rides:ride_list", kwargs={"park_slug": self.park.slug}) ) def form_valid(self, form: RideForm) -> HttpResponseRedirect: form.instance.park = self.park cleaned_data = prepare_form_data(form.cleaned_data, self.park) return self.handle_submission(form, cleaned_data) def form_invalid(self, form: RideForm) -> Union[HttpResponse, HttpResponseRedirect]: """Handle invalid form submission. Args: form: The invalid form Returns: Response with error messages """ handle_form_errors(self.request, form) return super().form_invalid(form) def get_success_url(self) -> str: return reverse( "parks:rides:ride_detail", kwargs={"park_slug": self.park.slug, "ride_slug": self.object.slug}, ) def get_context_data(self, **kwargs) -> Dict[str, Any]: context = super().get_context_data(**kwargs) context["park"] = self.park return context class RideUpdateView(LoginRequiredMixin, UpdateView): model = Ride form_class = RideForm template_name = "rides/ride_form.html" slug_url_kwarg = "ride_slug" def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None: super().setup(request, *args, **kwargs) self.park = get_object_or_404(Park, slug=self.kwargs["park_slug"]) def get_form_kwargs(self) -> Dict[str, Any]: kwargs = super().get_form_kwargs() kwargs["park"] = self.park return kwargs def get_context_data(self, **kwargs) -> Dict[str, Any]: context = super().get_context_data(**kwargs) context["park"] = self.park context["is_edit"] = True return context def handle_submission( self, form: RideForm, cleaned_data: Dict[str, Any] ) -> HttpResponseRedirect: """Handle the form submission. Args: form: The form to process cleaned_data: The cleaned form data Returns: HttpResponseRedirect to appropriate URL """ submission = create_edit_submission( self.request, "EDIT", cleaned_data, self.object.pk ) if is_privileged_user(self.request.user): success, error_msg = handle_privileged_save(self.request, form, submission) if success: self.object = form.instance uploaded_count = handle_photo_uploads(self.request, self.object) messages.success( self.request, f"Successfully updated {self.object.name}. " f"Added {uploaded_count} new photo(s).", ) return HttpResponseRedirect(self.get_success_url()) else: if error_msg: # Only add error message if there is one messages.error(self.request, error_msg) return cast(HttpResponseRedirect, self.form_invalid(form)) messages.success( self.request, f"Your changes to {self.object.name} have been sent for review. " "You will be notified when they are approved.", ) return HttpResponseRedirect( reverse( "parks:rides:ride_detail", kwargs={"park_slug": self.park.slug, "ride_slug": self.object.slug}, ) ) def form_valid(self, form: RideForm) -> HttpResponseRedirect: cleaned_data = prepare_form_data(form.cleaned_data, self.park) return self.handle_submission(form, cleaned_data) def form_invalid(self, form: RideForm) -> Union[HttpResponse, HttpResponseRedirect]: """Handle invalid form submission. Args: form: The invalid form Returns: Response with error messages """ handle_form_errors(self.request, form) return super().form_invalid(form) def get_success_url(self) -> str: return reverse( "parks:rides:ride_detail", kwargs={"park_slug": self.park.slug, "ride_slug": self.object.slug}, ) class RideDetailView( SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin, DetailView, ): model = Ride template_name = "rides/ride_detail.html" context_object_name = "ride" slug_url_kwarg = "ride_slug" def get_object(self, queryset=None): if queryset is None: queryset = self.get_queryset() park_slug = self.kwargs.get("park_slug") ride_slug = self.kwargs.get("ride_slug") obj, is_old_slug = self.model.get_by_slug(ride_slug) # type: ignore[attr-defined] if obj.park.slug != park_slug: raise self.model.DoesNotExist("Park slug doesn't match") return obj def get_context_data(self, **kwargs) -> Dict[str, Any]: context = super().get_context_data(**kwargs) if self.object.category == "RC": context["coaster_stats"] = RollerCoasterStats.objects.filter( ride=self.object ).first() return context def get_redirect_url_pattern(self) -> str: return "parks:rides:ride_detail" def get_redirect_url_kwargs(self) -> Dict[str, Any]: return {"park_slug": self.object.park.slug, "ride_slug": self.object.slug} class RideListView(ListView): model = Ride template_name = "rides/ride_list.html" context_object_name = "rides" def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None: super().setup(request, *args, **kwargs) self.park = None if "park_slug" in self.kwargs: self.park = get_object_or_404(Park, slug=self.kwargs["park_slug"]) def get_queryset(self): queryset = Ride.objects.select_related( "park", "coaster_stats", "manufacturer" ).prefetch_related("photos") if self.park: queryset = queryset.filter(park=self.park) search = self.request.GET.get("search", "").strip() or None category = self.request.GET.get("category", "").strip() or None status = self.request.GET.get("status", "").strip() or None manufacturer = self.request.GET.get("manufacturer", "").strip() or None if search: if self.park: queryset = queryset.filter(name__icontains=search) else: queryset = queryset.filter( Q(name__icontains=search) | Q(park__name__icontains=search) ) if category: queryset = queryset.filter(category=category) if status: queryset = queryset.filter(status=status) if manufacturer: queryset = queryset.exclude(manufacturer__isnull=True) return queryset def get_context_data(self, **kwargs) -> Dict[str, Any]: context = super().get_context_data(**kwargs) context["park"] = self.park manufacturer_query = Ride.objects if self.park: manufacturer_query = manufacturer_query.filter(park=self.park) context["manufacturers"] = list( manufacturer_query.exclude(manufacturer__isnull=True) .values_list("manufacturer__name", flat=True) .distinct() .order_by("manufacturer__name") ) context["current_filters"] = { "search": self.request.GET.get("search", ""), "category": self.request.GET.get("category", ""), "status": self.request.GET.get("status", ""), "manufacturer": self.request.GET.get("manufacturer", ""), } return context def get(self, request: HttpRequest, *args: Any, **kwargs: Any): if getattr(request, "htmx", False): # type: ignore[attr-defined] self.template_name = "rides/partials/ride_list.html" return super().get(request, *args, **kwargs)