rewrite hooks in sobjects as new Hook style into hooks sub-package
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 14 Aug 2009 11:14:10 +0200
changeset 2841 107ba1c45227
parent 2840 06daf13195d4
child 2842 0477fff5f897
rewrite hooks in sobjects as new Hook style into hooks sub-package
hooks/bookmark.py
hooks/email.py
hooks/integrity.py
hooks/metadata.py
hooks/notification.py
hooks/syncschema.py
hooks/syncsession.py
hooks/workflow.py
sobjects/email.py
sobjects/hooks.py
sobjects/notification.py
sobjects/supervising.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/bookmark.py	Fri Aug 14 11:14:10 2009 +0200
@@ -0,0 +1,31 @@
+"""bookmark related hooks
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb.selectors import entity_implements
+from cubicweb.server import hook
+
+
+class AutoDeleteBookmarkOp(hook.Operation):
+    bookmark = None # make pylint happy
+    def precommit_event(self):
+        if not self.session.deleted_in_transaction(self.bookmark.eid):
+            if not self.bookmark.bookmarked_by:
+                self.bookmark.delete()
+
+
+class DelBookmarkedByHook(hook.Hook):
+    """ensure user logins are stripped"""
+    __id__ = 'autodelbookmark'
+    __select__ = hook.Hook.__select__ & entity_implements('bookmarked_by',)
+    category = 'bookmark'
+    events = ('after_delete_relation',)
+
+    def __call__(self):
+        AutoDeleteBookmarkOp(self.cw_req,
+                             bookmark=self.cw_req.entity_from_eid(self.eidfrom))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/email.py	Fri Aug 14 11:14:10 2009 +0200
@@ -0,0 +1,62 @@
+"""hooks to ensure use_email / primary_email relations consistency
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb.server import hook
+
+class SetUseEmailRelationOp(hook.Operation):
+    """delay this operation to commit to avoid conflict with a late rql query
+    already setting the relation
+    """
+    rtype = 'use_email'
+    entity = email = None # make pylint happy
+
+    def condition(self):
+        """check entity has use_email set for the email address"""
+        return not any(e for e in self.entity.use_email
+                       if self.email.eid == e.eid)
+
+    def precommit_event(self):
+        if self.condition():
+            self.session.unsafe_execute(
+                'SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % self.rtype,
+                {'x': self.entity.eid, 'y': self.email.eid}, 'x')
+
+class SetPrimaryEmailRelationOp(SetUseEmailRelationOp):
+    rtype = 'primary_email'
+
+    def condition(self):
+        """check entity has no primary_email set"""
+        return not self.entity.primary_email
+
+
+class SetPrimaryEmailHook(hook.Hook):
+    """notify when a bug or story or version has its state modified"""
+    __id__ = 'setprimaryemail'
+    __select__ = hook.Hook.__select__ & hook.match_rtype('use_email')
+    category = 'email'
+    events = ('after_add_relation',)
+
+    def call(self, session, eidfrom, rtype, eidto):
+        entity = self.cw_req.entity_from_eid(self.eidfrom)
+        if 'primary_email' in entity.e_schema.subject_relations():
+            SetPrimaryEmailRelationOp(self.cw_req, entity=entity,
+                                      email=self.cw_req.entity_from_eid(self.eidto))
+
+class SetUseEmailHook(hook.Hook):
+    """notify when a bug or story or version has its state modified"""
+    __id__ = 'setprimaryemail'
+    __select__ = hook.Hook.__select__ & hook.match_rtype('primary_email')
+    category = 'email'
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        entity = self.cw_req.entity_from_eid(self.eidfrom)
+        if 'use_email' in entity.e_schema.subject_relations():
+            SetUseEmailRelationOp(self.cw_req, entity=entity,
+                                  email=self.cw_req.entity_from_eid(self.eidto))
--- a/hooks/integrity.py	Fri Aug 14 11:13:18 2009 +0200
+++ b/hooks/integrity.py	Fri Aug 14 11:14:10 2009 +0200
@@ -10,9 +10,9 @@
 
 from cubicweb import ValidationError
 from cubicweb.selectors import entity_implements
-from cubicweb.server.hook import Hook
+from cubicweb.common.uilib import soup2xhtml
+from cubicweb.server 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)
@@ -23,7 +23,7 @@
                                 'wf_info_for', 'from_state', 'to_state'))
 
 
-class _CheckRequiredRelationOperation(LateOperation):
+class _CheckRequiredRelationOperation(hook.LateOperation):
     """checking relation cardinality has to be done after commit in
     case the relation is being replaced
     """
@@ -31,7 +31,7 @@
 
     def precommit_event(self):
         # recheck pending eids
-        if self.eid in self.session.transaction_data.get('pendingeids', ()):
+        if self.session.deleted_in_transaction(self.eid):
             return
         if self.session.unsafe_execute(*self._rql()).rowcount < 1:
             etype = self.session.describe(self.eid)[0]
@@ -59,10 +59,14 @@
         return 'Any S WHERE O eid %%(x)s, S %s O' % self.rtype, {'x': self.eid}, 'x'
 
 
-class CheckCardinalityHook(Hook):
+class IntegrityHook(hook.Hook):
+    __abstract__ = True
+    category = 'integrity'
+
+
+class CheckCardinalityHook(IntegrityHook):
     """check cardinalities are satisfied"""
     __id__ = 'checkcard'
-    category = 'integrity'
     events = ('after_add_entity', 'before_delete_relation')
 
     def __call__(self):
@@ -103,28 +107,26 @@
             return
         session = self.cw_req
         eidfrom, eidto = self.eidfrom, self.eidto
-        card = rproperty(session, rtype, eidfrom, eidto, 'cardinality')
+        card = session.schema_rproperty(rtype, eidfrom, eidto, 'cardinality')
         pendingrdefs = session.transaction_data.get('pendingrdefs', ())
         if (session.describe(eidfrom)[0], rtype, session.describe(eidto)[0]) in pendingrdefs:
             return
-        pendingeids = session.transaction_data.get('pendingeids', ())
-        if card[0] in '1+' and not eidfrom in pendingeids:
+        if card[0] in '1+' and not session.deleted_in_transaction(eidfrom):
             self.checkrel_if_necessary(_CheckSRelationOp, rtype, eidfrom)
-        if card[1] in '1+' and not eidto in pendingeids:
+        if card[1] in '1+' and not session.deleted_in_transaction(eidto):
             self.checkrel_if_necessary(_CheckORelationOp, rtype, eidto)
 
 
-class _CheckConstraintsOp(LateOperation):
+class _CheckConstraintsOp(hook.LateOperation):
     """check a new relation satisfy its constraints
     """
     def precommit_event(self):
         eidfrom, rtype, eidto = self.rdef
         # first check related entities have not been deleted in the same
         # transaction
