mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:11:08 -05:00
- 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
315 lines
10 KiB
Python
315 lines
10 KiB
Python
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
|