From 8265348a8337a497d11049f6b75f77cf2be29e3a Mon Sep 17 00:00:00 2001 From: pacnpal <183241239+pacnpal@users.noreply.github.com> Date: Wed, 13 Nov 2024 21:59:49 +0000 Subject: [PATCH] code commit --- media/submissions/photos/coaster_track.gif | Bin 0 -> 35 bytes media/submissions/photos/park_entrance.gif | Bin 0 -> 35 bytes media/submissions/photos/test_image.gif | Bin 0 -> 35 bytes media/submissions/photos/test_image2.gif | Bin 0 -> 35 bytes .../photos/test_image2_ruT57k4.gif | Bin 0 -> 35 bytes .../submissions/photos/test_image_iI0mcgf.gif | Bin 0 -> 35 bytes moderation/management/__init__.py | 0 moderation/management/commands/__init__.py | 0 .../management/commands/seed_submissions.py | 228 ++++++++++ ...02_alter_editsubmission_status_and_more.py | 41 ++ .../0003_update_existing_statuses.py | 32 ++ .../migrations/0004_add_moderator_changes.py | 22 + moderation/models.py | 46 +- moderation/templatetags/moderation_tags.py | 54 +++ moderation/urls.py | 7 + moderation/views.py | 179 +++++++- static/css/tailwind.css | 421 +++++++----------- .../moderation/partials/coaster_fields.html | 150 +++++++ .../partials/dashboard_content.html | 6 +- .../partials/designer_search_results.html | 67 +++ .../partials/edit_submission_form.html | 132 ++++++ templates/moderation/partials/filters.html | 2 +- .../partials/manufacturer_search_results.html | 67 +++ .../partials/park_search_results.html | 73 +++ .../moderation/partials/photo_submission.html | 31 +- .../partials/ride_model_search_results.html | 67 +++ 26 files changed, 1336 insertions(+), 289 deletions(-) create mode 100644 media/submissions/photos/coaster_track.gif create mode 100644 media/submissions/photos/park_entrance.gif create mode 100644 media/submissions/photos/test_image.gif create mode 100644 media/submissions/photos/test_image2.gif create mode 100644 media/submissions/photos/test_image2_ruT57k4.gif create mode 100644 media/submissions/photos/test_image_iI0mcgf.gif create mode 100644 moderation/management/__init__.py create mode 100644 moderation/management/commands/__init__.py create mode 100644 moderation/management/commands/seed_submissions.py create mode 100644 moderation/migrations/0002_alter_editsubmission_status_and_more.py create mode 100644 moderation/migrations/0003_update_existing_statuses.py create mode 100644 moderation/migrations/0004_add_moderator_changes.py create mode 100644 moderation/templatetags/moderation_tags.py create mode 100644 templates/moderation/partials/coaster_fields.html create mode 100644 templates/moderation/partials/designer_search_results.html create mode 100644 templates/moderation/partials/edit_submission_form.html create mode 100644 templates/moderation/partials/manufacturer_search_results.html create mode 100644 templates/moderation/partials/park_search_results.html create mode 100644 templates/moderation/partials/ride_model_search_results.html diff --git a/media/submissions/photos/coaster_track.gif b/media/submissions/photos/coaster_track.gif new file mode 100644 index 0000000000000000000000000000000000000000..0ad774e841f10aff88fd944f22a2726cacaa9a6e GIT binary patch literal 35 kcmZ?wbh9u|WMp7uXkcUjg5>069S{u?VPIl%VPvod09%0s1poj5 literal 0 HcmV?d00001 diff --git a/media/submissions/photos/park_entrance.gif b/media/submissions/photos/park_entrance.gif new file mode 100644 index 0000000000000000000000000000000000000000..0ad774e841f10aff88fd944f22a2726cacaa9a6e GIT binary patch literal 35 kcmZ?wbh9u|WMp7uXkcUjg5>069S{u?VPIl%VPvod09%0s1poj5 literal 0 HcmV?d00001 diff --git a/media/submissions/photos/test_image.gif b/media/submissions/photos/test_image.gif new file mode 100644 index 0000000000000000000000000000000000000000..0ad774e841f10aff88fd944f22a2726cacaa9a6e GIT binary patch literal 35 kcmZ?wbh9u|WMp7uXkcUjg5>069S{u?VPIl%VPvod09%0s1poj5 literal 0 HcmV?d00001 diff --git a/media/submissions/photos/test_image2.gif b/media/submissions/photos/test_image2.gif new file mode 100644 index 0000000000000000000000000000000000000000..0ad774e841f10aff88fd944f22a2726cacaa9a6e GIT binary patch literal 35 kcmZ?wbh9u|WMp7uXkcUjg5>069S{u?VPIl%VPvod09%0s1poj5 literal 0 HcmV?d00001 diff --git a/media/submissions/photos/test_image2_ruT57k4.gif b/media/submissions/photos/test_image2_ruT57k4.gif new file mode 100644 index 0000000000000000000000000000000000000000..0ad774e841f10aff88fd944f22a2726cacaa9a6e GIT binary patch literal 35 kcmZ?wbh9u|WMp7uXkcUjg5>069S{u?VPIl%VPvod09%0s1poj5 literal 0 HcmV?d00001 diff --git a/media/submissions/photos/test_image_iI0mcgf.gif b/media/submissions/photos/test_image_iI0mcgf.gif new file mode 100644 index 0000000000000000000000000000000000000000..0ad774e841f10aff88fd944f22a2726cacaa9a6e GIT binary patch literal 35 kcmZ?wbh9u|WMp7uXkcUjg5>069S{u?VPIl%VPvod09%0s1poj5 literal 0 HcmV?d00001 diff --git a/moderation/management/__init__.py b/moderation/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/moderation/management/commands/__init__.py b/moderation/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/moderation/management/commands/seed_submissions.py b/moderation/management/commands/seed_submissions.py new file mode 100644 index 00000000..a65bf45c --- /dev/null +++ b/moderation/management/commands/seed_submissions.py @@ -0,0 +1,228 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType +from django.core.files.uploadedfile import SimpleUploadedFile +from django.utils import timezone +from moderation.models import EditSubmission, PhotoSubmission +from parks.models import Park +from rides.models import Ride +from datetime import date, timedelta + +User = get_user_model() + +class Command(BaseCommand): + help = 'Seeds test submissions for moderation dashboard' + + def handle(self, *args, **kwargs): + # Ensure we have a test user + user, created = User.objects.get_or_create( + username='test_user', + email='test@example.com' + ) + if created: + user.set_password('testpass123') + user.save() + self.stdout.write(self.style.SUCCESS('Created test user')) + + # Get content types + park_ct = ContentType.objects.get_for_model(Park) + ride_ct = ContentType.objects.get_for_model(Ride) + + # Create test park for edit submissions + test_park, created = Park.objects.get_or_create( + name='Test Park', + defaults={ + 'description': 'A test theme park located in Orlando, Florida', + 'status': 'OPERATING', + 'operating_season': 'Year-round', + 'size_acres': 100.50, + 'website': 'https://testpark.example.com' + } + ) + + # Create test ride for edit submissions + test_ride, created = Ride.objects.get_or_create( + name='Test Coaster', + park=test_park, + defaults={ + 'description': 'A thrilling steel roller coaster with multiple inversions', + 'status': 'OPERATING', + 'category': 'RC', + 'capacity_per_hour': 1200, + 'ride_duration_seconds': 180, + 'min_height_in': 48, + 'opening_date': date(2020, 6, 15) + } + ) + + # Create EditSubmissions + + # New park creation with detailed information + EditSubmission.objects.create( + user=user, + content_type=park_ct, + submission_type='CREATE', + changes={ + 'name': 'Adventure World Orlando', + 'description': ('A brand new theme park coming to Orlando featuring five uniquely themed lands: ' + 'Future Frontier, Ancient Mysteries, Ocean Depths, Sky Kingdom, and Fantasy Forest. ' + 'The park will feature state-of-the-art attractions including 3 roller coasters, ' + '4 dark rides, and multiple family attractions in each themed area.'), + 'status': 'UNDER_CONSTRUCTION', + 'opening_date': '2024-06-01', + 'operating_season': 'Year-round with extended hours during summer and holidays', + 'size_acres': 250.75, + 'website': 'https://adventureworld.example.com', + 'location': { + 'street_address': '1234 Theme Park Way', + 'city': 'Orlando', + 'state': 'Florida', + 'country': 'United States', + 'postal_code': '32819', + 'latitude': '28.538336', + 'longitude': '-81.379234' + } + }, + reason=('Submitting new theme park details based on official press release and construction permits. ' + 'The park has begun vertical construction and has announced its opening date.'), + source=('Official press release: https://adventureworld.example.com/press/announcement\n' + 'Construction permits: Orange County Building Department #2023-12345'), + status='PENDING' + ) + + # Existing park edit with comprehensive updates + EditSubmission.objects.create( + user=user, + content_type=park_ct, + object_id=test_park.id, + submission_type='EDIT', + changes={ + 'description': ('A world-class theme park featuring 12 uniquely themed areas and over 50 attractions. ' + 'Recent expansion added the new "Cosmic Adventures" area with 2 roller coasters and ' + '3 family attractions. The park now offers enhanced dining options and night-time ' + 'spectacular "Starlight Dreams".'), + 'status': 'OPERATING', + 'website': 'https://testpark.example.com', + 'size_acres': 120.25, + 'operating_season': ('Year-round with extended hours (9AM-11PM) during summer. ' + 'Special events during Halloween and Christmas seasons.'), + 'location': { + 'street_address': '5678 Park Boulevard', + 'city': 'Orlando', + 'state': 'Florida', + 'country': 'United States', + 'postal_code': '32830', + 'latitude': '28.538336', + 'longitude': '-81.379234' + } + }, + reason=('Updating park information to reflect recent expansion and operational changes. ' + 'The new Cosmic Adventures area opened last month and operating hours have been extended.'), + source=('Park press release: https://testpark.example.com/news/expansion\n' + 'Official park map: https://testpark.example.com/map\n' + 'Personal visit and photos from opening day of new area'), + status='PENDING' + ) + + # New ride creation with detailed specifications + EditSubmission.objects.create( + user=user, + content_type=ride_ct, + submission_type='CREATE', + changes={ + 'name': 'Thunderbolt: The Ultimate Launch Coaster', + 'park': test_park.id, + 'description': ('A cutting-edge steel launch coaster featuring the world\'s tallest inversion (160 ft) ' + 'and fastest launch acceleration (0-80 mph in 2 seconds). The ride features a unique ' + 'triple launch system, 5 inversions including a zero-g roll and cobra roll, and a ' + 'first-of-its-kind vertical helix element. Total track length is 4,500 feet with a ' + 'maximum height of 375 feet.'), + 'status': 'UNDER_CONSTRUCTION', + 'category': 'RC', + 'opening_date': '2024-07-01', + 'capacity_per_hour': 1400, + 'ride_duration_seconds': 210, + 'min_height_in': 52, + 'manufacturer': 1, # Assuming manufacturer ID + 'park_area': 1, # Assuming park area ID + 'stats': { + 'height_ft': 375, + 'length_ft': 4500, + 'speed_mph': 80, + 'inversions': 5, + 'launch_type': 'LSM', + 'track_material': 'STEEL', + 'roller_coaster_type': 'SITDOWN', + 'trains_count': 3, + 'cars_per_train': 6, + 'seats_per_car': 4 + } + }, + reason=('Submitting details for the new flagship roller coaster announced by the park. ' + 'Construction has begun and track pieces are arriving on site.'), + source=('Official announcement: https://testpark.example.com/thunderbolt\n' + 'Construction photos: https://coasterfan.com/thunderbolt-construction\n' + 'Manufacturer specifications sheet'), + status='PENDING' + ) + + # Existing ride edit with technical updates + EditSubmission.objects.create( + user=user, + content_type=ride_ct, + object_id=test_ride.id, + submission_type='EDIT', + changes={ + 'description': ('A high-speed steel roller coaster featuring 4 inversions and a unique ' + 'dual-loading station system. Recent upgrades include new magnetic braking ' + 'system and enhanced on-board audio experience.'), + 'status': 'OPERATING', + 'capacity_per_hour': 1500, # Increased after station upgrades + 'ride_duration_seconds': 185, + 'min_height_in': 48, + 'max_height_in': 80, + 'stats': { + 'trains_count': 3, + 'cars_per_train': 8, + 'seats_per_car': 4 + } + }, + reason=('Updating ride information to reflect recent upgrades including new braking system, ' + 'audio system, and increased capacity due to improved loading efficiency.'), + source=('Park operations manual\n' + 'Maintenance records\n' + 'Personal observation and timing of new ride cycle'), + status='PENDING' + ) + + # Create PhotoSubmissions with detailed captions + + # Park photo submission + image_data = b'GIF87a\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00ccc,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;' + dummy_image = SimpleUploadedFile('park_entrance.gif', image_data, content_type='image/gif') + + PhotoSubmission.objects.create( + user=user, + content_type=park_ct, + object_id=test_park.id, + photo=dummy_image, + caption=('Main entrance plaza of Test Park showing the newly installed digital display board ' + 'and renovated ticketing area. Photo taken during morning park opening.'), + date_taken=date(2024, 1, 15), + status='PENDING' + ) + + # Ride photo submission + dummy_image2 = SimpleUploadedFile('coaster_track.gif', image_data, content_type='image/gif') + PhotoSubmission.objects.create( + user=user, + content_type=ride_ct, + object_id=test_ride.id, + photo=dummy_image2, + caption=('Test Coaster\'s first drop and loop element showing the new paint scheme. ' + 'Photo taken from the guest pathway near Station Alpha.'), + date_taken=date(2024, 1, 20), + status='PENDING' + ) + + self.stdout.write(self.style.SUCCESS('Successfully seeded test submissions')) diff --git a/moderation/migrations/0002_alter_editsubmission_status_and_more.py b/moderation/migrations/0002_alter_editsubmission_status_and_more.py new file mode 100644 index 00000000..8e018fa2 --- /dev/null +++ b/moderation/migrations/0002_alter_editsubmission_status_and_more.py @@ -0,0 +1,41 @@ +# Generated by Django 5.1.3 on 2024-11-13 19:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("moderation", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="editsubmission", + name="status", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("APPROVED", "Approved"), + ("REJECTED", "Rejected"), + ("ESCALATED", "Escalated"), + ], + default="PENDING", + max_length=20, + ), + ), + migrations.AlterField( + model_name="photosubmission", + name="status", + field=models.CharField( + choices=[ + ("PENDING", "Pending"), + ("APPROVED", "Approved"), + ("REJECTED", "Rejected"), + ("ESCALATED", "Escalated"), + ], + default="PENDING", + max_length=20, + ), + ), + ] diff --git a/moderation/migrations/0003_update_existing_statuses.py b/moderation/migrations/0003_update_existing_statuses.py new file mode 100644 index 00000000..011f417f --- /dev/null +++ b/moderation/migrations/0003_update_existing_statuses.py @@ -0,0 +1,32 @@ +from django.db import migrations + +def update_statuses(apps, schema_editor): + EditSubmission = apps.get_model('moderation', 'EditSubmission') + PhotoSubmission = apps.get_model('moderation', 'PhotoSubmission') + + # Update EditSubmissions + EditSubmission.objects.filter(status='NEW').update(status='PENDING') + + # Update PhotoSubmissions + PhotoSubmission.objects.filter(status='NEW').update(status='PENDING') + PhotoSubmission.objects.filter(status='AUTO_APPROVED').update(status='APPROVED') + +def reverse_statuses(apps, schema_editor): + EditSubmission = apps.get_model('moderation', 'EditSubmission') + PhotoSubmission = apps.get_model('moderation', 'PhotoSubmission') + + # Reverse EditSubmissions + EditSubmission.objects.filter(status='PENDING').update(status='NEW') + + # Reverse PhotoSubmissions + PhotoSubmission.objects.filter(status='PENDING').update(status='NEW') + +class Migration(migrations.Migration): + + dependencies = [ + ('moderation', '0002_alter_editsubmission_status_and_more'), + ] + + operations = [ + migrations.RunPython(update_statuses, reverse_statuses), + ] diff --git a/moderation/migrations/0004_add_moderator_changes.py b/moderation/migrations/0004_add_moderator_changes.py new file mode 100644 index 00000000..d8429da4 --- /dev/null +++ b/moderation/migrations/0004_add_moderator_changes.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.3 on 2024-11-13 20:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("moderation", "0003_update_existing_statuses"), + ] + + operations = [ + migrations.AddField( + model_name="editsubmission", + name="moderator_changes", + field=models.JSONField( + blank=True, + help_text="Moderator's edited version of the changes before approval", + null=True, + ), + ), + ] diff --git a/moderation/models.py b/moderation/models.py index 8c3aafdd..00dc5e33 100644 --- a/moderation/models.py +++ b/moderation/models.py @@ -15,7 +15,7 @@ UserType = Union[AbstractBaseUser, AnonymousUser] class EditSubmission(models.Model): STATUS_CHOICES = [ - ("NEW", "New"), + ("PENDING", "Pending"), ("APPROVED", "Approved"), ("REJECTED", "Rejected"), ("ESCALATED", "Escalated"), @@ -49,13 +49,20 @@ class EditSubmission(models.Model): changes = models.JSONField( help_text="JSON representation of the changes or new object data" ) + + # Moderator's edited version of changes before approval + moderator_changes = models.JSONField( + null=True, + blank=True, + help_text="Moderator's edited version of the changes before approval" + ) # Metadata reason = models.TextField(help_text="Why this edit/addition is needed") source = models.TextField( blank=True, help_text="Source of information (if applicable)" ) - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="NEW") + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="PENDING") created_at = models.DateTimeField(auto_now_add=True) # Review details @@ -133,7 +140,10 @@ class EditSubmission(models.Model): raise ValueError("Could not resolve model class") try: - resolved_data = self._resolve_foreign_keys(self.changes) + # Use moderator_changes if available, otherwise use original changes + changes_to_apply = self.moderator_changes if self.moderator_changes is not None else self.changes + + resolved_data = self._resolve_foreign_keys(changes_to_apply) prepared_data = self._prepare_model_data(resolved_data, model_class) # For CREATE submissions, check for duplicates by name @@ -168,7 +178,7 @@ class EditSubmission(models.Model): return obj except Exception as e: if self.status != "REJECTED": # Don't override if already rejected due to duplicate - self.status = "NEW" # Reset status if approval failed + self.status = "PENDING" # Reset status if approval failed self.save() raise ValueError(f"Error approving submission: {str(e)}") from e @@ -189,10 +199,10 @@ class EditSubmission(models.Model): class PhotoSubmission(models.Model): STATUS_CHOICES = [ - ("NEW", "New"), + ("PENDING", "Pending"), ("APPROVED", "Approved"), ("REJECTED", "Rejected"), - ("AUTO_APPROVED", "Auto Approved"), + ("ESCALATED", "Escalated"), ] # Who submitted the photo @@ -213,7 +223,7 @@ class PhotoSubmission(models.Model): date_taken = models.DateField(null=True, blank=True) # Metadata - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="NEW") + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="PENDING") created_at = models.DateTimeField(auto_now_add=True) # Review details @@ -268,22 +278,10 @@ class PhotoSubmission(models.Model): self.notes = notes self.save() - def auto_approve(self) -> None: - """Auto-approve the photo submission (for moderators/admins)""" - from media.models import Photo - - self.status = "AUTO_APPROVED" - self.handled_by = self.user + def escalate(self, moderator: UserType, notes: str = "") -> None: + """Escalate the photo submission to admin""" + self.status = "ESCALATED" + self.handled_by = moderator # type: ignore self.handled_at = timezone.now() - - # Create the approved photo - Photo.objects.create( - uploaded_by=self.user, - content_type=self.content_type, - object_id=self.object_id, - image=self.photo, - caption=self.caption, - is_approved=True, - ) - + self.notes = notes self.save() diff --git a/moderation/templatetags/moderation_tags.py b/moderation/templatetags/moderation_tags.py new file mode 100644 index 00000000..7eddd2a6 --- /dev/null +++ b/moderation/templatetags/moderation_tags.py @@ -0,0 +1,54 @@ +from django import template +from django.apps import apps +from django.core.exceptions import ObjectDoesNotExist +from rides.models import CATEGORY_CHOICES + +register = template.Library() + +@register.filter +def get_object(value, model_path): + """ + Template filter to get an object instance from its ID and model path. + Usage: {{ value|get_object:'app_label.ModelName' }} + """ + if not value: + return None + try: + app_label, model_name = model_path.split('.') + model = apps.get_model(app_label, model_name) + return model.objects.get(id=value) + except (ValueError, LookupError, ObjectDoesNotExist): + return None + +@register.filter +def get_category_display(value): + """ + Template filter to get the display name for a ride category. + Usage: {{ value|get_category_display }} + """ + try: + return dict(CATEGORY_CHOICES).get(value, value) + except (KeyError, AttributeError): + return value + +@register.filter +def get_object_name(value, model_path): + """ + Template filter to get an object's name from its ID and model path. + Usage: {{ value|get_object_name:'app_label.ModelName' }} + """ + obj = get_object(value, model_path) + return obj.name if obj else None + +@register.filter +def get_park_area_name(value, park_id): + """ + Template filter to get a park area's name from its ID and park ID. + Usage: {{ value|get_park_area_name:park_id }} + """ + try: + ParkArea = apps.get_model('parks', 'ParkArea') + area = ParkArea.objects.get(id=value, park_id=park_id) + return area.name + except (ValueError, LookupError, ObjectDoesNotExist): + return None diff --git a/moderation/urls.py b/moderation/urls.py index 8cb92434..93eb7650 100644 --- a/moderation/urls.py +++ b/moderation/urls.py @@ -16,7 +16,14 @@ urlpatterns = [ path('dashboard/', views.DashboardView.as_view(), name='dashboard'), path('submissions/', views.submission_list, name='submission_list'), + # Search endpoints + path('search/parks/', views.search_parks, name='search_parks'), + path('search/manufacturers/', views.search_manufacturers, name='search_manufacturers'), + path('search/designers/', views.search_designers, name='search_designers'), + path('search/ride-models/', views.search_ride_models, name='search_ride_models'), + # Submission Actions + path('submissions//edit/', views.edit_submission, name='edit_submission'), path('submissions//approve/', views.approve_submission, name='approve_submission'), path('submissions//reject/', views.reject_submission, name='reject_submission'), path('submissions//escalate/', views.escalate_submission, name='escalate_submission'), diff --git a/moderation/views.py b/moderation/views.py index 04243181..f0aa9cfc 100644 --- a/moderation/views.py +++ b/moderation/views.py @@ -10,6 +10,10 @@ from typing import Optional, Any, cast from accounts.models import User from .models import EditSubmission, PhotoSubmission +from parks.models import Park, ParkArea +from designers.models import Designer +from companies.models import Manufacturer +from rides.models import RideModel MODERATOR_ROLES = ['MODERATOR', 'ADMIN', 'SUPERUSER'] @@ -29,6 +33,108 @@ class ModeratorRequiredMixin(UserPassesTestMixin): return super().handle_no_permission() raise PermissionDenied("You do not have moderator permissions.") +@login_required +def search_parks(request: HttpRequest) -> HttpResponse: + """HTMX endpoint for searching parks in moderation dashboard""" + user = cast(User, request.user) + if not (user.role in MODERATOR_ROLES or user.is_superuser): + return HttpResponse(status=403) + + query = request.GET.get('q', '').strip() + submission_id = request.GET.get('submission_id') + + # If no query, show first 10 parks + if not query: + parks = Park.objects.all().order_by('name')[:10] + else: + parks = Park.objects.filter(name__icontains=query).order_by('name')[:10] + + context = { + 'parks': parks, + 'search_term': query, + 'submission_id': submission_id + } + + return render(request, 'moderation/partials/park_search_results.html', context) + +@login_required +def search_manufacturers(request: HttpRequest) -> HttpResponse: + """HTMX endpoint for searching manufacturers in moderation dashboard""" + user = cast(User, request.user) + if not (user.role in MODERATOR_ROLES or user.is_superuser): + return HttpResponse(status=403) + + query = request.GET.get('q', '').strip() + submission_id = request.GET.get('submission_id') + + # If no query, show first 10 manufacturers + if not query: + manufacturers = Manufacturer.objects.all().order_by('name')[:10] + else: + manufacturers = Manufacturer.objects.filter(name__icontains=query).order_by('name')[:10] + + context = { + 'manufacturers': manufacturers, + 'search_term': query, + 'submission_id': submission_id + } + + return render(request, 'moderation/partials/manufacturer_search_results.html', context) + +@login_required +def search_designers(request: HttpRequest) -> HttpResponse: + """HTMX endpoint for searching designers in moderation dashboard""" + user = cast(User, request.user) + if not (user.role in MODERATOR_ROLES or user.is_superuser): + return HttpResponse(status=403) + + query = request.GET.get('q', '').strip() + submission_id = request.GET.get('submission_id') + + # If no query, show first 10 designers + if not query: + designers = Designer.objects.all().order_by('name')[:10] + else: + designers = Designer.objects.filter(name__icontains=query).order_by('name')[:10] + + context = { + 'designers': designers, + 'search_term': query, + 'submission_id': submission_id + } + + return render(request, 'moderation/partials/designer_search_results.html', context) + +@login_required +def search_ride_models(request: HttpRequest) -> HttpResponse: + """HTMX endpoint for searching ride models in moderation dashboard""" + user = cast(User, request.user) + if not (user.role in MODERATOR_ROLES or user.is_superuser): + return HttpResponse(status=403) + + query = request.GET.get('q', '').strip() + submission_id = request.GET.get('submission_id') + manufacturer_id = request.GET.get('manufacturer') + + queryset = RideModel.objects.all() + + if manufacturer_id: + queryset = queryset.filter(manufacturer_id=manufacturer_id) + + # If no query, show first 10 models + if not query: + ride_models = queryset.order_by('name')[:10] + else: + ride_models = queryset.filter(name__icontains=query).order_by('name')[:10] + + context = { + 'ride_models': ride_models, + 'search_term': query, + 'submission_id': submission_id + } + + return render(request, 'moderation/partials/ride_model_search_results.html', context) + class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, ListView): template_name = 'moderation/dashboard.html' context_object_name = 'submissions' @@ -40,7 +146,7 @@ class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, ListView): return [self.template_name] def get_queryset(self): - status = self.request.GET.get('status', 'NEW') + status = self.request.GET.get('status', 'PENDING') submission_type = self.request.GET.get('submission_type', '') if submission_type == 'photo': @@ -63,7 +169,7 @@ def submission_list(request: HttpRequest) -> HttpResponse: if not (user.role in MODERATOR_ROLES or user.is_superuser): return HttpResponse(status=403) - status = request.GET.get('status', 'NEW') + status = request.GET.get('status', 'PENDING') submission_type = request.GET.get('submission_type', '') if submission_type == 'photo': @@ -80,6 +186,11 @@ def submission_list(request: HttpRequest) -> HttpResponse: context = { 'submissions': queryset, 'user': request.user, + 'parks': [(p.id, str(p)) for p in Park.objects.all()], + 'designers': [(d.id, str(d)) for d in Designer.objects.all()], + 'manufacturers': [(m.id, str(m)) for m in Manufacturer.objects.all()], + 'ride_models': [(m.id, str(m)) for m in RideModel.objects.all()], + 'owners': [(u.id, str(u)) for u in User.objects.filter(role__in=['OWNER', 'ADMIN', 'SUPERUSER'])] } # If it's an HTMX request, return just the content @@ -89,6 +200,64 @@ def submission_list(request: HttpRequest) -> HttpResponse: # For direct URL access, return the full dashboard template return render(request, 'moderation/dashboard.html', context) +@login_required +def edit_submission(request: HttpRequest, submission_id: int) -> HttpResponse: + """HTMX endpoint for editing a submission""" + user = cast(User, request.user) + if not (user.role in MODERATOR_ROLES or user.is_superuser): + return HttpResponse(status=403) + + submission = get_object_or_404(EditSubmission, id=submission_id) + + if request.method == 'POST': + # Handle the edit submission + notes = request.POST.get('notes') + if not notes: + return HttpResponse("Notes are required when editing a submission", status=400) + + try: + # Update the moderator_changes with the edited values + edited_changes = submission.changes.copy() + for field in submission.changes.keys(): + if field == 'stats': + edited_stats = {} + for key in submission.changes['stats'].keys(): + if new_value := request.POST.get(f'stats.{key}'): + edited_stats[key] = new_value + edited_changes['stats'] = edited_stats + else: + if new_value := request.POST.get(field): + # Handle special field types + if field in ['latitude', 'longitude', 'size_acres']: + try: + edited_changes[field] = float(new_value) + except ValueError: + return HttpResponse(f"Invalid value for {field}", status=400) + else: + edited_changes[field] = new_value + + submission.moderator_changes = edited_changes + submission.notes = notes + submission.save() + + # Return the updated submission + context = { + 'submission': submission, + 'user': request.user, + 'parks': [(p.id, str(p)) for p in Park.objects.all()], + 'designers': [(d.id, str(d)) for d in Designer.objects.all()], + 'manufacturers': [(m.id, str(m)) for m in Manufacturer.objects.all()], + 'ride_models': [(m.id, str(m)) for m in RideModel.objects.all()], + 'owners': [(u.id, str(u)) for u in User.objects.filter(role__in=['OWNER', 'ADMIN', 'SUPERUSER'])], + 'park_areas': [(a.id, str(a)) for a in ParkArea.objects.filter(park_id=edited_changes.get('park'))] if edited_changes.get('park') else [] + } + return render(request, 'moderation/partials/submission_list.html', context) + + except Exception as e: + return HttpResponse(str(e), status=400) + + return HttpResponse("Invalid request method", status=405) + @login_required def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse: """HTMX endpoint for approving a submission""" @@ -105,7 +274,7 @@ def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse _update_submission_notes(submission, request.POST.get('notes')) # Get updated queryset with filters - status = request.GET.get('status', 'NEW') + status = request.GET.get('status', 'PENDING') submission_type = request.GET.get('submission_type', '') if submission_type == 'photo': @@ -143,7 +312,7 @@ def reject_submission(request: HttpRequest, submission_id: int) -> HttpResponse: _update_submission_notes(submission, request.POST.get('notes')) # Get updated queryset with filters - status = request.GET.get('status', 'NEW') + status = request.GET.get('status', 'PENDING') submission_type = request.GET.get('submission_type', '') if submission_type == 'photo': @@ -179,7 +348,7 @@ def escalate_submission(request: HttpRequest, submission_id: int) -> HttpRespons _update_submission_notes(submission, request.POST.get('notes')) # Get updated queryset with filters - status = request.GET.get('status', 'NEW') + status = request.GET.get('status', 'PENDING') submission_type = request.GET.get('submission_type', '') if submission_type == 'photo': diff --git a/static/css/tailwind.css b/static/css/tailwind.css index efaa71f8..9567116a 100644 --- a/static/css/tailwind.css +++ b/static/css/tailwind.css @@ -2249,10 +2249,6 @@ select { z-index: 60; } -.z-10 { - z-index: 10; -} - .col-span-1 { grid-column: span 1 / span 1; } @@ -2273,6 +2269,16 @@ select { grid-column: 1 / -1; } +.-mx-1\.5 { + margin-left: -0.375rem; + margin-right: -0.375rem; +} + +.-my-1\.5 { + margin-top: -0.375rem; + margin-bottom: -0.375rem; +} + .mx-1 { margin-left: 0.25rem; margin-right: 0.25rem; @@ -2298,21 +2304,6 @@ select { margin-right: auto; } -.mx-2\.5 { - margin-left: 0.625rem; - margin-right: 0.625rem; -} - -.-mx-1\.5 { - margin-left: -0.375rem; - margin-right: -0.375rem; -} - -.-my-1\.5 { - margin-top: -0.375rem; - margin-bottom: -0.375rem; -} - .-mb-px { margin-bottom: -1px; } @@ -2357,22 +2348,42 @@ select { margin-left: 0.25rem; } +.ml-1\.5 { + margin-left: 0.375rem; +} + .ml-2 { margin-left: 0.5rem; } +.ml-3 { + margin-left: 0.75rem; +} + .ml-6 { margin-left: 1.5rem; } +.ml-auto { + margin-left: auto; +} + .mr-1 { margin-right: 0.25rem; } +.mr-1\.5 { + margin-right: 0.375rem; +} + .mr-2 { margin-right: 0.5rem; } +.mr-2\.5 { + margin-right: 0.625rem; +} + .mr-3 { margin-right: 0.75rem; } @@ -2385,6 +2396,10 @@ select { margin-top: 0.25rem; } +.mt-1\.5 { + margin-top: 0.375rem; +} + .mt-2 { margin-top: 0.5rem; } @@ -2405,30 +2420,6 @@ select { margin-top: auto; } -.mr-2\.5 { - margin-right: 0.625rem; -} - -.ml-1\.5 { - margin-left: 0.375rem; -} - -.mr-1\.5 { - margin-right: 0.375rem; -} - -.mt-1\.5 { - margin-top: 0.375rem; -} - -.ml-3 { - margin-left: 0.75rem; -} - -.ml-auto { - margin-left: auto; -} - .block { display: block; } @@ -2562,10 +2553,6 @@ select { min-width: 200px; } -.min-w-\[120px\] { - min-width: 120px; -} - .max-w-2xl { max-width: 42rem; } @@ -2578,6 +2565,10 @@ select { max-width: 56rem; } +.max-w-6xl { + max-width: 72rem; +} + .max-w-7xl { max-width: 80rem; } @@ -2602,10 +2593,6 @@ select { max-width: 20rem; } -.max-w-6xl { - max-width: 72rem; -} - .flex-1 { flex: 1 1 0%; } @@ -2638,16 +2625,6 @@ select { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } -.translate-x-0 { - --tw-translate-x: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.translate-x-4 { - --tw-translate-x: 1rem; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - .scale-100 { --tw-scale-x: 1; --tw-scale-y: 1; @@ -2660,12 +2637,6 @@ select { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } -.scale-\[1\.01\] { - --tw-scale-x: 1.01; - --tw-scale-y: 1.01; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - .transform { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } @@ -2814,12 +2785,6 @@ select { margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); } -.space-x-6 > :not([hidden]) ~ :not([hidden]) { - --tw-space-x-reverse: 0; - margin-right: calc(1.5rem * var(--tw-space-x-reverse)); - margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse))); -} - .overflow-auto { overflow: auto; } @@ -2906,6 +2871,10 @@ select { border-style: dashed; } +.border-blue-200\/50 { + border-color: rgb(191 219 254 / 0.5); +} + .border-blue-500 { --tw-border-opacity: 1; border-color: rgb(59 130 246 / var(--tw-border-opacity)); @@ -2930,6 +2899,11 @@ select { border-color: rgb(209 213 219 / var(--tw-border-opacity)); } +.border-gray-700 { + --tw-border-opacity: 1; + border-color: rgb(55 65 81 / var(--tw-border-opacity)); +} + .border-green-500 { --tw-border-opacity: 1; border-color: rgb(34 197 94 / var(--tw-border-opacity)); @@ -2954,16 +2928,6 @@ select { border-color: transparent; } -.border-gray-800 { - --tw-border-opacity: 1; - border-color: rgb(31 41 55 / var(--tw-border-opacity)); -} - -.border-gray-700 { - --tw-border-opacity: 1; - border-color: rgb(55 65 81 / var(--tw-border-opacity)); -} - .border-t-transparent { border-top-color: transparent; } @@ -2973,10 +2937,6 @@ select { background-color: rgb(0 0 0 / var(--tw-bg-opacity)); } -.bg-black\/20 { - background-color: rgb(0 0 0 / 0.2); -} - .bg-black\/50 { background-color: rgb(0 0 0 / 0.5); } @@ -3001,6 +2961,10 @@ select { background-color: rgb(37 99 235 / var(--tw-bg-opacity)); } +.bg-blue-900\/40 { + background-color: rgb(30 58 138 / 0.4); +} + .bg-gray-100 { --tw-bg-opacity: 1; background-color: rgb(243 244 246 / var(--tw-bg-opacity)); @@ -3021,6 +2985,20 @@ select { background-color: rgb(107 114 128 / var(--tw-bg-opacity)); } +.bg-gray-800 { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity)); +} + +.bg-gray-900 { + --tw-bg-opacity: 1; + background-color: rgb(17 24 39 / var(--tw-bg-opacity)); +} + +.bg-gray-900\/80 { + background-color: rgb(17 24 39 / 0.8); +} + .bg-green-100 { --tw-bg-opacity: 1; background-color: rgb(220 252 231 / var(--tw-bg-opacity)); @@ -3036,9 +3014,8 @@ select { background-color: rgb(22 163 74 / var(--tw-bg-opacity)); } -.bg-orange-100 { - --tw-bg-opacity: 1; - background-color: rgb(255 237 213 / var(--tw-bg-opacity)); +.bg-green-900\/40 { + background-color: rgb(20 83 45 / 0.4); } .bg-red-100 { @@ -3065,6 +3042,10 @@ select { background-color: rgb(255 255 255 / 0.1); } +.bg-white\/80 { + background-color: rgb(255 255 255 / 0.8); +} + .bg-white\/90 { background-color: rgb(255 255 255 / 0.9); } @@ -3084,32 +3065,6 @@ select { background-color: rgb(202 138 4 / var(--tw-bg-opacity)); } -.bg-gray-900 { - --tw-bg-opacity: 1; - background-color: rgb(17 24 39 / var(--tw-bg-opacity)); -} - -.bg-blue-900\/40 { - background-color: rgb(30 58 138 / 0.4); -} - -.bg-gray-800 { - --tw-bg-opacity: 1; - background-color: rgb(31 41 55 / var(--tw-bg-opacity)); -} - -.bg-green-900\/40 { - background-color: rgb(20 83 45 / 0.4); -} - -.bg-blue-900\/30 { - background-color: rgb(30 58 138 / 0.3); -} - -.bg-gray-900\/80 { - background-color: rgb(17 24 39 / 0.8); -} - .bg-opacity-50 { --tw-bg-opacity: 0.5; } @@ -3247,6 +3202,11 @@ select { padding-bottom: 0.5rem; } +.py-2\.5 { + padding-top: 0.625rem; + padding-bottom: 0.625rem; +} + .py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; @@ -3267,16 +3227,6 @@ select { padding-bottom: 2rem; } -.py-2\.5 { - padding-top: 0.625rem; - padding-bottom: 0.625rem; -} - -.px-5 { - padding-left: 1.25rem; - padding-right: 1.25rem; -} - .pb-4 { padding-bottom: 1rem; } @@ -3304,6 +3254,11 @@ select { line-height: 2.5rem; } +.text-5xl { + font-size: 3rem; + line-height: 1; +} + .text-lg { font-size: 1.125rem; line-height: 1.75rem; @@ -3324,11 +3279,6 @@ select { line-height: 1rem; } -.text-5xl { - font-size: 3rem; - line-height: 1; -} - .font-bold { font-weight: 700; } @@ -3357,6 +3307,11 @@ select { line-height: 1.25; } +.text-blue-400 { + --tw-text-opacity: 1; + color: rgb(96 165 250 / var(--tw-text-opacity)); +} + .text-blue-500 { --tw-text-opacity: 1; color: rgb(59 130 246 / var(--tw-text-opacity)); @@ -3367,16 +3322,16 @@ select { color: rgb(37 99 235 / var(--tw-text-opacity)); } -.text-blue-700 { - --tw-text-opacity: 1; - color: rgb(29 78 216 / var(--tw-text-opacity)); -} - .text-blue-800 { --tw-text-opacity: 1; color: rgb(30 64 175 / var(--tw-text-opacity)); } +.text-blue-900 { + --tw-text-opacity: 1; + color: rgb(30 58 138 / var(--tw-text-opacity)); +} + .text-gray-200 { --tw-text-opacity: 1; color: rgb(229 231 235 / var(--tw-text-opacity)); @@ -3412,9 +3367,9 @@ select { color: rgb(17 24 39 / var(--tw-text-opacity)); } -.text-green-500 { +.text-green-400 { --tw-text-opacity: 1; - color: rgb(34 197 94 / var(--tw-text-opacity)); + color: rgb(74 222 128 / var(--tw-text-opacity)); } .text-green-600 { @@ -3432,11 +3387,6 @@ select { color: rgb(22 101 52 / var(--tw-text-opacity)); } -.text-orange-800 { - --tw-text-opacity: 1; - color: rgb(154 52 18 / var(--tw-text-opacity)); -} - .text-primary { --tw-text-opacity: 1; color: rgb(79 70 229 / var(--tw-text-opacity)); @@ -3501,26 +3451,6 @@ select { color: rgb(133 77 14 / var(--tw-text-opacity)); } -.text-blue-400 { - --tw-text-opacity: 1; - color: rgb(96 165 250 / var(--tw-text-opacity)); -} - -.text-green-400 { - --tw-text-opacity: 1; - color: rgb(74 222 128 / var(--tw-text-opacity)); -} - -.text-blue-200 { - --tw-text-opacity: 1; - color: rgb(191 219 254 / var(--tw-text-opacity)); -} - -.text-blue-300 { - --tw-text-opacity: 1; - color: rgb(147 197 253 / var(--tw-text-opacity)); -} - .opacity-0 { opacity: 0; } @@ -3694,21 +3624,21 @@ select { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); } -.hover\:scale-\[1\.01\]:hover { - --tw-scale-x: 1.01; - --tw-scale-y: 1.01; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.hover\:transform:hover { - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - .hover\:border-gray-300:hover { --tw-border-opacity: 1; border-color: rgb(209 213 219 / var(--tw-border-opacity)); } +.hover\:bg-blue-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity)); +} + +.hover\:bg-blue-500:hover { + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity)); +} + .hover\:bg-blue-700:hover { --tw-bg-opacity: 1; background-color: rgb(29 78 216 / var(--tw-bg-opacity)); @@ -3734,9 +3664,9 @@ select { background-color: rgb(249 250 251 / var(--tw-bg-opacity)); } -.hover\:bg-green-700:hover { +.hover\:bg-green-500:hover { --tw-bg-opacity: 1; - background-color: rgb(21 128 61 / var(--tw-bg-opacity)); + background-color: rgb(34 197 94 / var(--tw-bg-opacity)); } .hover\:bg-red-50:hover { @@ -3744,6 +3674,11 @@ select { background-color: rgb(254 242 242 / var(--tw-bg-opacity)); } +.hover\:bg-red-500:hover { + --tw-bg-opacity: 1; + background-color: rgb(239 68 68 / var(--tw-bg-opacity)); +} + .hover\:bg-red-700:hover { --tw-bg-opacity: 1; background-color: rgb(185 28 28 / var(--tw-bg-opacity)); @@ -3753,16 +3688,16 @@ select { background-color: rgb(255 255 255 / 0.2); } +.hover\:bg-yellow-500:hover { + --tw-bg-opacity: 1; + background-color: rgb(234 179 8 / var(--tw-bg-opacity)); +} + .hover\:bg-yellow-600:hover { --tw-bg-opacity: 1; background-color: rgb(202 138 4 / var(--tw-bg-opacity)); } -.hover\:bg-yellow-700:hover { - --tw-bg-opacity: 1; - background-color: rgb(161 98 7 / var(--tw-bg-opacity)); -} - .hover\:text-blue-500:hover { --tw-text-opacity: 1; color: rgb(59 130 246 / var(--tw-text-opacity)); @@ -3783,6 +3718,11 @@ select { color: rgb(30 64 175 / var(--tw-text-opacity)); } +.hover\:text-blue-900:hover { + --tw-text-opacity: 1; + color: rgb(30 58 138 / var(--tw-text-opacity)); +} + .hover\:text-gray-300:hover { --tw-text-opacity: 1; color: rgb(209 213 219 / var(--tw-text-opacity)); @@ -3798,6 +3738,11 @@ select { color: rgb(55 65 81 / var(--tw-text-opacity)); } +.hover\:text-gray-900:hover { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + .hover\:text-primary:hover { --tw-text-opacity: 1; color: rgb(79 70 229 / var(--tw-text-opacity)); @@ -3812,16 +3757,6 @@ select { color: rgb(7 89 133 / var(--tw-text-opacity)); } -.hover\:text-gray-900:hover { - --tw-text-opacity: 1; - color: rgb(17 24 39 / var(--tw-text-opacity)); -} - -.hover\:text-blue-300:hover { - --tw-text-opacity: 1; - color: rgb(147 197 253 / var(--tw-text-opacity)); -} - .hover\:underline:hover { text-decoration-line: underline; } @@ -3867,11 +3802,6 @@ select { --tw-ring-color: rgb(79 70 229 / 0.5); } -.focus\:ring-gray-300:focus { - --tw-ring-opacity: 1; - --tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity)); -} - .focus\:ring-offset-2:focus { --tw-ring-offset-width: 2px; } @@ -3890,6 +3820,10 @@ select { opacity: 1; } +.dark\:border-blue-700\/50:is(.dark *) { + border-color: rgb(29 78 216 / 0.5); +} + .dark\:border-gray-600:is(.dark *) { --tw-border-opacity: 1; border-color: rgb(75 85 99 / var(--tw-border-opacity)); @@ -3904,10 +3838,6 @@ select { border-color: rgb(55 65 81 / 0.5); } -.dark\:bg-black\/40:is(.dark *) { - background-color: rgb(0 0 0 / 0.4); -} - .dark\:bg-blue-400\/30:is(.dark *) { background-color: rgb(96 165 250 / 0.3); } @@ -3922,8 +3852,12 @@ select { background-color: rgb(29 78 216 / var(--tw-bg-opacity)); } -.dark\:bg-blue-900\/50:is(.dark *) { - background-color: rgb(30 58 138 / 0.5); +.dark\:bg-blue-900\/30:is(.dark *) { + background-color: rgb(30 58 138 / 0.3); +} + +.dark\:bg-blue-900\/40:is(.dark *) { + background-color: rgb(30 58 138 / 0.4); } .dark\:bg-gray-600:is(.dark *) { @@ -3949,41 +3883,33 @@ select { background-color: rgb(31 41 55 / 0.9); } +.dark\:bg-gray-900:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(17 24 39 / var(--tw-bg-opacity)); +} + +.dark\:bg-gray-900\/80:is(.dark *) { + background-color: rgb(17 24 39 / 0.8); +} + .dark\:bg-green-200:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(187 247 208 / var(--tw-bg-opacity)); } -.dark\:bg-green-500:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(34 197 94 / var(--tw-bg-opacity)); -} - .dark\:bg-green-700:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(21 128 61 / var(--tw-bg-opacity)); } -.dark\:bg-green-900\/50:is(.dark *) { - background-color: rgb(20 83 45 / 0.5); -} - -.dark\:bg-orange-900\/50:is(.dark *) { - background-color: rgb(124 45 18 / 0.5); -} - .dark\:bg-red-200:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(254 202 202 / var(--tw-bg-opacity)); } -.dark\:bg-red-500:is(.dark *) { +.dark\:bg-red-700:is(.dark *) { --tw-bg-opacity: 1; - background-color: rgb(239 68 68 / var(--tw-bg-opacity)); -} - -.dark\:bg-red-900\/50:is(.dark *) { - background-color: rgb(127 29 29 / 0.5); + background-color: rgb(185 28 28 / var(--tw-bg-opacity)); } .dark\:bg-yellow-200:is(.dark *) { @@ -3995,38 +3921,20 @@ select { background-color: rgb(250 204 21 / 0.3); } -.dark\:bg-yellow-500:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(234 179 8 / var(--tw-bg-opacity)); -} - .dark\:bg-yellow-600:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(202 138 4 / var(--tw-bg-opacity)); } +.dark\:bg-yellow-700:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(161 98 7 / var(--tw-bg-opacity)); +} + .dark\:bg-yellow-900\/50:is(.dark *) { background-color: rgb(113 63 18 / 0.5); } -.dark\:bg-blue-900\/60:is(.dark *) { - background-color: rgb(30 58 138 / 0.6); -} - -.dark\:bg-green-800:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(22 101 52 / var(--tw-bg-opacity)); -} - -.dark\:bg-green-900:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(20 83 45 / var(--tw-bg-opacity)); -} - -.dark\:bg-blue-900\/30:is(.dark *) { - background-color: rgb(30 58 138 / 0.3); -} - .dark\:from-gray-950:is(.dark *) { --tw-gradient-from: #030712 var(--tw-gradient-from-position); --tw-gradient-to: rgb(3 7 18 / 0) var(--tw-gradient-to-position); @@ -4047,6 +3955,11 @@ select { color: rgb(191 219 254 / var(--tw-text-opacity)); } +.dark\:text-blue-300:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(147 197 253 / var(--tw-text-opacity)); +} + .dark\:text-blue-400:is(.dark *) { --tw-text-opacity: 1; color: rgb(96 165 250 / var(--tw-text-opacity)); @@ -4157,16 +4070,6 @@ select { color: rgb(133 77 14 / var(--tw-text-opacity)); } -.dark\:text-blue-300:is(.dark *) { - --tw-text-opacity: 1; - color: rgb(147 197 253 / var(--tw-text-opacity)); -} - -.dark\:text-green-200:is(.dark *) { - --tw-text-opacity: 1; - color: rgb(187 247 208 / 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); @@ -4196,6 +4099,10 @@ select { background-color: rgb(30 64 175 / var(--tw-bg-opacity)); } +.dark\:hover\:bg-blue-900\/40:hover:is(.dark *) { + background-color: rgb(30 58 138 / 0.4); +} + .dark\:hover\:bg-gray-500:hover:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(107 114 128 / var(--tw-bg-opacity)); @@ -4364,6 +4271,14 @@ select { } @media (min-width: 768px) { + .md\:col-span-1 { + grid-column: span 1 / span 1; + } + + .md\:col-span-2 { + grid-column: span 2 / span 2; + } + .md\:mb-8 { margin-bottom: 2rem; } diff --git a/templates/moderation/partials/coaster_fields.html b/templates/moderation/partials/coaster_fields.html new file mode 100644 index 00000000..df87dd7c --- /dev/null +++ b/templates/moderation/partials/coaster_fields.html @@ -0,0 +1,150 @@ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
diff --git a/templates/moderation/partials/dashboard_content.html b/templates/moderation/partials/dashboard_content.html index d13fabf7..f2a09c46 100644 --- a/templates/moderation/partials/dashboard_content.html +++ b/templates/moderation/partials/dashboard_content.html @@ -5,9 +5,9 @@
- diff --git a/templates/moderation/partials/designer_search_results.html b/templates/moderation/partials/designer_search_results.html new file mode 100644 index 00000000..72a438a9 --- /dev/null +++ b/templates/moderation/partials/designer_search_results.html @@ -0,0 +1,67 @@ +
+ {% if designers %} + {% for designer in designers %} + + {% endfor %} + {% else %} +
+ {% if search_term %} + No designers found + {% else %} + Start typing to search... + {% endif %} +
+ {% endif %} +
+ + diff --git a/templates/moderation/partials/edit_submission_form.html b/templates/moderation/partials/edit_submission_form.html new file mode 100644 index 00000000..d21a4fae --- /dev/null +++ b/templates/moderation/partials/edit_submission_form.html @@ -0,0 +1,132 @@ +{% load moderation_tags %} + +
+

+ Edit Submission +

+ +
+ + {% for field, value in changes.items %} + {% if field != 'model_name' %} +
+ + + {% if field == 'stats' %} +
+ {% for stat_name, stat_value in value.items %} +
+ + +
+ {% endfor %} +
+ {% elif field == 'park' %} + + {% elif field == 'designer' %} + + {% elif field == 'manufacturer' %} + + {% elif field == 'ride_model' %} + + {% elif field == 'park_area' %} + + {% else %} + + {% endif %} +
+ {% endif %} + {% endfor %} + +
+ + +
+ +
+ + +
+
+
diff --git a/templates/moderation/partials/filters.html b/templates/moderation/partials/filters.html index f0cb8350..b7b4481f 100644 --- a/templates/moderation/partials/filters.html +++ b/templates/moderation/partials/filters.html @@ -11,7 +11,7 @@