fix: token-response forms, inquiry dept-response 500, get_email_header_html, lucide
All checks were successful
Build and Push Docker Image / build (push) Successful in 2m15s

Bugs 4-8 from the QA audit, all re-verified (re-run: 0 FAIL):
- token-response form pages (inquiry + observation, 8 templates) extended the
  static dashboard base (no {% block content %}) -> form was dropped. Switched
  to layouts/public_base.html; form + csrf now render, anonymous token POST works
- inquiry dept-response GET 500 (missing template) -> created
  complaints/inquiry_department_response.html (mirrors the observation one)
- NameError get_email_header_html in notifications/settings_service.py
  (send_inquiry_department_assigned/_resolved/_reopened) -> added to the imports
- "lucide is not defined" -> guarded the unguarded lucide.createIcons() in
  layouts/base.html and added a guarded init to layouts/public_base.html
- dept analytics XHR ERR_ABORTED -> verified endpoint returns 200 (test artifact)

Report updated: §13 issues marked fixed.
This commit is contained in:
ismail 2026-06-14 20:13:45 +03:00
parent 8c75baf30a
commit adff7dd8b5
13 changed files with 119 additions and 30 deletions

View File

@ -29,7 +29,7 @@ def _get_quiet_hours_end_datetime(settings):
@shared_task @shared_task
def _send_deferred_sms(phone, message, related_object_app=None, related_object_model=None, related_object_id=None): def _send_deferred_sms(phone, message, related_object_app=None, related_object_model=None, related_object_id=None):
from .services import NotificationService from .services import NotificationService, get_email_header_html
related_object = None related_object = None
if related_object_app and related_object_model and related_object_id: if related_object_app and related_object_model and related_object_id:
@ -43,7 +43,7 @@ def _send_deferred_sms(phone, message, related_object_app=None, related_object_m
@shared_task @shared_task
def _send_deferred_whatsapp(phone, message, related_object_app=None, related_object_model=None, related_object_id=None): def _send_deferred_whatsapp(phone, message, related_object_app=None, related_object_model=None, related_object_id=None):
from .services import NotificationService from .services import NotificationService, get_email_header_html
related_object = None related_object = None
if related_object_app and related_object_model and related_object_id: if related_object_app and related_object_model and related_object_id:
@ -144,7 +144,7 @@ class NotificationServiceWithSettings:
@staticmethod @staticmethod
def _defer_sms_if_quiet_hours(settings, phone, message, related_object): def _defer_sms_if_quiet_hours(settings, phone, message, related_object):
from .services import NotificationService from .services import NotificationService, get_email_header_html
if settings.is_quiet_hours(): if settings.is_quiet_hours():
eta = _get_quiet_hours_end_datetime(settings) eta = _get_quiet_hours_end_datetime(settings)
@ -165,7 +165,7 @@ class NotificationServiceWithSettings:
@staticmethod @staticmethod
def _defer_whatsapp_if_quiet_hours(settings, phone, message, related_object): def _defer_whatsapp_if_quiet_hours(settings, phone, message, related_object):
from .services import NotificationService from .services import NotificationService, get_email_header_html
if settings.is_quiet_hours(): if settings.is_quiet_hours():
eta = _get_quiet_hours_end_datetime(settings) eta = _get_quiet_hours_end_datetime(settings)
@ -249,7 +249,7 @@ class NotificationServiceWithSettings:
@staticmethod @staticmethod
def send_inquiry_department_assigned(department, inquiry, context_note_en="", context_note_ar="", recipient_type="staff"): def send_inquiry_department_assigned(department, inquiry, context_note_en="", context_note_ar="", recipient_type="staff"):
from apps.accounts.models import User from apps.accounts.models import User
from .services import NotificationService from .services import NotificationService, get_email_header_html
context_note_parts = [] context_note_parts = []
if context_note_en: if context_note_en:
@ -355,7 +355,7 @@ class NotificationServiceWithSettings:
@staticmethod @staticmethod
def send_observation_department_assigned(department, observation, context_note_en="", context_note_ar="", recipient_type="staff"): def send_observation_department_assigned(department, observation, context_note_en="", context_note_ar="", recipient_type="staff"):
from apps.accounts.models import User from apps.accounts.models import User
from .services import NotificationService from .services import NotificationService, get_email_header_html
context_note_parts = [] context_note_parts = []
if context_note_en: if context_note_en:
@ -541,7 +541,7 @@ class NotificationServiceWithSettings:
@staticmethod @staticmethod
def send_explanation_reminder(staff_email, complaint): def send_explanation_reminder(staff_email, complaint):
"""Send explanation reminder notification""" """Send explanation reminder notification"""
from .services import NotificationService from .services import NotificationService, get_email_header_html
hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint) hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint)
if not hospital_id: if not hospital_id:
@ -587,7 +587,7 @@ class NotificationServiceWithSettings:
@staticmethod @staticmethod
def send_explanation_overdue(manager_email, complaint, staff_name): def send_explanation_overdue(manager_email, complaint, staff_name):
"""Send explanation overdue/escalation notification to manager""" """Send explanation overdue/escalation notification to manager"""
from .services import NotificationService from .services import NotificationService, get_email_header_html
hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint) hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint)
if not hospital_id: if not hospital_id:
@ -627,7 +627,7 @@ class NotificationServiceWithSettings:
@staticmethod @staticmethod
def send_explanation_received(assignee_email, complaint, explanation): def send_explanation_received(assignee_email, complaint, explanation):
"""Send notification when explanation is received""" """Send notification when explanation is received"""
from .services import NotificationService from .services import NotificationService, get_email_header_html
hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint) hospital_id = NotificationServiceWithSettings._get_hospital_id_from_complaint(complaint)
if not hospital_id: if not hospital_id:
@ -660,7 +660,7 @@ class NotificationServiceWithSettings:
"""Send notification when complaint is assigned""" """Send notification when complaint is assigned"""
from django.template.loader import render_to_string from django.template.loader import render_to_string
from .services import NotificationService from .services import NotificationService, get_email_header_html
# Build template context # Build template context
context = { context = {
@ -721,7 +721,7 @@ class NotificationServiceWithSettings:
Send onboarding invitation to new provisional user. Send onboarding invitation to new provisional user.
Respects hospital notification settings. Respects hospital notification settings.
""" """
from .services import NotificationService from .services import NotificationService, get_email_header_html
hospital_id = getattr(provisional_user, "hospital_id", None) hospital_id = getattr(provisional_user, "hospital_id", None)
if not hospital_id: if not hospital_id:
@ -766,7 +766,7 @@ class NotificationServiceWithSettings:
@staticmethod @staticmethod
def send_onboarding_reminder(user_email, provisional_user): def send_onboarding_reminder(user_email, provisional_user):
"""Send onboarding reminder notification""" """Send onboarding reminder notification"""
from .services import NotificationService from .services import NotificationService, get_email_header_html
hospital_id = getattr(provisional_user, "hospital_id", None) hospital_id = getattr(provisional_user, "hospital_id", None)
if not hospital_id: if not hospital_id:
@ -807,7 +807,7 @@ class NotificationServiceWithSettings:
@staticmethod @staticmethod
def send_onboarding_completion_notification(admin_email, completed_user): def send_onboarding_completion_notification(admin_email, completed_user):
"""Send notification to admin when user completes onboarding""" """Send notification to admin when user completes onboarding"""
from .services import NotificationService from .services import NotificationService, get_email_header_html
hospital_id = getattr(completed_user, "hospital_id", None) hospital_id = getattr(completed_user, "hospital_id", None)
if not hospital_id: if not hospital_id:

View File

@ -235,15 +235,15 @@ These two modules use a **simpler** flow than complaints: **PX-team sends to dep
State machines behave correctly: send sets `transferred_to_department` (inquiry) / `sent_to_department=True` (observation); champion response sets `department_response_en` + `department_responded_at` + `dept_response_acceptance_status=pending`; PX `acceptable``acceptance_status=acceptable`; PX `not_acceptable`**clears the response** and returns it to the champion (verified — `response_en_set=False` after reject). Inquiry resolve requires a PX `response` first (`inquiry_respond`) — handled. State machines behave correctly: send sets `transferred_to_department` (inquiry) / `sent_to_department=True` (observation); champion response sets `department_response_en` + `department_responded_at` + `dept_response_acceptance_status=pending`; PX `acceptable``acceptance_status=acceptable`; PX `not_acceptable`**clears the response** and returns it to the champion (verified — `response_en_set=False` after reject). Inquiry resolve requires a PX `response` first (`inquiry_respond`) — handled.
## ⚠️ Issues found ## ⚠️ Issues found → ✅ fixed (re-verified, 0 FAIL)
1. **Token-response form page is broken (both modules).** GET `/inquiries/<id>/respond/<token>/` and `/observations/<id>/respond/<token>/` render the **dashboard chrome with no response form** (no `response_en` textarea) instead of `inquiry_response_form_token.html` / `response_form_token.html`. The backend POST still accepts the token response, but a **truly anonymous visitor can't submit** (the broken page renders no `{% csrf_token %}`, so they get a 403 on POST). High-impact for the email-link UX since champions receive this link by email. Reproduced via Django test client (GET returns a 13176-char page titled "PX360 Dashboard - Blue Edition" with no `<form>`). 1. **Token-response form page is broken (both modules)** — ✅ **FIXED.** Root cause: the 8 public/token templates `{% extends "template.html" %}`, a static dashboard with no `{% block content %}`, so the form block was dropped. Fix: changed all 8 to `{% extends "layouts/public_base.html" %}`. Re-verified: `B-token-form-render` now PASS (form + csrf render), anonymous token POST works.
2. **`NameError: get_email_header_html` during inquiry transfer** (`inquiry_transfer_to_department`) — the department-assigned notification email silently fails ("Failed to send department notification: name 'get_email_header_html' is not defined"). Caught/logged, so the transfer succeeds, but the notification isn't sent. 2. **`NameError: get_email_header_html` during inquiry transfer** — ✅ **FIXED.** Root cause: `apps/notifications/settings_service.py` imported `NotificationService` but not `get_email_header_html`. Fix: added `get_email_header_html` to the lazy imports. Re-verified: no NameError in the re-run.
3. **Observation can't jump `new → resolved`** — its status machine requires intermediate steps (new → triaged/assigned/in_progress → resolved). `observation_change_status` accepts the POST (302) but the status stays `new`. (Reported as WARN, not FAIL — resolution is outside the dept-response flow's core.) 3. **Inquiry dept-response page 500 (`DoesNotExist`)** — ✅ **FIXED.** Root cause: `templates/complaints/inquiry_department_response.html` was missing. Fix: created it (mirrors the observation template). Re-verified: GET renders, no 500.
4. **`lucide is not defined` (icons broken)** — ✅ **FIXED.** Root cause: unguarded `lucide.createIcons()` in `layouts/base.html:157` threw when lucide hadn't initialized during rapid navigation. Fix: guarded that call + added a guarded init to `layouts/public_base.html`.
5. **`/organizations/departments/<id>/analytics/` XHR failure** — ✅ **not a bug.** Endpoint returns 200; the `ERR_ABORTED` was a test-navigation artifact.
## 🔧 Improvements recommended ## ⚠️ Remaining (behavior, not bugs)
- **Fix the token-response form pages** so they render the `response_en`/`response_ar` form (and a csrf token) — the templates exist and are correct; the view/template-wiring is returning the dashboard instead. This unblocks the email-link response path for champions. - **Observation can't jump `new → resolved`** — its status machine requires intermediate steps (new → triaged/assigned/in_progress → resolved). `observation_change_status` accepts the POST (302) but the status stays `new`. Confirm whether the dept-response flow should drive the status to a resolvable state (e.g., auto-activate on send). Reported as WARN, not FAIL.
- **Fix `get_email_header_html` import** in `inquiry_transfer_to_department` (mirror the fix pattern used elsewhere, e.g. complaints).
- Confirm whether observation resolution should be reachable directly from the dept-response flow (add an `activate`-equivalent or allow `new → resolved` if appropriate).
## 🔩 Test harness added ## 🔩 Test harness added
- `apps/core/management/commands/seed_e2e_dept_response.py` — seeds an open inquiry/observation, prints ids. - `apps/core/management/commands/seed_e2e_dept_response.py` — seeds an open inquiry/observation, prints ids.

View File

@ -0,0 +1,88 @@
{% extends "layouts/base.html" %}
{% load i18n %}
{% block title %}{{ inquiry.reference_number }} - {% trans "Department Response" %} - PX360{% endblock %}
{% block content %}
<header class="mb-6">
<div class="flex items-center gap-2 text-sm text-slate mb-2">
<a href="{% url 'inquiries:inquiry_list' %}" class="hover:text-navy">{% trans "Inquiries" %}</a>
<i data-lucide="chevron-right" class="w-4 h-4"></i>
<a href="{% url 'inquiries:inquiry_detail' pk=inquiry.pk %}" class="hover:text-navy">{{ inquiry.reference_number }}</a>
<i data-lucide="chevron-right" class="w-4 h-4"></i>
<span class="font-bold text-navy">{% trans "Department Response" %}</span>
</div>
<h1 class="text-2xl font-bold text-navy">
{{ inquiry.subject }}
</h1>
</header>
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6">
<div class="flex items-center gap-2 mb-4">
<i data-lucide="message-square" class="w-5 h-5 text-navy"></i>
<h2 class="text-lg font-bold text-navy">{% trans "Submit Department Response" %}</h2>
</div>
<div class="bg-slate-50 rounded-xl p-4 border border-slate-200 mb-6">
<p class="text-[10px] text-slate-500 uppercase font-bold mb-1">{% trans "Reference" %}</p>
<p class="font-mono text-sm font-bold text-navy">{{ inquiry.reference_number }}</p>
{% if inquiry.contact_name %}
<p class="text-[10px] text-slate-500 uppercase font-bold mb-1 mt-3">{% trans "From" %}</p>
<p class="text-sm text-slate-700">{{ inquiry.contact_name }}</p>
{% endif %}
<p class="text-[10px] text-slate-500 uppercase font-bold mb-1 mt-3">{% trans "Message" %}</p>
<p class="text-sm text-slate-700 leading-relaxed">{{ inquiry.message|truncatewords:50 }}</p>
</div>
{% if messages %}
<div class="mb-4">
{% for message in messages %}
<div class="px-4 py-3 rounded-lg text-sm font-semibold {% if message.tags == 'error' %}bg-red-50 text-red-700 border border-red-200{% else %}bg-green-50 text-green-700 border border-green-200{% endif %}">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
<form method="post" action="{% url 'inquiries:inquiry_department_response' pk=inquiry.pk %}">
{% csrf_token %}
<div class="space-y-4">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-2">
{% trans "Response (English)" %}
</label>
<textarea name="response_en" rows="5"
class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl text-slate-700 focus:outline-none focus:border-navy focus:ring-2 focus:ring-navy/20 resize-none text-sm"
placeholder="{% trans 'Enter your response in English...' %}">{{ inquiry.department_response_en|default:'' }}</textarea>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-2">
{% trans "Response (Arabic)" %}
</label>
<textarea name="response_ar" rows="5" dir="rtl"
class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl text-slate-700 focus:outline-none focus:border-navy focus:ring-2 focus:ring-navy/20 resize-none text-sm"
placeholder="{% trans 'أدخل ردك باللغة العربية...' %}">{{ inquiry.department_response_ar|default:'' }}</textarea>
</div>
<p class="text-xs text-slate-500">
<i data-lucide="info" class="w-3 h-3 inline mr-1"></i>
{% trans "At least one language is required." %}
</p>
</div>
<div class="flex items-center gap-3 mt-6 pt-4 border-t border-slate-100">
<button type="submit"
class="px-6 py-2.5 bg-navy text-white font-semibold rounded-xl hover:bg-blue transition flex items-center gap-2 text-sm">
<i data-lucide="send" class="w-4 h-4"></i>
{% trans "Submit Response" %}
</button>
<a href="{% url 'inquiries:inquiry_detail' pk=inquiry.pk %}"
class="px-6 py-2.5 bg-slate-100 text-slate-700 font-medium rounded-xl hover:bg-slate-200 transition text-sm">
{% trans "Cancel" %}
</a>
</div>
</form>
</div>
{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "template.html" %} {% extends "layouts/public_base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Response Already Submitted" %}{% endblock %} {% block title %}{% trans "Response Already Submitted" %}{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "template.html" %} {% extends "layouts/public_base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Respond to Inquiry" %} - {{ inquiry.reference_number }}{% endblock %} {% block title %}{% trans "Respond to Inquiry" %} - {{ inquiry.reference_number }}{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "template.html" %} {% extends "layouts/public_base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Response Submitted" %}{% endblock %} {% block title %}{% trans "Response Submitted" %}{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "template.html" %} {% extends "layouts/public_base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Invalid Link" %}{% endblock %} {% block title %}{% trans "Invalid Link" %}{% endblock %}

View File

@ -154,7 +154,7 @@
} }
// Initialize Lucide icons // Initialize Lucide icons
lucide.createIcons(); if (typeof lucide !== 'undefined') { lucide.createIcons(); }
// Mobile sidebar toggle // Mobile sidebar toggle
function toggleSidebar() { function toggleSidebar() {

View File

@ -223,6 +223,7 @@
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') { lucide.createIcons(); }
document.querySelectorAll('select[data-tomselect]').forEach(function(el) { document.querySelectorAll('select[data-tomselect]').forEach(function(el) {
var instance = new TomSelect(el, { var instance = new TomSelect(el, {
create: false, create: false,

View File

@ -1,4 +1,4 @@
{% extends "template.html" %} {% extends "layouts/public_base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Response Already Submitted" %}{% endblock %} {% block title %}{% trans "Response Already Submitted" %}{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "template.html" %} {% extends "layouts/public_base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Respond to Observation" %} - {{ observation.tracking_code }}{% endblock %} {% block title %}{% trans "Respond to Observation" %} - {{ observation.tracking_code }}{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "template.html" %} {% extends "layouts/public_base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Response Submitted" %}{% endblock %} {% block title %}{% trans "Response Submitted" %}{% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "template.html" %} {% extends "layouts/public_base.html" %}
{% load i18n %} {% load i18n %}
{% block title %}{% trans "Invalid Link" %}{% endblock %} {% block title %}{% trans "Invalid Link" %}{% endblock %}