[session] cleanup hook / operation / entity edition api
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 25 Aug 2010 10:29:18 +0200
changeset 6142 8bc6eac1fac1
parent 6141 b8287e54b528
child 6146 f3d82f25ab61
[session] cleanup hook / operation / entity edition api Operation api ~~~~~~~~~~~~~ * commit_event killed, recently introduced postcommit_event is enough and has a better name * kill SingleOperation class, it's a) currently never used b) superseeded by set_operation if needed. Entity edition api ~~~~~~~~~~~~~~~~~~ edited_attributes turned into a special object holding edition specific attributes: - attributes to be edited (simply mirrored in cw_attr_cache, actual values are there) - former _cw_skip_security set (cw_edited) and querier_pending_relations It has also been renamed to `cw_edited` on the way (it may also contains inlined relations) The entity dict interface has been deprecated. One should explicitly use either cw_attr_cache or cw_edited according to the need. Also, there is now a control that we don't try to hi-jack edited attributes once this has no more effect (eg modification have already been saved) At last, _cw_set_defaults/cw_check internal methods have been moved to this special object Hook api ~~~~~~~~ hook.entity_oldnewvalue function now moved to a method of cw_edited object.
dataimport.py
dbapi.py
entity.py
goa/gaesource.py
hooks/integrity.py
hooks/metadata.py
hooks/notification.py
hooks/security.py
hooks/syncschema.py
hooks/syncsession.py
hooks/test/unittest_hooks.py
hooks/workflow.py
rset.py
server/hook.py
server/pool.py
server/querier.py
server/repository.py
server/session.py
server/sources/__init__.py
server/sources/ldapuser.py
server/sources/native.py
server/sources/storages.py
server/sqlutils.py
server/ssplanner.py
server/test/unittest_repository.py
sobjects/supervising.py
test/unittest_entity.py
test/unittest_rset.py
test/unittest_utils.py
web/formfields.py
web/views/cwproperties.py
web/views/xmlrss.py
--- a/dataimport.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/dataimport.py	Wed Aug 25 10:29:18 2010 +0200
@@ -81,6 +81,7 @@
 from logilab.common.deprecation import deprecated
 
 from cubicweb.server.utils import eschema_eid
+from cubicweb.server.ssplanner import EditedEntity
 
 def count_lines(stream_or_filename):
     if isinstance(stream_or_filename, basestring):
@@ -605,8 +606,7 @@
         entity = copy(entity)
         entity.cw_clear_relation_cache()
         self.metagen.init_entity(entity)
-        entity.update(kwargs)
-        entity.edited_attributes = set(entity)
+        entity.cw_edited.update(kwargs, skipsec=False)
         session = self.session
         self.source.add_entity(session, entity)
         self.source.add_info(session, entity, self.source, None, complete=False)
@@ -679,8 +679,9 @@
         entity = self.session.vreg['etypes'].etype_class(etype)(self.session)
         # entity are "surface" copied, avoid shared dict between copies
         del entity.cw_extra_kwargs
+        entity.cw_edited = EditedEntity(entity)
         for attr in self.etype_attrs:
-            entity[attr] = self.generate(entity, attr)
+            entity.cw_edited.attribute_edited(attr, self.generate(entity, attr))
         rels = {}
         for rel in self.etype_rels:
             rels[rel] = self.generate(entity, rel)
@@ -689,7 +690,7 @@
     def init_entity(self, entity):
         entity.eid = self.source.create_eid(self.session)
         for attr in self.entity_attrs:
-            entity[attr] = self.generate(entity, attr)
+            entity.cw_edited.attribute_edited(attr, self.generate(entity, attr))
 
     def generate(self, entity, rtype):
         return getattr(self, 'gen_%s' % rtype)(entity)
--- a/dbapi.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/dbapi.py	Wed Aug 25 10:29:18 2010 +0200
@@ -631,7 +631,7 @@
         else:
             from cubicweb.entity import Entity
             user = Entity(req, rset, row=0)
-        user['login'] = login # cache login
+        user.cw_attr_cache['login'] = login # cache login
         return user
 
     def __del__(self):
--- a/entity.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/entity.py	Wed Aug 25 10:29:18 2010 +0200
@@ -19,7 +19,6 @@
 
 __docformat__ = "restructuredtext en"
 
-from copy import copy
 from warnings import warn
 
 from logilab.common import interface
@@ -312,6 +311,9 @@
         return '<Entity %s %s %s at %s>' % (
             self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self))
 
+    def __cmp__(self, other):
+        raise NotImplementedError('comparison not implemented for %s' % self.__class__)
+
     def __json_encode__(self):
         """custom json dumps hook to dump the entity's eid
         which is not part of dict structure itself
@@ -320,107 +322,6 @@
         dumpable['eid'] = self.eid
         return dumpable
 
-    def __nonzero__(self):
-        return True
-
-    def __hash__(self):
-        return id(self)
-
-    def __cmp__(self, other):
-        raise NotImplementedError('comparison not implemented for %s' % self.__class__)
-
-    def __contains__(self, key):
-        return key in self.cw_attr_cache
-
-    def __iter__(self):
-        return iter(self.cw_attr_cache)
-
-    def __getitem__(self, key):
-        if key == 'eid':
-            warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
-                 DeprecationWarning, stacklevel=2)
-            return self.eid
-        return self.cw_attr_cache[key]
-
-    def __setitem__(self, attr, value):
-        """override __setitem__ to update self.edited_attributes.
-
-        Typically, a before_[update|add]_hook could do::
-
-            entity['generated_attr'] = generated_value
-
-        and this way, edited_attributes will be updated accordingly. Also, add
-        the attribute to skip_security since we don't want to check security
-        for such attributes set by hooks.
-        """
-        if attr == 'eid':
-            warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead',
-                 DeprecationWarning, stacklevel=2)
-            self.eid = value
-        else:
-            self.cw_attr_cache[attr] = value
-            # don't add attribute into skip_security if already in edited
-            # attributes, else we may accidentaly skip a desired security check
-            if hasattr(self, 'edited_attributes') and \
-                   attr not in self.edited_attributes:
-                self.edited_attributes.add(attr)
-                self._cw_skip_security_attributes.add(attr)
-
-    def __delitem__(self, attr):
-        """override __delitem__ to update self.edited_attributes on cleanup of
-        undesired changes introduced in the entity's dict. For example, see the
-        code snippet below from the `forge` cube:
-
-        .. sourcecode:: python
-
-            edited = self.entity.edited_attributes
-            has_load_left = 'load_left' in edited
-            if 'load' in edited and self.entity.load_left is None:
-                self.entity.load_left = self.entity['load']
-            elif not has_load_left and edited:
-                # cleanup, this may cause undesired changes
-                del self.entity['load_left']
-
-        """
-        del self.cw_attr_cache[attr]
-        if hasattr(self, 'edited_attributes'):
-            self.edited_attributes.remove(attr)
-
-    def clear(self):
-        self.cw_attr_cache.clear()
-
-    def get(self, key, default=None):
-        return self.cw_attr_cache.get(key, default)
-
-    def setdefault(self, attr, default):
-        """override setdefault to update self.edited_attributes"""
-        value = self.cw_attr_cache.setdefault(attr, default)
-        # don't add attribute into skip_security if already in edited
-        # attributes, else we may accidentaly skip a desired security check
-        if hasattr(self, 'edited_attributes') and \
-               attr not in self.edited_attributes:
-            self.edited_attributes.add(attr)
-            self._cw_skip_security_attributes.add(attr)
-        return value
-
-    def pop(self, attr, default=_marker):
-        """override pop to update self.edited_attributes on cleanup of
-        undesired changes introduced in the entity's dict. See `__delitem__`
-        """
-        if default is _marker:
-            value = self.cw_attr_cache.pop(attr)
-        else:
-            value = self.cw_attr_cache.pop(attr, default)
-        if hasattr(self, 'edited_attributes') and attr in self.edited_attributes:
-            self.edited_attributes.remove(attr)
-        return value
-
-    def update(self, values):
-        """override update to update self.edited_attributes. See `__setitem__`
-        """
-        for attr, value in values.items():
-            self[attr] = value # use self.__setitem__ implementation
-
     def cw_adapt_to(self, interface):
         """return an adapter the entity to the given interface name.
 
@@ -590,12 +491,6 @@
 
     # entity cloning ##########################################################
 
-    def cw_copy(self):
-        thecopy = copy(self)
-        thecopy.cw_attr_cache = copy(self.cw_attr_cache)
-        thecopy._cw_related_cache = {}
-        return thecopy
-
     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
@@ -680,7 +575,7 @@
             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[attr] = None
+                self.cw_attr_cache[attr] = None
                 continue
             yield attr
 
@@ -739,7 +634,7 @@
             rset = self._cw.execute(rql, {'x': self.eid}, build_descr=False)[0]
             # handle attributes
             for i in xrange(1, lastattr):
-                self[str(selected[i-1][0])] = rset[i]
+                self.cw_attr_cache[str(selected[i-1][0])] = rset[i]
             # handle relations
             for i in xrange(lastattr, len(rset)):
                 rtype, role = selected[i-1][0]
@@ -759,7 +654,7 @@
         :param name: name of the attribute to get
         """
         try:
-            value = self.cw_attr_cache[name]
+            return self.cw_attr_cache[name]
         except KeyError:
             if not self.cw_is_saved():
                 return None
@@ -767,21 +662,20 @@
             try:
                 rset = self._cw.execute(rql, {'x': self.eid})
             except Unauthorized:
-                self[name] = value = None
+                self.cw_attr_cache[name] = value = None
             else:
                 assert rset.rowcount <= 1, (self, rql, rset.rowcount)
                 try:
-                    self[name] = value = rset.rows[0][0]
+                    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':
-                        # XXX (syt) imo emtpy string is better
-                        self[name] = value = self._cw._('unaccessible')
+                        self.cw_attr_cache[name] = value = self._cw._('unaccessible')
                     else:
-                        self[name] = value = None
-        return value
+                        self.cw_attr_cache[name] = value = None
+            return value
 
     def related(self, rtype, role='subject', limit=None, entities=False): # XXX .cw_related
         """returns a resultset of related entities
@@ -985,7 +879,6 @@
         you should override this method to clear them as well.
         """
         # clear attributes cache
-        haseid = 'eid' in self
         self._cw_completed = False
         self.cw_attr_cache.clear()
         # clear relations cache
@@ -1012,9 +905,9 @@
                          kwargs)
         kwargs.pop('x')
         # update current local object _after_ the rql query to avoid
-        # interferences between the query execution itself and the
-        # edited_attributes / skip_security_attributes machinery
-        self.update(kwargs)
+        # interferences between the query execution itself and the cw_edited /
+        # skip_security machinery
+        self.cw_attr_cache.update(kwargs)
 
     def set_relations(self, **kwargs): # XXX cw_set_relations
         """add relations to the given object. To set a relation where this entity
@@ -1045,58 +938,13 @@
         self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
                          {'x': self.eid}, **kwargs)
 
