diff --git a/rhodecode/apps/admin/tests/test_admin_users.py b/rhodecode/apps/admin/tests/test_admin_users.py --- a/rhodecode/apps/admin/tests/test_admin_users.py +++ b/rhodecode/apps/admin/tests/test_admin_users.py @@ -517,7 +517,7 @@ class TestAdminUsersView(TestController) route_path('user_delete', user_id=new_user.user_id), params={'csrf_token': self.csrf_token}) - assert_session_flash(response, 'Successfully deleted user') + assert_session_flash(response, 'Successfully deleted user `{}`'.format(username)) def test_delete_owner_of_repository(self, request, user_util): self.log_user() diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py --- a/rhodecode/apps/admin/views/users.py +++ b/rhodecode/apps/admin/views/users.py @@ -376,59 +376,69 @@ class UsersView(UserAppView): _repos = c.user.repositories _repo_groups = c.user.repository_groups _user_groups = c.user.user_groups + _artifacts = c.user.artifacts handle_repos = None handle_repo_groups = None handle_user_groups = None - # dummy call for flash of handle - set_handle_flash_repos = lambda: None - set_handle_flash_repo_groups = lambda: None - set_handle_flash_user_groups = lambda: None + handle_artifacts = None + + # calls for flash of handle based on handle case detach or delete + def set_handle_flash_repos(): + handle = handle_repos + if handle == 'detach': + h.flash(_('Detached %s repositories') % len(_repos), + category='success') + elif handle == 'delete': + h.flash(_('Deleted %s repositories') % len(_repos), + category='success') + + def set_handle_flash_repo_groups(): + handle = handle_repo_groups + if handle == 'detach': + h.flash(_('Detached %s repository groups') % len(_repo_groups), + category='success') + elif handle == 'delete': + h.flash(_('Deleted %s repository groups') % len(_repo_groups), + category='success') + + def set_handle_flash_user_groups(): + handle = handle_user_groups + if handle == 'detach': + h.flash(_('Detached %s user groups') % len(_user_groups), + category='success') + elif handle == 'delete': + h.flash(_('Deleted %s user groups') % len(_user_groups), + category='success') + + def set_handle_flash_artifacts(): + handle = handle_artifacts + if handle == 'detach': + h.flash(_('Detached %s artifacts') % len(_artifacts), + category='success') + elif handle == 'delete': + h.flash(_('Deleted %s artifacts') % len(_artifacts), + category='success') if _repos and self.request.POST.get('user_repos'): - do = self.request.POST['user_repos'] - if do == 'detach': - handle_repos = 'detach' - set_handle_flash_repos = lambda: h.flash( - _('Detached %s repositories') % len(_repos), - category='success') - elif do == 'delete': - handle_repos = 'delete' - set_handle_flash_repos = lambda: h.flash( - _('Deleted %s repositories') % len(_repos), - category='success') + handle_repos = self.request.POST['user_repos'] if _repo_groups and self.request.POST.get('user_repo_groups'): - do = self.request.POST['user_repo_groups'] - if do == 'detach': - handle_repo_groups = 'detach' - set_handle_flash_repo_groups = lambda: h.flash( - _('Detached %s repository groups') % len(_repo_groups), - category='success') - elif do == 'delete': - handle_repo_groups = 'delete' - set_handle_flash_repo_groups = lambda: h.flash( - _('Deleted %s repository groups') % len(_repo_groups), - category='success') + handle_repo_groups = self.request.POST['user_repo_groups'] if _user_groups and self.request.POST.get('user_user_groups'): - do = self.request.POST['user_user_groups'] - if do == 'detach': - handle_user_groups = 'detach' - set_handle_flash_user_groups = lambda: h.flash( - _('Detached %s user groups') % len(_user_groups), - category='success') - elif do == 'delete': - handle_user_groups = 'delete' - set_handle_flash_user_groups = lambda: h.flash( - _('Deleted %s user groups') % len(_user_groups), - category='success') + handle_user_groups = self.request.POST['user_user_groups'] + + if _artifacts and self.request.POST.get('user_artifacts'): + handle_artifacts = self.request.POST['user_artifacts'] old_values = c.user.get_api_data() + try: UserModel().delete(c.user, handle_repos=handle_repos, handle_repo_groups=handle_repo_groups, - handle_user_groups=handle_user_groups) + handle_user_groups=handle_user_groups, + handle_artifacts=handle_artifacts) audit_logger.store_web( 'user.delete', action_data={'old_data': old_values}, @@ -438,7 +448,9 @@ class UsersView(UserAppView): set_handle_flash_repos() set_handle_flash_repo_groups() set_handle_flash_user_groups() - h.flash(_('Successfully deleted user'), category='success') + set_handle_flash_artifacts() + username = h.escape(old_values['username']) + h.flash(_('Successfully deleted user `{}`').format(username), category='success') except (UserOwnsReposException, UserOwnsRepoGroupsException, UserOwnsUserGroupsException, DefaultUserException) as e: h.flash(e, category='warning') diff --git a/rhodecode/lib/exceptions.py b/rhodecode/lib/exceptions.py --- a/rhodecode/lib/exceptions.py +++ b/rhodecode/lib/exceptions.py @@ -58,6 +58,10 @@ class UserOwnsUserGroupsException(Except pass +class UserOwnsArtifactsException(Exception): + pass + + class UserGroupAssignedException(Exception): pass diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -617,13 +617,19 @@ class User(Base, BaseModel): # user pull requests user_pull_requests = relationship('PullRequest', cascade='all') # external identities - extenal_identities = relationship( + external_identities = relationship( 'ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all') # review rules user_review_rules = relationship('RepoReviewRuleUser', cascade='all') + # artifacts owned + artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id') + + # no cascade, set NULL + scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id') + def __unicode__(self): return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.user_id, self.username) @@ -1704,7 +1710,8 @@ class Repository(Base, BaseModel): scoped_tokens = relationship('UserApiKeys', cascade="all") - artifacts = relationship('FileStore', cascade="all") + # no cascade, set NULL + artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id') def __unicode__(self): return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id, @@ -2579,6 +2586,9 @@ class RepoGroup(Base, BaseModel): user = relationship('User') integrations = relationship('Integration', cascade="all, delete-orphan") + # no cascade, set NULL + scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id') + def __init__(self, group_name='', parent_group=None): self.group_name = group_name self.parent_group = parent_group @@ -3870,6 +3880,7 @@ class _SetState(object): log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state) raise + class _PullRequestBase(BaseModel): """ Common attributes of pull request and version entries. @@ -4148,14 +4159,10 @@ class PullRequest(Base, _PullRequestBase else: return '' % id(self) - reviewers = relationship('PullRequestReviewers', - cascade="all, delete-orphan") - statuses = relationship('ChangesetStatus', - cascade="all, delete-orphan") - comments = relationship('ChangesetComment', - cascade="all, delete-orphan") - versions = relationship('PullRequestVersion', - cascade="all, delete-orphan", + reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan") + statuses = relationship('ChangesetStatus', cascade="all, delete-orphan") + comments = relationship('ChangesetComment', cascade="all, delete-orphan") + versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic') @classmethod diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -37,7 +37,7 @@ from rhodecode.lib.utils2 import ( AttributeDict, str2bool) from rhodecode.lib.exceptions import ( DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, - UserOwnsUserGroupsException, NotAllowedToCreateUserError) + UserOwnsUserGroupsException, NotAllowedToCreateUserError, UserOwnsArtifactsException) from rhodecode.lib.caching_query import FromCache from rhodecode.model import BaseModel from rhodecode.model.auth_token import AuthTokenModel @@ -505,8 +505,33 @@ class UserModel(BaseModel): # if nothing is done we have left overs left return left_overs + def _handle_user_artifacts(self, username, artifacts, handle_mode=None): + _superadmin = self.cls.get_first_super_admin() + left_overs = True + + if handle_mode == 'detach': + for a in artifacts: + a.upload_user = _superadmin + # set description we know why we super admin now owns + # additional artifacts that were orphaned ! + a.file_description += ' \n::detached artifact from deleted user: %s' % (username,) + self.sa.add(a) + left_overs = False + elif handle_mode == 'delete': + from rhodecode.apps.file_store import utils as store_utils + storage = store_utils.get_file_storage(self.request.registry.settings) + for a in artifacts: + file_uid = a.file_uid + storage.delete(file_uid) + self.sa.delete(a) + + left_overs = False + + # if nothing is done we have left overs left + return left_overs + def delete(self, user, cur_user=None, handle_repos=None, - handle_repo_groups=None, handle_user_groups=None): + handle_repo_groups=None, handle_user_groups=None, handle_artifacts=None): from rhodecode.lib.hooks_base import log_delete_user if not cur_user: @@ -548,6 +573,15 @@ class UserModel(BaseModel): u'removed. Switch owners or remove those user groups:%s' % (user.username, len(user_groups), ', '.join(user_groups))) + left_overs = self._handle_user_artifacts( + user.username, user.artifacts, handle_artifacts) + if left_overs and user.artifacts: + artifacts = [x.file_uid for x in user.artifacts] + raise UserOwnsArtifactsException( + u'user "%s" still owns %s artifacts and cannot be ' + u'removed. Switch owners or remove those artifacts:%s' + % (user.username, len(artifacts), ', '.join(artifacts))) + user_data = user.get_dict() # fetch user data before expire # we might change the user data with detach/delete, make sure diff --git a/rhodecode/templates/admin/users/user_edit_advanced.mako b/rhodecode/templates/admin/users/user_edit_advanced.mako --- a/rhodecode/templates/admin/users/user_edit_advanced.mako +++ b/rhodecode/templates/admin/users/user_edit_advanced.mako @@ -13,6 +13,8 @@ (_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]), (_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]), + (_('Owned Artifacts'), len(c.user.artifacts), '', [x.file_uid for x in c.user.artifacts]), + (_('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]), (_('Assigned to review rules'), len(c.user_to_review_rules), '', [x for x in c.user_to_review_rules]), @@ -23,10 +25,19 @@
-

${_('User: %s') % c.user.username}

+

${_('User: {}').format(c.user.username)}

- ${base.dt_info_panel(elems)} + + + + + + + % for elem in elems: + ${base.tr_info_entry(elem)} + % endfor +
NameValueAction
@@ -132,6 +143,19 @@ + + + + ${_ungettext('This user owns %s artifact.', 'This user owns %s artifacts.', len(c.user.artifacts)) % len(c.user.artifacts)} + + + + + + + + +
diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako --- a/rhodecode/templates/base/base.mako +++ b/rhodecode/templates/base/base.mako @@ -164,6 +164,36 @@ +<%def name="tr_info_entry(element)"> + <% key, val, title, show_items = element %> + + + ${key} + + %if callable(val): + ## allow lazy evaluation of elements + ${val()} + %else: + ${val} + %endif + %if show_items: + + %endif + + + %if show_items: + ${_('Show More')} + %endif + + + + + <%def name="gravatar(email, size=16)"> <% if (size > 16):