Separate guardians from members: a parent is not automatically a member
members/services/family.py enrolled a parent exactly like the child they were registering, so every parent held a full membership: counted in the member list, in the club and platform KPIs, and in the fee roll, with a fee record of their own. ClubMembership.kind (member | guardian) separates the two. A guardian is attached to the club only through their child. They hold the login, can be contacted and can sit in a Group -- the stated exception -- but they are not a member: no fee (clean() refuses one), absent from the member list, the fee list and every member count, and not eligible for a roster or a staff spot. A parent who also plays or coaches is a member who happens to be a parent; the two facts are independent, which is why this is its own field rather than inferred from FamilyMembership.role. A field on ClubMembership rather than a separate model because everything that answers "is this person attached to this club" already reads through that table -- tenancy, groups, the club-wide event audience -- and a second kind of link would need a parallel path through all of it. What changes is only who counts. Two things that weren't obvious going in: Excluding guardians had to be a subtraction, not a narrower filter. The obvious move -- match only member-kind rows and drop the MEMBER-role branch, since an active membership of any kind grants that role -- also hides someone the club knows but hasn't signed up for a season yet, which is a real state the member edit page supports. Two existing tests caught it. _guardians_only() subtracts instead, so anyone who also plays, is on staff or runs the club stays visible. Their tie to the club isn't seasonal but rides on a per-season row, so it has to be carried forward or a parent silently drops off at the season boundary while their child stays enrolled. Copied from the immediately preceding season only, so a deliberate removal isn't resurrected from an older row. The data migration reclassifies existing parents, deliberately skipping anyone who plays, is on a team's staff or holds an elevated ClubRole -- demoting them would strip them from their own team's roster eligibility. Anything ambiguous stays a member, which an admin can flip; noticing someone quietly vanished is much harder. The import template gains a membership_kind column next to family_role (a child marked guardian is refused), and the review screen shows what each row will join as. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
{% form_field form.parent_first_name %}
|
||||
{% form_field form.parent_last_name %}
|
||||
{% form_field form.parent_email %}
|
||||
{% form_field form.parent_is_member %}
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
@@ -79,7 +79,23 @@
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">{% lucide "id-card" size=18 %} {% trans "Club membership" %}</h2>
|
||||
{% if current_membership %}
|
||||
{% if current_membership.is_guardian %}
|
||||
{% comment %}
|
||||
Stated plainly rather than left to be inferred from an empty
|
||||
fee: a guardian is deliberately missing from the member list
|
||||
and every member count, and someone looking at this page needs
|
||||
to know that is on purpose. See club.models.ClubMembership.Kind.
|
||||
{% endcomment %}
|
||||
<div class="alert alert-warning py-2 my-1">
|
||||
{% lucide "users" size=16 %}
|
||||
<span class="text-sm">{% trans "Guardian: attached to the club as a parent of a member. Holds the login, owes no fee, and is not counted as a member." %}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<dl class="divide-y divide-base-200">
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<dt class="text-sm opacity-70">{% trans "Joined as" %}</dt>
|
||||
<dd class="font-semibold">{{ current_membership.get_kind_display|capfirst }}</dd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<dt class="text-sm opacity-70">{% trans "License" %}</dt>
|
||||
<dd class="font-semibold">{{ current_membership.license|default:"-" }}</dd>
|
||||
@@ -88,10 +104,12 @@
|
||||
<dt class="text-sm opacity-70">{% trans "Status" %}</dt>
|
||||
<dd class="font-semibold">{{ current_membership.get_status_display }}</dd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<dt class="text-sm opacity-70">{% trans "Fee status" %}</dt>
|
||||
<dd class="font-semibold">{{ current_membership.get_fee_status_display }}</dd>
|
||||
</div>
|
||||
{% if not current_membership.is_guardian %}
|
||||
<div class="flex items-center justify-between py-2">
|
||||
<dt class="text-sm opacity-70">{% trans "Fee status" %}</dt>
|
||||
<dd class="font-semibold">{{ current_membership.get_fee_status_display }}</dd>
|
||||
</div>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% else %}
|
||||
<p class="text-sm opacity-60">{% trans "Not rostered for the current season." %}</p>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<th>{% trans "First name" %}</th>
|
||||
<th>{% trans "Email" %}</th>
|
||||
<th>{% trans "Family" %}</th>
|
||||
<th>{% trans "Joining as" %}</th>
|
||||
<th>{% trans "Outcome" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -45,6 +46,18 @@
|
||||
<span class="opacity-40">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% comment %}
|
||||
A guardian holds the login for a child but is not a member:
|
||||
no fee, and not counted in any member total. See
|
||||
club.models.ClubMembership.Kind.
|
||||
{% endcomment %}
|
||||
{% if result.membership_kwargs.kind == "guardian" %}
|
||||
<span class="badge badge-warning badge-sm">{% trans "Guardian" %}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-neutral badge-sm">{% trans "Member" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if result.member %}
|
||||
<span class="badge badge-success badge-sm">{% trans "Will create" %}</span>
|
||||
@@ -56,7 +69,7 @@
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center opacity-60">{% trans "No rows found in the uploaded file." %}</td>
|
||||
<td colspan="7" class="text-center opacity-60">{% trans "No rows found in the uploaded file." %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user