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:
@@ -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,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 %}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
@@ -504,7 +545,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,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."""
|
||||
|
||||
Reference in New Issue
Block a user