-        pending = self.session.transaction_data.get('pendingeids', ())
-        if eidfrom in pending:
+        if self.session.deleted_in_transaction(eidfrom):
             return
-        if eidto in pending:
+        if self.session.deleted_in_transaction(eidto):
             return
         for constraint in self.constraints:
             try:
@@ -137,25 +139,25 @@
         pass
 
 
-class CheckConstraintHook(Hook):
+class CheckConstraintHook(IntegrityHook):
     """check the relation satisfy its constraints
 
     this is delayed to a precommit time operation since other relation which
     will make constraint satisfied may be added later.
     """
     __id__ = 'checkconstraint'
-    category = 'integrity'
     events = ('after_add_relation',)
+
     def __call__(self):
-        constraints = rproperty(self.cw_req, self.rtype, self.eidfrom, self.eidto,
+        constraints = self.cw_req.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
                                 'constraints')
         if constraints:
             _CheckConstraintsOp(self.cw_req, constraints=constraints,
                                rdef=(self.eidfrom, self.rtype, self.eidto))
 
-class CheckUniqueHook(Hook):
+
+class CheckUniqueHook(IntegrityHook):
     __id__ = 'checkunique'
-    category = 'integrity'
     events = ('before_add_entity', 'before_update_entity')
 
     def __call__(self):
@@ -174,7 +176,7 @@
                     raise ValidationError(entity.eid, {attr: msg % val})
 
 
-class _DelayedDeleteOp(PreCommitOperation):
+class _DelayedDeleteOp(hook.Operation):
     """delete the object of composite relation except if the relation
     has actually been redirected to another composite
     """
@@ -182,23 +184,23 @@
     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', ())):
+        if not (session.deleted_in_transaction(self.eid) or
+                session.added_in_transaction(self.eid)):
             etype = session.describe(self.eid)[0]
             session.unsafe_execute('DELETE %s X WHERE X eid %%(x)s, NOT %s'
                                    % (etype, self.relation),
                                    {'x': self.eid}, 'x')
 
 
