Example #1
0
 def __init__(self):
     self.envs = WeakSet()                   # weak set of environments
     self.cache = Cache()                    # cache for all records
     self.protected = StackMap()             # fields to protect {field: ids, ...}
     self.tocompute = defaultdict(set)       # recomputations {field: ids}
     # updates {model: {id: {field: value}}}
     self.towrite = defaultdict(lambda: defaultdict(dict))
Example #2
0
    def __new__(cls, cr, uid, context):
        assert context is not None
        args = (cr, uid, context)

        # if env already exists, return it
        env, envs = None, cls.envs
        for env in envs:
            if env.args == args:
                return env

        # otherwise create environment, and add it in the set
        self = object.__new__(cls)
        self.cr, self.uid, self.context = self.args = (cr, uid, frozendict(context))
        self.registry = Registry(cr.dbname)
        self.cache = envs.cache
        self._protected = StackMap()                # {field: ids, ...}
        self.dirty = defaultdict(set)               # {record: set(field_name), ...}
        self.all = envs
        envs.add(self)
        return self
Example #3
0
    def __new__(cls, cr, uid, context, su=False):
        if uid == SUPERUSER_ID:
            su = True
        assert context is not None
        args = (cr, uid, context, su)

        # if env already exists, return it
        env, envs = None, cls.envs
        for env in envs:
            if env.args == args:
                return env

        # otherwise create environment, and add it in the set
        self = object.__new__(cls)
        args = (cr, uid, frozendict(context), su)
        self.cr, self.uid, self.context, self.su = self.args = args
        self.registry = Registry(cr.dbname)
        self.cache = envs.cache
        self._protected = StackMap()                # {field: ids, ...}
        self.all = envs
        envs.add(self)
        return self
Example #4
0
File: api.py Project: 10537/odoo
    def __new__(cls, cr, uid, context):
        assert context is not None
        args = (cr, uid, context)

        # if env already exists, return it
        env, envs = None, cls.envs
        for env in envs:
            if env.args == args:
                return env

        # otherwise create environment, and add it in the set
        self = object.__new__(cls)
        self.cr, self.uid, self.context = self.args = (cr, uid, frozendict(context))
        self.registry = Registry(cr.dbname)
        self.cache = envs.cache
        self._protected = StackMap()                # {field: ids, ...}
        self.dirty = defaultdict(set)               # {record: set(field_name), ...}
        self.all = envs
        envs.add(self)
        return self
