(template) allow to insert recipient name in reply

This commit is contained in:
Sabrina Demagny
2025-10-29 15:54:07 +01:00
parent bcb6215d93
commit f7ee9dcc47
10 changed files with 127 additions and 12 deletions
+13
View File
@@ -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",
@@ -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(
+7 -4
View File
@@ -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", {})
@@ -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="<p>Hello {recipient_name}!</p><p> My name is {name} and I'm {job_title}.</p>",
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()
@@ -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<mailboxesMessageTemplatesRenderRetrieveResponse> => {
return fetchAPI<mailboxesMessageTemplatesRenderRetrieveResponse>(
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<ReturnType<typeof mailboxesMessageTemplatesRenderRetrieve>>
> = ({ 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,
);
@@ -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";
@@ -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;
};
@@ -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<string, string>;
}
/**
* 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<MessageComposerBlockSchema, MessageComposerInlineContentSchema, MessageComposerStyleSchema>();
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");
@@ -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,
@@ -212,6 +212,11 @@ export const MessageComposer = ({ mailboxId, blockNoteOptions, defaultValue, quo
<Toolbar>
<MessageTemplateSelector
mailboxId={mailboxId}
context={{
recipient_name: draft
? draft.to.map(to => to.contact.name).join(", ")
: quotedMessage?.sender?.name || ""
}}
/>
<SignatureTemplateSelector
templates={activeSignatures}