[MOV] mail: move rendering code to mail render mixin

We also have to update code calling directly the rendering itself. Indeed
some code bits does some rendering directly on jinja-enabled input and not
through templates. Those calls have to be updated accordingly.

LINKS

Task ID 1963529
Community PR odoo/odoo#32397
This commit is contained in:
Thibault Delavallée
2020-03-24 10:24:19 +00:00
parent a03f6483d6
commit 822f468059
11 changed files with 201 additions and 196 deletions
+2 -2
View File
@@ -136,8 +136,8 @@ class Digest(models.Model):
return False
tip.user_ids += user
body = tools.html_sanitize(tip.tip_description)
# FIXME: sanitize ? links ,
tip_description = self.env['mail.template']._render_template(body, 'digest.tip', self.ids)[self.id]
# FIXME: sanitize ? links ?
tip_description = self.env['mail.render.mixin']._render_template(body, 'digest.tip', self.ids)[self.id]
return tip_description
def compute_kpis_actions(self, company, user):
+185 -1
View File
@@ -1,7 +1,79 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
import babel
import copy
import datetime
import dateutil.relativedelta as relativedelta
import functools
import logging
from werkzeug import urls
from odoo import _, api, fields, models, tools
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
def format_date(env, date, pattern=False, lang_code=False):
try:
return tools.format_date(env, date, date_format=pattern, lang_code=lang_code)
except babel.core.UnknownLocaleError:
return date
def format_datetime(env, dt, tz=False, dt_format='medium', lang_code=False):
try:
return tools.format_datetime(env, dt, tz=tz, dt_format=dt_format, lang_code=lang_code)
except babel.core.UnknownLocaleError:
return dt
try:
# We use a jinja2 sandboxed environment to render mako templates.
# Note that the rendering does not cover all the mako syntax, in particular
# arbitrary Python statements are not accepted, and not all expressions are
# allowed: only "public" attributes (not starting with '_') of objects may
# be accessed.
# This is done on purpose: it prevents incidental or malicious execution of
# Python code that may break the security of the server.
from jinja2.sandbox import SandboxedEnvironment
jinja_template_env = SandboxedEnvironment(
block_start_string="<%",
block_end_string="%>",
variable_start_string="${",
variable_end_string="}",
comment_start_string="<%doc>",
comment_end_string="</%doc>",
line_statement_prefix="%",
line_comment_prefix="##",
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': 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_safe_template_env = copy.copy(jinja_template_env)
jinja_safe_template_env.autoescape = False
except ImportError:
_logger.warning("jinja2 not available, templating features will not work!")
class MailRenderMixin(models.AbstractModel):
@@ -66,3 +138,115 @@ class MailRenderMixin(models.AbstractModel):
expression += " or '''%s'''" % null_value
expression += "}"
return expression
# ------------------------------------------------------------
# RENDERING
# ------------------------------------------------------------
@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_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
@api.model
def _render_template_jinja(self, template_txt, model, res_ids):
""" 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
* ``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)
:return dict: {res_id: string of rendered template based on record}
"""
# TDE FIXME: remove that brol (6dde919bb9850912f618b561cd2141bffe41340c)
no_autoescape = self._context.get('safe')
results = dict.fromkeys(res_ids, u"")
if not template_txt:
return results
# try to load the template
try:
jinja_env = jinja_safe_template_env if no_autoescape else jinja_template_env
template = jinja_env.from_string(tools.ustr(template_txt))
except Exception:
_logger.info("Failed to load template %r", template_txt, exc_info=True)
return results
# prepare template variables
variables = self._render_jinja_eval_context()
# 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)
raise UserError(_("Failed to render template : %s") % e)
if render_result == u"False":
render_result = u""
results[record.id] = render_result
return results
@api.model
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').
:param rendered: result of ``_render_template``
:return dict: updated version of rendered
"""
for res_id, html in rendered.items():
rendered[res_id] = self.env['mail.thread']._replace_local_links(html)
return rendered
@api.model
def _render_template(self, template_txt, model, res_ids, engine='jinja', post_process=False):
""" Render the given string on records designed by model / res_ids using
the given rendering engine. Currently only jinja is supported.
: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 string engine: jinja
:param post_process: perform rendered str / html post processing (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 != 'jinja':
raise ValueError(_('Template rendering supports only jinja.'))
rendered = self._render_template_jinja(template_txt, model, res_ids)
if post_process:
rendered = self._render_template_postprocess(rendered)
return rendered
-174
View File
@@ -1,15 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import babel
import base64
import copy
import datetime
import dateutil.relativedelta as relativedelta
import functools
import logging
from werkzeug import urls
from odoo import _, api, fields, models, tools
from odoo.exceptions import UserError
@@ -17,66 +11,6 @@ from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
def format_date(env, date, pattern=False, lang_code=False):
try:
return tools.format_date(env, date, date_format=pattern, lang_code=lang_code)
except babel.core.UnknownLocaleError:
return date
def format_datetime(env, dt, tz=False, dt_format='medium', lang_code=False):
try:
return tools.format_datetime(env, dt, tz=tz, dt_format=dt_format, lang_code=lang_code)
except babel.core.UnknownLocaleError:
return dt
try:
# We use a jinja2 sandboxed environment to render mako templates.
# Note that the rendering does not cover all the mako syntax, in particular
# arbitrary Python statements are not accepted, and not all expressions are
# allowed: only "public" attributes (not starting with '_') of objects may
# be accessed.
# This is done on purpose: it prevents incidental or malicious execution of
# Python code that may break the security of the server.
from jinja2.sandbox import SandboxedEnvironment
jinja_template_env = SandboxedEnvironment(
block_start_string="<%",
block_end_string="%>",
variable_start_string="${",
variable_end_string="}",
comment_start_string="<%doc>",
comment_end_string="</%doc>",
line_statement_prefix="%",
line_comment_prefix="##",
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': 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_safe_template_env = copy.copy(jinja_template_env)
jinja_safe_template_env.autoescape = False
except ImportError:
_logger.warning("jinja2 not available, templating features will not work!")
class MailTemplate(models.Model):
"Templates for sending email"
_name = "mail.template"
@@ -180,114 +114,6 @@ class MailTemplate(models.Model):
# RENDERING
# ------------------------------------------------------------
@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_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
@api.model
def _render_template_jinja(self, template_txt, model, res_ids):
""" 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
* ``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)
:return dict: {res_id: string of rendered template based on record}
"""
# TDE FIXME: remove that brol (6dde919bb9850912f618b561cd2141bffe41340c)
no_autoescape = self._context.get('safe')
results = dict.fromkeys(res_ids, u"")
if not template_txt:
return results
# try to load the template
try:
jinja_env = jinja_safe_template_env if no_autoescape else jinja_template_env
template = jinja_env.from_string(tools.ustr(template_txt))
except Exception:
_logger.info("Failed to load template %r", template_txt, exc_info=True)
return results
# prepare template variables
variables = self._render_jinja_eval_context()
# 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)
raise UserError(_("Failed to render template : %s") % e)
if render_result == u"False":
render_result = u""
results[record.id] = render_result
return results
@api.model
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').
:param rendered: result of ``_render_template``
:return dict: updated version of rendered
"""
for res_id, html in rendered.items():
rendered[res_id] = self.env['mail.thread']._replace_local_links(html)
return rendered
@api.model
def _render_template(self, template_txt, model, res_ids, engine='jinja', post_process=False):
""" Render the given string on records designed by model / res_ids using
the given rendering engine. Currently only jinja is supported.
: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 string engine: jinja
:param post_process: perform rendered str / html post processing (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 != 'jinja':
raise ValueError(_('Template rendering supports only jinja.'))
rendered = self._render_template_jinja(template_txt, model, res_ids)
if post_process:
rendered = self._render_template_postprocess(rendered)
return rendered
def _render_lang(self, res_ids):
""" Given some record ids, return the lang for each record based on
lang field of template or through specific context-based key.
+4 -4
View File
@@ -455,10 +455,10 @@ class MailComposer(models.TransientModel):
multi_mode = False
res_ids = [res_ids]
subjects = self.env['mail.template']._render_template(self.subject, self.model, res_ids)
bodies = self.env['mail.template']._render_template(self.body, self.model, res_ids, post_process=True)
emails_from = self.env['mail.template']._render_template(self.email_from, self.model, res_ids)
replies_to = self.env['mail.template']._render_template(self.reply_to, self.model, 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)
default_recipients = {}
if not self.partner_ids:
records = self.env[self.model].browse(res_ids).sudo()
+1 -1
View File
@@ -7,7 +7,7 @@ from . import mailing_list
from . import mailing_trace
from . import mailing
from . import mail_mail
from . import mail_template
from . import mail_render_mixin
from . import mail_thread
from . import res_config_settings
from . import res_users
@@ -4,13 +4,13 @@
from odoo import api, models
class MailTemplate(models.Model):
_inherit = "mail.template"
class MailRenderMixin(models.AbstractModel):
_inherit = "mail.render.mixin"
@api.model
def _render_template_postprocess(self, rendered):
# super will transform relative url to absolute
rendered = super(MailTemplate, self)._render_template_postprocess(rendered)
rendered = super(MailRenderMixin, self)._render_template_postprocess(rendered)
# apply shortener after
if self.env.context.get('post_convert_links'):
+1 -1
View File
@@ -3,7 +3,7 @@
# Updating mako environement in order to be able to use slug
try:
from odoo.addons.mail.models.mail_template import jinja_template_env, jinja_safe_template_env
from odoo.addons.mail.models.mail_render_mixin import jinja_template_env, jinja_safe_template_env
from odoo.addons.http_routing.models.ir_http import slug
jinja_template_env.globals.update({
-5
View File
@@ -107,8 +107,3 @@ class SMSTemplate(models.Model):
template = self.with_context(lang=lang)
all_bodies.update(template._render_template(template.body, self.model, rids))
return all_bodies
@api.model
def _render_template(self, template_txt, model, res_ids):
""" Render the jinja template """
return self.env['mail.template']._render_template(template_txt, model, res_ids)
+1 -1
View File
@@ -281,7 +281,7 @@ class SendSMS(models.TransientModel):
if self.template_id and self.body == self.template_id.body:
all_bodies = self.template_id._get_translated_bodies(records.ids)
else:
all_bodies = self.env['mail.template']._render_template(self.body, records._name, records.ids)
all_bodies = self.env['mail.render.mixin']._render_template(self.body, records._name, records.ids)
return all_bodies
def _prepare_mass_sms_values(self, records):
+2 -2
View File
@@ -199,8 +199,8 @@ class SurveyInvite(models.TransientModel):
def _send_mail(self, answer):
""" Create mail specific for recipient containing notably its access token """
subject = self.env['mail.template']._render_template(self.subject, 'survey.user_input', answer.ids, post_process=True)[answer.id]
body = self.env['mail.template']._render_template(self.body, 'survey.user_input', answer.ids, post_process=True)[answer.id]
subject = self.env['mail.render.mixin']._render_template(self.subject, 'survey.user_input', answer.ids, post_process=True)[answer.id]
body = self.env['mail.render.mixin']._render_template(self.body, 'survey.user_input', answer.ids, post_process=True)[answer.id]
# post the message
mail_values = {
'email_from': self.email_from,
@@ -95,8 +95,8 @@ class SlideChannelInvite(models.TransientModel):
def _prepare_mail_values(self, slide_channel_partner):
""" Create mail specific for recipient """
subject = self.env['mail.template']._render_template(self.subject, 'slide.channel.partner', slide_channel_partner.ids, post_process=True)[slide_channel_partner.id]
body = self.env['mail.template']._render_template(self.body, 'slide.channel.partner', slide_channel_partner.ids, post_process=True)[slide_channel_partner.id]
subject = self.env['mail.render.mixin']._render_template(self.subject, 'slide.channel.partner', slide_channel_partner.ids, post_process=True)[slide_channel_partner.id]
body = self.env['mail.render.mixin']._render_template(self.body, 'slide.channel.partner', slide_channel_partner.ids, post_process=True)[slide_channel_partner.id]
# post the message
mail_values = {
'email_from': self.email_from,