-    # server side utilities ###################################################
-
-    def _cw_rql_set_value(self, attr, value):
-        """call by rql execution plan when some attribute is modified
-
-        don't use dict api in such case since we don't want attribute to be
-        added to skip_security_attributes.
-
-        This method is for internal use, you should not use it.
-        """
-        self.cw_attr_cache[attr] = value
+    # 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)
 
-    @property
-    def _cw_skip_security_attributes(self):
-        try:
-            return self.__cw_skip_security_attributes
-        except:
-            self.__cw_skip_security_attributes = set()
-            return self.__cw_skip_security_attributes
-
-    def _cw_set_defaults(self):
-        """set default values according to the schema"""
-        for attr, value in self.e_schema.defaults():
-            if not self.cw_attr_cache.has_key(attr):
-                self[str(attr)] = value
-
-    def _cw_check(self, creation=False):
-        """check this entity against its schema. Only final relation
-        are checked here, constraint on actual relations are checked in hooks
-        """
-        # necessary since eid is handled specifically and yams require it to be
-        # in the dictionary
-        if self._cw is None:
-            _ = unicode
-        else:
-            _ = self._cw._
-        if creation:
-            # on creations, we want to check all relations, especially
-            # required attributes
-            relations = [rschema for rschema in self.e_schema.subject_relations()
-                         if rschema.final and rschema.type != 'eid']
-        elif hasattr(self, 'edited_attributes'):
-            relations = [self._cw.vreg.schema.rschema(rtype)
-                         for rtype in self.edited_attributes]
-        else:
-            relations = None
-        self.e_schema.check(self, creation=creation, _=_,
-                            relations=relations)
+    # deprecated stuff #########################################################
 
     @deprecated('[3.9] use entity.cw_attr_value(attr)')
     def get_value(self, name):
@@ -1126,6 +974,109 @@
     def related_rql(self, rtype, role='subject', targettypes=None):
         return self.cw_related_rql(rtype, role, targettypes)
 
+    @property
+    @deprecated('[3.10] use entity.cw_edited')
+    def edited_attributes(self):
+        return self.cw_edited
+
+    @property
+    @deprecated('[3.10] use entity.cw_edited.skip_security')
+    def skip_security_attributes(self):
+        return self.cw_edited.skip_security
+
+    @property
+    @deprecated('[3.10] use entity.cw_edited.skip_security')
+    def _cw_skip_security_attributes(self):
+        return self.cw_edited.skip_security
+
+    @property
+    @deprecated('[3.10] use entity.cw_edited.skip_security')
+    def querier_pending_relations(self):
+        return self.cw_edited.querier_pending_relations
+
+    @deprecated('[3.10] use key in entity.cw_attr_cache')
+    def __contains__(self, key):
+        return key in self.cw_attr_cache
+
+    @deprecated('[3.10] iter on entity.cw_attr_cache')
+    def __iter__(self):
+        return iter(self.cw_attr_cache)
+
+    @deprecated('[3.10] use entity.cw_attr_cache[attr]')
+    def __getitem__(self, key):
+        if key == 'eid':
+            warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
+                 DeprecationWarning, stacklevel=2)
+            return self.eid
+        return self.cw_attr_cache[key]
+
+    @deprecated('[3.10] use entity.cw_attr_cache.get(attr[, default])')
+    def get(self, key, default=None):
+        return self.cw_attr_cache.get(key, default)
+
+    @deprecated('[3.10] use entity.cw_attr_cache.clear()')
+    def clear(self):
+        self.cw_attr_cache.clear()
+        # XXX clear cw_edited ?
+
+    @deprecated('[3.10] use entity.cw_edited[attr] = value or entity.cw_attr_cache[attr] = value')
+    def __setitem__(self, attr, value):
+        """override __setitem__ to update self.cw_edited.
+
+        Typically, a before_[update|add]_hook could do::
+
+            entity['generated_attr'] = generated_value
+
+        and this way, cw_edited will be updated accordingly. Also, add
+        the attribute to skip_security since we don't want to check security
+        for such attributes set by hooks.
+        """
+        if attr == 'eid':
+            warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead',
+                 DeprecationWarning, stacklevel=2)
+            self.eid = value
+        else:
+            try:
+                self.cw_edited[attr] = value
+            except AttributeError:
+                self.cw_attr_cache[attr] = value
+
+    @deprecated('[3.10] use del entity.cw_edited[attr]')
+    def __delitem__(self, attr):
+        """override __delitem__ to update self.cw_edited on cleanup of
+        undesired changes introduced in the entity's dict. For example, see the
+        code snippet below from the `forge` cube:
+
+        .. sourcecode:: python
+
+            edited = self.entity.cw_edited
+            has_load_left = 'load_left' in edited
+            if 'load' in edited and self.entity.load_left is None:
+                self.entity.load_left = self.entity['load']
+            elif not has_load_left and edited:
+                # cleanup, this may cause undesired changes
+                del self.entity['load_left']
+        """
+        del self.cw_edited[attr]
+
+    @deprecated('[3.10] use entity.cw_edited.setdefault(attr, default)')
+    def setdefault(self, attr, default):
+        """override setdefault to update self.cw_edited"""
+        return self.cw_edited.setdefault(attr, default)
+
+    @deprecated('[3.10] use entity.cw_edited.pop(attr[, default])')
+    def pop(self, attr, *args):
+        """override pop to update self.cw_edited on cleanup of
+        undesired changes introduced in the entity's dict. See `__delitem__`
+        """
+        return self.cw_edited.pop(attr, *args)
+
+    @deprecated('[3.10] use entity.cw_edited.update(values)')
+    def update(self, values):
+        """override update to update self.cw_edited. See `__setitem__`
+        """
+        self.cw_edited.update(values)
+
 
 # attribute and relation descriptors ##########################################
 
@@ -1141,8 +1092,9 @@
             return self
         return eobj.cw_attr_value(self._attrname)
 
+    @deprecated('[3.10] use entity.cw_attr_cache[attr] = value')
     def __set__(self, eobj, value):
-        eobj[self._attrname] = value
+        eobj.cw_attr_cache[self._attrname] = value
 
 
 class Relation(object):
--- a/goa/gaesource.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/goa/gaesource.py	Wed Aug 25 10:29:18 2010 +0200
@@ -118,7 +118,7 @@
             Put(gaeentity)
         modified.clear()
 
-    def commit_event(self):
+    def postcommit_event(self):
         self._put_entities()
 
     def precommit_event(self):
--- a/hooks/integrity.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/hooks/integrity.py	Wed Aug 25 10:29:18 2010 +0200
@@ -17,8 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Core hooks: check for data integrity according to the instance'schema
 validity
+"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from threading import Lock
@@ -64,8 +64,6 @@
         _UNIQUE_CONSTRAINTS_LOCK.release()
 
 class _ReleaseUniqueConstraintsOperation(hook.Operation):
-    def commit_event(self):
-        pass
     def postcommit_event(self):
         _release_unique_cstr_lock(self.session)
     def rollback_event(self):
@@ -185,9 +183,6 @@
                     self.critical('can\'t check constraint %s, not supported',
                                   constraint)
 
