End -
-
+
()
diff --git a/addons/website_event_exhibitor/views/event_exhibitor_templates_page.xml b/addons/website_event_exhibitor/views/event_exhibitor_templates_page.xml
index 9365bd9de0b..ee237d93b16 100644
--- a/addons/website_event_exhibitor/views/event_exhibitor_templates_page.xml
+++ b/addons/website_event_exhibitor/views/event_exhibitor_templates_page.xml
@@ -50,8 +50,8 @@
starts on
- ().
+ ().
Attendees will be able to join to meet .
diff --git a/addons/website_event_meet/views/event_meet_templates_list.xml b/addons/website_event_meet/views/event_meet_templates_list.xml
index 39e4750a18e..18908fe9130 100644
--- a/addons/website_event_meet/views/event_meet_templates_list.xml
+++ b/addons/website_event_meet/views/event_meet_templates_list.xml
@@ -74,7 +74,7 @@
This room is not open right now!
Join us here on the
-
+
()
to have a chat with us!
diff --git a/addons/website_hr_recruitment/models/hr_recruitment.py b/addons/website_hr_recruitment/models/hr_recruitment.py
index cd44a05586f..3eec7c3d0f2 100644
--- a/addons/website_hr_recruitment/models/hr_recruitment.py
+++ b/addons/website_hr_recruitment/models/hr_recruitment.py
@@ -4,6 +4,7 @@
from werkzeug import urls
from odoo import api, fields, models, _
+from odoo.tools import mute_logger
from odoo.tools.translate import html_translate
@@ -49,9 +50,9 @@ class Job(models.Model):
_name = 'hr.job'
_inherit = ['hr.job', 'website.seo.metadata', 'website.published.multi.mixin']
+ @mute_logger('odoo.addons.base.models.ir_qweb')
def _get_default_website_description(self):
- default_description = self.env.ref("website_hr_recruitment.default_website_description", raise_if_not_found=False)
- return (default_description._render() if default_description else "")
+ return self.env['ir.qweb']._render("website_hr_recruitment.default_website_description", raise_if_not_found=False)
website_published = fields.Boolean(help='Set if the application is published on the website of the company.')
website_description = fields.Html('Website description', translate=html_translate, sanitize_attributes=False, default=_get_default_website_description, prefetch=False, sanitize_form=False)
diff --git a/addons/website_payment/models/payment_transaction.py b/addons/website_payment/models/payment_transaction.py
index cfc8a628cbe..d8a50876b14 100644
--- a/addons/website_payment/models/payment_transaction.py
+++ b/addons/website_payment/models/payment_transaction.py
@@ -27,11 +27,11 @@ class PaymentTransaction(models.Model):
self.ensure_one()
if is_internal_notification or self.state == 'done':
subject = _('A donation has been made on your website') if is_internal_notification else _('Donation confirmation')
- body = self.env.ref('website_payment.donation_mail_body')._render({
+ body = self.env['ir.qweb']._render('website_payment.donation_mail_body', {
'is_internal_notification': is_internal_notification,
'tx': self,
'comment': comment,
- }, engine='ir.qweb', minimal_qcontext=True)
+ }, minimal_qcontext=True)
self.env.ref('website_payment.mail_template_donation').send_mail(
self.id,
email_layout_xmlid="mail.mail_notification_light",
diff --git a/addons/website_sale_stock_wishlist/models/product_wishlist.py b/addons/website_sale_stock_wishlist/models/product_wishlist.py
index 3630aa1c81f..9b516db6e8f 100644
--- a/addons/website_sale_stock_wishlist/models/product_wishlist.py
+++ b/addons/website_sale_stock_wishlist/models/product_wishlist.py
@@ -30,11 +30,10 @@ class ProductWishlist(models.Model):
notified = self.env['product.wishlist']
# cannot group by product_id because it depend of website_id -> warehouse_id
- tmpl = self.env.ref("website_sale_stock_wishlist.availability_email_body")
for wishlist in to_notify:
product = wishlist.with_context(website_id=wishlist.website_id.id).product_id
if not product._is_sold_out():
- body_html = tmpl._render({"wishlist": wishlist})
+ body_html = self.env['ir.qweb']._render('website_sale_stock_wishlist.availability_email_body', {"wishlist": wishlist})
msg = self.env["mail.message"].sudo().new(dict(body=body_html, record_name=product.name))
full_mail = self.env["mail.render.mixin"]._render_encapsulate(
"mail.mail_notification_light",
diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py
index efe110aa81c..89eaa62da78 100644
--- a/addons/website_slides/controllers/main.py
+++ b/addons/website_slides/controllers/main.py
@@ -988,7 +988,7 @@ class WebsiteSlides(WebsiteProfile):
]).write({'completed': False})
slide_question = request.env['slide.question'].create(new_question_values)
- return request.env.ref('website_slides.lesson_content_quiz_question')._render({
+ return request.env['ir.qweb']._render('website_slides.lesson_content_quiz_question', {
'slide': slide,
'question': slide_question,
})
diff --git a/addons/website_slides/wizard/slide_channel_invite.py b/addons/website_slides/wizard/slide_channel_invite.py
index 2aee778a3ce..7360f621628 100644
--- a/addons/website_slides/wizard/slide_channel_invite.py
+++ b/addons/website_slides/wizard/slide_channel_invite.py
@@ -84,20 +84,18 @@ class SlideChannelInvite(models.TransientModel):
# optional support of default_email_layout_xmlid in context
email_layout_xmlid = self.env.context.get('default_email_layout_xmlid', self.env.context.get('notif_layout'))
if email_layout_xmlid:
- try:
- template = self.env.ref(email_layout_xmlid, raise_if_not_found=True)
- except ValueError:
- _logger.warning('QWeb template %s not found when sending slide channel mails. Sending without layout.', email_layout_xmlid)
- else:
- # could be great to use ``_notify_by_email_prepare_rendering_context`` someday
- template_ctx = {
- 'message': self.env['mail.message'].sudo().new(dict(body=mail_values['body_html'], record_name=self.channel_id.name)),
- 'model_description': self.env['ir.model']._get('slide.channel').display_name,
- 'record': slide_channel_partner,
- 'company': self.env.company,
- 'signature': self.channel_id.user_id.signature,
- }
- body = template._render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
+ # could be great to use ``_notify_by_email_prepare_rendering_context`` someday
+ template_ctx = {
+ 'message': self.env['mail.message'].sudo().new({'body': mail_values['body_html'], 'record_name': self.channel_id.name}),
+ 'model_description': self.env['ir.model']._get('slide.channel').display_name,
+ 'record': slide_channel_partner,
+ 'company': self.env.company,
+ 'signature': self.channel_id.user_id.signature,
+ }
+ body = self.env['ir.qweb']._render(email_layout_xmlid, template_ctx, engine='ir.qweb', minimal_qcontext=True, raise_if_not_found=False)
+ if body:
mail_values['body_html'] = self.env['mail.render.mixin']._replace_local_links(body)
+ else:
+ _logger.warning('QWeb template %s not found when sending slide channel mails. Sending without layout.', email_layout_xmlid)
return mail_values
diff --git a/odoo/addons/base/models/ir_actions_report.py b/odoo/addons/base/models/ir_actions_report.py
index 54b8ce11103..817aed48c19 100644
--- a/odoo/addons/base/models/ir_actions_report.py
+++ b/odoo/addons/base/models/ir_actions_report.py
@@ -362,10 +362,9 @@ class IrActionsReport(models.Model):
IrConfig = self.env['ir.config_parameter'].sudo()
# Return empty dictionary if 'web.minimal_layout' not found.
- layout = self.env.ref('web.minimal_layout', False)
+ layout = self.env.ref('web.minimal_layout', raise_if_not_found=False)
if not layout:
return {}
- layout = self.env['ir.ui.view'].browse(self.env['ir.ui.view'].get_view_id('web.minimal_layout'))
base_url = IrConfig.get_param('report.url') or layout.get_base_url()
root = lxml.html.fromstring(html)
@@ -391,15 +390,15 @@ class IrActionsReport(models.Model):
# Retrieve bodies
for node in root.xpath(match_klass.format('article')):
- layout_with_lang = layout
# set context language to body language
+ IrQweb = self.env['ir.qweb']
if node.get('data-oe-lang'):
- layout_with_lang = layout_with_lang.with_context(lang=node.get('data-oe-lang'))
- body = layout_with_lang._render({
- 'subst': False,
- 'body': Markup(lxml.html.tostring(node, encoding='unicode')),
- 'base_url': base_url
- })
+ IrQweb = IrQweb.with_context(lang=node.get('data-oe-lang'))
+ body = IrQweb._render(layout.id, {
+ 'subst': False,
+ 'body': Markup(lxml.html.tostring(node, encoding='unicode')),
+ 'base_url': base_url
+ }, raise_if_not_found=False)
bodies.append(body)
if node.get('data-oe-model') == self.model:
res_ids.append(int(node.get('data-oe-id', 0)))
@@ -417,12 +416,12 @@ class IrActionsReport(models.Model):
if attribute[0].startswith('data-report-'):
specific_paperformat_args[attribute[0]] = attribute[1]
- header = layout._render({
+ header = self.env['ir.qweb']._render(layout.id, {
'subst': True,
'body': Markup(lxml.html.tostring(header_node, encoding='unicode')),
'base_url': base_url
})
- footer = layout._render({
+ footer = self.env['ir.qweb']._render(layout.id, {
'subst': True,
'body': Markup(lxml.html.tostring(footer_node, encoding='unicode')),
'base_url': base_url
@@ -608,23 +607,14 @@ class IrActionsReport(models.Model):
if values is None:
values = {}
- context = dict(self.env.context, inherit_branding=False)
-
# Browse the user instead of using the sudo self.env.user
user = self.env['res.users'].browse(self.env.uid)
- website = None
- if request and hasattr(request, 'website'):
- if request.website is not None:
- website = request.website
- context = dict(context, translatable=context.get('lang') != request.env['ir.http']._get_default_lang().code)
-
- view_obj = self.env['ir.ui.view'].sudo().with_context(context)
+ view_obj = self.env['ir.ui.view'].with_context(inherit_branding=False)
values.update(
time=time,
context_timestamp=lambda t: fields.Datetime.context_timestamp(self.with_context(tz=user.tz), t),
user=user,
res_company=user.company_id,
- website=website,
web_base_url=self.env['ir.config_parameter'].sudo().get_param('web.base.url', default=''),
)
return view_obj._render_template(template, values).encode()
diff --git a/odoo/addons/base/models/ir_qweb.py b/odoo/addons/base/models/ir_qweb.py
index bb9d574224d..9e222ed96d9 100644
--- a/odoo/addons/base/models/ir_qweb.py
+++ b/odoo/addons/base/models/ir_qweb.py
@@ -127,8 +127,8 @@ Activate the choosed debugger.
When dev mode is enabled this allows python developers to have access to the
state of variables being rendered. The code generated by the QWeb engine is
-not accessible, only the variables (values, compile_options, self) can be
-analyzed or the methods that called the QWeb rendering.
+not accessible, only the variables (values, self) can be analyzed or the
+methods that called the QWeb rendering.
``t-if``
~~~~~~~~
@@ -374,20 +374,26 @@ import token
import tokenize
import io
import textwrap
+import werkzeug
+import math
from markupsafe import Markup, escape
from collections.abc import Sized, Mapping
from itertools import count, chain
from lxml import etree
from psycopg2.extensions import TransactionRollbackError
+from dateutil.relativedelta import relativedelta
from odoo import api, models, tools
-from odoo.tools import pycompat, frozendict, SUPPORTED_DEBUGGER
-from odoo.tools.safe_eval import check_values, assert_valid_codeobj, _BUILTINS, to_opcodes, _EXPR_OPCODES, _BLACKLIST
+from odoo.tools import config, safe_eval, pycompat, SUPPORTED_DEBUGGER
+from odoo.tools.safe_eval import assert_valid_codeobj, _BUILTINS, to_opcodes, _EXPR_OPCODES, _BLACKLIST
+from odoo.tools.json import scriptsafe
from odoo.tools.misc import get_lang
+from odoo.tools.image import image_data_uri
from odoo.http import request
from odoo.modules.module import get_resource_path
from odoo.tools.profiler import QwebTracker
+from odoo.exceptions import UserError
from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.base.models.ir_asset import can_aggregate, STYLE_EXTENSIONS, SCRIPT_EXTENSIONS
@@ -447,25 +453,30 @@ def indent_code(code, level):
class QWebException(Exception):
""" Management of errors that raised when rendering a QWeb template.
"""
- def __init__(self, message, qweb, options, template=None, path=None, code=None):
+ def __init__(self, message, qweb, template=None, path=None, code=None):
self.name = template
- self.code = code if options and options.get('dev_mode') else None
+ self.code = code if qweb.env.context.get('dev_mode') else None
self.path = path
self.html = None
if template is not None and path and ':' not in path:
- element = qweb._get_template(template, options)[0]
- nodes = element.getroottree().xpath(self.path)
- if nodes:
- node = etree.Element(nodes[0].tag, nodes[0].attrib)
- self.html = etree.tostring(node, encoding='unicode')
+ try:
+ element = qweb._get_template(template)[0]
+ nodes = element.getroottree().xpath(self.path)
+ if nodes:
+ node = etree.Element(nodes[0].tag, nodes[0].attrib)
+ self.html = etree.tostring(node, encoding='unicode')
+ except Exception as e:
+ _logger.warning('Cannot load template %s: %s', template, e)
self.stack = traceback.format_exc()
self.title = message
super().__init__(message)
def __str__(self):
parts = [self.title]
- if self.__cause__ is not None:
+ if str(self.__cause__) != '':
parts.append(f"{self.__cause__.__class__.__name__}: {self.__cause__}")
+ elif str(self.__context__) != '':
+ parts.append(f"{self.__context__.__class__.__name__}: {self.__context__}")
if self.name is not None:
parts.append(f"Template: {self.name}")
if self.path is not None:
@@ -508,68 +519,99 @@ class IrQWeb(models.AbstractModel):
:param template: etree, xml_id, template name (see _get_template)
* Call the method ``load`` is not an etree.
:param dict values: template values to be used for rendering
- :param options: used to compile the template (the dict available for the rendering is frozen)
+ :param options: used to compile the template
+ Options will be add into the IrQweb.env.context for the rendering.
* ``load`` (function) overrides the load method
+ * ``lang`` (str) used language to render the template
+ * ``inherit_branding`` (bool) add the tag node branding
+ * ``inherit_branding_auto`` (bool) add the branding on fields
+ * ``minimal_qcontext``(bool) To use the minimum context and options
+ from ``_prepare_environment``
:returns: bytes marked as markup-safe (decode to :class:`markupsafe.Markup`
instead of `str`)
:rtype: MarkupSafe
"""
- compile_options = dict(self.env.context, dev_mode='qweb' in tools.config['dev_mode'])
- compile_options.update(options)
-
- if values and T_CALL_SLOT in values:
+ values = values.copy() if values else {}
+ if T_CALL_SLOT in values:
raise ValueError('values[0] should be unset when call the _render method and only set into the template.')
- render_template = self._compile(template, compile_options)
- rendering = render_template(self, values or {})
+ irQweb = self.with_context(**options)._prepare_environment(values)
+
+ safe_eval.check_values(values)
+
+ render_template = irQweb._compile(template)
+ rendering = render_template(irQweb, values)
result = ''.join(rendering)
return Markup(result)
# assume cache will be invalidated by third party on write to ir.ui.view
def _get_template_cache_keys(self):
- """ Return the list of context keys to use for caching ``_get_template``. """
- return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations', 'website_id', 'profile']
+ """ Return the list of context keys to use for caching ``_compile``. """
+ return ['lang', 'inherit_branding', 'edit_translations', 'profile']
# apply ormcache_context decorator unless in dev mode...
@tools.conditional(
'xml' not in tools.config['dev_mode'],
- tools.ormcache('template', 'tuple(options.get(k) for k in self._get_template_cache_keys())'),
+ tools.ormcache('template', 'tuple(self.env.context.get(k) for k in self._get_template_cache_keys())'),
)
@QwebTracker.wrap_compile
- def _compile(self, template, options):
+ def _compile(self, template):
""" Compile the given template into a rendering function (generator)::
render(qweb, values)
+ This method can be called only by the IrQweb `_render` method or by
+ the compiled code of t-call from an other template.
- where ``qweb`` is a QWeb instance and ``values`` are the values to render.
+ An `options` dictionary is created and attached to the function. It
+ contains rendering options that are part of the cache key in
+ addition to template references.
+
+ where ``qweb`` is a QWeb instance and ``values`` are the values to
+ render.
"""
- if options is None:
- options = {}
+ # The `context`` dictionary includes the elements used for the cache
+ # key to which are added the template references as well as technical
+ # information useful for generating the function. This dictionary is
+ # only used when compiling the template.
+ context = self.env.context.copy()
- element, document, ref = self._get_template(template, options)
- element.attrib.pop('t-name', None)
+ try:
+ element, document, ref = self._get_template(template)
+ except (ValueError, UserError) as e:
+ message = str(e)
+ ClassError = e.__class__
+ def not_found_template(self, values):
+ if self.env.context.get('raise_if_not_found', True):
+ raise ClassError(message)
+ _logger.warning('Cannot load template %s: %s', template, message)
+ return ''
+ return not_found_template
+ context.pop('raise_if_not_found', None)
# reference to get xml and etree (usually the template ID)
- options['ref'] = ref
+ context['ref'] = ref
+ # reference name or key to get xml and etree (usually the template XML ID)
+ context['ref_name'] = element.attrib.pop('t-name', template if isinstance(template, str) and '<' not in template else None)
# str xml of the reference template used for compilation. Useful for debugging, dev mode and profiling.
- options['ref_xml'] = document
-
- _options = dict(options)
- options = frozendict(options)
-
- # Initial template value send to render method (not in the froozen dict because it may be
- # different from one render to another. Indeed, it may be the view ID or the key)
- _options['template'] = template
+ context['ref_xml'] = document
+ # Identifier used to call `_compile`
+ context['template'] = template
# Root of the etree which will be processed during compilation.
- _options['root'] = element.getroottree()
- # Reference to the last node being compiled. It is mainly used for debugging and displaying
- # error messages.
- _options['last_path_node'] = None
+ context['root'] = element.getroottree()
+ # Reference to the last node being compiled. It is mainly used for debugging and displaying error messages.
+ context['last_path_node'] = None
- if not options.get('nsmap'):
- _options['nsmap'] = {}
+ if not context.get('nsmap'):
+ context['nsmap'] = {}
+
+ # The options dictionary includes cache key elements and template
+ # references. It will be attached to the generated function. This
+ # dictionary is only there for logs, performance or test information.
+ # The values of these `options` cannot be changed and must always be
+ # identical in `context` and `self.env.context`.
+ options = {k: context.get(k) for k in self._get_template_cache_keys() + ['ref', 'ref_name', 'ref_xml']}
# generate code
@@ -579,24 +621,24 @@ class IrQWeb(models.AbstractModel):
if element.text:
element.text = re.compile(r'^(\n[ \t]*)+(\n[ \t])').sub(r'\2', element.text)
- _options['_text_concat'] = []
- self._append_text("", _options) # To ensure the template function is a generator and doesn't become a regular function
+ context['_text_concat'] = []
+ self._append_text("", context) # To ensure the template function is a generator and doesn't become a regular function
code_lines = (
- [f'def {def_name}(self, compile_options, values, log):']
- + self._compile_node(element, _options, 1)
- + self._flush_text(_options, 1, rstrip=True)
+ [f'def {def_name}(self, values, log):']
+ + self._compile_node(element, context, 1)
+ + self._flush_text(context, 1, rstrip=True)
)
except QWebException:
raise
except Exception as e:
raise QWebException("Error when compiling xml template",
- self, options, template, path=_options.get('last_path_node')) from e
+ self, template, path=context.get('last_path_node')) from e
try:
code = '\n'.join(code_lines)
except Exception as e:
code = '\n'.join(map(str, code_lines))
raise QWebException("Error when compiling xml template",
- self, options, template, code=code) from e
+ self, template, code=code) from e
# compile code and defined default values
@@ -611,35 +653,38 @@ class IrQWeb(models.AbstractModel):
raise
except Exception as e:
raise QWebException("Error when compiling xml template",
- self, options, template, code=code) from e
+ self, template, code=code) from e
# return the wrapped function
def render_template(self, values):
try:
+ if not values.get('xmlid'):
+ values['xmlid'] = context['ref_name']
+ values['viewid'] = context['ref']
log = {'last_path_node': None}
- values = self._prepare_values(values, options)
- yield from compiled_fn(self, options, values, log)
+ yield from compiled_fn(self, values, log)
except (QWebException, TransactionRollbackError) as e:
raise
except Exception as e:
raise QWebException("Error while render the template",
- self, options, template, path=log.get('last_path_node'), code=code) from e
+ self, template, path=log.get('last_path_node'), code=code) from e
+
+ # add key cache options for profiling tools
+ render_template.options = options
return render_template
# read and load input template
- def _get_template(self, template, options):
+ def _get_template(self, template):
""" Retrieve the given template, and return it as a tuple ``(etree,
xml, ref)``, where ``element`` is an etree, ``document`` is the
string document that contains ``element``, and ``ref`` if the uniq
reference of the template (id, t-name or template).
:param template: template identifier or etree
- :param options: used to compile the template (the dict available for
- the rendering is frozen)
- ``load`` (function) overrides the load method
+ :param self.env.context['load']: (function(template)) overrides the load method
"""
assert template not in (False, None, ""), "template is required"
@@ -659,7 +704,7 @@ class IrQWeb(models.AbstractModel):
except ValueError:
ref_alias = template # e.g. web.layout
- doc_or_elem, ref = options.get('load', self._load)(ref_alias, options) or (None, None)
+ doc_or_elem, ref = self.env.context.get('load', self._load)(ref_alias) or (None, None)
if doc_or_elem is None:
raise ValueError(f"Can not load template: {ref_alias!r}")
if isinstance(doc_or_elem, etree._Element):
@@ -687,53 +732,71 @@ class IrQWeb(models.AbstractModel):
# use the document itself as ref when no t-name was found
return (element, document, document)
- def _load(self, ref, options):
+ def _load(self, ref):
"""
Load the template referenced by ``ref``.
:returns: The loaded template (as string or etree) and its
identifier
- :rtype: Optional[Tuple[Union[etree, str], Optional[str, int]]]
+ :rtype: Tuple[Union[etree, str], Optional[str, int]]
"""
- lang = options.get('lang') or get_lang(self.env).code
- view_id = self.env['ir.ui.view'].get_view_id(ref)
- template = self.env['ir.ui.view'].with_context(lang=lang).sudo()._read_template(view_id)
+ IrUIView = self.env['ir.ui.view']
+ view_id = IrUIView.get_view_id(ref)
+ view = IrUIView.sudo().browse(view_id)
+ template = IrUIView.sudo()._read_template(view_id)
+ etree_view = etree.fromstring(template)
# QWeb's ``_read_template`` will check if one of the first children of
# what we send to it has a "t-name" attribute having ``ref`` as value
# to consider it has found it. As it'll never be the case when working
# with view ids or children view or children primary views, force it here.
- def is_child_view(view_ref):
- view_id = self.env['ir.ui.view'].get_view_id(view_ref)
- view = self.env['ir.ui.view'].sudo().browse(view_id)
- return view.inherit_id is not None
-
- if isinstance(ref, int) or is_child_view(ref):
- view = etree.fromstring(template)
- for node in view:
- if node.get('t-name'):
- node.set('t-name', str(ref))
+ if view.inherit_id is not None:
+ for node in etree_view:
+ if node.get('t-name') == str(ref) or node.get('t-name') == str(view.key):
node.attrib.pop('name', None)
node.attrib.pop('id', None)
- return (view, view_id)
- else:
- return (template, view_id)
+ etree_view = node
+ break
+ etree_view.set('t-name', str(view.key or ref))
+ return (etree_view, view_id)
# values for running time
- def _prepare_values(self, values, options):
- """ Prepare the context that will sent to the compiled and evaluated
- function.
+ def _prepare_environment(self, values):
+ """ Prepare the values and options that will sent to the
+ compiled and evaluated function.
+ The options will be add into the rendering IrQweb.env.context.
:param values: template values to be used for rendering
- :param options: frozen dict of compilation parameters.
+
+ :returns self
"""
- check_values(values)
- values['true'] = True
- values['false'] = False
- if 'request' not in values:
- values['request'] = request
- return values
+ values.update(
+ true=True,
+ false=False,
+ )
+ if not self.env.context.get('minimal_qcontext'):
+ values.setdefault('debug', request and request.session.debug or '')
+ values.setdefault('user_id', self.env["res.users"].browse(self.env.user.id))
+ values.setdefault('res_company', self.env.company.sudo())
+
+ values.update(
+ request=request, # might be unbound if we're not in an httprequest context
+ test_mode_enabled=bool(config['test_enable'] or config['test_file']),
+ json=scriptsafe,
+ quote_plus=werkzeug.urls.url_quote_plus,
+ time=safe_eval.time,
+ datetime=safe_eval.datetime,
+ relativedelta=relativedelta,
+ image_data_uri=image_data_uri,
+ # specific 'math' functions to ease rounding in templates and lessen controller marshmalling
+ floor=math.floor,
+ ceil=math.ceil,
+ env=self.env,
+ lang=self.env.context.get('lang'),
+ )
+
+ return self.with_context(dev_mode='qweb' in tools.config['dev_mode'])
def _prepare_globals(self):
""" Prepare the global context that will sent to eval the qweb
@@ -1119,7 +1182,7 @@ class IrQWeb(models.AbstractModel):
""" Compile a purely static element into a list of string. """
if not el.nsmap:
unqualified_el_tag = el_tag = el.tag
- attrib = self._post_processing_att(el.tag, el.attrib, options)
+ attrib = self._post_processing_att(el.tag, el.attrib)
else:
# Etree will remove the ns prefixes indirection by inlining the corresponding
# nsmap definition into the tag attribute. Restore the tag and prefix here.
@@ -1149,7 +1212,7 @@ class IrQWeb(models.AbstractModel):
else:
attrib[key] = value
- attrib = self._post_processing_att(el.tag, attrib, options)
+ attrib = self._post_processing_att(el.tag, attrib)
# Update the dict of inherited namespaces before continuing the recursion. Note:
# since `options['nsmap']` is a dict (and therefore mutable) and we do **not**
@@ -1246,7 +1309,7 @@ class IrQWeb(models.AbstractModel):
debugger = el.attrib.pop('t-debug')
code = []
if options.get('dev_mode'):
- code.append(indent_code(f"self._debug_trace({debugger!r}, compile_options)", level))
+ code.append(indent_code(f"self._debug_trace({debugger!r})", level))
else:
_logger.warning("@t-debug in template is only available in qweb dev mode options")
return code
@@ -1390,7 +1453,7 @@ class IrQWeb(models.AbstractModel):
attrs = values.pop('__qweb_attrs__', None)
if attrs:
tagName = {el.tag!r}
- attrs = self._post_processing_att(tagName, attrs, compile_options)
+ attrs = self._post_processing_att(tagName, attrs)
for name, value in attrs.items():
if value or isinstance(value, str):
yield f' {{escape(str(name))}}="{{escape(str(value))}}"'
@@ -1454,9 +1517,9 @@ class IrQWeb(models.AbstractModel):
self._compile_directive(el, options, 'inner-content', level + 1) +
self._flush_text(options, level + 1))
if content:
- code.append(indent_code(f"def {def_name}(self, compile_options, values, log):", level))
+ code.append(indent_code(f"def {def_name}(self, values, log):", level))
code.extend(content)
- expr = f"Markup(''.join({def_name}(self, compile_options, values, log)))"
+ expr = f"Markup(''.join({def_name}(self, values, log)))"
else:
expr = "''"
code.append(indent_code(f"values[{varname!r}] = {expr}", level))
@@ -1777,7 +1840,7 @@ class IrQWeb(models.AbstractModel):
elif ttype == 't-field':
record, field_name = expr.rsplit('.', 1)
code.append(indent_code(f"""
- field_attrs, content, force_display = self._get_field({self._compile_expr(record, raise_on_missing=True)}, {field_name!r}, {expr!r}, {el.tag!r}, values.pop('__qweb_options__', {{}}), compile_options, values)
+ field_attrs, content, force_display = self._get_field({self._compile_expr(record, raise_on_missing=True)}, {field_name!r}, {expr!r}, {el.tag!r}, values.pop('__qweb_options__', {{}}), values)
if values.get('__qweb_attrs__') is None:
values['__qweb_attrs__'] = field_attrs
else:
@@ -1795,7 +1858,7 @@ class IrQWeb(models.AbstractModel):
if code_options == 'True':
code.append(indent_code(f"""
- widget_attrs, content, force_display = self._get_widget(content, {expr!r}, {el.tag!r}, values.pop('__qweb_options__', {{}}), compile_options, values)
+ widget_attrs, content, force_display = self._get_widget(content, {expr!r}, {el.tag!r}, values.pop('__qweb_options__', {{}}), values)
if values.get('__qweb_attrs__') is None:
values['__qweb_attrs__'] = widget_attrs
else:
@@ -1921,9 +1984,9 @@ class IrQWeb(models.AbstractModel):
code = self._flush_text(options, level, rstrip=el.tag.lower() == 't')
# options
+ el.attrib.pop('t-consumed-options', None)
code.append(indent_code(f"""
- t_options = values.pop('__qweb_options__', {{}})
- t_call_options = compile_options.copy()
+ t_call_options = dict(values.pop('__qweb_options__', {{}}))
t_call_options.update({{'caller_template': {str(options.get('template'))!r}, 'last_path_node': {str(options['root'].getpath(el))!r} }})
""", level))
if nsmap:
@@ -1939,28 +2002,22 @@ class IrQWeb(models.AbstractModel):
# values (t-out="0" from content and variables from t-set and t-set-*)
def_name = self._make_name("t_call_values")
- code.append(indent_code(f"def {def_name}(self, compile_options, values, log):", level))
+ code.append(indent_code(f"def {def_name}(self, values, log):", level))
code.extend(self._compile_directive(el, options, 'inner-content', level + 1))
code.extend(self._compile_directive(el, options, 'set', level + 1))
self._append_text('', options) # To ensure the template function is a generator and doesn't become a regular function
code.extend(self._flush_text(options, level + 1, rstrip=True))
code.append(indent_code("t_call_values = values.copy()", level))
- code.append(indent_code(f"t_call_values['0'] = Markup(''.join({def_name}(self, compile_options, t_call_values, log)))", level))
+ code.append(indent_code(f"t_call_values['0'] = Markup(''.join({def_name}(self, t_call_values, log)))", level))
template = self._compile_format(expr)
# call
- if el.attrib.pop('t-consumed-options', 'None') == 'True':
- code.append(indent_code("t_call_options.update(t_options)", level))
- code.append(indent_code(f"""
- if compile_options.get('lang') != t_call_options.get('lang'):
- self_lang = self.with_context(lang=t_call_options.get('lang'))
- yield from self_lang._compile({template}, t_call_options)(self_lang, t_call_values)
- else:
- yield from self._compile({template}, t_call_options)(self, t_call_values)
- """, level))
- else:
- code.append(indent_code(f"yield from self._compile({template}, t_call_options)(self, t_call_values)", level))
+ code.append(indent_code(f"""
+ irQweb = self.with_context(**t_call_options)
+ render_template = irQweb._compile({template})
+ yield from render_template(irQweb, t_call_values)
+ """, level))
return code
@@ -2003,7 +2060,7 @@ class IrQWeb(models.AbstractModel):
yield '<'
yield tagName
- attrs = self._post_processing_att(tagName, asset_attrs, compile_options)
+ attrs = self._post_processing_att(tagName, asset_attrs)
for name, value in attrs.items():
if value or isinstance(value, str):
yield f' {escape(str(name))}="{escape(str(value))}"'
@@ -2023,14 +2080,14 @@ class IrQWeb(models.AbstractModel):
# methods called by the compiled function at rendering time.
- def _debug_trace(self, debugger, options):
+ def _debug_trace(self, debugger):
"""Method called at compile time to load debugger."""
if debugger in SUPPORTED_DEBUGGER:
__import__(debugger).set_trace()
else:
raise ValueError(f"unsupported t-debug value: {debugger}")
- def _post_processing_att(self, tagName, atts, options):
+ def _post_processing_att(self, tagName, atts):
""" Method called at compile time for the static node and called at
runing time for the dynamic attributes.
@@ -2042,7 +2099,7 @@ class IrQWeb(models.AbstractModel):
"""
return atts
- def _get_field(self, record, field_name, expression, tagName, field_options, options, values):
+ def _get_field(self, record, field_name, expression, tagName, field_options, values):
"""Method called at compile time to return the field value.
:returns: tuple:
@@ -2052,16 +2109,16 @@ class IrQWeb(models.AbstractModel):
"""
field = record._fields[field_name]
- # adds template compile options for rendering fields
- field_options['template_options'] = options
-
# adds generic field options
field_options['tagName'] = tagName
field_options['expression'] = expression
field_options['type'] = field_options.get('widget', field.type)
- inherit_branding = options.get('inherit_branding', options.get('inherit_branding_auto') and record.check_access_rights('write', False))
+ inherit_branding = (
+ self.env.context['inherit_branding']
+ if 'inherit_branding' in self.env.context
+ else self.env.context.get('inherit_branding_auto') and record.check_access_rights('write', False))
field_options['inherit_branding'] = inherit_branding
- translate = options.get('edit_translations') and options.get('translatable') and field.translate
+ translate = self.env.context.get('edit_translations') and values.get('translatable') and field.translate
field_options['translate'] = translate
# field converter
@@ -2074,7 +2131,7 @@ class IrQWeb(models.AbstractModel):
return (attributes, content, inherit_branding or translate)
- def _get_widget(self, value, expression, tagName, field_options, options, values):
+ def _get_widget(self, value, expression, tagName, field_options, values):
"""Method called at compile time to return the widget value.
:returns: tuple:
@@ -2082,12 +2139,11 @@ class IrQWeb(models.AbstractModel):
* string or None: content
* boolean: force_display display the tag if the content and default_content are None
"""
- # adds template compile options for rendering fields
- field_options['template_options'] = options
-
field_options['type'] = field_options['widget']
field_options['tagName'] = tagName
field_options['expression'] = expression
+ inherit_branding = self.env.context.get('inherit_branding')
+ field_options['inherit_branding'] = inherit_branding
# field converter
model = 'ir.qweb.field.' + field_options['type']
@@ -2099,7 +2155,7 @@ class IrQWeb(models.AbstractModel):
attributes['data-oe-type'] = field_options['type']
attributes['data-oe-expression'] = field_options['expression']
- return (attributes, content, None)
+ return (attributes, content, inherit_branding)
def _get_asset_nodes(self, bundle, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False, media=None):
"""Generates asset nodes.
@@ -2185,11 +2241,10 @@ def render(template_name, values, load, **options):
(Widget, field, or asset rendering is not implemented.)
:param (string|int) template_name: template identifier
:param dict values: template values to be used for rendering
- :param def load: function like `load(template_name, options)` which
- returns an etree from the given template name (from initial rendering
- or template `t-call`).
- :param options: used to compile the template (the dict available for the
- rendering is frozen)
+ :param def load: function like `load(template_name)` which returns an etree
+ from the given template name (from initial rendering or template
+ `t-call`).
+ :param options: used to compile the template
:returns: bytes marked as markup-safe (decode to :class:`markupsafe.Markup`
instead of `str`)
:rtype: MarkupSafe
@@ -2203,6 +2258,11 @@ def render(template_name, values, load, **options):
pool = MockPool()
+ def _prepare_environment(self, values):
+ values['true'] = True
+ values['false'] = False
+ return self
+
def _get_field(self, *args):
raise NotImplementedError("Fields are not allowed in this rendering mode. Please use \"env['ir.qweb']._render\" method")
@@ -2217,6 +2277,13 @@ def render(template_name, values, load, **options):
super().__init__()
self.context = {}
- renderer = object.__new__(MockIrQWeb)
- renderer.env = MockEnv()
- return renderer._render(template_name, values, load=load, **options)
+ def __call__(self, cr=None, user=None, context=None, su=None):
+ """ Return an mocked environment based and update the sent context.
+ Allow to use `ir_qweb.with_context` with sand boxed qweb.
+ """
+ env = MockEnv()
+ env.context.update(self.context if context is None else context)
+ return env
+
+ renderer = MockIrQWeb(MockEnv(), tuple(), tuple())
+ return renderer._render(template_name, values, load=load, minimal_qcontext=True, **options)
diff --git a/odoo/addons/base/models/ir_qweb_fields.py b/odoo/addons/base/models/ir_qweb_fields.py
index 3080c3b6fc2..5bf43e581ad 100644
--- a/odoo/addons/base/models/ir_qweb_fields.py
+++ b/odoo/addons/base/models/ir_qweb_fields.py
@@ -117,8 +117,8 @@ class FieldConverter(models.AbstractModel):
"""
if not record:
return False
- value = record[field_name]
- return False if value is False else record.env[self._name].value_to_html(value, options=options)
+ value = record.with_context(**self.env.context)[field_name]
+ return False if value is False else self.value_to_html(value, options=options)
@api.model
def user_lang(self):
@@ -236,7 +236,6 @@ class DateTimeConverter(models.AbstractModel):
def value_to_html(self, value, options):
if not value:
return ''
- options = options or {}
lang = self.user_lang()
locale = babel_locale_parse(lang.code)
@@ -244,13 +243,14 @@ class DateTimeConverter(models.AbstractModel):
if isinstance(value, str):
value = fields.Datetime.from_string(value)
- value = fields.Datetime.context_timestamp(self, value)
-
if options.get('tz_name'):
+ self = self.with_context(tz=options['tz_name'])
tzinfo = babel.dates.get_timezone(options['tz_name'])
else:
tzinfo = None
+ value = fields.Datetime.context_timestamp(self, value)
+
if 'format' in options:
pattern = options['format']
else:
@@ -357,7 +357,7 @@ class HTMLConverter(models.AbstractModel):
for element in body.iter():
if element.attrib:
attrib = dict(element.attrib)
- attrib = irQweb._post_processing_att(element.tag, attrib, options.get('template_options'))
+ attrib = irQweb._post_processing_att(element.tag, attrib)
element.attrib.clear()
element.attrib.update(attrib)
return M(etree.tostring(body, encoding='unicode', method='html')[6:-7])
@@ -750,10 +750,9 @@ class Contact(models.AbstractModel):
opf = options.get('fields') or ["name", "address", "phone", "mobile", "email"]
sep = options.get('separator')
- template_options = options.get('template_options', {})
if sep:
opsep = escape(sep)
- elif template_options.get('no_tag_br'):
+ elif options.get('no_tag_br'):
# escaped joiners will auto-escape joined params
opsep = escape(', ')
else:
@@ -783,7 +782,7 @@ class Contact(models.AbstractModel):
'object': value,
'options': options
}
- return self.env['ir.qweb']._render('base.contact', val, **template_options)
+ return self.env['ir.qweb']._render('base.contact', val, minimal_qcontext=True)
class QwebView(models.AbstractModel):
@@ -801,4 +800,4 @@ class QwebView(models.AbstractModel):
_logger.warning("%s.%s must be a 'ir.ui.view', got %r.", record, field_name, view._name)
return ''
- return view._render(options.get('values', {}), engine='ir.qweb')
+ return self.env['ir.qweb']._render(view.id, options.get('values', {}))
diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py
index 7e15350b7fa..f29f95c20c1 100644
--- a/odoo/addons/base/models/ir_ui_view.py
+++ b/odoo/addons/base/models/ir_ui_view.py
@@ -15,9 +15,6 @@ import time
import uuid
import warnings
-from dateutil.relativedelta import relativedelta
-
-import werkzeug, werkzeug.urls
from lxml import etree
from lxml.etree import LxmlError
from lxml.builder import E
@@ -28,11 +25,9 @@ from odoo.http import request
from odoo.modules.module import get_resource_from_path, get_resource_path
from odoo.tools import config, ConstantMapping, get_diff, pycompat, apply_inheritance_specs, locate_node
from odoo.tools.convert import _fix_multiple_roots
-from odoo.tools.json import scriptsafe as json_scriptsafe
from odoo.tools import safe_eval, lazy_property, frozendict
from odoo.tools.view_validation import valid_view, get_variable_names, get_domain_identifiers, get_dict_asts
from odoo.tools.translate import xml_translate, TRANSLATED_ATTRS
-from odoo.tools.image import image_data_uri
from odoo.models import check_method_name
from odoo.osv.expression import expression
@@ -122,29 +117,6 @@ def transfer_modifiers_to_node(modifiers, node):
node.set('modifiers', json.dumps(modifiers))
-def keep_query(*keep_params, **additional_params):
- """
- Generate a query string keeping the current request querystring's parameters specified
- in ``keep_params`` and also adds the parameters specified in ``additional_params``.
-
- Multiple values query string params will be merged into a single one with comma seperated
- values.
-
- The ``keep_params`` arguments can use wildcards too, eg:
-
- keep_query('search', 'shop_*', page=4)
- """
- if not keep_params and not additional_params:
- keep_params = ('*',)
- params = additional_params.copy()
- qs_keys = list(request.httprequest.args) if request else []
- for keep_param in keep_params:
- for param in fnmatch.filter(qs_keys, keep_param):
- if param not in additional_params and param in qs_keys:
- params[param] = request.httprequest.args.getlist(param)
- return werkzeug.urls.url_encode(params)
-
-
class ViewCustom(models.Model):
_name = 'ir.ui.view.custom'
_description = 'Custom View'
@@ -1842,7 +1814,7 @@ actual arch.
def _read_template_keys(self):
""" Return the list of context keys to use for caching ``_read_template``. """
- return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations']
+ return ['lang', 'inherit_branding', 'edit_translations']
# apply ormcache_context decorator unless in dev mode...
@api.model
@@ -1963,46 +1935,10 @@ actual arch.
def render_public_asset(self, template, values=None):
template = self.sudo().browse(self.get_view_id(template))
template._check_view_access()
- return template.sudo()._render(values, engine="ir.qweb")
+ return self.env['ir.qweb'].sudo()._render(template, values)
- def _render_template(self, template, values=None, engine='ir.qweb'):
- return self.browse(self.get_view_id(template))._render(values, engine)
-
- def _render(self, values=None, engine='ir.qweb', minimal_qcontext=False, options=None):
- assert isinstance(self.id, int)
-
- qcontext = dict() if minimal_qcontext else self._prepare_qcontext()
- qcontext.update(values or {})
-
- return self.env[engine]._render(self.id, qcontext, **(options or {}))
-
- @api.model
- def _prepare_qcontext(self):
- """ Returns the qcontext : rendering context with website specific value (required
- to render website layout template)
- """
- qcontext = dict(
- env=self.env,
- user_id=self.env["res.users"].browse(self.env.user.id),
- res_company=self.env.company.sudo(),
- keep_query=keep_query,
- request=request, # might be unbound if we're not in an httprequest context
- debug=request.session.debug if request else '',
- test_mode_enabled=bool(config['test_enable'] or config['test_file']),
- json=json_scriptsafe,
- quote_plus=werkzeug.urls.url_quote_plus,
- time=safe_eval.time,
- datetime=safe_eval.datetime,
- relativedelta=relativedelta,
- xmlid=self.sudo().key,
- viewid=self.id,
- to_text=pycompat.to_text,
- image_data_uri=image_data_uri,
- # specific 'math' functions to ease rounding in templates and lessen controller marshmalling
- floor=math.floor,
- ceil=math.ceil,
- )
- return qcontext
+ def _render_template(self, template, values=None):
+ return self.env['ir.qweb']._render(template, values)
#------------------------------------------------------
# Misc
diff --git a/odoo/addons/base/tests/test_profiler.py b/odoo/addons/base/tests/test_profiler.py
index e75bcad0fd3..a47259a839d 100644
--- a/odoo/addons/base/tests/test_profiler.py
+++ b/odoo/addons/base/tests/test_profiler.py
@@ -461,6 +461,7 @@ class TestProfiling(TransactionCase):
template = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
+ 'key': 'root',
'arch_db': '''
[: ]
@@ -470,6 +471,7 @@ class TestProfiling(TransactionCase):
child_template = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
+ 'key': 'dummy',
'arch_db': ''
})
self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)"
diff --git a/odoo/addons/base/tests/test_qweb.py b/odoo/addons/base/tests/test_qweb.py
index e731cc93ebd..885a3d9bf33 100644
--- a/odoo/addons/base/tests/test_qweb.py
+++ b/odoo/addons/base/tests/test_qweb.py
@@ -17,7 +17,7 @@ from odoo.tests.common import TransactionCase
from odoo.addons.base.models.ir_qweb import QWebException, render
from odoo.tools import misc, mute_logger
from odoo.tools.json import scriptsafe as json_scriptsafe
-from odoo.exceptions import MissingError
+from odoo.exceptions import UserError, ValidationError, MissingError
unsafe_eval = eval
@@ -79,7 +79,7 @@ class TestQWebTField(TransactionCase):
"""
})
- text = etree.fromstring(view1._render()).find('span').text
+ text = etree.fromstring(self.env['ir.qweb']._render(view1.id)).find('span').text
self.assertEqual(text, '5.0000')
def test_xss_breakout(self):
@@ -95,7 +95,7 @@ class TestQWebTField(TransactionCase):
"""
})
- rendered = view._render({'malicious': '1