From 1a3432b439dc56868acf7a628c32bd0a994a5ebb Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Fri, 7 Jan 2011 11:26:55 +0200 Subject: [PATCH 1/2] ir.model: move the check inside create() and write(), simplfy defaults. Since the create() function does call cr.commit(), the validity check for selection options shall happen before the call to orm._auto_init(). bzr revid: p_christ@hol.gr-20110107092655-kokkx3pz2ilawyc0 --- bin/addons/base/ir/ir_model.py | 72 +++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 23 deletions(-) diff --git a/bin/addons/base/ir/ir_model.py b/bin/addons/base/ir/ir_model.py index 5cd65a65d25..a1bee8bfeaf 100644 --- a/bin/addons/base/ir/ir_model.py +++ b/bin/addons/base/ir/ir_model.py @@ -183,36 +183,41 @@ class ir_model_fields(osv.osv): } _rec_name='field_description' _defaults = { - 'view_load': lambda *a: 0, - 'selection': lambda *a: "[('key','label')]", - 'domain': lambda *a: "[]", - 'name': lambda *a: 'x_', + 'view_load': 0, + 'selection': "", + 'domain': "[]", + 'name': 'x_', 'state': lambda self,cr,uid,ctx={}: (ctx and ctx.get('manual',False)) and 'manual' or 'base', - 'on_delete': lambda *a: 'set null', - 'select_level': lambda *a: '0', - 'size': lambda *a: 64, - 'field_description': lambda *a: '', - 'selectable': lambda *a: 1, + 'on_delete': 'set null', + 'select_level': '0', + 'size': 64, + 'field_description': '', + 'selectable': 1, } _order = "name" - def _check_selection(self, cr, uid, ids, context=None): - selection_field = self.browse(cr, uid, ids[0], context=context) + def _check_selection(self, cr, uid, selection, context=None): try: - selection_list = eval(selection_field.selection) + selection_list = eval(selection) except Exception: - logging.getLogger('ir.model').error('Invalid selection list definition for fields.selection %s', selection_field.name , exc_info=True) - return False - if not (isinstance(selection_list, list) and selection_list): - return False - for selection_item in selection_list: - if not (isinstance(selection_item, (tuple,list)) and len(selection_item) == 2): - return False - return True + logging.getLogger('ir.model').warning('Invalid selection list definition for fields.selection', exc_info=True) + raise except_orm(_('Error'), + _("The Selection Options expression is not a valid Pythonic expression." \ + "Please provide an expression in the [('key','Label'), ...] format.")) - _constraints = [ - (_check_selection, "Wrong list of values specified for a field of type selection, it should be written as [('key','value')]", ['selection']) - ] + check = True + if not (isinstance(selection_list, list) and selection_list): + check = False + else: + for item in selection_list: + if not (isinstance(item, (tuple,list)) and len(item) == 2): + check = False + break + + if not check: + raise except_orm(_('Error'), + _("The Selection Options expression is must be in the [('key','Label'), ...] format!")) + return True def _size_gt_zero_msg(self, cr, user, ids, context=None): return _('Size of the field can never be less than 1 !') @@ -220,6 +225,7 @@ class ir_model_fields(osv.osv): _sql_constraints = [ ('size_gt_zero', 'CHECK (size>0)',_size_gt_zero_msg ), ] + def unlink(self, cr, user, ids, context=None): for field in self.browse(cr, user, ids, context): if field.state <> 'manual': @@ -239,6 +245,10 @@ class ir_model_fields(osv.osv): context = {} if context and context.get('manual',False): vals['state'] = 'manual' + if vals.get('ttype', False) == 'selection': + if not vals.get('selection',False): + raise except_orm(_('Error'), _('For selection fields, the Selection Options must be given!')) + self._check_selection(cr, user, vals['selection'], context=context) res = super(ir_model_fields,self).create(cr, user, vals, context) if vals.get('state','base') == 'manual': if not vals['name'].startswith('x_'): @@ -256,6 +266,22 @@ class ir_model_fields(osv.osv): return res + def write(self, cr, user, ids, vals, context=None): + # TODO: we have to check/restrict many more conditions here, like + # changing the type, name or model_id of fields eg. + + if 'selection' in vals: + have_selection = False + for item in self.browse(cr, user, ids, context=context): + if item.ttype == 'selection': + have_selection = True + break + if have_selection: + self._check_selection(cr, user, vals['selection'], context=context) + res = super(ir_model_fields,self).write(cr, user, ids, vals, context=context) + # TODO do we need to perform _auto_init here, too? + return res + ir_model_fields() class ir_model_access(osv.osv): From 89ec68926defbc1789c8cb7c884c943f4536f39b Mon Sep 17 00:00:00 2001 From: "P. Christeas" Date: Fri, 7 Jan 2011 11:29:17 +0200 Subject: [PATCH 2/2] ir.model.fields: implement the write(), allow custom fields to change Implement the missing feature of being able to modify those custom fields in the database, through the ORM interface. Now, using the ir.model or ir.model.fields forms, the admin is able to alter properties of custom fields on a live server (without server restart). In the future, we may even remove the fuse for base fields and allow them to change, too. bzr revid: p_christ@hol.gr-20110107092917-xh00qjbap7ixggwr --- bin/addons/base/ir/ir_model.py | 124 ++++++++++++++++++++++++++++++--- 1 file changed, 114 insertions(+), 10 deletions(-) diff --git a/bin/addons/base/ir/ir_model.py b/bin/addons/base/ir/ir_model.py index a1bee8bfeaf..289e2124e47 100644 --- a/bin/addons/base/ir/ir_model.py +++ b/bin/addons/base/ir/ir_model.py @@ -267,19 +267,123 @@ class ir_model_fields(osv.osv): return res def write(self, cr, user, ids, vals, context=None): - # TODO: we have to check/restrict many more conditions here, like - # changing the type, name or model_id of fields eg. + if context is None: + context = {} + if context and context.get('manual',False): + vals['state'] = 'manual' - if 'selection' in vals: - have_selection = False + have_custom_fields = False + column_rename = None # if set, *one* column can be renamed here + obj = None + models = {} # structs of (obj, [(field, prop, change_to),..]) + # data to be updated on the orm model + + # static table of properties + model_props = [ # (our-name, fields.prop, set_fn) + ('field_description', 'string', lambda a: a), + ('required', 'required', bool), + ('readonly', 'readonly', bool), + ('domain', '_domain', lambda a: a), + ('size', 'size', int), + ('on_delete', 'ondelete', str), + ('translate', 'translate', bool), + ('view_load', 'view_load', bool), + ('selectable', 'selectable', bool), + ('select_level', 'select', int), + ('selection', 'selection', eval), + ] + + if vals and ids: + checked_selection = False # need only check it once, so defer + for item in self.browse(cr, user, ids, context=context): - if item.ttype == 'selection': - have_selection = True - break - if have_selection: - self._check_selection(cr, user, vals['selection'], context=context) + if not (obj and obj._name == item.model): + obj = self.pool.get(item.model) + + if item.state == 'manual': + have_custom_fields = True + else: + for prop in ['name', 'model_id', 'ttype', 'relation', 'relation_field', + 'size', 'selection', 'required', 'readonly', 'translate', + 'domain', 'groups']: + if prop in vals and getattr(item, prop) != vals[prop]: + raise except_orm(_('Error!'), + _('Properties of base fields cannot be set here! ' + 'Please modify them through Python code, ' + 'preferably through a custom addon!')) + + if item.ttype == 'selection' and 'selection' in vals \ + and not checked_selection: + self._check_selection(cr, user, vals['selection'], context=context) + checked_selection = True + + final_name = item.name + if 'name' in vals and vals['name'] != item.name: + # We need to rename the column + if column_rename: + raise except_orm(_('Error!'), _('Can only rename one column at a time!')) + if vals['name'] in obj._columns: + raise except_orm(_('Error!'), _('Cannot rename column to %s, because that column already exists!') % vals['name']) + if vals.get('state', 'base') == 'manual' and not vals['name'].startswith('x_'): + raise except_orm(_('Error!'), _('New column name must still start with x_ , because it is a custom field!')) + if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']: + raise ValueError('Invalid character in column name') + column_rename = (obj, (obj._table, item.name, vals['name'])) + final_name = vals['name'] + + if 'model_id' in vals and vals['model_id'] != item.model_id: + raise except_orm(_("Error!"), _("Changing the model of a field is forbidden!")) + + if 'ttype' in vals and vals['ttype'] != item.ttype: + raise except_orm(_("Error!"), _("Changing the type of a column is not yet supported. " + "Please drop it and create it again!")) + + # We don't check the 'state', because it might come from the context + # (thus be set for multiple fields) and will be ignored anyway. + + if obj: + models.setdefault(obj._name, (obj,[])) + # find out which values (per model) we need to update there + for vname, fprop, set_fn in model_props: + if vname in vals: + prop_val = set_fn(vals[vname]) + if getattr(obj._columns[item.name], fprop) != prop_val: + models[obj._name][1].append((final_name, fprop, prop_val)) + # our dict is ready here, but no properties are changed so far + + # These shall never be written (modified) + if 'model_id' in vals: + del vals['model_id'] + if 'model' in vals: + del vals['model'] + if 'state' in vals: + del vals['state'] + res = super(ir_model_fields,self).write(cr, user, ids, vals, context=context) - # TODO do we need to perform _auto_init here, too? + + if column_rename: + cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO "%s"' % column_rename[1]) + # This is VERY risky, but let us have this feature: + # we want to change the key of column in obj._columns dict + col = column_rename[0]._columns.pop(column_rename[1][1]) # take object out, w/o copy + column_rename[0]._columns[column_rename[1][2]] = col + + if models: + # We have to update _columns of the model(s) and then call their + # _auto_init to sync the db with the model. Hopefully, since write() + # was called earlier, they will be in-sync before the _auto_init. + # Anything we don't update in _columns now will be reset from + # the model into ir.model.fields (db). + ctx = context.copy() + if have_custom_fields: + ctx.update({'select': vals.get('select_level','0'),'update_custom_fields':True}) + + for mkey, mstruct in models.items(): + obj = mstruct[0] + + for col_name, col_prop, val in mstruct[1]: + setattr(obj._columns[col_name], col_prop, val) + obj._auto_init(cr, ctx) return res ir_model_fields()