Move email previews onto the Club identity page; add PDF previews

Three top-level tabs (Identity/Email/PDF) replace the standalone
Settings > Email previews page and the small mock that used to live
inside the Identity tab's own Email sub-tab. Email and PDF now show
every real branded email/generated PDF this club can send, rendered
with sample data via a shared _document_preview_card.html partial
(email cards keep the HTML/plain-text toggle; PDF cards -- the
underlying HTML WeasyPrint would turn into a PDF, shown directly
rather than round-tripping through WeasyPrint -- don't need one).

Added a password-reset email preview (previously missing from the
audit -- it's allauth's own send path, not one of this app's, so it
didn't show up in the earlier grep for EmailMultiAlternatives/.send()
calls). Dropped parent-claim-approved: not directly used right now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-21 08:52:52 +02:00
parent 6d69ff7230
commit 3ff882d705
12 changed files with 423 additions and 310 deletions

View File

@@ -1,15 +1,15 @@
"""Sample renders of every branded email this app can send -- the Settings >
Email previews page (management/templates/management/email_previews.html),
"""Sample renders of every branded email this app can send -- the Email tab
on the Club identity page (management/templates/management/club_settings.html),
so a club can see exactly what a member/parent receives without anything
actually being sent.
Each entry renders the *real* templates the real send functions use (see
members.services.claims.send_claim_approved_email and
club.services.invoicing.send_invoice_email/send_reminder_email) against a
hand-built sample context -- never a real Member/ClubMembership/DuesInvoice
row, so this needs nothing from the database beyond the current club itself,
and can't leak anything real. Adding a new branded email later means adding
one entry here, not touching the view or template.
club.services.invoicing.send_invoice_email/send_reminder_email, and allauth's
own password-reset flow via templates/account/email/password_reset_key_message.html)
against a hand-built sample context -- never a real Member/ClubMembership/
DuesInvoice row, so this needs nothing from the database beyond the current
club itself, and can't leak anything real. Adding a new branded email later
means adding one entry here, not touching the view or template.
"""
import datetime
@@ -34,16 +34,6 @@ class EmailPreview:
build_context: Callable[..., dict]
def _claim_approved_context(club, request):
return {
"club": club,
"child": "Jamie Doe",
"parent_first_name": "Alex",
"set_password_url": f"https://{club.slug}.rosterchief.app/accounts/password/reset/key/example/",
"request": request,
}
def _dues_invoice_context(club, request, *, overdue):
today = timezone.now().date()
due_date = today - datetime.timedelta(days=5) if overdue else today + datetime.timedelta(days=14)
@@ -53,6 +43,21 @@ def _dues_invoice_context(club, request, *, overdue):
return {"club": club, "invoice": invoice, "membership": membership, "member": member, "request": request}
def _password_reset_context(club, request):
# Mirrors allauth.account.internal.flows.password_reset.request_password_reset's
# own context -- current_site is only used by the *stock* .txt template's
# base_message.txt wrapper (our .html override doesn't reference it, but the
# .txt preview does), username only matters when ACCOUNT_LOGIN_METHODS
# includes "username" (it doesn't here -- login is email-only), so it's left
# out entirely rather than faked.
return {
"club": club,
"current_site": SimpleNamespace(name="RosterChief", domain="rosterchief.app"),
"password_reset_url": f"https://{club.slug}.rosterchief.app/accounts/password/reset/key/example/",
"request": request,
}
EMAIL_PREVIEWS = [
EmailPreview(
key="dues_invoice",
@@ -73,16 +78,18 @@ EMAIL_PREVIEWS = [
build_context=lambda club, request: _dues_invoice_context(club, request, overdue=True),
),
EmailPreview(
key="claim_approved",
label=_("Parent claim approved"),
description=_("Sent when a staff member approves a parent or guardian's claim to a child, from the Parent claims page."),
subject_template="members/email/claim_approved_subject.txt",
text_template="members/email/claim_approved.txt",
html_template="members/email/claim_approved.html",
build_context=_claim_approved_context,
key="password_reset",
label=_("Password reset"),
description=_("Sent by the “Forgot your password?” link on the sign-in page."),
subject_template="account/email/password_reset_key_subject.txt",
text_template="account/email/password_reset_key_message.txt",
html_template="account/email/password_reset_key_message.html",
build_context=_password_reset_context,
),
]
EMAIL_PREVIEWS_BY_KEY = {preview.key: preview for preview in EMAIL_PREVIEWS}
def render_preview(preview: EmailPreview, *, club, request) -> dict:
context = preview.build_context(club, request)