summaryrefslogtreecommitdiff
path: root/odoo/fields.py
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/fields.py
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/fields.py')
-rw-r--r--odoo/fields.py59
1 files changed, 55 insertions, 4 deletions
diff --git a/odoo/fields.py b/odoo/fields.py
index 02a3aa37125..9a54022c986 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -2134,6 +2134,24 @@ class Selection(Field):
> result = [('a', 'A'), ('c', 'C'), ('b', 'B')]
:type selection_add: list(tuple(str,str))
+ :param ondelete: provides a fallback mechanism for any overridden
+ field with a selection_add. It is a dict that maps every option
+ from the selection_add to a fallback action.
+
+ This fallback action will be applied to all records whose
+ selection_add option maps to it.
+
+ The actions can be any of the following:
+ - 'set null' -- the default, all records with this option
+ will have their selection value set to False.
+ - 'cascade' -- all records with this option will be
+ deleted along with the option itself.
+ - 'set default' -- all records with this option will be
+ set to the default of the field definition
+ - <callable> -- a callable whose first and only argument will be
+ the set of records containing the specified Selection option,
+ for custom processing
+
The attribute ``selection`` is mandatory except in the case of
``related`` or extended fields.
"""
@@ -2142,6 +2160,7 @@ class Selection(Field):
_slots = {
'selection': None, # [(value, string), ...], function or method name
'validate': True, # whether validating upon write
+ 'ondelete': None, # {value: policy} (what to do when value is deleted)
}
def __init__(self, selection=Default, string=Default, **kwargs):
@@ -2175,15 +2194,14 @@ class Selection(Field):
_logger.warning("%s: selection attribute will be ignored as the field is related", self)
selection = field.args['selection']
if isinstance(selection, list):
- if (
- values is not None
- and values != [kv[0] for kv in selection]
- ):
+ if values is not None and values != [kv[0] for kv in selection]:
_logger.warning("%s: selection=%r overrides existing selection; use selection_add instead", self, selection)
values = [kv[0] for kv in selection]
labels.update(selection)
+ self.ondelete = {}
else:
self.selection = selection
+ self.ondelete = None
if 'selection_add' in field.args:
if self.related:
@@ -2193,8 +2211,41 @@ class Selection(Field):
"%s: selection_add=%r must be a list" % (self, selection_add)
assert values is not None, \
"%s: selection_add=%r on non-list selection %r" % (self, selection_add, self.selection)
+
+ ondelete = field.args.get('ondelete') or {}
+ new_values = [kv[0] for kv in selection_add if kv[0] not in values]
+ for key in new_values:
+ ondelete.setdefault(key, 'set null')
+ if self.required and new_values and 'set null' in ondelete.values():
+ raise ValueError(
+ "%r: required selection fields must define an ondelete policy that "
+ "implements the proper cleanup of the corresponding records upon "
+ "module uninstallation. Please use one or more of the following "
+ "policies: 'set default' (if the field has a default defined), 'cascade', "
+ "or a single-argument callable where the argument is the recordset "
+ "containing the specified option." % self
+ )
+
+ # check ondelete values
+ for key, val in ondelete.items():
+ if callable(val) or val in ('set null', 'cascade'):
+ continue
+ if val == 'set default':
+ assert self.default is not None, (
+ "%r: ondelete policy of type 'set default' is invalid for this field "
+ "as it does not define a default! Either define one in the base "
+ "field, or change the chosen ondelete policy" % self
+ )
+ continue
+ raise ValueError(
+ "%r: ondelete policy %r for selection value %r is not a valid ondelete "
+ "policy, please choose one of 'set null', 'set default', 'cascade' or "
+ "a callable" % (self, val, key)
+ )
+
values = merge_sequences(values, [kv[0] for kv in selection_add])
labels.update(kv for kv in selection_add if len(kv) == 2)
+ self.ondelete.update(ondelete)
if values is not None:
self.selection = [(value, labels[value]) for value in values]