##// END OF EJS Templates
premailer: replace logger instead of altering handlers....
super-admin -
r4857:916d130a default
parent child Browse files
Show More
@@ -1,453 +1,454 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 it will be generated automatically from notification_type if not specified
69 69 :param notification_body: body of notification text
70 70 it will be generated automatically from notification_type if not specified
71 71 :param notification_type: type of notification, based on that we
72 72 pick templates
73 73 :param recipients: list of int, str or User objects, when None
74 74 is given send to all admins
75 75 :param mention_recipients: list of int, str or User objects,
76 76 that were mentioned
77 77 :param with_email: send email with this notification
78 78 :param email_kwargs: dict with arguments to generate email
79 79 """
80 80
81 81 from rhodecode.lib.celerylib import tasks, run_task
82 82
83 83 if recipients and not getattr(recipients, '__iter__', False):
84 84 raise Exception('recipients must be an iterable object')
85 85
86 86 if not (notification_subject and notification_body) and not notification_type:
87 87 raise ValueError('notification_subject, and notification_body '
88 88 'cannot be empty when notification_type is not specified')
89 89
90 90 created_by_obj = self._get_user(created_by)
91 91
92 92 if not created_by_obj:
93 93 raise Exception('unknown user %s' % created_by)
94 94
95 95 # default MAIN body if not given
96 96 email_kwargs = email_kwargs or {'body': notification_body}
97 97 mention_recipients = mention_recipients or set()
98 98
99 99 if recipients is None:
100 100 # recipients is None means to all admins
101 101 recipients_objs = User.query().filter(User.admin == true()).all()
102 102 log.debug('sending notifications %s to admins: %s',
103 103 notification_type, recipients_objs)
104 104 else:
105 105 recipients_objs = set()
106 106 for u in recipients:
107 107 obj = self._get_user(u)
108 108 if obj:
109 109 recipients_objs.add(obj)
110 110 else: # we didn't find this user, log the error and carry on
111 111 log.error('cannot notify unknown user %r', u)
112 112
113 113 if not recipients_objs:
114 114 raise Exception('no valid recipients specified')
115 115
116 116 log.debug('sending notifications %s to %s',
117 117 notification_type, recipients_objs)
118 118
119 119 # add mentioned users into recipients
120 120 final_recipients = set(recipients_objs).union(mention_recipients)
121 121
122 122 (subject, email_body, email_body_plaintext) = \
123 123 EmailNotificationModel().render_email(notification_type, **email_kwargs)
124 124
125 125 if not notification_subject:
126 126 notification_subject = subject
127 127
128 128 if not notification_body:
129 129 notification_body = email_body_plaintext
130 130
131 131 notification = Notification.create(
132 132 created_by=created_by_obj, subject=notification_subject,
133 133 body=notification_body, recipients=final_recipients,
134 134 type_=notification_type
135 135 )
136 136
137 137 if not with_email: # skip sending email, and just create notification
138 138 return notification
139 139
140 140 # don't send email to person who created this comment
141 141 rec_objs = set(recipients_objs).difference({created_by_obj})
142 142
143 143 # now notify all recipients in question
144 144
145 145 for recipient in rec_objs.union(mention_recipients):
146 146 # inject current recipient
147 147 email_kwargs['recipient'] = recipient
148 148 email_kwargs['mention'] = recipient in mention_recipients
149 149 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
150 150 notification_type, **email_kwargs)
151 151
152 152 extra_headers = None
153 153 if 'thread_ids' in email_kwargs:
154 154 extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
155 155
156 156 log.debug('Creating notification email task for user:`%s`', recipient)
157 157 task = run_task(
158 158 tasks.send_email, recipient.email, subject,
159 159 email_body_plaintext, email_body, extra_headers=extra_headers)
160 160 log.debug('Created email task: %s', task)
161 161
162 162 return notification
163 163
164 164 def delete(self, user, notification):
165 165 # we don't want to remove actual notification just the assignment
166 166 try:
167 167 notification = self.__get_notification(notification)
168 168 user = self._get_user(user)
169 169 if notification and user:
170 170 obj = UserNotification.query()\
171 171 .filter(UserNotification.user == user)\
172 172 .filter(UserNotification.notification == notification)\
173 173 .one()
174 174 Session().delete(obj)
175 175 return True
176 176 except Exception:
177 177 log.error(traceback.format_exc())
178 178 raise
179 179
180 180 def get_for_user(self, user, filter_=None):
181 181 """
182 182 Get mentions for given user, filter them if filter dict is given
183 183 """
184 184 user = self._get_user(user)
185 185
186 186 q = UserNotification.query()\
187 187 .filter(UserNotification.user == user)\
188 188 .join((
189 189 Notification, UserNotification.notification_id ==
190 190 Notification.notification_id))
191 191 if filter_ == ['all']:
192 192 q = q # no filter
193 193 elif filter_ == ['unread']:
194 194 q = q.filter(UserNotification.read == false())
195 195 elif filter_:
196 196 q = q.filter(Notification.type_.in_(filter_))
197 197
198 198 return q
199 199
200 200 def mark_read(self, user, notification):
201 201 try:
202 202 notification = self.__get_notification(notification)
203 203 user = self._get_user(user)
204 204 if notification and user:
205 205 obj = UserNotification.query()\
206 206 .filter(UserNotification.user == user)\
207 207 .filter(UserNotification.notification == notification)\
208 208 .one()
209 209 obj.read = True
210 210 Session().add(obj)
211 211 return True
212 212 except Exception:
213 213 log.error(traceback.format_exc())
214 214 raise
215 215
216 216 def mark_all_read_for_user(self, user, filter_=None):
217 217 user = self._get_user(user)
218 218 q = UserNotification.query()\
219 219 .filter(UserNotification.user == user)\
220 220 .filter(UserNotification.read == false())\
221 221 .join((
222 222 Notification, UserNotification.notification_id ==
223 223 Notification.notification_id))
224 224 if filter_ == ['unread']:
225 225 q = q.filter(UserNotification.read == false())
226 226 elif filter_:
227 227 q = q.filter(Notification.type_.in_(filter_))
228 228
229 229 # this is a little inefficient but sqlalchemy doesn't support
230 230 # update on joined tables :(
231 231 for obj in q.all():
232 232 obj.read = True
233 233 Session().add(obj)
234 234
235 235 def get_unread_cnt_for_user(self, user):
236 236 user = self._get_user(user)
237 237 return UserNotification.query()\
238 238 .filter(UserNotification.read == false())\
239 239 .filter(UserNotification.user == user).count()
240 240
241 241 def get_unread_for_user(self, user):
242 242 user = self._get_user(user)
243 243 return [x.notification for x in UserNotification.query()
244 244 .filter(UserNotification.read == false())
245 245 .filter(UserNotification.user == user).all()]
246 246
247 247 def get_user_notification(self, user, notification):
248 248 user = self._get_user(user)
249 249 notification = self.__get_notification(notification)
250 250
251 251 return UserNotification.query()\
252 252 .filter(UserNotification.notification == notification)\
253 253 .filter(UserNotification.user == user).scalar()
254 254
255 255 def make_description(self, notification, translate, show_age=True):
256 256 """
257 257 Creates a human readable description based on properties
258 258 of notification object
259 259 """
260 260 _ = translate
261 261 _map = {
262 262 notification.TYPE_CHANGESET_COMMENT: [
263 263 _('%(user)s commented on commit %(date_or_age)s'),
264 264 _('%(user)s commented on commit at %(date_or_age)s'),
265 265 ],
266 266 notification.TYPE_MESSAGE: [
267 267 _('%(user)s sent message %(date_or_age)s'),
268 268 _('%(user)s sent message at %(date_or_age)s'),
269 269 ],
270 270 notification.TYPE_MENTION: [
271 271 _('%(user)s mentioned you %(date_or_age)s'),
272 272 _('%(user)s mentioned you at %(date_or_age)s'),
273 273 ],
274 274 notification.TYPE_REGISTRATION: [
275 275 _('%(user)s registered in RhodeCode %(date_or_age)s'),
276 276 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
277 277 ],
278 278 notification.TYPE_PULL_REQUEST: [
279 279 _('%(user)s opened new pull request %(date_or_age)s'),
280 280 _('%(user)s opened new pull request at %(date_or_age)s'),
281 281 ],
282 282 notification.TYPE_PULL_REQUEST_UPDATE: [
283 283 _('%(user)s updated pull request %(date_or_age)s'),
284 284 _('%(user)s updated pull request at %(date_or_age)s'),
285 285 ],
286 286 notification.TYPE_PULL_REQUEST_COMMENT: [
287 287 _('%(user)s commented on pull request %(date_or_age)s'),
288 288 _('%(user)s commented on pull request at %(date_or_age)s'),
289 289 ],
290 290 }
291 291
292 292 templates = _map[notification.type_]
293 293
294 294 if show_age:
295 295 template = templates[0]
296 296 date_or_age = h.age(notification.created_on)
297 297 if translate:
298 298 date_or_age = translate(date_or_age)
299 299
300 300 if isinstance(date_or_age, TranslationString):
301 301 date_or_age = date_or_age.interpolate()
302 302
303 303 else:
304 304 template = templates[1]
305 305 date_or_age = h.format_date(notification.created_on)
306 306
307 307 return template % {
308 308 'user': notification.created_by_user.username,
309 309 'date_or_age': date_or_age,
310 310 }
311 311
312 312
313 313 # Templates for Titles, that could be overwritten by rcextensions
314 314 # Title of email for pull-request update
315 315 EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = ''
316 316 # Title of email for request for pull request review
317 317 EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = ''
318 318
319 319 # Title of email for general comment on pull request
320 320 EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = ''
321 321 # Title of email for general comment which includes status change on pull request
322 322 EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
323 323 # Title of email for inline comment on a file in pull request
324 324 EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = ''
325 325
326 326 # Title of email for general comment on commit
327 327 EMAIL_COMMENT_SUBJECT_TEMPLATE = ''
328 328 # Title of email for general comment which includes status change on commit
329 329 EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
330 330 # Title of email for inline comment on a file in commit
331 331 EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = ''
332 332
333 import cssutils
334 # hijack css utils logger and replace with ours
335 log = logging.getLogger('rhodecode.cssutils.premailer')
336 cssutils.log.setLog(log)
337
333 338
334 339 class EmailNotificationModel(BaseModel):
335 340 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
336 341 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
337 342 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
338 343 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
339 344 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
340 345 TYPE_MAIN = Notification.TYPE_MESSAGE
341 346
342 347 TYPE_PASSWORD_RESET = 'password_reset'
343 348 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
344 349 TYPE_EMAIL_TEST = 'email_test'
345 350 TYPE_EMAIL_EXCEPTION = 'exception'
346 351 TYPE_UPDATE_AVAILABLE = 'update_available'
347 352 TYPE_TEST = 'test'
348 353
349 354 email_types = {
350 355 TYPE_MAIN:
351 356 'rhodecode:templates/email_templates/main.mako',
352 357 TYPE_TEST:
353 358 'rhodecode:templates/email_templates/test.mako',
354 359 TYPE_EMAIL_EXCEPTION:
355 360 'rhodecode:templates/email_templates/exception_tracker.mako',
356 361 TYPE_UPDATE_AVAILABLE:
357 362 'rhodecode:templates/email_templates/update_available.mako',
358 363 TYPE_EMAIL_TEST:
359 364 'rhodecode:templates/email_templates/email_test.mako',
360 365 TYPE_REGISTRATION:
361 366 'rhodecode:templates/email_templates/user_registration.mako',
362 367 TYPE_PASSWORD_RESET:
363 368 'rhodecode:templates/email_templates/password_reset.mako',
364 369 TYPE_PASSWORD_RESET_CONFIRMATION:
365 370 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
366 371 TYPE_COMMIT_COMMENT:
367 372 'rhodecode:templates/email_templates/commit_comment.mako',
368 373 TYPE_PULL_REQUEST:
369 374 'rhodecode:templates/email_templates/pull_request_review.mako',
370 375 TYPE_PULL_REQUEST_COMMENT:
371 376 'rhodecode:templates/email_templates/pull_request_comment.mako',
372 377 TYPE_PULL_REQUEST_UPDATE:
373 378 'rhodecode:templates/email_templates/pull_request_update.mako',
374 379 }
375 380
376 premailer_instance = premailer.Premailer(
377 cssutils_logging_level=logging.ERROR,
378 cssutils_logging_handler=logging.getLogger().handlers[0]
379 if logging.getLogger().handlers else None,
380 )
381 premailer_instance = premailer.Premailer()
381 382
382 383 def __init__(self):
383 384 """
384 385 Example usage::
385 386
386 387 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
387 388 EmailNotificationModel.TYPE_TEST, **email_kwargs)
388 389
389 390 """
390 391 super(EmailNotificationModel, self).__init__()
391 392 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
392 393
393 394 def _update_kwargs_for_render(self, kwargs):
394 395 """
395 396 Inject params required for Mako rendering
396 397
397 398 :param kwargs:
398 399 """
399 400
400 401 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
401 402 kwargs['rhodecode_version'] = rhodecode.__version__
402 403 instance_url = h.route_url('home')
403 404 _kwargs = {
404 405 'instance_url': instance_url,
405 406 'whitespace_filter': self.whitespace_filter,
406 407 'email_pr_update_subject_template': EMAIL_PR_UPDATE_SUBJECT_TEMPLATE,
407 408 'email_pr_review_subject_template': EMAIL_PR_REVIEW_SUBJECT_TEMPLATE,
408 409 'email_pr_comment_subject_template': EMAIL_PR_COMMENT_SUBJECT_TEMPLATE,
409 410 'email_pr_comment_status_change_subject_template': EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
410 411 'email_pr_comment_file_subject_template': EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE,
411 412 'email_comment_subject_template': EMAIL_COMMENT_SUBJECT_TEMPLATE,
412 413 'email_comment_status_change_subject_template': EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
413 414 'email_comment_file_subject_template': EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE,
414 415 }
415 416 _kwargs.update(kwargs)
416 417 return _kwargs
417 418
418 419 def whitespace_filter(self, text):
419 420 return text.replace('\n', '').replace('\t', '')
420 421
421 422 def get_renderer(self, type_, request):
422 423 template_name = self.email_types[type_]
423 424 return request.get_partial_renderer(template_name)
424 425
425 426 def render_email(self, type_, **kwargs):
426 427 """
427 428 renders template for email, and returns a tuple of
428 429 (subject, email_headers, email_html_body, email_plaintext_body)
429 430 """
430 431 request = get_current_request()
431 432
432 433 # translator and helpers inject
433 434 _kwargs = self._update_kwargs_for_render(kwargs)
434 435 email_template = self.get_renderer(type_, request=request)
435 436 subject = email_template.render('subject', **_kwargs)
436 437
437 438 try:
438 439 body_plaintext = email_template.render('body_plaintext', **_kwargs)
439 440 except AttributeError:
440 441 # it's not defined in template, ok we can skip it
441 442 body_plaintext = ''
442 443
443 444 # render WHOLE template
444 445 body = email_template.render(None, **_kwargs)
445 446
446 447 try:
447 448 # Inline CSS styles and conversion
448 449 body = self.premailer_instance.transform(body)
449 450 except Exception:
450 451 log.exception('Failed to parse body with premailer')
451 452 pass
452 453
453 454 return subject, body, body_plaintext
General Comments 0
You need to be logged in to leave comments. Login now