-    def commit_event(self):
-        pass
-
 
 class CheckConstraintHook(IntegrityHook):
     """check the relation satisfy its constraints
@@ -219,7 +214,7 @@
 
     def __call__(self):
         eschema = self.entity.e_schema
-        for attr in self.entity.edited_attributes:
+        for attr in self.entity.cw_edited:
             if eschema.subjrels[attr].final:
                 constraints = [c for c in eschema.rdef(attr).constraints
                                if isinstance(c, (RQLUniqueConstraint, RQLConstraint))]
@@ -236,9 +231,8 @@
     def __call__(self):
         entity = self.entity
         eschema = entity.e_schema
-        for attr in entity.edited_attributes:
+        for attr, val in entity.cw_edited.iteritems():
             if eschema.subjrels[attr].final and eschema.has_unique_values(attr):
-                val = entity[attr]
                 if val is None:
                     continue
                 rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
@@ -257,18 +251,17 @@
     events = ('before_delete_entity', 'before_update_entity')
 
     def __call__(self):
-        if self.event == 'before_delete_entity' and self.entity.name == 'owners':
+        entity = self.entity
+        if self.event == 'before_delete_entity' and entity.name == 'owners':
             msg = self._cw._('can\'t be deleted')
-            raise ValidationError(self.entity.eid, {None: msg})
-        elif self.event == 'before_update_entity' and \
-                 'name' in self.entity.edited_attributes:
-            newname = self.entity.pop('name')
-            oldname = self.entity.name
+            raise ValidationError(entity.eid, {None: msg})
+        elif self.event == 'before_update_entity' \
+                 and 'name' in entity.cw_edited:
+            oldname, newname = entity.cw_edited.oldnewvalue('name')
             if oldname == 'owners' and newname != oldname:
                 qname = role_name('name', 'subject')
                 msg = self._cw._('can\'t be changed')
-                raise ValidationError(self.entity.eid, {qname: msg})
-            self.entity['name'] = newname
+                raise ValidationError(entity.eid, {qname: msg})
 
 
 class TidyHtmlFields(IntegrityHook):
@@ -279,15 +272,16 @@
     def __call__(self):
         entity = self.entity
         metaattrs = entity.e_schema.meta_attributes()
+        edited = entity.cw_edited
         for metaattr, (metadata, attr) in metaattrs.iteritems():
-            if metadata == 'format' and attr in entity.edited_attributes:
+            if metadata == 'format' and attr in edited:
                 try:
-                    value = entity[attr]
+                    value = edited[attr]
                 except KeyError:
                     continue # no text to tidy
                 if isinstance(value, unicode): # filter out None and Binary
                     if getattr(entity, str(metaattr)) == 'text/html':
-                        entity[attr] = soup2xhtml(value, self._cw.encoding)
+                        edited[attr] = soup2xhtml(value, self._cw.encoding)
 
 
 class StripCWUserLoginHook(IntegrityHook):
@@ -297,9 +291,9 @@
     events = ('before_add_entity', 'before_update_entity',)
 
     def __call__(self):
-        user = self.entity
-        if 'login' in user.edited_attributes and user.login:
-            user.login = user.login.strip()
+        login = self.entity.cw_edited.get('login')
+        if login:
+            self.entity.cw_edited['login'] = login.strip()
 
 
 # 'active' integrity hooks: you usually don't want to deactivate them, they are
--- a/hooks/metadata.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/hooks/metadata.py	Wed Aug 25 10:29:18 2010 +0200
@@ -41,11 +41,12 @@
 
     def __call__(self):
         timestamp = datetime.now()
-        self.entity.setdefault('creation_date', timestamp)
-        self.entity.setdefault('modification_date', timestamp)
+        edited = self.entity.cw_edited
+        edited.setdefault('creation_date', timestamp)
+        edited.setdefault('modification_date', timestamp)
         if not self._cw.get_shared_data('do-not-insert-cwuri'):
             cwuri = u'%seid/%s' % (self._cw.base_url(), self.entity.eid)
-            self.entity.setdefault('cwuri', cwuri)
+            edited.setdefault('cwuri', cwuri)
 
 
 class UpdateMetaAttrsHook(MetaDataHook):
@@ -60,7 +61,7 @@
         # XXX to be really clean, we should turn off modification_date update
         # explicitly on each command where we do not want that behaviour.
         if not self._cw.vreg.config.repairing:
-            self.entity.setdefault('modification_date', datetime.now())
+            self.entity.cw_edited.setdefault('modification_date', datetime.now())
 
 
 class _SetCreatorOp(hook.Operation):
--- a/hooks/notification.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/hooks/notification.py	Wed Aug 25 10:29:18 2010 +0200
@@ -125,7 +125,7 @@
         if session.added_in_transaction(self.entity.eid):
             return # entity is being created
         # then compute changes
-        attrs = [k for k in self.entity.edited_attributes
+        attrs = [k for k in self.entity.cw_edited
                  if not k in self.skip_attrs]
         if not attrs:
             return
@@ -168,8 +168,9 @@
             if self._cw.added_in_transaction(self.entity.eid):
                 return False
             if self.entity.e_schema == 'CWUser':
-                if not (self.entity.edited_attributes - frozenset(('eid', 'modification_date',
-                                                                   'last_login_time'))):
+                if not (frozenset(self.entity.cw_edited)
+                        - frozenset(('eid', 'modification_date',
+                                     'last_login_time'))):
                     # don't record last_login_time update which are done
                     # automatically at login time
                     return False
--- a/hooks/security.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/hooks/security.py	Wed Aug 25 10:29:18 2010 +0200
@@ -31,12 +31,9 @@
     eschema = entity.e_schema
     # ._cw_skip_security_attributes is there to bypass security for attributes
     # set by hooks by modifying the entity's dictionnary
-    dontcheck = entity._cw_skip_security_attributes
     if editedattrs is None:
-        try:
-            editedattrs = entity.edited_attributes
-        except AttributeError:
-            editedattrs = entity # XXX unexpected
+        editedattrs = entity.cw_edited
+    dontcheck = editedattrs.skip_security
     for attr in editedattrs:
         if attr in dontcheck:
             continue
@@ -46,10 +43,6 @@
             if creation and not rdef.permissions.get('update'):
                 continue
             rdef.check_perm(session, 'update', eid=eid)
-    # don't update dontcheck until everything went fine: see usage in
-    # after_update_entity, where if we got an Unauthorized at hook time, we will
-    # retry and commit time
-    dontcheck |= frozenset(editedattrs)
 
 
 class _CheckEntityPermissionOp(hook.LateOperation):
@@ -57,15 +50,12 @@
         #print 'CheckEntityPermissionOp', self.session.user, self.entity, self.action
         session = self.session
         for values in session.transaction_data.pop('check_entity_perm_op'):
-            entity = session.entity_from_eid(values[0])
-            action = values[1]
+            eid, action, edited = values
+            entity = session.entity_from_eid(eid)
             entity.cw_check_perm(action)
-            check_entity_attributes(session, entity, values[2:],
+            check_entity_attributes(session, entity, edited,
                                     creation=self.creation)
 
-    def commit_event(self):
-        pass
-
 
 class _CheckRelationPermissionOp(hook.LateOperation):
     def precommit_event(self):
@@ -76,9 +66,6 @@
                                 session.describe(eidto)[0])
             rdef.check_perm(session, action, fromeid=eidfrom, toeid=eidto)
 
-    def commit_event(self):
-        pass
-
 
 @objectify_selector
 @lltrace
@@ -99,7 +86,7 @@
 
     def __call__(self):
         hook.set_operation(self._cw, 'check_entity_perm_op',
-                           (self.entity.eid, 'add') + tuple(self.entity.edited_attributes),
+                           (self.entity.eid, 'add', self.entity.cw_edited),
                            _CheckEntityPermissionOp, creation=True)
 
 
@@ -115,10 +102,10 @@
         except Unauthorized:
             self.entity._cw_clear_local_perm_cache('update')
             # save back editedattrs in case the entity is reedited later in the
-            # same transaction, which will lead to edited_attributes being
+            # same transaction, which will lead to cw_edited being
             # overwritten
             hook.set_operation(self._cw, 'check_entity_perm_op',
-                               (self.entity.eid, 'update') + tuple(self.entity.edited_attributes),
+                               (self.entity.eid, 'update', self.entity.cw_edited),
                                _CheckEntityPermissionOp, creation=False)
 
 
--- a/hooks/syncschema.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/hooks/syncschema.py	Wed Aug 25 10:29:18 2010 +0200
@@ -128,14 +128,12 @@
 def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
     errors = {}
     # don't use getattr(entity, attr), we would get the modified value if any
-    for attr in entity.edited_attributes:
+    for attr in entity.cw_edited:
         if attr in ro_attrs:
-            newval = entity.pop(attr)
-            origval = getattr(entity, attr)
+            origval, newval = entity.cw_edited.oldnewvalue(attr)
             if newval != origval:
                 errors[attr] = session._("can't change the %s attribute") % \
                                display_name(session, attr)
-            entity[attr] = newval
     if errors:
         raise ValidationError(entity.eid, errors)
 
@@ -862,7 +860,7 @@
 
     def __call__(self):
         entity = self.entity
-        if entity.get('final'):
+        if entity.cw_edited.get('final'):
             return
         CWETypeAddOp(self._cw, entity=entity)
 
@@ -876,8 +874,8 @@
         entity = self.entity
         check_valid_changes(self._cw, entity, ro_attrs=('final',))
         # don't use getattr(entity, attr), we would get the modified value if any
-        if 'name' in entity.edited_attributes:
-            oldname, newname = hook.entity_oldnewvalue(entity, 'name')
+        if 'name' in entity.cw_edited:
+            oldname, newname = entity.cw_edited.oldnewvalue('name')
             if newname.lower() != oldname.lower():
                 CWETypeRenameOp(self._cw, oldname=oldname, newname=newname)
 
@@ -920,8 +918,8 @@
         entity = self.entity
         rtypedef = ybo.RelationType(name=entity.name,
                                     description=entity.description,
-                                    inlined=entity.get('inlined', False),
-                                    symmetric=entity.get('symmetric', False),
+                                    inlined=entity.cw_edited.get('inlined', False),
+                                    symmetric=entity.cw_edited.get('symmetric', False),
                                     eid=entity.eid)
         MemSchemaCWRTypeAdd(self._cw, rtypedef=rtypedef)
 
@@ -936,10 +934,10 @@
         check_valid_changes(self._cw, entity)
         newvalues = {}
         for prop in ('symmetric', 'inlined', 'fulltext_container'):
-            if prop in entity.edited_attributes:
-                old, new = hook.entity_oldnewvalue(entity, prop)
+            if prop in entity.cw_edited:
+                old, new = entity.cw_edited.oldnewvalue(prop)
                 if old != new:
-                    newvalues[prop] = entity[prop]
+                    newvalues[prop] = new
         if newvalues:
             rschema = self._cw.vreg.schema.rschema(entity.name)
             CWRTypeUpdateOp(self._cw, rschema=rschema, entity=entity,
@@ -1024,8 +1022,8 @@
                 attr = 'ordernum'
             else:
                 attr = prop
-            if attr in entity.edited_attributes:
-                old, new = hook.entity_oldnewvalue(entity, attr)
+            if attr in entity.cw_edited:
+                old, new = entity.cw_edited.oldnewvalue(attr)
                 if old != new:
                     newvalues[prop] = new
         if newvalues:
--- a/hooks/syncsession.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/hooks/syncsession.py	Wed Aug 25 10:29:18 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Core hooks: synchronize living session on persistent data changes
+"""Core hooks: synchronize living session on persistent data changes"""
 
-"""
 __docformat__ = "restructuredtext en"
 
 from yams.schema import role_name
@@ -56,26 +55,25 @@
 
 class _DeleteGroupOp(_GroupOperation):
     """synchronize user when a in_group relation has been deleted"""
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been commited"""
         groups = self.cnxuser.groups
         try:
             groups.remove(self.group)
         except KeyError:
             self.error('user %s not in group %s',  self.cnxuser, self.group)
-            return
 
 
 class _AddGroupOp(_GroupOperation):
     """synchronize user when a in_group relation has been added"""
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been commited"""
         groups = self.cnxuser.groups
         if self.group in groups:
             self.warning('user %s already in group %s', self.cnxuser,
                          self.group)
-            return
-        groups.add(self.group)
+        else:
+            groups.add(self.group)
 
 
 class SyncInGroupHook(SyncSessionHook):
