HH/apps/surveys/forms.py
2026-04-08 17:13:35 +03:00

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)