mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-23 09:51:08 -05:00
Enhance moderation dashboard UI and UX:
- Add HTMX-powered filtering with instant updates - Add smooth transitions and loading states - Improve visual hierarchy and styling - Add review notes functionality - Add confirmation dialogs for actions - Make navigation sticky - Add hover effects and visual feedback - Improve dark mode support
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RidesConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'rides'
|
||||
|
||||
def ready(self):
|
||||
import rides.signals # noqa
|
||||
import rides.signals
|
||||
|
||||
335
rides/forms.py
335
rides/forms.py
@@ -1,74 +1,283 @@
|
||||
from django import forms
|
||||
from .models import Ride
|
||||
from django.forms import ModelChoiceField
|
||||
from django.urls import reverse_lazy
|
||||
from .models import Ride, RideModel
|
||||
from parks.models import Park, ParkArea
|
||||
from companies.models import Manufacturer, Designer
|
||||
|
||||
|
||||
class RideForm(forms.ModelForm):
|
||||
park_search = forms.CharField(
|
||||
label="Park *",
|
||||
required=True,
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"placeholder": "Search for a park...",
|
||||
"hx-get": "/parks/search/",
|
||||
"hx-trigger": "click, input delay:200ms",
|
||||
"hx-target": "#park-search-results",
|
||||
"name": "q",
|
||||
"autocomplete": "off",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
manufacturer_search = forms.CharField(
|
||||
label="Manufacturer",
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"placeholder": "Search for a manufacturer...",
|
||||
"hx-get": reverse_lazy("rides:search_manufacturers"),
|
||||
"hx-trigger": "click, input delay:200ms",
|
||||
"hx-target": "#manufacturer-search-results",
|
||||
"name": "q",
|
||||
"autocomplete": "off",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
designer_search = forms.CharField(
|
||||
label="Designer",
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"placeholder": "Search for a designer...",
|
||||
"hx-get": reverse_lazy("rides:search_designers"),
|
||||
"hx-trigger": "click, input delay:200ms",
|
||||
"hx-target": "#designer-search-results",
|
||||
"name": "q",
|
||||
"autocomplete": "off",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
ride_model_search = forms.CharField(
|
||||
label="Ride Model",
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"placeholder": "Search for a ride model...",
|
||||
"hx-get": reverse_lazy("rides:search_ride_models"),
|
||||
"hx-trigger": "click, input delay:200ms",
|
||||
"hx-target": "#ride-model-search-results",
|
||||
"hx-include": "[name='manufacturer']",
|
||||
"name": "q",
|
||||
"autocomplete": "off",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
park = forms.ModelChoiceField(
|
||||
queryset=Park.objects.all(),
|
||||
required=True,
|
||||
label="",
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
|
||||
manufacturer = forms.ModelChoiceField(
|
||||
queryset=Manufacturer.objects.all(),
|
||||
required=False,
|
||||
label="",
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
|
||||
designer = forms.ModelChoiceField(
|
||||
queryset=Designer.objects.all(),
|
||||
required=False,
|
||||
label="",
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
|
||||
ride_model = forms.ModelChoiceField(
|
||||
queryset=RideModel.objects.all(),
|
||||
required=False,
|
||||
label="",
|
||||
widget=forms.HiddenInput()
|
||||
)
|
||||
|
||||
park_area = ModelChoiceField(
|
||||
queryset=ParkArea.objects.none(),
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"placeholder": "Select an area within the park..."
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Ride
|
||||
fields = ['name', 'park_area', 'category', 'manufacturer', 'designer', 'model_name', 'status',
|
||||
'opening_date', 'closing_date', 'status_since', 'min_height_in', 'max_height_in',
|
||||
'accessibility_options', 'capacity_per_hour', 'ride_duration_seconds', 'description']
|
||||
fields = [
|
||||
"name",
|
||||
"category",
|
||||
"manufacturer",
|
||||
"designer",
|
||||
"ride_model",
|
||||
"status",
|
||||
"post_closing_status",
|
||||
"opening_date",
|
||||
"closing_date",
|
||||
"status_since",
|
||||
"min_height_in",
|
||||
"max_height_in",
|
||||
"capacity_per_hour",
|
||||
"ride_duration_seconds",
|
||||
"description",
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'park_area': forms.Select(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'category': forms.Select(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'manufacturer': forms.Select(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'designer': forms.Select(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'model_name': forms.TextInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'status': forms.Select(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'opening_date': forms.DateInput(attrs={
|
||||
'type': 'date',
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'closing_date': forms.DateInput(attrs={
|
||||
'type': 'date',
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'status_since': forms.DateInput(attrs={
|
||||
'type': 'date',
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'min_height_in': forms.NumberInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
|
||||
'min': '0'
|
||||
}),
|
||||
'max_height_in': forms.NumberInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
|
||||
'min': '0'
|
||||
}),
|
||||
'accessibility_options': forms.TextInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
'capacity_per_hour': forms.NumberInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
|
||||
'min': '0'
|
||||
}),
|
||||
'ride_duration_seconds': forms.NumberInput(attrs={
|
||||
'class': 'w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white',
|
||||
'min': '0'
|
||||
}),
|
||||
'description': forms.Textarea(attrs={
|
||||
'rows': 4,
|
||||
'class': 'w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white'
|
||||
}),
|
||||
"name": forms.TextInput(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"placeholder": "Official name of the ride"
|
||||
}
|
||||
),
|
||||
"category": forms.Select(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"hx-get": reverse_lazy("rides:coaster_fields"),
|
||||
"hx-target": "#coaster-fields",
|
||||
"hx-trigger": "change",
|
||||
"hx-include": "this",
|
||||
"hx-swap": "innerHTML"
|
||||
}
|
||||
),
|
||||
"status": forms.Select(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"placeholder": "Current operational status",
|
||||
"x-model": "status",
|
||||
"@change": "handleStatusChange"
|
||||
}
|
||||
),
|
||||
"post_closing_status": forms.Select(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"placeholder": "Status after closing",
|
||||
"x-show": "status === 'CLOSING'"
|
||||
}
|
||||
),
|
||||
"opening_date": forms.DateInput(
|
||||
attrs={
|
||||
"type": "date",
|
||||
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"placeholder": "Date when ride first opened"
|
||||
}
|
||||
),
|
||||
"closing_date": forms.DateInput(
|
||||
attrs={
|
||||
"type": "date",
|
||||
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"placeholder": "Date when ride will close",
|
||||
"x-show": "['CLOSING', 'SBNO', 'CLOSED_PERM', 'DEMOLISHED', 'RELOCATED'].includes(status)",
|
||||
":required": "status === 'CLOSING'"
|
||||
}
|
||||
),
|
||||
"status_since": forms.DateInput(
|
||||
attrs={
|
||||
"type": "date",
|
||||
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"placeholder": "Date when current status took effect"
|
||||
}
|
||||
),
|
||||
"min_height_in": forms.NumberInput(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"min": "0",
|
||||
"placeholder": "Minimum height requirement in inches"
|
||||
}
|
||||
),
|
||||
"max_height_in": forms.NumberInput(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"min": "0",
|
||||
"placeholder": "Maximum height limit in inches (if applicable)"
|
||||
}
|
||||
),
|
||||
"capacity_per_hour": forms.NumberInput(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"min": "0",
|
||||
"placeholder": "Theoretical hourly ride capacity"
|
||||
}
|
||||
),
|
||||
"ride_duration_seconds": forms.NumberInput(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"min": "0",
|
||||
"placeholder": "Total duration of one ride cycle in seconds"
|
||||
}
|
||||
),
|
||||
"description": forms.Textarea(
|
||||
attrs={
|
||||
"rows": 4,
|
||||
"class": "w-full border-gray-300 rounded-lg form-textarea dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"placeholder": "General description and notable features of the ride"
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
park = kwargs.pop('park', None)
|
||||
park = kwargs.pop("park", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Make category required
|
||||
self.fields['category'].required = True
|
||||
|
||||
# Clear any default values for date fields
|
||||
self.fields["opening_date"].initial = None
|
||||
self.fields["closing_date"].initial = None
|
||||
self.fields["status_since"].initial = None
|
||||
|
||||
# Move fields to the beginning in desired order
|
||||
field_order = [
|
||||
"park_search", "park", "park_area",
|
||||
"name", "manufacturer_search", "manufacturer",
|
||||
"designer_search", "designer", "ride_model_search",
|
||||
"ride_model", "category", "status",
|
||||
"post_closing_status", "opening_date", "closing_date", "status_since",
|
||||
"min_height_in", "max_height_in", "capacity_per_hour",
|
||||
"ride_duration_seconds", "description"
|
||||
]
|
||||
self.order_fields(field_order)
|
||||
|
||||
if park:
|
||||
# Filter park_area choices to only show areas from the current park
|
||||
self.fields['park_area'].queryset = park.areas.all()
|
||||
# If park is provided, set it as the initial value
|
||||
self.fields["park"].initial = park
|
||||
# Hide the park search field since we know the park
|
||||
del self.fields["park_search"]
|
||||
# Create new park_area field with park's areas
|
||||
self.fields["park_area"] = forms.ModelChoiceField(
|
||||
queryset=park.areas.all(),
|
||||
required=False,
|
||||
widget=forms.Select(
|
||||
attrs={
|
||||
"class": "w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white",
|
||||
"placeholder": "Select an area within the park..."
|
||||
}
|
||||
),
|
||||
)
|
||||
else:
|
||||
# If no park provided, show park search and disable park_area until park is selected
|
||||
self.fields["park_area"].widget.attrs["disabled"] = True
|
||||
# Initialize park search with current park name if editing
|
||||
if self.instance and self.instance.pk and self.instance.park:
|
||||
self.fields["park_search"].initial = self.instance.park.name
|
||||
self.fields["park"].initial = self.instance.park
|
||||
|
||||
# Initialize manufacturer, designer, and ride model search fields if editing
|
||||
if self.instance and self.instance.pk:
|
||||
if self.instance.manufacturer:
|
||||
self.fields["manufacturer_search"].initial = self.instance.manufacturer.name
|
||||
self.fields["manufacturer"].initial = self.instance.manufacturer
|
||||
if self.instance.designer:
|
||||
self.fields["designer_search"].initial = self.instance.designer.name
|
||||
self.fields["designer"].initial = self.instance.designer
|
||||
if self.instance.ride_model:
|
||||
self.fields["ride_model_search"].initial = self.instance.ride_model.name
|
||||
self.fields["ride_model"].initial = self.instance.ride_model
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.2 on 2024-10-28 21:53
|
||||
# Generated by Django 5.1.3 on 2024-11-12 18:07
|
||||
|
||||
import django.db.models.deletion
|
||||
import simple_history.models
|
||||
@@ -11,7 +11,8 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("companies", "0002_stats_fields"),
|
||||
("companies", "0001_initial"),
|
||||
("designers", "0001_initial"),
|
||||
("parks", "0001_initial"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
@@ -20,12 +21,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name="HistoricalRide",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigIntegerField(
|
||||
auto_created=True, blank=True, db_index=True, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("id", models.BigIntegerField(blank=True, db_index=True)),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("slug", models.SlugField(max_length=255)),
|
||||
("description", models.TextField(blank=True)),
|
||||
@@ -92,6 +88,18 @@ class Migration(migrations.Migration):
|
||||
max_length=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
"designer",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
help_text="The designer/engineering firm responsible for the ride",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="designers.designer",
|
||||
),
|
||||
),
|
||||
(
|
||||
"history_user",
|
||||
models.ForeignKey(
|
||||
@@ -146,15 +154,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name="Ride",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("id", models.BigAutoField(primary_key=True, serialize=False)),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("slug", models.SlugField(max_length=255)),
|
||||
("description", models.TextField(blank=True)),
|
||||
@@ -212,12 +212,20 @@ class Migration(migrations.Migration):
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"manufacturer",
|
||||
"designer",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="The designer/engineering firm responsible for the ride",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="rides",
|
||||
to="designers.designer",
|
||||
),
|
||||
),
|
||||
(
|
||||
"manufacturer",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="companies.manufacturer",
|
||||
),
|
||||
),
|
||||
@@ -248,12 +256,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name="HistoricalRollerCoasterStats",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigIntegerField(
|
||||
auto_created=True, blank=True, db_index=True, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
("id", models.BigIntegerField(blank=True, db_index=True)),
|
||||
(
|
||||
"height_ft",
|
||||
models.DecimalField(
|
||||
@@ -278,6 +281,57 @@ class Migration(migrations.Migration):
|
||||
models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
("track_type", models.CharField(blank=True, max_length=255)),
|
||||
(
|
||||
"track_material",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("STEEL", "Steel"),
|
||||
("WOOD", "Wood"),
|
||||
("HYBRID", "Hybrid"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
default="STEEL",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"roller_coaster_type",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("SITDOWN", "Sit-Down"),
|
||||
("INVERTED", "Inverted"),
|
||||
("FLYING", "Flying"),
|
||||
("STANDUP", "Stand-Up"),
|
||||
("WING", "Wing"),
|
||||
("SUSPENDED", "Suspended"),
|
||||
("BOBSLED", "Bobsled"),
|
||||
("PIPELINE", "Pipeline"),
|
||||
("MOTORBIKE", "Motorbike"),
|
||||
("FLOORLESS", "Floorless"),
|
||||
("DIVE", "Dive"),
|
||||
("FAMILY", "Family"),
|
||||
("WILD_MOUSE", "Wild Mouse"),
|
||||
("SPINNING", "Spinning"),
|
||||
("FOURTH_DIMENSION", "4th Dimension"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
default="SITDOWN",
|
||||
help_text="The type/style of roller coaster (e.g. Sit-Down, Inverted, Flying)",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"max_drop_height_ft",
|
||||
models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum vertical drop height in feet",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"launch_type",
|
||||
models.CharField(
|
||||
@@ -340,15 +394,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name="RollerCoasterStats",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("id", models.BigAutoField(primary_key=True, serialize=False)),
|
||||
(
|
||||
"height_ft",
|
||||
models.DecimalField(
|
||||
@@ -373,6 +419,57 @@ class Migration(migrations.Migration):
|
||||
models.PositiveIntegerField(blank=True, null=True),
|
||||
),
|
||||
("track_type", models.CharField(blank=True, max_length=255)),
|
||||
(
|
||||
"track_material",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("STEEL", "Steel"),
|
||||
("WOOD", "Wood"),
|
||||
("HYBRID", "Hybrid"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
default="STEEL",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"roller_coaster_type",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("SITDOWN", "Sit-Down"),
|
||||
("INVERTED", "Inverted"),
|
||||
("FLYING", "Flying"),
|
||||
("STANDUP", "Stand-Up"),
|
||||
("WING", "Wing"),
|
||||
("SUSPENDED", "Suspended"),
|
||||
("BOBSLED", "Bobsled"),
|
||||
("PIPELINE", "Pipeline"),
|
||||
("MOTORBIKE", "Motorbike"),
|
||||
("FLOORLESS", "Floorless"),
|
||||
("DIVE", "Dive"),
|
||||
("FAMILY", "Family"),
|
||||
("WILD_MOUSE", "Wild Mouse"),
|
||||
("SPINNING", "Spinning"),
|
||||
("FOURTH_DIMENSION", "4th Dimension"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
default="SITDOWN",
|
||||
help_text="The type/style of roller coaster (e.g. Sit-Down, Inverted, Flying)",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
(
|
||||
"max_drop_height_ft",
|
||||
models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum vertical drop height in feet",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"launch_type",
|
||||
models.CharField(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-04 00:28
|
||||
# Generated by Django 5.1.3 on 2024-11-12 20:23
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
@@ -7,12 +7,12 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("designers", "0001_initial"),
|
||||
("rides", "0004_historicalrollercoasterstats_roller_coaster_type_and_more"),
|
||||
("companies", "0002_add_designer_model"),
|
||||
("rides", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
migrations.AlterField(
|
||||
model_name="historicalride",
|
||||
name="designer",
|
||||
field=models.ForeignKey(
|
||||
@@ -22,10 +22,10 @@ class Migration(migrations.Migration):
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="designers.designer",
|
||||
to="companies.designer",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
migrations.AlterField(
|
||||
model_name="ride",
|
||||
name="designer",
|
||||
field=models.ForeignKey(
|
||||
@@ -34,7 +34,7 @@ class Migration(migrations.Migration):
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="rides",
|
||||
to="designers.designer",
|
||||
to="companies.designer",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,25 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-10-29 02:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("companies", "0004_add_total_parks"),
|
||||
("rides", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="ride",
|
||||
name="manufacturer",
|
||||
field=models.ForeignKey(
|
||||
default=1,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="companies.manufacturer",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,65 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-04 00:17
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("rides", "0002_alter_ride_manufacturer"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="historicalrollercoasterstats",
|
||||
name="max_drop_height_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum vertical drop height in feet",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="historicalrollercoasterstats",
|
||||
name="track_material",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("STEEL", "Steel"),
|
||||
("WOOD", "Wood"),
|
||||
("HYBRID", "Hybrid"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
default="STEEL",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="rollercoasterstats",
|
||||
name="max_drop_height_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Maximum vertical drop height in feet",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="rollercoasterstats",
|
||||
name="track_material",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("STEEL", "Steel"),
|
||||
("WOOD", "Wood"),
|
||||
("HYBRID", "Hybrid"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
default="STEEL",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,160 @@
|
||||
# Generated by Django 5.1.3 on 2024-11-12 21:40
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("companies", "0002_add_designer_model"),
|
||||
("rides", "0002_alter_historicalride_designer_alter_ride_designer"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="historicalride",
|
||||
name="accessibility_options",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="ride",
|
||||
name="accessibility_options",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="historicalride",
|
||||
name="category",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("", "Select ride type... *"),
|
||||
("RC", "Roller Coaster"),
|
||||
("DR", "Dark Ride"),
|
||||
("FR", "Flat Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("TR", "Transport"),
|
||||
("OT", "Other"),
|
||||
],
|
||||
default="",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="historicalride",
|
||||
name="designer",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="companies.designer",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="historicalrollercoasterstats",
|
||||
name="max_drop_height_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=6, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="historicalrollercoasterstats",
|
||||
name="roller_coaster_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("SITDOWN", "Sit-Down"),
|
||||
("INVERTED", "Inverted"),
|
||||
("FLYING", "Flying"),
|
||||
("STANDUP", "Stand-Up"),
|
||||
("WING", "Wing"),
|
||||
("SUSPENDED", "Suspended"),
|
||||
("BOBSLED", "Bobsled"),
|
||||
("PIPELINE", "Pipeline"),
|
||||
("MOTORBIKE", "Motorbike"),
|
||||
("FLOORLESS", "Floorless"),
|
||||
("DIVE", "Dive"),
|
||||
("FAMILY", "Family"),
|
||||
("WILD_MOUSE", "Wild Mouse"),
|
||||
("SPINNING", "Spinning"),
|
||||
("FOURTH_DIMENSION", "4th Dimension"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
default="SITDOWN",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ride",
|
||||
name="category",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("", "Select ride type... *"),
|
||||
("RC", "Roller Coaster"),
|
||||
("DR", "Dark Ride"),
|
||||
("FR", "Flat Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("TR", "Transport"),
|
||||
("OT", "Other"),
|
||||
],
|
||||
default="",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ride",
|
||||
name="designer",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="rides",
|
||||
to="companies.designer",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ride",
|
||||
name="manufacturer",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="companies.manufacturer",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="max_drop_height_ft",
|
||||
field=models.DecimalField(
|
||||
blank=True, decimal_places=2, max_digits=6, null=True
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="roller_coaster_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("SITDOWN", "Sit-Down"),
|
||||
("INVERTED", "Inverted"),
|
||||
("FLYING", "Flying"),
|
||||
("STANDUP", "Stand-Up"),
|
||||
("WING", "Wing"),
|
||||
("SUSPENDED", "Suspended"),
|
||||
("BOBSLED", "Bobsled"),
|
||||
("PIPELINE", "Pipeline"),
|
||||
("MOTORBIKE", "Motorbike"),
|
||||
("FLOORLESS", "Floorless"),
|
||||
("DIVE", "Dive"),
|
||||
("FAMILY", "Family"),
|
||||
("WILD_MOUSE", "Wild Mouse"),
|
||||
("SPINNING", "Spinning"),
|
||||
("FOURTH_DIMENSION", "4th Dimension"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
default="SITDOWN",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,49 @@
|
||||
# Generated by Django 5.1.3 on 2024-11-12 21:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("rides", "0003_remove_historicalride_accessibility_options_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="historicalride",
|
||||
name="category",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("", "Select ride type"),
|
||||
("RC", "Roller Coaster"),
|
||||
("DR", "Dark Ride"),
|
||||
("FR", "Flat Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("TR", "Transport"),
|
||||
("OT", "Other"),
|
||||
],
|
||||
default="",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ride",
|
||||
name="category",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("", "Select ride type"),
|
||||
("RC", "Roller Coaster"),
|
||||
("DR", "Dark Ride"),
|
||||
("FR", "Flat Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("TR", "Transport"),
|
||||
("OT", "Other"),
|
||||
],
|
||||
default="",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,69 +0,0 @@
|
||||
# Generated by Django 5.1.2 on 2024-11-04 00:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("rides", "0003_historicalrollercoasterstats_max_drop_height_ft_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="historicalrollercoasterstats",
|
||||
name="roller_coaster_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("SITDOWN", "Sit-Down"),
|
||||
("INVERTED", "Inverted"),
|
||||
("FLYING", "Flying"),
|
||||
("STANDUP", "Stand-Up"),
|
||||
("WING", "Wing"),
|
||||
("SUSPENDED", "Suspended"),
|
||||
("BOBSLED", "Bobsled"),
|
||||
("PIPELINE", "Pipeline"),
|
||||
("MOTORBIKE", "Motorbike"),
|
||||
("FLOORLESS", "Floorless"),
|
||||
("DIVE", "Dive"),
|
||||
("FAMILY", "Family"),
|
||||
("WILD_MOUSE", "Wild Mouse"),
|
||||
("SPINNING", "Spinning"),
|
||||
("FOURTH_DIMENSION", "4th Dimension"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
default="SITDOWN",
|
||||
help_text="The type/style of roller coaster (e.g. Sit-Down, Inverted, Flying)",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="rollercoasterstats",
|
||||
name="roller_coaster_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("SITDOWN", "Sit-Down"),
|
||||
("INVERTED", "Inverted"),
|
||||
("FLYING", "Flying"),
|
||||
("STANDUP", "Stand-Up"),
|
||||
("WING", "Wing"),
|
||||
("SUSPENDED", "Suspended"),
|
||||
("BOBSLED", "Bobsled"),
|
||||
("PIPELINE", "Pipeline"),
|
||||
("MOTORBIKE", "Motorbike"),
|
||||
("FLOORLESS", "Floorless"),
|
||||
("DIVE", "Dive"),
|
||||
("FAMILY", "Family"),
|
||||
("WILD_MOUSE", "Wild Mouse"),
|
||||
("SPINNING", "Spinning"),
|
||||
("FOURTH_DIMENSION", "4th Dimension"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
default="SITDOWN",
|
||||
help_text="The type/style of roller coaster (e.g. Sit-Down, Inverted, Flying)",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
||||
259
rides/migrations/0005_alter_rollercoasterstats_id_and_more.py
Normal file
259
rides/migrations/0005_alter_rollercoasterstats_id_and_more.py
Normal file
@@ -0,0 +1,259 @@
|
||||
# Generated by Django 5.1.3 on 2024-11-12 22:27
|
||||
|
||||
import django.db.models.deletion
|
||||
import simple_history.models
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("companies", "0002_add_designer_model"),
|
||||
("rides", "0004_alter_historicalride_category_alter_ride_category"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="id",
|
||||
field=models.BigAutoField(
|
||||
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="launch_type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("CHAIN", "Chain Lift"),
|
||||
("LSM", "LSM Launch"),
|
||||
("HYDRAULIC", "Hydraulic Launch"),
|
||||
("GRAVITY", "Gravity"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
default="CHAIN",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="roller_coaster_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("SITDOWN", "Sit Down"),
|
||||
("INVERTED", "Inverted"),
|
||||
("FLYING", "Flying"),
|
||||
("STANDUP", "Stand Up"),
|
||||
("WING", "Wing"),
|
||||
("DIVE", "Dive"),
|
||||
("FAMILY", "Family"),
|
||||
("WILD_MOUSE", "Wild Mouse"),
|
||||
("SPINNING", "Spinning"),
|
||||
("FOURTH_DIMENSION", "4th Dimension"),
|
||||
("OTHER", "Other"),
|
||||
],
|
||||
default="SITDOWN",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rollercoasterstats",
|
||||
name="track_material",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[("STEEL", "Steel"), ("WOOD", "Wood"), ("HYBRID", "Hybrid")],
|
||||
default="STEEL",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="HistoricalRideModel",
|
||||
fields=[
|
||||
("id", models.BigIntegerField(blank=True, db_index=True)),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("description", models.TextField(blank=True)),
|
||||
(
|
||||
"typical_height_ft",
|
||||
models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Typical height of this model in feet",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"typical_speed_mph",
|
||||
models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Typical speed of this model in mph",
|
||||
max_digits=5,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"typical_capacity_per_hour",
|
||||
models.PositiveIntegerField(
|
||||
blank=True,
|
||||
help_text="Typical hourly capacity of this model",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"category",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("", "Select ride type"),
|
||||
("RC", "Roller Coaster"),
|
||||
("DR", "Dark Ride"),
|
||||
("FR", "Flat Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("TR", "Transport"),
|
||||
("OT", "Other"),
|
||||
],
|
||||
default="",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(blank=True, editable=False)),
|
||||
("updated_at", models.DateTimeField(blank=True, editable=False)),
|
||||
("history_id", models.AutoField(primary_key=True, serialize=False)),
|
||||
("history_date", models.DateTimeField(db_index=True)),
|
||||
("history_change_reason", models.CharField(max_length=100, null=True)),
|
||||
(
|
||||
"history_type",
|
||||
models.CharField(
|
||||
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
|
||||
max_length=1,
|
||||
),
|
||||
),
|
||||
(
|
||||
"history_user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"manufacturer",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="companies.manufacturer",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "historical ride model",
|
||||
"verbose_name_plural": "historical ride models",
|
||||
"ordering": ("-history_date", "-history_id"),
|
||||
"get_latest_by": ("history_date", "history_id"),
|
||||
},
|
||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="RideModel",
|
||||
fields=[
|
||||
("id", models.BigAutoField(primary_key=True, serialize=False)),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("description", models.TextField(blank=True)),
|
||||
(
|
||||
"typical_height_ft",
|
||||
models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Typical height of this model in feet",
|
||||
max_digits=6,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"typical_speed_mph",
|
||||
models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text="Typical speed of this model in mph",
|
||||
max_digits=5,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"typical_capacity_per_hour",
|
||||
models.PositiveIntegerField(
|
||||
blank=True,
|
||||
help_text="Typical hourly capacity of this model",
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"category",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("", "Select ride type"),
|
||||
("RC", "Roller Coaster"),
|
||||
("DR", "Dark Ride"),
|
||||
("FR", "Flat Ride"),
|
||||
("WR", "Water Ride"),
|
||||
("TR", "Transport"),
|
||||
("OT", "Other"),
|
||||
],
|
||||
default="",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
(
|
||||
"manufacturer",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="ride_models",
|
||||
to="companies.manufacturer",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["manufacturer", "name"],
|
||||
"unique_together": {("manufacturer", "name")},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="historicalride",
|
||||
name="ride_model",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
db_constraint=False,
|
||||
help_text="The specific model/type of this ride",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name="+",
|
||||
to="rides.ridemodel",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="ride",
|
||||
name="ride_model",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="The specific model/type of this ride",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="rides",
|
||||
to="rides.ridemodel",
|
||||
),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="HistoricalRollerCoasterStats",
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 5.1.3 on 2024-11-13 00:20
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("rides", "0005_alter_rollercoasterstats_id_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="historicalridemodel",
|
||||
name="typical_capacity_per_hour",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="historicalridemodel",
|
||||
name="typical_height_ft",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="historicalridemodel",
|
||||
name="typical_speed_mph",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="ridemodel",
|
||||
name="typical_capacity_per_hour",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="ridemodel",
|
||||
name="typical_height_ft",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="ridemodel",
|
||||
name="typical_speed_mph",
|
||||
),
|
||||
]
|
||||
26
rides/migrations/0007_alter_ridemodel_manufacturer.py
Normal file
26
rides/migrations/0007_alter_ridemodel_manufacturer.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 5.1.3 on 2024-11-13 02:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("companies", "0002_add_designer_model"),
|
||||
("rides", "0006_remove_historicalridemodel_typical_capacity_per_hour_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="ridemodel",
|
||||
name="manufacturer",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="ride_models",
|
||||
to="companies.manufacturer",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,75 @@
|
||||
# Generated by Django 5.1.3 on 2024-11-13 04:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("rides", "0007_alter_ridemodel_manufacturer"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="historicalride",
|
||||
name="post_closing_status",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("SBNO", "Standing But Not Operating"),
|
||||
("CLOSED_PERM", "Permanently Closed"),
|
||||
],
|
||||
help_text="Status to change to after closing date",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="ride",
|
||||
name="post_closing_status",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("SBNO", "Standing But Not Operating"),
|
||||
("CLOSED_PERM", "Permanently Closed"),
|
||||
],
|
||||
help_text="Status to change to after closing date",
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="historicalride",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("OPERATING", "Operating"),
|
||||
("SBNO", "Standing But Not Operating"),
|
||||
("CLOSING", "Closing"),
|
||||
("CLOSED_PERM", "Permanently Closed"),
|
||||
("UNDER_CONSTRUCTION", "Under Construction"),
|
||||
("DEMOLISHED", "Demolished"),
|
||||
("RELOCATED", "Relocated"),
|
||||
],
|
||||
default="OPERATING",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="ride",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("OPERATING", "Operating"),
|
||||
("SBNO", "Standing But Not Operating"),
|
||||
("CLOSING", "Closing"),
|
||||
("CLOSED_PERM", "Permanently Closed"),
|
||||
("UNDER_CONSTRUCTION", "Under Construction"),
|
||||
("DEMOLISHED", "Demolished"),
|
||||
("RELOCATED", "Relocated"),
|
||||
],
|
||||
default="OPERATING",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.1.3 on 2024-11-13 04:44
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("rides", "0008_historicalride_post_closing_status_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="historicalride",
|
||||
name="model_name",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="ride",
|
||||
name="model_name",
|
||||
),
|
||||
]
|
||||
155
rides/models.py
155
rides/models.py
@@ -1,29 +1,68 @@
|
||||
from typing import Tuple, Optional, Any
|
||||
from django.db import models
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.utils.text import slugify
|
||||
from simple_history.models import HistoricalRecords
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from history_tracking.models import HistoricalModel
|
||||
|
||||
|
||||
# Shared choices that will be used by multiple models
|
||||
CATEGORY_CHOICES = [
|
||||
('', 'Select ride type'),
|
||||
('RC', 'Roller Coaster'),
|
||||
('DR', 'Dark Ride'),
|
||||
('FR', 'Flat Ride'),
|
||||
('WR', 'Water Ride'),
|
||||
('TR', 'Transport'),
|
||||
('OT', 'Other'),
|
||||
]
|
||||
|
||||
|
||||
class RideModel(HistoricalModel):
|
||||
"""
|
||||
Represents a specific model/type of ride that can be manufactured by different companies.
|
||||
For example: B&M Dive Coaster, Vekoma Boomerang, etc.
|
||||
"""
|
||||
name = models.CharField(max_length=255)
|
||||
manufacturer = models.ForeignKey(
|
||||
'companies.Manufacturer',
|
||||
on_delete=models.SET_NULL, # Changed to SET_NULL since it's optional
|
||||
related_name='ride_models',
|
||||
null=True, # Made optional
|
||||
blank=True # Made optional
|
||||
)
|
||||
description = models.TextField(blank=True)
|
||||
category = models.CharField(
|
||||
max_length=2,
|
||||
choices=CATEGORY_CHOICES,
|
||||
default='',
|
||||
blank=True
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['manufacturer', 'name']
|
||||
unique_together = ['manufacturer', 'name']
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name if not self.manufacturer else f"{self.manufacturer.name} {self.name}"
|
||||
|
||||
|
||||
class Ride(HistoricalModel):
|
||||
CATEGORY_CHOICES = [
|
||||
('RC', 'Roller Coaster'),
|
||||
('DR', 'Dark Ride'),
|
||||
('FR', 'Flat Ride'),
|
||||
('WR', 'Water Ride'),
|
||||
('TR', 'Transport'),
|
||||
('OT', 'Other'),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('OPERATING', 'Operating'),
|
||||
('CLOSED_TEMP', 'Temporarily Closed'),
|
||||
('SBNO', 'Standing But Not Operating'),
|
||||
('CLOSING', 'Closing'),
|
||||
('CLOSED_PERM', 'Permanently Closed'),
|
||||
('UNDER_CONSTRUCTION', 'Under Construction'),
|
||||
('DEMOLISHED', 'Demolished'),
|
||||
('RELOCATED', 'Relocated'),
|
||||
]
|
||||
|
||||
POST_CLOSING_STATUS_CHOICES = [
|
||||
('SBNO', 'Standing But Not Operating'),
|
||||
('CLOSED_PERM', 'Permanently Closed'),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
slug = models.SlugField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
@@ -42,34 +81,47 @@ class Ride(HistoricalModel):
|
||||
category = models.CharField(
|
||||
max_length=2,
|
||||
choices=CATEGORY_CHOICES,
|
||||
default='OT'
|
||||
default='',
|
||||
blank=True
|
||||
)
|
||||
manufacturer = models.ForeignKey(
|
||||
'companies.manufacturer',
|
||||
'companies.Manufacturer',
|
||||
on_delete=models.CASCADE,
|
||||
null=False,
|
||||
blank=False
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
designer = models.ForeignKey(
|
||||
'designers.Designer',
|
||||
'companies.Designer',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='rides',
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
ride_model = models.ForeignKey(
|
||||
'RideModel',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='rides',
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text='The designer/engineering firm responsible for the ride'
|
||||
help_text="The specific model/type of this ride"
|
||||
)
|
||||
model_name = models.CharField(max_length=255, blank=True)
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=STATUS_CHOICES,
|
||||
default='OPERATING'
|
||||
)
|
||||
post_closing_status = models.CharField(
|
||||
max_length=20,
|
||||
choices=POST_CLOSING_STATUS_CHOICES,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Status to change to after closing date"
|
||||
)
|
||||
opening_date = models.DateField(null=True, blank=True)
|
||||
closing_date = models.DateField(null=True, blank=True)
|
||||
status_since = models.DateField(null=True, blank=True)
|
||||
min_height_in = models.PositiveIntegerField(null=True, blank=True)
|
||||
max_height_in = models.PositiveIntegerField(null=True, blank=True)
|
||||
accessibility_options = models.TextField(blank=True)
|
||||
capacity_per_hour = models.PositiveIntegerField(null=True, blank=True)
|
||||
ride_duration_seconds = models.PositiveIntegerField(null=True, blank=True)
|
||||
average_rating = models.DecimalField(
|
||||
@@ -90,64 +142,25 @@ class Ride(HistoricalModel):
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name} at {self.park.name}"
|
||||
|
||||
def save(self, *args: Any, **kwargs: Any) -> None:
|
||||
def save(self, *args, **kwargs) -> None:
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_by_slug(cls, slug: str) -> Tuple['Ride', bool]:
|
||||
"""Get ride by current or historical slug.
|
||||
|
||||
Args:
|
||||
slug: The slug to look up
|
||||
|
||||
Returns:
|
||||
A tuple of (Ride object, bool indicating if it's a historical slug)
|
||||
|
||||
Raises:
|
||||
cls.DoesNotExist: If no ride is found with the given slug
|
||||
"""
|
||||
try:
|
||||
return cls.objects.get(slug=slug), False
|
||||
except cls.DoesNotExist as e:
|
||||
# Check historical slugs
|
||||
if history := cls.history.filter(slug=slug).order_by('-history_date').first(): # type: ignore[attr-defined]
|
||||
try:
|
||||
return cls.objects.get(pk=history.instance.pk), True
|
||||
except cls.DoesNotExist as inner_e:
|
||||
raise cls.DoesNotExist("No ride found with this slug") from inner_e
|
||||
raise cls.DoesNotExist("No ride found with this slug") from e
|
||||
|
||||
class RollerCoasterStats(HistoricalModel):
|
||||
LAUNCH_CHOICES = [
|
||||
('CHAIN', 'Chain Lift'),
|
||||
('CABLE', 'Cable Launch'),
|
||||
('HYDRAULIC', 'Hydraulic Launch'),
|
||||
('LSM', 'Linear Synchronous Motor'),
|
||||
('LIM', 'Linear Induction Motor'),
|
||||
('GRAVITY', 'Gravity'),
|
||||
('OTHER', 'Other'),
|
||||
]
|
||||
|
||||
class RollerCoasterStats(models.Model):
|
||||
TRACK_MATERIAL_CHOICES = [
|
||||
('STEEL', 'Steel'),
|
||||
('WOOD', 'Wood'),
|
||||
('HYBRID', 'Hybrid'),
|
||||
('OTHER', 'Other'),
|
||||
]
|
||||
|
||||
COASTER_TYPE_CHOICES = [
|
||||
('SITDOWN', 'Sit-Down'),
|
||||
('SITDOWN', 'Sit Down'),
|
||||
('INVERTED', 'Inverted'),
|
||||
('FLYING', 'Flying'),
|
||||
('STANDUP', 'Stand-Up'),
|
||||
('STANDUP', 'Stand Up'),
|
||||
('WING', 'Wing'),
|
||||
('SUSPENDED', 'Suspended'),
|
||||
('BOBSLED', 'Bobsled'),
|
||||
('PIPELINE', 'Pipeline'),
|
||||
('MOTORBIKE', 'Motorbike'),
|
||||
('FLOORLESS', 'Floorless'),
|
||||
('DIVE', 'Dive'),
|
||||
('FAMILY', 'Family'),
|
||||
('WILD_MOUSE', 'Wild Mouse'),
|
||||
@@ -156,6 +169,14 @@ class RollerCoasterStats(HistoricalModel):
|
||||
('OTHER', 'Other'),
|
||||
]
|
||||
|
||||
LAUNCH_CHOICES = [
|
||||
('CHAIN', 'Chain Lift'),
|
||||
('LSM', 'LSM Launch'),
|
||||
('HYDRAULIC', 'Hydraulic Launch'),
|
||||
('GRAVITY', 'Gravity'),
|
||||
('OTHER', 'Other'),
|
||||
]
|
||||
|
||||
ride = models.OneToOneField(
|
||||
Ride,
|
||||
on_delete=models.CASCADE,
|
||||
@@ -192,15 +213,13 @@ class RollerCoasterStats(HistoricalModel):
|
||||
max_length=20,
|
||||
choices=COASTER_TYPE_CHOICES,
|
||||
default='SITDOWN',
|
||||
blank=True,
|
||||
help_text='The type/style of roller coaster (e.g. Sit-Down, Inverted, Flying)'
|
||||
blank=True
|
||||
)
|
||||
max_drop_height_ft = models.DecimalField(
|
||||
max_digits=6,
|
||||
decimal_places=2,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text='Maximum vertical drop height in feet'
|
||||
blank=True
|
||||
)
|
||||
launch_type = models.CharField(
|
||||
max_length=20,
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from django.db.models.signals import pre_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
from .models import Ride
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Ride)
|
||||
def handle_ride_status(sender, instance, **kwargs):
|
||||
"""Handle ride status changes based on closing date"""
|
||||
if instance.closing_date:
|
||||
today = timezone.now().date()
|
||||
|
||||
# If we've reached the closing date and status is "Closing"
|
||||
if today >= instance.closing_date and instance.status == 'CLOSING':
|
||||
# Change to the selected post-closing status
|
||||
instance.status = instance.post_closing_status or 'SBNO'
|
||||
instance.status_since = instance.closing_date
|
||||
|
||||
@@ -1,21 +1,62 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'rides' # Add namespace
|
||||
app_name = "rides"
|
||||
|
||||
urlpatterns = [
|
||||
# Global category URLs
|
||||
path('', views.RideListView.as_view(), name='ride_list'),
|
||||
path('all/', views.RideListView.as_view(), name='all_rides'),
|
||||
path('roller_coasters/', views.SingleCategoryListView.as_view(), {'category': 'RC'}, name='roller_coasters'),
|
||||
path('dark_rides/', views.SingleCategoryListView.as_view(), {'category': 'DR'}, name='dark_rides'),
|
||||
path('flat_rides/', views.SingleCategoryListView.as_view(), {'category': 'FR'}, name='flat_rides'),
|
||||
path('water_rides/', views.SingleCategoryListView.as_view(), {'category': 'WR'}, name='water_rides'),
|
||||
path('transports/', views.SingleCategoryListView.as_view(), {'category': 'TR'}, name='transports'),
|
||||
path('others/', views.SingleCategoryListView.as_view(), {'category': 'OT'}, name='others'),
|
||||
# List views
|
||||
path("", views.RideListView.as_view(), name="ride_list"),
|
||||
path("create/", views.RideCreateView.as_view(), name="ride_create"),
|
||||
|
||||
# Basic ride URLs
|
||||
path('create/', views.RideCreateView.as_view(), name='ride_create'),
|
||||
path('<slug:ride_slug>/edit/', views.RideUpdateView.as_view(), name='ride_edit'),
|
||||
path('<slug:ride_slug>/', views.RideDetailView.as_view(), name='ride_detail'),
|
||||
# Search endpoints
|
||||
path(
|
||||
"search/manufacturers/", views.search_manufacturers, name="search_manufacturers"
|
||||
),
|
||||
path("search/designers/", views.search_designers, name="search_designers"),
|
||||
path("search/models/", views.search_ride_models, name="search_ride_models"),
|
||||
|
||||
# HTMX endpoints
|
||||
path("coaster-fields/", views.show_coaster_fields, name="coaster_fields"),
|
||||
|
||||
# Category views for global listing
|
||||
path(
|
||||
"roller_coasters/",
|
||||
views.SingleCategoryListView.as_view(),
|
||||
{"category": "RC"},
|
||||
name="roller_coasters",
|
||||
),
|
||||
path(
|
||||
"dark_rides/",
|
||||
views.SingleCategoryListView.as_view(),
|
||||
{"category": "DR"},
|
||||
name="dark_rides",
|
||||
),
|
||||
path(
|
||||
"flat_rides/",
|
||||
views.SingleCategoryListView.as_view(),
|
||||
{"category": "FR"},
|
||||
name="flat_rides",
|
||||
),
|
||||
path(
|
||||
"water_rides/",
|
||||
views.SingleCategoryListView.as_view(),
|
||||
{"category": "WR"},
|
||||
name="water_rides",
|
||||
),
|
||||
path(
|
||||
"transports/",
|
||||
views.SingleCategoryListView.as_view(),
|
||||
{"category": "TR"},
|
||||
name="transports",
|
||||
),
|
||||
path(
|
||||
"others/",
|
||||
views.SingleCategoryListView.as_view(),
|
||||
{"category": "OT"},
|
||||
name="others",
|
||||
),
|
||||
|
||||
# Detail and update views - must come after category views
|
||||
path("<slug:ride_slug>/", views.RideDetailView.as_view(), name="ride_detail"),
|
||||
path("<slug:ride_slug>/update/", views.RideUpdateView.as_view(), name="ride_update"),
|
||||
]
|
||||
|
||||
736
rides/views.py
736
rides/views.py
@@ -1,10 +1,11 @@
|
||||
from typing import Any, Dict, Optional, Tuple, Union, cast
|
||||
from django.views.generic import DetailView, ListView, CreateView, UpdateView
|
||||
from django.shortcuts import get_object_or_404
|
||||
from typing import Any, Dict, Optional, Tuple, Union, cast, Type
|
||||
from django.views.generic import DetailView, ListView, CreateView, UpdateView, RedirectView
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
from django.db.models import Q, Model
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib import messages
|
||||
from django.http import (
|
||||
@@ -17,7 +18,11 @@ from django.http import (
|
||||
from django.db.models import Count
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.forms import ModelForm
|
||||
from .models import Ride, RollerCoasterStats
|
||||
from django.db.models.query import QuerySet
|
||||
from simple_history.models import HistoricalRecords
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from .models import Ride, RollerCoasterStats, RideModel, CATEGORY_CHOICES
|
||||
from .forms import RideForm
|
||||
from parks.models import Park
|
||||
from core.views import SlugRedirectMixin
|
||||
@@ -25,481 +30,328 @@ from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, History
|
||||
from moderation.models import EditSubmission
|
||||
from media.models import Photo
|
||||
from accounts.models import User
|
||||
from companies.models import Manufacturer, Designer
|
||||
|
||||
|
||||
def is_privileged_user(user: Any) -> bool:
|
||||
"""Check if the user has privileged access.
|
||||
|
||||
Args:
|
||||
user: The user to check
|
||||
|
||||
Returns:
|
||||
bool: True if user has privileged or higher privileges
|
||||
"""
|
||||
return isinstance(user, User) and user.role in ["MODERATOR", "ADMIN", "SUPERUSER"]
|
||||
|
||||
|
||||
def handle_photo_uploads(request: HttpRequest, ride: Ride) -> int:
|
||||
"""Handle photo uploads for a ride.
|
||||
|
||||
Args:
|
||||
request: The HTTP request containing files
|
||||
ride: The ride to attach photos to
|
||||
|
||||
Returns:
|
||||
int: Number of successfully uploaded photos
|
||||
"""
|
||||
uploaded_count = 0
|
||||
photos = request.FILES.getlist("photos")
|
||||
for photo_file in photos:
|
||||
try:
|
||||
Photo.objects.create(
|
||||
image=photo_file,
|
||||
uploaded_by=request.user,
|
||||
content_type=ContentType.objects.get_for_model(Ride),
|
||||
object_id=ride.pk,
|
||||
)
|
||||
uploaded_count += 1
|
||||
except Exception as e:
|
||||
messages.error(request, f"Error uploading photo {photo_file.name}: {str(e)}")
|
||||
return uploaded_count
|
||||
|
||||
|
||||
def prepare_form_data(cleaned_data: Dict[str, Any], park: Park) -> Dict[str, Any]:
|
||||
"""Prepare form data for submission.
|
||||
|
||||
Args:
|
||||
cleaned_data: The form's cleaned data
|
||||
park: The park instance
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Processed form data ready for submission
|
||||
"""
|
||||
data = cleaned_data.copy()
|
||||
data["park"] = park.pk
|
||||
if data.get("park_area"):
|
||||
data["park_area"] = data["park_area"].pk
|
||||
if data.get("manufacturer"):
|
||||
data["manufacturer"] = data["manufacturer"].pk
|
||||
return data
|
||||
|
||||
|
||||
def handle_form_errors(request: HttpRequest, form: ModelForm) -> None:
|
||||
"""Handle form validation errors by adding appropriate error messages.
|
||||
|
||||
Args:
|
||||
request: The HTTP request
|
||||
form: The form containing validation errors
|
||||
"""
|
||||
messages.error(
|
||||
request,
|
||||
"Please correct the errors below. Required fields are marked with an asterisk (*).",
|
||||
)
|
||||
for field, errors in form.errors.items():
|
||||
for error in errors:
|
||||
messages.error(request, f"{field}: {error}")
|
||||
|
||||
|
||||
def create_edit_submission(
|
||||
request: HttpRequest,
|
||||
submission_type: str,
|
||||
changes: Dict[str, Any],
|
||||
object_id: Optional[int] = None,
|
||||
) -> EditSubmission:
|
||||
"""Create an EditSubmission object for ride changes.
|
||||
|
||||
Args:
|
||||
request: The HTTP request
|
||||
submission_type: Type of submission (CREATE or EDIT)
|
||||
changes: The changes to be submitted
|
||||
object_id: Optional ID of the existing object for edits
|
||||
|
||||
Returns:
|
||||
EditSubmission: The created submission object
|
||||
"""
|
||||
submission_data = {
|
||||
"user": request.user,
|
||||
"content_type": ContentType.objects.get_for_model(Ride),
|
||||
"submission_type": submission_type,
|
||||
"changes": changes,
|
||||
"reason": request.POST.get("reason", ""),
|
||||
"source": request.POST.get("source", ""),
|
||||
}
|
||||
|
||||
if object_id is not None:
|
||||
submission_data["object_id"] = object_id
|
||||
|
||||
return EditSubmission.objects.create(**submission_data)
|
||||
|
||||
|
||||
def handle_privileged_save(
|
||||
request: HttpRequest, form: RideForm, submission: EditSubmission
|
||||
) -> Tuple[bool, str]:
|
||||
"""Handle saving form and updating submission for privileged users.
|
||||
|
||||
Args:
|
||||
request: The HTTP request
|
||||
form: The form to save
|
||||
submission: The edit submission to update
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: Success status and error message (empty string if successful)
|
||||
"""
|
||||
try:
|
||||
ride = form.save()
|
||||
if submission.submission_type == "CREATE":
|
||||
submission.object_id = ride.pk
|
||||
submission.status = "APPROVED"
|
||||
submission.handled_by = request.user
|
||||
submission.save()
|
||||
return True, ""
|
||||
except Exception as e:
|
||||
error_msg = (
|
||||
f"Error {submission.submission_type.lower()}ing ride: {str(e)}. "
|
||||
"Please check your input and try again."
|
||||
)
|
||||
return False, error_msg
|
||||
|
||||
|
||||
class SingleCategoryListView(ListView):
|
||||
model = Ride
|
||||
template_name = "rides/ride_category_list.html"
|
||||
context_object_name = "categories"
|
||||
|
||||
def get_category_code(self) -> str:
|
||||
if category := self.kwargs.get("category"):
|
||||
return category
|
||||
raise Http404("Category not found")
|
||||
|
||||
def get_queryset(self):
|
||||
category_code = self.get_category_code()
|
||||
category_name = dict(Ride.CATEGORY_CHOICES)[category_code]
|
||||
|
||||
rides = (
|
||||
Ride.objects.filter(category=category_code)
|
||||
.select_related("park", "manufacturer")
|
||||
.order_by("name")
|
||||
)
|
||||
|
||||
return {category_name: rides} if rides.exists() else {}
|
||||
|
||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
category_code = self.get_category_code()
|
||||
category_name = dict(Ride.CATEGORY_CHOICES)[category_code]
|
||||
context["title"] = f"All {category_name}s"
|
||||
context["category_code"] = category_code
|
||||
return context
|
||||
|
||||
|
||||
class ParkSingleCategoryListView(ListView):
|
||||
model = Ride
|
||||
template_name = "rides/ride_category_list.html"
|
||||
context_object_name = "categories"
|
||||
|
||||
def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None:
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.park = get_object_or_404(Park, slug=self.kwargs["park_slug"])
|
||||
|
||||
def get_category_code(self) -> str:
|
||||
if category := self.kwargs.get("category"):
|
||||
return category
|
||||
raise Http404("Category not found")
|
||||
|
||||
def get_queryset(self):
|
||||
category_code = self.get_category_code()
|
||||
category_name = dict(Ride.CATEGORY_CHOICES)[category_code]
|
||||
|
||||
rides = (
|
||||
Ride.objects.filter(park=self.park, category=category_code)
|
||||
.select_related("manufacturer")
|
||||
.order_by("name")
|
||||
)
|
||||
|
||||
return {category_name: rides} if rides.exists() else {}
|
||||
|
||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["park"] = self.park
|
||||
category_code = self.get_category_code()
|
||||
category_name = dict(Ride.CATEGORY_CHOICES)[category_code]
|
||||
context["title"] = f"{category_name}s at {self.park.name}"
|
||||
context["category_code"] = category_code
|
||||
return context
|
||||
def show_coaster_fields(request: HttpRequest) -> HttpResponse:
|
||||
"""Show roller coaster specific fields based on category selection"""
|
||||
category = request.GET.get('category')
|
||||
if category != 'RC': # Only show for roller coasters
|
||||
return HttpResponse('')
|
||||
return render(request, "rides/partials/coaster_fields.html")
|
||||
|
||||
|
||||
class RideCreateView(LoginRequiredMixin, CreateView):
|
||||
"""View for creating a new ride"""
|
||||
model = Ride
|
||||
form_class = RideForm
|
||||
template_name = "rides/ride_form.html"
|
||||
template_name = 'rides/ride_form.html'
|
||||
|
||||
def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None:
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.park = get_object_or_404(Park, slug=self.kwargs["park_slug"])
|
||||
def get_success_url(self):
|
||||
"""Get URL to redirect to after successful creation"""
|
||||
if hasattr(self, 'park'):
|
||||
return reverse('parks:rides:ride_detail', kwargs={
|
||||
'park_slug': self.park.slug,
|
||||
'ride_slug': self.object.slug
|
||||
})
|
||||
return reverse('rides:ride_detail', kwargs={'ride_slug': self.object.slug})
|
||||
|
||||
def get_form_kwargs(self) -> Dict[str, Any]:
|
||||
def get_form_kwargs(self):
|
||||
"""Pass park to the form"""
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["park"] = self.park
|
||||
if 'park_slug' in self.kwargs:
|
||||
self.park = get_object_or_404(Park, slug=self.kwargs['park_slug'])
|
||||
kwargs['park'] = self.park
|
||||
return kwargs
|
||||
|
||||
def handle_submission(
|
||||
self, form: RideForm, cleaned_data: Dict[str, Any]
|
||||
) -> HttpResponseRedirect:
|
||||
"""Handle the form submission.
|
||||
|
||||
Args:
|
||||
form: The form to process
|
||||
cleaned_data: The cleaned form data
|
||||
|
||||
Returns:
|
||||
HttpResponseRedirect to appropriate URL
|
||||
"""
|
||||
submission = create_edit_submission(self.request, "CREATE", cleaned_data)
|
||||
|
||||
if is_privileged_user(self.request.user):
|
||||
success, error_msg = handle_privileged_save(self.request, form, submission)
|
||||
if success:
|
||||
self.object = form.instance
|
||||
uploaded_count = handle_photo_uploads(self.request, self.object)
|
||||
messages.success(
|
||||
self.request,
|
||||
f"Successfully created {self.object.name} at {self.park.name}. "
|
||||
f"Added {uploaded_count} photo(s).",
|
||||
)
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
else:
|
||||
if error_msg: # Only add error message if there is one
|
||||
messages.error(self.request, error_msg)
|
||||
return cast(HttpResponseRedirect, self.form_invalid(form))
|
||||
|
||||
messages.success(
|
||||
self.request,
|
||||
"Your ride submission has been sent for review. "
|
||||
"You will be notified when it is approved.",
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
reverse("parks:rides:ride_list", kwargs={"park_slug": self.park.slug})
|
||||
)
|
||||
|
||||
def form_valid(self, form: RideForm) -> HttpResponseRedirect:
|
||||
form.instance.park = self.park
|
||||
cleaned_data = prepare_form_data(form.cleaned_data, self.park)
|
||||
return self.handle_submission(form, cleaned_data)
|
||||
|
||||
def form_invalid(self, form: RideForm) -> Union[HttpResponse, HttpResponseRedirect]:
|
||||
"""Handle invalid form submission.
|
||||
|
||||
Args:
|
||||
form: The invalid form
|
||||
|
||||
Returns:
|
||||
Response with error messages
|
||||
"""
|
||||
handle_form_errors(self.request, form)
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse(
|
||||
"parks:rides:ride_detail",
|
||||
kwargs={"park_slug": self.park.slug, "ride_slug": self.object.slug},
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add park and park_slug to context"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["park"] = self.park
|
||||
if hasattr(self, 'park'):
|
||||
context['park'] = self.park
|
||||
context['park_slug'] = self.park.slug
|
||||
context['is_edit'] = False
|
||||
return context
|
||||
|
||||
|
||||
class RideUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = Ride
|
||||
form_class = RideForm
|
||||
template_name = "rides/ride_form.html"
|
||||
slug_url_kwarg = "ride_slug"
|
||||
|
||||
def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None:
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.park = get_object_or_404(Park, slug=self.kwargs["park_slug"])
|
||||
|
||||
def get_form_kwargs(self) -> Dict[str, Any]:
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["park"] = self.park
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["park"] = self.park
|
||||
context["is_edit"] = True
|
||||
return context
|
||||
|
||||
def handle_submission(
|
||||
self, form: RideForm, cleaned_data: Dict[str, Any]
|
||||
) -> HttpResponseRedirect:
|
||||
"""Handle the form submission.
|
||||
|
||||
Args:
|
||||
form: The form to process
|
||||
cleaned_data: The cleaned form data
|
||||
|
||||
Returns:
|
||||
HttpResponseRedirect to appropriate URL
|
||||
"""
|
||||
submission = create_edit_submission(
|
||||
self.request, "EDIT", cleaned_data, self.object.pk
|
||||
)
|
||||
|
||||
if is_privileged_user(self.request.user):
|
||||
success, error_msg = handle_privileged_save(self.request, form, submission)
|
||||
if success:
|
||||
self.object = form.instance
|
||||
uploaded_count = handle_photo_uploads(self.request, self.object)
|
||||
messages.success(
|
||||
self.request,
|
||||
f"Successfully updated {self.object.name}. "
|
||||
f"Added {uploaded_count} new photo(s).",
|
||||
)
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
else:
|
||||
if error_msg: # Only add error message if there is one
|
||||
messages.error(self.request, error_msg)
|
||||
return cast(HttpResponseRedirect, self.form_invalid(form))
|
||||
|
||||
messages.success(
|
||||
self.request,
|
||||
f"Your changes to {self.object.name} have been sent for review. "
|
||||
"You will be notified when they are approved.",
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
reverse(
|
||||
"parks:rides:ride_detail",
|
||||
kwargs={"park_slug": self.park.slug, "ride_slug": self.object.slug},
|
||||
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'):
|
||||
# Create submission for new manufacturer
|
||||
EditSubmission.objects.create(
|
||||
user=self.request.user,
|
||||
content_type=ContentType.objects.get_for_model(Manufacturer),
|
||||
submission_type="CREATE",
|
||||
changes={"name": manufacturer_name},
|
||||
)
|
||||
)
|
||||
|
||||
def form_valid(self, form: RideForm) -> HttpResponseRedirect:
|
||||
cleaned_data = prepare_form_data(form.cleaned_data, self.park)
|
||||
return self.handle_submission(form, cleaned_data)
|
||||
# Check for new designer
|
||||
designer_name = form.cleaned_data.get('designer_search')
|
||||
if designer_name and not form.cleaned_data.get('designer'):
|
||||
# Create submission for new designer
|
||||
EditSubmission.objects.create(
|
||||
user=self.request.user,
|
||||
content_type=ContentType.objects.get_for_model(Designer),
|
||||
submission_type="CREATE",
|
||||
changes={"name": designer_name},
|
||||
)
|
||||
|
||||
def form_invalid(self, form: RideForm) -> Union[HttpResponse, HttpResponseRedirect]:
|
||||
"""Handle invalid form submission.
|
||||
# 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:
|
||||
# Create submission for new ride model
|
||||
EditSubmission.objects.create(
|
||||
user=self.request.user,
|
||||
content_type=ContentType.objects.get_for_model(RideModel),
|
||||
submission_type="CREATE",
|
||||
changes={
|
||||
"name": ride_model_name,
|
||||
"manufacturer": manufacturer.id
|
||||
},
|
||||
)
|
||||
|
||||
Args:
|
||||
form: The invalid form
|
||||
|
||||
Returns:
|
||||
Response with error messages
|
||||
"""
|
||||
handle_form_errors(self.request, form)
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse(
|
||||
"parks:rides:ride_detail",
|
||||
kwargs={"park_slug": self.park.slug, "ride_slug": self.object.slug},
|
||||
)
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class RideDetailView(
|
||||
SlugRedirectMixin,
|
||||
EditSubmissionMixin,
|
||||
PhotoSubmissionMixin,
|
||||
HistoryMixin,
|
||||
DetailView,
|
||||
):
|
||||
class RideDetailView(DetailView):
|
||||
"""View for displaying ride details"""
|
||||
model = Ride
|
||||
template_name = "rides/ride_detail.html"
|
||||
context_object_name = "ride"
|
||||
slug_url_kwarg = "ride_slug"
|
||||
template_name = 'rides/ride_detail.html'
|
||||
slug_url_kwarg = 'ride_slug'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
if queryset is None:
|
||||
queryset = self.get_queryset()
|
||||
park_slug = self.kwargs.get("park_slug")
|
||||
ride_slug = self.kwargs.get("ride_slug")
|
||||
obj, is_old_slug = self.model.get_by_slug(ride_slug) # type: ignore[attr-defined]
|
||||
if obj.park.slug != park_slug:
|
||||
raise self.model.DoesNotExist("Park slug doesn't match")
|
||||
return obj
|
||||
def get_queryset(self):
|
||||
"""Get ride for the specific park if park_slug is provided"""
|
||||
queryset = Ride.objects.all().select_related(
|
||||
'park',
|
||||
'ride_model',
|
||||
'ride_model__manufacturer'
|
||||
).prefetch_related('photos')
|
||||
|
||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||
if 'park_slug' in self.kwargs:
|
||||
queryset = queryset.filter(park__slug=self.kwargs['park_slug'])
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add park_slug to context if it exists"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.object.category == "RC":
|
||||
context["coaster_stats"] = RollerCoasterStats.objects.filter(
|
||||
ride=self.object
|
||||
).first()
|
||||
if 'park_slug' in self.kwargs:
|
||||
context['park_slug'] = self.kwargs['park_slug']
|
||||
return context
|
||||
|
||||
def get_redirect_url_pattern(self) -> str:
|
||||
return "parks:rides:ride_detail"
|
||||
|
||||
def get_redirect_url_kwargs(self) -> Dict[str, Any]:
|
||||
return {"park_slug": self.object.park.slug, "ride_slug": self.object.slug}
|
||||
class RideUpdateView(LoginRequiredMixin, EditSubmissionMixin, UpdateView):
|
||||
"""View for updating an existing ride"""
|
||||
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"""
|
||||
if hasattr(self, 'park'):
|
||||
return reverse('parks:rides:ride_detail', kwargs={
|
||||
'park_slug': self.park.slug,
|
||||
'ride_slug': self.object.slug
|
||||
})
|
||||
return reverse('rides:ride_detail', kwargs={'ride_slug': self.object.slug})
|
||||
|
||||
def get_queryset(self):
|
||||
"""Get ride for the specific park if park_slug is provided"""
|
||||
queryset = Ride.objects.all()
|
||||
if 'park_slug' in self.kwargs:
|
||||
queryset = queryset.filter(park__slug=self.kwargs['park_slug'])
|
||||
return queryset
|
||||
|
||||
def get_form_kwargs(self):
|
||||
"""Pass park to the form"""
|
||||
kwargs = super().get_form_kwargs()
|
||||
# For park-specific URLs, use the park from the URL
|
||||
if 'park_slug' in self.kwargs:
|
||||
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
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add park and park_slug to context"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
if hasattr(self, 'park'):
|
||||
context['park'] = self.park
|
||||
context['park_slug'] = self.park.slug
|
||||
context['is_edit'] = True
|
||||
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'):
|
||||
# Create submission for new manufacturer
|
||||
EditSubmission.objects.create(
|
||||
user=self.request.user,
|
||||
content_type=ContentType.objects.get_for_model(Manufacturer),
|
||||
submission_type="CREATE",
|
||||
changes={"name": manufacturer_name},
|
||||
)
|
||||
|
||||
# Check for new designer
|
||||
designer_name = form.cleaned_data.get('designer_search')
|
||||
if designer_name and not form.cleaned_data.get('designer'):
|
||||
# Create submission for new designer
|
||||
EditSubmission.objects.create(
|
||||
user=self.request.user,
|
||||
content_type=ContentType.objects.get_for_model(Designer),
|
||||
submission_type="CREATE",
|
||||
changes={"name": 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:
|
||||
# Create submission for new ride model
|
||||
EditSubmission.objects.create(
|
||||
user=self.request.user,
|
||||
content_type=ContentType.objects.get_for_model(RideModel),
|
||||
submission_type="CREATE",
|
||||
changes={
|
||||
"name": ride_model_name,
|
||||
"manufacturer": manufacturer.id
|
||||
},
|
||||
)
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class RideListView(ListView):
|
||||
"""View for displaying a list of rides"""
|
||||
model = Ride
|
||||
template_name = "rides/ride_list.html"
|
||||
context_object_name = "rides"
|
||||
|
||||
def setup(self, request: HttpRequest, *args: Any, **kwargs: Any) -> None:
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.park = None
|
||||
if "park_slug" in self.kwargs:
|
||||
self.park = get_object_or_404(Park, slug=self.kwargs["park_slug"])
|
||||
template_name = 'rides/ride_list.html'
|
||||
context_object_name = 'rides'
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Ride.objects.select_related(
|
||||
"park", "coaster_stats", "manufacturer"
|
||||
).prefetch_related("photos")
|
||||
"""Get all rides or filter by park if park_slug is provided"""
|
||||
queryset = Ride.objects.all().select_related(
|
||||
'park',
|
||||
'ride_model',
|
||||
'ride_model__manufacturer'
|
||||
).prefetch_related('photos')
|
||||
|
||||
if self.park:
|
||||
if 'park_slug' in self.kwargs:
|
||||
self.park = get_object_or_404(Park, slug=self.kwargs['park_slug'])
|
||||
queryset = queryset.filter(park=self.park)
|
||||
|
||||
search = self.request.GET.get("search", "").strip() or None
|
||||
category = self.request.GET.get("category", "").strip() or None
|
||||
status = self.request.GET.get("status", "").strip() or None
|
||||
manufacturer = self.request.GET.get("manufacturer", "").strip() or None
|
||||
|
||||
if search:
|
||||
if self.park:
|
||||
queryset = queryset.filter(name__icontains=search)
|
||||
else:
|
||||
queryset = queryset.filter(
|
||||
Q(name__icontains=search) | Q(park__name__icontains=search)
|
||||
)
|
||||
if category:
|
||||
queryset = queryset.filter(category=category)
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
if manufacturer:
|
||||
queryset = queryset.exclude(manufacturer__isnull=True)
|
||||
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs) -> Dict[str, Any]:
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add park to context if park_slug is provided"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["park"] = self.park
|
||||
|
||||
manufacturer_query = Ride.objects
|
||||
if self.park:
|
||||
manufacturer_query = manufacturer_query.filter(park=self.park)
|
||||
|
||||
context["manufacturers"] = list(
|
||||
manufacturer_query.exclude(manufacturer__isnull=True)
|
||||
.values_list("manufacturer__name", flat=True)
|
||||
.distinct()
|
||||
.order_by("manufacturer__name")
|
||||
)
|
||||
|
||||
context["current_filters"] = {
|
||||
"search": self.request.GET.get("search", ""),
|
||||
"category": self.request.GET.get("category", ""),
|
||||
"status": self.request.GET.get("status", ""),
|
||||
"manufacturer": self.request.GET.get("manufacturer", ""),
|
||||
}
|
||||
|
||||
if hasattr(self, 'park'):
|
||||
context['park'] = self.park
|
||||
context['park_slug'] = self.kwargs['park_slug']
|
||||
return context
|
||||
|
||||
def get(self, request: HttpRequest, *args: Any, **kwargs: Any):
|
||||
if getattr(request, "htmx", False): # type: ignore[attr-defined]
|
||||
self.template_name = "rides/partials/ride_list.html"
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
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']
|
||||
context['category'] = dict(CATEGORY_CHOICES).get(self.kwargs['category'])
|
||||
return context
|
||||
|
||||
|
||||
# Alias for parks app to maintain backward compatibility
|
||||
ParkSingleCategoryListView = SingleCategoryListView
|
||||
|
||||
|
||||
def is_privileged_user(user: Any) -> bool:
|
||||
"""Check if user has privileged access"""
|
||||
return bool(user and hasattr(user, 'is_staff') and (user.is_staff or user.is_superuser))
|
||||
|
||||
|
||||
@login_required
|
||||
def search_manufacturers(request: HttpRequest) -> HttpResponse:
|
||||
"""Search manufacturers and return results for HTMX"""
|
||||
query = request.GET.get("q", "").strip()
|
||||
|
||||
# Show all manufacturers on click, filter on input
|
||||
manufacturers = Manufacturer.objects.all().order_by("name")
|
||||
if query:
|
||||
manufacturers = manufacturers.filter(name__icontains=query)
|
||||
manufacturers = manufacturers[:10]
|
||||
|
||||
return render(
|
||||
request,
|
||||
"rides/partials/manufacturer_search_results.html",
|
||||
{"manufacturers": manufacturers, "search_term": query},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
def search_designers(request: HttpRequest) -> HttpResponse:
|
||||
"""Search designers and return results for HTMX"""
|
||||
query = request.GET.get("q", "").strip()
|
||||
|
||||
# Show all designers on click, filter on input
|
||||
designers = Designer.objects.all().order_by("name")
|
||||
if query:
|
||||
designers = designers.filter(name__icontains=query)
|
||||
designers = designers[:10]
|
||||
|
||||
return render(
|
||||
request,
|
||||
"rides/partials/designer_search_results.html",
|
||||
{"designers": designers, "search_term": query},
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
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},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user