merge
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 14 Aug 2009 11:14:26 +0200
changeset 2842 0477fff5f897
parent 2841 107ba1c45227 (diff)
parent 2838 107421e426de (current diff)
child 2843 3f5194ef620d
child 2845 660caa3ddc4f
merge
--- a/appobject.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/appobject.py	Fri Aug 14 11:14:26 2009 +0200
@@ -84,11 +84,15 @@
         return AndSelector(self, other)
     def __rand__(self, other):
         return AndSelector(other, self)
+    def __iand__(self, other):
+        raise NotImplementedError('cant use inplace & (binary and)')
 
     def __or__(self, other):
         return OrSelector(self, other)
     def __ror__(self, other):
         return OrSelector(other, self)
+    def __ior__(self, other):
+        raise NotImplementedError('cant use inplace | (binary or)')
 
     def __invert__(self):
         return NotSelector(self)
--- a/debian/cubicweb-dev.install.in	Fri Aug 14 00:02:08 2009 +0200
+++ b/debian/cubicweb-dev.install.in	Fri Aug 14 11:14:26 2009 +0200
@@ -6,6 +6,7 @@
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/ext/test usr/lib/PY_VERSION/site-packages/cubicweb/ext/
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/server/test usr/lib/PY_VERSION/site-packages/cubicweb/server/
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/test usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/
+debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/hooks/test usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/web/test usr/lib/PY_VERSION/site-packages/cubicweb/web/
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/etwist/test usr/lib/PY_VERSION/site-packages/cubicweb/etwist/
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/goa/test usr/lib/PY_VERSION/site-packages/cubicweb/goa/
--- a/debian/cubicweb-server.install.in	Fri Aug 14 00:02:08 2009 +0200
+++ b/debian/cubicweb-server.install.in	Fri Aug 14 11:14:26 2009 +0200
@@ -1,4 +1,5 @@
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/server/ usr/lib/PY_VERSION/site-packages/cubicweb
+debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/hooks/ usr/lib/PY_VERSION/site-packages/cubicweb
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/sobjects/ usr/lib/PY_VERSION/site-packages/cubicweb
 debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/schemas/ usr/lib/PY_VERSION/site-packages/cubicweb
 debian/tmp/usr/share/cubicweb/migration/ usr/share/cubicweb/
--- a/debian/rules	Fri Aug 14 00:02:08 2009 +0200
+++ b/debian/rules	Fri Aug 14 11:14:26 2009 +0200
@@ -48,6 +48,7 @@
 
 	# Remove unittests directory (should be available in cubicweb-dev only)
 	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/server/test
