Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion admin_tests/preprints/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from osf.models.spam import SpamStatus
from osf.utils.workflows import DefaultStates, RequestTypes
from osf.utils.permissions import ADMIN
from framework.auth import Auth

from admin_tests.utilities import setup_view, setup_log_view, handle_post_view_request

Expand Down
29 changes: 25 additions & 4 deletions api/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from osf import features
from packaging.version import Version
from django.apps import apps
from django.db.models import F, Max, Q, Subquery
from django.db.models import F, Max, Q, Subquery, Exists, OuterRef
from django.db import transaction
from django.utils import timezone
from django.contrib.contenttypes.models import ContentType
from rest_framework import generics, permissions as drf_permissions, exceptions
Expand Down Expand Up @@ -154,6 +155,7 @@
Folder,
CedarMetadataRecord,
Preprint, Collection,
Contributor,
)
from addons.osfstorage.models import Region
from osf.utils.permissions import ADMIN, WRITE_NODE
Expand Down Expand Up @@ -549,9 +551,28 @@ def perform_destroy(self, instance):
auth = get_user_auth(self.request)
if node.visible_contributors.count() == 1 and instance.visible:
raise ValidationError('Must have at least one visible contributor')
removed = node.remove_contributor(instance, auth)
if not removed:
raise ValidationError('Must have at least one registered admin contributor')

include_children = is_truthy(self.request.query_params.get('include_children', False))

if include_children and isinstance(node, Node):
hierarchy = Node.objects.get_children(node, active=True, include_root=True)
targets = hierarchy.filter(
Exists(
Contributor.objects.filter(
node=OuterRef('pk'),
user=instance.user,
),
),
)
with transaction.atomic():
for descendant in targets:
removed = descendant.remove_contributor(instance, auth)
if not removed:
raise ValidationError(f'{descendant._id} must have at least one registered admin contributor')
else:
removed = node.remove_contributor(instance, auth)
if not removed:
raise ValidationError('Must have at least one registered admin contributor')


class NodeImplicitContributorsList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin, NodeMixin):
Expand Down
43 changes: 43 additions & 0 deletions api_tests/nodes/views/test_node_contributors_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,46 @@ def test_can_remove_self_as_contributor_not_unique_admin(self, app, user_write_c
)
assert res.status_code == 204
assert user_write_contrib not in project.contributors

def test_remove_contributor_include_children_removes_descendants(self, app, user, user_write_contrib, project):
child1 = ProjectFactory(parent=project, creator=user)
child2 = ProjectFactory(parent=project, creator=user)
child1.add_contributor(user_write_contrib, permissions=permissions.WRITE, visible=True, save=True)
child2.add_contributor(user_write_contrib, permissions=permissions.WRITE, visible=True, save=True)

assert user_write_contrib in project.contributors
assert user_write_contrib in child1.contributors
assert user_write_contrib in child2.contributors

url = f'/{API_BASE}nodes/{project._id}/contributors/{user_write_contrib._id}/?include_children=true'
with disconnected_from_listeners(contributor_removed):
res = app.delete(url, auth=user.auth)
assert res.status_code == 204

project.reload()
child1.reload()
child2.reload()

assert user_write_contrib not in project.contributors
assert user_write_contrib not in child1.contributors
assert user_write_contrib not in child2.contributors

def test_remove_contributor_include_children_is_atomic_on_violation(self, app, user, user_write_contrib, project):
child = ProjectFactory(parent=project, creator=user)
child.add_contributor(user_write_contrib, permissions=permissions.ADMIN, visible=True, save=True)
child.set_permissions(user, permissions.READ, save=True)

assert user_write_contrib in project.contributors
assert user_write_contrib in child.contributors

url = f'/{API_BASE}nodes/{project._id}/contributors/{user_write_contrib._id}/?include_children=true'
with disconnected_from_listeners(contributor_removed):
res = app.delete(url, auth=user.auth, expect_errors=True)

assert res.status_code == 400

project.reload()
child.reload()

assert user_write_contrib in project.contributors
assert user_write_contrib in child.contributors
Loading