sobjects/supervising.py
author sylvain.thenault@logilab.fr
Fri, 03 Apr 2009 19:04:00 +0200
changeset 1228 91ae10ffb611
parent 0 b97547f5f1fa
child 655 ca3c4992c7d1
permissions -rw-r--r--
* refactor ms planner (renaming, reorganization) * fix a bug originaly demonstrated by test_version_depends_on * enhance crossed relation support, though there is still some bug renaming. some tests were actually wrong. Buggy tests (wether they fail or not, they are byggy) marked by XXXFIXME)

"""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)