Let referee levels inherit from a lower level instead of relying on ordering alone

RefereeLevel.inherits_from chains levels together so a higher tier is
automatically eligible for everything a linked lower tier covers,
transitively, without hand-duplicating teams onto every level. Kept
the ordering field (still drives display order) but eligibility
everywhere (RefereeProfile.eligible_teams, events.services.referees,
the team detail page's eligible-referees list) now reads through
RefereeLevel.eligible_team_ids, which walks the inherits_from chain.
clean() rejects a loop, including an indirect one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-20 22:42:18 +02:00
parent 5889b3740e
commit 66d7de820f
10 changed files with 235 additions and 16 deletions

View File

@@ -1133,7 +1133,16 @@ class TeamDetailView(ClubStaffRequiredMixin, DetailView):
no_shows=no_shows,
# None (not an empty queryset) signals "federation-managed" to the
# template, distinct from "club-managed, nobody eligible yet".
eligible_referees=(Member.objects.filter(referee_profile__level__teams=team, referee_profile__valid_until__gte=timezone.localdate()).order_by("last_name", "first_name") if team.referee_management == Team.RefereeManagement.CLUB else None),
# Levels resolved to ids first, not a flat level__teams=team filter --
# same reasoning as events.services.referees.eligible_referees, since
# a level may qualify for this team only via what it inherits from.
eligible_referees=(
Member.objects.filter(
referee_profile__level_id__in=[level.pk for level in RefereeLevel.objects.filter(club=club) if team.pk in level.eligible_team_ids()], referee_profile__valid_until__gte=timezone.localdate()
).order_by("last_name", "first_name")
if team.referee_management == Team.RefereeManagement.CLUB
else None
),
**kwargs,
)
@@ -1835,7 +1844,7 @@ class RefereeLevelListView(ClubStaffRequiredMixin, ListView):
context_object_name = "levels"
def get_queryset(self):
return RefereeLevel.objects.filter(club=self.request.club).prefetch_related("teams")
return RefereeLevel.objects.filter(club=self.request.club).select_related("inherits_from").prefetch_related("teams")
class RefereeLevelCreateView(MemberAdminRequiredMixin, CreateView):
@@ -1890,8 +1899,11 @@ class RefereeListView(ClubStaffRequiredMixin, ListView):
context_object_name = "referees"
def get_queryset(self):
# No prefetch for eligible_teams below select_related's level: it walks the
# level's own inherits_from chain (RefereeLevel.eligible_team_ids), which a
# single prefetch_related path can't cover anyway.
members = members_visible_to(self.request.user, self.request.club, include_guardians=True).filter(referee_profile__isnull=False)
return members.select_related("referee_profile", "referee_profile__level").prefetch_related("referee_profile__level__teams").order_by("last_name", "first_name")
return members.select_related("referee_profile", "referee_profile__level").order_by("last_name", "first_name")
# --- Groups: a generic named collection of members (all coaches, all team managers,