From 2d2cf46f325515bab348b9c739578ab4e89f3fb5 Mon Sep 17 00:00:00 2001 From: Simonas Kareiva Date: Fri, 15 May 2026 16:47:37 +0300 Subject: [PATCH] add more status updates --- apps/submissions/emails.py | 33 +++++++++++++++- apps/submissions/models.py | 53 ++++++++++++++++---------- templates/emails/completed.body.html | 51 +++++++++++++++++++++++++ templates/emails/completed.body.txt | 19 +++++++++ templates/emails/completed.subject.txt | 1 + templates/emails/printing.body.html | 37 ++++++++++++++++++ templates/emails/printing.body.txt | 17 +++++++++ templates/emails/printing.subject.txt | 1 + 8 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 templates/emails/completed.body.html create mode 100644 templates/emails/completed.body.txt create mode 100644 templates/emails/completed.subject.txt create mode 100644 templates/emails/printing.body.html create mode 100644 templates/emails/printing.body.txt create mode 100644 templates/emails/printing.subject.txt diff --git a/apps/submissions/emails.py b/apps/submissions/emails.py index 4f41a5d..0e436c4 100644 --- a/apps/submissions/emails.py +++ b/apps/submissions/emails.py @@ -1,6 +1,6 @@ """Outgoing email -- plan.md §7 (state-transition side effects). -Three public functions, one per dedicated template: +Five public functions, one per dedicated template: send_confirmation_email(submission) Guest path: token-link emailed immediately after `SubmitView` creates @@ -20,7 +20,19 @@ Three public functions, one per dedicated template: 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 + send_printing_email(submission) + Fired on any transition into `status = printing` (operator clicks + "Start printing" in admin). Excited tone: "your print is on the + bed right now". Template: `emails/printing.*`. + + send_completed_email(submission) + Fired on any transition into `status = completed` (operator clicks + "Mark completed"). Pickup-ready announcement; renders + `submission.operator_notes` as a "note from the operator" callout + when present (typically pickup instructions). Template: + `emails/completed.*`. + +All five 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. @@ -146,3 +158,20 @@ def send_verifying_email(sub: Submission) -> bool: will be the queued / printing one.""" detail_url = f"{settings.SITE_URL}/p/{sub.slug}/" return _send("verifying", sub, {"detail_url": detail_url}) + + +def send_printing_email(sub: Submission) -> bool: + """Notify the submitter that the print has just started (plan.md §7.3 + `queued -> printing` transition). Excited tone -- the operator just + clicked "Start printing" in admin and the first layer is going down.""" + detail_url = f"{settings.SITE_URL}/p/{sub.slug}/" + return _send("printing", sub, {"detail_url": detail_url}) + + +def send_completed_email(sub: Submission) -> bool: + """Notify the submitter that the print finished successfully and is + ready for pickup (plan.md §7.3 `printing -> completed` transition). + `submission.operator_notes` is rendered when present so any + 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}) diff --git a/apps/submissions/models.py b/apps/submissions/models.py index 5200355..5d318a3 100644 --- a/apps/submissions/models.py +++ b/apps/submissions/models.py @@ -294,11 +294,12 @@ class Submission(models.Model): currently owns the row, so the per-email cap and trust list don't depend on the caller remembering to set it. - Additionally: when an UPDATE flips `status` to `rejected` from any - other state, this method queues `send_rejection_email()` via - `transaction.on_commit`. Centralising the email 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. + 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. """ if not self.slug: self.slug = self._generate_unique_slug() @@ -319,27 +320,37 @@ class Submission(models.Model): super().save(*args, **kwargs) - # Fire on TRANSITIONS only: an UPDATE that flips status to rejected. - # Don't fire on inserts that start out as rejected -- those should - # be impossible by design (plan.md §7.3 doesn't define a (none) -> - # rejected edge), and even if some weird path creates one we'd - # rather stay silent than spam a fresh victim. - if ( - not is_new - and old_status != new_status - and new_status == self.Status.REJECTED - ): + # 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. + 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). from django.db import transaction - from .emails import send_rejection_email - - transaction.on_commit( - lambda sub=self, prev=old_status: send_rejection_email( - sub, previous_status=prev - ) + from .emails import ( + send_completed_email, + send_printing_email, + send_rejection_email, ) + if new_status == self.Status.REJECTED: + transaction.on_commit( + lambda sub=self, prev=old_status: send_rejection_email( + sub, previous_status=prev + ) + ) + elif new_status == self.Status.PRINTING: + transaction.on_commit( + lambda sub=self: send_printing_email(sub) + ) + elif new_status == self.Status.COMPLETED: + transaction.on_commit( + lambda sub=self: send_completed_email(sub) + ) + # Refresh the snapshot so a follow-up save on the same instance # compares against the just-persisted state, not the original load. self._original_status = new_status diff --git a/templates/emails/completed.body.html b/templates/emails/completed.body.html new file mode 100644 index 0000000..dfe08ec --- /dev/null +++ b/templates/emails/completed.body.html @@ -0,0 +1,51 @@ +{% extends "emails/_base.html" %} +{% block body %} +

