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:
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -491,6 +491,50 @@ $(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 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>
|
||||
{% endif %}
|
||||
@@ -504,7 +548,9 @@ $(function() {
|
||||
fontSize: '12px',
|
||||
flat: true,
|
||||
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>
|
||||
|
||||
@@ -142,6 +142,141 @@ 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_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
|
||||
class TestHelpNavigationLink:
|
||||
"""Tests that the Help link appears in the navigation."""
|
||||
|
||||
Reference in New Issue
Block a user