Example #5
0
class Environment(Mapping):
    """ An environment wraps data for ORM records:

        - :attr:`cr`, the current database cursor;
        - :attr:`uid`, the current user id;
        - :attr:`context`, the current context dictionary.

        It provides access to the registry by implementing a mapping from model
        names to new api models. It also holds a cache for records, and a data
        structure to manage recomputations.
    """
    _local = Local()

    @classproperty
    def envs(cls):
        return cls._local.environments

    @classmethod
    @contextmanager
    def manage(cls):
        """ Context manager for a set of environments. """
        if hasattr(cls._local, 'environments'):
            yield
        else:
            try:
                cls._local.environments = Environments()
                yield
            finally:
                release_local(cls._local)

    @classmethod
    def reset(cls):
        """ Clear the set of environments.
            This may be useful when recreating a registry inside a transaction.
        """
        cls._local.environments = Environments()

    def __new__(cls, cr, uid, context):
        assert context is not None
        args = (cr, uid, context)

        # if env already exists, return it
        env, envs = None, cls.envs
        for env in envs:
            if env.args == args:
                return env

        # otherwise create environment, and add it in the set
        self = object.__new__(cls)
        self.cr, self.uid, self.context = self.args = (cr, uid, frozendict(context))
        self.registry = Registry(cr.dbname)
        self.cache = envs.cache
        self._cache_key = (cr, uid)
        self._protected = StackMap()                # {field: ids, ...}
        self.dirty = defaultdict(set)               # {record: set(field_name), ...}
        self.all = envs
        envs.add(self)
        return self

    #
    # Mapping methods
    #

    def __contains__(self, model_name):
        """ Test whether the given model exists. """
        return model_name in self.registry

    def __getitem__(self, model_name):
        """ Return an empty recordset from the given model. """
        return self.registry[model_name]._browse(self, (), ())

    def __iter__(self):
        """ Return an iterator on model names. """
        return iter(self.registry)

    def __len__(self):
        """ Return the size of the model registry. """
        return len(self.registry)

    def __eq__(self, other):
        return self is other

    def __ne__(self, other):
        return self is not other

    def __hash__(self):
        return object.__hash__(self)

    def __call__(self, cr=None, user=None, context=None):
        """ Return an environment based on ``self`` with modified parameters.

            :param cr: optional database cursor to change the current cursor
            :param user: optional user/user id to change the current user
            :param context: optional context dictionary to change the current context
        """
        cr = self.cr if cr is None else cr
        uid = self.uid if user is None else int(user)
        context = self.context if context is None else context
        return Environment(cr, uid, context)

    def ref(self, xml_id, raise_if_not_found=True):
        """ return the record corresponding to the given ``xml_id`` """
        return self['ir.model.data'].xmlid_to_object(xml_id, raise_if_not_found=raise_if_not_found)

    @property
    def user(self):
        """ return the current user (as an instance) """
        return self(user=SUPERUSER_ID)['res.users'].browse(self.uid)

    @property
    def lang(self):
        """ return the current language code """
        return self.context.get('lang')

    @contextmanager
    def _do_in_mode(self, mode):
        if self.all.mode:
            yield
        else:
            try:
                self.all.mode = mode
                yield
            finally:
                self.all.mode = False
                self.dirty.clear()

    def do_in_draft(self):
        """ Context-switch to draft mode, where all field updates are done in
            cache only.
        """
        return self._do_in_mode(True)

    @property
    def in_draft(self):
        """ Return whether we are in draft mode. """
        return bool(self.all.mode)

    def do_in_onchange(self):
        """ Context-switch to 'onchange' draft mode, which is a specialized
            draft mode used during execution of onchange methods.
        """
        return self._do_in_mode('onchange')

    @property
    def in_onchange(self):
        """ Return whether we are in 'onchange' draft mode. """
        return self.all.mode == 'onchange'

    def clear(self):
        """ Clear all record caches, and discard all fields to recompute.
            This may be useful when recovering from a failed ORM operation.
        """
        self.cache.invalidate()
        self.all.todo.clear()

    @contextmanager
    def clear_upon_failure(self):
        """ Context manager that clears the environments (caches and fields to
            recompute) upon exception.
        """
        try:
            yield
        except Exception:
            self.clear()
            raise

    def protected(self, field):
        """ Return the recordset for which ``field`` should not be invalidated or recomputed. """
        return self[field.model_name].browse(self._protected.get(field, ()))

    @contextmanager
    def protecting(self, what, records=None):
        """ Prevent the invalidation or recomputation of fields on records.
            The parameters are either:
             - ``what`` a collection of fields and ``records`` a recordset, or
             - ``what`` a collection of pairs ``(fields, records)``.
        """
        protected = self._protected
        try:
            protected.pushmap()
            what = what if records is None else [(what, records)]
            for fields, records in what:
                for field in fields:
                    ids = protected.get(field, frozenset())
                    protected[field] = ids.union(records._ids)
            yield
        finally:
            protected.popmap()

    def field_todo(self, field):
        """ Return a recordset with all records to recompute for ``field``. """
        ids = {rid for recs in self.all.todo.get(field, ()) for rid in recs.ids}
        return self[field.model_name].browse(ids)

    def check_todo(self, field, record):
        """ Check whether ``field`` must be recomputed on ``record``, and if so,
            return the corresponding recordset to recompute.
        """
        for recs in self.all.todo.get(field, []):
            if recs & record:
                return recs

    def add_todo(self, field, records):
        """ Mark ``field`` to be recomputed on ``records``. """
        recs_list = self.all.todo.setdefault(field, [])
        for i, recs in enumerate(recs_list):
            if recs.env == records.env:
                # only add records if not already in the recordset, much much
                # cheaper in case recs is big and records is a singleton
                # already present
                if not records <= recs:
                    recs_list[i] |= records
                break
        else:
            recs_list.append(records)

    def remove_todo(self, field, records):
        """ Mark ``field`` as recomputed on ``records``. """
        recs_list = [recs - records for recs in self.all.todo.pop(field, [])]
        recs_list = [r for r in recs_list if r]
        if recs_list:
            self.all.todo[field] = recs_list

    def has_todo(self):
        """ Return whether some fields must be recomputed. """
        return bool(self.all.todo)

    def get_todo(self):
        """ Return a pair ``(field, records)`` to recompute.
            The field is such that none of its dependencies must be recomputed.
        """
        field = min(self.all.todo, key=self.registry.field_sequence)
        return field, self.all.todo[field][0]

    @property
    def recompute(self):
        return self.all.recompute

    @contextmanager
    def norecompute(self):
        tmp = self.all.recompute
        self.all.recompute = False
        try:
            yield
        finally:
            self.all.recompute = tmp

    def cache_key(self, field):
        """ Return the key to store the value of ``field`` in cache, the full
            cache key being ``(key, field, record.id)``.
        """
        return self if field.context_dependent else self._cache_key
