445 lines
16 KiB
Python
445 lines
16 KiB
Python
"""
|
|
Survey forms for CRUD operations
|
|
"""
|
|
|
|
from django import forms
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from apps.organizations.models import Patient, Staff, Hospital
|
|
from apps.core.form_mixins import HospitalFieldMixin
|
|
from .models import (
|
|
SurveyInstance,
|
|
SurveyTemplate,
|
|
SurveyQuestion,
|
|
QuestionRoutingRule,
|
|
)
|
|
|
|
|
|
class SurveyTemplateForm(HospitalFieldMixin, forms.ModelForm):
|
|
"""Form for creating/editing survey templates"""
|
|
|
|
class Meta:
|
|
model = SurveyTemplate
|
|
fields = [
|
|
"name",
|
|
"name_ar",
|
|
"hospital",
|
|
"survey_type",
|
|
"scoring_method",
|
|
"negative_threshold",
|
|
"is_active",
|
|
"instructions_en",
|
|
"instructions_ar",
|
|
"consent_text_en",
|
|
"consent_text_ar",
|
|
"requires_consent",
|
|
]
|
|
widgets = {
|
|
"name": forms.TextInput(attrs={"class": "form-control", "placeholder": "e.g., MD Consultation Feedback"}),
|
|
"name_ar": forms.TextInput(attrs={"class": "form-control", "placeholder": "الاسم بالعربية"}),
|
|
"hospital": forms.Select(attrs={"class": "form-select"}),
|
|
"survey_type": forms.Select(attrs={"class": "form-select"}),
|
|
"scoring_method": forms.Select(attrs={"class": "form-select"}),
|
|
"negative_threshold": forms.NumberInput(
|
|
attrs={"class": "form-control", "step": "0.1", "min": "1", "max": "5"}
|
|
),
|
|
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
"instructions_en": forms.Textarea(attrs={"class": "form-control", "rows": 5}),
|
|
"instructions_ar": forms.Textarea(attrs={"class": "form-control", "rows": 5, "dir": "rtl"}),
|
|
"consent_text_en": forms.Textarea(attrs={"class": "form-control", "rows": 3}),
|
|
"consent_text_ar": forms.Textarea(attrs={"class": "form-control", "rows": 3, "dir": "rtl"}),
|
|
"requires_consent": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
}
|
|
|
|
|
|
class SurveyQuestionForm(forms.ModelForm):
|
|
"""Form for creating/editing survey questions"""
|
|
|
|
class Meta:
|
|
model = SurveyQuestion
|
|
fields = [
|
|
"text",
|
|
"text_ar",
|
|
"question_type",
|
|
"order",
|
|
"is_required",
|
|
"is_base",
|
|
"event_type",
|
|
"choices_json",
|
|
"is_conditional",
|
|
]
|
|
widgets = {
|
|
"text": forms.Textarea(
|
|
attrs={"class": "form-control", "rows": 2, "placeholder": "Enter question in English"}
|
|
),
|
|
"text_ar": forms.Textarea(
|
|
attrs={"class": "form-control", "rows": 2, "placeholder": "أدخل السؤال بالعربية"}
|
|
),
|
|
"question_type": forms.Select(attrs={"class": "form-select"}),
|
|
"order": forms.NumberInput(attrs={"class": "form-control", "min": "0"}),
|
|
"is_required": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
"is_base": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
"event_type": forms.TextInput(
|
|
attrs={
|
|
"class": "form-control",
|
|
"placeholder": "e.g., Lab Bill, Triage (leave blank for base questions)",
|
|
}
|
|
),
|
|
"choices_json": forms.Textarea(
|
|
attrs={
|
|
"class": "form-control",
|
|
"rows": 5,
|
|
"placeholder": '[{"value": "1", "label": "Option 1", "label_ar": "خيار 1"}]',
|
|
}
|
|
),
|
|
"is_conditional": forms.CheckboxInput(attrs={"class": "form-check-input"}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.fields["choices_json"].required = False
|
|
self.fields["choices_json"].help_text = _(
|
|
"JSON array of choices for multiple choice questions. "
|
|
'Format: [{"value": "1", "label": "Option 1", "label_ar": "خيار 1"}]'
|
|
)
|
|
self.fields["event_type"].required = False
|
|
self.fields["event_type"].help_text = _("Leave blank for base questions (always included).")
|
|
|
|
|
|
SurveyQuestionFormSet = forms.inlineformset_factory(
|
|
SurveyTemplate, SurveyQuestion, form=SurveyQuestionForm, extra=1, can_delete=True, min_num=1, validate_min=True
|
|
)
|
|
|
|
|
|
class QuestionRoutingRuleForm(forms.ModelForm):
|
|
"""Form for creating/editing routing rules on survey questions"""
|
|
|
|
class Meta:
|
|
model = QuestionRoutingRule
|
|
fields = ["source_question", "operator", "value", "action", "target_question", "order"]
|
|
widgets = {
|
|
"source_question": forms.Select(attrs={"class": "form-select routing-source"}),
|
|
"operator": forms.Select(attrs={"class": "form-select routing-operator"}),
|
|
"value": forms.TextInput(attrs={"class": "form-control routing-value", "placeholder": "Comparison value"}),
|
|
"action": forms.Select(attrs={"class": "form-select routing-action"}),
|
|
"target_question": forms.Select(attrs={"class": "form-select routing-target"}),
|
|
"order": forms.NumberInput(attrs={"class": "form-control", "min": "0", "value": "0"}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.fields["target_question"].required = False
|
|
self.fields["value"].required = False
|
|
self.fields["order"].required = False
|
|
self.fields["source_question"].label = _("When question")
|
|
self.fields["target_question"].label = _("Go to question")
|
|
|
|
template = self._get_template()
|
|
if template:
|
|
qs = template.questions.all().order_by("order")
|
|
self.fields["source_question"].queryset = qs
|
|
self.fields["target_question"].queryset = qs
|
|
|
|
def _get_template(self):
|
|
if not self.instance:
|
|
return None
|
|
try:
|
|
tmpl = self.instance.survey_template
|
|
if tmpl and tmpl.pk:
|
|
return tmpl
|
|
except Exception:
|
|
pass
|
|
tmpl = getattr(self.instance, "_survey_template", None)
|
|
if tmpl and tmpl.pk:
|
|
return tmpl
|
|
return None
|
|
|
|
|
|
class BaseQuestionRoutingRuleFormSet(forms.BaseInlineFormSet):
|
|
@property
|
|
def empty_form(self):
|
|
form = super().empty_form
|
|
self._set_question_querysets(form)
|
|
return form
|
|
|
|
def _construct_form(self, i, **kwargs):
|
|
form = super()._construct_form(i, **kwargs)
|
|
self._set_question_querysets(form)
|
|
return form
|
|
|
|
def _set_question_querysets(self, form):
|
|
qs = SurveyQuestion.objects.none()
|
|
if self.instance and self.instance.pk:
|
|
qs = self.instance.questions.all().order_by("order")
|
|
form.fields["source_question"].queryset = qs
|
|
form.fields["target_question"].queryset = qs
|
|
|
|
|
|
QuestionRoutingRuleFormSet = forms.inlineformset_factory(
|
|
SurveyTemplate,
|
|
QuestionRoutingRule,
|
|
form=QuestionRoutingRuleForm,
|
|
formset=BaseQuestionRoutingRuleFormSet,
|
|
extra=0,
|
|
can_delete=True,
|
|
min_num=0,
|
|
validate_min=False,
|
|
)
|
|
|
|
|
|
class ManualSurveySendForm(forms.Form):
|
|
"""Form for manually sending surveys to patients or staff"""
|
|
|
|
RECIPIENT_TYPE_CHOICES = [
|
|
("patient", _("Patient")),
|
|
("staff", _("Staff")),
|
|
]
|
|
|
|
DELIVERY_CHANNEL_CHOICES = [
|
|
("email", _("Email")),
|
|
("sms", _("SMS")),
|
|
]
|
|
|
|
def __init__(self, request=None, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.request = request
|
|
self.user = request.user if request else None
|
|
|
|
# Determine hospital context
|
|
hospital = None
|
|
if self.user and self.user.is_px_admin():
|
|
hospital = getattr(request, "tenant_hospital", None)
|
|
elif self.user and self.user.hospital:
|
|
hospital = self.user.hospital
|
|
|
|
# Filter survey templates by hospital
|
|
if hospital:
|
|
self.fields["survey_template"].queryset = SurveyTemplate.objects.filter(hospital=hospital, is_active=True)
|
|
|
|
survey_template = forms.ModelChoiceField(
|
|
queryset=SurveyTemplate.objects.filter(is_active=True),
|
|
label=_("Survey Template"),
|
|
widget=forms.Select(attrs={"class": "form-select", "data-placeholder": _("Select a survey template")}),
|
|
)
|
|
|
|
recipient_type = forms.ChoiceField(
|
|
choices=RECIPIENT_TYPE_CHOICES,
|
|
label=_("Recipient Type"),
|
|
widget=forms.RadioSelect(attrs={"class": "form-check-input"}),
|
|
)
|
|
|
|
recipient = forms.CharField(
|
|
label=_("Recipient"),
|
|
widget=forms.TextInput(
|
|
attrs={
|
|
"class": "form-control",
|
|
"placeholder": _("Search by name or ID..."),
|
|
"data-search-url": "/api/recipients/search/",
|
|
}
|
|
),
|
|
help_text=_("Start typing to search for patient or staff"),
|
|
)
|
|
|
|
delivery_channel = forms.ChoiceField(
|
|
choices=DELIVERY_CHANNEL_CHOICES,
|
|
label=_("Delivery Channel"),
|
|
widget=forms.Select(attrs={"class": "form-select"}),
|
|
)
|
|
|
|
custom_message = forms.CharField(
|
|
label=_("Custom Message (Optional)"),
|
|
required=False,
|
|
widget=forms.Textarea(
|
|
attrs={
|
|
"class": "form-control",
|
|
"rows": 3,
|
|
"placeholder": _("Add a custom message to the survey invitation..."),
|
|
}
|
|
),
|
|
)
|
|
|
|
|
|
class ManualPhoneSurveySendForm(forms.Form):
|
|
"""Form for sending surveys to a manually entered phone number"""
|
|
|
|
def __init__(self, request=None, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.request = request
|
|
self.user = request.user if request else None
|
|
|
|
# Determine hospital context
|
|
hospital = None
|
|
if self.user and self.user.is_px_admin():
|
|
hospital = getattr(request, "tenant_hospital", None)
|
|
elif self.user and self.user.hospital:
|
|
hospital = self.user.hospital
|
|
|
|
# Filter survey templates by hospital
|
|
if hospital:
|
|
self.fields["survey_template"].queryset = SurveyTemplate.objects.filter(hospital=hospital, is_active=True)
|
|
|
|
survey_template = forms.ModelChoiceField(
|
|
queryset=SurveyTemplate.objects.filter(is_active=True),
|
|
label=_("Survey Template"),
|
|
widget=forms.Select(attrs={"class": "form-select"}),
|
|
)
|
|
|
|
phone_number = forms.CharField(
|
|
label=_("Phone Number"),
|
|
widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("+966501234567")}),
|
|
help_text=_("Enter phone number with country code (e.g., +966...)"),
|
|
)
|
|
|
|
recipient_name = forms.CharField(
|
|
label=_("Recipient Name (Optional)"),
|
|
required=False,
|
|
widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Patient Name")}),
|
|
)
|
|
|
|
custom_message = forms.CharField(
|
|
label=_("Custom Message (Optional)"),
|
|
required=False,
|
|
widget=forms.Textarea(
|
|
attrs={
|
|
"class": "form-control",
|
|
"rows": 3,
|
|
"placeholder": _("Add a custom message to the survey invitation..."),
|
|
}
|
|
),
|
|
)
|
|
|
|
def clean_phone_number(self):
|
|
phone = self.cleaned_data["phone_number"].strip()
|
|
# Remove spaces, dashes, parentheses
|
|
phone = phone.replace(" ", "").replace("-", "").replace("(", "").replace(")", "")
|
|
if not phone.startswith("+"):
|
|
raise forms.ValidationError(_("Phone number must start with country code (e.g., +966)"))
|
|
return phone
|
|
|
|
|
|
class BulkCSVSurveySendForm(forms.Form):
|
|
"""Form for bulk sending surveys via CSV upload"""
|
|
|
|
survey_template = forms.ModelChoiceField(
|
|
queryset=SurveyTemplate.objects.filter(is_active=True),
|
|
label=_("Survey Template"),
|
|
widget=forms.Select(attrs={"class": "form-select"}),
|
|
)
|
|
|
|
csv_file = forms.FileField(
|
|
label=_("CSV File"),
|
|
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".csv"}),
|
|
help_text=_("Upload CSV with phone numbers. Format: phone_number,name(optional)"),
|
|
)
|
|
|
|
custom_message = forms.CharField(
|
|
label=_("Custom Message (Optional)"),
|
|
required=False,
|
|
widget=forms.Textarea(
|
|
attrs={
|
|
"class": "form-control",
|
|
"rows": 3,
|
|
"placeholder": _("Add a custom message to the survey invitation..."),
|
|
}
|
|
),
|
|
)
|
|
|
|
def __init__(self, request=None, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.request = request
|
|
self.user = request.user if request else None
|
|
|
|
# Determine hospital context
|
|
hospital = None
|
|
if self.user and self.user.is_px_admin():
|
|
hospital = getattr(request, "tenant_hospital", None)
|
|
elif self.user and self.user.hospital:
|
|
hospital = self.user.hospital
|
|
|
|
# Filter survey templates by hospital
|
|
if hospital:
|
|
self.fields["survey_template"].queryset = SurveyTemplate.objects.filter(hospital=hospital, is_active=True)
|
|
|
|
|
|
class HISPatientImportForm(HospitalFieldMixin, forms.Form):
|
|
"""
|
|
Form for importing patient data from HIS/MOH Statistics CSV.
|
|
|
|
Hospital field visibility:
|
|
- PX Admins: See dropdown with all hospitals
|
|
- Others: Hidden field, auto-set to user's hospital
|
|
"""
|
|
|
|
hospital = forms.ModelChoiceField(
|
|
queryset=Hospital.objects.filter(status="active"),
|
|
label=_("Hospital"),
|
|
widget=forms.Select(attrs={"class": "form-select"}),
|
|
help_text=_("Select the hospital for these patient records"),
|
|
)
|
|
|
|
csv_file = forms.FileField(
|
|
label=_("HIS Statistics CSV File"),
|
|
widget=forms.FileInput(attrs={"class": "form-control", "accept": ".csv"}),
|
|
help_text=_("Upload MOH Statistics CSV with patient visit data"),
|
|
)
|
|
|
|
skip_header_rows = forms.IntegerField(
|
|
label=_("Skip Header Rows"),
|
|
initial=5,
|
|
min_value=0,
|
|
max_value=10,
|
|
widget=forms.NumberInput(attrs={"class": "form-control"}),
|
|
help_text=_("Number of metadata/header rows to skip before data rows"),
|
|
)
|
|
|
|
|
|
class HISSurveySendForm(forms.Form):
|
|
"""Form for sending surveys to imported HIS patients"""
|
|
|
|
survey_template = forms.ModelChoiceField(
|
|
queryset=SurveyTemplate.objects.filter(is_active=True),
|
|
label=_("Survey Template"),
|
|
widget=forms.Select(attrs={"class": "form-select"}),
|
|
)
|
|
|
|
delivery_channel = forms.ChoiceField(
|
|
choices=[
|
|
("sms", _("SMS")),
|
|
("email", _("Email")),
|
|
("both", _("Both SMS and Email")),
|
|
],
|
|
label=_("Delivery Channel"),
|
|
initial="sms",
|
|
widget=forms.Select(attrs={"class": "form-select"}),
|
|
)
|
|
|
|
custom_message = forms.CharField(
|
|
label=_("Custom Message (Optional)"),
|
|
required=False,
|
|
widget=forms.Textarea(
|
|
attrs={
|
|
"class": "form-control",
|
|
"rows": 3,
|
|
"placeholder": _("Add a custom message to the survey invitation..."),
|
|
}
|
|
),
|
|
)
|
|
|
|
patient_ids = forms.CharField(widget=forms.HiddenInput(), required=True)
|
|
|
|
def __init__(self, request=None, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.request = request
|
|
self.user = request.user if request else None
|
|
|
|
# Determine hospital context
|
|
hospital = None
|
|
if self.user and self.user.is_px_admin():
|
|
hospital = getattr(request, "tenant_hospital", None)
|
|
elif self.user and self.user.hospital:
|
|
hospital = self.user.hospital
|
|
|
|
# Filter survey templates by hospital
|
|
if hospital:
|
|
self.fields["survey_template"].queryset = SurveyTemplate.objects.filter(hospital=hospital, is_active=True)
|