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