[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:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-3
@@ -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'):
|
||||
@@ -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({
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user