mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 17:51:08 -05:00
- 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.
791 lines
25 KiB
Python
791 lines
25 KiB
Python
"""
|
|
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.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 .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.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
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"):
|
|
return HttpResponse("")
|
|
|
|
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.
|
|
|
|
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},
|
|
)
|
|
|
|
|
|
class RideDetailView(HistoryMixin, DetailView):
|
|
"""
|
|
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 with optimized query."""
|
|
queryset = (
|
|
Ride.objects.all()
|
|
.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:
|
|
queryset = queryset.filter(park__slug=self.kwargs["park_slug"])
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add context data"""
|
|
context = super().get_context_data(**kwargs)
|
|
if "park_slug" in self.kwargs:
|
|
context["park_slug"] = self.kwargs["park_slug"]
|
|
context["park"] = self.object.park
|
|
|
|
return context
|
|
|
|
|
|
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."""
|
|
return reverse(
|
|
"parks:rides:ride_detail",
|
|
kwargs={
|
|
"park_slug": self.park.slug,
|
|
"ride_slug": self.object.slug,
|
|
},
|
|
)
|
|
|
|
def get_form_kwargs(self):
|
|
"""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."""
|
|
context = super().get_context_data(**kwargs)
|
|
context["park"] = self.park
|
|
context["park_slug"] = self.park.slug
|
|
context["is_edit"] = False
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
"""Handle form submission using RideFormMixin for entity suggestions."""
|
|
self.handle_entity_suggestions(form)
|
|
return super().form_valid(form)
|
|
|
|
|
|
class RideUpdateView(
|
|
LoginRequiredMixin,
|
|
ParkContextRequired,
|
|
RideFormMixin,
|
|
EditSubmissionMixin,
|
|
UpdateView,
|
|
):
|
|
"""
|
|
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
|
|
template_name = "rides/ride_form.html"
|
|
slug_url_kwarg = "ride_slug"
|
|
|
|
def get_success_url(self):
|
|
"""Get URL to redirect to after successful update."""
|
|
return reverse(
|
|
"parks:rides:ride_detail",
|
|
kwargs={
|
|
"park_slug": self.park.slug,
|
|
"ride_slug": self.object.slug,
|
|
},
|
|
)
|
|
|
|
def get_queryset(self):
|
|
"""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."""
|
|
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."""
|
|
context = super().get_context_data(**kwargs)
|
|
context["park"] = self.park
|
|
context["park_slug"] = self.park.slug
|
|
context["is_edit"] = True
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
"""Handle form submission using RideFormMixin for entity suggestions."""
|
|
self.handle_entity_suggestions(form)
|
|
return super().form_valid(form)
|
|
|
|
|
|
class RideListView(ListView):
|
|
"""Enhanced view for displaying a list of rides with advanced filtering"""
|
|
|
|
model = Ride
|
|
template_name = "rides/ride_list.html"
|
|
context_object_name = "rides"
|
|
paginate_by = 24
|
|
|
|
def get_queryset(self):
|
|
"""Get filtered rides using the advanced search service."""
|
|
# Apply park context if available
|
|
park = None
|
|
if "park_slug" in self.kwargs:
|
|
self.park = get_object_or_404(Park, slug=self.kwargs["park_slug"])
|
|
park = self.park
|
|
|
|
# For now, use a simpler approach until we can properly integrate the search service
|
|
queryset = (
|
|
Ride.objects.all()
|
|
.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()
|
|
if search_query:
|
|
queryset = queryset.filter(name__icontains=search_query)
|
|
|
|
return queryset
|
|
|
|
def get_template_names(self):
|
|
"""Return appropriate template based on request type"""
|
|
if hasattr(self.request, "htmx") and self.request.htmx:
|
|
return ["rides/partials/ride_list_results.html"]
|
|
return [self.template_name]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add filter form and context data"""
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
# Add park context
|
|
if hasattr(self, "park"):
|
|
context["park"] = self.park
|
|
context["park_slug"] = self.kwargs["park_slug"]
|
|
|
|
# Add filter form
|
|
filter_form = MasterFilterForm(self.request.GET)
|
|
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
|
|
]
|
|
|
|
# Add filter summary for display
|
|
if filter_form.is_valid():
|
|
context["active_filters"] = filter_form.get_filter_summary()
|
|
context["has_filters"] = filter_form.has_active_filters()
|
|
else:
|
|
context["active_filters"] = {}
|
|
context["has_filters"] = False
|
|
|
|
# Add total count before filtering (for display purposes)
|
|
if hasattr(self, "park"):
|
|
context["total_rides"] = Ride.objects.filter(park=self.park).count()
|
|
else:
|
|
context["total_rides"] = Ride.objects.count()
|
|
|
|
# Add filtered count
|
|
context["filtered_count"] = self.get_queryset().count()
|
|
|
|
return context
|
|
|
|
|
|
class SingleCategoryListView(ListView):
|
|
"""View for displaying rides of a specific category"""
|
|
|
|
model = Ride
|
|
template_name = "rides/park_category_list.html"
|
|
context_object_name = "rides"
|
|
|
|
def get_queryset(self):
|
|
"""Get rides filtered by category and optionally by park"""
|
|
category = self.kwargs.get("category")
|
|
queryset = Ride.objects.filter(category=category).select_related(
|
|
"park", "ride_model", "ride_model__manufacturer"
|
|
)
|
|
|
|
if "park_slug" in self.kwargs:
|
|
self.park = get_object_or_404(Park, slug=self.kwargs["park_slug"])
|
|
queryset = queryset.filter(park=self.park)
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add park and category information to context"""
|
|
context = super().get_context_data(**kwargs)
|
|
if hasattr(self, "park"):
|
|
context["park"] = self.park
|
|
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,
|
|
)
|
|
context["category"] = category_choice.label if category_choice else "Unknown"
|
|
return context
|
|
|
|
|
|
# Alias for parks app to maintain backward compatibility
|
|
ParkSingleCategoryListView = SingleCategoryListView
|
|
|
|
|
|
def search_companies(request: HttpRequest) -> HttpResponse:
|
|
"""Search companies and return results for HTMX"""
|
|
query = request.GET.get("q", "").strip()
|
|
role = request.GET.get("role", "").upper()
|
|
|
|
companies = Company.objects.only("id", "name", "slug", "roles").order_by("name")
|
|
if role:
|
|
companies = companies.filter(roles__contains=[role])
|
|
if query:
|
|
companies = companies.filter(name__icontains=query)
|
|
companies = companies[:10]
|
|
|
|
return render(
|
|
request,
|
|
"rides/partials/company_search_results.html",
|
|
{"companies": companies, "search_term": query},
|
|
)
|
|
|
|
|
|
def search_ride_models(request: HttpRequest) -> HttpResponse:
|
|
"""Search ride models and return results for HTMX"""
|
|
query = request.GET.get("q", "").strip()
|
|
manufacturer_id = request.GET.get("manufacturer")
|
|
|
|
# Show all ride models on click, filter on input
|
|
ride_models = RideModel.objects.select_related("manufacturer").order_by("name")
|
|
if query:
|
|
ride_models = ride_models.filter(name__icontains=query)
|
|
if manufacturer_id:
|
|
ride_models = ride_models.filter(manufacturer_id=manufacturer_id)
|
|
ride_models = ride_models[:10]
|
|
|
|
return render(
|
|
request,
|
|
"rides/partials/ride_model_search_results.html",
|
|
{
|
|
"ride_models": ride_models,
|
|
"search_term": query,
|
|
"manufacturer_id": manufacturer_id,
|
|
},
|
|
)
|
|
|
|
|
|
def get_search_suggestions(request: HttpRequest) -> HttpResponse:
|
|
"""Get smart search suggestions for rides
|
|
|
|
Returns suggestions including:
|
|
- Common matching ride names
|
|
- Matching parks
|
|
- Matching categories
|
|
"""
|
|
query = request.GET.get("q", "").strip().lower()
|
|
suggestions = []
|
|
|
|
if query:
|
|
# Get common ride names
|
|
matching_names = (
|
|
Ride.objects.filter(name__icontains=query)
|
|
.values("name")
|
|
.annotate(count=Count("id"))
|
|
.order_by("-count")[:3]
|
|
)
|
|
|
|
for match in matching_names:
|
|
suggestions.append(
|
|
{
|
|
"type": "ride",
|
|
"text": match["name"],
|
|
"count": match["count"],
|
|
}
|
|
)
|
|
|
|
# Get matching parks with optimized query
|
|
matching_parks = Park.objects.select_related("location").filter(
|
|
Q(name__icontains=query) | Q(location__city__icontains=query)
|
|
)[:3]
|
|
|
|
for park in matching_parks:
|
|
suggestions.append(
|
|
{
|
|
"type": "park",
|
|
"text": park.name,
|
|
"location": park.location.city if park.location else None,
|
|
}
|
|
)
|
|
|
|
# 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():
|
|
ride_count = Ride.objects.filter(category=choice.value).count()
|
|
suggestions.append(
|
|
{
|
|
"type": "category",
|
|
"code": choice.value,
|
|
"text": choice.label,
|
|
"count": ride_count,
|
|
}
|
|
)
|
|
|
|
return render(
|
|
request,
|
|
"rides/partials/search_suggestions.html",
|
|
{"suggestions": suggestions, "query": query},
|
|
)
|
|
|
|
|
|
class RideSearchView(ListView):
|
|
"""View for ride search functionality with HTMX support."""
|
|
|
|
model = Ride
|
|
template_name = "search/partials/ride_search_results.html"
|
|
context_object_name = "rides"
|
|
paginate_by = 20
|
|
|
|
def get_queryset(self):
|
|
"""Get filtered rides based on search form."""
|
|
queryset = Ride.objects.select_related("park").order_by("name")
|
|
|
|
# Process search form
|
|
form = RideSearchForm(self.request.GET)
|
|
if form.is_valid():
|
|
ride = form.cleaned_data.get("ride")
|
|
if ride:
|
|
# If specific ride selected, return just that ride
|
|
queryset = queryset.filter(id=ride.id)
|
|
else:
|
|
# If no specific ride, filter by search term
|
|
search_term = self.request.GET.get("ride", "").strip()
|
|
if search_term:
|
|
queryset = queryset.filter(name__icontains=search_term)
|
|
|
|
return queryset
|
|
|
|
def get_template_names(self):
|
|
"""Return appropriate template based on request type."""
|
|
if self.request.htmx:
|
|
return ["search/partials/ride_search_results.html"]
|
|
return ["search/ride_search.html"]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add search form to context."""
|
|
context = super().get_context_data(**kwargs)
|
|
context["search_form"] = RideSearchForm(self.request.GET)
|
|
return context
|
|
|
|
|
|
class RideRankingsView(ListView):
|
|
"""View for displaying ride rankings using the Internet Roller Coaster Poll."""
|
|
|
|
model = RideRanking
|
|
template_name = "rides/rankings.html"
|
|
context_object_name = "rankings"
|
|
paginate_by = 50
|
|
|
|
def get_queryset(self):
|
|
"""Get rankings with optimized queries."""
|
|
queryset = RideRanking.objects.select_related(
|
|
"ride", "ride__park", "ride__manufacturer", "ride__ride_model"
|
|
).order_by("rank")
|
|
|
|
# Filter by category if specified
|
|
category = self.request.GET.get("category")
|
|
if category and category != "all":
|
|
queryset = queryset.filter(ride__category=category)
|
|
|
|
# Filter by minimum mutual riders
|
|
min_riders = self.request.GET.get("min_riders")
|
|
if min_riders:
|
|
try:
|
|
min_riders = int(min_riders)
|
|
queryset = queryset.filter(mutual_riders_count__gte=min_riders)
|
|
except ValueError:
|
|
pass
|
|
|
|
return queryset
|
|
|
|
def get_template_names(self):
|
|
"""Return appropriate template based on request type."""
|
|
if self.request.htmx:
|
|
return ["rides/partials/rankings_table.html"]
|
|
return [self.template_name]
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add context for rankings view."""
|
|
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["selected_category"] = self.request.GET.get("category", "all")
|
|
context["min_riders"] = self.request.GET.get("min_riders", "")
|
|
|
|
# Add statistics
|
|
if self.object_list:
|
|
context["total_ranked"] = RideRanking.objects.count()
|
|
context["last_updated"] = (
|
|
self.object_list[0].last_calculated if self.object_list else None
|
|
)
|
|
|
|
return context
|
|
|
|
|
|
class RideRankingDetailView(DetailView):
|
|
"""View for displaying detailed ranking information for a specific ride."""
|
|
|
|
model = Ride
|
|
template_name = "rides/ranking_detail.html"
|
|
slug_url_kwarg = "ride_slug"
|
|
|
|
def get_queryset(self):
|
|
"""Get ride with ranking data."""
|
|
return Ride.objects.select_related(
|
|
"park", "manufacturer", "ranking"
|
|
).prefetch_related("comparisons_as_a", "comparisons_as_b", "ranking_history")
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add ranking details to context."""
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
# Get ranking details from service
|
|
service = RideRankingService()
|
|
ranking_details = service.get_ride_ranking_details(self.object)
|
|
|
|
if ranking_details:
|
|
context.update(ranking_details)
|
|
|
|
# Get recent movement
|
|
recent_snapshots = RankingSnapshot.objects.filter(
|
|
ride=self.object
|
|
).order_by("-snapshot_date")[:7]
|
|
|
|
if len(recent_snapshots) >= 2:
|
|
context["rank_change"] = (
|
|
recent_snapshots[0].rank - recent_snapshots[1].rank
|
|
)
|
|
context["previous_rank"] = recent_snapshots[1].rank
|
|
else:
|
|
context["not_ranked"] = True
|
|
|
|
return context
|
|
|
|
|
|
def ranking_history_chart(request: HttpRequest, ride_slug: str) -> HttpResponse:
|
|
"""HTMX endpoint for ranking history chart data."""
|
|
ride = get_object_or_404(Ride, slug=ride_slug)
|
|
|
|
# Get last 30 days of ranking history
|
|
history = RankingSnapshot.objects.filter(ride=ride).order_by("-snapshot_date")[:30]
|
|
|
|
# Prepare data for chart
|
|
chart_data = [
|
|
{
|
|
"date": snapshot.snapshot_date.isoformat(),
|
|
"rank": snapshot.rank,
|
|
"win_pct": float(snapshot.winning_percentage) * 100,
|
|
}
|
|
for snapshot in reversed(history)
|
|
]
|
|
|
|
return render(
|
|
request,
|
|
"rides/partials/ranking_chart.html",
|
|
{"chart_data": chart_data, "ride": ride},
|
|
)
|
|
|
|
|
|
def ranking_comparisons(request: HttpRequest, ride_slug: str) -> HttpResponse:
|
|
"""HTMX endpoint for ride head-to-head comparisons."""
|
|
ride = get_object_or_404(Ride, slug=ride_slug)
|
|
|
|
# Get head-to-head comparisons
|
|
from django.db.models import Q
|
|
from .models.rankings import RidePairComparison
|
|
|
|
comparisons = (
|
|
RidePairComparison.objects.filter(Q(ride_a=ride) | Q(ride_b=ride))
|
|
.select_related("ride_a", "ride_b", "ride_a__park", "ride_b__park")
|
|
.order_by("-mutual_riders_count")[:20]
|
|
)
|
|
|
|
# Format comparisons for display
|
|
comparison_data = []
|
|
for comp in comparisons:
|
|
if comp.ride_a == ride:
|
|
opponent = comp.ride_b
|
|
wins = comp.ride_a_wins
|
|
losses = comp.ride_b_wins
|
|
else:
|
|
opponent = comp.ride_a
|
|
wins = comp.ride_b_wins
|
|
losses = comp.ride_a_wins
|
|
|
|
result = "win" if wins > losses else "loss" if losses > wins else "tie"
|
|
|
|
comparison_data.append(
|
|
{
|
|
"opponent": opponent,
|
|
"wins": wins,
|
|
"losses": losses,
|
|
"ties": comp.ties,
|
|
"result": result,
|
|
"mutual_riders": comp.mutual_riders_count,
|
|
}
|
|
)
|
|
|
|
return render(
|
|
request,
|
|
"rides/partials/ranking_comparisons.html",
|
|
{"comparisons": comparison_data, "ride": ride},
|
|
)
|
|
|
|
|
|
class ManufacturerListView(ListView):
|
|
"""View for displaying a list of ride manufacturers"""
|
|
|
|
model = Company
|
|
template_name = "manufacturers/manufacturer_list.html"
|
|
context_object_name = "manufacturers"
|
|
paginate_by = 24
|
|
|
|
def get_queryset(self):
|
|
"""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")
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add context data"""
|
|
context = super().get_context_data(**kwargs)
|
|
context["total_manufacturers"] = self.get_queryset().count()
|
|
return context
|
|
|
|
|
|
class DesignerListView(ListView):
|
|
"""View for displaying a list of ride designers"""
|
|
|
|
model = Company
|
|
template_name = "designers/designer_list.html"
|
|
context_object_name = "designers"
|
|
paginate_by = 24
|
|
|
|
def get_queryset(self):
|
|
"""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")
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""Add context data"""
|
|
context = super().get_context_data(**kwargs)
|
|
context["total_designers"] = self.get_queryset().count()
|
|
return context
|