Add ParkContextRequired mixin to enforce park context in ride views; update URLs and templates for global ride listing

This commit is contained in:
pacnpal
2025-02-10 14:48:29 -05:00
parent db78de4cfe
commit df91eb97b8
4 changed files with 36 additions and 50 deletions

View File

@@ -7,7 +7,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib import messages from django.contrib import messages
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse, Http404
from django.db.models import Count from django.db.models import Count
from .models import ( from .models import (
Ride, RollerCoasterStats, RideModel, RideEvent, Ride, RollerCoasterStats, RideModel, RideEvent,
@@ -21,6 +21,13 @@ from moderation.models import EditSubmission
from companies.models import Manufacturer from companies.models import Manufacturer
from designers.models import Designer from designers.models import Designer
class ParkContextRequired:
"""Mixin to require park context for views"""
def dispatch(self, request, *args, **kwargs):
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: 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"""
category = request.GET.get('category') category = request.GET.get('category')
@@ -61,7 +68,7 @@ class RideDetailView(HistoryMixin, DetailView):
return context return context
class RideCreateView(LoginRequiredMixin, CreateView): class RideCreateView(LoginRequiredMixin, ParkContextRequired, CreateView):
"""View for creating a new ride""" """View for creating a new ride"""
model = Ride model = Ride
form_class = RideForm form_class = RideForm
@@ -69,27 +76,23 @@ class RideCreateView(LoginRequiredMixin, CreateView):
def get_success_url(self): def get_success_url(self):
"""Get URL to redirect to after successful creation""" """Get URL to redirect to after successful creation"""
if hasattr(self, 'park'): return reverse('parks:rides:ride_detail', kwargs={
return reverse('parks:rides:ride_detail', kwargs={ 'park_slug': self.park.slug,
'park_slug': self.park.slug, 'ride_slug': self.object.slug
'ride_slug': self.object.slug })
})
return reverse('rides:ride_detail', kwargs={'ride_slug': self.object.slug})
def get_form_kwargs(self): def get_form_kwargs(self):
"""Pass park to the form""" """Pass park to the form"""
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
if 'park_slug' in self.kwargs: self.park = get_object_or_404(Park, slug=self.kwargs['park_slug'])
self.park = get_object_or_404(Park, slug=self.kwargs['park_slug']) kwargs['park'] = self.park
kwargs['park'] = self.park
return kwargs return kwargs
def get_context_data(self, **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 = super().get_context_data(**kwargs)
if hasattr(self, 'park'): context['park'] = self.park
context['park'] = self.park context['park_slug'] = self.park.slug
context['park_slug'] = self.park.slug
context['is_edit'] = False context['is_edit'] = False
return context return context
@@ -131,7 +134,7 @@ class RideCreateView(LoginRequiredMixin, CreateView):
return super().form_valid(form) return super().form_valid(form)
class RideUpdateView(LoginRequiredMixin, EditSubmissionMixin, UpdateView): class RideUpdateView(LoginRequiredMixin, ParkContextRequired, EditSubmissionMixin, UpdateView):
"""View for updating an existing ride""" """View for updating an existing ride"""
model = Ride model = Ride
form_class = RideForm form_class = RideForm
@@ -140,39 +143,27 @@ class RideUpdateView(LoginRequiredMixin, EditSubmissionMixin, UpdateView):
def get_success_url(self): def get_success_url(self):
"""Get URL to redirect to after successful update""" """Get URL to redirect to after successful update"""
if hasattr(self, 'park'): return reverse('parks:rides:ride_detail', kwargs={
return reverse('parks:rides:ride_detail', kwargs={ 'park_slug': self.park.slug,
'park_slug': self.park.slug, 'ride_slug': self.object.slug
'ride_slug': self.object.slug })
})
return reverse('rides:ride_detail', kwargs={'ride_slug': self.object.slug})
def get_queryset(self): def get_queryset(self):
"""Get ride for the specific park if park_slug is provided""" """Get ride for the specific park"""
queryset = Ride.objects.all() return Ride.objects.filter(park__slug=self.kwargs['park_slug'])
if 'park_slug' in self.kwargs:
queryset = queryset.filter(park__slug=self.kwargs['park_slug'])
return queryset
def get_form_kwargs(self): def get_form_kwargs(self):
"""Pass park to the form""" """Pass park to the form"""
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
# For park-specific URLs, use the park from the URL self.park = get_object_or_404(Park, slug=self.kwargs['park_slug'])
if 'park_slug' in self.kwargs: kwargs['park'] = self.park
self.park = get_object_or_404(Park, slug=self.kwargs['park_slug'])
kwargs['park'] = self.park
# For global URLs, use the ride's park
else:
self.park = self.get_object().park
kwargs['park'] = self.park
return kwargs return kwargs
def get_context_data(self, **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 = super().get_context_data(**kwargs)
if hasattr(self, 'park'): context['park'] = self.park
context['park'] = self.park context['park_slug'] = self.park.slug
context['park_slug'] = self.park.slug
context['is_edit'] = True context['is_edit'] = True
return context return context

View File

@@ -99,7 +99,7 @@
<i class="fas fa-map-marker-alt"></i> <i class="fas fa-map-marker-alt"></i>
<span>Parks</span> <span>Parks</span>
</a> </a>
<a href="{% url 'rides:ride_list' %}" class="nav-link"> <a href="{% url 'rides:global_ride_list' %}" class="nav-link">
<i class="fas fa-rocket"></i> <i class="fas fa-rocket"></i>
<span>Rides</span> <span>Rides</span>
</a> </a>

View File

@@ -18,7 +18,7 @@
class="px-8 py-3 text-lg btn-primary"> class="px-8 py-3 text-lg btn-primary">
Explore Parks Explore Parks
</a> </a>
<a href="{% url 'rides:ride_list' %}" <a href="{% url 'rides:global_ride_list' %}"
class="px-8 py-3 text-lg btn-secondary"> class="px-8 py-3 text-lg btn-secondary">
View Rides View Rides
</a> </a>
@@ -40,7 +40,7 @@
</a> </a>
<!-- Total Attractions --> <!-- Total Attractions -->
<a href="{% url 'rides:ride_list' %}" <a href="{% url 'rides:global_ride_list' %}"
class="flex flex-col items-center justify-center p-6 text-center transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1 hover:shadow-xl"> class="flex flex-col items-center justify-center p-6 text-center transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1 hover:shadow-xl">
<div class="mb-2 text-4xl font-bold text-blue-600 dark:text-blue-400"> <div class="mb-2 text-4xl font-bold text-blue-600 dark:text-blue-400">
{{ stats.ride_count }} {{ stats.ride_count }}
@@ -51,7 +51,7 @@
</a> </a>
<!-- Total Roller Coasters --> <!-- Total Roller Coasters -->
<a href="{% url 'rides:roller_coasters' %}" <a href="{% url 'rides:global_roller_coasters' %}"
class="flex flex-col items-center justify-center p-6 text-center transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1 hover:shadow-xl"> class="flex flex-col items-center justify-center p-6 text-center transition-transform transform bg-white rounded-lg shadow-lg dark:bg-gray-800 hover:-translate-y-1 hover:shadow-xl">
<div class="mb-2 text-4xl font-bold text-blue-600 dark:text-blue-400"> <div class="mb-2 text-4xl font-bold text-blue-600 dark:text-blue-400">
{{ stats.coaster_count }} {{ stats.coaster_count }}
@@ -108,7 +108,7 @@
</h2> </h2>
<div class="space-y-4"> <div class="space-y-4">
{% for ride in popular_rides %} {% for ride in popular_rides %}
<a href="{% url 'rides:ride_detail' ride.slug %}" <a href="{% url 'parks:rides:ride_detail' ride.park.slug ride.slug %}"
class="relative block h-48 overflow-hidden transition-all rounded-lg group hover:-translate-y-1 hover:shadow-xl" class="relative block h-48 overflow-hidden transition-all rounded-lg group hover:-translate-y-1 hover:shadow-xl"
{% if ride.photos.first %} {% if ride.photos.first %}
style="background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.7)), url('{{ ride.photos.first.image.url }}') center/cover no-repeat;" style="background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.7)), url('{{ ride.photos.first.image.url }}') center/cover no-repeat;"
@@ -147,7 +147,7 @@
{% for item in highest_rated %} {% for item in highest_rated %}
{% if item.park %} {% if item.park %}
<!-- This is a ride --> <!-- This is a ride -->
<a href="{% url 'rides:ride_detail' item.slug %}" <a href="{% url 'parks:rides:ride_detail' item.park.slug item.slug %}"
class="relative block h-48 overflow-hidden transition-all rounded-lg group hover:-translate-y-1 hover:shadow-xl" class="relative block h-48 overflow-hidden transition-all rounded-lg group hover:-translate-y-1 hover:shadow-xl"
{% if item.photos.first %} {% if item.photos.first %}
style="background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.7)), url('{{ item.photos.first.image.url }}') center/cover no-repeat;" style="background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.7)), url('{{ item.photos.first.image.url }}') center/cover no-repeat;"

View File

@@ -32,12 +32,7 @@
Add Ride Add Ride
</a> </a>
{% else %} {% else %}
<a href="{% url 'rides:ride_create' %}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600"> <!-- No add ride button in global view - rides must be added from park pages -->
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
Add Ride
</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
@@ -45,7 +40,7 @@
<!-- Filters --> <!-- Filters -->
<div class="p-4 mb-6 bg-white rounded-lg shadow dark:bg-gray-800"> <div class="p-4 mb-6 bg-white rounded-lg shadow dark:bg-gray-800">
<form class="grid grid-cols-1 gap-4 md:grid-cols-3" <form class="grid grid-cols-1 gap-4 md:grid-cols-3"
hx-get="{% if park %}{% url 'parks:rides:ride_list' park.slug %}{% else %}{% url 'rides:ride_list' %}{% endif %}" hx-get="{% if park %}{% url 'parks:rides:ride_list' park.slug %}{% else %}{% url 'rides:global_ride_list' %}{% endif %}"
hx-trigger="change from:select, input from:input[type='text']" hx-trigger="change from:select, input from:input[type='text']"
hx-target="#rides-grid" hx-target="#rides-grid"
hx-push-url="true"> hx-push-url="true">