Skip to content

Commit

Permalink
Add institutional affiliation relationship to preprints
Browse files Browse the repository at this point in the history
  • Loading branch information
John Tordoff committed Jun 24, 2024
1 parent b6cecc0 commit 6d7b532
Show file tree
Hide file tree
Showing 9 changed files with 351 additions and 8 deletions.
15 changes: 15 additions & 0 deletions api/preprints/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ def has_object_permission(self, request, view, obj):
return True


class PreprintInstitutionsPermissions(permissions.BasePermission):

def has_object_permission(self, request, view, obj):
auth = get_user_auth(request)
if obj.is_public and request.method == 'GET':
return True

if not auth.user:
raise exceptions.NotAuthenticated(detail='User must has no authentication.')

if not obj.has_permission(auth.user, osf_permissions.WRITE):
raise exceptions.PermissionDenied(detail='User must have admin or write permissions to the preprint.')
return True


class ContributorDetailPermissions(PreprintPublishedOrAdmin):
"""Permissions for preprint contributor detail page."""

Expand Down
65 changes: 62 additions & 3 deletions api/preprints/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@
NodeTagField,
)
from api.base.metrics import MetricsSerializerMixin
from api.base.serializers import BaseAPISerializer
from api.taxonomies.serializers import TaxonomizableSerializerMixin
from framework.auth import Auth
from framework.exceptions import PermissionsError
from website.project import signals as project_signals
from osf.exceptions import NodeStateError
from osf.exceptions import NodeStateError, PreprintStateError
from osf.models import (
BaseFileNode,
Preprint,
Expand All @@ -48,8 +50,6 @@
)
from osf.utils import permissions as osf_permissions

from osf.exceptions import PreprintStateError


class PrimaryFileRelationshipField(RelationshipField):
def get_object(self, file_id):
Expand Down Expand Up @@ -190,6 +190,16 @@ class PreprintSerializer(TaxonomizableSerializerMixin, MetricsSerializerMixin, J
related_view_kwargs={'preprint_id': '<_id>'},
))

affiliated_institutions = RelationshipField(
related_view='preprints:preprint-institutions',
related_view_kwargs={'preprint_id': '<_id>'},
self_view='preprints:preprint-institutions',
self_view_kwargs={'preprint_id': '<_id>'},
read_only=False,
required=False,
allow_null=True,
)

links = LinksField(
{
'self': 'get_preprint_url',
Expand Down Expand Up @@ -530,3 +540,52 @@ def update(self, instance, validated_data):
links = LinksField({
'self': 'get_self_url',
})


class PreprintsInstitutionsSerializer(BaseAPISerializer):
id = IDField(read_only=True, source='_id')
name = ser.CharField(read_only=True)
type = ser.SerializerMethodField(read_only=True)

links = LinksField({
'self': 'get_self_url',
'html': 'get_related_url',
})

def get_self_url(self, obj):
return obj.absolute_api_v2_url

def get_type(self, obj):
return 'institution'

def get_related_url(self, obj):
return obj.absolute_api_v2_url + 'institutions/'

class Meta:
type_ = 'institutions'

def update(self, preprint, validated_data):
user = self.context['request'].user
try:
preprint.update_institutional_affiliation(
Auth(user),
institution_ids=[od['_id'] for od in validated_data['data']],
)
except ValidationError as e:
raise exceptions.ValidationError(list(e)[0])

preprint.save()

def create(self, validated_data):
preprint = Preprint.load(self.context['view'].kwargs['preprint_id'])
user = self.context['request'].user
data = self.context['request'].data['data']
try:
preprint.update_institutional_affiliation(
Auth(user),
institution_ids=[od['id'] for od in data],
)
except ValidationError as e:
raise exceptions.ValidationError(list(e)[0])
preprint.save()
return preprint
2 changes: 2 additions & 0 deletions api/preprints/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +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),

]
44 changes: 41 additions & 3 deletions api/preprints/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@
from api.base.views import JSONAPIBaseView, WaterButlerMixin
from api.base.filters import ListFilterMixin, PreprintFilterMixin
from api.base.parsers import (
JSONAPIOnetoOneRelationshipParser,
JSONAPIOnetoOneRelationshipParserForRegularJSON,
JSONAPIRelationshipParser,
JSONAPIRelationshipParserForRegularJSON,
JSONAPIMultipleRelationshipsParser,
JSONAPIMultipleRelationshipsParserForRegularJSON,
JSONAPIOnetoOneRelationshipParser,
JSONAPIOnetoOneRelationshipParserForRegularJSON,
)

from api.base.utils import absolute_reverse, get_user_auth, get_object_or_error
from api.base import permissions as base_permissions
from api.citations.utils import render_citation
Expand All @@ -41,6 +42,7 @@
NodeCitationStyleSerializer,
)


from api.identifiers.views import IdentifierList
from api.identifiers.serializers import PreprintIdentifierSerializer
from api.nodes.views import NodeMixin, NodeContributorsList, NodeContributorDetail, NodeFilesList, NodeStorageProvidersList, NodeStorageProvider
Expand All @@ -51,7 +53,9 @@
AdminOrPublic,
ContributorDetailPermissions,
PreprintFilesPermissions,
PreprintInstitutionsPermissions,
)
from api.preprints.serializers import PreprintsInstitutionsSerializer
from api.nodes.permissions import (
ContributorOrPublic,
)
Expand Down Expand Up @@ -614,3 +618,37 @@ def get_default_queryset(self):

def get_queryset(self):
return self.get_queryset_from_request()


