feat: Implement initial schema and add various API, service, and management command enhancements across the application.

This commit is contained in:
pacnpal
2026-01-01 15:13:01 -05:00
parent c95f99ca10
commit b243b17af7
413 changed files with 11164 additions and 17433 deletions

View File

@@ -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