cubicweb/entity.py
changeset 11057 0b59724cb3f2
parent 11047 bfd11ffa79f7
child 11129 97095348b3ee
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/entity.py	Sat Jan 16 13:48:51 2016 +0100
@@ -0,0 +1,1403 @@
+# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""Base class for entity objects manipulated in clients"""
+
+__docformat__ = "restructuredtext en"
+
+from warnings import warn
+from functools import partial
+
+from six import text_type, string_types, integer_types
+from six.moves import range
+
+from logilab.common.decorators import cached
+from logilab.common.deprecation import deprecated
+from logilab.common.registry import yes
+from logilab.mtconverter import TransformData, xml_escape
+
+from rql.utils import rqlvar_maker
+from rql.stmts import Select
+from rql.nodes import (Not, VariableRef, Constant, make_relation,
+                       Relation as RqlRelation)
+
+from cubicweb import Unauthorized, neg_role
+from cubicweb.utils import support_args
+from cubicweb.rset import ResultSet
+from cubicweb.appobject import AppObject
+from cubicweb.schema import (RQLVocabularyConstraint, RQLConstraint,
+                             GeneratedConstraint)
+from cubicweb.rqlrewrite import RQLRewriter
+
+from cubicweb.uilib import soup2xhtml
+from cubicweb.mttransforms import ENGINE
+
+_marker = object()
+
+def greater_card(rschema, subjtypes, objtypes, index):
+    for subjtype in subjtypes:
+        for objtype in objtypes:
+            card = rschema.rdef(subjtype, objtype).cardinality[index]
+            if card in '+*':
+                return card
+    return '1'
+
+def can_use_rest_path(value):
+    """return True if value can be used at the end of a Rest URL path"""
+    if value is None:
+        return False
+    value = text_type(value)
+    # the check for ?, /, & are to prevent problems when running
+    # behind Apache mod_proxy
+    if value == u'' or u'?' in value or u'/' in value or u'&' in value:
+        return False
+    return True
+
+def rel_vars(rel):
+    return ((isinstance(rel.children[0], VariableRef)
+             and rel.children[0].variable or None),
+            (isinstance(rel.children[1].children[0], VariableRef)
+             and rel.children[1].children[0].variable or None)
+            )
+
+def rel_matches(rel, rtype, role, varname, operator='='):
+    if rel.r_type == rtype and rel.children[1].operator == operator:
+        same_role_var_idx = 0 if role == 'subject' else 1
+        variables = rel_vars(rel)
+        if variables[same_role_var_idx].name == varname:
+            return variables[1 - same_role_var_idx]
+
+def build_cstr_with_linkto_infos(cstr, args, searchedvar, evar,
+                                 lt_infos, eidvars):
+    """restrict vocabulary as much as possible in entity creation,
+    based on infos provided by __linkto form param.
+
+    Example based on following schema:
+
+      class works_in(RelationDefinition):
+          subject = 'CWUser'
+          object = 'Lab'
+          cardinality = '1*'
+          constraints = [RQLConstraint('S in_group G, O welcomes G')]
+
+      class welcomes(RelationDefinition):
+          subject = 'Lab'
+          object = 'CWGroup'
+
+    If you create a CWUser in the "scientists" CWGroup you can show
+    only the labs that welcome them using :
+
+      lt_infos = {('in_group', 'subject'): 321}
+
+    You get following restriction : 'O welcomes G, G eid 321'
+
+    """
+    st = cstr.snippet_rqlst.copy()
+    # replace relations in ST by eid infos from linkto where possible
+    for (info_rtype, info_role), eids in lt_infos.items():
+        eid = eids[0] # NOTE: we currently assume a pruned lt_info with only 1 eid
+        for rel in st.iget_nodes(RqlRelation):
+            targetvar = rel_matches(rel, info_rtype, info_role, evar.name)
+            if targetvar is not None:
+                if targetvar.name in eidvars:
+                    rel.parent.remove(rel)
+                else:
+                    eidrel = make_relation(
+                        targetvar, 'eid', (targetvar.name, 'Substitute'),
+                        Constant)
+                    rel.parent.replace(rel, eidrel)
+                    args[targetvar.name] = eid
+                    eidvars.add(targetvar.name)
+    # if modified ST still contains evar references we must discard the
+    # constraint, otherwise evar is unknown in the final rql query which can
+    # lead to a SQL table cartesian product and multiple occurences of solutions
+    evarname = evar.name
+    for rel in st.iget_nodes(RqlRelation):
+        for variable in rel_vars(rel):
+            if variable and evarname == variable.name:
+                return
+    # else insert snippets into the global tree
+    return GeneratedConstraint(st, cstr.mainvars - set(evarname))
+
+def pruned_lt_info(eschema, lt_infos):
+    pruned = {}
+    for (lt_rtype, lt_role), eids in lt_infos.items():
+        # we can only use lt_infos describing relation with a cardinality
+        # of value 1 towards the linked entity
+        if not len(eids) == 1:
+            continue
+        lt_card = eschema.rdef(lt_rtype, lt_role).cardinality[
+            0 if lt_role == 'subject' else 1]
+        if lt_card not in '?1':
+            continue
+        pruned[(lt_rtype, lt_role)] = eids
+    return pruned
+
+
+class Entity(AppObject):
+    """an entity instance has e_schema automagically set on
+    the class and instances has access to their issuing cursor.
+
+    A property is set for each attribute and relation on each entity's type
+    class. Becare that among attributes, 'eid' is *NEITHER* stored in the
+    dict containment (which acts as a cache for other attributes dynamically
+    fetched)
+
+    :type e_schema: `cubicweb.schema.EntitySchema`
+    :ivar e_schema: the entity's schema
+
+    :type rest_attr: str
+    :cvar rest_attr: indicates which attribute should be used to build REST urls
+       If `None` is specified (the default), the first unique attribute will
+       be used ('eid' if none found)
+
+    :type cw_skip_copy_for: list
+    :cvar cw_skip_copy_for: a list of couples (rtype, role) for each relation
+       that should be skipped when copying this kind of entity. Note that some
+       relations such as composite relations or relations that have '?1' as
+       object cardinality are always skipped.
+    """
+    __registry__ = 'etypes'
+    __select__ = yes()
+
+    # class attributes that must be set in class definition
+    rest_attr = None
+    fetch_attrs = None
+    skip_copy_for = () # bw compat (< 3.14), use cw_skip_copy_for instead
+    cw_skip_copy_for = [('in_state', 'subject')]
+    # class attributes set automatically at registration time
+    e_schema = None
+
+    @classmethod
+    def __initialize__(cls, schema):
+        """initialize a specific entity class by adding descriptors to access
+        entity type's attributes and relations
+        """
+        etype = cls.__regid__
+        assert etype != 'Any', etype
+        cls.e_schema = eschema = schema.eschema(etype)
+        for rschema, _ in eschema.attribute_definitions():
+            if rschema.type == 'eid':
+                continue
+            setattr(cls, rschema.type, Attribute(rschema.type))
+        mixins = []
+        for rschema, _, role in eschema.relation_definitions():
+            if role == 'subject':
+                attr = rschema.type
+            else:
+                attr = 'reverse_%s' % rschema.type
+            setattr(cls, attr, Relation(rschema, role))
+
+    fetch_attrs = ('modification_date',)
+
+    @classmethod
+    def cw_fetch_order(cls, select, attr, var):
+        """This class method may be used to control sort order when multiple
+        entities of this type are fetched through ORM methods. Its arguments
+        are:
+
+        * `select`, the RQL syntax tree
+
+        * `attr`, the attribute being watched
+
+        * `var`, the variable through which this attribute's value may be
+          accessed in the query
+
+        When you want to do some sorting on the given attribute, you should
+        modify the syntax tree accordingly. For instance:
+
+        .. sourcecode:: python
+
+          from rql import nodes
+
+          class Version(AnyEntity):
+              __regid__ = 'Version'
+
+              fetch_attrs = ('num', 'description', 'in_state')
+
+              @classmethod
+              def cw_fetch_order(cls, select, attr, var):
+                  if attr == 'num':
+                      func = nodes.Function('version_sort_value')
+                      func.append(nodes.variable_ref(var))
+                      sterm = nodes.SortTerm(func, asc=False)
+                      select.add_sort_term(sterm)
+
+        The default implementation call
+        :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order`
+        """
+        cls.cw_fetch_unrelated_order(select, attr, var)
+
+    @classmethod
+    def cw_fetch_unrelated_order(cls, select, attr, var):
+        """This class method may be used to control sort order when multiple entities of
+        this type are fetched to use in edition (e.g. propose them to create a
+        new relation on an edited entity).
+
+        See :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order` for a
+        description of its arguments and usage.
+
+        By default entities will be listed on their modification date descending,
+        i.e. you'll get entities recently modified first.
+        """
+        if attr == 'modification_date':
+            select.add_sort_var(var, asc=False)
+
+    @classmethod
+    def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
+                  settype=True, ordermethod='fetch_order'):
+        st = cls.fetch_rqlst(user, mainvar=mainvar, fetchattrs=fetchattrs,
+                             settype=settype, ordermethod=ordermethod)
+        rql = st.as_string()
+        if restriction:
+            # cannot use RQLRewriter API to insert 'X rtype %(x)s' restriction
+            warn('[3.14] fetch_rql: use of `restriction` parameter is '
+                 'deprecated, please use fetch_rqlst and supply a syntax'
+                 'tree with your restriction instead', DeprecationWarning)
+            insert = ' WHERE ' + ','.join(restriction)
+            if ' WHERE ' in rql:
+                select, where = rql.split(' WHERE ', 1)
+                rql = select + insert + ',' + where
+            else:
+                rql += insert
+        return rql
+
+    @classmethod
+    def fetch_rqlst(cls, user, select=None, mainvar='X', fetchattrs=None,
+                    settype=True, ordermethod='fetch_order'):
+        if select is None:
+            select = Select()
+            mainvar = select.get_variable(mainvar)
+            select.add_selected(mainvar)
+        elif isinstance(mainvar, string_types):
+            assert mainvar in select.defined_vars
+            mainvar = select.get_variable(mainvar)
+        # eases string -> syntax tree test transition: please remove once stable
+        select._varmaker = rqlvar_maker(defined=select.defined_vars,
+                                        aliases=select.aliases, index=26)
+        if settype:
+            rel = select.add_type_restriction(mainvar, cls.__regid__)
+            # should use 'is_instance_of' instead of 'is' so we retrieve
+            # subclasses instances as well
+            rel.r_type = 'is_instance_of'
+        if fetchattrs is None:
+            fetchattrs = cls.fetch_attrs
+        cls._fetch_restrictions(mainvar, select, fetchattrs, user, ordermethod)
+        return select
+
+    @classmethod
+    def _fetch_ambiguous_rtypes(cls, select, var, fetchattrs, subjtypes, schema):
+        """find rtypes in `fetchattrs` that relate different subject etypes
+        taken from (`subjtypes`) to different target etypes; these so called
+        "ambiguous" relations, are added directly to the `select` syntax tree
+        selection but removed from `fetchattrs` to avoid the fetch recursion
+        because we have to choose only one targettype for the recursion and
+        adding its own fetch attrs to the selection -when we recurse- would
+        filter out the other possible target types from the result set
+        """
+        for attr in fetchattrs.copy():
+            rschema = schema.rschema(attr)
+            if rschema.final:
+                continue
+            ttypes = None
+            for subjtype in subjtypes:
+                cur_ttypes = set(rschema.objects(subjtype))
+                if ttypes is None:
+                    ttypes = cur_ttypes
+                elif cur_ttypes != ttypes:
+                    # we found an ambiguous relation: remove it from fetchattrs
+                    fetchattrs.remove(attr)
+                    # ... and add it to the selection
+                    targetvar = select.make_variable()
+                    select.add_selected(targetvar)
+                    rel = make_relation(var, attr, (targetvar,), VariableRef)
+                    select.add_restriction(rel)
+                    break
+
+    @classmethod
+    def _fetch_restrictions(cls, mainvar, select, fetchattrs,
+                            user, ordermethod='fetch_order', visited=None):
+        eschema = cls.e_schema
+        if visited is None:
+            visited = set((eschema.type,))
+        elif eschema.type in visited:
+            # avoid infinite recursion
+            return
+        else:
+            visited.add(eschema.type)
+        _fetchattrs = []
+        for attr in sorted(fetchattrs):
+            try:
+                rschema = eschema.subjrels[attr]
+            except KeyError:
+                cls.warning('skipping fetch_attr %s defined in %s (not found in schema)',
+                            attr, cls.__regid__)
+                continue
+            # XXX takefirst=True to remove warning triggered by ambiguous inlined relations
+            rdef = eschema.rdef(attr, takefirst=True)
+            if not user.matching_groups(rdef.get_groups('read')):
+                continue
+            if rschema.final or rdef.cardinality[0] in '?1':
+                var = select.make_variable()
+                select.add_selected(var)
+                rel = make_relation(mainvar, attr, (var,), VariableRef)
+                select.add_restriction(rel)
+            else:
+                cls.warning('bad relation %s specified in fetch attrs for %s',
+                            attr, cls)
+                continue
+            if not rschema.final:
+                # XXX we need outer join in case the relation is not mandatory
+                # (card == '?')  *or if the entity is being added*, since in
+                # that case the relation may still be missing. As we miss this
+                # later information here, systematically add it.
+                rel.change_optional('right')
+                targettypes = rschema.objects(eschema.type)
+                vreg = user._cw.vreg # XXX user._cw.vreg iiiirk
+                etypecls = vreg['etypes'].etype_class(targettypes[0])
+                if len(targettypes) > 1:
+                    # find fetch_attrs common to all destination types
+                    fetchattrs = vreg['etypes'].fetch_attrs(targettypes)
+                    # ... and handle ambiguous relations
+                    cls._fetch_ambiguous_rtypes(select, var, fetchattrs,
+                                                targettypes, vreg.schema)
+                else:
+                    fetchattrs = etypecls.fetch_attrs
+                etypecls._fetch_restrictions(var, select, fetchattrs,
+                                             user, None, visited=visited)
+            if ordermethod is not None:
+                try:
+                    cmeth = getattr(cls, ordermethod)
+                    warn('[3.14] %s %s class method should be renamed to cw_%s'
+                         % (cls.__regid__, ordermethod, ordermethod),
+                         DeprecationWarning)
+                except AttributeError:
+                    cmeth = getattr(cls, 'cw_' + ordermethod)
+                if support_args(cmeth, 'select'):
+                    cmeth(select, attr, var)
+                else:
+                    warn('[3.14] %s should now take (select, attr, var) and '
+                         'modify the syntax tree when desired instead of '
+                         'returning something' % cmeth, DeprecationWarning)
+                    orderterm = cmeth(attr, var.name)
+                    if orderterm is not None:
+                        try:
+                            var, order = orderterm.split()
+                        except ValueError:
+                            if '(' in orderterm:
+                                cls.error('ignore %s until %s is upgraded',
+                                          orderterm, cmeth)
+                                orderterm = None
+                            elif not ' ' in orderterm.strip():
+                                var = orderterm
+                                order = 'ASC'
+                        if orderterm is not None:
+                            select.add_sort_var(select.get_variable(var),
+                                                order=='ASC')
+
+    @classmethod
+    @cached
+    def cw_rest_attr_info(cls):
+        """this class method return an attribute name to be used in URL for
+        entities of this type and a boolean flag telling if its value should be
+        checked for uniqness.
+
+        The attribute returned is, in order of priority:
+
+        * class's `rest_attr` class attribute
+        * an attribute defined as unique in the class'schema
+        * 'eid'
+        """
+        mainattr, needcheck = 'eid', True
+        if cls.rest_attr:
+            mainattr = cls.rest_attr
+            needcheck = not cls.e_schema.has_unique_values(mainattr)
+        else:
+            for rschema in cls.e_schema.subject_relations():
+                if (rschema.final
+                    and rschema not in ('eid', 'cwuri')
+                    and cls.e_schema.has_unique_values(rschema)
+                    and cls.e_schema.rdef(rschema.type).cardinality[0] == '1'):
+                    mainattr = str(rschema)
+                    needcheck = False
+                    break
+        if mainattr == 'eid':
+            needcheck = False
+        return mainattr, needcheck
+
+    @classmethod
+    def _cw_build_entity_query(cls, kwargs):
+        relations = []
+        restrictions = set()
+        pendingrels = []
+        eschema = cls.e_schema
+        qargs = {}
+        attrcache = {}
+        for attr, value in kwargs.items():
+            if attr.startswith('reverse_'):
+                attr = attr[len('reverse_'):]
+                role = 'object'
+            else:
+                role = 'subject'
+            assert eschema.has_relation(attr, role), '%s %s not found on %s' % (attr, role, eschema)
+            rschema = eschema.subjrels[attr] if role == 'subject' else eschema.objrels[attr]
+            if not rschema.final and isinstance(value, (tuple, list, set, frozenset)):
+                if len(value) == 0:
+                    continue # avoid crash with empty IN clause
+                elif len(value) == 1:
+                    value = next(iter(value))
+                else:
+                    # prepare IN clause
+                    pendingrels.append( (attr, role, value) )
+                    continue
+            if rschema.final: # attribute
+                relations.append('X %s %%(%s)s' % (attr, attr))
+                attrcache[attr] = value
+            elif value is None:
+                pendingrels.append( (attr, role, value) )
+            else:
+                rvar = attr.upper()
+                if role == 'object':
+                    relations.append('%s %s X' % (rvar, attr))
+                else:
+                    relations.append('X %s %s' % (attr, rvar))
+                restriction = '%s eid %%(%s)s' % (rvar, attr)
+                if not restriction in restrictions:
+                    restrictions.add(restriction)
+                if hasattr(value, 'eid'):
+                    value = value.eid
+            qargs[attr] = value
+        rql = u''
+        if relations:
+            rql += ', '.join(relations)
+        if restrictions:
+            rql += ' WHERE %s' % ', '.join(restrictions)
+        return rql, qargs, pendingrels, attrcache
+
+    @classmethod
+    def _cw_handle_pending_relations(cls, eid, pendingrels, execute):
+        for attr, role, values in pendingrels:
+            if role == 'object':
+                restr = 'Y %s X' % attr
+            else:
+                restr = 'X %s Y' % attr
+            if values is None:
+                execute('DELETE %s WHERE X eid %%(x)s' % restr, {'x': eid})
+                continue
+            execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
+                restr, ','.join(str(getattr(r, 'eid', r)) for r in values)),
+                    {'x': eid}, build_descr=False)
+
+    @classmethod
+    def cw_instantiate(cls, execute, **kwargs):
+        """add a new entity of this given type
+
+        Example (in a shell session):
+
+        >>> companycls = vreg['etypes'].etype_class('Company')
+        >>> personcls = vreg['etypes'].etype_class('Person')
+        >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab')
+        >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe',
+        ...                              works_for=c)
+
+        You can also set relations where the entity has 'object' role by
+        prefixing the relation name by 'reverse_'. Also, relation values may be
+        an entity or eid, a list of entities or eids.
+        """
+        rql, qargs, pendingrels, attrcache = cls._cw_build_entity_query(kwargs)
+        if rql:
+            rql = 'INSERT %s X: %s' % (cls.__regid__, rql)
+        else:
+            rql = 'INSERT %s X' % (cls.__regid__)
+        try:
+            created = execute(rql, qargs).get_entity(0, 0)
+        except IndexError:
+            raise Exception('could not create a %r with %r (%r)' %
+                            (cls.__regid__, rql, qargs))
+        created._cw_update_attr_cache(attrcache)
+        cls._cw_handle_pending_relations(created.eid, pendingrels, execute)
+        return created
+
+    def __init__(self, req, rset=None, row=None, col=0):
+        AppObject.__init__(self, req, rset=rset, row=row, col=col)
+        self._cw_related_cache = {}
+        self._cw_adapters_cache = {}
+        if rset is not None:
+            self.eid = rset[row][col]
+        else:
+            self.eid = None
+        self._cw_is_saved = True
+        self.cw_attr_cache = {}
+
+    def __repr__(self):
+        return '<Entity %s %s %s at %s>' % (
+            self.e_schema, self.eid, list(self.cw_attr_cache), id(self))
+
+    def __lt__(self, other):
+        raise NotImplementedError('comparison not implemented for %s' % self.__class__)
+
+    def __eq__(self, other):
+        if isinstance(self.eid, integer_types):
+            return self.eid == other.eid
+        return self is other
+
+    def __hash__(self):
+        if isinstance(self.eid, integer_types):
+            return self.eid
+        return super(Entity, self).__hash__()
+
+    def _cw_update_attr_cache(self, attrcache):
+        trdata = self._cw.transaction_data
+        uncached_attrs = trdata.get('%s.storage-special-process-attrs' % self.eid, set())
+        uncached_attrs.update(trdata.get('%s.dont-cache-attrs' % self.eid, set()))
+        for attr in uncached_attrs:
+            attrcache.pop(attr, None)
+            self.cw_attr_cache.pop(attr, None)
+        self.cw_attr_cache.update(attrcache)
+
+    def _cw_dont_cache_attribute(self, attr, repo_side=False):
+        """Called when some attribute has been transformed by a *storage*,
+        hence the original value should not be cached **by anyone**.
+
+        For example we have a special "fs_importing" mode in BFSS
+        where a file path is given as attribute value and stored as is
+        in the data base. Later access to the attribute will provide
+        the content of the file at the specified path. We do not want
+        the "filepath" value to be cached.
+
+        """
+        trdata = self._cw.transaction_data
+        trdata.setdefault('%s.dont-cache-attrs' % self.eid, set()).add(attr)
+        if repo_side:
+            trdata.setdefault('%s.storage-special-process-attrs' % self.eid, set()).add(attr)
+
+    def __json_encode__(self):
+        """custom json dumps hook to dump the entity's eid
+        which is not part of dict structure itself
+        """
+        dumpable = self.cw_attr_cache.copy()
+        dumpable['eid'] = self.eid
+        return dumpable
+
+    def cw_adapt_to(self, interface):
+        """return an adapter the entity to the given interface name.
+
+        return None if it can not be adapted.
+        """
+        cache = self._cw_adapters_cache
+        try:
+            return cache[interface]
+        except KeyError:
+            adapter = self._cw.vreg['adapters'].select_or_none(
+                interface, self._cw, entity=self)
+            cache[interface] = adapter
+            return adapter
+
+    def has_eid(self): # XXX cw_has_eid
+        """return True if the entity has an attributed eid (False
+        meaning that the entity has to be created
+        """
+        try:
+            int(self.eid)
+            return True
+        except (ValueError, TypeError):
+            return False
+
+    def cw_is_saved(self):
+        """during entity creation, there is some time during which the entity
+        has an eid attributed though it's not saved (eg during
+        'before_add_entity' hooks). You can use this method to ensure the entity
+        has an eid *and* is saved in its source.
+        """
+        return self.has_eid() and self._cw_is_saved
+
+    @cached
+    def cw_metainformation(self):
+        metas = self._cw.entity_metas(self.eid)
+        metas['source'] = self._cw.source_defs()[metas['source']]
+        return metas
+
+    def cw_check_perm(self, action):
+        self.e_schema.check_perm(self._cw, action, eid=self.eid)
+
+    def cw_has_perm(self, action):
+        return self.e_schema.has_perm(self._cw, action, eid=self.eid)
+
+    def view(self, __vid, __registry='views', w=None, initargs=None, **kwargs): # XXX cw_view
+        """shortcut to apply a view on this entity"""
+        if initargs is None:
+            initargs = kwargs
+        else:
+            initargs.update(kwargs)
+        view = self._cw.vreg[__registry].select(__vid, self._cw, rset=self.cw_rset,
+                                                row=self.cw_row, col=self.cw_col,
+                                                **initargs)
+        return view.render(row=self.cw_row, col=self.cw_col, w=w, **kwargs)
+
+    def absolute_url(self, *args, **kwargs): # XXX cw_url
+        """return an absolute url to view this entity"""
+        # use *args since we don't want first argument to be "anonymous" to
+        # avoid potential clash with kwargs
+        if args:
+            assert len(args) == 1, 'only 0 or 1 non-named-argument expected'
+            method = args[0]
+        else:
+            method = None
+        # in linksearch mode, we don't want external urls else selecting
+        # the object for use in the relation is tricky
+        # XXX search_state is web specific
+        use_ext_id = False
+        if 'base_url' not in kwargs and \
+               getattr(self._cw, 'search_state', ('normal',))[0] == 'normal':
+            sourcemeta = self.cw_metainformation()['source']
+            if sourcemeta.get('use-cwuri-as-url'):
+                return self.cwuri # XXX consider kwargs?
+            if sourcemeta.get('base-url'):
+                kwargs['base_url'] = sourcemeta['base-url']
+                use_ext_id = True
+        if method in (None, 'view'):
+            kwargs['_restpath'] = self.rest_path(use_ext_id)
+        else:
+            kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
+        return self._cw.build_url(method, **kwargs)
+
+    def rest_path(self, use_ext_eid=False): # XXX cw_rest_path
+        """returns a REST-like (relative) path for this entity"""
+        mainattr, needcheck = self.cw_rest_attr_info()
+        etype = str(self.e_schema)
+        path = etype.lower()
+        fallback = False
+        if mainattr != 'eid':
+            value = getattr(self, mainattr)
+            if not can_use_rest_path(value):
+                mainattr = 'eid'
+                path = None
+            elif needcheck:
+                # make sure url is not ambiguous
+                try:
+                    nbresults = self.__unique
+                except AttributeError:
+                    rql = 'Any COUNT(X) WHERE X is %s, X %s %%(value)s' % (
+                        etype, mainattr)
+                    nbresults = self.__unique = self._cw.execute(rql, {'value' : value})[0][0]
+                if nbresults != 1: # ambiguity?
+                    mainattr = 'eid'
+                    path = None
+        if mainattr == 'eid':
+            if use_ext_eid:
+                value = self.cw_metainformation()['extid']
+            else:
+                value = self.eid
+        if path is None:
+            # fallback url: <base-url>/<eid> url is used as cw entities uri,
+            # prefer it to <base-url>/<etype>/eid/<eid>
+            return text_type(value)
+        return u'%s/%s' % (path, self._cw.url_quote(value))
+
+    def cw_attr_metadata(self, attr, metadata):
+        """return a metadata for an attribute (None if unspecified)"""
+        value = getattr(self, '%s_%s' % (attr, metadata), None)
+        if value is None and metadata == 'encoding':
+            value = self._cw.vreg.property_value('ui.encoding')
+        return value
+
+    def printable_value(self, attr, value=_marker, attrtype=None,
+                        format='text/html', displaytime=True): # XXX cw_printable_value
+        """return a displayable value (i.e. unicode string) which may contains
+        html tags
+        """
+        attr = str(attr)
+        if value is _marker:
+            value = getattr(self, attr)
+        if isinstance(value, string_types):
+            value = value.strip()
+        if value is None or value == '': # don't use "not", 0 is an acceptable value
+            return u''
+        if attrtype is None:
+            attrtype = self.e_schema.destination(attr)
+        props = self.e_schema.rdef(attr)
+        if attrtype == 'String':
+            # internalinalized *and* formatted string such as schema
+            # description...
+            if props.internationalizable:
+                value = self._cw._(value)
+            attrformat = self.cw_attr_metadata(attr, 'format')
+            if attrformat:
+                return self._cw_mtc_transform(value, attrformat, format,
+                                              self._cw.encoding)
+        elif attrtype == 'Bytes':
+            attrformat = self.cw_attr_metadata(attr, 'format')
+            if attrformat:
+                encoding = self.cw_attr_metadata(attr, 'encoding')
+                return self._cw_mtc_transform(value.getvalue(), attrformat, format,
+                                              encoding)
+            return u''
+        value = self._cw.printable_value(attrtype, value, props,
+                                         displaytime=displaytime)
+        if format == 'text/html':
+            value = xml_escape(value)
+        return value
+
+    def _cw_mtc_transform(self, data, format, target_format, encoding,
+                          _engine=ENGINE):
+        trdata = TransformData(data, format, encoding, appobject=self)
+        data = _engine.convert(trdata, target_format).decode()
+        if target_format == 'text/html':
+            data = soup2xhtml(data, self._cw.encoding)
+        return data
+
+    # entity cloning ##########################################################
+
+    def copy_relations(self, ceid): # XXX cw_copy_relations
+        """copy relations of the object with the given eid on this
+        object (this method is called on the newly created copy, and
+        ceid designates the original entity).
+
+        By default meta and composite relations are skipped.
+        Overrides this if you want another behaviour
+        """
+        assert self.has_eid()
+        execute = self._cw.execute
+        skip_copy_for = {'subject': set(), 'object': set()}
+        for rtype in self.skip_copy_for:
+            skip_copy_for['subject'].add(rtype)
+            warn('[3.14] skip_copy_for on entity classes (%s) is deprecated, '
+                 'use cw_skip_for instead with list of couples (rtype, role)' % self.cw_etype,
+                 DeprecationWarning)
+        for rtype, role in self.cw_skip_copy_for:
+            assert role in ('subject', 'object'), role
+            skip_copy_for[role].add(rtype)
+        for rschema in self.e_schema.subject_relations():
+            if rschema.type in skip_copy_for['subject']:
+                continue
+            if rschema.final or rschema.meta:
+                continue
+            # skip already defined relations
+            if getattr(self, rschema.type):
+                continue
+            # XXX takefirst=True to remove warning triggered by ambiguous relations
+            rdef = self.e_schema.rdef(rschema, takefirst=True)
+            # skip composite relation
+            if rdef.composite:
+                continue
+            # skip relation with card in ?1 else we either change the copied
+            # object (inlined relation) or inserting some inconsistency
+            if rdef.cardinality[1] in '?1':
+                continue
+            rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
+                rschema.type, rschema.type)
+            execute(rql, {'x': self.eid, 'y': ceid})
+            self.cw_clear_relation_cache(rschema.type, 'subject')
+        for rschema in self.e_schema.object_relations():
+            if rschema.meta:
+                continue
+            # skip already defined relations
+            if self.related(rschema.type, 'object'):
+                continue
+            if rschema.type in skip_copy_for['object']:
+                continue
+            # XXX takefirst=True to remove warning triggered by ambiguous relations
+            rdef = self.e_schema.rdef(rschema, 'object', takefirst=True)
+            # skip composite relation
+            if rdef.composite:
+                continue
+            # skip relation with card in ?1 else we either change the copied
+            # object (inlined relation) or inserting some inconsistency
+            if rdef.cardinality[0] in '?1':
+                continue
+            rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
+                rschema.type, rschema.type)
+            execute(rql, {'x': self.eid, 'y': ceid})
+            self.cw_clear_relation_cache(rschema.type, 'object')
+
+    # data fetching methods ###################################################
+
+    @cached
+    def as_rset(self): # XXX .cw_as_rset
+        """returns a resultset containing `self` information"""
+        rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
+                         {'x': self.eid}, [(self.cw_etype,)])
+        rset.req = self._cw
+        return rset
+
+    def _cw_to_complete_relations(self):
+        """by default complete final relations to when calling .complete()"""
+        for rschema in self.e_schema.subject_relations():
+            if rschema.final:
+                continue
+            targets = rschema.objects(self.e_schema)
+            if rschema.inlined:
+                matching_groups = self._cw.user.matching_groups
+                if all(matching_groups(e.get_groups('read')) and
+                       rschema.rdef(self.e_schema, e).get_groups('read')
+                       for e in targets):
+                    yield rschema, 'subject'
+
+    def _cw_to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
+        for rschema, attrschema in self.e_schema.attribute_definitions():
+            # skip binary data by default
+            if skip_bytes and attrschema.type == 'Bytes':
+                continue
+            attr = rschema.type
+            if attr == 'eid':
+                continue
+            # password retrieval is blocked at the repository server level
+            rdef = rschema.rdef(self.e_schema, attrschema)
+            if not self._cw.user.matching_groups(rdef.get_groups('read')) \
+                   or (attrschema.type == 'Password' and skip_pwd):
+                self.cw_attr_cache[attr] = None
+                continue
+            yield attr
+
+    _cw_completed = False
+    def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
+        """complete this entity by adding missing attributes (i.e. query the
+        repository to fill the entity)
+
+        :type skip_bytes: bool
+        :param skip_bytes:
+          if true, attribute of type Bytes won't be considered
+        """
+        assert self.has_eid()
+        if self._cw_completed:
+            return
+        if attributes is None:
+            self._cw_completed = True
+        varmaker = rqlvar_maker()
+        V = next(varmaker)
+        rql = ['WHERE %s eid %%(x)s' % V]
+        selected = []
+        for attr in (attributes or self._cw_to_complete_attributes(skip_bytes, skip_pwd)):
+            # if attribute already in entity, nothing to do
+            if attr in self.cw_attr_cache:
+                continue
+            # case where attribute must be completed, but is not yet in entity
+            var = next(varmaker)
+            rql.append('%s %s %s' % (V, attr, var))
+            selected.append((attr, var))
+        # +1 since this doesn't include the main variable
+        lastattr = len(selected) + 1
+        # don't fetch extra relation if attributes specified or of the entity is
+        # coming from an external source (may lead to error)
+        if attributes is None and self.cw_metainformation()['source']['uri'] == 'system':
+            # fetch additional relations (restricted to 0..1 relations)
+            for rschema, role in self._cw_to_complete_relations():
+                rtype = rschema.type
+                if self.cw_relation_cached(rtype, role):
+                    continue
+                # at this point we suppose that:
+                # * this is a inlined relation
+                # * entity (self) is the subject
+                # * user has read perm on the relation and on the target entity
+                assert rschema.inlined
+                assert role == 'subject'
+                var = next(varmaker)
+                # keep outer join anyway, we don't want .complete to crash on
+                # missing mandatory relation (see #1058267)
+                rql.append('%s %s %s?' % (V, rtype, var))
+                selected.append(((rtype, role), var))
+        if selected:
+            # select V, we need it as the left most selected variable
+            # if some outer join are included to fetch inlined relations
+            rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
+                                    ','.join(rql))
+            try:
+                rset = self._cw.execute(rql, {'x': self.eid}, build_descr=False)[0]
+            except IndexError:
+                raise Exception('unable to fetch attributes for entity with eid %s'
+                                % self.eid)
+            # handle attributes
+            for i in range(1, lastattr):
+                self.cw_attr_cache[str(selected[i-1][0])] = rset[i]
+            # handle relations
+            for i in range(lastattr, len(rset)):
+                rtype, role = selected[i-1][0]
+                value = rset[i]
+                if value is None:
+                    rrset = ResultSet([], rql, {'x': self.eid})
+                    rrset.req = self._cw
+                else:
+                    rrset = self._cw.eid_rset(value)
+                self.cw_set_relation_cache(rtype, role, rrset)
+
+    def cw_attr_value(self, name):
+        """get value for the attribute relation <name>, query the repository
+        to get the value if necessary.
+
+        :type name: str
+        :param name: name of the attribute to get
+        """
+        try:
+            return self.cw_attr_cache[name]
+        except KeyError:
+            if not self.cw_is_saved():
+                return None
+            rql = "Any A WHERE X eid %%(x)s, X %s A" % name
+            try:
+                rset = self._cw.execute(rql, {'x': self.eid})
+            except Unauthorized:
+                self.cw_attr_cache[name] = value = None
+            else:
+                assert rset.rowcount <= 1, (self, rql, rset.rowcount)
+                try:
+                    self.cw_attr_cache[name] = value = rset.rows[0][0]
+                except IndexError:
+                    # probably a multisource error
+                    self.critical("can't get value for attribute %s of entity with eid %s",
+                                  name, self.eid)
+                    if self.e_schema.destination(name) == 'String':
+                        self.cw_attr_cache[name] = value = self._cw._('unaccessible')
+                    else:
+                        self.cw_attr_cache[name] = value = None
+            return value
+
+    def related(self, rtype, role='subject', limit=None, entities=False, # XXX .cw_related
+                safe=False, targettypes=None):
+        """returns a resultset of related entities
+
+        :param rtype:
+          the name of the relation, aka relation type
+        :param role:
+          the role played by 'self' in the relation ('subject' or 'object')
+        :param limit:
+          resultset's maximum size
+        :param entities:
+          if True, the entites are returned; if False, a result set is returned
+        :param safe:
+          if True, an empty rset/list of entities will be returned in case of
+          :exc:`Unauthorized`, else (the default), the exception is propagated
+        :param targettypes:
+          a tuple of target entity types to restrict the query
+        """
+        rtype = str(rtype)
+        # Caching restricted/limited results is best avoided.
+        cacheable = limit is None and targettypes is None
+        if cacheable:
+            cache_key = '%s_%s' % (rtype, role)
+            if cache_key in self._cw_related_cache:
+                return self._cw_related_cache[cache_key][entities]
+        if not self.has_eid():
+            if entities:
+                return []
+            return self._cw.empty_rset()
+        rql = self.cw_related_rql(rtype, role, limit=limit, targettypes=targettypes)
+        try:
+            rset = self._cw.execute(rql, {'x': self.eid})
+        except Unauthorized:
+            if not safe:
+                raise
+            rset = self._cw.empty_rset()
+        if entities:
+            if cacheable:
+                self.cw_set_relation_cache(rtype, role, rset)
+                return self.related(rtype, role, entities=entities)
+            return list(rset.entities())
+        else:
+            return rset
+
+    def cw_related_rql(self, rtype, role='subject', targettypes=None, limit=None):
+        vreg = self._cw.vreg
+        rschema = vreg.schema[rtype]
+        select = Select()
+        mainvar, evar = select.get_variable('X'), select.get_variable('E')
+        select.add_selected(mainvar)
+        if limit is not None:
+            select.set_limit(limit)
+        select.add_eid_restriction(evar, 'x', 'Substitute')
+        if role == 'subject':
+            rel = make_relation(evar, rtype, (mainvar,), VariableRef)
+            select.add_restriction(rel)
+            if targettypes is None:
+                targettypes = rschema.objects(self.e_schema)
+            else:
+                select.add_constant_restriction(mainvar, 'is',
+                                                targettypes, 'etype')
+            gcard = greater_card(rschema, (self.e_schema,), targettypes, 0)
+        else:
+            rel = make_relation(mainvar, rtype, (evar,), VariableRef)
+            select.add_restriction(rel)
+            if targettypes is None:
+                targettypes = rschema.subjects(self.e_schema)
+            else:
+                select.add_constant_restriction(mainvar, 'is', targettypes,
+                                                'etype')
+            gcard = greater_card(rschema, targettypes, (self.e_schema,), 1)
+        etypecls = vreg['etypes'].etype_class(targettypes[0])
+        if len(targettypes) > 1:
+            fetchattrs = vreg['etypes'].fetch_attrs(targettypes)
+            self._fetch_ambiguous_rtypes(select, mainvar, fetchattrs,
+                                         targettypes, vreg.schema)
+        else:
+            fetchattrs = etypecls.fetch_attrs
+        etypecls.fetch_rqlst(self._cw.user, select, mainvar, fetchattrs,
+                             settype=False)
+        # optimisation: remove ORDERBY if cardinality is 1 or ? (though
+        # greater_card return 1 for those both cases)
+        if gcard == '1':
+            select.remove_sort_terms()
+        elif not select.orderby:
+            # if modification_date is already retrieved, we use it instead
+            # of adding another variable for sorting. This should not be
+            # problematic, but it is with sqlserver, see ticket #694445
+            for rel in select.where.get_nodes(RqlRelation):
+                if (rel.r_type == 'modification_date'
+                    and rel.children[0].variable == mainvar
+                    and rel.children[1].operator == '='):
+                    var = rel.children[1].children[0].variable
+                    select.add_sort_var(var, asc=False)
+                    break
+            else:
+                mdvar = select.make_variable()
+                rel = make_relation(mainvar, 'modification_date',
+                                    (mdvar,), VariableRef)
+                select.add_restriction(rel)
+                select.add_sort_var(mdvar, asc=False)
+        return select.as_string()
+
+    # generic vocabulary methods ##############################################
+
+    def cw_linkable_rql(self, rtype, targettype, role, ordermethod=None,
+                        vocabconstraints=True, lt_infos={}, limit=None):
+        """build a rql to fetch targettype entities either related or unrelated
+        to this entity using (rtype, role) relation.
+
+        Consider relation permissions so that returned entities may be actually
+        linked by `rtype`.
+
+        `lt_infos` are supplementary informations, usually coming from __linkto
+        parameter, that can help further restricting the results in case current
+        entity is not yet created. It is a dict describing entities the current
+        entity will be linked to, which keys are (rtype, role) tuples and values
+        are a list of eids.
+        """
+        return self._cw_compute_linkable_rql(rtype, targettype, role, ordermethod=None,
+                                             vocabconstraints=vocabconstraints,
+                                             lt_infos=lt_infos, limit=limit,
+                                             unrelated_only=False)
+
+    def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
+                         vocabconstraints=True, lt_infos={}, limit=None):
+        """build a rql to fetch `targettype` entities unrelated to this entity
+        using (rtype, role) relation.
+
+        Consider relation permissions so that returned entities may be actually
+        linked by `rtype`.
+
+        `lt_infos` are supplementary informations, usually coming from __linkto
+        parameter, that can help further restricting the results in case current
+        entity is not yet created. It is a dict describing entities the current
+        entity will be linked to, which keys are (rtype, role) tuples and values
+        are a list of eids.
+        """
+        return self._cw_compute_linkable_rql(rtype, targettype, role, ordermethod=None,
+                                             vocabconstraints=vocabconstraints,
+                                             lt_infos=lt_infos, limit=limit,
+                                             unrelated_only=True)
+
+    def _cw_compute_linkable_rql(self, rtype, targettype, role, ordermethod=None,
+                                 vocabconstraints=True, lt_infos={}, limit=None,
+                                 unrelated_only=False):
+        """build a rql to fetch `targettype` entities that may be related to
+        this entity using the (rtype, role) relation.
+
+        By default (unrelated_only=False), this includes the already linked
+        entities as well as the unrelated ones. If `unrelated_only` is True, the
+        rql filters out the already related entities.
+        """
+        ordermethod = ordermethod or 'fetch_unrelated_order'
+        rschema = self._cw.vreg.schema.rschema(rtype)
+        rdef = rschema.role_rdef(self.e_schema, targettype, role)
+        rewriter = RQLRewriter(self._cw)
+        select = Select()
+        # initialize some variables according to the `role` of `self` in the
+        # relation (variable names must respect constraints conventions):
+        # * variable for myself (`evar`)
+        # * variable for searched entities (`searchvedvar`)
+        if role == 'subject':
+            evar = subjvar = select.get_variable('S')
+            searchedvar = objvar = select.get_variable('O')
+        else:
+            searchedvar = subjvar = select.get_variable('S')
+            evar = objvar = select.get_variable('O')
+        select.add_selected(searchedvar)
+        if limit is not None:
+            select.set_limit(limit)
+        # initialize some variables according to `self` existence
+        if rdef.role_cardinality(neg_role(role)) in '?1':
+            # if cardinality in '1?', we want a target entity which isn't
+            # already linked using this relation
+            variable = select.make_variable()
+            if role == 'subject':
+                rel = make_relation(variable, rtype, (searchedvar,), VariableRef)
+            else:
+                rel = make_relation(searchedvar, rtype, (variable,), VariableRef)
+            select.add_restriction(Not(rel))
+        elif self.has_eid() and unrelated_only:
+            # elif we have an eid, we don't want a target entity which is
+            # already linked to ourself through this relation
+            rel = make_relation(subjvar, rtype, (objvar,), VariableRef)
+            select.add_restriction(Not(rel))
+        if self.has_eid():
+            rel = make_relation(evar, 'eid', ('x', 'Substitute'), Constant)
+            select.add_restriction(rel)
+            args = {'x': self.eid}
+            if role == 'subject':
+                sec_check_args = {'fromeid': self.eid}
+            else:
+                sec_check_args = {'toeid': self.eid}
+            existant = None # instead of 'SO', improve perfs
+        else:
+            args = {}
+            sec_check_args = {}
+            existant = searchedvar.name
+            # undefine unused evar, or the type resolver will consider it
+            select.undefine_variable(evar)
+        # retrieve entity class for targettype to compute base rql
+        etypecls = self._cw.vreg['etypes'].etype_class(targettype)
+        etypecls.fetch_rqlst(self._cw.user, select, searchedvar,
+                             ordermethod=ordermethod)
+        # from now on, we need variable type resolving
+        self._cw.vreg.solutions(self._cw, select, args)
+        # insert RQL expressions for schema constraints into the rql syntax tree
+        if vocabconstraints:
+            cstrcls = (RQLVocabularyConstraint, RQLConstraint)
+        else:
+            cstrcls = RQLConstraint
+        lt_infos = pruned_lt_info(self.e_schema, lt_infos or {})
+        # if there are still lt_infos, use set to keep track of added eid
+        # relations (adding twice the same eid relation is incorrect RQL)
+        eidvars = set()
+        for cstr in rdef.constraints:
+            # consider constraint.mainvars to check if constraint apply
+            if isinstance(cstr, cstrcls) and searchedvar.name in cstr.mainvars:
+                if not self.has_eid():
+                    if lt_infos:
+                        # we can perhaps further restrict with linkto infos using
+                        # a custom constraint built from cstr and lt_infos
+                        cstr = build_cstr_with_linkto_infos(
+                            cstr, args, searchedvar, evar, lt_infos, eidvars)
+                        if cstr is None:
+                            continue # could not build constraint -> discard
+                    elif evar.name in cstr.mainvars:
+                        continue
+                # compute a varmap suitable to RQLRewriter.rewrite argument
+                varmap = dict((v, v) for v in (searchedvar.name, evar.name)
+                              if v in select.defined_vars and v in cstr.mainvars)
+                # rewrite constraint by constraint since we want a AND between
+                # expressions.
+                rewriter.rewrite(select, [(varmap, (cstr,))], args, existant)
+        # insert security RQL expressions granting the permission to 'add' the
+        # relation into the rql syntax tree, if necessary
+        rqlexprs = rdef.get_rqlexprs('add')
+        if not self.has_eid():
+            rqlexprs = [rqlexpr for rqlexpr in rqlexprs
+                        if searchedvar.name in rqlexpr.mainvars]
+        if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args):
+            # compute a varmap suitable to RQLRewriter.rewrite argument
+            varmap = dict((v, v) for v in (searchedvar.name, evar.name)
+                          if v in select.defined_vars)
+            # rewrite all expressions at once since we want a OR between them.
+            rewriter.rewrite(select, [(varmap, rqlexprs)], args, existant)
+        # ensure we have an order defined
+        if not select.orderby:
+            select.add_sort_var(select.defined_vars[searchedvar.name])
+        # we're done, turn the rql syntax tree as a string
+        rql = select.as_string()
+        return rql, args
+
+    def unrelated(self, rtype, targettype, role='subject', limit=None,
+                  ordermethod=None, lt_infos={}): # XXX .cw_unrelated
+        """return a result set of target type objects that may be related
+        by a given relation, with self as subject or object
+        """
+        try:
+            rql, args = self.cw_unrelated_rql(rtype, targettype, role, limit=limit,
+                                              ordermethod=ordermethod, lt_infos=lt_infos)
+        except Unauthorized:
+            return self._cw.empty_rset()
+        return self._cw.execute(rql, args)
+
+    # relations cache handling #################################################
+
+    def cw_relation_cached(self, rtype, role):
+        """return None if the given relation isn't already cached on the
+        instance, else the content of the cache (a 2-uple (rset, entities)).
+        """
+        return self._cw_related_cache.get('%s_%s' % (rtype, role))
+
+    def cw_set_relation_cache(self, rtype, role, rset):
+        """set cached values for the given relation"""
+        if rset:
+            related = list(rset.entities(0))
+            rschema = self._cw.vreg.schema.rschema(rtype)
+            if role == 'subject':
+                rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1]
+                target = 'object'
+            else:
+                rcard = rschema.rdef(related[0].e_schema, self.e_schema).cardinality[0]
+                target = 'subject'
+            if rcard in '?1':
+                for rentity in related:
+                    rentity._cw_related_cache['%s_%s' % (rtype, target)] = (
+                        self.as_rset(), (self,))
+        else:
+            related = ()
+        self._cw_related_cache['%s_%s' % (rtype, role)] = (rset, related)
+
+    def cw_clear_relation_cache(self, rtype=None, role=None):
+        """clear cached values for the given relation or the entire cache if
+        no relation is given
+        """
+        if rtype is None:
+            self._cw_related_cache.clear()
+            self._cw_adapters_cache.clear()
+        else:
+            assert role
+            self._cw_related_cache.pop('%s_%s' % (rtype, role), None)
+
+    def cw_clear_all_caches(self):
+        """flush all caches on this entity. Further attributes/relations access
+        will triggers new database queries to get back values.
+
+        If you use custom caches on your entity class (take care to @cached!),
+        you should override this method to clear them as well.
+        """
+        # clear attributes cache
+        self._cw_completed = False
+        self.cw_attr_cache.clear()
+        # clear relations cache
+        self.cw_clear_relation_cache()
+        # rest path unique cache
+        try:
+            del self.__unique
+        except AttributeError:
+            pass
+
+    # raw edition utilities ###################################################
+
+    def cw_set(self, **kwargs):
+        """update this entity using given attributes / relation, working in the
+        same fashion as :meth:`cw_instantiate`.
+
+        Example (in a shell session):
+
+        >>> c = rql('Any X WHERE X is Company').get_entity(0, 0)
+        >>> p = rql('Any X WHERE X is Person').get_entity(0, 0)
+        >>> c.cw_set(name=u'Logilab')
+        >>> p.cw_set(firstname=u'John', lastname=u'Doe', works_for=c)
+
+        You can also set relations where the entity has 'object' role by
+        prefixing the relation name by 'reverse_'.  Also, relation values may be
+        an entity or eid, a list of entities or eids, or None (meaning that all
+        relations of the given type from or to this object should be deleted).
+        """
+        assert kwargs
+        assert self.cw_is_saved(), "should not call set_attributes while entity "\
+               "hasn't been saved yet"
+        rql, qargs, pendingrels, attrcache = self._cw_build_entity_query(kwargs)
+        if rql:
+            rql = 'SET ' + rql
+            qargs['x'] = self.eid
+            if ' WHERE ' in rql:
+                rql += ', X eid %(x)s'
+            else:
+                rql += ' WHERE X eid %(x)s'
+            self._cw.execute(rql, qargs)
+        # update current local object _after_ the rql query to avoid
+        # interferences between the query execution itself and the cw_edited /
+        # skip_security machinery
+        self._cw_update_attr_cache(attrcache)
+        self._cw_handle_pending_relations(self.eid, pendingrels, self._cw.execute)
+        # XXX update relation cache
+
+    def cw_delete(self, **kwargs):
+        assert self.has_eid(), self.eid
+        self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
+                         {'x': self.eid}, **kwargs)
+
+    # server side utilities ####################################################
+
+    def _cw_clear_local_perm_cache(self, action):
+        for rqlexpr in self.e_schema.get_rqlexprs(action):
+            self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
+
+    # deprecated stuff #########################################################
+
+    @deprecated('[3.16] use cw_set() instead of set_attributes()')
+    def set_attributes(self, **kwargs): # XXX cw_set_attributes
+        if kwargs:
+            self.cw_set(**kwargs)
+
+    @deprecated('[3.16] use cw_set() instead of set_relations()')
+    def set_relations(self, **kwargs): # XXX cw_set_relations
+        """add relations to the given object. To set a relation where this entity
+        is the object of the relation, use 'reverse_'<relation> as argument name.
+
+        Values may be an entity or eid, a list of entities or eids, or None
+        (meaning that all relations of the given type from or to this object
+        should be deleted).
+        """
+        if kwargs:
+            self.cw_set(**kwargs)
+
+    @deprecated('[3.13] use entity.cw_clear_all_caches()')
+    def clear_all_caches(self):
+        return self.cw_clear_all_caches()
+
+
+# attribute and relation descriptors ##########################################
+
+class Attribute(object):
+    """descriptor that controls schema attribute access"""
+
+    def __init__(self, attrname):
+        assert attrname != 'eid'
+        self._attrname = attrname
+
+    def __get__(self, eobj, eclass):
+        if eobj is None:
+            return self
+        return eobj.cw_attr_value(self._attrname)
+
+    @deprecated('[3.10] assign to entity.cw_attr_cache[attr] or entity.cw_edited[attr]')
+    def __set__(self, eobj, value):
+        if hasattr(eobj, 'cw_edited') and not eobj.cw_edited.saved:
+            eobj.cw_edited[self._attrname] = value
+        else:
+            eobj.cw_attr_cache[self._attrname] = value
+
+
+class Relation(object):
+    """descriptor that controls schema relation access"""
+
+    def __init__(self, rschema, role):
+        self._rtype = rschema.type
+        self._role = role
+
+    def __get__(self, eobj, eclass):
+        if eobj is None:
+            raise AttributeError('%s can only be accessed from instances'
+                                 % self._rtype)
+        return eobj.related(self._rtype, self._role, entities=True)
+
+    def __set__(self, eobj, value):
+        raise NotImplementedError
+
+
+from logging import getLogger
+from cubicweb import set_log_methods
+set_log_methods(Entity, getLogger('cubicweb.entity'))