diff --git a/addons/hr/tests/test_multi_company.py b/addons/hr/tests/test_multi_company.py
index 2cbbf33482c..4c01f6ecdc4 100644
--- a/addons/hr/tests/test_multi_company.py
+++ b/addons/hr/tests/test_multi_company.py
@@ -3,7 +3,7 @@
from odoo.tests import Form
from odoo.addons.hr.tests.common import TestHrCommon
-from odoo.addons.base.models.qweb import QWebException
+from odoo.addons.base.models.ir_qweb import QWebException
class TestMultiCompany(TestHrCommon):
diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py
index 54be2a76ffd..0a0a97ad86b 100644
--- a/addons/http_routing/models/ir_http.py
+++ b/addons/http_routing/models/ir_http.py
@@ -19,7 +19,7 @@ import odoo
from odoo import api, models, registry, exceptions, tools, http
from odoo.addons.base.models import ir_http
from odoo.addons.base.models.ir_http import RequestUID
-from odoo.addons.base.models.qweb import QWebException
+from odoo.addons.base.models.ir_qweb import QWebException
from odoo.http import request
from odoo.osv import expression
from odoo.tools import config, ustr, pycompat
diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py
index ce0ba151fb5..fd3c6bb8018 100644
--- a/addons/mail/models/__init__.py
+++ b/addons/mail/models/__init__.py
@@ -47,6 +47,7 @@ from . import ir_http
from . import ir_model
from . import ir_model_fields
from . import ir_ui_view
+from . import ir_qweb
from . import res_company
from . import res_config_settings
from . import res_partner
diff --git a/addons/mail/models/ir_qweb.py b/addons/mail/models/ir_qweb.py
new file mode 100644
index 00000000000..ea058b131b9
--- /dev/null
+++ b/addons/mail/models/ir_qweb.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from odoo import models
+
+class QWebCodeFound(Exception):
+ """
+ Exception raised when a qweb compilation encounter dynamic content if the
+ option `raise_on_code` is True.
+ """
+
+class IrQweb(models.AbstractModel):
+ _inherit = 'ir.qweb'
+
+ def _get_template_cache_keys(self):
+ return super()._get_template_cache_keys() + ['raise_on_code']
+
+ def _compile_directives(self, el, options, indent):
+ if options.get('raise_on_code'):
+ raise QWebCodeFound()
+ return super()._compile_directives(el, options, indent)
diff --git a/addons/mail/models/mail_render_mixin.py b/addons/mail/models/mail_render_mixin.py
index b24c9d3d357..45f55cd8c0c 100644
--- a/addons/mail/models/mail_render_mixin.py
+++ b/addons/mail/models/mail_render_mixin.py
@@ -10,8 +10,9 @@ from lxml import html
from markupsafe import Markup
from werkzeug import urls
+from .ir_qweb import QWebCodeFound
from odoo import _, api, fields, models, tools
-from odoo.addons.base.models.qweb import QWebCodeFound
+from odoo.addons.base.models.ir_qweb import QWebException
from odoo.exceptions import UserError, AccessError
from odoo.tools import is_html_empty, safe_eval
from odoo.tools.rendering_tools import convert_inline_template_to_qweb, parse_inline_template, render_inline_template, template_env_globals
@@ -284,10 +285,10 @@ class MailRenderMixin(models.AbstractModel):
)
# remove the rendered tag
that was added in order to wrap potentially multiples nodes into one.
render_result = render_result[5:-6]
- except QWebCodeFound:
- group = self.env.ref('mail.group_mail_template_editor')
- raise AccessError(_('Only users belonging to the "%s" group can modify dynamic templates.', group.name))
except Exception as e:
+ if isinstance(e, QWebException) and isinstance(e.error, QWebCodeFound):
+ group = self.env.ref('mail.group_mail_template_editor')
+ raise AccessError(_('Only users belonging to the "%s" group can modify dynamic templates.', group.name))
_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
diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb.py
index 3627dc12742..0d8e0e2e2d6 100644
--- a/addons/web_editor/models/ir_qweb.py
+++ b/addons/web_editor/models/ir_qweb.py
@@ -39,8 +39,8 @@ REMOTE_CONNECTION_TIMEOUT = 2.5
logger = logging.getLogger(__name__)
-class QWeb(models.AbstractModel):
- """ QWeb object for rendering editor stuff
+class IrQWeb(models.AbstractModel):
+ """ IrQWeb object for rendering editor stuff
"""
_inherit = 'ir.qweb'
@@ -114,12 +114,12 @@ class QWeb(models.AbstractModel):
def _compile_directive_tag(self, el, options, indent):
if el.get('t-placeholder'):
el.set('t-att-placeholder', el.attrib.pop('t-placeholder'))
- return super(QWeb, self)._compile_directive_tag(el, options, indent)
+ return super(IrQWeb, self)._compile_directive_tag(el, options, indent)
# order and ignore
def _directives_eval_order(self):
- directives = super(QWeb, self)._directives_eval_order()
+ directives = super(IrQWeb, self)._directives_eval_order()
directives.insert(directives.index('call'), 'snippet')
directives.insert(directives.index('call'), 'snippet-call')
directives.insert(directives.index('call'), 'install')
diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py
index c7924c732c1..c0240ff50d2 100644
--- a/addons/website/models/ir_qweb.py
+++ b/addons/website/models/ir_qweb.py
@@ -34,8 +34,8 @@ class AssetsBundleMultiWebsite(AssetsBundle):
extra = website_id_path + extra
return super(AssetsBundleMultiWebsite, self).get_debug_asset_url(extra, name, extension)
-class QWeb(models.AbstractModel):
- """ QWeb object for rendering stuff in the website context """
+class IrQWeb(models.AbstractModel):
+ """ IrQWeb object for rendering stuff in the website context """
_inherit = 'ir.qweb'
@@ -47,14 +47,14 @@ class QWeb(models.AbstractModel):
'img': 'src',
}
- def get_asset_bundle(self, xmlid, files, env=None, css=True, js=True):
+ def _get_asset_bundle(self, xmlid, files, env=None, css=True, js=True):
return AssetsBundleMultiWebsite(xmlid, files, env=env)
def _post_processing_att(self, tagName, atts, options):
if atts.get('data-no-post-process'):
return atts
- atts = super(QWeb, self)._post_processing_att(tagName, atts, options)
+ atts = super(IrQWeb, self)._post_processing_att(tagName, atts, options)
if tagName == 'img' and 'loading' not in atts:
atts['loading'] = 'lazy' # default is auto
diff --git a/odoo/addons/base/models/__init__.py b/odoo/addons/base/models/__init__.py
index b3d7f66202d..3dc60a48f6b 100644
--- a/odoo/addons/base/models/__init__.py
+++ b/odoo/addons/base/models/__init__.py
@@ -2,7 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import assetsbundle
-from . import qweb
from . import ir_model
from . import ir_sequence
diff --git a/odoo/addons/base/models/ir_qweb.py b/odoo/addons/base/models/ir_qweb.py
index 74a972a7d5c..7777febea44 100644
--- a/odoo/addons/base/models/ir_qweb.py
+++ b/odoo/addons/base/models/ir_qweb.py
@@ -1,28 +1,40 @@
# -*- coding: utf-8 -*-
-from __future__ import print_function
-from textwrap import dedent
-import copy
-import io
import logging
+import os.path
import re
-import markupsafe
+import traceback
+import token
import tokenize
-from lxml import html, etree
+import io
+import copy
+
+from markupsafe import Markup, escape
+from collections.abc import Sized, Mapping
+from itertools import count, chain
+from textwrap import dedent, indent as _indent
+from lxml import etree, html
+from psycopg2.extensions import TransactionRollbackError
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.misc import get_lang
from odoo.http import request
from odoo.modules.module import get_resource_path
from odoo.tools.profiler import QwebTracker
-from odoo.addons.base.models.qweb import QWeb
from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.base.models.ir_asset import can_aggregate, STYLE_EXTENSIONS, SCRIPT_EXTENSIONS
_logger = logging.getLogger(__name__)
+# QWeb token usefull for generate expression used in `_compile_expr_tokens` method
+token.QWEB = token.NT_OFFSET - 1
+token.tok_name[token.QWEB] = 'QWEB'
+
+
+# security safe eval opcodes for generated expression validation, used in `_compile_expr`
_SAFE_QWEB_OPCODES = _EXPR_OPCODES.union(to_opcodes([
'MAKE_FUNCTION', 'CALL_FUNCTION', 'CALL_FUNCTION_KW', 'CALL_FUNCTION_EX',
'CALL_METHOD', 'LOAD_METHOD',
@@ -38,7 +50,67 @@ _SAFE_QWEB_OPCODES = _EXPR_OPCODES.union(to_opcodes([
])) - _BLACKLIST
-class IrQWeb(models.AbstractModel, QWeb):
+# eval to compile generated string python code into binary code, used in `_compile`
+unsafe_eval = eval
+
+
+# regexpr for string formatting and extract ( ruby-style )|( jinja-style ) used in `_compile_format`
+_FORMAT_REGEX = re.compile(r'(?:#\{(.+?)\})|(?:\{\{(.+?)\}\})')
+
+
+RE_RSTRIP = re.compile(r'\n[ \t]*$')
+
+
+####################################
+### QWebException ###
+####################################
+
+class QWebException(Exception):
+ """ Management of errors that raised when rendering a QWeb template.
+ """
+ def __init__(self, message, qweb, options, error=None, template=None, path=None, code=None):
+ self.error = error
+ self.name = template
+ self.code = code if options and options.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 = nodes[0]
+ node[:] = []
+ node.text = None
+ self.html = etree.tostring(node, encoding='unicode')
+ self.stack = traceback.format_exc()
+
+ self.message = message
+ if self.error is not None:
+ self.message = "%s\n%s: %s" % (self.message, self.error.__class__.__name__, self.error)
+ if self.name is not None:
+ self.message = "%s\nTemplate: %s" % (self.message, self.name)
+ if self.path is not None:
+ self.message = "%s\nPath: %s" % (self.message, self.path)
+ if self.html is not None:
+ self.message = "%s\nNode: %s" % (self.message, self.html)
+
+ super(QWebException, self).__init__(message)
+
+ def __str__(self):
+ message = "%s\n%s\n%s" % (self.error, self.stack, self.message)
+ if self.code is not None:
+ message = "%s\nCompiled code:\n%s" % (message, self.code)
+ return message
+
+ def __repr__(self):
+ return f"QWebException({repr(self.message)})"
+
+####################################
+### QWeb ###
+####################################
+
+
+class IrQWeb(models.AbstractModel):
""" Base QWeb rendering engine
* to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
create new models called :samp:`ir.qweb.field.{widget}`
@@ -50,7 +122,13 @@ class IrQWeb(models.AbstractModel, QWeb):
_name = 'ir.qweb'
_description = 'Qweb'
+ _void_elements = frozenset([
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
+ 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
+ _name_gen = count()
+ # _available_objects builtins is not security safe (it's dangerous), is overridden by ir_qweb to only expose the safe_eval builtins.
_available_objects = dict(_BUILTINS)
+ _allowed_keyword = ['False', 'None', 'True', 'and', 'as', 'elif', 'else', 'for', 'if', 'in', 'is', 'not', 'or']
@QwebTracker.wrap_render
@api.model
@@ -72,26 +150,163 @@ class IrQWeb(models.AbstractModel, QWeb):
compile_options = dict(self.env.context, dev_mode='qweb' in tools.config['dev_mode'])
compile_options.update(options)
- result = super()._render(template, values=values, **compile_options)
- return result
+ render_template = self._compile(template, compile_options)
+ rendering = render_template(self, values or {})
+ 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', 'raise_on_code']
+ return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations', 'website_id', 'profile']
# apply ormcache_context decorator unless in dev mode...
@tools.conditional(
'xml' not in tools.config['dev_mode'],
- tools.ormcache('id_or_xml_id', 'tuple(options.get(k) for k in self._get_template_cache_keys())'),
+ tools.ormcache('template', 'tuple(options.get(k) for k in self._get_template_cache_keys())'),
)
@QwebTracker.wrap_compile
- def _compile(self, id_or_xml_id, options):
+ def _compile(self, template, options):
+ """ Compile the given template into a rendering function (generator)::
+
+ render(qweb, values)
+
+ where ``qweb`` is a QWeb instance and ``values`` are the values to render.
+ """
+ if options is None:
+ options = {}
+
+ element, document, ref = self._get_template(template, options)
+ if not ref:
+ ref = element.get('t-name', str(document))
+
+ # reference to get xml and etree (usually the template ID)
+ options['ref'] = ref
+ # str xml of the reference template used for compilation. Useful for debugging, dev mode and profiling.
+ options['ref_xml'] = document if isinstance(document, str) else str(document, 'utf-8')
+
+ _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
+ # 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
+
+ if not options.get('nsmap'):
+ _options['nsmap'] = {}
+
+ # generate code
+
+ def_name = f"template_{ref}" if isinstance(ref, int) else "template"
+
try:
- id_or_xml_id = int(id_or_xml_id)
+ _options['_text_concat'] = []
+ self._appendText("", _options) # To ensure the template function is a generator and doesn't become a regular function
+ code_lines = ([f"def {def_name}(self, values, log):"] +
+ self._compile_node(element, _options, 1) +
+ self._flushText(_options, 1))
+ except QWebException as e:
+ raise e
+ except Exception as e:
+ raise QWebException("Error when compiling xml template",
+ self, options, e, template, path=_options.get('last_path_node')) from e
+ try:
+ code = '\n'.join(code_lines)
+ except QWebException as e:
+ raise e
+ except Exception as e:
+ code = '\n'.join(map(str, code_lines))
+ raise QWebException("Error when compiling xml template",
+ self, options, e, template, code=code) from e
+
+ # compile code and defined default values
+
+ try:
+ # noinspection PyBroadException
+ compiled = compile(code, f'<{def_name}>', 'exec')
+ globals_dict = self._prepare_globals({}, options)
+ globals_dict['__builtins__'] = globals_dict # So that unknown/unsafe builtins are never added.
+ unsafe_eval(compiled, globals_dict)
+ compiled_fn = globals_dict[def_name]
+ except QWebException as e:
+ raise e
+ except Exception as e:
+ raise QWebException("Error when compiling xml template",
+ self, options, e, template, code=code) from e
+
+ # return the wrapped function
+
+ def render_template(self, values):
+ try:
+ log = {'last_path_node': None}
+ values = self._prepare_values(values, options)
+ yield from compiled_fn(self, values, log)
+ except (QWebException, TransactionRollbackError) as e:
+ raise e
+ except Exception as e:
+ raise QWebException("Error when render the template",
+ self, options, e, template, path=log.get('last_path_node'), code=code) from e
+
+ return render_template
+
+ # read and load input template
+
+ def _get_template(self, template, options):
+ """ 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, name or etree
+ :param options: used to compile the template (the dict available for
+ the rendering is frozen)
+ ``load`` (function) overrides the load method
+ """
+ try:
+ template = int(template)
except:
pass
- return super()._compile(id_or_xml_id, options=options)
+
+ ref = template
+ if isinstance(template, etree._Element):
+ element = template
+ document = etree.tostring(template)
+ return (element, document, template.get('t-name'))
+ else:
+ try:
+ loaded = options.get('load', self._load)(template, options)
+ if not loaded:
+ raise ValueError("Can not load template '%s'" % template)
+ document, ref = loaded
+ except QWebException as e:
+ raise e
+ except Exception as e:
+ template = options.get('caller_template', template)
+ raise QWebException("load could not load template",
+ self, options, e, template) from e
+
+ if document is None:
+ raise QWebException("Template not found",
+ self, options, template=template)
+
+ if isinstance(document, etree._Element):
+ element = document
+ document = etree.tostring(document, encoding='utf-8')
+ elif not document.strip().startswith('<') and os.path.exists(document):
+ element = etree.parse(document).getroot()
+ else:
+ element = etree.fromstring(document)
+
+ for node in element:
+ if node.get('t-name') == str(template):
+ return (node, document, ref)
+ return (element, document, ref)
def _load(self, name, options):
lang = options.get('lang', get_lang(self.env).code)
@@ -120,27 +335,813 @@ class IrQWeb(models.AbstractModel, QWeb):
else:
return (template, view_id)
+ # values for running time
+
+ def _prepare_values(self, values, options):
+ """ Prepare the context that will sent to the compiled and evaluated
+ function.
+
+ :param values: template values to be used for rendering
+ :param options: frozen dict of compilation parameters.
+ """
+ check_values(values)
+ values['true'] = True
+ values['false'] = False
+ if 'request' not in values:
+ values['request'] = request
+ return values
+
+ def _prepare_globals(self, globals_dict, options):
+ """ Prepare the global context that will sent to eval the qweb generated
+ code.
+
+ :param globals_dict: template global values use in compiled code
+ :param options: frozen dict of compilation parameters.
+ """
+ globals_dict['Sized'] = Sized
+ globals_dict['Mapping'] = Mapping
+ globals_dict['Markup'] = Markup
+ globals_dict['escape'] = escape
+ globals_dict['compile_options'] = options
+ globals_dict.update(self._available_objects)
+ return globals_dict
+
+ # helpers for compilation
+
+ def _appendText(self, text, options):
+ """ Add an item (converts to a string) to the list.
+ This will be concatenated and added during a call to the
+ `_flushText` method. This makes it possible to return only one
+ yield containing all the parts."""
+ options['_text_concat'].append(self._compile_to_str(text))
+
+ def _stripText(self, options):
+ text_concat = options['_text_concat']
+ if not text_concat:
+ return ''
+ result = RE_RSTRIP.search(text_concat[-1])
+ if not result:
+ return ''
+ return result.group(0)
+
+ def _flushText(self, options, indent, rstrip=False):
+ """Concatenate all the textual chunks added by the `_appendText`
+ method into a single yield."""
+ text_concat = options['_text_concat']
+ if text_concat:
+ if rstrip:
+ text_concat[-1] = RE_RSTRIP.sub('', text_concat[-1])
+ text = ''.join(text_concat)
+ text_concat.clear()
+ return [f"{' ' * indent}yield {repr(text)}"]
+ else:
+ return []
+
+ def _indent(self, code, indent):
+ """Indent the code to respect the python syntax."""
+ return _indent(code, ' ' * indent)
+
+ def _make_name(self, prefix='var'):
+ """Generates a unique name."""
+ return f"{prefix}_{next(self._name_gen)}"
+
+ def _is_static_node(self, el, options):
+ """ Test whether the given element is purely static, i.e. (there
+ are no t-* attributes), does not require dynamic rendering for its
+ attributes.
+ """
+ return el.tag != 't' and not any(att.startswith('t-') and att not in ['t-tag', 't-inner-content'] for att in el.attrib)
+
+ # compile python expression and format string
+
+ def _compile_format(self, expr):
+ """ Parses the provided format string and compiles it to a single
+ expression python, uses string with format method.
+ Use format is faster to concat string and values.
+ """
+ text = ''
+ values = []
+ base_idx = 0
+ for m in _FORMAT_REGEX.finditer(expr):
+ literal = expr[base_idx:m.start()]
+ if literal:
+ text += literal.replace('{', '{{').replace("}", "}}")
+ text += '{}'
+ values.append(f'self._compile_to_str({self._compile_expr(m.group(1) or m.group(2))})')
+ base_idx = m.end()
+ # string past last regex match
+ literal = expr[base_idx:]
+ if literal:
+ text += literal.replace('{', '{{').replace("}", "}}")
+
+ code = repr(text)
+ if values:
+ code += f'.format({", ".join(values)})'
+ return code
+
+ def _compile_expr_tokens(self, tokens, allowed_keys, argument_names=None, raise_on_missing=False):
+ """ Transform the list of token coming into a python instruction in
+ textual form by adding the namepaces for the dynamic values.
+
+ Example: `5 + a + b.c` to be `5 + values.get('a') + values['b'].c`
+ Unknown values are considered to be None, but using `values['b']`
+ gives a clear error message in cases where there is an attribute for
+ example (have a `KeyError: 'b'`, instead of `AttributeError: 'NoneType'
+ object has no attribute 'c'`).
+
+ @returns str
+ """
+ # Finds and extracts the current "scope"'s "allowed values": values
+ # which should not be accessed through the environment's namespace:
+ # * the local variables of a lambda should be accessed directly e.g.
+ # lambda a: a + b should be compiled to lambda a: a + values['b'],
+ # since a is local to the lambda it has to be accessed directly
+ # but b needs to be accessed through the rendering environment
+ # * similarly for a comprehensions [a + b for a in c] should be
+ # compiledto [a + values.get('b') for a in values.get('c')]
+ # to avoid the risk of confusion between nested lambdas / comprehensions,
+ # this is currently performed independently at each level of brackets
+ # nesting (hence the function being recursive).
+ index = 0
+ open_bracket_index = -1
+ bracket_depth = 0
+
+ argument_name = '_arg_%s__'
+ argument_names = argument_names or []
+
+ while index < len(tokens):
+ t = tokens[index]
+ if t.exact_type in [token.LPAR, token.LSQB, token.LBRACE]:
+ bracket_depth += 1
+ if t.exact_type in [token.RPAR, token.RSQB, token.RBRACE]:
+ bracket_depth -= 1
+ elif bracket_depth == 0 and t.exact_type == token.NAME:
+ string = t.string
+ if string == 'lambda': # lambda => allowed values for the current bracket depth
+ i = index + 1
+ while i < len(tokens):
+ t = tokens[i]
+ if t.exact_type == token.NAME:
+ argument_names.append(t.string)
+ elif t.exact_type == token.COMMA:
+ pass
+ elif t.exact_type == token.COLON:
+ break
+ elif t.exact_type == token.EQUAL:
+ raise NotImplementedError('Lambda default values are not supported')
+ else:
+ raise NotImplementedError('This lambda code style is not implemented.')
+ i += 1
+ elif string == 'for': # list comprehensions => allowed values for the current bracket depth
+ i = index + 1
+ while len(tokens) > i:
+ t = tokens[i]
+ if t.exact_type == token.NAME:
+ if t.string == 'in':
+ break
+ argument_names.append(t.string)
+ elif t.exact_type in [token.COMMA, token.LPAR, token.RPAR]:
+ pass
+ else:
+ raise NotImplementedError('This loop code style is not implemented.')
+ i += 1
+
+ index += 1
+
+ # Use bracket to nest structures.
+ # Recursively processes the "sub-scopes", and replace their content with
+ # a compiled node. During this recursive call we add to the allowed
+ # values the values provided by the list comprehension, lambda, etc.,
+ # previously extracted.
+ index = 0
+ open_bracket_index = -1
+ bracket_depth = 0
+
+ while index < len(tokens):
+ t = tokens[index]
+ string = t.string
+
+ if t.exact_type in [token.LPAR, token.LSQB, token.LBRACE]:
+ if bracket_depth == 0:
+ open_bracket_index = index
+ bracket_depth += 1
+ elif t.exact_type in [token.RPAR, token.RSQB, token.RBRACE]:
+ bracket_depth -= 1
+ if bracket_depth == 0:
+ code = self._compile_expr_tokens(
+ tokens[open_bracket_index + 1:index],
+ list(allowed_keys),
+ list(argument_names),
+ raise_on_missing,
+ )
+ code = tokens[open_bracket_index].string + code + t.string
+ tokens[open_bracket_index:index + 1] = [tokenize.TokenInfo(token.QWEB, code, tokens[open_bracket_index].start, t.end, '')]
+ index = open_bracket_index
+
+ index += 1
+
+ # The keys will be namespaced by values if they are not allowed. In
+ # order to have a clear keyError message, this will be replaced by
+ # values['key'] for certain cases (for example if an attribute is called
+ # key.attrib, or an index key[0] ...)
+ code = []
+ index = 0
+ pos = tokens and tokens[0].start # to keep indent when use expr on multi line
+ while index < len(tokens):
+ t = tokens[index]
+ string = t.string
+
+ if t.start[0] != pos[0]:
+ pos = (t.start[0], 0)
+ space = t.start[1] - pos[1]
+ if space:
+ code.append(' ' * space)
+ pos = t.start
+
+ if t.exact_type == token.NAME:
+ if string == 'lambda': # lambda => allowed values
+ code.append('lambda ')
+ index += 1
+ while index < len(tokens):
+ t = tokens[index]
+ if t.exact_type == token.NAME and t.string in argument_names:
+ code.append(argument_name % t.string)
+ if t.exact_type in [token.COMMA, token.COLON]:
+ code.append(t.string)
+ if t.exact_type == token.COLON:
+ break
+ index += 1
+ if t.end[0] != pos[0]:
+ pos = (t.end[0], 0)
+ else:
+ pos = t.end
+ elif string in argument_names:
+ code.append(argument_name % t.string)
+ elif string in allowed_keys:
+ code.append(string)
+ elif index + 1 < len(tokens) and tokens[index + 1].exact_type == token.EQUAL: # function kw
+ code.append(string)
+ elif index > 0 and tokens[index - 1] and tokens[index - 1].exact_type == token.DOT:
+ code.append(string)
+ elif raise_on_missing or index + 1 < len(tokens) and tokens[index + 1].exact_type in [token.DOT, token.LPAR, token.LSQB, 'qweb']:
+ # Should have values['product'].price to raise an error when get
+ # the 'product' value and not an 'NoneType' object has no
+ # attribute 'price' error.
+ code.append(f'values[{repr(string)}]')
+ else:
+ # not assignation allowed, only getter
+ code.append(f'values.get({repr(string)})')
+ elif t.type not in [tokenize.ENCODING, token.ENDMARKER, token.DEDENT]:
+ code.append(string)
+
+ if t.end[0] != pos[0]:
+ pos = (t.end[0], 0)
+ else:
+ pos = t.end
+
+ index += 1
+
+ return ''.join(code)
+
+ def _compile_expr(self, expr, raise_on_missing=False):
+ """Transform string coming into a python instruction in textual form by
+ adding the namepaces for the dynamic values.
+ This method tokenize the string and call ``_compile_expr_tokens``
+ method.
+ """
+ readable = io.BytesIO(f"({expr or ''})".strip().encode('utf-8'))
+ try:
+ tokens = list(tokenize.tokenize(readable.readline))
+ except tokenize.TokenError:
+ raise ValueError(f"Can not compile expression: {expr}")
+
+ expression = self._compile_expr_tokens(tokens, self._allowed_keyword + list(self._available_objects.keys()), raise_on_missing=raise_on_missing)
+
+ assert_valid_codeobj(_SAFE_QWEB_OPCODES, compile(expression, '<>', 'eval'), expr)
+
+ return f"({expression})"
+
+ def _compile_bool(self, attr, default=False):
+ """Convert the statements as a boolean."""
+ if attr:
+ if attr is True:
+ return True
+ attr = attr.lower()
+ if attr in ('false', '0'):
+ return False
+ elif attr in ('true', '1'):
+ return True
+ return bool(default)
+
+ def _compile_to_str(self, expr):
+ """ Generates a text value (an instance of text_type) from an arbitrary
+ source.
+ """
+ return pycompat.to_text(expr)
+
# order
def _directives_eval_order(self):
- directives = super()._directives_eval_order()
- directives.insert(directives.index('foreach'), 'groups')
- directives.insert(directives.index('call'), 'lang')
- directives.insert(directives.index('field'), 'call-assets')
- return directives
+ """ List all supported directives in the order in which they should be
+ evaluated on a given element. For instance, a node bearing both
+ ``foreach`` and ``if`` should see ``foreach`` executed before ``if`` aka
+ .. code-block:: xml
+
+ should be equivalent to
+ .. code-block:: xml
+
+
+
+ then this method should return ``['foreach', 'if']``.
+ """
+ return [
+ 'debug',
+ 'groups',
+ 'foreach',
+ 'if', 'elif', 'else',
+ 'call-assets',
+ 'field', 'esc', 'raw', 'out',
+ 'tag',
+ 'lang',
+ 'call',
+ 'set',
+ 'inner-content',
+ ]
# compile
def _compile_node(self, el, options, indent):
+ """ Compile the given element into python code.
+
+ The t-* attributes (directives) will be converted to a python instruction. If there
+ are no t-* attributes, the element will be considered static.
+
+ Directives are compiled using the order provided by the
+ ``_directives_eval_order`` method (an create the
+ ``options['iter_directives']`` iterator).
+ For compilation, the directives supported are those with a
+ compilation method ``_compile_directive_*``
+
+ :return: list of string
+ """
if el.get("groups"):
el.set("t-groups", el.attrib.pop("groups"))
- return super()._compile_node(el, options, indent)
- # compile directives
+ # if tag don't have qweb attributes don't use directives
+ if self._is_static_node(el, options):
+ return self._compile_static_node(el, options, indent)
+
+ path = options['root'].getpath(el)
+ if options['last_path_node'] != path:
+ options['last_path_node'] = path
+ body = [self._indent(f'log["last_path_node"] = {repr(path)}', indent)]
+ else:
+ body = []
+
+ # create an iterator on directives to compile in order
+ options['iter_directives'] = iter(self._directives_eval_order() + [None])
+
+ el.set('t-tag', el.tag)
+ if not ({'t-out', 't-esc', 't-raw', 't-field'} & set(el.attrib)):
+ el.set('t-inner-content', 'True')
+
+ return body + self._compile_directives(el, options, indent)
+
+ def _compile_static_node(self, el, options, indent):
+ """ 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)
+ else:
+ # Etree will remove the ns prefixes indirection by inlining the corresponding
+ # nsmap definition into the tag attribute. Restore the tag and prefix here.
+ unqualified_el_tag = etree.QName(el.tag).localname
+ el_tag = unqualified_el_tag
+ if el.prefix:
+ el_tag = f'{el.prefix}:{el_tag}'
+
+ attrib = {}
+ # If `el` introduced new namespaces, write them as attribute by using the
+ # `attrib` dict.
+ for ns_prefix, ns_definition in set(el.nsmap.items()) - set(options['nsmap'].items()):
+ if ns_prefix is None:
+ attrib['xmlns'] = ns_definition
+ else:
+ attrib[f'xmlns:{ns_prefix}'] = ns_definition
+
+ # Etree will also remove the ns prefixes indirection in the attributes. As we only have
+ # the namespace definition, we'll use an nsmap where the keys are the definitions and
+ # the values the prefixes in order to get back the right prefix and restore it.
+ ns = chain(options['nsmap'].items(), el.nsmap.items())
+ nsprefixmap = {v: k for k, v in ns}
+ for key, value in el.attrib.items():
+ attrib_qname = etree.QName(key)
+ if attrib_qname.namespace:
+ attrib[f'{nsprefixmap[attrib_qname.namespace]}:{attrib_qname.localname}'] = value
+ else:
+ attrib[key] = value
+
+ attrib = self._post_processing_att(el.tag, attrib, options)
+
+ # Update the dict of inherited namespaces before continuing the recursion. Note:
+ # since `options['nsmap']` is a dict (and therefore mutable) and we do **not**
+ # want changes done in deeper recursion to bevisible in earlier ones, we'll pass
+ # a copy before continuing the recursion and restore the original afterwards.
+ original_nsmap = dict(options['nsmap'])
+
+ if unqualified_el_tag != 't':
+ attributes = ''.join(f' {str(name)}="{str(escape(str(value)))}"'
+ for name, value in attrib.items() if value or isinstance(value, str))
+ self._appendText(f'<{el_tag}{attributes}', options)
+ if unqualified_el_tag in self._void_elements:
+ self._appendText('/>', options)
+ else:
+ self._appendText('>', options)
+
+ if el.nsmap:
+ options['nsmap'].update(el.nsmap)
+ body = self._compile_directive_inner_content(el, options, indent)
+ options['nsmap'] = original_nsmap
+ else:
+ body = self._compile_directive_inner_content(el, options, indent)
+
+ if unqualified_el_tag != 't':
+ if unqualified_el_tag not in self._void_elements:
+ self._appendText(f'{el_tag}>', options)
+
+ return body
+
+ def _compile_attributes(self, options, indent):
+ """Generates the part of the code that post-process the attributes
+ (this is ``attrs`` in the compiled code) during rendering time.
+ """
+ # Use str(value) to change Markup into str and escape it, then use str
+ # to avoid the escaping of the other html content.
+ body = self._flushText(options, indent)
+ body.append(self._indent(dedent("""
+ attrs = self._post_processing_att(tagName, attrs, compile_options)
+ for name, value in attrs.items():
+ if value or isinstance(value, str):
+ yield f' {str(escape(str(name)))}="{str(escape(str(value)))}"'
+ """).strip(), indent))
+ return body
+
+ def _compile_static_attributes(self, el, options, indent):
+ """ Compile the static and dynamc attributes of the given element.
+
+ We do not support namespaced dynamic attributes.
+ """
+ # Etree will also remove the ns prefixes indirection in the attributes. As we only have
+ # the namespace definition, we'll use an nsmap where the keys are the definitions and
+ # the values the prefixes in order to get back the right prefix and restore it.
+ nsprefixmap = {v: k for k, v in chain(options['nsmap'].items(), el.nsmap.items())}
+
+ code = []
+ for key, value in el.attrib.items():
+ if not key.startswith('t-'):
+ attrib_qname = etree.QName(key)
+ if attrib_qname.namespace:
+ key = f'{nsprefixmap[attrib_qname.namespace]}:{attrib_qname.localname}'
+ code.append(self._indent(f'attrs[{repr(key)}] = {repr(value)}', indent))
+ return code
+
+ def _compile_dynamic_attributes(self, el, options, indent):
+ """ Compile the dynamic attributes of the given element into a list
+ string (this is adding elements to ``attrs`` in the compiled code).
+
+ We do not support namespaced dynamic attributes.
+ """
+ code = []
+ for name, value in el.attrib.items():
+
+ if name.startswith('t-attf-'):
+ code.append(self._indent(f"attrs[{repr(name[7:])}] = {self._compile_format(value)}", indent))
+ elif name.startswith('t-att-'):
+ code.append(self._indent(f"attrs[{repr(name[6:])}] = {self._compile_expr(value)}", indent))
+ elif name == 't-att':
+ code.append(self._indent(dedent(f"""
+ atts_value = {self._compile_expr(value)}
+ if isinstance(atts_value, dict):
+ attrs.update(atts_value)
+ elif isinstance(atts_value, (list, tuple)) and not isinstance(atts_value[0], (list, tuple)):
+ attrs.update([atts_value])
+ elif isinstance(atts_value, (list, tuple)):
+ attrs.update(dict(atts_value))
+ """), indent))
+ return code
+
+ def _compile_all_attributes(self, el, options, indent, attr_already_created=False):
+ """ Compile the attributes (static and dynamic) of the given elements
+ into a list of str.
+ (this compiled The code will create the ``attrs`` in the compiled code).
+ """
+ code = []
+ if any(name.startswith('t-att') or not name.startswith('t-') for name, value in el.attrib.items()):
+ if not attr_already_created:
+ attr_already_created = True
+ code.append(self._indent("attrs = {}", indent))
+ code.extend(self._compile_static_attributes(el, options, indent))
+ code.extend(self._compile_dynamic_attributes(el, options, indent))
+ if attr_already_created:
+ code.append(self._indent(f"tagName = {repr(el.tag)}", indent))
+ code.extend(self._compile_attributes(options, indent))
+ return code
+
+ def _compile_tag_open(self, el, options, indent, attr_already_created=False):
+ """ Compile the opening tag of the given element into a list of string. """
+ extra_attrib = {}
+ if not el.nsmap:
+ unqualified_el_tag = el_tag = el.tag
+ else:
+ # Etree will remove the ns prefixes indirection by inlining the corresponding
+ # nsmap definition into the tag attribute. Restore the tag and prefix here.
+ # Note: we do not support namespace dynamic attributes, we need a default URI
+ # on the root and use attribute directive t-att="{'xmlns:example': value}".
+ unqualified_el_tag = etree.QName(el.tag).localname
+ el_tag = unqualified_el_tag
+ if el.prefix:
+ el_tag = f'{el.prefix}:{el_tag}'
+
+ # If `el` introduced new namespaces, write them as attribute by using the
+ # `extra_attrib` dict.
+ for ns_prefix, ns_definition in set(el.nsmap.items()) - set(options['nsmap'].items()):
+ if ns_prefix is None:
+ extra_attrib['xmlns'] = ns_definition
+ else:
+ extra_attrib[f'xmlns:{ns_prefix}'] = ns_definition
+
+ code = []
+ if unqualified_el_tag != 't':
+ attributes = ''.join(f' {str(name)}="{str(escape(self._compile_to_str(value)))}"'
+ for name, value in extra_attrib.items())
+ self._appendText("<{}{}".format(el_tag, attributes), options)
+ code.extend(self._compile_all_attributes(el, options, indent, attr_already_created))
+ if unqualified_el_tag in self._void_elements:
+ self._appendText('/>', options)
+ else:
+ self._appendText('>', options)
+
+ return code
+
+ def _compile_tag_close(self, el, options):
+ """ Compile the closing tag of the given element into a list of string. """
+ if not el.nsmap:
+ unqualified_el_tag = el_tag = el.tag
+ else:
+ unqualified_el_tag = etree.QName(el.tag).localname
+ el_tag = unqualified_el_tag
+ if el.prefix:
+ el_tag = f'{el.prefix}:{el_tag}'
+
+ if unqualified_el_tag != 't' and el_tag not in self._void_elements:
+ self._appendText(f'{el_tag}>', options)
+ return []
+
+ def _compile_directives(self, el, options, indent):
+ """ Compile the given element, following the directives given in the
+ iterator ``options['iter_directives']`` create by `_compile_node``
+ method.
+
+ :return: list of code lines
+ """
+
+ if self._is_static_node(el, options):
+ el.attrib.pop('t-tag', None)
+ el.attrib.pop('t-inner-content', None)
+ return self._compile_static_node(el, options, indent)
+
+ # compile the first directive present on the element
+ for directive in options['iter_directives']:
+ if ('t-' + directive) in el.attrib:
+ return self._compile_directive(el, options, directive, indent)
+
+ return []
@QwebTracker.wrap_compile_directive
def _compile_directive(self, el, options, directive, indent):
- return super()._compile_directive(el, options, directive, indent)
+ compile_handler = getattr(self, f"_compile_directive_{directive.replace('-', '_')}", None)
+ return compile_handler(el, options, indent)
+
+ # compile directives
+
+ def _compile_directive_debug(self, el, options, indent):
+ """Compile `t-debug` expressions into a python code as a list of
+ strings.
+
+ The code will contains the call to the debugger chosen from the valid
+ list.
+ """
+ debugger = el.attrib.pop('t-debug')
+ code = []
+ if options.get('dev_mode'):
+ code.append(self._indent(f"self._debug_trace({repr(debugger)}, compile_options)", indent))
+ else:
+ _logger.warning("@t-debug in template is only available in qweb dev mode options")
+ code.extend(self._compile_directives(el, options, indent))
+ return code
+
+ def _compile_directive_options(self, el, options, indent):
+ """
+ compile t-options and add to the dict the t-options-xxx values
+ """
+ varname = options.get('t_options_varname', 't_options')
+ code = []
+ dict_arg = []
+ for key in list(el.attrib):
+ if key.startswith('t-options-'):
+ value = el.attrib.pop(key)
+ option_name = key[10:]
+ dict_arg.append(f'{repr(option_name)}:{self._compile_expr(value)}')
+
+ t_options = el.attrib.pop('t-options', None)
+ if t_options and dict_arg:
+ code.append(self._indent(f"{varname} = {{**{self._compile_expr(t_options)}, {', '.join(dict_arg)}}}", indent))
+ elif dict_arg:
+ code.append(self._indent(f"{varname} = {{{', '.join(dict_arg)}}}", indent))
+ elif t_options:
+ code.append(self._indent(f"{varname} = {self._compile_expr(t_options)}", indent))
+
+ return code
+
+ def _compile_directive_tag(self, el, options, indent):
+ """Compile the element tag into a python code as a list of strings.
+
+ The code will contains the opening tag, namespace, static and dynamic
+ attributes and closing tag.
+ """
+ el.attrib.pop('t-tag', None)
+
+ code = self._compile_tag_open(el, options, indent, False)
+
+ # Update the dict of inherited namespaces before continuing the recursion. Note:
+ # since `options['nsmap']` is a dict (and therefore mutable) and we do **not**
+ # want changes done in deeper recursion to bevisible in earlier ones, we'll pass
+ # a copy before continuing the recursion and restore the original afterwards.
+ if el.nsmap:
+ code.extend(self._compile_directives(el, dict(options, nsmap=el.nsmap), indent))
+ else:
+ code.extend(self._compile_directives(el, options, indent))
+
+ code.extend(self._compile_tag_close(el, options))
+
+ return code
+
+ def _compile_directive_set(self, el, options, indent):
+ """Compile `t-set` expressions into a python code as a list of
+ strings.
+
+ There are 3 kinds of `t-set`:
+ * `t-value` containing python code;
+ * `t-valuef` containing strings to format;
+ * whose value is the content of the tag (being Markup safe).
+
+ The code will contain the assignment of the dynamically generated value.
+ """
+ varname = el.attrib.pop('t-set')
+ code = self._flushText(options, indent, True)
+
+ if 't-value' in el.attrib:
+ if varname == '0':
+ raise ValueError('t-set="0" should not contains t-value or t-valuef')
+ expr = el.attrib.pop('t-value') or 'None'
+ expr = self._compile_expr(expr)
+ elif 't-valuef' in el.attrib:
+ if varname == '0':
+ raise ValueError('t-set="0" should not contains t-value or t-valuef')
+ exprf = el.attrib.pop('t-valuef')
+ expr = self._compile_format(exprf)
+ else:
+ # set the content as value
+ def_name = self._make_name("qweb_t_set")
+ content = self._compile_directive_inner_content(el, options, indent + 1) + self._flushText(options, indent + 1, True)
+ if content:
+ code.append(self._indent(f"def {def_name}():", indent))
+ code.extend(content)
+ expr = f"Markup(''.join({def_name}()))"
+ else:
+ expr = "''"
+
+ code.append(self._indent(f"values[{repr(varname)}] = {expr}", indent))
+ return code
+
+ def _compile_directive_value(self, el, options, indent):
+ """Compile `t-value` expressions into a python code as a list of strings.
+
+ This method only check if this attributes is on the same node of a
+ `t-set` attribute.
+ """
+ if not options.get('t-set') is None:
+ raise ValueError("t-value must be on the same node of t-set")
+ return []
+
+ def _compile_directive_valuef(self, el, options, indent):
+ """Compile `t-valuef` expressions into a python code as a list of strings.
+
+ This method only check if this attributes is on the same node of a
+ `t-set` attribute.
+ """
+ if not options.get('t-set') is None:
+ raise ValueError("t-valuef must be on the same node of t-set")
+ return []
+
+ def _compile_directive_inner_content(self, el, options, indent):
+ """Compiles the content of the element (is the technical `t-inner-content`
+ directive created by QWeb) into a python code as a list of
+ strings.
+
+ The code will contains the text content of the node or the compliled
+ code from the recursive call of ``_compile_node``.
+ """
+ el.attrib.pop('t-inner-content', None)
+
+ if el.text is not None:
+ self._appendText(el.text, options)
+ body = []
+ if el.getchildren():
+ for item in el:
+ if isinstance(item, etree._Comment):
+ if options.get('preserve_comments'):
+ self._appendText("" % item.text, options)
+ else:
+ body.extend(self._compile_node(item, options, indent))
+ # comments can also contains tail text
+ if item.tail is not None:
+ self._appendText(item.tail, options)
+ return body
+
+ def _compile_directive_else(self, el, options, indent):
+ """Compile `t-else` expressions into a python code as a list of strings.
+
+ This method is linked with the `t-if` directive.
+ The code will contain the compiled code of the element (without `else`
+ python part).
+ """
+ if el.attrib.pop('t-else') == '_t_skip_else_':
+ return []
+ if not options.pop('t_if', None):
+ raise ValueError("t-else directive must be preceded by t-if or t-elif directive")
+ compiled = self._compile_directives(el, options, indent)
+ el.attrib['t-else'] = '_t_skip_else_'
+ return compiled
+
+ def _compile_directive_elif(self, el, options, indent):
+ """Compile `t-eif` expressions into a python code as a list of strings.
+
+ This method is linked with the `t-if` directive.
+ The code will contain the compiled code of the element (without `else`
+ python part).
+ """
+ _elif = el.attrib['t-elif']
+ if _elif == '_t_skip_else_':
+ el.attrib.pop('t-elif')
+ return []
+ if not options.pop('t_if', None):
+ raise ValueError("t-elif directive must be preceded by t-if or t-elif directive")
+ compiled = self._compile_directive_if(el, options, indent)
+ el.attrib['t-elif'] = '_t_skip_else_'
+ return compiled
+
+ def _compile_directive_if(self, el, options, indent):
+ """Compile `t-if` expressions into a python code as a list of strings.
+
+ The code will contain the condition `if`, `else` and `elif` part that
+ wrap the rest of the compiled code of this element.
+ """
+ if 't-elif' in el.attrib:
+ expr = el.attrib.pop('t-elif')
+ else:
+ expr = el.attrib.pop('t-if')
+
+ strip = self._stripText(options)
+
+ code = self._flushText(options, indent, True)
+ if el.tag.lower() != 't':
+ self._appendText(strip, options)
+ content_if = self._compile_directives(el, options, indent + 1) + self._flushText(options, indent + 1, True)
+
+ orelse = []
+ next_el = el.getnext()
+ comments_to_remove = []
+ while isinstance(next_el, etree._Comment):
+ comments_to_remove.append(next_el)
+ next_el = next_el.getnext()
+ if next_el is not None and {'t-else', 't-elif'} & set(next_el.attrib):
+ parent = el.getparent()
+ for comment in comments_to_remove:
+ parent.remove(comment)
+ if el.tail and not el.tail.isspace():
+ raise ValueError("Unexpected non-whitespace characters between t-if and t-else directives")
+ el.tail = None
+ if next_el.tag.lower() != 't':
+ self._appendText(strip, options)
+ orelse = self._compile_node(next_el, dict(options, t_if=True), indent + 1) + self._flushText(options, indent + 1, True)
+
+ code.append(self._indent(f"if {self._compile_expr(expr)}:", indent))
+ code.extend(content_if or [self._indent('pass', indent + 1)])
+ if orelse:
+ code.append(self._indent("else:", indent))
+ code.extend(orelse)
+ return code
def _compile_directive_groups(self, el, options, indent):
"""Compile `t-groups` expressions into a python code as a list of
@@ -155,6 +1156,302 @@ class IrQWeb(models.AbstractModel, QWeb):
code.extend(self._compile_directives(el, options, indent + 1) + self._flushText(options, indent + 1, True) or [self._indent('pass', indent + 1)])
return code
+ def _compile_directive_foreach(self, el, options, indent):
+ """Compile `t-foreach` expressions into a python code as a list of
+ strings.
+
+ `t-as` is used to define the key name.
+ `t-foreach` compiled value can be an iterable, an dictionary or a
+ number.
+
+ The code will contain loop `for` that wrap the rest of the compiled
+ code of this element.
+ Some key into values dictionary are create automatically:
+ *_size, *_index, *_value, *_first, *_last, *_odd, *_even, *_parity
+ """
+ expr_foreach = el.attrib.pop('t-foreach')
+ expr_as = el.attrib.pop('t-as')
+
+ strip = self._stripText(options)
+ if el.tag.lower() != 't':
+ el.text = strip+(el.text or '')
+
+ code = self._flushText(options, indent, True)
+ content_foreach = self._compile_directives(el, options, indent + 1) + self._flushText(options, indent + 1, True)
+
+ t_foreach = self._make_name('t_foreach')
+ size = self._make_name('size')
+ has_value = self._make_name('has_value')
+
+ if expr_foreach.isdigit():
+ code.append(self._indent(dedent(f"""
+ values[{repr(expr_as + '_size')}] = {size} = {int(expr_foreach)}
+ {t_foreach} = range({size})
+ {has_value} = False
+ """).strip(), indent))
+ else:
+ code.append(self._indent(dedent(f"""
+ {t_foreach} = {self._compile_expr(expr_foreach)} or []
+ if isinstance({t_foreach}, Sized):
+ values[{repr(expr_as + '_size')}] = {size} = len({t_foreach})
+ elif ({t_foreach}).__class__ == int:
+ values[{repr(expr_as + '_size')}] = {size} = {t_foreach}
+ {t_foreach} = range({size})
+ else:
+ {size} = None
+ {has_value} = False
+ if isinstance({t_foreach}, Mapping):
+ {t_foreach} = {t_foreach}.items()
+ {has_value} = True
+ """).strip(), indent))
+
+ code.append(self._indent(dedent(f"""
+ for index, item in enumerate({t_foreach}):
+ values[{repr(expr_as + '_index')}] = index
+ if {has_value}:
+ values[{repr(expr_as)}], values[{repr(expr_as + '_value')}] = item
+ else:
+ values[{repr(expr_as)}] = values[{repr(expr_as + '_value')}] = item
+ values[{repr(expr_as + '_first')}] = values[{repr(expr_as + '_index')}] == 0
+ if {size} is not None:
+ values[{repr(expr_as + '_last')}] = index + 1 == {size}
+ values[{repr(expr_as + '_odd')}] = index % 2
+ values[{repr(expr_as + '_even')}] = not values[{repr(expr_as + '_odd')}]
+ values[{repr(expr_as + '_parity')}] = 'odd' if values[{repr(expr_as + '_odd')}] else 'even'
+ """), indent))
+
+ code.append(self._indent(f'log["last_path_node"] = {repr(options["root"].getpath(el))} ', indent + 1))
+ code.extend(content_foreach or self._indent('continue', indent + 1))
+
+ return code
+
+ def _compile_directive_as(self, el, options, indent):
+ """Compile `t-as` expressions into a python code as a list of strings.
+
+ This method only check if this attributes is on the same node of a
+ `t-foreach` attribute.
+ """
+ if not options.get('t-foreach') is None:
+ raise ValueError("t-as must be on the same node of t-foreach")
+ return []
+
+ def _compile_directive_out(self, el, options, indent):
+ """Compile `t-out` expressions into a python code as a list of
+ strings.
+ The output can have some rendering option with `t-options-widget` or
+ `t-options={'widget': ...}. The compiled code will call ``_get_widget``
+ method at rendering time.
+ The code will contain evalution and rendering of the compiled value. If
+ the compiled value is None or False, the tag is not added to the render
+ (Except if the widget forces rendering or there is default content.).
+ """
+ ttype = 't-out'
+ expr = el.attrib.pop('t-out', None)
+ if expr is None:
+ # deprecated use.
+ ttype = 't-esc'
+ expr = el.attrib.pop('t-esc', None)
+ if expr is None:
+ ttype = 't-raw'
+ expr = el.attrib.pop('t-raw')
+
+ code = self._flushText(options, indent)
+ options['t_options_varname'] = 't_out_t_options'
+ code_options = self._compile_directive(el, options, 'options', indent)
+ code.extend(code_options)
+
+ if expr == "0":
+ if code_options:
+ code.append(self._indent("content = Markup(''.join(values.get('0', [])))", indent))
+ else:
+ code.extend(self._compile_tag_open(el, options, indent))
+ code.extend(self._flushText(options, indent))
+ code.append(self._indent("yield from values.get('0', [])", indent))
+ code.extend(self._compile_tag_close(el, options))
+ return code
+ else:
+ code.append(self._indent(f"content = {self._compile_expr(expr)}", indent))
+
+ if code_options:
+ code.append(self._indent(f"attrs, content, force_display = self._get_widget(content, {repr(expr)}, {repr(el.tag)}, t_out_t_options, compile_options, values)", indent))
+ else:
+ code.append(self._indent("force_display = None", indent))
+
+ if ttype == 't-raw':
+ # deprecated use.
+ code.append(self._indent(dedent("""
+ if content is not None and content is not False:
+ content = Markup(content)
+ """), indent))
+
+ code.extend(self._compile_widget_value(el, options, indent, without_attributes=not code_options))
+ return code
+
+ def _compile_directive_esc(self, el, options, indent):
+ # deprecated use.
+ if options.get('dev_mode'):
+ _logger.warning(
+ "Found deprecated directive @t-esc=%r in template %r. Replace by @t-out",
+ el.get('t-esc'),
+ options.get('ref', ''),
+ )
+ return self._compile_directive_out(el, options, indent)
+
+ def _compile_directive_raw(self, el, options, indent):
+ # deprecated use.
+ _logger.warning(
+ "Found deprecated directive @t-raw=%r in template %r. Replace by "
+ "@t-out, and explicitely wrap content in `Markup` if "
+ "necessary (which likely is not the case)",
+ el.get('t-raw'),
+ options.get('ref', ''),
+ )
+ return self._compile_directive_out(el, options, indent)
+
+ def _compile_directive_field(self, el, options, indent):
+ """Compile `t-field` expressions into a python code as a list of
+ strings.
+ The compiled code will call ``_get_field`` method at rendering time
+ using the type of value supplied by the field. This behavior can be
+ changed with `t-options-widget` or `t-options={'widget': ...}.
+ The code will contain evalution and rendering of the compiled value
+ value from the record field. If the compiled value is None or False,
+ the tag is not added to the render
+ (Except if the widget forces rendering or there is default content.).
+ """
+ tagName = el.tag
+ assert tagName not in ("table", "tbody", "thead", "tfoot", "tr", "td",
+ "li", "ul", "ol", "dl", "dt", "dd"),\
+ "RTE widgets do not work correctly on %r elements" % tagName
+ assert tagName != 't',\
+ "t-field can not be used on a t element, provide an actual HTML node"
+ assert "." in el.get('t-field'),\
+ "t-field must have at least a dot like 'record.field_name'"
+
+ expression = el.attrib.pop('t-field')
+ record, field_name = expression.rsplit('.', 1)
+
+ code = []
+ options['t_options_varname'] = 't_field_t_options'
+ code_options = self._compile_directive(el, options, 'options', indent) or [self._indent("t_field_t_options = {}", indent)]
+ code.extend(code_options)
+ code.append(self._indent(f"attrs, content, force_display = self._get_field({self._compile_expr(record, raise_on_missing=True)}, {repr(field_name)}, {repr(expression)}, {repr(tagName)}, t_field_t_options, compile_options, values)", indent))
+ code.append(self._indent("content = self._compile_to_str(content)", indent))
+ code.extend(self._compile_widget_value(el, options, indent))
+ return code
+
+ def _compile_widget_value(self, el, options, indent=0, without_attributes=False):
+ """Take care of part of the compilation of `t-out` and `t-field` (and
+ the technical directive `t-tag). This is the part that takes care of
+ whether or not created the tags and the default content of the element.
+ """
+ el.attrib.pop('t-tag', None)
+
+ code = self._flushText(options, indent)
+ code.append(self._indent("if content is not None and content is not False:", indent))
+ code.extend(self._compile_tag_open(el, options, indent + 1, not without_attributes))
+ code.extend(self._flushText(options, indent + 1))
+ # Use str to avoid the escaping of the other html content.
+ code.append(self._indent("yield str(escape(content))", indent + 1))
+ code.extend(self._compile_tag_close(el, options))
+ code.extend(self._flushText(options, indent + 1))
+
+ default_body = self._compile_directive_inner_content(el, options, indent + 1)
+ if default_body or options['_text_concat']:
+ # default content
+ _text_concat = list(options['_text_concat'])
+ options['_text_concat'].clear()
+ code.append(self._indent("else:", indent))
+ code.extend(self._compile_tag_open(el, options, indent + 1, not without_attributes))
+ code.extend(self._flushText(options, indent + 1))
+ code.extend(default_body)
+ options['_text_concat'].extend(_text_concat)
+ code.extend(self._compile_tag_close(el, options))
+ code.extend(self._flushText(options, indent + 1))
+ else:
+ content = (self._compile_tag_open(el, options, indent + 1, not without_attributes) +
+ self._compile_tag_close(el, options) +
+ self._flushText(options, indent + 2))
+ if content:
+ code.append(self._indent("elif force_display:", indent))
+ code.extend(content)
+
+ return code
+
+ def _compile_directive_call(self, el, options, indent):
+ """Compile `t-call` expressions into a python code as a list of
+ strings.
+
+ `t-call` allow formating string dynamic at rendering time.
+ Can use `t-options` used to call and render the sub-template at
+ rendering time.
+ The sub-template is called with a copy of the rendering values
+ dictionary. The dictionary contains the key 0 coming from the
+ compilation of the contents of this element
+
+ The code will contain the call of the template and a function from the
+ compilation of the content of this element.
+ """
+ expr = el.attrib.pop('t-call')
+
+ if el.attrib.get('t-call-options'): # retro-compatibility
+ el.attrib.set('t-options', el.attrib.pop('t-call-options'))
+
+ nsmap = options.get('nsmap')
+
+ code = self._flushText(options, indent, True)
+ options['t_options_varname'] = 't_call_t_options'
+ code_options = self._compile_directive(el, options, 'options', indent) or [self._indent("t_call_t_options = {}", indent)]
+ code.extend(code_options)
+
+ # content (t-out="0" and variables)
+ def_name = "t_call_content"
+ content = self._compile_directive_inner_content(el, options, indent + 1)
+ if content and not options['_text_concat']:
+ self._appendText('', options) # To ensure the template function is a generator and doesn't become a regular function
+ content.extend(self._flushText(options, indent + 1))
+ if content:
+ code.append(self._indent(f"def {def_name}(self, values, log):", indent))
+ code.extend(content)
+ code.append(self._indent("t_call_values = values.copy()", indent))
+ code.append(self._indent(f"t_call_values['0'] = Markup(''.join({def_name}(self, t_call_values, log)))", indent))
+ else:
+ code.append(self._indent("t_call_values = values.copy()", indent))
+ code.append(self._indent("t_call_values['0'] = Markup()", indent))
+
+ # options
+ code.append(self._indent(dedent(f"""
+ t_call_options = compile_options.copy()
+ t_call_options.update({{'caller_template': {repr(str(options.get('template')))}, 'last_path_node': {repr(str(options['root'].getpath(el)))} }})
+ """).strip(), indent))
+ if nsmap:
+ # update this dict with the current nsmap so that the callee know
+ # if he outputting the xmlns attributes is relevenat or not
+ nsmap = []
+ for key, value in options['nsmap'].items():
+ if isinstance(key, str):
+ nsmap.append(f'{repr(key)}:{repr(value)}')
+ else:
+ nsmap.append(f'None:{repr(value)}')
+ code.append(self._indent(f"t_call_options.update(nsmap={{{', '.join(nsmap)}}})", indent))
+
+ template = self._compile_format(expr)
+
+ # call
+ if code_options:
+ code.append(self._indent("t_call_options.update(t_call_t_options)", indent))
+ code.append(self._indent(dedent(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)
+ """).strip(), indent))
+ else:
+ code.append(self._indent(f"yield from self._compile({template}, t_call_options)(self, t_call_values)", indent))
+
+ return code
+
def _compile_directive_lang(self, el, options, indent):
el.attrib['t-options-lang'] = el.attrib.pop('t-lang')
return self._compile_node(el, options, indent)
@@ -165,14 +1462,7 @@ class IrQWeb(models.AbstractModel, QWeb):
raise SyntaxError("t-call-assets cannot contain children nodes")
code = self._flushText(options, indent)
- code.append(self._indent(dedent("""
- t_call_assets_nodes = self._get_asset_nodes(%(xmlid)s, css=%(css)s, js=%(js)s, debug=values.get("debug"), async_load=%(async_load)s, defer_load=%(defer_load)s, lazy_load=%(lazy_load)s, media=%(media)s)
- for index, (tagName, attrs, content) in enumerate(t_call_assets_nodes):
- if index:
- yield '\\n '
- yield '<'
- yield tagName
- """).strip() % {
+ code.append(self._indent("""t_call_assets_nodes = self._get_asset_nodes(%(xmlid)s, css=%(css)s, js=%(js)s, debug=values.get("debug"), async_load=%(async_load)s, defer_load=%(defer_load)s, lazy_load=%(lazy_load)s, media=%(media)s)""".strip() % {
'xmlid': repr(el.get('t-call-assets')),
'css': self._compile_bool(el.get('t-css', True)),
'js': self._compile_bool(el.get('t-js', True)),
@@ -181,6 +1471,14 @@ class IrQWeb(models.AbstractModel, QWeb):
'lazy_load': self._compile_bool(el.get('lazy_load', False)),
'media': repr(el.get('media')) if el.get('media') else False,
}, indent))
+
+ code.append(self._indent(dedent("""
+ for index, (tagName, attrs, content) in enumerate(t_call_assets_nodes):
+ if index:
+ yield '\\n '
+ yield '<'
+ yield tagName
+ """), indent))
code.extend(self._compile_attributes(options, indent + 1))
code.append(self._indent(dedent("""
if not content and tagName in self._void_elements:
@@ -196,10 +1494,85 @@ class IrQWeb(models.AbstractModel, QWeb):
return code
- # method called by computing code
+ # methods called by the compiled function at rendering time.
- def get_asset_bundle(self, bundle_name, files, env=None, css=True, js=True):
- return AssetsBundle(bundle_name, files, env=env, css=css, js=js)
+ def _debug_trace(self, debugger, options):
+ """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):
+ """ Method called at compile time for the static node and called at
+ runing time for the dynamic attributes.
+
+ This method may be overwrited to filter or modify the attributes
+ (during compilation for static node or after they compilation in
+ the case of dynamic elements).
+
+ @returns dict
+ """
+ return atts
+
+ def _get_field(self, record, field_name, expression, tagName, field_options, options, values):
+ """Method called at compile time to return the field value.
+
+ :returns: tuple:
+ * dict: attributes
+ * string or None: content
+ * boolean: force_display display the tag if the content and default_content are None
+ """
+ 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))
+ field_options['inherit_branding'] = inherit_branding
+ translate = options.get('edit_translations') and options.get('translatable') and field.translate
+ field_options['translate'] = translate
+
+ # field converter
+ model = 'ir.qweb.field.' + field_options['type']
+ converter = self.env[model] if model in self.env else self.env['ir.qweb.field']
+
+ # get content (the return values from fields are considered to be markup safe)
+ content = converter.record_to_html(record, field_name, field_options)
+ attributes = converter.attributes(record, field_name, field_options, values)
+
+ return (attributes, content, inherit_branding or translate)
+
+ def _get_widget(self, value, expression, tagName, field_options, options, values):
+ """Method called at compile time to return the widget value.
+
+ :returns: tuple:
+ * dict: attributes
+ * 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
+
+ # field converter
+ model = 'ir.qweb.field.' + field_options['type']
+ converter = self.env[model] if model in self.env else self.env['ir.qweb.field']
+
+ # get content (the return values from widget are considered to be markup safe)
+ content = converter.value_to_html(value, field_options)
+ attributes = {}
+ attributes['data-oe-type'] = field_options['type']
+ attributes['data-oe-expression'] = field_options['expression']
+
+ return (attributes, content, None)
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.
@@ -211,6 +1584,8 @@ class IrQWeb(models.AbstractModel, QWeb):
else:
return self._generate_asset_nodes_cache(bundle, css, js, debug, async_load, defer_load, lazy_load, media)
+ # other methods used for the asset bundles
+
@tools.conditional(
# in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
# by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
@@ -220,21 +1595,6 @@ class IrQWeb(models.AbstractModel, QWeb):
def _generate_asset_nodes_cache(self, bundle, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False, media=None):
return self._generate_asset_nodes(bundle, css, js, debug, async_load, defer_load, lazy_load, media)
- def _generate_asset_nodes(self, bundle, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False, media=None):
- nodeAttrs = None
- if css and media:
- nodeAttrs = {
- 'media': media,
- }
- files, remains = self._get_asset_content(bundle, nodeAttrs, defer_load=defer_load, lazy_load=lazy_load)
- asset = self.get_asset_bundle(bundle, files, env=self.env, css=css, js=js)
- remains = [node for node in remains if (css and node[0] == 'link') or (js and node[0] == 'script')]
- return remains + asset.to_node(css=css, js=js, debug=debug, async_load=async_load, defer_load=defer_load, lazy_load=lazy_load)
-
- def _get_asset_link_urls(self, bundle):
- asset_nodes = self._get_asset_nodes(bundle, js=False)
- return [node[1]['href'] for node in asset_nodes if node[0] == 'link']
-
@tools.ormcache_context('bundle', 'nodeAttrs and nodeAttrs.get("media")', 'defer_load', 'lazy_load', keys=("website_id", "lang"))
def _get_asset_content(self, bundle, nodeAttrs=None, defer_load=False, lazy_load=False):
asset_paths = self.env['ir.asset']._get_asset_paths(bundle=bundle, css=True, js=True)
@@ -279,83 +1639,23 @@ class IrQWeb(models.AbstractModel, QWeb):
return (files, remains)
- def _get_field(self, record, field_name, expression, tagName, field_options, options, values):
- field = record._fields[field_name]
+ def _get_asset_bundle(self, bundle_name, files, env=None, css=True, js=True):
+ return AssetsBundle(bundle_name, files, env=env, css=css, js=js)
- # 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))
- field_options['inherit_branding'] = inherit_branding
- translate = options.get('edit_translations') and options.get('translatable') and field.translate
- field_options['translate'] = translate
-
- # field converter
- model = 'ir.qweb.field.' + field_options['type']
- converter = self.env[model] if model in self.env else self.env['ir.qweb.field']
-
- # get content (the return values from fields are considered to be markup safe)
- content = converter.record_to_html(record, field_name, field_options)
- attributes = converter.attributes(record, field_name, field_options, values)
-
- return (attributes, content, inherit_branding or translate)
-
- def _get_widget(self, value, expression, tagName, field_options, options, values):
- # 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
-
- # field converter
- model = 'ir.qweb.field.' + field_options['type']
- converter = self.env[model] if model in self.env else self.env['ir.qweb.field']
-
- # get content (the return values from widget are considered to be markup safe)
- content = converter.value_to_html(value, field_options)
- attributes = {}
- attributes['data-oe-type'] = field_options['type']
- attributes['data-oe-expression'] = field_options['expression']
-
- return (attributes, content, None)
-
- def _prepare_values(self, values, options):
- """ Prepare the context that will be sent to the evaluated function.
-
- :param values: template values to be used for rendering
- :param options: frozen dict of compilation parameters.
- """
- check_values(values)
- values['true'] = True
- values['false'] = False
- if 'request' not in values:
- values['request'] = request
- return super()._prepare_values(values, options)
-
- def _compile_expr(self, expr, raise_on_missing=False):
- """ Compiles a purported Python expression to compiled code, verifies
- that it's safe (according to safe_eval's semantics) and alter its
- variable references to access values data instead
-
- :param expr: string
- """
- readable = io.BytesIO(expr.strip().encode('utf-8'))
- try:
- tokens = list(tokenize.tokenize(readable.readline))
- except tokenize.TokenError:
- raise ValueError(f"Cannot compile expression: {expr}")
-
- namespace_expr = self._compile_expr_tokens(tokens, self._allowed_keyword + list(self._available_objects.keys()), raise_on_missing=raise_on_missing)
- namespace_expr = f'({namespace_expr})'
-
- assert_valid_codeobj(_SAFE_QWEB_OPCODES, compile(namespace_expr, '<>', 'eval'), expr)
- return namespace_expr
+ def _generate_asset_nodes(self, bundle, css=True, js=True, debug=False, async_load=False, defer_load=False, lazy_load=False, media=None):
+ nodeAttrs = None
+ if css and media:
+ nodeAttrs = {
+ 'media': media,
+ }
+ files, remains = self._get_asset_content(bundle, nodeAttrs, defer_load=defer_load, lazy_load=lazy_load)
+ asset = self._get_asset_bundle(bundle, files, env=self.env, css=css, js=js)
+ remains = [node for node in remains if (css and node[0] == 'link') or (js and node[0] == 'script')]
+ return remains + asset.to_node(css=css, js=js, debug=debug, async_load=async_load, defer_load=defer_load, lazy_load=lazy_load)
+ def _get_asset_link_urls(self, bundle):
+ asset_nodes = self._get_asset_nodes(bundle, js=False)
+ return [node[1]['href'] for node in asset_nodes if node[0] == 'link']
def render(template_name, values, load, **options):
""" Rendering of a qweb template without database and outside the registry.
diff --git a/odoo/addons/base/models/qweb.py b/odoo/addons/base/models/qweb.py
deleted file mode 100644
index 7263e0d2482..00000000000
--- a/odoo/addons/base/models/qweb.py
+++ /dev/null
@@ -1,1369 +0,0 @@
-# -*- coding: utf-8 -*-
-import logging
-import os.path
-import re
-import traceback
-import builtins
-import token
-import tokenize
-import io
-
-from markupsafe import Markup, escape
-from collections.abc import Sized, Mapping
-from itertools import count, chain
-from textwrap import dedent, indent as _indent
-from lxml import etree
-from psycopg2.extensions import TransactionRollbackError
-
-from odoo.tools import pycompat, freehash
-
-_logger = logging.getLogger(__name__)
-
-# same list as for `--dev-mode`
-SUPPORTED_DEBUGGERS = ['pdb', 'ipdb', 'pudb', 'wdb']
-token.QWEB = token.NT_OFFSET - 1
-token.tok_name[token.QWEB] = 'QWEB'
-
-RE_RSTRIP = re.compile(r'\n[ \t]*$')
-
-
-####################################
-### qweb tools ###
-####################################
-
-class QWebCodeFound(Exception):
- """
- Exception raised when a qweb compilation encounter dynamic content if the
- option `raise_on_code` is True.
- """
-
-class QWebException(Exception):
- def __init__(self, message, qweb, options, error=None, template=None, path=None, code=None):
- self.error = error
- self.name = template
- self.code = code if options.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 = nodes[0]
- node[:] = []
- node.text = None
- self.html = etree.tostring(node, encoding='unicode')
- self.stack = traceback.format_exc()
-
- self.message = message
- if self.error is not None:
- self.message = "%s\n%s: %s" % (self.message, self.error.__class__.__name__, self.error)
- if self.name is not None:
- self.message = "%s\nTemplate: %s" % (self.message, self.name)
- if self.path is not None:
- self.message = "%s\nPath: %s" % (self.message, self.path)
- if self.html is not None:
- self.message = "%s\nNode: %s" % (self.message, self.html)
-
- super(QWebException, self).__init__(message)
-
- def __str__(self):
- message = "%s\n%s\n%s" % (self.error, self.stack, self.message)
- if self.code is not None:
- message = "%s\nCompiled code:\n%s" % (message, self.code)
- return message
-
- def __repr__(self):
- return str(self)
-
-class frozendict(dict):
- """ An implementation of an immutable dictionary. """
- def __delitem__(self, key):
- raise NotImplementedError("'__delitem__' not supported on frozendict")
- def __setitem__(self, key, val):
- raise NotImplementedError("'__setitem__' not supported on frozendict")
- def clear(self):
- raise NotImplementedError("'clear' not supported on frozendict")
- def pop(self, key, default=None):
- raise NotImplementedError("'pop' not supported on frozendict")
- def popitem(self):
- raise NotImplementedError("'popitem' not supported on frozendict")
- def setdefault(self, key, default=None):
- raise NotImplementedError("'setdefault' not supported on frozendict")
- def update(self, *args, **kwargs):
- raise NotImplementedError("'update' not supported on frozendict")
- def __hash__(self):
- return hash(frozenset((key, freehash(val)) for key, val in self.items()))
-
-unsafe_eval = eval
-
-_FORMAT_REGEX = re.compile(r'(?:#\{(.+?)\})|(?:\{\{(.+?)\}\})') # ( ruby-style )|( jinja-style )
-_VARNAME_REGEX = re.compile(r'\W')
-
-
-####################################
-### QWeb ###
-####################################
-
-
-class QWeb(object):
- __slots__ = ()
-
- _void_elements = frozenset([
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
- 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
- _name_gen = count()
- # _available_objects builtins is not security safe (it's dangerous), is overridden by ir_qweb to only expose the safe_eval builtins.
- _available_objects = {k: v for k, v in vars(builtins).items() if not k.startswith('_')}
- _allowed_keyword = ['False', 'None', 'True', 'and', 'as', 'elif', 'else', 'for', 'if', 'in', 'is', 'not', 'or']
-
- def _render(self, template, values=None, **options):
- """ render(template, values, **options)
-
- Render the template specified by the given name.
-
- :param template: template identifier, name or etree (see ``_get_template``)
- :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)
- * ``load`` (function) overrides the load method (returns: (template, ref))
-
- :returns: str as Markup
- :rtype: markupsafe.Markup
- """
- if values and 0 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, options)
- rendering = render_template(self, values or {})
- result = ''.join(rendering)
-
- return Markup(result)
-
- def _compile(self, template, options):
- """ Compile the given template into a rendering function (generator)::
-
- render(qweb, values)
-
- where ``qweb`` is a QWeb instance and ``values`` are the values to render.
- """
- if options is None:
- options = {}
-
- element, document, ref = self._get_template(template, options)
- if not ref:
- ref = element.get('t-name', str(document))
-
- # reference to get xml and etree (usually the template ID)
- options['ref'] = ref
- # str xml of the reference template used for compilation. Useful for debugging, dev mode and profiling.
- options['ref_xml'] = document if isinstance(document, str) else str(document, 'utf-8')
-
- _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
- # 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
-
- if not options.get('nsmap'):
- _options['nsmap'] = {}
-
- # generate code
-
- def_name = f"template_{ref}" if isinstance(ref, int) else "template"
-
- try:
- _options['_text_concat'] = []
- self._appendText("", _options) # To ensure the template function is a generator and doesn't become a regular function
- code_lines = ([f"def {def_name}(self, values, log):"] +
- self._compile_node(element, _options, 1) +
- self._flushText(_options, 1))
- except QWebException as e:
- raise e
- except QWebCodeFound as e:
- raise e
- except Exception as e:
- raise QWebException("Error when compiling xml template", self, options,
- error=e, template=template, path=_options.get('last_path_node'))
- try:
- code = '\n'.join(code_lines)
- except QWebException as e:
- raise e
- except Exception as e:
- code = '\n'.join(map(str, code_lines))
- raise QWebException("Error when compiling xml template", self, options,
- error=e, template=template, code=code)
-
- # compile code and defined default values
-
- try:
- # noinspection PyBroadException
- compiled = compile(code, f'<{def_name}>', 'exec')
- globals_dict = self._prepare_globals({}, options)
- globals_dict['__builtins__'] = globals_dict # So that unknown/unsafe builtins are never added.
- unsafe_eval(compiled, globals_dict)
- compiled_fn = globals_dict[def_name]
- except QWebException as e:
- raise e
- except Exception as e:
- raise QWebException("Error when compiling xml template", self, options,
- error=e, template=template, code=code)
-
- # return the wrapped function
-
- def render_template(self, values):
- try:
- log = {'last_path_node': None}
- values = self._prepare_values(values, options)
- yield from compiled_fn(self, values, log)
- except (QWebException, TransactionRollbackError) as e:
- raise e
- except Exception as e:
- raise QWebException("Error when render the template", self, options,
- error=e, template=template, path=log.get('last_path_node'), code=code)
-
- return render_template
-
- def _get_template(self, template, options):
- """ 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, name or etree
- :param options: used to compile the template (the dict available for
- the rendering is frozen)
- ``load`` (function) overrides the load method
- """
- ref = template
- if isinstance(template, etree._Element):
- element = template
- document = etree.tostring(template)
- return (element, document, template.get('t-name'))
- else:
- try:
- loaded = options.get('load', self._load)(template, options)
- if not loaded:
- raise ValueError("Can not load template '%s'" % template)
- document, ref = loaded
- except QWebException as e:
- raise e
- except Exception as e:
- template = options.get('caller_template', template)
- raise QWebException("load could not load template", self, options, e, template)
-
- if document is None:
- raise QWebException("Template not found", self, options, template=template)
-
- if isinstance(document, etree._Element):
- element = document
- document = etree.tostring(document, encoding='utf-8')
- elif not document.strip().startswith('<') and os.path.exists(document):
- element = etree.parse(document).getroot()
- else:
- element = etree.fromstring(document)
-
- for node in element:
- if node.get('t-name') == str(template):
- return (node, document, ref)
- return (element, document, ref)
-
- def _load(self, template, options):
- """ Load a given template and return a tuple ``(xml, ref)``` """
- return (template, None)
-
- # values for running time
-
- def _prepare_values(self, values, options):
- """ Prepare the context that will sent to the compiled and evaluated
- function.
-
- :param values: template values to be used for rendering
- :param options: frozen dict of compilation parameters.
- """
- return values
-
- def _prepare_globals(self, globals_dict, options):
- """ Prepare the global context that will sent to eval the qweb generated
- code.
-
- :param globals_dict: template global values use in compiled code
- :param options: frozen dict of compilation parameters.
- """
- globals_dict['Sized'] = Sized
- globals_dict['Mapping'] = Mapping
- globals_dict['Markup'] = Markup
- globals_dict['escape'] = escape
- globals_dict['compile_options'] = options
- globals_dict.update(self._available_objects)
- return globals_dict
-
- # compute helpers
-
- def _appendText(self, text, options):
- """ Add an item (converts to a string) to the list.
- This will be concatenated and added during a call to the
- `_flushText` method. This makes it possible to return only one
- yield containing all the parts."""
- options['_text_concat'].append(self._compile_to_str(text))
-
- def _stripText(self, options):
- text_concat = options['_text_concat']
- if not text_concat:
- return ''
- result = RE_RSTRIP.search(text_concat[-1])
- if not result:
- return ''
- return result.group(0)
-
- def _flushText(self, options, indent, rstrip=False):
- """Concatenate all the textual chunks added by the `_appendText`
- method into a single yield."""
- text_concat = options['_text_concat']
- if text_concat:
- if rstrip:
- text_concat[-1] = RE_RSTRIP.sub('', text_concat[-1])
- text = ''.join(text_concat)
- text_concat.clear()
- return [f"{' ' * indent}yield {repr(text)}"]
- else:
- return []
-
- def _indent(self, code, indent):
- """Indent the code to respect the python syntax."""
- return _indent(code, ' ' * indent)
-
- def _make_name(self, prefix='var'):
- """Generates a unique name."""
- return f"{prefix}_{next(self._name_gen)}"
-
- def _compile_node(self, el, options, indent):
- """ Compile the given element into python code.
-
- The t-* attributes (directives) will be converted to a python instruction. If there
- are no t-* attributes, the element will be considered static.
-
- Directives are compiled using the order provided by the
- ``_directives_eval_order`` method (an create the
- ``options['iter_directives']`` iterator).
- For compilation, the directives supported are those with a
- compilation method ``_compile_directive_*``
-
- :return: list of string
- """
- # if tag don't have qweb attributes don't use directives
- if self._is_static_node(el, options):
- return self._compile_static_node(el, options, indent)
-
- if options.get('raise_on_code'):
- raise QWebCodeFound()
-
- path = options['root'].getpath(el)
- if options['last_path_node'] != path:
- options['last_path_node'] = path
- body = [self._indent(f'log["last_path_node"] = {repr(path)}', indent)]
- else:
- body = []
-
- # create an iterator on directives to compile in order
- options['iter_directives'] = iter(self._directives_eval_order() + [None])
-
- el.set('t-tag', el.tag)
- if not ({'t-out', 't-esc', 't-raw', 't-field'} & set(el.attrib)):
- el.set('t-content', 'True')
-
- return body + self._compile_directives(el, options, indent)
-
- def _compile_directives(self, el, options, indent):
- """ Compile the given element, following the directives given in the
- iterator ``options['iter_directives']`` create by `_compile_node``
- method.
-
- :return: list of code lines
- """
-
- if self._is_static_node(el, options):
- el.attrib.pop('t-tag', None)
- el.attrib.pop('t-content', None)
- return self._compile_static_node(el, options, indent)
-
- # compile the first directive present on the element
- for directive in options['iter_directives']:
- if ('t-' + directive) in el.attrib:
- return self._compile_directive(el, options, directive, indent)
-
- return []
-
- def _compile_format(self, expr):
- """ Parses the provided format string and compiles it to a single
- expression python, uses string with format method.
- Use format is faster to concat string and values.
- """
- text = ''
- values = []
- base_idx = 0
- for m in _FORMAT_REGEX.finditer(expr):
- literal = expr[base_idx:m.start()]
- if literal:
- text += literal.replace('{', '{{').replace("}", "}}")
- text += '{}'
- values.append(f'self._compile_to_str({self._compile_expr(m.group(1) or m.group(2))})')
- base_idx = m.end()
- # string past last regex match
- literal = expr[base_idx:]
- if literal:
- text += literal.replace('{', '{{').replace("}", "}}")
-
- code = repr(text)
- if values:
- code += f'.format({", ".join(values)})'
- return code
-
- def _compile_expr_tokens(self, tokens, allowed_keys, argument_names=None, raise_on_missing=False):
- """ Transform the list of token coming into a python instruction in
- textual form by adding the namepaces for the dynamic values.
-
- Example: `5 + a + b.c` to be `5 + values.get('a') + values['b'].c`
- Unknown values are considered to be None, but using `values['b']`
- gives a clear error message in cases where there is an attribute for
- example (have a `KeyError: 'b'`, instead of `AttributeError: 'NoneType'
- object has no attribute 'c'`).
-
- @returns str
- """
- # Finds and extracts the current "scope"'s "allowed values": values
- # which should not be accessed through the environment's namespace:
- # * the local variables of a lambda should be accessed directly e.g.
- # lambda a: a + b should be compiled to lambda a: a + values['b'],
- # since a is local to the lambda it has to be accessed directly
- # but b needs to be accessed through the rendering environment
- # * similarly for a comprehensions [a + b for a in c] should be
- # compiledto [a + values.get('b') for a in values.get('c')]
- # to avoid the risk of confusion between nested lambdas / comprehensions,
- # this is currently performed independently at each level of brackets
- # nesting (hence the function being recursive).
- index = 0
- open_bracket_index = -1
- bracket_depth = 0
-
- argument_name = '_arg_%s__'
- argument_names = argument_names or []
-
- while index < len(tokens):
- t = tokens[index]
- if t.exact_type in [token.LPAR, token.LSQB, token.LBRACE]:
- bracket_depth += 1
- if t.exact_type in [token.RPAR, token.RSQB, token.RBRACE]:
- bracket_depth -= 1
- elif bracket_depth == 0 and t.exact_type == token.NAME:
- string = t.string
- if string == 'lambda': # lambda => allowed values for the current bracket depth
- i = index + 1
- while i < len(tokens):
- t = tokens[i]
- if t.exact_type == token.NAME:
- argument_names.append(t.string)
- elif t.exact_type == token.COMMA:
- pass
- elif t.exact_type == token.COLON:
- break
- elif t.exact_type == token.EQUAL:
- raise NotImplementedError('Lambda default values are not supported')
- else:
- raise NotImplementedError('This lambda code style is not implemented.')
- i += 1
- elif string == 'for': # list comprehensions => allowed values for the current bracket depth
- i = index + 1
- while len(tokens) > i:
- t = tokens[i]
- if t.exact_type == token.NAME:
- if t.string == 'in':
- break
- argument_names.append(t.string)
- elif t.exact_type in [token.COMMA, token.LPAR, token.RPAR]:
- pass
- else:
- raise NotImplementedError('This loop code style is not implemented.')
- i += 1
-
- index += 1
-
- # Use bracket to nest structures.
- # Recursively processes the "sub-scopes", and replace their content with
- # a compiled node. During this recursive call we add to the allowed
- # values the values provided by the list comprehension, lambda, etc.,
- # previously extracted.
- index = 0
- open_bracket_index = -1
- bracket_depth = 0
-
- while index < len(tokens):
- t = tokens[index]
- string = t.string
-
- if t.exact_type in [token.LPAR, token.LSQB, token.LBRACE]:
- if bracket_depth == 0:
- open_bracket_index = index
- bracket_depth += 1
- elif t.exact_type in [token.RPAR, token.RSQB, token.RBRACE]:
- bracket_depth -= 1
- if bracket_depth == 0:
- code = self._compile_expr_tokens(
- tokens[open_bracket_index + 1:index],
- list(allowed_keys),
- list(argument_names),
- raise_on_missing,
- )
- code = tokens[open_bracket_index].string + code + t.string
- tokens[open_bracket_index:index + 1] = [tokenize.TokenInfo(token.QWEB, code, tokens[open_bracket_index].start, t.end, '')]
- index = open_bracket_index
-
- index += 1
-
- # The keys will be namespaced by values if they are not allowed. In
- # order to have a clear keyError message, this will be replaced by
- # values['key'] for certain cases (for example if an attribute is called
- # key.attrib, or an index key[0] ...)
- code = []
- index = 0
- pos = tokens and tokens[0].start # to keep indent when use expr on multi line
- while index < len(tokens):
- t = tokens[index]
- string = t.string
-
- if t.start[0] != pos[0]:
- pos = (t.start[0], 0)
- space = t.start[1] - pos[1]
- if space:
- code.append(' ' * space)
- pos = t.start
-
- if t.exact_type == token.NAME:
- if string == 'lambda': # lambda => allowed values
- code.append('lambda ')
- index += 1
- while index < len(tokens):
- t = tokens[index]
- if t.exact_type == token.NAME and t.string in argument_names:
- code.append(argument_name % t.string)
- if t.exact_type in [token.COMMA, token.COLON]:
- code.append(t.string)
- if t.exact_type == token.COLON:
- break
- index += 1
- if t.end[0] != pos[0]:
- pos = (t.end[0], 0)
- else:
- pos = t.end
- elif string in argument_names:
- code.append(argument_name % t.string)
- elif string in allowed_keys:
- code.append(string)
- elif index + 1 < len(tokens) and tokens[index + 1].exact_type == token.EQUAL: # function kw
- code.append(string)
- elif index > 0 and tokens[index - 1] and tokens[index - 1].exact_type == token.DOT:
- code.append(string)
- elif raise_on_missing or index + 1 < len(tokens) and tokens[index + 1].exact_type in [token.DOT, token.LPAR, token.LSQB, 'qweb']:
- # Should have values['product'].price to raise an error when get
- # the 'product' value and not an 'NoneType' object has no
- # attribute 'price' error.
- code.append(f'values[{repr(string)}]')
- else:
- # not assignation allowed, only getter
- code.append(f'values.get({repr(string)})')
- elif t.type not in [tokenize.ENCODING, token.ENDMARKER, token.DEDENT]:
- code.append(string)
-
- if t.end[0] != pos[0]:
- pos = (t.end[0], 0)
- else:
- pos = t.end
-
- index += 1
-
- return ''.join(code)
-
- def _compile_expr(self, expr, raise_on_missing=False):
- """This method must be overridden by in order to compile the template."""
- raise NotImplementedError("Templates should use the ir.qweb compile method")
-
- def _compile_bool(self, attr, default=False):
- """Convert the statements as a boolean."""
- if attr:
- if attr is True:
- return True
- attr = attr.lower()
- if attr in ('false', '0'):
- return False
- elif attr in ('true', '1'):
- return True
- return bool(default)
-
- def _compile_to_str(self, expr):
- """ Generates a text value (an instance of text_type) from an arbitrary
- source.
- """
- return pycompat.to_text(expr)
-
- # order
-
- def _directives_eval_order(self):
- """ List all supported directives in the order in which they should be
- evaluated on a given element. For instance, a node bearing both
- ``foreach`` and ``if`` should see ``foreach`` executed before ``if`` aka
- .. code-block:: xml
-
- should be equivalent to
- .. code-block:: xml
-
-
-
- then this method should return ``['foreach', 'if']``.
- """
- return [
- 'debug',
- 'foreach',
- 'if', 'elif', 'else',
- 'field', 'esc', 'raw', 'out',
- 'tag',
- 'call',
- 'set',
- 'content',
- ]
-
- def _is_static_node(self, el, options):
- """ Test whether the given element is purely static, i.e. (there
- are no t-* attributes), does not require dynamic rendering for its
- attributes.
- """
- return el.tag != 't' and not any(att.startswith('t-') and att not in ['t-tag', 't-content'] for att in el.attrib)
-
- # compile
-
- def _compile_static_node(self, el, options, indent):
- """ 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)
- else:
- # Etree will remove the ns prefixes indirection by inlining the corresponding
- # nsmap definition into the tag attribute. Restore the tag and prefix here.
- unqualified_el_tag = etree.QName(el.tag).localname
- el_tag = unqualified_el_tag
- if el.prefix:
- el_tag = f'{el.prefix}:{el_tag}'
-
- attrib = {}
- # If `el` introduced new namespaces, write them as attribute by using the
- # `attrib` dict.
- for ns_prefix, ns_definition in set(el.nsmap.items()) - set(options['nsmap'].items()):
- if ns_prefix is None:
- attrib['xmlns'] = ns_definition
- else:
- attrib[f'xmlns:{ns_prefix}'] = ns_definition
-
- # Etree will also remove the ns prefixes indirection in the attributes. As we only have
- # the namespace definition, we'll use an nsmap where the keys are the definitions and
- # the values the prefixes in order to get back the right prefix and restore it.
- ns = chain(options['nsmap'].items(), el.nsmap.items())
- nsprefixmap = {v: k for k, v in ns}
- for key, value in el.attrib.items():
- attrib_qname = etree.QName(key)
- if attrib_qname.namespace:
- attrib[f'{nsprefixmap[attrib_qname.namespace]}:{attrib_qname.localname}'] = value
- else:
- attrib[key] = value
-
- attrib = self._post_processing_att(el.tag, attrib, options)
-
- # Update the dict of inherited namespaces before continuing the recursion. Note:
- # since `options['nsmap']` is a dict (and therefore mutable) and we do **not**
- # want changes done in deeper recursion to bevisible in earlier ones, we'll pass
- # a copy before continuing the recursion and restore the original afterwards.
- original_nsmap = dict(options['nsmap'])
-
- if unqualified_el_tag != 't':
- attributes = ''.join(f' {str(name)}="{str(escape(str(value)))}"'
- for name, value in attrib.items() if value or isinstance(value, str))
- self._appendText(f'<{el_tag}{attributes}', options)
- if unqualified_el_tag in self._void_elements:
- self._appendText('/>', options)
- else:
- self._appendText('>', options)
-
- if el.nsmap:
- options['nsmap'].update(el.nsmap)
- body = self._compile_directive_content(el, options, indent)
- options['nsmap'] = original_nsmap
- else:
- body = self._compile_directive_content(el, options, indent)
-
- if unqualified_el_tag != 't':
- if unqualified_el_tag not in self._void_elements:
- self._appendText(f'{el_tag}>', options)
-
- return body
-
- def _compile_attributes(self, options, indent):
- """Generates the part of the code that post-process the attributes
- (this is ``attrs`` in the compiled code) during rendering time.
- """
- # Use str(value) to change Markup into str and escape it, then use str
- # to avoid the escaping of the other html content.
- body = self._flushText(options, indent)
- body.append(self._indent(dedent("""
- attrs = self._post_processing_att(tagName, attrs, compile_options)
- for name, value in attrs.items():
- if value or isinstance(value, str):
- yield f' {str(escape(str(name)))}="{str(escape(str(value)))}"'
- """).strip(), indent))
- return body
-
- def _compile_static_attributes(self, el, options, indent):
- """ Compile the static and dynamc attributes of the given element.
-
- We do not support namespaced dynamic attributes.
- """
- # Etree will also remove the ns prefixes indirection in the attributes. As we only have
- # the namespace definition, we'll use an nsmap where the keys are the definitions and
- # the values the prefixes in order to get back the right prefix and restore it.
- nsprefixmap = {v: k for k, v in chain(options['nsmap'].items(), el.nsmap.items())}
-
- code = []
- for key, value in el.attrib.items():
- if not key.startswith('t-'):
- attrib_qname = etree.QName(key)
- if attrib_qname.namespace:
- key = f'{nsprefixmap[attrib_qname.namespace]}:{attrib_qname.localname}'
- code.append(self._indent(f'attrs[{repr(key)}] = {repr(value)}', indent))
- return code
-
- def _compile_dynamic_attributes(self, el, options, indent):
- """ Compile the dynamic attributes of the given element into a list
- string (this is adding elements to ``attrs`` in the compiled code).
-
- We do not support namespaced dynamic attributes.
- """
- code = []
- for name, value in el.attrib.items():
-
- if name.startswith('t-attf-'):
- code.append(self._indent(f"attrs[{repr(name[7:])}] = {self._compile_format(value)}", indent))
- elif name.startswith('t-att-'):
- code.append(self._indent(f"attrs[{repr(name[6:])}] = {self._compile_expr(value)}", indent))
- elif name == 't-att':
- code.append(self._indent(dedent(f"""
- atts_value = {self._compile_expr(value)}
- if isinstance(atts_value, dict):
- attrs.update(atts_value)
- elif isinstance(atts_value, (list, tuple)) and not isinstance(atts_value[0], (list, tuple)):
- attrs.update([atts_value])
- elif isinstance(atts_value, (list, tuple)):
- attrs.update(dict(atts_value))
- """), indent))
- return code
-
- def _compile_all_attributes(self, el, options, indent, attr_already_created=False):
- """ Compile the attributes (static and dynamic) of the given elements
- into a list of str.
- (this compiled The code will create the ``attrs`` in the compiled code).
- """
- code = []
- if any(name.startswith('t-att') or not name.startswith('t-') for name, value in el.attrib.items()):
- if not attr_already_created:
- attr_already_created = True
- code.append(self._indent("attrs = {}", indent))
- code.extend(self._compile_static_attributes(el, options, indent))
- code.extend(self._compile_dynamic_attributes(el, options, indent))
- if attr_already_created:
- code.append(self._indent(f"tagName = {repr(el.tag)}", indent))
- code.extend(self._compile_attributes(options, indent))
- return code
-
- def _compile_tag_open(self, el, options, indent, attr_already_created=False):
- """ Compile the opening tag of the given element into a list of string. """
- extra_attrib = {}
- if not el.nsmap:
- unqualified_el_tag = el_tag = el.tag
- else:
- # Etree will remove the ns prefixes indirection by inlining the corresponding
- # nsmap definition into the tag attribute. Restore the tag and prefix here.
- # Note: we do not support namespace dynamic attributes, we need a default URI
- # on the root and use attribute directive t-att="{'xmlns:example': value}".
- unqualified_el_tag = etree.QName(el.tag).localname
- el_tag = unqualified_el_tag
- if el.prefix:
- el_tag = f'{el.prefix}:{el_tag}'
-
- # If `el` introduced new namespaces, write them as attribute by using the
- # `extra_attrib` dict.
- for ns_prefix, ns_definition in set(el.nsmap.items()) - set(options['nsmap'].items()):
- if ns_prefix is None:
- extra_attrib['xmlns'] = ns_definition
- else:
- extra_attrib[f'xmlns:{ns_prefix}'] = ns_definition
-
- code = []
- if unqualified_el_tag != 't':
- attributes = ''.join(f' {str(name)}="{str(escape(self._compile_to_str(value)))}"'
- for name, value in extra_attrib.items())
- self._appendText("<{}{}".format(el_tag, attributes), options)
- code.extend(self._compile_all_attributes(el, options, indent, attr_already_created))
- if unqualified_el_tag in self._void_elements:
- self._appendText('/>', options)
- else:
- self._appendText('>', options)
-
- return code
-
- def _compile_tag_close(self, el, options):
- """ Compile the closing tag of the given element into a list of string. """
- if not el.nsmap:
- unqualified_el_tag = el_tag = el.tag
- else:
- unqualified_el_tag = etree.QName(el.tag).localname
- el_tag = unqualified_el_tag
- if el.prefix:
- el_tag = f'{el.prefix}:{el_tag}'
-
- if unqualified_el_tag != 't' and el_tag not in self._void_elements:
- self._appendText(f'{el_tag}>', options)
- return []
-
- # compile directives
-
- def _compile_directive(self, el, options, directive, indent):
- compile_handler = getattr(self, f"_compile_directive_{directive.replace('-', '_')}", None)
- return compile_handler(el, options, indent)
-
- def _compile_directive_debug(self, el, options, indent):
- """Compile `t-debug` expressions into a python code as a list of
- strings.
-
- The code will contains the call to the debugger chosen from the valid
- list.
- """
- debugger = el.attrib.pop('t-debug')
- code = []
- if options.get('dev_mode'):
- code.append(self._indent(f"self._debug_trace({repr(debugger)}, compile_options)", indent))
- else:
- _logger.warning("@t-debug in template is only available in qweb dev mode options")
- code.extend(self._compile_directives(el, options, indent))
- return code
-
- def _compile_directive_options(self, el, options, indent):
- """
- compile t-options and add to the dict the t-options-xxx values
- """
- varname = options.get('t_options_varname', 't_options')
- code = []
- dict_arg = []
- for key in list(el.attrib):
- if key.startswith('t-options-'):
- value = el.attrib.pop(key)
- option_name = key[10:]
- dict_arg.append(f'{repr(option_name)}:{self._compile_expr(value)}')
-
- t_options = el.attrib.pop('t-options', None)
- if t_options and dict_arg:
- code.append(self._indent(f"{varname} = {{**{self._compile_expr(t_options)}, {', '.join(dict_arg)}}}", indent))
- elif dict_arg:
- code.append(self._indent(f"{varname} = {{{', '.join(dict_arg)}}}", indent))
- elif t_options:
- code.append(self._indent(f"{varname} = {self._compile_expr(t_options)}", indent))
-
- return code
-
- def _compile_directive_tag(self, el, options, indent):
- """Compile the element tag into a python code as a list of strings.
-
- The code will contains the opening tag, namespace, static and dynamic
- attributes and closing tag.
- """
- el.attrib.pop('t-tag', None)
-
- code = self._compile_tag_open(el, options, indent, False)
-
- # Update the dict of inherited namespaces before continuing the recursion. Note:
- # since `options['nsmap']` is a dict (and therefore mutable) and we do **not**
- # want changes done in deeper recursion to bevisible in earlier ones, we'll pass
- # a copy before continuing the recursion and restore the original afterwards.
- if el.nsmap:
- code.extend(self._compile_directives(el, dict(options, nsmap=el.nsmap), indent))
- else:
- code.extend(self._compile_directives(el, options, indent))
-
- code.extend(self._compile_tag_close(el, options))
-
- return code
-
- def _compile_directive_set(self, el, options, indent):
- """Compile `t-set` expressions into a python code as a list of
- strings.
-
- There are 3 kinds of `t-set`:
- * `t-value` containing python code;
- * `t-valuef` containing strings to format;
- * whose value is the content of the tag (being Markup safe).
-
- The code will contain the assignment of the dynamically generated value.
- """
- varname = el.attrib.pop('t-set')
- code = self._flushText(options, indent, True)
-
- if 't-value' in el.attrib:
- if varname == '0':
- raise ValueError('t-set="0" should not contains t-value or t-valuef')
- expr = el.attrib.pop('t-value') or 'None'
- expr = self._compile_expr(expr)
- elif 't-valuef' in el.attrib:
- if varname == '0':
- raise ValueError('t-set="0" should not contains t-value or t-valuef')
- exprf = el.attrib.pop('t-valuef')
- expr = self._compile_format(exprf)
- else:
- # set the content as value
- def_name = f"qweb_t_set_{re.sub(_VARNAME_REGEX, '_', options['last_path_node'])}"
- content = self._compile_directive_content(el, options, indent + 1) + self._flushText(options, indent + 1, True)
- if content:
- code.append(self._indent(f"def {def_name}():", indent))
- code.extend(content)
- expr = f"Markup(''.join({def_name}()))"
- else:
- expr = "''"
-
- code.append(self._indent(f"values[{repr(varname)}] = {expr}", indent))
- return code
-
- def _compile_directive_content(self, el, options, indent):
- """Compiles the content of the element (is the technical `t-content`
- directive created by QWeb) into a python code as a list of
- strings.
-
- The code will contains the text content of the node or the compliled
- code from the recursive call of ``_compile_node``.
- """
- if el.text is not None:
- self._appendText(el.text, options)
- body = []
- if el.getchildren():
- for item in el:
- if isinstance(item, etree._Comment):
- if options.get('preserve_comments'):
- self._appendText("" % item.text, options)
- else:
- body.extend(self._compile_node(item, options, indent))
- # comments can also contains tail text
- if item.tail is not None:
- self._appendText(item.tail, options)
- return body
-
- def _compile_directive_else(self, el, options, indent):
- """Compile `t-else` expressions into a python code as a list of strings.
-
- This method is linked with the `t-if` directive.
- The code will contain the compiled code of the element (without `else`
- python part).
- """
- if el.attrib.pop('t-else') == '_t_skip_else_':
- return []
- if not options.pop('t_if', None):
- raise ValueError("t-else directive must be preceded by t-if directive")
- compiled = self._compile_directives(el, options, indent)
- el.attrib['t-else'] = '_t_skip_else_'
- return compiled
-
- def _compile_directive_elif(self, el, options, indent):
- """Compile `t-eif` expressions into a python code as a list of strings.
-
- This method is linked with the `t-if` directive.
- The code will contain the compiled code of the element (without `else`
- python part).
- """
- _elif = el.attrib['t-elif']
- if _elif == '_t_skip_else_':
- el.attrib.pop('t-elif')
- return []
- if not options.pop('t_if', None):
- raise ValueError("t-elif directive must be preceded by t-if directive")
- compiled = self._compile_directive_if(el, options, indent)
- el.attrib['t-elif'] = '_t_skip_else_'
- return compiled
-
- def _compile_directive_if(self, el, options, indent):
- """Compile `t-if` expressions into a python code as a list of strings.
-
- The code will contain the condition `if`, `else` and `elif` part that
- wrap the rest of the compiled code of this element.
- """
- if 't-elif' in el.attrib:
- expr = el.attrib.pop('t-elif')
- else:
- expr = el.attrib.pop('t-if')
-
- strip = self._stripText(options)
-
- code = self._flushText(options, indent, True)
- if el.tag.lower() != 't':
- self._appendText(strip, options)
- content_if = self._compile_directives(el, options, indent + 1) + self._flushText(options, indent + 1, True)
-
- orelse = []
- next_el = el.getnext()
- comments_to_remove = []
- while isinstance(next_el, etree._Comment):
- comments_to_remove.append(next_el)
- next_el = next_el.getnext()
- if next_el is not None and {'t-else', 't-elif'} & set(next_el.attrib):
- parent = el.getparent()
- for comment in comments_to_remove:
- parent.remove(comment)
- if el.tail and not el.tail.isspace():
- raise ValueError("Unexpected non-whitespace characters between t-if and t-else directives")
- el.tail = None
- if next_el.tag.lower() != 't':
- self._appendText(strip, options)
- orelse = self._compile_node(next_el, dict(options, t_if=True), indent + 1) + self._flushText(options, indent + 1, True)
-
- code.append(self._indent(f"if {self._compile_expr(expr)}:", indent))
- code.extend(content_if or [self._indent('pass', indent + 1)])
- if orelse:
- code.append(self._indent("else:", indent))
- code.extend(orelse)
- return code
-
- def _compile_directive_foreach(self, el, options, indent):
- """Compile `t-foreach` expressions into a python code as a list of
- strings.
-
- `t-as` is used to define the key name.
- `t-foreach` compiled value can be an iterable, an dictionary or a
- number.
-
- The code will contain loop `for` that wrap the rest of the compiled
- code of this element.
- Some key into values dictionary are create automatically:
- *_size, *_index, *_value, *_first, *_last, *_odd, *_even, *_parity
- """
- expr_foreach = el.attrib.pop('t-foreach')
- expr_as = el.attrib.pop('t-as')
-
- strip = self._stripText(options)
- if el.tag.lower() != 't':
- el.text = strip+(el.text or '')
-
- code = self._flushText(options, indent, True)
- content_foreach = self._compile_directives(el, options, indent + 1) + self._flushText(options, indent + 1, True)
-
- t_foreach = self._make_name('t_foreach')
- size = self._make_name('size')
- has_value = self._make_name('has_value')
-
- if expr_foreach.isdigit():
- code.append(self._indent(dedent(f"""
- values[{repr(expr_as + '_size')}] = {size} = {int(expr_foreach)}
- {t_foreach} = range({size})
- {has_value} = False
- """).strip(), indent))
- else:
- code.append(self._indent(dedent(f"""
- {t_foreach} = {self._compile_expr(expr_foreach)} or []
- if isinstance({t_foreach}, Sized):
- values[{repr(expr_as + '_size')}] = {size} = len({t_foreach})
- elif ({t_foreach}).__class__ == int:
- values[{repr(expr_as + '_size')}] = {size} = {t_foreach}
- {t_foreach} = range({size})
- else:
- {size} = None
- {has_value} = False
- if isinstance({t_foreach}, Mapping):
- {t_foreach} = {t_foreach}.items()
- {has_value} = True
- """).strip(), indent))
-
- code.append(self._indent(dedent(f"""
- for index, item in enumerate({t_foreach}):
- values[{repr(expr_as + '_index')}] = index
- if {has_value}:
- values[{repr(expr_as)}], values[{repr(expr_as + '_value')}] = item
- else:
- values[{repr(expr_as)}] = values[{repr(expr_as + '_value')}] = item
- values[{repr(expr_as + '_first')}] = values[{repr(expr_as + '_index')}] == 0
- if {size} is not None:
- values[{repr(expr_as + '_last')}] = index + 1 == {size}
- values[{repr(expr_as + '_odd')}] = index % 2
- values[{repr(expr_as + '_even')}] = not values[{repr(expr_as + '_odd')}]
- values[{repr(expr_as + '_parity')}] = 'odd' if values[{repr(expr_as + '_odd')}] else 'even'
- """), indent))
-
- code.append(self._indent(f'log["last_path_node"] = {repr(options["root"].getpath(el))} ', indent + 1))
- code.extend(content_foreach or self._indent('continue', indent + 1))
-
- return code
-
- def _compile_directive_out(self, el, options, indent):
- """Compile `t-out` expressions into a python code as a list of
- strings.
-
- The output can have some rendering option with `t-options-widget` or
- `t-options={'widget': ...}. The compiled code will call ``_get_widget``
- method at rendering time.
-
- The code will contain evalution and rendering of the compiled value. If
- the compiled value is None or False, the tag is not added to the render
- (Except if the widget forces rendering or there is default content.).
- """
- ttype = 't-out'
- expr = el.attrib.pop('t-out', None)
- if expr is None:
- # deprecated use.
- ttype = 't-esc'
- expr = el.attrib.pop('t-esc', None)
- if expr is None:
- ttype = 't-raw'
- expr = el.attrib.pop('t-raw')
-
- code = self._flushText(options, indent)
- options['t_options_varname'] = 't_out_t_options'
- code_options = self._compile_directive(el, options, 'options', indent)
- code.extend(code_options)
-
- if expr == "0":
- if code_options:
- code.append(self._indent("content = Markup(''.join(values.get('0', [])))", indent))
- else:
- code.extend(self._compile_tag_open(el, options, indent))
- code.extend(self._flushText(options, indent))
- code.append(self._indent("yield from values.get('0', [])", indent))
- code.extend(self._compile_tag_close(el, options))
- return code
- else:
- code.append(self._indent(f"content = {self._compile_expr(expr)}", indent))
-
- if code_options:
- code.append(self._indent(f"attrs, content, force_display = self._get_widget(content, {repr(expr)}, {repr(el.tag)}, t_out_t_options, compile_options, values)", indent))
- else:
- code.append(self._indent("force_display = None", indent))
-
- if ttype == 't-raw':
- # deprecated use.
- code.append(self._indent(dedent("""
- if content is not None and content is not False:
- content = Markup(content)
- """), indent))
-
- code.extend(self._compile_widget_value(el, options, indent, without_attributes=not code_options))
- return code
-
- def _compile_directive_esc(self, el, options, indent):
- # deprecated use.
- if options.get('dev_mode'):
- _logger.warning(
- "Found deprecated directive @t-esc=%r in template %r. Replace by @t-out",
- el.get('t-esc'),
- options.get('ref', ''),
- )
- return self._compile_directive_out(el, options, indent)
-
- def _compile_directive_raw(self, el, options, indent):
- # deprecated use.
- _logger.warning(
- "Found deprecated directive @t-raw=%r in template %r. Replace by "
- "@t-out, and explicitely wrap content in `Markup` if "
- "necessary (which likely is not the case)",
- el.get('t-raw'),
- options.get('ref', ''),
- )
- return self._compile_directive_out(el, options, indent)
-
- def _compile_directive_field(self, el, options, indent):
- """Compile `t-field` expressions into a python code as a list of
- strings.
-
- The compiled code will call ``_get_field`` method at rendering time
- using the type of value supplied by the field. This behavior can be
- changed with `t-options-widget` or `t-options={'widget': ...}.
-
- The code will contain evalution and rendering of the compiled value
- value from the record field. If the compiled value is None or False,
- the tag is not added to the render
- (Except if the widget forces rendering or there is default content.).
- """
- tagName = el.tag
- assert tagName not in ("table", "tbody", "thead", "tfoot", "tr", "td",
- "li", "ul", "ol", "dl", "dt", "dd"),\
- "RTE widgets do not work correctly on %r elements" % tagName
- assert tagName != 't',\
- "t-field can not be used on a t element, provide an actual HTML node"
- assert "." in el.get('t-field'),\
- "t-field must have at least a dot like 'record.field_name'"
-
- expression = el.attrib.pop('t-field')
- record, field_name = expression.rsplit('.', 1)
-
- code = []
- options['t_options_varname'] = 't_field_t_options'
- code_options = self._compile_directive(el, options, 'options', indent) or [self._indent("t_field_t_options = {}", indent)]
- code.extend(code_options)
- code.append(self._indent(f"attrs, content, force_display = self._get_field({self._compile_expr(record, raise_on_missing=True)}, {repr(field_name)}, {repr(expression)}, {repr(tagName)}, t_field_t_options, compile_options, values)", indent))
- code.append(self._indent("content = self._compile_to_str(content)", indent))
- code.extend(self._compile_widget_value(el, options, indent))
- return code
-
- def _compile_widget_value(self, el, options, indent=0, without_attributes=False):
- """Take care of part of the compilation of `t-out` and `t-field` (and
- the technical directive `t-tag). This is the part that takes care of
- whether or not created the tags and the default content of the element.
- """
- el.attrib.pop('t-tag', None)
-
- code = self._flushText(options, indent)
- code.append(self._indent("if content is not None and content is not False:", indent))
- code.extend(self._compile_tag_open(el, options, indent + 1, not without_attributes))
- code.extend(self._flushText(options, indent + 1))
- # Use str to avoid the escaping of the other html content.
- code.append(self._indent("yield str(escape(content))", indent + 1))
- code.extend(self._compile_tag_close(el, options))
- code.extend(self._flushText(options, indent + 1))
-
- default_body = self._compile_directive_content(el, options, indent + 1)
- if default_body or options['_text_concat']:
- # default content
- _text_concat = list(options['_text_concat'])
- options['_text_concat'].clear()
- code.append(self._indent("else:", indent))
- code.extend(self._compile_tag_open(el, options, indent + 1, not without_attributes))
- code.extend(self._flushText(options, indent + 1))
- code.extend(default_body)
- options['_text_concat'].extend(_text_concat)
- code.extend(self._compile_tag_close(el, options))
- code.extend(self._flushText(options, indent + 1))
- else:
- content = (self._compile_tag_open(el, options, indent + 1, not without_attributes) +
- self._compile_tag_close(el, options) +
- self._flushText(options, indent + 2))
- if content:
- code.append(self._indent("elif force_display:", indent))
- code.extend(content)
-
- return code
-
- def _compile_directive_call(self, el, options, indent):
- """Compile `t-call` expressions into a python code as a list of
- strings.
-
- `t-call` allow formating string dynamic at rendering time.
- Can use `t-options` used to call and render the sub-template at
- rendering time.
- The sub-template is called with a copy of the rendering values
- dictionary. The dictionary contains the key 0 coming from the
- compilation of the contents of this element
-
- The code will contain the call of the template and a function from the
- compilation of the content of this element.
- """
- expr = el.attrib.pop('t-call')
-
- if el.attrib.get('t-call-options'): # retro-compatibility
- el.attrib.set('t-options', el.attrib.pop('t-call-options'))
-
- nsmap = options.get('nsmap')
-
- code = self._flushText(options, indent, True)
- options['t_options_varname'] = 't_call_t_options'
- code_options = self._compile_directive(el, options, 'options', indent) or [self._indent("t_call_t_options = {}", indent)]
- code.extend(code_options)
-
- # content (t-out="0" and variables)
- def_name = "t_call_content"
- content = self._compile_directive_content(el, options, indent + 1)
- if content and not options['_text_concat']:
- self._appendText('', options) # To ensure the template function is a generator and doesn't become a regular function
- content.extend(self._flushText(options, indent + 1))
- if content:
- code.append(self._indent(f"def {def_name}(self, values, log):", indent))
- code.extend(content)
- code.append(self._indent("t_call_values = values.copy()", indent))
- code.append(self._indent(f"t_call_values['0'] = Markup(''.join({def_name}(self, t_call_values, log)))", indent))
- else:
- code.append(self._indent("t_call_values = values.copy()", indent))
- code.append(self._indent("t_call_values['0'] = Markup()", indent))
-
- # options
- code.append(self._indent(dedent(f"""
- t_call_options = compile_options.copy()
- t_call_options.update({{'caller_template': {repr(str(options.get('template')))}, 'last_path_node': {repr(str(options['root'].getpath(el)))} }})
- """).strip(), indent))
- if nsmap:
- # update this dict with the current nsmap so that the callee know
- # if he outputting the xmlns attributes is relevenat or not
- nsmap = []
- for key, value in options['nsmap'].items():
- if isinstance(key, str):
- nsmap.append(f'{repr(key)}:{repr(value)}')
- else:
- nsmap.append(f'None:{repr(value)}')
- code.append(self._indent(f"t_call_options.update(nsmap={{{', '.join(nsmap)}}})", indent))
-
- template = self._compile_format(expr)
-
- # call
- if code_options:
- code.append(self._indent("t_call_options.update(t_call_t_options)", indent))
- code.append(self._indent(dedent(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)
- """).strip(), indent))
- else:
- code.append(self._indent(f"yield from self._compile({template}, t_call_options)(self, t_call_values)", indent))
-
- return code
-
- # method called by computing code
-
- def _post_processing_att(self, tagName, atts, options):
- """ Method called at compile time for the static node and called at
- runing time for the dynamic attributes.
-
- This method may be overwrited to filter or modify the attributes
- (during compilation for static node or after they compilation in
- the case of dynamic elements).
-
- @returns dict
- """
- return atts
-
- def _get_field(self, record, field_name, expression, tagName, field_options, options, values):
- """Method called at compile time to return the field value.
-
- :returns: tuple:
- * dict: attributes
- * string or None: content
- * boolean: force_display display the tag if the content and default_content are None
- """
- return self._get_widget(getattr(record, field_name, None), expression, tagName, field_options, options, values)
-
- def _get_widget(self, value, expression, tagName, field_options, options, values):
- """Method called at compile time to return the widget value.
-
- :returns: tuple:
- * dict: attributes
- * string or None: content
- * boolean: force_display display the tag if the content and default_content are None
- """
- return ({}, value, False)
-
- def _debug_trace(self, debugger, options):
- """Method called at compile time to load debugger."""
- if debugger in SUPPORTED_DEBUGGERS:
- __import__(debugger).set_trace()
- else:
- raise QWebException(f"unsupported t-debug value: {debugger}", self, options)
diff --git a/odoo/addons/base/tests/test_qweb.py b/odoo/addons/base/tests/test_qweb.py
index f8dd91e3cb7..df10d269ca6 100644
--- a/odoo/addons/base/tests/test_qweb.py
+++ b/odoo/addons/base/tests/test_qweb.py
@@ -12,7 +12,7 @@ from lxml.builder import E
from odoo.modules import get_module_resource
from odoo.tests.common import TransactionCase
-from odoo.addons.base.models.qweb import QWebException
+from odoo.addons.base.models.ir_qweb import QWebException
from odoo.tools import misc, mute_logger
from odoo.tools.json import scriptsafe as json_scriptsafe
@@ -697,7 +697,9 @@ class TestQWebBasic(TransactionCase):
("(lambda a: a + 5)(x)", {'x': 10}, 15),
("(lambda a: a + x)(5)", {'x': 10}, 15),
("sum(x for x in range(4)) + ((x))", {'x': 10}, 16),
- ("['test_' + x for x in ['a', 'b']]", {}, ['test_a', 'test_b'])
+ ("['test_' + x for x in ['a', 'b']]", {}, ['test_a', 'test_b']),
+ ("""1 and 2 and 0
+ or 9""", {}, 9),
]
IrQweb = self.env['ir.qweb']
@@ -882,6 +884,16 @@ class TestQWebBasic(TransactionCase):
rendered = self.env['ir.qweb']._render(t.id)
self.assertEqual(rendered.strip(), result.strip())
+ def test_out(self):
+ t = self.env['ir.ui.view'].create({
+ 'name': 'test',
+ 'type': 'qweb',
+ 'arch_db': '''Default
'''
+ })
+ result = """1
"""
+ rendered = self.env['ir.qweb']._render(t.id, {'a': 1})
+ self.assertEqual(rendered.strip(), result.strip())
+
def test_out_format_1(self):
t = self.env['ir.ui.view'].create({
'name': 'test',
@@ -1161,7 +1173,7 @@ class TestQWebStaticXml(TransactionCase):
return lambda: self.run_test_file(os.path.join(path, f))
- @mute_logger('odoo.addons.base.models.qweb') # tests t-raw which is deprecated
+ @mute_logger('odoo.addons.base.models.ir_qweb') # tests t-raw which is deprecated
def run_test_file(self, path):
self.env.user.tz = 'Europe/Brussels'
doc = etree.parse(path).getroot()
diff --git a/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py b/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py
index 35f43462db5..7818e6cc9db 100644
--- a/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py
+++ b/odoo/addons/test_assetsbundle/tests/test_assetsbundle.py
@@ -21,7 +21,7 @@ from odoo.addons.base.models.ir_attachment import IrAttachment
from odoo.modules.module import get_resource_path, get_manifest
from odoo.tests import HttpCase, tagged
from odoo.tests.common import TransactionCase
-from odoo.addons.base.models.qweb import QWebException
+from odoo.addons.base.models.ir_qweb import QWebException
from odoo.tools import mute_logger, func
diff --git a/odoo/tools/profiler.py b/odoo/tools/profiler.py
index 988d8a34091..0c22ef415ce 100644
--- a/odoo/tools/profiler.py
+++ b/odoo/tools/profiler.py
@@ -313,7 +313,7 @@ class QwebTracker():
def wrap_compile_directive(cls, method_compile_directive):
@functools.wraps(method_compile_directive)
def _tracked_compile_directive(self, el, options, directive, indent):
- if not options.get('profile') or directive in ('content', 'tag'):
+ if not options.get('profile') or directive in ('inner-content', 'tag'):
return method_compile_directive(self, el, options, directive, indent)
enter = self._indent(f"self.env.context['qweb_tracker'].enter_directive({directive!r}, {el.attrib!r}, {options['last_path_node']!r})", indent)