Marwan Alwali be70e47e22 update
2025-08-30 09:45:26 +03:00

390 lines
13 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.

"""
Laboratory app forms for healthcare-focused CRUD operations.
"""
from django import forms
from django.core.exceptions import ValidationError
from django.utils import timezone
from .models import (
LabTest, LabOrder, Specimen, LabResult, QualityControl, ReferenceRange
)
from patients.models import PatientProfile
from accounts.models import User
class LabTestForm(forms.ModelForm):
"""
Form for creating and updating laboratory tests.
"""
def __init__(self, *args, tenant=None, **kwargs):
"""
Optionally accept a `tenant` kwarg if you plan on using it for something.
Re-populates the `test_category` choices from the models CHOICES tuple.
"""
super().__init__(*args, **kwargs)
# Pull the static choices from the model
all_choices = LabTest.TEST_CATEGORY_CHOICES
# If you need to adjust per-tenant, do it here. E.g.:
# filtered = [c for c in all_choices if c[0] in tenant_allowed_codes]
# self.fields['test_category'].choices = filtered
self.fields['test_category'].choices = all_choices
class Meta:
model = LabTest
fields = [
'test_code', 'test_name', 'test_description', 'test_category',
'specimen_type', 'methodology', 'turnaround_time', 'department',
'fasting_required', 'is_active', 'collection_instructions'
]
widgets = {
'test_category': forms.Select(attrs={'class': 'form-select'}),
'specimen_type': forms.Select(attrs={'class': 'form-select'}),
'department': forms.Select(attrs={'class': 'form-select'}),
'test_description': forms.Textarea(attrs={'rows': 3}),
'collection_instructions': forms.Textarea(attrs={'rows': 3}),
'turnaround_time': forms.NumberInput(attrs={'min': 1, 'max': 168}),
}
def clean_test_code(self):
test_code = self.cleaned_data['test_code']
# Check for duplicate test codes within the same tenant
if self.instance.pk:
existing = LabTest.objects.filter(
test_code=test_code,
tenant=self.instance.tenant
).exclude(pk=self.instance.pk)
else:
# For new instances, tenant will be set in the view
existing = LabTest.objects.filter(test_code=test_code)
if existing.exists():
raise ValidationError('A test with this code already exists.')
return test_code.upper()
class ReferenceRangeForm(forms.ModelForm):
"""
Form for creating and updating reference ranges.
"""
class Meta:
model = ReferenceRange
fields = [
'test', 'gender', 'age_min', 'age_max', 'range_low',
'range_high', 'unit', 'range_text'
]
widgets = {
'range_text': forms.Textarea(attrs={'rows': 2}),
'age_min': forms.NumberInput(attrs={'min': 0, 'max': 120}),
'age_max': forms.NumberInput(attrs={'min': 0, 'max': 120}),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
self.fields['test'].queryset = LabTest.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('test_name')
def clean(self):
cleaned_data = super().clean()
age_min = cleaned_data.get('age_min')
age_max = cleaned_data.get('age_max')
range_low = cleaned_data.get('range_low')
range_high = cleaned_data.get('range_high')
if age_min is not None and age_max is not None:
if age_min > age_max:
raise ValidationError('Minimum age cannot be greater than maximum age.')
if range_low is not None and range_high is not None:
if range_low >= range_high:
raise ValidationError('Lower limit must be less than upper limit.')
return cleaned_data
class LabOrderForm(forms.ModelForm):
"""
Form for creating laboratory orders.
"""
class Meta:
model = LabOrder
fields = [
'patient', 'ordering_provider', 'priority', 'clinical_indication',
'fasting_status', 'special_instructions'
]
widgets = {
'clinical_indication': forms.Textarea(attrs={'rows': 3}),
'special_instructions': forms.Textarea(attrs={'rows': 2}),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
self.fields['patient'].queryset = PatientProfile.objects.filter(
tenant=user.tenant
).order_by('last_name', 'first_name')
self.fields['test'].queryset = LabTest.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('test_name')
def clean(self):
cleaned_data = super().clean()
test = cleaned_data.get('test')
fasting_status = cleaned_data.get('fasting_status')
# Auto-set fasting requirement based on test
if test and test.fasting_required and not fasting_status:
cleaned_data['fasting_status'] = True
self.add_error(
'fasting_status',
f'This test ({test.test_name}) requires fasting.'
)
return cleaned_data
class SpecimenForm(forms.ModelForm):
"""
Form for creating specimen records.
"""
class Meta:
model = Specimen
fields = [
'order', 'specimen_type', 'collected_datetime',
'collection_method', 'collection_site', 'volume',
'container_type', 'quality_notes',
]
widgets = {
'collected_datetime': forms.DateTimeInput(
attrs={'type': 'datetime-local'},
format='%Y-%m-%dT%H:%M'
),
'quality_notes': forms.Textarea(attrs={'rows': 2}),
'volume': forms.NumberInput(attrs={'min': 0.1, 'step': 0.1}),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
self.fields['order'].queryset = LabOrder.objects.filter(
tenant=user.tenant,
# status__in=['PENDING', 'SCHEDULED']
).select_related('patient', 'test').order_by('-order_datetime')
# Set default collection time to now
if not self.instance.pk:
self.fields['collected_datetime'].initial = timezone.now()
def clean_collected_datetime(self):
collected_datetime = self.cleaned_data['collected_datetime']
if collected_datetime > timezone.now():
raise ValidationError('Collection time cannot be in the future.')
# Check if collection time is too far in the past (more than 7 days)
seven_days_ago = timezone.now() - timezone.timedelta(days=7)
if collected_datetime < seven_days_ago:
raise ValidationError('Collection time cannot be more than 7 days ago.')
return collected_datetime
class LabResultForm(forms.ModelForm):
"""
Form for entering laboratory results.
"""
class Meta:
model = LabResult
fields = [
'order', 'test', 'result_value', 'result_unit',
'abnormal_flag', 'is_critical', 'pathologist_comments',
'technician_comments', 'analyzer'
]
widgets = {
'pathologist_comments': forms.Textarea(attrs={'rows': 3}),
'technician_comments': forms.Textarea(attrs={'rows': 2}),
'result_value': forms.NumberInput(attrs={'step': 'any'}),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
self.fields['order'].queryset = LabOrder.objects.filter(
tenant=user.tenant,
status='IN_PROGRESS'
).select_related('patient', 'test').order_by('-order_datetime')
self.fields['test'].queryset = LabTest.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('test_name')
def clean(self):
cleaned_data = super().clean()
order = cleaned_data.get('order')
test = cleaned_data.get('test')
result_value = cleaned_data.get('result_value')
result_text = cleaned_data.get('result_text')
# Ensure order and test match
if order and test and order.test != test:
raise ValidationError('Selected test must match the order test.')
# Ensure either numeric or text result is provided
if not result_value and not result_text:
raise ValidationError('Either numeric result value or text result must be provided.')
# Auto-populate test from order if not provided
if order and not test:
cleaned_data['test'] = order.test
return cleaned_data
class QualityControlForm(forms.ModelForm):
"""
Form for quality control records.
"""
class Meta:
model = QualityControl
fields = [
'test', 'control_level', 'target_value', 'observed_value',
'status', 'control_lot', 'analyzer', 'comments'
]
widgets = {
'run_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
'comments': forms.Textarea(attrs={'rows': 2}),
'target_value': forms.NumberInput(attrs={'step': 'any'}),
'observed_value': forms.NumberInput(attrs={'step': 'any'}),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
self.fields['test'].queryset = LabTest.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('test_name')
def clean(self):
cleaned_data = super().clean()
target_value = cleaned_data.get('target_value')
observed_value = cleaned_data.get('observed_value')
status = cleaned_data.get('status')
# Auto-determine status based on values if both are provided
if target_value is not None and observed_value is not None:
# Calculate percentage difference
if target_value != 0:
percent_diff = abs((observed_value - target_value) / target_value) * 100
# Auto-set status based on percentage difference
if percent_diff <= 5: # Within 5% is acceptable
if not status:
cleaned_data['status'] = 'PASS'
elif percent_diff <= 10: # 5-10% is warning
if not status:
cleaned_data['status'] = 'WARNING'
else: # >10% is fail
if not status:
cleaned_data['status'] = 'FAIL'
return cleaned_data
# Additional forms for search and filtering
class LabOrderSearchForm(forms.Form):
"""
Form for searching and filtering laboratory orders.
"""
search = forms.CharField(
max_length=100,
required=False,
widget=forms.TextInput(attrs={
'placeholder': 'Search by patient name, MRN, or test...',
'class': 'form-control'
})
)
status = forms.ChoiceField(
choices=[('', 'All Statuses')] + LabOrder._meta.get_field('status').choices,
required=False,
widget=forms.Select(attrs={'class': 'form-control'})
)
priority = forms.ChoiceField(
choices=[('', 'All Priorities')] + LabOrder._meta.get_field('priority').choices,
required=False,
widget=forms.Select(attrs={'class': 'form-control'})
)
date_from = forms.DateField(
required=False,
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})
)
date_to = forms.DateField(
required=False,
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})
)
class LabResultSearchForm(forms.Form):
"""
Form for searching and filtering laboratory results.
"""
search = forms.CharField(
max_length=100,
required=False,
widget=forms.TextInput(attrs={
'placeholder': 'Search by patient name, MRN, or test...',
'class': 'form-control'
})
)
status = forms.ChoiceField(
choices=[('', 'All Statuses')] + LabResult._meta.get_field('status').choices,
required=False,
widget=forms.Select(attrs={'class': 'form-control'})
)
critical_only = forms.BooleanField(
required=False,
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
date_from = forms.DateField(
required=False,
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})
)
date_to = forms.DateField(
required=False,
widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'})
)