diff --git a/openerp/addons/test_new_api/ir.model.access.csv b/openerp/addons/test_new_api/ir.model.access.csv index ee467a56828..3862f4ce84f 100644 --- a/openerp/addons/test_new_api/ir.model.access.csv +++ b/openerp/addons/test_new_api/ir.model.access.csv @@ -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 diff --git a/openerp/addons/test_new_api/models.py b/openerp/addons/test_new_api/models.py index e302c25e031..f94eea8a4f9 100644 --- a/openerp/addons/test_new_api/models.py +++ b/openerp/addons/test_new_api/models.py @@ -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') diff --git a/openerp/addons/test_new_api/tests/test_new_fields.py b/openerp/addons/test_new_api/tests/test_new_fields.py index 068d300abb1..3c12988d194 100644 --- a/openerp/addons/test_new_api/tests/test_new_fields.py +++ b/openerp/addons/test_new_api/tests/test_new_fields.py @@ -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') diff --git a/openerp/fields.py b/openerp/fields.py index b021e09230f..c8a3dd8d010 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -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): diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index bb2268bbf13..da55a4bada3 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -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