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:
41
members/migrations/0005_parent_claim.py
Normal file
41
members/migrations/0005_parent_claim.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Generated by Django 6.0.6 on 2026-08-11 14:22
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('club', '0023_backfill_guardian_memberships'),
|
||||
('members', '0004_group_groupmembership_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ParentClaim',
|
||||
fields=[
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('parent_first_name', models.CharField(max_length=150, verbose_name='parent first name')),
|
||||
('parent_last_name', models.CharField(max_length=150, verbose_name='parent last name')),
|
||||
('parent_email', models.EmailField(max_length=254, verbose_name='parent email')),
|
||||
('child_first_name', models.CharField(max_length=150, verbose_name='child first name')),
|
||||
('child_last_name', models.CharField(max_length=150, verbose_name='child last name')),
|
||||
('child_date_of_birth', models.DateField(verbose_name='child date of birth')),
|
||||
('status', models.CharField(choices=[('pending', 'pending'), ('approved', 'approved'), ('rejected', 'rejected')], default='pending', max_length=20, verbose_name='status')),
|
||||
('reviewed_at', models.DateTimeField(blank=True, null=True, verbose_name='reviewed at')),
|
||||
('note', models.TextField(blank=True, help_text='Why it was rejected, or anything worth recording about the decision.', verbose_name='note')),
|
||||
('child', models.ForeignKey(blank=True, help_text='Set when an admin approves the claim -- the child it was matched to.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='parent_claims', to='members.member', verbose_name='matched child')),
|
||||
('club', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(class)ss', to='club.club')),
|
||||
('reviewed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='members.member', verbose_name='reviewed by')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'parent claim',
|
||||
'verbose_name_plural': 'parent claims',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user