From d8d0aedd9e07e64d4ed60392b22eccb25863f13e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 8 Mar 2026 18:14:30 +0000
Subject: [PATCH 1/3] Initial plan
From 209e82f7e5435773a6ed34d58632c9295b65c772 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 8 Mar 2026 18:27:14 +0000
Subject: [PATCH 2/3] feat(support): auto-fill Zammad chat and ticket widgets
with user context
Pass authenticated user's name, email, and username to Zammad form
and chat widgets. For the ticket form: pre-fill name/email fields
and append a User Context metadata block to the ticket body via
$.ajaxPrefilter. For the chat widget: pass name/email to the
ZammadChat constructor.
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
---
app/views/help.py | 16 ++++++++
docs/ConfigurationGuide.md | 9 ++++
frontend/templates/help.html | 45 +++++++++++++++++++-
tests/test_views_help.py | 79 ++++++++++++++++++++++++++++++++++++
4 files changed, 148 insertions(+), 1 deletion(-)
diff --git a/app/views/help.py b/app/views/help.py
index 9f3cc8fc..ce1cdf7a 100644
--- a/app/views/help.py
+++ b/app/views/help.py
@@ -29,6 +29,19 @@ _DOCS_BUILD_DIR = pathlib.Path(__file__).parents[2] / "docs_build"
@router.get("/help", include_in_schema=False)
async def help_center(request: Request):
"""Render the end-user Help Center page."""
+ # Extract user context for Zammad widget pre-fill ----------------------
+ session_user = None
+ if hasattr(request, "session"):
+ session_user = request.session.get("user")
+
+ user_name = ""
+ user_email = ""
+ user_id = ""
+ if session_user and isinstance(session_user, dict):
+ user_name = session_user.get("name") or session_user.get("display_name") or ""
+ user_email = session_user.get("email") or ""
+ user_id = session_user.get("preferred_username") or session_user.get("email") or session_user.get("id") or ""
+
return templates.TemplateResponse(
"help.html",
{
@@ -39,5 +52,8 @@ async def help_center(request: Request):
"zammad_chat_id": settings.zammad_chat_id,
"zammad_form_enabled": settings.zammad_form_enabled,
"support_email": settings.support_email,
+ "user_name": user_name,
+ "user_email": user_email,
+ "user_id": user_id,
},
)
diff --git a/docs/ConfigurationGuide.md b/docs/ConfigurationGuide.md
index b33a22a8..a2f5f8ca 100644
--- a/docs/ConfigurationGuide.md
+++ b/docs/ConfigurationGuide.md
@@ -1142,6 +1142,15 @@ SUPPORT_EMAIL=support@example.com
> **Note:** The live-chat widget requires at least one Zammad agent to be online. If no agent is available, the widget will not appear. Enable Zammad's debug mode (`debug: true`) for troubleshooting.
+#### Automatic User Context (Auto-Fill)
+
+When a user is logged in, DocuElevate automatically passes their identity to the Zammad widgets:
+
+- **Ticket form:** The user's **name** and **email** are pre-filled in the form fields. A *DocuElevate User Context* block containing the user's name, email, and username is appended to the ticket body so the support agent can immediately identify the requester.
+- **Live chat:** The user's **name** and **email** are passed to the Zammad chat widget constructor. Depending on your Zammad version, the agent may see this information in the chat session details.
+
+No additional configuration is required — the auto-fill uses the authenticated session data (OAuth, local login, or admin credentials). Anonymous visitors see the standard Zammad widgets without pre-filled data.
+
## Duplicate Document Detection
DocuElevate detects and flags documents that share the same content, even if they arrive as separate uploads.
diff --git a/frontend/templates/help.html b/frontend/templates/help.html
index 03bc64d5..76d8bdb4 100644
--- a/frontend/templates/help.html
+++ b/frontend/templates/help.html
@@ -491,6 +491,47 @@ $(function() {
messageThankYou: 'Thank you for your request (#%s)! We will get back to you shortly.',
modal: true
});
+
+ {% if user_name or user_email %}
+ {# ── Auto-fill Zammad form fields from the authenticated user context ── #}
+ $('#zammad-feedback-form').on('click', function() {
+ var attempts = 0;
+ var prefillForm = function() {
+ var $modal = $('.zammad-form-modal, .js-zammad-form-modal');
+ if ($modal.length === 0 && attempts < 30) {
+ attempts++;
+ setTimeout(prefillForm, 100);
+ return;
+ }
+ var $name = $modal.find('input[name="name"]');
+ var $email = $modal.find('input[name="email"]');
+ {% if user_name %}
+ if ($name.length && !$name.val()) $name.val({{ user_name | tojson }});
+ {% endif %}
+ {% if user_email %}
+ if ($email.length && !$email.val()) $email.val({{ user_email | tojson }});
+ {% endif %}
+ };
+ setTimeout(prefillForm, 200);
+ });
+
+ {# ── Append user metadata to the ticket body before Zammad posts it ── #}
+ $.ajaxPrefilter(function(options) {
+ if (options.url && options.url.indexOf('/api/v1/form_submit') !== -1 && options.data) {
+ try {
+ var data = JSON.parse(options.data);
+ if (data.body && data.body.indexOf('\n--- DocuElevate User Context ---') === -1) {
+ var ctx = '\n\n--- DocuElevate User Context ---';
+ {% if user_name %}ctx += '\nName: ' + {{ user_name | tojson }};{% endif %}
+ {% if user_email %}ctx += '\nEmail: ' + {{ user_email | tojson }};{% endif %}
+ {% if user_id %}ctx += '\nUsername: ' + {{ user_id | tojson }};{% endif %}
+ data.body += ctx;
+ options.data = JSON.stringify(data);
+ }
+ } catch(e) { /* ignore parse errors from non-JSON payloads */ }
+ }
+ });
+ {% endif %}
});
{% endif %}
@@ -504,7 +545,9 @@ $(function() {
fontSize: '12px',
flat: true,
chatId: {{ zammad_chat_id | int }},
- title: 'Chat with us!'
+ title: 'Chat with us!'{% if user_name or user_email %},
+ name: {{ user_name | tojson }},
+ email: {{ user_email | tojson }}{% endif %}
});
})();
diff --git a/tests/test_views_help.py b/tests/test_views_help.py
index 7f3509f8..d5b918d9 100644
--- a/tests/test_views_help.py
+++ b/tests/test_views_help.py
@@ -142,6 +142,85 @@ class TestHelpViewUnit:
assert b"ZammadForm" not in resp.content
+@pytest.mark.unit
+class TestHelpViewUserContext:
+ """Tests that user context is passed to Zammad widgets."""
+
+ @staticmethod
+ def _make_app_with_session(user_data: dict | None = None):
+ """Build a minimal FastAPI app with session middleware and optional user session."""
+ from fastapi import FastAPI
+ from fastapi.testclient import TestClient
+ from starlette.middleware.sessions import SessionMiddleware
+
+ from app.views.help import router
+
+ app = FastAPI()
+ app.add_middleware(SessionMiddleware, secret_key="test-secret")
+ app.include_router(router)
+ tc = TestClient(app)
+
+ if user_data is not None:
+ # Seed a session by setting the cookie through a helper endpoint
+ from fastapi import Request as _Req
+ from fastapi.responses import JSONResponse
+
+ @app.get("/_test_set_session")
+ async def _set_session(request: _Req):
+ request.session["user"] = user_data
+ return JSONResponse({"ok": True})
+
+ tc.get("/_test_set_session")
+
+ return tc
+
+ def test_user_context_not_in_page_when_anonymous(self):
+ """Anonymous visitors should not see user context variables in the output."""
+ tc = self._make_app_with_session(user_data=None)
+ resp = tc.get("/help")
+ assert resp.status_code == 200
+ # No user context metadata should appear
+ assert b"DocuElevate User Context" not in resp.content
+
+ def test_user_context_passed_to_template_when_logged_in(self):
+ """Logged-in user's name/email should be available in the template context."""
+ tc = self._make_app_with_session(
+ user_data={
+ "name": "Test User",
+ "email": "test@example.com",
+ "preferred_username": "testuser",
+ }
+ )
+ resp = tc.get("/help")
+ assert resp.status_code == 200
+ # The template receives user_name, user_email, user_id but they only
+ # appear in the rendered HTML when Zammad widgets are enabled.
+ # With default settings (Zammad disabled), the values are still passed
+ # but not rendered. Verify the view doesn't error out.
+
+ def test_user_context_fallback_for_missing_fields(self):
+ """User session with only email should still resolve user_id correctly."""
+ tc = self._make_app_with_session(
+ user_data={
+ "email": "only-email@example.com",
+ }
+ )
+ resp = tc.get("/help")
+ assert resp.status_code == 200
+
+ def test_user_context_with_display_name_fallback(self):
+ """When 'name' is absent, display_name should be used as fallback."""
+ tc = self._make_app_with_session(
+ user_data={
+ "display_name": "Display Only",
+ "email": "display@example.com",
+ "id": "user-123",
+ }
+ )
+ resp = tc.get("/help")
+ assert resp.status_code == 200
+
+
@pytest.mark.integration
class TestHelpNavigationLink:
"""Tests that the Help link appears in the navigation."""
From 80988561860f14403436355979e4a48c10115999 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 8 Mar 2026 18:33:39 +0000
Subject: [PATCH 3/3] test(support): improve Zammad user context tests with
meaningful assertions
Enable Zammad settings via mock and verify user data appears in the
rendered HTML. Add tests for chat widget, email-only fallback, and
display_name fallback. Also improve JS variable naming in help.html.
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
---
frontend/templates/help.html | 9 ++--
tests/test_views_help.py | 82 ++++++++++++++++++++++++++++++------
2 files changed, 75 insertions(+), 16 deletions(-)
diff --git a/frontend/templates/help.html b/frontend/templates/help.html
index 76d8bdb4..bfec5db0 100644
--- a/frontend/templates/help.html
+++ b/frontend/templates/help.html
@@ -495,12 +495,14 @@ $(function() {
{% if user_name or user_email %}
{# ── Auto-fill Zammad form fields from the authenticated user context ── #}
$('#zammad-feedback-form').on('click', function() {
+ var maxAttempts = 30; /* ~3 s total wait for modal to appear */
+ var retryDelayMs = 100;
var attempts = 0;
var prefillForm = function() {
var $modal = $('.zammad-form-modal, .js-zammad-form-modal');
- if ($modal.length === 0 && attempts < 30) {
+ if ($modal.length === 0 && attempts < maxAttempts) {
attempts++;
- setTimeout(prefillForm, 100);
+ setTimeout(prefillForm, retryDelayMs);
return;
}
var $name = $modal.find('input[name="name"]');
@@ -512,7 +514,8 @@ $(function() {
if ($email.length && !$email.val()) $email.val({{ user_email | tojson }});
{% endif %}
};
- setTimeout(prefillForm, 200);
+ var initialDelayMs = 200; /* wait for modal DOM to start rendering */
+ setTimeout(prefillForm, initialDelayMs);
});
{# ── Append user metadata to the ticket body before Zammad posts it ── #}
diff --git a/tests/test_views_help.py b/tests/test_views_help.py
index d5b918d9..21a297b6 100644
--- a/tests/test_views_help.py
+++ b/tests/test_views_help.py
@@ -182,8 +182,10 @@ class TestHelpViewUserContext:
# No user context metadata should appear
assert b"DocuElevate User Context" not in resp.content
- def test_user_context_passed_to_template_when_logged_in(self):
- """Logged-in user's name/email should be available in the template context."""
+ def test_user_context_rendered_in_zammad_form_when_logged_in(self):
+ """With Zammad form enabled, logged-in user's name/email should appear in the script block."""
+ from unittest.mock import patch
+
tc = self._make_app_with_session(
user_data={
"name": "Test User",
@@ -191,25 +193,70 @@ class TestHelpViewUserContext:
"preferred_username": "testuser",
}
)
- resp = tc.get("/help")
+ with patch("app.views.help.settings") as mock_settings:
+ mock_settings.external_hostname = "localhost"
+ mock_settings.zammad_url = "https://zammad.example.com"
+ mock_settings.zammad_form_enabled = True
+ mock_settings.zammad_chat_enabled = False
+ mock_settings.zammad_chat_id = 1
+ mock_settings.support_email = None
+ resp = tc.get("/help")
assert resp.status_code == 200
- # The template receives user_name, user_email, user_id but they only
- # appear in the rendered HTML when Zammad widgets are enabled.
- # With default settings (Zammad disabled), the values are still passed
- # but not rendered. Verify the view doesn't error out.
+ assert b"Test User" in resp.content
+ assert b"test@example.com" in resp.content
+ assert b"testuser" in resp.content
+ assert b"DocuElevate User Context" in resp.content
+
+ def test_user_context_rendered_in_zammad_chat_when_logged_in(self):
+ """With Zammad chat enabled, user's name/email should appear in the ZammadChat constructor."""
+ from unittest.mock import patch
+
+ tc = self._make_app_with_session(
+ user_data={
+ "name": "Chat User",
+ "email": "chat@example.com",
+ "preferred_username": "chatuser",
+ }
+ )
+ with patch("app.views.help.settings") as mock_settings:
+ mock_settings.external_hostname = "localhost"
+ mock_settings.zammad_url = "https://zammad.example.com"
+ mock_settings.zammad_form_enabled = False
+ mock_settings.zammad_chat_enabled = True
+ mock_settings.zammad_chat_id = 1
+ mock_settings.support_email = None
+ resp = tc.get("/help")
+ assert resp.status_code == 200
+ assert b"Chat User" in resp.content
+ assert b"chat@example.com" in resp.content
+
+ def test_user_id_falls_back_to_email(self):
+ """When preferred_username is absent, user_id should resolve to email."""
+ from unittest.mock import patch
- def test_user_context_fallback_for_missing_fields(self):
- """User session with only email should still resolve user_id correctly."""
tc = self._make_app_with_session(
user_data={
"email": "only-email@example.com",
}
)
- resp = tc.get("/help")
+ with patch("app.views.help.settings") as mock_settings:
+ mock_settings.external_hostname = "localhost"
+ mock_settings.zammad_url = "https://zammad.example.com"
+ mock_settings.zammad_form_enabled = True
+ mock_settings.zammad_chat_enabled = False
+ mock_settings.zammad_chat_id = 1
+ mock_settings.support_email = None
+ resp = tc.get("/help")
assert resp.status_code == 200
+ # user_id falls back to email; check it appears in the Username metadata line
+ content = resp.text
+ assert "only-email@example.com" in content
+ assert "DocuElevate User Context" in content
+
+ def test_user_name_falls_back_to_display_name(self):
+ """When 'name' is absent, display_name should be used as the user_name fallback."""
+ from unittest.mock import patch
- def test_user_context_with_display_name_fallback(self):
- """When 'name' is absent, display_name should be used as fallback."""
tc = self._make_app_with_session(
user_data={
"display_name": "Display Only",
@@ -217,8 +264,17 @@ class TestHelpViewUserContext:
"id": "user-123",
}
)
- resp = tc.get("/help")
+ with patch("app.views.help.settings") as mock_settings:
+ mock_settings.external_hostname = "localhost"
+ mock_settings.zammad_url = "https://zammad.example.com"
+ mock_settings.zammad_form_enabled = True
+ mock_settings.zammad_chat_enabled = False
+ mock_settings.zammad_chat_id = 1
+ mock_settings.support_email = None
+ resp = tc.get("/help")
assert resp.status_code == 200
+ # display_name used as fallback for user_name
+ assert b"Display Only" in resp.content
@pytest.mark.integration