Notify a member once when a roster/group change adds them to new events

Joining a team or group already re-synced Attendance rows for its upcoming
events (events/signals.py), but did so silently. A fresh membership (not a
field edit, not a removal) now sends one summary notification -- "N new
events on your calendar" -- rather than one per event, so joining mid-season
with a whole recurring series already scheduled doesn't flood the member
with pushes.

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 18:44:45 +02:00
parent 5db885c809
commit 618d15a3e0
4 changed files with 115 additions and 4 deletions

View File

@@ -18,11 +18,14 @@ deliberate staff decision that should win regardless.
from django.db.models import Count, Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext
from club.models import ClubMembership, Season
from club.services.onboarding import blocked_member_ids_for_event
from events.models import Attendance, Event
from members.models import Member
from notifications.services import notify_members
from teams.models import TeamMembership
@@ -85,6 +88,22 @@ def sync_event_attendances(event):
event.attendances.filter(member_id__in=to_remove).delete()
def notify_newly_invited(member, *, club, events):
"""One notification (push+email), not one per event -- used when a
roster/group change (events/signals.py's sync_on_roster_change/
sync_on_group_membership_change) adds ``member`` to a team/group that
already has upcoming events on the calendar. A flood of one-per-event
pushes for, say, joining a team mid-season with a whole recurring
practice series already scheduled would be worse than useless -- same
reasoning as news.tasks._dedupe_by_recipients's single-summary
preference over a flood, just per-recipient instead of cross-family."""
if not events:
return
title = _("New events on your calendar")
body = ngettext("%(count)d new event has been added to your calendar.", "%(count)d new events have been added to your calendar.", len(events)) % {"count": len(events)}
notify_members([member], club=club, title=title, body=body)
def record_check_in(attendance, *, showed_up):
"""Record whether ``attendance``'s member actually showed up, separate from
their RSVP status -- the hook a future check-in UI (the coaches app) writes