Skip to content

Commit

Permalink
Merge pull request CenterForOpenScience#10515 from cslzchen/feature/e…
Browse files Browse the repository at this point in the history
…ng-5020-record-crud-api

[ENG-5203] Add metadata records relationships to nodes, registrations and files
  • Loading branch information
cslzchen authored Jan 26, 2024
2 parents 476f22d + e47dff1 commit b472a96
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 42 deletions.
2 changes: 2 additions & 0 deletions api/base/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,8 @@ def to_representation(self, value):
elif related_type == 'custom-file-metadata':
related_id = resolved_url.kwargs['guid_id']
related_type = 'custom-file-metadata-records'
elif related_type == 'cedar-metadata-templates' and related_class.view_name == 'cedar-metadata-template-detail':
related_id = resolved_url.kwargs['template_id']
else:
related_id = resolved_url.kwargs[related_type[:-1] + '_id']
except KeyError:
Expand Down
99 changes: 62 additions & 37 deletions api/cedar_metadata_records/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from api.base.exceptions import InvalidModelValueError, JSONAPIException
from api.base.serializers import JSONAPISerializer, LinksField, RelationshipField
from api.base.utils import absolute_reverse
from api.cedar_metadata_records.utils import get_guids_related_view, get_guids_related_view_kwargs

from osf.exceptions import ValidationError
from osf.models import CedarMetadataRecord, CedarMetadataTemplate, Guid
Expand All @@ -31,7 +32,7 @@ def to_internal_value(self, data):
return self.get_object(data)


class CedarMetadataRecordsSerializer(JSONAPISerializer):
class CedarMetadataRecordsBaseSerializer(JSONAPISerializer):

class Meta:
type_ = 'cedar-metadata-records'
Expand All @@ -40,66 +41,52 @@ class Meta:

id = ser.CharField(source='_id', read_only=True)

target = RelationshipField(
source='guid',
related_view='guids:guid-detail',
related_view_kwargs={'guids': '<guid._id>'},
# always_embed=True,
read_only=True,
)
metadata = ser.DictField(read_only=True)

template = RelationshipField(
related_view='cedar-metadata-templates:cedar-metadata-template-detail',
related_view_kwargs={'template_id': '<template._id>'},
# always_embed=True,
read_only=True,
)

metadata = ser.DictField(read_only=False)

is_published = ser.BooleanField(read_only=False)
is_published = ser.BooleanField(read_only=True)

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
raise NotImplementedError

def create(self, validated_data):
raise NotImplementedError

class CedarMetadataRecordsCreateSerializer(CedarMetadataRecordsSerializer):

class CedarMetadataRecordsListSerializer(CedarMetadataRecordsBaseSerializer):

def update(self, instance, validated_data):
raise NotImplementedError

def create(self, validated_data):
raise NotImplementedError


class CedarMetadataRecordsListCreateSerializer(CedarMetadataRecordsBaseSerializer):

metadata = ser.DictField(read_only=False, required=True)

is_published = ser.BooleanField(read_only=False, required=True)

target = TargetRelationshipField(
source='guid',
related_view='guids:guid-detail',
related_view_kwargs={'guids': '<guid._id>'},
# always_embed=True,
related_view=lambda record: get_guids_related_view(record),
related_view_kwargs=lambda record: get_guids_related_view_kwargs(record),
read_only=False,
required=True,
)

