[IMP] mail: improve render mixin options and usage

PURPOSE

Cleanup usage of rendering mixin: clearly support void recordsets, add tests,
cleanup options.

SPECIFICATIONS

mail: support rendering on void or falsy record sets

In this commit we support calling rendering methods on void or falsy record
sets. Currently some rendering methods prevent from having 'None', some do
nothing specific. As Qweb or inline template support rendering on void records
we can safely support rendering on [], (), [False], [None]. Returned result
will either be void, either contain the rendered on void record for the
False or None key.

In this commit we also add a protection against trying to render a non
existing field. Instead of raising a KeyError, a ValueError is raised with
a clearer error message.

test_mail: add some tests on content translation

Purpose of this commit is to add tests on translations when using templates
and composer, on both email content (body, subject) and email layouting
content (view content, groups-based buttons, ...). This better highlights
some missing part of translation support, notably

  * when being in mass mode: as content is taken from the composer and rendered
    dynamically, it does not take the original translation from the template
    even if it matches template content exactly;
  * access button title is not translated as it is added during a separate
    part of the process;

mail, various: cleanup usage of options / post process in render mixin

Due to recent improvements (Jinja -> Qweb, safe rendering) rendering API
supports several ways of giving options to the rendering process. Those
have mainly two usage

  * ``preserve_comments`` : keep comments in rendered HTML, used notably
    in mass mailing or digest to keep browser-specific comments;
  * ``post_process`` : perform a post processing on rendered HTML, used notably
    to process local links and add tracking to shortened links;

All those are now given directly inside an optional ``options`` parameter
given to ``_render_field`` and its sub-method ``_render_template``.

They can also be defined as field level, using ``render_options`` field
parameter. Some fields are updated accordingly

  * composer mixin body (which impacts all inheriting models, notably
    mail composer, survey and elearning invite wizards as well as appraisal
    and appraisal feedback): post processing is now always done by default.
    It was already done manually in calls that can be simplified;
  * mail template body_html: post processing is now always done by default. It
    was already done manually in calls that can be simplified;
  * mailing body_html and preview are now post processed by default. It was
    already the case in testing wizard. Preview is updated to avoid post
    processing of links, as it was before. Remaining use case is the sending
    which uses the mail composer, therefore was already post processed;

LINKS

Task-2710804 (Mail: Clean MailThread API)
Prepares Task-3046371 (Mail: Improve language propagation in mail and layouting)
Prepares Task-2088884 (Mail: Use editable computed stored fields in composer)

closes odoo/odoo#106072