+	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/hooks/test
 	rm -rf debian/cubicweb-server/usr/lib/${PY_VERSION}/site-packages/cubicweb/sobjects/test
 	rm -rf debian/cubicweb-web/usr/lib/${PY_VERSION}/site-packages/cubicweb/web/test
 	rm -rf debian/cubicweb-twisted/usr/lib/${PY_VERSION}/site-packages/cubicweb/etwist/test
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/__init__.py	Fri Aug 14 11:14:26 2009 +0200
@@ -0,0 +1,1 @@
+"""core hooks"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/bookmark.py	Fri Aug 14 11:14:26 2009 +0200
@@ -0,0 +1,31 @@
+"""bookmark related hooks
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb.selectors import entity_implements
+from cubicweb.server import hook
+
+
+class AutoDeleteBookmarkOp(hook.Operation):
+    bookmark = None # make pylint happy
+    def precommit_event(self):
+        if not self.session.deleted_in_transaction(self.bookmark.eid):
+            if not self.bookmark.bookmarked_by:
+                self.bookmark.delete()
+
+
+class DelBookmarkedByHook(hook.Hook):
+    """ensure user logins are stripped"""
+    __id__ = 'autodelbookmark'
+    __select__ = hook.Hook.__select__ & entity_implements('bookmarked_by',)
+    category = 'bookmark'
+    events = ('after_delete_relation',)
+
+    def __call__(self):
+        AutoDeleteBookmarkOp(self.cw_req,
+                             bookmark=self.cw_req.entity_from_eid(self.eidfrom))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/email.py	Fri Aug 14 11:14:26 2009 +0200
@@ -0,0 +1,62 @@
+"""hooks to ensure use_email / primary_email relations consistency
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb.server import hook
+
+class SetUseEmailRelationOp(hook.Operation):
+    """delay this operation to commit to avoid conflict with a late rql query
+    already setting the relation
+    """
+    rtype = 'use_email'
+    entity = email = None # make pylint happy
+
+    def condition(self):
+        """check entity has use_email set for the email address"""
+        return not any(e for e in self.entity.use_email
+                       if self.email.eid == e.eid)
+
+    def precommit_event(self):
+        if self.condition():
+            self.session.unsafe_execute(
+                'SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % self.rtype,
+                {'x': self.entity.eid, 'y': self.email.eid}, 'x')
+
+class SetPrimaryEmailRelationOp(SetUseEmailRelationOp):
+    rtype = 'primary_email'
+
+    def condition(self):
+        """check entity has no primary_email set"""
+        return not self.entity.primary_email
+
+
+class SetPrimaryEmailHook(hook.Hook):
+    """notify when a bug or story or version has its state modified"""
+    __id__ = 'setprimaryemail'
+    __select__ = hook.Hook.__select__ & hook.match_rtype('use_email')
+    category = 'email'
+    events = ('after_add_relation',)
+
+    def call(self, session, eidfrom, rtype, eidto):
+        entity = self.cw_req.entity_from_eid(self.eidfrom)
+        if 'primary_email' in entity.e_schema.subject_relations():
+            SetPrimaryEmailRelationOp(self.cw_req, entity=entity,
+                                      email=self.cw_req.entity_from_eid(self.eidto))
+
+class SetUseEmailHook(hook.Hook):
+    """notify when a bug or story or version has its state modified"""
+    __id__ = 'setprimaryemail'
+    __select__ = hook.Hook.__select__ & hook.match_rtype('primary_email')
+    category = 'email'
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        entity = self.cw_req.entity_from_eid(self.eidfrom)
+        if 'use_email' in entity.e_schema.subject_relations():
+            SetUseEmailRelationOp(self.cw_req, entity=entity,
+                                  email=self.cw_req.entity_from_eid(self.eidto))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/integrity.py	Fri Aug 14 11:14:26 2009 +0200
@@ -0,0 +1,257 @@
+"""Core hooks: check for data integrity according to the instance'schema
+validity
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb import ValidationError
+from cubicweb.selectors import entity_implements
+from cubicweb.common.uilib import soup2xhtml
+from cubicweb.server import hook
+from cubicweb.server.pool import LateOperation, PreCommitOperation
+
+# special relations that don't have to be checked for integrity, usually
+# because they are handled internally by hooks (so we trust ourselves)
+DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by',
+                                'is', 'is_instance_of',
+                                'wf_info_for', 'from_state', 'to_state'))
+DONT_CHECK_RTYPES_ON_DEL = set(('is', 'is_instance_of',
+                                'wf_info_for', 'from_state', 'to_state'))
+
+
+class _CheckRequiredRelationOperation(hook.LateOperation):
+    """checking relation cardinality has to be done after commit in
+    case the relation is being replaced
+    """
+    eid, rtype = None, None
+
+    def precommit_event(self):
+        # recheck pending eids
+        if self.session.deleted_in_transaction(self.eid):
+            return
+        if self.session.unsafe_execute(*self._rql()).rowcount < 1:
+            etype = self.session.describe(self.eid)[0]
+            _ = self.session._
+            msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)')
+            msg %= {'rtype': _(self.rtype), 'etype': _(etype), 'eid': self.eid}
+            raise ValidationError(self.eid, {self.rtype: msg})
+
+    def commit_event(self):
+        pass
+
+    def _rql(self):
+        raise NotImplementedError()
+
+
+class _CheckSRelationOp(_CheckRequiredRelationOperation):
+    """check required subject relation"""
+    def _rql(self):
+        return 'Any O WHERE S eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
+
+
+class _CheckORelationOp(_CheckRequiredRelationOperation):
+    """check required object relation"""
+    def _rql(self):
+        return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
+
+
+class IntegrityHook(hook.Hook):
+    __abstract__ = True
+    category = 'integrity'
+
+
+class CheckCardinalityHook(IntegrityHook):
+    """check cardinalities are satisfied"""
+    __id__ = 'checkcard'
+    events = ('after_add_entity', 'before_delete_relation')
+
+    def __call__(self):
+        getattr(self, self.event)()
+
+    def checkrel_if_necessary(self, opcls, rtype, eid):
+        """check an equivalent operation has not already been added"""
+        for op in self.cw_req.pending_operations:
+            if isinstance(op, opcls) and op.rtype == rtype and op.eid == eid:
+                break
+        else:
+            opcls(self.cw_req, rtype=rtype, eid=eid)
+
+    def after_add_entity(self):
+        eid = self.entity.eid
+        eschema = self.entity.e_schema
+        for rschema, targetschemas, x in eschema.relation_definitions():
+            # skip automatically handled relations
+            if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
+                continue
+            if x == 'subject':
+                subjtype = eschema
+                objtype = targetschemas[0].type
+                cardindex = 0
+                opcls = _CheckSRelationOp
+            else:
+                subjtype = targetschemas[0].type
+                objtype = eschema
+                cardindex = 1
+                opcls = _CheckORelationOp
+            card = rschema.rproperty(subjtype, objtype, 'cardinality')
+            if card[cardindex] in '1+':
+                self.checkrel_if_necessary(opcls, rschema.type, eid)
+
+    def before_delete_relation(self):
+        rtype = self.rtype
+        if rtype in DONT_CHECK_RTYPES_ON_DEL:
+            return
+        session = self.cw_req
+        eidfrom, eidto = self.eidfrom, self.eidto
+        card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
+        pendingrdefs = session.transaction_data.get('pendingrdefs', ())
+        if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
+            return
+        if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
+            self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom)
+        if card[1] in '1+' and not session.deleted_in_transaction(eidto):
+            self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto)
+
+
+class _CheckConstraintsOp(hook.LateOperation):
+    """check a new relation satisfy its constraints
+    """
+    def precommit_event(self):
+        eidfrom, rtype, eidto = self.rdef
+        # first check related entities have not been deleted in the same
+        # transaction
+        if self.session.deleted_in_transaction(eidfrom):
+            return
+        if self.session.deleted_in_transaction(eidto):
+            return
+        for constraint in self.constraints:
+            try:
+                constraint.repo_check(self.session, eidfrom, rtype, eidto)
+            except NotImplementedError:
+                self.critical('can\'t check constraint %s, not supported',
+                              constraint)
+
+    def commit_event(self):
+        pass
+
+
+class CheckConstraintHook(IntegrityHook):
+    """check the relation satisfy its constraints
+
+    this is delayed to a precommit time operation since other relation which
+    will make constraint satisfied may be added later.
+    """
+    __id__ = 'checkconstraint'
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        constraints = self.cw_req.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
+                                'constraints')
+        if constraints:
+            _CheckConstraintsOp(self.cw_req, constraints=constraints,
+                               rdef=(self.eidfrom, self.rtype, self.eidto))
+
+
+class CheckUniqueHook(IntegrityHook):
+    __id__ = 'checkunique'
+    events = ('before_add_entity', 'before_update_entity')
+
+    def __call__(self):
+        entity = self.entity
+        eschema = entity.e_schema
+        for attr in entity.edited_attributes:
+            val = entity[attr]
+            if val is None:
+                continue
+            if eschema.subject_relation(attr).is_final() and \
+                   eschema.has_unique_values(attr):
+                rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
+                rset = self.cw_req.unsafe_execute(rql, {'val': val})
+                if rset and rset[0][0] != entity.eid:
+                    msg = self.cw_req._('the value "%s" is already used, use another one')
+                    raise ValidationError(entity.eid, {attr: msg % val})
+
+
+class _DelayedDeleteOp(hook.Operation):
+    """delete the object of composite relation except if the relation
+    has actually been redirected to another composite
+    """
+
+    def precommit_event(self):
+        session = self.session
+        # don't do anything if the entity is being created or deleted
+        if not (session.deleted_in_transaction(self.eid) or
+                session.added_in_transaction(self.eid)):
+            etype = session.describe(self.eid)[0]
+            session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'
+                                   % (etype, self.relation),
+                                   {'x': self.eid}, 'x')
+
+
+class DeleteCompositeOrphanHook(IntegrityHook):
+    """delete the composed of a composite relation when this relation is deleted
+    """
+    __id__ = 'deletecomposite'
+    events = ('before_delete_relation',)
+
+    def __call__(self):
+        composite = self.cw_req.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
+                                                 'composite')
+        if composite == 'subject':
+            _DelayedDeleteOp(self.cw_req, eid=self.eidto,
+                             relation='Y %s X' % self.rtype)
+        elif composite == 'object':
+            _DelayedDeleteOp(self.cw_req, eid=self.eidfrom,
+                             relation='X %s Y' % self.rtype)
+
+
+class DontRemoveOwnersGroupHook(IntegrityHook):
+    """delete the composed of a composite relation when this relation is deleted
+    """
+    __id__ = 'checkownersgroup'
+    __select__ = IntegrityHook.__select__ & entity_implements('CWGroup')
+    events = ('before_delete_entity', 'before_update_entity')
+
+    def __call__(self):
+        if self.event == 'before_delete_entity' and self.entity.name == 'owners':
+            raise ValidationError(self.entity.eid, {None: self.cw_req._('can\'t be deleted')})
+        elif self.event == 'before_update_entity' and 'name' in self.entity.edited_attribute:
+            newname = self.entity.pop('name')
+            oldname = self.entity.name
+            if oldname == 'owners' and newname != oldname:
+                raise ValidationError(self.entity.eid, {'name': self.cw_req._('can\'t be changed')})
+            self.entity['name'] = newname
+
+
+class TidyHtmlFields(IntegrityHook):
+    """tidy HTML in rich text strings"""
+    __id__ = 'htmltidy'
+    events = ('before_add_entity', 'before_update_entity')
+
+    def __call__(self):
+        entity = self.entity
+        metaattrs = entity.e_schema.meta_attributes()
+        for metaattr, (metadata, attr) in metaattrs.iteritems():
+            if metadata == 'format' and attr in entity.edited_attributes:
+                try:
+                    value = entity[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_req.encoding)
+
+
+class StripCWUserLoginHook(IntegrityHook):
+    """ensure user logins are stripped"""
+    __id__ = 'stripuserlogin'
+    __select__ = IntegrityHook.__select__ & entity_implements('CWUser')
+    events = ('before_add_entity', 'before_update_entity',)
+
+    def call(self, session, entity):
+        if 'login' in entity.edited_attributes and entity['login']:
+            entity['login'] = entity['login'].strip()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/metadata.py	Fri Aug 14 11:14:26 2009 +0200
@@ -0,0 +1,161 @@
+"""Core hooks: set generic metadata
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+
+from datetime import datetime
+
+from cubicweb.selectors import entity_implements
+from cubicweb.server import hook
+from cubicweb.server.repository import FTIndexEntityOp
+
+
+def eschema_type_eid(session, etype):
+    """get eid of the CWEType entity for the given yams type"""
+    eschema = session.repo.schema.eschema(etype)
+    # eschema.eid is None if schema has been readen from the filesystem, not
+    # from the database (eg during tests)
+    if eschema.eid is None:
+        eschema.eid = session.unsafe_execute(
+            'Any X WHERE X is CWEType, X name %(name)s', {'name': etype})[0][0]
+    return eschema.eid
+
+
+class MetaDataHook(hook.Hook):
+    __abstract__ = True
+    category = 'metadata'
+
+
+class InitMetaAttrsHook(MetaDataHook):
+    """before create a new entity -> set creation and modification date
+
+    this is a conveniency hook, you shouldn't have to disable it
+    """
+    __id__ = 'metaattrsinit'
+    events = ('before_add_entity',)
+
+    def __call__(self):
+        timestamp = datetime.now()
+        self.entity.setdefault('creation_date', timestamp)
+        self.entity.setdefault('modification_date', timestamp)
+        if not self.cw_req.get_shared_data('do-not-insert-cwuri'):
+            cwuri = u'%seid/%s' % (self.cw_req.base_url(), self.entity.eid)
+            self.entity.setdefault('cwuri', cwuri)
+
+
+class UpdateMetaAttrsHook(MetaDataHook):
+    """update an entity -> set modification date"""
+    __id__ = 'metaattrsupdate'
+    events = ('before_update_entity',)
+
+    def __call__(self):
+        self.entity.setdefault('modification_date', datetime.now())
+
+
+class _SetCreatorOp(hook.Operation):
+
+    def precommit_event(self):
+        session = self.session
+        if session.deleted_in_transaction(self.entity.eid):
+            # entity have been created and deleted in the same transaction
+            return
+        if not self.entity.created_by:
+            session.add_relation(self.entity.eid, 'created_by', session.user.eid)
+
+
+class SetIsHook(MetaDataHook):
+    """create a new entity -> set is relation"""
+    __id__ = 'setis'
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        if hasattr(self.entity, '_cw_recreating'):
+            return
+        session = self.cw_req
+        entity = self.entity
+        try:
+            session.add_relation(entity.eid, 'is',
+                                 eschema_type_eid(session, entity.id))
+        except IndexError:
+            # during schema serialization, skip
+            return
+        for etype in entity.e_schema.ancestors() + [entity.e_schema]:
+            session.add_relation(entity.eid, 'is_instance_of',
+                                 eschema_type_eid(session, etype))
+
+
+class SetOwnershipHook(MetaDataHook):
+    """create a new entity -> set owner and creator metadata"""
+    __id__ = 'setowner'
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        asession = self.cw_req.actual_session()
+        if not asession.is_internal_session:
+            self.cw_req.add_relation(self.entity.eid, 'owned_by', asession.user.eid)
+            _SetCreatorOp(asession, entity=self.entity)
+
+
+class _SyncOwnersOp(hook.Operation):
+    def precommit_event(self):
+        self.session.unsafe_execute('SET X owned_by U WHERE C owned_by U, C eid %(c)s,'
+                                    'NOT EXISTS(X owned_by U, X eid %(x)s)',
+                                    {'c': self.compositeeid, 'x': self.composedeid},
+                                    ('c', 'x'))
+
+
+class SyncCompositeOwner(MetaDataHook):
+    """when adding composite relation, the composed should have the same owners
+    has the composite
+    """
+    __id__ = 'synccompositeowner'
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        if self.rtype == 'wf_info_for':
+            # skip this special composite relation # XXX (syt) why?
+            return
+        eidfrom, eidto = self.eidfrom, self.eidto
+        composite = self.cw_req.schema_rproperty(self.rtype, eidfrom, eidto, 'composite')
+        if composite == 'subject':
+            _SyncOwnersOp(self.cw_req, compositeeid=eidfrom, composedeid=eidto)
+        elif composite == 'object':
+            _SyncOwnersOp(self.cw_req, compositeeid=eidto, composedeid=eidfrom)
+
+
+class FixUserOwnershipHook(MetaDataHook):
+    """when a user has been created, add owned_by relation on itself"""
+    __id__ = 'fixuserowner'
+    __select__ = MetaDataHook.__select__ & entity_implements('CWUser')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        self.cw_req.add_relation(self.entity.eid, 'owned_by', self.entity.eid)
+
+
+class UpdateFTIHook(MetaDataHook):
+    """sync fulltext index when relevant relation is added / removed
+    """
+    __id__ = 'updateftirel'
+    events = ('after_add_relation', 'after_delete_relation')
+
+    def __call__(self):
+        rtype = self.rtype
+        session = self.cw_req
+        if self.event == 'after_add_relation':
+            # Reindexing the contained entity is enough since it will implicitly
+            # reindex the container entity.
+            ftcontainer = session.vreg.schema.rschema(rtype).fulltext_container
+            if ftcontainer == 'subject':
+                FTIndexEntityOp(session, entity=session.entity_from_eid(self.eidto))
+            elif ftcontainer == 'object':
+                FTIndexEntityOp(session, entity=session.entity_from_eid(self.eidfrom))
+        elif session.repo.schema.rschema(rtype).fulltext_container:
+            FTIndexEntityOp(session, entity=session.entity_from_eid(self.eidto))
+            FTIndexEntityOp(session, entity=session.entity_from_eid(self.eidfrom))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/notification.py	Fri Aug 14 11:14:26 2009 +0200
@@ -0,0 +1,133 @@
+"""some hooks to handle notification on entity's changes
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab.common.textutils import normalize_text
+
+from cubicweb import RegistryException
+from cubicweb.selectors import entity_implements
+from cubicweb.server import hook
+
+
+class RenderAndSendNotificationView(hook.Operation):
+    """delay rendering of notification view until precommit"""
+    def precommit_event(self):
+        view = self.view
+        if view.cw_rset and self.session.deleted_in_transaction(view.cw_rset[cw_rset.cw_row or 0][cw_rset.cw_col or 0]):
+            return # entity added and deleted in the same transaction
+        self.view.render_and_send(**getattr(self, 'viewargs', {}))
+
+
+class NotificationHook(hook.Hook):
+    __abstract__ = True
+    category = 'notification'
+
+    def select_view(self, vid, rset, row=0, col=0):
+        return self.cw_req.vreg['views'].select_object(vid, self.cw_req,
+                                                       rset=rset, row=0, col=0)
+
+
+class StatusChangeHook(NotificationHook):
+    """notify when a workflowable entity has its state modified"""
+    __id__ = 'notifystatuschange'
+    __select__ = NotificationHook.__select__ & entity_implements('TrInfo')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        if not entity.from_state: # not a transition
+            return
+        rset = entity.related('wf_info_for')
+        view = self.select_view('notif_status_change', rset=rset, row=0)
+        if view is None:
+            return
+        comment = entity.printable_value('comment', format='text/plain')
+        if comment:
+            comment = normalize_text(comment, 80,
+                                     rest=entity.comment_format=='text/rest')
+        RenderAndSendNotificationView(self.cw_req, view=view, viewargs={
+            'comment': comment, 'previous_state': entity.previous_state.name,
+            'current_state': entity.new_state.name})
+
+
+class RelationChangeHook(NotificationHook):
+    __id__ = 'notifyrelationchange'
+    events = ('before_add_relation', 'after_add_relation',
+              'before_delete_relation', 'after_delete_relation')
+
+    def __call__(self):
+        """if a notification view is defined for the event, send notification
+        email defined by the view
+        """
+        rset = self.cw_req.eid_rset(self.eidfrom)
+        view = self.select_view('notif_%s_%s' % (self.event,  self.rtype),
+                                rset=rset, row=0)
+        if view is None:
+            return
+        RenderAndSendNotificationView(self.cw_req, view=view)
+
+
+class EntityChangeHook(NotificationHook):
+    """if a notification view is defined for the event, send notification
+    email defined by the view
+    """
+    __id__ = 'notifyentitychange'
+    events = ('after_add_entity', 'after_update_entity')
+
+    def __call__(self):
+        rset = self.entity.as_rset()
+        view = self.select_view('notif_%s' % self.event, rset=rset, row=0)
+        if view is None:
+            return
+        RenderAndSendNotificationView(self.cw_req, view=view)
+
+
+# supervising ##################################################################
+
+class SomethingChangedHook(NotificationHook):
+    __id__ = 'supervising'
+    events = ('before_add_relation', 'before_delete_relation',
+              'after_add_entity', 'before_update_entity')
+
+    def __call__(self):
+        dest = self.cw_req.vreg.config['supervising-addrs']
+        if not dest: # no supervisors, don't do this for nothing...
+            return
+        if self._call():
+            SupervisionMailOp(self.cw_req)
+
+    def _call(self):
+        event = self.event.split('_', 1)[1]
+        if event == 'update_entity':
+            if self.cw_req.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'))):
+                    # don't record last_login_time update which are done
+                    # automatically at login time
+                    return False
+        self.cw_req.transaction_data.setdefault('pendingchanges', []).append(
+            (event, self))
+        return True
+
+
+class EntityDeleteHook(SomethingChangedHook):
+    __id__ = 'supervisingentitydel'
+    events = ('before_delete_entity',)
+
+    def _call(self):
+        try:
+            title = self.entity.dc_title()
+        except:
+            # may raise an error during deletion process, for instance due to
+            # missing required relation
+            title = '#%s' % eid
+        self.cw_req.transaction_data.setdefault('pendingchanges', []).append(
+            ('delete_entity', (self.eid, str(self.entity.e_schema), title)))
+        return True
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/security.py	Fri Aug 14 11:14:26 2009 +0200
@@ -0,0 +1,121 @@
+"""Security hooks: check permissions to add/delete/update entities according to
+the user connected to a session
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb import Unauthorized
+from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS, hook
+
+
+def check_entity_attributes(session, entity):
+    eid = entity.eid
+    eschema = entity.e_schema
+    # ._default_set is only there on entity creation to indicate unspecified
+    # attributes which has been set to a default value defined in the schema
+    defaults = getattr(entity, '_default_set', ())
+    try:
+        editedattrs = entity.edited_attributes
+    except AttributeError:
+        editedattrs = entity
+    for attr in editedattrs:
+        if attr in defaults:
+            continue
+        rschema = eschema.subject_relation(attr)
+        if rschema.is_final(): # non final relation are checked by other hooks
+            # add/delete should be equivalent (XXX: unify them into 'update' ?)
+            rschema.check_perm(session, 'add', eid)
+
+
+class _CheckEntityPermissionOp(hook.LateOperation):
+    def precommit_event(self):
+        #print 'CheckEntityPermissionOp', self.session.user, self.entity, self.action
+        self.entity.check_perm(self.action)
+        check_entity_attributes(self.session, self.entity)
+
+    def commit_event(self):
+        pass
+
+
+class _CheckRelationPermissionOp(hook.LateOperation):
+    def precommit_event(self):
+        self.rschema.check_perm(self.session, self.action, self.eidfrom, self.eidto)
+
+    def commit_event(self):
+        pass
+
+
+class SecurityHook(hook.Hook):
+    __abstract__ = True
+    category = 'security'
+    __select__ = hook.Hook.__select__ & hook.regular_session()
+
+
+class AfterAddEntitySecurityHook(SecurityHook):
+    __id__ = 'securityafteraddentity'
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        _CheckEntityPermissionOp(self.cw_req, entity=self.entity, action='add')
+
+
+class AfterUpdateEntitySecurityHook(SecurityHook):
+    __id__ = 'securityafterupdateentity'
+    events = ('after_update_entity',)
+
+    def __call__(self):
+        try:
+            # check user has permission right now, if not retry at commit time
+            self.entity.check_perm('update')
+            check_entity_attributes(self.cw_req, self.entity)
+        except Unauthorized:
+            self.entity.clear_local_perm_cache('update')
+            _CheckEntityPermissionOp(self.cw_req, entity=self.entity, action='update')
+
+
+class BeforeDelEntitySecurityHook(SecurityHook):
+    __id__ = 'securitybeforedelentity'
+    events = ('before_delete_entity',)
+
+    def __call__(self):
+        self.entity.e_schema.check_perm(self.cw_req, 'delete', eid)
+
+
+class BeforeAddRelationSecurityHook(SecurityHook):
+    __id__ = 'securitybeforeaddrelation'
+    events = ('before_add_relation',)
+
+    def __call__(self):
+        if self.rtype in BEFORE_ADD_RELATIONS:
+            rschema = self.cw_req.repo.schema[self.rtype]
+            rschema.check_perm(self.cw_req, 'add', self.eidfrom, self.eidto)
+
+
+class AfterAddRelationSecurityHook(SecurityHook):
+    __id__ = 'securityafteraddrelation'
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        if not self.rtype in BEFORE_ADD_RELATIONS:
+            rschema = self.cw_req.repo.schema[self.rtype]
+            if self.rtype in ON_COMMIT_ADD_RELATIONS:
+                _CheckRelationPermissionOp(self.cw_req, action='add',
+                                           rschema=rschema,
+                                           eidfrom=self.eidfrom,
+                                           eidto=self.eidto)
+            else:
+                rschema.check_perm(self.cw_req, 'add', self.eidfrom, self.eidto)
+
+
+class BeforeDelRelationSecurityHook(SecurityHook):
+    __id__ = 'securitybeforedelrelation'
+    events = ('before_delete_relation',)
+
+    def __call__(self):
+        self.cw_req.repo.schema[self.rtype].check_perm(self.cw_req, 'delete',
+                                                       self.eidfrom, self.eidto)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/syncschema.py	Fri Aug 14 11:14:26 2009 +0200
@@ -0,0 +1,1103 @@
+"""schema hooks:
+
+- synchronize the living schema object with the persistent schema
+- perform physical update on the source when necessary
+
+checking for schema consistency is done in hooks.py
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from yams.schema import BASE_TYPES
+from yams.buildobjs import EntityType, RelationType, RelationDefinition
+from yams.schema2sql import eschema2sql, rschema2sql, type_from_constraints
+
+from cubicweb import ValidationError, RepositoryError
+from cubicweb.selectors import entity_implements
+from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS
+from cubicweb.server import hook, schemaserial as ss
+from cubicweb.server.sqlutils import SQL_PREFIX
+
+
+TYPE_CONVERTER = { # XXX
+    'Boolean': bool,
+    'Int': int,
+    'Float': float,
+    'Password': str,
+    'String': unicode,
+    'Date' : unicode,
+    'Datetime' : unicode,
+    'Time' : unicode,
+    }
+
+# core entity and relation types which can't be removed
+CORE_ETYPES = list(BASE_TYPES) + ['CWEType', 'CWRType', 'CWUser', 'CWGroup',
+                                  'CWConstraint', 'CWAttribute', 'CWRelation']
+CORE_RTYPES = ['eid', 'creation_date', 'modification_date', 'cwuri',
+               'login', 'upassword', 'name',
+               'is', 'instanceof', 'owned_by', 'created_by', 'in_group',
+               'relation_type', 'from_entity', 'to_entity',
+               'constrainted_by',
+               'read_permission', 'add_permission',
+               'delete_permission', 'updated_permission',
+               ]
+
+def get_constraints(session, entity):
+    constraints = []
+    for cstreid in session.transaction_data.get(entity.eid, ()):
+        cstrent = session.entity_from_eid(cstreid)
+        cstr = CONSTRAINTS[cstrent.type].deserialize(cstrent.value)
+        cstr.eid = cstreid
+        constraints.append(cstr)
+    return constraints
+
+def add_inline_relation_column(session, etype, rtype):
+    """add necessary column and index for an inlined relation"""
+    table = SQL_PREFIX + etype
+    column = SQL_PREFIX + rtype
+    try:
+        session.system_sql(str('ALTER TABLE %s ADD COLUMN %s integer'
+                               % (table, column)), rollback_on_failure=False)
+        session.info('added column %s to table %s', column, table)
+    except:
+        # silent exception here, if this error has not been raised because the
+        # column already exists, index creation will fail anyway
+        session.exception('error while adding column %s to table %s',
+                          table, column)
+    # create index before alter table which may expectingly fail during test
+    # (sqlite) while index creation should never fail (test for index existence
+    # is done by the dbhelper)
+    session.pool.source('system').create_index(session, table, column)
+    session.info('added index on %s(%s)', table, column)
+    session.transaction_data.setdefault('createdattrs', []).append(
+        '%s.%s' % (etype, rtype))
+
+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:
+        if attr in ro_attrs:
+            newval = entity.pop(attr)
+            origval = getattr(entity, 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)
+
+
+# operations for low-level database alteration  ################################
+
+class DropTable(hook.Operation):
+    """actually remove a database from the instance's schema"""
+    table = None # make pylint happy
+    def precommit_event(self):
+        dropped = self.session.transaction_data.setdefault('droppedtables',
+                                                           set())
+        if self.table in dropped:
+            return # already processed
+        dropped.add(self.table)
+        self.session.system_sql('DROP TABLE %s' % self.table)
+        self.info('dropped table %s', self.table)
+
+
+class DropRelationTable(DropTable):
+    def __init__(self, session, rtype):
+        super(DropRelationTable, self).__init__(
+            session, table='%s_relation' % rtype)
+        session.transaction_data.setdefault('pendingrtypes', set()).add(rtype)
+
+
+class DropColumn(hook.Operation):
+    """actually remove the attribut's column from entity table in the system
+    database
+    """
+    table = column = None # make pylint happy
+    def precommit_event(self):
+        session, table, column = self.session, self.table, self.column
+        # drop index if any
+        session.pool.source('system').drop_index(session, table, column)
+        try:
+            session.system_sql('ALTER TABLE %s DROP COLUMN %s'
+                               % (table, column), rollback_on_failure=False)
+            self.info('dropped column %s from table %s', column, table)
+        except Exception, ex:
+            # not supported by sqlite for instance
+            self.error('error while altering table %s: %s', table, ex)
+
+
+# base operations for in-memory schema synchronization  ########################
+
+class MemSchemaNotifyChanges(hook.SingleLastOperation):
+    """the update schema operation:
+
+    special operation which should be called once and after all other schema
+    operations. It will trigger internal structures rebuilding to consider
+    schema changes
+    """
+
+    def __init__(self, session):
+        self.repo = session.repo
+        hook.SingleLastOperation.__init__(self, session)
+
+    def commit_event(self):
+        self.repo.set_schema(self.repo.schema)
+
+
+class MemSchemaOperation(hook.Operation):
+    """base class for schema operations"""
+    def __init__(self, session, kobj=None, **kwargs):
+        self.schema = session.schema
+        self.kobj = kobj
+        # once Operation.__init__ has been called, event may be triggered, so
+        # do this last !
+        hook.Operation.__init__(self, session, **kwargs)
+        # every schema operation is triggering a schema update
+        MemSchemaNotifyChanges(session)
+
+    def prepare_constraints(self, subjtype, rtype, objtype):
+        constraints = rtype.rproperty(subjtype, objtype, 'constraints')
+        self.constraints = list(constraints)
+        rtype.set_rproperty(subjtype, objtype, 'constraints', self.constraints)
+
+
+class MemSchemaEarlyOperation(MemSchemaOperation):
+    def insert_index(self):
+        """schema operation which are inserted at the begining of the queue
+        (typically to add/remove entity or relation types)
+        """
+        i = -1
+        for i, op in enumerate(self.session.pending_operations):
+            if not isinstance(op, MemSchemaEarlyOperation):
+                return i
+        return i + 1
+
+
+class MemSchemaPermOperation(MemSchemaOperation):
+    """base class to synchronize schema permission definitions"""
+    def __init__(self, session, perm, etype_eid):
+        self.perm = perm
+        try:
+            self.name = session.entity_from_eid(etype_eid).name
+        except IndexError:
+            self.error('changing permission of a no more existant type #%s',
+                etype_eid)
+        else:
+            hook.Operation.__init__(self, session)
+
+
+# operations for high-level source database alteration  ########################
+
+class SourceDbCWETypeRename(hook.Operation):
+    """this operation updates physical storage accordingly"""
+    oldname = newname = None # make pylint happy
+
+    def precommit_event(self):
+        # we need sql to operate physical changes on the system database
+        sqlexec = self.session.system_sql
+        sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, self.oldname,
+                                                     SQL_PREFIX, self.newname))
+        self.info('renamed table %s to %s', self.oldname, self.newname)
+        sqlexec('UPDATE entities SET type=%s WHERE type=%s',
+                (self.newname, self.oldname))
+        sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',
+                (self.newname, self.oldname))
+
+
+class SourceDbCWRTypeUpdate(hook.Operation):
+    """actually update some properties of a relation definition"""
+    rschema = values = entity = None # make pylint happy
+
+    def precommit_event(self):
+        session = self.session
+        rschema = self.rschema
+        if rschema.is_final() or not 'inlined' in self.values:
+            return # nothing to do
+        inlined = self.values['inlined']
+        entity = self.entity
+        # check in-lining is necessary / possible
+        if not entity.inlined_changed(inlined):
+            return # nothing to do
+        # inlined changed, make necessary physical changes!
+        sqlexec = self.session.system_sql
+        rtype = rschema.type
+        eidcolumn = SQL_PREFIX + 'eid'
+        if not inlined:
+            # need to create the relation if it has not been already done by
+            # another event of the same transaction
+            if not rschema.type in session.transaction_data.get('createdtables', ()):
+                tablesql = rschema2sql(rschema)
+                # create the necessary table
+                for sql in tablesql.split(';'):
+                    if sql.strip():
+                        sqlexec(sql)
+                session.transaction_data.setdefault('createdtables', []).append(
+                    rschema.type)
+            # copy existant data
+            column = SQL_PREFIX + rtype
+            for etype in rschema.subjects():
+                table = SQL_PREFIX + str(etype)
+                sqlexec('INSERT INTO %s_relation SELECT %s, %s FROM %s WHERE NOT %s IS NULL'
+                        % (rtype, eidcolumn, column, table, column))
+            # drop existant columns
+            for etype in rschema.subjects():
+                DropColumn(session, table=SQL_PREFIX + str(etype),
+                             column=SQL_PREFIX + rtype)
+        else:
+            for etype in rschema.subjects():
+                try:
+                    add_inline_relation_column(session, str(etype), rtype)
+                except Exception, ex:
+                    # the column probably already exists. this occurs when the
+                    # entity's type has just been added or if the column has not
+                    # been previously dropped
+                    self.error('error while altering table %s: %s', etype, ex)
+                # copy existant data.
+                # XXX don't use, it's not supported by sqlite (at least at when i tried it)
+                #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to '
+                #        'FROM %(rtype)s_relation '
+                #        'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from'
+                #        % locals())
+                table = SQL_PREFIX + str(etype)
+                cursor = sqlexec('SELECT eid_from, eid_to FROM %(table)s, '
+                                 '%(rtype)s_relation WHERE %(table)s.%(eidcolumn)s='
+                                 '%(rtype)s_relation.eid_from' % locals())
+                args = [{'val': eid_to, 'x': eid} for eid, eid_to in cursor.fetchall()]
+                if args:
+                    column = SQL_PREFIX + rtype
+                    cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'
+                                       % (table, column, eidcolumn), args)
+                # drop existant table
+                DropRelationTable(session, rtype)
+
+
+class SourceDbCWAttributeAdd(hook.Operation):
+    """an attribute relation (CWAttribute) has been added:
+    * add the necessary column
+    * set default on this column if any and possible
+    * register an operation to add the relation definition to the
+      instance's schema on commit
+
+    constraints are handled by specific hooks
+    """
+    entity = None # make pylint happy
+
+    def init_rdef(self, **kwargs):
+        entity = self.entity
+        fromentity = entity.stype
+        self.session.execute('SET X ordernum Y+1 '
+                             'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
+                             'X ordernum >= %(order)s, NOT X eid %(x)s',
+                             {'x': entity.eid, 'se': fromentity.eid,
+                              'order': entity.ordernum or 0})
+        subj = str(fromentity.name)
+        rtype = entity.rtype.name
+        obj = str(entity.otype.name)
+        constraints = get_constraints(self.session, entity)
+        rdef = RelationDefinition(subj, rtype, obj,
+                                  description=entity.description,
+                                  cardinality=entity.cardinality,
+                                  constraints=constraints,
+                                  order=entity.ordernum,
+                                  eid=entity.eid,
+                                  **kwargs)
+        MemSchemaRDefAdd(self.session, rdef)
+        return rdef
+
+    def precommit_event(self):
+        session = self.session
+        entity = self.entity
+        # entity.defaultval is a string or None, but we need a correctly typed
+        # value
+        default = entity.defaultval
+        if default is not None:
+            default = TYPE_CONVERTER[entity.otype.name](default)
+        rdef = self.init_rdef(default=default,
+                              indexed=entity.indexed,
+                              fulltextindexed=entity.fulltextindexed,
+                              internationalizable=entity.internationalizable)
+        sysource = session.pool.source('system')
+        attrtype = type_from_constraints(sysource.dbhelper, rdef.object,
+                                         rdef.constraints)
+        # XXX should be moved somehow into lgc.adbh: sqlite doesn't support to
+        # add a new column with UNIQUE, it should be added after the ALTER TABLE
+        # using ADD INDEX
+        if sysource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
+            extra_unique_index = True
+            attrtype = attrtype.replace(' UNIQUE', '')
+        else:
+            extra_unique_index = False
+        # added some str() wrapping query since some backend (eg psycopg) don't
+        # allow unicode queries
+        table = SQL_PREFIX + rdef.subject
+        column = SQL_PREFIX + rdef.name
+        try:
+            session.system_sql(str('ALTER TABLE %s ADD COLUMN %s %s'
+                                   % (table, column, attrtype)),
+                               rollback_on_failure=False)
+            self.info('added column %s to table %s', table, column)
+        except Exception, ex:
+            # the column probably already exists. this occurs when
+            # the entity's type has just been added or if the column
+            # has not been previously dropped
+            self.error('error while altering table %s: %s', table, ex)
+        if extra_unique_index or entity.indexed:
+            try:
+                sysource.create_index(session, table, column,
+                                      unique=extra_unique_index)
+            except Exception, ex:
+                self.error('error while creating index for %s.%s: %s',
+                           table, column, ex)
+
+
+class SourceDbCWRelationAdd(SourceDbCWAttributeAdd):
+    """an actual relation has been added:
+    * if this is an inlined relation, add the necessary column
+      else if it's the first instance of this relation type, add the
+      necessary table and set default permissions
+    * register an operation to add the relation definition to the
+      instance's schema on commit
+
+    constraints are handled by specific hooks
+    """
+    entity = None # make pylint happy
+
+    def precommit_event(self):
+        session = self.session
+        entity = self.entity
+        rdef = self.init_rdef(composite=entity.composite)
+        schema = session.schema
+        rtype = rdef.name
+        rschema = session.schema.rschema(rtype)
+        # this have to be done before permissions setting
+        if rschema.inlined:
+            # need to add a column if the relation is inlined and if this is the
+            # first occurence of "Subject relation Something" whatever Something
+            # and if it has not been added during other event of the same
+            # transaction
+            key = '%s.%s' % (rdef.subject, rtype)
+            try:
+                alreadythere = bool(rschema.objects(rdef.subject))
+            except KeyError:
+                alreadythere = False
+            if not (alreadythere or
+                    key in session.transaction_data.get('createdattrs', ())):
+                add_inline_relation_column(session, rdef.subject, rtype)
+        else:
+            # need to create the relation if no relation definition in the
+            # schema and if it has not been added during other event of the same
+            # transaction
+            if not (rschema.subjects() or
+                    rtype in session.transaction_data.get('createdtables', ())):
+                try:
+                    rschema = session.schema.rschema(rtype)
+                    tablesql = rschema2sql(rschema)
+                except KeyError:
+                    # fake we add it to the schema now to get a correctly
+                    # initialized schema but remove it before doing anything
+                    # more dangerous...
+                    rschema = session.schema.add_relation_type(rdef)
+                    tablesql = rschema2sql(rschema)
+                    session.schema.del_relation_type(rtype)
+                # create the necessary table
+                for sql in tablesql.split(';'):
+                    if sql.strip():
+                        session.system_sql(sql)
+                session.transaction_data.setdefault('createdtables', []).append(
+                    rtype)
+
+
+class SourceDbRDefUpdate(hook.Operation):
+    """actually update some properties of a relation definition"""
+    rschema = values = None # make pylint happy
+
+    def precommit_event(self):
+        etype = self.kobj[0]
+        table = SQL_PREFIX + etype
+        column = SQL_PREFIX + self.rschema.type
+        if 'indexed' in self.values:
+            sysource = self.session.pool.source('system')
+            if self.values['indexed']:
+                sysource.create_index(self.session, table, column)
+            else:
+                sysource.drop_index(self.session, table, column)
+        if 'cardinality' in self.values and self.rschema.is_final():
+            adbh = self.session.pool.source('system').dbhelper
+            if not adbh.alter_column_support:
+                # not supported (and NOT NULL not set by yams in that case, so
+                # no worry)
+                return
+            atype = self.rschema.objects(etype)[0]
+            constraints = self.rschema.rproperty(etype, atype, 'constraints')
+            coltype = type_from_constraints(adbh, atype, constraints,
+                                            creating=False)
+            # XXX check self.values['cardinality'][0] actually changed?
+            sql = adbh.sql_set_null_allowed(table, column, coltype,
+                                            self.values['cardinality'][0] != '1')
+            self.session.system_sql(sql)
+
+
+class SourceDbCWConstraintAdd(hook.Operation):
+    """actually update constraint of a relation definition"""
+    entity = None # make pylint happy
+    cancelled = False
+
+    def precommit_event(self):
+        rdef = self.entity.reverse_constrained_by[0]
+        session = self.session
+        # when the relation is added in the same transaction, the constraint
+        # object is created by the operation adding the attribute or relation,
+        # so there is nothing to do here
+        if session.added_in_transaction(rdef.eid):
+            return
+        subjtype, rtype, objtype = session.schema.schema_by_eid(rdef.eid)
+        cstrtype = self.entity.type
+        oldcstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
+        newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
+        table = SQL_PREFIX + str(subjtype)
+        column = SQL_PREFIX + str(rtype)
+        # alter the physical schema on size constraint changes
+        if newcstr.type() == 'SizeConstraint' and (
+            oldcstr is None or oldcstr.max != newcstr.max):
+            adbh = self.session.pool.source('system').dbhelper
+            card = rtype.rproperty(subjtype, objtype, 'cardinality')
+            coltype = type_from_constraints(adbh, objtype, [newcstr],
+                                            creating=False)
+            sql = adbh.sql_change_col_type(table, column, coltype, card != '1')
+            try:
+                session.system_sql(sql, rollback_on_failure=False)
+                self.info('altered column %s of table %s: now VARCHAR(%s)',
+                          column, table, newcstr.max)
+            except Exception, ex:
+                # not supported by sqlite for instance
+                self.error('error while altering table %s: %s', table, ex)
+        elif cstrtype == 'UniqueConstraint' and oldcstr is None:
+            session.pool.source('system').create_index(
+                self.session, table, column, unique=True)
+
+
+class SourceDbCWConstraintDel(hook.Operation):
+    """actually remove a constraint of a relation definition"""
+    rtype = subjtype = objtype = None # make pylint happy
+
+    def precommit_event(self):
+        cstrtype = self.cstr.type()
+        table = SQL_PREFIX + str(self.subjtype)
+        column = SQL_PREFIX + str(self.rtype)
+        # alter the physical schema on size/unique constraint changes
+        if cstrtype == 'SizeConstraint':
+            try:
+                self.session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE TEXT'
+                                        % (table, column),
+                                        rollback_on_failure=False)
+                self.info('altered column %s of table %s: now TEXT',
+                          column, table)
+            except Exception, ex:
+                # not supported by sqlite for instance
+                self.error('error while altering table %s: %s', table, ex)
+        elif cstrtype == 'UniqueConstraint':
+            self.session.pool.source('system').drop_index(
+                self.session, table, column, unique=True)
+
+
+# operations for in-memory schema synchronization  #############################
+
+class MemSchemaCWETypeAdd(MemSchemaEarlyOperation):
+    """actually add the entity type to the instance's schema"""
+    eid = None # make pylint happy
+    def commit_event(self):
+        self.schema.add_entity_type(self.kobj)
+
+
+class MemSchemaCWETypeRename(MemSchemaOperation):
+    """this operation updates physical storage accordingly"""
+    oldname = newname = None # make pylint happy
+
+    def commit_event(self):
+        self.session.schema.rename_entity_type(self.oldname, self.newname)
+
+
+class MemSchemaCWETypeDel(MemSchemaOperation):
+    """actually remove the entity type from the instance's schema"""
+    def commit_event(self):
+        try:
+            # del_entity_type also removes entity's relations
+            self.schema.del_entity_type(self.kobj)
+        except KeyError:
+            # s/o entity type have already been deleted
+            pass
+
+
+class MemSchemaCWRTypeAdd(MemSchemaEarlyOperation):
+    """actually add the relation type to the instance's schema"""
+    eid = None # make pylint happy
+    def commit_event(self):
+        rschema = self.schema.add_relation_type(self.kobj)
+        rschema.set_default_groups()
+
+
+class MemSchemaCWRTypeUpdate(MemSchemaOperation):
+    """actually update some properties of a relation definition"""
+    rschema = values = None # make pylint happy
+
+    def commit_event(self):
+        # structure should be clean, not need to remove entity's relations
+        # at this point
+        self.rschema.__dict__.update(self.values)
+
+
+class MemSchemaCWRTypeDel(MemSchemaOperation):
+    """actually remove the relation type from the instance's schema"""
+    def commit_event(self):
+        try:
+            self.schema.del_relation_type(self.kobj)
+        except KeyError:
+            # s/o entity type have already been deleted
+            pass
+
+
+class MemSchemaRDefAdd(MemSchemaEarlyOperation):
+    """actually add the attribute relation definition to the instance's
+    schema
+    """
+    def commit_event(self):
+        self.schema.add_relation_def(self.kobj)
+
+
+class MemSchemaRDefUpdate(MemSchemaOperation):
+    """actually update some properties of a relation definition"""
+    rschema = values = None # make pylint happy
+
+    def commit_event(self):
+        # structure should be clean, not need to remove entity's relations
+        # at this point
+        self.rschema._rproperties[self.kobj].update(self.values)
+
+
+class MemSchemaRDefDel(MemSchemaOperation):
+    """actually remove the relation definition from the instance's schema"""
+    def commit_event(self):
+        subjtype, rtype, objtype = self.kobj
+        try:
+            self.schema.del_relation_def(subjtype, rtype, objtype)
+        except KeyError:
+            # relation type may have been already deleted
+            pass
+
+
+class MemSchemaCWConstraintAdd(MemSchemaOperation):
+    """actually update constraint of a relation definition
+
+    has to be called before SourceDbCWConstraintAdd
+    """
+    cancelled = False
+
+    def precommit_event(self):
+        rdef = self.entity.reverse_constrained_by[0]
+        # when the relation is added in the same transaction, the constraint
+        # object is created by the operation adding the attribute or relation,
+        # so there is nothing to do here
+        if session.added_in_transaction(rdef.eid):
+            self.cancelled = True
+            return
+        subjtype, rtype, objtype = self.session.schema.schema_by_eid(rdef.eid)
+        self.prepare_constraints(subjtype, rtype, objtype)
+        cstrtype = self.entity.type
+        self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
+        self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
+        self.newcstr.eid = self.entity.eid
+
+    def commit_event(self):
+        if self.cancelled:
+            return
+        # in-place modification
+        if not self.cstr is None:
+            self.constraints.remove(self.cstr)
+        self.constraints.append(self.newcstr)
+
+
+class MemSchemaCWConstraintDel(MemSchemaOperation):
+    """actually remove a constraint of a relation definition
+
+    has to be called before SourceDbCWConstraintDel
+    """
+    rtype = subjtype = objtype = None # make pylint happy
+    def precommit_event(self):
+        self.prepare_constraints(self.subjtype, self.rtype, self.objtype)
+
+    def commit_event(self):
+        self.constraints.remove(self.cstr)
+
+
+class MemSchemaPermCWGroupAdd(MemSchemaPermOperation):
+    """synchronize schema when a *_permission relation has been added on a group
+    """
+    def __init__(self, session, perm, etype_eid, group_eid):
+        self.group = session.entity_from_eid(group_eid).name
+        super(MemSchemaPermCWGroupAdd, self).__init__(
+            session, perm, etype_eid)
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        try:
+            erschema = self.schema[self.name]
+        except KeyError:
+            # duh, schema not found, log error and skip operation
+            self.error('no schema for %s', self.name)
+            return
+        groups = list(erschema.get_groups(self.perm))
+        try:
+            groups.index(self.group)
+            self.warning('group %s already have permission %s on %s',
+                         self.group, self.perm, erschema.type)
+        except ValueError:
+            groups.append(self.group)
+            erschema.set_groups(self.perm, groups)
+
+
+class MemSchemaPermCWGroupDel(MemSchemaPermCWGroupAdd):
+    """synchronize schema when a *_permission relation has been deleted from a
+    group
+    """
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        try:
+            erschema = self.schema[self.name]
+        except KeyError:
+            # duh, schema not found, log error and skip operation
+            self.error('no schema for %s', self.name)
+            return
+        groups = list(erschema.get_groups(self.perm))
+        try:
+            groups.remove(self.group)
+            erschema.set_groups(self.perm, groups)
+        except ValueError:
+            self.error('can\'t remove permission %s on %s to group %s',
+                self.perm, erschema.type, self.group)
+
+
+class MemSchemaPermRQLExpressionAdd(MemSchemaPermOperation):
+    """synchronize schema when a *_permission relation has been added on a rql
+    expression
+    """
+    def __init__(self, session, perm, etype_eid, expression):
+        self.expr = expression
+        super(MemSchemaPermRQLExpressionAdd, self).__init__(
+            session, perm, etype_eid)
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        try:
+            erschema = self.schema[self.name]
+        except KeyError:
+            # duh, schema not found, log error and skip operation
+            self.error('no schema for %s', self.name)
+            return
+        exprs = list(erschema.get_rqlexprs(self.perm))
+        exprs.append(erschema.rql_expression(self.expr))
+        erschema.set_rqlexprs(self.perm, exprs)
+
+
+class MemSchemaPermRQLExpressionDel(MemSchemaPermRQLExpressionAdd):
+    """synchronize schema when a *_permission relation has been deleted from an
+    rql expression
+    """
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        try:
+            erschema = self.schema[self.name]
+        except KeyError:
+            # duh, schema not found, log error and skip operation
+            self.error('no schema for %s', self.name)
+            return
+        rqlexprs = list(erschema.get_rqlexprs(self.perm))
+        for i, rqlexpr in enumerate(rqlexprs):
+            if rqlexpr.expression == self.expr:
+                rqlexprs.pop(i)
+                break
+        else:
+            self.error('can\'t remove permission %s on %s for expression %s',
+                self.perm, erschema.type, self.expr)
+            return
+        erschema.set_rqlexprs(self.perm, rqlexprs)
+
+
+class SyncSchemaHook(hook.Hook):
+    __abstract__ = True
+    category = 'syncschema'
+
+# CWEType hooks ################################################################
+
+class DelCWETypeHook(SyncSchemaHook):
+    """before deleting a CWEType entity:
+    * check that we don't remove a core entity type
+    * cascade to delete related CWAttribute and CWRelation entities
+    * instantiate an operation to delete the entity type on commit
+    """
+    __id__ = 'syncdelcwetype'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWEType')
+    events = ('before_delete_entity',)
+
+    def __call__(self):
+        # final entities can't be deleted, don't care about that
+        name = self.entity.name
+        if name in CORE_ETYPES:
+            raise ValidationError(self.entity.eid, {None: self.cw_req._('can\'t be deleted')})
+        # delete every entities of this type
+        self.cw_req.unsafe_execute('DELETE %s X' % name)
+        DropTable(self.cw_req, table=SQL_PREFIX + name)
+        MemSchemaCWETypeDel(self.cw_req, name)
+
+
+class AfterDelCWETypeHook(DelCWETypeHook):
+    __id__ = 'wfcleanup'
+    events = ('after_delete_entity',)
+
+    def __call__(self):
+        # workflow cleanup
+        self.cw_req.execute('DELETE State X WHERE NOT X state_of Y')
+        self.cw_req.execute('DELETE Transition X WHERE NOT X transition_of Y')
+
+
+class AfterAddCWETypeHook(DelCWETypeHook):
+    """after adding a CWEType entity:
+    * create the necessary table
+    * set creation_date and modification_date by creating the necessary
+      CWAttribute entities
+    * add owned_by relation by creating the necessary CWRelation entity
+    * register an operation to add the entity type to the instance's
+      schema on commit
+    """
+    __id__ = 'syncaddcwetype'
+    events = ('before_add_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        if entity.get('final'):
+            return
+        schema = self.cw_req.schema
+        name = entity['name']
+        etype = EntityType(name=name, description=entity.get('description'),
+                           meta=entity.get('meta')) # don't care about final
+        # fake we add it to the schema now to get a correctly initialized schema
+        # but remove it before doing anything more dangerous...
+        schema = self.cw_req.schema
+        eschema = schema.add_entity_type(etype)
+        eschema.set_default_groups()
+        # generate table sql and rql to add metadata
+        tablesql = eschema2sql(self.cw_req.pool.source('system').dbhelper, eschema,
+                               prefix=SQL_PREFIX)
+        relrqls = []
+        for rtype in (META_RTYPES - VIRTUAL_RTYPES):
+            rschema = schema[rtype]
+            sampletype = rschema.subjects()[0]
+            desttype = rschema.objects()[0]
+            props = rschema.rproperties(sampletype, desttype)
+            relrqls += list(ss.rdef2rql(rschema, name, desttype, props))
+        # now remove it !
+        schema.del_entity_type(name)
+        # create the necessary table
+        for sql in tablesql.split(';'):
+            if sql.strip():
+                self.cw_req.system_sql(sql)
+        # register operation to modify the schema on commit
+        # this have to be done before adding other relations definitions
+        # or permission settings
+        etype.eid = entity.eid
+        MemSchemaCWETypeAdd(self.cw_req, etype)
+        # add meta relations
+        for rql, kwargs in relrqls:
+            self.cw_req.execute(rql, kwargs)
+
+
+class BeforeUpdateCWETypeHook(DelCWETypeHook):
+    """check name change, handle final"""
+    __id__ = 'syncupdatecwetype'
+    events = ('before_update_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        check_valid_changes(self.cw_req, entity, ro_attrs=('final',))
+        # don't use getattr(entity, attr), we would get the modified value if any
+        if 'name' in entity.edited_attributes:
+            newname = entity.pop('name')
+            oldname = entity.name
+            if newname.lower() != oldname.lower():
+                SourceDbCWETypeRename(self.cw_req, oldname=oldname, newname=newname)
+                MemSchemaCWETypeRename(self.cw_req, oldname=oldname, newname=newname)
+            entity['name'] = newname
+
+
+# CWRType hooks ################################################################
+
+class DelCWRTypeHook(SyncSchemaHook):
+    """before deleting a CWRType entity:
+    * check that we don't remove a core relation type
+    * cascade to delete related CWAttribute and CWRelation entities
+    * instantiate an operation to delete the relation type on commit
+    """
+    __id__ = 'syncdelcwrtype'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWRType')
+    events = ('before_delete_entity',)
+
+    def __call__(self):
+        name = self.entity.name
+        if name in CORE_ETYPES:
+            raise ValidationError(self.entity.eid, {None: self.cw_req._('can\'t be deleted')})
+        # delete relation definitions using this relation type
+        self.cw_req.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
+                        {'x': self.entity.eid})
+        self.cw_req.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
+                        {'x': self.entity.eid})
+        MemSchemaCWRTypeDel(self.cw_req, name)
+
+
+class AfterAddCWRTypeHook(DelCWRTypeHook):
+    """after a CWRType entity has been added:
+    * register an operation to add the relation type to the instance's
+      schema on commit
+
+    We don't know yet this point if a table is necessary
+    """
+    __id__ = 'syncaddcwrtype'
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        rtype = RelationType(name=entity.name,
+                             description=entity.get('description'),
+                             meta=entity.get('meta', False),
+                             inlined=entity.get('inlined', False),
+                             symetric=entity.get('symetric', False),
+                             eid=entity.eid)
+        MemSchemaCWRTypeAdd(self.cw_req, rtype)
+
+
+class BeforeUpdateCWRTypeHook(DelCWRTypeHook):
+    """check name change, handle final"""
+    __id__ = 'checkupdatecwrtype'
+    events = ('before_update_entity',)
+
+    def __call__(self):
+        check_valid_changes(self.cw_req, self.entity)
+
+
+class AfterUpdateCWRTypeHook(DelCWRTypeHook):
+    __id__ = 'syncupdatecwrtype'
+    events = ('after_update_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        rschema = self.cw_req.schema.rschema(entity.name)
+        newvalues = {}
+        for prop in ('meta', 'symetric', 'inlined'):
+            if prop in entity:
+                newvalues[prop] = entity[prop]
+        if newvalues:
+            MemSchemaCWRTypeUpdate(self.cw_req, rschema=rschema, values=newvalues)
+            SourceDbCWRTypeUpdate(self.cw_req, rschema=rschema, values=newvalues,
+                                  entity=entity)
+
+
+# relation_type hooks ##########################################################
+
+class AfterDelRelationTypeHook(SyncSchemaHook):
+    """before deleting a CWAttribute or CWRelation entity:
+    * if this is a final or inlined relation definition, instantiate an
+      operation to drop necessary column, else if this is the last instance
+      of a non final relation, instantiate an operation to drop necessary
+      table
+    * instantiate an operation to delete the relation definition on commit
+    * delete the associated relation type when necessary
+    """
+    __id__ = 'syncdelrelationtype'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('relation_type')
+    events = ('after_delete_relation',)
+
+    def __call__(self):
+        session = self.cw_req
+        subjschema, rschema, objschema = session.schema.schema_by_eid(self.eidfrom)
+        pendings = session.transaction_data.get('pendingeids', ())
+        # first delete existing relation if necessary
+        if rschema.is_final():
+            rdeftype = 'CWAttribute'
+        else:
+            rdeftype = 'CWRelation'
+            if not (subjschema.eid in pendings or objschema.eid in pendings):
+                pending = session.transaction_data.setdefault('pendingrdefs', set())
+                pending.add((subjschema, rschema, objschema))
+                session.execute('DELETE X %s Y WHERE X is %s, Y is %s'
+                                % (rschema, subjschema, objschema))
+        execute = session.unsafe_execute
+        rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
+                       'R eid %%(x)s' % rdeftype, {'x': rteid})
+        lastrel = rset[0][0] == 0
+        # we have to update physical schema systematically for final and inlined
+        # relations, but only if it's the last instance for this relation type
+        # for other relations
+
+        if (rschema.is_final() or rschema.inlined):
+            rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
+                           'R eid %%(x)s, X from_entity E, E name %%(name)s'
+                           % rdeftype, {'x': rteid, 'name': str(subjschema)})
+            if rset[0][0] == 0 and not subjschema.eid in pendings:
+                ptypes = session.transaction_data.setdefault('pendingrtypes', set())
+                ptypes.add(rschema.type)
+                DropColumn(session, table=SQL_PREFIX + subjschema.type,
+                             column=SQL_PREFIX + rschema.type)
+        elif lastrel:
+            DropRelationTable(session, rschema.type)
+        # if this is the last instance, drop associated relation type
+        if lastrel and not rteid in pendings:
+            execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rteid}, 'x')
+        MemSchemaRDefDel(session, (subjschema, rschema, objschema))
+
+
+# CWAttribute / CWRelation hooks ###############################################
+
+class AfterAddCWAttributeHook(SyncSchemaHook):
+    __id__ = 'syncaddcwattribute'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWAttribute')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        SourceDbCWAttributeAdd(self.cw_req, entity=self.entity)
+
+
+class AfterAddCWRelationHook(AfterAddCWAttributeHook):
+    __id__ = 'syncaddcwrelation'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWRelation')
+
+    def __call__(self):
+        SourceDbCWRelationAdd(self.cw_req, entity=self.entity)
+
+
+class AfterUpdateCWRDefHook(SyncSchemaHook):
+    __id__ = 'syncaddcwattribute'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWAttribute',
+                                                               'CWRelation')
+    events = ('after_update_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        if self.cw_req.deleted_in_transaction(entity.eid):
+            return
+        desttype = entity.otype.name
+        rschema = self.cw_req.schema[entity.rtype.name]
+        newvalues = {}
+        for prop in rschema.rproperty_defs(desttype):
+            if prop == 'constraints':
+                continue
+            if prop == 'order':
+                prop = 'ordernum'
+            if prop in entity.edited_attributes:
+                newvalues[prop] = entity[prop]
+        if newvalues:
+            subjtype = entity.stype.name
+            MemSchemaRDefUpdate(self.cw_req, kobj=(subjtype, desttype),
+                                rschema=rschema, values=newvalues)
+            SourceDbRDefUpdate(self.cw_req, kobj=(subjtype, desttype),
+                               rschema=rschema, values=newvalues)
+
+
+# constraints synchronization hooks ############################################
+
+class AfterAddCWConstraintHook(SyncSchemaHook):
+    __id__ = 'syncaddcwconstraint'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWConstraint')
+    events = ('after_add_entity', 'after_update_entity')
+
+    def __call__(self):
+        MemSchemaCWConstraintAdd(self.cw_req, entity=self.entity)
+        SourceDbCWConstraintAdd(self.cw_req, entity=self.entity)
+
+
+class AfterAddConstrainedByHook(SyncSchemaHook):
+    __id__ = 'syncdelconstrainedby'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrainted_by')
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        if self.cw_req.added_in_transaction(self.eidfrom):
+            self.cw_req.transaction_data.setdefault(self.eidfrom, []).append(self.eidto)
+
+
+class BeforeDeleteConstrainedByHook(AfterAddConstrainedByHook):
+    __id__ = 'syncdelconstrainedby'
+    events = ('before_delete_relation',)
+
+    def __call__(self):
+        if self.cw_req.deleted_in_transaction(self.eidfrom):
+            return
+        schema = self.cw_req.schema
+        entity = self.cw_req.entity_from_eid(self.eidto)
+        subjtype, rtype, objtype = schema.schema_by_eid(self.eidfrom)
+        try:
+            cstr = rtype.constraint_by_type(subjtype, objtype,
+                                            entity.cstrtype[0].name)
+        except IndexError:
+            self.cw_req.critical('constraint type no more accessible')
+        else:
+            SourceDbCWConstraintDel(self.cw_req, subjtype=subjtype, rtype=rtype,
+                                    objtype=objtype, cstr=cstr)
+            MemSchemaCWConstraintDel(self.cw_req, subjtype=subjtype, rtype=rtype,
+                                     objtype=objtype, cstr=cstr)
+
+
+# permissions synchronization hooks ############################################
+
+
+class AfterAddPermissionHook(SyncSchemaHook):
+    """added entity/relation *_permission, need to update schema"""
+    __id__ = 'syncaddperm'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype(
+        'read_permission', 'add_permission', 'delete_permission',
+        'update_permission')
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        perm = self.rtype.split('_', 1)[0]
+        if self.cw_req.describe(self.eidto)[0] == 'CWGroup':
+            MemSchemaPermCWGroupAdd(self.cw_req, perm, self.eidfrom, self.eidto)
+        else: # RQLExpression
+            expr = self.cw_req.entity_from_eid(self.eidto).expression
+            MemSchemaPermRQLExpressionAdd(self.cw_req, perm, self.eidfrom, expr)
+
+
+class BeforeDelPermissionHook(AfterAddPermissionHook):
+    """delete entity/relation *_permission, need to update schema
+
+    skip the operation if the related type is being deleted
+    """
+    __id__ = 'syncdelperm'
+    events = ('before_delete_relation',)
+
+    def __call__(self):
+        if self.cw_req.deleted_in_transaction(self.eidfrom):
+            return
+        perm = self.rtype.split('_', 1)[0]
+        if self.cw_req.describe(self.eidto)[0] == 'CWGroup':
+            MemSchemaPermCWGroupDel(self.cw_req, perm, self.eidfrom, self.eidto)
+        else: # RQLExpression
+            expr = self.cw_req.entity_from_eid(self.eidto).expression
+            MemSchemaPermRQLExpressionDel(self.cw_req, perm, self.eidfrom, expr)
+
+
+
+class ModifySpecializesHook(SyncSchemaHook):
+    __id__ = 'syncspecializes'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('specializes')
+    events = ('after_add_relation', 'after_delete_relation')
+
+    def __call__(self):
+        # registering a schema operation will trigger a call to
+        # repo.set_schema() on commit which will in turn rebuild
+        # infered relation definitions
+        MemSchemaNotifyChanges(self.cw_req)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/syncsession.py	Fri Aug 14 11:14:26 2009 +0200
@@ -0,0 +1,234 @@
+"""Core hooks: synchronize living session on persistent data changes
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb import UnknownProperty, ValidationError, BadConnectionId
+from cubicweb.selectors import entity_implements
+from cubicweb.server import hook
+
+
+def get_user_sessions(repo, ueid):
+    for session in repo._sessions.values():
+        if ueid == session.user.eid:
+            yield session
+
+
+class SyncSessionHook(hook.Hook):
+    __abstract__ = True
+    category = 'syncsession'
+
+
+# user/groups synchronisation #################################################
+
+class _GroupOperation(hook.Operation):
+    """base class for group operation"""
+    geid = None
+    def __init__(self, session, *args, **kwargs):
+        """override to get the group name before actual groups manipulation:
+
+        we may temporarily loose right access during a commit event, so
+        no query should be emitted while comitting
+        """
+        rql = 'Any N WHERE G eid %(x)s, G name N'
+        result = session.execute(rql, {'x': kwargs['geid']}, 'x', build_descr=False)
+        hook.Operation.__init__(self, session, *args, **kwargs)
+        self.group = result[0][0]
+
+
+class _DeleteGroupOp(_GroupOperation):
+    """synchronize user when a in_group relation has been deleted"""
+    def commit_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):
+        """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)
+
+
+class SyncInGroupHook(SyncSessionHook):
+    __id__ = 'syncingroup'
+    __select__ = SyncSessionHook.__select__ & hook.match_rtype('in_group')
+    events = ('after_delete_relation', 'after_add_relation')
+
+    def __call__(self):
+        if self.event == 'after_delete_relation':
+            opcls = _DeleteGroupOp
+        else:
+            opcls = _AddGroupOp
+        for session in get_user_sessions(self.cw_req.repo, self.eidfrom):
+            opcls(self.cw_req, cnxuser=session.user, geid=self.eidto)
+
+
+class _DelUserOp(hook.Operation):
+    """close associated user's session when it is deleted"""
+    def __init__(self, session, cnxid):
+        self.cnxid = cnxid
+        hook.Operation.__init__(self, session)
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        try:
+            self.repo.close(self.cnxid)
+        except BadConnectionId:
+            pass # already closed
+
+
+class CloseDeletedUserSessionsHook(SyncSessionHook):
+    __id__ = 'closession'
+    __select__ = SyncSessionHook.__select__ & entity_implements('CWUser')
+    events = ('after_delete_entity',)
+
+    def __call__(self):
+        """modify user permission, need to update users"""
+        for session in get_user_sessions(self.cw_req.repo, self.entity.eid):
+            _DelUserOp(self.cw_req, session.id)
+
+
+# CWProperty hooks #############################################################
+
+
+class _DelCWPropertyOp(hook.Operation):
+    """a user's custom properties has been deleted"""
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        try:
+            del self.epropdict[self.key]
+        except KeyError:
+            self.error('%s has no associated value', self.key)
+
+
+class _ChangeCWPropertyOp(hook.Operation):
+    """a user's custom properties has been added/changed"""
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        self.epropdict[self.key] = self.value
+
+
+class _AddCWPropertyOp(hook.Operation):
+    """a user's custom properties has been added/changed"""
+
+    def commit_event(self):
+        """the observed connections pool has been commited"""
+        eprop = self.eprop
+        if not eprop.for_user:
+            self.repo.vreg.eprop_values[eprop.pkey] = eprop.value
+        # if for_user is set, update is handled by a ChangeCWPropertyOp operation
+
+
+class AddCWPropertyHook(SyncSessionHook):
+    __id__ = 'addcwprop'
+    __select__ = SyncSessionHook.__select__ & entity_implements('CWProperty')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        key, value = self.entity.pkey, self.entity.value
+        session = self.cw_req
+        try:
+            value = session.vreg.typed_value(key, value)
+        except UnknownProperty:
+            raise ValidationError(self.entity.eid,
+                                  {'pkey': session._('unknown property key')})
+        except ValueError, ex:
+            raise ValidationError(self.entity.eid,
+                                  {'value': session._(str(ex))})
+        if not session.user.matching_groups('managers'):
+            session.add_relation(entity.eid, 'for_user', session.user.eid)
+        else:
+            _AddCWPropertyOp(session, eprop=entity)
+
+
+class UpdateCWPropertyHook(AddCWPropertyHook):
+    __id__ = 'updatecwprop'
+    events = ('after_update_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        if not ('pkey' in entity.edited_attributes or
+                'value' in entity.edited_attributes):
+            return
+        key, value = entity.pkey, entity.value
+        session = self.cw_req
+        try:
+            value = session.vreg.typed_value(key, value)
+        except UnknownProperty:
+            return
+        except ValueError, ex:
+            raise ValidationError(entity.eid, {'value': session._(str(ex))})
+        if entity.for_user:
+            for session_ in get_user_sessions(session.repo, entity.for_user[0].eid):
+                _ChangeCWPropertyOp(session, epropdict=session_.user.properties,
+                                  key=key, value=value)
+        else:
+            # site wide properties
+            _ChangeCWPropertyOp(session, epropdict=session.vreg.eprop_values,
+                              key=key, value=value)
+
+
+class DeleteCWPropertyHook(AddCWPropertyHook):
+    __id__ = 'delcwprop'
+    events = ('before_delete_entity',)
+
+    def __call__(self):
+        eid = self.entity.eid
+        session = self.cw_req
+        for eidfrom, rtype, eidto in session.transaction_data.get('pendingrelations', ()):
+            if rtype == 'for_user' and eidfrom == self.entity.eid:
+                # if for_user was set, delete has already been handled
+                break
+        else:
+            _DelCWPropertyOp(session, epropdict=session.vreg.eprop_values, key=entity.pkey)
+
+
+class AddForUserRelationHook(SyncSessionHook):
+    __id__ = 'addcwpropforuser'
+    __select__ = SyncSessionHook.__select__ & hook.match_rtype('for_user')
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        session = self.cw_req
+        eidfrom = self.eidfrom
+        if not session.describe(eidfrom)[0] == 'CWProperty':
+            return
+        key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
+                                     {'x': eidfrom}, 'x')[0]
+        if session.vreg.property_info(key)['sitewide']:
+            raise ValidationError(eidfrom,
+                                  {'for_user': session._("site-wide property can't be set for user")})
+        for session_ in get_user_sessions(session.repo, self.eidto):
+            _ChangeCWPropertyOp(session, epropdict=session_.user.properties,
+                              key=key, value=value)
+
+
+class DelForUserRelationHook(AddForUserRelationHook):
+    __id__ = 'delcwpropforuser'
+    events = ('after_delete_relation',)
+
+    def __call__(self):
+        session = self.cw_req
+        key = session.execute('Any K WHERE P eid %(x)s, P pkey K',
+                              {'x': self.eidfrom}, 'x')[0][0]
+        session.transaction_data.setdefault('pendingrelations', []).append(
+            (self.eidfrom, self.rtype, self.eidto))
+        for session_ in get_user_sessions(session.repo, self.eidto):
+            _DelCWPropertyOp(session, epropdict=session_.user.properties, key=key)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/workflow.py	Fri Aug 14 11:14:26 2009 +0200
@@ -0,0 +1,146 @@
+"""Core hooks: workflow related hooks
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from datetime import datetime
+
+from cubicweb import RepositoryError, ValidationError
+from cubicweb.interfaces import IWorkflowable
+from cubicweb.selectors import entity_implements
+from cubicweb.server import hook
+
+
+def previous_state(session, eid):
+    """return the state of the entity with the given eid,
+    usually since it's changing in the current transaction. Due to internal
+    relation hooks, the relation may has been deleted at this point, so
+    we have handle that
+    """
+    if session.added_in_transaction(eid):
+        return
+    pending = session.transaction_data.get('pendingrelations', ())
+    for eidfrom, rtype, eidto in reversed(pending):
+        if rtype == 'in_state' and eidfrom == eid:
+            rset = session.execute('Any S,N WHERE S eid %(x)s, S name N',
+                                   {'x': eidto}, 'x')
+            return rset.get_entity(0, 0)
+    rset = session.execute('Any S,N WHERE X eid %(x)s, X in_state S, S name N',
+                           {'x': eid}, 'x')
+    if rset:
+        return rset.get_entity(0, 0)
+
+
+def relation_deleted(session, eidfrom, rtype, eidto):
+    session.transaction_data.setdefault('pendingrelations', []).append(
+        (eidfrom, rtype, eidto))
+
+
+class _SetInitialStateOp(hook.Operation):
+    """make initial state be a default state"""
+
+    def precommit_event(self):
+        session = self.session
+        entity = self.entity
+        # if there is an initial state and the entity's state is not set,
+        # use the initial state as a default state
+        if not session.deleted_in_transaction(entity.eid) and not entity.in_state:
+            rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s',
+                                   {'name': entity.id})
+            if rset:
+                session.add_relation(entity.eid, 'in_state', rset[0][0])
+
+class WorkflowHook(hook.Hook):
+    __abstract__ = True
+    category = 'worfklow'
+
+
+class SetInitialStateHook(WorkflowHook):
+    __id__ = 'wfsetinitial'
+    __select__ = WorkflowHook.__select__ & entity_implements(IWorkflowable)
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        _SetInitialStateOp(self.cw_req, entity=self.entity)
+
+
+class PrepareStateChangeHook(WorkflowHook):
+    """record previous state information"""
+    __id__ = 'cwdelstate'
+    __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
+    events = ('before_delete_relation',)
+
+    def __call__(self):
+        self.cw_req.transaction_data.setdefault('pendingrelations', []).append(
+            (self.eidfrom, self.rtype, self.eidto))
+
+
+class FireTransitionHook(PrepareStateChangeHook):
+    """check the transition is allowed and record transition information"""
+    __id__ = 'wffiretransition'
+    events = ('before_add_relation',)
+
+    def __call__(self):
+        session = self.cw_req
+        eidfrom = self.eidfrom
+        eidto = self.eidto
+        state = previous_state(session, eidfrom)
+        etype = session.describe(eidfrom)[0]
+        if not (session.is_super_session or 'managers' in session.user.groups):
+            if not state is None:
+                entity = session.entity_from_eid(eidfrom)
+                # we should find at least one transition going to this state
+                try:
+                    iter(state.transitions(entity, eidto)).next()
+                except StopIteration:
+                    msg = session._('transition is not allowed')
+                    raise ValidationError(eidfrom, {'in_state': msg})
+            else:
+                # not a transition
+                # check state is initial state if the workflow defines one
+                isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s',
+                                                {'etype': etype})
+                if isrset and not eidto == isrset[0][0]:
+                    msg = session._('not the initial state for this entity')
+                    raise ValidationError(eidfrom, {'in_state': msg})
+        eschema = session.repo.schema[etype]
+        if not 'wf_info_for' in eschema.object_relations():
+            # workflow history not activated for this entity type
+            return
+        rql = 'INSERT TrInfo T: T wf_info_for E, T to_state DS, T comment %(comment)s'
+        args = {'comment': session.get_shared_data('trcomment', None, pop=True),
+                'e': eidfrom, 'ds': eidto}
+        cformat = session.get_shared_data('trcommentformat', None, pop=True)
+        if cformat is not None:
+            args['comment_format'] = cformat
+            rql += ', T comment_format %(comment_format)s'
+        restriction = ['DS eid %(ds)s, E eid %(e)s']
+        if not state is None: # not a transition
+            rql += ', T from_state FS'
+            restriction.append('FS eid %(fs)s')
+            args['fs'] = state.eid
+        rql = '%s WHERE %s' % (rql, ', '.join(restriction))
+        session.unsafe_execute(rql, args, 'e')
+
+
+class SetModificationDateOnStateChange(WorkflowHook):
+    """update entity's modification date after changing its state"""
+    __id__ = 'wfsyncmdate'
+    __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        if self.cw_req.added_in_transaction(self.eidfrom):
+            # new entity, not needed
+            return
+        entity = self.cw_req.entity_from_eid(self.eidfrom)
+        try:
+            entity.set_attributes(modification_date=datetime.now())
+        except RepositoryError, ex:
+            # usually occurs if entity is coming from a read-only source
+            # (eg ldap user)
+            self.warning('cant change modification date for %s: %s', entity, ex)
--- a/rset.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/rset.py	Fri Aug 14 11:14:26 2009 +0200
@@ -422,16 +422,17 @@
         #     new attributes found in this resultset ?
         try:
             entity = req.entity_cache(eid)
