Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENG-5845] Preprint Institutions Relationship #10659

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
14 changes: 14 additions & 0 deletions api/base/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
from framework.auth.cas import CasResponse

from osf.models import ApiOAuth2Application, ApiOAuth2PersonalToken
from osf.utils import permissions as osf_permissions
from website.util.sanitize import is_iterable_but_not_string
from api.base.utils import get_user_auth


# Implementation built on django-oauth-toolkit, but with more granular control over read+write permissions
Expand Down Expand Up @@ -158,3 +160,15 @@ def has_object_permission(self, request, view, obj):
obj = self.get_object(request, view, obj)
return super(Perm, self).has_object_permission(request, view, obj)
return Perm


class WriteOrPublicForRelationshipInstitutions(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
assert isinstance(obj, dict)
auth = get_user_auth(request)
resource = obj['self']

if request.method in permissions.SAFE_METHODS:
return resource.is_public or resource.can_view(auth)
else:
return resource.has_permission(auth.user, osf_permissions.WRITE)
2 changes: 1 addition & 1 deletion api/draft_registrations/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from api.nodes.serializers import (
DraftRegistrationLegacySerializer,
DraftRegistrationDetailLegacySerializer,
update_institutions,
get_license_details,
NodeSerializer,
NodeLicenseSerializer,
Expand All @@ -18,6 +17,7 @@
NodeContributorDetailSerializer,
RegistrationSchemaRelationshipField,
)
from api.institutions.utils import update_institutions
from api.taxonomies.serializers import TaxonomizableSerializerMixin
from osf.exceptions import DraftRegistrationStateError
from osf.models import Node
Expand Down
6 changes: 6 additions & 0 deletions api/institutions/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,9 @@ def get_absolute_url(self, obj):
'version': 'v2',
},
)


class InstitutionRelated(JSONAPIRelationshipSerializer):
id = ser.CharField(source='_id', required=False, allow_null=True)
class Meta:
type_ = 'institutions'
58 changes: 58 additions & 0 deletions api/institutions/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from rest_framework import exceptions

from api.base.serializers import relationship_diff
from osf.models import Institution
from osf.utils import permissions as osf_permissions


def get_institutions_to_add_remove(institutions, new_institutions):
diff = relationship_diff(
current_items={inst._id: inst for inst in institutions.all()},
new_items={inst['_id']: inst for inst in new_institutions},
)

insts_to_add = []
for inst_id in diff['add']:
inst = Institution.load(inst_id)
if not inst:
raise exceptions.NotFound(detail=f'Institution with id "{inst_id}" was not found')
insts_to_add.append(inst)

return insts_to_add, diff['remove'].values()


def update_institutions(resource, new_institutions, user, post=False):
add, remove = get_institutions_to_add_remove(
institutions=resource.affiliated_institutions,
new_institutions=new_institutions,
)

if not post:
for inst in remove:
if not user.is_affiliated_with_institution(inst) and not resource.has_permission(user, osf_permissions.ADMIN):
raise exceptions.PermissionDenied(detail=f'User needs to be affiliated with {inst.name}')
resource.remove_affiliated_institution(inst, user)

for inst in add:
if not user.is_affiliated_with_institution(inst):
raise exceptions.PermissionDenied(detail=f'User needs to be affiliated with {inst.name}',)
resource.add_affiliated_institution(inst, user)


def update_institutions_if_user_associated(resource, desired_institutions_data, user):
"""Update institutions only if the user is associated with the institutions. Otherwise, raise an exception."""

desired_institutions = Institution.objects.filter(_id__in=[item['_id'] for item in desired_institutions_data])

# If a user wants to affiliate with a resource check that they have it.
for inst in desired_institutions:
if user.is_affiliated_with_institution(inst):
resource.add_affiliated_institution(inst, user)
else:
raise exceptions.PermissionDenied(detail=f'User needs to be affiliated with {inst.name}')

# If a user doesn't include an affiliation they have, then remove it.
resource_institutions = resource.affiliated_institutions.all()
for inst in user.get_affiliated_institutions():
if inst in resource_institutions and inst not in desired_institutions:
resource.remove_affiliated_institution(inst, user)
12 changes: 0 additions & 12 deletions api/nodes/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,18 +295,6 @@ def has_object_permission(self, request, view, obj):
return True


