Files
hamprint/apps/submissions/management/commands/cleanup_stale.py
2026-05-12 21:10:54 +03:00

75 lines
2.7 KiB
Python

"""`python manage.py cleanup_stale` -- implements plan.md §7.6.
Permanently deletes submissions that have been stuck in `identifying`
(guest awaiting email confirmation) or `rejected` (auto- or operator-
rejected) for longer than the TTL window (default 24 h). Both the DB row
and any uploaded `.stl` file under `MEDIA_ROOT` are removed -- the
file-on-disk side is handled by the `post_delete` signal in
`apps/submissions/signals.py`.
Designed to be invoked on a loop from a sidecar service (plan.md §10):
while true; do python manage.py cleanup_stale; sleep 300; done
"""
from __future__ import annotations
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.db.models import Q
from django.utils import timezone
from apps.submissions.models import Submission
class Command(BaseCommand):
help = (
"Delete submissions stuck in `identifying` or `rejected` for more "
"than --ttl-hours. Removes both the DB row and any uploaded STL."
)
def add_arguments(self, parser) -> None:
parser.add_argument(
"--ttl-hours",
type=int,
default=24,
help="How long a row can sit in `identifying` / `rejected` before "
"being deleted. Default: 24, matching plan.md §7.6.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Report what would be deleted but make no changes.",
)
def handle(self, *args, ttl_hours: int, dry_run: bool, **opts) -> None:
cutoff = timezone.now() - timedelta(hours=ttl_hours)
# `identifying` rows are aged off `created_at` (the row hasn't moved
# state since it was created). `rejected` rows are aged off
# `closed_at` (the moment the operator or auto-validator closed them).
stale = Submission.objects.filter(
Q(status=Submission.Status.IDENTIFYING, created_at__lt=cutoff)
| Q(status=Submission.Status.REJECTED, closed_at__lt=cutoff)
)
if dry_run:
count = stale.count()
self.stdout.write(
f"DRY RUN: would delete {count} stale submission(s) older than "
f"{ttl_hours} h."
)
for sub in stale:
self.stdout.write(f" - {sub.slug} ({sub.status})")
return
# `QuerySet.delete()` fires `post_delete` per row, which is how the
# uploaded `.stl` file gets unlinked from MEDIA_ROOT (see signals.py).
total, _by_model = stale.delete()
if total:
self.stdout.write(
self.style.SUCCESS(
f"Deleted {total} stale submission(s) older than {ttl_hours} h."
)
)