-            if entity.rset is None:
-                # entity has no rset set, this means entity has been cached by
-                # the repository (req is a repository session) which had no rset
-                # info. Add id.
-                entity.rset = self
-                entity.row = row
-                entity.col = col
-            return entity
         except KeyError:
             pass
+        else:
+            if entity.rset is None:
+                # entity has no rset set, this means entity has been created by
+                # the querier (req is a repository session) and so jas no rset
+                # info. Add it.
+                entity.cw_rset = self
+                entity.cw_row = row
+                entity.cw_col = col
+            return entity
         # build entity instance
         etype = self.description[row][col]
         entity = self.vreg['etypes'].etype_class(etype)(req, rset=self,
--- a/selectors.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/selectors.py	Fri Aug 14 11:14:26 2009 +0200
@@ -54,6 +54,7 @@
                       role, typed_eid)
 # even if not used, let yes here so it's importable through this module
 from cubicweb.appobject import Selector, objectify_selector, yes
+from cubicweb.vregistry import class_regid
 from cubicweb.cwconfig import CubicWebConfiguration
 from cubicweb.schema import split_expression
 
@@ -74,7 +75,7 @@
         else:
             selname = selector.__name__
             vobj = cls
-        oid = vobj.id
+        oid = class_regid(vobj)
         ret = selector(cls, *args, **kwargs)
         if TRACED_OIDS == 'all' or oid in TRACED_OIDS:
             #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/hook.py	Fri Aug 14 11:14:26 2009 +0200
