mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 04:11:09 -05:00
code commit
This commit is contained in:
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"),
|
||||
@@ -49,13 +49,20 @@ class EditSubmission(models.Model):
|
||||
changes = models.JSONField(
|
||||
help_text="JSON representation of the changes or new object data"
|
||||
)
|
||||
|
||||
# Moderator's edited version of changes before approval
|
||||
moderator_changes = models.JSONField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Moderator's edited version of the changes before approval"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
reason = models.TextField(help_text="Why this edit/addition is needed")
|
||||
source = models.TextField(
|
||||
blank=True, help_text="Source of information (if applicable)"
|
||||
)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="NEW")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="PENDING")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
# Review details
|
||||
@@ -133,7 +140,10 @@ class EditSubmission(models.Model):
|
||||
raise ValueError("Could not resolve model class")
|
||||
|
||||
try:
|
||||
resolved_data = self._resolve_foreign_keys(self.changes)
|
||||
# Use moderator_changes if available, otherwise use original changes
|
||||
changes_to_apply = self.moderator_changes if self.moderator_changes is not None else self.changes
|
||||
|
||||
resolved_data = self._resolve_foreign_keys(changes_to_apply)
|
||||
prepared_data = self._prepare_model_data(resolved_data, model_class)
|
||||
|
||||
# For CREATE submissions, check for duplicates by name
|
||||
@@ -168,7 +178,7 @@ class EditSubmission(models.Model):
|
||||
return obj
|
||||
except Exception as e:
|
||||
if self.status != "REJECTED": # Don't override if already rejected due to duplicate
|
||||
self.status = "NEW" # Reset status if approval failed
|
||||
self.status = "PENDING" # Reset status if approval failed
|
||||
self.save()
|
||||
raise ValueError(f"Error approving submission: {str(e)}") from e
|
||||
|
||||
@@ -189,10 +199,10 @@ class EditSubmission(models.Model):
|
||||
|
||||
class PhotoSubmission(models.Model):
|
||||
STATUS_CHOICES = [
|
||||
("NEW", "New"),
|
||||
("PENDING", "Pending"),
|
||||
("APPROVED", "Approved"),
|
||||
("REJECTED", "Rejected"),
|
||||
("AUTO_APPROVED", "Auto Approved"),
|
||||
("ESCALATED", "Escalated"),
|
||||
]
|
||||
|
||||
# Who submitted the photo
|
||||
@@ -213,7 +223,7 @@ class PhotoSubmission(models.Model):
|
||||
date_taken = models.DateField(null=True, blank=True)
|
||||
|
||||
# Metadata
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="NEW")
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="PENDING")
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
# Review details
|
||||
@@ -268,22 +278,10 @@ class PhotoSubmission(models.Model):
|
||||
self.notes = notes
|
||||
self.save()
|
||||
|
||||
def auto_approve(self) -> None:
|
||||
"""Auto-approve the photo submission (for moderators/admins)"""
|
||||
from media.models import Photo
|
||||
|
||||
self.status = "AUTO_APPROVED"
|
||||
self.handled_by = self.user
|
||||
def escalate(self, moderator: UserType, notes: str = "") -> None:
|
||||
"""Escalate the photo submission to admin"""
|
||||
self.status = "ESCALATED"
|
||||
self.handled_by = moderator # type: ignore
|
||||
self.handled_at = timezone.now()
|
||||
|
||||
# Create the approved photo
|
||||
Photo.objects.create(
|
||||
uploaded_by=self.user,
|
||||
content_type=self.content_type,
|
||||
object_id=self.object_id,
|
||||
image=self.photo,
|
||||
caption=self.caption,
|
||||
is_approved=True,
|
||||
)
|
||||
|
||||
self.notes = notes
|
||||
self.save()
|
||||
|
||||
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':
|
||||
|
||||
Reference in New Issue
Block a user