feat(notification): added notification model and admin

This commit is contained in:
sankalp committed 2025-12-11 21:02:16 +05:45
1 parent adfd2bf63f
commit 466d4b868a
13 files changed
+419 -1

No files matched your search

+6 -1
View File
@@ -1,6 +1,6 @@
# .
A Django project created with [djinitx/djinit/dj](https://github.com/S4NKALP/djinit).
A Django project folder structure generated with [djinitx/djinit/dj](https://github.com/S4NKALP/djinit).
## Features
@@ -16,6 +16,7 @@ A Django project created with [djinitx/djinit/dj](https://github.com/S4NKALP/dji
1. Set environment variables in `.env` file
2. Run setup (installs dependencies, runs migrations, creates superuser):
```bash
just setup
```
@@ -28,6 +29,7 @@ A Django project created with [djinitx/djinit/dj](https://github.com/S4NKALP/dji
### Traditional Method
1. Install dependencies:
```bash
pip install -r requirements.txt
```
@@ -35,11 +37,13 @@ A Django project created with [djinitx/djinit/dj](https://github.com/S4NKALP/dji
2. Set environment variables in `.env` file
3. Run migrations:
```bash
python manage.py migrate
```
4. Create superuser:
```bash
python manage.py createsuperuser
```
@@ -102,5 +106,6 @@ See the README files in `api/` and `models/` directories for more details.
## API Documentation
When running in development mode, API documentation is available at:
- Swagger UI: http://localhost:8000/docs/
- Schema: http://localhost:8000/schema/
+48
View File
@@ -0,0 +1,48 @@
# Notification System Workflow
This document outlines how the notification system works in DevSuite.
## Overview
The system automatically checks for upcoming deadlines, due dates, and other time-sensitive events and sends email notifications to Clients and Admins.
## Triggers & Recipients
The system checks for the following events daily:
| Module | Event | Condition | Recipient |
| :--- | :--- | :--- | :--- |
| **Projects** | Project Deadline | Deadline is **tomorrow** | Client & Admin |
| **Projects** | Milestone Due | Due date is **tomorrow** | Client & Admin |
| **Projects** | Task Due | Due date is **tomorrow** | Admin |
| **Services** | Service Expiry | Expiry date is **tomorrow** | Client |
| **Finance** | Invoice Due | Due date is **tomorrow** | Client |
| **Productivity** | Time Entry Logged | Completed in last **24 hours** | Admin |
## How It Works
1. **Command**: The logic is encapsulated in a Django management command:
```bash
python manage.py send_notifications
```
2. **Logic**:
- It queries the database for objects matching the conditions above.
- It generates an email with relevant details.
- It records the notification in the `Notification` model to prevent duplicates (for the same day).
- It sends the email using the configured `EMAIL_BACKEND`.
## Automation
To ensure notifications are sent daily, a cron job is used.
**Script**: `scripts/run_notifications.sh`
**Setup**:
Add this to your crontab (`crontab -e`) to run daily at 9:00 AM:
```bash
0 9 * * * /path/to/devsuite/scripts/run_notifications.sh >> /path/to/devsuite/logs/cron.log 2>&1
```
## Configuration
- **Admin Emails**: Configured in `src/settings/base.py` under `ADMINS`.
- **Email Backend**: Configured in `src/settings/base.py` (default is console for dev, SMTP for prod).
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
# Script to run daily notifications
# Add this to crontab: 0 9 * * * /path/to/devsuite/scripts/run_notifications.sh
cd "$(dirname "$0")/.." || exit
source .venv/bin/activate
python manage.py send_notifications
+1
View File
@@ -7,4 +7,5 @@ from src.admin import ( # noqa: F401
productivity,
projects,
services,
notifications,
)
+5
View File
@@ -86,6 +86,11 @@ class DevSuiteAdminSite(AdminSite):
"display_name": "Groups",
"order": 2,
},
"Notification": {
"group": "System & Users",
"display_name": "Notifications",
"order": 3,
},
}
# Group models according to the model_map
+11
View File
@@ -0,0 +1,11 @@
from django.contrib import admin
from src.admin.base import admin_site
from src.models.notifications import Notification
@admin.register(Notification, site=admin_site)
class NotificationAdmin(admin.ModelAdmin):
list_display = ("recipient", "subject", "status", "sent_at", "created_at")
list_filter = ("status", "created_at", "sent_at")
search_fields = ("recipient", "subject", "message")
readonly_fields = ("created_at", "updated_at", "sent_at")
date_hierarchy = "created_at"
@@ -0,0 +1,10 @@
from django.core.management.base import BaseCommand
from src.models.notifications import check_and_send_notifications
class Command(BaseCommand):
help = 'Checks for upcoming deadlines and sends notifications.'
def handle(self, *args, **options):
self.stdout.write("Checking for notifications...")
check_and_send_notifications()
self.stdout.write(self.style.SUCCESS("Successfully sent notifications."))
+60
View File
@@ -0,0 +1,60 @@
# Generated by Django 6.0 on 2025-12-11 14:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("src", "0002_alter_client_phone"),
]
operations = [
migrations.CreateModel(
name="Notification",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("recipient", models.EmailField(max_length=254)),
("subject", models.CharField(max_length=255)),
("message", models.TextField()),
(
"status",
models.CharField(
choices=[
("PENDING", "Pending"),
("SENT", "Sent"),
("FAILED", "Failed"),
],
default="PENDING",
max_length=20,
),
),
("sent_at", models.DateTimeField(blank=True, null=True)),
("object_id", models.PositiveIntegerField(blank=True, null=True)),
(
"content_type",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
],
options={
"ordering": ["-created_at"],
},
),
]
+2
View File
@@ -1,3 +1,5 @@
"""generated with djinit"""
# Create your models here.
from .notifications import Notification
+210
View File
@@ -0,0 +1,210 @@
from django.db import models
from django.core.mail import send_mail
from django.conf import settings
from django.utils import timezone
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth import get_user_model
from src.models.base import TimeStampedModel
from src.models.projects import Project, Milestone, Task
from src.models.services import Service
from src.models.finance import Invoice
from src.models.productivity import TimeEntry
User = get_user_model()
class Notification(TimeStampedModel):
STATUS_CHOICES = [
("PENDING", "Pending"),
("SENT", "Sent"),
("FAILED", "Failed"),
]
recipient = models.EmailField()
subject = models.CharField(max_length=255)
message = models.TextField()
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="PENDING")
sent_at = models.DateTimeField(null=True, blank=True)
# Generic relation to link to the source object (Project, Invoice, etc.)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True, blank=True)
object_id = models.PositiveIntegerField(null=True, blank=True)
content_object = GenericForeignKey('content_type', 'object_id')
class Meta:
ordering = ["-created_at"]
def __str__(self):
return f"Notification to {self.recipient}: {self.subject}"
def send(self):
try:
send_mail(
self.subject,
self.message,
settings.DEFAULT_FROM_EMAIL,
[self.recipient],
fail_silently=False,
)
self.status = "SENT"
self.sent_at = timezone.now()
except Exception as e:
self.status = "FAILED"
# In a real app, we might want to log the error 'e'
print(f"Failed to send email: {e}")
self.save()
def get_admin_emails():
"""Return a list of admin emails."""
# First try settings.ADMINS
admins = getattr(settings, "ADMINS", [])
if admins:
return [email for name, email in admins]
# Fallback to superusers
return list(User.objects.filter(is_superuser=True).values_list('email', flat=True))
def create_and_send(recipient, subject, message, obj=None):
"""Helper to create and send a notification."""
# Avoid duplicate notifications for the same event on the same day
if obj:
ct = ContentType.objects.get_for_model(obj)
already_sent = Notification.objects.filter(
recipient=recipient,
subject=subject,
content_type=ct,
object_id=obj.id,
created_at__date=timezone.localdate()
).exists()
if already_sent:
return
notification = Notification(
recipient=recipient,
subject=subject,
message=message,
content_type=ContentType.objects.get_for_model(obj) if obj else None,
object_id=obj.id if obj else None,
)
notification.save()
notification.send()
def check_and_send_notifications():
"""
Checks for upcoming deadlines/due dates and sends notifications.
Should be called periodically (e.g., via a cron job or Celery beat).
"""
today = timezone.localdate()
tomorrow = today + timezone.timedelta(days=1)
admin_emails = get_admin_emails()
if not admin_emails:
print("Warning: No admin emails found.")
# --- Projects (Deadline) ---
# Notify Client & Admin if deadline is tomorrow
projects_due = Project.objects.filter(deadline=tomorrow)
for project in projects_due:
# Notify Client
if project.client.email:
create_and_send(
recipient=project.client.email,
subject=f"Project Deadline Reminder: {project.name}",
message=f"Dear {project.client.name},\n\nThe project '{project.name}' is due on {project.deadline}.\n\nRegards,\nDevSuite",
obj=project
)
# Notify Admin
for email in admin_emails:
create_and_send(
recipient=email,
subject=f"Admin Alert: Project Deadline - {project.name}",
message=f"Project '{project.name}' for client {project.client.name} is due on {project.deadline}.",
obj=project
)
# --- Milestones (Due Date) ---
# Notify Client & Admin if due_date is tomorrow
milestones_due = Milestone.objects.filter(due_date=tomorrow, is_completed=False)
for milestone in milestones_due:
project = milestone.project
# Notify Client
if project.client.email:
create_and_send(
recipient=project.client.email,
subject=f"Milestone Due: {milestone.title}",
message=f"Dear {project.client.name},\n\nThe milestone '{milestone.title}' for project '{project.name}' is due on {milestone.due_date}.\n\nRegards,\nDevSuite",
obj=milestone
)
# Notify Admin
for email in admin_emails:
create_and_send(
recipient=email,
subject=f"Admin Alert: Milestone Due - {milestone.title}",
message=f"Milestone '{milestone.title}' (Project: {project.name}) is due on {milestone.due_date}.",
obj=milestone
)
# --- Tasks (Due Date) ---
# Notify Admin only
tasks_due = Task.objects.filter(due_date=tomorrow).exclude(status="DONE")
for task in tasks_due:
for email in admin_emails:
create_and_send(
recipient=email,
subject=f"Task Due: {task.title}",
message=f"Task '{task.title}' (Project: {task.project.name}) is due on {task.due_date}.",
obj=task
)
# --- Services (Expiry Date) ---
# Notify Client
services_expiring = Service.objects.filter(expiry_date=tomorrow)
for service in services_expiring:
if service.client.email:
create_and_send(
recipient=service.client.email,
subject=f"Service Expiry Warning: {service.name}",
message=f"Dear {service.client.name},\n\nYour service '{service.name}' ({service.get_service_type_display()}) expires on {service.expiry_date}.\nPlease renew it soon.\n\nRegards,\nDevSuite",
obj=service
)
# --- Invoices (Due Date) ---
# Notify Client
invoices_due = Invoice.objects.filter(due_date=tomorrow, status__in=["SENT", "OVERDUE"])
for invoice in invoices_due:
if invoice.client.email:
create_and_send(
recipient=invoice.client.email,
subject=f"Invoice Due: {invoice.invoice_number}",
message=f"Dear {invoice.client.name},\n\nInvoice {invoice.invoice_number} for {invoice.amount} is due on {invoice.due_date}.\nPlease make payment.\n\nRegards,\nDevSuite",
obj=invoice
)
# --- Productivity / TimeEntries ---
# Notify Admin for recently completed time entries (last 24h)
recent_entries = TimeEntry.objects.filter(
end_time__gte=timezone.now() - timezone.timedelta(hours=24),
end_time__lte=timezone.now()
)
for entry in recent_entries:
# Check if we already sent a notification for this entry
if Notification.objects.filter(
content_type=ContentType.objects.get_for_model(entry),
object_id=entry.id
).exists():
continue
for email in admin_emails:
create_and_send(
recipient=email,
subject=f"Time Entry Logged: {entry.description}",
message=f"User logged time for '{entry.description}'.\nDuration: {entry.duration}\nEnd Time: {entry.end_time}",
obj=entry
)
+3
View File
@@ -18,6 +18,9 @@ BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Application definition
ADMINS = [("Admin", env("ADMIN_EMAIL", default="admin@example.com"))]
MANAGERS = ADMINS
THIRD_PARTY_APPS = [
"django_components",
"django_htmx",
View File
Whitespace-only changes.
+56
View File
@@ -0,0 +1,56 @@
from django.test import TestCase
from django.utils import timezone
from django.core import mail
from django.contrib.auth import get_user_model
from src.models.projects import Project
from src.models.clients import Client
from src.models.notifications import check_and_send_notifications, Notification
User = get_user_model()
class NotificationTests(TestCase):
def test_send_deadline_notifications(self):
# Setup
admin = User.objects.create_superuser('admin', 'admin@example.com', 'password')
client = Client.objects.create(name="Test Client", email="client@example.com")
# Project due tomorrow
tomorrow = timezone.localdate() + timezone.timedelta(days=1)
project = Project.objects.create(
name="Test Project",
client=client,
deadline=tomorrow
)
# Run notification check
check_and_send_notifications()
# Verify emails sent
# Expecting 2 emails: 1 to client, 1 to admin
self.assertEqual(len(mail.outbox), 2)
# Verify Notification objects created
self.assertEqual(Notification.objects.count(), 2)
client_notif = Notification.objects.get(recipient="client@example.com")
self.assertIn("Project Deadline Reminder", client_notif.subject)
admin_notif = Notification.objects.get(recipient="admin@example.com")
self.assertIn("Admin Alert", admin_notif.subject)
def test_no_notifications_if_not_due(self):
# Setup
client = Client.objects.create(name="Test Client", email="client@example.com")
# Project due day after tomorrow
day_after = timezone.localdate() + timezone.timedelta(days=2)
Project.objects.create(
name="Future Project",
client=client,
deadline=day_after
)
check_and_send_notifications()
self.assertEqual(len(mail.outbox), 0)
self.assertEqual(Notification.objects.count(), 0)