@@ -0,0 +1,341 @@
+"""Hooks management
+
+This module defined the `Hook` class and registry and a set of abstract classes
+for operations.
+
+
+Hooks are called before / after any individual update of entities / relations
+in the repository and on special events such as server startup or shutdown.
+
+
+Operations may be registered by hooks during a transaction, which will  be
+fired when the pool is commited or rollbacked.
+
+
+Entity hooks (eg before_add_entity, after_add_entity, before_update_entity,
+after_update_entity, before_delete_entity, after_delete_entity) all have an
+`entity` attribute
+
+Relation (eg before_add_relation, after_add_relation, before_delete_relation,
+after_delete_relation) all have `eidfrom`, `rtype`, `eidto` attributes.
+
+Server start/stop hooks (eg server_startup, server_shutdown) have a `repo`
+attribute, but *their `cw_req` attribute is None*.
+
+Backup/restore hooks (eg server_backup, server_restore) have a `repo` and a
+`timestamp` attributes, but *their `cw_req` attribute is None*.
+
+Session hooks (eg session_open, session_close) have no special attribute.
+
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from warnings import warn
+from logging import getLogger
+
+from logilab.common.decorators import classproperty
+from logilab.common.logging_ext import set_log_methods
+
+from cubicweb.cwvreg import CWRegistry, VRegistry
+from cubicweb.selectors import (objectify_selector, lltrace, match_search_state,
+                                entity_implements)
+from cubicweb.appobject import AppObject
+
+
+ENTITIES_HOOKS = set(('before_add_entity',    'after_add_entity',
+                      'before_update_entity', 'after_update_entity',
+                      'before_delete_entity', 'after_delete_entity'))
+RELATIONS_HOOKS = set(('before_add_relation',   'after_add_relation' ,
+                       'before_delete_relation','after_delete_relation'))
+SYSTEM_HOOKS = set(('server_backup', 'server_restore',
+                    'server_startup', 'server_shutdown',
+                    'session_open', 'session_close'))
+ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS
+
+
+class HooksRegistry(CWRegistry):
+
+    def register(self, obj, **kwargs):
+        try:
+            iter(obj.events)
+        except:
+            raise Exception('bad .events attribute %s on %s' % (obj.event, obj))
+        for event in obj.events:
+            if event not in ALL_HOOKS:
+                raise Exception('bad event %s on %s' % (event, obj))
+        super(HooksRegistry, self).register(obj, **kwargs)
+
+    def call_hooks(self, event, req=None, **kwargs):
+        kwargs['event'] = event
+        # XXX remove .enabled
+        for hook in sorted([x for x in self.possible_objects(req, **kwargs)
+                            if x.enabled], key=lambda x: x.order):
+            hook()
+
+VRegistry.REGISTRY_FACTORY['hooks'] = HooksRegistry
+
+
+# some hook specific selectors #################################################
+
+@objectify_selector
+@lltrace
+def match_event(cls, req, **kwargs):
+    if kwargs.get('event') in cls.events:
+        return 1
+    return 0
+
+@objectify_selector
+@lltrace
+def enabled_category(cls, req, **kwargs):
+    if req is None:
+        # server startup / shutdown event
+        config = kwargs['repo'].config
+    else:
+        config = req.vreg.config
+    if enabled_category in config.disabled_hooks_categories:
+        return 0
+    return 1
+
+@objectify_selector
+@lltrace
+def regular_session(cls, req, **kwargs):
+    if req is None or req.is_super_session:
+        return 0
+    return 1
+
+class match_rtype(match_search_state):
+    """accept if parameters specified as initializer arguments are specified
+    in named arguments given to the selector
+
+    :param *expected: parameters (eg `basestring`) which are expected to be
+                      found in named arguments (kwargs)
+    """
+
+    @lltrace
+    def __call__(self, cls, req, *args, **kwargs):
+        return kwargs.get('rtype') in self.expected
+
+
+# base class for hook ##########################################################
+
+class Hook(AppObject):
+    __registry__ = 'hooks'
+    __select__ = match_event() & enabled_category()
+    # set this in derivated classes
+    events = None
+    category = None
+    order = 0
+    # XXX deprecates
+    enabled = True
+
+    @classproperty
+    def __id__(cls):
+        warn('[3.5] %s: please specify an id for your hook' % cls)
+        return str(id(cls))
+
+    @classmethod
+    def __registered__(cls, vreg):
+        super(Hook, cls).__registered__(vreg)
+        if getattr(cls, 'accepts', None):
+            warn('[3.5] %s: accepts is deprecated, define proper __select__' % cls)
+            rtypes = []
+            for ertype in cls.accepts:
+                if ertype.islower():
+                    rtypes.append(ertype)
+                else:
+                    cls.__select__ = cls.__select__ & entity_implements(ertype)
+            if rtypes:
+                cls.__select__ = cls.__select__ & match_rtype(*rtypes)
+        return cls
+
+    known_args = set(('entity', 'rtype', 'eidfrom', 'eidto', 'repo', 'timestamp'))
+    def __init__(self, req, event, **kwargs):
+        for arg in self.known_args:
+            if arg in kwargs:
+                setattr(self, arg, kwargs.pop(arg))
+        super(Hook, self).__init__(req, **kwargs)
+        self.event = event
+
+    def __call__(self):
+        if hasattr(self, 'call'):
+            warn('[3.5] %s: call is deprecated, implements __call__' % self.__class__)
+            if self.event.endswith('_relation'):
+                self.call(self.cw_req, self.eidfrom, self.rtype, self.eidto)
+            elif 'delete' in self.event:
+                self.call(self.cw_req, self.entity.eid)
+            elif self.event.startswith('server_'):
+                self.call(self.repo)
+            elif self.event.startswith('session_'):
+                self.call(self.cw_req)
+            else:
+                self.call(self.cw_req, self.entity)
+
+set_log_methods(Hook, getLogger('cubicweb.hook'))
+
+
+# abstract classes for operation ###############################################
+
+class Operation(object):
+    """an operation is triggered on connections pool events related to
+    commit / rollback transations. Possible events are:
+
+    precommit:
+      the pool is preparing to commit. You shouldn't do anything things which
+      has to be reverted if the commit fail at this point, but you can freely
+      do any heavy computation or raise an exception if the commit can't go.
+      You can add some new operation during this phase but their precommit
+      event won't be triggered
+
+    commit:
+      the pool is preparing to commit. You should avoid to do to expensive
+      stuff or something that may cause an exception in this event
+
+    revertcommit:
+      if an operation failed while commited, this event is triggered for
+      all operations which had their commit event already to let them
+      revert things (including the operation which made fail the commit)
+
+    rollback:
+      the transaction has been either rollbacked either
+      * intentionaly
+      * a precommit event failed, all operations are rollbacked
+      * a commit event failed, all operations which are not been triggered for
+        commit are rollbacked
+
+    order of operations may be important, and is controlled according to:
+    * operation's class
+    """
+
+    def __init__(self, session, **kwargs):
+        self.session = session
+        # XXX deprecates
+        self.user = session.user
+        self.repo = session.repo
+        self.schema = session.repo.schema
+        self.config = session.repo.config
+        # end deprecate
+        self.__dict__.update(kwargs)
+        self.register(session)
+        # execution information
+        self.processed = None # 'precommit', 'commit'
+        self.failed = False
+
+    def register(self, session):
+        session.add_operation(self, self.insert_index())
+
+    def insert_index(self):
+        """return the index of  the lastest instance which is not a
+        LateOperation instance
+        """
+        for i, op in enumerate(self.session.pending_operations):
+            if isinstance(op, (LateOperation, SingleLastOperation)):
+                return i
+        return None
+
+    def handle_event(self, event):
+        """delegate event handling to the opertaion"""
+        getattr(self, event)()
+
+    def precommit_event(self):
+        """the observed connections pool is preparing a commit"""
+
+    def revertprecommit_event(self):
+        """an error went when pre-commiting this operation or a later one
+
+        should revert pre-commit's changes but take care, they may have not
+        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
+
+        do nothing by default, the operation will just be removed from the pool
+        operation list
+        """
+
+set_log_methods(Operation, getLogger('cubicweb.session'))
+
+
+class LateOperation(Operation):
+    """special operation which should be called after all possible (ie non late)
+    operations
+    """
+    def insert_index(self):
+        """return the index of  the lastest instance which is not a
+        SingleLastOperation instance
+        """
+        for i, op in enumerate(self.session.pending_operations):
+            if isinstance(op, SingleLastOperation):
+                return i
+        return None
+
+
+class SingleOperation(Operation):
+    """special operation which should be called once"""
+    def register(self, session):
+        """override register to handle cases where this operation has already
+        been added
+        """
+        operations = session.pending_operations
+        index = self.equivalent_index(operations)
+        if index is not None:
+            equivalent = operations.pop(index)
+        else:
+            equivalent = None
+        session.add_operation(self, self.insert_index())
+        return equivalent
+
+    def equivalent_index(self, operations):
+        """return the index of the equivalent operation if any"""
+        equivalents = [i for i, op in enumerate(operations)
+                       if op.__class__ is self.__class__]
+        if equivalents:
+            return equivalents[0]
+        return None
+
+
+class SingleLastOperation(SingleOperation):
+    """special operation which should be called once and after all other
+    operations
+    """
+    def insert_index(self):
+        return None
+
+
+class SendMailOp(SingleLastOperation):
+    def __init__(self, session, msg=None, recipients=None, **kwargs):
+        # may not specify msg yet, as
+        # `cubicweb.sobjects.supervision.SupervisionMailOp`
+        if msg is not None:
+            assert recipients
+            self.to_send = [(msg, recipients)]
+        else:
+            assert recipients is None
+            self.to_send = []
+        super(SendMailOp, self).__init__(session, **kwargs)
+
+    def register(self, session):
+        previous = super(SendMailOp, self).register(session)
+        if previous:
+            self.to_send = previous.to_send + self.to_send
+
+    def commit_event(self):
+        self.repo.threaded_task(self.sendmails)
+
+    def sendmails(self):
+        self.config.sendmails(self.to_send)
--- a/server/hookhelper.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/server/hookhelper.py	Fri Aug 14 11:14:26 2009 +0200
@@ -7,82 +7,21 @@
 """
 __docformat__ = "restructuredtext en"
 
-from cubicweb import RepositoryError
-from cubicweb.server.pool import SingleLastOperation
-
-
-def entity_name(session, eid):
-    """return the "name" attribute of the entity with the given eid"""
-    return entity_attr(session, eid, 'name')
-
-def entity_attr(session, eid, attr):
-    """return an arbitrary attribute of the entity with the given eid"""
-    return getattr(session.entity_from_eid(eid), attr)
+from logilab.common.deprecation import deprecated, class_moved
 
-def rproperty(session, rtype, eidfrom, eidto, rprop):
-    rschema = session.repo.schema[rtype]
-    subjtype = session.describe(eidfrom)[0]
-    objtype = session.describe(eidto)[0]
-    return rschema.rproperty(subjtype, objtype, rprop)
-
-def check_internal_entity(session, eid, internal_names):
-    """check that the entity's name is not in the internal_names list.
-    raise a RepositoryError if so, else return the entity's name
-    """
-    name = entity_name(session, eid)
-    if name in internal_names:
-        raise RepositoryError('%s entity can\'t be deleted' % name)
-    return name
-
-def get_user_sessions(repo, ueid):
-    for session in repo._sessions.values():
-        if ueid == session.user.eid:
-            yield session
+from cubicweb import RepositoryError
 
 
-# mail related ################################################################
-
-class SendMailOp(SingleLastOperation):
-    def __init__(self, session, msg=None, recipients=None, **kwargs):
-        # may not specify msg yet, as
-        # `cubicweb.sobjects.supervision.SupervisionMailOp`
-        if msg is not None:
-            assert recipients
-            self.to_send = [(msg, recipients)]
-        else:
-            assert recipients is None
-            self.to_send = []
-        super(SendMailOp, self).__init__(session, **kwargs)
-
-    def register(self, session):
-        previous = super(SendMailOp, self).register(session)
-        if previous:
-            self.to_send = previous.to_send + self.to_send
-
-    def commit_event(self):
-        self.repo.threaded_task(self.sendmails)
+@deprecated('[3.5] entity_name is deprecated, use entity.name')
+def entity_name(session, eid):
+    """return the "name" attribute of the entity with the given eid"""
+    return session.entity_from_eid(eid).name
 
-    def sendmails(self):
-        self.config.sendmails(self.to_send)
-
-
-# state related ###############################################################
+@deprecated('[3.5] rproperty is deprecated, use session.schema_rproperty')
+def rproperty(session, rtype, eidfrom, eidto, rprop):
+    return session.rproperty(rtype, eidfrom, eidto, rprop)
 
