diff --git a/odoo/addons/base/ir/ir_actions.py b/odoo/addons/base/ir/ir_actions.py index aa4e2055afe..af88b8ab14a 100644 --- a/odoo/addons/base/ir/ir_actions.py +++ b/odoo/addons/base/ir/ir_actions.py @@ -228,7 +228,8 @@ class IrActionsActWindow(models.Model): if len(existing) < len(self): # mark missing records in cache with a failed value exc = MissingError(_("Record does not exist or has been deleted.")) - (self - existing)._cache.update(fields.FailedValue(exc)) + for record in (self - existing): + record._cache.set_failed(self._fields, exc) return existing @api.model diff --git a/odoo/api.py b/odoo/api.py index 4f4f93071fd..dbac3921988 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -941,7 +941,7 @@ class Environment(Mapping): def check_cache(self): """ Check the cache consistency. """ - from odoo.fields import SpecialValue + from odoo.models import SpecialValue # make a full copy of the cache, and invalidate it cache_dump = dict( diff --git a/odoo/fields.py b/odoo/fields.py index d5638541b48..9a2d737b7f2 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -37,24 +37,6 @@ _schema = logging.getLogger(__name__[:-7] + '.schema') Default = object() # default value for __init__() methods -class SpecialValue(object): - """ Encapsulates a value in the cache in place of a normal value. """ - def __init__(self, value): - self.value = value - def get(self): - return self.value - -class FailedValue(SpecialValue): - """ Special value that encapsulates an exception instead of a value. """ - def __init__(self, exception): - self.exception = exception - def get(self): - raise self.exception - -def _check_value(value): - """ Return ``value``, or call its getter if ``value`` is a :class:`SpecialValue`. """ - return value.get() if isinstance(value, SpecialValue) else value - def copy_cache(records, env): """ Recursively copy the cache of ``records`` to the environment ``env``. """ todo, done = set(records), set() @@ -911,14 +893,14 @@ class Field(MetaField('DummyField', (object,), {})): # only a single record may be accessed record.ensure_one() try: - value = record._cache[self] + value = record._cache[self.name] except KeyError: # cache miss, determine value and retrieve it if record.id: self.determine_value(record) else: self.determine_draft_value(record) - value = record._cache[self] + value = record._cache[self.name] else: # null record -> return the null value for this field value = self.convert_to_cache(False, record, validate=False) @@ -940,7 +922,7 @@ class Field(MetaField('DummyField', (object,), {})): spec = self.modified_draft(record) # set value in cache, inverse field, and mark record as dirty - record._cache[self] = value + record._cache[self.name] = value if env.in_onchange: for invf in record._field_inverses[self]: invf._update(record[self.name], record) @@ -957,7 +939,7 @@ class Field(MetaField('DummyField', (object,), {})): record.write({self.name: write_value}) # Update the cache unless value contains a new record if not (self.relational and not all(value)): - record._cache[self] = value + record._cache[self.name] = value ############################################################################ # @@ -970,7 +952,7 @@ class Field(MetaField('DummyField', (object,), {})): fields = records._field_computed[self] for field in fields: for record in records: - record._cache[field] = field.convert_to_cache(False, record, validate=False) + record._cache[field.name] = field.convert_to_cache(False, record, validate=False) if isinstance(self.compute, pycompat.string_types): getattr(records, self.compute)() else: @@ -988,7 +970,7 @@ class Field(MetaField('DummyField', (object,), {})): try: self._compute_value(record) except Exception as exc: - record._cache[self.name] = FailedValue(exc) + record._cache.set_failed([self.name], exc) def determine_value(self, record): """ Determine the value of ``self`` for ``record``. """ @@ -1007,12 +989,10 @@ class Field(MetaField('DummyField', (object,), {})): computed = record._field_computed[self] for source, target in pycompat.izip(recs, recs.with_env(env)): try: - values = target._convert_to_cache({ - f.name: source[f.name] for f in computed - }, validate=False) - except MissingError as e: - values = FailedValue(e) - target._cache.update(values) + values = {f.name: source[f.name] for f in computed} + target._cache.update(target._convert_to_cache(values, validate=False)) + except MissingError as exc: + target._cache.set_failed(target._fields, exc) # the result is saved to database by BaseModel.recompute() return @@ -1031,7 +1011,7 @@ class Field(MetaField('DummyField', (object,), {})): else: # this is a non-stored non-computed field - record._cache[self] = self.convert_to_cache(False, record, validate=False) + record._cache[self.name] = self.convert_to_cache(False, record, validate=False) def determine_draft_value(self, record): """ Determine the value of ``self`` for the given draft ``record``. """ @@ -1041,7 +1021,7 @@ class Field(MetaField('DummyField', (object,), {})): self._compute_value(record) else: null = self.convert_to_cache(False, record, validate=False) - record._cache[self] = SpecialValue(null) + record._cache.set_special(self.name, lambda: null) def determine_inverse(self, records): """ Given the value of ``self`` on ``records``, inverse the computation. """ @@ -1137,7 +1117,8 @@ class Integer(Field): def _update(self, records, value): # special case, when an integer field is used as inverse for a one2many - records._cache[self] = value.id or 0 + for record in records: + record._cache[self.name] = value.id or 0 def convert_to_export(self, value, record): if value or value == 0: @@ -1923,11 +1904,9 @@ class Many2one(_Relational): model.env['ir.model.constraint']._reflect_constraint(model, conname, 'f', None, self._module) def _update(self, records, value): - """ Update the cached value of ``self`` for ``records`` with ``value``. - This is used to reflect the assignment ``value[name] = records``, where - ``name`` is the inverse field of ``self``. - """ - records._cache[self] = self.convert_to_cache(value, records, validate=False) + """ Update the cached value of ``self`` for ``records`` with ``value``. """ + for record in records: + record._cache[self.name] = self.convert_to_cache(value, record, validate=False) def convert_to_column(self, value, record, values=None): return value or None @@ -1982,22 +1961,6 @@ class Many2one(_Relational): return False return super(Many2one, self).convert_to_onchange(value, record, fnames) -class UnionUpdate(SpecialValue): - """ Placeholder for a value update; when this value is taken from the cache, - it returns ``record[field.name] | value`` and stores it in the cache. - """ - def __init__(self, field, record, value): - self.args = (field, record, value) - - def get(self): - field, record, value = self.args - # in order to read the current field's value, remove self from cache - del record._cache[field] - # read the current field's value, and update it in cache only - value = field.convert_to_cache(record[field.name] | value, record, validate=False) - record._cache[field] = value - return value - class _RelationalMulti(_Relational): """ Abstract class for relational fields *2many. """ @@ -2005,11 +1968,20 @@ class _RelationalMulti(_Relational): def _update(self, records, value): """ Update the cached value of ``self`` for ``records`` with ``value``. """ for record in records: - if self in record._cache: + if self.name in record._cache: val = self.convert_to_cache(record[self.name] | value, record, validate=False) + record._cache[self.name] = val else: - val = UnionUpdate(self, record, value) - record._cache[self] = val + record._cache.set_special(self.name, self._update_getter(record, value)) + + def _update_getter(self, record, value): + def getter(): + # determine the current field's value, and update it in cache only + del record._cache[self.name] + val = self.convert_to_cache(record[self.name] | value, record, validate=False) + record._cache[self.name] = val + return val + return getter def convert_to_cache(self, value, record, validate=True): # cache format: tuple(ids) diff --git a/odoo/models.py b/odoo/models.py index 5b7081474b8..31a096673f1 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2571,13 +2571,13 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): result = self.read([f.name for f in fs], load='_classic_write') # check the cache, and update it if necessary - if field not in self._cache: + if not self._cache.has_value(field.name): for values in result: record = self.browse(values.pop('id'), self._prefetch) record._cache.update(record._convert_to_cache(values, validate=False)) - if not self._cache.contains(field): - e = AccessError("No value found for %s.%s" % (self, field.name)) - self._cache[field] = FailedValue(e) + if field.name not in self._cache: + exc = AccessError("No value found for %s.%s" % (self, field.name)) + self._cache.set_failed([field.name], exc) @api.multi def _read_from_database(self, field_names, inherited_field_names=[]): @@ -2674,15 +2674,16 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): # mark non-existing records in missing forbidden = missing.exists() if forbidden: + _logger.info( + _('The requested operation cannot be completed due to record rules: Document type: %s, Operation: %s, Records: %s, User: %s') % \ + (self._name, 'read', ','.join([str(r.id) for r in self][:6]), self._uid)) # store an access error exception in existing records exc = AccessError( _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \ (self._name, 'read') ) - _logger.info( - _('The requested operation cannot be completed due to record rules: Document type: %s, Operation: %s, Records: %s, User: %s') % \ - (self._name, 'read', ','.join([str(r.id) for r in self][:6]), self._uid)) - forbidden._cache.update(FailedValue(exc)) + for record in forbidden: + record._cache.set_failed(self._fields, exc) @api.multi def get_metadata(self): @@ -3869,7 +3870,8 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): if len(existing) < len(self): # mark missing records in cache with a failed value exc = MissingError(_("Record does not exist or has been deleted.")) - (self - existing)._cache.update(FailedValue(exc)) + for record in (self - existing): + record._cache.set_failed(self._fields, exc) return existing @api.multi @@ -4419,7 +4421,10 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): for name in name_seq.split('.'): field = recs._fields[name] null = field.convert_to_cache(False, self, validate=False) - recs = recs.mapped(lambda rec: field.convert_to_record(rec._cache.get(field, null), rec)) + if recs: + recs = recs.mapped(lambda rec: field.convert_to_record(rec._cache.get_value(name, null), rec)) + else: + recs = field.convert_to_record(null, recs) return recs def filtered(self, func): @@ -5011,81 +5016,77 @@ collections.Set.register(BaseModel) collections.Sequence.register(BaseModel) class RecordCache(MutableMapping): - """ Implements a proxy dictionary to read/update the cache of a record. - Upon iteration, it looks like a dictionary mapping field names to - values. However, fields may be used as keys as well. - """ - def __init__(self, records): - self._recs = records + """ A mapping from field names to values, to read and update the cache of a record. """ + def __init__(self, record): + assert len(record) == 1, "Unexpected RecordCache(%s)" % record + self._record = record - def contains(self, field): - """ Return whether `records[0]` has a value for ``field`` in cache. """ - if isinstance(field, pycompat.string_types): - field = self._recs._fields[field] - return self._recs.id in self._recs.env.cache[field] + def __contains__(self, name): + """ Return whether `record` has a cached value for field ``name``. """ + field = self._record._fields[name] + return self._record.id in self._record.env.cache[field] - def __contains__(self, field): - """ Return whether `records[0]` has a regular value for ``field`` in cache. """ - if isinstance(field, pycompat.string_types): - field = self._recs._fields[field] - dummy = SpecialValue(None) - value = self._recs.env.cache[field].get(self._recs.id, dummy) - return not isinstance(value, SpecialValue) - - def get(self, field, default=None): - """ Return the cached, regular value of ``field`` for `records[0]`, or ``default``. """ - if isinstance(field, pycompat.string_types): - field = self._recs._fields[field] - dummy = SpecialValue(None) - value = self._recs.env.cache[field].get(self._recs.id, dummy) - return default if isinstance(value, SpecialValue) else value - - def __getitem__(self, field): - """ Return the cached value of ``field`` for `records[0]`. """ - if isinstance(field, pycompat.string_types): - field = self._recs._fields[field] - value = self._recs.env.cache[field][self._recs.id] + def __getitem__(self, name): + """ Return the cached value of field ``name`` for `record`. """ + field = self._record._fields[name] + value = self._record.env.cache[field][self._record.id] return value.get() if isinstance(value, SpecialValue) else value - def __setitem__(self, field, value): - """ Assign the cached value of ``field`` for all records in ``records``. """ - if isinstance(field, pycompat.string_types): - field = self._recs._fields[field] - values = dict.fromkeys(self._recs._ids, value) - self._recs.env.cache[field].update(values) + def __setitem__(self, name, value): + """ Assign the cached value of field ``name`` for ``record``. """ + field = self._record._fields[name] + self._record.env.cache[field][self._record.id] = value - def update(self, *args, **kwargs): - """ Update the cache of all records in ``records``. If the argument is a - ``SpecialValue``, update all fields (except "magic" columns). - """ - if args and isinstance(args[0], SpecialValue): - values = dict.fromkeys(self._recs._ids, args[0]) - for name, field in self._recs._fields.items(): - if name != 'id': - self._recs.env.cache[field].update(values) - else: - return super(RecordCache, self).update(*args, **kwargs) - - def __delitem__(self, field): - """ Remove the cached value of ``field`` for all ``records``. """ - if isinstance(field, pycompat.string_types): - field = self._recs._fields[field] - field_cache = self._recs.env.cache[field] - for id in self._recs._ids: - field_cache.pop(id, None) + def __delitem__(self, name): + """ Remove the cached value of field ``name`` for ``record``. """ + field = self._record._fields[name] + del self._record.env.cache[field][self._record.id] def __iter__(self): - """ Iterate over the field names with a regular value in cache. """ - cache, id = self._recs.env.cache, self._recs.id - dummy = SpecialValue(None) - for name, field in self._recs._fields.items(): - if name != 'id' and not isinstance(cache[field].get(id, dummy), SpecialValue): + """ Iterate over the field names with a cached value. """ + cache, record_id = self._record.env.cache, self._record.id + for name, field in self._record._fields.items(): + if name != 'id' and record_id in cache[field]: yield name def __len__(self): - """ Return the number of fields with a regular value in cache. """ + """ Return the number of fields with a cached value. """ return sum(1 for name in self) + def has_value(self, name): + """ Return whether `record` has a cached, regular value for field ``name``. """ + field = self._record._fields[name] + dummy = SpecialValue(None) + value = self._record.env.cache[field].get(self._record.id, dummy) + return not isinstance(value, SpecialValue) + + def get_value(self, name, default=None): + """ Return the cached, regular value of field ``name`` for `record`, or ``default``. """ + field = self._record._fields[name] + dummy = SpecialValue(None) + value = self._record.env.cache[field].get(self._record.id, dummy) + return default if isinstance(value, SpecialValue) else value + + def set_special(self, name, getter): + """ Use the given getter to get the cached value of field ``name``. """ + field = self._record._fields[name] + self._record.env.cache[field][self._record.id] = SpecialValue(getter) + + def set_failed(self, names, exception): + """ Mark the given fields with the given exception. """ + def getter(): + raise exception + for name in names: + self.set_special(name, getter) + + +class SpecialValue(object): + """ Wrapper for a function to get the cached value of a field. """ + __slots__ = ['get'] + + def __init__(self, getter): + self.get = getter + AbstractModel = BaseModel @@ -5214,4 +5215,4 @@ def _normalize_ids(arg, atoms=set(IdType)): # keep those imports here to avoid dependency cycle errors from .osv import expression -from .fields import Field, SpecialValue, FailedValue +from .fields import Field