feat: Introduce a CLAIMED state for moderation submissions, requiring claims before approval or rejection, and add a scheduled task to expire stale claims.

This commit is contained in:
pacnpal
2026-01-07 13:41:52 -05:00
parent 28c9ec56da
commit 40cba5bdb2
8 changed files with 450 additions and 94 deletions

View File

@@ -0,0 +1,95 @@
"""
Management command to expire stale claims on submissions.
This command can be run manually or via cron as an alternative to the Celery
scheduled task when Celery is not available.
Usage:
python manage.py expire_stale_claims
python manage.py expire_stale_claims --minutes=10 # Custom timeout
"""
from django.core.management.base import BaseCommand
from apps.moderation.tasks import expire_stale_claims, DEFAULT_LOCK_DURATION_MINUTES
class Command(BaseCommand):
help = "Release stale claims on submissions that have exceeded the lock timeout"
def add_arguments(self, parser):
parser.add_argument(
"--minutes",
type=int,
default=DEFAULT_LOCK_DURATION_MINUTES,
help=f"Minutes after which a claim is considered stale (default: {DEFAULT_LOCK_DURATION_MINUTES})",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be released without actually releasing",
)
def handle(self, *args, **options):
from datetime import timedelta
from django.utils import timezone
from apps.moderation.models import EditSubmission, PhotoSubmission
minutes = options["minutes"]
dry_run = options["dry_run"]
cutoff_time = timezone.now() - timedelta(minutes=minutes)
self.stdout.write(f"Looking for claims older than {minutes} minutes...")
self.stdout.write(f"Cutoff time: {cutoff_time.isoformat()}")
# Find stale claims
stale_edit = EditSubmission.objects.filter(
status="CLAIMED",
claimed_at__lt=cutoff_time,
).select_related("claimed_by")
stale_photo = PhotoSubmission.objects.filter(
status="CLAIMED",
claimed_at__lt=cutoff_time,
).select_related("claimed_by")
stale_edit_count = stale_edit.count()
stale_photo_count = stale_photo.count()
if stale_edit_count == 0 and stale_photo_count == 0:
self.stdout.write(self.style.SUCCESS("No stale claims found."))
return
self.stdout.write(f"Found {stale_edit_count} stale EditSubmission claims:")
for sub in stale_edit:
self.stdout.write(
f" - ID {sub.id}: claimed by {sub.claimed_by} at {sub.claimed_at}"
)
self.stdout.write(f"Found {stale_photo_count} stale PhotoSubmission claims:")
for sub in stale_photo:
self.stdout.write(
f" - ID {sub.id}: claimed by {sub.claimed_by} at {sub.claimed_at}"
)
if dry_run:
self.stdout.write(self.style.WARNING("\n--dry-run: No changes made."))
return
# Run the actual expiration task
result = expire_stale_claims(lock_duration_minutes=minutes)
self.stdout.write(self.style.SUCCESS("\nExpiration complete:"))
self.stdout.write(
f" EditSubmissions: {result['edit_submissions']['released']} released, "
f"{result['edit_submissions']['failed']} failed"
)
self.stdout.write(
f" PhotoSubmissions: {result['photo_submissions']['released']} released, "
f"{result['photo_submissions']['failed']} failed"
)
if result["failures"]:
self.stdout.write(self.style.ERROR("\nFailures:"))
for failure in result["failures"]:
self.stdout.write(f" - {failure}")