483 lines
16 KiB
Python
483 lines
16 KiB
Python
"""
|
|
Radiology 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 (
|
|
ImagingOrder, ImagingStudy, ImagingSeries, DICOMImage,
|
|
RadiologyReport, ReportTemplate
|
|
)
|
|
from patients.models import PatientProfile
|
|
from accounts.models import User
|
|
|
|
|
|
class ReportTemplateForm(forms.ModelForm):
|
|
"""
|
|
Form for creating and updating radiology report templates.
|
|
"""
|
|
|
|
class Meta:
|
|
model = ReportTemplate
|
|
fields = [
|
|
'name', 'modality', 'body_part', 'description',
|
|
'findings_template', 'impression_template', 'is_active'
|
|
]
|
|
widgets = {
|
|
'description': forms.Textarea(attrs={'rows': 8}),
|
|
'findings_template': forms.Textarea(attrs={'rows': 5}),
|
|
'impression_template': forms.Textarea(attrs={'rows': 3}),
|
|
}
|
|
|
|
def clean_name(self):
|
|
name = self.cleaned_data['name']
|
|
|
|
# Check for duplicate template names within the same tenant
|
|
if self.instance.pk:
|
|
existing = ReportTemplate.objects.filter(
|
|
name=name,
|
|
tenant=self.instance.tenant
|
|
).exclude(pk=self.instance.pk)
|
|
else:
|
|
# For new instances, tenant will be set in the view
|
|
existing = ReportTemplate.objects.filter(name=name)
|
|
|
|
if existing.exists():
|
|
raise ValidationError('A template with this name already exists.')
|
|
|
|
return name
|
|
|
|
|
|
class ImagingOrderForm(forms.ModelForm):
|
|
"""
|
|
Form for creating imaging orders.
|
|
"""
|
|
|
|
class Meta:
|
|
model = ImagingOrder
|
|
fields = [
|
|
'patient', 'modality', 'study_description', 'body_part',
|
|
'clinical_indication', 'priority', 'contrast_required',
|
|
'special_instructions', 'clinical_history'
|
|
]
|
|
widgets = {
|
|
'clinical_indication': forms.Textarea(attrs={'rows': 3}),
|
|
'special_instructions': forms.Textarea(attrs={'rows': 2}),
|
|
'clinical_history': 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')
|
|
|
|
# Make urgent_reason required if priority is URGENT
|
|
if self.data and self.data.get('priority') == 'URGENT':
|
|
self.fields['urgent_reason'].required = True
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
priority = cleaned_data.get('priority')
|
|
urgent_reason = cleaned_data.get('urgent_reason')
|
|
contrast_required = cleaned_data.get('contrast_required')
|
|
modality = cleaned_data.get('modality')
|
|
|
|
# Validate urgent reason for urgent orders
|
|
if priority == 'URGENT' and not urgent_reason:
|
|
raise ValidationError('Urgent reason is required for urgent orders.')
|
|
|
|
# Validate contrast requirement for certain modalities
|
|
if modality in ['CT', 'MRI'] and contrast_required is None:
|
|
self.add_error(
|
|
'contrast_required',
|
|
f'Please specify if contrast is required for {modality} studies.'
|
|
)
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class ImagingStudyForm(forms.ModelForm):
|
|
"""
|
|
Form for creating imaging studies.
|
|
"""
|
|
|
|
class Meta:
|
|
model = ImagingStudy
|
|
fields = [
|
|
'imaging_order', 'study_datetime', 'radiologist', 'station_name',
|
|
'modality', 'study_description', 'body_part',
|
|
'clinical_indication', 'status'
|
|
]
|
|
widgets = {
|
|
'study_datetime': forms.DateTimeInput(
|
|
attrs={'type': 'datetime-local'},
|
|
format='%Y-%m-%dT%H:%M'
|
|
),
|
|
'clinical_indication': forms.Textarea(attrs={'rows': 3}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if user and hasattr(user, 'tenant'):
|
|
self.fields['order'].queryset = ImagingOrder.objects.filter(
|
|
tenant=user.tenant,
|
|
status__in=['PENDING', 'SCHEDULED']
|
|
).select_related('patient').order_by('-order_datetime')
|
|
|
|
self.fields['technologist'].queryset = User.objects.filter(
|
|
tenant=user.tenant,
|
|
groups__name__in=['Technologists', 'Radiologic Technologists']
|
|
).order_by('first_name', 'last_name')
|
|
|
|
# Set default study time to now
|
|
if not self.instance.pk:
|
|
self.fields['study_datetime'].initial = timezone.now()
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
contrast_used = cleaned_data.get('contrast_used')
|
|
contrast_agent = cleaned_data.get('contrast_agent')
|
|
contrast_volume_ml = cleaned_data.get('contrast_volume_ml')
|
|
study_datetime = cleaned_data.get('study_datetime')
|
|
|
|
# Validate contrast information
|
|
if contrast_used:
|
|
if not contrast_agent:
|
|
raise ValidationError('Contrast agent is required when contrast is used.')
|
|
if not contrast_volume_ml:
|
|
raise ValidationError('Contrast volume is required when contrast is used.')
|
|
|
|
# Validate study datetime
|
|
if study_datetime and study_datetime > timezone.now():
|
|
# Allow future scheduling but warn
|
|
pass
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class ImagingSeriesForm(forms.ModelForm):
|
|
"""
|
|
Form for creating imaging series.
|
|
"""
|
|
|
|
class Meta:
|
|
model = ImagingSeries
|
|
fields = [
|
|
'study', 'series_number', 'series_description', 'modality',
|
|
'body_part', 'slice_thickness', 'pixel_spacing',
|
|
'protocol_name', 'number_of_instances'
|
|
]
|
|
widgets = {
|
|
'slice_thickness': forms.NumberInput(attrs={'min': 0.1, 'step': 0.1}),
|
|
'pixel_spacing': forms.NumberInput(attrs={'min': 0.01, 'step': 0.01}),
|
|
'number_of_instances': forms.NumberInput(attrs={'min': 1}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if user and hasattr(user, 'tenant'):
|
|
self.fields['study'].queryset = ImagingStudy.objects.filter(
|
|
tenant=user.tenant,
|
|
status__in=['IN_PROGRESS', 'COMPLETED']
|
|
).select_related('order__patient').order_by('-study_datetime')
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
study = cleaned_data.get('study')
|
|
series_number = cleaned_data.get('series_number')
|
|
|
|
# Check for duplicate series numbers within the same study
|
|
if study and series_number:
|
|
existing = ImagingSeries.objects.filter(
|
|
study=study,
|
|
series_number=series_number
|
|
)
|
|
|
|
if self.instance.pk:
|
|
existing = existing.exclude(pk=self.instance.pk)
|
|
|
|
if existing.exists():
|
|
raise ValidationError('A series with this number already exists for this study.')
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class RadiologyReportForm(forms.ModelForm):
|
|
"""
|
|
Form for creating radiology reports.
|
|
"""
|
|
|
|
class Meta:
|
|
model = RadiologyReport
|
|
fields = [
|
|
'study', 'template_used', 'clinical_history', 'technique',
|
|
'findings', 'impression', 'recommendations',
|
|
'critical_finding', 'radiologist'
|
|
]
|
|
widgets = {
|
|
'clinical_history': forms.Textarea(attrs={'rows': 2}),
|
|
'technique': forms.Textarea(attrs={'rows': 3}),
|
|
'findings': forms.Textarea(attrs={'rows': 6}),
|
|
'impression': forms.Textarea(attrs={'rows': 3}),
|
|
'recommendations': 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['study'].queryset = ImagingStudy.objects.filter(
|
|
tenant=user.tenant,
|
|
status='COMPLETED'
|
|
).select_related('order__patient').order_by('-completed_datetime')
|
|
|
|
self.fields['template'].queryset = ReportTemplate.objects.filter(
|
|
tenant=user.tenant,
|
|
is_active=True
|
|
).order_by('template_name')
|
|
|
|
# Make critical findings description required if has_critical_findings is True
|
|
if self.data and self.data.get('has_critical_findings'):
|
|
self.fields['critical_findings_description'].required = True
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
has_critical_findings = cleaned_data.get('has_critical_findings')
|
|
critical_findings_description = cleaned_data.get('critical_findings_description')
|
|
study = cleaned_data.get('study')
|
|
|
|
# Validate critical findings
|
|
if has_critical_findings and not critical_findings_description:
|
|
raise ValidationError('Critical findings description is required when critical findings are present.')
|
|
|
|
# Check if report already exists for this study
|
|
if study:
|
|
existing = RadiologyReport.objects.filter(study=study)
|
|
if self.instance.pk:
|
|
existing = existing.exclude(pk=self.instance.pk)
|
|
|
|
if existing.exists():
|
|
raise ValidationError('A report already exists for this study.')
|
|
|
|
return cleaned_data
|
|
|
|
|
|
# Additional forms for search and filtering
|
|
|
|
class ImagingOrderSearchForm(forms.Form):
|
|
"""
|
|
Form for searching and filtering imaging orders.
|
|
"""
|
|
search = forms.CharField(
|
|
max_length=100,
|
|
required=False,
|
|
widget=forms.TextInput(attrs={
|
|
'placeholder': 'Search by patient name, MRN, or study...',
|
|
'class': 'form-control'
|
|
})
|
|
)
|
|
|
|
status = forms.ChoiceField(
|
|
choices=[('', 'All Statuses')] + ImagingOrder._meta.get_field('status').choices,
|
|
required=False,
|
|
widget=forms.Select(attrs={'class': 'form-control'})
|
|
)
|
|
|
|
modality = forms.ChoiceField(
|
|
choices=[('', 'All Modalities')] + ImagingOrder._meta.get_field('modality').choices,
|
|
required=False,
|
|
widget=forms.Select(attrs={'class': 'form-control'})
|
|
)
|
|
|
|
priority = forms.ChoiceField(
|
|
choices=[('', 'All Priorities')] + ImagingOrder._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 ImagingStudySearchForm(forms.Form):
|
|
"""
|
|
Form for searching and filtering imaging studies.
|
|
"""
|
|
search = forms.CharField(
|
|
max_length=100,
|
|
required=False,
|
|
widget=forms.TextInput(attrs={
|
|
'placeholder': 'Search by patient name, MRN, or study ID...',
|
|
'class': 'form-control'
|
|
})
|
|
)
|
|
|
|
status = forms.ChoiceField(
|
|
choices=[('', 'All Statuses')] + ImagingStudy._meta.get_field('status').choices,
|
|
required=False,
|
|
widget=forms.Select(attrs={'class': 'form-control'})
|
|
)
|
|
|
|
modality = forms.ChoiceField(
|
|
choices=[('', 'All Modalities')] + ImagingOrder._meta.get_field('modality').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 RadiologyReportSearchForm(forms.Form):
|
|
"""
|
|
Form for searching and filtering radiology reports.
|
|
"""
|
|
search = forms.CharField(
|
|
max_length=100,
|
|
required=False,
|
|
widget=forms.TextInput(attrs={
|
|
'placeholder': 'Search by patient name, MRN, findings, or impression...',
|
|
'class': 'form-control'
|
|
})
|
|
)
|
|
|
|
status = forms.ChoiceField(
|
|
choices=[('', 'All Statuses')] + RadiologyReport._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'})
|
|
)
|
|
|
|
|
|
class ReportTemplateSearchForm(forms.Form):
|
|
"""
|
|
Form for searching and filtering report templates.
|
|
"""
|
|
search = forms.CharField(
|
|
max_length=100,
|
|
required=False,
|
|
widget=forms.TextInput(attrs={
|
|
'placeholder': 'Search by template name, modality, or body part...',
|
|
'class': 'form-control'
|
|
})
|
|
)
|
|
|
|
modality = forms.ChoiceField(
|
|
choices=[('', 'All Modalities')] + ReportTemplate._meta.get_field('modality').choices,
|
|
required=False,
|
|
widget=forms.Select(attrs={'class': 'form-control'})
|
|
)
|
|
|
|
active_only = forms.BooleanField(
|
|
required=False,
|
|
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
|
|
)
|
|
|
|
|
|
# Quick action forms
|
|
|
|
class StudyScheduleForm(forms.Form):
|
|
"""
|
|
Form for scheduling imaging studies.
|
|
"""
|
|
scheduled_datetime = forms.DateTimeField(
|
|
widget=forms.DateTimeInput(
|
|
attrs={'type': 'datetime-local', 'class': 'form-control'},
|
|
format='%Y-%m-%dT%H:%M'
|
|
)
|
|
)
|
|
|
|
room_location = forms.CharField(
|
|
max_length=50,
|
|
required=False,
|
|
widget=forms.TextInput(attrs={'class': 'form-control'})
|
|
)
|
|
|
|
notes = forms.CharField(
|
|
required=False,
|
|
widget=forms.Textarea(attrs={'rows': 2, 'class': 'form-control'})
|
|
)
|
|
|
|
def clean_scheduled_datetime(self):
|
|
scheduled_datetime = self.cleaned_data['scheduled_datetime']
|
|
|
|
# Don't allow scheduling in the past (except for emergency cases)
|
|
if scheduled_datetime < timezone.now():
|
|
raise ValidationError('Cannot schedule studies in the past.')
|
|
|
|
return scheduled_datetime
|
|
|
|
|
|
class ReportSigningForm(forms.Form):
|
|
"""
|
|
Form for signing radiology reports.
|
|
"""
|
|
electronic_signature = forms.CharField(
|
|
max_length=100,
|
|
widget=forms.PasswordInput(attrs={
|
|
'class': 'form-control',
|
|
'placeholder': 'Enter your electronic signature'
|
|
})
|
|
)
|
|
|
|
signing_notes = forms.CharField(
|
|
required=False,
|
|
widget=forms.Textarea(attrs={
|
|
'rows': 2,
|
|
'class': 'form-control',
|
|
'placeholder': 'Optional signing notes'
|
|
})
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def clean_electronic_signature(self):
|
|
signature = self.cleaned_data['electronic_signature']
|
|
|
|
# Validate electronic signature (implement your signature validation logic)
|
|
if self.user and signature != self.user.username:
|
|
raise ValidationError('Invalid electronic signature.')
|
|
|
|
return signature
|
|
|