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>
45 lines
2.0 KiB
HTML
45 lines
2.0 KiB
HTML
{% extends "management/base.html" %}
|
|
{% load lucide ui i18n %}
|
|
|
|
{% block heading %}{% trans "Add family" %}{% endblock heading %}
|
|
|
|
{% block panel %}
|
|
<div class="card w-full bg-base-100 shadow">
|
|
<div class="card-body">
|
|
<form method="post">
|
|
{% csrf_token %}
|
|
|
|
{% for error in form.non_field_errors %}
|
|
<div class="alert alert-error my-2">
|
|
<span>{{ error }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
<h2 class="card-title text-base">{% lucide "user" size=18 %} {% trans "Parent / guardian" %}</h2>
|
|
<p class="text-sm opacity-70">{% trans "Gets a login -- they set a password via the reset link if this is a new email." %}</p>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{% 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>
|
|
|
|
<h2 class="card-title text-base">{% lucide "baby" size=18 %} {% trans "Child" %}</h2>
|
|
<p class="text-sm opacity-70">{% trans "No login of their own -- the parent above manages things for them." %}</p>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{% form_field form.child_first_name %}
|
|
{% form_field form.child_last_name %}
|
|
{% form_field form.child_date_of_birth %}
|
|
</div>
|
|
|
|
<div class="card-actions justify-start pt-2 mt-2">
|
|
<a class="btn btn-outline gap-2" href="{% url "management:member_list" %}">{% lucide "arrow-left" size=16 %} {% trans "Cancel" %}</a>
|
|
<button class="btn btn-primary gap-2" type="submit">{% lucide "save" size=16 %} {% trans "Save" %}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endblock panel %}
|