Show a child's guardians' phone numbers on their Personal information card

Member.guardians already scoped correctly to "the parents/guardians of
the families where this member is the CHILD" -- no model changes
needed. Adds clearly-labelled phone/emergency phone rows per guardian
plus a dial button each, following the same tel: link pattern already
used for the member's own numbers. Empty for members who aren't a
child in any family.
This commit is contained in:
2026-08-07 22:12:42 +02:00
parent 783b235bcd
commit 3d03ff644c
3 changed files with 64 additions and 0 deletions

View File

@@ -28,6 +28,16 @@
<dt class="text-sm opacity-70">{% trans "Emergency phone" %}</dt> <dt class="text-sm opacity-70">{% trans "Emergency phone" %}</dt>
<dd class="font-semibold">{{ member.emergency_phone.as_international|default:"-" }}</dd> <dd class="font-semibold">{{ member.emergency_phone.as_international|default:"-" }}</dd>
</div> </div>
{% for guardian in guardians %}
<div class="flex items-center justify-between py-2">
<dt class="text-sm opacity-70">{% trans "Parent phone" %} — {{ guardian.get_full_name }}</dt>
<dd class="font-semibold">{{ guardian.phone.as_international|default:"-" }}</dd>
</div>
<div class="flex items-center justify-between py-2">
<dt class="text-sm opacity-70">{% trans "Parent emergency phone" %} — {{ guardian.get_full_name }}</dt>
<dd class="font-semibold">{{ guardian.emergency_phone.as_international|default:"-" }}</dd>
</div>
{% endfor %}
</dl> </dl>
<div class="flex flex-row gap-2 items-center mt-2"> <div class="flex flex-row gap-2 items-center mt-2">
@@ -38,6 +48,25 @@
<a class="btn btn-sm btn-outline btn-error grow" href="tel:{{ member.emergency_phone.as_international }}">{% lucide "shield-alert" size=14 %} {% trans "Emergency call" %}</a> <a class="btn btn-sm btn-outline btn-error grow" href="tel:{{ member.emergency_phone.as_international }}">{% lucide "shield-alert" size=14 %} {% trans "Emergency call" %}</a>
{% endif %} {% endif %}
</div> </div>
{% if guardians %}
<div class="divider my-1"></div>
<div class="text-xs font-semibold uppercase opacity-60 flex items-center gap-1 mb-1">
{% lucide "users" size=12 %} {% trans "Parent/guardian contact" %}
</div>
{% for guardian in guardians %}
{% if guardian.phone or guardian.emergency_phone %}
<div class="flex flex-row gap-2 items-center mt-2">
{% if guardian.phone %}
<a class="btn btn-sm btn-outline btn-primary grow" href="tel:{{ guardian.phone.as_international }}">{% lucide "phone" size=14 %} {% blocktrans with name=guardian.get_full_name %}Call {{ name }}{% endblocktrans %}</a>
{% endif %}
{% if guardian.emergency_phone %}
<a class="btn btn-sm btn-outline btn-error grow" href="tel:{{ guardian.emergency_phone.as_international }}">{% lucide "shield-alert" size=14 %} {% blocktrans with name=guardian.get_full_name %}Emergency: {{ name }}{% endblocktrans %}</a>
{% endif %}
</div>
{% endif %}
{% endfor %}
{% endif %}
</div> </div>
</div> </div>

View File

@@ -1087,6 +1087,37 @@ class MemberFamilyAttachDetachTests(ManagementTestBase):
self.assertNotIn(family, response.context["attach_to_family_form"].fields["family"].queryset) self.assertNotIn(family, response.context["attach_to_family_form"].fields["family"].queryset)
def test_a_childs_page_shows_their_guardians_phone_numbers(self):
family = Family.objects.create()
parent = Member.objects.create(first_name="Pat", last_name="Parent", phone="+32470000001", emergency_phone="+32470000002")
FamilyMembership.objects.create(family=family, member=self.standalone, role=FamilyMembership.FamilyRole.CHILD)
FamilyMembership.objects.create(family=family, member=parent, role=FamilyMembership.FamilyRole.PARENT)
response = self.club_get("member_detail", self.standalone.pk)
self.assertIn(parent, response.context["guardians"])
self.assertContains(response, "Pat Parent")
self.assertContains(response, "tel:+32 470 00 00 01")
self.assertContains(response, "tel:+32 470 00 00 02")
def test_a_parents_page_does_not_show_guardian_numbers(self):
# self.standalone has no CHILD membership anywhere -- the section must not appear.
response = self.club_get("member_detail", self.standalone.pk)
self.assertEqual(len(response.context["guardians"]), 0)
self.assertNotContains(response, "Parent/guardian contact")
def test_a_guardian_with_no_phone_numbers_gets_no_dial_buttons(self):
family = Family.objects.create()
parent = Member.objects.create(first_name="Pat", last_name="Parent")
FamilyMembership.objects.create(family=family, member=self.standalone, role=FamilyMembership.FamilyRole.CHILD)
FamilyMembership.objects.create(family=family, member=parent, role=FamilyMembership.FamilyRole.PARENT)
response = self.club_get("member_detail", self.standalone.pk)
self.assertContains(response, "Pat Parent")
self.assertNotContains(response, "tel:")
class MemberDeleteTests(ManagementTestBase): class MemberDeleteTests(ManagementTestBase):
def setUp(self): def setUp(self):

View File

@@ -590,6 +590,10 @@ class MemberDetailView(ClubStaffRequiredMixin, DetailView):
attach_to_family_form=AttachToFamilyForm(club=self.request.club, member=self.object), attach_to_family_form=AttachToFamilyForm(club=self.request.club, member=self.object),
current_membership=ClubMembership.objects.filter(club=self.request.club, member=self.object, season=current_season(self.request.club)).first(), current_membership=ClubMembership.objects.filter(club=self.request.club, member=self.object, season=current_season(self.request.club)).first(),
membership_history=ClubMembership.objects.filter(club=self.request.club, member=self.object).select_related("season").order_by("-season__start_date"), membership_history=ClubMembership.objects.filter(club=self.request.club, member=self.object).select_related("season").order_by("-season__start_date"),
# Non-empty only when `member` is a CHILD in some family -- that's the same
# signal the Personal information card uses to decide whether to show parent
# contact numbers at all.
guardians=self.object.guardians,
**kwargs, **kwargs,
) )