mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-30 06:27:01 -05:00
feat: Implement MFA authentication, add ride statistics model, and update various services, APIs, and tests across the application.
This commit is contained in:
@@ -10,63 +10,62 @@ This module contains DRF viewsets for the moderation system, including:
|
||||
All views include comprehensive permissions, filtering, and pagination.
|
||||
"""
|
||||
|
||||
from rest_framework import viewsets, status, permissions
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q, Count
|
||||
from django.shortcuts import render
|
||||
from django.core.paginator import Paginator
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from django_fsm import can_proceed, TransitionNotAllowed
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Count, Q
|
||||
from django.shortcuts import render
|
||||
from django.utils import timezone
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_fsm import TransitionNotAllowed, can_proceed
|
||||
from rest_framework import permissions, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.response import Response
|
||||
|
||||
from apps.core.logging import log_business_event
|
||||
from apps.core.state_machine.exceptions import (
|
||||
TransitionPermissionDenied,
|
||||
TransitionValidationError,
|
||||
format_transition_error,
|
||||
)
|
||||
|
||||
from .filters import (
|
||||
BulkOperationFilter,
|
||||
ModerationActionFilter,
|
||||
ModerationQueueFilter,
|
||||
ModerationReportFilter,
|
||||
)
|
||||
from .models import (
|
||||
ModerationReport,
|
||||
ModerationQueue,
|
||||
ModerationAction,
|
||||
BulkOperation,
|
||||
EditSubmission,
|
||||
ModerationAction,
|
||||
ModerationQueue,
|
||||
ModerationReport,
|
||||
PhotoSubmission,
|
||||
)
|
||||
from .serializers import (
|
||||
ModerationReportSerializer,
|
||||
CreateModerationReportSerializer,
|
||||
UpdateModerationReportSerializer,
|
||||
ModerationQueueSerializer,
|
||||
AssignQueueItemSerializer,
|
||||
CompleteQueueItemSerializer,
|
||||
ModerationActionSerializer,
|
||||
CreateModerationActionSerializer,
|
||||
BulkOperationSerializer,
|
||||
CreateBulkOperationSerializer,
|
||||
UserModerationProfileSerializer,
|
||||
EditSubmissionSerializer,
|
||||
EditSubmissionListSerializer,
|
||||
PhotoSubmissionSerializer,
|
||||
)
|
||||
from .filters import (
|
||||
ModerationReportFilter,
|
||||
ModerationQueueFilter,
|
||||
ModerationActionFilter,
|
||||
BulkOperationFilter,
|
||||
)
|
||||
import logging
|
||||
|
||||
from apps.core.logging import log_exception, log_business_event
|
||||
|
||||
from .permissions import (
|
||||
IsModeratorOrAdmin,
|
||||
IsAdminOrSuperuser,
|
||||
CanViewModerationData,
|
||||
IsAdminOrSuperuser,
|
||||
IsModeratorOrAdmin,
|
||||
)
|
||||
from .serializers import (
|
||||
AssignQueueItemSerializer,
|
||||
BulkOperationSerializer,
|
||||
CompleteQueueItemSerializer,
|
||||
CreateBulkOperationSerializer,
|
||||
CreateModerationActionSerializer,
|
||||
CreateModerationReportSerializer,
|
||||
EditSubmissionListSerializer,
|
||||
EditSubmissionSerializer,
|
||||
ModerationActionSerializer,
|
||||
ModerationQueueSerializer,
|
||||
ModerationReportSerializer,
|
||||
PhotoSubmissionSerializer,
|
||||
UpdateModerationReportSerializer,
|
||||
UserModerationProfileSerializer,
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
@@ -352,8 +351,8 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
|
||||
@action(detail=True, methods=["get"], permission_classes=[CanViewModerationData])
|
||||
def history(self, request, pk=None):
|
||||
"""Get transition history for this report."""
|
||||
from django_fsm_log.models import StateLog
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django_fsm_log.models import StateLog
|
||||
|
||||
report = self.get_object()
|
||||
content_type = ContentType.objects.get_for_model(report)
|
||||
@@ -387,8 +386,8 @@ class ModerationReportViewSet(viewsets.ModelViewSet):
|
||||
|
||||
Supports both HTMX (returns HTML partials) and API (returns JSON) requests.
|
||||
"""
|
||||
from django_fsm_log.models import StateLog
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django_fsm_log.models import StateLog
|
||||
|
||||
queryset = StateLog.objects.select_related("by", "content_type").all()
|
||||
|
||||
@@ -829,8 +828,8 @@ class ModerationQueueViewSet(viewsets.ModelViewSet):
|
||||
@action(detail=True, methods=["get"], permission_classes=[CanViewModerationData])
|
||||
def history(self, request, pk=None):
|
||||
"""Get transition history for this queue item."""
|
||||
from django_fsm_log.models import StateLog
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django_fsm_log.models import StateLog
|
||||
|
||||
queue_item = self.get_object()
|
||||
content_type = ContentType.objects.get_for_model(queue_item)
|
||||
@@ -890,10 +889,7 @@ class ModerationActionViewSet(viewsets.ModelViewSet):
|
||||
|
||||
def get_permissions(self):
|
||||
"""Return appropriate permissions based on action."""
|
||||
if self.action == "create":
|
||||
permission_classes = [IsModeratorOrAdmin]
|
||||
else:
|
||||
permission_classes = [CanViewModerationData]
|
||||
permission_classes = [IsModeratorOrAdmin] if self.action == "create" else [CanViewModerationData]
|
||||
|
||||
return [permission() for permission in permission_classes]
|
||||
|
||||
@@ -1125,8 +1121,8 @@ class BulkOperationViewSet(viewsets.ModelViewSet):
|
||||
@action(detail=True, methods=["get"])
|
||||
def history(self, request, pk=None):
|
||||
"""Get transition history for this bulk operation."""
|
||||
from django_fsm_log.models import StateLog
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django_fsm_log.models import StateLog
|
||||
|
||||
operation = self.get_object()
|
||||
content_type = ContentType.objects.get_for_model(operation)
|
||||
@@ -1404,7 +1400,7 @@ class UserModerationViewSet(viewsets.ViewSet):
|
||||
class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
ViewSet for managing edit submissions.
|
||||
|
||||
|
||||
Includes claim/unclaim endpoints with concurrency protection using
|
||||
database row locking (select_for_update) to prevent race conditions.
|
||||
"""
|
||||
@@ -1425,7 +1421,7 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
status = self.request.query_params.get("status")
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
|
||||
|
||||
# User filter
|
||||
user_id = self.request.query_params.get("user")
|
||||
if user_id:
|
||||
@@ -1437,20 +1433,20 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
def claim(self, request, pk=None):
|
||||
"""
|
||||
Claim a submission for review with concurrency protection.
|
||||
|
||||
|
||||
Uses select_for_update() to acquire a database row lock,
|
||||
preventing race conditions when multiple moderators try to
|
||||
claim the same submission simultaneously.
|
||||
|
||||
|
||||
Returns:
|
||||
200: Submission successfully claimed
|
||||
404: Submission not found
|
||||
409: Submission already claimed or being claimed by another moderator
|
||||
400: Invalid state for claiming
|
||||
"""
|
||||
from django.db import transaction, DatabaseError
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from django.db import DatabaseError, transaction
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
# Lock the row for update - other transactions will fail immediately
|
||||
@@ -1466,7 +1462,7 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
{"error": "Submission is being claimed by another moderator. Please try again."},
|
||||
status=status.HTTP_409_CONFLICT
|
||||
)
|
||||
|
||||
|
||||
# Check if already claimed
|
||||
if submission.status == "CLAIMED":
|
||||
return Response(
|
||||
@@ -1477,14 +1473,14 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT
|
||||
)
|
||||
|
||||
|
||||
# Check if in valid state for claiming
|
||||
if submission.status != "PENDING":
|
||||
return Response(
|
||||
{"error": f"Cannot claim submission in {submission.status} state"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
submission.claim(user=request.user)
|
||||
log_business_event(
|
||||
@@ -1506,26 +1502,26 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
def unclaim(self, request, pk=None):
|
||||
"""
|
||||
Release claim on a submission.
|
||||
|
||||
|
||||
Only the claiming moderator or an admin can unclaim a submission.
|
||||
"""
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
submission = self.get_object()
|
||||
|
||||
|
||||
# Only the claiming user or an admin can unclaim
|
||||
if submission.claimed_by != request.user and not request.user.is_staff:
|
||||
return Response(
|
||||
{"error": "Only the claiming moderator or an admin can unclaim"},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
|
||||
if submission.status != "CLAIMED":
|
||||
return Response(
|
||||
{"error": "Submission is not claimed"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
submission.unclaim(user=request.user)
|
||||
log_business_event(
|
||||
@@ -1547,7 +1543,7 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
def approve(self, request, pk=None):
|
||||
submission = self.get_object()
|
||||
user = request.user
|
||||
|
||||
|
||||
try:
|
||||
submission.approve(moderator=user)
|
||||
return Response(self.get_serializer(submission).data)
|
||||
@@ -1559,19 +1555,19 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
submission = self.get_object()
|
||||
user = request.user
|
||||
reason = request.data.get("reason", "")
|
||||
|
||||
|
||||
try:
|
||||
submission.reject(moderator=user, reason=reason)
|
||||
return Response(self.get_serializer(submission).data)
|
||||
except Exception as e:
|
||||
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin])
|
||||
def escalate(self, request, pk=None):
|
||||
submission = self.get_object()
|
||||
user = request.user
|
||||
reason = request.data.get("reason", "")
|
||||
|
||||
|
||||
try:
|
||||
submission.escalate(moderator=user, reason=reason)
|
||||
return Response(self.get_serializer(submission).data)
|
||||
@@ -1582,7 +1578,7 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
class PhotoSubmissionViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
ViewSet for managing photo submissions.
|
||||
|
||||
|
||||
Includes claim/unclaim endpoints with concurrency protection using
|
||||
database row locking (select_for_update) to prevent race conditions.
|
||||
"""
|
||||
@@ -1599,24 +1595,24 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
|
||||
status = self.request.query_params.get("status")
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
|
||||
|
||||
# User filter
|
||||
user_id = self.request.query_params.get("user")
|
||||
if user_id:
|
||||
queryset = queryset.filter(user_id=user_id)
|
||||
|
||||
|
||||
return queryset
|
||||
|
||||
@action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin])
|
||||
def claim(self, request, pk=None):
|
||||
"""
|
||||
Claim a photo submission for review with concurrency protection.
|
||||
|
||||
|
||||
Uses select_for_update() to acquire a database row lock.
|
||||
"""
|
||||
from django.db import transaction, DatabaseError
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from django.db import DatabaseError, transaction
|
||||
|
||||
with transaction.atomic():
|
||||
try:
|
||||
submission = PhotoSubmission.objects.select_for_update(nowait=True).get(pk=pk)
|
||||
@@ -1630,7 +1626,7 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
|
||||
{"error": "Submission is being claimed by another moderator. Please try again."},
|
||||
status=status.HTTP_409_CONFLICT
|
||||
)
|
||||
|
||||
|
||||
if submission.status == "CLAIMED":
|
||||
return Response(
|
||||
{
|
||||
@@ -1640,13 +1636,13 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
|
||||
},
|
||||
status=status.HTTP_409_CONFLICT
|
||||
)
|
||||
|
||||
|
||||
if submission.status != "PENDING":
|
||||
return Response(
|
||||
{"error": f"Cannot claim submission in {submission.status} state"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
submission.claim(user=request.user)
|
||||
log_business_event(
|
||||
@@ -1668,21 +1664,21 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
|
||||
def unclaim(self, request, pk=None):
|
||||
"""Release claim on a photo submission."""
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
|
||||
submission = self.get_object()
|
||||
|
||||
|
||||
if submission.claimed_by != request.user and not request.user.is_staff:
|
||||
return Response(
|
||||
{"error": "Only the claiming moderator or an admin can unclaim"},
|
||||
status=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
|
||||
|
||||
if submission.status != "CLAIMED":
|
||||
return Response(
|
||||
{"error": "Submission is not claimed"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
submission.unclaim(user=request.user)
|
||||
log_business_event(
|
||||
@@ -1705,7 +1701,7 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
|
||||
submission = self.get_object()
|
||||
user = request.user
|
||||
notes = request.data.get("notes", "")
|
||||
|
||||
|
||||
try:
|
||||
submission.approve(moderator=user, notes=notes)
|
||||
return Response(self.get_serializer(submission).data)
|
||||
@@ -1717,7 +1713,7 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
|
||||
submission = self.get_object()
|
||||
user = request.user
|
||||
notes = request.data.get("notes", "")
|
||||
|
||||
|
||||
try:
|
||||
submission.reject(moderator=user, notes=notes)
|
||||
return Response(self.get_serializer(submission).data)
|
||||
@@ -1729,7 +1725,7 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
|
||||
submission = self.get_object()
|
||||
user = request.user
|
||||
notes = request.data.get("notes", "")
|
||||
|
||||
|
||||
try:
|
||||
submission.escalate(moderator=user, notes=notes)
|
||||
return Response(self.get_serializer(submission).data)
|
||||
|
||||
Reference in New Issue
Block a user