[REF] base: merge view methods _postprocess_view and postprocess, and simplify

This replaces recursive calls by a stack, which makes profiling
flamegraphs much simpler to analyze.

Part-of: odoo/odoo#75796
This commit is contained in:
Raphael Collet
2021-09-06 15:47:01 +00:00
parent 9ae8c1a7c9
commit 331b54770e
2 changed files with 112 additions and 89 deletions
+88 -81
View File
@@ -73,7 +73,7 @@ def transfer_field_to_modifiers(field, modifiers):
modifiers[attr] = default_value
def transfer_node_to_modifiers(node, modifiers, context=None, current_node_path=None):
def transfer_node_to_modifiers(node, modifiers, context=None):
# Don't deal with groups, it is done by check_group().
# Need the context to evaluate the invisible attribute on tree views.
# For non-tree views, the context shouldn't be given.
@@ -92,8 +92,9 @@ def transfer_node_to_modifiers(node, modifiers, context=None, current_node_path=
value_str = node.get(attr)
if value_str:
value = bool(quick_eval(value_str, {'context': context or {}}))
node_path = current_node_path or ()
if 'tree' in node_path and 'header' not in node_path and attr == 'invisible':
if (attr == 'invisible'
and any(parent.tag == 'tree' for parent in node.iterancestors())
and not any(parent.tag == 'header' for parent in node.iterancestors())):
# Invisible in a tree view has a specific meaning, make it a
# new key in the modifiers attribute.
modifiers['column_invisible'] = value
@@ -950,43 +951,94 @@ actual arch.
:param self: the view to postprocess
:param node: the architecture as an etree
:param model: the view's reference model
:param model: the view's reference model name
:param validate: whether the view must be validated
:return: a tuple (arch, fields) where arch is the given node as a
string and fields is the description of all the fields.
"""
self and self.ensure_one() # self is at most one view
if self:
self.ensure_one()
model = model or self.model
name_manager = self._postprocess_view(node, model or self.model, validate=validate)
arch, name_manager = self._postprocess_view(node, model, validate=validate)
# name_manager.final_check()
arch = etree.tostring(node, encoding="unicode").replace('\t', '')
return arch, name_manager.available_fields
def _postprocess_view(self, node, model, validate=True, editable=True):
def _postprocess_view(self, node, model_name, validate=True, editable=True):
""" Process the given architecture, modifying it in-place to add and
remove stuff.
if model not in self.env:
self._raise_view_error(_('Model not found: %(model)s', model=model), node)
:param self: the optional view to postprocess
:param node: the combined architecture as an etree
:param model_name: the view's reference model name
:param validate: whether the view must be validated
:param editable: whether the view is considered editable
:return: the processed architecture's NameManager
"""
root = node
if model_name not in self.env:
self._raise_view_error(_('Model not found: %(model)s', model=model_name), root)
model = self.env[model_name]
if not validate:
# validation mode does not care about on_change
self._postprocess_on_change(model, node)
self._postprocess_on_change(root, model)
name_manager = NameManager(validate, self.env[model])
self.postprocess(node, [], editable, name_manager)
name_manager = NameManager(validate, model)
name_manager.check_view_fields(self)
name_manager.update_view_fields()
# tree, form, graph, etc.
VIEW_TAGS = {item[0] for item in type(self).type.selection}
if not validate:
# validation mode does not care about access rights
self._postprocess_access_rights(model, node)
# use a stack to recursively traverse the tree
stack = [(root, editable)]
while stack:
node, editable = stack.pop()
return etree.tostring(node, encoding="unicode").replace('\t', ''), name_manager
# compute default
tag = node.tag
parent = node.getparent()
node_info = {
'modifiers': {},
'attr_model': name_manager.Model,
'editable': editable,
}
def _postprocess_on_change(self, model_name, arch):
# tag-specific postprocessing
postprocessor = getattr(self, f"_postprocess_tag_{tag}", None)
if postprocessor is not None:
postprocessor(node, name_manager, node_info)
if node.getparent() is not parent:
# the node has been removed, stop processing here
continue
elif node.tag in VIEW_TAGS:
node_info['editable'] = False
if name_manager.validate:
# tag-specific validation
validator = getattr(self, f"_validate_tag_{tag}", None)
if validator is not None:
validator(node, name_manager, node_info)
self._validate_attrs(node, name_manager, node_info)
self._apply_groups(node, name_manager, node_info)
transfer_node_to_modifiers(node, node_info['modifiers'], self._context)
transfer_modifiers_to_node(node_info['modifiers'], node)
# if present, iterate on node_info['children'] instead of node
for child in reversed(node_info.get('children', node)):
stack.append((child, node_info['editable']))
if validate:
name_manager.check_view_fields(self)
else:
# validation does not care about fields_get and access rights
name_manager.update_view_fields()
self._postprocess_access_rights(root, model.sudo(False))
return name_manager
def _postprocess_on_change(self, arch, model):
""" Add attribute on_change="1" on fields that are dependencies of
computed fields on the same view.
"""
@@ -1003,7 +1055,7 @@ actual arch.
for child in node:
collect(child, model)
collect(arch, self.env[model_name])
collect(arch, model)
for field, nodes in field_nodes.items():
# if field should trigger an onchange, add on_change="1" on the
@@ -1014,75 +1066,31 @@ actual arch.
if not node.get('on_change'):
node.set('on_change', '1')
def _postprocess_access_rights(self, model, node):
def _postprocess_access_rights(self, node, model):
""" Compute and set on node access rights based on view type. Specific
views can add additional specific rights like creating columns for
many2one-based grouping views. """
# testing ACL as real user
Model = self.env[model].sudo(False)
is_base_model = self.env.context.get('base_model_name', model) == model
is_base_model = self.env.context.get('base_model_name', model._name) == model._name
if node.tag in ('kanban', 'tree', 'form', 'activity'):
for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
if (not node.get(action) and
not Model.check_access_rights(operation, raise_exception=False) or
not model.check_access_rights(operation, raise_exception=False) or
not self._context.get(action, True) and is_base_model):
node.set(action, 'false')
if node.tag == 'kanban':
group_by_name = node.get('default_group_by')
group_by_field = Model._fields.get(group_by_name)
group_by_field = model._fields.get(group_by_name)
if group_by_field and group_by_field.type == 'many2one':
group_by_model = Model.env[group_by_field.comodel_name]
group_by_model = model.env[group_by_field.comodel_name]
for action, operation in (('group_create', 'create'), ('group_delete', 'unlink'), ('group_edit', 'write')):
if (not node.get(action) and
not group_by_model.check_access_rights(operation, raise_exception=False) or
not self._context.get(action, True) and is_base_model):
node.set(action, 'false')
def postprocess(self, node, current_node_path, editable, name_manager):
""" Process the given arch node, which may be the complete arch or some
subnode, and fill in the name manager with field information.
"""
stack = [(node, current_node_path, editable)]
while stack:
node, current_node_path, editable = stack.pop()
# compute default
tag = node.tag
parent = node.getparent()
node_info = dict(
modifiers={},
attr_model=name_manager.Model,
editable=editable,
)
current_node_path = current_node_path + [tag]
postprocessor = getattr(self, '_postprocess_tag_%s' % tag, False)
if postprocessor:
postprocessor(node, name_manager, node_info)
if node.getparent() is not parent:
# the node has been removed, stop processing here
continue
elif tag in {item[0] for item in type(self.env['ir.ui.view']).type.selection}:
node_info['editable'] = False
if name_manager.validate:
# structure validation
validator = getattr(self, '_validate_tag_%s' % tag, False)
if validator:
validator(node, name_manager, node_info)
self._validate_attrs(node, name_manager, node_info)
self._apply_groups(node, name_manager, node_info)
transfer_node_to_modifiers(node, node_info['modifiers'], self._context, current_node_path)
transfer_modifiers_to_node(node_info['modifiers'], node)
# if present, iterate on node_info['children'] instead of node
for child in reversed(node_info.get('children', node)):
stack.append((child, current_node_path, node_info['editable']))
#------------------------------------------------------
# Specific node postprocessors
#------------------------------------------------------
@@ -1117,24 +1125,25 @@ actual arch.
for child in node:
if child.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
node.remove(child)
xarch, sub_name_manager = self.with_context(
sub_name_manager = self.with_context(
base_model_name=name_manager.Model._name,
)._postprocess_view(
child, field.comodel_name, name_manager.validate,
editable=node_info['editable'],
)
name_manager.must_have_fields(sub_name_manager.mandatory_parent_fields)
xarch = etree.tostring(child, encoding="unicode").replace('\t', '')
views[child.tag] = {
'arch': xarch,
'fields': sub_name_manager.available_fields,
}
attrs['views'] = views
if field.comodel_name in self.env:
Comodel = self.env[field.comodel_name].sudo(False)
node_info['attr_model'] = Comodel
comodel = self.env[field.comodel_name].sudo(False)
node_info['attr_model'] = comodel
if field.type in ('many2one', 'many2many'):
can_create = Comodel.check_access_rights('create', raise_exception=False)
can_write = Comodel.check_access_rights('write', raise_exception=False)
can_create = comodel.check_access_rights('create', raise_exception=False)
can_write = comodel.check_access_rights('write', raise_exception=False)
node.set('can_create', 'true' if can_create else 'false')
node.set('can_write', 'true' if can_write else 'false')
@@ -1161,9 +1170,10 @@ actual arch.
node.remove(child)
groupby_node.append(child)
# validate the new node as a nested view, and associate it to the field
xarch, sub_name_manager = self.with_context(
sub_name_manager = self.with_context(
base_model_name=name_manager.Model._name,
)._postprocess_view(groupby_node, field.comodel_name, name_manager.validate, editable=False)
xarch = etree.tostring(groupby_node, encoding="unicode").replace('\t', '')
name_manager.has_field(name, {'views': {
'groupby': {
'arch': xarch,
@@ -2074,9 +2084,6 @@ class NameManager:
_logger.error("All parent.field should have been consummed at root level. \n %s", '\n'.join(msg))
def check_view_fields(self, view):
if not self.validate:
return
for action, use in self.mandatory_names_or_ids.items():
if action not in self.available_actions and action not in self.available_names_or_ids:
msg = _(
+24 -8
View File
@@ -1396,15 +1396,13 @@ class TestViews(ViewCase):
def test_modifiers(self):
def _test_modifiers(what, expected):
modifiers = {}
if isinstance(what, str):
node = etree.fromstring(what)
transfer_node_to_modifiers(node, modifiers)
simplify_modifiers(modifiers)
assert modifiers == expected, "%s != %s" % (modifiers, expected)
elif isinstance(what, dict):
if isinstance(what, dict):
transfer_field_to_modifiers(what, modifiers)
simplify_modifiers(modifiers)
assert modifiers == expected, "%s != %s" % (modifiers, expected)
else:
node = etree.fromstring(what) if isinstance(what, str) else what
transfer_node_to_modifiers(node, modifiers)
simplify_modifiers(modifiers)
assert modifiers == expected, "%s != %s" % (modifiers, expected)
_test_modifiers('<field name="a"/>', {})
_test_modifiers('<field name="a" invisible="1"/>', {"invisible": True})
@@ -1431,6 +1429,24 @@ class TestViews(ViewCase):
{"invisible": [["b", "=", "c"]]},
)
# fields in a tree view
tree = etree.fromstring('''
<tree>
<header>
<button name="a" invisible="1"/>
</header>
<field name="a"/>
<field name="a" invisible="0"/>
<field name="a" invisible="1"/>
<field name="a" attrs="{'invisible': [['b', '=', 'c']]}"/>
</tree>
''')
_test_modifiers(tree[0][0], {"invisible": True})
_test_modifiers(tree[1], {})
_test_modifiers(tree[2], {"column_invisible": False})
_test_modifiers(tree[3], {"column_invisible": True})
_test_modifiers(tree[4], {"invisible": [['b', '=', 'c']]})
# The dictionary is supposed to be the result of fields_get().
_test_modifiers({}, {})
_test_modifiers({"invisible": True}, {"invisible": True})