feat(events): team/invited audience with attendance sync

Rework an event's audience: replace the single team FK with a teams M2M
plus invited_members and excluded_members, and add a season FK (derived
from the start date when blank) so team rosters resolve correctly. A data
migration copies existing team -> teams.

Add an attendance sync service: the effective audience is the union of the
teams' rosters for the event's season plus invited, minus excluded;
sync_event_attendances reconciles Attendance rows for future events only,
adding NO_RESPONSE rows for new members and hard-deleting rows for members
no longer invited. Signals drive it: editing an event or its audience
re-syncs that event, and adding/removing a team-roster member re-syncs
that team's future events.

Register all events models in the admin (with an attendance inline) and
add events to the admin registration smoke test. Add Season.covering()
and pillow (Opponent.logo ImageField). Full suite at 100% coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 17:44:48 +02:00
parent 076a9cacbc
commit 866716d196
19 changed files with 654 additions and 1 deletions

View File

@@ -74,6 +74,11 @@ class Season(ClubScopedModel):
return cls.objects.current_club().filter(start_date__lte=date, end_date__gte=date).first()
@classmethod
def covering(cls, club, date: datetime.date):
"""Return ``club``'s season covering ``date`` (no tenant context needed)."""
return cls.objects.filter(club=club, start_date__lte=date, end_date__gte=date).first()
class ClubMembership(ClubScopedModel):
class StatusChoices(models.TextChoices):

View File

@@ -455,7 +455,7 @@ class AdminRegistrationSmokeTests(TestCase):
registered = set(django_admin.site._registry)
# Concrete, non-auto-created models in these apps should all be registered.
project_apps = {"authentication", "club", "members", "teams"}
project_apps = {"authentication", "club", "members", "teams", "events"}
for model in apps.get_models():
if model._meta.app_label not in project_apps or model._meta.auto_created:
continue