Let eligible referees self-serve sign-up for games needing one

Creating (or re-teaming) a home game for a club-managed team now auto-
invites every eligible referee (teams.RefereeProfile) via a new
RefereeSignup model, notifying them the same way news/events already do.
They see it as its own distinct row on the mobile Calendar (own accent
colour) and can accept or decline right there -- accepting routes through
the existing capacity-checked assign_referee (assigned_by=None marks it
self-service), so it lands as a real EventReferee row with no separate sync
step. The desktop referee-management screen and event detail page both
surface pending invites and flag self-signed-up referees distinctly from
admin assignments.

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 17:19:48 +02:00
parent 135580d83e
commit 747514f1ff
17 changed files with 596 additions and 18 deletions

View File

@@ -27,9 +27,10 @@ from club.services.fees import open_dues_rows
from club.services.onboarding import checklist_for
from club.services.sponsors import active_sponsors
from controlpanel.messages import notify
from events.models import Attendance, Event, Lineup
from events.models import Attendance, Event, Lineup, RefereeSignup
from events.services.calendar import week_bounds
from events.services.lineup import notify_dropout, selected_members_by_position
from events.services.referees import RefereeAssignmentError, accept_referee_signup, decline_referee_signup
from members.models import FamilyMembership, Member
from members.views import ClubScopedPublicMixin
from news.models import News
@@ -348,6 +349,31 @@ class CalendarView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
for attendance in attendances
]
# Referee sign-ups are scoped to self.me only, never managed_people --
# a referee is an adult with their own account, not something a
# parent does on a child's behalf, unlike every RSVP row above.
# Merged into the same chronological list (own dict shape, no
# pill_class/member) rather than a separate section, so it reads on
# the actual day it falls on -- distinct styling is what sets it
# apart (mobile/_calendar_referee_row.html), not a different place
# on the screen. Declined invites are dropped; accepted ones stay
# visible as a confirmed commitment.
if self.me is not None and kind_filter != "training":
signups = (
RefereeSignup.objects.filter(
member=self.me,
event__club=self.request.club,
event__cancelled=False,
event__start__gte=now,
status__in=[RefereeSignup.Status.INVITED, RefereeSignup.Status.ACCEPTED],
)
.select_related("event", "event__location", "event__opponent")
.prefetch_related("event__teams")
)
rows += [{"event": signup.event, "referee_signup": signup} for signup in signups]
rows.sort(key=lambda row: row["event"].start)
this_week, next_week, later_rows = [], [], []
for row in rows:
event_date = timezone.localtime(row["event"].start).date()
@@ -368,6 +394,33 @@ class CalendarView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
return super().get_context_data(this_week=this_week, next_week=next_week, later_months=later_months, kind_filter=kind_filter if kind_filter in self.KIND_FILTERS else "", **kwargs)
class RefereeSignupRespondView(PersonScopeMixin, LoginRequiredMixin, View):
"""Accept/decline a referee invite from a Calendar row
(mobile/_calendar_referee_row.html) -- routes through
events.services.referees.accept_referee_signup/decline_referee_signup,
so capacity is enforced in the one place the desktop admin flow already
enforces it, and the referee-management screen sees the result with no
separate sync step."""
def post(self, request, *args, **kwargs):
signup = get_object_or_404(RefereeSignup, pk=kwargs["signup_id"], member=self.me, event__club=request.club)
response = request.POST.get("response")
if response == "accept":
try:
accept_referee_signup(signup)
notify(request, f"s|{_('Referee sign-up confirmed')}|{_('Thanks for signing up -- see you there.')}")
except RefereeAssignmentError as exc:
notify(request, f"e|{_('Could not sign you up')}|{exc}")
elif response == "decline":
decline_referee_signup(signup)
notify(request, f"s|{_('Declined')}|{_('No problem -- thanks for letting us know.')}")
else:
return HttpResponseBadRequest(_("Unknown response."))
return HttpResponseRedirect(reverse("mobile:calendar"))
class EventDetailView(PersonScopeMixin, LoginRequiredMixin, TemplateView):
"""M2 -- design_handoff_rosterchief_platform/README.md's M2 section:
"answer for several". A hero header (no event photos in this codebase --