Skip to content

Commit

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

[ENG-5020] Implement CRUD API for CedarMetadataRecord
  • Loading branch information
cslzchen authored Jan 22, 2024
2 parents 05af8b6 + 16ed977 commit 476f22d
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 5 deletions.
1 change: 1 addition & 0 deletions api/base/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<provider_id>\w+)/bulk_create/(?P<filename>.*)/$', RegistrationBulkCreate.as_view(), name='bulk_create_csv'),
Expand Down
Empty file.
29 changes: 29 additions & 0 deletions api/cedar_metadata_records/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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):

def has_object_permission(self, request, view, obj):

assert isinstance(obj, CedarMetadataRecord), 'obj must be a CedarMetadataRecord'

auth = get_user_auth(request)

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 = permission_source.is_public and obj.is_published
return is_public or permission_source.can_view(auth)
return permission_source.can_edit(auth)
116 changes: 116 additions & 0 deletions api/cedar_metadata_records/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import logging

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, CedarMetadataTemplate, Guid

logger = logging.getLogger(__name__)


class TargetRelationshipField(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):

class Meta:
type_ = 'cedar-metadata-records'

filterable_fields = frozenset(['is_published'])

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,
)

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)

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


class CedarMetadataRecordsCreateSerializer(CedarMetadataRecordsSerializer):

target = TargetRelationshipField(
source='guid',
related_view='guids:guid-detail',
related_view_kwargs={'guids': '<guid._id>'},
# always_embed=True,
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')
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
10 changes: 10 additions & 0 deletions api/cedar_metadata_records/urls.py
Original file line number Diff line number Diff line change
@@ -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<record_id>[0-9A-Za-z]+)/$', views.CedarMetadataRecordDetail.as_view(), name=views.CedarMetadataRecordDetail.view_name),
]
73 changes: 73 additions & 0 deletions api/cedar_metadata_records/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from __future__ import unicode_literals
import logging

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.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, CedarMetadataRecordsCreateSerializer

from framework.auth.oauth_scopes import CoreScopes

from osf.models import CedarMetadataRecord

logger = logging.getLogger(__name__)


class CedarMetadataRecordList(JSONAPIBaseView, ListCreateAPIView, ListFilterMixin):

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

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

# 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.CEDAR_METADATA_RECORD_READ]
required_write_scopes = [CoreScopes.CEDAR_METADATA_RECORD_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
2 changes: 1 addition & 1 deletion api/cedar_metadata_templates/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})
6 changes: 3 additions & 3 deletions api/cedar_metadata_templates/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand 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:
Expand Down
5 changes: 5 additions & 0 deletions framework/auth/oauth_scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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
)
Expand Down
2 changes: 1 addition & 1 deletion osf/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 476f22d

Please sign in to comment.