Document the parent-claim, families, and email work in ARCHITECTURE.md
Covers submitted_by_user/family-merge semantics, season-scoped claim history, the member/guardian/both filter and Families list, sidebar counters, list pagination, and the HTML email templates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -466,6 +466,26 @@ ClubMembership(ClubScopedModel) # -> carries `club`
|
|||||||
cost the parent their place in the queue; the admin sees a distinct warning message
|
cost the parent their place in the queue; the admin sees a distinct warning message
|
||||||
(`management/views.py::ParentClaimApproveView`) telling them the email didn't go and to have
|
(`management/views.py::ParentClaimApproveView`) telling them the email didn't go and to have
|
||||||
the parent use "Forgot your password?" instead.
|
the parent use "Forgot your password?" instead.
|
||||||
|
- **A signed-in parent claiming a second child links, never duplicates.**
|
||||||
|
`ParentClaim.submitted_by_user` (nullable FK to `authentication.User`) records who was
|
||||||
|
actually logged in when a claim was submitted — kept separate from the free-text
|
||||||
|
`parent_email`/name fields, which stay exactly as typed as the admin's audit trail even when
|
||||||
|
they drift from the account. When set, `members/views.py::ParentClaimView` drops the parent
|
||||||
|
fields from the form entirely (shown as read-only "submitting as…" text, not just pre-filled
|
||||||
|
inputs — a disabled input doesn't even submit, and a pre-filled-but-editable one still lets a
|
||||||
|
typo through) and populates them server-side from the `Member` instead of trusting POST data.
|
||||||
|
On approval, `members/services/claims.py::approve_claim` resolves the parent via that FK
|
||||||
|
(never re-derived from email) and, if they already belong to a `Family`, moves the newly
|
||||||
|
claimed child's `FamilyMembership` onto *that* family rather than leaving them split across
|
||||||
|
two households — the child's now-empty solo family is cleaned up the same way
|
||||||
|
`members/services/family.py::detach_from_family` already does elsewhere. No existing family →
|
||||||
|
falls back to the original behaviour of joining the child's own family. This is the one place
|
||||||
|
a parent, once they exist, is the anchor for "one household," not the child.
|
||||||
|
- **The "already dealt with" history is scoped to the current season.** Last season's approved
|
||||||
|
and rejected claims are real history but not something a club needs staring at year after
|
||||||
|
year, so `management/views.py::ParentClaimListView` filters `reviewed` to
|
||||||
|
`reviewed_at__date__gte=` the current season's start (empty, not erroring, when there's no
|
||||||
|
current season) rather than dropping the section outright.
|
||||||
- **Why a field and not a separate model.** Everything that answers "is this person attached
|
- **Why a field and not a separate model.** Everything that answers "is this person attached
|
||||||
to this club" already reads through `ClubMembership` — tenancy scoping, group membership,
|
to this club" already reads through `ClubMembership` — tenancy scoping, group membership,
|
||||||
the club-wide event audience — and a second kind of link would need a parallel path through
|
the club-wide event audience — and a second kind of link would need a parallel path through
|
||||||
@@ -483,6 +503,26 @@ ClubMembership(ClubScopedModel) # -> carries `club`
|
|||||||
weed guardians out would take those people with it. Pass `include_guardians=True` where the
|
weed guardians out would take those people with it. Pass `include_guardians=True` where the
|
||||||
page is about a *person* rather than about members — a guardian's own detail page, editing
|
page is about a *person* rather than about members — a guardian's own detail page, editing
|
||||||
them, the group pickers, or a family page (whose parents are the whole point).
|
them, the group pickers, or a family page (whose parents are the whole point).
|
||||||
|
- **The members list makes the member/guardian split visible instead of silent.**
|
||||||
|
`management/views.py::MemberListView` reads `?kind=member|guardian|both` (default `member`,
|
||||||
|
unchanged from before this filter existed) — without it, a guardian was simply absent from
|
||||||
|
the page with nothing explaining why, which read as a bug rather than the intended "guardians
|
||||||
|
aren't members" design. `management/views.py::FamilyListView` (`management:family_list`) is
|
||||||
|
the complementary household-first view: one row per family, its parents/guardians in one
|
||||||
|
column and its children in another, with the family name and a separate "Edit" action both
|
||||||
|
landing on `family_detail` — matching the "Edit goes to the overview, not a bare rename form"
|
||||||
|
convention the Groups list already established.
|
||||||
|
- **List views are paginated** (`MemberListView`, `EventListView`, `TeamListView`,
|
||||||
|
`GroupListView`, `NewsListView`, `FamilyListView`; `paginate_by = 25`) through one shared
|
||||||
|
`management/templates/management/_pagination.html` partial, using Django's `{% querystring %}`
|
||||||
|
tag so a page link preserves whatever `?q=`/`?kind=`/etc. filter is active rather than
|
||||||
|
resetting it.
|
||||||
|
- **The sidebar surfaces two counts that would otherwise require opening the page to see**:
|
||||||
|
pending parent claims, and games in the next 10 with no referee assigned (the latter shares
|
||||||
|
its query with `RefereeManagementDashboardView`'s own KPI, factored out so the two can't drift
|
||||||
|
apart). Both are admin-gated the same way the rest of that nav section is, and both render
|
||||||
|
`0` explicitly rather than hiding the badge — the point is to make "nothing outstanding" just
|
||||||
|
as visible as "something needs attention," not to declutter the zero case away.
|
||||||
- One row per member **per season** — sign-up and fee payment are tracked independently
|
- One row per member **per season** — sign-up and fee payment are tracked independently
|
||||||
each season. `unique_together` moves from `(club, member)` → `(club, member, season)`
|
each season. `unique_together` moves from `(club, member)` → `(club, member, season)`
|
||||||
(a data migration must backfill existing rows with the current season).
|
(a data migration must backfill existing rows with the current season).
|
||||||
@@ -1244,7 +1284,27 @@ Two ways to use **Resend** (resend.com), no third-party SDK either way:
|
|||||||
`RESEND_API_KEY=…`) posts each message straight to Resend's `/emails` endpoint via `requests`
|
`RESEND_API_KEY=…`) posts each message straight to Resend's `/emails` endpoint via `requests`
|
||||||
(already a dependency, so no new one needed for this). Handles plain text, the HTML
|
(already a dependency, so no new one needed for this). Handles plain text, the HTML
|
||||||
alternative on an `EmailMultiAlternatives`, cc/bcc/reply-to, and base64-encoded attachments;
|
alternative on an `EmailMultiAlternatives`, cc/bcc/reply-to, and base64-encoded attachments;
|
||||||
`fail_silently` is honoured the same way Django's own backends honour it.
|
`fail_silently` is honoured the same way Django's own backends honour it. Confirmed against
|
||||||
|
Resend's own docs: `POST /emails` accepts `html` alongside `text` (and will derive `text` from
|
||||||
|
`html` if only the former is given), so this backend needed no changes to carry the HTML
|
||||||
|
templates below.
|
||||||
|
- **HTML email templates** *(built)*. `templates/email/_base.html` is the one shared shell
|
||||||
|
(inline styles only, ~600px single-column table — email clients strip `<style>` blocks and
|
||||||
|
ignore linked stylesheets, so daisyUI/Tailwind's compiled CSS is irrelevant here) with a
|
||||||
|
`{% include "email/_button.html" %}` for a styled CTA link. Every HTML email extends it and
|
||||||
|
supplies a plain-text sibling — `members/services/claims.py::send_claim_approved_email` builds
|
||||||
|
an `EmailMultiAlternatives` from `claim_approved.txt` + `claim_approved.html`
|
||||||
|
(`.attach_alternative(html, "text/html")`), the same never-fail-the-caller contract as before
|
||||||
|
(`except OSError: return False`). `club/templatetags/club_email.py::absolute_media_url` turns
|
||||||
|
`Club.logo` (a relative `FileSystemStorage` URL) into something an inbox can actually fetch,
|
||||||
|
by resolving it against the `request` carried in the template context.
|
||||||
|
- **allauth's password-reset email is overridden**, not left at its default plain text —
|
||||||
|
`templates/account/email/password_reset_key_message.html` sits next to allauth's own
|
||||||
|
`.txt` at the path its `render_mail` already looks for (no Python-side override needed;
|
||||||
|
allauth auto-attaches a same-named `.html` as the HTML alternative). Branches on whether a
|
||||||
|
`club` is in context (a reset requested from a club subdomain gets that club's branding) and
|
||||||
|
falls back to a plain RosterChief wordmark otherwise (the platform-level login has no club to
|
||||||
|
brand it with).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user