Notify members when a new event is planned

New events.tasks.notify_new_event, scheduled from
management.views.EventCreateView.form_valid -- the deliberate "a staff
member planned one new event" action, not every Event row that happens
to get created. A recurring series' rolling-horizon extension
(extend_event_series) and bulk fixture imports are NOT wired to this on
purpose: either would flood everyone with one push per occurrence
instead of the single, deliberate action this is meant to catch.

Notifies whoever is still NO_RESPONSE right after creation -- exactly
"everyone who needs to respond", using the existing notify_members() ->
Notification -> mobile.signals push chain already in place, so this is
in-app (visible in the M7 inbox, mark read/mark-all-read) and a push
notification both, with no new plumbing needed there.

Generalized mobile.views.NotificationsView's News-only "tap to open the
source" handling (previously isinstance(source, News)) into
_notification_source_link, covering Event sources too -- tapping an
"new event" notification now marks it read and opens the event's answer
screen, the same way a news notification already opened the article.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-21 17:03:26 +02:00
parent 1bf3a9d37a
commit 81f8f7f7dd
6 changed files with 101 additions and 28 deletions

View File

@@ -1,15 +1,17 @@
"""Celery task behind the `extend-event-series` beat schedule entry (see
rosterchief/settings.CELERY_BEAT_SCHEDULE and features/jobs.py).
Mirrors `manage.py extend_event_series` exactly -- that command still exists, unchanged, for
manual use from a shell (see events/management/commands/extend_event_series.py).
"""Celery tasks for events -- the `extend-event-series` beat schedule entry
(see rosterchief/settings.CELERY_BEAT_SCHEDULE and features/jobs.py), and
notifying members when a new event needs their RSVP.
"""
from celery import shared_task
from django.utils import timezone
from django.utils.translation import gettext as _
from events.models import EventSeries
from events.models import Attendance, Event, EventSeries
from events.services import generate_occurrences, horizon
from features.models import Maintenance
from members.models import Member
from notifications.services import notify_members
@shared_task(name="events.tasks.extend_event_series")
@@ -26,3 +28,32 @@ def extend_event_series():
total += len(generate_occurrences(series, until))
return f"Generated {total} occurrence(s) across {EventSeries.objects.count()} series."
@shared_task(name="events.tasks.notify_new_event")
def notify_new_event(event_id):
"""Scheduled from management.views.EventCreateView.form_valid -- a staff
member deliberately planning one new event, not every Event row that
happens to get created (a recurring series' rolling-horizon extension
via extend_event_series above, or a bulk fixture import, would otherwise
flood everyone with one push per occurrence; those are intentionally not
wired to this).
Notifies only whoever is still NO_RESPONSE -- right after creation every
invited member starts there (events.services.attendance.sync_event_attendances
just ran via the Event post_save signal), so this is exactly "everyone
who needs to respond", not the full invited list.
"""
event = Event.objects.filter(pk=event_id, cancelled=False).select_related("club").first()
if event is None:
return "Skipped: event no longer exists or was cancelled."
member_ids = Attendance.objects.filter(event=event, status=Attendance.AttendanceStatus.NO_RESPONSE).values_list("member_id", flat=True)
members = Member.objects.filter(id__in=member_ids)
if not members:
return "Skipped: no one to notify."
when = timezone.localtime(event.start).strftime("%a %d %b, %H:%M")
body = _("New %(kind)s: %(when)s. Let us know if you can make it.") % {"kind": event.get_kind_display(), "when": when}
notifications = notify_members(members, club=event.club, title=event.title, body=body, source=event)
return f"Notified {len(notifications)} member(s)."