@@ -98,7 +96,7 @@
         self.cnxid = cnxid
         hook.Operation.__init__(self, session)
 
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been commited"""
         try:
             self.session.repo.close(self.cnxid)
@@ -123,7 +121,7 @@
 class _DelCWPropertyOp(hook.Operation):
     """a user's custom properties has been deleted"""
 
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been commited"""
         try:
             del self.cwpropdict[self.key]
@@ -134,7 +132,7 @@
 class _ChangeCWPropertyOp(hook.Operation):
     """a user's custom properties has been added/changed"""
 
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been commited"""
         self.cwpropdict[self.key] = self.value
 
@@ -142,7 +140,7 @@
 class _AddCWPropertyOp(hook.Operation):
     """a user's custom properties has been added/changed"""
 
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been commited"""
         cwprop = self.cwprop
         if not cwprop.for_user:
@@ -180,8 +178,8 @@
 
     def __call__(self):
         entity = self.entity
-        if not ('pkey' in entity.edited_attributes or
-                'value' in entity.edited_attributes):
+        if not ('pkey' in entity.cw_edited or
+                'value' in entity.cw_edited):
             return
         key, value = entity.pkey, entity.value
         session = self._cw
--- a/hooks/test/unittest_hooks.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/hooks/test/unittest_hooks.py	Wed Aug 25 10:29:18 2010 +0200
@@ -143,7 +143,7 @@
         entity.set_attributes(name=u'wf2')
         self.assertEquals(entity.description, u'yo')
         entity.set_attributes(description=u'R&D<p>yo')
-        entity.pop('description')
+        entity.cw_attr_cache.pop('description')
         self.assertEquals(entity.description, u'R&amp;D<p>yo</p>')
 
 
--- a/hooks/workflow.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/hooks/workflow.py	Wed Aug 25 10:29:18 2010 +0200
@@ -135,7 +135,7 @@
             qname = role_name('to_state', 'subject')
             msg = session._("state doesn't belong to entity's current workflow")
             raise ValidationError(self.trinfo.eid, {'to_state': msg})
-        tostate = wftr.get_exit_point(forentity, trinfo['to_state'])
+        tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state'])
         if tostate is not None:
             # reached an exit point
             msg = session._('exiting from subworkflow %s')
@@ -185,7 +185,7 @@
         entity = self.entity
         # first retreive entity to which the state change apply
         try:
-            foreid = entity['wf_info_for']
+            foreid = entity.cw_attr_cache['wf_info_for']
         except KeyError:
             qname = role_name('wf_info_for', 'subject')
             msg = session._('mandatory relation')
@@ -213,7 +213,7 @@
                      or not session.write_security)
         # no investigate the requested state change...
         try:
-            treid = entity['by_transition']
+            treid = entity.cw_attr_cache['by_transition']
         except KeyError:
             # no transition set, check user is a manager and destination state
             # is specified (and valid)
@@ -221,7 +221,7 @@
                 qname = role_name('by_transition', 'subject')
                 msg = session._('mandatory relation')
                 raise ValidationError(entity.eid, {qname: msg})
-            deststateeid = entity.get('to_state')
+            deststateeid = entity.cw_attr_cache.get('to_state')
             if not deststateeid:
                 qname = role_name('by_transition', 'subject')
                 msg = session._('mandatory relation')
@@ -247,8 +247,8 @@
                 if not tr.may_be_fired(foreid):
                     msg = session._("transition may not be fired")
                     raise ValidationError(entity.eid, {qname: msg})
-            if entity.get('to_state'):
-                deststateeid = entity['to_state']
+            deststateeid = entity.cw_attr_cache.get('to_state')
+            if deststateeid is not None:
                 if not cowpowers and deststateeid != tr.destination(forentity).eid:
                     qname = role_name('by_transition', 'subject')
                     msg = session._("transition isn't allowed")
@@ -262,8 +262,8 @@
             else:
                 deststateeid = tr.destination(forentity).eid
         # everything is ok, add missing information on the trinfo entity
-        entity['from_state'] = fromstate.eid
-        entity['to_state'] = deststateeid
+        entity.cw_edited['from_state'] = fromstate.eid
+        entity.cw_edited['to_state'] = deststateeid
         nocheck = session.transaction_data.setdefault('skip-security', set())
         nocheck.add((entity.eid, 'from_state', fromstate.eid))
         nocheck.add((entity.eid, 'to_state', deststateeid))
@@ -278,11 +278,12 @@
 
     def __call__(self):
         trinfo = self.entity
-        _change_state(self._cw, trinfo['wf_info_for'],
-                      trinfo['from_state'], trinfo['to_state'])
-        forentity = self._cw.entity_from_eid(trinfo['wf_info_for'])
+        rcache = trinfo.cw_attr_cache
+        _change_state(self._cw, rcache['wf_info_for'], rcache['from_state'],
+                      rcache['to_state'])
+        forentity = self._cw.entity_from_eid(rcache['wf_info_for'])
         iworkflowable = forentity.cw_adapt_to('IWorkflowable')
-        assert iworkflowable.current_state.eid == trinfo['to_state']
+        assert iworkflowable.current_state.eid == rcache['to_state']
         if iworkflowable.main_workflow.eid != iworkflowable.current_workflow.eid:
             _SubWorkflowExitOp(self._cw, forentity=forentity, trinfo=trinfo)
 
--- a/rset.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/rset.py	Wed Aug 25 10:29:18 2010 +0200
@@ -484,7 +484,7 @@
                         if attr == 'eid':
                             entity.eid = rowvalues[outerselidx]
                         else:
-                            entity[attr] = rowvalues[outerselidx]
+                            entity.cw_attr_cache[attr] = rowvalues[outerselidx]
                         continue
                 else:
                     rschema = eschema.objrels[attr]
--- a/server/hook.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/server/hook.py	Wed Aug 25 10:29:18 2010 +0200
@@ -61,6 +61,7 @@
 from logilab.common.logging_ext import set_log_methods
 
 from cubicweb import RegistryNotFound
+from cubicweb.vregistry import classid
 from cubicweb.cwvreg import CWRegistry, VRegistry
 from cubicweb.selectors import (objectify_selector, lltrace, ExpectedValueSelector,
                                 is_instance)
@@ -83,7 +84,7 @@
         for appobjects in self.values():
             for cls in appobjects:
                 if not cls.enabled:
-                    warn('[3.6] %s: enabled is deprecated' % cls)
+                    warn('[3.6] %s: enabled is deprecated' % classid(cls))
                     self.unregister(cls)
 
     def register(self, obj, **kwargs):
@@ -119,21 +120,9 @@
 for event in ALL_HOOKS:
     VRegistry.REGISTRY_FACTORY['%s_hooks' % event] = HooksRegistry
 
-_MARKER = object()
+@deprecated('[3.10] use entity.cw_edited.oldnewvalue(attr)')
 def entity_oldnewvalue(entity, attr):
-    """returns the couple (old attr value, new attr value)
-
-    NOTE: will only work in a before_update_entity hook
-    """
-    # get new value and remove from local dict to force a db query to
-    # fetch old value
-    newvalue = entity.pop(attr, _MARKER)
-    oldvalue = getattr(entity, attr)
-    if newvalue is not _MARKER:
-        entity[attr] = newvalue
-    else:
-        newvalue = oldvalue
-    return oldvalue, newvalue
+    return entity.cw_edited.oldnewvalue(attr)
 
 
 # some hook specific selectors #################################################
@@ -231,16 +220,16 @@
 
     @classproperty
     def __regid__(cls):
-        warn('[3.6] %s.%s: please specify an id for your hook'
-             % (cls.__module__, cls.__name__), DeprecationWarning)
+        warn('[3.6] %s: please specify an id for your hook' % classid(cls),
+             DeprecationWarning)
         return str(id(cls))
 
     @classmethod
     def __registered__(cls, reg):
         super(Hook, cls).__registered__(reg)
         if getattr(cls, 'accepts', None):
-            warn('[3.6] %s.%s: accepts is deprecated, define proper __select__'
-                 % (cls.__module__, cls.__name__), DeprecationWarning)
+            warn('[3.6] %s: accepts is deprecated, define proper __select__'
+                 % classid(cls), DeprecationWarning)
             rtypes = []
             for ertype in cls.accepts:
                 if ertype.islower():
@@ -261,9 +250,8 @@
 
     def __call__(self):
         if hasattr(self, 'call'):
-            cls = self.__class__
-            warn('[3.6] %s.%s: call is deprecated, implement __call__'
-                 % (cls.__module__, cls.__name__), DeprecationWarning)
+            warn('[3.6] %s: call is deprecated, implement __call__'
+                 % classid(self.__class__), DeprecationWarning)
             if self.event.endswith('_relation'):
                 self.call(self._cw, self.eidfrom, self.rtype, self.eidto)
             elif 'delete' in self.event:
@@ -428,6 +416,10 @@
 
     def handle_event(self, event):
         """delegate event handling to the opertaion"""
+        if event == 'postcommit_event' and hasattr(self, 'commit_event'):
+            warn('[3.10] %s: commit_event method has been replaced by postcommit_event'
+                 % classid(self.__class__), DeprecationWarning)
+            self.commit_event()
         getattr(self, event)()
 
     def precommit_event(self):
@@ -440,16 +432,6 @@
         been all considered if it's this operation which failed
         """
 
-    def commit_event(self):
-        """the observed connections pool is commiting"""
-
-    def revertcommit_event(self):
-        """an error went when commiting this operation or a later one
-
-        should revert commit's changes but take care, they may have not
-        been all considered if it's this operation which failed
-        """
-
     def rollback_event(self):
         """the observed connections pool has been rollbacked
 
@@ -524,8 +506,12 @@
         return -(i + 1)
 
 
-class SingleOperation(Operation):
-    """special operation which should be called once"""
+
+class SingleLastOperation(Operation):
+    """special operation which should be called once and after all other
+    operations
+    """
+
     def register(self, session):
         """override register to handle cases where this operation has already
         been added
@@ -546,11 +532,6 @@
                 return -(i+1)
         return None
 
-
-class SingleLastOperation(SingleOperation):
-    """special operation which should be called once and after all other
-    operations
-    """
     def insert_index(self):
         return None
 
@@ -572,7 +553,7 @@
         if previous:
             self.to_send = previous.to_send + self.to_send
 
-    def commit_event(self):
+    def postcommit_event(self):
         self.session.repo.threaded_task(self.sendmails)
 
     def sendmails(self):
