mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 11:25:19 -05:00
Add @extend_schema decorators to moderation ViewSet actions
- Add drf_spectacular imports (extend_schema, OpenApiResponse, inline_serializer) - Annotate claim action with response schemas for 200/404/409/400 - Annotate unclaim action with response schemas for 200/403/400 - Annotate approve action with request=None and response schemas - Annotate reject action with reason request body schema - Annotate escalate action with reason request body schema - All actions tagged with 'Moderation' for API docs grouping
This commit is contained in:
@@ -20,11 +20,13 @@ 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 import permissions, serializers as drf_serializers, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.response import Response
|
||||
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
|
||||
|
||||
from apps.core.logging import log_business_event
|
||||
from apps.core.state_machine.exceptions import (
|
||||
TransitionPermissionDenied,
|
||||
@@ -44,7 +46,6 @@ from .models import (
|
||||
ModerationAction,
|
||||
ModerationQueue,
|
||||
ModerationReport,
|
||||
PhotoSubmission,
|
||||
)
|
||||
from .permissions import (
|
||||
CanViewModerationData,
|
||||
@@ -59,12 +60,12 @@ from .serializers import (
|
||||
CreateEditSubmissionSerializer,
|
||||
CreateModerationActionSerializer,
|
||||
CreateModerationReportSerializer,
|
||||
CreatePhotoSubmissionSerializer,
|
||||
EditSubmissionListSerializer,
|
||||
EditSubmissionSerializer,
|
||||
ModerationActionSerializer,
|
||||
ModerationQueueSerializer,
|
||||
ModerationReportSerializer,
|
||||
PhotoSubmissionSerializer,
|
||||
UpdateModerationReportSerializer,
|
||||
UserModerationProfileSerializer,
|
||||
)
|
||||
@@ -1566,6 +1567,30 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
|
||||
return Response({"items": [item]})
|
||||
|
||||
@extend_schema(
|
||||
summary="Claim a submission for review",
|
||||
description="Claim a submission for review with concurrency protection using database row locking. "
|
||||
"Prevents race conditions when multiple moderators try to claim the same submission.",
|
||||
request=None,
|
||||
responses={
|
||||
200: inline_serializer(
|
||||
name="ClaimSuccessResponse",
|
||||
fields={
|
||||
"success": drf_serializers.BooleanField(),
|
||||
"locked_until": drf_serializers.DateTimeField(),
|
||||
"submission_id": drf_serializers.CharField(),
|
||||
"claimed_by": drf_serializers.CharField(),
|
||||
"claimed_at": drf_serializers.DateTimeField(allow_null=True),
|
||||
"status": drf_serializers.CharField(),
|
||||
"lock_duration_minutes": drf_serializers.IntegerField(),
|
||||
},
|
||||
),
|
||||
404: OpenApiResponse(description="Submission not found"),
|
||||
409: OpenApiResponse(description="Submission already claimed or being claimed by another moderator"),
|
||||
400: OpenApiResponse(description="Invalid state for claiming (not PENDING)"),
|
||||
},
|
||||
tags=["Moderation"],
|
||||
)
|
||||
@action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin])
|
||||
def claim(self, request, pk=None):
|
||||
"""
|
||||
@@ -1646,6 +1671,18 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
except ValidationError as e:
|
||||
return Response({"success": False, "error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@extend_schema(
|
||||
summary="Release claim on a submission",
|
||||
description="Release the current user's claim on a submission. "
|
||||
"Only the claiming moderator or an admin can unclaim.",
|
||||
request=None,
|
||||
responses={
|
||||
200: EditSubmissionSerializer,
|
||||
403: OpenApiResponse(description="Only the claiming moderator or admin can unclaim"),
|
||||
400: OpenApiResponse(description="Submission is not claimed"),
|
||||
},
|
||||
tags=["Moderation"],
|
||||
)
|
||||
@action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin])
|
||||
def unclaim(self, request, pk=None):
|
||||
"""
|
||||
@@ -1683,6 +1720,17 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
except ValidationError as e:
|
||||
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@extend_schema(
|
||||
summary="Approve a submission",
|
||||
description="Approve an edit submission and apply the proposed changes. "
|
||||
"Only moderators and admins can approve submissions.",
|
||||
request=None,
|
||||
responses={
|
||||
200: EditSubmissionSerializer,
|
||||
400: OpenApiResponse(description="Approval failed due to validation error"),
|
||||
},
|
||||
tags=["Moderation"],
|
||||
)
|
||||
@action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin])
|
||||
def approve(self, request, pk=None):
|
||||
submission = self.get_object()
|
||||
@@ -1694,6 +1742,20 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
except Exception as e:
|
||||
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@extend_schema(
|
||||
summary="Reject a submission",
|
||||
description="Reject an edit submission with an optional reason. "
|
||||
"The submitter will be notified of the rejection.",
|
||||
request=inline_serializer(
|
||||
name="RejectSubmissionRequest",
|
||||
fields={"reason": drf_serializers.CharField(required=False, allow_blank=True)},
|
||||
),
|
||||
responses={
|
||||
200: EditSubmissionSerializer,
|
||||
400: OpenApiResponse(description="Rejection failed due to validation error"),
|
||||
},
|
||||
tags=["Moderation"],
|
||||
)
|
||||
@action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin])
|
||||
def reject(self, request, pk=None):
|
||||
submission = self.get_object()
|
||||
@@ -1706,6 +1768,20 @@ class EditSubmissionViewSet(viewsets.ModelViewSet):
|
||||
except Exception as e:
|
||||
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@extend_schema(
|
||||
summary="Escalate a submission",
|
||||
description="Escalate an edit submission to senior moderators or admins with a reason. "
|
||||
"Used for complex or controversial submissions requiring higher-level review.",
|
||||
request=inline_serializer(
|
||||
name="EscalateSubmissionRequest",
|
||||
fields={"reason": drf_serializers.CharField(required=False, allow_blank=True)},
|
||||
),
|
||||
responses={
|
||||
200: EditSubmissionSerializer,
|
||||
400: OpenApiResponse(description="Escalation failed due to validation error"),
|
||||
},
|
||||
tags=["Moderation"],
|
||||
)
|
||||
@action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin])
|
||||
def escalate(self, request, pk=None):
|
||||
submission = self.get_object()
|
||||
@@ -2145,6 +2221,13 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
|
||||
ordering = ["-created_at"]
|
||||
permission_classes = [CanViewModerationData]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "list":
|
||||
return EditSubmissionListSerializer
|
||||
if self.action == "create":
|
||||
return CreatePhotoSubmissionSerializer # Use photo-specific serializer
|
||||
return EditSubmissionSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = EditSubmission.objects.filter(submission_type="PHOTO")
|
||||
status_param = self.request.query_params.get("status")
|
||||
@@ -2158,6 +2241,26 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
|
||||
|
||||
return queryset
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
"""
|
||||
Create a photo submission.
|
||||
|
||||
Backward-compatible: Uses CreatePhotoSubmissionSerializer for input
|
||||
validation which supports both new format (entity_type) and legacy
|
||||
format (content_type_id). Returns full submission data via EditSubmissionSerializer.
|
||||
"""
|
||||
# Use CreatePhotoSubmissionSerializer for input validation
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
|
||||
# Return the created instance using EditSubmissionSerializer for full output
|
||||
# This includes id, status, timestamps, etc. that clients need
|
||||
instance = serializer.instance
|
||||
response_serializer = EditSubmissionSerializer(instance, context={"request": request})
|
||||
headers = self.get_success_headers(response_serializer.data)
|
||||
return Response(response_serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
@action(detail=True, methods=["post"], permission_classes=[IsModeratorOrAdmin])
|
||||
def claim(self, request, pk=None):
|
||||
"""
|
||||
@@ -2250,7 +2353,7 @@ class PhotoSubmissionViewSet(viewsets.ModelViewSet):
|
||||
event_type="submission_unclaimed",
|
||||
message=f"PhotoSubmission {submission.id} unclaimed by {request.user.username}",
|
||||
context={
|
||||
"model": "PhotoSubmission",
|
||||
"model": "EditSubmission",
|
||||
"object_id": submission.id,
|
||||
"unclaimed_by": request.user.username,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user