Skip to content

Commit

Permalink
Merge branch 'feature/preprints-affiliations' of https://github.com/C…
Browse files Browse the repository at this point in the history
…enterForOpenScience/osf.io into preprint-insetitutions-relationship-list-api

* 'feature/preprints-affiliations' of https://github.com/CenterForOpenScience/osf.io:
  remove literal string permissions
  add docstring to explain permission
  improve preprint tests by creating 404 case
  improve permissions by dropping unsafe method behavior
  remove vestigial serializer

# Conflicts:
#	api/preprints/permissions.py
#	api/preprints/serializers.py
#	api/preprints/views.py
#	api_tests/preprints/views/test_preprint_institutions.py
  • Loading branch information
John Tordoff committed Jul 5, 2024
2 parents 07bf405 + ebea701 commit 268d5c6
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 0 deletions.
27 changes: 27 additions & 0 deletions api/preprints/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,30 @@ def has_object_permission(self, request, view, obj):
raise exceptions.PermissionDenied(detail='Withdrawn preprints may not be edited')
return True
raise exceptions.NotFound


class PreprintInstitutionPermissionList(permissions.BasePermission):
"""
Custom permission class for checking access to a list of institutions
associated with a preprint.
Permissions:
- Allows safe methods (GET, HEAD, OPTIONS) for public preprints.
- For private preprints, checks if the user has read permissions.
Methods:
- has_object_permission: Raises MethodNotAllowed for non-safe methods and
checks if the user has the necessary permissions to access private preprints.
"""
def has_object_permission(self, request, view, obj):
if request.method not in permissions.SAFE_METHODS:
raise exceptions.MethodNotAllowed(method=request.method)

if obj.is_public:
return True

auth = get_user_auth(request)
if not auth.user:
return False
else:
return obj.has_permission(auth.user, osf_permissions.READ)
1 change: 1 addition & 0 deletions api/preprints/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
re_path(r'^(?P<preprint_id>\w+)/review_actions/$', views.PreprintActionList.as_view(), name=views.PreprintActionList.view_name),
re_path(r'^(?P<preprint_id>\w+)/requests/$', views.PreprintRequestListCreate.as_view(), name=views.PreprintRequestListCreate.view_name),
re_path(r'^(?P<preprint_id>\w+)/subjects/$', views.PreprintSubjectsList.as_view(), name=views.PreprintSubjectsList.view_name),
re_path(r'^(?P<preprint_id>\w+)/institutions/$', views.PreprintInstitutionsList.as_view(), name=views.PreprintInstitutionsList.view_name),
re_path(r'^(?P<preprint_id>\w+)/relationships/institutions/$', views.PreprintInstitutionsRelationshipList.as_view(), name=views.PreprintInstitutionsRelationshipList.view_name),
]
29 changes: 29 additions & 0 deletions api/preprints/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
ReviewAction,
Preprint,
PreprintContributor,
Institution,
)
from osf.utils.requests import check_select_for_update

Expand Down Expand Up @@ -49,6 +50,7 @@

from api.identifiers.views import IdentifierList
from api.identifiers.serializers import PreprintIdentifierSerializer
from api.institutions.serializers import InstitutionSerializer
from api.nodes.views import NodeMixin, NodeContributorsList, NodeContributorDetail, NodeFilesList, NodeStorageProvidersList, NodeStorageProvider
from api.preprints.permissions import (
PreprintPublishedOrAdmin,
Expand All @@ -57,6 +59,7 @@
AdminOrPublic,
ContributorDetailPermissions,
PreprintFilesPermissions,
PreprintInstitutionPermissionList,
)
from api.nodes.permissions import ContributorOrPublic
from api.base.permissions import WriteOrPublicForRelationshipInstitutions
Expand Down Expand Up @@ -622,6 +625,32 @@ def get_queryset(self):
return self.get_queryset_from_request()


class PreprintInstitutionsList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin, PreprintMixin):
"""The documentation for this endpoint can be found [here](https://developer.osf.io/#operation/preprint_institutions_list).
"""
permission_classes = (
drf_permissions.IsAuthenticatedOrReadOnly,
base_permissions.TokenHasScope,
PreprintInstitutionPermissionList,
)

required_read_scopes = [CoreScopes.PREPRINTS_READ, CoreScopes.INSTITUTION_READ]
required_write_scopes = [CoreScopes.NULL]
serializer_class = InstitutionSerializer

model = Institution
view_category = 'preprints'
view_name = 'preprints-institutions'

ordering = ('-id',)

def get_resource(self):
return self.get_preprint()

def get_queryset(self):
return self.get_resource().affiliated_institutions.all()


