907 lines
29 KiB
Python
907 lines
29 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 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")
|