From f8c16441fa69850fd581c23807a8a0e38f3239d4 Mon Sep 17 00:00:00 2001 From: Reupen Shah Date: Tue, 3 Sep 2019 14:25:44 +0100 Subject: [PATCH 1/7] Add support for pagination in OpenAPI response schemas (#6867) Refs #6846 This provides a way for pagination classes to add pagination properties (`count`, `next`, `results` etc.) to OpenAPI response schemas. A new method `get_paginated_response_schema()` has been added to `BasePagination`. This method is intended to mirror `get_paginated_response()` (which takes a `list` and wraps it in a `dict`). Hence, `get_paginated_response_schema()` takes an unpaginated response schema (of type `array`) and wraps that with a schema object of type `object` containing the relevant properties that the pagination class adds to responses. The default implementation of `BasePagination.get_paginated_response_schema()` simply passes the schema through unmodified, for backwards compatibility. --- rest_framework/pagination.py | 59 ++++++++++++++++++++ rest_framework/schemas/openapi.py | 15 ++++-- tests/schemas/test_openapi.py | 66 ++++++++++++++++++++++- tests/test_pagination.py | 89 +++++++++++++++++++++++++++++++ 4 files changed, 225 insertions(+), 4 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index a68e8ab12..1a1ba2f65 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -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(), diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 0af7510cd..343d0b624 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -206,11 +206,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): @@ -423,6 +422,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 +495,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 diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 78a5609da..e0fe3c9ab 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -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, diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 11fd6844d..cd84c8151 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -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): """ From 1cc4be47b40a0d0c062fccf28853ca448a8522ab Mon Sep 17 00:00:00 2001 From: Dima Knivets Date: Tue, 3 Sep 2019 16:43:54 +0300 Subject: [PATCH 2/7] Fixed min/max attributes for serializers.ListField (#6866) --- rest_framework/schemas/openapi.py | 18 +++++++++++------- tests/schemas/test_openapi.py | 3 +++ tests/schemas/views.py | 6 ++++++ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 343d0b624..4fee95439 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -377,7 +377,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 @@ -389,13 +389,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): @@ -405,9 +403,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): diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index e0fe3c9ab..508e7dba8 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -459,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' diff --git a/tests/schemas/views.py b/tests/schemas/views.py index 273f1d30a..d1fc75eb8 100644 --- a/tests/schemas/views.py +++ b/tests/schemas/views.py @@ -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),)) From b3f032fb8f93dae5291e19a8117dd45e01b1c2e8 Mon Sep 17 00:00:00 2001 From: "Peter J. Farrell" Date: Tue, 3 Sep 2019 09:05:43 -0500 Subject: [PATCH 3/7] Fixed #6875 -- Made OpenAPI Schema operationId casing consistent. (#6876) --- rest_framework/schemas/openapi.py | 9 ++++++--- tests/schemas/test_openapi.py | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 4fee95439..b9ab104a0 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -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 diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 508e7dba8..187672ca2 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -80,7 +80,7 @@ class TestOperationIntrospection(TestCase): operation = inspector.get_operation(path, method) assert operation == { - 'operationId': 'ListExamples', + 'operationId': 'listExamples', 'parameters': [], 'responses': { '200': { @@ -402,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() From e57c1505fc4ed957332ae547a35c8713acfdf30c Mon Sep 17 00:00:00 2001 From: Yann Savary Date: Tue, 3 Sep 2019 16:07:30 +0200 Subject: [PATCH 4/7] Replaced 'TODO' hardcoded version info by a parameter with default '0.1.0' (#6899) --- docs/api-guide/schemas.md | 5 ++++- rest_framework/schemas/__init__.py | 5 +++-- rest_framework/schemas/coreapi.py | 2 +- rest_framework/schemas/generators.py | 3 ++- rest_framework/schemas/openapi.py | 2 +- tests/schemas/test_openapi.py | 14 ++++++++++++++ 6 files changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 040c2ed14..e1ac16a22 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -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`. diff --git a/rest_framework/schemas/__init__.py b/rest_framework/schemas/__init__.py index 8fdb2d86a..588680362 100644 --- a/rest_framework/schemas/__init__.py +++ b/rest_framework/schemas/__init__.py @@ -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 diff --git a/rest_framework/schemas/coreapi.py b/rest_framework/schemas/coreapi.py index 8d9099f2a..d811da2d8 100644 --- a/rest_framework/schemas/coreapi.py +++ b/rest_framework/schemas/coreapi.py @@ -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.' diff --git a/rest_framework/schemas/generators.py b/rest_framework/schemas/generators.py index a656c3ba5..0b789fc79 100644 --- a/rest_framework/schemas/generators.py +++ b/rest_framework/schemas/generators.py @@ -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='0.1.0'): 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 diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index b9ab104a0..ac846bf80 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -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: diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 187672ca2..d9375585b 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -556,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' From c0cf37e35dd51a1c5fb6e98d0bc3dcff0f60d412 Mon Sep 17 00:00:00 2001 From: Pramod Pujara Date: Wed, 4 Sep 2019 00:06:53 +0545 Subject: [PATCH 5/7] Update tutorial links (#6890) --- docs/community/tutorials-and-resources.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/community/tutorials-and-resources.md b/docs/community/tutorials-and-resources.md index a03d63a3c..7993f54fb 100644 --- a/docs/community/tutorials-and-resources.md +++ b/docs/community/tutorials-and-resources.md @@ -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 From 4b30b320144897965bb12fa07745ba02a0452884 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Sep 2019 12:53:50 +0100 Subject: [PATCH 6/7] Default OpenAPI version to the empty string (#6907) --- rest_framework/schemas/generators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/schemas/generators.py b/rest_framework/schemas/generators.py index 0b789fc79..77e92eeb8 100644 --- a/rest_framework/schemas/generators.py +++ b/rest_framework/schemas/generators.py @@ -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, version='0.1.0'): + def __init__(self, title=None, url=None, description=None, patterns=None, urlconf=None, version=''): if url and not url.endswith('/'): url += '/' From 89ac0a1c7ef6153312890376ac142674b28b04f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Sep 2019 14:28:25 +0100 Subject: [PATCH 7/7] Version 3.10.3 (#6908) * Version 3.10.3 * Version 3.10.3 release notes --- docs/community/release-notes.md | 29 +++++++++++++++++++++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index e3f3820d3..cdaa35044 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -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] diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 6a64c8b18..4d4225c96 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -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'