class PreprintInstitutionsRelationshipList(JSONAPIBaseView, generics.RetrieveUpdateDestroyAPIView, generics.CreateAPIView, PreprintMixin):
""" """
permission_classes = (
Expand Down
162 changes: 162 additions & 0 deletions api_tests/preprints/views/test_preprint_institutions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import pytest

from api.base.settings.defaults import API_BASE
from osf_tests.factories import (
PreprintFactory,
AuthUserFactory,
InstitutionFactory,
)
from osf.utils import permissions as osf_permissions


@pytest.mark.django_db
class TestPrivatePreprintInstitutionsList:

@pytest.fixture()
def url(self, private_preprint):
return f'/{API_BASE}preprints/{private_preprint._id}/institutions/'

@pytest.fixture()
def invalid_url(self):
return f'/{API_BASE}preprints/invalid_id/institutions/'

@pytest.fixture()
def user(self):
return AuthUserFactory()

@pytest.fixture()
def private_preprint(self):
preprint = PreprintFactory()
preprint.is_public = False
preprint.save()
return preprint

@pytest.fixture()
def read_contrib(self, private_preprint):
user = AuthUserFactory()
private_preprint.add_permission(user, osf_permissions.READ)
return user

@pytest.fixture()
def write_contrib(self, private_preprint):
user = AuthUserFactory()
private_preprint.add_permission(user, osf_permissions.WRITE)
return user

@pytest.fixture()
def admin_contrib(self, private_preprint):
user = AuthUserFactory()
private_preprint.add_permission(user, osf_permissions.ADMIN)
return user

@pytest.fixture()
def institution(self):
return InstitutionFactory()

def test_preprint_institutions_no_auth(self, app, url):
res = app.get(url, expect_errors=True)
assert res.status_code == 401

def test_preprint_institutions_unauth(self, app, url, user, private_preprint):
res = app.get(url, auth=user.auth, expect_errors=True)
assert res.status_code == 403

def test_preprint_institutions_read(self, app, url, read_contrib, private_preprint, institution):

res = app.get(url, auth=read_contrib.auth)
assert res.status_code == 200
assert not res.json['data']

private_preprint.affiliated_institutions.add(institution)
res = app.get(url, auth=read_contrib.auth)
assert res.status_code == 200

assert res.json['data'][0]['id'] == institution._id
assert res.json['data'][0]['type'] == 'institutions'

def test_preprint_institutions_write(self, app, url, write_contrib, private_preprint, institution):

res = app.get(url, auth=write_contrib.auth)
assert res.status_code == 200

assert not res.json['data']

private_preprint.affiliated_institutions.add(institution)
res = app.get(url, auth=write_contrib.auth)
assert res.status_code == 200

assert res.json['data'][0]['id'] == institution._id
assert res.json['data'][0]['type'] == 'institutions'

def test_preprint_institutions_admin(self, app, url, admin_contrib, private_preprint, institution):

res = app.get(url, auth=admin_contrib.auth)
assert res.status_code == 200

assert not res.json['data']

private_preprint.affiliated_institutions.add(institution)
res = app.get(url, auth=admin_contrib.auth)
assert res.status_code == 200

assert res.json['data'][0]['id'] == institution._id
assert res.json['data'][0]['type'] == 'institutions'

def test_invalid_preprint_id(self, app, invalid_url):
res = app.get(invalid_url, expect_errors=True)
assert res.status_code == 404


@pytest.mark.django_db
class TestPublicPreprintInstitutionsList:

@pytest.fixture()
def url(self, public_preprint):
return f'/{API_BASE}preprints/{public_preprint._id}/institutions/'

@pytest.fixture()
def invalid_url(self):
return f'/{API_BASE}preprints/invalid_id/institutions/'

@pytest.fixture()
def user(self):
return AuthUserFactory()

@pytest.fixture()
def institution(self):
return InstitutionFactory()

@pytest.fixture()
def public_preprint(self):
return PreprintFactory()

@pytest.fixture()
def read_contrib(self, public_preprint):
user = AuthUserFactory()
public_preprint.add_permission(user, osf_permissions.READ)
return user

def test_preprint_institutions_no_auth(self, app, url):
res = app.get(url)
assert res.status_code == 200

def test_preprint_institutions_unauth(self, app, url, user):
res = app.get(url, auth=user.auth)
assert res.status_code == 200

def test_preprint_institutions_read(self, app, url, read_contrib, public_preprint, institution):

res = app.get(url, auth=read_contrib.auth)
assert res.status_code == 200
assert not res.json['data']

public_preprint.affiliated_institutions.add(institution)
res = app.get(url, auth=read_contrib.auth)
assert res.status_code == 200

assert res.json['data'][0]['id'] == institution._id
assert res.json['data'][0]['type'] == 'institutions'

def test_invalid_preprint_id(self, app, invalid_url):
res = app.get(invalid_url, expect_errors=True)
assert res.status_code == 404

0 comments on commit 268d5c6

Please sign in to comment.