-class DeleteCompositeOrphanHook(Hook):
+class DeleteCompositeOrphanHook(IntegrityHook):
     """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')
+        composite = self.cw_req.schema_rproperty(self.rtype, self.eidfrom, self.eidto,
+                                                 'composite')
         if composite == 'subject':
             _DelayedDeleteOp(self.cw_req, eid=self.eidto,
                              relation='Y %s X' % self.rtype)
@@ -207,12 +209,11 @@
                              relation='X %s Y' % self.rtype)
 
 
-class DontRemoveOwnersGroupHook(Hook):
+class DontRemoveOwnersGroupHook(IntegrityHook):
     """delete the composed of a composite relation when this relation is deleted
     """
     __id__ = 'checkownersgroup'
-    __select__ = Hook.__select__ & entity_implements('CWGroup')
-    category = 'integrity'
+    __select__ = IntegrityHook.__select__ & entity_implements('CWGroup')
     events = ('before_delete_entity', 'before_update_entity')
 
     def __call__(self):
@@ -226,3 +227,31 @@
             self.entity['name'] = newname
 
 
+class TidyHtmlFields(IntegrityHook):
+    """tidy HTML in rich text strings"""
+    __id__ = 'htmltidy'
+    events = ('before_add_entity', 'before_update_entity')
+
+    def __call__(self):
+        entity = self.entity
+        metaattrs = entity.e_schema.meta_attributes()
+        for metaattr, (metadata, attr) in metaattrs.iteritems():
+            if metadata == 'format' and attr in entity.edited_attributes:
+                try:
+                    value = entity[attr]
+                except KeyError:
+                    continue # no text to tidy
+                if isinstance(value, unicode): # filter out None and Binary
+                    if getattr(entity, str(metaattr)) == 'text/html':
+                        entity[attr] = soup2xhtml(value, self.cw_req.encoding)
+
+
+class StripCWUserLoginHook(IntegrityHook):
+    """ensure user logins are stripped"""
+    __id__ = 'stripuserlogin'
+    __select__ = IntegrityHook.__select__ & entity_implements('CWUser')
+    events = ('before_add_entity', 'before_update_entity',)
+
+    def call(self, session, entity):
+        if 'login' in entity.edited_attributes and entity['login']:
+            entity['login'] = entity['login'].strip()
--- a/hooks/metadata.py	Fri Aug 14 11:13:18 2009 +0200
+++ b/hooks/metadata.py	Fri Aug 14 11:14:10 2009 +0200
@@ -11,9 +11,7 @@
 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 import hook
 from cubicweb.server.repository import FTIndexEntityOp
 
 
@@ -28,14 +26,18 @@
     return eschema.eid
 
 
-class InitMetaAttrsHook(Hook):
+class MetaDataHook(hook.Hook):
+    __abstract__ = True
+    category = 'metadata'
+
+
+class InitMetaAttrsHook(MetaDataHook):
     """before create a new entity -> set creation and modification date
 
     this is a conveniency hook, you shouldn't have to disable it
     """
-    id = 'metaattrsinit'
+    __id__ = 'metaattrsinit'
     events = ('before_add_entity',)
-    category = 'metadata'
 
     def __call__(self):
         timestamp = datetime.now()
@@ -46,31 +48,31 @@
             self.entity.setdefault('cwuri', cwuri)
 
 
-class UpdateMetaAttrsHook(Hook):
+class UpdateMetaAttrsHook(MetaDataHook):
     """update an entity -> set modification date"""
-    id = 'metaattrsupdate'
+    __id__ = 'metaattrsupdate'
     events = ('before_update_entity',)
-    category = 'metadata'
+
     def __call__(self):
         self.entity.setdefault('modification_date', datetime.now())
 
 
-class _SetCreatorOp(PreCommitOperation):
+class _SetCreatorOp(hook.Operation):
 
     def precommit_event(self):
         session = self.session
-        if self.entity.eid in session.transaction_data.get('pendingeids', ()):
+        if session.deleted_in_transaction(self.entity.eid):
             # entity have been created and deleted in the same transaction
             return
         if not self.entity.created_by:
             session.add_relation(self.entity.eid, 'created_by', session.user.eid)
 
 
-class SetIsHook(Hook):
+class SetIsHook(MetaDataHook):
     """create a new entity -> set is relation"""
-    id = 'setis'
+    __id__ = 'setis'
     events = ('after_add_entity',)
-    category = 'metadata'
+
     def __call__(self):
         if hasattr(self.entity, '_cw_recreating'):
             return
@@ -87,11 +89,11 @@
                                  eschema_type_eid(session, etype))
 
 
-class SetOwnershipHook(Hook):
+class SetOwnershipHook(MetaDataHook):
     """create a new entity -> set owner and creator metadata"""
-    id = 'setowner'
+    __id__ = 'setowner'
     events = ('after_add_entity',)
-    category = 'metadata'
+
     def __call__(self):
         asession = self.cw_req.actual_session()
         if not asession.is_internal_session:
@@ -99,48 +101,48 @@
             _SetCreatorOp(asession, entity=self.entity)
 
 
-class _SyncOwnersOp(PreCommitOperation):
+class _SyncOwnersOp(hook.Operation):
     def precommit_event(self):
         self.session.unsafe_execute('SET X owned_by U WHERE C owned_by U, C eid %(c)s,'
                                     'NOT EXISTS(X owned_by U, X eid %(x)s)',
                                     {'c': self.compositeeid, 'x': self.composedeid},
                                     ('c', 'x'))
 
-class SyncCompositeOwner(Hook):
+
+class SyncCompositeOwner(MetaDataHook):
     """when adding composite relation, the composed should have the same owners
     has the composite
     """
-    id = 'synccompositeowner'
+    __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')
+        composite = self.cw_req.schema_rproperty(self.rtype, eidfrom, eidto, 'composite')
         if composite == 'subject':
             _SyncOwnersOp(self.cw_req, compositeeid=eidfrom, composedeid=eidto)
         elif composite == 'object':
             _SyncOwnersOp(self.cw_req, compositeeid=eidto, composedeid=eidfrom)
 
 
-class FixUserOwnershipHook(Hook):
+class FixUserOwnershipHook(MetaDataHook):
     """when a user has been created, add owned_by relation on itself"""
-    id = 'fixuserowner'
-    __select__ = Hook.__select__ & entity_implements('CWUser')
+    __id__ = 'fixuserowner'
+    __select__ = MetaDataHook.__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):
+class UpdateFTIHook(MetaDataHook):
     """sync fulltext index when relevant relation is added / removed
     """
-    id = 'updateftirel'
+    __id__ = 'updateftirel'
     events = ('after_add_relation', 'after_delete_relation')
-    category = 'metadata'
 
     def __call__(self):
         rtype = self.rtype
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/notification.py	Fri Aug 14 11:14:10 2009 +0200
@@ -0,0 +1,133 @@
+"""some hooks to handle notification on entity's changes
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab.common.textutils import normalize_text
+
+from cubicweb import RegistryException
+from cubicweb.selectors import entity_implements
+from cubicweb.server import hook
+
+
+class RenderAndSendNotificationView(hook.Operation):
+    """delay rendering of notification view until precommit"""
+    def precommit_event(self):
+        view = self.view
+        if view.cw_rset and self.session.deleted_in_transaction(view.cw_rset[cw_rset.cw_row or 0][cw_rset.cw_col or 0]):
+            return # entity added and deleted in the same transaction
+        self.view.render_and_send(**getattr(self, 'viewargs', {}))
+
+
+class NotificationHook(hook.Hook):
+    __abstract__ = True
+    category = 'notification'
+
+    def select_view(self, vid, rset, row=0, col=0):
+        return self.cw_req.vreg['views'].select_object(vid, self.cw_req,
+                                                       rset=rset, row=0, col=0)
+
+
+class StatusChangeHook(NotificationHook):
+    """notify when a workflowable entity has its state modified"""
+    __id__ = 'notifystatuschange'
+    __select__ = NotificationHook.__select__ & entity_implements('TrInfo')
+    events = ('after_add_entity',)
+
+    def __call__(self):
+        entity = self.entity
+        if not entity.from_state: # not a transition
+            return
+        rset = entity.related('wf_info_for')
+        view = self.select_view('notif_status_change', rset=rset, row=0)
+        if view is None:
+            return
+        comment = entity.printable_value('comment', format='text/plain')
+        if comment:
+            comment = normalize_text(comment, 80,
+                                     rest=entity.comment_format=='text/rest')
+        RenderAndSendNotificationView(self.cw_req, view=view, viewargs={
+            'comment': comment, 'previous_state': entity.previous_state.name,
+            'current_state': entity.new_state.name})
+
+
+class RelationChangeHook(NotificationHook):
+    __id__ = 'notifyrelationchange'
+    events = ('before_add_relation', 'after_add_relation',
+              'before_delete_relation', 'after_delete_relation')
+
+    def __call__(self):
+        """if a notification view is defined for the event, send notification
+        email defined by the view
+        """
+        rset = self.cw_req.eid_rset(self.eidfrom)
+        view = self.select_view('notif_%s_%s' % (self.event,  self.rtype),
+                                rset=rset, row=0)
+        if view is None:
+            return
+        RenderAndSendNotificationView(self.cw_req, view=view)
+
+
+class EntityChangeHook(NotificationHook):
+    """if a notification view is defined for the event, send notification
+    email defined by the view
+    """
+    __id__ = 'notifyentitychange'
+    events = ('after_add_entity', 'after_update_entity')
+
+    def __call__(self):
+        rset = self.entity.as_rset()
+        view = self.select_view('notif_%s' % self.event, rset=rset, row=0)
+        if view is None:
+            return
+        RenderAndSendNotificationView(self.cw_req, view=view)
+
+
+# supervising ##################################################################
+
+class SomethingChangedHook(NotificationHook):
+    __id__ = 'supervising'
+    events = ('before_add_relation', 'before_delete_relation',
+              'after_add_entity', 'before_update_entity')
+
+    def __call__(self):
+        dest = self.cw_req.vreg.config['supervising-addrs']
+        if not dest: # no supervisors, don't do this for nothing...
+            return
+        if self._call():
+            SupervisionMailOp(self.cw_req)
+
+    def _call(self):
+        event = self.event.split('_', 1)[1]
+        if event == 'update_entity':
+            if self.cw_req.added_in_transaction(self.entity.eid):
+                return False
+            if self.entity.e_schema == 'CWUser':
+                if not (self.entity.edited_attributes - frozenset(('eid', 'modification_date',
+                                                                   'last_login_time'))):
+                    # don't record last_login_time update which are done
+                    # automatically at login time
+                    return False
+        self.cw_req.transaction_data.setdefault('pendingchanges', []).append(
+            (event, self))
+        return True
+
+
+class EntityDeleteHook(SomethingChangedHook):
+    __id__ = 'supervisingentitydel'
+    events = ('before_delete_entity',)
+
+    def _call(self):
+        try:
+            title = self.entity.dc_title()
+        except:
+            # may raise an error during deletion process, for instance due to
+            # missing required relation
+            title = '#%s' % eid
+        self.cw_req.transaction_data.setdefault('pendingchanges', []).append(
+            ('delete_entity', (self.eid, str(self.entity.e_schema), title)))
+        return True
--- a/hooks/syncschema.py	Fri Aug 14 11:13:18 2009 +0200
+++ b/hooks/syncschema.py	Fri Aug 14 11:14:10 2009 +0200
@@ -453,7 +453,7 @@
         # 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', ()):
