Add parent claims: onboarding a roster of children with no parents on file
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>
This commit is contained in:
@@ -1 +1,63 @@
|
||||
# Create your views here.
|
||||
"""Public and member-facing pages: claiming a child, and a parent's own family.
|
||||
|
||||
Everything here is outside the management app on purpose -- a parent is not club
|
||||
staff, and ClubStaffRequiredMixin would (rightly) turn them away.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import FormView, TemplateView
|
||||
|
||||
from members.forms import ParentClaimForm
|
||||
from members.models import FamilyMembership, Member
|
||||
from members.services.claims import submit_claim
|
||||
|
||||
|
||||
class ClubScopedPublicMixin:
|
||||
"""A club subdomain resolves this page; the base domain has no club to claim
|
||||
a child at, so it simply doesn't exist there."""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if getattr(request, "club", None) is None:
|
||||
raise Http404("This page belongs to a club.")
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class ParentClaimView(ClubScopedPublicMixin, FormView):
|
||||
"""Public. Submitting is also how a parent registers -- there is no open
|
||||
signup page, so this form is the only way into an account for someone the
|
||||
club hasn't already added.
|
||||
|
||||
It always reports the same thing back, whether or not the child was found:
|
||||
the response must not tell an anonymous submitter which children the club
|
||||
has. What actually happens next is decided by an admin.
|
||||
"""
|
||||
|
||||
template_name = "members/parent_claim.html"
|
||||
form_class = ParentClaimForm
|
||||
|
||||
def form_valid(self, form):
|
||||
submit_claim(self.request.club, **form.cleaned_data)
|
||||
return render(self.request, "members/parent_claim_submitted.html", {"club": self.request.club})
|
||||
|
||||
|
||||
class MyFamilyView(ClubScopedPublicMixin, LoginRequiredMixin, TemplateView):
|
||||
"""What a linked parent sees: the children they're responsible for, and
|
||||
nothing else. The seam a real parent portal would grow from."""
|
||||
|
||||
template_name = "members/my_family.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
me = Member.objects.filter(user=self.request.user).first()
|
||||
children = Member.objects.none()
|
||||
if me is not None:
|
||||
children = Member.objects.filter(
|
||||
family_memberships__role=FamilyMembership.FamilyRole.CHILD,
|
||||
family_memberships__family__memberships__member=me,
|
||||
family_memberships__family__memberships__role__in=[FamilyMembership.FamilyRole.PARENT, FamilyMembership.FamilyRole.GUARDIAN],
|
||||
member_of__club=self.request.club,
|
||||
).distinct()
|
||||
|
||||
return super().get_context_data(club=self.request.club, me=me, children=children, **kwargs)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user