mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 22:51:09 -05:00
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:
@@ -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")
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user