Adopt django-allauth with allauth.mfa, giving TOTP, WebAuthn passkeys and
recovery codes — and the signup/password-reset flows we'll need next. There was
no login UI at all before this (only /admin/), so this brings the auth stack.
The critical piece is authentication/adapters.py. A passkey is bound to a
WebAuthn Relying Party ID (a domain), and allauth derives that from the request
host — which under our subdomain tenancy would bind a passkey to a *single* club
(ajax-united.clubmanager.app) and silently fail at every other one. The adapter
pins the RP ID to CLUBMANAGER_BASE_DOMAIN so one passkey works across all clubs.
Note this cuts both ways: changing that base domain invalidates every existing
passkey.
RequireMFAMiddleware makes a second factor mandatory for anyone who can change
other people's data — Django staff/superusers and holders of an elevated
ClubRole (ADMIN/EDITOR), via the access service — while leaving it optional for
regular members. /admin/login/ is routed through allauth, since Django's own
admin login knows nothing about second factors.
allauth is installed WITHOUT django.contrib.sites (optional since allauth 65),
so ARCHITECTURE.md's rejection of the Sites framework stands and no Club.site
bridge is needed. Sessions are shared across club subdomains, matching the
one-passkey-everywhere model; tenancy still scopes what you can see.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ClubRole (ADMIN / MEMBER / EDITOR, one per member per club) and complete
club/services/access.py — the single module all authorisation routes through:
- teams_managed_by / can_edit_event -> authority: a *management* StaffAssignment
in the *current season*; ADMIN overrides club-wide. A StaffAssignment is
per-season, so a former coach's authority expires with it.
- teams_staffed_by -> visibility: *any* staff position, so support staff (physio,
kit manager) can see the roster they work with without gaining authority.
- members_visible_to -> ADMIN sees everyone linked to the club; otherwise self +
children (family graph) + the current-season players and staff of the teams
they're staffed on.
- can_edit_event -> ADMIN/EDITOR, the event's owner, or a manager of one of its
teams for that event's season.
- can_manage_shop -> ADMIN.
Fix roles_in_club, which called .unique() — not a QuerySet method, so it would
have raised AttributeError on first use.
Keep ClubRole in sync with membership status: an active ClubMembership grants
the MEMBER role and losing it withdraws that role — but an elevated role
(ADMIN/EDITOR) is never downgraded or removed, so a lapsed membership or a
season rollover can never lock an admin out.
Validate ClubMembership.season against the membership's club.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix multi-tenancy/integrity bugs in the models: Form.slug and Field.key
were globally unique (unique=True), so two clubs couldn't reuse a form
slug and two forms couldn't reuse a field key — make slug unique per club
(constraint already present) and key unique per form. Add a
(submission, field) uniqueness constraint on Answer.
Register all four models in the admin (Field inline on Form, Answer inline
on Submission) and add formbuilder to the admin registration smoke test.
Add a service layer:
- submit_form(form, member, data): enforces is_active / login_required /
open window / max_submissions, validates required + choice fields, and
writes a Submission with Answers atomically (FormSubmissionError carries
per-field errors).
- build_form(form): a live django.forms.Form built from a Form's active
fields, mapping each FieldType to the matching form field.
- form_report(form): a tabular overview of every submission's answers plus
per-value tallies for choice-type fields.
Full suite at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework an event's audience: replace the single team FK with a teams M2M
plus invited_members and excluded_members, and add a season FK (derived
from the start date when blank) so team rosters resolve correctly. A data
migration copies existing team -> teams.
Add an attendance sync service: the effective audience is the union of the
teams' rosters for the event's season plus invited, minus excluded;
sync_event_attendances reconciles Attendance rows for future events only,
adding NO_RESPONSE rows for new members and hard-deleting rows for members
no longer invited. Signals drive it: editing an event or its audience
re-syncs that event, and adding/removing a team-roster member re-syncs
that team's future events.
Register all events models in the admin (with an attendance inline) and
add events to the admin registration smoke test. Add Season.covering()
and pillow (Opponent.logo ImageField). Full suite at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ClubMembership is now tied to a Season (plus status / fee_status / sign-up
dates) and unique per (club, member, season). The CSV importer attaches
each membership to the club's current season (Season.get_current), skipping
the row with a clear error when none exists; the now-unreachable
"clubs created" bookkeeping is removed.
Register SeasonAdmin and rebuild ClubMembershipAdmin for the new fields, and
add an admin smoke test that asserts every model in authentication/club/
members/teams is registered and its changelist + add pages load.
Convert every unique_together to a Meta UniqueConstraint (Season,
ClubMembership, FamilyMembership) per Django's guidance. Regenerate
migrations. club, members, and importer stay at 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complete the Season model:
- name property renders the start/end years as a "YY-YY" label (e.g.
"25-26") via strftime %y, and __str__ now returns it.
- get_current(date) returns the active club's season covering the given
date (today by default), inclusive of both boundaries, scoped through
the tenant queryset so it never crosses clubs.
Rename the tenant queryset's current() to current_club() for clarity and
update callers/tests. Cover the new behaviour; tenancy modules stay at
100%.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Exercise subdomain resolution (base-domain and generic hosts, unknown
slug, www, port stripping, contextvar cleanup), slug derivation and
uniqueness, the require_current_club/current() context helpers, and
ClubScopedModel save/for_club/current via Season. Add a with_club test
helper. Tenancy modules reach 100% coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Finish relocating Member/Family/FamilyMembership from authentication into
a dedicated members app, and revert the half-applied move of member names
onto the global User.
- members: add first_name/last_name back to Member (the whole codebase —
tests, CSV importer, club app, admin — assumes them, and login-less
children in families need a name); restore local ordering/index and the
member__last_name lookups in Family.__str__ and FamilyMembership.
- authentication: drop first_name/last_name from User; get_full_name/
get_short_name delegate to the linked member, else fall back to email.
- migrations: create members.0001_initial, repoint club.ClubMembership FK
(club.0005), delete the models from authentication (0004, rewritten to
plain DeleteModel ops in child-first order to avoid a SQLite table-remake
crash).
- fix stale imports across apps (authentication/club tests, CSV importer)
and missing imports/URLs in members tests; add missing _ import in
members/admin.py; export MemberCsvImporter from members.services.
Full suite green (64 tests), ruff clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Extend the ClubMembership model with `club`, `member`, and optional `license` fields, along with relevant constraints and ordering.
- Implement verbose names for Club and ClubMembership models and update admin configurations for better display and filtering.
- Add a `import_members_csv` management command for batch importing members, clubs, and memberships from a CSV file.
- Include extensive tests for the `import_members_csv` command, ClubMembership model, and Club model.
- Refactor related migrations, services, and test structure.
- Split the accounts app into new authentication and club apps for better separation of concerns.
- Migrate the custom User, Member, and Family models to the authentication app.
- Introduce Club and ClubMembership models in the club app.
- Refactor Family model to use UUID as the primary key and consolidate family-role relationships into a new FamilyMembership model.
- Update tests, managers, and migrations to align with the new structure.