From 9af92b73749ca835589dcdc56bd0ef9237b99e9a Mon Sep 17 00:00:00 2001 From: Gorash Date: Wed, 29 Dec 2021 13:10:42 +0000 Subject: [PATCH] [IMP] base: Qweb add `t-cache` and `t-nocache` directive 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 `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). Part-of: odoo/odoo#88276 --- addons/website/models/ir_http.py | 2 +- odoo/addons/base/models/ir_qweb.py | 550 ++++++--- odoo/addons/base/tests/test_profiler.py | 48 +- odoo/addons/base/tests/test_qweb.py | 1474 ++++++++++++++++++++--- odoo/exceptions.py | 5 - odoo/sql_db.py | 2 +- odoo/tools/profiler.py | 21 +- 7 files changed, 1725 insertions(+), 377 deletions(-) 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': ''' -
-
- content -
-
-
''' - }) - - 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': ''' -
-
- content -
-
-
''' - }) - - 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': ''' -
-
- content -
-
-
''' - }) - - 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': ''' -
-
- content -
-
-
''' - }) - - 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(""" -
- - - -
1
2
-
- """), '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(""" +
+ + + + +
1
2
3
+
+ """) + + 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(""" +
+ + + + +
1
2
3
+ + + + +
10
20
30
+
+ """), '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(""" +
+ + + + +
41
42
43
+ + + + +
51
52
53
+
+ """), '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(""" +
+ + + + +
1
2
3
+
+ """), '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(""" +
+ + + + +
1
20
3
+
+ """), '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(""" +
+ + + + + + +
1
+ + + + +
10
20
30
+
3
+
+ """), '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(""" +
+ + + + + + +
1
+ + + + +
51
52
53
+
3
+
+ """), '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(""" +
+ + + + + + +
31
+ + + + +
10
20
30
+
33
+
+ """), '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(""" +
+ + + + + + +
1
+ + + + +
10
20
30
+
3
+
+ """), '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(""" +
+ + + + + + +
1
+ + + + +
51
52
53
+
3
+
+ """), '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(""" +
+ + + + + + +
31
+ + + + +
10
20
30
+
33
+
+ """), '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(""" +
+ + + + +
1
2
3
+
+ """), 'First rendering') + + result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [10, 20, 30]})) + self.assertEqual(result, etree.fromstring(""" +
+ + + + +
10
20
30
+
+ """), '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(""" +
+ + + + +
1
2
3
+ + + + +
10
20
30
+
+ """), '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(""" +
+ + + + +
41
42
43
+ + + + +
51
52
53
+
+ """), '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(""" +
+ + + + +
1
2
3
+
+ """), 'First rendering') + + result = etree.fromstring(IrQweb._render(view1.id, {'cache_id': 1, 'value': [10, 20, 30]})) + self.assertEqual(result, etree.fromstring(""" +
+ + + + +
10
20
30
+
+ """), '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(""" +
+ + + + + + +
1
+ + + + +
10
20
30
+
3
+
+ """), '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(""" +
+ + + + + + +
41
+ + + + +
51
52
53
+
43
+
+ """), '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(""" +
+ + + + + + +
31
+ + + + +
51
52
53
+
33
+
+ """), '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(""" +
+ + + + + + +
1
+ + + + +
10
20
30
+
3
+
+ """), '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(""" +
+ + + + + + +
41
+ + + + +
51
52
53
+
43
+
+ """), '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(""" +
+ + + + + + +
31
+ + + + +
51
52
53
+
33
+
+ """), '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': """ + +
+ +
+
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 = """ +
+
1
+
cache: 101
+
+ """ + 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 = """ +
+
2
+
cache: 101
+
+ """ + 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 = """ +
+
3
+
cache: 103
+
+ """ + 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': """ + +
+ +
+
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 = """ +
+
101
+
cache: 101
+
+ """ + 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 = """ +
+
101
+
cache: 101
+
+ """ + 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 = """ +
+
103
+
cache: 103
+
+ """ + 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': """ + + + +
+
+
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 = """ + +
+
1
+
cache: 101
+
+
+ """ + 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 = """ + +
+
2
+
cache: 101
+
+
+ """ + 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 = """ + +
+
3
+
cache: 103
+
+
+ """ + 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': """ + + +
+ +
+
cache:
+
+
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 = """ + +
+
1
+
cache: 101
+
+
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 = """ + +
+
2
+
cache: 101
+
+
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 = """ + +
+
3
+
cache: 103
+
+
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': """ + + + +
+ +
+
cache:
+
+
+
+
+ +
+
+
+ """ + }) + 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 = """ + +
+
+
1
+
cache: 101
+
+
10
+
+
+ """ + 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 = """ + +
+
+
2
+
cache: 101
+
+
20
+
+
+ """ + 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 = """ + +
+
+
2
+
cache: 101
+
+
20
+
+
+ """ + 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 = """ + +
+
+
3
+
cache: 103
+
+
30
+
+
+ """ + 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': """ + + + +
+ +
+
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 = """ + +
+
+
1
+
cache: 101
+
+
10
+
+
+ """ + self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1') + + render = IrQweb._render(template_page.id, { + 'cache_id': 1, + 'counter': 2, + }) + result = """ + +
+
+
2
+
cache: 101
+
+
20
+
+
+ """ + self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2') + + render = IrQweb._render(template_page.id, { + 'cache_id': 3, + 'counter': 3, + }) + result = """ + +
+
+
3
+
cache: 103
+
+
30
+
+
+ """ + 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 = """ + +
+ 1 +
+ 1 +
+
+
+ """ + self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 1') + + render = IrQweb._render(template_page.id, { + 'key1': (1,), + 'key2': (1,), + 'val': 2, + }) + result = """ + +
+ 1 +
+ 2 +
+
+
+ """ + self.assertEqual(etree.fromstring(render), etree.fromstring(result), 'rendering 2') + + render = IrQweb._render(template_page.id, { + 'key1': (1,), + 'key2': (2,), + 'val': 3, + }) + result = """ + +
+ 1 +
+ 3 +
+
+
+ """ + 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(""" +
+ + + + +
1
2
3
+
+ """) + + 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(""" +
+ + + + +
10
20
30
+
+ """) + 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(""" +
+ + + + +
1
2
3
+
+ """) + 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(""" +
+ + + + +
10
20
30
+
+ """) + 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 []