from django.views.generic import ListView, TemplateView from django.shortcuts import get_object_or_404, render from django.http import HttpResponse, JsonResponse, HttpRequest from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.contrib.auth.decorators import login_required from django.template.loader import render_to_string from django.db.models import Q, QuerySet from django.core.exceptions import PermissionDenied from typing import Optional, Any, Dict, List, Tuple, Union, cast from django.db import models from django.core.serializers.json import DjangoJSONEncoder import json from accounts.models import User from .models import EditSubmission, PhotoSubmission from parks.models import Park, ParkArea from designers.models import Designer from manufacturers.models import Manufacturer from rides.models import RideModel from location.models import Location MODERATOR_ROLES = ['MODERATOR', 'ADMIN', 'SUPERUSER'] class ModeratorRequiredMixin(UserPassesTestMixin): request: HttpRequest def test_func(self) -> bool: """Check if user has moderator permissions.""" user = cast(User, self.request.user) return user.is_authenticated and (user.role in MODERATOR_ROLES or user.is_superuser) def handle_no_permission(self) -> HttpResponse: if not self.request.user.is_authenticated: return super().handle_no_permission() raise PermissionDenied("You do not have moderator permissions.") def get_filtered_queryset(request: HttpRequest, status: str, submission_type: str) -> QuerySet: """Get filtered queryset based on request parameters.""" if submission_type == 'photo': return PhotoSubmission.objects.filter(status=status).order_by('-created_at') queryset = EditSubmission.objects.filter(status=status).order_by('-created_at') if type_filter := request.GET.get('type'): queryset = queryset.filter(submission_type=type_filter) if content_type := request.GET.get('content_type'): queryset = queryset.filter(content_type__model=content_type) return queryset def get_context_data(request: HttpRequest, queryset: QuerySet) -> Dict[str, Any]: """Get common context data for views.""" park_areas_by_park: Dict[int, List[Tuple[int, str]]] = {} if isinstance(queryset.first(), EditSubmission): for submission in queryset: if (submission.content_type.model == 'park' and isinstance(submission.changes, dict) and 'park' in submission.changes): park_id = submission.changes['park'] if park_id not in park_areas_by_park: areas = ParkArea.objects.filter(park_id=park_id) park_areas_by_park[park_id] = [(area.pk, str(area)) for area in areas] return { 'submissions': queryset, 'user': request.user, 'parks': [(park.pk, str(park)) for park in Park.objects.all()], 'designers': [(designer.pk, str(designer)) for designer in Designer.objects.all()], 'manufacturers': [(manufacturer.pk, str(manufacturer)) for manufacturer in Manufacturer.objects.all()], 'ride_models': [(model.pk, str(model)) for model in RideModel.objects.all()], 'owners': [(user.pk, str(user)) for user in User.objects.filter(role__in=['OWNER', 'ADMIN', 'SUPERUSER'])], 'park_areas_by_park': park_areas_by_park } @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') parks = Park.objects.all().order_by('name') if query: parks = parks.filter(name__icontains=query) parks = parks[:10] return render(request, 'moderation/partials/park_search_results.html', { 'parks': parks, 'search_term': query, 'submission_id': submission_id }) @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') manufacturers = Manufacturer.objects.all().order_by('name') if query: manufacturers = manufacturers.filter(name__icontains=query) manufacturers = manufacturers[:10] return render(request, 'moderation/partials/manufacturer_search_results.html', { 'manufacturers': manufacturers, 'search_term': query, 'submission_id': submission_id }) @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') designers = Designer.objects.all().order_by('name') if query: designers = designers.filter(name__icontains=query) designers = designers[:10] return render(request, 'moderation/partials/designer_search_results.html', { 'designers': designers, 'search_term': query, 'submission_id': submission_id }) @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 query: queryset = queryset.filter(name__icontains=query) queryset = queryset.order_by('name')[:10] return render(request, 'moderation/partials/ride_model_search_results.html', { 'ride_models': queryset, 'search_term': query, 'submission_id': submission_id }) class DashboardView(LoginRequiredMixin, ModeratorRequiredMixin, ListView): template_name = 'moderation/dashboard.html' context_object_name = 'submissions' paginate_by = 10 def get_template_names(self) -> List[str]: if self.request.headers.get('HX-Request'): return ['moderation/partials/dashboard_content.html'] return [self.template_name] def get_queryset(self) -> QuerySet: status = self.request.GET.get('status', 'PENDING') submission_type = self.request.GET.get('submission_type', '') return get_filtered_queryset(self.request, status, submission_type) @login_required def submission_list(request: HttpRequest) -> HttpResponse: """View for submission list with filters""" user = cast(User, request.user) if not (user.role in MODERATOR_ROLES or user.is_superuser): return HttpResponse(status=403) status = request.GET.get('status', 'PENDING') submission_type = request.GET.get('submission_type', '') queryset = get_filtered_queryset(request, status, submission_type) # Process location data for park submissions for submission in queryset: if (submission.content_type.model == 'park' and isinstance(submission.changes, dict)): # Extract location fields into a location object location_fields = ['latitude', 'longitude', 'street_address', 'city', 'state', 'postal_code', 'country'] location_data = {field: submission.changes.get(field) for field in location_fields} # Add location data back as a single object submission.changes['location'] = location_data context = get_context_data(request, queryset) template_name = ('moderation/partials/dashboard_content.html' if request.headers.get('HX-Request') else 'moderation/dashboard.html') return render(request, template_name, 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': return HttpResponse("Invalid request method", status=405) notes = request.POST.get('notes') if not notes: return HttpResponse("Notes are required when editing a submission", status=400) try: edited_changes = dict(submission.changes) if submission.changes else {} # Update stats if present if 'stats' in edited_changes: edited_stats = {} for key in edited_changes['stats']: if new_value := request.POST.get(f'stats.{key}'): edited_stats[key] = new_value edited_changes['stats'] = edited_stats # Update location fields if present if submission.content_type.model == 'park': location_fields = ['latitude', 'longitude', 'street_address', 'city', 'state', 'postal_code', 'country'] location_data = {} for field in location_fields: if new_value := request.POST.get(field): if field in ['latitude', 'longitude']: try: location_data[field] = float(new_value) except ValueError: return HttpResponse(f"Invalid value for {field}", status=400) else: location_data[field] = new_value if location_data: edited_changes.update(location_data) # Update other fields for field in edited_changes: if field == 'stats' or field in ['latitude', 'longitude', 'street_address', 'city', 'state', 'postal_code', 'country']: continue if new_value := request.POST.get(field): if field in ['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 # Convert to JSON-serializable format json_changes = json.loads(json.dumps(edited_changes, cls=DjangoJSONEncoder)) submission.moderator_changes = json_changes submission.notes = notes submission.save() # Process location data for display if submission.content_type.model == 'park': location_fields = ['latitude', 'longitude', 'street_address', 'city', 'state', 'postal_code', 'country'] location_data = {field: json_changes.get(field) for field in location_fields} # Add location data back as a single object json_changes['location'] = location_data submission.changes = json_changes context = get_context_data(request, EditSubmission.objects.filter(id=submission_id)) return render(request, 'moderation/partials/submission_list.html', context) except Exception as e: return HttpResponse(str(e), status=400) @login_required def approve_submission(request: HttpRequest, submission_id: int) -> HttpResponse: """HTMX endpoint for approving a submission""" user = cast(User, request.user) submission = get_object_or_404(EditSubmission, id=submission_id) if not ((submission.status != 'ESCALATED' and user.role in MODERATOR_ROLES) or user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser): return HttpResponse("Insufficient permissions", status=403) try: submission.approve(user) _update_submission_notes(submission, request.POST.get('notes')) status = request.GET.get('status', 'PENDING') submission_type = request.GET.get('submission_type', '') queryset = get_filtered_queryset(request, status, submission_type) return render(request, 'moderation/partials/dashboard_content.html', { 'submissions': queryset, 'user': request.user, }) except ValueError as e: return HttpResponse(str(e), status=400) @login_required def reject_submission(request: HttpRequest, submission_id: int) -> HttpResponse: """HTMX endpoint for rejecting a submission""" user = cast(User, request.user) submission = get_object_or_404(EditSubmission, id=submission_id) if not ((submission.status != 'ESCALATED' and user.role in MODERATOR_ROLES) or user.role in ['ADMIN', 'SUPERUSER'] or user.is_superuser): return HttpResponse("Insufficient permissions", status=403) submission.reject(user) _update_submission_notes(submission, request.POST.get('notes')) status = request.GET.get('status', 'PENDING') submission_type = request.GET.get('submission_type', '') queryset = get_filtered_queryset(request, status, submission_type) context = get_context_data(request, queryset) return render(request, 'moderation/partials/submission_list.html', context) @login_required def escalate_submission(request: HttpRequest, submission_id: int) -> HttpResponse: """HTMX endpoint for escalating 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 submission.status == "ESCALATED": return HttpResponse("Submission is already escalated", status=400) submission.escalate(user) _update_submission_notes(submission, request.POST.get("notes")) status = request.GET.get("status", "PENDING") submission_type = request.GET.get("submission_type", "") queryset = get_filtered_queryset(request, status, submission_type) return render(request, "moderation/partials/dashboard_content.html", { "submissions": queryset, "user": request.user, }) @login_required def approve_photo(request: HttpRequest, submission_id: int) -> HttpResponse: """HTMX endpoint for approving a photo 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(PhotoSubmission, id=submission_id) try: submission.approve(user, request.POST.get("notes", "")) return render(request, "moderation/partials/photo_submission.html", {"submission": submission}) except Exception as e: return HttpResponse(str(e), status=400) @login_required def reject_photo(request: HttpRequest, submission_id: int) -> HttpResponse: """HTMX endpoint for rejecting a photo 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(PhotoSubmission, id=submission_id) submission.reject(user, request.POST.get("notes", "")) return render(request, "moderation/partials/photo_submission.html", {"submission": submission}) def _update_submission_notes(submission: EditSubmission, notes: Optional[str]) -> None: """Update submission notes if provided.""" if notes: submission.notes = notes submission.save()