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

@@ -304,6 +304,42 @@ class EventReferee(UUIDModel):
return (self.fee or Decimal("0")) + self.km_total
class RefereeSignup(UUIDModel):
"""One eligible referee's invite/response for one home game -- the
self-service counterpart to admin assignment (EventReferee). Created
automatically (events.services.referees.sync_referee_invites, wired from
events/signals.py) the moment a home game needs a club-arranged referee,
for every currently-eligible member (teams.RefereeProfile). Accepting
(events.services.referees.accept_referee_signup) creates a real
EventReferee row via the same capacity-checked assign_referee every admin
assignment goes through (assigned_by=None marks it self-service) -- this
table only ever tracks the invite/response, never payment/capacity, so
the desktop referee-management screen stays the single source of truth
for who's actually refereeing.
"""
class Status(models.TextChoices):
INVITED = "invited", _("invited")
ACCEPTED = "accepted", _("accepted")
DECLINED = "declined", _("declined")
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="referee_signups", verbose_name=_("event"))
member = models.ForeignKey(Member, on_delete=models.CASCADE, related_name="referee_signups", verbose_name=_("member"))
status = models.CharField(_("status"), max_length=20, choices=Status.choices, default=Status.INVITED)
responded_at = models.DateTimeField(_("responded at"), null=True, blank=True)
class Meta:
verbose_name = _("referee sign-up")
verbose_name_plural = _("referee sign-ups")
ordering = ["event", "member__last_name", "member__first_name"]
constraints = [
models.UniqueConstraint(fields=["event", "member"], name="unique_referee_signup_per_event_per_member"),
]
def __str__(self):
return f"{self.event} - {self.member} ({self.status})"
class Competition(models.Model):
"""A competition has a name with a specific URL to fetch data from. These are managed centrally."""