Example #6
0
class Environment(Mapping):
    """ An environment wraps data for ORM records:

        - :attr:`cr`, the current database cursor;
        - :attr:`uid`, the current user id;
        - :attr:`context`, the current context dictionary.

        It provides access to the registry by implementing a mapping from model
        names to new api models. It also holds a cache for records, and a data
        structure to manage recomputations.
    """
    _local = Local()

    @classproperty
    def envs(cls):
        return cls._local.environments

    @classmethod
    @contextmanager
    def manage(cls):
        """ Context manager for a set of environments. """
        if hasattr(cls._local, 'environments'):
            yield
        else:
            try:
                cls._local.environments = Environments()
                yield
            finally:
                release_local(cls._local)

    @classmethod
    def reset(cls):
        """ Clear the set of environments.
            This may be useful when recreating a registry inside a transaction.
        """
        cls._local.environments = Environments()

    def __new__(cls, cr, uid, context):
        assert context is not None
        args = (cr, uid, context)

        # if env already exists, return it
        env, envs = None, cls.envs
        for env in envs:
            if env.args == args:
                return env

        # otherwise create environment, and add it in the set
        self = object.__new__(cls)
        self.cr, self.uid, self.context = self.args = (cr, uid, frozendict(context))
        self.registry = Registry(cr.dbname)
        self.cache = envs.cache
        self._cache_key = (cr, uid)
        self._protected = StackMap()                # {field: ids, ...}
        self.dirty = defaultdict(set)               # {record: set(field_name), ...}
        self.all = envs
        envs.add(self)
        return self

    #
    # Mapping methods
    #

    def __contains__(self, model_name):
        """ Test whether the given model exists. """
        return model_name in self.registry

    def __getitem__(self, model_name):
        """ Return an empty recordset from the given model. """
        return self.registry[model_name]._browse((), self)

    def __iter__(self):
        """ Return an iterator on model names. """
        return iter(self.registry)

    def __len__(self):
        """ Return the size of the model registry. """
        return len(self.registry)

    def __eq__(self, other):
        return self is other

    def __ne__(self, other):
        return self is not other

    def __hash__(self):
        return object.__hash__(self)

    def __call__(self, cr=None, user=None, context=None):
        """ Return an environment based on ``self`` with modified parameters.

            :param cr: optional database cursor to change the current cursor
            :param user: optional user/user id to change the current user
            :param context: optional context dictionary to change the current context
        """
        cr = self.cr if cr is None else cr
        uid = self.uid if user is None else int(user)
        context = self.context if context is None else context
        return Environment(cr, uid, context)

    def ref(self, xml_id, raise_if_not_found=True):
        """ return the record corresponding to the given ``xml_id`` """
        return self['ir.model.data'].xmlid_to_object(xml_id, raise_if_not_found=raise_if_not_found)

    @property
    def user(self):
        """ return the current user (as an instance) """
        return self(user=SUPERUSER_ID)['res.users'].browse(self.uid)

    @property
    def lang(self):
        """ return the current language code """
        return self.context.get('lang')

    @contextmanager
    def _do_in_mode(self, mode):
        if self.all.mode:
            yield
        else:
            try:
                self.all.mode = mode
                yield
            finally:
                self.all.mode = False
                self.dirty.clear()

    def do_in_draft(self):
        """ Context-switch to draft mode, where all field updates are done in
            cache only.
        """
        return self._do_in_mode(True)

    @property
    def in_draft(self):
        """ Return whether we are in draft mode. """
        return bool(self.all.mode)

    def do_in_onchange(self):
        """ Context-switch to 'onchange' draft mode, which is a specialized
            draft mode used during execution of onchange methods.
        """
        return self._do_in_mode('onchange')

    @property
    def in_onchange(self):
        """ Return whether we are in 'onchange' draft mode. """
        return self.all.mode == 'onchange'

    def clear(self):
        """ Clear all record caches, and discard all fields to recompute.
            This may be useful when recovering from a failed ORM operation.
        """
        self.cache.invalidate()
        self.all.todo.clear()

    @contextmanager
    def clear_upon_failure(self):
        """ Context manager that clears the environments (caches and fields to
            recompute) upon exception.
        """
        try:
            yield
        except Exception:
            self.clear()
            raise

    def protected(self, field):
        """ Return the recordset for which ``field`` should not be invalidated or recomputed. """
        return self[field.model_name].browse(self._protected.get(field, ()))

    @contextmanager
    def protecting(self, what, records=None):
        """ Prevent the invalidation or recomputation of fields on records.
            The parameters are either:
             - ``what`` a collection of fields and ``records`` a recordset, or
             - ``what`` a collection of pairs ``(fields, records)``.
        """
        protected = self._protected
        try:
            protected.pushmap()
            what = what if records is None else [(what, records)]
            for fields, records in what:
                for field in fields:
                    ids = protected.get(field, frozenset())
                    protected[field] = ids.union(records._ids)
            yield
        finally:
            protected.popmap()

    def field_todo(self, field):
        """ Return a recordset with all records to recompute for ``field``. """
        ids = {rid for recs in self.all.todo.get(field, ()) for rid in recs.ids}
        return self[field.model_name].browse(ids)

    def check_todo(self, field, record):
        """ Check whether ``field`` must be recomputed on ``record``, and if so,
            return the corresponding recordset to recompute.
        """
        for recs in self.all.todo.get(field, []):
            if recs & record:
                return recs

    def add_todo(self, field, records):
        """ Mark ``field`` to be recomputed on ``records``. """
        recs_list = self.all.todo.setdefault(field, [])
        for i, recs in enumerate(recs_list):
            if recs.env == records.env:
                # only add records if not already in the recordset, much much
                # cheaper in case recs is big and records is a singleton
                # already present
                if not records <= recs:
                    recs_list[i] |= records
                break
        else:
            recs_list.append(records)

    def remove_todo(self, field, records):
        """ Mark ``field`` as recomputed on ``records``. """
        recs_list = [recs - records for recs in self.all.todo.pop(field, [])]
        recs_list = [r for r in recs_list if r]
        if recs_list:
            self.all.todo[field] = recs_list

    def has_todo(self):
        """ Return whether some fields must be recomputed. """
        return bool(self.all.todo)

    def get_todo(self):
        """ Return a pair ``(field, records)`` to recompute.
            The field is such that none of its dependencies must be recomputed.
        """
        field = min(self.all.todo, key=self.registry.field_sequence)
        return field, self.all.todo[field][0]

    @property
    def recompute(self):
        return self.all.recompute

    @contextmanager
    def norecompute(self):
        tmp = self.all.recompute
        self.all.recompute = False
        try:
            yield
        finally:
            self.all.recompute = tmp

    def cache_key(self, field):
        """ Return the key to store the value of ``field`` in cache, the full
            cache key being ``(key, field, record.id)``.
        """
        return self if field.context_dependent else self._cache_key
