mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2026-02-05 11:25:19 -05:00
feat: Implement a new notifications application, add admin API views for dashboard metrics, introduce scheduled tasks, and update API routing and project configurations.
This commit is contained in:
617
backend/apps/notifications/views.py
Normal file
617
backend/apps/notifications/views.py
Normal file
@@ -0,0 +1,617 @@
|
||||
"""
|
||||
Notification views.
|
||||
|
||||
Provides REST API endpoints for:
|
||||
- Subscriber management (legacy compatibility)
|
||||
- Preference updates
|
||||
- Notification triggering
|
||||
- Moderator notifications
|
||||
- System announcements
|
||||
- User notification list and management
|
||||
|
||||
Note: Now using django-notifications-hq for native Django notifications.
|
||||
The novu_service import provides backward compatibility.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAdminUser, IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.core.utils import capture_and_log
|
||||
|
||||
from .models import NotificationLog, NotificationPreference, Subscriber, SystemAnnouncement
|
||||
from .serializers import (
|
||||
CreateAnnouncementSerializer,
|
||||
CreateSubscriberSerializer,
|
||||
ModeratorReportNotificationSerializer,
|
||||
ModeratorSubmissionNotificationSerializer,
|
||||
NotificationPreferenceSerializer,
|
||||
SystemAnnouncementSerializer,
|
||||
TriggerNotificationSerializer,
|
||||
UpdatePreferencesSerializer,
|
||||
UpdateSubscriberSerializer,
|
||||
)
|
||||
from .services import novu_service, notification_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class CreateSubscriberView(APIView):
|
||||
"""
|
||||
POST /notifications/subscribers/
|
||||
Create or update a Novu subscriber.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
serializer = CreateSubscriberSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
data = serializer.validated_data
|
||||
subscriber_id = data["subscriber_id"]
|
||||
|
||||
try:
|
||||
# Update or create local subscriber record
|
||||
subscriber, created = Subscriber.objects.update_or_create(
|
||||
user=request.user,
|
||||
defaults={
|
||||
"novu_subscriber_id": subscriber_id,
|
||||
"first_name": data.get("first_name", ""),
|
||||
"last_name": data.get("last_name", ""),
|
||||
"email": data.get("email") or request.user.email,
|
||||
"phone": data.get("phone", ""),
|
||||
"avatar": data.get("avatar", ""),
|
||||
"locale": data.get("locale", "en"),
|
||||
"data": data.get("data", {}),
|
||||
},
|
||||
)
|
||||
|
||||
# Sync to Novu if configured
|
||||
if novu_service.is_configured:
|
||||
novu_service.create_subscriber(
|
||||
subscriber_id=subscriber_id,
|
||||
email=subscriber.email,
|
||||
first_name=subscriber.first_name,
|
||||
last_name=subscriber.last_name,
|
||||
phone=subscriber.phone,
|
||||
avatar=subscriber.avatar,
|
||||
locale=subscriber.locale,
|
||||
data=subscriber.data,
|
||||
)
|
||||
|
||||
return Response(
|
||||
{"subscriberId": subscriber_id, "created": created},
|
||||
status=status.HTTP_201_CREATED if created else status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Create notification subscriber", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class UpdateSubscriberView(APIView):
|
||||
"""
|
||||
POST /notifications/subscribers/update/
|
||||
Update a Novu subscriber.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
serializer = UpdateSubscriberSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
data = serializer.validated_data
|
||||
subscriber_id = data["subscriber_id"]
|
||||
|
||||
try:
|
||||
# Update local record
|
||||
subscriber = Subscriber.objects.filter(user=request.user).first()
|
||||
if not subscriber:
|
||||
return Response(
|
||||
{"detail": "Subscriber not found"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
# Update fields
|
||||
for field in ["first_name", "last_name", "email", "phone", "avatar", "locale", "data"]:
|
||||
if field in data:
|
||||
setattr(subscriber, field, data[field])
|
||||
subscriber.save()
|
||||
|
||||
# Sync to Novu
|
||||
if novu_service.is_configured:
|
||||
update_fields = {k: v for k, v in data.items() if k != "subscriber_id"}
|
||||
novu_service.update_subscriber(subscriber_id, **update_fields)
|
||||
|
||||
return Response({"success": True})
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Update notification subscriber", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class UpdatePreferencesView(APIView):
|
||||
"""
|
||||
POST /notifications/preferences/
|
||||
Update notification preferences.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
serializer = UpdatePreferencesSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
data = serializer.validated_data
|
||||
preferences = data["preferences"]
|
||||
|
||||
try:
|
||||
# Update local preferences
|
||||
pref, created = NotificationPreference.objects.update_or_create(
|
||||
user=request.user,
|
||||
defaults={
|
||||
"channel_preferences": preferences.get("channelPreferences", {}),
|
||||
"workflow_preferences": preferences.get("workflowPreferences", {}),
|
||||
"frequency_settings": preferences.get("frequencySettings", {}),
|
||||
},
|
||||
)
|
||||
|
||||
# Sync to Novu
|
||||
if novu_service.is_configured:
|
||||
subscriber = Subscriber.objects.filter(user=request.user).first()
|
||||
if subscriber:
|
||||
novu_service.update_preferences(subscriber.novu_subscriber_id, preferences)
|
||||
|
||||
return Response({"success": True})
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Update notification preferences", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
def get(self, request):
|
||||
"""Get current user's notification preferences."""
|
||||
try:
|
||||
pref = NotificationPreference.objects.filter(user=request.user).first()
|
||||
if not pref:
|
||||
return Response(
|
||||
{
|
||||
"channelPreferences": {},
|
||||
"workflowPreferences": {},
|
||||
"frequencySettings": {},
|
||||
}
|
||||
)
|
||||
return Response(NotificationPreferenceSerializer(pref).data)
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Get notification preferences", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class TriggerNotificationView(APIView):
|
||||
"""
|
||||
POST /notifications/trigger/
|
||||
Trigger a notification workflow.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
serializer = TriggerNotificationSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
data = serializer.validated_data
|
||||
|
||||
try:
|
||||
# Log the notification
|
||||
log = NotificationLog.objects.create(
|
||||
user=request.user,
|
||||
workflow_id=data["workflow_id"],
|
||||
notification_type="trigger",
|
||||
channel="all",
|
||||
payload=data.get("payload", {}),
|
||||
)
|
||||
|
||||
# Trigger via Novu
|
||||
if novu_service.is_configured:
|
||||
result = novu_service.trigger_notification(
|
||||
workflow_id=data["workflow_id"],
|
||||
subscriber_id=data["subscriber_id"],
|
||||
payload=data.get("payload"),
|
||||
overrides=data.get("overrides"),
|
||||
)
|
||||
log.novu_transaction_id = result.get("transactionId", "")
|
||||
log.status = NotificationLog.Status.SENT
|
||||
else:
|
||||
log.status = NotificationLog.Status.SENT # Mock success
|
||||
log.save()
|
||||
|
||||
return Response({"success": True, "transactionId": log.novu_transaction_id})
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Trigger notification", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class NotifyModeratorsSubmissionView(APIView):
|
||||
"""
|
||||
POST /notifications/moderators/submission/
|
||||
Notify moderators about a new submission.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
serializer = ModeratorSubmissionNotificationSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
data = serializer.validated_data
|
||||
|
||||
try:
|
||||
# Log the notification
|
||||
NotificationLog.objects.create(
|
||||
user=request.user,
|
||||
workflow_id="moderator-submission-notification",
|
||||
notification_type="moderator_submission",
|
||||
channel="in_app",
|
||||
payload=data,
|
||||
status=NotificationLog.Status.SENT,
|
||||
)
|
||||
|
||||
# Trigger to moderator topic
|
||||
if novu_service.is_configured:
|
||||
novu_service.trigger_topic_notification(
|
||||
workflow_id="moderator-submission-notification",
|
||||
topic_key="moderators",
|
||||
payload=data,
|
||||
)
|
||||
|
||||
return Response({"success": True})
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Notify moderators (submission)", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class NotifyModeratorsReportView(APIView):
|
||||
"""
|
||||
POST /notifications/moderators/report/
|
||||
Notify moderators about a new report.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
serializer = ModeratorReportNotificationSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
data = serializer.validated_data
|
||||
|
||||
try:
|
||||
# Log the notification
|
||||
NotificationLog.objects.create(
|
||||
user=request.user,
|
||||
workflow_id="moderator-report-notification",
|
||||
notification_type="moderator_report",
|
||||
channel="in_app",
|
||||
payload=data,
|
||||
status=NotificationLog.Status.SENT,
|
||||
)
|
||||
|
||||
# Trigger to moderator topic
|
||||
if novu_service.is_configured:
|
||||
novu_service.trigger_topic_notification(
|
||||
workflow_id="moderator-report-notification",
|
||||
topic_key="moderators",
|
||||
payload=data,
|
||||
)
|
||||
|
||||
return Response({"success": True})
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Notify moderators (report)", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class NotifyUserSubmissionStatusView(APIView):
|
||||
"""
|
||||
POST /notifications/user/submission-status/
|
||||
Notify a user about their submission status change.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
|
||||
try:
|
||||
subscriber_id = data.get("subscriber_id") or str(request.user.id)
|
||||
|
||||
# Log the notification
|
||||
NotificationLog.objects.create(
|
||||
user=request.user,
|
||||
workflow_id="submission-status-update",
|
||||
notification_type="submission_status",
|
||||
channel="email",
|
||||
payload=data,
|
||||
status=NotificationLog.Status.SENT,
|
||||
)
|
||||
|
||||
# Trigger notification
|
||||
if novu_service.is_configured:
|
||||
novu_service.trigger_notification(
|
||||
workflow_id="submission-status-update",
|
||||
subscriber_id=subscriber_id,
|
||||
payload=data,
|
||||
)
|
||||
|
||||
return Response({"success": True})
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Notify user submission status", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class SystemAnnouncementView(APIView):
|
||||
"""
|
||||
POST /notifications/system/announcement/
|
||||
Send a system-wide announcement (admin only).
|
||||
"""
|
||||
|
||||
permission_classes = [IsAdminUser]
|
||||
|
||||
def post(self, request):
|
||||
serializer = CreateAnnouncementSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
data = serializer.validated_data
|
||||
|
||||
try:
|
||||
# Create announcement record
|
||||
announcement = SystemAnnouncement.objects.create(
|
||||
title=data["title"],
|
||||
message=data["message"],
|
||||
severity=data.get("severity", "info"),
|
||||
action_url=data.get("action_url", ""),
|
||||
created_by=request.user,
|
||||
)
|
||||
|
||||
# Trigger to all users topic
|
||||
if novu_service.is_configured:
|
||||
novu_service.trigger_topic_notification(
|
||||
workflow_id="system-announcement",
|
||||
topic_key="users",
|
||||
payload={
|
||||
"title": announcement.title,
|
||||
"message": announcement.message,
|
||||
"severity": announcement.severity,
|
||||
"actionUrl": announcement.action_url,
|
||||
},
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"success": True,
|
||||
"announcementId": str(announcement.id),
|
||||
},
|
||||
status=status.HTTP_201_CREATED,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "System announcement", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class AdminAlertView(APIView):
|
||||
"""
|
||||
POST /notifications/admin/alert/
|
||||
Send alert to admins.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
|
||||
try:
|
||||
# Log the alert
|
||||
NotificationLog.objects.create(
|
||||
user=request.user,
|
||||
workflow_id="admin-alert",
|
||||
notification_type="admin_alert",
|
||||
channel="email",
|
||||
payload=data,
|
||||
status=NotificationLog.Status.SENT,
|
||||
)
|
||||
|
||||
# Trigger to admin topic
|
||||
if novu_service.is_configured:
|
||||
novu_service.trigger_topic_notification(
|
||||
workflow_id="admin-alert",
|
||||
topic_key="admins",
|
||||
payload=data,
|
||||
)
|
||||
|
||||
return Response({"success": True})
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Admin alert", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class AdminCriticalErrorView(APIView):
|
||||
"""
|
||||
POST /notifications/admin/critical-error/
|
||||
Send critical error alert to admins.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
|
||||
try:
|
||||
# Log the alert
|
||||
NotificationLog.objects.create(
|
||||
user=request.user,
|
||||
workflow_id="admin-critical-error",
|
||||
notification_type="critical_error",
|
||||
channel="email",
|
||||
payload=data,
|
||||
status=NotificationLog.Status.SENT,
|
||||
)
|
||||
|
||||
# Trigger to admin topic with urgent priority
|
||||
if novu_service.is_configured:
|
||||
novu_service.trigger_topic_notification(
|
||||
workflow_id="admin-critical-error",
|
||||
topic_key="admins",
|
||||
payload=data,
|
||||
)
|
||||
|
||||
return Response({"success": True})
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Admin critical error", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Native Notification Views (django-notifications-hq)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class NotificationListView(APIView):
|
||||
"""
|
||||
GET /notifications/
|
||||
Get list of notifications for the current user.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
try:
|
||||
unread_only = request.query_params.get("unread_only", "false").lower() == "true"
|
||||
limit = min(int(request.query_params.get("limit", 50)), 100)
|
||||
|
||||
notifications = notification_service.get_user_notifications(
|
||||
user=request.user,
|
||||
unread_only=unread_only,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
# Serialize notifications
|
||||
notification_list = []
|
||||
for notif in notifications:
|
||||
notification_list.append({
|
||||
"id": notif.id,
|
||||
"actor": str(notif.actor) if notif.actor else None,
|
||||
"verb": notif.verb,
|
||||
"description": notif.description or "",
|
||||
"target": str(notif.target) if notif.target else None,
|
||||
"actionObject": str(notif.action_object) if notif.action_object else None,
|
||||
"level": notif.level,
|
||||
"unread": notif.unread,
|
||||
"data": notif.data or {},
|
||||
"timestamp": notif.timestamp.isoformat(),
|
||||
})
|
||||
|
||||
return Response({
|
||||
"notifications": notification_list,
|
||||
"unreadCount": notification_service.get_unread_count(request.user),
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Get notifications", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class NotificationMarkReadView(APIView):
|
||||
"""
|
||||
POST /notifications/mark-read/
|
||||
Mark notification(s) as read.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
notification_id = request.data.get("notification_id")
|
||||
|
||||
notification_service.mark_as_read(
|
||||
user=request.user,
|
||||
notification_id=notification_id,
|
||||
)
|
||||
|
||||
return Response({
|
||||
"success": True,
|
||||
"unreadCount": notification_service.get_unread_count(request.user),
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Mark notification read", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
class NotificationUnreadCountView(APIView):
|
||||
"""
|
||||
GET /notifications/unread-count/
|
||||
Get count of unread notifications.
|
||||
"""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
try:
|
||||
count = notification_service.get_unread_count(request.user)
|
||||
return Response({"unreadCount": count})
|
||||
except Exception as e:
|
||||
capture_and_log(e, "Get unread count", source="api")
|
||||
return Response(
|
||||
{"detail": str(e)},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
Reference in New Issue
Block a user