Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
 into summary-reporter-for-insti-dashboard

* 'develop' of https://github.com/CenterForOpenScience/osf.io:
  Clean up local-ci
  Remove calls to mark_safe
  Update changelog and bump version
  [ENG-5028] [ENG-5920] Preprints Affiliation Project PR (BE) (#10745)

# Conflicts:
#	api/institutions/serializers.py
  • Loading branch information
John Tordoff committed Sep 19, 2024
2 parents 93f9efe + 73dbff8 commit cf164b1
Show file tree
Hide file tree
Showing 30 changed files with 1,228 additions and 504 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
POSTGRES_PASSWORD: ${{ env.OSF_DB_PASSWORD }}
options: >-
--health-cmd pg_isready
--health-interval 10s
Expand All @@ -66,7 +66,7 @@ jobs:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
POSTGRES_PASSWORD: ${{ env.OSF_DB_PASSWORD }}
options: >-
--health-cmd pg_isready
--health-interval 10s
Expand All @@ -93,7 +93,7 @@ jobs:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
POSTGRES_PASSWORD: ${{ env.OSF_DB_PASSWORD }}
options: >-
--health-cmd pg_isready
--health-interval 10s
Expand Down Expand Up @@ -122,7 +122,7 @@ jobs:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: postgres
POSTGRES_PASSWORD: ${{ env.OSF_DB_PASSWORD }}
options: >-
--health-cmd pg_isready
--health-interval 10s
Expand Down Expand Up @@ -150,7 +150,7 @@ jobs:
image: postgres

env:
POSTGRES_PASSWORD: postgres
POSTGRES_PASSWORD: ${{ env.OSF_DB_PASSWORD }}
options: >-
--health-cmd pg_isready
--health-interval 10s
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

We follow the CalVer (https://calver.org/) versioning scheme: YY.MINOR.MICRO.

24.07.0 (2024-09-19)
====================

- Preprints Affiliation Project BE Release

24.06.0 (2024-09-12)
====================

Expand Down
3 changes: 1 addition & 2 deletions admin/base/filters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django import template
from django.utils.safestring import mark_safe
import json


Expand All @@ -8,4 +7,4 @@

@register.filter
def jsonify(o):
return mark_safe(json.dumps(o))
return json.dumps(o)
3 changes: 1 addition & 2 deletions admin/base/templatetags/filters.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# h/t https://djangosnippets.org/snippets/1250/
from django import template
from django.utils.safestring import mark_safe
import json

register = template.Library()

@register.filter
def jsonify(o):
return mark_safe(json.dumps(o))
return json.dumps(o)
18 changes: 17 additions & 1 deletion api/base/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
from framework.auth import oauth_scopes
from framework.auth.cas import CasResponse

from osf.models import ApiOAuth2Application, ApiOAuth2PersonalToken
from osf.models import ApiOAuth2Application, ApiOAuth2PersonalToken, Preprint
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 @@ -160,3 +162,17 @@ def has_object_permission(self, request, view, obj):
obj = self.get_object(request, view, obj)
return super().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:
if isinstance(resource, Preprint):
return resource.can_edit(auth=auth)
return resource.has_permission(auth.user, osf_permissions.WRITE)
2 changes: 1 addition & 1 deletion api/base/settings/local-ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
ENABLE_VARNISH = True
ENABLE_ESI = False

OSF_DB_PASSWORD = 'postgres'
OSF_DB_PASSWORD = os.environ.get('OSF_DB_PASSWORD')

SESSION_ENGINE = 'django.contrib.sessions.backends.db'

Expand Down
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
13 changes: 10 additions & 3 deletions api/institutions/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def create(self, validated_data):
if not node.has_permission(user, osf_permissions.WRITE):
raise exceptions.PermissionDenied(detail='Write permission on node {} required'.format(node_dict['_id']))
if not node.is_affiliated_with_institution(inst):
node.add_affiliated_institution(inst, user, save=True)
node.add_affiliated_institution(inst, user)
changes_flag = True

if not changes_flag:
Expand All @@ -155,11 +155,19 @@ def create(self, validated_data):
'self': inst,
}


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


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


class InstitutionRegistrationsRelationshipSerializer(BaseAPISerializer):
data = ser.ListField(child=RegistrationRelated())
links = LinksField({
Expand Down Expand Up @@ -189,7 +197,7 @@ def create(self, validated_data):
if not registration.has_permission(user, osf_permissions.WRITE):
raise exceptions.PermissionDenied(detail='Write permission on registration {} required'.format(registration_dict['_id']))
if not registration.is_affiliated_with_institution(inst):
registration.add_affiliated_institution(inst, user, save=True)
registration.add_affiliated_institution(inst, user)
changes_flag = True

if not changes_flag:
Expand Down Expand Up @@ -313,7 +321,6 @@ def get_absolute_url(self, obj):
},
)


class NewInstitutionUserMetricsSerializer(JSONAPISerializer):
'''serializer for institution-users metrics
Expand Down
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 @@ -294,18 +294,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=f'Institution with id "{inst_id}" was not found')
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=f'User needs to be affiliated with {inst.name}',
)
node.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}',
)
node.add_affiliated_institution(inst, user)


class RegionRelationshipField(RelationshipField):

def to_internal_value(self, data):
Expand Down Expand Up @@ -1479,13 +1441,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
27 changes: 27 additions & 0 deletions api/preprints/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,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)
Loading

0 comments on commit cf164b1

Please sign in to comment.