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

@@ -51,6 +51,7 @@ from .services.onboarding import (
mark_bypassed,
mark_complete,
mark_incomplete,
open_requirements_blocking,
)
from .services.seasons import _initial_season_start, _season_end, generate_seasons, resync_seasons
from .tenancy import (
@@ -2268,6 +2269,34 @@ class OnboardingRequirementTests(TestCase):
self.assertEqual(blocked_member_ids_for_event(self.club, self.season, "game"), set())
# --- open_requirements_blocking (per-member "why can't I sign up" mirror) ---
def test_open_requirements_blocking_is_empty_when_nothing_is_configured_to_block(self):
self.assertEqual(open_requirements_blocking(self.member, self.club, self.season, "game"), [])
def test_open_requirements_blocking_returns_the_open_requirement(self):
self.medical.blocked_event_kinds = ["game"]
self.medical.save()
self.assertEqual(open_requirements_blocking(self.member, self.club, self.season, "game"), [self.medical])
def test_open_requirements_blocking_is_kind_specific(self):
self.medical.blocked_event_kinds = ["game"]
self.medical.save()
self.assertEqual(open_requirements_blocking(self.member, self.club, self.season, "training"), [])
def test_open_requirements_blocking_excludes_a_resolved_requirement(self):
self.medical.blocked_event_kinds = ["game"]
self.medical.save()
mark_complete(self.membership, self.medical, user=self.staff)
self.assertEqual(open_requirements_blocking(self.member, self.club, self.season, "game"), [])
def test_open_requirements_blocking_is_empty_with_no_club_membership(self):
stranger = Member.objects.create(first_name="No", last_name="Membership")
self.assertEqual(open_requirements_blocking(stranger, self.club, self.season, "game"), [])
# --- approve_all_clean ---
def test_approve_all_clean_activates_a_pending_paid_up_fully_checked_member(self):
pending = ClubMembership.objects.create(club=self.club, member=Member.objects.create(first_name="Tom", last_name="Roe"), season=self.season, status=ClubMembership.StatusChoices.PENDING, fee_status=ClubMembership.FeeStatus.PAID)