Marwan Alwali 4d06ca4b5e update
2025-09-20 14:26:19 +03:00

465 lines
22 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.

"""
Forms for EMR app.
"""
from django import forms
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.contrib.auth import get_user_model
from hr.models import Employee
from .models import (
Encounter, VitalSigns, ProblemList, CarePlan,
ClinicalNote, NoteTemplate
)
User = get_user_model()
class EncounterForm(forms.ModelForm):
"""
Form for creating and updating encounters.
"""
class Meta:
model = Encounter
fields = [
'patient', 'provider', 'encounter_type', 'encounter_class',
'start_datetime', 'end_datetime', 'status', 'location', 'room_number',
'appointment', 'admission', 'chief_complaint', 'reason_for_visit',
'priority', 'acuity_level', 'billable'
]
widgets = {
'start_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
'end_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
'chief_complaint': forms.Textarea(attrs={'rows': 3}),
'reason_for_visit': forms.Textarea(attrs={'rows': 3}),
}
def __init__(self, *args, **kwargs):
self.tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
# Filter related objects by tenant
if self.tenant:
self.fields['patient'].queryset = self.fields['patient'].queryset.filter(tenant=self.tenant)
self.fields['provider'].queryset = User.objects.filter(tenant=self.tenant)
self.fields['appointment'].queryset = self.fields['appointment'].queryset.filter(tenant=self.tenant)
self.fields['admission'].queryset = self.fields['admission'].queryset.filter(tenant=self.tenant)
def clean(self):
cleaned_data = super().clean()
start_datetime = cleaned_data.get('start_datetime')
end_datetime = cleaned_data.get('end_datetime')
status = cleaned_data.get('status')
# Validate end_datetime is after start_datetime
if start_datetime and end_datetime and end_datetime < start_datetime:
self.add_error('end_datetime', 'End time must be after start time')
# Validate status and end_datetime consistency
if status == 'FINISHED' and not end_datetime:
cleaned_data['end_datetime'] = timezone.now()
return cleaned_data
class VitalSignsForm(forms.ModelForm):
"""
Form for recording vital signs.
"""
class Meta:
model = VitalSigns
# Do NOT use both fields and exclude. Keep only fields.
fields = [
'measured_datetime',
'temperature', 'temperature_method',
'systolic_bp', 'diastolic_bp', 'bp_position', 'bp_cuff_size',
'heart_rate', 'heart_rhythm',
'respiratory_rate',
'oxygen_saturation', 'oxygen_delivery', 'oxygen_flow_rate',
'pain_scale', 'pain_location', 'pain_quality',
'weight', 'height',
'head_circumference',
'device_used', 'device_calibrated', 'notes'
]
widgets = {
'measured_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
'notes': forms.Textarea(attrs={'rows': 3}),
'pain_location': forms.TextInput(),
'pain_quality': forms.TextInput(),
}
def __init__(self, *args, **kwargs):
self.tenant = kwargs.pop('tenant', None)
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
# If you had tenant-scoped lookups for fields that ARE in the form
# (e.g., device_used is FK and tenant-scoped), filter them here, e.g.:
# if self.tenant and 'device_used' in self.fields:
# self.fields['device_used'].queryset = Device.objects.filter(tenant=self.tenant)
# DO NOT touch encounter/patient/measured_by here because they are not in the form
def clean(self):
cleaned_data = super().clean()
systolic_bp = cleaned_data.get('systolic_bp')
diastolic_bp = cleaned_data.get('diastolic_bp')
if (systolic_bp and not diastolic_bp) or (diastolic_bp and not systolic_bp):
self.add_error('systolic_bp', 'Both systolic and diastolic values must be provided')
self.add_error('diastolic_bp', 'Both systolic and diastolic values must be provided')
if systolic_bp and diastolic_bp and systolic_bp <= diastolic_bp:
self.add_error('systolic_bp', 'Systolic pressure must be greater than diastolic pressure')
self.add_error('diastolic_bp', 'Systolic pressure must be greater than diastolic pressure')
oxygen_delivery = cleaned_data.get('oxygen_delivery')
oxygen_flow_rate = cleaned_data.get('oxygen_flow_rate')
# Ensure the constant matches your choices exactly
ROOM_AIR = 'ROOM_AIR' # change to 'room_air' if thats what your choices use
if oxygen_delivery and oxygen_delivery != ROOM_AIR and not oxygen_flow_rate:
self.add_error('oxygen_flow_rate', 'Flow rate is required for this oxygen delivery method')
return cleaned_data
class ProblemListForm(forms.ModelForm):
"""
Form for creating and updating problems.
"""
class Meta:
model = ProblemList
fields = [
'patient', 'problem_name', 'problem_code', 'coding_system',
'problem_type', 'onset_date', 'onset_description',
'severity', 'priority', 'status',
'resolution_date', 'resolution_notes',
'diagnosing_provider', 'managing_provider',
'related_encounter', 'body_site', 'laterality',
'clinical_notes', 'patient_concerns',
'treatment_goals', 'outcome_measures',
'verified', 'verified_by'
]
widgets = {
'onset_date': forms.DateInput(attrs={'type': 'date'}),
'resolution_date': forms.DateInput(attrs={'type': 'date'}),
'clinical_notes': forms.Textarea(attrs={'rows': 3}),
'patient_concerns': forms.Textarea(attrs={'rows': 3}),
'resolution_notes': forms.Textarea(attrs={'rows': 3}),
'treatment_goals': forms.Textarea(attrs={'rows': 3}),
'outcome_measures': forms.Textarea(attrs={'rows': 3}),
}
def __init__(self, *args, **kwargs):
self.tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
# Filter related objects by tenant
if self.tenant:
self.fields['patient'].queryset = self.fields['patient'].queryset.filter(tenant=self.tenant)
self.fields['diagnosing_provider'].queryset = User.objects.filter(tenant=self.tenant)
self.fields['managing_provider'].queryset = User.objects.filter(tenant=self.tenant)
self.fields['verified_by'].queryset = User.objects.filter(tenant=self.tenant)
self.fields['related_encounter'].queryset = Encounter.objects.filter(tenant=self.tenant)
def clean(self):
cleaned_data = super().clean()
status = cleaned_data.get('status')
resolution_date = cleaned_data.get('resolution_date')
# Validate resolution date is provided for resolved problems
if status in ['RESOLVED', 'REMISSION'] and not resolution_date:
self.add_error('resolution_date', 'Resolution date is required for resolved problems')
# Validate resolution date is not in the future
if resolution_date and resolution_date > timezone.now().date():
self.add_error('resolution_date', 'Resolution date cannot be in the future')
# Validate onset date is not in the future
onset_date = cleaned_data.get('onset_date')
if onset_date and onset_date > timezone.now().date():
self.add_error('onset_date', 'Onset date cannot be in the future')
# Validate resolution date is after onset date
if onset_date and resolution_date and resolution_date < onset_date:
self.add_error('resolution_date', 'Resolution date must be after onset date')
return cleaned_data
class CarePlanForm(forms.ModelForm):
"""
Form for creating and updating care plans.
"""
class Meta:
model = CarePlan
fields = [
'patient', 'title', 'description', 'plan_type', 'category',
'start_date', 'end_date', 'target_completion_date',
'status', 'priority', 'primary_provider', 'care_team',
'related_problems', 'goals', 'objectives',
'interventions', 'activities',
'monitoring_parameters', 'evaluation_criteria',
'patient_goals', 'patient_preferences', 'patient_barriers',
'resources_needed', 'support_systems',
'progress_notes', 'last_reviewed', 'next_review_date',
'outcomes_achieved', 'completion_percentage',
'approved', 'approved_by'
]
widgets = {
'plan_type': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'category': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'patient': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'title': forms.TextInput(attrs={'class': 'form-control form-control-sm'}),
'priority': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'status': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'primary_provider': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'care_team': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'completion_percentage': forms.NumberInput(attrs={'class': 'form-control form-control-sm'}),
'approved': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'approved_by': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'related_problems': forms.SelectMultiple(attrs={'class': 'form-control form-control-sm'}),
'start_date': forms.DateInput(attrs={'type': 'date', 'class':'form-control form control-sm'}),
'end_date': forms.DateInput(attrs={'type': 'date', 'class':'form-control form control-sm'}),
'target_completion_date': forms.DateInput(attrs={'type': 'date', 'class':'form-control form control-sm'}),
'last_reviewed': forms.DateInput(attrs={'type': 'date', 'class':'form-control form control-sm'}),
'next_review_date': forms.DateInput(attrs={'type': 'date', 'class':'form-control form control-sm'}),
'description': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
'patient_goals': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
'patient_preferences': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
'patient_barriers': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
'progress_notes': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
'goals': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
'objectives': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
'interventions': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
'activities': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
'monitoring_parameters': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
'evaluation_criteria': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
'resources_needed': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
'support_systems': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
'outcomes_achieved': forms.Textarea(attrs={'rows': 3, 'class':'form-control form control-sm'}),
}
def __init__(self, *args, **kwargs):
self.tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
# Filter related objects by tenant
if self.tenant:
self.fields['patient'].queryset = self.fields['patient'].queryset.filter(tenant=self.tenant)
self.fields['primary_provider'].queryset = User.objects.filter(tenant=self.tenant)
self.fields['care_team'].queryset = User.objects.filter(tenant=self.tenant)
self.fields['approved_by'].queryset = User.objects.filter(tenant=self.tenant)
self.fields['related_problems'].queryset = ProblemList.objects.filter(tenant=self.tenant)
def clean(self):
cleaned_data = super().clean()
start_date = cleaned_data.get('start_date')
end_date = cleaned_data.get('end_date')
target_completion_date = cleaned_data.get('target_completion_date')
# Validate end_date is after start_date
if start_date and end_date and end_date < start_date:
self.add_error('end_date', 'End date must be after start date')
# Validate target_completion_date is after start_date
if start_date and target_completion_date and target_completion_date < start_date:
self.add_error('target_completion_date', 'Target completion date must be after start date')
# Validate completion percentage
completion_percentage = cleaned_data.get('completion_percentage')
status = cleaned_data.get('status')
if status == 'COMPLETED' and completion_percentage < 100:
self.add_error('completion_percentage', 'Completion percentage must be 100% for completed care plans')
if status == 'DRAFT' and completion_percentage > 0:
self.add_error('completion_percentage', 'Completion percentage must be 0% for draft care plans')
return cleaned_data
class CarePlanProgressForm(forms.ModelForm):
"""
Restricted form used to update progress on an existing care plan
without touching identity/scope fields (patient, goals, etc.).
"""
class Meta:
model = CarePlan
fields = [
'status',
'completion_percentage',
'progress_notes',
'outcomes_achieved',
'evaluation_criteria',
'monitoring_parameters',
'last_reviewed',
'next_review_date',
]
widgets = {
'status': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'completion_percentage': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'min': 0, 'max': 100, 'step': 1,
}),
'progress_notes': forms.Textarea(attrs={'rows': 4, 'class': 'form-control form-control-sm'}),
'outcomes_achieved': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}),
'evaluation_criteria': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}),
'monitoring_parameters': forms.Textarea(attrs={'rows': 3, 'class': 'form-control form-control-sm'}),
'last_reviewed': forms.DateInput(attrs={'type': 'date', 'class': 'form-control form-control-sm'}),
'next_review_date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control form-control-sm'}),
}
def __init__(self, *args, **kwargs):
# Accept tenant for potential future use; not strictly needed here.
self.tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
# Sensible default: prefill last_reviewed with today if empty
if not self.instance.pk and not self.fields['last_reviewed'].initial:
self.fields['last_reviewed'].initial = timezone.now().date()
def clean(self):
cleaned = super().clean()
status = cleaned.get('status')
pct = cleaned.get('completion_percentage')
# Range check
if pct is not None and not (0 <= pct <= 100):
self.add_error('completion_percentage', 'Completion must be between 0 and 100.')
# Status / % consistency
if status == 'COMPLETED' and (pct is None or pct < 100):
self.add_error('completion_percentage', 'Completion must be 100% when status is COMPLETED.')
if status == 'DRAFT' and pct and pct > 0:
self.add_error('completion_percentage', 'Draft care plans must have 0% completion.')
# Dates sanity: next_review_date should not be before last_reviewed
last_rev = cleaned.get('last_reviewed')
next_rev = cleaned.get('next_review_date')
if last_rev and next_rev and next_rev < last_rev:
self.add_error('next_review_date', 'Next review date must be on/after Last reviewed.')
return cleaned
class ClinicalNoteForm(forms.ModelForm):
"""
Form for creating and updating clinical notes.
"""
class Meta:
model = ClinicalNote
fields = [
'note_type', 'title', 'content',
'template', 'structured_data', 'co_signers',
'status', 'note_datetime',
'electronically_signed', 'signed_datetime', 'signature_method',
'amended_note', 'amendment_reason',
'confidential', 'restricted_access', 'access_restrictions',
'related_problems', 'related_care_plans'
]
widgets = {
'note_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
'signed_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
'content': forms.Textarea(attrs={'rows': 10, 'class': 'note-editor'}),
'amendment_reason': forms.Textarea(attrs={'rows': 3}),
'structured_data': forms.Textarea(attrs={'rows': 3}),
'access_restrictions': forms.Textarea(attrs={'rows': 3}),
}
def __init__(self, *args, **kwargs):
self.tenant = kwargs.pop('tenant', None)
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
# Filter related objects by tenant
if self.tenant:
# self.fields['encounter'].queryset = Encounter.objects.filter(tenant=self.tenant)
# self.fields['patient'].queryset = self.fields['patient'].queryset.filter(tenant=self.tenant)
# self.fields['author'].queryset = User.objects.filter(tenant=self.tenant)
self.fields['co_signers'].queryset = User.objects.filter(tenant=self.tenant)
self.fields['template'].queryset = NoteTemplate.objects.filter(tenant=self.tenant, is_active=True)
self.fields['amended_note'].queryset = ClinicalNote.objects.filter(
patient__tenant=self.tenant
).exclude(pk=self.instance.pk if self.instance.pk else None)
self.fields['related_problems'].queryset = ProblemList.objects.filter(tenant=self.tenant)
self.fields['related_care_plans'].queryset = CarePlan.objects.filter(tenant=self.tenant)
def clean(self):
cleaned_data = super().clean()
status = cleaned_data.get('status')
electronically_signed = cleaned_data.get('electronically_signed')
signed_datetime = cleaned_data.get('signed_datetime')
# Validate signature information
if status == 'SIGNED' and not electronically_signed:
self.add_error('electronically_signed', 'Note must be electronically signed if status is Signed')
if electronically_signed and not signed_datetime:
cleaned_data['signed_datetime'] = timezone.now()
# Validate amendment information
amended_note = cleaned_data.get('amended_note')
amendment_reason = cleaned_data.get('amendment_reason')
if amended_note and not amendment_reason:
self.add_error('amendment_reason', 'Amendment reason is required when amending a note')
return cleaned_data
class NoteTemplateForm(forms.ModelForm):
"""
Form for creating and updating note templates.
"""
class Meta:
model = NoteTemplate
fields = [
'name', 'description', 'note_type', 'specialty',
'template_content', 'structured_fields',
'is_active', 'is_default', 'version',
'previous_version', 'quality_indicators', 'compliance_requirements'
]
widgets = {
'description': forms.Textarea(attrs={'rows': 3}),
'template_content': forms.Textarea(attrs={'rows': 10, 'class': 'template-editor'}),
'structured_fields': forms.Textarea(attrs={'rows': 5}),
'quality_indicators': forms.Textarea(attrs={'rows': 3}),
'compliance_requirements': forms.Textarea(attrs={'rows': 3}),
}
def __init__(self, *args, **kwargs):
self.tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
# Filter related objects by tenant
if self.tenant:
self.fields['previous_version'].queryset = NoteTemplate.objects.filter(
tenant=self.tenant
).exclude(pk=self.instance.pk if self.instance.pk else None)
def clean(self):
cleaned_data = super().clean()
is_default = cleaned_data.get('is_default')
note_type = cleaned_data.get('note_type')
specialty = cleaned_data.get('specialty')
# Validate template content
template_content = cleaned_data.get('template_content')
if not template_content or len(template_content.strip()) < 10:
self.add_error('template_content', 'Template content is too short')
# Validate version format
version = cleaned_data.get('version')
if version:
import re
if not re.match(r'^\d+\.\d+$', version):
self.add_error('version', 'Version must be in format X.Y (e.g., 1.0)')
return cleaned_data