##// END OF EJS Templates
core: no longer rely on webob exception inside get_or_404 function....
marcink -
r1956:a7a935de default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,416 +1,417 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 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 time
21 import time
22 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25
25
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
27 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
28 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
28 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
29 from rhodecode.model import repo
29 from rhodecode.model import repo
30 from rhodecode.model import repo_group
30 from rhodecode.model import repo_group
31 from rhodecode.model.db import User
31 from rhodecode.model.db import User
32 from rhodecode.model.scm import ScmModel
32 from rhodecode.model.scm import ScmModel
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 ADMIN_PREFIX = '/_admin'
37 ADMIN_PREFIX = '/_admin'
38 STATIC_FILE_PREFIX = '/_static'
38 STATIC_FILE_PREFIX = '/_static'
39
39
40 URL_NAME_REQUIREMENTS = {
40 URL_NAME_REQUIREMENTS = {
41 # group name can have a slash in them, but they must not end with a slash
41 # group name can have a slash in them, but they must not end with a slash
42 'group_name': r'.*?[^/]',
42 'group_name': r'.*?[^/]',
43 'repo_group_name': r'.*?[^/]',
43 'repo_group_name': r'.*?[^/]',
44 # repo names can have a slash in them, but they must not end with a slash
44 # repo names can have a slash in them, but they must not end with a slash
45 'repo_name': r'.*?[^/]',
45 'repo_name': r'.*?[^/]',
46 # file path eats up everything at the end
46 # file path eats up everything at the end
47 'f_path': r'.*',
47 'f_path': r'.*',
48 # reference types
48 # reference types
49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
49 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
50 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
51 }
51 }
52
52
53
53
54 def add_route_with_slash(config,name, pattern, **kw):
54 def add_route_with_slash(config,name, pattern, **kw):
55 config.add_route(name, pattern, **kw)
55 config.add_route(name, pattern, **kw)
56 if not pattern.endswith('/'):
56 if not pattern.endswith('/'):
57 config.add_route(name + '_slash', pattern + '/', **kw)
57 config.add_route(name + '_slash', pattern + '/', **kw)
58
58
59
59
60 def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS):
60 def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS):
61 """
61 """
62 Adds regex requirements to pyramid routes using a mapping dict
62 Adds regex requirements to pyramid routes using a mapping dict
63 e.g::
63 e.g::
64 add_route_requirements('{repo_name}/settings')
64 add_route_requirements('{repo_name}/settings')
65 """
65 """
66 for key, regex in requirements.items():
66 for key, regex in requirements.items():
67 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
67 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
68 return route_path
68 return route_path
69
69
70
70
71 def get_format_ref_id(repo):
71 def get_format_ref_id(repo):
72 """Returns a `repo` specific reference formatter function"""
72 """Returns a `repo` specific reference formatter function"""
73 if h.is_svn(repo):
73 if h.is_svn(repo):
74 return _format_ref_id_svn
74 return _format_ref_id_svn
75 else:
75 else:
76 return _format_ref_id
76 return _format_ref_id
77
77
78
78
79 def _format_ref_id(name, raw_id):
79 def _format_ref_id(name, raw_id):
80 """Default formatting of a given reference `name`"""
80 """Default formatting of a given reference `name`"""
81 return name
81 return name
82
82
83
83
84 def _format_ref_id_svn(name, raw_id):
84 def _format_ref_id_svn(name, raw_id):
85 """Special way of formatting a reference for Subversion including path"""
85 """Special way of formatting a reference for Subversion including path"""
86 return '%s@%s' % (name, raw_id)
86 return '%s@%s' % (name, raw_id)
87
87
88
88
89 class TemplateArgs(StrictAttributeDict):
89 class TemplateArgs(StrictAttributeDict):
90 pass
90 pass
91
91
92
92
93 class BaseAppView(object):
93 class BaseAppView(object):
94
94
95 def __init__(self, context, request):
95 def __init__(self, context, request):
96 self.request = request
96 self.request = request
97 self.context = context
97 self.context = context
98 self.session = request.session
98 self.session = request.session
99 self._rhodecode_user = request.user # auth user
99 self._rhodecode_user = request.user # auth user
100 self._rhodecode_db_user = self._rhodecode_user.get_instance()
100 self._rhodecode_db_user = self._rhodecode_user.get_instance()
101 self._maybe_needs_password_change(
101 self._maybe_needs_password_change(
102 request.matched_route.name, self._rhodecode_db_user)
102 request.matched_route.name, self._rhodecode_db_user)
103
103
104 def _maybe_needs_password_change(self, view_name, user_obj):
104 def _maybe_needs_password_change(self, view_name, user_obj):
105 log.debug('Checking if user %s needs password change on view %s',
105 log.debug('Checking if user %s needs password change on view %s',
106 user_obj, view_name)
106 user_obj, view_name)
107 skip_user_views = [
107 skip_user_views = [
108 'logout', 'login',
108 'logout', 'login',
109 'my_account_password', 'my_account_password_update'
109 'my_account_password', 'my_account_password_update'
110 ]
110 ]
111
111
112 if not user_obj:
112 if not user_obj:
113 return
113 return
114
114
115 if user_obj.username == User.DEFAULT_USER:
115 if user_obj.username == User.DEFAULT_USER:
116 return
116 return
117
117
118 now = time.time()
118 now = time.time()
119 should_change = user_obj.user_data.get('force_password_change')
119 should_change = user_obj.user_data.get('force_password_change')
120 change_after = safe_int(should_change) or 0
120 change_after = safe_int(should_change) or 0
121 if should_change and now > change_after:
121 if should_change and now > change_after:
122 log.debug('User %s requires password change', user_obj)
122 log.debug('User %s requires password change', user_obj)
123 h.flash('You are required to change your password', 'warning',
123 h.flash('You are required to change your password', 'warning',
124 ignore_duplicate=True)
124 ignore_duplicate=True)
125
125
126 if view_name not in skip_user_views:
126 if view_name not in skip_user_views:
127 raise HTTPFound(
127 raise HTTPFound(
128 self.request.route_path('my_account_password'))
128 self.request.route_path('my_account_password'))
129
129
130 def _get_local_tmpl_context(self, include_app_defaults=False):
130 def _get_local_tmpl_context(self, include_app_defaults=False):
131 c = TemplateArgs()
131 c = TemplateArgs()
132 c.auth_user = self.request.user
132 c.auth_user = self.request.user
133 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
133 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
134 c.rhodecode_user = self.request.user
134 c.rhodecode_user = self.request.user
135
135
136 if include_app_defaults:
136 if include_app_defaults:
137 # NOTE(marcink): after full pyramid migration include_app_defaults
137 # NOTE(marcink): after full pyramid migration include_app_defaults
138 # should be turned on by default
138 # should be turned on by default
139 from rhodecode.lib.base import attach_context_attributes
139 from rhodecode.lib.base import attach_context_attributes
140 attach_context_attributes(c, self.request, self.request.user.user_id)
140 attach_context_attributes(c, self.request, self.request.user.user_id)
141
141
142 return c
142 return c
143
143
144 def _register_global_c(self, tmpl_args):
144 def _register_global_c(self, tmpl_args):
145 """
145 """
146 Registers attributes to pylons global `c`
146 Registers attributes to pylons global `c`
147 """
147 """
148
148
149 # TODO(marcink): remove once pyramid migration is finished
149 # TODO(marcink): remove once pyramid migration is finished
150 from pylons import tmpl_context as c
150 from pylons import tmpl_context as c
151 try:
151 try:
152 for k, v in tmpl_args.items():
152 for k, v in tmpl_args.items():
153 setattr(c, k, v)
153 setattr(c, k, v)
154 except TypeError:
154 except TypeError:
155 log.exception('Failed to register pylons C')
155 log.exception('Failed to register pylons C')
156 pass
156 pass
157
157
158 def _get_template_context(self, tmpl_args):
158 def _get_template_context(self, tmpl_args):
159 self._register_global_c(tmpl_args)
159 self._register_global_c(tmpl_args)
160
160
161 local_tmpl_args = {
161 local_tmpl_args = {
162 'defaults': {},
162 'defaults': {},
163 'errors': {},
163 'errors': {},
164 # register a fake 'c' to be used in templates instead of global
164 # register a fake 'c' to be used in templates instead of global
165 # pylons c, after migration to pyramid we should rename it to 'c'
165 # pylons c, after migration to pyramid we should rename it to 'c'
166 # make sure we replace usage of _c in templates too
166 # make sure we replace usage of _c in templates too
167 '_c': tmpl_args
167 '_c': tmpl_args
168 }
168 }
169 local_tmpl_args.update(tmpl_args)
169 local_tmpl_args.update(tmpl_args)
170 return local_tmpl_args
170 return local_tmpl_args
171
171
172 def load_default_context(self):
172 def load_default_context(self):
173 """
173 """
174 example:
174 example:
175
175
176 def load_default_context(self):
176 def load_default_context(self):
177 c = self._get_local_tmpl_context()
177 c = self._get_local_tmpl_context()
178 c.custom_var = 'foobar'
178 c.custom_var = 'foobar'
179 self._register_global_c(c)
179 self._register_global_c(c)
180 return c
180 return c
181 """
181 """
182 raise NotImplementedError('Needs implementation in view class')
182 raise NotImplementedError('Needs implementation in view class')
183
183
184
184
185 class RepoAppView(BaseAppView):
185 class RepoAppView(BaseAppView):
186
186
187 def __init__(self, context, request):
187 def __init__(self, context, request):
188 super(RepoAppView, self).__init__(context, request)
188 super(RepoAppView, self).__init__(context, request)
189 self.db_repo = request.db_repo
189 self.db_repo = request.db_repo
190 self.db_repo_name = self.db_repo.repo_name
190 self.db_repo_name = self.db_repo.repo_name
191 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
191 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
192
192
193 def _handle_missing_requirements(self, error):
193 def _handle_missing_requirements(self, error):
194 log.error(
194 log.error(
195 'Requirements are missing for repository %s: %s',
195 'Requirements are missing for repository %s: %s',
196 self.db_repo_name, error.message)
196 self.db_repo_name, error.message)
197
197
198 def _get_local_tmpl_context(self, include_app_defaults=False):
198 def _get_local_tmpl_context(self, include_app_defaults=False):
199 c = super(RepoAppView, self)._get_local_tmpl_context(
199 c = super(RepoAppView, self)._get_local_tmpl_context(
200 include_app_defaults=include_app_defaults)
200 include_app_defaults=include_app_defaults)
201
201
202 # register common vars for this type of view
202 # register common vars for this type of view
203 c.rhodecode_db_repo = self.db_repo
203 c.rhodecode_db_repo = self.db_repo
204 c.repo_name = self.db_repo_name
204 c.repo_name = self.db_repo_name
205 c.repository_pull_requests = self.db_repo_pull_requests
205 c.repository_pull_requests = self.db_repo_pull_requests
206
206
207 c.repository_requirements_missing = False
207 c.repository_requirements_missing = False
208 try:
208 try:
209 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
209 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
210 except RepositoryRequirementError as e:
210 except RepositoryRequirementError as e:
211 c.repository_requirements_missing = True
211 c.repository_requirements_missing = True
212 self._handle_missing_requirements(e)
212 self._handle_missing_requirements(e)
213
213
214 return c
214 return c
215
215
216 def _get_f_path(self, matchdict, default=None):
216 def _get_f_path(self, matchdict, default=None):
217 f_path = matchdict.get('f_path')
217 f_path = matchdict.get('f_path')
218 if f_path:
218 if f_path:
219 # fix for multiple initial slashes that causes errors for GIT
219 # fix for multiple initial slashes that causes errors for GIT
220 return f_path.lstrip('/')
220 return f_path.lstrip('/')
221
221
222 return default
222 return default
223
223
224
224 class DataGridAppView(object):
225 class DataGridAppView(object):
225 """
226 """
226 Common class to have re-usable grid rendering components
227 Common class to have re-usable grid rendering components
227 """
228 """
228
229
229 def _extract_ordering(self, request, column_map=None):
230 def _extract_ordering(self, request, column_map=None):
230 column_map = column_map or {}
231 column_map = column_map or {}
231 column_index = safe_int(request.GET.get('order[0][column]'))
232 column_index = safe_int(request.GET.get('order[0][column]'))
232 order_dir = request.GET.get(
233 order_dir = request.GET.get(
233 'order[0][dir]', 'desc')
234 'order[0][dir]', 'desc')
234 order_by = request.GET.get(
235 order_by = request.GET.get(
235 'columns[%s][data][sort]' % column_index, 'name_raw')
236 'columns[%s][data][sort]' % column_index, 'name_raw')
236
237
237 # translate datatable to DB columns
238 # translate datatable to DB columns
238 order_by = column_map.get(order_by) or order_by
239 order_by = column_map.get(order_by) or order_by
239
240
240 search_q = request.GET.get('search[value]')
241 search_q = request.GET.get('search[value]')
241 return search_q, order_by, order_dir
242 return search_q, order_by, order_dir
242
243
243 def _extract_chunk(self, request):
244 def _extract_chunk(self, request):
244 start = safe_int(request.GET.get('start'), 0)
245 start = safe_int(request.GET.get('start'), 0)
245 length = safe_int(request.GET.get('length'), 25)
246 length = safe_int(request.GET.get('length'), 25)
246 draw = safe_int(request.GET.get('draw'))
247 draw = safe_int(request.GET.get('draw'))
247 return draw, start, length
248 return draw, start, length
248
249
249
250
250 class BaseReferencesView(RepoAppView):
251 class BaseReferencesView(RepoAppView):
251 """
252 """
252 Base for reference view for branches, tags and bookmarks.
253 Base for reference view for branches, tags and bookmarks.
253 """
254 """
254 def load_default_context(self):
255 def load_default_context(self):
255 c = self._get_local_tmpl_context()
256 c = self._get_local_tmpl_context()
256
257
257 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
258 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
258 c.repo_info = self.db_repo
259 c.repo_info = self.db_repo
259
260
260 self._register_global_c(c)
261 self._register_global_c(c)
261 return c
262 return c
262
263
263 def load_refs_context(self, ref_items, partials_template):
264 def load_refs_context(self, ref_items, partials_template):
264 _render = self.request.get_partial_renderer(partials_template)
265 _render = self.request.get_partial_renderer(partials_template)
265 pre_load = ["author", "date", "message"]
266 pre_load = ["author", "date", "message"]
266
267
267 is_svn = h.is_svn(self.rhodecode_vcs_repo)
268 is_svn = h.is_svn(self.rhodecode_vcs_repo)
268 is_hg = h.is_hg(self.rhodecode_vcs_repo)
269 is_hg = h.is_hg(self.rhodecode_vcs_repo)
269
270
270 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
271 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
271
272
272 closed_refs = {}
273 closed_refs = {}
273 if is_hg:
274 if is_hg:
274 closed_refs = self.rhodecode_vcs_repo.branches_closed
275 closed_refs = self.rhodecode_vcs_repo.branches_closed
275
276
276 data = []
277 data = []
277 for ref_name, commit_id in ref_items:
278 for ref_name, commit_id in ref_items:
278 commit = self.rhodecode_vcs_repo.get_commit(
279 commit = self.rhodecode_vcs_repo.get_commit(
279 commit_id=commit_id, pre_load=pre_load)
280 commit_id=commit_id, pre_load=pre_load)
280 closed = ref_name in closed_refs
281 closed = ref_name in closed_refs
281
282
282 # TODO: johbo: Unify generation of reference links
283 # TODO: johbo: Unify generation of reference links
283 use_commit_id = '/' in ref_name or is_svn
284 use_commit_id = '/' in ref_name or is_svn
284
285
285 if use_commit_id:
286 if use_commit_id:
286 files_url = h.route_path(
287 files_url = h.route_path(
287 'repo_files',
288 'repo_files',
288 repo_name=self.db_repo_name,
289 repo_name=self.db_repo_name,
289 f_path=ref_name if is_svn else '',
290 f_path=ref_name if is_svn else '',
290 commit_id=commit_id)
291 commit_id=commit_id)
291
292
292 else:
293 else:
293 files_url = h.route_path(
294 files_url = h.route_path(
294 'repo_files',
295 'repo_files',
295 repo_name=self.db_repo_name,
296 repo_name=self.db_repo_name,
296 f_path=ref_name if is_svn else '',
297 f_path=ref_name if is_svn else '',
297 commit_id=ref_name,
298 commit_id=ref_name,
298 _query=dict(at=ref_name))
299 _query=dict(at=ref_name))
299
300
300 data.append({
301 data.append({
301 "name": _render('name', ref_name, files_url, closed),
302 "name": _render('name', ref_name, files_url, closed),
302 "name_raw": ref_name,
303 "name_raw": ref_name,
303 "date": _render('date', commit.date),
304 "date": _render('date', commit.date),
304 "date_raw": datetime_to_time(commit.date),
305 "date_raw": datetime_to_time(commit.date),
305 "author": _render('author', commit.author),
306 "author": _render('author', commit.author),
306 "commit": _render(
307 "commit": _render(
307 'commit', commit.message, commit.raw_id, commit.idx),
308 'commit', commit.message, commit.raw_id, commit.idx),
308 "commit_raw": commit.idx,
309 "commit_raw": commit.idx,
309 "compare": _render(
310 "compare": _render(
310 'compare', format_ref_id(ref_name, commit.raw_id)),
311 'compare', format_ref_id(ref_name, commit.raw_id)),
311 })
312 })
312
313
313 return data
314 return data
314
315
315
316
316 class RepoRoutePredicate(object):
317 class RepoRoutePredicate(object):
317 def __init__(self, val, config):
318 def __init__(self, val, config):
318 self.val = val
319 self.val = val
319
320
320 def text(self):
321 def text(self):
321 return 'repo_route = %s' % self.val
322 return 'repo_route = %s' % self.val
322
323
323 phash = text
324 phash = text
324
325
325 def __call__(self, info, request):
326 def __call__(self, info, request):
326
327
327 if hasattr(request, 'vcs_call'):
328 if hasattr(request, 'vcs_call'):
328 # skip vcs calls
329 # skip vcs calls
329 return
330 return
330
331
331 repo_name = info['match']['repo_name']
332 repo_name = info['match']['repo_name']
332 repo_model = repo.RepoModel()
333 repo_model = repo.RepoModel()
333 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
334 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
334
335
335 if by_name_match:
336 if by_name_match:
336 # register this as request object we can re-use later
337 # register this as request object we can re-use later
337 request.db_repo = by_name_match
338 request.db_repo = by_name_match
338 return True
339 return True
339
340
340 by_id_match = repo_model.get_repo_by_id(repo_name)
341 by_id_match = repo_model.get_repo_by_id(repo_name)
341 if by_id_match:
342 if by_id_match:
342 request.db_repo = by_id_match
343 request.db_repo = by_id_match
343 return True
344 return True
344
345
345 return False
346 return False
346
347
347
348
348 class RepoTypeRoutePredicate(object):
349 class RepoTypeRoutePredicate(object):
349 def __init__(self, val, config):
350 def __init__(self, val, config):
350 self.val = val or ['hg', 'git', 'svn']
351 self.val = val or ['hg', 'git', 'svn']
351
352
352 def text(self):
353 def text(self):
353 return 'repo_accepted_type = %s' % self.val
354 return 'repo_accepted_type = %s' % self.val
354
355
355 phash = text
356 phash = text
356
357
357 def __call__(self, info, request):
358 def __call__(self, info, request):
358 if hasattr(request, 'vcs_call'):
359 if hasattr(request, 'vcs_call'):
359 # skip vcs calls
360 # skip vcs calls
360 return
361 return
361
362
362 rhodecode_db_repo = request.db_repo
363 rhodecode_db_repo = request.db_repo
363
364
364 log.debug(
365 log.debug(
365 '%s checking repo type for %s in %s',
366 '%s checking repo type for %s in %s',
366 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
367 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
367
368
368 if rhodecode_db_repo.repo_type in self.val:
369 if rhodecode_db_repo.repo_type in self.val:
369 return True
370 return True
370 else:
371 else:
371 log.warning('Current view is not supported for repo type:%s',
372 log.warning('Current view is not supported for repo type:%s',
372 rhodecode_db_repo.repo_type)
373 rhodecode_db_repo.repo_type)
373 #
374 #
374 # h.flash(h.literal(
375 # h.flash(h.literal(
375 # _('Action not supported for %s.' % rhodecode_repo.alias)),
376 # _('Action not supported for %s.' % rhodecode_repo.alias)),
376 # category='warning')
377 # category='warning')
377 # return redirect(
378 # return redirect(
378 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
379 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
379
380
380 return False
381 return False
381
382
382
383
383 class RepoGroupRoutePredicate(object):
384 class RepoGroupRoutePredicate(object):
384 def __init__(self, val, config):
385 def __init__(self, val, config):
385 self.val = val
386 self.val = val
386
387
387 def text(self):
388 def text(self):
388 return 'repo_group_route = %s' % self.val
389 return 'repo_group_route = %s' % self.val
389
390
390 phash = text
391 phash = text
391
392
392 def __call__(self, info, request):
393 def __call__(self, info, request):
393 if hasattr(request, 'vcs_call'):
394 if hasattr(request, 'vcs_call'):
394 # skip vcs calls
395 # skip vcs calls
395 return
396 return
396
397
397 repo_group_name = info['match']['repo_group_name']
398 repo_group_name = info['match']['repo_group_name']
398 repo_group_model = repo_group.RepoGroupModel()
399 repo_group_model = repo_group.RepoGroupModel()
399 by_name_match = repo_group_model.get_by_group_name(
400 by_name_match = repo_group_model.get_by_group_name(
400 repo_group_name, cache=True)
401 repo_group_name, cache=True)
401
402
402 if by_name_match:
403 if by_name_match:
403 # register this as request object we can re-use later
404 # register this as request object we can re-use later
404 request.db_repo_group = by_name_match
405 request.db_repo_group = by_name_match
405 return True
406 return True
406
407
407 return False
408 return False
408
409
409
410
410 def includeme(config):
411 def includeme(config):
411 config.add_route_predicate(
412 config.add_route_predicate(
412 'repo_route', RepoRoutePredicate)
413 'repo_route', RepoRoutePredicate)
413 config.add_route_predicate(
414 config.add_route_predicate(
414 'repo_accepted_types', RepoTypeRoutePredicate)
415 'repo_accepted_types', RepoTypeRoutePredicate)
415 config.add_route_predicate(
416 config.add_route_predicate(
416 'repo_group_route', RepoGroupRoutePredicate)
417 'repo_group_route', RepoGroupRoutePredicate)
@@ -1,63 +1,63 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23
23
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26
26
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 from rhodecode.model.db import PullRequest
30 from rhodecode.model.db import PullRequest
31
31
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class AdminMainView(BaseAppView):
36 class AdminMainView(BaseAppView):
37
37
38 @LoginRequired()
38 @LoginRequired()
39 @HasPermissionAllDecorator('hg.admin')
39 @HasPermissionAllDecorator('hg.admin')
40 @view_config(
40 @view_config(
41 route_name='admin_home', request_method='GET')
41 route_name='admin_home', request_method='GET')
42 def admin_main(self):
42 def admin_main(self):
43 # redirect _admin to audit logs...
43 # redirect _admin to audit logs...
44 raise HTTPFound(h.route_path('admin_audit_logs'))
44 raise HTTPFound(h.route_path('admin_audit_logs'))
45
45
46 @LoginRequired()
46 @LoginRequired()
47 @view_config(route_name='pull_requests_global_0', request_method='GET')
47 @view_config(route_name='pull_requests_global_0', request_method='GET')
48 @view_config(route_name='pull_requests_global_1', request_method='GET')
48 @view_config(route_name='pull_requests_global_1', request_method='GET')
49 @view_config(route_name='pull_requests_global', request_method='GET')
49 @view_config(route_name='pull_requests_global', request_method='GET')
50 def pull_requests(self):
50 def pull_requests(self):
51 """
51 """
52 Global redirect for Pull Requests
52 Global redirect for Pull Requests
53
53
54 :param pull_request_id: id of pull requests in the system
54 :param pull_request_id: id of pull requests in the system
55 """
55 """
56
56
57 pull_request_id = self.request.matchdict.get('pull_request_id')
57 pull_request_id = self.request.matchdict.get('pull_request_id')
58 pull_request = PullRequest.get_or_404(pull_request_id, pyramid_exc=True)
58 pull_request = PullRequest.get_or_404(pull_request_id)
59 repo_name = pull_request.target_repo.repo_name
59 repo_name = pull_request.target_repo.repo_name
60
60
61 raise HTTPFound(
61 raise HTTPFound(
62 h.route_path('pullrequest_show', repo_name=repo_name,
62 h.route_path('pullrequest_show', repo_name=repo_name,
63 pull_request_id=pull_request_id))
63 pull_request_id=pull_request_id))
@@ -1,505 +1,505 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23 import formencode
23 import formencode
24
24
25 from pyramid.httpexceptions import HTTPFound
25 from pyramid.httpexceptions import HTTPFound
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from sqlalchemy.sql.functions import coalesce
27 from sqlalchemy.sql.functions import coalesce
28
28
29 from rhodecode.apps._base import BaseAppView, DataGridAppView
29 from rhodecode.apps._base import BaseAppView, DataGridAppView
30
30
31 from rhodecode.lib import audit_logger
31 from rhodecode.lib import audit_logger
32 from rhodecode.lib.ext_json import json
32 from rhodecode.lib.ext_json import json
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
34 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
35 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.lib.utils2 import safe_int, safe_unicode
36 from rhodecode.lib.utils2 import safe_int, safe_unicode
37 from rhodecode.model.auth_token import AuthTokenModel
37 from rhodecode.model.auth_token import AuthTokenModel
38 from rhodecode.model.user import UserModel
38 from rhodecode.model.user import UserModel
39 from rhodecode.model.user_group import UserGroupModel
39 from rhodecode.model.user_group import UserGroupModel
40 from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys
40 from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys
41 from rhodecode.model.meta import Session
41 from rhodecode.model.meta import Session
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 class AdminUsersView(BaseAppView, DataGridAppView):
46 class AdminUsersView(BaseAppView, DataGridAppView):
47 ALLOW_SCOPED_TOKENS = False
47 ALLOW_SCOPED_TOKENS = False
48 """
48 """
49 This view has alternative version inside EE, if modified please take a look
49 This view has alternative version inside EE, if modified please take a look
50 in there as well.
50 in there as well.
51 """
51 """
52
52
53 def load_default_context(self):
53 def load_default_context(self):
54 c = self._get_local_tmpl_context()
54 c = self._get_local_tmpl_context()
55 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
55 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
56 self._register_global_c(c)
56 self._register_global_c(c)
57 return c
57 return c
58
58
59 def _redirect_for_default_user(self, username):
59 def _redirect_for_default_user(self, username):
60 _ = self.request.translate
60 _ = self.request.translate
61 if username == User.DEFAULT_USER:
61 if username == User.DEFAULT_USER:
62 h.flash(_("You can't edit this user"), category='warning')
62 h.flash(_("You can't edit this user"), category='warning')
63 # TODO(marcink): redirect to 'users' admin panel once this
63 # TODO(marcink): redirect to 'users' admin panel once this
64 # is a pyramid view
64 # is a pyramid view
65 raise HTTPFound('/')
65 raise HTTPFound('/')
66
66
67 @HasPermissionAllDecorator('hg.admin')
67 @HasPermissionAllDecorator('hg.admin')
68 @view_config(
68 @view_config(
69 route_name='users', request_method='GET',
69 route_name='users', request_method='GET',
70 renderer='rhodecode:templates/admin/users/users.mako')
70 renderer='rhodecode:templates/admin/users/users.mako')
71 def users_list(self):
71 def users_list(self):
72 c = self.load_default_context()
72 c = self.load_default_context()
73 return self._get_template_context(c)
73 return self._get_template_context(c)
74
74
75 @HasPermissionAllDecorator('hg.admin')
75 @HasPermissionAllDecorator('hg.admin')
76 @view_config(
76 @view_config(
77 # renderer defined below
77 # renderer defined below
78 route_name='users_data', request_method='GET',
78 route_name='users_data', request_method='GET',
79 renderer='json_ext', xhr=True)
79 renderer='json_ext', xhr=True)
80 def users_list_data(self):
80 def users_list_data(self):
81 draw, start, limit = self._extract_chunk(self.request)
81 draw, start, limit = self._extract_chunk(self.request)
82 search_q, order_by, order_dir = self._extract_ordering(self.request)
82 search_q, order_by, order_dir = self._extract_ordering(self.request)
83
83
84 _render = self.request.get_partial_renderer(
84 _render = self.request.get_partial_renderer(
85 'data_table/_dt_elements.mako')
85 'data_table/_dt_elements.mako')
86
86
87 def user_actions(user_id, username):
87 def user_actions(user_id, username):
88 return _render("user_actions", user_id, username)
88 return _render("user_actions", user_id, username)
89
89
90 users_data_total_count = User.query()\
90 users_data_total_count = User.query()\
91 .filter(User.username != User.DEFAULT_USER) \
91 .filter(User.username != User.DEFAULT_USER) \
92 .count()
92 .count()
93
93
94 # json generate
94 # json generate
95 base_q = User.query().filter(User.username != User.DEFAULT_USER)
95 base_q = User.query().filter(User.username != User.DEFAULT_USER)
96
96
97 if search_q:
97 if search_q:
98 like_expression = u'%{}%'.format(safe_unicode(search_q))
98 like_expression = u'%{}%'.format(safe_unicode(search_q))
99 base_q = base_q.filter(or_(
99 base_q = base_q.filter(or_(
100 User.username.ilike(like_expression),
100 User.username.ilike(like_expression),
101 User._email.ilike(like_expression),
101 User._email.ilike(like_expression),
102 User.name.ilike(like_expression),
102 User.name.ilike(like_expression),
103 User.lastname.ilike(like_expression),
103 User.lastname.ilike(like_expression),
104 ))
104 ))
105
105
106 users_data_total_filtered_count = base_q.count()
106 users_data_total_filtered_count = base_q.count()
107
107
108 sort_col = getattr(User, order_by, None)
108 sort_col = getattr(User, order_by, None)
109 if sort_col:
109 if sort_col:
110 if order_dir == 'asc':
110 if order_dir == 'asc':
111 # handle null values properly to order by NULL last
111 # handle null values properly to order by NULL last
112 if order_by in ['last_activity']:
112 if order_by in ['last_activity']:
113 sort_col = coalesce(sort_col, datetime.date.max)
113 sort_col = coalesce(sort_col, datetime.date.max)
114 sort_col = sort_col.asc()
114 sort_col = sort_col.asc()
115 else:
115 else:
116 # handle null values properly to order by NULL last
116 # handle null values properly to order by NULL last
117 if order_by in ['last_activity']:
117 if order_by in ['last_activity']:
118 sort_col = coalesce(sort_col, datetime.date.min)
118 sort_col = coalesce(sort_col, datetime.date.min)
119 sort_col = sort_col.desc()
119 sort_col = sort_col.desc()
120
120
121 base_q = base_q.order_by(sort_col)
121 base_q = base_q.order_by(sort_col)
122 base_q = base_q.offset(start).limit(limit)
122 base_q = base_q.offset(start).limit(limit)
123
123
124 users_list = base_q.all()
124 users_list = base_q.all()
125
125
126 users_data = []
126 users_data = []
127 for user in users_list:
127 for user in users_list:
128 users_data.append({
128 users_data.append({
129 "username": h.gravatar_with_user(self.request, user.username),
129 "username": h.gravatar_with_user(self.request, user.username),
130 "email": user.email,
130 "email": user.email,
131 "first_name": user.first_name,
131 "first_name": user.first_name,
132 "last_name": user.last_name,
132 "last_name": user.last_name,
133 "last_login": h.format_date(user.last_login),
133 "last_login": h.format_date(user.last_login),
134 "last_activity": h.format_date(user.last_activity),
134 "last_activity": h.format_date(user.last_activity),
135 "active": h.bool2icon(user.active),
135 "active": h.bool2icon(user.active),
136 "active_raw": user.active,
136 "active_raw": user.active,
137 "admin": h.bool2icon(user.admin),
137 "admin": h.bool2icon(user.admin),
138 "extern_type": user.extern_type,
138 "extern_type": user.extern_type,
139 "extern_name": user.extern_name,
139 "extern_name": user.extern_name,
140 "action": user_actions(user.user_id, user.username),
140 "action": user_actions(user.user_id, user.username),
141 })
141 })
142
142
143 data = ({
143 data = ({
144 'draw': draw,
144 'draw': draw,
145 'data': users_data,
145 'data': users_data,
146 'recordsTotal': users_data_total_count,
146 'recordsTotal': users_data_total_count,
147 'recordsFiltered': users_data_total_filtered_count,
147 'recordsFiltered': users_data_total_filtered_count,
148 })
148 })
149
149
150 return data
150 return data
151
151
152 @LoginRequired()
152 @LoginRequired()
153 @HasPermissionAllDecorator('hg.admin')
153 @HasPermissionAllDecorator('hg.admin')
154 @view_config(
154 @view_config(
155 route_name='edit_user_auth_tokens', request_method='GET',
155 route_name='edit_user_auth_tokens', request_method='GET',
156 renderer='rhodecode:templates/admin/users/user_edit.mako')
156 renderer='rhodecode:templates/admin/users/user_edit.mako')
157 def auth_tokens(self):
157 def auth_tokens(self):
158 _ = self.request.translate
158 _ = self.request.translate
159 c = self.load_default_context()
159 c = self.load_default_context()
160
160
161 user_id = self.request.matchdict.get('user_id')
161 user_id = self.request.matchdict.get('user_id')
162 c.user = User.get_or_404(user_id, pyramid_exc=True)
162 c.user = User.get_or_404(user_id)
163 self._redirect_for_default_user(c.user.username)
163 self._redirect_for_default_user(c.user.username)
164
164
165 c.active = 'auth_tokens'
165 c.active = 'auth_tokens'
166
166
167 c.lifetime_values = [
167 c.lifetime_values = [
168 (str(-1), _('forever')),
168 (str(-1), _('forever')),
169 (str(5), _('5 minutes')),
169 (str(5), _('5 minutes')),
170 (str(60), _('1 hour')),
170 (str(60), _('1 hour')),
171 (str(60 * 24), _('1 day')),
171 (str(60 * 24), _('1 day')),
172 (str(60 * 24 * 30), _('1 month')),
172 (str(60 * 24 * 30), _('1 month')),
173 ]
173 ]
174 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
174 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
175 c.role_values = [
175 c.role_values = [
176 (x, AuthTokenModel.cls._get_role_name(x))
176 (x, AuthTokenModel.cls._get_role_name(x))
177 for x in AuthTokenModel.cls.ROLES]
177 for x in AuthTokenModel.cls.ROLES]
178 c.role_options = [(c.role_values, _("Role"))]
178 c.role_options = [(c.role_values, _("Role"))]
179 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
179 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
180 c.user.user_id, show_expired=True)
180 c.user.user_id, show_expired=True)
181 return self._get_template_context(c)
181 return self._get_template_context(c)
182
182
183 def maybe_attach_token_scope(self, token):
183 def maybe_attach_token_scope(self, token):
184 # implemented in EE edition
184 # implemented in EE edition
185 pass
185 pass
186
186
187 @LoginRequired()
187 @LoginRequired()
188 @HasPermissionAllDecorator('hg.admin')
188 @HasPermissionAllDecorator('hg.admin')
189 @CSRFRequired()
189 @CSRFRequired()
190 @view_config(
190 @view_config(
191 route_name='edit_user_auth_tokens_add', request_method='POST')
191 route_name='edit_user_auth_tokens_add', request_method='POST')
192 def auth_tokens_add(self):
192 def auth_tokens_add(self):
193 _ = self.request.translate
193 _ = self.request.translate
194 c = self.load_default_context()
194 c = self.load_default_context()
195
195
196 user_id = self.request.matchdict.get('user_id')
196 user_id = self.request.matchdict.get('user_id')
197 c.user = User.get_or_404(user_id, pyramid_exc=True)
197 c.user = User.get_or_404(user_id)
198
198
199 self._redirect_for_default_user(c.user.username)
199 self._redirect_for_default_user(c.user.username)
200
200
201 user_data = c.user.get_api_data()
201 user_data = c.user.get_api_data()
202 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
202 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
203 description = self.request.POST.get('description')
203 description = self.request.POST.get('description')
204 role = self.request.POST.get('role')
204 role = self.request.POST.get('role')
205
205
206 token = AuthTokenModel().create(
206 token = AuthTokenModel().create(
207 c.user.user_id, description, lifetime, role)
207 c.user.user_id, description, lifetime, role)
208 token_data = token.get_api_data()
208 token_data = token.get_api_data()
209
209
210 self.maybe_attach_token_scope(token)
210 self.maybe_attach_token_scope(token)
211 audit_logger.store_web(
211 audit_logger.store_web(
212 'user.edit.token.add', action_data={
212 'user.edit.token.add', action_data={
213 'data': {'token': token_data, 'user': user_data}},
213 'data': {'token': token_data, 'user': user_data}},
214 user=self._rhodecode_user, )
214 user=self._rhodecode_user, )
215 Session().commit()
215 Session().commit()
216
216
217 h.flash(_("Auth token successfully created"), category='success')
217 h.flash(_("Auth token successfully created"), category='success')
218 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
218 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
219
219
220 @LoginRequired()
220 @LoginRequired()
221 @HasPermissionAllDecorator('hg.admin')
221 @HasPermissionAllDecorator('hg.admin')
222 @CSRFRequired()
222 @CSRFRequired()
223 @view_config(
223 @view_config(
224 route_name='edit_user_auth_tokens_delete', request_method='POST')
224 route_name='edit_user_auth_tokens_delete', request_method='POST')
225 def auth_tokens_delete(self):
225 def auth_tokens_delete(self):
226 _ = self.request.translate
226 _ = self.request.translate
227 c = self.load_default_context()
227 c = self.load_default_context()
228
228
229 user_id = self.request.matchdict.get('user_id')
229 user_id = self.request.matchdict.get('user_id')
230 c.user = User.get_or_404(user_id, pyramid_exc=True)
230 c.user = User.get_or_404(user_id)
231 self._redirect_for_default_user(c.user.username)
231 self._redirect_for_default_user(c.user.username)
232 user_data = c.user.get_api_data()
232 user_data = c.user.get_api_data()
233
233
234 del_auth_token = self.request.POST.get('del_auth_token')
234 del_auth_token = self.request.POST.get('del_auth_token')
235
235
236 if del_auth_token:
236 if del_auth_token:
237 token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
237 token = UserApiKeys.get_or_404(del_auth_token)
238 token_data = token.get_api_data()
238 token_data = token.get_api_data()
239
239
240 AuthTokenModel().delete(del_auth_token, c.user.user_id)
240 AuthTokenModel().delete(del_auth_token, c.user.user_id)
241 audit_logger.store_web(
241 audit_logger.store_web(
242 'user.edit.token.delete', action_data={
242 'user.edit.token.delete', action_data={
243 'data': {'token': token_data, 'user': user_data}},
243 'data': {'token': token_data, 'user': user_data}},
244 user=self._rhodecode_user,)
244 user=self._rhodecode_user,)
245 Session().commit()
245 Session().commit()
246 h.flash(_("Auth token successfully deleted"), category='success')
246 h.flash(_("Auth token successfully deleted"), category='success')
247
247
248 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
248 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
249
249
250 @LoginRequired()
250 @LoginRequired()
251 @HasPermissionAllDecorator('hg.admin')
251 @HasPermissionAllDecorator('hg.admin')
252 @view_config(
252 @view_config(
253 route_name='edit_user_emails', request_method='GET',
253 route_name='edit_user_emails', request_method='GET',
254 renderer='rhodecode:templates/admin/users/user_edit.mako')
254 renderer='rhodecode:templates/admin/users/user_edit.mako')
255 def emails(self):
255 def emails(self):
256 _ = self.request.translate
256 _ = self.request.translate
257 c = self.load_default_context()
257 c = self.load_default_context()
258
258
259 user_id = self.request.matchdict.get('user_id')
259 user_id = self.request.matchdict.get('user_id')
260 c.user = User.get_or_404(user_id, pyramid_exc=True)
260 c.user = User.get_or_404(user_id)
261 self._redirect_for_default_user(c.user.username)
261 self._redirect_for_default_user(c.user.username)
262
262
263 c.active = 'emails'
263 c.active = 'emails'
264 c.user_email_map = UserEmailMap.query() \
264 c.user_email_map = UserEmailMap.query() \
265 .filter(UserEmailMap.user == c.user).all()
265 .filter(UserEmailMap.user == c.user).all()
266
266
267 return self._get_template_context(c)
267 return self._get_template_context(c)
268
268
269 @LoginRequired()
269 @LoginRequired()
270 @HasPermissionAllDecorator('hg.admin')
270 @HasPermissionAllDecorator('hg.admin')
271 @CSRFRequired()
271 @CSRFRequired()
272 @view_config(
272 @view_config(
273 route_name='edit_user_emails_add', request_method='POST')
273 route_name='edit_user_emails_add', request_method='POST')
274 def emails_add(self):
274 def emails_add(self):
275 _ = self.request.translate
275 _ = self.request.translate
276 c = self.load_default_context()
276 c = self.load_default_context()
277
277
278 user_id = self.request.matchdict.get('user_id')
278 user_id = self.request.matchdict.get('user_id')
279 c.user = User.get_or_404(user_id, pyramid_exc=True)
279 c.user = User.get_or_404(user_id)
280 self._redirect_for_default_user(c.user.username)
280 self._redirect_for_default_user(c.user.username)
281
281
282 email = self.request.POST.get('new_email')
282 email = self.request.POST.get('new_email')
283 user_data = c.user.get_api_data()
283 user_data = c.user.get_api_data()
284 try:
284 try:
285 UserModel().add_extra_email(c.user.user_id, email)
285 UserModel().add_extra_email(c.user.user_id, email)
286 audit_logger.store_web(
286 audit_logger.store_web(
287 'user.edit.email.add', action_data={'email': email, 'user': user_data},
287 'user.edit.email.add', action_data={'email': email, 'user': user_data},
288 user=self._rhodecode_user)
288 user=self._rhodecode_user)
289 Session().commit()
289 Session().commit()
290 h.flash(_("Added new email address `%s` for user account") % email,
290 h.flash(_("Added new email address `%s` for user account") % email,
291 category='success')
291 category='success')
292 except formencode.Invalid as error:
292 except formencode.Invalid as error:
293 h.flash(h.escape(error.error_dict['email']), category='error')
293 h.flash(h.escape(error.error_dict['email']), category='error')
294 except Exception:
294 except Exception:
295 log.exception("Exception during email saving")
295 log.exception("Exception during email saving")
296 h.flash(_('An error occurred during email saving'),
296 h.flash(_('An error occurred during email saving'),
297 category='error')
297 category='error')
298 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
298 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
299
299
300 @LoginRequired()
300 @LoginRequired()
301 @HasPermissionAllDecorator('hg.admin')
301 @HasPermissionAllDecorator('hg.admin')
302 @CSRFRequired()
302 @CSRFRequired()
303 @view_config(
303 @view_config(
304 route_name='edit_user_emails_delete', request_method='POST')
304 route_name='edit_user_emails_delete', request_method='POST')
305 def emails_delete(self):
305 def emails_delete(self):
306 _ = self.request.translate
306 _ = self.request.translate
307 c = self.load_default_context()
307 c = self.load_default_context()
308
308
309 user_id = self.request.matchdict.get('user_id')
309 user_id = self.request.matchdict.get('user_id')
310 c.user = User.get_or_404(user_id, pyramid_exc=True)
310 c.user = User.get_or_404(user_id)
311 self._redirect_for_default_user(c.user.username)
311 self._redirect_for_default_user(c.user.username)
312
312
313 email_id = self.request.POST.get('del_email_id')
313 email_id = self.request.POST.get('del_email_id')
314 user_model = UserModel()
314 user_model = UserModel()
315
315
316 email = UserEmailMap.query().get(email_id).email
316 email = UserEmailMap.query().get(email_id).email
317 user_data = c.user.get_api_data()
317 user_data = c.user.get_api_data()
318 user_model.delete_extra_email(c.user.user_id, email_id)
318 user_model.delete_extra_email(c.user.user_id, email_id)
319 audit_logger.store_web(
319 audit_logger.store_web(
320 'user.edit.email.delete', action_data={'email': email, 'user': user_data},
320 'user.edit.email.delete', action_data={'email': email, 'user': user_data},
321 user=self._rhodecode_user)
321 user=self._rhodecode_user)
322 Session().commit()
322 Session().commit()
323 h.flash(_("Removed email address from user account"),
323 h.flash(_("Removed email address from user account"),
324 category='success')
324 category='success')
325 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
325 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
326
326
327 @LoginRequired()
327 @LoginRequired()
328 @HasPermissionAllDecorator('hg.admin')
328 @HasPermissionAllDecorator('hg.admin')
329 @view_config(
329 @view_config(
330 route_name='edit_user_ips', request_method='GET',
330 route_name='edit_user_ips', request_method='GET',
331 renderer='rhodecode:templates/admin/users/user_edit.mako')
331 renderer='rhodecode:templates/admin/users/user_edit.mako')
332 def ips(self):
332 def ips(self):
333 _ = self.request.translate
333 _ = self.request.translate
334 c = self.load_default_context()
334 c = self.load_default_context()
335
335
336 user_id = self.request.matchdict.get('user_id')
336 user_id = self.request.matchdict.get('user_id')
337 c.user = User.get_or_404(user_id, pyramid_exc=True)
337 c.user = User.get_or_404(user_id)
338 self._redirect_for_default_user(c.user.username)
338 self._redirect_for_default_user(c.user.username)
339
339
340 c.active = 'ips'
340 c.active = 'ips'
341 c.user_ip_map = UserIpMap.query() \
341 c.user_ip_map = UserIpMap.query() \
342 .filter(UserIpMap.user == c.user).all()
342 .filter(UserIpMap.user == c.user).all()
343
343
344 c.inherit_default_ips = c.user.inherit_default_permissions
344 c.inherit_default_ips = c.user.inherit_default_permissions
345 c.default_user_ip_map = UserIpMap.query() \
345 c.default_user_ip_map = UserIpMap.query() \
346 .filter(UserIpMap.user == User.get_default_user()).all()
346 .filter(UserIpMap.user == User.get_default_user()).all()
347
347
348 return self._get_template_context(c)
348 return self._get_template_context(c)
349
349
350 @LoginRequired()
350 @LoginRequired()
351 @HasPermissionAllDecorator('hg.admin')
351 @HasPermissionAllDecorator('hg.admin')
352 @CSRFRequired()
352 @CSRFRequired()
353 @view_config(
353 @view_config(
354 route_name='edit_user_ips_add', request_method='POST')
354 route_name='edit_user_ips_add', request_method='POST')
355 def ips_add(self):
355 def ips_add(self):
356 _ = self.request.translate
356 _ = self.request.translate
357 c = self.load_default_context()
357 c = self.load_default_context()
358
358
359 user_id = self.request.matchdict.get('user_id')
359 user_id = self.request.matchdict.get('user_id')
360 c.user = User.get_or_404(user_id, pyramid_exc=True)
360 c.user = User.get_or_404(user_id)
361 # NOTE(marcink): this view is allowed for default users, as we can
361 # NOTE(marcink): this view is allowed for default users, as we can
362 # edit their IP white list
362 # edit their IP white list
363
363
364 user_model = UserModel()
364 user_model = UserModel()
365 desc = self.request.POST.get('description')
365 desc = self.request.POST.get('description')
366 try:
366 try:
367 ip_list = user_model.parse_ip_range(
367 ip_list = user_model.parse_ip_range(
368 self.request.POST.get('new_ip'))
368 self.request.POST.get('new_ip'))
369 except Exception as e:
369 except Exception as e:
370 ip_list = []
370 ip_list = []
371 log.exception("Exception during ip saving")
371 log.exception("Exception during ip saving")
372 h.flash(_('An error occurred during ip saving:%s' % (e,)),
372 h.flash(_('An error occurred during ip saving:%s' % (e,)),
373 category='error')
373 category='error')
374 added = []
374 added = []
375 user_data = c.user.get_api_data()
375 user_data = c.user.get_api_data()
376 for ip in ip_list:
376 for ip in ip_list:
377 try:
377 try:
378 user_model.add_extra_ip(c.user.user_id, ip, desc)
378 user_model.add_extra_ip(c.user.user_id, ip, desc)
379 audit_logger.store_web(
379 audit_logger.store_web(
380 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
380 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
381 user=self._rhodecode_user)
381 user=self._rhodecode_user)
382 Session().commit()
382 Session().commit()
383 added.append(ip)
383 added.append(ip)
384 except formencode.Invalid as error:
384 except formencode.Invalid as error:
385 msg = error.error_dict['ip']
385 msg = error.error_dict['ip']
386 h.flash(msg, category='error')
386 h.flash(msg, category='error')
387 except Exception:
387 except Exception:
388 log.exception("Exception during ip saving")
388 log.exception("Exception during ip saving")
389 h.flash(_('An error occurred during ip saving'),
389 h.flash(_('An error occurred during ip saving'),
390 category='error')
390 category='error')
391 if added:
391 if added:
392 h.flash(
392 h.flash(
393 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
393 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
394 category='success')
394 category='success')
395 if 'default_user' in self.request.POST:
395 if 'default_user' in self.request.POST:
396 # case for editing global IP list we do it for 'DEFAULT' user
396 # case for editing global IP list we do it for 'DEFAULT' user
397 raise HTTPFound(h.route_path('admin_permissions_ips'))
397 raise HTTPFound(h.route_path('admin_permissions_ips'))
398 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
398 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
399
399
400 @LoginRequired()
400 @LoginRequired()
401 @HasPermissionAllDecorator('hg.admin')
401 @HasPermissionAllDecorator('hg.admin')
402 @CSRFRequired()
402 @CSRFRequired()
403 @view_config(
403 @view_config(
404 route_name='edit_user_ips_delete', request_method='POST')
404 route_name='edit_user_ips_delete', request_method='POST')
405 def ips_delete(self):
405 def ips_delete(self):
406 _ = self.request.translate
406 _ = self.request.translate
407 c = self.load_default_context()
407 c = self.load_default_context()
408
408
409 user_id = self.request.matchdict.get('user_id')
409 user_id = self.request.matchdict.get('user_id')
410 c.user = User.get_or_404(user_id, pyramid_exc=True)
410 c.user = User.get_or_404(user_id)
411 # NOTE(marcink): this view is allowed for default users, as we can
411 # NOTE(marcink): this view is allowed for default users, as we can
412 # edit their IP white list
412 # edit their IP white list
413
413
414 ip_id = self.request.POST.get('del_ip_id')
414 ip_id = self.request.POST.get('del_ip_id')
415 user_model = UserModel()
415 user_model = UserModel()
416 user_data = c.user.get_api_data()
416 user_data = c.user.get_api_data()
417 ip = UserIpMap.query().get(ip_id).ip_addr
417 ip = UserIpMap.query().get(ip_id).ip_addr
418 user_model.delete_extra_ip(c.user.user_id, ip_id)
418 user_model.delete_extra_ip(c.user.user_id, ip_id)
419 audit_logger.store_web(
419 audit_logger.store_web(
420 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
420 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
421 user=self._rhodecode_user)
421 user=self._rhodecode_user)
422 Session().commit()
422 Session().commit()
423 h.flash(_("Removed ip address from user whitelist"), category='success')
423 h.flash(_("Removed ip address from user whitelist"), category='success')
424
424
425 if 'default_user' in self.request.POST:
425 if 'default_user' in self.request.POST:
426 # case for editing global IP list we do it for 'DEFAULT' user
426 # case for editing global IP list we do it for 'DEFAULT' user
427 raise HTTPFound(h.route_path('admin_permissions_ips'))
427 raise HTTPFound(h.route_path('admin_permissions_ips'))
428 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
428 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
429
429
430 @LoginRequired()
430 @LoginRequired()
431 @HasPermissionAllDecorator('hg.admin')
431 @HasPermissionAllDecorator('hg.admin')
432 @view_config(
432 @view_config(
433 route_name='edit_user_groups_management', request_method='GET',
433 route_name='edit_user_groups_management', request_method='GET',
434 renderer='rhodecode:templates/admin/users/user_edit.mako')
434 renderer='rhodecode:templates/admin/users/user_edit.mako')
435 def groups_management(self):
435 def groups_management(self):
436 c = self.load_default_context()
436 c = self.load_default_context()
437
437
438 user_id = self.request.matchdict.get('user_id')
438 user_id = self.request.matchdict.get('user_id')
439 c.user = User.get_or_404(user_id, pyramid_exc=True)
439 c.user = User.get_or_404(user_id)
440 c.data = c.user.group_member
440 c.data = c.user.group_member
441 self._redirect_for_default_user(c.user.username)
441 self._redirect_for_default_user(c.user.username)
442 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
442 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
443 for group in c.user.group_member]
443 for group in c.user.group_member]
444 c.groups = json.dumps(groups)
444 c.groups = json.dumps(groups)
445 c.active = 'groups'
445 c.active = 'groups'
446
446
447 return self._get_template_context(c)
447 return self._get_template_context(c)
448
448
449 @LoginRequired()
449 @LoginRequired()
450 @HasPermissionAllDecorator('hg.admin')
450 @HasPermissionAllDecorator('hg.admin')
451 @CSRFRequired()
451 @CSRFRequired()
452 @view_config(
452 @view_config(
453 route_name='edit_user_groups_management_updates', request_method='POST')
453 route_name='edit_user_groups_management_updates', request_method='POST')
454 def groups_management_updates(self):
454 def groups_management_updates(self):
455 _ = self.request.translate
455 _ = self.request.translate
456 c = self.load_default_context()
456 c = self.load_default_context()
457
457
458 user_id = self.request.matchdict.get('user_id')
458 user_id = self.request.matchdict.get('user_id')
459 c.user = User.get_or_404(user_id, pyramid_exc=True)
459 c.user = User.get_or_404(user_id)
460 self._redirect_for_default_user(c.user.username)
460 self._redirect_for_default_user(c.user.username)
461
461
462 users_groups = set(self.request.POST.getall('users_group_id'))
462 users_groups = set(self.request.POST.getall('users_group_id'))
463 users_groups_model = []
463 users_groups_model = []
464
464
465 for ugid in users_groups:
465 for ugid in users_groups:
466 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
466 users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
467 user_group_model = UserGroupModel()
467 user_group_model = UserGroupModel()
468 user_group_model.change_groups(c.user, users_groups_model)
468 user_group_model.change_groups(c.user, users_groups_model)
469
469
470 Session().commit()
470 Session().commit()
471 c.active = 'user_groups_management'
471 c.active = 'user_groups_management'
472 h.flash(_("Groups successfully changed"), category='success')
472 h.flash(_("Groups successfully changed"), category='success')
473
473
474 return HTTPFound(h.route_path(
474 return HTTPFound(h.route_path(
475 'edit_user_groups_management', user_id=user_id))
475 'edit_user_groups_management', user_id=user_id))
476
476
477 @LoginRequired()
477 @LoginRequired()
478 @HasPermissionAllDecorator('hg.admin')
478 @HasPermissionAllDecorator('hg.admin')
479 @view_config(
479 @view_config(
480 route_name='edit_user_audit_logs', request_method='GET',
480 route_name='edit_user_audit_logs', request_method='GET',
481 renderer='rhodecode:templates/admin/users/user_edit.mako')
481 renderer='rhodecode:templates/admin/users/user_edit.mako')
482 def user_audit_logs(self):
482 def user_audit_logs(self):
483 _ = self.request.translate
483 _ = self.request.translate
484 c = self.load_default_context()
484 c = self.load_default_context()
485
485
486 user_id = self.request.matchdict.get('user_id')
486 user_id = self.request.matchdict.get('user_id')
487 c.user = User.get_or_404(user_id, pyramid_exc=True)
487 c.user = User.get_or_404(user_id)
488 self._redirect_for_default_user(c.user.username)
488 self._redirect_for_default_user(c.user.username)
489 c.active = 'audit'
489 c.active = 'audit'
490
490
491 p = safe_int(self.request.GET.get('page', 1), 1)
491 p = safe_int(self.request.GET.get('page', 1), 1)
492
492
493 filter_term = self.request.GET.get('filter')
493 filter_term = self.request.GET.get('filter')
494 user_log = UserModel().get_user_log(c.user, filter_term)
494 user_log = UserModel().get_user_log(c.user, filter_term)
495
495
496 def url_generator(**kw):
496 def url_generator(**kw):
497 if filter_term:
497 if filter_term:
498 kw['filter'] = filter_term
498 kw['filter'] = filter_term
499 return self.request.current_route_path(_query=kw)
499 return self.request.current_route_path(_query=kw)
500
500
501 c.audit_logs = h.Page(
501 c.audit_logs = h.Page(
502 user_log, page=p, items_per_page=10, url=url_generator)
502 user_log, page=p, items_per_page=10, url=url_generator)
503 c.filter_term = filter_term
503 c.filter_term = filter_term
504 return self._get_template_context(c)
504 return self._get_template_context(c)
505
505
@@ -1,586 +1,586 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23
23
24 import formencode
24 import formencode
25 from pyramid.httpexceptions import HTTPFound
25 from pyramid.httpexceptions import HTTPFound
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode import forms
31 from rhodecode import forms
32 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib import audit_logger
33 from rhodecode.lib import audit_logger
34 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 from rhodecode.lib.channelstream import channelstream_request, \
36 from rhodecode.lib.channelstream import channelstream_request, \
37 ChannelstreamException
37 ChannelstreamException
38 from rhodecode.lib.utils2 import safe_int, md5, str2bool
38 from rhodecode.lib.utils2 import safe_int, md5, str2bool
39 from rhodecode.model.auth_token import AuthTokenModel
39 from rhodecode.model.auth_token import AuthTokenModel
40 from rhodecode.model.comment import CommentsModel
40 from rhodecode.model.comment import CommentsModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
42 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
43 PullRequest)
43 PullRequest)
44 from rhodecode.model.forms import UserForm
44 from rhodecode.model.forms import UserForm
45 from rhodecode.model.meta import Session
45 from rhodecode.model.meta import Session
46 from rhodecode.model.pull_request import PullRequestModel
46 from rhodecode.model.pull_request import PullRequestModel
47 from rhodecode.model.scm import RepoList
47 from rhodecode.model.scm import RepoList
48 from rhodecode.model.user import UserModel
48 from rhodecode.model.user import UserModel
49 from rhodecode.model.repo import RepoModel
49 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.validation_schema.schemas import user_schema
50 from rhodecode.model.validation_schema.schemas import user_schema
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 class MyAccountView(BaseAppView, DataGridAppView):
55 class MyAccountView(BaseAppView, DataGridAppView):
56 ALLOW_SCOPED_TOKENS = False
56 ALLOW_SCOPED_TOKENS = False
57 """
57 """
58 This view has alternative version inside EE, if modified please take a look
58 This view has alternative version inside EE, if modified please take a look
59 in there as well.
59 in there as well.
60 """
60 """
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context()
63 c = self._get_local_tmpl_context()
64 c.user = c.auth_user.get_instance()
64 c.user = c.auth_user.get_instance()
65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 self._register_global_c(c)
66 self._register_global_c(c)
67 return c
67 return c
68
68
69 @LoginRequired()
69 @LoginRequired()
70 @NotAnonymous()
70 @NotAnonymous()
71 @view_config(
71 @view_config(
72 route_name='my_account_profile', request_method='GET',
72 route_name='my_account_profile', request_method='GET',
73 renderer='rhodecode:templates/admin/my_account/my_account.mako')
73 renderer='rhodecode:templates/admin/my_account/my_account.mako')
74 def my_account_profile(self):
74 def my_account_profile(self):
75 c = self.load_default_context()
75 c = self.load_default_context()
76 c.active = 'profile'
76 c.active = 'profile'
77 return self._get_template_context(c)
77 return self._get_template_context(c)
78
78
79 @LoginRequired()
79 @LoginRequired()
80 @NotAnonymous()
80 @NotAnonymous()
81 @view_config(
81 @view_config(
82 route_name='my_account_password', request_method='GET',
82 route_name='my_account_password', request_method='GET',
83 renderer='rhodecode:templates/admin/my_account/my_account.mako')
83 renderer='rhodecode:templates/admin/my_account/my_account.mako')
84 def my_account_password(self):
84 def my_account_password(self):
85 c = self.load_default_context()
85 c = self.load_default_context()
86 c.active = 'password'
86 c.active = 'password'
87 c.extern_type = c.user.extern_type
87 c.extern_type = c.user.extern_type
88
88
89 schema = user_schema.ChangePasswordSchema().bind(
89 schema = user_schema.ChangePasswordSchema().bind(
90 username=c.user.username)
90 username=c.user.username)
91
91
92 form = forms.Form(
92 form = forms.Form(
93 schema,
93 schema,
94 action=h.route_path('my_account_password_update'),
94 action=h.route_path('my_account_password_update'),
95 buttons=(forms.buttons.save, forms.buttons.reset))
95 buttons=(forms.buttons.save, forms.buttons.reset))
96
96
97 c.form = form
97 c.form = form
98 return self._get_template_context(c)
98 return self._get_template_context(c)
99
99
100 @LoginRequired()
100 @LoginRequired()
101 @NotAnonymous()
101 @NotAnonymous()
102 @CSRFRequired()
102 @CSRFRequired()
103 @view_config(
103 @view_config(
104 route_name='my_account_password_update', request_method='POST',
104 route_name='my_account_password_update', request_method='POST',
105 renderer='rhodecode:templates/admin/my_account/my_account.mako')
105 renderer='rhodecode:templates/admin/my_account/my_account.mako')
106 def my_account_password_update(self):
106 def my_account_password_update(self):
107 _ = self.request.translate
107 _ = self.request.translate
108 c = self.load_default_context()
108 c = self.load_default_context()
109 c.active = 'password'
109 c.active = 'password'
110 c.extern_type = c.user.extern_type
110 c.extern_type = c.user.extern_type
111
111
112 schema = user_schema.ChangePasswordSchema().bind(
112 schema = user_schema.ChangePasswordSchema().bind(
113 username=c.user.username)
113 username=c.user.username)
114
114
115 form = forms.Form(
115 form = forms.Form(
116 schema, buttons=(forms.buttons.save, forms.buttons.reset))
116 schema, buttons=(forms.buttons.save, forms.buttons.reset))
117
117
118 if c.extern_type != 'rhodecode':
118 if c.extern_type != 'rhodecode':
119 raise HTTPFound(self.request.route_path('my_account_password'))
119 raise HTTPFound(self.request.route_path('my_account_password'))
120
120
121 controls = self.request.POST.items()
121 controls = self.request.POST.items()
122 try:
122 try:
123 valid_data = form.validate(controls)
123 valid_data = form.validate(controls)
124 UserModel().update_user(c.user.user_id, **valid_data)
124 UserModel().update_user(c.user.user_id, **valid_data)
125 c.user.update_userdata(force_password_change=False)
125 c.user.update_userdata(force_password_change=False)
126 Session().commit()
126 Session().commit()
127 except forms.ValidationFailure as e:
127 except forms.ValidationFailure as e:
128 c.form = e
128 c.form = e
129 return self._get_template_context(c)
129 return self._get_template_context(c)
130
130
131 except Exception:
131 except Exception:
132 log.exception("Exception updating password")
132 log.exception("Exception updating password")
133 h.flash(_('Error occurred during update of user password'),
133 h.flash(_('Error occurred during update of user password'),
134 category='error')
134 category='error')
135 else:
135 else:
136 instance = c.auth_user.get_instance()
136 instance = c.auth_user.get_instance()
137 self.session.setdefault('rhodecode_user', {}).update(
137 self.session.setdefault('rhodecode_user', {}).update(
138 {'password': md5(instance.password)})
138 {'password': md5(instance.password)})
139 self.session.save()
139 self.session.save()
140 h.flash(_("Successfully updated password"), category='success')
140 h.flash(_("Successfully updated password"), category='success')
141
141
142 raise HTTPFound(self.request.route_path('my_account_password'))
142 raise HTTPFound(self.request.route_path('my_account_password'))
143
143
144 @LoginRequired()
144 @LoginRequired()
145 @NotAnonymous()
145 @NotAnonymous()
146 @view_config(
146 @view_config(
147 route_name='my_account_auth_tokens', request_method='GET',
147 route_name='my_account_auth_tokens', request_method='GET',
148 renderer='rhodecode:templates/admin/my_account/my_account.mako')
148 renderer='rhodecode:templates/admin/my_account/my_account.mako')
149 def my_account_auth_tokens(self):
149 def my_account_auth_tokens(self):
150 _ = self.request.translate
150 _ = self.request.translate
151
151
152 c = self.load_default_context()
152 c = self.load_default_context()
153 c.active = 'auth_tokens'
153 c.active = 'auth_tokens'
154
154
155 c.lifetime_values = [
155 c.lifetime_values = [
156 (str(-1), _('forever')),
156 (str(-1), _('forever')),
157 (str(5), _('5 minutes')),
157 (str(5), _('5 minutes')),
158 (str(60), _('1 hour')),
158 (str(60), _('1 hour')),
159 (str(60 * 24), _('1 day')),
159 (str(60 * 24), _('1 day')),
160 (str(60 * 24 * 30), _('1 month')),
160 (str(60 * 24 * 30), _('1 month')),
161 ]
161 ]
162 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
162 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
163 c.role_values = [
163 c.role_values = [
164 (x, AuthTokenModel.cls._get_role_name(x))
164 (x, AuthTokenModel.cls._get_role_name(x))
165 for x in AuthTokenModel.cls.ROLES]
165 for x in AuthTokenModel.cls.ROLES]
166 c.role_options = [(c.role_values, _("Role"))]
166 c.role_options = [(c.role_values, _("Role"))]
167 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
167 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
168 c.user.user_id, show_expired=True)
168 c.user.user_id, show_expired=True)
169 return self._get_template_context(c)
169 return self._get_template_context(c)
170
170
171 def maybe_attach_token_scope(self, token):
171 def maybe_attach_token_scope(self, token):
172 # implemented in EE edition
172 # implemented in EE edition
173 pass
173 pass
174
174
175 @LoginRequired()
175 @LoginRequired()
176 @NotAnonymous()
176 @NotAnonymous()
177 @CSRFRequired()
177 @CSRFRequired()
178 @view_config(
178 @view_config(
179 route_name='my_account_auth_tokens_add', request_method='POST',)
179 route_name='my_account_auth_tokens_add', request_method='POST',)
180 def my_account_auth_tokens_add(self):
180 def my_account_auth_tokens_add(self):
181 _ = self.request.translate
181 _ = self.request.translate
182 c = self.load_default_context()
182 c = self.load_default_context()
183
183
184 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
184 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
185 description = self.request.POST.get('description')
185 description = self.request.POST.get('description')
186 role = self.request.POST.get('role')
186 role = self.request.POST.get('role')
187
187
188 token = AuthTokenModel().create(
188 token = AuthTokenModel().create(
189 c.user.user_id, description, lifetime, role)
189 c.user.user_id, description, lifetime, role)
190 token_data = token.get_api_data()
190 token_data = token.get_api_data()
191
191
192 self.maybe_attach_token_scope(token)
192 self.maybe_attach_token_scope(token)
193 audit_logger.store_web(
193 audit_logger.store_web(
194 'user.edit.token.add', action_data={
194 'user.edit.token.add', action_data={
195 'data': {'token': token_data, 'user': 'self'}},
195 'data': {'token': token_data, 'user': 'self'}},
196 user=self._rhodecode_user, )
196 user=self._rhodecode_user, )
197 Session().commit()
197 Session().commit()
198
198
199 h.flash(_("Auth token successfully created"), category='success')
199 h.flash(_("Auth token successfully created"), category='success')
200 return HTTPFound(h.route_path('my_account_auth_tokens'))
200 return HTTPFound(h.route_path('my_account_auth_tokens'))
201
201
202 @LoginRequired()
202 @LoginRequired()
203 @NotAnonymous()
203 @NotAnonymous()
204 @CSRFRequired()
204 @CSRFRequired()
205 @view_config(
205 @view_config(
206 route_name='my_account_auth_tokens_delete', request_method='POST')
206 route_name='my_account_auth_tokens_delete', request_method='POST')
207 def my_account_auth_tokens_delete(self):
207 def my_account_auth_tokens_delete(self):
208 _ = self.request.translate
208 _ = self.request.translate
209 c = self.load_default_context()
209 c = self.load_default_context()
210
210
211 del_auth_token = self.request.POST.get('del_auth_token')
211 del_auth_token = self.request.POST.get('del_auth_token')
212
212
213 if del_auth_token:
213 if del_auth_token:
214 token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
214 token = UserApiKeys.get_or_404(del_auth_token)
215 token_data = token.get_api_data()
215 token_data = token.get_api_data()
216
216
217 AuthTokenModel().delete(del_auth_token, c.user.user_id)
217 AuthTokenModel().delete(del_auth_token, c.user.user_id)
218 audit_logger.store_web(
218 audit_logger.store_web(
219 'user.edit.token.delete', action_data={
219 'user.edit.token.delete', action_data={
220 'data': {'token': token_data, 'user': 'self'}},
220 'data': {'token': token_data, 'user': 'self'}},
221 user=self._rhodecode_user,)
221 user=self._rhodecode_user,)
222 Session().commit()
222 Session().commit()
223 h.flash(_("Auth token successfully deleted"), category='success')
223 h.flash(_("Auth token successfully deleted"), category='success')
224
224
225 return HTTPFound(h.route_path('my_account_auth_tokens'))
225 return HTTPFound(h.route_path('my_account_auth_tokens'))
226
226
227 @LoginRequired()
227 @LoginRequired()
228 @NotAnonymous()
228 @NotAnonymous()
229 @view_config(
229 @view_config(
230 route_name='my_account_emails', request_method='GET',
230 route_name='my_account_emails', request_method='GET',
231 renderer='rhodecode:templates/admin/my_account/my_account.mako')
231 renderer='rhodecode:templates/admin/my_account/my_account.mako')
232 def my_account_emails(self):
232 def my_account_emails(self):
233 _ = self.request.translate
233 _ = self.request.translate
234
234
235 c = self.load_default_context()
235 c = self.load_default_context()
236 c.active = 'emails'
236 c.active = 'emails'
237
237
238 c.user_email_map = UserEmailMap.query()\
238 c.user_email_map = UserEmailMap.query()\
239 .filter(UserEmailMap.user == c.user).all()
239 .filter(UserEmailMap.user == c.user).all()
240 return self._get_template_context(c)
240 return self._get_template_context(c)
241
241
242 @LoginRequired()
242 @LoginRequired()
243 @NotAnonymous()
243 @NotAnonymous()
244 @CSRFRequired()
244 @CSRFRequired()
245 @view_config(
245 @view_config(
246 route_name='my_account_emails_add', request_method='POST')
246 route_name='my_account_emails_add', request_method='POST')
247 def my_account_emails_add(self):
247 def my_account_emails_add(self):
248 _ = self.request.translate
248 _ = self.request.translate
249 c = self.load_default_context()
249 c = self.load_default_context()
250
250
251 email = self.request.POST.get('new_email')
251 email = self.request.POST.get('new_email')
252
252
253 try:
253 try:
254 UserModel().add_extra_email(c.user.user_id, email)
254 UserModel().add_extra_email(c.user.user_id, email)
255 audit_logger.store_web(
255 audit_logger.store_web(
256 'user.edit.email.add', action_data={
256 'user.edit.email.add', action_data={
257 'data': {'email': email, 'user': 'self'}},
257 'data': {'email': email, 'user': 'self'}},
258 user=self._rhodecode_user,)
258 user=self._rhodecode_user,)
259
259
260 Session().commit()
260 Session().commit()
261 h.flash(_("Added new email address `%s` for user account") % email,
261 h.flash(_("Added new email address `%s` for user account") % email,
262 category='success')
262 category='success')
263 except formencode.Invalid as error:
263 except formencode.Invalid as error:
264 h.flash(h.escape(error.error_dict['email']), category='error')
264 h.flash(h.escape(error.error_dict['email']), category='error')
265 except Exception:
265 except Exception:
266 log.exception("Exception in my_account_emails")
266 log.exception("Exception in my_account_emails")
267 h.flash(_('An error occurred during email saving'),
267 h.flash(_('An error occurred during email saving'),
268 category='error')
268 category='error')
269 return HTTPFound(h.route_path('my_account_emails'))
269 return HTTPFound(h.route_path('my_account_emails'))
270
270
271 @LoginRequired()
271 @LoginRequired()
272 @NotAnonymous()
272 @NotAnonymous()
273 @CSRFRequired()
273 @CSRFRequired()
274 @view_config(
274 @view_config(
275 route_name='my_account_emails_delete', request_method='POST')
275 route_name='my_account_emails_delete', request_method='POST')
276 def my_account_emails_delete(self):
276 def my_account_emails_delete(self):
277 _ = self.request.translate
277 _ = self.request.translate
278 c = self.load_default_context()
278 c = self.load_default_context()
279
279
280 del_email_id = self.request.POST.get('del_email_id')
280 del_email_id = self.request.POST.get('del_email_id')
281 if del_email_id:
281 if del_email_id:
282 email = UserEmailMap.get_or_404(del_email_id, pyramid_exc=True).email
282 email = UserEmailMap.get_or_404(del_email_id).email
283 UserModel().delete_extra_email(c.user.user_id, del_email_id)
283 UserModel().delete_extra_email(c.user.user_id, del_email_id)
284 audit_logger.store_web(
284 audit_logger.store_web(
285 'user.edit.email.delete', action_data={
285 'user.edit.email.delete', action_data={
286 'data': {'email': email, 'user': 'self'}},
286 'data': {'email': email, 'user': 'self'}},
287 user=self._rhodecode_user,)
287 user=self._rhodecode_user,)
288 Session().commit()
288 Session().commit()
289 h.flash(_("Email successfully deleted"),
289 h.flash(_("Email successfully deleted"),
290 category='success')
290 category='success')
291 return HTTPFound(h.route_path('my_account_emails'))
291 return HTTPFound(h.route_path('my_account_emails'))
292
292
293 @LoginRequired()
293 @LoginRequired()
294 @NotAnonymous()
294 @NotAnonymous()
295 @CSRFRequired()
295 @CSRFRequired()
296 @view_config(
296 @view_config(
297 route_name='my_account_notifications_test_channelstream',
297 route_name='my_account_notifications_test_channelstream',
298 request_method='POST', renderer='json_ext')
298 request_method='POST', renderer='json_ext')
299 def my_account_notifications_test_channelstream(self):
299 def my_account_notifications_test_channelstream(self):
300 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
300 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
301 self._rhodecode_user.username, datetime.datetime.now())
301 self._rhodecode_user.username, datetime.datetime.now())
302 payload = {
302 payload = {
303 # 'channel': 'broadcast',
303 # 'channel': 'broadcast',
304 'type': 'message',
304 'type': 'message',
305 'timestamp': datetime.datetime.utcnow(),
305 'timestamp': datetime.datetime.utcnow(),
306 'user': 'system',
306 'user': 'system',
307 'pm_users': [self._rhodecode_user.username],
307 'pm_users': [self._rhodecode_user.username],
308 'message': {
308 'message': {
309 'message': message,
309 'message': message,
310 'level': 'info',
310 'level': 'info',
311 'topic': '/notifications'
311 'topic': '/notifications'
312 }
312 }
313 }
313 }
314
314
315 registry = self.request.registry
315 registry = self.request.registry
316 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
316 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
317 channelstream_config = rhodecode_plugins.get('channelstream', {})
317 channelstream_config = rhodecode_plugins.get('channelstream', {})
318
318
319 try:
319 try:
320 channelstream_request(channelstream_config, [payload], '/message')
320 channelstream_request(channelstream_config, [payload], '/message')
321 except ChannelstreamException as e:
321 except ChannelstreamException as e:
322 log.exception('Failed to send channelstream data')
322 log.exception('Failed to send channelstream data')
323 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
323 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
324 return {"response": 'Channelstream data sent. '
324 return {"response": 'Channelstream data sent. '
325 'You should see a new live message now.'}
325 'You should see a new live message now.'}
326
326
327 def _load_my_repos_data(self, watched=False):
327 def _load_my_repos_data(self, watched=False):
328 if watched:
328 if watched:
329 admin = False
329 admin = False
330 follows_repos = Session().query(UserFollowing)\
330 follows_repos = Session().query(UserFollowing)\
331 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
331 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
332 .options(joinedload(UserFollowing.follows_repository))\
332 .options(joinedload(UserFollowing.follows_repository))\
333 .all()
333 .all()
334 repo_list = [x.follows_repository for x in follows_repos]
334 repo_list = [x.follows_repository for x in follows_repos]
335 else:
335 else:
336 admin = True
336 admin = True
337 repo_list = Repository.get_all_repos(
337 repo_list = Repository.get_all_repos(
338 user_id=self._rhodecode_user.user_id)
338 user_id=self._rhodecode_user.user_id)
339 repo_list = RepoList(repo_list, perm_set=[
339 repo_list = RepoList(repo_list, perm_set=[
340 'repository.read', 'repository.write', 'repository.admin'])
340 'repository.read', 'repository.write', 'repository.admin'])
341
341
342 repos_data = RepoModel().get_repos_as_dict(
342 repos_data = RepoModel().get_repos_as_dict(
343 repo_list=repo_list, admin=admin)
343 repo_list=repo_list, admin=admin)
344 # json used to render the grid
344 # json used to render the grid
345 return json.dumps(repos_data)
345 return json.dumps(repos_data)
346
346
347 @LoginRequired()
347 @LoginRequired()
348 @NotAnonymous()
348 @NotAnonymous()
349 @view_config(
349 @view_config(
350 route_name='my_account_repos', request_method='GET',
350 route_name='my_account_repos', request_method='GET',
351 renderer='rhodecode:templates/admin/my_account/my_account.mako')
351 renderer='rhodecode:templates/admin/my_account/my_account.mako')
352 def my_account_repos(self):
352 def my_account_repos(self):
353 c = self.load_default_context()
353 c = self.load_default_context()
354 c.active = 'repos'
354 c.active = 'repos'
355
355
356 # json used to render the grid
356 # json used to render the grid
357 c.data = self._load_my_repos_data()
357 c.data = self._load_my_repos_data()
358 return self._get_template_context(c)
358 return self._get_template_context(c)
359
359
360 @LoginRequired()
360 @LoginRequired()
361 @NotAnonymous()
361 @NotAnonymous()
362 @view_config(
362 @view_config(
363 route_name='my_account_watched', request_method='GET',
363 route_name='my_account_watched', request_method='GET',
364 renderer='rhodecode:templates/admin/my_account/my_account.mako')
364 renderer='rhodecode:templates/admin/my_account/my_account.mako')
365 def my_account_watched(self):
365 def my_account_watched(self):
366 c = self.load_default_context()
366 c = self.load_default_context()
367 c.active = 'watched'
367 c.active = 'watched'
368
368
369 # json used to render the grid
369 # json used to render the grid
370 c.data = self._load_my_repos_data(watched=True)
370 c.data = self._load_my_repos_data(watched=True)
371 return self._get_template_context(c)
371 return self._get_template_context(c)
372
372
373 @LoginRequired()
373 @LoginRequired()
374 @NotAnonymous()
374 @NotAnonymous()
375 @view_config(
375 @view_config(
376 route_name='my_account_perms', request_method='GET',
376 route_name='my_account_perms', request_method='GET',
377 renderer='rhodecode:templates/admin/my_account/my_account.mako')
377 renderer='rhodecode:templates/admin/my_account/my_account.mako')
378 def my_account_perms(self):
378 def my_account_perms(self):
379 c = self.load_default_context()
379 c = self.load_default_context()
380 c.active = 'perms'
380 c.active = 'perms'
381
381
382 c.perm_user = c.auth_user
382 c.perm_user = c.auth_user
383 return self._get_template_context(c)
383 return self._get_template_context(c)
384
384
385 @LoginRequired()
385 @LoginRequired()
386 @NotAnonymous()
386 @NotAnonymous()
387 @view_config(
387 @view_config(
388 route_name='my_account_notifications', request_method='GET',
388 route_name='my_account_notifications', request_method='GET',
389 renderer='rhodecode:templates/admin/my_account/my_account.mako')
389 renderer='rhodecode:templates/admin/my_account/my_account.mako')
390 def my_notifications(self):
390 def my_notifications(self):
391 c = self.load_default_context()
391 c = self.load_default_context()
392 c.active = 'notifications'
392 c.active = 'notifications'
393
393
394 return self._get_template_context(c)
394 return self._get_template_context(c)
395
395
396 @LoginRequired()
396 @LoginRequired()
397 @NotAnonymous()
397 @NotAnonymous()
398 @CSRFRequired()
398 @CSRFRequired()
399 @view_config(
399 @view_config(
400 route_name='my_account_notifications_toggle_visibility',
400 route_name='my_account_notifications_toggle_visibility',
401 request_method='POST', renderer='json_ext')
401 request_method='POST', renderer='json_ext')
402 def my_notifications_toggle_visibility(self):
402 def my_notifications_toggle_visibility(self):
403 user = self._rhodecode_db_user
403 user = self._rhodecode_db_user
404 new_status = not user.user_data.get('notification_status', True)
404 new_status = not user.user_data.get('notification_status', True)
405 user.update_userdata(notification_status=new_status)
405 user.update_userdata(notification_status=new_status)
406 Session().commit()
406 Session().commit()
407 return user.user_data['notification_status']
407 return user.user_data['notification_status']
408
408
409 @LoginRequired()
409 @LoginRequired()
410 @NotAnonymous()
410 @NotAnonymous()
411 @view_config(
411 @view_config(
412 route_name='my_account_edit',
412 route_name='my_account_edit',
413 request_method='GET',
413 request_method='GET',
414 renderer='rhodecode:templates/admin/my_account/my_account.mako')
414 renderer='rhodecode:templates/admin/my_account/my_account.mako')
415 def my_account_edit(self):
415 def my_account_edit(self):
416 c = self.load_default_context()
416 c = self.load_default_context()
417 c.active = 'profile_edit'
417 c.active = 'profile_edit'
418
418
419 c.perm_user = c.auth_user
419 c.perm_user = c.auth_user
420 c.extern_type = c.user.extern_type
420 c.extern_type = c.user.extern_type
421 c.extern_name = c.user.extern_name
421 c.extern_name = c.user.extern_name
422
422
423 defaults = c.user.get_dict()
423 defaults = c.user.get_dict()
424
424
425 data = render('rhodecode:templates/admin/my_account/my_account.mako',
425 data = render('rhodecode:templates/admin/my_account/my_account.mako',
426 self._get_template_context(c), self.request)
426 self._get_template_context(c), self.request)
427 html = formencode.htmlfill.render(
427 html = formencode.htmlfill.render(
428 data,
428 data,
429 defaults=defaults,
429 defaults=defaults,
430 encoding="UTF-8",
430 encoding="UTF-8",
431 force_defaults=False
431 force_defaults=False
432 )
432 )
433 return Response(html)
433 return Response(html)
434
434
435 @LoginRequired()
435 @LoginRequired()
436 @NotAnonymous()
436 @NotAnonymous()
437 @CSRFRequired()
437 @CSRFRequired()
438 @view_config(
438 @view_config(
439 route_name='my_account_update',
439 route_name='my_account_update',
440 request_method='POST',
440 request_method='POST',
441 renderer='rhodecode:templates/admin/my_account/my_account.mako')
441 renderer='rhodecode:templates/admin/my_account/my_account.mako')
442 def my_account_update(self):
442 def my_account_update(self):
443 _ = self.request.translate
443 _ = self.request.translate
444 c = self.load_default_context()
444 c = self.load_default_context()
445 c.active = 'profile_edit'
445 c.active = 'profile_edit'
446
446
447 c.perm_user = c.auth_user
447 c.perm_user = c.auth_user
448 c.extern_type = c.user.extern_type
448 c.extern_type = c.user.extern_type
449 c.extern_name = c.user.extern_name
449 c.extern_name = c.user.extern_name
450
450
451 _form = UserForm(edit=True,
451 _form = UserForm(edit=True,
452 old_data={'user_id': self._rhodecode_user.user_id,
452 old_data={'user_id': self._rhodecode_user.user_id,
453 'email': self._rhodecode_user.email})()
453 'email': self._rhodecode_user.email})()
454 form_result = {}
454 form_result = {}
455 try:
455 try:
456 post_data = dict(self.request.POST)
456 post_data = dict(self.request.POST)
457 post_data['new_password'] = ''
457 post_data['new_password'] = ''
458 post_data['password_confirmation'] = ''
458 post_data['password_confirmation'] = ''
459 form_result = _form.to_python(post_data)
459 form_result = _form.to_python(post_data)
460 # skip updating those attrs for my account
460 # skip updating those attrs for my account
461 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
461 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
462 'new_password', 'password_confirmation']
462 'new_password', 'password_confirmation']
463 # TODO: plugin should define if username can be updated
463 # TODO: plugin should define if username can be updated
464 if c.extern_type != "rhodecode":
464 if c.extern_type != "rhodecode":
465 # forbid updating username for external accounts
465 # forbid updating username for external accounts
466 skip_attrs.append('username')
466 skip_attrs.append('username')
467
467
468 UserModel().update_user(
468 UserModel().update_user(
469 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
469 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
470 **form_result)
470 **form_result)
471 h.flash(_('Your account was updated successfully'),
471 h.flash(_('Your account was updated successfully'),
472 category='success')
472 category='success')
473 Session().commit()
473 Session().commit()
474
474
475 except formencode.Invalid as errors:
475 except formencode.Invalid as errors:
476 data = render(
476 data = render(
477 'rhodecode:templates/admin/my_account/my_account.mako',
477 'rhodecode:templates/admin/my_account/my_account.mako',
478 self._get_template_context(c), self.request)
478 self._get_template_context(c), self.request)
479
479
480 html = formencode.htmlfill.render(
480 html = formencode.htmlfill.render(
481 data,
481 data,
482 defaults=errors.value,
482 defaults=errors.value,
483 errors=errors.error_dict or {},
483 errors=errors.error_dict or {},
484 prefix_error=False,
484 prefix_error=False,
485 encoding="UTF-8",
485 encoding="UTF-8",
486 force_defaults=False)
486 force_defaults=False)
487 return Response(html)
487 return Response(html)
488
488
489 except Exception:
489 except Exception:
490 log.exception("Exception updating user")
490 log.exception("Exception updating user")
491 h.flash(_('Error occurred during update of user %s')
491 h.flash(_('Error occurred during update of user %s')
492 % form_result.get('username'), category='error')
492 % form_result.get('username'), category='error')
493 raise HTTPFound(h.route_path('my_account_profile'))
493 raise HTTPFound(h.route_path('my_account_profile'))
494
494
495 raise HTTPFound(h.route_path('my_account_profile'))
495 raise HTTPFound(h.route_path('my_account_profile'))
496
496
497 def _get_pull_requests_list(self, statuses):
497 def _get_pull_requests_list(self, statuses):
498 draw, start, limit = self._extract_chunk(self.request)
498 draw, start, limit = self._extract_chunk(self.request)
499 search_q, order_by, order_dir = self._extract_ordering(self.request)
499 search_q, order_by, order_dir = self._extract_ordering(self.request)
500 _render = self.request.get_partial_renderer(
500 _render = self.request.get_partial_renderer(
501 'data_table/_dt_elements.mako')
501 'data_table/_dt_elements.mako')
502
502
503 pull_requests = PullRequestModel().get_im_participating_in(
503 pull_requests = PullRequestModel().get_im_participating_in(
504 user_id=self._rhodecode_user.user_id,
504 user_id=self._rhodecode_user.user_id,
505 statuses=statuses,
505 statuses=statuses,
506 offset=start, length=limit, order_by=order_by,
506 offset=start, length=limit, order_by=order_by,
507 order_dir=order_dir)
507 order_dir=order_dir)
508
508
509 pull_requests_total_count = PullRequestModel().count_im_participating_in(
509 pull_requests_total_count = PullRequestModel().count_im_participating_in(
510 user_id=self._rhodecode_user.user_id, statuses=statuses)
510 user_id=self._rhodecode_user.user_id, statuses=statuses)
511
511
512 data = []
512 data = []
513 comments_model = CommentsModel()
513 comments_model = CommentsModel()
514 for pr in pull_requests:
514 for pr in pull_requests:
515 repo_id = pr.target_repo_id
515 repo_id = pr.target_repo_id
516 comments = comments_model.get_all_comments(
516 comments = comments_model.get_all_comments(
517 repo_id, pull_request=pr)
517 repo_id, pull_request=pr)
518 owned = pr.user_id == self._rhodecode_user.user_id
518 owned = pr.user_id == self._rhodecode_user.user_id
519
519
520 data.append({
520 data.append({
521 'target_repo': _render('pullrequest_target_repo',
521 'target_repo': _render('pullrequest_target_repo',
522 pr.target_repo.repo_name),
522 pr.target_repo.repo_name),
523 'name': _render('pullrequest_name',
523 'name': _render('pullrequest_name',
524 pr.pull_request_id, pr.target_repo.repo_name,
524 pr.pull_request_id, pr.target_repo.repo_name,
525 short=True),
525 short=True),
526 'name_raw': pr.pull_request_id,
526 'name_raw': pr.pull_request_id,
527 'status': _render('pullrequest_status',
527 'status': _render('pullrequest_status',
528 pr.calculated_review_status()),
528 pr.calculated_review_status()),
529 'title': _render(
529 'title': _render(
530 'pullrequest_title', pr.title, pr.description),
530 'pullrequest_title', pr.title, pr.description),
531 'description': h.escape(pr.description),
531 'description': h.escape(pr.description),
532 'updated_on': _render('pullrequest_updated_on',
532 'updated_on': _render('pullrequest_updated_on',
533 h.datetime_to_time(pr.updated_on)),
533 h.datetime_to_time(pr.updated_on)),
534 'updated_on_raw': h.datetime_to_time(pr.updated_on),
534 'updated_on_raw': h.datetime_to_time(pr.updated_on),
535 'created_on': _render('pullrequest_updated_on',
535 'created_on': _render('pullrequest_updated_on',
536 h.datetime_to_time(pr.created_on)),
536 h.datetime_to_time(pr.created_on)),
537 'created_on_raw': h.datetime_to_time(pr.created_on),
537 'created_on_raw': h.datetime_to_time(pr.created_on),
538 'author': _render('pullrequest_author',
538 'author': _render('pullrequest_author',
539 pr.author.full_contact, ),
539 pr.author.full_contact, ),
540 'author_raw': pr.author.full_name,
540 'author_raw': pr.author.full_name,
541 'comments': _render('pullrequest_comments', len(comments)),
541 'comments': _render('pullrequest_comments', len(comments)),
542 'comments_raw': len(comments),
542 'comments_raw': len(comments),
543 'closed': pr.is_closed(),
543 'closed': pr.is_closed(),
544 'owned': owned
544 'owned': owned
545 })
545 })
546
546
547 # json used to render the grid
547 # json used to render the grid
548 data = ({
548 data = ({
549 'draw': draw,
549 'draw': draw,
550 'data': data,
550 'data': data,
551 'recordsTotal': pull_requests_total_count,
551 'recordsTotal': pull_requests_total_count,
552 'recordsFiltered': pull_requests_total_count,
552 'recordsFiltered': pull_requests_total_count,
553 })
553 })
554 return data
554 return data
555
555
556 @LoginRequired()
556 @LoginRequired()
557 @NotAnonymous()
557 @NotAnonymous()
558 @view_config(
558 @view_config(
559 route_name='my_account_pullrequests',
559 route_name='my_account_pullrequests',
560 request_method='GET',
560 request_method='GET',
561 renderer='rhodecode:templates/admin/my_account/my_account.mako')
561 renderer='rhodecode:templates/admin/my_account/my_account.mako')
562 def my_account_pullrequests(self):
562 def my_account_pullrequests(self):
563 c = self.load_default_context()
563 c = self.load_default_context()
564 c.active = 'pullrequests'
564 c.active = 'pullrequests'
565 req_get = self.request.GET
565 req_get = self.request.GET
566
566
567 c.closed = str2bool(req_get.get('pr_show_closed'))
567 c.closed = str2bool(req_get.get('pr_show_closed'))
568
568
569 return self._get_template_context(c)
569 return self._get_template_context(c)
570
570
571 @LoginRequired()
571 @LoginRequired()
572 @NotAnonymous()
572 @NotAnonymous()
573 @view_config(
573 @view_config(
574 route_name='my_account_pullrequests_data',
574 route_name='my_account_pullrequests_data',
575 request_method='GET', renderer='json_ext')
575 request_method='GET', renderer='json_ext')
576 def my_account_pullrequests_data(self):
576 def my_account_pullrequests_data(self):
577 req_get = self.request.GET
577 req_get = self.request.GET
578 closed = str2bool(req_get.get('closed'))
578 closed = str2bool(req_get.get('closed'))
579
579
580 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
580 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
581 if closed:
581 if closed:
582 statuses += [PullRequest.STATUS_CLOSED]
582 statuses += [PullRequest.STATUS_CLOSED]
583
583
584 data = self._get_pull_requests_list(statuses=statuses)
584 data = self._get_pull_requests_list(statuses=statuses)
585 return data
585 return data
586
586
@@ -1,557 +1,557 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31
31
32 from rhodecode.lib import diffs, codeblocks
32 from rhodecode.lib import diffs, codeblocks
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35
35
36 from rhodecode.lib.compat import OrderedDict
36 from rhodecode.lib.compat import OrderedDict
37 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
37 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
38 import rhodecode.lib.helpers as h
38 import rhodecode.lib.helpers as h
39 from rhodecode.lib.utils2 import safe_unicode, safe_int
39 from rhodecode.lib.utils2 import safe_unicode, safe_int
40 from rhodecode.lib.vcs.backends.base import EmptyCommit
40 from rhodecode.lib.vcs.backends.base import EmptyCommit
41 from rhodecode.lib.vcs.exceptions import (
41 from rhodecode.lib.vcs.exceptions import (
42 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
42 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
43 from rhodecode.model.db import ChangesetComment, ChangesetStatus
43 from rhodecode.model.db import ChangesetComment, ChangesetStatus
44 from rhodecode.model.changeset_status import ChangesetStatusModel
44 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.comment import CommentsModel
45 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 def _update_with_GET(params, request):
52 def _update_with_GET(params, request):
53 for k in ['diff1', 'diff2', 'diff']:
53 for k in ['diff1', 'diff2', 'diff']:
54 params[k] += request.GET.getall(k)
54 params[k] += request.GET.getall(k)
55
55
56
56
57 def get_ignore_ws(fid, request):
57 def get_ignore_ws(fid, request):
58 ig_ws_global = request.GET.get('ignorews')
58 ig_ws_global = request.GET.get('ignorews')
59 ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid))
59 ig_ws = filter(lambda k: k.startswith('WS'), request.GET.getall(fid))
60 if ig_ws:
60 if ig_ws:
61 try:
61 try:
62 return int(ig_ws[0].split(':')[-1])
62 return int(ig_ws[0].split(':')[-1])
63 except Exception:
63 except Exception:
64 pass
64 pass
65 return ig_ws_global
65 return ig_ws_global
66
66
67
67
68 def _ignorews_url(request, fileid=None):
68 def _ignorews_url(request, fileid=None):
69 _ = request.translate
69 _ = request.translate
70 fileid = str(fileid) if fileid else None
70 fileid = str(fileid) if fileid else None
71 params = collections.defaultdict(list)
71 params = collections.defaultdict(list)
72 _update_with_GET(params, request)
72 _update_with_GET(params, request)
73 label = _('Show whitespace')
73 label = _('Show whitespace')
74 tooltiplbl = _('Show whitespace for all diffs')
74 tooltiplbl = _('Show whitespace for all diffs')
75 ig_ws = get_ignore_ws(fileid, request)
75 ig_ws = get_ignore_ws(fileid, request)
76 ln_ctx = get_line_ctx(fileid, request)
76 ln_ctx = get_line_ctx(fileid, request)
77
77
78 if ig_ws is None:
78 if ig_ws is None:
79 params['ignorews'] += [1]
79 params['ignorews'] += [1]
80 label = _('Ignore whitespace')
80 label = _('Ignore whitespace')
81 tooltiplbl = _('Ignore whitespace for all diffs')
81 tooltiplbl = _('Ignore whitespace for all diffs')
82 ctx_key = 'context'
82 ctx_key = 'context'
83 ctx_val = ln_ctx
83 ctx_val = ln_ctx
84
84
85 # if we have passed in ln_ctx pass it along to our params
85 # if we have passed in ln_ctx pass it along to our params
86 if ln_ctx:
86 if ln_ctx:
87 params[ctx_key] += [ctx_val]
87 params[ctx_key] += [ctx_val]
88
88
89 if fileid:
89 if fileid:
90 params['anchor'] = 'a_' + fileid
90 params['anchor'] = 'a_' + fileid
91 return h.link_to(label, request.current_route_path(_query=params),
91 return h.link_to(label, request.current_route_path(_query=params),
92 title=tooltiplbl, class_='tooltip')
92 title=tooltiplbl, class_='tooltip')
93
93
94
94
95 def get_line_ctx(fid, request):
95 def get_line_ctx(fid, request):
96 ln_ctx_global = request.GET.get('context')
96 ln_ctx_global = request.GET.get('context')
97 if fid:
97 if fid:
98 ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid))
98 ln_ctx = filter(lambda k: k.startswith('C'), request.GET.getall(fid))
99 else:
99 else:
100 _ln_ctx = filter(lambda k: k.startswith('C'), request.GET)
100 _ln_ctx = filter(lambda k: k.startswith('C'), request.GET)
101 ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
101 ln_ctx = request.GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
102 if ln_ctx:
102 if ln_ctx:
103 ln_ctx = [ln_ctx]
103 ln_ctx = [ln_ctx]
104
104
105 if ln_ctx:
105 if ln_ctx:
106 retval = ln_ctx[0].split(':')[-1]
106 retval = ln_ctx[0].split(':')[-1]
107 else:
107 else:
108 retval = ln_ctx_global
108 retval = ln_ctx_global
109
109
110 try:
110 try:
111 return int(retval)
111 return int(retval)
112 except Exception:
112 except Exception:
113 return 3
113 return 3
114
114
115
115
116 def _context_url(request, fileid=None):
116 def _context_url(request, fileid=None):
117 """
117 """
118 Generates a url for context lines.
118 Generates a url for context lines.
119
119
120 :param fileid:
120 :param fileid:
121 """
121 """
122
122
123 _ = request.translate
123 _ = request.translate
124 fileid = str(fileid) if fileid else None
124 fileid = str(fileid) if fileid else None
125 ig_ws = get_ignore_ws(fileid, request)
125 ig_ws = get_ignore_ws(fileid, request)
126 ln_ctx = (get_line_ctx(fileid, request) or 3) * 2
126 ln_ctx = (get_line_ctx(fileid, request) or 3) * 2
127
127
128 params = collections.defaultdict(list)
128 params = collections.defaultdict(list)
129 _update_with_GET(params, request)
129 _update_with_GET(params, request)
130
130
131 if ln_ctx > 0:
131 if ln_ctx > 0:
132 params['context'] += [ln_ctx]
132 params['context'] += [ln_ctx]
133
133
134 if ig_ws:
134 if ig_ws:
135 ig_ws_key = 'ignorews'
135 ig_ws_key = 'ignorews'
136 ig_ws_val = 1
136 ig_ws_val = 1
137 params[ig_ws_key] += [ig_ws_val]
137 params[ig_ws_key] += [ig_ws_val]
138
138
139 lbl = _('Increase context')
139 lbl = _('Increase context')
140 tooltiplbl = _('Increase context for all diffs')
140 tooltiplbl = _('Increase context for all diffs')
141
141
142 if fileid:
142 if fileid:
143 params['anchor'] = 'a_' + fileid
143 params['anchor'] = 'a_' + fileid
144 return h.link_to(lbl, request.current_route_path(_query=params),
144 return h.link_to(lbl, request.current_route_path(_query=params),
145 title=tooltiplbl, class_='tooltip')
145 title=tooltiplbl, class_='tooltip')
146
146
147
147
148 class RepoCommitsView(RepoAppView):
148 class RepoCommitsView(RepoAppView):
149 def load_default_context(self):
149 def load_default_context(self):
150 c = self._get_local_tmpl_context(include_app_defaults=True)
150 c = self._get_local_tmpl_context(include_app_defaults=True)
151
151
152 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
152 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
153 c.repo_info = self.db_repo
153 c.repo_info = self.db_repo
154 c.rhodecode_repo = self.rhodecode_vcs_repo
154 c.rhodecode_repo = self.rhodecode_vcs_repo
155
155
156 self._register_global_c(c)
156 self._register_global_c(c)
157 return c
157 return c
158
158
159 def _commit(self, commit_id_range, method):
159 def _commit(self, commit_id_range, method):
160 _ = self.request.translate
160 _ = self.request.translate
161 c = self.load_default_context()
161 c = self.load_default_context()
162 c.ignorews_url = _ignorews_url
162 c.ignorews_url = _ignorews_url
163 c.context_url = _context_url
163 c.context_url = _context_url
164 c.fulldiff = self.request.GET.get('fulldiff')
164 c.fulldiff = self.request.GET.get('fulldiff')
165
165
166 # fetch global flags of ignore ws or context lines
166 # fetch global flags of ignore ws or context lines
167 context_lcl = get_line_ctx('', self.request)
167 context_lcl = get_line_ctx('', self.request)
168 ign_whitespace_lcl = get_ignore_ws('', self.request)
168 ign_whitespace_lcl = get_ignore_ws('', self.request)
169
169
170 # diff_limit will cut off the whole diff if the limit is applied
170 # diff_limit will cut off the whole diff if the limit is applied
171 # otherwise it will just hide the big files from the front-end
171 # otherwise it will just hide the big files from the front-end
172 diff_limit = c.visual.cut_off_limit_diff
172 diff_limit = c.visual.cut_off_limit_diff
173 file_limit = c.visual.cut_off_limit_file
173 file_limit = c.visual.cut_off_limit_file
174
174
175 # get ranges of commit ids if preset
175 # get ranges of commit ids if preset
176 commit_range = commit_id_range.split('...')[:2]
176 commit_range = commit_id_range.split('...')[:2]
177
177
178 try:
178 try:
179 pre_load = ['affected_files', 'author', 'branch', 'date',
179 pre_load = ['affected_files', 'author', 'branch', 'date',
180 'message', 'parents']
180 'message', 'parents']
181
181
182 if len(commit_range) == 2:
182 if len(commit_range) == 2:
183 commits = self.rhodecode_vcs_repo.get_commits(
183 commits = self.rhodecode_vcs_repo.get_commits(
184 start_id=commit_range[0], end_id=commit_range[1],
184 start_id=commit_range[0], end_id=commit_range[1],
185 pre_load=pre_load)
185 pre_load=pre_load)
186 commits = list(commits)
186 commits = list(commits)
187 else:
187 else:
188 commits = [self.rhodecode_vcs_repo.get_commit(
188 commits = [self.rhodecode_vcs_repo.get_commit(
189 commit_id=commit_id_range, pre_load=pre_load)]
189 commit_id=commit_id_range, pre_load=pre_load)]
190
190
191 c.commit_ranges = commits
191 c.commit_ranges = commits
192 if not c.commit_ranges:
192 if not c.commit_ranges:
193 raise RepositoryError(
193 raise RepositoryError(
194 'The commit range returned an empty result')
194 'The commit range returned an empty result')
195 except CommitDoesNotExistError:
195 except CommitDoesNotExistError:
196 msg = _('No such commit exists for this repository')
196 msg = _('No such commit exists for this repository')
197 h.flash(msg, category='error')
197 h.flash(msg, category='error')
198 raise HTTPNotFound()
198 raise HTTPNotFound()
199 except Exception:
199 except Exception:
200 log.exception("General failure")
200 log.exception("General failure")
201 raise HTTPNotFound()
201 raise HTTPNotFound()
202
202
203 c.changes = OrderedDict()
203 c.changes = OrderedDict()
204 c.lines_added = 0
204 c.lines_added = 0
205 c.lines_deleted = 0
205 c.lines_deleted = 0
206
206
207 # auto collapse if we have more than limit
207 # auto collapse if we have more than limit
208 collapse_limit = diffs.DiffProcessor._collapse_commits_over
208 collapse_limit = diffs.DiffProcessor._collapse_commits_over
209 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
209 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
210
210
211 c.commit_statuses = ChangesetStatus.STATUSES
211 c.commit_statuses = ChangesetStatus.STATUSES
212 c.inline_comments = []
212 c.inline_comments = []
213 c.files = []
213 c.files = []
214
214
215 c.statuses = []
215 c.statuses = []
216 c.comments = []
216 c.comments = []
217 c.unresolved_comments = []
217 c.unresolved_comments = []
218 if len(c.commit_ranges) == 1:
218 if len(c.commit_ranges) == 1:
219 commit = c.commit_ranges[0]
219 commit = c.commit_ranges[0]
220 c.comments = CommentsModel().get_comments(
220 c.comments = CommentsModel().get_comments(
221 self.db_repo.repo_id,
221 self.db_repo.repo_id,
222 revision=commit.raw_id)
222 revision=commit.raw_id)
223 c.statuses.append(ChangesetStatusModel().get_status(
223 c.statuses.append(ChangesetStatusModel().get_status(
224 self.db_repo.repo_id, commit.raw_id))
224 self.db_repo.repo_id, commit.raw_id))
225 # comments from PR
225 # comments from PR
226 statuses = ChangesetStatusModel().get_statuses(
226 statuses = ChangesetStatusModel().get_statuses(
227 self.db_repo.repo_id, commit.raw_id,
227 self.db_repo.repo_id, commit.raw_id,
228 with_revisions=True)
228 with_revisions=True)
229 prs = set(st.pull_request for st in statuses
229 prs = set(st.pull_request for st in statuses
230 if st.pull_request is not None)
230 if st.pull_request is not None)
231 # from associated statuses, check the pull requests, and
231 # from associated statuses, check the pull requests, and
232 # show comments from them
232 # show comments from them
233 for pr in prs:
233 for pr in prs:
234 c.comments.extend(pr.comments)
234 c.comments.extend(pr.comments)
235
235
236 c.unresolved_comments = CommentsModel()\
236 c.unresolved_comments = CommentsModel()\
237 .get_commit_unresolved_todos(commit.raw_id)
237 .get_commit_unresolved_todos(commit.raw_id)
238
238
239 diff = None
239 diff = None
240 # Iterate over ranges (default commit view is always one commit)
240 # Iterate over ranges (default commit view is always one commit)
241 for commit in c.commit_ranges:
241 for commit in c.commit_ranges:
242 c.changes[commit.raw_id] = []
242 c.changes[commit.raw_id] = []
243
243
244 commit2 = commit
244 commit2 = commit
245 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
245 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
246
246
247 _diff = self.rhodecode_vcs_repo.get_diff(
247 _diff = self.rhodecode_vcs_repo.get_diff(
248 commit1, commit2,
248 commit1, commit2,
249 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
249 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
250 diff_processor = diffs.DiffProcessor(
250 diff_processor = diffs.DiffProcessor(
251 _diff, format='newdiff', diff_limit=diff_limit,
251 _diff, format='newdiff', diff_limit=diff_limit,
252 file_limit=file_limit, show_full_diff=c.fulldiff)
252 file_limit=file_limit, show_full_diff=c.fulldiff)
253
253
254 commit_changes = OrderedDict()
254 commit_changes = OrderedDict()
255 if method == 'show':
255 if method == 'show':
256 _parsed = diff_processor.prepare()
256 _parsed = diff_processor.prepare()
257 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
257 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
258
258
259 _parsed = diff_processor.prepare()
259 _parsed = diff_processor.prepare()
260
260
261 def _node_getter(commit):
261 def _node_getter(commit):
262 def get_node(fname):
262 def get_node(fname):
263 try:
263 try:
264 return commit.get_node(fname)
264 return commit.get_node(fname)
265 except NodeDoesNotExistError:
265 except NodeDoesNotExistError:
266 return None
266 return None
267 return get_node
267 return get_node
268
268
269 inline_comments = CommentsModel().get_inline_comments(
269 inline_comments = CommentsModel().get_inline_comments(
270 self.db_repo.repo_id, revision=commit.raw_id)
270 self.db_repo.repo_id, revision=commit.raw_id)
271 c.inline_cnt = CommentsModel().get_inline_comments_count(
271 c.inline_cnt = CommentsModel().get_inline_comments_count(
272 inline_comments)
272 inline_comments)
273
273
274 diffset = codeblocks.DiffSet(
274 diffset = codeblocks.DiffSet(
275 repo_name=self.db_repo_name,
275 repo_name=self.db_repo_name,
276 source_node_getter=_node_getter(commit1),
276 source_node_getter=_node_getter(commit1),
277 target_node_getter=_node_getter(commit2),
277 target_node_getter=_node_getter(commit2),
278 comments=inline_comments)
278 comments=inline_comments)
279 diffset = diffset.render_patchset(
279 diffset = diffset.render_patchset(
280 _parsed, commit1.raw_id, commit2.raw_id)
280 _parsed, commit1.raw_id, commit2.raw_id)
281
281
282 c.changes[commit.raw_id] = diffset
282 c.changes[commit.raw_id] = diffset
283 else:
283 else:
284 # downloads/raw we only need RAW diff nothing else
284 # downloads/raw we only need RAW diff nothing else
285 diff = diff_processor.as_raw()
285 diff = diff_processor.as_raw()
286 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
286 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
287
287
288 # sort comments by how they were generated
288 # sort comments by how they were generated
289 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
289 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
290
290
291 if len(c.commit_ranges) == 1:
291 if len(c.commit_ranges) == 1:
292 c.commit = c.commit_ranges[0]
292 c.commit = c.commit_ranges[0]
293 c.parent_tmpl = ''.join(
293 c.parent_tmpl = ''.join(
294 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
294 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
295
295
296 if method == 'download':
296 if method == 'download':
297 response = Response(diff)
297 response = Response(diff)
298 response.content_type = 'text/plain'
298 response.content_type = 'text/plain'
299 response.content_disposition = (
299 response.content_disposition = (
300 'attachment; filename=%s.diff' % commit_id_range[:12])
300 'attachment; filename=%s.diff' % commit_id_range[:12])
301 return response
301 return response
302 elif method == 'patch':
302 elif method == 'patch':
303 c.diff = safe_unicode(diff)
303 c.diff = safe_unicode(diff)
304 patch = render(
304 patch = render(
305 'rhodecode:templates/changeset/patch_changeset.mako',
305 'rhodecode:templates/changeset/patch_changeset.mako',
306 self._get_template_context(c), self.request)
306 self._get_template_context(c), self.request)
307 response = Response(patch)
307 response = Response(patch)
308 response.content_type = 'text/plain'
308 response.content_type = 'text/plain'
309 return response
309 return response
310 elif method == 'raw':
310 elif method == 'raw':
311 response = Response(diff)
311 response = Response(diff)
312 response.content_type = 'text/plain'
312 response.content_type = 'text/plain'
313 return response
313 return response
314 elif method == 'show':
314 elif method == 'show':
315 if len(c.commit_ranges) == 1:
315 if len(c.commit_ranges) == 1:
316 html = render(
316 html = render(
317 'rhodecode:templates/changeset/changeset.mako',
317 'rhodecode:templates/changeset/changeset.mako',
318 self._get_template_context(c), self.request)
318 self._get_template_context(c), self.request)
319 return Response(html)
319 return Response(html)
320 else:
320 else:
321 c.ancestor = None
321 c.ancestor = None
322 c.target_repo = self.db_repo
322 c.target_repo = self.db_repo
323 html = render(
323 html = render(
324 'rhodecode:templates/changeset/changeset_range.mako',
324 'rhodecode:templates/changeset/changeset_range.mako',
325 self._get_template_context(c), self.request)
325 self._get_template_context(c), self.request)
326 return Response(html)
326 return Response(html)
327
327
328 raise HTTPBadRequest()
328 raise HTTPBadRequest()
329
329
330 @LoginRequired()
330 @LoginRequired()
331 @HasRepoPermissionAnyDecorator(
331 @HasRepoPermissionAnyDecorator(
332 'repository.read', 'repository.write', 'repository.admin')
332 'repository.read', 'repository.write', 'repository.admin')
333 @view_config(
333 @view_config(
334 route_name='repo_commit', request_method='GET',
334 route_name='repo_commit', request_method='GET',
335 renderer=None)
335 renderer=None)
336 def repo_commit_show(self):
336 def repo_commit_show(self):
337 commit_id = self.request.matchdict['commit_id']
337 commit_id = self.request.matchdict['commit_id']
338 return self._commit(commit_id, method='show')
338 return self._commit(commit_id, method='show')
339
339
340 @LoginRequired()
340 @LoginRequired()
341 @HasRepoPermissionAnyDecorator(
341 @HasRepoPermissionAnyDecorator(
342 'repository.read', 'repository.write', 'repository.admin')
342 'repository.read', 'repository.write', 'repository.admin')
343 @view_config(
343 @view_config(
344 route_name='repo_commit_raw', request_method='GET',
344 route_name='repo_commit_raw', request_method='GET',
345 renderer=None)
345 renderer=None)
346 @view_config(
346 @view_config(
347 route_name='repo_commit_raw_deprecated', request_method='GET',
347 route_name='repo_commit_raw_deprecated', request_method='GET',
348 renderer=None)
348 renderer=None)
349 def repo_commit_raw(self):
349 def repo_commit_raw(self):
350 commit_id = self.request.matchdict['commit_id']
350 commit_id = self.request.matchdict['commit_id']
351 return self._commit(commit_id, method='raw')
351 return self._commit(commit_id, method='raw')
352
352
353 @LoginRequired()
353 @LoginRequired()
354 @HasRepoPermissionAnyDecorator(
354 @HasRepoPermissionAnyDecorator(
355 'repository.read', 'repository.write', 'repository.admin')
355 'repository.read', 'repository.write', 'repository.admin')
356 @view_config(
356 @view_config(
357 route_name='repo_commit_patch', request_method='GET',
357 route_name='repo_commit_patch', request_method='GET',
358 renderer=None)
358 renderer=None)
359 def repo_commit_patch(self):
359 def repo_commit_patch(self):
360 commit_id = self.request.matchdict['commit_id']
360 commit_id = self.request.matchdict['commit_id']
361 return self._commit(commit_id, method='patch')
361 return self._commit(commit_id, method='patch')
362
362
363 @LoginRequired()
363 @LoginRequired()
364 @HasRepoPermissionAnyDecorator(
364 @HasRepoPermissionAnyDecorator(
365 'repository.read', 'repository.write', 'repository.admin')
365 'repository.read', 'repository.write', 'repository.admin')
366 @view_config(
366 @view_config(
367 route_name='repo_commit_download', request_method='GET',
367 route_name='repo_commit_download', request_method='GET',
368 renderer=None)
368 renderer=None)
369 def repo_commit_download(self):
369 def repo_commit_download(self):
370 commit_id = self.request.matchdict['commit_id']
370 commit_id = self.request.matchdict['commit_id']
371 return self._commit(commit_id, method='download')
371 return self._commit(commit_id, method='download')
372
372
373 @LoginRequired()
373 @LoginRequired()
374 @NotAnonymous()
374 @NotAnonymous()
375 @HasRepoPermissionAnyDecorator(
375 @HasRepoPermissionAnyDecorator(
376 'repository.read', 'repository.write', 'repository.admin')
376 'repository.read', 'repository.write', 'repository.admin')
377 @CSRFRequired()
377 @CSRFRequired()
378 @view_config(
378 @view_config(
379 route_name='repo_commit_comment_create', request_method='POST',
379 route_name='repo_commit_comment_create', request_method='POST',
380 renderer='json_ext')
380 renderer='json_ext')
381 def repo_commit_comment_create(self):
381 def repo_commit_comment_create(self):
382 _ = self.request.translate
382 _ = self.request.translate
383 commit_id = self.request.matchdict['commit_id']
383 commit_id = self.request.matchdict['commit_id']
384
384
385 c = self.load_default_context()
385 c = self.load_default_context()
386 status = self.request.POST.get('changeset_status', None)
386 status = self.request.POST.get('changeset_status', None)
387 text = self.request.POST.get('text')
387 text = self.request.POST.get('text')
388 comment_type = self.request.POST.get('comment_type')
388 comment_type = self.request.POST.get('comment_type')
389 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
389 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
390
390
391 if status:
391 if status:
392 text = text or (_('Status change %(transition_icon)s %(status)s')
392 text = text or (_('Status change %(transition_icon)s %(status)s')
393 % {'transition_icon': '>',
393 % {'transition_icon': '>',
394 'status': ChangesetStatus.get_status_lbl(status)})
394 'status': ChangesetStatus.get_status_lbl(status)})
395
395
396 multi_commit_ids = []
396 multi_commit_ids = []
397 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
397 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
398 if _commit_id not in ['', None, EmptyCommit.raw_id]:
398 if _commit_id not in ['', None, EmptyCommit.raw_id]:
399 if _commit_id not in multi_commit_ids:
399 if _commit_id not in multi_commit_ids:
400 multi_commit_ids.append(_commit_id)
400 multi_commit_ids.append(_commit_id)
401
401
402 commit_ids = multi_commit_ids or [commit_id]
402 commit_ids = multi_commit_ids or [commit_id]
403
403
404 comment = None
404 comment = None
405 for current_id in filter(None, commit_ids):
405 for current_id in filter(None, commit_ids):
406 comment = CommentsModel().create(
406 comment = CommentsModel().create(
407 text=text,
407 text=text,
408 repo=self.db_repo.repo_id,
408 repo=self.db_repo.repo_id,
409 user=self._rhodecode_db_user.user_id,
409 user=self._rhodecode_db_user.user_id,
410 commit_id=current_id,
410 commit_id=current_id,
411 f_path=self.request.POST.get('f_path'),
411 f_path=self.request.POST.get('f_path'),
412 line_no=self.request.POST.get('line'),
412 line_no=self.request.POST.get('line'),
413 status_change=(ChangesetStatus.get_status_lbl(status)
413 status_change=(ChangesetStatus.get_status_lbl(status)
414 if status else None),
414 if status else None),
415 status_change_type=status,
415 status_change_type=status,
416 comment_type=comment_type,
416 comment_type=comment_type,
417 resolves_comment_id=resolves_comment_id
417 resolves_comment_id=resolves_comment_id
418 )
418 )
419
419
420 # get status if set !
420 # get status if set !
421 if status:
421 if status:
422 # if latest status was from pull request and it's closed
422 # if latest status was from pull request and it's closed
423 # disallow changing status !
423 # disallow changing status !
424 # dont_allow_on_closed_pull_request = True !
424 # dont_allow_on_closed_pull_request = True !
425
425
426 try:
426 try:
427 ChangesetStatusModel().set_status(
427 ChangesetStatusModel().set_status(
428 self.db_repo.repo_id,
428 self.db_repo.repo_id,
429 status,
429 status,
430 self._rhodecode_db_user.user_id,
430 self._rhodecode_db_user.user_id,
431 comment,
431 comment,
432 revision=current_id,
432 revision=current_id,
433 dont_allow_on_closed_pull_request=True
433 dont_allow_on_closed_pull_request=True
434 )
434 )
435 except StatusChangeOnClosedPullRequestError:
435 except StatusChangeOnClosedPullRequestError:
436 msg = _('Changing the status of a commit associated with '
436 msg = _('Changing the status of a commit associated with '
437 'a closed pull request is not allowed')
437 'a closed pull request is not allowed')
438 log.exception(msg)
438 log.exception(msg)
439 h.flash(msg, category='warning')
439 h.flash(msg, category='warning')
440 raise HTTPFound(h.route_path(
440 raise HTTPFound(h.route_path(
441 'repo_commit', repo_name=self.db_repo_name,
441 'repo_commit', repo_name=self.db_repo_name,
442 commit_id=current_id))
442 commit_id=current_id))
443
443
444 # finalize, commit and redirect
444 # finalize, commit and redirect
445 Session().commit()
445 Session().commit()
446
446
447 data = {
447 data = {
448 'target_id': h.safeid(h.safe_unicode(
448 'target_id': h.safeid(h.safe_unicode(
449 self.request.POST.get('f_path'))),
449 self.request.POST.get('f_path'))),
450 }
450 }
451 if comment:
451 if comment:
452 c.co = comment
452 c.co = comment
453 rendered_comment = render(
453 rendered_comment = render(
454 'rhodecode:templates/changeset/changeset_comment_block.mako',
454 'rhodecode:templates/changeset/changeset_comment_block.mako',
455 self._get_template_context(c), self.request)
455 self._get_template_context(c), self.request)
456
456
457 data.update(comment.get_dict())
457 data.update(comment.get_dict())
458 data.update({'rendered_text': rendered_comment})
458 data.update({'rendered_text': rendered_comment})
459
459
460 return data
460 return data
461
461
462 @LoginRequired()
462 @LoginRequired()
463 @NotAnonymous()
463 @NotAnonymous()
464 @HasRepoPermissionAnyDecorator(
464 @HasRepoPermissionAnyDecorator(
465 'repository.read', 'repository.write', 'repository.admin')
465 'repository.read', 'repository.write', 'repository.admin')
466 @CSRFRequired()
466 @CSRFRequired()
467 @view_config(
467 @view_config(
468 route_name='repo_commit_comment_preview', request_method='POST',
468 route_name='repo_commit_comment_preview', request_method='POST',
469 renderer='string', xhr=True)
469 renderer='string', xhr=True)
470 def repo_commit_comment_preview(self):
470 def repo_commit_comment_preview(self):
471 # Technically a CSRF token is not needed as no state changes with this
471 # Technically a CSRF token is not needed as no state changes with this
472 # call. However, as this is a POST is better to have it, so automated
472 # call. However, as this is a POST is better to have it, so automated
473 # tools don't flag it as potential CSRF.
473 # tools don't flag it as potential CSRF.
474 # Post is required because the payload could be bigger than the maximum
474 # Post is required because the payload could be bigger than the maximum
475 # allowed by GET.
475 # allowed by GET.
476
476
477 text = self.request.POST.get('text')
477 text = self.request.POST.get('text')
478 renderer = self.request.POST.get('renderer') or 'rst'
478 renderer = self.request.POST.get('renderer') or 'rst'
479 if text:
479 if text:
480 return h.render(text, renderer=renderer, mentions=True)
480 return h.render(text, renderer=renderer, mentions=True)
481 return ''
481 return ''
482
482
483 @LoginRequired()
483 @LoginRequired()
484 @NotAnonymous()
484 @NotAnonymous()
485 @HasRepoPermissionAnyDecorator(
485 @HasRepoPermissionAnyDecorator(
486 'repository.read', 'repository.write', 'repository.admin')
486 'repository.read', 'repository.write', 'repository.admin')
487 @CSRFRequired()
487 @CSRFRequired()
488 @view_config(
488 @view_config(
489 route_name='repo_commit_comment_delete', request_method='POST',
489 route_name='repo_commit_comment_delete', request_method='POST',
490 renderer='json_ext')
490 renderer='json_ext')
491 def repo_commit_comment_delete(self):
491 def repo_commit_comment_delete(self):
492 commit_id = self.request.matchdict['commit_id']
492 commit_id = self.request.matchdict['commit_id']
493 comment_id = self.request.matchdict['comment_id']
493 comment_id = self.request.matchdict['comment_id']
494
494
495 comment = ChangesetComment.get_or_404(safe_int(comment_id))
495 comment = ChangesetComment.get_or_404(comment_id)
496 if not comment:
496 if not comment:
497 log.debug('Comment with id:%s not found, skipping', comment_id)
497 log.debug('Comment with id:%s not found, skipping', comment_id)
498 # comment already deleted in another call probably
498 # comment already deleted in another call probably
499 return True
499 return True
500
500
501 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
501 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
502 super_admin = h.HasPermissionAny('hg.admin')()
502 super_admin = h.HasPermissionAny('hg.admin')()
503 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
503 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
504 is_repo_comment = comment.repo.repo_name == self.db_repo_name
504 is_repo_comment = comment.repo.repo_name == self.db_repo_name
505 comment_repo_admin = is_repo_admin and is_repo_comment
505 comment_repo_admin = is_repo_admin and is_repo_comment
506
506
507 if super_admin or comment_owner or comment_repo_admin:
507 if super_admin or comment_owner or comment_repo_admin:
508 CommentsModel().delete(comment=comment, user=self._rhodecode_db_user)
508 CommentsModel().delete(comment=comment, user=self._rhodecode_db_user)
509 Session().commit()
509 Session().commit()
510 return True
510 return True
511 else:
511 else:
512 log.warning('No permissions for user %s to delete comment_id: %s',
512 log.warning('No permissions for user %s to delete comment_id: %s',
513 self._rhodecode_db_user, comment_id)
513 self._rhodecode_db_user, comment_id)
514 raise HTTPNotFound()
514 raise HTTPNotFound()
515
515
516 @LoginRequired()
516 @LoginRequired()
517 @HasRepoPermissionAnyDecorator(
517 @HasRepoPermissionAnyDecorator(
518 'repository.read', 'repository.write', 'repository.admin')
518 'repository.read', 'repository.write', 'repository.admin')
519 @view_config(
519 @view_config(
520 route_name='repo_commit_data', request_method='GET',
520 route_name='repo_commit_data', request_method='GET',
521 renderer='json_ext', xhr=True)
521 renderer='json_ext', xhr=True)
522 def repo_commit_data(self):
522 def repo_commit_data(self):
523 commit_id = self.request.matchdict['commit_id']
523 commit_id = self.request.matchdict['commit_id']
524 self.load_default_context()
524 self.load_default_context()
525
525
526 try:
526 try:
527 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
527 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
528 except CommitDoesNotExistError as e:
528 except CommitDoesNotExistError as e:
529 return EmptyCommit(message=str(e))
529 return EmptyCommit(message=str(e))
530
530
531 @LoginRequired()
531 @LoginRequired()
532 @HasRepoPermissionAnyDecorator(
532 @HasRepoPermissionAnyDecorator(
533 'repository.read', 'repository.write', 'repository.admin')
533 'repository.read', 'repository.write', 'repository.admin')
534 @view_config(
534 @view_config(
535 route_name='repo_commit_children', request_method='GET',
535 route_name='repo_commit_children', request_method='GET',
536 renderer='json_ext', xhr=True)
536 renderer='json_ext', xhr=True)
537 def repo_commit_children(self):
537 def repo_commit_children(self):
538 commit_id = self.request.matchdict['commit_id']
538 commit_id = self.request.matchdict['commit_id']
539 self.load_default_context()
539 self.load_default_context()
540
540
541 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
541 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
542 result = {"results": commit.children}
542 result = {"results": commit.children}
543 return result
543 return result
544
544
545 @LoginRequired()
545 @LoginRequired()
546 @HasRepoPermissionAnyDecorator(
546 @HasRepoPermissionAnyDecorator(
547 'repository.read', 'repository.write', 'repository.admin')
547 'repository.read', 'repository.write', 'repository.admin')
548 @view_config(
548 @view_config(
549 route_name='repo_commit_parents', request_method='GET',
549 route_name='repo_commit_parents', request_method='GET',
550 renderer='json_ext')
550 renderer='json_ext')
551 def repo_commit_parents(self):
551 def repo_commit_parents(self):
552 commit_id = self.request.matchdict['commit_id']
552 commit_id = self.request.matchdict['commit_id']
553 self.load_default_context()
553 self.load_default_context()
554
554
555 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
555 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
556 result = {"results": commit.parents}
556 result = {"results": commit.parents}
557 return result
557 return result
@@ -1,1018 +1,1018 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import peppercorn
24 import peppercorn
25 import formencode
25 import formencode
26 import logging
26 import logging
27 import collections
27 import collections
28
28
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 from pylons import request, tmpl_context as c, url
30 from pylons import request, tmpl_context as c, url
31 from pylons.controllers.util import redirect
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
34 from pyramid.httpexceptions import HTTPFound
34 from pyramid.httpexceptions import HTTPFound
35 from sqlalchemy.sql import func
35 from sqlalchemy.sql import func
36 from sqlalchemy.sql.expression import or_
36 from sqlalchemy.sql.expression import or_
37
37
38 from rhodecode import events
38 from rhodecode import events
39 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
39 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib.ext_json import json
40 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.base import (
41 from rhodecode.lib.base import (
42 BaseRepoController, render, vcs_operation_context)
42 BaseRepoController, render, vcs_operation_context)
43 from rhodecode.lib.auth import (
43 from rhodecode.lib.auth import (
44 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
44 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 HasAcceptedRepoType, XHRRequired)
45 HasAcceptedRepoType, XHRRequired)
46 from rhodecode.lib.channelstream import channelstream_request
46 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.utils import jsonify
47 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils2 import (
48 from rhodecode.lib.utils2 import (
49 safe_int, safe_str, str2bool, safe_unicode)
49 safe_int, safe_str, str2bool, safe_unicode)
50 from rhodecode.lib.vcs.backends.base import (
50 from rhodecode.lib.vcs.backends.base import (
51 EmptyCommit, UpdateFailureReason, EmptyRepository)
51 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 from rhodecode.lib.vcs.exceptions import (
52 from rhodecode.lib.vcs.exceptions import (
53 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
53 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 NodeDoesNotExistError)
54 NodeDoesNotExistError)
55
55
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 (PullRequest, ChangesetStatus, ChangesetComment,
58 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 Repository, PullRequestVersion)
59 Repository, PullRequestVersion)
60 from rhodecode.model.forms import PullRequestForm
60 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
62 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
63
63
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66
66
67 class PullrequestsController(BaseRepoController):
67 class PullrequestsController(BaseRepoController):
68
68
69 def __before__(self):
69 def __before__(self):
70 super(PullrequestsController, self).__before__()
70 super(PullrequestsController, self).__before__()
71 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
71 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
72 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
72 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
73
73
74 @LoginRequired()
74 @LoginRequired()
75 @NotAnonymous()
75 @NotAnonymous()
76 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
76 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
77 'repository.admin')
77 'repository.admin')
78 @HasAcceptedRepoType('git', 'hg')
78 @HasAcceptedRepoType('git', 'hg')
79 def index(self):
79 def index(self):
80 source_repo = c.rhodecode_db_repo
80 source_repo = c.rhodecode_db_repo
81
81
82 try:
82 try:
83 source_repo.scm_instance().get_commit()
83 source_repo.scm_instance().get_commit()
84 except EmptyRepositoryError:
84 except EmptyRepositoryError:
85 h.flash(h.literal(_('There are no commits yet')),
85 h.flash(h.literal(_('There are no commits yet')),
86 category='warning')
86 category='warning')
87 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
87 redirect(h.route_path('repo_summary', repo_name=source_repo.repo_name))
88
88
89 commit_id = request.GET.get('commit')
89 commit_id = request.GET.get('commit')
90 branch_ref = request.GET.get('branch')
90 branch_ref = request.GET.get('branch')
91 bookmark_ref = request.GET.get('bookmark')
91 bookmark_ref = request.GET.get('bookmark')
92
92
93 try:
93 try:
94 source_repo_data = PullRequestModel().generate_repo_data(
94 source_repo_data = PullRequestModel().generate_repo_data(
95 source_repo, commit_id=commit_id,
95 source_repo, commit_id=commit_id,
96 branch=branch_ref, bookmark=bookmark_ref)
96 branch=branch_ref, bookmark=bookmark_ref)
97 except CommitDoesNotExistError as e:
97 except CommitDoesNotExistError as e:
98 log.exception(e)
98 log.exception(e)
99 h.flash(_('Commit does not exist'), 'error')
99 h.flash(_('Commit does not exist'), 'error')
100 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
100 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
101
101
102 default_target_repo = source_repo
102 default_target_repo = source_repo
103
103
104 if source_repo.parent:
104 if source_repo.parent:
105 parent_vcs_obj = source_repo.parent.scm_instance()
105 parent_vcs_obj = source_repo.parent.scm_instance()
106 if parent_vcs_obj and not parent_vcs_obj.is_empty():
106 if parent_vcs_obj and not parent_vcs_obj.is_empty():
107 # change default if we have a parent repo
107 # change default if we have a parent repo
108 default_target_repo = source_repo.parent
108 default_target_repo = source_repo.parent
109
109
110 target_repo_data = PullRequestModel().generate_repo_data(
110 target_repo_data = PullRequestModel().generate_repo_data(
111 default_target_repo)
111 default_target_repo)
112
112
113 selected_source_ref = source_repo_data['refs']['selected_ref']
113 selected_source_ref = source_repo_data['refs']['selected_ref']
114
114
115 title_source_ref = selected_source_ref.split(':', 2)[1]
115 title_source_ref = selected_source_ref.split(':', 2)[1]
116 c.default_title = PullRequestModel().generate_pullrequest_title(
116 c.default_title = PullRequestModel().generate_pullrequest_title(
117 source=source_repo.repo_name,
117 source=source_repo.repo_name,
118 source_ref=title_source_ref,
118 source_ref=title_source_ref,
119 target=default_target_repo.repo_name
119 target=default_target_repo.repo_name
120 )
120 )
121
121
122 c.default_repo_data = {
122 c.default_repo_data = {
123 'source_repo_name': source_repo.repo_name,
123 'source_repo_name': source_repo.repo_name,
124 'source_refs_json': json.dumps(source_repo_data),
124 'source_refs_json': json.dumps(source_repo_data),
125 'target_repo_name': default_target_repo.repo_name,
125 'target_repo_name': default_target_repo.repo_name,
126 'target_refs_json': json.dumps(target_repo_data),
126 'target_refs_json': json.dumps(target_repo_data),
127 }
127 }
128 c.default_source_ref = selected_source_ref
128 c.default_source_ref = selected_source_ref
129
129
130 return render('/pullrequests/pullrequest.mako')
130 return render('/pullrequests/pullrequest.mako')
131
131
132 @LoginRequired()
132 @LoginRequired()
133 @NotAnonymous()
133 @NotAnonymous()
134 @XHRRequired()
134 @XHRRequired()
135 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
135 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
136 'repository.admin')
136 'repository.admin')
137 @jsonify
137 @jsonify
138 def get_repo_refs(self, repo_name, target_repo_name):
138 def get_repo_refs(self, repo_name, target_repo_name):
139 repo = Repository.get_by_repo_name(target_repo_name)
139 repo = Repository.get_by_repo_name(target_repo_name)
140 if not repo:
140 if not repo:
141 raise HTTPNotFound
141 raise HTTPNotFound
142 return PullRequestModel().generate_repo_data(repo)
142 return PullRequestModel().generate_repo_data(repo)
143
143
144 @LoginRequired()
144 @LoginRequired()
145 @NotAnonymous()
145 @NotAnonymous()
146 @XHRRequired()
146 @XHRRequired()
147 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
147 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
148 'repository.admin')
148 'repository.admin')
149 @jsonify
149 @jsonify
150 def get_repo_destinations(self, repo_name):
150 def get_repo_destinations(self, repo_name):
151 repo = Repository.get_by_repo_name(repo_name)
151 repo = Repository.get_by_repo_name(repo_name)
152 if not repo:
152 if not repo:
153 raise HTTPNotFound
153 raise HTTPNotFound
154 filter_query = request.GET.get('query')
154 filter_query = request.GET.get('query')
155
155
156 query = Repository.query() \
156 query = Repository.query() \
157 .order_by(func.length(Repository.repo_name)) \
157 .order_by(func.length(Repository.repo_name)) \
158 .filter(or_(
158 .filter(or_(
159 Repository.repo_name == repo.repo_name,
159 Repository.repo_name == repo.repo_name,
160 Repository.fork_id == repo.repo_id))
160 Repository.fork_id == repo.repo_id))
161
161
162 if filter_query:
162 if filter_query:
163 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
163 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
164 query = query.filter(
164 query = query.filter(
165 Repository.repo_name.ilike(ilike_expression))
165 Repository.repo_name.ilike(ilike_expression))
166
166
167 add_parent = False
167 add_parent = False
168 if repo.parent:
168 if repo.parent:
169 if filter_query in repo.parent.repo_name:
169 if filter_query in repo.parent.repo_name:
170 parent_vcs_obj = repo.parent.scm_instance()
170 parent_vcs_obj = repo.parent.scm_instance()
171 if parent_vcs_obj and not parent_vcs_obj.is_empty():
171 if parent_vcs_obj and not parent_vcs_obj.is_empty():
172 add_parent = True
172 add_parent = True
173
173
174 limit = 20 - 1 if add_parent else 20
174 limit = 20 - 1 if add_parent else 20
175 all_repos = query.limit(limit).all()
175 all_repos = query.limit(limit).all()
176 if add_parent:
176 if add_parent:
177 all_repos += [repo.parent]
177 all_repos += [repo.parent]
178
178
179 repos = []
179 repos = []
180 for obj in self.scm_model.get_repos(all_repos):
180 for obj in self.scm_model.get_repos(all_repos):
181 repos.append({
181 repos.append({
182 'id': obj['name'],
182 'id': obj['name'],
183 'text': obj['name'],
183 'text': obj['name'],
184 'type': 'repo',
184 'type': 'repo',
185 'obj': obj['dbrepo']
185 'obj': obj['dbrepo']
186 })
186 })
187
187
188 data = {
188 data = {
189 'more': False,
189 'more': False,
190 'results': [{
190 'results': [{
191 'text': _('Repositories'),
191 'text': _('Repositories'),
192 'children': repos
192 'children': repos
193 }] if repos else []
193 }] if repos else []
194 }
194 }
195 return data
195 return data
196
196
197 @LoginRequired()
197 @LoginRequired()
198 @NotAnonymous()
198 @NotAnonymous()
199 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
199 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
200 'repository.admin')
200 'repository.admin')
201 @HasAcceptedRepoType('git', 'hg')
201 @HasAcceptedRepoType('git', 'hg')
202 @auth.CSRFRequired()
202 @auth.CSRFRequired()
203 def create(self, repo_name):
203 def create(self, repo_name):
204 repo = Repository.get_by_repo_name(repo_name)
204 repo = Repository.get_by_repo_name(repo_name)
205 if not repo:
205 if not repo:
206 raise HTTPNotFound
206 raise HTTPNotFound
207
207
208 controls = peppercorn.parse(request.POST.items())
208 controls = peppercorn.parse(request.POST.items())
209
209
210 try:
210 try:
211 _form = PullRequestForm(repo.repo_id)().to_python(controls)
211 _form = PullRequestForm(repo.repo_id)().to_python(controls)
212 except formencode.Invalid as errors:
212 except formencode.Invalid as errors:
213 if errors.error_dict.get('revisions'):
213 if errors.error_dict.get('revisions'):
214 msg = 'Revisions: %s' % errors.error_dict['revisions']
214 msg = 'Revisions: %s' % errors.error_dict['revisions']
215 elif errors.error_dict.get('pullrequest_title'):
215 elif errors.error_dict.get('pullrequest_title'):
216 msg = _('Pull request requires a title with min. 3 chars')
216 msg = _('Pull request requires a title with min. 3 chars')
217 else:
217 else:
218 msg = _('Error creating pull request: {}').format(errors)
218 msg = _('Error creating pull request: {}').format(errors)
219 log.exception(msg)
219 log.exception(msg)
220 h.flash(msg, 'error')
220 h.flash(msg, 'error')
221
221
222 # would rather just go back to form ...
222 # would rather just go back to form ...
223 return redirect(url('pullrequest_home', repo_name=repo_name))
223 return redirect(url('pullrequest_home', repo_name=repo_name))
224
224
225 source_repo = _form['source_repo']
225 source_repo = _form['source_repo']
226 source_ref = _form['source_ref']
226 source_ref = _form['source_ref']
227 target_repo = _form['target_repo']
227 target_repo = _form['target_repo']
228 target_ref = _form['target_ref']
228 target_ref = _form['target_ref']
229 commit_ids = _form['revisions'][::-1]
229 commit_ids = _form['revisions'][::-1]
230
230
231 # find the ancestor for this pr
231 # find the ancestor for this pr
232 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
232 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
233 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
233 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
234
234
235 source_scm = source_db_repo.scm_instance()
235 source_scm = source_db_repo.scm_instance()
236 target_scm = target_db_repo.scm_instance()
236 target_scm = target_db_repo.scm_instance()
237
237
238 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
238 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
239 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
239 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
240
240
241 ancestor = source_scm.get_common_ancestor(
241 ancestor = source_scm.get_common_ancestor(
242 source_commit.raw_id, target_commit.raw_id, target_scm)
242 source_commit.raw_id, target_commit.raw_id, target_scm)
243
243
244 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
244 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
245 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
245 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
246
246
247 pullrequest_title = _form['pullrequest_title']
247 pullrequest_title = _form['pullrequest_title']
248 title_source_ref = source_ref.split(':', 2)[1]
248 title_source_ref = source_ref.split(':', 2)[1]
249 if not pullrequest_title:
249 if not pullrequest_title:
250 pullrequest_title = PullRequestModel().generate_pullrequest_title(
250 pullrequest_title = PullRequestModel().generate_pullrequest_title(
251 source=source_repo,
251 source=source_repo,
252 source_ref=title_source_ref,
252 source_ref=title_source_ref,
253 target=target_repo
253 target=target_repo
254 )
254 )
255
255
256 description = _form['pullrequest_desc']
256 description = _form['pullrequest_desc']
257
257
258 get_default_reviewers_data, validate_default_reviewers = \
258 get_default_reviewers_data, validate_default_reviewers = \
259 PullRequestModel().get_reviewer_functions()
259 PullRequestModel().get_reviewer_functions()
260
260
261 # recalculate reviewers logic, to make sure we can validate this
261 # recalculate reviewers logic, to make sure we can validate this
262 reviewer_rules = get_default_reviewers_data(
262 reviewer_rules = get_default_reviewers_data(
263 c.rhodecode_user.get_instance(), source_db_repo,
263 c.rhodecode_user.get_instance(), source_db_repo,
264 source_commit, target_db_repo, target_commit)
264 source_commit, target_db_repo, target_commit)
265
265
266 given_reviewers = _form['review_members']
266 given_reviewers = _form['review_members']
267 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
267 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
268
268
269 try:
269 try:
270 pull_request = PullRequestModel().create(
270 pull_request = PullRequestModel().create(
271 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
271 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
272 target_ref, commit_ids, reviewers, pullrequest_title,
272 target_ref, commit_ids, reviewers, pullrequest_title,
273 description, reviewer_rules
273 description, reviewer_rules
274 )
274 )
275 Session().commit()
275 Session().commit()
276 h.flash(_('Successfully opened new pull request'),
276 h.flash(_('Successfully opened new pull request'),
277 category='success')
277 category='success')
278 except Exception as e:
278 except Exception as e:
279 msg = _('Error occurred during creation of this pull request.')
279 msg = _('Error occurred during creation of this pull request.')
280 log.exception(msg)
280 log.exception(msg)
281 h.flash(msg, category='error')
281 h.flash(msg, category='error')
282 return redirect(url('pullrequest_home', repo_name=repo_name))
282 return redirect(url('pullrequest_home', repo_name=repo_name))
283
283
284 raise HTTPFound(
284 raise HTTPFound(
285 h.route_path('pullrequest_show', repo_name=target_repo,
285 h.route_path('pullrequest_show', repo_name=target_repo,
286 pull_request_id=pull_request.pull_request_id))
286 pull_request_id=pull_request.pull_request_id))
287
287
288 @LoginRequired()
288 @LoginRequired()
289 @NotAnonymous()
289 @NotAnonymous()
290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
290 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
291 'repository.admin')
291 'repository.admin')
292 @auth.CSRFRequired()
292 @auth.CSRFRequired()
293 @jsonify
293 @jsonify
294 def update(self, repo_name, pull_request_id):
294 def update(self, repo_name, pull_request_id):
295 pull_request_id = safe_int(pull_request_id)
295 pull_request_id = safe_int(pull_request_id)
296 pull_request = PullRequest.get_or_404(pull_request_id)
296 pull_request = PullRequest.get_or_404(pull_request_id)
297 # only owner or admin can update it
297 # only owner or admin can update it
298 allowed_to_update = PullRequestModel().check_user_update(
298 allowed_to_update = PullRequestModel().check_user_update(
299 pull_request, c.rhodecode_user)
299 pull_request, c.rhodecode_user)
300 if allowed_to_update:
300 if allowed_to_update:
301 controls = peppercorn.parse(request.POST.items())
301 controls = peppercorn.parse(request.POST.items())
302
302
303 if 'review_members' in controls:
303 if 'review_members' in controls:
304 self._update_reviewers(
304 self._update_reviewers(
305 pull_request_id, controls['review_members'],
305 pull_request_id, controls['review_members'],
306 pull_request.reviewer_data)
306 pull_request.reviewer_data)
307 elif str2bool(request.POST.get('update_commits', 'false')):
307 elif str2bool(request.POST.get('update_commits', 'false')):
308 self._update_commits(pull_request)
308 self._update_commits(pull_request)
309 elif str2bool(request.POST.get('edit_pull_request', 'false')):
309 elif str2bool(request.POST.get('edit_pull_request', 'false')):
310 self._edit_pull_request(pull_request)
310 self._edit_pull_request(pull_request)
311 else:
311 else:
312 raise HTTPBadRequest()
312 raise HTTPBadRequest()
313 return True
313 return True
314 raise HTTPForbidden()
314 raise HTTPForbidden()
315
315
316 def _edit_pull_request(self, pull_request):
316 def _edit_pull_request(self, pull_request):
317 try:
317 try:
318 PullRequestModel().edit(
318 PullRequestModel().edit(
319 pull_request, request.POST.get('title'),
319 pull_request, request.POST.get('title'),
320 request.POST.get('description'), c.rhodecode_user)
320 request.POST.get('description'), c.rhodecode_user)
321 except ValueError:
321 except ValueError:
322 msg = _(u'Cannot update closed pull requests.')
322 msg = _(u'Cannot update closed pull requests.')
323 h.flash(msg, category='error')
323 h.flash(msg, category='error')
324 return
324 return
325 else:
325 else:
326 Session().commit()
326 Session().commit()
327
327
328 msg = _(u'Pull request title & description updated.')
328 msg = _(u'Pull request title & description updated.')
329 h.flash(msg, category='success')
329 h.flash(msg, category='success')
330 return
330 return
331
331
332 def _update_commits(self, pull_request):
332 def _update_commits(self, pull_request):
333 resp = PullRequestModel().update_commits(pull_request)
333 resp = PullRequestModel().update_commits(pull_request)
334
334
335 if resp.executed:
335 if resp.executed:
336
336
337 if resp.target_changed and resp.source_changed:
337 if resp.target_changed and resp.source_changed:
338 changed = 'target and source repositories'
338 changed = 'target and source repositories'
339 elif resp.target_changed and not resp.source_changed:
339 elif resp.target_changed and not resp.source_changed:
340 changed = 'target repository'
340 changed = 'target repository'
341 elif not resp.target_changed and resp.source_changed:
341 elif not resp.target_changed and resp.source_changed:
342 changed = 'source repository'
342 changed = 'source repository'
343 else:
343 else:
344 changed = 'nothing'
344 changed = 'nothing'
345
345
346 msg = _(
346 msg = _(
347 u'Pull request updated to "{source_commit_id}" with '
347 u'Pull request updated to "{source_commit_id}" with '
348 u'{count_added} added, {count_removed} removed commits. '
348 u'{count_added} added, {count_removed} removed commits. '
349 u'Source of changes: {change_source}')
349 u'Source of changes: {change_source}')
350 msg = msg.format(
350 msg = msg.format(
351 source_commit_id=pull_request.source_ref_parts.commit_id,
351 source_commit_id=pull_request.source_ref_parts.commit_id,
352 count_added=len(resp.changes.added),
352 count_added=len(resp.changes.added),
353 count_removed=len(resp.changes.removed),
353 count_removed=len(resp.changes.removed),
354 change_source=changed)
354 change_source=changed)
355 h.flash(msg, category='success')
355 h.flash(msg, category='success')
356
356
357 registry = get_current_registry()
357 registry = get_current_registry()
358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
359 channelstream_config = rhodecode_plugins.get('channelstream', {})
359 channelstream_config = rhodecode_plugins.get('channelstream', {})
360 if channelstream_config.get('enabled'):
360 if channelstream_config.get('enabled'):
361 message = msg + (
361 message = msg + (
362 ' - <a onclick="window.location.reload()">'
362 ' - <a onclick="window.location.reload()">'
363 '<strong>{}</strong></a>'.format(_('Reload page')))
363 '<strong>{}</strong></a>'.format(_('Reload page')))
364 channel = '/repo${}$/pr/{}'.format(
364 channel = '/repo${}$/pr/{}'.format(
365 pull_request.target_repo.repo_name,
365 pull_request.target_repo.repo_name,
366 pull_request.pull_request_id
366 pull_request.pull_request_id
367 )
367 )
368 payload = {
368 payload = {
369 'type': 'message',
369 'type': 'message',
370 'user': 'system',
370 'user': 'system',
371 'exclude_users': [request.user.username],
371 'exclude_users': [request.user.username],
372 'channel': channel,
372 'channel': channel,
373 'message': {
373 'message': {
374 'message': message,
374 'message': message,
375 'level': 'success',
375 'level': 'success',
376 'topic': '/notifications'
376 'topic': '/notifications'
377 }
377 }
378 }
378 }
379 channelstream_request(
379 channelstream_request(
380 channelstream_config, [payload], '/message',
380 channelstream_config, [payload], '/message',
381 raise_exc=False)
381 raise_exc=False)
382 else:
382 else:
383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
384 warning_reasons = [
384 warning_reasons = [
385 UpdateFailureReason.NO_CHANGE,
385 UpdateFailureReason.NO_CHANGE,
386 UpdateFailureReason.WRONG_REF_TYPE,
386 UpdateFailureReason.WRONG_REF_TYPE,
387 ]
387 ]
388 category = 'warning' if resp.reason in warning_reasons else 'error'
388 category = 'warning' if resp.reason in warning_reasons else 'error'
389 h.flash(msg, category=category)
389 h.flash(msg, category=category)
390
390
391 @auth.CSRFRequired()
391 @auth.CSRFRequired()
392 @LoginRequired()
392 @LoginRequired()
393 @NotAnonymous()
393 @NotAnonymous()
394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
395 'repository.admin')
395 'repository.admin')
396 def merge(self, repo_name, pull_request_id):
396 def merge(self, repo_name, pull_request_id):
397 """
397 """
398 POST /{repo_name}/pull-request/{pull_request_id}
398 POST /{repo_name}/pull-request/{pull_request_id}
399
399
400 Merge will perform a server-side merge of the specified
400 Merge will perform a server-side merge of the specified
401 pull request, if the pull request is approved and mergeable.
401 pull request, if the pull request is approved and mergeable.
402 After successful merging, the pull request is automatically
402 After successful merging, the pull request is automatically
403 closed, with a relevant comment.
403 closed, with a relevant comment.
404 """
404 """
405 pull_request_id = safe_int(pull_request_id)
405 pull_request_id = safe_int(pull_request_id)
406 pull_request = PullRequest.get_or_404(pull_request_id)
406 pull_request = PullRequest.get_or_404(pull_request_id)
407 user = c.rhodecode_user
407 user = c.rhodecode_user
408
408
409 check = MergeCheck.validate(pull_request, user)
409 check = MergeCheck.validate(pull_request, user)
410 merge_possible = not check.failed
410 merge_possible = not check.failed
411
411
412 for err_type, error_msg in check.errors:
412 for err_type, error_msg in check.errors:
413 h.flash(error_msg, category=err_type)
413 h.flash(error_msg, category=err_type)
414
414
415 if merge_possible:
415 if merge_possible:
416 log.debug("Pre-conditions checked, trying to merge.")
416 log.debug("Pre-conditions checked, trying to merge.")
417 extras = vcs_operation_context(
417 extras = vcs_operation_context(
418 request.environ, repo_name=pull_request.target_repo.repo_name,
418 request.environ, repo_name=pull_request.target_repo.repo_name,
419 username=user.username, action='push',
419 username=user.username, action='push',
420 scm=pull_request.target_repo.repo_type)
420 scm=pull_request.target_repo.repo_type)
421 self._merge_pull_request(pull_request, user, extras)
421 self._merge_pull_request(pull_request, user, extras)
422
422
423 raise HTTPFound(
423 raise HTTPFound(
424 h.route_path('pullrequest_show',
424 h.route_path('pullrequest_show',
425 repo_name=pull_request.target_repo.repo_name,
425 repo_name=pull_request.target_repo.repo_name,
426 pull_request_id=pull_request.pull_request_id))
426 pull_request_id=pull_request.pull_request_id))
427
427
428 def _merge_pull_request(self, pull_request, user, extras):
428 def _merge_pull_request(self, pull_request, user, extras):
429 merge_resp = PullRequestModel().merge(
429 merge_resp = PullRequestModel().merge(
430 pull_request, user, extras=extras)
430 pull_request, user, extras=extras)
431
431
432 if merge_resp.executed:
432 if merge_resp.executed:
433 log.debug("The merge was successful, closing the pull request.")
433 log.debug("The merge was successful, closing the pull request.")
434 PullRequestModel().close_pull_request(
434 PullRequestModel().close_pull_request(
435 pull_request.pull_request_id, user)
435 pull_request.pull_request_id, user)
436 Session().commit()
436 Session().commit()
437 msg = _('Pull request was successfully merged and closed.')
437 msg = _('Pull request was successfully merged and closed.')
438 h.flash(msg, category='success')
438 h.flash(msg, category='success')
439 else:
439 else:
440 log.debug(
440 log.debug(
441 "The merge was not successful. Merge response: %s",
441 "The merge was not successful. Merge response: %s",
442 merge_resp)
442 merge_resp)
443 msg = PullRequestModel().merge_status_message(
443 msg = PullRequestModel().merge_status_message(
444 merge_resp.failure_reason)
444 merge_resp.failure_reason)
445 h.flash(msg, category='error')
445 h.flash(msg, category='error')
446
446
447 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
447 def _update_reviewers(self, pull_request_id, review_members, reviewer_rules):
448
448
449 get_default_reviewers_data, validate_default_reviewers = \
449 get_default_reviewers_data, validate_default_reviewers = \
450 PullRequestModel().get_reviewer_functions()
450 PullRequestModel().get_reviewer_functions()
451
451
452 try:
452 try:
453 reviewers = validate_default_reviewers(review_members, reviewer_rules)
453 reviewers = validate_default_reviewers(review_members, reviewer_rules)
454 except ValueError as e:
454 except ValueError as e:
455 log.error('Reviewers Validation: {}'.format(e))
455 log.error('Reviewers Validation: {}'.format(e))
456 h.flash(e, category='error')
456 h.flash(e, category='error')
457 return
457 return
458
458
459 PullRequestModel().update_reviewers(
459 PullRequestModel().update_reviewers(
460 pull_request_id, reviewers, c.rhodecode_user)
460 pull_request_id, reviewers, c.rhodecode_user)
461 h.flash(_('Pull request reviewers updated.'), category='success')
461 h.flash(_('Pull request reviewers updated.'), category='success')
462 Session().commit()
462 Session().commit()
463
463
464 @LoginRequired()
464 @LoginRequired()
465 @NotAnonymous()
465 @NotAnonymous()
466 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
466 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
467 'repository.admin')
467 'repository.admin')
468 @auth.CSRFRequired()
468 @auth.CSRFRequired()
469 @jsonify
469 @jsonify
470 def delete(self, repo_name, pull_request_id):
470 def delete(self, repo_name, pull_request_id):
471 pull_request_id = safe_int(pull_request_id)
471 pull_request_id = safe_int(pull_request_id)
472 pull_request = PullRequest.get_or_404(pull_request_id)
472 pull_request = PullRequest.get_or_404(pull_request_id)
473
473
474 pr_closed = pull_request.is_closed()
474 pr_closed = pull_request.is_closed()
475 allowed_to_delete = PullRequestModel().check_user_delete(
475 allowed_to_delete = PullRequestModel().check_user_delete(
476 pull_request, c.rhodecode_user) and not pr_closed
476 pull_request, c.rhodecode_user) and not pr_closed
477
477
478 # only owner can delete it !
478 # only owner can delete it !
479 if allowed_to_delete:
479 if allowed_to_delete:
480 PullRequestModel().delete(pull_request, c.rhodecode_user)
480 PullRequestModel().delete(pull_request, c.rhodecode_user)
481 Session().commit()
481 Session().commit()
482 h.flash(_('Successfully deleted pull request'),
482 h.flash(_('Successfully deleted pull request'),
483 category='success')
483 category='success')
484 return redirect(url('my_account_pullrequests'))
484 return redirect(url('my_account_pullrequests'))
485
485
486 h.flash(_('Your are not allowed to delete this pull request'),
486 h.flash(_('Your are not allowed to delete this pull request'),
487 category='error')
487 category='error')
488 raise HTTPForbidden()
488 raise HTTPForbidden()
489
489
490 def _get_pr_version(self, pull_request_id, version=None):
490 def _get_pr_version(self, pull_request_id, version=None):
491 pull_request_id = safe_int(pull_request_id)
491 pull_request_id = safe_int(pull_request_id)
492 at_version = None
492 at_version = None
493
493
494 if version and version == 'latest':
494 if version and version == 'latest':
495 pull_request_ver = PullRequest.get(pull_request_id)
495 pull_request_ver = PullRequest.get(pull_request_id)
496 pull_request_obj = pull_request_ver
496 pull_request_obj = pull_request_ver
497 _org_pull_request_obj = pull_request_obj
497 _org_pull_request_obj = pull_request_obj
498 at_version = 'latest'
498 at_version = 'latest'
499 elif version:
499 elif version:
500 pull_request_ver = PullRequestVersion.get_or_404(version)
500 pull_request_ver = PullRequestVersion.get_or_404(version)
501 pull_request_obj = pull_request_ver
501 pull_request_obj = pull_request_ver
502 _org_pull_request_obj = pull_request_ver.pull_request
502 _org_pull_request_obj = pull_request_ver.pull_request
503 at_version = pull_request_ver.pull_request_version_id
503 at_version = pull_request_ver.pull_request_version_id
504 else:
504 else:
505 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
505 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
506 pull_request_id)
506 pull_request_id)
507
507
508 pull_request_display_obj = PullRequest.get_pr_display_object(
508 pull_request_display_obj = PullRequest.get_pr_display_object(
509 pull_request_obj, _org_pull_request_obj)
509 pull_request_obj, _org_pull_request_obj)
510
510
511 return _org_pull_request_obj, pull_request_obj, \
511 return _org_pull_request_obj, pull_request_obj, \
512 pull_request_display_obj, at_version
512 pull_request_display_obj, at_version
513
513
514 def _get_diffset(
514 def _get_diffset(
515 self, source_repo, source_ref_id, target_ref_id, target_commit,
515 self, source_repo, source_ref_id, target_ref_id, target_commit,
516 source_commit, diff_limit, file_limit, display_inline_comments):
516 source_commit, diff_limit, file_limit, display_inline_comments):
517 vcs_diff = PullRequestModel().get_diff(
517 vcs_diff = PullRequestModel().get_diff(
518 source_repo, source_ref_id, target_ref_id)
518 source_repo, source_ref_id, target_ref_id)
519
519
520 diff_processor = diffs.DiffProcessor(
520 diff_processor = diffs.DiffProcessor(
521 vcs_diff, format='newdiff', diff_limit=diff_limit,
521 vcs_diff, format='newdiff', diff_limit=diff_limit,
522 file_limit=file_limit, show_full_diff=c.fulldiff)
522 file_limit=file_limit, show_full_diff=c.fulldiff)
523
523
524 _parsed = diff_processor.prepare()
524 _parsed = diff_processor.prepare()
525
525
526 def _node_getter(commit):
526 def _node_getter(commit):
527 def get_node(fname):
527 def get_node(fname):
528 try:
528 try:
529 return commit.get_node(fname)
529 return commit.get_node(fname)
530 except NodeDoesNotExistError:
530 except NodeDoesNotExistError:
531 return None
531 return None
532
532
533 return get_node
533 return get_node
534
534
535 diffset = codeblocks.DiffSet(
535 diffset = codeblocks.DiffSet(
536 repo_name=c.repo_name,
536 repo_name=c.repo_name,
537 source_repo_name=c.source_repo.repo_name,
537 source_repo_name=c.source_repo.repo_name,
538 source_node_getter=_node_getter(target_commit),
538 source_node_getter=_node_getter(target_commit),
539 target_node_getter=_node_getter(source_commit),
539 target_node_getter=_node_getter(source_commit),
540 comments=display_inline_comments
540 comments=display_inline_comments
541 )
541 )
542 diffset = diffset.render_patchset(
542 diffset = diffset.render_patchset(
543 _parsed, target_commit.raw_id, source_commit.raw_id)
543 _parsed, target_commit.raw_id, source_commit.raw_id)
544
544
545 return diffset
545 return diffset
546
546
547 @LoginRequired()
547 @LoginRequired()
548 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
548 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
549 'repository.admin')
549 'repository.admin')
550 def show(self, repo_name, pull_request_id):
550 def show(self, repo_name, pull_request_id):
551 pull_request_id = safe_int(pull_request_id)
551 pull_request_id = safe_int(pull_request_id)
552 version = request.GET.get('version')
552 version = request.GET.get('version')
553 from_version = request.GET.get('from_version') or version
553 from_version = request.GET.get('from_version') or version
554 merge_checks = request.GET.get('merge_checks')
554 merge_checks = request.GET.get('merge_checks')
555 c.fulldiff = str2bool(request.GET.get('fulldiff'))
555 c.fulldiff = str2bool(request.GET.get('fulldiff'))
556
556
557 (pull_request_latest,
557 (pull_request_latest,
558 pull_request_at_ver,
558 pull_request_at_ver,
559 pull_request_display_obj,
559 pull_request_display_obj,
560 at_version) = self._get_pr_version(
560 at_version) = self._get_pr_version(
561 pull_request_id, version=version)
561 pull_request_id, version=version)
562 pr_closed = pull_request_latest.is_closed()
562 pr_closed = pull_request_latest.is_closed()
563
563
564 if pr_closed and (version or from_version):
564 if pr_closed and (version or from_version):
565 # not allow to browse versions
565 # not allow to browse versions
566 return redirect(h.url('pullrequest_show', repo_name=repo_name,
566 return redirect(h.url('pullrequest_show', repo_name=repo_name,
567 pull_request_id=pull_request_id))
567 pull_request_id=pull_request_id))
568
568
569 versions = pull_request_display_obj.versions()
569 versions = pull_request_display_obj.versions()
570
570
571 c.at_version = at_version
571 c.at_version = at_version
572 c.at_version_num = (at_version
572 c.at_version_num = (at_version
573 if at_version and at_version != 'latest'
573 if at_version and at_version != 'latest'
574 else None)
574 else None)
575 c.at_version_pos = ChangesetComment.get_index_from_version(
575 c.at_version_pos = ChangesetComment.get_index_from_version(
576 c.at_version_num, versions)
576 c.at_version_num, versions)
577
577
578 (prev_pull_request_latest,
578 (prev_pull_request_latest,
579 prev_pull_request_at_ver,
579 prev_pull_request_at_ver,
580 prev_pull_request_display_obj,
580 prev_pull_request_display_obj,
581 prev_at_version) = self._get_pr_version(
581 prev_at_version) = self._get_pr_version(
582 pull_request_id, version=from_version)
582 pull_request_id, version=from_version)
583
583
584 c.from_version = prev_at_version
584 c.from_version = prev_at_version
585 c.from_version_num = (prev_at_version
585 c.from_version_num = (prev_at_version
586 if prev_at_version and prev_at_version != 'latest'
586 if prev_at_version and prev_at_version != 'latest'
587 else None)
587 else None)
588 c.from_version_pos = ChangesetComment.get_index_from_version(
588 c.from_version_pos = ChangesetComment.get_index_from_version(
589 c.from_version_num, versions)
589 c.from_version_num, versions)
590
590
591 # define if we're in COMPARE mode or VIEW at version mode
591 # define if we're in COMPARE mode or VIEW at version mode
592 compare = at_version != prev_at_version
592 compare = at_version != prev_at_version
593
593
594 # pull_requests repo_name we opened it against
594 # pull_requests repo_name we opened it against
595 # ie. target_repo must match
595 # ie. target_repo must match
596 if repo_name != pull_request_at_ver.target_repo.repo_name:
596 if repo_name != pull_request_at_ver.target_repo.repo_name:
597 raise HTTPNotFound
597 raise HTTPNotFound
598
598
599 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
599 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
600 pull_request_at_ver)
600 pull_request_at_ver)
601
601
602 c.pull_request = pull_request_display_obj
602 c.pull_request = pull_request_display_obj
603 c.pull_request_latest = pull_request_latest
603 c.pull_request_latest = pull_request_latest
604
604
605 if compare or (at_version and not at_version == 'latest'):
605 if compare or (at_version and not at_version == 'latest'):
606 c.allowed_to_change_status = False
606 c.allowed_to_change_status = False
607 c.allowed_to_update = False
607 c.allowed_to_update = False
608 c.allowed_to_merge = False
608 c.allowed_to_merge = False
609 c.allowed_to_delete = False
609 c.allowed_to_delete = False
610 c.allowed_to_comment = False
610 c.allowed_to_comment = False
611 c.allowed_to_close = False
611 c.allowed_to_close = False
612 else:
612 else:
613 can_change_status = PullRequestModel().check_user_change_status(
613 can_change_status = PullRequestModel().check_user_change_status(
614 pull_request_at_ver, c.rhodecode_user)
614 pull_request_at_ver, c.rhodecode_user)
615 c.allowed_to_change_status = can_change_status and not pr_closed
615 c.allowed_to_change_status = can_change_status and not pr_closed
616
616
617 c.allowed_to_update = PullRequestModel().check_user_update(
617 c.allowed_to_update = PullRequestModel().check_user_update(
618 pull_request_latest, c.rhodecode_user) and not pr_closed
618 pull_request_latest, c.rhodecode_user) and not pr_closed
619 c.allowed_to_merge = PullRequestModel().check_user_merge(
619 c.allowed_to_merge = PullRequestModel().check_user_merge(
620 pull_request_latest, c.rhodecode_user) and not pr_closed
620 pull_request_latest, c.rhodecode_user) and not pr_closed
621 c.allowed_to_delete = PullRequestModel().check_user_delete(
621 c.allowed_to_delete = PullRequestModel().check_user_delete(
622 pull_request_latest, c.rhodecode_user) and not pr_closed
622 pull_request_latest, c.rhodecode_user) and not pr_closed
623 c.allowed_to_comment = not pr_closed
623 c.allowed_to_comment = not pr_closed
624 c.allowed_to_close = c.allowed_to_merge and not pr_closed
624 c.allowed_to_close = c.allowed_to_merge and not pr_closed
625
625
626 c.forbid_adding_reviewers = False
626 c.forbid_adding_reviewers = False
627 c.forbid_author_to_review = False
627 c.forbid_author_to_review = False
628 c.forbid_commit_author_to_review = False
628 c.forbid_commit_author_to_review = False
629
629
630 if pull_request_latest.reviewer_data and \
630 if pull_request_latest.reviewer_data and \
631 'rules' in pull_request_latest.reviewer_data:
631 'rules' in pull_request_latest.reviewer_data:
632 rules = pull_request_latest.reviewer_data['rules'] or {}
632 rules = pull_request_latest.reviewer_data['rules'] or {}
633 try:
633 try:
634 c.forbid_adding_reviewers = rules.get(
634 c.forbid_adding_reviewers = rules.get(
635 'forbid_adding_reviewers')
635 'forbid_adding_reviewers')
636 c.forbid_author_to_review = rules.get(
636 c.forbid_author_to_review = rules.get(
637 'forbid_author_to_review')
637 'forbid_author_to_review')
638 c.forbid_commit_author_to_review = rules.get(
638 c.forbid_commit_author_to_review = rules.get(
639 'forbid_commit_author_to_review')
639 'forbid_commit_author_to_review')
640 except Exception:
640 except Exception:
641 pass
641 pass
642
642
643 # check merge capabilities
643 # check merge capabilities
644 _merge_check = MergeCheck.validate(
644 _merge_check = MergeCheck.validate(
645 pull_request_latest, user=c.rhodecode_user)
645 pull_request_latest, user=c.rhodecode_user)
646 c.pr_merge_errors = _merge_check.error_details
646 c.pr_merge_errors = _merge_check.error_details
647 c.pr_merge_possible = not _merge_check.failed
647 c.pr_merge_possible = not _merge_check.failed
648 c.pr_merge_message = _merge_check.merge_msg
648 c.pr_merge_message = _merge_check.merge_msg
649
649
650 c.pull_request_review_status = _merge_check.review_status
650 c.pull_request_review_status = _merge_check.review_status
651 if merge_checks:
651 if merge_checks:
652 return render('/pullrequests/pullrequest_merge_checks.mako')
652 return render('/pullrequests/pullrequest_merge_checks.mako')
653
653
654 comments_model = CommentsModel()
654 comments_model = CommentsModel()
655
655
656 # reviewers and statuses
656 # reviewers and statuses
657 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
657 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
658 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
658 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
659
659
660 # GENERAL COMMENTS with versions #
660 # GENERAL COMMENTS with versions #
661 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
661 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
662 q = q.order_by(ChangesetComment.comment_id.asc())
662 q = q.order_by(ChangesetComment.comment_id.asc())
663 general_comments = q
663 general_comments = q
664
664
665 # pick comments we want to render at current version
665 # pick comments we want to render at current version
666 c.comment_versions = comments_model.aggregate_comments(
666 c.comment_versions = comments_model.aggregate_comments(
667 general_comments, versions, c.at_version_num)
667 general_comments, versions, c.at_version_num)
668 c.comments = c.comment_versions[c.at_version_num]['until']
668 c.comments = c.comment_versions[c.at_version_num]['until']
669
669
670 # INLINE COMMENTS with versions #
670 # INLINE COMMENTS with versions #
671 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
671 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
672 q = q.order_by(ChangesetComment.comment_id.asc())
672 q = q.order_by(ChangesetComment.comment_id.asc())
673 inline_comments = q
673 inline_comments = q
674
674
675 c.inline_versions = comments_model.aggregate_comments(
675 c.inline_versions = comments_model.aggregate_comments(
676 inline_comments, versions, c.at_version_num, inline=True)
676 inline_comments, versions, c.at_version_num, inline=True)
677
677
678 # inject latest version
678 # inject latest version
679 latest_ver = PullRequest.get_pr_display_object(
679 latest_ver = PullRequest.get_pr_display_object(
680 pull_request_latest, pull_request_latest)
680 pull_request_latest, pull_request_latest)
681
681
682 c.versions = versions + [latest_ver]
682 c.versions = versions + [latest_ver]
683
683
684 # if we use version, then do not show later comments
684 # if we use version, then do not show later comments
685 # than current version
685 # than current version
686 display_inline_comments = collections.defaultdict(
686 display_inline_comments = collections.defaultdict(
687 lambda: collections.defaultdict(list))
687 lambda: collections.defaultdict(list))
688 for co in inline_comments:
688 for co in inline_comments:
689 if c.at_version_num:
689 if c.at_version_num:
690 # pick comments that are at least UPTO given version, so we
690 # pick comments that are at least UPTO given version, so we
691 # don't render comments for higher version
691 # don't render comments for higher version
692 should_render = co.pull_request_version_id and \
692 should_render = co.pull_request_version_id and \
693 co.pull_request_version_id <= c.at_version_num
693 co.pull_request_version_id <= c.at_version_num
694 else:
694 else:
695 # showing all, for 'latest'
695 # showing all, for 'latest'
696 should_render = True
696 should_render = True
697
697
698 if should_render:
698 if should_render:
699 display_inline_comments[co.f_path][co.line_no].append(co)
699 display_inline_comments[co.f_path][co.line_no].append(co)
700
700
701 # load diff data into template context, if we use compare mode then
701 # load diff data into template context, if we use compare mode then
702 # diff is calculated based on changes between versions of PR
702 # diff is calculated based on changes between versions of PR
703
703
704 source_repo = pull_request_at_ver.source_repo
704 source_repo = pull_request_at_ver.source_repo
705 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
705 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
706
706
707 target_repo = pull_request_at_ver.target_repo
707 target_repo = pull_request_at_ver.target_repo
708 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
708 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
709
709
710 if compare:
710 if compare:
711 # in compare switch the diff base to latest commit from prev version
711 # in compare switch the diff base to latest commit from prev version
712 target_ref_id = prev_pull_request_display_obj.revisions[0]
712 target_ref_id = prev_pull_request_display_obj.revisions[0]
713
713
714 # despite opening commits for bookmarks/branches/tags, we always
714 # despite opening commits for bookmarks/branches/tags, we always
715 # convert this to rev to prevent changes after bookmark or branch change
715 # convert this to rev to prevent changes after bookmark or branch change
716 c.source_ref_type = 'rev'
716 c.source_ref_type = 'rev'
717 c.source_ref = source_ref_id
717 c.source_ref = source_ref_id
718
718
719 c.target_ref_type = 'rev'
719 c.target_ref_type = 'rev'
720 c.target_ref = target_ref_id
720 c.target_ref = target_ref_id
721
721
722 c.source_repo = source_repo
722 c.source_repo = source_repo
723 c.target_repo = target_repo
723 c.target_repo = target_repo
724
724
725 # diff_limit is the old behavior, will cut off the whole diff
725 # diff_limit is the old behavior, will cut off the whole diff
726 # if the limit is applied otherwise will just hide the
726 # if the limit is applied otherwise will just hide the
727 # big files from the front-end
727 # big files from the front-end
728 diff_limit = self.cut_off_limit_diff
728 diff_limit = self.cut_off_limit_diff
729 file_limit = self.cut_off_limit_file
729 file_limit = self.cut_off_limit_file
730
730
731 c.commit_ranges = []
731 c.commit_ranges = []
732 source_commit = EmptyCommit()
732 source_commit = EmptyCommit()
733 target_commit = EmptyCommit()
733 target_commit = EmptyCommit()
734 c.missing_requirements = False
734 c.missing_requirements = False
735
735
736 source_scm = source_repo.scm_instance()
736 source_scm = source_repo.scm_instance()
737 target_scm = target_repo.scm_instance()
737 target_scm = target_repo.scm_instance()
738
738
739 # try first shadow repo, fallback to regular repo
739 # try first shadow repo, fallback to regular repo
740 try:
740 try:
741 commits_source_repo = pull_request_latest.get_shadow_repo()
741 commits_source_repo = pull_request_latest.get_shadow_repo()
742 except Exception:
742 except Exception:
743 log.debug('Failed to get shadow repo', exc_info=True)
743 log.debug('Failed to get shadow repo', exc_info=True)
744 commits_source_repo = source_scm
744 commits_source_repo = source_scm
745
745
746 c.commits_source_repo = commits_source_repo
746 c.commits_source_repo = commits_source_repo
747 commit_cache = {}
747 commit_cache = {}
748 try:
748 try:
749 pre_load = ["author", "branch", "date", "message"]
749 pre_load = ["author", "branch", "date", "message"]
750 show_revs = pull_request_at_ver.revisions
750 show_revs = pull_request_at_ver.revisions
751 for rev in show_revs:
751 for rev in show_revs:
752 comm = commits_source_repo.get_commit(
752 comm = commits_source_repo.get_commit(
753 commit_id=rev, pre_load=pre_load)
753 commit_id=rev, pre_load=pre_load)
754 c.commit_ranges.append(comm)
754 c.commit_ranges.append(comm)
755 commit_cache[comm.raw_id] = comm
755 commit_cache[comm.raw_id] = comm
756
756
757 # Order here matters, we first need to get target, and then
757 # Order here matters, we first need to get target, and then
758 # the source
758 # the source
759 target_commit = commits_source_repo.get_commit(
759 target_commit = commits_source_repo.get_commit(
760 commit_id=safe_str(target_ref_id))
760 commit_id=safe_str(target_ref_id))
761
761
762 source_commit = commits_source_repo.get_commit(
762 source_commit = commits_source_repo.get_commit(
763 commit_id=safe_str(source_ref_id))
763 commit_id=safe_str(source_ref_id))
764
764
765 except CommitDoesNotExistError:
765 except CommitDoesNotExistError:
766 log.warning(
766 log.warning(
767 'Failed to get commit from `{}` repo'.format(
767 'Failed to get commit from `{}` repo'.format(
768 commits_source_repo), exc_info=True)
768 commits_source_repo), exc_info=True)
769 except RepositoryRequirementError:
769 except RepositoryRequirementError:
770 log.warning(
770 log.warning(
771 'Failed to get all required data from repo', exc_info=True)
771 'Failed to get all required data from repo', exc_info=True)
772 c.missing_requirements = True
772 c.missing_requirements = True
773
773
774 c.ancestor = None # set it to None, to hide it from PR view
774 c.ancestor = None # set it to None, to hide it from PR view
775
775
776 try:
776 try:
777 ancestor_id = source_scm.get_common_ancestor(
777 ancestor_id = source_scm.get_common_ancestor(
778 source_commit.raw_id, target_commit.raw_id, target_scm)
778 source_commit.raw_id, target_commit.raw_id, target_scm)
779 c.ancestor_commit = source_scm.get_commit(ancestor_id)
779 c.ancestor_commit = source_scm.get_commit(ancestor_id)
780 except Exception:
780 except Exception:
781 c.ancestor_commit = None
781 c.ancestor_commit = None
782
782
783 c.statuses = source_repo.statuses(
783 c.statuses = source_repo.statuses(
784 [x.raw_id for x in c.commit_ranges])
784 [x.raw_id for x in c.commit_ranges])
785
785
786 # auto collapse if we have more than limit
786 # auto collapse if we have more than limit
787 collapse_limit = diffs.DiffProcessor._collapse_commits_over
787 collapse_limit = diffs.DiffProcessor._collapse_commits_over
788 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
788 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
789 c.compare_mode = compare
789 c.compare_mode = compare
790
790
791 c.missing_commits = False
791 c.missing_commits = False
792 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
792 if (c.missing_requirements or isinstance(source_commit, EmptyCommit)
793 or source_commit == target_commit):
793 or source_commit == target_commit):
794
794
795 c.missing_commits = True
795 c.missing_commits = True
796 else:
796 else:
797
797
798 c.diffset = self._get_diffset(
798 c.diffset = self._get_diffset(
799 commits_source_repo, source_ref_id, target_ref_id,
799 commits_source_repo, source_ref_id, target_ref_id,
800 target_commit, source_commit,
800 target_commit, source_commit,
801 diff_limit, file_limit, display_inline_comments)
801 diff_limit, file_limit, display_inline_comments)
802
802
803 c.limited_diff = c.diffset.limited_diff
803 c.limited_diff = c.diffset.limited_diff
804
804
805 # calculate removed files that are bound to comments
805 # calculate removed files that are bound to comments
806 comment_deleted_files = [
806 comment_deleted_files = [
807 fname for fname in display_inline_comments
807 fname for fname in display_inline_comments
808 if fname not in c.diffset.file_stats]
808 if fname not in c.diffset.file_stats]
809
809
810 c.deleted_files_comments = collections.defaultdict(dict)
810 c.deleted_files_comments = collections.defaultdict(dict)
811 for fname, per_line_comments in display_inline_comments.items():
811 for fname, per_line_comments in display_inline_comments.items():
812 if fname in comment_deleted_files:
812 if fname in comment_deleted_files:
813 c.deleted_files_comments[fname]['stats'] = 0
813 c.deleted_files_comments[fname]['stats'] = 0
814 c.deleted_files_comments[fname]['comments'] = list()
814 c.deleted_files_comments[fname]['comments'] = list()
815 for lno, comments in per_line_comments.items():
815 for lno, comments in per_line_comments.items():
816 c.deleted_files_comments[fname]['comments'].extend(
816 c.deleted_files_comments[fname]['comments'].extend(
817 comments)
817 comments)
818
818
819 # this is a hack to properly display links, when creating PR, the
819 # this is a hack to properly display links, when creating PR, the
820 # compare view and others uses different notation, and
820 # compare view and others uses different notation, and
821 # compare_commits.mako renders links based on the target_repo.
821 # compare_commits.mako renders links based on the target_repo.
822 # We need to swap that here to generate it properly on the html side
822 # We need to swap that here to generate it properly on the html side
823 c.target_repo = c.source_repo
823 c.target_repo = c.source_repo
824
824
825 c.commit_statuses = ChangesetStatus.STATUSES
825 c.commit_statuses = ChangesetStatus.STATUSES
826
826
827 c.show_version_changes = not pr_closed
827 c.show_version_changes = not pr_closed
828 if c.show_version_changes:
828 if c.show_version_changes:
829 cur_obj = pull_request_at_ver
829 cur_obj = pull_request_at_ver
830 prev_obj = prev_pull_request_at_ver
830 prev_obj = prev_pull_request_at_ver
831
831
832 old_commit_ids = prev_obj.revisions
832 old_commit_ids = prev_obj.revisions
833 new_commit_ids = cur_obj.revisions
833 new_commit_ids = cur_obj.revisions
834 commit_changes = PullRequestModel()._calculate_commit_id_changes(
834 commit_changes = PullRequestModel()._calculate_commit_id_changes(
835 old_commit_ids, new_commit_ids)
835 old_commit_ids, new_commit_ids)
836 c.commit_changes_summary = commit_changes
836 c.commit_changes_summary = commit_changes
837
837
838 # calculate the diff for commits between versions
838 # calculate the diff for commits between versions
839 c.commit_changes = []
839 c.commit_changes = []
840 mark = lambda cs, fw: list(
840 mark = lambda cs, fw: list(
841 h.itertools.izip_longest([], cs, fillvalue=fw))
841 h.itertools.izip_longest([], cs, fillvalue=fw))
842 for c_type, raw_id in mark(commit_changes.added, 'a') \
842 for c_type, raw_id in mark(commit_changes.added, 'a') \
843 + mark(commit_changes.removed, 'r') \
843 + mark(commit_changes.removed, 'r') \
844 + mark(commit_changes.common, 'c'):
844 + mark(commit_changes.common, 'c'):
845
845
846 if raw_id in commit_cache:
846 if raw_id in commit_cache:
847 commit = commit_cache[raw_id]
847 commit = commit_cache[raw_id]
848 else:
848 else:
849 try:
849 try:
850 commit = commits_source_repo.get_commit(raw_id)
850 commit = commits_source_repo.get_commit(raw_id)
851 except CommitDoesNotExistError:
851 except CommitDoesNotExistError:
852 # in case we fail extracting still use "dummy" commit
852 # in case we fail extracting still use "dummy" commit
853 # for display in commit diff
853 # for display in commit diff
854 commit = h.AttributeDict(
854 commit = h.AttributeDict(
855 {'raw_id': raw_id,
855 {'raw_id': raw_id,
856 'message': 'EMPTY or MISSING COMMIT'})
856 'message': 'EMPTY or MISSING COMMIT'})
857 c.commit_changes.append([c_type, commit])
857 c.commit_changes.append([c_type, commit])
858
858
859 # current user review statuses for each version
859 # current user review statuses for each version
860 c.review_versions = {}
860 c.review_versions = {}
861 if c.rhodecode_user.user_id in allowed_reviewers:
861 if c.rhodecode_user.user_id in allowed_reviewers:
862 for co in general_comments:
862 for co in general_comments:
863 if co.author.user_id == c.rhodecode_user.user_id:
863 if co.author.user_id == c.rhodecode_user.user_id:
864 # each comment has a status change
864 # each comment has a status change
865 status = co.status_change
865 status = co.status_change
866 if status:
866 if status:
867 _ver_pr = status[0].comment.pull_request_version_id
867 _ver_pr = status[0].comment.pull_request_version_id
868 c.review_versions[_ver_pr] = status[0]
868 c.review_versions[_ver_pr] = status[0]
869
869
870 return render('/pullrequests/pullrequest_show.mako')
870 return render('/pullrequests/pullrequest_show.mako')
871
871
872 @LoginRequired()
872 @LoginRequired()
873 @NotAnonymous()
873 @NotAnonymous()
874 @HasRepoPermissionAnyDecorator(
874 @HasRepoPermissionAnyDecorator(
875 'repository.read', 'repository.write', 'repository.admin')
875 'repository.read', 'repository.write', 'repository.admin')
876 @auth.CSRFRequired()
876 @auth.CSRFRequired()
877 @jsonify
877 @jsonify
878 def comment(self, repo_name, pull_request_id):
878 def comment(self, repo_name, pull_request_id):
879 pull_request_id = safe_int(pull_request_id)
879 pull_request_id = safe_int(pull_request_id)
880 pull_request = PullRequest.get_or_404(pull_request_id)
880 pull_request = PullRequest.get_or_404(pull_request_id)
881 if pull_request.is_closed():
881 if pull_request.is_closed():
882 log.debug('comment: forbidden because pull request is closed')
882 log.debug('comment: forbidden because pull request is closed')
883 raise HTTPForbidden()
883 raise HTTPForbidden()
884
884
885 status = request.POST.get('changeset_status', None)
885 status = request.POST.get('changeset_status', None)
886 text = request.POST.get('text')
886 text = request.POST.get('text')
887 comment_type = request.POST.get('comment_type')
887 comment_type = request.POST.get('comment_type')
888 resolves_comment_id = request.POST.get('resolves_comment_id', None)
888 resolves_comment_id = request.POST.get('resolves_comment_id', None)
889 close_pull_request = request.POST.get('close_pull_request')
889 close_pull_request = request.POST.get('close_pull_request')
890
890
891 # the logic here should work like following, if we submit close
891 # the logic here should work like following, if we submit close
892 # pr comment, use `close_pull_request_with_comment` function
892 # pr comment, use `close_pull_request_with_comment` function
893 # else handle regular comment logic
893 # else handle regular comment logic
894 user = c.rhodecode_user
894 user = c.rhodecode_user
895 repo = c.rhodecode_db_repo
895 repo = c.rhodecode_db_repo
896
896
897 if close_pull_request:
897 if close_pull_request:
898 # only owner or admin or person with write permissions
898 # only owner or admin or person with write permissions
899 allowed_to_close = PullRequestModel().check_user_update(
899 allowed_to_close = PullRequestModel().check_user_update(
900 pull_request, c.rhodecode_user)
900 pull_request, c.rhodecode_user)
901 if not allowed_to_close:
901 if not allowed_to_close:
902 log.debug('comment: forbidden because not allowed to close '
902 log.debug('comment: forbidden because not allowed to close '
903 'pull request %s', pull_request_id)
903 'pull request %s', pull_request_id)
904 raise HTTPForbidden()
904 raise HTTPForbidden()
905 comment, status = PullRequestModel().close_pull_request_with_comment(
905 comment, status = PullRequestModel().close_pull_request_with_comment(
906 pull_request, user, repo, message=text)
906 pull_request, user, repo, message=text)
907 Session().flush()
907 Session().flush()
908 events.trigger(
908 events.trigger(
909 events.PullRequestCommentEvent(pull_request, comment))
909 events.PullRequestCommentEvent(pull_request, comment))
910
910
911 else:
911 else:
912 # regular comment case, could be inline, or one with status.
912 # regular comment case, could be inline, or one with status.
913 # for that one we check also permissions
913 # for that one we check also permissions
914
914
915 allowed_to_change_status = PullRequestModel().check_user_change_status(
915 allowed_to_change_status = PullRequestModel().check_user_change_status(
916 pull_request, c.rhodecode_user)
916 pull_request, c.rhodecode_user)
917
917
918 if status and allowed_to_change_status:
918 if status and allowed_to_change_status:
919 message = (_('Status change %(transition_icon)s %(status)s')
919 message = (_('Status change %(transition_icon)s %(status)s')
920 % {'transition_icon': '>',
920 % {'transition_icon': '>',
921 'status': ChangesetStatus.get_status_lbl(status)})
921 'status': ChangesetStatus.get_status_lbl(status)})
922 text = text or message
922 text = text or message
923
923
924 comment = CommentsModel().create(
924 comment = CommentsModel().create(
925 text=text,
925 text=text,
926 repo=c.rhodecode_db_repo.repo_id,
926 repo=c.rhodecode_db_repo.repo_id,
927 user=c.rhodecode_user.user_id,
927 user=c.rhodecode_user.user_id,
928 pull_request=pull_request_id,
928 pull_request=pull_request_id,
929 f_path=request.POST.get('f_path'),
929 f_path=request.POST.get('f_path'),
930 line_no=request.POST.get('line'),
930 line_no=request.POST.get('line'),
931 status_change=(ChangesetStatus.get_status_lbl(status)
931 status_change=(ChangesetStatus.get_status_lbl(status)
932 if status and allowed_to_change_status else None),
932 if status and allowed_to_change_status else None),
933 status_change_type=(status
933 status_change_type=(status
934 if status and allowed_to_change_status else None),
934 if status and allowed_to_change_status else None),
935 comment_type=comment_type,
935 comment_type=comment_type,
936 resolves_comment_id=resolves_comment_id
936 resolves_comment_id=resolves_comment_id
937 )
937 )
938
938
939 if allowed_to_change_status:
939 if allowed_to_change_status:
940 # calculate old status before we change it
940 # calculate old status before we change it
941 old_calculated_status = pull_request.calculated_review_status()
941 old_calculated_status = pull_request.calculated_review_status()
942
942
943 # get status if set !
943 # get status if set !
944 if status:
944 if status:
945 ChangesetStatusModel().set_status(
945 ChangesetStatusModel().set_status(
946 c.rhodecode_db_repo.repo_id,
946 c.rhodecode_db_repo.repo_id,
947 status,
947 status,
948 c.rhodecode_user.user_id,
948 c.rhodecode_user.user_id,
949 comment,
949 comment,
950 pull_request=pull_request_id
950 pull_request=pull_request_id
951 )
951 )
952
952
953 Session().flush()
953 Session().flush()
954 events.trigger(
954 events.trigger(
955 events.PullRequestCommentEvent(pull_request, comment))
955 events.PullRequestCommentEvent(pull_request, comment))
956
956
957 # we now calculate the status of pull request, and based on that
957 # we now calculate the status of pull request, and based on that
958 # calculation we set the commits status
958 # calculation we set the commits status
959 calculated_status = pull_request.calculated_review_status()
959 calculated_status = pull_request.calculated_review_status()
960 if old_calculated_status != calculated_status:
960 if old_calculated_status != calculated_status:
961 PullRequestModel()._trigger_pull_request_hook(
961 PullRequestModel()._trigger_pull_request_hook(
962 pull_request, c.rhodecode_user, 'review_status_change')
962 pull_request, c.rhodecode_user, 'review_status_change')
963
963
964 Session().commit()
964 Session().commit()
965
965
966 if not request.is_xhr:
966 if not request.is_xhr:
967 raise HTTPFound(
967 raise HTTPFound(
968 h.route_path('pullrequest_show',
968 h.route_path('pullrequest_show',
969 repo_name=repo_name,
969 repo_name=repo_name,
970 pull_request_id=pull_request_id))
970 pull_request_id=pull_request_id))
971
971
972 data = {
972 data = {
973 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
973 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
974 }
974 }
975 if comment:
975 if comment:
976 c.co = comment
976 c.co = comment
977 rendered_comment = render('changeset/changeset_comment_block.mako')
977 rendered_comment = render('changeset/changeset_comment_block.mako')
978 data.update(comment.get_dict())
978 data.update(comment.get_dict())
979 data.update({'rendered_text': rendered_comment})
979 data.update({'rendered_text': rendered_comment})
980
980
981 return data
981 return data
982
982
983 @LoginRequired()
983 @LoginRequired()
984 @NotAnonymous()
984 @NotAnonymous()
985 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
985 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
986 'repository.admin')
986 'repository.admin')
987 @auth.CSRFRequired()
987 @auth.CSRFRequired()
988 @jsonify
988 @jsonify
989 def delete_comment(self, repo_name, comment_id):
989 def delete_comment(self, repo_name, comment_id):
990 comment = ChangesetComment.get_or_404(safe_int(comment_id))
990 comment = ChangesetComment.get_or_404(comment_id)
991 if not comment:
991 if not comment:
992 log.debug('Comment with id:%s not found, skipping', comment_id)
992 log.debug('Comment with id:%s not found, skipping', comment_id)
993 # comment already deleted in another call probably
993 # comment already deleted in another call probably
994 return True
994 return True
995
995
996 if comment.pull_request.is_closed():
996 if comment.pull_request.is_closed():
997 # don't allow deleting comments on closed pull request
997 # don't allow deleting comments on closed pull request
998 raise HTTPForbidden()
998 raise HTTPForbidden()
999
999
1000 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1000 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1001 super_admin = h.HasPermissionAny('hg.admin')()
1001 super_admin = h.HasPermissionAny('hg.admin')()
1002 comment_owner = comment.author.user_id == c.rhodecode_user.user_id
1002 comment_owner = comment.author.user_id == c.rhodecode_user.user_id
1003 is_repo_comment = comment.repo.repo_name == c.repo_name
1003 is_repo_comment = comment.repo.repo_name == c.repo_name
1004 comment_repo_admin = is_repo_admin and is_repo_comment
1004 comment_repo_admin = is_repo_admin and is_repo_comment
1005
1005
1006 if super_admin or comment_owner or comment_repo_admin:
1006 if super_admin or comment_owner or comment_repo_admin:
1007 old_calculated_status = comment.pull_request.calculated_review_status()
1007 old_calculated_status = comment.pull_request.calculated_review_status()
1008 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
1008 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
1009 Session().commit()
1009 Session().commit()
1010 calculated_status = comment.pull_request.calculated_review_status()
1010 calculated_status = comment.pull_request.calculated_review_status()
1011 if old_calculated_status != calculated_status:
1011 if old_calculated_status != calculated_status:
1012 PullRequestModel()._trigger_pull_request_hook(
1012 PullRequestModel()._trigger_pull_request_hook(
1013 comment.pull_request, c.rhodecode_user, 'review_status_change')
1013 comment.pull_request, c.rhodecode_user, 'review_status_change')
1014 return True
1014 return True
1015 else:
1015 else:
1016 log.warning('No permissions for user %s to delete comment_id: %s',
1016 log.warning('No permissions for user %s to delete comment_id: %s',
1017 c.rhodecode_user, comment_id)
1017 c.rhodecode_user, comment_id)
1018 raise HTTPNotFound()
1018 raise HTTPNotFound()
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now