Example #7
0
class Environment(Mapping):
    """ An environment wraps data for ORM records:

        - :attr:`cr`, the current database cursor;
        - :attr:`uid`, the current user id;
        - :attr:`context`, the current context dictionary;
        - :attr:`su`, whether in superuser mode.

        It provides access to the registry by implementing a mapping from model
        names to new api models. It also holds a cache for records, and a data
        structure to manage recomputations.
    """
    _local = Local()

    @classproperty
    def envs(cls):
        return getattr(cls._local, 'environments', ())

    @classmethod
    @contextmanager
    def manage(cls):
        """ Context manager for a set of environments. """
        if hasattr(cls._local, 'environments'):
            yield
        else:
            try:
                cls._local.environments = Environments()
                yield
            finally:
                release_local(cls._local)

    @classmethod
    def reset(cls):
        """ Clear the set of environments.
            This may be useful when recreating a registry inside a transaction.
        """
        cls._local.environments = Environments()

    def __new__(cls, cr, uid, context, su=False):
        if uid == SUPERUSER_ID:
            su = True
        assert context is not None
        args = (cr, uid, context, su)

        # if env already exists, return it
        env, envs = None, cls.envs
        for env in envs:
            if env.args == args:
                return env

        # otherwise create environment, and add it in the set
        self = object.__new__(cls)
        args = (cr, uid, frozendict(context), su)
        self.cr, self.uid, self.context, self.su = self.args = args
        self.registry = Registry(cr.dbname)
        self.cache = envs.cache
        self._protected = StackMap()                # {field: ids, ...}
        self.all = envs
        envs.add(self)
        return self

    #
    # Mapping methods
    #

    def __contains__(self, model_name):
        """ Test whether the given model exists. """
        return model_name in self.registry

    def __getitem__(self, model_name):
        """ Return an empty recordset from the given model. """
        return self.registry[model_name]._browse(self, (), ())

    def __iter__(self):
        """ Return an iterator on model names. """
        return iter(self.registry)

    def __len__(self):
        """ Return the size of the model registry. """
        return len(self.registry)

    def __eq__(self, other):
        return self is other

    def __ne__(self, other):
        return self is not other

    def __hash__(self):
        return object.__hash__(self)

    def __call__(self, cr=None, user=None, context=None, su=None):
        """ Return an environment based on ``self`` with modified parameters.

            :param cr: optional database cursor to change the current cursor
            :param user: optional user/user id to change the current user
            :param context: optional context dictionary to change the current context
            :param su: optional boolean to change the superuser mode
        """
        cr = self.cr if cr is None else cr
        uid = self.uid if user is None else int(user)
        context = self.context if context is None else context
        su = (user is None and self.su) if su is None else su
        return Environment(cr, uid, context, su)

    def ref(self, xml_id, raise_if_not_found=True):
        """ return the record corresponding to the given ``xml_id`` """
        return self['ir.model.data'].xmlid_to_object(xml_id, raise_if_not_found=raise_if_not_found)

    def is_superuser(self):
        """ Return whether the environment is in superuser mode. """
        return self.su

    def is_admin(self):
        """ Return whether the current user has group "Access Rights", or is in
            superuser mode. """
        return self.su or self.user._is_admin()

    def is_system(self):
        """ Return whether the current user has group "Settings", or is in
            superuser mode. """
        return self.su or self.user._is_system()

    @property
    def user(self):
        """ return the current user (as an instance) """
        return self(su=True)['res.users'].browse(self.uid)

    @property
    def company(self):
        """ return the company in which the user is logged in (as an instance) """
        try:
            company_id = int(self.context.get('allowed_company_ids')[0])
            if company_id in self.user.company_ids.ids:
                return self['res.company'].browse(company_id)
            return self.user.company_id
        except Exception:
            return self.user.company_id

    @property
    def companies(self):
        """ return a recordset of the enabled companies by the user """
        try:  # In case the user tries to bidouille the url (eg: cids=1,foo,bar)
            allowed_company_ids = self.context.get('allowed_company_ids')
            # Prevent the user to enable companies for which he doesn't have any access
            users_company_ids = self.user.company_ids.ids
            allowed_company_ids = [company_id for company_id in allowed_company_ids if company_id in users_company_ids]
        except Exception:
            # By setting the default companies to all user companies instead of the main one
            # we save a lot of potential trouble in all "out of context" calls, such as
            # /mail/redirect or /web/image, etc. And it is not unsafe because the user does
            # have access to these other companies. The risk of exposing foreign records
            # (wrt to the context) is low because all normal RPCs will have a proper
            # allowed_company_ids.
            # Examples:
            #   - when printing a report for several records from several companies
            #   - when accessing to a record from the notification email template
            #   - when loading an binary image on a template
            allowed_company_ids = self.user.company_ids.ids
        return self['res.company'].browse(allowed_company_ids)

    @property
    def lang(self):
        """ return the current language code """
        return self.context.get('lang')

    def clear(self):
        """ Clear all record caches, and discard all fields to recompute.
            This may be useful when recovering from a failed ORM operation.
        """
        self.cache.invalidate()
        self.all.tocompute.clear()
        self.all.towrite.clear()

    @contextmanager
    def clear_upon_failure(self):
        """ Context manager that clears the environments (caches and fields to
            recompute) upon exception.
        """
        tocompute = {
            field: set(ids)
            for field, ids in self.all.tocompute.items()
        }
        towrite = {
            model: {
                record_id: dict(values)
                for record_id, values in id_values.items()
            }
            for model, id_values in self.all.towrite.items()
        }
        try:
            yield
        except Exception:
            self.clear()
            self.all.tocompute.update(tocompute)
            for model, id_values in towrite.items():
                for record_id, values in id_values.items():
                    self.all.towrite[model][record_id].update(values)
            raise

    def is_protected(self, field, record):
        """ Return whether `record` is protected against invalidation or
            recomputation for `field`.
        """
        return record.id in self._protected.get(field, ())

    def protected(self, field):
        """ Return the recordset for which ``field`` should not be invalidated or recomputed. """
        return self[field.model_name].browse(self._protected.get(field, ()))

    @contextmanager
    def protecting(self, what, records=None):
        """ Prevent the invalidation or recomputation of fields on records.
            The parameters are either:
             - ``what`` a collection of fields and ``records`` a recordset, or
             - ``what`` a collection of pairs ``(fields, records)``.
        """
        protected = self._protected
        try:
            protected.pushmap()
            what = what if records is None else [(what, records)]
            for fields, records in what:
                for field in fields:
                    ids = protected.get(field, frozenset())
                    protected[field] = ids.union(records._ids)
            yield
        finally:
            protected.popmap()

    def fields_to_compute(self):
        """ Return a view on the field to compute. """
        return self.all.tocompute.keys()

    def records_to_compute(self, field):
        """ Return the records to compute for ``field``. """
        ids = self.all.tocompute.get(field, ())
        return self[field.model_name].browse(ids)

    def is_to_compute(self, field, record):
        """ Return whether ``field`` must be computed on ``record``. """
        return record.id in self.all.tocompute.get(field, ())

    def add_to_compute(self, field, records):
        """ Mark ``field`` to be computed on ``records``, return newly added records. """
        if not records:
            return records
        ids = self.all.tocompute[field]
        added_ids = [id_ for id_ in records._ids if id_ not in ids]
        ids.update(added_ids)
        return records.browse(added_ids)

    def remove_to_compute(self, field, records):
        """ Mark ``field`` as computed on ``records``. """
        if not records:
            return
        ids = self.all.tocompute.get(field, None)
        if ids is None:
            return
        ids.difference_update(records._ids)
        if not ids:
            del self.all.tocompute[field]

    @contextmanager
    def norecompute(self):
        """ Delay recomputations (deprecated: this is not the default behavior). """
        yield
