Files
training-software/notifications/migrations/0001_initial.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

75 lines
3.1 KiB
Python

import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Notification",
fields=[
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("event_type", models.CharField(
choices=[
("course_assigned", "Course Assigned"),
("course_due", "Course Due Soon"),
("attempt_limit_reached", "Attempt Limit Reached"),
("certificate_issued", "Certificate Issued"),
("certificate_expiring", "Certificate Expiring"),
],
db_index=True,
max_length=40,
)),
("title", models.CharField(max_length=255)),
("body", models.TextField(blank=True)),
("object_type", models.CharField(blank=True, max_length=64)),
("object_id", models.CharField(blank=True, max_length=128)),
("idempotency_key", models.CharField(db_index=True, max_length=255, unique=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("read_at", models.DateTimeField(blank=True, null=True)),
("recipient", models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to=settings.AUTH_USER_MODEL,
)),
],
options={"db_table": "notifications_notification", "ordering": ["-created_at"]},
),
migrations.CreateModel(
name="NotificationDelivery",
fields=[
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("channel", models.CharField(
choices=[("in_app", "In-App"), ("email", "Email")],
max_length=20,
)),
("status", models.CharField(
choices=[
("pending", "Pending"), ("sent", "Sent"),
("failed", "Failed"), ("delivered", "Delivered"),
],
default="pending",
max_length=20,
)),
("sent_at", models.DateTimeField(blank=True, null=True)),
("error_detail", models.TextField(blank=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("notification", models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="deliveries",
to="notifications.notification",
)),
],
options={"db_table": "notifications_delivery", "ordering": ["-created_at"]},
),
]