Updates
This commit is contained in:
653
ETB-API/sla_oncall/models.py
Normal file
653
ETB-API/sla_oncall/models.py
Normal file
@@ -0,0 +1,653 @@
|
||||
"""
|
||||
SLA & On-Call Enhancement models for Enterprise Incident Management API
|
||||
Implements dynamic SLAs, escalation policies, on-call rotations, and business hours management
|
||||
"""
|
||||
import uuid
|
||||
import json
|
||||
from datetime import datetime, timedelta, time
|
||||
from typing import Dict, Any, Optional, List
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class BusinessHours(models.Model):
|
||||
"""Business hours configuration for different teams and services"""
|
||||
|
||||
WEEKDAYS = [
|
||||
('MONDAY', 'Monday'),
|
||||
('TUESDAY', 'Tuesday'),
|
||||
('WEDNESDAY', 'Wednesday'),
|
||||
('THURSDAY', 'Thursday'),
|
||||
('FRIDAY', 'Friday'),
|
||||
('SATURDAY', 'Saturday'),
|
||||
('SUNDAY', 'Sunday'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=200, unique=True)
|
||||
description = models.TextField()
|
||||
timezone = models.CharField(max_length=50, default='UTC')
|
||||
|
||||
# Business hours configuration
|
||||
weekday_start = models.TimeField(default=time(9, 0)) # 9:00 AM
|
||||
weekday_end = models.TimeField(default=time(17, 0)) # 5:00 PM
|
||||
weekend_start = models.TimeField(default=time(10, 0)) # 10:00 AM
|
||||
weekend_end = models.TimeField(default=time(16, 0)) # 4:00 PM
|
||||
|
||||
# Specific day overrides
|
||||
day_overrides = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Override hours for specific dates (YYYY-MM-DD format)"
|
||||
)
|
||||
|
||||
# Holiday configuration
|
||||
holiday_calendar = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of holidays (YYYY-MM-DD format) when business hours don't apply"
|
||||
)
|
||||
|
||||
# Status
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_default = models.BooleanField(default=False, help_text="Default business hours for the system")
|
||||
|
||||
# Metadata
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = [
|
||||
models.Index(fields=['is_active', 'is_default']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.timezone})"
|
||||
|
||||
def is_business_hours(self, check_time: datetime = None) -> bool:
|
||||
"""Check if given time (or now) is within business hours"""
|
||||
if check_time is None:
|
||||
check_time = timezone.now()
|
||||
|
||||
# Convert to business hours timezone
|
||||
if check_time.tzinfo is None:
|
||||
check_time = timezone.make_aware(check_time)
|
||||
|
||||
local_time = check_time.astimezone(ZoneInfo(self.timezone))
|
||||
current_date = local_time.date()
|
||||
current_time = local_time.time()
|
||||
|
||||
# Check for holiday
|
||||
if current_date.strftime('%Y-%m-%d') in self.holiday_calendar:
|
||||
return False
|
||||
|
||||
# Check for day override
|
||||
date_key = current_date.strftime('%Y-%m-%d')
|
||||
if date_key in self.day_overrides:
|
||||
override = self.day_overrides[date_key]
|
||||
if override.get('is_holiday', False):
|
||||
return False
|
||||
start_time = time.fromisoformat(override.get('start_time', '09:00'))
|
||||
end_time = time.fromisoformat(override.get('end_time', '17:00'))
|
||||
return start_time <= current_time <= end_time
|
||||
|
||||
# Regular business hours
|
||||
is_weekend = current_date.weekday() >= 5 # Saturday = 5, Sunday = 6
|
||||
|
||||
if is_weekend:
|
||||
return self.weekend_start <= current_time <= self.weekend_end
|
||||
else:
|
||||
return self.weekday_start <= current_time <= self.weekday_end
|
||||
|
||||
|
||||
class SLADefinition(models.Model):
|
||||
"""Dynamic SLA definitions based on incident type, severity, and business context"""
|
||||
|
||||
SLA_TYPES = [
|
||||
('RESPONSE_TIME', 'Response Time'),
|
||||
('RESOLUTION_TIME', 'Resolution Time'),
|
||||
('ACKNOWLEDGMENT_TIME', 'Acknowledgment Time'),
|
||||
('FIRST_RESPONSE', 'First Response Time'),
|
||||
]
|
||||
|
||||
SEVERITY_CHOICES = [
|
||||
('ALL', 'All Severities'),
|
||||
('LOW', 'Low'),
|
||||
('MEDIUM', 'Medium'),
|
||||
('HIGH', 'High'),
|
||||
('CRITICAL', 'Critical'),
|
||||
('EMERGENCY', 'Emergency'),
|
||||
]
|
||||
|
||||
PRIORITY_CHOICES = [
|
||||
('ALL', 'All Priorities'),
|
||||
('P1', 'P1 - Critical'),
|
||||
('P2', 'P2 - High'),
|
||||
('P3', 'P3 - Medium'),
|
||||
('P4', 'P4 - Low'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=200)
|
||||
description = models.TextField()
|
||||
sla_type = models.CharField(max_length=20, choices=SLA_TYPES)
|
||||
|
||||
# Targeting criteria
|
||||
incident_categories = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of incident categories this SLA applies to"
|
||||
)
|
||||
incident_severities = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of incident severities this SLA applies to"
|
||||
)
|
||||
incident_priorities = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of incident priorities this SLA applies to"
|
||||
)
|
||||
|
||||
# SLA targets
|
||||
target_duration_minutes = models.PositiveIntegerField(
|
||||
help_text="SLA target in minutes"
|
||||
)
|
||||
business_hours_only = models.BooleanField(
|
||||
default=False,
|
||||
help_text="Whether SLA only applies during business hours"
|
||||
)
|
||||
business_hours = models.ForeignKey(
|
||||
BusinessHours,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Business hours configuration for this SLA"
|
||||
)
|
||||
|
||||
# Escalation configuration
|
||||
escalation_enabled = models.BooleanField(default=True)
|
||||
escalation_threshold_percent = models.FloatField(
|
||||
default=80.0,
|
||||
validators=[MinValueValidator(0.0), MaxValueValidator(100.0)],
|
||||
help_text="Escalate when X% of SLA time has passed"
|
||||
)
|
||||
|
||||
# Status and metadata
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_default = models.BooleanField(default=False)
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = [
|
||||
models.Index(fields=['sla_type', 'is_active']),
|
||||
models.Index(fields=['incident_severities']),
|
||||
models.Index(fields=['incident_categories']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.sla_type})"
|
||||
|
||||
def applies_to_incident(self, incident) -> bool:
|
||||
"""Check if this SLA applies to the given incident"""
|
||||
if not self.is_active:
|
||||
return False
|
||||
|
||||
# Check categories
|
||||
if self.incident_categories and incident.category not in self.incident_categories:
|
||||
return False
|
||||
|
||||
# Check severities
|
||||
if self.incident_severities and incident.severity not in self.incident_severities:
|
||||
return False
|
||||
|
||||
# Check priorities
|
||||
if self.incident_priorities and incident.priority not in self.incident_priorities:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class EscalationPolicy(models.Model):
|
||||
"""Escalation policies for incidents and SLAs"""
|
||||
|
||||
ESCALATION_TYPES = [
|
||||
('TIME_BASED', 'Time-based Escalation'),
|
||||
('SEVERITY_BASED', 'Severity-based Escalation'),
|
||||
('RESOURCE_BASED', 'Resource-based Escalation'),
|
||||
('CUSTOM', 'Custom Escalation'),
|
||||
]
|
||||
|
||||
TRIGGER_CONDITIONS = [
|
||||
('SLA_BREACH', 'SLA Breach'),
|
||||
('SLA_THRESHOLD', 'SLA Threshold Reached'),
|
||||
('NO_RESPONSE', 'No Response'),
|
||||
('NO_ACKNOWLEDGMENT', 'No Acknowledgment'),
|
||||
('CUSTOM', 'Custom Condition'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=200, unique=True)
|
||||
description = models.TextField()
|
||||
escalation_type = models.CharField(max_length=20, choices=ESCALATION_TYPES)
|
||||
trigger_condition = models.CharField(max_length=20, choices=TRIGGER_CONDITIONS)
|
||||
|
||||
# Targeting criteria
|
||||
incident_severities = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of incident severities this policy applies to"
|
||||
)
|
||||
incident_categories = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of incident categories this policy applies to"
|
||||
)
|
||||
|
||||
# Escalation configuration
|
||||
trigger_delay_minutes = models.PositiveIntegerField(
|
||||
default=0,
|
||||
help_text="Delay before escalation triggers (in minutes)"
|
||||
)
|
||||
escalation_steps = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of escalation steps with timing and actions"
|
||||
)
|
||||
|
||||
# Notification configuration
|
||||
notification_channels = models.JSONField(
|
||||
default=list,
|
||||
help_text="Channels to notify during escalation (email, sms, slack, etc.)"
|
||||
)
|
||||
notification_templates = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Templates for different notification channels"
|
||||
)
|
||||
|
||||
# Status and metadata
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_default = models.BooleanField(default=False)
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = [
|
||||
models.Index(fields=['escalation_type', 'is_active']),
|
||||
models.Index(fields=['trigger_condition']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.escalation_type})"
|
||||
|
||||
def applies_to_incident(self, incident) -> bool:
|
||||
"""Check if this escalation policy applies to the given incident"""
|
||||
if not self.is_active:
|
||||
return False
|
||||
|
||||
# Check severities
|
||||
if self.incident_severities and incident.severity not in self.incident_severities:
|
||||
return False
|
||||
|
||||
# Check categories
|
||||
if self.incident_categories and incident.category not in self.incident_categories:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class OnCallRotation(models.Model):
|
||||
"""On-call rotation management"""
|
||||
|
||||
ROTATION_TYPES = [
|
||||
('WEEKLY', 'Weekly Rotation'),
|
||||
('DAILY', 'Daily Rotation'),
|
||||
('MONTHLY', 'Monthly Rotation'),
|
||||
('CUSTOM', 'Custom Schedule'),
|
||||
]
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('ACTIVE', 'Active'),
|
||||
('PAUSED', 'Paused'),
|
||||
('INACTIVE', 'Inactive'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=200, unique=True)
|
||||
description = models.TextField()
|
||||
rotation_type = models.CharField(max_length=20, choices=ROTATION_TYPES)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='ACTIVE')
|
||||
|
||||
# Team configuration
|
||||
team_name = models.CharField(max_length=100)
|
||||
team_description = models.TextField(blank=True, null=True)
|
||||
|
||||
# Schedule configuration
|
||||
schedule_config = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Configuration for the rotation schedule"
|
||||
)
|
||||
timezone = models.CharField(max_length=50, default='UTC')
|
||||
|
||||
# Integration configuration
|
||||
external_system = models.CharField(
|
||||
max_length=50,
|
||||
choices=[
|
||||
('PAGERDUTY', 'PagerDuty'),
|
||||
('OPSGENIE', 'OpsGenie'),
|
||||
('INTERNAL', 'Internal System'),
|
||||
('CUSTOM', 'Custom Integration'),
|
||||
],
|
||||
default='INTERNAL'
|
||||
)
|
||||
external_system_id = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="ID in external system (PagerDuty schedule ID, etc.)"
|
||||
)
|
||||
integration_config = models.JSONField(
|
||||
default=dict,
|
||||
help_text="Configuration for external system integration"
|
||||
)
|
||||
|
||||
# Status and metadata
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['name']
|
||||
indexes = [
|
||||
models.Index(fields=['status', 'rotation_type']),
|
||||
models.Index(fields=['external_system']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.rotation_type})"
|
||||
|
||||
def get_current_oncall(self, check_time: datetime = None):
|
||||
"""Get the current on-call person for the given time"""
|
||||
if check_time is None:
|
||||
check_time = timezone.now()
|
||||
|
||||
return self.assignments.filter(
|
||||
start_time__lte=check_time,
|
||||
end_time__gte=check_time,
|
||||
status='ACTIVE'
|
||||
).first()
|
||||
|
||||
|
||||
class OnCallAssignment(models.Model):
|
||||
"""Individual on-call assignments within rotations"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('SCHEDULED', 'Scheduled'),
|
||||
('ACTIVE', 'Active'),
|
||||
('COMPLETED', 'Completed'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
rotation = models.ForeignKey(
|
||||
OnCallRotation,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='assignments'
|
||||
)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
# Schedule
|
||||
start_time = models.DateTimeField()
|
||||
end_time = models.DateTimeField()
|
||||
|
||||
# Status
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='SCHEDULED')
|
||||
|
||||
# Handoff information
|
||||
handoff_notes = models.TextField(blank=True, null=True)
|
||||
handed_off_from = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='handed_off_assignments'
|
||||
)
|
||||
handoff_time = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# Performance tracking
|
||||
incidents_handled = models.PositiveIntegerField(default=0)
|
||||
response_time_avg = models.DurationField(null=True, blank=True)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['start_time']
|
||||
indexes = [
|
||||
models.Index(fields=['rotation', 'start_time']),
|
||||
models.Index(fields=['user', 'start_time']),
|
||||
models.Index(fields=['status', 'start_time']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} on-call {self.start_time} - {self.end_time}"
|
||||
|
||||
def is_current(self, check_time: datetime = None) -> bool:
|
||||
"""Check if this assignment is currently active"""
|
||||
if check_time is None:
|
||||
check_time = timezone.now()
|
||||
|
||||
return (self.status == 'ACTIVE' and
|
||||
self.start_time <= check_time <= self.end_time)
|
||||
|
||||
|
||||
class SLAInstance(models.Model):
|
||||
"""Instance of SLA tracking for a specific incident"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('ACTIVE', 'Active'),
|
||||
('MET', 'SLA Met'),
|
||||
('BREACHED', 'SLA Breached'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
sla_definition = models.ForeignKey(SLADefinition, on_delete=models.CASCADE)
|
||||
incident = models.ForeignKey(
|
||||
'incident_intelligence.Incident',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='sla_instances'
|
||||
)
|
||||
|
||||
# SLA tracking
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='ACTIVE')
|
||||
target_time = models.DateTimeField(help_text="When the SLA should be met")
|
||||
|
||||
# Timing
|
||||
started_at = models.DateTimeField(auto_now_add=True)
|
||||
met_at = models.DateTimeField(null=True, blank=True)
|
||||
breached_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# Escalation tracking
|
||||
escalation_policy = models.ForeignKey(
|
||||
EscalationPolicy,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
escalation_triggered = models.BooleanField(default=False)
|
||||
escalation_triggered_at = models.DateTimeField(null=True, blank=True)
|
||||
escalation_level = models.PositiveIntegerField(default=0)
|
||||
|
||||
# Performance metrics
|
||||
response_time = models.DurationField(null=True, blank=True)
|
||||
resolution_time = models.DurationField(null=True, blank=True)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['incident', 'status']),
|
||||
models.Index(fields=['sla_definition', 'status']),
|
||||
models.Index(fields=['target_time', 'status']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"SLA {self.sla_definition.name} for {self.incident.title}"
|
||||
|
||||
@property
|
||||
def is_breached(self) -> bool:
|
||||
"""Check if SLA is breached"""
|
||||
if self.status == 'BREACHED':
|
||||
return True
|
||||
|
||||
if self.status == 'ACTIVE':
|
||||
return timezone.now() > self.target_time
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def time_remaining(self) -> timedelta:
|
||||
"""Get time remaining until SLA breach"""
|
||||
if self.status != 'ACTIVE':
|
||||
return timedelta(0)
|
||||
|
||||
remaining = self.target_time - timezone.now()
|
||||
return remaining if remaining > timedelta(0) else timedelta(0)
|
||||
|
||||
@property
|
||||
def breach_time(self) -> timedelta:
|
||||
"""Get time since SLA breach"""
|
||||
if not self.is_breached:
|
||||
return timedelta(0)
|
||||
|
||||
return timezone.now() - self.target_time
|
||||
|
||||
|
||||
class EscalationInstance(models.Model):
|
||||
"""Instance of escalation for a specific incident"""
|
||||
|
||||
STATUS_CHOICES = [
|
||||
('PENDING', 'Pending'),
|
||||
('TRIGGERED', 'Triggered'),
|
||||
('ACKNOWLEDGED', 'Acknowledged'),
|
||||
('RESOLVED', 'Resolved'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
escalation_policy = models.ForeignKey(EscalationPolicy, on_delete=models.CASCADE)
|
||||
incident = models.ForeignKey(
|
||||
'incident_intelligence.Incident',
|
||||
on_delete=models.CASCADE,
|
||||
related_name='escalation_instances'
|
||||
)
|
||||
sla_instance = models.ForeignKey(
|
||||
SLAInstance,
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='escalation_instances'
|
||||
)
|
||||
|
||||
# Escalation details
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING')
|
||||
escalation_level = models.PositiveIntegerField(default=1)
|
||||
current_step = models.PositiveIntegerField(default=0)
|
||||
|
||||
# Timing
|
||||
triggered_at = models.DateTimeField(null=True, blank=True)
|
||||
acknowledged_at = models.DateTimeField(null=True, blank=True)
|
||||
resolved_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
# Actions taken
|
||||
notifications_sent = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of notifications sent during escalation"
|
||||
)
|
||||
actions_taken = models.JSONField(
|
||||
default=list,
|
||||
help_text="List of actions taken during escalation"
|
||||
)
|
||||
|
||||
# Metadata
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['incident', 'status']),
|
||||
models.Index(fields=['escalation_policy', 'status']),
|
||||
models.Index(fields=['triggered_at']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"Escalation {self.escalation_policy.name} for {self.incident.title}"
|
||||
|
||||
|
||||
class NotificationTemplate(models.Model):
|
||||
"""Templates for escalation and on-call notifications"""
|
||||
|
||||
TEMPLATE_TYPES = [
|
||||
('ESCALATION', 'Escalation Notification'),
|
||||
('ONCALL_HANDOFF', 'On-Call Handoff'),
|
||||
('SLA_BREACH', 'SLA Breach Alert'),
|
||||
('SLA_WARNING', 'SLA Warning'),
|
||||
('CUSTOM', 'Custom Notification'),
|
||||
]
|
||||
|
||||
CHANNEL_TYPES = [
|
||||
('EMAIL', 'Email'),
|
||||
('SMS', 'SMS'),
|
||||
('SLACK', 'Slack'),
|
||||
('TEAMS', 'Microsoft Teams'),
|
||||
('WEBHOOK', 'Webhook'),
|
||||
('CUSTOM', 'Custom Channel'),
|
||||
]
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
name = models.CharField(max_length=200)
|
||||
template_type = models.CharField(max_length=20, choices=TEMPLATE_TYPES)
|
||||
channel_type = models.CharField(max_length=20, choices=CHANNEL_TYPES)
|
||||
|
||||
# Template content
|
||||
subject_template = models.CharField(
|
||||
max_length=500,
|
||||
help_text="Subject template with variables"
|
||||
)
|
||||
body_template = models.TextField(
|
||||
help_text="Body template with variables"
|
||||
)
|
||||
variables = models.JSONField(
|
||||
default=list,
|
||||
help_text="Available variables for this template"
|
||||
)
|
||||
|
||||
# Status and metadata
|
||||
is_active = models.BooleanField(default=True)
|
||||
is_default = models.BooleanField(default=False)
|
||||
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['template_type', 'channel_type', 'name']
|
||||
unique_together = ['template_type', 'channel_type', 'name']
|
||||
indexes = [
|
||||
models.Index(fields=['template_type', 'channel_type']),
|
||||
models.Index(fields=['is_active']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.template_type} - {self.channel_type})"
|
||||
Reference in New Issue
Block a user