cubicweb/hooks/notification.py
changeset 11057 0b59724cb3f2
parent 10351 91e63306e277
child 11129 97095348b3ee
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """some hooks to handle notification on entity's changes"""
       
    19 
       
    20 __docformat__ = "restructuredtext en"
       
    21 
       
    22 from logilab.common.textutils import normalize_text
       
    23 from logilab.common.deprecation import deprecated
       
    24 
       
    25 from cubicweb import RegistryNotFound
       
    26 from cubicweb.predicates import is_instance
       
    27 from cubicweb.server import hook
       
    28 from cubicweb.sobjects.supervising import SupervisionMailOp
       
    29 
       
    30 
       
    31 @deprecated('[3.17] use notify_on_commit instead')
       
    32 def RenderAndSendNotificationView(cnx, view, viewargs=None):
       
    33     notify_on_commit(cnx, view, viewargs)
       
    34 
       
    35 
       
    36 def notify_on_commit(cnx, view, viewargs=None):
       
    37     """register a notification view (see
       
    38     :class:`~cubicweb.sobjects.notification.NotificationView`) to be sent at
       
    39     post-commit time, ie only if the transaction has succeeded.
       
    40 
       
    41     `viewargs` is an optional dictionary containing extra argument to be given
       
    42     to :meth:`~cubicweb.sobjects.notification.NotificationView.render_and_send`
       
    43     """
       
    44     if viewargs is None:
       
    45         viewargs = {}
       
    46     notif_op = _RenderAndSendNotificationOp.get_instance(cnx)
       
    47     notif_op.add_data((view, viewargs))
       
    48 
       
    49 
       
    50 class _RenderAndSendNotificationOp(hook.DataOperationMixIn, hook.Operation):
       
    51     """End of the notification chain. Do render and send views after commit
       
    52 
       
    53     All others Operations end up adding data to this Operation.
       
    54     The notification are done on ``postcommit_event`` to make sure to prevent
       
    55     sending notification about rolled back data.
       
    56     """
       
    57 
       
    58     containercls = list
       
    59 
       
    60     def postcommit_event(self):
       
    61         deleted = self.cnx.deleted_in_transaction
       
    62         for view, viewargs in self.get_data():
       
    63             if view.cw_rset is not None:
       
    64                 if not view.cw_rset:
       
    65                     # entity added and deleted in the same transaction
       
    66                     # (cache effect)
       
    67                     continue
       
    68                 elif deleted(view.cw_rset[view.cw_row or 0][view.cw_col or 0]):
       
    69                     # entity added and deleted in the same transaction
       
    70                     continue
       
    71             try:
       
    72                 view.render_and_send(**viewargs)
       
    73             except Exception:
       
    74                 # error in post commit are not propagated
       
    75                 # We keep this logic here to prevent a small notification error
       
    76                 # to prevent them all.
       
    77                 self.exception('Notification failed')
       
    78 
       
    79 
       
    80 class NotificationHook(hook.Hook):
       
    81     __abstract__ = True
       
    82     category = 'notification'
       
    83 
       
    84     def select_view(self, vid, rset, row=0, col=0):
       
    85         try:
       
    86             return self._cw.vreg['views'].select_or_none(vid, self._cw, rset=rset,
       
    87                                                          row=row, col=col)
       
    88         except RegistryNotFound: # can happen in some config
       
    89                                  # (e.g. repo only config with no
       
    90                                  # notification views registered by
       
    91                                  # the instance's cubes)
       
    92             return None
       
    93 
       
    94 
       
    95 class StatusChangeHook(NotificationHook):
       
    96     """notify when a workflowable entity has its state modified"""
       
    97     __regid__ = 'notifystatuschange'
       
    98     __select__ = NotificationHook.__select__ & is_instance('TrInfo')
       
    99     events = ('after_add_entity',)
       
   100 
       
   101     def __call__(self):
       
   102         entity = self.entity
       
   103         if not entity.from_state: # not a transition
       
   104             return
       
   105         rset = entity.related('wf_info_for')
       
   106         view = self.select_view('notif_status_change', rset=rset, row=0)
       
   107         if view is None:
       
   108             return
       
   109         comment = entity.printable_value('comment', format='text/plain')
       
   110         # XXX don't try to wrap rest until we've a proper transformation (see
       
   111         # #103822)
       
   112         if comment and entity.comment_format != 'text/rest':
       
   113             comment = normalize_text(comment, 80)
       
   114         viewargs = {'comment': comment,
       
   115                     'previous_state': entity.previous_state.name,
       
   116                     'current_state': entity.new_state.name}
       
   117         notify_on_commit(self._cw, view, viewargs=viewargs)
       
   118 
       
   119 class RelationChangeHook(NotificationHook):
       
   120     __regid__ = 'notifyrelationchange'
       
   121     events = ('before_add_relation', 'after_add_relation',
       
   122               'before_delete_relation', 'after_delete_relation')
       
   123 
       
   124     def __call__(self):
       
   125         """if a notification view is defined for the event, send notification
       
   126         email defined by the view
       
   127         """
       
   128         rset = self._cw.eid_rset(self.eidfrom)
       
   129         view = self.select_view('notif_%s_%s' % (self.event,  self.rtype),
       
   130                                 rset=rset, row=0)
       
   131         if view is None:
       
   132             return
       
   133         notify_on_commit(self._cw, view)
       
   134 
       
   135 
       
   136 class EntityChangeHook(NotificationHook):
       
   137     """if a notification view is defined for the event, send notification
       
   138     email defined by the view
       
   139     """
       
   140     __regid__ = 'notifyentitychange'
       
   141     events = ('after_add_entity', 'after_update_entity')
       
   142 
       
   143     def __call__(self):
       
   144         rset = self.entity.as_rset()
       
   145         view = self.select_view('notif_%s' % self.event, rset=rset, row=0)
       
   146         if view is None:
       
   147             return
       
   148         notify_on_commit(self._cw, view)
       
   149 
       
   150 
       
   151 class EntityUpdatedNotificationOp(hook.SingleLastOperation):
       
   152     """scrap all changed entity to prepare a Notification Operation for them"""
       
   153 
       
   154     def precommit_event(self):
       
   155         # precommit event that creates postcommit operation
       
   156         cnx = self.cnx
       
   157         for eid in cnx.transaction_data['changes']:
       
   158             view = cnx.vreg['views'].select('notif_entity_updated', cnx,
       
   159                                             rset=cnx.eid_rset(eid),
       
   160                                             row=0)
       
   161             notify_on_commit(self.cnx, view,
       
   162                     viewargs={'changes': cnx.transaction_data['changes'][eid]})
       
   163 
       
   164 
       
   165 class EntityUpdateHook(NotificationHook):
       
   166     __regid__ = 'notifentityupdated'
       
   167     __abstract__ = True # do not register by default
       
   168     __select__ = NotificationHook.__select__ & hook.issued_from_user_query()
       
   169     events = ('before_update_entity',)
       
   170     skip_attrs = set()
       
   171 
       
   172     def __call__(self):
       
   173         cnx = self._cw
       
   174         if cnx.added_in_transaction(self.entity.eid):
       
   175             return # entity is being created
       
   176         # then compute changes
       
   177         attrs = [k for k in self.entity.cw_edited
       
   178                  if not k in self.skip_attrs]
       
   179         if not attrs:
       
   180             return
       
   181         changes = cnx.transaction_data.setdefault('changes', {})
       
   182         thisentitychanges = changes.setdefault(self.entity.eid, set())
       
   183         rqlsel, rqlrestr = [], ['X eid %(x)s']
       
   184         for i, attr in enumerate(attrs):
       
   185             var = chr(65+i)
       
   186             rqlsel.append(var)
       
   187             rqlrestr.append('X %s %s' % (attr, var))
       
   188         rql = 'Any %s WHERE %s' % (','.join(rqlsel), ','.join(rqlrestr))
       
   189         rset = cnx.execute(rql, {'x': self.entity.eid})
       
   190         for i, attr in enumerate(attrs):
       
   191             oldvalue = rset[0][i]
       
   192             newvalue = self.entity.cw_edited[attr]
       
   193             if oldvalue != newvalue:
       
   194                 thisentitychanges.add((attr, oldvalue, newvalue))
       
   195         if thisentitychanges:
       
   196             EntityUpdatedNotificationOp(cnx)
       
   197 
       
   198 
       
   199 # supervising ##################################################################
       
   200 
       
   201 class SomethingChangedHook(NotificationHook):
       
   202     __regid__ = 'supervising'
       
   203     __select__ = NotificationHook.__select__ & hook.issued_from_user_query()
       
   204     events = ('before_add_relation', 'before_delete_relation',
       
   205               'after_add_entity', 'before_update_entity')
       
   206 
       
   207     def __call__(self):
       
   208         dest = self._cw.vreg.config['supervising-addrs']
       
   209         if not dest: # no supervisors, don't do this for nothing...
       
   210             return
       
   211         if self._call():
       
   212             SupervisionMailOp(self._cw)
       
   213 
       
   214     def _call(self):
       
   215         event = self.event.split('_', 1)[1]
       
   216         if event == 'update_entity':
       
   217             if self._cw.added_in_transaction(self.entity.eid):
       
   218                 return False
       
   219             if self.entity.e_schema == 'CWUser':
       
   220                 if not (frozenset(self.entity.cw_edited)
       
   221                         - frozenset(('eid', 'modification_date',
       
   222                                      'last_login_time'))):
       
   223                     # don't record last_login_time update which are done
       
   224                     # automatically at login time
       
   225                     return False
       
   226         self._cw.transaction_data.setdefault('pendingchanges', []).append(
       
   227             (event, self))
       
   228         return True
       
   229 
       
   230 
       
   231 class EntityDeleteHook(SomethingChangedHook):
       
   232     __regid__ = 'supervisingentitydel'
       
   233     events = ('before_delete_entity',)
       
   234 
       
   235     def _call(self):
       
   236         try:
       
   237             title = self.entity.dc_title()
       
   238         except Exception:
       
   239             # may raise an error during deletion process, for instance due to
       
   240             # missing required relation
       
   241             title = '#%s' % self.entity.eid
       
   242         self._cw.transaction_data.setdefault('pendingchanges', []).append(
       
   243             ('delete_entity', (self.entity.eid, self.entity.cw_etype, title)))
       
   244         return True