##// END OF EJS Templates
emails: set References header for threading in mail user agents even with different subjects...
marcink -
r4447:ae62a3cc default
parent child Browse files
Show More
@@ -1,784 +1,783 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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 import logging
22 import logging
23 import collections
23 import collections
24
24
25 import datetime
25 import datetime
26 import formencode
26 import formencode
27 import formencode.htmlfill
27 import formencode.htmlfill
28
28
29 import rhodecode
29 import rhodecode
30 from pyramid.view import view_config
30 from pyramid.view import view_config
31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
32 from pyramid.renderers import render
32 from pyramid.renderers import render
33 from pyramid.response import Response
33 from pyramid.response import Response
34
34
35 from rhodecode.apps._base import BaseAppView
35 from rhodecode.apps._base import BaseAppView
36 from rhodecode.apps._base.navigation import navigation_list
36 from rhodecode.apps._base.navigation import navigation_list
37 from rhodecode.apps.svn_support.config_keys import generate_config
37 from rhodecode.apps.svn_support.config_keys import generate_config
38 from rhodecode.lib import helpers as h
38 from rhodecode.lib import helpers as h
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
41 from rhodecode.lib.celerylib import tasks, run_task
41 from rhodecode.lib.celerylib import tasks, run_task
42 from rhodecode.lib.utils import repo2db_mapper
42 from rhodecode.lib.utils import repo2db_mapper
43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
44 from rhodecode.lib.index import searcher_from_config
44 from rhodecode.lib.index import searcher_from_config
45
45
46 from rhodecode.model.db import RhodeCodeUi, Repository
46 from rhodecode.model.db import RhodeCodeUi, Repository
47 from rhodecode.model.forms import (ApplicationSettingsForm,
47 from rhodecode.model.forms import (ApplicationSettingsForm,
48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 LabsSettingsForm, IssueTrackerPatternsForm)
49 LabsSettingsForm, IssueTrackerPatternsForm)
50 from rhodecode.model.permission import PermissionModel
50 from rhodecode.model.permission import PermissionModel
51 from rhodecode.model.repo_group import RepoGroupModel
51 from rhodecode.model.repo_group import RepoGroupModel
52
52
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54 from rhodecode.model.notification import EmailNotificationModel
54 from rhodecode.model.notification import EmailNotificationModel
55 from rhodecode.model.meta import Session
55 from rhodecode.model.meta import Session
56 from rhodecode.model.settings import (
56 from rhodecode.model.settings import (
57 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
57 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
58 SettingsModel)
58 SettingsModel)
59
59
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 class AdminSettingsView(BaseAppView):
64 class AdminSettingsView(BaseAppView):
65
65
66 def load_default_context(self):
66 def load_default_context(self):
67 c = self._get_local_tmpl_context()
67 c = self._get_local_tmpl_context()
68 c.labs_active = str2bool(
68 c.labs_active = str2bool(
69 rhodecode.CONFIG.get('labs_settings_active', 'true'))
69 rhodecode.CONFIG.get('labs_settings_active', 'true'))
70 c.navlist = navigation_list(self.request)
70 c.navlist = navigation_list(self.request)
71
71
72 return c
72 return c
73
73
74 @classmethod
74 @classmethod
75 def _get_ui_settings(cls):
75 def _get_ui_settings(cls):
76 ret = RhodeCodeUi.query().all()
76 ret = RhodeCodeUi.query().all()
77
77
78 if not ret:
78 if not ret:
79 raise Exception('Could not get application ui settings !')
79 raise Exception('Could not get application ui settings !')
80 settings = {}
80 settings = {}
81 for each in ret:
81 for each in ret:
82 k = each.ui_key
82 k = each.ui_key
83 v = each.ui_value
83 v = each.ui_value
84 if k == '/':
84 if k == '/':
85 k = 'root_path'
85 k = 'root_path'
86
86
87 if k in ['push_ssl', 'publish', 'enabled']:
87 if k in ['push_ssl', 'publish', 'enabled']:
88 v = str2bool(v)
88 v = str2bool(v)
89
89
90 if k.find('.') != -1:
90 if k.find('.') != -1:
91 k = k.replace('.', '_')
91 k = k.replace('.', '_')
92
92
93 if each.ui_section in ['hooks', 'extensions']:
93 if each.ui_section in ['hooks', 'extensions']:
94 v = each.ui_active
94 v = each.ui_active
95
95
96 settings[each.ui_section + '_' + k] = v
96 settings[each.ui_section + '_' + k] = v
97 return settings
97 return settings
98
98
99 @classmethod
99 @classmethod
100 def _form_defaults(cls):
100 def _form_defaults(cls):
101 defaults = SettingsModel().get_all_settings()
101 defaults = SettingsModel().get_all_settings()
102 defaults.update(cls._get_ui_settings())
102 defaults.update(cls._get_ui_settings())
103
103
104 defaults.update({
104 defaults.update({
105 'new_svn_branch': '',
105 'new_svn_branch': '',
106 'new_svn_tag': '',
106 'new_svn_tag': '',
107 })
107 })
108 return defaults
108 return defaults
109
109
110 @LoginRequired()
110 @LoginRequired()
111 @HasPermissionAllDecorator('hg.admin')
111 @HasPermissionAllDecorator('hg.admin')
112 @view_config(
112 @view_config(
113 route_name='admin_settings_vcs', request_method='GET',
113 route_name='admin_settings_vcs', request_method='GET',
114 renderer='rhodecode:templates/admin/settings/settings.mako')
114 renderer='rhodecode:templates/admin/settings/settings.mako')
115 def settings_vcs(self):
115 def settings_vcs(self):
116 c = self.load_default_context()
116 c = self.load_default_context()
117 c.active = 'vcs'
117 c.active = 'vcs'
118 model = VcsSettingsModel()
118 model = VcsSettingsModel()
119 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
119 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
120 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
120 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
121
121
122 settings = self.request.registry.settings
122 settings = self.request.registry.settings
123 c.svn_proxy_generate_config = settings[generate_config]
123 c.svn_proxy_generate_config = settings[generate_config]
124
124
125 defaults = self._form_defaults()
125 defaults = self._form_defaults()
126
126
127 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
127 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
128
128
129 data = render('rhodecode:templates/admin/settings/settings.mako',
129 data = render('rhodecode:templates/admin/settings/settings.mako',
130 self._get_template_context(c), self.request)
130 self._get_template_context(c), self.request)
131 html = formencode.htmlfill.render(
131 html = formencode.htmlfill.render(
132 data,
132 data,
133 defaults=defaults,
133 defaults=defaults,
134 encoding="UTF-8",
134 encoding="UTF-8",
135 force_defaults=False
135 force_defaults=False
136 )
136 )
137 return Response(html)
137 return Response(html)
138
138
139 @LoginRequired()
139 @LoginRequired()
140 @HasPermissionAllDecorator('hg.admin')
140 @HasPermissionAllDecorator('hg.admin')
141 @CSRFRequired()
141 @CSRFRequired()
142 @view_config(
142 @view_config(
143 route_name='admin_settings_vcs_update', request_method='POST',
143 route_name='admin_settings_vcs_update', request_method='POST',
144 renderer='rhodecode:templates/admin/settings/settings.mako')
144 renderer='rhodecode:templates/admin/settings/settings.mako')
145 def settings_vcs_update(self):
145 def settings_vcs_update(self):
146 _ = self.request.translate
146 _ = self.request.translate
147 c = self.load_default_context()
147 c = self.load_default_context()
148 c.active = 'vcs'
148 c.active = 'vcs'
149
149
150 model = VcsSettingsModel()
150 model = VcsSettingsModel()
151 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
151 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
152 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
152 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
153
153
154 settings = self.request.registry.settings
154 settings = self.request.registry.settings
155 c.svn_proxy_generate_config = settings[generate_config]
155 c.svn_proxy_generate_config = settings[generate_config]
156
156
157 application_form = ApplicationUiSettingsForm(self.request.translate)()
157 application_form = ApplicationUiSettingsForm(self.request.translate)()
158
158
159 try:
159 try:
160 form_result = application_form.to_python(dict(self.request.POST))
160 form_result = application_form.to_python(dict(self.request.POST))
161 except formencode.Invalid as errors:
161 except formencode.Invalid as errors:
162 h.flash(
162 h.flash(
163 _("Some form inputs contain invalid data."),
163 _("Some form inputs contain invalid data."),
164 category='error')
164 category='error')
165 data = render('rhodecode:templates/admin/settings/settings.mako',
165 data = render('rhodecode:templates/admin/settings/settings.mako',
166 self._get_template_context(c), self.request)
166 self._get_template_context(c), self.request)
167 html = formencode.htmlfill.render(
167 html = formencode.htmlfill.render(
168 data,
168 data,
169 defaults=errors.value,
169 defaults=errors.value,
170 errors=errors.error_dict or {},
170 errors=errors.error_dict or {},
171 prefix_error=False,
171 prefix_error=False,
172 encoding="UTF-8",
172 encoding="UTF-8",
173 force_defaults=False
173 force_defaults=False
174 )
174 )
175 return Response(html)
175 return Response(html)
176
176
177 try:
177 try:
178 if c.visual.allow_repo_location_change:
178 if c.visual.allow_repo_location_change:
179 model.update_global_path_setting(form_result['paths_root_path'])
179 model.update_global_path_setting(form_result['paths_root_path'])
180
180
181 model.update_global_ssl_setting(form_result['web_push_ssl'])
181 model.update_global_ssl_setting(form_result['web_push_ssl'])
182 model.update_global_hook_settings(form_result)
182 model.update_global_hook_settings(form_result)
183
183
184 model.create_or_update_global_svn_settings(form_result)
184 model.create_or_update_global_svn_settings(form_result)
185 model.create_or_update_global_hg_settings(form_result)
185 model.create_or_update_global_hg_settings(form_result)
186 model.create_or_update_global_git_settings(form_result)
186 model.create_or_update_global_git_settings(form_result)
187 model.create_or_update_global_pr_settings(form_result)
187 model.create_or_update_global_pr_settings(form_result)
188 except Exception:
188 except Exception:
189 log.exception("Exception while updating settings")
189 log.exception("Exception while updating settings")
190 h.flash(_('Error occurred during updating '
190 h.flash(_('Error occurred during updating '
191 'application settings'), category='error')
191 'application settings'), category='error')
192 else:
192 else:
193 Session().commit()
193 Session().commit()
194 h.flash(_('Updated VCS settings'), category='success')
194 h.flash(_('Updated VCS settings'), category='success')
195 raise HTTPFound(h.route_path('admin_settings_vcs'))
195 raise HTTPFound(h.route_path('admin_settings_vcs'))
196
196
197 data = render('rhodecode:templates/admin/settings/settings.mako',
197 data = render('rhodecode:templates/admin/settings/settings.mako',
198 self._get_template_context(c), self.request)
198 self._get_template_context(c), self.request)
199 html = formencode.htmlfill.render(
199 html = formencode.htmlfill.render(
200 data,
200 data,
201 defaults=self._form_defaults(),
201 defaults=self._form_defaults(),
202 encoding="UTF-8",
202 encoding="UTF-8",
203 force_defaults=False
203 force_defaults=False
204 )
204 )
205 return Response(html)
205 return Response(html)
206
206
207 @LoginRequired()
207 @LoginRequired()
208 @HasPermissionAllDecorator('hg.admin')
208 @HasPermissionAllDecorator('hg.admin')
209 @CSRFRequired()
209 @CSRFRequired()
210 @view_config(
210 @view_config(
211 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
211 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
212 renderer='json_ext', xhr=True)
212 renderer='json_ext', xhr=True)
213 def settings_vcs_delete_svn_pattern(self):
213 def settings_vcs_delete_svn_pattern(self):
214 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
214 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
215 model = VcsSettingsModel()
215 model = VcsSettingsModel()
216 try:
216 try:
217 model.delete_global_svn_pattern(delete_pattern_id)
217 model.delete_global_svn_pattern(delete_pattern_id)
218 except SettingNotFound:
218 except SettingNotFound:
219 log.exception(
219 log.exception(
220 'Failed to delete svn_pattern with id %s', delete_pattern_id)
220 'Failed to delete svn_pattern with id %s', delete_pattern_id)
221 raise HTTPNotFound()
221 raise HTTPNotFound()
222
222
223 Session().commit()
223 Session().commit()
224 return True
224 return True
225
225
226 @LoginRequired()
226 @LoginRequired()
227 @HasPermissionAllDecorator('hg.admin')
227 @HasPermissionAllDecorator('hg.admin')
228 @view_config(
228 @view_config(
229 route_name='admin_settings_mapping', request_method='GET',
229 route_name='admin_settings_mapping', request_method='GET',
230 renderer='rhodecode:templates/admin/settings/settings.mako')
230 renderer='rhodecode:templates/admin/settings/settings.mako')
231 def settings_mapping(self):
231 def settings_mapping(self):
232 c = self.load_default_context()
232 c = self.load_default_context()
233 c.active = 'mapping'
233 c.active = 'mapping'
234
234
235 data = render('rhodecode:templates/admin/settings/settings.mako',
235 data = render('rhodecode:templates/admin/settings/settings.mako',
236 self._get_template_context(c), self.request)
236 self._get_template_context(c), self.request)
237 html = formencode.htmlfill.render(
237 html = formencode.htmlfill.render(
238 data,
238 data,
239 defaults=self._form_defaults(),
239 defaults=self._form_defaults(),
240 encoding="UTF-8",
240 encoding="UTF-8",
241 force_defaults=False
241 force_defaults=False
242 )
242 )
243 return Response(html)
243 return Response(html)
244
244
245 @LoginRequired()
245 @LoginRequired()
246 @HasPermissionAllDecorator('hg.admin')
246 @HasPermissionAllDecorator('hg.admin')
247 @CSRFRequired()
247 @CSRFRequired()
248 @view_config(
248 @view_config(
249 route_name='admin_settings_mapping_update', request_method='POST',
249 route_name='admin_settings_mapping_update', request_method='POST',
250 renderer='rhodecode:templates/admin/settings/settings.mako')
250 renderer='rhodecode:templates/admin/settings/settings.mako')
251 def settings_mapping_update(self):
251 def settings_mapping_update(self):
252 _ = self.request.translate
252 _ = self.request.translate
253 c = self.load_default_context()
253 c = self.load_default_context()
254 c.active = 'mapping'
254 c.active = 'mapping'
255 rm_obsolete = self.request.POST.get('destroy', False)
255 rm_obsolete = self.request.POST.get('destroy', False)
256 invalidate_cache = self.request.POST.get('invalidate', False)
256 invalidate_cache = self.request.POST.get('invalidate', False)
257 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
257 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
258
258
259 if invalidate_cache:
259 if invalidate_cache:
260 log.debug('invalidating all repositories cache')
260 log.debug('invalidating all repositories cache')
261 for repo in Repository.get_all():
261 for repo in Repository.get_all():
262 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
262 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
263
263
264 filesystem_repos = ScmModel().repo_scan()
264 filesystem_repos = ScmModel().repo_scan()
265 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
265 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
266 PermissionModel().trigger_permission_flush()
266 PermissionModel().trigger_permission_flush()
267
267
268 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
268 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
269 h.flash(_('Repositories successfully '
269 h.flash(_('Repositories successfully '
270 'rescanned added: %s ; removed: %s') %
270 'rescanned added: %s ; removed: %s') %
271 (_repr(added), _repr(removed)),
271 (_repr(added), _repr(removed)),
272 category='success')
272 category='success')
273 raise HTTPFound(h.route_path('admin_settings_mapping'))
273 raise HTTPFound(h.route_path('admin_settings_mapping'))
274
274
275 @LoginRequired()
275 @LoginRequired()
276 @HasPermissionAllDecorator('hg.admin')
276 @HasPermissionAllDecorator('hg.admin')
277 @view_config(
277 @view_config(
278 route_name='admin_settings', request_method='GET',
278 route_name='admin_settings', request_method='GET',
279 renderer='rhodecode:templates/admin/settings/settings.mako')
279 renderer='rhodecode:templates/admin/settings/settings.mako')
280 @view_config(
280 @view_config(
281 route_name='admin_settings_global', request_method='GET',
281 route_name='admin_settings_global', request_method='GET',
282 renderer='rhodecode:templates/admin/settings/settings.mako')
282 renderer='rhodecode:templates/admin/settings/settings.mako')
283 def settings_global(self):
283 def settings_global(self):
284 c = self.load_default_context()
284 c = self.load_default_context()
285 c.active = 'global'
285 c.active = 'global'
286 c.personal_repo_group_default_pattern = RepoGroupModel()\
286 c.personal_repo_group_default_pattern = RepoGroupModel()\
287 .get_personal_group_name_pattern()
287 .get_personal_group_name_pattern()
288
288
289 data = render('rhodecode:templates/admin/settings/settings.mako',
289 data = render('rhodecode:templates/admin/settings/settings.mako',
290 self._get_template_context(c), self.request)
290 self._get_template_context(c), self.request)
291 html = formencode.htmlfill.render(
291 html = formencode.htmlfill.render(
292 data,
292 data,
293 defaults=self._form_defaults(),
293 defaults=self._form_defaults(),
294 encoding="UTF-8",
294 encoding="UTF-8",
295 force_defaults=False
295 force_defaults=False
296 )
296 )
297 return Response(html)
297 return Response(html)
298
298
299 @LoginRequired()
299 @LoginRequired()
300 @HasPermissionAllDecorator('hg.admin')
300 @HasPermissionAllDecorator('hg.admin')
301 @CSRFRequired()
301 @CSRFRequired()
302 @view_config(
302 @view_config(
303 route_name='admin_settings_update', request_method='POST',
303 route_name='admin_settings_update', request_method='POST',
304 renderer='rhodecode:templates/admin/settings/settings.mako')
304 renderer='rhodecode:templates/admin/settings/settings.mako')
305 @view_config(
305 @view_config(
306 route_name='admin_settings_global_update', request_method='POST',
306 route_name='admin_settings_global_update', request_method='POST',
307 renderer='rhodecode:templates/admin/settings/settings.mako')
307 renderer='rhodecode:templates/admin/settings/settings.mako')
308 def settings_global_update(self):
308 def settings_global_update(self):
309 _ = self.request.translate
309 _ = self.request.translate
310 c = self.load_default_context()
310 c = self.load_default_context()
311 c.active = 'global'
311 c.active = 'global'
312 c.personal_repo_group_default_pattern = RepoGroupModel()\
312 c.personal_repo_group_default_pattern = RepoGroupModel()\
313 .get_personal_group_name_pattern()
313 .get_personal_group_name_pattern()
314 application_form = ApplicationSettingsForm(self.request.translate)()
314 application_form = ApplicationSettingsForm(self.request.translate)()
315 try:
315 try:
316 form_result = application_form.to_python(dict(self.request.POST))
316 form_result = application_form.to_python(dict(self.request.POST))
317 except formencode.Invalid as errors:
317 except formencode.Invalid as errors:
318 h.flash(
318 h.flash(
319 _("Some form inputs contain invalid data."),
319 _("Some form inputs contain invalid data."),
320 category='error')
320 category='error')
321 data = render('rhodecode:templates/admin/settings/settings.mako',
321 data = render('rhodecode:templates/admin/settings/settings.mako',
322 self._get_template_context(c), self.request)
322 self._get_template_context(c), self.request)
323 html = formencode.htmlfill.render(
323 html = formencode.htmlfill.render(
324 data,
324 data,
325 defaults=errors.value,
325 defaults=errors.value,
326 errors=errors.error_dict or {},
326 errors=errors.error_dict or {},
327 prefix_error=False,
327 prefix_error=False,
328 encoding="UTF-8",
328 encoding="UTF-8",
329 force_defaults=False
329 force_defaults=False
330 )
330 )
331 return Response(html)
331 return Response(html)
332
332
333 settings = [
333 settings = [
334 ('title', 'rhodecode_title', 'unicode'),
334 ('title', 'rhodecode_title', 'unicode'),
335 ('realm', 'rhodecode_realm', 'unicode'),
335 ('realm', 'rhodecode_realm', 'unicode'),
336 ('pre_code', 'rhodecode_pre_code', 'unicode'),
336 ('pre_code', 'rhodecode_pre_code', 'unicode'),
337 ('post_code', 'rhodecode_post_code', 'unicode'),
337 ('post_code', 'rhodecode_post_code', 'unicode'),
338 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
338 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
339 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
339 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
340 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
340 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
341 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
341 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
342 ]
342 ]
343 try:
343 try:
344 for setting, form_key, type_ in settings:
344 for setting, form_key, type_ in settings:
345 sett = SettingsModel().create_or_update_setting(
345 sett = SettingsModel().create_or_update_setting(
346 setting, form_result[form_key], type_)
346 setting, form_result[form_key], type_)
347 Session().add(sett)
347 Session().add(sett)
348
348
349 Session().commit()
349 Session().commit()
350 SettingsModel().invalidate_settings_cache()
350 SettingsModel().invalidate_settings_cache()
351 h.flash(_('Updated application settings'), category='success')
351 h.flash(_('Updated application settings'), category='success')
352 except Exception:
352 except Exception:
353 log.exception("Exception while updating application settings")
353 log.exception("Exception while updating application settings")
354 h.flash(
354 h.flash(
355 _('Error occurred during updating application settings'),
355 _('Error occurred during updating application settings'),
356 category='error')
356 category='error')
357
357
358 raise HTTPFound(h.route_path('admin_settings_global'))
358 raise HTTPFound(h.route_path('admin_settings_global'))
359
359
360 @LoginRequired()
360 @LoginRequired()
361 @HasPermissionAllDecorator('hg.admin')
361 @HasPermissionAllDecorator('hg.admin')
362 @view_config(
362 @view_config(
363 route_name='admin_settings_visual', request_method='GET',
363 route_name='admin_settings_visual', request_method='GET',
364 renderer='rhodecode:templates/admin/settings/settings.mako')
364 renderer='rhodecode:templates/admin/settings/settings.mako')
365 def settings_visual(self):
365 def settings_visual(self):
366 c = self.load_default_context()
366 c = self.load_default_context()
367 c.active = 'visual'
367 c.active = 'visual'
368
368
369 data = render('rhodecode:templates/admin/settings/settings.mako',
369 data = render('rhodecode:templates/admin/settings/settings.mako',
370 self._get_template_context(c), self.request)
370 self._get_template_context(c), self.request)
371 html = formencode.htmlfill.render(
371 html = formencode.htmlfill.render(
372 data,
372 data,
373 defaults=self._form_defaults(),
373 defaults=self._form_defaults(),
374 encoding="UTF-8",
374 encoding="UTF-8",
375 force_defaults=False
375 force_defaults=False
376 )
376 )
377 return Response(html)
377 return Response(html)
378
378
379 @LoginRequired()
379 @LoginRequired()
380 @HasPermissionAllDecorator('hg.admin')
380 @HasPermissionAllDecorator('hg.admin')
381 @CSRFRequired()
381 @CSRFRequired()
382 @view_config(
382 @view_config(
383 route_name='admin_settings_visual_update', request_method='POST',
383 route_name='admin_settings_visual_update', request_method='POST',
384 renderer='rhodecode:templates/admin/settings/settings.mako')
384 renderer='rhodecode:templates/admin/settings/settings.mako')
385 def settings_visual_update(self):
385 def settings_visual_update(self):
386 _ = self.request.translate
386 _ = self.request.translate
387 c = self.load_default_context()
387 c = self.load_default_context()
388 c.active = 'visual'
388 c.active = 'visual'
389 application_form = ApplicationVisualisationForm(self.request.translate)()
389 application_form = ApplicationVisualisationForm(self.request.translate)()
390 try:
390 try:
391 form_result = application_form.to_python(dict(self.request.POST))
391 form_result = application_form.to_python(dict(self.request.POST))
392 except formencode.Invalid as errors:
392 except formencode.Invalid as errors:
393 h.flash(
393 h.flash(
394 _("Some form inputs contain invalid data."),
394 _("Some form inputs contain invalid data."),
395 category='error')
395 category='error')
396 data = render('rhodecode:templates/admin/settings/settings.mako',
396 data = render('rhodecode:templates/admin/settings/settings.mako',
397 self._get_template_context(c), self.request)
397 self._get_template_context(c), self.request)
398 html = formencode.htmlfill.render(
398 html = formencode.htmlfill.render(
399 data,
399 data,
400 defaults=errors.value,
400 defaults=errors.value,
401 errors=errors.error_dict or {},
401 errors=errors.error_dict or {},
402 prefix_error=False,
402 prefix_error=False,
403 encoding="UTF-8",
403 encoding="UTF-8",
404 force_defaults=False
404 force_defaults=False
405 )
405 )
406 return Response(html)
406 return Response(html)
407
407
408 try:
408 try:
409 settings = [
409 settings = [
410 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
410 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
411 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
411 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
412 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
412 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
413 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
413 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
414 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
414 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
415 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
415 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
416 ('show_version', 'rhodecode_show_version', 'bool'),
416 ('show_version', 'rhodecode_show_version', 'bool'),
417 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
417 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
418 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
418 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
419 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
419 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
420 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
420 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
421 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
421 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
422 ('support_url', 'rhodecode_support_url', 'unicode'),
422 ('support_url', 'rhodecode_support_url', 'unicode'),
423 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
423 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
424 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
424 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
425 ]
425 ]
426 for setting, form_key, type_ in settings:
426 for setting, form_key, type_ in settings:
427 sett = SettingsModel().create_or_update_setting(
427 sett = SettingsModel().create_or_update_setting(
428 setting, form_result[form_key], type_)
428 setting, form_result[form_key], type_)
429 Session().add(sett)
429 Session().add(sett)
430
430
431 Session().commit()
431 Session().commit()
432 SettingsModel().invalidate_settings_cache()
432 SettingsModel().invalidate_settings_cache()
433 h.flash(_('Updated visualisation settings'), category='success')
433 h.flash(_('Updated visualisation settings'), category='success')
434 except Exception:
434 except Exception:
435 log.exception("Exception updating visualization settings")
435 log.exception("Exception updating visualization settings")
436 h.flash(_('Error occurred during updating '
436 h.flash(_('Error occurred during updating '
437 'visualisation settings'),
437 'visualisation settings'),
438 category='error')
438 category='error')
439
439
440 raise HTTPFound(h.route_path('admin_settings_visual'))
440 raise HTTPFound(h.route_path('admin_settings_visual'))
441
441
442 @LoginRequired()
442 @LoginRequired()
443 @HasPermissionAllDecorator('hg.admin')
443 @HasPermissionAllDecorator('hg.admin')
444 @view_config(
444 @view_config(
445 route_name='admin_settings_issuetracker', request_method='GET',
445 route_name='admin_settings_issuetracker', request_method='GET',
446 renderer='rhodecode:templates/admin/settings/settings.mako')
446 renderer='rhodecode:templates/admin/settings/settings.mako')
447 def settings_issuetracker(self):
447 def settings_issuetracker(self):
448 c = self.load_default_context()
448 c = self.load_default_context()
449 c.active = 'issuetracker'
449 c.active = 'issuetracker'
450 defaults = c.rc_config
450 defaults = c.rc_config
451
451
452 entry_key = 'rhodecode_issuetracker_pat_'
452 entry_key = 'rhodecode_issuetracker_pat_'
453
453
454 c.issuetracker_entries = {}
454 c.issuetracker_entries = {}
455 for k, v in defaults.items():
455 for k, v in defaults.items():
456 if k.startswith(entry_key):
456 if k.startswith(entry_key):
457 uid = k[len(entry_key):]
457 uid = k[len(entry_key):]
458 c.issuetracker_entries[uid] = None
458 c.issuetracker_entries[uid] = None
459
459
460 for uid in c.issuetracker_entries:
460 for uid in c.issuetracker_entries:
461 c.issuetracker_entries[uid] = AttributeDict({
461 c.issuetracker_entries[uid] = AttributeDict({
462 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
462 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
463 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
463 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
464 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
464 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
465 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
465 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
466 })
466 })
467
467
468 return self._get_template_context(c)
468 return self._get_template_context(c)
469
469
470 @LoginRequired()
470 @LoginRequired()
471 @HasPermissionAllDecorator('hg.admin')
471 @HasPermissionAllDecorator('hg.admin')
472 @CSRFRequired()
472 @CSRFRequired()
473 @view_config(
473 @view_config(
474 route_name='admin_settings_issuetracker_test', request_method='POST',
474 route_name='admin_settings_issuetracker_test', request_method='POST',
475 renderer='string', xhr=True)
475 renderer='string', xhr=True)
476 def settings_issuetracker_test(self):
476 def settings_issuetracker_test(self):
477 return h.urlify_commit_message(
477 return h.urlify_commit_message(
478 self.request.POST.get('test_text', ''),
478 self.request.POST.get('test_text', ''),
479 'repo_group/test_repo1')
479 'repo_group/test_repo1')
480
480
481 @LoginRequired()
481 @LoginRequired()
482 @HasPermissionAllDecorator('hg.admin')
482 @HasPermissionAllDecorator('hg.admin')
483 @CSRFRequired()
483 @CSRFRequired()
484 @view_config(
484 @view_config(
485 route_name='admin_settings_issuetracker_update', request_method='POST',
485 route_name='admin_settings_issuetracker_update', request_method='POST',
486 renderer='rhodecode:templates/admin/settings/settings.mako')
486 renderer='rhodecode:templates/admin/settings/settings.mako')
487 def settings_issuetracker_update(self):
487 def settings_issuetracker_update(self):
488 _ = self.request.translate
488 _ = self.request.translate
489 self.load_default_context()
489 self.load_default_context()
490 settings_model = IssueTrackerSettingsModel()
490 settings_model = IssueTrackerSettingsModel()
491
491
492 try:
492 try:
493 form = IssueTrackerPatternsForm(self.request.translate)()
493 form = IssueTrackerPatternsForm(self.request.translate)()
494 data = form.to_python(self.request.POST)
494 data = form.to_python(self.request.POST)
495 except formencode.Invalid as errors:
495 except formencode.Invalid as errors:
496 log.exception('Failed to add new pattern')
496 log.exception('Failed to add new pattern')
497 error = errors
497 error = errors
498 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
498 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
499 category='error')
499 category='error')
500 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
500 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
501
501
502 if data:
502 if data:
503 for uid in data.get('delete_patterns', []):
503 for uid in data.get('delete_patterns', []):
504 settings_model.delete_entries(uid)
504 settings_model.delete_entries(uid)
505
505
506 for pattern in data.get('patterns', []):
506 for pattern in data.get('patterns', []):
507 for setting, value, type_ in pattern:
507 for setting, value, type_ in pattern:
508 sett = settings_model.create_or_update_setting(
508 sett = settings_model.create_or_update_setting(
509 setting, value, type_)
509 setting, value, type_)
510 Session().add(sett)
510 Session().add(sett)
511
511
512 Session().commit()
512 Session().commit()
513
513
514 SettingsModel().invalidate_settings_cache()
514 SettingsModel().invalidate_settings_cache()
515 h.flash(_('Updated issue tracker entries'), category='success')
515 h.flash(_('Updated issue tracker entries'), category='success')
516 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
516 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
517
517
518 @LoginRequired()
518 @LoginRequired()
519 @HasPermissionAllDecorator('hg.admin')
519 @HasPermissionAllDecorator('hg.admin')
520 @CSRFRequired()
520 @CSRFRequired()
521 @view_config(
521 @view_config(
522 route_name='admin_settings_issuetracker_delete', request_method='POST',
522 route_name='admin_settings_issuetracker_delete', request_method='POST',
523 renderer='json_ext', xhr=True)
523 renderer='json_ext', xhr=True)
524 def settings_issuetracker_delete(self):
524 def settings_issuetracker_delete(self):
525 _ = self.request.translate
525 _ = self.request.translate
526 self.load_default_context()
526 self.load_default_context()
527 uid = self.request.POST.get('uid')
527 uid = self.request.POST.get('uid')
528 try:
528 try:
529 IssueTrackerSettingsModel().delete_entries(uid)
529 IssueTrackerSettingsModel().delete_entries(uid)
530 except Exception:
530 except Exception:
531 log.exception('Failed to delete issue tracker setting %s', uid)
531 log.exception('Failed to delete issue tracker setting %s', uid)
532 raise HTTPNotFound()
532 raise HTTPNotFound()
533
533
534 SettingsModel().invalidate_settings_cache()
534 SettingsModel().invalidate_settings_cache()
535 h.flash(_('Removed issue tracker entry.'), category='success')
535 h.flash(_('Removed issue tracker entry.'), category='success')
536
536
537 return {'deleted': uid}
537 return {'deleted': uid}
538
538
539 @LoginRequired()
539 @LoginRequired()
540 @HasPermissionAllDecorator('hg.admin')
540 @HasPermissionAllDecorator('hg.admin')
541 @view_config(
541 @view_config(
542 route_name='admin_settings_email', request_method='GET',
542 route_name='admin_settings_email', request_method='GET',
543 renderer='rhodecode:templates/admin/settings/settings.mako')
543 renderer='rhodecode:templates/admin/settings/settings.mako')
544 def settings_email(self):
544 def settings_email(self):
545 c = self.load_default_context()
545 c = self.load_default_context()
546 c.active = 'email'
546 c.active = 'email'
547 c.rhodecode_ini = rhodecode.CONFIG
547 c.rhodecode_ini = rhodecode.CONFIG
548
548
549 data = render('rhodecode:templates/admin/settings/settings.mako',
549 data = render('rhodecode:templates/admin/settings/settings.mako',
550 self._get_template_context(c), self.request)
550 self._get_template_context(c), self.request)
551 html = formencode.htmlfill.render(
551 html = formencode.htmlfill.render(
552 data,
552 data,
553 defaults=self._form_defaults(),
553 defaults=self._form_defaults(),
554 encoding="UTF-8",
554 encoding="UTF-8",
555 force_defaults=False
555 force_defaults=False
556 )
556 )
557 return Response(html)
557 return Response(html)
558
558
559 @LoginRequired()
559 @LoginRequired()
560 @HasPermissionAllDecorator('hg.admin')
560 @HasPermissionAllDecorator('hg.admin')
561 @CSRFRequired()
561 @CSRFRequired()
562 @view_config(
562 @view_config(
563 route_name='admin_settings_email_update', request_method='POST',
563 route_name='admin_settings_email_update', request_method='POST',
564 renderer='rhodecode:templates/admin/settings/settings.mako')
564 renderer='rhodecode:templates/admin/settings/settings.mako')
565 def settings_email_update(self):
565 def settings_email_update(self):
566 _ = self.request.translate
566 _ = self.request.translate
567 c = self.load_default_context()
567 c = self.load_default_context()
568 c.active = 'email'
568 c.active = 'email'
569
569
570 test_email = self.request.POST.get('test_email')
570 test_email = self.request.POST.get('test_email')
571
571
572 if not test_email:
572 if not test_email:
573 h.flash(_('Please enter email address'), category='error')
573 h.flash(_('Please enter email address'), category='error')
574 raise HTTPFound(h.route_path('admin_settings_email'))
574 raise HTTPFound(h.route_path('admin_settings_email'))
575
575
576 email_kwargs = {
576 email_kwargs = {
577 'date': datetime.datetime.now(),
577 'date': datetime.datetime.now(),
578 'user': self._rhodecode_db_user
578 'user': self._rhodecode_db_user
579 }
579 }
580
580
581 (subject, headers, email_body,
581 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
582 email_body_plaintext) = EmailNotificationModel().render_email(
583 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
582 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
584
583
585 recipients = [test_email] if test_email else None
584 recipients = [test_email] if test_email else None
586
585
587 run_task(tasks.send_email, recipients, subject,
586 run_task(tasks.send_email, recipients, subject,
588 email_body_plaintext, email_body)
587 email_body_plaintext, email_body)
589
588
590 h.flash(_('Send email task created'), category='success')
589 h.flash(_('Send email task created'), category='success')
591 raise HTTPFound(h.route_path('admin_settings_email'))
590 raise HTTPFound(h.route_path('admin_settings_email'))
592
591
593 @LoginRequired()
592 @LoginRequired()
594 @HasPermissionAllDecorator('hg.admin')
593 @HasPermissionAllDecorator('hg.admin')
595 @view_config(
594 @view_config(
596 route_name='admin_settings_hooks', request_method='GET',
595 route_name='admin_settings_hooks', request_method='GET',
597 renderer='rhodecode:templates/admin/settings/settings.mako')
596 renderer='rhodecode:templates/admin/settings/settings.mako')
598 def settings_hooks(self):
597 def settings_hooks(self):
599 c = self.load_default_context()
598 c = self.load_default_context()
600 c.active = 'hooks'
599 c.active = 'hooks'
601
600
602 model = SettingsModel()
601 model = SettingsModel()
603 c.hooks = model.get_builtin_hooks()
602 c.hooks = model.get_builtin_hooks()
604 c.custom_hooks = model.get_custom_hooks()
603 c.custom_hooks = model.get_custom_hooks()
605
604
606 data = render('rhodecode:templates/admin/settings/settings.mako',
605 data = render('rhodecode:templates/admin/settings/settings.mako',
607 self._get_template_context(c), self.request)
606 self._get_template_context(c), self.request)
608 html = formencode.htmlfill.render(
607 html = formencode.htmlfill.render(
609 data,
608 data,
610 defaults=self._form_defaults(),
609 defaults=self._form_defaults(),
611 encoding="UTF-8",
610 encoding="UTF-8",
612 force_defaults=False
611 force_defaults=False
613 )
612 )
614 return Response(html)
613 return Response(html)
615
614
616 @LoginRequired()
615 @LoginRequired()
617 @HasPermissionAllDecorator('hg.admin')
616 @HasPermissionAllDecorator('hg.admin')
618 @CSRFRequired()
617 @CSRFRequired()
619 @view_config(
618 @view_config(
620 route_name='admin_settings_hooks_update', request_method='POST',
619 route_name='admin_settings_hooks_update', request_method='POST',
621 renderer='rhodecode:templates/admin/settings/settings.mako')
620 renderer='rhodecode:templates/admin/settings/settings.mako')
622 @view_config(
621 @view_config(
623 route_name='admin_settings_hooks_delete', request_method='POST',
622 route_name='admin_settings_hooks_delete', request_method='POST',
624 renderer='rhodecode:templates/admin/settings/settings.mako')
623 renderer='rhodecode:templates/admin/settings/settings.mako')
625 def settings_hooks_update(self):
624 def settings_hooks_update(self):
626 _ = self.request.translate
625 _ = self.request.translate
627 c = self.load_default_context()
626 c = self.load_default_context()
628 c.active = 'hooks'
627 c.active = 'hooks'
629 if c.visual.allow_custom_hooks_settings:
628 if c.visual.allow_custom_hooks_settings:
630 ui_key = self.request.POST.get('new_hook_ui_key')
629 ui_key = self.request.POST.get('new_hook_ui_key')
631 ui_value = self.request.POST.get('new_hook_ui_value')
630 ui_value = self.request.POST.get('new_hook_ui_value')
632
631
633 hook_id = self.request.POST.get('hook_id')
632 hook_id = self.request.POST.get('hook_id')
634 new_hook = False
633 new_hook = False
635
634
636 model = SettingsModel()
635 model = SettingsModel()
637 try:
636 try:
638 if ui_value and ui_key:
637 if ui_value and ui_key:
639 model.create_or_update_hook(ui_key, ui_value)
638 model.create_or_update_hook(ui_key, ui_value)
640 h.flash(_('Added new hook'), category='success')
639 h.flash(_('Added new hook'), category='success')
641 new_hook = True
640 new_hook = True
642 elif hook_id:
641 elif hook_id:
643 RhodeCodeUi.delete(hook_id)
642 RhodeCodeUi.delete(hook_id)
644 Session().commit()
643 Session().commit()
645
644
646 # check for edits
645 # check for edits
647 update = False
646 update = False
648 _d = self.request.POST.dict_of_lists()
647 _d = self.request.POST.dict_of_lists()
649 for k, v in zip(_d.get('hook_ui_key', []),
648 for k, v in zip(_d.get('hook_ui_key', []),
650 _d.get('hook_ui_value_new', [])):
649 _d.get('hook_ui_value_new', [])):
651 model.create_or_update_hook(k, v)
650 model.create_or_update_hook(k, v)
652 update = True
651 update = True
653
652
654 if update and not new_hook:
653 if update and not new_hook:
655 h.flash(_('Updated hooks'), category='success')
654 h.flash(_('Updated hooks'), category='success')
656 Session().commit()
655 Session().commit()
657 except Exception:
656 except Exception:
658 log.exception("Exception during hook creation")
657 log.exception("Exception during hook creation")
659 h.flash(_('Error occurred during hook creation'),
658 h.flash(_('Error occurred during hook creation'),
660 category='error')
659 category='error')
661
660
662 raise HTTPFound(h.route_path('admin_settings_hooks'))
661 raise HTTPFound(h.route_path('admin_settings_hooks'))
663
662
664 @LoginRequired()
663 @LoginRequired()
665 @HasPermissionAllDecorator('hg.admin')
664 @HasPermissionAllDecorator('hg.admin')
666 @view_config(
665 @view_config(
667 route_name='admin_settings_search', request_method='GET',
666 route_name='admin_settings_search', request_method='GET',
668 renderer='rhodecode:templates/admin/settings/settings.mako')
667 renderer='rhodecode:templates/admin/settings/settings.mako')
669 def settings_search(self):
668 def settings_search(self):
670 c = self.load_default_context()
669 c = self.load_default_context()
671 c.active = 'search'
670 c.active = 'search'
672
671
673 c.searcher = searcher_from_config(self.request.registry.settings)
672 c.searcher = searcher_from_config(self.request.registry.settings)
674 c.statistics = c.searcher.statistics(self.request.translate)
673 c.statistics = c.searcher.statistics(self.request.translate)
675
674
676 return self._get_template_context(c)
675 return self._get_template_context(c)
677
676
678 @LoginRequired()
677 @LoginRequired()
679 @HasPermissionAllDecorator('hg.admin')
678 @HasPermissionAllDecorator('hg.admin')
680 @view_config(
679 @view_config(
681 route_name='admin_settings_automation', request_method='GET',
680 route_name='admin_settings_automation', request_method='GET',
682 renderer='rhodecode:templates/admin/settings/settings.mako')
681 renderer='rhodecode:templates/admin/settings/settings.mako')
683 def settings_automation(self):
682 def settings_automation(self):
684 c = self.load_default_context()
683 c = self.load_default_context()
685 c.active = 'automation'
684 c.active = 'automation'
686
685
687 return self._get_template_context(c)
686 return self._get_template_context(c)
688
687
689 @LoginRequired()
688 @LoginRequired()
690 @HasPermissionAllDecorator('hg.admin')
689 @HasPermissionAllDecorator('hg.admin')
691 @view_config(
690 @view_config(
692 route_name='admin_settings_labs', request_method='GET',
691 route_name='admin_settings_labs', request_method='GET',
693 renderer='rhodecode:templates/admin/settings/settings.mako')
692 renderer='rhodecode:templates/admin/settings/settings.mako')
694 def settings_labs(self):
693 def settings_labs(self):
695 c = self.load_default_context()
694 c = self.load_default_context()
696 if not c.labs_active:
695 if not c.labs_active:
697 raise HTTPFound(h.route_path('admin_settings'))
696 raise HTTPFound(h.route_path('admin_settings'))
698
697
699 c.active = 'labs'
698 c.active = 'labs'
700 c.lab_settings = _LAB_SETTINGS
699 c.lab_settings = _LAB_SETTINGS
701
700
702 data = render('rhodecode:templates/admin/settings/settings.mako',
701 data = render('rhodecode:templates/admin/settings/settings.mako',
703 self._get_template_context(c), self.request)
702 self._get_template_context(c), self.request)
704 html = formencode.htmlfill.render(
703 html = formencode.htmlfill.render(
705 data,
704 data,
706 defaults=self._form_defaults(),
705 defaults=self._form_defaults(),
707 encoding="UTF-8",
706 encoding="UTF-8",
708 force_defaults=False
707 force_defaults=False
709 )
708 )
710 return Response(html)
709 return Response(html)
711
710
712 @LoginRequired()
711 @LoginRequired()
713 @HasPermissionAllDecorator('hg.admin')
712 @HasPermissionAllDecorator('hg.admin')
714 @CSRFRequired()
713 @CSRFRequired()
715 @view_config(
714 @view_config(
716 route_name='admin_settings_labs_update', request_method='POST',
715 route_name='admin_settings_labs_update', request_method='POST',
717 renderer='rhodecode:templates/admin/settings/settings.mako')
716 renderer='rhodecode:templates/admin/settings/settings.mako')
718 def settings_labs_update(self):
717 def settings_labs_update(self):
719 _ = self.request.translate
718 _ = self.request.translate
720 c = self.load_default_context()
719 c = self.load_default_context()
721 c.active = 'labs'
720 c.active = 'labs'
722
721
723 application_form = LabsSettingsForm(self.request.translate)()
722 application_form = LabsSettingsForm(self.request.translate)()
724 try:
723 try:
725 form_result = application_form.to_python(dict(self.request.POST))
724 form_result = application_form.to_python(dict(self.request.POST))
726 except formencode.Invalid as errors:
725 except formencode.Invalid as errors:
727 h.flash(
726 h.flash(
728 _("Some form inputs contain invalid data."),
727 _("Some form inputs contain invalid data."),
729 category='error')
728 category='error')
730 data = render('rhodecode:templates/admin/settings/settings.mako',
729 data = render('rhodecode:templates/admin/settings/settings.mako',
731 self._get_template_context(c), self.request)
730 self._get_template_context(c), self.request)
732 html = formencode.htmlfill.render(
731 html = formencode.htmlfill.render(
733 data,
732 data,
734 defaults=errors.value,
733 defaults=errors.value,
735 errors=errors.error_dict or {},
734 errors=errors.error_dict or {},
736 prefix_error=False,
735 prefix_error=False,
737 encoding="UTF-8",
736 encoding="UTF-8",
738 force_defaults=False
737 force_defaults=False
739 )
738 )
740 return Response(html)
739 return Response(html)
741
740
742 try:
741 try:
743 session = Session()
742 session = Session()
744 for setting in _LAB_SETTINGS:
743 for setting in _LAB_SETTINGS:
745 setting_name = setting.key[len('rhodecode_'):]
744 setting_name = setting.key[len('rhodecode_'):]
746 sett = SettingsModel().create_or_update_setting(
745 sett = SettingsModel().create_or_update_setting(
747 setting_name, form_result[setting.key], setting.type)
746 setting_name, form_result[setting.key], setting.type)
748 session.add(sett)
747 session.add(sett)
749
748
750 except Exception:
749 except Exception:
751 log.exception('Exception while updating lab settings')
750 log.exception('Exception while updating lab settings')
752 h.flash(_('Error occurred during updating labs settings'),
751 h.flash(_('Error occurred during updating labs settings'),
753 category='error')
752 category='error')
754 else:
753 else:
755 Session().commit()
754 Session().commit()
756 SettingsModel().invalidate_settings_cache()
755 SettingsModel().invalidate_settings_cache()
757 h.flash(_('Updated Labs settings'), category='success')
756 h.flash(_('Updated Labs settings'), category='success')
758 raise HTTPFound(h.route_path('admin_settings_labs'))
757 raise HTTPFound(h.route_path('admin_settings_labs'))
759
758
760 data = render('rhodecode:templates/admin/settings/settings.mako',
759 data = render('rhodecode:templates/admin/settings/settings.mako',
761 self._get_template_context(c), self.request)
760 self._get_template_context(c), self.request)
762 html = formencode.htmlfill.render(
761 html = formencode.htmlfill.render(
763 data,
762 data,
764 defaults=self._form_defaults(),
763 defaults=self._form_defaults(),
765 encoding="UTF-8",
764 encoding="UTF-8",
766 force_defaults=False
765 force_defaults=False
767 )
766 )
768 return Response(html)
767 return Response(html)
769
768
770
769
771 # :param key: name of the setting including the 'rhodecode_' prefix
770 # :param key: name of the setting including the 'rhodecode_' prefix
772 # :param type: the RhodeCodeSetting type to use.
771 # :param type: the RhodeCodeSetting type to use.
773 # :param group: the i18ned group in which we should dispaly this setting
772 # :param group: the i18ned group in which we should dispaly this setting
774 # :param label: the i18ned label we should display for this setting
773 # :param label: the i18ned label we should display for this setting
775 # :param help: the i18ned help we should dispaly for this setting
774 # :param help: the i18ned help we should dispaly for this setting
776 LabSetting = collections.namedtuple(
775 LabSetting = collections.namedtuple(
777 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
776 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
778
777
779
778
780 # This list has to be kept in sync with the form
779 # This list has to be kept in sync with the form
781 # rhodecode.model.forms.LabsSettingsForm.
780 # rhodecode.model.forms.LabsSettingsForm.
782 _LAB_SETTINGS = [
781 _LAB_SETTINGS = [
783
782
784 ]
783 ]
@@ -1,419 +1,418 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.view import view_config
25 from pyramid.view import view_config
26 from pyramid.renderers import render_to_response
26 from pyramid.renderers import render_to_response
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib.celerylib import run_task, tasks
28 from rhodecode.lib.celerylib import run_task, tasks
29 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.lib.utils2 import AttributeDict
30 from rhodecode.model.db import User
30 from rhodecode.model.db import User
31 from rhodecode.model.notification import EmailNotificationModel
31 from rhodecode.model.notification import EmailNotificationModel
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class DebugStyleView(BaseAppView):
36 class DebugStyleView(BaseAppView):
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
39
40 return c
40 return c
41
41
42 @view_config(
42 @view_config(
43 route_name='debug_style_home', request_method='GET',
43 route_name='debug_style_home', request_method='GET',
44 renderer=None)
44 renderer=None)
45 def index(self):
45 def index(self):
46 c = self.load_default_context()
46 c = self.load_default_context()
47 c.active = 'index'
47 c.active = 'index'
48
48
49 return render_to_response(
49 return render_to_response(
50 'debug_style/index.html', self._get_template_context(c),
50 'debug_style/index.html', self._get_template_context(c),
51 request=self.request)
51 request=self.request)
52
52
53 @view_config(
53 @view_config(
54 route_name='debug_style_email', request_method='GET',
54 route_name='debug_style_email', request_method='GET',
55 renderer=None)
55 renderer=None)
56 @view_config(
56 @view_config(
57 route_name='debug_style_email_plain_rendered', request_method='GET',
57 route_name='debug_style_email_plain_rendered', request_method='GET',
58 renderer=None)
58 renderer=None)
59 def render_email(self):
59 def render_email(self):
60 c = self.load_default_context()
60 c = self.load_default_context()
61 email_id = self.request.matchdict['email_id']
61 email_id = self.request.matchdict['email_id']
62 c.active = 'emails'
62 c.active = 'emails'
63
63
64 pr = AttributeDict(
64 pr = AttributeDict(
65 pull_request_id=123,
65 pull_request_id=123,
66 title='digital_ocean: fix redis, elastic search start on boot, '
66 title='digital_ocean: fix redis, elastic search start on boot, '
67 'fix fd limits on supervisor, set postgres 11 version',
67 'fix fd limits on supervisor, set postgres 11 version',
68 description='''
68 description='''
69 Check if we should use full-topic or mini-topic.
69 Check if we should use full-topic or mini-topic.
70
70
71 - full topic produces some problems with merge states etc
71 - full topic produces some problems with merge states etc
72 - server-mini-topic needs probably tweeks.
72 - server-mini-topic needs probably tweeks.
73 ''',
73 ''',
74 repo_name='foobar',
74 repo_name='foobar',
75 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
75 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 target_ref_parts=AttributeDict(type='branch', name='master'),
76 target_ref_parts=AttributeDict(type='branch', name='master'),
77 )
77 )
78 target_repo = AttributeDict(repo_name='repo_group/target_repo')
78 target_repo = AttributeDict(repo_name='repo_group/target_repo')
79 source_repo = AttributeDict(repo_name='repo_group/source_repo')
79 source_repo = AttributeDict(repo_name='repo_group/source_repo')
80 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
80 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
81 # file/commit changes for PR update
81 # file/commit changes for PR update
82 commit_changes = AttributeDict({
82 commit_changes = AttributeDict({
83 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
83 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
84 'removed': ['eeeeeeeeeee'],
84 'removed': ['eeeeeeeeeee'],
85 })
85 })
86 file_changes = AttributeDict({
86 file_changes = AttributeDict({
87 'added': ['a/file1.md', 'file2.py'],
87 'added': ['a/file1.md', 'file2.py'],
88 'modified': ['b/modified_file.rst'],
88 'modified': ['b/modified_file.rst'],
89 'removed': ['.idea'],
89 'removed': ['.idea'],
90 })
90 })
91
91
92 exc_traceback = {
92 exc_traceback = {
93 'exc_utc_date': '2020-03-26T12:54:50.683281',
93 'exc_utc_date': '2020-03-26T12:54:50.683281',
94 'exc_id': 139638856342656,
94 'exc_id': 139638856342656,
95 'exc_timestamp': '1585227290.683288',
95 'exc_timestamp': '1585227290.683288',
96 'version': 'v1',
96 'version': 'v1',
97 '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',
97 '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',
98 'exc_type': 'AttributeError'
98 'exc_type': 'AttributeError'
99 }
99 }
100 email_kwargs = {
100 email_kwargs = {
101 'test': {},
101 'test': {},
102 'message': {
102 'message': {
103 'body': 'message body !'
103 'body': 'message body !'
104 },
104 },
105 'email_test': {
105 'email_test': {
106 'user': user,
106 'user': user,
107 'date': datetime.datetime.now(),
107 'date': datetime.datetime.now(),
108 },
108 },
109 'exception': {
109 'exception': {
110 'email_prefix': '[RHODECODE ERROR]',
110 'email_prefix': '[RHODECODE ERROR]',
111 'exc_id': exc_traceback['exc_id'],
111 'exc_id': exc_traceback['exc_id'],
112 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
112 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
113 'exc_type_name': 'NameError',
113 'exc_type_name': 'NameError',
114 'exc_traceback': exc_traceback,
114 'exc_traceback': exc_traceback,
115 },
115 },
116 'password_reset': {
116 'password_reset': {
117 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
117 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
118
118
119 'user': user,
119 'user': user,
120 'date': datetime.datetime.now(),
120 'date': datetime.datetime.now(),
121 'email': 'test@rhodecode.com',
121 'email': 'test@rhodecode.com',
122 'first_admin_email': User.get_first_super_admin().email
122 'first_admin_email': User.get_first_super_admin().email
123 },
123 },
124 'password_reset_confirmation': {
124 'password_reset_confirmation': {
125 'new_password': 'new-password-example',
125 'new_password': 'new-password-example',
126 'user': user,
126 'user': user,
127 'date': datetime.datetime.now(),
127 'date': datetime.datetime.now(),
128 'email': 'test@rhodecode.com',
128 'email': 'test@rhodecode.com',
129 'first_admin_email': User.get_first_super_admin().email
129 'first_admin_email': User.get_first_super_admin().email
130 },
130 },
131 'registration': {
131 'registration': {
132 'user': user,
132 'user': user,
133 'date': datetime.datetime.now(),
133 'date': datetime.datetime.now(),
134 },
134 },
135
135
136 'pull_request_comment': {
136 'pull_request_comment': {
137 'user': user,
137 'user': user,
138
138
139 'status_change': None,
139 'status_change': None,
140 'status_change_type': None,
140 'status_change_type': None,
141
141
142 'pull_request': pr,
142 'pull_request': pr,
143 'pull_request_commits': [],
143 'pull_request_commits': [],
144
144
145 'pull_request_target_repo': target_repo,
145 'pull_request_target_repo': target_repo,
146 'pull_request_target_repo_url': 'http://target-repo/url',
146 'pull_request_target_repo_url': 'http://target-repo/url',
147
147
148 'pull_request_source_repo': source_repo,
148 'pull_request_source_repo': source_repo,
149 'pull_request_source_repo_url': 'http://source-repo/url',
149 'pull_request_source_repo_url': 'http://source-repo/url',
150
150
151 'pull_request_url': 'http://localhost/pr1',
151 'pull_request_url': 'http://localhost/pr1',
152 'pr_comment_url': 'http://comment-url',
152 'pr_comment_url': 'http://comment-url',
153 'pr_comment_reply_url': 'http://comment-url#reply',
153 'pr_comment_reply_url': 'http://comment-url#reply',
154
154
155 'comment_file': None,
155 'comment_file': None,
156 'comment_line': None,
156 'comment_line': None,
157 'comment_type': 'note',
157 'comment_type': 'note',
158 'comment_body': 'This is my comment body. *I like !*',
158 'comment_body': 'This is my comment body. *I like !*',
159 'comment_id': 2048,
159 'comment_id': 2048,
160 'renderer_type': 'markdown',
160 'renderer_type': 'markdown',
161 'mention': True,
161 'mention': True,
162
162
163 },
163 },
164 'pull_request_comment+status': {
164 'pull_request_comment+status': {
165 'user': user,
165 'user': user,
166
166
167 'status_change': 'approved',
167 'status_change': 'approved',
168 'status_change_type': 'approved',
168 'status_change_type': 'approved',
169
169
170 'pull_request': pr,
170 'pull_request': pr,
171 'pull_request_commits': [],
171 'pull_request_commits': [],
172
172
173 'pull_request_target_repo': target_repo,
173 'pull_request_target_repo': target_repo,
174 'pull_request_target_repo_url': 'http://target-repo/url',
174 'pull_request_target_repo_url': 'http://target-repo/url',
175
175
176 'pull_request_source_repo': source_repo,
176 'pull_request_source_repo': source_repo,
177 'pull_request_source_repo_url': 'http://source-repo/url',
177 'pull_request_source_repo_url': 'http://source-repo/url',
178
178
179 'pull_request_url': 'http://localhost/pr1',
179 'pull_request_url': 'http://localhost/pr1',
180 'pr_comment_url': 'http://comment-url',
180 'pr_comment_url': 'http://comment-url',
181 'pr_comment_reply_url': 'http://comment-url#reply',
181 'pr_comment_reply_url': 'http://comment-url#reply',
182
182
183 'comment_type': 'todo',
183 'comment_type': 'todo',
184 'comment_file': None,
184 'comment_file': None,
185 'comment_line': None,
185 'comment_line': None,
186 'comment_body': '''
186 'comment_body': '''
187 I think something like this would be better
187 I think something like this would be better
188
188
189 ```py
189 ```py
190 // markdown renderer
190 // markdown renderer
191
191
192 def db():
192 def db():
193 global connection
193 global connection
194 return connection
194 return connection
195
195
196 ```
196 ```
197
197
198 ''',
198 ''',
199 'comment_id': 2048,
199 'comment_id': 2048,
200 'renderer_type': 'markdown',
200 'renderer_type': 'markdown',
201 'mention': True,
201 'mention': True,
202
202
203 },
203 },
204 'pull_request_comment+file': {
204 'pull_request_comment+file': {
205 'user': user,
205 'user': user,
206
206
207 'status_change': None,
207 'status_change': None,
208 'status_change_type': None,
208 'status_change_type': None,
209
209
210 'pull_request': pr,
210 'pull_request': pr,
211 'pull_request_commits': [],
211 'pull_request_commits': [],
212
212
213 'pull_request_target_repo': target_repo,
213 'pull_request_target_repo': target_repo,
214 'pull_request_target_repo_url': 'http://target-repo/url',
214 'pull_request_target_repo_url': 'http://target-repo/url',
215
215
216 'pull_request_source_repo': source_repo,
216 'pull_request_source_repo': source_repo,
217 'pull_request_source_repo_url': 'http://source-repo/url',
217 'pull_request_source_repo_url': 'http://source-repo/url',
218
218
219 'pull_request_url': 'http://localhost/pr1',
219 'pull_request_url': 'http://localhost/pr1',
220
220
221 'pr_comment_url': 'http://comment-url',
221 'pr_comment_url': 'http://comment-url',
222 'pr_comment_reply_url': 'http://comment-url#reply',
222 'pr_comment_reply_url': 'http://comment-url#reply',
223
223
224 'comment_file': 'rhodecode/model/get_flow_commits',
224 'comment_file': 'rhodecode/model/get_flow_commits',
225 'comment_line': 'o1210',
225 'comment_line': 'o1210',
226 'comment_type': 'todo',
226 'comment_type': 'todo',
227 'comment_body': '''
227 'comment_body': '''
228 I like this !
228 I like this !
229
229
230 But please check this code
230 But please check this code
231
231
232 .. code-block:: javascript
232 .. code-block:: javascript
233
233
234 // THIS IS RST CODE
234 // THIS IS RST CODE
235
235
236 this.createResolutionComment = function(commentId) {
236 this.createResolutionComment = function(commentId) {
237 // hide the trigger text
237 // hide the trigger text
238 $('#resolve-comment-{0}'.format(commentId)).hide();
238 $('#resolve-comment-{0}'.format(commentId)).hide();
239
239
240 var comment = $('#comment-'+commentId);
240 var comment = $('#comment-'+commentId);
241 var commentData = comment.data();
241 var commentData = comment.data();
242 if (commentData.commentInline) {
242 if (commentData.commentInline) {
243 this.createComment(comment, commentId)
243 this.createComment(comment, commentId)
244 } else {
244 } else {
245 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
245 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
246 }
246 }
247
247
248 return false;
248 return false;
249 };
249 };
250
250
251 This should work better !
251 This should work better !
252 ''',
252 ''',
253 'comment_id': 2048,
253 'comment_id': 2048,
254 'renderer_type': 'rst',
254 'renderer_type': 'rst',
255 'mention': True,
255 'mention': True,
256
256
257 },
257 },
258
258
259 'pull_request_update': {
259 'pull_request_update': {
260 'updating_user': user,
260 'updating_user': user,
261
261
262 'status_change': None,
262 'status_change': None,
263 'status_change_type': None,
263 'status_change_type': None,
264
264
265 'pull_request': pr,
265 'pull_request': pr,
266 'pull_request_commits': [],
266 'pull_request_commits': [],
267
267
268 'pull_request_target_repo': target_repo,
268 'pull_request_target_repo': target_repo,
269 'pull_request_target_repo_url': 'http://target-repo/url',
269 'pull_request_target_repo_url': 'http://target-repo/url',
270
270
271 'pull_request_source_repo': source_repo,
271 'pull_request_source_repo': source_repo,
272 'pull_request_source_repo_url': 'http://source-repo/url',
272 'pull_request_source_repo_url': 'http://source-repo/url',
273
273
274 'pull_request_url': 'http://localhost/pr1',
274 'pull_request_url': 'http://localhost/pr1',
275
275
276 # update comment links
276 # update comment links
277 'pr_comment_url': 'http://comment-url',
277 'pr_comment_url': 'http://comment-url',
278 'pr_comment_reply_url': 'http://comment-url#reply',
278 'pr_comment_reply_url': 'http://comment-url#reply',
279 'ancestor_commit_id': 'f39bd443',
279 'ancestor_commit_id': 'f39bd443',
280 'added_commits': commit_changes.added,
280 'added_commits': commit_changes.added,
281 'removed_commits': commit_changes.removed,
281 'removed_commits': commit_changes.removed,
282 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
282 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
283 'added_files': file_changes.added,
283 'added_files': file_changes.added,
284 'modified_files': file_changes.modified,
284 'modified_files': file_changes.modified,
285 'removed_files': file_changes.removed,
285 'removed_files': file_changes.removed,
286 },
286 },
287
287
288 'cs_comment': {
288 'cs_comment': {
289 'user': user,
289 'user': user,
290 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
290 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
291 'status_change': None,
291 'status_change': None,
292 'status_change_type': None,
292 'status_change_type': None,
293
293
294 'commit_target_repo_url': 'http://foo.example.com/#comment1',
294 'commit_target_repo_url': 'http://foo.example.com/#comment1',
295 'repo_name': 'test-repo',
295 'repo_name': 'test-repo',
296 'comment_type': 'note',
296 'comment_type': 'note',
297 'comment_file': None,
297 'comment_file': None,
298 'comment_line': None,
298 'comment_line': None,
299 'commit_comment_url': 'http://comment-url',
299 'commit_comment_url': 'http://comment-url',
300 'commit_comment_reply_url': 'http://comment-url#reply',
300 'commit_comment_reply_url': 'http://comment-url#reply',
301 'comment_body': 'This is my comment body. *I like !*',
301 'comment_body': 'This is my comment body. *I like !*',
302 'comment_id': 2048,
302 'comment_id': 2048,
303 'renderer_type': 'markdown',
303 'renderer_type': 'markdown',
304 'mention': True,
304 'mention': True,
305 },
305 },
306 'cs_comment+status': {
306 'cs_comment+status': {
307 'user': user,
307 'user': user,
308 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
308 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
309 'status_change': 'approved',
309 'status_change': 'approved',
310 'status_change_type': 'approved',
310 'status_change_type': 'approved',
311
311
312 'commit_target_repo_url': 'http://foo.example.com/#comment1',
312 'commit_target_repo_url': 'http://foo.example.com/#comment1',
313 'repo_name': 'test-repo',
313 'repo_name': 'test-repo',
314 'comment_type': 'note',
314 'comment_type': 'note',
315 'comment_file': None,
315 'comment_file': None,
316 'comment_line': None,
316 'comment_line': None,
317 'commit_comment_url': 'http://comment-url',
317 'commit_comment_url': 'http://comment-url',
318 'commit_comment_reply_url': 'http://comment-url#reply',
318 'commit_comment_reply_url': 'http://comment-url#reply',
319 'comment_body': '''
319 'comment_body': '''
320 Hello **world**
320 Hello **world**
321
321
322 This is a multiline comment :)
322 This is a multiline comment :)
323
323
324 - list
324 - list
325 - list2
325 - list2
326 ''',
326 ''',
327 'comment_id': 2048,
327 'comment_id': 2048,
328 'renderer_type': 'markdown',
328 'renderer_type': 'markdown',
329 'mention': True,
329 'mention': True,
330 },
330 },
331 'cs_comment+file': {
331 'cs_comment+file': {
332 'user': user,
332 'user': user,
333 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
333 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
334 'status_change': None,
334 'status_change': None,
335 'status_change_type': None,
335 'status_change_type': None,
336
336
337 'commit_target_repo_url': 'http://foo.example.com/#comment1',
337 'commit_target_repo_url': 'http://foo.example.com/#comment1',
338 'repo_name': 'test-repo',
338 'repo_name': 'test-repo',
339
339
340 'comment_type': 'note',
340 'comment_type': 'note',
341 'comment_file': 'test-file.py',
341 'comment_file': 'test-file.py',
342 'comment_line': 'n100',
342 'comment_line': 'n100',
343
343
344 'commit_comment_url': 'http://comment-url',
344 'commit_comment_url': 'http://comment-url',
345 'commit_comment_reply_url': 'http://comment-url#reply',
345 'commit_comment_reply_url': 'http://comment-url#reply',
346 'comment_body': 'This is my comment body. *I like !*',
346 'comment_body': 'This is my comment body. *I like !*',
347 'comment_id': 2048,
347 'comment_id': 2048,
348 'renderer_type': 'markdown',
348 'renderer_type': 'markdown',
349 'mention': True,
349 'mention': True,
350 },
350 },
351
351
352 'pull_request': {
352 'pull_request': {
353 'user': user,
353 'user': user,
354 'pull_request': pr,
354 'pull_request': pr,
355 'pull_request_commits': [
355 'pull_request_commits': [
356 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
356 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
357 my-account: moved email closer to profile as it's similar data just moved outside.
357 my-account: moved email closer to profile as it's similar data just moved outside.
358 '''),
358 '''),
359 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
359 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
360 users: description edit fixes
360 users: description edit fixes
361
361
362 - tests
362 - tests
363 - added metatags info
363 - added metatags info
364 '''),
364 '''),
365 ],
365 ],
366
366
367 'pull_request_target_repo': target_repo,
367 'pull_request_target_repo': target_repo,
368 'pull_request_target_repo_url': 'http://target-repo/url',
368 'pull_request_target_repo_url': 'http://target-repo/url',
369
369
370 'pull_request_source_repo': source_repo,
370 'pull_request_source_repo': source_repo,
371 'pull_request_source_repo_url': 'http://source-repo/url',
371 'pull_request_source_repo_url': 'http://source-repo/url',
372
372
373 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
373 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
374 }
374 }
375
375
376 }
376 }
377
377
378 template_type = email_id.split('+')[0]
378 template_type = email_id.split('+')[0]
379 (c.subject, c.headers, c.email_body,
379 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
380 c.email_body_plaintext) = EmailNotificationModel().render_email(
381 template_type, **email_kwargs.get(email_id, {}))
380 template_type, **email_kwargs.get(email_id, {}))
382
381
383 test_email = self.request.GET.get('email')
382 test_email = self.request.GET.get('email')
384 if test_email:
383 if test_email:
385 recipients = [test_email]
384 recipients = [test_email]
386 run_task(tasks.send_email, recipients, c.subject,
385 run_task(tasks.send_email, recipients, c.subject,
387 c.email_body_plaintext, c.email_body)
386 c.email_body_plaintext, c.email_body)
388
387
389 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
388 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
390 template = 'debug_style/email_plain_rendered.mako'
389 template = 'debug_style/email_plain_rendered.mako'
391 else:
390 else:
392 template = 'debug_style/email.mako'
391 template = 'debug_style/email.mako'
393 return render_to_response(
392 return render_to_response(
394 template, self._get_template_context(c),
393 template, self._get_template_context(c),
395 request=self.request)
394 request=self.request)
396
395
397 @view_config(
396 @view_config(
398 route_name='debug_style_template', request_method='GET',
397 route_name='debug_style_template', request_method='GET',
399 renderer=None)
398 renderer=None)
400 def template(self):
399 def template(self):
401 t_path = self.request.matchdict['t_path']
400 t_path = self.request.matchdict['t_path']
402 c = self.load_default_context()
401 c = self.load_default_context()
403 c.active = os.path.splitext(t_path)[0]
402 c.active = os.path.splitext(t_path)[0]
404 c.came_from = ''
403 c.came_from = ''
405 c.email_types = {
404 c.email_types = {
406 'cs_comment+file': {},
405 'cs_comment+file': {},
407 'cs_comment+status': {},
406 'cs_comment+status': {},
408
407
409 'pull_request_comment+file': {},
408 'pull_request_comment+file': {},
410 'pull_request_comment+status': {},
409 'pull_request_comment+status': {},
411
410
412 'pull_request_update': {},
411 'pull_request_update': {},
413 }
412 }
414 c.email_types.update(EmailNotificationModel.email_types)
413 c.email_types.update(EmailNotificationModel.email_types)
415
414
416 return render_to_response(
415 return render_to_response(
417 'debug_style/' + t_path, self._get_template_context(c),
416 'debug_style/' + t_path, self._get_template_context(c),
418 request=self.request)
417 request=self.request)
419
418
@@ -1,366 +1,379 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
33
33 import rhodecode
34 import rhodecode
34 from rhodecode.lib import audit_logger
35 from rhodecode.lib import audit_logger
35 from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask
36 from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask
36 from rhodecode.lib import hooks_base
37 from rhodecode.lib import hooks_base
37 from rhodecode.lib.utils2 import safe_int, str2bool
38 from rhodecode.lib.utils2 import safe_int, str2bool
38 from rhodecode.model.db import (
39 from rhodecode.model.db import (
39 Session, IntegrityError, true, Repository, RepoGroup, User)
40 Session, IntegrityError, true, Repository, RepoGroup, User)
40
41
41
42
42 @async_task(ignore_result=True, base=RequestContextTask)
43 @async_task(ignore_result=True, base=RequestContextTask)
43 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):
44 """
46 """
45 Sends an email with defined parameters from the .ini files.
47 Sends an email with defined parameters from the .ini files.
46
48
47 :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
48 address from field 'email_to' is used instead
50 address from field 'email_to' is used instead
49 :param subject: subject of the mail
51 :param subject: subject of the mail
50 :param body: body of the mail
52 :param body: body of the mail
51 :param html_body: html version of body
53 :param html_body: html version of body
52 :param email_config: specify custom configuration for mailer
54 :param email_config: specify custom configuration for mailer
55 :param extra_headers: specify custom headers
53 """
56 """
54 log = get_logger(send_email)
57 log = get_logger(send_email)
55
58
56 email_config = email_config or rhodecode.CONFIG
59 email_config = email_config or rhodecode.CONFIG
57
60
58 mail_server = email_config.get('smtp_server') or None
61 mail_server = email_config.get('smtp_server') or None
59 if mail_server is None:
62 if mail_server is None:
60 log.error("SMTP server information missing. Sending email failed. "
63 log.error("SMTP server information missing. Sending email failed. "
61 "Make sure that `smtp_server` variable is configured "
64 "Make sure that `smtp_server` variable is configured "
62 "inside the .ini file")
65 "inside the .ini file")
63 return False
66 return False
64
67
65 subject = "%s %s" % (email_config.get('email_prefix', ''), subject)
68 subject = "%s %s" % (email_config.get('email_prefix', ''), subject)
66
69
67 if recipients:
70 if recipients:
68 if isinstance(recipients, compat.string_types):
71 if isinstance(recipients, compat.string_types):
69 recipients = recipients.split(',')
72 recipients = recipients.split(',')
70 else:
73 else:
71 # 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
72 admins = []
75 admins = []
73 for u in User.query().filter(User.admin == true()).all():
76 for u in User.query().filter(User.admin == true()).all():
74 if u.email:
77 if u.email:
75 admins.append(u.email)
78 admins.append(u.email)
76 recipients = []
79 recipients = []
77 config_email = email_config.get('email_to')
80 config_email = email_config.get('email_to')
78 if config_email:
81 if config_email:
79 recipients += [config_email]
82 recipients += [config_email]
80 recipients += admins
83 recipients += admins
81
84
82 # translate our LEGACY config into the one that pyramid_mailer supports
85 # translate our LEGACY config into the one that pyramid_mailer supports
83 email_conf = dict(
86 email_conf = dict(
84 host=mail_server,
87 host=mail_server,
85 port=email_config.get('smtp_port', 25),
88 port=email_config.get('smtp_port', 25),
86 username=email_config.get('smtp_username'),
89 username=email_config.get('smtp_username'),
87 password=email_config.get('smtp_password'),
90 password=email_config.get('smtp_password'),
88
91
89 tls=str2bool(email_config.get('smtp_use_tls')),
92 tls=str2bool(email_config.get('smtp_use_tls')),
90 ssl=str2bool(email_config.get('smtp_use_ssl')),
93 ssl=str2bool(email_config.get('smtp_use_ssl')),
91
94
92 # SSL key file
95 # SSL key file
93 # keyfile='',
96 # keyfile='',
94
97
95 # SSL certificate file
98 # SSL certificate file
96 # certfile='',
99 # certfile='',
97
100
98 # Location of maildir
101 # Location of maildir
99 # queue_path='',
102 # queue_path='',
100
103
101 default_sender=email_config.get('app_email_from', 'RhodeCode'),
104 default_sender=email_config.get('app_email_from', 'RhodeCode'),
102
105
103 debug=str2bool(email_config.get('smtp_debug')),
106 debug=str2bool(email_config.get('smtp_debug')),
104 # /usr/sbin/sendmail Sendmail executable
107 # /usr/sbin/sendmail Sendmail executable
105 # sendmail_app='',
108 # sendmail_app='',
106
109
107 # {sendmail_app} -t -i -f {sender} Template for sendmail execution
110 # {sendmail_app} -t -i -f {sender} Template for sendmail execution
108 # sendmail_template='',
111 # sendmail_template='',
109 )
112 )
110
113
114 if extra_headers is None:
115 extra_headers = {}
116
117 extra_headers.setdefault('Date', formatdate(time.time()))
118
119 if 'thread_ids' in extra_headers:
120 thread_ids = extra_headers.pop('thread_ids')
121 extra_headers['References'] = ' '.join('<{}>'.format(t) for t in thread_ids)
122
111 try:
123 try:
112 mailer = Mailer(**email_conf)
124 mailer = Mailer(**email_conf)
113
125
114 message = Message(subject=subject,
126 message = Message(subject=subject,
115 sender=email_conf['default_sender'],
127 sender=email_conf['default_sender'],
116 recipients=recipients,
128 recipients=recipients,
117 body=body, html=html_body)
129 body=body, html=html_body,
130 extra_headers=extra_headers)
118 mailer.send_immediately(message)
131 mailer.send_immediately(message)
119
132
120 except Exception:
133 except Exception:
121 log.exception('Mail sending failed')
134 log.exception('Mail sending failed')
122 return False
135 return False
123 return True
136 return True
124
137
125
138
126 @async_task(ignore_result=True, base=RequestContextTask)
139 @async_task(ignore_result=True, base=RequestContextTask)
127 def create_repo(form_data, cur_user):
140 def create_repo(form_data, cur_user):
128 from rhodecode.model.repo import RepoModel
141 from rhodecode.model.repo import RepoModel
129 from rhodecode.model.user import UserModel
142 from rhodecode.model.user import UserModel
130 from rhodecode.model.scm import ScmModel
143 from rhodecode.model.scm import ScmModel
131 from rhodecode.model.settings import SettingsModel
144 from rhodecode.model.settings import SettingsModel
132
145
133 log = get_logger(create_repo)
146 log = get_logger(create_repo)
134
147
135 cur_user = UserModel()._get_user(cur_user)
148 cur_user = UserModel()._get_user(cur_user)
136 owner = cur_user
149 owner = cur_user
137
150
138 repo_name = form_data['repo_name']
151 repo_name = form_data['repo_name']
139 repo_name_full = form_data['repo_name_full']
152 repo_name_full = form_data['repo_name_full']
140 repo_type = form_data['repo_type']
153 repo_type = form_data['repo_type']
141 description = form_data['repo_description']
154 description = form_data['repo_description']
142 private = form_data['repo_private']
155 private = form_data['repo_private']
143 clone_uri = form_data.get('clone_uri')
156 clone_uri = form_data.get('clone_uri')
144 repo_group = safe_int(form_data['repo_group'])
157 repo_group = safe_int(form_data['repo_group'])
145 copy_fork_permissions = form_data.get('copy_permissions')
158 copy_fork_permissions = form_data.get('copy_permissions')
146 copy_group_permissions = form_data.get('repo_copy_permissions')
159 copy_group_permissions = form_data.get('repo_copy_permissions')
147 fork_of = form_data.get('fork_parent_id')
160 fork_of = form_data.get('fork_parent_id')
148 state = form_data.get('repo_state', Repository.STATE_PENDING)
161 state = form_data.get('repo_state', Repository.STATE_PENDING)
149
162
150 # repo creation defaults, private and repo_type are filled in form
163 # repo creation defaults, private and repo_type are filled in form
151 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
164 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
152 enable_statistics = form_data.get(
165 enable_statistics = form_data.get(
153 'enable_statistics', defs.get('repo_enable_statistics'))
166 'enable_statistics', defs.get('repo_enable_statistics'))
154 enable_locking = form_data.get(
167 enable_locking = form_data.get(
155 'enable_locking', defs.get('repo_enable_locking'))
168 'enable_locking', defs.get('repo_enable_locking'))
156 enable_downloads = form_data.get(
169 enable_downloads = form_data.get(
157 'enable_downloads', defs.get('repo_enable_downloads'))
170 'enable_downloads', defs.get('repo_enable_downloads'))
158
171
159 # set landing rev based on default branches for SCM
172 # set landing rev based on default branches for SCM
160 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
173 landing_ref, _label = ScmModel.backend_landing_ref(repo_type)
161
174
162 try:
175 try:
163 RepoModel()._create_repo(
176 RepoModel()._create_repo(
164 repo_name=repo_name_full,
177 repo_name=repo_name_full,
165 repo_type=repo_type,
178 repo_type=repo_type,
166 description=description,
179 description=description,
167 owner=owner,
180 owner=owner,
168 private=private,
181 private=private,
169 clone_uri=clone_uri,
182 clone_uri=clone_uri,
170 repo_group=repo_group,
183 repo_group=repo_group,
171 landing_rev=landing_ref,
184 landing_rev=landing_ref,
172 fork_of=fork_of,
185 fork_of=fork_of,
173 copy_fork_permissions=copy_fork_permissions,
186 copy_fork_permissions=copy_fork_permissions,
174 copy_group_permissions=copy_group_permissions,
187 copy_group_permissions=copy_group_permissions,
175 enable_statistics=enable_statistics,
188 enable_statistics=enable_statistics,
176 enable_locking=enable_locking,
189 enable_locking=enable_locking,
177 enable_downloads=enable_downloads,
190 enable_downloads=enable_downloads,
178 state=state
191 state=state
179 )
192 )
180 Session().commit()
193 Session().commit()
181
194
182 # now create this repo on Filesystem
195 # now create this repo on Filesystem
183 RepoModel()._create_filesystem_repo(
196 RepoModel()._create_filesystem_repo(
184 repo_name=repo_name,
197 repo_name=repo_name,
185 repo_type=repo_type,
198 repo_type=repo_type,
186 repo_group=RepoModel()._get_repo_group(repo_group),
199 repo_group=RepoModel()._get_repo_group(repo_group),
187 clone_uri=clone_uri,
200 clone_uri=clone_uri,
188 )
201 )
189 repo = Repository.get_by_repo_name(repo_name_full)
202 repo = Repository.get_by_repo_name(repo_name_full)
190 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
203 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
191
204
192 # update repo commit caches initially
205 # update repo commit caches initially
193 repo.update_commit_cache()
206 repo.update_commit_cache()
194
207
195 # set new created state
208 # set new created state
196 repo.set_state(Repository.STATE_CREATED)
209 repo.set_state(Repository.STATE_CREATED)
197 repo_id = repo.repo_id
210 repo_id = repo.repo_id
198 repo_data = repo.get_api_data()
211 repo_data = repo.get_api_data()
199
212
200 audit_logger.store(
213 audit_logger.store(
201 'repo.create', action_data={'data': repo_data},
214 'repo.create', action_data={'data': repo_data},
202 user=cur_user,
215 user=cur_user,
203 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))
204
217
205 Session().commit()
218 Session().commit()
206 except Exception as e:
219 except Exception as e:
207 log.warning('Exception occurred when creating repository, '
220 log.warning('Exception occurred when creating repository, '
208 'doing cleanup...', exc_info=True)
221 'doing cleanup...', exc_info=True)
209 if isinstance(e, IntegrityError):
222 if isinstance(e, IntegrityError):
210 Session().rollback()
223 Session().rollback()
211
224
212 # rollback things manually !
225 # rollback things manually !
213 repo = Repository.get_by_repo_name(repo_name_full)
226 repo = Repository.get_by_repo_name(repo_name_full)
214 if repo:
227 if repo:
215 Repository.delete(repo.repo_id)
228 Repository.delete(repo.repo_id)
216 Session().commit()
229 Session().commit()
217 RepoModel()._delete_filesystem_repo(repo)
230 RepoModel()._delete_filesystem_repo(repo)
218 log.info('Cleanup of repo %s finished', repo_name_full)
231 log.info('Cleanup of repo %s finished', repo_name_full)
219 raise
232 raise
220
233
221 return True
234 return True
222
235
223
236
224 @async_task(ignore_result=True, base=RequestContextTask)
237 @async_task(ignore_result=True, base=RequestContextTask)
225 def create_repo_fork(form_data, cur_user):
238 def create_repo_fork(form_data, cur_user):
226 """
239 """
227 Creates a fork of repository using internal VCS methods
240 Creates a fork of repository using internal VCS methods
228 """
241 """
229 from rhodecode.model.repo import RepoModel
242 from rhodecode.model.repo import RepoModel
230 from rhodecode.model.user import UserModel
243 from rhodecode.model.user import UserModel
231
244
232 log = get_logger(create_repo_fork)
245 log = get_logger(create_repo_fork)
233
246
234 cur_user = UserModel()._get_user(cur_user)
247 cur_user = UserModel()._get_user(cur_user)
235 owner = cur_user
248 owner = cur_user
236
249
237 repo_name = form_data['repo_name'] # fork in this case
250 repo_name = form_data['repo_name'] # fork in this case
238 repo_name_full = form_data['repo_name_full']
251 repo_name_full = form_data['repo_name_full']
239 repo_type = form_data['repo_type']
252 repo_type = form_data['repo_type']
240 description = form_data['description']
253 description = form_data['description']
241 private = form_data['private']
254 private = form_data['private']
242 clone_uri = form_data.get('clone_uri')
255 clone_uri = form_data.get('clone_uri')
243 repo_group = safe_int(form_data['repo_group'])
256 repo_group = safe_int(form_data['repo_group'])
244 landing_ref = form_data['landing_rev']
257 landing_ref = form_data['landing_rev']
245 copy_fork_permissions = form_data.get('copy_permissions')
258 copy_fork_permissions = form_data.get('copy_permissions')
246 fork_id = safe_int(form_data.get('fork_parent_id'))
259 fork_id = safe_int(form_data.get('fork_parent_id'))
247
260
248 try:
261 try:
249 fork_of = RepoModel()._get_repo(fork_id)
262 fork_of = RepoModel()._get_repo(fork_id)
250 RepoModel()._create_repo(
263 RepoModel()._create_repo(
251 repo_name=repo_name_full,
264 repo_name=repo_name_full,
252 repo_type=repo_type,
265 repo_type=repo_type,
253 description=description,
266 description=description,
254 owner=owner,
267 owner=owner,
255 private=private,
268 private=private,
256 clone_uri=clone_uri,
269 clone_uri=clone_uri,
257 repo_group=repo_group,
270 repo_group=repo_group,
258 landing_rev=landing_ref,
271 landing_rev=landing_ref,
259 fork_of=fork_of,
272 fork_of=fork_of,
260 copy_fork_permissions=copy_fork_permissions
273 copy_fork_permissions=copy_fork_permissions
261 )
274 )
262
275
263 Session().commit()
276 Session().commit()
264
277
265 base_path = Repository.base_path()
278 base_path = Repository.base_path()
266 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)
267
280
268 # now create this repo on Filesystem
281 # now create this repo on Filesystem
269 RepoModel()._create_filesystem_repo(
282 RepoModel()._create_filesystem_repo(
270 repo_name=repo_name,
283 repo_name=repo_name,
271 repo_type=repo_type,
284 repo_type=repo_type,
272 repo_group=RepoModel()._get_repo_group(repo_group),
285 repo_group=RepoModel()._get_repo_group(repo_group),
273 clone_uri=source_repo_path,
286 clone_uri=source_repo_path,
274 )
287 )
275 repo = Repository.get_by_repo_name(repo_name_full)
288 repo = Repository.get_by_repo_name(repo_name_full)
276 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
289 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
277
290
278 # update repo commit caches initially
291 # update repo commit caches initially
279 config = repo._config
292 config = repo._config
280 config.set('extensions', 'largefiles', '')
293 config.set('extensions', 'largefiles', '')
281 repo.update_commit_cache(config=config)
294 repo.update_commit_cache(config=config)
282
295
283 # set new created state
296 # set new created state
284 repo.set_state(Repository.STATE_CREATED)
297 repo.set_state(Repository.STATE_CREATED)
285
298
286 repo_id = repo.repo_id
299 repo_id = repo.repo_id
287 repo_data = repo.get_api_data()
300 repo_data = repo.get_api_data()
288 audit_logger.store(
301 audit_logger.store(
289 'repo.fork', action_data={'data': repo_data},
302 'repo.fork', action_data={'data': repo_data},
290 user=cur_user,
303 user=cur_user,
291 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))
292
305
293 Session().commit()
306 Session().commit()
294 except Exception as e:
307 except Exception as e:
295 log.warning('Exception occurred when forking repository, '
308 log.warning('Exception occurred when forking repository, '
296 'doing cleanup...', exc_info=True)
309 'doing cleanup...', exc_info=True)
297 if isinstance(e, IntegrityError):
310 if isinstance(e, IntegrityError):
298 Session().rollback()
311 Session().rollback()
299
312
300 # rollback things manually !
313 # rollback things manually !
301 repo = Repository.get_by_repo_name(repo_name_full)
314 repo = Repository.get_by_repo_name(repo_name_full)
302 if repo:
315 if repo:
303 Repository.delete(repo.repo_id)
316 Repository.delete(repo.repo_id)
304 Session().commit()
317 Session().commit()
305 RepoModel()._delete_filesystem_repo(repo)
318 RepoModel()._delete_filesystem_repo(repo)
306 log.info('Cleanup of repo %s finished', repo_name_full)
319 log.info('Cleanup of repo %s finished', repo_name_full)
307 raise
320 raise
308
321
309 return True
322 return True
310
323
311
324
312 @async_task(ignore_result=True)
325 @async_task(ignore_result=True)
313 def repo_maintenance(repoid):
326 def repo_maintenance(repoid):
314 from rhodecode.lib import repo_maintenance as repo_maintenance_lib
327 from rhodecode.lib import repo_maintenance as repo_maintenance_lib
315 log = get_logger(repo_maintenance)
328 log = get_logger(repo_maintenance)
316 repo = Repository.get_by_id_or_repo_name(repoid)
329 repo = Repository.get_by_id_or_repo_name(repoid)
317 if repo:
330 if repo:
318 maintenance = repo_maintenance_lib.RepoMaintenance()
331 maintenance = repo_maintenance_lib.RepoMaintenance()
319 tasks = maintenance.get_tasks_for_repo(repo)
332 tasks = maintenance.get_tasks_for_repo(repo)
320 log.debug('Executing %s tasks on repo `%s`', tasks, repoid)
333 log.debug('Executing %s tasks on repo `%s`', tasks, repoid)
321 executed_types = maintenance.execute(repo)
334 executed_types = maintenance.execute(repo)
322 log.debug('Got execution results %s', executed_types)
335 log.debug('Got execution results %s', executed_types)
323 else:
336 else:
324 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)
325
338
326
339
327 @async_task(ignore_result=True)
340 @async_task(ignore_result=True)
328 def check_for_update():
341 def check_for_update():
329 from rhodecode.model.update import UpdateModel
342 from rhodecode.model.update import UpdateModel
330 update_url = UpdateModel().get_update_url()
343 update_url = UpdateModel().get_update_url()
331 cur_ver = rhodecode.__version__
344 cur_ver = rhodecode.__version__
332
345
333 try:
346 try:
334 data = UpdateModel().get_update_data(update_url)
347 data = UpdateModel().get_update_data(update_url)
335 latest = data['versions'][0]
348 latest = data['versions'][0]
336 UpdateModel().store_version(latest['version'])
349 UpdateModel().store_version(latest['version'])
337 except Exception:
350 except Exception:
338 pass
351 pass
339
352
340
353
341 @async_task(ignore_result=False)
354 @async_task(ignore_result=False)
342 def beat_check(*args, **kwargs):
355 def beat_check(*args, **kwargs):
343 log = get_logger(beat_check)
356 log = get_logger(beat_check)
344 log.info('Got args: %r and kwargs %r', args, kwargs)
357 log.info('Got args: %r and kwargs %r', args, kwargs)
345 return time.time()
358 return time.time()
346
359
347
360
348 @async_task(ignore_result=True)
361 @async_task(ignore_result=True)
349 def sync_last_update(*args, **kwargs):
362 def sync_last_update(*args, **kwargs):
350
363
351 skip_repos = kwargs.get('skip_repos')
364 skip_repos = kwargs.get('skip_repos')
352 if not skip_repos:
365 if not skip_repos:
353 repos = Repository.query() \
366 repos = Repository.query() \
354 .order_by(Repository.group_id.asc())
367 .order_by(Repository.group_id.asc())
355
368
356 for repo in repos:
369 for repo in repos:
357 repo.update_commit_cache()
370 repo.update_commit_cache()
358
371
359 skip_groups = kwargs.get('skip_groups')
372 skip_groups = kwargs.get('skip_groups')
360 if not skip_groups:
373 if not skip_groups:
361 repo_groups = RepoGroup.query() \
374 repo_groups = RepoGroup.query() \
362 .filter(RepoGroup.group_parent_id == None)
375 .filter(RepoGroup.group_parent_id == None)
363
376
364 for root_gr in repo_groups:
377 for root_gr in repo_groups:
365 for repo_gr in reversed(root_gr.recursive_groups()):
378 for repo_gr in reversed(root_gr.recursive_groups()):
366 repo_gr.update_commit_cache()
379 repo_gr.update_commit_cache()
@@ -1,231 +1,230 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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 time
22 import time
23 import datetime
23 import datetime
24 import msgpack
24 import msgpack
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import tempfile
27 import tempfile
28 import glob
28 import glob
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32 # NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking
32 # NOTE: Any changes should be synced with exc_tracking at vcsserver.lib.exc_tracking
33 global_prefix = 'rhodecode'
33 global_prefix = 'rhodecode'
34 exc_store_dir_name = 'rc_exception_store_v1'
34 exc_store_dir_name = 'rc_exception_store_v1'
35
35
36
36
37 def exc_serialize(exc_id, tb, exc_type, extra_data=None):
37 def exc_serialize(exc_id, tb, exc_type, extra_data=None):
38
38
39 data = {
39 data = {
40 'version': 'v1',
40 'version': 'v1',
41 'exc_id': exc_id,
41 'exc_id': exc_id,
42 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
42 'exc_utc_date': datetime.datetime.utcnow().isoformat(),
43 'exc_timestamp': repr(time.time()),
43 'exc_timestamp': repr(time.time()),
44 'exc_message': tb,
44 'exc_message': tb,
45 'exc_type': exc_type,
45 'exc_type': exc_type,
46 }
46 }
47 if extra_data:
47 if extra_data:
48 data.update(extra_data)
48 data.update(extra_data)
49 return msgpack.packb(data), data
49 return msgpack.packb(data), data
50
50
51
51
52 def exc_unserialize(tb):
52 def exc_unserialize(tb):
53 return msgpack.unpackb(tb)
53 return msgpack.unpackb(tb)
54
54
55 _exc_store = None
55 _exc_store = None
56
56
57
57
58 def get_exc_store():
58 def get_exc_store():
59 """
59 """
60 Get and create exception store if it's not existing
60 Get and create exception store if it's not existing
61 """
61 """
62 global _exc_store
62 global _exc_store
63 import rhodecode as app
63 import rhodecode as app
64
64
65 if _exc_store is not None:
65 if _exc_store is not None:
66 # quick global cache
66 # quick global cache
67 return _exc_store
67 return _exc_store
68
68
69 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
69 exc_store_dir = app.CONFIG.get('exception_tracker.store_path', '') or tempfile.gettempdir()
70 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
70 _exc_store_path = os.path.join(exc_store_dir, exc_store_dir_name)
71
71
72 _exc_store_path = os.path.abspath(_exc_store_path)
72 _exc_store_path = os.path.abspath(_exc_store_path)
73 if not os.path.isdir(_exc_store_path):
73 if not os.path.isdir(_exc_store_path):
74 os.makedirs(_exc_store_path)
74 os.makedirs(_exc_store_path)
75 log.debug('Initializing exceptions store at %s', _exc_store_path)
75 log.debug('Initializing exceptions store at %s', _exc_store_path)
76 _exc_store = _exc_store_path
76 _exc_store = _exc_store_path
77
77
78 return _exc_store_path
78 return _exc_store_path
79
79
80
80
81 def _store_exception(exc_id, exc_type_name, exc_traceback, prefix, send_email=None):
81 def _store_exception(exc_id, exc_type_name, exc_traceback, prefix, send_email=None):
82 """
82 """
83 Low level function to store exception in the exception tracker
83 Low level function to store exception in the exception tracker
84 """
84 """
85 from pyramid.threadlocal import get_current_request
85 from pyramid.threadlocal import get_current_request
86 import rhodecode as app
86 import rhodecode as app
87 request = get_current_request()
87 request = get_current_request()
88 extra_data = {}
88 extra_data = {}
89 # NOTE(marcink): store request information into exc_data
89 # NOTE(marcink): store request information into exc_data
90 if request:
90 if request:
91 extra_data['client_address'] = getattr(request, 'client_addr', '')
91 extra_data['client_address'] = getattr(request, 'client_addr', '')
92 extra_data['user_agent'] = getattr(request, 'user_agent', '')
92 extra_data['user_agent'] = getattr(request, 'user_agent', '')
93 extra_data['method'] = getattr(request, 'method', '')
93 extra_data['method'] = getattr(request, 'method', '')
94 extra_data['url'] = getattr(request, 'url', '')
94 extra_data['url'] = getattr(request, 'url', '')
95
95
96 exc_store_path = get_exc_store()
96 exc_store_path = get_exc_store()
97 exc_data, org_data = exc_serialize(exc_id, exc_traceback, exc_type_name, extra_data=extra_data)
97 exc_data, org_data = exc_serialize(exc_id, exc_traceback, exc_type_name, extra_data=extra_data)
98
98
99 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
99 exc_pref_id = '{}_{}_{}'.format(exc_id, prefix, org_data['exc_timestamp'])
100 if not os.path.isdir(exc_store_path):
100 if not os.path.isdir(exc_store_path):
101 os.makedirs(exc_store_path)
101 os.makedirs(exc_store_path)
102 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
102 stored_exc_path = os.path.join(exc_store_path, exc_pref_id)
103 with open(stored_exc_path, 'wb') as f:
103 with open(stored_exc_path, 'wb') as f:
104 f.write(exc_data)
104 f.write(exc_data)
105 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
105 log.debug('Stored generated exception %s as: %s', exc_id, stored_exc_path)
106
106
107 if send_email is None:
107 if send_email is None:
108 # NOTE(marcink): read app config unless we specify explicitly
108 # NOTE(marcink): read app config unless we specify explicitly
109 send_email = app.CONFIG.get('exception_tracker.send_email', False)
109 send_email = app.CONFIG.get('exception_tracker.send_email', False)
110
110
111 mail_server = app.CONFIG.get('smtp_server') or None
111 mail_server = app.CONFIG.get('smtp_server') or None
112 send_email = send_email and mail_server
112 send_email = send_email and mail_server
113 if send_email:
113 if send_email:
114 try:
114 try:
115 send_exc_email(request, exc_id, exc_type_name)
115 send_exc_email(request, exc_id, exc_type_name)
116 except Exception:
116 except Exception:
117 log.exception('Failed to send exception email')
117 log.exception('Failed to send exception email')
118 pass
118 pass
119
119
120
120
121 def send_exc_email(request, exc_id, exc_type_name):
121 def send_exc_email(request, exc_id, exc_type_name):
122 import rhodecode as app
122 import rhodecode as app
123 from rhodecode.apps._base import TemplateArgs
123 from rhodecode.apps._base import TemplateArgs
124 from rhodecode.lib.utils2 import aslist
124 from rhodecode.lib.utils2 import aslist
125 from rhodecode.lib.celerylib import run_task, tasks
125 from rhodecode.lib.celerylib import run_task, tasks
126 from rhodecode.lib.base import attach_context_attributes
126 from rhodecode.lib.base import attach_context_attributes
127 from rhodecode.model.notification import EmailNotificationModel
127 from rhodecode.model.notification import EmailNotificationModel
128
128
129 recipients = aslist(app.CONFIG.get('exception_tracker.send_email_recipients', ''))
129 recipients = aslist(app.CONFIG.get('exception_tracker.send_email_recipients', ''))
130 log.debug('Sending Email exception to: `%s`', recipients or 'all super admins')
130 log.debug('Sending Email exception to: `%s`', recipients or 'all super admins')
131
131
132 # NOTE(marcink): needed for email template rendering
132 # NOTE(marcink): needed for email template rendering
133 user_id = None
133 user_id = None
134 if request:
134 if request:
135 user_id = request.user.user_id
135 user_id = request.user.user_id
136 attach_context_attributes(TemplateArgs(), request, user_id=user_id, is_api=True)
136 attach_context_attributes(TemplateArgs(), request, user_id=user_id, is_api=True)
137
137
138 email_kwargs = {
138 email_kwargs = {
139 'email_prefix': app.CONFIG.get('exception_tracker.email_prefix', '') or '[RHODECODE ERROR]',
139 'email_prefix': app.CONFIG.get('exception_tracker.email_prefix', '') or '[RHODECODE ERROR]',
140 'exc_url': request.route_url('admin_settings_exception_tracker_show', exception_id=exc_id),
140 'exc_url': request.route_url('admin_settings_exception_tracker_show', exception_id=exc_id),
141 'exc_id': exc_id,
141 'exc_id': exc_id,
142 'exc_type_name': exc_type_name,
142 'exc_type_name': exc_type_name,
143 'exc_traceback': read_exception(exc_id, prefix=None),
143 'exc_traceback': read_exception(exc_id, prefix=None),
144 }
144 }
145
145
146 (subject, headers, email_body,
146 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
147 email_body_plaintext) = EmailNotificationModel().render_email(
148 EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs)
147 EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs)
149
148
150 run_task(tasks.send_email, recipients, subject,
149 run_task(tasks.send_email, recipients, subject,
151 email_body_plaintext, email_body)
150 email_body_plaintext, email_body)
152
151
153
152
154 def _prepare_exception(exc_info):
153 def _prepare_exception(exc_info):
155 exc_type, exc_value, exc_traceback = exc_info
154 exc_type, exc_value, exc_traceback = exc_info
156 exc_type_name = exc_type.__name__
155 exc_type_name = exc_type.__name__
157
156
158 tb = ''.join(traceback.format_exception(
157 tb = ''.join(traceback.format_exception(
159 exc_type, exc_value, exc_traceback, None))
158 exc_type, exc_value, exc_traceback, None))
160
159
161 return exc_type_name, tb
160 return exc_type_name, tb
162
161
163
162
164 def store_exception(exc_id, exc_info, prefix=global_prefix):
163 def store_exception(exc_id, exc_info, prefix=global_prefix):
165 """
164 """
166 Example usage::
165 Example usage::
167
166
168 exc_info = sys.exc_info()
167 exc_info = sys.exc_info()
169 store_exception(id(exc_info), exc_info)
168 store_exception(id(exc_info), exc_info)
170 """
169 """
171
170
172 try:
171 try:
173 exc_type_name, exc_traceback = _prepare_exception(exc_info)
172 exc_type_name, exc_traceback = _prepare_exception(exc_info)
174 _store_exception(exc_id=exc_id, exc_type_name=exc_type_name,
173 _store_exception(exc_id=exc_id, exc_type_name=exc_type_name,
175 exc_traceback=exc_traceback, prefix=prefix)
174 exc_traceback=exc_traceback, prefix=prefix)
176 return exc_id, exc_type_name
175 return exc_id, exc_type_name
177 except Exception:
176 except Exception:
178 log.exception('Failed to store exception `%s` information', exc_id)
177 log.exception('Failed to store exception `%s` information', exc_id)
179 # there's no way this can fail, it will crash server badly if it does.
178 # there's no way this can fail, it will crash server badly if it does.
180 pass
179 pass
181
180
182
181
183 def _find_exc_file(exc_id, prefix=global_prefix):
182 def _find_exc_file(exc_id, prefix=global_prefix):
184 exc_store_path = get_exc_store()
183 exc_store_path = get_exc_store()
185 if prefix:
184 if prefix:
186 exc_id = '{}_{}'.format(exc_id, prefix)
185 exc_id = '{}_{}'.format(exc_id, prefix)
187 else:
186 else:
188 # search without a prefix
187 # search without a prefix
189 exc_id = '{}'.format(exc_id)
188 exc_id = '{}'.format(exc_id)
190
189
191 found_exc_id = None
190 found_exc_id = None
192 matches = glob.glob(os.path.join(exc_store_path, exc_id) + '*')
191 matches = glob.glob(os.path.join(exc_store_path, exc_id) + '*')
193 if matches:
192 if matches:
194 found_exc_id = matches[0]
193 found_exc_id = matches[0]
195
194
196 return found_exc_id
195 return found_exc_id
197
196
198
197
199 def _read_exception(exc_id, prefix):
198 def _read_exception(exc_id, prefix):
200 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
199 exc_id_file_path = _find_exc_file(exc_id=exc_id, prefix=prefix)
201 if exc_id_file_path:
200 if exc_id_file_path:
202 with open(exc_id_file_path, 'rb') as f:
201 with open(exc_id_file_path, 'rb') as f:
203 return exc_unserialize(f.read())
202 return exc_unserialize(f.read())
204 else:
203 else:
205 log.debug('Exception File `%s` not found', exc_id_file_path)
204 log.debug('Exception File `%s` not found', exc_id_file_path)
206 return None
205 return None
207
206
208
207
209 def read_exception(exc_id, prefix=global_prefix):
208 def read_exception(exc_id, prefix=global_prefix):
210 try:
209 try:
211 return _read_exception(exc_id=exc_id, prefix=prefix)
210 return _read_exception(exc_id=exc_id, prefix=prefix)
212 except Exception:
211 except Exception:
213 log.exception('Failed to read exception `%s` information', exc_id)
212 log.exception('Failed to read exception `%s` information', exc_id)
214 # there's no way this can fail, it will crash server badly if it does.
213 # there's no way this can fail, it will crash server badly if it does.
215 return None
214 return None
216
215
217
216
218 def delete_exception(exc_id, prefix=global_prefix):
217 def delete_exception(exc_id, prefix=global_prefix):
219 try:
218 try:
220 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
219 exc_id_file_path = _find_exc_file(exc_id, prefix=prefix)
221 if exc_id_file_path:
220 if exc_id_file_path:
222 os.remove(exc_id_file_path)
221 os.remove(exc_id_file_path)
223
222
224 except Exception:
223 except Exception:
225 log.exception('Failed to remove exception `%s` information', exc_id)
224 log.exception('Failed to remove exception `%s` information', exc_id)
226 # there's no way this can fail, it will crash server badly if it does.
225 # there's no way this can fail, it will crash server badly if it does.
227 pass
226 pass
228
227
229
228
230 def generate_id():
229 def generate_id():
231 return id(object())
230 return id(object())
@@ -1,836 +1,840 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 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24 import datetime
24 import datetime
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28 import collections
28 import collections
29
29
30 from pyramid.threadlocal import get_current_registry, get_current_request
30 from pyramid.threadlocal import get_current_registry, get_current_request
31 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.expression import null
32 from sqlalchemy.sql.functions import coalesce
32 from sqlalchemy.sql.functions import coalesce
33
33
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 from rhodecode.lib import audit_logger
35 from rhodecode.lib import audit_logger
36 from rhodecode.lib.exceptions import CommentVersionMismatch
36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 from rhodecode.model import BaseModel
38 from rhodecode.model import BaseModel
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 ChangesetComment,
40 ChangesetComment,
41 User,
41 User,
42 Notification,
42 Notification,
43 PullRequest,
43 PullRequest,
44 AttributeDict,
44 AttributeDict,
45 ChangesetCommentHistory,
45 ChangesetCommentHistory,
46 )
46 )
47 from rhodecode.model.notification import NotificationModel
47 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.settings import VcsSettingsModel
49 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.notification import EmailNotificationModel
50 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.validation_schema.schemas import comment_schema
51 from rhodecode.model.validation_schema.schemas import comment_schema
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class CommentsModel(BaseModel):
57 class CommentsModel(BaseModel):
58
58
59 cls = ChangesetComment
59 cls = ChangesetComment
60
60
61 DIFF_CONTEXT_BEFORE = 3
61 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_AFTER = 3
62 DIFF_CONTEXT_AFTER = 3
63
63
64 def __get_commit_comment(self, changeset_comment):
64 def __get_commit_comment(self, changeset_comment):
65 return self._get_instance(ChangesetComment, changeset_comment)
65 return self._get_instance(ChangesetComment, changeset_comment)
66
66
67 def __get_pull_request(self, pull_request):
67 def __get_pull_request(self, pull_request):
68 return self._get_instance(PullRequest, pull_request)
68 return self._get_instance(PullRequest, pull_request)
69
69
70 def _extract_mentions(self, s):
70 def _extract_mentions(self, s):
71 user_objects = []
71 user_objects = []
72 for username in extract_mentioned_users(s):
72 for username in extract_mentioned_users(s):
73 user_obj = User.get_by_username(username, case_insensitive=True)
73 user_obj = User.get_by_username(username, case_insensitive=True)
74 if user_obj:
74 if user_obj:
75 user_objects.append(user_obj)
75 user_objects.append(user_obj)
76 return user_objects
76 return user_objects
77
77
78 def _get_renderer(self, global_renderer='rst', request=None):
78 def _get_renderer(self, global_renderer='rst', request=None):
79 request = request or get_current_request()
79 request = request or get_current_request()
80
80
81 try:
81 try:
82 global_renderer = request.call_context.visual.default_renderer
82 global_renderer = request.call_context.visual.default_renderer
83 except AttributeError:
83 except AttributeError:
84 log.debug("Renderer not set, falling back "
84 log.debug("Renderer not set, falling back "
85 "to default renderer '%s'", global_renderer)
85 "to default renderer '%s'", global_renderer)
86 except Exception:
86 except Exception:
87 log.error(traceback.format_exc())
87 log.error(traceback.format_exc())
88 return global_renderer
88 return global_renderer
89
89
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 # group by versions, and count until, and display objects
91 # group by versions, and count until, and display objects
92
92
93 comment_groups = collections.defaultdict(list)
93 comment_groups = collections.defaultdict(list)
94 [comment_groups[
94 [comment_groups[
95 _co.pull_request_version_id].append(_co) for _co in comments]
95 _co.pull_request_version_id].append(_co) for _co in comments]
96
96
97 def yield_comments(pos):
97 def yield_comments(pos):
98 for co in comment_groups[pos]:
98 for co in comment_groups[pos]:
99 yield co
99 yield co
100
100
101 comment_versions = collections.defaultdict(
101 comment_versions = collections.defaultdict(
102 lambda: collections.defaultdict(list))
102 lambda: collections.defaultdict(list))
103 prev_prvid = -1
103 prev_prvid = -1
104 # fake last entry with None, to aggregate on "latest" version which
104 # fake last entry with None, to aggregate on "latest" version which
105 # doesn't have an pull_request_version_id
105 # doesn't have an pull_request_version_id
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
107 prvid = ver.pull_request_version_id
107 prvid = ver.pull_request_version_id
108 if prev_prvid == -1:
108 if prev_prvid == -1:
109 prev_prvid = prvid
109 prev_prvid = prvid
110
110
111 for co in yield_comments(prvid):
111 for co in yield_comments(prvid):
112 comment_versions[prvid]['at'].append(co)
112 comment_versions[prvid]['at'].append(co)
113
113
114 # save until
114 # save until
115 current = comment_versions[prvid]['at']
115 current = comment_versions[prvid]['at']
116 prev_until = comment_versions[prev_prvid]['until']
116 prev_until = comment_versions[prev_prvid]['until']
117 cur_until = prev_until + current
117 cur_until = prev_until + current
118 comment_versions[prvid]['until'].extend(cur_until)
118 comment_versions[prvid]['until'].extend(cur_until)
119
119
120 # save outdated
120 # save outdated
121 if inline:
121 if inline:
122 outdated = [x for x in cur_until
122 outdated = [x for x in cur_until
123 if x.outdated_at_version(show_version)]
123 if x.outdated_at_version(show_version)]
124 else:
124 else:
125 outdated = [x for x in cur_until
125 outdated = [x for x in cur_until
126 if x.older_than_version(show_version)]
126 if x.older_than_version(show_version)]
127 display = [x for x in cur_until if x not in outdated]
127 display = [x for x in cur_until if x not in outdated]
128
128
129 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['outdated'] = outdated
130 comment_versions[prvid]['display'] = display
130 comment_versions[prvid]['display'] = display
131
131
132 prev_prvid = prvid
132 prev_prvid = prvid
133
133
134 return comment_versions
134 return comment_versions
135
135
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
137 qry = Session().query(ChangesetComment) \
137 qry = Session().query(ChangesetComment) \
138 .filter(ChangesetComment.repo == repo)
138 .filter(ChangesetComment.repo == repo)
139
139
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
142
142
143 if user:
143 if user:
144 user = self._get_user(user)
144 user = self._get_user(user)
145 if user:
145 if user:
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
147
147
148 if commit_id:
148 if commit_id:
149 qry = qry.filter(ChangesetComment.revision == commit_id)
149 qry = qry.filter(ChangesetComment.revision == commit_id)
150
150
151 qry = qry.order_by(ChangesetComment.created_on)
151 qry = qry.order_by(ChangesetComment.created_on)
152 return qry.all()
152 return qry.all()
153
153
154 def get_repository_unresolved_todos(self, repo):
154 def get_repository_unresolved_todos(self, repo):
155 todos = Session().query(ChangesetComment) \
155 todos = Session().query(ChangesetComment) \
156 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.repo == repo) \
157 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.resolved_by == None) \
158 .filter(ChangesetComment.comment_type
158 .filter(ChangesetComment.comment_type
159 == ChangesetComment.COMMENT_TYPE_TODO)
159 == ChangesetComment.COMMENT_TYPE_TODO)
160 todos = todos.all()
160 todos = todos.all()
161
161
162 return todos
162 return todos
163
163
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
165
165
166 todos = Session().query(ChangesetComment) \
166 todos = Session().query(ChangesetComment) \
167 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.pull_request == pull_request) \
168 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.resolved_by == None) \
169 .filter(ChangesetComment.comment_type
169 .filter(ChangesetComment.comment_type
170 == ChangesetComment.COMMENT_TYPE_TODO)
170 == ChangesetComment.COMMENT_TYPE_TODO)
171
171
172 if not show_outdated:
172 if not show_outdated:
173 todos = todos.filter(
173 todos = todos.filter(
174 coalesce(ChangesetComment.display_state, '') !=
174 coalesce(ChangesetComment.display_state, '') !=
175 ChangesetComment.COMMENT_OUTDATED)
175 ChangesetComment.COMMENT_OUTDATED)
176
176
177 todos = todos.all()
177 todos = todos.all()
178
178
179 return todos
179 return todos
180
180
181 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
181 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
182
182
183 todos = Session().query(ChangesetComment) \
183 todos = Session().query(ChangesetComment) \
184 .filter(ChangesetComment.pull_request == pull_request) \
184 .filter(ChangesetComment.pull_request == pull_request) \
185 .filter(ChangesetComment.resolved_by != None) \
185 .filter(ChangesetComment.resolved_by != None) \
186 .filter(ChangesetComment.comment_type
186 .filter(ChangesetComment.comment_type
187 == ChangesetComment.COMMENT_TYPE_TODO)
187 == ChangesetComment.COMMENT_TYPE_TODO)
188
188
189 if not show_outdated:
189 if not show_outdated:
190 todos = todos.filter(
190 todos = todos.filter(
191 coalesce(ChangesetComment.display_state, '') !=
191 coalesce(ChangesetComment.display_state, '') !=
192 ChangesetComment.COMMENT_OUTDATED)
192 ChangesetComment.COMMENT_OUTDATED)
193
193
194 todos = todos.all()
194 todos = todos.all()
195
195
196 return todos
196 return todos
197
197
198 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
198 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
199
199
200 todos = Session().query(ChangesetComment) \
200 todos = Session().query(ChangesetComment) \
201 .filter(ChangesetComment.revision == commit_id) \
201 .filter(ChangesetComment.revision == commit_id) \
202 .filter(ChangesetComment.resolved_by == None) \
202 .filter(ChangesetComment.resolved_by == None) \
203 .filter(ChangesetComment.comment_type
203 .filter(ChangesetComment.comment_type
204 == ChangesetComment.COMMENT_TYPE_TODO)
204 == ChangesetComment.COMMENT_TYPE_TODO)
205
205
206 if not show_outdated:
206 if not show_outdated:
207 todos = todos.filter(
207 todos = todos.filter(
208 coalesce(ChangesetComment.display_state, '') !=
208 coalesce(ChangesetComment.display_state, '') !=
209 ChangesetComment.COMMENT_OUTDATED)
209 ChangesetComment.COMMENT_OUTDATED)
210
210
211 todos = todos.all()
211 todos = todos.all()
212
212
213 return todos
213 return todos
214
214
215 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
215 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
216
216
217 todos = Session().query(ChangesetComment) \
217 todos = Session().query(ChangesetComment) \
218 .filter(ChangesetComment.revision == commit_id) \
218 .filter(ChangesetComment.revision == commit_id) \
219 .filter(ChangesetComment.resolved_by != None) \
219 .filter(ChangesetComment.resolved_by != None) \
220 .filter(ChangesetComment.comment_type
220 .filter(ChangesetComment.comment_type
221 == ChangesetComment.COMMENT_TYPE_TODO)
221 == ChangesetComment.COMMENT_TYPE_TODO)
222
222
223 if not show_outdated:
223 if not show_outdated:
224 todos = todos.filter(
224 todos = todos.filter(
225 coalesce(ChangesetComment.display_state, '') !=
225 coalesce(ChangesetComment.display_state, '') !=
226 ChangesetComment.COMMENT_OUTDATED)
226 ChangesetComment.COMMENT_OUTDATED)
227
227
228 todos = todos.all()
228 todos = todos.all()
229
229
230 return todos
230 return todos
231
231
232 def _log_audit_action(self, action, action_data, auth_user, comment):
232 def _log_audit_action(self, action, action_data, auth_user, comment):
233 audit_logger.store(
233 audit_logger.store(
234 action=action,
234 action=action,
235 action_data=action_data,
235 action_data=action_data,
236 user=auth_user,
236 user=auth_user,
237 repo=comment.repo)
237 repo=comment.repo)
238
238
239 def create(self, text, repo, user, commit_id=None, pull_request=None,
239 def create(self, text, repo, user, commit_id=None, pull_request=None,
240 f_path=None, line_no=None, status_change=None,
240 f_path=None, line_no=None, status_change=None,
241 status_change_type=None, comment_type=None,
241 status_change_type=None, comment_type=None,
242 resolves_comment_id=None, closing_pr=False, send_email=True,
242 resolves_comment_id=None, closing_pr=False, send_email=True,
243 renderer=None, auth_user=None, extra_recipients=None):
243 renderer=None, auth_user=None, extra_recipients=None):
244 """
244 """
245 Creates new comment for commit or pull request.
245 Creates new comment for commit or pull request.
246 IF status_change is not none this comment is associated with a
246 IF status_change is not none this comment is associated with a
247 status change of commit or commit associated with pull request
247 status change of commit or commit associated with pull request
248
248
249 :param text:
249 :param text:
250 :param repo:
250 :param repo:
251 :param user:
251 :param user:
252 :param commit_id:
252 :param commit_id:
253 :param pull_request:
253 :param pull_request:
254 :param f_path:
254 :param f_path:
255 :param line_no:
255 :param line_no:
256 :param status_change: Label for status change
256 :param status_change: Label for status change
257 :param comment_type: Type of comment
257 :param comment_type: Type of comment
258 :param resolves_comment_id: id of comment which this one will resolve
258 :param resolves_comment_id: id of comment which this one will resolve
259 :param status_change_type: type of status change
259 :param status_change_type: type of status change
260 :param closing_pr:
260 :param closing_pr:
261 :param send_email:
261 :param send_email:
262 :param renderer: pick renderer for this comment
262 :param renderer: pick renderer for this comment
263 :param auth_user: current authenticated user calling this method
263 :param auth_user: current authenticated user calling this method
264 :param extra_recipients: list of extra users to be added to recipients
264 :param extra_recipients: list of extra users to be added to recipients
265 """
265 """
266
266
267 if not text:
267 if not text:
268 log.warning('Missing text for comment, skipping...')
268 log.warning('Missing text for comment, skipping...')
269 return
269 return
270 request = get_current_request()
270 request = get_current_request()
271 _ = request.translate
271 _ = request.translate
272
272
273 if not renderer:
273 if not renderer:
274 renderer = self._get_renderer(request=request)
274 renderer = self._get_renderer(request=request)
275
275
276 repo = self._get_repo(repo)
276 repo = self._get_repo(repo)
277 user = self._get_user(user)
277 user = self._get_user(user)
278 auth_user = auth_user or user
278 auth_user = auth_user or user
279
279
280 schema = comment_schema.CommentSchema()
280 schema = comment_schema.CommentSchema()
281 validated_kwargs = schema.deserialize(dict(
281 validated_kwargs = schema.deserialize(dict(
282 comment_body=text,
282 comment_body=text,
283 comment_type=comment_type,
283 comment_type=comment_type,
284 comment_file=f_path,
284 comment_file=f_path,
285 comment_line=line_no,
285 comment_line=line_no,
286 renderer_type=renderer,
286 renderer_type=renderer,
287 status_change=status_change_type,
287 status_change=status_change_type,
288 resolves_comment_id=resolves_comment_id,
288 resolves_comment_id=resolves_comment_id,
289 repo=repo.repo_id,
289 repo=repo.repo_id,
290 user=user.user_id,
290 user=user.user_id,
291 ))
291 ))
292
292
293 comment = ChangesetComment()
293 comment = ChangesetComment()
294 comment.renderer = validated_kwargs['renderer_type']
294 comment.renderer = validated_kwargs['renderer_type']
295 comment.text = validated_kwargs['comment_body']
295 comment.text = validated_kwargs['comment_body']
296 comment.f_path = validated_kwargs['comment_file']
296 comment.f_path = validated_kwargs['comment_file']
297 comment.line_no = validated_kwargs['comment_line']
297 comment.line_no = validated_kwargs['comment_line']
298 comment.comment_type = validated_kwargs['comment_type']
298 comment.comment_type = validated_kwargs['comment_type']
299
299
300 comment.repo = repo
300 comment.repo = repo
301 comment.author = user
301 comment.author = user
302 resolved_comment = self.__get_commit_comment(
302 resolved_comment = self.__get_commit_comment(
303 validated_kwargs['resolves_comment_id'])
303 validated_kwargs['resolves_comment_id'])
304 # check if the comment actually belongs to this PR
304 # check if the comment actually belongs to this PR
305 if resolved_comment and resolved_comment.pull_request and \
305 if resolved_comment and resolved_comment.pull_request and \
306 resolved_comment.pull_request != pull_request:
306 resolved_comment.pull_request != pull_request:
307 log.warning('Comment tried to resolved unrelated todo comment: %s',
307 log.warning('Comment tried to resolved unrelated todo comment: %s',
308 resolved_comment)
308 resolved_comment)
309 # comment not bound to this pull request, forbid
309 # comment not bound to this pull request, forbid
310 resolved_comment = None
310 resolved_comment = None
311
311
312 elif resolved_comment and resolved_comment.repo and \
312 elif resolved_comment and resolved_comment.repo and \
313 resolved_comment.repo != repo:
313 resolved_comment.repo != repo:
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
315 resolved_comment)
315 resolved_comment)
316 # comment not bound to this repo, forbid
316 # comment not bound to this repo, forbid
317 resolved_comment = None
317 resolved_comment = None
318
318
319 comment.resolved_comment = resolved_comment
319 comment.resolved_comment = resolved_comment
320
320
321 pull_request_id = pull_request
321 pull_request_id = pull_request
322
322
323 commit_obj = None
323 commit_obj = None
324 pull_request_obj = None
324 pull_request_obj = None
325
325
326 if commit_id:
326 if commit_id:
327 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
327 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
328 # do a lookup, so we don't pass something bad here
328 # do a lookup, so we don't pass something bad here
329 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
329 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
330 comment.revision = commit_obj.raw_id
330 comment.revision = commit_obj.raw_id
331
331
332 elif pull_request_id:
332 elif pull_request_id:
333 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
333 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
334 pull_request_obj = self.__get_pull_request(pull_request_id)
334 pull_request_obj = self.__get_pull_request(pull_request_id)
335 comment.pull_request = pull_request_obj
335 comment.pull_request = pull_request_obj
336 else:
336 else:
337 raise Exception('Please specify commit or pull_request_id')
337 raise Exception('Please specify commit or pull_request_id')
338
338
339 Session().add(comment)
339 Session().add(comment)
340 Session().flush()
340 Session().flush()
341 kwargs = {
341 kwargs = {
342 'user': user,
342 'user': user,
343 'renderer_type': renderer,
343 'renderer_type': renderer,
344 'repo_name': repo.repo_name,
344 'repo_name': repo.repo_name,
345 'status_change': status_change,
345 'status_change': status_change,
346 'status_change_type': status_change_type,
346 'status_change_type': status_change_type,
347 'comment_body': text,
347 'comment_body': text,
348 'comment_file': f_path,
348 'comment_file': f_path,
349 'comment_line': line_no,
349 'comment_line': line_no,
350 'comment_type': comment_type or 'note',
350 'comment_type': comment_type or 'note',
351 'comment_id': comment.comment_id
351 'comment_id': comment.comment_id
352 }
352 }
353
353
354 if commit_obj:
354 if commit_obj:
355 recipients = ChangesetComment.get_users(
355 recipients = ChangesetComment.get_users(
356 revision=commit_obj.raw_id)
356 revision=commit_obj.raw_id)
357 # add commit author if it's in RhodeCode system
357 # add commit author if it's in RhodeCode system
358 cs_author = User.get_from_cs_author(commit_obj.author)
358 cs_author = User.get_from_cs_author(commit_obj.author)
359 if not cs_author:
359 if not cs_author:
360 # use repo owner if we cannot extract the author correctly
360 # use repo owner if we cannot extract the author correctly
361 cs_author = repo.user
361 cs_author = repo.user
362 recipients += [cs_author]
362 recipients += [cs_author]
363
363
364 commit_comment_url = self.get_url(comment, request=request)
364 commit_comment_url = self.get_url(comment, request=request)
365 commit_comment_reply_url = self.get_url(
365 commit_comment_reply_url = self.get_url(
366 comment, request=request,
366 comment, request=request,
367 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
367 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
368
368
369 target_repo_url = h.link_to(
369 target_repo_url = h.link_to(
370 repo.repo_name,
370 repo.repo_name,
371 h.route_url('repo_summary', repo_name=repo.repo_name))
371 h.route_url('repo_summary', repo_name=repo.repo_name))
372
372
373 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
374 commit_id=commit_id)
375
373 # commit specifics
376 # commit specifics
374 kwargs.update({
377 kwargs.update({
375 'commit': commit_obj,
378 'commit': commit_obj,
376 'commit_message': commit_obj.message,
379 'commit_message': commit_obj.message,
377 'commit_target_repo_url': target_repo_url,
380 'commit_target_repo_url': target_repo_url,
378 'commit_comment_url': commit_comment_url,
381 'commit_comment_url': commit_comment_url,
379 'commit_comment_reply_url': commit_comment_reply_url
382 'commit_comment_reply_url': commit_comment_reply_url,
383 'commit_url': commit_url,
384 'thread_ids': [commit_url, commit_comment_url],
380 })
385 })
381
386
382 elif pull_request_obj:
387 elif pull_request_obj:
383 # get the current participants of this pull request
388 # get the current participants of this pull request
384 recipients = ChangesetComment.get_users(
389 recipients = ChangesetComment.get_users(
385 pull_request_id=pull_request_obj.pull_request_id)
390 pull_request_id=pull_request_obj.pull_request_id)
386 # add pull request author
391 # add pull request author
387 recipients += [pull_request_obj.author]
392 recipients += [pull_request_obj.author]
388
393
389 # add the reviewers to notification
394 # add the reviewers to notification
390 recipients += [x.user for x in pull_request_obj.reviewers]
395 recipients += [x.user for x in pull_request_obj.reviewers]
391
396
392 pr_target_repo = pull_request_obj.target_repo
397 pr_target_repo = pull_request_obj.target_repo
393 pr_source_repo = pull_request_obj.source_repo
398 pr_source_repo = pull_request_obj.source_repo
394
399
395 pr_comment_url = self.get_url(comment, request=request)
400 pr_comment_url = self.get_url(comment, request=request)
396 pr_comment_reply_url = self.get_url(
401 pr_comment_reply_url = self.get_url(
397 comment, request=request,
402 comment, request=request,
398 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
403 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
399
404
400 pr_url = h.route_url(
405 pr_url = h.route_url(
401 'pullrequest_show',
406 'pullrequest_show',
402 repo_name=pr_target_repo.repo_name,
407 repo_name=pr_target_repo.repo_name,
403 pull_request_id=pull_request_obj.pull_request_id, )
408 pull_request_id=pull_request_obj.pull_request_id, )
404
409
405 # set some variables for email notification
410 # set some variables for email notification
406 pr_target_repo_url = h.route_url(
411 pr_target_repo_url = h.route_url(
407 'repo_summary', repo_name=pr_target_repo.repo_name)
412 'repo_summary', repo_name=pr_target_repo.repo_name)
408
413
409 pr_source_repo_url = h.route_url(
414 pr_source_repo_url = h.route_url(
410 'repo_summary', repo_name=pr_source_repo.repo_name)
415 'repo_summary', repo_name=pr_source_repo.repo_name)
411
416
412 # pull request specifics
417 # pull request specifics
413 kwargs.update({
418 kwargs.update({
414 'pull_request': pull_request_obj,
419 'pull_request': pull_request_obj,
415 'pr_id': pull_request_obj.pull_request_id,
420 'pr_id': pull_request_obj.pull_request_id,
416 'pull_request_url': pr_url,
421 'pull_request_url': pr_url,
417 'pull_request_target_repo': pr_target_repo,
422 'pull_request_target_repo': pr_target_repo,
418 'pull_request_target_repo_url': pr_target_repo_url,
423 'pull_request_target_repo_url': pr_target_repo_url,
419 'pull_request_source_repo': pr_source_repo,
424 'pull_request_source_repo': pr_source_repo,
420 'pull_request_source_repo_url': pr_source_repo_url,
425 'pull_request_source_repo_url': pr_source_repo_url,
421 'pr_comment_url': pr_comment_url,
426 'pr_comment_url': pr_comment_url,
422 'pr_comment_reply_url': pr_comment_reply_url,
427 'pr_comment_reply_url': pr_comment_reply_url,
423 'pr_closing': closing_pr,
428 'pr_closing': closing_pr,
429 'thread_ids': [pr_url, pr_comment_url],
424 })
430 })
425
431
426 recipients += [self._get_user(u) for u in (extra_recipients or [])]
432 recipients += [self._get_user(u) for u in (extra_recipients or [])]
427
433
428 if send_email:
434 if send_email:
429 # pre-generate the subject for notification itself
435 # pre-generate the subject for notification itself
430 (subject,
436 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
431 _h, _e, # we don't care about those
432 body_plaintext) = EmailNotificationModel().render_email(
433 notification_type, **kwargs)
437 notification_type, **kwargs)
434
438
435 mention_recipients = set(
439 mention_recipients = set(
436 self._extract_mentions(text)).difference(recipients)
440 self._extract_mentions(text)).difference(recipients)
437
441
438 # create notification objects, and emails
442 # create notification objects, and emails
439 NotificationModel().create(
443 NotificationModel().create(
440 created_by=user,
444 created_by=user,
441 notification_subject=subject,
445 notification_subject=subject,
442 notification_body=body_plaintext,
446 notification_body=body_plaintext,
443 notification_type=notification_type,
447 notification_type=notification_type,
444 recipients=recipients,
448 recipients=recipients,
445 mention_recipients=mention_recipients,
449 mention_recipients=mention_recipients,
446 email_kwargs=kwargs,
450 email_kwargs=kwargs,
447 )
451 )
448
452
449 Session().flush()
453 Session().flush()
450 if comment.pull_request:
454 if comment.pull_request:
451 action = 'repo.pull_request.comment.create'
455 action = 'repo.pull_request.comment.create'
452 else:
456 else:
453 action = 'repo.commit.comment.create'
457 action = 'repo.commit.comment.create'
454
458
455 comment_data = comment.get_api_data()
459 comment_data = comment.get_api_data()
456 self._log_audit_action(
460 self._log_audit_action(
457 action, {'data': comment_data}, auth_user, comment)
461 action, {'data': comment_data}, auth_user, comment)
458
462
459 msg_url = ''
463 msg_url = ''
460 channel = None
464 channel = None
461 if commit_obj:
465 if commit_obj:
462 msg_url = commit_comment_url
466 msg_url = commit_comment_url
463 repo_name = repo.repo_name
467 repo_name = repo.repo_name
464 channel = u'/repo${}$/commit/{}'.format(
468 channel = u'/repo${}$/commit/{}'.format(
465 repo_name,
469 repo_name,
466 commit_obj.raw_id
470 commit_obj.raw_id
467 )
471 )
468 elif pull_request_obj:
472 elif pull_request_obj:
469 msg_url = pr_comment_url
473 msg_url = pr_comment_url
470 repo_name = pr_target_repo.repo_name
474 repo_name = pr_target_repo.repo_name
471 channel = u'/repo${}$/pr/{}'.format(
475 channel = u'/repo${}$/pr/{}'.format(
472 repo_name,
476 repo_name,
473 pull_request_id
477 pull_request_id
474 )
478 )
475
479
476 message = '<strong>{}</strong> {} - ' \
480 message = '<strong>{}</strong> {} - ' \
477 '<a onclick="window.location=\'{}\';' \
481 '<a onclick="window.location=\'{}\';' \
478 'window.location.reload()">' \
482 'window.location.reload()">' \
479 '<strong>{}</strong></a>'
483 '<strong>{}</strong></a>'
480 message = message.format(
484 message = message.format(
481 user.username, _('made a comment'), msg_url,
485 user.username, _('made a comment'), msg_url,
482 _('Show it now'))
486 _('Show it now'))
483
487
484 channelstream.post_message(
488 channelstream.post_message(
485 channel, message, user.username,
489 channel, message, user.username,
486 registry=get_current_registry())
490 registry=get_current_registry())
487
491
488 return comment
492 return comment
489
493
490 def edit(self, comment_id, text, auth_user, version):
494 def edit(self, comment_id, text, auth_user, version):
491 """
495 """
492 Change existing comment for commit or pull request.
496 Change existing comment for commit or pull request.
493
497
494 :param comment_id:
498 :param comment_id:
495 :param text:
499 :param text:
496 :param auth_user: current authenticated user calling this method
500 :param auth_user: current authenticated user calling this method
497 :param version: last comment version
501 :param version: last comment version
498 """
502 """
499 if not text:
503 if not text:
500 log.warning('Missing text for comment, skipping...')
504 log.warning('Missing text for comment, skipping...')
501 return
505 return
502
506
503 comment = ChangesetComment.get(comment_id)
507 comment = ChangesetComment.get(comment_id)
504 old_comment_text = comment.text
508 old_comment_text = comment.text
505 comment.text = text
509 comment.text = text
506 comment.modified_at = datetime.datetime.now()
510 comment.modified_at = datetime.datetime.now()
507 version = safe_int(version)
511 version = safe_int(version)
508
512
509 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
513 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
510 # would return 3 here
514 # would return 3 here
511 comment_version = ChangesetCommentHistory.get_version(comment_id)
515 comment_version = ChangesetCommentHistory.get_version(comment_id)
512
516
513 if isinstance(version, (int, long)) and (comment_version - version) != 1:
517 if isinstance(version, (int, long)) and (comment_version - version) != 1:
514 log.warning(
518 log.warning(
515 'Version mismatch comment_version {} submitted {}, skipping'.format(
519 'Version mismatch comment_version {} submitted {}, skipping'.format(
516 comment_version-1, # -1 since note above
520 comment_version-1, # -1 since note above
517 version
521 version
518 )
522 )
519 )
523 )
520 raise CommentVersionMismatch()
524 raise CommentVersionMismatch()
521
525
522 comment_history = ChangesetCommentHistory()
526 comment_history = ChangesetCommentHistory()
523 comment_history.comment_id = comment_id
527 comment_history.comment_id = comment_id
524 comment_history.version = comment_version
528 comment_history.version = comment_version
525 comment_history.created_by_user_id = auth_user.user_id
529 comment_history.created_by_user_id = auth_user.user_id
526 comment_history.text = old_comment_text
530 comment_history.text = old_comment_text
527 # TODO add email notification
531 # TODO add email notification
528 Session().add(comment_history)
532 Session().add(comment_history)
529 Session().add(comment)
533 Session().add(comment)
530 Session().flush()
534 Session().flush()
531
535
532 if comment.pull_request:
536 if comment.pull_request:
533 action = 'repo.pull_request.comment.edit'
537 action = 'repo.pull_request.comment.edit'
534 else:
538 else:
535 action = 'repo.commit.comment.edit'
539 action = 'repo.commit.comment.edit'
536
540
537 comment_data = comment.get_api_data()
541 comment_data = comment.get_api_data()
538 comment_data['old_comment_text'] = old_comment_text
542 comment_data['old_comment_text'] = old_comment_text
539 self._log_audit_action(
543 self._log_audit_action(
540 action, {'data': comment_data}, auth_user, comment)
544 action, {'data': comment_data}, auth_user, comment)
541
545
542 return comment_history
546 return comment_history
543
547
544 def delete(self, comment, auth_user):
548 def delete(self, comment, auth_user):
545 """
549 """
546 Deletes given comment
550 Deletes given comment
547 """
551 """
548 comment = self.__get_commit_comment(comment)
552 comment = self.__get_commit_comment(comment)
549 old_data = comment.get_api_data()
553 old_data = comment.get_api_data()
550 Session().delete(comment)
554 Session().delete(comment)
551
555
552 if comment.pull_request:
556 if comment.pull_request:
553 action = 'repo.pull_request.comment.delete'
557 action = 'repo.pull_request.comment.delete'
554 else:
558 else:
555 action = 'repo.commit.comment.delete'
559 action = 'repo.commit.comment.delete'
556
560
557 self._log_audit_action(
561 self._log_audit_action(
558 action, {'old_data': old_data}, auth_user, comment)
562 action, {'old_data': old_data}, auth_user, comment)
559
563
560 return comment
564 return comment
561
565
562 def get_all_comments(self, repo_id, revision=None, pull_request=None):
566 def get_all_comments(self, repo_id, revision=None, pull_request=None):
563 q = ChangesetComment.query()\
567 q = ChangesetComment.query()\
564 .filter(ChangesetComment.repo_id == repo_id)
568 .filter(ChangesetComment.repo_id == repo_id)
565 if revision:
569 if revision:
566 q = q.filter(ChangesetComment.revision == revision)
570 q = q.filter(ChangesetComment.revision == revision)
567 elif pull_request:
571 elif pull_request:
568 pull_request = self.__get_pull_request(pull_request)
572 pull_request = self.__get_pull_request(pull_request)
569 q = q.filter(ChangesetComment.pull_request == pull_request)
573 q = q.filter(ChangesetComment.pull_request == pull_request)
570 else:
574 else:
571 raise Exception('Please specify commit or pull_request')
575 raise Exception('Please specify commit or pull_request')
572 q = q.order_by(ChangesetComment.created_on)
576 q = q.order_by(ChangesetComment.created_on)
573 return q.all()
577 return q.all()
574
578
575 def get_url(self, comment, request=None, permalink=False, anchor=None):
579 def get_url(self, comment, request=None, permalink=False, anchor=None):
576 if not request:
580 if not request:
577 request = get_current_request()
581 request = get_current_request()
578
582
579 comment = self.__get_commit_comment(comment)
583 comment = self.__get_commit_comment(comment)
580 if anchor is None:
584 if anchor is None:
581 anchor = 'comment-{}'.format(comment.comment_id)
585 anchor = 'comment-{}'.format(comment.comment_id)
582
586
583 if comment.pull_request:
587 if comment.pull_request:
584 pull_request = comment.pull_request
588 pull_request = comment.pull_request
585 if permalink:
589 if permalink:
586 return request.route_url(
590 return request.route_url(
587 'pull_requests_global',
591 'pull_requests_global',
588 pull_request_id=pull_request.pull_request_id,
592 pull_request_id=pull_request.pull_request_id,
589 _anchor=anchor)
593 _anchor=anchor)
590 else:
594 else:
591 return request.route_url(
595 return request.route_url(
592 'pullrequest_show',
596 'pullrequest_show',
593 repo_name=safe_str(pull_request.target_repo.repo_name),
597 repo_name=safe_str(pull_request.target_repo.repo_name),
594 pull_request_id=pull_request.pull_request_id,
598 pull_request_id=pull_request.pull_request_id,
595 _anchor=anchor)
599 _anchor=anchor)
596
600
597 else:
601 else:
598 repo = comment.repo
602 repo = comment.repo
599 commit_id = comment.revision
603 commit_id = comment.revision
600
604
601 if permalink:
605 if permalink:
602 return request.route_url(
606 return request.route_url(
603 'repo_commit', repo_name=safe_str(repo.repo_id),
607 'repo_commit', repo_name=safe_str(repo.repo_id),
604 commit_id=commit_id,
608 commit_id=commit_id,
605 _anchor=anchor)
609 _anchor=anchor)
606
610
607 else:
611 else:
608 return request.route_url(
612 return request.route_url(
609 'repo_commit', repo_name=safe_str(repo.repo_name),
613 'repo_commit', repo_name=safe_str(repo.repo_name),
610 commit_id=commit_id,
614 commit_id=commit_id,
611 _anchor=anchor)
615 _anchor=anchor)
612
616
613 def get_comments(self, repo_id, revision=None, pull_request=None):
617 def get_comments(self, repo_id, revision=None, pull_request=None):
614 """
618 """
615 Gets main comments based on revision or pull_request_id
619 Gets main comments based on revision or pull_request_id
616
620
617 :param repo_id:
621 :param repo_id:
618 :param revision:
622 :param revision:
619 :param pull_request:
623 :param pull_request:
620 """
624 """
621
625
622 q = ChangesetComment.query()\
626 q = ChangesetComment.query()\
623 .filter(ChangesetComment.repo_id == repo_id)\
627 .filter(ChangesetComment.repo_id == repo_id)\
624 .filter(ChangesetComment.line_no == None)\
628 .filter(ChangesetComment.line_no == None)\
625 .filter(ChangesetComment.f_path == None)
629 .filter(ChangesetComment.f_path == None)
626 if revision:
630 if revision:
627 q = q.filter(ChangesetComment.revision == revision)
631 q = q.filter(ChangesetComment.revision == revision)
628 elif pull_request:
632 elif pull_request:
629 pull_request = self.__get_pull_request(pull_request)
633 pull_request = self.__get_pull_request(pull_request)
630 q = q.filter(ChangesetComment.pull_request == pull_request)
634 q = q.filter(ChangesetComment.pull_request == pull_request)
631 else:
635 else:
632 raise Exception('Please specify commit or pull_request')
636 raise Exception('Please specify commit or pull_request')
633 q = q.order_by(ChangesetComment.created_on)
637 q = q.order_by(ChangesetComment.created_on)
634 return q.all()
638 return q.all()
635
639
636 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
640 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
637 q = self._get_inline_comments_query(repo_id, revision, pull_request)
641 q = self._get_inline_comments_query(repo_id, revision, pull_request)
638 return self._group_comments_by_path_and_line_number(q)
642 return self._group_comments_by_path_and_line_number(q)
639
643
640 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
644 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
641 version=None):
645 version=None):
642 inline_cnt = 0
646 inline_cnt = 0
643 for fname, per_line_comments in inline_comments.iteritems():
647 for fname, per_line_comments in inline_comments.iteritems():
644 for lno, comments in per_line_comments.iteritems():
648 for lno, comments in per_line_comments.iteritems():
645 for comm in comments:
649 for comm in comments:
646 if not comm.outdated_at_version(version) and skip_outdated:
650 if not comm.outdated_at_version(version) and skip_outdated:
647 inline_cnt += 1
651 inline_cnt += 1
648
652
649 return inline_cnt
653 return inline_cnt
650
654
651 def get_outdated_comments(self, repo_id, pull_request):
655 def get_outdated_comments(self, repo_id, pull_request):
652 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
656 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
653 # of a pull request.
657 # of a pull request.
654 q = self._all_inline_comments_of_pull_request(pull_request)
658 q = self._all_inline_comments_of_pull_request(pull_request)
655 q = q.filter(
659 q = q.filter(
656 ChangesetComment.display_state ==
660 ChangesetComment.display_state ==
657 ChangesetComment.COMMENT_OUTDATED
661 ChangesetComment.COMMENT_OUTDATED
658 ).order_by(ChangesetComment.comment_id.asc())
662 ).order_by(ChangesetComment.comment_id.asc())
659
663
660 return self._group_comments_by_path_and_line_number(q)
664 return self._group_comments_by_path_and_line_number(q)
661
665
662 def _get_inline_comments_query(self, repo_id, revision, pull_request):
666 def _get_inline_comments_query(self, repo_id, revision, pull_request):
663 # TODO: johbo: Split this into two methods: One for PR and one for
667 # TODO: johbo: Split this into two methods: One for PR and one for
664 # commit.
668 # commit.
665 if revision:
669 if revision:
666 q = Session().query(ChangesetComment).filter(
670 q = Session().query(ChangesetComment).filter(
667 ChangesetComment.repo_id == repo_id,
671 ChangesetComment.repo_id == repo_id,
668 ChangesetComment.line_no != null(),
672 ChangesetComment.line_no != null(),
669 ChangesetComment.f_path != null(),
673 ChangesetComment.f_path != null(),
670 ChangesetComment.revision == revision)
674 ChangesetComment.revision == revision)
671
675
672 elif pull_request:
676 elif pull_request:
673 pull_request = self.__get_pull_request(pull_request)
677 pull_request = self.__get_pull_request(pull_request)
674 if not CommentsModel.use_outdated_comments(pull_request):
678 if not CommentsModel.use_outdated_comments(pull_request):
675 q = self._visible_inline_comments_of_pull_request(pull_request)
679 q = self._visible_inline_comments_of_pull_request(pull_request)
676 else:
680 else:
677 q = self._all_inline_comments_of_pull_request(pull_request)
681 q = self._all_inline_comments_of_pull_request(pull_request)
678
682
679 else:
683 else:
680 raise Exception('Please specify commit or pull_request_id')
684 raise Exception('Please specify commit or pull_request_id')
681 q = q.order_by(ChangesetComment.comment_id.asc())
685 q = q.order_by(ChangesetComment.comment_id.asc())
682 return q
686 return q
683
687
684 def _group_comments_by_path_and_line_number(self, q):
688 def _group_comments_by_path_and_line_number(self, q):
685 comments = q.all()
689 comments = q.all()
686 paths = collections.defaultdict(lambda: collections.defaultdict(list))
690 paths = collections.defaultdict(lambda: collections.defaultdict(list))
687 for co in comments:
691 for co in comments:
688 paths[co.f_path][co.line_no].append(co)
692 paths[co.f_path][co.line_no].append(co)
689 return paths
693 return paths
690
694
691 @classmethod
695 @classmethod
692 def needed_extra_diff_context(cls):
696 def needed_extra_diff_context(cls):
693 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
697 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
694
698
695 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
699 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
696 if not CommentsModel.use_outdated_comments(pull_request):
700 if not CommentsModel.use_outdated_comments(pull_request):
697 return
701 return
698
702
699 comments = self._visible_inline_comments_of_pull_request(pull_request)
703 comments = self._visible_inline_comments_of_pull_request(pull_request)
700 comments_to_outdate = comments.all()
704 comments_to_outdate = comments.all()
701
705
702 for comment in comments_to_outdate:
706 for comment in comments_to_outdate:
703 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
707 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
704
708
705 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
709 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
706 diff_line = _parse_comment_line_number(comment.line_no)
710 diff_line = _parse_comment_line_number(comment.line_no)
707
711
708 try:
712 try:
709 old_context = old_diff_proc.get_context_of_line(
713 old_context = old_diff_proc.get_context_of_line(
710 path=comment.f_path, diff_line=diff_line)
714 path=comment.f_path, diff_line=diff_line)
711 new_context = new_diff_proc.get_context_of_line(
715 new_context = new_diff_proc.get_context_of_line(
712 path=comment.f_path, diff_line=diff_line)
716 path=comment.f_path, diff_line=diff_line)
713 except (diffs.LineNotInDiffException,
717 except (diffs.LineNotInDiffException,
714 diffs.FileNotInDiffException):
718 diffs.FileNotInDiffException):
715 comment.display_state = ChangesetComment.COMMENT_OUTDATED
719 comment.display_state = ChangesetComment.COMMENT_OUTDATED
716 return
720 return
717
721
718 if old_context == new_context:
722 if old_context == new_context:
719 return
723 return
720
724
721 if self._should_relocate_diff_line(diff_line):
725 if self._should_relocate_diff_line(diff_line):
722 new_diff_lines = new_diff_proc.find_context(
726 new_diff_lines = new_diff_proc.find_context(
723 path=comment.f_path, context=old_context,
727 path=comment.f_path, context=old_context,
724 offset=self.DIFF_CONTEXT_BEFORE)
728 offset=self.DIFF_CONTEXT_BEFORE)
725 if not new_diff_lines:
729 if not new_diff_lines:
726 comment.display_state = ChangesetComment.COMMENT_OUTDATED
730 comment.display_state = ChangesetComment.COMMENT_OUTDATED
727 else:
731 else:
728 new_diff_line = self._choose_closest_diff_line(
732 new_diff_line = self._choose_closest_diff_line(
729 diff_line, new_diff_lines)
733 diff_line, new_diff_lines)
730 comment.line_no = _diff_to_comment_line_number(new_diff_line)
734 comment.line_no = _diff_to_comment_line_number(new_diff_line)
731 else:
735 else:
732 comment.display_state = ChangesetComment.COMMENT_OUTDATED
736 comment.display_state = ChangesetComment.COMMENT_OUTDATED
733
737
734 def _should_relocate_diff_line(self, diff_line):
738 def _should_relocate_diff_line(self, diff_line):
735 """
739 """
736 Checks if relocation shall be tried for the given `diff_line`.
740 Checks if relocation shall be tried for the given `diff_line`.
737
741
738 If a comment points into the first lines, then we can have a situation
742 If a comment points into the first lines, then we can have a situation
739 that after an update another line has been added on top. In this case
743 that after an update another line has been added on top. In this case
740 we would find the context still and move the comment around. This
744 we would find the context still and move the comment around. This
741 would be wrong.
745 would be wrong.
742 """
746 """
743 should_relocate = (
747 should_relocate = (
744 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
748 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
745 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
749 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
746 return should_relocate
750 return should_relocate
747
751
748 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
752 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
749 candidate = new_diff_lines[0]
753 candidate = new_diff_lines[0]
750 best_delta = _diff_line_delta(diff_line, candidate)
754 best_delta = _diff_line_delta(diff_line, candidate)
751 for new_diff_line in new_diff_lines[1:]:
755 for new_diff_line in new_diff_lines[1:]:
752 delta = _diff_line_delta(diff_line, new_diff_line)
756 delta = _diff_line_delta(diff_line, new_diff_line)
753 if delta < best_delta:
757 if delta < best_delta:
754 candidate = new_diff_line
758 candidate = new_diff_line
755 best_delta = delta
759 best_delta = delta
756 return candidate
760 return candidate
757
761
758 def _visible_inline_comments_of_pull_request(self, pull_request):
762 def _visible_inline_comments_of_pull_request(self, pull_request):
759 comments = self._all_inline_comments_of_pull_request(pull_request)
763 comments = self._all_inline_comments_of_pull_request(pull_request)
760 comments = comments.filter(
764 comments = comments.filter(
761 coalesce(ChangesetComment.display_state, '') !=
765 coalesce(ChangesetComment.display_state, '') !=
762 ChangesetComment.COMMENT_OUTDATED)
766 ChangesetComment.COMMENT_OUTDATED)
763 return comments
767 return comments
764
768
765 def _all_inline_comments_of_pull_request(self, pull_request):
769 def _all_inline_comments_of_pull_request(self, pull_request):
766 comments = Session().query(ChangesetComment)\
770 comments = Session().query(ChangesetComment)\
767 .filter(ChangesetComment.line_no != None)\
771 .filter(ChangesetComment.line_no != None)\
768 .filter(ChangesetComment.f_path != None)\
772 .filter(ChangesetComment.f_path != None)\
769 .filter(ChangesetComment.pull_request == pull_request)
773 .filter(ChangesetComment.pull_request == pull_request)
770 return comments
774 return comments
771
775
772 def _all_general_comments_of_pull_request(self, pull_request):
776 def _all_general_comments_of_pull_request(self, pull_request):
773 comments = Session().query(ChangesetComment)\
777 comments = Session().query(ChangesetComment)\
774 .filter(ChangesetComment.line_no == None)\
778 .filter(ChangesetComment.line_no == None)\
775 .filter(ChangesetComment.f_path == None)\
779 .filter(ChangesetComment.f_path == None)\
776 .filter(ChangesetComment.pull_request == pull_request)
780 .filter(ChangesetComment.pull_request == pull_request)
777
781
778 return comments
782 return comments
779
783
780 @staticmethod
784 @staticmethod
781 def use_outdated_comments(pull_request):
785 def use_outdated_comments(pull_request):
782 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
786 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
783 settings = settings_model.get_general_settings()
787 settings = settings_model.get_general_settings()
784 return settings.get('rhodecode_use_outdated_comments', False)
788 return settings.get('rhodecode_use_outdated_comments', False)
785
789
786 def trigger_commit_comment_hook(self, repo, user, action, data=None):
790 def trigger_commit_comment_hook(self, repo, user, action, data=None):
787 repo = self._get_repo(repo)
791 repo = self._get_repo(repo)
788 target_scm = repo.scm_instance()
792 target_scm = repo.scm_instance()
789 if action == 'create':
793 if action == 'create':
790 trigger_hook = hooks_utils.trigger_comment_commit_hooks
794 trigger_hook = hooks_utils.trigger_comment_commit_hooks
791 elif action == 'edit':
795 elif action == 'edit':
792 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
796 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
793 else:
797 else:
794 return
798 return
795
799
796 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
800 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
797 repo, action, trigger_hook)
801 repo, action, trigger_hook)
798 trigger_hook(
802 trigger_hook(
799 username=user.username,
803 username=user.username,
800 repo_name=repo.repo_name,
804 repo_name=repo.repo_name,
801 repo_type=target_scm.alias,
805 repo_type=target_scm.alias,
802 repo=repo,
806 repo=repo,
803 data=data)
807 data=data)
804
808
805
809
806 def _parse_comment_line_number(line_no):
810 def _parse_comment_line_number(line_no):
807 """
811 """
808 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
812 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
809 """
813 """
810 old_line = None
814 old_line = None
811 new_line = None
815 new_line = None
812 if line_no.startswith('o'):
816 if line_no.startswith('o'):
813 old_line = int(line_no[1:])
817 old_line = int(line_no[1:])
814 elif line_no.startswith('n'):
818 elif line_no.startswith('n'):
815 new_line = int(line_no[1:])
819 new_line = int(line_no[1:])
816 else:
820 else:
817 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
821 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
818 return diffs.DiffLineNumber(old_line, new_line)
822 return diffs.DiffLineNumber(old_line, new_line)
819
823
820
824
821 def _diff_to_comment_line_number(diff_line):
825 def _diff_to_comment_line_number(diff_line):
822 if diff_line.new is not None:
826 if diff_line.new is not None:
823 return u'n{}'.format(diff_line.new)
827 return u'n{}'.format(diff_line.new)
824 elif diff_line.old is not None:
828 elif diff_line.old is not None:
825 return u'o{}'.format(diff_line.old)
829 return u'o{}'.format(diff_line.old)
826 return u''
830 return u''
827
831
828
832
829 def _diff_line_delta(a, b):
833 def _diff_line_delta(a, b):
830 if None not in (a.new, b.new):
834 if None not in (a.new, b.new):
831 return abs(a.new - b.new)
835 return abs(a.new - b.new)
832 elif None not in (a.old, b.old):
836 elif None not in (a.old, b.old):
833 return abs(a.old - b.old)
837 return abs(a.old - b.old)
834 else:
838 else:
835 raise ValueError(
839 raise ValueError(
836 "Cannot compute delta between {} and {}".format(a, b))
840 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,411 +1,406 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 :param notification_body: body of notification text
68 :param notification_body: body of notification text
69 :param notification_type: type of notification, based on that we
69 :param notification_type: type of notification, based on that we
70 pick templates
70 pick templates
71
71
72 :param recipients: list of int, str or User objects, when None
72 :param recipients: list of int, str or User objects, when None
73 is given send to all admins
73 is given send to all admins
74 :param mention_recipients: list of int, str or User objects,
74 :param mention_recipients: list of int, str or User objects,
75 that were mentioned
75 that were mentioned
76 :param with_email: send email with this notification
76 :param with_email: send email with this notification
77 :param email_kwargs: dict with arguments to generate email
77 :param email_kwargs: dict with arguments to generate email
78 """
78 """
79
79
80 from rhodecode.lib.celerylib import tasks, run_task
80 from rhodecode.lib.celerylib import tasks, run_task
81
81
82 if recipients and not getattr(recipients, '__iter__', False):
82 if recipients and not getattr(recipients, '__iter__', False):
83 raise Exception('recipients must be an iterable object')
83 raise Exception('recipients must be an iterable object')
84
84
85 created_by_obj = self._get_user(created_by)
85 created_by_obj = self._get_user(created_by)
86 # default MAIN body if not given
86 # default MAIN body if not given
87 email_kwargs = email_kwargs or {'body': notification_body}
87 email_kwargs = email_kwargs or {'body': notification_body}
88 mention_recipients = mention_recipients or set()
88 mention_recipients = mention_recipients or set()
89
89
90 if not created_by_obj:
90 if not created_by_obj:
91 raise Exception('unknown user %s' % created_by)
91 raise Exception('unknown user %s' % created_by)
92
92
93 if recipients is None:
93 if recipients is None:
94 # recipients is None means to all admins
94 # recipients is None means to all admins
95 recipients_objs = User.query().filter(User.admin == true()).all()
95 recipients_objs = User.query().filter(User.admin == true()).all()
96 log.debug('sending notifications %s to admins: %s',
96 log.debug('sending notifications %s to admins: %s',
97 notification_type, recipients_objs)
97 notification_type, recipients_objs)
98 else:
98 else:
99 recipients_objs = set()
99 recipients_objs = set()
100 for u in recipients:
100 for u in recipients:
101 obj = self._get_user(u)
101 obj = self._get_user(u)
102 if obj:
102 if obj:
103 recipients_objs.add(obj)
103 recipients_objs.add(obj)
104 else: # we didn't find this user, log the error and carry on
104 else: # we didn't find this user, log the error and carry on
105 log.error('cannot notify unknown user %r', u)
105 log.error('cannot notify unknown user %r', u)
106
106
107 if not recipients_objs:
107 if not recipients_objs:
108 raise Exception('no valid recipients specified')
108 raise Exception('no valid recipients specified')
109
109
110 log.debug('sending notifications %s to %s',
110 log.debug('sending notifications %s to %s',
111 notification_type, recipients_objs)
111 notification_type, recipients_objs)
112
112
113 # add mentioned users into recipients
113 # add mentioned users into recipients
114 final_recipients = set(recipients_objs).union(mention_recipients)
114 final_recipients = set(recipients_objs).union(mention_recipients)
115
115
116 notification = Notification.create(
116 notification = Notification.create(
117 created_by=created_by_obj, subject=notification_subject,
117 created_by=created_by_obj, subject=notification_subject,
118 body=notification_body, recipients=final_recipients,
118 body=notification_body, recipients=final_recipients,
119 type_=notification_type
119 type_=notification_type
120 )
120 )
121
121
122 if not with_email: # skip sending email, and just create notification
122 if not with_email: # skip sending email, and just create notification
123 return notification
123 return notification
124
124
125 # don't send email to person who created this comment
125 # don't send email to person who created this comment
126 rec_objs = set(recipients_objs).difference({created_by_obj})
126 rec_objs = set(recipients_objs).difference({created_by_obj})
127
127
128 # now notify all recipients in question
128 # now notify all recipients in question
129
129
130 for recipient in rec_objs.union(mention_recipients):
130 for recipient in rec_objs.union(mention_recipients):
131 # inject current recipient
131 # inject current recipient
132 email_kwargs['recipient'] = recipient
132 email_kwargs['recipient'] = recipient
133 email_kwargs['mention'] = recipient in mention_recipients
133 email_kwargs['mention'] = recipient in mention_recipients
134 (subject, headers, email_body,
134 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
135 email_body_plaintext) = EmailNotificationModel().render_email(
136 notification_type, **email_kwargs)
135 notification_type, **email_kwargs)
137
136
138 log.debug(
137 extra_headers = None
139 'Creating notification email task for user:`%s`', recipient)
138 if 'thread_ids' in email_kwargs:
139 extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
140
141 log.debug('Creating notification email task for user:`%s`', recipient)
140 task = run_task(
142 task = run_task(
141 tasks.send_email, recipient.email, subject,
143 tasks.send_email, recipient.email, subject,
142 email_body_plaintext, email_body)
144 email_body_plaintext, email_body, extra_headers=extra_headers)
143 log.debug('Created email task: %s', task)
145 log.debug('Created email task: %s', task)
144
146
145 return notification
147 return notification
146
148
147 def delete(self, user, notification):
149 def delete(self, user, notification):
148 # we don't want to remove actual notification just the assignment
150 # we don't want to remove actual notification just the assignment
149 try:
151 try:
150 notification = self.__get_notification(notification)
152 notification = self.__get_notification(notification)
151 user = self._get_user(user)
153 user = self._get_user(user)
152 if notification and user:
154 if notification and user:
153 obj = UserNotification.query()\
155 obj = UserNotification.query()\
154 .filter(UserNotification.user == user)\
156 .filter(UserNotification.user == user)\
155 .filter(UserNotification.notification == notification)\
157 .filter(UserNotification.notification == notification)\
156 .one()
158 .one()
157 Session().delete(obj)
159 Session().delete(obj)
158 return True
160 return True
159 except Exception:
161 except Exception:
160 log.error(traceback.format_exc())
162 log.error(traceback.format_exc())
161 raise
163 raise
162
164
163 def get_for_user(self, user, filter_=None):
165 def get_for_user(self, user, filter_=None):
164 """
166 """
165 Get mentions for given user, filter them if filter dict is given
167 Get mentions for given user, filter them if filter dict is given
166 """
168 """
167 user = self._get_user(user)
169 user = self._get_user(user)
168
170
169 q = UserNotification.query()\
171 q = UserNotification.query()\
170 .filter(UserNotification.user == user)\
172 .filter(UserNotification.user == user)\
171 .join((
173 .join((
172 Notification, UserNotification.notification_id ==
174 Notification, UserNotification.notification_id ==
173 Notification.notification_id))
175 Notification.notification_id))
174 if filter_ == ['all']:
176 if filter_ == ['all']:
175 q = q # no filter
177 q = q # no filter
176 elif filter_ == ['unread']:
178 elif filter_ == ['unread']:
177 q = q.filter(UserNotification.read == false())
179 q = q.filter(UserNotification.read == false())
178 elif filter_:
180 elif filter_:
179 q = q.filter(Notification.type_.in_(filter_))
181 q = q.filter(Notification.type_.in_(filter_))
180
182
181 return q
183 return q
182
184
183 def mark_read(self, user, notification):
185 def mark_read(self, user, notification):
184 try:
186 try:
185 notification = self.__get_notification(notification)
187 notification = self.__get_notification(notification)
186 user = self._get_user(user)
188 user = self._get_user(user)
187 if notification and user:
189 if notification and user:
188 obj = UserNotification.query()\
190 obj = UserNotification.query()\
189 .filter(UserNotification.user == user)\
191 .filter(UserNotification.user == user)\
190 .filter(UserNotification.notification == notification)\
192 .filter(UserNotification.notification == notification)\
191 .one()
193 .one()
192 obj.read = True
194 obj.read = True
193 Session().add(obj)
195 Session().add(obj)
194 return True
196 return True
195 except Exception:
197 except Exception:
196 log.error(traceback.format_exc())
198 log.error(traceback.format_exc())
197 raise
199 raise
198
200
199 def mark_all_read_for_user(self, user, filter_=None):
201 def mark_all_read_for_user(self, user, filter_=None):
200 user = self._get_user(user)
202 user = self._get_user(user)
201 q = UserNotification.query()\
203 q = UserNotification.query()\
202 .filter(UserNotification.user == user)\
204 .filter(UserNotification.user == user)\
203 .filter(UserNotification.read == false())\
205 .filter(UserNotification.read == false())\
204 .join((
206 .join((
205 Notification, UserNotification.notification_id ==
207 Notification, UserNotification.notification_id ==
206 Notification.notification_id))
208 Notification.notification_id))
207 if filter_ == ['unread']:
209 if filter_ == ['unread']:
208 q = q.filter(UserNotification.read == false())
210 q = q.filter(UserNotification.read == false())
209 elif filter_:
211 elif filter_:
210 q = q.filter(Notification.type_.in_(filter_))
212 q = q.filter(Notification.type_.in_(filter_))
211
213
212 # this is a little inefficient but sqlalchemy doesn't support
214 # this is a little inefficient but sqlalchemy doesn't support
213 # update on joined tables :(
215 # update on joined tables :(
214 for obj in q.all():
216 for obj in q.all():
215 obj.read = True
217 obj.read = True
216 Session().add(obj)
218 Session().add(obj)
217
219
218 def get_unread_cnt_for_user(self, user):
220 def get_unread_cnt_for_user(self, user):
219 user = self._get_user(user)
221 user = self._get_user(user)
220 return UserNotification.query()\
222 return UserNotification.query()\
221 .filter(UserNotification.read == false())\
223 .filter(UserNotification.read == false())\
222 .filter(UserNotification.user == user).count()
224 .filter(UserNotification.user == user).count()
223
225
224 def get_unread_for_user(self, user):
226 def get_unread_for_user(self, user):
225 user = self._get_user(user)
227 user = self._get_user(user)
226 return [x.notification for x in UserNotification.query()
228 return [x.notification for x in UserNotification.query()
227 .filter(UserNotification.read == false())
229 .filter(UserNotification.read == false())
228 .filter(UserNotification.user == user).all()]
230 .filter(UserNotification.user == user).all()]
229
231
230 def get_user_notification(self, user, notification):
232 def get_user_notification(self, user, notification):
231 user = self._get_user(user)
233 user = self._get_user(user)
232 notification = self.__get_notification(notification)
234 notification = self.__get_notification(notification)
233
235
234 return UserNotification.query()\
236 return UserNotification.query()\
235 .filter(UserNotification.notification == notification)\
237 .filter(UserNotification.notification == notification)\
236 .filter(UserNotification.user == user).scalar()
238 .filter(UserNotification.user == user).scalar()
237
239
238 def make_description(self, notification, translate, show_age=True):
240 def make_description(self, notification, translate, show_age=True):
239 """
241 """
240 Creates a human readable description based on properties
242 Creates a human readable description based on properties
241 of notification object
243 of notification object
242 """
244 """
243 _ = translate
245 _ = translate
244 _map = {
246 _map = {
245 notification.TYPE_CHANGESET_COMMENT: [
247 notification.TYPE_CHANGESET_COMMENT: [
246 _('%(user)s commented on commit %(date_or_age)s'),
248 _('%(user)s commented on commit %(date_or_age)s'),
247 _('%(user)s commented on commit at %(date_or_age)s'),
249 _('%(user)s commented on commit at %(date_or_age)s'),
248 ],
250 ],
249 notification.TYPE_MESSAGE: [
251 notification.TYPE_MESSAGE: [
250 _('%(user)s sent message %(date_or_age)s'),
252 _('%(user)s sent message %(date_or_age)s'),
251 _('%(user)s sent message at %(date_or_age)s'),
253 _('%(user)s sent message at %(date_or_age)s'),
252 ],
254 ],
253 notification.TYPE_MENTION: [
255 notification.TYPE_MENTION: [
254 _('%(user)s mentioned you %(date_or_age)s'),
256 _('%(user)s mentioned you %(date_or_age)s'),
255 _('%(user)s mentioned you at %(date_or_age)s'),
257 _('%(user)s mentioned you at %(date_or_age)s'),
256 ],
258 ],
257 notification.TYPE_REGISTRATION: [
259 notification.TYPE_REGISTRATION: [
258 _('%(user)s registered in RhodeCode %(date_or_age)s'),
260 _('%(user)s registered in RhodeCode %(date_or_age)s'),
259 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
261 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
260 ],
262 ],
261 notification.TYPE_PULL_REQUEST: [
263 notification.TYPE_PULL_REQUEST: [
262 _('%(user)s opened new pull request %(date_or_age)s'),
264 _('%(user)s opened new pull request %(date_or_age)s'),
263 _('%(user)s opened new pull request at %(date_or_age)s'),
265 _('%(user)s opened new pull request at %(date_or_age)s'),
264 ],
266 ],
265 notification.TYPE_PULL_REQUEST_UPDATE: [
267 notification.TYPE_PULL_REQUEST_UPDATE: [
266 _('%(user)s updated pull request %(date_or_age)s'),
268 _('%(user)s updated pull request %(date_or_age)s'),
267 _('%(user)s updated pull request at %(date_or_age)s'),
269 _('%(user)s updated pull request at %(date_or_age)s'),
268 ],
270 ],
269 notification.TYPE_PULL_REQUEST_COMMENT: [
271 notification.TYPE_PULL_REQUEST_COMMENT: [
270 _('%(user)s commented on pull request %(date_or_age)s'),
272 _('%(user)s commented on pull request %(date_or_age)s'),
271 _('%(user)s commented on pull request at %(date_or_age)s'),
273 _('%(user)s commented on pull request at %(date_or_age)s'),
272 ],
274 ],
273 }
275 }
274
276
275 templates = _map[notification.type_]
277 templates = _map[notification.type_]
276
278
277 if show_age:
279 if show_age:
278 template = templates[0]
280 template = templates[0]
279 date_or_age = h.age(notification.created_on)
281 date_or_age = h.age(notification.created_on)
280 if translate:
282 if translate:
281 date_or_age = translate(date_or_age)
283 date_or_age = translate(date_or_age)
282
284
283 if isinstance(date_or_age, TranslationString):
285 if isinstance(date_or_age, TranslationString):
284 date_or_age = date_or_age.interpolate()
286 date_or_age = date_or_age.interpolate()
285
287
286 else:
288 else:
287 template = templates[1]
289 template = templates[1]
288 date_or_age = h.format_date(notification.created_on)
290 date_or_age = h.format_date(notification.created_on)
289
291
290 return template % {
292 return template % {
291 'user': notification.created_by_user.username,
293 'user': notification.created_by_user.username,
292 'date_or_age': date_or_age,
294 'date_or_age': date_or_age,
293 }
295 }
294
296
295
297
296 class EmailNotificationModel(BaseModel):
298 class EmailNotificationModel(BaseModel):
297 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
299 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
298 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
300 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
299 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
301 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
300 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
302 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
301 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
303 TYPE_PULL_REQUEST_UPDATE = Notification.TYPE_PULL_REQUEST_UPDATE
302 TYPE_MAIN = Notification.TYPE_MESSAGE
304 TYPE_MAIN = Notification.TYPE_MESSAGE
303
305
304 TYPE_PASSWORD_RESET = 'password_reset'
306 TYPE_PASSWORD_RESET = 'password_reset'
305 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
307 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
306 TYPE_EMAIL_TEST = 'email_test'
308 TYPE_EMAIL_TEST = 'email_test'
307 TYPE_EMAIL_EXCEPTION = 'exception'
309 TYPE_EMAIL_EXCEPTION = 'exception'
308 TYPE_TEST = 'test'
310 TYPE_TEST = 'test'
309
311
310 email_types = {
312 email_types = {
311 TYPE_MAIN:
313 TYPE_MAIN:
312 'rhodecode:templates/email_templates/main.mako',
314 'rhodecode:templates/email_templates/main.mako',
313 TYPE_TEST:
315 TYPE_TEST:
314 'rhodecode:templates/email_templates/test.mako',
316 'rhodecode:templates/email_templates/test.mako',
315 TYPE_EMAIL_EXCEPTION:
317 TYPE_EMAIL_EXCEPTION:
316 'rhodecode:templates/email_templates/exception_tracker.mako',
318 'rhodecode:templates/email_templates/exception_tracker.mako',
317 TYPE_EMAIL_TEST:
319 TYPE_EMAIL_TEST:
318 'rhodecode:templates/email_templates/email_test.mako',
320 'rhodecode:templates/email_templates/email_test.mako',
319 TYPE_REGISTRATION:
321 TYPE_REGISTRATION:
320 'rhodecode:templates/email_templates/user_registration.mako',
322 'rhodecode:templates/email_templates/user_registration.mako',
321 TYPE_PASSWORD_RESET:
323 TYPE_PASSWORD_RESET:
322 'rhodecode:templates/email_templates/password_reset.mako',
324 'rhodecode:templates/email_templates/password_reset.mako',
323 TYPE_PASSWORD_RESET_CONFIRMATION:
325 TYPE_PASSWORD_RESET_CONFIRMATION:
324 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
326 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
325 TYPE_COMMIT_COMMENT:
327 TYPE_COMMIT_COMMENT:
326 'rhodecode:templates/email_templates/commit_comment.mako',
328 'rhodecode:templates/email_templates/commit_comment.mako',
327 TYPE_PULL_REQUEST:
329 TYPE_PULL_REQUEST:
328 'rhodecode:templates/email_templates/pull_request_review.mako',
330 'rhodecode:templates/email_templates/pull_request_review.mako',
329 TYPE_PULL_REQUEST_COMMENT:
331 TYPE_PULL_REQUEST_COMMENT:
330 'rhodecode:templates/email_templates/pull_request_comment.mako',
332 'rhodecode:templates/email_templates/pull_request_comment.mako',
331 TYPE_PULL_REQUEST_UPDATE:
333 TYPE_PULL_REQUEST_UPDATE:
332 'rhodecode:templates/email_templates/pull_request_update.mako',
334 'rhodecode:templates/email_templates/pull_request_update.mako',
333 }
335 }
334
336
335 premailer_instance = premailer.Premailer(
337 premailer_instance = premailer.Premailer(
336 cssutils_logging_level=logging.ERROR,
338 cssutils_logging_level=logging.ERROR,
337 cssutils_logging_handler=logging.getLogger().handlers[0]
339 cssutils_logging_handler=logging.getLogger().handlers[0]
338 if logging.getLogger().handlers else None,
340 if logging.getLogger().handlers else None,
339 )
341 )
340
342
341 def __init__(self):
343 def __init__(self):
342 """
344 """
343 Example usage::
345 Example usage::
344
346
345 (subject, headers, email_body,
347 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
346 email_body_plaintext) = EmailNotificationModel().render_email(
347 EmailNotificationModel.TYPE_TEST, **email_kwargs)
348 EmailNotificationModel.TYPE_TEST, **email_kwargs)
348
349
349 """
350 """
350 super(EmailNotificationModel, self).__init__()
351 super(EmailNotificationModel, self).__init__()
351 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
352 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
352
353
353 def _update_kwargs_for_render(self, kwargs):
354 def _update_kwargs_for_render(self, kwargs):
354 """
355 """
355 Inject params required for Mako rendering
356 Inject params required for Mako rendering
356
357
357 :param kwargs:
358 :param kwargs:
358 """
359 """
359
360
360 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
361 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
361 kwargs['rhodecode_version'] = rhodecode.__version__
362 kwargs['rhodecode_version'] = rhodecode.__version__
362 instance_url = h.route_url('home')
363 instance_url = h.route_url('home')
363 _kwargs = {
364 _kwargs = {
364 'instance_url': instance_url,
365 'instance_url': instance_url,
365 'whitespace_filter': self.whitespace_filter
366 'whitespace_filter': self.whitespace_filter
366 }
367 }
367 _kwargs.update(kwargs)
368 _kwargs.update(kwargs)
368 return _kwargs
369 return _kwargs
369
370
370 def whitespace_filter(self, text):
371 def whitespace_filter(self, text):
371 return text.replace('\n', '').replace('\t', '')
372 return text.replace('\n', '').replace('\t', '')
372
373
373 def get_renderer(self, type_, request):
374 def get_renderer(self, type_, request):
374 template_name = self.email_types[type_]
375 template_name = self.email_types[type_]
375 return request.get_partial_renderer(template_name)
376 return request.get_partial_renderer(template_name)
376
377
377 def render_email(self, type_, **kwargs):
378 def render_email(self, type_, **kwargs):
378 """
379 """
379 renders template for email, and returns a tuple of
380 renders template for email, and returns a tuple of
380 (subject, email_headers, email_html_body, email_plaintext_body)
381 (subject, email_headers, email_html_body, email_plaintext_body)
381 """
382 """
382 # translator and helpers inject
383 # translator and helpers inject
383 _kwargs = self._update_kwargs_for_render(kwargs)
384 _kwargs = self._update_kwargs_for_render(kwargs)
384 request = get_current_request()
385 request = get_current_request()
385 email_template = self.get_renderer(type_, request=request)
386 email_template = self.get_renderer(type_, request=request)
386
387
387 subject = email_template.render('subject', **_kwargs)
388 subject = email_template.render('subject', **_kwargs)
388
389
389 try:
390 try:
390 headers = email_template.render('headers', **_kwargs)
391 except AttributeError:
392 # it's not defined in template, ok we can skip it
393 headers = ''
394
395 try:
396 body_plaintext = email_template.render('body_plaintext', **_kwargs)
391 body_plaintext = email_template.render('body_plaintext', **_kwargs)
397 except AttributeError:
392 except AttributeError:
398 # it's not defined in template, ok we can skip it
393 # it's not defined in template, ok we can skip it
399 body_plaintext = ''
394 body_plaintext = ''
400
395
401 # render WHOLE template
396 # render WHOLE template
402 body = email_template.render(None, **_kwargs)
397 body = email_template.render(None, **_kwargs)
403
398
404 try:
399 try:
405 # Inline CSS styles and conversion
400 # Inline CSS styles and conversion
406 body = self.premailer_instance.transform(body)
401 body = self.premailer_instance.transform(body)
407 except Exception:
402 except Exception:
408 log.exception('Failed to parse body with premailer')
403 log.exception('Failed to parse body with premailer')
409 pass
404 pass
410
405
411 return subject, headers, body, body_plaintext
406 return subject, body, body_plaintext
@@ -1,2074 +1,2072 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 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30
30
31 import datetime
31 import datetime
32 import urllib
32 import urllib
33 import collections
33 import collections
34
34
35 from pyramid import compat
35 from pyramid import compat
36 from pyramid.threadlocal import get_current_request
36 from pyramid.threadlocal import get_current_request
37
37
38 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.lib.vcs.nodes import FileNode
39 from rhodecode.translation import lazy_ugettext
39 from rhodecode.translation import lazy_ugettext
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
41 from rhodecode.lib import audit_logger
41 from rhodecode.lib import audit_logger
42 from rhodecode.lib.compat import OrderedDict
42 from rhodecode.lib.compat import OrderedDict
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.markup_renderer import (
44 from rhodecode.lib.markup_renderer import (
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
46 from rhodecode.lib.utils2 import (
46 from rhodecode.lib.utils2 import (
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
48 get_current_rhodecode_user)
48 get_current_rhodecode_user)
49 from rhodecode.lib.vcs.backends.base import (
49 from rhodecode.lib.vcs.backends.base import (
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
51 TargetRefMissing, SourceRefMissing)
51 TargetRefMissing, SourceRefMissing)
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 CommitDoesNotExistError, EmptyRepositoryError)
54 CommitDoesNotExistError, EmptyRepositoryError)
55 from rhodecode.model import BaseModel
55 from rhodecode.model import BaseModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.comment import CommentsModel
57 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.db import (
58 from rhodecode.model.db import (
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62 from rhodecode.model.notification import NotificationModel, \
62 from rhodecode.model.notification import NotificationModel, \
63 EmailNotificationModel
63 EmailNotificationModel
64 from rhodecode.model.scm import ScmModel
64 from rhodecode.model.scm import ScmModel
65 from rhodecode.model.settings import VcsSettingsModel
65 from rhodecode.model.settings import VcsSettingsModel
66
66
67
67
68 log = logging.getLogger(__name__)
68 log = logging.getLogger(__name__)
69
69
70
70
71 # Data structure to hold the response data when updating commits during a pull
71 # Data structure to hold the response data when updating commits during a pull
72 # request update.
72 # request update.
73 class UpdateResponse(object):
73 class UpdateResponse(object):
74
74
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
76 commit_changes, source_changed, target_changed):
76 commit_changes, source_changed, target_changed):
77
77
78 self.executed = executed
78 self.executed = executed
79 self.reason = reason
79 self.reason = reason
80 self.new = new
80 self.new = new
81 self.old = old
81 self.old = old
82 self.common_ancestor_id = common_ancestor_id
82 self.common_ancestor_id = common_ancestor_id
83 self.changes = commit_changes
83 self.changes = commit_changes
84 self.source_changed = source_changed
84 self.source_changed = source_changed
85 self.target_changed = target_changed
85 self.target_changed = target_changed
86
86
87
87
88 def get_diff_info(
88 def get_diff_info(
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
90 get_commit_authors=True):
90 get_commit_authors=True):
91 """
91 """
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
93 This is also used for default reviewers logic
93 This is also used for default reviewers logic
94 """
94 """
95
95
96 source_scm = source_repo.scm_instance()
96 source_scm = source_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
98
98
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
100 if not ancestor_id:
100 if not ancestor_id:
101 raise ValueError(
101 raise ValueError(
102 'cannot calculate diff info without a common ancestor. '
102 'cannot calculate diff info without a common ancestor. '
103 'Make sure both repositories are related, and have a common forking commit.')
103 'Make sure both repositories are related, and have a common forking commit.')
104
104
105 # case here is that want a simple diff without incoming commits,
105 # case here is that want a simple diff without incoming commits,
106 # previewing what will be merged based only on commits in the source.
106 # previewing what will be merged based only on commits in the source.
107 log.debug('Using ancestor %s as source_ref instead of %s',
107 log.debug('Using ancestor %s as source_ref instead of %s',
108 ancestor_id, source_ref)
108 ancestor_id, source_ref)
109
109
110 # source of changes now is the common ancestor
110 # source of changes now is the common ancestor
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
112 # target commit becomes the source ref as it is the last commit
112 # target commit becomes the source ref as it is the last commit
113 # for diff generation this logic gives proper diff
113 # for diff generation this logic gives proper diff
114 target_commit = source_scm.get_commit(commit_id=source_ref)
114 target_commit = source_scm.get_commit(commit_id=source_ref)
115
115
116 vcs_diff = \
116 vcs_diff = \
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
118 ignore_whitespace=False, context=3)
118 ignore_whitespace=False, context=3)
119
119
120 diff_processor = diffs.DiffProcessor(
120 diff_processor = diffs.DiffProcessor(
121 vcs_diff, format='newdiff', diff_limit=None,
121 vcs_diff, format='newdiff', diff_limit=None,
122 file_limit=None, show_full_diff=True)
122 file_limit=None, show_full_diff=True)
123
123
124 _parsed = diff_processor.prepare()
124 _parsed = diff_processor.prepare()
125
125
126 all_files = []
126 all_files = []
127 all_files_changes = []
127 all_files_changes = []
128 changed_lines = {}
128 changed_lines = {}
129 stats = [0, 0]
129 stats = [0, 0]
130 for f in _parsed:
130 for f in _parsed:
131 all_files.append(f['filename'])
131 all_files.append(f['filename'])
132 all_files_changes.append({
132 all_files_changes.append({
133 'filename': f['filename'],
133 'filename': f['filename'],
134 'stats': f['stats']
134 'stats': f['stats']
135 })
135 })
136 stats[0] += f['stats']['added']
136 stats[0] += f['stats']['added']
137 stats[1] += f['stats']['deleted']
137 stats[1] += f['stats']['deleted']
138
138
139 changed_lines[f['filename']] = []
139 changed_lines[f['filename']] = []
140 if len(f['chunks']) < 2:
140 if len(f['chunks']) < 2:
141 continue
141 continue
142 # first line is "context" information
142 # first line is "context" information
143 for chunks in f['chunks'][1:]:
143 for chunks in f['chunks'][1:]:
144 for chunk in chunks['lines']:
144 for chunk in chunks['lines']:
145 if chunk['action'] not in ('del', 'mod'):
145 if chunk['action'] not in ('del', 'mod'):
146 continue
146 continue
147 changed_lines[f['filename']].append(chunk['old_lineno'])
147 changed_lines[f['filename']].append(chunk['old_lineno'])
148
148
149 commit_authors = []
149 commit_authors = []
150 user_counts = {}
150 user_counts = {}
151 email_counts = {}
151 email_counts = {}
152 author_counts = {}
152 author_counts = {}
153 _commit_cache = {}
153 _commit_cache = {}
154
154
155 commits = []
155 commits = []
156 if get_commit_authors:
156 if get_commit_authors:
157 commits = target_scm.compare(
157 commits = target_scm.compare(
158 target_ref, source_ref, source_scm, merge=True,
158 target_ref, source_ref, source_scm, merge=True,
159 pre_load=["author"])
159 pre_load=["author"])
160
160
161 for commit in commits:
161 for commit in commits:
162 user = User.get_from_cs_author(commit.author)
162 user = User.get_from_cs_author(commit.author)
163 if user and user not in commit_authors:
163 if user and user not in commit_authors:
164 commit_authors.append(user)
164 commit_authors.append(user)
165
165
166 # lines
166 # lines
167 if get_authors:
167 if get_authors:
168 target_commit = source_repo.get_commit(ancestor_id)
168 target_commit = source_repo.get_commit(ancestor_id)
169
169
170 for fname, lines in changed_lines.items():
170 for fname, lines in changed_lines.items():
171 try:
171 try:
172 node = target_commit.get_node(fname)
172 node = target_commit.get_node(fname)
173 except Exception:
173 except Exception:
174 continue
174 continue
175
175
176 if not isinstance(node, FileNode):
176 if not isinstance(node, FileNode):
177 continue
177 continue
178
178
179 for annotation in node.annotate:
179 for annotation in node.annotate:
180 line_no, commit_id, get_commit_func, line_text = annotation
180 line_no, commit_id, get_commit_func, line_text = annotation
181 if line_no in lines:
181 if line_no in lines:
182 if commit_id not in _commit_cache:
182 if commit_id not in _commit_cache:
183 _commit_cache[commit_id] = get_commit_func()
183 _commit_cache[commit_id] = get_commit_func()
184 commit = _commit_cache[commit_id]
184 commit = _commit_cache[commit_id]
185 author = commit.author
185 author = commit.author
186 email = commit.author_email
186 email = commit.author_email
187 user = User.get_from_cs_author(author)
187 user = User.get_from_cs_author(author)
188 if user:
188 if user:
189 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
189 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
190 author_counts[author] = author_counts.get(author, 0) + 1
190 author_counts[author] = author_counts.get(author, 0) + 1
191 email_counts[email] = email_counts.get(email, 0) + 1
191 email_counts[email] = email_counts.get(email, 0) + 1
192
192
193 return {
193 return {
194 'commits': commits,
194 'commits': commits,
195 'files': all_files_changes,
195 'files': all_files_changes,
196 'stats': stats,
196 'stats': stats,
197 'ancestor': ancestor_id,
197 'ancestor': ancestor_id,
198 # original authors of modified files
198 # original authors of modified files
199 'original_authors': {
199 'original_authors': {
200 'users': user_counts,
200 'users': user_counts,
201 'authors': author_counts,
201 'authors': author_counts,
202 'emails': email_counts,
202 'emails': email_counts,
203 },
203 },
204 'commit_authors': commit_authors
204 'commit_authors': commit_authors
205 }
205 }
206
206
207
207
208 class PullRequestModel(BaseModel):
208 class PullRequestModel(BaseModel):
209
209
210 cls = PullRequest
210 cls = PullRequest
211
211
212 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
212 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
213
213
214 UPDATE_STATUS_MESSAGES = {
214 UPDATE_STATUS_MESSAGES = {
215 UpdateFailureReason.NONE: lazy_ugettext(
215 UpdateFailureReason.NONE: lazy_ugettext(
216 'Pull request update successful.'),
216 'Pull request update successful.'),
217 UpdateFailureReason.UNKNOWN: lazy_ugettext(
217 UpdateFailureReason.UNKNOWN: lazy_ugettext(
218 'Pull request update failed because of an unknown error.'),
218 'Pull request update failed because of an unknown error.'),
219 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
219 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
220 'No update needed because the source and target have not changed.'),
220 'No update needed because the source and target have not changed.'),
221 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
221 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
222 'Pull request cannot be updated because the reference type is '
222 'Pull request cannot be updated because the reference type is '
223 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
223 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
224 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
224 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
225 'This pull request cannot be updated because the target '
225 'This pull request cannot be updated because the target '
226 'reference is missing.'),
226 'reference is missing.'),
227 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
227 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
228 'This pull request cannot be updated because the source '
228 'This pull request cannot be updated because the source '
229 'reference is missing.'),
229 'reference is missing.'),
230 }
230 }
231 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
231 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
232 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
232 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
233
233
234 def __get_pull_request(self, pull_request):
234 def __get_pull_request(self, pull_request):
235 return self._get_instance((
235 return self._get_instance((
236 PullRequest, PullRequestVersion), pull_request)
236 PullRequest, PullRequestVersion), pull_request)
237
237
238 def _check_perms(self, perms, pull_request, user, api=False):
238 def _check_perms(self, perms, pull_request, user, api=False):
239 if not api:
239 if not api:
240 return h.HasRepoPermissionAny(*perms)(
240 return h.HasRepoPermissionAny(*perms)(
241 user=user, repo_name=pull_request.target_repo.repo_name)
241 user=user, repo_name=pull_request.target_repo.repo_name)
242 else:
242 else:
243 return h.HasRepoPermissionAnyApi(*perms)(
243 return h.HasRepoPermissionAnyApi(*perms)(
244 user=user, repo_name=pull_request.target_repo.repo_name)
244 user=user, repo_name=pull_request.target_repo.repo_name)
245
245
246 def check_user_read(self, pull_request, user, api=False):
246 def check_user_read(self, pull_request, user, api=False):
247 _perms = ('repository.admin', 'repository.write', 'repository.read',)
247 _perms = ('repository.admin', 'repository.write', 'repository.read',)
248 return self._check_perms(_perms, pull_request, user, api)
248 return self._check_perms(_perms, pull_request, user, api)
249
249
250 def check_user_merge(self, pull_request, user, api=False):
250 def check_user_merge(self, pull_request, user, api=False):
251 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
251 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
252 return self._check_perms(_perms, pull_request, user, api)
252 return self._check_perms(_perms, pull_request, user, api)
253
253
254 def check_user_update(self, pull_request, user, api=False):
254 def check_user_update(self, pull_request, user, api=False):
255 owner = user.user_id == pull_request.user_id
255 owner = user.user_id == pull_request.user_id
256 return self.check_user_merge(pull_request, user, api) or owner
256 return self.check_user_merge(pull_request, user, api) or owner
257
257
258 def check_user_delete(self, pull_request, user):
258 def check_user_delete(self, pull_request, user):
259 owner = user.user_id == pull_request.user_id
259 owner = user.user_id == pull_request.user_id
260 _perms = ('repository.admin',)
260 _perms = ('repository.admin',)
261 return self._check_perms(_perms, pull_request, user) or owner
261 return self._check_perms(_perms, pull_request, user) or owner
262
262
263 def check_user_change_status(self, pull_request, user, api=False):
263 def check_user_change_status(self, pull_request, user, api=False):
264 reviewer = user.user_id in [x.user_id for x in
264 reviewer = user.user_id in [x.user_id for x in
265 pull_request.reviewers]
265 pull_request.reviewers]
266 return self.check_user_update(pull_request, user, api) or reviewer
266 return self.check_user_update(pull_request, user, api) or reviewer
267
267
268 def check_user_comment(self, pull_request, user):
268 def check_user_comment(self, pull_request, user):
269 owner = user.user_id == pull_request.user_id
269 owner = user.user_id == pull_request.user_id
270 return self.check_user_read(pull_request, user) or owner
270 return self.check_user_read(pull_request, user) or owner
271
271
272 def get(self, pull_request):
272 def get(self, pull_request):
273 return self.__get_pull_request(pull_request)
273 return self.__get_pull_request(pull_request)
274
274
275 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
275 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
276 statuses=None, opened_by=None, order_by=None,
276 statuses=None, opened_by=None, order_by=None,
277 order_dir='desc', only_created=False):
277 order_dir='desc', only_created=False):
278 repo = None
278 repo = None
279 if repo_name:
279 if repo_name:
280 repo = self._get_repo(repo_name)
280 repo = self._get_repo(repo_name)
281
281
282 q = PullRequest.query()
282 q = PullRequest.query()
283
283
284 if search_q:
284 if search_q:
285 like_expression = u'%{}%'.format(safe_unicode(search_q))
285 like_expression = u'%{}%'.format(safe_unicode(search_q))
286 q = q.join(User)
286 q = q.join(User)
287 q = q.filter(or_(
287 q = q.filter(or_(
288 cast(PullRequest.pull_request_id, String).ilike(like_expression),
288 cast(PullRequest.pull_request_id, String).ilike(like_expression),
289 User.username.ilike(like_expression),
289 User.username.ilike(like_expression),
290 PullRequest.title.ilike(like_expression),
290 PullRequest.title.ilike(like_expression),
291 PullRequest.description.ilike(like_expression),
291 PullRequest.description.ilike(like_expression),
292 ))
292 ))
293
293
294 # source or target
294 # source or target
295 if repo and source:
295 if repo and source:
296 q = q.filter(PullRequest.source_repo == repo)
296 q = q.filter(PullRequest.source_repo == repo)
297 elif repo:
297 elif repo:
298 q = q.filter(PullRequest.target_repo == repo)
298 q = q.filter(PullRequest.target_repo == repo)
299
299
300 # closed,opened
300 # closed,opened
301 if statuses:
301 if statuses:
302 q = q.filter(PullRequest.status.in_(statuses))
302 q = q.filter(PullRequest.status.in_(statuses))
303
303
304 # opened by filter
304 # opened by filter
305 if opened_by:
305 if opened_by:
306 q = q.filter(PullRequest.user_id.in_(opened_by))
306 q = q.filter(PullRequest.user_id.in_(opened_by))
307
307
308 # only get those that are in "created" state
308 # only get those that are in "created" state
309 if only_created:
309 if only_created:
310 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
310 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
311
311
312 if order_by:
312 if order_by:
313 order_map = {
313 order_map = {
314 'name_raw': PullRequest.pull_request_id,
314 'name_raw': PullRequest.pull_request_id,
315 'id': PullRequest.pull_request_id,
315 'id': PullRequest.pull_request_id,
316 'title': PullRequest.title,
316 'title': PullRequest.title,
317 'updated_on_raw': PullRequest.updated_on,
317 'updated_on_raw': PullRequest.updated_on,
318 'target_repo': PullRequest.target_repo_id
318 'target_repo': PullRequest.target_repo_id
319 }
319 }
320 if order_dir == 'asc':
320 if order_dir == 'asc':
321 q = q.order_by(order_map[order_by].asc())
321 q = q.order_by(order_map[order_by].asc())
322 else:
322 else:
323 q = q.order_by(order_map[order_by].desc())
323 q = q.order_by(order_map[order_by].desc())
324
324
325 return q
325 return q
326
326
327 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
327 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
328 opened_by=None):
328 opened_by=None):
329 """
329 """
330 Count the number of pull requests for a specific repository.
330 Count the number of pull requests for a specific repository.
331
331
332 :param repo_name: target or source repo
332 :param repo_name: target or source repo
333 :param search_q: filter by text
333 :param search_q: filter by text
334 :param source: boolean flag to specify if repo_name refers to source
334 :param source: boolean flag to specify if repo_name refers to source
335 :param statuses: list of pull request statuses
335 :param statuses: list of pull request statuses
336 :param opened_by: author user of the pull request
336 :param opened_by: author user of the pull request
337 :returns: int number of pull requests
337 :returns: int number of pull requests
338 """
338 """
339 q = self._prepare_get_all_query(
339 q = self._prepare_get_all_query(
340 repo_name, search_q=search_q, source=source, statuses=statuses,
340 repo_name, search_q=search_q, source=source, statuses=statuses,
341 opened_by=opened_by)
341 opened_by=opened_by)
342
342
343 return q.count()
343 return q.count()
344
344
345 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
345 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
346 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
346 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
347 """
347 """
348 Get all pull requests for a specific repository.
348 Get all pull requests for a specific repository.
349
349
350 :param repo_name: target or source repo
350 :param repo_name: target or source repo
351 :param search_q: filter by text
351 :param search_q: filter by text
352 :param source: boolean flag to specify if repo_name refers to source
352 :param source: boolean flag to specify if repo_name refers to source
353 :param statuses: list of pull request statuses
353 :param statuses: list of pull request statuses
354 :param opened_by: author user of the pull request
354 :param opened_by: author user of the pull request
355 :param offset: pagination offset
355 :param offset: pagination offset
356 :param length: length of returned list
356 :param length: length of returned list
357 :param order_by: order of the returned list
357 :param order_by: order of the returned list
358 :param order_dir: 'asc' or 'desc' ordering direction
358 :param order_dir: 'asc' or 'desc' ordering direction
359 :returns: list of pull requests
359 :returns: list of pull requests
360 """
360 """
361 q = self._prepare_get_all_query(
361 q = self._prepare_get_all_query(
362 repo_name, search_q=search_q, source=source, statuses=statuses,
362 repo_name, search_q=search_q, source=source, statuses=statuses,
363 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
363 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
364
364
365 if length:
365 if length:
366 pull_requests = q.limit(length).offset(offset).all()
366 pull_requests = q.limit(length).offset(offset).all()
367 else:
367 else:
368 pull_requests = q.all()
368 pull_requests = q.all()
369
369
370 return pull_requests
370 return pull_requests
371
371
372 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
372 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
373 opened_by=None):
373 opened_by=None):
374 """
374 """
375 Count the number of pull requests for a specific repository that are
375 Count the number of pull requests for a specific repository that are
376 awaiting review.
376 awaiting review.
377
377
378 :param repo_name: target or source repo
378 :param repo_name: target or source repo
379 :param search_q: filter by text
379 :param search_q: filter by text
380 :param source: boolean flag to specify if repo_name refers to source
380 :param source: boolean flag to specify if repo_name refers to source
381 :param statuses: list of pull request statuses
381 :param statuses: list of pull request statuses
382 :param opened_by: author user of the pull request
382 :param opened_by: author user of the pull request
383 :returns: int number of pull requests
383 :returns: int number of pull requests
384 """
384 """
385 pull_requests = self.get_awaiting_review(
385 pull_requests = self.get_awaiting_review(
386 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
386 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
387
387
388 return len(pull_requests)
388 return len(pull_requests)
389
389
390 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
390 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
391 opened_by=None, offset=0, length=None,
391 opened_by=None, offset=0, length=None,
392 order_by=None, order_dir='desc'):
392 order_by=None, order_dir='desc'):
393 """
393 """
394 Get all pull requests for a specific repository that are awaiting
394 Get all pull requests for a specific repository that are awaiting
395 review.
395 review.
396
396
397 :param repo_name: target or source repo
397 :param repo_name: target or source repo
398 :param search_q: filter by text
398 :param search_q: filter by text
399 :param source: boolean flag to specify if repo_name refers to source
399 :param source: boolean flag to specify if repo_name refers to source
400 :param statuses: list of pull request statuses
400 :param statuses: list of pull request statuses
401 :param opened_by: author user of the pull request
401 :param opened_by: author user of the pull request
402 :param offset: pagination offset
402 :param offset: pagination offset
403 :param length: length of returned list
403 :param length: length of returned list
404 :param order_by: order of the returned list
404 :param order_by: order of the returned list
405 :param order_dir: 'asc' or 'desc' ordering direction
405 :param order_dir: 'asc' or 'desc' ordering direction
406 :returns: list of pull requests
406 :returns: list of pull requests
407 """
407 """
408 pull_requests = self.get_all(
408 pull_requests = self.get_all(
409 repo_name, search_q=search_q, source=source, statuses=statuses,
409 repo_name, search_q=search_q, source=source, statuses=statuses,
410 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
410 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
411
411
412 _filtered_pull_requests = []
412 _filtered_pull_requests = []
413 for pr in pull_requests:
413 for pr in pull_requests:
414 status = pr.calculated_review_status()
414 status = pr.calculated_review_status()
415 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
415 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
416 ChangesetStatus.STATUS_UNDER_REVIEW]:
416 ChangesetStatus.STATUS_UNDER_REVIEW]:
417 _filtered_pull_requests.append(pr)
417 _filtered_pull_requests.append(pr)
418 if length:
418 if length:
419 return _filtered_pull_requests[offset:offset+length]
419 return _filtered_pull_requests[offset:offset+length]
420 else:
420 else:
421 return _filtered_pull_requests
421 return _filtered_pull_requests
422
422
423 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
423 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
424 opened_by=None, user_id=None):
424 opened_by=None, user_id=None):
425 """
425 """
426 Count the number of pull requests for a specific repository that are
426 Count the number of pull requests for a specific repository that are
427 awaiting review from a specific user.
427 awaiting review from a specific user.
428
428
429 :param repo_name: target or source repo
429 :param repo_name: target or source repo
430 :param search_q: filter by text
430 :param search_q: filter by text
431 :param source: boolean flag to specify if repo_name refers to source
431 :param source: boolean flag to specify if repo_name refers to source
432 :param statuses: list of pull request statuses
432 :param statuses: list of pull request statuses
433 :param opened_by: author user of the pull request
433 :param opened_by: author user of the pull request
434 :param user_id: reviewer user of the pull request
434 :param user_id: reviewer user of the pull request
435 :returns: int number of pull requests
435 :returns: int number of pull requests
436 """
436 """
437 pull_requests = self.get_awaiting_my_review(
437 pull_requests = self.get_awaiting_my_review(
438 repo_name, search_q=search_q, source=source, statuses=statuses,
438 repo_name, search_q=search_q, source=source, statuses=statuses,
439 opened_by=opened_by, user_id=user_id)
439 opened_by=opened_by, user_id=user_id)
440
440
441 return len(pull_requests)
441 return len(pull_requests)
442
442
443 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
443 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
444 opened_by=None, user_id=None, offset=0,
444 opened_by=None, user_id=None, offset=0,
445 length=None, order_by=None, order_dir='desc'):
445 length=None, order_by=None, order_dir='desc'):
446 """
446 """
447 Get all pull requests for a specific repository that are awaiting
447 Get all pull requests for a specific repository that are awaiting
448 review from a specific user.
448 review from a specific user.
449
449
450 :param repo_name: target or source repo
450 :param repo_name: target or source repo
451 :param search_q: filter by text
451 :param search_q: filter by text
452 :param source: boolean flag to specify if repo_name refers to source
452 :param source: boolean flag to specify if repo_name refers to source
453 :param statuses: list of pull request statuses
453 :param statuses: list of pull request statuses
454 :param opened_by: author user of the pull request
454 :param opened_by: author user of the pull request
455 :param user_id: reviewer user of the pull request
455 :param user_id: reviewer user of the pull request
456 :param offset: pagination offset
456 :param offset: pagination offset
457 :param length: length of returned list
457 :param length: length of returned list
458 :param order_by: order of the returned list
458 :param order_by: order of the returned list
459 :param order_dir: 'asc' or 'desc' ordering direction
459 :param order_dir: 'asc' or 'desc' ordering direction
460 :returns: list of pull requests
460 :returns: list of pull requests
461 """
461 """
462 pull_requests = self.get_all(
462 pull_requests = self.get_all(
463 repo_name, search_q=search_q, source=source, statuses=statuses,
463 repo_name, search_q=search_q, source=source, statuses=statuses,
464 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
464 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
465
465
466 _my = PullRequestModel().get_not_reviewed(user_id)
466 _my = PullRequestModel().get_not_reviewed(user_id)
467 my_participation = []
467 my_participation = []
468 for pr in pull_requests:
468 for pr in pull_requests:
469 if pr in _my:
469 if pr in _my:
470 my_participation.append(pr)
470 my_participation.append(pr)
471 _filtered_pull_requests = my_participation
471 _filtered_pull_requests = my_participation
472 if length:
472 if length:
473 return _filtered_pull_requests[offset:offset+length]
473 return _filtered_pull_requests[offset:offset+length]
474 else:
474 else:
475 return _filtered_pull_requests
475 return _filtered_pull_requests
476
476
477 def get_not_reviewed(self, user_id):
477 def get_not_reviewed(self, user_id):
478 return [
478 return [
479 x.pull_request for x in PullRequestReviewers.query().filter(
479 x.pull_request for x in PullRequestReviewers.query().filter(
480 PullRequestReviewers.user_id == user_id).all()
480 PullRequestReviewers.user_id == user_id).all()
481 ]
481 ]
482
482
483 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
483 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
484 order_by=None, order_dir='desc'):
484 order_by=None, order_dir='desc'):
485 q = PullRequest.query()
485 q = PullRequest.query()
486 if user_id:
486 if user_id:
487 reviewers_subquery = Session().query(
487 reviewers_subquery = Session().query(
488 PullRequestReviewers.pull_request_id).filter(
488 PullRequestReviewers.pull_request_id).filter(
489 PullRequestReviewers.user_id == user_id).subquery()
489 PullRequestReviewers.user_id == user_id).subquery()
490 user_filter = or_(
490 user_filter = or_(
491 PullRequest.user_id == user_id,
491 PullRequest.user_id == user_id,
492 PullRequest.pull_request_id.in_(reviewers_subquery)
492 PullRequest.pull_request_id.in_(reviewers_subquery)
493 )
493 )
494 q = PullRequest.query().filter(user_filter)
494 q = PullRequest.query().filter(user_filter)
495
495
496 # closed,opened
496 # closed,opened
497 if statuses:
497 if statuses:
498 q = q.filter(PullRequest.status.in_(statuses))
498 q = q.filter(PullRequest.status.in_(statuses))
499
499
500 if query:
500 if query:
501 like_expression = u'%{}%'.format(safe_unicode(query))
501 like_expression = u'%{}%'.format(safe_unicode(query))
502 q = q.join(User)
502 q = q.join(User)
503 q = q.filter(or_(
503 q = q.filter(or_(
504 cast(PullRequest.pull_request_id, String).ilike(like_expression),
504 cast(PullRequest.pull_request_id, String).ilike(like_expression),
505 User.username.ilike(like_expression),
505 User.username.ilike(like_expression),
506 PullRequest.title.ilike(like_expression),
506 PullRequest.title.ilike(like_expression),
507 PullRequest.description.ilike(like_expression),
507 PullRequest.description.ilike(like_expression),
508 ))
508 ))
509 if order_by:
509 if order_by:
510 order_map = {
510 order_map = {
511 'name_raw': PullRequest.pull_request_id,
511 'name_raw': PullRequest.pull_request_id,
512 'title': PullRequest.title,
512 'title': PullRequest.title,
513 'updated_on_raw': PullRequest.updated_on,
513 'updated_on_raw': PullRequest.updated_on,
514 'target_repo': PullRequest.target_repo_id
514 'target_repo': PullRequest.target_repo_id
515 }
515 }
516 if order_dir == 'asc':
516 if order_dir == 'asc':
517 q = q.order_by(order_map[order_by].asc())
517 q = q.order_by(order_map[order_by].asc())
518 else:
518 else:
519 q = q.order_by(order_map[order_by].desc())
519 q = q.order_by(order_map[order_by].desc())
520
520
521 return q
521 return q
522
522
523 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
523 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
524 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
524 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
525 return q.count()
525 return q.count()
526
526
527 def get_im_participating_in(
527 def get_im_participating_in(
528 self, user_id=None, statuses=None, query='', offset=0,
528 self, user_id=None, statuses=None, query='', offset=0,
529 length=None, order_by=None, order_dir='desc'):
529 length=None, order_by=None, order_dir='desc'):
530 """
530 """
531 Get all Pull requests that i'm participating in, or i have opened
531 Get all Pull requests that i'm participating in, or i have opened
532 """
532 """
533
533
534 q = self._prepare_participating_query(
534 q = self._prepare_participating_query(
535 user_id, statuses=statuses, query=query, order_by=order_by,
535 user_id, statuses=statuses, query=query, order_by=order_by,
536 order_dir=order_dir)
536 order_dir=order_dir)
537
537
538 if length:
538 if length:
539 pull_requests = q.limit(length).offset(offset).all()
539 pull_requests = q.limit(length).offset(offset).all()
540 else:
540 else:
541 pull_requests = q.all()
541 pull_requests = q.all()
542
542
543 return pull_requests
543 return pull_requests
544
544
545 def get_versions(self, pull_request):
545 def get_versions(self, pull_request):
546 """
546 """
547 returns version of pull request sorted by ID descending
547 returns version of pull request sorted by ID descending
548 """
548 """
549 return PullRequestVersion.query()\
549 return PullRequestVersion.query()\
550 .filter(PullRequestVersion.pull_request == pull_request)\
550 .filter(PullRequestVersion.pull_request == pull_request)\
551 .order_by(PullRequestVersion.pull_request_version_id.asc())\
551 .order_by(PullRequestVersion.pull_request_version_id.asc())\
552 .all()
552 .all()
553
553
554 def get_pr_version(self, pull_request_id, version=None):
554 def get_pr_version(self, pull_request_id, version=None):
555 at_version = None
555 at_version = None
556
556
557 if version and version == 'latest':
557 if version and version == 'latest':
558 pull_request_ver = PullRequest.get(pull_request_id)
558 pull_request_ver = PullRequest.get(pull_request_id)
559 pull_request_obj = pull_request_ver
559 pull_request_obj = pull_request_ver
560 _org_pull_request_obj = pull_request_obj
560 _org_pull_request_obj = pull_request_obj
561 at_version = 'latest'
561 at_version = 'latest'
562 elif version:
562 elif version:
563 pull_request_ver = PullRequestVersion.get_or_404(version)
563 pull_request_ver = PullRequestVersion.get_or_404(version)
564 pull_request_obj = pull_request_ver
564 pull_request_obj = pull_request_ver
565 _org_pull_request_obj = pull_request_ver.pull_request
565 _org_pull_request_obj = pull_request_ver.pull_request
566 at_version = pull_request_ver.pull_request_version_id
566 at_version = pull_request_ver.pull_request_version_id
567 else:
567 else:
568 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
568 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
569 pull_request_id)
569 pull_request_id)
570
570
571 pull_request_display_obj = PullRequest.get_pr_display_object(
571 pull_request_display_obj = PullRequest.get_pr_display_object(
572 pull_request_obj, _org_pull_request_obj)
572 pull_request_obj, _org_pull_request_obj)
573
573
574 return _org_pull_request_obj, pull_request_obj, \
574 return _org_pull_request_obj, pull_request_obj, \
575 pull_request_display_obj, at_version
575 pull_request_display_obj, at_version
576
576
577 def create(self, created_by, source_repo, source_ref, target_repo,
577 def create(self, created_by, source_repo, source_ref, target_repo,
578 target_ref, revisions, reviewers, title, description=None,
578 target_ref, revisions, reviewers, title, description=None,
579 common_ancestor_id=None,
579 common_ancestor_id=None,
580 description_renderer=None,
580 description_renderer=None,
581 reviewer_data=None, translator=None, auth_user=None):
581 reviewer_data=None, translator=None, auth_user=None):
582 translator = translator or get_current_request().translate
582 translator = translator or get_current_request().translate
583
583
584 created_by_user = self._get_user(created_by)
584 created_by_user = self._get_user(created_by)
585 auth_user = auth_user or created_by_user.AuthUser()
585 auth_user = auth_user or created_by_user.AuthUser()
586 source_repo = self._get_repo(source_repo)
586 source_repo = self._get_repo(source_repo)
587 target_repo = self._get_repo(target_repo)
587 target_repo = self._get_repo(target_repo)
588
588
589 pull_request = PullRequest()
589 pull_request = PullRequest()
590 pull_request.source_repo = source_repo
590 pull_request.source_repo = source_repo
591 pull_request.source_ref = source_ref
591 pull_request.source_ref = source_ref
592 pull_request.target_repo = target_repo
592 pull_request.target_repo = target_repo
593 pull_request.target_ref = target_ref
593 pull_request.target_ref = target_ref
594 pull_request.revisions = revisions
594 pull_request.revisions = revisions
595 pull_request.title = title
595 pull_request.title = title
596 pull_request.description = description
596 pull_request.description = description
597 pull_request.description_renderer = description_renderer
597 pull_request.description_renderer = description_renderer
598 pull_request.author = created_by_user
598 pull_request.author = created_by_user
599 pull_request.reviewer_data = reviewer_data
599 pull_request.reviewer_data = reviewer_data
600 pull_request.pull_request_state = pull_request.STATE_CREATING
600 pull_request.pull_request_state = pull_request.STATE_CREATING
601 pull_request.common_ancestor_id = common_ancestor_id
601 pull_request.common_ancestor_id = common_ancestor_id
602
602
603 Session().add(pull_request)
603 Session().add(pull_request)
604 Session().flush()
604 Session().flush()
605
605
606 reviewer_ids = set()
606 reviewer_ids = set()
607 # members / reviewers
607 # members / reviewers
608 for reviewer_object in reviewers:
608 for reviewer_object in reviewers:
609 user_id, reasons, mandatory, rules = reviewer_object
609 user_id, reasons, mandatory, rules = reviewer_object
610 user = self._get_user(user_id)
610 user = self._get_user(user_id)
611
611
612 # skip duplicates
612 # skip duplicates
613 if user.user_id in reviewer_ids:
613 if user.user_id in reviewer_ids:
614 continue
614 continue
615
615
616 reviewer_ids.add(user.user_id)
616 reviewer_ids.add(user.user_id)
617
617
618 reviewer = PullRequestReviewers()
618 reviewer = PullRequestReviewers()
619 reviewer.user = user
619 reviewer.user = user
620 reviewer.pull_request = pull_request
620 reviewer.pull_request = pull_request
621 reviewer.reasons = reasons
621 reviewer.reasons = reasons
622 reviewer.mandatory = mandatory
622 reviewer.mandatory = mandatory
623
623
624 # NOTE(marcink): pick only first rule for now
624 # NOTE(marcink): pick only first rule for now
625 rule_id = list(rules)[0] if rules else None
625 rule_id = list(rules)[0] if rules else None
626 rule = RepoReviewRule.get(rule_id) if rule_id else None
626 rule = RepoReviewRule.get(rule_id) if rule_id else None
627 if rule:
627 if rule:
628 review_group = rule.user_group_vote_rule(user_id)
628 review_group = rule.user_group_vote_rule(user_id)
629 # we check if this particular reviewer is member of a voting group
629 # we check if this particular reviewer is member of a voting group
630 if review_group:
630 if review_group:
631 # NOTE(marcink):
631 # NOTE(marcink):
632 # can be that user is member of more but we pick the first same,
632 # can be that user is member of more but we pick the first same,
633 # same as default reviewers algo
633 # same as default reviewers algo
634 review_group = review_group[0]
634 review_group = review_group[0]
635
635
636 rule_data = {
636 rule_data = {
637 'rule_name':
637 'rule_name':
638 rule.review_rule_name,
638 rule.review_rule_name,
639 'rule_user_group_entry_id':
639 'rule_user_group_entry_id':
640 review_group.repo_review_rule_users_group_id,
640 review_group.repo_review_rule_users_group_id,
641 'rule_user_group_name':
641 'rule_user_group_name':
642 review_group.users_group.users_group_name,
642 review_group.users_group.users_group_name,
643 'rule_user_group_members':
643 'rule_user_group_members':
644 [x.user.username for x in review_group.users_group.members],
644 [x.user.username for x in review_group.users_group.members],
645 'rule_user_group_members_id':
645 'rule_user_group_members_id':
646 [x.user.user_id for x in review_group.users_group.members],
646 [x.user.user_id for x in review_group.users_group.members],
647 }
647 }
648 # e.g {'vote_rule': -1, 'mandatory': True}
648 # e.g {'vote_rule': -1, 'mandatory': True}
649 rule_data.update(review_group.rule_data())
649 rule_data.update(review_group.rule_data())
650
650
651 reviewer.rule_data = rule_data
651 reviewer.rule_data = rule_data
652
652
653 Session().add(reviewer)
653 Session().add(reviewer)
654 Session().flush()
654 Session().flush()
655
655
656 # Set approval status to "Under Review" for all commits which are
656 # Set approval status to "Under Review" for all commits which are
657 # part of this pull request.
657 # part of this pull request.
658 ChangesetStatusModel().set_status(
658 ChangesetStatusModel().set_status(
659 repo=target_repo,
659 repo=target_repo,
660 status=ChangesetStatus.STATUS_UNDER_REVIEW,
660 status=ChangesetStatus.STATUS_UNDER_REVIEW,
661 user=created_by_user,
661 user=created_by_user,
662 pull_request=pull_request
662 pull_request=pull_request
663 )
663 )
664 # we commit early at this point. This has to do with a fact
664 # we commit early at this point. This has to do with a fact
665 # that before queries do some row-locking. And because of that
665 # that before queries do some row-locking. And because of that
666 # we need to commit and finish transaction before below validate call
666 # we need to commit and finish transaction before below validate call
667 # that for large repos could be long resulting in long row locks
667 # that for large repos could be long resulting in long row locks
668 Session().commit()
668 Session().commit()
669
669
670 # prepare workspace, and run initial merge simulation. Set state during that
670 # prepare workspace, and run initial merge simulation. Set state during that
671 # operation
671 # operation
672 pull_request = PullRequest.get(pull_request.pull_request_id)
672 pull_request = PullRequest.get(pull_request.pull_request_id)
673
673
674 # set as merging, for merge simulation, and if finished to created so we mark
674 # set as merging, for merge simulation, and if finished to created so we mark
675 # simulation is working fine
675 # simulation is working fine
676 with pull_request.set_state(PullRequest.STATE_MERGING,
676 with pull_request.set_state(PullRequest.STATE_MERGING,
677 final_state=PullRequest.STATE_CREATED) as state_obj:
677 final_state=PullRequest.STATE_CREATED) as state_obj:
678 MergeCheck.validate(
678 MergeCheck.validate(
679 pull_request, auth_user=auth_user, translator=translator)
679 pull_request, auth_user=auth_user, translator=translator)
680
680
681 self.notify_reviewers(pull_request, reviewer_ids)
681 self.notify_reviewers(pull_request, reviewer_ids)
682 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
682 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
683
683
684 creation_data = pull_request.get_api_data(with_merge_state=False)
684 creation_data = pull_request.get_api_data(with_merge_state=False)
685 self._log_audit_action(
685 self._log_audit_action(
686 'repo.pull_request.create', {'data': creation_data},
686 'repo.pull_request.create', {'data': creation_data},
687 auth_user, pull_request)
687 auth_user, pull_request)
688
688
689 return pull_request
689 return pull_request
690
690
691 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
691 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
692 pull_request = self.__get_pull_request(pull_request)
692 pull_request = self.__get_pull_request(pull_request)
693 target_scm = pull_request.target_repo.scm_instance()
693 target_scm = pull_request.target_repo.scm_instance()
694 if action == 'create':
694 if action == 'create':
695 trigger_hook = hooks_utils.trigger_create_pull_request_hook
695 trigger_hook = hooks_utils.trigger_create_pull_request_hook
696 elif action == 'merge':
696 elif action == 'merge':
697 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
697 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
698 elif action == 'close':
698 elif action == 'close':
699 trigger_hook = hooks_utils.trigger_close_pull_request_hook
699 trigger_hook = hooks_utils.trigger_close_pull_request_hook
700 elif action == 'review_status_change':
700 elif action == 'review_status_change':
701 trigger_hook = hooks_utils.trigger_review_pull_request_hook
701 trigger_hook = hooks_utils.trigger_review_pull_request_hook
702 elif action == 'update':
702 elif action == 'update':
703 trigger_hook = hooks_utils.trigger_update_pull_request_hook
703 trigger_hook = hooks_utils.trigger_update_pull_request_hook
704 elif action == 'comment':
704 elif action == 'comment':
705 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
705 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
706 elif action == 'comment_edit':
706 elif action == 'comment_edit':
707 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
707 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
708 else:
708 else:
709 return
709 return
710
710
711 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
711 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
712 pull_request, action, trigger_hook)
712 pull_request, action, trigger_hook)
713 trigger_hook(
713 trigger_hook(
714 username=user.username,
714 username=user.username,
715 repo_name=pull_request.target_repo.repo_name,
715 repo_name=pull_request.target_repo.repo_name,
716 repo_type=target_scm.alias,
716 repo_type=target_scm.alias,
717 pull_request=pull_request,
717 pull_request=pull_request,
718 data=data)
718 data=data)
719
719
720 def _get_commit_ids(self, pull_request):
720 def _get_commit_ids(self, pull_request):
721 """
721 """
722 Return the commit ids of the merged pull request.
722 Return the commit ids of the merged pull request.
723
723
724 This method is not dealing correctly yet with the lack of autoupdates
724 This method is not dealing correctly yet with the lack of autoupdates
725 nor with the implicit target updates.
725 nor with the implicit target updates.
726 For example: if a commit in the source repo is already in the target it
726 For example: if a commit in the source repo is already in the target it
727 will be reported anyways.
727 will be reported anyways.
728 """
728 """
729 merge_rev = pull_request.merge_rev
729 merge_rev = pull_request.merge_rev
730 if merge_rev is None:
730 if merge_rev is None:
731 raise ValueError('This pull request was not merged yet')
731 raise ValueError('This pull request was not merged yet')
732
732
733 commit_ids = list(pull_request.revisions)
733 commit_ids = list(pull_request.revisions)
734 if merge_rev not in commit_ids:
734 if merge_rev not in commit_ids:
735 commit_ids.append(merge_rev)
735 commit_ids.append(merge_rev)
736
736
737 return commit_ids
737 return commit_ids
738
738
739 def merge_repo(self, pull_request, user, extras):
739 def merge_repo(self, pull_request, user, extras):
740 log.debug("Merging pull request %s", pull_request.pull_request_id)
740 log.debug("Merging pull request %s", pull_request.pull_request_id)
741 extras['user_agent'] = 'internal-merge'
741 extras['user_agent'] = 'internal-merge'
742 merge_state = self._merge_pull_request(pull_request, user, extras)
742 merge_state = self._merge_pull_request(pull_request, user, extras)
743 if merge_state.executed:
743 if merge_state.executed:
744 log.debug("Merge was successful, updating the pull request comments.")
744 log.debug("Merge was successful, updating the pull request comments.")
745 self._comment_and_close_pr(pull_request, user, merge_state)
745 self._comment_and_close_pr(pull_request, user, merge_state)
746
746
747 self._log_audit_action(
747 self._log_audit_action(
748 'repo.pull_request.merge',
748 'repo.pull_request.merge',
749 {'merge_state': merge_state.__dict__},
749 {'merge_state': merge_state.__dict__},
750 user, pull_request)
750 user, pull_request)
751
751
752 else:
752 else:
753 log.warn("Merge failed, not updating the pull request.")
753 log.warn("Merge failed, not updating the pull request.")
754 return merge_state
754 return merge_state
755
755
756 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
756 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
757 target_vcs = pull_request.target_repo.scm_instance()
757 target_vcs = pull_request.target_repo.scm_instance()
758 source_vcs = pull_request.source_repo.scm_instance()
758 source_vcs = pull_request.source_repo.scm_instance()
759
759
760 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
760 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
761 pr_id=pull_request.pull_request_id,
761 pr_id=pull_request.pull_request_id,
762 pr_title=pull_request.title,
762 pr_title=pull_request.title,
763 source_repo=source_vcs.name,
763 source_repo=source_vcs.name,
764 source_ref_name=pull_request.source_ref_parts.name,
764 source_ref_name=pull_request.source_ref_parts.name,
765 target_repo=target_vcs.name,
765 target_repo=target_vcs.name,
766 target_ref_name=pull_request.target_ref_parts.name,
766 target_ref_name=pull_request.target_ref_parts.name,
767 )
767 )
768
768
769 workspace_id = self._workspace_id(pull_request)
769 workspace_id = self._workspace_id(pull_request)
770 repo_id = pull_request.target_repo.repo_id
770 repo_id = pull_request.target_repo.repo_id
771 use_rebase = self._use_rebase_for_merging(pull_request)
771 use_rebase = self._use_rebase_for_merging(pull_request)
772 close_branch = self._close_branch_before_merging(pull_request)
772 close_branch = self._close_branch_before_merging(pull_request)
773 user_name = self._user_name_for_merging(pull_request, user)
773 user_name = self._user_name_for_merging(pull_request, user)
774
774
775 target_ref = self._refresh_reference(
775 target_ref = self._refresh_reference(
776 pull_request.target_ref_parts, target_vcs)
776 pull_request.target_ref_parts, target_vcs)
777
777
778 callback_daemon, extras = prepare_callback_daemon(
778 callback_daemon, extras = prepare_callback_daemon(
779 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
779 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
780 host=vcs_settings.HOOKS_HOST,
780 host=vcs_settings.HOOKS_HOST,
781 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
781 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
782
782
783 with callback_daemon:
783 with callback_daemon:
784 # TODO: johbo: Implement a clean way to run a config_override
784 # TODO: johbo: Implement a clean way to run a config_override
785 # for a single call.
785 # for a single call.
786 target_vcs.config.set(
786 target_vcs.config.set(
787 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
787 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
788
788
789 merge_state = target_vcs.merge(
789 merge_state = target_vcs.merge(
790 repo_id, workspace_id, target_ref, source_vcs,
790 repo_id, workspace_id, target_ref, source_vcs,
791 pull_request.source_ref_parts,
791 pull_request.source_ref_parts,
792 user_name=user_name, user_email=user.email,
792 user_name=user_name, user_email=user.email,
793 message=message, use_rebase=use_rebase,
793 message=message, use_rebase=use_rebase,
794 close_branch=close_branch)
794 close_branch=close_branch)
795 return merge_state
795 return merge_state
796
796
797 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
797 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
798 pull_request.merge_rev = merge_state.merge_ref.commit_id
798 pull_request.merge_rev = merge_state.merge_ref.commit_id
799 pull_request.updated_on = datetime.datetime.now()
799 pull_request.updated_on = datetime.datetime.now()
800 close_msg = close_msg or 'Pull request merged and closed'
800 close_msg = close_msg or 'Pull request merged and closed'
801
801
802 CommentsModel().create(
802 CommentsModel().create(
803 text=safe_unicode(close_msg),
803 text=safe_unicode(close_msg),
804 repo=pull_request.target_repo.repo_id,
804 repo=pull_request.target_repo.repo_id,
805 user=user.user_id,
805 user=user.user_id,
806 pull_request=pull_request.pull_request_id,
806 pull_request=pull_request.pull_request_id,
807 f_path=None,
807 f_path=None,
808 line_no=None,
808 line_no=None,
809 closing_pr=True
809 closing_pr=True
810 )
810 )
811
811
812 Session().add(pull_request)
812 Session().add(pull_request)
813 Session().flush()
813 Session().flush()
814 # TODO: paris: replace invalidation with less radical solution
814 # TODO: paris: replace invalidation with less radical solution
815 ScmModel().mark_for_invalidation(
815 ScmModel().mark_for_invalidation(
816 pull_request.target_repo.repo_name)
816 pull_request.target_repo.repo_name)
817 self.trigger_pull_request_hook(pull_request, user, 'merge')
817 self.trigger_pull_request_hook(pull_request, user, 'merge')
818
818
819 def has_valid_update_type(self, pull_request):
819 def has_valid_update_type(self, pull_request):
820 source_ref_type = pull_request.source_ref_parts.type
820 source_ref_type = pull_request.source_ref_parts.type
821 return source_ref_type in self.REF_TYPES
821 return source_ref_type in self.REF_TYPES
822
822
823 def get_flow_commits(self, pull_request):
823 def get_flow_commits(self, pull_request):
824
824
825 # source repo
825 # source repo
826 source_ref_name = pull_request.source_ref_parts.name
826 source_ref_name = pull_request.source_ref_parts.name
827 source_ref_type = pull_request.source_ref_parts.type
827 source_ref_type = pull_request.source_ref_parts.type
828 source_ref_id = pull_request.source_ref_parts.commit_id
828 source_ref_id = pull_request.source_ref_parts.commit_id
829 source_repo = pull_request.source_repo.scm_instance()
829 source_repo = pull_request.source_repo.scm_instance()
830
830
831 try:
831 try:
832 if source_ref_type in self.REF_TYPES:
832 if source_ref_type in self.REF_TYPES:
833 source_commit = source_repo.get_commit(source_ref_name)
833 source_commit = source_repo.get_commit(source_ref_name)
834 else:
834 else:
835 source_commit = source_repo.get_commit(source_ref_id)
835 source_commit = source_repo.get_commit(source_ref_id)
836 except CommitDoesNotExistError:
836 except CommitDoesNotExistError:
837 raise SourceRefMissing()
837 raise SourceRefMissing()
838
838
839 # target repo
839 # target repo
840 target_ref_name = pull_request.target_ref_parts.name
840 target_ref_name = pull_request.target_ref_parts.name
841 target_ref_type = pull_request.target_ref_parts.type
841 target_ref_type = pull_request.target_ref_parts.type
842 target_ref_id = pull_request.target_ref_parts.commit_id
842 target_ref_id = pull_request.target_ref_parts.commit_id
843 target_repo = pull_request.target_repo.scm_instance()
843 target_repo = pull_request.target_repo.scm_instance()
844
844
845 try:
845 try:
846 if target_ref_type in self.REF_TYPES:
846 if target_ref_type in self.REF_TYPES:
847 target_commit = target_repo.get_commit(target_ref_name)
847 target_commit = target_repo.get_commit(target_ref_name)
848 else:
848 else:
849 target_commit = target_repo.get_commit(target_ref_id)
849 target_commit = target_repo.get_commit(target_ref_id)
850 except CommitDoesNotExistError:
850 except CommitDoesNotExistError:
851 raise TargetRefMissing()
851 raise TargetRefMissing()
852
852
853 return source_commit, target_commit
853 return source_commit, target_commit
854
854
855 def update_commits(self, pull_request, updating_user):
855 def update_commits(self, pull_request, updating_user):
856 """
856 """
857 Get the updated list of commits for the pull request
857 Get the updated list of commits for the pull request
858 and return the new pull request version and the list
858 and return the new pull request version and the list
859 of commits processed by this update action
859 of commits processed by this update action
860
860
861 updating_user is the user_object who triggered the update
861 updating_user is the user_object who triggered the update
862 """
862 """
863 pull_request = self.__get_pull_request(pull_request)
863 pull_request = self.__get_pull_request(pull_request)
864 source_ref_type = pull_request.source_ref_parts.type
864 source_ref_type = pull_request.source_ref_parts.type
865 source_ref_name = pull_request.source_ref_parts.name
865 source_ref_name = pull_request.source_ref_parts.name
866 source_ref_id = pull_request.source_ref_parts.commit_id
866 source_ref_id = pull_request.source_ref_parts.commit_id
867
867
868 target_ref_type = pull_request.target_ref_parts.type
868 target_ref_type = pull_request.target_ref_parts.type
869 target_ref_name = pull_request.target_ref_parts.name
869 target_ref_name = pull_request.target_ref_parts.name
870 target_ref_id = pull_request.target_ref_parts.commit_id
870 target_ref_id = pull_request.target_ref_parts.commit_id
871
871
872 if not self.has_valid_update_type(pull_request):
872 if not self.has_valid_update_type(pull_request):
873 log.debug("Skipping update of pull request %s due to ref type: %s",
873 log.debug("Skipping update of pull request %s due to ref type: %s",
874 pull_request, source_ref_type)
874 pull_request, source_ref_type)
875 return UpdateResponse(
875 return UpdateResponse(
876 executed=False,
876 executed=False,
877 reason=UpdateFailureReason.WRONG_REF_TYPE,
877 reason=UpdateFailureReason.WRONG_REF_TYPE,
878 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
878 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
879 source_changed=False, target_changed=False)
879 source_changed=False, target_changed=False)
880
880
881 try:
881 try:
882 source_commit, target_commit = self.get_flow_commits(pull_request)
882 source_commit, target_commit = self.get_flow_commits(pull_request)
883 except SourceRefMissing:
883 except SourceRefMissing:
884 return UpdateResponse(
884 return UpdateResponse(
885 executed=False,
885 executed=False,
886 reason=UpdateFailureReason.MISSING_SOURCE_REF,
886 reason=UpdateFailureReason.MISSING_SOURCE_REF,
887 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
887 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
888 source_changed=False, target_changed=False)
888 source_changed=False, target_changed=False)
889 except TargetRefMissing:
889 except TargetRefMissing:
890 return UpdateResponse(
890 return UpdateResponse(
891 executed=False,
891 executed=False,
892 reason=UpdateFailureReason.MISSING_TARGET_REF,
892 reason=UpdateFailureReason.MISSING_TARGET_REF,
893 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
893 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
894 source_changed=False, target_changed=False)
894 source_changed=False, target_changed=False)
895
895
896 source_changed = source_ref_id != source_commit.raw_id
896 source_changed = source_ref_id != source_commit.raw_id
897 target_changed = target_ref_id != target_commit.raw_id
897 target_changed = target_ref_id != target_commit.raw_id
898
898
899 if not (source_changed or target_changed):
899 if not (source_changed or target_changed):
900 log.debug("Nothing changed in pull request %s", pull_request)
900 log.debug("Nothing changed in pull request %s", pull_request)
901 return UpdateResponse(
901 return UpdateResponse(
902 executed=False,
902 executed=False,
903 reason=UpdateFailureReason.NO_CHANGE,
903 reason=UpdateFailureReason.NO_CHANGE,
904 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
904 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
905 source_changed=target_changed, target_changed=source_changed)
905 source_changed=target_changed, target_changed=source_changed)
906
906
907 change_in_found = 'target repo' if target_changed else 'source repo'
907 change_in_found = 'target repo' if target_changed else 'source repo'
908 log.debug('Updating pull request because of change in %s detected',
908 log.debug('Updating pull request because of change in %s detected',
909 change_in_found)
909 change_in_found)
910
910
911 # Finally there is a need for an update, in case of source change
911 # Finally there is a need for an update, in case of source change
912 # we create a new version, else just an update
912 # we create a new version, else just an update
913 if source_changed:
913 if source_changed:
914 pull_request_version = self._create_version_from_snapshot(pull_request)
914 pull_request_version = self._create_version_from_snapshot(pull_request)
915 self._link_comments_to_version(pull_request_version)
915 self._link_comments_to_version(pull_request_version)
916 else:
916 else:
917 try:
917 try:
918 ver = pull_request.versions[-1]
918 ver = pull_request.versions[-1]
919 except IndexError:
919 except IndexError:
920 ver = None
920 ver = None
921
921
922 pull_request.pull_request_version_id = \
922 pull_request.pull_request_version_id = \
923 ver.pull_request_version_id if ver else None
923 ver.pull_request_version_id if ver else None
924 pull_request_version = pull_request
924 pull_request_version = pull_request
925
925
926 source_repo = pull_request.source_repo.scm_instance()
926 source_repo = pull_request.source_repo.scm_instance()
927 target_repo = pull_request.target_repo.scm_instance()
927 target_repo = pull_request.target_repo.scm_instance()
928
928
929 # re-compute commit ids
929 # re-compute commit ids
930 old_commit_ids = pull_request.revisions
930 old_commit_ids = pull_request.revisions
931 pre_load = ["author", "date", "message", "branch"]
931 pre_load = ["author", "date", "message", "branch"]
932 commit_ranges = target_repo.compare(
932 commit_ranges = target_repo.compare(
933 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
933 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
934 pre_load=pre_load)
934 pre_load=pre_load)
935
935
936 target_ref = target_commit.raw_id
936 target_ref = target_commit.raw_id
937 source_ref = source_commit.raw_id
937 source_ref = source_commit.raw_id
938 ancestor_commit_id = target_repo.get_common_ancestor(
938 ancestor_commit_id = target_repo.get_common_ancestor(
939 target_ref, source_ref, source_repo)
939 target_ref, source_ref, source_repo)
940
940
941 if not ancestor_commit_id:
941 if not ancestor_commit_id:
942 raise ValueError(
942 raise ValueError(
943 'cannot calculate diff info without a common ancestor. '
943 'cannot calculate diff info without a common ancestor. '
944 'Make sure both repositories are related, and have a common forking commit.')
944 'Make sure both repositories are related, and have a common forking commit.')
945
945
946 pull_request.common_ancestor_id = ancestor_commit_id
946 pull_request.common_ancestor_id = ancestor_commit_id
947
947
948 pull_request.source_ref = '%s:%s:%s' % (
948 pull_request.source_ref = '%s:%s:%s' % (
949 source_ref_type, source_ref_name, source_commit.raw_id)
949 source_ref_type, source_ref_name, source_commit.raw_id)
950 pull_request.target_ref = '%s:%s:%s' % (
950 pull_request.target_ref = '%s:%s:%s' % (
951 target_ref_type, target_ref_name, ancestor_commit_id)
951 target_ref_type, target_ref_name, ancestor_commit_id)
952
952
953 pull_request.revisions = [
953 pull_request.revisions = [
954 commit.raw_id for commit in reversed(commit_ranges)]
954 commit.raw_id for commit in reversed(commit_ranges)]
955 pull_request.updated_on = datetime.datetime.now()
955 pull_request.updated_on = datetime.datetime.now()
956 Session().add(pull_request)
956 Session().add(pull_request)
957 new_commit_ids = pull_request.revisions
957 new_commit_ids = pull_request.revisions
958
958
959 old_diff_data, new_diff_data = self._generate_update_diffs(
959 old_diff_data, new_diff_data = self._generate_update_diffs(
960 pull_request, pull_request_version)
960 pull_request, pull_request_version)
961
961
962 # calculate commit and file changes
962 # calculate commit and file changes
963 commit_changes = self._calculate_commit_id_changes(
963 commit_changes = self._calculate_commit_id_changes(
964 old_commit_ids, new_commit_ids)
964 old_commit_ids, new_commit_ids)
965 file_changes = self._calculate_file_changes(
965 file_changes = self._calculate_file_changes(
966 old_diff_data, new_diff_data)
966 old_diff_data, new_diff_data)
967
967
968 # set comments as outdated if DIFFS changed
968 # set comments as outdated if DIFFS changed
969 CommentsModel().outdate_comments(
969 CommentsModel().outdate_comments(
970 pull_request, old_diff_data=old_diff_data,
970 pull_request, old_diff_data=old_diff_data,
971 new_diff_data=new_diff_data)
971 new_diff_data=new_diff_data)
972
972
973 valid_commit_changes = (commit_changes.added or commit_changes.removed)
973 valid_commit_changes = (commit_changes.added or commit_changes.removed)
974 file_node_changes = (
974 file_node_changes = (
975 file_changes.added or file_changes.modified or file_changes.removed)
975 file_changes.added or file_changes.modified or file_changes.removed)
976 pr_has_changes = valid_commit_changes or file_node_changes
976 pr_has_changes = valid_commit_changes or file_node_changes
977
977
978 # Add an automatic comment to the pull request, in case
978 # Add an automatic comment to the pull request, in case
979 # anything has changed
979 # anything has changed
980 if pr_has_changes:
980 if pr_has_changes:
981 update_comment = CommentsModel().create(
981 update_comment = CommentsModel().create(
982 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
982 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
983 repo=pull_request.target_repo,
983 repo=pull_request.target_repo,
984 user=pull_request.author,
984 user=pull_request.author,
985 pull_request=pull_request,
985 pull_request=pull_request,
986 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
986 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
987
987
988 # Update status to "Under Review" for added commits
988 # Update status to "Under Review" for added commits
989 for commit_id in commit_changes.added:
989 for commit_id in commit_changes.added:
990 ChangesetStatusModel().set_status(
990 ChangesetStatusModel().set_status(
991 repo=pull_request.source_repo,
991 repo=pull_request.source_repo,
992 status=ChangesetStatus.STATUS_UNDER_REVIEW,
992 status=ChangesetStatus.STATUS_UNDER_REVIEW,
993 comment=update_comment,
993 comment=update_comment,
994 user=pull_request.author,
994 user=pull_request.author,
995 pull_request=pull_request,
995 pull_request=pull_request,
996 revision=commit_id)
996 revision=commit_id)
997
997
998 # send update email to users
998 # send update email to users
999 try:
999 try:
1000 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1000 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1001 ancestor_commit_id=ancestor_commit_id,
1001 ancestor_commit_id=ancestor_commit_id,
1002 commit_changes=commit_changes,
1002 commit_changes=commit_changes,
1003 file_changes=file_changes)
1003 file_changes=file_changes)
1004 except Exception:
1004 except Exception:
1005 log.exception('Failed to send email notification to users')
1005 log.exception('Failed to send email notification to users')
1006
1006
1007 log.debug(
1007 log.debug(
1008 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1008 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1009 'removed_ids: %s', pull_request.pull_request_id,
1009 'removed_ids: %s', pull_request.pull_request_id,
1010 commit_changes.added, commit_changes.common, commit_changes.removed)
1010 commit_changes.added, commit_changes.common, commit_changes.removed)
1011 log.debug(
1011 log.debug(
1012 'Updated pull request with the following file changes: %s',
1012 'Updated pull request with the following file changes: %s',
1013 file_changes)
1013 file_changes)
1014
1014
1015 log.info(
1015 log.info(
1016 "Updated pull request %s from commit %s to commit %s, "
1016 "Updated pull request %s from commit %s to commit %s, "
1017 "stored new version %s of this pull request.",
1017 "stored new version %s of this pull request.",
1018 pull_request.pull_request_id, source_ref_id,
1018 pull_request.pull_request_id, source_ref_id,
1019 pull_request.source_ref_parts.commit_id,
1019 pull_request.source_ref_parts.commit_id,
1020 pull_request_version.pull_request_version_id)
1020 pull_request_version.pull_request_version_id)
1021 Session().commit()
1021 Session().commit()
1022 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1022 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1023
1023
1024 return UpdateResponse(
1024 return UpdateResponse(
1025 executed=True, reason=UpdateFailureReason.NONE,
1025 executed=True, reason=UpdateFailureReason.NONE,
1026 old=pull_request, new=pull_request_version,
1026 old=pull_request, new=pull_request_version,
1027 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1027 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1028 source_changed=source_changed, target_changed=target_changed)
1028 source_changed=source_changed, target_changed=target_changed)
1029
1029
1030 def _create_version_from_snapshot(self, pull_request):
1030 def _create_version_from_snapshot(self, pull_request):
1031 version = PullRequestVersion()
1031 version = PullRequestVersion()
1032 version.title = pull_request.title
1032 version.title = pull_request.title
1033 version.description = pull_request.description
1033 version.description = pull_request.description
1034 version.status = pull_request.status
1034 version.status = pull_request.status
1035 version.pull_request_state = pull_request.pull_request_state
1035 version.pull_request_state = pull_request.pull_request_state
1036 version.created_on = datetime.datetime.now()
1036 version.created_on = datetime.datetime.now()
1037 version.updated_on = pull_request.updated_on
1037 version.updated_on = pull_request.updated_on
1038 version.user_id = pull_request.user_id
1038 version.user_id = pull_request.user_id
1039 version.source_repo = pull_request.source_repo
1039 version.source_repo = pull_request.source_repo
1040 version.source_ref = pull_request.source_ref
1040 version.source_ref = pull_request.source_ref
1041 version.target_repo = pull_request.target_repo
1041 version.target_repo = pull_request.target_repo
1042 version.target_ref = pull_request.target_ref
1042 version.target_ref = pull_request.target_ref
1043
1043
1044 version._last_merge_source_rev = pull_request._last_merge_source_rev
1044 version._last_merge_source_rev = pull_request._last_merge_source_rev
1045 version._last_merge_target_rev = pull_request._last_merge_target_rev
1045 version._last_merge_target_rev = pull_request._last_merge_target_rev
1046 version.last_merge_status = pull_request.last_merge_status
1046 version.last_merge_status = pull_request.last_merge_status
1047 version.last_merge_metadata = pull_request.last_merge_metadata
1047 version.last_merge_metadata = pull_request.last_merge_metadata
1048 version.shadow_merge_ref = pull_request.shadow_merge_ref
1048 version.shadow_merge_ref = pull_request.shadow_merge_ref
1049 version.merge_rev = pull_request.merge_rev
1049 version.merge_rev = pull_request.merge_rev
1050 version.reviewer_data = pull_request.reviewer_data
1050 version.reviewer_data = pull_request.reviewer_data
1051
1051
1052 version.revisions = pull_request.revisions
1052 version.revisions = pull_request.revisions
1053 version.common_ancestor_id = pull_request.common_ancestor_id
1053 version.common_ancestor_id = pull_request.common_ancestor_id
1054 version.pull_request = pull_request
1054 version.pull_request = pull_request
1055 Session().add(version)
1055 Session().add(version)
1056 Session().flush()
1056 Session().flush()
1057
1057
1058 return version
1058 return version
1059
1059
1060 def _generate_update_diffs(self, pull_request, pull_request_version):
1060 def _generate_update_diffs(self, pull_request, pull_request_version):
1061
1061
1062 diff_context = (
1062 diff_context = (
1063 self.DIFF_CONTEXT +
1063 self.DIFF_CONTEXT +
1064 CommentsModel.needed_extra_diff_context())
1064 CommentsModel.needed_extra_diff_context())
1065 hide_whitespace_changes = False
1065 hide_whitespace_changes = False
1066 source_repo = pull_request_version.source_repo
1066 source_repo = pull_request_version.source_repo
1067 source_ref_id = pull_request_version.source_ref_parts.commit_id
1067 source_ref_id = pull_request_version.source_ref_parts.commit_id
1068 target_ref_id = pull_request_version.target_ref_parts.commit_id
1068 target_ref_id = pull_request_version.target_ref_parts.commit_id
1069 old_diff = self._get_diff_from_pr_or_version(
1069 old_diff = self._get_diff_from_pr_or_version(
1070 source_repo, source_ref_id, target_ref_id,
1070 source_repo, source_ref_id, target_ref_id,
1071 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1071 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1072
1072
1073 source_repo = pull_request.source_repo
1073 source_repo = pull_request.source_repo
1074 source_ref_id = pull_request.source_ref_parts.commit_id
1074 source_ref_id = pull_request.source_ref_parts.commit_id
1075 target_ref_id = pull_request.target_ref_parts.commit_id
1075 target_ref_id = pull_request.target_ref_parts.commit_id
1076
1076
1077 new_diff = self._get_diff_from_pr_or_version(
1077 new_diff = self._get_diff_from_pr_or_version(
1078 source_repo, source_ref_id, target_ref_id,
1078 source_repo, source_ref_id, target_ref_id,
1079 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1079 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1080
1080
1081 old_diff_data = diffs.DiffProcessor(old_diff)
1081 old_diff_data = diffs.DiffProcessor(old_diff)
1082 old_diff_data.prepare()
1082 old_diff_data.prepare()
1083 new_diff_data = diffs.DiffProcessor(new_diff)
1083 new_diff_data = diffs.DiffProcessor(new_diff)
1084 new_diff_data.prepare()
1084 new_diff_data.prepare()
1085
1085
1086 return old_diff_data, new_diff_data
1086 return old_diff_data, new_diff_data
1087
1087
1088 def _link_comments_to_version(self, pull_request_version):
1088 def _link_comments_to_version(self, pull_request_version):
1089 """
1089 """
1090 Link all unlinked comments of this pull request to the given version.
1090 Link all unlinked comments of this pull request to the given version.
1091
1091
1092 :param pull_request_version: The `PullRequestVersion` to which
1092 :param pull_request_version: The `PullRequestVersion` to which
1093 the comments shall be linked.
1093 the comments shall be linked.
1094
1094
1095 """
1095 """
1096 pull_request = pull_request_version.pull_request
1096 pull_request = pull_request_version.pull_request
1097 comments = ChangesetComment.query()\
1097 comments = ChangesetComment.query()\
1098 .filter(
1098 .filter(
1099 # TODO: johbo: Should we query for the repo at all here?
1099 # TODO: johbo: Should we query for the repo at all here?
1100 # Pending decision on how comments of PRs are to be related
1100 # Pending decision on how comments of PRs are to be related
1101 # to either the source repo, the target repo or no repo at all.
1101 # to either the source repo, the target repo or no repo at all.
1102 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1102 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1103 ChangesetComment.pull_request == pull_request,
1103 ChangesetComment.pull_request == pull_request,
1104 ChangesetComment.pull_request_version == None)\
1104 ChangesetComment.pull_request_version == None)\
1105 .order_by(ChangesetComment.comment_id.asc())
1105 .order_by(ChangesetComment.comment_id.asc())
1106
1106
1107 # TODO: johbo: Find out why this breaks if it is done in a bulk
1107 # TODO: johbo: Find out why this breaks if it is done in a bulk
1108 # operation.
1108 # operation.
1109 for comment in comments:
1109 for comment in comments:
1110 comment.pull_request_version_id = (
1110 comment.pull_request_version_id = (
1111 pull_request_version.pull_request_version_id)
1111 pull_request_version.pull_request_version_id)
1112 Session().add(comment)
1112 Session().add(comment)
1113
1113
1114 def _calculate_commit_id_changes(self, old_ids, new_ids):
1114 def _calculate_commit_id_changes(self, old_ids, new_ids):
1115 added = [x for x in new_ids if x not in old_ids]
1115 added = [x for x in new_ids if x not in old_ids]
1116 common = [x for x in new_ids if x in old_ids]
1116 common = [x for x in new_ids if x in old_ids]
1117 removed = [x for x in old_ids if x not in new_ids]
1117 removed = [x for x in old_ids if x not in new_ids]
1118 total = new_ids
1118 total = new_ids
1119 return ChangeTuple(added, common, removed, total)
1119 return ChangeTuple(added, common, removed, total)
1120
1120
1121 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1121 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1122
1122
1123 old_files = OrderedDict()
1123 old_files = OrderedDict()
1124 for diff_data in old_diff_data.parsed_diff:
1124 for diff_data in old_diff_data.parsed_diff:
1125 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1125 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1126
1126
1127 added_files = []
1127 added_files = []
1128 modified_files = []
1128 modified_files = []
1129 removed_files = []
1129 removed_files = []
1130 for diff_data in new_diff_data.parsed_diff:
1130 for diff_data in new_diff_data.parsed_diff:
1131 new_filename = diff_data['filename']
1131 new_filename = diff_data['filename']
1132 new_hash = md5_safe(diff_data['raw_diff'])
1132 new_hash = md5_safe(diff_data['raw_diff'])
1133
1133
1134 old_hash = old_files.get(new_filename)
1134 old_hash = old_files.get(new_filename)
1135 if not old_hash:
1135 if not old_hash:
1136 # file is not present in old diff, we have to figure out from parsed diff
1136 # file is not present in old diff, we have to figure out from parsed diff
1137 # operation ADD/REMOVE
1137 # operation ADD/REMOVE
1138 operations_dict = diff_data['stats']['ops']
1138 operations_dict = diff_data['stats']['ops']
1139 if diffs.DEL_FILENODE in operations_dict:
1139 if diffs.DEL_FILENODE in operations_dict:
1140 removed_files.append(new_filename)
1140 removed_files.append(new_filename)
1141 else:
1141 else:
1142 added_files.append(new_filename)
1142 added_files.append(new_filename)
1143 else:
1143 else:
1144 if new_hash != old_hash:
1144 if new_hash != old_hash:
1145 modified_files.append(new_filename)
1145 modified_files.append(new_filename)
1146 # now remove a file from old, since we have seen it already
1146 # now remove a file from old, since we have seen it already
1147 del old_files[new_filename]
1147 del old_files[new_filename]
1148
1148
1149 # removed files is when there are present in old, but not in NEW,
1149 # removed files is when there are present in old, but not in NEW,
1150 # since we remove old files that are present in new diff, left-overs
1150 # since we remove old files that are present in new diff, left-overs
1151 # if any should be the removed files
1151 # if any should be the removed files
1152 removed_files.extend(old_files.keys())
1152 removed_files.extend(old_files.keys())
1153
1153
1154 return FileChangeTuple(added_files, modified_files, removed_files)
1154 return FileChangeTuple(added_files, modified_files, removed_files)
1155
1155
1156 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1156 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1157 """
1157 """
1158 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1158 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1159 so it's always looking the same disregarding on which default
1159 so it's always looking the same disregarding on which default
1160 renderer system is using.
1160 renderer system is using.
1161
1161
1162 :param ancestor_commit_id: ancestor raw_id
1162 :param ancestor_commit_id: ancestor raw_id
1163 :param changes: changes named tuple
1163 :param changes: changes named tuple
1164 :param file_changes: file changes named tuple
1164 :param file_changes: file changes named tuple
1165
1165
1166 """
1166 """
1167 new_status = ChangesetStatus.get_status_lbl(
1167 new_status = ChangesetStatus.get_status_lbl(
1168 ChangesetStatus.STATUS_UNDER_REVIEW)
1168 ChangesetStatus.STATUS_UNDER_REVIEW)
1169
1169
1170 changed_files = (
1170 changed_files = (
1171 file_changes.added + file_changes.modified + file_changes.removed)
1171 file_changes.added + file_changes.modified + file_changes.removed)
1172
1172
1173 params = {
1173 params = {
1174 'under_review_label': new_status,
1174 'under_review_label': new_status,
1175 'added_commits': changes.added,
1175 'added_commits': changes.added,
1176 'removed_commits': changes.removed,
1176 'removed_commits': changes.removed,
1177 'changed_files': changed_files,
1177 'changed_files': changed_files,
1178 'added_files': file_changes.added,
1178 'added_files': file_changes.added,
1179 'modified_files': file_changes.modified,
1179 'modified_files': file_changes.modified,
1180 'removed_files': file_changes.removed,
1180 'removed_files': file_changes.removed,
1181 'ancestor_commit_id': ancestor_commit_id
1181 'ancestor_commit_id': ancestor_commit_id
1182 }
1182 }
1183 renderer = RstTemplateRenderer()
1183 renderer = RstTemplateRenderer()
1184 return renderer.render('pull_request_update.mako', **params)
1184 return renderer.render('pull_request_update.mako', **params)
1185
1185
1186 def edit(self, pull_request, title, description, description_renderer, user):
1186 def edit(self, pull_request, title, description, description_renderer, user):
1187 pull_request = self.__get_pull_request(pull_request)
1187 pull_request = self.__get_pull_request(pull_request)
1188 old_data = pull_request.get_api_data(with_merge_state=False)
1188 old_data = pull_request.get_api_data(with_merge_state=False)
1189 if pull_request.is_closed():
1189 if pull_request.is_closed():
1190 raise ValueError('This pull request is closed')
1190 raise ValueError('This pull request is closed')
1191 if title:
1191 if title:
1192 pull_request.title = title
1192 pull_request.title = title
1193 pull_request.description = description
1193 pull_request.description = description
1194 pull_request.updated_on = datetime.datetime.now()
1194 pull_request.updated_on = datetime.datetime.now()
1195 pull_request.description_renderer = description_renderer
1195 pull_request.description_renderer = description_renderer
1196 Session().add(pull_request)
1196 Session().add(pull_request)
1197 self._log_audit_action(
1197 self._log_audit_action(
1198 'repo.pull_request.edit', {'old_data': old_data},
1198 'repo.pull_request.edit', {'old_data': old_data},
1199 user, pull_request)
1199 user, pull_request)
1200
1200
1201 def update_reviewers(self, pull_request, reviewer_data, user):
1201 def update_reviewers(self, pull_request, reviewer_data, user):
1202 """
1202 """
1203 Update the reviewers in the pull request
1203 Update the reviewers in the pull request
1204
1204
1205 :param pull_request: the pr to update
1205 :param pull_request: the pr to update
1206 :param reviewer_data: list of tuples
1206 :param reviewer_data: list of tuples
1207 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1207 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1208 """
1208 """
1209 pull_request = self.__get_pull_request(pull_request)
1209 pull_request = self.__get_pull_request(pull_request)
1210 if pull_request.is_closed():
1210 if pull_request.is_closed():
1211 raise ValueError('This pull request is closed')
1211 raise ValueError('This pull request is closed')
1212
1212
1213 reviewers = {}
1213 reviewers = {}
1214 for user_id, reasons, mandatory, rules in reviewer_data:
1214 for user_id, reasons, mandatory, rules in reviewer_data:
1215 if isinstance(user_id, (int, compat.string_types)):
1215 if isinstance(user_id, (int, compat.string_types)):
1216 user_id = self._get_user(user_id).user_id
1216 user_id = self._get_user(user_id).user_id
1217 reviewers[user_id] = {
1217 reviewers[user_id] = {
1218 'reasons': reasons, 'mandatory': mandatory}
1218 'reasons': reasons, 'mandatory': mandatory}
1219
1219
1220 reviewers_ids = set(reviewers.keys())
1220 reviewers_ids = set(reviewers.keys())
1221 current_reviewers = PullRequestReviewers.query()\
1221 current_reviewers = PullRequestReviewers.query()\
1222 .filter(PullRequestReviewers.pull_request ==
1222 .filter(PullRequestReviewers.pull_request ==
1223 pull_request).all()
1223 pull_request).all()
1224 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1224 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1225
1225
1226 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1226 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1227 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1227 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1228
1228
1229 log.debug("Adding %s reviewers", ids_to_add)
1229 log.debug("Adding %s reviewers", ids_to_add)
1230 log.debug("Removing %s reviewers", ids_to_remove)
1230 log.debug("Removing %s reviewers", ids_to_remove)
1231 changed = False
1231 changed = False
1232 added_audit_reviewers = []
1232 added_audit_reviewers = []
1233 removed_audit_reviewers = []
1233 removed_audit_reviewers = []
1234
1234
1235 for uid in ids_to_add:
1235 for uid in ids_to_add:
1236 changed = True
1236 changed = True
1237 _usr = self._get_user(uid)
1237 _usr = self._get_user(uid)
1238 reviewer = PullRequestReviewers()
1238 reviewer = PullRequestReviewers()
1239 reviewer.user = _usr
1239 reviewer.user = _usr
1240 reviewer.pull_request = pull_request
1240 reviewer.pull_request = pull_request
1241 reviewer.reasons = reviewers[uid]['reasons']
1241 reviewer.reasons = reviewers[uid]['reasons']
1242 # NOTE(marcink): mandatory shouldn't be changed now
1242 # NOTE(marcink): mandatory shouldn't be changed now
1243 # reviewer.mandatory = reviewers[uid]['reasons']
1243 # reviewer.mandatory = reviewers[uid]['reasons']
1244 Session().add(reviewer)
1244 Session().add(reviewer)
1245 added_audit_reviewers.append(reviewer.get_dict())
1245 added_audit_reviewers.append(reviewer.get_dict())
1246
1246
1247 for uid in ids_to_remove:
1247 for uid in ids_to_remove:
1248 changed = True
1248 changed = True
1249 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1249 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1250 # that prevents and fixes cases that we added the same reviewer twice.
1250 # that prevents and fixes cases that we added the same reviewer twice.
1251 # this CAN happen due to the lack of DB checks
1251 # this CAN happen due to the lack of DB checks
1252 reviewers = PullRequestReviewers.query()\
1252 reviewers = PullRequestReviewers.query()\
1253 .filter(PullRequestReviewers.user_id == uid,
1253 .filter(PullRequestReviewers.user_id == uid,
1254 PullRequestReviewers.pull_request == pull_request)\
1254 PullRequestReviewers.pull_request == pull_request)\
1255 .all()
1255 .all()
1256
1256
1257 for obj in reviewers:
1257 for obj in reviewers:
1258 added_audit_reviewers.append(obj.get_dict())
1258 added_audit_reviewers.append(obj.get_dict())
1259 Session().delete(obj)
1259 Session().delete(obj)
1260
1260
1261 if changed:
1261 if changed:
1262 Session().expire_all()
1262 Session().expire_all()
1263 pull_request.updated_on = datetime.datetime.now()
1263 pull_request.updated_on = datetime.datetime.now()
1264 Session().add(pull_request)
1264 Session().add(pull_request)
1265
1265
1266 # finally store audit logs
1266 # finally store audit logs
1267 for user_data in added_audit_reviewers:
1267 for user_data in added_audit_reviewers:
1268 self._log_audit_action(
1268 self._log_audit_action(
1269 'repo.pull_request.reviewer.add', {'data': user_data},
1269 'repo.pull_request.reviewer.add', {'data': user_data},
1270 user, pull_request)
1270 user, pull_request)
1271 for user_data in removed_audit_reviewers:
1271 for user_data in removed_audit_reviewers:
1272 self._log_audit_action(
1272 self._log_audit_action(
1273 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1273 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1274 user, pull_request)
1274 user, pull_request)
1275
1275
1276 self.notify_reviewers(pull_request, ids_to_add)
1276 self.notify_reviewers(pull_request, ids_to_add)
1277 return ids_to_add, ids_to_remove
1277 return ids_to_add, ids_to_remove
1278
1278
1279 def get_url(self, pull_request, request=None, permalink=False):
1279 def get_url(self, pull_request, request=None, permalink=False):
1280 if not request:
1280 if not request:
1281 request = get_current_request()
1281 request = get_current_request()
1282
1282
1283 if permalink:
1283 if permalink:
1284 return request.route_url(
1284 return request.route_url(
1285 'pull_requests_global',
1285 'pull_requests_global',
1286 pull_request_id=pull_request.pull_request_id,)
1286 pull_request_id=pull_request.pull_request_id,)
1287 else:
1287 else:
1288 return request.route_url('pullrequest_show',
1288 return request.route_url('pullrequest_show',
1289 repo_name=safe_str(pull_request.target_repo.repo_name),
1289 repo_name=safe_str(pull_request.target_repo.repo_name),
1290 pull_request_id=pull_request.pull_request_id,)
1290 pull_request_id=pull_request.pull_request_id,)
1291
1291
1292 def get_shadow_clone_url(self, pull_request, request=None):
1292 def get_shadow_clone_url(self, pull_request, request=None):
1293 """
1293 """
1294 Returns qualified url pointing to the shadow repository. If this pull
1294 Returns qualified url pointing to the shadow repository. If this pull
1295 request is closed there is no shadow repository and ``None`` will be
1295 request is closed there is no shadow repository and ``None`` will be
1296 returned.
1296 returned.
1297 """
1297 """
1298 if pull_request.is_closed():
1298 if pull_request.is_closed():
1299 return None
1299 return None
1300 else:
1300 else:
1301 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1301 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1302 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1302 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1303
1303
1304 def notify_reviewers(self, pull_request, reviewers_ids):
1304 def notify_reviewers(self, pull_request, reviewers_ids):
1305 # notification to reviewers
1305 # notification to reviewers
1306 if not reviewers_ids:
1306 if not reviewers_ids:
1307 return
1307 return
1308
1308
1309 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1309 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1310
1310
1311 pull_request_obj = pull_request
1311 pull_request_obj = pull_request
1312 # get the current participants of this pull request
1312 # get the current participants of this pull request
1313 recipients = reviewers_ids
1313 recipients = reviewers_ids
1314 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1314 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1315
1315
1316 pr_source_repo = pull_request_obj.source_repo
1316 pr_source_repo = pull_request_obj.source_repo
1317 pr_target_repo = pull_request_obj.target_repo
1317 pr_target_repo = pull_request_obj.target_repo
1318
1318
1319 pr_url = h.route_url('pullrequest_show',
1319 pr_url = h.route_url('pullrequest_show',
1320 repo_name=pr_target_repo.repo_name,
1320 repo_name=pr_target_repo.repo_name,
1321 pull_request_id=pull_request_obj.pull_request_id,)
1321 pull_request_id=pull_request_obj.pull_request_id,)
1322
1322
1323 # set some variables for email notification
1323 # set some variables for email notification
1324 pr_target_repo_url = h.route_url(
1324 pr_target_repo_url = h.route_url(
1325 'repo_summary', repo_name=pr_target_repo.repo_name)
1325 'repo_summary', repo_name=pr_target_repo.repo_name)
1326
1326
1327 pr_source_repo_url = h.route_url(
1327 pr_source_repo_url = h.route_url(
1328 'repo_summary', repo_name=pr_source_repo.repo_name)
1328 'repo_summary', repo_name=pr_source_repo.repo_name)
1329
1329
1330 # pull request specifics
1330 # pull request specifics
1331 pull_request_commits = [
1331 pull_request_commits = [
1332 (x.raw_id, x.message)
1332 (x.raw_id, x.message)
1333 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1333 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1334
1334
1335 kwargs = {
1335 kwargs = {
1336 'user': pull_request.author,
1336 'user': pull_request.author,
1337 'pull_request': pull_request_obj,
1337 'pull_request': pull_request_obj,
1338 'pull_request_commits': pull_request_commits,
1338 'pull_request_commits': pull_request_commits,
1339
1339
1340 'pull_request_target_repo': pr_target_repo,
1340 'pull_request_target_repo': pr_target_repo,
1341 'pull_request_target_repo_url': pr_target_repo_url,
1341 'pull_request_target_repo_url': pr_target_repo_url,
1342
1342
1343 'pull_request_source_repo': pr_source_repo,
1343 'pull_request_source_repo': pr_source_repo,
1344 'pull_request_source_repo_url': pr_source_repo_url,
1344 'pull_request_source_repo_url': pr_source_repo_url,
1345
1345
1346 'pull_request_url': pr_url,
1346 'pull_request_url': pr_url,
1347 'thread_ids': [pr_url],
1347 }
1348 }
1348
1349
1349 # pre-generate the subject for notification itself
1350 # pre-generate the subject for notification itself
1350 (subject,
1351 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1351 _h, _e, # we don't care about those
1352 body_plaintext) = EmailNotificationModel().render_email(
1353 notification_type, **kwargs)
1352 notification_type, **kwargs)
1354
1353
1355 # create notification objects, and emails
1354 # create notification objects, and emails
1356 NotificationModel().create(
1355 NotificationModel().create(
1357 created_by=pull_request.author,
1356 created_by=pull_request.author,
1358 notification_subject=subject,
1357 notification_subject=subject,
1359 notification_body=body_plaintext,
1358 notification_body=body_plaintext,
1360 notification_type=notification_type,
1359 notification_type=notification_type,
1361 recipients=recipients,
1360 recipients=recipients,
1362 email_kwargs=kwargs,
1361 email_kwargs=kwargs,
1363 )
1362 )
1364
1363
1365 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1364 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1366 commit_changes, file_changes):
1365 commit_changes, file_changes):
1367
1366
1368 updating_user_id = updating_user.user_id
1367 updating_user_id = updating_user.user_id
1369 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1368 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1370 # NOTE(marcink): send notification to all other users except to
1369 # NOTE(marcink): send notification to all other users except to
1371 # person who updated the PR
1370 # person who updated the PR
1372 recipients = reviewers.difference(set([updating_user_id]))
1371 recipients = reviewers.difference(set([updating_user_id]))
1373
1372
1374 log.debug('Notify following recipients about pull-request update %s', recipients)
1373 log.debug('Notify following recipients about pull-request update %s', recipients)
1375
1374
1376 pull_request_obj = pull_request
1375 pull_request_obj = pull_request
1377
1376
1378 # send email about the update
1377 # send email about the update
1379 changed_files = (
1378 changed_files = (
1380 file_changes.added + file_changes.modified + file_changes.removed)
1379 file_changes.added + file_changes.modified + file_changes.removed)
1381
1380
1382 pr_source_repo = pull_request_obj.source_repo
1381 pr_source_repo = pull_request_obj.source_repo
1383 pr_target_repo = pull_request_obj.target_repo
1382 pr_target_repo = pull_request_obj.target_repo
1384
1383
1385 pr_url = h.route_url('pullrequest_show',
1384 pr_url = h.route_url('pullrequest_show',
1386 repo_name=pr_target_repo.repo_name,
1385 repo_name=pr_target_repo.repo_name,
1387 pull_request_id=pull_request_obj.pull_request_id,)
1386 pull_request_id=pull_request_obj.pull_request_id,)
1388
1387
1389 # set some variables for email notification
1388 # set some variables for email notification
1390 pr_target_repo_url = h.route_url(
1389 pr_target_repo_url = h.route_url(
1391 'repo_summary', repo_name=pr_target_repo.repo_name)
1390 'repo_summary', repo_name=pr_target_repo.repo_name)
1392
1391
1393 pr_source_repo_url = h.route_url(
1392 pr_source_repo_url = h.route_url(
1394 'repo_summary', repo_name=pr_source_repo.repo_name)
1393 'repo_summary', repo_name=pr_source_repo.repo_name)
1395
1394
1396 email_kwargs = {
1395 email_kwargs = {
1397 'date': datetime.datetime.now(),
1396 'date': datetime.datetime.now(),
1398 'updating_user': updating_user,
1397 'updating_user': updating_user,
1399
1398
1400 'pull_request': pull_request_obj,
1399 'pull_request': pull_request_obj,
1401
1400
1402 'pull_request_target_repo': pr_target_repo,
1401 'pull_request_target_repo': pr_target_repo,
1403 'pull_request_target_repo_url': pr_target_repo_url,
1402 'pull_request_target_repo_url': pr_target_repo_url,
1404
1403
1405 'pull_request_source_repo': pr_source_repo,
1404 'pull_request_source_repo': pr_source_repo,
1406 'pull_request_source_repo_url': pr_source_repo_url,
1405 'pull_request_source_repo_url': pr_source_repo_url,
1407
1406
1408 'pull_request_url': pr_url,
1407 'pull_request_url': pr_url,
1409
1408
1410 'ancestor_commit_id': ancestor_commit_id,
1409 'ancestor_commit_id': ancestor_commit_id,
1411 'added_commits': commit_changes.added,
1410 'added_commits': commit_changes.added,
1412 'removed_commits': commit_changes.removed,
1411 'removed_commits': commit_changes.removed,
1413 'changed_files': changed_files,
1412 'changed_files': changed_files,
1414 'added_files': file_changes.added,
1413 'added_files': file_changes.added,
1415 'modified_files': file_changes.modified,
1414 'modified_files': file_changes.modified,
1416 'removed_files': file_changes.removed,
1415 'removed_files': file_changes.removed,
1416 'thread_ids': [pr_url],
1417 }
1417 }
1418
1418
1419 (subject,
1419 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1420 _h, _e, # we don't care about those
1421 body_plaintext) = EmailNotificationModel().render_email(
1422 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1420 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1423
1421
1424 # create notification objects, and emails
1422 # create notification objects, and emails
1425 NotificationModel().create(
1423 NotificationModel().create(
1426 created_by=updating_user,
1424 created_by=updating_user,
1427 notification_subject=subject,
1425 notification_subject=subject,
1428 notification_body=body_plaintext,
1426 notification_body=body_plaintext,
1429 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1427 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1430 recipients=recipients,
1428 recipients=recipients,
1431 email_kwargs=email_kwargs,
1429 email_kwargs=email_kwargs,
1432 )
1430 )
1433
1431
1434 def delete(self, pull_request, user=None):
1432 def delete(self, pull_request, user=None):
1435 if not user:
1433 if not user:
1436 user = getattr(get_current_rhodecode_user(), 'username', None)
1434 user = getattr(get_current_rhodecode_user(), 'username', None)
1437
1435
1438 pull_request = self.__get_pull_request(pull_request)
1436 pull_request = self.__get_pull_request(pull_request)
1439 old_data = pull_request.get_api_data(with_merge_state=False)
1437 old_data = pull_request.get_api_data(with_merge_state=False)
1440 self._cleanup_merge_workspace(pull_request)
1438 self._cleanup_merge_workspace(pull_request)
1441 self._log_audit_action(
1439 self._log_audit_action(
1442 'repo.pull_request.delete', {'old_data': old_data},
1440 'repo.pull_request.delete', {'old_data': old_data},
1443 user, pull_request)
1441 user, pull_request)
1444 Session().delete(pull_request)
1442 Session().delete(pull_request)
1445
1443
1446 def close_pull_request(self, pull_request, user):
1444 def close_pull_request(self, pull_request, user):
1447 pull_request = self.__get_pull_request(pull_request)
1445 pull_request = self.__get_pull_request(pull_request)
1448 self._cleanup_merge_workspace(pull_request)
1446 self._cleanup_merge_workspace(pull_request)
1449 pull_request.status = PullRequest.STATUS_CLOSED
1447 pull_request.status = PullRequest.STATUS_CLOSED
1450 pull_request.updated_on = datetime.datetime.now()
1448 pull_request.updated_on = datetime.datetime.now()
1451 Session().add(pull_request)
1449 Session().add(pull_request)
1452 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1450 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1453
1451
1454 pr_data = pull_request.get_api_data(with_merge_state=False)
1452 pr_data = pull_request.get_api_data(with_merge_state=False)
1455 self._log_audit_action(
1453 self._log_audit_action(
1456 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1454 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1457
1455
1458 def close_pull_request_with_comment(
1456 def close_pull_request_with_comment(
1459 self, pull_request, user, repo, message=None, auth_user=None):
1457 self, pull_request, user, repo, message=None, auth_user=None):
1460
1458
1461 pull_request_review_status = pull_request.calculated_review_status()
1459 pull_request_review_status = pull_request.calculated_review_status()
1462
1460
1463 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1461 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1464 # approved only if we have voting consent
1462 # approved only if we have voting consent
1465 status = ChangesetStatus.STATUS_APPROVED
1463 status = ChangesetStatus.STATUS_APPROVED
1466 else:
1464 else:
1467 status = ChangesetStatus.STATUS_REJECTED
1465 status = ChangesetStatus.STATUS_REJECTED
1468 status_lbl = ChangesetStatus.get_status_lbl(status)
1466 status_lbl = ChangesetStatus.get_status_lbl(status)
1469
1467
1470 default_message = (
1468 default_message = (
1471 'Closing with status change {transition_icon} {status}.'
1469 'Closing with status change {transition_icon} {status}.'
1472 ).format(transition_icon='>', status=status_lbl)
1470 ).format(transition_icon='>', status=status_lbl)
1473 text = message or default_message
1471 text = message or default_message
1474
1472
1475 # create a comment, and link it to new status
1473 # create a comment, and link it to new status
1476 comment = CommentsModel().create(
1474 comment = CommentsModel().create(
1477 text=text,
1475 text=text,
1478 repo=repo.repo_id,
1476 repo=repo.repo_id,
1479 user=user.user_id,
1477 user=user.user_id,
1480 pull_request=pull_request.pull_request_id,
1478 pull_request=pull_request.pull_request_id,
1481 status_change=status_lbl,
1479 status_change=status_lbl,
1482 status_change_type=status,
1480 status_change_type=status,
1483 closing_pr=True,
1481 closing_pr=True,
1484 auth_user=auth_user,
1482 auth_user=auth_user,
1485 )
1483 )
1486
1484
1487 # calculate old status before we change it
1485 # calculate old status before we change it
1488 old_calculated_status = pull_request.calculated_review_status()
1486 old_calculated_status = pull_request.calculated_review_status()
1489 ChangesetStatusModel().set_status(
1487 ChangesetStatusModel().set_status(
1490 repo.repo_id,
1488 repo.repo_id,
1491 status,
1489 status,
1492 user.user_id,
1490 user.user_id,
1493 comment=comment,
1491 comment=comment,
1494 pull_request=pull_request.pull_request_id
1492 pull_request=pull_request.pull_request_id
1495 )
1493 )
1496
1494
1497 Session().flush()
1495 Session().flush()
1498
1496
1499 self.trigger_pull_request_hook(pull_request, user, 'comment',
1497 self.trigger_pull_request_hook(pull_request, user, 'comment',
1500 data={'comment': comment})
1498 data={'comment': comment})
1501
1499
1502 # we now calculate the status of pull request again, and based on that
1500 # we now calculate the status of pull request again, and based on that
1503 # calculation trigger status change. This might happen in cases
1501 # calculation trigger status change. This might happen in cases
1504 # that non-reviewer admin closes a pr, which means his vote doesn't
1502 # that non-reviewer admin closes a pr, which means his vote doesn't
1505 # change the status, while if he's a reviewer this might change it.
1503 # change the status, while if he's a reviewer this might change it.
1506 calculated_status = pull_request.calculated_review_status()
1504 calculated_status = pull_request.calculated_review_status()
1507 if old_calculated_status != calculated_status:
1505 if old_calculated_status != calculated_status:
1508 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1506 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1509 data={'status': calculated_status})
1507 data={'status': calculated_status})
1510
1508
1511 # finally close the PR
1509 # finally close the PR
1512 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1510 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1513
1511
1514 return comment, status
1512 return comment, status
1515
1513
1516 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1514 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1517 _ = translator or get_current_request().translate
1515 _ = translator or get_current_request().translate
1518
1516
1519 if not self._is_merge_enabled(pull_request):
1517 if not self._is_merge_enabled(pull_request):
1520 return None, False, _('Server-side pull request merging is disabled.')
1518 return None, False, _('Server-side pull request merging is disabled.')
1521
1519
1522 if pull_request.is_closed():
1520 if pull_request.is_closed():
1523 return None, False, _('This pull request is closed.')
1521 return None, False, _('This pull request is closed.')
1524
1522
1525 merge_possible, msg = self._check_repo_requirements(
1523 merge_possible, msg = self._check_repo_requirements(
1526 target=pull_request.target_repo, source=pull_request.source_repo,
1524 target=pull_request.target_repo, source=pull_request.source_repo,
1527 translator=_)
1525 translator=_)
1528 if not merge_possible:
1526 if not merge_possible:
1529 return None, merge_possible, msg
1527 return None, merge_possible, msg
1530
1528
1531 try:
1529 try:
1532 merge_response = self._try_merge(
1530 merge_response = self._try_merge(
1533 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1531 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1534 log.debug("Merge response: %s", merge_response)
1532 log.debug("Merge response: %s", merge_response)
1535 return merge_response, merge_response.possible, merge_response.merge_status_message
1533 return merge_response, merge_response.possible, merge_response.merge_status_message
1536 except NotImplementedError:
1534 except NotImplementedError:
1537 return None, False, _('Pull request merging is not supported.')
1535 return None, False, _('Pull request merging is not supported.')
1538
1536
1539 def _check_repo_requirements(self, target, source, translator):
1537 def _check_repo_requirements(self, target, source, translator):
1540 """
1538 """
1541 Check if `target` and `source` have compatible requirements.
1539 Check if `target` and `source` have compatible requirements.
1542
1540
1543 Currently this is just checking for largefiles.
1541 Currently this is just checking for largefiles.
1544 """
1542 """
1545 _ = translator
1543 _ = translator
1546 target_has_largefiles = self._has_largefiles(target)
1544 target_has_largefiles = self._has_largefiles(target)
1547 source_has_largefiles = self._has_largefiles(source)
1545 source_has_largefiles = self._has_largefiles(source)
1548 merge_possible = True
1546 merge_possible = True
1549 message = u''
1547 message = u''
1550
1548
1551 if target_has_largefiles != source_has_largefiles:
1549 if target_has_largefiles != source_has_largefiles:
1552 merge_possible = False
1550 merge_possible = False
1553 if source_has_largefiles:
1551 if source_has_largefiles:
1554 message = _(
1552 message = _(
1555 'Target repository large files support is disabled.')
1553 'Target repository large files support is disabled.')
1556 else:
1554 else:
1557 message = _(
1555 message = _(
1558 'Source repository large files support is disabled.')
1556 'Source repository large files support is disabled.')
1559
1557
1560 return merge_possible, message
1558 return merge_possible, message
1561
1559
1562 def _has_largefiles(self, repo):
1560 def _has_largefiles(self, repo):
1563 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1561 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1564 'extensions', 'largefiles')
1562 'extensions', 'largefiles')
1565 return largefiles_ui and largefiles_ui[0].active
1563 return largefiles_ui and largefiles_ui[0].active
1566
1564
1567 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1565 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1568 """
1566 """
1569 Try to merge the pull request and return the merge status.
1567 Try to merge the pull request and return the merge status.
1570 """
1568 """
1571 log.debug(
1569 log.debug(
1572 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1570 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1573 pull_request.pull_request_id, force_shadow_repo_refresh)
1571 pull_request.pull_request_id, force_shadow_repo_refresh)
1574 target_vcs = pull_request.target_repo.scm_instance()
1572 target_vcs = pull_request.target_repo.scm_instance()
1575 # Refresh the target reference.
1573 # Refresh the target reference.
1576 try:
1574 try:
1577 target_ref = self._refresh_reference(
1575 target_ref = self._refresh_reference(
1578 pull_request.target_ref_parts, target_vcs)
1576 pull_request.target_ref_parts, target_vcs)
1579 except CommitDoesNotExistError:
1577 except CommitDoesNotExistError:
1580 merge_state = MergeResponse(
1578 merge_state = MergeResponse(
1581 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1579 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1582 metadata={'target_ref': pull_request.target_ref_parts})
1580 metadata={'target_ref': pull_request.target_ref_parts})
1583 return merge_state
1581 return merge_state
1584
1582
1585 target_locked = pull_request.target_repo.locked
1583 target_locked = pull_request.target_repo.locked
1586 if target_locked and target_locked[0]:
1584 if target_locked and target_locked[0]:
1587 locked_by = 'user:{}'.format(target_locked[0])
1585 locked_by = 'user:{}'.format(target_locked[0])
1588 log.debug("The target repository is locked by %s.", locked_by)
1586 log.debug("The target repository is locked by %s.", locked_by)
1589 merge_state = MergeResponse(
1587 merge_state = MergeResponse(
1590 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1588 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1591 metadata={'locked_by': locked_by})
1589 metadata={'locked_by': locked_by})
1592 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1590 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1593 pull_request, target_ref):
1591 pull_request, target_ref):
1594 log.debug("Refreshing the merge status of the repository.")
1592 log.debug("Refreshing the merge status of the repository.")
1595 merge_state = self._refresh_merge_state(
1593 merge_state = self._refresh_merge_state(
1596 pull_request, target_vcs, target_ref)
1594 pull_request, target_vcs, target_ref)
1597 else:
1595 else:
1598 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1596 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1599 metadata = {
1597 metadata = {
1600 'unresolved_files': '',
1598 'unresolved_files': '',
1601 'target_ref': pull_request.target_ref_parts,
1599 'target_ref': pull_request.target_ref_parts,
1602 'source_ref': pull_request.source_ref_parts,
1600 'source_ref': pull_request.source_ref_parts,
1603 }
1601 }
1604 if pull_request.last_merge_metadata:
1602 if pull_request.last_merge_metadata:
1605 metadata.update(pull_request.last_merge_metadata)
1603 metadata.update(pull_request.last_merge_metadata)
1606
1604
1607 if not possible and target_ref.type == 'branch':
1605 if not possible and target_ref.type == 'branch':
1608 # NOTE(marcink): case for mercurial multiple heads on branch
1606 # NOTE(marcink): case for mercurial multiple heads on branch
1609 heads = target_vcs._heads(target_ref.name)
1607 heads = target_vcs._heads(target_ref.name)
1610 if len(heads) != 1:
1608 if len(heads) != 1:
1611 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1609 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1612 metadata.update({
1610 metadata.update({
1613 'heads': heads
1611 'heads': heads
1614 })
1612 })
1615
1613
1616 merge_state = MergeResponse(
1614 merge_state = MergeResponse(
1617 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1615 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1618
1616
1619 return merge_state
1617 return merge_state
1620
1618
1621 def _refresh_reference(self, reference, vcs_repository):
1619 def _refresh_reference(self, reference, vcs_repository):
1622 if reference.type in self.UPDATABLE_REF_TYPES:
1620 if reference.type in self.UPDATABLE_REF_TYPES:
1623 name_or_id = reference.name
1621 name_or_id = reference.name
1624 else:
1622 else:
1625 name_or_id = reference.commit_id
1623 name_or_id = reference.commit_id
1626
1624
1627 refreshed_commit = vcs_repository.get_commit(name_or_id)
1625 refreshed_commit = vcs_repository.get_commit(name_or_id)
1628 refreshed_reference = Reference(
1626 refreshed_reference = Reference(
1629 reference.type, reference.name, refreshed_commit.raw_id)
1627 reference.type, reference.name, refreshed_commit.raw_id)
1630 return refreshed_reference
1628 return refreshed_reference
1631
1629
1632 def _needs_merge_state_refresh(self, pull_request, target_reference):
1630 def _needs_merge_state_refresh(self, pull_request, target_reference):
1633 return not(
1631 return not(
1634 pull_request.revisions and
1632 pull_request.revisions and
1635 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1633 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1636 target_reference.commit_id == pull_request._last_merge_target_rev)
1634 target_reference.commit_id == pull_request._last_merge_target_rev)
1637
1635
1638 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1636 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1639 workspace_id = self._workspace_id(pull_request)
1637 workspace_id = self._workspace_id(pull_request)
1640 source_vcs = pull_request.source_repo.scm_instance()
1638 source_vcs = pull_request.source_repo.scm_instance()
1641 repo_id = pull_request.target_repo.repo_id
1639 repo_id = pull_request.target_repo.repo_id
1642 use_rebase = self._use_rebase_for_merging(pull_request)
1640 use_rebase = self._use_rebase_for_merging(pull_request)
1643 close_branch = self._close_branch_before_merging(pull_request)
1641 close_branch = self._close_branch_before_merging(pull_request)
1644 merge_state = target_vcs.merge(
1642 merge_state = target_vcs.merge(
1645 repo_id, workspace_id,
1643 repo_id, workspace_id,
1646 target_reference, source_vcs, pull_request.source_ref_parts,
1644 target_reference, source_vcs, pull_request.source_ref_parts,
1647 dry_run=True, use_rebase=use_rebase,
1645 dry_run=True, use_rebase=use_rebase,
1648 close_branch=close_branch)
1646 close_branch=close_branch)
1649
1647
1650 # Do not store the response if there was an unknown error.
1648 # Do not store the response if there was an unknown error.
1651 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1649 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1652 pull_request._last_merge_source_rev = \
1650 pull_request._last_merge_source_rev = \
1653 pull_request.source_ref_parts.commit_id
1651 pull_request.source_ref_parts.commit_id
1654 pull_request._last_merge_target_rev = target_reference.commit_id
1652 pull_request._last_merge_target_rev = target_reference.commit_id
1655 pull_request.last_merge_status = merge_state.failure_reason
1653 pull_request.last_merge_status = merge_state.failure_reason
1656 pull_request.last_merge_metadata = merge_state.metadata
1654 pull_request.last_merge_metadata = merge_state.metadata
1657
1655
1658 pull_request.shadow_merge_ref = merge_state.merge_ref
1656 pull_request.shadow_merge_ref = merge_state.merge_ref
1659 Session().add(pull_request)
1657 Session().add(pull_request)
1660 Session().commit()
1658 Session().commit()
1661
1659
1662 return merge_state
1660 return merge_state
1663
1661
1664 def _workspace_id(self, pull_request):
1662 def _workspace_id(self, pull_request):
1665 workspace_id = 'pr-%s' % pull_request.pull_request_id
1663 workspace_id = 'pr-%s' % pull_request.pull_request_id
1666 return workspace_id
1664 return workspace_id
1667
1665
1668 def generate_repo_data(self, repo, commit_id=None, branch=None,
1666 def generate_repo_data(self, repo, commit_id=None, branch=None,
1669 bookmark=None, translator=None):
1667 bookmark=None, translator=None):
1670 from rhodecode.model.repo import RepoModel
1668 from rhodecode.model.repo import RepoModel
1671
1669
1672 all_refs, selected_ref = \
1670 all_refs, selected_ref = \
1673 self._get_repo_pullrequest_sources(
1671 self._get_repo_pullrequest_sources(
1674 repo.scm_instance(), commit_id=commit_id,
1672 repo.scm_instance(), commit_id=commit_id,
1675 branch=branch, bookmark=bookmark, translator=translator)
1673 branch=branch, bookmark=bookmark, translator=translator)
1676
1674
1677 refs_select2 = []
1675 refs_select2 = []
1678 for element in all_refs:
1676 for element in all_refs:
1679 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1677 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1680 refs_select2.append({'text': element[1], 'children': children})
1678 refs_select2.append({'text': element[1], 'children': children})
1681
1679
1682 return {
1680 return {
1683 'user': {
1681 'user': {
1684 'user_id': repo.user.user_id,
1682 'user_id': repo.user.user_id,
1685 'username': repo.user.username,
1683 'username': repo.user.username,
1686 'firstname': repo.user.first_name,
1684 'firstname': repo.user.first_name,
1687 'lastname': repo.user.last_name,
1685 'lastname': repo.user.last_name,
1688 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1686 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1689 },
1687 },
1690 'name': repo.repo_name,
1688 'name': repo.repo_name,
1691 'link': RepoModel().get_url(repo),
1689 'link': RepoModel().get_url(repo),
1692 'description': h.chop_at_smart(repo.description_safe, '\n'),
1690 'description': h.chop_at_smart(repo.description_safe, '\n'),
1693 'refs': {
1691 'refs': {
1694 'all_refs': all_refs,
1692 'all_refs': all_refs,
1695 'selected_ref': selected_ref,
1693 'selected_ref': selected_ref,
1696 'select2_refs': refs_select2
1694 'select2_refs': refs_select2
1697 }
1695 }
1698 }
1696 }
1699
1697
1700 def generate_pullrequest_title(self, source, source_ref, target):
1698 def generate_pullrequest_title(self, source, source_ref, target):
1701 return u'{source}#{at_ref} to {target}'.format(
1699 return u'{source}#{at_ref} to {target}'.format(
1702 source=source,
1700 source=source,
1703 at_ref=source_ref,
1701 at_ref=source_ref,
1704 target=target,
1702 target=target,
1705 )
1703 )
1706
1704
1707 def _cleanup_merge_workspace(self, pull_request):
1705 def _cleanup_merge_workspace(self, pull_request):
1708 # Merging related cleanup
1706 # Merging related cleanup
1709 repo_id = pull_request.target_repo.repo_id
1707 repo_id = pull_request.target_repo.repo_id
1710 target_scm = pull_request.target_repo.scm_instance()
1708 target_scm = pull_request.target_repo.scm_instance()
1711 workspace_id = self._workspace_id(pull_request)
1709 workspace_id = self._workspace_id(pull_request)
1712
1710
1713 try:
1711 try:
1714 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1712 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1715 except NotImplementedError:
1713 except NotImplementedError:
1716 pass
1714 pass
1717
1715
1718 def _get_repo_pullrequest_sources(
1716 def _get_repo_pullrequest_sources(
1719 self, repo, commit_id=None, branch=None, bookmark=None,
1717 self, repo, commit_id=None, branch=None, bookmark=None,
1720 translator=None):
1718 translator=None):
1721 """
1719 """
1722 Return a structure with repo's interesting commits, suitable for
1720 Return a structure with repo's interesting commits, suitable for
1723 the selectors in pullrequest controller
1721 the selectors in pullrequest controller
1724
1722
1725 :param commit_id: a commit that must be in the list somehow
1723 :param commit_id: a commit that must be in the list somehow
1726 and selected by default
1724 and selected by default
1727 :param branch: a branch that must be in the list and selected
1725 :param branch: a branch that must be in the list and selected
1728 by default - even if closed
1726 by default - even if closed
1729 :param bookmark: a bookmark that must be in the list and selected
1727 :param bookmark: a bookmark that must be in the list and selected
1730 """
1728 """
1731 _ = translator or get_current_request().translate
1729 _ = translator or get_current_request().translate
1732
1730
1733 commit_id = safe_str(commit_id) if commit_id else None
1731 commit_id = safe_str(commit_id) if commit_id else None
1734 branch = safe_unicode(branch) if branch else None
1732 branch = safe_unicode(branch) if branch else None
1735 bookmark = safe_unicode(bookmark) if bookmark else None
1733 bookmark = safe_unicode(bookmark) if bookmark else None
1736
1734
1737 selected = None
1735 selected = None
1738
1736
1739 # order matters: first source that has commit_id in it will be selected
1737 # order matters: first source that has commit_id in it will be selected
1740 sources = []
1738 sources = []
1741 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1739 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1742 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1740 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1743
1741
1744 if commit_id:
1742 if commit_id:
1745 ref_commit = (h.short_id(commit_id), commit_id)
1743 ref_commit = (h.short_id(commit_id), commit_id)
1746 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1744 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1747
1745
1748 sources.append(
1746 sources.append(
1749 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1747 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1750 )
1748 )
1751
1749
1752 groups = []
1750 groups = []
1753
1751
1754 for group_key, ref_list, group_name, match in sources:
1752 for group_key, ref_list, group_name, match in sources:
1755 group_refs = []
1753 group_refs = []
1756 for ref_name, ref_id in ref_list:
1754 for ref_name, ref_id in ref_list:
1757 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1755 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1758 group_refs.append((ref_key, ref_name))
1756 group_refs.append((ref_key, ref_name))
1759
1757
1760 if not selected:
1758 if not selected:
1761 if set([commit_id, match]) & set([ref_id, ref_name]):
1759 if set([commit_id, match]) & set([ref_id, ref_name]):
1762 selected = ref_key
1760 selected = ref_key
1763
1761
1764 if group_refs:
1762 if group_refs:
1765 groups.append((group_refs, group_name))
1763 groups.append((group_refs, group_name))
1766
1764
1767 if not selected:
1765 if not selected:
1768 ref = commit_id or branch or bookmark
1766 ref = commit_id or branch or bookmark
1769 if ref:
1767 if ref:
1770 raise CommitDoesNotExistError(
1768 raise CommitDoesNotExistError(
1771 u'No commit refs could be found matching: {}'.format(ref))
1769 u'No commit refs could be found matching: {}'.format(ref))
1772 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1770 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1773 selected = u'branch:{}:{}'.format(
1771 selected = u'branch:{}:{}'.format(
1774 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1772 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1775 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1773 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1776 )
1774 )
1777 elif repo.commit_ids:
1775 elif repo.commit_ids:
1778 # make the user select in this case
1776 # make the user select in this case
1779 selected = None
1777 selected = None
1780 else:
1778 else:
1781 raise EmptyRepositoryError()
1779 raise EmptyRepositoryError()
1782 return groups, selected
1780 return groups, selected
1783
1781
1784 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1782 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1785 hide_whitespace_changes, diff_context):
1783 hide_whitespace_changes, diff_context):
1786
1784
1787 return self._get_diff_from_pr_or_version(
1785 return self._get_diff_from_pr_or_version(
1788 source_repo, source_ref_id, target_ref_id,
1786 source_repo, source_ref_id, target_ref_id,
1789 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1787 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1790
1788
1791 def _get_diff_from_pr_or_version(
1789 def _get_diff_from_pr_or_version(
1792 self, source_repo, source_ref_id, target_ref_id,
1790 self, source_repo, source_ref_id, target_ref_id,
1793 hide_whitespace_changes, diff_context):
1791 hide_whitespace_changes, diff_context):
1794
1792
1795 target_commit = source_repo.get_commit(
1793 target_commit = source_repo.get_commit(
1796 commit_id=safe_str(target_ref_id))
1794 commit_id=safe_str(target_ref_id))
1797 source_commit = source_repo.get_commit(
1795 source_commit = source_repo.get_commit(
1798 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1796 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1799 if isinstance(source_repo, Repository):
1797 if isinstance(source_repo, Repository):
1800 vcs_repo = source_repo.scm_instance()
1798 vcs_repo = source_repo.scm_instance()
1801 else:
1799 else:
1802 vcs_repo = source_repo
1800 vcs_repo = source_repo
1803
1801
1804 # TODO: johbo: In the context of an update, we cannot reach
1802 # TODO: johbo: In the context of an update, we cannot reach
1805 # the old commit anymore with our normal mechanisms. It needs
1803 # the old commit anymore with our normal mechanisms. It needs
1806 # some sort of special support in the vcs layer to avoid this
1804 # some sort of special support in the vcs layer to avoid this
1807 # workaround.
1805 # workaround.
1808 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1806 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1809 vcs_repo.alias == 'git'):
1807 vcs_repo.alias == 'git'):
1810 source_commit.raw_id = safe_str(source_ref_id)
1808 source_commit.raw_id = safe_str(source_ref_id)
1811
1809
1812 log.debug('calculating diff between '
1810 log.debug('calculating diff between '
1813 'source_ref:%s and target_ref:%s for repo `%s`',
1811 'source_ref:%s and target_ref:%s for repo `%s`',
1814 target_ref_id, source_ref_id,
1812 target_ref_id, source_ref_id,
1815 safe_unicode(vcs_repo.path))
1813 safe_unicode(vcs_repo.path))
1816
1814
1817 vcs_diff = vcs_repo.get_diff(
1815 vcs_diff = vcs_repo.get_diff(
1818 commit1=target_commit, commit2=source_commit,
1816 commit1=target_commit, commit2=source_commit,
1819 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1817 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1820 return vcs_diff
1818 return vcs_diff
1821
1819
1822 def _is_merge_enabled(self, pull_request):
1820 def _is_merge_enabled(self, pull_request):
1823 return self._get_general_setting(
1821 return self._get_general_setting(
1824 pull_request, 'rhodecode_pr_merge_enabled')
1822 pull_request, 'rhodecode_pr_merge_enabled')
1825
1823
1826 def _use_rebase_for_merging(self, pull_request):
1824 def _use_rebase_for_merging(self, pull_request):
1827 repo_type = pull_request.target_repo.repo_type
1825 repo_type = pull_request.target_repo.repo_type
1828 if repo_type == 'hg':
1826 if repo_type == 'hg':
1829 return self._get_general_setting(
1827 return self._get_general_setting(
1830 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1828 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1831 elif repo_type == 'git':
1829 elif repo_type == 'git':
1832 return self._get_general_setting(
1830 return self._get_general_setting(
1833 pull_request, 'rhodecode_git_use_rebase_for_merging')
1831 pull_request, 'rhodecode_git_use_rebase_for_merging')
1834
1832
1835 return False
1833 return False
1836
1834
1837 def _user_name_for_merging(self, pull_request, user):
1835 def _user_name_for_merging(self, pull_request, user):
1838 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1836 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1839 if env_user_name_attr and hasattr(user, env_user_name_attr):
1837 if env_user_name_attr and hasattr(user, env_user_name_attr):
1840 user_name_attr = env_user_name_attr
1838 user_name_attr = env_user_name_attr
1841 else:
1839 else:
1842 user_name_attr = 'short_contact'
1840 user_name_attr = 'short_contact'
1843
1841
1844 user_name = getattr(user, user_name_attr)
1842 user_name = getattr(user, user_name_attr)
1845 return user_name
1843 return user_name
1846
1844
1847 def _close_branch_before_merging(self, pull_request):
1845 def _close_branch_before_merging(self, pull_request):
1848 repo_type = pull_request.target_repo.repo_type
1846 repo_type = pull_request.target_repo.repo_type
1849 if repo_type == 'hg':
1847 if repo_type == 'hg':
1850 return self._get_general_setting(
1848 return self._get_general_setting(
1851 pull_request, 'rhodecode_hg_close_branch_before_merging')
1849 pull_request, 'rhodecode_hg_close_branch_before_merging')
1852 elif repo_type == 'git':
1850 elif repo_type == 'git':
1853 return self._get_general_setting(
1851 return self._get_general_setting(
1854 pull_request, 'rhodecode_git_close_branch_before_merging')
1852 pull_request, 'rhodecode_git_close_branch_before_merging')
1855
1853
1856 return False
1854 return False
1857
1855
1858 def _get_general_setting(self, pull_request, settings_key, default=False):
1856 def _get_general_setting(self, pull_request, settings_key, default=False):
1859 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1857 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1860 settings = settings_model.get_general_settings()
1858 settings = settings_model.get_general_settings()
1861 return settings.get(settings_key, default)
1859 return settings.get(settings_key, default)
1862
1860
1863 def _log_audit_action(self, action, action_data, user, pull_request):
1861 def _log_audit_action(self, action, action_data, user, pull_request):
1864 audit_logger.store(
1862 audit_logger.store(
1865 action=action,
1863 action=action,
1866 action_data=action_data,
1864 action_data=action_data,
1867 user=user,
1865 user=user,
1868 repo=pull_request.target_repo)
1866 repo=pull_request.target_repo)
1869
1867
1870 def get_reviewer_functions(self):
1868 def get_reviewer_functions(self):
1871 """
1869 """
1872 Fetches functions for validation and fetching default reviewers.
1870 Fetches functions for validation and fetching default reviewers.
1873 If available we use the EE package, else we fallback to CE
1871 If available we use the EE package, else we fallback to CE
1874 package functions
1872 package functions
1875 """
1873 """
1876 try:
1874 try:
1877 from rc_reviewers.utils import get_default_reviewers_data
1875 from rc_reviewers.utils import get_default_reviewers_data
1878 from rc_reviewers.utils import validate_default_reviewers
1876 from rc_reviewers.utils import validate_default_reviewers
1879 except ImportError:
1877 except ImportError:
1880 from rhodecode.apps.repository.utils import get_default_reviewers_data
1878 from rhodecode.apps.repository.utils import get_default_reviewers_data
1881 from rhodecode.apps.repository.utils import validate_default_reviewers
1879 from rhodecode.apps.repository.utils import validate_default_reviewers
1882
1880
1883 return get_default_reviewers_data, validate_default_reviewers
1881 return get_default_reviewers_data, validate_default_reviewers
1884
1882
1885
1883
1886 class MergeCheck(object):
1884 class MergeCheck(object):
1887 """
1885 """
1888 Perform Merge Checks and returns a check object which stores information
1886 Perform Merge Checks and returns a check object which stores information
1889 about merge errors, and merge conditions
1887 about merge errors, and merge conditions
1890 """
1888 """
1891 TODO_CHECK = 'todo'
1889 TODO_CHECK = 'todo'
1892 PERM_CHECK = 'perm'
1890 PERM_CHECK = 'perm'
1893 REVIEW_CHECK = 'review'
1891 REVIEW_CHECK = 'review'
1894 MERGE_CHECK = 'merge'
1892 MERGE_CHECK = 'merge'
1895 WIP_CHECK = 'wip'
1893 WIP_CHECK = 'wip'
1896
1894
1897 def __init__(self):
1895 def __init__(self):
1898 self.review_status = None
1896 self.review_status = None
1899 self.merge_possible = None
1897 self.merge_possible = None
1900 self.merge_msg = ''
1898 self.merge_msg = ''
1901 self.merge_response = None
1899 self.merge_response = None
1902 self.failed = None
1900 self.failed = None
1903 self.errors = []
1901 self.errors = []
1904 self.error_details = OrderedDict()
1902 self.error_details = OrderedDict()
1905 self.source_commit = AttributeDict()
1903 self.source_commit = AttributeDict()
1906 self.target_commit = AttributeDict()
1904 self.target_commit = AttributeDict()
1907
1905
1908 def __repr__(self):
1906 def __repr__(self):
1909 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
1907 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
1910 self.merge_possible, self.failed, self.errors)
1908 self.merge_possible, self.failed, self.errors)
1911
1909
1912 def push_error(self, error_type, message, error_key, details):
1910 def push_error(self, error_type, message, error_key, details):
1913 self.failed = True
1911 self.failed = True
1914 self.errors.append([error_type, message])
1912 self.errors.append([error_type, message])
1915 self.error_details[error_key] = dict(
1913 self.error_details[error_key] = dict(
1916 details=details,
1914 details=details,
1917 error_type=error_type,
1915 error_type=error_type,
1918 message=message
1916 message=message
1919 )
1917 )
1920
1918
1921 @classmethod
1919 @classmethod
1922 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1920 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1923 force_shadow_repo_refresh=False):
1921 force_shadow_repo_refresh=False):
1924 _ = translator
1922 _ = translator
1925 merge_check = cls()
1923 merge_check = cls()
1926
1924
1927 # title has WIP:
1925 # title has WIP:
1928 if pull_request.work_in_progress:
1926 if pull_request.work_in_progress:
1929 log.debug("MergeCheck: cannot merge, title has wip: marker.")
1927 log.debug("MergeCheck: cannot merge, title has wip: marker.")
1930
1928
1931 msg = _('WIP marker in title prevents from accidental merge.')
1929 msg = _('WIP marker in title prevents from accidental merge.')
1932 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
1930 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
1933 if fail_early:
1931 if fail_early:
1934 return merge_check
1932 return merge_check
1935
1933
1936 # permissions to merge
1934 # permissions to merge
1937 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
1935 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
1938 if not user_allowed_to_merge:
1936 if not user_allowed_to_merge:
1939 log.debug("MergeCheck: cannot merge, approval is pending.")
1937 log.debug("MergeCheck: cannot merge, approval is pending.")
1940
1938
1941 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1939 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1942 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1940 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1943 if fail_early:
1941 if fail_early:
1944 return merge_check
1942 return merge_check
1945
1943
1946 # permission to merge into the target branch
1944 # permission to merge into the target branch
1947 target_commit_id = pull_request.target_ref_parts.commit_id
1945 target_commit_id = pull_request.target_ref_parts.commit_id
1948 if pull_request.target_ref_parts.type == 'branch':
1946 if pull_request.target_ref_parts.type == 'branch':
1949 branch_name = pull_request.target_ref_parts.name
1947 branch_name = pull_request.target_ref_parts.name
1950 else:
1948 else:
1951 # for mercurial we can always figure out the branch from the commit
1949 # for mercurial we can always figure out the branch from the commit
1952 # in case of bookmark
1950 # in case of bookmark
1953 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1951 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1954 branch_name = target_commit.branch
1952 branch_name = target_commit.branch
1955
1953
1956 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1954 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1957 pull_request.target_repo.repo_name, branch_name)
1955 pull_request.target_repo.repo_name, branch_name)
1958 if branch_perm and branch_perm == 'branch.none':
1956 if branch_perm and branch_perm == 'branch.none':
1959 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1957 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1960 branch_name, rule)
1958 branch_name, rule)
1961 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1959 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1962 if fail_early:
1960 if fail_early:
1963 return merge_check
1961 return merge_check
1964
1962
1965 # review status, must be always present
1963 # review status, must be always present
1966 review_status = pull_request.calculated_review_status()
1964 review_status = pull_request.calculated_review_status()
1967 merge_check.review_status = review_status
1965 merge_check.review_status = review_status
1968
1966
1969 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1967 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1970 if not status_approved:
1968 if not status_approved:
1971 log.debug("MergeCheck: cannot merge, approval is pending.")
1969 log.debug("MergeCheck: cannot merge, approval is pending.")
1972
1970
1973 msg = _('Pull request reviewer approval is pending.')
1971 msg = _('Pull request reviewer approval is pending.')
1974
1972
1975 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1973 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1976
1974
1977 if fail_early:
1975 if fail_early:
1978 return merge_check
1976 return merge_check
1979
1977
1980 # left over TODOs
1978 # left over TODOs
1981 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1979 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1982 if todos:
1980 if todos:
1983 log.debug("MergeCheck: cannot merge, {} "
1981 log.debug("MergeCheck: cannot merge, {} "
1984 "unresolved TODOs left.".format(len(todos)))
1982 "unresolved TODOs left.".format(len(todos)))
1985
1983
1986 if len(todos) == 1:
1984 if len(todos) == 1:
1987 msg = _('Cannot merge, {} TODO still not resolved.').format(
1985 msg = _('Cannot merge, {} TODO still not resolved.').format(
1988 len(todos))
1986 len(todos))
1989 else:
1987 else:
1990 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1988 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1991 len(todos))
1989 len(todos))
1992
1990
1993 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1991 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1994
1992
1995 if fail_early:
1993 if fail_early:
1996 return merge_check
1994 return merge_check
1997
1995
1998 # merge possible, here is the filesystem simulation + shadow repo
1996 # merge possible, here is the filesystem simulation + shadow repo
1999 merge_response, merge_status, msg = PullRequestModel().merge_status(
1997 merge_response, merge_status, msg = PullRequestModel().merge_status(
2000 pull_request, translator=translator,
1998 pull_request, translator=translator,
2001 force_shadow_repo_refresh=force_shadow_repo_refresh)
1999 force_shadow_repo_refresh=force_shadow_repo_refresh)
2002
2000
2003 merge_check.merge_possible = merge_status
2001 merge_check.merge_possible = merge_status
2004 merge_check.merge_msg = msg
2002 merge_check.merge_msg = msg
2005 merge_check.merge_response = merge_response
2003 merge_check.merge_response = merge_response
2006
2004
2007 source_ref_id = pull_request.source_ref_parts.commit_id
2005 source_ref_id = pull_request.source_ref_parts.commit_id
2008 target_ref_id = pull_request.target_ref_parts.commit_id
2006 target_ref_id = pull_request.target_ref_parts.commit_id
2009
2007
2010 try:
2008 try:
2011 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2009 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2012 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2010 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2013 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2011 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2014 merge_check.source_commit.current_raw_id = source_commit.raw_id
2012 merge_check.source_commit.current_raw_id = source_commit.raw_id
2015 merge_check.source_commit.previous_raw_id = source_ref_id
2013 merge_check.source_commit.previous_raw_id = source_ref_id
2016
2014
2017 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2015 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2018 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2016 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2019 merge_check.target_commit.current_raw_id = target_commit.raw_id
2017 merge_check.target_commit.current_raw_id = target_commit.raw_id
2020 merge_check.target_commit.previous_raw_id = target_ref_id
2018 merge_check.target_commit.previous_raw_id = target_ref_id
2021 except (SourceRefMissing, TargetRefMissing):
2019 except (SourceRefMissing, TargetRefMissing):
2022 pass
2020 pass
2023
2021
2024 if not merge_status:
2022 if not merge_status:
2025 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2023 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2026 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2024 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2027
2025
2028 if fail_early:
2026 if fail_early:
2029 return merge_check
2027 return merge_check
2030
2028
2031 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2029 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2032 return merge_check
2030 return merge_check
2033
2031
2034 @classmethod
2032 @classmethod
2035 def get_merge_conditions(cls, pull_request, translator):
2033 def get_merge_conditions(cls, pull_request, translator):
2036 _ = translator
2034 _ = translator
2037 merge_details = {}
2035 merge_details = {}
2038
2036
2039 model = PullRequestModel()
2037 model = PullRequestModel()
2040 use_rebase = model._use_rebase_for_merging(pull_request)
2038 use_rebase = model._use_rebase_for_merging(pull_request)
2041
2039
2042 if use_rebase:
2040 if use_rebase:
2043 merge_details['merge_strategy'] = dict(
2041 merge_details['merge_strategy'] = dict(
2044 details={},
2042 details={},
2045 message=_('Merge strategy: rebase')
2043 message=_('Merge strategy: rebase')
2046 )
2044 )
2047 else:
2045 else:
2048 merge_details['merge_strategy'] = dict(
2046 merge_details['merge_strategy'] = dict(
2049 details={},
2047 details={},
2050 message=_('Merge strategy: explicit merge commit')
2048 message=_('Merge strategy: explicit merge commit')
2051 )
2049 )
2052
2050
2053 close_branch = model._close_branch_before_merging(pull_request)
2051 close_branch = model._close_branch_before_merging(pull_request)
2054 if close_branch:
2052 if close_branch:
2055 repo_type = pull_request.target_repo.repo_type
2053 repo_type = pull_request.target_repo.repo_type
2056 close_msg = ''
2054 close_msg = ''
2057 if repo_type == 'hg':
2055 if repo_type == 'hg':
2058 close_msg = _('Source branch will be closed before the merge.')
2056 close_msg = _('Source branch will be closed before the merge.')
2059 elif repo_type == 'git':
2057 elif repo_type == 'git':
2060 close_msg = _('Source branch will be deleted after the merge.')
2058 close_msg = _('Source branch will be deleted after the merge.')
2061
2059
2062 merge_details['close_branch'] = dict(
2060 merge_details['close_branch'] = dict(
2063 details={},
2061 details={},
2064 message=close_msg
2062 message=close_msg
2065 )
2063 )
2066
2064
2067 return merge_details
2065 return merge_details
2068
2066
2069
2067
2070 ChangeTuple = collections.namedtuple(
2068 ChangeTuple = collections.namedtuple(
2071 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2069 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2072
2070
2073 FileChangeTuple = collections.namedtuple(
2071 FileChangeTuple = collections.namedtuple(
2074 'FileChangeTuple', ['added', 'modified', 'removed'])
2072 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,1050 +1,1046 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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 users model for RhodeCode
22 users model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import datetime
27 import datetime
28 import ipaddress
28 import ipaddress
29
29
30 from pyramid.threadlocal import get_current_request
30 from pyramid.threadlocal import get_current_request
31 from sqlalchemy.exc import DatabaseError
31 from sqlalchemy.exc import DatabaseError
32
32
33 from rhodecode import events
33 from rhodecode import events
34 from rhodecode.lib.user_log_filter import user_log_filter
34 from rhodecode.lib.user_log_filter import user_log_filter
35 from rhodecode.lib.utils2 import (
35 from rhodecode.lib.utils2 import (
36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
37 AttributeDict, str2bool)
37 AttributeDict, str2bool)
38 from rhodecode.lib.exceptions import (
38 from rhodecode.lib.exceptions import (
39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
41 UserOwnsPullRequestsException, UserOwnsArtifactsException)
41 UserOwnsPullRequestsException, UserOwnsArtifactsException)
42 from rhodecode.lib.caching_query import FromCache
42 from rhodecode.lib.caching_query import FromCache
43 from rhodecode.model import BaseModel
43 from rhodecode.model import BaseModel
44 from rhodecode.model.db import (
44 from rhodecode.model.db import (
45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
46 UserEmailMap, UserIpMap, UserLog)
46 UserEmailMap, UserIpMap, UserLog)
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.auth_token import AuthTokenModel
48 from rhodecode.model.auth_token import AuthTokenModel
49 from rhodecode.model.repo_group import RepoGroupModel
49 from rhodecode.model.repo_group import RepoGroupModel
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 class UserModel(BaseModel):
54 class UserModel(BaseModel):
55 cls = User
55 cls = User
56
56
57 def get(self, user_id, cache=False):
57 def get(self, user_id, cache=False):
58 user = self.sa.query(User)
58 user = self.sa.query(User)
59 if cache:
59 if cache:
60 user = user.options(
60 user = user.options(
61 FromCache("sql_cache_short", "get_user_%s" % user_id))
61 FromCache("sql_cache_short", "get_user_%s" % user_id))
62 return user.get(user_id)
62 return user.get(user_id)
63
63
64 def get_user(self, user):
64 def get_user(self, user):
65 return self._get_user(user)
65 return self._get_user(user)
66
66
67 def _serialize_user(self, user):
67 def _serialize_user(self, user):
68 import rhodecode.lib.helpers as h
68 import rhodecode.lib.helpers as h
69
69
70 return {
70 return {
71 'id': user.user_id,
71 'id': user.user_id,
72 'first_name': user.first_name,
72 'first_name': user.first_name,
73 'last_name': user.last_name,
73 'last_name': user.last_name,
74 'username': user.username,
74 'username': user.username,
75 'email': user.email,
75 'email': user.email,
76 'icon_link': h.gravatar_url(user.email, 30),
76 'icon_link': h.gravatar_url(user.email, 30),
77 'profile_link': h.link_to_user(user),
77 'profile_link': h.link_to_user(user),
78 'value_display': h.escape(h.person(user)),
78 'value_display': h.escape(h.person(user)),
79 'value': user.username,
79 'value': user.username,
80 'value_type': 'user',
80 'value_type': 'user',
81 'active': user.active,
81 'active': user.active,
82 }
82 }
83
83
84 def get_users(self, name_contains=None, limit=20, only_active=True):
84 def get_users(self, name_contains=None, limit=20, only_active=True):
85
85
86 query = self.sa.query(User)
86 query = self.sa.query(User)
87 if only_active:
87 if only_active:
88 query = query.filter(User.active == true())
88 query = query.filter(User.active == true())
89
89
90 if name_contains:
90 if name_contains:
91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
92 query = query.filter(
92 query = query.filter(
93 or_(
93 or_(
94 User.name.ilike(ilike_expression),
94 User.name.ilike(ilike_expression),
95 User.lastname.ilike(ilike_expression),
95 User.lastname.ilike(ilike_expression),
96 User.username.ilike(ilike_expression)
96 User.username.ilike(ilike_expression)
97 )
97 )
98 )
98 )
99 query = query.limit(limit)
99 query = query.limit(limit)
100 users = query.all()
100 users = query.all()
101
101
102 _users = [
102 _users = [
103 self._serialize_user(user) for user in users
103 self._serialize_user(user) for user in users
104 ]
104 ]
105 return _users
105 return _users
106
106
107 def get_by_username(self, username, cache=False, case_insensitive=False):
107 def get_by_username(self, username, cache=False, case_insensitive=False):
108
108
109 if case_insensitive:
109 if case_insensitive:
110 user = self.sa.query(User).filter(User.username.ilike(username))
110 user = self.sa.query(User).filter(User.username.ilike(username))
111 else:
111 else:
112 user = self.sa.query(User)\
112 user = self.sa.query(User)\
113 .filter(User.username == username)
113 .filter(User.username == username)
114 if cache:
114 if cache:
115 name_key = _hash_key(username)
115 name_key = _hash_key(username)
116 user = user.options(
116 user = user.options(
117 FromCache("sql_cache_short", "get_user_%s" % name_key))
117 FromCache("sql_cache_short", "get_user_%s" % name_key))
118 return user.scalar()
118 return user.scalar()
119
119
120 def get_by_email(self, email, cache=False, case_insensitive=False):
120 def get_by_email(self, email, cache=False, case_insensitive=False):
121 return User.get_by_email(email, case_insensitive, cache)
121 return User.get_by_email(email, case_insensitive, cache)
122
122
123 def get_by_auth_token(self, auth_token, cache=False):
123 def get_by_auth_token(self, auth_token, cache=False):
124 return User.get_by_auth_token(auth_token, cache)
124 return User.get_by_auth_token(auth_token, cache)
125
125
126 def get_active_user_count(self, cache=False):
126 def get_active_user_count(self, cache=False):
127 qry = User.query().filter(
127 qry = User.query().filter(
128 User.active == true()).filter(
128 User.active == true()).filter(
129 User.username != User.DEFAULT_USER)
129 User.username != User.DEFAULT_USER)
130 if cache:
130 if cache:
131 qry = qry.options(
131 qry = qry.options(
132 FromCache("sql_cache_short", "get_active_users"))
132 FromCache("sql_cache_short", "get_active_users"))
133 return qry.count()
133 return qry.count()
134
134
135 def create(self, form_data, cur_user=None):
135 def create(self, form_data, cur_user=None):
136 if not cur_user:
136 if not cur_user:
137 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
137 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
138
138
139 user_data = {
139 user_data = {
140 'username': form_data['username'],
140 'username': form_data['username'],
141 'password': form_data['password'],
141 'password': form_data['password'],
142 'email': form_data['email'],
142 'email': form_data['email'],
143 'firstname': form_data['firstname'],
143 'firstname': form_data['firstname'],
144 'lastname': form_data['lastname'],
144 'lastname': form_data['lastname'],
145 'active': form_data['active'],
145 'active': form_data['active'],
146 'extern_type': form_data['extern_type'],
146 'extern_type': form_data['extern_type'],
147 'extern_name': form_data['extern_name'],
147 'extern_name': form_data['extern_name'],
148 'admin': False,
148 'admin': False,
149 'cur_user': cur_user
149 'cur_user': cur_user
150 }
150 }
151
151
152 if 'create_repo_group' in form_data:
152 if 'create_repo_group' in form_data:
153 user_data['create_repo_group'] = str2bool(
153 user_data['create_repo_group'] = str2bool(
154 form_data.get('create_repo_group'))
154 form_data.get('create_repo_group'))
155
155
156 try:
156 try:
157 if form_data.get('password_change'):
157 if form_data.get('password_change'):
158 user_data['force_password_change'] = True
158 user_data['force_password_change'] = True
159 return UserModel().create_or_update(**user_data)
159 return UserModel().create_or_update(**user_data)
160 except Exception:
160 except Exception:
161 log.error(traceback.format_exc())
161 log.error(traceback.format_exc())
162 raise
162 raise
163
163
164 def update_user(self, user, skip_attrs=None, **kwargs):
164 def update_user(self, user, skip_attrs=None, **kwargs):
165 from rhodecode.lib.auth import get_crypt_password
165 from rhodecode.lib.auth import get_crypt_password
166
166
167 user = self._get_user(user)
167 user = self._get_user(user)
168 if user.username == User.DEFAULT_USER:
168 if user.username == User.DEFAULT_USER:
169 raise DefaultUserException(
169 raise DefaultUserException(
170 "You can't edit this user (`%(username)s`) since it's "
170 "You can't edit this user (`%(username)s`) since it's "
171 "crucial for entire application" % {
171 "crucial for entire application" % {
172 'username': user.username})
172 'username': user.username})
173
173
174 # first store only defaults
174 # first store only defaults
175 user_attrs = {
175 user_attrs = {
176 'updating_user_id': user.user_id,
176 'updating_user_id': user.user_id,
177 'username': user.username,
177 'username': user.username,
178 'password': user.password,
178 'password': user.password,
179 'email': user.email,
179 'email': user.email,
180 'firstname': user.name,
180 'firstname': user.name,
181 'lastname': user.lastname,
181 'lastname': user.lastname,
182 'description': user.description,
182 'description': user.description,
183 'active': user.active,
183 'active': user.active,
184 'admin': user.admin,
184 'admin': user.admin,
185 'extern_name': user.extern_name,
185 'extern_name': user.extern_name,
186 'extern_type': user.extern_type,
186 'extern_type': user.extern_type,
187 'language': user.user_data.get('language')
187 'language': user.user_data.get('language')
188 }
188 }
189
189
190 # in case there's new_password, that comes from form, use it to
190 # in case there's new_password, that comes from form, use it to
191 # store password
191 # store password
192 if kwargs.get('new_password'):
192 if kwargs.get('new_password'):
193 kwargs['password'] = kwargs['new_password']
193 kwargs['password'] = kwargs['new_password']
194
194
195 # cleanups, my_account password change form
195 # cleanups, my_account password change form
196 kwargs.pop('current_password', None)
196 kwargs.pop('current_password', None)
197 kwargs.pop('new_password', None)
197 kwargs.pop('new_password', None)
198
198
199 # cleanups, user edit password change form
199 # cleanups, user edit password change form
200 kwargs.pop('password_confirmation', None)
200 kwargs.pop('password_confirmation', None)
201 kwargs.pop('password_change', None)
201 kwargs.pop('password_change', None)
202
202
203 # create repo group on user creation
203 # create repo group on user creation
204 kwargs.pop('create_repo_group', None)
204 kwargs.pop('create_repo_group', None)
205
205
206 # legacy forms send name, which is the firstname
206 # legacy forms send name, which is the firstname
207 firstname = kwargs.pop('name', None)
207 firstname = kwargs.pop('name', None)
208 if firstname:
208 if firstname:
209 kwargs['firstname'] = firstname
209 kwargs['firstname'] = firstname
210
210
211 for k, v in kwargs.items():
211 for k, v in kwargs.items():
212 # skip if we don't want to update this
212 # skip if we don't want to update this
213 if skip_attrs and k in skip_attrs:
213 if skip_attrs and k in skip_attrs:
214 continue
214 continue
215
215
216 user_attrs[k] = v
216 user_attrs[k] = v
217
217
218 try:
218 try:
219 return self.create_or_update(**user_attrs)
219 return self.create_or_update(**user_attrs)
220 except Exception:
220 except Exception:
221 log.error(traceback.format_exc())
221 log.error(traceback.format_exc())
222 raise
222 raise
223
223
224 def create_or_update(
224 def create_or_update(
225 self, username, password, email, firstname='', lastname='',
225 self, username, password, email, firstname='', lastname='',
226 active=True, admin=False, extern_type=None, extern_name=None,
226 active=True, admin=False, extern_type=None, extern_name=None,
227 cur_user=None, plugin=None, force_password_change=False,
227 cur_user=None, plugin=None, force_password_change=False,
228 allow_to_create_user=True, create_repo_group=None,
228 allow_to_create_user=True, create_repo_group=None,
229 updating_user_id=None, language=None, description='',
229 updating_user_id=None, language=None, description='',
230 strict_creation_check=True):
230 strict_creation_check=True):
231 """
231 """
232 Creates a new instance if not found, or updates current one
232 Creates a new instance if not found, or updates current one
233
233
234 :param username:
234 :param username:
235 :param password:
235 :param password:
236 :param email:
236 :param email:
237 :param firstname:
237 :param firstname:
238 :param lastname:
238 :param lastname:
239 :param active:
239 :param active:
240 :param admin:
240 :param admin:
241 :param extern_type:
241 :param extern_type:
242 :param extern_name:
242 :param extern_name:
243 :param cur_user:
243 :param cur_user:
244 :param plugin: optional plugin this method was called from
244 :param plugin: optional plugin this method was called from
245 :param force_password_change: toggles new or existing user flag
245 :param force_password_change: toggles new or existing user flag
246 for password change
246 for password change
247 :param allow_to_create_user: Defines if the method can actually create
247 :param allow_to_create_user: Defines if the method can actually create
248 new users
248 new users
249 :param create_repo_group: Defines if the method should also
249 :param create_repo_group: Defines if the method should also
250 create an repo group with user name, and owner
250 create an repo group with user name, and owner
251 :param updating_user_id: if we set it up this is the user we want to
251 :param updating_user_id: if we set it up this is the user we want to
252 update this allows to editing username.
252 update this allows to editing username.
253 :param language: language of user from interface.
253 :param language: language of user from interface.
254 :param description: user description
254 :param description: user description
255 :param strict_creation_check: checks for allowed creation license wise etc.
255 :param strict_creation_check: checks for allowed creation license wise etc.
256
256
257 :returns: new User object with injected `is_new_user` attribute.
257 :returns: new User object with injected `is_new_user` attribute.
258 """
258 """
259
259
260 if not cur_user:
260 if not cur_user:
261 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
261 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
262
262
263 from rhodecode.lib.auth import (
263 from rhodecode.lib.auth import (
264 get_crypt_password, check_password)
264 get_crypt_password, check_password)
265 from rhodecode.lib import hooks_base
265 from rhodecode.lib import hooks_base
266
266
267 def _password_change(new_user, password):
267 def _password_change(new_user, password):
268 old_password = new_user.password or ''
268 old_password = new_user.password or ''
269 # empty password
269 # empty password
270 if not old_password:
270 if not old_password:
271 return False
271 return False
272
272
273 # password check is only needed for RhodeCode internal auth calls
273 # password check is only needed for RhodeCode internal auth calls
274 # in case it's a plugin we don't care
274 # in case it's a plugin we don't care
275 if not plugin:
275 if not plugin:
276
276
277 # first check if we gave crypted password back, and if it
277 # first check if we gave crypted password back, and if it
278 # matches it's not password change
278 # matches it's not password change
279 if new_user.password == password:
279 if new_user.password == password:
280 return False
280 return False
281
281
282 password_match = check_password(password, old_password)
282 password_match = check_password(password, old_password)
283 if not password_match:
283 if not password_match:
284 return True
284 return True
285
285
286 return False
286 return False
287
287
288 # read settings on default personal repo group creation
288 # read settings on default personal repo group creation
289 if create_repo_group is None:
289 if create_repo_group is None:
290 default_create_repo_group = RepoGroupModel()\
290 default_create_repo_group = RepoGroupModel()\
291 .get_default_create_personal_repo_group()
291 .get_default_create_personal_repo_group()
292 create_repo_group = default_create_repo_group
292 create_repo_group = default_create_repo_group
293
293
294 user_data = {
294 user_data = {
295 'username': username,
295 'username': username,
296 'password': password,
296 'password': password,
297 'email': email,
297 'email': email,
298 'firstname': firstname,
298 'firstname': firstname,
299 'lastname': lastname,
299 'lastname': lastname,
300 'active': active,
300 'active': active,
301 'admin': admin
301 'admin': admin
302 }
302 }
303
303
304 if updating_user_id:
304 if updating_user_id:
305 log.debug('Checking for existing account in RhodeCode '
305 log.debug('Checking for existing account in RhodeCode '
306 'database with user_id `%s` ', updating_user_id)
306 'database with user_id `%s` ', updating_user_id)
307 user = User.get(updating_user_id)
307 user = User.get(updating_user_id)
308 else:
308 else:
309 log.debug('Checking for existing account in RhodeCode '
309 log.debug('Checking for existing account in RhodeCode '
310 'database with username `%s` ', username)
310 'database with username `%s` ', username)
311 user = User.get_by_username(username, case_insensitive=True)
311 user = User.get_by_username(username, case_insensitive=True)
312
312
313 if user is None:
313 if user is None:
314 # we check internal flag if this method is actually allowed to
314 # we check internal flag if this method is actually allowed to
315 # create new user
315 # create new user
316 if not allow_to_create_user:
316 if not allow_to_create_user:
317 msg = ('Method wants to create new user, but it is not '
317 msg = ('Method wants to create new user, but it is not '
318 'allowed to do so')
318 'allowed to do so')
319 log.warning(msg)
319 log.warning(msg)
320 raise NotAllowedToCreateUserError(msg)
320 raise NotAllowedToCreateUserError(msg)
321
321
322 log.debug('Creating new user %s', username)
322 log.debug('Creating new user %s', username)
323
323
324 # only if we create user that is active
324 # only if we create user that is active
325 new_active_user = active
325 new_active_user = active
326 if new_active_user and strict_creation_check:
326 if new_active_user and strict_creation_check:
327 # raises UserCreationError if it's not allowed for any reason to
327 # raises UserCreationError if it's not allowed for any reason to
328 # create new active user, this also executes pre-create hooks
328 # create new active user, this also executes pre-create hooks
329 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
329 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
330 events.trigger(events.UserPreCreate(user_data))
330 events.trigger(events.UserPreCreate(user_data))
331 new_user = User()
331 new_user = User()
332 edit = False
332 edit = False
333 else:
333 else:
334 log.debug('updating user `%s`', username)
334 log.debug('updating user `%s`', username)
335 events.trigger(events.UserPreUpdate(user, user_data))
335 events.trigger(events.UserPreUpdate(user, user_data))
336 new_user = user
336 new_user = user
337 edit = True
337 edit = True
338
338
339 # we're not allowed to edit default user
339 # we're not allowed to edit default user
340 if user.username == User.DEFAULT_USER:
340 if user.username == User.DEFAULT_USER:
341 raise DefaultUserException(
341 raise DefaultUserException(
342 "You can't edit this user (`%(username)s`) since it's "
342 "You can't edit this user (`%(username)s`) since it's "
343 "crucial for entire application"
343 "crucial for entire application"
344 % {'username': user.username})
344 % {'username': user.username})
345
345
346 # inject special attribute that will tell us if User is new or old
346 # inject special attribute that will tell us if User is new or old
347 new_user.is_new_user = not edit
347 new_user.is_new_user = not edit
348 # for users that didn's specify auth type, we use RhodeCode built in
348 # for users that didn's specify auth type, we use RhodeCode built in
349 from rhodecode.authentication.plugins import auth_rhodecode
349 from rhodecode.authentication.plugins import auth_rhodecode
350 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
350 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
351 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
351 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
352
352
353 try:
353 try:
354 new_user.username = username
354 new_user.username = username
355 new_user.admin = admin
355 new_user.admin = admin
356 new_user.email = email
356 new_user.email = email
357 new_user.active = active
357 new_user.active = active
358 new_user.extern_name = safe_unicode(extern_name)
358 new_user.extern_name = safe_unicode(extern_name)
359 new_user.extern_type = safe_unicode(extern_type)
359 new_user.extern_type = safe_unicode(extern_type)
360 new_user.name = firstname
360 new_user.name = firstname
361 new_user.lastname = lastname
361 new_user.lastname = lastname
362 new_user.description = description
362 new_user.description = description
363
363
364 # set password only if creating an user or password is changed
364 # set password only if creating an user or password is changed
365 if not edit or _password_change(new_user, password):
365 if not edit or _password_change(new_user, password):
366 reason = 'new password' if edit else 'new user'
366 reason = 'new password' if edit else 'new user'
367 log.debug('Updating password reason=>%s', reason)
367 log.debug('Updating password reason=>%s', reason)
368 new_user.password = get_crypt_password(password) if password else None
368 new_user.password = get_crypt_password(password) if password else None
369
369
370 if force_password_change:
370 if force_password_change:
371 new_user.update_userdata(force_password_change=True)
371 new_user.update_userdata(force_password_change=True)
372 if language:
372 if language:
373 new_user.update_userdata(language=language)
373 new_user.update_userdata(language=language)
374 new_user.update_userdata(notification_status=True)
374 new_user.update_userdata(notification_status=True)
375
375
376 self.sa.add(new_user)
376 self.sa.add(new_user)
377
377
378 if not edit and create_repo_group:
378 if not edit and create_repo_group:
379 RepoGroupModel().create_personal_repo_group(
379 RepoGroupModel().create_personal_repo_group(
380 new_user, commit_early=False)
380 new_user, commit_early=False)
381
381
382 if not edit:
382 if not edit:
383 # add the RSS token
383 # add the RSS token
384 self.add_auth_token(
384 self.add_auth_token(
385 user=username, lifetime_minutes=-1,
385 user=username, lifetime_minutes=-1,
386 role=self.auth_token_role.ROLE_FEED,
386 role=self.auth_token_role.ROLE_FEED,
387 description=u'Generated feed token')
387 description=u'Generated feed token')
388
388
389 kwargs = new_user.get_dict()
389 kwargs = new_user.get_dict()
390 # backward compat, require api_keys present
390 # backward compat, require api_keys present
391 kwargs['api_keys'] = kwargs['auth_tokens']
391 kwargs['api_keys'] = kwargs['auth_tokens']
392 hooks_base.create_user(created_by=cur_user, **kwargs)
392 hooks_base.create_user(created_by=cur_user, **kwargs)
393 events.trigger(events.UserPostCreate(user_data))
393 events.trigger(events.UserPostCreate(user_data))
394 return new_user
394 return new_user
395 except (DatabaseError,):
395 except (DatabaseError,):
396 log.error(traceback.format_exc())
396 log.error(traceback.format_exc())
397 raise
397 raise
398
398
399 def create_registration(self, form_data,
399 def create_registration(self, form_data,
400 extern_name='rhodecode', extern_type='rhodecode'):
400 extern_name='rhodecode', extern_type='rhodecode'):
401 from rhodecode.model.notification import NotificationModel
401 from rhodecode.model.notification import NotificationModel
402 from rhodecode.model.notification import EmailNotificationModel
402 from rhodecode.model.notification import EmailNotificationModel
403
403
404 try:
404 try:
405 form_data['admin'] = False
405 form_data['admin'] = False
406 form_data['extern_name'] = extern_name
406 form_data['extern_name'] = extern_name
407 form_data['extern_type'] = extern_type
407 form_data['extern_type'] = extern_type
408 new_user = self.create(form_data)
408 new_user = self.create(form_data)
409
409
410 self.sa.add(new_user)
410 self.sa.add(new_user)
411 self.sa.flush()
411 self.sa.flush()
412
412
413 user_data = new_user.get_dict()
413 user_data = new_user.get_dict()
414 user_data.update({
414 user_data.update({
415 'first_name': user_data.get('firstname'),
415 'first_name': user_data.get('firstname'),
416 'last_name': user_data.get('lastname'),
416 'last_name': user_data.get('lastname'),
417 })
417 })
418 kwargs = {
418 kwargs = {
419 # use SQLALCHEMY safe dump of user data
419 # use SQLALCHEMY safe dump of user data
420 'user': AttributeDict(user_data),
420 'user': AttributeDict(user_data),
421 'date': datetime.datetime.now()
421 'date': datetime.datetime.now()
422 }
422 }
423 notification_type = EmailNotificationModel.TYPE_REGISTRATION
423 notification_type = EmailNotificationModel.TYPE_REGISTRATION
424 # pre-generate the subject for notification itself
424 # pre-generate the subject for notification itself
425 (subject,
425 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
426 _h, _e, # we don't care about those
427 body_plaintext) = EmailNotificationModel().render_email(
428 notification_type, **kwargs)
426 notification_type, **kwargs)
429
427
430 # create notification objects, and emails
428 # create notification objects, and emails
431 NotificationModel().create(
429 NotificationModel().create(
432 created_by=new_user,
430 created_by=new_user,
433 notification_subject=subject,
431 notification_subject=subject,
434 notification_body=body_plaintext,
432 notification_body=body_plaintext,
435 notification_type=notification_type,
433 notification_type=notification_type,
436 recipients=None, # all admins
434 recipients=None, # all admins
437 email_kwargs=kwargs,
435 email_kwargs=kwargs,
438 )
436 )
439
437
440 return new_user
438 return new_user
441 except Exception:
439 except Exception:
442 log.error(traceback.format_exc())
440 log.error(traceback.format_exc())
443 raise
441 raise
444
442
445 def _handle_user_repos(self, username, repositories, handle_user,
443 def _handle_user_repos(self, username, repositories, handle_user,
446 handle_mode=None):
444 handle_mode=None):
447
445
448 left_overs = True
446 left_overs = True
449
447
450 from rhodecode.model.repo import RepoModel
448 from rhodecode.model.repo import RepoModel
451
449
452 if handle_mode == 'detach':
450 if handle_mode == 'detach':
453 for obj in repositories:
451 for obj in repositories:
454 obj.user = handle_user
452 obj.user = handle_user
455 # set description we know why we super admin now owns
453 # set description we know why we super admin now owns
456 # additional repositories that were orphaned !
454 # additional repositories that were orphaned !
457 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
455 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
458 self.sa.add(obj)
456 self.sa.add(obj)
459 left_overs = False
457 left_overs = False
460 elif handle_mode == 'delete':
458 elif handle_mode == 'delete':
461 for obj in repositories:
459 for obj in repositories:
462 RepoModel().delete(obj, forks='detach')
460 RepoModel().delete(obj, forks='detach')
463 left_overs = False
461 left_overs = False
464
462
465 # if nothing is done we have left overs left
463 # if nothing is done we have left overs left
466 return left_overs
464 return left_overs
467
465
468 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
466 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
469 handle_mode=None):
467 handle_mode=None):
470
468
471 left_overs = True
469 left_overs = True
472
470
473 from rhodecode.model.repo_group import RepoGroupModel
471 from rhodecode.model.repo_group import RepoGroupModel
474
472
475 if handle_mode == 'detach':
473 if handle_mode == 'detach':
476 for r in repository_groups:
474 for r in repository_groups:
477 r.user = handle_user
475 r.user = handle_user
478 # set description we know why we super admin now owns
476 # set description we know why we super admin now owns
479 # additional repositories that were orphaned !
477 # additional repositories that were orphaned !
480 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
478 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
481 r.personal = False
479 r.personal = False
482 self.sa.add(r)
480 self.sa.add(r)
483 left_overs = False
481 left_overs = False
484 elif handle_mode == 'delete':
482 elif handle_mode == 'delete':
485 for r in repository_groups:
483 for r in repository_groups:
486 RepoGroupModel().delete(r)
484 RepoGroupModel().delete(r)
487 left_overs = False
485 left_overs = False
488
486
489 # if nothing is done we have left overs left
487 # if nothing is done we have left overs left
490 return left_overs
488 return left_overs
491
489
492 def _handle_user_user_groups(self, username, user_groups, handle_user,
490 def _handle_user_user_groups(self, username, user_groups, handle_user,
493 handle_mode=None):
491 handle_mode=None):
494
492
495 left_overs = True
493 left_overs = True
496
494
497 from rhodecode.model.user_group import UserGroupModel
495 from rhodecode.model.user_group import UserGroupModel
498
496
499 if handle_mode == 'detach':
497 if handle_mode == 'detach':
500 for r in user_groups:
498 for r in user_groups:
501 for user_user_group_to_perm in r.user_user_group_to_perm:
499 for user_user_group_to_perm in r.user_user_group_to_perm:
502 if user_user_group_to_perm.user.username == username:
500 if user_user_group_to_perm.user.username == username:
503 user_user_group_to_perm.user = handle_user
501 user_user_group_to_perm.user = handle_user
504 r.user = handle_user
502 r.user = handle_user
505 # set description we know why we super admin now owns
503 # set description we know why we super admin now owns
506 # additional repositories that were orphaned !
504 # additional repositories that were orphaned !
507 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
505 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
508 self.sa.add(r)
506 self.sa.add(r)
509 left_overs = False
507 left_overs = False
510 elif handle_mode == 'delete':
508 elif handle_mode == 'delete':
511 for r in user_groups:
509 for r in user_groups:
512 UserGroupModel().delete(r)
510 UserGroupModel().delete(r)
513 left_overs = False
511 left_overs = False
514
512
515 # if nothing is done we have left overs left
513 # if nothing is done we have left overs left
516 return left_overs
514 return left_overs
517
515
518 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
516 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
519 handle_mode=None):
517 handle_mode=None):
520 left_overs = True
518 left_overs = True
521
519
522 from rhodecode.model.pull_request import PullRequestModel
520 from rhodecode.model.pull_request import PullRequestModel
523
521
524 if handle_mode == 'detach':
522 if handle_mode == 'detach':
525 for pr in pull_requests:
523 for pr in pull_requests:
526 pr.user_id = handle_user.user_id
524 pr.user_id = handle_user.user_id
527 # set description we know why we super admin now owns
525 # set description we know why we super admin now owns
528 # additional repositories that were orphaned !
526 # additional repositories that were orphaned !
529 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
527 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
530 self.sa.add(pr)
528 self.sa.add(pr)
531 left_overs = False
529 left_overs = False
532 elif handle_mode == 'delete':
530 elif handle_mode == 'delete':
533 for pr in pull_requests:
531 for pr in pull_requests:
534 PullRequestModel().delete(pr)
532 PullRequestModel().delete(pr)
535
533
536 left_overs = False
534 left_overs = False
537
535
538 # if nothing is done we have left overs left
536 # if nothing is done we have left overs left
539 return left_overs
537 return left_overs
540
538
541 def _handle_user_artifacts(self, username, artifacts, handle_user,
539 def _handle_user_artifacts(self, username, artifacts, handle_user,
542 handle_mode=None):
540 handle_mode=None):
543
541
544 left_overs = True
542 left_overs = True
545
543
546 if handle_mode == 'detach':
544 if handle_mode == 'detach':
547 for a in artifacts:
545 for a in artifacts:
548 a.upload_user = handle_user
546 a.upload_user = handle_user
549 # set description we know why we super admin now owns
547 # set description we know why we super admin now owns
550 # additional artifacts that were orphaned !
548 # additional artifacts that were orphaned !
551 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
549 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
552 self.sa.add(a)
550 self.sa.add(a)
553 left_overs = False
551 left_overs = False
554 elif handle_mode == 'delete':
552 elif handle_mode == 'delete':
555 from rhodecode.apps.file_store import utils as store_utils
553 from rhodecode.apps.file_store import utils as store_utils
556 request = get_current_request()
554 request = get_current_request()
557 storage = store_utils.get_file_storage(request.registry.settings)
555 storage = store_utils.get_file_storage(request.registry.settings)
558 for a in artifacts:
556 for a in artifacts:
559 file_uid = a.file_uid
557 file_uid = a.file_uid
560 storage.delete(file_uid)
558 storage.delete(file_uid)
561 self.sa.delete(a)
559 self.sa.delete(a)
562
560
563 left_overs = False
561 left_overs = False
564
562
565 # if nothing is done we have left overs left
563 # if nothing is done we have left overs left
566 return left_overs
564 return left_overs
567
565
568 def delete(self, user, cur_user=None, handle_repos=None,
566 def delete(self, user, cur_user=None, handle_repos=None,
569 handle_repo_groups=None, handle_user_groups=None,
567 handle_repo_groups=None, handle_user_groups=None,
570 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
568 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
571 from rhodecode.lib import hooks_base
569 from rhodecode.lib import hooks_base
572
570
573 if not cur_user:
571 if not cur_user:
574 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
572 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
575
573
576 user = self._get_user(user)
574 user = self._get_user(user)
577
575
578 try:
576 try:
579 if user.username == User.DEFAULT_USER:
577 if user.username == User.DEFAULT_USER:
580 raise DefaultUserException(
578 raise DefaultUserException(
581 u"You can't remove this user since it's"
579 u"You can't remove this user since it's"
582 u" crucial for entire application")
580 u" crucial for entire application")
583 handle_user = handle_new_owner or self.cls.get_first_super_admin()
581 handle_user = handle_new_owner or self.cls.get_first_super_admin()
584 log.debug('New detached objects owner %s', handle_user)
582 log.debug('New detached objects owner %s', handle_user)
585
583
586 left_overs = self._handle_user_repos(
584 left_overs = self._handle_user_repos(
587 user.username, user.repositories, handle_user, handle_repos)
585 user.username, user.repositories, handle_user, handle_repos)
588 if left_overs and user.repositories:
586 if left_overs and user.repositories:
589 repos = [x.repo_name for x in user.repositories]
587 repos = [x.repo_name for x in user.repositories]
590 raise UserOwnsReposException(
588 raise UserOwnsReposException(
591 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
589 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
592 u'removed. Switch owners or remove those repositories:%(list_repos)s'
590 u'removed. Switch owners or remove those repositories:%(list_repos)s'
593 % {'username': user.username, 'len_repos': len(repos),
591 % {'username': user.username, 'len_repos': len(repos),
594 'list_repos': ', '.join(repos)})
592 'list_repos': ', '.join(repos)})
595
593
596 left_overs = self._handle_user_repo_groups(
594 left_overs = self._handle_user_repo_groups(
597 user.username, user.repository_groups, handle_user, handle_repo_groups)
595 user.username, user.repository_groups, handle_user, handle_repo_groups)
598 if left_overs and user.repository_groups:
596 if left_overs and user.repository_groups:
599 repo_groups = [x.group_name for x in user.repository_groups]
597 repo_groups = [x.group_name for x in user.repository_groups]
600 raise UserOwnsRepoGroupsException(
598 raise UserOwnsRepoGroupsException(
601 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
599 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
602 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
600 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
603 % {'username': user.username, 'len_repo_groups': len(repo_groups),
601 % {'username': user.username, 'len_repo_groups': len(repo_groups),
604 'list_repo_groups': ', '.join(repo_groups)})
602 'list_repo_groups': ', '.join(repo_groups)})
605
603
606 left_overs = self._handle_user_user_groups(
604 left_overs = self._handle_user_user_groups(
607 user.username, user.user_groups, handle_user, handle_user_groups)
605 user.username, user.user_groups, handle_user, handle_user_groups)
608 if left_overs and user.user_groups:
606 if left_overs and user.user_groups:
609 user_groups = [x.users_group_name for x in user.user_groups]
607 user_groups = [x.users_group_name for x in user.user_groups]
610 raise UserOwnsUserGroupsException(
608 raise UserOwnsUserGroupsException(
611 u'user "%s" still owns %s user groups and cannot be '
609 u'user "%s" still owns %s user groups and cannot be '
612 u'removed. Switch owners or remove those user groups:%s'
610 u'removed. Switch owners or remove those user groups:%s'
613 % (user.username, len(user_groups), ', '.join(user_groups)))
611 % (user.username, len(user_groups), ', '.join(user_groups)))
614
612
615 left_overs = self._handle_user_pull_requests(
613 left_overs = self._handle_user_pull_requests(
616 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
614 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
617 if left_overs and user.user_pull_requests:
615 if left_overs and user.user_pull_requests:
618 pull_requests = ['!{}'.format(x.pull_request_id) for x in user.user_pull_requests]
616 pull_requests = ['!{}'.format(x.pull_request_id) for x in user.user_pull_requests]
619 raise UserOwnsPullRequestsException(
617 raise UserOwnsPullRequestsException(
620 u'user "%s" still owns %s pull requests and cannot be '
618 u'user "%s" still owns %s pull requests and cannot be '
621 u'removed. Switch owners or remove those pull requests:%s'
619 u'removed. Switch owners or remove those pull requests:%s'
622 % (user.username, len(pull_requests), ', '.join(pull_requests)))
620 % (user.username, len(pull_requests), ', '.join(pull_requests)))
623
621
624 left_overs = self._handle_user_artifacts(
622 left_overs = self._handle_user_artifacts(
625 user.username, user.artifacts, handle_user, handle_artifacts)
623 user.username, user.artifacts, handle_user, handle_artifacts)
626 if left_overs and user.artifacts:
624 if left_overs and user.artifacts:
627 artifacts = [x.file_uid for x in user.artifacts]
625 artifacts = [x.file_uid for x in user.artifacts]
628 raise UserOwnsArtifactsException(
626 raise UserOwnsArtifactsException(
629 u'user "%s" still owns %s artifacts and cannot be '
627 u'user "%s" still owns %s artifacts and cannot be '
630 u'removed. Switch owners or remove those artifacts:%s'
628 u'removed. Switch owners or remove those artifacts:%s'
631 % (user.username, len(artifacts), ', '.join(artifacts)))
629 % (user.username, len(artifacts), ', '.join(artifacts)))
632
630
633 user_data = user.get_dict() # fetch user data before expire
631 user_data = user.get_dict() # fetch user data before expire
634
632
635 # we might change the user data with detach/delete, make sure
633 # we might change the user data with detach/delete, make sure
636 # the object is marked as expired before actually deleting !
634 # the object is marked as expired before actually deleting !
637 self.sa.expire(user)
635 self.sa.expire(user)
638 self.sa.delete(user)
636 self.sa.delete(user)
639
637
640 hooks_base.delete_user(deleted_by=cur_user, **user_data)
638 hooks_base.delete_user(deleted_by=cur_user, **user_data)
641 except Exception:
639 except Exception:
642 log.error(traceback.format_exc())
640 log.error(traceback.format_exc())
643 raise
641 raise
644
642
645 def reset_password_link(self, data, pwd_reset_url):
643 def reset_password_link(self, data, pwd_reset_url):
646 from rhodecode.lib.celerylib import tasks, run_task
644 from rhodecode.lib.celerylib import tasks, run_task
647 from rhodecode.model.notification import EmailNotificationModel
645 from rhodecode.model.notification import EmailNotificationModel
648 user_email = data['email']
646 user_email = data['email']
649 try:
647 try:
650 user = User.get_by_email(user_email)
648 user = User.get_by_email(user_email)
651 if user:
649 if user:
652 log.debug('password reset user found %s', user)
650 log.debug('password reset user found %s', user)
653
651
654 email_kwargs = {
652 email_kwargs = {
655 'password_reset_url': pwd_reset_url,
653 'password_reset_url': pwd_reset_url,
656 'user': user,
654 'user': user,
657 'email': user_email,
655 'email': user_email,
658 'date': datetime.datetime.now(),
656 'date': datetime.datetime.now(),
659 'first_admin_email': User.get_first_super_admin().email
657 'first_admin_email': User.get_first_super_admin().email
660 }
658 }
661
659
662 (subject, headers, email_body,
660 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
663 email_body_plaintext) = EmailNotificationModel().render_email(
664 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
661 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
665
662
666 recipients = [user_email]
663 recipients = [user_email]
667
664
668 action_logger_generic(
665 action_logger_generic(
669 'sending password reset email to user: {}'.format(
666 'sending password reset email to user: {}'.format(
670 user), namespace='security.password_reset')
667 user), namespace='security.password_reset')
671
668
672 run_task(tasks.send_email, recipients, subject,
669 run_task(tasks.send_email, recipients, subject,
673 email_body_plaintext, email_body)
670 email_body_plaintext, email_body)
674
671
675 else:
672 else:
676 log.debug("password reset email %s not found", user_email)
673 log.debug("password reset email %s not found", user_email)
677 except Exception:
674 except Exception:
678 log.error(traceback.format_exc())
675 log.error(traceback.format_exc())
679 return False
676 return False
680
677
681 return True
678 return True
682
679
683 def reset_password(self, data):
680 def reset_password(self, data):
684 from rhodecode.lib.celerylib import tasks, run_task
681 from rhodecode.lib.celerylib import tasks, run_task
685 from rhodecode.model.notification import EmailNotificationModel
682 from rhodecode.model.notification import EmailNotificationModel
686 from rhodecode.lib import auth
683 from rhodecode.lib import auth
687 user_email = data['email']
684 user_email = data['email']
688 pre_db = True
685 pre_db = True
689 try:
686 try:
690 user = User.get_by_email(user_email)
687 user = User.get_by_email(user_email)
691 new_passwd = auth.PasswordGenerator().gen_password(
688 new_passwd = auth.PasswordGenerator().gen_password(
692 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
689 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
693 if user:
690 if user:
694 user.password = auth.get_crypt_password(new_passwd)
691 user.password = auth.get_crypt_password(new_passwd)
695 # also force this user to reset his password !
692 # also force this user to reset his password !
696 user.update_userdata(force_password_change=True)
693 user.update_userdata(force_password_change=True)
697
694
698 Session().add(user)
695 Session().add(user)
699
696
700 # now delete the token in question
697 # now delete the token in question
701 UserApiKeys = AuthTokenModel.cls
698 UserApiKeys = AuthTokenModel.cls
702 UserApiKeys().query().filter(
699 UserApiKeys().query().filter(
703 UserApiKeys.api_key == data['token']).delete()
700 UserApiKeys.api_key == data['token']).delete()
704
701
705 Session().commit()
702 Session().commit()
706 log.info('successfully reset password for `%s`', user_email)
703 log.info('successfully reset password for `%s`', user_email)
707
704
708 if new_passwd is None:
705 if new_passwd is None:
709 raise Exception('unable to generate new password')
706 raise Exception('unable to generate new password')
710
707
711 pre_db = False
708 pre_db = False
712
709
713 email_kwargs = {
710 email_kwargs = {
714 'new_password': new_passwd,
711 'new_password': new_passwd,
715 'user': user,
712 'user': user,
716 'email': user_email,
713 'email': user_email,
717 'date': datetime.datetime.now(),
714 'date': datetime.datetime.now(),
718 'first_admin_email': User.get_first_super_admin().email
715 'first_admin_email': User.get_first_super_admin().email
719 }
716 }
720
717
721 (subject, headers, email_body,
718 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
722 email_body_plaintext) = EmailNotificationModel().render_email(
723 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
719 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
724 **email_kwargs)
720 **email_kwargs)
725
721
726 recipients = [user_email]
722 recipients = [user_email]
727
723
728 action_logger_generic(
724 action_logger_generic(
729 'sent new password to user: {} with email: {}'.format(
725 'sent new password to user: {} with email: {}'.format(
730 user, user_email), namespace='security.password_reset')
726 user, user_email), namespace='security.password_reset')
731
727
732 run_task(tasks.send_email, recipients, subject,
728 run_task(tasks.send_email, recipients, subject,
733 email_body_plaintext, email_body)
729 email_body_plaintext, email_body)
734
730
735 except Exception:
731 except Exception:
736 log.error('Failed to update user password')
732 log.error('Failed to update user password')
737 log.error(traceback.format_exc())
733 log.error(traceback.format_exc())
738 if pre_db:
734 if pre_db:
739 # we rollback only if local db stuff fails. If it goes into
735 # we rollback only if local db stuff fails. If it goes into
740 # run_task, we're pass rollback state this wouldn't work then
736 # run_task, we're pass rollback state this wouldn't work then
741 Session().rollback()
737 Session().rollback()
742
738
743 return True
739 return True
744
740
745 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
741 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
746 """
742 """
747 Fetches auth_user by user_id,or api_key if present.
743 Fetches auth_user by user_id,or api_key if present.
748 Fills auth_user attributes with those taken from database.
744 Fills auth_user attributes with those taken from database.
749 Additionally set's is_authenitated if lookup fails
745 Additionally set's is_authenitated if lookup fails
750 present in database
746 present in database
751
747
752 :param auth_user: instance of user to set attributes
748 :param auth_user: instance of user to set attributes
753 :param user_id: user id to fetch by
749 :param user_id: user id to fetch by
754 :param api_key: api key to fetch by
750 :param api_key: api key to fetch by
755 :param username: username to fetch by
751 :param username: username to fetch by
756 """
752 """
757 def token_obfuscate(token):
753 def token_obfuscate(token):
758 if token:
754 if token:
759 return token[:4] + "****"
755 return token[:4] + "****"
760
756
761 if user_id is None and api_key is None and username is None:
757 if user_id is None and api_key is None and username is None:
762 raise Exception('You need to pass user_id, api_key or username')
758 raise Exception('You need to pass user_id, api_key or username')
763
759
764 log.debug(
760 log.debug(
765 'AuthUser: fill data execution based on: '
761 'AuthUser: fill data execution based on: '
766 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
762 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
767 try:
763 try:
768 dbuser = None
764 dbuser = None
769 if user_id:
765 if user_id:
770 dbuser = self.get(user_id)
766 dbuser = self.get(user_id)
771 elif api_key:
767 elif api_key:
772 dbuser = self.get_by_auth_token(api_key)
768 dbuser = self.get_by_auth_token(api_key)
773 elif username:
769 elif username:
774 dbuser = self.get_by_username(username)
770 dbuser = self.get_by_username(username)
775
771
776 if not dbuser:
772 if not dbuser:
777 log.warning(
773 log.warning(
778 'Unable to lookup user by id:%s api_key:%s username:%s',
774 'Unable to lookup user by id:%s api_key:%s username:%s',
779 user_id, token_obfuscate(api_key), username)
775 user_id, token_obfuscate(api_key), username)
780 return False
776 return False
781 if not dbuser.active:
777 if not dbuser.active:
782 log.debug('User `%s:%s` is inactive, skipping fill data',
778 log.debug('User `%s:%s` is inactive, skipping fill data',
783 username, user_id)
779 username, user_id)
784 return False
780 return False
785
781
786 log.debug('AuthUser: filling found user:%s data', dbuser)
782 log.debug('AuthUser: filling found user:%s data', dbuser)
787
783
788 attrs = {
784 attrs = {
789 'user_id': dbuser.user_id,
785 'user_id': dbuser.user_id,
790 'username': dbuser.username,
786 'username': dbuser.username,
791 'name': dbuser.name,
787 'name': dbuser.name,
792 'first_name': dbuser.first_name,
788 'first_name': dbuser.first_name,
793 'firstname': dbuser.firstname,
789 'firstname': dbuser.firstname,
794 'last_name': dbuser.last_name,
790 'last_name': dbuser.last_name,
795 'lastname': dbuser.lastname,
791 'lastname': dbuser.lastname,
796 'admin': dbuser.admin,
792 'admin': dbuser.admin,
797 'active': dbuser.active,
793 'active': dbuser.active,
798
794
799 'email': dbuser.email,
795 'email': dbuser.email,
800 'emails': dbuser.emails_cached(),
796 'emails': dbuser.emails_cached(),
801 'short_contact': dbuser.short_contact,
797 'short_contact': dbuser.short_contact,
802 'full_contact': dbuser.full_contact,
798 'full_contact': dbuser.full_contact,
803 'full_name': dbuser.full_name,
799 'full_name': dbuser.full_name,
804 'full_name_or_username': dbuser.full_name_or_username,
800 'full_name_or_username': dbuser.full_name_or_username,
805
801
806 '_api_key': dbuser._api_key,
802 '_api_key': dbuser._api_key,
807 '_user_data': dbuser._user_data,
803 '_user_data': dbuser._user_data,
808
804
809 'created_on': dbuser.created_on,
805 'created_on': dbuser.created_on,
810 'extern_name': dbuser.extern_name,
806 'extern_name': dbuser.extern_name,
811 'extern_type': dbuser.extern_type,
807 'extern_type': dbuser.extern_type,
812
808
813 'inherit_default_permissions': dbuser.inherit_default_permissions,
809 'inherit_default_permissions': dbuser.inherit_default_permissions,
814
810
815 'language': dbuser.language,
811 'language': dbuser.language,
816 'last_activity': dbuser.last_activity,
812 'last_activity': dbuser.last_activity,
817 'last_login': dbuser.last_login,
813 'last_login': dbuser.last_login,
818 'password': dbuser.password,
814 'password': dbuser.password,
819 }
815 }
820 auth_user.__dict__.update(attrs)
816 auth_user.__dict__.update(attrs)
821 except Exception:
817 except Exception:
822 log.error(traceback.format_exc())
818 log.error(traceback.format_exc())
823 auth_user.is_authenticated = False
819 auth_user.is_authenticated = False
824 return False
820 return False
825
821
826 return True
822 return True
827
823
828 def has_perm(self, user, perm):
824 def has_perm(self, user, perm):
829 perm = self._get_perm(perm)
825 perm = self._get_perm(perm)
830 user = self._get_user(user)
826 user = self._get_user(user)
831
827
832 return UserToPerm.query().filter(UserToPerm.user == user)\
828 return UserToPerm.query().filter(UserToPerm.user == user)\
833 .filter(UserToPerm.permission == perm).scalar() is not None
829 .filter(UserToPerm.permission == perm).scalar() is not None
834
830
835 def grant_perm(self, user, perm):
831 def grant_perm(self, user, perm):
836 """
832 """
837 Grant user global permissions
833 Grant user global permissions
838
834
839 :param user:
835 :param user:
840 :param perm:
836 :param perm:
841 """
837 """
842 user = self._get_user(user)
838 user = self._get_user(user)
843 perm = self._get_perm(perm)
839 perm = self._get_perm(perm)
844 # if this permission is already granted skip it
840 # if this permission is already granted skip it
845 _perm = UserToPerm.query()\
841 _perm = UserToPerm.query()\
846 .filter(UserToPerm.user == user)\
842 .filter(UserToPerm.user == user)\
847 .filter(UserToPerm.permission == perm)\
843 .filter(UserToPerm.permission == perm)\
848 .scalar()
844 .scalar()
849 if _perm:
845 if _perm:
850 return
846 return
851 new = UserToPerm()
847 new = UserToPerm()
852 new.user = user
848 new.user = user
853 new.permission = perm
849 new.permission = perm
854 self.sa.add(new)
850 self.sa.add(new)
855 return new
851 return new
856
852
857 def revoke_perm(self, user, perm):
853 def revoke_perm(self, user, perm):
858 """
854 """
859 Revoke users global permissions
855 Revoke users global permissions
860
856
861 :param user:
857 :param user:
862 :param perm:
858 :param perm:
863 """
859 """
864 user = self._get_user(user)
860 user = self._get_user(user)
865 perm = self._get_perm(perm)
861 perm = self._get_perm(perm)
866
862
867 obj = UserToPerm.query()\
863 obj = UserToPerm.query()\
868 .filter(UserToPerm.user == user)\
864 .filter(UserToPerm.user == user)\
869 .filter(UserToPerm.permission == perm)\
865 .filter(UserToPerm.permission == perm)\
870 .scalar()
866 .scalar()
871 if obj:
867 if obj:
872 self.sa.delete(obj)
868 self.sa.delete(obj)
873
869
874 def add_extra_email(self, user, email):
870 def add_extra_email(self, user, email):
875 """
871 """
876 Adds email address to UserEmailMap
872 Adds email address to UserEmailMap
877
873
878 :param user:
874 :param user:
879 :param email:
875 :param email:
880 """
876 """
881
877
882 user = self._get_user(user)
878 user = self._get_user(user)
883
879
884 obj = UserEmailMap()
880 obj = UserEmailMap()
885 obj.user = user
881 obj.user = user
886 obj.email = email
882 obj.email = email
887 self.sa.add(obj)
883 self.sa.add(obj)
888 return obj
884 return obj
889
885
890 def delete_extra_email(self, user, email_id):
886 def delete_extra_email(self, user, email_id):
891 """
887 """
892 Removes email address from UserEmailMap
888 Removes email address from UserEmailMap
893
889
894 :param user:
890 :param user:
895 :param email_id:
891 :param email_id:
896 """
892 """
897 user = self._get_user(user)
893 user = self._get_user(user)
898 obj = UserEmailMap.query().get(email_id)
894 obj = UserEmailMap.query().get(email_id)
899 if obj and obj.user_id == user.user_id:
895 if obj and obj.user_id == user.user_id:
900 self.sa.delete(obj)
896 self.sa.delete(obj)
901
897
902 def parse_ip_range(self, ip_range):
898 def parse_ip_range(self, ip_range):
903 ip_list = []
899 ip_list = []
904
900
905 def make_unique(value):
901 def make_unique(value):
906 seen = []
902 seen = []
907 return [c for c in value if not (c in seen or seen.append(c))]
903 return [c for c in value if not (c in seen or seen.append(c))]
908
904
909 # firsts split by commas
905 # firsts split by commas
910 for ip_range in ip_range.split(','):
906 for ip_range in ip_range.split(','):
911 if not ip_range:
907 if not ip_range:
912 continue
908 continue
913 ip_range = ip_range.strip()
909 ip_range = ip_range.strip()
914 if '-' in ip_range:
910 if '-' in ip_range:
915 start_ip, end_ip = ip_range.split('-', 1)
911 start_ip, end_ip = ip_range.split('-', 1)
916 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
912 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
917 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
913 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
918 parsed_ip_range = []
914 parsed_ip_range = []
919
915
920 for index in range(int(start_ip), int(end_ip) + 1):
916 for index in range(int(start_ip), int(end_ip) + 1):
921 new_ip = ipaddress.ip_address(index)
917 new_ip = ipaddress.ip_address(index)
922 parsed_ip_range.append(str(new_ip))
918 parsed_ip_range.append(str(new_ip))
923 ip_list.extend(parsed_ip_range)
919 ip_list.extend(parsed_ip_range)
924 else:
920 else:
925 ip_list.append(ip_range)
921 ip_list.append(ip_range)
926
922
927 return make_unique(ip_list)
923 return make_unique(ip_list)
928
924
929 def add_extra_ip(self, user, ip, description=None):
925 def add_extra_ip(self, user, ip, description=None):
930 """
926 """
931 Adds ip address to UserIpMap
927 Adds ip address to UserIpMap
932
928
933 :param user:
929 :param user:
934 :param ip:
930 :param ip:
935 """
931 """
936
932
937 user = self._get_user(user)
933 user = self._get_user(user)
938 obj = UserIpMap()
934 obj = UserIpMap()
939 obj.user = user
935 obj.user = user
940 obj.ip_addr = ip
936 obj.ip_addr = ip
941 obj.description = description
937 obj.description = description
942 self.sa.add(obj)
938 self.sa.add(obj)
943 return obj
939 return obj
944
940
945 auth_token_role = AuthTokenModel.cls
941 auth_token_role = AuthTokenModel.cls
946
942
947 def add_auth_token(self, user, lifetime_minutes, role, description=u'',
943 def add_auth_token(self, user, lifetime_minutes, role, description=u'',
948 scope_callback=None):
944 scope_callback=None):
949 """
945 """
950 Add AuthToken for user.
946 Add AuthToken for user.
951
947
952 :param user: username/user_id
948 :param user: username/user_id
953 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
949 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
954 :param role: one of AuthTokenModel.cls.ROLE_*
950 :param role: one of AuthTokenModel.cls.ROLE_*
955 :param description: optional string description
951 :param description: optional string description
956 """
952 """
957
953
958 token = AuthTokenModel().create(
954 token = AuthTokenModel().create(
959 user, description, lifetime_minutes, role)
955 user, description, lifetime_minutes, role)
960 if scope_callback and callable(scope_callback):
956 if scope_callback and callable(scope_callback):
961 # call the callback if we provide, used to attach scope for EE edition
957 # call the callback if we provide, used to attach scope for EE edition
962 scope_callback(token)
958 scope_callback(token)
963 return token
959 return token
964
960
965 def delete_extra_ip(self, user, ip_id):
961 def delete_extra_ip(self, user, ip_id):
966 """
962 """
967 Removes ip address from UserIpMap
963 Removes ip address from UserIpMap
968
964
969 :param user:
965 :param user:
970 :param ip_id:
966 :param ip_id:
971 """
967 """
972 user = self._get_user(user)
968 user = self._get_user(user)
973 obj = UserIpMap.query().get(ip_id)
969 obj = UserIpMap.query().get(ip_id)
974 if obj and obj.user_id == user.user_id:
970 if obj and obj.user_id == user.user_id:
975 self.sa.delete(obj)
971 self.sa.delete(obj)
976
972
977 def get_accounts_in_creation_order(self, current_user=None):
973 def get_accounts_in_creation_order(self, current_user=None):
978 """
974 """
979 Get accounts in order of creation for deactivation for license limits
975 Get accounts in order of creation for deactivation for license limits
980
976
981 pick currently logged in user, and append to the list in position 0
977 pick currently logged in user, and append to the list in position 0
982 pick all super-admins in order of creation date and add it to the list
978 pick all super-admins in order of creation date and add it to the list
983 pick all other accounts in order of creation and add it to the list.
979 pick all other accounts in order of creation and add it to the list.
984
980
985 Based on that list, the last accounts can be disabled as they are
981 Based on that list, the last accounts can be disabled as they are
986 created at the end and don't include any of the super admins as well
982 created at the end and don't include any of the super admins as well
987 as the current user.
983 as the current user.
988
984
989 :param current_user: optionally current user running this operation
985 :param current_user: optionally current user running this operation
990 """
986 """
991
987
992 if not current_user:
988 if not current_user:
993 current_user = get_current_rhodecode_user()
989 current_user = get_current_rhodecode_user()
994 active_super_admins = [
990 active_super_admins = [
995 x.user_id for x in User.query()
991 x.user_id for x in User.query()
996 .filter(User.user_id != current_user.user_id)
992 .filter(User.user_id != current_user.user_id)
997 .filter(User.active == true())
993 .filter(User.active == true())
998 .filter(User.admin == true())
994 .filter(User.admin == true())
999 .order_by(User.created_on.asc())]
995 .order_by(User.created_on.asc())]
1000
996
1001 active_regular_users = [
997 active_regular_users = [
1002 x.user_id for x in User.query()
998 x.user_id for x in User.query()
1003 .filter(User.user_id != current_user.user_id)
999 .filter(User.user_id != current_user.user_id)
1004 .filter(User.active == true())
1000 .filter(User.active == true())
1005 .filter(User.admin == false())
1001 .filter(User.admin == false())
1006 .order_by(User.created_on.asc())]
1002 .order_by(User.created_on.asc())]
1007
1003
1008 list_of_accounts = [current_user.user_id]
1004 list_of_accounts = [current_user.user_id]
1009 list_of_accounts += active_super_admins
1005 list_of_accounts += active_super_admins
1010 list_of_accounts += active_regular_users
1006 list_of_accounts += active_regular_users
1011
1007
1012 return list_of_accounts
1008 return list_of_accounts
1013
1009
1014 def deactivate_last_users(self, expected_users, current_user=None):
1010 def deactivate_last_users(self, expected_users, current_user=None):
1015 """
1011 """
1016 Deactivate accounts that are over the license limits.
1012 Deactivate accounts that are over the license limits.
1017 Algorithm of which accounts to disabled is based on the formula:
1013 Algorithm of which accounts to disabled is based on the formula:
1018
1014
1019 Get current user, then super admins in creation order, then regular
1015 Get current user, then super admins in creation order, then regular
1020 active users in creation order.
1016 active users in creation order.
1021
1017
1022 Using that list we mark all accounts from the end of it as inactive.
1018 Using that list we mark all accounts from the end of it as inactive.
1023 This way we block only latest created accounts.
1019 This way we block only latest created accounts.
1024
1020
1025 :param expected_users: list of users in special order, we deactivate
1021 :param expected_users: list of users in special order, we deactivate
1026 the end N amount of users from that list
1022 the end N amount of users from that list
1027 """
1023 """
1028
1024
1029 list_of_accounts = self.get_accounts_in_creation_order(
1025 list_of_accounts = self.get_accounts_in_creation_order(
1030 current_user=current_user)
1026 current_user=current_user)
1031
1027
1032 for acc_id in list_of_accounts[expected_users + 1:]:
1028 for acc_id in list_of_accounts[expected_users + 1:]:
1033 user = User.get(acc_id)
1029 user = User.get(acc_id)
1034 log.info('Deactivating account %s for license unlock', user)
1030 log.info('Deactivating account %s for license unlock', user)
1035 user.active = False
1031 user.active = False
1036 Session().add(user)
1032 Session().add(user)
1037 Session().commit()
1033 Session().commit()
1038
1034
1039 return
1035 return
1040
1036
1041 def get_user_log(self, user, filter_term):
1037 def get_user_log(self, user, filter_term):
1042 user_log = UserLog.query()\
1038 user_log = UserLog.query()\
1043 .filter(or_(UserLog.user_id == user.user_id,
1039 .filter(or_(UserLog.user_id == user.user_id,
1044 UserLog.username == user.username))\
1040 UserLog.username == user.username))\
1045 .options(joinedload(UserLog.user))\
1041 .options(joinedload(UserLog.user))\
1046 .options(joinedload(UserLog.repository))\
1042 .options(joinedload(UserLog.repository))\
1047 .order_by(UserLog.action_date.desc())
1043 .order_by(UserLog.action_date.desc())
1048
1044
1049 user_log = user_log_filter(user_log, filter_term)
1045 user_log = user_log_filter(user_log, filter_term)
1050 return user_log
1046 return user_log
@@ -1,29 +1,24 b''
1 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
1 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
2
2
3 <html>
3 <html>
4 <head></head>
4 <head></head>
5
5
6 <body>
6 <body>
7
7
8 SUBJECT:
8 SUBJECT:
9 <pre>${c.subject}</pre>
9 <pre>${c.subject}</pre>
10
10
11 HEADERS:
12 <pre>
13 ${c.headers}
14 </pre>
15
16 PLAINTEXT:
11 PLAINTEXT:
17 <pre>
12 <pre>
18 ${c.email_body_plaintext|n}
13 ${c.email_body_plaintext|n}
19 </pre>
14 </pre>
20
15
21 </body>
16 </body>
22 </html>
17 </html>
23 <br/><br/>
18 <br/><br/>
24
19
25 HTML:
20 HTML:
26
21
27 ${c.email_body|n}
22 ${c.email_body|n}
28
23
29
24
@@ -1,642 +1,639 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 ## helpers
3 ## helpers
4 <%def name="tag_button(text, tag_type=None)">
4 <%def name="tag_button(text, tag_type=None)">
5 <%
5 <%
6 color_scheme = {
6 color_scheme = {
7 'default': 'border:1px solid #979797;color:#666666;background-color:#f9f9f9',
7 'default': 'border:1px solid #979797;color:#666666;background-color:#f9f9f9',
8 'approved': 'border:1px solid #0ac878;color:#0ac878;background-color:#f9f9f9',
8 'approved': 'border:1px solid #0ac878;color:#0ac878;background-color:#f9f9f9',
9 'rejected': 'border:1px solid #e85e4d;color:#e85e4d;background-color:#f9f9f9',
9 'rejected': 'border:1px solid #e85e4d;color:#e85e4d;background-color:#f9f9f9',
10 'under_review': 'border:1px solid #ffc854;color:#ffc854;background-color:#f9f9f9',
10 'under_review': 'border:1px solid #ffc854;color:#ffc854;background-color:#f9f9f9',
11 }
11 }
12
12
13 css_style = ';'.join([
13 css_style = ';'.join([
14 'display:inline',
14 'display:inline',
15 'border-radius:2px',
15 'border-radius:2px',
16 'font-size:12px',
16 'font-size:12px',
17 'padding:.2em',
17 'padding:.2em',
18 ])
18 ])
19
19
20 %>
20 %>
21 <pre style="${css_style}; ${color_scheme.get(tag_type, color_scheme['default'])}">${text}</pre>
21 <pre style="${css_style}; ${color_scheme.get(tag_type, color_scheme['default'])}">${text}</pre>
22 </%def>
22 </%def>
23
23
24 <%def name="status_text(text, tag_type=None)">
24 <%def name="status_text(text, tag_type=None)">
25 <%
25 <%
26 color_scheme = {
26 color_scheme = {
27 'default': 'color:#666666',
27 'default': 'color:#666666',
28 'approved': 'color:#0ac878',
28 'approved': 'color:#0ac878',
29 'rejected': 'color:#e85e4d',
29 'rejected': 'color:#e85e4d',
30 'under_review': 'color:#ffc854',
30 'under_review': 'color:#ffc854',
31 }
31 }
32 %>
32 %>
33 <span style="font-weight:bold;font-size:12px;padding:.2em;${color_scheme.get(tag_type, color_scheme['default'])}">${text}</span>
33 <span style="font-weight:bold;font-size:12px;padding:.2em;${color_scheme.get(tag_type, color_scheme['default'])}">${text}</span>
34 </%def>
34 </%def>
35
35
36 <%def name="gravatar_img(email, size=16)">
36 <%def name="gravatar_img(email, size=16)">
37 <%
37 <%
38 css_style = ';'.join([
38 css_style = ';'.join([
39 'padding: 0',
39 'padding: 0',
40 'margin: -4px 0',
40 'margin: -4px 0',
41 'border-radius: 50%',
41 'border-radius: 50%',
42 'box-sizing: content-box',
42 'box-sizing: content-box',
43 'display: inline',
43 'display: inline',
44 'line-height: 1em',
44 'line-height: 1em',
45 'min-width: 16px',
45 'min-width: 16px',
46 'min-height: 16px',
46 'min-height: 16px',
47 ])
47 ])
48 %>
48 %>
49
49
50 <img alt="gravatar" style="${css_style}" src="${h.gravatar_url(email, size)}" height="${size}" width="${size}">
50 <img alt="gravatar" style="${css_style}" src="${h.gravatar_url(email, size)}" height="${size}" width="${size}">
51 </%def>
51 </%def>
52
52
53 <%def name="link_css()">\
53 <%def name="link_css()">\
54 <%
54 <%
55 css_style = ';'.join([
55 css_style = ';'.join([
56 'color:#427cc9',
56 'color:#427cc9',
57 'text-decoration:none',
57 'text-decoration:none',
58 'cursor:pointer'
58 'cursor:pointer'
59 ])
59 ])
60 %>\
60 %>\
61 ${css_style}\
61 ${css_style}\
62 </%def>
62 </%def>
63
63
64 ## Constants
64 ## Constants
65 <%
65 <%
66 text_regular = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, sans-serif"
66 text_regular = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, sans-serif"
67 text_monospace = "'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace"
67 text_monospace = "'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace"
68
68
69 %>
69 %>
70
70
71 ## headers we additionally can set for email
72 <%def name="headers()" filter="n,trim"></%def>
73
74 <%def name="plaintext_footer()" filter="trim">
71 <%def name="plaintext_footer()" filter="trim">
75 ${_('This is a notification from RhodeCode.')} ${instance_url}
72 ${_('This is a notification from RhodeCode.')} ${instance_url}
76 </%def>
73 </%def>
77
74
78 <%def name="body_plaintext()" filter="n,trim">
75 <%def name="body_plaintext()" filter="n,trim">
79 ## this example is not called itself but overridden in each template
76 ## this example is not called itself but overridden in each template
80 ## the plaintext_footer should be at the bottom of both html and text emails
77 ## the plaintext_footer should be at the bottom of both html and text emails
81 ${self.plaintext_footer()}
78 ${self.plaintext_footer()}
82 </%def>
79 </%def>
83
80
84 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
81 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
85 <html xmlns="http://www.w3.org/1999/xhtml">
82 <html xmlns="http://www.w3.org/1999/xhtml">
86 <head>
83 <head>
87 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
84 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
88 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
85 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
89 <title>${self.subject()}</title>
86 <title>${self.subject()}</title>
90 <style type="text/css">
87 <style type="text/css">
91 /* Based on The MailChimp Reset INLINE: Yes. */
88 /* Based on The MailChimp Reset INLINE: Yes. */
92 #outlook a {
89 #outlook a {
93 padding: 0;
90 padding: 0;
94 }
91 }
95
92
96 /* Force Outlook to provide a "view in browser" menu link. */
93 /* Force Outlook to provide a "view in browser" menu link. */
97 body {
94 body {
98 width: 100% !important;
95 width: 100% !important;
99 -webkit-text-size-adjust: 100%;
96 -webkit-text-size-adjust: 100%;
100 -ms-text-size-adjust: 100%;
97 -ms-text-size-adjust: 100%;
101 margin: 0;
98 margin: 0;
102 padding: 0;
99 padding: 0;
103 font-family: ${text_regular|n};
100 font-family: ${text_regular|n};
104 color: #000000;
101 color: #000000;
105 }
102 }
106
103
107 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
104 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
108 .ExternalClass {
105 .ExternalClass {
109 width: 100%;
106 width: 100%;
110 }
107 }
111
108
112 /* Force Hotmail to display emails at full width */
109 /* Force Hotmail to display emails at full width */
113 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {
110 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {
114 line-height: 100%;
111 line-height: 100%;
115 }
112 }
116
113
117 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
114 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
118 #backgroundTable {
115 #backgroundTable {
119 margin: 0;
116 margin: 0;
120 padding: 0;
117 padding: 0;
121 line-height: 100% !important;
118 line-height: 100% !important;
122 }
119 }
123
120
124 /* End reset */
121 /* End reset */
125
122
126 /* defaults for images*/
123 /* defaults for images*/
127 img {
124 img {
128 outline: none;
125 outline: none;
129 text-decoration: none;
126 text-decoration: none;
130 -ms-interpolation-mode: bicubic;
127 -ms-interpolation-mode: bicubic;
131 }
128 }
132
129
133 a img {
130 a img {
134 border: none;
131 border: none;
135 }
132 }
136
133
137 .image_fix {
134 .image_fix {
138 display: block;
135 display: block;
139 }
136 }
140
137
141 body {
138 body {
142 line-height: 1.2em;
139 line-height: 1.2em;
143 }
140 }
144
141
145 p {
142 p {
146 margin: 0 0 20px;
143 margin: 0 0 20px;
147 }
144 }
148
145
149 h1, h2, h3, h4, h5, h6 {
146 h1, h2, h3, h4, h5, h6 {
150 color: #323232 !important;
147 color: #323232 !important;
151 }
148 }
152
149
153 a {
150 a {
154 color: #427cc9;
151 color: #427cc9;
155 text-decoration: none;
152 text-decoration: none;
156 outline: none;
153 outline: none;
157 cursor: pointer;
154 cursor: pointer;
158 }
155 }
159
156
160 a:focus {
157 a:focus {
161 outline: none;
158 outline: none;
162 }
159 }
163
160
164 a:hover {
161 a:hover {
165 color: #305b91;
162 color: #305b91;
166 }
163 }
167
164
168 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
165 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
169 color: #427cc9 !important;
166 color: #427cc9 !important;
170 text-decoration: none !important;
167 text-decoration: none !important;
171 }
168 }
172
169
173 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {
170 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {
174 color: #305b91 !important;
171 color: #305b91 !important;
175 }
172 }
176
173
177 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {
174 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {
178 color: #305b91 !important;
175 color: #305b91 !important;
179 }
176 }
180
177
181 table {
178 table {
182 font-size: 13px;
179 font-size: 13px;
183 border-collapse: collapse;
180 border-collapse: collapse;
184 mso-table-lspace: 0pt;
181 mso-table-lspace: 0pt;
185 mso-table-rspace: 0pt;
182 mso-table-rspace: 0pt;
186 }
183 }
187
184
188 table tr {
185 table tr {
189 display: table-row;
186 display: table-row;
190 vertical-align: inherit;
187 vertical-align: inherit;
191 border-color: inherit;
188 border-color: inherit;
192 border-spacing: 0 3px;
189 border-spacing: 0 3px;
193 }
190 }
194
191
195 table td {
192 table td {
196 padding: .65em 1em .65em 0;
193 padding: .65em 1em .65em 0;
197 border-collapse: collapse;
194 border-collapse: collapse;
198 vertical-align: top;
195 vertical-align: top;
199 text-align: left;
196 text-align: left;
200 }
197 }
201
198
202 input {
199 input {
203 display: inline;
200 display: inline;
204 border-radius: 2px;
201 border-radius: 2px;
205 border: 1px solid #dbd9da;
202 border: 1px solid #dbd9da;
206 padding: .5em;
203 padding: .5em;
207 }
204 }
208
205
209 input:focus {
206 input:focus {
210 outline: 1px solid #979797
207 outline: 1px solid #979797
211 }
208 }
212
209
213 code {
210 code {
214 font-family: ${text_monospace|n};
211 font-family: ${text_monospace|n};
215 white-space: pre-line !important;
212 white-space: pre-line !important;
216 color: #000000;
213 color: #000000;
217 }
214 }
218
215
219 ul.changes-ul {
216 ul.changes-ul {
220 list-style: none;
217 list-style: none;
221 list-style-type: none;
218 list-style-type: none;
222 padding: 0;
219 padding: 0;
223 margin: 10px 0;
220 margin: 10px 0;
224 }
221 }
225 ul.changes-ul li {
222 ul.changes-ul li {
226 list-style: none;
223 list-style: none;
227 list-style-type: none;
224 list-style-type: none;
228 margin: 2px 0;
225 margin: 2px 0;
229 }
226 }
230
227
231 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
228 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
232 /* Put your iPhone 4g styles in here */
229 /* Put your iPhone 4g styles in here */
233 }
230 }
234
231
235 /* Android targeting */
232 /* Android targeting */
236 @media only screen and (-webkit-device-pixel-ratio:.75){
233 @media only screen and (-webkit-device-pixel-ratio:.75){
237 /* Put CSS for low density (ldpi) Android layouts in here */
234 /* Put CSS for low density (ldpi) Android layouts in here */
238 }
235 }
239 @media only screen and (-webkit-device-pixel-ratio:1){
236 @media only screen and (-webkit-device-pixel-ratio:1){
240 /* Put CSS for medium density (mdpi) Android layouts in here */
237 /* Put CSS for medium density (mdpi) Android layouts in here */
241 }
238 }
242 @media only screen and (-webkit-device-pixel-ratio:1.5){
239 @media only screen and (-webkit-device-pixel-ratio:1.5){
243 /* Put CSS for high density (hdpi) Android layouts in here */
240 /* Put CSS for high density (hdpi) Android layouts in here */
244 }
241 }
245 /* end Android targeting */
242 /* end Android targeting */
246
243
247 /** MARKDOWN styling **/
244 /** MARKDOWN styling **/
248 div.markdown-block {
245 div.markdown-block {
249 clear: both;
246 clear: both;
250 overflow: hidden;
247 overflow: hidden;
251 margin: 0;
248 margin: 0;
252 padding: 3px 5px 3px
249 padding: 3px 5px 3px
253 }
250 }
254
251
255 div.markdown-block h1,
252 div.markdown-block h1,
256 div.markdown-block h2,
253 div.markdown-block h2,
257 div.markdown-block h3,
254 div.markdown-block h3,
258 div.markdown-block h4,
255 div.markdown-block h4,
259 div.markdown-block h5,
256 div.markdown-block h5,
260 div.markdown-block h6 {
257 div.markdown-block h6 {
261 border-bottom: none !important;
258 border-bottom: none !important;
262 padding: 0 !important;
259 padding: 0 !important;
263 overflow: visible !important
260 overflow: visible !important
264 }
261 }
265
262
266 div.markdown-block h1,
263 div.markdown-block h1,
267 div.markdown-block h2 {
264 div.markdown-block h2 {
268 border-bottom: 1px #e6e5e5 solid !important
265 border-bottom: 1px #e6e5e5 solid !important
269 }
266 }
270
267
271 div.markdown-block h1 {
268 div.markdown-block h1 {
272 font-size: 32px;
269 font-size: 32px;
273 margin: 15px 0 15px 0 !important;
270 margin: 15px 0 15px 0 !important;
274 padding-bottom: 5px !important
271 padding-bottom: 5px !important
275 }
272 }
276
273
277 div.markdown-block h2 {
274 div.markdown-block h2 {
278 font-size: 24px !important;
275 font-size: 24px !important;
279 margin: 34px 0 10px 0 !important;
276 margin: 34px 0 10px 0 !important;
280 padding-top: 15px !important;
277 padding-top: 15px !important;
281 padding-bottom: 8px !important
278 padding-bottom: 8px !important
282 }
279 }
283
280
284 div.markdown-block h3 {
281 div.markdown-block h3 {
285 font-size: 18px !important;
282 font-size: 18px !important;
286 margin: 30px 0 8px 0 !important;
283 margin: 30px 0 8px 0 !important;
287 padding-bottom: 2px !important
284 padding-bottom: 2px !important
288 }
285 }
289
286
290 div.markdown-block h4 {
287 div.markdown-block h4 {
291 font-size: 13px !important;
288 font-size: 13px !important;
292 margin: 18px 0 3px 0 !important
289 margin: 18px 0 3px 0 !important
293 }
290 }
294
291
295 div.markdown-block h5 {
292 div.markdown-block h5 {
296 font-size: 12px !important;
293 font-size: 12px !important;
297 margin: 15px 0 3px 0 !important
294 margin: 15px 0 3px 0 !important
298 }
295 }
299
296
300 div.markdown-block h6 {
297 div.markdown-block h6 {
301 font-size: 12px;
298 font-size: 12px;
302 color: #777777;
299 color: #777777;
303 margin: 15px 0 3px 0 !important
300 margin: 15px 0 3px 0 !important
304 }
301 }
305
302
306 div.markdown-block hr {
303 div.markdown-block hr {
307 border: 0;
304 border: 0;
308 color: #e6e5e5;
305 color: #e6e5e5;
309 background-color: #e6e5e5;
306 background-color: #e6e5e5;
310 height: 3px;
307 height: 3px;
311 margin-bottom: 13px
308 margin-bottom: 13px
312 }
309 }
313
310
314 div.markdown-block ol,
311 div.markdown-block ol,
315 div.markdown-block ul,
312 div.markdown-block ul,
316 div.markdown-block p,
313 div.markdown-block p,
317 div.markdown-block blockquote,
314 div.markdown-block blockquote,
318 div.markdown-block dl,
315 div.markdown-block dl,
319 div.markdown-block li,
316 div.markdown-block li,
320 div.markdown-block table {
317 div.markdown-block table {
321 margin: 3px 0 13px 0 !important;
318 margin: 3px 0 13px 0 !important;
322 color: #424242 !important;
319 color: #424242 !important;
323 font-size: 13px !important;
320 font-size: 13px !important;
324 font-family: ${text_regular|n};
321 font-family: ${text_regular|n};
325 font-weight: normal !important;
322 font-weight: normal !important;
326 overflow: visible !important;
323 overflow: visible !important;
327 line-height: 140% !important
324 line-height: 140% !important
328 }
325 }
329
326
330 div.markdown-block pre {
327 div.markdown-block pre {
331 margin: 3px 0 13px 0 !important;
328 margin: 3px 0 13px 0 !important;
332 padding: .5em;
329 padding: .5em;
333 color: #424242 !important;
330 color: #424242 !important;
334 font-size: 13px !important;
331 font-size: 13px !important;
335 overflow: visible !important;
332 overflow: visible !important;
336 line-height: 140% !important;
333 line-height: 140% !important;
337 background-color: #F5F5F5
334 background-color: #F5F5F5
338 }
335 }
339
336
340 div.markdown-block img {
337 div.markdown-block img {
341 border-style: none;
338 border-style: none;
342 background-color: #fff;
339 background-color: #fff;
343 padding-right: 20px;
340 padding-right: 20px;
344 max-width: 100%
341 max-width: 100%
345 }
342 }
346
343
347 div.markdown-block strong {
344 div.markdown-block strong {
348 font-weight: 600;
345 font-weight: 600;
349 margin: 0
346 margin: 0
350 }
347 }
351
348
352 div.markdown-block ul.checkbox, div.markdown-block ol.checkbox {
349 div.markdown-block ul.checkbox, div.markdown-block ol.checkbox {
353 padding-left: 20px !important;
350 padding-left: 20px !important;
354 margin-top: 0 !important;
351 margin-top: 0 !important;
355 margin-bottom: 18px !important
352 margin-bottom: 18px !important
356 }
353 }
357
354
358 div.markdown-block ul, div.markdown-block ol {
355 div.markdown-block ul, div.markdown-block ol {
359 padding-left: 30px !important;
356 padding-left: 30px !important;
360 margin-top: 0 !important;
357 margin-top: 0 !important;
361 margin-bottom: 18px !important
358 margin-bottom: 18px !important
362 }
359 }
363
360
364 div.markdown-block ul.checkbox li, div.markdown-block ol.checkbox li {
361 div.markdown-block ul.checkbox li, div.markdown-block ol.checkbox li {
365 list-style: none !important;
362 list-style: none !important;
366 margin: 6px !important;
363 margin: 6px !important;
367 padding: 0 !important
364 padding: 0 !important
368 }
365 }
369
366
370 div.markdown-block ul li, div.markdown-block ol li {
367 div.markdown-block ul li, div.markdown-block ol li {
371 list-style: disc !important;
368 list-style: disc !important;
372 margin: 6px !important;
369 margin: 6px !important;
373 padding: 0 !important
370 padding: 0 !important
374 }
371 }
375
372
376 div.markdown-block ol li {
373 div.markdown-block ol li {
377 list-style: decimal !important
374 list-style: decimal !important
378 }
375 }
379
376
380 div.markdown-block #message {
377 div.markdown-block #message {
381 -webkit-border-radius: 2px;
378 -webkit-border-radius: 2px;
382 -moz-border-radius: 2px;
379 -moz-border-radius: 2px;
383 border-radius: 2px;
380 border-radius: 2px;
384 border: 1px solid #dbd9da;
381 border: 1px solid #dbd9da;
385 display: block;
382 display: block;
386 width: 100%;
383 width: 100%;
387 height: 60px;
384 height: 60px;
388 margin: 6px 0
385 margin: 6px 0
389 }
386 }
390
387
391 div.markdown-block button, div.markdown-block #ws {
388 div.markdown-block button, div.markdown-block #ws {
392 font-size: 13px;
389 font-size: 13px;
393 padding: 4px 6px;
390 padding: 4px 6px;
394 -webkit-border-radius: 2px;
391 -webkit-border-radius: 2px;
395 -moz-border-radius: 2px;
392 -moz-border-radius: 2px;
396 border-radius: 2px;
393 border-radius: 2px;
397 border: 1px solid #dbd9da;
394 border: 1px solid #dbd9da;
398 background-color: #eeeeee
395 background-color: #eeeeee
399 }
396 }
400
397
401 div.markdown-block code,
398 div.markdown-block code,
402 div.markdown-block pre,
399 div.markdown-block pre,
403 div.markdown-block #ws,
400 div.markdown-block #ws,
404 div.markdown-block #message {
401 div.markdown-block #message {
405 font-family: ${text_monospace|n};
402 font-family: ${text_monospace|n};
406 font-size: 11px;
403 font-size: 11px;
407 -webkit-border-radius: 2px;
404 -webkit-border-radius: 2px;
408 -moz-border-radius: 2px;
405 -moz-border-radius: 2px;
409 border-radius: 2px;
406 border-radius: 2px;
410 background-color: #FFFFFF;
407 background-color: #FFFFFF;
411 color: #7E7F7F
408 color: #7E7F7F
412 }
409 }
413
410
414 div.markdown-block code {
411 div.markdown-block code {
415 border: 1px solid #7E7F7F;
412 border: 1px solid #7E7F7F;
416 margin: 0 2px;
413 margin: 0 2px;
417 padding: 0 5px
414 padding: 0 5px
418 }
415 }
419
416
420 div.markdown-block pre {
417 div.markdown-block pre {
421 border: 1px solid #7E7F7F;
418 border: 1px solid #7E7F7F;
422 overflow: auto;
419 overflow: auto;
423 padding: .5em;
420 padding: .5em;
424 background-color: #FFFFFF;
421 background-color: #FFFFFF;
425 }
422 }
426
423
427 div.markdown-block pre > code {
424 div.markdown-block pre > code {
428 border: 0;
425 border: 0;
429 margin: 0;
426 margin: 0;
430 padding: 0
427 padding: 0
431 }
428 }
432
429
433 div.rst-block {
430 div.rst-block {
434 clear: both;
431 clear: both;
435 overflow: hidden;
432 overflow: hidden;
436 margin: 0;
433 margin: 0;
437 padding: 3px 5px 3px
434 padding: 3px 5px 3px
438 }
435 }
439
436
440 div.rst-block h2 {
437 div.rst-block h2 {
441 font-weight: normal
438 font-weight: normal
442 }
439 }
443
440
444 div.rst-block h1,
441 div.rst-block h1,
445 div.rst-block h2,
442 div.rst-block h2,
446 div.rst-block h3,
443 div.rst-block h3,
447 div.rst-block h4,
444 div.rst-block h4,
448 div.rst-block h5,
445 div.rst-block h5,
449 div.rst-block h6 {
446 div.rst-block h6 {
450 border-bottom: 0 !important;
447 border-bottom: 0 !important;
451 margin: 0 !important;
448 margin: 0 !important;
452 padding: 0 !important;
449 padding: 0 !important;
453 line-height: 1.5em !important
450 line-height: 1.5em !important
454 }
451 }
455
452
456 div.rst-block h1:first-child {
453 div.rst-block h1:first-child {
457 padding-top: .25em !important
454 padding-top: .25em !important
458 }
455 }
459
456
460 div.rst-block h2, div.rst-block h3 {
457 div.rst-block h2, div.rst-block h3 {
461 margin: 1em 0 !important
458 margin: 1em 0 !important
462 }
459 }
463
460
464 div.rst-block h1, div.rst-block h2 {
461 div.rst-block h1, div.rst-block h2 {
465 border-bottom: 1px #e6e5e5 solid !important
462 border-bottom: 1px #e6e5e5 solid !important
466 }
463 }
467
464
468 div.rst-block h2 {
465 div.rst-block h2 {
469 margin-top: 1.5em !important;
466 margin-top: 1.5em !important;
470 padding-top: .5em !important
467 padding-top: .5em !important
471 }
468 }
472
469
473 div.rst-block p {
470 div.rst-block p {
474 color: black !important;
471 color: black !important;
475 margin: 1em 0 !important;
472 margin: 1em 0 !important;
476 line-height: 1.5em !important
473 line-height: 1.5em !important
477 }
474 }
478
475
479 div.rst-block ul {
476 div.rst-block ul {
480 list-style: disc !important;
477 list-style: disc !important;
481 margin: 1em 0 1em 2em !important;
478 margin: 1em 0 1em 2em !important;
482 clear: both
479 clear: both
483 }
480 }
484
481
485 div.rst-block ol {
482 div.rst-block ol {
486 list-style: decimal;
483 list-style: decimal;
487 margin: 1em 0 1em 2em !important
484 margin: 1em 0 1em 2em !important
488 }
485 }
489
486
490 div.rst-block pre, div.rst-block code {
487 div.rst-block pre, div.rst-block code {
491 font: 12px "Bitstream Vera Sans Mono", "Courier", monospace
488 font: 12px "Bitstream Vera Sans Mono", "Courier", monospace
492 }
489 }
493
490
494 div.rst-block code {
491 div.rst-block code {
495 font-size: 12px !important;
492 font-size: 12px !important;
496 background-color: ghostWhite !important;
493 background-color: ghostWhite !important;
497 color: #444 !important;
494 color: #444 !important;
498 padding: 0 .2em !important;
495 padding: 0 .2em !important;
499 border: 1px solid #7E7F7F !important
496 border: 1px solid #7E7F7F !important
500 }
497 }
501
498
502 div.rst-block pre code {
499 div.rst-block pre code {
503 padding: 0 !important;
500 padding: 0 !important;
504 font-size: 12px !important;
501 font-size: 12px !important;
505 background-color: #eee !important;
502 background-color: #eee !important;
506 border: none !important
503 border: none !important
507 }
504 }
508
505
509 div.rst-block pre {
506 div.rst-block pre {
510 margin: 1em 0;
507 margin: 1em 0;
511 padding: 15px;
508 padding: 15px;
512 border: 1px solid #7E7F7F;
509 border: 1px solid #7E7F7F;
513 -webkit-border-radius: 2px;
510 -webkit-border-radius: 2px;
514 -moz-border-radius: 2px;
511 -moz-border-radius: 2px;
515 border-radius: 2px;
512 border-radius: 2px;
516 overflow: auto;
513 overflow: auto;
517 font-size: 12px;
514 font-size: 12px;
518 color: #444;
515 color: #444;
519 background-color: #FFFFFF;
516 background-color: #FFFFFF;
520 }
517 }
521
518
522 .clear-both {
519 .clear-both {
523 clear:both;
520 clear:both;
524 }
521 }
525
522
526 /*elasticmatch is custom rhodecode tag*/
523 /*elasticmatch is custom rhodecode tag*/
527 .codehilite .c-ElasticMatch {
524 .codehilite .c-ElasticMatch {
528 background-color: #faffa6;
525 background-color: #faffa6;
529 padding: 0.2em;
526 padding: 0.2em;
530 }
527 }
531
528
532 .codehilite .c-ElasticMatch { background-color: #faffa6; padding: 0.2em;}
529 .codehilite .c-ElasticMatch { background-color: #faffa6; padding: 0.2em;}
533 .codehilite .hll { background-color: #ffffcc }
530 .codehilite .hll { background-color: #ffffcc }
534 .codehilite .c { color: #408080; font-style: italic } /* Comment */
531 .codehilite .c { color: #408080; font-style: italic } /* Comment */
535 .codehilite .err { border: none } /* Error */
532 .codehilite .err { border: none } /* Error */
536 .codehilite .k { color: #008000; font-weight: bold } /* Keyword */
533 .codehilite .k { color: #008000; font-weight: bold } /* Keyword */
537 .codehilite .o { color: #666666 } /* Operator */
534 .codehilite .o { color: #666666 } /* Operator */
538 .codehilite .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
535 .codehilite .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
539 .codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */
536 .codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */
540 .codehilite .cp { color: #BC7A00 } /* Comment.Preproc */
537 .codehilite .cp { color: #BC7A00 } /* Comment.Preproc */
541 .codehilite .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
538 .codehilite .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
542 .codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */
539 .codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */
543 .codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */
540 .codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */
544 .codehilite .gd { color: #A00000 } /* Generic.Deleted */
541 .codehilite .gd { color: #A00000 } /* Generic.Deleted */
545 .codehilite .ge { font-style: italic } /* Generic.Emph */
542 .codehilite .ge { font-style: italic } /* Generic.Emph */
546 .codehilite .gr { color: #FF0000 } /* Generic.Error */
543 .codehilite .gr { color: #FF0000 } /* Generic.Error */
547 .codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
544 .codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
548 .codehilite .gi { color: #00A000 } /* Generic.Inserted */
545 .codehilite .gi { color: #00A000 } /* Generic.Inserted */
549 .codehilite .go { color: #888888 } /* Generic.Output */
546 .codehilite .go { color: #888888 } /* Generic.Output */
550 .codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
547 .codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
551 .codehilite .gs { font-weight: bold } /* Generic.Strong */
548 .codehilite .gs { font-weight: bold } /* Generic.Strong */
552 .codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
549 .codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
553 .codehilite .gt { color: #0044DD } /* Generic.Traceback */
550 .codehilite .gt { color: #0044DD } /* Generic.Traceback */
554 .codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
551 .codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
555 .codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
552 .codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
556 .codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
553 .codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
557 .codehilite .kp { color: #008000 } /* Keyword.Pseudo */
554 .codehilite .kp { color: #008000 } /* Keyword.Pseudo */
558 .codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
555 .codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
559 .codehilite .kt { color: #B00040 } /* Keyword.Type */
556 .codehilite .kt { color: #B00040 } /* Keyword.Type */
560 .codehilite .m { color: #666666 } /* Literal.Number */
557 .codehilite .m { color: #666666 } /* Literal.Number */
561 .codehilite .s { color: #BA2121 } /* Literal.String */
558 .codehilite .s { color: #BA2121 } /* Literal.String */
562 .codehilite .na { color: #7D9029 } /* Name.Attribute */
559 .codehilite .na { color: #7D9029 } /* Name.Attribute */
563 .codehilite .nb { color: #008000 } /* Name.Builtin */
560 .codehilite .nb { color: #008000 } /* Name.Builtin */
564 .codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */
561 .codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */
565 .codehilite .no { color: #880000 } /* Name.Constant */
562 .codehilite .no { color: #880000 } /* Name.Constant */
566 .codehilite .nd { color: #AA22FF } /* Name.Decorator */
563 .codehilite .nd { color: #AA22FF } /* Name.Decorator */
567 .codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */
564 .codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */
568 .codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
565 .codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
569 .codehilite .nf { color: #0000FF } /* Name.Function */
566 .codehilite .nf { color: #0000FF } /* Name.Function */
570 .codehilite .nl { color: #A0A000 } /* Name.Label */
567 .codehilite .nl { color: #A0A000 } /* Name.Label */
571 .codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
568 .codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
572 .codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
569 .codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
573 .codehilite .nv { color: #19177C } /* Name.Variable */
570 .codehilite .nv { color: #19177C } /* Name.Variable */
574 .codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
571 .codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
575 .codehilite .w { color: #bbbbbb } /* Text.Whitespace */
572 .codehilite .w { color: #bbbbbb } /* Text.Whitespace */
576 .codehilite .mb { color: #666666 } /* Literal.Number.Bin */
573 .codehilite .mb { color: #666666 } /* Literal.Number.Bin */
577 .codehilite .mf { color: #666666 } /* Literal.Number.Float */
574 .codehilite .mf { color: #666666 } /* Literal.Number.Float */
578 .codehilite .mh { color: #666666 } /* Literal.Number.Hex */
575 .codehilite .mh { color: #666666 } /* Literal.Number.Hex */
579 .codehilite .mi { color: #666666 } /* Literal.Number.Integer */
576 .codehilite .mi { color: #666666 } /* Literal.Number.Integer */
580 .codehilite .mo { color: #666666 } /* Literal.Number.Oct */
577 .codehilite .mo { color: #666666 } /* Literal.Number.Oct */
581 .codehilite .sa { color: #BA2121 } /* Literal.String.Affix */
578 .codehilite .sa { color: #BA2121 } /* Literal.String.Affix */
582 .codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
579 .codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
583 .codehilite .sc { color: #BA2121 } /* Literal.String.Char */
580 .codehilite .sc { color: #BA2121 } /* Literal.String.Char */
584 .codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */
581 .codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */
585 .codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
582 .codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
586 .codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
583 .codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
587 .codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
584 .codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
588 .codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
585 .codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
589 .codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
586 .codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
590 .codehilite .sx { color: #008000 } /* Literal.String.Other */
587 .codehilite .sx { color: #008000 } /* Literal.String.Other */
591 .codehilite .sr { color: #BB6688 } /* Literal.String.Regex */
588 .codehilite .sr { color: #BB6688 } /* Literal.String.Regex */
592 .codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
589 .codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
593 .codehilite .ss { color: #19177C } /* Literal.String.Symbol */
590 .codehilite .ss { color: #19177C } /* Literal.String.Symbol */
594 .codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
591 .codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
595 .codehilite .fm { color: #0000FF } /* Name.Function.Magic */
592 .codehilite .fm { color: #0000FF } /* Name.Function.Magic */
596 .codehilite .vc { color: #19177C } /* Name.Variable.Class */
593 .codehilite .vc { color: #19177C } /* Name.Variable.Class */
597 .codehilite .vg { color: #19177C } /* Name.Variable.Global */
594 .codehilite .vg { color: #19177C } /* Name.Variable.Global */
598 .codehilite .vi { color: #19177C } /* Name.Variable.Instance */
595 .codehilite .vi { color: #19177C } /* Name.Variable.Instance */
599 .codehilite .vm { color: #19177C } /* Name.Variable.Magic */
596 .codehilite .vm { color: #19177C } /* Name.Variable.Magic */
600 .codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */
597 .codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */
601
598
602 </style>
599 </style>
603
600
604 </head>
601 </head>
605 <body>
602 <body>
606
603
607 <div>
604 <div>
608 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
605 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
609 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:${text_regular|n};font-weight:100;border:1px solid #dbd9da">
606 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:${text_regular|n};font-weight:100;border:1px solid #dbd9da">
610 <tr>
607 <tr>
611 <td valign="top" style="padding:0;">
608 <td valign="top" style="padding:0;">
612 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
609 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
613 <tr>
610 <tr>
614 <td style="width:100%;padding:10px 15px;background-color:#202020" valign="top">
611 <td style="width:100%;padding:10px 15px;background-color:#202020" valign="top">
615 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
612 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
616 ${_('RhodeCode')}
613 ${_('RhodeCode')}
617 % if rhodecode_instance_name:
614 % if rhodecode_instance_name:
618 - ${rhodecode_instance_name}
615 - ${rhodecode_instance_name}
619 % endif
616 % endif
620 </a>
617 </a>
621 </td>
618 </td>
622 </tr>
619 </tr>
623 <tr style="background-color: #fff">
620 <tr style="background-color: #fff">
624 <td style="padding:15px;" valign="top">${self.body()}</td>
621 <td style="padding:15px;" valign="top">${self.body()}</td>
625 </tr>
622 </tr>
626 </table>
623 </table>
627 </td>
624 </td>
628 </tr>
625 </tr>
629 </table>
626 </table>
630 <!-- End of wrapper table -->
627 <!-- End of wrapper table -->
631 </div>
628 </div>
632
629
633 <div style="width:100%; clear: both; height: 1px">&nbsp;</div>
630 <div style="width:100%; clear: both; height: 1px">&nbsp;</div>
634
631
635 <div style="margin-left:1%;font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace};">
632 <div style="margin-left:1%;font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace};">
636 ${_('This is a notification from RhodeCode.')}
633 ${_('This is a notification from RhodeCode.')}
637 <a style="font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace};" href="${instance_url}">
634 <a style="font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace};" href="${instance_url}">
638 ${instance_url}
635 ${instance_url}
639 </a>
636 </a>
640 </div>
637 </div>
641 </body>
638 </body>
642 </html>
639 </html>
@@ -1,172 +1,173 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'user': '@'+h.person(user),
9 'user': '@'+h.person(user),
10 'repo_name': repo_name,
10 'repo_name': repo_name,
11 'status': status_change,
11 'status': status_change,
12 'comment_file': comment_file,
12 'comment_file': comment_file,
13 'comment_line': comment_line,
13 'comment_line': comment_line,
14 'comment_type': comment_type,
14 'comment_type': comment_type,
15 'comment_id': comment_id,
15 'comment_id': comment_id,
16
16
17 'commit_id': h.show_id(commit),
17 'commit_id': h.show_id(commit),
18 'mention_prefix': '[mention] ' if mention else '',
18 }
19 }
19 %>
20 %>
20
21
21
22
22 % if comment_file:
23 % if comment_file:
23 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`').format(**data)} ${_('in the `{repo_name}` repository').format(**data) |n}
24 ${_('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`').format(**data)} ${_('in the `{repo_name}` repository').format(**data) |n}
24 % else:
25 % else:
25 % if status_change:
26 % if status_change:
26 ${(_('[mention]') if mention else '')} ${_('[status: {status}] {user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
27 ${_('{mention_prefix}[status: {status}] {user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
27 % else:
28 % else:
28 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
29 ${_('{mention_prefix}{user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
29 % endif
30 % endif
30 % endif
31 % endif
31
32
32 </%def>
33 </%def>
33
34
34 ## PLAINTEXT VERSION OF BODY
35 ## PLAINTEXT VERSION OF BODY
35 <%def name="body_plaintext()" filter="n,trim">
36 <%def name="body_plaintext()" filter="n,trim">
36 <%
37 <%
37 data = {
38 data = {
38 'user': h.person(user),
39 'user': h.person(user),
39 'repo_name': repo_name,
40 'repo_name': repo_name,
40 'status': status_change,
41 'status': status_change,
41 'comment_file': comment_file,
42 'comment_file': comment_file,
42 'comment_line': comment_line,
43 'comment_line': comment_line,
43 'comment_type': comment_type,
44 'comment_type': comment_type,
44 'comment_id': comment_id,
45 'comment_id': comment_id,
45
46
46 'commit_id': h.show_id(commit),
47 'commit_id': h.show_id(commit),
47 }
48 }
48 %>
49 %>
49
50
50 * ${_('Comment link')}: ${commit_comment_url}
51 * ${_('Comment link')}: ${commit_comment_url}
51
52
52 %if status_change:
53 %if status_change:
53 * ${_('Commit status')}: ${_('Status was changed to')}: *${status_change}*
54 * ${_('Commit status')}: ${_('Status was changed to')}: *${status_change}*
54
55
55 %endif
56 %endif
56 * ${_('Commit')}: ${h.show_id(commit)}
57 * ${_('Commit')}: ${h.show_id(commit)}
57
58
58 * ${_('Commit message')}: ${commit.message}
59 * ${_('Commit message')}: ${commit.message}
59
60
60 %if comment_file:
61 %if comment_file:
61 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
62 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
62
63
63 %endif
64 %endif
64 % if comment_type == 'todo':
65 % if comment_type == 'todo':
65 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
66 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
66 % else:
67 % else:
67 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
68 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
68 % endif
69 % endif
69
70
70 ${comment_body |n, trim}
71 ${comment_body |n, trim}
71
72
72 ---
73 ---
73 ${self.plaintext_footer()}
74 ${self.plaintext_footer()}
74 </%def>
75 </%def>
75
76
76
77
77 <%
78 <%
78 data = {
79 data = {
79 'user': h.person(user),
80 'user': h.person(user),
80 'comment_file': comment_file,
81 'comment_file': comment_file,
81 'comment_line': comment_line,
82 'comment_line': comment_line,
82 'comment_type': comment_type,
83 'comment_type': comment_type,
83 'comment_id': comment_id,
84 'comment_id': comment_id,
84 'renderer_type': renderer_type or 'plain',
85 'renderer_type': renderer_type or 'plain',
85
86
86 'repo': commit_target_repo_url,
87 'repo': commit_target_repo_url,
87 'repo_name': repo_name,
88 'repo_name': repo_name,
88 'commit_id': h.show_id(commit),
89 'commit_id': h.show_id(commit),
89 }
90 }
90 %>
91 %>
91
92
92 ## header
93 ## header
93 <table style="text-align:left;vertical-align:middle;width: 100%">
94 <table style="text-align:left;vertical-align:middle;width: 100%">
94 <tr>
95 <tr>
95 <td style="width:100%;border-bottom:1px solid #dbd9da;">
96 <td style="width:100%;border-bottom:1px solid #dbd9da;">
96
97
97 <div style="margin: 0; font-weight: bold">
98 <div style="margin: 0; font-weight: bold">
98 <div class="clear-both" style="margin-bottom: 4px">
99 <div class="clear-both" style="margin-bottom: 4px">
99 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
100 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
100 ${_('left a')}
101 ${_('left a')}
101 <a href="${commit_comment_url}" style="${base.link_css()}">
102 <a href="${commit_comment_url}" style="${base.link_css()}">
102 % if comment_file:
103 % if comment_file:
103 ${_('{comment_type} on file `{comment_file}` in commit.').format(**data)}
104 ${_('{comment_type} on file `{comment_file}` in commit.').format(**data)}
104 % else:
105 % else:
105 ${_('{comment_type} on commit.').format(**data) |n}
106 ${_('{comment_type} on commit.').format(**data) |n}
106 % endif
107 % endif
107 </a>
108 </a>
108 </div>
109 </div>
109 <div style="margin-top: 10px"></div>
110 <div style="margin-top: 10px"></div>
110 ${_('Commit')} <code>${data['commit_id']}</code> ${_('of repository')}: ${data['repo_name']}
111 ${_('Commit')} <code>${data['commit_id']}</code> ${_('of repository')}: ${data['repo_name']}
111 </div>
112 </div>
112
113
113 </td>
114 </td>
114 </tr>
115 </tr>
115
116
116 </table>
117 </table>
117 <div class="clear-both"></div>
118 <div class="clear-both"></div>
118 ## main body
119 ## main body
119 <table style="text-align:left;vertical-align:middle;width: 100%">
120 <table style="text-align:left;vertical-align:middle;width: 100%">
120
121
121 ## spacing def
122 ## spacing def
122 <tr>
123 <tr>
123 <td style="width: 130px"></td>
124 <td style="width: 130px"></td>
124 <td></td>
125 <td></td>
125 </tr>
126 </tr>
126
127
127 % if status_change:
128 % if status_change:
128 <tr>
129 <tr>
129 <td style="padding-right:20px;">${_('Commit Status')}:</td>
130 <td style="padding-right:20px;">${_('Commit Status')}:</td>
130 <td>
131 <td>
131 ${_('Status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}
132 ${_('Status was changed to')}: ${base.status_text(status_change, tag_type=status_change_type)}
132 </td>
133 </td>
133 </tr>
134 </tr>
134 % endif
135 % endif
135
136
136 <tr>
137 <tr>
137 <td style="padding-right:20px;">${_('Commit')}:</td>
138 <td style="padding-right:20px;">${_('Commit')}:</td>
138 <td>
139 <td>
139 <a href="${commit_comment_url}" style="${base.link_css()}">${h.show_id(commit)}</a>
140 <a href="${commit_comment_url}" style="${base.link_css()}">${h.show_id(commit)}</a>
140 </td>
141 </td>
141 </tr>
142 </tr>
142 <tr>
143 <tr>
143 <td style="padding-right:20px;">${_('Commit message')}:</td>
144 <td style="padding-right:20px;">${_('Commit message')}:</td>
144 <td style="white-space:pre-wrap">${h.urlify_commit_message(commit.message, repo_name)}</td>
145 <td style="white-space:pre-wrap">${h.urlify_commit_message(commit.message, repo_name)}</td>
145 </tr>
146 </tr>
146
147
147 % if comment_file:
148 % if comment_file:
148 <tr>
149 <tr>
149 <td style="padding-right:20px;">${_('File')}:</td>
150 <td style="padding-right:20px;">${_('File')}:</td>
150 <td><a href="${commit_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
151 <td><a href="${commit_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
151 </tr>
152 </tr>
152 % endif
153 % endif
153
154
154 <tr style="border-bottom:1px solid #dbd9da;">
155 <tr style="border-bottom:1px solid #dbd9da;">
155 <td colspan="2" style="padding-right:20px;">
156 <td colspan="2" style="padding-right:20px;">
156 % if comment_type == 'todo':
157 % if comment_type == 'todo':
157 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
158 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
158 % else:
159 % else:
159 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
160 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
160 % endif
161 % endif
161 </td>
162 </td>
162 </tr>
163 </tr>
163
164
164 <tr>
165 <tr>
165 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
166 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
166 </tr>
167 </tr>
167
168
168 <tr>
169 <tr>
169 <td><a href="${commit_comment_reply_url}">${_('Reply')}</a></td>
170 <td><a href="${commit_comment_reply_url}">${_('Reply')}</a></td>
170 <td></td>
171 <td></td>
171 </tr>
172 </tr>
172 </table>
173 </table>
@@ -1,203 +1,204 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
3 <%namespace name="base" file="base.mako"/>
4
4
5 ## EMAIL SUBJECT
5 ## EMAIL SUBJECT
6 <%def name="subject()" filter="n,trim,whitespace_filter">
6 <%def name="subject()" filter="n,trim,whitespace_filter">
7 <%
7 <%
8 data = {
8 data = {
9 'user': '@'+h.person(user),
9 'user': '@'+h.person(user),
10 'repo_name': repo_name,
10 'repo_name': repo_name,
11 'status': status_change,
11 'status': status_change,
12 'comment_file': comment_file,
12 'comment_file': comment_file,
13 'comment_line': comment_line,
13 'comment_line': comment_line,
14 'comment_type': comment_type,
14 'comment_type': comment_type,
15 'comment_id': comment_id,
15 'comment_id': comment_id,
16
16
17 'pr_title': pull_request.title,
17 'pr_title': pull_request.title,
18 'pr_id': pull_request.pull_request_id,
18 'pr_id': pull_request.pull_request_id,
19 'mention_prefix': '[mention] ' if mention else '',
19 }
20 }
20 %>
21 %>
21
22
22
23
23 % if comment_file:
24 % if comment_file:
24 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data) |n}
25 ${_('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data) |n}
25 % else:
26 % else:
26 % if status_change:
27 % if status_change:
27 ${(_('[mention]') if mention else '')} ${_('[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
28 ${_('{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
28 % else:
29 % else:
29 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
30 ${_('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
30 % endif
31 % endif
31 % endif
32 % endif
32
33
33 </%def>
34 </%def>
34
35
35 ## PLAINTEXT VERSION OF BODY
36 ## PLAINTEXT VERSION OF BODY
36 <%def name="body_plaintext()" filter="n,trim">
37 <%def name="body_plaintext()" filter="n,trim">
37 <%
38 <%
38 data = {
39 data = {
39 'user': h.person(user),
40 'user': h.person(user),
40 'repo_name': repo_name,
41 'repo_name': repo_name,
41 'status': status_change,
42 'status': status_change,
42 'comment_file': comment_file,
43 'comment_file': comment_file,
43 'comment_line': comment_line,
44 'comment_line': comment_line,
44 'comment_type': comment_type,
45 'comment_type': comment_type,
45 'comment_id': comment_id,
46 'comment_id': comment_id,
46
47
47 'pr_title': pull_request.title,
48 'pr_title': pull_request.title,
48 'pr_id': pull_request.pull_request_id,
49 'pr_id': pull_request.pull_request_id,
49 'source_ref_type': pull_request.source_ref_parts.type,
50 'source_ref_type': pull_request.source_ref_parts.type,
50 'source_ref_name': pull_request.source_ref_parts.name,
51 'source_ref_name': pull_request.source_ref_parts.name,
51 'target_ref_type': pull_request.target_ref_parts.type,
52 'target_ref_type': pull_request.target_ref_parts.type,
52 'target_ref_name': pull_request.target_ref_parts.name,
53 'target_ref_name': pull_request.target_ref_parts.name,
53 'source_repo': pull_request_source_repo.repo_name,
54 'source_repo': pull_request_source_repo.repo_name,
54 'target_repo': pull_request_target_repo.repo_name,
55 'target_repo': pull_request_target_repo.repo_name,
55 'source_repo_url': pull_request_source_repo_url,
56 'source_repo_url': pull_request_source_repo_url,
56 'target_repo_url': pull_request_target_repo_url,
57 'target_repo_url': pull_request_target_repo_url,
57 }
58 }
58 %>
59 %>
59
60
60 * ${_('Comment link')}: ${pr_comment_url}
61 * ${_('Comment link')}: ${pr_comment_url}
61
62
62 * ${_('Pull Request')}: !${pull_request.pull_request_id}
63 * ${_('Pull Request')}: !${pull_request.pull_request_id}
63
64
64 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
65 * ${h.literal(_('Commit flow: {source_ref_type}:{source_ref_name} of {source_repo_url} into {target_ref_type}:{target_ref_name} of {target_repo_url}').format(**data))}
65
66
66 %if status_change and not closing_pr:
67 %if status_change and not closing_pr:
67 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
68 * ${_('{user} submitted pull request !{pr_id} status: *{status}*').format(**data)}
68
69
69 %elif status_change and closing_pr:
70 %elif status_change and closing_pr:
70 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
71 * ${_('{user} submitted pull request !{pr_id} status: *{status} and closed*').format(**data)}
71
72
72 %endif
73 %endif
73 %if comment_file:
74 %if comment_file:
74 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
75 * ${_('File: {comment_file} on line {comment_line}').format(**data)}
75
76
76 %endif
77 %endif
77 % if comment_type == 'todo':
78 % if comment_type == 'todo':
78 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
79 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
79 % else:
80 % else:
80 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
81 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
81 % endif
82 % endif
82
83
83 ${comment_body |n, trim}
84 ${comment_body |n, trim}
84
85
85 ---
86 ---
86 ${self.plaintext_footer()}
87 ${self.plaintext_footer()}
87 </%def>
88 </%def>
88
89
89
90
90 <%
91 <%
91 data = {
92 data = {
92 'user': h.person(user),
93 'user': h.person(user),
93 'comment_file': comment_file,
94 'comment_file': comment_file,
94 'comment_line': comment_line,
95 'comment_line': comment_line,
95 'comment_type': comment_type,
96 'comment_type': comment_type,
96 'comment_id': comment_id,
97 'comment_id': comment_id,
97 'renderer_type': renderer_type or 'plain',
98 'renderer_type': renderer_type or 'plain',
98
99
99 'pr_title': pull_request.title,
100 'pr_title': pull_request.title,
100 'pr_id': pull_request.pull_request_id,
101 'pr_id': pull_request.pull_request_id,
101 'status': status_change,
102 'status': status_change,
102 'source_ref_type': pull_request.source_ref_parts.type,
103 'source_ref_type': pull_request.source_ref_parts.type,
103 'source_ref_name': pull_request.source_ref_parts.name,
104 'source_ref_name': pull_request.source_ref_parts.name,
104 'target_ref_type': pull_request.target_ref_parts.type,
105 'target_ref_type': pull_request.target_ref_parts.type,
105 'target_ref_name': pull_request.target_ref_parts.name,
106 'target_ref_name': pull_request.target_ref_parts.name,
106 'source_repo': pull_request_source_repo.repo_name,
107 'source_repo': pull_request_source_repo.repo_name,
107 'target_repo': pull_request_target_repo.repo_name,
108 'target_repo': pull_request_target_repo.repo_name,
108 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
109 'source_repo_url': h.link_to(pull_request_source_repo.repo_name, pull_request_source_repo_url),
109 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
110 'target_repo_url': h.link_to(pull_request_target_repo.repo_name, pull_request_target_repo_url),
110 }
111 }
111 %>
112 %>
112
113
113 ## header
114 ## header
114 <table style="text-align:left;vertical-align:middle;width: 100%">
115 <table style="text-align:left;vertical-align:middle;width: 100%">
115 <tr>
116 <tr>
116 <td style="width:100%;border-bottom:1px solid #dbd9da;">
117 <td style="width:100%;border-bottom:1px solid #dbd9da;">
117
118
118 <div style="margin: 0; font-weight: bold">
119 <div style="margin: 0; font-weight: bold">
119 <div class="clear-both" style="margin-bottom: 4px">
120 <div class="clear-both" style="margin-bottom: 4px">
120 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
121 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
121 ${_('left a')}
122 ${_('left a')}
122 <a href="${pr_comment_url}" style="${base.link_css()}">
123 <a href="${pr_comment_url}" style="${base.link_css()}">
123 % if comment_file:
124 % if comment_file:
124 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
125 ${_('{comment_type} on file `{comment_file}` in pull request.').format(**data)}
125 % else:
126 % else:
126 ${_('{comment_type} on pull request.').format(**data) |n}
127 ${_('{comment_type} on pull request.').format(**data) |n}
127 % endif
128 % endif
128 </a>
129 </a>
129 </div>
130 </div>
130 <div style="margin-top: 10px"></div>
131 <div style="margin-top: 10px"></div>
131 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
132 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
132 </div>
133 </div>
133
134
134 </td>
135 </td>
135 </tr>
136 </tr>
136
137
137 </table>
138 </table>
138 <div class="clear-both"></div>
139 <div class="clear-both"></div>
139 ## main body
140 ## main body
140 <table style="text-align:left;vertical-align:middle;width: 100%">
141 <table style="text-align:left;vertical-align:middle;width: 100%">
141
142
142 ## spacing def
143 ## spacing def
143 <tr>
144 <tr>
144 <td style="width: 130px"></td>
145 <td style="width: 130px"></td>
145 <td></td>
146 <td></td>
146 </tr>
147 </tr>
147
148
148 % if status_change:
149 % if status_change:
149 <tr>
150 <tr>
150 <td style="padding-right:20px;">${_('Review Status')}:</td>
151 <td style="padding-right:20px;">${_('Review Status')}:</td>
151 <td>
152 <td>
152 % if closing_pr:
153 % if closing_pr:
153 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
154 ${_('Closed pull request with status')}: ${base.status_text(status_change, tag_type=status_change_type)}
154 % else:
155 % else:
155 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
156 ${_('Submitted review status')}: ${base.status_text(status_change, tag_type=status_change_type)}
156 % endif
157 % endif
157 </td>
158 </td>
158 </tr>
159 </tr>
159 % endif
160 % endif
160 <tr>
161 <tr>
161 <td style="padding-right:20px;">${_('Pull request')}:</td>
162 <td style="padding-right:20px;">${_('Pull request')}:</td>
162 <td>
163 <td>
163 <a href="${pull_request_url}" style="${base.link_css()}">
164 <a href="${pull_request_url}" style="${base.link_css()}">
164 !${pull_request.pull_request_id}
165 !${pull_request.pull_request_id}
165 </a>
166 </a>
166 </td>
167 </td>
167 </tr>
168 </tr>
168
169
169 <tr>
170 <tr>
170 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
171 <td style="padding-right:20px;line-height:20px;">${_('Commit Flow')}:</td>
171 <td style="line-height:20px;">
172 <td style="line-height:20px;">
172 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
173 <code>${'{}:{}'.format(data['source_ref_type'], pull_request.source_ref_parts.name)}</code> ${_('of')} ${data['source_repo_url']}
173 &rarr;
174 &rarr;
174 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
175 <code>${'{}:{}'.format(data['target_ref_type'], pull_request.target_ref_parts.name)}</code> ${_('of')} ${data['target_repo_url']}
175 </td>
176 </td>
176 </tr>
177 </tr>
177
178
178 % if comment_file:
179 % if comment_file:
179 <tr>
180 <tr>
180 <td style="padding-right:20px;">${_('File')}:</td>
181 <td style="padding-right:20px;">${_('File')}:</td>
181 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
182 <td><a href="${pr_comment_url}" style="${base.link_css()}">${_('`{comment_file}` on line {comment_line}').format(**data)}</a></td>
182 </tr>
183 </tr>
183 % endif
184 % endif
184
185
185 <tr style="border-bottom:1px solid #dbd9da;">
186 <tr style="border-bottom:1px solid #dbd9da;">
186 <td colspan="2" style="padding-right:20px;">
187 <td colspan="2" style="padding-right:20px;">
187 % if comment_type == 'todo':
188 % if comment_type == 'todo':
188 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
189 ${('Inline' if comment_file else 'General')} ${_('`TODO` number')} ${comment_id}:
189 % else:
190 % else:
190 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
191 ${('Inline' if comment_file else 'General')} ${_('`Note` number')} ${comment_id}:
191 % endif
192 % endif
192 </td>
193 </td>
193 </tr>
194 </tr>
194
195
195 <tr>
196 <tr>
196 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
197 <td colspan="2" style="background: #F7F7F7">${h.render(comment_body, renderer=data['renderer_type'], mentions=True)}</td>
197 </tr>
198 </tr>
198
199
199 <tr>
200 <tr>
200 <td><a href="${pr_comment_reply_url}">${_('Reply')}</a></td>
201 <td><a href="${pr_comment_reply_url}">${_('Reply')}</a></td>
201 <td></td>
202 <td></td>
202 </tr>
203 </tr>
203 </table>
204 </table>
@@ -1,22 +1,18 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.mako"/>
2 <%inherit file="base.mako"/>
3
3
4 <%def name="subject()" filter="n,trim,whitespace_filter">
4 <%def name="subject()" filter="n,trim,whitespace_filter">
5 Test "Subject" ${_('hello "world"')|n}
5 Test "Subject" ${_('hello "world"')|n}
6 </%def>
6 </%def>
7
7
8 <%def name="headers()" filter="n,trim">
9 X=Y
10 </%def>
11
12 ## plain text version of the email. Empty by default
8 ## plain text version of the email. Empty by default
13 <%def name="body_plaintext()" filter="n,trim">
9 <%def name="body_plaintext()" filter="n,trim">
14 Email Plaintext Body
10 Email Plaintext Body
15 </%def>
11 </%def>
16
12
17 ## BODY GOES BELOW
13 ## BODY GOES BELOW
18 <strong>Email Body</strong>
14 <strong>Email Body</strong>
19 <br/>
15 <br/>
20 <br/>
16 <br/>
21 `h.short_id()`: ${h.short_id('0' * 40)}<br/>
17 `h.short_id()`: ${h.short_id('0' * 40)}<br/>
22 ${_('Translation String')}<br/>
18 ${_('Translation String')}<br/>
@@ -1,194 +1,190 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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 pytest
21 import pytest
22 import collections
22 import collections
23
23
24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
25 from rhodecode.lib.utils2 import AttributeDict
25 from rhodecode.lib.utils2 import AttributeDict
26 from rhodecode.model.db import User
26 from rhodecode.model.db import User
27 from rhodecode.model.notification import EmailNotificationModel
27 from rhodecode.model.notification import EmailNotificationModel
28
28
29
29
30 def test_get_template_obj(app, request_stub):
30 def test_get_template_obj(app, request_stub):
31 template = EmailNotificationModel().get_renderer(
31 template = EmailNotificationModel().get_renderer(
32 EmailNotificationModel.TYPE_TEST, request_stub)
32 EmailNotificationModel.TYPE_TEST, request_stub)
33 assert isinstance(template, PyramidPartialRenderer)
33 assert isinstance(template, PyramidPartialRenderer)
34
34
35
35
36 def test_render_email(app, http_host_only_stub):
36 def test_render_email(app, http_host_only_stub):
37 kwargs = {}
37 kwargs = {}
38 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
38 subject, body, body_plaintext = EmailNotificationModel().render_email(
39 EmailNotificationModel.TYPE_TEST, **kwargs)
39 EmailNotificationModel.TYPE_TEST, **kwargs)
40
40
41 # subject
41 # subject
42 assert subject == 'Test "Subject" hello "world"'
42 assert subject == 'Test "Subject" hello "world"'
43
43
44 # headers
45 assert headers == 'X=Y'
46
47 # body plaintext
44 # body plaintext
48 assert body_plaintext == 'Email Plaintext Body'
45 assert body_plaintext == 'Email Plaintext Body'
49
46
50 # body
47 # body
51 notification_footer1 = 'This is a notification from RhodeCode.'
48 notification_footer1 = 'This is a notification from RhodeCode.'
52 notification_footer2 = 'http://{}/'.format(http_host_only_stub)
49 notification_footer2 = 'http://{}/'.format(http_host_only_stub)
53 assert notification_footer1 in body
50 assert notification_footer1 in body
54 assert notification_footer2 in body
51 assert notification_footer2 in body
55 assert 'Email Body' in body
52 assert 'Email Body' in body
56
53
57
54
58 def test_render_pr_email(app, user_admin):
55 def test_render_pr_email(app, user_admin):
59 ref = collections.namedtuple(
56 ref = collections.namedtuple(
60 'Ref', 'name, type')('fxies123', 'book')
57 'Ref', 'name, type')('fxies123', 'book')
61
58
62 pr = collections.namedtuple('PullRequest',
59 pr = collections.namedtuple('PullRequest',
63 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
60 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
64 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
61 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
65
62
66 source_repo = target_repo = collections.namedtuple(
63 source_repo = target_repo = collections.namedtuple(
67 'Repo', 'type, repo_name')('hg', 'pull_request_1')
64 'Repo', 'type, repo_name')('hg', 'pull_request_1')
68
65
69 kwargs = {
66 kwargs = {
70 'user': User.get_first_super_admin(),
67 'user': User.get_first_super_admin(),
71 'pull_request': pr,
68 'pull_request': pr,
72 'pull_request_commits': [],
69 'pull_request_commits': [],
73
70
74 'pull_request_target_repo': target_repo,
71 'pull_request_target_repo': target_repo,
75 'pull_request_target_repo_url': 'x',
72 'pull_request_target_repo_url': 'x',
76
73
77 'pull_request_source_repo': source_repo,
74 'pull_request_source_repo': source_repo,
78 'pull_request_source_repo_url': 'x',
75 'pull_request_source_repo_url': 'x',
79
76
80 'pull_request_url': 'http://localhost/pr1',
77 'pull_request_url': 'http://localhost/pr1',
81 }
78 }
82
79
83 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
80 subject, body, body_plaintext = EmailNotificationModel().render_email(
84 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
81 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
85
82
86 # subject
83 # subject
87 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
84 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
88
85
89
86
90 def test_render_pr_update_email(app, user_admin):
87 def test_render_pr_update_email(app, user_admin):
91 ref = collections.namedtuple(
88 ref = collections.namedtuple(
92 'Ref', 'name, type')('fxies123', 'book')
89 'Ref', 'name, type')('fxies123', 'book')
93
90
94 pr = collections.namedtuple('PullRequest',
91 pr = collections.namedtuple('PullRequest',
95 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
92 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
96 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
93 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
97
94
98 source_repo = target_repo = collections.namedtuple(
95 source_repo = target_repo = collections.namedtuple(
99 'Repo', 'type, repo_name')('hg', 'pull_request_1')
96 'Repo', 'type, repo_name')('hg', 'pull_request_1')
100
97
101 commit_changes = AttributeDict({
98 commit_changes = AttributeDict({
102 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
99 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
103 'removed': ['eeeeeeeeeee'],
100 'removed': ['eeeeeeeeeee'],
104 })
101 })
105 file_changes = AttributeDict({
102 file_changes = AttributeDict({
106 'added': ['a/file1.md', 'file2.py'],
103 'added': ['a/file1.md', 'file2.py'],
107 'modified': ['b/modified_file.rst'],
104 'modified': ['b/modified_file.rst'],
108 'removed': ['.idea'],
105 'removed': ['.idea'],
109 })
106 })
110
107
111 kwargs = {
108 kwargs = {
112 'updating_user': User.get_first_super_admin(),
109 'updating_user': User.get_first_super_admin(),
113
110
114 'pull_request': pr,
111 'pull_request': pr,
115 'pull_request_commits': [],
112 'pull_request_commits': [],
116
113
117 'pull_request_target_repo': target_repo,
114 'pull_request_target_repo': target_repo,
118 'pull_request_target_repo_url': 'x',
115 'pull_request_target_repo_url': 'x',
119
116
120 'pull_request_source_repo': source_repo,
117 'pull_request_source_repo': source_repo,
121 'pull_request_source_repo_url': 'x',
118 'pull_request_source_repo_url': 'x',
122
119
123 'pull_request_url': 'http://localhost/pr1',
120 'pull_request_url': 'http://localhost/pr1',
124
121
125 'pr_comment_url': 'http://comment-url',
122 'pr_comment_url': 'http://comment-url',
126 'pr_comment_reply_url': 'http://comment-url#reply',
123 'pr_comment_reply_url': 'http://comment-url#reply',
127 'ancestor_commit_id': 'f39bd443',
124 'ancestor_commit_id': 'f39bd443',
128 'added_commits': commit_changes.added,
125 'added_commits': commit_changes.added,
129 'removed_commits': commit_changes.removed,
126 'removed_commits': commit_changes.removed,
130 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
127 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
131 'added_files': file_changes.added,
128 'added_files': file_changes.added,
132 'modified_files': file_changes.modified,
129 'modified_files': file_changes.modified,
133 'removed_files': file_changes.removed,
130 'removed_files': file_changes.removed,
134 }
131 }
135
132
136 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
133 subject, body, body_plaintext = EmailNotificationModel().render_email(
137 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **kwargs)
134 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **kwargs)
138
135
139 # subject
136 # subject
140 assert subject == '@test_admin (RhodeCode Admin) updated pull request. !200: "Example Pull Request"'
137 assert subject == '@test_admin (RhodeCode Admin) updated pull request. !200: "Example Pull Request"'
141
138
142
139
143 @pytest.mark.parametrize('mention', [
140 @pytest.mark.parametrize('mention', [
144 True,
141 True,
145 False
142 False
146 ])
143 ])
147 @pytest.mark.parametrize('email_type', [
144 @pytest.mark.parametrize('email_type', [
148 EmailNotificationModel.TYPE_COMMIT_COMMENT,
145 EmailNotificationModel.TYPE_COMMIT_COMMENT,
149 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
146 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
150 ])
147 ])
151 def test_render_comment_subject_no_newlines(app, mention, email_type):
148 def test_render_comment_subject_no_newlines(app, mention, email_type):
152 ref = collections.namedtuple(
149 ref = collections.namedtuple(
153 'Ref', 'name, type')('fxies123', 'book')
150 'Ref', 'name, type')('fxies123', 'book')
154
151
155 pr = collections.namedtuple('PullRequest',
152 pr = collections.namedtuple('PullRequest',
156 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
153 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
157 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
154 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
158
155
159 source_repo = target_repo = collections.namedtuple(
156 source_repo = target_repo = collections.namedtuple(
160 'Repo', 'type, repo_name')('hg', 'pull_request_1')
157 'Repo', 'type, repo_name')('hg', 'pull_request_1')
161
158
162 kwargs = {
159 kwargs = {
163 'user': User.get_first_super_admin(),
160 'user': User.get_first_super_admin(),
164 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
161 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
165 'status_change': 'approved',
162 'status_change': 'approved',
166 'commit_target_repo_url': 'http://foo.example.com/#comment1',
163 'commit_target_repo_url': 'http://foo.example.com/#comment1',
167 'repo_name': 'test-repo',
164 'repo_name': 'test-repo',
168 'comment_file': 'test-file.py',
165 'comment_file': 'test-file.py',
169 'comment_line': 'n100',
166 'comment_line': 'n100',
170 'comment_type': 'note',
167 'comment_type': 'note',
171 'comment_id': 2048,
168 'comment_id': 2048,
172 'commit_comment_url': 'http://comment-url',
169 'commit_comment_url': 'http://comment-url',
173 'commit_comment_reply_url': 'http://comment-url/#Reply',
170 'commit_comment_reply_url': 'http://comment-url/#Reply',
174 'instance_url': 'http://rc-instance',
171 'instance_url': 'http://rc-instance',
175 'comment_body': 'hello world',
172 'comment_body': 'hello world',
176 'mention': mention,
173 'mention': mention,
177
174
178 'pr_comment_url': 'http://comment-url',
175 'pr_comment_url': 'http://comment-url',
179 'pr_comment_reply_url': 'http://comment-url/#Reply',
176 'pr_comment_reply_url': 'http://comment-url/#Reply',
180 'pull_request': pr,
177 'pull_request': pr,
181 'pull_request_commits': [],
178 'pull_request_commits': [],
182
179
183 'pull_request_target_repo': target_repo,
180 'pull_request_target_repo': target_repo,
184 'pull_request_target_repo_url': 'x',
181 'pull_request_target_repo_url': 'x',
185
182
186 'pull_request_source_repo': source_repo,
183 'pull_request_source_repo': source_repo,
187 'pull_request_source_repo_url': 'x',
184 'pull_request_source_repo_url': 'x',
188
185
189 'pull_request_url': 'http://code.rc.com/_pr/123'
186 'pull_request_url': 'http://code.rc.com/_pr/123'
190 }
187 }
191 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
188 subject, body, body_plaintext = EmailNotificationModel().render_email(email_type, **kwargs)
192 email_type, **kwargs)
193
189
194 assert '\n' not in subject
190 assert '\n' not in subject
General Comments 0
You need to be logged in to leave comments. Login now