Skip to content
Merged
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
6 changes: 3 additions & 3 deletions api/base/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,7 @@ def get_ordering(self):
return self.default_ordering

# overrides GenericAPIView
def get_queryset(self):
def get_queryset(self, *args, **kwargs):
"""
Returns non-deleted children of the current resource that the user has permission to view -
Children could be public, viewable through a view-only link (if provided), or the user
Expand All @@ -494,8 +494,8 @@ def get_queryset(self):
if self.request.query_params.get('sort', None) == '_order':
# Order by the order of the node_relations
order = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(node_pks)])
return self.get_queryset_from_request().filter(pk__in=node_pks).can_view(auth.user, auth.private_link).order_by(order)
return self.get_queryset_from_request().filter(pk__in=node_pks).can_view(auth.user, auth.private_link)
return self.get_queryset_from_request().filter(pk__in=node_pks).can_view(auth.user, auth.private_link, *args, **kwargs).order_by(order)
return self.get_queryset_from_request().filter(pk__in=node_pks).can_view(auth.user, auth.private_link, *args, **kwargs)


class BaseContributorDetail(JSONAPIBaseView, generics.RetrieveAPIView):
Expand Down
15 changes: 14 additions & 1 deletion api/nodes/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,9 +681,22 @@ def get_node_count(self, obj):
AND UG.osfuser_id = %s)
)
)
OR (
osf_abstractnode.type = 'osf.registration'
AND osf_abstractnode.moderation_state IN ('pending', 'pending_withdraw', 'embargo', 'pending_embargo_termination')
AND EXISTS (
SELECT 1
FROM auth_permission AS P2
INNER JOIN osf_abstractprovidergroupobjectpermission AS G2 ON (P2.id = G2.permission_id)
INNER JOIN osf_osfuser_groups AS UG2 ON (G2.group_id = UG2.group_id)
WHERE P2.codename = 'view_submissions'
AND G2.content_object_id = osf_abstractnode.provider_id
AND UG2.osfuser_id = %s
)
)
OR (osf_privatelink.key = %s AND osf_privatelink.is_deleted = FALSE)
);
""", [obj.id, obj.id, user_id, obj.id, user_id, auth.private_key],
""", [obj.id, obj.id, user_id, obj.id, user_id, user_id, auth.private_key],
)

return int(cursor.fetchone()[0])
Expand Down
14 changes: 14 additions & 0 deletions api/registrations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,20 @@ class RegistrationChildrenList(BaseChildrenList, generics.ListAPIView, Registrat

model_class = Registration

def get_queryset(self):
node = self.get_node()
auth = get_user_auth(self.request)
user = auth.user
provider = getattr(node, 'provider', None)
is_moderated = getattr(provider, 'is_reviewed', False)
custom_filters = {}

if is_moderated and user and user.is_authenticated and provider.is_moderator(user):
from osf.utils.workflows import RegistrationModerationStates
custom_filters['moderation_state__in'] = RegistrationModerationStates.in_moderation_states()

return super().get_queryset(**custom_filters)


class RegistrationCitationDetail(NodeCitationDetail, RegistrationMixin):
"""The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/registrations_citations_list).
Expand Down
38 changes: 34 additions & 4 deletions api_tests/registrations/views/test_registrations_childrens_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
NodeFactory,
ProjectFactory,
RegistrationFactory,
RegistrationProviderFactory,
AuthUserFactory,
PrivateLinkFactory,
)
from osf.utils.workflows import RegistrationModerationStates


