""" 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 Area, Department, Hospital, LocationType from apps.organizations.models import Section as OrgSection 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 = [ "hospital", "location_type", "area", "title", "description", "location_text", "incident_datetime", "reporter_staff_id", "reporter_name", "reporter_phone", "reporter_email", ] widgets = { "location_type": forms.Select( attrs={ "class": "form-select", } ), "area": forms.Select( attrs={ "class": "form-select", "id": "area_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) self.fields["location_type"].required = True self.fields["location_type"].empty_label = None self.fields["location_type"].choices = [("", "Select Location Type")] + list(LocationType.choices) self.fields["area"].queryset = Area.objects.none() self.fields["area"].required = False self.fields["area"].empty_label = "Select Area (optional)" hospital_id = self.data.get("hospital") or self.initial.get("hospital") location_type = self.data.get("location_type") or self.initial.get("location_type") if hospital_id: qs = Area.objects.filter(hospital_id=hospital_id, status="active") if location_type: qs = qs.filter(location_type=location_type) self.fields["area"].queryset = qs.order_by("name_en") # 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. """ hospital = forms.ModelChoiceField( label=_("Hospital"), queryset=Hospital.objects.filter(status="active"), empty_label=_("Select Hospital"), required=True, widget=forms.Select(attrs={"class": "form-select", "id": "hospitalSelect"}), ) class Meta: model = Observation fields = [ "hospital", "location_type", "area", "description", "section", "incident_datetime", "patient_file_number", "assigned_department", "assigned_to", "px_source", ] widgets = { "location_type": forms.Select( attrs={ "class": "form-select", "id": "locationTypeSelect", } ), "area": forms.Select( attrs={ "class": "form-select", "id": "areaSelect", } ), "description": forms.Textarea( attrs={ "class": "form-control", "rows": 5, "placeholder": "Please describe what you observed in detail...", } ), "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", } ), "assigned_department": forms.Select( attrs={ "class": "form-select", } ), "assigned_to": forms.Select( attrs={ "class": "form-select", } ), } section = forms.ModelChoiceField( label=_("Section"), queryset=None, empty_label=_("Select Section"), required=False, widget=forms.Select(attrs={"class": "form-select", "id": "sectionSelect"}), ) def __init__(self, *args, request=None, **kwargs): self.request = request self.user = request.user if request else None super().__init__(*args, **kwargs) from apps.px_sources.models import PXSource self.fields["section"].queryset = OrgSection.objects.none() self.fields["location_type"].required = True self.fields["location_type"].choices = [("", "Select Location Type")] + list(LocationType.choices) self.fields["area"].queryset = Area.objects.none() self.fields["area"].required = False self.fields["area"].empty_label = "Select Area (optional)" hospital = None if self.user: hospital = getattr(self.user, 'hospital', None) if hospital: self.fields["hospital"].initial = hospital self.fields["hospital"].queryset = Hospital.objects.filter(status="active") hospital_id = self.data.get("hospital") or (str(hospital.id) if hospital else "") if not hospital_id and self.initial.get("hospital"): hospital_id = str(self.initial["hospital"].id) if hasattr(self.initial["hospital"], "id") else str(self.initial["hospital"]) location_type = self.data.get("location_type") or self.initial.get("location_type") if hospital_id: qs = Area.objects.filter(hospital_id=hospital_id, status="active") if location_type: qs = qs.filter(location_type=location_type) self.fields["area"].queryset = qs.order_by("name_en") department_id = self.data.get("assigned_department") or self.initial.get("assigned_department") if department_id: self.fields["section"].queryset = OrgSection.objects.filter( department_id=department_id ).order_by("name_en") 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 hospital_id: self.fields["assigned_department"].queryset = Department.objects.filter( hospital_id=hospital_id, status="active" ).order_by("name_en") else: self.fields["assigned_department"].queryset = Department.objects.filter(status="active").order_by("name_en") 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)" 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"}), ) location_type = forms.ChoiceField( label=_("Location Type"), choices=[("", _("Select Location Type"))] + list(LocationType.choices), required=True, widget=forms.Select(attrs={"class": "form-control", "id": "location_type_select"}), ) department = forms.ModelChoiceField( label=_("Department (Optional)"), queryset=Department.objects.none(), empty_label=_("Select Department"), required=False, widget=forms.Select(attrs={"class": "form-control", "id": "department_select"}), ) section = forms.ModelChoiceField( label=_("Section (Optional)"), queryset=OrgSection.objects.none(), empty_label=_("Select Section"), required=False, widget=forms.Select(attrs={"class": "form-control", "id": "section_select"}), ) area = forms.ModelChoiceField( label=_("Area (Optional)"), queryset=Area.objects.none(), empty_label=_("Select Area"), required=False, widget=forms.Select(attrs={"class": "form-control", "id": "area_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", "location_type", "area", "department", "section", "title", "description", "description_en", ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) from apps.organizations.models import Hospital self.fields["hospital"].queryset = Hospital.objects.filter(status="active").order_by("name") self.fields["section"].queryset = OrgSection.objects.none() self.fields["area"].queryset = Area.objects.none() hospital_id = None if "hospital" in self.initial: hospital_id = self.initial["hospital"] elif "hospital" in self.data: hospital_id = self.data["hospital"] location_type = self.data.get("location_type") or self.initial.get("location_type") if hospital_id: self.fields["department"].queryset = Department.objects.filter( hospital_id=hospital_id, status="active" ).order_by("name") area_qs = Area.objects.filter(hospital_id=hospital_id, status="active") if location_type: area_qs = area_qs.filter(location_type=location_type) self.fields["area"].queryset = area_qs.order_by("name_en") department_id = None if "department" in self.initial: department_id = self.initial["department"] elif "department" in self.data: department_id = self.data["department"] if department_id: self.fields["section"].queryset = OrgSection.objects.filter( department_id=department_id, status="active" ).order_by("name_en")