diff --git a/addons/auth_signup/models/res_users.py b/addons/auth_signup/models/res_users.py
index 50ae72c0f85..78c3411b2ea 100644
--- a/addons/auth_signup/models/res_users.py
+++ b/addons/auth_signup/models/res_users.py
@@ -187,15 +187,14 @@ class ResUsers(models.Model):
template = self.env.ref('auth_signup.reset_password_email')
assert template._name == 'mail.template'
- template_values = {
+ email_values = {
'email_to': '${object.email|safe}',
'email_cc': False,
'auto_delete': True,
- 'partner_to': False,
+ 'recipient_ids': [],
+ 'partner_ids': [],
'scheduled_date': False,
}
- if any(template[field] != value for (field, value) in template_values.items()):
- template.write(template_values)
for user in self:
if not user.email:
@@ -203,7 +202,7 @@ class ResUsers(models.Model):
# TDE FIXME: make this template technical (qweb)
with self.env.cr.savepoint():
force_send = not(self.env.context.get('import_file', False))
- template.send_mail(user.id, force_send=force_send, raise_exception=True)
+ template.send_mail(user.id, force_send=force_send, raise_exception=True, email_values=email_values)
_logger.info("Password reset email sent for user <%s> to <%s>", user.login, user.email)
def send_unregistered_user_reminder(self, after_days=5):
diff --git a/addons/digest/models/digest.py b/addons/digest/models/digest.py
index 30dc4a4fdb0..d5a282adcf4 100644
--- a/addons/digest/models/digest.py
+++ b/addons/digest/models/digest.py
@@ -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'),
diff --git a/addons/gamification/data/mail_template_data.xml b/addons/gamification/data/mail_template_data.xml
index 0c48fc7bc14..380e18fd470 100644
--- a/addons/gamification/data/mail_template_data.xml
+++ b/addons/gamification/data/mail_template_data.xml
@@ -6,7 +6,7 @@
New badge ${object.badge_id.name} granted${object.user_id.partner_id.id}
-
+
diff --git a/addons/gift_card/__manifest__.py b/addons/gift_card/__manifest__.py
index 58e24abdec1..f4eac0cb69e 100644
--- a/addons/gift_card/__manifest__.py
+++ b/addons/gift_card/__manifest__.py
@@ -9,7 +9,7 @@
'version': '1.0',
'depends': ['sale'],
'data': [
- 'data/gift_card_template_email.xml',
+ 'data/mail_template_data.xml',
'data/gift_card_data.xml',
'views/views.xml',
'views/templates.xml',
diff --git a/addons/gift_card/data/gift_card_template_email.xml b/addons/gift_card/data/mail_template_data.xml
similarity index 98%
rename from addons/gift_card/data/gift_card_template_email.xml
rename to addons/gift_card/data/mail_template_data.xml
index b68bc35fe88..adefbb72867 100644
--- a/addons/gift_card/data/gift_card_template_email.xml
+++ b/addons/gift_card/data/mail_template_data.xml
@@ -33,5 +33,6 @@
+
diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py
index 03c7e597fa2..97eaa360340 100644
--- a/addons/mail/models/__init__.py
+++ b/addons/mail/models/__init__.py
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import mail_render_mixin
+from . import mail_composer_mixin
+
from . import mail_message_subtype
from . import mail_tracking_value
from . import mail_alias
from . import mail_alias_mixin
from . import mail_followers
from . import mail_notification
-from . import mail_render_mixin
from . import mail_message
from . import mail_activity
from . import mail_mail
diff --git a/addons/mail/models/mail_composer_mixin.py b/addons/mail/models/mail_composer_mixin.py
new file mode 100644
index 00000000000..03d4fcc6a87
--- /dev/null
+++ b/addons/mail/models/mail_composer_mixin.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class MailComposerMixin(models.AbstractModel):
+ """ Mixin used to edit and render some fields used when sending emails or
+ notifications based on a mail template.
+
+ Main current purpose is to hide details related to subject and body computation
+ and rendering based on a mail.template. It also give the base tools to control
+ who is allowed to edit body, notably when dealing with templating language
+ like jinja or qweb.
+
+ It is meant to evolve in a near future with upcoming support of qweb and fine
+ grain control of rendering access.
+ """
+ _name = 'mail.composer.mixin'
+ _inherit = 'mail.render.mixin'
+ _description = 'Mail Composer Mixin'
+
+ # Content
+ subject = fields.Char('Subject', compute='_compute_subject', readonly=False, store=True)
+ body = fields.Html('Contents', sanitize_style=True, compute='_compute_body', store=True, readonly=False)
+ template_id = fields.Many2one('mail.template', 'Mail Template', domain="[('model', '=', render_model)]")
+ # Access
+ can_edit_body = fields.Boolean('Can Edit Body', compute='_compute_can_edit_body')
+
+ @api.depends('template_id')
+ def _compute_subject(self):
+ for composer_mixin in self:
+ if composer_mixin.template_id:
+ composer_mixin.subject = composer_mixin.template_id.subject
+ elif not composer_mixin.subject:
+ composer_mixin.subject = False
+
+ @api.depends('template_id')
+ def _compute_body(self):
+ for composer_mixin in self:
+ if composer_mixin.template_id:
+ composer_mixin.body = composer_mixin.template_id.body_html
+ elif not composer_mixin.body:
+ composer_mixin.body = False
+
+ @api.depends('template_id')
+ @api.depends_context('uid')
+ def _compute_can_edit_body(self):
+ self.can_edit_body = True
diff --git a/addons/mail/models/mail_render_mixin.py b/addons/mail/models/mail_render_mixin.py
index 1f4b97c5ab4..7476becde46 100644
--- a/addons/mail/models/mail_render_mixin.py
+++ b/addons/mail/models/mail_render_mixin.py
@@ -8,6 +8,7 @@ import logging
import re
import dateutil.relativedelta as relativedelta
+from lxml import html
from markupsafe import Markup
from werkzeug import urls
@@ -37,6 +38,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 +82,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 +99,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 +121,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 """
@@ -154,6 +167,14 @@ class MailRenderMixin(models.AbstractModel):
expression += "}"
return expression
+ # ------------------------------------------------------------
+ # ORM
+ # ------------------------------------------------------------
+
+ def _valid_field_parameter(self, field, name):
+ # allow specifying rendering options directly from field when using the render mixin
+ return name in ['render_engine', 'render_options'] or super()._valid_field_parameter(field, name)
+
# ------------------------------------------------------------
# TOOLS
# ------------------------------------------------------------
@@ -243,97 +264,150 @@ 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(self, template_src, model, res_ids,
+ add_context=None, options=None):
+ """ Render a raw QWeb template.
- :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)``;
+ :param str template_src: raw QWeb template to render;
+ :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}
+
+ :notice: Experimental. Use at your own risks only.
"""
- 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:
- return results
# prepare template variables
variables = self._render_qweb_eval_context()
if add_context:
variables.update(**add_context)
+ for record in self.env[model].browse(res_ids):
+ variables['object'] = record
+ try:
+ render_result = self.env['ir.qweb']._render(html.fragment_fromstring(template_src), variables)
+ except Exception as e:
+ _logger.info("Failed to render template : %s", template_src, exc_info=True)
+ raise UserError(_("Failed to render QWeb template : %s)", e))
+ results[record.id] = render_result
+
+ return results
+
+ @api.model
+ 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_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_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:
+ return results
+
+ # prepare template variables
+ 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 +426,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 +444,66 @@ 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__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'):
- raise ValueError(_('Template rendering supports only jinja or qweb.'))
+ if engine not in ('jinja', 'qweb', 'qweb_view'):
+ raise ValueError(_('Template rendering supports only raw jinja or qweb (view or raw).'))
- 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)
+ elif engine == 'qweb':
+ rendered = self._render_template_qweb(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 +511,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 +542,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,11 +561,16 @@ 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``);
:return dict: {res_id: string of rendered template based on record}
"""
+ if options is None:
+ options = {}
+
self.ensure_one()
if compute_lang:
templates_res_ids = self._classify_per_lang(res_ids)
@@ -483,11 +579,16 @@ class MailRenderMixin(models.AbstractModel):
else:
templates_res_ids = {self._context.get('lang'): (self, res_ids)}
+ # rendering 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
+
return dict(
(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()
)
diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py
index 479af463d16..9f79d717a7c 100644
--- a/addons/mail/models/mail_template.py
+++ b/addons/mail/models/mail_template.py
@@ -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():
diff --git a/addons/mail/tests/test_mail_render.py b/addons/mail/tests/test_mail_render.py
index 2f14cbca97e..d3b3e19e7ae 100644
--- a/addons/mail/tests/test_mail_render.py
+++ b/addons/mail/tests/test_mail_render.py
@@ -52,20 +52,20 @@ class TestMailRender(common.MailCommon):
"""
]
- # some qweb templates and their xml ids
- cls.base_qweb_templates = cls.env['ir.ui.view'].create([
- {'name': 'TestRender', 'type': 'qweb',
- 'arch': '
-% set line_statement_variable = 3
-We have ${line_statement_variable} cookies in stock
-We have <% set block_variable = 4 %>${block_variable} cookies in stock
-
"""
+ 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, """
-We have 3 cookies in stock
-We have 4 cookies in stock
-
""")
+ MailRenderMixin = self.env['mail.render.mixin']
+
+ custom_ctx = {'custom_ctx': 'Custom Context Value'}
+ add_context = {
+ 'custom_value': 'Custom Render Value'
+ }
+ srces = [
+ 'I am ${user.name}',
+ 'Datetime is ${format_datetime(datetime.datetime(2021, 6, 1), dt_format="MM - d - YYY")}',
+ 'Context ${ctx.get("custom_ctx")}, value ${custom_value}',
+ ]
+ results = [
+ 'I am %s' % self.env.user.name,
+ 'Datetime is 06 - 1 - 2021',
+ 'Context Custom Context Value, value Custom Render Value'
+ ]
+ 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,134 @@ 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 = """
This is a test
+
${cust_function()}
"""
+ expected = """
This is a test
+
return value
"""
+ 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 = '
This is a string
'
+ expected = '
This is a string
'
+ for engine in ['jinja', 'qweb']:
+ result = MailRenderMixin._render_template(
+ src, partner._name, partner.ids, engine=engine,
+ )[partner.id]
+ self.assertEqual(expected, result if engine == 'jinja' else result.decode()) # tde: checkme
+
+ # code xml
+ srces = [
+ '
This is a string with a number ${13+13}
',
+ '
This is a string with a number
',
+ ]
+ expected = '
This is a string with a number 26
'
+ for engine, src in zip(['jinja', 'qweb'], srces):
+ result = MailRenderMixin._render_template(
+ src, partner._name, partner.ids, engine=engine,
+ )[partner.id]
+ self.assertEqual(expected, result if engine == 'jinja' else result.decode()) # tde: checkme
+
+ # condition block xml
+ src = """Test
+% if False:
+ Code not executed
+% endif"""
+ expected = "Test\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 = """
+% set line_statement_variable = 3
+We have ${line_statement_variable} cookies in stock
+We have <% set block_variable = 4 %>${block_variable} cookies in stock
+
"""
+ expected = """
+We have 3 cookies in stock
+We have 4 cookies in stock
+
"""
+ for engine in ['jinja']:
+ result = MailRenderMixin._render_template(
+ src, partner._name, partner.ids, engine=engine,
+ )[partner.id]
+ self.assertEqual(result, expected)
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index e1d790afcab..b55645c5755 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -37,6 +37,7 @@ class MailComposer(models.TransientModel):
before being sent to each recipient.
"""
_name = 'mail.compose.message'
+ _inherit = 'mail.composer.mixin'
_description = 'Email composition wizard'
_log_access = True
_batch_size = 500
@@ -81,8 +82,8 @@ class MailComposer(models.TransientModel):
return filtered_result
# content
- subject = fields.Char('Subject')
- body = fields.Html('Contents', default='', sanitize_style=True)
+ subject = fields.Char('Subject', compute=False)
+ body = fields.Html('Contents', compute=False, default='', sanitize_style=True)
parent_id = fields.Many2one(
'mail.message', 'Parent Message', index=True, ondelete='set null',
help="Initial thread message.")
@@ -154,6 +155,12 @@ class MailComposer(models.TransientModel):
for composer in self:
composer.reply_to_force_new = composer.reply_to_mode == 'new'
+ # Overrides of mail.render.mixin
+ @api.depends('model')
+ def _compute_render_model(self):
+ for composer in self:
+ composer.render_model = composer.model
+
@api.model
def get_record_data(self, values):
""" Returns a defaults-like dict with initial values for the composition
@@ -469,10 +476,10 @@ class MailComposer(models.TransientModel):
multi_mode = False
res_ids = [res_ids]
- subjects = self.env['mail.render.mixin']._render_template(self.subject, self.model, res_ids)
- bodies = self.env['mail.render.mixin']._render_template(self.body, self.model, res_ids, post_process=True)
- emails_from = self.env['mail.render.mixin']._render_template(self.email_from, self.model, res_ids)
- replies_to = self.env['mail.render.mixin']._render_template(self.reply_to, self.model, res_ids)
+ subjects = self._render_field('subject', res_ids, options={"render_safe": True})
+ bodies = self._render_field('body', res_ids, post_process=True)
+ emails_from = self._render_field('email_from', res_ids)
+ replies_to = self._render_field('reply_to', res_ids)
default_recipients = {}
if not self.partner_ids:
records = self.env[self.model].browse(res_ids).sudo()
diff --git a/addons/mass_mailing/models/mailing.py b/addons/mass_mailing/models/mailing.py
index 568a283cf1b..4552e2d4c89 100644
--- a/addons/mass_mailing/models/mailing.py
+++ b/addons/mass_mailing/models/mailing.py
@@ -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
# ------------------------------------------------------
diff --git a/addons/mass_mailing/wizard/mailing_mailing_test.py b/addons/mass_mailing/wizard/mailing_mailing_test.py
index 595425638a0..933b81badbf 100644
--- a/addons/mass_mailing/wizard/mailing_mailing_test.py
+++ b/addons/mass_mailing/wizard/mailing_mailing_test.py
@@ -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],
diff --git a/addons/repair/data/mail_template_data.xml b/addons/repair/data/mail_template_data.xml
index 1fbdd62c191..7ba753b197b 100644
--- a/addons/repair/data/mail_template_data.xml
+++ b/addons/repair/data/mail_template_data.xml
@@ -7,7 +7,7 @@
${object.partner_id.name} Repair Orders (Ref ${object.name or 'n/a' })${(object.create_uid.email_formatted or user.email_formatted) | safe}${object.partner_id.id}
-
+
Hello ${object.partner_id.name},
diff --git a/addons/sms/models/sms_template.py b/addons/sms/models/sms_template.py
index be402f02a37..6991fecf741 100644
--- a/addons/sms/models/sms_template.py
+++ b/addons/sms/models/sms_template.py
@@ -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 {},
diff --git a/addons/survey/__manifest__.py b/addons/survey/__manifest__.py
index 12ec1e16e53..fdc1e41dfc5 100644
--- a/addons/survey/__manifest__.py
+++ b/addons/survey/__manifest__.py
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Surveys',
- 'version': '3.3',
+ 'version': '3.4',
'category': 'Marketing/Surveys',
'description': """
Create beautiful surveys and visualize answers
diff --git a/addons/survey/data/mail_template_data.xml b/addons/survey/data/mail_template_data.xml
index 1e138281ec9..3fac468272f 100644
--- a/addons/survey/data/mail_template_data.xml
+++ b/addons/survey/data/mail_template_data.xml
@@ -4,7 +4,7 @@
Survey: Invite
- Participate to ${object.survey_id.title} survey
+ Participate to ${object.survey_id.display_name} survey${(object.partner_id.email_formatted or object.email) |safe}
@@ -43,31 +43,31 @@
Certification: ${object.survey_id.display_name}${(object.survey_id.create_uid.email_formatted or user.email_formatted or user.company_id.catchall_formatted) |safe}${(object.partner_id.email_formatted or object.email) |safe}
-
-
')
diff --git a/addons/test_mail_full/tests/test_sms_template.py b/addons/test_mail_full/tests/test_sms_template.py
index abc22fcf269..e8382546a5e 100644
--- a/addons/test_mail_full/tests/test_sms_template.py
+++ b/addons/test_mail_full/tests/test_sms_template.py
@@ -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'})
diff --git a/addons/website_event_track/data/mail_template_data.xml b/addons/website_event_track/data/mail_template_data.xml
index 3772e6fe60a..4df9ca0993a 100644
--- a/addons/website_event_track/data/mail_template_data.xml
+++ b/addons/website_event_track/data/mail_template_data.xml
@@ -5,7 +5,7 @@
Confirmation of ${object.name}
-
+
Dear ${object.partner_id.name or object.partner_name or ''}
We are pleased to inform you that your proposal ${object.name} has been accepted and confirmed for the event ${object.event_id.name}.
diff --git a/addons/website_sale/data/mail_template_data.xml b/addons/website_sale/data/mail_template_data.xml
index f63177338da..2eac01a9886 100644
--- a/addons/website_sale/data/mail_template_data.xml
+++ b/addons/website_sale/data/mail_template_data.xml
@@ -7,7 +7,7 @@
You left items in your cart!${(object.user_id.email_formatted or user.email_formatted or '') | safe}${object.partner_id.id}
-
+