[REF] base: split method handle_view_error() into _raise_view_error() and _log_view_warning()

This improves code readability but does not impact performance.

Part-of: odoo/odoo#75796
This commit is contained in:
Raphael Collet
2021-09-06 15:47:01 +00:00
parent 1de6c4e55a
commit 9ae8c1a7c9
2 changed files with 257 additions and 391 deletions
+100 -100
View File
@@ -384,7 +384,7 @@ actual arch.
match = TRANSLATED_ATTRS_RE.search(node.get('expr', ''))
if match:
message = "View inheritance may not use attribute %r as a selector." % match.group(1)
self.handle_view_error(message, node)
self._raise_view_error(message, node)
if WRONGCLASS.search(node.get('expr', '')):
_logger.warning(
"Error-prone use of @class in view %s (%s): use the "
@@ -395,7 +395,7 @@ actual arch.
for attr in TRANSLATED_ATTRS:
if node.get(attr):
message = "View inheritance may not use attribute %r as a selector." % attr
self.handle_view_error(message, node)
self._raise_view_error(message, node)
return True
@api.constrains('arch_db')
@@ -700,14 +700,12 @@ actual arch.
error = _("View '%(name)s' is private", name=self.key)
raise AccessError(error)
def handle_view_error(self, message, failed_node, *, raise_exception=True, from_exception=None, from_traceback=None):
""" Handle a view error by raising an exception or logging a warning,
depending on the value of `raise_exception`.
def _raise_view_error(self, message, node=None, *, from_exception=None, from_traceback=None):
""" Handle a view error by raising an exception.
:param str message: message to raise or log, augmented with contextual
view information
:param bool raise_exception:
whether to raise an exception (the default) or just log a warning
:param node: the lxml element where the error is located (if any)
:param BaseException from_exception:
when raising an exception, chain it to the provided one (default:
disable chaining)
@@ -715,6 +713,25 @@ actual arch.
when raising an exception, start with this traceback (default: start
at exception creation)
"""
err = ValueError(message).with_traceback(from_traceback)
err.context = {
'view': self,
'name': getattr(self, 'name', None),
'xmlid': self.env.context.get('install_xmlid') or self.xml_id,
'view.model': self.model,
'view.parent': self.inherit_id,
'file': self.env.context.get('install_filename'),
'line': node.sourceline if node is not None else 1,
}
raise err from from_exception
def _log_view_warning(self, message, node):
""" Handle a view issue by logging a warning.
:param str message: message to raise or log, augmented with contextual
view information
:param node: the lxml element where the error is located (if any)
"""
error_context = {
'view': self,
'name': getattr(self, 'name', None),
@@ -722,13 +739,8 @@ actual arch.
'view.model': self.model,
'view.parent': self.inherit_id,
'file': self.env.context.get('install_filename'),
'line': failed_node.sourceline if failed_node is not None else 1,
'line': node.sourceline if node is not None else 1,
}
if raise_exception:
err = ValueError(message).with_traceback(from_traceback)
err.context = error_context
raise err from from_exception
_logger.warning(
"%s\nView error context:\n%s",
message, pprint.pformat(error_context)
@@ -788,7 +800,7 @@ actual arch.
pre_locate=pre_locate,
)
except ValueError as e:
self.handle_view_error(str(e), specs_tree)
self._raise_view_error(str(e), specs_tree)
return source
def _combine(self, hierarchy: dict):
@@ -956,7 +968,7 @@ actual arch.
def _postprocess_view(self, node, model, validate=True, editable=True):
if model not in self.env:
self.handle_view_error(_('Model not found: %(model)s', model=model), node)
self._raise_view_error(_('Model not found: %(model)s', model=model), node)
if not validate:
# validation mode does not care about on_change
@@ -1187,7 +1199,7 @@ actual arch.
def _validate_tag_field(self, node, name_manager, node_info):
name = node.get('name')
if not name:
self.handle_view_error(_("Field tag must have a \"name\" attribute defined"), node)
self._raise_view_error(_("Field tag must have a \"name\" attribute defined"), node)
field = name_manager.Model._fields.get(name)
if not field and name in name_manager.fields_get:
return
@@ -1196,13 +1208,13 @@ actual arch.
'Field "%(field_name)s" does not exist in model "%(model_name)s"',
field_name=name, model_name=name_manager.Model._name,
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
if node.get('domain') and field.comodel_name not in self.env:
msg = _(
'Domain on non-relational field "%(name)s" makes no sense (domain:%(domain)s)',
name=name, domain=node.get('domain'),
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
for attribute in ('invisible', 'readonly', 'required'):
val = node.get(attribute)
@@ -1213,7 +1225,7 @@ actual arch.
'Attribute %(attribute)s evaluation expects a boolean, got %(value)s',
attribute=attribute, value=val,
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
def _validate_tag_button(self, node, name_manager, node_info):
name = node.get('name')
@@ -1221,12 +1233,12 @@ actual arch.
type_ = node.get('type')
if special:
if special not in ('cancel', 'save', 'add'):
self.handle_view_error(_("Invalid special '%(value)s' in button", value=special), node)
self._raise_view_error(_("Invalid special '%(value)s' in button", value=special), node)
elif type_:
if type_ == 'edit': # list_renderer, used in kanban view
return
elif not name:
self.handle_view_error(_("Button must have a name"), node)
self._raise_view_error(_("Button must have a name"), node)
elif type_ == 'object':
func = getattr(type(name_manager.Model), name, None)
if not func:
@@ -1234,7 +1246,7 @@ actual arch.
"%(action_name)s is not a valid action on %(model_name)s",
action_name=name, model_name=name_manager.Model._name,
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
try:
check_method_name(name)
except AccessError:
@@ -1242,12 +1254,12 @@ actual arch.
"%(method)s on %(model)s is private and cannot be called from a button",
method=name, model=name_manager.Model._name,
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
try:
inspect.signature(func).bind(self=name_manager.Model)
except TypeError:
msg = "%s on %s has parameters and cannot be called from a button"
self.handle_view_error(msg % (name, name_manager.Model._name), node, raise_exception=False)
self._log_view_warning(msg % (name, name_manager.Model._name), node)
elif type_ == 'action':
# logic mimics /web/action/load behaviour
action = False
@@ -1257,20 +1269,20 @@ actual arch.
model, action_id = self.env['ir.model.data']._xmlid_to_res_model_res_id(name, raise_if_not_found=False)
if not action_id:
msg = _("Invalid xmlid %(xmlid)s for button of type action.", xmlid=name)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
if not issubclass(self.pool[model], self.pool['ir.actions.actions']):
msg = _(
"%(xmlid)s is of type %(xmlid_model)s, expected a subclass of ir.actions.actions",
xmlid=name, xmlid_model=model,
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
action = self.env['ir.actions.actions'].browse(action_id).exists()
if not action:
msg = _(
"Action %(action_reference)s (id: %(action_id)s) does not exist for button of type action.",
action_reference=name, action_id=action_id,
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
name_manager.has_action(name)
elif node.get('icon'):
@@ -1281,7 +1293,7 @@ actual arch.
for child in node.iterchildren(tag=etree.Element):
if child.tag != 'field' and not isinstance(child, etree._Comment):
msg = _('A <graph> can only contains <field> nodes, found a <%s>', child.tag)
self.handle_view_error(msg, child)
self._raise_view_error(msg, child)
def _validate_tag_groupby(self, node, name_manager, node_info):
# groupby nodes should be considered as nested view because they may
@@ -1295,7 +1307,7 @@ actual arch.
"Field '%(name)s' found in 'groupby' node can only be of type many2one, found %(type)s",
name=field.name, type=field.type,
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
name_manager.must_have_fields(
self._get_field_domain_variables(node, field, node_info['editable'])
)
@@ -1304,7 +1316,7 @@ actual arch.
"Field '%(field)s' found in 'groupby' node does not exist in model %(model)s",
field=name, model=name_manager.Model._name,
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
def _validate_tag_tree(self, node, name_manager, node_info):
allowed_tags = ('field', 'button', 'control', 'groupby', 'widget', 'header')
@@ -1314,26 +1326,22 @@ actual arch.
'Tree child can only have one of %(tags)s tag (not %(wrong_tag)s)',
tags=', '.join(allowed_tags), wrong_tag=child.tag,
)
self.handle_view_error(msg, child)
self._raise_view_error(msg, child)
def _validate_tag_search(self, node, name_manager, node_info):
if len([c for c in node if c.tag == 'searchpanel']) > 1:
self.handle_view_error(_('Search tag can only contain one search panel'), node)
self._raise_view_error(_('Search tag can only contain one search panel'), node)
if not list(node.iterdescendants(tag="field")):
# the field of the search view may be within a group node, which is why we must check
# for all descendants containing a node with a field tag, if this is not the case
# then a search is not possible.
self.handle_view_error(
'Search tag requires at least one field element',
failed_node=node,
raise_exception=False
)
self._log_view_warning('Search tag requires at least one field element', node)
def _validate_tag_searchpanel(self, node, name_manager, node_info):
for child in node.iterchildren(tag=etree.Element):
if child.get('domain') and child.get('select') != 'multi':
msg = _('Searchpanel item with select multi cannot have a domain.')
self.handle_view_error(msg, child)
self._raise_view_error(msg, child)
def _validate_tag_label(self, node, name_manager, node_info):
# replace return not arch.xpath('//label[not(@for) and not(descendant::input)]')
@@ -1341,28 +1349,24 @@ actual arch.
if not for_:
msg = _('Label tag must contain a "for". To match label style '
'without corresponding field or button, use \'class="o_form_label"\'.')
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
else:
name_manager.must_have_name_or_id(for_, 'label for') # this could be done in check_attr
name_manager.must_have_name_or_id(for_, '<label for="...">')
def _validate_tag_page(self, node, name_manager, node_info):
if node.getparent() is None or node.getparent().tag != 'notebook':
self.handle_view_error(_('Page direct ancestor must be notebook'), node)
self._raise_view_error(_('Page direct ancestor must be notebook'), node)
def _validate_tag_img(self, node, name_manager, node_info):
if not any(node.get(alt) for alt in self._att_list('alt')):
self.handle_view_error(
'<img> tag must contain an alt attribute',
failed_node=node,
raise_exception=False
)
self._log_view_warning('<img> tag must contain an alt attribute', node)
def _validate_tag_a(self, node, name_manager, node_info):
#('calendar', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
if any('btn' in node.get(cl, '') for cl in self._att_list('class')):
if node.get('role') != 'button':
msg = '"<a>" tag with "btn" class must have "button" role'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
def _validate_tag_ul(self, node, name_manager, node_info):
self._check_dropdown_menu(node) # was applied to all node, but in practice, only used on div and ul
@@ -1380,22 +1384,22 @@ actual arch.
if any('dropdown-menu' in node.get(cl, '') for cl in self._att_list('class')):
if node.get('role') != 'menu':
msg = 'dropdown-menu class must have menu role'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
def _check_progress_bar(self, node):
if any('o_progressbar' in node.get(cl, '') for cl in self._att_list('class')):
if node.get('role') != 'progressbar':
msg = 'o_progressbar class must have progressbar role'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
if not any(node.get(at) for at in self._att_list('aria-valuenow')):
msg = 'o_progressbar class must have aria-valuenow attribute'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
if not any(node.get(at) for at in self._att_list('aria-valuemin')):
msg = 'o_progressbar class must have aria-valuemin attribute'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
if not any(node.get(at) for at in self._att_list('aria-valuemax')):
msg = 'o_progressbar class must have aria-valuemaxattribute'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
def _att_list(self, name):
return [name, 't-att-%s' % name, 't-attf-%s' % name]
@@ -1406,7 +1410,7 @@ actual arch.
for attr, expr in node.items():
if attr == 'domain':
fields = self._get_server_domain_variables(node, expr, 'domain of <%s%s> ' % (node.tag, (' name="%s"' % node.get('name')) if node.get('name') else '' ), Model)
fields = self._get_server_domain_variables(node, expr, 'domain of <%s%s>' % (node.tag, (' name="%s"' % node.get('name')) if node.get('name') else ''), Model)
name_manager.must_have_fields(fields)
elif attr.startswith('decoration-'):
@@ -1418,8 +1422,7 @@ actual arch.
if attr == 'attrs' and isinstance(val_ast, ast.List):
# domains in attrs are used for readonly, invisible, ...
# and thus are only executed client side
desc = '%s.%s' % (attr, key)
fields = self._get_client_domain_variables(node, val_ast, desc, expr)
fields = self._get_client_domain_variables(node, val_ast, attr, expr)
name_manager.must_have_fields(fields)
elif key == 'group_by': # only in context
@@ -1428,7 +1431,7 @@ actual arch.
'"group_by" value must be a string %(attribute)s=%(value)r',
attribute=attr, value=expr,
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
group_by = val_ast.s
fname = group_by.split(':')[0]
if not fname in Model._fields:
@@ -1436,9 +1439,9 @@ actual arch.
'Unknown field "%(field)s" in "group_by" value in %(attribute)s=%(value)r',
field=fname, attribute=attr, value=expr,
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
else:
use = '%s.%s (%s)' % (attr, key, expr)
use = '%s (%s)' % (attr, expr)
fields = dict.fromkeys(get_variable_names(val_ast), use)
name_manager.must_have_fields(fields)
@@ -1446,10 +1449,10 @@ actual arch.
# col check is mainly there for the tag 'group', but previous
# check was generic in view form
if not expr.isdigit():
self.handle_view_error(
self._raise_view_error(
_('%(attribute)r value must be an integer (%(value)s)',
attribute=attr, value=expr),
failed_node=node,
node,
)
elif attr in ('class', 't-att-class', 't-attf-class'):
@@ -1462,28 +1465,28 @@ actual arch.
# order to batch check them at the end
if not self.env['ir.model.data']._xmlid_to_res_id(group.strip(), raise_if_not_found=False):
msg = "The group %r defined in view does not exist!"
self.handle_view_error(msg % group, node, raise_exception=False)
self._log_view_warning(msg % group, node)
elif attr == 'group':
msg = "attribute 'group' is not valid. Did you mean 'groups'?"
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
elif attr == 'data-toggle' and expr == 'tab':
if node.get('role') != 'tab':
msg = 'tab link (data-toggle="tab") must have "tab" role'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
aria_control = node.get('aria-controls') or node.get('t-att-aria-controls')
if not aria_control and not node.get('t-attf-aria-controls'):
msg = 'tab link (data-toggle="tab") must have "aria_control" defined'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
if aria_control and '#' in aria_control:
msg = 'aria-controls in tablink cannot contains "#"'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
elif attr == "role" and expr in ('presentation', 'none'):
msg = ("A role cannot be `none` or `presentation`. "
"All your elements must be accessible with screen readers, describe it.")
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
def _validate_classes(self, node, expr):
""" Validate the classes present on node. """
@@ -1492,27 +1495,27 @@ actual arch.
# example: <div t-attf-class="{{!selection_mode ? 'oe_kanban_color_' + kanban_getcolor(record.color.raw_value) : ''}} oe_kanban_card oe_kanban_global_click oe_applicant_kanban oe_semantic_html_override">
if 'modal' in classes and node.get('role') != 'dialog':
msg = '"modal" class should only be used with "dialog" role'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
if 'modal-header' in classes and node.tag != 'header':
msg = '"modal-header" class should only be used in "header" tag'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
if 'modal-body' in classes and node.tag != 'main':
msg = '"modal-body" class should only be used in "main" tag'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
if 'modal-footer' in classes and node.tag != 'footer':
msg = '"modal-footer" class should only be used in "footer" tag'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
if 'tab-pane' in classes and node.get('role') != 'tabpanel':
msg = '"tab-pane" class should only be used with "tabpanel" role'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
if 'nav-tabs' in classes and node.get('role') != 'tablist':
msg = 'A tab list with class nav-tabs must have role="tablist"'
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
if any(klass.startswith('alert-') for klass in classes):
if (
@@ -1523,7 +1526,7 @@ actual arch.
"status role or an alert-link class. Please use alert and "
"alertdialog only for what expects to stop any activity to "
"be read immediately.")
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
if any(klass.startswith('fa-') for klass in classes):
description = 'A <%s> with fa class (%s)' % (node.tag, expr)
@@ -1540,7 +1543,7 @@ actual arch.
msg = ("A simili button must be in tag a/button/select or tag `input` "
"with type button/submit/reset or have class in "
"btn-group/btn-toolbar/btn-ship")
self.handle_view_error(msg, node, raise_exception=False)
self._log_view_warning(msg, node)
def _validate_fa_class_accessibility(self, node, description):
valid_aria_attrs = set(
@@ -1598,7 +1601,7 @@ actual arch.
return
msg = ('%s must have title in its tag, parents, descendants or have text')
self.handle_view_error(msg % description, node, raise_exception=False)
self._log_view_warning(msg % description, node)
def _get_client_domain_variables(self, node, domain, key, expr):
""" Returns all field and variable names present in the given domain
@@ -1610,11 +1613,8 @@ actual arch.
try:
(field_names, var_names) = get_domain_identifiers(domain)
except ValueError:
msg = _(
'Invalid domain format while checking %(attribute)s in %(value)s',
attribute=expr, value=key,
)
self.handle_view_error(msg, node)
msg = _('Invalid domain format %(expr)s in %(key)s', expr=expr, key=key)
self._raise_view_error(msg, node)
return dict.fromkeys(field_names | var_names, '%s (%s)' % (key, expr))
@@ -1625,8 +1625,8 @@ actual arch.
try:
(field_names, var_names) = get_domain_identifiers(domain)
except ValueError as e:
msg = _('Invalid domain format while checking %s in %s', domain, key)
self.handle_view_error(msg, node, from_traceback=e.__traceback__)
msg = _('Invalid domain format %(expr)s in %(key)s', expr=domain, key=key)
self._raise_view_error(msg, node, from_traceback=e.__traceback__)
# checking field names
for name_seq in field_names:
@@ -1636,24 +1636,24 @@ actual arch.
for fname in fnames:
if not isinstance(model, models.BaseModel):
msg = _(
'Trying to access "%(field)s" on %(model)s in path %(field_path)r in %(attribute)s=%(value)r',
field=fname, model=model, field_path=name_seq, attribute=key, value=domain,
'Non-relational field %(field)r in path %(field_path)r in %(key)s (%(domain)s)',
field=fname, field_path=name_seq, key=key, domain=domain,
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
field = model._fields[fname]
if not field._description_searchable:
msg = _(
'Unsearchable field "%(field)s" in path %(field_path)r in %(attribute)s=%(value)r',
field=field, field_path=name_seq, attribute=key, value=domain,
'Unsearchable field %(field)r in path %(field_path)r in %(key)s (%(domain)s)',
field=fname, field_path=name_seq, key=key, domain=domain,
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
model = model[fname]
except KeyError:
msg = _(
'Unknown field "%(model)s.%(field)s" in %(attribute)s%(value)r',
model=model._name, field=fname, attribute=key, value=domain,
'Unknown field "%(model)s.%(field)s" in %(key)s (%(domain)s)',
model=model._name, field=fname, key=key, domain=domain,
)
self.handle_view_error(msg, node)
self._raise_view_error(msg, node)
return dict.fromkeys(var_names, "%s (%s)" % (key, domain))
@@ -1667,7 +1667,7 @@ actual arch.
return self._get_server_domain_variables(
node,
domain,
'field %s default domain' % field.name,
'domain of field %r' % field.name,
self.env[field.comodel_name],
)
return {}
@@ -2080,15 +2080,15 @@ class NameManager:
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 = _(
"Name or id '%(name_or_id)s' used in '%(use)s' must be present in view but is missing.",
"Name or id %(name_or_id)r in %(use)s must be present in view but is missing.",
name_or_id=action, use=use,
)
view.handle_view_error(msg, None)
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)
view.handle_view_error(message, None)
view._raise_view_error(message)
for field, use in self.mandatory_fields.items():
if field == 'id': # always available
@@ -2098,20 +2098,20 @@ class NameManager:
"Invalid composed field %(definition)s in %(use)s",
definition=field, use=use,
)
view.handle_view_error(msg, None)
view._raise_view_error(msg)
corresponding_field = self.available_fields.get(str(field))
if corresponding_field is None:
msg = _(
"Field %(name)s used in %(use)s must be present in view but is missing.",
"Field %(name)r used in %(use)s must be present in view but is missing.",
name=field, use=use,
)
view.handle_view_error(msg, None)
view._raise_view_error(msg)
if corresponding_field.get('select') == 'multi': # mainly for searchpanel, but can be a generic behaviour.
msg = _(
"Field %(name)s used in %(use)s is present in view but is in select multi.",
"Field %(name)r used in %(use)s is present in view but is in select multi.",
name=field, use=use,
)
view.handle_view_error(msg, None)
view._raise_view_error(msg)
def update_view_fields(self):
for field_name, field_infos in self.available_fields.items():
+157 -291
View File
@@ -1452,22 +1452,21 @@ class TestViews(ViewCase):
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_invalid_subfield(self):
with self.assertRaises(ValidationError):
self.View.create({
'name': 'invalid subfield',
'model': 'ir.ui.view',
'arch': """
<form string="View">
arch = """
<form string="View">
<field name="name"/>
<field name="inherit_children_ids">
<tree name="Children">
<field name="name"/>
<field name="inherit_children_ids">
<tree name="Children">
<field name="name"/>
<field name="not_a_field"/>
</tree>
</field>
</form>
""",
})
<field name="not_a_field"/>
</tree>
</field>
</form>
"""
self.assertInvalid(
arch,
'''Field "not_a_field" does not exist in model "ir.ui.view"''',
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_context_in_view(self):
@@ -1477,17 +1476,11 @@ class TestViews(ViewCase):
<field name="inherit_id" context="{'stuff': model}"/>
</form>
"""
self.View.create({
'name': 'valid context',
'model': 'ir.ui.view',
'arch': arch % '<field name="model"/>',
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid context',
'model': 'ir.ui.view',
'arch': arch % '',
})
self.assertValid(arch % '<field name="model"/>')
self.assertInvalid(
arch % '',
"""Field 'model' used in context ({'stuff': model}) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_context_in_subview(self):
@@ -1502,24 +1495,15 @@ class TestViews(ViewCase):
</field>
</form>
"""
self.View.create({
'name': 'valid context',
'model': 'ir.ui.view',
'arch': arch % ('', '<field name="model"/>'),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid context',
'model': 'ir.ui.view',
'arch': arch % ('', ''),
})
with self.assertRaises(ValidationError):
# field is in view but not in subview
self.View.create({
'name': 'valid context',
'model': 'ir.ui.view',
'arch': arch % ('<field name="model"/>', ''),
})
self.assertValid(arch % ('', '<field name="model"/>'))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in context ({'stuff': model}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('<field name="model"/>', ''),
"""Field 'model' used in context ({'stuff': model}) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_context_in_subview_with_parent(self):
@@ -1534,23 +1518,15 @@ class TestViews(ViewCase):
</field>
</form>
"""
self.View.create({
'name': 'valid context',
'model': 'ir.ui.view',
'arch': arch % ('<field name="model"/>', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid context',
'model': 'ir.ui.view',
'arch': arch % ('', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid context',
'model': 'ir.ui.view',
'arch': arch % ('', '<field name="model"/>'),
})
self.assertValid(arch % ('<field name="model"/>', ''))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in context ({'stuff': parent.model}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="model"/>'),
"""Field 'model' used in context ({'stuff': parent.model}) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_context_in_subsubview_with_parent(self):
@@ -1570,29 +1546,19 @@ class TestViews(ViewCase):
</field>
</form>
"""
self.View.create({
'name': 'valid context',
'model': 'ir.ui.view',
'arch': arch % ('<field name="model"/>', '', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid context',
'model': 'ir.ui.view',
'arch': arch % ('', '', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid context',
'model': 'ir.ui.view',
'arch': arch % ('', '<field name="model"/>', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid context',
'model': 'ir.ui.view',
'arch': arch % ('', '', '<field name="model"/>'),
})
self.assertValid(arch % ('<field name="model"/>', '', ''))
self.assertInvalid(
arch % ('', '', ''),
"""Field 'model' used in context ({'stuff': parent.parent.model}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="model"/>', ''),
"""Field 'model' used in context ({'stuff': parent.parent.model}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '', '<field name="model"/>'),
"""Field 'model' used in context ({'stuff': parent.parent.model}) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_id_case(self):
@@ -1618,7 +1584,7 @@ class TestViews(ViewCase):
# self.assertInvalid(arch % ('<field name="name"/><field name="type"/>', "'tata' if name else 'tutu'", 'type'), 'xxxx')
self.assertInvalid(
arch % ('', '0 if name else 1', '1'),
"""Field name used in domain of <field name="inherit_id"> ([(0 if name else 1, '=', 1)]) must be present in view but is missing""",
"""Field 'name' used in domain of <field name="inherit_id"> ([(0 if name else 1, '=', 1)]) must be present in view but is missing""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
@@ -1629,17 +1595,11 @@ class TestViews(ViewCase):
<field name="inherit_id" domain="[('model', '=', model)]"/>
</form>
"""
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % '<field name="model"/>',
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % '',
})
self.assertValid(arch % '<field name="model"/>')
self.assertInvalid(
arch % '',
"""Field 'model' used in domain of <field name="inherit_id"> ([('model', '=', model)]) must be present in view but is missing.""",
)
def test_domain_unknown_field(self):
self.assertInvalid("""
@@ -1648,7 +1608,7 @@ class TestViews(ViewCase):
<field name="inherit_id" domain="[('invalid_field', '=', 'res.users')]"/>
</form>
""",
'''Unknown field "ir.ui.view.invalid_field" in domain of <field name="inherit_id"> "[('invalid_field', '=', 'res.users')]"''',
'''Unknown field "ir.ui.view.invalid_field" in domain of <field name="inherit_id"> ([('invalid_field', '=', 'res.users')])''',
)
def test_domain_field_searchable(self):
@@ -1663,7 +1623,7 @@ class TestViews(ViewCase):
# computed field, not stored, no search
self.assertInvalid(
arch % 'xml_id',
'''Unsearchable field "ir.ui.view.xml_id" in path 'xml_id' in domain of <field name="inherit_id"> ="[('xml_id', '=', 'test')]"''',
'''Unsearchable field 'xml_id' in path 'xml_id' in domain of <field name="inherit_id"> ([('xml_id', '=', 'test')])''',
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
@@ -1687,23 +1647,15 @@ class TestViews(ViewCase):
</field>
</form>
"""
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ('', '<field name="model"/>'),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ('', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ('<field name="model"/>', ''),
})
self.assertValid(arch % ('', '<field name="model"/>'))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in domain of <field name="inherit_id"> ([('model', '=', model)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('<field name="model"/>', ''),
"""Field 'model' used in domain of <field name="inherit_id"> ([('model', '=', model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_in_subview_with_parent(self):
@@ -1718,28 +1670,16 @@ class TestViews(ViewCase):
</field>%s
</form>
"""
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ('<field name="model"/>', '', ''),
})
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ('', '', '<field name="model"/>'),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ('', '', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ('', '<field name="model"/>', ''),
})
self.assertValid(arch % ('<field name="model"/>', '', ''))
self.assertValid(arch % ('', '', '<field name="model"/>'))
self.assertInvalid(
arch % ('', '', ''),
"""Field 'model' used in domain of <field name="inherit_id"> ([('model', '=', parent.model)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="model"/>', ''),
"""Field 'model' used in domain of <field name="inherit_id"> ([('model', '=', parent.model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_on_field_in_view(self):
@@ -1752,17 +1692,11 @@ class TestViews(ViewCase):
<field name="inherit_id"/>
</form>
"""
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % '<field name="model"/>',
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % '',
})
self.assertValid(arch % '<field name="model"/>')
self.assertInvalid(
arch % '',
"""Field 'model' used in domain of field 'inherit_id' ([('model', '=', model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_on_field_in_subview(self):
@@ -1780,23 +1714,15 @@ class TestViews(ViewCase):
</field>
</form>
"""
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ('', '<field name="model"/>'),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ('', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ('<field name="model"/>', ''),
})
self.assertValid(arch % ('', '<field name="model"/>'))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in domain of field 'inherit_id' ([('model', '=', model)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('<field name="model"/>', ''),
"""Field 'model' used in domain of field 'inherit_id' ([('model', '=', model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_on_field_in_subview_with_parent(self):
@@ -1814,23 +1740,15 @@ class TestViews(ViewCase):
</field>
</form>
"""
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ('<field name="model"/>', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ('', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ('', '<field name="model"/>'),
})
self.assertValid(arch % ('<field name="model"/>', ''))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in domain of field 'inherit_id' ([('model', '=', parent.model)]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="model"/>'),
"""Field 'model' used in domain of field 'inherit_id' ([('model', '=', parent.model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_on_field_in_noneditable_subview(self):
@@ -1848,17 +1766,11 @@ class TestViews(ViewCase):
</field>
</form>
"""
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % '',
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ' editable="bottom"',
})
self.assertValid(arch % '')
self.assertInvalid(
arch % ' editable="bottom"',
"""Field 'model' used in domain of field 'inherit_id' ([('model', '=', model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_on_readonly_field_in_view(self):
@@ -1871,11 +1783,7 @@ class TestViews(ViewCase):
<field name="inherit_id" readonly="1"/>
</form>
"""
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch,
})
self.assertValid(arch)
self.patch(field, 'readonly', True)
arch = """
@@ -1884,11 +1792,7 @@ class TestViews(ViewCase):
<field name="inherit_id"/>
</form>
"""
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch,
})
self.assertValid(arch)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_on_readonly_field_in_subview(self):
@@ -1906,17 +1810,11 @@ class TestViews(ViewCase):
</field>
</form>
"""
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % ' readonly="1"',
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid domain',
'model': 'ir.ui.view',
'arch': arch % '',
})
self.assertValid(arch % ' readonly="1"')
self.assertInvalid(
arch % '',
"""Field 'model' used in domain of field 'inherit_id' ([('model', '=', model)]) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_modifier_attribute_is_boolean(self):
@@ -1949,11 +1847,11 @@ class TestViews(ViewCase):
)
self.assertInvalid(
arch % ('name', 'invalid_field'),
"""Unknown field "ir.ui.view.invalid_field" in domain of <filter name="draft"> "[('invalid_field', '=', 'dummy')]""",
"""Unknown field "ir.ui.view.invalid_field" in domain of <filter name="draft"> ([('invalid_field', '=', 'dummy')])""",
)
self.assertInvalid(
arch % ('name', 'inherit_children_ids.invalid_field'),
"""Unknown field "ir.ui.view.invalid_field" in domain of <filter name="draft"> "[('inherit_children_ids.invalid_field', '=', 'dummy')]""",
"""Unknown field "ir.ui.view.invalid_field" in domain of <filter name="draft"> ([('inherit_children_ids.invalid_field', '=', 'dummy')])""",
)
# todo add check for non searchable fields and group by
@@ -1978,7 +1876,7 @@ class TestViews(ViewCase):
<filter string="Dummy" name="draft" domain="['name', '=', 'dummy']"/>
</search>
""",
"""Invalid domain format while checking ['name', '=', 'dummy'] in domain of <filter name="draft">""",
"""Invalid domain format ['name', '=', 'dummy'] in domain of <filter name="draft">""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
@@ -1995,19 +1893,19 @@ class TestViews(ViewCase):
self.assertValid(arch % ('', '<field name="inherit_id"/>', 'view_access', 'inherit_id'))
self.assertInvalid(
arch % ('<field name="inherit_id"/>', '', 'view_access', 'inherit_id'),
"""Field inherit_id used in domain of <field name="groups_id"> ([['view_access', '=', inherit_id]]) must be present in view but is missing.""",
"""Field 'inherit_id' used in domain of <field name="groups_id"> ([['view_access', '=', inherit_id]]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="inherit_id"/>', 'view_access', 'view_access'),
"""Field view_access used in domain of <field name="groups_id"> ([['view_access', '=', view_access]]) must be present in view but is missing.""",
"""Field 'view_access' used in domain of <field name="groups_id"> ([['view_access', '=', view_access]]) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="inherit_id"/>', 'inherit_id', 'inherit_id'),
"""Unknown field "res.groups.inherit_id" in domain of <field name="groups_id"> "[['inherit_id', '=', inherit_id]]""",
"""Unknown field "res.groups.inherit_id" in domain of <field name="groups_id"> ([['inherit_id', '=', inherit_id]])""",
)
self.assertInvalid(
arch % ('', '<field name="inherit_id" select="multi"/>', 'view_access', 'inherit_id'),
"""Field inherit_id used in domain of <field name="groups_id"> ([['view_access', '=', inherit_id]]) is present in view but is in select multi.""",
"""Field 'inherit_id' used in domain of <field name="groups_id"> ([['view_access', '=', inherit_id]]) is present in view but is in select multi.""",
)
arch = """
@@ -2040,17 +1938,11 @@ class TestViews(ViewCase):
attrs="{'readonly': [('model', '=', 'ir.ui.view')]}"/>
</form>
"""
self.View.create({
'name': 'valid attrs',
'model': 'ir.ui.view',
'arch': arch % '<field name="model"/>',
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid attrs',
'model': 'ir.ui.view',
'arch': arch % '',
})
self.assertValid(arch % '<field name="model"/>')
self.assertInvalid(
arch % '',
"""Field 'model' used in attrs ({'readonly': [('model', '=', 'ir.ui.view')]}) must be present in view but is missing""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_attrs_invalid_domain(self):
@@ -2064,7 +1956,7 @@ class TestViews(ViewCase):
"""
self.assertInvalid(
arch,
"""Invalid domain format while checking {'readonly': [('model', 'ir.ui.view')]} in attrs.readonly""",
"""Invalid domain format {'readonly': [('model', 'ir.ui.view')]} in attrs""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
@@ -2081,23 +1973,15 @@ class TestViews(ViewCase):
</field>
</form>
"""
self.View.create({
'name': 'valid attrs',
'model': 'ir.ui.view',
'arch': arch % ('', '<field name="model"/>'),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid attrs',
'model': 'ir.ui.view',
'arch': arch % ('', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid attrs',
'model': 'ir.ui.view',
'arch': arch % ('<field name="model"/>', ''),
})
self.assertValid(arch % ('', '<field name="model"/>'))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in attrs ({'readonly': [('model', '=', 'ir.ui.view')]}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('<field name="model"/>', ''),
"""Field 'model' used in attrs ({'readonly': [('model', '=', 'ir.ui.view')]}) must be present in view but is missing.""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_attrs_subfield_with_parent(self):
@@ -2113,23 +1997,15 @@ class TestViews(ViewCase):
</field>
</form>
"""
self.View.create({
'name': 'valid attrs',
'model': 'ir.ui.view',
'arch': arch % ('<field name="model"/>', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid attrs',
'model': 'ir.ui.view',
'arch': arch % ('', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'valid attrs',
'model': 'ir.ui.view',
'arch': arch % ('', '<field name="model"/>'),
})
self.assertValid(arch % ('<field name="model"/>', ''))
self.assertInvalid(
arch % ('', ''),
"""Field 'model' used in attrs ({'readonly': [('parent.model', '=', 'ir.ui.view')]}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('', '<field name="model"/>'),
"""Field 'model' used in attrs ({'readonly': [('parent.model', '=', 'ir.ui.view')]}) must be present in view but is missing.""",
)
def test_button(self):
arch = """
@@ -2198,29 +2074,19 @@ class TestViews(ViewCase):
</groupby>
</tree>
"""
self.View.create({
'name': 'valid groupby',
'model': 'ir.ui.view',
'arch': arch % ('', '<field name="noupdate"/>'),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'invalid groupby',
'model': 'ir.ui.view',
'arch': arch % ('', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'invalid groupby',
'model': 'ir.ui.view',
'arch': arch % ('<field name="noupdate"/>', ''),
})
with self.assertRaises(ValidationError):
self.View.create({
'name': 'invalid groupby',
'model': 'ir.ui.view',
'arch': arch % ('', '<field name="noupdate"/><field name="fake_field"/>'),
})
self.assertValid(arch % ('', '<field name="noupdate"/>'))
self.assertInvalid(
arch % ('', ''),
"""Field 'noupdate' used in attrs ({'invisible': [('noupdate', '=', True)]}) must be present in view but is missing.""",
)
self.assertInvalid(
arch % ('<field name="noupdate"/>', ''),
'''Field "noupdate" does not exist in model "ir.ui.view"''',
)
self.assertInvalid(
arch % ('', '<field name="noupdate"/><field name="fake_field"/>'),
'''Field "fake_field" does not exist in model "ir.model.data"''',
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_check_xml_on_reenable(self):
@@ -2265,7 +2131,7 @@ class TestViews(ViewCase):
)
self.assertInvalid(
'<form><label for="model"/></form>',
"Name or id 'model' used in 'label for' must be present in view but is missing.",
"""Name or id 'model' in <label for="..."> must be present in view but is missing.""",
)
def test_col_colspan_numerical(self):