Add detail page

This commit is contained in:
2026-05-15 00:08:14 +03:00
parent 219f0a5259
commit 15dc6147dd
9 changed files with 295 additions and 22 deletions

View File

@@ -1,18 +1,26 @@
"""Outgoing email -- plan.md §7 (state-transition side effects).
Two public functions:
Three public functions, one per dedicated template:
send_confirmation_email(submission)
Guest path: token-link emailed immediately after `SubmitView` creates
an `identifying` row. The user must click within 24 h (plan.md §7.6)
or the row is cleaned up.
or the row is cleaned up. Template: `emails/confirmation.*`.
send_status_update_email(submission, *, previous_status=None)
Generic notifier for any state transition the user should know about
(queued, rejected, completed, failed). Callers pick when to fire it
-- typically operator admin actions and the validation worker.
send_verifying_email(submission)
Fired by the `process_submissions` worker on the `processing ->
verifying` success branch: "auto-checks cleared, awaiting operator
review". Template: `emails/verifying.*`.
Both delegate to Django's email machinery. The backend is wired in
send_rejection_email(submission, *, previous_status=None)
Always fired on any transition into `status = rejected`, whether
auto (validator failure from `processing`) or operator-driven (admin
"Reject" action from `verifying`). The body renders
`submission.operator_notes` verbatim so the user sees the same
rejection reason in the email as on the public detail page.
Template: `emails/rejected.*`.
All three delegate to Django's email machinery. The backend is wired in
`hamprint/settings/base.py`: Mailtrap via `django-anymail` when
`MAILTRAP_API_TOKEN` is present, console when not. Send failures are caught
+ logged so a flaky transport never blocks the submission flow.
@@ -104,19 +112,30 @@ def send_confirmation_email(sub: Submission) -> bool:
return _send("confirmation", sub, {"confirm_url": confirm_url})
def send_status_update_email(
def send_rejection_email(
sub: Submission, *, previous_status: str | None = None
) -> bool:
"""Notify the submitter that their submission moved to a new state.
"""Notify the submitter that their submission was rejected.
Pass `previous_status` so the email can render "was X, now Y" when
useful; omit it for first-time-ever notifications.
Always fired on any transition into `status = rejected`:
- automatic rejection from `processing` (URL/STL validation failure),
- operator rejection from `verifying` (admin "Reject" action).
The email body renders `submission.operator_notes` verbatim -- that's
the same string the auto-validator writes ("Automatic rejection: …")
or that an operator types when clicking "Reject" in admin, so the
user sees one consistent reason across the email + the public detail
page.
`previous_status` is the state we left to land in `rejected`. Useful
so the email can subtly distinguish "rejected before a human even
looked" (from `processing`) vs. "rejected after operator review"
(from `verifying`); both render with the same template.
"""
detail_url = f"{settings.SITE_URL}/p/{sub.slug}/"
return _send(
"status_update",
"rejected",
sub,
{"detail_url": detail_url, "previous_status": previous_status},
{"previous_status": previous_status},
)

View File

@@ -18,7 +18,7 @@ from django.core.management.base import BaseCommand
from django.db import transaction
from django.utils import timezone
from apps.submissions.emails import send_status_update_email, send_verifying_email
from apps.submissions.emails import send_rejection_email, send_verifying_email
from apps.submissions.models import Submission
from apps.submissions.validation import (
ValidationError,
@@ -71,7 +71,7 @@ class Command(BaseCommand):
# closed_by stays NULL -- the validator did the rejecting,
# not an operator (plan.md §5 / §7.3).
sub.save()
send_status_update_email(sub, previous_status="processing")
send_rejection_email(sub, previous_status="processing")
self.stdout.write(f"rejected {sub.slug}: {exc}")
else:
sub.status = Submission.Status.VERIFYING