75 lines
2.7 KiB
Python
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."
|
|
)
|
|
)
|