mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:51:09 -05:00
code commit
This commit is contained in:
BIN
media/submissions/photos/coaster_track.gif
Normal file
BIN
media/submissions/photos/coaster_track.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/park_entrance.gif
Normal file
BIN
media/submissions/photos/park_entrance.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_image.gif
Normal file
BIN
media/submissions/photos/test_image.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_image2.gif
Normal file
BIN
media/submissions/photos/test_image2.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_image2_ruT57k4.gif
Normal file
BIN
media/submissions/photos/test_image2_ruT57k4.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 B |
BIN
media/submissions/photos/test_image_iI0mcgf.gif
Normal file
BIN
media/submissions/photos/test_image_iI0mcgf.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 B |
0
moderation/management/__init__.py
Normal file
0
moderation/management/__init__.py
Normal file
0
moderation/management/commands/__init__.py
Normal file
0
moderation/management/commands/__init__.py
Normal file
228
moderation/management/commands/seed_submissions.py
Normal file
228
moderation/management/commands/seed_submissions.py
Normal 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'))
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
32
moderation/migrations/0003_update_existing_statuses.py
Normal file
32
moderation/migrations/0003_update_existing_statuses.py
Normal 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),
|
||||
]
|
||||
22
moderation/migrations/0004_add_moderator_changes.py
Normal file
22
moderation/migrations/0004_add_moderator_changes.py
Normal 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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -15,7 +15,7 @@ UserType = Union[AbstractBaseUser, AnonymousUser]
|
||||
|
||||
class EditSubmission(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
("NEW", "New"),
|
||||
("PENDING", "Pending"),
|
||||
("APPROVED", "Approved"),
|
||||
("REJECTED", "Rejected"),
|
||||
("ESCALATED", "Escalated"),
|
||||
@@ -50,12 +50,19 @@ class EditSubmission(models.Model):
|
||||
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()
|
||||
|
||||
54
moderation/templatetags/moderation_tags.py
Normal file
54
moderation/templatetags/moderation_tags.py
Normal 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
|
||||
@@ -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/<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>/reject/', views.reject_submission, name='reject_submission'),
|
||||
path('submissions/<int:submission_id>/escalate/', views.escalate_submission, name='escalate_submission'),
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
150
templates/moderation/partials/coaster_fields.html
Normal file
150
templates/moderation/partials/coaster_fields.html
Normal 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>
|
||||
@@ -5,9 +5,9 @@
|
||||
<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 space-x-4">
|
||||
<a href="{% url 'moderation:submission_list' %}?status=NEW"
|
||||
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 %}"
|
||||
hx-get="{% 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 == '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=PENDING"
|
||||
hx-target="#dashboard-content"
|
||||
hx-push-url="true"
|
||||
hx-indicator="#loading-indicator">
|
||||
|
||||
67
templates/moderation/partials/designer_search_results.html
Normal file
67
templates/moderation/partials/designer_search_results.html
Normal 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>
|
||||
132
templates/moderation/partials/edit_submission_form.html
Normal file
132
templates/moderation/partials/edit_submission_form.html
Normal 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>
|
||||
@@ -11,7 +11,7 @@
|
||||
</label>
|
||||
<select name="status" class="w-full form-select">
|
||||
<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="REJECTED" {% if request.GET.status == 'REJECTED' %}selected{% endif %}>Rejected</option>
|
||||
<option value="ESCALATED" {% if request.GET.status == 'ESCALATED' %}selected{% endif %}>Escalated</option>
|
||||
|
||||
@@ -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>
|
||||
73
templates/moderation/partials/park_search_results.html
Normal file
73
templates/moderation/partials/park_search_results.html
Normal 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>
|
||||
@@ -3,10 +3,14 @@
|
||||
<div>
|
||||
<h3 class="flex items-center gap-2 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
<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 == '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 }}
|
||||
</span>
|
||||
Photo for {{ submission.content_object }}
|
||||
@@ -45,7 +49,14 @@
|
||||
</div>
|
||||
{% 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 }">
|
||||
<textarea x-show="showNotes"
|
||||
name="notes"
|
||||
@@ -60,6 +71,7 @@
|
||||
Add Notes
|
||||
</button>
|
||||
|
||||
{% if submission.status != 'ESCALATED' or user.role in 'ADMIN,SUPERUSER' %}
|
||||
<button class="btn-approve"
|
||||
hx-post="{% url 'moderation:approve_photo' submission.id %}"
|
||||
hx-target="#submission-{{ submission.id }}"
|
||||
@@ -79,6 +91,19 @@
|
||||
<i class="mr-2 fas fa-times"></i>
|
||||
Reject
|
||||
</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>
|
||||
{% endif %}
|
||||
|
||||
67
templates/moderation/partials/ride_model_search_results.html
Normal file
67
templates/moderation/partials/ride_model_search_results.html
Normal 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>
|
||||
Reference in New Issue
Block a user