# apps/notification/admin.py
"""
Django Admin for the Notification domain.
Models registered:
- Notification : Delivered notification ledger (read-only)
- NotificationTemplate : Admin-editable message templates
- NotificationPreference : Per-user channel opt-in/out settings
Production rules:
- Notification records are immutable (delivery audit log)
- Templates can be edited by staff
- Preferences can be toggled via bulk action
- Retry failed notifications via admin action
- CSV export for sent/failed notification analytics
2026 features:
- Status badge (sent / failed / pending)
- Channel badge (email / sms / push / in_app)
- Date hierarchy for temporal analysis
- show_full_result_count = False (table can have millions of rows)
"""
import logging
from django.contrib import admin
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django.contrib import messages
from apps.notification.models import (
Notification,
NotificationTemplate,
NotificationPreference,
)
logger = logging.getLogger(__name__)
_CHANNEL_COLOURS = {
"email": ("#6366f1", "#fff"),
"sms": ("#10b981", "#fff"),
"push": ("#f59e0b", "#fff"),
"in_app": ("#3b82f6", "#fff"),
}
# ── Notification Admin (append-only) ──────────────────────────────────────────
@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
"""
Read-only audit log of every notification delivered (or failed to deliver).
"""
list_display = [
"id", "recipient", "type_badge", "channel_badge",
"title_preview", "read_badge",
"failed", "retry_count", "created_at",
]
list_filter = [
"notification_type", "channel", "failed",
]
search_fields = ["recipient__email", "title", "body"]
ordering = ["-created_at"]
date_hierarchy = "created_at"
list_select_related = ["recipient"]
raw_id_fields = ["recipient"]
list_per_page = 25
list_max_show_all = 200
show_full_result_count = False
empty_value_display = "-N/A-"
readonly_fields = [
"recipient", "notification_type", "channel",
"title", "body", "metadata",
"sent_at", "read_at", "external_id",
"retry_count", "failed", "error_msg",
"created_at", "updated_at",
]
fieldsets = (
(_("Recipient"), {
"fields": ("recipient",),
}),
(_("Notification"), {
"fields": ("notification_type", "channel", "title", "body"),
}),
(_("Delivery"), {
"fields": (
"sent_at", "read_at", "external_id",
"retry_count", "failed", "error_msg",
),
}),
(_("Metadata"), {
"fields": ("metadata",),
"classes": ("collapse",),
}),
(_("Timestamps"), {
"fields": ("created_at", "updated_at"),
"classes": ("collapse",),
}),
)
actions = ["action_retry_failed"]
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser # superuser can purge old records
@admin.display(description="Type")
def type_badge(self, obj):
return format_html(
'{}',
obj.notification_type,
)
@admin.display(description="Channel")
def channel_badge(self, obj):
bg, fg = _CHANNEL_COLOURS.get(obj.channel, ("#6b7280", "#fff"))
return format_html(
'{}',
bg, fg, obj.channel.upper(),
)
@admin.display(description="Title")
def title_preview(self, obj):
return (obj.title or "")[:60]
@admin.display(description="Read", boolean=True)
def read_badge(self, obj):
return obj.is_read
@admin.action(description="🔁 Retry selected failed notifications")
def action_retry_failed(self, request, queryset):
qs = queryset.filter(failed=True)
count = qs.count()
if count == 0:
self.message_user(
request, "No failed notifications selected.", level=messages.WARNING
)
return
qs.update(failed=False, retry_count=0, error_msg="")
self.message_user(
request,
f"🔁 {count} notification(s) queued for retry.",
level=messages.SUCCESS,
)
# ── Notification Template Admin ───────────────────────────────────────────────
@admin.register(NotificationTemplate)
class NotificationTemplateAdmin(admin.ModelAdmin):
list_display = [
"notification_type", "channel", "is_active",
"updated_at",
]
list_filter = ["channel", "is_active"]
search_fields = ["notification_type", "title_template", "body_template"]
ordering = ["notification_type", "channel"]
list_per_page = 25
empty_value_display = "-N/A-"
readonly_fields = ["created_at", "updated_at"]
fieldsets = (
(_("Template"), {
"fields": (
"notification_type", "channel", "is_active",
),
}),
(_("Content"), {
"fields": ("title_template", "body_template", "metadata_template"),
}),
(_("Timestamps"), {
"fields": ("created_at", "updated_at"),
"classes": ("collapse",),
}),
)
# ── Notification Preference Admin ─────────────────────────────────────────────
@admin.register(NotificationPreference)
class NotificationPreferenceAdmin(admin.ModelAdmin):
list_display = [
"user", "notification_type", "channel", "enabled",
]
list_filter = ["channel", "enabled", "notification_type"]
search_fields = ["user__email", "notification_type"]
ordering = ["user", "notification_type"]
raw_id_fields = ["user"]
list_select_related = ["user"]
list_per_page = 25
show_full_result_count = False
empty_value_display = "-N/A-"
readonly_fields = ["created_at", "updated_at"]
actions = ["enable_all", "disable_all"]
@admin.action(description="✅ Enable selected notification preferences")
def enable_all(self, request, queryset):
queryset.update(enabled=True)
self.message_user(request, "✅ Preferences enabled.", level=messages.SUCCESS)
@admin.action(description="🔕 Disable selected notification preferences")
def disable_all(self, request, queryset):
queryset.update(enabled=False)
self.message_user(request, "🔕 Preferences disabled.", level=messages.WARNING)