@@ -612,7 +593,7 @@
     type/source cache eids of entities deleted in that transaction.
     """
 
-    def commit_event(self):
+    def postcommit_event(self):
         """the observed connections pool has been rollbacked,
         remove inserted eid from repository type/source cache
         """
--- a/server/pool.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/server/pool.py	Wed Aug 25 10:29:18 2010 +0200
@@ -144,11 +144,9 @@
         self._cursors.pop(source.uri, None)
 
 
-from cubicweb.server.hook import (Operation, LateOperation, SingleOperation,
-                                  SingleLastOperation)
+from cubicweb.server.hook import Operation, LateOperation, SingleLastOperation
 from logilab.common.deprecation import class_moved, class_renamed
 Operation = class_moved(Operation)
 PreCommitOperation = class_renamed('PreCommitOperation', Operation)
 LateOperation = class_moved(LateOperation)
-SingleOperation = class_moved(SingleOperation)
 SingleLastOperation = class_moved(SingleLastOperation)
--- a/server/querier.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/server/querier.py	Wed Aug 25 10:29:18 2010 +0200
@@ -450,7 +450,7 @@
         # save originaly selected variable, we may modify this
         # dictionary for substitution (query parameters)
         self.selected = rqlst.selection
-        # list of new or updated entities definition (utils.Entity)
+        # list of rows of entities definition (ssplanner.EditedEntity)
         self.e_defs = [[]]
         # list of new relation definition (3-uple (from_eid, r_type, to_eid)
         self.r_defs = set()
@@ -461,7 +461,6 @@
 
     def add_entity_def(self, edef):
         """add an entity definition to build"""
-        edef.querier_pending_relations = {}
         self.e_defs[-1].append(edef)
 
     def add_relation_def(self, rdef):
@@ -493,8 +492,9 @@
             self.e_defs[i][colidx] = edefs[0]
             samplerow = self.e_defs[i]
             for edef_ in edefs[1:]:
-                row = samplerow[:]
-                row[colidx] = edef_
+                row = [ed.clone() for i, ed in enumerate(samplerow)
+                       if i != colidx]
+                row.insert(colidx, edef_)
                 self.e_defs.append(row)
         # now, see if this entity def is referenced as subject in some relation
         # definition
@@ -560,15 +560,16 @@
             if isinstance(subj, basestring):
                 subj = typed_eid(subj)
             elif not isinstance(subj, (int, long)):
-                subj = subj.eid
+                subj = subj.entity.eid
             if isinstance(obj, basestring):
                 obj = typed_eid(obj)
             elif not isinstance(obj, (int, long)):
-                obj = obj.eid
+                obj = obj.entity.eid
             if repo.schema.rschema(rtype).inlined:
                 entity = session.entity_from_eid(subj)
-                entity[rtype] = obj
-                repo.glob_update_entity(session, entity, set((rtype,)))
+                edited = EditedEntity(entity)
+                edited.edited_attribute(rtype, obj)
+                repo.glob_update_entity(session, edited)
             else:
                 repo.glob_add_relation(session, subj, rtype, obj)
 
--- a/server/repository.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/server/repository.py	Wed Aug 25 10:29:18 2010 +0200
@@ -55,6 +55,7 @@
 from cubicweb.server import utils, hook, pool, querier, sources
 from cubicweb.server.session import Session, InternalSession, InternalManager, \
      security_enabled
+from cubicweb.server.ssplanner import EditedEntity
 
 
 def del_existing_rel_if_needed(session, eidfrom, rtype, eidto):
@@ -536,8 +537,7 @@
                 password = password.encode('UTF8')
             kwargs['login'] = login
             kwargs['upassword'] = password
-            user.update(kwargs)
-            self.glob_add_entity(session, user)
+            self.glob_add_entity(session, EditedEntity(user, **kwargs))
             session.execute('SET X in_group G WHERE X eid %(x)s, G name "users"',
                             {'x': user.eid})
             if email or '@' in login:
@@ -940,7 +940,6 @@
             self._extid_cache[cachekey] = eid
             self._type_source_cache[eid] = (etype, source.uri, extid)
             entity = source.before_entity_insertion(session, extid, etype, eid)
-            entity.edited_attributes = set(entity.cw_attr_cache)
             if source.should_call_hooks:
                 self.hm.call_hooks('before_add_entity', session, entity=entity)
             # XXX call add_info with complete=False ?
@@ -1043,15 +1042,16 @@
         self._type_source_cache[entity.eid] = (entity.__regid__, suri, extid)
         return extid
 
-    def glob_add_entity(self, session, entity):
+    def glob_add_entity(self, session, edited):
         """add an entity to the repository
 
         the entity eid should originaly be None and a unique eid is assigned to
         the entity instance
         """
-        # init edited_attributes before calling before_add_entity hooks
+        entity = edited.entity
         entity._cw_is_saved = False # entity has an eid but is not yet saved
-        entity.edited_attributes = set(entity.cw_attr_cache) # XXX cw_edited_attributes
+        # init edited_attributes before calling before_add_entity hooks
+        entity.cw_edited = edited
         eschema = entity.e_schema
         source = self.locate_etype_source(entity.__regid__)
         # allocate an eid to the entity before calling hooks
@@ -1063,17 +1063,15 @@
         relations = []
         if source.should_call_hooks:
             self.hm.call_hooks('before_add_entity', session, entity=entity)
-        # XXX use entity.keys here since edited_attributes is not updated for
-        # inline relations XXX not true, right? (see edited_attributes
-        # affectation above)
-        for attr in entity.cw_attr_cache.iterkeys():
+        for attr in edited.iterkeys():
             rschema = eschema.subjrels[attr]
             if not rschema.final: # inlined relation
-                relations.append((attr, entity[attr]))
-        entity._cw_set_defaults()
+                relations.append((attr, edited[attr]))
+        edited.set_defaults()
         if session.is_hook_category_activated('integrity'):
-            entity._cw_check(creation=True)
+            edited.check(creation=True)
         source.add_entity(session, entity)
+        edited.saved = True
         self.add_info(session, entity, source, extid, complete=False)
         entity._cw_is_saved = True # entity has an eid and is saved
         # prefill entity relation caches
@@ -1082,7 +1080,7 @@
             if rtype in schema.VIRTUAL_RTYPES:
                 continue
             if rschema.final:
-                entity.setdefault(rtype, None)
+                entity.cw_attr_cache.setdefault(rtype, None)
             else:
                 entity.cw_set_relation_cache(rtype, 'subject',
                                              session.empty_rset())
@@ -1105,23 +1103,24 @@
                                     eidfrom=entity.eid, rtype=attr, eidto=value)
         return entity.eid
 
-    def glob_update_entity(self, session, entity, edited_attributes):
+    def glob_update_entity(self, session, edited):
         """replace an entity in the repository
         the type and the eid of an entity must not be changed
         """
+        entity = edited.entity
         if server.DEBUG & server.DBG_REPO:
             print 'UPDATE entity', entity.__regid__, entity.eid, \
-                  entity.cw_attr_cache, edited_attributes
+                  entity.cw_attr_cache, edited
         hm = self.hm
         eschema = entity.e_schema
         session.set_entity_cache(entity)
-        orig_edited_attributes = getattr(entity, 'edited_attributes', None)
-        entity.edited_attributes = edited_attributes
+        orig_edited = getattr(entity, 'cw_edited', None)
+        entity.cw_edited = edited
         try:
             only_inline_rels, need_fti_update = True, False
             relations = []
             source = self.source_from_eid(entity.eid, session)
-            for attr in list(edited_attributes):
+            for attr in list(edited):
                 if attr == 'eid':
                     continue
                 rschema = eschema.subjrels[attr]
@@ -1134,13 +1133,13 @@
                     previous_value = entity.related(attr) or None
                     if previous_value is not None:
                         previous_value = previous_value[0][0] # got a result set
-                        if previous_value == entity[attr]:
+                        if previous_value == entity.cw_attr_cache[attr]:
                             previous_value = None
                         elif source.should_call_hooks:
                             hm.call_hooks('before_delete_relation', session,
                                           eidfrom=entity.eid, rtype=attr,
                                           eidto=previous_value)
-                    relations.append((attr, entity[attr], previous_value))
+                    relations.append((attr, edited[attr], previous_value))
             if source.should_call_hooks:
                 # call hooks for inlined relations
                 for attr, value, _ in relations:
@@ -1149,8 +1148,9 @@
                 if not only_inline_rels:
                     hm.call_hooks('before_update_entity', session, entity=entity)
             if session.is_hook_category_activated('integrity'):
-                entity._cw_check()
+                edited.check()
             source.update_entity(session, entity)
+            edited.saved = True
             self.system_source.update_info(session, entity, need_fti_update)
             if source.should_call_hooks:
                 if not only_inline_rels:
@@ -1172,8 +1172,8 @@
                     hm.call_hooks('after_add_relation', session,
                                   eidfrom=entity.eid, rtype=attr, eidto=value)
         finally:
-            if orig_edited_attributes is not None:
-                entity.edited_attributes = orig_edited_attributes
+            if orig_edited is not None:
+                entity.cw_edited = orig_edited
 
     def glob_delete_entity(self, session, eid):
         """delete an entity and all related entities from the repository"""
--- a/server/session.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/server/session.py	Wed Aug 25 10:29:18 2010 +0200
@@ -739,51 +739,50 @@
         try:
             # by default, operations are executed with security turned off
             with security_enabled(self, False, False):
-                for trstate in ('precommit', 'commit'):
-                    processed = []
-                    self.commit_state = trstate
-                    try:
-                        while self.pending_operations:
-                            operation = self.pending_operations.pop(0)
-                            operation.processed = trstate
-                            processed.append(operation)
-                            operation.handle_event('%s_event' % trstate)
-                        self.pending_operations[:] = processed
-                        self.debug('%s session %s done', trstate, self.id)
-                    except:
-                        # if error on [pre]commit:
-                        #
-                        # * set .failed = True on the operation causing the failure
-                        # * call revert<event>_event on processed operations
-                        # * call rollback_event on *all* operations
-                        #
-                        # that seems more natural than not calling rollback_event
-                        # for processed operations, and allow generic rollback
-                        # instead of having to implements rollback, revertprecommit
-                        # and revertcommit, that will be enough in mont case.
-                        operation.failed = True
-                        for operation in reversed(processed):
-                            try:
-                                operation.handle_event('revert%s_event' % trstate)
-                            except:
-                                self.critical('error while reverting %sing', trstate,
-                                              exc_info=True)
-                        # XXX use slice notation since self.pending_operations is a
-                        # read-only property.
-                        self.pending_operations[:] = processed + self.pending_operations
-                        self.rollback(reset_pool)
-                        raise
+                processed = []
+                self.commit_state = 'precommit'
+                try:
+                    while self.pending_operations:
+                        operation = self.pending_operations.pop(0)
+                        operation.processed = 'precommit'
+                        processed.append(operation)
+                        operation.handle_event('precommit_event')
+                    self.pending_operations[:] = processed
+                    self.debug('precommit session %s done', self.id)
+                except:
+                    # if error on [pre]commit:
+                    #
+                    # * set .failed = True on the operation causing the failure
+                    # * call revert<event>_event on processed operations
+                    # * call rollback_event on *all* operations
+                    #
+                    # that seems more natural than not calling rollback_event
+                    # for processed operations, and allow generic rollback
+                    # instead of having to implements rollback, revertprecommit
+                    # and revertcommit, that will be enough in mont case.
+                    operation.failed = True
+                    for operation in reversed(processed):
+                        try:
+                            operation.handle_event('revertprecommit_event')
+                        except:
+                            self.critical('error while reverting precommit',
+                                          exc_info=True)
+                    # XXX use slice notation since self.pending_operations is a
+                    # read-only property.
+                    self.pending_operations[:] = processed + self.pending_operations
+                    self.rollback(reset_pool)
+                    raise
                 self.pool.commit()
-                self.commit_state = trstate = 'postcommit'
+                self.commit_state = 'postcommit'
                 while self.pending_operations:
                     operation = self.pending_operations.pop(0)
-                    operation.processed = trstate
+                    operation.processed = 'postcommit'
                     try:
-                        operation.handle_event('%s_event' % trstate)
+                        operation.handle_event('postcommit_event')
                     except:
-                        self.critical('error while %sing', trstate,
+                        self.critical('error while postcommit',
                                       exc_info=sys.exc_info())
-                self.debug('%s session %s done', trstate, self.id)
+                self.debug('postcommit session %s done', self.id)
                 return self.transaction_uuid(set=False)
         finally:
             self._touch()
--- a/server/sources/__init__.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/server/sources/__init__.py	Wed Aug 25 10:29:18 2010 +0200
@@ -26,6 +26,7 @@
 from cubicweb import set_log_methods, server
 from cubicweb.schema import VIRTUAL_RTYPES
 from cubicweb.server.sqlutils import SQL_PREFIX
+from cubicweb.server.ssplanner import EditedEntity
 
 
 def dbg_st_search(uri, union, varmap, args, cachekey=None, prefix='rql for'):
@@ -343,6 +344,7 @@
         """
         entity = self.repo.vreg['etypes'].etype_class(etype)(session)
         entity.eid = eid
+        entity.cw_edited = EditedEntity(entity)
         return entity
 
     def after_entity_insertion(self, session, lid, entity):
--- a/server/sources/ldapuser.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/server/sources/ldapuser.py	Wed Aug 25 10:29:18 2010 +0200
@@ -574,7 +574,7 @@
         entity = super(LDAPUserSource, self).before_entity_insertion(session, lid, etype, eid)
         res = self._search(session, lid, BASE)[0]
         for attr in entity.e_schema.indexable_attributes():
-            entity[attr] = res[self.user_rev_attrs[attr]]
+            entity.cw_edited[attr] = res[self.user_rev_attrs[attr]]
         return entity
 
     def after_entity_insertion(self, session, dn, entity):
--- a/server/sources/native.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/server/sources/native.py	Wed Aug 25 10:29:18 2010 +0200
@@ -54,6 +54,7 @@
 from cubicweb.server.rqlannotation import set_qdata
 from cubicweb.server.hook import CleanupDeletedEidsCacheOp
 from cubicweb.server.session import hooks_control, security_enabled
+from cubicweb.server.ssplanner import EditedEntity
 from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
 from cubicweb.server.sources.rql2sql import SQLGenerator
 
@@ -546,21 +547,20 @@
         etype = entity.__regid__
         for attr, storage in self._storages.get(etype, {}).items():
             try:
