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:
@@ -172,6 +172,28 @@ def blocked_member_ids_for_event(club, season, event_kind) -> set:
|
||||
return blocked_member_ids
|
||||
|
||||
|
||||
def open_requirements_blocking(member, club, season, event_kind) -> list:
|
||||
"""The specific, still-open `OnboardingRequirement`s blocking `member` from
|
||||
`event_kind` events this season -- the per-member, explain-*why* mirror of
|
||||
`blocked_member_ids_for_event`'s bulk set. Built for the member-facing "you
|
||||
can't sign up for this yet" card (mobile app): unlike that function, this
|
||||
one is meant to be called once for a person looking at one event, not once
|
||||
per event for a whole club, so the per-member cost here is fine.
|
||||
|
||||
Empty (never blocked) for a member with no current-season ClubMembership.MEMBER
|
||||
row -- same "nothing to check" reasoning as blocked_member_ids_for_event's own."""
|
||||
membership = ClubMembership.objects.filter(club=club, season=season, member=member, kind=ClubMembership.Kind.MEMBER).first()
|
||||
if membership is None:
|
||||
return []
|
||||
|
||||
blocking = [requirement for requirement in OnboardingRequirement.objects.filter(club=club, is_active=True) if event_kind in requirement.blocked_event_kinds]
|
||||
if not blocking:
|
||||
return []
|
||||
|
||||
resolved_ids = set(MemberRequirementStatus.objects.filter(membership=membership, requirement__in=blocking).filter(_RESOLVED).values_list("requirement_id", flat=True))
|
||||
return [requirement for requirement in blocking if requirement.pk not in resolved_ids]
|
||||
|
||||
|
||||
#: Fee states "clean" enough to activate on -- PARTIALLY_PAID/UNPAID never are.
|
||||
_CLEAN_FEE_STATUSES = (ClubMembership.FeeStatus.PAID, ClubMembership.FeeStatus.WAIVED)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user