""" 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