Files
thrillwiki_django_no_react/rides/views.py
pacnpal 01e0a609d2 feat: Implement ride management views and utility functions
- Added functions for checking user privileges, handling photo uploads, preparing form data, and managing form errors.
- Created views for listing, creating, updating, and displaying rides, including category-specific views.
- Integrated submission handling for ride changes and improved user feedback through messages.
- Enhanced data handling with appropriate context and queryset management for better performance and usability.
2024-11-04 05:25:53 +00:00

506 lines
17 KiB
Python

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)