Files
thrillwiki_django_no_react/moderation/views.py
pacnpal 751cd86a31 Add operators and property owners functionality
- Implemented OperatorListView and OperatorDetailView for managing operators.
- Created corresponding templates for operator listing and detail views.
- Added PropertyOwnerListView and PropertyOwnerDetailView for managing property owners.
- Developed templates for property owner listing and detail views.
- Established relationships between parks and operators, and parks and property owners in the models.
- Created migrations to reflect the new relationships and fields in the database.
- Added admin interfaces for PropertyOwner management.
- Implemented tests for operators and property owners.
2025-07-04 14:49:36 -04:00

387 lines
16 KiB
Python

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()