Email a set-password link on claim approval, flash identically either way

Two additions to the parent-claim flow: Club.contact_email (set from the
control panel, next to legal_name), and an email sent when an admin approves a
claim -- a real one-time set-password link built with allauth's own token
generator, so it lands in the same flow the login page's own reset would send
a parent to rather than a second, parallel one that could drift out of step
with it.

Never allowed to fail the approval: the family link and the guardian row are
real either way, and a mail server being briefly unreachable must not cost a
parent their place in the queue. The admin gets a distinct warning telling
them the email didn't go and to have the parent use "Forgot your password?"
instead.

The public submission flash keeps the enumeration guarantee the claim form
itself was built around: worded and timed identically whether or not a
matching child was found, sent before any lookup happens at all, mentioning
the club's contact email when the club has set one. A test compares the
rendered flash across a matching and a non-matching submission byte for byte.

One test-writing trap worth recording: assertRedirects follows the redirect
itself by default, and its own probe GET consumed the one-shot flash message
before a later explicit GET in the same test could see it --
fetch_redirect_response=False avoids the double-fetch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 18:33:41 +02:00
parent ca2b1a11b5
commit cb7f56709b
12 changed files with 186 additions and 35 deletions

View File

@@ -40,7 +40,7 @@ from formbuilder.models import Form as FormBuilderForm
from formbuilder.models import Submission
from members.forms import ClaimReviewForm
from members.models import Family, FamilyMembership, Group, GroupMembership, Member, ParentClaim
from members.services.claims import ClaimError, approve_claim, children_awaiting_a_parent, reject_claim, suggested_children
from members.services.claims import ClaimError, approve_claim, children_awaiting_a_parent, reject_claim, send_claim_approved_email, suggested_children
from members.services.family import add_child_to_family, add_parent_to_family, attach_to_family, detach_from_family, get_or_create_login_user, grant_login, register_family
from news.models import News, NewsPhoto
from shop.models import Discount, Invoice, Order, Product
@@ -1487,8 +1487,16 @@ class ParentClaimApproveView(ClubAdminRequiredMixin, View):
except ClaimError as error:
notify(request, f"e|{_('Could not approve')}|{error}")
else:
body = _("%(parent)s” is now linked to %(child)s and can set up their login.") % {"parent": claim.parent_name, "child": form.cleaned_data["child"]}
notify(request, f"s|{_('Claim approved')}|{body}")
child = form.cleaned_data["child"]
emailed = send_claim_approved_email(claim, child=child, request=request)
if emailed:
body = _("%(parent)s” is now linked to %(child)s. They've been emailed a link to set their password.") % {"parent": claim.parent_name, "child": child}
notify(request, f"s|{_('Claim approved')}|{body}")
else:
# The link is made either way -- say so plainly rather than letting
# the club assume the parent has been told.
body = _("%(parent)s” is now linked to %(child)s, but the email could not be sent. Ask them to use “Forgot your password?” on the sign-in page.") % {"parent": claim.parent_name, "child": child}
notify(request, f"w|{_('Claim approved, email not sent')}|{body}")
return redirect("management:parent_claim_list")