Build M6 Edit personal info for the mobile app
The last of the seven Member-mode screens. A real editable form for the
fields Member actually has (first/last name, date of birth, email,
phone, emergency phone) -- the design mock's national-register-number,
address, allergies/notes and consent-toggle rows have no backing field
and are omitted rather than added as new schema for a screen-building
pass. Two mock rows do have real data and are shown read-only instead:
every guardian via Member.guardians ("Emergency contact"), and any open
onboarding requirement for the person's current-season membership (via
club.services.onboarding.checklist_for) as a banner -- no upload/complete
action, that stays staff-only elsewhere in the platform.
Authorization: the target Member (from the URL) must be one of the
signed-in account's managed_people, checked on both GET and POST -- an
unmanaged id 404s, same as another club's Event/News already does
elsewhere in this app.
This completes M1-M7 (Home, Event detail, Calendar, News article, Me &
my people, Edit personal info, Notifications). Coach mode (C1-C6) is a
separate, later phase per the design doc and is intentionally not
started here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
131
mobile/tests.py
131
mobile/tests.py
@@ -6,7 +6,7 @@ from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone, translation
|
||||
|
||||
from club.models import Club, ClubMembership, DuesInvoice, Season
|
||||
from club.models import Club, ClubMembership, DuesInvoice, MemberRequirementStatus, OnboardingRequirement, Season
|
||||
from events.models import Attendance, Event
|
||||
from members.models import Family, FamilyMembership, Member
|
||||
from news.models import News
|
||||
@@ -850,3 +850,132 @@ class MeViewTests(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "No one to show yet")
|
||||
self.assertNotContains(response, "Coach mode")
|
||||
|
||||
|
||||
@override_settings(ROSTERCHIEF_BASE_DOMAIN="rosterchief.app", ALLOWED_HOSTS=["rosterchief.app", "ajax-united.rosterchief.app", "testserver"])
|
||||
class EditProfileViewTests(TestCase):
|
||||
"""M6 -- design_handoff_rosterchief_platform/README.md's M6 section,
|
||||
"Edit personal info". See EditProfileView's own docstring for the
|
||||
judgment calls: no schema fields for national register no./address/
|
||||
allergies/consent (all omitted), guardians and open onboarding
|
||||
requirements are shown read-only, and an unmanaged member 404s rather
|
||||
than mirroring EventDetailView.post's 400."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.club = make_club()
|
||||
today = timezone.localdate()
|
||||
cls.season = Season.objects.create(club=cls.club, start_date=today - datetime.timedelta(days=30), end_date=today + datetime.timedelta(days=300))
|
||||
cls.user = User.objects.create_user(email="parent@example.com", password="pw-secret-123")
|
||||
cls.member = Member.objects.create(first_name="Lars", last_name="Bakker", email="lars@example.com", user=cls.user)
|
||||
ClubMembership.objects.create(club=cls.club, member=cls.member, season=cls.season)
|
||||
|
||||
cls.family = Family.objects.create(name="Bakker")
|
||||
FamilyMembership.objects.create(family=cls.family, member=cls.member, role=FamilyMembership.FamilyRole.PARENT)
|
||||
cls.child = Member.objects.create(first_name="Noor", last_name="Bakker")
|
||||
FamilyMembership.objects.create(family=cls.family, member=cls.child, role=FamilyMembership.FamilyRole.CHILD)
|
||||
cls.child_membership = ClubMembership.objects.create(club=cls.club, member=cls.child, season=cls.season)
|
||||
|
||||
def _get(self, member):
|
||||
return self.client.get(reverse("mobile:edit_profile", kwargs={"member_id": member.pk}), HTTP_HOST="ajax-united.rosterchief.app")
|
||||
|
||||
def _post(self, member, data):
|
||||
return self.client.post(reverse("mobile:edit_profile", kwargs={"member_id": member.pk}), data=data, HTTP_HOST="ajax-united.rosterchief.app")
|
||||
|
||||
def test_requires_login(self):
|
||||
response = self._get(self.member)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_get_renders_the_form_prefilled_with_the_persons_current_data(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get(self.member)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Lars")
|
||||
self.assertContains(response, "Bakker")
|
||||
self.assertContains(response, "lars@example.com")
|
||||
|
||||
def test_post_with_valid_data_updates_the_member_and_redirects_to_me(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._post(
|
||||
self.member,
|
||||
{"first_name": "Larsen", "last_name": "Bakker", "email": "larsen@example.com", "phone": "", "emergency_phone": ""},
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse("mobile:me"), fetch_redirect_response=False)
|
||||
self.member.refresh_from_db()
|
||||
self.assertEqual(self.member.first_name, "Larsen")
|
||||
self.assertEqual(self.member.email, "larsen@example.com")
|
||||
|
||||
def test_post_with_invalid_data_rerenders_with_errors_and_does_not_save(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._post(
|
||||
self.member,
|
||||
{"first_name": "", "last_name": "Bakker", "email": "lars@example.com", "phone": "", "emergency_phone": ""},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.context["form"].errors)
|
||||
self.member.refresh_from_db()
|
||||
self.assertEqual(self.member.first_name, "Lars")
|
||||
|
||||
def test_cannot_edit_a_member_who_isnt_managed(self):
|
||||
stranger = Member.objects.create(first_name="Someone", last_name="Else")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
get_response = self._get(stranger)
|
||||
post_response = self._post(stranger, {"first_name": "Hacked", "last_name": "Else", "email": "", "phone": "", "emergency_phone": ""})
|
||||
|
||||
self.assertEqual(get_response.status_code, 404)
|
||||
self.assertEqual(post_response.status_code, 404)
|
||||
stranger.refresh_from_db()
|
||||
self.assertEqual(stranger.first_name, "Someone")
|
||||
|
||||
def test_a_managed_child_can_be_edited_too(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._post(
|
||||
self.child,
|
||||
{"first_name": "Noor", "last_name": "Bakker", "email": "noor@example.com", "phone": "", "emergency_phone": ""},
|
||||
)
|
||||
|
||||
self.assertRedirects(response, reverse("mobile:me"), fetch_redirect_response=False)
|
||||
self.child.refresh_from_db()
|
||||
self.assertEqual(self.child.email, "noor@example.com")
|
||||
|
||||
def test_guardian_shows_as_a_readonly_emergency_contact_line(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get(self.child)
|
||||
|
||||
self.assertContains(response, "Lars Bakker")
|
||||
self.assertContains(response, "parent")
|
||||
|
||||
def test_no_emergency_contact_line_when_the_person_has_no_guardians(self):
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get(self.member)
|
||||
|
||||
self.assertNotContains(response, "Emergency contact")
|
||||
|
||||
def test_open_onboarding_requirement_shows_as_a_banner(self):
|
||||
OnboardingRequirement.objects.create(club=self.club, name="Medical form")
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get(self.child)
|
||||
|
||||
self.assertContains(response, "Medical form")
|
||||
self.assertContains(response, "still open")
|
||||
|
||||
def test_resolved_requirement_does_not_show_in_the_banner(self):
|
||||
requirement = OnboardingRequirement.objects.create(club=self.club, name="Medical form")
|
||||
MemberRequirementStatus.objects.create(membership=self.child_membership, requirement=requirement, is_complete=True)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self._get(self.child)
|
||||
|
||||
self.assertNotContains(response, "still open")
|
||||
|
||||
Reference in New Issue
Block a user