""" 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, )