Skip to content

Commit 93f5ed3

Browse files
committed
Merge branch 'release/19.28.0'
2 parents 309b983 + 34f9161 commit 93f5ed3

30 files changed

+1442
-97
lines changed

CHANGELOG

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

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

5+
19.28.0 (2019-09-24)
6+
===================
7+
- API v2: Use consistent naming for JSON API type (kebab-case)
8+
- API v2: Fix sorting on fields with source attribute
9+
- API v2: Add SparseLists for Nodes and Registrations
10+
- Management command to remove duplicate files and folders
11+
512
19.27.0 (2019-09-18)
613
===================
714
- Automatically map subjects when a preprint is moved to a different

api/addons/serializers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from rest_framework import serializers as ser
22
from api.base.serializers import JSONAPISerializer, LinksField
33
from api.base.utils import absolute_reverse
4+
from api.base.versioning import get_kebab_snake_case_field
45

56
class NodeAddonFolderSerializer(JSONAPISerializer):
67
class Meta:
7-
type_ = 'node_addon_folders'
8+
@staticmethod
9+
def get_type(request):
10+
return get_kebab_snake_case_field(request.version, 'node-addon-folders')
811

912
id = ser.CharField(read_only=True)
1013
kind = ser.CharField(default='folder', read_only=True)

api/base/filters.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,51 @@ def filter_queryset(self, request, queryset, view):
6666
return queryset.sort(*ordering)
6767
return queryset
6868

69+
def get_serializer_source_field(self, view, request):
70+
"""
71+
Returns a dictionary of serializer fields and source names. i.e. {'date_created': 'created'}
72+
73+
Logic borrowed from OrderingFilter.get_default_valid_fields with modifications to retrieve
74+
source fields for serializer field names.
75+
76+
:param view api view
77+
:
78+
"""
79+
field_to_source_mapping = {}
80+
81+
if hasattr(view, 'get_serializer_class'):
82+
serializer_class = view.get_serializer_class()
83+
else:
84+
serializer_class = getattr(view, 'serializer_class', None)
85+
86+
# This will not allow any serializer fields with nested related fields to be sorted on
87+
for field_name, field in serializer_class(context={'request': request}).fields.items():
88+
if not getattr(field, 'write_only', False) and not field.source == '*' and field_name != field.source:
89+
field_to_source_mapping[field_name] = field.source.replace('.', '_')
90+
91+
return field_to_source_mapping
92+
93+
# Overrides OrderingFilter
94+
def remove_invalid_fields(self, queryset, fields, view, request):
95+
"""
96+
Returns an array of valid fields to be used for ordering.
97+
Any valid source fields which are input remain in the valid fields list using the super method.
98+
Serializer fields are mapped to their source fields and returned.
99+
:param fields, array, input sort fields
100+
:returns array of source fields for sorting.
101+
"""
102+
valid_fields = super(OSFOrderingFilter, self).remove_invalid_fields(queryset, fields, view, request)
103+
if not valid_fields:
104+
for invalid_field in fields:
105+
ordering_sign = '-' if invalid_field[0] == '-' else ''
106+
invalid_field = invalid_field.lstrip('-')
107+
108+
field_source_mapping = self.get_serializer_source_field(view, request)
109+
source_field = field_source_mapping.get(invalid_field, None)
110+
if source_field:
111+
valid_fields.append(ordering_sign + source_field)
112+
return valid_fields
113+
69114

70115
class FilterMixin(object):
71116
""" View mixin with helper functions for filtering. """

api/base/renderers.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,14 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
2323
# See JSON-API documentation on meta information: http://jsonapi.org/format/#document-meta
2424
data_type = type(data)
2525
if renderer_context is not None and data_type != str and data is not None:
26-
meta_dict = renderer_context.get('meta')
26+
meta_dict = renderer_context.get('meta', {})
2727
version = getattr(renderer_context['request'], 'version', None)
28-
if meta_dict is not None:
29-
if version:
30-
meta_dict['version'] = renderer_context['request'].version
31-
data.setdefault('meta', {}).update(meta_dict)
32-
elif version:
33-
meta_dict = {'version': renderer_context['request'].version}
34-
data.setdefault('meta', {}).update(meta_dict)
28+
warning = renderer_context['request'].META.get('warning', None)
29+
if version:
30+
meta_dict['version'] = version
31+
if warning:
32+
meta_dict['warning'] = warning
33+
data.setdefault('meta', {}).update(meta_dict)
3534
return super(JSONAPIRenderer, self).render(data, accepted_media_type, renderer_context)
3635

3736

api/base/serializers.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from osf.models import AbstractNode, MaintenanceState, Preprint
2424
from website import settings
2525
from website.project.model import has_anonymous_link
26+
from api.base.versioning import KEBAB_CASE_VERSION, get_kebab_snake_case_field
2627

2728

