diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index d98465b1281..ca590174688 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -382,7 +382,7 @@ class Http(models.AbstractModel):
# contains branding (
).
et = view.with_context(inherit_branding=False)._get_combined_arch()
node = et.xpath(exception.path) if exception.path else et
- line = node is not None and etree.tostring(node[0], encoding='unicode')
+ line = node is not None and len(node) > 0 and etree.tostring(node[0], encoding='unicode')
if line:
values['view'] = View._views_get(exception_template).filtered(
lambda v: line in v.arch
diff --git a/odoo/addons/base/models/ir_qweb.py b/odoo/addons/base/models/ir_qweb.py
index 379593cdd23..f3284c5149d 100644
--- a/odoo/addons/base/models/ir_qweb.py
+++ b/odoo/addons/base/models/ir_qweb.py
@@ -1,5 +1,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
"""
================
IrQWeb / ir.qweb
@@ -77,8 +76,11 @@ in the IrQweb class.
┣━► _compile_directives (loop) Consume all remaining directives ◄━━━┓ ┃ ┃
┃ ┃ (e.g.: to change the indentation) ┃ ┃ ┃
┃ ┣━► _compile_directive ┃ ┃ ┃
- ┃ ┃ ┗━► t-if ━━► _compile_directive_if ━┫ ┃ ┃
- ┃ ┃ ┗━► t-foreach ━━► _compile_directive_foreach ━┛ ┃ ┃
+ ┃ ┃ ┗━► t-nocache ━━► _compile_directive_nocache ━┫ ┃ ┃
+ ┃ ┃ ┗━► t-cache ━━► _compile_directive_cache ━┫ ┃ ┃
+ ┃ ┃ ┗━► t-groups ━━► _compile_directive_groups ━┫ ┃ ┃
+ ┃ ┃ ┗━► t-foreach ━━► _compile_directive_foreach ━┫ ┃ ┃
+ ┃ ┃ ┗━► t-if ━━► _compile_directive_if ━┛ ┃ ┃
┃ ┃ ┗━► t-inner-content ━━► _compile_directive_inner_content ◄━━━━━┓ ━┛ ┃
┃ ┃ ┗━► t-options ━━► _compile_directive_options ┃ ┃
┃ ┃ ┗━► t-set ━━► _compile_directive_set ◄━━┓ ┃ ┃
@@ -322,16 +324,6 @@ same node.
The compilation method only validates if ``t-valuef`` and ``t-set`` are on the
same node.
-``t-set-*`` and ``t-setf-*``
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-**Values**: python expression or format string expression
-
-For each key-value ``t-set-key="value"`` the value will be compiled into a
-python expression or into a format string expression for ``t-setf-*``.
-At **rendering time** the ``values`` dictionary is updated.
-
-(for example: ``t-set-bib="1 + 1"`` is equal to ``t-set="bib" t-value="2"``)
-
Technical directives
--------------------
@@ -382,8 +374,8 @@ from markupsafe import Markup, escape
from collections.abc import Sized, Mapping
from itertools import count, chain
from lxml import etree
-from psycopg2.extensions import TransactionRollbackError
from dateutil.relativedelta import relativedelta
+from psycopg2.extensions import TransactionRollbackError
from odoo import api, models, tools
from odoo.tools import config, safe_eval, pycompat, SUPPORTED_DEBUGGER
@@ -394,7 +386,7 @@ from odoo.tools.image import image_data_uri
from odoo.http import request
from odoo.modules.module import get_resource_path
from odoo.tools.profiler import QwebTracker
-from odoo.exceptions import UserError
+from odoo.exceptions import UserError, AccessDenied, AccessError, MissingError, ValidationError
from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.base.models.ir_asset import can_aggregate, STYLE_EXTENSIONS, SCRIPT_EXTENSIONS
@@ -435,7 +427,7 @@ ALLOWED_KEYWORD = frozenset(['False', 'None', 'True', 'and', 'as', 'elif', 'else
# regexpr for string formatting and extract ( ruby-style )|( jinja-style ) used in `_compile_format`
FORMAT_REGEX = re.compile(r'(?:#\{(.+?)\})|(?:\{\{(.+?)\}\})')
RSTRIP_REGEXP = re.compile(r'\n[ \t]*$')
-FISRT_RSTRIP_REGEXP = re.compile(r'^(\n[ \t]*)+(\n[ \t])')
+FIRST_RSTRIP_REGEXP = re.compile(r'^(\n[ \t]*)+(\n[ \t])')
VARNAME_REGEXP = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$')
TO_VARNAME_REGEXP = re.compile(r'[^A-Za-z0-9_]+')
# Attribute name used outside the context of the QWeb.
@@ -479,29 +471,35 @@ def keep_query(*keep_params, **additional_params):
class QWebException(Exception):
""" Management of errors that raised when rendering a QWeb template.
"""
- def __init__(self, message, qweb, template=None, path=None, code=None):
- self.name = template
- self.code = code if qweb.env.context.get('dev_mode') else None
- self.path = path
- self.html = None
- if template is not None and path and ':' not in path:
- try:
- element = qweb._get_template(template)[0]
- nodes = element.getroottree().xpath(self.path)
- if nodes:
- node = etree.Element(nodes[0].tag, nodes[0].attrib)
- self.html = etree.tostring(node, encoding='unicode')
- except Exception as e:
- _logger.warning('Cannot load template %s: %s', template, e)
+ def __init__(self, message, qweb, template=None, ref=None, path_xml=None, code=None):
self.stack = traceback.format_exc()
+ self.name = template
+ self.ref = ref
+ self.path, self.html = path_xml or (None, None)
+ self.code = None
+ if code:
+ self.code = '\n'.join(code.split('\n')[:-1]) if qweb.env.context.get('dev_mode') else None
+ line_nb = 0
+ for error_line in reversed(self.stack.split('\n')):
+ if f'File "<{self.ref}>"' in error_line:
+ line_function = error_line.split(', line ')[1]
+ line_nb = int(line_function.split(',')[0])
+ break
+ for code_line in reversed(code.split('\n')[:line_nb]):
+ match = re.match(r'\s*# element: (.*) , (.*)', code_line)
+ if match:
+ self.path = match[1][1:-1]
+ self.html = match[2][1:-1]
+ break
+
self.title = message
super().__init__(message)
def __str__(self):
parts = [self.title]
- if str(self.__cause__) != '':
+ if self.__cause__ and str(self.__cause__) != '':
parts.append(f"{self.__cause__.__class__.__name__}: {self.__cause__}")
- elif str(self.__context__) != '':
+ elif self.__context__ and str(self.__context__) != '':
parts.append(f"{self.__context__.__class__.__name__}: {self.__context__}")
if self.name is not None:
parts.append(f"Template: {self.name}")
@@ -545,7 +543,6 @@ class IrQWeb(models.AbstractModel):
:param dict values: template values to be used for rendering
:param options: used to compile the template
Options will be add into the IrQweb.env.context for the rendering.
- * ``load`` (function) overrides the load method
* ``lang`` (str) used language to render the template
* ``inherit_branding`` (bool) add the tag node branding
* ``inherit_branding_auto`` (bool) add the branding on fields
@@ -558,13 +555,14 @@ class IrQWeb(models.AbstractModel):
"""
values = values.copy() if values else {}
if T_CALL_SLOT in values:
- raise ValueError('values[0] should be unset when call the _render method and only set into the template.')
+ raise ValueError(f'values[{T_CALL_SLOT}] should be unset when call the _render method and only set into the template.')
irQweb = self.with_context(**options)._prepare_environment(values)
safe_eval.check_values(values)
- render_template = irQweb._compile(template)
+ template_functions, def_name = irQweb._compile(template)
+ render_template = template_functions[def_name]
rendering = render_template(irQweb, values)
result = ''.join(rendering)
@@ -575,16 +573,59 @@ class IrQWeb(models.AbstractModel):
""" Return the list of context keys to use for caching ``_compile``. """
return ['lang', 'inherit_branding', 'edit_translations', 'profile']
- # apply ormcache_context decorator unless in dev mode...
@tools.conditional(
'xml' not in tools.config['dev_mode'],
tools.ormcache('template', 'tuple(self.env.context.get(k) for k in self._get_template_cache_keys())'),
)
+ def _get_view_id(self, template):
+ try:
+ return self.env['ir.ui.view'].sudo().with_context(load_all_views=True)._get_view_id(template)
+ except Exception:
+ return None
+
@QwebTracker.wrap_compile
def _compile(self, template):
+ if isinstance(template, etree._Element):
+ self = self.with_context(is_t_cache_disabled=True)
+ ref = None
+ else:
+ ref = self._get_view_id(template)
+
+ # define the base key cache for code in cache and t-cache feature
+ base_key_cache = None
+ if ref:
+ base_key_cache = self._get_cache_key(tuple([ref] + [self.env.context.get(k) for k in self._get_template_cache_keys()]))
+ self = self.with_context(__qweb_base_key_cache=base_key_cache)
+
+ # generate the template functions and the root function name
+ def generate_functions():
+ code, options, def_name = self._generate_code(template)
+ code = '\n'.join([
+ "def generate_functions():",
+ " template_functions = {}",
+ indent_code(code, 1),
+ f" template_functions['options'] = {options if self.env.context.get('profile') else None!r}",
+ " return template_functions",
+ ])
+
+ try:
+ compiled = compile(code, f"<{ref}>", 'exec')
+ globals_dict = self._prepare_globals()
+ globals_dict['__builtins__'] = globals_dict # So that unknown/unsafe builtins are never added.
+ unsafe_eval(compiled, globals_dict)
+ return globals_dict['generate_functions'](), def_name
+ except QWebException:
+ raise
+ except Exception as e:
+ raise QWebException("Error when compiling xml template",
+ self, template, code=code, ref=ref) from e
+
+ return self._load_values(base_key_cache, generate_functions)
+
+ def _generate_code(self, template):
""" Compile the given template into a rendering function (generator)::
- render(qweb, values)
+ render_template(qweb, values)
This method can be called only by the IrQweb `_render` method or by
the compiled code of t-call from an other template.
@@ -594,6 +635,8 @@ class IrQWeb(models.AbstractModel):
where ``qweb`` is a QWeb instance and ``values`` are the values to
render.
+
+ :returns: tuple containing code, options and main method name
"""
# The `compile_context`` dictionary includes the elements used for the
# cache key to which are added the template references as well as
@@ -604,14 +647,18 @@ class IrQWeb(models.AbstractModel):
try:
element, document, ref = self._get_template(template)
except (ValueError, UserError) as e:
+ # return the error function if the template is not found or fail
message = str(e)
- ClassError = e.__class__
- def not_found_template(self, values, gen0=None):
- if self.env.context.get('raise_if_not_found', True):
- raise ClassError(message)
- _logger.warning('Cannot load template %s: %s', template, message)
- return ''
- return not_found_template
+ code = indent_code(f"""
+ def not_found_template(self, values):
+ if self.env.context.get('raise_if_not_found', True):
+ raise {e.__class__.__name__}({message!r})
+ warning('Cannot load template %s: %s', {template!r}, {message!r})
+ return ''
+ template_functions = {{'not_found_template': not_found_template}}
+ """, 0)
+ return (code, {}, 'not_found_template')
+
compile_context.pop('raise_if_not_found', None)
# reference to get xml and etree (usually the template ID)
@@ -625,7 +672,7 @@ class IrQWeb(models.AbstractModel):
# Root of the etree which will be processed during compilation.
compile_context['root'] = element.getroottree()
# Reference to the last node being compiled. It is mainly used for debugging and displaying error messages.
- compile_context['last_path_node'] = None
+ compile_context['_qweb_error_path_xml'] = None
if not compile_context.get('nsmap'):
compile_context['nsmap'] = {}
@@ -639,72 +686,63 @@ class IrQWeb(models.AbstractModel):
# generate code
- compile_context['qweb_name_gen'] = count()
+ def_name = TO_VARNAME_REGEXP.sub(r'_', f'template_{ref}')
- def_name = self._make_name(TO_VARNAME_REGEXP.sub(r'_', f'template_{ref}'), compile_context)
+ name_gen = count()
+ compile_context['make_name'] = lambda prefix: f"{def_name}_{prefix}_{next(name_gen)}"
try:
if element.text:
- element.text = FISRT_RSTRIP_REGEXP.sub(r'\2', element.text)
+ element.text = FIRST_RSTRIP_REGEXP.sub(r'\2', element.text)
+
+ compile_context['template_functions'] = {}
- compile_context['_all_def'] = []
compile_context['_text_concat'] = []
self._append_text("", compile_context) # To ensure the template function is a generator and doesn't become a regular function
- code_lines = (
- [f'def {def_name}(self, values, gen0, log):']
- + self._compile_node(element, compile_context, 1)
- + self._flush_text(compile_context, 1, rstrip=True)
- )
- for lines in compile_context['_all_def']:
- code_lines.extend(lines)
+ compile_context['template_functions'][f'{def_name}_content'] = (
+ [f"def {def_name}_content(self, values):"]
+ + self._compile_node(element, compile_context, 2)
+ + self._flush_text(compile_context, 2, rstrip=True))
+
+ compile_context['template_functions'][def_name] = [indent_code(f"""
+ def {def_name}(self, values):
+ try:
+ if '__qweb_loaded_values' not in values:
+ values['__qweb_loaded_values'] = {{}}
+ values['__qweb_root_values'] = values.copy()
+ values['xmlid'] = {options['ref_name']!r}
+ values['viewid'] = {options['ref']!r}
+ values['__qweb_loaded_values'].update(template_functions)
+
+ yield from {def_name}_content(self, values)
+ except QWebException:
+ raise
+ except Exception as e:
+ if isinstance(e, TransactionRollbackError):
+ raise
+ raise QWebException("Error while render the template",
+ self, template, ref={compile_context['ref']!r}, code=code) from e
+ """, 0)]
except QWebException:
raise
except Exception as e:
raise QWebException("Error when compiling xml template",
- self, template, path=compile_context.get('last_path_node')) from e
- try:
- code = '\n'.join(code_lines)
- except Exception as e:
- code = '\n'.join(map(str, code_lines))
- raise QWebException("Error when compiling xml template",
- self, template, code=code) from e
+ self, template, ref=compile_context['ref'], path_xml=compile_context['_qweb_error_path_xml']) from e
- # compile code and defined default values
+ code_lines = ['code = None']
+ code_lines.append(f'template = {(document if isinstance(template, etree._Element) else template)!r}')
+ code_lines.append('template_functions = {}')
- try:
- # noinspection PyBroadException
- compiled = compile(code, f'<{def_name}>', 'exec')
- globals_dict = self._prepare_globals()
- 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:
- raise
- except Exception as e:
- raise QWebException("Error when compiling xml template",
- self, template, code=code) from e
+ for lines in compile_context['template_functions'].values():
+ code_lines.extend(lines)
- # return the wrapped function
+ for name in compile_context['template_functions']:
+ code_lines.append(f'template_functions[{name!r}] = {name}')
- def render_template(self, values, gen0=None):
- try:
- if not values.get('xmlid'):
- values['xmlid'] = options['ref_name']
- values['viewid'] = options['ref']
- log = {'last_path_node': None}
- if gen0 is None:
- gen0 = []
- yield from compiled_fn(self, values, gen0, log)
- except (QWebException, TransactionRollbackError) as e:
- raise
- except Exception as e:
- raise QWebException("Error while render the template",
- self, template, path=log.get('last_path_node'), code=code) from e
+ code = '\n'.join(code_lines)
+ code += f'\n\ncode = {code!r}'
- # add key cache options for profiling tools
- render_template.options = options
-
- return render_template
+ return (code, options, def_name)
# read and load input template
@@ -715,7 +753,6 @@ class IrQWeb(models.AbstractModel):
reference of the template (id, t-name or template).
:param template: template identifier or etree
- :param self.env.context['load']: (function(template)) overrides the load method
"""
assert template not in (False, None, ""), "template is required"
@@ -735,7 +772,7 @@ class IrQWeb(models.AbstractModel):
except ValueError:
ref_alias = template # e.g. web.layout
- doc_or_elem, ref = self.env.context.get('load', self._load)(ref_alias) or (None, None)
+ doc_or_elem, ref = self._load(ref_alias) or (None, None)
if doc_or_elem is None:
raise ValueError(f"Can not load template: {ref_alias!r}")
if isinstance(doc_or_elem, etree._Element):
@@ -807,12 +844,13 @@ class IrQWeb(models.AbstractModel):
:returns self (with new context)
"""
+ debug = request and request.session.debug or ''
values.update(
true=True,
false=False,
)
if not self.env.context.get('minimal_qcontext'):
- values.setdefault('debug', request and request.session.debug or '')
+ values.setdefault('debug', debug)
values.setdefault('user_id', self.env.user.with_env(self.env))
values.setdefault('res_company', self.env.company.sudo())
@@ -833,7 +871,12 @@ class IrQWeb(models.AbstractModel):
keep_query=keep_query,
)
- return self.with_context(dev_mode='qweb' in tools.config['dev_mode'])
+ context = {'dev_mode': 'qweb' in tools.config['dev_mode']}
+ if 'xml' in tools.config['dev_mode']:
+ context['is_t_cache_disabled'] = True
+ elif 'disable-t-cache' in debug:
+ context['is_t_cache_disabled'] = True
+ return self.with_context(**context)
def _prepare_globals(self):
""" Prepare the global context that will sent to eval the qweb
@@ -845,6 +888,16 @@ class IrQWeb(models.AbstractModel):
'Markup': Markup,
'escape': escape,
'VOID_ELEMENTS': VOID_ELEMENTS,
+ 'QWebException': QWebException,
+ 'Exception': Exception,
+ 'TransactionRollbackError': TransactionRollbackError, # for SerializationFailure in assets
+ 'ValueError': ValueError,
+ 'UserError': UserError,
+ 'AccessDenied': AccessDenied,
+ 'AccessError': AccessError,
+ 'MissingError': MissingError,
+ 'ValidationError': ValidationError,
+ 'warning': lambda *args: _logger.warning(*args),
**_BUILTINS,
}
@@ -889,10 +942,6 @@ class IrQWeb(models.AbstractModel):
text_concat.clear()
return [f"{' ' * level}yield {text!r}"]
- def _make_name(self, prefix, compile_context):
- """Generates a unique name."""
- return f"{prefix}_{next(compile_context['qweb_name_gen'])}"
-
def _is_static_node(self, el, compile_context):
""" Test whether the given element is purely static, i.e. (there
are no t-* attributes), does not require dynamic rendering for its
@@ -1142,6 +1191,8 @@ class IrQWeb(models.AbstractModel):
'elif', # Must be the first because compiled by the previous if.
'else', # Must be the first because compiled by the previous if.
'debug',
+ 'nocache',
+ 'cache',
'groups',
'as', 'foreach',
'if',
@@ -1182,11 +1233,9 @@ class IrQWeb(models.AbstractModel):
return self._compile_static_node(el, compile_context, level)
path = compile_context['root'].getpath(el)
- if compile_context['last_path_node'] != path:
- compile_context['last_path_node'] = path
- body = [indent_code(f'log["last_path_node"] = {path!r}', level)]
- else:
- body = []
+ xml = etree.tostring(etree.Element(el.tag, el.attrib), encoding='unicode')
+ compile_context['_qweb_error_path_xml'] = (path, xml)
+ body = [indent_code(f'# element: {path!r} , {xml!r}', level)]
# create an iterator on directives to compile in order
compile_context['iter_directives'] = iter(self._directives_eval_order())
@@ -1312,10 +1361,8 @@ class IrQWeb(models.AbstractModel):
elif directive == 'options':
if any(name.startswith('t-options-') for name in el.attrib):
code.extend(self._compile_directive(el, compile_context, directive, level))
- elif directive == 'set':
- if any(name.startswith('t-set-') or
- name.startswith('t-setf-')
- for name in el.attrib):
+ elif directive == 'nocache':
+ if any(name.startswith('t-nocache-') for name in el.attrib):
code.extend(self._compile_directive(el, compile_context, directive, level))
# compile unordered directives still present on the element
@@ -1346,7 +1393,7 @@ class IrQWeb(models.AbstractModel):
debugger = el.attrib.pop('t-debug')
code = []
if compile_context.get('dev_mode'):
- code.append(indent_code(f"self._debug_trace({debugger!r})", level))
+ code.append(indent_code(f"self._debug_trace({debugger!r}, values)", level))
else:
_logger.warning("@t-debug in template is only available in qweb dev mode")
return code
@@ -1553,26 +1600,28 @@ class IrQWeb(models.AbstractModel):
self._compile_directive(el, compile_context, 'inner-content', 1) +
self._flush_text(compile_context, 1))
if content:
- def_name = self._make_name('t_set', compile_context)
- compile_context['_all_def'].append([f"def {def_name}(self, values, gen0, log):"] + content)
- expr = f"Markup(''.join({def_name}(self, values, gen0, log)))"
+ def_name = compile_context['make_name']('t_set')
+ compile_context['template_functions'][def_name] = [f"def {def_name}(self, values):"] + content
+ code.append(indent_code(f"""
+ t_set = []
+ for item in {def_name}(self, values):
+ if isinstance(item, str):
+ t_set.append(item)
+ else:
+ ref, function_name, cached_values = item
+ t_nocache_function = values['__qweb_loaded_values'].get(function_name)
+ if not t_nocache_function:
+ t_call_template_functions, def_name = self._compile(ref)
+ t_nocache_function = t_call_template_functions[function_name]
+
+ nocache_values = values['__qweb_root_values'].copy()
+ nocache_values.update(cached_values)
+ t_set.extend(t_nocache_function(self, nocache_values))
+ """, level))
+ expr = "Markup(''.join(t_set))"
else:
expr = "''"
- if varname == T_CALL_SLOT:
- code.append(indent_code(f"gen0 = {expr}", level))
- else:
- code.append(indent_code(f"values[{varname!r}] = {expr}", level))
-
- for key in list(el.attrib):
- if key.startswith('t-set-') or key.startswith('t-setf-'):
- expr = el.attrib.pop(key)
- varname = key.split('-', 2)
- if not VARNAME_REGEXP.match(varname[2]):
- raise ValueError(f'The varname {varname[2]!r} can only contain alphanumeric characters and underscores.')
- if varname[1] == 'set':
- code.append(indent_code(f"values[{varname[2]!r}] = {self._compile_expr(expr)}", level))
- else:
- code.append(indent_code(f"values[{varname[2]!r}] = {self._compile_format(expr)}", level))
+ code.append(indent_code(f"values[{varname!r}] = {expr}", level))
return code
@@ -1769,9 +1818,9 @@ class IrQWeb(models.AbstractModel):
self._compile_directives(el, compile_context, level + 1) +
self._flush_text(compile_context, level + 1, rstrip=True))
- t_foreach = self._make_name('t_foreach', compile_context)
- size = self._make_name('size', compile_context)
- has_value = self._make_name('has_value', compile_context)
+ t_foreach = compile_context['make_name']('t_foreach')
+ size = compile_context['make_name']('size')
+ has_value = compile_context['make_name']('has_value')
if expr_foreach.isdigit():
code.append(indent_code(f"""
@@ -1810,7 +1859,6 @@ class IrQWeb(models.AbstractModel):
values[{expr_as + '_parity'!r}] = 'odd' if values[{expr_as + '_odd'!r}] else 'even'
""", level))
- code.append(indent_code(f'log["last_path_node"] = {compile_context["root"].getpath(el)!r} ', level + 1))
code.extend(content_foreach or indent_code('continue', level + 1))
return code
@@ -1876,7 +1924,7 @@ class IrQWeb(models.AbstractModel):
if expr == T_CALL_SLOT and code_options != 'True':
code.append(indent_code("if True:", level))
code.extend(tag_open)
- code.append(indent_code("yield from gen0", level + 1))
+ code.append(indent_code(f"yield from values.get({T_CALL_SLOT}, [])", level + 1))
code.extend(tag_close)
return code
elif ttype == 't-field':
@@ -1894,7 +1942,7 @@ class IrQWeb(models.AbstractModel):
force_display_dependent = True
else:
if expr == T_CALL_SLOT:
- code.append(indent_code("content = Markup(''.join(gen0))", level))
+ code.append(indent_code(f"content = Markup(''.join(values.get({T_CALL_SLOT}, [])))", level))
else:
code.append(indent_code(f"content = {self._compile_expr(expr)}", level))
@@ -2027,11 +2075,7 @@ class IrQWeb(models.AbstractModel):
# options
el.attrib.pop('t-consumed-options', None)
- code.append(indent_code(f"""
- t_call_options = dict(values.pop('__qweb_options__', {{}}))
- t_call_options['caller_template'] = {str(compile_context.get('template'))!r}
- t_call_options['last_path_node'] = {str(compile_context['root'].getpath(el))!r}
- """, level))
+ code.append(indent_code("t_call_options = values.pop('__qweb_options__', {})", level))
if nsmap:
# update this dict with the current nsmap so that the callee know
# if he outputting the xmlns attributes is relevenat or not
@@ -2043,27 +2087,32 @@ class IrQWeb(models.AbstractModel):
nsmap.append(f'None:{value!r}')
code.append(indent_code(f"t_call_options.update(nsmap={{{', '.join(nsmap)}}})", level))
- # values (t-out="0" from content and variables from t-set and t-set-*)
- def_name = self._make_name('t_call', compile_context)
+ # values (t-out="0" from content and variables from t-set)
+ def_name = compile_context['make_name']('t_call')
# values from content (t-out="0" and t-set inside the content)
- code_content = [f"def {def_name}(self, values, gen0, log):"]
+ code_content = [f"def {def_name}(self, values):"]
code_content.extend(self._compile_directive(el, compile_context, 'inner-content', 1))
- code_content.extend(self._compile_directive(el, compile_context, 'set', 1))
self._append_text('', compile_context) # To ensure the template function is a generator and doesn't become a regular function
code_content.extend(self._flush_text(compile_context, 1, rstrip=True))
- compile_context['_all_def'].append(code_content)
+ compile_context['template_functions'][def_name] = code_content
- code.append(indent_code("t_call_values = values.copy()", level))
- code.append(indent_code(f"t_call_gen0 = Markup(''.join({def_name}(self, t_call_values, gen0, log)))", level))
+ code.append(indent_code(f"""
+ t_call_values = values.copy()
+ t_call_values[{T_CALL_SLOT}] = list({def_name}(self, t_call_values))
+ """, level))
template = self._compile_format(expr)
# call
code.append(indent_code(f"""
irQweb = self.with_context(**t_call_options)
- render_template = irQweb._compile({template})
- yield from render_template(irQweb, t_call_values, t_call_gen0)
+ template = {template}
+ if template.isnumeric():
+ template = int(template)
+ t_call_template_functions, def_name = irQweb._compile(template)
+ render_template = t_call_template_functions[def_name]
+ yield from render_template(irQweb, t_call_values)
""", level))
return code
@@ -2125,10 +2174,147 @@ class IrQWeb(models.AbstractModel):
return code
+ def _compile_directive_cache(self, el, compile_context, level):
+ """Compile the `t-cache` tuple expression into a key cache.
+
+ The `t-cache` directive allows you to keep the rendered result
+ of a template part. The supplied key must be a tuple. This tuple
+ can contain recordset in this case the zone will be invalidated
+ each time the write_date of these records changes.
+ The values are scoped into the `t-cache` and are not available
+ outside.
+ see: `t-nocache`
+ """
+ expr = el.attrib.pop('t-cache')
+ code = self._flush_text(compile_context, level)
+
+ def_name = compile_context['make_name']('t_cache')
+
+ # Generate the content function
+ def_code = [indent_code(f"""def {def_name}(self, values):""", 0)]
+ def_content = self._compile_directives(el, compile_context, 1)
+ if def_content and not compile_context['_text_concat']:
+ self._append_text('', compile_context) # To ensure the template function is a generator and doesn't become a regular function
+ def_code.extend(def_content)
+ def_code.extend(self._flush_text(compile_context, 1))
+ compile_context['template_functions'][def_name] = def_code
+
+ # Get the dynamic key for the cache and load the content.
+ # The t-nocache yield a tuple (ref, function name) instead of a
+ # When reading tuple coming from t-nocache, we check if the
+ # method is already known otherwise the corresponding template
+ # and its functions are loaded.
+ code.append(indent_code(f"""
+ template_cache_key = {self._compile_expr(expr)} if not self.env.context.get('is_t_cache_disabled') else None
+ cache_key = self._get_cache_key(template_cache_key) if template_cache_key else None
+ uniq_cache_key = cache_key and ({str(self.env.context['__qweb_base_key_cache'])!r}, '{def_name}_cache', cache_key)
+ loaded_values = values['__qweb_loaded_values']
+ def {def_name}_cache():
+ content = []
+ text = []
+ for item in {def_name}(self, {{**values, '__qweb_in_cache': True}}):
+ if isinstance(item, str):
+ text.append(item)
+ else:
+ content.append(''.join(text))
+ content.append(item)
+ text = []
+ if text:
+ content.append(''.join(text))
+ return content
+ cache_content = self._load_values(uniq_cache_key, {def_name}_cache, loaded_values)
+ if values.get('__qweb_in_cache'):
+ yield from cache_content
+ else:
+ for item in cache_content:
+ if isinstance(item, str):
+ yield item
+ else:
+ ref, function_name, cached_values = item
+ t_nocache_function = loaded_values.get(function_name)
+ if not t_nocache_function:
+ t_call_template_functions, def_name = self._compile(ref)
+ t_nocache_function = t_call_template_functions[function_name]
+
+ nocache_values = values['__qweb_root_values'].copy()
+ nocache_values.update(cached_values)
+ yield ''.join(t_nocache_function(self, nocache_values))
+ """, level))
+
+ return code
+
+ def _compile_directive_nocache(self, el, compile_context, level):
+ """
+ The `t-nocache` directive makes it possible to force rendering
+ of a part even if it is in a `t-cache`. The values available in
+ the `t-nocache` are the one provided when calling the template
+ (and therefore ignores any t-set that could have been done).
+
+ The `t-nocache-*` are the values whose result of the
+ expression will be cached and added to the root's values when
+ rendering the no cache part. Only primitive types can be cached.
+
+ see: `t-cache`
+ """
+ if 't-nocache' not in el.attrib:
+ raise SyntaxError("t-nocache-* must be on the same node as t-nocache")
+
+ el.attrib.pop('t-nocache')
+ code = self._flush_text(compile_context, level)
+
+ # t-nocache-* will generate the values to put in cache
+ # must cosume this attributes before generate the cached content.
+ code_cache_values = []
+ for key in list(el.attrib):
+ if key.startswith('t-nocache-'):
+ expr = el.attrib.pop(key)
+ varname = key[10:]
+ if not VARNAME_REGEXP.match(varname):
+ raise ValueError(f'The varname {varname!r} can only contain alphanumeric characters and underscores.')
+ code_cache_values.append(indent_code(f"""
+ cached_value = {self._compile_expr(expr)}
+ if cached_value is not None and not isinstance(cached_value, (str, int, float, bool)):
+ raise ValueError(f'''The value type of {key!r} cannot be cached: {{cached_value!r}}''')
+ cached_values[{varname!r}] = cached_value
+ """, level + 1))
+
+ # generate the cached content method
+ def_name = compile_context['make_name']('t_nocache')
+ def_code = [f"def {def_name}(self, values):"]
+ def_code.append(indent_code("try:", 1))
+ def_content = self._compile_directives(el, compile_context, 2)
+ if def_content and not compile_context['_text_concat']:
+ self._append_text('', compile_context) # To ensure the template function is a generator and doesn't become a regular function
+ def_code.extend(def_content)
+ def_code.extend(self._flush_text(compile_context, 2))
+ def_code.append(indent_code(f"""
+ except QWebException:
+ raise
+ except Exception as e:
+ raise QWebException("Error while render the template",
+ self, template, ref={compile_context['ref']!r}, code=code) from e
+ """, 1))
+ compile_context['template_functions'][def_name] = def_code
+
+ # if the nocache is inside a cache return a tuple with the method name and the cached values
+ code.append(indent_code("""
+ if values.get('__qweb_in_cache'):
+ cached_values = {}
+ """, level))
+ code.extend(code_cache_values)
+ code.append(indent_code(f"yield ({compile_context['template']!r}, {def_name!r}, cached_values)", level+1))
+ # else render the content
+ code.append(indent_code(f"""
+ else:
+ yield from {def_name}(self, values)
+ """, level))
+
+ return code
+
# methods called by the compiled function at rendering time.
- def _debug_trace(self, debugger):
- """Method called at compile time to load debugger."""
+ def _debug_trace(self, debugger, values):
+ """Method called at running time to load debugger."""
if debugger in SUPPORTED_DEBUGGER:
__import__(debugger).set_trace()
else:
@@ -2214,8 +2400,52 @@ class IrQWeb(models.AbstractModel):
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
+ # qweb cache feature
+ def _get_cache_key(self, cache_key):
+ """
+ Convert the template cache key item into a hashable key.
+ :param cache_key: tuple
+ :returns: tuple of hashable items
+ """
+ if not isinstance(cache_key, (tuple, list)):
+ cache_key = (cache_key,)
+ keys = []
+ for item in cache_key:
+ try:
+ # use try catch instead of isinstance to detect lazy values
+ keys.append(item._name)
+ keys.append(tuple(item.ids))
+ dates = item.mapped('write_date')
+ if dates:
+ keys.append(max(dates).timestamp())
+ except AttributeError:
+ keys.append(repr(item))
+ return tuple(keys)
+
+ def _load_values(self, cache_key, get_value, loaded_values=None):
+ """ generate value from the function if the result is not cached. """
+ if not cache_key:
+ return get_value()
+
+ value = loaded_values and loaded_values.get(cache_key)
+ if not value:
+ value = self._get_cached_values(cache_key, get_value)
+ if loaded_values is not None:
+ loaded_values[cache_key] = value
+ return value
+
+ # The cache does not need to be invalidated if the 'base_key_cache'
+ # in '_compile' method contains the write_date of all inherited views.
+ @tools.conditional(
+ 'xml' not in tools.config['dev_mode'],
+ tools.ormcache('cache_key'),
+ )
+ def _get_cached_values(self, cache_key, get_value):
+ """ generate value from the function if the result is not cached. """
+ return get_value()
+
+ # 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)
@@ -2305,10 +2535,20 @@ def render(template_name, values, load, **options):
pool = MockPool()
+ def _load(self, ref):
+ """
+ Load the template referenced by ``ref``.
+
+ :returns: The loaded template (as string or etree) and its
+ identifier
+ :rtype: Tuple[Union[etree, str], Optional[str, int]]
+ """
+ return self.env.context['load'](ref)
+
def _prepare_environment(self, values):
values['true'] = True
values['false'] = False
- return self
+ return self.with_context(is_t_cache_disabled=True, __qweb_loaded_values={})
def _get_field(self, *args):
raise NotImplementedError("Fields are not allowed in this rendering mode. Please use \"env['ir.qweb']._render\" method")
diff --git a/odoo/addons/base/tests/test_profiler.py b/odoo/addons/base/tests/test_profiler.py
index dab112f1533..fc5207fa453 100644
--- a/odoo/addons/base/tests/test_profiler.py
+++ b/odoo/addons/base/tests/test_profiler.py
@@ -464,15 +464,15 @@ class TestProfiling(TransactionCase):
'key': 'root',
'arch_db': '''
- [: ]
-
+ [: ]
+
'''
})
child_template = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
'key': 'dummy',
- 'arch_db': ' '
+ 'arch_db': ' '
})
self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)"
"VALUES ('dummy', 'ir.ui.view', %s, 'base')", [child_template.id])
@@ -518,33 +518,33 @@ class TestProfiling(TransactionCase):
# first template and first directive
{'view_id': template.id, 'xpath': '/t/t', 'directive': """t-foreach="{'a': 3, 'b': 2, 'c': 1}" t-as='item'""", 'query': 0},
# first pass in the loop
- {'view_id': template.id, 'xpath': '/t/t/t[1]', 'directive': "t-esc='item_index'", 'query': 0},
- {'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-call='base.dummy'", 'query': 0}, # 0 because the template is in ir.ui.view cache
- {'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-set-record='item'", 'query': 0}, # t-set it's consumed by t-call
+ {'view_id': template.id, 'xpath': '/t/t/t[1]', 'directive': "t-out='item_index'", 'query': 0},
+ {'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-set='record' t-value='item'", 'query': 0},
+ {'view_id': template.id, 'xpath': '/t/t/t[3]', 'directive': "t-call='base.dummy'", 'query': 0}, # 0 because the template is in ir.ui.view cache
# first pass in the loop: content of the child template
{'view_id': child_template.id, 'xpath': '/t/span', 'directive': "t-attf-class='myclass'", 'query': 0},
- {'view_id': child_template.id, 'xpath': '/t/span/t[1]', 'directive': "t-esc='record'", 'query': 0},
- {'view_id': child_template.id, 'xpath': '/t/span/t[2]', 'directive': "t-esc='add_one_query()'", 'query': 1},
- {'view_id': template.id, 'xpath': '/t/t/t[3]', 'directive': "t-esc='item_value'", 'query': 0},
- {'view_id': template.id, 'xpath': '/t/t/b', 'directive': "t-esc='add_one_query()'", 'query':1},
+ {'view_id': child_template.id, 'xpath': '/t/span/t[1]', 'directive': "t-out='record'", 'query': 0},
+ {'view_id': child_template.id, 'xpath': '/t/span/t[2]', 'directive': "t-out='add_one_query()'", 'query': 1},
+ {'view_id': template.id, 'xpath': '/t/t/t[4]', 'directive': "t-out='item_value'", 'query': 0},
+ {'view_id': template.id, 'xpath': '/t/t/b', 'directive': "t-out='add_one_query()'", 'query':1},
# second pass in the loop
- {'view_id': template.id, 'xpath': '/t/t/t[1]', 'directive': "t-esc='item_index'", 'query': 0},
- {'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-call='base.dummy'", 'query': 0},
- {'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-set-record='item'", 'query': 0},
+ {'view_id': template.id, 'xpath': '/t/t/t[1]', 'directive': "t-out='item_index'", 'query': 0},
+ {'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-set='record' t-value='item'", 'query': 0},
+ {'view_id': template.id, 'xpath': '/t/t/t[3]', 'directive': "t-call='base.dummy'", 'query': 0},
{'view_id': child_template.id, 'xpath': '/t/span', 'directive': "t-attf-class='myclass'", 'query': 0},
- {'view_id': child_template.id, 'xpath': '/t/span/t[1]', 'directive': "t-esc='record'", 'query': 0},
- {'view_id': child_template.id, 'xpath': '/t/span/t[2]', 'directive': "t-esc='add_one_query()'", 'query': 1},
- {'view_id': template.id, 'xpath': '/t/t/t[3]', 'directive': "t-esc='item_value'", 'query': 0},
- {'view_id': template.id, 'xpath': '/t/t/b', 'directive': "t-esc='add_one_query()'", 'query':1},
+ {'view_id': child_template.id, 'xpath': '/t/span/t[1]', 'directive': "t-out='record'", 'query': 0},
+ {'view_id': child_template.id, 'xpath': '/t/span/t[2]', 'directive': "t-out='add_one_query()'", 'query': 1},
+ {'view_id': template.id, 'xpath': '/t/t/t[4]', 'directive': "t-out='item_value'", 'query': 0},
+ {'view_id': template.id, 'xpath': '/t/t/b', 'directive': "t-out='add_one_query()'", 'query':1},
# third pass in the loop
- {'view_id': template.id, 'xpath': '/t/t/t[1]', 'directive': "t-esc='item_index'", 'query': 0},
- {'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-call='base.dummy'", 'query': 0},
- {'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-set-record='item'", 'query': 0},
+ {'view_id': template.id, 'xpath': '/t/t/t[1]', 'directive': "t-out='item_index'", 'query': 0},
+ {'view_id': template.id, 'xpath': '/t/t/t[2]', 'directive': "t-set='record' t-value='item'", 'query': 0},
+ {'view_id': template.id, 'xpath': '/t/t/t[3]', 'directive': "t-call='base.dummy'", 'query': 0},
{'view_id': child_template.id, 'xpath': '/t/span', 'directive': "t-attf-class='myclass'", 'query': 0},
- {'view_id': child_template.id, 'xpath': '/t/span/t[1]', 'directive': "t-esc='record'", 'query': 0},
- {'view_id': child_template.id, 'xpath': '/t/span/t[2]', 'directive': "t-esc='add_one_query()'", 'query': 1},
- {'view_id': template.id, 'xpath': '/t/t/t[3]', 'directive': "t-esc='item_value'", 'query': 0},
- {'view_id': template.id, 'xpath': '/t/t/b', 'directive': "t-esc='add_one_query()'", 'query':1},
+ {'view_id': child_template.id, 'xpath': '/t/span/t[1]', 'directive': "t-out='record'", 'query': 0},
+ {'view_id': child_template.id, 'xpath': '/t/span/t[2]', 'directive': "t-out='add_one_query()'", 'query': 1},
+ {'view_id': template.id, 'xpath': '/t/t/t[4]', 'directive': "t-out='item_value'", 'query': 0},
+ {'view_id': template.id, 'xpath': '/t/t/b', 'directive': "t-out='add_one_query()'", 'query':1},
]
self.assertEqual(data, expected)
diff --git a/odoo/addons/base/tests/test_qweb.py b/odoo/addons/base/tests/test_qweb.py
index a67fdb8e5fe..340a1ef961d 100644
--- a/odoo/addons/base/tests/test_qweb.py
+++ b/odoo/addons/base/tests/test_qweb.py
@@ -880,36 +880,6 @@ class TestQWebBasic(TransactionCase):
self.assertEqual(rendered.strip(), result.strip())
def test_set_3(self):
- t = self.env['ir.ui.view'].create({
- 'name': 'test',
- 'type': 'qweb',
- 'arch_db': '''
-
-
- '''
- })
- result = """
- abc 1
- """
- rendered = self.env['ir.qweb']._render(t.id)
- self.assertEqual(rendered.strip(), result.strip())
-
- def test_set_4(self):
- t = self.env['ir.ui.view'].create({
- 'name': 'test',
- 'type': 'qweb',
- 'arch_db': '''
-
-
- '''
- })
- result = """
- abc 1
- """
- rendered = self.env['ir.qweb']._render(t.id)
- self.assertEqual(rendered.strip(), result.strip())
-
- def test_set_5(self):
t = self.env['ir.ui.view'].create({
'name': 'test',
'type': 'qweb',
@@ -1288,101 +1258,41 @@ class TestQWebBasic(TransactionCase):
def test_error_message_5(self):
# Error not found a first rendering.
- t = self.env['ir.ui.view'].create({
- 'name': 'test',
- 'type': 'qweb',
- 'arch_db': '''
-
- '''
- })
-
- def load(ref_alias):
- raise ValueError(f'Not Found: {ref_alias}')
-
- with self.assertRaises(ValueError, msg=f"Not Found: {t.id}"):
- self.env['ir.qweb']._render(t.id, load=load)
+ with self.assertRaises(MissingError, msg="Not Found"):
+ self.env['ir.qweb']._render(-9999)
@mute_logger('odoo.addons.base.models.ir_qweb') # warning for template not found
def test_error_message_6(self):
# Error not found a second rendering (first rendering with option hide this error).
- t = self.env['ir.ui.view'].create({
- 'name': 'test',
- 'type': 'qweb',
- 'arch_db': '''
-
- '''
- })
-
- def load(ref_alias):
- raise ValueError(f'Not Found: {ref_alias}')
-
- html = self.env['ir.qweb']._render(t.id, load=load, raise_if_not_found=False)
+ html = self.env['ir.qweb']._render(-9999, raise_if_not_found=False)
self.assertEqual('', html)
# re try this rendering without any error (use cached method)
- html = self.env['ir.qweb']._render(t.id, load=load, raise_if_not_found=False)
+ html = self.env['ir.qweb']._render(-9999, raise_if_not_found=False)
self.assertEqual('', html)
# re try this rendering but raise (use cached method)
- with self.assertRaises(ValueError, msg=f"Not Found: {t.id}"):
- self.env['ir.qweb']._render(t.id, load=load)
+ with self.assertRaises(MissingError, msg="Not Found"):
+ self.env['ir.qweb']._render(-9999)
def test_error_message_7(self):
# UserError not found a first rendering.
- t = self.env['ir.ui.view'].create({
- 'name': 'test',
- 'type': 'qweb',
- 'arch_db': '''
-
- '''
- })
-
- def load(ref_alias):
- raise ValidationError(f'Not Found: {ref_alias}')
-
- with self.assertRaises(UserError, msg=f"Not Found: {t.id}"):
- self.env['ir.qweb']._render(t.id, load=load)
+ with self.assertRaises(UserError, msg="Not Found"):
+ self.env['ir.qweb']._render(-9999)
@mute_logger('odoo.addons.base.models.ir_qweb') # warning for template not found
def test_error_message_8(self):
# UserError not found a second rendering (first rendering with option hide this error).
- t = self.env['ir.ui.view'].create({
- 'name': 'test',
- 'type': 'qweb',
- 'arch_db': '''
-
- '''
- })
-
- def load(ref_alias):
- raise ValidationError(f'Not Found: {ref_alias}')
-
- html = self.env['ir.qweb']._render(t.id, load=load, raise_if_not_found=False)
+ html = self.env['ir.qweb']._render(-9999, raise_if_not_found=False)
self.assertEqual('', html)
# re try this rendering without any error (use cached method)
- html = self.env['ir.qweb']._render(t.id, load=load, raise_if_not_found=False)
+ html = self.env['ir.qweb']._render(-9999, raise_if_not_found=False)
self.assertEqual('', html)
# re try this rendering but raise (use cached method)
- with self.assertRaises(UserError, msg=f"Not Found: {t.id}"):
- self.env['ir.qweb']._render(t.id, load=load)
+ with self.assertRaises(UserError, msg="Not Found"):
+ self.env['ir.qweb']._render(-9999)
def test_call_set(self):
view0 = self.env['ir.ui.view'].create({
@@ -1429,82 +1339,6 @@ class TestQWebBasic(TransactionCase):
"""), 'render t-call use lexical scoping, t-call content use independant scoping')
- def test_call_arg(self):
- view0 = self.env['ir.ui.view'].create({
- 'name': "dummy",
- 'type': 'qweb',
- 'arch': """
-
-
-
- """
- })
- self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)"
- "VALUES ('dummy', 'ir.ui.view', %s, 'base')", [view0.id])
-
- view1 = self.env['ir.ui.view'].create({
- 'name': "other",
- 'type': 'qweb',
- 'arch': """
-
-
-
-
-
- """
- })
-
- result = self.env['ir.qweb']._render(view1.id, {})
- self.assertEqual(etree.fromstring(result), etree.fromstring("""
-
- """), 'render t-call with t-set attributes')
-
- def test_call_arg_error(self):
- view0 = self.env['ir.ui.view'].create({
- 'name': "dummy",
- 'type': 'qweb',
- 'arch': """
-
-
-
- """
- })
- self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)"
- "VALUES ('dummy', 'ir.ui.view', %s, 'base')", [view0.id])
-
- view1 = self.env['ir.ui.view'].create({
- 'name': "other",
- 'type': 'qweb',
- 'arch': """
-
-
-
-
-
- """
- })
-
- with self.assertRaises(QWebException):
- self.env['ir.qweb']._render(view1.id)
-
- try:
- self.env['ir.qweb']._render(view1.id)
- except QWebException as e:
- error = str(e)
- self.assertIn("The varname 'a-2' can only contain alphanumeric characters and underscores", error)
- self.assertIn('', error)
-
def test_call_error(self):
view1 = self.env['ir.ui.view'].create({
'name': "other",
@@ -1804,6 +1638,1284 @@ class TestQWebBasic(TransactionCase):
rendered = self.env['ir.qweb']._render(view.id)
self.assertEqual(str(rendered), result)
+class TestQwebCache(TransactionCase):
+ def test_render_xml_cache_base(self):
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "dummy",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+ expected_result = etree.fromstring("""
+
+ """)
+
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
+
+ result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [1, 2, 3]}))
+ self.assertEqual(result, expected_result, 'First rendering (add in cache)')
+
+ result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [10, 20, 30]}))
+ self.assertEqual(result, expected_result, 'Next rendering use cache')
+
+ def test_render_xml_cache_different(self):
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "dummy",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
+
+ # use same cache id, display the same content
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': (1,),
+ 'cache_id2': (1,),
+ 'value': [1, 2, 3],
+ 'value2': [10, 20, 30]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'First rendering (add in cache with different cache)')
+
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': (2, 5, 6),
+ 'cache_id2': (2, 5, 5),
+ 'value': [41, 42, 43],
+ 'value2': [51, 52, 53]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'Use different cache id')
+
+ def test_render_xml_cache_contains_nocache(self):
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "dummy",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
+
+ result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [1, 2, 3]}))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'First rendering add compiled values in cache')
+
+ result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [10, 20, 30]}))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'Next rendering use cache exept for t-nocache=""')
+
+ def test_render_xml_cache_nocache_cache(self):
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "dummy",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
+
+ # use same cache id, display the same content
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': (1, 0),
+ 'cache_id2': (2, 0),
+ 'value': [1, 2, 3],
+ 'value2': [10, 20, 30]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'First rendering (add in cache)')
+
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': (1, 0),
+ 'cache_id2': (2, 1),
+ 'value': [41, 42, 43],
+ 'value2': [51, 52, 53]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'Second rendering (change inside cache id)')
+
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': (1, 1),
+ 'cache_id2': (2, 0),
+ 'value': [31, 32, 33],
+ 'value2': [51, 52, 53]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'Third rendering (change main cache id, old cache inside)')
+
+ def test_render_xml_cache_nocache_cache_on_same_tag(self):
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "dummy",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
+
+ # use same cache id, display the same content
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': (1, 0),
+ 'cache_id2': (2, 0),
+ 'value': [1, 2, 3],
+ 'value2': [10, 20, 30]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'First rendering (add in cache)')
+
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': (1, 0),
+ 'cache_id2': (2, 1),
+ 'value': [41, 42, 43],
+ 'value2': [51, 52, 53]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'Second rendering (change inside cache id)')
+
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': (1, 1),
+ 'cache_id2': (2, 0),
+ 'value': [31, 32, 33],
+ 'value2': [51, 52, 53]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'Third rendering (change main cache id, old cache inside)')
+
+ def test_render_xml_dont_use_cache_base(self):
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "dummy",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=True)
+
+ result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [1, 2, 3]}))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'First rendering')
+
+ result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [10, 20, 30]}))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'Next rendering cannot cache (use_qweb_t_cache is False)')
+
+ def test_render_xml_dont_use_cache_different(self):
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "dummy",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=True)
+
+ # use same cache id, display the same content
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': 1,
+ 'cache_id2': 1,
+ 'value': [1, 2, 3],
+ 'value2': [10, 20, 30]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'First rendering')
+
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': (2, 5, 6),
+ 'cache_id2': (2, 5, 5),
+ 'value': [41, 42, 43],
+ 'value2': [51, 52, 53]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'Use different cache id')
+
+ def test_render_xml_dont_use_cache_contains_nocache(self):
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "dummy",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=True)
+
+ result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [1, 2, 3]}))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'First rendering')
+
+ result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [10, 20, 30]}))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'Next rendering cannot use cache (use_qweb_t_cache is False)')
+
+ def test_render_xml_dont_use_cache_recursive(self):
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "dummy",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=True)
+
+ # use same cache id, display the same content
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': (1, 0),
+ 'cache_id2': (2, 0),
+ 'value': [1, 2, 3],
+ 'value2': [10, 20, 30]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'First rendering')
+
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': (1, 0),
+ 'cache_id2': (2, 1),
+ 'value': [41, 42, 43],
+ 'value2': [51, 52, 53]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'Next rendering cannot use cache (use_qweb_t_cache is False)')
+
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': (1, 1),
+ 'cache_id2': (2, 0),
+ 'value': [31, 32, 33],
+ 'value2': [51, 52, 53]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'Third rendering cannot use cache (use_qweb_t_cache is False)')
+
+ def test_render_xml_dont_use_cache_false_recursive(self):
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "dummy",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=True)
+
+ # use same cache id, display the same content
+ result = etree.fromstring(IrQweb._render(view1.id, {
+ 'cache_id': (1, 0),
+ 'cache_id2': (2, 0),
+ 'value': [1, 2, 3],
+ 'value2': [10, 20, 30]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'First rendering')
+
+ result = etree.fromstring(self.env['ir.qweb']._render(view1.id, {
+ 'cache_id': (1, 0),
+ 'cache_id2': (2, 1),
+ 'value': [41, 42, 43],
+ 'value2': [51, 52, 53]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'Next rendering cannot use cache (use_qweb_t_cache is False)')
+
+ result = etree.fromstring(self.env['ir.qweb']._render(view1.id, {
+ 'cache_id': (1, 1),
+ 'cache_id2': (2, 0),
+ 'value': [31, 32, 33],
+ 'value2': [51, 52, 53]
+ }))
+ self.assertEqual(result, etree.fromstring("""
+
+ """), 'Third rendering cannot use cache (use_qweb_t_cache is False)')
+
+ def test_render_xml_nocache_use_the_root_values(self):
+ template_page = self.env['ir.ui.view'].create({
+ 'name': "template_page",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 1,
+ 'counter': 1,
+ })
+ result = """
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1 (101 != 1: cached t-set should never be applied on root rendering)')
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 1,
+ 'counter': 2,
+ })
+ result = """
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2 (102 != 2: cached t-set should never be applied on root rendering)')
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 3,
+ 'counter': 3,
+ })
+ result = """
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3 (103 != 3: cached t-set should never be applied on root rendering)')
+
+ def test_render_xml_nocache_use_the_root_values_and_cached_values(self):
+ template_page = self.env['ir.ui.view'].create({
+ 'name': "template_page",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 1,
+ 'counter': 1,
+ })
+ result = """
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1 (1 != 101: new cached values should be add to the root rendering)')
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 1,
+ 'counter': 2,
+ })
+ result = """
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2 (102 != 2: cached values should be used)')
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 3,
+ 'counter': 3,
+ })
+ result = """
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3 (3 != 103: new cached values should be add to the root rendering)')
+
+ def test_render_xml_nocache_use_the_root_values_and_cached_values_error(self):
+ template_page = self.env['ir.ui.view'].create({
+ 'name': "template_page",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+
+ with self.assertRaisesRegex(QWebException, "The value type of 't-nocache-record' cannot be cached"):
+ self.env['ir.qweb'].with_context(is_t_cache_disabled=False)._render(template_page.id, {
+ 'cache_id': 1,
+ 'view_record': self.env['ir.ui.view'].search([], limit=1),
+ })
+
+ def test_render_xml_cache_with_t_set_out_of_cache(self):
+ template_page = self.env['ir.ui.view'].create({
+ 'name': "template_page",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+
+
+
+ """
+ })
+
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 1,
+ 'counter': 1,
+ })
+ result = """
+
+
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1 (1 != 101: cached t-set should is applied on first rendering)')
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 1,
+ 'counter': 2,
+ })
+ result = """
+
+
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2 (2 != 102: cached t-set should be applied the template part are rendered every time)')
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 3,
+ 'counter': 3,
+ })
+ result = """
+
+
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3 (3 != 103: cached t-set should applied because the new cache key is created)')
+
+ def test_render_xml_cache_with_t_set_in_cache(self):
+ template_page = self.env['ir.ui.view'].create({
+ 'name': "template_page",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ out of cache:
+
+
+ """
+ })
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 1,
+ 'counter': 1,
+ })
+ result = """
+
+
+ out of cache: 1
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1')
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 1,
+ 'counter': 2,
+ })
+ result = """
+
+
+ out of cache: 2
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2')
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 3,
+ 'counter': 3,
+ })
+ result = """
+
+
+ out of cache: 3
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3')
+
+ def test_render_xml_cache_with_t_set_wrap_t_cache(self):
+ template_page = self.env['ir.ui.view'].create({
+ 'name': "template_page",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+
+
+
+
+
+
+
+
+ """
+ })
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_1': 1,
+ 'cache_2': 1,
+ 'counter': 1,
+ })
+ result = """
+
+
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1')
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_1': 2,
+ 'cache_2': 1,
+ 'counter': 2,
+ })
+ result = """
+
+
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2')
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_1': 2,
+ 'cache_2': 3,
+ 'counter': 3,
+ })
+ result = """
+
+
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3')
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_1': 3,
+ 'cache_2': 3,
+ 'counter': 3,
+ })
+ result = """
+
+
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 4')
+
+ def test_render_xml_t_set_wrap_t_cache(self):
+ template_page = self.env['ir.ui.view'].create({
+ 'name': "template_page",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+
+
+
+
+
+
+
+
+ """
+ })
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 1,
+ 'counter': 1,
+ })
+ result = """
+
+
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1')
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 1,
+ 'counter': 2,
+ })
+ result = """
+
+
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2')
+
+ render = IrQweb._render(template_page.id, {
+ 'cache_id': 3,
+ 'counter': 3,
+ })
+ result = """
+
+
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3')
+
+ def test_render_xml_nocache_in_cache_in_cache(self):
+ template_page = self.env['ir.ui.view'].create({
+ 'name': "template_page",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+
+
+ """
+ })
+
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
+
+ render = IrQweb._render(template_page.id, {
+ 'key1': (1,),
+ 'key2': (1,),
+ 'val': 1,
+ })
+ result = """
+
+
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1')
+
+ render = IrQweb._render(template_page.id, {
+ 'key1': (1,),
+ 'key2': (1,),
+ 'val': 2,
+ })
+ result = """
+
+
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2')
+
+ render = IrQweb._render(template_page.id, {
+ 'key1': (1,),
+ 'key2': (2,),
+ 'val': 3,
+ })
+ result = """
+
+
+
+ """
+ self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 3')
+
+ def test_render_xml_conditional_cache(self):
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "dummy",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+ expected_result = etree.fromstring("""
+
+ """)
+
+ IrQweb = self.env['ir.qweb'].with_context(is_t_cache_disabled=False)
+
+ result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'condition': True, 'value': [1, 2, 3]}))
+ self.assertEqual(result, expected_result, 'First rendering (add in cache)')
+
+ result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'condition': True, 'value': [10, 20, 30]}))
+ self.assertEqual(result, expected_result, 'Next rendering use cache')
+
+
+ expected_result = etree.fromstring("""
+
+ """)
+ result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [10, 20, 30]}))
+ self.assertEqual(result, expected_result, 'Next rendering use cache')
+
+ def test_render_xml_cache_and_inherit_view(self):
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "dummy",
+ 'type': 'qweb',
+ 'arch': """
+
+
+
+ """
+ })
+ # t-cache value can be an interable then we can add value as a tuple (without parenthesis)
+ view2 = self.env['ir.ui.view'].create({
+ 'name': 'Child View',
+ 'mode': 'extension',
+ 'inherit_id': view1.id,
+ 'arch': '''
+
+
+
+ ''',
+ })
+
+ IrQweb = self.env['ir.qweb'].with_context(use_qweb_t_cache=True)
+
+ expected_result = etree.fromstring("""
+
+ """)
+ result = etree.fromstring(IrQweb._render(view2.id, {'value': [1, 2, 3]}))
+ self.assertEqual(result, expected_result, 'First rendering create cache from company and the value 1')
+
+ expected_result = etree.fromstring("""
+
+ """)
+ result = etree.fromstring(IrQweb._render(view2.id, {'value': [10, 20, 30]}))
+ self.assertEqual(result, expected_result, 'Next rendering create cache from company and the value 10')
+
class FileSystemLoader(object):
def __init__(self, path):
# TODO: support multiple files #add_file() + add cache
@@ -1906,13 +3018,9 @@ class TestQWebStaticXml(TransactionCase):
return re.compile(r'\>[ \n\t]*\<').sub('><', text.strip())
result = remove_space(doc.find('result[@id="{}"]'.format(template)).text or u'').replace('"', '"')
- rendering = remove_space(self.env['ir.qweb']._render(template, values=params, load=loader))
- self.assertEqual(rendering, result, template)
-
- rendering_static = None
try:
- rendering_static = remove_space(render(template, params, loader))
+ rendering_static = remove_space(render(template, values=params, load=loader))
self.assertEqual(rendering_static, result, "%s (static rendering)" % template)
except QWebException as e:
if not isinstance(e.__cause__, NotImplementedError) and "Please use \"env['ir.qweb']._render\" method" in str(e):
diff --git a/odoo/exceptions.py b/odoo/exceptions.py
index 18629031999..b4b46063102 100644
--- a/odoo/exceptions.py
+++ b/odoo/exceptions.py
@@ -130,8 +130,3 @@ class Warning(UserError):
def __init__(self, *args, **kwargs):
warnings.warn("Warning is a deprecated alias to UserError.", DeprecationWarning)
super().__init__(*args, **kwargs)
-
-class QWebException(Exception):
- def __init__(self, *args, **kwargs):
- warnings.warn("qweb.QWebException is the exception you are looking for.", DeprecationWarning)
- super().__init__(*args, **kwargs)
diff --git a/odoo/sql_db.py b/odoo/sql_db.py
index a2ec403defc..6a015165d87 100644
--- a/odoo/sql_db.py
+++ b/odoo/sql_db.py
@@ -9,6 +9,7 @@ the ORM does, in fact.
"""
from contextlib import contextmanager
+from datetime import timedelta, datetime
import itertools
import logging
import os
@@ -17,7 +18,6 @@ import threading
import uuid
import warnings
-from datetime import timedelta, datetime
from inspect import currentframe
import psycopg2
import psycopg2.extras
diff --git a/odoo/tools/profiler.py b/odoo/tools/profiler.py
index 2347f2003f1..96619601628 100644
--- a/odoo/tools/profiler.py
+++ b/odoo/tools/profiler.py
@@ -296,17 +296,22 @@ class QwebTracker():
if not self.env.context.get('profile'):
return method_compile(self, template)
- render_template = method_compile(self, template)
- def profiled_method_compile(self, values, gen0=None):
- ref = render_template.options.get('ref')
- ref_xml = render_template.options.get('ref_xml')
+ template_functions, def_name = method_compile(self, template)
+ render_template = template_functions[def_name]
+
+ def profiled_method_compile(self, values):
+ options = template_functions['options']
+ ref = options.get('ref')
+ ref_xml = options.get('ref_xml')
qweb_tracker = QwebTracker(ref, ref_xml, self.env.cr)
self = self.with_context(qweb_tracker=qweb_tracker)
if qweb_tracker.execution_context_enabled:
with ExecutionContext(template=ref):
- return render_template(self, values, gen0)
- return render_template(self, values, gen0)
- return profiled_method_compile
+ return render_template(self, values)
+ return render_template(self, values)
+ template_functions[def_name] = profiled_method_compile
+
+ return (template_functions, def_name)
return _tracked_compile
@classmethod
@@ -315,7 +320,7 @@ class QwebTracker():
def _tracked_compile_directive(self, el, options, directive, level):
if not options.get('profile') or directive in ('inner-content', 'tag-open', 'tag-close'):
return method_compile_directive(self, el, options, directive, level)
- enter = f"{' ' * 4 * level}self.env.context['qweb_tracker'].enter_directive({directive!r}, {el.attrib!r}, {options['last_path_node']!r})"
+ enter = f"{' ' * 4 * level}self.env.context['qweb_tracker'].enter_directive({directive!r}, {el.attrib!r}, {options['_qweb_error_path_xml'][0]!r})"
leave = f"{' ' * 4 * level}self.env.context['qweb_tracker'].leave_directive()"
code_directive = method_compile_directive(self, el, options, directive, level)
return [enter, *code_directive, leave] if code_directive else []