diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8c9bd1f4..670b9bb9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,7 @@ Added - Django check for unique app setting names within each plugin (#1456) - App setting ``user_modifiable`` validation (#1536) - ``AppSettingAPI.get_all_by_scope()`` helper (#1534) + - ``removeroles`` management command (#1391) Changed ------- diff --git a/projectroles/management/commands/removeroles.py b/projectroles/management/commands/removeroles.py new file mode 100644 index 00000000..809b700e --- /dev/null +++ b/projectroles/management/commands/removeroles.py @@ -0,0 +1,117 @@ +""" +Removeroles management command for removing all roles from a user. +""" + +import sys + +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand +from django.db import transaction + +from projectroles.management.logging import ManagementCommandLogger +from projectroles.models import RoleAssignment, SODAR_CONSTANTS +from projectroles.views import RoleAssignmentDeleteMixin + +logger = ManagementCommandLogger(__name__) +User = get_user_model() + + +# SODAR constants +PROJECT_ROLE_OWNER = SODAR_CONSTANTS['PROJECT_ROLE_OWNER'] + +# Local constants +USER_NOT_FOUND_MSG = 'User not found with username: {}' + + +class Command(RoleAssignmentDeleteMixin, BaseCommand): + help = ( + 'Remove all roles from a user. Replace owner roles with given user or ' + 'parent owner.' + ) + + def add_arguments(self, parser): + parser.add_argument( + '-u', + '--user', + dest='user', + required=True, + help='User name of user whose roles will be removed', + ) + parser.add_argument( + '-o', + '--owner', + dest='owner', + required=False, + help='Set owner role for user by given user name if set, otherwise ' + 'set to parent owner', + ) + + def handle(self, *args, **options): + if options['user'] == options.get('owner'): + logger.error( + 'Same username given for both user and new owner: {}'.format( + options['user'] + ) + ) + sys.exit(1) + try: + user = User.objects.get(username=options['user']) + except User.DoesNotExist: + logger.error(USER_NOT_FOUND_MSG.format(options['user'])) + sys.exit(1) + owner_name = options.get('owner') + owner = None + if owner_name: + try: + owner = User.objects.get(username=owner_name) + except User.DoesNotExist: + logger.error(USER_NOT_FOUND_MSG.format(owner_name)) + sys.exit(1) + + logger.info('Removing roles from user "{}"..'.format(user.username)) + if owner: + logger.info( + 'New owner for replacing owner roles: {}'.format(owner.username) + ) + role_count = 0 + fail_count = 0 + roles = RoleAssignment.objects.filter(user=user).order_by( + 'project__full_title' + ) + if roles.count() == 0: + logger.info('No roles found') + return + + for role_as in roles: + r_name = role_as.role.name + project = role_as.project + p_title = project.get_log_title() + if project.is_remote(): # Skip remote projects + logger.debug('Skipping remote project: {}'.format(p_title)) + continue + # Owner role reassignment + if role_as.role.name == PROJECT_ROLE_OWNER: + # TODO: If no set owner, get parent owner + # TODO: If parent owner is not found, fail and continue + # TODO: If parent owner has existing local role, promote that + # TODO: Else add owner role + pass + # Non-owner role removal + else: + try: + with transaction.atomic(): + self.delete_assignment(role_as, None, False) + except Exception as ex: + logger.error( + 'Failed to delete assignment "{}" from {}: ' + '{}'.format(r_name, p_title, ex) + ) + fail_count += 1 + continue + logger.info('Deleted role "{}" from {}'.format(r_name, p_title)) + role_count += 1 + logger.info( + 'Removed roles from user "{}" ({} OK, {} failed)'.format( + user.username, role_count, fail_count + ) + ) diff --git a/projectroles/tests/test_commands.py b/projectroles/tests/test_commands.py index e903cf4e..0cf41009 100644 --- a/projectroles/tests/test_commands.py +++ b/projectroles/tests/test_commands.py @@ -962,6 +962,9 @@ def test_command_check(self): self.assertEqual(AppSetting.objects.count(), 8) +# TODO: Add removeroles tests + + class TestSyncGroups(TestCase): """Tests for syncgroups command""" diff --git a/projectroles/views.py b/projectroles/views.py index 2189da0e..9903679c 100644 --- a/projectroles/views.py +++ b/projectroles/views.py @@ -1894,20 +1894,28 @@ def _update_app_alerts(self, app_alerts, project, user, inh_as): project=project, ) - def delete_assignment(self, request, instance): + def delete_assignment(self, role_as, request=None, notify=True): + """ + Delete RoleAssignment. Calls the modify API for additional actions, + raises app alerts and sends email notifications about the deletion. + + :param role_as: RoleAssingment object + :param request: HttpRequest object or None + :param notify: Add app alerts and send email if True + """ app_alerts = get_backend_api('appalerts_backend') timeline = get_backend_api('timeline_backend') tl_event = None - project = instance.project - user = instance.user - role = instance.role + project = role_as.project + user = role_as.user + role = role_as.role # Init Timeline event if timeline: tl_event = timeline.add_event( project=project, app_name=APP_NAME, - user=request.user, + user=request.user if request else None, event_name='role_delete', description='delete role "{}" from {{{}}}'.format( role.name, 'user' @@ -1918,10 +1926,10 @@ def delete_assignment(self, request, instance): # Call the project plugin modify API for additional actions if getattr(settings, 'PROJECTROLES_ENABLE_MODIFY_API', False): self.call_project_modify_api( - 'perform_role_delete', 'revert_role_delete', [instance, request] + 'perform_role_delete', 'revert_role_delete', [role_as, request] ) # Delete object itself - instance.delete() + role_as.delete() # Delete corresponding PROJECT_USER settings if ( @@ -1935,23 +1943,26 @@ def delete_assignment(self, request, instance): APP_SETTING_SCOPE_PROJECT_USER, project, user ) - inh_as = project.get_role(user, inherited_only=True) if tl_event: tl_event.set_status(timeline.TL_STATUS_OK) - if app_alerts: - self._update_app_alerts(app_alerts, project, user, inh_as) - if SEND_EMAIL and app_settings.get( - APP_NAME, 'notify_email_role', user=user - ): - if inh_as: - email.send_role_change_mail( - 'update', project, user, inh_as.role, request - ) - else: - email.send_role_change_mail( - 'delete', project, user, None, request - ) - return instance + if notify: + inh_as = project.get_role(user, inherited_only=True) + if app_alerts: + self._update_app_alerts(app_alerts, project, user, inh_as) + if ( + SEND_EMAIL + and request + and app_settings.get(APP_NAME, 'notify_email_role', user=user) + ): + if inh_as: + email.send_role_change_mail( + 'update', project, user, inh_as.role, request + ) + else: + email.send_role_change_mail( + 'delete', project, user, None, request + ) + return role_as class RoleAssignmentCreateView( @@ -2104,7 +2115,7 @@ def post(self, *args, **kwargs): else: try: self.object = self.delete_assignment( - request=self.request, instance=self.object + role_as=self.object, request=self.request ) messages.success( self.request, diff --git a/projectroles/views_api.py b/projectroles/views_api.py index 38eb3e58..8186b8b1 100644 --- a/projectroles/views_api.py +++ b/projectroles/views_api.py @@ -597,7 +597,7 @@ def perform_destroy(self, instance): ) ): raise PermissionDenied('User lacks permission to assign delegates') - self.delete_assignment(request=self.request, instance=instance) + self.delete_assignment(role_as=instance, request=self.request) class RoleAssignmentOwnerTransferAPIView(