Files
training-software/notifications/views.py
Paperclip CTO 6384eac890
Some checks failed
CI / Tests (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / OpenAPI Schema (push) Has been cancelled
feat(TRA-245): Notification service with email and in-app delivery
- Notification model with idempotency_key dedup (unique per event+object+recipient)
- NotificationDelivery audit model (pending/sent/failed/delivered per channel)
- notify() service: creates notification idempotently, enqueues per-channel tasks
- deliver_notification_task Celery task: sends email via send_mail, marks in-app
  as sent without email; marks FAILED with error_detail on exception (autoretry x3)
- Event trigger helpers: notify_course_assigned, notify_attempt_limit_reached,
  notify_certificate_issued, notify_certificate_expiring (daily-reminder safe)
- send_course_due_reminders_task: periodic Celery Beat stub for due-date alerts
- REST API: list notifications (with ?unread=true filter), mark-read, mark-all-read
- Admin registrations with inline delivery audit view
- Initial migration (Notification + NotificationDelivery tables)
- Pytest test suite: idempotency, delivery dispatch, view auth/filtering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 09:34:24 +02:00

62 lines
2.1 KiB
Python

from django.utils.timezone import now
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Notification
class NotificationListView(APIView):
"""List the authenticated user's notifications, newest first."""
permission_classes = [IsAuthenticated]
def get(self, request):
qs = Notification.objects.filter(recipient=request.user)
unread_only = request.query_params.get("unread") == "true"
if unread_only:
qs = qs.filter(read_at__isnull=True)
data = [
{
"id": str(n.id),
"event_type": n.event_type,
"title": n.title,
"body": n.body,
"object_type": n.object_type,
"object_id": n.object_id,
"is_read": n.is_read,
"created_at": n.created_at.isoformat(),
"read_at": n.read_at.isoformat() if n.read_at else None,
}
for n in qs[:100] # cap at 100 per page; pagination is a future concern
]
return Response(data)
class NotificationMarkReadView(APIView):
"""Mark a notification as read."""
permission_classes = [IsAuthenticated]
def post(self, request, notification_id):
try:
n = Notification.objects.get(pk=notification_id, recipient=request.user)
except Notification.DoesNotExist:
return Response({"detail": "Not found."}, status=status.HTTP_404_NOT_FOUND)
if n.read_at is None:
n.read_at = now()
n.save(update_fields=["read_at"])
return Response({"id": str(n.id), "is_read": True})
class NotificationMarkAllReadView(APIView):
"""Mark all of the authenticated user's unread notifications as read."""
permission_classes = [IsAuthenticated]
def post(self, request):
ts = now()
count = Notification.objects.filter(
recipient=request.user, read_at__isnull=True
).update(read_at=ts)
return Response({"marked_read": count})