""" OT Scoring Service Handles scoring calculations for OT consultations with dynamic configuration support. """ from django.db.models import Q from .models import OTConsult, OTScoringConfig class OTScoringService: """ Service class for calculating OT consultation scores. Supports dynamic scoring configuration. """ def __init__(self, consult: OTConsult): """ Initialize scoring service for a consultation. Args: consult: OTConsult instance to score """ self.consult = consult self.config = self._get_active_config() def _get_active_config(self) -> OTScoringConfig: """Get active scoring configuration for the tenant.""" config = OTScoringConfig.objects.filter( tenant=self.consult.tenant, is_active=True ).first() if not config: # Create default configuration if none exists config = OTScoringConfig.objects.create( tenant=self.consult.tenant, name="Default OT Scoring Configuration", is_active=True ) return config def calculate_self_help_score(self) -> int: """ Calculate self-help skills score. Returns: Score based on yes/no responses (yes=2, no=0) """ score = 0 for skill in self.consult.self_help_skills.all(): if skill.response == 'yes': score += 2 return min(score, self.config.self_help_max) def calculate_behavior_score(self) -> int: """ Calculate behavior score from infant and current behaviors. Returns: Score based on responses (yes=2, sometimes=1, no=0) """ score = 0 # Infant behaviors for behavior in self.consult.infant_behaviors.all(): if behavior.response == 'yes': score += 2 elif behavior.response == 'sometimes': score += 1 # Current behaviors for behavior in self.consult.current_behaviors.all(): if behavior.response == 'yes': score += 2 elif behavior.response == 'sometimes': score += 1 return min(score, self.config.behavior_max) def calculate_developmental_score(self) -> int: """ Calculate developmental milestones score. Only counts required milestones. Returns: Score based on required milestones achieved (2 points each) """ score = 0 required_milestones = self.consult.milestones.filter(is_required=True) for milestone in required_milestones: if milestone.age_achieved and milestone.age_achieved.strip(): score += 2 return min(score, self.config.developmental_max) def calculate_eating_score(self) -> int: """ Calculate eating/feeding score. Returns: Score based on yes/no responses (yes=2, no=0) """ score = 0 if self.consult.eats_healthy_variety: score += 2 if self.consult.eats_variety_textures: score += 2 if self.consult.participates_family_meals: score += 2 return min(score, self.config.eating_max) def calculate_total_score(self) -> dict: """ Calculate all scores and interpretation. Returns: Dictionary with all scores and interpretation """ self_help = self.calculate_self_help_score() behavior = self.calculate_behavior_score() developmental = self.calculate_developmental_score() eating = self.calculate_eating_score() total = self_help + behavior + developmental + eating # Determine interpretation based on thresholds if total <= self.config.immediate_attention_threshold: interpretation = self.config.immediate_attention_label recommendation = self.config.immediate_attention_recommendation elif total <= self.config.moderate_difficulty_threshold: interpretation = self.config.moderate_difficulty_label recommendation = self.config.moderate_difficulty_recommendation else: interpretation = self.config.age_appropriate_label recommendation = self.config.age_appropriate_recommendation # Check for critical flags critical_flags = self._get_critical_flags() if critical_flags: recommendation += "\n\n⚠ Additional concerns flagged: " + "; ".join(critical_flags) recommendation += ". These should be reviewed in the full evaluation." return { 'self_help': self_help, 'behavior': behavior, 'developmental': developmental, 'eating': eating, 'total': total, 'interpretation': interpretation, 'recommendation': recommendation, 'critical_flags': critical_flags, 'max_scores': { 'self_help': self.config.self_help_max, 'behavior': self.config.behavior_max, 'developmental': self.config.developmental_max, 'eating': self.config.eating_max, 'total': self.config.total_max_score, } } def _get_critical_flags(self) -> list: """ Identify critical concerns that need attention. Returns: List of critical concern descriptions """ flags = [] # Check for developmental regression if self.consult.motor_skill_regression: flags.append("Developmental regression reported") # Check for irregular sleep patterns in infancy infant_sleep = self.consult.infant_behaviors.filter( behavior='sleepIrregular', response='yes' ).first() if infant_sleep: flags.append("Irregular sleep patterns (infancy)") # Check for feeding difficulties if self.consult.eats_variety_textures is False: flags.append("Feeding difficulty with textures") # Check for aggressive behavior current_fights = self.consult.current_behaviors.filter( behavior='fights', response='yes' ).first() if current_fights: flags.append("Frequent aggressive behavior (fights)") # Check for frequent tantrums current_tantrums = self.consult.current_behaviors.filter( behavior='tantrums', response='yes' ).first() if current_tantrums: flags.append("Frequent temper tantrums") # Check for high restlessness current_restless = self.consult.current_behaviors.filter( behavior='restless', response='yes' ).first() if current_restless: flags.append("High restlessness and inattention") # Check for resistance to change current_resistant = self.consult.current_behaviors.filter( behavior='resistant', response='yes' ).first() if current_resistant: flags.append("Strong resistance to change or routines") return flags def save_scores(self) -> OTConsult: """ Calculate and save all scores to the consultation. Returns: Updated OTConsult instance """ scores = self.calculate_total_score() self.consult.self_help_score = scores['self_help'] self.consult.behavior_score = scores['behavior'] self.consult.developmental_score = scores['developmental'] self.consult.eating_score = scores['eating'] self.consult.total_score = scores['total'] self.consult.score_interpretation = scores['interpretation'] # Update recommendation if not already set if not self.consult.recommendation_notes: self.consult.recommendation_notes = scores['recommendation'] self.consult.save() return self.consult def get_score_summary(self) -> dict: """ Get a formatted summary of scores for display. Returns: Dictionary with formatted score information """ scores = self.calculate_total_score() return { 'scores': scores, 'percentages': { 'self_help': round((scores['self_help'] / scores['max_scores']['self_help']) * 100, 1) if scores['max_scores']['self_help'] > 0 else 0, 'behavior': round((scores['behavior'] / scores['max_scores']['behavior']) * 100, 1) if scores['max_scores']['behavior'] > 0 else 0, 'developmental': round((scores['developmental'] / scores['max_scores']['developmental']) * 100, 1) if scores['max_scores']['developmental'] > 0 else 0, 'eating': round((scores['eating'] / scores['max_scores']['eating']) * 100, 1) if scores['max_scores']['eating'] > 0 else 0, 'total': round((scores['total'] / scores['max_scores']['total']) * 100, 1) if scores['max_scores']['total'] > 0 else 0, }, 'chart_data': { 'labels': ['Self-Help', 'Behavior', 'Developmental', 'Eating'], 'scores': [scores['self_help'], scores['behavior'], scores['developmental'], scores['eating']], 'max_scores': [ scores['max_scores']['self_help'], scores['max_scores']['behavior'], scores['max_scores']['developmental'], scores['max_scores']['eating'] ] } } def initialize_consultation_data(consult: OTConsult): """ Initialize all required related data for a new consultation. Creates empty records for milestones, self-help skills, and behaviors. Args: consult: OTConsult instance to initialize """ from .models import OTMilestone, OTSelfHelpSkill, OTInfantBehavior, OTCurrentBehavior # Initialize milestones milestone_data = [ ('headControl', False), ('reachObject', False), ('rollOver', False), ('fingerFeed', False), ('sitting', True), # Required ('pullStand', False), ('crawling', True), # Required ('drawCircle', False), ('spoon', False), ('cutScissors', False), ('walking', True), # Required ('drinkCup', False), ('jump', False), ('hop', False), ('hopOneFoot', False), ('bike', False), ] for milestone, is_required in milestone_data: OTMilestone.objects.get_or_create( consult=consult, milestone=milestone, defaults={'is_required': is_required} ) # Initialize self-help skills self_help_data = [ ('8-9', 'Grasps small items with thumb and index finger'), ('8-9', 'Finger feeds self'), ('12-18', 'Holds a spoon'), ('12-18', 'Removes socks'), ('12-18', 'Notifies parent that diapers are soiled'), ('12-18', 'Cooperates with dressing'), ('18-24', 'Holds and drinks from a cup with minimal spilling'), ('18-24', 'Able to load spoon and bring to mouth with moderate spilling'), ('2-3', 'Unzips zippers and unbuttons large buttons'), ('2-3', 'Requires assistance to manage pullover clothing'), ('2-3', 'Able to take off pants, coat, socks and shoes without fasteners'), ('2-3', 'Able to feed self with little to no spilling'), ('3-4', 'Independently dresses self, may need help with fasteners'), ('3-4', 'Independent with toilet control and notification'), ('5-6', 'Independent with all dressing, including shoe tying'), ] for age_range, skill_name in self_help_data: OTSelfHelpSkill.objects.get_or_create( consult=consult, age_range=age_range, skill_name=skill_name ) # Initialize infant behaviors infant_behaviors = [ 'cried', 'good', 'alert', 'quiet', 'passive', 'active', 'likedHeld', 'resistedHeld', 'floppy', 'tense', 'sleepGood', 'sleepIrregular' ] for behavior in infant_behaviors: OTInfantBehavior.objects.get_or_create( consult=consult, behavior=behavior ) # Initialize current behaviors current_behaviors = [ 'quiet', 'active', 'tires', 'talks', 'impulsive', 'restless', 'stubborn', 'resistant', 'fights', 'tantrums', 'clumsy', 'frustrated' ] for behavior in current_behaviors: OTCurrentBehavior.objects.get_or_create( consult=consult, behavior=behavior )