diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 0c1d34afa0d..d7af93fb303 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -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 can only contains 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_, '