From 2f1fa9cfedc74db0cc8a1f39909ca7957fd5747b Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Wed, 17 Jan 2024 10:29:33 -0500 Subject: [PATCH 1/9] Implement CRUD API for CedarMetadataRecord --- api/base/urls.py | 1 + api/cedar_metadata_records/__init__.py | 0 api/cedar_metadata_records/permissions.py | 25 ++++++++ api/cedar_metadata_records/serializers.py | 66 +++++++++++++++++++++ api/cedar_metadata_records/urls.py | 10 ++++ api/cedar_metadata_records/views.py | 64 ++++++++++++++++++++ api/cedar_metadata_templates/serializers.py | 2 +- api/cedar_metadata_templates/views.py | 4 +- osf/models/__init__.py | 2 +- 9 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 api/cedar_metadata_records/__init__.py create mode 100644 api/cedar_metadata_records/permissions.py create mode 100644 api/cedar_metadata_records/serializers.py create mode 100644 api/cedar_metadata_records/urls.py create mode 100644 api/cedar_metadata_records/views.py diff --git a/api/base/urls.py b/api/base/urls.py index b2a1771b03d..f7e4cb74e71 100644 --- a/api/base/urls.py +++ b/api/base/urls.py @@ -23,6 +23,7 @@ re_path(r'^crossref/', include('api.crossref.urls', namespace='crossref')), re_path(r'^chronos/', include('api.chronos.urls', namespace='chronos')), re_path(r'^cedar_metadata_templates/', include('api.cedar_metadata_templates.urls', namespace='cedar-metadata-templates')), + re_path(r'^cedar_metadata_records/', include('api.cedar_metadata_records.urls', namespace='cedar-metadata-records')), re_path(r'^meetings/', include('api.meetings.urls', namespace='meetings')), re_path(r'^metrics/', include('api.metrics.urls', namespace='metrics')), re_path(r'^registries/(?P\w+)/bulk_create/(?P.*)/$', RegistrationBulkCreate.as_view(), name='bulk_create_csv'), diff --git a/api/cedar_metadata_records/__init__.py b/api/cedar_metadata_records/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/api/cedar_metadata_records/permissions.py b/api/cedar_metadata_records/permissions.py new file mode 100644 index 00000000000..b6ebb1d62fa --- /dev/null +++ b/api/cedar_metadata_records/permissions.py @@ -0,0 +1,25 @@ +from rest_framework import permissions + +from api.base.utils import get_user_auth + +from osf.models import BaseFileNode, CedarMetadataRecord, Node, Registration + + +class CedarMetadataRecordPermission(permissions.BasePermission): + + def has_object_permission(self, request, view, obj): + + assert isinstance(obj, CedarMetadataRecord), 'obj must be a CedarMetadataRecord' + + auth = get_user_auth(request) + + delegated_object = obj.guid.referent + if isinstance(delegated_object, BaseFileNode): + delegated_object = delegated_object.target + elif not isinstance(delegated_object, Node) and not isinstance(delegated_object, Registration): + return False + + if request.method in permissions.SAFE_METHODS: + is_public = delegated_object.is_public and obj.is_published + return is_public or delegated_object.can_view(auth) + return delegated_object.can_edit(auth) diff --git a/api/cedar_metadata_records/serializers.py b/api/cedar_metadata_records/serializers.py new file mode 100644 index 00000000000..1a68c4ec7fb --- /dev/null +++ b/api/cedar_metadata_records/serializers.py @@ -0,0 +1,66 @@ +from django.db import IntegrityError +from rest_framework import serializers as ser + +from api.base.exceptions import InvalidModelValueError, JSONAPIException +from api.base.serializers import JSONAPISerializer, LinksField, RelationshipField +from api.base.utils import absolute_reverse + +from osf.exceptions import ValidationError +from osf.models import CedarMetadataRecord + + +class CedarMetadataRecordsSerializer(JSONAPISerializer): + + class Meta: + type_ = 'cedar-metadata-records' + + filterable_fields = frozenset(['is_published']) + + id = ser.CharField(source='_id', read_only=True) + + target = RelationshipField( + related_view='guids:guid-detail', + related_view_kwargs={'guids': ''}, + always_embed=True, + ) + + template = RelationshipField( + related_view='cedar-metadata-templates:cedar-metadata-template-detail', + related_view_kwargs={'cedar-metadata-templates': ''}, + always_embed=True, + ) + + metadata = ser.DictField(read_only=False) + + is_published = ser.BooleanField(read_only=False) + + links = LinksField({'self': 'get_absolute_url'}) + + def get_absolute_url(self, obj): + return absolute_reverse('cedar-metadata-records:cedar-metadata-record-detail', kwargs={'record_id': obj._id}) + + def update(self, instance, validated_data): + assert isinstance(instance, CedarMetadataRecord), 'instance must be a CedarMetadataRecord' + for key, value in validated_data.items(): + if key == 'metadata': + instance.metadata = value + elif key == 'is_published': + instance.is_published = value + else: + continue # ignore other attributes + instance.save() + return instance + + def create(self, validated_data): + guid = validated_data.pop('target') + template = validated_data.pop('template') + metadata = validated_data.pop('metadata') + is_published = validated_data.pop('is_published') + record = CedarMetadataRecord(guid=guid, template=template, metadata=metadata, is_published=is_published) + try: + record.save() + except ValidationError as e: + raise InvalidModelValueError(detail=e.messages[0]) + except IntegrityError: + raise JSONAPIException(detail=f'Cedar metadata record already exists: guid=[{guid._id}], template=[{template._id}]') + return record diff --git a/api/cedar_metadata_records/urls.py b/api/cedar_metadata_records/urls.py new file mode 100644 index 00000000000..3a869a9a371 --- /dev/null +++ b/api/cedar_metadata_records/urls.py @@ -0,0 +1,10 @@ +from django.urls import re_path + +from api.cedar_metadata_records import views + +app_name = 'osf' + +urlpatterns = [ + re_path(r'^$', views.CedarMetadataRecordList.as_view(), name=views.CedarMetadataRecordList.view_name), + re_path(r'^(?P[0-9A-Za-z]+)/$', views.CedarMetadataRecordDetail.as_view(), name=views.CedarMetadataRecordDetail.view_name), +] diff --git a/api/cedar_metadata_records/views.py b/api/cedar_metadata_records/views.py new file mode 100644 index 00000000000..4eccd2dd27c --- /dev/null +++ b/api/cedar_metadata_records/views.py @@ -0,0 +1,64 @@ +from __future__ import unicode_literals + +from rest_framework import permissions as drf_permissions +from rest_framework.exceptions import NotFound +from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView + +from api.base import permissions as base_permissions +from api.base.filters import ListFilterMixin +from api.base.versioning import PrivateVersioning +from api.base.views import JSONAPIBaseView +from api.cedar_metadata_records.permissions import CedarMetadataRecordPermission +from api.cedar_metadata_records.serializers import CedarMetadataRecordsSerializer + +from framework.auth.oauth_scopes import CoreScopes + +from osf.models import CedarMetadataRecord + + +class CedarMetadataRecordList(JSONAPIBaseView, ListCreateAPIView, ListFilterMixin): + + permission_classes = ( + CedarMetadataRecordPermission, + drf_permissions.IsAuthenticatedOrReadOnly, + base_permissions.TokenHasScope, + ) + required_read_scopes = [CoreScopes.ALWAYS_PUBLIC] + required_write_scopes = [CoreScopes.NODE_BASE_WRITE, CoreScopes.NODE_FILE_WRITE, CoreScopes.NODE_REGISTRATIONS_WRITE] + + serializer_class = CedarMetadataRecordsSerializer + + # This view goes under the _/ namespace + versioning_class = PrivateVersioning + view_category = 'cedar-metadata-records' + view_name = 'cedar-metadata-record-list' + + def get_default_queryset(self): + return CedarMetadataRecord.objects.filter(is_published=True) + + def get_queryset(self): + return self.get_queryset_from_request() + + +class CedarMetadataRecordDetail(JSONAPIBaseView, RetrieveUpdateDestroyAPIView): + + permission_classes = ( + CedarMetadataRecordPermission, + drf_permissions.IsAuthenticatedOrReadOnly, + base_permissions.TokenHasScope, + ) + required_read_scopes = [CoreScopes.NODE_BASE_READ, CoreScopes.NODE_FILE_READ, CoreScopes.NODE_REGISTRATIONS_READ] + required_write_scopes = [CoreScopes.NODE_BASE_WRITE, CoreScopes.NODE_FILE_WRITE, CoreScopes.NODE_REGISTRATIONS_WRITE] + + serializer_class = CedarMetadataRecordsSerializer + + # This view goes under the _/ namespace + versioning_class = PrivateVersioning + view_category = 'cedar-metadata-records' + view_name = 'cedar-metadata-record-detail' + + def get_object(self): + try: + return CedarMetadataRecord.objects.get(_id=self.kwargs['record_id']) + except CedarMetadataRecord.DoesNotExist: + raise NotFound diff --git a/api/cedar_metadata_templates/serializers.py b/api/cedar_metadata_templates/serializers.py index 626f99ca057..cfb94533190 100644 --- a/api/cedar_metadata_templates/serializers.py +++ b/api/cedar_metadata_templates/serializers.py @@ -21,4 +21,4 @@ class Meta: links = LinksField({'self': 'get_absolute_url'}) def get_absolute_url(self, obj): - return absolute_reverse('cedar-metadata-templates:cedar-metadata-templates-detail', kwargs={'template_id': obj._id}) + return absolute_reverse('cedar-metadata-templates:cedar-metadata-template-detail', kwargs={'template_id': obj._id}) diff --git a/api/cedar_metadata_templates/views.py b/api/cedar_metadata_templates/views.py index 73bdca565af..65de418c9e3 100644 --- a/api/cedar_metadata_templates/views.py +++ b/api/cedar_metadata_templates/views.py @@ -28,7 +28,7 @@ class CedarMetadataTemplateList(JSONAPIBaseView, generics.ListAPIView, ListFilte # This view goes under the _/ namespace versioning_class = PrivateVersioning view_category = 'cedar-metadata-templates' - view_name = 'cedar-metadata-templates-list' + view_name = 'cedar-metadata-template-list' def get_default_queryset(self): return CedarMetadataTemplate.objects.all() @@ -51,7 +51,7 @@ class CedarMetadataTemplateDetail(JSONAPIBaseView, generics.RetrieveAPIView): # This view goes under the _/ namespace versioning_class = PrivateVersioning view_category = 'cedar-metadata-templates' - view_name = 'cedar-metadata-templates-detail' + view_name = 'cedar-metadata-template-detail' def get_object(self): try: diff --git a/osf/models/__init__.py b/osf/models/__init__.py index 3b2f3603e0d..cad31ea323f 100644 --- a/osf/models/__init__.py +++ b/osf/models/__init__.py @@ -18,7 +18,7 @@ Guid, ) from .brand import Brand -from .cedar_metadata import CedarMetadataTemplate +from .cedar_metadata import CedarMetadataRecord, CedarMetadataTemplate from .chronos import ChronosJournal, ChronosSubmission from .citation import CitationStyle from .collection import Collection From 4973e84a57e35d6936388abda9c98ee6f5efb1e0 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 22 Jan 2024 09:00:14 -0500 Subject: [PATCH 2/9] Fix views and serializers --- api/cedar_metadata_records/permissions.py | 4 ++ api/cedar_metadata_records/serializers.py | 58 ++++++++++++++++++++--- api/cedar_metadata_records/views.py | 13 ++++- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/api/cedar_metadata_records/permissions.py b/api/cedar_metadata_records/permissions.py index b6ebb1d62fa..262bf24aa14 100644 --- a/api/cedar_metadata_records/permissions.py +++ b/api/cedar_metadata_records/permissions.py @@ -1,9 +1,13 @@ +import logging + from rest_framework import permissions from api.base.utils import get_user_auth from osf.models import BaseFileNode, CedarMetadataRecord, Node, Registration +logger = logging.getLogger(__name__) + class CedarMetadataRecordPermission(permissions.BasePermission): diff --git a/api/cedar_metadata_records/serializers.py b/api/cedar_metadata_records/serializers.py index 1a68c4ec7fb..37573a5093f 100644 --- a/api/cedar_metadata_records/serializers.py +++ b/api/cedar_metadata_records/serializers.py @@ -1,3 +1,5 @@ +import logging + from django.db import IntegrityError from rest_framework import serializers as ser @@ -6,7 +8,27 @@ from api.base.utils import absolute_reverse from osf.exceptions import ValidationError -from osf.models import CedarMetadataRecord +from osf.models import CedarMetadataRecord, CedarMetadataTemplate, Guid + +logger = logging.getLogger(__name__) + + +class GuidRelationshipField(RelationshipField): + + def get_object(self, _id): + return Guid.load(_id) + + def to_internal_value(self, data): + return self.get_object(data) + + +class CedarMetadataTemplateRelationshipField(RelationshipField): + + def get_object(self, _id): + return CedarMetadataTemplate.load(_id) + + def to_internal_value(self, data): + return self.get_object(data) class CedarMetadataRecordsSerializer(JSONAPISerializer): @@ -18,16 +40,18 @@ class Meta: id = ser.CharField(source='_id', read_only=True) - target = RelationshipField( + guid = RelationshipField( related_view='guids:guid-detail', related_view_kwargs={'guids': ''}, - always_embed=True, + # always_embed=True, + read_only=False, ) template = RelationshipField( related_view='cedar-metadata-templates:cedar-metadata-template-detail', - related_view_kwargs={'cedar-metadata-templates': ''}, - always_embed=True, + related_view_kwargs={'template_id': ''}, + # always_embed=True, + read_only=False, ) metadata = ser.DictField(read_only=False) @@ -51,8 +75,30 @@ def update(self, instance, validated_data): instance.save() return instance + +class CedarMetadataRecordsCreateSerializer(CedarMetadataRecordsSerializer): + + guid = GuidRelationshipField( + related_view='guids:guid-detail', + related_view_kwargs={'guids': ''}, + read_only=False, + required=True, + ) + + template = CedarMetadataTemplateRelationshipField( + related_view='cedar-metadata-templates:cedar-metadata-template-detail', + related_view_kwargs={'template_id': ''}, + read_only=False, + required=True, + ) + + metadata = ser.DictField(read_only=False, required=True) + + is_published = ser.BooleanField(read_only=False, required=True) + def create(self, validated_data): - guid = validated_data.pop('target') + + guid = validated_data.pop('guid') template = validated_data.pop('template') metadata = validated_data.pop('metadata') is_published = validated_data.pop('is_published') diff --git a/api/cedar_metadata_records/views.py b/api/cedar_metadata_records/views.py index 4eccd2dd27c..f1f559f9431 100644 --- a/api/cedar_metadata_records/views.py +++ b/api/cedar_metadata_records/views.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import logging from rest_framework import permissions as drf_permissions from rest_framework.exceptions import NotFound @@ -6,15 +7,21 @@ from api.base import permissions as base_permissions from api.base.filters import ListFilterMixin +from api.base.parsers import ( + JSONAPIMultipleRelationshipsParser, + JSONAPIMultipleRelationshipsParserForRegularJSON, +) from api.base.versioning import PrivateVersioning from api.base.views import JSONAPIBaseView from api.cedar_metadata_records.permissions import CedarMetadataRecordPermission -from api.cedar_metadata_records.serializers import CedarMetadataRecordsSerializer +from api.cedar_metadata_records.serializers import CedarMetadataRecordsSerializer, CedarMetadataRecordsCreateSerializer from framework.auth.oauth_scopes import CoreScopes from osf.models import CedarMetadataRecord +logger = logging.getLogger(__name__) + class CedarMetadataRecordList(JSONAPIBaseView, ListCreateAPIView, ListFilterMixin): @@ -26,7 +33,9 @@ class CedarMetadataRecordList(JSONAPIBaseView, ListCreateAPIView, ListFilterMixi required_read_scopes = [CoreScopes.ALWAYS_PUBLIC] required_write_scopes = [CoreScopes.NODE_BASE_WRITE, CoreScopes.NODE_FILE_WRITE, CoreScopes.NODE_REGISTRATIONS_WRITE] - serializer_class = CedarMetadataRecordsSerializer + serializer_class = CedarMetadataRecordsCreateSerializer + parser_classes = (JSONAPIMultipleRelationshipsParser, JSONAPIMultipleRelationshipsParserForRegularJSON, ) + model_class = CedarMetadataRecord # This view goes under the _/ namespace versioning_class = PrivateVersioning From 87069333139449d5a0c78730237a51d47d77a586 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 22 Jan 2024 09:20:24 -0500 Subject: [PATCH 3/9] Update model and api for CedarMetadataRecord to use 'target' --- api/cedar_metadata_records/serializers.py | 16 +++++++------- .../0019_cedarmetadatarecord_update.py | 22 +++++++++++++++++++ osf/models/cedar_metadata.py | 6 ++--- 3 files changed, 33 insertions(+), 11 deletions(-) create mode 100644 osf/migrations/0019_cedarmetadatarecord_update.py diff --git a/api/cedar_metadata_records/serializers.py b/api/cedar_metadata_records/serializers.py index 37573a5093f..42e24c8852e 100644 --- a/api/cedar_metadata_records/serializers.py +++ b/api/cedar_metadata_records/serializers.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) -class GuidRelationshipField(RelationshipField): +class TargetRelationshipField(RelationshipField): def get_object(self, _id): return Guid.load(_id) @@ -40,9 +40,9 @@ class Meta: id = ser.CharField(source='_id', read_only=True) - guid = RelationshipField( + target = RelationshipField( related_view='guids:guid-detail', - related_view_kwargs={'guids': ''}, + related_view_kwargs={'guids': ''}, # always_embed=True, read_only=False, ) @@ -78,9 +78,9 @@ def update(self, instance, validated_data): class CedarMetadataRecordsCreateSerializer(CedarMetadataRecordsSerializer): - guid = GuidRelationshipField( + target = TargetRelationshipField( related_view='guids:guid-detail', - related_view_kwargs={'guids': ''}, + related_view_kwargs={'guids': ''}, read_only=False, required=True, ) @@ -98,15 +98,15 @@ class CedarMetadataRecordsCreateSerializer(CedarMetadataRecordsSerializer): def create(self, validated_data): - guid = validated_data.pop('guid') + target = validated_data.pop('target') template = validated_data.pop('template') metadata = validated_data.pop('metadata') is_published = validated_data.pop('is_published') - record = CedarMetadataRecord(guid=guid, template=template, metadata=metadata, is_published=is_published) + record = CedarMetadataRecord(target=target, template=template, metadata=metadata, is_published=is_published) try: record.save() except ValidationError as e: raise InvalidModelValueError(detail=e.messages[0]) except IntegrityError: - raise JSONAPIException(detail=f'Cedar metadata record already exists: guid=[{guid._id}], template=[{template._id}]') + raise JSONAPIException(detail=f'Cedar metadata record already exists: guid=[{target._id}], template=[{template._id}]') return record diff --git a/osf/migrations/0019_cedarmetadatarecord_update.py b/osf/migrations/0019_cedarmetadatarecord_update.py new file mode 100644 index 00000000000..5403049ca9b --- /dev/null +++ b/osf/migrations/0019_cedarmetadatarecord_update.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.17 on 2024-01-22 14:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('osf', '0018_cedarmetadatarecord'), + ] + + operations = [ + migrations.RenameField( + model_name='cedarmetadatarecord', + old_name='guid', + new_name='target', + ), + migrations.AlterUniqueTogether( + name='cedarmetadatarecord', + unique_together={('target', 'template')}, + ), + ] diff --git a/osf/models/cedar_metadata.py b/osf/models/cedar_metadata.py index 68378d23312..daea99a111a 100644 --- a/osf/models/cedar_metadata.py +++ b/osf/models/cedar_metadata.py @@ -20,13 +20,13 @@ def __unicode__(self): class CedarMetadataRecord(ObjectIDMixin, BaseModel): - guid = models.ForeignKey('Guid', on_delete=models.CASCADE) + target = models.ForeignKey('Guid', on_delete=models.CASCADE) template = models.ForeignKey('CedarMetadataTemplate', on_delete=models.CASCADE) metadata = DateTimeAwareJSONField(default=dict) is_published = models.BooleanField(default=False) class Meta: - unique_together = ('guid', 'template') + unique_together = ('target', 'template') def __unicode__(self): - return f'(guid=[{self.guid._id}], template=[{self.template._id}])' + return f'(guid=[{self.target._id}], template=[{self.template._id}])' From 36c61e3183c85f9d7753f3cfb235fbe4e1afdeef Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 22 Jan 2024 09:25:13 -0500 Subject: [PATCH 4/9] Improve permission check --- api/cedar_metadata_records/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/cedar_metadata_records/permissions.py b/api/cedar_metadata_records/permissions.py index 262bf24aa14..611fa02578f 100644 --- a/api/cedar_metadata_records/permissions.py +++ b/api/cedar_metadata_records/permissions.py @@ -20,7 +20,7 @@ def has_object_permission(self, request, view, obj): delegated_object = obj.guid.referent if isinstance(delegated_object, BaseFileNode): delegated_object = delegated_object.target - elif not isinstance(delegated_object, Node) and not isinstance(delegated_object, Registration): + elif not isinstance(delegated_object, (Node, Registration)): return False if request.method in permissions.SAFE_METHODS: From 5da6cd4913a9de4230845c81b6c1d036a604d4a2 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 22 Jan 2024 09:46:07 -0500 Subject: [PATCH 5/9] Make target and tempalte read-only for details view --- api/cedar_metadata_records/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/cedar_metadata_records/serializers.py b/api/cedar_metadata_records/serializers.py index 42e24c8852e..b63c993b9c6 100644 --- a/api/cedar_metadata_records/serializers.py +++ b/api/cedar_metadata_records/serializers.py @@ -44,14 +44,14 @@ class Meta: related_view='guids:guid-detail', related_view_kwargs={'guids': ''}, # always_embed=True, - read_only=False, + read_only=True, ) template = RelationshipField( related_view='cedar-metadata-templates:cedar-metadata-template-detail', related_view_kwargs={'template_id': ''}, # always_embed=True, - read_only=False, + read_only=True, ) metadata = ser.DictField(read_only=False) From 923bea8ba3c3d0074813516a1995165b3b12759b Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 22 Jan 2024 11:14:49 -0500 Subject: [PATCH 6/9] Add decicated oauth scopes for cedar metadta templates & records --- api/cedar_metadata_records/views.py | 8 ++++---- api/cedar_metadata_templates/views.py | 2 +- framework/auth/oauth_scopes.py | 5 +++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/api/cedar_metadata_records/views.py b/api/cedar_metadata_records/views.py index f1f559f9431..8fde6cdeea8 100644 --- a/api/cedar_metadata_records/views.py +++ b/api/cedar_metadata_records/views.py @@ -30,8 +30,8 @@ class CedarMetadataRecordList(JSONAPIBaseView, ListCreateAPIView, ListFilterMixi drf_permissions.IsAuthenticatedOrReadOnly, base_permissions.TokenHasScope, ) - required_read_scopes = [CoreScopes.ALWAYS_PUBLIC] - required_write_scopes = [CoreScopes.NODE_BASE_WRITE, CoreScopes.NODE_FILE_WRITE, CoreScopes.NODE_REGISTRATIONS_WRITE] + required_read_scopes = [CoreScopes.CEDAR_METADATA_RECORD_READ] + required_write_scopes = [CoreScopes.CEDAR_METADATA_RECORD_WRITE] serializer_class = CedarMetadataRecordsCreateSerializer parser_classes = (JSONAPIMultipleRelationshipsParser, JSONAPIMultipleRelationshipsParserForRegularJSON, ) @@ -56,8 +56,8 @@ class CedarMetadataRecordDetail(JSONAPIBaseView, RetrieveUpdateDestroyAPIView): drf_permissions.IsAuthenticatedOrReadOnly, base_permissions.TokenHasScope, ) - required_read_scopes = [CoreScopes.NODE_BASE_READ, CoreScopes.NODE_FILE_READ, CoreScopes.NODE_REGISTRATIONS_READ] - required_write_scopes = [CoreScopes.NODE_BASE_WRITE, CoreScopes.NODE_FILE_WRITE, CoreScopes.NODE_REGISTRATIONS_WRITE] + required_read_scopes = [CoreScopes.CEDAR_METADATA_RECORD_READ] + required_write_scopes = [CoreScopes.CEDAR_METADATA_RECORD_WRITE] serializer_class = CedarMetadataRecordsSerializer diff --git a/api/cedar_metadata_templates/views.py b/api/cedar_metadata_templates/views.py index 65de418c9e3..896f3a8be39 100644 --- a/api/cedar_metadata_templates/views.py +++ b/api/cedar_metadata_templates/views.py @@ -19,7 +19,7 @@ class CedarMetadataTemplateList(JSONAPIBaseView, generics.ListAPIView, ListFilte drf_permissions.IsAuthenticatedOrReadOnly, base_permissions.TokenHasScope, ) - required_read_scopes = [CoreScopes.ALWAYS_PUBLIC] + required_read_scopes = [CoreScopes.CEDAR_METADATA_RECORD_READ] required_write_scopes = [CoreScopes.NULL] serializer_class = CedarMetadataTemplateSerializer diff --git a/framework/auth/oauth_scopes.py b/framework/auth/oauth_scopes.py index 9bcd68efdc9..8068e0f6466 100644 --- a/framework/auth/oauth_scopes.py +++ b/framework/auth/oauth_scopes.py @@ -160,6 +160,9 @@ class CoreScopes(object): WAFFLE_READ = 'waffle_read' + CEDAR_METADATA_RECORD_READ = 'cedar_metadata_record_read' + CEDAR_METADATA_RECORD_WRITE = 'cedar_metadata_record_write' + NULL = 'null' # NOTE: Use with extreme caution. @@ -316,6 +319,7 @@ class ComposedScopes(object): + PREPRINT_ALL_READ\ + GROUP_READ\ + ( + CoreScopes.CEDAR_METADATA_RECORD_READ, CoreScopes.MEETINGS_READ, CoreScopes.INSTITUTION_READ, CoreScopes.SEARCH, @@ -336,6 +340,7 @@ class ComposedScopes(object): + PREPRINT_ALL_WRITE\ + GROUP_WRITE\ + ( + CoreScopes.CEDAR_METADATA_RECORD_WRITE, CoreScopes.WRITE_COLLECTION_SUBMISSION_ACTION, CoreScopes.WRITE_COLLECTION_SUBMISSION ) From 6b5f487cadd97e649a8ab3d9a3529b893663251c Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 22 Jan 2024 11:24:56 -0500 Subject: [PATCH 7/9] Revert "Update model and api for CedarMetadataRecord to use 'target'" This reverts commit 87069333139449d5a0c78730237a51d47d77a586. --- api/cedar_metadata_records/serializers.py | 16 +++++++------- .../0019_cedarmetadatarecord_update.py | 22 ------------------- osf/models/cedar_metadata.py | 6 ++--- 3 files changed, 11 insertions(+), 33 deletions(-) delete mode 100644 osf/migrations/0019_cedarmetadatarecord_update.py diff --git a/api/cedar_metadata_records/serializers.py b/api/cedar_metadata_records/serializers.py index b63c993b9c6..88607e2fe7e 100644 --- a/api/cedar_metadata_records/serializers.py +++ b/api/cedar_metadata_records/serializers.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) -class TargetRelationshipField(RelationshipField): +class GuidRelationshipField(RelationshipField): def get_object(self, _id): return Guid.load(_id) @@ -40,9 +40,9 @@ class Meta: id = ser.CharField(source='_id', read_only=True) - target = RelationshipField( + guid = RelationshipField( related_view='guids:guid-detail', - related_view_kwargs={'guids': ''}, + related_view_kwargs={'guids': ''}, # always_embed=True, read_only=True, ) @@ -78,9 +78,9 @@ def update(self, instance, validated_data): class CedarMetadataRecordsCreateSerializer(CedarMetadataRecordsSerializer): - target = TargetRelationshipField( + guid = GuidRelationshipField( related_view='guids:guid-detail', - related_view_kwargs={'guids': ''}, + related_view_kwargs={'guids': ''}, read_only=False, required=True, ) @@ -98,15 +98,15 @@ class CedarMetadataRecordsCreateSerializer(CedarMetadataRecordsSerializer): def create(self, validated_data): - target = validated_data.pop('target') + guid = validated_data.pop('guid') template = validated_data.pop('template') metadata = validated_data.pop('metadata') is_published = validated_data.pop('is_published') - record = CedarMetadataRecord(target=target, template=template, metadata=metadata, is_published=is_published) + record = CedarMetadataRecord(guid=guid, template=template, metadata=metadata, is_published=is_published) try: record.save() except ValidationError as e: raise InvalidModelValueError(detail=e.messages[0]) except IntegrityError: - raise JSONAPIException(detail=f'Cedar metadata record already exists: guid=[{target._id}], template=[{template._id}]') + raise JSONAPIException(detail=f'Cedar metadata record already exists: guid=[{guid._id}], template=[{template._id}]') return record diff --git a/osf/migrations/0019_cedarmetadatarecord_update.py b/osf/migrations/0019_cedarmetadatarecord_update.py deleted file mode 100644 index 5403049ca9b..00000000000 --- a/osf/migrations/0019_cedarmetadatarecord_update.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 3.2.17 on 2024-01-22 14:11 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('osf', '0018_cedarmetadatarecord'), - ] - - operations = [ - migrations.RenameField( - model_name='cedarmetadatarecord', - old_name='guid', - new_name='target', - ), - migrations.AlterUniqueTogether( - name='cedarmetadatarecord', - unique_together={('target', 'template')}, - ), - ] diff --git a/osf/models/cedar_metadata.py b/osf/models/cedar_metadata.py index daea99a111a..68378d23312 100644 --- a/osf/models/cedar_metadata.py +++ b/osf/models/cedar_metadata.py @@ -20,13 +20,13 @@ def __unicode__(self): class CedarMetadataRecord(ObjectIDMixin, BaseModel): - target = models.ForeignKey('Guid', on_delete=models.CASCADE) + guid = models.ForeignKey('Guid', on_delete=models.CASCADE) template = models.ForeignKey('CedarMetadataTemplate', on_delete=models.CASCADE) metadata = DateTimeAwareJSONField(default=dict) is_published = models.BooleanField(default=False) class Meta: - unique_together = ('target', 'template') + unique_together = ('guid', 'template') def __unicode__(self): - return f'(guid=[{self.target._id}], template=[{self.template._id}])' + return f'(guid=[{self.guid._id}], template=[{self.template._id}])' From 62a441d7eea21c756dde8541cd224c69d237df9d Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 22 Jan 2024 12:02:24 -0500 Subject: [PATCH 8/9] Use source= to connect target to guid + fix related_view_kwargs --- api/cedar_metadata_records/serializers.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/api/cedar_metadata_records/serializers.py b/api/cedar_metadata_records/serializers.py index 88607e2fe7e..1b6c29c4e69 100644 --- a/api/cedar_metadata_records/serializers.py +++ b/api/cedar_metadata_records/serializers.py @@ -13,7 +13,7 @@ logger = logging.getLogger(__name__) -class GuidRelationshipField(RelationshipField): +class TargetRelationshipField(RelationshipField): def get_object(self, _id): return Guid.load(_id) @@ -40,7 +40,8 @@ class Meta: id = ser.CharField(source='_id', read_only=True) - guid = RelationshipField( + target = RelationshipField( + source='guid', related_view='guids:guid-detail', related_view_kwargs={'guids': ''}, # always_embed=True, @@ -78,9 +79,11 @@ def update(self, instance, validated_data): class CedarMetadataRecordsCreateSerializer(CedarMetadataRecordsSerializer): - guid = GuidRelationshipField( + target = TargetRelationshipField( + source='guid', related_view='guids:guid-detail', related_view_kwargs={'guids': ''}, + # always_embed=True, read_only=False, required=True, ) @@ -88,6 +91,7 @@ class CedarMetadataRecordsCreateSerializer(CedarMetadataRecordsSerializer): template = CedarMetadataTemplateRelationshipField( related_view='cedar-metadata-templates:cedar-metadata-template-detail', related_view_kwargs={'template_id': ''}, + # always_embed=True, read_only=False, required=True, ) From 16ed977884e3f450f36899d6cac654d7a70240a3 Mon Sep 17 00:00:00 2001 From: Longze Chen Date: Mon, 22 Jan 2024 12:08:08 -0500 Subject: [PATCH 9/9] Rename delegated_obj to permission_src for better readability --- api/cedar_metadata_records/permissions.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/api/cedar_metadata_records/permissions.py b/api/cedar_metadata_records/permissions.py index 611fa02578f..2a78b932f6c 100644 --- a/api/cedar_metadata_records/permissions.py +++ b/api/cedar_metadata_records/permissions.py @@ -17,13 +17,13 @@ def has_object_permission(self, request, view, obj): auth = get_user_auth(request) - delegated_object = obj.guid.referent - if isinstance(delegated_object, BaseFileNode): - delegated_object = delegated_object.target - elif not isinstance(delegated_object, (Node, Registration)): + permission_source = obj.guid.referent + if isinstance(permission_source, BaseFileNode): + permission_source = permission_source.target + elif not isinstance(permission_source, (Node, Registration)): return False if request.method in permissions.SAFE_METHODS: - is_public = delegated_object.is_public and obj.is_published - return is_public or delegated_object.can_view(auth) - return delegated_object.can_edit(auth) + is_public = permission_source.is_public and obj.is_published + return is_public or permission_source.can_view(auth) + return permission_source.can_edit(auth)