template = CedarMetadataTemplateRelationshipField(
related_view='cedar-metadata-templates:cedar-metadata-template-detail',
related_view_kwargs={'template_id': '<template._id>'},
# always_embed=True,
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('guid')
Expand All @@ -114,3 +101,41 @@ def create(self, validated_data):
except IntegrityError:
raise JSONAPIException(detail=f'Cedar metadata record already exists: guid=[{guid._id}], template=[{template._id}]')
return record

def update(self, instance, validated_data):
raise NotImplementedError


class CedarMetadataRecordsDetailSerializer(CedarMetadataRecordsBaseSerializer):

metadata = ser.DictField(read_only=False, required=False)

is_published = ser.BooleanField(read_only=False, required=False)

target = RelationshipField(
source='guid',
related_view=lambda record: get_guids_related_view(record),
related_view_kwargs=lambda record: get_guids_related_view_kwargs(record),
read_only=True,
)

template = RelationshipField(
related_view='cedar-metadata-templates:cedar-metadata-template-detail',
related_view_kwargs={'template_id': '<template._id>'},
read_only=True,
)

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):
raise NotImplementedError
29 changes: 29 additions & 0 deletions api/cedar_metadata_records/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import logging

from osf.models import BaseFileNode, CedarMetadataRecord, Node, Registration

logger = logging.getLogger(__name__)


def get_guids_related_view(obj):
assert isinstance(obj, CedarMetadataRecord), 'object must be a CedarMetadataRecord'
referent = obj.guid.referent
if isinstance(referent, Node):
return 'nodes:node-detail'
elif isinstance(referent, Registration):
return 'registrations:registration-detail'
elif isinstance(referent, BaseFileNode):
return 'files:file-detail'
else:
raise NotImplementedError()


def get_guids_related_view_kwargs(obj):
assert isinstance(obj, CedarMetadataRecord), 'object must be a CedarMetadataRecord'
referent = obj.guid.referent
if isinstance(referent, (Node, Registration)):
return {'node_id': '<guid._id>'}
elif isinstance(referent, BaseFileNode):
return {'file_id': '<guid._id>'}
else:
raise NotImplementedError()
17 changes: 13 additions & 4 deletions api/cedar_metadata_records/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
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, CedarMetadataRecordsCreateSerializer

from api.cedar_metadata_records.serializers import (
CedarMetadataRecordsListSerializer,
CedarMetadataRecordsListCreateSerializer,
CedarMetadataRecordsDetailSerializer,
)
from framework.auth.oauth_scopes import CoreScopes

from osf.models import CedarMetadataRecord
Expand All @@ -33,7 +36,7 @@ class CedarMetadataRecordList(JSONAPIBaseView, ListCreateAPIView, ListFilterMixi
required_read_scopes = [CoreScopes.CEDAR_METADATA_RECORD_READ]
required_write_scopes = [CoreScopes.CEDAR_METADATA_RECORD_WRITE]

serializer_class = CedarMetadataRecordsCreateSerializer
serializer_class = CedarMetadataRecordsListSerializer
parser_classes = (JSONAPIMultipleRelationshipsParser, JSONAPIMultipleRelationshipsParserForRegularJSON, )
model_class = CedarMetadataRecord

Expand All @@ -42,6 +45,12 @@ class CedarMetadataRecordList(JSONAPIBaseView, ListCreateAPIView, ListFilterMixi
view_category = 'cedar-metadata-records'
view_name = 'cedar-metadata-record-list'

def get_serializer_class(self):
if self.request.method == 'POST':
return CedarMetadataRecordsListCreateSerializer
else:
return CedarMetadataRecordsListSerializer

def get_default_queryset(self):
return CedarMetadataRecord.objects.filter(is_published=True)

Expand All @@ -59,7 +68,7 @@ class CedarMetadataRecordDetail(JSONAPIBaseView, RetrieveUpdateDestroyAPIView):
required_read_scopes = [CoreScopes.CEDAR_METADATA_RECORD_READ]
required_write_scopes = [CoreScopes.CEDAR_METADATA_RECORD_WRITE]

serializer_class = CedarMetadataRecordsSerializer
serializer_class = CedarMetadataRecordsDetailSerializer

# This view goes under the _/ namespace
versioning_class = PrivateVersioning
Expand Down
5 changes: 5 additions & 0 deletions api/files/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,11 @@ class FileSerializer(BaseFileSerializer):
help_text='Whether to mark the file as unviewed for the current user',
)

cedar_metadata_records = RelationshipField(
related_view='files:file-cedar-metadata-records-list',
related_view_kwargs={'file_id': '<_id>'},
)

def get_target_type(self, obj):
if isinstance(obj, Preprint):
return 'preprints'
Expand Down
1 change: 1 addition & 0 deletions api/files/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

urlpatterns = [
re_path(r'^(?P<file_id>\w+)/$', views.FileDetail.as_view(), name=views.FileDetail.view_name),
re_path(r'^(?P<file_id>\w+)/cedar_metadata_records/$', views.FileCedarMetadataRecordsList.as_view(), name=views.FileCedarMetadataRecordsList.view_name),
re_path(r'^(?P<file_id>\w+)/versions/$', views.FileVersionsList.as_view(), name=views.FileVersionsList.view_name),
re_path(r'^(?P<file_id>\w+)/versions/(?P<version_id>\w+)/$', views.FileVersionDetail.as_view(), name=views.FileVersionDetail.view_name),
]
26 changes: 26 additions & 0 deletions api/files/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@
Guid,
BaseFileNode,
FileVersion,
CedarMetadataRecord,
)

