agdar/ot/models.py
Marwan Alwali 2f1681b18c update
2025-11-11 13:44:48 +03:00

894 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Occupational Therapy (OT) models for the Tenhal Multidisciplinary Healthcare Platform.
This module handles OT consultations and session notes based on OT-F-1 and OT-F-3 forms.
Enhanced with comprehensive field-level data capture and scoring system.
"""
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,
ClinicallySignableMixin,
)
class OTConsult(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin, ClinicallySignableMixin):
"""
OT consultation form (OT-F-1).
Initial assessment and evaluation with comprehensive data capture.
"""
class ReferralReason(models.TextChoices):
DIAGNOSIS = 'Diagnosis', _('Multi-disciplinary Team Diagnosis')
CONSULTATION = 'Consultation', _('Consultation')
ASSESSMENT = 'Assessment', _('Assessment')
INTERVENTION = 'Intervention', _('Intervention')
PARENT_TRAINING = 'ParentTraining', _('Parent Training')
class Recommendation(models.TextChoices):
CONTINUE = 'CONTINUE', _('Continue Treatment')
DISCHARGE = 'DISCHARGE', _('Discharge')
REFER_TO_OTHER = 'REFER_TO_OTHER', _('Refer to Other Service')
# Core Relationships
patient = models.ForeignKey(
'core.Patient',
on_delete=models.CASCADE,
related_name='ot_consults',
verbose_name=_("Patient")
)
appointment = models.ForeignKey(
'appointments.Appointment',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='ot_consults',
verbose_name=_("Appointment")
)
consultation_date = models.DateField(
verbose_name=_("Consultation Date")
)
provider = models.ForeignKey(
'core.User',
on_delete=models.SET_NULL,
null=True,
related_name='ot_consults_provided',
verbose_name=_("Provider")
)
# Section 2: Reasons for Referral
referral_reason = models.CharField(
max_length=50,
choices=ReferralReason.choices,
blank=True,
verbose_name=_("Referral Reason")
)
# Section 4: Developmental History - Motor Learning & Regression
motor_learning_difficulty = models.BooleanField(
null=True,
blank=True,
verbose_name=_("Difficulty Learning New Motor Skills")
)
motor_learning_details = models.TextField(
blank=True,
verbose_name=_("Motor Learning Details")
)
motor_skill_regression = models.BooleanField(
null=True,
blank=True,
verbose_name=_("Lost Previously Gained Motor Skills")
)
regression_details = models.TextField(
blank=True,
verbose_name=_("Regression Details")
)
# Section 6: Eating/Feeding
eats_healthy_variety = models.BooleanField(
null=True,
blank=True,
verbose_name=_("Eats Healthy Variety of Food")
)
eats_variety_textures = models.BooleanField(
null=True,
blank=True,
verbose_name=_("Eats Variety of Textures and Flavors")
)
participates_family_meals = models.BooleanField(
null=True,
blank=True,
verbose_name=_("Participates in Family Meals")
)
eating_comments = models.TextField(
blank=True,
verbose_name=_("Eating Comments")
)
# Section 7: Behavior Comments
infant_behavior_comments = models.TextField(
blank=True,
verbose_name=_("Infant Behavior Comments")
)
current_behavior_comments = models.TextField(
blank=True,
verbose_name=_("Current Behavior Comments")
)
# Section 8: Recommendation
recommendation = models.CharField(
max_length=20,
choices=Recommendation.choices,
blank=True,
verbose_name=_("Recommendation")
)
recommendation_notes = models.TextField(
blank=True,
verbose_name=_("Recommendation Notes")
)
# Section 10: Clinician Signature
clinician_name = models.CharField(
max_length=200,
blank=True,
verbose_name=_("Clinician Name")
)
clinician_signature = models.CharField(
max_length=200,
blank=True,
verbose_name=_("Clinician Signature")
)
# Scoring fields (calculated)
self_help_score = models.PositiveIntegerField(
default=0,
verbose_name=_("Self-Help Score")
)
behavior_score = models.PositiveIntegerField(
default=0,
verbose_name=_("Behavior Score")
)
developmental_score = models.PositiveIntegerField(
default=0,
verbose_name=_("Developmental Score")
)
eating_score = models.PositiveIntegerField(
default=0,
verbose_name=_("Eating Score")
)
total_score = models.PositiveIntegerField(
default=0,
verbose_name=_("Total Score")
)
score_interpretation = models.CharField(
max_length=100,
blank=True,
verbose_name=_("Score Interpretation")
)
history = HistoricalRecords()
class Meta:
verbose_name = _("OT Consultation")
verbose_name_plural = _("OT Consultations")
ordering = ['-consultation_date', '-created_at']
indexes = [
models.Index(fields=['patient', 'consultation_date']),
models.Index(fields=['provider', 'consultation_date']),
models.Index(fields=['tenant', 'consultation_date']),
]
def __str__(self):
return f"OT Consultation - {self.patient} - {self.consultation_date}"
def calculate_scores(self):
"""Calculate all scores based on related data."""
# Self-Help Score (max 24)
self_help_score = 0
for skill in self.self_help_skills.all():
if skill.response == 'yes':
self_help_score += 2
# Behavior Score (max 48)
behavior_score = 0
for behavior in self.infant_behaviors.all():
if behavior.response == 'yes':
behavior_score += 2
elif behavior.response == 'sometimes':
behavior_score += 1
for behavior in self.current_behaviors.all():
if behavior.response == 'yes':
behavior_score += 2
elif behavior.response == 'sometimes':
behavior_score += 1
# Developmental Score (max 6)
developmental_score = 0
required_milestones = self.milestones.filter(is_required=True)
for milestone in required_milestones:
if milestone.age_achieved:
developmental_score += 2
# Eating Score (max 6)
eating_score = 0
if self.eats_healthy_variety:
eating_score += 2
if self.eats_variety_textures:
eating_score += 2
if self.participates_family_meals:
eating_score += 2
# Total Score
total_score = self_help_score + behavior_score + developmental_score + eating_score
# Interpretation
if total_score <= 30:
interpretation = "⚠️ Needs Immediate Attention"
elif total_score <= 60:
interpretation = "⚠ Moderate Difficulty - Follow-Up Needed"
else:
interpretation = "✅ Age-Appropriate Skills"
# Update fields
self.self_help_score = self_help_score
self.behavior_score = behavior_score
self.developmental_score = developmental_score
self.eating_score = eating_score
self.total_score = total_score
self.score_interpretation = interpretation
return {
'self_help': self_help_score,
'behavior': behavior_score,
'developmental': developmental_score,
'eating': eating_score,
'total': total_score,
'interpretation': interpretation
}
class OTDifficultyArea(UUIDPrimaryKeyMixin):
"""
Section 3: Areas of Difficulty (max 3 selections).
"""
AREA_CHOICES = [
('sensory', _('Sensory skills')),
('fineMotor', _('Fine motor skills')),
('grossMotor', _('Gross motor skills')),
('oralMotor', _('Oral motor / Feeding')),
('adl', _('ADL Activities')),
('handwriting', _('Handwriting')),
('play', _('Play')),
('social', _('Social skills')),
('selfInjury', _('Self-injurious behavior')),
('disorganized', _('Disorganized behaviors')),
('homeRec', _('Home recommendations')),
('parentEd', _('Parental education')),
]
consult = models.ForeignKey(
OTConsult,
on_delete=models.CASCADE,
related_name='difficulty_areas',
verbose_name=_("Consultation")
)
area = models.CharField(
max_length=50,
choices=AREA_CHOICES,
verbose_name=_("Difficulty Area")
)
details = models.TextField(
blank=True,
verbose_name=_("Details")
)
order = models.PositiveSmallIntegerField(
default=0,
verbose_name=_("Order")
)
class Meta:
verbose_name = _("OT Difficulty Area")
verbose_name_plural = _("OT Difficulty Areas")
ordering = ['consult', 'order']
unique_together = [['consult', 'area']]
def __str__(self):
return f"{self.get_area_display()} - {self.consult}"
class OTMilestone(UUIDPrimaryKeyMixin):
"""
Section 4: Developmental History - Motor Milestones.
"""
MILESTONE_CHOICES = [
('headControl', _('Controlling head')),
('reachObject', _('Reaching for object')),
('rollOver', _('Rolling over both ways')),
('fingerFeed', _('Finger feeding')),
('sitting', _('Sitting alone')),
('pullStand', _('Pulling to stand')),
('crawling', _('Creeping on all fours')),
('drawCircle', _('Drawing a circle')),
('spoon', _('Eating with spoon')),
('cutScissors', _('Cutting with scissors')),
('walking', _('Walking')),
('drinkCup', _('Drinking from a cup')),
('jump', _('Jumping')),
('hop', _('Hopping')),
('hopOneFoot', _('Hopping on one foot')),
('bike', _('Riding a bike')),
]
consult = models.ForeignKey(
OTConsult,
on_delete=models.CASCADE,
related_name='milestones',
verbose_name=_("Consultation")
)
milestone = models.CharField(
max_length=50,
choices=MILESTONE_CHOICES,
verbose_name=_("Milestone")
)
age_achieved = models.CharField(
max_length=50,
blank=True,
verbose_name=_("Age Achieved")
)
notes = models.TextField(
blank=True,
verbose_name=_("Notes")
)
is_required = models.BooleanField(
default=False,
verbose_name=_("Required Field")
)
class Meta:
verbose_name = _("OT Milestone")
verbose_name_plural = _("OT Milestones")
ordering = ['consult', 'id']
unique_together = [['consult', 'milestone']]
def __str__(self):
return f"{self.get_milestone_display()} - {self.age_achieved}"
class OTSelfHelpSkill(UUIDPrimaryKeyMixin):
"""
Section 5: Self-Help Skills by age range.
"""
AGE_RANGE_CHOICES = [
('8-9', _('8-9 months')),
('12-18', _('12-18 months')),
('18-24', _('18-24 months')),
('2-3', _('2-3 years')),
('3-4', _('3-4 years')),
('5-6', _('5-6 years')),
]
RESPONSE_CHOICES = [
('yes', _('Yes')),
('no', _('No')),
]
consult = models.ForeignKey(
OTConsult,
on_delete=models.CASCADE,
related_name='self_help_skills',
verbose_name=_("Consultation")
)
age_range = models.CharField(
max_length=10,
choices=AGE_RANGE_CHOICES,
verbose_name=_("Age Range")
)
skill_name = models.CharField(
max_length=200,
verbose_name=_("Skill")
)
response = models.CharField(
max_length=10,
choices=RESPONSE_CHOICES,
blank=True,
verbose_name=_("Response")
)
comments = models.TextField(
blank=True,
verbose_name=_("Comments")
)
class Meta:
verbose_name = _("OT Self-Help Skill")
verbose_name_plural = _("OT Self-Help Skills")
ordering = ['consult', 'age_range', 'id']
def __str__(self):
return f"{self.age_range}: {self.skill_name}"
class OTInfantBehavior(UUIDPrimaryKeyMixin):
"""
Section 7: Infant Behavior (First 12 Months).
"""
BEHAVIOR_CHOICES = [
('cried', _('Cried a lot, fussy, irritable')),
('good', _('Was good, non-demanding')),
('alert', _('Was alert')),
('quiet', _('Was quiet')),
('passive', _('Was passive')),
('active', _('Was active')),
('likedHeld', _('Liked being held')),
('resistedHeld', _('Resisted being held')),
('floppy', _('Was floppy when held')),
('tense', _('Was tense when held')),
('sleepGood', _('Had good sleep patterns')),
('sleepIrregular', _('Had irregular sleep patterns')),
]
RESPONSE_CHOICES = [
('yes', _('Yes')),
('no', _('No')),
('sometimes', _('Sometimes')),
]
consult = models.ForeignKey(
OTConsult,
on_delete=models.CASCADE,
related_name='infant_behaviors',
verbose_name=_("Consultation")
)
behavior = models.CharField(
max_length=50,
choices=BEHAVIOR_CHOICES,
verbose_name=_("Behavior")
)
response = models.CharField(
max_length=20,
choices=RESPONSE_CHOICES,
blank=True,
verbose_name=_("Response")
)
class Meta:
verbose_name = _("OT Infant Behavior")
verbose_name_plural = _("OT Infant Behaviors")
ordering = ['consult', 'id']
unique_together = [['consult', 'behavior']]
def __str__(self):
return f"{self.get_behavior_display()}: {self.response}"
class OTCurrentBehavior(UUIDPrimaryKeyMixin):
"""
Section 7: Current Behavior.
"""
BEHAVIOR_CHOICES = [
('quiet', _('Is mostly quiet')),
('active', _('Is overly active')),
('tires', _('Tires easily')),
('talks', _('Talks constantly')),
('impulsive', _('Is impulsive')),
('restless', _('Is restless')),
('stubborn', _('Is stubborn')),
('resistant', _('Is resistant to change')),
('fights', _('Fights frequently')),
('tantrums', _('Exhibits frequent temper tantrums')),
('clumsy', _('Is clumsy')),
('frustrated', _('Is frustrated easily')),
]
RESPONSE_CHOICES = [
('yes', _('Yes')),
('no', _('No')),
('sometimes', _('Sometimes')),
]
consult = models.ForeignKey(
OTConsult,
on_delete=models.CASCADE,
related_name='current_behaviors',
verbose_name=_("Consultation")
)
behavior = models.CharField(
max_length=50,
choices=BEHAVIOR_CHOICES,
verbose_name=_("Behavior")
)
response = models.CharField(
max_length=20,
choices=RESPONSE_CHOICES,
blank=True,
verbose_name=_("Response")
)
class Meta:
verbose_name = _("OT Current Behavior")
verbose_name_plural = _("OT Current Behaviors")
ordering = ['consult', 'id']
unique_together = [['consult', 'behavior']]
def __str__(self):
return f"{self.get_behavior_display()}: {self.response}"
# ============================================================================
# OT Session Models (OT-F-3)
# ============================================================================
class OTSession(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin, ClinicallySignableMixin):
"""
OT session notes form (OT-F-3).
Progress tracking and session documentation.
"""
class SessionType(models.TextChoices):
CONSULT = 'CONSULT', _('Consultation')
INDIVIDUAL = 'INDIVIDUAL', _('Individual Session')
GROUP = 'GROUP', _('Group Session')
PARENT_TRAINING = 'PARENT_TRAINING', _('Parent Training')
# Core Relationships
patient = models.ForeignKey(
'core.Patient',
on_delete=models.CASCADE,
related_name='ot_sessions',
verbose_name=_("Patient")
)
appointment = models.ForeignKey(
'appointments.Appointment',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='ot_sessions',
verbose_name=_("Appointment")
)
# NEW: Link to centralized session (for scheduling/capacity management)
session = models.ForeignKey(
'appointments.Session',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='ot_notes',
verbose_name=_("Session"),
help_text=_("Link to centralized session for scheduling")
)
# NEW: Link to specific participant (for group sessions)
session_participant = models.ForeignKey(
'appointments.SessionParticipant',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='ot_notes',
verbose_name=_("Session Participant"),
help_text=_("For group sessions: which participant these notes are for")
)
session_date = models.DateField(
verbose_name=_("Session Date")
)
provider = models.ForeignKey(
'core.User',
on_delete=models.SET_NULL,
null=True,
related_name='ot_sessions_provided',
verbose_name=_("Provider")
)
# Session Details
session_type = models.CharField(
max_length=20,
choices=SessionType.choices,
verbose_name=_("Session Type")
)
cooperative_level = models.PositiveSmallIntegerField(
null=True,
blank=True,
help_text=_("Cooperative level (1-4 scale)"),
verbose_name=_("Cooperative Level (1-4)")
)
distraction_tolerance = models.PositiveSmallIntegerField(
null=True,
blank=True,
help_text=_("Distraction tolerance (1-4 scale)"),
verbose_name=_("Distraction Tolerance (1-4)")
)
# Activities Checklist (TextField: "Today we work on...")
activities_checklist = models.TextField(
blank=True,
help_text=_("List of activities worked on during session"),
verbose_name=_("Activities Checklist")
)
# Session Notes
observations = models.TextField(
blank=True,
verbose_name=_("Observations")
)
activities_performed = models.TextField(
blank=True,
verbose_name=_("Activities Performed")
)
recommendations = models.TextField(
blank=True,
verbose_name=_("Recommendations")
)
history = HistoricalRecords()
class Meta:
verbose_name = _("OT Session")
verbose_name_plural = _("OT Sessions")
ordering = ['-session_date', '-created_at']
indexes = [
models.Index(fields=['patient', 'session_date']),
models.Index(fields=['provider', 'session_date']),
models.Index(fields=['tenant', 'session_date']),
]
def __str__(self):
return f"OT Session - {self.patient} - {self.session_date}"
@property
def cooperative_level_display(self):
"""Return cooperative level with description."""
levels = {
1: _("Poor"),
2: _("Fair"),
3: _("Good"),
4: _("Excellent")
}
return levels.get(self.cooperative_level, "")
@property
def distraction_tolerance_display(self):
"""Return distraction tolerance with description."""
levels = {
1: _("Low"),
2: _("Moderate"),
3: _("Good"),
4: _("High")
}
return levels.get(self.distraction_tolerance, "")
class OTTargetSkill(UUIDPrimaryKeyMixin):
"""
Target skills with 0-10 scoring for OT sessions.
Tracks progress on specific therapeutic goals.
"""
session = models.ForeignKey(
OTSession,
on_delete=models.CASCADE,
related_name='target_skills',
verbose_name=_("Session")
)
skill_name = models.CharField(
max_length=200,
verbose_name=_("Skill Name")
)
score = models.PositiveSmallIntegerField(
help_text=_("Score from 0 (not achieved) to 10 (fully achieved)"),
verbose_name=_("Score (0-10)")
)
notes = models.TextField(
blank=True,
verbose_name=_("Notes")
)
order = models.PositiveIntegerField(
default=0,
help_text=_("Order of skills in the list"),
verbose_name=_("Order")
)
class Meta:
verbose_name = _("OT Target Skill")
verbose_name_plural = _("OT Target Skills")
ordering = ['session', 'order']
def __str__(self):
return f"{self.skill_name} - Score: {self.score}/10"
@property
def score_percentage(self):
"""Return score as percentage."""
return (self.score / 10) * 100
@property
def achievement_level(self):
"""Return achievement level description."""
if self.score == 0:
return _("Not Achieved")
elif self.score <= 3:
return _("Emerging")
elif self.score <= 6:
return _("Developing")
elif self.score <= 9:
return _("Proficient")
else:
return _("Mastered")
class OTProgressReport(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin, ClinicallySignableMixin):
"""
OT progress report summarizing treatment outcomes.
"""
patient = models.ForeignKey(
'core.Patient',
on_delete=models.CASCADE,
related_name='ot_progress_reports',
verbose_name=_("Patient")
)
report_date = models.DateField(
verbose_name=_("Report Date")
)
provider = models.ForeignKey(
'core.User',
on_delete=models.SET_NULL,
null=True,
related_name='ot_progress_reports_provided',
verbose_name=_("Provider")
)
# Session Summary
sessions_scheduled = models.PositiveIntegerField(
default=0,
verbose_name=_("Sessions Scheduled")
)
sessions_attended = models.PositiveIntegerField(
default=0,
verbose_name=_("Sessions Attended")
)
# Progress Summary
goals_progress = models.TextField(
blank=True,
help_text=_("Progress on each goal"),
verbose_name=_("Goals Progress")
)
overall_progress = models.TextField(
blank=True,
verbose_name=_("Overall Progress")
)
# Recommendations
recommendations = models.TextField(
blank=True,
verbose_name=_("Recommendations")
)
continue_treatment = models.BooleanField(
default=True,
verbose_name=_("Continue Treatment")
)
history = HistoricalRecords()
class Meta:
verbose_name = _("OT Progress Report")
verbose_name_plural = _("OT Progress Reports")
ordering = ['-report_date', '-created_at']
indexes = [
models.Index(fields=['patient', 'report_date']),
models.Index(fields=['tenant', 'report_date']),
]
def __str__(self):
return f"OT Progress Report - {self.patient} - {self.report_date}"
@property
def attendance_rate(self):
"""Calculate attendance rate percentage."""
if self.sessions_scheduled > 0:
return round((self.sessions_attended / self.sessions_scheduled) * 100, 2)
return 0
# ============================================================================
# Scoring Configuration Model
# ============================================================================
class OTScoringConfig(UUIDPrimaryKeyMixin, TimeStampedMixin, TenantOwnedMixin):
"""
Dynamic scoring configuration for OT consultations.
Allows customization of scoring thresholds and interpretations.
"""
name = models.CharField(
max_length=200,
verbose_name=_("Configuration Name")
)
is_active = models.BooleanField(
default=True,
verbose_name=_("Is Active")
)
# Maximum scores for each domain
self_help_max = models.PositiveIntegerField(
default=24,
verbose_name=_("Self-Help Max Score")
)
behavior_max = models.PositiveIntegerField(
default=48,
verbose_name=_("Behavior Max Score")
)
developmental_max = models.PositiveIntegerField(
default=6,
verbose_name=_("Developmental Max Score")
)
eating_max = models.PositiveIntegerField(
default=6,
verbose_name=_("Eating Max Score")
)
# Interpretation thresholds
immediate_attention_threshold = models.PositiveIntegerField(
default=30,
help_text=_("Scores at or below this require immediate attention"),
verbose_name=_("Immediate Attention Threshold")
)
moderate_difficulty_threshold = models.PositiveIntegerField(
default=60,
help_text=_("Scores at or below this indicate moderate difficulty"),
verbose_name=_("Moderate Difficulty Threshold")
)
# Interpretation labels
immediate_attention_label = models.CharField(
max_length=200,
default="⚠️ Needs Immediate Attention",
verbose_name=_("Immediate Attention Label")
)
moderate_difficulty_label = models.CharField(
max_length=200,
default="⚠ Moderate Difficulty - Follow-Up Needed",
verbose_name=_("Moderate Difficulty Label")
)
age_appropriate_label = models.CharField(
max_length=200,
default="✅ Age-Appropriate Skills",
verbose_name=_("Age-Appropriate Label")
)
# Recommendation templates
immediate_attention_recommendation = models.TextField(
default="The child presents significant delays or difficulties across multiple developmental domains. Immediate referral to Occupational Therapy and interdisciplinary evaluation is recommended.",
verbose_name=_("Immediate Attention Recommendation")
)
moderate_difficulty_recommendation = models.TextField(
default="The child shows moderate concerns that warrant intervention. Recommend starting OT sessions and monitoring progress within 24 months.",
verbose_name=_("Moderate Difficulty Recommendation")
)
age_appropriate_recommendation = models.TextField(
default="Child demonstrates age-appropriate functioning in assessed areas. Recommend regular developmental screening as part of preventive care.",
verbose_name=_("Age-Appropriate Recommendation")
)
class Meta:
verbose_name = _("OT Scoring Configuration")
verbose_name_plural = _("OT Scoring Configurations")
ordering = ['-is_active', '-created_at']
def __str__(self):
return f"{self.name} {'(Active)' if self.is_active else '(Inactive)'}"
@property
def total_max_score(self):
"""Calculate total maximum possible score."""
return self.self_help_max + self.behavior_max + self.developmental_max + self.eating_max