-def previous_state(session, eid):
-    """return the state of the entity with the given eid,
-    usually since it's changing in the current transaction. Due to internal
-    relation hooks, the relation may has been deleted at this point, so
-    we have handle that
-    """
-    if eid in session.transaction_data.get('neweids', ()):
-        return
-    pending = session.transaction_data.get('pendingrelations', ())
-    for eidfrom, rtype, eidto in reversed(pending):
-        if rtype == 'in_state' and eidfrom == eid:
-            rset = session.execute('Any S,N WHERE S eid %(x)s, S name N',
-                                   {'x': eidto}, 'x')
-            return rset.get_entity(0, 0)
-    rset = session.execute('Any S,N WHERE X eid %(x)s, X in_state S, S name N',
-                           {'x': eid}, 'x')
-    if rset:
-        return rset.get_entity(0, 0)
+from cubicweb.server.hook import SendMailOp
+from cubicweb.hooks.workflow import previous_state
+SendMailOp = class_moved(SendMailOp)
+previous_state = deprecated('[3.5] use cubicweb.hooks.workflow.previous_state')(previous_state)
--- a/server/hooks.py	Fri Aug 14 00:02:08 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,600 +0,0 @@
-"""Core hooks: check schema validity, unsure we are not deleting necessary
-entities...
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-__docformat__ = "restructuredtext en"
-
-from datetime import datetime
-
-from cubicweb import UnknownProperty, ValidationError, BadConnectionId
-
-from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation
-from cubicweb.server.hookhelper import (check_internal_entity, previous_state,
-                                     get_user_sessions, rproperty)
-from cubicweb.server.repository import FTIndexEntityOp
-
-# special relations that don't have to be checked for integrity, usually
-# because they are handled internally by hooks (so we trust ourselves)
-DONT_CHECK_RTYPES_ON_ADD = set(('owned_by', 'created_by',
-                                'is', 'is_instance_of',
-                                'wf_info_for', 'from_state', 'to_state'))
-DONT_CHECK_RTYPES_ON_DEL = set(('is', 'is_instance_of',
-                                'wf_info_for', 'from_state', 'to_state'))
-
-
-def relation_deleted(session, eidfrom, rtype, eidto):
-    session.transaction_data.setdefault('pendingrelations', []).append(
-        (eidfrom, rtype, eidto))
-
-def eschema_type_eid(session, etype):
-    """get eid of the CWEType entity for the given yams type"""
-    eschema = session.repo.schema.eschema(etype)
-    # eschema.eid is None if schema has been readen from the filesystem, not
-    # from the database (eg during tests)
-    if eschema.eid is None:
-        eschema.eid = session.unsafe_execute(
-            'Any X WHERE X is CWEType, X name %(name)s', {'name': etype})[0][0]
-    return eschema.eid
-
-
-# base meta-data handling ######################################################
-
-def setctime_before_add_entity(session, entity):
-    """before create a new entity -> set creation and modification date
-
-    this is a conveniency hook, you shouldn't have to disable it
-    """
-    timestamp = datetime.now()
-    entity.setdefault('creation_date', timestamp)
-    entity.setdefault('modification_date', timestamp)
-    if not session.get_shared_data('do-not-insert-cwuri'):
-        entity.setdefault('cwuri', u'%seid/%s' % (session.base_url(), entity.eid))
-
-
-def setmtime_before_update_entity(session, entity):
-    """update an entity -> set modification date"""
-    entity.setdefault('modification_date', datetime.now())
-
-
-class SetCreatorOp(PreCommitOperation):
-
-    def precommit_event(self):
-        session = self.session
-        if self.entity.eid in session.transaction_data.get('pendingeids', ()):
-            # entity have been created and deleted in the same transaction
-            return
-        if not self.entity.created_by:
-            session.add_relation(self.entity.eid, 'created_by', session.user.eid)
-
-
-def setowner_after_add_entity(session, entity):
-    """create a new entity -> set owner and creator metadata"""
-    asession = session.actual_session()
-    if not asession.is_internal_session:
-        session.add_relation(entity.eid, 'owned_by', asession.user.eid)
-        SetCreatorOp(asession, entity=entity)
-
-
-def setis_after_add_entity(session, entity):
-    """create a new entity -> set is relation"""
-    if hasattr(entity, '_cw_recreating'):
-        return
-    try:
-        session.add_relation(entity.eid, 'is',
-                             eschema_type_eid(session, entity.id))
-    except IndexError:
-        # during schema serialization, skip
-        return
-    # XXX < 2.50 bw compat
-    if not session.get_shared_data('do-not-insert-is_instance_of'):
-        for etype in entity.e_schema.ancestors() + [entity.e_schema]:
-            session.add_relation(entity.eid, 'is_instance_of',
-                                 eschema_type_eid(session, etype))
-
-
-def setowner_after_add_user(session, entity):
-    """when a user has been created, add owned_by relation on itself"""
-    session.add_relation(entity.eid, 'owned_by', entity.eid)
-
-
-def fti_update_after_add_relation(session, eidfrom, rtype, eidto):
-    """sync fulltext index when relevant relation is added. Reindexing the
-    contained entity is enough since it will implicitly reindex the container
-    entity.
-    """
-    ftcontainer = session.repo.schema.rschema(rtype).fulltext_container
-    if ftcontainer == 'subject':
-        FTIndexEntityOp(session, entity=session.entity_from_eid(eidto))
-    elif ftcontainer == 'object':
-        FTIndexEntityOp(session, entity=session.entity_from_eid(eidfrom))
-
-
-def fti_update_after_delete_relation(session, eidfrom, rtype, eidto):
-    """sync fulltext index when relevant relation is deleted. Reindexing both
-    entities is necessary.
-    """
-    if session.repo.schema.rschema(rtype).fulltext_container:
-        FTIndexEntityOp(session, entity=session.entity_from_eid(eidto))
-        FTIndexEntityOp(session, entity=session.entity_from_eid(eidfrom))
-
-
-class SyncOwnersOp(PreCommitOperation):
-
-    def precommit_event(self):
-        self.session.unsafe_execute('SET X owned_by U WHERE C owned_by U, C eid %(c)s,'
-                                    'NOT EXISTS(X owned_by U, X eid %(x)s)',
-                                    {'c': self.compositeeid, 'x': self.composedeid},
-                                    ('c', 'x'))
-
-
-def sync_owner_after_add_composite_relation(session, eidfrom, rtype, eidto):
-    """when adding composite relation, the composed should have the same owners
-    has the composite
-    """
-    if rtype == 'wf_info_for':
-        # skip this special composite relation # XXX (syt) why?
-        return
-    composite = rproperty(session, rtype, eidfrom, eidto, 'composite')
-    if composite == 'subject':
-        SyncOwnersOp(session, compositeeid=eidfrom, composedeid=eidto)
-    elif composite == 'object':
-        SyncOwnersOp(session, compositeeid=eidto, composedeid=eidfrom)
-
-
-def _register_metadata_hooks(hm):
-    """register meta-data related hooks on the hooks manager"""
-    hm.register_hook(setctime_before_add_entity, 'before_add_entity', '')
-    hm.register_hook(setmtime_before_update_entity, 'before_update_entity', '')
-    hm.register_hook(setowner_after_add_entity, 'after_add_entity', '')
-    hm.register_hook(sync_owner_after_add_composite_relation, 'after_add_relation', '')
-    hm.register_hook(fti_update_after_add_relation, 'after_add_relation', '')
-    hm.register_hook(fti_update_after_delete_relation, 'after_delete_relation', '')
-    if 'is' in hm.schema:
-        hm.register_hook(setis_after_add_entity, 'after_add_entity', '')
-    if 'CWUser' in hm.schema:
-        hm.register_hook(setowner_after_add_user, 'after_add_entity', 'CWUser')
-
-
-# core hooks ##################################################################
-
-class DelayedDeleteOp(PreCommitOperation):
-    """delete the object of composite relation except if the relation
-    has actually been redirected to another composite
-    """
-
-    def precommit_event(self):
-        session = self.session
-        # don't do anything if the entity is being created or deleted
-        if not (self.eid in session.transaction_data.get('pendingeids', ()) or
-                self.eid in session.transaction_data.get('neweids', ())):
-            etype = session.describe(self.eid)[0]
-            session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'
-                                   % (etype, self.relation),
-                                   {'x': self.eid}, 'x')
-
-
-def handle_composite_before_del_relation(session, eidfrom, rtype, eidto):
-    """delete the object of composite relation"""
-    composite = rproperty(session, rtype, eidfrom, eidto, 'composite')
-    if composite == 'subject':
-        DelayedDeleteOp(session, eid=eidto, relation='Y %s X' % rtype)
-    elif composite == 'object':
-        DelayedDeleteOp(session, eid=eidfrom, relation='X %s Y' % rtype)
-
-
-def before_del_group(session, eid):
-    """check that we don't remove the owners group"""
-    check_internal_entity(session, eid, ('owners',))
-
-
-# schema validation hooks #####################################################
-
-class CheckConstraintsOperation(LateOperation):
-    """check a new relation satisfy its constraints
-    """
-    def precommit_event(self):
-        eidfrom, rtype, eidto = self.rdef
-        # first check related entities have not been deleted in the same
-        # transaction
-        pending = self.session.transaction_data.get('pendingeids', ())
-        if eidfrom in pending:
-            return
-        if eidto in pending:
-            return
-        for constraint in self.constraints:
-            try:
-                constraint.repo_check(self.session, eidfrom, rtype, eidto)
-            except NotImplementedError:
-                self.critical('can\'t check constraint %s, not supported',
-                              constraint)
-
-    def commit_event(self):
-        pass
-
-
-def cstrcheck_after_add_relation(session, eidfrom, rtype, eidto):
-    """check the relation satisfy its constraints
-
-    this is delayed to a precommit time operation since other relation which
-    will make constraint satisfied may be added later.
-    """
-    constraints = rproperty(session, rtype, eidfrom, eidto, 'constraints')
-    if constraints:
-        CheckConstraintsOperation(session, constraints=constraints,
-                                  rdef=(eidfrom, rtype, eidto))
-
-def uniquecstrcheck_before_modification(session, entity):
-    eschema = entity.e_schema
-    for attr, val in entity.items():
-        if val is None:
-            continue
-        if eschema.subject_relation(attr).is_final() and \
-               eschema.has_unique_values(attr):
-            rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
-            rset = session.unsafe_execute(rql, {'val': val})
-            if rset and rset[0][0] != entity.eid:
-                msg = session._('the value "%s" is already used, use another one')
-                raise ValidationError(entity.eid, {attr: msg % val})
-
-
-class CheckRequiredRelationOperation(LateOperation):
-    """checking relation cardinality has to be done after commit in
-    case the relation is being replaced
-    """
-    eid, rtype = None, None
-
-    def precommit_event(self):
-        # recheck pending eids
-        if self.eid in self.session.transaction_data.get('pendingeids', ()):
-            return
-        if self.session.unsafe_execute(*self._rql()).rowcount < 1:
-            etype = self.session.describe(self.eid)[0]
-            _ = self.session._
-            msg = _('at least one relation %(rtype)s is required on %(etype)s (%(eid)s)')
-            msg %= {'rtype': _(self.rtype), 'etype': _(etype), 'eid': self.eid}
-            raise ValidationError(self.eid, {self.rtype: msg})
-
-    def commit_event(self):
-        pass
-
-    def _rql(self):
-        raise NotImplementedError()
-
-
-class CheckSRelationOp(CheckRequiredRelationOperation):
-    """check required subject relation"""
-    def _rql(self):
-        return 'Any O WHERE S eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
-
-
-class CheckORelationOp(CheckRequiredRelationOperation):
-    """check required object relation"""
-    def _rql(self):
-        return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
-
-
-def checkrel_if_necessary(session, opcls, rtype, eid):
-    """check an equivalent operation has not already been added"""
-    for op in session.pending_operations:
-        if isinstance(op, opcls) and op.rtype == rtype and op.eid == eid:
-            break
-    else:
-        opcls(session, rtype=rtype, eid=eid)
-
-
-def cardinalitycheck_after_add_entity(session, entity):
-    """check cardinalities are satisfied"""
-    eid = entity.eid
-    for rschema, targetschemas, x in entity.e_schema.relation_definitions():
-        # skip automatically handled relations
-        if rschema.type in DONT_CHECK_RTYPES_ON_ADD:
-            continue
-        if x == 'subject':
-            subjtype = entity.e_schema
-            objtype = targetschemas[0].type
-            cardindex = 0
-            opcls = CheckSRelationOp
-        else:
-            subjtype = targetschemas[0].type
-            objtype = entity.e_schema
-            cardindex = 1
-            opcls = CheckORelationOp
-        card = rschema.rproperty(subjtype, objtype, 'cardinality')
-        if card[cardindex] in '1+':
-            checkrel_if_necessary(session, opcls, rschema.type, eid)
-
-
-def cardinalitycheck_before_del_relation(session, eidfrom, rtype, eidto):
-    """check cardinalities are satisfied"""
-    if rtype in DONT_CHECK_RTYPES_ON_DEL:
-        return
-    card = rproperty(session, rtype, eidfrom, eidto, 'cardinality')
-    pendingrdefs = session.transaction_data.get('pendingrdefs', ())
-    if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
-        return
-    pendingeids = session.transaction_data.get('pendingeids', ())
-    if card[0] in '1+' and not eidfrom in pendingeids:
-        checkrel_if_necessary(session, CheckSRelationOp, rtype, eidfrom)
-    if card[1] in '1+' and not eidto in pendingeids:
-        checkrel_if_necessary(session, CheckORelationOp, rtype, eidto)
-
-
-def _register_core_hooks(hm):
-    hm.register_hook(handle_composite_before_del_relation, 'before_delete_relation', '')
-    hm.register_hook(before_del_group, 'before_delete_entity', 'CWGroup')
-
-    #hm.register_hook(cstrcheck_before_update_entity, 'before_update_entity', '')
-    hm.register_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '')
-    hm.register_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '')
-    hm.register_hook(cstrcheck_after_add_relation, 'after_add_relation', '')
-    hm.register_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
-    hm.register_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
-
-
-# user/groups synchronisation #################################################
-
-class GroupOperation(Operation):
-    """base class for group operation"""
-    geid = None
-    def __init__(self, session, *args, **kwargs):
-        """override to get the group name before actual groups manipulation:
-
-        we may temporarily loose right access during a commit event, so
-        no query should be emitted while comitting
-        """
-        rql = 'Any N WHERE G eid %(x)s, G name N'
-        result = session.execute(rql, {'x': kwargs['geid']}, 'x', build_descr=False)
-        Operation.__init__(self, session, *args, **kwargs)
-        self.group = result[0][0]
-
-
-class DeleteGroupOp(GroupOperation):
-    """synchronize user when a in_group relation has been deleted"""
-    def commit_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
-
-
-def after_del_in_group(session, fromeid, rtype, toeid):
-    """modify user permission, need to update users"""
-    for session_ in get_user_sessions(session.repo, fromeid):
-        DeleteGroupOp(session, cnxuser=session_.user, geid=toeid)
-
-
-class AddGroupOp(GroupOperation):
-    """synchronize user when a in_group relation has been added"""
-    def commit_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)
-
-
-def after_add_in_group(session, fromeid, rtype, toeid):
-    """modify user permission, need to update users"""
-    for session_ in get_user_sessions(session.repo, fromeid):
-        AddGroupOp(session, cnxuser=session_.user, geid=toeid)
-
-
-class DelUserOp(Operation):
-    """synchronize user when a in_group relation has been added"""
-    def __init__(self, session, cnxid):
-        self.cnxid = cnxid
-        Operation.__init__(self, session)
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        try:
-            self.repo.close(self.cnxid)
-        except BadConnectionId:
-            pass # already closed
-
-
-def after_del_user(session, eid):
-    """modify user permission, need to update users"""
-    for session_ in get_user_sessions(session.repo, eid):
-        DelUserOp(session, session_.id)
-
-
-def _register_usergroup_hooks(hm):
-    """register user/group related hooks on the hooks manager"""
-    hm.register_hook(after_del_user, 'after_delete_entity', 'CWUser')
-    hm.register_hook(after_add_in_group, 'after_add_relation', 'in_group')
-    hm.register_hook(after_del_in_group, 'after_delete_relation', 'in_group')
-
-
-# workflow handling ###########################################################
-
-def before_add_in_state(session, fromeid, rtype, toeid):
-    """check the transition is allowed and record transition information
-    """
-    assert rtype == 'in_state'
-    state = previous_state(session, fromeid)
-    etype = session.describe(fromeid)[0]
-    if not (session.is_super_session or 'managers' in session.user.groups):
-        if not state is None:
-            entity = session.entity_from_eid(fromeid)
-            # we should find at least one transition going to this state
-            try:
-                iter(state.transitions(entity, toeid)).next()
-            except StopIteration:
-                msg = session._('transition is not allowed')
-                raise ValidationError(fromeid, {'in_state': msg})
-        else:
-            # not a transition
-            # check state is initial state if the workflow defines one
-            isrset = session.unsafe_execute('Any S WHERE ET initial_state S, ET name %(etype)s',
-                                            {'etype': etype})
-            if isrset and not toeid == isrset[0][0]:
-                msg = session._('not the initial state for this entity')
-                raise ValidationError(fromeid, {'in_state': msg})
-    eschema = session.repo.schema[etype]
-    if not 'wf_info_for' in eschema.object_relations():
-        # workflow history not activated for this entity type
-        return
-    rql = 'INSERT TrInfo T: T wf_info_for E, T to_state DS, T comment %(comment)s'
-    args = {'comment': session.get_shared_data('trcomment', None, pop=True),
-            'e': fromeid, 'ds': toeid}
-    cformat = session.get_shared_data('trcommentformat', None, pop=True)
-    if cformat is not None:
-        args['comment_format'] = cformat
-        rql += ', T comment_format %(comment_format)s'
-    restriction = ['DS eid %(ds)s, E eid %(e)s']
-    if not state is None: # not a transition
-        rql += ', T from_state FS'
-        restriction.append('FS eid %(fs)s')
-        args['fs'] = state.eid
-    rql = '%s WHERE %s' % (rql, ', '.join(restriction))
-    session.unsafe_execute(rql, args, 'e')
-
-
-class SetInitialStateOp(PreCommitOperation):
-    """make initial state be a default state"""
-
-    def precommit_event(self):
-        session = self.session
-        entity = self.entity
-        # if there is an initial state and the entity's state is not set,
-        # use the initial state as a default state
-        pendingeids = session.transaction_data.get('pendingeids', ())
-        if not entity.eid in pendingeids and not entity.in_state:
-            rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s',
-                                   {'name': entity.id})
-            if rset:
-                session.add_relation(entity.eid, 'in_state', rset[0][0])
-
-
-def set_initial_state_after_add(session, entity):
-    SetInitialStateOp(session, entity=entity)
-
-
-def _register_wf_hooks(hm):
-    """register workflow related hooks on the hooks manager"""
-    if 'in_state' in hm.schema:
-        hm.register_hook(before_add_in_state, 'before_add_relation', 'in_state')
-        hm.register_hook(relation_deleted, 'before_delete_relation', 'in_state')
-        for eschema in hm.schema.entities():
-            if 'in_state' in eschema.subject_relations():
-                hm.register_hook(set_initial_state_after_add, 'after_add_entity',
-                                 str(eschema))
-
-
-# CWProperty hooks #############################################################
-
-
-class DelCWPropertyOp(Operation):
-    """a user's custom properties has been deleted"""
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        try:
-            del self.epropdict[self.key]
-        except KeyError:
-            self.error('%s has no associated value', self.key)
-
-
-class ChangeCWPropertyOp(Operation):
-    """a user's custom properties has been added/changed"""
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        self.epropdict[self.key] = self.value
-
-
-class AddCWPropertyOp(Operation):
-    """a user's custom properties has been added/changed"""
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        eprop = self.eprop
-        if not eprop.for_user:
-            self.repo.vreg.eprop_values[eprop.pkey] = eprop.value
-        # if for_user is set, update is handled by a ChangeCWPropertyOp operation
-
-
-def after_add_eproperty(session, entity):
-    key, value = entity.pkey, entity.value
-    try:
-        value = session.vreg.typed_value(key, value)
-    except UnknownProperty:
-        raise ValidationError(entity.eid, {'pkey': session._('unknown property key')})
-    except ValueError, ex:
-        raise ValidationError(entity.eid, {'value': session._(str(ex))})
-    if not session.user.matching_groups('managers'):
-        session.add_relation(entity.eid, 'for_user', session.user.eid)
-    else:
-        AddCWPropertyOp(session, eprop=entity)
-
-
-def after_update_eproperty(session, entity):
-    if not ('pkey' in entity.edited_attributes or
-            'value' in entity.edited_attributes):
-        return
-    key, value = entity.pkey, entity.value
-    try:
-        value = session.vreg.typed_value(key, value)
-    except UnknownProperty:
-        return
-    except ValueError, ex:
-        raise ValidationError(entity.eid, {'value': session._(str(ex))})
-    if entity.for_user:
-        for session_ in get_user_sessions(session.repo, entity.for_user[0].eid):
-            ChangeCWPropertyOp(session, epropdict=session_.user.properties,
-                              key=key, value=value)
-    else:
-        # site wide properties
-        ChangeCWPropertyOp(session, epropdict=session.vreg.eprop_values,
-                          key=key, value=value)
-
-
-def before_del_eproperty(session, eid):
-    for eidfrom, rtype, eidto in session.transaction_data.get('pendingrelations', ()):
-        if rtype == 'for_user' and eidfrom == eid:
-            # if for_user was set, delete has already been handled
-            break
-    else:
-        key = session.execute('Any K WHERE P eid %(x)s, P pkey K',
-                              {'x': eid}, 'x')[0][0]
-        DelCWPropertyOp(session, epropdict=session.vreg.eprop_values, key=key)
-
-
-def after_add_for_user(session, fromeid, rtype, toeid):
-    if not session.describe(fromeid)[0] == 'CWProperty':
-        return
-    key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
-                                 {'x': fromeid}, 'x')[0]
-    if session.vreg.property_info(key)['sitewide']:
-        raise ValidationError(fromeid,
-                              {'for_user': session._("site-wide property can't be set for user")})
-    for session_ in get_user_sessions(session.repo, toeid):
-        ChangeCWPropertyOp(session, epropdict=session_.user.properties,
-                          key=key, value=value)
-
-
-def before_del_for_user(session, fromeid, rtype, toeid):
-    key = session.execute('Any K WHERE P eid %(x)s, P pkey K',
-                          {'x': fromeid}, 'x')[0][0]
-    relation_deleted(session, fromeid, rtype, toeid)
-    for session_ in get_user_sessions(session.repo, toeid):
-        DelCWPropertyOp(session, epropdict=session_.user.properties, key=key)
-
-
-def _register_eproperty_hooks(hm):
-    """register workflow related hooks on the hooks manager"""
-    hm.register_hook(after_add_eproperty, 'after_add_entity', 'CWProperty')
-    hm.register_hook(after_update_eproperty, 'after_update_entity', 'CWProperty')
-    hm.register_hook(before_del_eproperty, 'before_delete_entity', 'CWProperty')
-    hm.register_hook(after_add_for_user, 'after_add_relation', 'for_user')
-    hm.register_hook(before_del_for_user, 'before_delete_relation', 'for_user')
--- a/server/hooksmanager.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/server/hooksmanager.py	Fri Aug 14 11:14:26 2009 +0200
@@ -1,270 +1,4 @@
-"""Hooks management
-
-Hooks are called before / after any individual update of entities / relations
-in the repository.
-
-Here is the prototype of the different hooks:
-
-* filtered on the entity's type:
-
-  before_add_entity    (session, entity)
-  after_add_entity     (session, entity)
-  before_update_entity (session, entity)
-  after_update_entity  (session, entity)
-  before_delete_entity (session, eid)
-  after_delete_entity  (session, eid)
-
-* filtered on the relation's type:
-
-  before_add_relation    (session, fromeid, rtype, toeid)
-  after_add_relation     (session, fromeid, rtype, toeid)
-  before_delete_relation (session, fromeid, rtype, toeid)
-  after_delete_relation  (session, fromeid, rtype, toeid)
-
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-__docformat__ = "restructuredtext en"
-
-ENTITIES_HOOKS = ('before_add_entity',    'after_add_entity',
-                  'before_update_entity', 'after_update_entity',
-                  'before_delete_entity', 'after_delete_entity')
-RELATIONS_HOOKS = ('before_add_relation',   'after_add_relation' ,
-                   'before_delete_relation','after_delete_relation')
-SYSTEM_HOOKS = ('server_backup', 'server_restore',
-                'server_startup', 'server_shutdown',
-                'session_open', 'session_close')
-
-ALL_HOOKS = frozenset(ENTITIES_HOOKS + RELATIONS_HOOKS + SYSTEM_HOOKS)
-
-class HooksManager(object):
-    """handle hooks registration and calls
-    """
-    verification_hooks_activated = True
-
-    def __init__(self, schema):
-        self.set_schema(schema)
-
-    def set_schema(self, schema):
-        self._hooks = {}
-        self.schema = schema
-        self._init_hooks(schema)
-
-    def register_hooks(self, hooks):
-        """register a dictionary of hooks :
-
-             {'event': {'entity or relation type': [callbacks list]}}
-        """
-        for event, subevents in hooks.items():
-            for subevent, callbacks in subevents.items():
-                for callback in callbacks:
-                    self.register_hook(callback, event, subevent)
-
-    def register_hook(self, function, event, etype=''):
-        """register a function to call when <event> occurs
-
-        <etype> is an entity/relation type or an empty string.
-
-        If etype is the empty string, the function will be called at each event,
-        else the function will be called only when event occurs on an entity or
-        relation of the given type.
-        """
-        assert event in ALL_HOOKS, '%r NOT IN %r' % (event, ALL_HOOKS)
-        assert (not event in SYSTEM_HOOKS or not etype), (event, etype)
-        etype = etype or ''
-        try:
-            self._hooks[event][etype].append(function)
-            self.debug('registered hook %s on %s (%s)', event, etype or 'any',
-                       function.func_name)
-
-        except KeyError:
-            self.error('can\'t register hook %s on %s (%s)',
-                       event, etype or 'any', function.func_name)
-
-    def unregister_hook(self, function_or_cls, event=None, etype=''):
-        """unregister a function to call when <event> occurs, or a Hook subclass.
-        In the later case, event/type information are extracted from the given
-        class.
-        """
-        if isinstance(function_or_cls, type) and issubclass(function_or_cls, Hook):
-            for event, ertype in function_or_cls.register_to(self.schema):
-                for hook in self._hooks[event][ertype]:
-                    if getattr(hook, 'im_self', None).__class__ is function_or_cls:
-                        self._hooks[event][ertype].remove(hook)
-                        self.info('unregister hook %s on %s (%s)', event, etype,
-                                  function_or_cls.__name__)
-                        break
-                else:
-                    self.warning("can't unregister hook %s on %s (%s), not found",
-                                 event, etype, function_or_cls.__name__)
-        else:
-            assert event in ALL_HOOKS, event
-            etype = etype or ''
-            self.info('unregister hook %s on %s (%s)', event, etype,
-                      function_or_cls.func_name)
-            self._hooks[event][etype].remove(function_or_cls)
-
-    def call_hooks(self, __event, __type='', *args, **kwargs):
-        """call hook matching event and optional type"""
-        if __type:
-            self.info('calling hooks for event %s (%s)', __event, __type)
-        else:
-            self.info('calling hooks for event %s', __event)
-        # call generic hooks first
-        for hook in self._hooks[__event]['']:
-            #print '[generic]', hook.__name__
-            hook(*args, **kwargs)
-        if __type:
-            for hook in self._hooks[__event][__type]:
-                #print '[%s]'%__type, hook.__name__
-                hook(*args, **kwargs)
-
-    def _init_hooks(self, schema):
-        """initialize the hooks map"""
-        for hook_event in ENTITIES_HOOKS:
-            self._hooks[hook_event] = {'': []}
-            for etype in schema.entities():
-                self._hooks[hook_event][etype] = []
-        for hook_event in RELATIONS_HOOKS:
-            self._hooks[hook_event] = {'': []}
-            for r_type in schema.relations():
-                self._hooks[hook_event][r_type] = []
-        for hook_event in SYSTEM_HOOKS:
-            self._hooks[hook_event] = {'': []}
-
-    def register_system_hooks(self, config):
-        """register system hooks according to the configuration"""
-        self.info('register core hooks')
-        from cubicweb.server.hooks import _register_metadata_hooks, _register_wf_hooks
-        _register_metadata_hooks(self)
-        self.info('register workflow hooks')
-        _register_wf_hooks(self)
-        if config.core_hooks:
-            from cubicweb.server.hooks import _register_core_hooks
-            _register_core_hooks(self)
-        if config.schema_hooks:
-            from cubicweb.server.schemahooks import _register_schema_hooks
-            self.info('register schema hooks')
-            _register_schema_hooks(self)
-        if config.usergroup_hooks:
-            from cubicweb.server.hooks import _register_usergroup_hooks
-            from cubicweb.server.hooks import _register_eproperty_hooks
-            self.info('register user/group hooks')
-            _register_usergroup_hooks(self)
-            _register_eproperty_hooks(self)
-        if config.security_hooks:
-            from cubicweb.server.securityhooks import register_security_hooks
-            self.info('register security hooks')
-            register_security_hooks(self)
-        if not self.verification_hooks_activated:
-            self.deactivate_verification_hooks()
-
-    def deactivate_verification_hooks(self):
-        from cubicweb.server.hooks import (cardinalitycheck_after_add_entity,
-                                        cardinalitycheck_before_del_relation,
-                                        cstrcheck_after_add_relation,
-                                        uniquecstrcheck_before_modification)
-        self.warning('deactivating verification hooks')
-        self.verification_hooks_activated = False
-        self.unregister_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '')
-        self.unregister_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '')
-        self.unregister_hook(cstrcheck_after_add_relation, 'after_add_relation', '')
-        self.unregister_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
-        self.unregister_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
-#         self.unregister_hook(tidy_html_fields('before_add_entity'), 'before_add_entity', '')
-#         self.unregister_hook(tidy_html_fields('before_update_entity'), 'before_update_entity', '')
-
-    def reactivate_verification_hooks(self):
-        from cubicweb.server.hooks import (cardinalitycheck_after_add_entity,
-                                        cardinalitycheck_before_del_relation,
-                                        cstrcheck_after_add_relation,
-                                        uniquecstrcheck_before_modification)
-        self.warning('reactivating verification hooks')
-        self.verification_hooks_activated = True
-        self.register_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '')
-        self.register_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '')
-        self.register_hook(cstrcheck_after_add_relation, 'after_add_relation', '')
-        self.register_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
-        self.register_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
-#         self.register_hook(tidy_html_fields('before_add_entity'), 'before_add_entity', '')
-#         self.register_hook(tidy_html_fields('before_update_entity'), 'before_update_entity', '')
-
-from cubicweb.selectors import yes
-from cubicweb.appobject import AppObject
-
-class autoid(type):
-    """metaclass to create an unique 'id' attribute on the class using it"""
-    # XXX is this metaclass really necessary ?
-    def __new__(mcs, name, bases, classdict):
-        cls = super(autoid, mcs).__new__(mcs, name, bases, classdict)
-        cls.id = str(id(cls))
-        return cls
-
-class Hook(AppObject):
-    __metaclass__ = autoid
-    __registry__ = 'hooks'
-    __select__ = yes()
-    # set this in derivated classes
-    events = None
-    accepts = None
-    enabled = True
-
-    def __init__(self, event=None):
-        super(Hook, self).__init__(None)
-        self.event = event
-
-    @classmethod
-    def __registered__(cls, vreg):
-        super(Hook, cls).__registered__(vreg)
-        return cls()
-
-    @classmethod
-    def register_to(cls, schema):
-        if not cls.enabled:
-            cls.warning('%s hook has been disabled', cls)
-            return
-        done = set()
-        assert isinstance(cls.events, (tuple, list)), \
-               '%s: events is expected to be a tuple, not %s' % (
-            cls, type(cls.events))
-        for event in cls.events:
-            if event in SYSTEM_HOOKS:
-                assert not cls.accepts or cls.accepts == ('Any',), \
-                       '%s doesnt make sense on %s' % (cls.accepts, event)
-                cls.accepts = ('Any',)
-            for ertype in cls.accepts:
-                if (event, ertype) in done:
-                    continue
-                yield event, ertype
-                done.add((event, ertype))
-                try:
-                    eschema = schema.eschema(ertype)
-                except KeyError:
-                    # relation schema
-                    pass
-                else:
-                    for eetype in eschema.specialized_by():
-                        if (event, eetype) in done:
-                            continue
-                        yield event, str(eetype)
-                        done.add((event, eetype))
-
-
-    def make_callback(self, event):
-        if len(self.events) == 1:
-            return self.call
-        return self.__class__(event=event).call
-
-    def call(self):
-        raise NotImplementedError
-
-class SystemHook(Hook):
-    accepts = ()
-
-from logging import getLogger
-from cubicweb import set_log_methods
-set_log_methods(HooksManager, getLogger('cubicweb.hooksmanager'))
-set_log_methods(Hook, getLogger('cubicweb.hooks'))
+from logilab.common.deprecation import class_renamed, class_moved
+from cubicweb.server.hook import Hook
+SystemHook = class_renamed('SystemHook', Hook)
+Hook = class_moved(Hook)
--- a/server/migractions.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/server/migractions.py	Fri Aug 14 11:14:26 2009 +0200
@@ -276,7 +276,7 @@
                 from cubicweb.server.hooks import setowner_after_add_entity
                 self.repo.hm.unregister_hook(setowner_after_add_entity,
                                              'after_add_entity', '')
-                self.deactivate_verification_hooks()
+                self.cmd_deactivate_verification_hooks()
             self.info('executing %s', apc)
             confirm = self.confirm
             execscript_confirm = self.execscript_confirm
@@ -290,7 +290,7 @@
                 if self.config.free_wheel:
                     self.repo.hm.register_hook(setowner_after_add_entity,
                                                'after_add_entity', '')
-                    self.reactivate_verification_hooks()
+                    self.cmd_reactivate_verification_hooks()
 
     # schema synchronization internals ########################################
 
@@ -1073,10 +1073,10 @@
         return ForRqlIterator(self, rql, None, ask_confirm)
 
     def cmd_deactivate_verification_hooks(self):
-        self.repo.hm.deactivate_verification_hooks()
+        self.config.disabled_hooks_categories.add('integrity')
 
     def cmd_reactivate_verification_hooks(self):
-        self.repo.hm.reactivate_verification_hooks()
+        self.config.disabled_hooks_categories.remove('integrity')
 
     # broken db commands ######################################################
 
--- a/server/pool.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/server/pool.py	Fri Aug 14 11:14:26 2009 +0200
@@ -1,13 +1,7 @@
-"""CubicWeb server connections pool :
-
-* the rql repository has a limited number of connections pools, each of them
-  dealing with a set of connections on each source used by the repository
-
-* operation may be registered by hooks during a transaction, which will  be
-  fired when the pool is commited or rollbacked
-
-This module defined the `ConnectionsPool` class and a set of abstract classes
-for operation.
+"""CubicWeb server connections pool : the repository has a limited number of
+connections pools, each of them dealing with a set of connections on each source
+used by the repository. A connections pools (`ConnectionsPool`) is an
+abstraction for a group of connection to each source.
 
 
 :organization: Logilab
@@ -129,152 +123,11 @@
         self.source_cnxs[source.uri] = (source, cnx)
         self._cursors.pop(source.uri, None)
 
-
-class Operation(object):
-    """an operation is triggered on connections pool events related to
-    commit / rollback transations. Possible events are:
-
-    precommit:
-      the pool is preparing to commit. You shouldn't do anything things which
-      has to be reverted if the commit fail at this point, but you can freely
-      do any heavy computation or raise an exception if the commit can't go.
-      You can add some new operation during this phase but their precommit
-      event won't be triggered
-
-    commit:
-      the pool is preparing to commit. You should avoid to do to expensive
-      stuff or something that may cause an exception in this event
-
-    revertcommit:
-      if an operation failed while commited, this event is triggered for
-      all operations which had their commit event already to let them
-      revert things (including the operation which made fail the commit)
-
-    rollback:
-      the transaction has been either rollbacked either
-      * intentionaly
-      * a precommit event failed, all operations are rollbacked
-      * a commit event failed, all operations which are not been triggered for
-        commit are rollbacked
-
-    order of operations may be important, and is controlled according to:
-    * operation's class
-    """
-
-    def __init__(self, session, **kwargs):
-        self.session = session
-        self.user = session.user
-        self.repo = session.repo
-        self.schema = session.repo.schema
-        self.config = session.repo.config
-        self.__dict__.update(kwargs)
-        self.register(session)
-        # execution information
-        self.processed = None # 'precommit', 'commit'
-        self.failed = False
-
-    def register(self, session):
-        session.add_operation(self, self.insert_index())
-
-    def insert_index(self):
-        """return the index of  the lastest instance which is not a
-        LateOperation instance
-        """
-        for i, op in enumerate(self.session.pending_operations):
-            if isinstance(op, (LateOperation, SingleLastOperation)):
-                return i
-        return None
-
-    def handle_event(self, event):
-        """delegate event handling to the opertaion"""
-        getattr(self, event)()
-
-    def precommit_event(self):
-        """the observed connections pool is preparing a commit"""
-
-    def revertprecommit_event(self):
-        """an error went when pre-commiting this operation or a later one
-
-        should revert pre-commit's changes but take care, they may have not
-        been all considered if it's this operation which failed
-        """
-
-    def commit_event(self):
-        """the observed connections pool is commiting"""
-        raise NotImplementedError()
-
-    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
-
-        do nothing by default, the operation will just be removed from the pool
-        operation list
-        """
-
-
-class PreCommitOperation(Operation):
-    """base class for operation only defining a precommit operation
-    """
-
-    def precommit_event(self):
-        """the observed connections pool is preparing a commit"""
-        raise NotImplementedError()
-
-    def commit_event(self):
-        """the observed connections pool is commiting"""
-
-
-class LateOperation(Operation):
-    """special operation which should be called after all possible (ie non late)
-    operations
-    """
-    def insert_index(self):
-        """return the index of  the lastest instance which is not a
-        SingleLastOperation instance
-        """
-        for i, op in enumerate(self.session.pending_operations):
-            if isinstance(op, SingleLastOperation):
-                return i
-        return None
-
-
-class SingleOperation(Operation):
-    """special operation which should be called once"""
-    def register(self, session):
-        """override register to handle cases where this operation has already
-        been added
-        """
-        operations = session.pending_operations
-        index = self.equivalent_index(operations)
-        if index is not None:
-            equivalent = operations.pop(index)
-        else:
-            equivalent = None
-        session.add_operation(self, self.insert_index())
-        return equivalent
-
-    def equivalent_index(self, operations):
-        """return the index of the equivalent operation if any"""
-        equivalents = [i for i, op in enumerate(operations)
-                       if op.__class__ is self.__class__]
-        if equivalents:
-            return equivalents[0]
-        return None
-
-
-class SingleLastOperation(SingleOperation):
-    """special operation which should be called once and after all other
-    operations
-    """
-    def insert_index(self):
-        return None
-
-from logging import getLogger
-from cubicweb import set_log_methods
-set_log_methods(Operation, getLogger('cubicweb.session'))
+from cubicweb.server.hook import (Operation, LateOperation, SingleOperation,
+                                  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/repository.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/server/repository.py	Fri Aug 14 11:14:26 2009 +0200
@@ -33,19 +33,12 @@
                       ETypeNotSupportedBySources, RTypeNotSupportedBySources,
                       BadConnectionId, Unauthorized, ValidationError,
                       typed_eid)
-from cubicweb.cwvreg import CubicWebVRegistry
-from cubicweb.schema import VIRTUAL_RTYPES, CubicWebSchema
-from cubicweb import server
-from cubicweb.server.utils import RepoThread, LoopTask
-from cubicweb.server.pool import ConnectionsPool, LateOperation, SingleLastOperation
+from cubicweb import cwvreg, schema, server
+from cubicweb.server import utils, hook, pool, querier, sources
 from cubicweb.server.session import Session, InternalSession
-from cubicweb.server.querier import QuerierHelper
-from cubicweb.server.sources import get_source
-from cubicweb.server.hooksmanager import HooksManager
-from cubicweb.server.hookhelper import rproperty
 
 
-class CleanupEidTypeCacheOp(SingleLastOperation):
+class CleanupEidTypeCacheOp(hook.SingleLastOperation):
     """on rollback of a insert query or commit of delete query, we have to
     clear repository's cache from no more valid entries
 
@@ -75,7 +68,7 @@
             pass
 
 
-class FTIndexEntityOp(LateOperation):
+class FTIndexEntityOp(hook.LateOperation):
     """operation to delay entity full text indexation to commit
 
     since fti indexing may trigger discovery of other entities, it should be
