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>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-08 18:27:14 +00:00
parent d8d0aedd9e
commit 209e82f7e5
4 changed files with 148 additions and 1 deletions
+16
View File
@@ -29,6 +29,19 @@ _DOCS_BUILD_DIR = pathlib.Path(__file__).parents[2] / "docs_build"
@router.get("/help", include_in_schema=False) @router.get("/help", include_in_schema=False)
async def help_center(request: Request): async def help_center(request: Request):
"""Render the end-user Help Center page.""" """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( return templates.TemplateResponse(
"help.html", "help.html",
{ {
@@ -39,5 +52,8 @@ async def help_center(request: Request):
"zammad_chat_id": settings.zammad_chat_id, "zammad_chat_id": settings.zammad_chat_id,
"zammad_form_enabled": settings.zammad_form_enabled, "zammad_form_enabled": settings.zammad_form_enabled,
"support_email": settings.support_email, "support_email": settings.support_email,
"user_name": user_name,
"user_email": user_email,
"user_id": user_id,
}, },
) )
+9
View File
@@ -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. > **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 ## Duplicate Document Detection
DocuElevate detects and flags documents that share the same content, even if they arrive as separate uploads. DocuElevate detects and flags documents that share the same content, even if they arrive as separate uploads.
+44 -1
View File
@@ -491,6 +491,47 @@ $(function() {
messageThankYou: 'Thank you for your request (#%s)! We will get back to you shortly.', messageThankYou: 'Thank you for your request (#%s)! We will get back to you shortly.',
modal: true 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 %}
}); });
</script> </script>
{% endif %} {% endif %}
@@ -504,7 +545,9 @@ $(function() {
fontSize: '12px', fontSize: '12px',
flat: true, flat: true,
chatId: {{ zammad_chat_id | int }}, chatId: {{ zammad_chat_id | int }},
title: '<strong>Chat</strong> with us!' title: '<strong>Chat</strong> with us!'{% if user_name or user_email %},
name: {{ user_name | tojson }},
email: {{ user_email | tojson }}{% endif %}
}); });
})(); })();
</script> </script>
+79
View File
@@ -142,6 +142,85 @@ class TestHelpViewUnit:
assert b"ZammadForm" not in resp.content 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 @pytest.mark.integration
class TestHelpNavigationLink: class TestHelpNavigationLink:
"""Tests that the Help link appears in the navigation.""" """Tests that the Help link appears in the navigation."""