+        if session.added_in_transaction(rdef.eid):
             return
         subjtype, rtype, objtype = session.schema.schema_by_eid(rdef.eid)
         cstrtype = self.entity.type
@@ -602,7 +602,7 @@
         # 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', ()):
+        if session.added_in_transaction(rdef.eid):
             self.cancelled = True
             return
         subjtype, rtype, objtype = self.session.schema.schema_by_eid(rdef.eid)
@@ -729,17 +729,20 @@
         erschema.set_rqlexprs(self.perm, rqlexprs)
 
 
-# deletion hooks ###############################################################
+class SyncSchemaHook(hook.Hook):
+    __abstract__ = True
+    category = 'syncschema'
 
-class DelCWETypeHook(hook.Hook):
+# CWEType hooks ################################################################
+
+class DelCWETypeHook(SyncSchemaHook):
     """before deleting a CWEType entity:
     * check that we don't remove a core entity type
     * cascade to delete related CWAttribute and CWRelation entities
     * instantiate an operation to delete the entity type on commit
     """
     __id__ = 'syncdelcwetype'
-    __select__ = hook.Hook.__select__ & entity_implements('CWEType')
-    category = 'syncschema'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWEType')
     events = ('before_delete_entity',)
 
     def __call__(self):
@@ -831,16 +834,19 @@
                 MemSchemaCWETypeRename(self.cw_req, oldname=oldname, newname=newname)
             entity['name'] = newname
 
-class DelCWRTypeHook(hook.Hook):
+
+# CWRType hooks ################################################################
+
+class DelCWRTypeHook(SyncSchemaHook):
     """before deleting a CWRType entity:
     * check that we don't remove a core relation type
     * cascade to delete related CWAttribute and CWRelation entities
     * instantiate an operation to delete the relation type on commit
     """
     __id__ = 'syncdelcwrtype'
-    __select__ = hook.Hook.__select__ & entity_implements('CWRType')
-    category = 'syncschema'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWRType')
     events = ('before_delete_entity',)
+
     def __call__(self):
         name = self.entity.name
         if name in CORE_ETYPES:
@@ -900,8 +906,9 @@
                                   entity=entity)
 
 
+# relation_type hooks ##########################################################
 
-class AfterDelRelationTypeHook(hook.Hook):
+class AfterDelRelationTypeHook(SyncSchemaHook):
     """before deleting a CWAttribute or CWRelation entity:
     * if this is a final or inlined relation definition, instantiate an
       operation to drop necessary column, else if this is the last instance
@@ -911,8 +918,7 @@
     * delete the associated relation type when necessary
     """
     __id__ = 'syncdelrelationtype'
-    __select__ = hook.Hook.__select__ & hook.match_rtype('relation_type')
-    category = 'syncschema'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('relation_type')
     events = ('after_delete_relation',)
 
     def __call__(self):
@@ -954,10 +960,11 @@
         MemSchemaRDefDel(session, (subjschema, rschema, objschema))
 
 
-class AfterAddCWAttributeHook(hook.Hook):
+# CWAttribute / CWRelation hooks ###############################################
+
+class AfterAddCWAttributeHook(SyncSchemaHook):
     __id__ = 'syncaddcwattribute'
-    __select__ = hook.Hook.__select__ & entity_implements('CWAttribute')
-    category = 'syncschema'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWAttribute')
     events = ('after_add_entity',)
 
     def __call__(self):
@@ -966,21 +973,21 @@
 
 class AfterAddCWRelationHook(AfterAddCWAttributeHook):
     __id__ = 'syncaddcwrelation'
-    __select__ = hook.Hook.__select__ & entity_implements('CWRelation')
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWRelation')
 
     def __call__(self):
         SourceDbCWRelationAdd(self.cw_req, entity=self.entity)
 
 
-class AfterUpdateCWRDefHook(hook.Hook):
+class AfterUpdateCWRDefHook(SyncSchemaHook):
     __id__ = 'syncaddcwattribute'
-    __select__ = hook.Hook.__select__ & entity_implements('CWAttribute', 'CWRelation')
-    category = 'syncschema'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWAttribute',
+                                                               'CWRelation')
     events = ('after_update_entity',)
 
     def __call__(self):
         entity = self.entity
-        if entity.eid in self.cw_req.transaction_data.get('pendingeids', ()):
+        if self.cw_req.deleted_in_transaction(entity.eid):
             return
         desttype = entity.otype.name
         rschema = self.cw_req.schema[entity.rtype.name]
@@ -1002,10 +1009,9 @@
 
 # constraints synchronization hooks ############################################
 
-class AfterAddCWConstraintHook(hook.Hook):
+class AfterAddCWConstraintHook(SyncSchemaHook):
     __id__ = 'syncaddcwconstraint'
-    __select__ = hook.Hook.__select__ & entity_implements('CWConstraint')
-    category = 'syncschema'
+    __select__ = SyncSchemaHook.__select__ & entity_implements('CWConstraint')
     events = ('after_add_entity', 'after_update_entity')
 
     def __call__(self):
@@ -1013,14 +1019,13 @@
         SourceDbCWConstraintAdd(self.cw_req, entity=self.entity)
 
 
-class AfterAddConstrainedByHook(hook.Hook):
+class AfterAddConstrainedByHook(SyncSchemaHook):
     __id__ = 'syncdelconstrainedby'
-    __select__ = hook.Hook.__select__ & hook.match_rtype('constrainted_by')
-    category = 'syncschema'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('constrainted_by')
     events = ('after_add_relation',)
 
     def __call__(self):
-        if self.eidfrom in self.cw_req.transaction_data.get('neweids', ()):
+        if self.cw_req.added_in_transaction(self.eidfrom):
             self.cw_req.transaction_data.setdefault(self.eidfrom, []).append(self.eidto)
 
 
@@ -1029,7 +1034,7 @@
     events = ('before_delete_relation',)
 
     def __call__(self):
-        if self.eidfrom in self.cw_req.transaction_data.get('pendingeids', ()):
+        if self.cw_req.deleted_in_transaction(self.eidfrom):
             return
         schema = self.cw_req.schema
         entity = self.cw_req.entity_from_eid(self.eidto)
@@ -1049,13 +1054,12 @@
 # permissions synchronization hooks ############################################
 
 
-class AfterAddPermissionHook(hook.Hook):
+class AfterAddPermissionHook(SyncSchemaHook):
     """added entity/relation *_permission, need to update schema"""
     __id__ = 'syncaddperm'