@@ -108,7 +101,7 @@
     # hooks responsability to ensure they do not violate relation's cardinality
     if session.is_super_session:
         return
-    card = rproperty(session, rtype, eidfrom, eidto, 'cardinality')
+    card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
     # one may be tented to check for neweids but this may cause more than one
     # relation even with '1?'  cardinality if thoses relations are added in the
     # same transaction where the entity is being created. This never occurs from
@@ -137,7 +130,7 @@
     def __init__(self, config, vreg=None, debug=False):
         self.config = config
         if vreg is None:
-            vreg = CubicWebVRegistry(config, debug)
+            vreg = cwvreg.CubicWebVRegistry(config, debug)
         self.vreg = vreg
         self.pyro_registered = False
         self.info('starting repository from %s', self.config.apphome)
@@ -148,9 +141,9 @@
         # list of running threads
         self._running_threads = []
         # initial schema, should be build or replaced latter
-        self.schema = CubicWebSchema(config.appid)
+        self.schema = schema.CubicWebSchema(config.appid)
         # querier helper, need to be created after sources initialization
-        self.querier = QuerierHelper(self, self.schema)
+        self.querier = querier.QuerierHelper(self, self.schema)
         # should we reindex in changes?
         self.do_fti = not config['delay-full-text-indexation']
         # sources
@@ -173,11 +166,9 @@
         self._type_source_cache = {}
         # cache (extid, source uri) -> eid
         self._extid_cache = {}
-        # create the hooks manager
-        self.hm = HooksManager(self.schema)
         # open some connections pools
         self._available_pools = Queue.Queue()
-        self._available_pools.put_nowait(ConnectionsPool(self.sources))
+        self._available_pools.put_nowait(pool.ConnectionsPool(self.sources))
         if config.read_instance_schema:
             # normal start: load the instance schema from the database
             self.fill_schema()
@@ -185,7 +176,7 @@
             # usually during repository creation
             self.warning("set fs instance'schema as bootstrap schema")
             config.bootstrap_cubes()
-            self.set_bootstrap_schema(self.config.load_schema())
+            self.set_schema(self.config.load_schema(), resetvreg=False)
             # need to load the Any and CWUser entity types
             self.vreg.schema = self.schema
             etdirectory = join(CW_SOFTWARE_ROOT, 'entities')
@@ -219,22 +210,22 @@
         # list of available pools (we can't iterated on Queue instance)
         self.pools = []
         for i in xrange(config['connections-pool-size']):
-            self.pools.append(ConnectionsPool(self.sources))
+            self.pools.append(pool.ConnectionsPool(self.sources))
             self._available_pools.put_nowait(self.pools[-1])
         self._shutting_down = False
+        self.hm = vreg['hooks']
         if not (config.creating or config.repairing):
             # call instance level initialisation hooks
             self.hm.call_hooks('server_startup', repo=self)
             # register a task to cleanup expired session
             self.looping_task(self.config['session-time']/3.,
                               self.clean_sessions)
-        CW_EVENT_MANAGER.bind('after-registry-load', self.reset_hooks)
 
     # internals ###############################################################
 
     def get_source(self, uri, source_config):
         source_config['uri'] = uri
-        return get_source(source_config, self.schema, self)
+        return sources.get_source(source_config, self.schema, self)
 
     def set_schema(self, schema, resetvreg=True):
         schema.rebuild_infered_relations()
@@ -248,22 +239,13 @@
             # full reload of all appobjects
             self.vreg.reset()
             self.vreg.set_schema(schema)
-        self.reset_hooks()
-
-    def reset_hooks(self):
-        self.hm.set_schema(self.schema)
-        self.hm.register_system_hooks(self.config)
-        # instance specific hooks
-        if self.config.instance_hooks:
-            self.info('loading instance hooks')
-            self.hm.register_hooks(self.config.load_hooks(self.vreg))
 
     def fill_schema(self):
         """lod schema from the repository"""
         from cubicweb.server.schemaserial import deserialize_schema
         self.info('loading schema from the repository')
-        appschema = CubicWebSchema(self.config.appid)
-        self.set_bootstrap_schema(self.config.load_bootstrap_schema())
+        appschema = schema.CubicWebSchema(self.config.appid)
+        self.set_schema(self.config.load_bootstrap_schema(), resetvreg=False)
         self.debug('deserializing db schema into %s %#x', appschema.name, id(appschema))
         session = self.internal_session()
         try:
@@ -277,44 +259,15 @@
                 raise Exception('Is the database initialised ? (cause: %s)' %
                                 (ex.args and ex.args[0].strip() or 'unknown')), \
                                 None, sys.exc_info()[-1]
-            self.info('set the actual schema')
-            # XXX have to do this since CWProperty isn't in the bootstrap schema
-            #     it'll be redone in set_schema
-            self.set_bootstrap_schema(appschema)
-            # 2.49 migration
-            if exists(join(self.config.apphome, 'vc.conf')):
-                session.set_pool()
-                if not 'template' in file(join(self.config.apphome, 'vc.conf')).read():
-                    # remaning from cubicweb < 2.38...
-                    session.execute('DELETE CWProperty X WHERE X pkey "system.version.template"')
-                    session.commit()
         finally:
             session.close()
+        self.set_schema(appschema)
         self.config.init_cubes(self.get_cubes())
-        self.set_schema(appschema)
-
-    def set_bootstrap_schema(self, schema):
-        """disable hooks when setting a bootstrap schema, but restore
-        the configuration for the next time
-        """
-        config = self.config
-        # XXX refactor
-        config.core_hooks = False
-        config.usergroup_hooks = False
-        config.schema_hooks = False
-        config.notification_hooks = False
-        config.instance_hooks = False
-        self.set_schema(schema, resetvreg=False)
-        config.core_hooks = True
-        config.usergroup_hooks = True
-        config.schema_hooks = True
-        config.notification_hooks = True
-        config.instance_hooks = True
 
     def start_looping_tasks(self):
         assert isinstance(self._looping_tasks, list), 'already started'
         for i, (interval, func, args) in enumerate(self._looping_tasks):
-            self._looping_tasks[i] = task = LoopTask(interval, func, args)
+            self._looping_tasks[i] = task = utils.LoopTask(interval, func, args)
             self.info('starting task %s with interval %.2fs', task.name,
                       interval)
             task.start()
@@ -334,7 +287,7 @@
 
     def threaded_task(self, func):
         """start function in a separated thread"""
-        t = RepoThread(func, self._running_threads)
+        t = utils.RepoThread(func, self._running_threads)
         t.start()
 
     #@locked
@@ -578,7 +531,7 @@
         user.clear_related_cache()
         self._sessions[session.id] = session
         self.info('opened %s', session)
-        self.hm.call_hooks('session_open', session=session)
+        self.hm.call_hooks('session_open', session)
         # commit session at this point in case write operation has been done
         # during `session_open` hooks
         session.commit()
@@ -669,7 +622,7 @@
                                     checkshuttingdown=checkshuttingdown)
         # operation uncommited before close are rollbacked before hook is called
         session.rollback()
-        self.hm.call_hooks('session_close', session=session)
+        self.hm.call_hooks('session_close', session)
         # commit session at this point in case write operation has been done
         # during `session_close` hooks
         session.commit()
@@ -850,11 +803,11 @@
                 entity = source.before_entity_insertion(session, extid, etype, eid)
                 entity._cw_recreating = True
                 if source.should_call_hooks:
-                    self.hm.call_hooks('before_add_entity', etype, session, entity)
+                    self.hm.call_hooks('before_add_entity', session, entity=entity)
                 # XXX add fti op ?
                 source.after_entity_insertion(session, extid, entity)
                 if source.should_call_hooks:
-                    self.hm.call_hooks('after_add_entity', etype, session, entity)
+                    self.hm.call_hooks('after_add_entity', session, entity=entity)
             if reset_pool:
                 session.reset_pool()
             return eid
@@ -875,12 +828,12 @@
             self._type_source_cache[eid] = (etype, source.uri, extid)
             entity = source.before_entity_insertion(session, extid, etype, eid)
             if source.should_call_hooks:
-                self.hm.call_hooks('before_add_entity', etype, session, entity)
+                self.hm.call_hooks('before_add_entity', session, entity=entity)
             # XXX call add_info with complete=False ?
             self.add_info(session, entity, source, extid)
             source.after_entity_insertion(session, extid, entity)
             if source.should_call_hooks:
