Show blocked sign-ups on mobile instead of silently hiding the event

A member excluded from an event's Attendance sync by an open onboarding
requirement (events.services.attendance.effective_members) previously just
never saw that event anywhere -- no row, no explanation. Two new read-side
functions mirror that exclusion instead of hiding it: club.services.
onboarding.open_requirements_blocking (per-member, "why") and events.
services.attendance.blocked_upcoming_events_for_member (which of their
upcoming events are affected).

The Calendar now shows those events as a distinct muted "Blocked" row
naming the outstanding requirement, and the event detail page shows a
"Can't sign up yet" card for the same reason -- no RSVP buttons, no lineup/
referee actions, just the explanation. Write-side blocking (who actually
gets an Attendance row, who a coach can select) is untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-22 19:24:11 +02:00
parent 86ea8d1b15
commit a7fdab4352
11 changed files with 324 additions and 15 deletions

View File

@@ -1007,6 +1007,39 @@ class CalendarViewTests(TestCase):
self.assertIn(event, self._events_in_context(response))
def test_a_blocked_event_shows_up_with_an_explanation_instead_of_disappearing(self):
team = Team.objects.create(club=self.club, name="U16", short_name="U16")
position = Position.objects.create(club=self.club, name="Forward", short_name="F")
TeamMembership.objects.create(team=team, member=self.member, season=self.season, position=position)
OnboardingRequirement.objects.create(club=self.club, name="Medical certificate", blocked_event_kinds=["game"])
event = self.make_event(title="Cup game", kind=Event.EventKind.GAME)
event.teams.add(team)
self.client.force_login(self.user)
response = self._get()
self.assertContains(response, "Cup game")
self.assertContains(response, "Blocked")
self.assertContains(response, "Medical certificate")
self.assertFalse(Attendance.objects.filter(event=event, member=self.member).exists())
def test_a_resolved_requirement_shows_the_event_normally_not_blocked(self):
team = Team.objects.create(club=self.club, name="U16", short_name="U16")
position = Position.objects.create(club=self.club, name="Forward", short_name="F")
TeamMembership.objects.create(team=team, member=self.member, season=self.season, position=position)
requirement = OnboardingRequirement.objects.create(club=self.club, name="Medical certificate", blocked_event_kinds=["game"])
club_membership = ClubMembership.objects.get(club=self.club, member=self.member, season=self.season)
MemberRequirementStatus.objects.create(membership=club_membership, requirement=requirement, is_complete=True)
event = self.make_event(title="Cup game", kind=Event.EventKind.GAME)
event.teams.add(team)
self.client.force_login(self.user)
response = self._get()
self.assertContains(response, "Cup game")
self.assertNotContains(response, "Blocked")
self.assertTrue(Attendance.objects.filter(event=event, member=self.member).exists())
@override_settings(ROSTERCHIEF_BASE_DOMAIN="rosterchief.app", ALLOWED_HOSTS=["rosterchief.app", "ajax-united.rosterchief.app", "testserver"])
class CalendarRefereeSignupTests(TestCase):
@@ -1338,6 +1371,32 @@ class EventDetailScreenTests(TestCase):
self.assertNotContains(response, 'name="status" value="dropout"')
def test_blocked_signup_shows_why_instead_of_disappearing(self):
OnboardingRequirement.objects.create(club=self.club, name="Medical certificate", blocked_event_kinds=["game"])
game = Event.objects.create(club=self.club, title="Cup game", kind=Event.EventKind.GAME, start=timezone.now() + datetime.timedelta(days=7))
game.teams.add(self.team)
self.client.force_login(self.user)
response = self._get(game)
self.assertEqual(len(response.context["blocked_signups"]), 1)
self.assertContains(response, "Can't sign up yet")
self.assertContains(response, "Medical certificate")
self.assertFalse(Attendance.objects.filter(event=game, member=self.member).exists())
def test_no_blocked_card_once_the_requirement_is_resolved(self):
requirement = OnboardingRequirement.objects.create(club=self.club, name="Medical certificate", blocked_event_kinds=["game"])
club_membership = ClubMembership.objects.get(club=self.club, member=self.member, season=self.season)
MemberRequirementStatus.objects.create(membership=club_membership, requirement=requirement, is_complete=True)
game = Event.objects.create(club=self.club, title="Cup game", kind=Event.EventKind.GAME, start=timezone.now() + datetime.timedelta(days=7))
game.teams.add(self.team)
self.client.force_login(self.user)
response = self._get(game)
self.assertEqual(response.context["blocked_signups"], [])
self.assertNotContains(response, "Can't sign up yet")
def test_no_referee_card_when_not_invited(self):
self.client.force_login(self.user)