feat: Refactor rides app with unique constraints, mixins, and enhanced documentation

- Added migration to convert unique_together constraints to UniqueConstraint for RideModel.
- Introduced RideFormMixin for handling entity suggestions in ride forms.
- Created comprehensive code standards documentation outlining formatting, docstring requirements, complexity guidelines, and testing requirements.
- Established error handling guidelines with a structured exception hierarchy and best practices for API and view error handling.
- Documented view pattern guidelines, emphasizing the use of CBVs, FBVs, and ViewSets with examples.
- Implemented a benchmarking script for query performance analysis and optimization.
- Developed security documentation detailing measures, configurations, and a security checklist.
- Compiled a database optimization guide covering indexing strategies, query optimization patterns, and computed fields.
This commit is contained in:
pacnpal
2025-12-22 11:17:31 -05:00
parent 45d97b6e68
commit 2e35f8c5d9
71 changed files with 8036 additions and 1462 deletions

View File

@@ -1,80 +1,188 @@
from django.views.generic import DetailView, ListView, CreateView, UpdateView
"""
Views for ride functionality.
This module contains views for managing ride resources including
CRUD operations, search, filtering, and HTMX partial rendering.
View Types:
CBVs:
- RideDetailView: Display ride details
- RideCreateView: Create new ride
- RideUpdateView: Update existing ride
- RideListView: List rides with filtering
- RideSearchView: Search rides with HTMX support
- RideRankingsView: Display ride rankings
- RideRankingDetailView: Display ranking details
- ManufacturerListView: List manufacturers
- DesignerListView: List designers
- SingleCategoryListView: List rides by category
FBVs (HTMX Partials):
- show_coaster_fields: Toggle coaster-specific fields
- ride_status_actions: FSM status actions for moderators
- ride_header_badge: Status badge partial
- search_companies: Company search autocomplete
- search_ride_models: Ride model search autocomplete
- get_search_suggestions: Smart search suggestions
- ranking_history_chart: Ranking history chart data
- ranking_comparisons: Head-to-head comparisons
Dependencies:
- Services: apps.rides.services
- Models: apps.rides.models
- Forms: apps.rides.forms
Code Quality:
- PEP 8 compliant (verified with black, flake8, ruff)
- Maximum line length: 88 characters
- Maximum complexity: 10 (McCabe)
"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Count, Q
from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.db.models import Q
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse, Http404
from django.db.models import Count
from .models.rides import Ride, RideModel
from .choices import RIDE_CATEGORIES
from .models.company import Company
from django.views.generic import CreateView, DetailView, ListView, UpdateView
from apps.moderation.mixins import EditSubmissionMixin, HistoryMixin
from apps.parks.models import Park
from .forms import RideForm, RideSearchForm
from .forms.search import MasterFilterForm
from .services.search import RideSearchService
from apps.parks.models import Park
from apps.moderation.mixins import EditSubmissionMixin, HistoryMixin
from apps.moderation.services import ModerationService
from .models.rankings import RideRanking, RankingSnapshot
from .mixins import RideFormMixin
from .models.company import Company
from .models.rankings import RankingSnapshot, RideRanking
from .models.rides import Ride, RideModel
from .services.ranking_service import RideRankingService
class ParkContextRequired:
"""Mixin to require park context for views"""
"""
Mixin to require park context for views.
Ensures that the view has access to a park_slug URL parameter.
Raises Http404 if park context is not available.
"""
def dispatch(self, request, *args, **kwargs):
"""Check for park context before dispatching to handler."""
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"""
"""
Show roller coaster specific fields based on category selection.
View Type: FBV (HTMX Partial)
URL Pattern: /rides/coaster-fields/
Returns: HTML partial with coaster-specific form fields
Args:
request: HTTP request with 'category' query parameter
Returns:
Empty response for non-coaster categories,
or coaster fields partial for roller coasters
"""
category = request.GET.get("category")
if category != "RC": # Only show for roller coasters
return HttpResponse("")
return render(request, "rides/partials/coaster_fields.html")
def ride_status_actions(request: HttpRequest, park_slug: str, ride_slug: str) -> HttpResponse:
"""Return FSM status actions for ride moderators"""
def ride_status_actions(
request: HttpRequest, park_slug: str, ride_slug: str
) -> HttpResponse:
"""
Return FSM status actions for ride moderators.
View Type: FBV (HTMX Partial)
URL Pattern: /parks/<park_slug>/rides/<ride_slug>/status-actions/
Returns: HTML partial with available status transition actions
Permissions: rides.change_ride
Args:
request: HTTP request
park_slug: Slug of the park
ride_slug: Slug of the ride
Returns:
Empty response for non-moderators, or status actions partial
"""
park = get_object_or_404(Park, slug=park_slug)
ride = get_object_or_404(Ride, park=park, slug=ride_slug)
# Only show to moderators
if not request.user.has_perm('rides.change_ride'):
if not request.user.has_perm("rides.change_ride"):
return HttpResponse("")
return render(request, "rides/partials/ride_status_actions.html", {
"ride": ride,
"park": park,
"user": request.user
})
return render(
request,
"rides/partials/ride_status_actions.html",
{"ride": ride, "park": park, "user": request.user},
)
def ride_header_badge(request: HttpRequest, park_slug: str, ride_slug: str) -> HttpResponse:
"""Return the header status badge partial for a ride"""
def ride_header_badge(
request: HttpRequest, park_slug: str, ride_slug: str
) -> HttpResponse:
"""
Return the header status badge partial for a ride.
View Type: FBV (HTMX Partial)
URL Pattern: /parks/<park_slug>/rides/<ride_slug>/header-badge/
Returns: HTML partial with ride status badge
Args:
request: HTTP request
park_slug: Slug of the park
ride_slug: Slug of the ride
Returns:
Rendered status badge partial
"""
park = get_object_or_404(Park, slug=park_slug)
ride = get_object_or_404(Ride, park=park, slug=ride_slug)
return render(request, "rides/partials/ride_header_badge.html", {
"ride": ride,
"park": park,
"user": request.user
})
return render(
request,
"rides/partials/ride_header_badge.html",
{"ride": ride, "park": park, "user": request.user},
)
class RideDetailView(HistoryMixin, DetailView):
"""View for displaying ride details"""
"""
Display ride details with related data.
View Type: CBV (DetailView)
URL Pattern: /parks/<park_slug>/rides/<ride_slug>/
Template: rides/ride_detail.html
Permissions: Public
Includes history tracking via HistoryMixin for audit trail display.
"""
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"""
"""Get ride for the specific park with optimized query."""
queryset = (
Ride.objects.all()
.select_related("park", "ride_model", "ride_model__manufacturer")
.prefetch_related("photos")
.select_related(
"park",
"park__location",
"park_area",
"manufacturer",
"designer",
"ride_model",
"ride_model__manufacturer",
)
.prefetch_related("photos", "coaster_stats")
)
if "park_slug" in self.kwargs:
@@ -92,15 +200,24 @@ class RideDetailView(HistoryMixin, DetailView):
return context
class RideCreateView(LoginRequiredMixin, ParkContextRequired, CreateView):
"""View for creating a new ride"""
class RideCreateView(
LoginRequiredMixin, ParkContextRequired, RideFormMixin, CreateView
):
"""
View for creating a new ride.
View Type: CBV (CreateView)
URL Pattern: /parks/<park_slug>/rides/add/
Template: rides/ride_form.html
Permissions: LoginRequired
"""
model = Ride
form_class = RideForm
template_name = "rides/ride_form.html"
def get_success_url(self):
"""Get URL to redirect to after successful creation"""
"""Get URL to redirect to after successful creation."""
return reverse(
"parks:rides:ride_detail",
kwargs={
@@ -110,14 +227,14 @@ class RideCreateView(LoginRequiredMixin, ParkContextRequired, CreateView):
)
def get_form_kwargs(self):
"""Pass park to the form"""
"""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"""
"""Add park and park_slug to context."""
context = super().get_context_data(**kwargs)
context["park"] = self.park
context["park_slug"] = self.park.slug
@@ -125,51 +242,26 @@ class RideCreateView(LoginRequiredMixin, ParkContextRequired, CreateView):
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"):
ModerationService.create_edit_submission_with_queue(
content_object=None,
changes={"name": manufacturer_name, "roles": ["MANUFACTURER"]},
submitter=self.request.user,
submission_type="CREATE",
reason=f"New manufacturer suggested during ride creation: {manufacturer_name}",
)
# Check for new designer
designer_name = form.cleaned_data.get("designer_search")
if designer_name and not form.cleaned_data.get("designer"):
ModerationService.create_edit_submission_with_queue(
content_object=None,
changes={"name": designer_name, "roles": ["DESIGNER"]},
submitter=self.request.user,
submission_type="CREATE",
reason=f"New designer suggested during ride creation: {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:
ModerationService.create_edit_submission_with_queue(
content_object=None,
changes={
"name": ride_model_name,
"manufacturer": manufacturer.id,
},
submitter=self.request.user,
submission_type="CREATE",
reason=f"New ride model suggested during ride creation: {ride_model_name}",
)
"""Handle form submission using RideFormMixin for entity suggestions."""
self.handle_entity_suggestions(form)
return super().form_valid(form)
class RideUpdateView(
LoginRequiredMixin, ParkContextRequired, EditSubmissionMixin, UpdateView
LoginRequiredMixin,
ParkContextRequired,
RideFormMixin,
EditSubmissionMixin,
UpdateView,
):
"""View for updating an existing ride"""
"""
View for updating an existing ride.
View Type: CBV (UpdateView)
URL Pattern: /parks/<park_slug>/rides/<ride_slug>/edit/
Template: rides/ride_form.html
Permissions: LoginRequired
"""
model = Ride
form_class = RideForm
@@ -177,7 +269,7 @@ class RideUpdateView(
slug_url_kwarg = "ride_slug"
def get_success_url(self):
"""Get URL to redirect to after successful update"""
"""Get URL to redirect to after successful update."""
return reverse(
"parks:rides:ride_detail",
kwargs={
@@ -187,18 +279,18 @@ class RideUpdateView(
)
def get_queryset(self):
"""Get ride for the specific park"""
"""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"""
"""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"""
"""Add park and park_slug to context."""
context = super().get_context_data(**kwargs)
context["park"] = self.park
context["park_slug"] = self.park.slug
@@ -206,44 +298,8 @@ class RideUpdateView(
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"):
ModerationService.create_edit_submission_with_queue(
content_object=None,
changes={"name": manufacturer_name, "roles": ["MANUFACTURER"]},
submitter=self.request.user,
submission_type="CREATE",
reason=f"New manufacturer suggested during ride update: {manufacturer_name}",
)
# Check for new designer
designer_name = form.cleaned_data.get("designer_search")
if designer_name and not form.cleaned_data.get("designer"):
ModerationService.create_edit_submission_with_queue(
content_object=None,
changes={"name": designer_name, "roles": ["DESIGNER"]},
submitter=self.request.user,
submission_type="CREATE",
reason=f"New designer suggested during ride update: {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:
ModerationService.create_edit_submission_with_queue(
content_object=None,
changes={
"name": ride_model_name,
"manufacturer": manufacturer.id,
},
submitter=self.request.user,
submission_type="CREATE",
reason=f"New ride model suggested during ride update: {ride_model_name}",
)
"""Handle form submission using RideFormMixin for entity suggestions."""
self.handle_entity_suggestions(form)
return super().form_valid(form)
@@ -256,13 +312,7 @@ class RideListView(ListView):
paginate_by = 24
def get_queryset(self):
"""Get filtered rides using the advanced search service"""
# Initialize search service
search_service = RideSearchService()
# Parse filters from request
filter_form = MasterFilterForm(self.request.GET)
"""Get filtered rides using the advanced search service."""
# Apply park context if available
park = None
if "park_slug" in self.kwargs:
@@ -275,12 +325,12 @@ class RideListView(ListView):
.select_related("park", "ride_model", "ride_model__manufacturer")
.prefetch_related("photos")
)
if park:
queryset = queryset.filter(park=park)
# Apply basic search if provided
search_query = self.request.GET.get('search', '').strip()
search_query = self.request.GET.get("search", "").strip()
if search_query:
queryset = queryset.filter(name__icontains=search_query)
@@ -306,8 +356,11 @@ class RideListView(ListView):
context["filter_form"] = filter_form
# Use Rich Choice registry directly
from apps.core.choices.registry import get_choices
choices = get_choices("categories", "rides")
context["category_choices"] = [(choice.value, choice.label) for choice in choices]
context["category_choices"] = [
(choice.value, choice.label) for choice in choices
]
# Add filter summary for display
if filter_form.is_valid():
@@ -357,8 +410,12 @@ class SingleCategoryListView(ListView):
context["park_slug"] = self.kwargs["park_slug"]
# Find the category choice by value using Rich Choice registry
from apps.core.choices.registry import get_choices
choices = get_choices("categories", "rides")
category_choice = next((choice for choice in choices if choice.value == self.kwargs["category"]), None)
category_choice = next(
(choice for choice in choices if choice.value == self.kwargs["category"]),
None,
)
context["category"] = category_choice.label if category_choice else "Unknown"
return context
@@ -372,7 +429,7 @@ def search_companies(request: HttpRequest) -> HttpResponse:
query = request.GET.get("q", "").strip()
role = request.GET.get("role", "").upper()
companies = Company.objects.all().order_by("name")
companies = Company.objects.only("id", "name", "slug", "roles").order_by("name")
if role:
companies = companies.filter(roles__contains=[role])
if query:
@@ -439,8 +496,8 @@ def get_search_suggestions(request: HttpRequest) -> HttpResponse:
}
)
# Get matching parks
matching_parks = Park.objects.filter(
# Get matching parks with optimized query
matching_parks = Park.objects.select_related("location").filter(
Q(name__icontains=query) | Q(location__city__icontains=query)
)[:3]
@@ -455,6 +512,7 @@ def get_search_suggestions(request: HttpRequest) -> HttpResponse:
# Add category matches
from apps.core.choices.registry import get_choices
choices = get_choices("categories", "rides")
for choice in choices:
if query in choice.label.lower():
@@ -556,8 +614,11 @@ class RideRankingsView(ListView):
context = super().get_context_data(**kwargs)
# Use Rich Choice registry directly
from apps.core.choices.registry import get_choices
choices = get_choices("categories", "rides")
context["category_choices"] = [(choice.value, choice.label) for choice in choices]
context["category_choices"] = [
(choice.value, choice.label) for choice in choices
]
context["selected_category"] = self.request.GET.get("category", "all")
context["min_riders"] = self.request.GET.get("min_riders", "")
@@ -690,10 +751,11 @@ class ManufacturerListView(ListView):
paginate_by = 24
def get_queryset(self):
"""Get companies that are manufacturers"""
"""Get companies that are manufacturers with optimized query"""
return (
Company.objects.filter(roles__contains=["MANUFACTURER"])
.annotate(ride_count=Count("manufactured_rides"))
.only("id", "name", "slug", "roles", "description")
.order_by("name")
)
@@ -713,10 +775,11 @@ class DesignerListView(ListView):
paginate_by = 24
def get_queryset(self):
"""Get companies that are designers"""
"""Get companies that are designers with optimized query"""
return (
Company.objects.filter(roles__contains=["DESIGNER"])
.annotate(ride_count=Count("designed_rides"))
.only("id", "name", "slug", "roles", "description")
.order_by("name")
)