From f0481392c6501cae2c38359f526da1eefa451337 Mon Sep 17 00:00:00 2001 From: Adrian Torres Date: Thu, 13 Feb 2020 16:31:17 +0000 Subject: [PATCH] [IMP] core: introduce mechanism for selection_add cleanup - Let A and B be two different modules. - A defines a required Selection field F of model M and B extends it through the `selection_add` argument. - Create records of model M and have some of them have any of the options introduced by module B selected for field F. - Uninstall module B. The result will be records of Model M with an option for field F that no longer exists, this makes the registry inconsistent and prone to crashing (it is sufficient to access the form view of such a record to trigger a crash). This commit introduces a mechanism similar to the `ondelete` argument found in Many2one fields, the argument name is the same but it's different both in behaviour and in implementation. The `ondelete` mechanism for Selection fields is enforced for **any** Selection field with required set to `True`, this means that the developer is required to set a cleanup behaviour for when their module is uninstalled. For possible cleanup options, see fields.Selection's docstring. As far as implementation goes, everything is implemented in Python unlike with Many2one fields where the behaviour is delegated to PostgreSQL. The `ondelete` setting will be processed during `ir.model.fields.selection.unlink()` to ensure that the registry is left in an appropriate state after module uninstall. --- odoo/addons/base/models/ir_model.py | 47 +++- .../test_new_api/models/test_new_api.py | 97 +++++++++ .../test_new_api/security/ir.model.access.csv | 3 + .../test_new_api/tests/test_new_fields.py | 204 +++++++++++++++++- odoo/addons/test_new_api/tests/test_schema.py | 13 +- odoo/fields.py | 59 ++++- 6 files changed, 406 insertions(+), 17 deletions(-) diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py index 53ae02f6d5d..cd726c77f29 100644 --- a/odoo/addons/base/models/ir_model.py +++ b/odoo/addons/base/models/ir_model.py @@ -893,7 +893,7 @@ class IrModelFields(models.Model): 'index': bool(field.index), 'store': bool(field.store), 'copied': bool(field.copy), - 'on_delete': getattr(field, 'ondelete', None), + 'on_delete': field.ondelete if field.type == 'many2one' else None, 'related': ".".join(field.related) if field.related else None, 'readonly': bool(field.readonly), 'required': bool(field.required), @@ -1238,16 +1238,7 @@ class IrModelSelection(models.Model): 'Please modify them through Python code, ' 'preferably through a custom addon!')) - for selection in self: - if selection.field_id.store and \ - not self.env[selection.field_id.model]._abstract: - # replace the value by NULL in the field's corresponding column - query = 'UPDATE "{table}" SET "{field}"=NULL WHERE "{field}"=%s'.format( - table=self.env[selection.field_id.model]._table, - field=selection.field_id.name, - ) - self.env.cr.execute(query, [selection.value]) - + self._process_ondelete() result = super().unlink() # Reload registry for normal unlink only. For module uninstall, the @@ -1259,6 +1250,40 @@ class IrModelSelection(models.Model): return result + def _process_ondelete(self): + """ Process the 'ondelete' of the given selection values. """ + for selection in self: + Model = self.env[selection.field_id.model] + field = Model._fields[selection.field_id.name] + if not field.store or Model._abstract: + continue + + ondelete = (field.ondelete or {}).get(selection.value) or 'set null' + if callable(ondelete): + ondelete(selection._get_records()) + elif ondelete == 'set null': + selection._get_records().write({field.name: False}) + elif ondelete == 'set default': + value = field.convert_to_write(field.default(Model), Model) + selection._get_records().write({field.name: value}) + elif ondelete == 'cascade': + selection._get_records().unlink() + else: + # this shouldn't happen... simply a sanity check + raise ValueError(_( + "The ondelete policy %r is not valid for field %r" + ) % (ondelete, selection)) + + def _get_records(self): + """ Return the records having 'self' as a value. """ + self.ensure_one() + Model = self.env[self.field_id.model] + query = 'SELECT id FROM "{table}" WHERE "{field}"=%s'.format( + table=Model._table, field=self.field_id.name, + ) + self.env.cr.execute(query, [self.value]) + return Model.browse(r[0] for r in self.env.cr.fetchall()) + class IrModelConstraint(models.Model): """ diff --git a/odoo/addons/test_new_api/models/test_new_api.py b/odoo/addons/test_new_api/models/test_new_api.py index 92887d66d68..5a192d755a5 100644 --- a/odoo/addons/test_new_api/models/test_new_api.py +++ b/odoo/addons/test_new_api/models/test_new_api.py @@ -894,3 +894,100 @@ class StateMixin(models.AbstractModel): ('confirmed', 'Confirmed'), ('done', 'Done'), ]) + + +class SelectionBase(models.Model): + _name = 'test_new_api.model_selection_base' + _description = "Model with a base selection field" + + my_selection = fields.Selection([ + ('foo', "Foo"), + ('bar', "Bar"), + ]) + + +class SelectionBaseNullExplicit(models.Model): + _inherit = 'test_new_api.model_selection_base' + _description = "Model with a selection field extension with ondelete null" + + my_selection = fields.Selection(selection_add=[ + ('quux', "Quux"), + ], ondelete={'quux': 'set null'}) + + +class SelectionBaseNullImplicit(models.Model): + _inherit = 'test_new_api.model_selection_base' + _description = "Model with a selection field extension without ondelete" + + my_selection = fields.Selection(selection_add=[ + ('ham', "Ham"), + ]) + + +class SelectionRequired(models.Model): + _name = 'test_new_api.model_selection_required' + _description = "Model with a required selection field" + + active = fields.Boolean(default=True) + my_selection = fields.Selection([ + ('foo', "Foo"), + ('bar', "Bar"), + ], required=True, default='foo') + + +class SelectionRequiredDefault(models.Model): + _inherit = 'test_new_api.model_selection_required' + _description = "Model with a selection field extension with ondelete default" + + my_selection = fields.Selection(selection_add=[ + ('baz', "Baz"), + ], ondelete={'baz': 'set default'}) + + +class SelectionRequiredCascade(models.Model): + _inherit = 'test_new_api.model_selection_required' + _description = "Model with a selection field extension with ondelete cascade" + + my_selection = fields.Selection(selection_add=[ + ('eggs', "Eggs"), + ], ondelete={'eggs': 'cascade'}) + + +class SelectionRequiredLiteral(models.Model): + _inherit = 'test_new_api.model_selection_required' + _description = "Model with a selection field extension with ondelete set