658 lines
19 KiB
Python
658 lines
19 KiB
Python
"""
|
|
Communications app admin configuration.
|
|
"""
|
|
|
|
from django.contrib import admin
|
|
from django.utils.html import format_html
|
|
from django.urls import reverse
|
|
from django.utils.safestring import mark_safe
|
|
from decimal import Decimal
|
|
from datetime import datetime, timedelta
|
|
from .models import (
|
|
Message, MessageRecipient, NotificationTemplate, AlertRule,
|
|
AlertInstance, CommunicationChannel, DeliveryLog
|
|
)
|
|
|
|
|
|
class MessageRecipientInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for message recipients.
|
|
"""
|
|
model = MessageRecipient
|
|
extra = 0
|
|
fields = [
|
|
'recipient_type', 'user', 'email_address', 'phone_number',
|
|
'status', 'sent_at', 'delivered_at', 'read_at'
|
|
]
|
|
readonly_fields = [
|
|
'recipient_id', 'sent_at', 'delivered_at', 'read_at',
|
|
'acknowledged_at', 'delivery_attempts'
|
|
]
|
|
|
|
|
|
class DeliveryLogInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for delivery logs.
|
|
"""
|
|
model = DeliveryLog
|
|
extra = 0
|
|
fields = [
|
|
'channel', 'status', 'attempt_number',
|
|
'started_at', 'completed_at', 'response_code'
|
|
]
|
|
readonly_fields = [
|
|
'log_id', 'started_at', 'completed_at',
|
|
'processing_time_ms', 'payload_size_bytes'
|
|
]
|
|
|
|
|
|
@admin.register(Message)
|
|
class MessageAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for messages.
|
|
"""
|
|
list_display = [
|
|
'subject', 'message_type', 'priority',
|
|
'sender', 'recipient_count_display',
|
|
'status', 'created_at', 'sent_at'
|
|
]
|
|
list_filter = [
|
|
'tenant', 'message_type', 'priority', 'status',
|
|
'is_urgent', 'is_confidential', 'created_at'
|
|
]
|
|
search_fields = [
|
|
'subject', 'content', 'sender__username',
|
|
'sender__first_name', 'sender__last_name'
|
|
]
|
|
readonly_fields = [
|
|
'message_id', 'delivery_attempts',
|
|
'created_at', 'sent_at'
|
|
]
|
|
fieldsets = [
|
|
('Message Information', {
|
|
'fields': [
|
|
'message_id', 'tenant', 'subject', 'content'
|
|
]
|
|
}),
|
|
('Message Type', {
|
|
'fields': [
|
|
'message_type', 'priority'
|
|
]
|
|
}),
|
|
('Sender', {
|
|
'fields': [
|
|
'sender'
|
|
]
|
|
}),
|
|
('Message Configuration', {
|
|
'fields': [
|
|
'is_urgent', 'requires_acknowledgment', 'is_confidential'
|
|
]
|
|
}),
|
|
('Scheduling', {
|
|
'fields': [
|
|
'scheduled_at', 'expires_at'
|
|
]
|
|
}),
|
|
('Message Status', {
|
|
'fields': [
|
|
'status'
|
|
]
|
|
}),
|
|
('Delivery Settings', {
|
|
'fields': [
|
|
'delivery_attempts', 'max_delivery_attempts'
|
|
]
|
|
}),
|
|
('Threading', {
|
|
'fields': [
|
|
'message_thread_id', 'reply_to_message'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('External References', {
|
|
'fields': [
|
|
'external_message_id'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Content Details', {
|
|
'fields': [
|
|
'has_attachments', 'content_type'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'sent_at'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
inlines = [MessageRecipientInline, DeliveryLogInline]
|
|
date_hierarchy = 'created_at'
|
|
|
|
def recipient_count_display(self, obj):
|
|
"""Display recipient count."""
|
|
return obj.recipients.count()
|
|
recipient_count_display.short_description = 'Recipients'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(tenant=request.user.tenant)
|
|
return qs.select_related('sender', 'reply_to_message')
|
|
|
|
|
|
@admin.register(MessageRecipient)
|
|
class MessageRecipientAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for message recipients.
|
|
"""
|
|
list_display = [
|
|
'message_subject', 'recipient_display', 'recipient_type',
|
|
'status', 'sent_at', 'delivered_at', 'read_at',
|
|
'delivery_attempts'
|
|
]
|
|
list_filter = [
|
|
'message__tenant', 'recipient_type', 'status',
|
|
'sent_at', 'delivered_at'
|
|
]
|
|
search_fields = [
|
|
'message__subject', 'user__username', 'user__first_name',
|
|
'user__last_name', 'email_address', 'phone_number'
|
|
]
|
|
readonly_fields = [
|
|
'recipient_id', 'sent_at', 'delivered_at', 'read_at',
|
|
'acknowledged_at', 'last_attempt_at'
|
|
]
|
|
fieldsets = [
|
|
('Recipient Information', {
|
|
'fields': [
|
|
'recipient_id', 'message', 'recipient_type'
|
|
]
|
|
}),
|
|
('Recipient Details', {
|
|
'fields': [
|
|
'user', 'email_address', 'phone_number', 'role_name'
|
|
]
|
|
}),
|
|
('Delivery Status', {
|
|
'fields': [
|
|
'status'
|
|
]
|
|
}),
|
|
('Timing', {
|
|
'fields': [
|
|
'sent_at', 'delivered_at', 'read_at', 'acknowledged_at'
|
|
]
|
|
}),
|
|
('Delivery Tracking', {
|
|
'fields': [
|
|
'delivery_attempts', 'last_attempt_at', 'error_message'
|
|
]
|
|
}),
|
|
('External Tracking', {
|
|
'fields': [
|
|
'external_delivery_id'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
inlines = [DeliveryLogInline]
|
|
|
|
def message_subject(self, obj):
|
|
"""Display message subject."""
|
|
return obj.message.subject
|
|
message_subject.short_description = 'Message'
|
|
|
|
def recipient_display(self, obj):
|
|
"""Display recipient information."""
|
|
if obj.user:
|
|
return obj.user.get_full_name() or obj.user.username
|
|
elif obj.email_address:
|
|
return obj.email_address
|
|
elif obj.phone_number:
|
|
return obj.phone_number
|
|
elif obj.role_name:
|
|
return f"Role: {obj.role_name}"
|
|
return obj.recipient_type
|
|
recipient_display.short_description = 'Recipient'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(message__tenant=request.user.tenant)
|
|
return qs.select_related('message', 'user')
|
|
|
|
|
|
@admin.register(NotificationTemplate)
|
|
class NotificationTemplateAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for notification templates.
|
|
"""
|
|
list_display = [
|
|
'name', 'template_type', 'category',
|
|
'usage_count', 'is_active', 'is_system_template',
|
|
'last_used_at', 'created_at'
|
|
]
|
|
list_filter = [
|
|
'tenant', 'template_type', 'category',
|
|
'is_active', 'is_system_template', 'requires_approval'
|
|
]
|
|
search_fields = [
|
|
'name', 'description', 'subject_template', 'content_template'
|
|
]
|
|
readonly_fields = [
|
|
'template_id', 'usage_count', 'last_used_at',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Template Information', {
|
|
'fields': [
|
|
'template_id', 'tenant', 'name', 'description'
|
|
]
|
|
}),
|
|
('Template Type', {
|
|
'fields': [
|
|
'template_type', 'category'
|
|
]
|
|
}),
|
|
('Template Content', {
|
|
'fields': [
|
|
'subject_template', 'content_template'
|
|
]
|
|
}),
|
|
('Template Configuration', {
|
|
'fields': [
|
|
'variables', 'default_values', 'formatting_rules'
|
|
]
|
|
}),
|
|
('Template Settings', {
|
|
'fields': [
|
|
'is_active', 'is_system_template', 'requires_approval'
|
|
]
|
|
}),
|
|
('Usage Tracking', {
|
|
'fields': [
|
|
'usage_count', 'last_used_at'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(tenant=request.user.tenant)
|
|
return qs.select_related('created_by')
|
|
|
|
|
|
class AlertInstanceInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for alert instances.
|
|
"""
|
|
model = AlertInstance
|
|
extra = 0
|
|
fields = [
|
|
'title', 'severity', 'status',
|
|
'triggered_at', 'acknowledged_at', 'resolved_at'
|
|
]
|
|
readonly_fields = [
|
|
'alert_id', 'triggered_at', 'acknowledged_at',
|
|
'resolved_at', 'escalation_level'
|
|
]
|
|
|
|
|
|
@admin.register(AlertRule)
|
|
class AlertRuleAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for alert rules.
|
|
"""
|
|
list_display = [
|
|
'name', 'trigger_type', 'severity',
|
|
'trigger_count', 'last_triggered_at',
|
|
'is_active', 'is_system_rule'
|
|
]
|
|
list_filter = [
|
|
'tenant', 'trigger_type', 'severity',
|
|
'is_active', 'is_system_rule'
|
|
]
|
|
search_fields = [
|
|
'name', 'description'
|
|
]
|
|
readonly_fields = [
|
|
'rule_id', 'trigger_count', 'last_triggered_at',
|
|
'last_evaluated_at', 'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Rule Information', {
|
|
'fields': [
|
|
'rule_id', 'tenant', 'name', 'description'
|
|
]
|
|
}),
|
|
('Rule Type', {
|
|
'fields': [
|
|
'trigger_type', 'severity'
|
|
]
|
|
}),
|
|
('Rule Configuration', {
|
|
'fields': [
|
|
'trigger_conditions', 'evaluation_frequency', 'cooldown_period'
|
|
]
|
|
}),
|
|
('Notification Configuration', {
|
|
'fields': [
|
|
'notification_template', 'notification_channels', 'escalation_rules'
|
|
]
|
|
}),
|
|
('Recipients', {
|
|
'fields': [
|
|
'default_recipients', 'recipient_roles'
|
|
]
|
|
}),
|
|
('Rule Status', {
|
|
'fields': [
|
|
'is_active', 'is_system_rule'
|
|
]
|
|
}),
|
|
('Tracking', {
|
|
'fields': [
|
|
'trigger_count', 'last_triggered_at', 'last_evaluated_at'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
inlines = [AlertInstanceInline]
|
|
filter_horizontal = ['default_recipients']
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(tenant=request.user.tenant)
|
|
return qs.select_related('notification_template', 'created_by')
|
|
|
|
|
|
@admin.register(AlertInstance)
|
|
class AlertInstanceAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for alert instances.
|
|
"""
|
|
list_display = [
|
|
'title', 'alert_rule_name', 'severity',
|
|
'status', 'triggered_at', 'acknowledged_at',
|
|
'resolved_at', 'escalation_level'
|
|
]
|
|
list_filter = [
|
|
'alert_rule__tenant', 'severity', 'status',
|
|
'triggered_at', 'escalation_level'
|
|
]
|
|
search_fields = [
|
|
'title', 'description', 'alert_rule__name'
|
|
]
|
|
readonly_fields = [
|
|
'alert_id', 'triggered_at', 'acknowledged_at',
|
|
'resolved_at', 'escalated_at', 'notifications_sent',
|
|
'last_notification_at'
|
|
]
|
|
fieldsets = [
|
|
('Alert Information', {
|
|
'fields': [
|
|
'alert_id', 'alert_rule', 'title', 'description'
|
|
]
|
|
}),
|
|
('Alert Details', {
|
|
'fields': [
|
|
'severity', 'trigger_data', 'context_data'
|
|
]
|
|
}),
|
|
('Alert Status', {
|
|
'fields': [
|
|
'status'
|
|
]
|
|
}),
|
|
('Timing', {
|
|
'fields': [
|
|
'triggered_at', 'acknowledged_at', 'resolved_at', 'expires_at'
|
|
]
|
|
}),
|
|
('Response Tracking', {
|
|
'fields': [
|
|
'acknowledged_by', 'resolved_by', 'resolution_notes'
|
|
]
|
|
}),
|
|
('Escalation Tracking', {
|
|
'fields': [
|
|
'escalation_level', 'escalated_at'
|
|
]
|
|
}),
|
|
('Notification Tracking', {
|
|
'fields': [
|
|
'notifications_sent', 'last_notification_at'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'triggered_at'
|
|
|
|
def alert_rule_name(self, obj):
|
|
"""Display alert rule name."""
|
|
return obj.alert_rule.name
|
|
alert_rule_name.short_description = 'Alert Rule'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(alert_rule__tenant=request.user.tenant)
|
|
return qs.select_related('alert_rule', 'acknowledged_by', 'resolved_by')
|
|
|
|
|
|
@admin.register(CommunicationChannel)
|
|
class CommunicationChannelAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for communication channels.
|
|
"""
|
|
list_display = [
|
|
'name', 'channel_type', 'provider_type',
|
|
'health_status_display', 'success_rate_display',
|
|
'message_count', 'last_used_at', 'is_active'
|
|
]
|
|
list_filter = [
|
|
'tenant', 'channel_type', 'provider_type',
|
|
'is_active', 'is_healthy'
|
|
]
|
|
search_fields = [
|
|
'name', 'description'
|
|
]
|
|
readonly_fields = [
|
|
'channel_id', 'is_healthy', 'last_health_check',
|
|
'message_count', 'success_count', 'failure_count',
|
|
'last_used_at', 'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Channel Information', {
|
|
'fields': [
|
|
'channel_id', 'tenant', 'name', 'description'
|
|
]
|
|
}),
|
|
('Channel Type', {
|
|
'fields': [
|
|
'channel_type', 'provider_type'
|
|
]
|
|
}),
|
|
('Configuration', {
|
|
'fields': [
|
|
'configuration', 'authentication_config', 'rate_limits'
|
|
]
|
|
}),
|
|
('Channel Status', {
|
|
'fields': [
|
|
'is_active', 'is_healthy', 'last_health_check', 'health_check_interval'
|
|
]
|
|
}),
|
|
('Usage Tracking', {
|
|
'fields': [
|
|
'message_count', 'success_count', 'failure_count', 'last_used_at'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
|
|
def health_status_display(self, obj):
|
|
"""Display health status with color coding."""
|
|
if obj.is_healthy:
|
|
return format_html('<span style="color: green;">✓ Healthy</span>')
|
|
return format_html('<span style="color: red;">✗ Unhealthy</span>')
|
|
health_status_display.short_description = 'Health'
|
|
|
|
def success_rate_display(self, obj):
|
|
"""Display success rate with color coding."""
|
|
rate = obj.success_rate
|
|
if rate >= 95:
|
|
color = 'green'
|
|
elif rate >= 85:
|
|
color = 'orange'
|
|
else:
|
|
color = 'red'
|
|
return format_html(
|
|
'<span style="color: {};">{:.1f}%</span>',
|
|
color, rate
|
|
)
|
|
success_rate_display.short_description = 'Success Rate'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(tenant=request.user.tenant)
|
|
return qs.select_related('created_by')
|
|
|
|
|
|
@admin.register(DeliveryLog)
|
|
class DeliveryLogAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for delivery logs.
|
|
"""
|
|
list_display = [
|
|
'message_subject', 'recipient_display', 'channel_name',
|
|
'status', 'attempt_number', 'started_at',
|
|
'duration_display', 'response_code'
|
|
]
|
|
list_filter = [
|
|
'message__tenant', 'status', 'channel__channel_type',
|
|
'started_at', 'attempt_number'
|
|
]
|
|
search_fields = [
|
|
'message__subject', 'recipient__user__username',
|
|
'recipient__email_address', 'channel__name'
|
|
]
|
|
readonly_fields = [
|
|
'log_id', 'started_at', 'completed_at',
|
|
'processing_time_ms', 'payload_size_bytes'
|
|
]
|
|
fieldsets = [
|
|
('Delivery Information', {
|
|
'fields': [
|
|
'log_id', 'message', 'recipient', 'channel'
|
|
]
|
|
}),
|
|
('Delivery Details', {
|
|
'fields': [
|
|
'status', 'attempt_number'
|
|
]
|
|
}),
|
|
('Timing', {
|
|
'fields': [
|
|
'started_at', 'completed_at'
|
|
]
|
|
}),
|
|
('Response Tracking', {
|
|
'fields': [
|
|
'external_id', 'response_code', 'response_message', 'error_details'
|
|
]
|
|
}),
|
|
('Performance Metrics', {
|
|
'fields': [
|
|
'processing_time_ms', 'payload_size_bytes'
|
|
]
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'metadata'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'started_at'
|
|
|
|
def message_subject(self, obj):
|
|
"""Display message subject."""
|
|
return obj.message.subject
|
|
message_subject.short_description = 'Message'
|
|
|
|
def recipient_display(self, obj):
|
|
"""Display recipient information."""
|
|
if obj.recipient.user:
|
|
return obj.recipient.user.get_full_name() or obj.recipient.user.username
|
|
elif obj.recipient.email_address:
|
|
return obj.recipient.email_address
|
|
elif obj.recipient.phone_number:
|
|
return obj.recipient.phone_number
|
|
return obj.recipient.recipient_type
|
|
recipient_display.short_description = 'Recipient'
|
|
|
|
def channel_name(self, obj):
|
|
"""Display channel name."""
|
|
return obj.channel.name
|
|
channel_name.short_description = 'Channel'
|
|
|
|
def duration_display(self, obj):
|
|
"""Display delivery duration."""
|
|
duration = obj.duration
|
|
if duration:
|
|
total_seconds = duration.total_seconds()
|
|
if total_seconds >= 60:
|
|
minutes = int(total_seconds // 60)
|
|
seconds = int(total_seconds % 60)
|
|
return f"{minutes}m {seconds}s"
|
|
return f"{total_seconds:.1f}s"
|
|
return "-"
|
|
duration_display.short_description = 'Duration'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(message__tenant=request.user.tenant)
|
|
return qs.select_related('message', 'recipient', 'channel')
|
|
|
|
|
|
# Customize admin site
|
|
admin.site.site_header = "Hospital Management System - Communications"
|
|
admin.site.site_title = "Communications Admin"
|
|
admin.site.index_title = "Communications Administration"
|
|
|