feat: replace email flows with in-app notifications
Add recipient notification detail and filtering, a queued admin composer, and contextual automatic titles. Retire email delivery, obsolete auth routes, newsletter data, and related templates and settings.
This commit is contained in:
@@ -9,10 +9,10 @@ class NotificationSchema(Schema):
|
||||
type: str
|
||||
title: str
|
||||
message: str
|
||||
has_body: bool = False
|
||||
level: str
|
||||
created_at: datetime | str
|
||||
is_seen: bool
|
||||
delete_on_seen: bool
|
||||
action_url: str | None = None
|
||||
entity_type: str | None = None
|
||||
entity_id: int | str | None = None
|
||||
@@ -25,6 +25,10 @@ class NotificationListSchema(Schema):
|
||||
notifications: list[NotificationSchema]
|
||||
|
||||
|
||||
class NotificationDetailSchema(NotificationSchema):
|
||||
body_markdown: str | None = None
|
||||
|
||||
|
||||
class NotificationSeenIn(Schema):
|
||||
id: str
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.views import View
|
||||
from ninja import Router
|
||||
|
||||
from apps.notifications.api.schemas import (
|
||||
NotificationDetailSchema,
|
||||
NotificationDeleteResponseSchema,
|
||||
NotificationListSchema,
|
||||
NotificationMarkAllReadResponseSchema,
|
||||
@@ -54,7 +55,9 @@ def _validate_stream_token(token: str | None) -> str:
|
||||
|
||||
|
||||
@notifications_router.get("/", response=NotificationListSchema, auth=jwt_auth)
|
||||
def list_notifications(request, limit: int | None = None, offset: int = 0, type: str | None = None):
|
||||
def list_notifications(request, limit: int | None = None, offset: int = 0, type: str | None = None, status: str = "all"):
|
||||
if status not in {"all", "unread", "read"}:
|
||||
return JsonResponse({"error": "Invalid status filter."}, status=400)
|
||||
user_id = str(request.auth.id)
|
||||
safe_limit = min(_safe_int(limit, settings.NOTIFICATION_DEFAULT_PAGE_SIZE), settings.NOTIFICATION_MAX_PAGE_SIZE)
|
||||
notifications, total_count = RedisNotificationStore.list(
|
||||
@@ -62,11 +65,12 @@ def list_notifications(request, limit: int | None = None, offset: int = 0, type:
|
||||
limit=safe_limit,
|
||||
offset=_safe_int(offset, 0),
|
||||
type_filter=type,
|
||||
read_filter=status,
|
||||
)
|
||||
return {
|
||||
"count": total_count,
|
||||
"unread_count": RedisNotificationStore.unread_count(user_id),
|
||||
"notifications": notifications,
|
||||
"notifications": [RedisNotificationStore.summary(item) for item in notifications],
|
||||
}
|
||||
|
||||
|
||||
@@ -78,18 +82,6 @@ def mark_notification_seen(request, data: NotificationSeenIn):
|
||||
return 200, {"marked_read": True, **payload}
|
||||
|
||||
|
||||
@notifications_router.delete("/{notif_id}", response={200: NotificationDeleteResponseSchema, 404: ErrorSchema}, auth=jwt_auth)
|
||||
def delete_notification(request, notif_id: str):
|
||||
deleted = RedisNotificationStore.delete(str(request.auth.id), notif_id)
|
||||
if not deleted:
|
||||
return 404, {"error": "Notification not found."}
|
||||
return 200, {
|
||||
"deleted": True,
|
||||
"notification_id": notif_id,
|
||||
"unread_count": RedisNotificationStore.unread_count(str(request.auth.id)),
|
||||
}
|
||||
|
||||
|
||||
@notifications_router.post("/mark-all-read", response=NotificationMarkAllReadResponseSchema, auth=jwt_auth)
|
||||
def mark_all_notifications_read(request, type: str | None = None):
|
||||
updated = RedisNotificationStore.mark_all_seen(str(request.auth.id), type_filter=type)
|
||||
@@ -106,6 +98,26 @@ def get_notification_stream_token(request):
|
||||
}
|
||||
|
||||
|
||||
@notifications_router.get("/{notif_id}", response={200: NotificationDetailSchema, 404: ErrorSchema}, auth=jwt_auth)
|
||||
def get_notification(request, notif_id: str):
|
||||
notification = RedisNotificationStore.get(str(request.auth.id), notif_id)
|
||||
if notification is None:
|
||||
return 404, {"error": "Notification not found."}
|
||||
return 200, {**notification, "has_body": bool(notification.get("body_markdown"))}
|
||||
|
||||
|
||||
@notifications_router.delete("/{notif_id}", response={200: NotificationDeleteResponseSchema, 404: ErrorSchema}, auth=jwt_auth)
|
||||
def delete_notification(request, notif_id: str):
|
||||
deleted = RedisNotificationStore.delete(str(request.auth.id), notif_id)
|
||||
if not deleted:
|
||||
return 404, {"error": "Notification not found."}
|
||||
return 200, {
|
||||
"deleted": True,
|
||||
"notification_id": notif_id,
|
||||
"unread_count": RedisNotificationStore.unread_count(str(request.auth.id)),
|
||||
}
|
||||
|
||||
|
||||
class NotificationStreamView(View):
|
||||
def _build_stream(self, user_id: str) -> Iterator[str]:
|
||||
pubsub = RedisNotificationStore.get_pubsub()
|
||||
@@ -118,7 +130,7 @@ class NotificationStreamView(View):
|
||||
)
|
||||
unread_count = RedisNotificationStore.unread_count(user_id)
|
||||
yield f"retry: {settings.NOTIFICATION_SSE_RETRY_MS}\n\n"
|
||||
yield _format_sse_event("connected", {"notifications": initial_notifications, "unread_count": unread_count})
|
||||
yield _format_sse_event("connected", {"notifications": [RedisNotificationStore.summary(item) for item in initial_notifications], "unread_count": unread_count})
|
||||
yield _format_sse_event("unread_count", {"unread_count": unread_count})
|
||||
|
||||
pubsub.subscribe(channel)
|
||||
|
||||
Reference in New Issue
Block a user