class WriteOrPublicForRelationshipInstitutions(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
assert isinstance(obj, dict)
auth = get_user_auth(request)
node = obj['self']

if request.method in permissions.SAFE_METHODS:
return node.is_public or node.can_view(auth)
else:
return node.has_permission(auth.user, osf_permissions.WRITE)


class ReadOnlyIfRegistration(permissions.BasePermission):
"""Makes PUT and POST forbidden for registrations."""

Expand Down
51 changes: 5 additions & 46 deletions api/nodes/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
)
from api.base.serializers import (
VersionedDateTimeField, HideIfRegistration, IDField,
JSONAPIRelationshipSerializer,
JSONAPISerializer, LinksField,
NodeFileHyperLinkField, RelationshipField,
ShowIfVersion, TargetTypeField, TypeField,
WaterbutlerLink, relationship_diff, BaseAPISerializer,
WaterbutlerLink, BaseAPISerializer,
HideIfWikiDisabled, ShowIfAdminScopeOrAnonymous,
ValuesListField, TargetField,
)
Expand All @@ -21,6 +20,7 @@
get_user_auth, is_truthy,
)
from api.base.versioning import get_kebab_snake_case_field
from api.institutions.utils import update_institutions
from api.taxonomies.serializers import TaxonomizableSerializerMixin
from django.apps import apps
from django.conf import settings
Expand All @@ -34,7 +34,7 @@
from addons.osfstorage.models import Region
from osf.exceptions import NodeStateError
from osf.models import (
Comment, DraftRegistration, ExternalAccount, Institution,
Comment, DraftRegistration, ExternalAccount,
RegistrationSchema, AbstractNode, PrivateLink, Preprint,
RegistrationProvider, OSFGroup, NodeLicense, DraftNode,
Registration, Node,
Expand All @@ -52,44 +52,6 @@ def to_internal_value(self, data):
return self.get_object(data)


def get_institutions_to_add_remove(institutions, new_institutions):
diff = relationship_diff(
current_items={inst._id: inst for inst in institutions.all()},
new_items={inst['_id']: inst for inst in new_institutions},
)

insts_to_add = []
for inst_id in diff['add']:
inst = Institution.load(inst_id)
if not inst:
raise exceptions.NotFound(detail='Institution with id "{}" was not found'.format(inst_id))
insts_to_add.append(inst)

return insts_to_add, diff['remove'].values()


def update_institutions(node, new_institutions, user, post=False):
add, remove = get_institutions_to_add_remove(
institutions=node.affiliated_institutions,
new_institutions=new_institutions,
)

if not post:
for inst in remove:
if not user.is_affiliated_with_institution(inst) and not node.has_permission(user, osf_permissions.ADMIN):
raise exceptions.PermissionDenied(
detail='User needs to be affiliated with {}'.format(inst.name),
)
node.remove_affiliated_institution(inst, user)

for inst in add:
if not user.is_affiliated_with_institution(inst):
raise exceptions.PermissionDenied(
detail='User needs to be affiliated with {}'.format(inst.name),
)
node.add_affiliated_institution(inst, user)


class RegionRelationshipField(RelationshipField):

def to_internal_value(self, data):
Expand Down Expand Up @@ -1467,13 +1429,10 @@ def get_storage_addons_url(self, obj):
},
)

class InstitutionRelated(JSONAPIRelationshipSerializer):
id = ser.CharField(source='_id', required=False, allow_null=True)
class Meta:
type_ = 'institutions'


class NodeInstitutionsRelationshipSerializer(BaseAPISerializer):
from api.institutions.serializers import InstitutionRelated # Avoid circular import

