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