The migration path for a club arriving with a list of children from a federation export and no parent records. Children import without logins, each into a family of their own -- that shape *is* the "nobody is responsible for this child" state, so there's no unclaimed flag to drift out of step with reality, and a family drops off the worklist by itself the moment a parent joins it. `family_role=child` with a blank `family_group` asks for that; any other lone role is still a mistake in the file. Verification is a human decision, deliberately. A parent submits a public form with the child's name and date of birth as free text -- no search, no autocomplete, and the same response whether or not the child was found, because the page needs no login and anything that resolved the child would turn it into a way to enumerate the club's children. An admin matches it from a queue against a shortlist that only ever contains children with nobody on file, so approving can never quietly re-parent a child who already has one. The alternatives were worse. A claim code needs a delivery channel the club may not have and is a bearer token besides. Matching on name plus birthday hands out someone else's child to whoever guesses a birthday. The club is the only party that actually knows its own families. That form is also the registration: open self-registration is now closed (shadowing account_signup rather than removing the route, so the URL name allauth's templates reverse still resolves). The account is created on approval, not on submission, so a public form can't fill the user table. An approved parent lands as a guardian -- login and family link, no membership, no fee -- gets a password-reset link, and a minimal "my family" page. One bug worth recording: families_awaiting_a_parent first used annotate(Count(..., filter=...)) over a queryset already filtered on the same join, so Django reused that join for the counts and a parent with no ClubMembership of their own -- exactly what a newly linked guardian is -- went uncounted, leaving the family unclaimed forever. Exists subqueries avoid it. A test pins both directions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
38 lines
1.9 KiB
Python
38 lines
1.9 KiB
Python
from django import forms
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
|
|
class ParentClaimForm(forms.Form):
|
|
"""The public "link me to my child" form -- see members.models.ParentClaim.
|
|
|
|
Every child field is free text. There is deliberately no picker, no search
|
|
and no autocomplete: this page is reachable without logging in, so anything
|
|
that confirmed whether a given child exists would turn it into a way to
|
|
enumerate the club's children. The submitter types what they know and an
|
|
admin matches it against the real record.
|
|
"""
|
|
|
|
parent_first_name = forms.CharField(label=_("Your first name"), max_length=150)
|
|
parent_last_name = forms.CharField(label=_("Your last name"), max_length=150)
|
|
parent_email = forms.EmailField(label=_("Your email address"), help_text=_("We'll use this to set up your login once the club has confirmed the link."))
|
|
|
|
child_first_name = forms.CharField(label=_("Child's first name"), max_length=150)
|
|
child_last_name = forms.CharField(label=_("Child's last name"), max_length=150)
|
|
child_date_of_birth = forms.DateField(label=_("Child's date of birth"), widget=forms.DateInput(attrs={"type": "date"}))
|
|
|
|
|
|
class ClaimReviewForm(forms.Form):
|
|
"""An admin approving one claim: which child it actually refers to.
|
|
|
|
The child is chosen from the shortlist rather than typed, and the shortlist
|
|
is only ever children who have no parent on file -- approving a claim can
|
|
never quietly re-parent a child who already has one.
|
|
"""
|
|
|
|
child = forms.ModelChoiceField(queryset=None, label=_("Link to"), widget=forms.Select(attrs={"class": "select select-bordered w-full"}))
|
|
|
|
def __init__(self, *args, candidates=None, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.fields["child"].queryset = candidates
|
|
self.fields["child"].label_from_instance = lambda child: f"{child} ({child.date_of_birth:%d %b %Y})" if child.date_of_birth else str(child)
|