Files
RosterChief/management/email_previews.py
Bernard Siebens 3ff882d705 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
2026-08-21 08:52:52 +02:00

100 lines
4.4 KiB
Python

"""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
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
from collections.abc import Callable
from dataclasses import dataclass
from decimal import Decimal
from types import SimpleNamespace
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
@dataclass(frozen=True)
class EmailPreview:
key: str
label: str
description: str
subject_template: str
text_template: str
html_template: str
build_context: Callable[..., dict]
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)
invoice = SimpleNamespace(number="DUE-2026-00042", amount=Decimal("45.00"), due_date=due_date)
membership = SimpleNamespace(season="2026-2027")
member = SimpleNamespace(first_name="Jamie")
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",
label=_("Membership invoice"),
description=_("Sent when a staff member clicks “Send invoice” for one or more members on the Dues & billing page."),
subject_template="club/email/dues_invoice_subject.txt",
text_template="club/email/dues_invoice.txt",
html_template="club/email/dues_invoice.html",
build_context=lambda club, request: _dues_invoice_context(club, request, overdue=False),
),
EmailPreview(
key="dues_invoice_reminder",
label=_("Invoice reminder"),
description=_("Sent by the “Send reminders” button, once per invoice that's still unpaid past its due date."),
subject_template="club/email/dues_invoice_reminder_subject.txt",
text_template="club/email/dues_invoice_reminder.txt",
html_template="club/email/dues_invoice_reminder.html",
build_context=lambda club, request: _dues_invoice_context(club, request, overdue=True),
),
EmailPreview(
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)
subject = " ".join(render_to_string(preview.subject_template, context).split())
text_body = render_to_string(preview.text_template, context).strip()
html_body = render_to_string(preview.html_template, context)
return {"subject": subject, "text": text_body, "html": html_body}