data = ser.ListField(child=InstitutionRelated())
links = LinksField({
'self': 'get_self_url',
Expand Down
2 changes: 1 addition & 1 deletion api/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
WaterButlerMixin,
)
from api.base.waffle_decorators import require_flag
from api.base.permissions import WriteOrPublicForRelationshipInstitutions
from api.cedar_metadata_records.serializers import CedarMetadataRecordsListSerializer
from api.cedar_metadata_records.utils import can_view_record
from api.citations.utils import render_citation
Expand Down Expand Up @@ -87,7 +88,6 @@
NodeGroupDetailPermissions,
IsContributorOrGroupMember,
AdminDeletePermissions,
WriteOrPublicForRelationshipInstitutions,
ExcludeWithdrawals,
NodeLinksShowIfVersion,
ReadOnlyIfWithdrawn,
Expand Down
46 changes: 43 additions & 3 deletions api/preprints/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from api.base.exceptions import Conflict, JSONAPIException
from api.base.serializers import (
BaseAPISerializer,
JSONAPISerializer,
IDField,
TypeField,
Expand Down Expand Up @@ -35,10 +36,11 @@
NodeTagField,
)
from api.base.metrics import MetricsSerializerMixin
from api.institutions.utils import update_institutions_if_user_associated
from api.taxonomies.serializers import TaxonomizableSerializerMixin
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 @@ -530,3 +530,43 @@ def update(self, instance, validated_data):
links = LinksField({
'self': 'get_self_url',
})


class PreprintsInstitutionsRelationshipSerializer(BaseAPISerializer):
from api.institutions.serializers import InstitutionRelated # Avoid circular import
data = ser.ListField(child=InstitutionRelated())

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

def get_self_url(self, obj):
return obj['self'].absolute_api_v2_url

def get_related_url(self, obj):
return f"{obj['self'].absolute_api_v2_url}institutions/"

class Meta:
type_ = 'institutions'

def make_instance_obj(self, obj):
return {
'data': obj.affiliated_institutions.all(),
'self': obj,
}

def update(self, instance, validated_data):
preprint = instance['self']
user = self.context['request'].user
update_institutions_if_user_associated(preprint, validated_data['data'], user)
cslzchen marked this conversation as resolved.
Show resolved Hide resolved
preprint.save()
return self.make_instance_obj(preprint)

def create(self, validated_data):
instance = self.context['view'].get_object()
preprint = instance['self']
user = self.context['request'].user
update_institutions_if_user_associated(preprint, validated_data['data'], user)
preprint.save()
return self.make_instance_obj(preprint)
1 change: 1 addition & 0 deletions api/preprints/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@
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.PreprintInstitutionsRelationship.as_view(), name=views.PreprintInstitutionsRelationship.view_name),
]
46 changes: 38 additions & 8 deletions api/preprints/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
JSONAPIMultipleRelationshipsParserForRegularJSON,
JSONAPIOnetoOneRelationshipParser,
JSONAPIOnetoOneRelationshipParserForRegularJSON,
JSONAPIRelationshipParser,
JSONAPIRelationshipParserForRegularJSON,
)
from api.base.utils import absolute_reverse, get_user_auth, get_object_or_error
from api.base import permissions as base_permissions
Expand All @@ -39,15 +41,14 @@
PreprintStorageProviderSerializer,
PreprintNodeRelationshipSerializer,
PreprintContributorsCreateSerializer,
PreprintsInstitutionsRelationshipSerializer,
)
from api.files.serializers import OsfStorageFileSerializer
from api.nodes.serializers import (
NodeCitationStyleSerializer,
)

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.nodes.serializers import NodeCitationStyleSerializer
from api.preprints.permissions import (
PreprintPublishedOrAdmin,
PreprintPublishedOrWrite,
Expand All @@ -57,16 +58,14 @@
PreprintFilesPermissions,
PreprintInstitutionPermissionList,
)
from api.nodes.permissions import (
ContributorOrPublic,
)
from api.nodes.permissions import ContributorOrPublic
from api.base.permissions import WriteOrPublicForRelationshipInstitutions
from api.requests.permissions import PreprintRequestPermission
from api.requests.serializers import PreprintRequestSerializer, PreprintRequestCreateSerializer
from api.requests.views import PreprintRequestMixin
from api.subjects.views import BaseResourceSubjectsList
from api.base.metrics import PreprintMetricsViewMixin
from osf.metrics import PreprintDownload, PreprintView
from api.institutions.serializers import InstitutionSerializer


class PreprintMixin(NodeMixin):
Expand Down Expand Up @@ -647,3 +646,34 @@ def get_resource(self):

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


class PreprintInstitutionsRelationship(JSONAPIBaseView, generics.RetrieveUpdateAPIView, PreprintMixin):
cslzchen marked this conversation as resolved.
Show resolved Hide resolved
""" """
permission_classes = (
drf_permissions.IsAuthenticatedOrReadOnly,
base_permissions.TokenHasScope,
WriteOrPublicForRelationshipInstitutions,
)
required_read_scopes = [CoreScopes.PREPRINTS_READ]
required_write_scopes = [CoreScopes.PREPRINTS_WRITE]
serializer_class = PreprintsInstitutionsRelationshipSerializer
parser_classes = (JSONAPIRelationshipParser, JSONAPIRelationshipParserForRegularJSON, )

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

def get_resource(self):
return self.get_preprint(check_object_permissions=False)

def get_object(self):
preprint = self.get_resource()
obj = {
'data': preprint.affiliated_institutions.all(),
'self': preprint,
}
self.check_object_permissions(self.request, obj)
return obj

def patch(self, *args, **kwargs):
raise MethodNotAllowed(self.request.method)
Loading
Loading