diff --git a/assets/management.css b/assets/management.css index 78e8c03..09db1a5 100644 --- a/assets/management.css +++ b/assets/management.css @@ -1811,10 +1811,12 @@ background: var(--color-paper); } - /* Live preview tabs -- a real (if minimal) tab control: click toggles which - .preview-panel is shown (club_settings.html's own inline script). App and - Website share one tab since the mobile app isn't built yet and the two - mocks were already always shown together underneath it. */ + /* A real (if minimal) tab control: click toggles which panel is shown -- + club_settings.html's own inline script uses this for its top-level + Identity/Email/PDF tabs. The Identity tab's own phone/website mock has + no tab of its own; the two are simply shown together (the mobile app + isn't built yet, and D9's separate App/Website tabs would only ever + have shown the same two mocks anyway). */ .preview-tab { padding: 0.3125rem 0.625rem; font-family: var(--font-display); @@ -2054,58 +2056,4 @@ .preview-brand-dot-neutral { background: var(--color-dim); } - - /* The Email tab -- a generic sample, not any one real message. Shaped like - templates/email/_base.html's actual output (plain white header with a - coloured crest badge, a primary-coloured button in the body) rather than - the phone/website mocks' solid colour bar: that's genuinely how every - branded email (club/templates/club/email/*.html, members/.../claim_approved.html) - looks, so this preview is accurate, not another approximation. */ - .preview-email { - overflow: hidden; - border: 1px solid var(--color-line); - border-radius: 0.625rem; - } - - .preview-email-header { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.625rem 0.75rem; - background: #fff; - border-bottom: 1px solid var(--color-line); - } - - .preview-email-crest { - transition: background-color 0.15s ease, color 0.15s ease; - } - - .preview-email-name { - font-family: var(--font-display); - font-size: 0.8125rem; - font-weight: 800; - letter-spacing: 0.04em; - text-transform: uppercase; - color: var(--color-ink); - } - - .preview-email-body { - display: flex; - flex-direction: column; - gap: 0.5rem; - padding: 0.875rem 0.75rem; - } - - .preview-email-button { - align-self: flex-start; - border-radius: 0.25rem; - padding: 0.3125rem 0.75rem; - margin: 0.125rem 0; - font-family: var(--font-display); - font-size: 0.6875rem; - font-weight: 800; - letter-spacing: 0.04em; - text-transform: uppercase; - transition: background-color 0.15s ease, color 0.15s ease; - } } diff --git a/management/context_processors.py b/management/context_processors.py index 39487ce..bed78d4 100644 --- a/management/context_processors.py +++ b/management/context_processors.py @@ -127,7 +127,6 @@ _NAV_SECTIONS = { "form_list": "form_list", "submission_list": "form_list", "club_settings": "club_settings", - "email_preview_list": "email_preview_list", "onboarding_requirement_list": "onboarding_requirement_list", "onboarding_requirement_create": "onboarding_requirement_list", "onboarding_requirement_update": "onboarding_requirement_list", @@ -173,7 +172,6 @@ _TOP_SECTION = { "invoice_list": "finance", "form_list": "settings", "club_settings": "settings", - "email_preview_list": "settings", "onboarding_requirement_list": "settings", "role_list": "settings", "position_list": "settings", diff --git a/management/email_previews.py b/management/email_previews.py index 01a9f4b..609cce7 100644 --- a/management/email_previews.py +++ b/management/email_previews.py @@ -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) diff --git a/management/pdf_previews.py b/management/pdf_previews.py new file mode 100644 index 0000000..f6a26ef --- /dev/null +++ b/management/pdf_previews.py @@ -0,0 +1,87 @@ +"""Sample renders of every PDF this app can generate -- the PDF tab on the +Club identity page (management/templates/management/club_settings.html). + +Both PDFs (management.pdf.event_referee_form_pdf, and +club.services.invoicing.invoice_pdf) are themselves just WeasyPrint run over +an HTML template -- see that HTML directly here rather than running +WeasyPrint for a preview: it's the same document, without needing the native +pango/cairo libraries installed just to look at it, and it's exactly what +management.views.EmailPreviewRenderView's sibling PDFPreviewRenderView shows +in an iframe. Same "hand-built sample context, no real DB rows" reasoning as +email_previews.py. +""" + +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 _ + +from .pdf import referee_form_colors + + +@dataclass(frozen=True) +class PDFPreview: + key: str + label: str + description: str + template: str + build_context: Callable[..., dict] + + +def _dues_invoice_pdf_context(club, request): + invoice = SimpleNamespace( + number="DUE-2026-00042", + amount=Decimal("45.00"), + due_date=timezone.now().date() + datetime.timedelta(days=14), + sent_at=timezone.now(), + is_paid=False, + sent_to_email="jamie.doe@example.com", + sent_to_guardian=False, + ) + membership = SimpleNamespace(season="2026-2027") + return {"club": club, "invoice": invoice, "membership": membership, "member": "Jamie Doe"} + + +def _referee_form_pdf_context(club, request): + referees = [ + SimpleNamespace(display_name="Jamie Doe", fee=Decimal("35.00"), km=Decimal("12.0"), km_rate=Decimal("0.35"), total_payable=Decimal("39.20")), + SimpleNamespace(display_name="Alex Referee", fee=Decimal("35.00"), km=None, km_rate=None, total_payable=Decimal("35.00")), + ] + event = SimpleNamespace( + start=timezone.now() + datetime.timedelta(days=7), + teams=SimpleNamespace(all=lambda: [SimpleNamespace(short_name="U16")]), + opponent="Leuven", + external_game_id="", + ) + home_location = SimpleNamespace(address="Sportlaan 1", zip_code="1000", city="Brussels") + grand_total = sum((referee.total_payable for referee in referees), Decimal("0")) + return {"club": club, "event": event, "referees": referees, "home_location": home_location, "grand_total": grand_total} | referee_form_colors(club) + + +PDF_PREVIEWS = [ + PDFPreview( + key="dues_invoice", + label=_("Membership invoice"), + description=_("The PDF attached to the membership invoice email, also downloadable from a member's invoice page."), + template="club/dues_invoice_pdf.html", + build_context=_dues_invoice_pdf_context, + ), + PDFPreview( + key="referee_form", + label=_("Referee payment form"), + description=_("Downloadable from a game's page, for the club-arranged referee(s) to sign."), + template="management/event_referee_form_pdf.html", + build_context=_referee_form_pdf_context, + ), +] + +PDF_PREVIEWS_BY_KEY = {preview.key: preview for preview in PDF_PREVIEWS} + + +def render_pdf_preview(preview: PDFPreview, *, club, request) -> str: + return render_to_string(preview.template, preview.build_context(club, request)) diff --git a/management/templates/management/_document_preview_card.html b/management/templates/management/_document_preview_card.html new file mode 100644 index 0000000..120707e --- /dev/null +++ b/management/templates/management/_document_preview_card.html @@ -0,0 +1,34 @@ +{% load i18n %} + +{% comment %} + One card in the Club identity page's Email/PDF tabs -- included with + `preview` (a dict: key/label/description, plus subject/text for an email), + `render_url` (the iframe's src, already resolved via {% url %} by the + caller since {% url %} can't take a variable view name here), and + `show_text_toggle` (True for an email, which has a plain-text body to + switch to; False for a PDF, which doesn't). +{% endcomment %} +
{{ preview.description }}
+ {% if preview.subject %} +{{ preview.text }}
+ {% endif %}
+