From f31516566e9a73039cbd476e0ab98e52c0cbefbb Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Thu, 15 Mar 2018 12:29:29 +0100 Subject: [PATCH] [FIX] base: validate domain of field used in view When a field has a domain like `"[('foo', '=', bar)]"` and is in a view, check that `bar` is in the view as well. Note that the check should only occurs in editable views (form or editable list) and when the field itself is editable. OPW 1820269 --- odoo/addons/base/ir/ir_ui_view.py | 55 +++++++-- odoo/addons/base/tests/test_views.py | 177 +++++++++++++++++++++++++++ 2 files changed, 222 insertions(+), 10 deletions(-) diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py index 386abda72f8..328f3aca56a 100644 --- a/odoo/addons/base/ir/ir_ui_view.py +++ b/odoo/addons/base/ir/ir_ui_view.py @@ -172,6 +172,7 @@ xpath_utils['hasclass'] = _hasclass TRANSLATED_ATTRS_RE = re.compile(r"@(%s)\b" % "|".join(TRANSLATED_ATTRS)) WRONGCLASS = re.compile(r"(@class\s*=|=\s*@class|contains\(@class)") +READONLY = re.compile(r"\breadonly\b") class View(models.Model): @@ -786,12 +787,16 @@ actual arch. attrs = {} field = Model._fields.get(node.get('name')) if field: + editable = self.env.context.get('view_is_editable', True) and self._field_is_editable(field, node) children = False views = {} for f in node: if f.tag in ('form', 'tree', 'graph', 'kanban', 'calendar'): node.remove(f) - xarch, xfields = self.with_context(base_model_name=model).postprocess_and_fields(field.comodel_name, f, view_id) + xarch, xfields = self.with_context( + base_model_name=model, + view_is_editable=editable, + ).postprocess_and_fields(field.comodel_name, f, view_id) views[str(f.tag)] = { 'arch': xarch, 'fields': xfields, @@ -866,6 +871,17 @@ actual arch. return arch + def _view_is_editable(self, node): + """ Return whether the node is an editable view. """ + return node.tag == 'form' or node.tag == 'tree' and node.get('editable') + + def _field_is_editable(self, field, node): + """ Return whether a field is editable (not always readonly). """ + return ( + (not field.readonly or READONLY.search(str(field.states or ""))) and + (node.get('readonly') != "1" or READONLY.search(node.get('attrs') or "")) + ) + def get_attrs_symbols(self): """ Return a set of predefined symbols for evaluating attrs. """ return { @@ -885,10 +901,11 @@ actual arch. 'current_date', } - def get_attrs_field_names(self, arch): + def get_attrs_field_names(self, arch, model, editable): """ Retrieve the field names appearing in context, domain and attrs, and return a list of triples ``(field_name, attr_name, attr_value)``. """ + VIEW_TYPES = {item[0] for item in type(self).type.selection} symbols = self.get_attrs_symbols() | {None} result = [] @@ -918,8 +935,17 @@ actual arch. if isinstance(arg, (tuple, list)): process_expr(str(arg[0]), get, key, expr) - def process(node, get=get_name): + def process(node, model, editable, get=get_name): """ traverse `node` and collect triples """ + if node.tag in VIEW_TYPES: + # determine whether this view is editable + editable = editable and self._view_is_editable(node) + elif node.tag == 'field': + # determine whether the field is editable + field = model._fields.get(node.get('name')) + if field: + editable = editable and self._field_is_editable(field, node) + for key, val in node.items(): if not val: continue @@ -927,13 +953,21 @@ actual arch. process_expr(val, get, key, val) elif key == 'attrs': process_attrs(val, get, key, val) - if node.tag == 'field': - # retrieve subfields of 'parent' - get = partial(get_subname, get) - for child in node: - process(child, get) - process(arch) + if node.tag == 'field' and field and field.relational: + if editable and not node.get('domain'): + domain = field._description_domain(self.env) + # process the field's domain as if it was in the view + if isinstance(domain, pycompat.string_types): + process_expr(domain, get, 'domain', domain) + # retrieve subfields of 'parent' + model = self.env[field.comodel_name] + get = partial(get_subname, get) + + for child in node: + process(child, model, editable, get) + + process(arch, model, editable) return result @api.model @@ -974,7 +1008,8 @@ actual arch. attrs_fields = [] if self.env.context.get('check_field_names'): - attrs_fields = self.get_attrs_field_names(node) + editable = self.env.context.get('view_is_editable', True) + attrs_fields = self.get_attrs_field_names(node, Model, editable) fields_def = self.postprocess(model, node, view_id, False, fields) if node.tag in ('kanban', 'tree', 'form', 'gantt'): diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py index db28cce587e..7a1382cb752 100644 --- a/odoo/addons/base/tests/test_views.py +++ b/odoo/addons/base/tests/test_views.py @@ -1106,6 +1106,183 @@ class TestViews(ViewCase): 'arch': arch % ('', ''), }) + @mute_logger('odoo.addons.base.ir.ir_ui_view') + def test_domain_on_field_in_view(self): + field = self.env['ir.ui.view']._fields['inherit_id'] + self.patch(field, 'domain', "[('model', '=', model)]") + + arch = """ +
+ %s + + + """ + 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 % '', + }) + + @mute_logger('odoo.addons.base.ir.ir_ui_view') + def test_domain_on_field_in_subview(self): + field = self.env['ir.ui.view']._fields['inherit_id'] + self.patch(field, 'domain', "[('model', '=', model)]") + + arch = """ +
+ %s + + + %s + + + + + """ + 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 % ('', ''), + }) + with self.assertRaises(ValidationError): + self.View.create({ + 'name': 'valid domain', + 'model': 'ir.ui.view', + 'arch': arch % ('', ''), + }) + + @mute_logger('odoo.addons.base.ir.ir_ui_view') + def test_domain_on_field_in_subview_with_parent(self): + field = self.env['ir.ui.view']._fields['inherit_id'] + self.patch(field, 'domain', "[('model', '=', parent.model)]") + + arch = """ +
+ %s + + + %s + + + + + """ + 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 % ('', ''), + }) + with self.assertRaises(ValidationError): + self.View.create({ + 'name': 'valid domain', + 'model': 'ir.ui.view', + 'arch': arch % ('', ''), + }) + + @mute_logger('odoo.addons.base.ir.ir_ui_view') + def test_domain_on_field_in_noneditable_subview(self): + field = self.env['ir.ui.view']._fields['inherit_id'] + self.patch(field, 'domain', "[('model', '=', model)]") + + arch = """ +
+ + + + + + + + + """ + 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"', + }) + + @mute_logger('odoo.addons.base.ir.ir_ui_view') + def test_domain_on_readonly_field_in_view(self): + field = self.env['ir.ui.view']._fields['inherit_id'] + self.patch(field, 'domain', "[('model', '=', model)]") + + arch = """ +
+ + + + """ + self.View.create({ + 'name': 'valid domain', + 'model': 'ir.ui.view', + 'arch': arch, + }) + + self.patch(field, 'readonly', True) + arch = """ +
+ + + + """ + self.View.create({ + 'name': 'valid domain', + 'model': 'ir.ui.view', + 'arch': arch, + }) + + @mute_logger('odoo.addons.base.ir.ir_ui_view') + def test_domain_on_readonly_field_in_subview(self): + field = self.env['ir.ui.view']._fields['inherit_id'] + self.patch(field, 'domain', "[('model', '=', model)]") + + arch = """ +
+ + + + + + + + + """ + 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 % '', + }) + @mute_logger('odoo.addons.base.ir.ir_ui_view') def test_attrs_field(self): arch = """