code commit

This commit is contained in:
pacnpal
2024-11-13 21:59:49 +00:00
parent 8f7f7add2d
commit 8265348a83
26 changed files with 1336 additions and 289 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 B

View File

View File

@@ -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'))

View File

@@ -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,
),
),
]

View File

@@ -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),
]

View File

@@ -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,
),
),
]

View File

@@ -15,7 +15,7 @@ UserType = Union[AbstractBaseUser, AnonymousUser]
class EditSubmission(models.Model): class EditSubmission(models.Model):
STATUS_CHOICES = [ STATUS_CHOICES = [
("NEW", "New"), ("PENDING", "Pending"),
("APPROVED", "Approved"), ("APPROVED", "Approved"),
("REJECTED", "Rejected"), ("REJECTED", "Rejected"),
("ESCALATED", "Escalated"), ("ESCALATED", "Escalated"),
@@ -50,12 +50,19 @@ class EditSubmission(models.Model):
help_text="JSON representation of the changes or new object data" 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 # Metadata
reason = models.TextField(help_text="Why this edit/addition is needed") reason = models.TextField(help_text="Why this edit/addition is needed")
source = models.TextField( source = models.TextField(
blank=True, help_text="Source of information (if applicable)" 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) created_at = models.DateTimeField(auto_now_add=True)
# Review details # Review details
@@ -133,7 +140,10 @@ class EditSubmission(models.Model):
raise ValueError("Could not resolve model class") raise ValueError("Could not resolve model class")
try: 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) prepared_data = self._prepare_model_data(resolved_data, model_class)
# For CREATE submissions, check for duplicates by name # For CREATE submissions, check for duplicates by name
@@ -168,7 +178,7 @@ class EditSubmission(models.Model):
return obj return obj
except Exception as e: except Exception as e:
if self.status != "REJECTED": # Don't override if already rejected due to duplicate 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() self.save()
raise ValueError(f"Error approving submission: {str(e)}") from e raise ValueError(f"Error approving submission: {str(e)}") from e
@@ -189,10 +199,10 @@ class EditSubmission(models.Model):
class PhotoSubmission(models.Model): class PhotoSubmission(models.Model):
STATUS_CHOICES = [ STATUS_CHOICES = [
("NEW", "New"), ("PENDING", "Pending"),
("APPROVED", "Approved"), ("APPROVED", "Approved"),
("REJECTED", "Rejected"), ("REJECTED", "Rejected"),
("AUTO_APPROVED", "Auto Approved"), ("ESCALATED", "Escalated"),
] ]
# Who submitted the photo # Who submitted the photo
@@ -213,7 +223,7 @@ class PhotoSubmission(models.Model):
date_taken = models.DateField(null=True, blank=True) date_taken = models.DateField(null=True, blank=True)
# Metadata # 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) created_at = models.DateTimeField(auto_now_add=True)
# Review details # Review details
@@ -268,22 +278,10 @@ class PhotoSubmission(models.Model):
self.notes = notes self.notes = notes
self.save() self.save()
def auto_approve(self) -> None: def escalate(self, moderator: UserType, notes: str = "") -> None:
"""Auto-approve the photo submission (for moderators/admins)""" """Escalate the photo submission to admin"""
from media.models import Photo self.status = "ESCALATED"
self.handled_by = moderator # type: ignore
self.status = "AUTO_APPROVED"
self.handled_by = self.user
self.handled_at = timezone.now() self.handled_at = timezone.now()
self.notes = notes
# 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.save() self.save()

View File

@@ -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

View File

@@ -16,7 +16,14 @@ urlpatterns = [
path('dashboard/', views.DashboardView.as_view(), name='dashboard'), path('dashboard/', views.DashboardView.as_view(), name='dashboard'),
path('submissions/', views.submission_list, name='submission_list'), 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 # Submission Actions
path('submissions/<int:submission_id>/edit/', views.edit_submission, name='edit_submission'),
path('submissions/<int:submission_id>/approve/', views.approve_submission, name='approve_submission'), path('submissions/<int:submission_id>/approve/', views.approve_submission, name='approve_submission'),
path('submissions/<int:submission_id>/reject/', views.reject_submission, name='reject_submission'), path('submissions/<int:submission_id>/reject/', views.reject_submission, name='reject_submission'),
path('submissions/<int:submission_id>/escalate/', views.escalate_submission, name='escalate_submission'), path('submissions/<int:submission_id>/escalate/', views.escalate_submission, name='escalate_submission'),

View File

@@ -10,6 +10,10 @@ from typing import Optional, Any, cast
from accounts.models import User from accounts.models import User
from .models import EditSubmission, PhotoSubmission 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'] MODERATOR_ROLES = ['MODERATOR', 'ADMIN', 'SUPERUSER']
@@ -29,6 +33,108 @@ class ModeratorRequiredMixin(UserPassesTestMixin):
return super().handle_no_permission() return super().handle_no_permission()
raise PermissionDenied("You do not have moderator permissions.") 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): class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, ListView):
template_name = 'moderation/dashboard.html' template_name = 'moderation/dashboard.html'
context_object_name = 'submissions' context_object_name = 'submissions'
@@ -40,7 +146,7 @@ class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, ListView):
return [self.template_name] return [self.template_name]
def get_queryset(self): 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', '') submission_type = self.request.GET.get('submission_type', '')
if submission_type == 'photo': 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): if not (user.role in MODERATOR_ROLES or user.is_superuser):
return HttpResponse(status=403) return HttpResponse(status=403)
status = request.GET.get('status', 'NEW') status = request.GET.get('status', 'PENDING')
submission_type = request.GET.get('submission_type', '') submission_type = request.GET.get('submission_type', '')
if submission_type == 'photo': if submission_type == 'photo':
@@ -80,6 +186,11 @@ def submission_list(request: HttpRequest) -> HttpResponse:
context = { context = {
'submissions': queryset, 'submissions': queryset,
'user': request.user, '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 # 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 # For direct URL access, return the full dashboard template
return render(request, 'moderation/dashboard.html', context) 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 @login_required
def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse: def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse:
"""HTMX endpoint for approving a submission""" """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')) _update_submission_notes(submission, request.POST.get('notes'))
# Get updated queryset with filters # Get updated queryset with filters
status = request.GET.get('status', 'NEW') status = request.GET.get('status', 'PENDING')
submission_type = request.GET.get('submission_type', '') submission_type = request.GET.get('submission_type', '')
if submission_type == 'photo': 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')) _update_submission_notes(submission, request.POST.get('notes'))
# Get updated queryset with filters # Get updated queryset with filters
status = request.GET.get('status', 'NEW') status = request.GET.get('status', 'PENDING')
submission_type = request.GET.get('submission_type', '') submission_type = request.GET.get('submission_type', '')
if submission_type == 'photo': 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')) _update_submission_notes(submission, request.POST.get('notes'))
# Get updated queryset with filters # Get updated queryset with filters
status = request.GET.get('status', 'NEW') status = request.GET.get('status', 'PENDING')
submission_type = request.GET.get('submission_type', '') submission_type = request.GET.get('submission_type', '')
if submission_type == 'photo': if submission_type == 'photo':

View File

@@ -2249,10 +2249,6 @@ select {
z-index: 60; z-index: 60;
} }
.z-10 {
z-index: 10;
}
.col-span-1 { .col-span-1 {
grid-column: span 1 / span 1; grid-column: span 1 / span 1;
} }
@@ -2273,6 +2269,16 @@ select {
grid-column: 1 / -1; 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 { .mx-1 {
margin-left: 0.25rem; margin-left: 0.25rem;
margin-right: 0.25rem; margin-right: 0.25rem;
@@ -2298,21 +2304,6 @@ select {
margin-right: auto; 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 { .-mb-px {
margin-bottom: -1px; margin-bottom: -1px;
} }
@@ -2357,22 +2348,42 @@ select {
margin-left: 0.25rem; margin-left: 0.25rem;
} }
.ml-1\.5 {
margin-left: 0.375rem;
}
.ml-2 { .ml-2 {
margin-left: 0.5rem; margin-left: 0.5rem;
} }
.ml-3 {
margin-left: 0.75rem;
}
.ml-6 { .ml-6 {
margin-left: 1.5rem; margin-left: 1.5rem;
} }
.ml-auto {
margin-left: auto;
}
.mr-1 { .mr-1 {
margin-right: 0.25rem; margin-right: 0.25rem;
} }
.mr-1\.5 {
margin-right: 0.375rem;
}
.mr-2 { .mr-2 {
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.mr-2\.5 {
margin-right: 0.625rem;
}
.mr-3 { .mr-3 {
margin-right: 0.75rem; margin-right: 0.75rem;
} }
@@ -2385,6 +2396,10 @@ select {
margin-top: 0.25rem; margin-top: 0.25rem;
} }
.mt-1\.5 {
margin-top: 0.375rem;
}
.mt-2 { .mt-2 {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
@@ -2405,30 +2420,6 @@ select {
margin-top: auto; 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 { .block {
display: block; display: block;
} }
@@ -2562,10 +2553,6 @@ select {
min-width: 200px; min-width: 200px;
} }
.min-w-\[120px\] {
min-width: 120px;
}
.max-w-2xl { .max-w-2xl {
max-width: 42rem; max-width: 42rem;
} }
@@ -2578,6 +2565,10 @@ select {
max-width: 56rem; max-width: 56rem;
} }
.max-w-6xl {
max-width: 72rem;
}
.max-w-7xl { .max-w-7xl {
max-width: 80rem; max-width: 80rem;
} }
@@ -2602,10 +2593,6 @@ select {
max-width: 20rem; max-width: 20rem;
} }
.max-w-6xl {
max-width: 72rem;
}
.flex-1 { .flex-1 {
flex: 1 1 0%; 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)); 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 { .scale-100 {
--tw-scale-x: 1; --tw-scale-x: 1;
--tw-scale-y: 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)); 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 {
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: 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)); 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 {
overflow: auto; overflow: auto;
} }
@@ -2906,6 +2871,10 @@ select {
border-style: dashed; border-style: dashed;
} }
.border-blue-200\/50 {
border-color: rgb(191 219 254 / 0.5);
}
.border-blue-500 { .border-blue-500 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity)); 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-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 { .border-green-500 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(34 197 94 / var(--tw-border-opacity)); border-color: rgb(34 197 94 / var(--tw-border-opacity));
@@ -2954,16 +2928,6 @@ select {
border-color: transparent; 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-t-transparent {
border-top-color: transparent; border-top-color: transparent;
} }
@@ -2973,10 +2937,6 @@ select {
background-color: rgb(0 0 0 / var(--tw-bg-opacity)); background-color: rgb(0 0 0 / var(--tw-bg-opacity));
} }
.bg-black\/20 {
background-color: rgb(0 0 0 / 0.2);
}
.bg-black\/50 { .bg-black\/50 {
background-color: rgb(0 0 0 / 0.5); background-color: rgb(0 0 0 / 0.5);
} }
@@ -3001,6 +2961,10 @@ select {
background-color: rgb(37 99 235 / var(--tw-bg-opacity)); 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 { .bg-gray-100 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity)); 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)); 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 { .bg-green-100 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(220 252 231 / var(--tw-bg-opacity)); 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)); background-color: rgb(22 163 74 / var(--tw-bg-opacity));
} }
.bg-orange-100 { .bg-green-900\/40 {
--tw-bg-opacity: 1; background-color: rgb(20 83 45 / 0.4);
background-color: rgb(255 237 213 / var(--tw-bg-opacity));
} }
.bg-red-100 { .bg-red-100 {
@@ -3065,6 +3042,10 @@ select {
background-color: rgb(255 255 255 / 0.1); background-color: rgb(255 255 255 / 0.1);
} }
.bg-white\/80 {
background-color: rgb(255 255 255 / 0.8);
}
.bg-white\/90 { .bg-white\/90 {
background-color: rgb(255 255 255 / 0.9); background-color: rgb(255 255 255 / 0.9);
} }
@@ -3084,32 +3065,6 @@ select {
background-color: rgb(202 138 4 / var(--tw-bg-opacity)); 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 { .bg-opacity-50 {
--tw-bg-opacity: 0.5; --tw-bg-opacity: 0.5;
} }
@@ -3247,6 +3202,11 @@ select {
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
.py-2\.5 {
padding-top: 0.625rem;
padding-bottom: 0.625rem;
}
.py-3 { .py-3 {
padding-top: 0.75rem; padding-top: 0.75rem;
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
@@ -3267,16 +3227,6 @@ select {
padding-bottom: 2rem; 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 { .pb-4 {
padding-bottom: 1rem; padding-bottom: 1rem;
} }
@@ -3304,6 +3254,11 @@ select {
line-height: 2.5rem; line-height: 2.5rem;
} }
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.text-lg { .text-lg {
font-size: 1.125rem; font-size: 1.125rem;
line-height: 1.75rem; line-height: 1.75rem;
@@ -3324,11 +3279,6 @@ select {
line-height: 1rem; line-height: 1rem;
} }
.text-5xl {
font-size: 3rem;
line-height: 1;
}
.font-bold { .font-bold {
font-weight: 700; font-weight: 700;
} }
@@ -3357,6 +3307,11 @@ select {
line-height: 1.25; line-height: 1.25;
} }
.text-blue-400 {
--tw-text-opacity: 1;
color: rgb(96 165 250 / var(--tw-text-opacity));
}
.text-blue-500 { .text-blue-500 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity)); color: rgb(59 130 246 / var(--tw-text-opacity));
@@ -3367,16 +3322,16 @@ select {
color: rgb(37 99 235 / var(--tw-text-opacity)); 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 { .text-blue-800 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(30 64 175 / var(--tw-text-opacity)); 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 { .text-gray-200 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity)); color: rgb(229 231 235 / var(--tw-text-opacity));
@@ -3412,9 +3367,9 @@ select {
color: rgb(17 24 39 / var(--tw-text-opacity)); color: rgb(17 24 39 / var(--tw-text-opacity));
} }
.text-green-500 { .text-green-400 {
--tw-text-opacity: 1; --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 { .text-green-600 {
@@ -3432,11 +3387,6 @@ select {
color: rgb(22 101 52 / var(--tw-text-opacity)); 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 { .text-primary {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(79 70 229 / var(--tw-text-opacity)); color: rgb(79 70 229 / var(--tw-text-opacity));
@@ -3501,26 +3451,6 @@ select {
color: rgb(133 77 14 / var(--tw-text-opacity)); 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 {
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)); 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 { .hover\:border-gray-300:hover {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity)); 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 { .hover\:bg-blue-700:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity)); 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)); background-color: rgb(249 250 251 / var(--tw-bg-opacity));
} }
.hover\:bg-green-700:hover { .hover\:bg-green-500:hover {
--tw-bg-opacity: 1; --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 { .hover\:bg-red-50:hover {
@@ -3744,6 +3674,11 @@ select {
background-color: rgb(254 242 242 / var(--tw-bg-opacity)); 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 { .hover\:bg-red-700:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity)); background-color: rgb(185 28 28 / var(--tw-bg-opacity));
@@ -3753,16 +3688,16 @@ select {
background-color: rgb(255 255 255 / 0.2); 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 { .hover\:bg-yellow-600:hover {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(202 138 4 / var(--tw-bg-opacity)); 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 { .hover\:text-blue-500:hover {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity)); color: rgb(59 130 246 / var(--tw-text-opacity));
@@ -3783,6 +3718,11 @@ select {
color: rgb(30 64 175 / var(--tw-text-opacity)); 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 { .hover\:text-gray-300:hover {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity)); color: rgb(209 213 219 / var(--tw-text-opacity));
@@ -3798,6 +3738,11 @@ select {
color: rgb(55 65 81 / var(--tw-text-opacity)); 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 { .hover\:text-primary:hover {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(79 70 229 / var(--tw-text-opacity)); color: rgb(79 70 229 / var(--tw-text-opacity));
@@ -3812,16 +3757,6 @@ select {
color: rgb(7 89 133 / var(--tw-text-opacity)); 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 { .hover\:underline:hover {
text-decoration-line: underline; text-decoration-line: underline;
} }
@@ -3867,11 +3802,6 @@ select {
--tw-ring-color: rgb(79 70 229 / 0.5); --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 { .focus\:ring-offset-2:focus {
--tw-ring-offset-width: 2px; --tw-ring-offset-width: 2px;
} }
@@ -3890,6 +3820,10 @@ select {
opacity: 1; opacity: 1;
} }
.dark\:border-blue-700\/50:is(.dark *) {
border-color: rgb(29 78 216 / 0.5);
}
.dark\:border-gray-600:is(.dark *) { .dark\:border-gray-600:is(.dark *) {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity)); border-color: rgb(75 85 99 / var(--tw-border-opacity));
@@ -3904,10 +3838,6 @@ select {
border-color: rgb(55 65 81 / 0.5); 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 *) { .dark\:bg-blue-400\/30:is(.dark *) {
background-color: rgb(96 165 250 / 0.3); background-color: rgb(96 165 250 / 0.3);
} }
@@ -3922,8 +3852,12 @@ select {
background-color: rgb(29 78 216 / var(--tw-bg-opacity)); background-color: rgb(29 78 216 / var(--tw-bg-opacity));
} }
.dark\:bg-blue-900\/50:is(.dark *) { .dark\:bg-blue-900\/30:is(.dark *) {
background-color: rgb(30 58 138 / 0.5); 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 *) { .dark\:bg-gray-600:is(.dark *) {
@@ -3949,41 +3883,33 @@ select {
background-color: rgb(31 41 55 / 0.9); 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 *) { .dark\:bg-green-200:is(.dark *) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(187 247 208 / var(--tw-bg-opacity)); 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 *) { .dark\:bg-green-700:is(.dark *) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(21 128 61 / var(--tw-bg-opacity)); 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 *) { .dark\:bg-red-200:is(.dark *) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(254 202 202 / var(--tw-bg-opacity)); 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; --tw-bg-opacity: 1;
background-color: rgb(239 68 68 / var(--tw-bg-opacity)); background-color: rgb(185 28 28 / var(--tw-bg-opacity));
}
.dark\:bg-red-900\/50:is(.dark *) {
background-color: rgb(127 29 29 / 0.5);
} }
.dark\:bg-yellow-200:is(.dark *) { .dark\:bg-yellow-200:is(.dark *) {
@@ -3995,38 +3921,20 @@ select {
background-color: rgb(250 204 21 / 0.3); 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 *) { .dark\:bg-yellow-600:is(.dark *) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(202 138 4 / var(--tw-bg-opacity)); 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 *) { .dark\:bg-yellow-900\/50:is(.dark *) {
background-color: rgb(113 63 18 / 0.5); 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 *) { .dark\:from-gray-950:is(.dark *) {
--tw-gradient-from: #030712 var(--tw-gradient-from-position); --tw-gradient-from: #030712 var(--tw-gradient-from-position);
--tw-gradient-to: rgb(3 7 18 / 0) var(--tw-gradient-to-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)); 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 *) { .dark\:text-blue-400:is(.dark *) {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(96 165 250 / var(--tw-text-opacity)); color: rgb(96 165 250 / var(--tw-text-opacity));
@@ -4157,16 +4070,6 @@ select {
color: rgb(133 77 14 / var(--tw-text-opacity)); 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 *) { .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-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); --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)); 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 *) { .dark\:hover\:bg-gray-500:hover:is(.dark *) {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(107 114 128 / var(--tw-bg-opacity)); background-color: rgb(107 114 128 / var(--tw-bg-opacity));
@@ -4364,6 +4271,14 @@ select {
} }
@media (min-width: 768px) { @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 { .md\:mb-8 {
margin-bottom: 2rem; margin-bottom: 2rem;
} }

View File

@@ -0,0 +1,150 @@
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<!-- Left Column -->
<div class="space-y-6">
<div>
<label for="id_height_ft" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Height (ft)
</label>
<input type="number"
name="stats.height_ft"
id="id_height_ft"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Total height of the coaster in feet"
min="0"
value="{{ stats.height_ft }}">
</div>
<div>
<label for="id_length_ft" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Length (ft)
</label>
<input type="number"
name="stats.length_ft"
id="id_length_ft"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Total track length in feet"
min="0"
value="{{ stats.length_ft }}">
</div>
<div>
<label for="id_speed_mph" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Speed (mph)
</label>
<input type="number"
name="stats.speed_mph"
id="id_speed_mph"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Maximum speed in miles per hour"
min="0"
value="{{ stats.speed_mph }}">
</div>
<div>
<label for="id_inversions" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Inversions
</label>
<input type="number"
name="stats.inversions"
id="id_inversions"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Number of inversions"
min="0"
value="{{ stats.inversions }}">
</div>
<div>
<label for="id_trains_count" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Number of Trains
</label>
<input type="number"
name="stats.trains_count"
id="id_trains_count"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Number of trains"
min="0"
value="{{ stats.trains_count }}">
</div>
<div>
<label for="id_cars_per_train" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Cars per Train
</label>
<input type="number"
name="stats.cars_per_train"
id="id_cars_per_train"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Number of cars per train"
min="0"
value="{{ stats.cars_per_train }}">
</div>
<div>
<label for="id_seats_per_car" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Seats per Car
</label>
<input type="number"
name="stats.seats_per_car"
id="id_seats_per_car"
class="w-full border-gray-300 rounded-lg form-input dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder="Number of seats per car"
min="0"
value="{{ stats.seats_per_car }}">
</div>
</div>
<!-- Right Column -->
<div class="space-y-6">
<div>
<label for="id_track_material" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Track Material
</label>
<select name="stats.track_material"
id="id_track_material"
class="w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="">Select track material...</option>
<option value="STEEL" {% if stats.track_material == 'STEEL' %}selected{% endif %}>Steel</option>
<option value="WOOD" {% if stats.track_material == 'WOOD' %}selected{% endif %}>Wood</option>
<option value="HYBRID" {% if stats.track_material == 'HYBRID' %}selected{% endif %}>Hybrid</option>
<option value="OTHER" {% if stats.track_material == 'OTHER' %}selected{% endif %}>Other</option>
</select>
</div>
<div>
<label for="id_roller_coaster_type" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Coaster Type
</label>
<select name="stats.roller_coaster_type"
id="id_roller_coaster_type"
class="w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="">Select coaster type...</option>
<option value="SITDOWN" {% if stats.roller_coaster_type == 'SITDOWN' %}selected{% endif %}>Sit-Down</option>
<option value="INVERTED" {% if stats.roller_coaster_type == 'INVERTED' %}selected{% endif %}>Inverted</option>
<option value="FLYING" {% if stats.roller_coaster_type == 'FLYING' %}selected{% endif %}>Flying</option>
<option value="STANDUP" {% if stats.roller_coaster_type == 'STANDUP' %}selected{% endif %}>Stand-Up</option>
<option value="WING" {% if stats.roller_coaster_type == 'WING' %}selected{% endif %}>Wing</option>
<option value="SUSPENDED" {% if stats.roller_coaster_type == 'SUSPENDED' %}selected{% endif %}>Suspended</option>
<option value="BOBSLED" {% if stats.roller_coaster_type == 'BOBSLED' %}selected{% endif %}>Bobsled</option>
<option value="PIPELINE" {% if stats.roller_coaster_type == 'PIPELINE' %}selected{% endif %}>Pipeline</option>
<option value="MOTORBIKE" {% if stats.roller_coaster_type == 'MOTORBIKE' %}selected{% endif %}>Motorbike</option>
<option value="FLOORLESS" {% if stats.roller_coaster_type == 'FLOORLESS' %}selected{% endif %}>Floorless</option>
<option value="DIVE" {% if stats.roller_coaster_type == 'DIVE' %}selected{% endif %}>Dive</option>
<option value="FAMILY" {% if stats.roller_coaster_type == 'FAMILY' %}selected{% endif %}>Family</option>
<option value="WILD_MOUSE" {% if stats.roller_coaster_type == 'WILD_MOUSE' %}selected{% endif %}>Wild Mouse</option>
<option value="SPINNING" {% if stats.roller_coaster_type == 'SPINNING' %}selected{% endif %}>Spinning</option>
<option value="FOURTH_DIMENSION" {% if stats.roller_coaster_type == 'FOURTH_DIMENSION' %}selected{% endif %}>4th Dimension</option>
<option value="OTHER" {% if stats.roller_coaster_type == 'OTHER' %}selected{% endif %}>Other</option>
</select>
</div>
<div>
<label for="id_launch_type" class="block mb-1 text-sm font-medium text-gray-700 dark:text-gray-300">
Launch Type
</label>
<select name="stats.launch_type"
id="id_launch_type"
class="w-full border-gray-300 rounded-lg form-select dark:border-gray-600 dark:bg-gray-700 dark:text-white">
<option value="">Select launch type...</option>
<option value="CHAIN" {% if stats.launch_type == 'CHAIN' %}selected{% endif %}>Chain Lift</option>
<option value="CABLE" {% if stats.launch_type == 'CABLE' %}selected{% endif %}>Cable Launch</option>
<option value="HYDRAULIC" {% if stats.launch_type == 'HYDRAULIC' %}selected{% endif %}>Hydraulic Launch</option>
<option value="LSM" {% if stats.launch_type == 'LSM' %}selected{% endif %}>Linear Synchronous Motor</option>
<option value="LIM" {% if stats.launch_type == 'LIM' %}selected{% endif %}>Linear Induction Motor</option>
<option value="GRAVITY" {% if stats.launch_type == 'GRAVITY' %}selected{% endif %}>Gravity</option>
<option value="OTHER" {% if stats.launch_type == 'OTHER' %}selected{% endif %}>Other</option>
</select>
</div>
</div>
</div>

View File

@@ -5,9 +5,9 @@
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center justify-between p-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50"> <div class="flex items-center justify-between p-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<a href="{% url 'moderation:submission_list' %}?status=NEW" <a href="{% url 'moderation:submission_list' %}?status=PENDING"
class="flex items-center px-4 py-2.5 rounded-lg font-medium transition-all duration-200 {% if request.GET.status == 'NEW' or not request.GET.status %}bg-blue-100 text-blue-900 dark:bg-blue-900/40 dark:text-blue-400{% else %}text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-300{% endif %}" class="flex items-center px-4 py-2.5 rounded-lg font-medium transition-all duration-200 {% if request.GET.status == 'PENDING' or not request.GET.status %}bg-blue-100 text-blue-900 dark:bg-blue-900/40 dark:text-blue-400{% else %}text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-300{% endif %}"
hx-get="{% url 'moderation:submission_list' %}?status=NEW" hx-get="{% url 'moderation:submission_list' %}?status=PENDING"
hx-target="#dashboard-content" hx-target="#dashboard-content"
hx-push-url="true" hx-push-url="true"
hx-indicator="#loading-indicator"> hx-indicator="#loading-indicator">

View File

@@ -0,0 +1,67 @@
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;">
{% if designers %}
{% for designer in designers %}
<button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectDesignerForSubmission('{{ designer.id }}', '{{ designer.name|escapejs }}', '{{ submission_id }}')">
{{ designer.name }}
</button>
{% endfor %}
{% else %}
<div class="px-4 py-2 text-gray-700 dark:text-gray-300">
{% if search_term %}
No designers found
{% else %}
Start typing to search...
{% endif %}
</div>
{% endif %}
</div>
<script>
function selectDesignerForSubmission(id, name, submissionId) {
// Debug logging
console.log('Selecting designer:', {id, name, submissionId});
// Find elements
const designerInput = document.querySelector(`#designer-input-${submissionId}`);
const searchInput = document.querySelector(`#designer-search-${submissionId}`);
const resultsDiv = document.querySelector(`#designer-search-results-${submissionId}`);
// Debug logging
console.log('Found elements:', {
designerInput: designerInput?.id,
searchInput: searchInput?.id,
resultsDiv: resultsDiv?.id
});
// Update hidden input
if (designerInput) {
designerInput.value = id;
console.log('Updated designer input value:', designerInput.value);
}
// Update search input
if (searchInput) {
searchInput.value = name;
console.log('Updated search input value:', searchInput.value);
}
// Clear results
if (resultsDiv) {
resultsDiv.innerHTML = '';
console.log('Cleared results div');
}
}
// Close search results when clicking outside
document.addEventListener('click', function(e) {
const searchResults = document.querySelectorAll('[id^="designer-search-results-"]');
searchResults.forEach(function(resultsDiv) {
const searchInput = document.querySelector(`#designer-search-${resultsDiv.id.split('-').pop()}`);
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
resultsDiv.innerHTML = '';
}
});
});
</script>

View File

@@ -0,0 +1,132 @@
{% load moderation_tags %}
<div class="p-6 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50"
id="edit-form-{{ submission.id }}">
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-300">
Edit Submission
</h3>
<form hx-post="{% url 'moderation:edit_submission' submission.id %}"
hx-target="#submissions-content"
class="space-y-4">
{% for field, value in changes.items %}
{% if field != 'model_name' %}
<div class="p-4 bg-gray-100 border rounded-lg dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300">
{% if field == 'stats' %}
Coaster Stats:
{% elif field == 'park_area' %}
Park Area:
{% elif field == 'ride_model' %}
Ride Model:
{% elif field == 'min_height_in' %}
Minimum Height:
{% elif field == 'max_height_in' %}
Maximum Height:
{% elif field == 'capacity_per_hour' %}
Hourly Capacity:
{% elif field == 'ride_duration_seconds' %}
Ride Duration:
{% elif field == 'opening_date' %}
Opening Date:
{% elif field == 'closing_date' %}
Closing Date:
{% elif field == 'status_since' %}
Status Since:
{% elif field == 'operating_season' %}
Operating Season:
{% elif field == 'size_acres' %}
Size (Acres):
{% elif field == 'post_closing_status' %}
Post-Closing Status:
{% else %}
{{ field|title }}:
{% endif %}
</label>
{% if field == 'stats' %}
<div class="space-y-2">
{% for stat_name, stat_value in value.items %}
<div class="flex items-center gap-2">
<label class="text-sm text-gray-700 dark:text-gray-400">{{ stat_name|title }}:</label>
<input type="text"
name="stats.{{ stat_name }}"
value="{{ stat_value }}"
class="flex-1 px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
</div>
{% endfor %}
</div>
{% elif field == 'park' %}
<select name="{{ field }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
{% for park_id, park_name in parks %}
<option value="{{ park_id }}" {% if park_id == value %}selected{% endif %}>{{ park_name }}</option>
{% endfor %}
</select>
{% elif field == 'designer' %}
<select name="{{ field }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<option value="">None</option>
{% for designer_id, designer_name in designers %}
<option value="{{ designer_id }}" {% if designer_id == value %}selected{% endif %}>{{ designer_name }}</option>
{% endfor %}
</select>
{% elif field == 'manufacturer' %}
<select name="{{ field }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<option value="">None</option>
{% for manufacturer_id, manufacturer_name in manufacturers %}
<option value="{{ manufacturer_id }}" {% if manufacturer_id == value %}selected{% endif %}>{{ manufacturer_name }}</option>
{% endfor %}
</select>
{% elif field == 'ride_model' %}
<select name="{{ field }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<option value="">None</option>
{% for model_id, model_name in ride_models %}
<option value="{{ model_id }}" {% if model_id == value %}selected{% endif %}>{{ model_name }}</option>
{% endfor %}
</select>
{% elif field == 'park_area' %}
<select name="{{ field }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
<option value="">None</option>
{% for area_id, area_name in park_areas %}
<option value="{{ area_id }}" {% if area_id == value %}selected{% endif %}>{{ area_name }}</option>
{% endfor %}
</select>
{% else %}
<input type="{% if field == 'opening_date' or field == 'closing_date' or field == 'status_since' %}date{% else %}text{% endif %}"
name="{{ field }}"
value="{{ value }}"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50">
{% endif %}
</div>
{% endif %}
{% endfor %}
<div class="p-4 border rounded-lg bg-blue-50 dark:bg-blue-900/30 border-blue-200/50 dark:border-blue-700/50">
<label class="block mb-2 text-sm font-medium text-blue-900 dark:text-blue-300">
Notes (required):
</label>
<textarea name="notes"
class="w-full px-3 py-2 text-gray-900 bg-white border rounded-lg resize-none dark:text-gray-300 dark:bg-gray-900 border-gray-200/50 dark:border-gray-700/50"
rows="3"
required
placeholder="Explain why you're editing this submission"></textarea>
</div>
<div class="flex justify-end gap-3">
<button type="button"
class="px-4 py-2 font-medium text-gray-700 transition-all duration-200 bg-gray-100 rounded-lg hover:bg-gray-200 hover:text-gray-900 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
@click="showEditForm = false">
Cancel
</button>
<button type="submit"
class="px-4 py-2 font-medium text-white transition-all duration-200 bg-blue-600 rounded-lg hover:bg-blue-500 dark:bg-blue-700 dark:hover:bg-blue-600">
Save Changes
</button>
</div>
</form>
</div>

View File

@@ -11,7 +11,7 @@
</label> </label>
<select name="status" class="w-full form-select"> <select name="status" class="w-full form-select">
<option value="">All Statuses</option> <option value="">All Statuses</option>
<option value="NEW" {% if request.GET.status == 'NEW' %}selected{% endif %}>Pending</option> <option value="PENDING" {% if request.GET.status == 'PENDING' %}selected{% endif %}>Pending</option>
<option value="APPROVED" {% if request.GET.status == 'APPROVED' %}selected{% endif %}>Approved</option> <option value="APPROVED" {% if request.GET.status == 'APPROVED' %}selected{% endif %}>Approved</option>
<option value="REJECTED" {% if request.GET.status == 'REJECTED' %}selected{% endif %}>Rejected</option> <option value="REJECTED" {% if request.GET.status == 'REJECTED' %}selected{% endif %}>Rejected</option>
<option value="ESCALATED" {% if request.GET.status == 'ESCALATED' %}selected{% endif %}>Escalated</option> <option value="ESCALATED" {% if request.GET.status == 'ESCALATED' %}selected{% endif %}>Escalated</option>

View File

@@ -0,0 +1,67 @@
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;">
{% if manufacturers %}
{% for manufacturer in manufacturers %}
<button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectManufacturerForSubmission('{{ manufacturer.id }}', '{{ manufacturer.name|escapejs }}', '{{ submission_id }}')">
{{ manufacturer.name }}
</button>
{% endfor %}
{% else %}
<div class="px-4 py-2 text-gray-700 dark:text-gray-300">
{% if search_term %}
No manufacturers found
{% else %}
Start typing to search...
{% endif %}
</div>
{% endif %}
</div>
<script>
function selectManufacturerForSubmission(id, name, submissionId) {
// Debug logging
console.log('Selecting manufacturer:', {id, name, submissionId});
// Find elements
const manufacturerInput = document.querySelector(`#manufacturer-input-${submissionId}`);
const searchInput = document.querySelector(`#manufacturer-search-${submissionId}`);
const resultsDiv = document.querySelector(`#manufacturer-search-results-${submissionId}`);
// Debug logging
console.log('Found elements:', {
manufacturerInput: manufacturerInput?.id,
searchInput: searchInput?.id,
resultsDiv: resultsDiv?.id
});
// Update hidden input
if (manufacturerInput) {
manufacturerInput.value = id;
console.log('Updated manufacturer input value:', manufacturerInput.value);
}
// Update search input
if (searchInput) {
searchInput.value = name;
console.log('Updated search input value:', searchInput.value);
}
// Clear results
if (resultsDiv) {
resultsDiv.innerHTML = '';
console.log('Cleared results div');
}
}
// Close search results when clicking outside
document.addEventListener('click', function(e) {
const searchResults = document.querySelectorAll('[id^="manufacturer-search-results-"]');
searchResults.forEach(function(resultsDiv) {
const searchInput = document.querySelector(`#manufacturer-search-${resultsDiv.id.split('-').pop()}`);
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
resultsDiv.innerHTML = '';
}
});
});
</script>

View File

@@ -0,0 +1,73 @@
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;">
{% if parks %}
{% for park in parks %}
<button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectParkForSubmission('{{ park.id }}', '{{ park.name|escapejs }}', '{{ submission_id }}')">
{{ park.name }}
</button>
{% endfor %}
{% else %}
<div class="px-4 py-2 text-gray-700 dark:text-gray-300">
{% if search_term %}
No parks found
{% else %}
Start typing to search...
{% endif %}
</div>
{% endif %}
</div>
<script>
function selectParkForSubmission(id, name, submissionId) {
// Debug logging
console.log('Selecting park:', {id, name, submissionId});
// Find elements
const parkInput = document.querySelector(`#park-input-${submissionId}`);
const searchInput = document.querySelector(`#park-search-${submissionId}`);
const resultsDiv = document.querySelector(`#park-search-results-${submissionId}`);
// Debug logging
console.log('Found elements:', {
parkInput: parkInput?.id,
searchInput: searchInput?.id,
resultsDiv: resultsDiv?.id
});
// Update hidden input
if (parkInput) {
parkInput.value = id;
console.log('Updated park input value:', parkInput.value);
}
// Update search input
if (searchInput) {
searchInput.value = name;
console.log('Updated search input value:', searchInput.value);
}
// Clear results
if (resultsDiv) {
resultsDiv.innerHTML = '';
console.log('Cleared results div');
}
// Trigger park areas update
if (parkInput) {
htmx.trigger(parkInput, 'change');
console.log('Triggered change event');
}
}
// Close search results when clicking outside
document.addEventListener('click', function(e) {
const searchResults = document.querySelectorAll('[id^="park-search-results-"]');
searchResults.forEach(function(resultsDiv) {
const searchInput = document.querySelector(`#park-search-${resultsDiv.id.split('-').pop()}`);
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
resultsDiv.innerHTML = '';
}
});
});
</script>

View File

@@ -3,10 +3,14 @@
<div> <div>
<h3 class="flex items-center gap-2 text-lg font-semibold text-gray-900 dark:text-white"> <h3 class="flex items-center gap-2 text-lg font-semibold text-gray-900 dark:text-white">
<span class="status-badge <span class="status-badge
{% if submission.status == 'NEW' %}status-pending {% if submission.status == 'PENDING' %}status-pending
{% elif submission.status == 'APPROVED' %}status-approved {% elif submission.status == 'APPROVED' %}status-approved
{% elif submission.status == 'REJECTED' %}status-rejected {% elif submission.status == 'REJECTED' %}status-rejected
{% elif submission.status == 'AUTO_APPROVED' %}status-approved{% endif %}"> {% elif submission.status == 'ESCALATED' %}status-escalated{% endif %}">
<i class="mr-1.5 fas fa-{% if submission.status == 'PENDING' %}clock
{% elif submission.status == 'APPROVED' %}check
{% elif submission.status == 'REJECTED' %}times
{% elif submission.status == 'ESCALATED' %}exclamation{% endif %}"></i>
{{ submission.get_status_display }} {{ submission.get_status_display }}
</span> </span>
Photo for {{ submission.content_object }} Photo for {{ submission.content_object }}
@@ -45,7 +49,14 @@
</div> </div>
{% endif %} {% endif %}
{% if submission.status == 'NEW' %} {% if submission.notes %}
<div class="p-4 mt-4 border rounded-lg bg-blue-50 dark:bg-blue-900/30 border-blue-200/50 dark:border-blue-700/50">
<div class="text-sm font-medium text-blue-900 dark:text-blue-300">Review Notes:</div>
<div class="mt-1.5 text-blue-800 dark:text-blue-200">{{ submission.notes }}</div>
</div>
{% endif %}
{% if submission.status == 'PENDING' or submission.status == 'ESCALATED' and user.role in 'ADMIN,SUPERUSER' %}
<div class="mt-4 review-notes" x-data="{ showNotes: false }"> <div class="mt-4 review-notes" x-data="{ showNotes: false }">
<textarea x-show="showNotes" <textarea x-show="showNotes"
name="notes" name="notes"
@@ -60,6 +71,7 @@
Add Notes Add Notes
</button> </button>
{% if submission.status != 'ESCALATED' or user.role in 'ADMIN,SUPERUSER' %}
<button class="btn-approve" <button class="btn-approve"
hx-post="{% url 'moderation:approve_photo' submission.id %}" hx-post="{% url 'moderation:approve_photo' submission.id %}"
hx-target="#submission-{{ submission.id }}" hx-target="#submission-{{ submission.id }}"
@@ -79,6 +91,19 @@
<i class="mr-2 fas fa-times"></i> <i class="mr-2 fas fa-times"></i>
Reject Reject
</button> </button>
{% endif %}
{% if user.role == 'MODERATOR' and submission.status != 'ESCALATED' %}
<button class="btn-escalate"
hx-post="{% url 'moderation:escalate_photo' submission.id %}"
hx-target="#submission-{{ submission.id }}"
hx-include="closest .review-notes"
hx-confirm="Are you sure you want to escalate this photo?"
hx-indicator="#loading-indicator">
<i class="mr-2 fas fa-arrow-up"></i>
Escalate
</button>
{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@@ -0,0 +1,67 @@
<div class="w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg dark:bg-gray-700 dark:border-gray-600" style="max-height: 240px; overflow-y: auto;">
{% if ride_models %}
{% for model in ride_models %}
<button type="button"
class="w-full px-4 py-2 text-left text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-600"
onclick="selectRideModelForSubmission('{{ model.id }}', '{{ model.name|escapejs }}', '{{ submission_id }}')">
{{ model.name }}
</button>
{% endfor %}
{% else %}
<div class="px-4 py-2 text-gray-700 dark:text-gray-300">
{% if search_term %}
No ride models found
{% else %}
Start typing to search...
{% endif %}
</div>
{% endif %}
</div>
<script>
function selectRideModelForSubmission(id, name, submissionId) {
// Debug logging
console.log('Selecting ride model:', {id, name, submissionId});
// Find elements
const modelInput = document.querySelector(`#ride-model-input-${submissionId}`);
const searchInput = document.querySelector(`#ride-model-search-${submissionId}`);
const resultsDiv = document.querySelector(`#ride-model-search-results-${submissionId}`);
// Debug logging
console.log('Found elements:', {
modelInput: modelInput?.id,
searchInput: searchInput?.id,
resultsDiv: resultsDiv?.id
});
// Update hidden input
if (modelInput) {
modelInput.value = id;
console.log('Updated ride model input value:', modelInput.value);
}
// Update search input
if (searchInput) {
searchInput.value = name;
console.log('Updated search input value:', searchInput.value);
}
// Clear results
if (resultsDiv) {
resultsDiv.innerHTML = '';
console.log('Cleared results div');
}
}
// Close search results when clicking outside
document.addEventListener('click', function(e) {
const searchResults = document.querySelectorAll('[id^="ride-model-search-results-"]');
searchResults.forEach(function(resultsDiv) {
const searchInput = document.querySelector(`#ride-model-search-${resultsDiv.id.split('-').pop()}`);
if (!resultsDiv.contains(e.target) && e.target !== searchInput) {
resultsDiv.innerHTML = '';
}
});
});
</script>