diff --git a/admin/nodes/urls.py b/admin/nodes/urls.py index 1d5f6e0bac9..ab96aa49fc2 100644 --- a/admin/nodes/urls.py +++ b/admin/nodes/urls.py @@ -46,4 +46,5 @@ re_path(r'^(?P[a-z0-9]+)/update_moderation_state/$', views.NodeUpdateModerationStateView.as_view(), name='node-update-mod-state'), re_path(r'^(?P[a-z0-9]+)/resync_datacite/$', views.NodeResyncDataCiteView.as_view(), name='resync-datacite'), re_path(r'^(?P[a-z0-9]+)/revert/$', views.NodeRevertToDraft.as_view(), name='revert-to-draft'), + re_path(r'^(?P[a-z0-9]+)/update_permissions/$', views.NodeUpdatePermissionsView.as_view(), name='update-permissions'), ] diff --git a/admin/nodes/views.py b/admin/nodes/views.py index db0f0119f18..9dfc147e47d 100644 --- a/admin/nodes/views.py +++ b/admin/nodes/views.py @@ -47,7 +47,7 @@ REINDEX_SHARE, REINDEX_ELASTIC, ) -from osf.utils.permissions import ADMIN +from osf.utils.permissions import ADMIN, API_CONTRIBUTOR_PERMISSIONS from scripts.approve_registrations import approve_past_pendings @@ -109,7 +109,11 @@ def get_context_data(self, **kwargs): 'SPAM_STATUS': SpamStatus, 'STORAGE_LIMITS': settings.StorageLimits, 'node': node, + # to edit contributors we should have guid as django prohibits _id usage as it starts with an underscore + 'annotated_contributors': node.contributor_set.prefetch_related('user__guids').annotate(guid=F('user__guids___id')), 'children': children, + 'permissions': API_CONTRIBUTOR_PERMISSIONS, + 'has_update_permission': node.is_admin_contributor(self.request.user) }) return context @@ -195,6 +199,75 @@ def add_contributor_removed_log(self, node, user): ).save() +class NodeUpdatePermissionsView(NodeMixin, View): + permission_required = ('osf.view_node', 'osf.change_node') + raise_exception = True + redirect_view = NodeRemoveContributorView + + def post(self, request, *args, **kwargs): + data = dict(request.POST) + contributor_id_to_remove = data.get('remove-user') + resource = self.get_object() + + if contributor_id_to_remove: + contributor_id = contributor_id_to_remove[0] + # html renders form into form incorrectly, + # so this view handles contributors deletion and permissions update + return self.redirect_view( + request=request, + kwargs={'guid': resource.guid, 'user_id': contributor_id} + ).post(request, user_id=contributor_id) + + new_emails_to_add = data.get('new-emails', []) + new_permissions_to_add = data.get('new-permissions', []) + + new_permission_indexes_to_remove = [] + for email, permission in zip(new_emails_to_add, new_permissions_to_add): + contributor_user = OSFUser.objects.filter(emails__address=email.lower()).first() + if not contributor_user: + new_permission_indexes_to_remove.append(new_emails_to_add.index(email)) + messages.error(self.request, f'Email {email} is not registered in OSF.') + continue + elif resource.is_contributor(contributor_user): + new_permission_indexes_to_remove.append(new_emails_to_add.index(email)) + messages.error(self.request, f'User with email {email} is already a contributor.') + continue + + resource.add_contributor_registered_or_not( + auth=request, + user_id=contributor_user._id, + permissions=permission, + save=True + ) + messages.success(self.request, f'User with email {email} was successfully added.') + + # should remove permissions of invalid emails because + # admin can make all existing contributors non admins + # and enter an invalid email with the only admin permission + for permission_index in new_permission_indexes_to_remove: + new_permissions_to_add.pop(permission_index) + + updated_permissions = data.get('updated-permissions', []) + all_permissions = updated_permissions + new_permissions_to_add + has_admin = list(filter(lambda permission: ADMIN in permission, all_permissions)) + if not has_admin: + messages.error(self.request, 'Must be at least one admin on this node.') + return redirect(self.get_success_url()) + + for contributor_permission in updated_permissions: + guid, permission = contributor_permission.split('-') + user = OSFUser.load(guid) + resource.update_contributor( + user, + permission, + resource.get_visible(user), + request, + save=True + ) + + return redirect(self.get_success_url()) + + class NodeDeleteView(NodeMixin, View): """ Allows authorized users to mark nodes as deleted. """ diff --git a/admin/preprints/urls.py b/admin/preprints/urls.py index 4ab9bd33939..4d9fdb0306d 100644 --- a/admin/preprints/urls.py +++ b/admin/preprints/urls.py @@ -29,4 +29,5 @@ re_path(r'^(?P\w+)/resync_crossref/$', views.PreprintResyncCrossRefView.as_view(), name='resync-crossref'), re_path(r'^(?P\w+)/make_published/$', views.PreprintMakePublishedView.as_view(), name='make-published'), re_path(r'^(?P\w+)/unwithdraw/$', views.PreprintUnwithdrawView.as_view(), name='unwithdraw'), + re_path(r'^(?P\w+)/update_permissions/$', views.PreprintUpdatePermissionsView.as_view(), name='update-permissions'), ] diff --git a/admin/preprints/views.py b/admin/preprints/views.py index ef7d1860e76..3eedceaf9e6 100644 --- a/admin/preprints/views.py +++ b/admin/preprints/views.py @@ -15,7 +15,7 @@ from admin.base.views import GuidView from admin.base.forms import GuidForm -from admin.nodes.views import NodeRemoveContributorView +from admin.nodes.views import NodeRemoveContributorView, NodeUpdatePermissionsView from admin.preprints.forms import ChangeProviderForm, MachineStateForm from api.share.utils import update_share @@ -48,6 +48,7 @@ UNFLAG_SPAM, ) from osf.utils.workflows import DefaultStates +from osf.utils.permissions import API_CONTRIBUTOR_PERMISSIONS from website import search from website.files.utils import copy_files from website.preprints.tasks import on_preprint_updated @@ -75,9 +76,13 @@ def get_context_data(self, **kwargs): preprint = self.get_object() return super().get_context_data(**{ 'preprint': preprint, + # to edit contributors we should have guid as django prohibits _id usage as it starts with an underscore + 'annotated_contributors': preprint.contributor_set.prefetch_related('user__guids').annotate(guid=F('user__guids___id')), 'SPAM_STATUS': SpamStatus, 'change_provider_form': ChangeProviderForm(instance=preprint), 'change_machine_state_form': MachineStateForm(instance=preprint), + 'permissions': API_CONTRIBUTOR_PERMISSIONS, + 'has_update_permission': preprint.is_admin_contributor(self.request.user) }, **kwargs) @@ -272,6 +277,12 @@ def add_contributor_removed_log(self, preprint, user): ).save() +class PreprintUpdatePermissionsView(PreprintMixin, NodeUpdatePermissionsView): + permission_required = ('osf.view_preprint', 'osf.change_preprint') + raise_exception = True + redirect_view = PreprintRemoveContributorView + + class PreprintDeleteView(PreprintMixin, View): """ Allows authorized users to mark preprints as deleted. """ diff --git a/admin/templates/nodes/contributors.html b/admin/templates/nodes/contributors.html index 945e0bf7578..48a30fe98a2 100644 --- a/admin/templates/nodes/contributors.html +++ b/admin/templates/nodes/contributors.html @@ -9,11 +9,7 @@ Email Name - Permissions - Actions - {% if perms.osf.change_node %} - - {% endif %} + Permission @@ -26,37 +22,11 @@ {{ user.fullname }} {% get_permissions user node %} - {% if perms.osf.change_node %} - - Remove - - - {% endif %} {% endfor %} + {% include 'nodes/edit_contributors.html' with contributors=annotated_contributors resource=node %} \ No newline at end of file diff --git a/admin/templates/nodes/edit_contributors.html b/admin/templates/nodes/edit_contributors.html new file mode 100644 index 00000000000..f976e65aed9 --- /dev/null +++ b/admin/templates/nodes/edit_contributors.html @@ -0,0 +1,137 @@ + + Edit Contributors + + + + \ No newline at end of file diff --git a/admin/templates/preprints/contributors.html b/admin/templates/preprints/contributors.html index d225994f388..a3d8697a0c6 100644 --- a/admin/templates/preprints/contributors.html +++ b/admin/templates/preprints/contributors.html @@ -8,8 +8,7 @@ Email Name - Permissions - Actions + Permission @@ -22,36 +21,10 @@ {{ user.fullname }} {% get_permissions user preprint %} - {% if perms.osf.change_preprint %} - - Remove - - - {% endif %} {% endfor %} + {% include 'nodes/edit_contributors.html' with contributors=annotated_contributors resource=preprint %} \ No newline at end of file diff --git a/admin_tests/nodes/test_views.py b/admin_tests/nodes/test_views.py index 9607a1a0c99..71ee77c4c03 100644 --- a/admin_tests/nodes/test_views.py +++ b/admin_tests/nodes/test_views.py @@ -103,6 +103,7 @@ def test_name_data(self): node = ProjectFactory() guid = node._id request = RequestFactory().get('/fake_path') + request.user = AuthUserFactory() view = NodeView() view = setup_view(view, request, guid=guid) temp_object = view.get_object()