2026-01-04 10:32:40 +03:00

414 lines
13 KiB
Python

"""
Observations forms - Public and internal forms for observation management.
"""
from django import forms
from django.core.validators import FileExtensionValidator
from django.utils import timezone
from apps.accounts.models import User
from apps.organizations.models import Department
from .models import (
Observation,
ObservationAttachment,
ObservationCategory,
ObservationNote,
ObservationSeverity,
ObservationStatus,
)
# Allowed file extensions for attachments
ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'xls', 'xlsx']
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
class ObservationPublicForm(forms.ModelForm):
"""
Public form for submitting observations.
Features:
- No login required
- Optional reporter information
- Honeypot field for spam protection
- File attachments support
"""
# Honeypot field for spam protection
website = forms.CharField(
required=False,
widget=forms.TextInput(attrs={
'style': 'display:none !important;',
'tabindex': '-1',
'autocomplete': 'off',
}),
label=''
)
# File attachments (handled separately in the view)
# Note: Multiple file upload is handled via HTML attribute and view processing
attachments = forms.FileField(
required=False,
widget=forms.FileInput(attrs={
'accept': '.jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.xls,.xlsx',
'class': 'form-control',
}),
help_text="Upload a file (max 10MB). Allowed: images, PDF, Word, Excel."
)
class Meta:
model = Observation
fields = [
'category',
'title',
'description',
'severity',
'location_text',
'incident_datetime',
'reporter_staff_id',
'reporter_name',
'reporter_phone',
'reporter_email',
]
widgets = {
'category': forms.Select(attrs={
'class': 'form-select',
}),
'title': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Brief title (optional)',
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 5,
'placeholder': 'Please describe what you observed in detail...',
}),
'severity': forms.Select(attrs={
'class': 'form-select',
}),
'location_text': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., Building A, Floor 2, Room 205',
}),
'incident_datetime': forms.DateTimeInput(attrs={
'class': 'form-control',
'type': 'datetime-local',
}),
'reporter_staff_id': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Your staff ID (optional)',
}),
'reporter_name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Your name (optional)',
}),
'reporter_phone': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Your phone number (optional)',
}),
'reporter_email': forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'Your email (optional)',
}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Only show active categories
self.fields['category'].queryset = ObservationCategory.objects.filter(
is_active=True
).order_by('sort_order', 'name_en')
self.fields['category'].empty_label = "Select a category (optional)"
# Set default incident datetime to now
if not self.instance.pk:
self.initial['incident_datetime'] = timezone.now().strftime('%Y-%m-%dT%H:%M')
def clean_website(self):
"""Honeypot validation - should be empty."""
website = self.cleaned_data.get('website')
if website:
raise forms.ValidationError("Spam detected.")
return website
def clean_description(self):
"""Validate description is not too short."""
description = self.cleaned_data.get('description', '')
if len(description.strip()) < 10:
raise forms.ValidationError(
"Please provide a more detailed description (at least 10 characters)."
)
return description
class ObservationTriageForm(forms.Form):
"""
Form for triaging observations.
Used by PX360 staff to assign department, owner, and update status.
"""
assigned_department = forms.ModelChoiceField(
queryset=Department.objects.filter(status='active'),
required=False,
empty_label="Select department",
widget=forms.Select(attrs={'class': 'form-select'})
)
assigned_to = forms.ModelChoiceField(
queryset=User.objects.filter(is_active=True),
required=False,
empty_label="Select assignee",
widget=forms.Select(attrs={'class': 'form-select'})
)
status = forms.ChoiceField(
choices=ObservationStatus.choices,
widget=forms.Select(attrs={'class': 'form-select'})
)
note = forms.CharField(
required=False,
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Add a note about this triage action (optional)...',
})
)
def __init__(self, *args, hospital=None, **kwargs):
super().__init__(*args, **kwargs)
# Filter departments by hospital if provided
if hospital:
self.fields['assigned_department'].queryset = Department.objects.filter(
hospital=hospital,
status='active'
)
self.fields['assigned_to'].queryset = User.objects.filter(
is_active=True,
hospital=hospital
)
class ObservationStatusForm(forms.Form):
"""
Form for changing observation status.
"""
status = forms.ChoiceField(
choices=ObservationStatus.choices,
widget=forms.Select(attrs={'class': 'form-select'})
)
comment = forms.CharField(
required=False,
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Add a comment about this status change (optional)...',
})
)
class ObservationNoteForm(forms.ModelForm):
"""
Form for adding internal notes to observations.
"""
class Meta:
model = ObservationNote
fields = ['note', 'is_internal']
widgets = {
'note': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Add your note here...',
}),
'is_internal': forms.CheckboxInput(attrs={
'class': 'form-check-input',
}),
}
class ObservationCategoryForm(forms.ModelForm):
"""
Form for managing observation categories.
"""
class Meta:
model = ObservationCategory
fields = ['name_en', 'name_ar', 'description', 'icon', 'sort_order', 'is_active']
widgets = {
'name_en': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Category name in English',
}),
'name_ar': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Category name in Arabic',
'dir': 'rtl',
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Optional description...',
}),
'icon': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'e.g., bi-exclamation-triangle',
}),
'sort_order': forms.NumberInput(attrs={
'class': 'form-control',
'min': 0,
}),
'is_active': forms.CheckboxInput(attrs={
'class': 'form-check-input',
}),
}
class ObservationFilterForm(forms.Form):
"""
Form for filtering observations in the list view.
"""
search = forms.CharField(
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Search by tracking code, description...',
})
)
status = forms.ChoiceField(
required=False,
choices=[('', 'All Statuses')] + list(ObservationStatus.choices),
widget=forms.Select(attrs={'class': 'form-select'})
)
severity = forms.ChoiceField(
required=False,
choices=[('', 'All Severities')] + list(ObservationSeverity.choices),
widget=forms.Select(attrs={'class': 'form-select'})
)
category = forms.ModelChoiceField(
required=False,
queryset=ObservationCategory.objects.filter(is_active=True),
empty_label="All Categories",
widget=forms.Select(attrs={'class': 'form-select'})
)
assigned_department = forms.ModelChoiceField(
required=False,
queryset=Department.objects.filter(status='active'),
empty_label="All Departments",
widget=forms.Select(attrs={'class': 'form-select'})
)
assigned_to = forms.ModelChoiceField(
required=False,
queryset=User.objects.filter(is_active=True),
empty_label="All Assignees",
widget=forms.Select(attrs={'class': 'form-select'})
)
date_from = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
})
)
date_to = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
})
)
is_anonymous = forms.ChoiceField(
required=False,
choices=[
('', 'All'),
('yes', 'Anonymous Only'),
('no', 'Identified Only'),
],
widget=forms.Select(attrs={'class': 'form-select'})
)
class ConvertToActionForm(forms.Form):
"""
Form for converting an observation to a PX Action.
"""
title = forms.CharField(
max_length=500,
widget=forms.TextInput(attrs={
'class': 'form-control',
})
)
description = forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
})
)
category = forms.ChoiceField(
choices=[
('clinical_quality', 'Clinical Quality'),
('patient_safety', 'Patient Safety'),
('service_quality', 'Service Quality'),
('staff_behavior', 'Staff Behavior'),
('facility', 'Facility & Environment'),
('process_improvement', 'Process Improvement'),
('other', 'Other'),
],
widget=forms.Select(attrs={'class': 'form-select'})
)
priority = forms.ChoiceField(
choices=[
('low', 'Low'),
('medium', 'Medium'),
('high', 'High'),
('critical', 'Critical'),
],
widget=forms.Select(attrs={'class': 'form-select'})
)
assigned_department = forms.ModelChoiceField(
required=False,
queryset=Department.objects.filter(status='active'),
empty_label="Select department",
widget=forms.Select(attrs={'class': 'form-select'})
)
assigned_to = forms.ModelChoiceField(
required=False,
queryset=User.objects.filter(is_active=True),
empty_label="Select assignee",
widget=forms.Select(attrs={'class': 'form-select'})
)
class ObservationTrackForm(forms.Form):
"""
Form for tracking an observation by its tracking code.
"""
tracking_code = forms.CharField(
max_length=20,
widget=forms.TextInput(attrs={
'class': 'form-control form-control-lg',
'placeholder': 'Enter your tracking code (e.g., OBS-ABC123)',
'autofocus': True,
})
)
def clean_tracking_code(self):
"""Normalize tracking code."""
code = self.cleaned_data.get('tracking_code', '').strip().upper()
if not code:
raise forms.ValidationError("Please enter a tracking code.")
return code