@pytest.fixture()
Expand Down Expand Up @@ -69,15 +71,13 @@ def test_registrations_children_list(self, user, app, registration_with_children
assert component_two._id in ids

def test_return_registrations_list_no_auth_approved(self, user, app, registration_with_children_approved, registration_with_children_approved_url):
component_one, component_two, component_three, component_four = registration_with_children_approved.nodes

res = app.get(registration_with_children_approved_url)
ids = [node['id'] for node in res.json['data']]

assert res.status_code == 200
assert res.content_type == 'application/vnd.api+json'
assert component_one._id in ids
assert component_two._id in ids
for component in registration_with_children_approved.nodes:
assert component._id in ids

def test_registrations_list_no_auth_unapproved(self, user, app, registration_with_children, registration_with_children_url):
res = app.get(registration_with_children_url, expect_errors=True)
Expand Down Expand Up @@ -138,6 +138,36 @@ def test_registration_children_no_auth_vol(self, user, app, registration_with_ch
res = app.get(view_only_link_url, expect_errors=True)
assert res.status_code == 401

def test_registration_children_count_and_visibility_for_moderator(self, app, user):
non_contrib_moderator = AuthUserFactory()

# Setup provider and assign moderator permission
provider = RegistrationProviderFactory(reviews_workflow='pre-moderation')
provider.add_to_group(non_contrib_moderator, 'admin')
provider.save()

project = ProjectFactory(creator=user)
child = NodeFactory(parent=project, creator=user)

registration = RegistrationFactory(project=project, provider=provider)
registration.moderation_state = RegistrationModerationStates.PENDING.db_name
registration.save()

pending_child = RegistrationFactory(project=child, parent=registration, provider=provider)
pending_child.moderation_state = RegistrationModerationStates.PENDING.db_name
pending_child.save()

url = f'/v2/registrations/{registration._id}/children/'

res = app.get(url, auth=non_contrib_moderator.auth)
ids = [node['id'] for node in res.json['data']]
assert pending_child._id in ids

# Count should be 1
node_url = f'/v2/registrations/{registration._id}/?related_counts=children'
res = app.get(node_url, auth=non_contrib_moderator.auth)
assert res.json['data']['relationships']['children']['links']['related']['meta']['count'] == 1


@pytest.mark.django_db
class TestRegistrationChildrenListFiltering:
Expand Down
12 changes: 7 additions & 5 deletions osf/models/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,7 @@ def get_children(self, root, active=False, include_root=False):
row.append(root.pk)
return AbstractNode.objects.filter(id__in=row)

def can_view(self, user=None, private_link=None):
qs = self.filter(is_public=True)

def can_view(self, user=None, private_link=None, **custom_filters):
if private_link is not None:
if isinstance(private_link, PrivateLink):
private_link = private_link.key
Expand All @@ -157,9 +155,12 @@ def can_view(self, user=None, private_link=None):
return self.filter(private_links__is_deleted=False, private_links__key=private_link).filter(
is_deleted=False)

# By default, only public nodes are shown. However, custom filters can be provided.
# This is useful when you want to display a specific subset of nodes unrelated to
# the current user (e.g. only `pending` nodes for moderators).
qs = self.filter(is_public=True) if not custom_filters else self.filter(**custom_filters)
if user is not None and not isinstance(user, AnonymousUser):
read_user_query = get_objects_for_user(user, READ_NODE, self, with_superuser=False)
qs |= read_user_query
qs |= get_objects_for_user(user, READ_NODE, self, with_superuser=False)
qs |= self.extra(where=["""
"osf_abstractnode".id in (
WITH RECURSIVE implicit_read AS (
Expand All @@ -179,6 +180,7 @@ def can_view(self, user=None, private_link=None):
) SELECT * FROM implicit_read
)
"""], params=(user.id,))

return qs.filter(is_deleted=False)


Expand Down
6 changes: 6 additions & 0 deletions osf/models/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,12 @@ def validate_schema(self, schema):
if not self.schemas.filter(id=schema.id).exists():
raise ValidationError('Invalid schema for provider.')

def is_moderator(self, user):
"""Return True if the user is a moderator for this provider"""
if user and user.is_authenticated:
return user.has_perm('osf.view_submissions', self)
return False


class PreprintProvider(AbstractProvider):
"""
Expand Down
9 changes: 1 addition & 8 deletions osf/models/registrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,14 +452,7 @@ def can_view(self, auth):
if not auth or not auth.user or not self.is_moderated:
return False

moderator_viewable_states = {
RegistrationModerationStates.PENDING.db_name,
RegistrationModerationStates.PENDING_WITHDRAW.db_name,
RegistrationModerationStates.EMBARGO.db_name,
RegistrationModerationStates.PENDING_EMBARGO_TERMINATION.db_name,
}
user_is_moderator = auth.user.has_perm('view_submissions', self.provider)
if self.moderation_state in moderator_viewable_states and user_is_moderator:
if self.moderation_state in RegistrationModerationStates.in_moderation_states() and self.provider.is_moderator(auth.user):
return True

return False
Expand Down
9 changes: 9 additions & 0 deletions osf/utils/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ def from_sanction(cls, sanction):

return new_state

@classmethod
def in_moderation_states(cls):
return [
cls.PENDING.db_name,
cls.EMBARGO.db_name,
cls.PENDING_EMBARGO_TERMINATION.db_name,
cls.PENDING_WITHDRAW.db_name,
]


class RegistrationModerationTriggers(ModerationEnum):
'''The acceptable 'triggers' to describe a moderated action on a Registration.'''
Expand Down
Loading