[IMP] base: refactor NameManager in views

Also speed up view validation by not calling fields_get when possible.
This avoids a call that is usually not necessary for validating a view.
Most of the time, fields can be found on the model itself.  Only
overridden fields_get() with fake fields are actually useful, like the
one of model 'res.users'.

The time spent in method _check_xml() to install a database from scratch with
module sale_management went from 3.510s to 2.730s (-22%), and the number
of queries made in the method went from 2995 to 2283 (-24%).

Part-of: odoo/odoo#75796
This commit is contained in:
Raphael Collet
2021-09-06 15:47:03 +00:00
parent eda3352cdd
commit c70c97b127
+90 -93
View File
@@ -29,7 +29,7 @@ from odoo.modules.module import get_resource_from_path, get_resource_path
from odoo.tools import config, ConstantMapping, get_diff, pycompat, apply_inheritance_specs, locate_node
from odoo.tools.convert import _fix_multiple_roots
from odoo.tools.json import scriptsafe as json_scriptsafe
from odoo.tools import safe_eval
from odoo.tools import safe_eval, lazy_property, frozendict
from odoo.tools.view_validation import valid_view, get_variable_names, get_domain_identifiers, get_dict_asts
from odoo.tools.translate import xml_translate, TRANSLATED_ATTRS
from odoo.tools.image import image_data_uri
@@ -967,7 +967,7 @@ actual arch.
name_manager = self._postprocess_view(node, model or self.model)
arch = etree.tostring(node, encoding="unicode").replace('\t', '')
return arch, name_manager.available_fields
return arch, dict(name_manager.available_fields)
def _postprocess_view(self, node, model_name, editable=True):
""" Process the given architecture, modifying it in-place to add and
@@ -987,7 +987,7 @@ actual arch.
self._postprocess_on_change(root, model)
name_manager = NameManager(False, model)
name_manager = NameManager(model)
# use a stack to recursively traverse the tree
stack = [(root, editable)]
@@ -1018,7 +1018,7 @@ actual arch.
for child in reversed(node_info.get('children', node)):
stack.append((child, node_info['editable']))
name_manager.update_view_fields()
name_manager.update_available_fields()
self._postprocess_access_rights(root, model.sudo(False))
return name_manager
@@ -1082,7 +1082,7 @@ actual arch.
def _postprocess_tag_calendar(self, node, name_manager, node_info):
for additional_field in ('date_start', 'date_delay', 'date_stop', 'color', 'all_day'):
if node.get(additional_field):
name_manager.has_field(node.get(additional_field).split('.', 1)[0], {})
name_manager.has_field(node.get(additional_field).split('.', 1)[0])
for f in node:
if f.tag == 'filter':
name_manager.has_field(f.get('name'))
@@ -1090,7 +1090,7 @@ actual arch.
def _postprocess_tag_field(self, node, name_manager, node_info):
if node.get('name'):
attrs = {'id': node.get('id'), 'select': node.get('select')}
field = name_manager.Model._fields.get(node.get('name'))
field = name_manager.model._fields.get(node.get('name'))
if field:
# apply groups (no tested)
if field.groups and not self.user_has_groups(groups=field.groups):
@@ -1102,14 +1102,14 @@ actual arch.
if child.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'):
node.remove(child)
sub_name_manager = self.with_context(
base_model_name=name_manager.Model._name,
base_model_name=name_manager.model._name,
)._postprocess_view(
child, field.comodel_name, editable=node_info['editable'],
)
xarch = etree.tostring(child, encoding="unicode").replace('\t', '')
views[child.tag] = {
'arch': xarch,
'fields': sub_name_manager.available_fields,
'fields': dict(sub_name_manager.available_fields),
}
attrs['views'] = views
if field.type in ('many2one', 'many2many'):
@@ -1121,12 +1121,12 @@ actual arch.
name_manager.has_field(node.get('name'), attrs)
field = name_manager.fields_get.get(node.get('name'))
if field:
transfer_field_to_modifiers(field, node_info['modifiers'])
field_info = name_manager.field_info.get(node.get('name'))
if field_info:
transfer_field_to_modifiers(field_info, node_info['modifiers'])
def _postprocess_tag_form(self, node, name_manager, node_info):
result = name_manager.Model.view_header_get(False, node.tag)
result = name_manager.model.view_header_get(False, node.tag)
if result:
node.set('string', result)
@@ -1134,26 +1134,26 @@ actual arch.
# groupby nodes should be considered as nested view because they may
# contain fields on the comodel
name = node.get('name')
field = name_manager.Model._fields.get(name)
field = name_manager.model._fields.get(name)
if not field or not field.comodel_name:
return
# move all children nodes into a new node <groupby>
groupby_node = E.groupby(*node)
# post-process the node as a nested view, and associate it to the field
sub_name_manager = self.with_context(
base_model_name=name_manager.Model._name,
base_model_name=name_manager.model._name,
)._postprocess_view(groupby_node, field.comodel_name, editable=False)
xarch = etree.tostring(groupby_node, encoding="unicode").replace('\t', '')
name_manager.has_field(name, {'views': {
'groupby': {
'arch': xarch,
'fields': sub_name_manager.available_fields,
'fields': dict(sub_name_manager.available_fields),
}
}})
def _postprocess_tag_label(self, node, name_manager, node_info):
if node.get('for'):
field = name_manager.Model._fields.get(node.get('for'))
field = name_manager.model._fields.get(node.get('for'))
if field and field.groups and not self.user_has_groups(groups=field.groups):
node.getparent().remove(node)
@@ -1161,9 +1161,9 @@ actual arch.
searchpanel = [child for child in node if child.tag == 'searchpanel']
if searchpanel:
self.with_context(
base_model_name=name_manager.Model._name,
base_model_name=name_manager.model._name,
)._postprocess_view(
searchpanel[0], name_manager.Model._name, editable=False,
searchpanel[0], name_manager.model._name, editable=False,
)
node_info['children'] = [child for child in node if child.tag != 'searchpanel']
@@ -1190,7 +1190,7 @@ actual arch.
return node.get('editable')
def _editable_tag_field(self, node, name_manager):
field = name_manager.Model._fields.get(node.get('name'))
field = name_manager.model._fields.get(node.get('name'))
return field is None or field.is_editable() and (
node.get('readonly') not in ('1', 'True')
or get_dict_asts(node.get('attrs') or "{}")
@@ -1215,8 +1215,9 @@ actual arch.
if model_name not in self.env:
self._raise_view_error(_('Model not found: %(model)s', model=model_name), node)
model = self.env[model_name]
name_manager = NameManager(True, model)
# fields_get() optimization: validation does not require translations
model = self.env[model_name].with_context(lang=None)
name_manager = NameManager(model)
# use a stack to recursively traverse the tree
stack = [(node, editable)]
@@ -1239,7 +1240,7 @@ actual arch.
for child in reversed(node):
stack.append((child, node_info['editable']))
name_manager.check_view_fields(self)
name_manager.check(self)
return name_manager
@@ -1270,10 +1271,10 @@ actual arch.
def _validate_tag_calendar(self, node, name_manager, node_info):
for additional_field in ('date_start', 'date_delay', 'date_stop', 'color', 'all_day'):
if node.get(additional_field):
name_manager.has_field(node.get(additional_field).split('.', 1)[0], {})
name_manager.has_field(node.get(additional_field).split('.', 1)[0])
for f in node:
if f.tag == 'filter':
name_manager.has_field(f.get('name'), {})
name_manager.has_field(f.get('name'))
def _validate_tag_search(self, node, name_manager, node_info):
if not list(node.iterdescendants(tag="field")):
@@ -1287,14 +1288,14 @@ actual arch.
if len(searchpanels) > 1:
self._raise_view_error(_('Search tag can only contain one search panel'), node)
node.remove(searchpanels[0])
self._validate_view(searchpanels[0], name_manager.Model._name, editable=False)
self._validate_view(searchpanels[0], name_manager.model._name, editable=False)
def _validate_tag_field(self, node, name_manager, node_info):
name = node.get('name')
if not name:
self._raise_view_error(_("Field tag must have a \"name\" attribute defined"), node)
field = name_manager.Model._fields.get(name)
field = name_manager.model._fields.get(name)
if field:
if field.relational:
domain = (
@@ -1309,7 +1310,7 @@ actual arch.
fnames, vnames = self._get_domain_identifiers(node, domain, desc)
self._check_field_paths(node, fnames, field.comodel_name, f"{desc} ({domain})")
if vnames:
name_manager.must_have_fields(dict.fromkeys(vnames, f"{desc} ({domain})"))
name_manager.must_have_fields(vnames, f"{desc} ({domain})")
elif node.get('domain'):
msg = _(
@@ -1323,12 +1324,13 @@ actual arch.
continue
node.remove(child)
sub_manager = self._validate_view(child, field.comodel_name, node_info['editable'])
name_manager.must_have_fields(sub_manager.mandatory_parent_fields)
for fname, use in sub_manager.mandatory_parent_fields.items():
name_manager.must_have_field(fname, use)
elif name not in name_manager.fields_get:
elif name not in name_manager.field_info:
msg = _(
'Field "%(field_name)s" does not exist in model "%(model_name)s"',
field_name=name, model_name=name_manager.Model._name,
field_name=name, model_name=name_manager.model._name,
)
self._raise_view_error(msg, node)
@@ -1351,9 +1353,9 @@ actual arch.
name = node.get('name')
desc = f'domain of <filter name="{name}">' if name else 'domain of <filter>'
fnames, vnames = self._get_domain_identifiers(node, domain, desc)
self._check_field_paths(node, fnames, name_manager.Model._name, f"{desc} ({domain})")
self._check_field_paths(node, fnames, name_manager.model._name, f"{desc} ({domain})")
if vnames:
name_manager.must_have_fields(dict.fromkeys(vnames, f"{desc} ({domain})"))
name_manager.must_have_fields(vnames, f"{desc} ({domain})")
def _validate_tag_button(self, node, name_manager, node_info):
name = node.get('name')
@@ -1368,11 +1370,11 @@ actual arch.
elif not name:
self._raise_view_error(_("Button must have a name"), node)
elif type_ == 'object':
func = getattr(type(name_manager.Model), name, None)
func = getattr(type(name_manager.model), name, None)
if not func:
msg = _(
"%(action_name)s is not a valid action on %(model_name)s",
action_name=name, model_name=name_manager.Model._name,
action_name=name, model_name=name_manager.model._name,
)
self._raise_view_error(msg, node)
try:
@@ -1380,14 +1382,14 @@ actual arch.
except AccessError:
msg = _(
"%(method)s on %(model)s is private and cannot be called from a button",
method=name, model=name_manager.Model._name,
method=name, model=name_manager.model._name,
)
self._raise_view_error(msg, node)
try:
inspect.signature(func).bind(self=name_manager.Model)
inspect.signature(func).bind(self=name_manager.model)
except TypeError:
msg = "%s on %s has parameters and cannot be called from a button"
self._log_view_warning(msg % (name, name_manager.Model._name), node)
self._log_view_warning(msg % (name, name_manager.model._name), node)
elif type_ == 'action':
# logic mimics /web/action/load behaviour
action = False
@@ -1423,7 +1425,7 @@ actual arch.
name = node.get('name')
if not name:
return
field = name_manager.Model._fields.get(name)
field = name_manager.model._fields.get(name)
if field:
if field.type != 'many2one':
msg = _(
@@ -1437,18 +1439,19 @@ actual arch.
fnames, vnames = self._get_domain_identifiers(node, domain, desc)
self._check_field_paths(node, fnames, field.comodel_name, f"{desc} ({domain})")
if vnames:
name_manager.must_have_fields(dict.fromkeys(vnames, f"{desc} ({domain})"))
name_manager.must_have_fields(vnames, f"{desc} ({domain})")
# move all children nodes into a new node <groupby>
groupby_node = E.groupby(*node)
# validate the node as a nested view
sub_manager = self._validate_view(groupby_node, field.comodel_name, editable=False)
name_manager.has_field(name, {})
name_manager.must_have_fields(sub_manager.mandatory_parent_fields)
name_manager.has_field(name)
for fname, use in sub_manager.mandatory_parent_fields.items():
name_manager.must_have_field(fname, use)
else:
msg = _(
"Field '%(field)s' found in 'groupby' node does not exist in model %(model)s",
field=name, model=name_manager.Model._name,
field=name, model=name_manager.model._name,
)
self._raise_view_error(msg, node)
@@ -1466,7 +1469,7 @@ actual arch.
'without corresponding field or button, use \'class="o_form_label"\'.')
self._raise_view_error(msg, node)
else:
name_manager.must_have_name_or_id(for_, '<label for="...">')
name_manager.must_have_name(for_, '<label for="...">')
def _validate_tag_page(self, node, name_manager, node_info):
if node.getparent() is None or node.getparent().tag != 'notebook':
@@ -1528,12 +1531,11 @@ actual arch.
# domains in attrs are used for readonly, invisible, ...
# and thus are only executed client side
fnames, vnames = self._get_domain_identifiers(node, val_ast, attr, expr)
fields = fnames | vnames
name_manager.must_have_fields(dict.fromkeys(fields, f"attrs ({expr})"))
name_manager.must_have_fields(fnames | vnames, f"attrs ({expr})")
else:
vnames = get_variable_names(val_ast)
if vnames:
name_manager.must_have_fields(dict.fromkeys(vnames, f"attrs ({expr})"))
name_manager.must_have_fields(vnames, f"attrs ({expr})")
elif attr == 'context':
for key, val_ast in get_dict_asts(expr).items():
@@ -1546,7 +1548,7 @@ actual arch.
self._raise_view_error(msg, node)
group_by = val_ast.s
fname = group_by.split(':')[0]
if fname not in name_manager.Model._fields:
if fname not in name_manager.model._fields:
msg = _(
'Unknown field "%(field)s" in "group_by" value in %(attribute)s=%(value)r',
field=fname, attribute=attr, value=expr,
@@ -1555,7 +1557,7 @@ actual arch.
else:
vnames = get_variable_names(val_ast)
if vnames:
name_manager.must_have_fields(dict.fromkeys(vnames, f"context ({expr})"))
name_manager.must_have_fields(vnames, f"context ({expr})")
elif attr == 'groups':
for group in expr.replace('!', '').split(','):
@@ -1578,7 +1580,7 @@ actual arch.
elif attr.startswith('decoration-'):
vnames = get_variable_names(expr)
if vnames:
name_manager.must_have_fields(dict.fromkeys(vnames, f"{attr}={expr}"))
name_manager.must_have_fields(vnames, f"{attr}={expr}")
elif attr == 'data-toggle' and expr == 'tab':
if node.get('role') != 'tab':
@@ -2113,21 +2115,22 @@ class ResetViewArchWizard(models.TransientModel):
class NameManager:
""" An object that manages all the named elements in a view. """
def __init__(self, validate, Model):
self.available_fields = dict()
self.mandatory_fields = dict()
self.mandatory_parent_fields = dict()
def __init__(self, model):
self.model = model
self.available_fields = collections.defaultdict(dict) # {field_name: field_info}
self.available_actions = set()
self.mandatory_names_or_ids = dict()
self.available_names_or_ids = set()
self.validate = validate
# optimization: post-processing requires translations, but validation does not
self.Model = Model.with_context(lang=False) if validate else Model
self.fields_get = self.Model.fields_get()
self.available_names = set()
self.mandatory_fields = dict() # {field_name: use}
self.mandatory_parent_fields = dict() # {field_name: use}
self.mandatory_names = dict() # {name: use}
def has_field(self, name, info=()):
self.available_fields.setdefault(name, {}).update(info)
self.available_names_or_ids.add(info.get('id') or name)
@lazy_property
def field_info(self):
return self.model.fields_get()
def has_field(self, name, info=frozendict()):
self.available_fields[name].update(info)
self.available_names.add(info.get('id') or name)
def has_action(self, name):
self.available_actions.add(name)
@@ -2138,59 +2141,53 @@ class NameManager:
else:
self.mandatory_fields[name] = use
def must_have_fields(self, name_uses):
for name, use in name_uses.items():
def must_have_fields(self, names, use):
for name in names:
self.must_have_field(name, use)
def must_have_name_or_id(self, name, use):
self.mandatory_names_or_ids[name] = use
def must_have_name(self, name, use):
self.mandatory_names[name] = use
def final_check(self):
if self.mandatory_fields:
msg = []
for field in self.mandatory_fields:
msg.append(str(field))
_logger.error("All parent.field should have been consummed at root level. \n %s", '\n'.join(msg))
def check(self, view):
# context for translations below
context = view.env.context # pylint: disable=unused-variable
def check_view_fields(self, view):
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:
for name, use in self.mandatory_names.items():
if name not in self.available_actions and name not in self.available_names:
msg = _(
"Name or id %(name_or_id)r in %(use)s must be present in view but is missing.",
name_or_id=action, use=use,
name_or_id=name, use=use,
)
view._raise_view_error(msg)
for field_name in self.available_fields:
if field_name not in self.fields_get:
message = _("Field `%(name)s` does not exist", name=field_name)
for name in self.available_fields:
if name not in self.model._fields and name not in self.field_info:
message = _("Field `%(name)s` does not exist", name=name)
view._raise_view_error(message)
for field, use in self.mandatory_fields.items():
if field == 'id': # always available
for name, use in self.mandatory_fields.items():
if name == 'id': # always available
continue
if "." in field:
if "." in name:
msg = _(
"Invalid composed field %(definition)s in %(use)s",
definition=field, use=use,
definition=name, use=use,
)
view._raise_view_error(msg)
corresponding_field = self.available_fields.get(str(field))
if corresponding_field is None:
info = self.available_fields.get(name)
if info is None:
msg = _(
"Field %(name)r used in %(use)s must be present in view but is missing.",
name=field, use=use,
name=name, use=use,
)
view._raise_view_error(msg)
if corresponding_field.get('select') == 'multi': # mainly for searchpanel, but can be a generic behaviour.
if info.get('select') == 'multi': # mainly for searchpanel, but can be a generic behaviour.
msg = _(
"Field %(name)r used in %(use)s is present in view but is in select multi.",
name=field, use=use,
name=name, use=use,
)
view._raise_view_error(msg)
def update_view_fields(self):
for field_name, field_infos in self.available_fields.items():
model_field_infos = self.fields_get.get(field_name)
if model_field_infos:
field_infos.update(model_field_infos)
def update_available_fields(self):
for name, info in self.available_fields.items():
info.update(self.field_info.get(name, ()))