Design player evaluations in ARCHITECTURE.md, reusing formbuilder
New §5.8: a thin PlayerEvaluation/EvaluationSettings envelope around formbuilder's already-built Form/Field/Submission/Answer (submission = evaluator, envelope = who it was about + team/season), club-wide EVALUATOR role independent of MEMBER_ADMIN, player profile combining evaluation history with existing attendance/games-per-team data. Never visible to the evaluated player or their guardians. Also corrects formbuilder's status (models/services are built, just no submit UI yet -- not "planned" as this doc previously had it) and moves the referee self-service item from "still open" to resolved, since it shipped this session. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
119
ARCHITECTURE.md
119
ARCHITECTURE.md
@@ -26,6 +26,12 @@ The `isort` `known-first-party` roadmap lists nine apps. The build split the ori
|
||||
two apps (`formbuilder`, `shop`) are added **beyond the original roadmap** — add all new
|
||||
labels to `known-first-party` in `pyproject.toml` when they land. The target decomposition:
|
||||
|
||||
> Several rows below (`teams`, `events`, `members`, `club`'s `Season`/`ClubRole`) are marked
|
||||
> "planned" but have since been built out considerably further than this table reflects —
|
||||
> it hasn't been kept in lockstep with every session's work. Only `formbuilder` and the new
|
||||
> `evaluations` row have been corrected here; treat the rest as directional, not current,
|
||||
> and verify against the actual tree (per `CLAUDE.md`) before relying on a "planned" marker.
|
||||
|
||||
| App | Status | Responsibility | Models |
|
||||
|------------------|--------------|-----------------------------------------------------------|--------|
|
||||
| `authentication` | **built** | Login identity + tenancy/role services (global, cross-club) | `User` |
|
||||
@@ -36,9 +42,10 @@ labels to `known-first-party` in `pyproject.toml` when they land. The target dec
|
||||
| `news` | **built** | Club news: coach_manager-authored, editor-released | `News`, `NewsPhoto` |
|
||||
| `pages` | planned | Flat CMS pages for the public site | `Page` |
|
||||
| `home` | planned | Homepage composition / featured content | `HomeConfig` (per-club) or config-only |
|
||||
| `formbuilder` | planned | Admin-defined dynamic forms + submissions + reporting | `Form`, `Field`, `Submission`, `Answer` |
|
||||
| `formbuilder` | **partial** | Admin-defined dynamic forms + submissions + reporting — models, dynamic-form-class builder, and the submit service are built (§5.6); no view/template renders a form for someone to fill in yet | `Form`, `Field`, `Submission`, `Answer` |
|
||||
| `shop` | planned | Cart-like shop, orders, payments, PDF invoices | `Product`, `Cart`, `CartItem`, `Order`, `OrderLine`, `Payment`, `Invoice` |
|
||||
| `search` | planned | Site search (likely no models; index/config only) | — |
|
||||
| `evaluations` | **design only** | Player evaluations: customizable rubric (reuses `formbuilder`), restricted to a new `EVALUATOR` role/ADMIN, player profile (skills + attendance + notes) (§5.8) | `EvaluationSettings`, `PlayerEvaluation` |
|
||||
|
||||
**`User` stays global** (one login identity across the whole platform); everything else
|
||||
that belongs to a club is tenant-scoped (§2.4). This is why `Member` — a *person within a
|
||||
@@ -217,17 +224,26 @@ platform-operator layer (`is_staff` / `is_superuser` in Django admin).
|
||||
```
|
||||
ClubRole(ClubScopedModel) # ClubScopedModel -> carries `club` (§2.4)
|
||||
member FK Member (CASCADE, related_name="roles")
|
||||
role CharField (TextChoices: MEMBER | EDITOR | TREASURER | BOARD)
|
||||
role CharField (TextChoices: MEMBER | EDITOR | TREASURER | BOARD | EVALUATOR)
|
||||
Meta: unique_together (club, member, role)
|
||||
```
|
||||
|
||||
| Role | Grants (representative) |
|
||||
|-------------|------------------------------------------------------------------------------|
|
||||
| *Public* | Anonymous — no row; read-only public site of that club. |
|
||||
| `MEMBER` | View own + family data, own rosters/attendance, own orders/invoices, submit member-only forms. |
|
||||
| `EDITOR` | Manage that club's `news`, `pages`, `formbuilder` content. |
|
||||
| `TREASURER` | Manage that club's `shop`: products, orders, payments, issue/void invoices. |
|
||||
| `BOARD` | Full management of that club: members, roles, all of the above. |
|
||||
> ⚠️ **This table is aspirational, not current.** The actually-implemented
|
||||
> `ClubRole.Roles` (`club/models.py`) is `ADMIN | MEMBER | EDITOR | MEMBER_ADMIN` — no
|
||||
> `TREASURER`/`BOARD` yet, and `MEMBER_ADMIN` (full read/write on people, short of
|
||||
> Finance/Club identity/role-granting — see `club.services.access.can_manage_members`)
|
||||
> isn't reflected below either. Treat this table as the target shape; verify the real
|
||||
> enum before writing code against it. `EVALUATOR` (§5.8) is **new, not yet added** to
|
||||
> either the aspirational list here or the real code enum.
|
||||
|
||||
| Role | Grants (representative) |
|
||||
|--------------|--------------------------------------------------------------------------------|
|
||||
| *Public* | Anonymous — no row; read-only public site of that club. |
|
||||
| `MEMBER` | View own + family data, own rosters/attendance, own orders/invoices, submit member-only forms. |
|
||||
| `EDITOR` | Manage that club's `news`, `pages`, `formbuilder` content. |
|
||||
| `TREASURER` | Manage that club's `shop`: products, orders, payments, issue/void invoices. |
|
||||
| `BOARD` | Full management of that club: members, roles, all of the above. |
|
||||
| `EVALUATOR` | Write + view **every** player evaluation club-wide (§5.8) — independent of `MEMBER_ADMIN`/coach `StaffAssignment`, since evaluating isn't the same trust boundary as either (a technical director might get this without full people-management access; a team's own coach doesn't get it just for coaching that team — see §5.8's own access note). Never granted to the evaluated player or their guardians. |
|
||||
|
||||
`news` is the one place a `ClubRole` and a derived role (`COACH_MANAGER`, see below)
|
||||
share a single workflow rather than each owning a separate permission: drafting is
|
||||
@@ -1111,6 +1127,67 @@ per-member entitlements would extend this — add an eligibility rule / code fie
|
||||
auto-apply service on top of the same model when that need is real, rather than a parallel
|
||||
mechanism.
|
||||
|
||||
### 5.8 `evaluations` — player evaluations *(new app, design only)*
|
||||
|
||||
Coaching-staff-only assessment of a player: a customizable rubric (skills, ratings, notes),
|
||||
recorded per team/season, feeding a player profile alongside their attendance history —
|
||||
**never visible to the evaluated player or their guardians**, regardless of how they're
|
||||
otherwise permitted (a parent with `EVALUATOR` sees every *other* child's evaluations but
|
||||
not their own kid's — see the access note below).
|
||||
|
||||
**Built on `formbuilder` (§5.6), not a parallel form engine.** `formbuilder`'s `Form`/
|
||||
`Field`/`Submission`/`Answer` already do everything a rubric needs (admin-defined fields,
|
||||
normalized answers, a submit service) — reuse them literally. The one thing `formbuilder`
|
||||
can't express is *who the submission is about*: `Submission.member` is the **submitter**
|
||||
(here, the evaluator), and a generic form has no notion of a separate subject. Rather than
|
||||
growing `formbuilder.Submission` an evaluation-specific field, `evaluations` owns a thin
|
||||
envelope that pairs a `Submission` with the player it was about:
|
||||
|
||||
```
|
||||
EvaluationSettings(club OneToOne) # which Form is *the* current rubric
|
||||
form FK formbuilder.Form (PROTECT)
|
||||
|
||||
PlayerEvaluation(UUIDModel) # the "this was about whom, on which roster" envelope
|
||||
player FK members.Member (CASCADE, related_name="evaluations_received")
|
||||
team FK teams.Team (CASCADE)
|
||||
season FK club.Season (PROTECT)
|
||||
submission OneToOne FK formbuilder.Submission (CASCADE) # evaluator + Answers live here
|
||||
Meta: ordering = ["-created"]
|
||||
```
|
||||
|
||||
- **Rubric changes don't corrupt history.** Swapping the active rubric re-points
|
||||
`EvaluationSettings.form` at a new `Form`; existing `PlayerEvaluation`s keep referencing
|
||||
their original `Form`/`Field`s (already immutable/`PROTECT`-ed once submissions exist —
|
||||
§5.6's own design notes), so an old evaluation still renders with the questions it was
|
||||
actually scored against.
|
||||
- **One rubric, club-wide** (not per team/age-group) — simplest, and keeps every age group
|
||||
on a comparable scale. Revisit only if a club actually needs per-team rubrics.
|
||||
- **Entry point**: an "Evaluate" action from the team roster (same "act from the page the
|
||||
data lives on" pattern as the referee-assignment panel, §5.3) renders the active rubric's
|
||||
`Form` for one player (`formbuilder.services.form_factory.build_form`) and, on submit,
|
||||
calls `formbuilder.services.submission.submit_form` then wraps the resulting `Submission`
|
||||
in a `PlayerEvaluation` — one transaction.
|
||||
- **Player profile** (a tab on the member detail page, gated the same as everything else
|
||||
here): evaluation history (each `PlayerEvaluation`'s answers + evaluator + date), games
|
||||
played **per team** ("U12: 14 games · U14: 6 games" — `Attendance` grouped by
|
||||
`event__teams`, no new tracking), and attendance rate (already-existing `Attendance`
|
||||
data, same definition `team_attendance_rate` uses, §5.2). No trend chart in v1.
|
||||
|
||||
**Access.** A new `ClubRole.EVALUATOR` (§3.2), club-wide and independent of `MEMBER_ADMIN`
|
||||
and coach `StaffAssignment` — evaluating is its own trust boundary, not "manages people" or
|
||||
"coaches this team." `club/services/access.py` gets `can_evaluate_players(user, club) =
|
||||
is_club_admin(user, club) or has_club_role(user, club, ClubRole.Roles.EVALUATOR)`. Gated at
|
||||
the view/mixin level (a `EvaluatorRequiredMixin`, mirroring `MemberAdminRequiredMixin`) —
|
||||
nothing evaluation-related is ever built into a mobile/member-facing context, not hidden
|
||||
behind a flag: the member app's views simply never query it.
|
||||
|
||||
**Build order**: (1) the `EVALUATOR` role + `can_evaluate_players`; (2) `EvaluationSettings`
|
||||
+ `PlayerEvaluation` models + migration; (3) the "Evaluate" entry point + submit flow; (4)
|
||||
the player-profile tab. `formbuilder`'s own model/service layer (§5.6) is **already built**
|
||||
— no prerequisite work needed there beyond, eventually, its own missing render/submit *view*
|
||||
if a public-facing form ever needs one (evaluations builds its own player-scoped one instead
|
||||
of waiting on that).
|
||||
|
||||
---
|
||||
|
||||
## 6. Entity-relationship overview
|
||||
@@ -1183,6 +1260,17 @@ Legend: `───<` one-to-many, `>───<` many-to-many via a through model
|
||||
presets on a `pending` order (each = a snapshotting `AppliedDiscount` row) before
|
||||
`finalize()`, rather than typing values. Presets stack against the same subtotal base;
|
||||
optional per-row value override for one-offs. Adds an `Order.pending → finalized` step.
|
||||
10. ✅ **Referee self-service sign-up** — **built**, superseding the "still open" note this
|
||||
replaces. A `RefereeSignup(event, member, status: invited|accepted|declined)` model
|
||||
tracks the invite/response; `sync_referee_invites` (wired from the same signal points
|
||||
as attendance sync, §5.3) auto-invites every eligible referee the moment a home game
|
||||
needs one. Accepting calls the existing `assign_referee(event, member, assigned_by=
|
||||
None)` — capacity-checked in the one place admin assignment already enforces it, so it
|
||||
lands as a real `EventReferee` row with no separate sync step (`assigned_by=None`
|
||||
marks it self-service, exactly the extension point this doc's earlier note predicted).
|
||||
Surfaced on the mobile Calendar and event detail page, scoped to every managed person
|
||||
(not just the account's own `self.me`) — a referee-eligible child is exactly as real
|
||||
as a referee-eligible parent.
|
||||
|
||||
Infrastructure/config for the above (media storage, dependencies + exact setup) is
|
||||
specified in **§8**.
|
||||
@@ -1205,12 +1293,13 @@ specified in **§8**.
|
||||
(checkout-date anchor, recommended, frozen total) or by *paying* before it (payment-date
|
||||
anchor, mutable total)? Doc implements checkout-date; confirm no club needs the literal
|
||||
"paid before date" semantics (§5.7.1).
|
||||
- **Referee self-service sign-up** — `EventReferee` (§5.3) is admin-assigned only for now
|
||||
(a team manager/coach can see the panel but not use it); a referee cannot yet subscribe
|
||||
themself to a game. Adding it later means making `assigned_by` nullable (null =
|
||||
self-subscribed) and a permission mixin scoping a referee to their own eligible games — no
|
||||
new model needed. Not built because this app has no self-service (member-facing) surface
|
||||
of any kind yet; the first one deserves its own pass rather than riding along here.
|
||||
- **Player evaluations** (§5.8) — designed, not built. Open question worth confirming
|
||||
before implementation: should `PlayerEvaluation` be strictly one-per-(player, team,
|
||||
season, evaluator) or allow several evaluators to each leave their own entry for the same
|
||||
player/season (the design as written allows the latter — no uniqueness constraint — since
|
||||
a rubric answered once per submission is `formbuilder`'s own natural shape, and multiple
|
||||
coaches' perspectives on the same player seems like a feature, not a bug, but it hasn't
|
||||
been explicitly confirmed).
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user