mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 20:11:08 -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:
@@ -75,13 +75,14 @@ class RideQuerySet(StatusQuerySet, ReviewableQuerySet):
|
||||
|
||||
return self.select_related(
|
||||
"park",
|
||||
"park__location",
|
||||
"park_area",
|
||||
"manufacturer",
|
||||
"designer",
|
||||
"ride_model",
|
||||
"ride_model__manufacturer",
|
||||
).prefetch_related(
|
||||
"location",
|
||||
"rollercoaster_stats",
|
||||
"coaster_stats",
|
||||
Prefetch(
|
||||
"reviews",
|
||||
queryset=RideReview.objects.select_related("user")
|
||||
@@ -91,6 +92,12 @@ class RideQuerySet(StatusQuerySet, ReviewableQuerySet):
|
||||
"photos",
|
||||
)
|
||||
|
||||
def with_coaster_stats(self):
|
||||
"""Always prefetch coaster_stats for roller coaster queries."""
|
||||
return self.select_related(
|
||||
"park", "manufacturer", "ride_model"
|
||||
).prefetch_related("coaster_stats")
|
||||
|
||||
def for_map_display(self):
|
||||
"""Optimize for map display."""
|
||||
return (
|
||||
@@ -176,6 +183,10 @@ class RideManager(StatusManager, ReviewableManager):
|
||||
def optimized_for_detail(self):
|
||||
return self.get_queryset().optimized_for_detail()
|
||||
|
||||
def with_coaster_stats(self):
|
||||
"""Always prefetch coaster_stats for roller coaster queries."""
|
||||
return self.get_queryset().with_coaster_stats()
|
||||
|
||||
|
||||
class RideModelQuerySet(BaseQuerySet):
|
||||
"""QuerySet for RideModel model."""
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Convert RideModel unique_together to UniqueConstraint.
|
||||
|
||||
This migration converts the legacy unique_together constraints to the modern
|
||||
UniqueConstraint syntax which provides better error messages and more flexibility.
|
||||
"""
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('rides', '0025_convert_ride_status_to_fsm'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Remove the old unique_together constraint
|
||||
migrations.AlterUniqueTogether(
|
||||
name='ridemodel',
|
||||
unique_together=set(),
|
||||
),
|
||||
# Add new UniqueConstraints with better error messages
|
||||
migrations.AddConstraint(
|
||||
model_name='ridemodel',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=['manufacturer', 'name'],
|
||||
name='ridemodel_manufacturer_name_unique',
|
||||
violation_error_message='A ride model with this name already exists for this manufacturer'
|
||||
),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='ridemodel',
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=['manufacturer', 'slug'],
|
||||
name='ridemodel_manufacturer_slug_unique',
|
||||
violation_error_message='A ride model with this slug already exists for this manufacturer'
|
||||
),
|
||||
),
|
||||
]
|
||||
49
backend/apps/rides/mixins.py
Normal file
49
backend/apps/rides/mixins.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Mixins for ride views.
|
||||
|
||||
This module contains mixins that provide reusable functionality
|
||||
for ride-related views, reducing code duplication.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.contrib import messages
|
||||
|
||||
from apps.rides.services import RideService
|
||||
|
||||
|
||||
class RideFormMixin:
|
||||
"""
|
||||
Mixin for handling ride form submissions with entity suggestions.
|
||||
|
||||
Provides common functionality for RideCreateView and RideUpdateView
|
||||
to handle new manufacturer, designer, and ride model suggestions.
|
||||
"""
|
||||
|
||||
def handle_entity_suggestions(self, form) -> Dict[str, Any]:
|
||||
"""
|
||||
Process new entity suggestions from form.
|
||||
|
||||
Creates moderation submissions for any new manufacturers,
|
||||
designers, or ride models that were suggested but don't
|
||||
exist in the system.
|
||||
|
||||
Args:
|
||||
form: Validated form instance with cleaned_data
|
||||
|
||||
Returns:
|
||||
Dictionary with submission results from RideService
|
||||
"""
|
||||
result = RideService.handle_new_entity_suggestions(
|
||||
form_data=form.cleaned_data,
|
||||
submitter=self.request.user
|
||||
)
|
||||
|
||||
if result['total_submissions'] > 0:
|
||||
messages.info(
|
||||
self.request,
|
||||
f"Created {result['total_submissions']} moderation submission(s) "
|
||||
"for new entities"
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.db import models
|
||||
from django.utils.text import slugify
|
||||
from django.core.exceptions import ValidationError
|
||||
from config.django import base as settings
|
||||
from apps.core.models import TrackedModel
|
||||
from apps.core.choices import RichChoiceField
|
||||
@@ -165,8 +166,18 @@ class RideModel(TrackedModel):
|
||||
|
||||
class Meta(TrackedModel.Meta):
|
||||
ordering = ["manufacturer__name", "name"]
|
||||
unique_together = [["manufacturer", "name"], ["manufacturer", "slug"]]
|
||||
constraints = [
|
||||
# Unique constraints (replacing unique_together for better error messages)
|
||||
models.UniqueConstraint(
|
||||
fields=['manufacturer', 'name'],
|
||||
name='ridemodel_manufacturer_name_unique',
|
||||
violation_error_message='A ride model with this name already exists for this manufacturer'
|
||||
),
|
||||
models.UniqueConstraint(
|
||||
fields=['manufacturer', 'slug'],
|
||||
name='ridemodel_manufacturer_slug_unique',
|
||||
violation_error_message='A ride model with this slug already exists for this manufacturer'
|
||||
),
|
||||
# Height range validation
|
||||
models.CheckConstraint(
|
||||
name="ride_model_height_range_logical",
|
||||
@@ -222,6 +233,14 @@ class RideModel(TrackedModel):
|
||||
else f"{self.manufacturer.name} {self.name}"
|
||||
)
|
||||
|
||||
def clean(self) -> None:
|
||||
"""Validate RideModel business rules."""
|
||||
super().clean()
|
||||
if self.is_discontinued and not self.last_installation_year:
|
||||
raise ValidationError({
|
||||
'last_installation_year': 'Discontinued models must have a last installation year'
|
||||
})
|
||||
|
||||
def save(self, *args, **kwargs) -> None:
|
||||
if not self.slug:
|
||||
from django.utils.text import slugify
|
||||
|
||||
@@ -307,3 +307,85 @@ class RideService:
|
||||
ride = Ride.objects.select_for_update().get(id=ride_id)
|
||||
ride.open(user=user)
|
||||
return ride
|
||||
|
||||
@staticmethod
|
||||
def handle_new_entity_suggestions(
|
||||
*,
|
||||
form_data: Dict[str, Any],
|
||||
submitter: UserType,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Handle suggestions for new manufacturers, designers, and ride models.
|
||||
|
||||
Creates moderation submissions for entities that don't exist in the system.
|
||||
This extracts the business logic from RideCreateView and RideUpdateView.
|
||||
|
||||
Args:
|
||||
form_data: Cleaned form data containing search fields and selections
|
||||
submitter: User making the suggestions
|
||||
|
||||
Returns:
|
||||
Dictionary with lists of created submission IDs by type:
|
||||
{
|
||||
'manufacturers': [...],
|
||||
'designers': [...],
|
||||
'ride_models': [...],
|
||||
'total_submissions': int
|
||||
}
|
||||
"""
|
||||
from apps.moderation.services import ModerationService
|
||||
|
||||
result = {
|
||||
'manufacturers': [],
|
||||
'designers': [],
|
||||
'ride_models': [],
|
||||
'total_submissions': 0
|
||||
}
|
||||
|
||||
# Check for new manufacturer
|
||||
manufacturer_name = form_data.get("manufacturer_search")
|
||||
if manufacturer_name and not form_data.get("manufacturer"):
|
||||
submission = ModerationService.create_edit_submission_with_queue(
|
||||
content_object=None,
|
||||
changes={"name": manufacturer_name, "roles": ["MANUFACTURER"]},
|
||||
submitter=submitter,
|
||||
submission_type="CREATE",
|
||||
reason=f"New manufacturer suggested: {manufacturer_name}",
|
||||
)
|
||||
if submission:
|
||||
result['manufacturers'].append(submission.id)
|
||||
result['total_submissions'] += 1
|
||||
|
||||
# Check for new designer
|
||||
designer_name = form_data.get("designer_search")
|
||||
if designer_name and not form_data.get("designer"):
|
||||
submission = ModerationService.create_edit_submission_with_queue(
|
||||
content_object=None,
|
||||
changes={"name": designer_name, "roles": ["DESIGNER"]},
|
||||
submitter=submitter,
|
||||
submission_type="CREATE",
|
||||
reason=f"New designer suggested: {designer_name}",
|
||||
)
|
||||
if submission:
|
||||
result['designers'].append(submission.id)
|
||||
result['total_submissions'] += 1
|
||||
|
||||
# Check for new ride model
|
||||
ride_model_name = form_data.get("ride_model_search")
|
||||
manufacturer = form_data.get("manufacturer")
|
||||
if ride_model_name and not form_data.get("ride_model") and manufacturer:
|
||||
submission = ModerationService.create_edit_submission_with_queue(
|
||||
content_object=None,
|
||||
changes={
|
||||
"name": ride_model_name,
|
||||
"manufacturer": manufacturer.id,
|
||||
},
|
||||
submitter=submitter,
|
||||
submission_type="CREATE",
|
||||
reason=f"New ride model suggested: {ride_model_name}",
|
||||
)
|
||||
if submission:
|
||||
result['ride_models'].append(submission.id)
|
||||
result['total_submissions'] += 1
|
||||
|
||||
return result
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
|
||||
from django.db.models.signals import pre_save
|
||||
from django.db.models.signals import pre_save, post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
|
||||
@@ -10,6 +10,28 @@ from .models import Ride
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Computed Field Maintenance
|
||||
# =============================================================================
|
||||
|
||||
def update_ride_search_text(ride):
|
||||
"""
|
||||
Update ride's search_text computed field.
|
||||
|
||||
This is called when related objects (park, manufacturer, ride_model)
|
||||
change and might affect the ride's search text.
|
||||
"""
|
||||
if ride is None:
|
||||
return
|
||||
|
||||
try:
|
||||
ride._populate_computed_fields()
|
||||
ride.save(update_fields=['search_text'])
|
||||
logger.debug(f"Updated search_text for ride {ride.pk}")
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to update search_text for ride {ride.pk}: {e}")
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Ride)
|
||||
def handle_ride_status(sender, instance, **kwargs):
|
||||
"""
|
||||
@@ -186,3 +208,58 @@ def apply_post_closing_status(instance, user=None):
|
||||
f"Applied post_closing_status {target_status} to ride {instance.pk} (direct)"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Computed Field Maintenance Signal Handlers
|
||||
# =============================================================================
|
||||
|
||||
@receiver(post_save, sender='parks.Park')
|
||||
def update_ride_search_text_on_park_change(sender, instance, **kwargs):
|
||||
"""
|
||||
Update ride search_text when park name or location changes.
|
||||
|
||||
When a park's name changes, all rides at that park need their
|
||||
search_text regenerated.
|
||||
"""
|
||||
try:
|
||||
for ride in instance.rides.all():
|
||||
update_ride_search_text(ride)
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to update ride search_text on park change: {e}")
|
||||
|
||||
|
||||
@receiver(post_save, sender='parks.Company')
|
||||
def update_ride_search_text_on_company_change(sender, instance, **kwargs):
|
||||
"""
|
||||
Update ride search_text when manufacturer/designer name changes.
|
||||
|
||||
When a company's name changes, all rides manufactured or designed
|
||||
by that company need their search_text regenerated.
|
||||
"""
|
||||
try:
|
||||
# Update all rides manufactured by this company
|
||||
for ride in instance.manufactured_rides.all():
|
||||
update_ride_search_text(ride)
|
||||
|
||||
# Update all rides designed by this company
|
||||
for ride in instance.designed_rides.all():
|
||||
update_ride_search_text(ride)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to update ride search_text on company change: {e}")
|
||||
|
||||
|
||||
@receiver(post_save, sender='rides.RideModel')
|
||||
def update_ride_search_text_on_ride_model_change(sender, instance, **kwargs):
|
||||
"""
|
||||
Update ride search_text when ride model name changes.
|
||||
|
||||
When a ride model's name changes, all rides using that model need
|
||||
their search_text regenerated.
|
||||
"""
|
||||
try:
|
||||
for ride in instance.rides.all():
|
||||
update_ride_search_text(ride)
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to update ride search_text on ride model change: {e}")
|
||||
|
||||
@@ -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