mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 09:45:17 -05:00
feat: Implement initial schema and add various API, service, and management command enhancements across the application.
This commit is contained in:
@@ -19,9 +19,7 @@ class ModerationService:
|
||||
"""Service for handling content moderation workflows."""
|
||||
|
||||
@staticmethod
|
||||
def approve_submission(
|
||||
*, submission_id: int, moderator: User, notes: str | None = None
|
||||
) -> object | None:
|
||||
def approve_submission(*, submission_id: int, moderator: User, notes: str | None = None) -> object | None:
|
||||
"""
|
||||
Approve a content submission and apply changes.
|
||||
|
||||
@@ -39,9 +37,7 @@ class ModerationService:
|
||||
ValueError: If submission cannot be processed
|
||||
"""
|
||||
with transaction.atomic():
|
||||
submission = EditSubmission.objects.select_for_update().get(
|
||||
id=submission_id
|
||||
)
|
||||
submission = EditSubmission.objects.select_for_update().get(id=submission_id)
|
||||
|
||||
if submission.status != "PENDING":
|
||||
raise ValueError(f"Submission {submission_id} is not pending approval")
|
||||
@@ -75,9 +71,7 @@ class ModerationService:
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def reject_submission(
|
||||
*, submission_id: int, moderator: User, reason: str
|
||||
) -> EditSubmission:
|
||||
def reject_submission(*, submission_id: int, moderator: User, reason: str) -> EditSubmission:
|
||||
"""
|
||||
Reject a content submission.
|
||||
|
||||
@@ -94,9 +88,7 @@ class ModerationService:
|
||||
ValueError: If submission cannot be rejected
|
||||
"""
|
||||
with transaction.atomic():
|
||||
submission = EditSubmission.objects.select_for_update().get(
|
||||
id=submission_id
|
||||
)
|
||||
submission = EditSubmission.objects.select_for_update().get(id=submission_id)
|
||||
|
||||
if submission.status != "PENDING":
|
||||
raise ValueError(f"Submission {submission_id} is not pending review")
|
||||
@@ -175,9 +167,7 @@ class ModerationService:
|
||||
ValueError: If submission cannot be modified
|
||||
"""
|
||||
with transaction.atomic():
|
||||
submission = EditSubmission.objects.select_for_update().get(
|
||||
id=submission_id
|
||||
)
|
||||
submission = EditSubmission.objects.select_for_update().get(id=submission_id)
|
||||
|
||||
if submission.status != "PENDING":
|
||||
raise ValueError(f"Submission {submission_id} is not pending review")
|
||||
@@ -220,9 +210,7 @@ class ModerationService:
|
||||
return pending_submissions_for_review(content_type=content_type, limit=limit)
|
||||
|
||||
@staticmethod
|
||||
def get_submission_statistics(
|
||||
*, days: int = 30, moderator: User | None = None
|
||||
) -> dict[str, Any]:
|
||||
def get_submission_statistics(*, days: int = 30, moderator: User | None = None) -> dict[str, Any]:
|
||||
"""
|
||||
Get moderation statistics for a time period.
|
||||
|
||||
@@ -248,7 +236,7 @@ class ModerationService:
|
||||
Returns:
|
||||
True if user is MODERATOR, ADMIN, or SUPERUSER
|
||||
"""
|
||||
return user.role in ['MODERATOR', 'ADMIN', 'SUPERUSER']
|
||||
return user.role in ["MODERATOR", "ADMIN", "SUPERUSER"]
|
||||
|
||||
@staticmethod
|
||||
def create_edit_submission_with_queue(
|
||||
@@ -297,33 +285,32 @@ class ModerationService:
|
||||
try:
|
||||
created_object = submission.approve(submitter)
|
||||
return {
|
||||
'submission': submission,
|
||||
'status': 'auto_approved',
|
||||
'created_object': created_object,
|
||||
'queue_item': None,
|
||||
'message': 'Submission auto-approved for moderator'
|
||||
"submission": submission,
|
||||
"status": "auto_approved",
|
||||
"created_object": created_object,
|
||||
"queue_item": None,
|
||||
"message": "Submission auto-approved for moderator",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'submission': submission,
|
||||
'status': 'failed',
|
||||
'created_object': None,
|
||||
'queue_item': None,
|
||||
'message': f'Auto-approval failed: {str(e)}'
|
||||
"submission": submission,
|
||||
"status": "failed",
|
||||
"created_object": None,
|
||||
"queue_item": None,
|
||||
"message": f"Auto-approval failed: {str(e)}",
|
||||
}
|
||||
else:
|
||||
# Create queue item for regular users
|
||||
queue_item = ModerationService._create_queue_item_for_submission(
|
||||
submission=submission,
|
||||
submitter=submitter
|
||||
submission=submission, submitter=submitter
|
||||
)
|
||||
|
||||
return {
|
||||
'submission': submission,
|
||||
'status': 'queued',
|
||||
'created_object': None,
|
||||
'queue_item': queue_item,
|
||||
'message': 'Submission added to moderation queue'
|
||||
"submission": submission,
|
||||
"status": "queued",
|
||||
"created_object": None,
|
||||
"queue_item": queue_item,
|
||||
"message": "Submission added to moderation queue",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -370,36 +357,33 @@ class ModerationService:
|
||||
try:
|
||||
submission.auto_approve()
|
||||
return {
|
||||
'submission': submission,
|
||||
'status': 'auto_approved',
|
||||
'queue_item': None,
|
||||
'message': 'Photo submission auto-approved for moderator'
|
||||
"submission": submission,
|
||||
"status": "auto_approved",
|
||||
"queue_item": None,
|
||||
"message": "Photo submission auto-approved for moderator",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
'submission': submission,
|
||||
'status': 'failed',
|
||||
'queue_item': None,
|
||||
'message': f'Auto-approval failed: {str(e)}'
|
||||
"submission": submission,
|
||||
"status": "failed",
|
||||
"queue_item": None,
|
||||
"message": f"Auto-approval failed: {str(e)}",
|
||||
}
|
||||
else:
|
||||
# Create queue item for regular users
|
||||
queue_item = ModerationService._create_queue_item_for_photo_submission(
|
||||
submission=submission,
|
||||
submitter=submitter
|
||||
submission=submission, submitter=submitter
|
||||
)
|
||||
|
||||
return {
|
||||
'submission': submission,
|
||||
'status': 'queued',
|
||||
'queue_item': queue_item,
|
||||
'message': 'Photo submission added to moderation queue'
|
||||
"submission": submission,
|
||||
"status": "queued",
|
||||
"queue_item": queue_item,
|
||||
"message": "Photo submission added to moderation queue",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _create_queue_item_for_submission(
|
||||
*, submission: EditSubmission, submitter: User
|
||||
) -> ModerationQueue:
|
||||
def _create_queue_item_for_submission(*, submission: EditSubmission, submitter: User) -> ModerationQueue:
|
||||
"""
|
||||
Create a moderation queue item for an edit submission.
|
||||
|
||||
@@ -417,13 +401,13 @@ class ModerationService:
|
||||
|
||||
# Create preview data
|
||||
entity_preview = {
|
||||
'submission_type': submission.submission_type,
|
||||
'changes_count': len(submission.changes) if submission.changes else 0,
|
||||
'reason': submission.reason[:100] if submission.reason else "",
|
||||
"submission_type": submission.submission_type,
|
||||
"changes_count": len(submission.changes) if submission.changes else 0,
|
||||
"reason": submission.reason[:100] if submission.reason else "",
|
||||
}
|
||||
|
||||
if submission.content_object:
|
||||
entity_preview['object_name'] = str(submission.content_object)
|
||||
entity_preview["object_name"] = str(submission.content_object)
|
||||
|
||||
# Determine title and description
|
||||
action = "creation" if submission.submission_type == "CREATE" else "edit"
|
||||
@@ -435,7 +419,7 @@ class ModerationService:
|
||||
|
||||
# Create queue item
|
||||
queue_item = ModerationQueue(
|
||||
item_type='CONTENT_REVIEW',
|
||||
item_type="CONTENT_REVIEW",
|
||||
title=title,
|
||||
description=description,
|
||||
entity_type=entity_type,
|
||||
@@ -443,9 +427,9 @@ class ModerationService:
|
||||
entity_preview=entity_preview,
|
||||
content_type=content_type,
|
||||
flagged_by=submitter,
|
||||
priority='MEDIUM',
|
||||
priority="MEDIUM",
|
||||
estimated_review_time=15, # 15 minutes default
|
||||
tags=['edit_submission', submission.submission_type.lower()],
|
||||
tags=["edit_submission", submission.submission_type.lower()],
|
||||
)
|
||||
|
||||
queue_item.full_clean()
|
||||
@@ -454,9 +438,7 @@ class ModerationService:
|
||||
return queue_item
|
||||
|
||||
@staticmethod
|
||||
def _create_queue_item_for_photo_submission(
|
||||
*, submission: PhotoSubmission, submitter: User
|
||||
) -> ModerationQueue:
|
||||
def _create_queue_item_for_photo_submission(*, submission: PhotoSubmission, submitter: User) -> ModerationQueue:
|
||||
"""
|
||||
Create a moderation queue item for a photo submission.
|
||||
|
||||
@@ -474,13 +456,13 @@ class ModerationService:
|
||||
|
||||
# Create preview data
|
||||
entity_preview = {
|
||||
'caption': submission.caption,
|
||||
'date_taken': submission.date_taken.isoformat() if submission.date_taken else None,
|
||||
'photo_url': submission.photo.url if submission.photo else None,
|
||||
"caption": submission.caption,
|
||||
"date_taken": submission.date_taken.isoformat() if submission.date_taken else None,
|
||||
"photo_url": submission.photo.url if submission.photo else None,
|
||||
}
|
||||
|
||||
if submission.content_object:
|
||||
entity_preview['object_name'] = str(submission.content_object)
|
||||
entity_preview["object_name"] = str(submission.content_object)
|
||||
|
||||
# Create title and description
|
||||
title = f"Photo submission for {entity_type} by {submitter.username}"
|
||||
@@ -490,7 +472,7 @@ class ModerationService:
|
||||
|
||||
# Create queue item
|
||||
queue_item = ModerationQueue(
|
||||
item_type='CONTENT_REVIEW',
|
||||
item_type="CONTENT_REVIEW",
|
||||
title=title,
|
||||
description=description,
|
||||
entity_type=entity_type,
|
||||
@@ -498,9 +480,9 @@ class ModerationService:
|
||||
entity_preview=entity_preview,
|
||||
content_type=content_type,
|
||||
flagged_by=submitter,
|
||||
priority='LOW', # Photos typically lower priority
|
||||
priority="LOW", # Photos typically lower priority
|
||||
estimated_review_time=5, # 5 minutes default for photos
|
||||
tags=['photo_submission'],
|
||||
tags=["photo_submission"],
|
||||
)
|
||||
|
||||
queue_item.full_clean()
|
||||
@@ -525,11 +507,9 @@ class ModerationService:
|
||||
Dictionary with processing results
|
||||
"""
|
||||
with transaction.atomic():
|
||||
queue_item = ModerationQueue.objects.select_for_update().get(
|
||||
id=queue_item_id
|
||||
)
|
||||
queue_item = ModerationQueue.objects.select_for_update().get(id=queue_item_id)
|
||||
|
||||
if queue_item.status != 'PENDING':
|
||||
if queue_item.status != "PENDING":
|
||||
raise ValueError(f"Queue item {queue_item_id} is not pending")
|
||||
|
||||
# Transition queue item into an active state before processing
|
||||
@@ -542,7 +522,7 @@ class ModerationService:
|
||||
pass
|
||||
except AttributeError:
|
||||
# Fallback for environments without the generated transition method
|
||||
queue_item.status = 'IN_PROGRESS'
|
||||
queue_item.status = "IN_PROGRESS"
|
||||
moved_to_in_progress = True
|
||||
|
||||
if moved_to_in_progress:
|
||||
@@ -554,116 +534,94 @@ class ModerationService:
|
||||
try:
|
||||
queue_item.transition_to_completed(user=moderator)
|
||||
except TransitionNotAllowed:
|
||||
queue_item.status = 'COMPLETED'
|
||||
queue_item.status = "COMPLETED"
|
||||
except AttributeError:
|
||||
queue_item.status = 'COMPLETED'
|
||||
queue_item.status = "COMPLETED"
|
||||
|
||||
# Find related submission
|
||||
if 'edit_submission' in queue_item.tags:
|
||||
if "edit_submission" in queue_item.tags:
|
||||
# Find EditSubmission
|
||||
submissions = EditSubmission.objects.filter(
|
||||
user=queue_item.flagged_by,
|
||||
content_type=queue_item.content_type,
|
||||
object_id=queue_item.entity_id,
|
||||
status='PENDING'
|
||||
).order_by('-created_at')
|
||||
status="PENDING",
|
||||
).order_by("-created_at")
|
||||
|
||||
if not submissions.exists():
|
||||
raise ValueError(
|
||||
"No pending edit submission found for this queue item")
|
||||
raise ValueError("No pending edit submission found for this queue item")
|
||||
|
||||
submission = submissions.first()
|
||||
|
||||
if action == 'approve':
|
||||
if action == "approve":
|
||||
try:
|
||||
created_object = submission.approve(moderator)
|
||||
# Use FSM transition for queue status
|
||||
_complete_queue_item()
|
||||
result = {
|
||||
'status': 'approved',
|
||||
'created_object': created_object,
|
||||
'message': 'Submission approved successfully'
|
||||
"status": "approved",
|
||||
"created_object": created_object,
|
||||
"message": "Submission approved successfully",
|
||||
}
|
||||
except Exception as e:
|
||||
# Use FSM transition for queue status
|
||||
_complete_queue_item()
|
||||
result = {
|
||||
'status': 'failed',
|
||||
'created_object': None,
|
||||
'message': f'Approval failed: {str(e)}'
|
||||
}
|
||||
elif action == 'reject':
|
||||
result = {"status": "failed", "created_object": None, "message": f"Approval failed: {str(e)}"}
|
||||
elif action == "reject":
|
||||
submission.reject(moderator, notes or "Rejected by moderator")
|
||||
# Use FSM transition for queue status
|
||||
_complete_queue_item()
|
||||
result = {
|
||||
'status': 'rejected',
|
||||
'created_object': None,
|
||||
'message': 'Submission rejected'
|
||||
}
|
||||
elif action == 'escalate':
|
||||
result = {"status": "rejected", "created_object": None, "message": "Submission rejected"}
|
||||
elif action == "escalate":
|
||||
submission.escalate(moderator, notes or "Escalated for review")
|
||||
queue_item.priority = 'HIGH'
|
||||
queue_item.priority = "HIGH"
|
||||
# Keep status as PENDING for escalation
|
||||
result = {
|
||||
'status': 'escalated',
|
||||
'created_object': None,
|
||||
'message': 'Submission escalated'
|
||||
}
|
||||
result = {"status": "escalated", "created_object": None, "message": "Submission escalated"}
|
||||
else:
|
||||
raise ValueError(f"Unknown action: {action}")
|
||||
|
||||
elif 'photo_submission' in queue_item.tags:
|
||||
elif "photo_submission" in queue_item.tags:
|
||||
# Find PhotoSubmission
|
||||
submissions = PhotoSubmission.objects.filter(
|
||||
user=queue_item.flagged_by,
|
||||
content_type=queue_item.content_type,
|
||||
object_id=queue_item.entity_id,
|
||||
status='PENDING'
|
||||
).order_by('-created_at')
|
||||
status="PENDING",
|
||||
).order_by("-created_at")
|
||||
|
||||
if not submissions.exists():
|
||||
raise ValueError(
|
||||
"No pending photo submission found for this queue item")
|
||||
raise ValueError("No pending photo submission found for this queue item")
|
||||
|
||||
submission = submissions.first()
|
||||
|
||||
if action == 'approve':
|
||||
if action == "approve":
|
||||
try:
|
||||
submission.approve(moderator, notes or "")
|
||||
# Use FSM transition for queue status
|
||||
_complete_queue_item()
|
||||
result = {
|
||||
'status': 'approved',
|
||||
'created_object': None,
|
||||
'message': 'Photo submission approved successfully'
|
||||
"status": "approved",
|
||||
"created_object": None,
|
||||
"message": "Photo submission approved successfully",
|
||||
}
|
||||
except Exception as e:
|
||||
# Use FSM transition for queue status
|
||||
_complete_queue_item()
|
||||
result = {
|
||||
'status': 'failed',
|
||||
'created_object': None,
|
||||
'message': f'Photo approval failed: {str(e)}'
|
||||
"status": "failed",
|
||||
"created_object": None,
|
||||
"message": f"Photo approval failed: {str(e)}",
|
||||
}
|
||||
elif action == 'reject':
|
||||
elif action == "reject":
|
||||
submission.reject(moderator, notes or "Rejected by moderator")
|
||||
# Use FSM transition for queue status
|
||||
_complete_queue_item()
|
||||
result = {
|
||||
'status': 'rejected',
|
||||
'created_object': None,
|
||||
'message': 'Photo submission rejected'
|
||||
}
|
||||
elif action == 'escalate':
|
||||
result = {"status": "rejected", "created_object": None, "message": "Photo submission rejected"}
|
||||
elif action == "escalate":
|
||||
submission.escalate(moderator, notes or "Escalated for review")
|
||||
queue_item.priority = 'HIGH'
|
||||
queue_item.priority = "HIGH"
|
||||
# Keep status as PENDING for escalation
|
||||
result = {
|
||||
'status': 'escalated',
|
||||
'created_object': None,
|
||||
'message': 'Photo submission escalated'
|
||||
}
|
||||
result = {"status": "escalated", "created_object": None, "message": "Photo submission escalated"}
|
||||
else:
|
||||
raise ValueError(f"Unknown action: {action}")
|
||||
else:
|
||||
@@ -678,5 +636,5 @@ class ModerationService:
|
||||
queue_item.full_clean()
|
||||
queue_item.save()
|
||||
|
||||
result['queue_item'] = queue_item
|
||||
result["queue_item"] = queue_item
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user