Add email notification for staff
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"""Outgoing email -- plan.md §7 (state-transition side effects).
|
||||
|
||||
Five public functions, one per dedicated template:
|
||||
Six public functions, one per dedicated template:
|
||||
|
||||
send_confirmation_email(submission)
|
||||
Guest path: token-link emailed immediately after `SubmitView` creates
|
||||
@@ -32,7 +32,15 @@ Five public functions, one per dedicated template:
|
||||
when present (typically pickup instructions). Template:
|
||||
`emails/completed.*`.
|
||||
|
||||
All five delegate to Django's email machinery. The backend is wired in
|
||||
send_staff_verify_email(submission)
|
||||
Operator broadcast fired on any transition into `status = verifying`.
|
||||
One message to every active staff user with a non-empty email
|
||||
(recipients placed in `bcc` so individual addresses stay hidden).
|
||||
Surfaces the submitter's private `notes_for_op` -- not echoed on the
|
||||
public detail page (plan.md §5) -- since the audience is staff only.
|
||||
Template: `emails/staff_verify.*`.
|
||||
|
||||
All six 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.
|
||||
@@ -175,3 +183,62 @@ def send_completed_email(sub: Submission) -> bool:
|
||||
pickup-instruction the operator typed in admin reaches the user."""
|
||||
detail_url = f"{settings.SITE_URL}/p/{sub.slug}/"
|
||||
return _send("completed", sub, {"detail_url": detail_url})
|
||||
|
||||
|
||||
def send_staff_verify_email(sub: Submission) -> bool:
|
||||
"""Broadcast a "please verify" notice to every active staff user when
|
||||
a submission lands in `verifying`. Fired by the centralised
|
||||
`Submission.save()` hook so the worker's `processing -> verifying`
|
||||
success branch AND any future admin path that returns a row to
|
||||
`verifying` both trigger it (plan.md §7.3).
|
||||
|
||||
Recipients are placed in `bcc` so individual staff addresses stay
|
||||
hidden from each other; `to` carries `DEFAULT_FROM_EMAIL` purely to
|
||||
satisfy transports that reject envelopes with no `to`. Returns False
|
||||
(without raising) when there is no active staff with a usable email
|
||||
or when the transport surfaces an exception.
|
||||
"""
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
UserModel = get_user_model()
|
||||
recipients = list(
|
||||
UserModel.objects
|
||||
.filter(is_staff=True, is_active=True)
|
||||
.exclude(email="")
|
||||
.values_list("email", flat=True)
|
||||
)
|
||||
if not recipients:
|
||||
logger.info(
|
||||
"no active staff with email; skipping staff_verify for %s", sub.slug
|
||||
)
|
||||
return False
|
||||
|
||||
admin_url = (
|
||||
f"{settings.SITE_URL}/admin/submissions/submission/{sub.pk}/change/"
|
||||
)
|
||||
detail_url = f"{settings.SITE_URL}/p/{sub.slug}/"
|
||||
context = {
|
||||
"submission": sub,
|
||||
"site_url": settings.SITE_URL,
|
||||
"admin_url": admin_url,
|
||||
"detail_url": detail_url,
|
||||
}
|
||||
subject, body_text, body_html = _render("staff_verify", context)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=body_text,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[settings.DEFAULT_FROM_EMAIL],
|
||||
bcc=recipients,
|
||||
)
|
||||
msg.attach_alternative(body_html, "text/html")
|
||||
try:
|
||||
msg.send()
|
||||
except Exception:
|
||||
logger.exception("failed to send staff_verify email for %s", sub.slug)
|
||||
return False
|
||||
logger.info(
|
||||
"sent staff_verify email for %s to %d staff", sub.slug, len(recipients)
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -295,11 +295,14 @@ class Submission(models.Model):
|
||||
depend on the caller remembering to set it.
|
||||
|
||||
Additionally: when an UPDATE flips `status` to a state with a
|
||||
dedicated email (`rejected`, `printing`, `completed`), this method
|
||||
queues the matching `send_*_email()` via `transaction.on_commit`.
|
||||
Centralising the dispatch here means **every** save path -- admin,
|
||||
the validation worker, ad-hoc shell, any future view -- fires the
|
||||
email through a single hook. Plan.md §7.3.
|
||||
dedicated email (`rejected`, `printing`, `completed`, `verifying`),
|
||||
this method queues the matching `send_*_email()` via
|
||||
`transaction.on_commit`. Centralising the dispatch here means
|
||||
**every** save path -- admin, the validation worker, ad-hoc shell,
|
||||
any future view -- fires the email through a single hook. The
|
||||
`verifying` branch is a staff broadcast (the user-facing
|
||||
`send_verifying_email` is still emitted explicitly by the worker
|
||||
on its success branch). Plan.md §7.3.
|
||||
"""
|
||||
if not self.slug:
|
||||
self.slug = self._generate_unique_slug()
|
||||
@@ -323,9 +326,9 @@ class Submission(models.Model):
|
||||
# Fire on TRANSITIONS only: an UPDATE that flips status into one of
|
||||
# the email-bearing target states. Don't fire on inserts that start
|
||||
# out in those states -- by plan.md §7.3 no submit-time edge lands
|
||||
# in rejected/printing/completed, and even if some weird path did,
|
||||
# we'd rather stay silent than send "your print is ready" to a fresh
|
||||
# victim of a fixture/data-migration import.
|
||||
# in rejected/printing/completed/verifying, and even if some weird
|
||||
# path did, we'd rather stay silent than send "your print is ready"
|
||||
# to a fresh victim of a fixture/data-migration import.
|
||||
if not is_new and old_status != new_status:
|
||||
# Local imports keep this module out of the apps/submissions
|
||||
# import-cycle (emails.py imports from here).
|
||||
@@ -334,6 +337,7 @@ class Submission(models.Model):
|
||||
send_completed_email,
|
||||
send_printing_email,
|
||||
send_rejection_email,
|
||||
send_staff_verify_email,
|
||||
)
|
||||
|
||||
if new_status == self.Status.REJECTED:
|
||||
@@ -350,6 +354,10 @@ class Submission(models.Model):
|
||||
transaction.on_commit(
|
||||
lambda sub=self: send_completed_email(sub)
|
||||
)
|
||||
elif new_status == self.Status.VERIFYING:
|
||||
transaction.on_commit(
|
||||
lambda sub=self: send_staff_verify_email(sub)
|
||||
)
|
||||
|
||||
# Refresh the snapshot so a follow-up save on the same instance
|
||||
# compares against the just-persisted state, not the original load.
|
||||
|
||||
Reference in New Issue
Block a user