2829
def get_meta_type(serializer_class, request):
@@ -404,8 +405,11 @@ def to_internal_value(self, data):
404405
type_ = get_meta_type(self.root.child, request)
405406
else:
406407
type_ = get_meta_type(self.root, request)
407-
408-
if type_ != data:
408+
kebab_case = str(type_).replace('-', '_')
409+
if type_ != data and kebab_case == data:
410+
type_ = kebab_case
411+
self.context['request'].META.setdefault('warning', 'As of API Version {0}, all types are now Kebab-case. {0} will accept snake_case, but this will be deprecated in future versions.'.format(KEBAB_CASE_VERSION))
412+
elif type_ != data:
409413
raise api_exceptions.Conflict(detail=('This resource has a type of "{}", but you set the json body\'s type field to "{}". You probably need to change the type field to match the resource\'s type.'.format(type_, data)))
410414
return super(TypeField, self).to_internal_value(data)
411415

@@ -1578,7 +1582,9 @@ class AddonAccountSerializer(JSONAPISerializer):
15781582
})
15791583

15801584
class Meta:
1581-
type_ = 'external_accounts'
1585+
@staticmethod
1586+
def get_type(request):
1587+
return get_kebab_snake_case_field(request.version, 'external-accounts')
15821588

15831589
def get_absolute_url(self, obj):
15841590
kwargs = self.context['request'].parser_context['kwargs']

api/base/settings/defaults.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
'2.15',
169169
'2.16',
170170
'2.17',
171+
'2.18',
171172
),
172173
'DEFAULT_FILTER_BACKENDS': ('api.base.filters.OSFOrderingFilter',),
173174
'DEFAULT_PAGINATION_CLASS': 'api.base.pagination.JSONAPIPagination',

api/base/urls.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
url(r'^status/', views.status_check, name='status_check'),
3434
url(r'^actions/', include('api.actions.urls', namespace='actions')),
3535
url(r'^addons/', include('api.addons.urls', namespace='addons')),
36+
url(r'^alerts/', include('api.alerts.urls', namespace='alerts')),
3637
url(r'^applications/', include('api.applications.urls', namespace='applications')),
3738
url(r'^citations/', include('api.citations.urls', namespace='citations')),
3839
url(r'^collections/', include('api.collections.urls', namespace='collections')),
@@ -56,16 +57,16 @@
5657
url(r'^requests/', include('api.requests.urls', namespace='requests')),
5758
url(r'^scopes/', include('api.scopes.urls', namespace='scopes')),
5859
url(r'^search/', include('api.search.urls', namespace='search')),
60+
url(r'^sparse/', include('api.sparse.urls', namespace='sparse')),
5961
url(r'^subjects/', include('api.subjects.urls', namespace='subjects')),
6062
url(r'^subscriptions/', include('api.subscriptions.urls', namespace='subscriptions')),
6163
url(r'^taxonomies/', include('api.taxonomies.urls', namespace='taxonomies')),
6264
url(r'^test/', include('api.test.urls', namespace='test')),
6365
url(r'^tokens/', include('api.tokens.urls', namespace='tokens')),
6466
url(r'^users/', include('api.users.urls', namespace='users')),
6567
url(r'^view_only_links/', include('api.view_only_links.urls', namespace='view-only-links')),
66-
url(r'^_waffle/', include('api.waffle.urls', namespace='waffle')),
6768
url(r'^wikis/', include('api.wikis.urls', namespace='wikis')),
68-
url(r'^alerts/', include('api.alerts.urls', namespace='alerts')),
69+
url(r'^_waffle/', include('api.waffle.urls', namespace='waffle')),
6970
],
7071
),
7172
),

api/base/versioning.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
from rest_framework import versioning as drf_versioning
44
from rest_framework.compat import unicode_http_header
55
from rest_framework.utils.mediatypes import _MediaType
6+
from distutils.version import StrictVersion
67

78
from api.base import exceptions
89
from api.base import utils
910
from api.base.renderers import BrowsableAPIRendererNoForms
1011
from api.base.settings import LATEST_VERSIONS
1112

13+
# KEBAB_CASE_VERSION determines the API version in which kebab-case will begin being accepted.
14+
# Note that this version will not deprecate snake_case yet.
15+
KEBAB_CASE_VERSION = '2.18'
1216

1317
def get_major_version(version):
1418
return int(version.split('.')[0])
@@ -28,6 +32,12 @@ def get_latest_sub_version(major_version):
2832
# '2' --> '2.6'
2933
return LATEST_VERSIONS.get(major_version, None)
3034

35+
def get_kebab_snake_case_field(version, field):
36+
if StrictVersion(version) < StrictVersion(KEBAB_CASE_VERSION):
37+
return field.replace('-', '_')
38+
else:
39+
return field
40+
3141
class BaseVersioning(drf_versioning.BaseVersioning):
3242

3343
def __init__(self):

