agdar/aba/forms.py
2025-11-02 14:35:35 +03:00

465 lines
19 KiB
Python

"""
ABA forms for the Tenhal Multidisciplinary Healthcare Platform.
This module contains forms for ABA consultations and behavior tracking.
Based on ABA-F-1 form.
"""
from django import forms
from django.forms import inlineformset_factory
from django.utils.translation import gettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Fieldset, Row, Column, Submit, HTML
from .models import ABAConsult, ABABehavior, ABAGoal, ABASession, ABASkillTarget
class ABAConsultForm(forms.ModelForm):
"""
Form for ABA consultation (ABA-F-1).
Functional behavior assessment.
"""
class Meta:
model = ABAConsult
fields = [
'patient', 'appointment', 'consultation_date', 'provider',
'reason_of_referral', 'parental_concern', 'school_concern',
'respondents', 'interviewer', 'diagnosed_condition',
'interaction_hours_per_day', 'physiological_factors',
'medical_factors', 'recommendations',
]
widgets = {
'consultation_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'reason_of_referral': forms.RadioSelect(),
'parental_concern': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
'school_concern': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
'recommendations': forms.Textarea(attrs={'rows': 4, 'class': 'form-control'}),
'respondents': forms.TextInput(attrs={'class': 'form-control'}),
'interviewer': forms.TextInput(attrs={'class': 'form-control'}),
'diagnosed_condition': forms.TextInput(attrs={'class': 'form-control'}),
'interaction_hours_per_day': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.5', 'min': '0'}),
'physiological_factors': forms.Textarea(attrs={
'rows': 4,
'class': 'form-control',
'placeholder': _('e.g., Sleep: 8 hours per night, adequate. Appetite: Normal. Medical conditions: None reported.')
}),
'medical_factors': forms.Textarea(attrs={
'rows': 4,
'class': 'form-control',
'placeholder': _('e.g., Conditions: Asthma (controlled). Medications: Albuterol inhaler as needed. Allergies: Peanuts, dust.')
}),
'provider': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select provider'}),
'patient': forms.HiddenInput(),
'appointment': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
'patient',
'appointment',
Fieldset(
_('Consultation Information'),
Row(
Column('consultation_date', css_class='form-group col-md-6 mb-0'),
Column('provider', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
),
Fieldset(
_('Referral Information'),
'reason_of_referral',
'parental_concern',
'school_concern',
),
Fieldset(
_('Interview Details'),
Row(
Column('respondents', css_class='form-group col-md-6 mb-0'),
Column('interviewer', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
Row(
Column('diagnosed_condition', css_class='form-group col-md-6 mb-0'),
Column('interaction_hours_per_day', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
),
Fieldset(
_('Physiological Factors'),
HTML('<p class="text-muted">{}</p>'.format(_('Sleep, appetite, medical conditions affecting behavior'))),
'physiological_factors',
HTML('<small class="form-text text-muted">{}</small>'.format(_('Enter as JSON object with relevant factors'))),
),
Fieldset(
_('Medical Factors'),
HTML('<p class="text-muted">{}</p>'.format(_('Medical conditions and medications'))),
'medical_factors',
HTML('<small class="form-text text-muted">{}</small>'.format(_('Enter as JSON object with medical information'))),
),
Fieldset(
_('Recommendations'),
'recommendations',
),
HTML('<hr><h5>{}</h5>'.format(_('Behaviors'))),
HTML('<p class="text-muted">{}</p>'.format(_('Add specific behaviors below'))),
Submit('submit', _('Save ABA Consultation'), css_class='btn btn-primary')
)
class ABABehaviorForm(forms.ModelForm):
"""
Form for ABA behaviors.
"""
class Meta:
model = ABABehavior
fields = [
'behavior_description', 'frequency', 'duration', 'intensity',
'antecedents_likely', 'antecedents_least_likely', 'consequences',
'order',
]
widgets = {
'behavior_description': forms.Textarea(attrs={'rows': 2, 'class': 'form-control'}),
'frequency': forms.Select(attrs={'class': 'form-control'}),
'duration': forms.TextInput(attrs={'placeholder': _('e.g., 5-10 minutes'), 'class': 'form-control'}),
'intensity': forms.Select(attrs={'class': 'form-control'}),
'antecedents_likely': forms.Textarea(attrs={'rows': 2, 'class': 'form-control'}),
'antecedents_least_likely': forms.Textarea(attrs={'rows': 2, 'class': 'form-control'}),
'consequences': forms.Textarea(attrs={'rows': 2, 'class': 'form-control'}),
'order': forms.HiddenInput(),
}
# Inline formset for behaviors
ABABehaviorFormSet = inlineformset_factory(
ABAConsult,
ABABehavior,
form=ABABehaviorForm,
extra=1,
can_delete=True,
min_num=1,
validate_min=True,
)
class ABAGoalForm(forms.ModelForm):
"""
Form for ABA treatment goals.
"""
class Meta:
model = ABAGoal
fields = [
'consult', 'goal_description', 'target_date',
'status', 'progress_notes', 'achieved_date',
]
widgets = {
'consult': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select consultation'}),
'goal_description': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
'target_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'status': forms.Select(attrs={'class': 'form-control'}),
'achieved_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'progress_notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
'consult',
'goal_description',
Row(
Column('target_date', css_class='form-group col-md-6 mb-0'),
Column('status', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
'progress_notes',
'achieved_date',
Submit('submit', _('Save Goal'), css_class='btn btn-primary')
)
def clean(self):
cleaned_data = super().clean()
status = cleaned_data.get('status')
achieved_date = cleaned_data.get('achieved_date')
# Validate achieved date is set when status is ACHIEVED
if status == ABAGoal.Status.ACHIEVED and not achieved_date:
raise forms.ValidationError(
_('Achieved date is required when status is "Achieved".')
)
return cleaned_data
class ABAConsultSearchForm(forms.Form):
"""
Form for searching ABA consultations.
"""
search_query = forms.CharField(
required=False,
label=_('Search'),
widget=forms.TextInput(attrs={
'placeholder': _('Patient name, MRN, Diagnosed condition...'),
'class': 'form-control'
})
)
reason_of_referral = forms.ChoiceField(
required=False,
label=_('Reason of Referral'),
choices=[('', _('All'))] + list(ABAConsult.ReasonOfReferral.choices),
widget=forms.Select(attrs={'class': 'form-control'})
)
provider = forms.ModelChoiceField(
required=False,
label=_('Provider'),
queryset=None, # Will be set in __init__
widget=forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select provider'})
)
date_from = forms.DateField(
required=False,
label=_('From Date'),
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})
)
date_to = forms.DateField(
required=False,
label=_('To Date'),
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})
)
def __init__(self, *args, **kwargs):
tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
if tenant:
from core.models import User
self.fields['provider'].queryset = User.objects.filter(
tenant=tenant,
role=User.Role.ABA
)
self.helper = FormHelper()
self.helper.form_method = 'get'
self.helper.layout = Layout(
Row(
Column('search_query', css_class='form-group col-md-3 mb-0'),
Column('reason_of_referral', css_class='form-group col-md-2 mb-0'),
Column('provider', css_class='form-group col-md-2 mb-0'),
Column('date_from', css_class='form-group col-md-2 mb-0'),
Column('date_to', css_class='form-group col-md-2 mb-0'),
css_class='form-row'
),
Submit('search', _('Search'), css_class='btn btn-primary')
)
class ABASessionForm(forms.ModelForm):
"""
Form for ABA session notes.
Session documentation with skill targets.
"""
class Meta:
model = ABASession
fields = [
'patient', 'appointment', 'session_date', 'provider',
'session_type', 'duration_minutes', 'engagement_level',
'cooperation_level', 'target_behaviors', 'interventions_used',
'activities_performed', 'behavioral_observations',
'progress_notes', 'recommendations', 'home_program',
]
widgets = {
'session_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'provider': forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select provider'}),
'session_type': forms.Select(attrs={'class': 'form-control'}),
'duration_minutes': forms.NumberInput(attrs={'class': 'form-control', 'min': '0', 'step': '5'}),
'engagement_level': forms.NumberInput(attrs={'class': 'form-control', 'min': '1', 'max': '5'}),
'cooperation_level': forms.NumberInput(attrs={'class': 'form-control', 'min': '1', 'max': '5'}),
'target_behaviors': forms.Textarea(attrs={'rows': 2, 'class': 'form-control', 'placeholder': _('e.g., Reduce tantrums, Increase eye contact, Improve turn-taking')}),
'interventions_used': forms.Textarea(attrs={'rows': 2, 'class': 'form-control', 'placeholder': _('e.g., Positive reinforcement, Visual schedules, Token economy')}),
'activities_performed': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
'behavioral_observations': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
'progress_notes': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
'recommendations': forms.Textarea(attrs={'rows': 3, 'class': 'form-control'}),
'home_program': forms.Textarea(attrs={'rows': 3, 'class': 'form-control', 'placeholder': _('Recommendations for parents to practice at home')}),
'patient': forms.HiddenInput(),
'appointment': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.layout = Layout(
'patient',
'appointment',
Fieldset(
_('Session Information'),
Row(
Column('session_date', css_class='form-group col-md-6 mb-0'),
Column('session_type', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
Row(
Column('provider', css_class='form-group col-md-6 mb-0'),
Column('duration_minutes', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
),
Fieldset(
_('Session Metrics'),
Row(
Column('engagement_level', css_class='form-group col-md-6 mb-0'),
Column('cooperation_level', css_class='form-group col-md-6 mb-0'),
css_class='form-row'
),
HTML('<small class="text-muted">{}</small>'.format(_('Rate from 1 (Very Low) to 5 (Excellent)'))),
),
Fieldset(
_('Session Content'),
'target_behaviors',
'interventions_used',
'activities_performed',
),
Fieldset(
_('Observations & Progress'),
'behavioral_observations',
'progress_notes',
),
Fieldset(
_('Recommendations'),
'recommendations',
'home_program',
),
HTML('<hr><h5>{}</h5>'.format(_('Skill Targets'))),
HTML('<p class="text-muted">{}</p>'.format(_('Add skill targets below with mastery levels and trial data'))),
Submit('submit', _('Save ABA Session'), css_class='btn btn-primary')
)
def clean(self):
cleaned_data = super().clean()
engagement_level = cleaned_data.get('engagement_level')
cooperation_level = cleaned_data.get('cooperation_level')
# Validate levels are within range
if engagement_level and (engagement_level < 1 or engagement_level > 5):
self.add_error('engagement_level', _('Level must be between 1 and 5'))
if cooperation_level and (cooperation_level < 1 or cooperation_level > 5):
self.add_error('cooperation_level', _('Level must be between 1 and 5'))
return cleaned_data
class ABASkillTargetForm(forms.ModelForm):
"""
Form for ABA skill targets with mastery tracking.
"""
class Meta:
model = ABASkillTarget
fields = [
'skill_name', 'mastery_level', 'trials_correct',
'trials_total', 'notes', 'order',
]
widgets = {
'skill_name': forms.TextInput(attrs={'placeholder': _('e.g., Follows one-step directions'), 'class': 'form-control'}),
'mastery_level': forms.Select(attrs={'class': 'form-control'}),
'trials_correct': forms.NumberInput(attrs={'class': 'form-control', 'min': '0'}),
'trials_total': forms.NumberInput(attrs={'class': 'form-control', 'min': '0'}),
'notes': forms.Textarea(attrs={'rows': 2, 'class': 'form-control'}),
'order': forms.HiddenInput(),
}
def clean(self):
cleaned_data = super().clean()
trials_correct = cleaned_data.get('trials_correct')
trials_total = cleaned_data.get('trials_total')
# Validate trials_correct is not greater than trials_total
if trials_correct is not None and trials_total is not None:
if trials_correct > trials_total:
self.add_error('trials_correct', _('Correct trials cannot exceed total trials'))
return cleaned_data
# Inline formset for skill targets
ABASkillTargetFormSet = inlineformset_factory(
ABASession,
ABASkillTarget,
form=ABASkillTargetForm,
extra=1,
can_delete=True,
min_num=1,
validate_min=True,
)
class ABASessionSearchForm(forms.Form):
"""
Form for searching ABA sessions.
"""
search_query = forms.CharField(
required=False,
label=_('Search'),
widget=forms.TextInput(attrs={
'placeholder': _('Patient name, MRN...'),
'class': 'form-control'
})
)
session_type = forms.ChoiceField(
required=False,
label=_('Session Type'),
choices=[('', _('All'))] + list(ABASession.SessionType.choices),
widget=forms.Select(attrs={'class': 'form-control'})
)
provider = forms.ModelChoiceField(
required=False,
label=_('Provider'),
queryset=None, # Will be set in __init__
widget=forms.Select(attrs={'class': 'form-select select2', 'data-placeholder': 'Select provider'})
)
date_from = forms.DateField(
required=False,
label=_('From Date'),
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})
)
date_to = forms.DateField(
required=False,
label=_('To Date'),
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})
)
def __init__(self, *args, **kwargs):
tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
if tenant:
from core.models import User
self.fields['provider'].queryset = User.objects.filter(
tenant=tenant,
role=User.Role.ABA
)
self.helper = FormHelper()
self.helper.form_method = 'get'
self.helper.layout = Layout(
Row(
Column('search_query', css_class='form-group col-md-3 mb-0'),
Column('session_type', css_class='form-group col-md-2 mb-0'),
Column('provider', css_class='form-group col-md-2 mb-0'),
Column('date_from', css_class='form-group col-md-2 mb-0'),
Column('date_to', css_class='form-group col-md-2 mb-0'),
css_class='form-row'
),
Submit('search', _('Search'), css_class='btn btn-primary')
)