from api.base.exceptions import Gone
from api.base.filters import ListFilterMixin
from api.base.permissions import PermissionWithGetter
from api.base.throttling import CreateGuidThrottle, NonCookieAuthThrottle, UserRateThrottle, BurstRateThrottle
from api.base import utils
from api.base.views import JSONAPIBaseView
from api.base import permissions as base_permissions
from api.cedar_metadata_records.serializers import CedarMetadataRecordsListSerializer
from api.cedar_metadata_records.permissions import CedarMetadataRecordPermission
from api.nodes.permissions import ContributorOrPublic
from api.files import annotations
from api.files.permissions import IsPreprintFile
Expand Down Expand Up @@ -171,3 +175,25 @@ def get_serializer_context(self):
context = JSONAPIBaseView.get_serializer_context(self)
context['file'] = self.file
return context


class FileCedarMetadataRecordsList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin):

permission_classes = (
CedarMetadataRecordPermission,
drf_permissions.IsAuthenticatedOrReadOnly,
base_permissions.TokenHasScope,
)
required_read_scopes = [CoreScopes.CEDAR_METADATA_RECORD_READ]
required_write_scopes = [CoreScopes.NULL]

serializer_class = CedarMetadataRecordsListSerializer

view_category = 'files'
view_name = 'file-cedar-metadata-records-list'

def get_default_queryset(self):
return CedarMetadataRecord.objects.filter(guid___id=self.kwargs['file_id'])

def get_queryset(self):
return self.get_queryset_from_request()
5 changes: 5 additions & 0 deletions api/nodes/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,11 @@ class NodeSerializer(TaxonomizableSerializerMixin, JSONAPISerializer):
related_view_kwargs={'node_id': '<_id>'},
)

cedar_metadata_records = RelationshipField(
related_view='nodes:node-cedar-metadata-records-list',
related_view_kwargs={'node_id': '<_id>'},
)

