##// END OF EJS Templates
emails: fixed newlines in email templates that can break email sending code.
marcink -
r1728:49fb0cec default
parent child Browse files
Show More
@@ -1,374 +1,379 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 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.i18n.translation import _, ungettext
31 31 from sqlalchemy.sql.expression import false, true
32 32 from mako import exceptions
33 33
34 34 import rhodecode
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.utils import PartialRenderer
37 37 from rhodecode.model import BaseModel
38 38 from rhodecode.model.db import Notification, User, UserNotification
39 39 from rhodecode.model.meta import Session
40 40 from rhodecode.model.settings import SettingsModel
41 41 from rhodecode.translation import TranslationString
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, translate=None):
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 if translate:
278 278 date_or_age = translate(date_or_age)
279 279
280 280 if isinstance(date_or_age, TranslationString):
281 281 date_or_age = date_or_age.interpolate()
282 282
283 283 else:
284 284 template = templates[1]
285 285 date_or_age = h.format_date(notification.created_on)
286 286
287 287 return template % {
288 288 'user': notification.created_by_user.username,
289 289 'date_or_age': date_or_age,
290 290 }
291 291
292 292
293 293 class EmailNotificationModel(BaseModel):
294 294 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
295 295 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
296 296 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
297 297 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
298 298 TYPE_MAIN = Notification.TYPE_MESSAGE
299 299
300 300 TYPE_PASSWORD_RESET = 'password_reset'
301 301 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
302 302 TYPE_EMAIL_TEST = 'email_test'
303 303 TYPE_TEST = 'test'
304 304
305 305 email_types = {
306 306 TYPE_MAIN: 'email_templates/main.mako',
307 307 TYPE_TEST: 'email_templates/test.mako',
308 308 TYPE_EMAIL_TEST: 'email_templates/email_test.mako',
309 309 TYPE_REGISTRATION: 'email_templates/user_registration.mako',
310 310 TYPE_PASSWORD_RESET: 'email_templates/password_reset.mako',
311 311 TYPE_PASSWORD_RESET_CONFIRMATION: 'email_templates/password_reset_confirmation.mako',
312 312 TYPE_COMMIT_COMMENT: 'email_templates/commit_comment.mako',
313 313 TYPE_PULL_REQUEST: 'email_templates/pull_request_review.mako',
314 314 TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.mako',
315 315 }
316 316
317 317 def __init__(self):
318 318 """
319 319 Example usage::
320 320
321 321 (subject, headers, email_body,
322 322 email_body_plaintext) = EmailNotificationModel().render_email(
323 323 EmailNotificationModel.TYPE_TEST, **email_kwargs)
324 324
325 325 """
326 326 super(EmailNotificationModel, self).__init__()
327 327 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
328 328
329 329 def _update_kwargs_for_render(self, kwargs):
330 330 """
331 331 Inject params required for Mako rendering
332 332
333 333 :param kwargs:
334 334 """
335
335 336 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
336 337
337 338 _kwargs = {
338 339 'instance_url': h.url('home', qualified=True),
340 'whitespace_filter': self.whitespace_filter
339 341 }
340 342 _kwargs.update(kwargs)
341 343 return _kwargs
342 344
345 def whitespace_filter(self, text):
346 return text.replace('\n', '').replace('\t', '')
347
343 348 def get_renderer(self, type_):
344 349 template_name = self.email_types[type_]
345 350 return PartialRenderer(template_name)
346 351
347 352 def render_email(self, type_, **kwargs):
348 353 """
349 354 renders template for email, and returns a tuple of
350 355 (subject, email_headers, email_html_body, email_plaintext_body)
351 356 """
352 357 # translator and helpers inject
353 358 _kwargs = self._update_kwargs_for_render(kwargs)
354 359
355 360 email_template = self.get_renderer(type_)
356 361
357 362 subject = email_template.render('subject', **_kwargs)
358 363
359 364 try:
360 365 headers = email_template.render('headers', **_kwargs)
361 366 except AttributeError:
362 367 # it's not defined in template, ok we can skip it
363 368 headers = ''
364 369
365 370 try:
366 371 body_plaintext = email_template.render('body_plaintext', **_kwargs)
367 372 except AttributeError:
368 373 # it's not defined in template, ok we can skip it
369 374 body_plaintext = ''
370 375
371 376 # render WHOLE template
372 377 body = email_template.render(None, **_kwargs)
373 378
374 379 return subject, headers, body, body_plaintext
@@ -1,105 +1,105 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 7 <%
8 8 data = {
9 9 'user': h.person(user),
10 10 'repo_name': repo_name,
11 11 'commit_id': h.show_id(commit),
12 12 'status': status_change,
13 13 'comment_file': comment_file,
14 14 'comment_line': comment_line,
15 15 'comment_type': comment_type,
16 16 }
17 17 %>
18 18 ${_('[mention]') if mention else ''} \
19 19
20 20 % if comment_file:
21 21 ${_('%(user)s left %(comment_type)s on commit `%(commit_id)s` (file: `%(comment_file)s`)') % data} ${_('in the %(repo_name)s repository') % data |n}
22 22 % else:
23 23 % if status_change:
24 24 ${_('%(user)s left %(comment_type)s on commit `%(commit_id)s` (status: %(status)s)') % data |n} ${_('in the %(repo_name)s repository') % data |n}
25 25 % else:
26 26 ${_('%(user)s left %(comment_type)s on commit `%(commit_id)s`') % data |n} ${_('in the %(repo_name)s repository') % data |n}
27 27 % endif
28 28 % endif
29 29
30 30 </%def>
31 31
32 32 ## PLAINTEXT VERSION OF BODY
33 33 <%def name="body_plaintext()" filter="n,trim">
34 34 <%
35 35 data = {
36 36 'user': h.person(user),
37 37 'repo_name': repo_name,
38 38 'commit_id': h.show_id(commit),
39 39 'status': status_change,
40 40 'comment_file': comment_file,
41 41 'comment_line': comment_line,
42 42 'comment_type': comment_type,
43 43 }
44 44 %>
45 45 ${self.subject()}
46 46
47 47 * ${_('Comment link')}: ${commit_comment_url}
48 48
49 49 * ${_('Commit')}: ${h.show_id(commit)}
50 50
51 51 %if comment_file:
52 52 * ${_('File: %(comment_file)s on line %(comment_line)s') % data}
53 53 %endif
54 54
55 55 ---
56 56
57 57 %if status_change:
58 58 ${_('Commit status was changed to')}: *${status_change}*
59 59 %endif
60 60
61 61 ${comment_body|n}
62 62
63 63 ${self.plaintext_footer()}
64 64 </%def>
65 65
66 66
67 67 <%
68 68 data = {
69 69 'user': h.person(user),
70 70 'repo': commit_target_repo,
71 71 'repo_name': repo_name,
72 72 'commit_id': h.show_id(commit),
73 73 'comment_file': comment_file,
74 74 'comment_line': comment_line,
75 75 'comment_type': comment_type,
76 76 }
77 77 %>
78 78 <table style="text-align:left;vertical-align:middle;">
79 79 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
80 80
81 81 % if comment_file:
82 82 <h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on commit `%(commit_id)s` (file:`%(comment_file)s`)') % data}</a> ${_('in the %(repo)s repository') % data |n}</h4>
83 83 % else:
84 84 <h4><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on commit `%(commit_id)s`') % data |n}</a> ${_('in the %(repo)s repository') % data |n}</h4>
85 85 % endif
86 86 </td></tr>
87 87
88 88 <tr><td style="padding-right:20px;padding-top:15px;">${_('Commit')}</td><td style="padding-top:15px;"><a href="${commit_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${h.show_id(commit)}</a></td></tr>
89 89 <tr><td style="padding-right:20px;">${_('Description')}</td><td style="white-space:pre-wrap">${h.urlify_commit_message(commit.message, repo_name)}</td></tr>
90 90
91 91 % if status_change:
92 92 <tr><td style="padding-right:20px;">${_('Status')}</td>
93 93 <td>${_('The commit status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}</td>
94 94 </tr>
95 95 % endif
96 96 <tr>
97 97 <td style="padding-right:20px;">
98 98 % if comment_type == 'todo':
99 99 ${(_('TODO comment on line: %(comment_line)s') if comment_file else _('TODO comment')) % data}
100 100 % else:
101 101 ${(_('Note comment on line: %(comment_line)s') if comment_file else _('Note comment')) % data}
102 102 % endif
103 103 </td>
104 104 <td style="line-height:1.2em;white-space:pre-wrap">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td></tr>
105 105 </table>
@@ -1,13 +1,13 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3
4 <%def name="subject()" filter="n,trim">
4 <%def name="subject()" filter="n,trim,whitespace_filter">
5 5 RhodeCode test email: ${h.format_date(date)}
6 6 </%def>
7 7
8 8 ## plain text version of the email. Empty by default
9 9 <%def name="body_plaintext()" filter="n,trim">
10 10 Test Email from RhodeCode version: ${rhodecode_version}, sent by: ${user}
11 11 </%def>
12 12
13 13 ${body_plaintext()} No newline at end of file
@@ -1,21 +1,21 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3
4 <%def name="subject()" filter="n,trim">
4 <%def name="subject()" filter="n,trim,whitespace_filter">
5 5 </%def>
6 6
7 7
8 8 ## plain text version of the email. Empty by default
9 9 <%def name="body_plaintext()" filter="n,trim">
10 10 ${body}
11 11
12 12 ${self.plaintext_footer()}
13 13 </%def>
14 14
15 15 ## BODY GOES BELOW
16 16 <table style="text-align:left;vertical-align:top;">
17 17 <tr><td style="padding-right:20px;padding-top:15px;white-space:pre-wrap">${body}</td></tr>
18 18 </table>
19 19 <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}">
20 20 ${self.plaintext_footer()}
21 21 </a></p> No newline at end of file
@@ -1,33 +1,33 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3
4 <%def name="subject()" filter="n,trim">
4 <%def name="subject()" filter="n,trim,whitespace_filter">
5 5 RhodeCode Password reset
6 6 </%def>
7 7
8 8 ## plain text version of the email. Empty by default
9 9 <%def name="body_plaintext()" filter="n,trim">
10 10 Hi ${user.username},
11 11
12 12 There was a request to reset your password using the email address ${email} on ${h.format_date(date)}
13 13
14 14 *If you didn't do this, please contact your RhodeCode administrator.*
15 15
16 16 You can continue, and generate new password by clicking following URL:
17 17 ${password_reset_url}
18 18
19 19 This link will be active for 10 minutes.
20 20 ${self.plaintext_footer()}
21 21 </%def>
22 22
23 23 ## BODY GOES BELOW
24 24 <p>
25 25 Hello ${user.username},
26 26 </p><p>
27 27 There was a request to reset your password using the email address ${email} on ${h.format_date(date)}
28 28 <br/>
29 29 <strong>If you did not request a password reset, please contact your RhodeCode administrator.</strong>
30 30 </p><p>
31 31 <a href="${password_reset_url}">${_('Generate new password here')}.</a>
32 32 This link will be active for 10 minutes.
33 33 </p>
@@ -1,29 +1,29 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3
4 <%def name="subject()" filter="n,trim">
4 <%def name="subject()" filter="n,trim,whitespace_filter">
5 5 Your new RhodeCode password
6 6 </%def>
7 7
8 8 ## plain text version of the email. Empty by default
9 9 <%def name="body_plaintext()" filter="n,trim">
10 10 Hi ${user.username},
11 11
12 12 Below is your new access password for RhodeCode.
13 13
14 14 *If you didn't do this, please contact your RhodeCode administrator.*
15 15
16 16 password: ${new_password}
17 17
18 18 ${self.plaintext_footer()}
19 19 </%def>
20 20
21 21 ## BODY GOES BELOW
22 22 <p>
23 23 Hello ${user.username},
24 24 </p><p>
25 25 Below is your new access password for RhodeCode.
26 26 <br/>
27 27 <strong>If you didn't request a new password, please contact your RhodeCode administrator.</strong>
28 28 </p>
29 29 <p>password: <pre>${new_password}</pre>
@@ -1,114 +1,114 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 7 <%
8 8 data = {
9 9 'user': h.person(user),
10 10 'pr_title': pull_request.title,
11 11 'pr_id': pull_request.pull_request_id,
12 12 'status': status_change,
13 13 'comment_file': comment_file,
14 14 'comment_line': comment_line,
15 15 'comment_type': comment_type,
16 16 }
17 17 %>
18 18
19 19 ${_('[mention]') if mention else ''} \
20 20
21 21 % if comment_file:
22 22 ${_('%(user)s left %(comment_type)s on pull request #%(pr_id)s "%(pr_title)s" (file: `%(comment_file)s`)') % data |n}
23 23 % else:
24 24 % if status_change:
25 25 ${_('%(user)s left %(comment_type)s on pull request #%(pr_id)s "%(pr_title)s" (status: %(status)s)') % data |n}
26 26 % else:
27 27 ${_('%(user)s left %(comment_type)s on pull request #%(pr_id)s "%(pr_title)s"') % data |n}
28 28 % endif
29 29 % endif
30 30 </%def>
31 31
32 32 ## PLAINTEXT VERSION OF BODY
33 33 <%def name="body_plaintext()" filter="n,trim">
34 34 <%
35 35 data = {
36 36 'user': h.person(user),
37 37 'pr_title': pull_request.title,
38 38 'pr_id': pull_request.pull_request_id,
39 39 'status': status_change,
40 40 'comment_file': comment_file,
41 41 'comment_line': comment_line,
42 42 'comment_type': comment_type,
43 43 }
44 44 %>
45 45 ${self.subject()}
46 46
47 47 * ${_('Comment link')}: ${pr_comment_url}
48 48
49 49 * ${_('Source repository')}: ${pr_source_repo_url}
50 50
51 51 %if comment_file:
52 52 * ${_('File: %(comment_file)s on line %(comment_line)s') % {'comment_file': comment_file, 'comment_line': comment_line}}
53 53 %endif
54 54
55 55 ---
56 56
57 57 %if status_change and not closing_pr:
58 58 ${_('%(user)s submitted pull request #%(pr_id)s status: *%(status)s*') % data}
59 59 %elif status_change and closing_pr:
60 60 ${_('%(user)s submitted pull request #%(pr_id)s status: *%(status)s and closed*') % data}
61 61 %endif
62 62
63 63 ${comment_body|n}
64 64
65 65 ${self.plaintext_footer()}
66 66 </%def>
67 67
68 68
69 69 <%
70 70 data = {
71 71 'user': h.person(user),
72 72 'pr_title': pull_request.title,
73 73 'pr_id': pull_request.pull_request_id,
74 74 'status': status_change,
75 75 'comment_file': comment_file,
76 76 'comment_line': comment_line,
77 77 'comment_type': comment_type,
78 78 }
79 79 %>
80 80 <table style="text-align:left;vertical-align:middle;">
81 81 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;">
82 82
83 83 % if comment_file:
84 84 <h4><a href="${pr_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s" (file:`%(comment_file)s`)') % data |n}</a></h4>
85 85 % else:
86 86 <h4><a href="${pr_comment_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s commented on pull request #%(pr_id)s "%(pr_title)s"') % data |n}</a></h4>
87 87 % endif
88 88
89 89 </td></tr>
90 90 <tr><td style="padding-right:20px;padding-top:15px;">${_('Source')}</td><td style="padding-top:15px;"><a style="color:#427cc9;text-decoration:none;cursor:pointer" href="${pr_source_repo_url}">${pr_source_repo.repo_name}</a></td></tr>
91 91
92 92 % if status_change:
93 93 <tr>
94 94 <td style="padding-right:20px;">${_('Status')}</td>
95 95 <td>
96 96 % if closing_pr:
97 97 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
98 98 % else:
99 99 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
100 100 % endif
101 101 </td>
102 102 </tr>
103 103 % endif
104 104 <tr>
105 105 <td style="padding-right:20px;">
106 106 % if comment_type == 'todo':
107 107 ${(_('TODO comment on line: %(comment_line)s') if comment_file else _('TODO comment')) % data}
108 108 % else:
109 109 ${(_('Note comment on line: %(comment_line)s') if comment_file else _('Note comment')) % data}
110 110 % endif
111 111 </td>
112 112 <td style="line-height:1.2em;white-space:pre-wrap">${h.render(comment_body, renderer=renderer_type, mentions=True)}</td>
113 113 </tr>
114 114 </table>
@@ -1,85 +1,85 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3 <%namespace name="base" file="base.mako"/>
4 4
5 <%def name="subject()" filter="n,trim">
5 <%def name="subject()" filter="n,trim,whitespace_filter">
6 6 <%
7 7 data = {
8 8 'user': h.person(user),
9 9 'pr_id': pull_request.pull_request_id,
10 10 'pr_title': pull_request.title,
11 11 }
12 12 %>
13 13
14 14 ${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s"') % data |n}
15 15 </%def>
16 16
17 17
18 18 <%def name="body_plaintext()" filter="n,trim">
19 19 <%
20 20 data = {
21 21 'user': h.person(user),
22 22 'pr_id': pull_request.pull_request_id,
23 23 'pr_title': pull_request.title,
24 24 'source_ref_type': pull_request.source_ref_parts.type,
25 25 'source_ref_name': pull_request.source_ref_parts.name,
26 26 'target_ref_type': pull_request.target_ref_parts.type,
27 27 'target_ref_name': pull_request.target_ref_parts.name,
28 28 'repo_url': pull_request_source_repo_url
29 29 }
30 30 %>
31 31 ${self.subject()}
32 32
33 33
34 34 ${h.literal(_('Pull request from %(source_ref_type)s:%(source_ref_name)s of %(repo_url)s into %(target_ref_type)s:%(target_ref_name)s') % data)}
35 35
36 36
37 37 * ${_('Link')}: ${pull_request_url}
38 38
39 39 * ${_('Title')}: ${pull_request.title}
40 40
41 41 * ${_('Description')}:
42 42
43 43 ${pull_request.description}
44 44
45 45
46 46 * ${ungettext('Commit (%(num)s)', 'Commits (%(num)s)', len(pull_request_commits) ) % {'num': len(pull_request_commits)}}:
47 47
48 48 % for commit_id, message in pull_request_commits:
49 49 - ${h.short_id(commit_id)}
50 50 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
51 51
52 52 % endfor
53 53
54 54 ${self.plaintext_footer()}
55 55 </%def>
56 56 <%
57 57 data = {
58 58 'user': h.person(user),
59 59 'pr_id': pull_request.pull_request_id,
60 60 'pr_title': pull_request.title,
61 61 'source_ref_type': pull_request.source_ref_parts.type,
62 62 'source_ref_name': pull_request.source_ref_parts.name,
63 63 'target_ref_type': pull_request.target_ref_parts.type,
64 64 'target_ref_name': pull_request.target_ref_parts.name,
65 65 'repo_url': pull_request_source_repo_url,
66 66 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
67 67 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url)
68 68 }
69 69 %>
70 70 <table style="text-align:left;vertical-align:middle;">
71 71 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;"><h4><a href="${pull_request_url}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('%(user)s wants you to review pull request #%(pr_id)s: "%(pr_title)s".') % data }</a></h4></td></tr>
72 72 <tr><td style="padding-right:20px;padding-top:15px;">${_('Title')}</td><td style="padding-top:15px;">${pull_request.title}</td></tr>
73 73 <tr><td style="padding-right:20px;">${_('Source')}</td><td>${base.tag_button(pull_request.source_ref_parts.name)} ${h.literal(_('%(source_ref_type)s of %(source_repo_url)s') % data)}</td></tr>
74 74 <tr><td style="padding-right:20px;">${_('Target')}</td><td>${base.tag_button(pull_request.target_ref_parts.name)} ${h.literal(_('%(target_ref_type)s of %(target_repo_url)s') % data)}</td></tr>
75 75 <tr><td style="padding-right:20px;">${_('Description')}</td><td style="white-space:pre-wrap">${pull_request.description}</td></tr>
76 76 <tr><td style="padding-right:20px;">${ungettext('%(num)s Commit', '%(num)s Commits', len(pull_request_commits)) % {'num': len(pull_request_commits)}}</td>
77 77 <td><ol style="margin:0 0 0 1em;padding:0;text-align:left;">
78 78 % for commit_id, message in pull_request_commits:
79 79 <li style="margin:0 0 1em;"><pre style="margin:0 0 .5em">${h.short_id(commit_id)}</pre>
80 80 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
81 81 </li>
82 82 % endfor
83 83 </ol></td>
84 84 </tr>
85 85 </table>
@@ -1,21 +1,21 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3
4 <%def name="subject()" filter="n,trim">
4 <%def name="subject()" filter="n,trim,whitespace_filter">
5 5 Test "Subject" ${_('hello "world"')|n}
6 6 </%def>
7 7
8 8 <%def name="headers()" filter="n,trim">
9 9 X=Y
10 10 </%def>
11 11
12 12 ## plain text version of the email. Empty by default
13 13 <%def name="body_plaintext()" filter="n,trim">
14 14 Email Plaintext Body
15 15 </%def>
16 16
17 17 ## BODY GOES BELOW
18 18 <b>Email Body</b>
19 19
20 20 ${h.short_id('0' * 40)}
21 21 ${_('Translation')} No newline at end of file
@@ -1,27 +1,27 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.mako"/>
3 3
4 <%def name="subject()" filter="n,trim">
4 <%def name="subject()" filter="n,trim,whitespace_filter">
5 5 RhodeCode new user registration: ${user.username}
6 6 </%def>
7 7
8 8 <%def name="body_plaintext()" filter="n,trim">
9 9
10 10 A new user `${user.username}` has registered on ${h.format_date(date)}
11 11
12 12 - Username: ${user.username}
13 13 - Full Name: ${user.firstname} ${user.lastname}
14 14 - Email: ${user.email}
15 15 - Profile link: ${h.route_path('user_profile', username=user.username, qualified=True)}
16 16
17 17 ${self.plaintext_footer()}
18 18 </%def>
19 19
20 20 ## BODY GOES BELOW
21 21 <table style="text-align:left;vertical-align:middle;">
22 22 <tr><td colspan="2" style="width:100%;padding-bottom:15px;border-bottom:1px solid #dbd9da;"><h4><a href="${h.route_url('user_profile', username=user.username)}" style="color:#427cc9;text-decoration:none;cursor:pointer">${_('New user %(user)s has registered on %(date)s') % {'user': user.username, 'date': h.format_date(date)}}</a></h4></td></tr>
23 23 <tr><td style="padding-right:20px;padding-top:20px;">${_('Username')}</td><td style="line-height:1;padding-top:20px;"><img style="margin-bottom:-5px;text-align:left;border:1px solid #dbd9da" src="${h.gravatar_url(user.email, 16)}" height="16" width="16">&nbsp;${user.username}</td></tr>
24 24 <tr><td style="padding-right:20px;">${_('Full Name')}</td><td>${user.firstname} ${user.lastname}</td></tr>
25 25 <tr><td style="padding-right:20px;">${_('Email')}</td><td>${user.email}</td></tr>
26 26 <tr><td style="padding-right:20px;">${_('Profile')}</td><td><a href="${h.route_url('user_profile', username=user.username)}">${h.route_url('user_profile', username=user.username)}</a></td></tr>
27 27 </table> No newline at end of file
@@ -1,68 +1,123 b''
1 1 import collections
2 2
3 3 import pytest
4 4
5 5 from rhodecode.lib.utils import PartialRenderer
6 from rhodecode.lib.utils2 import AttributeDict
6 7 from rhodecode.model.notification import EmailNotificationModel
7 8
8 9
9 10 def test_get_template_obj(pylonsapp):
10 11 template = EmailNotificationModel().get_renderer(
11 12 EmailNotificationModel.TYPE_TEST)
12 13 assert isinstance(template, PartialRenderer)
13 14
14 15
15 16 def test_render_email(pylonsapp):
16 17 kwargs = {}
17 18 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
18 19 EmailNotificationModel.TYPE_TEST, **kwargs)
19 20
20 21 # subject
21 22 assert subject == 'Test "Subject" hello "world"'
22 23
23 24 # headers
24 25 assert headers == 'X=Y'
25 26
26 27 # body plaintext
27 28 assert body_plaintext == 'Email Plaintext Body'
28 29
29 30 # body
30 31 assert 'This is a notification ' \
31 32 'from RhodeCode. http://test.example.com:80/' in body
32 33 assert 'Email Body' in body
33 34
34 35
35 36 def test_render_pr_email(pylonsapp, user_admin):
36 37
37 38 ref = collections.namedtuple('Ref',
38 39 'name, type')(
39 40 'fxies123', 'book'
40 41 )
41 42
42 43 pr = collections.namedtuple('PullRequest',
43 44 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
44 45 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
45 46
46 47 source_repo = target_repo = collections.namedtuple('Repo',
47 48 'type, repo_name')(
48 49 'hg', 'pull_request_1')
49 50
50 51 kwargs = {
51 52 'user': '<marcin@rhodecode.com> Marcin Kuzminski',
52 53 'pull_request': pr,
53 54 'pull_request_commits': [],
54 55
55 56 'pull_request_target_repo': target_repo,
56 57 'pull_request_target_repo_url': 'x',
57 58
58 59 'pull_request_source_repo': source_repo,
59 60 'pull_request_source_repo_url': 'x',
60 61
61 62 'pull_request_url': 'http://localhost/pr1',
62 63 }
63 64
64 65 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
65 66 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
66 67
67 68 # subject
68 69 assert subject == 'Marcin Kuzminski wants you to review pull request #200: "Example Pull Request"'
70
71
72 @pytest.mark.parametrize('mention', [
73 True,
74 False
75 ])
76 @pytest.mark.parametrize('email_type', [
77 EmailNotificationModel.TYPE_COMMIT_COMMENT,
78 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
79 ])
80 def test_render_comment_subject_no_newlines(pylonsapp, mention, email_type):
81 ref = collections.namedtuple('Ref',
82 'name, type')(
83 'fxies123', 'book'
84 )
85
86 pr = collections.namedtuple('PullRequest',
87 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
88 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
89
90 source_repo = target_repo = collections.namedtuple('Repo',
91 'type, repo_name')(
92 'hg', 'pull_request_1')
93
94 kwargs = {
95 'user': '<marcin@rhodecode.com> Marcin Kuzminski',
96 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
97 'status_change': 'approved',
98 'commit_target_repo': AttributeDict(),
99 'repo_name': 'test-repo',
100 'comment_file': 'test-file.py',
101 'comment_line': 'n100',
102 'comment_type': 'note',
103 'commit_comment_url': 'http://comment-url',
104 'instance_url': 'http://rc-instance',
105 'comment_body': 'hello world',
106 'mention': mention,
107
108 'pr_comment_url': 'http://comment-url',
109 'pr_source_repo': AttributeDict(repo_name='foobar'),
110 'pr_source_repo_url': 'http://soirce-repo/url',
111 'pull_request': pr,
112 'pull_request_commits': [],
113
114 'pull_request_target_repo': target_repo,
115 'pull_request_target_repo_url': 'x',
116
117 'pull_request_source_repo': source_repo,
118 'pull_request_source_repo_url': 'x',
119 }
120 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
121 email_type, **kwargs)
122
123 assert '\n' not in subject
General Comments 0
You need to be logged in to leave comments. Login now