HH/apps/feedback/forms.py
ismail 7369d08012
All checks were successful
Build and Push Docker Image / build (push) Successful in 4m14s
feat: unified reference numbers + feedback modules QA audit
Reference numbers (unified scheme PREFIX-YYYYMM-HOSP-NNNN, e.g. CMP-202606-HHN-0001):
- new ReferenceSequence model + generate_reference() helper (apps/core)
- Complaint/Inquiry/Observation/Appreciation/Suggestion emit unified refs via save()
- prefix-based auto-routing in public track API (CMP/INQ/OBS trackable; APR/SGT internal-only)
- removed legacy CMP-/INQ- generators in ui_views, integrations, px_sources
- migrations: core.0003_referencesequence, appreciation.0006, feedback.0008, observations.0012
- unit tests (format, sanitization, monthly reset, 40-thread concurrency)

QA audit:
- isolated E2E hospital sandbox mirroring HH-N + 10 role users (create_e2e_isolated_env)
- feedback-modules-audit.spec.ts + audit helper (headed, run-to-completion)
- reports/feedback-modules-qa-report.md

Also bundles accumulated in-progress work across complaints, observations,
organizations, templates, and other modules.
2026-06-14 14:29:23 +03:00

346 lines
13 KiB
Python

"""
Feedback forms - Forms for feedback management
"""
from django import forms
from django.utils.translation import gettext_lazy as _
from apps.organizations.models import Department, Hospital, Patient, Staff
from apps.core.form_mixins import HospitalFieldMixin
from .models import Feedback, FeedbackResponse, FeedbackStatus, FeedbackType, FeedbackCategory
class FeedbackForm(HospitalFieldMixin, forms.ModelForm):
"""Form for creating and editing feedback"""
class Meta:
model = Feedback
fields = [
"patient",
"is_anonymous",
"contact_name",
"contact_email",
"contact_phone",
"hospital",
"department",
"staff",
"feedback_type",
"title",
"message",
"category",
"subcategory",
"rating",
"priority",
"encounter_id",
]
widgets = {
"patient": forms.Select(attrs={"class": "form-select", "id": "id_patient"}),
"is_anonymous": forms.CheckboxInput(attrs={"class": "form-check-input", "id": "id_is_anonymous"}),
"contact_name": forms.TextInput(attrs={"class": "form-control", "placeholder": "Enter contact name"}),
"contact_email": forms.EmailInput(attrs={"class": "form-control", "placeholder": "Enter email address"}),
"contact_phone": forms.TextInput(attrs={"class": "form-control", "placeholder": "Enter phone number"}),
"hospital": forms.Select(attrs={"class": "form-select", "required": True}),
"department": forms.Select(attrs={"class": "form-select"}),
"staff": forms.Select(attrs={"class": "form-select"}),
"feedback_type": forms.Select(attrs={"class": "form-select", "required": True}),
"title": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Enter feedback title", "required": True}
),
"message": forms.Textarea(
attrs={
"class": "form-control",
"rows": 5,
"placeholder": "Enter your feedback message...",
"required": True,
}
),
"category": forms.Select(attrs={"class": "form-select", "required": True}),
"subcategory": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Enter subcategory (optional)"}
),
"rating": forms.NumberInput(
attrs={"class": "form-control", "min": 1, "max": 5, "placeholder": "Rate from 1 to 5"}
),
"priority": forms.Select(attrs={"class": "form-select"}),
"encounter_id": forms.TextInput(
attrs={"class": "form-control", "placeholder": "Enter encounter ID (optional)"}
),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Hospital field is configured by HospitalFieldMixin (always hidden)
# Filter departments and staff based on hospital
hospital = None
if self.instance.pk and hasattr(self.instance, "hospital") and self.instance.hospital_id:
hospital = self.instance.hospital
elif self.user and self.user.is_px_admin():
hospital = getattr(self.request, "tenant_hospital", None)
elif self.user and self.user.hospital:
hospital = self.user.hospital
if hospital:
self.fields["department"].queryset = Department.objects.filter(hospital=hospital, status="active")
self.fields["staff"].queryset = Staff.objects.filter(hospital=hospital, status="active")
else:
self.fields["department"].queryset = Department.objects.none()
self.fields["staff"].queryset = Staff.objects.none()
# Make patient optional if anonymous
if self.data.get("is_anonymous"):
self.fields["patient"].required = False
def clean(self):
cleaned_data = super().clean()
is_anonymous = cleaned_data.get("is_anonymous")
patient = cleaned_data.get("patient")
contact_name = cleaned_data.get("contact_name")
# Validate anonymous feedback
if is_anonymous:
if not contact_name:
raise forms.ValidationError("Contact name is required for anonymous feedback.")
else:
if not patient:
raise forms.ValidationError("Please select a patient or mark as anonymous.")
# Validate rating
rating = cleaned_data.get("rating")
if rating is not None and (rating < 1 or rating > 5):
raise forms.ValidationError("Rating must be between 1 and 5.")
return cleaned_data
class FeedbackResponseForm(forms.ModelForm):
"""Form for adding responses to feedback"""
class Meta:
model = FeedbackResponse
fields = ["response_type", "message", "is_internal"]
widgets = {
"response_type": forms.Select(attrs={"class": "form-select", "required": True}),
"message": forms.Textarea(
attrs={"class": "form-control", "rows": 4, "placeholder": "Enter your response...", "required": True}
),
"is_internal": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}
class FeedbackFilterForm(forms.Form):
"""Form for filtering feedback list"""
search = forms.CharField(
required=False,
widget=forms.TextInput(attrs={"class": "form-control", "placeholder": "Search by title, message, patient..."}),
)
feedback_type = forms.ChoiceField(
required=False,
choices=[("", "All Types")] + list(FeedbackType.choices),
widget=forms.Select(attrs={"class": "form-select"}),
)
status = forms.ChoiceField(
required=False,
choices=[("", "All Statuses")] + list(FeedbackStatus.choices),
widget=forms.Select(attrs={"class": "form-select"}),
)
category = forms.ChoiceField(
required=False,
choices=[("", "All Categories")] + list(FeedbackCategory.choices),
widget=forms.Select(attrs={"class": "form-select"}),
)
sentiment = forms.ChoiceField(
required=False,
choices=[
("", "All Sentiments"),
("positive", "Positive"),
("neutral", "Neutral"),
("negative", "Negative"),
],
widget=forms.Select(attrs={"class": "form-select"}),
)
priority = forms.ChoiceField(
required=False,
choices=[
("", "All Priorities"),
("low", "Low"),
("medium", "Medium"),
("high", "High"),
("urgent", "Urgent"),
],
widget=forms.Select(attrs={"class": "form-select"}),
)
hospital = forms.ModelChoiceField(
required=False,
queryset=Hospital.objects.filter(status="active"),
widget=forms.Select(attrs={"class": "form-select"}),
empty_label="All Hospitals",
)
department = forms.ModelChoiceField(
required=False,
queryset=Department.objects.filter(status="active"),
widget=forms.Select(attrs={"class": "form-select"}),
empty_label="All Departments",
)
rating_min = forms.IntegerField(
required=False,
min_value=1,
max_value=5,
widget=forms.NumberInput(attrs={"class": "form-control", "placeholder": "Min rating"}),
)
rating_max = forms.IntegerField(
required=False,
min_value=1,
max_value=5,
widget=forms.NumberInput(attrs={"class": "form-control", "placeholder": "Max rating"}),
)
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_featured = forms.BooleanField(required=False, widget=forms.CheckboxInput(attrs={"class": "form-check-input"}))
requires_follow_up = forms.BooleanField(
required=False, widget=forms.CheckboxInput(attrs={"class": "form-check-input"})
)
class FeedbackStatusChangeForm(forms.Form):
"""Form for changing feedback status"""
status = forms.ChoiceField(
choices=FeedbackStatus.choices, widget=forms.Select(attrs={"class": "form-select", "required": True})
)
note = forms.CharField(
required=False,
widget=forms.Textarea(
attrs={
"class": "form-control",
"rows": 3,
"placeholder": "Add a note about this status change (optional)...",
}
),
)
class FeedbackAssignForm(forms.Form):
"""Form for assigning feedback to a user"""
user_id = forms.UUIDField(widget=forms.Select(attrs={"class": "form-select", "required": True}))
note = forms.CharField(
required=False,
widget=forms.Textarea(
attrs={"class": "form-control", "rows": 2, "placeholder": "Add a note about this assignment (optional)..."}
),
)
class PublicSuggestionForm(forms.Form):
contact_name = forms.CharField(
label=_("Contact Name"),
max_length=200,
required=True,
widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Your full name")}),
)
contact_phone = forms.CharField(
label=_("Mobile Number"),
max_length=20,
required=True,
widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Your mobile number")}),
)
contact_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.ChoiceField(
label=_("Area"),
choices=FeedbackCategory.choices,
required=True,
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=_("Suggestion Title"),
max_length=500,
required=True,
widget=forms.TextInput(attrs={"class": "form-control", "placeholder": _("Brief title for your suggestion")}),
)
message = forms.CharField(
label=_("Your Suggestion"),
required=True,
widget=forms.Textarea(attrs={"class": "form-control", "rows": 5, "placeholder": _("Describe your suggestion in detail...")}),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from apps.organizations.models import Hospital, LegacyLocation, LegacyMainSection, LegacySubSection
self.fields["hospital"].queryset = Hospital.objects.filter(status="active").order_by("name")
self.fields["location"].queryset = LegacyLocation.active_locations()
self.fields["main_section"].queryset = LegacyMainSection.objects.none()
self.fields["subsection"].queryset = LegacySubSection.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 = (
LegacySubSection.objects.filter(location_id=location_id).values_list("main_section_id", flat=True).distinct()
)
self.fields["main_section"].queryset = LegacyMainSection.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 = LegacySubSection.objects.filter(
location_id=location_id, main_section_id=section_id
).order_by("name_en")