-                self.hm.call_hooks('after_add_entity', etype, session, entity)
+                self.hm.call_hooks('after_add_entity', session, entity=entity)
             else:
                 # minimal meta-data
                 session.execute('SET X is E WHERE X eid %(x)s, E name %(name)s',
@@ -939,7 +892,7 @@
         pendingrtypes = session.transaction_data.get('pendingrtypes', ())
         for rschema, targetschemas, x in eschema.relation_definitions():
             rtype = rschema.type
-            if rtype in VIRTUAL_RTYPES or rtype in pendingrtypes:
+            if rtype in schema.VIRTUAL_RTYPES or rtype in pendingrtypes:
                 continue
             var = '%s%s' % (rtype.upper(), x.upper())
             if x == 'subject':
@@ -998,13 +951,13 @@
         relations = []
         # if inlined relations are specified, fill entity's related cache to
         # avoid unnecessary queries
-        for attr in entity.keys():
+        entity.edited_attributes = set(entity)
+        for attr in entity.edited_attributes:
             rschema = eschema.subject_relation(attr)
             if not rschema.is_final(): # inlined relation
                 relations.append((attr, entity[attr]))
         if source.should_call_hooks:
-            self.hm.call_hooks('before_add_entity', etype, session, entity)
-        entity.edited_attributes = entity.keys()
+            self.hm.call_hooks('before_add_entity', session, entity=entity)
         entity.set_defaults()
         entity.check(creation=True)
         source.add_entity(session, entity)
@@ -1019,7 +972,7 @@
         session.set_entity_cache(entity)
         for rschema in eschema.subject_relations():
             rtype = str(rschema)
-            if rtype in VIRTUAL_RTYPES:
+            if rtype in schema.VIRTUAL_RTYPES:
                 continue
             if rschema.is_final():
                 entity.setdefault(rtype, None)
@@ -1027,7 +980,7 @@
                 entity.set_related_cache(rtype, 'subject', session.empty_rset())
         for rschema in eschema.object_relations():
             rtype = str(rschema)
-            if rtype in VIRTUAL_RTYPES:
+            if rtype in schema.VIRTUAL_RTYPES:
                 continue
             entity.set_related_cache(rtype, 'object', session.empty_rset())
         # set inline relation cache before call to after_add_entity
@@ -1035,13 +988,13 @@
             session.update_rel_cache_add(entity.eid, attr, value)
         # trigger after_add_entity after after_add_relation
         if source.should_call_hooks:
-            self.hm.call_hooks('after_add_entity', etype, session, entity)
+            self.hm.call_hooks('after_add_entity', session, entity=entity)
             # call hooks for inlined relations
             for attr, value in relations:
-                self.hm.call_hooks('before_add_relation', attr, session,
-                                    entity.eid, attr, value)
-                self.hm.call_hooks('after_add_relation', attr, session,
-                                    entity.eid, attr, value)
+                self.hm.call_hooks('before_add_relation', session,
+                                    eidfrom=entity.eid, rtype=attr, eidto=value)
+                self.hm.call_hooks('after_add_relation', session,
+                                    eidfrom=entity.eid, rtype=attr, eidto=value)
         return entity.eid
 
     def glob_update_entity(self, session, entity, edited_attributes):
@@ -1074,19 +1027,18 @@
                     if previous_value == entity[attr]:
                         previous_value = None
                     else:
-                        self.hm.call_hooks('before_delete_relation', attr,
-                                           session, entity.eid, attr,
-                                           previous_value)
+                        self.hm.call_hooks('before_delete_relation', session,
+                                           eidfrom=entity.eid, rtype=attr,
+                                           eidto=previous_value)
                 relations.append((attr, entity[attr], previous_value))
         source = self.source_from_eid(entity.eid, session)
         if source.should_call_hooks:
             # call hooks for inlined relations
             for attr, value, _ in relations:
-                self.hm.call_hooks('before_add_relation', attr, session,
-                                    entity.eid, attr, value)
+                self.hm.call_hooks('before_add_relation', session,
+                                    eidfrom=entity.eid, rtype=attr, eidto=value)
             if not only_inline_rels:
-                self.hm.call_hooks('before_update_entity', etype, session,
-                                    entity)
+                self.hm.call_hooks('before_update_entity', session, entity=entity)
         source.update_entity(session, entity)
         if not only_inline_rels:
             if need_fti_update and self.do_fti:
@@ -1094,15 +1046,14 @@
                 # one indexable attribute
                 FTIndexEntityOp(session, entity=entity)
             if source.should_call_hooks:
-                self.hm.call_hooks('after_update_entity', etype, session,
-                                    entity)
+                self.hm.call_hooks('after_update_entity', session, entity=entity)
         if source.should_call_hooks:
             for attr, value, prevvalue in relations:
                 # if the relation is already cached, update existant cache
                 relcache = entity.relation_cached(attr, 'subject')
                 if prevvalue:
-                    self.hm.call_hooks('after_delete_relation', attr, session,
-                                       entity.eid, attr, prevvalue)
+                    self.hm.call_hooks('after_delete_relation', session,
+                                       eidfrom=entity.eid, rtype=attr, eidto=prevvalue)
                     if relcache is not None:
                         session.update_rel_cache_del(entity.eid, attr, prevvalue)
                 del_existing_rel_if_needed(session, entity.eid, attr, value)
@@ -1111,8 +1062,8 @@
                 else:
                     entity.set_related_cache(attr, 'subject',
                                              session.eid_rset(value))
-                self.hm.call_hooks('after_add_relation', attr, session,
-                                    entity.eid, attr, value)
+                self.hm.call_hooks('after_add_relation', session,
+                                    eidfrom=entity.eid, rtype=attr, eidto=value)
 
     def glob_delete_entity(self, session, eid):
         """delete an entity and all related entities from the repository"""
@@ -1125,11 +1076,12 @@
                 server.DEBUG |= (server.DBG_SQL | server.DBG_RQL | server.DBG_MORE)
         source = self.sources_by_uri[uri]
         if source.should_call_hooks:
-            self.hm.call_hooks('before_delete_entity', etype, session, eid)
+            entity = session.entity_from_eid(eid)
+            self.hm.call_hooks('before_delete_entity', session, entity=entity)
         self._delete_info(session, eid)
         source.delete_entity(session, etype, eid)
         if source.should_call_hooks:
-            self.hm.call_hooks('after_delete_entity', etype, session, eid)
+            self.hm.call_hooks('after_delete_entity', session, entity=entity)
         # don't clear cache here this is done in a hook on commit
 
     def glob_add_relation(self, session, subject, rtype, object):
@@ -1139,14 +1091,14 @@
         source = self.locate_relation_source(session, subject, rtype, object)
         if source.should_call_hooks:
             del_existing_rel_if_needed(session, subject, rtype, object)
-            self.hm.call_hooks('before_add_relation', rtype, session,
-                               subject, rtype, object)
+            self.hm.call_hooks('before_add_relation', session,
+                               eidfrom=subject, rtype=rtype, eidto=object)
         source.add_relation(session, subject, rtype, object)
         rschema = self.schema.rschema(rtype)
         session.update_rel_cache_add(subject, rtype, object, rschema.symetric)
         if source.should_call_hooks:
-            self.hm.call_hooks('after_add_relation', rtype, session,
-                               subject, rtype, object)
+            self.hm.call_hooks('after_add_relation', session,
+                               eidfrom=subject, rtype=rtype, eidto=object)
 
     def glob_delete_relation(self, session, subject, rtype, object):
         """delete a relation from the repository"""
@@ -1154,8 +1106,8 @@
             print 'DELETE relation', subject, rtype, object
         source = self.locate_relation_source(session, subject, rtype, object)
         if source.should_call_hooks:
-            self.hm.call_hooks('before_delete_relation', rtype, session,
-                               subject, rtype, object)
+            self.hm.call_hooks('before_delete_relation', session,
+                               eidfrom=subject, rtype=rtype, eidto=object)
         source.delete_relation(session, subject, rtype, object)
         rschema = self.schema.rschema(rtype)
         session.update_rel_cache_del(subject, rtype, object, rschema.symetric)
@@ -1164,8 +1116,8 @@
             # stored so try to delete both
             source.delete_relation(session, object, rtype, subject)
         if source.should_call_hooks:
-            self.hm.call_hooks('after_delete_relation', rtype, session,
-                               subject, rtype, object)
+            self.hm.call_hooks('after_delete_relation', session,
+                               eidfrom=subject, rtype=rtype, eidto=object)
 
 
     # pyro handling ###########################################################
--- a/server/schemahooks.py	Fri Aug 14 00:02:08 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1057 +0,0 @@
-"""schema hooks:
-
-- synchronize the living schema object with the persistent schema
-- perform physical update on the source when necessary
-
-checking for schema consistency is done in hooks.py
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-__docformat__ = "restructuredtext en"
-
-from yams.schema import BASE_TYPES
-from yams.buildobjs import EntityType, RelationType, RelationDefinition
-from yams.schema2sql import eschema2sql, rschema2sql, type_from_constraints
-
-
-from cubicweb import ValidationError, RepositoryError
-from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES, CONSTRAINTS
-from cubicweb.server import schemaserial as ss
-from cubicweb.server.sqlutils import SQL_PREFIX
-from cubicweb.server.pool import Operation, SingleLastOperation, PreCommitOperation
-from cubicweb.server.hookhelper import (entity_attr, entity_name,
-                                        check_internal_entity)
-
-
-TYPE_CONVERTER = { # XXX
-    'Boolean': bool,
-    'Int': int,
-    'Float': float,
-    'Password': str,
-    'String': unicode,
-    'Date' : unicode,
-    'Datetime' : unicode,
-    'Time' : unicode,
-    }
-
-# core entity and relation types which can't be removed
-CORE_ETYPES = list(BASE_TYPES) + ['CWEType', 'CWRType', 'CWUser', 'CWGroup',
-                                  'CWConstraint', 'CWAttribute', 'CWRelation']
-CORE_RTYPES = ['eid', 'creation_date', 'modification_date', 'cwuri',
-               'login', 'upassword', 'name',
-               'is', 'instanceof', 'owned_by', 'created_by', 'in_group',
-               'relation_type', 'from_entity', 'to_entity',
-               'constrainted_by',
-               'read_permission', 'add_permission',
-               'delete_permission', 'updated_permission',
-               ]
-
-def get_constraints(session, entity):
-    constraints = []
-    for cstreid in session.transaction_data.get(entity.eid, ()):
-        cstrent = session.entity_from_eid(cstreid)
-        cstr = CONSTRAINTS[cstrent.type].deserialize(cstrent.value)
-        cstr.eid = cstreid
-        constraints.append(cstr)
-    return constraints
-
-def add_inline_relation_column(session, etype, rtype):
-    """add necessary column and index for an inlined relation"""
-    table = SQL_PREFIX + etype
-    column = SQL_PREFIX + rtype
-    try:
-        session.system_sql(str('ALTER TABLE %s ADD COLUMN %s integer'
-                               % (table, column)), rollback_on_failure=False)
-        session.info('added column %s to table %s', column, table)
-    except:
-        # silent exception here, if this error has not been raised because the
-        # column already exists, index creation will fail anyway
-        session.exception('error while adding column %s to table %s',
-                          table, column)
-    # create index before alter table which may expectingly fail during test
-    # (sqlite) while index creation should never fail (test for index existence
-    # is done by the dbhelper)
-    session.pool.source('system').create_index(session, table, column)
-    session.info('added index on %s(%s)', table, column)
-    session.transaction_data.setdefault('createdattrs', []).append(
-        '%s.%s' % (etype, rtype))
-
-
-# operations for low-level database alteration  ################################
-
-class DropTable(PreCommitOperation):
-    """actually remove a database from the instance's schema"""
-    table = None # make pylint happy
-    def precommit_event(self):
-        dropped = self.session.transaction_data.setdefault('droppedtables',
-                                                           set())
-        if self.table in dropped:
-            return # already processed
-        dropped.add(self.table)
-        self.session.system_sql('DROP TABLE %s' % self.table)
-        self.info('dropped table %s', self.table)
-
-
-class DropRelationTable(DropTable):
-    def __init__(self, session, rtype):
-        super(DropRelationTable, self).__init__(
-            session, table='%s_relation' % rtype)
-        session.transaction_data.setdefault('pendingrtypes', set()).add(rtype)
-
-
-class DropColumn(PreCommitOperation):
-    """actually remove the attribut's column from entity table in the system
-    database
-    """
-    table = column = None # make pylint happy
-    def precommit_event(self):
-        session, table, column = self.session, self.table, self.column
-        # drop index if any
-        session.pool.source('system').drop_index(session, table, column)
-        try:
-            session.system_sql('ALTER TABLE %s DROP COLUMN %s'
-                               % (table, column), rollback_on_failure=False)
-            self.info('dropped column %s from table %s', column, table)
-        except Exception, ex:
-            # not supported by sqlite for instance
-            self.error('error while altering table %s: %s', table, ex)
-
-
-# base operations for in-memory schema synchronization  ########################
-
-class MemSchemaNotifyChanges(SingleLastOperation):
-    """the update schema operation:
-
-    special operation which should be called once and after all other schema
-    operations. It will trigger internal structures rebuilding to consider
-    schema changes
-    """
-
-    def __init__(self, session):
-        self.repo = session.repo
-        SingleLastOperation.__init__(self, session)
-
-    def commit_event(self):
-        self.repo.set_schema(self.repo.schema)
-
-
-class MemSchemaOperation(Operation):
-    """base class for schema operations"""
-    def __init__(self, session, kobj=None, **kwargs):
-        self.schema = session.schema
-        self.kobj = kobj
-        # once Operation.__init__ has been called, event may be triggered, so
-        # do this last !
-        Operation.__init__(self, session, **kwargs)
-        # every schema operation is triggering a schema update
-        MemSchemaNotifyChanges(session)
-
-    def prepare_constraints(self, subjtype, rtype, objtype):
-        constraints = rtype.rproperty(subjtype, objtype, 'constraints')
-        self.constraints = list(constraints)
-        rtype.set_rproperty(subjtype, objtype, 'constraints', self.constraints)
-
-
-class MemSchemaEarlyOperation(MemSchemaOperation):
-    def insert_index(self):
-        """schema operation which are inserted at the begining of the queue
-        (typically to add/remove entity or relation types)
-        """
-        i = -1
-        for i, op in enumerate(self.session.pending_operations):
-            if not isinstance(op, MemSchemaEarlyOperation):
-                return i
-        return i + 1
-
-
-class MemSchemaPermissionOperation(MemSchemaOperation):
-    """base class to synchronize schema permission definitions"""
-    def __init__(self, session, perm, etype_eid):
-        self.perm = perm
-        try:
-            self.name = entity_name(session, etype_eid)
-        except IndexError:
-            self.error('changing permission of a no more existant type #%s',
-                etype_eid)
-        else:
-            Operation.__init__(self, session)
-
-
-# operations for high-level source database alteration  ########################
-
-class SourceDbCWETypeRename(PreCommitOperation):
-    """this operation updates physical storage accordingly"""
-    oldname = newname = None # make pylint happy
-
-    def precommit_event(self):
-        # we need sql to operate physical changes on the system database
-        sqlexec = self.session.system_sql
-        sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, self.oldname,
-                                                     SQL_PREFIX, self.newname))
-        self.info('renamed table %s to %s', self.oldname, self.newname)
-        sqlexec('UPDATE entities SET type=%s WHERE type=%s',
-                (self.newname, self.oldname))
-        sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',
-                (self.newname, self.oldname))
-
-
-class SourceDbCWRTypeUpdate(PreCommitOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = entity = None # make pylint happy
-
-    def precommit_event(self):
-        session = self.session
-        rschema = self.rschema
-        if rschema.is_final() or not 'inlined' in self.values:
-            return # nothing to do
-        inlined = self.values['inlined']
-        entity = self.entity
-        # check in-lining is necessary / possible
-        if not entity.inlined_changed(inlined):
-            return # nothing to do
-        # inlined changed, make necessary physical changes!
-        sqlexec = self.session.system_sql
-        rtype = rschema.type
-        eidcolumn = SQL_PREFIX + 'eid'
-        if not inlined:
-            # need to create the relation if it has not been already done by
-            # another event of the same transaction
-            if not rschema.type in session.transaction_data.get('createdtables', ()):
-                tablesql = rschema2sql(rschema)
-                # create the necessary table
-                for sql in tablesql.split(';'):
-                    if sql.strip():
-                        sqlexec(sql)
-                session.transaction_data.setdefault('createdtables', []).append(
-                    rschema.type)
-            # copy existant data
-            column = SQL_PREFIX + rtype
-            for etype in rschema.subjects():
-                table = SQL_PREFIX + str(etype)
-                sqlexec('INSERT INTO %s_relation SELECT %s, %s FROM %s WHERE NOT %s IS NULL'
-                        % (rtype, eidcolumn, column, table, column))
-            # drop existant columns
-            for etype in rschema.subjects():
-                DropColumn(session, table=SQL_PREFIX + str(etype),
-                             column=SQL_PREFIX + rtype)
-        else:
-            for etype in rschema.subjects():
-                try:
-                    add_inline_relation_column(session, str(etype), rtype)
-                except Exception, ex:
-                    # the column probably already exists. this occurs when the
-                    # entity's type has just been added or if the column has not
-                    # been previously dropped
-                    self.error('error while altering table %s: %s', etype, ex)
-                # copy existant data.
-                # XXX don't use, it's not supported by sqlite (at least at when i tried it)
-                #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to '
-                #        'FROM %(rtype)s_relation '
-                #        'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from'
-                #        % locals())
-                table = SQL_PREFIX + str(etype)
-                cursor = sqlexec('SELECT eid_from, eid_to FROM %(table)s, '
-                                 '%(rtype)s_relation WHERE %(table)s.%(eidcolumn)s='
-                                 '%(rtype)s_relation.eid_from' % locals())
-                args = [{'val': eid_to, 'x': eid} for eid, eid_to in cursor.fetchall()]
-                if args:
-                    column = SQL_PREFIX + rtype
-                    cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'
-                                       % (table, column, eidcolumn), args)
-                # drop existant table
-                DropRelationTable(session, rtype)
-
-
-class SourceDbCWAttributeAdd(PreCommitOperation):
-    """an attribute relation (CWAttribute) has been added:
-    * add the necessary column
-    * set default on this column if any and possible
-    * register an operation to add the relation definition to the
-      instance's schema on commit
-
-    constraints are handled by specific hooks
-    """
-    entity = None # make pylint happy
-
-    def init_rdef(self, **kwargs):
-        entity = self.entity
-        fromentity = entity.stype
-        self.session.execute('SET X ordernum Y+1 '
-                             'WHERE X from_entity SE, SE eid %(se)s, X ordernum Y, '
-                             'X ordernum >= %(order)s, NOT X eid %(x)s',
-                             {'x': entity.eid, 'se': fromentity.eid,
-                              'order': entity.ordernum or 0})
-        subj = str(fromentity.name)
-        rtype = entity.rtype.name
-        obj = str(entity.otype.name)
-        constraints = get_constraints(self.session, entity)
-        rdef = RelationDefinition(subj, rtype, obj,
-                                  description=entity.description,
-                                  cardinality=entity.cardinality,
-                                  constraints=constraints,
-                                  order=entity.ordernum,
-                                  eid=entity.eid,
-                                  **kwargs)
-        MemSchemaRDefAdd(self.session, rdef)
-        return rdef
-
-    def precommit_event(self):
-        session = self.session
-        entity = self.entity
-        # entity.defaultval is a string or None, but we need a correctly typed
-        # value
-        default = entity.defaultval
-        if default is not None:
-            default = TYPE_CONVERTER[entity.otype.name](default)
-        rdef = self.init_rdef(default=default,
-                              indexed=entity.indexed,
-                              fulltextindexed=entity.fulltextindexed,
-                              internationalizable=entity.internationalizable)
-        sysource = session.pool.source('system')
-        attrtype = type_from_constraints(sysource.dbhelper, rdef.object,
-                                         rdef.constraints)
-        # XXX should be moved somehow into lgc.adbh: sqlite doesn't support to
-        # add a new column with UNIQUE, it should be added after the ALTER TABLE
-        # using ADD INDEX
-        if sysource.dbdriver == 'sqlite' and 'UNIQUE' in attrtype:
-            extra_unique_index = True
-            attrtype = attrtype.replace(' UNIQUE', '')
-        else:
-            extra_unique_index = False
-        # added some str() wrapping query since some backend (eg psycopg) don't
-        # allow unicode queries
-        table = SQL_PREFIX + rdef.subject
-        column = SQL_PREFIX + rdef.name
-        try:
-            session.system_sql(str('ALTER TABLE %s ADD COLUMN %s %s'
-                                   % (table, column, attrtype)),
-                               rollback_on_failure=False)
-            self.info('added column %s to table %s', table, column)
-        except Exception, ex:
-            # the column probably already exists. this occurs when
-            # the entity's type has just been added or if the column
-            # has not been previously dropped
-            self.error('error while altering table %s: %s', table, ex)
-        if extra_unique_index or entity.indexed:
-            try:
-                sysource.create_index(session, table, column,
-                                      unique=extra_unique_index)
-            except Exception, ex:
-                self.error('error while creating index for %s.%s: %s',
-                           table, column, ex)
-
-
-class SourceDbCWRelationAdd(SourceDbCWAttributeAdd):
-    """an actual relation has been added:
-    * if this is an inlined relation, add the necessary column
-      else if it's the first instance of this relation type, add the
-      necessary table and set default permissions
-    * register an operation to add the relation definition to the
-      instance's schema on commit
-
-    constraints are handled by specific hooks
-    """
-    entity = None # make pylint happy
-
-    def precommit_event(self):
-        session = self.session
-        entity = self.entity
-        rdef = self.init_rdef(composite=entity.composite)
-        schema = session.schema
-        rtype = rdef.name
-        rschema = session.schema.rschema(rtype)
-        # this have to be done before permissions setting
-        if rschema.inlined:
-            # need to add a column if the relation is inlined and if this is the
-            # first occurence of "Subject relation Something" whatever Something
-            # and if it has not been added during other event of the same
-            # transaction
-            key = '%s.%s' % (rdef.subject, rtype)
-            try:
-                alreadythere = bool(rschema.objects(rdef.subject))
-            except KeyError:
-                alreadythere = False
-            if not (alreadythere or
-                    key in session.transaction_data.get('createdattrs', ())):
-                add_inline_relation_column(session, rdef.subject, rtype)
-        else:
-            # need to create the relation if no relation definition in the
-            # schema and if it has not been added during other event of the same
-            # transaction
-            if not (rschema.subjects() or
-                    rtype in session.transaction_data.get('createdtables', ())):
-                try:
-                    rschema = session.schema.rschema(rtype)
-                    tablesql = rschema2sql(rschema)
-                except KeyError:
-                    # fake we add it to the schema now to get a correctly
-                    # initialized schema but remove it before doing anything
-                    # more dangerous...
-                    rschema = session.schema.add_relation_type(rdef)
-                    tablesql = rschema2sql(rschema)
-                    session.schema.del_relation_type(rtype)
-                # create the necessary table
-                for sql in tablesql.split(';'):
-                    if sql.strip():
-                        session.system_sql(sql)
-                session.transaction_data.setdefault('createdtables', []).append(
-                    rtype)
-
-
-class SourceDbRDefUpdate(PreCommitOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
-
-    def precommit_event(self):
-        etype = self.kobj[0]
-        table = SQL_PREFIX + etype
-        column = SQL_PREFIX + self.rschema.type
-        if 'indexed' in self.values:
-            sysource = self.session.pool.source('system')
-            if self.values['indexed']:
-                sysource.create_index(self.session, table, column)
-            else:
-                sysource.drop_index(self.session, table, column)
-        if 'cardinality' in self.values and self.rschema.is_final():
-            adbh = self.session.pool.source('system').dbhelper
-            if not adbh.alter_column_support:
-                # not supported (and NOT NULL not set by yams in that case, so
-                # no worry)
-                return
-            atype = self.rschema.objects(etype)[0]
-            constraints = self.rschema.rproperty(etype, atype, 'constraints')
-            coltype = type_from_constraints(adbh, atype, constraints,
-                                            creating=False)
-            # XXX check self.values['cardinality'][0] actually changed?
-            sql = adbh.sql_set_null_allowed(table, column, coltype,
-                                            self.values['cardinality'][0] != '1')
-            self.session.system_sql(sql)
-
-
-class SourceDbCWConstraintAdd(PreCommitOperation):
-    """actually update constraint of a relation definition"""
-    entity = None # make pylint happy
-    cancelled = False
-
-    def precommit_event(self):
-        rdef = self.entity.reverse_constrained_by[0]
-        session = self.session
-        # when the relation is added in the same transaction, the constraint
-        # object is created by the operation adding the attribute or relation,
-        # so there is nothing to do here
-        if rdef.eid in session.transaction_data.get('neweids', ()):
-            return
-        subjtype, rtype, objtype = session.schema.schema_by_eid(rdef.eid)
-        cstrtype = self.entity.type
-        oldcstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
-        newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
-        table = SQL_PREFIX + str(subjtype)
-        column = SQL_PREFIX + str(rtype)
-        # alter the physical schema on size constraint changes
-        if newcstr.type() == 'SizeConstraint' and (
-            oldcstr is None or oldcstr.max != newcstr.max):
-            adbh = self.session.pool.source('system').dbhelper
-            card = rtype.rproperty(subjtype, objtype, 'cardinality')
-            coltype = type_from_constraints(adbh, objtype, [newcstr],
-                                            creating=False)
-            sql = adbh.sql_change_col_type(table, column, coltype, card != '1')
-            try:
-                session.system_sql(sql, rollback_on_failure=False)
-                self.info('altered column %s of table %s: now VARCHAR(%s)',
-                          column, table, newcstr.max)
-            except Exception, ex:
-                # not supported by sqlite for instance
-                self.error('error while altering table %s: %s', table, ex)
-        elif cstrtype == 'UniqueConstraint' and oldcstr is None:
-            session.pool.source('system').create_index(
-                self.session, table, column, unique=True)
-
-
-class SourceDbCWConstraintDel(PreCommitOperation):
-    """actually remove a constraint of a relation definition"""
-    rtype = subjtype = objtype = None # make pylint happy
-
-    def precommit_event(self):
-        cstrtype = self.cstr.type()
-        table = SQL_PREFIX + str(self.subjtype)
-        column = SQL_PREFIX + str(self.rtype)
-        # alter the physical schema on size/unique constraint changes
-        if cstrtype == 'SizeConstraint':
-            try:
-                self.session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE TEXT'
-                                        % (table, column),
-                                        rollback_on_failure=False)
-                self.info('altered column %s of table %s: now TEXT',
-                          column, table)
-            except Exception, ex:
-                # not supported by sqlite for instance
-                self.error('error while altering table %s: %s', table, ex)
-        elif cstrtype == 'UniqueConstraint':
-            self.session.pool.source('system').drop_index(
-                self.session, table, column, unique=True)
-
-
-# operations for in-memory schema synchronization  #############################
-
-class MemSchemaCWETypeAdd(MemSchemaEarlyOperation):
-    """actually add the entity type to the instance's schema"""
-    eid = None # make pylint happy
-    def commit_event(self):
-        self.schema.add_entity_type(self.kobj)
-
-
-class MemSchemaCWETypeRename(MemSchemaOperation):
-    """this operation updates physical storage accordingly"""
-    oldname = newname = None # make pylint happy
-
-    def commit_event(self):
-        self.session.schema.rename_entity_type(self.oldname, self.newname)
-
-
-class MemSchemaCWETypeDel(MemSchemaOperation):
-    """actually remove the entity type from the instance's schema"""
-    def commit_event(self):
-        try:
-            # del_entity_type also removes entity's relations
-            self.schema.del_entity_type(self.kobj)
-        except KeyError:
-            # s/o entity type have already been deleted
-            pass
-
-
-class MemSchemaCWRTypeAdd(MemSchemaEarlyOperation):
-    """actually add the relation type to the instance's schema"""
-    eid = None # make pylint happy
-    def commit_event(self):
-        rschema = self.schema.add_relation_type(self.kobj)
-        rschema.set_default_groups()
-
-
-class MemSchemaCWRTypeUpdate(MemSchemaOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
-
-    def commit_event(self):
-        # structure should be clean, not need to remove entity's relations
-        # at this point
-        self.rschema.__dict__.update(self.values)
-
-
-class MemSchemaCWRTypeDel(MemSchemaOperation):
-    """actually remove the relation type from the instance's schema"""
-    def commit_event(self):
-        try:
-            self.schema.del_relation_type(self.kobj)
-        except KeyError:
-            # s/o entity type have already been deleted
-            pass
-
-
-class MemSchemaRDefAdd(MemSchemaEarlyOperation):
-    """actually add the attribute relation definition to the instance's
-    schema
-    """
-    def commit_event(self):
-        self.schema.add_relation_def(self.kobj)
-
-
-class MemSchemaRDefUpdate(MemSchemaOperation):
-    """actually update some properties of a relation definition"""
-    rschema = values = None # make pylint happy
-
-    def commit_event(self):
-        # structure should be clean, not need to remove entity's relations
-        # at this point
-        self.rschema._rproperties[self.kobj].update(self.values)
-
-
-class MemSchemaRDefDel(MemSchemaOperation):
-    """actually remove the relation definition from the instance's schema"""
-    def commit_event(self):
-        subjtype, rtype, objtype = self.kobj
-        try:
-            self.schema.del_relation_def(subjtype, rtype, objtype)
-        except KeyError:
-            # relation type may have been already deleted
-            pass
-
-
-class MemSchemaCWConstraintAdd(MemSchemaOperation):
-    """actually update constraint of a relation definition
-
-    has to be called before SourceDbCWConstraintAdd
-    """
-    cancelled = False
-
-    def precommit_event(self):
-        rdef = self.entity.reverse_constrained_by[0]
-        # when the relation is added in the same transaction, the constraint
-        # object is created by the operation adding the attribute or relation,
-        # so there is nothing to do here
-        if rdef.eid in self.session.transaction_data.get('neweids', ()):
-            self.cancelled = True
-            return
-        subjtype, rtype, objtype = self.session.schema.schema_by_eid(rdef.eid)
-        self.prepare_constraints(subjtype, rtype, objtype)
-        cstrtype = self.entity.type
-        self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
-        self.newcstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
-        self.newcstr.eid = self.entity.eid
-
-    def commit_event(self):
-        if self.cancelled:
-            return
-        # in-place modification
-        if not self.cstr is None:
-            self.constraints.remove(self.cstr)
-        self.constraints.append(self.newcstr)
-
-
-class MemSchemaCWConstraintDel(MemSchemaOperation):
-    """actually remove a constraint of a relation definition
-
-    has to be called before SourceDbCWConstraintDel
-    """
-    rtype = subjtype = objtype = None # make pylint happy
-    def precommit_event(self):
-        self.prepare_constraints(self.subjtype, self.rtype, self.objtype)
-
-    def commit_event(self):
-        self.constraints.remove(self.cstr)
-
-
-class MemSchemaPermissionCWGroupAdd(MemSchemaPermissionOperation):
-    """synchronize schema when a *_permission relation has been added on a group
-    """
-    def __init__(self, session, perm, etype_eid, group_eid):
-        self.group = entity_name(session, group_eid)
-        super(MemSchemaPermissionCWGroupAdd, self).__init__(
-            session, perm, etype_eid)
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        try:
-            erschema = self.schema[self.name]
-        except KeyError:
-            # duh, schema not found, log error and skip operation
-            self.error('no schema for %s', self.name)
-            return
-        groups = list(erschema.get_groups(self.perm))
-        try:
-            groups.index(self.group)
-            self.warning('group %s already have permission %s on %s',
-                         self.group, self.perm, erschema.type)
-        except ValueError:
-            groups.append(self.group)
-            erschema.set_groups(self.perm, groups)
-
-
-class MemSchemaPermissionCWGroupDel(MemSchemaPermissionCWGroupAdd):
-    """synchronize schema when a *_permission relation has been deleted from a
-    group
-    """
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        try:
-            erschema = self.schema[self.name]
-        except KeyError:
-            # duh, schema not found, log error and skip operation
-            self.error('no schema for %s', self.name)
-            return
-        groups = list(erschema.get_groups(self.perm))
-        try:
-            groups.remove(self.group)
-            erschema.set_groups(self.perm, groups)
-        except ValueError:
-            self.error('can\'t remove permission %s on %s to group %s',
-                self.perm, erschema.type, self.group)
-
-
-class MemSchemaPermissionRQLExpressionAdd(MemSchemaPermissionOperation):
-    """synchronize schema when a *_permission relation has been added on a rql
-    expression
-    """
-    def __init__(self, session, perm, etype_eid, expression):
-        self.expr = expression
-        super(MemSchemaPermissionRQLExpressionAdd, self).__init__(
-            session, perm, etype_eid)
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        try:
-            erschema = self.schema[self.name]
-        except KeyError:
-            # duh, schema not found, log error and skip operation
-            self.error('no schema for %s', self.name)
-            return
-        exprs = list(erschema.get_rqlexprs(self.perm))
-        exprs.append(erschema.rql_expression(self.expr))
-        erschema.set_rqlexprs(self.perm, exprs)
-
-
-class MemSchemaPermissionRQLExpressionDel(MemSchemaPermissionRQLExpressionAdd):
-    """synchronize schema when a *_permission relation has been deleted from an
-    rql expression
-    """
-
-    def commit_event(self):
-        """the observed connections pool has been commited"""
-        try:
-            erschema = self.schema[self.name]
-        except KeyError:
-            # duh, schema not found, log error and skip operation
-            self.error('no schema for %s', self.name)
-            return
-        rqlexprs = list(erschema.get_rqlexprs(self.perm))
-        for i, rqlexpr in enumerate(rqlexprs):
-            if rqlexpr.expression == self.expr:
-                rqlexprs.pop(i)
-                break
-        else:
-            self.error('can\'t remove permission %s on %s for expression %s',
-                self.perm, erschema.type, self.expr)
-            return
-        erschema.set_rqlexprs(self.perm, rqlexprs)
-
-
-# deletion hooks ###############################################################
-
-def before_del_eetype(session, eid):
-    """before deleting a CWEType entity:
-    * check that we don't remove a core entity type
-    * cascade to delete related CWAttribute and CWRelation entities
-    * instantiate an operation to delete the entity type on commit
-    """
-    # final entities can't be deleted, don't care about that
-    name = check_internal_entity(session, eid, CORE_ETYPES)
-    # delete every entities of this type
-    session.unsafe_execute('DELETE %s X' % name)
-    DropTable(session, table=SQL_PREFIX + name)
-    MemSchemaCWETypeDel(session, name)
-
-
-def after_del_eetype(session, eid):
-    # workflow cleanup
-    session.execute('DELETE State X WHERE NOT X state_of Y')
-    session.execute('DELETE Transition X WHERE NOT X transition_of Y')
-
-
-def before_del_ertype(session, eid):
-    """before deleting a CWRType entity:
-    * check that we don't remove a core relation type
-    * cascade to delete related CWAttribute and CWRelation entities
-    * instantiate an operation to delete the relation type on commit
-    """
-    name = check_internal_entity(session, eid, CORE_RTYPES)
-    # delete relation definitions using this relation type
-    session.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
-                    {'x': eid})
-    session.execute('DELETE CWRelation X WHERE X relation_type Y, Y eid %(x)s',
-                    {'x': eid})
-    MemSchemaCWRTypeDel(session, name)
-
-
-def after_del_relation_type(session, rdefeid, rtype, rteid):
-    """before deleting a CWAttribute or CWRelation entity:
-    * if this is a final or inlined relation definition, instantiate an
-      operation to drop necessary column, else if this is the last instance
-      of a non final relation, instantiate an operation to drop necessary
-      table
-    * instantiate an operation to delete the relation definition on commit
-    * delete the associated relation type when necessary
-    """
-    subjschema, rschema, objschema = session.schema.schema_by_eid(rdefeid)
-    pendings = session.transaction_data.get('pendingeids', ())
-    # first delete existing relation if necessary
-    if rschema.is_final():
-        rdeftype = 'CWAttribute'
-    else:
-        rdeftype = 'CWRelation'
-        if not (subjschema.eid in pendings or objschema.eid in pendings):
-            pending = session.transaction_data.setdefault('pendingrdefs', set())
-            pending.add((subjschema, rschema, objschema))
-            session.execute('DELETE X %s Y WHERE X is %s, Y is %s'
-                            % (rschema, subjschema, objschema))
-    execute = session.unsafe_execute
-    rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R,'
-                   'R eid %%(x)s' % rdeftype, {'x': rteid})
-    lastrel = rset[0][0] == 0
-    # we have to update physical schema systematically for final and inlined
-    # relations, but only if it's the last instance for this relation type
-    # for other relations
-
-    if (rschema.is_final() or rschema.inlined):
-        rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
-                       'R eid %%(x)s, X from_entity E, E name %%(name)s'
-                       % rdeftype, {'x': rteid, 'name': str(subjschema)})
-        if rset[0][0] == 0 and not subjschema.eid in pendings:
-            ptypes = session.transaction_data.setdefault('pendingrtypes', set())
-            ptypes.add(rschema.type)
-            DropColumn(session, table=SQL_PREFIX + subjschema.type,
-                         column=SQL_PREFIX + rschema.type)
-    elif lastrel:
-        DropRelationTable(session, rschema.type)
-    # if this is the last instance, drop associated relation type
-    if lastrel and not rteid in pendings:
-        execute('DELETE CWRType X WHERE X eid %(x)s', {'x': rteid}, 'x')
-    MemSchemaRDefDel(session, (subjschema, rschema, objschema))
-
-
-# addition hooks ###############################################################
-
-def before_add_eetype(session, entity):
-    """before adding a CWEType entity:
-    * check that we are not using an existing entity type,
-    """
-    name = entity['name']
-    schema = session.schema
-    if name in schema and schema[name].eid is not None:
-        raise RepositoryError('an entity type %s already exists' % name)
-
-def after_add_eetype(session, entity):
-    """after adding a CWEType entity:
-    * create the necessary table
-    * set creation_date and modification_date by creating the necessary
-      CWAttribute entities
-    * add owned_by relation by creating the necessary CWRelation entity
-    * register an operation to add the entity type to the instance's
-      schema on commit
-    """
-    if entity.get('final'):
-        return
-    schema = session.schema
-    name = entity['name']
-    etype = EntityType(name=name, description=entity.get('description'),
-                       meta=entity.get('meta')) # don't care about final
-    # fake we add it to the schema now to get a correctly initialized schema
-    # but remove it before doing anything more dangerous...
-    schema = session.schema
-    eschema = schema.add_entity_type(etype)
-    eschema.set_default_groups()
-    # generate table sql and rql to add metadata
-    tablesql = eschema2sql(session.pool.source('system').dbhelper, eschema,
-                           prefix=SQL_PREFIX)
-    relrqls = []
-    for rtype in (META_RTYPES - VIRTUAL_RTYPES):
-        rschema = schema[rtype]
-        sampletype = rschema.subjects()[0]
-        desttype = rschema.objects()[0]
-        props = rschema.rproperties(sampletype, desttype)
-        relrqls += list(ss.rdef2rql(rschema, name, desttype, props))
-    # now remove it !
-    schema.del_entity_type(name)
-    # create the necessary table
-    for sql in tablesql.split(';'):
-        if sql.strip():
-            session.system_sql(sql)
-    # register operation to modify the schema on commit
-    # this have to be done before adding other relations definitions
-    # or permission settings
-    etype.eid = entity.eid
-    MemSchemaCWETypeAdd(session, etype)
-    # add meta relations
-    for rql, kwargs in relrqls:
-        session.execute(rql, kwargs)
-
-
-def before_add_ertype(session, entity):
-    """before adding a CWRType entity:
-    * check that we are not using an existing relation type,
-    * register an operation to add the relation type to the instance's
-      schema on commit
-
-    We don't know yeat this point if a table is necessary
-    """
-    name = entity['name']
-    if name in session.schema.relations():
-        raise RepositoryError('a relation type %s already exists' % name)
-
-
-def after_add_ertype(session, entity):
-    """after a CWRType entity has been added:
-    * register an operation to add the relation type to the instance's
-      schema on commit
-    We don't know yeat this point if a table is necessary
-    """
-    rtype = RelationType(name=entity['name'],
-                         description=entity.get('description'),
-                         meta=entity.get('meta', False),
-                         inlined=entity.get('inlined', False),
-                         symetric=entity.get('symetric', False))
-    rtype.eid = entity.eid
-    MemSchemaCWRTypeAdd(session, rtype)
-
-
-def after_add_efrdef(session, entity):
-    SourceDbCWAttributeAdd(session, entity=entity)
-
-def after_add_enfrdef(session, entity):
-    SourceDbCWRelationAdd(session, entity=entity)
-
-
-# update hooks #################################################################
-
-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 ro_attrs:
-        origval = entity_attr(session, entity.eid, attr)
-        if entity.get(attr, origval) != origval:
-            errors[attr] = session._("can't change the %s attribute") % \
-                           display_name(session, attr)
-    if errors:
-        raise ValidationError(entity.eid, errors)
-
-def before_update_eetype(session, entity):
-    """check name change, handle final"""
-    check_valid_changes(session, entity, ro_attrs=('final',))
-    # don't use getattr(entity, attr), we would get the modified value if any
-    oldname = entity_attr(session, entity.eid, 'name')
-    newname = entity.get('name', oldname)
-    if newname.lower() != oldname.lower():
-        SourceDbCWETypeRename(session, oldname=oldname, newname=newname)
-        MemSchemaCWETypeRename(session, oldname=oldname, newname=newname)
-
-def before_update_ertype(session, entity):
-    """check name change, handle final"""
-    check_valid_changes(session, entity)
-
-
-def after_update_erdef(session, entity):
-    if entity.eid in session.transaction_data.get('pendingeids', ()):
-        return
-    desttype = entity.otype.name
-    rschema = session.schema[entity.rtype.name]
-    newvalues = {}
-    for prop in rschema.rproperty_defs(desttype):
-        if prop == 'constraints':
-            continue
-        if prop == 'order':
-            prop = 'ordernum'
-        if prop in entity.edited_attributes:
-            newvalues[prop] = entity[prop]
-    if newvalues:
-        subjtype = entity.stype.name
-        MemSchemaRDefUpdate(session, kobj=(subjtype, desttype),
-                            rschema=rschema, values=newvalues)
-        SourceDbRDefUpdate(session, kobj=(subjtype, desttype),
-                           rschema=rschema, values=newvalues)
-
-def after_update_ertype(session, entity):
-    rschema = session.schema.rschema(entity.name)
-    newvalues = {}
-    for prop in ('meta', 'symetric', 'inlined'):
-        if prop in entity:
-            newvalues[prop] = entity[prop]
-    if newvalues:
-        MemSchemaCWRTypeUpdate(session, rschema=rschema, values=newvalues)
-        SourceDbCWRTypeUpdate(session, rschema=rschema, values=newvalues,
-                              entity=entity)
-
-# constraints synchronization hooks ############################################
-
-def after_add_econstraint(session, entity):
-    MemSchemaCWConstraintAdd(session, entity=entity)
-    SourceDbCWConstraintAdd(session, entity=entity)
-
-
-def after_update_econstraint(session, entity):
-    MemSchemaCWConstraintAdd(session, entity=entity)
-    SourceDbCWConstraintAdd(session, entity=entity)
-
-
-def before_delete_constrained_by(session, fromeid, rtype, toeid):
-    if not fromeid in session.transaction_data.get('pendingeids', ()):
-        schema = session.schema
-        entity = session.entity_from_eid(toeid)
-        subjtype, rtype, objtype = schema.schema_by_eid(fromeid)
-        try:
-            cstr = rtype.constraint_by_type(subjtype, objtype,
-                                            entity.cstrtype[0].name)
-        except IndexError:
-            session.critical('constraint type no more accessible')
-        else:
-            SourceDbCWConstraintDel(session, subjtype=subjtype, rtype=rtype,
-                                    objtype=objtype, cstr=cstr)
-            MemSchemaCWConstraintDel(session, subjtype=subjtype, rtype=rtype,
-                                     objtype=objtype, cstr=cstr)
-
-
-def after_add_constrained_by(session, fromeid, rtype, toeid):
-    if fromeid in session.transaction_data.get('neweids', ()):
-        session.transaction_data.setdefault(fromeid, []).append(toeid)
-
-
-# permissions synchronization hooks ############################################
-
-def after_add_permission(session, subject, rtype, object):
-    """added entity/relation *_permission, need to update schema"""
-    perm = rtype.split('_', 1)[0]
-    if session.describe(object)[0] == 'CWGroup':
-        MemSchemaPermissionCWGroupAdd(session, perm, subject, object)
-    else: # RQLExpression
-        expr = session.execute('Any EXPR WHERE X eid %(x)s, X expression EXPR',
-                               {'x': object}, 'x')[0][0]
-        MemSchemaPermissionRQLExpressionAdd(session, perm, subject, expr)
-
-
-def before_del_permission(session, subject, rtype, object):
-    """delete entity/relation *_permission, need to update schema
-
-    skip the operation if the related type is being deleted
-    """
-    if subject in session.transaction_data.get('pendingeids', ()):
-        return
-    perm = rtype.split('_', 1)[0]
-    if session.describe(object)[0] == 'CWGroup':
-        MemSchemaPermissionCWGroupDel(session, perm, subject, object)
-    else: # RQLExpression
-        expr = session.execute('Any EXPR WHERE X eid %(x)s, X expression EXPR',
-                               {'x': object}, 'x')[0][0]
-        MemSchemaPermissionRQLExpressionDel(session, perm, subject, expr)
-
-
-def rebuild_infered_relations(session, subject, rtype, object):
-    # registering a schema operation will trigger a call to
-    # repo.set_schema() on commit which will in turn rebuild
-    # infered relation definitions
-    MemSchemaNotifyChanges(session)
-
-
-def _register_schema_hooks(hm):
-    """register schema related hooks on the hooks manager"""
-    # schema synchronisation #####################
-    # before/after add
-    hm.register_hook(before_add_eetype, 'before_add_entity', 'CWEType')
-    hm.register_hook(before_add_ertype, 'before_add_entity', 'CWRType')
-    hm.register_hook(after_add_eetype, 'after_add_entity', 'CWEType')
-    hm.register_hook(after_add_ertype, 'after_add_entity', 'CWRType')
-    hm.register_hook(after_add_efrdef, 'after_add_entity', 'CWAttribute')
-    hm.register_hook(after_add_enfrdef, 'after_add_entity', 'CWRelation')
-    # before/after update
-    hm.register_hook(before_update_eetype, 'before_update_entity', 'CWEType')
-    hm.register_hook(before_update_ertype, 'before_update_entity', 'CWRType')
-    hm.register_hook(after_update_ertype, 'after_update_entity', 'CWRType')
-    hm.register_hook(after_update_erdef, 'after_update_entity', 'CWAttribute')
-    hm.register_hook(after_update_erdef, 'after_update_entity', 'CWRelation')
-    # before/after delete
-    hm.register_hook(before_del_eetype, 'before_delete_entity', 'CWEType')
-    hm.register_hook(after_del_eetype, 'after_delete_entity', 'CWEType')
-    hm.register_hook(before_del_ertype, 'before_delete_entity', 'CWRType')
-    hm.register_hook(after_del_relation_type, 'after_delete_relation', 'relation_type')
-    hm.register_hook(rebuild_infered_relations, 'after_add_relation', 'specializes')
-    hm.register_hook(rebuild_infered_relations, 'after_delete_relation', 'specializes')
-    # constraints synchronization hooks
-    hm.register_hook(after_add_econstraint, 'after_add_entity', 'CWConstraint')
-    hm.register_hook(after_update_econstraint, 'after_update_entity', 'CWConstraint')
-    hm.register_hook(before_delete_constrained_by, 'before_delete_relation', 'constrained_by')
-    hm.register_hook(after_add_constrained_by, 'after_add_relation', 'constrained_by')
-    # permissions synchronisation ################
-    for perm in ('read_permission', 'add_permission',
-                 'delete_permission', 'update_permission'):
-        hm.register_hook(after_add_permission, 'after_add_relation', perm)
-        hm.register_hook(before_del_permission, 'before_delete_relation', perm)
--- a/server/securityhooks.py	Fri Aug 14 00:02:08 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,97 +0,0 @@
-"""Security hooks: check permissions to add/delete/update entities according to
-the user connected to a session
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-__docformat__ = "restructuredtext en"
-
-from cubicweb import Unauthorized
-from cubicweb.server.pool import LateOperation
-from cubicweb.server import BEFORE_ADD_RELATIONS, ON_COMMIT_ADD_RELATIONS
-
-def check_entity_attributes(session, entity):
-    eid = entity.eid
-    eschema = entity.e_schema
-    # ._default_set is only there on entity creation to indicate unspecified
-    # attributes which has been set to a default value defined in the schema
-    defaults = getattr(entity, '_default_set', ())
-    try:
-        editedattrs = entity.edited_attributes
-    except AttributeError:
-        editedattrs = entity.keys()
-    for attr in editedattrs:
-        if attr in defaults:
-            continue
-        rschema = eschema.subject_relation(attr)
-        if rschema.is_final(): # non final relation are checked by other hooks
-            # add/delete should be equivalent (XXX: unify them into 'update' ?)
-            rschema.check_perm(session, 'add', eid)
-
-
-class CheckEntityPermissionOp(LateOperation):
-    def precommit_event(self):
-        #print 'CheckEntityPermissionOp', self.session.user, self.entity, self.action
-        self.entity.check_perm(self.action)
-        check_entity_attributes(self.session, self.entity)
-
-    def commit_event(self):
-        pass
-
-
-class CheckRelationPermissionOp(LateOperation):
-    def precommit_event(self):
-        self.rschema.check_perm(self.session, self.action, self.fromeid, self.toeid)
-
-    def commit_event(self):
-        pass
-
-def after_add_entity(session, entity):
-    if not session.is_super_session:
-        CheckEntityPermissionOp(session, entity=entity, action='add')
-
-def after_update_entity(session, entity):
-    if not session.is_super_session:
-        try:
-            # check user has permission right now, if not retry at commit time
-            entity.check_perm('update')
-            check_entity_attributes(session, entity)
-        except Unauthorized:
-            entity.clear_local_perm_cache('update')
-            CheckEntityPermissionOp(session, entity=entity, action='update')
-
-def before_del_entity(session, eid):
-    if not session.is_super_session:
-        eschema = session.repo.schema[session.describe(eid)[0]]
-        eschema.check_perm(session, 'delete', eid)
-
-
-def before_add_relation(session, fromeid, rtype, toeid):
-    if rtype in BEFORE_ADD_RELATIONS and not session.is_super_session:
-        rschema = session.repo.schema[rtype]
-        rschema.check_perm(session, 'add', fromeid, toeid)
-
-def after_add_relation(session, fromeid, rtype, toeid):
-    if not rtype in BEFORE_ADD_RELATIONS and not session.is_super_session:
-        rschema = session.repo.schema[rtype]
-        if rtype in ON_COMMIT_ADD_RELATIONS:
-            CheckRelationPermissionOp(session, action='add', rschema=rschema,
-                                      fromeid=fromeid, toeid=toeid)
-        else:
-            rschema.check_perm(session, 'add', fromeid, toeid)
-
-def before_del_relation(session, fromeid, rtype, toeid):
-    if not session.is_super_session:
-        session.repo.schema[rtype].check_perm(session, 'delete', fromeid, toeid)
-
-def register_security_hooks(hm):
-    """register meta-data related hooks on the hooks manager"""
-    hm.register_hook(after_add_entity, 'after_add_entity', '')
-    hm.register_hook(after_update_entity, 'after_update_entity', '')
-    hm.register_hook(before_del_entity, 'before_delete_entity', '')
-    hm.register_hook(before_add_relation, 'before_add_relation', '')
-    hm.register_hook(after_add_relation, 'after_add_relation', '')
-    hm.register_hook(before_del_relation, 'before_delete_relation', '')
-
--- a/server/serverconfig.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/server/serverconfig.py	Fri Aug 14 11:14:26 2009 +0200
@@ -82,7 +82,7 @@
     else:
         BACKUP_DIR = '/var/lib/cubicweb/backup/'
 
-    cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set(['sobjects'])
+    cubicweb_appobject_path = CubicWebConfiguration.cubicweb_appobject_path | set(['sobjects', 'hooks'])
     cube_appobject_path = CubicWebConfiguration.cube_appobject_path | set(['sobjects', 'hooks'])
 
     options = merge_options((
@@ -185,14 +185,9 @@
     # check user's state at login time
     consider_user_state = True
 
-    # hooks registration configuration
+    # hooks activation configuration
     # all hooks should be activated during normal execution
-    core_hooks = True
-    usergroup_hooks = True
-    schema_hooks = True
-    notification_hooks = True
-    security_hooks = True
-    instance_hooks = True
+    disabled_hooks_categories = set()
 
     # should some hooks be deactivated during [pre|post]create script execution
     free_wheel = False
@@ -256,22 +251,6 @@
         """pyro is always enabled in standalone repository configuration"""
         return True
 
-    def load_hooks(self, vreg):
-        hooks = {}
-        try:
-            apphookdefs = vreg['hooks'].all_objects()
-        except RegistryNotFound:
-            return hooks
-        for hookdef in apphookdefs:
-            # XXX < 3.5 bw compat
-            hookdef.__dict__['config'] = self
-            for event, ertype in hookdef.register_to(vreg.schema):
-                if ertype == 'Any':
-                    ertype = ''
-                cb = hookdef.make_callback(event)
-                hooks.setdefault(event, {}).setdefault(ertype, []).append(cb)
-        return hooks
-
     def load_schema(self, expand_cubes=False, **kwargs):
         from cubicweb.schema import CubicWebSchemaLoader
         if expand_cubes:
--- a/server/session.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/server/session.py	Fri Aug 14 11:14:26 2009 +0200
@@ -169,6 +169,18 @@
         assert prop == 'lang' # this is the only one changeable property for now
         self.set_language(value)
 
+    def deleted_in_transaction(self, eid):
+        return eid in self.transaction_data.get('pendingeids', ())
+
+    def added_in_transaction(self, eid):
+        return eid in self.transaction_data.get('neweids', ())
+
+    def schema_rproperty(self, rtype, eidfrom, eidto, rprop):
+        rschema = self.repo.schema[rtype]
+        subjtype = self.describe(eidfrom)[0]
+        objtype = self.describe(eidto)[0]
+        return rschema.rproperty(subjtype, objtype, rprop)
+
     # connection management ###################################################
 
     def keep_pool_mode(self, mode):
--- a/sobjects/email.py	Fri Aug 14 00:02:08 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-"""hooks to ensure use_email / primary_email relations consistency
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-__docformat__ = "restructuredtext en"
-
-from cubicweb.server.hooksmanager import Hook
-from cubicweb.server.pool import PreCommitOperation
-
-class SetUseEmailRelationOp(PreCommitOperation):
-    """delay this operation to commit to avoid conflict with a late rql query
-    already setting the relation
-    """
-    rtype = 'use_email'
-    fromeid = toeid = None # make pylint happy
-
-    def condition(self):
-        """check entity has use_email set for the email address"""
-        return not self.session.unsafe_execute(
-            'Any X WHERE X eid %(x)s, X use_email Y, Y eid %(y)s',
-            {'x': self.fromeid, 'y': self.toeid}, 'x')
-
-    def precommit_event(self):
-        session = self.session
-        if self.condition():
-            session.unsafe_execute(
-                'SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % self.rtype,
-                {'x': self.fromeid, 'y': self.toeid}, 'x')
-
-class SetPrimaryEmailRelationOp(SetUseEmailRelationOp):
-    rtype = 'primary_email'
-
-    def condition(self):
-        """check entity has no primary_email set"""
-        return not self.session.unsafe_execute(
-            'Any X WHERE X eid %(x)s, X primary_email Y',
-            {'x': self.fromeid}, 'x')
-
-
-class SetPrimaryEmailHook(Hook):
-    """notify when a bug or story or version has its state modified"""
-    events = ('after_add_relation',)
-    accepts = ('use_email',)
-
-    def call(self, session, fromeid, rtype, toeid):
-        subjtype = session.describe(fromeid)[0]
-        eschema = self.vreg.schema[subjtype]
-        if 'primary_email' in eschema.subject_relations():
-            SetPrimaryEmailRelationOp(session, vreg=self.vreg,
-                                      fromeid=fromeid, toeid=toeid)
-
-class SetUseEmailHook(Hook):
-    """notify when a bug or story or version has its state modified"""
-    events = ('after_add_relation',)
-    accepts = ('primary_email',)
-
-    def call(self, session, fromeid, rtype, toeid):
-        subjtype = session.describe(fromeid)[0]
-        eschema = self.vreg.schema[subjtype]
-        if 'use_email' in eschema.subject_relations():
-            SetUseEmailRelationOp(session, vreg=self.vreg,
-                                  fromeid=fromeid, toeid=toeid)
--- a/sobjects/hooks.py	Fri Aug 14 00:02:08 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-"""various library content hooks
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-__docformat__ = "restructuredtext en"
-
-from datetime import datetime
-
-from cubicweb import RepositoryError
-from cubicweb.common.uilib import soup2xhtml
-from cubicweb.server.hooksmanager import Hook
-from cubicweb.server.pool import PreCommitOperation
-
-
-class SetModificationDateOnStateChange(Hook):
-    """update entity's modification date after changing its state"""
-    events = ('after_add_relation',)
-    accepts = ('in_state',)
-
-    def call(self, session, fromeid, rtype, toeid):
-        if fromeid in session.transaction_data.get('neweids', ()):
-            # new entity, not needed
-            return
-        entity = session.entity_from_eid(fromeid)
-        try:
-            entity.set_attributes(modification_date=datetime.now())
-        except RepositoryError, ex:
-            # usually occurs if entity is coming from a read-only source
-            # (eg ldap user)
-            self.warning('cant change modification date for %s: %s', entity, ex)
-
-
-class AddUpdateCWUserHook(Hook):
-    """ensure user logins are stripped"""
-    events = ('before_add_entity', 'before_update_entity',)
-    accepts = ('CWUser',)
-
-    def call(self, session, entity):
-        if 'login' in entity and entity['login']:
-            entity['login'] = entity['login'].strip()
-
-
-class AutoDeleteBookmark(PreCommitOperation):
-    beid = None # make pylint happy
-    def precommit_event(self):
-        session = self.session
-        if not self.beid in session.transaction_data.get('pendingeids', ()):
-            if not session.unsafe_execute('Any X WHERE X bookmarked_by U, X eid %(x)s',
-                                          {'x': self.beid}, 'x'):
-                session.unsafe_execute('DELETE Bookmark X WHERE X eid %(x)s',
-                                       {'x': self.beid}, 'x')
-
-class DelBookmarkedByHook(Hook):
-    """ensure user logins are stripped"""
-    events = ('after_delete_relation',)
-    accepts = ('bookmarked_by',)
-
-    def call(self, session, subj, rtype, obj):
-        AutoDeleteBookmark(session, beid=subj)
-
-
-class TidyHtmlFields(Hook):
-    """tidy HTML in rich text strings"""
-    events = ('before_add_entity', 'before_update_entity')
-    accepts = ('Any',)
-
-    def call(self, session, entity):
-        metaattrs = entity.e_schema.meta_attributes()
-        for metaattr, (metadata, attr) in metaattrs.iteritems():
-            if metadata == 'format':
-                try:
-                    value = entity[attr]
-                except KeyError:
-                    continue # no text to tidy
-                if isinstance(value, unicode): # filter out None and Binary
-                    if self.event == 'before_add_entity':
-                        fmt = entity.get(metaattr)
-                    else:
-                        fmt = entity.get_value(metaattr)
-                    if fmt == 'text/html':
-                        entity[attr] = soup2xhtml(value, session.encoding)
--- a/sobjects/notification.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/sobjects/notification.py	Fri Aug 14 11:14:26 2009 +0200
@@ -1,4 +1,4 @@
-"""some hooks and views to handle notification on entity's changes
+"""some views to handle notification on data changes
 
 :organization: Logilab
 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
