##// END OF EJS Templates
notifications: adjusting how instance name is passed and fixing tests
lisaq -
r512:fafa37cf default
parent child Browse files
Show More
@@ -1,366 +1,367 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 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
27 27 import logging
28 28 import traceback
29 29
30 30 from pylons import tmpl_context as c
31 31 from pylons.i18n.translation import _, ungettext
32 32 from sqlalchemy.sql.expression import false, true
33 33 from mako import exceptions
34 34
35 35 import rhodecode
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.lib.utils import PartialRenderer
38 38 from rhodecode.model import BaseModel
39 39 from rhodecode.model.db import Notification, User, UserNotification
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.settings import SettingsModel
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class NotificationModel(BaseModel):
47 47
48 48 cls = Notification
49 49
50 50 def __get_notification(self, notification):
51 51 if isinstance(notification, Notification):
52 52 return notification
53 53 elif isinstance(notification, (int, long)):
54 54 return Notification.get(notification)
55 55 else:
56 56 if notification:
57 57 raise Exception('notification must be int, long or Instance'
58 58 ' of Notification got %s' % type(notification))
59 59
60 60 def create(
61 61 self, created_by, notification_subject, notification_body,
62 62 notification_type=Notification.TYPE_MESSAGE, recipients=None,
63 63 mention_recipients=None, with_email=True, email_kwargs=None):
64 64 """
65 65
66 66 Creates notification of given type
67 67
68 68 :param created_by: int, str or User instance. User who created this
69 69 notification
70 70 :param notification_subject: subject of notification itself
71 71 :param notification_body: body of notification text
72 72 :param notification_type: type of notification, based on that we
73 73 pick templates
74 74
75 75 :param recipients: list of int, str or User objects, when None
76 76 is given send to all admins
77 77 :param mention_recipients: list of int, str or User objects,
78 78 that were mentioned
79 79 :param with_email: send email with this notification
80 80 :param email_kwargs: dict with arguments to generate email
81 81 """
82 82
83 83 from rhodecode.lib.celerylib import tasks, run_task
84 84
85 85 if recipients and not getattr(recipients, '__iter__', False):
86 86 raise Exception('recipients must be an iterable object')
87 87
88 88 created_by_obj = self._get_user(created_by)
89 89 # default MAIN body if not given
90 90 email_kwargs = email_kwargs or {'body': notification_body}
91 91 mention_recipients = mention_recipients or set()
92 92
93 93 if not created_by_obj:
94 94 raise Exception('unknown user %s' % created_by)
95 95
96 96 if recipients is None:
97 97 # recipients is None means to all admins
98 98 recipients_objs = User.query().filter(User.admin == true()).all()
99 99 log.debug('sending notifications %s to admins: %s',
100 100 notification_type, recipients_objs)
101 101 else:
102 102 recipients_objs = []
103 103 for u in recipients:
104 104 obj = self._get_user(u)
105 105 if obj:
106 106 recipients_objs.append(obj)
107 107 else: # we didn't find this user, log the error and carry on
108 108 log.error('cannot notify unknown user %r', u)
109 109
110 110 recipients_objs = set(recipients_objs)
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 notification = Notification.create(
120 120 created_by=created_by_obj, subject=notification_subject,
121 121 body=notification_body, recipients=final_recipients,
122 122 type_=notification_type
123 123 )
124 124
125 125 if not with_email: # skip sending email, and just create notification
126 126 return notification
127 127
128 128 # don't send email to person who created this comment
129 129 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
130 130
131 131 # now notify all recipients in question
132 132
133 133 for recipient in rec_objs.union(mention_recipients):
134 134 # inject current recipient
135 135 email_kwargs['recipient'] = recipient
136 136 email_kwargs['mention'] = recipient in mention_recipients
137 137 (subject, headers, email_body,
138 138 email_body_plaintext) = EmailNotificationModel().render_email(
139 139 notification_type, **email_kwargs)
140 140
141 141 log.debug(
142 142 'Creating notification email task for user:`%s`', recipient)
143 143 task = run_task(
144 144 tasks.send_email, recipient.email, subject,
145 145 email_body_plaintext, email_body)
146 146 log.debug('Created email task: %s', task)
147 147
148 148 return notification
149 149
150 150 def delete(self, user, notification):
151 151 # we don't want to remove actual notification just the assignment
152 152 try:
153 153 notification = self.__get_notification(notification)
154 154 user = self._get_user(user)
155 155 if notification and user:
156 156 obj = UserNotification.query()\
157 157 .filter(UserNotification.user == user)\
158 158 .filter(UserNotification.notification == notification)\
159 159 .one()
160 160 Session().delete(obj)
161 161 return True
162 162 except Exception:
163 163 log.error(traceback.format_exc())
164 164 raise
165 165
166 166 def get_for_user(self, user, filter_=None):
167 167 """
168 168 Get mentions for given user, filter them if filter dict is given
169 169
170 170 :param user:
171 171 :param filter:
172 172 """
173 173 user = self._get_user(user)
174 174
175 175 q = UserNotification.query()\
176 176 .filter(UserNotification.user == user)\
177 177 .join((
178 178 Notification, UserNotification.notification_id ==
179 179 Notification.notification_id))
180 180
181 181 if filter_:
182 182 q = q.filter(Notification.type_.in_(filter_))
183 183
184 184 return q.all()
185 185
186 186 def mark_read(self, user, notification):
187 187 try:
188 188 notification = self.__get_notification(notification)
189 189 user = self._get_user(user)
190 190 if notification and user:
191 191 obj = UserNotification.query()\
192 192 .filter(UserNotification.user == user)\
193 193 .filter(UserNotification.notification == notification)\
194 194 .one()
195 195 obj.read = True
196 196 Session().add(obj)
197 197 return True
198 198 except Exception:
199 199 log.error(traceback.format_exc())
200 200 raise
201 201
202 202 def mark_all_read_for_user(self, user, filter_=None):
203 203 user = self._get_user(user)
204 204 q = UserNotification.query()\
205 205 .filter(UserNotification.user == user)\
206 206 .filter(UserNotification.read == false())\
207 207 .join((
208 208 Notification, UserNotification.notification_id ==
209 209 Notification.notification_id))
210 210 if filter_:
211 211 q = q.filter(Notification.type_.in_(filter_))
212 212
213 213 # this is a little inefficient but sqlalchemy doesn't support
214 214 # update on joined tables :(
215 215 for obj in q.all():
216 216 obj.read = True
217 217 Session().add(obj)
218 218
219 219 def get_unread_cnt_for_user(self, user):
220 220 user = self._get_user(user)
221 221 return UserNotification.query()\
222 222 .filter(UserNotification.read == false())\
223 223 .filter(UserNotification.user == user).count()
224 224
225 225 def get_unread_for_user(self, user):
226 226 user = self._get_user(user)
227 227 return [x.notification for x in UserNotification.query()
228 228 .filter(UserNotification.read == false())
229 229 .filter(UserNotification.user == user).all()]
230 230
231 231 def get_user_notification(self, user, notification):
232 232 user = self._get_user(user)
233 233 notification = self.__get_notification(notification)
234 234
235 235 return UserNotification.query()\
236 236 .filter(UserNotification.notification == notification)\
237 237 .filter(UserNotification.user == user).scalar()
238 238
239 239 def make_description(self, notification, show_age=True):
240 240 """
241 241 Creates a human readable description based on properties
242 242 of notification object
243 243 """
244 244
245 245 _map = {
246 246 notification.TYPE_CHANGESET_COMMENT: [
247 247 _('%(user)s commented on commit %(date_or_age)s'),
248 248 _('%(user)s commented on commit at %(date_or_age)s'),
249 249 ],
250 250 notification.TYPE_MESSAGE: [
251 251 _('%(user)s sent message %(date_or_age)s'),
252 252 _('%(user)s sent message at %(date_or_age)s'),
253 253 ],
254 254 notification.TYPE_MENTION: [
255 255 _('%(user)s mentioned you %(date_or_age)s'),
256 256 _('%(user)s mentioned you at %(date_or_age)s'),
257 257 ],
258 258 notification.TYPE_REGISTRATION: [
259 259 _('%(user)s registered in RhodeCode %(date_or_age)s'),
260 260 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
261 261 ],
262 262 notification.TYPE_PULL_REQUEST: [
263 263 _('%(user)s opened new pull request %(date_or_age)s'),
264 264 _('%(user)s opened new pull request at %(date_or_age)s'),
265 265 ],
266 266 notification.TYPE_PULL_REQUEST_COMMENT: [
267 267 _('%(user)s commented on pull request %(date_or_age)s'),
268 268 _('%(user)s commented on pull request at %(date_or_age)s'),
269 269 ],
270 270 }
271 271
272 272 templates = _map[notification.type_]
273 273
274 274 if show_age:
275 275 template = templates[0]
276 276 date_or_age = h.age(notification.created_on)
277 277 else:
278 278 template = templates[1]
279 279 date_or_age = h.format_date(notification.created_on)
280 280
281 281 return template % {
282 282 'user': notification.created_by_user.username,
283 283 'date_or_age': date_or_age,
284 284 }
285 285
286 286
287 287 class EmailNotificationModel(BaseModel):
288 288 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
289 289 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
290 290 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
291 291 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
292 292 TYPE_MAIN = Notification.TYPE_MESSAGE
293 293
294 294 TYPE_PASSWORD_RESET = 'password_reset'
295 295 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
296 296 TYPE_EMAIL_TEST = 'email_test'
297 297 TYPE_TEST = 'test'
298 298
299 299 email_types = {
300 300 TYPE_MAIN: 'email_templates/main.mako',
301 301 TYPE_TEST: 'email_templates/test.mako',
302 302 TYPE_EMAIL_TEST: 'email_templates/email_test.mako',
303 303 TYPE_REGISTRATION: 'email_templates/user_registration.mako',
304 304 TYPE_PASSWORD_RESET: 'email_templates/password_reset.mako',
305 305 TYPE_PASSWORD_RESET_CONFIRMATION: 'email_templates/password_reset_confirmation.mako',
306 306 TYPE_COMMIT_COMMENT: 'email_templates/commit_comment.mako',
307 307 TYPE_PULL_REQUEST: 'email_templates/pull_request_review.mako',
308 308 TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.mako',
309 309 }
310 310
311 311 def __init__(self):
312 312 """
313 313 Example usage::
314 314
315 315 (subject, headers, email_body,
316 316 email_body_plaintext) = EmailNotificationModel().render_email(
317 317 EmailNotificationModel.TYPE_TEST, **email_kwargs)
318 318
319 319 """
320 320 super(EmailNotificationModel, self).__init__()
321 321
322 322 def _update_kwargs_for_render(self, kwargs):
323 323 """
324 324 Inject params required for Mako rendering
325 325
326 326 :param kwargs:
327 327 :return:
328 328 """
329 329 _kwargs = {
330 'instance_url': h.url('home', qualified=True)
330 'instance_url': h.url('home', qualified=True),
331 'rhodecode_instance_name': getattr(c, 'rhodecode_name', '')
331 332 }
332 333 _kwargs.update(kwargs)
333 334 return _kwargs
334 335
335 336 def get_renderer(self, type_):
336 337 template_name = self.email_types[type_]
337 338 return PartialRenderer(template_name)
338 339
339 340 def render_email(self, type_, **kwargs):
340 341 """
341 342 renders template for email, and returns a tuple of
342 343 (subject, email_headers, email_html_body, email_plaintext_body)
343 344 """
344 345 # translator and helpers inject
345 346 _kwargs = self._update_kwargs_for_render(kwargs)
346 347
347 348 email_template = self.get_renderer(type_)
348 349
349 350 subject = email_template.render('subject', **_kwargs)
350 351
351 352 try:
352 353 headers = email_template.render('headers', **_kwargs)
353 354 except AttributeError:
354 355 # it's not defined in template, ok we can skip it
355 356 headers = ''
356 357
357 358 try:
358 359 body_plaintext = email_template.render('body_plaintext', **_kwargs)
359 360 except AttributeError:
360 361 # it's not defined in template, ok we can skip it
361 362 body_plaintext = ''
362 363
363 364 # render WHOLE template
364 365 body = email_template.render(None, **_kwargs)
365 366
366 367 return subject, headers, body, body_plaintext
@@ -1,106 +1,106 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 ## headers we additionally can set for email
4 4 <%def name="headers()" filter="n,trim"></%def>
5 5
6 6 <%def name="plaintext_footer()">
7 7 ${_('This is a notification from RhodeCode. %(instance_url)s') % {'instance_url': instance_url}}
8 8 </%def>
9 9
10 10 <%def name="body_plaintext()" filter="n,trim">
11 11 ## this example is not called itself but overridden in each template
12 12 ## the plaintext_footer should be at the bottom of both html and text emails
13 13 ${self.plaintext_footer()}
14 14 </%def>
15 15
16 16 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
17 17 <html xmlns="http://www.w3.org/1999/xhtml">
18 18 <head>
19 19 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
20 20 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
21 21 <title>${self.subject()}</title>
22 22 <style type="text/css">
23 23 /* Based on The MailChimp Reset INLINE: Yes. */
24 24 #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
25 25 body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;}
26 26 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
27 27 .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
28 28 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
29 29 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
30 30 #backgroundTable {margin:0; padding:0; line-height: 100% !important;}
31 31 /* End reset */
32 32
33 33 /* defaults for images*/
34 34 img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
35 35 a img {border:none;}
36 36 .image_fix {display:block;}
37 37
38 38 body {line-height:1.2em;}
39 39 p {margin: 0 0 20px;}
40 40 h1, h2, h3, h4, h5, h6 {color:#323232!important;}
41 41 a {color:#427cc9;text-decoration:none;outline:none;cursor:pointer;}
42 42 a:focus {outline:none;}
43 43 a:hover {color: #305b91;}
44 44 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color:#427cc9!important;text-decoration:none!important;}
45 45 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: #305b91!important;}
46 46 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: #305b91!important;}
47 47 table {font-size:13px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}
48 48 table td {padding:.65em 1em .65em 0;border-collapse:collapse;vertical-align:top;text-align:left;}
49 49 input {display:inline;border-radius:2px;border-style:solid;border: 1px solid #dbd9da;padding:.5em;}
50 50 input:focus {outline: 1px solid #979797}
51 51 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
52 52 /* Put your iPhone 4g styles in here */
53 53 }
54 54
55 55 /* Android targeting */
56 56 @media only screen and (-webkit-device-pixel-ratio:.75){
57 57 /* Put CSS for low density (ldpi) Android layouts in here */
58 58 }
59 59 @media only screen and (-webkit-device-pixel-ratio:1){
60 60 /* Put CSS for medium density (mdpi) Android layouts in here */
61 61 }
62 62 @media only screen and (-webkit-device-pixel-ratio:1.5){
63 63 /* Put CSS for high density (hdpi) Android layouts in here */
64 64 }
65 65 /* end Android targeting */
66 66
67 67 </style>
68 68
69 69 <!-- Targeting Windows Mobile -->
70 70 <!--[if IEMobile 7]>
71 71 <style type="text/css">
72 72
73 73 </style>
74 74 <![endif]-->
75 75
76 76 <!--[if gte mso 9]>
77 77 <style>
78 78 /* Target Outlook 2007 and 2010 */
79 79 </style>
80 80 <![endif]-->
81 81 </head>
82 82 <body>
83 83 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
84 84 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:sans-serif;font-weight:100;border:1px solid #dbd9da">
85 85 <tr>
86 86 <td valign="top" style="padding:0;">
87 87 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
88 88 <tr><td style="width:100%;padding:7px;background-color:#202020" valign="top">
89 89 <a style="width:100%;height:100%;display:block;color:#eeeeee;text-decoration:none;" href="${instance_url}">
90 90 ${_('RhodeCode')}
91 % if c.rhodecode_name:
92 - ${c.rhodecode_name}
91 % if rhodecode_instance_name:
92 - ${rhodecode_instance_name}
93 93 % endif
94 94 </a>
95 95 </td></tr>
96 96 <tr><td style="padding:15px;" valign="top">${self.body()}</td></tr>
97 97 </table>
98 98 </td>
99 99 </tr>
100 100 </table>
101 101 <!-- End of wrapper table -->
102 102 <p><a style="margin-top:15px;margin-left:1%;font-family:sans-serif;font-weight:100;font-size:11px;display:block;color:#666666;text-decoration:none;" href="${instance_url}">
103 103 ${self.plaintext_footer()}
104 104 </a></p>
105 105 </body>
106 106 </html>
@@ -1,68 +1,68 b''
1 1 import collections
2 2
3 3 import pytest
4 4
5 5 from rhodecode.lib.utils import PartialRenderer
6 6 from rhodecode.model.notification import EmailNotificationModel
7 7
8 8
9 9 def test_get_template_obj(pylonsapp):
10 10 template = EmailNotificationModel().get_renderer(
11 11 EmailNotificationModel.TYPE_TEST)
12 12 assert isinstance(template, PartialRenderer)
13 13
14 14
15 15 def test_render_email(pylonsapp):
16 16 kwargs = {}
17 17 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
18 18 EmailNotificationModel.TYPE_TEST, **kwargs)
19 19
20 20 # subject
21 21 assert subject == 'Test "Subject" hello "world"'
22 22
23 23 # headers
24 24 assert headers == 'X=Y'
25 25
26 26 # body plaintext
27 27 assert body_plaintext == 'Email Plaintext Body'
28 28
29 29 # body
30 assert '<b>This is a notification ' \
31 'from RhodeCode. http://test.example.com:80/</b>' in body
32 assert '<b>Email Body' in body
30 assert 'This is a notification ' \
31 'from RhodeCode. http://test.example.com:80/' in body
32 assert 'Email Body' in body
33 33
34 34
35 35 def test_render_pr_email(pylonsapp, user_admin):
36 36
37 37 ref = collections.namedtuple('Ref',
38 38 'name, type')(
39 39 'fxies123', 'book'
40 40 )
41 41
42 42 pr = collections.namedtuple('PullRequest',
43 43 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
44 44 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
45 45
46 46 source_repo = target_repo = collections.namedtuple('Repo',
47 47 'type, repo_name')(
48 48 'hg', 'pull_request_1')
49 49
50 50 kwargs = {
51 51 'user': '<marcin@rhodecode.com> Marcin Kuzminski',
52 52 'pull_request': pr,
53 53 'pull_request_commits': [],
54 54
55 55 'pull_request_target_repo': target_repo,
56 56 'pull_request_target_repo_url': 'x',
57 57
58 58 'pull_request_source_repo': source_repo,
59 59 'pull_request_source_repo_url': 'x',
60 60
61 61 'pull_request_url': 'http://localhost/pr1',
62 62 }
63 63
64 64 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
65 65 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
66 66
67 67 # subject
68 68 assert subject == 'Marcin Kuzminski wants you to review pull request #200: "Example Pull Request"'
General Comments 0
You need to be logged in to leave comments. Login now