Files
RosterChief/events/signals.py
Bernard Siebens 866716d196 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>
2026-07-12 17:44:48 +02:00

48 lines
1.6 KiB
Python

"""Signal wiring that keeps attendance in sync with the event audience.
Registered from ``EventsConfig.ready``. Two triggers:
* editing an event (its ``start``, or its ``teams`` / ``invited_members`` /
``excluded_members``) re-syncs that event;
* adding or removing a member from a team roster re-syncs that team's future
events.
"""
from django.db.models.signals import m2m_changed, post_delete, post_save
from django.dispatch import receiver
from django.utils import timezone
from events.models import Event
from events.services import sync_event_attendances
from teams.models import TeamMembership
M2M_SYNC_ACTIONS = {"post_add", "post_remove", "post_clear"}
@receiver(post_save, sender=Event)
def sync_on_event_save(sender, instance, **kwargs):
sync_event_attendances(instance)
@receiver(m2m_changed, sender=Event.teams.through)
@receiver(m2m_changed, sender=Event.invited_members.through)
@receiver(m2m_changed, sender=Event.excluded_members.through)
def sync_on_audience_change(sender, instance, action, reverse, model, pk_set, **kwargs):
if action not in M2M_SYNC_ACTIONS:
return
if isinstance(instance, Event):
sync_event_attendances(instance)
else:
# Reverse relation edited (e.g. member.invited_to_events.add(event)).
for event in Event.objects.filter(pk__in=pk_set or []):
sync_event_attendances(event)
@receiver(post_save, sender=TeamMembership)
@receiver(post_delete, sender=TeamMembership)
def sync_on_roster_change(sender, instance, **kwargs):
now = timezone.now()
for event in Event.objects.filter(teams=instance.team_id, start__gte=now).distinct():
sync_event_attendances(event)