Implement notifications

This commit is contained in:
Keannu Bernasol 2025-01-21 13:57:31 +08:00
parent 63f3bd0eab
commit 298501b973
16 changed files with 293 additions and 1 deletions

View file

@ -15,6 +15,7 @@ urlpatterns = [
path("requests/", include("document_requests.urls")), path("requests/", include("document_requests.urls")),
path("authorization_requests/", include("authorization_requests.urls")), path("authorization_requests/", include("authorization_requests.urls")),
path("questionnaires/", include("questionnaires.urls")), path("questionnaires/", include("questionnaires.urls")),
path("notifications/", include("notifications.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("schema/", SpectacularAPIView.as_view(), name="schema"), path("schema/", SpectacularAPIView.as_view(), name="schema"),
path( path(

View file

@ -2,6 +2,7 @@ from rest_framework import serializers
from accounts.models import CustomUser from accounts.models import CustomUser
from emails.templates import RequestUpdateEmail from emails.templates import RequestUpdateEmail
from .models import AuthorizationRequest, AuthorizationRequestUnit from .models import AuthorizationRequest, AuthorizationRequestUnit
from notifications.models import Notification
class AuthorizationRequestUnitCreationSerializer(serializers.ModelSerializer): class AuthorizationRequestUnitCreationSerializer(serializers.ModelSerializer):
@ -61,6 +62,11 @@ class AuthorizationRequestCreationSerializer(serializers.ModelSerializer):
AUTHORIZATION_REQUEST.documents.set(AUTHORIZATION_REQUEST_UNITS) AUTHORIZATION_REQUEST.documents.set(AUTHORIZATION_REQUEST_UNITS)
AUTHORIZATION_REQUEST.save() AUTHORIZATION_REQUEST.save()
Notification.objects.create(
type="info",
audience="head",
content=f"A new authorization request ID:{AUTHORIZATION_REQUEST.id} requires your attention")
return AUTHORIZATION_REQUEST return AUTHORIZATION_REQUEST
@ -148,7 +154,14 @@ class AuthorizationRequestUnitUpdateSerializer(serializers.ModelSerializer):
# And send an email notification # And send an email notification
email = RequestUpdateEmail() email = RequestUpdateEmail()
email.context = {"request_status": "approved"} email.context = {"request_status": "approved"}
email.context = {"remarks": "N/A"}
email.send(to=[instance.authorization_request.requester.email]) email.send(to=[instance.authorization_request.requester.email])
Notification.objects.create(
client=instance.authorization_request.requester,
type="info",
audience="client",
content=f"Your authorization request ID:{instance.authorization_request.id} has been approved")
return representation return representation
@ -199,9 +212,19 @@ class AuthorizationRequestUpdateSerializer(serializers.ModelSerializer):
if validated_data["status"] == "denied": if validated_data["status"] == "denied":
email.context = {"request_status": "denied"} email.context = {"request_status": "denied"}
email.context = {"remarks": validated_data["remarks"]} email.context = {"remarks": validated_data["remarks"]}
Notification.objects.create(
client=instance.requester,
type="info",
audience="client",
content=f"Your authorization request ID:{instance.id} has been denied")
else: else:
email.context = {"request_status": "approved"} email.context = {"request_status": "approved"}
email.context = {"remarks": "N/A"} email.context = {"remarks": "N/A"}
Notification.objects.create(
client=instance.requester,
type="info",
audience="client",
content=f"Your authorization request ID:{instance.id} has been approved")
email.send(to=[instance.requester.email]) email.send(to=[instance.requester.email])
except Exception as e: except Exception as e:
# Silence out errors if email sending fails # Silence out errors if email sending fails

View file

@ -20,6 +20,7 @@ from ollama import Client
from pydantic import BaseModel from pydantic import BaseModel
from datetime import date from datetime import date
from typing import Optional from typing import Optional
from notifications.models import Notification
class PDFHandler(FileSystemEventHandler): class PDFHandler(FileSystemEventHandler):
@ -223,6 +224,7 @@ class PDFHandler(FileSystemEventHandler):
# If that fails, just use regular OCR read the title as a dirty fix/fallback # If that fails, just use regular OCR read the title as a dirty fix/fallback
except Exception as e: except Exception as e:
document_subject = "placeholder_document_name"
document_type = "other" document_type = "other"
sent_from = "N/A" sent_from = "N/A"
document_month = "no_month" document_month = "no_month"
@ -232,6 +234,11 @@ class PDFHandler(FileSystemEventHandler):
self.logger.warning( self.logger.warning(
"Ollama OCR offload failed. Using defaults for missing values") "Ollama OCR offload failed. Using defaults for missing values")
Notification.objects.create(
type="warning",
audience="staff",
content=f"Ollama OCR failed for document {document_subject}. Please check if the Ollama API is reachable.")
metadata += text metadata += text
# Open the file for instance creation # Open the file for instance creation
@ -257,9 +264,18 @@ class PDFHandler(FileSystemEventHandler):
document_type}'. sent_from: {sent_from}, document_month: {document_month}, document_year: {document_year}" document_type}'. sent_from: {sent_from}, document_month: {document_month}, document_year: {document_year}"
) )
Notification.objects.create(
type="info",
audience="staff",
content=f"New Document Scanned: {document_subject}.")
else: else:
self.logger.info( self.logger.info(
f"Document '{document_subject}' already exists.") f"Document '{document_subject}' already exists.")
Notification.objects.create(
type="info",
audience="staff",
content=f"Skipping Scanned Document {document_subject}: Already exists.")
os.remove(file_path) os.remove(file_path)
except Exception as e: except Exception as e:

View file

@ -98,6 +98,7 @@ INSTALLED_APPS = [
"document_requests", "document_requests",
"authorization_requests", "authorization_requests",
"questionnaires", "questionnaires",
"notifications",
"django_cleanup.apps.CleanupConfig", "django_cleanup.apps.CleanupConfig",
] ]
@ -267,7 +268,7 @@ AUTH_USER_MODEL = "accounts.CustomUser"
DATA_UPLOAD_MAX_NUMBER_FIELDS = 20480 DATA_UPLOAD_MAX_NUMBER_FIELDS = 20480
GRAPH_MODELS = { GRAPH_MODELS = {
"app_labels": ["accounts", "documents", "document_requests", "questionnaires", "authorization_requests"] "app_labels": ["accounts", "documents", "document_requests", "questionnaires", "authorization_requests", "notifications"]
} }
CORS_ORIGIN_ALLOW_ALL = True CORS_ORIGIN_ALLOW_ALL = True

View file

@ -5,6 +5,7 @@ from questionnaires.models import Questionnaire
from accounts.models import CustomUser from accounts.models import CustomUser
from emails.templates import RequestUpdateEmail from emails.templates import RequestUpdateEmail
from .models import DocumentRequest, DocumentRequestUnit from .models import DocumentRequest, DocumentRequestUnit
from notifications.models import Notification
class DocumentRequestUnitCreationSerializer(serializers.ModelSerializer): class DocumentRequestUnitCreationSerializer(serializers.ModelSerializer):
@ -60,6 +61,11 @@ class DocumentRequestCreationSerializer(serializers.ModelSerializer):
DOCUMENT_REQUEST.documents.set(DOCUMENT_REQUEST_UNITS) DOCUMENT_REQUEST.documents.set(DOCUMENT_REQUEST_UNITS)
DOCUMENT_REQUEST.save() DOCUMENT_REQUEST.save()
Notification.objects.create(
type="info",
audience="head",
content=f"A new document request ID:{DOCUMENT_REQUEST.id} requires your attention")
return DOCUMENT_REQUEST return DOCUMENT_REQUEST
@ -218,9 +224,19 @@ class DocumentRequestUpdateSerializer(serializers.ModelSerializer):
if validated_data["status"] == "denied": if validated_data["status"] == "denied":
email.context = {"request_status": "denied"} email.context = {"request_status": "denied"}
email.context = {"remarks": validated_data["remarks"]} email.context = {"remarks": validated_data["remarks"]}
Notification.objects.create(
client=instance.requester,
type="info",
audience="client",
content=f"Your authorization request ID:{instance.id} has been denied")
else: else:
email.context = {"request_status": "approved"} email.context = {"request_status": "approved"}
email.context = {"remarks": "N/A"} email.context = {"remarks": "N/A"}
Notification.objects.create(
client=instance.requester,
type="info",
audience="client",
content=f"Your authorization request ID:{instance.id} has been approved")
email.send(to=[instance.requester.email]) email.send(to=[instance.requester.email])
except: except:
# Silence out errors if email sending fails # Silence out errors if email sending fails

View file

@ -0,0 +1,16 @@
from django.contrib import admin
from unfold.admin import ModelAdmin
from .models import Notification
from unfold.contrib.filters.admin import RangeDateFilter
# Register your models here.
@admin.register(Notification)
class NotificationAdmin(ModelAdmin):
search_fields = ["id"]
list_display = ["id", "content", "type", "timestamp", "audience", "client"]
list_filter = [
("timestamp", RangeDateFilter),
]

View file

@ -0,0 +1,11 @@
from django.apps import AppConfig
class NotificationsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "notifications"
def ready(self) -> None:
import notifications.signals
return super().ready()

View file

@ -0,0 +1,60 @@
# Generated by Django 5.1.3 on 2025-01-21 04:14
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Notification",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"timestamp",
models.DateTimeField(
default=django.utils.timezone.now, editable=False
),
),
("content", models.TextField(blank=True, max_length=512, null=True)),
(
"audience",
models.CharField(
choices=[
("client", "Client"),
("staff", "Staff"),
("head", "Head"),
],
default="staff",
max_length=16,
),
),
(
"client",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View file

@ -0,0 +1,22 @@
# Generated by Django 5.1.3 on 2025-01-21 04:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("notifications", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="notification",
name="type",
field=models.CharField(
choices=[("info", "Info"), ("warning", "Warning")],
default="info",
max_length=16,
),
),
]

