This commit is contained in:
pacnpal
2025-09-21 20:04:42 -04:00
parent 42a3dc7637
commit 75cc618c2b
610 changed files with 1719 additions and 4816 deletions

314
apps/moderation/mixins.py Normal file
View File

@@ -0,0 +1,314 @@
from typing import Any, Dict, Optional, Type, cast
from django.contrib.auth.mixins import UserPassesTestMixin
from django.contrib.contenttypes.models import ContentType
from django.http import (
JsonResponse,
HttpResponseForbidden,
HttpRequest,
HttpResponse,
)
from django.views.generic import DetailView
from django.db import models
from django.contrib.auth import get_user_model
import json
from .models import EditSubmission, PhotoSubmission, UserType
User = get_user_model()
class EditSubmissionMixin(DetailView):
"""
Mixin for handling edit submissions with proper moderation.
"""
model: Optional[Type[models.Model]] = None
def handle_edit_submission(
self,
request: HttpRequest,
changes: Dict[str, Any],
reason: str = "",
source: str = "",
submission_type: str = "EDIT",
) -> JsonResponse:
"""
Handle an edit submission based on user's role.
Args:
request: The HTTP request
changes: Dict of field changes {field_name: new_value}
reason: Why this edit is needed
source: Source of information (optional)
submission_type: 'EDIT' or 'CREATE'
Returns:
JsonResponse with status and message
"""
if not request.user.is_authenticated:
return JsonResponse(
{
"status": "error",
"message": "You must be logged in to make edits.",
},
status=403,
)
if not self.model:
raise ValueError("model attribute must be set")
content_type = ContentType.objects.get_for_model(self.model)
# Create the submission
submission = EditSubmission(
user=request.user,
content_type=content_type,
submission_type=submission_type,
changes=changes,
reason=reason,
source=source,
)
# For edits, set the object_id
if submission_type == "EDIT":
obj = self.get_object()
submission.object_id = getattr(obj, "id", None)
# Auto-approve for moderators and above
user_role = getattr(request.user, "role", None)
if user_role in ["MODERATOR", "ADMIN", "SUPERUSER"]:
obj = submission.approve(cast(UserType, request.user))
return JsonResponse(
{
"status": "success",
"message": "Changes saved successfully.",
"auto_approved": True,
"redirect_url": getattr(obj, "get_absolute_url", lambda: None)(),
}
)
# Submit for approval for regular users
submission.save()
return JsonResponse(
{
"status": "success",
"message": "Your changes have been submitted for approval.",
"auto_approved": False,
}
)
def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> JsonResponse:
"""Handle POST requests for editing"""
if not request.user.is_authenticated:
return JsonResponse(
{
"status": "error",
"message": "You must be logged in to make edits.",
},
status=403,
)
try:
data = json.loads(request.body)
changes = data.get("changes", {})
reason = data.get("reason", "")
source = data.get("source", "")
submission_type = data.get("submission_type", "EDIT")
if not changes:
return JsonResponse(
{"status": "error", "message": "No changes provided."},
status=400,
)
user_role = getattr(request.user, "role", None)
if not reason and user_role == "USER":
return JsonResponse(
{
"status": "error",
"message": "Please provide a reason for your changes.",
},
status=400,
)
return self.handle_edit_submission(
request, changes, reason, source, submission_type
)
except json.JSONDecodeError:
return JsonResponse(
{"status": "error", "message": "Invalid JSON data."},
status=400,
)
except Exception as e:
return JsonResponse({"status": "error", "message": str(e)}, status=500)
class PhotoSubmissionMixin(DetailView):
"""
Mixin for handling photo submissions with proper moderation.
"""
model: Optional[Type[models.Model]] = None
def handle_photo_submission(self, request: HttpRequest) -> JsonResponse:
"""Handle a photo submission based on user's role"""
if not request.user.is_authenticated:
return JsonResponse(
{
"status": "error",
"message": "You must be logged in to upload photos.",
},
status=403,
)
if not self.model:
raise ValueError("model attribute must be set")
try:
obj = self.get_object()
except (AttributeError, self.model.DoesNotExist):
return JsonResponse(
{"status": "error", "message": "Invalid object."}, status=400
)
if not request.FILES.get("photo"):
return JsonResponse(
{"status": "error", "message": "No photo provided."},
status=400,
)
content_type = ContentType.objects.get_for_model(obj)
submission = PhotoSubmission(
user=request.user,
content_type=content_type,
object_id=getattr(obj, "id", None),
photo=request.FILES["photo"],
caption=request.POST.get("caption", ""),
date_taken=request.POST.get("date_taken"),
)
# Auto-approve for moderators and above
user_role = getattr(request.user, "role", None)
if user_role in ["MODERATOR", "ADMIN", "SUPERUSER"]:
submission.auto_approve()
return JsonResponse(
{
"status": "success",
"message": "Photo uploaded successfully.",
"auto_approved": True,
}
)
# Submit for approval for regular users
submission.save()
return JsonResponse(
{
"status": "success",
"message": "Your photo has been submitted for approval.",
"auto_approved": False,
}
)
class ModeratorRequiredMixin(UserPassesTestMixin):
"""Require moderator or higher role for access"""
request: Optional[HttpRequest] = None
def test_func(self) -> bool:
if not self.request:
return False
user_role = getattr(self.request.user, "role", None)
return self.request.user.is_authenticated and user_role in [
"MODERATOR",
"ADMIN",
"SUPERUSER",
]
def handle_no_permission(self) -> HttpResponse:
if not self.request or not self.request.user.is_authenticated:
return super().handle_no_permission()
return HttpResponseForbidden("You must be a moderator to access this page.")
class AdminRequiredMixin(UserPassesTestMixin):
"""Require admin or superuser role for access"""
request: Optional[HttpRequest] = None
def test_func(self) -> bool:
if not self.request:
return False
user_role = getattr(self.request.user, "role", None)
return self.request.user.is_authenticated and user_role in [
"ADMIN",
"SUPERUSER",
]
def handle_no_permission(self) -> HttpResponse:
if not self.request or not self.request.user.is_authenticated:
return super().handle_no_permission()
return HttpResponseForbidden("You must be an admin to access this page.")
class InlineEditMixin:
"""Add inline editing context to views"""
request: Optional[HttpRequest] = None
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs) # type: ignore
if self.request and self.request.user.is_authenticated:
context["can_edit"] = True
user_role = getattr(self.request.user, "role", None)
context["can_auto_approve"] = user_role in [
"MODERATOR",
"ADMIN",
"SUPERUSER",
]
if isinstance(self, DetailView):
obj = self.get_object() # type: ignore
context["pending_edits"] = (
EditSubmission.objects.filter(
content_type=ContentType.objects.get_for_model(obj.__class__),
object_id=getattr(obj, "id", None),
status="NEW",
)
.select_related("user")
.order_by("-created_at")
)
return context
class HistoryMixin:
"""Add edit history context to views"""
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
context = super().get_context_data(**kwargs) # type: ignore
# Only add history context for DetailViews
if isinstance(self, DetailView):
obj = self.get_object() # type: ignore
# Get historical records ordered by date if available
try:
# Use pghistory's get_history method
context["history"] = obj.get_history()
except (AttributeError, TypeError):
context["history"] = []
# Get related edit submissions
content_type = ContentType.objects.get_for_model(obj.__class__)
context["edit_submissions"] = (
EditSubmission.objects.filter(
content_type=content_type,
object_id=getattr(obj, "id", None),
)
.exclude(status="NEW")
.select_related("user", "handled_by")
.order_by("-created_at")
)
return context