diff --git a/companies/__pycache__/views.cpython-312.pyc b/companies/__pycache__/views.cpython-312.pyc index cd843db9..9be12c9a 100644 Binary files a/companies/__pycache__/views.cpython-312.pyc and b/companies/__pycache__/views.cpython-312.pyc differ diff --git a/companies/views.py b/companies/views.py index db27238a..6d6d1e5e 100644 --- a/companies/views.py +++ b/companies/views.py @@ -5,6 +5,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType from django.contrib import messages from django.http import HttpResponseRedirect +from django.db.models import Count, Sum from .models import Company, Manufacturer from .forms import CompanyForm, ManufacturerForm from rides.models import Ride @@ -173,9 +174,13 @@ class CompanyDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmissionM def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['parks'] = Park.objects.filter( + parks = Park.objects.filter( owner=self.object ).select_related('owner') + + context['parks'] = parks + context['total_rides'] = Ride.objects.filter(park__in=parks).count() + return context def get_redirect_url_pattern(self): @@ -195,9 +200,14 @@ class ManufacturerDetailView(SlugRedirectMixin, EditSubmissionMixin, PhotoSubmis def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['rides'] = Ride.objects.filter( + rides = Ride.objects.filter( manufacturer=self.object ).select_related('park', 'coaster_stats') + + context['rides'] = rides + context['coaster_count'] = rides.filter(category='ROLLER_COASTER').count() + context['parks_count'] = rides.values('park').distinct().count() + return context def get_redirect_url_pattern(self): diff --git a/designers/__init__.py b/designers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/designers/admin.py b/designers/admin.py new file mode 100644 index 00000000..26f980e0 --- /dev/null +++ b/designers/admin.py @@ -0,0 +1,10 @@ +from django.contrib import admin +from simple_history.admin import SimpleHistoryAdmin +from .models import Designer + +@admin.register(Designer) +class DesignerAdmin(SimpleHistoryAdmin): + list_display = ('name', 'headquarters', 'founded_date', 'website') + search_fields = ('name', 'headquarters') + list_filter = ('founded_date',) + prepopulated_fields = {'slug': ('name',)} diff --git a/designers/apps.py b/designers/apps.py new file mode 100644 index 00000000..bffb09e3 --- /dev/null +++ b/designers/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DesignersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "designers" diff --git a/designers/migrations/0001_initial.py b/designers/migrations/0001_initial.py new file mode 100644 index 00000000..afb8916f --- /dev/null +++ b/designers/migrations/0001_initial.py @@ -0,0 +1,88 @@ +# Generated by Django 5.1.2 on 2024-11-04 00:28 + +import django.db.models.deletion +import simple_history.models +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Designer", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(max_length=255, unique=True)), + ("description", models.TextField(blank=True)), + ("website", models.URLField(blank=True)), + ("founded_date", models.DateField(blank=True, null=True)), + ("headquarters", models.CharField(blank=True, max_length=255)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ], + options={ + "ordering": ["name"], + }, + ), + migrations.CreateModel( + name="HistoricalDesigner", + fields=[ + ( + "id", + models.BigIntegerField( + auto_created=True, blank=True, db_index=True, verbose_name="ID" + ), + ), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(max_length=255)), + ("description", models.TextField(blank=True)), + ("website", models.URLField(blank=True)), + ("founded_date", models.DateField(blank=True, null=True)), + ("headquarters", models.CharField(blank=True, max_length=255)), + ("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, + ), + ), + ], + options={ + "verbose_name": "historical designer", + "verbose_name_plural": "historical designers", + "ordering": ("-history_date", "-history_id"), + "get_latest_by": ("history_date", "history_id"), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + ] diff --git a/designers/migrations/__init__.py b/designers/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/designers/models.py b/designers/models.py new file mode 100644 index 00000000..912503c4 --- /dev/null +++ b/designers/models.py @@ -0,0 +1,37 @@ +from django.db import models +from django.utils.text import slugify +from simple_history.models import HistoricalRecords + +class Designer(models.Model): + name = models.CharField(max_length=255) + slug = models.SlugField(max_length=255, unique=True) + description = models.TextField(blank=True) + website = models.URLField(blank=True) + founded_date = models.DateField(null=True, blank=True) + headquarters = models.CharField(max_length=255, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + history = HistoricalRecords() + + class Meta: + ordering = ['name'] + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + + @classmethod + def get_by_slug(cls, slug): + """Get designer by current or historical slug""" + try: + return cls.objects.get(slug=slug), False + except cls.DoesNotExist: + # Check historical slugs + history = cls.history.filter(slug=slug).order_by('-history_date').first() + if history: + return cls.objects.get(id=history.id), True + raise cls.DoesNotExist("No designer found with this slug") diff --git a/designers/tests.py b/designers/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/designers/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/designers/urls.py b/designers/urls.py new file mode 100644 index 00000000..c71f8420 --- /dev/null +++ b/designers/urls.py @@ -0,0 +1,8 @@ +from django.urls import path +from . import views + +app_name = 'designers' + +urlpatterns = [ + path('/', views.DesignerDetailView.as_view(), name='designer_detail'), +] diff --git a/designers/views.py b/designers/views.py new file mode 100644 index 00000000..d0b133b5 --- /dev/null +++ b/designers/views.py @@ -0,0 +1,29 @@ +from django.views.generic import DetailView +from .models import Designer +from django.db.models import Count + +class DesignerDetailView(DetailView): + model = Designer + template_name = 'designers/designer_detail.html' + context_object_name = 'designer' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # Get all rides by this designer + context['rides'] = self.object.rides.select_related( + 'park', + 'manufacturer', + 'coaster_stats' + ).order_by('-opening_date') + + # Get stats + context['stats'] = { + 'total_rides': self.object.rides.count(), + 'total_parks': self.object.rides.values('park').distinct().count(), + 'total_coasters': self.object.rides.filter(category='RC').count(), + 'total_countries': self.object.rides.values( + 'park__location__country' + ).distinct().count(), + } + + return context diff --git a/history_tracking/migrations/0002_remove_historicalpark_history_user_delete_park_and_more.py b/history_tracking/migrations/0002_remove_historicalpark_history_user_delete_park_and_more.py new file mode 100644 index 00000000..a5941190 --- /dev/null +++ b/history_tracking/migrations/0002_remove_historicalpark_history_user_delete_park_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.2 on 2024-11-04 00:17 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("history_tracking", "0001_initial"), + ] + + operations = [ + migrations.RemoveField( + model_name="historicalpark", + name="history_user", + ), + migrations.DeleteModel( + name="Park", + ), + migrations.DeleteModel( + name="HistoricalPark", + ), + ] diff --git a/parks/__pycache__/urls.cpython-312.pyc b/parks/__pycache__/urls.cpython-312.pyc index f4c37adf..b285ca98 100644 Binary files a/parks/__pycache__/urls.cpython-312.pyc and b/parks/__pycache__/urls.cpython-312.pyc differ diff --git a/parks/urls.py b/parks/urls.py index 3f86783c..b0532682 100644 --- a/parks/urls.py +++ b/parks/urls.py @@ -1,5 +1,6 @@ from django.urls import path, include from . import views +from rides.views import ParkSingleCategoryListView app_name = "parks" @@ -17,6 +18,14 @@ urlpatterns = [ # Area views path("/areas//", views.ParkAreaDetailView.as_view(), name="area_detail"), + # Park-specific category URLs + path("/roller_coasters/", ParkSingleCategoryListView.as_view(), {'category': 'RC'}, name="park_roller_coasters"), + path("/dark_rides/", ParkSingleCategoryListView.as_view(), {'category': 'DR'}, name="park_dark_rides"), + path("/flat_rides/", ParkSingleCategoryListView.as_view(), {'category': 'FR'}, name="park_flat_rides"), + path("/water_rides/", ParkSingleCategoryListView.as_view(), {'category': 'WR'}, name="park_water_rides"), + path("/transports/", ParkSingleCategoryListView.as_view(), {'category': 'TR'}, name="park_transports"), + path("/others/", ParkSingleCategoryListView.as_view(), {'category': 'OT'}, name="park_others"), + # Include rides URLs path("/rides/", include("rides.urls", namespace="rides")), ] diff --git a/rides/__pycache__/models.cpython-312.pyc b/rides/__pycache__/models.cpython-312.pyc index fa2c2d12..f5cd228a 100644 Binary files a/rides/__pycache__/models.cpython-312.pyc and b/rides/__pycache__/models.cpython-312.pyc differ diff --git a/rides/__pycache__/urls.cpython-312.pyc b/rides/__pycache__/urls.cpython-312.pyc index 967d0df5..bd07a329 100644 Binary files a/rides/__pycache__/urls.cpython-312.pyc and b/rides/__pycache__/urls.cpython-312.pyc differ diff --git a/rides/__pycache__/views.cpython-312.pyc b/rides/__pycache__/views.cpython-312.pyc index fe42bfbf..73623e5c 100644 Binary files a/rides/__pycache__/views.cpython-312.pyc and b/rides/__pycache__/views.cpython-312.pyc differ diff --git a/rides/forms.py b/rides/forms.py index 94a27918..4d7d177f 100644 --- a/rides/forms.py +++ b/rides/forms.py @@ -4,7 +4,7 @@ from .models import Ride class RideForm(forms.ModelForm): class Meta: model = Ride - fields = ['name', 'park_area', 'category', 'manufacturer', 'model_name', 'status', + 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'] widgets = { @@ -20,6 +20,9 @@ class RideForm(forms.ModelForm): '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' }), diff --git a/rides/migrations/0003_historicalrollercoasterstats_max_drop_height_ft_and_more.py b/rides/migrations/0003_historicalrollercoasterstats_max_drop_height_ft_and_more.py new file mode 100644 index 00000000..4994ce94 --- /dev/null +++ b/rides/migrations/0003_historicalrollercoasterstats_max_drop_height_ft_and_more.py @@ -0,0 +1,65 @@ +# 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, + ), + ), + ] diff --git a/rides/migrations/0004_historicalrollercoasterstats_roller_coaster_type_and_more.py b/rides/migrations/0004_historicalrollercoasterstats_roller_coaster_type_and_more.py new file mode 100644 index 00000000..11003d24 --- /dev/null +++ b/rides/migrations/0004_historicalrollercoasterstats_roller_coaster_type_and_more.py @@ -0,0 +1,69 @@ +# 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, + ), + ), + ] diff --git a/rides/migrations/0005_historicalride_designer_ride_designer.py b/rides/migrations/0005_historicalride_designer_ride_designer.py new file mode 100644 index 00000000..4255cf13 --- /dev/null +++ b/rides/migrations/0005_historicalride_designer_ride_designer.py @@ -0,0 +1,40 @@ +# Generated by Django 5.1.2 on 2024-11-04 00:28 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("designers", "0001_initial"), + ("rides", "0004_historicalrollercoasterstats_roller_coaster_type_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="historicalride", + name="designer", + field=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", + ), + ), + migrations.AddField( + model_name="ride", + name="designer", + field=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", + ), + ), + ] diff --git a/rides/models.py b/rides/models.py index 6fdebbfc..c3577616 100644 --- a/rides/models.py +++ b/rides/models.py @@ -43,10 +43,19 @@ class Ride(models.Model): default='OT' ) manufacturer = models.ForeignKey( - 'companies.manufacturer', on_delete=models.CASCADE, null=False, blank=False - ) - # other fields... - + 'companies.manufacturer', + on_delete=models.CASCADE, + null=False, + blank=False + ) + designer = models.ForeignKey( + 'designers.Designer', + on_delete=models.SET_NULL, + related_name='rides', + null=True, + blank=True, + help_text='The designer/engineering firm responsible for the ride' + ) model_name = models.CharField(max_length=255, blank=True) status = models.CharField( max_length=20, @@ -108,6 +117,32 @@ class RollerCoasterStats(models.Model): ('OTHER', 'Other'), ] + TRACK_MATERIAL_CHOICES = [ + ('STEEL', 'Steel'), + ('WOOD', 'Wood'), + ('HYBRID', 'Hybrid'), + ('OTHER', 'Other'), + ] + + COASTER_TYPE_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'), + ] + ride = models.OneToOneField( Ride, on_delete=models.CASCADE, @@ -134,6 +169,26 @@ class RollerCoasterStats(models.Model): inversions = models.PositiveIntegerField(default=0) ride_time_seconds = models.PositiveIntegerField(null=True, blank=True) track_type = models.CharField(max_length=255, blank=True) + track_material = models.CharField( + max_length=20, + choices=TRACK_MATERIAL_CHOICES, + default='STEEL', + blank=True + ) + roller_coaster_type = models.CharField( + 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)' + ) + max_drop_height_ft = models.DecimalField( + max_digits=6, + decimal_places=2, + null=True, + blank=True, + help_text='Maximum vertical drop height in feet' + ) launch_type = models.CharField( max_length=20, choices=LAUNCH_CHOICES, diff --git a/rides/urls.py b/rides/urls.py index 77db4f29..d8358aac 100644 --- a/rides/urls.py +++ b/rides/urls.py @@ -4,8 +4,17 @@ from . import views app_name = 'rides' # Add namespace urlpatterns = [ - path('all/', views.RideListView.as_view(), name='all_rides'), # New pattern for all rides + # 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'), + + # Basic ride URLs path('create/', views.RideCreateView.as_view(), name='ride_create'), path('/edit/', views.RideUpdateView.as_view(), name='ride_edit'), path('/', views.RideDetailView.as_view(), name='ride_detail'), diff --git a/rides/views.py b/rides/views.py index 95973d89..1734783a 100644 --- a/rides/views.py +++ b/rides/views.py @@ -6,15 +6,79 @@ from django.db.models import Q from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType from django.contrib import messages -from django.http import JsonResponse, HttpResponseRedirect +from django.http import JsonResponse, HttpResponseRedirect, Http404 +from django.db.models import Count from .models import Ride, RollerCoasterStats from .forms import RideForm from parks.models import Park from core.views import SlugRedirectMixin from moderation.mixins import EditSubmissionMixin, PhotoSubmissionMixin, HistoryMixin -from moderation.models import EditSubmission from media.models import Photo +class SingleCategoryListView(ListView): + model = Ride + template_name = 'rides/ride_category_list.html' + context_object_name = 'categories' + + def get_category_code(self): + category = self.kwargs.get('category') + if not category: + raise Http404("Category not found") + return category + + 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): + 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, *args, **kwargs): + super().setup(request, *args, **kwargs) + self.park = get_object_or_404(Park, slug=self.kwargs['park_slug']) + + def get_category_code(self): + category = self.kwargs.get('category') + if not category: + raise Http404("Category not found") + return category + + 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): + 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 + class RideCreateView(LoginRequiredMixin, CreateView): model = Ride form_class = RideForm diff --git a/static/css/tailwind.css b/static/css/tailwind.css index 2a0d290b..2bb1a0f0 100644 --- a/static/css/tailwind.css +++ b/static/css/tailwind.css @@ -2253,6 +2253,10 @@ select { grid-column: span 1 / span 1; } +.col-span-12 { + grid-column: span 12 / span 12; +} + .mx-1 { margin-left: 0.25rem; margin-right: 0.25rem; @@ -2361,6 +2365,30 @@ select { margin-top: 2rem; } +.mr-1\.5 { + margin-right: 0.375rem; +} + +.mb-0\.5 { + margin-bottom: 0.125rem; +} + +.ml-0\.5 { + margin-left: 0.125rem; +} + +.mr-0\.5 { + margin-right: 0.125rem; +} + +.mt-1\.5 { + margin-top: 0.375rem; +} + +.mb-10 { + margin-bottom: 2.5rem; +} + .block { display: block; } @@ -2433,6 +2461,10 @@ select { height: 340px; } +.h-auto { + height: auto; +} + .max-h-60 { max-height: 15rem; } @@ -2441,6 +2473,10 @@ select { max-height: 90vh; } +.max-h-\[340px\] { + max-height: 340px; +} + .min-h-\[calc\(100vh-16rem\)\] { min-height: calc(100vh - 16rem); } @@ -2453,6 +2489,10 @@ select { min-height: 0px; } +.min-h-\[200px\] { + min-height: 200px; +} + .w-16 { width: 4rem; } @@ -2526,6 +2566,10 @@ select { flex: 1 1 0%; } +.flex-shrink-0 { + flex-shrink: 0; +} + .flex-grow { flex-grow: 1; } @@ -2659,6 +2703,14 @@ select { gap: 2rem; } +.gap-1\.5 { + gap: 0.375rem; +} + +.gap-1 { + gap: 0.25rem; +} + .gap-x-8 { -moz-column-gap: 2rem; column-gap: 2rem; @@ -2732,6 +2784,10 @@ select { overflow: hidden; } +.overflow-y-auto { + overflow-y: auto; +} + .rounded { border-radius: 0.25rem; } @@ -2935,6 +2991,11 @@ select { background-color: rgb(202 138 4 / var(--tw-bg-opacity)); } +.bg-gray-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + .bg-opacity-50 { --tw-bg-opacity: 0.5; } @@ -3006,6 +3067,18 @@ select { padding: 2rem; } +.p-2\.5 { + padding: 0.625rem; +} + +.p-0\.5 { + padding: 0.125rem; +} + +.p-1\.5 { + padding: 0.375rem; +} + .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -3071,6 +3144,11 @@ select { padding-bottom: 2rem; } +.py-0\.5 { + padding-top: 0.125rem; + padding-bottom: 0.125rem; +} + .pb-4 { padding-bottom: 1rem; } @@ -3079,6 +3157,10 @@ select { text-align: center; } +.align-middle { + vertical-align: middle; +} + .text-2xl { font-size: 1.5rem; line-height: 2rem; @@ -3114,6 +3196,11 @@ select { line-height: 1rem; } +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + .font-bold { font-weight: 700; } @@ -3126,6 +3213,10 @@ select { font-weight: 600; } +.leading-tight { + line-height: 1.25; +} + .text-blue-500 { --tw-text-opacity: 1; color: rgb(59 130 246 / var(--tw-text-opacity)); @@ -3240,6 +3331,16 @@ select { color: rgb(22 163 74 / var(--tw-text-opacity)); } +.text-sky-400 { + --tw-text-opacity: 1; + color: rgb(56 189 248 / var(--tw-text-opacity)); +} + +.text-sky-900 { + --tw-text-opacity: 1; + color: rgb(12 74 110 / var(--tw-text-opacity)); +} + .opacity-0 { opacity: 0; } @@ -3335,6 +3436,12 @@ select { transition-duration: 150ms; } +.transition-shadow { + transition-property: box-shadow; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + .duration-100 { transition-duration: 100ms; } @@ -3455,6 +3562,11 @@ select { background-color: rgb(202 138 4 / var(--tw-bg-opacity)); } +.hover\:bg-gray-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + .hover\:text-blue-500:hover { --tw-text-opacity: 1; color: rgb(59 130 246 / var(--tw-text-opacity)); @@ -3494,10 +3606,41 @@ select { color: rgb(79 70 229 / 0.8); } +.hover\:text-sky-300:hover { + --tw-text-opacity: 1; + color: rgb(125 211 252 / var(--tw-text-opacity)); +} + +.hover\:text-sky-900:hover { + --tw-text-opacity: 1; + color: rgb(12 74 110 / var(--tw-text-opacity)); +} + +.hover\:text-sky-950:hover { + --tw-text-opacity: 1; + color: rgb(8 47 73 / var(--tw-text-opacity)); +} + +.hover\:text-sky-800:hover { + --tw-text-opacity: 1; + color: rgb(7 89 133 / var(--tw-text-opacity)); +} + +.hover\:text-blue-800:hover { + --tw-text-opacity: 1; + color: rgb(30 64 175 / var(--tw-text-opacity)); +} + .hover\:underline:hover { text-decoration-line: underline; } +.hover\:shadow-md:hover { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + .focus\:border-blue-500:focus { --tw-border-opacity: 1; border-color: rgb(59 130 246 / var(--tw-border-opacity)); @@ -3765,6 +3908,11 @@ select { color: rgb(74 222 128 / var(--tw-text-opacity)); } +.dark\:text-sky-400:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(56 189 248 / var(--tw-text-opacity)); +} + .dark\:ring-1:is(.dark *) { --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); @@ -3838,6 +3986,26 @@ select { color: rgb(79 70 229 / var(--tw-text-opacity)); } +.dark\:hover\:text-sky-400:hover:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(56 189 248 / var(--tw-text-opacity)); +} + +.dark\:hover\:text-sky-600:hover:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(2 132 199 / var(--tw-text-opacity)); +} + +.dark\:hover\:text-sky-200:hover:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(186 230 253 / var(--tw-text-opacity)); +} + +.dark\:hover\:text-sky-300:hover:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(125 211 252 / var(--tw-text-opacity)); +} + @media (min-width: 640px) { .sm\:col-span-2 { grid-column: span 2 / span 2; @@ -3847,6 +4015,54 @@ select { grid-column: span 4 / span 4; } + .sm\:col-span-3 { + grid-column: span 3 / span 3; + } + + .sm\:col-span-8 { + grid-column: span 8 / span 8; + } + + .sm\:col-span-9 { + grid-column: span 9 / span 9; + } + + .sm\:mb-8 { + margin-bottom: 2rem; + } + + .sm\:mb-16 { + margin-bottom: 4rem; + } + + .sm\:flex { + display: flex; + } + + .sm\:h-\[340px\] { + height: 340px; + } + + .sm\:h-\[300px\] { + height: 300px; + } + + .sm\:h-\[200px\] { + height: 200px; + } + + .sm\:h-\[160px\] { + height: 160px; + } + + .sm\:h-\[140px\] { + height: 140px; + } + + .sm\:h-auto { + height: auto; + } + .sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -3855,6 +4071,26 @@ select { grid-template-columns: repeat(6, minmax(0, 1fr)); } + .sm\:grid-cols-12 { + grid-template-columns: repeat(12, minmax(0, 1fr)); + } + + .sm\:grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + + .sm\:grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + + .sm\:flex-col { + flex-direction: column; + } + + .sm\:gap-4 { + gap: 1rem; + } + .sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(1rem * var(--tw-space-x-reverse)); @@ -3866,6 +4102,41 @@ select { margin-right: calc(1.5rem * var(--tw-space-x-reverse)); margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse))); } + + .sm\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } + + .sm\:text-base { + font-size: 1rem; + line-height: 1.5rem; + } + + .sm\:text-sm { + font-size: 0.875rem; + line-height: 1.25rem; + } + + .sm\:text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; + } + + .sm\:text-lg { + font-size: 1.125rem; + line-height: 1.75rem; + } + + .sm\:text-xs { + font-size: 0.75rem; + line-height: 1rem; + } + + .sm\:text-2xl { + font-size: 1.5rem; + line-height: 2rem; + } } @media (min-width: 768px) { @@ -3893,6 +4164,14 @@ select { margin-top: 0px; } + .md\:mb-8 { + margin-bottom: 2rem; + } + + .md\:h-\[140px\] { + height: 140px; + } + .md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } diff --git a/templates/companies/company_detail.html b/templates/companies/company_detail.html index 58a68233..118ca122 100644 --- a/templates/companies/company_detail.html +++ b/templates/companies/company_detail.html @@ -5,75 +5,100 @@ {% block content %}
- -
-
-
-

{{ company.name }}

- {% if company.headquarters %} -

- {{ company.headquarters }} -

- {% endif %} -
-
- {% if company.website %} - - Visit Website - - {% endif %} - {% if user.is_authenticated %} - - Edit - - {% endif %} -
-
- - {% if company.description %} -
- {{ company.description|linebreaks }} -
+ +
+ {% if company.website %} + + Visit Website + + {% endif %} + {% if user.is_authenticated %} + + Edit + {% endif %}
- -
-
-
- {{ parks.count }} + +
+ +
+

{{ company.name }}

+ + {% if company.headquarters %} +
+ +

{{ company.headquarters }}

-
Theme Parks
+ {% endif %}
- -
-
- {{ parks|length }} + + +
+ +
+
+
Total Parks
+
{{ parks.count }}
+
+
+
Active Parks
+
{{ parks|length }}
+
-
Active Parks
-
- -
-
- {% with total_rides=0 %} - {% for park in parks %} - {% with total_rides=total_rides|add:park.rides.count %}{% endwith %} - {% endfor %} - {{ total_rides }} - {% endwith %} + + +
+
+ +
Total Attractions
+
{{ total_rides }}
+
+ + {% if company.founded_date %} +
+ +
Founded
+
{{ company.founded_date }}
+
+ {% endif %} + + {% if company.website %} +
+ +
Website
+
+ + Visit + + +
+
+ {% endif %}
-
Total Attractions
+ {% if company.description %} +
+

About

+
+ {{ company.description|linebreaks }} +
+
+ {% endif %} +
-

Theme Parks

+

Theme Parks

{% for park in parks %} -
+
{% if park.photos.exists %} {{ park.name }}

+ class="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"> {{ park.name }}

diff --git a/templates/companies/manufacturer_detail.html b/templates/companies/manufacturer_detail.html index dac5026a..6431c9fc 100644 --- a/templates/companies/manufacturer_detail.html +++ b/templates/companies/manufacturer_detail.html @@ -5,70 +5,100 @@ {% block content %}
- -
-
-
-

{{ manufacturer.name }}

- {% if manufacturer.headquarters %} -

- {{ manufacturer.headquarters }} -

- {% endif %} -
-
- {% if manufacturer.website %} - - Visit Website - - {% endif %} - {% if user.is_authenticated %} - - Edit - - {% endif %} -
-
- - {% if manufacturer.description %} -
- {{ manufacturer.description|linebreaks }} -
+ +
+ {% if manufacturer.website %} + + Visit Website + + {% endif %} + {% if user.is_authenticated %} + + Edit + {% endif %}
- -
-
-
- {{ rides.count }} + +
+ +
+

{{ manufacturer.name }}

+ + {% if manufacturer.headquarters %} +
+ +

{{ manufacturer.headquarters }}

-
Total Rides
+ {% endif %}
- -
-
- {{ rides|filter:"type='ROLLER_COASTER'"|length }} + + +
+ +
+
+
Total Rides
+
{{ rides.count }}
+
+
+
Coasters
+
{{ coaster_count }}
+
-
Roller Coasters
-
- -
-
- {{ rides|regroup:"park"|length }} + + +
+
+ +
Parks Served
+
{{ parks_count }}
+
+ + {% if manufacturer.founded_date %} +
+ +
Founded
+
{{ manufacturer.founded_date }}
+
+ {% endif %} + + {% if manufacturer.website %} +
+ +
Website
+
+ + Visit + + +
+
+ {% endif %}
-
Parks with Rides
+ {% if manufacturer.description %} +
+

About

+
+ {{ manufacturer.description|linebreaks }} +
+
+ {% endif %} +
-

Rides

+

Rides

{% for ride in rides %} -
+
{% if ride.photos.exists %} {{ ride.name }}

+ class="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300"> {{ ride.name }}

diff --git a/templates/designers/designer_detail.html b/templates/designers/designer_detail.html new file mode 100644 index 00000000..3b58e621 --- /dev/null +++ b/templates/designers/designer_detail.html @@ -0,0 +1,121 @@ +{% extends 'base/base.html' %} +{% load static %} + +{% block title %}{{ designer.name }} - ThrillWiki{% endblock %} + +{% block content %} +
+ +
+ +
+
+

{{ designer.name }}

+ {% if designer.description %} +
+ {{ designer.description|linebreaks }} +
+ {% endif %} +
+
+ + +
+
+

Quick Stats

+
+
+
Total Rides
+
{{ stats.total_rides }}
+
+
+
Roller Coasters
+
{{ stats.total_coasters }}
+
+
+
Parks
+
{{ stats.total_parks }}
+
+
+
Countries
+
{{ stats.total_countries }}
+
+
+ {% if designer.website %} + + {% endif %} +
+
+
+ + +
+

Designed Rides

+ + {% if rides %} +
+ {% for ride in rides %} +
+
+ + + {{ ride.get_category_display }} + +
+ +
+ {% if ride.opening_date %} +
+
Opened
+
{{ ride.opening_date }}
+
+ {% endif %} + {% if ride.manufacturer %} +
+
Manufacturer
+
{{ ride.manufacturer.name }}
+
+ {% endif %} + {% if ride.category == 'RC' and ride.coaster_stats %} + {% if ride.coaster_stats.height_ft %} +
+
Height
+
{{ ride.coaster_stats.height_ft }} ft
+
+ {% endif %} + {% if ride.coaster_stats.speed_mph %} +
+
Speed
+
{{ ride.coaster_stats.speed_mph }} mph
+
+ {% endif %} + {% endif %} +
+
+ {% endfor %} +
+ {% else %} +

No rides found.

+ {% endif %} +
+
+{% endblock %} diff --git a/templates/parks/park_detail.html b/templates/parks/park_detail.html index cb969f03..df15234c 100644 --- a/templates/parks/park_detail.html +++ b/templates/parks/park_detail.html @@ -22,35 +22,35 @@
{% if user.is_authenticated %} -
+
- Edit + Edit {% if perms.media.add_photo %} {% endif %}
{% endif %} -
+
-
-

{{ park.name }}

+
+

{{ park.name }}

{% if park.formatted_location %} -
- +
+

{{ park.formatted_location }}

{% endif %} -
- + - + + {{ park.average_rating|floatformat:1 }}/10 {% endif %} @@ -67,36 +67,36 @@
-
+
-
+
{% if park.total_rides %} -
Total Rides
-
{{ park.total_rides }}
+ class="flex flex-col items-center justify-center p-2 text-center transition-transform bg-white rounded-lg shadow-lg hover:scale-[1.02] dark:bg-gray-800"> +
Total Rides
+
{{ park.total_rides }}
{% endif %} {% if park.total_roller_coasters %} -
-
Total Roller Coasters
-
{{ park.total_roller_coasters }}
+
+
Roller Coasters
+
{{ park.total_roller_coasters }}
{% endif %}
-
+
{% if park.owner %} -
- -
Owner/Operator
-
+
+ +
Owner
+
+ class="text-blue-600 text-2xs sm:text-xs hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"> {{ park.owner.name }}
@@ -104,39 +104,23 @@ {% endif %} {% if park.opening_date %} -
- -
Opening Date
-
{{ park.opening_date }}
-
- {% endif %} - - {% if park.operating_season %} -
- -
Operating Season
-
{{ park.operating_season }}
-
- {% endif %} - - {% if park.size_acres %} -
- -
Size
-
{{ park.size_acres }} acres
+
+ +
Opened
+
{{ park.opening_date }}
{% endif %} {% if park.website %} -
- -
Website
-
+
+ +
Website
+
- Official Website - + Visit +
@@ -144,7 +128,8 @@
- + + {% if park.photos.exists %}

Photos

diff --git a/templates/rides/ride_category_list.html b/templates/rides/ride_category_list.html new file mode 100644 index 00000000..a21b670d --- /dev/null +++ b/templates/rides/ride_category_list.html @@ -0,0 +1,154 @@ +{% extends "base/base.html" %} +{% load static %} +{% load ride_tags %} + +{% block title %}{{ title }}{% endblock %} + +{% block content %} +
+
+
+

{{ title }}

+ {% if park %} + + Back to {{ park.name }} + + {% endif %} +
+
+ + + + + {% if not categories %} +

No rides found.

+ {% endif %} + + {% for category_name, rides in categories.items %} +
+

{{ category_name }}s

+
+ {% for ride in rides %} +
+
+ {% if ride.photos.exists %} + {{ ride.name }} + {% else %} + {{ ride.name }} + {% endif %} +
+ +
+

+ + {{ ride.name }} + +

+ {% if not park %} +

+ at + {{ ride.park.name }} + +

+ {% endif %} + {% if ride.manufacturer %} +

{{ ride.manufacturer.name }}

+ {% endif %} +
+ + {{ ride.get_category_display }} + + + {{ ride.get_status_display }} + + {% if ride.average_rating %} + + + {{ ride.average_rating|floatformat:1 }}/10 + + {% endif %} +
+ {% if ride.coaster_stats %} +
+ {% if ride.coaster_stats.height_ft %} +
+ Height: {{ ride.coaster_stats.height_ft }}ft +
+ {% endif %} + {% if ride.coaster_stats.speed_mph %} +
+ Speed: {{ ride.coaster_stats.speed_mph }}mph +
+ {% endif %} +
+ {% endif %} +
+
+ {% endfor %} +
+
+ {% endfor %} +
+{% endblock %} diff --git a/templates/rides/ride_detail.html b/templates/rides/ride_detail.html index 7c406a56..47741f94 100644 --- a/templates/rides/ride_detail.html +++ b/templates/rides/ride_detail.html @@ -5,54 +5,161 @@ {% block content %}
- -
-
-
-

{{ ride.name }}

-

- at - {{ ride.park.name }} - - {% if ride.park_area %} - - {{ ride.park_area.name }} - {% endif %} -

-
- - {{ ride.get_status_display }} - - - {{ ride.get_category_display }} - - {% if ride.average_rating %} - - - {{ ride.average_rating|floatformat:1 }}/10 - - {% endif %} -
+ + {% if user.is_authenticated %} +
+ + Edit + + {% if perms.media.add_photo %} + + {% endif %} +
+ {% endif %} + + +
+ +
+

{{ ride.name }}

+ +
+ at + {{ ride.park.name }} + + {% if ride.park_area %} + - {{ ride.park_area.name }} + {% endif %}
- {% if user.is_authenticated %} -
- - Edit - - {% if perms.media.add_photo %} - + +
+ + {{ ride.get_status_display }} + + + {{ ride.get_category_display }} + + {% if ride.average_rating %} + + + {{ ride.average_rating|floatformat:1 }}/10 + + {% endif %} +
+
+ + +
+ +
+ {% if coaster_stats %} + {% if coaster_stats.height_ft %} +
+
Height
+
{{ coaster_stats.height_ft }} ft
+
{% endif %} + {% if coaster_stats.speed_mph %} +
+
Speed
+
{{ coaster_stats.speed_mph }} mph
+
+ {% endif %} + {% if coaster_stats.inversions %} +
+
Inversions
+
{{ coaster_stats.inversions }}
+
+ {% endif %} + {% if coaster_stats.length_ft %} +
+
Length
+
{{ coaster_stats.length_ft }} ft
+
+ {% endif %} + {% endif %} +
+ + +
+ {% if ride.manufacturer %} +
+ +
Manufacturer
+
+ + {{ ride.manufacturer.name }} + +
- {% endif %} + {% endif %} + + {% if ride.designer %} +
+ +
Designer
+
+ + {{ ride.designer.name }} + +
+
+ {% endif %} + + {% if coaster_stats.roller_coaster_type %} +
+ +
Coaster Type
+
{{ coaster_stats.get_roller_coaster_type_display }}
+
+ {% endif %} + + {% if coaster_stats.track_material %} +
+ +
Track Material
+
{{ coaster_stats.get_track_material_display }}
+
+ {% endif %} + + {% if ride.opening_date %} +
+ +
Opened
+
{{ ride.opening_date }}
+
+ {% endif %} + + {% if ride.capacity_per_hour %} +
+ +
Capacity
+
{{ ride.capacity_per_hour }}/hr
+
+ {% endif %} + + {% if coaster_stats.launch_type %} +
+ +
Launch Type
+
{{ coaster_stats.get_launch_type_display }}
+
+ {% endif %} +
- + {% if ride.photos.exists %}

Photos

@@ -60,13 +167,50 @@
{% endif %} + +
+
+

Reviews

+ {% if user.is_authenticated %} + + {% endif %} +
+ + {% if ride.reviews.exists %} +
+ {% for review in ride.reviews.all %} +
+
+
+

{{ review.title }}

+

+ by {{ review.user.username }} on {{ review.created_at|date }} +

+
+
+ + {{ review.rating }}/10 +
+
+

{{ review.content }}

+
+ {% endfor %} +
+ {% else %} +

No reviews yet. Be the first to review this ride!

+ {% endif %} +
+
{% if ride.description %}
-

About

+

Trivia

{{ ride.description|linebreaks }}
@@ -79,8 +223,8 @@
{% for name_history in ride.previous_names %}
- {{ name_history.name }} - {{ name_history.period }} + {{ name_history.name }} + {{ name_history.period }}
{% endfor %}
@@ -91,44 +235,129 @@

Roller Coaster Statistics

+ + {% if coaster_stats.roller_coaster_type %} +
+ Coaster Type + + {{ coaster_stats.get_roller_coaster_type_display }} + +
+ {% endif %} + + {% if coaster_stats.height_ft %}
- Height + Maximum Height {{ coaster_stats.height_ft }} ft
{% endif %} + {% if coaster_stats.max_drop_height_ft %} +
+ Drop Height + + {{ coaster_stats.max_drop_height_ft }} ft + +
+ {% endif %} + + {% if coaster_stats.length_ft %}
- Length + Track Length {{ coaster_stats.length_ft }} ft
{% endif %} + {% if coaster_stats.track_type %} +
+ Track Layout + + {{ coaster_stats.track_type }} + +
+ {% endif %} + {% if coaster_stats.track_material %} +
+ Track Material + + {{ coaster_stats.get_track_material_display }} + +
+ {% endif %} + + {% if coaster_stats.speed_mph %}
- Speed + Maximum Speed {{ coaster_stats.speed_mph }} mph
{% endif %} -
- Inversions - - {{ coaster_stats.inversions }} - -
{% if coaster_stats.ride_time_seconds %}
- Ride Duration + Ride Duration {{ coaster_stats.ride_time_seconds }} sec
{% endif %} + + + {% if coaster_stats.train_style %} +
+ Train Style + + {{ coaster_stats.train_style }} + +
+ {% endif %} + {% if coaster_stats.trains_count %} +
+ Number of Trains + + {{ coaster_stats.trains_count }} + +
+ {% endif %} + {% if coaster_stats.cars_per_train %} +
+ Cars per Train + + {{ coaster_stats.cars_per_train }} + +
+ {% endif %} + {% if coaster_stats.seats_per_car %} +
+ Seats per Car + + {{ coaster_stats.seats_per_car }} + +
+ {% endif %} + + + {% if coaster_stats.inversions %} +
+ Inversions + + {{ coaster_stats.inversions }} + +
+ {% endif %} + {% if coaster_stats.launch_type %} +
+ Launch Type + + {{ coaster_stats.get_launch_type_display }} + +
+ {% endif %}
{% endif %} @@ -140,18 +369,29 @@

Quick Facts

-
Manufacturer
+
Manufacturer
{{ ride.manufacturer }}
+ {% if ride.designer %} + + {% endif %} {% if ride.model_name %}
-
Model
+
Model
{{ ride.model_name }}
{% endif %} {% if ride.opening_date %}
-
Opening Date
+
Opening Date
{{ ride.opening_date }}
@@ -159,7 +399,7 @@ {% endif %} {% if ride.status_since %}
-
Status Since
+
Status Since
{{ ride.status_since }}
@@ -167,7 +407,7 @@ {% endif %} {% if ride.closing_date %}
-
Closing Date
+
Closing Date
{{ ride.closing_date }}
@@ -175,7 +415,7 @@ {% endif %} {% if ride.capacity_per_hour %}
-
Capacity
+
Capacity
{{ ride.capacity_per_hour }} riders/hour
@@ -183,7 +423,7 @@ {% endif %} {% if ride.min_height_in %}
-
Minimum Height
+
Minimum Height
{{ ride.min_height_in }} inches
@@ -206,65 +446,33 @@
{% for field, changes in record.diff_against_previous.items %} -
- {{ field }}: - {{ changes.old }} → {{ changes.new }} -
+ {% if field != "updated_at" %} +
+ {{ field|title }}: + {{ changes.old }} + + {{ changes.new }} +
+ {% endif %} {% endfor %}
{% empty %} -

No history available.

+

No history available.

{% endfor %}
- - -
-
-

Reviews

- {% if user.is_authenticated %} - - {% endif %} -
- - {% if ride.reviews.exists %} -
- {% for review in ride.reviews.all %} -
-
-
-

{{ review.title }}

-

- by {{ review.user.username }} on {{ review.created_at|date }} -

-
-
- - {{ review.rating }}/10 -
-
-

{{ review.content }}

-
- {% endfor %} -
- {% else %} -

No reviews yet. Be the first to review this ride!

- {% endif %} -
{% if perms.media.add_photo %} -
diff --git a/thrillwiki/__pycache__/settings.cpython-312.pyc b/thrillwiki/__pycache__/settings.cpython-312.pyc index 0de63485..d24daa64 100644 Binary files a/thrillwiki/__pycache__/settings.cpython-312.pyc and b/thrillwiki/__pycache__/settings.cpython-312.pyc differ diff --git a/thrillwiki/__pycache__/urls.cpython-312.pyc b/thrillwiki/__pycache__/urls.cpython-312.pyc index b8c2ff10..0fa0eba8 100644 Binary files a/thrillwiki/__pycache__/urls.cpython-312.pyc and b/thrillwiki/__pycache__/urls.cpython-312.pyc differ diff --git a/thrillwiki/settings.py b/thrillwiki/settings.py index 18f3198e..f60c1b1b 100644 --- a/thrillwiki/settings.py +++ b/thrillwiki/settings.py @@ -45,6 +45,7 @@ INSTALLED_APPS = [ "media.apps.MediaConfig", "moderation", "history_tracking", + "designers", ] MIDDLEWARE = [ diff --git a/thrillwiki/urls.py b/thrillwiki/urls.py index 1f6e5fa4..9aa724c6 100644 --- a/thrillwiki/urls.py +++ b/thrillwiki/urls.py @@ -18,6 +18,7 @@ urlpatterns = [ # Other URLs path("reviews/", include("reviews.urls")), path("companies/", include("companies.urls")), + path("designers/", include("designers.urls", namespace="designers")), path("photos/", include("media.urls", namespace="photos")), # Add photos URLs path("search/", SearchView.as_view(), name="search"), path(