Related: odoo/enterprise#34147
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2022-11-21 19:55:14 +01:00
committed by GitHub
20 changed files with 354 additions and 125 deletions
+1 -2
View File
@@ -123,8 +123,7 @@ class Attendee(models.Model):
body = mail_template._render_field(
'body_html',
attendee.ids,
compute_lang=True,
post_process=True)[attendee.id]
compute_lang=True)[attendee.id]
subject = mail_template._render_field(
'subject',
attendee.ids,
+13 -3
View File
@@ -165,8 +165,10 @@ class Digest(models.Model):
'tips': self._compute_tips(user.company_id, user, tips_count=tips_count, consumed=consume_tips),
'preferences': self._compute_preferences(user.company_id, user),
},
post_process=True,
options={'preserve_comments': True}
options={
'preserve_comments': True,
'post_process': True,
},
)[self.id]
full_mail = self.env['mail.render.mixin']._render_encapsulate(
'digest.digest_mail_layout',
@@ -288,7 +290,15 @@ class Digest(models.Model):
'|', ('group_id', 'in', user.groups_id.ids), ('group_id', '=', False)
], limit=tips_count)
tip_descriptions = [
tools.html_sanitize(self.env['mail.render.mixin'].sudo()._render_template(tip.tip_description, 'digest.tip', tip.ids, post_process=True, engine="qweb")[tip.id])
tools.html_sanitize(
self.env['mail.render.mixin'].sudo()._render_template(
tip.tip_description,
'digest.tip',
tip.ids,
engine="qweb",
options={'post_process': True},
)[tip.id]
)
for tip in tips
]
if consumed:
+3 -1
View File
@@ -22,7 +22,9 @@ class MailComposerMixin(models.AbstractModel):
# Content
subject = fields.Char('Subject', compute='_compute_subject', readonly=False, store=True)
body = fields.Html('Contents', compute='_compute_body', render_engine='qweb', store=True, readonly=False, sanitize=False)
body = fields.Html(
'Contents', compute='_compute_body', readonly=False, store=True,
render_engine='qweb', render_options={'post_process': True}, sanitize=False)
template_id = fields.Many2one('mail.template', 'Mail Template', domain="[('model', '=', render_model)]")
# Access
is_mail_template_editor = fields.Boolean('Is Editor', compute='_compute_is_mail_template_editor')
+45 -27
View File
@@ -284,12 +284,13 @@ class MailRenderMixin(models.AbstractModel):
:param dict add_context: additional context to give to renderer. It
allows to add or update values to base rendering context generated
by ``MailRenderMixin._render_eval_context()``;
:param dict options: options for rendering (not used currently);
:param dict options: options for rendering propagated to IrQweb render
(see docstring for available options);
:return dict: {res_id: string of rendered template based on record}
"""
results = dict.fromkeys(res_ids, u"")
if not template_src:
if not template_src or not res_ids:
return results
# prepare template variables
@@ -337,15 +338,14 @@ class MailRenderMixin(models.AbstractModel):
:param dict add_context: additional context to give to renderer. It
allows to add or update values to base rendering context generated
by ``MailRenderMixin._render_eval_context()``;
:param dict options: options for rendering (not used currently);
:param dict options: options for rendering propagated to IrQweb render
(see docstring for available options);
:return dict: {res_id: string of rendered template based on record}
"""
# prevent wrong values (rendering on a void record set, ...)
if any(r is None for r in res_ids):
raise ValueError(_('Template rendering should be called on a valid record IDs.'))
results = {}
if not res_ids:
return results
# prepare template variables
variables = self._render_eval_context()
@@ -380,16 +380,13 @@ class MailRenderMixin(models.AbstractModel):
:param dict add_context: additional context to give to renderer. It
allows to add or update values to base rendering context generated
by ``MailRenderMixin._render_inline_template_eval_context()``;
:param dict options: options for rendering;
:param dict options: options for rendering (no options available
currently);
:return dict: {res_id: string of rendered template based on record}
"""
# prevent wrong values (rendering on a void record set, ...)
if any(r is None for r in res_ids):
raise ValueError(_('Template rendering should be called on a valid record IDs.'))
results = dict.fromkeys(res_ids, u"")
if not template_txt:
if not template_txt or not res_ids:
return results
template_instructions = parse_inline_template(str(template_txt))
@@ -440,7 +437,7 @@ class MailRenderMixin(models.AbstractModel):
@api.model
def _render_template(self, template_src, model, res_ids, engine='inline_template',
add_context=None, options=None, post_process=False):
add_context=None, options=None):
""" Render the given string on records designed by model / res_ids using
the given rendering engine. Possible engine are small_web, qweb, or
qweb_view.
@@ -455,12 +452,20 @@ class MailRenderMixin(models.AbstractModel):
:param dict add_context: additional context to give to renderer. It
allows to add or update values to base rendering context generated
by ``MailRenderMixin._render_<engine>_eval_context()``;
:param dict options: options for rendering;
:param boolean post_process: perform a post processing on rendered result
(notably html links management). See``_render_template_postprocess``;
:param dict options: options for rendering. Use in this method and also
propagated to rendering sub-methods. May contain notably
boolean post_process: perform a post processing on rendered result
(notably html links management). See``_render_template_postprocess``;
boolean preserve_comments: if set, comments are preserved. Default
behavior is to remove them. It is used notably for browser-specific
code implemented like comments;
:return dict: {res_id: string of rendered template based on record}
"""
if options is None:
options = {}
if not isinstance(res_ids, (list, tuple)):
raise ValueError(_('Template rendering should be called only using on a list of IDs.'))
if engine not in ('inline_template', 'qweb', 'qweb_view'):
@@ -476,7 +481,7 @@ class MailRenderMixin(models.AbstractModel):
rendered = self._render_template_inline_template(template_src, model, res_ids,
add_context=add_context, options=options)
if post_process:
if options.get('post_process'):
rendered = self._render_template_postprocess(rendered)
return rendered
@@ -529,7 +534,7 @@ class MailRenderMixin(models.AbstractModel):
def _render_field(self, field, res_ids, engine='inline_template',
compute_lang=False, set_lang=False,
add_context=None, options=None, post_process=False):
add_context=None, options=None):
""" Given some record ids, render a template located on field on all
records. ``field`` should be a field of self (i.e. ``body_html`` on
``mail.template``). res_ids are record IDs linked to ``model`` field
@@ -548,12 +553,25 @@ class MailRenderMixin(models.AbstractModel):
``compute_lang`` is False;
:param dict add_context: additional context to give to renderer;
:param dict options: options for rendering;
:param boolean post_process: perform a post processing on rendered result
(notably html links management). See``_render_template_postprocess``);
:param dict options: options for rendering. Use in this method and also
propagated to rendering sub-methods. Base values come from the field
(coming from ``render_options`` parameter) and are updated by this
optional dictionary. May contain notably
boolean post_process: perform a post processing on rendered result
(notably html links management). See``_render_template_postprocess``;
boolean preserve_comments: if set, comments are preserved. Default
behavior is to remove them. It is used notably for browser-specific
code implemented like comments;
:return dict: {res_id: string of rendered template based on record}
"""
if field not in self:
raise ValueError(
_('Rendering of %(field_name)s is not possible as not defined on template.',
field_name=field
)
)
if options is None:
options = {}
@@ -565,10 +583,11 @@ class MailRenderMixin(models.AbstractModel):
else:
templates_res_ids = {self._context.get('lang'): (self, res_ids)}
# rendering options
# rendering options (update default defined on field by asked options)
engine = getattr(self._fields[field], 'render_engine', engine)
options.update(**getattr(self._fields[field], 'render_options', {}))
post_process = options.get('post_process') or post_process
field_options = getattr(self._fields[field], 'render_options', {})
if options:
field_options.update(**options)
return dict(
(res_id, rendered)
@@ -579,7 +598,6 @@ class MailRenderMixin(models.AbstractModel):
tpl_res_ids,
engine=engine,
add_context=add_context,
options=options,
post_process=post_process
options=field_options,
).items()
)
+4 -3
View File
@@ -56,7 +56,9 @@ class MailTemplate(models.Model):
email_cc = fields.Char('Cc', help="Carbon copy recipients (placeholders may be used here)")
reply_to = fields.Char('Reply To', help="Email address to which replies will be redirected when sending emails in mass; only used when the reply is not logged in the original discussion thread.")
# content
body_html = fields.Html('Body', render_engine='qweb', translate=True, prefetch=True, sanitize=False)
body_html = fields.Html(
'Body', render_engine='qweb', render_options={'post_process': True},
prefetch=True, translate=True, sanitize=False)
attachment_ids = fields.Many2many('ir.attachment', 'email_template_attachment_rel', 'email_template_id',
'attachment_id', 'Attachments',
help="You may attach files to this template, to be added to all "
@@ -246,8 +248,7 @@ class MailTemplate(models.Model):
for lang, (template, template_res_ids) in self._classify_per_lang(res_ids).items():
for field in fields:
generated_field_values = template._render_field(
field, template_res_ids,
post_process=(field == 'body_html')
field, template_res_ids
)
for res_id, field_value in generated_field_values.items():
results.setdefault(res_id, dict())[field] = field_value
+14 -9
View File
@@ -19,7 +19,7 @@ from odoo.addons.mail.models.mail_mail import MailMail
from odoo.addons.mail.models.mail_message import Message
from odoo.addons.mail.models.mail_notification import MailNotification
from odoo.tests import common, new_test_user
from odoo.tools import formataddr, pycompat
from odoo.tools import formataddr, mute_logger, pycompat
from odoo.tools.translate import code_translations
mail_new_test_user = partial(new_test_user, context={'mail_create_nolog': True,
@@ -1102,9 +1102,9 @@ class MailCommon(common.TransactionCase, MailCase):
* 'English Layout for' -> Spanish Layout para
* model
* description: English: Lang Chatter Model (depends on test_record._name)
translated: Spanish description
translated: Spanish Model Description
* module
* _('TestStuff') -> TestSpanishStuff (used as link button name in layout)
* _('NotificationButtonTitle') -> SpanishNotificationButtonTitle (used as link button name in layout)
* _('View %s') -> SpanishView %s
* template
* body: English: <p>EnglishBody for <t t-out="object.name"/></p> (depends on test_template.body)
@@ -1114,17 +1114,22 @@ class MailCommon(common.TransactionCase, MailCase):
"""
# activate translations
cls.env['res.lang']._activate_lang(lang_code)
cls.env.ref('base.module_base')._update_translations([lang_code])
with mute_logger("odoo.addons.base.models.ir_module", "odoo.tools.translate"):
cls.env.ref('base.module_base')._update_translations([lang_code])
cls.env.ref('base.module_mail')._update_translations([lang_code])
cls.env.ref('base.module_test_mail')._update_translations([lang_code])
code_translations.get_python_translations('mail', lang_code)
code_translations.get_python_translations('test_mail', lang_code)
# Make sure Spanish translations have not been altered
if test_record:
cls.env['ir.model']._get(test_record._name).with_context(lang=lang_code).name = 'Spanish description'
cls.env['ir.model']._get(test_record._name).with_context(lang=lang_code).name = 'Spanish Model Description'
# Translate some code strings used in mailing
code_translations.python_translations[('test_mail', 'es_ES')] = {'TestStuff': 'TestSpanishStuff'}
cls.addClassCleanup(code_translations.python_translations.pop, ('test_mail', 'es_ES'))
code_translations.python_translations[('mail', 'es_ES')] = {'View %s': 'SpanishView %s'}
cls.addClassCleanup(code_translations.python_translations.pop, ('mail', 'es_ES'))
code_translations.python_translations[('test_mail', 'es_ES')]['NotificationButtonTitle'] = 'SpanishButtonTitle'
cls.addClassCleanup(code_translations.python_translations[('test_mail', 'es_ES')].pop, 'NotificationButtonTitle')
code_translations.python_translations[('mail', 'es_ES')]['View %s'] = 'SpanishView %s'
cls.addClassCleanup(code_translations.python_translations[('mail', 'es_ES')].pop, 'View %s')
# Prepare some translated value for template if given
if test_template:
+44
View File
@@ -97,6 +97,13 @@ class TestMailRenderCommon(common.MailCommon):
<span>Autre Narrateur</span>
</p>"""
]
cls.base_rendered_void = [
'<p>Hello</p>',
'<p>Hello </p>',
"""<p>
<span>English Speaker</span>
</p>"""
]
# link to mail template
cls.test_template = cls.env['mail.template'].create({
@@ -198,6 +205,43 @@ class TestMailRender(TestMailRenderCommon):
)[partner.id]
self.assertEqual(rendered, expected)
@users('employee')
def test_render_field_no_records(self):
""" Test rendering on void IDs, or a list with dummy / falsy ID """
template = self.test_template.with_env(self.env)
partner = self.render_object.with_env(self.env)
for res_ids in ([], (), [False], [''], [None], [False, partner.id]): # various corner cases
for fname, expected_obj, expected_void in zip(['subject', 'body_html'], self.base_rendered, self.base_rendered_void):
with self.subTest():
rendered_all = template._render_field(
fname,
res_ids,
compute_lang=True
)
if res_ids:
self.assertTrue(res_ids[0] in rendered_all,
f'Rendering: key {repr(res_ids[0])} is considered as valid and should have an entry')
self.assertEqual(rendered_all[res_ids[0]], expected_void)
if len(res_ids) == 2: # second is partner
self.assertTrue(res_ids[1] in rendered_all)
self.assertEqual(rendered_all[res_ids[1]], expected_obj)
if not res_ids:
self.assertFalse(rendered_all,
'Rendering: void input -> void output')
@users('employee')
def test_render_field_not_existing(self):
""" Test trying to render a not-existing field: raise a proper ValueError
instead of crashing / raising a KeyError """
template = self.env['mail.template'].browse(self.test_template.ids)
partner = self.env['res.partner'].browse(self.render_object_fr.ids)
with self.assertRaises(ValueError):
_rendered = template._render_field(
'not_existing',
partner.ids,
compute_lang=True
)[partner.id]
@users('employee')
def test_render_template_inline_template(self):
partner = self.env['res.partner'].browse(self.render_object.ids)
+4 -2
View File
@@ -89,7 +89,9 @@ class MailComposer(models.TransientModel):
# content
subject = fields.Char('Subject', compute=False)
body = fields.Html('Contents', render_engine='qweb', compute=False, default='', sanitize_style=True)
body = fields.Html(
'Contents', render_engine='qweb', render_options={'post_process': True},
compute=False, default='', sanitize_style=True)
parent_id = fields.Many2one(
'mail.message', 'Parent Message', ondelete='set null')
template_id = fields.Many2one('mail.template', 'Use template', domain="[('model', '=', model)]")
@@ -658,7 +660,7 @@ class MailComposer(models.TransientModel):
subjects = self._render_field('subject', res_ids)
# We want to preserve comments in emails so as to keep mso conditionals
bodies = self._render_field('body', res_ids, post_process=True, options={'preserve_comments': self.composition_mode == 'mass_mail'})
bodies = self._render_field('body', res_ids, options={'preserve_comments': self.composition_mode == 'mass_mail'})
emails_from = self._render_field('email_from', res_ids)
replies_to = self._render_field('reply_to', res_ids)
default_recipients = {}
+7 -1
View File
@@ -167,7 +167,13 @@ class MassMailController(http.Controller):
if not self._valid_unsubscribe_token(mailing_id, res_id, email, str(token)) and not request.env.user.has_group('mass_mailing.group_mass_mailing_user'):
raise exceptions.AccessDenied()
html_markupsafe = mailing._render_field('body_html', [res_id])[res_id]
# do not force lang, will simply use user context
html_markupsafe = mailing._render_field(
'body_html',
[res_id],
compute_lang=False,
options={'post_process': False}
)[res_id]
# Update generic URLs (without parameters) to final ones
html_markupsafe = html_markupsafe.replace('/unsubscribe_from_list',
mailing._get_unsubscribe_url(email, res_id))
+4 -3
View File
@@ -73,6 +73,7 @@ class MassMailing(models.Model):
'Subject', required=True, translate=False)
preview = fields.Char(
'Preview', translate=False,
render_engine='inline_template', render_options={'post_process': True},
help='Catchy preview sentence that encourages recipients to open this email.\n'
'In most inboxes, this is displayed next to the subject.\n'
'Keep it empty if you prefer the first characters of your email content to appear instead.')
@@ -104,9 +105,9 @@ class MassMailing(models.Model):
help="Date at which the mailing was or will be sent.")
# don't translate 'body_arch', the translations are only on 'body_html'
body_arch = fields.Html(string='Body', translate=False, sanitize=False)
body_html = fields.Html(string='Body converted to be sent by mail', render_engine='qweb', sanitize=False)
# used to determine if the mail body is empty
body_html = fields.Html(
string='Body converted to be sent by mail', sanitize=False,
render_engine='qweb', render_options={'post_process': True})
is_body_empty = fields.Boolean(compute="_compute_is_body_empty")
attachment_ids = fields.Many2many(
'ir.attachment', 'mass_mailing_ir_attachments_rel',
@@ -37,10 +37,11 @@ class TestMassMailing(models.TransientModel):
# Downside: Qweb syntax is only tested when there is atleast one record of the mailing's model
if record:
# Returns a proper error if there is a syntax error with Qweb
body = mailing._render_field('body_html', record.ids, post_process=True)[record.id]
preview = mailing._render_field('preview', record.ids, post_process=True)[record.id]
# do not force lang, will simply use user context
body = mailing._render_field('body_html', record.ids, compute_lang=False)[record.id]
preview = mailing._render_field('preview', record.ids, compute_lang=False)[record.id]
full_body = mailing._prepend_preview(Markup(body), preview)
subject = mailing._render_field('subject', record.ids)[record.id]
subject = mailing._render_field('subject', record.ids, compute_lang=False)[record.id]
else:
full_body = mailing._prepend_preview(mailing.body_html, mailing.preview)
subject = mailing.subject
+14 -17
View File
@@ -60,26 +60,23 @@ class SaleOrderCancel(models.TransientModel):
@api.depends('order_id')
def _compute_subject(self):
for wizard in self:
if wizard.template_id:
wizard.subject = self.sudo()._render_template(
wizard.template_id.subject,
'sale.order',
[wizard.order_id.id],
post_process=True,
)[wizard.order_id.id]
for wizard_su in self.filtered('template_id').sudo():
wizard_su.subject = wizard_su.template_id._render_field(
'subject',
[wizard_su.order_id.id],
compute_lang=True,
options={'post_process': True},
)[wizard_su.order_id.id]
@api.depends('order_id')
def _compute_body(self):
for wizard in self:
if wizard.template_id:
wizard.body = self.sudo()._render_template(
wizard.template_id.body_html,
'sale.order',
[wizard.order_id.id],
post_process=True,
engine='qweb',
)[wizard.order_id.id]
for wizard_su in self.filtered('template_id').sudo():
wizard_su.body = wizard_su.template_id._render_field(
'body_html',
[wizard_su.order_id.id],
compute_lang=True,
options={'post_process': True},
)[wizard_su.order_id.id]
def action_send_mail_and_cancel(self):
self.ensure_one()
+1 -1
View File
@@ -210,7 +210,7 @@ class SurveyInvite(models.TransientModel):
def _send_mail(self, answer):
""" Create mail specific for recipient containing notably its access token """
subject = self._render_field('subject', answer.ids)[answer.id]
body = self._render_field('body', answer.ids, post_process=True)[answer.id]
body = self._render_field('body', answer.ids)[answer.id]
# post the message
mail_values = {
'attachment_ids': [(4, att.id) for att in self.attachment_ids],
@@ -76,7 +76,7 @@ class MailTestLang(models.Model):
group_options['has_button_access'] = True
group_options['actions'] = [
{'url': self._notify_get_action_link('controller', controller='/test_mail/do_stuff', **local_msg_vals),
'title': _('TestStuff')}
'title': _('NotificationButtonTitle')}
]
return groups
+23 -2
View File
@@ -225,7 +225,9 @@ class MailTestContainerMC(models.Model):
class MailTestComposerMixin(models.Model):
""" A simple invite-like wizard using the composer mixin, rendering on
itself. """
composer source test model. Purpose is to have a minimal composer which
runs on other records and check notably dynamic template support and
translations. """
_description = 'Invite-like Wizard'
_name = 'mail.test.composer.mixin'
_inherit = ['mail.composer.mixin']
@@ -233,6 +235,25 @@ class MailTestComposerMixin(models.Model):
name = fields.Char('Name')
author_id = fields.Many2one('res.partner')
description = fields.Html('Description', render_engine="qweb", render_options={"post_process": True}, sanitize=False)
source_ids = fields.Many2many('mail.test.composer.source', string='Invite source')
def _compute_render_model(self):
self.render_model = self._name
self.render_model = 'mail.test.composer.source'
class MailTestComposerSource(models.Model):
""" A simple model on which invites are sent. """
_description = 'Invite-like Wizard'
_name = 'mail.test.composer.source'
_inherit = ['mail.thread.blacklist']
_primary_email = 'email_from'
name = fields.Char('Name')
customer_id = fields.Many2one('res.partner', 'Main customer')
email_from = fields.Char(
'Email',
compute='_compute_email_from', readonly=False, store=True)
def _compute_email_from(self):
for source in self.filtered(lambda r: r.customer_id and not r.email_from):
source.email_from = source.customer_id.email_formatted
@@ -19,6 +19,8 @@ access_mail_test_ticket_mc_portal,mail.test.ticket.mc.portal,model_mail_test_tic
access_mail_test_ticket_mc_user,mail.test.ticket.mc.user,model_mail_test_ticket_mc,base.group_user,1,1,1,1
access_mail_test_composer_mixin_all,mail.test.composer.mixin.all,model_mail_test_composer_mixin,,0,0,0,0
access_mail_test_composer_mixin_user,mail.test.composer.mixin.user,model_mail_test_composer_mixin,base.group_user,1,1,1,1
access_mail_test_composer_source_all,mail.test.composer.source.all,model_mail_test_composer_source,,1,0,0,0
access_mail_test_composer_source_user,mail.test.composer.source.user,model_mail_test_composer_source,base.group_user,1,1,1,1
access_mail_test_container_portal,mail.test.container_portal,model_mail_test_container,base.group_portal,1,0,0,0
access_mail_test_container_user,mail.test.container.user,model_mail_test_container,base.group_user,1,1,1,1
access_mail_test_container_mc_portal,mail.test.container.mc.portal,model_mail_test_container_mc,base.group_portal,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
19 access_mail_test_ticket_mc_user mail.test.ticket.mc.user model_mail_test_ticket_mc base.group_user 1 1 1 1
20 access_mail_test_composer_mixin_all mail.test.composer.mixin.all model_mail_test_composer_mixin 0 0 0 0
21 access_mail_test_composer_mixin_user mail.test.composer.mixin.user model_mail_test_composer_mixin base.group_user 1 1 1 1
22 access_mail_test_composer_source_all mail.test.composer.source.all model_mail_test_composer_source 1 0 0 0
23 access_mail_test_composer_source_user mail.test.composer.source.user model_mail_test_composer_source base.group_user 1 1 1 1
24 access_mail_test_container_portal mail.test.container_portal model_mail_test_container base.group_portal 1 0 0 0
25 access_mail_test_container_user mail.test.container.user model_mail_test_container base.group_user 1 1 1 1
26 access_mail_test_container_mc_portal mail.test.container.mc.portal model_mail_test_container_mc base.group_portal 1 0 0 0
@@ -13,39 +13,101 @@ class TestMailComposerMixin(TestMailCommon, TestRecipients):
def setUpClass(cls):
super(TestMailComposerMixin, cls).setUpClass()
cls.mail_template = cls.env['mail.template'].create({
'subject': 'Subject for {{ object.name }}',
'body_html': '<p>Body for <t t-out="object.name"/></p>',
# ensure employee can create partners, necessary for templates
cls.user_employee.write({
'groups_id': [(4, cls.env.ref('base.group_partner_manager').id)],
})
cls.mail_template = cls.env['mail.template'].create({
'body_html': '<p>EnglishBody for <t t-out="object.name"/></p>',
'model_id': cls.env['ir.model']._get('mail.test.composer.source').id,
'name': 'Test Template for mail.test.composer.source',
'lang': '{{ object.customer_id.lang }}',
'subject': 'EnglishSubject for {{ object.name }}',
})
cls.test_record = cls.env['mail.test.composer.source'].create({
'name': cls.partner_1.name,
'customer_id': cls.partner_1.id,
})
cls._activate_multi_lang(
layout_arch_db='<body><t t-out="message.body"/> English Layout for <t t-esc="model_description"/></body>',
lang_code='es_ES',
test_record=cls.test_record,
test_template=cls.mail_template,
)
@users("employee")
def test_content_sync(self):
record = self.env['mail.test.composer.mixin'].create({
""" Test updating template updates the dynamic fields accordingly. """
source = self.test_record.with_env(self.env)
composer = self.env['mail.test.composer.mixin'].create({
'name': 'Invite',
'template_id': self.mail_template.id,
'source_ids': [(4, source.id)],
})
self.assertEqual(record.subject, self.mail_template.subject)
self.assertEqual(record.body, self.mail_template.body_html)
self.assertEqual(composer.body, self.mail_template.body_html)
self.assertEqual(composer.subject, self.mail_template.subject)
self.assertFalse(composer.lang, 'Fixme: lang is not propagated currently')
subject = record._render_field('subject', record.ids)[record.id]
self.assertEqual(subject, 'Subject for %s' % record.name)
body = record._render_field('body', record.ids)[record.id]
self.assertEqual(body, '<p>Body for %s</p>' % record.name)
subject = composer._render_field('subject', source.ids)[source.id]
self.assertEqual(subject, f'EnglishSubject for {source.name}')
body = composer._render_field('body', source.ids)[source.id]
self.assertEqual(body, f'<p>EnglishBody for {source.name}</p>')
@users("employee")
def test_rendering(self):
record = self.env['mail.test.composer.mixin'].create({
'name': 'Invite',
'subject': 'Subject for {{ object.name }}',
'body': '<p>Content from <t t-out="user.name"/></p>',
def test_rendering_custom(self):
""" Test rendering with custom strings (not coming from template) """
source = self.test_record.with_env(self.env)
composer = self.env['mail.test.composer.mixin'].create({
'description': '<p>Description for <t t-esc="object.name"/></p>',
'body': '<p>SpecificBody from <t t-out="user.name"/></p>',
'name': 'Invite',
'subject': 'SpecificSubject for {{ object.name }}',
})
self.assertEqual(record.subject, 'Subject for {{ object.name }}')
self.assertEqual(record.body, '<p>Content from <t t-out="user.name"/></p>')
self.assertEqual(composer.body, '<p>SpecificBody from <t t-out="user.name"/></p>')
self.assertEqual(composer.subject, 'SpecificSubject for {{ object.name }}')
subject = record._render_field('subject', record.ids)[record.id]
self.assertEqual(subject, 'Subject for %s' % record.name)
body = record._render_field('body', record.ids)[record.id]
self.assertEqual(body, '<p>Content from %s</p>' % self.env.user.name)
description = record._render_field('description', record.ids)[record.id]
self.assertEqual(description, '<p>Description for Invite</p>')
subject = composer._render_field('subject', source.ids)[source.id]
self.assertEqual(subject, f'SpecificSubject for {source.name}')
body = composer._render_field('body', source.ids)[source.id]
self.assertEqual(body, f'<p>SpecificBody from {self.env.user.name}</p>')
description = composer._render_field('description', source.ids)[source.id]
self.assertEqual(description, f'<p>Description for {source.name}</p>')
@users("employee")
def test_rendering_lang(self):
""" Test rendering with language involved """
template = self.mail_template.with_env(self.env)
customer = self.partner_1.with_env(self.env)
customer.lang = 'es_ES'
source = self.test_record.with_env(self.env)
composer = self.env['mail.test.composer.mixin'].create({
'description': '<p>Description for <t t-esc="object.name"/></p>',
'lang': '{{ object.customer_id.lang }}',
'name': 'Invite',
'template_id': self.mail_template.id,
'source_ids': [(4, source.id)],
})
self.assertEqual(composer.body, template.body_html)
self.assertEqual(composer.subject, template.subject)
self.assertEqual(composer.lang, '{{ object.customer_id.lang }}')
# do not specifically ask for language computation
subject = composer._render_field('subject', source.ids, compute_lang=False)[source.id]
self.assertEqual(subject, f'EnglishSubject for {source.name}')
body = composer._render_field('body', source.ids, compute_lang=False)[source.id]
self.assertEqual(body, f'<p>EnglishBody for {source.name}</p>')
description = composer._render_field('description', source.ids)[source.id]
self.assertEqual(description, f'<p>Description for {source.name}</p>')
# ask for dynamic language computation
subject = composer._render_field('subject', source.ids, compute_lang=True)[source.id]
self.assertEqual(subject, f'EnglishSubject for {source.name}',
'Fixme: translations are not done, as taking composer translations and not template one')
body = composer._render_field('body', source.ids, compute_lang=True)[source.id]
self.assertEqual(body, f'<p>EnglishBody for {source.name}</p>',
'Fixme: translations are not done, as taking composer translations and not template one'
)
description = composer._render_field('description', source.ids)[source.id]
self.assertEqual(description, f'<p>Description for {source.name}</p>')
+2 -2
View File
@@ -128,7 +128,7 @@ class TestMailTemplateLanguages(TestMailTemplateCommon):
mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='mail.test_layout')
mail = self.env['mail.mail'].sudo().browse(mail_id)
self.assertEqual(mail.body_html,
'<body><p>SpanishBody for %s</p> Spanish Layout para Spanish description</body>' % self.test_record.name)
'<body><p>SpanishBody for %s</p> Spanish Layout para Spanish Model Description</body>' % self.test_record.name)
self.assertEqual(mail.subject, 'SpanishSubject for %s' % self.test_record.name)
@mute_logger('odoo.addons.mail.models.mail_mail')
@@ -147,5 +147,5 @@ class TestMailTemplateLanguages(TestMailTemplateCommon):
mail_id = test_template.send_mail(test_record.id, email_layout_xmlid='mail.test_layout')
mail = self.env['mail.mail'].sudo().browse(mail_id)
self.assertEqual(mail.body_html,
'<body><p>SpanishBody for %s</p> Spanish Layout para Spanish description</body>' % self.test_record.name)
'<body><p>SpanishBody for %s</p> Spanish Layout para Spanish Model Description</body>' % self.test_record.name)
self.assertEqual(mail.subject, 'SpanishSubject for %s' % self.test_record.name)
+82 -24
View File
@@ -1210,7 +1210,49 @@ class TestMessagePostLang(TestMailCommon, TestRecipients):
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_composer_lang_template(self):
def test_composer_lang_template_comment(self):
test_record = self.test_records[0].with_user(self.env.user)
test_template = self.test_template.with_user(self.env.user)
with self.mock_mail_gateway():
test_record.message_post_with_template(
test_template.id,
composition_mode='comment',
email_layout_xmlid='mail.test_layout',
message_type='comment',
subtype_id=self.env.ref('mail.mt_comment').id,
)
record0_customer = self.env['res.partner'].search([('email_normalized', '=', 'test.record.1@test.customer.com')], limit=1)
self.assertTrue(record0_customer, 'Template usage should have created a contact based on record email')
customer_email = self._find_sent_mail_wemail(record0_customer.email_formatted)
self.assertTrue(customer_email)
body = customer_email['body']
# check content
self.assertIn(f'SpanishBody for {test_record.name}', body,
'Body based on template should be translated')
# check subject
self.assertEqual(f'SpanishSubject for {test_record.name}', customer_email['subject'],
'Subject based on template should be translated')
# check notification layout content
self.assertIn('Spanish Layout para', body, 'Layout content should be translated')
self.assertNotIn('English Layout for', body)
self.assertIn('Spanish Layout para Spanish Model Description', body, 'Model name should be translated')
# check notification layout strings
self.assertIn('View Lang Chatter Model', body,
'Fixme: "View document" should be translated')
# self.assertIn('SpanishView Spanish Model Description', body,
# '"View document" should be translated')
# self.assertNotIn(f'View {test_record._description}', body,
# '"View document" should be translated')
# self.assertIn('SpanishButtonTitle', body,
# 'Groups-based action names should be translated')
self.assertIn('NotificationButtonTitle', body, 'Fixme: Groups-based action names should be translated')
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
def test_composer_lang_template_mass(self):
test_records = self.test_records.with_user(self.env.user)
test_template = self.test_template.with_user(self.env.user)
@@ -1231,11 +1273,15 @@ class TestMessagePostLang(TestMailCommon, TestRecipients):
self.assertTrue(customer_email)
body = customer_email['body']
# check content
# self.assertIn('SpanishBody for %s' % record.name, body, 'Body based on template should be translated')
self.assertIn('EnglishBody for %s' % record.name, body, 'Fixme: this should be translated')
# self.assertIn(f'SpanishBody for {record.name}', body,
# 'Body based on template should be translated')
self.assertIn(f'EnglishBody for {record.name}', body,
'Fixme: Body based on template should be translated')
# check subject
# self.assertEqual('SpanishSubject for %s' % record.name, customer_email['subject'], 'Subject based on template should be translated')
self.assertEqual('EnglishSubject for %s' % record.name, customer_email['subject'], 'Fixme: this should be translated')
# self.assertEqual(f'SpanishSubject for {record.name}', customer_email['subject'],
# 'Subject based on template should be translated')
self.assertEqual(f'EnglishSubject for {record.name}', customer_email['subject'],
'Fixme: Subject based on template should be translated')
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
@@ -1255,16 +1301,20 @@ class TestMessagePostLang(TestMailCommon, TestRecipients):
customer_email = self._find_sent_mail_wemail(self.partner_2.email_formatted)
self.assertTrue(customer_email)
body = customer_email['body']
# check notification layout translation
self.assertIn('Spanish Layout para', body, 'Layout content should be translated')
self.assertNotIn('English Layout for', body)
self.assertIn('Spanish Layout para Spanish description', body, 'Model name should be translated')
self.assertIn('SpanishView Spanish description', body, '"View document" should be translated')
self.assertNotIn('View %s' % test_records[1]._description, body)
self.assertIn('TestSpanishStuff', body, 'Groups-based action names should be translated')
self.assertNotIn('TestStuff', body)
# check content
self.assertIn('Hello', body, 'Body of posted message should be present')
self.assertIn('<p>Hello</p>', body, 'Body of posted message should be present')
# check notification layout content
self.assertIn('Spanish Layout para', body,
'Layout content should be translated')
self.assertNotIn('English Layout for', body)
self.assertIn('Spanish Layout para Spanish Model Description', body,
'Model name should be translated')
# check notification layout strings
self.assertIn('SpanishView Spanish Model Description', body,
'"View document" should be translated')
self.assertNotIn(f'View {test_records[1]._description}', body)
self.assertIn('SpanishButtonTitle', body, 'Groups-based action names should be translated')
self.assertNotIn('NotificationButtonTitle', body)
@users('employee')
@mute_logger('odoo.addons.mail.models.mail_mail')
@@ -1288,15 +1338,23 @@ class TestMessagePostLang(TestMailCommon, TestRecipients):
customer_email = self._find_sent_mail_wemail(customer.email_formatted)
self.assertTrue(customer_email)
body = customer_email['body']
# check notification layout translation
self.assertIn('Spanish Layout para', body, 'Layout content should be translated')
self.assertNotIn('English Layout for', body)
self.assertIn('Spanish Layout para Spanish description', body, 'Model name should be translated')
# self.assertIn('SpanishView Spanish description', body, '"View document" should be translated')
self.assertIn('View %s' % test_records[1]._description, body, 'Fixme: this should be translated')
# self.assertIn('TestSpanishStuff', body, 'Groups-based action names should be translated')
self.assertIn('TestStuff', body, 'Fixme: groups-based action names should be translated')
# check content
self.assertIn('SpanishBody for %s' % record.name, body, 'Body based on template should be translated')
self.assertIn(f'SpanishBody for {record.name}', body,
'Body based on template should be translated')
# check subject
self.assertEqual('SpanishSubject for %s' % record.name, customer_email['subject'], 'Subject based on template should be translated')
self.assertEqual(f'SpanishSubject for {record.name}', customer_email['subject'],
'Subject based on template should be translated')
# check notification layout translation
self.assertIn('Spanish Layout para', body,
'Layout content should be translated')
self.assertNotIn('English Layout for', body)
self.assertIn('Spanish Layout para Spanish Model Description', body,
'Model name should be translated')
# self.assertIn('SpanishView Spanish Model Description', body,
# '"View document" should be translated')
self.assertIn(f'View {test_records[1]._description}', body,
'Fixme: "View document" should be translated')
# self.assertIn('NotificationButtonTitle', body,
# 'Groups-based action names should be translated')
self.assertIn('NotificationButtonTitle', body,
'Fixme: groups-based action names should be translated')
@@ -67,7 +67,7 @@ class SlideChannelInvite(models.TransientModel):
def _prepare_mail_values(self, slide_channel_partner):
""" Create mail specific for recipient """
subject = self._render_field('subject', slide_channel_partner.ids)[slide_channel_partner.id]
body = self._render_field('body', slide_channel_partner.ids, post_process=True)[slide_channel_partner.id]
body = self._render_field('body', slide_channel_partner.ids)[slide_channel_partner.id]
# post the message
mail_values = {
'email_from': self.env.user.email_formatted,