Merge pull request #557 from christianlouis/copilot/auto-fill-data-for-chat-ticket

feat(support): auto-fill Zammad chat/ticket widgets with user context
This commit is contained in:
Christian Krakau-Louis
2026-03-08 19:59:12 +01:00
committed by GitHub
4 changed files with 207 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.
+47 -1
View File
@@ -491,6 +491,50 @@ $(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 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 < maxAttempts) {
attempts++;
setTimeout(prefillForm, retryDelayMs);
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 %}
};
var initialDelayMs = 200; /* wait for modal DOM to start rendering */
setTimeout(prefillForm, initialDelayMs);
});
{# 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 +548,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>
+135
View File
@@ -142,6 +142,141 @@ 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_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",
"email": "test@example.com",
"preferred_username": "testuser",
}
)
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
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
tc = self._make_app_with_session(
user_data={
"email": "only-email@example.com",
}
)
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
tc = self._make_app_with_session(
user_data={
"display_name": "Display Only",
"email": "display@example.com",
"id": "user-123",
}
)
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 @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."""