Add ReviewEvent model and ReviewSubmissionService for review management

- Created a new ReviewEvent model to track review events with fields for content, rating, moderation status, and timestamps.
- Added ForeignKey relationships to connect ReviewEvent with ContentSubmission, User, and Review.
- Implemented ReviewSubmissionService to handle review submissions, including creation, updates, and moderation workflows.
- Introduced atomic transactions to ensure data integrity during review submissions and updates.
- Added logging for review submission and moderation actions for better traceability.
- Implemented validation to prevent duplicate reviews and ensure only the review owner can update their review.
This commit is contained in:
pacnpal
2025-11-08 16:49:58 -05:00
parent 618310a87b
commit 9122320e7e
18 changed files with 3170 additions and 171 deletions

View File

@@ -12,6 +12,7 @@ from django.core.exceptions import ValidationError, PermissionDenied
from apps.moderation.models import ContentSubmission, SubmissionItem
from apps.moderation.services import ModerationService
from apps.users.permissions import jwt_auth, require_auth
from api.v1.schemas import (
ContentSubmissionCreate,
ContentSubmissionOut,
@@ -109,20 +110,20 @@ def _get_entity(entity_type: str, entity_id: UUID):
# Submission Endpoints
# ============================================================================
@router.post('/submissions', response={201: ContentSubmissionOut, 400: ErrorResponse, 401: ErrorResponse})
@router.post('/submissions', response={201: ContentSubmissionOut, 400: ErrorResponse, 401: ErrorResponse}, auth=jwt_auth)
@require_auth
def create_submission(request, data: ContentSubmissionCreate):
"""
Create a new content submission.
Creates a submission with multiple items representing field changes.
If auto_submit is True, the submission is immediately moved to pending state.
"""
# TODO: Require authentication
# For now, use a test user or get from request
from apps.users.models import User
user = User.objects.first() # TEMP: Get first user for testing
if not user:
**Authentication:** Required
"""
user = request.auth
if not user or not user.is_authenticated:
return 401, {'detail': 'Authentication required'}
try:
@@ -227,14 +228,18 @@ def get_submission(request, submission_id: UUID):
return 404, {'detail': 'Submission not found'}
@router.delete('/submissions/{submission_id}', response={204: None, 403: ErrorResponse, 404: ErrorResponse})
@router.delete('/submissions/{submission_id}', response={204: None, 403: ErrorResponse, 404: ErrorResponse}, auth=jwt_auth)
@require_auth
def delete_submission(request, submission_id: UUID):
"""
Delete a submission (only if draft/pending and owned by user).
**Authentication:** Required
"""
# TODO: Get current user from request
from apps.users.models import User
user = User.objects.first() # TEMP
user = request.auth
if not user or not user.is_authenticated:
return 401, {'detail': 'Authentication required'}
try:
ModerationService.delete_submission(submission_id, user)
@@ -254,17 +259,26 @@ def delete_submission(request, submission_id: UUID):
@router.post(
'/submissions/{submission_id}/start-review',
response={200: ContentSubmissionOut, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse}
response={200: ContentSubmissionOut, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse},
auth=jwt_auth
)
@require_auth
def start_review(request, submission_id: UUID, data: StartReviewRequest):
"""
Start reviewing a submission (lock it for 15 minutes).
Only moderators can start reviews.
**Authentication:** Required (Moderator role)
"""
# TODO: Get current user (moderator) from request
from apps.users.models import User
user = User.objects.first() # TEMP
user = request.auth
if not user or not user.is_authenticated:
return 401, {'detail': 'Authentication required'}
# Check moderator permission
if not hasattr(user, 'role') or not user.role.is_moderator:
return 403, {'detail': 'Moderator permission required'}
try:
submission = ModerationService.start_review(submission_id, user)
@@ -280,18 +294,27 @@ def start_review(request, submission_id: UUID, data: StartReviewRequest):
@router.post(
'/submissions/{submission_id}/approve',
response={200: ApprovalResponse, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse}
response={200: ApprovalResponse, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse},
auth=jwt_auth
)
@require_auth
def approve_submission(request, submission_id: UUID, data: ApproveRequest):
"""
Approve an entire submission and apply all changes.
Uses atomic transactions - all changes are applied or none are.
Only moderators can approve submissions.
**Authentication:** Required (Moderator role)
"""
# TODO: Get current user (moderator) from request
from apps.users.models import User
user = User.objects.first() # TEMP
user = request.auth
if not user or not user.is_authenticated:
return 401, {'detail': 'Authentication required'}
# Check moderator permission
if not hasattr(user, 'role') or not user.role.is_moderator:
return 403, {'detail': 'Moderator permission required'}
try:
submission = ModerationService.approve_submission(submission_id, user)
@@ -312,18 +335,27 @@ def approve_submission(request, submission_id: UUID, data: ApproveRequest):
@router.post(
'/submissions/{submission_id}/approve-selective',
response={200: SelectiveApprovalResponse, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse}
response={200: SelectiveApprovalResponse, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse},
auth=jwt_auth
)
@require_auth
def approve_selective(request, submission_id: UUID, data: ApproveSelectiveRequest):
"""
Approve only specific items in a submission.
Allows moderators to approve some changes while leaving others pending or rejected.
Uses atomic transactions for data integrity.
**Authentication:** Required (Moderator role)
"""
# TODO: Get current user (moderator) from request
from apps.users.models import User
user = User.objects.first() # TEMP
user = request.auth
if not user or not user.is_authenticated:
return 401, {'detail': 'Authentication required'}
# Check moderator permission
if not hasattr(user, 'role') or not user.role.is_moderator:
return 403, {'detail': 'Moderator permission required'}
try:
result = ModerationService.approve_selective(
@@ -348,18 +380,27 @@ def approve_selective(request, submission_id: UUID, data: ApproveSelectiveReques
@router.post(
'/submissions/{submission_id}/reject',
response={200: ApprovalResponse, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse}
response={200: ApprovalResponse, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse},
auth=jwt_auth
)
@require_auth
def reject_submission(request, submission_id: UUID, data: RejectRequest):
"""
Reject an entire submission.
All pending items are rejected with the provided reason.
Only moderators can reject submissions.
**Authentication:** Required (Moderator role)
"""
# TODO: Get current user (moderator) from request
from apps.users.models import User
user = User.objects.first() # TEMP
user = request.auth
if not user or not user.is_authenticated:
return 401, {'detail': 'Authentication required'}
# Check moderator permission
if not hasattr(user, 'role') or not user.role.is_moderator:
return 403, {'detail': 'Moderator permission required'}
try:
submission = ModerationService.reject_submission(submission_id, user, data.reason)
@@ -380,17 +421,26 @@ def reject_submission(request, submission_id: UUID, data: RejectRequest):
@router.post(
'/submissions/{submission_id}/reject-selective',
response={200: SelectiveRejectionResponse, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse}
response={200: SelectiveRejectionResponse, 400: ErrorResponse, 403: ErrorResponse, 404: ErrorResponse},
auth=jwt_auth
)
@require_auth
def reject_selective(request, submission_id: UUID, data: RejectSelectiveRequest):
"""
Reject only specific items in a submission.
Allows moderators to reject some changes while leaving others pending or approved.
**Authentication:** Required (Moderator role)
"""
# TODO: Get current user (moderator) from request
from apps.users.models import User
user = User.objects.first() # TEMP
user = request.auth
if not user or not user.is_authenticated:
return 401, {'detail': 'Authentication required'}
# Check moderator permission
if not hasattr(user, 'role') or not user.role.is_moderator:
return 403, {'detail': 'Moderator permission required'}
try:
result = ModerationService.reject_selective(
@@ -456,16 +506,20 @@ def get_reviewing_queue(request, page: int = 1, page_size: int = 50):
return list_submissions(request, status='reviewing', page=page, page_size=page_size)
@router.get('/queue/my-submissions', response=SubmissionListOut)
@router.get('/queue/my-submissions', response=SubmissionListOut, auth=jwt_auth)
@require_auth
def get_my_submissions(request, page: int = 1, page_size: int = 50):
"""
Get current user's submissions.
Returns all submissions created by the authenticated user.
**Authentication:** Required
"""
# TODO: Get current user from request
from apps.users.models import User
user = User.objects.first() # TEMP
user = request.auth
if not user or not user.is_authenticated:
return {'items': [], 'total': 0, 'page': page, 'page_size': page_size, 'total_pages': 0}
# Validate page_size
page_size = min(page_size, 100)