Marwan Alwali 610e165e17 update
2025-09-04 19:19:52 +03:00

681 lines
29 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.

"""
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
import json, re
from accounts.models import User
from patients.models import PatientProfile
from .models import (
OperatingRoom, ORBlock, SurgicalCase, SurgicalNote,
EquipmentUsage, SurgicalNoteTemplate
)
# ============================================================================
# OPERATING ROOM FORMS
# ============================================================================
def _list_to_text(value):
if isinstance(value, list):
return "\n".join(str(v) for v in value)
return "" if value in (None, "", []) else str(value)
def _text_to_list(value):
if value is None:
return []
raw = str(value).strip()
if not raw:
return []
# Try JSON list first
try:
parsed = json.loads(raw)
if isinstance(parsed, list):
return [str(x).strip() for x in parsed if str(x).strip()]
except Exception:
pass
# Fallback: newline / comma split
items = []
for line in raw.splitlines():
for piece in line.split(","):
p = piece.strip()
if p:
items.append(p)
return items
class OperatingRoomForm(forms.ModelForm):
"""
Full form aligned to the panel template:
- exposes environment, capabilities, scheduling fields
- maps equipment_list & special_features (JSON) <-> textarea
- enforces tenant-scoped uniqueness of room_number
- validates temp/humidity ranges, and simple numeric sanity checks
"""
class Meta:
model = OperatingRoom
fields = [
# Basic
'room_number', 'room_name', 'room_type', 'status',
# Physical
'floor_number', 'building', 'wing', 'room_size', 'ceiling_height',
# Environment
'temperature_min', 'temperature_max',
'humidity_min', 'humidity_max',
'air_changes_per_hour', 'positive_pressure',
# Capabilities & equipment
'supports_robotic', 'supports_laparoscopic',
'supports_microscopy', 'supports_laser',
'has_c_arm', 'has_ct', 'has_mri', 'has_ultrasound', 'has_neuromonitoring',
'equipment_list', 'special_features',
# Scheduling / staffing
'max_case_duration', 'turnover_time', 'cleaning_time',
'required_nurses', 'required_techs',
'is_active', 'accepts_emergency',
]
widgets = {
# Basic
'room_number': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., OR-01', 'required': True}),
'room_name': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., Main Operating Room 1', 'required': True}),
'room_type': forms.Select(attrs={'class': 'form-control', 'required': True}),
'status': forms.Select(attrs={'class': 'form-control', 'required': True}),
# Physical
'floor_number': forms.NumberInput(attrs={'class': 'form-control', 'min': 0, 'max': 200, 'required': True}),
'building': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., Main Building'}),
'wing': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'e.g., East Wing'}),
'room_size': forms.NumberInput(attrs={'class': 'form-control', 'min': 1, 'max': 2000}),
'ceiling_height': forms.NumberInput(attrs={'class': 'form-control', 'min': 1, 'max': 20}),
# Environment
'temperature_min': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
'temperature_max': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
'humidity_min': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
'humidity_max': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1'}),
'air_changes_per_hour':forms.NumberInput(attrs={'class': 'form-control', 'min': 0, 'max': 120}),
'positive_pressure': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
# Capabilities & imaging
'supports_robotic': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'supports_laparoscopic':forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'supports_microscopy': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'supports_laser': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'has_c_arm': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'has_ct': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'has_mri': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'has_ultrasound': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'has_neuromonitoring': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'equipment_list': forms.Textarea(attrs={'class': 'form-control', 'rows': 4, 'placeholder': 'One per line, or comma-separated…'}),
'special_features': forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Hybrid OR, laminar flow, etc.'}),
# Scheduling / staffing
'max_case_duration': forms.NumberInput(attrs={'class': 'form-control', 'min': 30, 'max': 1440}),
'turnover_time': forms.NumberInput(attrs={'class': 'form-control', 'min': 0, 'max': 240}),
'cleaning_time': forms.NumberInput(attrs={'class': 'form-control', 'min': 0, 'max': 240}),
'required_nurses': forms.NumberInput(attrs={'class': 'form-control', 'min': 0, 'max': 20}),
'required_techs': forms.NumberInput(attrs={'class': 'form-control', 'min': 0, 'max': 20}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'accepts_emergency':forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}
help_texts = {
'room_number': 'Unique identifier (per tenant). Letters, numbers, dashes.',
'room_size': 'Square meters.',
'temperature_min': 'Typical ORs: 1826°C.',
'humidity_min': 'Typical ORs: 3060%.',
'air_changes_per_hour': '20+ is common in OR standards.',
}
def __init__(self, *args, **kwargs):
self.tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
if self.instance and self.instance.pk:
self.fields['equipment_list'].initial = _list_to_text(self.instance.equipment_list)
self.fields['special_features'].initial = _list_to_text(self.instance.special_features)
# JSONField <-> textarea mapping
def clean_equipment_list(self):
return _text_to_list(self.cleaned_data.get('equipment_list'))
def clean_special_features(self):
return _text_to_list(self.cleaned_data.get('special_features'))
def clean_room_number(self):
room_number = (self.cleaned_data.get('room_number') or '').strip()
if not room_number:
return room_number
if not re.match(r'^[A-Za-z0-9\-]+$', room_number):
raise ValidationError('Room number may contain only letters, numbers, and dashes.')
# tenant-scoped uniqueness
qs = OperatingRoom.objects.all()
tenant = self.tenant or getattr(self.instance, 'tenant', None)
if tenant is not None:
qs = qs.filter(tenant=tenant)
qs = qs.filter(room_number__iexact=room_number)
if self.instance.pk:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise ValidationError('Room number must be unique within the tenant.')
return room_number
def clean(self):
cleaned = super().clean()
# Temperature/humidity ranges
tmin, tmax = cleaned.get('temperature_min'), cleaned.get('temperature_max')
hmin, hmax = cleaned.get('humidity_min'), cleaned.get('humidity_max')
if tmin is not None and tmax is not None and tmin >= tmax:
self.add_error('temperature_max', 'Maximum temperature must be greater than minimum temperature.')
if hmin is not None and hmax is not None and hmin >= hmax:
self.add_error('humidity_max', 'Maximum humidity must be greater than minimum humidity.')
# Simple sanity checks
for field, minv in [('max_case_duration', 1), ('turnover_time', 0), ('cleaning_time', 0)]:
v = cleaned.get(field)
if v is not None and v < minv:
self.add_error(field, f'{field.replace("_", " ").title()} must be ≥ {minv}.')
return cleaned
# ============================================================================
# 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