Show More
@@ -517,7 +517,7 b' class TestAdminUsersView(TestController)' | |||||
517 | route_path('user_delete', user_id=new_user.user_id), |
|
517 | route_path('user_delete', user_id=new_user.user_id), | |
518 | params={'csrf_token': self.csrf_token}) |
|
518 | params={'csrf_token': self.csrf_token}) | |
519 |
|
519 | |||
520 | assert_session_flash(response, 'Successfully deleted user') |
|
520 | assert_session_flash(response, 'Successfully deleted user `{}`'.format(username)) | |
521 |
|
521 | |||
522 | def test_delete_owner_of_repository(self, request, user_util): |
|
522 | def test_delete_owner_of_repository(self, request, user_util): | |
523 | self.log_user() |
|
523 | self.log_user() |
@@ -376,59 +376,69 b' class UsersView(UserAppView):' | |||||
376 | _repos = c.user.repositories |
|
376 | _repos = c.user.repositories | |
377 | _repo_groups = c.user.repository_groups |
|
377 | _repo_groups = c.user.repository_groups | |
378 | _user_groups = c.user.user_groups |
|
378 | _user_groups = c.user.user_groups | |
|
379 | _artifacts = c.user.artifacts | |||
379 |
|
380 | |||
380 | handle_repos = None |
|
381 | handle_repos = None | |
381 | handle_repo_groups = None |
|
382 | handle_repo_groups = None | |
382 | handle_user_groups = None |
|
383 | handle_user_groups = None | |
383 | # dummy call for flash of handle |
|
384 | handle_artifacts = None | |
384 | set_handle_flash_repos = lambda: None |
|
385 | ||
385 | set_handle_flash_repo_groups = lambda: None |
|
386 | # calls for flash of handle based on handle case detach or delete | |
386 |
|
|
387 | def set_handle_flash_repos(): | |
|
388 | handle = handle_repos | |||
|
389 | if handle == 'detach': | |||
|
390 | h.flash(_('Detached %s repositories') % len(_repos), | |||
|
391 | category='success') | |||
|
392 | elif handle == 'delete': | |||
|
393 | h.flash(_('Deleted %s repositories') % len(_repos), | |||
|
394 | category='success') | |||
|
395 | ||||
|
396 | def set_handle_flash_repo_groups(): | |||
|
397 | handle = handle_repo_groups | |||
|
398 | if handle == 'detach': | |||
|
399 | h.flash(_('Detached %s repository groups') % len(_repo_groups), | |||
|
400 | category='success') | |||
|
401 | elif handle == 'delete': | |||
|
402 | h.flash(_('Deleted %s repository groups') % len(_repo_groups), | |||
|
403 | category='success') | |||
|
404 | ||||
|
405 | def set_handle_flash_user_groups(): | |||
|
406 | handle = handle_user_groups | |||
|
407 | if handle == 'detach': | |||
|
408 | h.flash(_('Detached %s user groups') % len(_user_groups), | |||
|
409 | category='success') | |||
|
410 | elif handle == 'delete': | |||
|
411 | h.flash(_('Deleted %s user groups') % len(_user_groups), | |||
|
412 | category='success') | |||
|
413 | ||||
|
414 | def set_handle_flash_artifacts(): | |||
|
415 | handle = handle_artifacts | |||
|
416 | if handle == 'detach': | |||
|
417 | h.flash(_('Detached %s artifacts') % len(_artifacts), | |||
|
418 | category='success') | |||
|
419 | elif handle == 'delete': | |||
|
420 | h.flash(_('Deleted %s artifacts') % len(_artifacts), | |||
|
421 | category='success') | |||
387 |
|
422 | |||
388 | if _repos and self.request.POST.get('user_repos'): |
|
423 | if _repos and self.request.POST.get('user_repos'): | |
389 | do = self.request.POST['user_repos'] |
|
424 | handle_repos = self.request.POST['user_repos'] | |
390 | if do == 'detach': |
|
|||
391 | handle_repos = 'detach' |
|
|||
392 | set_handle_flash_repos = lambda: h.flash( |
|
|||
393 | _('Detached %s repositories') % len(_repos), |
|
|||
394 | category='success') |
|
|||
395 | elif do == 'delete': |
|
|||
396 | handle_repos = 'delete' |
|
|||
397 | set_handle_flash_repos = lambda: h.flash( |
|
|||
398 | _('Deleted %s repositories') % len(_repos), |
|
|||
399 | category='success') |
|
|||
400 |
|
425 | |||
401 | if _repo_groups and self.request.POST.get('user_repo_groups'): |
|
426 | if _repo_groups and self.request.POST.get('user_repo_groups'): | |
402 | do = self.request.POST['user_repo_groups'] |
|
427 | handle_repo_groups = self.request.POST['user_repo_groups'] | |
403 | if do == 'detach': |
|
|||
404 | handle_repo_groups = 'detach' |
|
|||
405 | set_handle_flash_repo_groups = lambda: h.flash( |
|
|||
406 | _('Detached %s repository groups') % len(_repo_groups), |
|
|||
407 | category='success') |
|
|||
408 | elif do == 'delete': |
|
|||
409 | handle_repo_groups = 'delete' |
|
|||
410 | set_handle_flash_repo_groups = lambda: h.flash( |
|
|||
411 | _('Deleted %s repository groups') % len(_repo_groups), |
|
|||
412 | category='success') |
|
|||
413 |
|
428 | |||
414 | if _user_groups and self.request.POST.get('user_user_groups'): |
|
429 | if _user_groups and self.request.POST.get('user_user_groups'): | |
415 | do = self.request.POST['user_user_groups'] |
|
430 | handle_user_groups = self.request.POST['user_user_groups'] | |
416 | if do == 'detach': |
|
431 | ||
417 | handle_user_groups = 'detach' |
|
432 | if _artifacts and self.request.POST.get('user_artifacts'): | |
418 | set_handle_flash_user_groups = lambda: h.flash( |
|
433 | handle_artifacts = self.request.POST['user_artifacts'] | |
419 | _('Detached %s user groups') % len(_user_groups), |
|
|||
420 | category='success') |
|
|||
421 | elif do == 'delete': |
|
|||
422 | handle_user_groups = 'delete' |
|
|||
423 | set_handle_flash_user_groups = lambda: h.flash( |
|
|||
424 | _('Deleted %s user groups') % len(_user_groups), |
|
|||
425 | category='success') |
|
|||
426 |
|
434 | |||
427 | old_values = c.user.get_api_data() |
|
435 | old_values = c.user.get_api_data() | |
|
436 | ||||
428 | try: |
|
437 | try: | |
429 | UserModel().delete(c.user, handle_repos=handle_repos, |
|
438 | UserModel().delete(c.user, handle_repos=handle_repos, | |
430 | handle_repo_groups=handle_repo_groups, |
|
439 | handle_repo_groups=handle_repo_groups, | |
431 |
handle_user_groups=handle_user_groups |
|
440 | handle_user_groups=handle_user_groups, | |
|
441 | handle_artifacts=handle_artifacts) | |||
432 |
|
442 | |||
433 | audit_logger.store_web( |
|
443 | audit_logger.store_web( | |
434 | 'user.delete', action_data={'old_data': old_values}, |
|
444 | 'user.delete', action_data={'old_data': old_values}, | |
@@ -438,7 +448,9 b' class UsersView(UserAppView):' | |||||
438 | set_handle_flash_repos() |
|
448 | set_handle_flash_repos() | |
439 | set_handle_flash_repo_groups() |
|
449 | set_handle_flash_repo_groups() | |
440 | set_handle_flash_user_groups() |
|
450 | set_handle_flash_user_groups() | |
441 | h.flash(_('Successfully deleted user'), category='success') |
|
451 | set_handle_flash_artifacts() | |
|
452 | username = h.escape(old_values['username']) | |||
|
453 | h.flash(_('Successfully deleted user `{}`').format(username), category='success') | |||
442 | except (UserOwnsReposException, UserOwnsRepoGroupsException, |
|
454 | except (UserOwnsReposException, UserOwnsRepoGroupsException, | |
443 | UserOwnsUserGroupsException, DefaultUserException) as e: |
|
455 | UserOwnsUserGroupsException, DefaultUserException) as e: | |
444 | h.flash(e, category='warning') |
|
456 | h.flash(e, category='warning') |
@@ -58,6 +58,10 b' class UserOwnsUserGroupsException(Except' | |||||
58 | pass |
|
58 | pass | |
59 |
|
59 | |||
60 |
|
60 | |||
|
61 | class UserOwnsArtifactsException(Exception): | |||
|
62 | pass | |||
|
63 | ||||
|
64 | ||||
61 | class UserGroupAssignedException(Exception): |
|
65 | class UserGroupAssignedException(Exception): | |
62 | pass |
|
66 | pass | |
63 |
|
67 |
@@ -617,13 +617,19 b' class User(Base, BaseModel):' | |||||
617 | # user pull requests |
|
617 | # user pull requests | |
618 | user_pull_requests = relationship('PullRequest', cascade='all') |
|
618 | user_pull_requests = relationship('PullRequest', cascade='all') | |
619 | # external identities |
|
619 | # external identities | |
620 | extenal_identities = relationship( |
|
620 | external_identities = relationship( | |
621 | 'ExternalIdentity', |
|
621 | 'ExternalIdentity', | |
622 | primaryjoin="User.user_id==ExternalIdentity.local_user_id", |
|
622 | primaryjoin="User.user_id==ExternalIdentity.local_user_id", | |
623 | cascade='all') |
|
623 | cascade='all') | |
624 | # review rules |
|
624 | # review rules | |
625 | user_review_rules = relationship('RepoReviewRuleUser', cascade='all') |
|
625 | user_review_rules = relationship('RepoReviewRuleUser', cascade='all') | |
626 |
|
626 | |||
|
627 | # artifacts owned | |||
|
628 | artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id') | |||
|
629 | ||||
|
630 | # no cascade, set NULL | |||
|
631 | scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id') | |||
|
632 | ||||
627 | def __unicode__(self): |
|
633 | def __unicode__(self): | |
628 | return u"<%s('id:%s:%s')>" % (self.__class__.__name__, |
|
634 | return u"<%s('id:%s:%s')>" % (self.__class__.__name__, | |
629 | self.user_id, self.username) |
|
635 | self.user_id, self.username) | |
@@ -1704,7 +1710,8 b' class Repository(Base, BaseModel):' | |||||
1704 |
|
1710 | |||
1705 | scoped_tokens = relationship('UserApiKeys', cascade="all") |
|
1711 | scoped_tokens = relationship('UserApiKeys', cascade="all") | |
1706 |
|
1712 | |||
1707 | artifacts = relationship('FileStore', cascade="all") |
|
1713 | # no cascade, set NULL | |
|
1714 | artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id') | |||
1708 |
|
1715 | |||
1709 | def __unicode__(self): |
|
1716 | def __unicode__(self): | |
1710 | return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id, |
|
1717 | return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id, | |
@@ -2579,6 +2586,9 b' class RepoGroup(Base, BaseModel):' | |||||
2579 | user = relationship('User') |
|
2586 | user = relationship('User') | |
2580 | integrations = relationship('Integration', cascade="all, delete-orphan") |
|
2587 | integrations = relationship('Integration', cascade="all, delete-orphan") | |
2581 |
|
2588 | |||
|
2589 | # no cascade, set NULL | |||
|
2590 | scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id') | |||
|
2591 | ||||
2582 | def __init__(self, group_name='', parent_group=None): |
|
2592 | def __init__(self, group_name='', parent_group=None): | |
2583 | self.group_name = group_name |
|
2593 | self.group_name = group_name | |
2584 | self.parent_group = parent_group |
|
2594 | self.parent_group = parent_group | |
@@ -3870,6 +3880,7 b' class _SetState(object):' | |||||
3870 | log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state) |
|
3880 | log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state) | |
3871 | raise |
|
3881 | raise | |
3872 |
|
3882 | |||
|
3883 | ||||
3873 | class _PullRequestBase(BaseModel): |
|
3884 | class _PullRequestBase(BaseModel): | |
3874 | """ |
|
3885 | """ | |
3875 | Common attributes of pull request and version entries. |
|
3886 | Common attributes of pull request and version entries. | |
@@ -4148,14 +4159,10 b' class PullRequest(Base, _PullRequestBase' | |||||
4148 | else: |
|
4159 | else: | |
4149 | return '<DB:PullRequest at %#x>' % id(self) |
|
4160 | return '<DB:PullRequest at %#x>' % id(self) | |
4150 |
|
4161 | |||
4151 | reviewers = relationship('PullRequestReviewers', |
|
4162 | reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan") | |
4152 | cascade="all, delete-orphan") |
|
4163 | statuses = relationship('ChangesetStatus', cascade="all, delete-orphan") | |
4153 | statuses = relationship('ChangesetStatus', |
|
4164 | comments = relationship('ChangesetComment', cascade="all, delete-orphan") | |
4154 | cascade="all, delete-orphan") |
|
4165 | versions = relationship('PullRequestVersion', cascade="all, delete-orphan", | |
4155 | comments = relationship('ChangesetComment', |
|
|||
4156 | cascade="all, delete-orphan") |
|
|||
4157 | versions = relationship('PullRequestVersion', |
|
|||
4158 | cascade="all, delete-orphan", |
|
|||
4159 | lazy='dynamic') |
|
4166 | lazy='dynamic') | |
4160 |
|
4167 | |||
4161 | @classmethod |
|
4168 | @classmethod |
@@ -37,7 +37,7 b' from rhodecode.lib.utils2 import (' | |||||
37 | AttributeDict, str2bool) |
|
37 | AttributeDict, str2bool) | |
38 | from rhodecode.lib.exceptions import ( |
|
38 | from rhodecode.lib.exceptions import ( | |
39 | DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, |
|
39 | DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, | |
40 | UserOwnsUserGroupsException, NotAllowedToCreateUserError) |
|
40 | UserOwnsUserGroupsException, NotAllowedToCreateUserError, UserOwnsArtifactsException) | |
41 | from rhodecode.lib.caching_query import FromCache |
|
41 | from rhodecode.lib.caching_query import FromCache | |
42 | from rhodecode.model import BaseModel |
|
42 | from rhodecode.model import BaseModel | |
43 | from rhodecode.model.auth_token import AuthTokenModel |
|
43 | from rhodecode.model.auth_token import AuthTokenModel | |
@@ -505,8 +505,33 b' class UserModel(BaseModel):' | |||||
505 | # if nothing is done we have left overs left |
|
505 | # if nothing is done we have left overs left | |
506 | return left_overs |
|
506 | return left_overs | |
507 |
|
507 | |||
|
508 | def _handle_user_artifacts(self, username, artifacts, handle_mode=None): | |||
|
509 | _superadmin = self.cls.get_first_super_admin() | |||
|
510 | left_overs = True | |||
|
511 | ||||
|
512 | if handle_mode == 'detach': | |||
|
513 | for a in artifacts: | |||
|
514 | a.upload_user = _superadmin | |||
|
515 | # set description we know why we super admin now owns | |||
|
516 | # additional artifacts that were orphaned ! | |||
|
517 | a.file_description += ' \n::detached artifact from deleted user: %s' % (username,) | |||
|
518 | self.sa.add(a) | |||
|
519 | left_overs = False | |||
|
520 | elif handle_mode == 'delete': | |||
|
521 | from rhodecode.apps.file_store import utils as store_utils | |||
|
522 | storage = store_utils.get_file_storage(self.request.registry.settings) | |||
|
523 | for a in artifacts: | |||
|
524 | file_uid = a.file_uid | |||
|
525 | storage.delete(file_uid) | |||
|
526 | self.sa.delete(a) | |||
|
527 | ||||
|
528 | left_overs = False | |||
|
529 | ||||
|
530 | # if nothing is done we have left overs left | |||
|
531 | return left_overs | |||
|
532 | ||||
508 | def delete(self, user, cur_user=None, handle_repos=None, |
|
533 | def delete(self, user, cur_user=None, handle_repos=None, | |
509 | handle_repo_groups=None, handle_user_groups=None): |
|
534 | handle_repo_groups=None, handle_user_groups=None, handle_artifacts=None): | |
510 | from rhodecode.lib.hooks_base import log_delete_user |
|
535 | from rhodecode.lib.hooks_base import log_delete_user | |
511 |
|
536 | |||
512 | if not cur_user: |
|
537 | if not cur_user: | |
@@ -548,6 +573,15 b' class UserModel(BaseModel):' | |||||
548 | u'removed. Switch owners or remove those user groups:%s' |
|
573 | u'removed. Switch owners or remove those user groups:%s' | |
549 | % (user.username, len(user_groups), ', '.join(user_groups))) |
|
574 | % (user.username, len(user_groups), ', '.join(user_groups))) | |
550 |
|
575 | |||
|
576 | left_overs = self._handle_user_artifacts( | |||
|
577 | user.username, user.artifacts, handle_artifacts) | |||
|
578 | if left_overs and user.artifacts: | |||
|
579 | artifacts = [x.file_uid for x in user.artifacts] | |||
|
580 | raise UserOwnsArtifactsException( | |||
|
581 | u'user "%s" still owns %s artifacts and cannot be ' | |||
|
582 | u'removed. Switch owners or remove those artifacts:%s' | |||
|
583 | % (user.username, len(artifacts), ', '.join(artifacts))) | |||
|
584 | ||||
551 | user_data = user.get_dict() # fetch user data before expire |
|
585 | user_data = user.get_dict() # fetch user data before expire | |
552 |
|
586 | |||
553 | # we might change the user data with detach/delete, make sure |
|
587 | # we might change the user data with detach/delete, make sure |
@@ -13,6 +13,8 b'' | |||||
13 | (_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]), |
|
13 | (_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]), | |
14 | (_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]), |
|
14 | (_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]), | |
15 |
|
15 | |||
|
16 | (_('Owned Artifacts'), len(c.user.artifacts), '', [x.file_uid for x in c.user.artifacts]), | |||
|
17 | ||||
16 | (_('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]), |
|
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]), | |
17 | (_('Assigned to review rules'), len(c.user_to_review_rules), '', [x for x in c.user_to_review_rules]), |
|
19 | (_('Assigned to review rules'), len(c.user_to_review_rules), '', [x for x in c.user_to_review_rules]), | |
18 |
|
20 | |||
@@ -23,10 +25,19 b'' | |||||
23 |
|
25 | |||
24 | <div class="panel panel-default"> |
|
26 | <div class="panel panel-default"> | |
25 | <div class="panel-heading"> |
|
27 | <div class="panel-heading"> | |
26 |
<h3 class="panel-title">${_('User: |
|
28 | <h3 class="panel-title">${_('User: {}').format(c.user.username)}</h3> | |
27 | </div> |
|
29 | </div> | |
28 | <div class="panel-body"> |
|
30 | <div class="panel-body"> | |
29 | ${base.dt_info_panel(elems)} |
|
31 | <table class="rctable"> | |
|
32 | <tr> | |||
|
33 | <th>Name</th> | |||
|
34 | <th>Value</th> | |||
|
35 | <th>Action</th> | |||
|
36 | </tr> | |||
|
37 | % for elem in elems: | |||
|
38 | ${base.tr_info_entry(elem)} | |||
|
39 | % endfor | |||
|
40 | </table> | |||
30 | </div> |
|
41 | </div> | |
31 | </div> |
|
42 | </div> | |
32 |
|
43 | |||
@@ -132,6 +143,19 b'' | |||||
132 | <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> |
|
143 | <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> | |
133 | </td> |
|
144 | </td> | |
134 | </tr> |
|
145 | </tr> | |
|
146 | ||||
|
147 | <tr> | |||
|
148 | <td> | |||
|
149 | ${_ungettext('This user owns %s artifact.', 'This user owns %s artifacts.', len(c.user.artifacts)) % len(c.user.artifacts)} | |||
|
150 | </td> | |||
|
151 | <td> | |||
|
152 | <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> | |||
|
153 | </td> | |||
|
154 | <td> | |||
|
155 | <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> | |||
|
156 | </td> | |||
|
157 | </tr> | |||
|
158 | ||||
135 | </table> |
|
159 | </table> | |
136 | <div style="margin: 0 0 20px 0" class="fake-space"></div> |
|
160 | <div style="margin: 0 0 20px 0" class="fake-space"></div> | |
137 | <div class="pull-left"> |
|
161 | <div class="pull-left"> |
@@ -164,6 +164,36 b'' | |||||
164 | </dl> |
|
164 | </dl> | |
165 | </%def> |
|
165 | </%def> | |
166 |
|
166 | |||
|
167 | <%def name="tr_info_entry(element)"> | |||
|
168 | <% key, val, title, show_items = element %> | |||
|
169 | ||||
|
170 | <tr> | |||
|
171 | <td style="vertical-align: top">${key}</td> | |||
|
172 | <td title="${h.tooltip(title)}"> | |||
|
173 | %if callable(val): | |||
|
174 | ## allow lazy evaluation of elements | |||
|
175 | ${val()} | |||
|
176 | %else: | |||
|
177 | ${val} | |||
|
178 | %endif | |||
|
179 | %if show_items: | |||
|
180 | <div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none"> | |||
|
181 | % for item in show_items: | |||
|
182 | <dt></dt> | |||
|
183 | <dd>${item}</dd> | |||
|
184 | % endfor | |||
|
185 | </div> | |||
|
186 | %endif | |||
|
187 | </td> | |||
|
188 | <td style="vertical-align: top"> | |||
|
189 | %if show_items: | |||
|
190 | <span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span> | |||
|
191 | %endif | |||
|
192 | </td> | |||
|
193 | </tr> | |||
|
194 | ||||
|
195 | </%def> | |||
|
196 | ||||
167 | <%def name="gravatar(email, size=16)"> |
|
197 | <%def name="gravatar(email, size=16)"> | |
168 | <% |
|
198 | <% | |
169 | if (size > 16): |
|
199 | if (size > 16): |
General Comments 0
You need to be logged in to leave comments.
Login now