##// END OF EJS Templates
notifications: reduce logging for email notifications.
dan -
r4292:6a785798 default
parent child Browse files
Show More
@@ -1,411 +1,411 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Model for notifications
24 24 """
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 import premailer
30 30 from pyramid.threadlocal import get_current_request
31 31 from sqlalchemy.sql.expression import false, true
32 32
33 33 import rhodecode
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.model import BaseModel
36 36 from rhodecode.model.db import Notification, User, UserNotification
37 37 from rhodecode.model.meta import Session
38 38 from rhodecode.translation import TranslationString
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class NotificationModel(BaseModel):
44 44
45 45 cls = Notification
46 46
47 47 def __get_notification(self, notification):
48 48 if isinstance(notification, Notification):
49 49 return notification
50 50 elif isinstance(notification, (int, long)):
51 51 return Notification.get(notification)
52 52 else:
53 53 if notification:
54 54 raise Exception('notification must be int, long or Instance'
55 55 ' of Notification got %s' % type(notification))
56 56
57 57 def create(
58 58 self, created_by, notification_subject, notification_body,
59 59 notification_type=Notification.TYPE_MESSAGE, recipients=None,
60 60 mention_recipients=None, with_email=True, email_kwargs=None):
61 61 """
62 62
63 63 Creates notification of given type
64 64
65 65 :param created_by: int, str or User instance. User who created this
66 66 notification
67 67 :param notification_subject: subject of notification itself
68 68 :param notification_body: body of notification text
69 69 :param notification_type: type of notification, based on that we
70 70 pick templates
71 71
72 72 :param recipients: list of int, str or User objects, when None
73 73 is given send to all admins
74 74 :param mention_recipients: list of int, str or User objects,
75 75 that were mentioned
76 76 :param with_email: send email with this notification
77 77 :param email_kwargs: dict with arguments to generate email
78 78 """
79 79
80 80 from rhodecode.lib.celerylib import tasks, run_task
81 81
82 82 if recipients and not getattr(recipients, '__iter__', False):
83 83 raise Exception('recipients must be an iterable object')
84 84
85 85 created_by_obj = self._get_user(created_by)
86 86 # default MAIN body if not given
87 87 email_kwargs = email_kwargs or {'body': notification_body}
88 88 mention_recipients = mention_recipients or set()
89 89
90 90 if not created_by_obj:
91 91 raise Exception('unknown user %s' % created_by)
92 92
93 93 if recipients is None:
94 94 # recipients is None means to all admins
95 95 recipients_objs = User.query().filter(User.admin == true()).all()
96 96 log.debug('sending notifications %s to admins: %s',
97 97 notification_type, recipients_objs)
98 98 else:
99 99 recipients_objs = set()
100 100 for u in recipients:
101 101 obj = self._get_user(u)
102 102 if obj:
103 103 recipients_objs.add(obj)
104 104 else: # we didn't find this user, log the error and carry on
105 105 log.error('cannot notify unknown user %r', u)
106 106
107 107 if not recipients_objs:
108 108 raise Exception('no valid recipients specified')
109 109
110 110 log.debug('sending notifications %s to %s',
111 111 notification_type, recipients_objs)
112 112
113 113 # add mentioned users into recipients
114 114 final_recipients = set(recipients_objs).union(mention_recipients)
115 115
116 116 notification = Notification.create(
117 117 created_by=created_by_obj, subject=notification_subject,
118 118 body=notification_body, recipients=final_recipients,
119 119 type_=notification_type
120 120 )
121 121
122 122 if not with_email: # skip sending email, and just create notification
123 123 return notification
124 124
125 125 # don't send email to person who created this comment
126 126 rec_objs = set(recipients_objs).difference({created_by_obj})
127 127
128 128 # now notify all recipients in question
129 129
130 130 for recipient in rec_objs.union(mention_recipients):
131 131 # inject current recipient
132 132 email_kwargs['recipient'] = recipient
133 133 email_kwargs['mention'] = recipient in mention_recipients
134 134 (subject, headers, email_body,
135 135 email_body_plaintext) = EmailNotificationModel().render_email(
136 136 notification_type, **email_kwargs)
137 137
138 138 log.debug(
139 139 'Creating notification email task for user:`%s`', recipient)
140 140 task = run_task(
141 141 tasks.send_email, recipient.email, subject,
142 142 email_body_plaintext, email_body)
143 143 log.debug('Created email task: %s', task)
144 144
145 145 return notification
146 146
147 147 def delete(self, user, notification):
148 148 # we don't want to remove actual notification just the assignment
149 149 try:
150 150 notification = self.__get_notification(notification)
151 151 user = self._get_user(user)
152 152 if notification and user:
153 153 obj = UserNotification.query()\
154 154 .filter(UserNotification.user == user)\
155 155 .filter(UserNotification.notification == notification)\
156 156 .one()
157 157 Session().delete(obj)
158 158 return True
159 159 except Exception:
160 160 log.error(traceback.format_exc())
161 161 raise
162 162
163 163 def get_for_user(self, user, filter_=None):
164 164 """
165 165 Get mentions for given user, filter them if filter dict is given
166 166 """
167 167 user = self._get_user(user)
168 168
169 169 q = UserNotification.query()\
170 170 .filter(UserNotification.user == user)\
171 171 .join((
172 172 Notification, UserNotification.notification_id ==
173 173 Notification.notification_id))
174 174 if filter_ == ['all']:
175 175 q = q # no filter
176 176 elif filter_ == ['unread']:
177 177 q = q.filter(UserNotification.read == false())
178 178 elif filter_:
179 179 q = q.filter(Notification.type_.in_(filter_))
180 180
181 181 return q
182 182
183 183 def mark_read(self, user, notification):
184 184 try:
185 185 notification = self.__get_notification(notification)
186 186 user = self._get_user(user)
187 187 if notification and user:
188 188 obj = UserNotification.query()\
189 189 .filter(UserNotification.user == user)\
190 190 .filter(UserNotification.notification == notification)\
191 191 .one()
192 192 obj.read = True
193 193 Session().add(obj)
194 194 return True
195 195 except Exception:
196 196 log.error(traceback.format_exc())
197 197 raise
198 198
199 199 def mark_all_read_for_user(self, user, filter_=None):
200 200 user = self._get_user(user)
201 201 q = UserNotification.query()\
202 202 .filter(UserNotification.user == user)\
203 203 .filter(UserNotification.read == false())\
204 204 .join((
205 205 Notification, UserNotification.notification_id ==
206 206 Notification.notification_id))
207 207 if filter_ == ['unread']:
208 208 q = q.filter(UserNotification.read == false())
209 209 elif filter_:
210 210 q = q.filter(Notification.type_.in_(filter_))
211 211
212 212 # this is a little inefficient but sqlalchemy doesn't support
213 213 # update on joined tables :(
214 214 for obj in q.all():
215 215 obj.read = True
216 216 Session().add(obj)
217 217
218 218 def get_unread_cnt_for_user(self, user):
219 219 user = self._get_user(user)
220 220 return UserNotification.query()\
221 221 .filter(UserNotification.read == false())\
222 222 .filter(UserNotification.user == user).count()
223 223
224 224 def get_unread_for_user(self, user):
225 225 user = self._get_user(user)
226 226 return [x.notification for x in UserNotification.query()
227 227 .filter(UserNotification.read == false())
228 228 .filter(UserNotification.user == user).all()]
229 229
230 230 def get_user_notification(self, user, notification):
231 231 user = self._get_user(user)
232 232 notification = self.__get_notification(notification)
233 233
234 234 return UserNotification.query()\
235 235 .filter(UserNotification.notification == notification)\
236 236 .filter(UserNotification.user == user).scalar()
237 237
238 238 def make_description(self, notification, translate, show_age=True):
239 239 """
240 240 Creates a human readable description based on properties
241 241 of notification object
242 242 """
243 243 _ = translate
244 244 _map = {
245 245 notification.TYPE_CHANGESET_COMMENT: [
246 246 _('%(user)s commented on commit %(date_or_age)s'),
247 247 _('%(user)s commented on commit at %(date_or_age)s'),
248 248 ],
249 249 notification.TYPE_MESSAGE: [
250 250 _('%(user)s sent message %(date_or_age)s'),
251 251 _('%(user)s sent message at %(date_or_age)s'),
252 252 ],
253 253 notification.TYPE_MENTION: [
254 254 _('%(user)s mentioned you %(date_or_age)s'),
255 255 _('%(user)s mentioned you at %(date_or_age)s'),
256 256 ],
257 257 notification.TYPE_REGISTRATION: [
258 258 _('%(user)s registered in RhodeCode %(date_or_age)s'),
259 259 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
260 260 ],
261 261 notification.TYPE_PULL_REQUEST: [
262 262 _('%(user)s opened new pull request %(date_or_age)s'),
263 263 _('%(user)s opened new pull request at %(date_or_age)s'),
264 264 ],
265 265 notification.TYPE_PULL_REQUEST_UPDATE: [
266 266 _('%(user)s updated pull request %(date_or_age)s'),
267 267 _('%(user)s updated pull request at %(date_or_age)s'),
268 268 ],
269 269 notification.TYPE_PULL_REQUEST_COMMENT: [
270 270 _('%(user)s commented on pull request %(date_or_age)s'),
271 271 _('%(user)s commented on pull request at %(date_or_age)s'),
272 272 ],
273 273 }
274 274
275 275 templates = _map[notification.type_]
276 276
277 277 if show_age:
278 278 template = templates[0]
279 279 date_or_age = h.age(notification.created_on)
280 280 if translate:
281 281 date_or_age = translate(date_or_age)
282 282
283 283 if isinstance(date_or_age, TranslationString):
284 284 date_or_age = date_or_age.interpolate()
285 285
286 286 else:
287 287 template = templates[1]
288 288 date_or_age = h.format_date(notification.created_on)
289 289
290 290 return template % {
291 291 'user': notification.created_by_user.username,
292 292 'date_or_age': date_or_age,
293 293 }
294 294
295 295
296 296 class EmailNotificationModel(BaseModel):
297 297 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
298 298 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
299 299 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
300 300 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
301 301 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
302 302 TYPE_MAIN = Notification.TYPE_MESSAGE
303 303
304 304 TYPE_PASSWORD_RESET = 'password_reset'
305 305 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
306 306 TYPE_EMAIL_TEST = 'email_test'
307 307 TYPE_EMAIL_EXCEPTION = 'exception'
308 308 TYPE_TEST = 'test'
309 309
310 310 email_types = {
311 311 TYPE_MAIN:
312 312 'rhodecode:templates/email_templates/main.mako',
313 313 TYPE_TEST:
314 314 'rhodecode:templates/email_templates/test.mako',
315 315 TYPE_EMAIL_EXCEPTION:
316 316 'rhodecode:templates/email_templates/exception_tracker.mako',
317 317 TYPE_EMAIL_TEST:
318 318 'rhodecode:templates/email_templates/email_test.mako',
319 319 TYPE_REGISTRATION:
320 320 'rhodecode:templates/email_templates/user_registration.mako',
321 321 TYPE_PASSWORD_RESET:
322 322 'rhodecode:templates/email_templates/password_reset.mako',
323 323 TYPE_PASSWORD_RESET_CONFIRMATION:
324 324 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
325 325 TYPE_COMMIT_COMMENT:
326 326 'rhodecode:templates/email_templates/commit_comment.mako',
327 327 TYPE_PULL_REQUEST:
328 328 'rhodecode:templates/email_templates/pull_request_review.mako',
329 329 TYPE_PULL_REQUEST_COMMENT:
330 330 'rhodecode:templates/email_templates/pull_request_comment.mako',
331 331 TYPE_PULL_REQUEST_UPDATE:
332 332 'rhodecode:templates/email_templates/pull_request_update.mako',
333 333 }
334 334
335 335 premailer_instance = premailer.Premailer(
336 cssutils_logging_level=logging.DEBUG,
336 cssutils_logging_level=logging.WARNING,
337 337 cssutils_logging_handler=logging.getLogger().handlers[0]
338 338 if logging.getLogger().handlers else None,
339 339 )
340 340
341 341 def __init__(self):
342 342 """
343 343 Example usage::
344 344
345 345 (subject, headers, email_body,
346 346 email_body_plaintext) = EmailNotificationModel().render_email(
347 347 EmailNotificationModel.TYPE_TEST, **email_kwargs)
348 348
349 349 """
350 350 super(EmailNotificationModel, self).__init__()
351 351 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
352 352
353 353 def _update_kwargs_for_render(self, kwargs):
354 354 """
355 355 Inject params required for Mako rendering
356 356
357 357 :param kwargs:
358 358 """
359 359
360 360 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
361 361 kwargs['rhodecode_version'] = rhodecode.__version__
362 362 instance_url = h.route_url('home')
363 363 _kwargs = {
364 364 'instance_url': instance_url,
365 365 'whitespace_filter': self.whitespace_filter
366 366 }
367 367 _kwargs.update(kwargs)
368 368 return _kwargs
369 369
370 370 def whitespace_filter(self, text):
371 371 return text.replace('\n', '').replace('\t', '')
372 372
373 373 def get_renderer(self, type_, request):
374 374 template_name = self.email_types[type_]
375 375 return request.get_partial_renderer(template_name)
376 376
377 377 def render_email(self, type_, **kwargs):
378 378 """
379 379 renders template for email, and returns a tuple of
380 380 (subject, email_headers, email_html_body, email_plaintext_body)
381 381 """
382 382 # translator and helpers inject
383 383 _kwargs = self._update_kwargs_for_render(kwargs)
384 384 request = get_current_request()
385 385 email_template = self.get_renderer(type_, request=request)
386 386
387 387 subject = email_template.render('subject', **_kwargs)
388 388
389 389 try:
390 390 headers = email_template.render('headers', **_kwargs)
391 391 except AttributeError:
392 392 # it's not defined in template, ok we can skip it
393 393 headers = ''
394 394
395 395 try:
396 396 body_plaintext = email_template.render('body_plaintext', **_kwargs)
397 397 except AttributeError:
398 398 # it's not defined in template, ok we can skip it
399 399 body_plaintext = ''
400 400
401 401 # render WHOLE template
402 402 body = email_template.render(None, **_kwargs)
403 403
404 404 try:
405 405 # Inline CSS styles and conversion
406 406 body = self.premailer_instance.transform(body)
407 407 except Exception:
408 408 log.exception('Failed to parse body with premailer')
409 409 pass
410 410
411 411 return subject, headers, body, body_plaintext
General Comments 0
You need to be logged in to leave comments. Login now