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