2025-08-12 13:33:25 +03:00

616 lines
24 KiB
Python

"""
Operating Theatre app forms for CRUD operations.
Implements healthcare-appropriate form validation and business rules.
"""
from django import forms
from django.core.exceptions import ValidationError
from django.utils import timezone
from datetime import datetime, time, timedelta
from accounts.models import User
from patients.models import PatientProfile
from .models import (
OperatingRoom, ORBlock, SurgicalCase, SurgicalNote,
EquipmentUsage, SurgicalNoteTemplate
)
# ============================================================================
# OPERATING ROOM FORMS
# ============================================================================
class OperatingRoomForm(forms.ModelForm):
"""
Form for creating and updating operating rooms.
"""
class Meta:
model = OperatingRoom
fields = [
'room_number', 'room_name', 'room_type', 'floor_number', 'building',
'room_size', 'equipment_list', 'special_features',
'status', 'is_active', 'wing'
]
widgets = {
'room_number': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., OR-01'
}),
'room_name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Main Operating Room 1'
}),
'room_type': forms.Select(attrs={'class': 'form-control'}),
'floor_number': forms.NumberInput(attrs={
'class': 'form-control',
'min': 1,
'max': 50
}),
'building': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Main Building'
}),
'room_size': forms.NumberInput(attrs={
'class': 'form-control',
'min': 1,
'max': 1000
}),
'equipment_list': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'List available equipment...'
}),
'special_features': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Special capabilities and features...'
}),
'status': forms.Select(attrs={'class': 'form-control'}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'wing': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., East Wing'
}),
}
help_texts = {
'room_number': 'Unique identifier for the operating room',
'room_type': 'Type of procedures this room is designed for',
'room_size': 'Size of the room in square meters',
'equipment_list': 'List of permanently installed equipment',
'special_features': 'Special features like imaging, robotics, etc.',
}
def clean_room_number(self):
room_number = self.cleaned_data['room_number']
# Check for uniqueness within tenant
queryset = OperatingRoom.objects.filter(room_number=room_number)
if self.instance.pk:
queryset = queryset.exclude(pk=self.instance.pk)
if queryset.exists():
raise ValidationError('Room number must be unique.')
return room_number
def clean(self):
cleaned_data = super().clean()
status = cleaned_data.get('status')
is_active = cleaned_data.get('is_active')
# Validate status and active state consistency
if not is_active and status not in ['OUT_OF_SERVICE', 'MAINTENANCE']:
raise ValidationError(
'Inactive rooms must have status "Out of Service" or "Maintenance".'
)
return cleaned_data
# ============================================================================
# OR BLOCK FORMS
# ============================================================================
class ORBlockForm(forms.ModelForm):
"""
Form for creating and updating OR blocks.
"""
class Meta:
model = ORBlock
fields = [
'operating_room', 'primary_surgeon', 'date', 'start_time', 'end_time',
'block_type', 'notes'
]
widgets = {
'operating_room': forms.Select(attrs={'class': 'form-control'}),
'primary_surgeon': forms.Select(attrs={'class': 'form-control'}),
'date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
'start_time': forms.TimeInput(attrs={
'class': 'form-control',
'type': 'time'
}),
'end_time': forms.TimeInput(attrs={
'class': 'form-control',
'type': 'time'
}),
'block_type': forms.Select(attrs={'class': 'form-control'}),
'notes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Block notes and special instructions...'
}),
}
help_texts = {
'date': 'Date for this OR block',
'start_time': 'Block start time',
'end_time': 'Block end time',
'block_type': 'Type of surgical block',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter operating rooms by tenant and active status
self.fields['operating_room'].queryset = OperatingRoom.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('room_number')
# Filter surgeons by tenant and role
self.fields['surgeon'].queryset = User.objects.filter(
tenant=user.tenant,
is_active=True,
role__in=['SURGEON', 'ATTENDING_PHYSICIAN']
).order_by('first_name', 'last_name')
def clean(self):
cleaned_data = super().clean()
date = cleaned_data.get('date')
start_time = cleaned_data.get('start_time')
end_time = cleaned_data.get('end_time')
operating_room = cleaned_data.get('operating_room')
# Validate date is not in the past
if date and date < timezone.now().date():
raise ValidationError('Block date cannot be in the past.')
# Validate time range
if start_time and end_time:
if start_time >= end_time:
raise ValidationError('End time must be after start time.')
# Check minimum block duration (30 minutes)
start_datetime = datetime.combine(timezone.now().date(), start_time)
end_datetime = datetime.combine(timezone.now().date(), end_time)
duration = end_datetime - start_datetime
if duration < timedelta(minutes=30):
raise ValidationError('Block duration must be at least 30 minutes.')
# Check for overlapping blocks
if date and start_time and end_time and operating_room:
overlapping_blocks = ORBlock.objects.filter(
operating_room=operating_room,
date=date,
start_time__lt=end_time,
end_time__gt=start_time
)
if self.instance.pk:
overlapping_blocks = overlapping_blocks.exclude(pk=self.instance.pk)
if overlapping_blocks.exists():
raise ValidationError(
f'This time slot overlaps with an existing block in {operating_room.room_number}.'
)
return cleaned_data
# ============================================================================
# SURGICAL CASE FORMS
# ============================================================================
class SurgicalCaseForm(forms.ModelForm):
"""
Form for creating and updating surgical cases.
"""
class Meta:
model = SurgicalCase
fields = [
'patient', 'primary_procedure', 'procedure_codes', 'primary_surgeon',
'anesthesiologist', 'or_block',
'scheduled_start', 'estimated_duration',
'case_type', 'anesthesia_type',
'diagnosis', 'diagnosis_codes', 'complications',
'clinical_notes', 'status'
]
widgets = {
'patient': forms.Select(attrs={'class': 'form-control'}),
'primary_procedure': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Laparoscopic Cholecystectomy'
}),
'primary_surgeon': forms.Select(attrs={'class': 'form-control'}),
'anesthesiologist': forms.Select(attrs={'class': 'form-control'}),
'or_block': forms.Select(attrs={'class': 'form-control'}),
'scheduled_start': forms.DateTimeInput(attrs={
'class': 'form-control',
'type': 'datetime-local'
}),
'estimated_duration': forms.NumberInput(attrs={
'class': 'form-control',
'min': 15,
'max': 1440,
'step': 15
}),
'case_type': forms.Select(attrs={'class': 'form-control'}),
'anesthesia_type': forms.Select(attrs={'class': 'form-control'}),
'diagnosis': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Primary diagnosis...'
}),
'diagnosis_codes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 2,
'placeholder': 'ICD-10 codes...'
}),
'complications': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Any complications encountered...'
}),
'clinical_notes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Additional case notes...'
}),
'status': forms.Select(attrs={'class': 'form-control'}),
}
help_texts = {
'procedure_name': 'Full name of the surgical procedure',
'procedure_code': 'CPT or ICD procedure code',
'estimated_duration_minutes': 'Estimated duration in minutes (15-minute increments)',
'priority': 'Case priority level',
'case_type': 'Type of surgical case',
'anesthesia_type': 'Type of anesthesia required',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter patients by tenant
self.fields['patient'].queryset = PatientProfile.objects.filter(
tenant=user.tenant
).order_by('last_name', 'first_name')
# Filter operating rooms by tenant and active status
self.fields['operating_room'].queryset = OperatingRoom.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('room_number')
# Filter medical staff by tenant and appropriate roles
surgeon_queryset = User.objects.filter(
tenant=user.tenant,
is_active=True,
role__in=['SURGEON', 'ATTENDING_PHYSICIAN']
).order_by('first_name', 'last_name')
anesthesiologist_queryset = User.objects.filter(
tenant=user.tenant,
is_active=True,
role='ANESTHESIOLOGIST'
).order_by('first_name', 'last_name')
self.fields['primary_surgeon'].queryset = surgeon_queryset
self.fields['assisting_surgeon'].queryset = surgeon_queryset
self.fields['anesthesiologist'].queryset = anesthesiologist_queryset
# Make assisting surgeon and anesthesiologist optional
self.fields['assisting_surgeon'].required = False
self.fields['anesthesiologist'].required = False
def clean_scheduled_start_time(self):
scheduled_start_time = self.cleaned_data['scheduled_start_time']
# Validate scheduled time is not in the past (allow some buffer for updates)
if scheduled_start_time and scheduled_start_time < timezone.now() - timedelta(hours=1):
raise ValidationError('Scheduled start time cannot be more than 1 hour in the past.')
return scheduled_start_time
def clean_estimated_duration_minutes(self):
duration = self.cleaned_data['estimated_duration_minutes']
if duration and (duration < 15 or duration > 1440):
raise ValidationError('Duration must be between 15 minutes and 24 hours.')
if duration and duration % 15 != 0:
raise ValidationError('Duration must be in 15-minute increments.')
return duration
def clean(self):
cleaned_data = super().clean()
scheduled_start_time = cleaned_data.get('scheduled_start_time')
operating_room = cleaned_data.get('operating_room')
estimated_duration = cleaned_data.get('estimated_duration_minutes')
priority = cleaned_data.get('priority')
# Check for room availability (basic conflict detection)
if scheduled_start_time and operating_room and estimated_duration:
estimated_end_time = scheduled_start_time + timedelta(minutes=estimated_duration)
# Check for overlapping cases (unless emergency)
if priority != 'EMERGENCY':
overlapping_cases = SurgicalCase.objects.filter(
operating_room=operating_room,
scheduled_start_time__lt=estimated_end_time,
status__in=['SCHEDULED', 'IN_PROGRESS']
)
# Add estimated end time filter for existing cases
for case in overlapping_cases:
if case.estimated_duration_minutes:
case_end_time = case.scheduled_start_time + timedelta(
minutes=case.estimated_duration_minutes
)
if case_end_time > scheduled_start_time:
if self.instance.pk != case.pk:
raise ValidationError(
f'Time slot conflicts with existing case in {operating_room.room_number}. '
f'Existing case: {case.procedure_name} '
f'({case.scheduled_start_time.strftime("%H:%M")} - '
f'{case_end_time.strftime("%H:%M")})'
)
return cleaned_data
# ============================================================================
# SURGICAL NOTE FORMS
# ============================================================================
class SurgicalNoteForm(forms.ModelForm):
"""
Form for creating surgical notes.
"""
class Meta:
model = SurgicalNote
fields = [
'surgical_case', 'template_used', 'surgeon', 'procedure_performed',
'findings', 'complications', 'postop_instructions'
]
widgets = {
'surgical_case': forms.Select(attrs={'class': 'form-control'}),
'template_used': forms.Select(attrs={'class': 'form-control'}),
'surgeon': forms.Select(attrs={'class': 'form-control'}),
'procedure_performed': forms.Textarea(attrs={
'class': 'form-control',
'rows': 10,
'placeholder': 'Enter procedure details...'
}),
'findings': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Surgical findings...'
}),
'complications': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Any complications encountered...'
}),
'postop_instructions': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Post-operative instructions...'
}),
}
help_texts = {
'template_used': 'Select a template to pre-populate content',
'surgeon': 'Primary surgeon for this note',
'procedure_performed': 'Detailed description of the procedure',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter surgical cases by tenant
self.fields['surgical_case'].queryset = SurgicalCase.objects.filter(
tenant=user.tenant
).select_related('patient').order_by('-scheduled_start_time')
# Filter templates by tenant and active status
self.fields['template'].queryset = SurgicalNoteTemplate.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('template_name')
# Make template optional
self.fields['template'].required = False
def clean_note_content(self):
note_content = self.cleaned_data['note_content']
if not note_content or len(note_content.strip()) < 10:
raise ValidationError('Note content must be at least 10 characters long.')
return note_content
# ============================================================================
# EQUIPMENT USAGE FORMS
# ============================================================================
class EquipmentUsageForm(forms.ModelForm):
"""
Form for creating and updating equipment usage records.
"""
class Meta:
model = EquipmentUsage
fields = [
'equipment_name', 'equipment_type', 'serial_number',
'surgical_case', 'start_time', 'end_time',
'quantity_used', 'notes'
]
widgets = {
'equipment_name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Da Vinci Robot #1'
}),
'equipment_type': forms.Select(attrs={'class': 'form-control'}),
'serial_number': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Equipment serial number'
}),
'surgical_case': forms.Select(attrs={'class': 'form-control'}),
'start_time': forms.DateTimeInput(attrs={
'class': 'form-control',
'type': 'datetime-local'
}),
'end_time': forms.DateTimeInput(attrs={
'class': 'form-control',
'type': 'datetime-local'
}),
'quantity_used': forms.NumberInput(attrs={
'class': 'form-control',
'min': 1
}),
'notes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Equipment usage notes...'
}),
}
help_texts = {
'equipment_name': 'Name or description of the equipment',
'equipment_type': 'Category of equipment',
'serial_number': 'Unique serial number for the equipment',
'start_time': 'When equipment usage started',
'end_time': 'When equipment usage ended (leave blank if still in use)',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter operating rooms by tenant
self.fields['operating_room'].queryset = OperatingRoom.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('room_number')
# Filter surgical cases by tenant
self.fields['surgical_case'].queryset = SurgicalCase.objects.filter(
tenant=user.tenant
).select_related('patient').order_by('-scheduled_start_time')
# Make surgical case optional
self.fields['surgical_case'].required = False
# Make end time optional
self.fields['end_time'].required = False
def clean(self):
cleaned_data = super().clean()
start_time = cleaned_data.get('start_time')
end_time = cleaned_data.get('end_time')
# Validate time range if both are provided
if start_time and end_time:
if start_time >= end_time:
raise ValidationError('End time must be after start time.')
# Check for reasonable duration (not more than 24 hours)
duration = end_time - start_time
if duration > timedelta(hours=24):
raise ValidationError('Equipment usage cannot exceed 24 hours.')
# Validate start time is not too far in the future
if start_time and start_time > timezone.now() + timedelta(hours=24):
raise ValidationError('Start time cannot be more than 24 hours in the future.')
return cleaned_data
# ============================================================================
# SURGICAL NOTE TEMPLATE FORMS
# ============================================================================
class SurgicalNoteTemplateForm(forms.ModelForm):
"""
Form for creating and updating surgical note templates.
"""
class Meta:
model = SurgicalNoteTemplate
fields = [
'name', 'specialty', 'procedure_type',
'description', 'findings_template', 'postop_instructions_template',
'is_active'
]
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Laparoscopic Cholecystectomy Template'
}),
'specialty': forms.Select(attrs={'class': 'form-control'}),
'procedure_type': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Laparoscopic Surgery'
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Template description and usage notes...'
}),
'findings_template': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Default findings template text...'
}),
'postop_instructions_template': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Default post-op instructions template...'
}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}
help_texts = {
'name': 'Descriptive name for this template',
'specialty': 'Medical specialty this template is for',
'procedure_type': 'Type of procedure this template covers',
'description': 'Template description and usage notes',
'is_active': 'Whether this template is available for use',
}
def clean_name(self):
name = self.cleaned_data['name']
# Check for uniqueness within tenant
queryset = SurgicalNoteTemplate.objects.filter(name=name)
if self.instance.pk:
queryset = queryset.exclude(pk=self.instance.pk)
if queryset.exists():
raise ValidationError('Template name must be unique.')
return name