[REF] fields: implement sparse fields as computed fields

This commit is contained in:
Raphael Collet
2016-08-31 17:21:47 +02:00
parent b258f4b842
commit 0fc697a3f7
5 changed files with 87 additions and 83 deletions
@@ -11,4 +11,5 @@ access_test_new_api_foo,access_test_new_api_foo,model_test_new_api_foo,,1,1,1,1
access_test_new_api_bar,access_test_new_api_bar,model_test_new_api_bar,,1,1,1,1
access_test_new_api_related,access_test_new_api_related,model_test_new_api_related,,1,1,1,1
access_test_new_api_company,access_test_new_api_company,model_test_new_api_company,,1,1,1,1
access_test_new_api_sparse,access_test_new_api_sparse,model_test_new_api_sparse,,1,1,1,1
access_test_new_api_compute_inverse,access_test_new_api_compute_inverse,model_test_new_api_compute_inverse,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
11 access_test_new_api_bar access_test_new_api_bar model_test_new_api_bar 1 1 1 1
12 access_test_new_api_related access_test_new_api_related model_test_new_api_related 1 1 1 1
13 access_test_new_api_company access_test_new_api_company model_test_new_api_company 1 1 1 1
14 access_test_new_api_sparse access_test_new_api_sparse model_test_new_api_sparse 1 1 1 1
15 access_test_new_api_compute_inverse access_test_new_api_compute_inverse model_test_new_api_compute_inverse 1 1 1 1
+12
View File
@@ -280,3 +280,15 @@ class CompanyDependent(models.Model):
_name = 'test_new_api.company'
foo = fields.Char(company_dependent=True)
class Sparse(models.Model):
_name = 'test_new_api.sparse'
data = fields.Serialized()
boolean = fields.Boolean(sparse='data')
integer = fields.Integer(sparse='data')
float = fields.Float(sparse='data')
char = fields.Char(sparse='data')
selection = fields.Selection([('one', 'One'), ('two', 'Two')], sparse='data')
partner = fields.Many2one('res.partner', sparse='data')
@@ -479,6 +479,32 @@ class TestFields(common.TransactionCase):
self.assertEqual(record.sudo(user1).foo, 'alpha')
self.assertEqual(record.sudo(user2).foo, 'default')
def test_28_sparse(self):
""" test sparse fields. """
record = self.env['test_new_api.sparse'].create({})
self.assertFalse(record.data)
partner = self.env.ref('base.main_partner')
values = [
('boolean', True),
('integer', 42),
('float', 3.14),
('char', 'John'),
('selection', 'two'),
('partner', partner.id),
]
for n, (key, val) in enumerate(values):
record.write({key: val})
self.assertEqual(record.data, dict(values[:n+1]))
for key, val in values[:-1]:
self.assertEqual(record[key], val)
self.assertEqual(record.partner, partner)
for n, (key, val) in enumerate(values):
record.write({key: False})
self.assertEqual(record.data, dict(values[n+1:]))
def test_30_read(self):
""" test computed fields as returned by read(). """
discussion = self.env.ref('test_new_api.discussion_0')
+45 -1
View File
@@ -250,6 +250,17 @@ class Field(object):
:param company_dependent: whether the field is company-dependent (boolean)
.. _field-sparse:
.. rubric:: Sparse fields
Sparse fields have a very small probability of being not null. Therefore
many such fields can be serialized compactly into a common location, the
latter being a so-called "serialized" field.
:param sparse: the name of the field where the value of this field must
be stored.
.. _field-incremental-definition:
.. rubric:: Incremental definition
@@ -306,6 +317,7 @@ class Field(object):
'related': None, # sequence of field names, for related fields
'related_sudo': True, # whether related fields should be read as admin
'company_dependent': False, # whether ``self`` is company-dependent (property field)
'sparse': None, # the name of the corresponding serialized field, if any
'default': None, # default(recs) returns the default value
'string': None, # field label
@@ -427,6 +439,13 @@ class Field(object):
if not attrs.get('readonly'):
attrs['inverse'] = self._inverse_company_dependent
attrs['search'] = self._search_company_dependent
if attrs.get('sparse'):
# by default, sparse fields are not stored and not copied
attrs['store'] = False
attrs['copy'] = attrs.get('copy', False)
attrs['compute'] = self._compute_sparse
if not attrs.get('readonly'):
attrs['inverse'] = self._inverse_sparse
# fix for function fields overridden by regular columns
if not isinstance(attrs.get('origin'), (NoneType, fields.function)):
@@ -596,6 +615,31 @@ class Field(object):
Property = records.env['ir.property']
return Property.search_multi(self.name, self.model_name, operator, value)
#
# Sparse fields
#
def _compute_sparse(self, records):
for record in records:
values = record[self.sparse]
record[self.name] = values.get(self.name)
if self.relational:
for record in records:
record[self.name] = record[self.name].exists()
def _inverse_sparse(self, records):
for record in records:
values = record[self.sparse]
value = self.convert_to_read(record[self.name], record, use_name_get=False)
if value:
if values.get(self.name) != value:
values[self.name] = value
record[self.sparse] = values
else:
if self.name in values:
values.pop(self.name)
record[self.sparse] = values
#
# Setup of field triggers
#
@@ -2047,7 +2091,7 @@ class Many2many(_RelationalMulti):
class Serialized(Field):
""" Minimal support for existing sparse and serialized fields. """
""" Serialized fields provide the storage for sparse fields. """
type = 'serialized'
def convert_to_cache(self, value, record, validate=True):
+3 -82
View File
@@ -1619,92 +1619,13 @@ class sparse(function):
__slots__ = ['serialization_field']
def convert_value(self, obj, cr, uid, record, value, read_value, context=None):
"""
+ For a many2many field, a list of tuples is expected.
Here is the list of tuple that are accepted, with the corresponding semantics ::
(0, 0, { values }) link to a new record that needs to be created with the given values dictionary
(1, ID, { values }) update the linked record with id = ID (write *values* on it)
(2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
(3, ID) cut the link to the linked record with id = ID (delete the relationship between the two objects but does not delete the target object itself)
(4, ID) link to existing record with id = ID (adds a relationship)
(5) unlink all (like using (3,ID) for all linked records)
(6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
Example:
[(6, 0, [8, 5, 6, 4])] sets the many2many to ids [8, 5, 6, 4]
+ For a one2many field, a lits of tuples is expected.
Here is the list of tuple that are accepted, with the corresponding semantics ::
(0, 0, { values }) link to a new record that needs to be created with the given values dictionary
(1, ID, { values }) update the linked record with id = ID (write *values* on it)
(2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
Example:
[(0, 0, {'field_name':field_value_record1, ...}), (0, 0, {'field_name':field_value_record2, ...})]
"""
if self._type == 'many2many':
if not value:
return []
assert value[0][0] == 6, 'Unsupported m2m value for sparse field: %s' % value
return value[0][2]
elif self._type == 'one2many':
if not read_value:
read_value = []
relation_obj = obj.pool[self.relation]
for vals in value:
assert vals[0] in (0,1,2), 'Unsupported o2m value for sparse field: %s' % vals
if vals[0] == 0:
read_value.append(relation_obj.create(cr, uid, vals[2], context=context))
elif vals[0] == 1:
relation_obj.write(cr, uid, vals[1], vals[2], context=context)
elif vals[0] == 2:
relation_obj.unlink(cr, uid, vals[1], context=context)
read_value.remove(vals[1])
return read_value
return value
raise TypeError("Dead method")
def _sparse_write(self,obj,cr, uid, ids, field_name, value, args, context=None):
if not type(ids) == list:
ids = [ids]
records = obj.browse(cr, uid, ids, context=context)
for record in records:
# grab serialized value as object - already deserialized
serialized = getattr(record, self.serialization_field)
if value is None:
# simply delete the key to unset it.
serialized.pop(field_name, None)
else:
serialized[field_name] = self.convert_value(obj, cr, uid, record, value, serialized.get(field_name), context=context)
obj.write(cr, uid, ids, {self.serialization_field: serialized}, context=context)
return True
raise TypeError("Dead method")
def _sparse_read(self, obj, cr, uid, ids, field_names, args, context=None):
results = {}
records = obj.browse(cr, uid, ids, context=context)
for record in records:
# grab serialized value as object - already deserialized
serialized = getattr(record, self.serialization_field)
results[record.id] = {}
for field_name in field_names:
field_type = obj._columns[field_name]._type
value = serialized.get(field_name, False)
if field_type in ('one2many','many2many'):
value = value or []
if value:
# filter out deleted records as superuser
relation_obj = obj.pool[obj._columns[field_name].relation]
value = relation_obj.exists(cr, openerp.SUPERUSER_ID, value)
if type(value) in (int,long) and field_type == 'many2one':
relation_obj = obj.pool[obj._columns[field_name].relation]
# check for deleted record as superuser
if not relation_obj.exists(cr, openerp.SUPERUSER_ID, [value]):
value = False
results[record.id][field_name] = value
return results
raise TypeError("Dead method")
def __init__(self, serialization_field, **kwargs):
self.serialization_field = serialization_field