[IMP] mail, sms, mass_mailing: define rendering model at render level and improve mixin code

Currently ``mail.render.mixin`` offers rendering tools, some of them being
based on a ``model`` field. It allows to know which model to use to fetch
records on which we perform rendering. However this field is not defined at
mixin level but in inheriting models without being clearly implemented that
way (see ``mail.template`` or ``sms.template`` models).

In order to clean this mixin it is now defined at mixin level, using a
not stored computed field allowing to define how to find this model. Sub
models are updated accordingly.

Other cleaning is done in the render mixin
  * rename ``_render_template_qweb`` to ``_render_template_qweb_view``
    to indicate it works on views, not on raw qweb templates;
  * extract some common available variables for rendering in a method then
    called / upated for jinja and qweb views;
  * correctly set same rendering context for jinja and qweb views rendering;
  * allow to propagate an additional context from _render_field to sub
    rendering methods;
  * allow to propagate options through rendering methods (notably for escaping
    or safe attributes in jinja);
  * allow to specify engine used to render lang;
  * clean or update some docstrings;

Some tests are also added, as render mixin lacks some more detailed test to
ensure various use cases will be correctly migrated to other engines like
QWeb.

Task ID-2534550 (Template usage improvement)
Task ID-2477164 (Composer mixin)
Prepares Task ID-27033 (QWeb in templates)
COM PR odoo/odoo#70889
ENT PR odoo/enterprise#18352