Example #8
0
File: api.py Project: wpareja/odoo
class Environment(Mapping):
    """ An environment wraps data for ORM records:

        - :attr:`cr`, the current database cursor;
        - :attr:`uid`, the current user id;
        - :attr:`context`, the current context dictionary;
        - :attr:`su`, whether in superuser mode.

        It provides access to the registry by implementing a mapping from model
        names to new api models. It also holds a cache for records, and a data
        structure to manage recomputations.
    """
    _local = Local()

    @classproperty
    def envs(cls):
        return cls._local.environments

    @classmethod
    @contextmanager
    def manage(cls):
        """ Context manager for a set of environments. """
        if hasattr(cls._local, 'environments'):
            yield
        else:
            try:
                cls._local.environments = Environments()
                yield
            finally:
                release_local(cls._local)

    @classmethod
    def reset(cls):
        """ Clear the set of environments.
            This may be useful when recreating a registry inside a transaction.
        """
        cls._local.environments = Environments()

    def __new__(cls, cr, uid, context, su=False):
        if uid == SUPERUSER_ID:
            su = True
        assert context is not None
        args = (cr, uid, context, su)

        # if env already exists, return it
        env, envs = None, cls.envs
        for env in envs:
            if env.args == args:
                return env

        # otherwise create environment, and add it in the set
        self = object.__new__(cls)
        args = (cr, uid, frozendict(context), su)
        self.cr, self.uid, self.context, self.su = self.args = args
        self.registry = Registry(cr.dbname)
        self.cache = envs.cache
        self._cache_key = (cr, uid, su)
        self._protected = StackMap()  # {field: ids, ...}
        self.all = envs
        envs.add(self)
        return self

    #
    # Mapping methods
    #

    def __contains__(self, model_name):
        """ Test whether the given model exists. """
        return model_name in self.registry

    def __getitem__(self, model_name):
        """ Return an empty recordset from the given model. """
        return self.registry[model_name]._browse(self, (), ())

    def __iter__(self):
        """ Return an iterator on model names. """
        return iter(self.registry)

    def __len__(self):
        """ Return the size of the model registry. """
        return len(self.registry)

    def __eq__(self, other):
        return self is other

    def __ne__(self, other):
        return self is not other

    def __hash__(self):
        return object.__hash__(self)

    def __call__(self, cr=None, user=None, context=None, su=None):
        """ Return an environment based on ``self`` with modified parameters.

            :param cr: optional database cursor to change the current cursor
            :param user: optional user/user id to change the current user
            :param context: optional context dictionary to change the current context
            :param su: optional boolean to change the superuser mode
        """
        cr = self.cr if cr is None else cr
        uid = self.uid if user is None else int(user)
        context = self.context if context is None else context
        su = (user is None and self.su) if su is None else su
        return Environment(cr, uid, context, su)

    def ref(self, xml_id, raise_if_not_found=True):
        """ return the record corresponding to the given ``xml_id`` """
        return self['ir.model.data'].xmlid_to_object(
            xml_id, raise_if_not_found=raise_if_not_found)

    def is_superuser(self):
        """ Return whether the environment is in superuser mode. """
        return self.su

    def is_admin(self):
        """ Return whether the current user has group "Access Rights", or is in
            superuser mode. """
        return self.su or self.user._is_admin()

    def is_system(self):
        """ Return whether the current user has group "Settings", or is in
            superuser mode. """
        return self.su or self.user._is_system()

    @property
    def user(self):
        """ return the current user (as an instance) """
        return self(su=True)['res.users'].browse(self.uid)

    @property
    def company(self):
        """ return the company in which the user is logged in (as an instance) """
        try:
            company_id = int(self.context.get('allowed_company_ids')[0])
            if company_id in self.user.company_ids.ids:
                return self['res.company'].browse(company_id)
            return self.user.company_id
        except Exception:
            return self.user.company_id

    @property
    def companies(self):
        """ return a recordset of the enabled companies by the user """
        try:  # In case the user tries to bidouille the url (eg: cids=1,foo,bar)
            allowed_company_ids = self.context.get('allowed_company_ids')
            # Prevent the user to enable companies for which he doesn't have any access
            users_company_ids = self.user.company_ids.ids
            allowed_company_ids = [
                company_id for company_id in allowed_company_ids
                if company_id in users_company_ids
            ]
        except Exception:
            # By setting the default companies to all user companies instead of the main one
            # we save a lot of potential trouble in all "out of context" calls, such as
            # /mail/redirect or /web/image, etc. And it is not unsafe because the user does
            # have access to these other companies. The risk of exposing foreign records
            # (wrt to the context) is low because all normal RPCs will have a proper
            # allowed_company_ids.
            # Examples:
            #   - when printing a report for several records from several companies
            #   - when accessing to a record from the notification email template
            #   - when loading an binary image on a template
            allowed_company_ids = self.user.company_ids.ids
        return self['res.company'].browse(allowed_company_ids)

    @property
    def lang(self):
        """ return the current language code """
        return self.context.get('lang')

    @contextmanager
    def do_in_draft(self):
        """ Context-switch to draft mode, where all field updates are done in
            cache only.
        """
        if self.all.in_draft:
            yield
        else:
            try:
                self.all.in_draft = True
                yield
            finally:
                self.all.in_draft = False

    @property
    def in_draft(self):
        """ Return whether we are in draft mode. """
        return self.all.in_draft

    def clear(self):
        """ Clear all record caches, and discard all fields to recompute.
            This may be useful when recovering from a failed ORM operation.
        """
        self.cache.invalidate()
        self.all.todo.clear()

    @contextmanager
    def clear_upon_failure(self):
        """ Context manager that clears the environments (caches and fields to
            recompute) upon exception.
        """
        try:
            yield
        except Exception:
            self.clear()
            raise

    def protected(self, field):
        """ Return the recordset for which ``field`` should not be invalidated or recomputed. """
        return self[field.model_name].browse(self._protected.get(field, ()))

    @contextmanager
    def protecting(self, what, records=None):
        """ Prevent the invalidation or recomputation of fields on records.
            The parameters are either:
             - ``what`` a collection of fields and ``records`` a recordset, or
             - ``what`` a collection of pairs ``(fields, records)``.
        """
        protected = self._protected
        try:
            protected.pushmap()
            what = what if records is None else [(what, records)]
            for fields, records in what:
                for field in fields:
                    ids = protected.get(field, frozenset())
                    protected[field] = ids.union(records._ids)
            yield
        finally:
            protected.popmap()

    def field_todo(self, field):
        """ Return a recordset with all records to recompute for ``field``. """
        ids = {
            rid
            for recs in self.all.todo.get(field, ()) for rid in recs.ids
        }
        return self[field.model_name].browse(ids)

    def check_todo(self, field, record):
        """ Check whether ``field`` must be recomputed on ``record``, and if so,
            return the corresponding recordset to recompute.
        """
        for recs in self.all.todo.get(field, []):
            if recs & record:
                return recs

    def add_todo(self, field, records):
        """ Mark ``field`` to be recomputed on ``records``. """
        recs_list = self.all.todo.setdefault(field, [])
        for i, recs in enumerate(recs_list):
            if recs.env == records.env:
                # only add records if not already in the recordset, much much
                # cheaper in case recs is big and records is a singleton
                # already present
                if not records <= recs:
                    recs_list[i] |= records
                break
        else:
            recs_list.append(records)

    def remove_todo(self, field, records):
        """ Mark ``field`` as recomputed on ``records``. """
        recs_list = [recs - records for recs in self.all.todo.pop(field, [])]
        recs_list = [r for r in recs_list if r]
        if recs_list:
            self.all.todo[field] = recs_list

    def has_todo(self):
        """ Return whether some fields must be recomputed. """
        return bool(self.all.todo)

    def get_todo(self):
        """ Return a pair ``(field, records)`` to recompute.
            The field is such that none of its dependencies must be recomputed.
        """
        field = min(self.all.todo, key=self.registry.field_sequence)
        return field, self.all.todo[field][0]

    @property
    def recompute(self):
        return self.all.recompute

    @contextmanager
    def norecompute(self):
        tmp = self.all.recompute
        self.all.recompute = False
        try:
            yield
        finally:
            self.all.recompute = tmp

    def cache_key(self, field):
        """ Return the key to store the value of ``field`` in cache, the full
            cache key being ``(key, field, record.id)``.
        """
        return self if field.context_dependent else self._cache_key