nexus-5/core/models/events.py
2026-01-26 11:09:40 -05:00

270 lines
8.5 KiB
Python

from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField
from django_choices_field import TextChoicesField
from core.models.base import BaseModel
from core.models.enums import (
EventTypeChoices,
NotificationChannelChoices,
NotificationStatusChoices,
DeliveryStatusChoices,
RoleChoices
)
class Event(BaseModel):
"""
Event model to track system events that may trigger notifications.
Provides audit trail and basis for notification system.
"""
event_type = TextChoicesField(
choices_enum=EventTypeChoices,
help_text="Type of event that occurred"
)
entity_type = models.CharField(
max_length=100,
help_text="Type of entity (e.g., 'Project', 'Report', 'Invoice')"
)
entity_id = models.UUIDField(
help_text="UUID of the entity that triggered this event"
)
metadata = models.JSONField(
default=dict,
blank=True,
help_text="Additional event metadata (e.g., old_status, new_status, changed_fields)"
)
# Generic foreign key to support both TeamProfile and CustomerProfile
triggered_by_content_type = models.ForeignKey(
ContentType,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='triggered_events'
)
triggered_by_object_id = models.UUIDField(
null=True,
blank=True
)
triggered_by = GenericForeignKey('triggered_by_content_type', 'triggered_by_object_id')
class Meta:
db_table = 'events'
ordering = ['-created_at']
indexes = [
models.Index(fields=['event_type', 'created_at']),
models.Index(fields=['entity_type', 'entity_id']),
models.Index(fields=['created_at']),
]
def __str__(self):
return f"{self.event_type} - {self.entity_type}:{self.entity_id} at {self.created_at}"
class NotificationRule(BaseModel):
"""
Admin-defined rules for generating notifications based on events.
"""
name = models.CharField(
max_length=200,
help_text="Descriptive name for this notification rule"
)
description = models.TextField(
blank=True,
help_text="Description of when and how this rule applies"
)
event_types = ArrayField(
TextChoicesField(choices_enum=EventTypeChoices),
help_text="List of event types that trigger this rule"
)
channels = ArrayField(
TextChoicesField(choices_enum=NotificationChannelChoices),
help_text="Delivery channels for notifications (IN_APP, EMAIL, SMS)"
)
target_roles = ArrayField(
TextChoicesField(choices_enum=RoleChoices),
blank=True,
default=list,
help_text="Roles that should receive notifications (empty = all authenticated users)"
)
target_team_profiles = models.ManyToManyField(
'TeamProfile',
blank=True,
related_name='notification_rules',
help_text="Specific team profiles to notify"
)
target_customer_profiles = models.ManyToManyField(
'CustomerProfile',
blank=True,
related_name='notification_rules',
help_text="Specific customer profiles to notify"
)
is_active = models.BooleanField(
default=True,
help_text="Whether this rule is currently active"
)
template_subject = models.CharField(
max_length=500,
blank=True,
help_text="Template for notification subject (supports variables)"
)
template_body = models.TextField(
blank=True,
help_text="Template for notification body (supports variables)"
)
conditions = models.JSONField(
default=dict,
blank=True,
help_text="Additional conditions for when this rule applies (e.g., {'status': 'COMPLETED'})"
)
class Meta:
db_table = 'notification_rules'
ordering = ['name']
def __str__(self):
return f"{self.name} ({', '.join(self.event_types)})"
class Notification(BaseModel):
"""
Individual notification instance sent to a specific recipient.
"""
event = models.ForeignKey(
Event,
on_delete=models.CASCADE,
related_name='notifications',
help_text="Event that triggered this notification"
)
rule = models.ForeignKey(
NotificationRule,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='notifications',
help_text="Rule that generated this notification"
)
# Generic foreign key to support both TeamProfile and CustomerProfile
recipient_content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
related_name='notifications'
)
recipient_object_id = models.UUIDField()
recipient = GenericForeignKey('recipient_content_type', 'recipient_object_id')
status = TextChoicesField(
choices_enum=NotificationStatusChoices,
default=NotificationStatusChoices.PENDING,
help_text="Current status of the notification"
)
subject = models.CharField(
max_length=500,
help_text="Notification subject line"
)
body = models.TextField(
help_text="Notification body content"
)
action_url = models.URLField(
blank=True,
max_length=500,
help_text="Optional URL for action button (e.g., link to project detail)"
)
read_at = models.DateTimeField(
null=True,
blank=True,
help_text="Timestamp when notification was marked as read"
)
metadata = models.JSONField(
default=dict,
blank=True,
help_text="Additional notification metadata"
)
class Meta:
db_table = 'notifications'
ordering = ['-created_at']
indexes = [
models.Index(fields=['recipient_content_type', 'recipient_object_id', 'status', 'created_at']),
models.Index(fields=['recipient_content_type', 'recipient_object_id', 'read_at']),
models.Index(fields=['event']),
]
def __str__(self):
return f"Notification for {self.recipient} - {self.subject}"
def mark_as_read(self):
"""Mark notification as read"""
if not self.read_at:
from django.utils import timezone
self.read_at = timezone.now()
self.status = NotificationStatusChoices.READ
self.save(update_fields=['read_at', 'status', 'updated_at'])
class NotificationDelivery(BaseModel):
"""
Track delivery attempts for a notification via specific channels.
"""
notification = models.ForeignKey(
Notification,
on_delete=models.CASCADE,
related_name='deliveries',
help_text="Notification being delivered"
)
channel = TextChoicesField(
choices_enum=NotificationChannelChoices,
help_text="Delivery channel (IN_APP, EMAIL, SMS)"
)
status = TextChoicesField(
choices_enum=DeliveryStatusChoices,
default=DeliveryStatusChoices.PENDING,
help_text="Current delivery status"
)
attempts = models.PositiveIntegerField(
default=0,
help_text="Number of delivery attempts"
)
last_attempt_at = models.DateTimeField(
null=True,
blank=True,
help_text="Timestamp of last delivery attempt"
)
sent_at = models.DateTimeField(
null=True,
blank=True,
help_text="Timestamp when successfully sent"
)
delivered_at = models.DateTimeField(
null=True,
blank=True,
help_text="Timestamp when delivery was confirmed (if supported by channel)"
)
error_message = models.TextField(
blank=True,
help_text="Error message from failed delivery attempts"
)
external_id = models.CharField(
max_length=200,
blank=True,
help_text="External service ID (e.g., Twilio message SID, email message ID)"
)
metadata = models.JSONField(
default=dict,
blank=True,
help_text="Additional delivery metadata"
)
class Meta:
db_table = 'notification_deliveries'
ordering = ['-created_at']
indexes = [
models.Index(fields=['notification', 'channel']),
models.Index(fields=['status', 'last_attempt_at']),
models.Index(fields=['channel', 'status']),
]
unique_together = [['notification', 'channel']]
def __str__(self):
return f"{self.channel} delivery for notification {self.notification_id} - {self.status}"