-                edited = entity.edited_attributes
+                edited = entity.cw_edited
             except AttributeError:
                 assert event == 'deleted'
                 getattr(storage, 'entity_deleted')(entity, attr)
             else:
                 if attr in edited:
                     handler = getattr(storage, 'entity_%s' % event)
-                    real_value = handler(entity, attr)
-                    restore_values[attr] = real_value
+                    restore_values[attr] = handler(entity, attr)
         try:
             yield # 2/ execute the source's instructions
         finally:
             # 3/ restore original values
             for attr, value in restore_values.items():
-                entity[attr] = value
+                entity.cw_edited.edited_attribute(attr, value)
 
     def add_entity(self, session, entity):
         """add a new entity to the source"""
@@ -1108,6 +1108,7 @@
             err("can't restore entity %s of type %s, type no more supported"
                 % (eid, etype))
             return errors
+        entity.cw_edited = edited = EditedEntity(entity)
         # check for schema changes, entities linked through inlined relation
         # still exists, rewrap binary values
         eschema = entity.e_schema
@@ -1124,15 +1125,14 @@
                 assert value is None
             elif eschema.destination(rtype) in ('Bytes', 'Password'):
                 action.changes[column] = self._binary(value)
-                entity[rtype] = Binary(value)
+                edited[rtype] = Binary(value)
             elif isinstance(value, str):
-                entity[rtype] = unicode(value, session.encoding, 'replace')
+                edited[rtype] = unicode(value, session.encoding, 'replace')
             else:
-                entity[rtype] = value
+                edited[rtype] = value
         entity.eid = eid
         session.repo.init_entity_caches(session, entity, self)
-        entity.edited_attributes = set(entity)
-        entity._cw_check()
+        edited.check()
         self.repo.hm.call_hooks('before_add_entity', session, entity=entity)
         # restore the entity
         action.changes['cw_eid'] = eid
--- a/server/sources/storages.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/server/sources/storages.py	Wed Aug 25 10:29:18 2010 +0200
@@ -23,6 +23,8 @@
 
 from cubicweb import Binary, ValidationError
 from cubicweb.server import hook
+from cubicweb.server.ssplanner import EditedEntity
+
 
 def set_attribute_storage(repo, etype, attr, storage):
     repo.system_source.set_storage(etype, attr, storage)
@@ -30,6 +32,7 @@
 def unset_attribute_storage(repo, etype, attr):
     repo.system_source.unset_storage(etype, attr)
 
+
 class Storage(object):
     """abstract storage
 
@@ -114,12 +117,12 @@
     def entity_added(self, entity, attr):
         """an entity using this storage for attr has been added"""
         if entity._cw.transaction_data.get('fs_importing'):
-            binary = Binary(file(entity[attr].getvalue(), 'rb').read())
+            binary = Binary(file(entity.cw_edited[attr].getvalue(), 'rb').read())
         else:
-            binary = entity.pop(attr)
+            binary = entity.cw_edited.pop(attr)
             fpath = self.new_fs_path(entity, attr)
             # bytes storage used to store file's path
-            entity[attr] = Binary(fpath)
+            entity.cw_edited.edited_attribute(attr, Binary(fpath))
             file(fpath, 'wb').write(binary.getvalue())
             hook.set_operation(entity._cw, 'bfss_added', fpath, AddFileOp)
         return binary
@@ -132,7 +135,7 @@
             # If we are importing from the filesystem, the file already exists.
             # We do not need to create it but we need to fetch the content of
             # the file as the actual content of the attribute
-            fpath = entity[attr].getvalue()
+            fpath = entity.cw_edited[attr].getvalue()
             binary = Binary(file(fpath, 'rb').read())
         else:
             # We must store the content of the attributes
@@ -144,7 +147,7 @@
             # went ok.
             #
             # fetch the current attribute value in memory
-            binary = entity.pop(attr)
+            binary = entity.cw_edited.pop(attr)
             # Get filename for it
             fpath = self.new_fs_path(entity, attr)
             assert not osp.exists(fpath)
@@ -155,7 +158,7 @@
             hook.set_operation(entity._cw, 'bfss_added', fpath, AddFileOp)
         if oldpath != fpath:
             # register the new location for the file.
-            entity[attr] = Binary(fpath)
+            entity.cw_edited.edited_attribute(attr, Binary(fpath))
             # Mark the old file as useless so the file will be removed at
             # commit.
             hook.set_operation(entity._cw, 'bfss_deleted', oldpath,
@@ -197,7 +200,7 @@
 
     def migrate_entity(self, entity, attribute):
         """migrate an entity attribute to the storage"""
-        entity.edited_attributes = set()
+        entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache)
         self.entity_added(entity, attribute)
         session = entity._cw
         source = session.repo.system_source
@@ -205,6 +208,7 @@
         sql = source.sqlgen.update('cw_' + entity.__regid__, attrs,
                                    ['cw_eid'])
         source.doexec(session, sql, attrs)
+        entity.cw_edited = None
 
 
 class AddFileOp(hook.Operation):
@@ -216,7 +220,7 @@
                 self.error('cant remove %s: %s' % (filepath, ex))
 
 class DeleteFileOp(hook.Operation):
-    def commit_event(self):
+    def postcommit_event(self):
         for filepath in self.session.transaction_data.pop('bfss_deleted'):
             try:
                 unlink(filepath)
--- a/server/sqlutils.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/server/sqlutils.py	Wed Aug 25 10:29:18 2010 +0200
@@ -260,11 +260,10 @@
         """
         attrs = {}
         eschema = entity.e_schema
-        for attr in entity.edited_attributes:
-            value = entity[attr]
+        for attr, value in entity.cw_edited.iteritems():
             rschema = eschema.subjrels[attr]
             if rschema.final:
-                atype = str(entity.e_schema.destination(attr))
+                atype = str(eschema.destination(attr))
                 if atype == 'Boolean':
                     value = self.dbhelper.boolean_value(value)
                 elif atype == 'Password':
--- a/server/ssplanner.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/server/ssplanner.py	Wed Aug 25 10:29:18 2010 +0200
@@ -21,6 +21,8 @@
 
 __docformat__ = "restructuredtext en"
 
+from copy import copy
+
 from rql.stmts import Union, Select
 from rql.nodes import Constant, Relation
 
@@ -55,11 +57,11 @@
             if isinstance(rhs, Constant) and not rhs.uid:
                 # add constant values to entity def
                 value = rhs.eval(plan.args)
-                eschema = edef.e_schema
+                eschema = edef.entity.e_schema
                 attrtype = eschema.subjrels[rtype].objects(eschema)[0]
                 if attrtype == 'Password' and isinstance(value, unicode):
                     value = value.encode('UTF8')
-                edef[rtype] = value
+                edef.edited_attribute(rtype, value)
             elif to_build.has_key(str(rhs)):
                 # create a relation between two newly created variables
                 plan.add_relation_def((edef, rtype, to_build[rhs.name]))
@@ -126,6 +128,132 @@
     return select
 
 
+_MARKER = object()
+
+class dict_protocol_catcher(object):
+    def __init__(self, entity):
+        self.__entity = entity
+    def __getitem__(self, attr):
+        return self.__entity.cw_edited[attr]
+    def __setitem__(self, attr, value):
+        self.__entity.cw_edited[attr] = value
+    def __getattr__(self, attr):
+        return getattr(self.__entity, attr)
+
+
+class EditedEntity(dict):
+    """encapsulate entities attributes being written by an RQL query"""
+    def __init__(self, entity, **kwargs):
+        dict.__init__(self, **kwargs)
+        self.entity = entity
+        self.skip_security = set()
+        self.querier_pending_relations = {}
+        self.saved = False
+
+    def __hash__(self):
+        # dict|set keyable
+        return hash(id(self))
+
+    def __cmp__(self, other):
+        # we don't want comparison by value inherited from dict
+        return cmp(id(self), id(other))
+
+    def __setitem__(self, attr, value):
+        assert attr != 'eid'
+        # don't add attribute into skip_security if already in edited
+        # attributes, else we may accidentaly skip a desired security check
+        if attr not in self:
+            self.skip_security.add(attr)
+        self.edited_attribute(attr, value)
+
+    def __delitem__(self, attr):
+        assert not self.saved, 'too late to modify edited attributes'
+        super(EditedEntity, self).__delitem__(attr)
+        self.entity.cw_attr_cache.pop(attr, None)
+
+    def pop(self, attr, *args):
+        # don't update skip_security by design (think to storage api)
+        assert not self.saved, 'too late to modify edited attributes'
+        value = super(EditedEntity, self).pop(attr, *args)
+        self.entity.cw_attr_cache.pop(attr, *args)
+        return value
+
+    def setdefault(self, attr, default):
+        assert attr != 'eid'
+        # don't add attribute into skip_security if already in edited
+        # attributes, else we may accidentaly skip a desired security check
+        if attr not in self:
+            self[attr] = default
+        return self[attr]
+
+    def update(self, values, skipsec=True):
+        if skipsec:
+            setitem = self.__setitem__
+        else:
+            setitem = self.edited_attribute
+        for attr, value in values.iteritems():
+            setitem(attr, value)
+
+    def edited_attribute(self, attr, value):
+        """attribute being edited by a rql query: should'nt be added to
+        skip_security
+        """
+        assert not self.saved, 'too late to modify edited attributes'
+        super(EditedEntity, self).__setitem__(attr, value)
+        self.entity.cw_attr_cache[attr] = value
+
+    def oldnewvalue(self, attr):
+        """returns the couple (old attr value, new attr value)
+
+        NOTE: will only work in a before_update_entity hook
+        """
+        assert not self.saved, 'too late to get the old value'
+        # get new value and remove from local dict to force a db query to
+        # fetch old value
+        newvalue = self.entity.cw_attr_cache.pop(attr, _MARKER)
+        oldvalue = getattr(self.entity, attr)
+        if newvalue is not _MARKER:
+            self.entity.cw_attr_cache[attr] = newvalue
+        else:
+            newvalue = oldvalue
+        return oldvalue, newvalue
+
+    def set_defaults(self):
+        """set default values according to the schema"""
+        for attr, value in self.entity.e_schema.defaults():
+            if not attr in self:
+                self[str(attr)] = value
+
+    def check(self, creation=False):
+        """check the entity edition against its schema. Only final relation
+        are checked here, constraint on actual relations are checked in hooks
+        """
+        entity = self.entity
+        if creation:
+            # on creations, we want to check all relations, especially
+            # required attributes
+            relations = [rschema for rschema in entity.e_schema.subject_relations()
+                         if rschema.final and rschema.type != 'eid']
+        else:
+            relations = [entity._cw.vreg.schema.rschema(rtype)
+                         for rtype in self]
+        from yams import ValidationError
+        try:
+            entity.e_schema.check(dict_protocol_catcher(entity),
+                                  creation=creation, _=entity._cw._,
+                                  relations=relations)
+        except ValidationError, ex:
+            ex.entity = self.entity
+            raise
+
+    def clone(self):
+        thecopy = EditedEntity(copy(self.entity))
+        thecopy.entity.cw_attr_cache = copy(self.entity.cw_attr_cache)
+        thecopy.entity._cw_related_cache = {}
+        thecopy.update(self, skipsec=False)
+        return thecopy
+
+
 class SSPlanner(object):
     """SingleSourcePlanner: build execution plan for rql queries
 
@@ -162,7 +290,7 @@
         etype_class = session.vreg['etypes'].etype_class
         for etype, var in rqlst.main_variables:
             # need to do this since entity class is shared w. web client code !
-            to_build[var.name] = etype_class(etype)(session)
+            to_build[var.name] = EditedEntity(etype_class(etype)(session))
             plan.add_entity_def(to_build[var.name])
         # add constant values to entity def, mark variables to be selected
         to_select = _extract_const_attributes(plan, rqlst, to_build)
@@ -177,7 +305,7 @@
         for edef, rdefs in to_select.items():
             # create a select rql st to fetch needed data
             select = Select()
-            eschema = edef.e_schema
+            eschema = edef.entity.e_schema
             for i, (rtype, term, reverse) in enumerate(rdefs):
                 if getattr(term, 'variable', None) in eidconsts:
                     value = eidconsts[term.variable]
@@ -284,10 +412,8 @@
                 rhsinfo = selectedidx[rhskey][:-1] + (None,)
             rschema = getrschema(relation.r_type)
             updatedefs.append( (lhsinfo, rhsinfo, rschema) )
-            if rschema.final or rschema.inlined:
-                attributes.add(relation.r_type)
         # the update step
-        step = UpdateStep(plan, updatedefs, attributes)
+        step = UpdateStep(plan, updatedefs)
         # when necessary add substep to fetch yet unknown values
         select = _build_substep_query(select, rqlst)
         if select is not None:
@@ -476,7 +602,7 @@
             result = [[]]
         for row in result:
             # get a new entity definition for this row
-            edef = base_edef.cw_copy()
+            edef = base_edef.clone()
             # complete this entity def using row values
             index = 0
             for rtype, rorder, value in self.rdefs:
@@ -484,7 +610,7 @@
                     value = row[index]
                     index += 1
                 if rorder == InsertRelationsStep.FINAL:
-                    edef._cw_rql_set_value(rtype, value)
+                    edef.edited_attribute(rtype, value)
                 elif rorder == InsertRelationsStep.RELATION:
                     self.plan.add_relation_def( (edef, rtype, value) )
                     edef.querier_pending_relations[(rtype, 'subject')] = value
@@ -495,6 +621,7 @@
         self.plan.substitute_entity_def(base_edef, edefs)
         return result
 
+
 class InsertStep(Step):
     """step consisting in inserting new entities / relations"""
 
@@ -555,10 +682,9 @@
     definitions and from results fetched in previous step
     """
 
