diff options
Diffstat (limited to 'openerp/fields.py')
| -rw-r--r-- | openerp/fields.py | 402 |
1 files changed, 207 insertions, 195 deletions
diff --git a/openerp/fields.py b/openerp/fields.py index a0d3f59d4b7..91a65359ee7 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -42,16 +42,43 @@ def _check_value(value): return value.get() if isinstance(value, SpecialValue) else value -def resolve_all_mro(cls, name, reverse=False): - """ Return the (successively overridden) values of attribute ``name`` in ``cls`` - in mro order, or inverse mro order if ``reverse`` is true. +def resolve_all_mro(model, name, reverse=False): + """ Return the (successively overridden) values of attribute ``name`` in the + class of ``model`` in mro order (or inverse order if ``reverse``). """ - klasses = reversed(cls.__mro__) if reverse else cls.__mro__ + klasses = reversed(type(model).__mro__) if reverse else type(model).__mro__ for klass in klasses: if name in klass.__dict__: yield klass.__dict__[name] +def default_new_to_new(field, value): + """ Convert the new-API default ``value`` to a callable. """ + return value if callable(value) else lambda model: value + +def default_new_to_old(field, value): + """ Convert the new-API default ``value`` to the old API. """ + if callable(value): + from openerp import api + return api.model(lambda model: field.convert_to_write(value(model))) + else: + return value + +def default_old_to_new(field, value): + """ Convert the old-API default ``value`` to the new API. """ + if callable(value): + return lambda model: field.convert_to_cache( + value(model._model, model._cr, model._uid, model._context), + model, validate=False, + ) + else: + return lambda model: field.convert_to_cache(value, model, validate=False) + +def default_old_to_old(field, value): + """ Convert the old-API default ``value`` to the old API. """ + return value + + class MetaField(type): """ Metaclass for field classes. """ by_type = {} @@ -256,14 +283,14 @@ class Field(object): relational = False # whether the field is a relational one _slots = { - '_attrs': EMPTY_DICT, # dictionary of field attributes; it contains: - # - all attributes after __init__() - # - free attributes only after set_class_name() + 'args': EMPTY_DICT, # the parameters given to __init__() + '_attrs': EMPTY_DICT, # the field's non-slot attributes + 'setup_full_done': False, # whether the field has been fully setup 'automatic': False, # whether the field is automatically created ("magic" field) 'inherited': False, # whether the field is inherited (_inherits) + 'origin': None, # the column from which the field was created 'column': None, # the column corresponding to the field - 'setup_done': False, # whether the field has been set up 'name': None, # name of the field 'model_name': None, # name of the model of this field @@ -293,16 +320,18 @@ class Field(object): 'change_default': False, # whether the field may trigger a "user-onchange" 'deprecated': None, # whether the field is deprecated - 'inverse_fields': (), # collection of inverse fields (objects) - 'computed_fields': (), # fields computed with the same method as self 'related_field': None, # corresponding related field - '_triggers': (), # invalidation and recomputation triggers } def __init__(self, string=None, **kwargs): kwargs['string'] = string - attrs = {key: val for key, val in kwargs.iteritems() if val is not None} - self._attrs = attrs or EMPTY_DICT + args = {key: val for key, val in kwargs.iteritems() if val is not None} + self.args = args or EMPTY_DICT + self.setup_full_done = False + + def new(self, **kwargs): + """ Return a field of the same type as ``self``, with its own parameters. """ + return type(self)(**kwargs) def __getattr__(self, name): """ Access non-slot field attribute. """ @@ -321,6 +350,15 @@ class Field(object): else: self._attrs = {name: value} # replace EMPTY_DICT + def set_all_attrs(self, attrs): + """ Set all field attributes at once (with slot defaults). """ + # optimization: we assign slots only + assign = object.__setattr__ + for key, val in self._slots.iteritems(): + assign(self, key, attrs.pop(key, val)) + if attrs: + assign(self, '_attrs', attrs) + def __delattr__(self, name): """ Remove non-slot field attribute. """ try: @@ -328,27 +366,46 @@ class Field(object): except KeyError: raise AttributeError(name) - def new(self, **kwargs): - """ Return a field of the same type as ``self``, with its own parameters. """ - return type(self)(**kwargs) + def __str__(self): + return "%s.%s" % (self.model_name, self.name) - def set_class_name(self, cls, name): - """ Assign the model class and field name of ``self``. """ - self_attrs = self._attrs - for attr, value in self._slots.iteritems(): - setattr(self, attr, value) + def __repr__(self): + return "%s.%s" % (self.model_name, self.name) - self.model_name = cls._name - self.name = name + ############################################################################ + # + # Base field setup: things that do not depend on other models/fields + # + def setup_base(self, model, name): + """ Base setup: things that do not depend on other models/fields. """ + if self.setup_full_done and not self.related: + # optimization for regular fields: keep the base setup + self.setup_full_done = False + else: + # do the base setup from scratch + self._setup_attrs(model, name) + if not self.related: + self._setup_regular_base(model) + + # + # Setup field parameter attributes + # + + def _setup_attrs(self, model, name): + """ Determine field parameter attributes. """ # determine all inherited field attributes attrs = {} - for field in resolve_all_mro(cls, name, reverse=True): + for field in resolve_all_mro(model, name, reverse=True): if isinstance(field, type(self)): - attrs.update(field._attrs) + attrs.update(field.args) else: attrs.clear() - attrs.update(self_attrs) # necessary in case self is not in cls + attrs.update(self.args) # necessary in case self is not in class + + attrs['args'] = self.args + attrs['model_name'] = model._name + attrs['name'] = name # initialize ``self`` with ``attrs`` if attrs.get('compute'): @@ -362,125 +419,108 @@ class Field(object): attrs['copy'] = attrs.get('copy', False) # fix for function fields overridden by regular columns - if not isinstance(attrs.get('column'), (NoneType, fields.function)): + if not isinstance(attrs.get('origin'), (NoneType, fields.function)): attrs.pop('store', None) - for attr, value in attrs.iteritems(): - setattr(self, attr, value) + self.set_all_attrs(attrs) if not self.string and not self.related: # related fields get their string from their parent field self.string = name.replace('_', ' ').capitalize() - # determine self.default and cls._defaults in a consistent way - self._determine_default(cls, name) + self._setup_default(model, name) - def _determine_default(self, cls, name): - """ Retrieve the default value for ``self`` in the hierarchy of ``cls``, and - determine ``self.default`` and ``cls._defaults`` accordingly. - """ + def _setup_default(self, model, name): + """ Determine ``self.default`` and the corresponding ``model._defaults``. """ self.default = None + model._defaults.pop(name, None) # traverse the class hierarchy upwards, and take the first field # definition with a default or _defaults for self - for klass in cls.__mro__: + for klass in type(model).__mro__: if name in klass.__dict__: field = klass.__dict__[name] if not isinstance(field, type(self)): # klass contains another value overridden by self return - if 'default' in field._attrs: - # take the default in field, and adapt it for cls._defaults - value = field._attrs['default'] - if callable(value): - from openerp import api - self.default = value - cls._defaults[name] = api.model( - lambda recs: self.convert_to_write(value(recs)) - ) - else: - self.default = lambda recs: value - cls._defaults[name] = value + if 'default' in field.args: + # take the value, and adapt it for model._defaults + value = field.args['default'] + self.default = default_new_to_new(self, value) + model._defaults[name] = default_new_to_old(self, value) return defaults = klass.__dict__.get('_defaults') or {} if name in defaults: # take the value from _defaults, and adapt it for self.default value = defaults[name] - if callable(value): - func = lambda recs: value(recs._model, recs._cr, recs._uid, recs._context) - else: - func = lambda recs: value - self.default = lambda recs: self.convert_to_cache( - func(recs), recs, validate=False, - ) - cls._defaults[name] = value + self.default = default_old_to_new(self, value) + model._defaults[name] = default_old_to_old(self, value) return - def __str__(self): - return "%s.%s" % (self.model_name, self.name) - - def __repr__(self): - return "%s.%s" % (self.model_name, self.name) - ############################################################################ # - # Field setup + # Full field setup: everything else, except recomputation triggers # - def setup(self, env): - """ Make sure that ``self`` is set up, except for recomputation triggers. """ - if not self.setup_done: - if self.related: - self._setup_related(env) + def setup_full(self, model): + """ Full setup: everything else, except recomputation triggers. """ + if not self.setup_full_done: + if not self.related: + self._setup_regular_full(model) else: - self._setup_regular(env) - self.setup_done = True + self._setup_related_full(model) + self.setup_full_done = True # # Setup of non-related fields # - def _setup_regular(self, env): + def _setup_regular_base(self, model): """ Setup the attributes of a non-related field. """ - recs = env[self.model_name] - def make_depends(deps): - return tuple(deps(recs) if callable(deps) else deps) + return tuple(deps(model) if callable(deps) else deps) + + def make_callable(name): + return lambda recs, *args, **kwargs: getattr(recs, name)(*args, **kwargs) # convert compute into a callable and determine depends if isinstance(self.compute, basestring): # if the compute method has been overridden, concatenate all their _depends self.depends = () - for method in resolve_all_mro(type(recs), self.compute, reverse=True): + for method in resolve_all_mro(model, self.compute, reverse=True): self.depends += make_depends(getattr(method, '_depends', ())) - self.compute = getattr(type(recs), self.compute) + self.compute = make_callable(self.compute) else: self.depends = make_depends(getattr(self.compute, '_depends', ())) # convert inverse and search into callables if isinstance(self.inverse, basestring): - self.inverse = getattr(type(recs), self.inverse) + self.inverse = make_callable(self.inverse) if isinstance(self.search, basestring): - self.search = getattr(type(recs), self.search) + self.search = make_callable(self.search) + + def _setup_regular_full(self, model): + """ Setup the inverse field(s) of ``self``. """ + pass # # Setup of related fields # - def _setup_related(self, env): + def _setup_related_full(self, model): """ Setup the attributes of a related field. """ # fix the type of self.related if necessary if isinstance(self.related, basestring): self.related = tuple(self.related.split('.')) # determine the chain of fields, and make sure they are all set up - recs = env[self.model_name] + target = model for name in self.related: - field = recs._fields[name] - field.setup(env) - recs = recs[name] + field = target._fields[name] + field.setup_full(target) + target = target[name] self.related_field = field @@ -541,7 +581,7 @@ class Field(object): """ Determine the domain to search on field ``self``. """ return [('.'.join(self.related), operator, value)] - # properties used by _setup_related() to copy values from related field + # properties used by _setup_related_full() to copy values from related field _related_comodel_name = property(attrgetter('comodel_name')) _related_string = property(attrgetter('string')) _related_help = property(attrgetter('help')) @@ -562,54 +602,27 @@ class Field(object): # See method ``modified`` below for details. # - def add_trigger(self, trigger): - """ Add a recomputation trigger on ``self``. """ - if trigger not in self._triggers: - self._triggers += (trigger,) - def setup_triggers(self, env): """ Add the necessary triggers to invalidate/recompute ``self``. """ - model = env[self.model_name] - for path in self.depends: - self._setup_dependency([], model, path.split('.')) - - def _setup_dependency(self, path0, model, path1): - """ Make ``self`` depend on ``model``; `path0 + path1` is a dependency of - ``self``, and ``path0`` is the sequence of field names from ``self.model`` - to ``model``. - """ - env = model.env - head, tail = path1[0], path1[1:] - - if head == '*': - # special case: add triggers on all fields of model (except self) - fields = set(model._fields.itervalues()) - set([self]) - else: - fields = [model._fields[head]] - - for field in fields: - if field == self: - _logger.debug("Field %s is recursively defined", self) - self.recursive = True - continue - - #_logger.debug("Add trigger on %s to recompute %s", field, self) - field.add_trigger((self, '.'.join(path0 or ['id']))) - - # add trigger on inverse fields, too - for invf in field.inverse_fields: - #_logger.debug("Add trigger on %s to recompute %s", invf, self) - invf.add_trigger((self, '.'.join(path0 + [head]))) - - # recursively traverse the dependency - if tail: - comodel = env[field.comodel_name] - self._setup_dependency(path0 + [head], comodel, tail) - - @property - def dependents(self): - """ Return the computed fields that depend on ``self``. """ - return (field for field, path in self._triggers) + for path_str in self.depends: + path = path_str.split('.') + + # traverse path and add triggers on fields along the way + field = None + for i, name in enumerate(path): + model = env[field.comodel_name if field else self.model_name] + field = model._fields[name] + # env[self.model_name] --- path[:i] --> model with field + + if field is self: + self.recursive = True + continue + + # add trigger on field and its inverses to recompute self + model._field_triggers.add(field, (self, '.'.join(path[:i] or ['id']))) + for invf in model._field_inverses[field]: + invm = env[invf.model_name] + invm._field_triggers.add(invf, (self, '.'.join(path[:i+1]))) ############################################################################ # @@ -670,9 +683,11 @@ class Field(object): def to_column(self): """ Return a column object corresponding to ``self``, or ``None``. """ - if not self.store and (self.compute or not self.column): + if self.column: + return self.column + + if not self.store and (self.compute or not self.origin): # non-stored computed fields do not have a corresponding column - self.column = None return None # determine column parameters @@ -688,9 +703,9 @@ class Field(object): args['type'] = self.type args['relation'] = self.comodel_name self.column = fields.property(**args) - elif self.column: - # let the column provide a valid column for the given parameters - self.column = self.column.new(_computed_field=bool(self.compute), **args) + elif self.origin: + # let the origin provide a valid column for the given parameters + self.column = self.origin.new(_computed_field=bool(self.compute), **args) else: # create a fresh new column of the right type self.column = getattr(fields, self.type)(**args) @@ -819,7 +834,7 @@ class Field(object): # set value in cache, inverse field, and mark record as dirty record._cache[self] = value if env.in_onchange: - for invf in self.inverse_fields: + for invf in record._field_inverses[self]: invf._update(value, record) record._set_dirty(self.name) @@ -841,11 +856,12 @@ class Field(object): def _compute_value(self, records): """ Invoke the compute method on ``records``. """ # initialize the fields to their corresponding null value in cache - for field in self.computed_fields: + computed = records._field_computed[self] + for field in computed: records._cache[field] = field.null(records.env) records.env.computed[field].update(records._ids) self.compute(records) - for field in self.computed_fields: + for field in computed: records.env.computed[field].difference_update(records._ids) def compute_value(self, records): @@ -875,10 +891,11 @@ class Field(object): self.compute_value(recs) # HACK: if result is in the wrong cache, copy values if recs.env != env: + computed = record._field_computed[self] for source, target in zip(recs, recs.with_env(env)): try: values = target._convert_to_cache({ - f.name: source[f.name] for f in self.computed_fields + f.name: source[f.name] for f in computed }, validate=False) except MissingError as e: values = FailedValue(e) @@ -933,7 +950,7 @@ class Field(object): """ # invalidate the fields that depend on self, and prepare recomputation spec = [(self, records._ids)] - for field, path in self._triggers: + for field, path in records._field_triggers[self]: if path and field.store: # don't move this line to function top, see log env = records.env(user=SUPERUSER_ID, context={'active_test': False}) @@ -959,7 +976,7 @@ class Field(object): # invalidate the fields on the records in cache that depend on # ``records``, except fields currently being computed spec = [] - for field, path in self._triggers: + for field, path in records._field_triggers[self]: target = env[field.model_name] computed = target.browse(env.computed[field]) if path == 'id': @@ -1044,14 +1061,6 @@ class Float(Field): else: return self._digits - def _setup_digits(self, env): - """ Setup the digits for ``self`` and its corresponding column """ - pass - - def _setup_regular(self, env): - super(Float, self)._setup_regular(env) - self._setup_digits(env) - _related__digits = property(attrgetter('_digits')) _related_group_operator = property(attrgetter('group_operator')) @@ -1091,11 +1100,14 @@ class Monetary(Field): _related_currency_field = property(attrgetter('currency_field')) _description_currency_field = property(attrgetter('currency_field')) - def _setup_regular(self, env): - super(Monetary, self)._setup_regular(env) + def _setup_regular_base(self, model): + super(Monetary, self)._setup_regular_base(model) if not self.currency_field: self.currency_field = 'currency_id' - assert self.currency_field in env[self.model_name]._fields, \ + + def _setup_regular_full(self, model): + super(Monetary, self)._setup_regular_full(model) + assert self.currency_field in model._fields, \ "Field %s with unknown currency_field %r" % (self, self.currency_field) def convert_to_write(self, value, target=None, fnames=None): @@ -1183,8 +1195,8 @@ class Char(_String): _related_size = property(attrgetter('size')) _description_size = property(attrgetter('size')) - def _setup_regular(self, env): - super(Char, self)._setup_regular(env) + def _setup_regular_base(self, model): + super(Char, self)._setup_regular_base(model) assert isinstance(self.size, (NoneType, int)), \ "Char field %s with non-integer size %r" % (self, self.size) @@ -1392,28 +1404,28 @@ class Selection(Field): selection = api.expected(api.model, selection) super(Selection, self).__init__(selection=selection, string=string, **kwargs) - def _setup_regular(self, env): - super(Selection, self)._setup_regular(env) + def _setup_regular_base(self, model): + super(Selection, self)._setup_regular_base(model) assert self.selection is not None, "Field %s without selection" % self - def _setup_related(self, env): - super(Selection, self)._setup_related(env) + def _setup_related_full(self, model): + super(Selection, self)._setup_related_full(model) # selection must be computed on related field field = self.related_field self.selection = lambda model: field._description_selection(model.env) - def set_class_name(self, cls, name): - super(Selection, self).set_class_name(cls, name) + def _setup_attrs(self, model, name): + super(Selection, self)._setup_attrs(model, name) # determine selection (applying 'selection_add' extensions) - for field in resolve_all_mro(cls, name, reverse=True): + for field in resolve_all_mro(model, name, reverse=True): if isinstance(field, type(self)): # We cannot use field.selection or field.selection_add here - # because those attributes are overridden by ``set_class_name``. - if 'selection' in field._attrs: - self.selection = field._attrs['selection'] - if 'selection_add' in field._attrs: + # because those attributes are overridden by ``_setup_attrs``. + if 'selection' in field.args: + self.selection = field.args['selection'] + if 'selection_add' in field.args: # use an OrderedDict to update existing values - selection_add = field._attrs['selection_add'] + selection_add = field.args['selection_add'] self.selection = OrderedDict(self.selection + selection_add).items() else: self.selection = None @@ -1482,8 +1494,8 @@ class Reference(Selection): _related_size = property(attrgetter('size')) _column_size = property(attrgetter('size')) - def _setup_regular(self, env): - super(Reference, self)._setup_regular(env) + def _setup_regular_base(self, model): + super(Reference, self)._setup_regular_base(model) assert isinstance(self.size, (NoneType, int)), \ "Reference field %s with non-integer size %r" % (self, self.size) @@ -1517,11 +1529,10 @@ class _Relational(Field): 'context': {}, # context for searching values } - def _setup_regular(self, env): - super(_Relational, self)._setup_regular(env) - if self.comodel_name not in env.registry: - _logger.warning("Field %s with unknown comodel_name %r" - % (self, self.comodel_name)) + def _setup_regular_base(self, model): + super(_Relational, self)._setup_regular_base(model) + if self.comodel_name not in model.pool: + _logger.warning("Field %s with unknown comodel_name %r", self, self.comodel_name) self.comodel_name = '_unknown' @property @@ -1549,11 +1560,11 @@ class _Relational(Field): return env[self.comodel_name] def modified(self, records): - # Invalidate cache for self.inverse_fields, too. Note that recomputation - # of fields that depend on self.inverse_fields is already covered by the - # triggers (see above). + # Invalidate cache for inverse fields, too. Note that the recomputation + # of fields that depend on inverse fields is already covered by the + # triggers. spec = super(_Relational, self).modified(records) - for invf in self.inverse_fields: + for invf in records._field_inverses[self]: spec.append((invf, None)) return spec @@ -1592,11 +1603,11 @@ class Many2one(_Relational): def __init__(self, comodel_name=None, string=None, **kwargs): super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs) - def set_class_name(self, cls, name): - super(Many2one, self).set_class_name(cls, name) + def _setup_attrs(self, model, name): + super(Many2one, self)._setup_attrs(model, name) # determine self.delegate if not self.delegate: - self.delegate = name in cls._inherits.values() + self.delegate = name in model._inherits.values() _column_ondelete = property(attrgetter('ondelete')) _column_auto_join = property(attrgetter('auto_join')) @@ -1730,7 +1741,8 @@ class _RelationalMulti(_Relational): if fnames is None: # take all fields in cache, except the inverses of self fnames = set(value._fields) - set(MAGIC_COLUMNS) - for invf in self.inverse_fields: + model = value.env[self.model_name] + for invf in model._field_inverses[self]: fnames.discard(invf.name) # add new and existing records @@ -1801,19 +1813,18 @@ class One2many(_RelationalMulti): **kwargs ) - def _setup_regular(self, env): - super(One2many, self)._setup_regular(env) - + def _setup_regular_full(self, model): + super(One2many, self)._setup_regular_full(model) if self.inverse_name: # link self to its inverse field and vice-versa - comodel = env[self.comodel_name] + comodel = model.env[self.comodel_name] invf = comodel._fields[self.inverse_name] # In some rare cases, a ``One2many`` field can link to ``Int`` field # (res_model/res_id pattern). Only inverse the field if this is # a ``Many2one`` field. if isinstance(invf, Many2one): - self.inverse_fields += (invf,) - invf.inverse_fields += (self,) + model._field_inverses.add(self, invf) + comodel._field_inverses.add(invf, self) _description_relation_field = property(attrgetter('inverse_name')) @@ -1871,23 +1882,24 @@ class Many2many(_RelationalMulti): **kwargs ) - def _setup_regular(self, env): - super(Many2many, self)._setup_regular(env) - + def _setup_regular_base(self, model): + super(Many2many, self)._setup_regular_base(model) if not self.relation and self.store: # retrieve self.relation from the corresponding column column = self.to_column() if isinstance(column, fields.many2many): - self.relation, self.column1, self.column2 = \ - column._sql_names(env[self.model_name]) + self.relation, self.column1, self.column2 = column._sql_names(model) + def _setup_regular_full(self, model): + super(Many2many, self)._setup_regular_full(model) if self.relation: - m2m = env.registry._m2m + m2m = model.pool._m2m # if inverse field has already been setup, it is present in m2m invf = m2m.get((self.relation, self.column2, self.column1)) if invf: - self.inverse_fields += (invf,) - invf.inverse_fields += (self,) + comodel = model.env[self.comodel_name] + model._field_inverses.add(self, invf) + comodel._field_inverses.add(invf, self) else: # add self in m2m, so that its inverse field can find it m2m[(self.relation, self.column1, self.column2)] = self |
