agdar/core/signals.py
Marwan Alwali 98e13df2f6 update
2025-11-06 14:52:40 +03:00

398 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: FILE-YYYY-NNNNNN
"""
current_year = timezone.now().year
# Get last file for this tenant in current year
last_file = File.objects.filter(
tenant=tenant,
created_at__year=current_year
).order_by('-created_at').first()
if last_file and last_file.file_number:
try:
last_number = int(last_file.file_number[-1])
new_number = last_number + 1
except (ValueError, IndexError):
new_number = 1
else:
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")