333 lines
10 KiB
Python
333 lines
10 KiB
Python
"""
|
|
Patient Safety and Clinical Risk Management Models.
|
|
|
|
This module handles safety flags, behavioral risk indicators, and clinical alerts
|
|
for patient safety management.
|
|
"""
|
|
|
|
from django.db import models
|
|
from django.utils.translation import gettext_lazy as _
|
|
from simple_history.models import HistoricalRecords
|
|
|
|
from core.models import (
|
|
UUIDPrimaryKeyMixin,
|
|
TimeStampedMixin,
|
|
TenantOwnedMixin,
|
|
)
|
|
|
|
|
|
class PatientSafetyFlag(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin):
|
|
"""
|
|
Safety flags for patients with behavioral risks or medical alerts.
|
|
Only editable by Senior Therapists and Administrators.
|
|
"""
|
|
|
|
class FlagType(models.TextChoices):
|
|
AGGRESSION = 'AGGRESSION', _('Aggression Risk')
|
|
ELOPEMENT = 'ELOPEMENT', _('Elopement Risk')
|
|
SELF_HARM = 'SELF_HARM', _('Self-Harm Risk')
|
|
ALLERGY = 'ALLERGY', _('Allergy Alert')
|
|
MEDICAL = 'MEDICAL', _('Medical Alert')
|
|
SEIZURE = 'SEIZURE', _('Seizure Risk')
|
|
SENSORY = 'SENSORY', _('Sensory Sensitivity')
|
|
COMMUNICATION = 'COMMUNICATION', _('Communication Needs')
|
|
DIETARY = 'DIETARY', _('Dietary Restriction')
|
|
OTHER = 'OTHER', _('Other')
|
|
|
|
class Severity(models.TextChoices):
|
|
LOW = 'LOW', _('Low')
|
|
MEDIUM = 'MEDIUM', _('Medium')
|
|
HIGH = 'HIGH', _('High')
|
|
CRITICAL = 'CRITICAL', _('Critical')
|
|
|
|
patient = models.ForeignKey(
|
|
'core.Patient',
|
|
on_delete=models.CASCADE,
|
|
related_name='safety_flags',
|
|
verbose_name=_("Patient")
|
|
)
|
|
flag_type = models.CharField(
|
|
max_length=20,
|
|
choices=FlagType.choices,
|
|
verbose_name=_("Flag Type")
|
|
)
|
|
severity = models.CharField(
|
|
max_length=10,
|
|
choices=Severity.choices,
|
|
default=Severity.MEDIUM,
|
|
verbose_name=_("Severity")
|
|
)
|
|
title = models.CharField(
|
|
max_length=200,
|
|
verbose_name=_("Title"),
|
|
help_text=_("Brief description of the safety concern")
|
|
)
|
|
description = models.TextField(
|
|
verbose_name=_("Description"),
|
|
help_text=_("Detailed description of the safety concern and protocols")
|
|
)
|
|
protocols = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Safety Protocols"),
|
|
help_text=_("Specific protocols or procedures to follow")
|
|
)
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
verbose_name=_("Is Active")
|
|
)
|
|
created_by = models.ForeignKey(
|
|
'core.User',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
related_name='created_safety_flags',
|
|
verbose_name=_("Created By"),
|
|
help_text=_("Must be Senior Therapist or Administrator")
|
|
)
|
|
deactivated_at = models.DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Deactivated At")
|
|
)
|
|
deactivated_by = models.ForeignKey(
|
|
'core.User',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='deactivated_safety_flags',
|
|
verbose_name=_("Deactivated By")
|
|
)
|
|
deactivation_reason = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Deactivation Reason")
|
|
)
|
|
|
|
history = HistoricalRecords()
|
|
|
|
class Meta:
|
|
verbose_name = _("Patient Safety Flag")
|
|
verbose_name_plural = _("Patient Safety Flags")
|
|
ordering = ['-severity', '-created_at']
|
|
indexes = [
|
|
models.Index(fields=['patient', 'is_active']),
|
|
models.Index(fields=['flag_type', 'severity']),
|
|
models.Index(fields=['tenant', 'is_active']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.get_flag_type_display()} - {self.patient} ({self.get_severity_display()})"
|
|
|
|
def get_severity_color(self):
|
|
"""Get Bootstrap color class for severity display."""
|
|
colors = {
|
|
'LOW': 'info',
|
|
'MEDIUM': 'warning',
|
|
'HIGH': 'danger',
|
|
'CRITICAL': 'dark',
|
|
}
|
|
return colors.get(self.severity, 'secondary')
|
|
|
|
def get_icon(self):
|
|
"""Get icon class for flag type."""
|
|
icons = {
|
|
'AGGRESSION': 'bi-exclamation-triangle-fill',
|
|
'ELOPEMENT': 'bi-door-open-fill',
|
|
'SELF_HARM': 'bi-heart-pulse-fill',
|
|
'ALLERGY': 'bi-capsule-pill',
|
|
'MEDICAL': 'bi-hospital-fill',
|
|
'SEIZURE': 'bi-lightning-fill',
|
|
'SENSORY': 'bi-ear-fill',
|
|
'COMMUNICATION': 'bi-chat-dots-fill',
|
|
'DIETARY': 'bi-egg-fried',
|
|
'OTHER': 'bi-flag-fill',
|
|
}
|
|
return icons.get(self.flag_type, 'bi-flag-fill')
|
|
|
|
def deactivate(self, user, reason=""):
|
|
"""Deactivate this safety flag."""
|
|
from django.utils import timezone
|
|
self.is_active = False
|
|
self.deactivated_at = timezone.now()
|
|
self.deactivated_by = user
|
|
self.deactivation_reason = reason
|
|
self.save()
|
|
|
|
@classmethod
|
|
def get_active_flags_for_patient(cls, patient):
|
|
"""Get all active safety flags for a patient."""
|
|
return cls.objects.filter(
|
|
patient=patient,
|
|
is_active=True
|
|
).order_by('-severity', 'flag_type')
|
|
|
|
@classmethod
|
|
def has_critical_flags(cls, patient):
|
|
"""Check if patient has any critical safety flags."""
|
|
return cls.objects.filter(
|
|
patient=patient,
|
|
is_active=True,
|
|
severity=cls.Severity.CRITICAL
|
|
).exists()
|
|
|
|
|
|
class CrisisBehaviorProtocol(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin):
|
|
"""
|
|
Crisis behavior protocols and intervention strategies for patients.
|
|
Linked to safety flags for comprehensive risk management.
|
|
"""
|
|
|
|
patient = models.ForeignKey(
|
|
'core.Patient',
|
|
on_delete=models.CASCADE,
|
|
related_name='crisis_protocols',
|
|
verbose_name=_("Patient")
|
|
)
|
|
safety_flag = models.ForeignKey(
|
|
PatientSafetyFlag,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='crisis_protocols',
|
|
verbose_name=_("Related Safety Flag")
|
|
)
|
|
trigger_description = models.TextField(
|
|
verbose_name=_("Trigger Description"),
|
|
help_text=_("What triggers this behavior?")
|
|
)
|
|
warning_signs = models.TextField(
|
|
verbose_name=_("Warning Signs"),
|
|
help_text=_("Early warning signs to watch for")
|
|
)
|
|
intervention_steps = models.TextField(
|
|
verbose_name=_("Intervention Steps"),
|
|
help_text=_("Step-by-step intervention protocol")
|
|
)
|
|
de_escalation_techniques = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("De-escalation Techniques")
|
|
)
|
|
emergency_contacts = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Emergency Contacts"),
|
|
help_text=_("Who to contact in case of crisis")
|
|
)
|
|
medications = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Emergency Medications"),
|
|
help_text=_("Any emergency medications or medical interventions")
|
|
)
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
verbose_name=_("Is Active")
|
|
)
|
|
last_reviewed = models.DateField(
|
|
auto_now=True,
|
|
verbose_name=_("Last Reviewed")
|
|
)
|
|
reviewed_by = models.ForeignKey(
|
|
'core.User',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
related_name='reviewed_protocols',
|
|
verbose_name=_("Reviewed By")
|
|
)
|
|
|
|
history = HistoricalRecords()
|
|
|
|
class Meta:
|
|
verbose_name = _("Crisis Behavior Protocol")
|
|
verbose_name_plural = _("Crisis Behavior Protocols")
|
|
ordering = ['-last_reviewed']
|
|
indexes = [
|
|
models.Index(fields=['patient', 'is_active']),
|
|
models.Index(fields=['tenant', 'is_active']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"Crisis Protocol for {self.patient}"
|
|
|
|
|
|
class PatientAllergy(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin):
|
|
"""
|
|
Structured allergy tracking for patients.
|
|
Linked to safety flags for comprehensive medical alerts.
|
|
"""
|
|
|
|
class AllergyType(models.TextChoices):
|
|
FOOD = 'FOOD', _('Food Allergy')
|
|
MEDICATION = 'MEDICATION', _('Medication Allergy')
|
|
ENVIRONMENTAL = 'ENVIRONMENTAL', _('Environmental Allergy')
|
|
LATEX = 'LATEX', _('Latex Allergy')
|
|
OTHER = 'OTHER', _('Other')
|
|
|
|
class Severity(models.TextChoices):
|
|
MILD = 'MILD', _('Mild')
|
|
MODERATE = 'MODERATE', _('Moderate')
|
|
SEVERE = 'SEVERE', _('Severe')
|
|
ANAPHYLAXIS = 'ANAPHYLAXIS', _('Anaphylaxis Risk')
|
|
|
|
patient = models.ForeignKey(
|
|
'core.Patient',
|
|
on_delete=models.CASCADE,
|
|
related_name='allergies',
|
|
verbose_name=_("Patient")
|
|
)
|
|
safety_flag = models.ForeignKey(
|
|
PatientSafetyFlag,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='allergies',
|
|
verbose_name=_("Related Safety Flag")
|
|
)
|
|
allergy_type = models.CharField(
|
|
max_length=20,
|
|
choices=AllergyType.choices,
|
|
verbose_name=_("Allergy Type")
|
|
)
|
|
allergen = models.CharField(
|
|
max_length=200,
|
|
verbose_name=_("Allergen"),
|
|
help_text=_("Specific allergen (e.g., peanuts, penicillin)")
|
|
)
|
|
severity = models.CharField(
|
|
max_length=15,
|
|
choices=Severity.choices,
|
|
verbose_name=_("Severity")
|
|
)
|
|
reaction_description = models.TextField(
|
|
verbose_name=_("Reaction Description"),
|
|
help_text=_("Describe the allergic reaction")
|
|
)
|
|
treatment = models.TextField(
|
|
blank=True,
|
|
verbose_name=_("Treatment"),
|
|
help_text=_("Treatment protocol (e.g., EpiPen, antihistamine)")
|
|
)
|
|
verified_by_doctor = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("Verified by Doctor")
|
|
)
|
|
verification_date = models.DateField(
|
|
null=True,
|
|
blank=True,
|
|
verbose_name=_("Verification Date")
|
|
)
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
verbose_name=_("Is Active")
|
|
)
|
|
|
|
history = HistoricalRecords()
|
|
|
|
class Meta:
|
|
verbose_name = _("Patient Allergy")
|
|
verbose_name_plural = _("Patient Allergies")
|
|
ordering = ['-severity', 'allergen']
|
|
indexes = [
|
|
models.Index(fields=['patient', 'is_active']),
|
|
models.Index(fields=['allergy_type', 'severity']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.allergen} - {self.patient} ({self.get_severity_display()})"
|