mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-07-29 17:39:48 +03:00
Merge branch 'master' into add_django_restql_to_third_party_packages
This commit is contained in:
commit
7cd283bb6d
|
@ -60,7 +60,8 @@ urlpatterns = [
|
||||||
# * Provide view name for use with `reverse()`.
|
# * Provide view name for use with `reverse()`.
|
||||||
path('openapi', get_schema_view(
|
path('openapi', get_schema_view(
|
||||||
title="Your Project",
|
title="Your Project",
|
||||||
description="API for all things …"
|
description="API for all things …",
|
||||||
|
version="1.0.0"
|
||||||
), name='openapi-schema'),
|
), 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.
|
* `title`: May be used to provide a descriptive title for the schema definition.
|
||||||
* `description`: Longer descriptive text.
|
* `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.
|
* `url`: May be used to pass a canonical base URL for the schema.
|
||||||
|
|
||||||
schema_view = get_schema_view(
|
schema_view = get_schema_view(
|
||||||
|
@ -137,6 +139,7 @@ Arguments:
|
||||||
|
|
||||||
* `title` **required**: The name of the API.
|
* `title` **required**: The name of the API.
|
||||||
* `description`: Longer descriptive text.
|
* `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.
|
* `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.
|
* `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`.
|
* `urlconf`: A URL conf module name to use when generating the schema. Defaults to `settings.ROOT_URLCONF`.
|
||||||
|
|
|
@ -218,7 +218,7 @@ in the `.validate()` method, or else in the view.
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
class BillingRecordSerializer(serializers.ModelSerializer):
|
class BillingRecordSerializer(serializers.ModelSerializer):
|
||||||
def validate(self, data):
|
def validate(self, attrs):
|
||||||
# Apply custom validation either here, or in the view.
|
# Apply custom validation either here, or in the view.
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -40,6 +40,35 @@ You can determine your currently installed version using `pip show`:
|
||||||
|
|
||||||
## 3.10.x series
|
## 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
|
### 3.10.0
|
||||||
|
|
||||||
**Date**: [15th July 2019][3.10.0-milestone]
|
**Date**: [15th July 2019][3.10.0-milestone]
|
||||||
|
|
|
@ -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-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.
|
* [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).
|
* [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
|
### 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
|
[django-restql]: https://github.com/yezyilomo/django-restql
|
||||||
[drf-flex-fields]: https://github.com/rsinger86/drf-flex-fields
|
[drf-flex-fields]: https://github.com/rsinger86/drf-flex-fields
|
||||||
[drf-action-serializer]: https://github.com/gregschmit/drf-action-serializer
|
[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
|
[djangorestframework-mvt]: https://github.com/corteva/djangorestframework-mvt
|
||||||
[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian
|
[django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian
|
||||||
|
|
|
@ -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
|
[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
|
[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/
|
[start-your-api-django-rest-framework-part-1]: https://www.youtube.com/watch?v=hqo2kk91WpE
|
||||||
[permissions-authentication-django-rest-framework-part-2]: https://godjango.com/43-permissions-authentication-django-rest-framework-part-2/
|
[permissions-authentication-django-rest-framework-part-2]: https://www.youtube.com/watch?v=R3xvUDUZxGU
|
||||||
[viewsets-and-routers-django-rest-framework-part-3]: https://godjango.com/45-viewsets-and-routers-django-rest-framework-part-3/
|
[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/
|
[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/
|
[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
|
[ember-and-django-part 1-video]: http://www.neckbeardrepublic.com/screencasts/ember-and-django-part-1
|
||||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__title__ = 'Django REST framework'
|
__title__ = 'Django REST framework'
|
||||||
__version__ = '3.10.1'
|
__version__ = '3.10.3'
|
||||||
__author__ = 'Tom Christie'
|
__author__ = 'Tom Christie'
|
||||||
__license__ = 'BSD 2-Clause'
|
__license__ = 'BSD 2-Clause'
|
||||||
__copyright__ = 'Copyright 2011-2019 Encode OSS Ltd'
|
__copyright__ = 'Copyright 2011-2019 Encode OSS Ltd'
|
||||||
|
|
|
@ -138,6 +138,9 @@ class BasePagination:
|
||||||
def get_paginated_response(self, data): # pragma: no cover
|
def get_paginated_response(self, data): # pragma: no cover
|
||||||
raise NotImplementedError('get_paginated_response() must be implemented.')
|
raise NotImplementedError('get_paginated_response() must be implemented.')
|
||||||
|
|
||||||
|
def get_paginated_response_schema(self, schema):
|
||||||
|
return schema
|
||||||
|
|
||||||
def to_html(self): # pragma: no cover
|
def to_html(self): # pragma: no cover
|
||||||
raise NotImplementedError('to_html() must be implemented to display page controls.')
|
raise NotImplementedError('to_html() must be implemented to display page controls.')
|
||||||
|
|
||||||
|
@ -222,6 +225,26 @@ class PageNumberPagination(BasePagination):
|
||||||
('results', data)
|
('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):
|
def get_page_size(self, request):
|
||||||
if self.page_size_query_param:
|
if self.page_size_query_param:
|
||||||
try:
|
try:
|
||||||
|
@ -369,6 +392,26 @@ class LimitOffsetPagination(BasePagination):
|
||||||
('results', data)
|
('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):
|
def get_limit(self, request):
|
||||||
if self.limit_query_param:
|
if self.limit_query_param:
|
||||||
try:
|
try:
|
||||||
|
@ -840,6 +883,22 @@ class CursorPagination(BasePagination):
|
||||||
('results', data)
|
('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):
|
def get_html_context(self):
|
||||||
return {
|
return {
|
||||||
'previous_url': self.get_previous_link(),
|
'previous_url': self.get_previous_link(),
|
||||||
|
|
|
@ -31,7 +31,8 @@ def get_schema_view(
|
||||||
title=None, url=None, description=None, urlconf=None, renderer_classes=None,
|
title=None, url=None, description=None, urlconf=None, renderer_classes=None,
|
||||||
public=False, patterns=None, generator_class=None,
|
public=False, patterns=None, generator_class=None,
|
||||||
authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES,
|
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.
|
Return a schema view.
|
||||||
"""
|
"""
|
||||||
|
@ -43,7 +44,7 @@ def get_schema_view(
|
||||||
|
|
||||||
generator = generator_class(
|
generator = generator_class(
|
||||||
title=title, url=url, description=description,
|
title=title, url=url, description=description,
|
||||||
urlconf=urlconf, patterns=patterns,
|
urlconf=urlconf, patterns=patterns, version=version
|
||||||
)
|
)
|
||||||
|
|
||||||
# Avoid import cycle on APIView
|
# Avoid import cycle on APIView
|
||||||
|
|
|
@ -124,7 +124,7 @@ class SchemaGenerator(BaseSchemaGenerator):
|
||||||
# Set by 'SCHEMA_COERCE_METHOD_NAMES'.
|
# Set by 'SCHEMA_COERCE_METHOD_NAMES'.
|
||||||
coerce_method_names = None
|
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 coreapi, '`coreapi` must be installed for schema support.'
|
||||||
assert coreschema, '`coreschema` must be installed for schema support.'
|
assert coreschema, '`coreschema` must be installed for schema support.'
|
||||||
|
|
||||||
|
|
|
@ -151,7 +151,7 @@ class BaseSchemaGenerator(object):
|
||||||
# Set by 'SCHEMA_COERCE_PATH_PK'.
|
# Set by 'SCHEMA_COERCE_PATH_PK'.
|
||||||
coerce_path_pk = None
|
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('/'):
|
if url and not url.endswith('/'):
|
||||||
url += '/'
|
url += '/'
|
||||||
|
|
||||||
|
@ -161,6 +161,7 @@ class BaseSchemaGenerator(object):
|
||||||
self.urlconf = urlconf
|
self.urlconf = urlconf
|
||||||
self.title = title
|
self.title = title
|
||||||
self.description = description
|
self.description = description
|
||||||
|
self.version = version
|
||||||
self.url = url
|
self.url = url
|
||||||
self.endpoints = None
|
self.endpoints = None
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ class SchemaGenerator(BaseSchemaGenerator):
|
||||||
def get_info(self):
|
def get_info(self):
|
||||||
info = {
|
info = {
|
||||||
'title': self.title,
|
'title': self.title,
|
||||||
'version': 'TODO',
|
'version': self.version,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.description is not None:
|
if self.description is not None:
|
||||||
|
@ -111,7 +111,7 @@ class AutoSchema(ViewInspector):
|
||||||
"""
|
"""
|
||||||
method_name = getattr(self.view, 'action', method.lower())
|
method_name = getattr(self.view, 'action', method.lower())
|
||||||
if is_list_view(path, method, self.view):
|
if is_list_view(path, method, self.view):
|
||||||
action = 'List'
|
action = 'list'
|
||||||
elif method_name not in self.method_mapping:
|
elif method_name not in self.method_mapping:
|
||||||
action = method_name
|
action = method_name
|
||||||
else:
|
else:
|
||||||
|
@ -135,10 +135,13 @@ class AutoSchema(ViewInspector):
|
||||||
name = name[:-7]
|
name = name[:-7]
|
||||||
elif name.endswith('View'):
|
elif name.endswith('View'):
|
||||||
name = name[:-4]
|
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)]
|
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'
|
name += 's'
|
||||||
|
|
||||||
return action + name
|
return action + name
|
||||||
|
@ -206,11 +209,10 @@ class AutoSchema(ViewInspector):
|
||||||
if not is_list_view(path, method, view):
|
if not is_list_view(path, method, view):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
pagination = getattr(view, 'pagination_class', None)
|
paginator = self._get_pagninator()
|
||||||
if not pagination:
|
if not paginator:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
paginator = view.pagination_class()
|
|
||||||
return paginator.get_schema_operation_parameters(view)
|
return paginator.get_schema_operation_parameters(view)
|
||||||
|
|
||||||
def _map_field(self, field):
|
def _map_field(self, field):
|
||||||
|
@ -378,7 +380,7 @@ class AutoSchema(ViewInspector):
|
||||||
schema['default'] = field.default
|
schema['default'] = field.default
|
||||||
if field.help_text:
|
if field.help_text:
|
||||||
schema['description'] = str(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
|
properties[field.field_name] = schema
|
||||||
|
|
||||||
|
@ -390,13 +392,11 @@ class AutoSchema(ViewInspector):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _map_field_validators(self, validators, schema):
|
def _map_field_validators(self, field, schema):
|
||||||
"""
|
"""
|
||||||
map field validators
|
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."
|
# "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
|
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#data-types
|
||||||
if isinstance(v, EmailValidator):
|
if isinstance(v, EmailValidator):
|
||||||
|
@ -406,9 +406,15 @@ class AutoSchema(ViewInspector):
|
||||||
if isinstance(v, RegexValidator):
|
if isinstance(v, RegexValidator):
|
||||||
schema['pattern'] = v.regex.pattern
|
schema['pattern'] = v.regex.pattern
|
||||||
elif isinstance(v, MaxLengthValidator):
|
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):
|
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):
|
elif isinstance(v, MaxValueValidator):
|
||||||
schema['maximum'] = v.limit_value
|
schema['maximum'] = v.limit_value
|
||||||
elif isinstance(v, MinValueValidator):
|
elif isinstance(v, MinValueValidator):
|
||||||
|
@ -423,6 +429,13 @@ class AutoSchema(ViewInspector):
|
||||||
schema['maximum'] = int(digits * '9') + 1
|
schema['maximum'] = int(digits * '9') + 1
|
||||||
schema['minimum'] = -schema['maximum']
|
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):
|
def _get_serializer(self, method, path):
|
||||||
view = self.view
|
view = self.view
|
||||||
|
|
||||||
|
@ -489,6 +502,9 @@ class AutoSchema(ViewInspector):
|
||||||
'type': 'array',
|
'type': 'array',
|
||||||
'items': item_schema,
|
'items': item_schema,
|
||||||
}
|
}
|
||||||
|
paginator = self._get_pagninator()
|
||||||
|
if paginator:
|
||||||
|
response_schema = paginator.get_paginated_response_schema(response_schema)
|
||||||
else:
|
else:
|
||||||
response_schema = item_schema
|
response_schema = item_schema
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ class TestOperationIntrospection(TestCase):
|
||||||
|
|
||||||
operation = inspector.get_operation(path, method)
|
operation = inspector.get_operation(path, method)
|
||||||
assert operation == {
|
assert operation == {
|
||||||
'operationId': 'ListExamples',
|
'operationId': 'listExamples',
|
||||||
'parameters': [],
|
'parameters': [],
|
||||||
'responses': {
|
'responses': {
|
||||||
'200': {
|
'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):
|
def test_delete_response_body_generation(self):
|
||||||
"""Test that a view's delete method generates a proper response body schema."""
|
"""Test that a view's delete method generates a proper response body schema."""
|
||||||
path = '/{id}/'
|
path = '/{id}/'
|
||||||
|
@ -288,15 +340,27 @@ class TestOperationIntrospection(TestCase):
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_retrieve_response_body_generation(self):
|
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}/'
|
path = '/{id}/'
|
||||||
method = 'GET'
|
method = 'GET'
|
||||||
|
|
||||||
|
class Pagination(pagination.BasePagination):
|
||||||
|
def get_paginated_response_schema(self, schema):
|
||||||
|
return {
|
||||||
|
'type': 'object',
|
||||||
|
'item': schema,
|
||||||
|
}
|
||||||
|
|
||||||
class ItemSerializer(serializers.Serializer):
|
class ItemSerializer(serializers.Serializer):
|
||||||
text = serializers.CharField()
|
text = serializers.CharField()
|
||||||
|
|
||||||
class View(generics.GenericAPIView):
|
class View(generics.GenericAPIView):
|
||||||
serializer_class = ItemSerializer
|
serializer_class = ItemSerializer
|
||||||
|
pagination_class = Pagination
|
||||||
|
|
||||||
view = create_view(
|
view = create_view(
|
||||||
View,
|
View,
|
||||||
|
@ -338,7 +402,7 @@ class TestOperationIntrospection(TestCase):
|
||||||
inspector.view = view
|
inspector.view = view
|
||||||
|
|
||||||
operationId = inspector._get_operation_id(path, method)
|
operationId = inspector._get_operation_id(path, method)
|
||||||
assert operationId == 'ListExamples'
|
assert operationId == 'listExamples'
|
||||||
|
|
||||||
def test_repeat_operation_ids(self):
|
def test_repeat_operation_ids(self):
|
||||||
router = routers.SimpleRouter()
|
router = routers.SimpleRouter()
|
||||||
|
@ -395,6 +459,9 @@ class TestOperationIntrospection(TestCase):
|
||||||
assert properties['string']['minLength'] == 2
|
assert properties['string']['minLength'] == 2
|
||||||
assert properties['string']['maxLength'] == 10
|
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']['pattern'] == r'[ABC]12{3}'
|
||||||
assert properties['regex']['description'] == 'must have an A, B, or C followed by 1222'
|
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 'openapi' in schema
|
||||||
assert 'paths' 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'
|
||||||
|
|
|
@ -85,6 +85,12 @@ class ExampleValidatedSerializer(serializers.Serializer):
|
||||||
),
|
),
|
||||||
help_text='must have an A, B, or C followed by 1222'
|
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)
|
decimal1 = serializers.DecimalField(max_digits=6, decimal_places=2)
|
||||||
decimal2 = serializers.DecimalField(max_digits=5, decimal_places=0,
|
decimal2 = serializers.DecimalField(max_digits=5, decimal_places=0,
|
||||||
validators=(DecimalValidator(max_digits=17, decimal_places=4),))
|
validators=(DecimalValidator(max_digits=17, decimal_places=4),))
|
||||||
|
|
|
@ -259,6 +259,37 @@ class TestPageNumberPagination:
|
||||||
with pytest.raises(exceptions.NotFound):
|
with pytest.raises(exceptions.NotFound):
|
||||||
self.paginate_queryset(request)
|
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:
|
class TestPageNumberPaginationOverride:
|
||||||
"""
|
"""
|
||||||
|
@ -535,6 +566,37 @@ class TestLimitOffset:
|
||||||
assert content.get('next') == next_url
|
assert content.get('next') == next_url
|
||||||
assert content.get('previous') == prev_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:
|
class CursorPaginationTestsMixin:
|
||||||
|
|
||||||
|
@ -834,6 +896,33 @@ class CursorPaginationTestsMixin:
|
||||||
assert current == [1, 1, 1, 1, 1]
|
assert current == [1, 1, 1, 1, 1]
|
||||||
assert next == [1, 2, 3, 4, 4]
|
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):
|
class TestCursorPagination(CursorPaginationTestsMixin):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue
Block a user