[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
This commit is contained in:
@@ -382,7 +382,7 @@ class Http(models.AbstractModel):
|
||||
# contains branding (<div t-att-data="request.browse('ok')"/>).
|
||||
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
|
||||
|
||||
+395
-155
@@ -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")
|
||||
|
||||
@@ -464,15 +464,15 @@ class TestProfiling(TransactionCase):
|
||||
'key': 'root',
|
||||
'arch_db': '''<t t-name="root">
|
||||
<t t-foreach="{'a': 3, 'b': 2, 'c': 1}" t-as="item">
|
||||
[<t t-esc="item_index"/>: <t t-call="base.dummy" t-set-record="item"/> <t t-esc="item_value"/>]
|
||||
<b t-esc="add_one_query()"/></t>
|
||||
[<t t-out="item_index"/>: <t t-set="record" t-value="item"/><t t-call="base.dummy"/> <t t-out="item_value"/>]
|
||||
<b t-out="add_one_query()"/></t>
|
||||
</t>'''
|
||||
})
|
||||
child_template = self.env['ir.ui.view'].create({
|
||||
'name': 'test',
|
||||
'type': 'qweb',
|
||||
'key': 'dummy',
|
||||
'arch_db': '<t t-name="dummy"><span t-attf-class="myclass"><t t-esc="record"/> <t t-esc="add_one_query()"/></span></t>'
|
||||
'arch_db': '<t t-name="dummy"><span t-attf-class="myclass"><t t-out="record"/> <t t-out="add_one_query()"/></span></t>'
|
||||
})
|
||||
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)
|
||||
|
||||
|
||||
+1291
-183
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+13
-8
@@ -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 []
|
||||
|
||||
Reference in New Issue
Block a user