mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-04-01 11:18:22 -04:00
Based on the git diff provided, here's a concise and descriptive commit message:
feat: add passkey authentication and enhance user preferences - Add passkey login security event type with fingerprint icon - Include request and site context in email confirmation for backend - Add user_id exact match filter to prevent incorrect user lookups - Enable PATCH method for updating user preferences via API - Add moderation_preferences support to user settings - Optimize ticket queries with select_related and prefetch_related This commit introduces passkey authentication tracking, improves user profile filtering accuracy, and extends the preferences API to support updates. Query optimizations reduce database hits for ticket listings.
This commit is contained in:
46
backend/apps/notifications/api/log_serializers.py
Normal file
46
backend/apps/notifications/api/log_serializers.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
Serializers for Notification Log API.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.core.choices.serializers import RichChoiceSerializerField
|
||||
from apps.notifications.models import NotificationLog
|
||||
|
||||
|
||||
class NotificationLogSerializer(serializers.ModelSerializer):
|
||||
"""Serializer for notification logs."""
|
||||
|
||||
status = RichChoiceSerializerField(
|
||||
choice_group="notification_log_statuses",
|
||||
domain="notifications",
|
||||
)
|
||||
user_username = serializers.CharField(
|
||||
source="user.username",
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
)
|
||||
user_email = serializers.EmailField(
|
||||
source="user.email",
|
||||
read_only=True,
|
||||
allow_null=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = NotificationLog
|
||||
fields = [
|
||||
"id",
|
||||
"user",
|
||||
"user_username",
|
||||
"user_email",
|
||||
"workflow_id",
|
||||
"notification_type",
|
||||
"channel",
|
||||
"status",
|
||||
"payload",
|
||||
"error_message",
|
||||
"novu_transaction_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["id", "created_at", "updated_at", "user_username", "user_email"]
|
||||
61
backend/apps/notifications/api/log_views.py
Normal file
61
backend/apps/notifications/api/log_views.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
ViewSet for Notification Log API.
|
||||
"""
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from rest_framework.permissions import IsAdminUser
|
||||
|
||||
from apps.notifications.models import NotificationLog
|
||||
|
||||
from .log_serializers import NotificationLogSerializer
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
summary="List notification logs",
|
||||
description="Get all notification logs with optional filtering by status, channel, or workflow.",
|
||||
tags=["Admin - Notifications"],
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
summary="Get notification log",
|
||||
description="Get details of a specific notification log entry.",
|
||||
tags=["Admin - Notifications"],
|
||||
),
|
||||
)
|
||||
class NotificationLogViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
ViewSet for viewing notification logs.
|
||||
|
||||
Provides read-only access to notification delivery history.
|
||||
"""
|
||||
|
||||
queryset = NotificationLog.objects.select_related("user").all()
|
||||
serializer_class = NotificationLogSerializer
|
||||
permission_classes = [IsAdminUser]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||
filterset_fields = ["status", "channel", "workflow_id", "notification_type"]
|
||||
search_fields = ["workflow_id", "notification_type", "error_message"]
|
||||
ordering_fields = ["created_at", "status"]
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# Filter by user ID if provided
|
||||
user_id = self.request.query_params.get("user_id")
|
||||
if user_id:
|
||||
queryset = queryset.filter(user_id=user_id)
|
||||
|
||||
# Date range filtering
|
||||
start_date = self.request.query_params.get("start_date")
|
||||
end_date = self.request.query_params.get("end_date")
|
||||
|
||||
if start_date:
|
||||
queryset = queryset.filter(created_at__gte=start_date)
|
||||
if end_date:
|
||||
queryset = queryset.filter(created_at__lte=end_date)
|
||||
|
||||
return queryset
|
||||
185
backend/apps/notifications/choices.py
Normal file
185
backend/apps/notifications/choices.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Rich Choice Objects for Notifications Domain
|
||||
|
||||
This module defines all choice objects for the notifications domain,
|
||||
using the RichChoices pattern for consistent UI rendering and validation.
|
||||
"""
|
||||
|
||||
from apps.core.choices import ChoiceCategory, RichChoice
|
||||
from apps.core.choices.registry import register_choices
|
||||
|
||||
# ============================================================================
|
||||
# Notification Log Status Choices
|
||||
# ============================================================================
|
||||
NOTIFICATION_LOG_STATUSES = [
|
||||
RichChoice(
|
||||
value="pending",
|
||||
label="Pending",
|
||||
description="Notification is queued for delivery",
|
||||
metadata={
|
||||
"color": "yellow",
|
||||
"icon": "clock",
|
||||
"css_class": "bg-yellow-100 text-yellow-800",
|
||||
"sort_order": 1,
|
||||
},
|
||||
category=ChoiceCategory.STATUS,
|
||||
),
|
||||
RichChoice(
|
||||
value="sent",
|
||||
label="Sent",
|
||||
description="Notification has been sent",
|
||||
metadata={
|
||||
"color": "blue",
|
||||
"icon": "send",
|
||||
"css_class": "bg-blue-100 text-blue-800",
|
||||
"sort_order": 2,
|
||||
},
|
||||
category=ChoiceCategory.STATUS,
|
||||
),
|
||||
RichChoice(
|
||||
value="delivered",
|
||||
label="Delivered",
|
||||
description="Notification was successfully delivered",
|
||||
metadata={
|
||||
"color": "green",
|
||||
"icon": "check-circle",
|
||||
"css_class": "bg-green-100 text-green-800",
|
||||
"sort_order": 3,
|
||||
},
|
||||
category=ChoiceCategory.STATUS,
|
||||
),
|
||||
RichChoice(
|
||||
value="failed",
|
||||
label="Failed",
|
||||
description="Notification delivery failed",
|
||||
metadata={
|
||||
"color": "red",
|
||||
"icon": "x-circle",
|
||||
"css_class": "bg-red-100 text-red-800",
|
||||
"sort_order": 4,
|
||||
},
|
||||
category=ChoiceCategory.STATUS,
|
||||
),
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# System Announcement Severity Choices
|
||||
# ============================================================================
|
||||
ANNOUNCEMENT_SEVERITIES = [
|
||||
RichChoice(
|
||||
value="info",
|
||||
label="Information",
|
||||
description="Informational announcement",
|
||||
metadata={
|
||||
"color": "blue",
|
||||
"icon": "info",
|
||||
"css_class": "bg-blue-100 text-blue-800 border-blue-200",
|
||||
"sort_order": 1,
|
||||
},
|
||||
category=ChoiceCategory.PRIORITY,
|
||||
),
|
||||
RichChoice(
|
||||
value="warning",
|
||||
label="Warning",
|
||||
description="Warning announcement requiring attention",
|
||||
metadata={
|
||||
"color": "yellow",
|
||||
"icon": "alert-triangle",
|
||||
"css_class": "bg-yellow-100 text-yellow-800 border-yellow-200",
|
||||
"sort_order": 2,
|
||||
},
|
||||
category=ChoiceCategory.PRIORITY,
|
||||
),
|
||||
RichChoice(
|
||||
value="critical",
|
||||
label="Critical",
|
||||
description="Critical announcement requiring immediate attention",
|
||||
metadata={
|
||||
"color": "red",
|
||||
"icon": "alert-octagon",
|
||||
"css_class": "bg-red-100 text-red-800 border-red-200",
|
||||
"sort_order": 3,
|
||||
},
|
||||
category=ChoiceCategory.PRIORITY,
|
||||
),
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# Notification Level Choices (for in-app notifications)
|
||||
# ============================================================================
|
||||
NOTIFICATION_LEVELS = [
|
||||
RichChoice(
|
||||
value="info",
|
||||
label="Info",
|
||||
description="Informational notification",
|
||||
metadata={
|
||||
"color": "blue",
|
||||
"icon": "info",
|
||||
"css_class": "bg-blue-100 text-blue-800",
|
||||
"sort_order": 1,
|
||||
},
|
||||
category=ChoiceCategory.NOTIFICATION,
|
||||
),
|
||||
RichChoice(
|
||||
value="success",
|
||||
label="Success",
|
||||
description="Success notification",
|
||||
metadata={
|
||||
"color": "green",
|
||||
"icon": "check-circle",
|
||||
"css_class": "bg-green-100 text-green-800",
|
||||
"sort_order": 2,
|
||||
},
|
||||
category=ChoiceCategory.NOTIFICATION,
|
||||
),
|
||||
RichChoice(
|
||||
value="warning",
|
||||
label="Warning",
|
||||
description="Warning notification",
|
||||
metadata={
|
||||
"color": "yellow",
|
||||
"icon": "alert-triangle",
|
||||
"css_class": "bg-yellow-100 text-yellow-800",
|
||||
"sort_order": 3,
|
||||
},
|
||||
category=ChoiceCategory.NOTIFICATION,
|
||||
),
|
||||
RichChoice(
|
||||
value="error",
|
||||
label="Error",
|
||||
description="Error notification",
|
||||
metadata={
|
||||
"color": "red",
|
||||
"icon": "x-circle",
|
||||
"css_class": "bg-red-100 text-red-800",
|
||||
"sort_order": 4,
|
||||
},
|
||||
category=ChoiceCategory.NOTIFICATION,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def register_notifications_choices() -> None:
|
||||
"""Register all notifications domain choices with the global registry."""
|
||||
register_choices(
|
||||
name="notification_log_statuses",
|
||||
choices=NOTIFICATION_LOG_STATUSES,
|
||||
domain="notifications",
|
||||
description="Status options for notification logs",
|
||||
)
|
||||
register_choices(
|
||||
name="announcement_severities",
|
||||
choices=ANNOUNCEMENT_SEVERITIES,
|
||||
domain="notifications",
|
||||
description="Severity levels for system announcements",
|
||||
)
|
||||
register_choices(
|
||||
name="notification_levels",
|
||||
choices=NOTIFICATION_LEVELS,
|
||||
domain="notifications",
|
||||
description="Level options for in-app notifications",
|
||||
)
|
||||
|
||||
|
||||
# Auto-register choices when module is imported
|
||||
register_notifications_choices()
|
||||
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 5.2.10 on 2026-01-10 22:01
|
||||
|
||||
import apps.core.choices.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("notifications", "0002_add_notification_model"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="notification",
|
||||
name="level",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="notification_levels",
|
||||
choices=[("info", "Info"), ("success", "Success"), ("warning", "Warning"), ("error", "Error")],
|
||||
default="info",
|
||||
domain="notifications",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="notificationlog",
|
||||
name="status",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="notification_log_statuses",
|
||||
choices=[("pending", "Pending"), ("sent", "Sent"), ("delivered", "Delivered"), ("failed", "Failed")],
|
||||
default="pending",
|
||||
domain="notifications",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="systemannouncement",
|
||||
name="severity",
|
||||
field=apps.core.choices.fields.RichChoiceField(
|
||||
allow_deprecated=False,
|
||||
choice_group="announcement_severities",
|
||||
choices=[("info", "Information"), ("warning", "Warning"), ("critical", "Critical")],
|
||||
default="info",
|
||||
domain="notifications",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -14,6 +14,11 @@ Subscriber model is kept for backward compatibility but is optional.
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
from apps.core.choices.fields import RichChoiceField
|
||||
|
||||
# Import choices to ensure registration on app load
|
||||
from . import choices # noqa: F401
|
||||
|
||||
|
||||
class Subscriber(models.Model):
|
||||
"""
|
||||
@@ -100,12 +105,6 @@ class NotificationLog(models.Model):
|
||||
Audit log of sent notifications.
|
||||
"""
|
||||
|
||||
class Status(models.TextChoices):
|
||||
PENDING = "pending", "Pending"
|
||||
SENT = "sent", "Sent"
|
||||
DELIVERED = "delivered", "Delivered"
|
||||
FAILED = "failed", "Failed"
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -115,10 +114,11 @@ class NotificationLog(models.Model):
|
||||
workflow_id = models.CharField(max_length=100, db_index=True)
|
||||
notification_type = models.CharField(max_length=50)
|
||||
channel = models.CharField(max_length=20) # email, push, in_app, sms
|
||||
status = models.CharField(
|
||||
status = RichChoiceField(
|
||||
choice_group="notification_log_statuses",
|
||||
domain="notifications",
|
||||
max_length=20,
|
||||
choices=Status.choices,
|
||||
default=Status.PENDING,
|
||||
default="pending",
|
||||
)
|
||||
payload = models.JSONField(default=dict, blank=True)
|
||||
error_message = models.TextField(blank=True)
|
||||
@@ -144,17 +144,13 @@ class SystemAnnouncement(models.Model):
|
||||
System-wide announcements.
|
||||
"""
|
||||
|
||||
class Severity(models.TextChoices):
|
||||
INFO = "info", "Information"
|
||||
WARNING = "warning", "Warning"
|
||||
CRITICAL = "critical", "Critical"
|
||||
|
||||
title = models.CharField(max_length=255)
|
||||
message = models.TextField()
|
||||
severity = models.CharField(
|
||||
severity = RichChoiceField(
|
||||
choice_group="announcement_severities",
|
||||
domain="notifications",
|
||||
max_length=20,
|
||||
choices=Severity.choices,
|
||||
default=Severity.INFO,
|
||||
default="info",
|
||||
)
|
||||
action_url = models.URLField(blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
@@ -184,12 +180,6 @@ class Notification(models.Model):
|
||||
supporting both in-app and email notification channels.
|
||||
"""
|
||||
|
||||
class Level(models.TextChoices):
|
||||
INFO = "info", "Info"
|
||||
SUCCESS = "success", "Success"
|
||||
WARNING = "warning", "Warning"
|
||||
ERROR = "error", "Error"
|
||||
|
||||
# Who receives the notification
|
||||
recipient = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
@@ -207,10 +197,11 @@ class Notification(models.Model):
|
||||
# What happened
|
||||
verb = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
level = models.CharField(
|
||||
level = RichChoiceField(
|
||||
choice_group="notification_levels",
|
||||
domain="notifications",
|
||||
max_length=20,
|
||||
choices=Level.choices,
|
||||
default=Level.INFO,
|
||||
default="info",
|
||||
)
|
||||
# The object that was acted upon (generic foreign key)
|
||||
action_object_content_type = models.ForeignKey(
|
||||
|
||||
@@ -4,6 +4,8 @@ Notification serializers.
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.core.choices.serializers import RichChoiceSerializerField
|
||||
|
||||
from .models import NotificationLog, NotificationPreference, Subscriber, SystemAnnouncement
|
||||
|
||||
|
||||
@@ -131,8 +133,9 @@ class CreateAnnouncementSerializer(serializers.Serializer):
|
||||
|
||||
title = serializers.CharField(required=True, max_length=255)
|
||||
message = serializers.CharField(required=True)
|
||||
severity = serializers.ChoiceField(
|
||||
choices=["info", "warning", "critical"],
|
||||
severity = RichChoiceSerializerField(
|
||||
choice_group="announcement_severities",
|
||||
domain="notifications",
|
||||
default="info",
|
||||
)
|
||||
action_url = serializers.URLField(required=False, allow_blank=True)
|
||||
|
||||
Reference in New Issue
Block a user