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