##// END OF EJS Templates
users: added option to detach pull requests for users which we delete....
dan -
r4351:2d86851b default
parent child Browse files
Show More

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

@@ -1,1381 +1,1414 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
33 33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
34 34 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
35 35 from rhodecode.authentication.plugins import auth_rhodecode
36 36 from rhodecode.events import trigger
37 37 from rhodecode.model.db import true, UserNotice
38 38
39 39 from rhodecode.lib import audit_logger, rc_cache
40 40 from rhodecode.lib.exceptions import (
41 41 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
42 UserOwnsUserGroupsException, DefaultUserException)
42 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
43 UserOwnsArtifactsException, DefaultUserException)
43 44 from rhodecode.lib.ext_json import json
44 45 from rhodecode.lib.auth import (
45 46 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
46 47 from rhodecode.lib import helpers as h
47 48 from rhodecode.lib.helpers import SqlPage
48 49 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
49 50 from rhodecode.model.auth_token import AuthTokenModel
50 51 from rhodecode.model.forms import (
51 52 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
52 53 UserExtraEmailForm, UserExtraIpForm)
53 54 from rhodecode.model.permission import PermissionModel
54 55 from rhodecode.model.repo_group import RepoGroupModel
55 56 from rhodecode.model.ssh_key import SshKeyModel
56 57 from rhodecode.model.user import UserModel
57 58 from rhodecode.model.user_group import UserGroupModel
58 59 from rhodecode.model.db import (
59 60 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
60 61 UserApiKeys, UserSshKeys, RepoGroup)
61 62 from rhodecode.model.meta import Session
62 63
63 64 log = logging.getLogger(__name__)
64 65
65 66
66 67 class AdminUsersView(BaseAppView, DataGridAppView):
67 68
68 69 def load_default_context(self):
69 70 c = self._get_local_tmpl_context()
70 71 return c
71 72
72 73 @LoginRequired()
73 74 @HasPermissionAllDecorator('hg.admin')
74 75 @view_config(
75 76 route_name='users', request_method='GET',
76 77 renderer='rhodecode:templates/admin/users/users.mako')
77 78 def users_list(self):
78 79 c = self.load_default_context()
79 80 return self._get_template_context(c)
80 81
81 82 @LoginRequired()
82 83 @HasPermissionAllDecorator('hg.admin')
83 84 @view_config(
84 85 # renderer defined below
85 86 route_name='users_data', request_method='GET',
86 87 renderer='json_ext', xhr=True)
87 88 def users_list_data(self):
88 89 self.load_default_context()
89 90 column_map = {
90 91 'first_name': 'name',
91 92 'last_name': 'lastname',
92 93 }
93 94 draw, start, limit = self._extract_chunk(self.request)
94 95 search_q, order_by, order_dir = self._extract_ordering(
95 96 self.request, column_map=column_map)
96 97 _render = self.request.get_partial_renderer(
97 98 'rhodecode:templates/data_table/_dt_elements.mako')
98 99
99 100 def user_actions(user_id, username):
100 101 return _render("user_actions", user_id, username)
101 102
102 103 users_data_total_count = User.query()\
103 104 .filter(User.username != User.DEFAULT_USER) \
104 105 .count()
105 106
106 107 users_data_total_inactive_count = User.query()\
107 108 .filter(User.username != User.DEFAULT_USER) \
108 109 .filter(User.active != true())\
109 110 .count()
110 111
111 112 # json generate
112 113 base_q = User.query().filter(User.username != User.DEFAULT_USER)
113 114 base_inactive_q = base_q.filter(User.active != true())
114 115
115 116 if search_q:
116 117 like_expression = u'%{}%'.format(safe_unicode(search_q))
117 118 base_q = base_q.filter(or_(
118 119 User.username.ilike(like_expression),
119 120 User._email.ilike(like_expression),
120 121 User.name.ilike(like_expression),
121 122 User.lastname.ilike(like_expression),
122 123 ))
123 124 base_inactive_q = base_q.filter(User.active != true())
124 125
125 126 users_data_total_filtered_count = base_q.count()
126 127 users_data_total_filtered_inactive_count = base_inactive_q.count()
127 128
128 129 sort_col = getattr(User, order_by, None)
129 130 if sort_col:
130 131 if order_dir == 'asc':
131 132 # handle null values properly to order by NULL last
132 133 if order_by in ['last_activity']:
133 134 sort_col = coalesce(sort_col, datetime.date.max)
134 135 sort_col = sort_col.asc()
135 136 else:
136 137 # handle null values properly to order by NULL last
137 138 if order_by in ['last_activity']:
138 139 sort_col = coalesce(sort_col, datetime.date.min)
139 140 sort_col = sort_col.desc()
140 141
141 142 base_q = base_q.order_by(sort_col)
142 143 base_q = base_q.offset(start).limit(limit)
143 144
144 145 users_list = base_q.all()
145 146
146 147 users_data = []
147 148 for user in users_list:
148 149 users_data.append({
149 150 "username": h.gravatar_with_user(self.request, user.username),
150 151 "email": user.email,
151 152 "first_name": user.first_name,
152 153 "last_name": user.last_name,
153 154 "last_login": h.format_date(user.last_login),
154 155 "last_activity": h.format_date(user.last_activity),
155 156 "active": h.bool2icon(user.active),
156 157 "active_raw": user.active,
157 158 "admin": h.bool2icon(user.admin),
158 159 "extern_type": user.extern_type,
159 160 "extern_name": user.extern_name,
160 161 "action": user_actions(user.user_id, user.username),
161 162 })
162 163 data = ({
163 164 'draw': draw,
164 165 'data': users_data,
165 166 'recordsTotal': users_data_total_count,
166 167 'recordsFiltered': users_data_total_filtered_count,
167 168 'recordsTotalInactive': users_data_total_inactive_count,
168 169 'recordsFilteredInactive': users_data_total_filtered_inactive_count
169 170 })
170 171
171 172 return data
172 173
173 174 def _set_personal_repo_group_template_vars(self, c_obj):
174 175 DummyUser = AttributeDict({
175 176 'username': '${username}',
176 177 'user_id': '${user_id}',
177 178 })
178 179 c_obj.default_create_repo_group = RepoGroupModel() \
179 180 .get_default_create_personal_repo_group()
180 181 c_obj.personal_repo_group_name = RepoGroupModel() \
181 182 .get_personal_group_name(DummyUser)
182 183
183 184 @LoginRequired()
184 185 @HasPermissionAllDecorator('hg.admin')
185 186 @view_config(
186 187 route_name='users_new', request_method='GET',
187 188 renderer='rhodecode:templates/admin/users/user_add.mako')
188 189 def users_new(self):
189 190 _ = self.request.translate
190 191 c = self.load_default_context()
191 192 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
192 193 self._set_personal_repo_group_template_vars(c)
193 194 return self._get_template_context(c)
194 195
195 196 @LoginRequired()
196 197 @HasPermissionAllDecorator('hg.admin')
197 198 @CSRFRequired()
198 199 @view_config(
199 200 route_name='users_create', request_method='POST',
200 201 renderer='rhodecode:templates/admin/users/user_add.mako')
201 202 def users_create(self):
202 203 _ = self.request.translate
203 204 c = self.load_default_context()
204 205 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
205 206 user_model = UserModel()
206 207 user_form = UserForm(self.request.translate)()
207 208 try:
208 209 form_result = user_form.to_python(dict(self.request.POST))
209 210 user = user_model.create(form_result)
210 211 Session().flush()
211 212 creation_data = user.get_api_data()
212 213 username = form_result['username']
213 214
214 215 audit_logger.store_web(
215 216 'user.create', action_data={'data': creation_data},
216 217 user=c.rhodecode_user)
217 218
218 219 user_link = h.link_to(
219 220 h.escape(username),
220 221 h.route_path('user_edit', user_id=user.user_id))
221 222 h.flash(h.literal(_('Created user %(user_link)s')
222 223 % {'user_link': user_link}), category='success')
223 224 Session().commit()
224 225 except formencode.Invalid as errors:
225 226 self._set_personal_repo_group_template_vars(c)
226 227 data = render(
227 228 'rhodecode:templates/admin/users/user_add.mako',
228 229 self._get_template_context(c), self.request)
229 230 html = formencode.htmlfill.render(
230 231 data,
231 232 defaults=errors.value,
232 233 errors=errors.error_dict or {},
233 234 prefix_error=False,
234 235 encoding="UTF-8",
235 236 force_defaults=False
236 237 )
237 238 return Response(html)
238 239 except UserCreationError as e:
239 240 h.flash(e, 'error')
240 241 except Exception:
241 242 log.exception("Exception creation of user")
242 243 h.flash(_('Error occurred during creation of user %s')
243 244 % self.request.POST.get('username'), category='error')
244 245 raise HTTPFound(h.route_path('users'))
245 246
246 247
247 248 class UsersView(UserAppView):
248 249 ALLOW_SCOPED_TOKENS = False
249 250 """
250 251 This view has alternative version inside EE, if modified please take a look
251 252 in there as well.
252 253 """
253 254
254 255 def get_auth_plugins(self):
255 256 valid_plugins = []
256 257 authn_registry = get_authn_registry(self.request.registry)
257 258 for plugin in authn_registry.get_plugins_for_authentication():
258 259 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
259 260 valid_plugins.append(plugin)
260 261 elif plugin.name == 'rhodecode':
261 262 valid_plugins.append(plugin)
262 263
263 264 # extend our choices if user has set a bound plugin which isn't enabled at the
264 265 # moment
265 266 extern_type = self.db_user.extern_type
266 267 if extern_type not in [x.uid for x in valid_plugins]:
267 268 try:
268 269 plugin = authn_registry.get_plugin_by_uid(extern_type)
269 270 if plugin:
270 271 valid_plugins.append(plugin)
271 272
272 273 except Exception:
273 274 log.exception(
274 275 'Could not extend user plugins with `{}`'.format(extern_type))
275 276 return valid_plugins
276 277
277 278 def load_default_context(self):
278 279 req = self.request
279 280
280 281 c = self._get_local_tmpl_context()
281 282 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
282 283 c.allowed_languages = [
283 284 ('en', 'English (en)'),
284 285 ('de', 'German (de)'),
285 286 ('fr', 'French (fr)'),
286 287 ('it', 'Italian (it)'),
287 288 ('ja', 'Japanese (ja)'),
288 289 ('pl', 'Polish (pl)'),
289 290 ('pt', 'Portuguese (pt)'),
290 291 ('ru', 'Russian (ru)'),
291 292 ('zh', 'Chinese (zh)'),
292 293 ]
293 294
294 295 c.allowed_extern_types = [
295 296 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
296 297 ]
297 298
298 299 c.available_permissions = req.registry.settings['available_permissions']
299 300 PermissionModel().set_global_permission_choices(
300 301 c, gettext_translator=req.translate)
301 302
302 303 return c
303 304
304 305 @LoginRequired()
305 306 @HasPermissionAllDecorator('hg.admin')
306 307 @CSRFRequired()
307 308 @view_config(
308 309 route_name='user_update', request_method='POST',
309 310 renderer='rhodecode:templates/admin/users/user_edit.mako')
310 311 def user_update(self):
311 312 _ = self.request.translate
312 313 c = self.load_default_context()
313 314
314 315 user_id = self.db_user_id
315 316 c.user = self.db_user
316 317
317 318 c.active = 'profile'
318 319 c.extern_type = c.user.extern_type
319 320 c.extern_name = c.user.extern_name
320 321 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
321 322 available_languages = [x[0] for x in c.allowed_languages]
322 323 _form = UserForm(self.request.translate, edit=True,
323 324 available_languages=available_languages,
324 325 old_data={'user_id': user_id,
325 326 'email': c.user.email})()
326 327 form_result = {}
327 328 old_values = c.user.get_api_data()
328 329 try:
329 330 form_result = _form.to_python(dict(self.request.POST))
330 331 skip_attrs = ['extern_name']
331 332 # TODO: plugin should define if username can be updated
332 333 if c.extern_type != "rhodecode":
333 334 # forbid updating username for external accounts
334 335 skip_attrs.append('username')
335 336
336 337 UserModel().update_user(
337 338 user_id, skip_attrs=skip_attrs, **form_result)
338 339
339 340 audit_logger.store_web(
340 341 'user.edit', action_data={'old_data': old_values},
341 342 user=c.rhodecode_user)
342 343
343 344 Session().commit()
344 345 h.flash(_('User updated successfully'), category='success')
345 346 except formencode.Invalid as errors:
346 347 data = render(
347 348 'rhodecode:templates/admin/users/user_edit.mako',
348 349 self._get_template_context(c), self.request)
349 350 html = formencode.htmlfill.render(
350 351 data,
351 352 defaults=errors.value,
352 353 errors=errors.error_dict or {},
353 354 prefix_error=False,
354 355 encoding="UTF-8",
355 356 force_defaults=False
356 357 )
357 358 return Response(html)
358 359 except UserCreationError as e:
359 360 h.flash(e, 'error')
360 361 except Exception:
361 362 log.exception("Exception updating user")
362 363 h.flash(_('Error occurred during update of user %s')
363 364 % form_result.get('username'), category='error')
364 365 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
365 366
366 367 @LoginRequired()
367 368 @HasPermissionAllDecorator('hg.admin')
368 369 @CSRFRequired()
369 370 @view_config(
370 371 route_name='user_delete', request_method='POST',
371 372 renderer='rhodecode:templates/admin/users/user_edit.mako')
372 373 def user_delete(self):
373 374 _ = self.request.translate
374 375 c = self.load_default_context()
375 376 c.user = self.db_user
376 377
377 378 _repos = c.user.repositories
378 379 _repo_groups = c.user.repository_groups
379 380 _user_groups = c.user.user_groups
381 _pull_requests = c.user.user_pull_requests
380 382 _artifacts = c.user.artifacts
381 383
382 384 handle_repos = None
383 385 handle_repo_groups = None
384 386 handle_user_groups = None
387 handle_pull_requests = None
385 388 handle_artifacts = None
386 389
387 390 # calls for flash of handle based on handle case detach or delete
388 391 def set_handle_flash_repos():
389 392 handle = handle_repos
390 393 if handle == 'detach':
391 394 h.flash(_('Detached %s repositories') % len(_repos),
392 395 category='success')
393 396 elif handle == 'delete':
394 397 h.flash(_('Deleted %s repositories') % len(_repos),
395 398 category='success')
396 399
397 400 def set_handle_flash_repo_groups():
398 401 handle = handle_repo_groups
399 402 if handle == 'detach':
400 403 h.flash(_('Detached %s repository groups') % len(_repo_groups),
401 404 category='success')
402 405 elif handle == 'delete':
403 406 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
404 407 category='success')
405 408
406 409 def set_handle_flash_user_groups():
407 410 handle = handle_user_groups
408 411 if handle == 'detach':
409 412 h.flash(_('Detached %s user groups') % len(_user_groups),
410 413 category='success')
411 414 elif handle == 'delete':
412 415 h.flash(_('Deleted %s user groups') % len(_user_groups),
413 416 category='success')
414 417
418 def set_handle_flash_pull_requests():
419 handle = handle_pull_requests
420 if handle == 'detach':
421 h.flash(_('Detached %s pull requests') % len(_pull_requests),
422 category='success')
423 elif handle == 'delete':
424 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
425 category='success')
426
415 427 def set_handle_flash_artifacts():
416 428 handle = handle_artifacts
417 429 if handle == 'detach':
418 430 h.flash(_('Detached %s artifacts') % len(_artifacts),
419 431 category='success')
420 432 elif handle == 'delete':
421 433 h.flash(_('Deleted %s artifacts') % len(_artifacts),
422 434 category='success')
423 435
436 handle_user = User.get_first_super_admin()
437 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
438 if handle_user_id:
439 # NOTE(marcink): we get new owner for objects...
440 handle_user = User.get_or_404(handle_user_id)
441
424 442 if _repos and self.request.POST.get('user_repos'):
425 443 handle_repos = self.request.POST['user_repos']
426 444
427 445 if _repo_groups and self.request.POST.get('user_repo_groups'):
428 446 handle_repo_groups = self.request.POST['user_repo_groups']
429 447
430 448 if _user_groups and self.request.POST.get('user_user_groups'):
431 449 handle_user_groups = self.request.POST['user_user_groups']
432 450
451 if _pull_requests and self.request.POST.get('user_pull_requests'):
452 handle_pull_requests = self.request.POST['user_pull_requests']
453
433 454 if _artifacts and self.request.POST.get('user_artifacts'):
434 455 handle_artifacts = self.request.POST['user_artifacts']
435 456
436 457 old_values = c.user.get_api_data()
437 458
438 459 try:
439 UserModel().delete(c.user, handle_repos=handle_repos,
440 handle_repo_groups=handle_repo_groups,
441 handle_user_groups=handle_user_groups,
442 handle_artifacts=handle_artifacts)
460
461 UserModel().delete(
462 c.user,
463 handle_repos=handle_repos,
464 handle_repo_groups=handle_repo_groups,
465 handle_user_groups=handle_user_groups,
466 handle_pull_requests=handle_pull_requests,
467 handle_artifacts=handle_artifacts,
468 handle_new_owner=handle_user
469 )
443 470
444 471 audit_logger.store_web(
445 472 'user.delete', action_data={'old_data': old_values},
446 473 user=c.rhodecode_user)
447 474
448 475 Session().commit()
449 476 set_handle_flash_repos()
450 477 set_handle_flash_repo_groups()
451 478 set_handle_flash_user_groups()
479 set_handle_flash_pull_requests()
452 480 set_handle_flash_artifacts()
453 481 username = h.escape(old_values['username'])
454 482 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
455 483 except (UserOwnsReposException, UserOwnsRepoGroupsException,
456 UserOwnsUserGroupsException, DefaultUserException) as e:
484 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
485 UserOwnsArtifactsException, DefaultUserException) as e:
457 486 h.flash(e, category='warning')
458 487 except Exception:
459 488 log.exception("Exception during deletion of user")
460 489 h.flash(_('An error occurred during deletion of user'),
461 490 category='error')
462 491 raise HTTPFound(h.route_path('users'))
463 492
464 493 @LoginRequired()
465 494 @HasPermissionAllDecorator('hg.admin')
466 495 @view_config(
467 496 route_name='user_edit', request_method='GET',
468 497 renderer='rhodecode:templates/admin/users/user_edit.mako')
469 498 def user_edit(self):
470 499 _ = self.request.translate
471 500 c = self.load_default_context()
472 501 c.user = self.db_user
473 502
474 503 c.active = 'profile'
475 504 c.extern_type = c.user.extern_type
476 505 c.extern_name = c.user.extern_name
477 506 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
478 507
479 508 defaults = c.user.get_dict()
480 509 defaults.update({'language': c.user.user_data.get('language')})
481 510
482 511 data = render(
483 512 'rhodecode:templates/admin/users/user_edit.mako',
484 513 self._get_template_context(c), self.request)
485 514 html = formencode.htmlfill.render(
486 515 data,
487 516 defaults=defaults,
488 517 encoding="UTF-8",
489 518 force_defaults=False
490 519 )
491 520 return Response(html)
492 521
493 522 @LoginRequired()
494 523 @HasPermissionAllDecorator('hg.admin')
495 524 @view_config(
496 525 route_name='user_edit_advanced', request_method='GET',
497 526 renderer='rhodecode:templates/admin/users/user_edit.mako')
498 527 def user_edit_advanced(self):
499 528 _ = self.request.translate
500 529 c = self.load_default_context()
501 530
502 531 user_id = self.db_user_id
503 532 c.user = self.db_user
504 533
534 c.detach_user = User.get_first_super_admin()
535 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
536 if detach_user_id:
537 c.detach_user = User.get_or_404(detach_user_id)
538
505 539 c.active = 'advanced'
506 540 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
507 541 c.personal_repo_group_name = RepoGroupModel()\
508 542 .get_personal_group_name(c.user)
509 543
510 544 c.user_to_review_rules = sorted(
511 545 (x.user for x in c.user.user_review_rules),
512 546 key=lambda u: u.username.lower())
513 547
514 c.first_admin = User.get_first_super_admin()
515 548 defaults = c.user.get_dict()
516 549
517 550 # Interim workaround if the user participated on any pull requests as a
518 551 # reviewer.
519 552 has_review = len(c.user.reviewer_pull_requests)
520 553 c.can_delete_user = not has_review
521 554 c.can_delete_user_message = ''
522 555 inactive_link = h.link_to(
523 556 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
524 557 if has_review == 1:
525 558 c.can_delete_user_message = h.literal(_(
526 559 'The user participates as reviewer in {} pull request and '
527 560 'cannot be deleted. \nYou can set the user to '
528 561 '"{}" instead of deleting it.').format(
529 562 has_review, inactive_link))
530 563 elif has_review:
531 564 c.can_delete_user_message = h.literal(_(
532 565 'The user participates as reviewer in {} pull requests and '
533 566 'cannot be deleted. \nYou can set the user to '
534 567 '"{}" instead of deleting it.').format(
535 568 has_review, inactive_link))
536 569
537 570 data = render(
538 571 'rhodecode:templates/admin/users/user_edit.mako',
539 572 self._get_template_context(c), self.request)
540 573 html = formencode.htmlfill.render(
541 574 data,
542 575 defaults=defaults,
543 576 encoding="UTF-8",
544 577 force_defaults=False
545 578 )
546 579 return Response(html)
547 580
548 581 @LoginRequired()
549 582 @HasPermissionAllDecorator('hg.admin')
550 583 @view_config(
551 584 route_name='user_edit_global_perms', request_method='GET',
552 585 renderer='rhodecode:templates/admin/users/user_edit.mako')
553 586 def user_edit_global_perms(self):
554 587 _ = self.request.translate
555 588 c = self.load_default_context()
556 589 c.user = self.db_user
557 590
558 591 c.active = 'global_perms'
559 592
560 593 c.default_user = User.get_default_user()
561 594 defaults = c.user.get_dict()
562 595 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
563 596 defaults.update(c.default_user.get_default_perms())
564 597 defaults.update(c.user.get_default_perms())
565 598
566 599 data = render(
567 600 'rhodecode:templates/admin/users/user_edit.mako',
568 601 self._get_template_context(c), self.request)
569 602 html = formencode.htmlfill.render(
570 603 data,
571 604 defaults=defaults,
572 605 encoding="UTF-8",
573 606 force_defaults=False
574 607 )
575 608 return Response(html)
576 609
577 610 @LoginRequired()
578 611 @HasPermissionAllDecorator('hg.admin')
579 612 @CSRFRequired()
580 613 @view_config(
581 614 route_name='user_edit_global_perms_update', request_method='POST',
582 615 renderer='rhodecode:templates/admin/users/user_edit.mako')
583 616 def user_edit_global_perms_update(self):
584 617 _ = self.request.translate
585 618 c = self.load_default_context()
586 619
587 620 user_id = self.db_user_id
588 621 c.user = self.db_user
589 622
590 623 c.active = 'global_perms'
591 624 try:
592 625 # first stage that verifies the checkbox
593 626 _form = UserIndividualPermissionsForm(self.request.translate)
594 627 form_result = _form.to_python(dict(self.request.POST))
595 628 inherit_perms = form_result['inherit_default_permissions']
596 629 c.user.inherit_default_permissions = inherit_perms
597 630 Session().add(c.user)
598 631
599 632 if not inherit_perms:
600 633 # only update the individual ones if we un check the flag
601 634 _form = UserPermissionsForm(
602 635 self.request.translate,
603 636 [x[0] for x in c.repo_create_choices],
604 637 [x[0] for x in c.repo_create_on_write_choices],
605 638 [x[0] for x in c.repo_group_create_choices],
606 639 [x[0] for x in c.user_group_create_choices],
607 640 [x[0] for x in c.fork_choices],
608 641 [x[0] for x in c.inherit_default_permission_choices])()
609 642
610 643 form_result = _form.to_python(dict(self.request.POST))
611 644 form_result.update({'perm_user_id': c.user.user_id})
612 645
613 646 PermissionModel().update_user_permissions(form_result)
614 647
615 648 # TODO(marcink): implement global permissions
616 649 # audit_log.store_web('user.edit.permissions')
617 650
618 651 Session().commit()
619 652
620 653 h.flash(_('User global permissions updated successfully'),
621 654 category='success')
622 655
623 656 except formencode.Invalid as errors:
624 657 data = render(
625 658 'rhodecode:templates/admin/users/user_edit.mako',
626 659 self._get_template_context(c), self.request)
627 660 html = formencode.htmlfill.render(
628 661 data,
629 662 defaults=errors.value,
630 663 errors=errors.error_dict or {},
631 664 prefix_error=False,
632 665 encoding="UTF-8",
633 666 force_defaults=False
634 667 )
635 668 return Response(html)
636 669 except Exception:
637 670 log.exception("Exception during permissions saving")
638 671 h.flash(_('An error occurred during permissions saving'),
639 672 category='error')
640 673
641 674 affected_user_ids = [user_id]
642 675 PermissionModel().trigger_permission_flush(affected_user_ids)
643 676 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
644 677
645 678 @LoginRequired()
646 679 @HasPermissionAllDecorator('hg.admin')
647 680 @CSRFRequired()
648 681 @view_config(
649 682 route_name='user_enable_force_password_reset', request_method='POST',
650 683 renderer='rhodecode:templates/admin/users/user_edit.mako')
651 684 def user_enable_force_password_reset(self):
652 685 _ = self.request.translate
653 686 c = self.load_default_context()
654 687
655 688 user_id = self.db_user_id
656 689 c.user = self.db_user
657 690
658 691 try:
659 692 c.user.update_userdata(force_password_change=True)
660 693
661 694 msg = _('Force password change enabled for user')
662 695 audit_logger.store_web('user.edit.password_reset.enabled',
663 696 user=c.rhodecode_user)
664 697
665 698 Session().commit()
666 699 h.flash(msg, category='success')
667 700 except Exception:
668 701 log.exception("Exception during password reset for user")
669 702 h.flash(_('An error occurred during password reset for user'),
670 703 category='error')
671 704
672 705 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
673 706
674 707 @LoginRequired()
675 708 @HasPermissionAllDecorator('hg.admin')
676 709 @CSRFRequired()
677 710 @view_config(
678 711 route_name='user_disable_force_password_reset', request_method='POST',
679 712 renderer='rhodecode:templates/admin/users/user_edit.mako')
680 713 def user_disable_force_password_reset(self):
681 714 _ = self.request.translate
682 715 c = self.load_default_context()
683 716
684 717 user_id = self.db_user_id
685 718 c.user = self.db_user
686 719
687 720 try:
688 721 c.user.update_userdata(force_password_change=False)
689 722
690 723 msg = _('Force password change disabled for user')
691 724 audit_logger.store_web(
692 725 'user.edit.password_reset.disabled',
693 726 user=c.rhodecode_user)
694 727
695 728 Session().commit()
696 729 h.flash(msg, category='success')
697 730 except Exception:
698 731 log.exception("Exception during password reset for user")
699 732 h.flash(_('An error occurred during password reset for user'),
700 733 category='error')
701 734
702 735 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
703 736
704 737 @LoginRequired()
705 738 @HasPermissionAllDecorator('hg.admin')
706 739 @CSRFRequired()
707 740 @view_config(
708 741 route_name='user_notice_dismiss', request_method='POST',
709 742 renderer='json_ext', xhr=True)
710 743 def user_notice_dismiss(self):
711 744 _ = self.request.translate
712 745 c = self.load_default_context()
713 746
714 747 user_id = self.db_user_id
715 748 c.user = self.db_user
716 749 user_notice_id = safe_int(self.request.POST.get('notice_id'))
717 750 notice = UserNotice().query()\
718 751 .filter(UserNotice.user_id == user_id)\
719 752 .filter(UserNotice.user_notice_id == user_notice_id)\
720 753 .scalar()
721 754 read = False
722 755 if notice:
723 756 notice.notice_read = True
724 757 Session().add(notice)
725 758 Session().commit()
726 759 read = True
727 760
728 761 return {'notice': user_notice_id, 'read': read}
729 762
730 763 @LoginRequired()
731 764 @HasPermissionAllDecorator('hg.admin')
732 765 @CSRFRequired()
733 766 @view_config(
734 767 route_name='user_create_personal_repo_group', request_method='POST',
735 768 renderer='rhodecode:templates/admin/users/user_edit.mako')
736 769 def user_create_personal_repo_group(self):
737 770 """
738 771 Create personal repository group for this user
739 772 """
740 773 from rhodecode.model.repo_group import RepoGroupModel
741 774
742 775 _ = self.request.translate
743 776 c = self.load_default_context()
744 777
745 778 user_id = self.db_user_id
746 779 c.user = self.db_user
747 780
748 781 personal_repo_group = RepoGroup.get_user_personal_repo_group(
749 782 c.user.user_id)
750 783 if personal_repo_group:
751 784 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
752 785
753 786 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
754 787 named_personal_group = RepoGroup.get_by_group_name(
755 788 personal_repo_group_name)
756 789 try:
757 790
758 791 if named_personal_group and named_personal_group.user_id == c.user.user_id:
759 792 # migrate the same named group, and mark it as personal
760 793 named_personal_group.personal = True
761 794 Session().add(named_personal_group)
762 795 Session().commit()
763 796 msg = _('Linked repository group `%s` as personal' % (
764 797 personal_repo_group_name,))
765 798 h.flash(msg, category='success')
766 799 elif not named_personal_group:
767 800 RepoGroupModel().create_personal_repo_group(c.user)
768 801
769 802 msg = _('Created repository group `%s`' % (
770 803 personal_repo_group_name,))
771 804 h.flash(msg, category='success')
772 805 else:
773 806 msg = _('Repository group `%s` is already taken' % (
774 807 personal_repo_group_name,))
775 808 h.flash(msg, category='warning')
776 809 except Exception:
777 810 log.exception("Exception during repository group creation")
778 811 msg = _(
779 812 'An error occurred during repository group creation for user')
780 813 h.flash(msg, category='error')
781 814 Session().rollback()
782 815
783 816 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
784 817
785 818 @LoginRequired()
786 819 @HasPermissionAllDecorator('hg.admin')
787 820 @view_config(
788 821 route_name='edit_user_auth_tokens', request_method='GET',
789 822 renderer='rhodecode:templates/admin/users/user_edit.mako')
790 823 def auth_tokens(self):
791 824 _ = self.request.translate
792 825 c = self.load_default_context()
793 826 c.user = self.db_user
794 827
795 828 c.active = 'auth_tokens'
796 829
797 830 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
798 831 c.role_values = [
799 832 (x, AuthTokenModel.cls._get_role_name(x))
800 833 for x in AuthTokenModel.cls.ROLES]
801 834 c.role_options = [(c.role_values, _("Role"))]
802 835 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
803 836 c.user.user_id, show_expired=True)
804 837 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
805 838 return self._get_template_context(c)
806 839
807 840 @LoginRequired()
808 841 @HasPermissionAllDecorator('hg.admin')
809 842 @view_config(
810 843 route_name='edit_user_auth_tokens_view', request_method='POST',
811 844 renderer='json_ext', xhr=True)
812 845 def auth_tokens_view(self):
813 846 _ = self.request.translate
814 847 c = self.load_default_context()
815 848 c.user = self.db_user
816 849
817 850 auth_token_id = self.request.POST.get('auth_token_id')
818 851
819 852 if auth_token_id:
820 853 token = UserApiKeys.get_or_404(auth_token_id)
821 854
822 855 return {
823 856 'auth_token': token.api_key
824 857 }
825 858
826 859 def maybe_attach_token_scope(self, token):
827 860 # implemented in EE edition
828 861 pass
829 862
830 863 @LoginRequired()
831 864 @HasPermissionAllDecorator('hg.admin')
832 865 @CSRFRequired()
833 866 @view_config(
834 867 route_name='edit_user_auth_tokens_add', request_method='POST')
835 868 def auth_tokens_add(self):
836 869 _ = self.request.translate
837 870 c = self.load_default_context()
838 871
839 872 user_id = self.db_user_id
840 873 c.user = self.db_user
841 874
842 875 user_data = c.user.get_api_data()
843 876 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
844 877 description = self.request.POST.get('description')
845 878 role = self.request.POST.get('role')
846 879
847 880 token = UserModel().add_auth_token(
848 881 user=c.user.user_id,
849 882 lifetime_minutes=lifetime, role=role, description=description,
850 883 scope_callback=self.maybe_attach_token_scope)
851 884 token_data = token.get_api_data()
852 885
853 886 audit_logger.store_web(
854 887 'user.edit.token.add', action_data={
855 888 'data': {'token': token_data, 'user': user_data}},
856 889 user=self._rhodecode_user, )
857 890 Session().commit()
858 891
859 892 h.flash(_("Auth token successfully created"), category='success')
860 893 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
861 894
862 895 @LoginRequired()
863 896 @HasPermissionAllDecorator('hg.admin')
864 897 @CSRFRequired()
865 898 @view_config(
866 899 route_name='edit_user_auth_tokens_delete', request_method='POST')
867 900 def auth_tokens_delete(self):
868 901 _ = self.request.translate
869 902 c = self.load_default_context()
870 903
871 904 user_id = self.db_user_id
872 905 c.user = self.db_user
873 906
874 907 user_data = c.user.get_api_data()
875 908
876 909 del_auth_token = self.request.POST.get('del_auth_token')
877 910
878 911 if del_auth_token:
879 912 token = UserApiKeys.get_or_404(del_auth_token)
880 913 token_data = token.get_api_data()
881 914
882 915 AuthTokenModel().delete(del_auth_token, c.user.user_id)
883 916 audit_logger.store_web(
884 917 'user.edit.token.delete', action_data={
885 918 'data': {'token': token_data, 'user': user_data}},
886 919 user=self._rhodecode_user,)
887 920 Session().commit()
888 921 h.flash(_("Auth token successfully deleted"), category='success')
889 922
890 923 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
891 924
892 925 @LoginRequired()
893 926 @HasPermissionAllDecorator('hg.admin')
894 927 @view_config(
895 928 route_name='edit_user_ssh_keys', request_method='GET',
896 929 renderer='rhodecode:templates/admin/users/user_edit.mako')
897 930 def ssh_keys(self):
898 931 _ = self.request.translate
899 932 c = self.load_default_context()
900 933 c.user = self.db_user
901 934
902 935 c.active = 'ssh_keys'
903 936 c.default_key = self.request.GET.get('default_key')
904 937 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
905 938 return self._get_template_context(c)
906 939
907 940 @LoginRequired()
908 941 @HasPermissionAllDecorator('hg.admin')
909 942 @view_config(
910 943 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
911 944 renderer='rhodecode:templates/admin/users/user_edit.mako')
912 945 def ssh_keys_generate_keypair(self):
913 946 _ = self.request.translate
914 947 c = self.load_default_context()
915 948
916 949 c.user = self.db_user
917 950
918 951 c.active = 'ssh_keys_generate'
919 952 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
920 953 private_format = self.request.GET.get('private_format') \
921 954 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
922 955 c.private, c.public = SshKeyModel().generate_keypair(
923 956 comment=comment, private_format=private_format)
924 957
925 958 return self._get_template_context(c)
926 959
927 960 @LoginRequired()
928 961 @HasPermissionAllDecorator('hg.admin')
929 962 @CSRFRequired()
930 963 @view_config(
931 964 route_name='edit_user_ssh_keys_add', request_method='POST')
932 965 def ssh_keys_add(self):
933 966 _ = self.request.translate
934 967 c = self.load_default_context()
935 968
936 969 user_id = self.db_user_id
937 970 c.user = self.db_user
938 971
939 972 user_data = c.user.get_api_data()
940 973 key_data = self.request.POST.get('key_data')
941 974 description = self.request.POST.get('description')
942 975
943 976 fingerprint = 'unknown'
944 977 try:
945 978 if not key_data:
946 979 raise ValueError('Please add a valid public key')
947 980
948 981 key = SshKeyModel().parse_key(key_data.strip())
949 982 fingerprint = key.hash_md5()
950 983
951 984 ssh_key = SshKeyModel().create(
952 985 c.user.user_id, fingerprint, key.keydata, description)
953 986 ssh_key_data = ssh_key.get_api_data()
954 987
955 988 audit_logger.store_web(
956 989 'user.edit.ssh_key.add', action_data={
957 990 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
958 991 user=self._rhodecode_user, )
959 992 Session().commit()
960 993
961 994 # Trigger an event on change of keys.
962 995 trigger(SshKeyFileChangeEvent(), self.request.registry)
963 996
964 997 h.flash(_("Ssh Key successfully created"), category='success')
965 998
966 999 except IntegrityError:
967 1000 log.exception("Exception during ssh key saving")
968 1001 err = 'Such key with fingerprint `{}` already exists, ' \
969 1002 'please use a different one'.format(fingerprint)
970 1003 h.flash(_('An error occurred during ssh key saving: {}').format(err),
971 1004 category='error')
972 1005 except Exception as e:
973 1006 log.exception("Exception during ssh key saving")
974 1007 h.flash(_('An error occurred during ssh key saving: {}').format(e),
975 1008 category='error')
976 1009
977 1010 return HTTPFound(
978 1011 h.route_path('edit_user_ssh_keys', user_id=user_id))
979 1012
980 1013 @LoginRequired()
981 1014 @HasPermissionAllDecorator('hg.admin')
982 1015 @CSRFRequired()
983 1016 @view_config(
984 1017 route_name='edit_user_ssh_keys_delete', request_method='POST')
985 1018 def ssh_keys_delete(self):
986 1019 _ = self.request.translate
987 1020 c = self.load_default_context()
988 1021
989 1022 user_id = self.db_user_id
990 1023 c.user = self.db_user
991 1024
992 1025 user_data = c.user.get_api_data()
993 1026
994 1027 del_ssh_key = self.request.POST.get('del_ssh_key')
995 1028
996 1029 if del_ssh_key:
997 1030 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
998 1031 ssh_key_data = ssh_key.get_api_data()
999 1032
1000 1033 SshKeyModel().delete(del_ssh_key, c.user.user_id)
1001 1034 audit_logger.store_web(
1002 1035 'user.edit.ssh_key.delete', action_data={
1003 1036 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
1004 1037 user=self._rhodecode_user,)
1005 1038 Session().commit()
1006 1039 # Trigger an event on change of keys.
1007 1040 trigger(SshKeyFileChangeEvent(), self.request.registry)
1008 1041 h.flash(_("Ssh key successfully deleted"), category='success')
1009 1042
1010 1043 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
1011 1044
1012 1045 @LoginRequired()
1013 1046 @HasPermissionAllDecorator('hg.admin')
1014 1047 @view_config(
1015 1048 route_name='edit_user_emails', request_method='GET',
1016 1049 renderer='rhodecode:templates/admin/users/user_edit.mako')
1017 1050 def emails(self):
1018 1051 _ = self.request.translate
1019 1052 c = self.load_default_context()
1020 1053 c.user = self.db_user
1021 1054
1022 1055 c.active = 'emails'
1023 1056 c.user_email_map = UserEmailMap.query() \
1024 1057 .filter(UserEmailMap.user == c.user).all()
1025 1058
1026 1059 return self._get_template_context(c)
1027 1060
1028 1061 @LoginRequired()
1029 1062 @HasPermissionAllDecorator('hg.admin')
1030 1063 @CSRFRequired()
1031 1064 @view_config(
1032 1065 route_name='edit_user_emails_add', request_method='POST')
1033 1066 def emails_add(self):
1034 1067 _ = self.request.translate
1035 1068 c = self.load_default_context()
1036 1069
1037 1070 user_id = self.db_user_id
1038 1071 c.user = self.db_user
1039 1072
1040 1073 email = self.request.POST.get('new_email')
1041 1074 user_data = c.user.get_api_data()
1042 1075 try:
1043 1076
1044 1077 form = UserExtraEmailForm(self.request.translate)()
1045 1078 data = form.to_python({'email': email})
1046 1079 email = data['email']
1047 1080
1048 1081 UserModel().add_extra_email(c.user.user_id, email)
1049 1082 audit_logger.store_web(
1050 1083 'user.edit.email.add',
1051 1084 action_data={'email': email, 'user': user_data},
1052 1085 user=self._rhodecode_user)
1053 1086 Session().commit()
1054 1087 h.flash(_("Added new email address `%s` for user account") % email,
1055 1088 category='success')
1056 1089 except formencode.Invalid as error:
1057 1090 h.flash(h.escape(error.error_dict['email']), category='error')
1058 1091 except IntegrityError:
1059 1092 log.warning("Email %s already exists", email)
1060 1093 h.flash(_('Email `{}` is already registered for another user.').format(email),
1061 1094 category='error')
1062 1095 except Exception:
1063 1096 log.exception("Exception during email saving")
1064 1097 h.flash(_('An error occurred during email saving'),
1065 1098 category='error')
1066 1099 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1067 1100
1068 1101 @LoginRequired()
1069 1102 @HasPermissionAllDecorator('hg.admin')
1070 1103 @CSRFRequired()
1071 1104 @view_config(
1072 1105 route_name='edit_user_emails_delete', request_method='POST')
1073 1106 def emails_delete(self):
1074 1107 _ = self.request.translate
1075 1108 c = self.load_default_context()
1076 1109
1077 1110 user_id = self.db_user_id
1078 1111 c.user = self.db_user
1079 1112
1080 1113 email_id = self.request.POST.get('del_email_id')
1081 1114 user_model = UserModel()
1082 1115
1083 1116 email = UserEmailMap.query().get(email_id).email
1084 1117 user_data = c.user.get_api_data()
1085 1118 user_model.delete_extra_email(c.user.user_id, email_id)
1086 1119 audit_logger.store_web(
1087 1120 'user.edit.email.delete',
1088 1121 action_data={'email': email, 'user': user_data},
1089 1122 user=self._rhodecode_user)
1090 1123 Session().commit()
1091 1124 h.flash(_("Removed email address from user account"),
1092 1125 category='success')
1093 1126 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1094 1127
1095 1128 @LoginRequired()
1096 1129 @HasPermissionAllDecorator('hg.admin')
1097 1130 @view_config(
1098 1131 route_name='edit_user_ips', request_method='GET',
1099 1132 renderer='rhodecode:templates/admin/users/user_edit.mako')
1100 1133 def ips(self):
1101 1134 _ = self.request.translate
1102 1135 c = self.load_default_context()
1103 1136 c.user = self.db_user
1104 1137
1105 1138 c.active = 'ips'
1106 1139 c.user_ip_map = UserIpMap.query() \
1107 1140 .filter(UserIpMap.user == c.user).all()
1108 1141
1109 1142 c.inherit_default_ips = c.user.inherit_default_permissions
1110 1143 c.default_user_ip_map = UserIpMap.query() \
1111 1144 .filter(UserIpMap.user == User.get_default_user()).all()
1112 1145
1113 1146 return self._get_template_context(c)
1114 1147
1115 1148 @LoginRequired()
1116 1149 @HasPermissionAllDecorator('hg.admin')
1117 1150 @CSRFRequired()
1118 1151 @view_config(
1119 1152 route_name='edit_user_ips_add', request_method='POST')
1120 1153 # NOTE(marcink): this view is allowed for default users, as we can
1121 1154 # edit their IP white list
1122 1155 def ips_add(self):
1123 1156 _ = self.request.translate
1124 1157 c = self.load_default_context()
1125 1158
1126 1159 user_id = self.db_user_id
1127 1160 c.user = self.db_user
1128 1161
1129 1162 user_model = UserModel()
1130 1163 desc = self.request.POST.get('description')
1131 1164 try:
1132 1165 ip_list = user_model.parse_ip_range(
1133 1166 self.request.POST.get('new_ip'))
1134 1167 except Exception as e:
1135 1168 ip_list = []
1136 1169 log.exception("Exception during ip saving")
1137 1170 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1138 1171 category='error')
1139 1172 added = []
1140 1173 user_data = c.user.get_api_data()
1141 1174 for ip in ip_list:
1142 1175 try:
1143 1176 form = UserExtraIpForm(self.request.translate)()
1144 1177 data = form.to_python({'ip': ip})
1145 1178 ip = data['ip']
1146 1179
1147 1180 user_model.add_extra_ip(c.user.user_id, ip, desc)
1148 1181 audit_logger.store_web(
1149 1182 'user.edit.ip.add',
1150 1183 action_data={'ip': ip, 'user': user_data},
1151 1184 user=self._rhodecode_user)
1152 1185 Session().commit()
1153 1186 added.append(ip)
1154 1187 except formencode.Invalid as error:
1155 1188 msg = error.error_dict['ip']
1156 1189 h.flash(msg, category='error')
1157 1190 except Exception:
1158 1191 log.exception("Exception during ip saving")
1159 1192 h.flash(_('An error occurred during ip saving'),
1160 1193 category='error')
1161 1194 if added:
1162 1195 h.flash(
1163 1196 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1164 1197 category='success')
1165 1198 if 'default_user' in self.request.POST:
1166 1199 # case for editing global IP list we do it for 'DEFAULT' user
1167 1200 raise HTTPFound(h.route_path('admin_permissions_ips'))
1168 1201 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1169 1202
1170 1203 @LoginRequired()
1171 1204 @HasPermissionAllDecorator('hg.admin')
1172 1205 @CSRFRequired()
1173 1206 @view_config(
1174 1207 route_name='edit_user_ips_delete', request_method='POST')
1175 1208 # NOTE(marcink): this view is allowed for default users, as we can
1176 1209 # edit their IP white list
1177 1210 def ips_delete(self):
1178 1211 _ = self.request.translate
1179 1212 c = self.load_default_context()
1180 1213
1181 1214 user_id = self.db_user_id
1182 1215 c.user = self.db_user
1183 1216
1184 1217 ip_id = self.request.POST.get('del_ip_id')
1185 1218 user_model = UserModel()
1186 1219 user_data = c.user.get_api_data()
1187 1220 ip = UserIpMap.query().get(ip_id).ip_addr
1188 1221 user_model.delete_extra_ip(c.user.user_id, ip_id)
1189 1222 audit_logger.store_web(
1190 1223 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1191 1224 user=self._rhodecode_user)
1192 1225 Session().commit()
1193 1226 h.flash(_("Removed ip address from user whitelist"), category='success')
1194 1227
1195 1228 if 'default_user' in self.request.POST:
1196 1229 # case for editing global IP list we do it for 'DEFAULT' user
1197 1230 raise HTTPFound(h.route_path('admin_permissions_ips'))
1198 1231 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1199 1232
1200 1233 @LoginRequired()
1201 1234 @HasPermissionAllDecorator('hg.admin')
1202 1235 @view_config(
1203 1236 route_name='edit_user_groups_management', request_method='GET',
1204 1237 renderer='rhodecode:templates/admin/users/user_edit.mako')
1205 1238 def groups_management(self):
1206 1239 c = self.load_default_context()
1207 1240 c.user = self.db_user
1208 1241 c.data = c.user.group_member
1209 1242
1210 1243 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1211 1244 for group in c.user.group_member]
1212 1245 c.groups = json.dumps(groups)
1213 1246 c.active = 'groups'
1214 1247
1215 1248 return self._get_template_context(c)
1216 1249
1217 1250 @LoginRequired()
1218 1251 @HasPermissionAllDecorator('hg.admin')
1219 1252 @CSRFRequired()
1220 1253 @view_config(
1221 1254 route_name='edit_user_groups_management_updates', request_method='POST')
1222 1255 def groups_management_updates(self):
1223 1256 _ = self.request.translate
1224 1257 c = self.load_default_context()
1225 1258
1226 1259 user_id = self.db_user_id
1227 1260 c.user = self.db_user
1228 1261
1229 1262 user_groups = set(self.request.POST.getall('users_group_id'))
1230 1263 user_groups_objects = []
1231 1264
1232 1265 for ugid in user_groups:
1233 1266 user_groups_objects.append(
1234 1267 UserGroupModel().get_group(safe_int(ugid)))
1235 1268 user_group_model = UserGroupModel()
1236 1269 added_to_groups, removed_from_groups = \
1237 1270 user_group_model.change_groups(c.user, user_groups_objects)
1238 1271
1239 1272 user_data = c.user.get_api_data()
1240 1273 for user_group_id in added_to_groups:
1241 1274 user_group = UserGroup.get(user_group_id)
1242 1275 old_values = user_group.get_api_data()
1243 1276 audit_logger.store_web(
1244 1277 'user_group.edit.member.add',
1245 1278 action_data={'user': user_data, 'old_data': old_values},
1246 1279 user=self._rhodecode_user)
1247 1280
1248 1281 for user_group_id in removed_from_groups:
1249 1282 user_group = UserGroup.get(user_group_id)
1250 1283 old_values = user_group.get_api_data()
1251 1284 audit_logger.store_web(
1252 1285 'user_group.edit.member.delete',
1253 1286 action_data={'user': user_data, 'old_data': old_values},
1254 1287 user=self._rhodecode_user)
1255 1288
1256 1289 Session().commit()
1257 1290 c.active = 'user_groups_management'
1258 1291 h.flash(_("Groups successfully changed"), category='success')
1259 1292
1260 1293 return HTTPFound(h.route_path(
1261 1294 'edit_user_groups_management', user_id=user_id))
1262 1295
1263 1296 @LoginRequired()
1264 1297 @HasPermissionAllDecorator('hg.admin')
1265 1298 @view_config(
1266 1299 route_name='edit_user_audit_logs', request_method='GET',
1267 1300 renderer='rhodecode:templates/admin/users/user_edit.mako')
1268 1301 def user_audit_logs(self):
1269 1302 _ = self.request.translate
1270 1303 c = self.load_default_context()
1271 1304 c.user = self.db_user
1272 1305
1273 1306 c.active = 'audit'
1274 1307
1275 1308 p = safe_int(self.request.GET.get('page', 1), 1)
1276 1309
1277 1310 filter_term = self.request.GET.get('filter')
1278 1311 user_log = UserModel().get_user_log(c.user, filter_term)
1279 1312
1280 1313 def url_generator(page_num):
1281 1314 query_params = {
1282 1315 'page': page_num
1283 1316 }
1284 1317 if filter_term:
1285 1318 query_params['filter'] = filter_term
1286 1319 return self.request.current_route_path(_query=query_params)
1287 1320
1288 1321 c.audit_logs = SqlPage(
1289 1322 user_log, page=p, items_per_page=10, url_maker=url_generator)
1290 1323 c.filter_term = filter_term
1291 1324 return self._get_template_context(c)
1292 1325
1293 1326 @LoginRequired()
1294 1327 @HasPermissionAllDecorator('hg.admin')
1295 1328 @view_config(
1296 1329 route_name='edit_user_audit_logs_download', request_method='GET',
1297 1330 renderer='string')
1298 1331 def user_audit_logs_download(self):
1299 1332 _ = self.request.translate
1300 1333 c = self.load_default_context()
1301 1334 c.user = self.db_user
1302 1335
1303 1336 user_log = UserModel().get_user_log(c.user, filter_term=None)
1304 1337
1305 1338 audit_log_data = {}
1306 1339 for entry in user_log:
1307 1340 audit_log_data[entry.user_log_id] = entry.get_dict()
1308 1341
1309 1342 response = Response(json.dumps(audit_log_data, indent=4))
1310 1343 response.content_disposition = str(
1311 1344 'attachment; filename=%s' % 'user_{}_audit_logs.json'.format(c.user.user_id))
1312 1345 response.content_type = 'application/json'
1313 1346
1314 1347 return response
1315 1348
1316 1349 @LoginRequired()
1317 1350 @HasPermissionAllDecorator('hg.admin')
1318 1351 @view_config(
1319 1352 route_name='edit_user_perms_summary', request_method='GET',
1320 1353 renderer='rhodecode:templates/admin/users/user_edit.mako')
1321 1354 def user_perms_summary(self):
1322 1355 _ = self.request.translate
1323 1356 c = self.load_default_context()
1324 1357 c.user = self.db_user
1325 1358
1326 1359 c.active = 'perms_summary'
1327 1360 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1328 1361
1329 1362 return self._get_template_context(c)
1330 1363
1331 1364 @LoginRequired()
1332 1365 @HasPermissionAllDecorator('hg.admin')
1333 1366 @view_config(
1334 1367 route_name='edit_user_perms_summary_json', request_method='GET',
1335 1368 renderer='json_ext')
1336 1369 def user_perms_summary_json(self):
1337 1370 self.load_default_context()
1338 1371 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1339 1372
1340 1373 return perm_user.permissions
1341 1374
1342 1375 @LoginRequired()
1343 1376 @HasPermissionAllDecorator('hg.admin')
1344 1377 @view_config(
1345 1378 route_name='edit_user_caches', request_method='GET',
1346 1379 renderer='rhodecode:templates/admin/users/user_edit.mako')
1347 1380 def user_caches(self):
1348 1381 _ = self.request.translate
1349 1382 c = self.load_default_context()
1350 1383 c.user = self.db_user
1351 1384
1352 1385 c.active = 'caches'
1353 1386 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1354 1387
1355 1388 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1356 1389 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1357 1390 c.backend = c.region.backend
1358 1391 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1359 1392
1360 1393 return self._get_template_context(c)
1361 1394
1362 1395 @LoginRequired()
1363 1396 @HasPermissionAllDecorator('hg.admin')
1364 1397 @CSRFRequired()
1365 1398 @view_config(
1366 1399 route_name='edit_user_caches_update', request_method='POST')
1367 1400 def user_caches_update(self):
1368 1401 _ = self.request.translate
1369 1402 c = self.load_default_context()
1370 1403 c.user = self.db_user
1371 1404
1372 1405 c.active = 'caches'
1373 1406 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1374 1407
1375 1408 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1376 1409 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1377 1410
1378 1411 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1379 1412
1380 1413 return HTTPFound(h.route_path(
1381 1414 'edit_user_caches', user_id=c.user.user_id))
@@ -1,175 +1,179 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Set of custom exceptions used in RhodeCode
23 23 """
24 24
25 25 from webob.exc import HTTPClientError
26 26 from pyramid.httpexceptions import HTTPBadGateway
27 27
28 28
29 29 class LdapUsernameError(Exception):
30 30 pass
31 31
32 32
33 33 class LdapPasswordError(Exception):
34 34 pass
35 35
36 36
37 37 class LdapConnectionError(Exception):
38 38 pass
39 39
40 40
41 41 class LdapImportError(Exception):
42 42 pass
43 43
44 44
45 45 class DefaultUserException(Exception):
46 46 pass
47 47
48 48
49 49 class UserOwnsReposException(Exception):
50 50 pass
51 51
52 52
53 53 class UserOwnsRepoGroupsException(Exception):
54 54 pass
55 55
56 56
57 57 class UserOwnsUserGroupsException(Exception):
58 58 pass
59 59
60 60
61 class UserOwnsPullRequestsException(Exception):
62 pass
63
64
61 65 class UserOwnsArtifactsException(Exception):
62 66 pass
63 67
64 68
65 69 class UserGroupAssignedException(Exception):
66 70 pass
67 71
68 72
69 73 class StatusChangeOnClosedPullRequestError(Exception):
70 74 pass
71 75
72 76
73 77 class AttachedForksError(Exception):
74 78 pass
75 79
76 80
77 81 class AttachedPullRequestsError(Exception):
78 82 pass
79 83
80 84
81 85 class RepoGroupAssignmentError(Exception):
82 86 pass
83 87
84 88
85 89 class NonRelativePathError(Exception):
86 90 pass
87 91
88 92
89 93 class HTTPRequirementError(HTTPClientError):
90 94 title = explanation = 'Repository Requirement Missing'
91 95 reason = None
92 96
93 97 def __init__(self, message, *args, **kwargs):
94 98 self.title = self.explanation = message
95 99 super(HTTPRequirementError, self).__init__(*args, **kwargs)
96 100 self.args = (message, )
97 101
98 102
99 103 class HTTPLockedRC(HTTPClientError):
100 104 """
101 105 Special Exception For locked Repos in RhodeCode, the return code can
102 106 be overwritten by _code keyword argument passed into constructors
103 107 """
104 108 code = 423
105 109 title = explanation = 'Repository Locked'
106 110 reason = None
107 111
108 112 def __init__(self, message, *args, **kwargs):
109 113 from rhodecode import CONFIG
110 114 from rhodecode.lib.utils2 import safe_int
111 115 _code = CONFIG.get('lock_ret_code')
112 116 self.code = safe_int(_code, self.code)
113 117 self.title = self.explanation = message
114 118 super(HTTPLockedRC, self).__init__(*args, **kwargs)
115 119 self.args = (message, )
116 120
117 121
118 122 class HTTPBranchProtected(HTTPClientError):
119 123 """
120 124 Special Exception For Indicating that branch is protected in RhodeCode, the
121 125 return code can be overwritten by _code keyword argument passed into constructors
122 126 """
123 127 code = 403
124 128 title = explanation = 'Branch Protected'
125 129 reason = None
126 130
127 131 def __init__(self, message, *args, **kwargs):
128 132 self.title = self.explanation = message
129 133 super(HTTPBranchProtected, self).__init__(*args, **kwargs)
130 134 self.args = (message, )
131 135
132 136
133 137 class IMCCommitError(Exception):
134 138 pass
135 139
136 140
137 141 class UserCreationError(Exception):
138 142 pass
139 143
140 144
141 145 class NotAllowedToCreateUserError(Exception):
142 146 pass
143 147
144 148
145 149 class RepositoryCreationError(Exception):
146 150 pass
147 151
148 152
149 153 class VCSServerUnavailable(HTTPBadGateway):
150 154 """ HTTP Exception class for VCS Server errors """
151 155 code = 502
152 156 title = 'VCS Server Error'
153 157 causes = [
154 158 'VCS Server is not running',
155 159 'Incorrect vcs.server=host:port',
156 160 'Incorrect vcs.server.protocol',
157 161 ]
158 162
159 163 def __init__(self, message=''):
160 164 self.explanation = 'Could not connect to VCS Server'
161 165 if message:
162 166 self.explanation += ': ' + message
163 167 super(VCSServerUnavailable, self).__init__()
164 168
165 169
166 170 class ArtifactMetadataDuplicate(ValueError):
167 171
168 172 def __init__(self, *args, **kwargs):
169 173 self.err_section = kwargs.pop('err_section', None)
170 174 self.err_key = kwargs.pop('err_key', None)
171 175 super(ArtifactMetadataDuplicate, self).__init__(*args, **kwargs)
172 176
173 177
174 178 class ArtifactMetadataBadValueType(ValueError):
175 179 pass
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,2067 +1,2072 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26
27 27 import json
28 28 import logging
29 29 import os
30 30
31 31 import datetime
32 32 import urllib
33 33 import collections
34 34
35 35 from pyramid import compat
36 36 from pyramid.threadlocal import get_current_request
37 37
38 38 from rhodecode.lib.vcs.nodes import FileNode
39 39 from rhodecode.translation import lazy_ugettext
40 40 from rhodecode.lib import helpers as h, hooks_utils, diffs
41 41 from rhodecode.lib import audit_logger
42 42 from rhodecode.lib.compat import OrderedDict
43 43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 44 from rhodecode.lib.markup_renderer import (
45 45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
46 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe, AttributeDict, safe_int
46 from rhodecode.lib.utils2 import (
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
48 get_current_rhodecode_user)
47 49 from rhodecode.lib.vcs.backends.base import (
48 50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
49 51 TargetRefMissing, SourceRefMissing)
50 52 from rhodecode.lib.vcs.conf import settings as vcs_settings
51 53 from rhodecode.lib.vcs.exceptions import (
52 54 CommitDoesNotExistError, EmptyRepositoryError)
53 55 from rhodecode.model import BaseModel
54 56 from rhodecode.model.changeset_status import ChangesetStatusModel
55 57 from rhodecode.model.comment import CommentsModel
56 58 from rhodecode.model.db import (
57 59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
58 60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
59 61 from rhodecode.model.meta import Session
60 62 from rhodecode.model.notification import NotificationModel, \
61 63 EmailNotificationModel
62 64 from rhodecode.model.scm import ScmModel
63 65 from rhodecode.model.settings import VcsSettingsModel
64 66
65 67
66 68 log = logging.getLogger(__name__)
67 69
68 70
69 71 # Data structure to hold the response data when updating commits during a pull
70 72 # request update.
71 73 class UpdateResponse(object):
72 74
73 75 def __init__(self, executed, reason, new, old, common_ancestor_id,
74 76 commit_changes, source_changed, target_changed):
75 77
76 78 self.executed = executed
77 79 self.reason = reason
78 80 self.new = new
79 81 self.old = old
80 82 self.common_ancestor_id = common_ancestor_id
81 83 self.changes = commit_changes
82 84 self.source_changed = source_changed
83 85 self.target_changed = target_changed
84 86
85 87
86 88 def get_diff_info(
87 89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
88 90 get_commit_authors=True):
89 91 """
90 92 Calculates detailed diff information for usage in preview of creation of a pull-request.
91 93 This is also used for default reviewers logic
92 94 """
93 95
94 96 source_scm = source_repo.scm_instance()
95 97 target_scm = target_repo.scm_instance()
96 98
97 99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
98 100 if not ancestor_id:
99 101 raise ValueError(
100 102 'cannot calculate diff info without a common ancestor. '
101 103 'Make sure both repositories are related, and have a common forking commit.')
102 104
103 105 # case here is that want a simple diff without incoming commits,
104 106 # previewing what will be merged based only on commits in the source.
105 107 log.debug('Using ancestor %s as source_ref instead of %s',
106 108 ancestor_id, source_ref)
107 109
108 110 # source of changes now is the common ancestor
109 111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
110 112 # target commit becomes the source ref as it is the last commit
111 113 # for diff generation this logic gives proper diff
112 114 target_commit = source_scm.get_commit(commit_id=source_ref)
113 115
114 116 vcs_diff = \
115 117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
116 118 ignore_whitespace=False, context=3)
117 119
118 120 diff_processor = diffs.DiffProcessor(
119 121 vcs_diff, format='newdiff', diff_limit=None,
120 122 file_limit=None, show_full_diff=True)
121 123
122 124 _parsed = diff_processor.prepare()
123 125
124 126 all_files = []
125 127 all_files_changes = []
126 128 changed_lines = {}
127 129 stats = [0, 0]
128 130 for f in _parsed:
129 131 all_files.append(f['filename'])
130 132 all_files_changes.append({
131 133 'filename': f['filename'],
132 134 'stats': f['stats']
133 135 })
134 136 stats[0] += f['stats']['added']
135 137 stats[1] += f['stats']['deleted']
136 138
137 139 changed_lines[f['filename']] = []
138 140 if len(f['chunks']) < 2:
139 141 continue
140 142 # first line is "context" information
141 143 for chunks in f['chunks'][1:]:
142 144 for chunk in chunks['lines']:
143 145 if chunk['action'] not in ('del', 'mod'):
144 146 continue
145 147 changed_lines[f['filename']].append(chunk['old_lineno'])
146 148
147 149 commit_authors = []
148 150 user_counts = {}
149 151 email_counts = {}
150 152 author_counts = {}
151 153 _commit_cache = {}
152 154
153 155 commits = []
154 156 if get_commit_authors:
155 157 commits = target_scm.compare(
156 158 target_ref, source_ref, source_scm, merge=True,
157 159 pre_load=["author"])
158 160
159 161 for commit in commits:
160 162 user = User.get_from_cs_author(commit.author)
161 163 if user and user not in commit_authors:
162 164 commit_authors.append(user)
163 165
164 166 # lines
165 167 if get_authors:
166 168 target_commit = source_repo.get_commit(ancestor_id)
167 169
168 170 for fname, lines in changed_lines.items():
169 171 try:
170 172 node = target_commit.get_node(fname)
171 173 except Exception:
172 174 continue
173 175
174 176 if not isinstance(node, FileNode):
175 177 continue
176 178
177 179 for annotation in node.annotate:
178 180 line_no, commit_id, get_commit_func, line_text = annotation
179 181 if line_no in lines:
180 182 if commit_id not in _commit_cache:
181 183 _commit_cache[commit_id] = get_commit_func()
182 184 commit = _commit_cache[commit_id]
183 185 author = commit.author
184 186 email = commit.author_email
185 187 user = User.get_from_cs_author(author)
186 188 if user:
187 189 user_counts[user] = user_counts.get(user, 0) + 1
188 190 author_counts[author] = author_counts.get(author, 0) + 1
189 191 email_counts[email] = email_counts.get(email, 0) + 1
190 192
191 193 return {
192 194 'commits': commits,
193 195 'files': all_files_changes,
194 196 'stats': stats,
195 197 'ancestor': ancestor_id,
196 198 # original authors of modified files
197 199 'original_authors': {
198 200 'users': user_counts,
199 201 'authors': author_counts,
200 202 'emails': email_counts,
201 203 },
202 204 'commit_authors': commit_authors
203 205 }
204 206
205 207
206 208 class PullRequestModel(BaseModel):
207 209
208 210 cls = PullRequest
209 211
210 212 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
211 213
212 214 UPDATE_STATUS_MESSAGES = {
213 215 UpdateFailureReason.NONE: lazy_ugettext(
214 216 'Pull request update successful.'),
215 217 UpdateFailureReason.UNKNOWN: lazy_ugettext(
216 218 'Pull request update failed because of an unknown error.'),
217 219 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
218 220 'No update needed because the source and target have not changed.'),
219 221 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
220 222 'Pull request cannot be updated because the reference type is '
221 223 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
222 224 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
223 225 'This pull request cannot be updated because the target '
224 226 'reference is missing.'),
225 227 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
226 228 'This pull request cannot be updated because the source '
227 229 'reference is missing.'),
228 230 }
229 231 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
230 232 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
231 233
232 234 def __get_pull_request(self, pull_request):
233 235 return self._get_instance((
234 236 PullRequest, PullRequestVersion), pull_request)
235 237
236 238 def _check_perms(self, perms, pull_request, user, api=False):
237 239 if not api:
238 240 return h.HasRepoPermissionAny(*perms)(
239 241 user=user, repo_name=pull_request.target_repo.repo_name)
240 242 else:
241 243 return h.HasRepoPermissionAnyApi(*perms)(
242 244 user=user, repo_name=pull_request.target_repo.repo_name)
243 245
244 246 def check_user_read(self, pull_request, user, api=False):
245 247 _perms = ('repository.admin', 'repository.write', 'repository.read',)
246 248 return self._check_perms(_perms, pull_request, user, api)
247 249
248 250 def check_user_merge(self, pull_request, user, api=False):
249 251 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
250 252 return self._check_perms(_perms, pull_request, user, api)
251 253
252 254 def check_user_update(self, pull_request, user, api=False):
253 255 owner = user.user_id == pull_request.user_id
254 256 return self.check_user_merge(pull_request, user, api) or owner
255 257
256 258 def check_user_delete(self, pull_request, user):
257 259 owner = user.user_id == pull_request.user_id
258 260 _perms = ('repository.admin',)
259 261 return self._check_perms(_perms, pull_request, user) or owner
260 262
261 263 def check_user_change_status(self, pull_request, user, api=False):
262 264 reviewer = user.user_id in [x.user_id for x in
263 265 pull_request.reviewers]
264 266 return self.check_user_update(pull_request, user, api) or reviewer
265 267
266 268 def check_user_comment(self, pull_request, user):
267 269 owner = user.user_id == pull_request.user_id
268 270 return self.check_user_read(pull_request, user) or owner
269 271
270 272 def get(self, pull_request):
271 273 return self.__get_pull_request(pull_request)
272 274
273 275 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
274 276 statuses=None, opened_by=None, order_by=None,
275 277 order_dir='desc', only_created=False):
276 278 repo = None
277 279 if repo_name:
278 280 repo = self._get_repo(repo_name)
279 281
280 282 q = PullRequest.query()
281 283
282 284 if search_q:
283 285 like_expression = u'%{}%'.format(safe_unicode(search_q))
284 286 q = q.join(User)
285 287 q = q.filter(or_(
286 288 cast(PullRequest.pull_request_id, String).ilike(like_expression),
287 289 User.username.ilike(like_expression),
288 290 PullRequest.title.ilike(like_expression),
289 291 PullRequest.description.ilike(like_expression),
290 292 ))
291 293
292 294 # source or target
293 295 if repo and source:
294 296 q = q.filter(PullRequest.source_repo == repo)
295 297 elif repo:
296 298 q = q.filter(PullRequest.target_repo == repo)
297 299
298 300 # closed,opened
299 301 if statuses:
300 302 q = q.filter(PullRequest.status.in_(statuses))
301 303
302 304 # opened by filter
303 305 if opened_by:
304 306 q = q.filter(PullRequest.user_id.in_(opened_by))
305 307
306 308 # only get those that are in "created" state
307 309 if only_created:
308 310 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
309 311
310 312 if order_by:
311 313 order_map = {
312 314 'name_raw': PullRequest.pull_request_id,
313 315 'id': PullRequest.pull_request_id,
314 316 'title': PullRequest.title,
315 317 'updated_on_raw': PullRequest.updated_on,
316 318 'target_repo': PullRequest.target_repo_id
317 319 }
318 320 if order_dir == 'asc':
319 321 q = q.order_by(order_map[order_by].asc())
320 322 else:
321 323 q = q.order_by(order_map[order_by].desc())
322 324
323 325 return q
324 326
325 327 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
326 328 opened_by=None):
327 329 """
328 330 Count the number of pull requests for a specific repository.
329 331
330 332 :param repo_name: target or source repo
331 333 :param search_q: filter by text
332 334 :param source: boolean flag to specify if repo_name refers to source
333 335 :param statuses: list of pull request statuses
334 336 :param opened_by: author user of the pull request
335 337 :returns: int number of pull requests
336 338 """
337 339 q = self._prepare_get_all_query(
338 340 repo_name, search_q=search_q, source=source, statuses=statuses,
339 341 opened_by=opened_by)
340 342
341 343 return q.count()
342 344
343 345 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
344 346 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
345 347 """
346 348 Get all pull requests for a specific repository.
347 349
348 350 :param repo_name: target or source repo
349 351 :param search_q: filter by text
350 352 :param source: boolean flag to specify if repo_name refers to source
351 353 :param statuses: list of pull request statuses
352 354 :param opened_by: author user of the pull request
353 355 :param offset: pagination offset
354 356 :param length: length of returned list
355 357 :param order_by: order of the returned list
356 358 :param order_dir: 'asc' or 'desc' ordering direction
357 359 :returns: list of pull requests
358 360 """
359 361 q = self._prepare_get_all_query(
360 362 repo_name, search_q=search_q, source=source, statuses=statuses,
361 363 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
362 364
363 365 if length:
364 366 pull_requests = q.limit(length).offset(offset).all()
365 367 else:
366 368 pull_requests = q.all()
367 369
368 370 return pull_requests
369 371
370 372 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
371 373 opened_by=None):
372 374 """
373 375 Count the number of pull requests for a specific repository that are
374 376 awaiting review.
375 377
376 378 :param repo_name: target or source repo
377 379 :param search_q: filter by text
378 380 :param source: boolean flag to specify if repo_name refers to source
379 381 :param statuses: list of pull request statuses
380 382 :param opened_by: author user of the pull request
381 383 :returns: int number of pull requests
382 384 """
383 385 pull_requests = self.get_awaiting_review(
384 386 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
385 387
386 388 return len(pull_requests)
387 389
388 390 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
389 391 opened_by=None, offset=0, length=None,
390 392 order_by=None, order_dir='desc'):
391 393 """
392 394 Get all pull requests for a specific repository that are awaiting
393 395 review.
394 396
395 397 :param repo_name: target or source repo
396 398 :param search_q: filter by text
397 399 :param source: boolean flag to specify if repo_name refers to source
398 400 :param statuses: list of pull request statuses
399 401 :param opened_by: author user of the pull request
400 402 :param offset: pagination offset
401 403 :param length: length of returned list
402 404 :param order_by: order of the returned list
403 405 :param order_dir: 'asc' or 'desc' ordering direction
404 406 :returns: list of pull requests
405 407 """
406 408 pull_requests = self.get_all(
407 409 repo_name, search_q=search_q, source=source, statuses=statuses,
408 410 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
409 411
410 412 _filtered_pull_requests = []
411 413 for pr in pull_requests:
412 414 status = pr.calculated_review_status()
413 415 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
414 416 ChangesetStatus.STATUS_UNDER_REVIEW]:
415 417 _filtered_pull_requests.append(pr)
416 418 if length:
417 419 return _filtered_pull_requests[offset:offset+length]
418 420 else:
419 421 return _filtered_pull_requests
420 422
421 423 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
422 424 opened_by=None, user_id=None):
423 425 """
424 426 Count the number of pull requests for a specific repository that are
425 427 awaiting review from a specific user.
426 428
427 429 :param repo_name: target or source repo
428 430 :param search_q: filter by text
429 431 :param source: boolean flag to specify if repo_name refers to source
430 432 :param statuses: list of pull request statuses
431 433 :param opened_by: author user of the pull request
432 434 :param user_id: reviewer user of the pull request
433 435 :returns: int number of pull requests
434 436 """
435 437 pull_requests = self.get_awaiting_my_review(
436 438 repo_name, search_q=search_q, source=source, statuses=statuses,
437 439 opened_by=opened_by, user_id=user_id)
438 440
439 441 return len(pull_requests)
440 442
441 443 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
442 444 opened_by=None, user_id=None, offset=0,
443 445 length=None, order_by=None, order_dir='desc'):
444 446 """
445 447 Get all pull requests for a specific repository that are awaiting
446 448 review from a specific user.
447 449
448 450 :param repo_name: target or source repo
449 451 :param search_q: filter by text
450 452 :param source: boolean flag to specify if repo_name refers to source
451 453 :param statuses: list of pull request statuses
452 454 :param opened_by: author user of the pull request
453 455 :param user_id: reviewer user of the pull request
454 456 :param offset: pagination offset
455 457 :param length: length of returned list
456 458 :param order_by: order of the returned list
457 459 :param order_dir: 'asc' or 'desc' ordering direction
458 460 :returns: list of pull requests
459 461 """
460 462 pull_requests = self.get_all(
461 463 repo_name, search_q=search_q, source=source, statuses=statuses,
462 464 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
463 465
464 466 _my = PullRequestModel().get_not_reviewed(user_id)
465 467 my_participation = []
466 468 for pr in pull_requests:
467 469 if pr in _my:
468 470 my_participation.append(pr)
469 471 _filtered_pull_requests = my_participation
470 472 if length:
471 473 return _filtered_pull_requests[offset:offset+length]
472 474 else:
473 475 return _filtered_pull_requests
474 476
475 477 def get_not_reviewed(self, user_id):
476 478 return [
477 479 x.pull_request for x in PullRequestReviewers.query().filter(
478 480 PullRequestReviewers.user_id == user_id).all()
479 481 ]
480 482
481 483 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
482 484 order_by=None, order_dir='desc'):
483 485 q = PullRequest.query()
484 486 if user_id:
485 487 reviewers_subquery = Session().query(
486 488 PullRequestReviewers.pull_request_id).filter(
487 489 PullRequestReviewers.user_id == user_id).subquery()
488 490 user_filter = or_(
489 491 PullRequest.user_id == user_id,
490 492 PullRequest.pull_request_id.in_(reviewers_subquery)
491 493 )
492 494 q = PullRequest.query().filter(user_filter)
493 495
494 496 # closed,opened
495 497 if statuses:
496 498 q = q.filter(PullRequest.status.in_(statuses))
497 499
498 500 if query:
499 501 like_expression = u'%{}%'.format(safe_unicode(query))
500 502 q = q.join(User)
501 503 q = q.filter(or_(
502 504 cast(PullRequest.pull_request_id, String).ilike(like_expression),
503 505 User.username.ilike(like_expression),
504 506 PullRequest.title.ilike(like_expression),
505 507 PullRequest.description.ilike(like_expression),
506 508 ))
507 509 if order_by:
508 510 order_map = {
509 511 'name_raw': PullRequest.pull_request_id,
510 512 'title': PullRequest.title,
511 513 'updated_on_raw': PullRequest.updated_on,
512 514 'target_repo': PullRequest.target_repo_id
513 515 }
514 516 if order_dir == 'asc':
515 517 q = q.order_by(order_map[order_by].asc())
516 518 else:
517 519 q = q.order_by(order_map[order_by].desc())
518 520
519 521 return q
520 522
521 523 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
522 524 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
523 525 return q.count()
524 526
525 527 def get_im_participating_in(
526 528 self, user_id=None, statuses=None, query='', offset=0,
527 529 length=None, order_by=None, order_dir='desc'):
528 530 """
529 531 Get all Pull requests that i'm participating in, or i have opened
530 532 """
531 533
532 534 q = self._prepare_participating_query(
533 535 user_id, statuses=statuses, query=query, order_by=order_by,
534 536 order_dir=order_dir)
535 537
536 538 if length:
537 539 pull_requests = q.limit(length).offset(offset).all()
538 540 else:
539 541 pull_requests = q.all()
540 542
541 543 return pull_requests
542 544
543 545 def get_versions(self, pull_request):
544 546 """
545 547 returns version of pull request sorted by ID descending
546 548 """
547 549 return PullRequestVersion.query()\
548 550 .filter(PullRequestVersion.pull_request == pull_request)\
549 551 .order_by(PullRequestVersion.pull_request_version_id.asc())\
550 552 .all()
551 553
552 554 def get_pr_version(self, pull_request_id, version=None):
553 555 at_version = None
554 556
555 557 if version and version == 'latest':
556 558 pull_request_ver = PullRequest.get(pull_request_id)
557 559 pull_request_obj = pull_request_ver
558 560 _org_pull_request_obj = pull_request_obj
559 561 at_version = 'latest'
560 562 elif version:
561 563 pull_request_ver = PullRequestVersion.get_or_404(version)
562 564 pull_request_obj = pull_request_ver
563 565 _org_pull_request_obj = pull_request_ver.pull_request
564 566 at_version = pull_request_ver.pull_request_version_id
565 567 else:
566 568 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
567 569 pull_request_id)
568 570
569 571 pull_request_display_obj = PullRequest.get_pr_display_object(
570 572 pull_request_obj, _org_pull_request_obj)
571 573
572 574 return _org_pull_request_obj, pull_request_obj, \
573 575 pull_request_display_obj, at_version
574 576
575 577 def create(self, created_by, source_repo, source_ref, target_repo,
576 578 target_ref, revisions, reviewers, title, description=None,
577 579 common_ancestor_id=None,
578 580 description_renderer=None,
579 581 reviewer_data=None, translator=None, auth_user=None):
580 582 translator = translator or get_current_request().translate
581 583
582 584 created_by_user = self._get_user(created_by)
583 585 auth_user = auth_user or created_by_user.AuthUser()
584 586 source_repo = self._get_repo(source_repo)
585 587 target_repo = self._get_repo(target_repo)
586 588
587 589 pull_request = PullRequest()
588 590 pull_request.source_repo = source_repo
589 591 pull_request.source_ref = source_ref
590 592 pull_request.target_repo = target_repo
591 593 pull_request.target_ref = target_ref
592 594 pull_request.revisions = revisions
593 595 pull_request.title = title
594 596 pull_request.description = description
595 597 pull_request.description_renderer = description_renderer
596 598 pull_request.author = created_by_user
597 599 pull_request.reviewer_data = reviewer_data
598 600 pull_request.pull_request_state = pull_request.STATE_CREATING
599 601 pull_request.common_ancestor_id = common_ancestor_id
600 602
601 603 Session().add(pull_request)
602 604 Session().flush()
603 605
604 606 reviewer_ids = set()
605 607 # members / reviewers
606 608 for reviewer_object in reviewers:
607 609 user_id, reasons, mandatory, rules = reviewer_object
608 610 user = self._get_user(user_id)
609 611
610 612 # skip duplicates
611 613 if user.user_id in reviewer_ids:
612 614 continue
613 615
614 616 reviewer_ids.add(user.user_id)
615 617
616 618 reviewer = PullRequestReviewers()
617 619 reviewer.user = user
618 620 reviewer.pull_request = pull_request
619 621 reviewer.reasons = reasons
620 622 reviewer.mandatory = mandatory
621 623
622 624 # NOTE(marcink): pick only first rule for now
623 625 rule_id = list(rules)[0] if rules else None
624 626 rule = RepoReviewRule.get(rule_id) if rule_id else None
625 627 if rule:
626 628 review_group = rule.user_group_vote_rule(user_id)
627 629 # we check if this particular reviewer is member of a voting group
628 630 if review_group:
629 631 # NOTE(marcink):
630 632 # can be that user is member of more but we pick the first same,
631 633 # same as default reviewers algo
632 634 review_group = review_group[0]
633 635
634 636 rule_data = {
635 637 'rule_name':
636 638 rule.review_rule_name,
637 639 'rule_user_group_entry_id':
638 640 review_group.repo_review_rule_users_group_id,
639 641 'rule_user_group_name':
640 642 review_group.users_group.users_group_name,
641 643 'rule_user_group_members':
642 644 [x.user.username for x in review_group.users_group.members],
643 645 'rule_user_group_members_id':
644 646 [x.user.user_id for x in review_group.users_group.members],
645 647 }
646 648 # e.g {'vote_rule': -1, 'mandatory': True}
647 649 rule_data.update(review_group.rule_data())
648 650
649 651 reviewer.rule_data = rule_data
650 652
651 653 Session().add(reviewer)
652 654 Session().flush()
653 655
654 656 # Set approval status to "Under Review" for all commits which are
655 657 # part of this pull request.
656 658 ChangesetStatusModel().set_status(
657 659 repo=target_repo,
658 660 status=ChangesetStatus.STATUS_UNDER_REVIEW,
659 661 user=created_by_user,
660 662 pull_request=pull_request
661 663 )
662 664 # we commit early at this point. This has to do with a fact
663 665 # that before queries do some row-locking. And because of that
664 666 # we need to commit and finish transaction before below validate call
665 667 # that for large repos could be long resulting in long row locks
666 668 Session().commit()
667 669
668 670 # prepare workspace, and run initial merge simulation. Set state during that
669 671 # operation
670 672 pull_request = PullRequest.get(pull_request.pull_request_id)
671 673
672 674 # set as merging, for merge simulation, and if finished to created so we mark
673 675 # simulation is working fine
674 676 with pull_request.set_state(PullRequest.STATE_MERGING,
675 677 final_state=PullRequest.STATE_CREATED) as state_obj:
676 678 MergeCheck.validate(
677 679 pull_request, auth_user=auth_user, translator=translator)
678 680
679 681 self.notify_reviewers(pull_request, reviewer_ids)
680 682 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
681 683
682 684 creation_data = pull_request.get_api_data(with_merge_state=False)
683 685 self._log_audit_action(
684 686 'repo.pull_request.create', {'data': creation_data},
685 687 auth_user, pull_request)
686 688
687 689 return pull_request
688 690
689 691 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
690 692 pull_request = self.__get_pull_request(pull_request)
691 693 target_scm = pull_request.target_repo.scm_instance()
692 694 if action == 'create':
693 695 trigger_hook = hooks_utils.trigger_create_pull_request_hook
694 696 elif action == 'merge':
695 697 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
696 698 elif action == 'close':
697 699 trigger_hook = hooks_utils.trigger_close_pull_request_hook
698 700 elif action == 'review_status_change':
699 701 trigger_hook = hooks_utils.trigger_review_pull_request_hook
700 702 elif action == 'update':
701 703 trigger_hook = hooks_utils.trigger_update_pull_request_hook
702 704 elif action == 'comment':
703 705 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
704 706 else:
705 707 return
706 708
707 709 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
708 710 pull_request, action, trigger_hook)
709 711 trigger_hook(
710 712 username=user.username,
711 713 repo_name=pull_request.target_repo.repo_name,
712 714 repo_type=target_scm.alias,
713 715 pull_request=pull_request,
714 716 data=data)
715 717
716 718 def _get_commit_ids(self, pull_request):
717 719 """
718 720 Return the commit ids of the merged pull request.
719 721
720 722 This method is not dealing correctly yet with the lack of autoupdates
721 723 nor with the implicit target updates.
722 724 For example: if a commit in the source repo is already in the target it
723 725 will be reported anyways.
724 726 """
725 727 merge_rev = pull_request.merge_rev
726 728 if merge_rev is None:
727 729 raise ValueError('This pull request was not merged yet')
728 730
729 731 commit_ids = list(pull_request.revisions)
730 732 if merge_rev not in commit_ids:
731 733 commit_ids.append(merge_rev)
732 734
733 735 return commit_ids
734 736
735 737 def merge_repo(self, pull_request, user, extras):
736 738 log.debug("Merging pull request %s", pull_request.pull_request_id)
737 739 extras['user_agent'] = 'internal-merge'
738 740 merge_state = self._merge_pull_request(pull_request, user, extras)
739 741 if merge_state.executed:
740 742 log.debug("Merge was successful, updating the pull request comments.")
741 743 self._comment_and_close_pr(pull_request, user, merge_state)
742 744
743 745 self._log_audit_action(
744 746 'repo.pull_request.merge',
745 747 {'merge_state': merge_state.__dict__},
746 748 user, pull_request)
747 749
748 750 else:
749 751 log.warn("Merge failed, not updating the pull request.")
750 752 return merge_state
751 753
752 754 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
753 755 target_vcs = pull_request.target_repo.scm_instance()
754 756 source_vcs = pull_request.source_repo.scm_instance()
755 757
756 758 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
757 759 pr_id=pull_request.pull_request_id,
758 760 pr_title=pull_request.title,
759 761 source_repo=source_vcs.name,
760 762 source_ref_name=pull_request.source_ref_parts.name,
761 763 target_repo=target_vcs.name,
762 764 target_ref_name=pull_request.target_ref_parts.name,
763 765 )
764 766
765 767 workspace_id = self._workspace_id(pull_request)
766 768 repo_id = pull_request.target_repo.repo_id
767 769 use_rebase = self._use_rebase_for_merging(pull_request)
768 770 close_branch = self._close_branch_before_merging(pull_request)
769 771 user_name = self._user_name_for_merging(pull_request, user)
770 772
771 773 target_ref = self._refresh_reference(
772 774 pull_request.target_ref_parts, target_vcs)
773 775
774 776 callback_daemon, extras = prepare_callback_daemon(
775 777 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
776 778 host=vcs_settings.HOOKS_HOST,
777 779 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
778 780
779 781 with callback_daemon:
780 782 # TODO: johbo: Implement a clean way to run a config_override
781 783 # for a single call.
782 784 target_vcs.config.set(
783 785 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
784 786
785 787 merge_state = target_vcs.merge(
786 788 repo_id, workspace_id, target_ref, source_vcs,
787 789 pull_request.source_ref_parts,
788 790 user_name=user_name, user_email=user.email,
789 791 message=message, use_rebase=use_rebase,
790 792 close_branch=close_branch)
791 793 return merge_state
792 794
793 795 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
794 796 pull_request.merge_rev = merge_state.merge_ref.commit_id
795 797 pull_request.updated_on = datetime.datetime.now()
796 798 close_msg = close_msg or 'Pull request merged and closed'
797 799
798 800 CommentsModel().create(
799 801 text=safe_unicode(close_msg),
800 802 repo=pull_request.target_repo.repo_id,
801 803 user=user.user_id,
802 804 pull_request=pull_request.pull_request_id,
803 805 f_path=None,
804 806 line_no=None,
805 807 closing_pr=True
806 808 )
807 809
808 810 Session().add(pull_request)
809 811 Session().flush()
810 812 # TODO: paris: replace invalidation with less radical solution
811 813 ScmModel().mark_for_invalidation(
812 814 pull_request.target_repo.repo_name)
813 815 self.trigger_pull_request_hook(pull_request, user, 'merge')
814 816
815 817 def has_valid_update_type(self, pull_request):
816 818 source_ref_type = pull_request.source_ref_parts.type
817 819 return source_ref_type in self.REF_TYPES
818 820
819 821 def get_flow_commits(self, pull_request):
820 822
821 823 # source repo
822 824 source_ref_name = pull_request.source_ref_parts.name
823 825 source_ref_type = pull_request.source_ref_parts.type
824 826 source_ref_id = pull_request.source_ref_parts.commit_id
825 827 source_repo = pull_request.source_repo.scm_instance()
826 828
827 829 try:
828 830 if source_ref_type in self.REF_TYPES:
829 831 source_commit = source_repo.get_commit(source_ref_name)
830 832 else:
831 833 source_commit = source_repo.get_commit(source_ref_id)
832 834 except CommitDoesNotExistError:
833 835 raise SourceRefMissing()
834 836
835 837 # target repo
836 838 target_ref_name = pull_request.target_ref_parts.name
837 839 target_ref_type = pull_request.target_ref_parts.type
838 840 target_ref_id = pull_request.target_ref_parts.commit_id
839 841 target_repo = pull_request.target_repo.scm_instance()
840 842
841 843 try:
842 844 if target_ref_type in self.REF_TYPES:
843 845 target_commit = target_repo.get_commit(target_ref_name)
844 846 else:
845 847 target_commit = target_repo.get_commit(target_ref_id)
846 848 except CommitDoesNotExistError:
847 849 raise TargetRefMissing()
848 850
849 851 return source_commit, target_commit
850 852
851 853 def update_commits(self, pull_request, updating_user):
852 854 """
853 855 Get the updated list of commits for the pull request
854 856 and return the new pull request version and the list
855 857 of commits processed by this update action
856 858
857 859 updating_user is the user_object who triggered the update
858 860 """
859 861 pull_request = self.__get_pull_request(pull_request)
860 862 source_ref_type = pull_request.source_ref_parts.type
861 863 source_ref_name = pull_request.source_ref_parts.name
862 864 source_ref_id = pull_request.source_ref_parts.commit_id
863 865
864 866 target_ref_type = pull_request.target_ref_parts.type
865 867 target_ref_name = pull_request.target_ref_parts.name
866 868 target_ref_id = pull_request.target_ref_parts.commit_id
867 869
868 870 if not self.has_valid_update_type(pull_request):
869 871 log.debug("Skipping update of pull request %s due to ref type: %s",
870 872 pull_request, source_ref_type)
871 873 return UpdateResponse(
872 874 executed=False,
873 875 reason=UpdateFailureReason.WRONG_REF_TYPE,
874 876 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
875 877 source_changed=False, target_changed=False)
876 878
877 879 try:
878 880 source_commit, target_commit = self.get_flow_commits(pull_request)
879 881 except SourceRefMissing:
880 882 return UpdateResponse(
881 883 executed=False,
882 884 reason=UpdateFailureReason.MISSING_SOURCE_REF,
883 885 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
884 886 source_changed=False, target_changed=False)
885 887 except TargetRefMissing:
886 888 return UpdateResponse(
887 889 executed=False,
888 890 reason=UpdateFailureReason.MISSING_TARGET_REF,
889 891 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
890 892 source_changed=False, target_changed=False)
891 893
892 894 source_changed = source_ref_id != source_commit.raw_id
893 895 target_changed = target_ref_id != target_commit.raw_id
894 896
895 897 if not (source_changed or target_changed):
896 898 log.debug("Nothing changed in pull request %s", pull_request)
897 899 return UpdateResponse(
898 900 executed=False,
899 901 reason=UpdateFailureReason.NO_CHANGE,
900 902 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
901 903 source_changed=target_changed, target_changed=source_changed)
902 904
903 905 change_in_found = 'target repo' if target_changed else 'source repo'
904 906 log.debug('Updating pull request because of change in %s detected',
905 907 change_in_found)
906 908
907 909 # Finally there is a need for an update, in case of source change
908 910 # we create a new version, else just an update
909 911 if source_changed:
910 912 pull_request_version = self._create_version_from_snapshot(pull_request)
911 913 self._link_comments_to_version(pull_request_version)
912 914 else:
913 915 try:
914 916 ver = pull_request.versions[-1]
915 917 except IndexError:
916 918 ver = None
917 919
918 920 pull_request.pull_request_version_id = \
919 921 ver.pull_request_version_id if ver else None
920 922 pull_request_version = pull_request
921 923
922 924 source_repo = pull_request.source_repo.scm_instance()
923 925 target_repo = pull_request.target_repo.scm_instance()
924 926
925 927 # re-compute commit ids
926 928 old_commit_ids = pull_request.revisions
927 929 pre_load = ["author", "date", "message", "branch"]
928 930 commit_ranges = target_repo.compare(
929 931 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
930 932 pre_load=pre_load)
931 933
932 934 target_ref = target_commit.raw_id
933 935 source_ref = source_commit.raw_id
934 936 ancestor_commit_id = target_repo.get_common_ancestor(
935 937 target_ref, source_ref, source_repo)
936 938
937 939 if not ancestor_commit_id:
938 940 raise ValueError(
939 941 'cannot calculate diff info without a common ancestor. '
940 942 'Make sure both repositories are related, and have a common forking commit.')
941 943
942 944 pull_request.common_ancestor_id = ancestor_commit_id
943 945
944 946 pull_request.source_ref = '%s:%s:%s' % (
945 947 source_ref_type, source_ref_name, source_commit.raw_id)
946 948 pull_request.target_ref = '%s:%s:%s' % (
947 949 target_ref_type, target_ref_name, ancestor_commit_id)
948 950
949 951 pull_request.revisions = [
950 952 commit.raw_id for commit in reversed(commit_ranges)]
951 953 pull_request.updated_on = datetime.datetime.now()
952 954 Session().add(pull_request)
953 955 new_commit_ids = pull_request.revisions
954 956
955 957 old_diff_data, new_diff_data = self._generate_update_diffs(
956 958 pull_request, pull_request_version)
957 959
958 960 # calculate commit and file changes
959 961 commit_changes = self._calculate_commit_id_changes(
960 962 old_commit_ids, new_commit_ids)
961 963 file_changes = self._calculate_file_changes(
962 964 old_diff_data, new_diff_data)
963 965
964 966 # set comments as outdated if DIFFS changed
965 967 CommentsModel().outdate_comments(
966 968 pull_request, old_diff_data=old_diff_data,
967 969 new_diff_data=new_diff_data)
968 970
969 971 valid_commit_changes = (commit_changes.added or commit_changes.removed)
970 972 file_node_changes = (
971 973 file_changes.added or file_changes.modified or file_changes.removed)
972 974 pr_has_changes = valid_commit_changes or file_node_changes
973 975
974 976 # Add an automatic comment to the pull request, in case
975 977 # anything has changed
976 978 if pr_has_changes:
977 979 update_comment = CommentsModel().create(
978 980 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
979 981 repo=pull_request.target_repo,
980 982 user=pull_request.author,
981 983 pull_request=pull_request,
982 984 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
983 985
984 986 # Update status to "Under Review" for added commits
985 987 for commit_id in commit_changes.added:
986 988 ChangesetStatusModel().set_status(
987 989 repo=pull_request.source_repo,
988 990 status=ChangesetStatus.STATUS_UNDER_REVIEW,
989 991 comment=update_comment,
990 992 user=pull_request.author,
991 993 pull_request=pull_request,
992 994 revision=commit_id)
993 995
994 996 # send update email to users
995 997 try:
996 998 self.notify_users(pull_request=pull_request, updating_user=updating_user,
997 999 ancestor_commit_id=ancestor_commit_id,
998 1000 commit_changes=commit_changes,
999 1001 file_changes=file_changes)
1000 1002 except Exception:
1001 1003 log.exception('Failed to send email notification to users')
1002 1004
1003 1005 log.debug(
1004 1006 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1005 1007 'removed_ids: %s', pull_request.pull_request_id,
1006 1008 commit_changes.added, commit_changes.common, commit_changes.removed)
1007 1009 log.debug(
1008 1010 'Updated pull request with the following file changes: %s',
1009 1011 file_changes)
1010 1012
1011 1013 log.info(
1012 1014 "Updated pull request %s from commit %s to commit %s, "
1013 1015 "stored new version %s of this pull request.",
1014 1016 pull_request.pull_request_id, source_ref_id,
1015 1017 pull_request.source_ref_parts.commit_id,
1016 1018 pull_request_version.pull_request_version_id)
1017 1019 Session().commit()
1018 1020 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1019 1021
1020 1022 return UpdateResponse(
1021 1023 executed=True, reason=UpdateFailureReason.NONE,
1022 1024 old=pull_request, new=pull_request_version,
1023 1025 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1024 1026 source_changed=source_changed, target_changed=target_changed)
1025 1027
1026 1028 def _create_version_from_snapshot(self, pull_request):
1027 1029 version = PullRequestVersion()
1028 1030 version.title = pull_request.title
1029 1031 version.description = pull_request.description
1030 1032 version.status = pull_request.status
1031 1033 version.pull_request_state = pull_request.pull_request_state
1032 1034 version.created_on = datetime.datetime.now()
1033 1035 version.updated_on = pull_request.updated_on
1034 1036 version.user_id = pull_request.user_id
1035 1037 version.source_repo = pull_request.source_repo
1036 1038 version.source_ref = pull_request.source_ref
1037 1039 version.target_repo = pull_request.target_repo
1038 1040 version.target_ref = pull_request.target_ref
1039 1041
1040 1042 version._last_merge_source_rev = pull_request._last_merge_source_rev
1041 1043 version._last_merge_target_rev = pull_request._last_merge_target_rev
1042 1044 version.last_merge_status = pull_request.last_merge_status
1043 1045 version.last_merge_metadata = pull_request.last_merge_metadata
1044 1046 version.shadow_merge_ref = pull_request.shadow_merge_ref
1045 1047 version.merge_rev = pull_request.merge_rev
1046 1048 version.reviewer_data = pull_request.reviewer_data
1047 1049
1048 1050 version.revisions = pull_request.revisions
1049 1051 version.common_ancestor_id = pull_request.common_ancestor_id
1050 1052 version.pull_request = pull_request
1051 1053 Session().add(version)
1052 1054 Session().flush()
1053 1055
1054 1056 return version
1055 1057
1056 1058 def _generate_update_diffs(self, pull_request, pull_request_version):
1057 1059
1058 1060 diff_context = (
1059 1061 self.DIFF_CONTEXT +
1060 1062 CommentsModel.needed_extra_diff_context())
1061 1063 hide_whitespace_changes = False
1062 1064 source_repo = pull_request_version.source_repo
1063 1065 source_ref_id = pull_request_version.source_ref_parts.commit_id
1064 1066 target_ref_id = pull_request_version.target_ref_parts.commit_id
1065 1067 old_diff = self._get_diff_from_pr_or_version(
1066 1068 source_repo, source_ref_id, target_ref_id,
1067 1069 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1068 1070
1069 1071 source_repo = pull_request.source_repo
1070 1072 source_ref_id = pull_request.source_ref_parts.commit_id
1071 1073 target_ref_id = pull_request.target_ref_parts.commit_id
1072 1074
1073 1075 new_diff = self._get_diff_from_pr_or_version(
1074 1076 source_repo, source_ref_id, target_ref_id,
1075 1077 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1076 1078
1077 1079 old_diff_data = diffs.DiffProcessor(old_diff)
1078 1080 old_diff_data.prepare()
1079 1081 new_diff_data = diffs.DiffProcessor(new_diff)
1080 1082 new_diff_data.prepare()
1081 1083
1082 1084 return old_diff_data, new_diff_data
1083 1085
1084 1086 def _link_comments_to_version(self, pull_request_version):
1085 1087 """
1086 1088 Link all unlinked comments of this pull request to the given version.
1087 1089
1088 1090 :param pull_request_version: The `PullRequestVersion` to which
1089 1091 the comments shall be linked.
1090 1092
1091 1093 """
1092 1094 pull_request = pull_request_version.pull_request
1093 1095 comments = ChangesetComment.query()\
1094 1096 .filter(
1095 1097 # TODO: johbo: Should we query for the repo at all here?
1096 1098 # Pending decision on how comments of PRs are to be related
1097 1099 # to either the source repo, the target repo or no repo at all.
1098 1100 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1099 1101 ChangesetComment.pull_request == pull_request,
1100 1102 ChangesetComment.pull_request_version == None)\
1101 1103 .order_by(ChangesetComment.comment_id.asc())
1102 1104
1103 1105 # TODO: johbo: Find out why this breaks if it is done in a bulk
1104 1106 # operation.
1105 1107 for comment in comments:
1106 1108 comment.pull_request_version_id = (
1107 1109 pull_request_version.pull_request_version_id)
1108 1110 Session().add(comment)
1109 1111
1110 1112 def _calculate_commit_id_changes(self, old_ids, new_ids):
1111 1113 added = [x for x in new_ids if x not in old_ids]
1112 1114 common = [x for x in new_ids if x in old_ids]
1113 1115 removed = [x for x in old_ids if x not in new_ids]
1114 1116 total = new_ids
1115 1117 return ChangeTuple(added, common, removed, total)
1116 1118
1117 1119 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1118 1120
1119 1121 old_files = OrderedDict()
1120 1122 for diff_data in old_diff_data.parsed_diff:
1121 1123 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1122 1124
1123 1125 added_files = []
1124 1126 modified_files = []
1125 1127 removed_files = []
1126 1128 for diff_data in new_diff_data.parsed_diff:
1127 1129 new_filename = diff_data['filename']
1128 1130 new_hash = md5_safe(diff_data['raw_diff'])
1129 1131
1130 1132 old_hash = old_files.get(new_filename)
1131 1133 if not old_hash:
1132 1134 # file is not present in old diff, we have to figure out from parsed diff
1133 1135 # operation ADD/REMOVE
1134 1136 operations_dict = diff_data['stats']['ops']
1135 1137 if diffs.DEL_FILENODE in operations_dict:
1136 1138 removed_files.append(new_filename)
1137 1139 else:
1138 1140 added_files.append(new_filename)
1139 1141 else:
1140 1142 if new_hash != old_hash:
1141 1143 modified_files.append(new_filename)
1142 1144 # now remove a file from old, since we have seen it already
1143 1145 del old_files[new_filename]
1144 1146
1145 1147 # removed files is when there are present in old, but not in NEW,
1146 1148 # since we remove old files that are present in new diff, left-overs
1147 1149 # if any should be the removed files
1148 1150 removed_files.extend(old_files.keys())
1149 1151
1150 1152 return FileChangeTuple(added_files, modified_files, removed_files)
1151 1153
1152 1154 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1153 1155 """
1154 1156 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1155 1157 so it's always looking the same disregarding on which default
1156 1158 renderer system is using.
1157 1159
1158 1160 :param ancestor_commit_id: ancestor raw_id
1159 1161 :param changes: changes named tuple
1160 1162 :param file_changes: file changes named tuple
1161 1163
1162 1164 """
1163 1165 new_status = ChangesetStatus.get_status_lbl(
1164 1166 ChangesetStatus.STATUS_UNDER_REVIEW)
1165 1167
1166 1168 changed_files = (
1167 1169 file_changes.added + file_changes.modified + file_changes.removed)
1168 1170
1169 1171 params = {
1170 1172 'under_review_label': new_status,
1171 1173 'added_commits': changes.added,
1172 1174 'removed_commits': changes.removed,
1173 1175 'changed_files': changed_files,
1174 1176 'added_files': file_changes.added,
1175 1177 'modified_files': file_changes.modified,
1176 1178 'removed_files': file_changes.removed,
1177 1179 'ancestor_commit_id': ancestor_commit_id
1178 1180 }
1179 1181 renderer = RstTemplateRenderer()
1180 1182 return renderer.render('pull_request_update.mako', **params)
1181 1183
1182 1184 def edit(self, pull_request, title, description, description_renderer, user):
1183 1185 pull_request = self.__get_pull_request(pull_request)
1184 1186 old_data = pull_request.get_api_data(with_merge_state=False)
1185 1187 if pull_request.is_closed():
1186 1188 raise ValueError('This pull request is closed')
1187 1189 if title:
1188 1190 pull_request.title = title
1189 1191 pull_request.description = description
1190 1192 pull_request.updated_on = datetime.datetime.now()
1191 1193 pull_request.description_renderer = description_renderer
1192 1194 Session().add(pull_request)
1193 1195 self._log_audit_action(
1194 1196 'repo.pull_request.edit', {'old_data': old_data},
1195 1197 user, pull_request)
1196 1198
1197 1199 def update_reviewers(self, pull_request, reviewer_data, user):
1198 1200 """
1199 1201 Update the reviewers in the pull request
1200 1202
1201 1203 :param pull_request: the pr to update
1202 1204 :param reviewer_data: list of tuples
1203 1205 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1204 1206 """
1205 1207 pull_request = self.__get_pull_request(pull_request)
1206 1208 if pull_request.is_closed():
1207 1209 raise ValueError('This pull request is closed')
1208 1210
1209 1211 reviewers = {}
1210 1212 for user_id, reasons, mandatory, rules in reviewer_data:
1211 1213 if isinstance(user_id, (int, compat.string_types)):
1212 1214 user_id = self._get_user(user_id).user_id
1213 1215 reviewers[user_id] = {
1214 1216 'reasons': reasons, 'mandatory': mandatory}
1215 1217
1216 1218 reviewers_ids = set(reviewers.keys())
1217 1219 current_reviewers = PullRequestReviewers.query()\
1218 1220 .filter(PullRequestReviewers.pull_request ==
1219 1221 pull_request).all()
1220 1222 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1221 1223
1222 1224 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1223 1225 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1224 1226
1225 1227 log.debug("Adding %s reviewers", ids_to_add)
1226 1228 log.debug("Removing %s reviewers", ids_to_remove)
1227 1229 changed = False
1228 1230 added_audit_reviewers = []
1229 1231 removed_audit_reviewers = []
1230 1232
1231 1233 for uid in ids_to_add:
1232 1234 changed = True
1233 1235 _usr = self._get_user(uid)
1234 1236 reviewer = PullRequestReviewers()
1235 1237 reviewer.user = _usr
1236 1238 reviewer.pull_request = pull_request
1237 1239 reviewer.reasons = reviewers[uid]['reasons']
1238 1240 # NOTE(marcink): mandatory shouldn't be changed now
1239 1241 # reviewer.mandatory = reviewers[uid]['reasons']
1240 1242 Session().add(reviewer)
1241 1243 added_audit_reviewers.append(reviewer.get_dict())
1242 1244
1243 1245 for uid in ids_to_remove:
1244 1246 changed = True
1245 1247 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1246 1248 # that prevents and fixes cases that we added the same reviewer twice.
1247 1249 # this CAN happen due to the lack of DB checks
1248 1250 reviewers = PullRequestReviewers.query()\
1249 1251 .filter(PullRequestReviewers.user_id == uid,
1250 1252 PullRequestReviewers.pull_request == pull_request)\
1251 1253 .all()
1252 1254
1253 1255 for obj in reviewers:
1254 1256 added_audit_reviewers.append(obj.get_dict())
1255 1257 Session().delete(obj)
1256 1258
1257 1259 if changed:
1258 1260 Session().expire_all()
1259 1261 pull_request.updated_on = datetime.datetime.now()
1260 1262 Session().add(pull_request)
1261 1263
1262 1264 # finally store audit logs
1263 1265 for user_data in added_audit_reviewers:
1264 1266 self._log_audit_action(
1265 1267 'repo.pull_request.reviewer.add', {'data': user_data},
1266 1268 user, pull_request)
1267 1269 for user_data in removed_audit_reviewers:
1268 1270 self._log_audit_action(
1269 1271 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1270 1272 user, pull_request)
1271 1273
1272 1274 self.notify_reviewers(pull_request, ids_to_add)
1273 1275 return ids_to_add, ids_to_remove
1274 1276
1275 1277 def get_url(self, pull_request, request=None, permalink=False):
1276 1278 if not request:
1277 1279 request = get_current_request()
1278 1280
1279 1281 if permalink:
1280 1282 return request.route_url(
1281 1283 'pull_requests_global',
1282 1284 pull_request_id=pull_request.pull_request_id,)
1283 1285 else:
1284 1286 return request.route_url('pullrequest_show',
1285 1287 repo_name=safe_str(pull_request.target_repo.repo_name),
1286 1288 pull_request_id=pull_request.pull_request_id,)
1287 1289
1288 1290 def get_shadow_clone_url(self, pull_request, request=None):
1289 1291 """
1290 1292 Returns qualified url pointing to the shadow repository. If this pull
1291 1293 request is closed there is no shadow repository and ``None`` will be
1292 1294 returned.
1293 1295 """
1294 1296 if pull_request.is_closed():
1295 1297 return None
1296 1298 else:
1297 1299 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1298 1300 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1299 1301
1300 1302 def notify_reviewers(self, pull_request, reviewers_ids):
1301 1303 # notification to reviewers
1302 1304 if not reviewers_ids:
1303 1305 return
1304 1306
1305 1307 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1306 1308
1307 1309 pull_request_obj = pull_request
1308 1310 # get the current participants of this pull request
1309 1311 recipients = reviewers_ids
1310 1312 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1311 1313
1312 1314 pr_source_repo = pull_request_obj.source_repo
1313 1315 pr_target_repo = pull_request_obj.target_repo
1314 1316
1315 1317 pr_url = h.route_url('pullrequest_show',
1316 1318 repo_name=pr_target_repo.repo_name,
1317 1319 pull_request_id=pull_request_obj.pull_request_id,)
1318 1320
1319 1321 # set some variables for email notification
1320 1322 pr_target_repo_url = h.route_url(
1321 1323 'repo_summary', repo_name=pr_target_repo.repo_name)
1322 1324
1323 1325 pr_source_repo_url = h.route_url(
1324 1326 'repo_summary', repo_name=pr_source_repo.repo_name)
1325 1327
1326 1328 # pull request specifics
1327 1329 pull_request_commits = [
1328 1330 (x.raw_id, x.message)
1329 1331 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1330 1332
1331 1333 kwargs = {
1332 1334 'user': pull_request.author,
1333 1335 'pull_request': pull_request_obj,
1334 1336 'pull_request_commits': pull_request_commits,
1335 1337
1336 1338 'pull_request_target_repo': pr_target_repo,
1337 1339 'pull_request_target_repo_url': pr_target_repo_url,
1338 1340
1339 1341 'pull_request_source_repo': pr_source_repo,
1340 1342 'pull_request_source_repo_url': pr_source_repo_url,
1341 1343
1342 1344 'pull_request_url': pr_url,
1343 1345 }
1344 1346
1345 1347 # pre-generate the subject for notification itself
1346 1348 (subject,
1347 1349 _h, _e, # we don't care about those
1348 1350 body_plaintext) = EmailNotificationModel().render_email(
1349 1351 notification_type, **kwargs)
1350 1352
1351 1353 # create notification objects, and emails
1352 1354 NotificationModel().create(
1353 1355 created_by=pull_request.author,
1354 1356 notification_subject=subject,
1355 1357 notification_body=body_plaintext,
1356 1358 notification_type=notification_type,
1357 1359 recipients=recipients,
1358 1360 email_kwargs=kwargs,
1359 1361 )
1360 1362
1361 1363 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1362 1364 commit_changes, file_changes):
1363 1365
1364 1366 updating_user_id = updating_user.user_id
1365 1367 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1366 1368 # NOTE(marcink): send notification to all other users except to
1367 1369 # person who updated the PR
1368 1370 recipients = reviewers.difference(set([updating_user_id]))
1369 1371
1370 1372 log.debug('Notify following recipients about pull-request update %s', recipients)
1371 1373
1372 1374 pull_request_obj = pull_request
1373 1375
1374 1376 # send email about the update
1375 1377 changed_files = (
1376 1378 file_changes.added + file_changes.modified + file_changes.removed)
1377 1379
1378 1380 pr_source_repo = pull_request_obj.source_repo
1379 1381 pr_target_repo = pull_request_obj.target_repo
1380 1382
1381 1383 pr_url = h.route_url('pullrequest_show',
1382 1384 repo_name=pr_target_repo.repo_name,
1383 1385 pull_request_id=pull_request_obj.pull_request_id,)
1384 1386
1385 1387 # set some variables for email notification
1386 1388 pr_target_repo_url = h.route_url(
1387 1389 'repo_summary', repo_name=pr_target_repo.repo_name)
1388 1390
1389 1391 pr_source_repo_url = h.route_url(
1390 1392 'repo_summary', repo_name=pr_source_repo.repo_name)
1391 1393
1392 1394 email_kwargs = {
1393 1395 'date': datetime.datetime.now(),
1394 1396 'updating_user': updating_user,
1395 1397
1396 1398 'pull_request': pull_request_obj,
1397 1399
1398 1400 'pull_request_target_repo': pr_target_repo,
1399 1401 'pull_request_target_repo_url': pr_target_repo_url,
1400 1402
1401 1403 'pull_request_source_repo': pr_source_repo,
1402 1404 'pull_request_source_repo_url': pr_source_repo_url,
1403 1405
1404 1406 'pull_request_url': pr_url,
1405 1407
1406 1408 'ancestor_commit_id': ancestor_commit_id,
1407 1409 'added_commits': commit_changes.added,
1408 1410 'removed_commits': commit_changes.removed,
1409 1411 'changed_files': changed_files,
1410 1412 'added_files': file_changes.added,
1411 1413 'modified_files': file_changes.modified,
1412 1414 'removed_files': file_changes.removed,
1413 1415 }
1414 1416
1415 1417 (subject,
1416 1418 _h, _e, # we don't care about those
1417 1419 body_plaintext) = EmailNotificationModel().render_email(
1418 1420 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1419 1421
1420 1422 # create notification objects, and emails
1421 1423 NotificationModel().create(
1422 1424 created_by=updating_user,
1423 1425 notification_subject=subject,
1424 1426 notification_body=body_plaintext,
1425 1427 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1426 1428 recipients=recipients,
1427 1429 email_kwargs=email_kwargs,
1428 1430 )
1429 1431
1430 def delete(self, pull_request, user):
1432 def delete(self, pull_request, user=None):
1433 if not user:
1434 user = getattr(get_current_rhodecode_user(), 'username', None)
1435
1431 1436 pull_request = self.__get_pull_request(pull_request)
1432 1437 old_data = pull_request.get_api_data(with_merge_state=False)
1433 1438 self._cleanup_merge_workspace(pull_request)
1434 1439 self._log_audit_action(
1435 1440 'repo.pull_request.delete', {'old_data': old_data},
1436 1441 user, pull_request)
1437 1442 Session().delete(pull_request)
1438 1443
1439 1444 def close_pull_request(self, pull_request, user):
1440 1445 pull_request = self.__get_pull_request(pull_request)
1441 1446 self._cleanup_merge_workspace(pull_request)
1442 1447 pull_request.status = PullRequest.STATUS_CLOSED
1443 1448 pull_request.updated_on = datetime.datetime.now()
1444 1449 Session().add(pull_request)
1445 1450 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1446 1451
1447 1452 pr_data = pull_request.get_api_data(with_merge_state=False)
1448 1453 self._log_audit_action(
1449 1454 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1450 1455
1451 1456 def close_pull_request_with_comment(
1452 1457 self, pull_request, user, repo, message=None, auth_user=None):
1453 1458
1454 1459 pull_request_review_status = pull_request.calculated_review_status()
1455 1460
1456 1461 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1457 1462 # approved only if we have voting consent
1458 1463 status = ChangesetStatus.STATUS_APPROVED
1459 1464 else:
1460 1465 status = ChangesetStatus.STATUS_REJECTED
1461 1466 status_lbl = ChangesetStatus.get_status_lbl(status)
1462 1467
1463 1468 default_message = (
1464 1469 'Closing with status change {transition_icon} {status}.'
1465 1470 ).format(transition_icon='>', status=status_lbl)
1466 1471 text = message or default_message
1467 1472
1468 1473 # create a comment, and link it to new status
1469 1474 comment = CommentsModel().create(
1470 1475 text=text,
1471 1476 repo=repo.repo_id,
1472 1477 user=user.user_id,
1473 1478 pull_request=pull_request.pull_request_id,
1474 1479 status_change=status_lbl,
1475 1480 status_change_type=status,
1476 1481 closing_pr=True,
1477 1482 auth_user=auth_user,
1478 1483 )
1479 1484
1480 1485 # calculate old status before we change it
1481 1486 old_calculated_status = pull_request.calculated_review_status()
1482 1487 ChangesetStatusModel().set_status(
1483 1488 repo.repo_id,
1484 1489 status,
1485 1490 user.user_id,
1486 1491 comment=comment,
1487 1492 pull_request=pull_request.pull_request_id
1488 1493 )
1489 1494
1490 1495 Session().flush()
1491 1496
1492 1497 self.trigger_pull_request_hook(pull_request, user, 'comment',
1493 1498 data={'comment': comment})
1494 1499
1495 1500 # we now calculate the status of pull request again, and based on that
1496 1501 # calculation trigger status change. This might happen in cases
1497 1502 # that non-reviewer admin closes a pr, which means his vote doesn't
1498 1503 # change the status, while if he's a reviewer this might change it.
1499 1504 calculated_status = pull_request.calculated_review_status()
1500 1505 if old_calculated_status != calculated_status:
1501 1506 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1502 1507 data={'status': calculated_status})
1503 1508
1504 1509 # finally close the PR
1505 1510 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1506 1511
1507 1512 return comment, status
1508 1513
1509 1514 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1510 1515 _ = translator or get_current_request().translate
1511 1516
1512 1517 if not self._is_merge_enabled(pull_request):
1513 1518 return None, False, _('Server-side pull request merging is disabled.')
1514 1519
1515 1520 if pull_request.is_closed():
1516 1521 return None, False, _('This pull request is closed.')
1517 1522
1518 1523 merge_possible, msg = self._check_repo_requirements(
1519 1524 target=pull_request.target_repo, source=pull_request.source_repo,
1520 1525 translator=_)
1521 1526 if not merge_possible:
1522 1527 return None, merge_possible, msg
1523 1528
1524 1529 try:
1525 1530 merge_response = self._try_merge(
1526 1531 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1527 1532 log.debug("Merge response: %s", merge_response)
1528 1533 return merge_response, merge_response.possible, merge_response.merge_status_message
1529 1534 except NotImplementedError:
1530 1535 return None, False, _('Pull request merging is not supported.')
1531 1536
1532 1537 def _check_repo_requirements(self, target, source, translator):
1533 1538 """
1534 1539 Check if `target` and `source` have compatible requirements.
1535 1540
1536 1541 Currently this is just checking for largefiles.
1537 1542 """
1538 1543 _ = translator
1539 1544 target_has_largefiles = self._has_largefiles(target)
1540 1545 source_has_largefiles = self._has_largefiles(source)
1541 1546 merge_possible = True
1542 1547 message = u''
1543 1548
1544 1549 if target_has_largefiles != source_has_largefiles:
1545 1550 merge_possible = False
1546 1551 if source_has_largefiles:
1547 1552 message = _(
1548 1553 'Target repository large files support is disabled.')
1549 1554 else:
1550 1555 message = _(
1551 1556 'Source repository large files support is disabled.')
1552 1557
1553 1558 return merge_possible, message
1554 1559
1555 1560 def _has_largefiles(self, repo):
1556 1561 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1557 1562 'extensions', 'largefiles')
1558 1563 return largefiles_ui and largefiles_ui[0].active
1559 1564
1560 1565 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1561 1566 """
1562 1567 Try to merge the pull request and return the merge status.
1563 1568 """
1564 1569 log.debug(
1565 1570 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1566 1571 pull_request.pull_request_id, force_shadow_repo_refresh)
1567 1572 target_vcs = pull_request.target_repo.scm_instance()
1568 1573 # Refresh the target reference.
1569 1574 try:
1570 1575 target_ref = self._refresh_reference(
1571 1576 pull_request.target_ref_parts, target_vcs)
1572 1577 except CommitDoesNotExistError:
1573 1578 merge_state = MergeResponse(
1574 1579 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1575 1580 metadata={'target_ref': pull_request.target_ref_parts})
1576 1581 return merge_state
1577 1582
1578 1583 target_locked = pull_request.target_repo.locked
1579 1584 if target_locked and target_locked[0]:
1580 1585 locked_by = 'user:{}'.format(target_locked[0])
1581 1586 log.debug("The target repository is locked by %s.", locked_by)
1582 1587 merge_state = MergeResponse(
1583 1588 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1584 1589 metadata={'locked_by': locked_by})
1585 1590 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1586 1591 pull_request, target_ref):
1587 1592 log.debug("Refreshing the merge status of the repository.")
1588 1593 merge_state = self._refresh_merge_state(
1589 1594 pull_request, target_vcs, target_ref)
1590 1595 else:
1591 1596 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1592 1597 metadata = {
1593 1598 'unresolved_files': '',
1594 1599 'target_ref': pull_request.target_ref_parts,
1595 1600 'source_ref': pull_request.source_ref_parts,
1596 1601 }
1597 1602 if pull_request.last_merge_metadata:
1598 1603 metadata.update(pull_request.last_merge_metadata)
1599 1604
1600 1605 if not possible and target_ref.type == 'branch':
1601 1606 # NOTE(marcink): case for mercurial multiple heads on branch
1602 1607 heads = target_vcs._heads(target_ref.name)
1603 1608 if len(heads) != 1:
1604 1609 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1605 1610 metadata.update({
1606 1611 'heads': heads
1607 1612 })
1608 1613
1609 1614 merge_state = MergeResponse(
1610 1615 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1611 1616
1612 1617 return merge_state
1613 1618
1614 1619 def _refresh_reference(self, reference, vcs_repository):
1615 1620 if reference.type in self.UPDATABLE_REF_TYPES:
1616 1621 name_or_id = reference.name
1617 1622 else:
1618 1623 name_or_id = reference.commit_id
1619 1624
1620 1625 refreshed_commit = vcs_repository.get_commit(name_or_id)
1621 1626 refreshed_reference = Reference(
1622 1627 reference.type, reference.name, refreshed_commit.raw_id)
1623 1628 return refreshed_reference
1624 1629
1625 1630 def _needs_merge_state_refresh(self, pull_request, target_reference):
1626 1631 return not(
1627 1632 pull_request.revisions and
1628 1633 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1629 1634 target_reference.commit_id == pull_request._last_merge_target_rev)
1630 1635
1631 1636 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1632 1637 workspace_id = self._workspace_id(pull_request)
1633 1638 source_vcs = pull_request.source_repo.scm_instance()
1634 1639 repo_id = pull_request.target_repo.repo_id
1635 1640 use_rebase = self._use_rebase_for_merging(pull_request)
1636 1641 close_branch = self._close_branch_before_merging(pull_request)
1637 1642 merge_state = target_vcs.merge(
1638 1643 repo_id, workspace_id,
1639 1644 target_reference, source_vcs, pull_request.source_ref_parts,
1640 1645 dry_run=True, use_rebase=use_rebase,
1641 1646 close_branch=close_branch)
1642 1647
1643 1648 # Do not store the response if there was an unknown error.
1644 1649 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1645 1650 pull_request._last_merge_source_rev = \
1646 1651 pull_request.source_ref_parts.commit_id
1647 1652 pull_request._last_merge_target_rev = target_reference.commit_id
1648 1653 pull_request.last_merge_status = merge_state.failure_reason
1649 1654 pull_request.last_merge_metadata = merge_state.metadata
1650 1655
1651 1656 pull_request.shadow_merge_ref = merge_state.merge_ref
1652 1657 Session().add(pull_request)
1653 1658 Session().commit()
1654 1659
1655 1660 return merge_state
1656 1661
1657 1662 def _workspace_id(self, pull_request):
1658 1663 workspace_id = 'pr-%s' % pull_request.pull_request_id
1659 1664 return workspace_id
1660 1665
1661 1666 def generate_repo_data(self, repo, commit_id=None, branch=None,
1662 1667 bookmark=None, translator=None):
1663 1668 from rhodecode.model.repo import RepoModel
1664 1669
1665 1670 all_refs, selected_ref = \
1666 1671 self._get_repo_pullrequest_sources(
1667 1672 repo.scm_instance(), commit_id=commit_id,
1668 1673 branch=branch, bookmark=bookmark, translator=translator)
1669 1674
1670 1675 refs_select2 = []
1671 1676 for element in all_refs:
1672 1677 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1673 1678 refs_select2.append({'text': element[1], 'children': children})
1674 1679
1675 1680 return {
1676 1681 'user': {
1677 1682 'user_id': repo.user.user_id,
1678 1683 'username': repo.user.username,
1679 1684 'firstname': repo.user.first_name,
1680 1685 'lastname': repo.user.last_name,
1681 1686 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1682 1687 },
1683 1688 'name': repo.repo_name,
1684 1689 'link': RepoModel().get_url(repo),
1685 1690 'description': h.chop_at_smart(repo.description_safe, '\n'),
1686 1691 'refs': {
1687 1692 'all_refs': all_refs,
1688 1693 'selected_ref': selected_ref,
1689 1694 'select2_refs': refs_select2
1690 1695 }
1691 1696 }
1692 1697
1693 1698 def generate_pullrequest_title(self, source, source_ref, target):
1694 1699 return u'{source}#{at_ref} to {target}'.format(
1695 1700 source=source,
1696 1701 at_ref=source_ref,
1697 1702 target=target,
1698 1703 )
1699 1704
1700 1705 def _cleanup_merge_workspace(self, pull_request):
1701 1706 # Merging related cleanup
1702 1707 repo_id = pull_request.target_repo.repo_id
1703 1708 target_scm = pull_request.target_repo.scm_instance()
1704 1709 workspace_id = self._workspace_id(pull_request)
1705 1710
1706 1711 try:
1707 1712 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1708 1713 except NotImplementedError:
1709 1714 pass
1710 1715
1711 1716 def _get_repo_pullrequest_sources(
1712 1717 self, repo, commit_id=None, branch=None, bookmark=None,
1713 1718 translator=None):
1714 1719 """
1715 1720 Return a structure with repo's interesting commits, suitable for
1716 1721 the selectors in pullrequest controller
1717 1722
1718 1723 :param commit_id: a commit that must be in the list somehow
1719 1724 and selected by default
1720 1725 :param branch: a branch that must be in the list and selected
1721 1726 by default - even if closed
1722 1727 :param bookmark: a bookmark that must be in the list and selected
1723 1728 """
1724 1729 _ = translator or get_current_request().translate
1725 1730
1726 1731 commit_id = safe_str(commit_id) if commit_id else None
1727 1732 branch = safe_unicode(branch) if branch else None
1728 1733 bookmark = safe_unicode(bookmark) if bookmark else None
1729 1734
1730 1735 selected = None
1731 1736
1732 1737 # order matters: first source that has commit_id in it will be selected
1733 1738 sources = []
1734 1739 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1735 1740 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1736 1741
1737 1742 if commit_id:
1738 1743 ref_commit = (h.short_id(commit_id), commit_id)
1739 1744 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1740 1745
1741 1746 sources.append(
1742 1747 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1743 1748 )
1744 1749
1745 1750 groups = []
1746 1751
1747 1752 for group_key, ref_list, group_name, match in sources:
1748 1753 group_refs = []
1749 1754 for ref_name, ref_id in ref_list:
1750 1755 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1751 1756 group_refs.append((ref_key, ref_name))
1752 1757
1753 1758 if not selected:
1754 1759 if set([commit_id, match]) & set([ref_id, ref_name]):
1755 1760 selected = ref_key
1756 1761
1757 1762 if group_refs:
1758 1763 groups.append((group_refs, group_name))
1759 1764
1760 1765 if not selected:
1761 1766 ref = commit_id or branch or bookmark
1762 1767 if ref:
1763 1768 raise CommitDoesNotExistError(
1764 1769 u'No commit refs could be found matching: {}'.format(ref))
1765 1770 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1766 1771 selected = u'branch:{}:{}'.format(
1767 1772 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1768 1773 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1769 1774 )
1770 1775 elif repo.commit_ids:
1771 1776 # make the user select in this case
1772 1777 selected = None
1773 1778 else:
1774 1779 raise EmptyRepositoryError()
1775 1780 return groups, selected
1776 1781
1777 1782 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1778 1783 hide_whitespace_changes, diff_context):
1779 1784
1780 1785 return self._get_diff_from_pr_or_version(
1781 1786 source_repo, source_ref_id, target_ref_id,
1782 1787 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1783 1788
1784 1789 def _get_diff_from_pr_or_version(
1785 1790 self, source_repo, source_ref_id, target_ref_id,
1786 1791 hide_whitespace_changes, diff_context):
1787 1792
1788 1793 target_commit = source_repo.get_commit(
1789 1794 commit_id=safe_str(target_ref_id))
1790 1795 source_commit = source_repo.get_commit(
1791 1796 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1792 1797 if isinstance(source_repo, Repository):
1793 1798 vcs_repo = source_repo.scm_instance()
1794 1799 else:
1795 1800 vcs_repo = source_repo
1796 1801
1797 1802 # TODO: johbo: In the context of an update, we cannot reach
1798 1803 # the old commit anymore with our normal mechanisms. It needs
1799 1804 # some sort of special support in the vcs layer to avoid this
1800 1805 # workaround.
1801 1806 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1802 1807 vcs_repo.alias == 'git'):
1803 1808 source_commit.raw_id = safe_str(source_ref_id)
1804 1809
1805 1810 log.debug('calculating diff between '
1806 1811 'source_ref:%s and target_ref:%s for repo `%s`',
1807 1812 target_ref_id, source_ref_id,
1808 1813 safe_unicode(vcs_repo.path))
1809 1814
1810 1815 vcs_diff = vcs_repo.get_diff(
1811 1816 commit1=target_commit, commit2=source_commit,
1812 1817 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1813 1818 return vcs_diff
1814 1819
1815 1820 def _is_merge_enabled(self, pull_request):
1816 1821 return self._get_general_setting(
1817 1822 pull_request, 'rhodecode_pr_merge_enabled')
1818 1823
1819 1824 def _use_rebase_for_merging(self, pull_request):
1820 1825 repo_type = pull_request.target_repo.repo_type
1821 1826 if repo_type == 'hg':
1822 1827 return self._get_general_setting(
1823 1828 pull_request, 'rhodecode_hg_use_rebase_for_merging')
1824 1829 elif repo_type == 'git':
1825 1830 return self._get_general_setting(
1826 1831 pull_request, 'rhodecode_git_use_rebase_for_merging')
1827 1832
1828 1833 return False
1829 1834
1830 1835 def _user_name_for_merging(self, pull_request, user):
1831 1836 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
1832 1837 if env_user_name_attr and hasattr(user, env_user_name_attr):
1833 1838 user_name_attr = env_user_name_attr
1834 1839 else:
1835 1840 user_name_attr = 'short_contact'
1836 1841
1837 1842 user_name = getattr(user, user_name_attr)
1838 1843 return user_name
1839 1844
1840 1845 def _close_branch_before_merging(self, pull_request):
1841 1846 repo_type = pull_request.target_repo.repo_type
1842 1847 if repo_type == 'hg':
1843 1848 return self._get_general_setting(
1844 1849 pull_request, 'rhodecode_hg_close_branch_before_merging')
1845 1850 elif repo_type == 'git':
1846 1851 return self._get_general_setting(
1847 1852 pull_request, 'rhodecode_git_close_branch_before_merging')
1848 1853
1849 1854 return False
1850 1855
1851 1856 def _get_general_setting(self, pull_request, settings_key, default=False):
1852 1857 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1853 1858 settings = settings_model.get_general_settings()
1854 1859 return settings.get(settings_key, default)
1855 1860
1856 1861 def _log_audit_action(self, action, action_data, user, pull_request):
1857 1862 audit_logger.store(
1858 1863 action=action,
1859 1864 action_data=action_data,
1860 1865 user=user,
1861 1866 repo=pull_request.target_repo)
1862 1867
1863 1868 def get_reviewer_functions(self):
1864 1869 """
1865 1870 Fetches functions for validation and fetching default reviewers.
1866 1871 If available we use the EE package, else we fallback to CE
1867 1872 package functions
1868 1873 """
1869 1874 try:
1870 1875 from rc_reviewers.utils import get_default_reviewers_data
1871 1876 from rc_reviewers.utils import validate_default_reviewers
1872 1877 except ImportError:
1873 1878 from rhodecode.apps.repository.utils import get_default_reviewers_data
1874 1879 from rhodecode.apps.repository.utils import validate_default_reviewers
1875 1880
1876 1881 return get_default_reviewers_data, validate_default_reviewers
1877 1882
1878 1883
1879 1884 class MergeCheck(object):
1880 1885 """
1881 1886 Perform Merge Checks and returns a check object which stores information
1882 1887 about merge errors, and merge conditions
1883 1888 """
1884 1889 TODO_CHECK = 'todo'
1885 1890 PERM_CHECK = 'perm'
1886 1891 REVIEW_CHECK = 'review'
1887 1892 MERGE_CHECK = 'merge'
1888 1893 WIP_CHECK = 'wip'
1889 1894
1890 1895 def __init__(self):
1891 1896 self.review_status = None
1892 1897 self.merge_possible = None
1893 1898 self.merge_msg = ''
1894 1899 self.merge_response = None
1895 1900 self.failed = None
1896 1901 self.errors = []
1897 1902 self.error_details = OrderedDict()
1898 1903 self.source_commit = AttributeDict()
1899 1904 self.target_commit = AttributeDict()
1900 1905
1901 1906 def __repr__(self):
1902 1907 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
1903 1908 self.merge_possible, self.failed, self.errors)
1904 1909
1905 1910 def push_error(self, error_type, message, error_key, details):
1906 1911 self.failed = True
1907 1912 self.errors.append([error_type, message])
1908 1913 self.error_details[error_key] = dict(
1909 1914 details=details,
1910 1915 error_type=error_type,
1911 1916 message=message
1912 1917 )
1913 1918
1914 1919 @classmethod
1915 1920 def validate(cls, pull_request, auth_user, translator, fail_early=False,
1916 1921 force_shadow_repo_refresh=False):
1917 1922 _ = translator
1918 1923 merge_check = cls()
1919 1924
1920 1925 # title has WIP:
1921 1926 if pull_request.work_in_progress:
1922 1927 log.debug("MergeCheck: cannot merge, title has wip: marker.")
1923 1928
1924 1929 msg = _('WIP marker in title prevents from accidental merge.')
1925 1930 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
1926 1931 if fail_early:
1927 1932 return merge_check
1928 1933
1929 1934 # permissions to merge
1930 1935 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
1931 1936 if not user_allowed_to_merge:
1932 1937 log.debug("MergeCheck: cannot merge, approval is pending.")
1933 1938
1934 1939 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
1935 1940 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1936 1941 if fail_early:
1937 1942 return merge_check
1938 1943
1939 1944 # permission to merge into the target branch
1940 1945 target_commit_id = pull_request.target_ref_parts.commit_id
1941 1946 if pull_request.target_ref_parts.type == 'branch':
1942 1947 branch_name = pull_request.target_ref_parts.name
1943 1948 else:
1944 1949 # for mercurial we can always figure out the branch from the commit
1945 1950 # in case of bookmark
1946 1951 target_commit = pull_request.target_repo.get_commit(target_commit_id)
1947 1952 branch_name = target_commit.branch
1948 1953
1949 1954 rule, branch_perm = auth_user.get_rule_and_branch_permission(
1950 1955 pull_request.target_repo.repo_name, branch_name)
1951 1956 if branch_perm and branch_perm == 'branch.none':
1952 1957 msg = _('Target branch `{}` changes rejected by rule {}.').format(
1953 1958 branch_name, rule)
1954 1959 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
1955 1960 if fail_early:
1956 1961 return merge_check
1957 1962
1958 1963 # review status, must be always present
1959 1964 review_status = pull_request.calculated_review_status()
1960 1965 merge_check.review_status = review_status
1961 1966
1962 1967 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1963 1968 if not status_approved:
1964 1969 log.debug("MergeCheck: cannot merge, approval is pending.")
1965 1970
1966 1971 msg = _('Pull request reviewer approval is pending.')
1967 1972
1968 1973 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
1969 1974
1970 1975 if fail_early:
1971 1976 return merge_check
1972 1977
1973 1978 # left over TODOs
1974 1979 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
1975 1980 if todos:
1976 1981 log.debug("MergeCheck: cannot merge, {} "
1977 1982 "unresolved TODOs left.".format(len(todos)))
1978 1983
1979 1984 if len(todos) == 1:
1980 1985 msg = _('Cannot merge, {} TODO still not resolved.').format(
1981 1986 len(todos))
1982 1987 else:
1983 1988 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1984 1989 len(todos))
1985 1990
1986 1991 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1987 1992
1988 1993 if fail_early:
1989 1994 return merge_check
1990 1995
1991 1996 # merge possible, here is the filesystem simulation + shadow repo
1992 1997 merge_response, merge_status, msg = PullRequestModel().merge_status(
1993 1998 pull_request, translator=translator,
1994 1999 force_shadow_repo_refresh=force_shadow_repo_refresh)
1995 2000
1996 2001 merge_check.merge_possible = merge_status
1997 2002 merge_check.merge_msg = msg
1998 2003 merge_check.merge_response = merge_response
1999 2004
2000 2005 source_ref_id = pull_request.source_ref_parts.commit_id
2001 2006 target_ref_id = pull_request.target_ref_parts.commit_id
2002 2007
2003 2008 try:
2004 2009 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2005 2010 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2006 2011 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2007 2012 merge_check.source_commit.current_raw_id = source_commit.raw_id
2008 2013 merge_check.source_commit.previous_raw_id = source_ref_id
2009 2014
2010 2015 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2011 2016 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2012 2017 merge_check.target_commit.current_raw_id = target_commit.raw_id
2013 2018 merge_check.target_commit.previous_raw_id = target_ref_id
2014 2019 except (SourceRefMissing, TargetRefMissing):
2015 2020 pass
2016 2021
2017 2022 if not merge_status:
2018 2023 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2019 2024 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2020 2025
2021 2026 if fail_early:
2022 2027 return merge_check
2023 2028
2024 2029 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2025 2030 return merge_check
2026 2031
2027 2032 @classmethod
2028 2033 def get_merge_conditions(cls, pull_request, translator):
2029 2034 _ = translator
2030 2035 merge_details = {}
2031 2036
2032 2037 model = PullRequestModel()
2033 2038 use_rebase = model._use_rebase_for_merging(pull_request)
2034 2039
2035 2040 if use_rebase:
2036 2041 merge_details['merge_strategy'] = dict(
2037 2042 details={},
2038 2043 message=_('Merge strategy: rebase')
2039 2044 )
2040 2045 else:
2041 2046 merge_details['merge_strategy'] = dict(
2042 2047 details={},
2043 2048 message=_('Merge strategy: explicit merge commit')
2044 2049 )
2045 2050
2046 2051 close_branch = model._close_branch_before_merging(pull_request)
2047 2052 if close_branch:
2048 2053 repo_type = pull_request.target_repo.repo_type
2049 2054 close_msg = ''
2050 2055 if repo_type == 'hg':
2051 2056 close_msg = _('Source branch will be closed after merge.')
2052 2057 elif repo_type == 'git':
2053 2058 close_msg = _('Source branch will be deleted after merge.')
2054 2059
2055 2060 merge_details['close_branch'] = dict(
2056 2061 details={},
2057 2062 message=close_msg
2058 2063 )
2059 2064
2060 2065 return merge_details
2061 2066
2062 2067
2063 2068 ChangeTuple = collections.namedtuple(
2064 2069 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2065 2070
2066 2071 FileChangeTuple = collections.namedtuple(
2067 2072 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,1011 +1,1051 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27 import datetime
28 28 import ipaddress
29 29
30 30 from pyramid.threadlocal import get_current_request
31 31 from sqlalchemy.exc import DatabaseError
32 32
33 33 from rhodecode import events
34 34 from rhodecode.lib.user_log_filter import user_log_filter
35 35 from rhodecode.lib.utils2 import (
36 36 safe_unicode, get_current_rhodecode_user, action_logger_generic,
37 37 AttributeDict, str2bool)
38 38 from rhodecode.lib.exceptions import (
39 39 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError, UserOwnsArtifactsException)
40 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
41 UserOwnsPullRequestsException, UserOwnsArtifactsException)
41 42 from rhodecode.lib.caching_query import FromCache
42 43 from rhodecode.model import BaseModel
43 from rhodecode.model.auth_token import AuthTokenModel
44 44 from rhodecode.model.db import (
45 45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
46 46 UserEmailMap, UserIpMap, UserLog)
47 47 from rhodecode.model.meta import Session
48 from rhodecode.model.auth_token import AuthTokenModel
48 49 from rhodecode.model.repo_group import RepoGroupModel
49 50
50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 cls = User
56 56
57 57 def get(self, user_id, cache=False):
58 58 user = self.sa.query(User)
59 59 if cache:
60 60 user = user.options(
61 61 FromCache("sql_cache_short", "get_user_%s" % user_id))
62 62 return user.get(user_id)
63 63
64 64 def get_user(self, user):
65 65 return self._get_user(user)
66 66
67 67 def _serialize_user(self, user):
68 68 import rhodecode.lib.helpers as h
69 69
70 70 return {
71 71 'id': user.user_id,
72 72 'first_name': user.first_name,
73 73 'last_name': user.last_name,
74 74 'username': user.username,
75 75 'email': user.email,
76 76 'icon_link': h.gravatar_url(user.email, 30),
77 77 'profile_link': h.link_to_user(user),
78 78 'value_display': h.escape(h.person(user)),
79 79 'value': user.username,
80 80 'value_type': 'user',
81 81 'active': user.active,
82 82 }
83 83
84 84 def get_users(self, name_contains=None, limit=20, only_active=True):
85 85
86 86 query = self.sa.query(User)
87 87 if only_active:
88 88 query = query.filter(User.active == true())
89 89
90 90 if name_contains:
91 91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
92 92 query = query.filter(
93 93 or_(
94 94 User.name.ilike(ilike_expression),
95 95 User.lastname.ilike(ilike_expression),
96 96 User.username.ilike(ilike_expression)
97 97 )
98 98 )
99 99 query = query.limit(limit)
100 100 users = query.all()
101 101
102 102 _users = [
103 103 self._serialize_user(user) for user in users
104 104 ]
105 105 return _users
106 106
107 107 def get_by_username(self, username, cache=False, case_insensitive=False):
108 108
109 109 if case_insensitive:
110 110 user = self.sa.query(User).filter(User.username.ilike(username))
111 111 else:
112 112 user = self.sa.query(User)\
113 113 .filter(User.username == username)
114 114 if cache:
115 115 name_key = _hash_key(username)
116 116 user = user.options(
117 117 FromCache("sql_cache_short", "get_user_%s" % name_key))
118 118 return user.scalar()
119 119
120 120 def get_by_email(self, email, cache=False, case_insensitive=False):
121 121 return User.get_by_email(email, case_insensitive, cache)
122 122
123 123 def get_by_auth_token(self, auth_token, cache=False):
124 124 return User.get_by_auth_token(auth_token, cache)
125 125
126 126 def get_active_user_count(self, cache=False):
127 127 qry = User.query().filter(
128 128 User.active == true()).filter(
129 129 User.username != User.DEFAULT_USER)
130 130 if cache:
131 131 qry = qry.options(
132 132 FromCache("sql_cache_short", "get_active_users"))
133 133 return qry.count()
134 134
135 135 def create(self, form_data, cur_user=None):
136 136 if not cur_user:
137 137 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
138 138
139 139 user_data = {
140 140 'username': form_data['username'],
141 141 'password': form_data['password'],
142 142 'email': form_data['email'],
143 143 'firstname': form_data['firstname'],
144 144 'lastname': form_data['lastname'],
145 145 'active': form_data['active'],
146 146 'extern_type': form_data['extern_type'],
147 147 'extern_name': form_data['extern_name'],
148 148 'admin': False,
149 149 'cur_user': cur_user
150 150 }
151 151
152 152 if 'create_repo_group' in form_data:
153 153 user_data['create_repo_group'] = str2bool(
154 154 form_data.get('create_repo_group'))
155 155
156 156 try:
157 157 if form_data.get('password_change'):
158 158 user_data['force_password_change'] = True
159 159 return UserModel().create_or_update(**user_data)
160 160 except Exception:
161 161 log.error(traceback.format_exc())
162 162 raise
163 163
164 164 def update_user(self, user, skip_attrs=None, **kwargs):
165 165 from rhodecode.lib.auth import get_crypt_password
166 166
167 167 user = self._get_user(user)
168 168 if user.username == User.DEFAULT_USER:
169 169 raise DefaultUserException(
170 170 "You can't edit this user (`%(username)s`) since it's "
171 171 "crucial for entire application" % {
172 172 'username': user.username})
173 173
174 174 # first store only defaults
175 175 user_attrs = {
176 176 'updating_user_id': user.user_id,
177 177 'username': user.username,
178 178 'password': user.password,
179 179 'email': user.email,
180 180 'firstname': user.name,
181 181 'lastname': user.lastname,
182 182 'description': user.description,
183 183 'active': user.active,
184 184 'admin': user.admin,
185 185 'extern_name': user.extern_name,
186 186 'extern_type': user.extern_type,
187 187 'language': user.user_data.get('language')
188 188 }
189 189
190 190 # in case there's new_password, that comes from form, use it to
191 191 # store password
192 192 if kwargs.get('new_password'):
193 193 kwargs['password'] = kwargs['new_password']
194 194
195 195 # cleanups, my_account password change form
196 196 kwargs.pop('current_password', None)
197 197 kwargs.pop('new_password', None)
198 198
199 199 # cleanups, user edit password change form
200 200 kwargs.pop('password_confirmation', None)
201 201 kwargs.pop('password_change', None)
202 202
203 203 # create repo group on user creation
204 204 kwargs.pop('create_repo_group', None)
205 205
206 206 # legacy forms send name, which is the firstname
207 207 firstname = kwargs.pop('name', None)
208 208 if firstname:
209 209 kwargs['firstname'] = firstname
210 210
211 211 for k, v in kwargs.items():
212 212 # skip if we don't want to update this
213 213 if skip_attrs and k in skip_attrs:
214 214 continue
215 215
216 216 user_attrs[k] = v
217 217
218 218 try:
219 219 return self.create_or_update(**user_attrs)
220 220 except Exception:
221 221 log.error(traceback.format_exc())
222 222 raise
223 223
224 224 def create_or_update(
225 225 self, username, password, email, firstname='', lastname='',
226 226 active=True, admin=False, extern_type=None, extern_name=None,
227 227 cur_user=None, plugin=None, force_password_change=False,
228 228 allow_to_create_user=True, create_repo_group=None,
229 229 updating_user_id=None, language=None, description='',
230 230 strict_creation_check=True):
231 231 """
232 232 Creates a new instance if not found, or updates current one
233 233
234 234 :param username:
235 235 :param password:
236 236 :param email:
237 237 :param firstname:
238 238 :param lastname:
239 239 :param active:
240 240 :param admin:
241 241 :param extern_type:
242 242 :param extern_name:
243 243 :param cur_user:
244 244 :param plugin: optional plugin this method was called from
245 245 :param force_password_change: toggles new or existing user flag
246 246 for password change
247 247 :param allow_to_create_user: Defines if the method can actually create
248 248 new users
249 249 :param create_repo_group: Defines if the method should also
250 250 create an repo group with user name, and owner
251 251 :param updating_user_id: if we set it up this is the user we want to
252 252 update this allows to editing username.
253 253 :param language: language of user from interface.
254 254 :param description: user description
255 255 :param strict_creation_check: checks for allowed creation license wise etc.
256 256
257 257 :returns: new User object with injected `is_new_user` attribute.
258 258 """
259 259
260 260 if not cur_user:
261 261 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
262 262
263 263 from rhodecode.lib.auth import (
264 get_crypt_password, check_password, generate_auth_token)
264 get_crypt_password, check_password)
265 265 from rhodecode.lib.hooks_base import (
266 266 log_create_user, check_allowed_create_user)
267 267
268 268 def _password_change(new_user, password):
269 269 old_password = new_user.password or ''
270 270 # empty password
271 271 if not old_password:
272 272 return False
273 273
274 274 # password check is only needed for RhodeCode internal auth calls
275 275 # in case it's a plugin we don't care
276 276 if not plugin:
277 277
278 278 # first check if we gave crypted password back, and if it
279 279 # matches it's not password change
280 280 if new_user.password == password:
281 281 return False
282 282
283 283 password_match = check_password(password, old_password)
284 284 if not password_match:
285 285 return True
286 286
287 287 return False
288 288
289 289 # read settings on default personal repo group creation
290 290 if create_repo_group is None:
291 291 default_create_repo_group = RepoGroupModel()\
292 292 .get_default_create_personal_repo_group()
293 293 create_repo_group = default_create_repo_group
294 294
295 295 user_data = {
296 296 'username': username,
297 297 'password': password,
298 298 'email': email,
299 299 'firstname': firstname,
300 300 'lastname': lastname,
301 301 'active': active,
302 302 'admin': admin
303 303 }
304 304
305 305 if updating_user_id:
306 306 log.debug('Checking for existing account in RhodeCode '
307 307 'database with user_id `%s` ', updating_user_id)
308 308 user = User.get(updating_user_id)
309 309 else:
310 310 log.debug('Checking for existing account in RhodeCode '
311 311 'database with username `%s` ', username)
312 312 user = User.get_by_username(username, case_insensitive=True)
313 313
314 314 if user is None:
315 315 # we check internal flag if this method is actually allowed to
316 316 # create new user
317 317 if not allow_to_create_user:
318 318 msg = ('Method wants to create new user, but it is not '
319 319 'allowed to do so')
320 320 log.warning(msg)
321 321 raise NotAllowedToCreateUserError(msg)
322 322
323 323 log.debug('Creating new user %s', username)
324 324
325 325 # only if we create user that is active
326 326 new_active_user = active
327 327 if new_active_user and strict_creation_check:
328 328 # raises UserCreationError if it's not allowed for any reason to
329 329 # create new active user, this also executes pre-create hooks
330 330 check_allowed_create_user(user_data, cur_user, strict_check=True)
331 331 events.trigger(events.UserPreCreate(user_data))
332 332 new_user = User()
333 333 edit = False
334 334 else:
335 335 log.debug('updating user `%s`', username)
336 336 events.trigger(events.UserPreUpdate(user, user_data))
337 337 new_user = user
338 338 edit = True
339 339
340 340 # we're not allowed to edit default user
341 341 if user.username == User.DEFAULT_USER:
342 342 raise DefaultUserException(
343 343 "You can't edit this user (`%(username)s`) since it's "
344 344 "crucial for entire application"
345 345 % {'username': user.username})
346 346
347 347 # inject special attribute that will tell us if User is new or old
348 348 new_user.is_new_user = not edit
349 349 # for users that didn's specify auth type, we use RhodeCode built in
350 350 from rhodecode.authentication.plugins import auth_rhodecode
351 351 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
352 352 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
353 353
354 354 try:
355 355 new_user.username = username
356 356 new_user.admin = admin
357 357 new_user.email = email
358 358 new_user.active = active
359 359 new_user.extern_name = safe_unicode(extern_name)
360 360 new_user.extern_type = safe_unicode(extern_type)
361 361 new_user.name = firstname
362 362 new_user.lastname = lastname
363 363 new_user.description = description
364 364
365 365 # set password only if creating an user or password is changed
366 366 if not edit or _password_change(new_user, password):
367 367 reason = 'new password' if edit else 'new user'
368 368 log.debug('Updating password reason=>%s', reason)
369 369 new_user.password = get_crypt_password(password) if password else None
370 370
371 371 if force_password_change:
372 372 new_user.update_userdata(force_password_change=True)
373 373 if language:
374 374 new_user.update_userdata(language=language)
375 375 new_user.update_userdata(notification_status=True)
376 376
377 377 self.sa.add(new_user)
378 378
379 379 if not edit and create_repo_group:
380 380 RepoGroupModel().create_personal_repo_group(
381 381 new_user, commit_early=False)
382 382
383 383 if not edit:
384 384 # add the RSS token
385 385 self.add_auth_token(
386 386 user=username, lifetime_minutes=-1,
387 387 role=self.auth_token_role.ROLE_FEED,
388 388 description=u'Generated feed token')
389 389
390 390 kwargs = new_user.get_dict()
391 391 # backward compat, require api_keys present
392 392 kwargs['api_keys'] = kwargs['auth_tokens']
393 393 log_create_user(created_by=cur_user, **kwargs)
394 394 events.trigger(events.UserPostCreate(user_data))
395 395 return new_user
396 396 except (DatabaseError,):
397 397 log.error(traceback.format_exc())
398 398 raise
399 399
400 400 def create_registration(self, form_data,
401 401 extern_name='rhodecode', extern_type='rhodecode'):
402 402 from rhodecode.model.notification import NotificationModel
403 403 from rhodecode.model.notification import EmailNotificationModel
404 404
405 405 try:
406 406 form_data['admin'] = False
407 407 form_data['extern_name'] = extern_name
408 408 form_data['extern_type'] = extern_type
409 409 new_user = self.create(form_data)
410 410
411 411 self.sa.add(new_user)
412 412 self.sa.flush()
413 413
414 414 user_data = new_user.get_dict()
415 415 user_data.update({
416 416 'first_name': user_data.get('firstname'),
417 417 'last_name': user_data.get('lastname'),
418 418 })
419 419 kwargs = {
420 420 # use SQLALCHEMY safe dump of user data
421 421 'user': AttributeDict(user_data),
422 422 'date': datetime.datetime.now()
423 423 }
424 424 notification_type = EmailNotificationModel.TYPE_REGISTRATION
425 425 # pre-generate the subject for notification itself
426 426 (subject,
427 427 _h, _e, # we don't care about those
428 428 body_plaintext) = EmailNotificationModel().render_email(
429 429 notification_type, **kwargs)
430 430
431 431 # create notification objects, and emails
432 432 NotificationModel().create(
433 433 created_by=new_user,
434 434 notification_subject=subject,
435 435 notification_body=body_plaintext,
436 436 notification_type=notification_type,
437 437 recipients=None, # all admins
438 438 email_kwargs=kwargs,
439 439 )
440 440
441 441 return new_user
442 442 except Exception:
443 443 log.error(traceback.format_exc())
444 444 raise
445 445
446 def _handle_user_repos(self, username, repositories, handle_mode=None):
447 _superadmin = self.cls.get_first_super_admin()
446 def _handle_user_repos(self, username, repositories, handle_user,
447 handle_mode=None):
448
448 449 left_overs = True
449 450
450 451 from rhodecode.model.repo import RepoModel
451 452
452 453 if handle_mode == 'detach':
453 454 for obj in repositories:
454 obj.user = _superadmin
455 obj.user = handle_user
455 456 # set description we know why we super admin now owns
456 457 # additional repositories that were orphaned !
457 458 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
458 459 self.sa.add(obj)
459 460 left_overs = False
460 461 elif handle_mode == 'delete':
461 462 for obj in repositories:
462 463 RepoModel().delete(obj, forks='detach')
463 464 left_overs = False
464 465
465 466 # if nothing is done we have left overs left
466 467 return left_overs
467 468
468 def _handle_user_repo_groups(self, username, repository_groups,
469 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
469 470 handle_mode=None):
470 _superadmin = self.cls.get_first_super_admin()
471
471 472 left_overs = True
472 473
473 474 from rhodecode.model.repo_group import RepoGroupModel
474 475
475 476 if handle_mode == 'detach':
476 477 for r in repository_groups:
477 r.user = _superadmin
478 r.user = handle_user
478 479 # set description we know why we super admin now owns
479 480 # additional repositories that were orphaned !
480 481 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
481 482 r.personal = False
482 483 self.sa.add(r)
483 484 left_overs = False
484 485 elif handle_mode == 'delete':
485 486 for r in repository_groups:
486 487 RepoGroupModel().delete(r)
487 488 left_overs = False
488 489
489 490 # if nothing is done we have left overs left
490 491 return left_overs
491 492
492 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
493 _superadmin = self.cls.get_first_super_admin()
493 def _handle_user_user_groups(self, username, user_groups, handle_user,
494 handle_mode=None):
495
494 496 left_overs = True
495 497
496 498 from rhodecode.model.user_group import UserGroupModel
497 499
498 500 if handle_mode == 'detach':
499 501 for r in user_groups:
500 502 for user_user_group_to_perm in r.user_user_group_to_perm:
501 503 if user_user_group_to_perm.user.username == username:
502 user_user_group_to_perm.user = _superadmin
503 r.user = _superadmin
504 user_user_group_to_perm.user = handle_user
505 r.user = handle_user
504 506 # set description we know why we super admin now owns
505 507 # additional repositories that were orphaned !
506 508 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
507 509 self.sa.add(r)
508 510 left_overs = False
509 511 elif handle_mode == 'delete':
510 512 for r in user_groups:
511 513 UserGroupModel().delete(r)
512 514 left_overs = False
513 515
514 516 # if nothing is done we have left overs left
515 517 return left_overs
516 518
517 def _handle_user_artifacts(self, username, artifacts, handle_mode=None):
518 _superadmin = self.cls.get_first_super_admin()
519 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
520 handle_mode=None):
521 left_overs = True
522
523 from rhodecode.model.pull_request import PullRequestModel
524
525 if handle_mode == 'detach':
526 for pr in pull_requests:
527 pr.user_id = handle_user.user_id
528 # set description we know why we super admin now owns
529 # additional repositories that were orphaned !
530 pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
531 self.sa.add(pr)
532 left_overs = False
533 elif handle_mode == 'delete':
534 for pr in pull_requests:
535 PullRequestModel().delete(pr)
536
537 left_overs = False
538
539 # if nothing is done we have left overs left
540 return left_overs
541
542 def _handle_user_artifacts(self, username, artifacts, handle_user,
543 handle_mode=None):
544
519 545 left_overs = True
520 546
521 547 if handle_mode == 'detach':
522 548 for a in artifacts:
523 a.upload_user = _superadmin
549 a.upload_user = handle_user
524 550 # set description we know why we super admin now owns
525 551 # additional artifacts that were orphaned !
526 552 a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
527 553 self.sa.add(a)
528 554 left_overs = False
529 555 elif handle_mode == 'delete':
530 556 from rhodecode.apps.file_store import utils as store_utils
531 storage = store_utils.get_file_storage(self.request.registry.settings)
557 request = get_current_request()
558 storage = store_utils.get_file_storage(request.registry.settings)
532 559 for a in artifacts:
533 560 file_uid = a.file_uid
534 561 storage.delete(file_uid)
535 562 self.sa.delete(a)
536 563
537 564 left_overs = False
538 565
539 566 # if nothing is done we have left overs left
540 567 return left_overs
541 568
542 569 def delete(self, user, cur_user=None, handle_repos=None,
543 handle_repo_groups=None, handle_user_groups=None, handle_artifacts=None):
570 handle_repo_groups=None, handle_user_groups=None,
571 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
544 572 from rhodecode.lib.hooks_base import log_delete_user
545 573
546 574 if not cur_user:
547 575 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
576
548 577 user = self._get_user(user)
549 578
550 579 try:
551 580 if user.username == User.DEFAULT_USER:
552 581 raise DefaultUserException(
553 582 u"You can't remove this user since it's"
554 583 u" crucial for entire application")
584 handle_user = handle_new_owner or self.cls.get_first_super_admin()
585 log.debug('New detached objects owner %s', handle_user)
555 586
556 587 left_overs = self._handle_user_repos(
557 user.username, user.repositories, handle_repos)
588 user.username, user.repositories, handle_user, handle_repos)
558 589 if left_overs and user.repositories:
559 590 repos = [x.repo_name for x in user.repositories]
560 591 raise UserOwnsReposException(
561 592 u'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
562 593 u'removed. Switch owners or remove those repositories:%(list_repos)s'
563 594 % {'username': user.username, 'len_repos': len(repos),
564 595 'list_repos': ', '.join(repos)})
565 596
566 597 left_overs = self._handle_user_repo_groups(
567 user.username, user.repository_groups, handle_repo_groups)
598 user.username, user.repository_groups, handle_user, handle_repo_groups)
568 599 if left_overs and user.repository_groups:
569 600 repo_groups = [x.group_name for x in user.repository_groups]
570 601 raise UserOwnsRepoGroupsException(
571 602 u'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
572 603 u'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
573 604 % {'username': user.username, 'len_repo_groups': len(repo_groups),
574 605 'list_repo_groups': ', '.join(repo_groups)})
575 606
576 607 left_overs = self._handle_user_user_groups(
577 user.username, user.user_groups, handle_user_groups)
608 user.username, user.user_groups, handle_user, handle_user_groups)
578 609 if left_overs and user.user_groups:
579 610 user_groups = [x.users_group_name for x in user.user_groups]
580 611 raise UserOwnsUserGroupsException(
581 612 u'user "%s" still owns %s user groups and cannot be '
582 613 u'removed. Switch owners or remove those user groups:%s'
583 614 % (user.username, len(user_groups), ', '.join(user_groups)))
584 615
616 left_overs = self._handle_user_pull_requests(
617 user.username, user.user_pull_requests, handle_user, handle_pull_requests)
618 if left_overs and user.user_pull_requests:
619 pull_requests = ['!{}'.format(x.pull_request_id) for x in user.user_pull_requests]
620 raise UserOwnsPullRequestsException(
621 u'user "%s" still owns %s pull requests and cannot be '
622 u'removed. Switch owners or remove those pull requests:%s'
623 % (user.username, len(pull_requests), ', '.join(pull_requests)))
624
585 625 left_overs = self._handle_user_artifacts(
586 user.username, user.artifacts, handle_artifacts)
626 user.username, user.artifacts, handle_user, handle_artifacts)
587 627 if left_overs and user.artifacts:
588 628 artifacts = [x.file_uid for x in user.artifacts]
589 629 raise UserOwnsArtifactsException(
590 630 u'user "%s" still owns %s artifacts and cannot be '
591 631 u'removed. Switch owners or remove those artifacts:%s'
592 632 % (user.username, len(artifacts), ', '.join(artifacts)))
593 633
594 634 user_data = user.get_dict() # fetch user data before expire
595 635
596 636 # we might change the user data with detach/delete, make sure
597 637 # the object is marked as expired before actually deleting !
598 638 self.sa.expire(user)
599 639 self.sa.delete(user)
600 640
601 641 log_delete_user(deleted_by=cur_user, **user_data)
602 642 except Exception:
603 643 log.error(traceback.format_exc())
604 644 raise
605 645
606 646 def reset_password_link(self, data, pwd_reset_url):
607 647 from rhodecode.lib.celerylib import tasks, run_task
608 648 from rhodecode.model.notification import EmailNotificationModel
609 649 user_email = data['email']
610 650 try:
611 651 user = User.get_by_email(user_email)
612 652 if user:
613 653 log.debug('password reset user found %s', user)
614 654
615 655 email_kwargs = {
616 656 'password_reset_url': pwd_reset_url,
617 657 'user': user,
618 658 'email': user_email,
619 659 'date': datetime.datetime.now(),
620 660 'first_admin_email': User.get_first_super_admin().email
621 661 }
622 662
623 663 (subject, headers, email_body,
624 664 email_body_plaintext) = EmailNotificationModel().render_email(
625 665 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
626 666
627 667 recipients = [user_email]
628 668
629 669 action_logger_generic(
630 670 'sending password reset email to user: {}'.format(
631 671 user), namespace='security.password_reset')
632 672
633 673 run_task(tasks.send_email, recipients, subject,
634 674 email_body_plaintext, email_body)
635 675
636 676 else:
637 677 log.debug("password reset email %s not found", user_email)
638 678 except Exception:
639 679 log.error(traceback.format_exc())
640 680 return False
641 681
642 682 return True
643 683
644 684 def reset_password(self, data):
645 685 from rhodecode.lib.celerylib import tasks, run_task
646 686 from rhodecode.model.notification import EmailNotificationModel
647 687 from rhodecode.lib import auth
648 688 user_email = data['email']
649 689 pre_db = True
650 690 try:
651 691 user = User.get_by_email(user_email)
652 692 new_passwd = auth.PasswordGenerator().gen_password(
653 693 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
654 694 if user:
655 695 user.password = auth.get_crypt_password(new_passwd)
656 696 # also force this user to reset his password !
657 697 user.update_userdata(force_password_change=True)
658 698
659 699 Session().add(user)
660 700
661 701 # now delete the token in question
662 702 UserApiKeys = AuthTokenModel.cls
663 703 UserApiKeys().query().filter(
664 704 UserApiKeys.api_key == data['token']).delete()
665 705
666 706 Session().commit()
667 707 log.info('successfully reset password for `%s`', user_email)
668 708
669 709 if new_passwd is None:
670 710 raise Exception('unable to generate new password')
671 711
672 712 pre_db = False
673 713
674 714 email_kwargs = {
675 715 'new_password': new_passwd,
676 716 'user': user,
677 717 'email': user_email,
678 718 'date': datetime.datetime.now(),
679 719 'first_admin_email': User.get_first_super_admin().email
680 720 }
681 721
682 722 (subject, headers, email_body,
683 723 email_body_plaintext) = EmailNotificationModel().render_email(
684 724 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
685 725 **email_kwargs)
686 726
687 727 recipients = [user_email]
688 728
689 729 action_logger_generic(
690 730 'sent new password to user: {} with email: {}'.format(
691 731 user, user_email), namespace='security.password_reset')
692 732
693 733 run_task(tasks.send_email, recipients, subject,
694 734 email_body_plaintext, email_body)
695 735
696 736 except Exception:
697 737 log.error('Failed to update user password')
698 738 log.error(traceback.format_exc())
699 739 if pre_db:
700 740 # we rollback only if local db stuff fails. If it goes into
701 741 # run_task, we're pass rollback state this wouldn't work then
702 742 Session().rollback()
703 743
704 744 return True
705 745
706 746 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
707 747 """
708 748 Fetches auth_user by user_id,or api_key if present.
709 749 Fills auth_user attributes with those taken from database.
710 750 Additionally set's is_authenitated if lookup fails
711 751 present in database
712 752
713 753 :param auth_user: instance of user to set attributes
714 754 :param user_id: user id to fetch by
715 755 :param api_key: api key to fetch by
716 756 :param username: username to fetch by
717 757 """
718 758 def token_obfuscate(token):
719 759 if token:
720 760 return token[:4] + "****"
721 761
722 762 if user_id is None and api_key is None and username is None:
723 763 raise Exception('You need to pass user_id, api_key or username')
724 764
725 765 log.debug(
726 766 'AuthUser: fill data execution based on: '
727 767 'user_id:%s api_key:%s username:%s', user_id, api_key, username)
728 768 try:
729 769 dbuser = None
730 770 if user_id:
731 771 dbuser = self.get(user_id)
732 772 elif api_key:
733 773 dbuser = self.get_by_auth_token(api_key)
734 774 elif username:
735 775 dbuser = self.get_by_username(username)
736 776
737 777 if not dbuser:
738 778 log.warning(
739 779 'Unable to lookup user by id:%s api_key:%s username:%s',
740 780 user_id, token_obfuscate(api_key), username)
741 781 return False
742 782 if not dbuser.active:
743 783 log.debug('User `%s:%s` is inactive, skipping fill data',
744 784 username, user_id)
745 785 return False
746 786
747 787 log.debug('AuthUser: filling found user:%s data', dbuser)
748 788
749 789 attrs = {
750 790 'user_id': dbuser.user_id,
751 791 'username': dbuser.username,
752 792 'name': dbuser.name,
753 793 'first_name': dbuser.first_name,
754 794 'firstname': dbuser.firstname,
755 795 'last_name': dbuser.last_name,
756 796 'lastname': dbuser.lastname,
757 797 'admin': dbuser.admin,
758 798 'active': dbuser.active,
759 799
760 800 'email': dbuser.email,
761 801 'emails': dbuser.emails_cached(),
762 802 'short_contact': dbuser.short_contact,
763 803 'full_contact': dbuser.full_contact,
764 804 'full_name': dbuser.full_name,
765 805 'full_name_or_username': dbuser.full_name_or_username,
766 806
767 807 '_api_key': dbuser._api_key,
768 808 '_user_data': dbuser._user_data,
769 809
770 810 'created_on': dbuser.created_on,
771 811 'extern_name': dbuser.extern_name,
772 812 'extern_type': dbuser.extern_type,
773 813
774 814 'inherit_default_permissions': dbuser.inherit_default_permissions,
775 815
776 816 'language': dbuser.language,
777 817 'last_activity': dbuser.last_activity,
778 818 'last_login': dbuser.last_login,
779 819 'password': dbuser.password,
780 820 }
781 821 auth_user.__dict__.update(attrs)
782 822 except Exception:
783 823 log.error(traceback.format_exc())
784 824 auth_user.is_authenticated = False
785 825 return False
786 826
787 827 return True
788 828
789 829 def has_perm(self, user, perm):
790 830 perm = self._get_perm(perm)
791 831 user = self._get_user(user)
792 832
793 833 return UserToPerm.query().filter(UserToPerm.user == user)\
794 834 .filter(UserToPerm.permission == perm).scalar() is not None
795 835
796 836 def grant_perm(self, user, perm):
797 837 """
798 838 Grant user global permissions
799 839
800 840 :param user:
801 841 :param perm:
802 842 """
803 843 user = self._get_user(user)
804 844 perm = self._get_perm(perm)
805 845 # if this permission is already granted skip it
806 846 _perm = UserToPerm.query()\
807 847 .filter(UserToPerm.user == user)\
808 848 .filter(UserToPerm.permission == perm)\
809 849 .scalar()
810 850 if _perm:
811 851 return
812 852 new = UserToPerm()
813 853 new.user = user
814 854 new.permission = perm
815 855 self.sa.add(new)
816 856 return new
817 857
818 858 def revoke_perm(self, user, perm):
819 859 """
820 860 Revoke users global permissions
821 861
822 862 :param user:
823 863 :param perm:
824 864 """
825 865 user = self._get_user(user)
826 866 perm = self._get_perm(perm)
827 867
828 868 obj = UserToPerm.query()\
829 869 .filter(UserToPerm.user == user)\
830 870 .filter(UserToPerm.permission == perm)\
831 871 .scalar()
832 872 if obj:
833 873 self.sa.delete(obj)
834 874
835 875 def add_extra_email(self, user, email):
836 876 """
837 877 Adds email address to UserEmailMap
838 878
839 879 :param user:
840 880 :param email:
841 881 """
842 882
843 883 user = self._get_user(user)
844 884
845 885 obj = UserEmailMap()
846 886 obj.user = user
847 887 obj.email = email
848 888 self.sa.add(obj)
849 889 return obj
850 890
851 891 def delete_extra_email(self, user, email_id):
852 892 """
853 893 Removes email address from UserEmailMap
854 894
855 895 :param user:
856 896 :param email_id:
857 897 """
858 898 user = self._get_user(user)
859 899 obj = UserEmailMap.query().get(email_id)
860 900 if obj and obj.user_id == user.user_id:
861 901 self.sa.delete(obj)
862 902
863 903 def parse_ip_range(self, ip_range):
864 904 ip_list = []
865 905
866 906 def make_unique(value):
867 907 seen = []
868 908 return [c for c in value if not (c in seen or seen.append(c))]
869 909
870 910 # firsts split by commas
871 911 for ip_range in ip_range.split(','):
872 912 if not ip_range:
873 913 continue
874 914 ip_range = ip_range.strip()
875 915 if '-' in ip_range:
876 916 start_ip, end_ip = ip_range.split('-', 1)
877 917 start_ip = ipaddress.ip_address(safe_unicode(start_ip.strip()))
878 918 end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip()))
879 919 parsed_ip_range = []
880 920
881 for index in xrange(int(start_ip), int(end_ip) + 1):
921 for index in range(int(start_ip), int(end_ip) + 1):
882 922 new_ip = ipaddress.ip_address(index)
883 923 parsed_ip_range.append(str(new_ip))
884 924 ip_list.extend(parsed_ip_range)
885 925 else:
886 926 ip_list.append(ip_range)
887 927
888 928 return make_unique(ip_list)
889 929
890 930 def add_extra_ip(self, user, ip, description=None):
891 931 """
892 932 Adds ip address to UserIpMap
893 933
894 934 :param user:
895 935 :param ip:
896 936 """
897 937
898 938 user = self._get_user(user)
899 939 obj = UserIpMap()
900 940 obj.user = user
901 941 obj.ip_addr = ip
902 942 obj.description = description
903 943 self.sa.add(obj)
904 944 return obj
905 945
906 946 auth_token_role = AuthTokenModel.cls
907 947
908 948 def add_auth_token(self, user, lifetime_minutes, role, description=u'',
909 949 scope_callback=None):
910 950 """
911 951 Add AuthToken for user.
912 952
913 953 :param user: username/user_id
914 954 :param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
915 955 :param role: one of AuthTokenModel.cls.ROLE_*
916 956 :param description: optional string description
917 957 """
918 958
919 959 token = AuthTokenModel().create(
920 960 user, description, lifetime_minutes, role)
921 961 if scope_callback and callable(scope_callback):
922 962 # call the callback if we provide, used to attach scope for EE edition
923 963 scope_callback(token)
924 964 return token
925 965
926 966 def delete_extra_ip(self, user, ip_id):
927 967 """
928 968 Removes ip address from UserIpMap
929 969
930 970 :param user:
931 971 :param ip_id:
932 972 """
933 973 user = self._get_user(user)
934 974 obj = UserIpMap.query().get(ip_id)
935 975 if obj and obj.user_id == user.user_id:
936 976 self.sa.delete(obj)
937 977
938 978 def get_accounts_in_creation_order(self, current_user=None):
939 979 """
940 980 Get accounts in order of creation for deactivation for license limits
941 981
942 982 pick currently logged in user, and append to the list in position 0
943 983 pick all super-admins in order of creation date and add it to the list
944 984 pick all other accounts in order of creation and add it to the list.
945 985
946 986 Based on that list, the last accounts can be disabled as they are
947 987 created at the end and don't include any of the super admins as well
948 988 as the current user.
949 989
950 990 :param current_user: optionally current user running this operation
951 991 """
952 992
953 993 if not current_user:
954 994 current_user = get_current_rhodecode_user()
955 995 active_super_admins = [
956 996 x.user_id for x in User.query()
957 997 .filter(User.user_id != current_user.user_id)
958 998 .filter(User.active == true())
959 999 .filter(User.admin == true())
960 1000 .order_by(User.created_on.asc())]
961 1001
962 1002 active_regular_users = [
963 1003 x.user_id for x in User.query()
964 1004 .filter(User.user_id != current_user.user_id)
965 1005 .filter(User.active == true())
966 1006 .filter(User.admin == false())
967 1007 .order_by(User.created_on.asc())]
968 1008
969 1009 list_of_accounts = [current_user.user_id]
970 1010 list_of_accounts += active_super_admins
971 1011 list_of_accounts += active_regular_users
972 1012
973 1013 return list_of_accounts
974 1014
975 1015 def deactivate_last_users(self, expected_users, current_user=None):
976 1016 """
977 1017 Deactivate accounts that are over the license limits.
978 1018 Algorithm of which accounts to disabled is based on the formula:
979 1019
980 1020 Get current user, then super admins in creation order, then regular
981 1021 active users in creation order.
982 1022
983 1023 Using that list we mark all accounts from the end of it as inactive.
984 1024 This way we block only latest created accounts.
985 1025
986 1026 :param expected_users: list of users in special order, we deactivate
987 1027 the end N amount of users from that list
988 1028 """
989 1029
990 1030 list_of_accounts = self.get_accounts_in_creation_order(
991 1031 current_user=current_user)
992 1032
993 1033 for acc_id in list_of_accounts[expected_users + 1:]:
994 1034 user = User.get(acc_id)
995 1035 log.info('Deactivating account %s for license unlock', user)
996 1036 user.active = False
997 1037 Session().add(user)
998 1038 Session().commit()
999 1039
1000 1040 return
1001 1041
1002 1042 def get_user_log(self, user, filter_term):
1003 1043 user_log = UserLog.query()\
1004 1044 .filter(or_(UserLog.user_id == user.user_id,
1005 1045 UserLog.username == user.username))\
1006 1046 .options(joinedload(UserLog.user))\
1007 1047 .options(joinedload(UserLog.repository))\
1008 1048 .order_by(UserLog.action_date.desc())
1009 1049
1010 1050 user_log = user_log_filter(user_log, filter_term)
1011 1051 return user_log
@@ -1,198 +1,211 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <%
4 4 elems = [
5 5 (_('User ID'), c.user.user_id, '', ''),
6 6 (_('Created on'), h.format_date(c.user.created_on), '', ''),
7 7 (_('Source of Record'), c.user.extern_type, '', ''),
8 8
9 9 (_('Last login'), c.user.last_login or '-', '', ''),
10 10 (_('Last activity'), c.user.last_activity, '', ''),
11 11
12 12 (_('Repositories'), len(c.user.repositories), '', [x.repo_name for x in c.user.repositories]),
13 13 (_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]),
14 14 (_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]),
15 15
16 16 (_('Owned Artifacts'), len(c.user.artifacts), '', [x.file_uid for x in c.user.artifacts]),
17 17
18 18 (_('Reviewer of pull requests'), len(c.user.reviewer_pull_requests), '', ['Pull Request #{}'.format(x.pull_request.pull_request_id) for x in c.user.reviewer_pull_requests]),
19 19 (_('Assigned to review rules'), len(c.user_to_review_rules), '', [x for x in c.user_to_review_rules]),
20 20
21 21 (_('Member of User groups'), len(c.user.group_member), '', [x.users_group.users_group_name for x in c.user.group_member]),
22 22 (_('Force password change'), c.user.user_data.get('force_password_change', 'False'), '', ''),
23 23 ]
24 24 %>
25 25
26 26 <div class="panel panel-default">
27 27 <div class="panel-heading">
28 28 <h3 class="panel-title">
29 29 ${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')}
30 30 &nbsp;- ${_('Access Permissions')}
31 31 </h3>
32 32 </div>
33 33 <div class="panel-body">
34 34 <table class="rctable">
35 35 <tr>
36 36 <th>Name</th>
37 37 <th>Value</th>
38 38 <th>Action</th>
39 39 </tr>
40 40 % for elem in elems:
41 41 ${base.tr_info_entry(elem)}
42 42 % endfor
43 43 </table>
44 44 </div>
45 45 </div>
46 46
47 47 <div class="panel panel-default">
48 48 <div class="panel-heading">
49 49 <h3 class="panel-title">${_('Force Password Reset')}</h3>
50 50 </div>
51 51 <div class="panel-body">
52 52 ${h.secure_form(h.route_path('user_disable_force_password_reset', user_id=c.user.user_id), request=request)}
53 53 <div class="field">
54 54 <button class="btn btn-default" type="submit">
55 55 <i class="icon-unlock"></i> ${_('Disable forced password reset')}
56 56 </button>
57 57 </div>
58 58 <div class="field">
59 59 <span class="help-block">
60 60 ${_("Clear the forced password change flag.")}
61 61 </span>
62 62 </div>
63 63 ${h.end_form()}
64 64
65 65 ${h.secure_form(h.route_path('user_enable_force_password_reset', user_id=c.user.user_id), request=request)}
66 66 <div class="field">
67 67 <button class="btn btn-default" type="submit" onclick="return confirm('${_('Confirm to enable forced password change')}');">
68 68 <i class="icon-lock"></i> ${_('Enable forced password reset')}
69 69 </button>
70 70 </div>
71 71 <div class="field">
72 72 <span class="help-block">
73 73 ${_("When this is enabled user will have to change they password when they next use RhodeCode system. This will also forbid vcs operations until someone makes a password change in the web interface")}
74 74 </span>
75 75 </div>
76 76 ${h.end_form()}
77 77
78 78 </div>
79 79 </div>
80 80
81 81 <div class="panel panel-default">
82 82 <div class="panel-heading">
83 83 <h3 class="panel-title">${_('Personal Repository Group')}</h3>
84 84 </div>
85 85 <div class="panel-body">
86 86 ${h.secure_form(h.route_path('user_create_personal_repo_group', user_id=c.user.user_id), request=request)}
87 87
88 88 %if c.personal_repo_group:
89 89 <div class="panel-body-title-text">${_('Users personal repository group')} : ${h.link_to(c.personal_repo_group.group_name, h.route_path('repo_group_home', repo_group_name=c.personal_repo_group.group_name))}</div>
90 90 %else:
91 91 <div class="panel-body-title-text">
92 92 ${_('This user currently does not have a personal repository group')}
93 93 <br/>
94 94 ${_('New group will be created at: `/%(path)s`') % {'path': c.personal_repo_group_name}}
95 95 </div>
96 96 %endif
97 97 <button class="btn btn-default" type="submit" ${'disabled="disabled"' if c.personal_repo_group else ''}>
98 98 <i class="icon-repo-group"></i>
99 99 ${_('Create personal repository group')}
100 100 </button>
101 101 ${h.end_form()}
102 102 </div>
103 103 </div>
104 104
105 105
106 106 <div class="panel panel-danger">
107 107 <div class="panel-heading">
108 108 <h3 class="panel-title">${_('Delete User')}</h3>
109 109 </div>
110 110 <div class="panel-body">
111 111 ${h.secure_form(h.route_path('user_delete', user_id=c.user.user_id), request=request)}
112 112
113 113 <table class="display rctable">
114 114 <tr>
115 115 <td>
116 116 ${_ungettext('This user owns %s repository.', 'This user owns %s repositories.', len(c.user.repositories)) % len(c.user.repositories)}
117 117 </td>
118 118 <td>
119 119 <input type="radio" id="user_repos_1" name="user_repos" value="detach" checked="checked" ${'disabled=1' if len(c.user.repositories) == 0 else ''} /> <label for="user_repos_1">${_('Detach repositories')}</label>
120 120 </td>
121 121 <td>
122 122 <input type="radio" id="user_repos_2" name="user_repos" value="delete" ${'disabled=1' if len(c.user.repositories) == 0 else ''} /> <label for="user_repos_2">${_('Delete repositories')}</label>
123 123 </td>
124 124 </tr>
125 125
126 126 <tr>
127 127 <td>
128 128 ${_ungettext('This user owns %s repository group.', 'This user owns %s repository groups.', len(c.user.repository_groups)) % len(c.user.repository_groups)}
129 129 </td>
130 130 <td>
131 131 <input type="radio" id="user_repo_groups_1" name="user_repo_groups" value="detach" checked="checked" ${'disabled=1' if len(c.user.repository_groups) == 0 else ''} /> <label for="user_repo_groups_1">${_('Detach repository groups')}</label>
132 132 </td>
133 133 <td>
134 134 <input type="radio" id="user_repo_groups_2" name="user_repo_groups" value="delete" ${'disabled=1' if len(c.user.repository_groups) == 0 else ''}/> <label for="user_repo_groups_2">${_('Delete repositories')}</label>
135 135 </td>
136 136 </tr>
137 137
138 138 <tr>
139 139 <td>
140 140 ${_ungettext('This user owns %s user group.', 'This user owns %s user groups.', len(c.user.user_groups)) % len(c.user.user_groups)}
141 141 </td>
142 142 <td>
143 143 <input type="radio" id="user_user_groups_1" name="user_user_groups" value="detach" checked="checked" ${'disabled=1' if len(c.user.user_groups) == 0 else ''}/> <label for="user_user_groups_1">${_('Detach user groups')}</label>
144 144 </td>
145 145 <td>
146 146 <input type="radio" id="user_user_groups_2" name="user_user_groups" value="delete" ${'disabled=1' if len(c.user.user_groups) == 0 else ''}/> <label for="user_user_groups_2">${_('Delete repositories')}</label>
147 147 </td>
148 148 </tr>
149 149
150 150 <tr>
151 151 <td>
152 ${_ungettext('This user owns %s pull request.', 'This user owns %s pull requests.', len(c.user.user_pull_requests)) % len(c.user.user_pull_requests)}
153 </td>
154 <td>
155 <input type="radio" id="user_pull_requests_1" name="user_pull_requests" value="detach" checked="checked" ${'disabled=1' if len(c.user.user_pull_requests) == 0 else ''}/> <label for="user_pull_requests_1">${_('Detach pull requests')}</label>
156 </td>
157 <td>
158 <input type="radio" id="user_pull_requests_2" name="user_pull_requests" value="delete" ${'disabled=1' if len(c.user.user_pull_requests) == 0 else ''}/> <label for="user_pull_requests_2">${_('Delete pull requests')}</label>
159 </td>
160 </tr>
161
162 <tr>
163 <td>
152 164 ${_ungettext('This user owns %s artifact.', 'This user owns %s artifacts.', len(c.user.artifacts)) % len(c.user.artifacts)}
153 165 </td>
154 166 <td>
155 167 <input type="radio" id="user_artifacts_1" name="user_artifacts" value="detach" checked="checked" ${'disabled=1' if len(c.user.artifacts) == 0 else ''}/> <label for="user_artifacts_1">${_('Detach Artifacts')}</label>
156 168 </td>
157 169 <td>
158 170 <input type="radio" id="user_artifacts_2" name="user_artifacts" value="delete" ${'disabled=1' if len(c.user.artifacts) == 0 else ''}/> <label for="user_artifacts_2">${_('Delete Artifacts')}</label>
159 171 </td>
160 172 </tr>
161 173
162 174 </table>
163 175 <div style="margin: 0 0 20px 0" class="fake-space"></div>
164 176 <div class="pull-left">
165 177 % if len(c.user.repositories) > 0 or len(c.user.repository_groups) > 0 or len(c.user.user_groups) > 0:
166 178 % endif
167 179
168 180 <span style="padding: 0 5px 0 0">${_('New owner for detached objects')}:</span>
169 <div class="pull-right">${base.gravatar_with_user(c.first_admin.email, 16)}</div>
181 <div class="pull-right">${base.gravatar_with_user(c.detach_user.email, 16, tooltip=True)}</div>
182 <input type="hidden" name="detach_user_id" value="${c.detach_user.user_id}">
170 183 </div>
171 184 <div style="clear: both">
172 185
173 186 <div>
174 187 <p class="help-block">
175 188 ${_("When selecting the detach option, the depending objects owned by this user will be assigned to the above user.")}
176 189 <br/>
177 190 ${_("The delete option will delete the user and all his owned objects!")}
178 191 </p>
179 192 </div>
180 193
181 194 % if c.can_delete_user_message:
182 195 <p class="pre-formatting">${c.can_delete_user_message}</p>
183 196 % endif
184 197 </div>
185 198
186 199 <div style="margin: 0 0 20px 0" class="fake-space"></div>
187 200
188 201 <div class="field">
189 <button class="btn btn-small btn-danger" type="submit"
190 onclick="return confirm('${_('Confirm to delete this user: %s') % c.user.username}');"
191 ${"disabled" if not c.can_delete_user else ""}>
192 ${_('Delete this user')}
193 </button>
202 <input class="btn btn-small btn-danger" id="remove_user" name="remove_user"
203 onclick="submitConfirm(event, this, _gettext('Confirm to delete this user'), _gettext('Confirm Delete'), '${c.user.username}')"
204 ${("disabled=1" if not c.can_delete_user else "")}
205 type="submit" value="${_('Delete this user')}"
206 >
194 207 </div>
195 208
196 209 ${h.end_form()}
197 210 </div>
198 211 </div>
General Comments 0
You need to be logged in to leave comments. Login now