api/comments/serializers.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
AnonymizedRegexField,
1818
VersionedDateTimeField,
1919
)
20+
from api.base.versioning import get_kebab_snake_case_field
2021

2122

2223
class CommentReport(object):
@@ -223,7 +224,9 @@ class CommentReportSerializer(JSONAPISerializer):
223224
links = LinksField({'self': 'get_absolute_url'})
224225

225226
class Meta:
226-
type_ = 'comment_reports'
227+
@staticmethod
228+
def get_type(request):
229+
return get_kebab_snake_case_field(request.version, 'comment-reports')
227230

228231
def get_absolute_url(self, obj):
229232
return absolute_reverse(

api/files/serializers.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from api.base.utils import absolute_reverse, get_user_auth
3636
from api.base.exceptions import Conflict, InvalidModelValueError
3737
from api.base.schemas.utils import from_json
38+
from api.base.versioning import get_kebab_snake_case_field
3839

3940
class CheckoutField(ser.HyperlinkedRelatedField):
4041

@@ -423,7 +424,9 @@ def get_name(self, obj):
423424
return obj.get_basefilenode_version(file).version_name
424425

425426
class Meta:
426-
type_ = 'file_versions'
427+
@staticmethod
428+
def get_type(request):
429+
return get_kebab_snake_case_field(request.version, 'file-versions')
427430

428431
def self_url(self, obj):
429432
return absolute_reverse(
@@ -514,7 +517,9 @@ def get_absolute_url(self, obj):
514517
return obj.absolute_api_v2_url
515518

516519
class Meta:
517-
type_ = 'metadata_records'
520+
@staticmethod
521+
def get_type(request):
522+
return get_kebab_snake_case_field(request.version, 'metadata-records')
518523

519524

520525
def get_file_download_link(obj, version=None, view_only=None):

api/nodes/serializers.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
absolute_reverse, get_object_or_error,
2222
get_user_auth, is_truthy,
2323
)
24+
from api.base.versioning import get_kebab_snake_case_field
2425
from api.taxonomies.serializers import TaxonomizableSerializerMixin
2526
from django.apps import apps
2627
from django.conf import settings
@@ -886,7 +887,9 @@ def update(self, node, validated_data):
886887

887888
class NodeAddonSettingsSerializerBase(JSONAPISerializer):
888889
class Meta:
889-
type_ = 'node_addons'
890+
@staticmethod
891+
def get_type(request):
892+
return get_kebab_snake_case_field(request.version, 'node-addons')
890893

891894
id = ser.CharField(source='config.short_name', read_only=True)
892895
node_has_auth = ser.BooleanField(source='has_auth', read_only=True)
@@ -1313,7 +1316,9 @@ class NodeLinksSerializer(JSONAPISerializer):
13131316

13141317
)
13151318
class Meta:
1316-
type_ = 'node_links'
1319+
@staticmethod
1320+
def get_type(request):
1321+
return get_kebab_snake_case_field(request.version, 'node-links')
13171322

13181323
links = LinksField({
13191324
'self': 'get_absolute_url',
@@ -1520,7 +1525,9 @@ def create(self, validated_data):
15201525
return draft
15211526

15221527
class Meta:
1523-
type_ = 'draft_registrations'
1528+
@staticmethod
1529+
def get_type(request):
1530+
return get_kebab_snake_case_field(request.version, 'draft-registrations')
15241531

15251532

15261533
class DraftRegistrationDetailSerializer(DraftRegistrationSerializer):
@@ -1629,7 +1636,9 @@ def get_absolute_url(self, obj):
16291636
)
16301637

16311638
class Meta:
1632-
type_ = 'view_only_links'
1639+
@staticmethod
1640+
def get_type(request):
1641+
return get_kebab_snake_case_field(request.version, 'view-only-links')
16331642

16341643

16351644
class NodeViewOnlyLinkUpdateSerializer(NodeViewOnlyLinkSerializer):

api/schemas/serializers.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from rest_framework import serializers as ser
22
from api.base.serializers import (JSONAPISerializer, IDField, TypeField, LinksField)
3-
3+
from api.base.versioning import get_kebab_snake_case_field
44

55
class SchemaSerializer(JSONAPISerializer):
66

@@ -28,13 +28,17 @@ class RegistrationSchemaSerializer(SchemaSerializer):
2828
filterable_fields = ['active']
2929

3030
class Meta:
31-
type_ = 'registration_schemas'
31+
@staticmethod
32+
def get_type(request):
33+
return get_kebab_snake_case_field(request.version, 'registration-schemas')
3234

3335

3436
class FileMetadataSchemaSerializer(SchemaSerializer):
3537

3638
class Meta:
37-
type_ = 'file_metadata_schemas'
39+
@staticmethod
40+
def get_type(request):
41+
return get_kebab_snake_case_field(request.version, 'file-metadata-schemas')
3842

3943

4044
class DeprecatedMetaSchemaSerializer(SchemaSerializer):

api/sparse/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)