diff --git a/openerp/addons/base/base.sql b/openerp/addons/base/base.sql index 55c4d41aba5..8c64aa69c57 100644 --- a/openerp/addons/base/base.sql +++ b/openerp/addons/base/base.sql @@ -49,6 +49,8 @@ CREATE TABLE ir_model_fields ( primary key(id) ); +ALTER TABLE ir_model_fields ADD column serialization_field_id int references ir_model_fields on delete cascade; + ------------------------------------------------------------------------- -- Actions diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index da6f1e167f9..ee775bf6fd5 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -1018,6 +1018,8 @@ + + @@ -1130,6 +1132,8 @@ + + diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 014d5652891..82dca02d962 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -207,6 +207,7 @@ class ir_model_fields(osv.osv): 'view_load': fields.boolean('View Auto-Load'), 'selectable': fields.boolean('Selectable'), 'modules': fields.function(_in_modules, method=True, type='char', size=128, string='In modules', help='List of modules in which the field is defined'), + 'serialization_field_id': fields.many2one('ir.model.fields', 'Serialization Field', domain = "[('ttype','=','serialized')]", ondelete='cascade'), } _rec_name='field_description' _defaults = { @@ -299,6 +300,14 @@ class ir_model_fields(osv.osv): if context and context.get('manual',False): vals['state'] = 'manual' + #For the moment renaming a sparse field or changing the storing system is not allowed. This will be done later + if 'serialization_field_id' in vals or 'name' in vals: + for field in self.browse(cr, user, ids, context=context): + if 'serialization_field_id' in vals and field.serialization_field_id.id != vals['serialization_field_id']: + raise except_orm(_('Error!'), _('Changing the storing system for the field "%s" is not allowed.'%field.name)) + if field.serialization_field_id and (field.name != vals['name']): + raise except_orm(_('Error!'), _('Renaming the sparse field "%s" is not allowed'%field.name)) + column_rename = None # if set, *one* column can be renamed here obj = None models_patch = {} # structs of (obj, [(field, prop, change_to),..]) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 72df86eeff9..04364a63074 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -45,6 +45,7 @@ import openerp import openerp.netsvc as netsvc import openerp.tools as tools from openerp.tools.translate import _ +import json def _symbol_set(symb): if symb == None or symb == False: @@ -1178,6 +1179,99 @@ class related(function): obj_name = f['relation'] self._relations[-1]['relation'] = f['relation'] + +class sparse(function): + + 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': + #NOTE only the option (0, 0, { values }) is supported for many2many + if value[0][0] == 6: + return value[0][2] + + elif self._type == 'one2many': + if not read_value: + read_value=[] + relation_obj = obj.pool.get(self.relation) + for vals in value: + 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]) + read_value.remove(vals[1]) + return read_value + return value + + + def _fnct_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 = record.__getattr__(self.serialization_field) + # we have to delete the key in the json when the value is null + if value is None: + if field_name in serialized: + del serialized[field_name] + else: + # nothing to do, we dont wan't to store the key with a null value + continue + 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 + + def _fnct_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 = record.__getattr__(self.serialization_field) + results[record.id] ={} + for field_name in field_names: + if obj._columns[field_name]._type in ['one2many']: + results[record.id].update({field_name : serialized.get(field_name, [])}) + else: + results[record.id].update({field_name : serialized.get(field_name)}) + return results + + def __init__(self, serialization_field, **kwargs): + self.serialization_field = serialization_field + #assert serialization_field._type == 'serialized' + return super(sparse, self).__init__(self._fnct_read, fnct_inv=self._fnct_write, multi='_json_multi', method=True, **kwargs) + + + + + # --------------------------------------------------------- # Dummy fields # --------------------------------------------------------- @@ -1200,14 +1294,26 @@ class dummy(function): # --------------------------------------------------------- # Serialized fields # --------------------------------------------------------- + class serialized(_column): - def __init__(self, string='unknown', serialize_func=repr, deserialize_func=eval, type='text', **args): - self._serialize_func = serialize_func - self._deserialize_func = deserialize_func - self._type = type - self._symbol_set = (self._symbol_c, self._serialize_func) - self._symbol_get = self._deserialize_func - super(serialized, self).__init__(string=string, **args) + """ A field able to store an arbitrary python data structure. + + Note: only plain components allowed. + """ + + def _symbol_set_struct(val): + return json.dumps(val) + + def _symbol_get_struct(self, val): + return json.loads(val or '{}') + + _prefetch = False + _type = 'serialized' + + _symbol_c = '%s' + _symbol_f = _symbol_set_struct + _symbol_set = (_symbol_c, _symbol_f) + _symbol_get = _symbol_get_struct # TODO: review completly this class for speed improvement class property(function): diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 58d35e21775..357204bd374 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -552,6 +552,7 @@ FIELDS_TO_PGTYPES = { fields.datetime: 'timestamp', fields.binary: 'bytea', fields.many2one: 'int4', + fields.serialized: 'text', } def get_pg_type(f, type_override=None): @@ -754,7 +755,17 @@ class BaseModel(object): for rec in cr.dictfetchall(): cols[rec['name']] = rec - for (k, f) in self._columns.items(): + ir_model_fields_obj = self.pool.get('ir.model.fields') + + high_priority_items=[] + low_priority_items=[] + #sparse field should be created at the end, indeed this field depend of other field + for item in self._columns.items(): + if item[1].__class__ == fields.sparse: + low_priority_items.append(item) + else: + high_priority_items.append(item) + for (k, f) in high_priority_items + low_priority_items: vals = { 'model_id': model_id, 'model': self._name, @@ -769,7 +780,14 @@ class BaseModel(object): 'selectable': (f.selectable and 1) or 0, 'translate': (f.translate and 1) or 0, 'relation_field': (f._type=='one2many' and isinstance(f, fields.one2many)) and f._fields_id or '', + 'serialization_field_id': None, } + if 'serialization_field' in dir(f): + serialization_field_id = ir_model_fields_obj.search(cr, 1, [('model','=',vals['model']), ('name', '=', f.serialization_field)]) + if not serialization_field_id: + raise except_orm(_('Error'), _("The field %s does not exist!" %f.serialization_field)) + vals['serialization_field_id'] = serialization_field_id[0] + # When its a custom field,it does not contain f.select if context.get('field_state', 'base') == 'manual': if context.get('field_name', '') == k: @@ -784,13 +802,13 @@ class BaseModel(object): vals['id'] = id cr.execute("""INSERT INTO ir_model_fields ( id, model_id, model, name, field_description, ttype, - relation,view_load,state,select_level,relation_field, translate + relation,view_load,state,select_level,relation_field, translate, serialization_field_id ) VALUES ( - %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s + %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s )""", ( id, vals['model_id'], vals['model'], vals['name'], vals['field_description'], vals['ttype'], vals['relation'], bool(vals['view_load']), 'base', - vals['select_level'], vals['relation_field'], bool(vals['translate']) + vals['select_level'], vals['relation_field'], bool(vals['translate']), vals['serialization_field_id'] )) if 'module' in context: name1 = 'field_' + self._table + '_' + k @@ -807,12 +825,12 @@ class BaseModel(object): cr.commit() cr.execute("""UPDATE ir_model_fields SET model_id=%s, field_description=%s, ttype=%s, relation=%s, - view_load=%s, select_level=%s, readonly=%s ,required=%s, selectable=%s, relation_field=%s, translate=%s + view_load=%s, select_level=%s, readonly=%s ,required=%s, selectable=%s, relation_field=%s, translate=%s, serialization_field_id=%s WHERE model=%s AND name=%s""", ( vals['model_id'], vals['field_description'], vals['ttype'], vals['relation'], bool(vals['view_load']), - vals['select_level'], bool(vals['readonly']), bool(vals['required']), bool(vals['selectable']), vals['relation_field'], bool(vals['translate']), vals['model'], vals['name'] + vals['select_level'], bool(vals['readonly']), bool(vals['required']), bool(vals['selectable']), vals['relation_field'], bool(vals['translate']), vals['serialization_field_id'], vals['model'], vals['name'] )) break cr.commit() @@ -1001,6 +1019,12 @@ class BaseModel(object): #'select': int(field['select_level']) } + if field['serialization_field_id']: + cr.execute('SELECT name FROM ir_model_fields WHERE id=%s', (field['serialization_field_id'],)) + attrs.update({'serialization_field': cr.fetchone()[0], 'type': field['ttype']}) + if field['ttype'] in ['many2one', 'one2many', 'many2many']: + attrs.update({'relation': field['relation']}) + self._columns[field['name']] = fields.sparse(**attrs) if field['ttype'] == 'selection': self._columns[field['name']] = fields.selection(eval(field['selection']), **attrs) elif field['ttype'] == 'reference':