Your print is ready

+ +

+ Done! {{ submission.slug }} came off the printer successfully and is waiting for you at hamlab.lt. +

+ +{# Pill colour comes from `Submission.STATUS_EMAIL_COLORS[COMPLETED]` -- the emerald palette used everywhere else for the "success" terminal state. #} + + + + +
+ + {{ submission.get_status_display }} + +
+ +{% if submission.operator_notes %} +{# Pickup-instructions callout -- only rendered when the operator left a note (e.g. "in the green bin by the lasers"). #} + + + + +
+

+ Note from the operator +

+

{{ submission.operator_notes }}

+
+{% endif %} + +

+ Come grab it whenever the lab is open. Thanks for printing with us! +

+ + + + + +
+ + View pickup details + +
+ +

+ Direct link: {{ detail_url }} +

+{% endblock %} diff --git a/templates/emails/completed.body.txt b/templates/emails/completed.body.txt new file mode 100644 index 0000000..fa07f38 --- /dev/null +++ b/templates/emails/completed.body.txt @@ -0,0 +1,19 @@ +Hi, + +Your hamprint is done! "{{ submission.slug }}" came off the printer +successfully and is waiting for you at hamlab.lt. + + Codename : {{ submission.slug }} + Status : Completed +{% if submission.operator_notes %} +A note from the operator: + + {{ submission.operator_notes }} +{% endif %} +Come grab it whenever the lab is open. Thanks for printing with us! + +Pickup details and a photo (if the operator left one) are here: + {{ detail_url }} + +— hamprint +{{ site_url }} diff --git a/templates/emails/completed.subject.txt b/templates/emails/completed.subject.txt new file mode 100644 index 0000000..6999f5a --- /dev/null +++ b/templates/emails/completed.subject.txt @@ -0,0 +1 @@ +hamprint: {{ submission.slug }} is ready for pickup diff --git a/templates/emails/printing.body.html b/templates/emails/printing.body.html new file mode 100644 index 0000000..914feca --- /dev/null +++ b/templates/emails/printing.body.html @@ -0,0 +1,37 @@ +{% extends "emails/_base.html" %} +{% block body %} +

Your print is starting

+ +

+ Great news — {{ submission.slug }} is on the printer right now. The operator has started the job and the first layer is going down. +

+ +{# Pill colour comes from `Submission.STATUS_EMAIL_COLORS[PRINTING]` -- warm orange to mirror the live-printing chip on the dashboard. #} + + + + +
+ + {{ submission.get_status_display }} + +
+ +

+ We'll email you again the moment it finishes so you know when to come pick it up. Nothing for you to do right now — feel free to track progress at the link below. +

+ + + + + +
+ + Follow the print + +
+ +

+ Direct link: {{ detail_url }} +

+{% endblock %} diff --git a/templates/emails/printing.body.txt b/templates/emails/printing.body.txt new file mode 100644 index 0000000..2aded02 --- /dev/null +++ b/templates/emails/printing.body.txt @@ -0,0 +1,17 @@ +Hi, + +Great news -- your hamprint submission "{{ submission.slug }}" is on the +printer right now. The operator has started the job and the first layer +is going down. + + Codename : {{ submission.slug }} + Status : Printing + +We'll send one more email when it finishes, so you know when to come +pick it up. No action needed in the meantime. + +You can also follow along here: + {{ detail_url }} + +— hamprint +{{ site_url }} diff --git a/templates/emails/printing.subject.txt b/templates/emails/printing.subject.txt new file mode 100644 index 0000000..7ff00ca --- /dev/null +++ b/templates/emails/printing.subject.txt @@ -0,0 +1 @@ +hamprint: {{ submission.slug }} is on the printer -- 2.49.1