""" 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")