-    __select__ = hook.Hook.__select__ & hook.match_rtype(
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype(
         'read_permission', 'add_permission', 'delete_permission',
         'update_permission')
-    category = 'syncschema'
     events = ('after_add_relation',)
 
     def __call__(self):
@@ -1076,7 +1080,7 @@
     events = ('before_delete_relation',)
 
     def __call__(self):
-        if self.eidfrom in self.cw_req.transaction_data.get('pendingeids', ()):
+        if self.cw_req.deleted_in_transaction(self.eidfrom):
             return
         perm = self.rtype.split('_', 1)[0]
         if self.cw_req.describe(self.eidto)[0] == 'CWGroup':
@@ -1087,10 +1091,9 @@
 
 
 
-class ModifySpecializesHook(hook.Hook):
+class ModifySpecializesHook(SyncSchemaHook):
     __id__ = 'syncspecializes'
-    __select__ = hook.Hook.__select__ & hook.match_rtype('specializes')
-    category = 'syncschema'
+    __select__ = SyncSchemaHook.__select__ & hook.match_rtype('specializes')
     events = ('after_add_relation', 'after_delete_relation')
 
     def __call__(self):
--- a/hooks/syncsession.py	Fri Aug 14 11:13:18 2009 +0200
+++ b/hooks/syncsession.py	Fri Aug 14 11:14:10 2009 +0200
@@ -9,14 +9,23 @@
 
 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
+from cubicweb.server import hook
+
+
+def get_user_sessions(repo, ueid):
+    for session in repo._sessions.values():
+        if ueid == session.user.eid:
+            yield session
+
+
+class SyncSessionHook(hook.Hook):
+    __abstract__ = True
+    category = 'syncsession'
 
 
 # user/groups synchronisation #################################################
 
-class _GroupOperation(Operation):
+class _GroupOperation(hook.Operation):
     """base class for group operation"""
     geid = None
     def __init__(self, session, *args, **kwargs):
@@ -27,7 +36,7 @@
         """
         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)
+        hook.Operation.__init__(self, session, *args, **kwargs)
         self.group = result[0][0]
 
 
@@ -55,11 +64,10 @@
         groups.add(self.group)
 
 
-class SyncInGroupHook(Hook):
+class SyncInGroupHook(SyncSessionHook):
     __id__ = 'syncingroup'
-    __select__ = Hook.__select__ & match_rtype('in_group')
+    __select__ = SyncSessionHook.__select__ & hook.match_rtype('in_group')
     events = ('after_delete_relation', 'after_add_relation')
-    category = 'syncsession'
 
     def __call__(self):
         if self.event == 'after_delete_relation':
@@ -70,11 +78,11 @@
             opcls(self.cw_req, cnxuser=session.user, geid=self.eidto)
 
 
-class _DelUserOp(Operation):
+class _DelUserOp(hook.Operation):
     """close associated user's session when it is deleted"""
     def __init__(self, session, cnxid):
         self.cnxid = cnxid
-        Operation.__init__(self, session)
+        hook.Operation.__init__(self, session)
 
     def commit_event(self):
         """the observed connections pool has been commited"""
@@ -84,11 +92,10 @@
             pass # already closed
 
 
-class CloseDeletedUserSessionsHook(Hook):
+class CloseDeletedUserSessionsHook(SyncSessionHook):
     __id__ = 'closession'
-    __select__ = Hook.__select__ & entity_implements('CWUser')
+    __select__ = SyncSessionHook.__select__ & entity_implements('CWUser')
     events = ('after_delete_entity',)
-    category = 'syncsession'
 
     def __call__(self):
         """modify user permission, need to update users"""
@@ -99,7 +106,7 @@
 # CWProperty hooks #############################################################
 
 
-class _DelCWPropertyOp(Operation):
+class _DelCWPropertyOp(hook.Operation):
     """a user's custom properties has been deleted"""
 
     def commit_event(self):
@@ -110,7 +117,7 @@
             self.error('%s has no associated value', self.key)
 
 
-class _ChangeCWPropertyOp(Operation):
+class _ChangeCWPropertyOp(hook.Operation):
     """a user's custom properties has been added/changed"""
 
     def commit_event(self):
@@ -118,7 +125,7 @@
         self.epropdict[self.key] = self.value
 
 
-class _AddCWPropertyOp(Operation):
+class _AddCWPropertyOp(hook.Operation):
     """a user's custom properties has been added/changed"""
 
     def commit_event(self):
@@ -129,10 +136,9 @@
         # if for_user is set, update is handled by a ChangeCWPropertyOp operation
 
 
-class AddCWPropertyHook(Hook):
+class AddCWPropertyHook(SyncSessionHook):
     __id__ = 'addcwprop'
-    __select__ = Hook.__select__ & entity_implements('CWProperty')
-    category = 'syncsession'
+    __select__ = SyncSessionHook.__select__ & entity_implements('CWProperty')
     events = ('after_add_entity',)
 
     def __call__(self):
@@ -194,11 +200,10 @@
             _DelCWPropertyOp(session, epropdict=session.vreg.eprop_values, key=entity.pkey)
 
 
-class AddForUserRelationHook(Hook):
+class AddForUserRelationHook(SyncSessionHook):
     __id__ = 'addcwpropforuser'
-    __select__ = Hook.__select__ & match_rtype('for_user')
+    __select__ = SyncSessionHook.__select__ & hook.match_rtype('for_user')
     events = ('after_add_relation',)
-    category = 'syncsession'
 
     def __call__(self):
         session = self.cw_req
--- a/hooks/workflow.py	Fri Aug 14 11:13:18 2009 +0200
+++ b/hooks/workflow.py	Fri Aug 14 11:14:10 2009 +0200
@@ -7,12 +7,32 @@
 """
 __docformat__ = "restructuredtext en"
 
-from cubicweb import ValidationError
+from datetime import datetime
+
+from cubicweb import RepositoryError, 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
+from cubicweb.server import hook
+
+
+def previous_state(session, eid):
+    """return the state of the entity with the given eid,
+    usually since it's changing in the current transaction. Due to internal
+    relation hooks, the relation may has been deleted at this point, so
+    we have handle that
+    """
+    if session.added_in_transaction(eid):
+        return
+    pending = session.transaction_data.get('pendingrelations', ())
+    for eidfrom, rtype, eidto in reversed(pending):
+        if rtype == 'in_state' and eidfrom == eid:
+            rset = session.execute('Any S,N WHERE S eid %(x)s, S name N',
+                                   {'x': eidto}, 'x')
+            return rset.get_entity(0, 0)
+    rset = session.execute('Any S,N WHERE X eid %(x)s, X in_state S, S name N',
+                           {'x': eid}, 'x')
+    if rset:
+        return rset.get_entity(0, 0)
 
 
 def relation_deleted(session, eidfrom, rtype, eidto):
@@ -20,7 +40,7 @@
         (eidfrom, rtype, eidto))
 
 
-class _SetInitialStateOp(PreCommitOperation):
+class _SetInitialStateOp(hook.Operation):
     """make initial state be a default state"""
 
     def precommit_event(self):
@@ -28,29 +48,30 @@
         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:
+        if not session.deleted_in_transaction(entity.eid) and not entity.in_state:
             rset = session.execute('Any S WHERE ET initial_state S, ET name %(name)s',
                                    {'name': entity.id})
             if rset:
                 session.add_relation(entity.eid, 'in_state', rset[0][0])
 
+class WorkflowHook(hook.Hook):
+    __abstract__ = True
+    category = 'worfklow'
 
-class SetInitialStateHook(Hook):
+
+class SetInitialStateHook(WorkflowHook):
     __id__ = 'wfsetinitial'
-    __select__ = Hook.__select__ & entity_implements(IWorkflowable)
-    category = 'worfklow'
+    __select__ = WorkflowHook.__select__ & entity_implements(IWorkflowable)
     events = ('after_add_entity',)
 
     def __call__(self):
         _SetInitialStateOp(self.cw_req, entity=self.entity)
 
 
-class PrepareStateChangeHook(Hook):
+class PrepareStateChangeHook(WorkflowHook):
     """record previous state information"""
     __id__ = 'cwdelstate'
-    __select__ = Hook.__select__ & match_rtype('in_state')
-    category = 'worfklow'
+    __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
     events = ('before_delete_relation',)
 
     def __call__(self):
@@ -104,3 +125,22 @@
             args['fs'] = state.eid
         rql = '%s WHERE %s' % (rql, ', '.join(restriction))
         session.unsafe_execute(rql, args, 'e')
+
+
+class SetModificationDateOnStateChange(WorkflowHook):
+    """update entity's modification date after changing its state"""
+    __id__ = 'wfsyncmdate'
+    __select__ = WorkflowHook.__select__ & hook.match_rtype('in_state')
+    events = ('after_add_relation',)
+
+    def __call__(self):
+        if self.cw_req.added_in_transaction(self.eidfrom):
+            # new entity, not needed
+            return
+        entity = self.cw_req.entity_from_eid(self.eidfrom)
+        try:
+            entity.set_attributes(modification_date=datetime.now())
+        except RepositoryError, ex:
+            # usually occurs if entity is coming from a read-only source
+            # (eg ldap user)
+            self.warning('cant change modification date for %s: %s', entity, ex)
--- a/sobjects/email.py	Fri Aug 14 11:13:18 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-"""hooks to ensure use_email / primary_email relations consistency
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-__docformat__ = "restructuredtext en"
-
-from cubicweb.server.hooksmanager import Hook
-from cubicweb.server.pool import PreCommitOperation
-
-class SetUseEmailRelationOp(PreCommitOperation):
-    """delay this operation to commit to avoid conflict with a late rql query
-    already setting the relation
-    """
-    rtype = 'use_email'
-    fromeid = toeid = None # make pylint happy
-
-    def condition(self):
-        """check entity has use_email set for the email address"""
-        return not self.session.unsafe_execute(
-            'Any X WHERE X eid %(x)s, X use_email Y, Y eid %(y)s',
-            {'x': self.fromeid, 'y': self.toeid}, 'x')
-
-    def precommit_event(self):
-        session = self.session
-        if self.condition():
-            session.unsafe_execute(
-                'SET X %s Y WHERE X eid %%(x)s, Y eid %%(y)s' % self.rtype,
-                {'x': self.fromeid, 'y': self.toeid}, 'x')
-
-class SetPrimaryEmailRelationOp(SetUseEmailRelationOp):
-    rtype = 'primary_email'
-
-    def condition(self):
-        """check entity has no primary_email set"""
-        return not self.session.unsafe_execute(
-            'Any X WHERE X eid %(x)s, X primary_email Y',
-            {'x': self.fromeid}, 'x')
-
-
-class SetPrimaryEmailHook(Hook):
-    """notify when a bug or story or version has its state modified"""
-    events = ('after_add_relation',)
-    accepts = ('use_email',)
-
-    def call(self, session, fromeid, rtype, toeid):
-        subjtype = session.describe(fromeid)[0]
-        eschema = self.vreg.schema[subjtype]
-        if 'primary_email' in eschema.subject_relations():
-            SetPrimaryEmailRelationOp(session, vreg=self.vreg,
-                                      fromeid=fromeid, toeid=toeid)
-
-class SetUseEmailHook(Hook):
-    """notify when a bug or story or version has its state modified"""
-    events = ('after_add_relation',)
-    accepts = ('primary_email',)
-
-    def call(self, session, fromeid, rtype, toeid):
-        subjtype = session.describe(fromeid)[0]
-        eschema = self.vreg.schema[subjtype]
-        if 'use_email' in eschema.subject_relations():
-            SetUseEmailRelationOp(session, vreg=self.vreg,
-                                  fromeid=fromeid, toeid=toeid)
--- a/sobjects/hooks.py	Fri Aug 14 11:13:18 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-"""various library content hooks
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-__docformat__ = "restructuredtext en"
-
-from datetime import datetime
-
-from cubicweb import RepositoryError
-from cubicweb.common.uilib import soup2xhtml
-from cubicweb.server.hooksmanager import Hook
-from cubicweb.server.pool import PreCommitOperation
-
-
-class SetModificationDateOnStateChange(Hook):
-    """update entity's modification date after changing its state"""
-    events = ('after_add_relation',)
-    accepts = ('in_state',)
-
-    def call(self, session, fromeid, rtype, toeid):
-        if fromeid in session.transaction_data.get('neweids', ()):
-            # new entity, not needed
-            return
-        entity = session.entity_from_eid(fromeid)
-        try:
-            entity.set_attributes(modification_date=datetime.now())
-        except RepositoryError, ex:
-            # usually occurs if entity is coming from a read-only source
-            # (eg ldap user)
-            self.warning('cant change modification date for %s: %s', entity, ex)
-
-
-class AddUpdateCWUserHook(Hook):
-    """ensure user logins are stripped"""
-    events = ('before_add_entity', 'before_update_entity',)
-    accepts = ('CWUser',)
-
-    def call(self, session, entity):
-        if 'login' in entity and entity['login']:
-            entity['login'] = entity['login'].strip()
-
-
-class AutoDeleteBookmark(PreCommitOperation):
-    beid = None # make pylint happy
-    def precommit_event(self):
-        session = self.session
-        if not self.beid in session.transaction_data.get('pendingeids', ()):
-            if not session.unsafe_execute('Any X WHERE X bookmarked_by U, X eid %(x)s',
-                                          {'x': self.beid}, 'x'):
-                session.unsafe_execute('DELETE Bookmark X WHERE X eid %(x)s',
-                                       {'x': self.beid}, 'x')
-
-class DelBookmarkedByHook(Hook):
-    """ensure user logins are stripped"""
-    events = ('after_delete_relation',)
-    accepts = ('bookmarked_by',)
-
-    def call(self, session, subj, rtype, obj):
-        AutoDeleteBookmark(session, beid=subj)
-
-
-class TidyHtmlFields(Hook):
-    """tidy HTML in rich text strings"""
-    events = ('before_add_entity', 'before_update_entity')
-    accepts = ('Any',)
-
-    def call(self, session, entity):
-        metaattrs = entity.e_schema.meta_attributes()
-        for metaattr, (metadata, attr) in metaattrs.iteritems():
-            if metadata == 'format':
-                try:
-                    value = entity[attr]
-                except KeyError:
-                    continue # no text to tidy
-                if isinstance(value, unicode): # filter out None and Binary
-                    if self.event == 'before_add_entity':
-                        fmt = entity.get(metaattr)
-                    else:
-                        fmt = entity.get_value(metaattr)
-                    if fmt == 'text/html':
-                        entity[attr] = soup2xhtml(value, session.encoding)
--- a/sobjects/notification.py	Fri Aug 14 11:13:18 2009 +0200
+++ b/sobjects/notification.py	Fri Aug 14 11:14:10 2009 +0200
@@ -1,4 +1,4 @@
-"""some hooks and views to handle notification on entity's changes
+"""some views to handle notification on data changes
 
 :organization: Logilab
 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
@@ -18,16 +18,12 @@
         return 'XXX'
 
 from logilab.common.textutils import normalize_text
-from logilab.common.deprecation import class_renamed
 
-from cubicweb import RegistryException
-from cubicweb.selectors import implements, yes
+from cubicweb.selectors import yes
 from cubicweb.view import EntityView, Component
 from cubicweb.common.mail import format_mail
 
-from cubicweb.server.pool import PreCommitOperation
 from cubicweb.server.hookhelper import SendMailOp
-from cubicweb.server.hooksmanager import Hook
 
 
 class RecipientsFinder(Component):
@@ -57,72 +53,6 @@
         return dests
 
 
-# hooks #######################################################################
-
-class RenderAndSendNotificationView(PreCommitOperation):
-    """delay rendering of notification view until precommit"""
-    def precommit_event(self):
-        if self.view.rset and self.view.rset[0][0] in self.session.transaction_data.get('pendingeids', ()):
-            return # entity added and deleted in the same transaction
-        self.view.render_and_send(**getattr(self, 'viewargs', {}))
-
-class StatusChangeHook(Hook):
-    """notify when a workflowable entity has its state modified"""
-    events = ('after_add_entity',)
-    accepts = ('TrInfo',)
-
-    def call(self, session, entity):
-        if not entity.from_state: # not a transition
-            return
-        rset = entity.related('wf_info_for')
-        try:
-            view = session.vreg['views'].select('notif_status_change', session,
-                                                rset=rset, row=0)
-        except RegistryException:
-            return
-        comment = entity.printable_value('comment', format='text/plain')
-        if comment:
-            comment = normalize_text(comment, 80,
-                                     rest=entity.comment_format=='text/rest')
-        RenderAndSendNotificationView(session, view=view, viewargs={
-            'comment': comment, 'previous_state': entity.previous_state.name,
-            'current_state': entity.new_state.name})
-
-
-class RelationChangeHook(Hook):
-    events = ('before_add_relation', 'after_add_relation',
-              'before_delete_relation', 'after_delete_relation')
-    accepts = ('Any',)
-    def call(self, session, fromeid, rtype, toeid):
-        """if a notification view is defined for the event, send notification
-        email defined by the view
-        """
-        rset = session.eid_rset(fromeid)
-        vid = 'notif_%s_%s' % (self.event,  rtype)
-        try:
-            view = session.vreg['views'].select(vid, session, rset=rset, row=0)
-        except RegistryException:
-            return
-        RenderAndSendNotificationView(session, view=view)
-
-
-class EntityChangeHook(Hook):
-    events = ('after_add_entity',
-              'after_update_entity')
-    accepts = ('Any',)
-    def call(self, session, entity):
-        """if a notification view is defined for the event, send notification
-        email defined by the view
-        """
-        rset = entity.as_rset()
-        vid = 'notif_%s' % self.event
-        try:
-            view = session.vreg['views'].select(vid, session, rset=rset, row=0)
-        except RegistryException:
-            return
-        RenderAndSendNotificationView(session, view=view)
-
-
 # abstract or deactivated notification views and mixin ########################
 
 class NotificationView(EntityView):
@@ -296,4 +226,7 @@
         return  u'%s #%s (%s)' % (self.req.__('New %s' % entity.e_schema),
                                   entity.eid, self.user_login())
 
+from logilab.common.deprecation import class_renamed, class_moved
 NormalizedTextView = class_renamed('NormalizedTextView', ContentAddedView)
+from cubicweb.hooks.notification import RenderAndSendNotificationView
+RenderAndSendNotificationView = class_moved(RenderAndSendNotificationView)
--- a/sobjects/supervising.py	Fri Aug 14 11:13:18 2009 +0200
+++ b/sobjects/supervising.py	Fri Aug 14 11:14:10 2009 +0200
@@ -13,58 +13,9 @@
 from cubicweb.schema import display_name
 from cubicweb.view import Component
 from cubicweb.common.mail import format_mail
-from cubicweb.server.hooksmanager import Hook
 from cubicweb.server.hookhelper import SendMailOp
 
 
-class SomethingChangedHook(Hook):
-    events = ('before_add_relation', 'before_delete_relation',
-              'after_add_entity', 'before_update_entity')
-    accepts = ('Any',)
-
-    def call(self, session, *args):
-        dest = session.vreg.config['supervising-addrs']
-        if not dest: # no supervisors, don't do this for nothing...
-            return
-        self.session = session
-        if self._call(*args):
-            SupervisionMailOp(session)
-
-    def _call(self, *args):
-        if self._event() == 'update_entity':
-            if args[0].eid in self.session.transaction_data.get('neweids', ()):
-                return False
-            if args[0].e_schema == 'CWUser':
-                updated = set(args[0].iterkeys())
-                if not (updated - frozenset(('eid', 'modification_date',
-                                             'last_login_time'))):
-                    # don't record last_login_time update which are done
-                    # automatically at login time
-                    return False
-        self.session.transaction_data.setdefault('pendingchanges', []).append(
-            (self._event(), args))
-        return True
-
-    def _event(self):
-        return self.event.split('_', 1)[1]
-
-
-class EntityDeleteHook(SomethingChangedHook):
-    events = ('before_delete_entity',)
-
-    def _call(self, eid):
-        entity = self.session.entity_from_eid(eid)
-        try:
-            title = entity.dc_title()
-        except:
-            # may raise an error during deletion process, for instance due to
-            # missing required relation
-            title = '#%s' % eid
-        self.session.transaction_data.setdefault('pendingchanges', []).append(
-            ('delete_entity', (eid, str(entity.e_schema), title)))
-        return True
-
-
 def filter_changes(changes):
     """
     * when an entity has been deleted:
@@ -79,7 +30,7 @@
     for change in changes[:]:
         event, changedescr = change
         if event == 'add_entity':
-            entity = changedescr[0]
+            entity = changedescr.entity
             added.add(entity.eid)
             if entity.e_schema == 'TrInfo':
                 changes.remove(change)
@@ -111,14 +62,14 @@
                 changedescr = change[1]
                 # skip meta-relations which are set automatically
                 # XXX generate list below using rtags (category = 'generated')
-                if changedescr[1] in ('created_by', 'owned_by', 'is', 'is_instance_of',
+                if changedescr.rtype in ('created_by', 'owned_by', 'is', 'is_instance_of',
                                       'from_state', 'to_state', 'wf_info_for',) \
-                       and changedescr[0] == eid:
+                       and changedescr.eidfrom == eid:
                     index['add_relation'].remove(change)
                 # skip in_state relation if the entity is being created
                 # XXX this may be automatized by skipping all mandatory relation
                 #     at entity creation time
-                elif changedescr[1] == 'in_state' and changedescr[0] in added:
+                elif changedescr.rtype == 'in_state' and changedescr.eidfrom in added:
                     index['add_relation'].remove(change)
 
         except KeyError:
@@ -126,15 +77,14 @@
     for eid in deleted:
         try:
             for change in index['delete_relation'].copy():
-                fromeid, rtype, toeid = change[1]
-                if fromeid == eid:
+                if change.eidfrom == eid:
                     index['delete_relation'].remove(change)
-                elif toeid == eid:
+                elif change.eidto == eid:
                     index['delete_relation'].remove(change)
-                    if rtype == 'wf_info_for':
-                        for change in index['delete_entity'].copy():
-                            if change[1][0] == fromeid:
-                                index['delete_entity'].remove(change)
+                    if change.rtype == 'wf_info_for':
+                        for change_ in index['delete_entity'].copy():
+                            if change_[1].eidfrom == change.eidfrom:
+                                index['delete_entity'].remove(change_)
         except KeyError:
             break
     for change in changes:
@@ -161,7 +111,7 @@
                % user.login)
         for event, changedescr in filter_changes(changes):
             self.w(u'* ')
-            getattr(self, event)(*changedescr)
+            getattr(self, event)(changedescr)
             self.w(u'\n\n')
 
     def _entity_context(self, entity):
@@ -169,30 +119,30 @@
                 'etype': entity.dc_type().lower(),
                 'title': entity.dc_title()}
 
-    def add_entity(self, entity):
+    def add_entity(self, changedescr):
         msg = self.req._('added %(etype)s #%(eid)s (%(title)s)')
-        self.w(u'%s\n' % (msg % self._entity_context(entity)))
-        self.w(u'  %s' % entity.absolute_url())
+        self.w(u'%s\n' % (msg % self._entity_context(changedescr.entity)))
+        self.w(u'  %s' % changedescr.entity.absolute_url())
 
-    def update_entity(self, entity):
+    def update_entity(self, changedescr):
         msg = self.req._('updated %(etype)s #%(eid)s (%(title)s)')
-        self.w(u'%s\n' % (msg % self._entity_context(entity)))
+        self.w(u'%s\n' % (msg % self._entity_context(changedescr.entity)))
         # XXX print changes
-        self.w(u'  %s' % entity.absolute_url())
+        self.w(u'  %s' % changedescr.entity.absolute_url())
 
-    def delete_entity(self, eid, etype, title):
+    def delete_entity(self, (eid, etype, title)):
         msg = self.req._('deleted %(etype)s #%(eid)s (%(title)s)')
         etype = display_name(self.req, etype).lower()
         self.w(msg % locals())
 
-    def change_state(self, entity, fromstate, tostate):
+    def change_state(self, (entity, fromstate, tostate)):
         msg = self.req._('changed state of %(etype)s #%(eid)s (%(title)s)')
         self.w(u'%s\n' % (msg % self._entity_context(entity)))
         self.w(_('  from state %(fromstate)s to state %(tostate)s\n' %
                  {'fromstate': _(fromstate.name), 'tostate': _(tostate.name)}))
         self.w(u'  %s' % entity.absolute_url())
 
-    def _relation_context(self, fromeid, rtype, toeid):
+    def _relation_context(self, changedescr):
         _ = self.req._
         session = self.req.actual_session()
         def describe(eid):
@@ -202,19 +152,20 @@
                 # may occurs when an entity has been deleted from an external
                 # source and we're cleaning its relation
                 return _('unknown external entity')
+        eidfrom, rtype, eidto = changedescr.eidfrom, changedescr.rtype, changedescr.eidto
         return {'rtype': _(rtype),
-                'fromeid': fromeid,
-                'frometype': describe(fromeid),
-                'toeid': toeid,
-                'toetype': describe(toeid)}
+                'eidfrom': eidfrom,
+                'frometype': describe(eidfrom),
+                'eidto': eidto,
+                'toetype': describe(eidto)}
 
-    def add_relation(self, fromeid, rtype, toeid):
-        msg = self.req._('added relation %(rtype)s from %(frometype)s #%(fromeid)s to %(toetype)s #%(toeid)s')
-        self.w(msg % self._relation_context(fromeid, rtype, toeid))
+    def add_relation(self, changedescr):
+        msg = self.req._('added relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%(eidto)s')
+        self.w(msg % self._relation_context(changedescr))
 
-    def delete_relation(self, fromeid, rtype, toeid):
-        msg = self.req._('deleted relation %(rtype)s from %(frometype)s #%(fromeid)s to %(toetype)s #%(toeid)s')
-        self.w(msg % self._relation_context(fromeid, rtype, toeid))
+    def delete_relation(self, eidfrom, rtype, eidto):
+        msg = self.req._('deleted relation %(rtype)s from %(frometype)s #%(eidfrom)s to %(toetype)s #%(eidto)s')
+        self.w(msg % self._relation_context(changedescr))
 
 
 class SupervisionMailOp(SendMailOp):