Merge branch 'master' into add_django_restql_to_third_party_packages

This commit is contained in:
Yezy Ilomo 2019-09-20 13:10:15 +03:00 committed by GitHub
commit 7cd283bb6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 315 additions and 28 deletions

View File

@ -60,7 +60,8 @@ urlpatterns = [
# * Provide view name for use with `reverse()`.
path('openapi', get_schema_view(
title="Your Project",
description="API for all things …"
description="API for all things …",
version="1.0.0"
), name='openapi-schema'),
# ...
]
@ -72,6 +73,7 @@ The `get_schema_view()` helper takes the following keyword arguments:
* `title`: May be used to provide a descriptive title for the schema definition.
* `description`: Longer descriptive text.
* `version`: The version of the API. Defaults to `0.1.0`.
* `url`: May be used to pass a canonical base URL for the schema.
schema_view = get_schema_view(
@ -137,6 +139,7 @@ Arguments:
* `title` **required**: The name of the API.
* `description`: Longer descriptive text.
* `version`: The version of the API. Defaults to `0.1.0`.
* `url`: The root URL of the API schema. This option is not required unless the schema is included under path prefix.
* `patterns`: A list of URLs to inspect when generating the schema. Defaults to the project's URL conf.
* `urlconf`: A URL conf module name to use when generating the schema. Defaults to `settings.ROOT_URLCONF`.

View File

@ -218,7 +218,7 @@ in the `.validate()` method, or else in the view.
For example:
class BillingRecordSerializer(serializers.ModelSerializer):
def validate(self, data):
def validate(self, attrs):
# Apply custom validation either here, or in the view.
class Meta:

View File

@ -40,6 +40,35 @@ You can determine your currently installed version using `pip show`:
## 3.10.x series
### 3.10.3
* Include API version in OpenAPI schema generation, defaulting to empty string.
* Add pagination properties to OpenAPI response schemas.
* Add missing "description" property to OpenAPI response schemas.
* Only include "required" for non-empty cases in OpenAPI schemas.
* Fix response schemas for "DELETE" case in OpenAPI schemas.
* Use an array type for list view response schemas.
* Use consistent `lowerInitialCamelCase` style in OpenAPI operation IDs.
* Fix `minLength`/`maxLength`/`minItems`/`maxItems` properties in OpenAPI schemas.
* Only call `FileField.url` once in serialization, for improved performance.
* Fix an edge case where throttling calcualtions could error after a configuration change.
* TODO
### 3.10.2
**Date**: 29th July 2019
* Various `OpenAPI` schema fixes.
* Ability to specify urlconf in include_docs_urls.
### 3.10.1
**Date**: 17th July 2019
* Don't include autocomplete fields on TokenAuth admin, since it forces constraints on custom user models & admin.
* Require `uritemplate` for OpenAPI schema generation, but not `coreapi`.
### 3.10.0
**Date**: [15th July 2019][3.10.0-milestone]

View File

@ -212,6 +212,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [drf-flex-fields][drf-flex-fields] - Serializer providing dynamic field expansion and sparse field sets via URL parameters.
* [drf-action-serializer][drf-action-serializer] - Serializer providing per-action fields config for use with ViewSets to prevent having to write multiple serializers.
* [django-restql][django-restql] - Turn your REST API into a GraphQL like API(It allows clients to control which fields will be sent in a response, supports both flat and nested fields, uses GraphQL like syntax).
* [djangorestframework-dataclasses][djangorestframework-dataclasses] - Serializer providing automatic field generation for Python dataclasses, like the built-in ModelSerializer does for models.
### Serializer fields
@ -350,5 +351,6 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[django-restql]: https://github.com/yezyilomo/django-restql
[drf-flex-fields]: https://github.com/rsinger86/drf-flex-fields
[drf-action-serializer]: https://github.com/gregschmit/drf-action-serializer
[djangorestframework-dataclasses]: https://github.com/oxan/djangorestframework-dataclasses
[djangorestframework-mvt]: https://github.com/corteva/djangorestframework-mvt
[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian

View File

@ -85,11 +85,11 @@ Want your Django REST Framework talk/tutorial/article to be added to our website
[beginners-guide-to-the-django-rest-framework]: https://code.tutsplus.com/tutorials/beginners-guide-to-the-django-rest-framework--cms-19786
[getting-started-with-django-rest-framework-and-angularjs]: https://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html
[getting-started-with-django-rest-framework-and-angularjs]: https://blog.kevinastone.com/django-rest-framework-and-angular-js
[end-to-end-web-app-with-django-rest-framework-angularjs]: https://mourafiq.com/2013/07/01/end-to-end-web-app-with-django-angular-1.html
[start-your-api-django-rest-framework-part-1]: https://godjango.com/41-start-your-api-django-rest-framework-part-1/
[permissions-authentication-django-rest-framework-part-2]: https://godjango.com/43-permissions-authentication-django-rest-framework-part-2/
[viewsets-and-routers-django-rest-framework-part-3]: https://godjango.com/45-viewsets-and-routers-django-rest-framework-part-3/
[start-your-api-django-rest-framework-part-1]: https://www.youtube.com/watch?v=hqo2kk91WpE
[permissions-authentication-django-rest-framework-part-2]: https://www.youtube.com/watch?v=R3xvUDUZxGU
[viewsets-and-routers-django-rest-framework-part-3]: https://www.youtube.com/watch?v=2d6w4DGQ4OU
[django-rest-framework-user-endpoint]: https://richardtier.com/2014/02/25/django-rest-framework-user-endpoint/
[check-credentials-using-django-rest-framework]: https://richardtier.com/2014/03/06/110/
[ember-and-django-part 1-video]: http://www.neckbeardrepublic.com/screencasts/ember-and-django-part-1

View File

@ -8,7 +8,7 @@ ______ _____ _____ _____ __
"""
__title__ = 'Django REST framework'
__version__ = '3.10.1'
__version__ = '3.10.3'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2019 Encode OSS Ltd'

View File

@ -138,6 +138,9 @@ class BasePagination:
def get_paginated_response(self, data): # pragma: no cover
raise NotImplementedError('get_paginated_response() must be implemented.')
def get_paginated_response_schema(self, schema):
return schema
def to_html(self): # pragma: no cover
raise NotImplementedError('to_html() must be implemented to display page controls.')
@ -222,6 +225,26 @@ class PageNumberPagination(BasePagination):
('results', data)
]))
def get_paginated_response_schema(self, schema):
return {
'type': 'object',
'properties': {
'count': {
'type': 'integer',
'example': 123,
},
'next': {
'type': 'string',
'nullable': True,
},
'previous': {
'type': 'string',
'nullable': True,
},
'results': schema,
},
}
def get_page_size(self, request):
if self.page_size_query_param:
try:
@ -369,6 +392,26 @@ class LimitOffsetPagination(BasePagination):
('results', data)
]))
def get_paginated_response_schema(self, schema):
return {
'type': 'object',
'properties': {
'count': {
'type': 'integer',
'example': 123,
},
'next': {
'type': 'string',
'nullable': True,
},
'previous': {
'type': 'string',
'nullable': True,
},
'results': schema,
},
}
def get_limit(self, request):
if self.limit_query_param:
try:
@ -840,6 +883,22 @@ class CursorPagination(BasePagination):
('results', data)
]))
def get_paginated_response_schema(self, schema):
return {
'type': 'object',
'properties': {
'next': {
'type': 'string',
'nullable': True,
},
'previous': {
'type': 'string',
'nullable': True,
},
'results': schema,
},
}
def get_html_context(self):
return {
'previous_url': self.get_previous_link(),

View File

@ -31,7 +31,8 @@ def get_schema_view(
title=None, url=None, description=None, urlconf=None, renderer_classes=None,
public=False, patterns=None, generator_class=None,
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES):
permission_classes=api_settings.DEFAULT_PERMISSION_CLASSES,
version=None):
"""
Return a schema view.
"""
@ -43,7 +44,7 @@ def get_schema_view(
generator = generator_class(
title=title, url=url, description=description,
urlconf=urlconf, patterns=patterns,
urlconf=urlconf, patterns=patterns, version=version
)
# Avoid import cycle on APIView

View File

@ -124,7 +124,7 @@ class SchemaGenerator(BaseSchemaGenerator):
# Set by 'SCHEMA_COERCE_METHOD_NAMES'.
coerce_method_names = None
def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None):
def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None, version=None):
assert coreapi, '`coreapi` must be installed for schema support.'
assert coreschema, '`coreschema` must be installed for schema support.'

View File

@ -151,7 +151,7 @@ class BaseSchemaGenerator(object):
# Set by 'SCHEMA_COERCE_PATH_PK'.
coerce_path_pk = None
def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None):
def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None, version=''):
if url and not url.endswith('/'):
url += '/'
@ -161,6 +161,7 @@ class BaseSchemaGenerator(object):
self.urlconf = urlconf
self.title = title
self.description = description
self.version = version
self.url = url
self.endpoints = None

View File

@ -24,7 +24,7 @@ class SchemaGenerator(BaseSchemaGenerator):
def get_info(self):
info = {
'title': self.title,
'version': 'TODO',
'version': self.version,
}
if self.description is not None:
@ -111,7 +111,7 @@ class AutoSchema(ViewInspector):
"""
method_name = getattr(self.view, 'action', method.lower())
if is_list_view(path, method, self.view):
action = 'List'
action = 'list'
elif method_name not in self.method_mapping:
action = method_name
else:
@ -135,10 +135,13 @@ class AutoSchema(ViewInspector):
name = name[:-7]
elif name.endswith('View'):
name = name[:-4]
if name.endswith(action): # ListView, UpdateAPIView, ThingDelete ...
# Due to camel-casing of classes and `action` being lowercase, apply title in order to find if action truly
# comes at the end of the name
if name.endswith(action.title()): # ListView, UpdateAPIView, ThingDelete ...
name = name[:-len(action)]
if action == 'List' and not name.endswith('s'): # ListThings instead of ListThing
if action == 'list' and not name.endswith('s'): # listThings instead of listThing
name += 's'
return action + name
@ -206,11 +209,10 @@ class AutoSchema(ViewInspector):
if not is_list_view(path, method, view):
return []
pagination = getattr(view, 'pagination_class', None)
if not pagination:
paginator = self._get_pagninator()
if not paginator:
return []
paginator = view.pagination_class()
return paginator.get_schema_operation_parameters(view)
def _map_field(self, field):
@ -378,7 +380,7 @@ class AutoSchema(ViewInspector):
schema['default'] = field.default
if field.help_text:
schema['description'] = str(field.help_text)
self._map_field_validators(field.validators, schema)
self._map_field_validators(field, schema)
properties[field.field_name] = schema
@ -390,13 +392,11 @@ class AutoSchema(ViewInspector):
return result
def _map_field_validators(self, validators, schema):
def _map_field_validators(self, field, schema):
"""
map field validators
:param list:validators: list of field validators
:param dict:schema: schema that the validators get added to
"""
for v in validators:
for v in field.validators:
# "Formats such as "email", "uuid", and so on, MAY be used even though undefined by this specification."
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#data-types
if isinstance(v, EmailValidator):
@ -406,9 +406,15 @@ class AutoSchema(ViewInspector):
if isinstance(v, RegexValidator):
schema['pattern'] = v.regex.pattern
elif isinstance(v, MaxLengthValidator):
schema['maxLength'] = v.limit_value
attr_name = 'maxLength'
if isinstance(field, serializers.ListField):
attr_name = 'maxItems'
schema[attr_name] = v.limit_value
elif isinstance(v, MinLengthValidator):
schema['minLength'] = v.limit_value
attr_name = 'minLength'
if isinstance(field, serializers.ListField):
attr_name = 'minItems'
schema[attr_name] = v.limit_value
elif isinstance(v, MaxValueValidator):
schema['maximum'] = v.limit_value
elif isinstance(v, MinValueValidator):
@ -423,6 +429,13 @@ class AutoSchema(ViewInspector):
schema['maximum'] = int(digits * '9') + 1
schema['minimum'] = -schema['maximum']
def _get_pagninator(self):
pagination_class = getattr(self.view, 'pagination_class', None)
if pagination_class:
return pagination_class()
return None
def _get_serializer(self, method, path):
view = self.view
@ -489,6 +502,9 @@ class AutoSchema(ViewInspector):
'type': 'array',
'items': item_schema,
}
paginator = self._get_pagninator()
if paginator:
response_schema = paginator.get_paginated_response_schema(response_schema)
else:
response_schema = item_schema

View File

@ -80,7 +80,7 @@ class TestOperationIntrospection(TestCase):
operation = inspector.get_operation(path, method)
assert operation == {
'operationId': 'ListExamples',
'operationId': 'listExamples',
'parameters': [],
'responses': {
'200': {
@ -264,6 +264,58 @@ class TestOperationIntrospection(TestCase):
},
}
def test_paginated_list_response_body_generation(self):
"""Test that pagination properties are added for a paginated list view."""
path = '/'
method = 'GET'
class Pagination(pagination.BasePagination):
def get_paginated_response_schema(self, schema):
return {
'type': 'object',
'item': schema,
}
class ItemSerializer(serializers.Serializer):
text = serializers.CharField()
class View(generics.GenericAPIView):
serializer_class = ItemSerializer
pagination_class = Pagination
view = create_view(
View,
method,
create_request(path),
)
inspector = AutoSchema()
inspector.view = view
responses = inspector._get_responses(path, method)
assert responses == {
'200': {
'description': '',
'content': {
'application/json': {
'schema': {
'type': 'object',
'item': {
'type': 'array',
'items': {
'properties': {
'text': {
'type': 'string',
},
},
'required': ['text'],
},
},
},
},
},
},
}
def test_delete_response_body_generation(self):
"""Test that a view's delete method generates a proper response body schema."""
path = '/{id}/'
@ -288,15 +340,27 @@ class TestOperationIntrospection(TestCase):
}
def test_retrieve_response_body_generation(self):
"""Test that a list of properties is returned for retrieve item views."""
"""
Test that a list of properties is returned for retrieve item views.
Pagination properties should not be added as the view represents a single item.
"""
path = '/{id}/'
method = 'GET'
class Pagination(pagination.BasePagination):
def get_paginated_response_schema(self, schema):
return {
'type': 'object',
'item': schema,
}
class ItemSerializer(serializers.Serializer):
text = serializers.CharField()
class View(generics.GenericAPIView):
serializer_class = ItemSerializer
pagination_class = Pagination
view = create_view(
View,
@ -338,7 +402,7 @@ class TestOperationIntrospection(TestCase):
inspector.view = view
operationId = inspector._get_operation_id(path, method)
assert operationId == 'ListExamples'
assert operationId == 'listExamples'
def test_repeat_operation_ids(self):
router = routers.SimpleRouter()
@ -395,6 +459,9 @@ class TestOperationIntrospection(TestCase):
assert properties['string']['minLength'] == 2
assert properties['string']['maxLength'] == 10
assert properties['lst']['minItems'] == 2
assert properties['lst']['maxItems'] == 10
assert properties['regex']['pattern'] == r'[ABC]12{3}'
assert properties['regex']['description'] == 'must have an A, B, or C followed by 1222'
@ -489,3 +556,17 @@ class TestGenerator(TestCase):
assert 'openapi' in schema
assert 'paths' in schema
def test_schema_information(self):
"""Construction of the top level dictionary."""
patterns = [
url(r'^example/?$', views.ExampleListView.as_view()),
]
generator = SchemaGenerator(patterns=patterns, title='My title', version='1.2.3', description='My description')
request = create_request('/')
schema = generator.get_schema(request=request)
assert schema['info']['title'] == 'My title'
assert schema['info']['version'] == '1.2.3'
assert schema['info']['description'] == 'My description'

View File

@ -85,6 +85,12 @@ class ExampleValidatedSerializer(serializers.Serializer):
),
help_text='must have an A, B, or C followed by 1222'
)
lst = serializers.ListField(
validators=(
MaxLengthValidator(limit_value=10),
MinLengthValidator(limit_value=2),
)
)
decimal1 = serializers.DecimalField(max_digits=6, decimal_places=2)
decimal2 = serializers.DecimalField(max_digits=5, decimal_places=0,
validators=(DecimalValidator(max_digits=17, decimal_places=4),))