@property
def subjects_related_view(self):
# Overrides TaxonomizableSerializerMixin
Expand Down
1 change: 1 addition & 0 deletions api/nodes/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
re_path(r'^(?P<node_id>\w+)/addons/(?P<provider>\w+)/folders/$', views.NodeAddonFolderList.as_view(), name=views.NodeAddonFolderList.view_name),
re_path(r'^(?P<node_id>\w+)/bibliographic_contributors/$', views.NodeBibliographicContributorsList.as_view(), name=views.NodeBibliographicContributorsList.view_name),
re_path(r'^(?P<node_id>\w+)/children/$', views.NodeChildrenList.as_view(), name=views.NodeChildrenList.view_name),
re_path(r'^(?P<node_id>\w+)/cedar_metadata_records/$', views.NodeCedarMetadataRecordsList.as_view(), name=views.NodeCedarMetadataRecordsList.view_name),
re_path(r'^(?P<node_id>\w+)/citation/$', views.NodeCitationDetail.as_view(), name=views.NodeCitationDetail.view_name),
re_path(r'^(?P<node_id>\w+)/citation/(?P<style_id>[-\w]+)/$', views.NodeCitationStyleDetail.as_view(), name=views.NodeCitationStyleDetail.view_name),
re_path(r'^(?P<node_id>\w+)/comments/$', views.NodeCommentsList.as_view(), name=views.NodeCommentsList.view_name),
Expand Down
25 changes: 25 additions & 0 deletions api/nodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
WaterButlerMixin,
)
from api.base.waffle_decorators import require_flag
from api.cedar_metadata_records.serializers import CedarMetadataRecordsListSerializer
from api.cedar_metadata_records.permissions import CedarMetadataRecordPermission
from api.citations.utils import render_citation
from api.comments.permissions import CanCommentOrPublic
from api.comments.serializers import (
Expand Down Expand Up @@ -150,6 +152,7 @@
Guid,
File,
Folder,
CedarMetadataRecord,
)
from addons.osfstorage.models import Region
from osf.utils.permissions import ADMIN, WRITE_NODE
Expand Down Expand Up @@ -2285,3 +2288,25 @@ def get_serializer_context(self):
context['wiki_addon'] = node.get_addon('wiki')
context['forward_addon'] = node.get_addon('forward')
return context


class NodeCedarMetadataRecordsList(JSONAPIBaseView, generics.ListAPIView, ListFilterMixin):

permission_classes = (
CedarMetadataRecordPermission,
drf_permissions.IsAuthenticatedOrReadOnly,
base_permissions.TokenHasScope,
)
required_read_scopes = [CoreScopes.CEDAR_METADATA_RECORD_READ]
required_write_scopes = [CoreScopes.NULL]

serializer_class = CedarMetadataRecordsListSerializer

view_category = 'nodes'
view_name = 'node-cedar-metadata-records-list'

def get_default_queryset(self):
return CedarMetadataRecord.objects.filter(guid___id=self.kwargs['node_id'])

def get_queryset(self):
return self.get_queryset_from_request()
5 changes: 5 additions & 0 deletions api/registrations/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,11 @@ class RegistrationSerializer(NodeSerializer):
related_view_kwargs={'node_id': '<_id>'},
))

cedar_metadata_records = RelationshipField(
related_view='registrations:registration-cedar-metadata-records-list',
related_view_kwargs={'node_id': '<_id>'},
)

@property
def subjects_related_view(self):
# Overrides TaxonomizableSerializerMixin
Expand Down
1 change: 1 addition & 0 deletions api/registrations/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
re_path(r'^$', views.RegistrationList.as_view(), name=views.RegistrationList.view_name),
re_path(r'^(?P<node_id>\w+)/$', views.RegistrationDetail.as_view(), name=views.RegistrationDetail.view_name),
re_path(r'^(?P<node_id>\w+)/bibliographic_contributors/$', views.RegistrationBibliographicContributorsList.as_view(), name=views.RegistrationBibliographicContributorsList.view_name),
re_path(r'^(?P<node_id>\w+)/cedar_metadata_records/$', views.RegistrationCedarMetadataRecordsList.as_view(), name=views.RegistrationCedarMetadataRecordsList.view_name),
re_path(r'^(?P<node_id>\w+)/children/$', views.RegistrationChildrenList.as_view(), name=views.RegistrationChildrenList.view_name),
re_path(r'^(?P<node_id>\w+)/comments/$', views.RegistrationCommentsList.as_view(), name=views.RegistrationCommentsList.view_name),
re_path(r'^(?P<node_id>\w+)/contributors/$', views.RegistrationContributorsList.as_view(), name=views.RegistrationContributorsList.view_name),
Expand Down
Loading

0 comments on commit b472a96

Please sign in to comment.