summaryrefslogtreecommitdiff
path: root/odoo/addons/test_new_api/tests
diff options
context:
space:
mode:
authorAdrian Torres <adt@odoo.com>2020-02-13 16:31:17 +0000
committerRaphael Collet <rco@odoo.com>2020-03-30 13:42:04 +0000
commitf0481392c6501cae2c38359f526da1eefa451337 (patch)
treee17b0fedb9abeb7ee71380005c1e7f8ec4ca9891 /odoo/addons/test_new_api/tests
parent96223568c445bc1d378fa4d8833f90d3dad8ec7e (diff)
[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.
Diffstat (limited to 'odoo/addons/test_new_api/tests')
-rw-r--r--odoo/addons/test_new_api/tests/test_new_fields.py204
-rw-r--r--odoo/addons/test_new_api/tests/test_schema.py13
2 files changed, 215 insertions, 2 deletions
diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py
index 55f0d59ea04..ee89c51ab8d 100644
--- a/odoo/addons/test_new_api/tests/test_new_fields.py
+++ b/odoo/addons/test_new_api/tests/test_new_fields.py
@@ -11,7 +11,7 @@ import io
from PIL import Image
import psycopg2
-from odoo import fields
+from odoo import models, fields
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tests import common
@@ -2462,3 +2462,205 @@ class TestSelectionDeleteUpdate(common.TransactionCase):
('field_id.name', '=', 'state'),
('value', '=', 'confirmed'),
], limit=1).unlink()
+
+
+@common.tagged('selection_ondelete_base')
+class TestSelectionOndelete(common.TransactionCase):
+
+ MODEL_BASE = 'test_new_api.model_selection_base'
+ MODEL_REQUIRED = 'test_new_api.model_selection_required'
+ MODEL_NONSTORED = 'test_new_api.model_selection_non_stored'
+
+ def setUp(self):
+ super().setUp()
+ # enable unlinking ir.model.fields.selection
+ self.patch(self.registry, 'ready', False)
+
+ def _unlink_option(self, model, option):
+ self.env['ir.model.fields.selection'].search([
+ ('field_id.model', '=', model),
+ ('field_id.name', '=', 'my_selection'),
+ ('value', '=', option),
+ ], limit=1).unlink()
+
+ def test_ondelete_default(self):
+ # create some records, one of which having the extended selection option
+ rec1 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'foo'})
+ rec2 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'bar'})
+ rec3 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'baz'})
+
+ # test that all values are correct before the removal of the value
+ self.assertEqual(rec1.my_selection, 'foo')
+ self.assertEqual(rec2.my_selection, 'bar')
+ self.assertEqual(rec3.my_selection, 'baz')
+
+ # unlink the extended option (simulates a module uninstall)
+ self._unlink_option(self.MODEL_REQUIRED, 'baz')
+
+ # verify that the ondelete policy has succesfully been applied
+ self.assertEqual(rec1.my_selection, 'foo')
+ self.assertEqual(rec2.my_selection, 'bar')
+ self.assertEqual(rec3.my_selection, 'foo') # reset to default
+
+ def test_ondelete_base_null_explicit(self):
+ rec1 = self.env[self.MODEL_BASE].create({'my_selection': 'foo'})
+ rec2 = self.env[self.MODEL_BASE].create({'my_selection': 'bar'})
+ rec3 = self.env[self.MODEL_BASE].create({'my_selection': 'quux'})
+
+ self.assertEqual(rec1.my_selection, 'foo')
+ self.assertEqual(rec2.my_selection, 'bar')
+ self.assertEqual(rec3.my_selection, 'quux')
+
+ self._unlink_option(self.MODEL_BASE, 'quux')
+
+ self.assertEqual(rec1.my_selection, 'foo')
+ self.assertEqual(rec2.my_selection, 'bar')
+ self.assertFalse(rec3.my_selection)
+
+ def test_ondelete_base_null_implicit(self):
+ rec1 = self.env[self.MODEL_BASE].create({'my_selection': 'foo'})
+ rec2 = self.env[self.MODEL_BASE].create({'my_selection': 'bar'})
+ rec3 = self.env[self.MODEL_BASE].create({'my_selection': 'ham'})
+
+ self.assertEqual(rec1.my_selection, 'foo')
+ self.assertEqual(rec2.my_selection, 'bar')
+ self.assertEqual(rec3.my_selection, 'ham')
+
+ self._unlink_option(self.MODEL_BASE, 'ham')
+
+ self.assertEqual(rec1.my_selection, 'foo')
+ self.assertEqual(rec2.my_selection, 'bar')
+ self.assertFalse(rec3.my_selection)
+
+ def test_ondelete_cascade(self):
+ rec1 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'foo'})
+ rec2 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'bar'})
+ rec3 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'eggs'})
+
+ self.assertEqual(rec1.my_selection, 'foo')
+ self.assertEqual(rec2.my_selection, 'bar')
+ self.assertEqual(rec3.my_selection, 'eggs')
+
+ self._unlink_option(self.MODEL_REQUIRED, 'eggs')
+
+ self.assertEqual(rec1.my_selection, 'foo')
+ self.assertEqual(rec2.my_selection, 'bar')
+ self.assertFalse(rec3.exists())
+
+ def test_ondelete_literal(self):
+ rec1 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'foo'})
+ rec2 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'bar'})
+ rec3 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'bacon'})
+
+ self.assertEqual(rec1.my_selection, 'foo')
+ self.assertEqual(rec2.my_selection, 'bar')
+ self.assertEqual(rec3.my_selection, 'bacon')
+
+ self._unlink_option(self.MODEL_REQUIRED, 'bacon')
+
+ self.assertEqual(rec1.my_selection, 'foo')
+ self.assertEqual(rec2.my_selection, 'bar')
+ self.assertEqual(rec3.my_selection, 'bar')
+
+ def test_ondelete_multiple_explicit(self):
+ rec1 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'foo'})
+ rec2 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'eevee'})
+ rec3 = self.env[self.MODEL_REQUIRED].create({'my_selection': 'pikachu'})
+
+ self.assertEqual(rec1.my_selection, 'foo')
+ self.assertEqual(rec2.my_selection, 'eevee')
+ self.assertEqual(rec3.my_selection, 'pikachu')
+
+ self._unlink_option(self.MODEL_REQUIRED, 'eevee')
+ self._unlink_option(self.MODEL_REQUIRED, 'pikachu')
+
+ self.assertEqual(rec1.my_selection, 'foo')
+ self.assertEqual(rec2.my_selection, 'bar')
+ self.assertEqual(rec3.my_selection, 'foo')
+
+ def test_ondelete_callback(self):
+ rec = self.env[self.MODEL_REQUIRED].create({'my_selection': 'knickers'})
+
+ self.assertEqual(rec.my_selection, 'knickers')
+
+ self._unlink_option(self.MODEL_REQUIRED, 'knickers')
+
+ self.assertEqual(rec.my_selection, 'foo')
+ self.assertFalse(rec.active)
+
+ def test_non_stored_selection(self):
+ rec = self.env[self.MODEL_NONSTORED].create({})
+ rec.my_selection = 'foo'
+
+ self.assertEqual(rec.my_selection, 'foo')
+
+ self._unlink_option(self.MODEL_NONSTORED, 'foo')
+
+ self.assertFalse(rec.my_selection)
+
+
+@common.tagged('selection_ondelete_advanced')
+class TestSelectionOndeleteAdvanced(common.TransactionCase):
+
+ MODEL_BASE = 'test_new_api.model_selection_base'
+ MODEL_REQUIRED = 'test_new_api.model_selection_required'
+
+ def setUp(self):
+ super().setUp()
+ # necessary cleanup for resetting changes in the registry
+ for model_name in (self.MODEL_BASE, self.MODEL_REQUIRED):
+ Model = self.registry[model_name]
+ self.addCleanup(setattr, Model, '__bases__', Model.__bases__)
+ self.addCleanup(self.registry.model_cache.clear)
+
+ def test_ondelete_unexisting_policy(self):
+ class Foo(models.Model):
+ _inherit = self.MODEL_REQUIRED
+
+ my_selection = fields.Selection(selection_add=[
+ ('random', "Random stuff"),
+ ], ondelete={'random': 'poop'})
+
+ Foo._build_model(self.registry, self.env.cr)
+
+ with self.assertRaises(ValueError):
+ self.registry.setup_models(self.env.cr)
+
+ def test_ondelete_default_no_default(self):
+ class Foo(models.Model):
+ _inherit = self.MODEL_BASE
+
+ my_selection = fields.Selection(selection_add=[
+ ('corona', "Corona beers suck"),
+ ], ondelete={'corona': 'set default'})
+
+ Foo._build_model(self.registry, self.env.cr)
+
+ with self.assertRaises(AssertionError):
+ self.registry.setup_models(self.env.cr)
+
+ def test_ondelete_required_null_explicit(self):
+ class Foo(models.Model):
+ _inherit = self.MODEL_REQUIRED
+
+ my_selection = fields.Selection(selection_add=[
+ ('brap', "Brap"),
+ ], ondelete={'brap': 'set null'})
+
+ Foo._build_model(self.registry, self.env.cr)
+
+ with self.assertRaises(ValueError):
+ self.registry.setup_models(self.env.cr)
+
+ def test_ondelete_required_null_implicit(self):
+ class Foo(models.Model):
+ _inherit = self.MODEL_REQUIRED
+
+ my_selection = fields.Selection(selection_add=[
+ ('boing', "Boyoyoyoing"),
+ ])
+
+ Foo._build_model(self.registry, self.env.cr)
+
+ with self.assertRaises(ValueError):
+ self.registry.setup_models(self.env.cr)
diff --git a/odoo/addons/test_new_api/tests/test_schema.py b/odoo/addons/test_new_api/tests/test_schema.py
index 4aa99e87a47..d8849dd22f6 100644
--- a/odoo/addons/test_new_api/tests/test_schema.py
+++ b/odoo/addons/test_new_api/tests/test_schema.py
@@ -3,13 +3,24 @@ from odoo.models import MetaModel
from odoo.tests import common
+def get_model_name(cls):
+ name = cls._name
+ if not name:
+ [name] = cls._inherit if isinstance(cls._inherit, list) else [cls._inherit]
+ assert isinstance(name, str)
+ return name
+
+
class TestReflection(common.TransactionCase):
""" Test the reflection into 'ir.model', 'ir.model.fields', etc. """
def test_models_fields(self):
""" check that all models and fields are reflected as expected. """
# retrieve the models defined in this module, and check them
- model_names = {cls._name for cls in MetaModel.module_to_models['test_new_api']}
+ model_names = {
+ get_model_name(cls)
+ for cls in MetaModel.module_to_models['test_new_api']
+ }
ir_models = self.env['ir.model'].search([('model', 'in', list(model_names))])
self.assertEqual(len(ir_models), len(model_names))
for ir_model in ir_models: