mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 08:51:09 -05:00
feat: complete monorepo structure with frontend and shared resources
- Add complete backend/ directory with full Django application - Add frontend/ directory with Vite + TypeScript setup ready for Next.js - Add comprehensive shared/ directory with: - Complete documentation and memory-bank archives - Media files and avatars (letters, park/ride images) - Deployment scripts and automation tools - Shared types and utilities - Add architecture/ directory with migration guides - Configure pnpm workspace for monorepo development - Update .gitignore to exclude .django_tailwind_cli/ build artifacts - Preserve all historical documentation in shared/docs/memory-bank/ - Set up proper structure for full-stack development with shared resources
This commit is contained in:
429
backend/apps/moderation/views.py
Normal file
429
backend/apps/moderation/views.py
Normal file
@@ -0,0 +1,429 @@
|
||||
from django.views.generic import ListView
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.http import HttpResponse, HttpRequest
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import QuerySet
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from typing import Optional, Any, Dict, List, Tuple, cast
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
import json
|
||||
from apps.accounts.models import User
|
||||
|
||||
from .models import EditSubmission, PhotoSubmission
|
||||
from apps.parks.models import Park, ParkArea
|
||||
from apps.rides.models import RideModel
|
||||
|
||||
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()],
|
||||
"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_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()
|
||||
Reference in New Issue
Block a user