View File

@ -259,6 +259,37 @@ class TestPageNumberPagination:
with pytest.raises(exceptions.NotFound):
self.paginate_queryset(request)
def test_get_paginated_response_schema(self):
unpaginated_schema = {
'type': 'object',
'item': {
'properties': {
'test-property': {
'type': 'integer',
},
},
},
}
assert self.pagination.get_paginated_response_schema(unpaginated_schema) == {
'type': 'object',
'properties': {
'count': {
'type': 'integer',
'example': 123,
},
'next': {
'type': 'string',
'nullable': True,
},
'previous': {
'type': 'string',
'nullable': True,
},
'results': unpaginated_schema,
},
}
class TestPageNumberPaginationOverride:
"""
@ -535,6 +566,37 @@ class TestLimitOffset:
assert content.get('next') == next_url
assert content.get('previous') == prev_url
def test_get_paginated_response_schema(self):
unpaginated_schema = {
'type': 'object',
'item': {
'properties': {
'test-property': {
'type': 'integer',
},
},
},
}
assert self.pagination.get_paginated_response_schema(unpaginated_schema) == {
'type': 'object',
'properties': {
'count': {
'type': 'integer',
'example': 123,
},
'next': {
'type': 'string',
'nullable': True,
},
'previous': {
'type': 'string',
'nullable': True,
},
'results': unpaginated_schema,
},
}
class CursorPaginationTestsMixin:
@ -834,6 +896,33 @@ class CursorPaginationTestsMixin:
assert current == [1, 1, 1, 1, 1]
assert next == [1, 2, 3, 4, 4]
def test_get_paginated_response_schema(self):
unpaginated_schema = {
'type': 'object',
'item': {
'properties': {
'test-property': {
'type': 'integer',
},
},
},
}
assert self.pagination.get_paginated_response_schema(unpaginated_schema) == {
'type': 'object',
'properties': {
'next': {
'type': 'string',
'nullable': True,
},
'previous': {
'type': 'string',
'nullable': True,
},
'results': unpaginated_schema,
},
}
class TestCursorPagination(CursorPaginationTestsMixin):
"""