class PreprintInstitutionsList(JSONAPIBaseView, generics.ListCreateAPIView, ListFilterMixin):
"""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,
PreprintInstitutionsPermissions,
)

required_read_scopes = [CoreScopes.NODE_BASE_READ]
required_write_scopes = [CoreScopes.NODE_BASE_WRITE]
serializer_class = PreprintsInstitutionsSerializer
parser_classes = (JSONAPIRelationshipParser, JSONAPIRelationshipParserForRegularJSON, )

view_category = 'preprints'
view_name = 'preprint-institutions'

def get(self, request, *args, **kwargs):
preprint = Preprint.objects.get(guids___id=self.kwargs['preprint_id'])
self.check_object_permissions(request, preprint)
return self.list(request, *args, **kwargs)

def put(self, request, *args, **kwargs):
preprint = Preprint.objects.get(guids___id=self.kwargs['preprint_id'])
self.check_object_permissions(request, preprint)
return self.create(request, *args, **kwargs)

def get_default_queryset(self):
return Preprint.objects.get(guids___id=self.kwargs['preprint_id']).affiliated_institutions.all()

def get_queryset(self):
return self.get_queryset_from_request()
171 changes: 171 additions & 0 deletions api_tests/preprints/views/test_preprint_institutions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import pytest

from api.base.settings.defaults import API_BASE
from osf_tests.factories import (
PreprintFactory,
AuthUserFactory,
InstitutionFactory,
)


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


@pytest.fixture()
def admin_with_institutional_affilation(institution, preprint):
user = AuthUserFactory()
preprint.add_permission(user, 'admin')
user.add_or_update_affiliated_institution(institution)
return user


@pytest.fixture()
def no_auth_with_institutional_affilation(institution):
user = AuthUserFactory()
user.add_or_update_affiliated_institution(institution)
return user


@pytest.fixture()
def admin_without_institutional_affilation(institution, preprint):
user = AuthUserFactory()
preprint.add_permission(user, 'admin')
return user


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


@pytest.fixture()
def preprint():
return PreprintFactory()


@pytest.fixture()
def url(preprint):
return '/{}preprints/{}/institutions/'.format(API_BASE, preprint._id)


@pytest.mark.django_db
class TestPreprintInstitutionsList:

def test_update_affiliated_institutions_add(self, app, user, admin_with_institutional_affilation, admin_without_institutional_affilation, preprint, url,
institution):
update_institutions_payload = {
'data': [{'type': 'institutions', 'id': institution._id}]
}

res = app.put_json_api(
url,
update_institutions_payload,
auth=user.auth,
expect_errors=True
)
assert res.status_code == 403

res = app.put_json_api(
url,
update_institutions_payload,
auth=admin_without_institutional_affilation.auth,
expect_errors=True
)
assert res.status_code == 400
assert res.json['errors'][0]['detail'] == f'User is not affiliated with {institution.name},'

res = app.put_json_api(
url,
update_institutions_payload,
auth=admin_with_institutional_affilation.auth
)
assert res.status_code == 201

preprint.reload()
assert institution in preprint.affiliated_institutions.all()

log = preprint.logs.latest()
assert log.action == 'affiliated_institution_added'
assert log.params['institution'] == {
'id': institution._id,
'name': institution.name
}

def test_update_affiliated_institutions_remove(self, app, user, admin_with_institutional_affilation, no_auth_with_institutional_affilation, admin_without_institutional_affilation, preprint, url,
institution):

preprint.affiliated_institutions.add(institution)
preprint.save()

update_institutions_payload = {
'data': []
}

res = app.put_json_api(
url,
update_institutions_payload,
auth=user.auth,
expect_errors=True
)
assert res.status_code == 403

res = app.put_json_api(
url,
update_institutions_payload,
auth=no_auth_with_institutional_affilation.auth,
expect_errors=True
)
assert res.status_code == 403

res = app.put_json_api(
url,
update_institutions_payload,
auth=admin_without_institutional_affilation.auth,
expect_errors=True
)
assert res.status_code == 201 # you can always remove it you are an admin

res = app.put_json_api(
url,
update_institutions_payload,
auth=admin_with_institutional_affilation.auth
)
assert res.status_code == 201

preprint.reload()
assert institution not in preprint.affiliated_institutions.all()

log = preprint.logs.latest()
assert log.action == 'affiliated_institution_removed'
assert log.params['institution'] == {
'id': institution._id,
'name': institution.name
}

def test_preprint_institutions_list_get(self, app, user, admin_with_institutional_affilation, admin_without_institutional_affilation, preprint, url,
institution):
# For testing purposes
preprint.is_public = False
preprint.save()

res = app.get(url, expect_errors=True)
assert res.status_code == 401

res = app.get(url, auth=user.auth, expect_errors=True)
assert res.status_code == 403

res = app.get(url, auth=admin_without_institutional_affilation.auth, expect_errors=True)
assert res.status_code == 200

assert res.status_code == 200
assert not res.json['data']

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

print(res.json['data'])
assert res.json['data'][0]['id'] == institution._id
assert res.json['data'][0]['type'] == 'institution'
18 changes: 18 additions & 0 deletions osf/migrations/0021_preprint_affiliated_institutions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.17 on 2024-06-21 16:25

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('osf', '0020_abstractprovider_advertise_on_discover_page'),
]

operations = [
migrations.AddField(
model_name='preprint',
name='affiliated_institutions',
field=models.ManyToManyField(related_name='preprints', to='osf.Institution'),
),
]
Loading

0 comments on commit 6d7b532

Please sign in to comment.