[hooks] major refactoring:
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 14 Aug 2009 09:26:41 +0200
changeset 2835 04034421b072
parent 2834 7df3494ae657
child 2839 6419af16faa0
[hooks] major refactoring: * they are no "proper" appobject, selected when an event is fired according to its context * new module cubicweb.server.hook containing the Hook class and Operation class * deprecated SystemHook and PreCommitOperation classes * rewrite core server hooks as appobjects in the cubicweb/hooks directory -> deprecates hooksmanager, remove hooks, schemahooks, securityhooks cubicweb.server sub-modules -> new cubicweb.hooks sub-package -> get back to a (somewhat) working state
debian/cubicweb-dev.install.in
debian/cubicweb-server.install.in
debian/rules
hooks/__init__.py
hooks/integrity.py
hooks/metadata.py
hooks/security.py
hooks/syncschema.py
hooks/syncsession.py
hooks/workflow.py
server/hook.py
server/hooks.py
server/hooksmanager.py
server/migractions.py
server/pool.py
server/repository.py
server/schemahooks.py
server/securityhooks.py
server/serverconfig.py
test/unittest_cwconfig.py
test/unittest_entity.py
--- a/debian/cubicweb-dev.install.in	Fri Aug 14 09:20:33 2009 +0200
+++ b/debian/cubicweb-dev.install.in	Fri Aug 14 09:26:41 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 09:20:33 2009 +0200
+++ b/debian/cubicweb-server.install.in	Fri Aug 14 09:26:41 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 09:20:33 2009 +0200
+++ b/debian/rules	Fri Aug 14 09:26:41 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 09:26:41 2009 +0200
@@ -0,0 +1,1 @@
+"""core hooks"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/integrity.py	Fri Aug 14 09:26:41 2009 +0200
@@ -0,0 +1,228 @@
+"""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.server.hook import Hook
+from cubicweb.server.pool import LateOperation, PreCommitOperation
+from cubicweb.server.hookhelper import rproperty
+
+# 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(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'
+
+
+class CheckCardinalityHook(Hook):
+    """check cardinalities are satisfied"""
+    __id__ = 'checkcard'
+    category = 'integrity'
+    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 = 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:
+            self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom)
+        if card[1] in '1+' and not eidto in pendingeids:
+            self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto)
+
+
+class _CheckConstraintsOp(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
+
+
+class CheckConstraintHook(Hook):
+    """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'
+    category = 'integrity'
+    events = ('after_add_relation',)
+    def __call__(self):
+        constraints = rproperty(self.cw_req, self.rtype, self.eidfrom, self.eidto,
+                                'constraints')
+        if constraints:
+            _CheckConstraintsOp(self.cw_req, constraints=constraints,
+                               rdef=(self.eidfrom, self.rtype, self.eidto))
+
+class CheckUniqueHook(Hook):
+    __id__ = 'checkunique'
+    category = 'integrity'
+    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(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')
+
+
+class DeleteCompositeOrphanHook(Hook):
+    """delete the composed of a composite relation when this relation is deleted
+    """
+    __id__ = 'deletecomposite'
+    category = 'integrity'
+    events = ('before_delete_relation',)
+    def __call__(self):
+        composite = rproperty(self.cw_req, 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(Hook):
+    """delete the composed of a composite relation when this relation is deleted
+    """
+    __id__ = 'checkownersgroup'
+    __select__ = Hook.__select__ & entity_implements('CWGroup')
+    category = 'integrity'
+    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
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/metadata.py	Fri Aug 14 09:26:41 2009 +0200
@@ -0,0 +1,159 @@
+"""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.hook import Hook
+from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation
+from cubicweb.server.hookhelper import rproperty
+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 InitMetaAttrsHook(Hook):
+    """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',)
+    category = 'metadata'
+
+    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(Hook):
+    """update an entity -> set modification date"""
+    id = 'metaattrsupdate'
+    events = ('before_update_entity',)
+    category = 'metadata'
+    def __call__(self):
+        self.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)
+
+
+class SetIsHook(Hook):
+    """create a new entity -> set is relation"""
+    id = 'setis'
+    events = ('after_add_entity',)
+    category = 'metadata'
+    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(Hook):
+    """create a new entity -> set owner and creator metadata"""
+    id = 'setowner'
+    events = ('after_add_entity',)
+    category = 'metadata'
+    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(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'))
+
+class SyncCompositeOwner(Hook):
+    """when adding composite relation, the composed should have the same owners
+    has the composite
+    """
+    id = 'synccompositeowner'
+    events = ('after_add_relation',)
+    category = 'metadata'
+    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 = rproperty(self.cw_req, 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(Hook):
+    """when a user has been created, add owned_by relation on itself"""
+    id = 'fixuserowner'
+    __select__ = Hook.__select__ & entity_implements('CWUser')
+    events = ('after_add_entity',)
+    category = 'metadata'
+    def __call__(self):
+        self.cw_req.add_relation(self.entity.eid, 'owned_by', self.entity.eid)
+
+
+class UpdateFTIHook(Hook):
+    """sync fulltext index when relevant relation is added / removed
+    """
+    id = 'updateftirel'
+    events = ('after_add_relation', 'after_delete_relation')
+    category = 'metadata'
+
+    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/security.py	Fri Aug 14 09:26:41 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 09:26:41 2009 +0200
@@ -0,0 +1,1100 @@
+"""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 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(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 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 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)
+
+
+# deletion hooks ###############################################################
+
+class DelCWETypeHook(hook.Hook):
+    """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__ = hook.Hook.__select__ & entity_implements('CWEType')
+    category = 'syncschema'
+    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
+
+class DelCWRTypeHook(hook.Hook):
+    """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__ = hook.Hook.__select__ & entity_implements('CWRType')
+    category = 'syncschema'
+    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)
+
+
+
+class AfterDelRelationTypeHook(hook.Hook):
+    """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__ = hook.Hook.__select__ & hook.match_rtype('relation_type')
+    category = 'syncschema'
+    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))
+
+
+class AfterAddCWAttributeHook(hook.Hook):
+    __id__ = 'syncaddcwattribute'
+    __select__ = hook.Hook.__select__ & entity_implements('CWAttribute')
+    category = 'syncschema'
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        SourceDbCWAttributeAdd(self.cw_req, entity=self.entity)
+
+
+class AfterAddCWRelationHook(AfterAddCWAttributeHook):
+    __id__ = 'syncaddcwrelation'
+    __select__ = hook.Hook.__select__ & entity_implements('CWRelation')
+
+    def __call__(self):
+        SourceDbCWRelationAdd(self.cw_req, entity=self.entity)
+
+
+class AfterUpdateCWRDefHook(hook.Hook):
+    __id__ = 'syncaddcwattribute'
+    __select__ = hook.Hook.__select__ & entity_implements('CWAttribute', 'CWRelation')
+    category = 'syncschema'
+    events = ('after_update_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        if entity.eid in self.cw_req.transaction_data.get('pendingeids', ()):
+            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(hook.Hook):
+    __id__ = 'syncaddcwconstraint'
+    __select__ = hook.Hook.__select__ & entity_implements('CWConstraint')
+    category = 'syncschema'
+    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(hook.Hook):
+    __id__ = 'syncdelconstrainedby'
+    __select__ = hook.Hook.__select__ & hook.match_rtype('constrainted_by')
+    category = 'syncschema'
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        if self.eidfrom in self.cw_req.transaction_data.get('neweids', ()):
+            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.eidfrom in self.cw_req.transaction_data.get('pendingeids', ()):
+            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(hook.Hook):
+    """added entity/relation *_permission, need to update schema"""
+    __id__ = 'syncaddperm'
+    __select__ = hook.Hook.__select__ & hook.match_rtype(
+        'read_permission', 'add_permission', 'delete_permission',
+        'update_permission')
+    category = 'syncschema'
+    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.eidfrom in self.cw_req.transaction_data.get('pendingeids', ()):
+            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(hook.Hook):
+    __id__ = 'syncspecializes'
+    __select__ = hook.Hook.__select__ & hook.match_rtype('specializes')
+    category = 'syncschema'
+    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 09:26:41 2009 +0200
@@ -0,0 +1,229 @@
+"""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.hook import Hook, match_rtype
+from cubicweb.server.pool import Operation
+from cubicweb.server.hookhelper import get_user_sessions
+
+
+# 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
+
+
+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(Hook):
+    __id__ = 'syncingroup'
+    __select__ = Hook.__select__ & match_rtype('in_group')
+    events = ('after_delete_relation', 'after_add_relation')
+    category = 'syncsession'
+
+    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(Operation):
+    """close associated user's session when it is deleted"""
+    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
+
+
+class CloseDeletedUserSessionsHook(Hook):
+    __id__ = 'closession'
+    __select__ = Hook.__select__ & entity_implements('CWUser')
+    events = ('after_delete_entity',)
+    category = 'syncsession'
+
+    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(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
+
+
+class AddCWPropertyHook(Hook):
+    __id__ = 'addcwprop'
+    __select__ = Hook.__select__ & entity_implements('CWProperty')
+    category = 'syncsession'
+    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(Hook):
+    __id__ = 'addcwpropforuser'
+    __select__ = Hook.__select__ & match_rtype('for_user')
+    events = ('after_add_relation',)
+    category = 'syncsession'
+
+    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 09:26:41 2009 +0200
@@ -0,0 +1,106 @@
+"""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 cubicweb import ValidationError
+from cubicweb.interfaces import IWorkflowable
+from cubicweb.selectors import entity_implements
+from cubicweb.server.hook import Hook, match_rtype
+from cubicweb.server.pool import PreCommitOperation
+from cubicweb.server.hookhelper import previous_state
+
+
+def relation_deleted(session, eidfrom, rtype, eidto):
+    session.transaction_data.setdefault('pendingrelations', []).append(
+        (eidfrom, rtype, eidto))
+
+
+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])
+
+
+class SetInitialStateHook(Hook):
+    __id__ = 'wfsetinitial'
+    __select__ = Hook.__select__ & entity_implements(IWorkflowable)
+    category = 'worfklow'
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        _SetInitialStateOp(self.cw_req, entity=self.entity)
+
+
+class PrepareStateChangeHook(Hook):
+    """record previous state information"""
+    __id__ = 'cwdelstate'
+    __select__ = Hook.__select__ & match_rtype('in_state')
+    category = 'worfklow'
+    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')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/server/hook.py	Fri Aug 14 09:26:41 2009 +0200
@@ -0,0 +1,311 @@
+"""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):
+        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
+        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"""
+
+    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
--- a/server/hooks.py	Fri Aug 14 09:20:33 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 09:20:33 2009 +0200
+++ b/server/hooksmanager.py	Fri Aug 14 09:26:41 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 09:20:33 2009 +0200
+++ b/server/migractions.py	Fri Aug 14 09:26:41 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 09:20:33 2009 +0200
+++ b/server/pool.py	Fri Aug 14 09:26:41 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 09:20:33 2009 +0200
+++ b/server/repository.py	Fri Aug 14 09:26:41 2009 +0200
@@ -41,7 +41,6 @@
 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
 
 
@@ -173,8 +172,6 @@
         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))
@@ -185,7 +182,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')
@@ -222,13 +219,13 @@
             self.pools.append(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 ###############################################################
 
@@ -248,22 +245,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())
+        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,39 +265,10 @@
                 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'
@@ -578,7 +537,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 +628,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 +809,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 +834,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',
@@ -998,13 +957,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)
@@ -1035,13 +994,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 +1033,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 +1052,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 +1068,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 +1082,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 +1097,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 +1112,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 +1122,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 09:20:33 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 09:20:33 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 09:20:33 2009 +0200
+++ b/server/serverconfig.py	Fri Aug 14 09:26:41 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/test/unittest_cwconfig.py	Fri Aug 14 09:20:33 2009 +0200
+++ b/test/unittest_cwconfig.py	Fri Aug 14 09:26:41 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 09:20:33 2009 +0200
+++ b/test/unittest_entity.py	Fri Aug 14 09:26:41 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'])