notifications app
Cross-cutting notification fan-out. Most apps emit notifications; this app handles delivery.
Models (4)
Notification— single in-app notification; read flag, action URL, event_typeNotificationPreference— per-user, per-event-type channel toggles (in_app / email / push)ForumSubscription— subscriptions to forum threads (for new-reply notifications)DigestPreference— per-user digest config (daily / weekly), with which event categories included
Key endpoints
| URL | Purpose |
|---|---|
GET /api/notifications/ | List my notifications (paginated, sorted by date desc) |
PATCH /api/notifications/<id>/read/ | Mark as read |
POST /api/notifications/mark-all-read/ | Bulk mark read |
GET /api/notifications/unread-count/ | For badge in navbar |
GET /api/notification-preferences/ | My preferences |
PATCH /api/notification-preferences/ | Update preferences |
GET /api/digest-preferences/ | My digest config |
Permissions
IsAuthenticated— all endpoints (own notifications only)
Background tasks
send_notification_emails(notification_ids)— batches per recipient, sends via email backendgenerate_digest_emails— cron at 06:00 UTC daily; assembles per-user digest from previous day's notificationscleanup_old_notifications— daily; removes read notifications older than 90 days
External integrations
- Email backend (
apps.platform.PlatformEmailConfig) - Mobile push (FCM / APNs via Capacitor's push registration — token stored in
User.push_tokens)
Notable patterns
Notification fan-out pattern
Other apps create notifications by importing the helper:
from apps.notifications.services import notify
notify(
recipient=user,
event_type="forum.new_reply",
title="New reply in 'Spring Formal Planning'",
body="Alice replied to your post.",
action_url=f"/chapter/engage/forums/{forum.id}/posts/{post.id}",
metadata={"post_id": post.id},
)
The helper:
- Creates the
Notificationrow (in-app) - Looks up the user's
NotificationPreferencefor this event_type - Queues an email task if enabled
- Sends a push if enabled and the user has a registered token
- Adds to the digest queue if the user has digests enabled
Event types
Defined in apps/notifications/event_types.py (constants):
EVENT_TYPES = [
"bulletin.new_chapter",
"bulletin.new_org",
"forum.new_thread",
"forum.new_reply",
"forum.mention",
"election.opened",
"election.reminder",
"election.results",
"billing.invoice_issued",
"billing.payment_due",
"billing.payment_confirmed",
"approval.requested",
"approval.decided",
"event.invited",
"event.reminder",
"event.changed",
"learning.assigned",
"learning.due_soon",
"learning.certificate_ready",
"compliance.requirement_due",
"compliance.overdue",
"recognition.received",
"messages.new_dm",
"messages.group_mention",
]
Each has a default channel set (e.g., payment_due defaults to email + push; recognition.received defaults to in-app only).
Digest
If a user has digests enabled for a category, individual notifications still appear in-app but emails are suppressed. The daily / weekly digest collects them into one summary email.
Digest email template at templates/notifications/digest_email.html.
Read state
Notification.read boolean; auto-set to True when the user clicks through. Unread count drives the navbar badge; computed via:
Notification.objects.filter(recipient=user, read=False).count()
Cached for 30s to keep badge updates fast.
Signals
post_saveonNotification— triggers email send (if preferences allow)
Code paths
- Models:
backend/apps/notifications/models.py - Service:
backend/apps/notifications/services.py - Tasks:
backend/apps/notifications/tasks.py - Email templates:
backend/apps/notifications/templates/