-    def __init__(self, plan, updatedefs, attributes):
+    def __init__(self, plan, updatedefs):
         Step.__init__(self, plan)
         self.updatedefs = updatedefs
-        self.attributes = attributes
 
     def execute(self):
         """execute this step"""
@@ -578,16 +704,17 @@
                 if rschema.final or rschema.inlined:
                     eid = typed_eid(lhsval)
                     try:
-                        edef = edefs[eid]
+                        edited = edefs[eid]
                     except KeyError:
-                        edefs[eid] = edef = session.entity_from_eid(eid)
-                    edef._cw_rql_set_value(str(rschema), rhsval)
+                        edef = session.entity_from_eid(eid)
+                        edefs[eid] = edited = EditedEntity(edef)
+                    edited.edited_attribute(str(rschema), rhsval)
                 else:
                     repo.glob_add_relation(session, lhsval, str(rschema), rhsval)
             result[i] = newrow
         # update entities
-        for eid, edef in edefs.iteritems():
-            repo.glob_update_entity(session, edef, set(self.attributes))
+        for eid, edited in edefs.iteritems():
+            repo.glob_update_entity(session, edited)
         return result
 
 def _handle_relterm(info, row, newrow):
--- a/server/test/unittest_repository.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/server/test/unittest_repository.py	Wed Aug 25 10:29:18 2010 +0200
@@ -423,7 +423,7 @@
                               'EmailAddress', address=u'a@b.fr')
 
     def test_multiple_edit_set_attributes(self):
-        """make sure edited_attributes doesn't get cluttered
+        """make sure cw_edited doesn't get cluttered
         by previous entities on multiple set
         """
         # local hook
@@ -434,9 +434,9 @@
             events = ('before_update_entity',)
             def __call__(self):
                 # invoiced attribute shouldn't be considered "edited" before the hook
-                self._test.failIf('invoiced' in self.entity.edited_attributes,
-                                  'edited_attributes cluttered by previous update')
-                self.entity['invoiced'] = 10
+                self._test.failIf('invoiced' in self.entity.cw_edited,
+                                  'cw_edited cluttered by previous update')
+                self.entity.cw_edited['invoiced'] = 10
         with self.temporary_appobjects(DummyBeforeHook):
             req = self.request()
             req.create_entity('Affaire', ref=u'AFF01')
--- a/sobjects/supervising.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/sobjects/supervising.py	Wed Aug 25 10:29:18 2010 +0200
@@ -15,10 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""some hooks and views to handle supervising of any data changes
+"""some hooks and views to handle supervising of any data changes"""
 
-
-"""
 __docformat__ = "restructuredtext en"
 
 from cubicweb import UnknownEid
@@ -185,6 +183,6 @@
         msg = format_mail(uinfo, recipients, content, view.subject(), config=config)
         self.to_send = [(msg, recipients)]
 
-    def commit_event(self):
+    def postcommit_event(self):
         self._prepare_email()
-        SendMailOp.commit_event(self)
+        SendMailOp.postcommit_event(self)
--- a/test/unittest_entity.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/test/unittest_entity.py	Wed Aug 25 10:29:18 2010 +0200
@@ -322,30 +322,30 @@
                             content_format=u'text/rest')
         self.assertEquals(e.printable_value('content'),
                           '<p>du <a class="reference" href="http://testing.fr/cubicweb/cwgroup/guests">*ReST*</a></p>\n')
-        e['content'] = 'du <em>html</em> <ref rql="CWUser X">users</ref>'
-        e['content_format'] = 'text/html'
+        e.cw_attr_cache['content'] = 'du <em>html</em> <ref rql="CWUser X">users</ref>'
+        e.cw_attr_cache['content_format'] = 'text/html'
         self.assertEquals(e.printable_value('content'),
                           'du <em>html</em> <a href="http://testing.fr/cubicweb/view?rql=CWUser%20X">users</a>')
-        e['content'] = 'du *texte*'
-        e['content_format'] = 'text/plain'
+        e.cw_attr_cache['content'] = 'du *texte*'
+        e.cw_attr_cache['content_format'] = 'text/plain'
         self.assertEquals(e.printable_value('content'),
                           '<p>\ndu *texte*\n</p>')
