diff --git a/src/backend/core/api/openapi.json b/src/backend/core/api/openapi.json
index 0502fe29..bdf90cee 100644
--- a/src/backend/core/api/openapi.json
+++ b/src/backend/core/api/openapi.json
@@ -2689,6 +2689,19 @@
"operationId": "mailboxes_message_templates_render_retrieve",
"description": "Render a template with the provided context variables.",
"parameters": [
+ {
+ "in": "query",
+ "name": "*",
+ "schema": {
+ "type": "string"
+ },
+ "description": "Any other parameter will be available in the template context",
+ "examples": {
+ "Example": {
+ "value": "value"
+ }
+ }
+ },
{
"in": "path",
"name": "id",
diff --git a/src/backend/core/api/viewsets/message_template.py b/src/backend/core/api/viewsets/message_template.py
index 7d5786d0..2c22fdd8 100644
--- a/src/backend/core/api/viewsets/message_template.py
+++ b/src/backend/core/api/viewsets/message_template.py
@@ -4,6 +4,7 @@ from django.db.models import Q
from django.utils.functional import cached_property
from drf_spectacular.utils import (
+ OpenApiExample,
OpenApiParameter,
OpenApiResponse,
OpenApiTypes,
@@ -75,6 +76,16 @@ class MailboxMessageTemplateViewSet(
return context
@extend_schema(
+ parameters=[
+ OpenApiParameter(
+ name="*",
+ type=OpenApiTypes.STR,
+ location=OpenApiParameter.QUERY,
+ description="Any other parameter will be available in the template context",
+ required=False,
+ examples=[OpenApiExample("Example", value="value")],
+ )
+ ],
responses={
200: OpenApiResponse(
description="Template rendered with provided context",
@@ -95,7 +106,11 @@ class MailboxMessageTemplateViewSet(
"""Render a template."""
template = self.get_object()
try:
- rendered = template.render_template(mailbox=self.mailbox, user=request.user)
+ rendered = template.render_template(
+ mailbox=self.mailbox,
+ user=request.user,
+ context=request.query_params.dict(),
+ )
return Response(rendered)
except (KeyError, ValueError, TypeError) as e:
return Response(
diff --git a/src/backend/core/models.py b/src/backend/core/models.py
index a241e75b..9134628a 100644
--- a/src/backend/core/models.py
+++ b/src/backend/core/models.py
@@ -1,7 +1,7 @@
"""
Declare and configure the models for the messages core application
"""
-# pylint: disable=too-many-lines,too-many-instance-attributes
+# pylint: disable=too-many-lines,too-many-instance-attributes,dangerous-default-value
import base64
import hashlib
@@ -1822,7 +1822,10 @@ class MessageTemplate(BaseModel):
return None
def render_template(
- self, mailbox: Mailbox = None, user: User = None
+ self,
+ mailbox: Mailbox = None,
+ user: User = None,
+ context: Dict[str, Any] = {}, # noqa: B006
) -> Dict[str, str]:
"""
Render the template with the given context.
@@ -1834,12 +1837,12 @@ class MessageTemplate(BaseModel):
Returns:
Dictionary with 'html_body' and 'text_body' keys containing rendered content
"""
- name = (
+ context = context.copy()
+ context["name"] = (
mailbox.contact.name
if mailbox and mailbox.contact
else (getattr(user, "full_name", None) if user else "")
)
- context = {"name": name}
schema = settings.SCHEMA_CUSTOM_ATTRIBUTES_USER
schema_properties = schema.get("properties", {})
diff --git a/src/backend/core/tests/api/test_message_template_render.py b/src/backend/core/tests/api/test_message_template_render.py
index c58f387b..af1d6287 100644
--- a/src/backend/core/tests/api/test_message_template_render.py
+++ b/src/backend/core/tests/api/test_message_template_render.py
@@ -164,6 +164,44 @@ class TestMessageTemplateRender:
assert "Cordialement, John Doe - Adjointe" in response.data["html_body"]
assert "Cordialement, John Doe - Adjointe" in response.data["text_body"]
+ @patch(
+ "django.conf.settings.SCHEMA_CUSTOM_ATTRIBUTES_USER",
+ {"properties": {"job_title": {"type": "string"}}},
+ )
+ @pytest.mark.parametrize(
+ "role",
+ [
+ models.MailboxRoleChoices.EDITOR,
+ models.MailboxRoleChoices.SENDER,
+ models.MailboxRoleChoices.VIEWER,
+ models.MailboxRoleChoices.ADMIN,
+ ],
+ )
+ def test_success_with_context(self, user, mailbox, role):
+ """Test successful template rendering with context."""
+ factories.MailboxAccessFactory(
+ mailbox=mailbox,
+ user=user,
+ role=role,
+ )
+ template_reply = factories.MessageTemplateFactory(
+ html_body="
Hello {recipient_name}!
My name is {name} and I'm {job_title}.
",
+ text_body="Hello {recipient_name}! My name is {name} and I'm {job_title}.",
+ mailbox=mailbox,
+ )
+ client = APIClient()
+ client.force_authenticate(user=user)
+ url = reverse(
+ "mailbox-message-templates-render-template",
+ kwargs={"mailbox_id": mailbox.id, "pk": template_reply.id},
+ )
+ response = client.get(f"{url}?recipient_name=Jane Smith")
+ assert response.status_code == status.HTTP_200_OK
+ assert "Hello Jane Smith!" in response.data["html_body"]
+ assert "Hello Jane Smith!" in response.data["text_body"]
+ assert "My name is John Doe and I'm Adjointe." in response.data["html_body"]
+ assert "My name is John Doe and I'm Adjointe." in response.data["text_body"]
+
def test_render_template_no_access_mailbox(self, user, mailbox):
"""Test rendering a template from a mailbox that doesn't exist."""
client = APIClient()
diff --git a/src/frontend/src/features/api/gen/mailboxes/mailboxes.ts b/src/frontend/src/features/api/gen/mailboxes/mailboxes.ts
index 354ee397..33face90 100644
--- a/src/frontend/src/features/api/gen/mailboxes/mailboxes.ts
+++ b/src/frontend/src/features/api/gen/mailboxes/mailboxes.ts
@@ -27,6 +27,7 @@ import type {
MailboxesMessageTemplatesAvailableListParams,
MailboxesMessageTemplatesListParams,
MailboxesMessageTemplatesRenderRetrieve200,
+ MailboxesMessageTemplatesRenderRetrieveParams,
MailboxesSearchListParams,
MessageTemplate,
MessageTemplateRequest,
@@ -1070,17 +1071,31 @@ export type mailboxesMessageTemplatesRenderRetrieveResponse =
export const getMailboxesMessageTemplatesRenderRetrieveUrl = (
mailboxId: string,
id: string,
+ params?: MailboxesMessageTemplatesRenderRetrieveParams,
) => {
- return `/api/v1.0/mailboxes/${mailboxId}/message-templates/${id}/render/`;
+ const normalizedParams = new URLSearchParams();
+
+ Object.entries(params || {}).forEach(([key, value]) => {
+ if (value !== undefined) {
+ normalizedParams.append(key, value === null ? "null" : value.toString());
+ }
+ });
+
+ const stringifiedParams = normalizedParams.toString();
+
+ return stringifiedParams.length > 0
+ ? `/api/v1.0/mailboxes/${mailboxId}/message-templates/${id}/render/?${stringifiedParams}`
+ : `/api/v1.0/mailboxes/${mailboxId}/message-templates/${id}/render/`;
};
export const mailboxesMessageTemplatesRenderRetrieve = async (
mailboxId: string,
id: string,
+ params?: MailboxesMessageTemplatesRenderRetrieveParams,
options?: RequestInit,
): Promise => {
return fetchAPI(
- getMailboxesMessageTemplatesRenderRetrieveUrl(mailboxId, id),
+ getMailboxesMessageTemplatesRenderRetrieveUrl(mailboxId, id, params),
{
...options,
method: "GET",
@@ -1091,9 +1106,11 @@ export const mailboxesMessageTemplatesRenderRetrieve = async (
export const getMailboxesMessageTemplatesRenderRetrieveQueryKey = (
mailboxId: string,
id: string,
+ params?: MailboxesMessageTemplatesRenderRetrieveParams,
) => {
return [
`/api/v1.0/mailboxes/${mailboxId}/message-templates/${id}/render/`,
+ ...(params ? [params] : []),
] as const;
};
@@ -1103,6 +1120,7 @@ export const getMailboxesMessageTemplatesRenderRetrieveQueryOptions = <
>(
mailboxId: string,
id: string,
+ params?: MailboxesMessageTemplatesRenderRetrieveParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -1118,12 +1136,12 @@ export const getMailboxesMessageTemplatesRenderRetrieveQueryOptions = <
const queryKey =
queryOptions?.queryKey ??
- getMailboxesMessageTemplatesRenderRetrieveQueryKey(mailboxId, id);
+ getMailboxesMessageTemplatesRenderRetrieveQueryKey(mailboxId, id, params);
const queryFn: QueryFunction<
Awaited>
> = ({ signal }) =>
- mailboxesMessageTemplatesRenderRetrieve(mailboxId, id, {
+ mailboxesMessageTemplatesRenderRetrieve(mailboxId, id, params, {
signal,
...requestOptions,
});
@@ -1151,6 +1169,7 @@ export function useMailboxesMessageTemplatesRenderRetrieve<
>(
mailboxId: string,
id: string,
+ params: undefined | MailboxesMessageTemplatesRenderRetrieveParams,
options: {
query: Partial<
UseQueryOptions<
@@ -1179,6 +1198,7 @@ export function useMailboxesMessageTemplatesRenderRetrieve<
>(
mailboxId: string,
id: string,
+ params?: MailboxesMessageTemplatesRenderRetrieveParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -1207,6 +1227,7 @@ export function useMailboxesMessageTemplatesRenderRetrieve<
>(
mailboxId: string,
id: string,
+ params?: MailboxesMessageTemplatesRenderRetrieveParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -1228,6 +1249,7 @@ export function useMailboxesMessageTemplatesRenderRetrieve<
>(
mailboxId: string,
id: string,
+ params?: MailboxesMessageTemplatesRenderRetrieveParams,
options?: {
query?: Partial<
UseQueryOptions<
@@ -1245,6 +1267,7 @@ export function useMailboxesMessageTemplatesRenderRetrieve<
const queryOptions = getMailboxesMessageTemplatesRenderRetrieveQueryOptions(
mailboxId,
id,
+ params,
options,
);
diff --git a/src/frontend/src/features/api/gen/models/index.ts b/src/frontend/src/features/api/gen/models/index.ts
index 3f7e1c52..2227992a 100644
--- a/src/frontend/src/features/api/gen/models/index.ts
+++ b/src/frontend/src/features/api/gen/models/index.ts
@@ -70,6 +70,7 @@ export * from "./mailboxes_message_templates_available_list_type";
export * from "./mailboxes_message_templates_list_params";
export * from "./mailboxes_message_templates_list_type_item";
export * from "./mailboxes_message_templates_render_retrieve200";
+export * from "./mailboxes_message_templates_render_retrieve_params";
export * from "./mailboxes_search_list_params";
export * from "./maildomain_access_read";
export * from "./maildomain_access_write";
diff --git a/src/frontend/src/features/api/gen/models/mailboxes_message_templates_render_retrieve_params.ts b/src/frontend/src/features/api/gen/models/mailboxes_message_templates_render_retrieve_params.ts
new file mode 100644
index 00000000..a10c1f41
--- /dev/null
+++ b/src/frontend/src/features/api/gen/models/mailboxes_message_templates_render_retrieve_params.ts
@@ -0,0 +1,14 @@
+/**
+ * Generated by orval v7.10.0 🍺
+ * Do not edit manually.
+ * messages API
+ * This is the messages API schema.
+ * OpenAPI spec version: 1.0.0 (v1.0)
+ */
+
+export type MailboxesMessageTemplatesRenderRetrieveParams = {
+ /**
+ * Any other parameter will be available in the template context
+ */
+ "*"?: string;
+};
diff --git a/src/frontend/src/features/blocknote/message-template-block/index.tsx b/src/frontend/src/features/blocknote/message-template-block/index.tsx
index 54b10fa6..cd7878da 100644
--- a/src/frontend/src/features/blocknote/message-template-block/index.tsx
+++ b/src/frontend/src/features/blocknote/message-template-block/index.tsx
@@ -2,19 +2,20 @@ import { useBlockNoteEditor, useComponentsContext } from "@blocknote/react";
import { useTranslation } from "react-i18next";
import { Icon, IconSize, Spinner } from "@gouvfr-lasuite/ui-kit";
import { Modal, ModalSize } from "@openfun/cunningham-react";
-import { MessageTemplateTypeChoices, ReadOnlyMessageTemplate, useMailboxesMessageTemplatesAvailableList, mailboxesMessageTemplatesRenderRetrieve } from "@/features/api/gen";
+import { MessageTemplateTypeChoices, ReadOnlyMessageTemplate, useMailboxesMessageTemplatesAvailableList, mailboxesMessageTemplatesRenderRetrieve, MailboxesMessageTemplatesRenderRetrieveParams } from "@/features/api/gen";
import { MessageComposerBlockSchema, MessageComposerInlineContentSchema, MessageComposerStyleSchema, PartialMessageComposerBlockSchema } from "@/features/forms/components/message-composer";
import { useModal } from "@openfun/cunningham-react";
type MessageTemplateSelectorProps = {
mailboxId: string;
+ context?: Record;
}
/**
* A BlockNote toolbar selector which allows the user to select a message template
* from all active templates for a given mailbox.
*/
-export const MessageTemplateSelector = ({ mailboxId }: MessageTemplateSelectorProps) => {
+export const MessageTemplateSelector = ({ mailboxId, context = {} }: MessageTemplateSelectorProps) => {
const { t } = useTranslation();
const editor = useBlockNoteEditor();
const Components = useComponentsContext()!;
@@ -35,7 +36,8 @@ export const MessageTemplateSelector = ({ mailboxId }: MessageTemplateSelectorPr
// Get rendered template content (allows to use placeholders)
const { data: renderedTemplate } = await mailboxesMessageTemplatesRenderRetrieve(
mailboxId,
- template.id
+ template.id,
+ context as MailboxesMessageTemplatesRenderRetrieveParams,
);
if (!renderedTemplate?.html_body) {
console.error("Failed to render template");
diff --git a/src/frontend/src/features/blocknote/signature-block/index.tsx b/src/frontend/src/features/blocknote/signature-block/index.tsx
index 9f7e85d9..8b4ea9ed 100644
--- a/src/frontend/src/features/blocknote/signature-block/index.tsx
+++ b/src/frontend/src/features/blocknote/signature-block/index.tsx
@@ -154,6 +154,7 @@ export const BlockSignature = createReactBlockSpec(
const { data: { data: preview = null } = {}, isLoading } = useMailboxesMessageTemplatesRenderRetrieve(
props.mailboxId,
props.templateId,
+ {},
{
query: {
enabled: !!props.mailboxId && !!props.templateId,
diff --git a/src/frontend/src/features/forms/components/message-composer/index.tsx b/src/frontend/src/features/forms/components/message-composer/index.tsx
index bc6f4e64..b747ffee 100644
--- a/src/frontend/src/features/forms/components/message-composer/index.tsx
+++ b/src/frontend/src/features/forms/components/message-composer/index.tsx
@@ -212,6 +212,11 @@ export const MessageComposer = ({ mailboxId, blockNoteOptions, defaultValue, quo
to.contact.name).join(", ")
+ : quotedMessage?.sender?.name || ""
+ }}
/>