@@ -18,16 +18,12 @@
         return 'XXX'
 
 from logilab.common.textutils import normalize_text
-from logilab.common.deprecation import class_renamed
 
-from cubicweb import RegistryException
-from cubicweb.selectors import implements, yes
+from cubicweb.selectors import yes
 from cubicweb.view import EntityView, Component
 from cubicweb.common.mail import format_mail
 
-from cubicweb.server.pool import PreCommitOperation
 from cubicweb.server.hookhelper import SendMailOp
-from cubicweb.server.hooksmanager import Hook
 
 
 class RecipientsFinder(Component):
@@ -57,72 +53,6 @@
         return dests
 
 
-# hooks #######################################################################
-
-class RenderAndSendNotificationView(PreCommitOperation):
-    """delay rendering of notification view until precommit"""
-    def precommit_event(self):
-        if self.view.rset and self.view.rset[0][0] in self.session.transaction_data.get('pendingeids', ()):
-            return # entity added and deleted in the same transaction
-        self.view.render_and_send(**getattr(self, 'viewargs', {}))
-
-class StatusChangeHook(Hook):
-    """notify when a workflowable entity has its state modified"""
-    events = ('after_add_entity',)
-    accepts = ('TrInfo',)
-
-    def call(self, session, entity):
-        if not entity.from_state: # not a transition
-            return
-        rset = entity.related('wf_info_for')
-        try:
-            view = session.vreg['views'].select('notif_status_change', session,
-                                                rset=rset, row=0)
-        except RegistryException:
-            return
-        comment = entity.printable_value('comment', format='text/plain')
-        if comment:
-            comment = normalize_text(comment, 80,
-                                     rest=entity.comment_format=='text/rest')
-        RenderAndSendNotificationView(session, view=view, viewargs={
-            'comment': comment, 'previous_state': entity.previous_state.name,
-            'current_state': entity.new_state.name})
-
-
-class RelationChangeHook(Hook):
-    events = ('before_add_relation', 'after_add_relation',
-              'before_delete_relation', 'after_delete_relation')
-    accepts = ('Any',)
-    def call(self, session, fromeid, rtype, toeid):
-        """if a notification view is defined for the event, send notification
-        email defined by the view
-        """
-        rset = session.eid_rset(fromeid)
-        vid = 'notif_%s_%s' % (self.event,  rtype)
-        try:
-            view = session.vreg['views'].select(vid, session, rset=rset, row=0)
-        except RegistryException:
-            return
-        RenderAndSendNotificationView(session, view=view)
-
-
-class EntityChangeHook(Hook):
-    events = ('after_add_entity',
-              'after_update_entity')
-    accepts = ('Any',)
-    def call(self, session, entity):
-        """if a notification view is defined for the event, send notification
-        email defined by the view
-        """
-        rset = entity.as_rset()
-        vid = 'notif_%s' % self.event
-        try:
-            view = session.vreg['views'].select(vid, session, rset=rset, row=0)
-        except RegistryException:
-            return
-        RenderAndSendNotificationView(session, view=view)
-
-
 # abstract or deactivated notification views and mixin ########################
 
 class NotificationView(EntityView):
@@ -296,4 +226,7 @@
         return  u'%s #%s (%s)' % (self.req.__('New %s' % entity.e_schema),
                                   entity.eid, self.user_login())
 
+from logilab.common.deprecation import class_renamed, class_moved
 NormalizedTextView = class_renamed('NormalizedTextView', ContentAddedView)
+from cubicweb.hooks.notification import RenderAndSendNotificationView
+RenderAndSendNotificationView = class_moved(RenderAndSendNotificationView)
--- a/sobjects/supervising.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/sobjects/supervising.py	Fri Aug 14 11:14:26 2009 +0200
@@ -13,58 +13,9 @@
 from cubicweb.schema import display_name
 from cubicweb.view import Component
 from cubicweb.common.mail import format_mail
-from cubicweb.server.hooksmanager import Hook
 from cubicweb.server.hookhelper import SendMailOp
 
 
-class SomethingChangedHook(Hook):
-    events = ('before_add_relation', 'before_delete_relation',
-              'after_add_entity', 'before_update_entity')
-    accepts = ('Any',)
-
-    def call(self, session, *args):
-        dest = session.vreg.config['supervising-addrs']
-        if not dest: # no supervisors, don't do this for nothing...
-            return
-        self.session = session
-        if self._call(*args):
-            SupervisionMailOp(session)
-
-    def _call(self, *args):
-        if self._event() == 'update_entity':
-            if args[0].eid in self.session.transaction_data.get('neweids', ()):
-                return False
-            if args[0].e_schema == 'CWUser':
-                updated = set(args[0].iterkeys())
-                if not (updated - frozenset(('eid', 'modification_date',
-                                             'last_login_time'))):
-                    # don't record last_login_time update which are done
-                    # automatically at login time
-                    return False
-        self.session.transaction_data.setdefault('pendingchanges', []).append(
-            (self._event(), args))
-        return True
-
-    def _event(self):
-        return self.event.split('_', 1)[1]
-
-
-class EntityDeleteHook(SomethingChangedHook):
-    events = ('before_delete_entity',)
-
-    def _call(self, eid):
-        entity = self.session.entity_from_eid(eid)
-        try:
-            title = entity.dc_title()
-        except:
-            # may raise an error during deletion process, for instance due to
-            # missing required relation
-            title = '#%s' % eid
-        self.session.transaction_data.setdefault('pendingchanges', []).append(
-            ('delete_entity', (eid, str(entity.e_schema), title)))
-        return True
-
-
 def filter_changes(changes):
     """
     * when an entity has been deleted:
@@ -79,7 +30,7 @@
     for change in changes[:]:
         event, changedescr = change
         if event == 'add_entity':
-            entity = changedescr[0]
+            entity = changedescr.entity
             added.add(entity.eid)
             if entity.e_schema == 'TrInfo':
                 changes.remove(change)
@@ -111,14 +62,14 @@
                 changedescr = change[1]
                 # skip meta-relations which are set automatically
                 # XXX generate list below using rtags (category = 'generated')
-                if changedescr[1] in ('created_by', 'owned_by', 'is', 'is_instance_of',
+                if changedescr.rtype in ('created_by', 'owned_by', 'is', 'is_instance_of',
                                       'from_state', 'to_state', 'wf_info_for',) \
-                       and changedescr[0] == eid:
+                       and changedescr.eidfrom == eid:
                     index['add_relation'].remove(change)
                 # skip in_state relation if the entity is being created
                 # XXX this may be automatized by skipping all mandatory relation
                 #     at entity creation time
-                elif changedescr[1] == 'in_state' and changedescr[0] in added:
+                elif changedescr.rtype == 'in_state' and changedescr.eidfrom in added:
                     index['add_relation'].remove(change)
 
         except KeyError:
@@ -126,15 +77,14 @@
     for eid in deleted:
         try:
             for change in index['delete_relation'].copy():
-                fromeid, rtype, toeid = change[1]
-                if fromeid == eid:
+                if change.eidfrom == eid:
                     index['delete_relation'].remove(change)
-                elif toeid == eid:
+                elif change.eidto == eid:
                     index['delete_relation'].remove(change)
-                    if rtype == 'wf_info_for':
-                        for change in index['delete_entity'].copy():
-                            if change[1][0] == fromeid:
-                                index['delete_entity'].remove(change)
+                    if change.rtype == 'wf_info_for':
+                        for change_ in index['delete_entity'].copy():
+                            if change_[1].eidfrom == change.eidfrom:
+                                index['delete_entity'].remove(change_)
         except KeyError:
             break
     for change in changes:
@@ -161,7 +111,7 @@
                % user.login)
         for event, changedescr in filter_changes(changes):
             self.w(u'* ')
-            getattr(self, event)(*changedescr)
+            getattr(self, event)(changedescr)
             self.w(u'\n\n')
 
     def _entity_context(self, entity):
@@ -169,30 +119,30 @@
                 'etype': entity.dc_type().lower(),
                 'title': entity.dc_title()}
 
-    def add_entity(self, entity):
+    def add_entity(self, changedescr):
         msg = self.req._('added %(etype)s #%(eid)s (%(title)s)')
-        self.w(u'%s\n' % (msg % self._entity_context(entity)))
-        self.w(u'  %s' % entity.absolute_url())
+        self.w(u'%s\n' % (msg % self._entity_context(changedescr.entity)))
+        self.w(u'  %s' % changedescr.entity.absolute_url())
 
-    def update_entity(self, entity):
+    def update_entity(self, changedescr):
         msg = self.req._('updated %(etype)s #%(eid)s (%(title)s)')
-        self.w(u'%s\n' % (msg % self._entity_context(entity)))
+        self.w(u'%s\n' % (msg % self._entity_context(changedescr.entity)))
         # XXX print changes
-        self.w(u'  %s' % entity.absolute_url())
+        self.w(u'  %s' % changedescr.entity.absolute_url())
 
-    def delete_entity(self, eid, etype, title):
+    def delete_entity(self, (eid, etype, title)):
         msg = self.req._('deleted %(etype)s #%(eid)s (%(title)s)')
         etype = display_name(self.req, etype).lower()
         self.w(msg % locals())
 
-    def change_state(self, entity, fromstate, tostate):
+    def change_state(self, (entity, fromstate, tostate)):
         msg = self.req._('changed state of %(etype)s #%(eid)s (%(title)s)')
         self.w(u'%s\n' % (msg % self._entity_context(entity)))
         self.w(_('  from state %(fromstate)s to state %(tostate)s\n' %
                  {'fromstate': _(fromstate.name), 'tostate': _(tostate.name)}))
         self.w(u'  %s' % entity.absolute_url())
 
-    def _relation_context(self, fromeid, rtype, toeid):
+    def _relation_context(self, changedescr):
         _ = self.req._
         session = self.req.actual_session()
         def describe(eid):
@@ -202,19 +152,20 @@
                 # may occurs when an entity has been deleted from an external
                 # source and we're cleaning its relation
                 return _('unknown external entity')
+        eidfrom, rtype, eidto = changedescr.eidfrom, changedescr.rtype, changedescr.eidto
         return {'rtype': _(rtype),
-                'fromeid': fromeid,
-                'frometype': describe(fromeid),
-                'toeid': toeid,
-                'toetype': describe(toeid)}
+                'eidfrom': eidfrom,
+                'frometype': describe(eidfrom),
+                'eidto': eidto,
+                'toetype': describe(eidto)}
 
-    def add_relation(self, fromeid, rtype, toeid):
-        msg = self.req._('added relation %(rtype)s from %(frometype)s #%(fromeid)s to %(toetype)s #%(toeid)s')
-        self.w(msg % self._relation_context(fromeid, rtype, toeid))
+    def add_relation(self, changedescr):
+        msg = self.req._('added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%(eidto)s')
+        self.w(msg % self._relation_context(changedescr))
 
-    def delete_relation(self, fromeid, rtype, toeid):
-        msg = self.req._('deleted relation %(rtype)s from %(frometype)s #%(fromeid)s to %(toetype)s #%(toeid)s')
-        self.w(msg % self._relation_context(fromeid, rtype, toeid))
+    def delete_relation(self, eidfrom, rtype, eidto):
+        msg = self.req._('deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%(eidto)s')
+        self.w(msg % self._relation_context(changedescr))
 
 
 class SupervisionMailOp(SendMailOp):
--- a/test/unittest_cwconfig.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/test/unittest_cwconfig.py	Fri Aug 14 11:14:26 2009 +0200
@@ -77,7 +77,7 @@
 
     def test_vregistry_path(self):
         self.assertEquals([unabsolutize(p) for p in self.config.vregistry_path()],
-                          ['entities', 'web/views', 'sobjects',
+                          ['entities', 'web/views', 'sobjects', 'hooks',
                            'file/entities.py', 'file/views', 'file/hooks.py',
                            'email/entities.py', 'email/views', 'email/hooks.py',
                            'test/data/entities.py'])
--- a/test/unittest_entity.py	Fri Aug 14 00:02:08 2009 +0200
+++ b/test/unittest_entity.py	Fri Aug 14 11:14:26 2009 +0200
@@ -101,7 +101,7 @@
         user = self.entity('Any X WHERE X eid %(x)s', {'x':self.user().eid}, 'x')
         adeleid = self.execute('INSERT EmailAddress X: X address "toto@logilab.org", U use_email X WHERE U login "admin"')[0][0]
         self.commit()
-        self.assertEquals(user._related_cache.keys(), [])
+        self.assertEquals(user._related_cache, {})
         email = user.primary_email[0]
         self.assertEquals(sorted(user._related_cache), ['primary_email_subject'])
         self.assertEquals(email._related_cache.keys(), ['primary_email_object'])