##// END OF EJS Templates
automation: enabled automated check for new versions.
milka -
r4634:836f1886 stable
parent child Browse files
Show More
@@ -0,0 +1,32 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
3
4 <%def name="subject()" filter="n,trim,whitespace_filter">
5 New Version of RhodeCode is available !
6 </%def>
7
8 ## plain text version of the email. Empty by default
9 <%def name="body_plaintext()" filter="n,trim">
10 A new version of RhodeCode is available!
11
12 Your version: ${current_ver}
13 New version: ${latest_ver}
14
15 Release notes:
16
17 https://docs.rhodecode.com/RhodeCode-Enterprise/release-notes/release-notes-${latest_ver}.html
18 </%def>
19
20 ## BODY GOES BELOW
21
22 <h3>A new version of RhodeCode is available!</h3>
23 <br/>
24 Your version: ${current_ver}<br/>
25 New version: <strong>${latest_ver}</strong><br/>
26
27 <h4>Release notes</h4>
28
29 <a href="https://docs.rhodecode.com/RhodeCode-Enterprise/release-notes/release-notes-${latest_ver}.html">
30 https://docs.rhodecode.com/RhodeCode-Enterprise/release-notes/release-notes-${latest_ver}.html
31 </a>
32
@@ -1,471 +1,476 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 import os
22 22 import logging
23 23 import datetime
24 24
25 25 from pyramid.renderers import render_to_response
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.lib.celerylib import run_task, tasks
28 28 from rhodecode.lib.utils2 import AttributeDict
29 29 from rhodecode.model.db import User
30 30 from rhodecode.model.notification import EmailNotificationModel
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 class DebugStyleView(BaseAppView):
36 36
37 37 def load_default_context(self):
38 38 c = self._get_local_tmpl_context()
39 39 return c
40 40
41 41 def index(self):
42 42 c = self.load_default_context()
43 43 c.active = 'index'
44 44
45 45 return render_to_response(
46 46 'debug_style/index.html', self._get_template_context(c),
47 47 request=self.request)
48 48
49 49 def render_email(self):
50 50 c = self.load_default_context()
51 51 email_id = self.request.matchdict['email_id']
52 52 c.active = 'emails'
53 53
54 54 pr = AttributeDict(
55 55 pull_request_id=123,
56 56 title='digital_ocean: fix redis, elastic search start on boot, '
57 57 'fix fd limits on supervisor, set postgres 11 version',
58 58 description='''
59 59 Check if we should use full-topic or mini-topic.
60 60
61 61 - full topic produces some problems with merge states etc
62 62 - server-mini-topic needs probably tweeks.
63 63 ''',
64 64 repo_name='foobar',
65 65 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
66 66 target_ref_parts=AttributeDict(type='branch', name='master'),
67 67 )
68 68
69 69 target_repo = AttributeDict(repo_name='repo_group/target_repo')
70 70 source_repo = AttributeDict(repo_name='repo_group/source_repo')
71 71 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
72 72 # file/commit changes for PR update
73 73 commit_changes = AttributeDict({
74 74 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
75 75 'removed': ['eeeeeeeeeee'],
76 76 })
77 77
78 78 file_changes = AttributeDict({
79 79 'added': ['a/file1.md', 'file2.py'],
80 80 'modified': ['b/modified_file.rst'],
81 81 'removed': ['.idea'],
82 82 })
83 83
84 84 exc_traceback = {
85 85 'exc_utc_date': '2020-03-26T12:54:50.683281',
86 86 'exc_id': 139638856342656,
87 87 'exc_timestamp': '1585227290.683288',
88 88 'version': 'v1',
89 89 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
90 90 'exc_type': 'AttributeError'
91 91 }
92 92
93 93 email_kwargs = {
94 94 'test': {},
95 95
96 96 'message': {
97 97 'body': 'message body !'
98 98 },
99 99
100 100 'email_test': {
101 101 'user': user,
102 102 'date': datetime.datetime.now(),
103 103 },
104 104
105 'update_available': {
106 'current_ver': '4.23.0',
107 'latest_ver': '4.24.0',
108 },
109
105 110 'exception': {
106 111 'email_prefix': '[RHODECODE ERROR]',
107 112 'exc_id': exc_traceback['exc_id'],
108 113 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
109 114 'exc_type_name': 'NameError',
110 115 'exc_traceback': exc_traceback,
111 116 },
112 117
113 118 'password_reset': {
114 119 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
115 120
116 121 'user': user,
117 122 'date': datetime.datetime.now(),
118 123 'email': 'test@rhodecode.com',
119 124 'first_admin_email': User.get_first_super_admin().email
120 125 },
121 126
122 127 'password_reset_confirmation': {
123 128 'new_password': 'new-password-example',
124 129 'user': user,
125 130 'date': datetime.datetime.now(),
126 131 'email': 'test@rhodecode.com',
127 132 'first_admin_email': User.get_first_super_admin().email
128 133 },
129 134
130 135 'registration': {
131 136 'user': user,
132 137 'date': datetime.datetime.now(),
133 138 },
134 139
135 140 'pull_request_comment': {
136 141 'user': user,
137 142
138 143 'status_change': None,
139 144 'status_change_type': None,
140 145
141 146 'pull_request': pr,
142 147 'pull_request_commits': [],
143 148
144 149 'pull_request_target_repo': target_repo,
145 150 'pull_request_target_repo_url': 'http://target-repo/url',
146 151
147 152 'pull_request_source_repo': source_repo,
148 153 'pull_request_source_repo_url': 'http://source-repo/url',
149 154
150 155 'pull_request_url': 'http://localhost/pr1',
151 156 'pr_comment_url': 'http://comment-url',
152 157 'pr_comment_reply_url': 'http://comment-url#reply',
153 158
154 159 'comment_file': None,
155 160 'comment_line': None,
156 161 'comment_type': 'note',
157 162 'comment_body': 'This is my comment body. *I like !*',
158 163 'comment_id': 2048,
159 164 'renderer_type': 'markdown',
160 165 'mention': True,
161 166
162 167 },
163 168
164 169 'pull_request_comment+status': {
165 170 'user': user,
166 171
167 172 'status_change': 'approved',
168 173 'status_change_type': 'approved',
169 174
170 175 'pull_request': pr,
171 176 'pull_request_commits': [],
172 177
173 178 'pull_request_target_repo': target_repo,
174 179 'pull_request_target_repo_url': 'http://target-repo/url',
175 180
176 181 'pull_request_source_repo': source_repo,
177 182 'pull_request_source_repo_url': 'http://source-repo/url',
178 183
179 184 'pull_request_url': 'http://localhost/pr1',
180 185 'pr_comment_url': 'http://comment-url',
181 186 'pr_comment_reply_url': 'http://comment-url#reply',
182 187
183 188 'comment_type': 'todo',
184 189 'comment_file': None,
185 190 'comment_line': None,
186 191 'comment_body': '''
187 192 I think something like this would be better
188 193
189 194 ```py
190 195 // markdown renderer
191 196
192 197 def db():
193 198 global connection
194 199 return connection
195 200
196 201 ```
197 202
198 203 ''',
199 204 'comment_id': 2048,
200 205 'renderer_type': 'markdown',
201 206 'mention': True,
202 207
203 208 },
204 209
205 210 'pull_request_comment+file': {
206 211 'user': user,
207 212
208 213 'status_change': None,
209 214 'status_change_type': None,
210 215
211 216 'pull_request': pr,
212 217 'pull_request_commits': [],
213 218
214 219 'pull_request_target_repo': target_repo,
215 220 'pull_request_target_repo_url': 'http://target-repo/url',
216 221
217 222 'pull_request_source_repo': source_repo,
218 223 'pull_request_source_repo_url': 'http://source-repo/url',
219 224
220 225 'pull_request_url': 'http://localhost/pr1',
221 226
222 227 'pr_comment_url': 'http://comment-url',
223 228 'pr_comment_reply_url': 'http://comment-url#reply',
224 229
225 230 'comment_file': 'rhodecode/model/get_flow_commits',
226 231 'comment_line': 'o1210',
227 232 'comment_type': 'todo',
228 233 'comment_body': '''
229 234 I like this !
230 235
231 236 But please check this code
232 237
233 238 .. code-block:: javascript
234 239
235 240 // THIS IS RST CODE
236 241
237 242 this.createResolutionComment = function(commentId) {
238 243 // hide the trigger text
239 244 $('#resolve-comment-{0}'.format(commentId)).hide();
240 245
241 246 var comment = $('#comment-'+commentId);
242 247 var commentData = comment.data();
243 248 if (commentData.commentInline) {
244 249 this.createComment(comment, f_path, line_no, commentId)
245 250 } else {
246 251 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
247 252 }
248 253
249 254 return false;
250 255 };
251 256
252 257 This should work better !
253 258 ''',
254 259 'comment_id': 2048,
255 260 'renderer_type': 'rst',
256 261 'mention': True,
257 262
258 263 },
259 264
260 265 'pull_request_update': {
261 266 'updating_user': user,
262 267
263 268 'status_change': None,
264 269 'status_change_type': None,
265 270
266 271 'pull_request': pr,
267 272 'pull_request_commits': [],
268 273
269 274 'pull_request_target_repo': target_repo,
270 275 'pull_request_target_repo_url': 'http://target-repo/url',
271 276
272 277 'pull_request_source_repo': source_repo,
273 278 'pull_request_source_repo_url': 'http://source-repo/url',
274 279
275 280 'pull_request_url': 'http://localhost/pr1',
276 281
277 282 # update comment links
278 283 'pr_comment_url': 'http://comment-url',
279 284 'pr_comment_reply_url': 'http://comment-url#reply',
280 285 'ancestor_commit_id': 'f39bd443',
281 286 'added_commits': commit_changes.added,
282 287 'removed_commits': commit_changes.removed,
283 288 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
284 289 'added_files': file_changes.added,
285 290 'modified_files': file_changes.modified,
286 291 'removed_files': file_changes.removed,
287 292 },
288 293
289 294 'cs_comment': {
290 295 'user': user,
291 296 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
292 297 'status_change': None,
293 298 'status_change_type': None,
294 299
295 300 'commit_target_repo_url': 'http://foo.example.com/#comment1',
296 301 'repo_name': 'test-repo',
297 302 'comment_type': 'note',
298 303 'comment_file': None,
299 304 'comment_line': None,
300 305 'commit_comment_url': 'http://comment-url',
301 306 'commit_comment_reply_url': 'http://comment-url#reply',
302 307 'comment_body': 'This is my comment body. *I like !*',
303 308 'comment_id': 2048,
304 309 'renderer_type': 'markdown',
305 310 'mention': True,
306 311 },
307 312
308 313 'cs_comment+status': {
309 314 'user': user,
310 315 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
311 316 'status_change': 'approved',
312 317 'status_change_type': 'approved',
313 318
314 319 'commit_target_repo_url': 'http://foo.example.com/#comment1',
315 320 'repo_name': 'test-repo',
316 321 'comment_type': 'note',
317 322 'comment_file': None,
318 323 'comment_line': None,
319 324 'commit_comment_url': 'http://comment-url',
320 325 'commit_comment_reply_url': 'http://comment-url#reply',
321 326 'comment_body': '''
322 327 Hello **world**
323 328
324 329 This is a multiline comment :)
325 330
326 331 - list
327 332 - list2
328 333 ''',
329 334 'comment_id': 2048,
330 335 'renderer_type': 'markdown',
331 336 'mention': True,
332 337 },
333 338
334 339 'cs_comment+file': {
335 340 'user': user,
336 341 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
337 342 'status_change': None,
338 343 'status_change_type': None,
339 344
340 345 'commit_target_repo_url': 'http://foo.example.com/#comment1',
341 346 'repo_name': 'test-repo',
342 347
343 348 'comment_type': 'note',
344 349 'comment_file': 'test-file.py',
345 350 'comment_line': 'n100',
346 351
347 352 'commit_comment_url': 'http://comment-url',
348 353 'commit_comment_reply_url': 'http://comment-url#reply',
349 354 'comment_body': 'This is my comment body. *I like !*',
350 355 'comment_id': 2048,
351 356 'renderer_type': 'markdown',
352 357 'mention': True,
353 358 },
354 359
355 360 'pull_request': {
356 361 'user': user,
357 362 'pull_request': pr,
358 363 'pull_request_commits': [
359 364 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
360 365 my-account: moved email closer to profile as it's similar data just moved outside.
361 366 '''),
362 367 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
363 368 users: description edit fixes
364 369
365 370 - tests
366 371 - added metatags info
367 372 '''),
368 373 ],
369 374
370 375 'pull_request_target_repo': target_repo,
371 376 'pull_request_target_repo_url': 'http://target-repo/url',
372 377
373 378 'pull_request_source_repo': source_repo,
374 379 'pull_request_source_repo_url': 'http://source-repo/url',
375 380
376 381 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
377 382 'user_role': 'reviewer',
378 383 },
379 384
380 385 'pull_request+reviewer_role': {
381 386 'user': user,
382 387 'pull_request': pr,
383 388 'pull_request_commits': [
384 389 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
385 390 my-account: moved email closer to profile as it's similar data just moved outside.
386 391 '''),
387 392 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
388 393 users: description edit fixes
389 394
390 395 - tests
391 396 - added metatags info
392 397 '''),
393 398 ],
394 399
395 400 'pull_request_target_repo': target_repo,
396 401 'pull_request_target_repo_url': 'http://target-repo/url',
397 402
398 403 'pull_request_source_repo': source_repo,
399 404 'pull_request_source_repo_url': 'http://source-repo/url',
400 405
401 406 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
402 407 'user_role': 'reviewer',
403 408 },
404 409
405 410 'pull_request+observer_role': {
406 411 'user': user,
407 412 'pull_request': pr,
408 413 'pull_request_commits': [
409 414 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
410 415 my-account: moved email closer to profile as it's similar data just moved outside.
411 416 '''),
412 417 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
413 418 users: description edit fixes
414 419
415 420 - tests
416 421 - added metatags info
417 422 '''),
418 423 ],
419 424
420 425 'pull_request_target_repo': target_repo,
421 426 'pull_request_target_repo_url': 'http://target-repo/url',
422 427
423 428 'pull_request_source_repo': source_repo,
424 429 'pull_request_source_repo_url': 'http://source-repo/url',
425 430
426 431 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
427 432 'user_role': 'observer'
428 433 }
429 434 }
430 435
431 436 template_type = email_id.split('+')[0]
432 437 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
433 438 template_type, **email_kwargs.get(email_id, {}))
434 439
435 440 test_email = self.request.GET.get('email')
436 441 if test_email:
437 442 recipients = [test_email]
438 443 run_task(tasks.send_email, recipients, c.subject,
439 444 c.email_body_plaintext, c.email_body)
440 445
441 446 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
442 447 template = 'debug_style/email_plain_rendered.mako'
443 448 else:
444 449 template = 'debug_style/email.mako'
445 450 return render_to_response(
446 451 template, self._get_template_context(c),
447 452 request=self.request)
448 453
449 454 def template(self):
450 455 t_path = self.request.matchdict['t_path']
451 456 c = self.load_default_context()
452 457 c.active = os.path.splitext(t_path)[0]
453 458 c.came_from = ''
454 459 # NOTE(marcink): extend the email types with variations based on data sets
455 460 c.email_types = {
456 461 'cs_comment+file': {},
457 462 'cs_comment+status': {},
458 463
459 464 'pull_request_comment+file': {},
460 465 'pull_request_comment+status': {},
461 466
462 467 'pull_request_update': {},
463 468
464 469 'pull_request+reviewer_role': {},
465 470 'pull_request+observer_role': {},
466 471 }
467 472 c.email_types.update(EmailNotificationModel.email_types)
468 473
469 474 return render_to_response(
470 475 'debug_style/' + t_path, self._get_template_context(c),
471 476 request=self.request)
@@ -1,379 +1,403 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-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 RhodeCode task modules, containing all task that suppose to be run
23 23 by celery daemon
24 24 """
25 25
26 26 import os
27 27 import time
28 28
29 29 from pyramid import compat
30 30 from pyramid_mailer.mailer import Mailer
31 31 from pyramid_mailer.message import Message
32 32 from email.utils import formatdate
33 33
34 34 import rhodecode
35 35 from rhodecode.lib import audit_logger
36 from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask
36 from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask, run_task
37 37 from rhodecode.lib import hooks_base
38 from rhodecode.lib.utils2 import safe_int, str2bool
38 from rhodecode.lib.utils2 import safe_int, str2bool, aslist
39 39 from rhodecode.model.db import (
40 40 Session, IntegrityError, true, Repository, RepoGroup, User)
41 41
42 42
43 43 @async_task(ignore_result=True, base=RequestContextTask)
44 44 def send_email(recipients, subject, body='', html_body='', email_config=None,
45 45 extra_headers=None):
46 46 """
47 47 Sends an email with defined parameters from the .ini files.
48 48
49 49 :param recipients: list of recipients, it this is empty the defined email
50 50 address from field 'email_to' is used instead
51 51 :param subject: subject of the mail
52 52 :param body: body of the mail
53 53 :param html_body: html version of body
54 54 :param email_config: specify custom configuration for mailer
55 55 :param extra_headers: specify custom headers
56 56 """
57 57 log = get_logger(send_email)
58 58
59 59 email_config = email_config or rhodecode.CONFIG
60 60
61 61 mail_server = email_config.get('smtp_server') or None
62 62 if mail_server is None:
63 63 log.error("SMTP server information missing. Sending email failed. "
64 64 "Make sure that `smtp_server` variable is configured "
65 65 "inside the .ini file")
66 66 return False
67 67
68 68 subject = "%s %s" % (email_config.get('email_prefix', ''), subject)
69 69
70 70 if recipients:
71 71 if isinstance(recipients, compat.string_types):
72 72 recipients = recipients.split(',')
73 73 else:
74 74 # if recipients are not defined we send to email_config + all admins
75 75 admins = []
76 76 for u in User.query().filter(User.admin == true()).all():
77 77 if u.email:
78 78 admins.append(u.email)
79 79 recipients = []
80 80 config_email = email_config.get('email_to')
81 81 if config_email:
82 82 recipients += [config_email]
83 83 recipients += admins
84 84
85 85 # translate our LEGACY config into the one that pyramid_mailer supports
86 86 email_conf = dict(
87 87 host=mail_server,
88 88 port=email_config.get('smtp_port', 25),
89 89 username=email_config.get('smtp_username'),
90 90 password=email_config.get('smtp_password'),
91 91
92 92 tls=str2bool(email_config.get('smtp_use_tls')),
93 93 ssl=str2bool(email_config.get('smtp_use_ssl')),
94 94
95 95 # SSL key file
96 96 # keyfile='',
97 97
98 98 # SSL certificate file
99 99 # certfile='',
100 100
101 101 # Location of maildir
102 102 # queue_path='',
103 103
104 104 default_sender=email_config.get('app_email_from', 'RhodeCode'),
105 105
106 106 debug=str2bool(email_config.get('smtp_debug')),
107 107 # /usr/sbin/sendmail Sendmail executable
108 108 # sendmail_app='',
109 109
110 110 # {sendmail_app} -t -i -f {sender} Template for sendmail execution
111 111 # sendmail_template='',
112 112 )
113 113
114 114 if extra_headers is None:
115 115 extra_headers = {}
116 116
117 117 extra_headers.setdefault('Date', formatdate(time.time()))
118 118
119 119 if 'thread_ids' in extra_headers:
120 120 thread_ids = extra_headers.pop('thread_ids')
121 121 extra_headers['References'] = ' '.join('<{}>'.format(t) for t in thread_ids)
122 122
123 123 try:
124 124 mailer = Mailer(**email_conf)
125 125
126 126 message = Message(subject=subject,
127 127 sender=email_conf['default_sender'],
128 128 recipients=recipients,
129 129 body=body, html=html_body,
130 130 extra_headers=extra_headers)
131 131 mailer.send_immediately(message)
132 132
133 133 except Exception:
134 134 log.exception('Mail sending failed')
135 135 return False
136 136 return True
137 137
138 138
139 139 @async_task(ignore_result=True, base=RequestContextTask)
140 140 def create_repo(form_data, cur_user):
141 141 from rhodecode.model.repo import RepoModel
142 142 from rhodecode.model.user import UserModel
143 143 from rhodecode.model.scm import ScmModel
144 144 from rhodecode.model.settings import SettingsModel
145 145
146 146 log = get_logger(create_repo)
147 147
148 148 cur_user = UserModel()._get_user(cur_user)
149 149 owner = cur_user
150 150
151 151 repo_name = form_data['repo_name']
152 152 repo_name_full = form_data['repo_name_full']
153 153 repo_type = form_data['repo_type']
154 154 description = form_data['repo_description']
155 155 private = form_data['repo_private']
156 156 clone_uri = form_data.get('clone_uri')
157 157 repo_group = safe_int(form_data['repo_group'])
158 158 copy_fork_permissions = form_data.get('copy_permissions')
159 159 copy_group_permissions = form_data.get('repo_copy_permissions')
160 160 fork_of = form_data.get('fork_parent_id')
161 161 state = form_data.get('repo_state', Repository.STATE_PENDING)
162 162
163 163 # repo creation defaults, private and repo_type are filled in form
164 164 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
165 165 enable_statistics = form_data.get(
166 166 'enable_statistics', defs.get('repo_enable_statistics'))
167 167 enable_locking = form_data.get(
168 168 'enable_locking', defs.get('repo_enable_locking'))
169 169 enable_downloads = form_data.get(
170 170 'enable_downloads', defs.get('repo_enable_downloads'))
171 171
172 172 # set landing rev based on default branches for SCM
173 173 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
174 174
175 175 try:
176 176 RepoModel()._create_repo(
177 177 repo_name=repo_name_full,
178 178 repo_type=repo_type,
179 179 description=description,
180 180 owner=owner,
181 181 private=private,
182 182 clone_uri=clone_uri,
183 183 repo_group=repo_group,
184 184 landing_rev=landing_ref,
185 185 fork_of=fork_of,
186 186 copy_fork_permissions=copy_fork_permissions,
187 187 copy_group_permissions=copy_group_permissions,
188 188 enable_statistics=enable_statistics,
189 189 enable_locking=enable_locking,
190 190 enable_downloads=enable_downloads,
191 191 state=state
192 192 )
193 193 Session().commit()
194 194
195 195 # now create this repo on Filesystem
196 196 RepoModel()._create_filesystem_repo(
197 197 repo_name=repo_name,
198 198 repo_type=repo_type,
199 199 repo_group=RepoModel()._get_repo_group(repo_group),
200 200 clone_uri=clone_uri,
201 201 )
202 202 repo = Repository.get_by_repo_name(repo_name_full)
203 203 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
204 204
205 205 # update repo commit caches initially
206 206 repo.update_commit_cache()
207 207
208 208 # set new created state
209 209 repo.set_state(Repository.STATE_CREATED)
210 210 repo_id = repo.repo_id
211 211 repo_data = repo.get_api_data()
212 212
213 213 audit_logger.store(
214 214 'repo.create', action_data={'data': repo_data},
215 215 user=cur_user,
216 216 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
217 217
218 218 Session().commit()
219 219 except Exception as e:
220 220 log.warning('Exception occurred when creating repository, '
221 221 'doing cleanup...', exc_info=True)
222 222 if isinstance(e, IntegrityError):
223 223 Session().rollback()
224 224
225 225 # rollback things manually !
226 226 repo = Repository.get_by_repo_name(repo_name_full)
227 227 if repo:
228 228 Repository.delete(repo.repo_id)
229 229 Session().commit()
230 230 RepoModel()._delete_filesystem_repo(repo)
231 231 log.info('Cleanup of repo %s finished', repo_name_full)
232 232 raise
233 233
234 234 return True
235 235
236 236
237 237 @async_task(ignore_result=True, base=RequestContextTask)
238 238 def create_repo_fork(form_data, cur_user):
239 239 """
240 240 Creates a fork of repository using internal VCS methods
241 241 """
242 242 from rhodecode.model.repo import RepoModel
243 243 from rhodecode.model.user import UserModel
244 244
245 245 log = get_logger(create_repo_fork)
246 246
247 247 cur_user = UserModel()._get_user(cur_user)
248 248 owner = cur_user
249 249
250 250 repo_name = form_data['repo_name'] # fork in this case
251 251 repo_name_full = form_data['repo_name_full']
252 252 repo_type = form_data['repo_type']
253 253 description = form_data['description']
254 254 private = form_data['private']
255 255 clone_uri = form_data.get('clone_uri')
256 256 repo_group = safe_int(form_data['repo_group'])
257 257 landing_ref = form_data['landing_rev']
258 258 copy_fork_permissions = form_data.get('copy_permissions')
259 259 fork_id = safe_int(form_data.get('fork_parent_id'))
260 260
261 261 try:
262 262 fork_of = RepoModel()._get_repo(fork_id)
263 263 RepoModel()._create_repo(
264 264 repo_name=repo_name_full,
265 265 repo_type=repo_type,
266 266 description=description,
267 267 owner=owner,
268 268 private=private,
269 269 clone_uri=clone_uri,
270 270 repo_group=repo_group,
271 271 landing_rev=landing_ref,
272 272 fork_of=fork_of,
273 273 copy_fork_permissions=copy_fork_permissions
274 274 )
275 275
276 276 Session().commit()
277 277
278 278 base_path = Repository.base_path()
279 279 source_repo_path = os.path.join(base_path, fork_of.repo_name)
280 280
281 281 # now create this repo on Filesystem
282 282 RepoModel()._create_filesystem_repo(
283 283 repo_name=repo_name,
284 284 repo_type=repo_type,
285 285 repo_group=RepoModel()._get_repo_group(repo_group),
286 286 clone_uri=source_repo_path,
287 287 )
288 288 repo = Repository.get_by_repo_name(repo_name_full)
289 289 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
290 290
291 291 # update repo commit caches initially
292 292 config = repo._config
293 293 config.set('extensions', 'largefiles', '')
294 294 repo.update_commit_cache(config=config)
295 295
296 296 # set new created state
297 297 repo.set_state(Repository.STATE_CREATED)
298 298
299 299 repo_id = repo.repo_id
300 300 repo_data = repo.get_api_data()
301 301 audit_logger.store(
302 302 'repo.fork', action_data={'data': repo_data},
303 303 user=cur_user,
304 304 repo=audit_logger.RepoWrap(repo_name=repo_name, repo_id=repo_id))
305 305
306 306 Session().commit()
307 307 except Exception as e:
308 308 log.warning('Exception occurred when forking repository, '
309 309 'doing cleanup...', exc_info=True)
310 310 if isinstance(e, IntegrityError):
311 311 Session().rollback()
312 312
313 313 # rollback things manually !
314 314 repo = Repository.get_by_repo_name(repo_name_full)
315 315 if repo:
316 316 Repository.delete(repo.repo_id)
317 317 Session().commit()
318 318 RepoModel()._delete_filesystem_repo(repo)
319 319 log.info('Cleanup of repo %s finished', repo_name_full)
320 320 raise
321 321
322 322 return True
323 323
324 324
325 325 @async_task(ignore_result=True)
326 326 def repo_maintenance(repoid):
327 327 from rhodecode.lib import repo_maintenance as repo_maintenance_lib
328 328 log = get_logger(repo_maintenance)
329 329 repo = Repository.get_by_id_or_repo_name(repoid)
330 330 if repo:
331 331 maintenance = repo_maintenance_lib.RepoMaintenance()
332 332 tasks = maintenance.get_tasks_for_repo(repo)
333 333 log.debug('Executing %s tasks on repo `%s`', tasks, repoid)
334 334 executed_types = maintenance.execute(repo)
335 335 log.debug('Got execution results %s', executed_types)
336 336 else:
337 337 log.debug('Repo `%s` not found or without a clone_url', repoid)
338 338
339 339
340 340 @async_task(ignore_result=True)
341 def check_for_update():
341 def check_for_update(send_email_notification=True, email_recipients=None):
342 342 from rhodecode.model.update import UpdateModel
343 from rhodecode.model.notification import EmailNotificationModel
344
345 log = get_logger(check_for_update)
343 346 update_url = UpdateModel().get_update_url()
344 347 cur_ver = rhodecode.__version__
345 348
346 349 try:
347 350 data = UpdateModel().get_update_data(update_url)
348 latest = data['versions'][0]
349 UpdateModel().store_version(latest['version'])
351
352 current_ver = UpdateModel().get_stored_version(fallback=cur_ver)
353 latest_ver = data['versions'][0]['version']
354 UpdateModel().store_version(latest_ver)
355
356 if send_email_notification:
357 log.debug('Send email notification is enabled. '
358 'Current RhodeCode version: %s, latest known: %s', current_ver, latest_ver)
359 if UpdateModel().is_outdated(current_ver, latest_ver):
360
361 email_kwargs = {
362 'current_ver': current_ver,
363 'latest_ver': latest_ver,
364 }
365
366 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
367 EmailNotificationModel.TYPE_UPDATE_AVAILABLE, **email_kwargs)
368
369 email_recipients = aslist(email_recipients, sep=',') or \
370 [user.email for user in User.get_all_super_admins()]
371 run_task(send_email, email_recipients, subject,
372 email_body_plaintext, email_body)
373
350 374 except Exception:
351 375 pass
352 376
353 377
354 378 @async_task(ignore_result=False)
355 379 def beat_check(*args, **kwargs):
356 380 log = get_logger(beat_check)
357 381 log.info('%r: Got args: %r and kwargs %r', beat_check, args, kwargs)
358 382 return time.time()
359 383
360 384
361 385 @async_task(ignore_result=True)
362 386 def sync_last_update(*args, **kwargs):
363 387
364 388 skip_repos = kwargs.get('skip_repos')
365 389 if not skip_repos:
366 390 repos = Repository.query() \
367 391 .order_by(Repository.group_id.asc())
368 392
369 393 for repo in repos:
370 394 repo.update_commit_cache()
371 395
372 396 skip_groups = kwargs.get('skip_groups')
373 397 if not skip_groups:
374 398 repo_groups = RepoGroup.query() \
375 399 .filter(RepoGroup.group_parent_id == None)
376 400
377 401 for root_gr in repo_groups:
378 402 for repo_gr in reversed(root_gr.recursive_groups()):
379 403 repo_gr.update_commit_cache()
@@ -1,450 +1,453 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 333
334 334 class EmailNotificationModel(BaseModel):
335 335 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
336 336 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
337 337 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
338 338 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
339 339 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
340 340 TYPE_MAIN = Notification.TYPE_MESSAGE
341 341
342 342 TYPE_PASSWORD_RESET = 'password_reset'
343 343 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
344 344 TYPE_EMAIL_TEST = 'email_test'
345 345 TYPE_EMAIL_EXCEPTION = 'exception'
346 TYPE_UPDATE_AVAILABLE = 'update_available'
346 347 TYPE_TEST = 'test'
347 348
348 349 email_types = {
349 350 TYPE_MAIN:
350 351 'rhodecode:templates/email_templates/main.mako',
351 352 TYPE_TEST:
352 353 'rhodecode:templates/email_templates/test.mako',
353 354 TYPE_EMAIL_EXCEPTION:
354 355 'rhodecode:templates/email_templates/exception_tracker.mako',
356 TYPE_UPDATE_AVAILABLE:
357 'rhodecode:templates/email_templates/update_available.mako',
355 358 TYPE_EMAIL_TEST:
356 359 'rhodecode:templates/email_templates/email_test.mako',
357 360 TYPE_REGISTRATION:
358 361 'rhodecode:templates/email_templates/user_registration.mako',
359 362 TYPE_PASSWORD_RESET:
360 363 'rhodecode:templates/email_templates/password_reset.mako',
361 364 TYPE_PASSWORD_RESET_CONFIRMATION:
362 365 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
363 366 TYPE_COMMIT_COMMENT:
364 367 'rhodecode:templates/email_templates/commit_comment.mako',
365 368 TYPE_PULL_REQUEST:
366 369 'rhodecode:templates/email_templates/pull_request_review.mako',
367 370 TYPE_PULL_REQUEST_COMMENT:
368 371 'rhodecode:templates/email_templates/pull_request_comment.mako',
369 372 TYPE_PULL_REQUEST_UPDATE:
370 373 'rhodecode:templates/email_templates/pull_request_update.mako',
371 374 }
372 375
373 376 premailer_instance = premailer.Premailer(
374 377 cssutils_logging_level=logging.ERROR,
375 378 cssutils_logging_handler=logging.getLogger().handlers[0]
376 379 if logging.getLogger().handlers else None,
377 380 )
378 381
379 382 def __init__(self):
380 383 """
381 384 Example usage::
382 385
383 386 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
384 387 EmailNotificationModel.TYPE_TEST, **email_kwargs)
385 388
386 389 """
387 390 super(EmailNotificationModel, self).__init__()
388 391 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
389 392
390 393 def _update_kwargs_for_render(self, kwargs):
391 394 """
392 395 Inject params required for Mako rendering
393 396
394 397 :param kwargs:
395 398 """
396 399
397 400 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
398 401 kwargs['rhodecode_version'] = rhodecode.__version__
399 402 instance_url = h.route_url('home')
400 403 _kwargs = {
401 404 'instance_url': instance_url,
402 405 'whitespace_filter': self.whitespace_filter,
403 406 'email_pr_update_subject_template': EMAIL_PR_UPDATE_SUBJECT_TEMPLATE,
404 407 'email_pr_review_subject_template': EMAIL_PR_REVIEW_SUBJECT_TEMPLATE,
405 408 'email_pr_comment_subject_template': EMAIL_PR_COMMENT_SUBJECT_TEMPLATE,
406 409 'email_pr_comment_status_change_subject_template': EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
407 410 'email_pr_comment_file_subject_template': EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE,
408 411 'email_comment_subject_template': EMAIL_COMMENT_SUBJECT_TEMPLATE,
409 412 'email_comment_status_change_subject_template': EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE,
410 413 'email_comment_file_subject_template': EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE,
411 414 }
412 415 _kwargs.update(kwargs)
413 416 return _kwargs
414 417
415 418 def whitespace_filter(self, text):
416 419 return text.replace('\n', '').replace('\t', '')
417 420
418 421 def get_renderer(self, type_, request):
419 422 template_name = self.email_types[type_]
420 423 return request.get_partial_renderer(template_name)
421 424
422 425 def render_email(self, type_, **kwargs):
423 426 """
424 427 renders template for email, and returns a tuple of
425 428 (subject, email_headers, email_html_body, email_plaintext_body)
426 429 """
427 430 # translator and helpers inject
428 431 _kwargs = self._update_kwargs_for_render(kwargs)
429 432 request = get_current_request()
430 433 email_template = self.get_renderer(type_, request=request)
431 434
432 435 subject = email_template.render('subject', **_kwargs)
433 436
434 437 try:
435 438 body_plaintext = email_template.render('body_plaintext', **_kwargs)
436 439 except AttributeError:
437 440 # it's not defined in template, ok we can skip it
438 441 body_plaintext = ''
439 442
440 443 # render WHOLE template
441 444 body = email_template.render(None, **_kwargs)
442 445
443 446 try:
444 447 # Inline CSS styles and conversion
445 448 body = self.premailer_instance.transform(body)
446 449 except Exception:
447 450 log.exception('Failed to parse body with premailer')
448 451 pass
449 452
450 453 return subject, body, body_plaintext
@@ -1,83 +1,83 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-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 import logging
22 22 import urllib2
23 23 from packaging.version import Version
24 24
25 25 import rhodecode
26 26 from rhodecode.lib.ext_json import json
27 27 from rhodecode.model import BaseModel
28 28 from rhodecode.model.meta import Session
29 29 from rhodecode.model.settings import SettingsModel
30 30
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 class UpdateModel(BaseModel):
36 36 UPDATE_SETTINGS_KEY = 'update_version'
37 37 UPDATE_URL_SETTINGS_KEY = 'rhodecode_update_url'
38 38
39 39 @staticmethod
40 40 def get_update_data(update_url):
41 41 """Return the JSON update data."""
42 42 ver = rhodecode.__version__
43 43 log.debug('Checking for upgrade on `%s` server', update_url)
44 44 opener = urllib2.build_opener()
45 45 opener.addheaders = [('User-agent', 'RhodeCode-SCM/%s' % ver)]
46 46 response = opener.open(update_url)
47 47 response_data = response.read()
48 48 data = json.loads(response_data)
49 49 log.debug('update server returned data')
50 50 return data
51 51
52 52 def get_update_url(self):
53 53 settings = SettingsModel().get_all_settings()
54 54 return settings.get(self.UPDATE_URL_SETTINGS_KEY)
55 55
56 56 def store_version(self, version):
57 57 log.debug('Storing version %s into settings', version)
58 58 setting = SettingsModel().create_or_update_setting(
59 59 self.UPDATE_SETTINGS_KEY, version)
60 60 Session().add(setting)
61 61 Session().commit()
62 62
63 def get_stored_version(self):
63 def get_stored_version(self, fallback=None):
64 64 obj = SettingsModel().get_setting_by_name(self.UPDATE_SETTINGS_KEY)
65 65 if obj:
66 66 return obj.app_settings_value
67 return '0.0.0'
67 return fallback or '0.0.0'
68 68
69 69 def _sanitize_version(self, version):
70 70 """
71 71 Cleanup our custom ver.
72 72 e.g 4.11.0_20171204_204825_CE_default_EE_default to 4.11.0
73 73 """
74 74 return version.split('_')[0]
75 75
76 76 def is_outdated(self, cur_version, latest_version=None):
77 77 latest_version = latest_version or self.get_stored_version()
78 78 try:
79 79 cur_version = self._sanitize_version(cur_version)
80 80 return Version(latest_version) > Version(cur_version)
81 81 except Exception:
82 82 # could be invalid version, etc
83 83 return False
General Comments 0
You need to be logged in to leave comments. Login now