Co-Authored-By: Stéphane Debauche <std@odoo.com>
Co-Authored-By: Thibault Delavallée <tde@odoo.com>
This commit is contained in:
Stéphane Debauche
2021-06-01 09:05:42 +00:00
committed by Thibault Delavallée
co-authored by Thibault Delavallée
parent c795927ce1
commit 2d6df1fe7a
8 changed files with 343 additions and 139 deletions
+1 -1
View File
@@ -121,7 +121,7 @@ class Digest(models.Model):
'digest.digest_mail_main',
'digest.digest',
self.ids,
engine='qweb',
engine='qweb_view',
add_context={
'title': self.name,
'top_button_label': _('Connect'),
+141 -96
View File
@@ -37,6 +37,29 @@ def format_time(env, time, tz=False, time_format='medium', lang_code=False):
except babel.core.UnknownLocaleError:
return time
def relativedelta_proxy(*args, **kwargs):
# dateutil.relativedelta is an old-style class and cannot be directly
# instanciated wihtin a jinja2 expression, so a lambda "proxy" is
# is needed, apparently
return relativedelta.relativedelta(*args, **kwargs)
template_env_globals = {
'str': str,
'quote': urls.url_quote,
'urlencode': urls.url_encode,
'datetime': safe_eval.datetime,
'len': len,
'abs': abs,
'min': min,
'max': max,
'sum': sum,
'filter': filter,
'reduce': functools.reduce,
'map': map,
'relativedelta': relativedelta_proxy,
'round': round,
}
try:
# We use a jinja2 sandboxed environment to render mako templates.
# Note that the rendering does not cover all the mako syntax, in particular
@@ -58,26 +81,7 @@ try:
trim_blocks=True, # do not output newline after blocks
autoescape=True, # XML/HTML automatic escaping
)
jinja_template_env.globals.update({
'str': str,
'quote': urls.url_quote,
'urlencode': urls.url_encode,
'datetime': safe_eval.datetime,
'len': len,
'abs': abs,
'min': min,
'max': max,
'sum': sum,
'filter': filter,
'reduce': functools.reduce,
'map': map,
'round': round,
# dateutil.relativedelta is an old-style class and cannot be directly
# instanciated wihtin a jinja2 expression, so a lambda "proxy" is
# is needed, apparently.
'relativedelta': lambda *a, **kw : relativedelta.relativedelta(*a, **kw),
})
jinja_template_env.globals.update(template_env_globals)
jinja_safe_template_env = copy.copy(jinja_template_env)
jinja_safe_template_env.autoescape = False
except ImportError:
@@ -94,6 +98,8 @@ class MailRenderMixin(models.AbstractModel):
help="Optional translation language (ISO code) to select when sending out an email. "
"If not set, the english version will be used. This should usually be a placeholder expression "
"that provides the appropriate language, e.g. ${object.partner_id.lang}.")
# rendering context
render_model = fields.Char("Rendering Model", compute='_compute_render_model', store=False)
# expression builder
model_object_field = fields.Many2one(
'ir.model.fields', string="Field", store=False,
@@ -114,6 +120,12 @@ class MailRenderMixin(models.AbstractModel):
'Placeholder Expression', store=False,
help="Final placeholder expression, to be copy-pasted in the desired template field.")
def _compute_render_model(self):
""" Give the target model for rendering. Void by default as models
inheriting from ``mail.render.mixin`` should define how to find this
model. """
self.render_model = False
@api.onchange('model_object_field', 'sub_model_object_field', 'null_value')
def _onchange_dynamic_placeholder(self):
""" Generate the dynamic placeholder """
@@ -243,38 +255,56 @@ class MailRenderMixin(models.AbstractModel):
# ------------------------------------------------------------
@api.model
def _render_qweb_eval_context(self):
""" Prepare qweb evaluation context, containing for all rendering
def _get_common_eval_context(self):
""" Evaluation context used in all rendering engines. Contains
* ``user``: current user browse record;
* ``ctx```: current context;
* various formatting tools;
"""
render_context = {
return {
'format_date': lambda date, date_format=False, lang_code=False: format_date(self.env, date, date_format, lang_code),
'format_datetime': lambda dt, tz=False, dt_format=False, lang_code=False: format_datetime(self.env, dt, tz, dt_format, lang_code),
'format_time': lambda time, tz=False, time_format=False, lang_code=False: format_time(self.env, time, tz, time_format, lang_code),
'format_amount': lambda amount, currency, lang_code=False: tools.format_amount(self.env, amount, currency, lang_code),
'format_duration': lambda value: tools.format_duration(value),
'user': self.env.user,
'ctx': self._context,
}
@api.model
def _render_qweb_eval_context(self):
""" Prepare qweb evaluation context, containing common context with
some specific values for Qweb. """
render_context = self._get_common_eval_context()
render_context.update(copy.copy(template_env_globals))
return render_context
@api.model
def _render_template_qweb(self, template_src, model, res_ids, add_context=None):
""" Render a QWeb template.
def _render_template_qweb_view(self, template_src, model, res_ids,
add_context=None, options=None):
""" Render a QWeb template based on an ir.ui.view content.
In addition to the generic evaluation context available, some other
variables are added:
* ``object``: record based on which the template is rendered;
:param str template_src: source QWeb template. It should be a string
XmlID allowing to fetch an ir.ui.view;
:param str model: see ``MailRenderMixin._render_field)``;
:param list res_ids: see ``MailRenderMixin._render_field)``;
XmlID allowing to fetch an ``ir.ui.view``;
:param str model: see ``MailRenderMixin._render_template()``;
:param list res_ids: see ``MailRenderMixin._render_template()``;
:param dict add_context: additional context to give to renderer. It
allows to add values to base rendering context generated by
``MailRenderMixin._render_qweb_eval_context()``;
allows to add or update values to base rendering context generated
by ``MailRenderMixin._render_qweb_eval_context()``;
:param dict options: options for rendering (not used 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.'))
view = self.env.ref(template_src, raise_if_not_found=False) or self.env['ir.ui.view']
results = dict.fromkeys(res_ids, u"")
if not view:
@@ -284,56 +314,55 @@ class MailRenderMixin(models.AbstractModel):
variables = self._render_qweb_eval_context()
if add_context:
variables.update(**add_context)
safe_eval.check_values(variables)
for record in self.env[model].browse(res_ids):
variables['object'] = record
try:
render_result = view._render(variables, engine='ir.qweb', minimal_qcontext=True)
except Exception as e:
_logger.info("Failed to render template : %s (%d)" % (template_src, view.id), exc_info=True)
raise UserError(_("Failed to render template : %s (%d)", template_src, view.id))
_logger.info("Failed to render template : %s (%d)", template_src, view.id, exc_info=True)
raise UserError(_("Failed to render template : %(xml_id)s (%(view_id)d)",
xml_id=template_src,
view_id=view.id))
results[record.id] = render_result
return results
@api.model
def _render_jinja_eval_context(self):
""" Prepare jinja evaluation context, containing for all rendering
* ``user``: current user browse record;
* ``ctx```: current context, named ctx to avoid clash with jinja
internals that already uses context;
* various formatting tools;
"""
render_context = {
'format_date': lambda date, date_format=False, lang_code=False: format_date(self.env, date, date_format, lang_code),
'format_datetime': lambda dt, tz=False, dt_format=False, lang_code=False: format_datetime(self.env, dt, tz, dt_format, lang_code),
'format_time': lambda time, tz=False, time_format=False, lang_code=False: format_time(self.env, time, tz, time_format, lang_code),
'format_amount': lambda amount, currency, lang_code=False: tools.format_amount(self.env, amount, currency, lang_code),
'format_duration': lambda value: tools.format_duration(value),
'user': self.env.user,
'ctx': self._context,
}
return render_context
return self._get_common_eval_context()
@api.model
def _render_template_jinja(self, template_txt, model, res_ids, add_context=None):
def _render_template_jinja(self, template_txt, model, res_ids,
add_context=None, options=None):
""" Render a string-based template on records given by a model and a list
of IDs, using jinja.
In addition to the generic evaluation context given by _render_jinja_eval_context
some new variables are added, depending on each record
In addition to the generic evaluation context available, some other
variables are added:
* ``object``: record based on which the template is rendered;
:param str template_txt: template text to render
:param str model: model name of records on which we want to perform rendering
:param list res_ids: list of ids of records (all belonging to same model)
:param str model: see ``MailRenderMixin._render_template()``;
:param list res_ids: see ``MailRenderMixin._render_template()``;
: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_jinja_eval_context()``;
:param dict options: options for rendering;
:return dict: {res_id: string of rendered template based on record}
"""
# TDE FIXME: remove that brol (6dde919bb9850912f618b561cd2141bffe41340c)
no_autoescape = self._context.get('safe')
# 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.'))
# TDE note: support 'safe' context key as backward compatibility for 6dde919bb9850912f618b561cd2141bffe41340c
if options is None:
options = {}
no_autoescape = options.get('render_safe') or self._context.get('safe')
results = dict.fromkeys(res_ids, u"")
if not template_txt:
return results
@@ -352,17 +381,13 @@ class MailRenderMixin(models.AbstractModel):
variables.update(**add_context)
safe_eval.check_values(variables)
# TDE CHECKME
# records = self.env[model].browse(it for it in res_ids if it) # filter to avoid browsing [None]
if any(r is None for r in res_ids):
raise ValueError(_('Unsuspected None'))
for record in self.env[model].browse(res_ids):
variables['object'] = record
try:
render_result = template.render(variables)
except Exception as e:
_logger.info("Failed to render template : %s" % e, exc_info=True)
_logger.info("Failed to render template : %s", e, exc_info=True)
raise UserError(_("Failed to render template : %s", e))
if render_result == u"False":
render_result = u""
@@ -374,50 +399,63 @@ class MailRenderMixin(models.AbstractModel):
def _render_template_postprocess(self, rendered):
""" Tool method for post processing. In this method we ensure local
links ('/shop/Basil-1') are replaced by global links ('https://www.
mygardin.com/hop/Basil-1').
mygarden.com/shop/Basil-1').
:param rendered: result of ``_render_template``
:param rendered: result of ``_render_template``;
:return dict: updated version of rendered
:return dict: updated version of rendered per record ID;
"""
for res_id, html in rendered.items():
rendered[res_id] = self._replace_local_links(html)
for res_id, rendered_html in rendered.items():
rendered[res_id] = self._replace_local_links(rendered_html)
return rendered
@api.model
def _render_template(self, template_src, model, res_ids, engine='jinja', add_context=None, post_process=False):
def _render_template(self, template_src, model, res_ids, engine='jinja',
add_context=None, options=None, post_process=False):
""" Render the given string on records designed by model / res_ids using
the given rendering engine. Currently only jinja or qweb are supported.
:param str template_src: template text to render (jinja) or xml id of view (qweb)
this could be cleaned but hey, we are in a rush
:param str model: model name of records on which we want to perform rendering
:param list res_ids: list of ids of records (all belonging to same model)
:param string engine: jinja
:param post_process: see ``MailRenderMixin._render_field``;
:param str template_src: template text to render (jinja) or xml id of
view (qweb);
:param str model: model name of records on which we want to perform
rendering (aka 'crm.lead');
:param list res_ids: list of ids of records. All should belong to the
Odoo model given by model;
:param string engine: jinja or qweb_view;
: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``;
:return dict: {res_id: string of rendered template based on record}
"""
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 ('jinja', 'qweb'):
if engine not in ('jinja', 'qweb_view'):
raise ValueError(_('Template rendering supports only jinja or qweb.'))
if engine == 'qweb':
rendered = self._render_template_qweb(template_src, model, res_ids, add_context=add_context)
if engine == 'qweb_view':
rendered = self._render_template_qweb_view(template_src, model, res_ids,
add_context=add_context, options=options)
else:
rendered = self._render_template_jinja(template_src, model, res_ids, add_context=add_context)
rendered = self._render_template_jinja(template_src, model, res_ids,
add_context=add_context, options=options)
if post_process:
rendered = self._render_template_postprocess(rendered)
return rendered
def _render_lang(self, res_ids):
def _render_lang(self, res_ids, engine='jinja'):
""" Given some record ids, return the lang for each record based on
lang field of template or through specific context-based key.
lang field of template or through specific context-based key. Lang is
computed by performing a rendering on res_ids, based on self.render_model.
:param list res_ids: list of ids of records (all belonging to same model
defined by self.model)
:param list res_ids: list of ids of records. All should belong to the
Odoo model given by model;
:param string engine: jinja or qweb_view;
:return dict: {res_id: lang code (i.e. en_US)}
"""
@@ -425,27 +463,30 @@ class MailRenderMixin(models.AbstractModel):
if not isinstance(res_ids, (list, tuple)):
raise ValueError(_('Template rendering for language should be called with a list of IDs.'))
if self.env.context.get('template_preview_lang'):
return dict((res_id, self.env.context['template_preview_lang']) for res_id in res_ids)
else:
rendered_langs = self._render_template(self.lang, self.model, res_ids)
return dict((res_id, lang)
for res_id, lang in rendered_langs.items())
rendered_langs = self._render_template(self.lang, self.render_model, res_ids, engine=engine)
return dict(
(res_id, lang)
for res_id, lang in rendered_langs.items()
)
def _classify_per_lang(self, res_ids):
def _classify_per_lang(self, res_ids, engine='jinja'):
""" Given some record ids, return for computed each lang a contextualized
template and its subset of res_ids.
:param list res_ids: list of ids of records (all belonging to same model
defined by self.model)
defined by self.render_model)
:param string engine: jinja or qweb_view;
:return dict: {lang: (template with lang=lang_code if specific lang computed
or template, res_ids targeted by that language}
"""
self.ensure_one()
if self.env.context.get('template_preview_lang'):
return dict((res_id, self.env.context['template_preview_lang']) for res_id in res_ids)
lang_to_res_ids = {}
for res_id, lang in self._render_lang(res_ids).items():
for res_id, lang in self._render_lang(res_ids, engine=engine).items():
lang_to_res_ids.setdefault(lang, []).append(res_id)
return dict(
@@ -453,16 +494,18 @@ class MailRenderMixin(models.AbstractModel):
for lang, lang_res_ids in lang_to_res_ids.items()
)
def _render_field(self, field, res_ids,
def _render_field(self, field, res_ids, engine='jinja',
compute_lang=False, set_lang=False,
post_process=False):
add_context=None, options=None, post_process=False):
""" 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
on self.
:param field: a field name existing on self;
:param list res_ids: list of ids of records (all belonging to same model
defined by ``self.model``)
defined by ``self.render_model``)
:param string engine: jinja or qweb_view;
:param boolean compute_lang: compute language to render on translated
version of the template instead of default (probably english) one.
@@ -470,6 +513,8 @@ class MailRenderMixin(models.AbstractModel):
:param string set_lang: force language for rendering. It should be a
valid lang code matching an activate res.lang. Checked only if
``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``);
@@ -487,7 +532,7 @@ class MailRenderMixin(models.AbstractModel):
(res_id, rendered)
for lang, (template, tpl_res_ids) in templates_res_ids.items()
for res_id, rendered in template._render_template(
template[field], template.model, tpl_res_ids,
post_process=post_process
template[field], template.render_model, tpl_res_ids, engine=engine,
add_context=add_context, options=options, post_process=post_process
).items()
)
+11 -1
View File
@@ -67,6 +67,16 @@ class MailTemplate(models.Model):
help="Sidebar action to make this template available on records "
"of the related document model")
# Overrides of mail.render.mixin
@api.depends('model')
def _compute_render_model(self):
for template in self:
template.render_model = template.model
# ------------------------------------------------------------
# CRUD
# ------------------------------------------------------------
def unlink(self):
self.unlink_action()
return super(MailTemplate, self).unlink()
@@ -163,9 +173,9 @@ class MailTemplate(models.Model):
results = dict()
for lang, (template, template_res_ids) in self._classify_per_lang(res_ids).items():
for field in fields:
template = template.with_context(safe=(field == 'subject'))
generated_field_values = template._render_field(
field, template_res_ids,
options={'render_safe': field == 'subject'},
post_process=(field == 'body_html')
)
for res_id, field_value in generated_field_values.items():
+157 -30
View File
@@ -52,20 +52,20 @@ class TestMailRender(common.MailCommon):
</p>"""
]
# some qweb templates and their xml ids
cls.base_qweb_templates = cls.env['ir.ui.view'].create([
{'name': 'TestRender', 'type': 'qweb',
'arch': '<p>Hello</p>',
},
{'name': 'TestRender2', 'type': 'qweb',
'arch': '<p>Hello <t t-esc="object.name"/></p>',
},
{'name': 'TestRender3', 'type': 'qweb',
'arch': """<p>
# some qweb templates, their views and their xml ids
cls.base_qweb_bits = [
'<p>Hello</p>',
'<p>Hello <t t-esc="object.name"/></p>',
"""<p>
<span t-if="object.lang == 'en_US'">English Speaker</span>
<span t-else="">Other Speaker</span>
</p>""",
},
</p>"""
]
cls.base_qweb_templates = cls.env['ir.ui.view'].create([
{'name': 'TestRender%d' % index,
'type': 'qweb',
'arch': qweb_content,
} for index, qweb_content in enumerate(cls.base_qweb_bits)
])
cls.base_qweb_templates_data = cls.env['ir.model.data'].create([
{'name': template.name, 'module': 'mail',
@@ -127,23 +127,32 @@ class TestMailRender(common.MailCommon):
})
@users('employee')
def test_render_jinja(self):
source = """<p>
% set line_statement_variable = 3
<span>We have ${line_statement_variable} cookies in stock</span>
<span>We have <% set block_variable = 4 %>${block_variable} cookies in stock</span>
</p>"""
def test_evaluation_context(self):
""" Test evaluation context and various ways of tweaking it. """
partner = self.env['res.partner'].browse(self.render_object.ids)
result = self.env['mail.render.mixin']._render_template(
source,
partner._name,
partner.ids,
engine='jinja',
)[partner.id]
self.assertEqual(result, """<p>
<span>We have 3 cookies in stock</span>
<span>We have 4 cookies in stock</span>
</p>""")
MailRenderMixin = self.env['mail.render.mixin']
custom_ctx = {'custom_ctx': 'Custom Context Value'}
add_context = {
'custom_value': 'Custom Render Value'
}
srces = [
'<b>I am ${user.name}</b>',
'<span>Datetime is ${format_datetime(datetime.datetime(2021, 6, 1), dt_format="MM - d - YYY")}</span>',
'<span>Context ${ctx.get("custom_ctx")}, value ${custom_value}</span>',
]
results = [
'<b>I am %s</b>' % self.env.user.name,
'<span>Datetime is 06 - 1 - 2021</span>',
'<span>Context Custom Context Value, value Custom Render Value</span>'
]
for src, expected in zip(srces, results):
for engine in ['jinja']:
result = MailRenderMixin.with_context(**custom_ctx)._render_template(
src, partner._name, partner.ids,
engine=engine, add_context=add_context
)[partner.id]
self.assertEqual(expected, result)
@users('employee')
def test_render_mail_template_jinja(self):
@@ -179,13 +188,131 @@ class TestMailRender(common.MailCommon):
self.assertEqual(rendered, expected)
@users('employee')
def test_render_template_qweb(self):
def test_render_template_qweb_view(self):
partner = self.env['res.partner'].browse(self.render_object.ids)
for source, expected in zip(self.base_qweb_templates_xmlids, self.base_rendered):
rendered = self.env['mail.render.mixin']._render_template(
source,
partner._name,
partner.ids,
engine='qweb',
engine='qweb_view',
)[partner.id].decode()
self.assertEqual(rendered, expected)
@users('employee')
def test_template_rendering_impersonate(self):
""" Test that the use of SUDO do not change the current user. """
partner = self.env['res.partner'].browse(self.render_object.ids)
src = '${user.name} - ${object.name}'
expected = '%s - %s' % (self.env.user.name, partner.name)
result = self.env['mail.render.mixin'].sudo()._render_template_jinja(
src, partner._name, partner.ids
)[partner.id]
self.assertIn(expected, result)
@users('employee')
def test_template_rendering_function_call(self):
"""Test the case when the template call a custom function.
This function should not be called when the template is not rendered.
"""
partner = self.env['res.partner'].browse(self.render_object.ids)
def cust_function():
# Can not use "MagicMock" in a Jinja sand-boxed environment
# so create our own function
cust_function.call = True
return 'return value'
cust_function.call = False
src = """<h1>This is a test</h1>
<p>${cust_function()}</p>"""
expected = """<h1>This is a test</h1>
<p>return value</p>"""
context = {'cust_function': cust_function}
result = self.env['mail.render.mixin']._render_template_jinja(
src, partner._name, partner.ids,
add_context=context
)[partner.id]
self.assertEqual(expected, result)
self.assertTrue(cust_function.call)
@users('employee')
def test_template_rendering_various(self):
""" Test static rendering """
partner = self.env['res.partner'].browse(self.render_object.ids)
MailRenderMixin = self.env['mail.render.mixin']
# static string
src = 'This is a string'
expected = 'This is a string'
for engine in ['jinja']:
result = MailRenderMixin._render_template(
src, partner._name, partner.ids, engine=engine,
)[partner.id]
self.assertEqual(expected, result)
# code string
src = 'This is a string with a number ${13+13}'
expected = 'This is a string with a number 26'
for engine in ['jinja']:
result = MailRenderMixin._render_template(
src, partner._name, partner.ids, engine=engine,
)[partner.id]
self.assertEqual(expected, result)
# block string
src = "This is a string with a block ${'hidden' if False else 'displayed'}"
expected = 'This is a string with a block displayed'
for engine in ['jinja']:
result = MailRenderMixin._render_template(
src, partner._name, partner.ids, engine=engine,
)[partner.id]
self.assertEqual(expected, result)
# static xml
src = '<p class="text-muted"><span>This is a string</span></p>'
expected = '<p class="text-muted"><span>This is a string</span></p>'
for engine in ['jinja']:
result = MailRenderMixin._render_template(
src, partner._name, partner.ids, engine=engine,
)[partner.id]
self.assertEqual(expected, result)
# code xml
src = '<p class="text-muted"><span>This is a string with a number ${13+13}</span></p>'
expected = '<p class="text-muted"><span>This is a string with a number 26</span></p>'
for engine in ['jinja']:
result = MailRenderMixin._render_template(
src, partner._name, partner.ids, engine=engine,
)[partner.id]
self.assertEqual(expected, result)
# condition block xml
src = """<b>Test</b>
% if False:
<b>Code not executed</b>
% endif"""
expected = "<b>Test</b>\n" # TDE: to check
for engine in ['jinja']:
result = MailRenderMixin._render_template(
src, partner._name, partner.ids, engine=engine,
)[partner.id]
self.assertEqual(expected, result)
# complete block xml
src = """<p>
% set line_statement_variable = 3
<span>We have ${line_statement_variable} cookies in stock</span>
<span>We have <% set block_variable = 4 %>${block_variable} cookies in stock</span>
</p>"""
expected = """<p>
<span>We have 3 cookies in stock</span>
<span>We have 4 cookies in stock</span>
</p>"""
for engine in ['jinja']:
result = MailRenderMixin._render_template(
src, partner._name, partner.ids, engine=engine,
)[partner.id]
self.assertEqual(result, expected)
+10 -4
View File
@@ -113,7 +113,7 @@ class MassMailing(models.Model):
string='Reply To', compute='_compute_reply_to', readonly=False, store=True,
help='Preferred Reply-To Address')
# recipients
mailing_model_real = fields.Char(string='Recipients Real Model', compute='_compute_model')
mailing_model_real = fields.Char(string='Recipients Real Model', compute='_compute_mailing_model_real')
mailing_model_id = fields.Many2one(
'ir.model', string='Recipients Model', ondelete='cascade', required=True,
domain=[('is_mailing_enabled', '=', True)],
@@ -239,9 +239,9 @@ class MassMailing(models.Model):
mailing.medium_id = self.env.ref('utm.utm_medium_email').id
@api.depends('mailing_model_id')
def _compute_model(self):
for record in self:
record.mailing_model_real = (record.mailing_model_name != 'mailing.list') and record.mailing_model_name or 'mailing.contact'
def _compute_mailing_model_real(self):
for mailing in self:
mailing.mailing_model_real = (mailing.mailing_model_name != 'mailing.list') and mailing.mailing_model_name or 'mailing.contact'
@api.depends('mailing_model_real')
def _compute_reply_to_mode(self):
@@ -293,6 +293,12 @@ class MassMailing(models.Model):
def _compute_mail_server_available(self):
self.mail_server_available = self.env['ir.config_parameter'].sudo().get_param('mass_mailing.outgoing_mail_server')
# Overrides of mail.render.mixin
@api.depends('mailing_model_real')
def _compute_render_model(self):
for mailing in self:
mailing.render_model = mailing.mailing_model_real
# ------------------------------------------------------
# ORM
# ------------------------------------------------------
@@ -32,19 +32,22 @@ class TestMassMailing(models.TransientModel):
mass_mail_layout = self.env.ref('mass_mailing.mass_mailing_mail_layout')
record = self.env[mailing.mailing_model_real].search([], limit=1)
body = mailing._prepend_preview(mailing.body_html, mailing.preview)
subject = mailing.subject
# If there is atleast 1 record for the model used in this mailing, then we use this one to render the template
# Downside: Jinja 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 jinja
body = self.env['mail.render.mixin']._render_template(body, mailing.mailing_model_real, record.ids, post_process=True)[record.id]
subject = self.env['mail.render.mixin']._render_template(subject, mailing.mailing_model_real, record.ids)[record.id]
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]
full_body = mailing._prepend_preview(body, preview)
subject = mailing._render_field('subject', record.ids)[record.id]
else:
full_body = mailing._prepend_preview(mailing.body_html, mailing.preview)
subject = mailing.subject
# Convert links in absolute URLs before the application of the shortener
body = self.env['mail.render.mixin']._replace_local_links(body)
body = tools.html_sanitize(body, sanitize_attributes=True, sanitize_style=True)
full_body = self.env['mail.render.mixin']._replace_local_links(full_body)
full_body = tools.html_sanitize(full_body, sanitize_attributes=True, sanitize_style=True)
for valid_email in valid_emails:
mail_values = {
@@ -52,7 +55,7 @@ class TestMassMailing(models.TransientModel):
'reply_to': mailing.reply_to,
'email_to': valid_email,
'subject': subject,
'body_html': mass_mail_layout._render({'body': body}, engine='ir.qweb', minimal_qcontext=True),
'body_html': mass_mail_layout._render({'body': full_body}, engine='ir.qweb', minimal_qcontext=True),
'notification': True,
'mailing_id': mailing.id,
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
+10
View File
@@ -29,6 +29,16 @@ class SMSTemplate(models.Model):
help="Sidebar action to make this template available on records "
"of the related document model")
# Overrides of mail.render.mixin
@api.depends('model')
def _compute_render_model(self):
for template in self:
template.render_model = template.model
# ------------------------------------------------------------
# CRUD
# ------------------------------------------------------------
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
default = dict(default or {},
@@ -23,6 +23,9 @@ class TestSmsTemplate(TestMailFullCommon, TestMailFullRecipients):
rendered_body = self.sms_template._render_template(self.sms_template.body, self.sms_template.model, self.test_record.ids)
self.assertEqual(rendered_body[self.test_record.id], 'Dear %s this is an SMS.' % self.test_record.display_name)
rendered_body = self.sms_template._render_field('body', self.test_record.ids)
self.assertEqual(rendered_body[self.test_record.id], 'Dear %s this is an SMS.' % self.test_record.display_name)
def test_sms_template_lang(self):
self.env['res.lang']._activate_lang('fr_FR')
self.user_admin.write({'lang': 'en_US'})