-        e['title'] = 'zou'
-        e['content'] = '''\
+        e.cw_attr_cache['title'] = 'zou'
+        e.cw_attr_cache['content'] = '''\
 a title
 =======
 du :eid:`1:*ReST*`'''
-        e['content_format'] = 'text/rest'
+        e.cw_attr_cache['content_format'] = 'text/rest'
         self.assertEquals(e.printable_value('content', format='text/plain'),
-                          e['content'])
+                          e.cw_attr_cache['content'])
 
-        e['content'] = u'<b>yo (zou éà ;)</b>'
-        e['content_format'] = 'text/html'
+        e.cw_attr_cache['content'] = u'<b>yo (zou éà ;)</b>'
+        e.cw_attr_cache['content_format'] = 'text/html'
         self.assertEquals(e.printable_value('content', format='text/plain').strip(),
                           u'**yo (zou éà ;)**')
         if HAS_TAL:
-            e['content'] = '<h1 tal:content="self/title">titre</h1>'
-            e['content_format'] = 'text/cubicweb-page-template'
+            e.cw_attr_cache['content'] = '<h1 tal:content="self/title">titre</h1>'
+            e.cw_attr_cache['content_format'] = 'text/cubicweb-page-template'
             self.assertEquals(e.printable_value('content'),
                               '<h1>zou</h1>')
 
@@ -387,30 +387,30 @@
         tidy = lambda x: x.replace('\n', '')
         self.assertEquals(tidy(e.printable_value('content')),
                           '<div>R&amp;D<br/></div>')
-        e['content'] = u'yo !! R&D <div> pas fermé'
+        e.cw_attr_cache['content'] = u'yo !! R&D <div> pas fermé'
         self.assertEquals(tidy(e.printable_value('content')),
                           u'yo !! R&amp;D <div> pas fermé</div>')
-        e['content'] = u'R&D'
+        e.cw_attr_cache['content'] = u'R&D'
         self.assertEquals(tidy(e.printable_value('content')), u'R&amp;D')
-        e['content'] = u'R&D;'
+        e.cw_attr_cache['content'] = u'R&D;'
         self.assertEquals(tidy(e.printable_value('content')), u'R&amp;D;')
-        e['content'] = u'yo !! R&amp;D <div> pas fermé'
+        e.cw_attr_cache['content'] = u'yo !! R&amp;D <div> pas fermé'
         self.assertEquals(tidy(e.printable_value('content')),
                           u'yo !! R&amp;D <div> pas fermé</div>')
-        e['content'] = u'été <div> été'
+        e.cw_attr_cache['content'] = u'été <div> été'
         self.assertEquals(tidy(e.printable_value('content')),
                           u'été <div> été</div>')
-        e['content'] = u'C&apos;est un exemple s&eacute;rieux'
+        e.cw_attr_cache['content'] = u'C&apos;est un exemple s&eacute;rieux'
         self.assertEquals(tidy(e.printable_value('content')),
                           u"C'est un exemple sérieux")
         # make sure valid xhtml is left untouched
-        e['content'] = u'<div>R&amp;D<br/></div>'
-        self.assertEquals(e.printable_value('content'), e['content'])
-        e['content'] = u'<div>été</div>'
-        self.assertEquals(e.printable_value('content'), e['content'])
-        e['content'] = u'été'
-        self.assertEquals(e.printable_value('content'), e['content'])
-        e['content'] = u'hop\r\nhop\nhip\rmomo'
+        e.cw_attr_cache['content'] = u'<div>R&amp;D<br/></div>'
+        self.assertEquals(e.printable_value('content'), e.cw_attr_cache['content'])
+        e.cw_attr_cache['content'] = u'<div>été</div>'
+        self.assertEquals(e.printable_value('content'), e.cw_attr_cache['content'])
+        e.cw_attr_cache['content'] = u'été'
+        self.assertEquals(e.printable_value('content'), e.cw_attr_cache['content'])
+        e.cw_attr_cache['content'] = u'hop\r\nhop\nhip\rmomo'
         self.assertEquals(e.printable_value('content'), u'hop\nhop\nhip\nmomo')
 
     def test_printable_value_bad_html_ms(self):
@@ -419,7 +419,7 @@
         e = req.create_entity('Card', title=u'bad html', content=u'<div>R&D<br>',
                             content_format=u'text/html')
         tidy = lambda x: x.replace('\n', '')
-        e['content'] = u'<div x:foo="bar">ms orifice produces weird html</div>'
+        e.cw_attr_cache['content'] = u'<div x:foo="bar">ms orifice produces weird html</div>'
         self.assertEquals(tidy(e.printable_value('content')),
                           u'<div>ms orifice produces weird html</div>')
         import tidy as tidymod # apt-get install python-tidy
@@ -435,12 +435,12 @@
 
     def test_fulltextindex(self):
         e = self.vreg['etypes'].etype_class('File')(self.request())
-        e['description'] = 'du <em>html</em>'
-        e['description_format'] = 'text/html'
-        e['data'] = Binary('some <em>data</em>')
-        e['data_name'] = 'an html file'
-        e['data_format'] = 'text/html'
-        e['data_encoding'] = 'ascii'
+        e.cw_attr_cache['description'] = 'du <em>html</em>'
+        e.cw_attr_cache['description_format'] = 'text/html'
+        e.cw_attr_cache['data'] = Binary('some <em>data</em>')
+        e.cw_attr_cache['data_name'] = 'an html file'
+        e.cw_attr_cache['data_format'] = 'text/html'
+        e.cw_attr_cache['data_encoding'] = 'ascii'
         e._cw.transaction_data = {} # XXX req should be a session
         self.assertEquals(e.cw_adapt_to('IFTIndexable').get_words(),
                           {'C': [u'du', u'html', 'an', 'html', 'file', u'some', u'data']})
@@ -461,7 +461,7 @@
             'WHERE U login "admin", S1 name "activated", S2 name "deactivated"')[0][0]
         trinfo = self.execute('Any X WHERE X eid %(x)s', {'x': eid}).get_entity(0, 0)
         trinfo.complete()
-        self.failUnless(isinstance(trinfo['creation_date'], datetime))
+        self.failUnless(isinstance(trinfo.cw_attr_cache['creation_date'], datetime))
         self.failUnless(trinfo.cw_relation_cached('from_state', 'subject'))
         self.failUnless(trinfo.cw_relation_cached('to_state', 'subject'))
         self.failUnless(trinfo.cw_relation_cached('wf_info_for', 'subject'))
--- a/test/unittest_rset.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/test/unittest_rset.py	Wed Aug 25 10:29:18 2010 +0200
@@ -157,13 +157,13 @@
         rs.req = self.request()
         rs.vreg = self.vreg
 
-        rs2 = rs.sorted_rset(lambda e:e['login'])
+        rs2 = rs.sorted_rset(lambda e:e.cw_attr_cache['login'])
         self.assertEquals(len(rs2), 3)
         self.assertEquals([login for _, login in rs2], ['adim', 'nico', 'syt'])
         # make sure rs is unchanged
         self.assertEquals([login for _, login in rs], ['adim', 'syt', 'nico'])
 
-        rs2 = rs.sorted_rset(lambda e:e['login'], reverse=True)
+        rs2 = rs.sorted_rset(lambda e:e.cw_attr_cache['login'], reverse=True)
         self.assertEquals(len(rs2), 3)
         self.assertEquals([login for _, login in rs2], ['syt', 'nico', 'adim'])
         # make sure rs is unchanged
@@ -187,7 +187,7 @@
         rs.req = self.request()
         rs.vreg = self.vreg
 
-        rsets = rs.split_rset(lambda e:e['login'])
+        rsets = rs.split_rset(lambda e:e.cw_attr_cache['login'])
         self.assertEquals(len(rsets), 3)
         self.assertEquals([login for _, login,_ in rsets[0]], ['adim', 'adim'])
         self.assertEquals([login for _, login,_ in rsets[1]], ['syt'])
@@ -195,7 +195,7 @@
         # make sure rs is unchanged
         self.assertEquals([login for _, login,_ in rs], ['adim', 'adim', 'syt', 'nico', 'nico'])
 
-        rsets = rs.split_rset(lambda e:e['login'], return_dict=True)
+        rsets = rs.split_rset(lambda e:e.cw_attr_cache['login'], return_dict=True)
         self.assertEquals(len(rsets), 3)
         self.assertEquals([login for _, login,_ in rsets['nico']], ['nico', 'nico'])
         self.assertEquals([login for _, login,_ in rsets['adim']], ['adim', 'adim'])
@@ -230,12 +230,12 @@
         self.request().create_entity('CWUser', login=u'adim', upassword='adim',
                         surname=u'di mascio', firstname=u'adrien')
         e = self.execute('Any X,T WHERE X login "adim", X surname T').get_entity(0, 0)
-        self.assertEquals(e['surname'], 'di mascio')
-        self.assertRaises(KeyError, e.__getitem__, 'firstname')
-        self.assertRaises(KeyError, e.__getitem__, 'creation_date')
+        self.assertEquals(e.cw_attr_cache['surname'], 'di mascio')
+        self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'firstname')
+        self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'creation_date')
         self.assertEquals(pprelcachedict(e._cw_related_cache), [])
         e.complete()
-        self.assertEquals(e['firstname'], 'adrien')
+        self.assertEquals(e.cw_attr_cache['firstname'], 'adrien')
         self.assertEquals(pprelcachedict(e._cw_related_cache), [])
 
     def test_get_entity_advanced(self):
@@ -246,20 +246,20 @@
         e = rset.get_entity(0, 0)
         self.assertEquals(e.cw_row, 0)
         self.assertEquals(e.cw_col, 0)
-        self.assertEquals(e['title'], 'zou')
-        self.assertRaises(KeyError, e.__getitem__, 'path')
+        self.assertEquals(e.cw_attr_cache['title'], 'zou')
+        self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'path')
         self.assertEquals(e.view('text'), 'zou')
         self.assertEquals(pprelcachedict(e._cw_related_cache), [])
 
         e = rset.get_entity(0, 1)
         self.assertEquals(e.cw_row, 0)
         self.assertEquals(e.cw_col, 1)
-        self.assertEquals(e['login'], 'anon')
-        self.assertRaises(KeyError, e.__getitem__, 'firstname')
+        self.assertEquals(e.cw_attr_cache['login'], 'anon')
+        self.assertRaises(KeyError, e.cw_attr_cache.__getitem__, 'firstname')
         self.assertEquals(pprelcachedict(e._cw_related_cache),
                           [])
         e.complete()
-        self.assertEquals(e['firstname'], None)
+        self.assertEquals(e.cw_attr_cache['firstname'], None)
         self.assertEquals(e.view('text'), 'anon')
         self.assertEquals(pprelcachedict(e._cw_related_cache),
                           [])
@@ -282,17 +282,17 @@
         rset = self.execute('Any X,U,S,XT,UL,SN WHERE X created_by U, U in_state S, '
                             'X title XT, S name SN, U login UL, X eid %s' % e.eid)
         e = rset.get_entity(0, 0)
-        self.assertEquals(e['title'], 'zou')
+        self.assertEquals(e.cw_attr_cache['title'], 'zou')
         self.assertEquals(pprelcachedict(e._cw_related_cache),
                           [('created_by_subject', [5])])
         # first level of recursion
         u = e.created_by[0]
-        self.assertEquals(u['login'], 'admin')
-        self.assertRaises(KeyError, u.__getitem__, 'firstname')
+        self.assertEquals(u.cw_attr_cache['login'], 'admin')
+        self.assertRaises(KeyError, u.cw_attr_cache.__getitem__, 'firstname')
         # second level of recursion
         s = u.in_state[0]
-        self.assertEquals(s['name'], 'activated')
-        self.assertRaises(KeyError, s.__getitem__, 'description')
+        self.assertEquals(s.cw_attr_cache['name'], 'activated')
+        self.assertRaises(KeyError, s.cw_attr_cache.__getitem__, 'description')
 
 
     def test_get_entity_cache_with_left_outer_join(self):
@@ -322,7 +322,7 @@
             etype, n = expected[entity.cw_row]
             self.assertEquals(entity.__regid__, etype)
             attr = etype == 'Bookmark' and 'title' or 'name'
-            self.assertEquals(entity[attr], n)
+            self.assertEquals(entity.cw_attr_cache[attr], n)
 
     def test_related_entity_optional(self):
         e = self.request().create_entity('Bookmark', title=u'aaaa', path=u'path')
--- a/test/unittest_utils.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/test/unittest_utils.py	Wed Aug 25 10:29:18 2010 +0200
@@ -140,14 +140,14 @@
 
     def test_encoding_bare_entity(self):
         e = Entity(None)
-        e['pouet'] = 'hop'
+        e.cw_attr_cache['pouet'] = 'hop'
         e.eid = 2
         self.assertEquals(json.loads(self.encode(e)),
                           {'pouet': 'hop', 'eid': 2})
 
     def test_encoding_entity_in_list(self):
         e = Entity(None)
-        e['pouet'] = 'hop'
+        e.cw_attr_cache['pouet'] = 'hop'
         e.eid = 2
         self.assertEquals(json.loads(self.encode([e])),
                           [{'pouet': 'hop', 'eid': 2}])
--- a/web/formfields.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/web/formfields.py	Wed Aug 25 10:29:18 2010 +0200
@@ -333,7 +333,7 @@
         if self.eidparam and self.role is not None:
             entity = form.edited_entity
             if form._cw.vreg.schema.rschema(self.name).final:
-                if entity.has_eid() or self.name in entity:
+                if entity.has_eid() or self.name in entity.cw_attr_cache:
                     value = getattr(entity, self.name)
                     if value is not None or not self.fallback_on_none_attribute:
                         return value
@@ -428,7 +428,7 @@
         if self.eidparam and self.role == 'subject':
             entity = form.edited_entity
             if entity.e_schema.has_metadata(self.name, 'format') and (
-                entity.has_eid() or '%s_format' % self.name in entity):
+                entity.has_eid() or '%s_format' % self.name in entity.cw_attr_cache):
                 return form.edited_entity.cw_attr_metadata(self.name, 'format')
         return form._cw.property_value('ui.default-text-format')
 
--- a/web/views/cwproperties.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/web/views/cwproperties.py	Wed Aug 25 10:29:18 2010 +0200
@@ -200,8 +200,8 @@
         else:
             entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw)
             entity.eid = self._cw.varmaker.next()
-            entity['pkey'] = key
-            entity['value'] = self._cw.vreg.property_value(key)
+            entity.cw_attr_cache['pkey'] = key
+            entity.cw_attr_cache['value'] = self._cw.vreg.property_value(key)
         return entity
 
     def form(self, formid, keys, splitlabel=False):
@@ -329,7 +329,7 @@
 
     def form_init(self, form):
         entity = form.edited_entity
-        if not (entity.has_eid() or 'pkey' in entity):
+        if not (entity.has_eid() or 'pkey' in entity.cw_attr_cache):
             # no key set yet, just include an empty div which will be filled
             # on key selection
             return
--- a/web/views/xmlrss.py	Wed Aug 25 10:29:07 2010 +0200
+++ b/web/views/xmlrss.py	Wed Aug 25 10:29:18 2010 +0200
@@ -68,7 +68,7 @@
                 value = entity.eid
             else:
                 try:
-                    value = entity[attr]
+                    value = entity.cw_attr_cache[attr]
                 except KeyError:
                     # Bytes
                     continue