sobjects/supervising.py
changeset 0 b97547f5f1fa
child 655 ca3c4992c7d1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sobjects/supervising.py	Wed Nov 05 15:52:50 2008 +0100
@@ -0,0 +1,234 @@
+"""some hooks and views to handle supervising of any data changes
+
+
+:organization: Logilab
+:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb import UnknownEid
+from cubicweb.common.view import ComponentMixIn, StartupView
+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 = self.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' and args[0].e_schema == 'EUser':
+            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.add_query_data('pendingchanges', (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(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.add_query_data('pendingchanges',
+                                    ('delete_entity',
+                                     (eid, str(entity.e_schema),
+                                      title)))
+        return True
+
+
+def filter_changes(changes):
+    """
+    * when an entity has been deleted:
+      * don't show deletion of its relations
+      * don't show related TrInfo deletion if any
+    * when an entity has been added don't show owned_by relation addition
+    * don't show new TrInfo entities if any
+    """
+    # first build an index of changes
+    index = {}
+    added, deleted = set(), set()
+    for change in changes[:]:
+        event, changedescr = change
+        if event == 'add_entity':
+            entity = changedescr[0]
+            added.add(entity.eid)
+            if entity.e_schema == 'TrInfo':
+                changes.remove(change)
+                if entity.from_state:
+                    try:
+                        changes.remove( ('delete_relation', 
+                                         (entity.wf_info_for[0].eid, 'in_state', 
+                                          entity.from_state[0].eid)) )
+                    except ValueError:
+                        pass
+                    try:
+                        changes.remove( ('add_relation', 
+                                         (entity.wf_info_for[0].eid, 'in_state', 
+                                          entity.to_state[0].eid)) )
+                    except ValueError:
+                        pass
+                    event = 'change_state'
+                    change = (event, 
+                              (entity.wf_info_for[0],
+                               entity.from_state[0], entity.to_state[0]))
+                    changes.append(change)
+        elif event == 'delete_entity':
+            deleted.add(changedescr[0])
+        index.setdefault(event, set()).add(change)
+    # filter changes
+    for eid in added:
+        try:
+            for change in index['add_relation'].copy():
+                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',
+                                      'from_state', 'to_state', 'wf_info_for',) \
+                       and changedescr[0] == 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:
+                    index['add_relation'].remove(change)
+                    
+        except KeyError:
+            break
+    for eid in deleted:
+        try:
+            for change in index['delete_relation'].copy():
+                fromeid, rtype, toeid = change[1]
+                if fromeid == eid:
+                    index['delete_relation'].remove(change)
+                elif toeid == 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)
+        except KeyError:
+            break
+    for change in changes:
+        event, changedescr = change
+        if change in index[event]:
+            yield change
+
+
+class SupervisionEmailView(ComponentMixIn, StartupView):
+    """view implementing the email API for data changes supervision notification
+    """
+    id = 'supervision_notif'
+
+    def recipients(self):
+        return self.config['supervising-addrs']
+        
+    def subject(self):
+        return self.req._('[%s supervision] changes summary') % self.config.appid
+    
+    def call(self, changes):
+        user = self.req.actual_session().user
+        self.w(self.req._('user %s has made the following change(s):\n\n')
+               % user.login)
+        for event, changedescr in filter_changes(changes):
+            self.w(u'* ')
+            getattr(self, event)(*changedescr)
+            self.w(u'\n\n')
+
+    def _entity_context(self, entity):
+        return {'eid': entity.eid,
+                'etype': entity.dc_type().lower(),
+                'title': entity.dc_title()}
+    
+    def add_entity(self, entity):
+        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())
+            
+    def update_entity(self, entity):
+        msg = self.req._('updated %(etype)s #%(eid)s (%(title)s)')
+        self.w(u'%s\n' % (msg % self._entity_context(entity)))
+        # XXX print changes
+        self.w(u'  %s' % entity.absolute_url())
+            
+    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):
+        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):
+        _ = self.req._
+        session = self.req.actual_session()
+        def describe(eid):
+            try:
+                return _(session.describe(eid)[0]).lower()
+            except UnknownEid:
+                # may occurs when an entity has been deleted from an external
+                # source and we're cleaning its relation
+                return _('unknown external entity')
+        return {'rtype': _(rtype),
+                'fromeid': fromeid,
+                'frometype': describe(fromeid),
+                'toeid': toeid,
+                'toetype': describe(toeid)}
+        
+    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 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))
+        
+                
+class SupervisionMailOp(SendMailOp):
+    """special send email operation which should be done only once for a bunch
+    of changes
+    """
+    def _get_view(self):
+        return self.session.vreg.select_component('supervision_notif',
+                                                  self.session, None)
+        
+    def _prepare_email(self):
+        session = self.session
+        config = session.vreg.config
+        uinfo = {'email': config['sender-addr'],
+                 'name': config['sender-name']}
+        view = self._get_view()
+        content = view.dispatch(changes=session.query_data('pendingchanges'))
+        recipients = view.recipients()
+        msg = format_mail(uinfo, recipients, content, view.subject(), config=config)
+        self.to_send = [(msg, recipients)]
+
+    def commit_event(self):
+        self._prepare_email()
+        SendMailOp.commit_event(self)