Skip to content

Commit

Permalink
add institution relationship view
Browse files Browse the repository at this point in the history
  • Loading branch information
John Tordoff committed Jun 21, 2024
1 parent 016482a commit 902e29e
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 21 deletions.
2 changes: 1 addition & 1 deletion api/base/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class JSONAPIParser(JSONParser):
@staticmethod
def get_relationship(data, related_resource):
target_type = data.get('type')
if not target_type:
if not target_type and data:
raise JSONAPIException(
source={'pointer': 'data/relationships/{}/data/type'.format(related_resource)},
detail=NO_TYPE_ERROR,
Expand Down
8 changes: 5 additions & 3 deletions api/preprints/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ def to_internal_value(self, license_id):

class PreprintSerializer(TaxonomizableSerializerMixin, MetricsSerializerMixin, JSONAPISerializer):
filterable_fields = frozenset([
'affiliated_institutions',
'id',
'date_created',
'date_modified',
Expand Down Expand Up @@ -191,12 +192,13 @@ class PreprintSerializer(TaxonomizableSerializerMixin, MetricsSerializerMixin, J
))

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

links = LinksField(
Expand Down Expand Up @@ -392,7 +394,7 @@ def update(self, preprint, validated_data):
if 'affiliated_institutions' in validated_data:
try:
preprint.update_institutional_affiliation(auth, validated_data['affiliated_institutions'])
except PreprintStateError as e:
except Exception as e:
raise exceptions.ValidationError(detail=str(e))

if published is not None:
Expand Down
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),

]
31 changes: 31 additions & 0 deletions api/preprints/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@
from api.base.metrics import PreprintMetricsViewMixin
from osf.metrics import PreprintDownload, PreprintView

from api.institutions.serializers import InstitutionSerializer
from osf.models import Institution


class PreprintMixin(NodeMixin):
serializer_class = PreprintSerializer
preprint_lookup_url_kwarg = 'preprint_id'
Expand Down Expand Up @@ -614,3 +618,30 @@ def get_default_queryset(self):

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/nodes_institutions_list).
"""
permission_classes = (
drf_permissions.IsAuthenticatedOrReadOnly,
base_permissions.TokenHasScope,
AdminOrPublic,
)

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

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

ordering = ('-id',)

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

def get_queryset(self):
resource = self.get_preprint()
return resource.affiliated_institutions.all() or []
80 changes: 71 additions & 9 deletions api_tests/preprints/views/test_preprint_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ def institution(self):
def url(self, preprint):
return '/{}preprints/{}/'.format(API_BASE, preprint._id)

@pytest.fixture()
def user_with_institutional_affilation(self, user, institution):
user.add_or_update_affiliated_institution(institution)
return user

@pytest.fixture()
def subject(self):
return SubjectFactory()
Expand Down Expand Up @@ -1156,7 +1161,7 @@ def test_sloan_updates(self, app, user, preprint, url):
assert preprint.has_prereg_links == 'no'
assert preprint.why_no_prereg == 'My dog ate it.'

def test_update_affiliated_institutions(self, app, user, preprint, url, institution):
def test_update_affiliated_institutions_add(self, app, user, user_with_institutional_affilation, preprint, url, institution):
update_institutions_payload = {
'data': {
'type': 'preprints',
Expand All @@ -1169,22 +1174,79 @@ def test_update_affiliated_institutions(self, app, user, preprint, url, institut
}
}

# Test with unauthorized user
unauthorized_user = AuthUserFactory()
res = app.patch_json_api(url, update_institutions_payload, auth=unauthorized_user.auth, expect_errors=True)
assert res.status_code == 403
res = app.patch_json_api(
url,
update_institutions_payload,
auth=user.auth,
expect_errors=True
)
assert res.status_code == 200

# Test with authorized user
res = app.patch_json_api(url, update_institutions_payload, auth=user.auth)
res = app.patch_json_api(
url,
update_institutions_payload,
auth=user_with_institutional_affilation.auth
)
assert res.status_code == 200

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

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

def test_update_affiliated_institutions_remove(self, app, user, user_with_institutional_affilation, preprint, url,
institution):
# First, add the institution to the preprint to ensure it exists for removal
preprint.affiliated_institutions.add(institution)
preprint.save()

update_institutions_payload = {
'data': {
'type': 'preprints',
'id': preprint._id,
'relationships': {
'affiliated_institutions': {
'type': 'institutions',
'data': [{}]
}
}
}
}

# Attempt to remove the institution with the first user and check for 200 status code
res = app.patch_json_api(
url,
update_institutions_payload,
auth=user.auth,
expect_errors=True
)
assert res.status_code == 200

# Attempt to remove the institution with the second user and check for 200 status code
res = app.patch_json_api(
url,
update_institutions_payload,
auth=user_with_institutional_affilation.auth
)
assert res.status_code == 200

# Reload preprint and check that the institution has been removed
preprint.reload()
assert institution not in preprint.affiliated_institutions.all()

# Verify that the correct log entry has been created
log = preprint.logs.latest()
assert log.action == 'affiliated_institution_removed'
assert log.params['institution'] == {
'id': institution._id,
'name': institution.name
}


@pytest.mark.django_db
class TestPreprintUpdateSubjects(UpdateSubjectsMixin):
Expand Down
14 changes: 7 additions & 7 deletions osf/models/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,9 +339,9 @@ def remove_affiliated_institution(self, inst, user, save=False, log=True):
self.save()
self.update_search()

def update_institutional_affiliation(self, institution_ids, user):

current_institutions = set(self.affiliated_institutions.values_list('id', flat=True))
def update_institutional_affiliation(self, auth, institution_ids):
user = auth.user
current_institutions = set(self.affiliated_institutions.values_list('_id', flat=True))

institutions_to_add = set(institution_ids) - current_institutions
institutions_to_remove = current_institutions - set(institution_ids)
Expand All @@ -350,18 +350,18 @@ def update_institutional_affiliation(self, institution_ids, user):

for institution_id in institutions_to_add:
try:
institution = Institution.objects.get(id=institution_id)
institution = Institution.objects.get(_id=institution_id)
self.add_affiliated_institution(institution, user, save=False, log=True)
except Institution.DoesNotExist:
raise UserNotAffiliatedError(f'User is not affiliated with {institution.name},'
raise ValidationError(f'User is not affiliated with {institution.name},'
f' it was not found in records')

for institution_id in institutions_to_remove:
try:
institution = Institution.objects.get(id=institution_id)
institution = Institution.objects.get(_id=institution_id)
self.remove_affiliated_institution(institution, user, save=False, log=True)
except Institution.DoesNotExist:
raise UserNotAffiliatedError(f'User is not affiliated with {institution.name},'
raise ValidationError(f'User is not affiliated with {institution.name},'
f' it was not found in records')

self.save()
Expand Down
2 changes: 2 additions & 0 deletions osf/models/preprintlog.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class PreprintLog(ObjectIDMixin, BaseModel):
CONFIRM_HAM = 'confirm_ham'
FLAG_SPAM = 'flag_spam'
CONFIRM_SPAM = 'confirm_spam'
AFFILIATED_INSTITUTION_ADDED = 'affiliated_institution_added'
AFFILIATED_INSTITUTION_REMOVED = 'affiliated_institution_removed'

actions = ([
DELETED,
Expand Down
2 changes: 1 addition & 1 deletion website/settings/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def parent_dir(path):
USE_EXTERNAL_EMBER = False
PROXY_EMBER_APPS = False
# http://docs.python-requests.org/en/master/user/advanced/#timeouts
EXTERNAL_EMBER_SERVER_TIMEOUT = 3.05
EXTERNAL_EMBER_SERVER_TIMEOUT = 10
EXTERNAL_EMBER_APPS = {}

LOG_PATH = os.path.join(APP_PATH, 'logs')
Expand Down

0 comments on commit 902e29e

Please sign in to comment.