[IMP] base: better translation of view validation messages

* Use of new _ signature (and placeholders)
* Ensure all error messages are translated
* No translation for warning logs
* Improved/fixed validation messages

closes odoo/odoo#53804

Signed-off-by: Victor Feyens (vfe) <vfe@odoo.com>
This commit is contained in:
Victor Feyens
2020-07-02 09:14:33 +00:00
parent efe51fa56a
commit 07e4b9e02e
2 changed files with 175 additions and 97 deletions
+164 -86
View File
@@ -398,11 +398,17 @@ actual arch.
check = valid_view(view_arch, env=self.env, model=view.model)
view_name = ('%s (%s)' % (view.name, view.xml_id)) if view.xml_id else view.name
if not check:
raise ValidationError(_('Invalid view %s definition in %s') % (view_name, view.arch_fs))
raise ValidationError(_(
'Invalid view %(name)s definition in %(file)s',
name=view_name, file=view.arch_fs
))
if check == "Warning":
_logger.warning(_('Invalid view %s definition in %s \n%s'), view_name, view.arch_fs, view.arch)
_logger.warning('Invalid view %s definition in %s \n%s', view_name, view.arch_fs, view.arch)
except ValueError as e:
raise ValidationError(_("Error while validating view:\n\n%s", tools.ustr(e))).with_traceback(e.__traceback__) from None
raise ValidationError(_(
"Error while validating view:\n\n%(error)s",
error=tools.ustr(e),
)).with_traceback(e.__traceback__) from None
return True
@@ -619,10 +625,13 @@ actual arch.
if self.groups_id & self.env.user.groups_id:
return True
if self.groups_id:
error = _("View '%s' accessible only to groups %s ") % \
(self.key, ", ".join([g.name for g in self.groups_id]))
error = _(
"View '%(name)s' accessible only to groups %(groups)s ",
name=self.key,
groups=", ".join([g.name for g in self.groups_id]
))
else:
error = _("View '%s' is private", self.key)
error = _("View '%(name)s' is private", name=self.key)
raise AccessError(error)
def handle_view_error(self, message, *, raise_exception=True, from_exception=None, from_traceback=None):
@@ -640,9 +649,11 @@ actual arch.
when raising an exception, start with this traceback (default: start
at exception creation)
"""
# Do not translate warning logs
_t = _ if raise_exception else lambda txt, *args, **kwargs: txt % (args or kwargs)
lines = [message]
if self.name:
lines.append("\n%s: %s" % (_('View name'), self.name))
lines.append("\n%s" % _t('View name: %(name)s', name=self.name))
error_context = {
'view': self,
@@ -652,7 +663,7 @@ actual arch.
'file': self.env.context.get('install_filename'),
}
if any(error_context.values()):
lines.append("%s:" % _("Error context"))
lines.append(_t("Error context:"))
lines.extend(" %s: %s" % (k, v) for k, v in error_context.items() if v)
lines.append("")
@@ -837,7 +848,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') % dict(model=model))
self.handle_view_error(_('Model not found: %(model)s', model=model))
self._postprocess_on_change(model, node)
@@ -1065,19 +1076,28 @@ actual arch.
if not field and name in name_manager.fields_get:
return
if not field:
msg = _('Field "%s" does not exist in model "%s"')
self.handle_view_error(msg % (node.get('name'), name_manager.Model._name))
msg = _(
'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)
if node.get('domain') and field.comodel_name not in self.env:
msg = _('Domain on field without comodel makes no sense for "%s" (domain:%s)')
self.handle_view_error(msg % (node.get('name'), node.get('domain')))
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)
for attribute in ('invisible', 'readonly', 'required'):
val = node.get(attribute)
if val:
res = safe_eval(val, {'context': self._context})
if res not in (1, 0, True, False, None):
msg = _('Attribute %s evaluation must give a boolean, got %s')
self.handle_view_error(msg % (attribute, val))
msg = _(
'Attribute %(attribute)s evaluation expects a boolean, got %(value)s',
attribute=attribute, value=val,
)
self.handle_view_error(msg)
def _validate_tag_button(self, node, name_manager, node_info):
name = node.get('name')
@@ -1085,7 +1105,7 @@ actual arch.
type_ = node.get('type')
if special:
if special not in ('cancel', 'save', 'add'):
self.handle_view_error(_("Invalid special '%s' in button", special))
self.handle_view_error(_("Invalid special '%(value)s' in button", value=special))
elif type_:
if type_ == 'edit': # list_renderer, used in kanban view
return
@@ -1094,17 +1114,23 @@ actual arch.
elif type_ == 'object':
func = getattr(type(name_manager.Model), name, None)
if not func:
msg = _("%s is not a valid action on %s")
self.handle_view_error(msg % (name, name_manager.Model._name))
msg = _(
"%(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)
try:
check_method_name(name)
except AccessError:
msg = _("%s on %s is private and cannot be called from a button")
self.handle_view_error(msg % (name, name_manager.Model._name))
msg = _(
"%(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)
try:
inspect.signature(func).bind(self=name_manager.Model)
except TypeError:
msg = _("%s on %s has parameters and cannot be called from a button")
msg = "%s on %s has parameters and cannot be called from a button"
self.handle_view_error(msg % (name, name_manager.Model._name), raise_exception=False)
elif type_ == 'action':
# logic mimics /web/action/load behaviour
@@ -1114,15 +1140,21 @@ actual arch.
except ValueError:
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 %s for button of type action.")
self.handle_view_error(msg % name)
msg = _("Invalid xmlid %(xmlid)s for button of type action.", xmlid=name)
self.handle_view_error(msg)
if not issubclass(self.pool[model], self.pool['ir.actions.actions']):
msg = _("%s is of type %s, expected a subclass of ir.actions.actions")
self.handle_view_error(msg % (name, model))
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)
action = self.env['ir.actions.actions'].browse(action_id).exists()
if not action:
msg = _("Action %s (id: %s) does not exist for button of type action.")
self.handle_view_error(msg % (name, action_id))
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)
name_manager.has_action(name)
elif node.get('icon'):
@@ -1132,8 +1164,8 @@ actual arch.
def _validate_tag_graph(self, node, name_manager, node_info):
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>')
self.handle_view_error(msg % child.tag)
msg = _('A <graph> can only contains <field> nodes, found a <%s>', child.tag)
self.handle_view_error(msg)
def _validate_tag_groupby(self, node, name_manager, node_info):
# groupby nodes should be considered as nested view because they may
@@ -1143,31 +1175,40 @@ actual arch.
field = name_manager.Model._fields.get(name)
if field:
if field.type != 'many2one':
msg = _("field '%s' found in 'groupby' node can only be of type many2one, found %s")
self.handle_view_error(msg % (field.name, field.type))
msg = _(
"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)
name_manager.must_have_fields(
self._get_field_domain_variables(node, field, node_info['editable'])
)
else:
msg = _("field '%s' found in 'groupby' node does not exist in model %s")
self.handle_view_error(msg % (name, name_manager.Model._name))
msg = _(
"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)
def _validate_tag_tree(self, node, name_manager, node_info):
allowed_tags = ('field', 'button', 'control', 'groupby', 'widget')
for child in node.iterchildren(tag=etree.Element):
if child.tag not in allowed_tags and not isinstance(child, etree._Comment):
msg = _('Tree child can only be have one of %s tag (not %s)')
self.handle_view_error(msg % (', '.join(allowed_tags), child.tag))
msg = _(
'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)
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 contains one search panel'))
self.handle_view_error(_('Search tag can only contain one search panel'))
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'), raise_exception=False)
'Search tag requires at least one field element', raise_exception=False)
def _validate_tag_searchpanel(self, node, name_manager, node_info):
for child in node.iterchildren(tag=etree.Element):
@@ -1191,17 +1232,14 @@ actual arch.
def _validate_tag_img(self, node, name_manager, node_info):
if not any(node.get(alt) for alt in self._att_list('alt')):
src = next((node.get(src) for src in self._att_list('src') if node.get(src)), "")
if src:
src = ' with src (%s)'
msg = _('<img> tag%s must contain an alt attribute')
self.handle_view_error(msg % src, raise_exception=False)
self.handle_view_error(
'<img> tag must contain an alt attribute', raise_exception=False)
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')
msg = '"<a>" tag with "btn" class must have "button" role'
self.handle_view_error(msg, raise_exception=False)
def _validate_tag_ul(self, node, name_manager, node_info):
@@ -1219,22 +1257,22 @@ actual arch.
#('calendar', 'form', 'graph', 'kanban', 'pivot', 'search', 'tree', 'activity')
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')
msg = 'dropdown-menu class must have menu role'
self.handle_view_error(msg, raise_exception=False)
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')
msg = 'o_progressbar class must have progressbar role'
self.handle_view_error(msg, raise_exception=False)
if not any(node.get(at) for at in self._att_list('aria-valuenow')):
msg = _('o_progressbar class must have aria-valuenow attribute')
msg = 'o_progressbar class must have aria-valuenow attribute'
self.handle_view_error(msg, raise_exception=False)
if not any(node.get(at) for at in self._att_list('aria-valuemin')):
msg = _('o_progressbar class must have aria-valuemin attribute')
msg = 'o_progressbar class must have aria-valuemin attribute'
self.handle_view_error(msg, raise_exception=False)
if not any(node.get(at) for at in self._att_list('aria-valuemax')):
msg = _('o_progressbar class must have aria-valuemaxattribute')
msg = 'o_progressbar class must have aria-valuemaxattribute'
self.handle_view_error(msg, raise_exception=False)
def _att_list(self, name):
@@ -1264,13 +1302,19 @@ actual arch.
elif key == 'group_by': # only in context
if not isinstance(val_ast, ast.Str):
msg = _('"group_by" value must be a string %s=%r')
self.handle_view_error(msg % (attr, expr))
msg = _(
'"group_by" value must be a string %(attribute)s=%(value)r',
attribute=attr, value=expr,
)
self.handle_view_error(msg)
group_by = val_ast.s
if not group_by.split(':')[0] in Model._fields:
msg = _('Unknow field "%s" in "group_by" value in %s=%r')
self.handle_view_error(msg % (group_by, attr, expr))
fname = group_by.split(':')[0]
if not fname in Model._fields:
msg = _(
'Unknown field "%(field)s" in "group_by" value in %(attribute)s=%(value)r',
field=fname, attribute=attr, value=expr,
)
self.handle_view_error(msg)
else:
use = '%s.%s (%s)' % (attr, key, expr)
fields = dict.fromkeys(get_variable_names(val_ast), use)
@@ -1280,7 +1324,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(_('%r value must be an integer (%s)') % (attr, expr))
self.handle_view_error(_(
'%(attribute)r value must be an integer (%(value)s)',
attribute=attr, value=expr,
))
elif attr in ('class', 't-att-class', 't-attf-class'):
self._validate_classes(node, expr)
@@ -1291,28 +1338,28 @@ actual arch.
# further improvement: add all groups to name_manager in
# 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!", group)
self.handle_view_error(msg, raise_exception=False)
msg = "The group %r defined in view does not exist!"
self.handle_view_error(msg % group, raise_exception=False)
elif attr == 'group':
msg = _("attribute 'group' is not valid. Did you mean 'groups'?")
msg = "attribute 'group' is not valid. Did you mean 'groups'?"
self.handle_view_error(msg, raise_exception=False)
elif attr == 'data-toggle' and expr == 'tab':
if node.get('role') != 'tab':
msg = _('tab link (data-toggle="tab") must have "tab" role')
msg = 'tab link (data-toggle="tab") must have "tab" role'
self.handle_view_error(msg, raise_exception=False)
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')
msg = 'tab link (data-toggle="tab") must have "aria_control" defined'
self.handle_view_error(msg, raise_exception=False)
if aria_control and '#' in aria_control:
msg = _('aria-controls in tablink cannot contains "#"')
msg = 'aria-controls in tablink cannot contains "#"'
self.handle_view_error(msg, raise_exception=False)
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.")
msg = ("A role cannot be `none` or `presentation`. "
"All your elements must be accessible with screen readers, describe it.")
self.handle_view_error(msg, raise_exception=False)
def _validate_classes(self, node, expr):
@@ -1321,27 +1368,27 @@ actual arch.
# Be careful: not always true if it is an expression
# 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')
msg = '"modal" class should only be used with "dialog" role'
self.handle_view_error(msg, raise_exception=False)
if 'modal-header' in classes and node.tag != 'header':
msg = _('"modal-header" class should only be used in "header" tag')
msg = '"modal-header" class should only be used in "header" tag'
self.handle_view_error(msg, raise_exception=False)
if 'modal-body' in classes and node.tag != 'main':
msg = _('"modal-body" class should only be used in "main" tag')
msg = '"modal-body" class should only be used in "main" tag'
self.handle_view_error(msg, raise_exception=False)
if 'modal-footer' in classes and node.tag != 'footer':
msg = _('"modal-footer" class should only be used in "footer" tag')
msg = '"modal-footer" class should only be used in "footer" tag'
self.handle_view_error(msg, raise_exception=False)
if 'tab-pane' in classes and node.get('role') != 'tabpanel':
msg = _('"tab-pane" class should only be used with "tabpanel" role')
msg = '"tab-pane" class should only be used with "tabpanel" role'
self.handle_view_error(msg, raise_exception=False)
if 'nav-tabs' in classes and node.get('role') != 'tablist':
msg = _('A tab list with class nav-tabs must have role="tablist"')
msg = 'A tab list with class nav-tabs must have role="tablist"'
self.handle_view_error(msg, raise_exception=False)
if any(klass.startswith('alert-') for klass in classes):
@@ -1349,7 +1396,7 @@ actual arch.
node.get('role') not in ('alert', 'alertdialog', 'status')
and 'alert-link' not in classes
):
msg = _("An alert (class alert-*) must have an alert, alertdialog or "
msg = ("An alert (class alert-*) must have an alert, alertdialog or "
"status role or an alert-link class. Please use alert and "
"alertdialog only for what expects to stop any activity to "
"be read immediately.")
@@ -1367,7 +1414,7 @@ actual arch.
elif any(klass in classes for klass in ('btn-group', 'btn-toolbar', 'btn-ship')):
pass
else:
msg = _("A simili button must be in tag a/button/select or tag `input` "
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, raise_exception=False)
@@ -1427,17 +1474,23 @@ actual arch.
if contains_description(node):
return
msg = _('%s must have title in its tag, parents, descendants or have text')
msg = ('%s must have title in its tag, parents, descendants or have text')
self.handle_view_error(msg % description, raise_exception=False)
def _get_client_domain_variables(self, domain, key, expr):
""" Returns all field and variable names present in the given domain
(to be used client-side).
:param str: key (attrs.<attrs_key>)
:param str domain:
"""
try:
(field_names, var_names) = get_domain_identifiers(domain)
except ValueError:
msg = _('Invalid domain format while checking %s in %s') % (expr, key)
msg = _(
'Invalid domain format while checking %(attribute)s in %(value)s',
attribute=expr, value=key,
)
self.handle_view_error(msg)
return dict.fromkeys(field_names | var_names, '%s (%s)' % (key, expr))
@@ -1449,8 +1502,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')
self.handle_view_error(msg % (domain, key), from_traceback=e.__traceback__)
msg = _('Invalid domain format while checking %s in %s', domain, key)
self.handle_view_error(msg, from_traceback=e.__traceback__)
# checking field names
for name_seq in field_names:
@@ -1459,16 +1512,25 @@ actual arch.
try:
for fname in fnames:
if not isinstance(model, models.BaseModel):
msg = _('Trying to access "%s" on %s in path %r in %s=%r')
self.handle_view_error(msg % (fname, model, name_seq, key, domain))
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,
)
self.handle_view_error(msg)
field = model._fields[fname]
if not field._description_searchable:
msg = _('Unsearchable field "%s" in path %r in %s=%r')
self.handle_view_error(msg % (field, name_seq, key, domain))
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,
)
self.handle_view_error(msg)
model = model[fname]
except KeyError:
msg = _('Unknow field "%s.%s" in %s%r')
self.handle_view_error(msg % (model._name, fname, key, domain))
msg = _(
'Unknown field "%(model)s.%(field)s" in %(attribute)s%(value)r',
model=model._name, field=fname, attribute=key, value=domain,
)
self.handle_view_error(msg)
return dict.fromkeys(var_names, "%s (%s)" % (key, domain))
@@ -1883,23 +1945,39 @@ class NameManager:
for action, use in self.mandatory_names.items():
if action not in self.available_actions and action not in self.available_fields:
view.handle_view_error("Name '%s' used in '%s' must be present in view but is missing." % (action, use))
msg = _(
"Name '%(name)s' used in '%(use)s' must be present in view but is missing.",
name=action, use=use,
)
view.handle_view_error(msg)
for field_name in self.available_fields:
if field_name not in self.fields_get:
message = _("Field `%s` does not exist", field_name)
message = _("Field `%(name)s` does not exist", name=field_name)
view.handle_view_error(message)
for field, use in self.mandatory_fields.items():
if field == 'id': # always available
continue
if "." in field:
view.handle_view_error('Invalid composed field %s in %s' % (field, use))
msg = _(
"Invalid composed field %(definition)s in %(use)s",
definition=field, use=use,
)
view.handle_view_error(msg)
corresponding_field = self.available_fields.get(str(field))
if corresponding_field is None:
view.handle_view_error('Field %s used in %s must be present in view but is missing.' % (field, use))
msg = _(
"Field %(name)s used in %(use)s must be present in view but is missing.",
name=field, use=use,
)
view.handle_view_error(msg)
if corresponding_field.get('select') == 'multi': # mainly for searchpanel, but can be a generic behaviour.
view.handle_view_error('Field %s used in %s is present in view but is in select multi.' % (field, use))
msg = _(
"Field %(name)s used in %(use)s is present in view but is in select multi.",
name=field, use=use,
)
view.handle_view_error(msg)
def update_view_fields(self):
for field_name, field_infos in self.available_fields.items():
+11 -11
View File
@@ -1513,7 +1513,7 @@ class TestViews(ViewCase):
<field name="inherit_id" domain="[('invalid_field', '=', 'res.users')]"/>
</form>
""",
'''Unknow 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):
@@ -1537,7 +1537,7 @@ class TestViews(ViewCase):
<form string="View">
<field name="name" domain="[('test', '=', 'test')]"/>
</form>
""", "Domain on field without comodel makes no sense for \"name\" (domain:[('test', '=', 'test')])")
""", "Domain on non-relational field \"name\" makes no sense (domain:[('test', '=', 'test')])")
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_domain_in_subview(self):
@@ -1795,7 +1795,7 @@ class TestViews(ViewCase):
self.assertValid(arch % 'True')
self.assertInvalid(
arch % "[('model', '=', '1')]",
"Attribute readonly evaluation must give a boolean, got [('model', '=', '1')]",
"Attribute readonly evaluation expects a boolean, got [('model', '=', '1')]",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
@@ -1814,11 +1814,11 @@ class TestViews(ViewCase):
)
self.assertInvalid(
arch % ('name', 'invalid_field'),
"""Unknow 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'),
"""Unknow 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
@@ -1832,7 +1832,7 @@ class TestViews(ViewCase):
self.assertValid(arch % 'name')
self.assertInvalid(
arch % 'invalid_field',
"""Unknow field "invalid_field" in "group_by" value in context="{'group_by':'invalid_field'}""",
"""Unknown field "invalid_field" in "group_by" value in context="{'group_by':'invalid_field'}""",
)
@mute_logger('odoo.addons.base.models.ir_ui_view')
@@ -1868,7 +1868,7 @@ class TestViews(ViewCase):
)
self.assertInvalid(
arch % ('', '<field name="inherit_id"/>', 'inherit_id', 'inherit_id'),
"""Unknow 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'),
@@ -1885,7 +1885,7 @@ class TestViews(ViewCase):
</searchpanel>
</search>
"""
self.assertInvalid(arch, "Search tag can only contains one search panel")
self.assertInvalid(arch, "Search tag can only contain one search panel")
def test_groups_field(self):
arch = """
@@ -2035,7 +2035,7 @@ class TestViews(ViewCase):
</tree>
"""
self.assertValid(arch % '')
self.assertInvalid(arch % '<group/>', "Tree child can only be have one of field, button, control, groupby, widget tag (not group)")
self.assertInvalid(arch % '<group/>', "Tree child can only have one of field, button, control, groupby, widget tag (not group)")
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_tree_groupby(self):
@@ -2048,8 +2048,8 @@ class TestViews(ViewCase):
</tree>
"""
self.assertValid(arch % ('model_data_id'))
self.assertInvalid(arch % ('type'), "field 'type' found in 'groupby' node can only be of type many2one, found selection")
self.assertInvalid(arch % ('dummy'), "field 'dummy' found in 'groupby' node does not exist in model ir.ui.view")
self.assertInvalid(arch % ('type'), "Field 'type' found in 'groupby' node can only be of type many2one, found selection")
self.assertInvalid(arch % ('dummy'), "Field 'dummy' found in 'groupby' node does not exist in model ir.ui.view")
@mute_logger('odoo.addons.base.models.ir_ui_view')
def test_tree_groupby_many2one(self):