""" 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 django.utils.translation import gettext_lazy as _ from apps.accounts.models import User from apps.organizations.models import Department from .models import ( Observation, ObservationAttachment, ObservationCategory, ObservationNote, ObservationSeverity, ObservationStatus, ObservationSubCategory, ) # 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 = [ "hospital", "category", "title", "description", "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...", } ), "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 ObservationInternalForm(forms.ModelForm): """ Internal form for authenticated staff to create observations. Differences from ObservationPublicForm: - No honeypot field (authenticated users) - No reporter fields (auto-filled from request.user) - Includes assignment fields (department, assignee) - Source field for PXSource selection """ 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 files (max 10MB each). Allowed: images, PDF, Word, Excel.", ) description_en = forms.CharField( required=False, widget=forms.Textarea(attrs={"class": "form-control", "rows": 5, "placeholder": "English description of the observation..."}), help_text="Full English version of the observation description.", ) class Meta: model = Observation fields = [ "category", "sub_category", "title", "description", "description_en", "location_text", "location", "main_section", "subsection", "incident_datetime", "patient_file_number", "assigned_department", "assigned_to", "person_noted", "department_noted", "communication_method", "communication_datetime", "px_source", ] widgets = { "category": forms.Select( attrs={ "class": "form-select", } ), "sub_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...", } ), "description_en": forms.Textarea( attrs={ "class": "form-control", "rows": 5, "placeholder": "English description...", } ), "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", } ), "patient_file_number": forms.TextInput( attrs={ "class": "form-control", "placeholder": "Patient MRN / file number", } ), "person_noted": forms.TextInput( attrs={ "class": "form-control", "placeholder": "Person who was informed about this", } ), "communication_method": forms.TextInput( attrs={ "class": "form-control", "placeholder": "e.g., extension, mobile, in-person", } ), "communication_datetime": forms.DateTimeInput( attrs={ "class": "form-control", "type": "datetime-local", } ), "assigned_department": forms.Select( attrs={ "class": "form-select", } ), "assigned_to": forms.Select( attrs={ "class": "form-select", } ), "department_noted": forms.Select( attrs={ "class": "form-select", } ), } location = forms.ModelChoiceField( label=_("Location"), queryset=None, empty_label=_("Select Location"), required=False, widget=forms.Select(attrs={"class": "form-select", "id": "locationSelect", "data-action": "load-sections"}), ) main_section = forms.ModelChoiceField( label=_("Section"), queryset=None, empty_label=_("Select Section"), required=False, widget=forms.Select( attrs={"class": "form-select", "id": "mainSectionSelect", "data-action": "load-subsections"} ), ) subsection = forms.ModelChoiceField( label=_("Subsection"), queryset=None, empty_label=_("Select Subsection"), required=False, widget=forms.Select(attrs={"class": "form-select", "id": "subsectionSelect"}), ) def __init__(self, *args, request=None, **kwargs): super().__init__(*args, **kwargs) from apps.organizations.models import Location, MainSection, SubSection from apps.px_sources.models import PXSource self.fields["main_section"].queryset = MainSection.objects.none() self.fields["subsection"].queryset = SubSection.objects.none() self.fields["location"].queryset = Location.active_locations() location_id = self.data.get("location") or self.initial.get("location") if location_id: available_sections = ( SubSection.objects.filter(location_id=location_id) .values_list("main_section_id", flat=True) .distinct() ) self.fields["main_section"].queryset = MainSection.objects.filter( id__in=available_sections ).order_by("name_en") section_id = self.data.get("main_section") or self.initial.get("main_section") if section_id: self.fields["subsection"].queryset = SubSection.objects.filter( location_id=location_id, main_section_id=section_id ).order_by("name_en") 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)" # Sub-category filtered by selected category self.fields["sub_category"].queryset = ObservationSubCategory.objects.none() self.fields["sub_category"].empty_label = "Select a sub-category (optional)" category_id = self.data.get("category") or self.initial.get("category") if category_id: self.fields["sub_category"].queryset = ObservationSubCategory.objects.filter( category_id=category_id, is_active=True ).order_by("sort_order", "name_en") # Load active PX sources for optional selection self.fields["px_source"].queryset = PXSource.objects.filter(is_active=True).order_by("name_en") self.fields["px_source"].empty_label = "Select source (optional)" self.fields["px_source"].required = False if not self.instance.pk: self.initial["incident_datetime"] = timezone.now().strftime("%Y-%m-%dT%H:%M") if request: user = request.user hospital = user.hospital if hospital: self.fields["assigned_department"].queryset = Department.objects.filter( hospital=hospital, status="active" ).order_by("name") else: self.fields["assigned_department"].queryset = Department.objects.filter(status="active").order_by( "name" ) self.fields["assigned_department"].empty_label = "Select department (optional)" if hospital: from apps.core.utils import get_assignable_users self.fields["assigned_to"].queryset = get_assignable_users(hospital) else: self.fields["assigned_to"].queryset = User.objects.filter(is_active=True).order_by( "first_name", "last_name" ) self.fields["assigned_to"].empty_label = "Select assignee (optional)" # Department noted uses same queryset as assigned_department self.fields["department_noted"].queryset = self.fields["assigned_department"].queryset self.fields["department_noted"].empty_label = "Select department (optional)" def clean_description(self): 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") from apps.core.utils import get_assignable_users self.fields["assigned_to"].queryset = get_assignable_users(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"}), ) def __init__(self, *args, hospital=None, **kwargs): super().__init__(*args, **kwargs) if hospital: from apps.core.utils import get_assignable_users self.fields["assigned_to"].queryset = get_assignable_users(hospital) self.fields["assigned_department"].queryset = Department.objects.filter(hospital=hospital, status="active") class ObservationSendToDepartmentForm(forms.Form): """ Form for sending an observation to a department for response. """ department = forms.ModelChoiceField( queryset=Department.objects.filter(status="active"), required=True, empty_label="Select department", widget=forms.Select(attrs={"class": "form-select"}), ) note_en = forms.CharField( required=False, widget=forms.Textarea( attrs={ "class": "form-control", "rows": 3, "placeholder": "Optional note in English...", } ), ) note_ar = forms.CharField( required=False, widget=forms.Textarea( attrs={ "class": "form-control", "rows": 3, "placeholder": "ملاحظة اختيارية بالعربية...", "dir": "rtl", } ), ) def __init__(self, *args, hospital=None, **kwargs): super().__init__(*args, **kwargs) if hospital: self.fields["department"].queryset = Department.objects.filter(hospital=hospital, status="active") class ObservationDepartmentResponseForm(forms.Form): """ Form for submitting a department response to an observation. """ response_en = forms.CharField( required=False, widget=forms.Textarea( attrs={ "class": "form-control", "rows": 6, "placeholder": "Enter your response in English...", } ), ) response_ar = forms.CharField( required=False, widget=forms.Textarea( attrs={ "class": "form-control", "rows": 6, "placeholder": "أدخل ردك باللغة العربية...", "dir": "rtl", } ), ) def clean(self): cleaned_data = super().clean() response_en = cleaned_data.get("response_en", "").strip() response_ar = cleaned_data.get("response_ar", "").strip() if not response_en and not response_ar: raise forms.ValidationError("Please enter a response in at least one language.") return cleaned_data 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 class PublicObservationForm(forms.ModelForm): reporter_name = forms.CharField( label=_("Reporter Name"), max_length=200, required=True, widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Your full name")}), ) reporter_phone = forms.CharField( label=_("Mobile Number"), max_length=20, required=True, widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Your mobile number")}), ) reporter_email = forms.EmailField( label=_("Email Address"), required=False, widget=forms.EmailInput(attrs={"class": "form-control", "placeholder": _("your@email.com")}), ) hospital = forms.ModelChoiceField( label=_("Hospital"), queryset=None, empty_label=_("Select Hospital"), required=True, widget=forms.Select(attrs={"class": "form-control", "id": "hospital_select"}), ) category = forms.ModelChoiceField( label=_("Category"), queryset=None, empty_label=_("Select Category"), required=False, widget=forms.Select(attrs={"class": "form-control"}), ) location = forms.ModelChoiceField( label=_("Location"), queryset=None, empty_label=_("Select Location"), required=False, widget=forms.Select(attrs={"class": "form-control", "id": "location_select", "data-action": "load-sections"}), ) main_section = forms.ModelChoiceField( label=_("Section"), queryset=None, empty_label=_("Select Section"), required=False, widget=forms.Select(attrs={"class": "form-control", "id": "main_section_select", "data-action": "load-subsections"}), ) subsection = forms.ModelChoiceField( label=_("Subsection"), queryset=None, empty_label=_("Select Subsection"), required=False, widget=forms.Select(attrs={"class": "form-control", "id": "subsection_select"}), ) title = forms.CharField( label=_("Title"), max_length=200, required=False, widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Brief title (optional)")}), ) description = forms.CharField( label=_("Description"), required=True, widget=forms.Textarea(attrs={"class": "form-control", "rows": 5, "placeholder": _("Describe the observation in detail...")}), ) class Meta: model = Observation fields = [ "reporter_name", "reporter_phone", "reporter_email", "hospital", "category", "location", "main_section", "subsection", "title", "description", "description_en", ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) from apps.organizations.models import Hospital, Location, MainSection, SubSection self.fields["hospital"].queryset = Hospital.objects.filter(status="active").order_by("name") self.fields["category"].queryset = ObservationCategory.objects.filter(is_active=True).order_by("name_en") self.fields["location"].queryset = Location.active_locations() self.fields["main_section"].queryset = MainSection.objects.none() self.fields["subsection"].queryset = SubSection.objects.none() location_id = None if "location" in self.initial: location_id = self.initial["location"] elif "location" in self.data: location_id = self.data["location"] if location_id: available_sections = ( SubSection.objects.filter(location_id=location_id).values_list("main_section_id", flat=True).distinct() ) self.fields["main_section"].queryset = MainSection.objects.filter(id__in=available_sections).order_by("name_en") section_id = None if "main_section" in self.initial: section_id = self.initial["main_section"] elif "main_section" in self.data: section_id = self.data["main_section"] if section_id: self.fields["subsection"].queryset = SubSection.objects.filter( location_id=location_id, main_section_id=section_id ).order_by("name_en")