View file

@ -0,0 +1,27 @@
from django.db import models
from django.utils.timezone import now
class Notification(models.Model):
client = models.ForeignKey(
"accounts.CustomUser", on_delete=models.CASCADE, null=True, blank=True)
timestamp = models.DateTimeField(default=now, editable=False)
content = models.TextField(max_length=512, blank=True, null=True)
AUDIENCE_CHOICES = (
("client", "Client"),
("staff", "Staff"),
("head", "Head")
)
TYPE_CHOICES = (
("info", "Info"),
("warning", "Warning"),
)
type = models.CharField(
max_length=16, choices=TYPE_CHOICES, default="info")
audience = models.CharField(
max_length=16, choices=AUDIENCE_CHOICES, default="staff")

View file

@ -0,0 +1,23 @@
from rest_framework import serializers
from accounts.models import CustomUser
from .models import Notification
class NotificationSerializer(serializers.ModelSerializer):
client = serializers.SlugRelatedField(
many=False, slug_field="id", queryset=CustomUser.objects.all(), required=False
)
timestamp = serializers.DateTimeField(
format="%m-%d-%Y %I:%M %p", read_only=True
)
class Meta:
model = Notification
fields = [
"id",
"client",
"timestamp",
"content",
"type",
"audience",
]

View file

