sobjects/supervising.py
changeset 0 b97547f5f1fa
child 655 ca3c4992c7d1
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """some hooks and views to handle supervising of any data changes
       
     2 
       
     3 
       
     4 :organization: Logilab
       
     5 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     7 """
       
     8 __docformat__ = "restructuredtext en"
       
     9 
       
    10 from cubicweb import UnknownEid
       
    11 from cubicweb.common.view import ComponentMixIn, StartupView
       
    12 from cubicweb.common.mail import format_mail
       
    13 from cubicweb.server.hooksmanager import Hook
       
    14 from cubicweb.server.hookhelper import SendMailOp
       
    15 
       
    16 
       
    17 class SomethingChangedHook(Hook):
       
    18     events = ('before_add_relation', 'before_delete_relation',
       
    19               'after_add_entity', 'before_update_entity')
       
    20     accepts = ('Any',)
       
    21     
       
    22     def call(self, session, *args):
       
    23         dest = self.config['supervising-addrs']
       
    24         if not dest: # no supervisors, don't do this for nothing...
       
    25             return
       
    26         self.session = session
       
    27         if self._call(*args):
       
    28             SupervisionMailOp(session)
       
    29         
       
    30     def _call(self, *args):
       
    31         if self._event() == 'update_entity' and args[0].e_schema == 'EUser':
       
    32             updated = set(args[0].iterkeys())
       
    33             if not (updated - frozenset(('eid', 'modification_date', 'last_login_time'))):
       
    34                 # don't record last_login_time update which are done 
       
    35                 # automatically at login time
       
    36                 return False
       
    37         self.session.add_query_data('pendingchanges', (self._event(), args))
       
    38         return True
       
    39         
       
    40     def _event(self):
       
    41         return self.event.split('_', 1)[1]
       
    42 
       
    43 
       
    44 class EntityDeleteHook(SomethingChangedHook):
       
    45     events = ('before_delete_entity',)
       
    46     
       
    47     def _call(self, eid):
       
    48         entity = self.session.entity(eid)
       
    49         try:
       
    50             title = entity.dc_title()
       
    51         except:
       
    52             # may raise an error during deletion process, for instance due to
       
    53             # missing required relation
       
    54             title = '#%s' % eid
       
    55         self.session.add_query_data('pendingchanges',
       
    56                                     ('delete_entity',
       
    57                                      (eid, str(entity.e_schema),
       
    58                                       title)))
       
    59         return True
       
    60 
       
    61 
       
    62 def filter_changes(changes):
       
    63     """
       
    64     * when an entity has been deleted:
       
    65       * don't show deletion of its relations
       
    66       * don't show related TrInfo deletion if any
       
    67     * when an entity has been added don't show owned_by relation addition
       
    68     * don't show new TrInfo entities if any
       
    69     """
       
    70     # first build an index of changes
       
    71     index = {}
       
    72     added, deleted = set(), set()
       
    73     for change in changes[:]:
       
    74         event, changedescr = change
       
    75         if event == 'add_entity':
       
    76             entity = changedescr[0]
       
    77             added.add(entity.eid)
       
    78             if entity.e_schema == 'TrInfo':
       
    79                 changes.remove(change)
       
    80                 if entity.from_state:
       
    81                     try:
       
    82                         changes.remove( ('delete_relation', 
       
    83                                          (entity.wf_info_for[0].eid, 'in_state', 
       
    84                                           entity.from_state[0].eid)) )
       
    85                     except ValueError:
       
    86                         pass
       
    87                     try:
       
    88                         changes.remove( ('add_relation', 
       
    89                                          (entity.wf_info_for[0].eid, 'in_state', 
       
    90                                           entity.to_state[0].eid)) )
       
    91                     except ValueError:
       
    92                         pass
       
    93                     event = 'change_state'
       
    94                     change = (event, 
       
    95                               (entity.wf_info_for[0],
       
    96                                entity.from_state[0], entity.to_state[0]))
       
    97                     changes.append(change)
       
    98         elif event == 'delete_entity':
       
    99             deleted.add(changedescr[0])
       
   100         index.setdefault(event, set()).add(change)
       
   101     # filter changes
       
   102     for eid in added:
       
   103         try:
       
   104             for change in index['add_relation'].copy():
       
   105                 changedescr = change[1]
       
   106                 # skip meta-relations which are set automatically
       
   107                 # XXX generate list below using rtags (category = 'generated')
       
   108                 if changedescr[1] in ('created_by', 'owned_by', 'is', 'is_instance_of',
       
   109                                       'from_state', 'to_state', 'wf_info_for',) \
       
   110                        and changedescr[0] == eid:
       
   111                     index['add_relation'].remove(change)
       
   112                 # skip in_state relation if the entity is being created
       
   113                 # XXX this may be automatized by skipping all mandatory relation
       
   114                 #     at entity creation time
       
   115                 elif changedescr[1] == 'in_state' and changedescr[0] in added:
       
   116                     index['add_relation'].remove(change)
       
   117                     
       
   118         except KeyError:
       
   119             break
       
   120     for eid in deleted:
       
   121         try:
       
   122             for change in index['delete_relation'].copy():
       
   123                 fromeid, rtype, toeid = change[1]
       
   124                 if fromeid == eid:
       
   125                     index['delete_relation'].remove(change)
       
   126                 elif toeid == eid:
       
   127                     index['delete_relation'].remove(change)
       
   128                     if rtype == 'wf_info_for':
       
   129                         for change in index['delete_entity'].copy():
       
   130                             if change[1][0] == fromeid:
       
   131                                 index['delete_entity'].remove(change)
       
   132         except KeyError:
       
   133             break
       
   134     for change in changes:
       
   135         event, changedescr = change
       
   136         if change in index[event]:
       
   137             yield change
       
   138 
       
   139 
       
   140 class SupervisionEmailView(ComponentMixIn, StartupView):
       
   141     """view implementing the email API for data changes supervision notification
       
   142     """
       
   143     id = 'supervision_notif'
       
   144 
       
   145     def recipients(self):
       
   146         return self.config['supervising-addrs']
       
   147         
       
   148     def subject(self):
       
   149         return self.req._('[%s supervision] changes summary') % self.config.appid
       
   150     
       
   151     def call(self, changes):
       
   152         user = self.req.actual_session().user
       
   153         self.w(self.req._('user %s has made the following change(s):\n\n')
       
   154                % user.login)
       
   155         for event, changedescr in filter_changes(changes):
       
   156             self.w(u'* ')
       
   157             getattr(self, event)(*changedescr)
       
   158             self.w(u'\n\n')
       
   159 
       
   160     def _entity_context(self, entity):
       
   161         return {'eid': entity.eid,
       
   162                 'etype': entity.dc_type().lower(),
       
   163                 'title': entity.dc_title()}
       
   164     
       
   165     def add_entity(self, entity):
       
   166         msg = self.req._('added %(etype)s #%(eid)s (%(title)s)')
       
   167         self.w(u'%s\n' % (msg % self._entity_context(entity)))
       
   168         self.w(u'  %s' % entity.absolute_url())
       
   169             
       
   170     def update_entity(self, entity):
       
   171         msg = self.req._('updated %(etype)s #%(eid)s (%(title)s)')
       
   172         self.w(u'%s\n' % (msg % self._entity_context(entity)))
       
   173         # XXX print changes
       
   174         self.w(u'  %s' % entity.absolute_url())
       
   175             
       
   176     def delete_entity(self, eid, etype, title):
       
   177         msg = self.req._('deleted %(etype)s #%(eid)s (%(title)s)')
       
   178         etype = display_name(self.req, etype).lower()
       
   179         self.w(msg % locals())
       
   180         
       
   181     def change_state(self, entity, fromstate, tostate):
       
   182         msg = self.req._('changed state of %(etype)s #%(eid)s (%(title)s)')
       
   183         self.w(u'%s\n' % (msg % self._entity_context(entity)))
       
   184         self.w(_('  from state %(fromstate)s to state %(tostate)s\n' % 
       
   185                  {'fromstate': _(fromstate.name), 'tostate': _(tostate.name)}))
       
   186         self.w(u'  %s' % entity.absolute_url())
       
   187         
       
   188     def _relation_context(self, fromeid, rtype, toeid):
       
   189         _ = self.req._
       
   190         session = self.req.actual_session()
       
   191         def describe(eid):
       
   192             try:
       
   193                 return _(session.describe(eid)[0]).lower()
       
   194             except UnknownEid:
       
   195                 # may occurs when an entity has been deleted from an external
       
   196                 # source and we're cleaning its relation
       
   197                 return _('unknown external entity')
       
   198         return {'rtype': _(rtype),
       
   199                 'fromeid': fromeid,
       
   200                 'frometype': describe(fromeid),
       
   201                 'toeid': toeid,
       
   202                 'toetype': describe(toeid)}
       
   203         
       
   204     def add_relation(self, fromeid, rtype, toeid):
       
   205         msg = self.req._('added relation %(rtype)s from %(frometype)s #%(fromeid)s to %(toetype)s #%(toeid)s')
       
   206         self.w(msg % self._relation_context(fromeid, rtype, toeid))
       
   207 
       
   208     def delete_relation(self, fromeid, rtype, toeid):
       
   209         msg = self.req._('deleted relation %(rtype)s from %(frometype)s #%(fromeid)s to %(toetype)s #%(toeid)s')
       
   210         self.w(msg % self._relation_context(fromeid, rtype, toeid))
       
   211         
       
   212                 
       
   213 class SupervisionMailOp(SendMailOp):
       
   214     """special send email operation which should be done only once for a bunch
       
   215     of changes
       
   216     """
       
   217     def _get_view(self):
       
   218         return self.session.vreg.select_component('supervision_notif',
       
   219                                                   self.session, None)
       
   220         
       
   221     def _prepare_email(self):
       
   222         session = self.session
       
   223         config = session.vreg.config
       
   224         uinfo = {'email': config['sender-addr'],
       
   225                  'name': config['sender-name']}
       
   226         view = self._get_view()
       
   227         content = view.dispatch(changes=session.query_data('pendingchanges'))
       
   228         recipients = view.recipients()
       
   229         msg = format_mail(uinfo, recipients, content, view.subject(), config=config)
       
   230         self.to_send = [(msg, recipients)]
       
   231 
       
   232     def commit_event(self):
       
   233         self._prepare_email()
       
   234         SendMailOp.commit_event(self)