Files
RosterChief/members/views.py
Bernard Siebens c4f0ec71c1 Season-scope claim history, self-service second-child claims, redesign approval screen
Signed-in parents get their details locked and pre-filled on the claim
form instead of retyped; approving links to their existing user and
merges into their existing family instead of creating a duplicate.
The approval screen is now a card grid with a searchable, pre-selected
child dropdown and a reason modal for rejection. The "already dealt
with" history is scoped to the current season.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 23:52:50 +02:00

105 lines
4.6 KiB
Python

"""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 redirect
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView, TemplateView
from controlpanel.messages import notify
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.
A signed-in parent claiming a second child (a sibling, most often) gets
their "about you" fields locked to read-only text instead of blank inputs
-- typing their name/email again risks a typo forking off a second
User/Member instead of reusing theirs, and approval also uses this to
merge the new child into the family they and their first child already
belong to (see members.services.claims.approve_claim).
"""
template_name = "members/parent_claim.html"
form_class = ParentClaimForm
def get_member(self):
if not self.request.user.is_authenticated:
return None
return Member.objects.filter(user=self.request.user).first()
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["lock_parent_fields"] = self.get_member() is not None
return kwargs
def get_context_data(self, **kwargs):
return super().get_context_data(locked_member=self.get_member(), **kwargs)
def form_valid(self, form):
data = dict(form.cleaned_data)
member = self.get_member()
if member is not None:
# Never trust the client for this -- the fields aren't even in the
# submitted form when locked. Pulled straight from the account
# that's actually signed in.
data["parent_first_name"] = member.first_name
data["parent_last_name"] = member.last_name
data["parent_email"] = member.contact_email
submit_claim(self.request.club, submitted_by_user=self.request.user if self.request.user.is_authenticated else None, **data)
club = self.request.club
title = _("Request received")
body = _("%(club)s will check this against their records. If it matches, you'll get an email with a link to set your password.") % {"club": club.name}
if club.contact_email:
body += " " + _("Any questions, write to %(email)s.") % {"email": club.contact_email}
# Worded identically whether or not that child exists, and sent before any
# lookup happens at all -- a message that differed would tell an anonymous
# submitter which children the club has.
notify(self.request, f"s|{title}|{body}")
return redirect("members:parent_claim")
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)