From f7ee9dcc47a20783efea401b38bcc080413a8dd8 Mon Sep 17 00:00:00 2001 From: Sabrina Demagny Date: Sun, 26 Oct 2025 23:05:01 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(template)=20allow=20to=20insert=20rec?= =?UTF-8?q?ipient=20name=20in=20reply?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/core/api/openapi.json | 13 +++++++ .../core/api/viewsets/message_template.py | 17 ++++++++- src/backend/core/models.py | 11 ++++-- .../tests/api/test_message_template_render.py | 38 +++++++++++++++++++ .../features/api/gen/mailboxes/mailboxes.ts | 31 +++++++++++++-- .../src/features/api/gen/models/index.ts | 1 + ...essage_templates_render_retrieve_params.ts | 14 +++++++ .../message-template-block/index.tsx | 8 ++-- .../blocknote/signature-block/index.tsx | 1 + .../components/message-composer/index.tsx | 5 +++ 10 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 src/frontend/src/features/api/gen/models/mailboxes_message_templates_render_retrieve_params.ts 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 || "" + }} />