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("authorization_requests/", include("authorization_requests.urls")),
path("questionnaires/", include("questionnaires.urls")),
path("notifications/", include("notifications.urls")),
path("admin/", admin.site.urls),
path("schema/", SpectacularAPIView.as_view(), name="schema"),
path(

View file

@ -2,6 +2,7 @@ from rest_framework import serializers
from accounts.models import CustomUser
from emails.templates import RequestUpdateEmail
from .models import AuthorizationRequest, AuthorizationRequestUnit
from notifications.models import Notification
class AuthorizationRequestUnitCreationSerializer(serializers.ModelSerializer):
@ -61,6 +62,11 @@ class AuthorizationRequestCreationSerializer(serializers.ModelSerializer):
AUTHORIZATION_REQUEST.documents.set(AUTHORIZATION_REQUEST_UNITS)
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
@ -148,7 +154,14 @@ class AuthorizationRequestUnitUpdateSerializer(serializers.ModelSerializer):
# And send an email notification
email = RequestUpdateEmail()
email.context = {"request_status": "approved"}
email.context = {"remarks": "N/A"}
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
@ -199,9 +212,19 @@ class AuthorizationRequestUpdateSerializer(serializers.ModelSerializer):
if validated_data["status"] == "denied":
email.context = {"request_status": "denied"}
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:
email.context = {"request_status": "approved"}
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])
except Exception as e:
# Silence out errors if email sending fails

View file

@ -20,6 +20,7 @@ from ollama import Client
from pydantic import BaseModel
from datetime import date
from typing import Optional
from notifications.models import Notification
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
except Exception as e:
document_subject = "placeholder_document_name"
document_type = "other"
sent_from = "N/A"
document_month = "no_month"
@ -232,6 +234,11 @@ class PDFHandler(FileSystemEventHandler):
self.logger.warning(
"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
# 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}"
)
Notification.objects.create(
type="info",
audience="staff",
content=f"New Document Scanned: {document_subject}.")
else:
self.logger.info(
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)
except Exception as e:

View file

@ -98,6 +98,7 @@ INSTALLED_APPS = [
"document_requests",
"authorization_requests",
"questionnaires",
"notifications",
"django_cleanup.apps.CleanupConfig",
]
@ -267,7 +268,7 @@ AUTH_USER_MODEL = "accounts.CustomUser"
DATA_UPLOAD_MAX_NUMBER_FIELDS = 20480
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

View file

@ -5,6 +5,7 @@ from questionnaires.models import Questionnaire
from accounts.models import CustomUser
from emails.templates import RequestUpdateEmail
from .models import DocumentRequest, DocumentRequestUnit
from notifications.models import Notification
class DocumentRequestUnitCreationSerializer(serializers.ModelSerializer):
@ -60,6 +61,11 @@ class DocumentRequestCreationSerializer(serializers.ModelSerializer):
DOCUMENT_REQUEST.documents.set(DOCUMENT_REQUEST_UNITS)
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
@ -218,9 +224,19 @@ class DocumentRequestUpdateSerializer(serializers.ModelSerializer):
if validated_data["status"] == "denied":
email.context = {"request_status": "denied"}
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:
email.context = {"request_status": "approved"}
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])
except:
# 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")
)