400 lines
14 KiB
Python
400 lines
14 KiB
Python
"""
|
|
Core Django signals for automation and audit logging.
|
|
|
|
This module contains signal handlers for the core app models to automate
|
|
audit logging, notifications, and other business logic.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from django.db.models.signals import post_delete, post_save, pre_delete, pre_save
|
|
from django.dispatch import receiver
|
|
from django.utils import timezone
|
|
|
|
from core.models import AuditLog, Consent, File, Patient, SubFile, User
|
|
from core.tasks import create_notification_task, send_email_task
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# ID Generation Helpers
|
|
# ============================================================================
|
|
|
|
|
|
def generate_mrn(tenant):
|
|
"""
|
|
Generate unique Medical Record Number (MRN).
|
|
Format: NNNNNN (6 digits)
|
|
"""
|
|
# Get the highest MRN number for this tenant
|
|
last_patient = Patient.objects.filter(
|
|
tenant=tenant,
|
|
mrn__isnull=False
|
|
).exclude(mrn='').order_by('-mrn').first()
|
|
|
|
if last_patient and last_patient.mrn:
|
|
# Extract number from last MRN
|
|
try:
|
|
last_number = int(last_patient.mrn)
|
|
new_number = last_number + 1
|
|
except (ValueError, TypeError):
|
|
new_number = 1
|
|
else:
|
|
new_number = 1
|
|
|
|
# Ensure uniqueness by checking if MRN already exists
|
|
while Patient.objects.filter(tenant=tenant, mrn=f"{new_number:06d}").exists():
|
|
new_number += 1
|
|
|
|
return f"{new_number:06d}"
|
|
|
|
|
|
def generate_file_number(tenant):
|
|
"""
|
|
Generate unique File Number.
|
|
Format: NNNNNN (6 digits)
|
|
"""
|
|
# Get the highest file number for this tenant
|
|
last_file = File.objects.filter(
|
|
tenant=tenant,
|
|
file_number__isnull=False
|
|
).exclude(file_number='').order_by('-file_number').first()
|
|
|
|
if last_file and last_file.file_number:
|
|
try:
|
|
last_number = int(last_file.file_number)
|
|
new_number = last_number + 1
|
|
except (ValueError, TypeError):
|
|
new_number = 1
|
|
else:
|
|
new_number = 1
|
|
|
|
# Ensure uniqueness by checking if file number already exists
|
|
while File.objects.filter(tenant=tenant, file_number=f"{new_number:06d}").exists():
|
|
new_number += 1
|
|
|
|
return f"{new_number:06d}"
|
|
|
|
|
|
def generate_subfile_number(file, clinic):
|
|
"""
|
|
Generate unique Sub-File Number.
|
|
Format: {FILE_NUMBER}-{CLINIC_CODE}-NN
|
|
Example: FILE-2025-000001-MED-01
|
|
"""
|
|
# Get count of existing sub-files for this file
|
|
subfile_count = SubFile.objects.filter(file=file).count()
|
|
new_number = subfile_count + 1
|
|
|
|
return f"{file.file_number}-{clinic.code}-{new_number:02d}"
|
|
|
|
|
|
# ============================================================================
|
|
# Patient Signals
|
|
# ============================================================================
|
|
|
|
|
|
@receiver(pre_save, sender=Patient)
|
|
def patient_pre_save(sender, instance, **kwargs):
|
|
"""
|
|
Handle pre-save actions for Patient model.
|
|
|
|
Actions:
|
|
- Auto-generate MRN if not set
|
|
"""
|
|
if not instance.mrn:
|
|
instance.mrn = generate_mrn(instance.tenant)
|
|
logger.info(f"Generated MRN: {instance.mrn}")
|
|
|
|
|
|
@receiver(post_save, sender=Patient)
|
|
def patient_post_save(sender, instance, created, **kwargs):
|
|
"""
|
|
Handle post-save actions for Patient model.
|
|
|
|
Actions:
|
|
- Create main File for new patients
|
|
- Create audit log entry for new patients
|
|
- Create audit log entry for patient updates
|
|
- Send welcome notification for new patients
|
|
"""
|
|
if created:
|
|
# Auto-create main File for patient
|
|
File.objects.create(
|
|
tenant=instance.tenant,
|
|
patient=instance,
|
|
status='ACTIVE'
|
|
)
|
|
logger.info(f"Created main file for patient: {instance.mrn}")
|
|
|
|
# Create audit log for new patient
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
AuditLog.objects.create(
|
|
tenant=instance.tenant,
|
|
user=kwargs.get('request_user'), # Set by middleware or view
|
|
action='CREATE',
|
|
content_type=ContentType.objects.get_for_model(instance),
|
|
object_id=instance.id,
|
|
changes={
|
|
'mrn': instance.mrn,
|
|
'name': f"{instance.first_name_en} {instance.last_name_en}",
|
|
'date_of_birth': str(instance.date_of_birth),
|
|
},
|
|
)
|
|
|
|
logger.info(f"Patient created: {instance.mrn} - {instance.full_name_en}")
|
|
|
|
# Send welcome notification to patient if email exists
|
|
if instance.email:
|
|
send_email_task.delay(
|
|
subject="Welcome to Tenhal Healthcare",
|
|
message=f"Dear {instance.full_name_en},\n\n"
|
|
f"Welcome to Tenhal Multidisciplinary Healthcare Platform. "
|
|
f"Your Medical Record Number (MRN) is: {instance.mrn}\n\n"
|
|
f"Best regards,\nTenhal Healthcare Team",
|
|
recipient_list=[instance.email],
|
|
)
|
|
else:
|
|
# Create audit log for patient update
|
|
if hasattr(instance, '_changed_fields'):
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
AuditLog.objects.create(
|
|
tenant=instance.tenant,
|
|
user=kwargs.get('request_user'),
|
|
action='UPDATE',
|
|
content_type=ContentType.objects.get_for_model(instance),
|
|
object_id=instance.id,
|
|
changes=instance._changed_fields,
|
|
)
|
|
|
|
logger.info(f"Patient updated: {instance.mrn} - {instance.full_name_en}")
|
|
|
|
|
|
# ============================================================================
|
|
# File Signals
|
|
# ============================================================================
|
|
|
|
|
|
@receiver(pre_save, sender=File)
|
|
def file_pre_save(sender, instance, **kwargs):
|
|
"""
|
|
Handle pre-save actions for File model.
|
|
|
|
Actions:
|
|
- Auto-generate file number if not set
|
|
"""
|
|
if not instance.file_number:
|
|
instance.file_number = generate_file_number(instance.tenant)
|
|
logger.info(f"Generated File Number: {instance.file_number}")
|
|
|
|
|
|
@receiver(post_save, sender=File)
|
|
def file_post_save(sender, instance, created, **kwargs):
|
|
"""
|
|
Handle post-save actions for File model.
|
|
|
|
Actions:
|
|
- Create audit log entry
|
|
"""
|
|
if created:
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
AuditLog.objects.create(
|
|
tenant=instance.tenant,
|
|
action='CREATE',
|
|
content_type=ContentType.objects.get_for_model(instance),
|
|
object_id=str(instance.id),
|
|
changes={
|
|
'file_number': instance.file_number,
|
|
'patient_mrn': instance.patient.mrn,
|
|
'status': instance.status,
|
|
},
|
|
)
|
|
logger.info(f"File created: {instance.file_number} for patient {instance.patient.mrn}")
|
|
|
|
|
|
# ============================================================================
|
|
# SubFile Signals
|
|
# ============================================================================
|
|
|
|
|
|
@receiver(pre_save, sender=SubFile)
|
|
def subfile_pre_save(sender, instance, **kwargs):
|
|
"""
|
|
Handle pre-save actions for SubFile model.
|
|
|
|
Actions:
|
|
- Auto-generate sub-file number if not set
|
|
"""
|
|
if not instance.sub_file_number:
|
|
instance.sub_file_number = generate_subfile_number(instance.file, instance.clinic)
|
|
logger.info(f"Generated Sub-File Number: {instance.sub_file_number}")
|
|
|
|
|
|
@receiver(post_save, sender=SubFile)
|
|
def subfile_post_save(sender, instance, created, **kwargs):
|
|
"""
|
|
Handle post-save actions for SubFile model.
|
|
|
|
Actions:
|
|
- Create audit log entry
|
|
"""
|
|
if created:
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
AuditLog.objects.create(
|
|
tenant=instance.file.tenant,
|
|
action='CREATE',
|
|
content_type=ContentType.objects.get_for_model(instance),
|
|
object_id=str(instance.id),
|
|
changes={
|
|
'sub_file_number': instance.sub_file_number,
|
|
'file_number': instance.file.file_number,
|
|
'clinic': instance.clinic.name_en,
|
|
'status': instance.status,
|
|
},
|
|
)
|
|
logger.info(f"SubFile created: {instance.sub_file_number} for clinic {instance.clinic.name_en}")
|
|
|
|
|
|
@receiver(pre_delete, sender=Patient)
|
|
def patient_pre_delete(sender, instance, **kwargs):
|
|
"""
|
|
Handle pre-delete actions for Patient model.
|
|
|
|
Actions:
|
|
- Implement soft delete by setting is_active=False instead of actual deletion
|
|
- Create audit log entry
|
|
"""
|
|
# This signal is for hard deletes, but we prefer soft deletes
|
|
# The actual soft delete is handled in the model's delete() method
|
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
AuditLog.objects.create(
|
|
tenant=instance.tenant,
|
|
user=kwargs.get('request_user'),
|
|
action='DELETE',
|
|
content_type=ContentType.objects.get_for_model(instance),
|
|
object_id=instance.id,
|
|
changes={'is_active': False},
|
|
)
|
|
|
|
logger.warning(f"Patient deleted: {instance.mrn} - {instance.full_name_en}")
|
|
|
|
|
|
# ============================================================================
|
|
# User Signals
|
|
# ============================================================================
|
|
|
|
|
|
@receiver(post_save, sender=User)
|
|
def user_post_save(sender, instance, created, **kwargs):
|
|
"""
|
|
Handle post-save actions for User model.
|
|
|
|
Actions:
|
|
- Send welcome email to new users
|
|
- Create audit log entry
|
|
- Send notification about role changes
|
|
"""
|
|
if created:
|
|
# Send welcome email
|
|
if instance.email:
|
|
send_email_task.delay(
|
|
subject="Welcome to Tenhal Healthcare Platform",
|
|
message=f"Dear {instance.get_full_name()},\n\n"
|
|
f"Your account has been created successfully.\n"
|
|
f"Username: {instance.username}\n"
|
|
f"Role: {instance.get_role_display()}\n\n"
|
|
f"Please log in and change your password.\n\n"
|
|
f"Best regards,\nTenhal Healthcare Team",
|
|
recipient_list=[instance.email],
|
|
)
|
|
|
|
logger.info(f"User created: {instance.username} ({instance.get_role_display()})")
|
|
else:
|
|
# Check if role changed
|
|
if hasattr(instance, '_changed_fields') and 'role' in instance._changed_fields:
|
|
old_role = instance._changed_fields['role']['old']
|
|
new_role = instance._changed_fields['role']['new']
|
|
|
|
# Send notification about role change
|
|
if instance.email:
|
|
send_email_task.delay(
|
|
subject="Your Role Has Been Updated",
|
|
message=f"Dear {instance.get_full_name()},\n\n"
|
|
f"Your role has been changed from {old_role} to {new_role}.\n\n"
|
|
f"Best regards,\nTenhal Healthcare Team",
|
|
recipient_list=[instance.email],
|
|
)
|
|
|
|
logger.info(f"User role changed: {instance.username} from {old_role} to {new_role}")
|
|
|
|
|
|
# ============================================================================
|
|
# Consent Signals
|
|
# ============================================================================
|
|
|
|
|
|
@receiver(post_save, sender=Consent)
|
|
def consent_post_save(sender, instance, created, **kwargs):
|
|
"""
|
|
Handle post-save actions for Consent model.
|
|
|
|
Actions:
|
|
- Notify relevant staff when consent is signed
|
|
- Create audit log entry
|
|
- Send confirmation to patient
|
|
"""
|
|
if created:
|
|
logger.info(f"Consent created: {instance.consent_type} for patient {instance.patient.mrn}")
|
|
|
|
# If consent was just signed (check if signed_at is set)
|
|
if instance.signed_at and not created:
|
|
# Notify relevant staff
|
|
if hasattr(instance.patient, 'primary_provider') and instance.patient.primary_provider:
|
|
create_notification_task.delay(
|
|
user_id=str(instance.patient.primary_provider.id),
|
|
title="Consent Signed",
|
|
message=f"Patient {instance.patient.full_name_en} has signed "
|
|
f"{instance.get_consent_type_display()} consent.",
|
|
notification_type='INFO',
|
|
related_object_type='consent',
|
|
related_object_id=str(instance.id),
|
|
)
|
|
|
|
# Send confirmation to patient
|
|
if instance.patient.email:
|
|
send_email_task.delay(
|
|
subject="Consent Form Signed",
|
|
message=f"Dear {instance.patient.full_name_en},\n\n"
|
|
f"Your {instance.get_consent_type_display()} consent form has been "
|
|
f"signed and recorded.\n\n"
|
|
f"Signed on: {instance.signed_at}\n"
|
|
f"Signed by: {instance.signed_by_name}\n\n"
|
|
f"Best regards,\nTenhal Healthcare Team",
|
|
recipient_list=[instance.patient.email],
|
|
)
|
|
|
|
logger.info(f"Consent signed: {instance.consent_type} for patient {instance.patient.mrn}")
|
|
|
|
|
|
# ============================================================================
|
|
# Signal Connection Helper
|
|
# ============================================================================
|
|
|
|
|
|
def connect_signals():
|
|
"""
|
|
Explicitly connect all signals.
|
|
|
|
This function can be called from apps.py to ensure signals are connected.
|
|
"""
|
|
# Signals are automatically connected via @receiver decorator
|
|
# This function is here for explicit connection if needed
|
|
logger.info("Core signals connected")
|