@ -0,0 +1,14 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from datetime import timedelta
from notifications.models import Notification
from django.utils import timezone
@receiver(post_save, sender=Notification)
def notification_post_save(sender, instance, **kwargs):
# Calculate the time threshold (15 minutes ago)
threshold = timezone.now() - timedelta(minutes=15)
# Find and delete all notifications older than 15 minutes
Notification.objects.filter(timestamp__lt=threshold).delete()

View file

@ -0,0 +1,10 @@
from django.urls import path
from .views import (
NotificationListView,
NotificationDeleteView
)
urlpatterns = [
path("list/", NotificationListView.as_view()),
path("delete/<int:pk>/", NotificationDeleteView.as_view()),
]

View file

@ -0,0 +1,52 @@
from rest_framework import generics
from django.db.models import Q
from .serializers import NotificationSerializer
from .models import Notification
from rest_framework.permissions import IsAuthenticated
class NotificationListView(generics.ListAPIView):
"""
Used by all users to view notifications. Returns user-specific notifications for clients.
Returns role-wide notifications for staff and head roles
"""
http_method_names = ["get"]
serializer_class = NotificationSerializer
queryset = Notification.objects.all().order_by("-timestamp")
permission_classes = [IsAuthenticated]
def get_queryset(self):
user = self.request.user
if user.role == "client":
queryset = Notification.objects.filter(client=user)
elif user.role == "staff":
queryset = Notification.objects.filter(audience="staff")
elif user.role in ["head", "admin"]:
queryset = Notification.objects.filter(
Q(audience="staff") | Q(audience="head"))
return queryset
class NotificationDeleteView(generics.DestroyAPIView):
"""
Used by all users to dismiss or delete notifications.
"""
http_method_names = ["delete"]
serializer_class = NotificationSerializer
queryset = Notification.objects.all().order_by("-timestamp")
permission_classes = [IsAuthenticated]
def get_queryset(self):
user = self.request.user
if user.role == "client":
return Notification.objects.filter(client=user)
elif user.role == "staff":
return Notification.objects.filter(audience="staff")
else:
# For head or admin roles
return Notification.objects.filter(
Q(audience="head") | Q(audience="staff")
)