From ed7f3c55f7647b19ce2358ebce861a0cee8944d5 Mon Sep 17 00:00:00 2001
From: Max Wittig
Date: Mon, 29 Jul 2019 14:20:11 +0200
Subject: [PATCH 001/407] docs(tutorial): add missing permission import in
viewsets
---
docs/tutorial/6-viewsets-and-routers.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md
index 11e24448f..3719bbf18 100644
--- a/docs/tutorial/6-viewsets-and-routers.md
+++ b/docs/tutorial/6-viewsets-and-routers.md
@@ -27,6 +27,7 @@ Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighl
from rest_framework.decorators import action
from rest_framework.response import Response
+ from rest_framework import permissions
class SnippetViewSet(viewsets.ModelViewSet):
"""
From dd33ebb4e20768b509e2360e05ea4396523b3866 Mon Sep 17 00:00:00 2001
From: Ryan P Kilby
Date: Sun, 29 Mar 2020 04:01:14 -0700
Subject: [PATCH 002/407] Add Request repr (#7239)
---
rest_framework/request.py | 7 +++++++
tests/test_request.py | 6 ++++++
2 files changed, 13 insertions(+)
diff --git a/rest_framework/request.py b/rest_framework/request.py
index ec4b749c2..43cdf5e9b 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -179,6 +179,13 @@ class Request:
forced_auth = ForcedAuthentication(force_user, force_token)
self.authenticators = (forced_auth,)
+ def __repr__(self):
+ return '<%s.%s: %s %r>' % (
+ self.__class__.__module__,
+ self.__class__.__name__,
+ self.method,
+ self.get_full_path())
+
def _default_negotiator(self):
return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS()
diff --git a/tests/test_request.py b/tests/test_request.py
index 0f682deb0..ed1b2e867 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -272,6 +272,12 @@ class TestSecure(TestCase):
class TestHttpRequest(TestCase):
+ def test_repr(self):
+ http_request = factory.get('/path')
+ request = Request(http_request)
+
+ assert repr(request) == ""
+
def test_attribute_access_proxy(self):
http_request = factory.get('/')
request = Request(http_request)
From 734c534dbb9c5758af335dba1fdbc2690388f076 Mon Sep 17 00:00:00 2001
From: David Szotten
Date: Tue, 31 Mar 2020 14:53:33 +0100
Subject: [PATCH 003/407] update release note to include full message (#7251)
the truncation changes the meaning somewhat..
---
docs/community/release-notes.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md
index 97ec774fe..0e634aa1e 100644
--- a/docs/community/release-notes.md
+++ b/docs/community/release-notes.md
@@ -174,7 +174,7 @@ Be sure to upgrade to Python 3 before upgrading to Django REST Framework 3.10.
* Add testing of Python 3.7 support [#6141][gh6141]
* Test using Django 2.1 final release. [#6109][gh6109]
* Added djangorestframework-datatables to third-party packages [#5931][gh5931]
-* Change ISO 8601 date format to exclude year/month [#5936][gh5936]
+* Change ISO 8601 date format to exclude year/month-only options [#5936][gh5936]
* Update all pypi.python.org URLs to pypi.org [#5942][gh5942]
* Ensure that html forms (multipart form data) respect optional fields [#5927][gh5927]
* Allow hashing of ErrorDetail. [#5932][gh5932]
From a7a362813b63894a6243180dd8c3374ffc8330ea Mon Sep 17 00:00:00 2001
From: Rotzbua
Date: Mon, 6 Apr 2020 15:54:30 +0200
Subject: [PATCH 004/407] Update optional dependencies list. (#7243)
Co-authored-by: Ryan P Kilby
---
docs/index.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/docs/index.md b/docs/index.md
index bccc1fb46..899118896 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -93,7 +93,7 @@ each Python and Django series.
The following packages are optional:
-* [coreapi][coreapi] (1.32.0+) - Schema generation support.
+* [PyYAML][pyyaml], [uritemplate][uriteemplate] (5.1+, 3.0.0+) - Schema generation support.
* [Markdown][markdown] (3.0.0+) - Markdown support for the browsable API.
* [Pygments][pygments] (2.4.0+) - Add syntax highlighting to Markdown processing.
* [django-filter][django-filter] (1.0.1+) - Filtering support.
@@ -237,7 +237,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[redhat]: https://www.redhat.com/
[heroku]: https://www.heroku.com/
[eventbrite]: https://www.eventbrite.co.uk/about/
-[coreapi]: https://pypi.org/project/coreapi/
+[pyyaml]: https://pypi.org/project/PyYAML/
+[uriteemplate]: https://pypi.org/project/uritemplate/
[markdown]: https://pypi.org/project/Markdown/
[pygments]: https://pypi.org/project/Pygments/
[django-filter]: https://pypi.org/project/django-filter/
From 0c8eb91737513029a7a12e719d7de8d68c8181f3 Mon Sep 17 00:00:00 2001
From: tsurutan
Date: Tue, 7 Apr 2020 00:09:23 +0900
Subject: [PATCH 005/407] Fixed docs' custom render example. (#7171)
---
docs/api-guide/renderers.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index a3321e860..a508a9ff9 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -273,7 +273,7 @@ By default this will include the following keys: `view`, `request`, `response`,
The following is an example plaintext renderer that will return a response with the `data` parameter as the content of the response.
- from django.utils.encoding import smart_unicode
+ from django.utils.encoding import smart_text
from rest_framework import renderers
@@ -282,7 +282,7 @@ The following is an example plaintext renderer that will return a response with
format = 'txt'
def render(self, data, media_type=None, renderer_context=None):
- return data.encode(self.charset)
+ return smart_text(data, encoding=self.charset)
## Setting the character set
From e6c1afbcf97e7080c0632ac9e2d60a6d10bd1a5c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?=
Date: Tue, 7 Apr 2020 10:28:09 +0000
Subject: [PATCH 006/407] Tighten checks for invalid field name in ordering
(#7259)
Django master removed the ORDER_PATTERN regex with commit
https://github.com/django/django/commit/513948735b799239f3ef8c89397592445e1a0cd5
---
rest_framework/filters.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index c15723ec3..8ef01743c 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -8,7 +8,6 @@ from functools import reduce
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.db.models.constants import LOOKUP_SEP
-from django.db.models.sql.constants import ORDER_PATTERN
from django.template import loader
from django.utils.encoding import force_str
from django.utils.translation import gettext_lazy as _
@@ -256,7 +255,13 @@ class OrderingFilter(BaseFilterBackend):
def remove_invalid_fields(self, queryset, fields, view, request):
valid_fields = [item[0] for item in self.get_valid_fields(queryset, view, {'request': request})]
- return [term for term in fields if term.lstrip('-') in valid_fields and ORDER_PATTERN.match(term)]
+
+ def term_valid(term):
+ if term.startswith("-"):
+ term = term[1:]
+ return term in valid_fields
+
+ return [term for term in fields if term_valid(term)]
def filter_queryset(self, request, queryset, view):
ordering = self.get_ordering(request, queryset, view)
From 41f27c3b43c16dd0dcc4cba387ec4450ac6ec10c Mon Sep 17 00:00:00 2001
From: Dhaval Mehta <20968146+dhaval-mehta@users.noreply.github.com>
Date: Thu, 9 Apr 2020 22:40:50 +0530
Subject: [PATCH 007/407] Schemas: Don't generate component for DELETE method.
(#7229)
---
rest_framework/schemas/openapi.py | 4 ++++
tests/schemas/test_openapi.py | 12 ++++++++++++
2 files changed, 16 insertions(+)
diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py
index 1d0ec35d5..088f792c4 100644
--- a/rest_framework/schemas/openapi.py
+++ b/rest_framework/schemas/openapi.py
@@ -185,6 +185,10 @@ class AutoSchema(ViewInspector):
"""
Return components with their properties from the serializer.
"""
+
+ if method.lower() == 'delete':
+ return {}
+
serializer = self._get_serializer(path, method)
if not isinstance(serializer, serializers.Serializer):
diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py
index c9f6d967e..43101635d 100644
--- a/tests/schemas/test_openapi.py
+++ b/tests/schemas/test_openapi.py
@@ -1087,3 +1087,15 @@ class TestGenerator(TestCase):
assert 'components' in schema
assert 'schemas' in schema['components']
assert 'Duplicate' in schema['components']['schemas']
+
+ def test_component_should_not_be_generated_for_delete_method(self):
+ class ExampleView(generics.DestroyAPIView):
+ schema = AutoSchema(operation_id_base='example')
+
+ url_patterns = [
+ url(r'^example/?$', ExampleView.as_view()),
+ ]
+ generator = SchemaGenerator(patterns=url_patterns)
+ schema = generator.get_schema(request=create_request('/'))
+ assert 'components' not in schema
+ assert 'content' not in schema['paths']['/example/']['delete']['responses']['204']
From 603aac7db10671dea1975ea3023b5488815aa1ca Mon Sep 17 00:00:00 2001
From: Clinton Blackburn
Date: Thu, 9 Apr 2020 10:16:17 -0700
Subject: [PATCH 008/407] Corrected OpenAPI schema type for DecimalField
(#7254)
---
rest_framework/schemas/openapi.py | 20 ++++++++++++++------
tests/schemas/test_openapi.py | 10 ++++++++++
tests/schemas/views.py | 8 ++++++--
3 files changed, 30 insertions(+), 8 deletions(-)
diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py
index 088f792c4..7af013444 100644
--- a/rest_framework/schemas/openapi.py
+++ b/rest_framework/schemas/openapi.py
@@ -15,6 +15,7 @@ from django.utils.encoding import force_str
from rest_framework import exceptions, renderers, serializers
from rest_framework.compat import uritemplate
from rest_framework.fields import _UnvalidatedField, empty
+from rest_framework.settings import api_settings
from .generators import BaseSchemaGenerator
from .inspectors import ViewInspector
@@ -446,11 +447,17 @@ class AutoSchema(ViewInspector):
content['format'] = field.protocol
return content
- # DecimalField has multipleOf based on decimal_places
if isinstance(field, serializers.DecimalField):
- content = {
- 'type': 'number'
- }
+ if getattr(field, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING):
+ content = {
+ 'type': 'string',
+ 'format': 'decimal',
+ }
+ else:
+ content = {
+ 'type': 'number'
+ }
+
if field.decimal_places:
content['multipleOf'] = float('.' + (field.decimal_places - 1) * '0' + '1')
if field.max_whole_digits:
@@ -461,7 +468,7 @@ class AutoSchema(ViewInspector):
if isinstance(field, serializers.FloatField):
content = {
- 'type': 'number'
+ 'type': 'number',
}
self._map_min_max(field, content)
return content
@@ -560,7 +567,8 @@ class AutoSchema(ViewInspector):
schema['maximum'] = v.limit_value
elif isinstance(v, MinValueValidator):
schema['minimum'] = v.limit_value
- elif isinstance(v, DecimalValidator):
+ elif isinstance(v, DecimalValidator) and \
+ not getattr(field, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING):
if v.decimal_places:
schema['multipleOf'] = float('.' + (v.decimal_places - 1) * '0' + '1')
if v.max_digits:
diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py
index 43101635d..774636972 100644
--- a/tests/schemas/test_openapi.py
+++ b/tests/schemas/test_openapi.py
@@ -838,6 +838,16 @@ class TestOperationIntrospection(TestCase):
assert properties['decimal2']['type'] == 'number'
assert properties['decimal2']['multipleOf'] == .0001
+ assert properties['decimal3'] == {
+ 'type': 'string', 'format': 'decimal', 'maximum': 1000000, 'minimum': -1000000, 'multipleOf': 0.01
+ }
+ assert properties['decimal4'] == {
+ 'type': 'string', 'format': 'decimal', 'maximum': 1000000, 'minimum': -1000000, 'multipleOf': 0.01
+ }
+ assert properties['decimal5'] == {
+ 'type': 'string', 'format': 'decimal', 'maximum': 10000, 'minimum': -10000, 'multipleOf': 0.01
+ }
+
assert properties['email']['type'] == 'string'
assert properties['email']['format'] == 'email'
assert properties['email']['default'] == 'foo@bar.com'
diff --git a/tests/schemas/views.py b/tests/schemas/views.py
index 5645f59bf..18b3beae4 100644
--- a/tests/schemas/views.py
+++ b/tests/schemas/views.py
@@ -119,9 +119,13 @@ class ExampleValidatedSerializer(serializers.Serializer):
MinLengthValidator(limit_value=2),
)
)
- decimal1 = serializers.DecimalField(max_digits=6, decimal_places=2)
- decimal2 = serializers.DecimalField(max_digits=5, decimal_places=0,
+ decimal1 = serializers.DecimalField(max_digits=6, decimal_places=2, coerce_to_string=False)
+ decimal2 = serializers.DecimalField(max_digits=5, decimal_places=0, coerce_to_string=False,
validators=(DecimalValidator(max_digits=17, decimal_places=4),))
+ decimal3 = serializers.DecimalField(max_digits=8, decimal_places=2, coerce_to_string=True)
+ decimal4 = serializers.DecimalField(max_digits=8, decimal_places=2, coerce_to_string=True,
+ validators=(DecimalValidator(max_digits=17, decimal_places=4),))
+ decimal5 = serializers.DecimalField(max_digits=6, decimal_places=2)
email = serializers.EmailField(default='foo@bar.com')
url = serializers.URLField(default='http://www.example.com', allow_null=True)
uuid = serializers.UUIDField()
From 1872bde4625987202d23029d1f492253cbd43b58 Mon Sep 17 00:00:00 2001
From: Dhaval Mehta <20968146+dhaval-mehta@users.noreply.github.com>
Date: Thu, 9 Apr 2020 22:48:00 +0530
Subject: [PATCH 009/407] Schemas: Improved decimal handling when mapping
ChoiceField. (#7264)
---
rest_framework/schemas/openapi.py | 35 +++++++++++++++----------------
tests/schemas/test_openapi.py | 13 ++++++++++++
2 files changed, 30 insertions(+), 18 deletions(-)
diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py
index 7af013444..8c948048e 100644
--- a/rest_framework/schemas/openapi.py
+++ b/rest_framework/schemas/openapi.py
@@ -330,30 +330,29 @@ class AutoSchema(ViewInspector):
def _map_choicefield(self, field):
choices = list(OrderedDict.fromkeys(field.choices)) # preserve order and remove duplicates
- if all(isinstance(choice, bool) for choice in choices):
- type = 'boolean'
- elif all(isinstance(choice, int) for choice in choices):
- type = 'integer'
- elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer`
- # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
- type = 'number'
- elif all(isinstance(choice, str) for choice in choices):
- type = 'string'
- else:
- type = None
-
mapping = {
# The value of `enum` keyword MUST be an array and SHOULD be unique.
# Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.20
'enum': choices
}
- # If We figured out `type` then and only then we should set it. It must be a string.
- # Ref: https://swagger.io/docs/specification/data-models/data-types/#mixed-type
- # It is optional but it can not be null.
- # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
- if type:
- mapping['type'] = type
+ if all(isinstance(choice, bool) for choice in choices):
+ mapping['type'] = 'boolean'
+ elif all(isinstance(choice, int) for choice in choices):
+ mapping['type'] = 'integer'
+ elif all(isinstance(choice, Decimal) for choice in choices):
+ mapping['format'] = 'decimal'
+ if api_settings.COERCE_DECIMAL_TO_STRING:
+ mapping['enum'] = [str(choice) for choice in mapping['enum']]
+ mapping['type'] = 'string'
+ else:
+ mapping['type'] = 'number'
+ elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer`
+ # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
+ mapping['type'] = 'number'
+ elif all(isinstance(choice, str) for choice in choices):
+ mapping['type'] = 'string'
+
return mapping
def _map_field(self, field):
diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py
index 774636972..1eef8fa2f 100644
--- a/tests/schemas/test_openapi.py
+++ b/tests/schemas/test_openapi.py
@@ -1,5 +1,6 @@
import uuid
import warnings
+from decimal import Decimal
import pytest
from django.conf.urls import url
@@ -78,6 +79,9 @@ class TestFieldMapping(TestCase):
(1, 'One'), (2, 'Two'), (3, 'Three'), (2, 'Two'), (3, 'Three'), (1, 'One'),
])),
{'items': {'enum': [1, 2, 3], 'type': 'integer'}, 'type': 'array'}),
+ (serializers.ListField(child=
+ serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')])),
+ {'items': {'enum': ['1.111', '2.222'], 'type': 'string', 'format': 'decimal'}, 'type': 'array'}),
(serializers.IntegerField(min_value=2147483648),
{'type': 'integer', 'minimum': 2147483648, 'format': 'int64'}),
]
@@ -85,6 +89,15 @@ class TestFieldMapping(TestCase):
with self.subTest(field=field):
assert inspector._map_field(field) == mapping
+ @override_settings(REST_FRAMEWORK={'COERCE_DECIMAL_TO_STRING': False})
+ def test_decimal_schema_for_choice_field(self):
+ inspector = AutoSchema()
+ field = serializers.ListField(
+ child=serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')]))
+ mapping = {'items': {'enum': [Decimal('1.111'), Decimal('2.222')], 'type': 'number'}, 'type': 'array'}
+ assert inspector._map_field(field) == mapping
+
+
def test_lazy_string_field(self):
class ItemSerializer(serializers.Serializer):
text = serializers.CharField(help_text=_('lazy string'))
From b1bfff4f1c2ca3ad2ad844931747afec135b3fb8 Mon Sep 17 00:00:00 2001
From: Carlton Gibson
Date: Thu, 9 Apr 2020 19:35:46 +0200
Subject: [PATCH 010/407] Revert "Schemas: Improved decimal handling when
mapping ChoiceField. (#7264)"
This reverts commit 1872bde4625987202d23029d1f492253cbd43b58.
---
rest_framework/schemas/openapi.py | 35 ++++++++++++++++---------------
tests/schemas/test_openapi.py | 13 ------------
2 files changed, 18 insertions(+), 30 deletions(-)
diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py
index 8c948048e..7af013444 100644
--- a/rest_framework/schemas/openapi.py
+++ b/rest_framework/schemas/openapi.py
@@ -330,29 +330,30 @@ class AutoSchema(ViewInspector):
def _map_choicefield(self, field):
choices = list(OrderedDict.fromkeys(field.choices)) # preserve order and remove duplicates
+ if all(isinstance(choice, bool) for choice in choices):
+ type = 'boolean'
+ elif all(isinstance(choice, int) for choice in choices):
+ type = 'integer'
+ elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer`
+ # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
+ type = 'number'
+ elif all(isinstance(choice, str) for choice in choices):
+ type = 'string'
+ else:
+ type = None
+
mapping = {
# The value of `enum` keyword MUST be an array and SHOULD be unique.
# Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.20
'enum': choices
}
- if all(isinstance(choice, bool) for choice in choices):
- mapping['type'] = 'boolean'
- elif all(isinstance(choice, int) for choice in choices):
- mapping['type'] = 'integer'
- elif all(isinstance(choice, Decimal) for choice in choices):
- mapping['format'] = 'decimal'
- if api_settings.COERCE_DECIMAL_TO_STRING:
- mapping['enum'] = [str(choice) for choice in mapping['enum']]
- mapping['type'] = 'string'
- else:
- mapping['type'] = 'number'
- elif all(isinstance(choice, (int, float, Decimal)) for choice in choices): # `number` includes `integer`
- # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
- mapping['type'] = 'number'
- elif all(isinstance(choice, str) for choice in choices):
- mapping['type'] = 'string'
-
+ # If We figured out `type` then and only then we should set it. It must be a string.
+ # Ref: https://swagger.io/docs/specification/data-models/data-types/#mixed-type
+ # It is optional but it can not be null.
+ # Ref: https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5.21
+ if type:
+ mapping['type'] = type
return mapping
def _map_field(self, field):
diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py
index 1eef8fa2f..774636972 100644
--- a/tests/schemas/test_openapi.py
+++ b/tests/schemas/test_openapi.py
@@ -1,6 +1,5 @@
import uuid
import warnings
-from decimal import Decimal
import pytest
from django.conf.urls import url
@@ -79,9 +78,6 @@ class TestFieldMapping(TestCase):
(1, 'One'), (2, 'Two'), (3, 'Three'), (2, 'Two'), (3, 'Three'), (1, 'One'),
])),
{'items': {'enum': [1, 2, 3], 'type': 'integer'}, 'type': 'array'}),
- (serializers.ListField(child=
- serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')])),
- {'items': {'enum': ['1.111', '2.222'], 'type': 'string', 'format': 'decimal'}, 'type': 'array'}),
(serializers.IntegerField(min_value=2147483648),
{'type': 'integer', 'minimum': 2147483648, 'format': 'int64'}),
]
@@ -89,15 +85,6 @@ class TestFieldMapping(TestCase):
with self.subTest(field=field):
assert inspector._map_field(field) == mapping
- @override_settings(REST_FRAMEWORK={'COERCE_DECIMAL_TO_STRING': False})
- def test_decimal_schema_for_choice_field(self):
- inspector = AutoSchema()
- field = serializers.ListField(
- child=serializers.ChoiceField(choices=[(Decimal('1.111'), 'one'), (Decimal('2.222'), 'two')]))
- mapping = {'items': {'enum': [Decimal('1.111'), Decimal('2.222')], 'type': 'number'}, 'type': 'array'}
- assert inspector._map_field(field) == mapping
-
-
def test_lazy_string_field(self):
class ItemSerializer(serializers.Serializer):
text = serializers.CharField(help_text=_('lazy string'))
From d45e0005f32bc246a49b209836d233f3d23d77b0 Mon Sep 17 00:00:00 2001
From: Carlton Gibson
Date: Mon, 6 Apr 2020 16:30:49 +0200
Subject: [PATCH 011/407] Updated deprecation warnings for 3.12
---
rest_framework/__init__.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index b6f3f65ce..8f2bc4466 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -25,9 +25,9 @@ ISO_8601 = 'iso-8601'
default_app_config = 'rest_framework.apps.RestFrameworkConfig'
-class RemovedInDRF312Warning(DeprecationWarning):
+class RemovedInDRF313Warning(DeprecationWarning):
pass
-class RemovedInDRF313Warning(PendingDeprecationWarning):
+class RemovedInDRF314Warning(PendingDeprecationWarning):
pass
From b2497fc2456c607a3c639ed2355c28dac672a70f Mon Sep 17 00:00:00 2001
From: Carlton Gibson
Date: Mon, 6 Apr 2020 17:03:10 +0200
Subject: [PATCH 012/407] Convert openapi.AutoSchema methods to public API.
---
rest_framework/schemas/openapi.py | 169 +++++++++++++++++++++++-------
tests/schemas/test_openapi.py | 30 +++---
2 files changed, 148 insertions(+), 51 deletions(-)
diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py
index 7af013444..9b3082822 100644
--- a/rest_framework/schemas/openapi.py
+++ b/rest_framework/schemas/openapi.py
@@ -12,7 +12,9 @@ from django.core.validators import (
from django.db import models
from django.utils.encoding import force_str
-from rest_framework import exceptions, renderers, serializers
+from rest_framework import (
+ RemovedInDRF314Warning, exceptions, renderers, serializers
+)
from rest_framework.compat import uritemplate
from rest_framework.fields import _UnvalidatedField, empty
from rest_framework.settings import api_settings
@@ -146,15 +148,15 @@ class AutoSchema(ViewInspector):
operation['description'] = self.get_description(path, method)
parameters = []
- parameters += self._get_path_parameters(path, method)
- parameters += self._get_pagination_parameters(path, method)
- parameters += self._get_filter_parameters(path, method)
+ parameters += self.get_path_parameters(path, method)
+ parameters += self.get_pagination_parameters(path, method)
+ parameters += self.get_filter_parameters(path, method)
operation['parameters'] = parameters
- request_body = self._get_request_body(path, method)
+ request_body = self.get_request_body(path, method)
if request_body:
operation['requestBody'] = request_body
- operation['responses'] = self._get_responses(path, method)
+ operation['responses'] = self.get_responses(path, method)
operation['tags'] = self.get_tags(path, method)
return operation
@@ -190,14 +192,14 @@ class AutoSchema(ViewInspector):
if method.lower() == 'delete':
return {}
- serializer = self._get_serializer(path, method)
+ serializer = self.get_serializer(path, method)
if not isinstance(serializer, serializers.Serializer):
return {}
component_name = self.get_component_name(serializer)
- content = self._map_serializer(serializer)
+ content = self.map_serializer(serializer)
return {component_name: content}
def _to_camel_case(self, snake_str):
@@ -220,8 +222,8 @@ class AutoSchema(ViewInspector):
name = model.__name__
# Try with the serializer class name
- elif self._get_serializer(path, method) is not None:
- name = self._get_serializer(path, method).__class__.__name__
+ elif self.get_serializer(path, method) is not None:
+ name = self.get_serializer(path, method).__class__.__name__
if name.endswith('Serializer'):
name = name[:-10]
@@ -259,7 +261,7 @@ class AutoSchema(ViewInspector):
return action + name
- def _get_path_parameters(self, path, method):
+ def get_path_parameters(self, path, method):
"""
Return a list of parameters from templated path variables.
"""
@@ -295,15 +297,15 @@ class AutoSchema(ViewInspector):
return parameters
- def _get_filter_parameters(self, path, method):
- if not self._allows_filters(path, method):
+ def get_filter_parameters(self, path, method):
+ if not self.allows_filters(path, method):
return []
parameters = []
for filter_backend in self.view.filter_backends:
parameters += filter_backend().get_schema_operation_parameters(self.view)
return parameters
- def _allows_filters(self, path, method):
+ def allows_filters(self, path, method):
"""
Determine whether to include filter Fields in schema.
@@ -316,19 +318,19 @@ class AutoSchema(ViewInspector):
return self.view.action in ["list", "retrieve", "update", "partial_update", "destroy"]
return method.lower() in ["get", "put", "patch", "delete"]
- def _get_pagination_parameters(self, path, method):
+ def get_pagination_parameters(self, path, method):
view = self.view
if not is_list_view(path, method, view):
return []
- paginator = self._get_paginator()
+ paginator = self.get_paginator()
if not paginator:
return []
return paginator.get_schema_operation_parameters(view)
- def _map_choicefield(self, field):
+ def map_choicefield(self, field):
choices = list(OrderedDict.fromkeys(field.choices)) # preserve order and remove duplicates
if all(isinstance(choice, bool) for choice in choices):
type = 'boolean'
@@ -356,16 +358,16 @@ class AutoSchema(ViewInspector):
mapping['type'] = type
return mapping
- def _map_field(self, field):
+ def map_field(self, field):
# Nested Serializers, `many` or not.
if isinstance(field, serializers.ListSerializer):
return {
'type': 'array',
- 'items': self._map_serializer(field.child)
+ 'items': self.map_serializer(field.child)
}
if isinstance(field, serializers.Serializer):
- data = self._map_serializer(field)
+ data = self.map_serializer(field)
data['type'] = 'object'
return data
@@ -373,7 +375,7 @@ class AutoSchema(ViewInspector):
if isinstance(field, serializers.ManyRelatedField):
return {
'type': 'array',
- 'items': self._map_field(field.child_relation)
+ 'items': self.map_field(field.child_relation)
}
if isinstance(field, serializers.PrimaryKeyRelatedField):
model = getattr(field.queryset, 'model', None)
@@ -389,11 +391,11 @@ class AutoSchema(ViewInspector):
if isinstance(field, serializers.MultipleChoiceField):
return {
'type': 'array',
- 'items': self._map_choicefield(field)
+ 'items': self.map_choicefield(field)
}
if isinstance(field, serializers.ChoiceField):
- return self._map_choicefield(field)
+ return self.map_choicefield(field)
# ListField.
if isinstance(field, serializers.ListField):
@@ -402,7 +404,7 @@ class AutoSchema(ViewInspector):
'items': {},
}
if not isinstance(field.child, _UnvalidatedField):
- mapping['items'] = self._map_field(field.child)
+ mapping['items'] = self.map_field(field.child)
return mapping
# DateField and DateTimeField type is string
@@ -504,7 +506,7 @@ class AutoSchema(ViewInspector):
if field.min_value:
content['minimum'] = field.min_value
- def _map_serializer(self, serializer):
+ def map_serializer(self, serializer):
# Assuming we have a valid serializer instance.
required = []
properties = {}
@@ -516,7 +518,7 @@ class AutoSchema(ViewInspector):
if field.required:
required.append(field.field_name)
- schema = self._map_field(field)
+ schema = self.map_field(field)
if field.read_only:
schema['readOnly'] = True
if field.write_only:
@@ -527,7 +529,7 @@ class AutoSchema(ViewInspector):
schema['default'] = field.default
if field.help_text:
schema['description'] = str(field.help_text)
- self._map_field_validators(field, schema)
+ self.map_field_validators(field, schema)
properties[field.field_name] = schema
@@ -540,7 +542,7 @@ class AutoSchema(ViewInspector):
return result
- def _map_field_validators(self, field, schema):
+ def map_field_validators(self, field, schema):
"""
map field validators
"""
@@ -578,7 +580,7 @@ class AutoSchema(ViewInspector):
schema['maximum'] = int(digits * '9') + 1
schema['minimum'] = -schema['maximum']
- def _get_paginator(self):
+ def get_paginator(self):
pagination_class = getattr(self.view, 'pagination_class', None)
if pagination_class:
return pagination_class()
@@ -596,7 +598,7 @@ class AutoSchema(ViewInspector):
media_types.append(renderer.media_type)
return media_types
- def _get_serializer(self, path, method):
+ def get_serializer(self, path, method):
view = self.view
if not hasattr(view, 'get_serializer'):
@@ -614,13 +616,13 @@ class AutoSchema(ViewInspector):
def _get_reference(self, serializer):
return {'$ref': '#/components/schemas/{}'.format(self.get_component_name(serializer))}
- def _get_request_body(self, path, method):
+ def get_request_body(self, path, method):
if method not in ('PUT', 'PATCH', 'POST'):
return {}
self.request_media_types = self.map_parsers(path, method)
- serializer = self._get_serializer(path, method)
+ serializer = self.get_serializer(path, method)
if not isinstance(serializer, serializers.Serializer):
item_schema = {}
@@ -634,8 +636,7 @@ class AutoSchema(ViewInspector):
}
}
- def _get_responses(self, path, method):
- # TODO: Handle multiple codes and pagination classes.
+ def get_responses(self, path, method):
if method == 'DELETE':
return {
'204': {
@@ -645,7 +646,7 @@ class AutoSchema(ViewInspector):
self.response_media_types = self.map_renderers(path, method)
- serializer = self._get_serializer(path, method)
+ serializer = self.get_serializer(path, method)
if not isinstance(serializer, serializers.Serializer):
item_schema = {}
@@ -657,7 +658,7 @@ class AutoSchema(ViewInspector):
'type': 'array',
'items': item_schema,
}
- paginator = self._get_paginator()
+ paginator = self.get_paginator()
if paginator:
response_schema = paginator.get_paginated_response_schema(response_schema)
else:
@@ -688,3 +689,99 @@ class AutoSchema(ViewInspector):
path = path[1:]
return [path.split('/')[0].replace('_', '-')]
+
+ def _get_path_parameters(self, path, method):
+ warnings.warn(
+ "Method `_get_path_parameters()` has been renamed to `get_path_parameters()`. "
+ "The old name will be removed in DRF v3.14.",
+ RemovedInDRF314Warning, stacklevel=2
+ )
+ return self.get_path_parameters(path, method)
+
+ def _get_filter_parameters(self, path, method):
+ warnings.warn(
+ "Method `_get_filter_parameters()` has been renamed to `get_filter_parameters()`. "
+ "The old name will be removed in DRF v3.14.",
+ RemovedInDRF314Warning, stacklevel=2
+ )
+ return self.get_filter_parameters(path, method)
+
+ def _get_responses(self, path, method):
+ warnings.warn(
+ "Method `_get_responses()` has been renamed to `get_responses()`. "
+ "The old name will be removed in DRF v3.14.",
+ RemovedInDRF314Warning, stacklevel=2
+ )
+ return self.get_responses(path, method)
+
+ def _get_request_body(self, path, method):
+ warnings.warn(
+ "Method `_get_request_body()` has been renamed to `get_request_body()`. "
+ "The old name will be removed in DRF v3.14.",
+ RemovedInDRF314Warning, stacklevel=2
+ )
+ return self.get_request_body(path, method)
+
+ def _get_serializer(self, path, method):
+ warnings.warn(
+ "Method `_get_serializer()` has been renamed to `get_serializer()`. "
+ "The old name will be removed in DRF v3.14.",
+ RemovedInDRF314Warning, stacklevel=2
+ )
+ return self.get_serializer(path, method)
+
+ def _get_paginator(self):
+ warnings.warn(
+ "Method `_get_paginator()` has been renamed to `get_paginator()`. "
+ "The old name will be removed in DRF v3.14.",
+ RemovedInDRF314Warning, stacklevel=2
+ )
+ return self.get_paginator()
+
+ def _map_field_validators(self, field, schema):
+ warnings.warn(
+ "Method `_map_field_validators()` has been renamed to `map_field_validators()`. "
+ "The old name will be removed in DRF v3.14.",
+ RemovedInDRF314Warning, stacklevel=2
+ )
+ return self.map_field_validators(field, schema)
+
+ def _map_serializer(self, serializer):
+ warnings.warn(
+ "Method `_map_serializer()` has been renamed to `map_serializer()`. "
+ "The old name will be removed in DRF v3.14.",
+ RemovedInDRF314Warning, stacklevel=2
+ )
+ return self.map_serializer(serializer)
+
+ def _map_field(self, field):
+ warnings.warn(
+ "Method `_map_field()` has been renamed to `map_field()`. "
+ "The old name will be removed in DRF v3.14.",
+ RemovedInDRF314Warning, stacklevel=2
+ )
+ return self.map_field(field)
+
+ def _map_choicefield(self, field):
+ warnings.warn(
+ "Method `_map_choicefield()` has been renamed to `map_choicefield()`. "
+ "The old name will be removed in DRF v3.14.",
+ RemovedInDRF314Warning, stacklevel=2
+ )
+ return self.map_choicefield(field)
+
+ def _get_pagination_parameters(self, path, method):
+ warnings.warn(
+ "Method `_get_pagination_parameters()` has been renamed to `get_pagination_parameters()`. "
+ "The old name will be removed in DRF v3.14.",
+ RemovedInDRF314Warning, stacklevel=2
+ )
+ return self.get_pagination_parameters(path, method)
+
+ def _allows_filters(self, path, method):
+ warnings.warn(
+ "Method `_allows_filters()` has been renamed to `allows_filters()`. "
+ "The old name will be removed in DRF v3.14.",
+ RemovedInDRF314Warning, stacklevel=2
+ )
+ return self.allows_filters(path, method)
diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py
index 774636972..0e86a7f50 100644
--- a/tests/schemas/test_openapi.py
+++ b/tests/schemas/test_openapi.py
@@ -83,7 +83,7 @@ class TestFieldMapping(TestCase):
]
for field, mapping in cases:
with self.subTest(field=field):
- assert inspector._map_field(field) == mapping
+ assert inspector.map_field(field) == mapping
def test_lazy_string_field(self):
class ItemSerializer(serializers.Serializer):
@@ -91,7 +91,7 @@ class TestFieldMapping(TestCase):
inspector = AutoSchema()
- data = inspector._map_serializer(ItemSerializer())
+ data = inspector.map_serializer(ItemSerializer())
assert isinstance(data['properties']['text']['description'], str), "description must be str"
def test_boolean_default_field(self):
@@ -102,7 +102,7 @@ class TestFieldMapping(TestCase):
inspector = AutoSchema()
- data = inspector._map_serializer(Serializer())
+ data = inspector.map_serializer(Serializer())
assert data['properties']['default_true']['default'] is True, "default must be true"
assert data['properties']['default_false']['default'] is False, "default must be false"
assert 'default' not in data['properties']['without_default'], "default must not be defined"
@@ -202,7 +202,7 @@ class TestOperationIntrospection(TestCase):
inspector = AutoSchema()
inspector.view = view
- request_body = inspector._get_request_body(path, method)
+ request_body = inspector.get_request_body(path, method)
print(request_body)
assert request_body['content']['application/json']['schema']['$ref'] == '#/components/schemas/Item'
@@ -229,7 +229,7 @@ class TestOperationIntrospection(TestCase):
inspector = AutoSchema()
inspector.view = view
- serializer = inspector._get_serializer(path, method)
+ serializer = inspector.get_serializer(path, method)
with pytest.raises(Exception) as exc:
inspector.get_component_name(serializer)
@@ -259,7 +259,7 @@ class TestOperationIntrospection(TestCase):
# there should be no empty 'required' property, see #6834
assert 'required' not in component
- for response in inspector._get_responses(path, method).values():
+ for response in inspector.get_responses(path, method).values():
assert 'required' not in component
def test_empty_required_with_patch_method(self):
@@ -285,7 +285,7 @@ class TestOperationIntrospection(TestCase):
component = components['Item']
# there should be no empty 'required' property, see #6834
assert 'required' not in component
- for response in inspector._get_responses(path, method).values():
+ for response in inspector.get_responses(path, method).values():
assert 'required' not in component
def test_response_body_generation(self):
@@ -307,7 +307,7 @@ class TestOperationIntrospection(TestCase):
inspector = AutoSchema()
inspector.view = view
- responses = inspector._get_responses(path, method)
+ responses = inspector.get_responses(path, method)
assert responses['201']['content']['application/json']['schema']['$ref'] == '#/components/schemas/Item'
components = inspector.get_components(path, method)
@@ -337,7 +337,7 @@ class TestOperationIntrospection(TestCase):
inspector = AutoSchema()
inspector.view = view
- responses = inspector._get_responses(path, method)
+ responses = inspector.get_responses(path, method)
assert responses['201']['content']['application/json']['schema']['$ref'] == '#/components/schemas/Item'
components = inspector.get_components(path, method)
assert components['Item']
@@ -368,7 +368,7 @@ class TestOperationIntrospection(TestCase):
inspector = AutoSchema()
inspector.view = view
- responses = inspector._get_responses(path, method)
+ responses = inspector.get_responses(path, method)
assert responses == {
'200': {
'description': '',
@@ -424,7 +424,7 @@ class TestOperationIntrospection(TestCase):
inspector = AutoSchema()
inspector.view = view
- responses = inspector._get_responses(path, method)
+ responses = inspector.get_responses(path, method)
assert responses == {
'200': {
'description': '',
@@ -472,7 +472,7 @@ class TestOperationIntrospection(TestCase):
inspector = AutoSchema()
inspector.view = view
- responses = inspector._get_responses(path, method)
+ responses = inspector.get_responses(path, method)
assert responses == {
'204': {
'description': '',
@@ -496,7 +496,7 @@ class TestOperationIntrospection(TestCase):
inspector = AutoSchema()
inspector.view = view
- request_body = inspector._get_request_body(path, method)
+ request_body = inspector.get_request_body(path, method)
assert len(request_body['content'].keys()) == 2
assert 'multipart/form-data' in request_body['content']
@@ -519,7 +519,7 @@ class TestOperationIntrospection(TestCase):
inspector = AutoSchema()
inspector.view = view
- responses = inspector._get_responses(path, method)
+ responses = inspector.get_responses(path, method)
# TODO this should be changed once the multiple response
# schema support is there
success_response = responses['200']
@@ -594,7 +594,7 @@ class TestOperationIntrospection(TestCase):
inspector = AutoSchema()
inspector.view = view
- responses = inspector._get_responses(path, method)
+ responses = inspector.get_responses(path, method)
assert responses == {
'200': {
'description': '',
From 8cba4f87ca8e785d1a8c022a7a8ea9649e049c11 Mon Sep 17 00:00:00 2001
From: Tasawar Hussain
Date: Mon, 13 Apr 2020 07:58:51 +0500
Subject: [PATCH 013/407] Updates Ruby on Rails reference link (#7269)
The cited sentences are from actually the action controller docs from ruby on rails
---
docs/api-guide/viewsets.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md
index cd765d3e6..d7aa7ad3b 100644
--- a/docs/api-guide/viewsets.md
+++ b/docs/api-guide/viewsets.md
@@ -317,5 +317,5 @@ To create a base viewset class that provides `create`, `list` and `retrieve` ope
By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple viewsets across your API.
-[cite]: https://guides.rubyonrails.org/routing.html
+[cite]: https://guides.rubyonrails.org/action_controller_overview.html
[routers]: routers.md
From 676aa772234421cbb338cca31b6eaf00a482b47e Mon Sep 17 00:00:00 2001
From: Byron Peebles
Date: Mon, 20 Apr 2020 19:40:05 -0400
Subject: [PATCH 014/407] Fix punctuation in system check (#7281)
---
rest_framework/checks.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/checks.py b/rest_framework/checks.py
index c1e626018..d5d77bc59 100644
--- a/rest_framework/checks.py
+++ b/rest_framework/checks.py
@@ -9,7 +9,7 @@ def pagination_system_check(app_configs, **kwargs):
if api_settings.PAGE_SIZE and not api_settings.DEFAULT_PAGINATION_CLASS:
errors.append(
Warning(
- "You have specified a default PAGE_SIZE pagination rest_framework setting,"
+ "You have specified a default PAGE_SIZE pagination rest_framework setting, "
"without specifying also a DEFAULT_PAGINATION_CLASS.",
hint="The default for DEFAULT_PAGINATION_CLASS is None. "
"In previous versions this was PageNumberPagination. "
From 13c08370e7f65753da285dd4b644a77905d7b979 Mon Sep 17 00:00:00 2001
From: spollard
Date: Thu, 23 Apr 2020 02:36:46 -0600
Subject: [PATCH 015/407] Remove a few submenu scroll bars on docs site (#7283)
Remove the scroll bars for the "Topics" and "Tutorial" submenus on the website. Previously, unnecessary scroll bars appeared in Firefox and Chrome. Applying this change allows "Community" and "API Guide" submenues to still have scroll bars as necessary.
---
docs_theme/css/default.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs_theme/css/default.css b/docs_theme/css/default.css
index e9d7f23bf..992bc60a4 100644
--- a/docs_theme/css/default.css
+++ b/docs_theme/css/default.css
@@ -6,7 +6,7 @@ pre {
.dropdown .dropdown-menu {
display: none;
- overflow-y: scroll;
+ overflow-y: auto;
}
.dropdown.open .dropdown-menu {
From 812f254bbd8ad02300098207e25b87694f9b3fd6 Mon Sep 17 00:00:00 2001
From: Matthaus Woolard
Date: Thu, 23 Apr 2020 20:38:14 +1200
Subject: [PATCH 016/407] SearchFilter to support JSONField and HStoreField
(#7121)
* SearchFilter to support Custom query Transforms
Since Some fields support `__` as a custom Transform for query lookups we needed to update the m2m checking code to handle search_fields that contain __ that are not relationships.
* Update documentation on SearchFilter to include references to JSON and HStore Fields.
---
docs/api-guide/filtering.md | 6 ++++++
rest_framework/filters.py | 3 +++
tests/test_filters.py | 38 +++++++++++++++++++++++++++++++++++++
3 files changed, 47 insertions(+)
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index bad57b441..8e3bd9ef5 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -205,6 +205,10 @@ This will allow the client to filter the items in the list by making queries suc
You can also perform a related lookup on a ForeignKey or ManyToManyField with the lookup API double-underscore notation:
search_fields = ['username', 'email', 'profile__profession']
+
+For [JSONField][JSONField] and [HStoreField][HStoreField] fields you can filter based on nested values within the data structure using the same double-underscore notation:
+
+ search_fields = ['data__breed', 'data__owner__other_pets__0__name']
By default, searches will use case-insensitive partial matches. The search parameter may contain multiple search terms, which should be whitespace and/or comma separated. If multiple search terms are used then objects will be returned in the list only if all the provided terms are matched.
@@ -360,3 +364,5 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter]
[django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter
[django-url-filter]: https://github.com/miki725/django-url-filter
[drf-url-filter]: https://github.com/manjitkumar/drf-url-filters
+[HStoreField]: https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/fields/#hstorefield
+[JSONField]: https://docs.djangoproject.com/en/3.0/ref/contrib/postgres/fields/#jsonfield
\ No newline at end of file
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index 8ef01743c..213182dad 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -96,6 +96,9 @@ class SearchFilter(BaseFilterBackend):
if any(path.m2m for path in path_info):
# This field is a m2m relation so we know we need to call distinct
return True
+ else:
+ # This field has a custom __ query transform but is not a relational field.
+ break
return False
def filter_queryset(self, request, queryset, view):
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 6d7969a92..30cedc7d7 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -1,9 +1,11 @@
import datetime
from importlib import reload as reload_module
+import django
import pytest
from django.core.exceptions import ImproperlyConfigured
from django.db import models
+from django.db.models import CharField, Transform
from django.db.models.functions import Concat, Upper
from django.test import TestCase
from django.test.utils import override_settings
@@ -189,6 +191,42 @@ class SearchFilterTests(TestCase):
assert terms == ['asdf']
+ @pytest.mark.skipif(django.VERSION[:2] < (2, 2), reason="requires django 2.2 or higher")
+ def test_search_field_with_additional_transforms(self):
+ from django.test.utils import register_lookup
+
+ class SearchListView(generics.ListAPIView):
+ queryset = SearchFilterModel.objects.all()
+ serializer_class = SearchFilterSerializer
+ filter_backends = (filters.SearchFilter,)
+ search_fields = ('text__trim', )
+
+ view = SearchListView.as_view()
+
+ # an example custom transform, that trims `a` from the string.
+ class TrimA(Transform):
+ function = 'TRIM'
+ lookup_name = 'trim'
+
+ def as_sql(self, compiler, connection):
+ sql, params = compiler.compile(self.lhs)
+ return "trim(%s, 'a')" % sql, params
+
+ with register_lookup(CharField, TrimA):
+ # Search including `a`
+ request = factory.get('/', {'search': 'abc'})
+
+ response = view(request)
+ assert response.data == []
+
+ # Search excluding `a`
+ request = factory.get('/', {'search': 'bc'})
+ response = view(request)
+ assert response.data == [
+ {'id': 1, 'title': 'z', 'text': 'abc'},
+ {'id': 2, 'title': 'zz', 'text': 'bcd'},
+ ]
+
class AttributeModel(models.Model):
label = models.CharField(max_length=32)
From e1336387d1af300e17407628fc1a00ed5a2c3ecf Mon Sep 17 00:00:00 2001
From: Dhaval Mehta <20968146+dhaval-mehta@users.noreply.github.com>
Date: Fri, 24 Apr 2020 19:43:13 +0530
Subject: [PATCH 017/407] Added `example` for pagination in schemas. (#7275)
---
rest_framework/pagination.py | 12 ++++++++++++
tests/test_pagination.py | 8 ++++++++
2 files changed, 20 insertions(+)
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index 1a1ba2f65..a73f2e526 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -236,10 +236,16 @@ class PageNumberPagination(BasePagination):
'next': {
'type': 'string',
'nullable': True,
+ 'format': 'uri',
+ 'example': 'http://api.example.org/accounts/?{page_query_param}=4'.format(
+ page_query_param=self.page_query_param)
},
'previous': {
'type': 'string',
'nullable': True,
+ 'format': 'uri',
+ 'example': 'http://api.example.org/accounts/?{page_query_param}=2'.format(
+ page_query_param=self.page_query_param)
},
'results': schema,
},
@@ -403,10 +409,16 @@ class LimitOffsetPagination(BasePagination):
'next': {
'type': 'string',
'nullable': True,
+ 'format': 'uri',
+ 'example': 'http://api.example.org/accounts/?{offset_param}=400&{limit_param}=100'.format(
+ offset_param=self.offset_query_param, limit_param=self.limit_query_param),
},
'previous': {
'type': 'string',
'nullable': True,
+ 'format': 'uri',
+ 'example': 'http://api.example.org/accounts/?{offset_param}=200&{limit_param}=100'.format(
+ offset_param=self.offset_query_param, limit_param=self.limit_query_param),
},
'results': schema,
},
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index cd84c8151..c028f0ea8 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -281,10 +281,14 @@ class TestPageNumberPagination:
'next': {
'type': 'string',
'nullable': True,
+ 'format': 'uri',
+ 'example': 'http://api.example.org/accounts/?page=4',
},
'previous': {
'type': 'string',
'nullable': True,
+ 'format': 'uri',
+ 'example': 'http://api.example.org/accounts/?page=2',
},
'results': unpaginated_schema,
},
@@ -588,10 +592,14 @@ class TestLimitOffset:
'next': {
'type': 'string',
'nullable': True,
+ 'format': 'uri',
+ 'example': 'http://api.example.org/accounts/?offset=400&limit=100',
},
'previous': {
'type': 'string',
'nullable': True,
+ 'format': 'uri',
+ 'example': 'http://api.example.org/accounts/?offset=200&limit=100',
},
'results': unpaginated_schema,
},
From 26cd52a461073fcfd5d3a93f53eb38bb7201de5b Mon Sep 17 00:00:00 2001
From: Victor Mireyev
Date: Fri, 24 Apr 2020 17:17:35 +0300
Subject: [PATCH 018/407] Fix markdown lists formatting. (#7288)
---
docs/api-guide/schemas.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md
index 2d74882ad..67cdd9147 100644
--- a/docs/api-guide/schemas.md
+++ b/docs/api-guide/schemas.md
@@ -326,6 +326,7 @@ class MyView(APIView):
Since DRF 3.12, Schema uses the [OpenAPI Components](openapi-components). This method defines components in the schema and [references them](openapi-reference) inside request and response objects. By default, the component's name is deduced from the Serializer's name.
Using OpenAPI's components provides the following advantages:
+
* The schema is more readable and lightweight.
* If you use the schema to generate an SDK (using [openapi-generator](openapi-generator) or [swagger-codegen](swagger-codegen)). The generator can name your SDK's models.
@@ -347,6 +348,7 @@ Schema component "ComponentName" has been overriden with a different value.
This warning occurs when different components have the same name in one schema. Your component name should be unique across your project. This is likely an error that may lead to an invalid schema.
You have two ways to solve the previous issues:
+
* You can rename your serializer with a unique name and another name than "Serializer".
* You can set the `component_name` kwarg parameter of the AutoSchema constructor (see below).
* You can override the `get_component_name` method of the AutoSchema class (see below).
From 3eef5f47f3c0faaf71ba9aee755433d003ce4759 Mon Sep 17 00:00:00 2001
From: Victor Mireyev
Date: Sat, 25 Apr 2020 05:38:01 +0300
Subject: [PATCH 019/407] Fix typo in links (#7289)
Reference-style link should be enclosed in square brackets. Otherwise it is treated as an inline-style link.
---
docs/api-guide/schemas.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md
index 67cdd9147..3dc3f5628 100644
--- a/docs/api-guide/schemas.md
+++ b/docs/api-guide/schemas.md
@@ -290,7 +290,7 @@ class MyView(APIView):
### OperationId
-The schema generator generates an [operationid](openapi-operationid) for each operation. This `operationId` is deduced from the model name, serializer name or view name. The operationId may looks like "listItems", "retrieveItem", "updateItem", etc..
+The schema generator generates an [operationid][openapi-operationid] for each operation. This `operationId` is deduced from the model name, serializer name or view name. The operationId may looks like "listItems", "retrieveItem", "updateItem", etc..
The `operationId` is camelCase by convention.
If you have several views with the same model, the generator may generate duplicate operationId.
@@ -323,12 +323,12 @@ class MyView(APIView):
### Components
-Since DRF 3.12, Schema uses the [OpenAPI Components](openapi-components). This method defines components in the schema and [references them](openapi-reference) inside request and response objects. By default, the component's name is deduced from the Serializer's name.
+Since DRF 3.12, Schema uses the [OpenAPI Components][openapi-components]. This method defines components in the schema and [references them][openapi-reference] inside request and response objects. By default, the component's name is deduced from the Serializer's name.
Using OpenAPI's components provides the following advantages:
* The schema is more readable and lightweight.
-* If you use the schema to generate an SDK (using [openapi-generator](openapi-generator) or [swagger-codegen](swagger-codegen)). The generator can name your SDK's models.
+* If you use the schema to generate an SDK (using [openapi-generator][openapi-generator] or [swagger-codegen][swagger-codegen]). The generator can name your SDK's models.
### Handling component's schema errors
From bda84372d445471ce4291547edf36fe683143b42 Mon Sep 17 00:00:00 2001
From: Xavier Ordoquy
Date: Sat, 25 Apr 2020 09:36:10 +0200
Subject: [PATCH 020/407] Fix viewsets action urls with namespaces (#7287)
Use the current request's namespace to resolve action urls.
---
rest_framework/viewsets.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py
index cad032dd9..9cb48729e 100644
--- a/rest_framework/viewsets.py
+++ b/rest_framework/viewsets.py
@@ -150,6 +150,11 @@ class ViewSetMixin:
Reverse the action for the given `url_name`.
"""
url_name = '%s-%s' % (self.basename, url_name)
+ namespace = None
+ if self.request and self.request.resolver_match:
+ namespace = self.request.resolver_match.namespace
+ if namespace:
+ url_name = namespace + ':' + url_name
kwargs.setdefault('request', self.request)
return reverse(url_name, *args, **kwargs)
From 05a59095cef36f672eaa630881ce5c2175f3eeaa Mon Sep 17 00:00:00 2001
From: Robert Singer
Date: Mon, 27 Apr 2020 14:12:13 -0500
Subject: [PATCH 021/407] Adds drf-typed-views to 3rd party packages (#7293)
---
docs/community/third-party-packages.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md
index 9fce55e94..2033d97ab 100644
--- a/docs/community/third-party-packages.md
+++ b/docs/community/third-party-packages.md
@@ -223,6 +223,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
### Views
* [django-rest-multiple-models][django-rest-multiple-models] - Provides a generic view (and mixin) for sending multiple serialized models and/or querysets via a single API request.
+* [drf-typed-views][drf-typed-views] - Use Python type annotations to validate/deserialize request parameters. Inspired by API Star, Hug and FastAPI.
### Routers
@@ -348,6 +349,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[django-rest-witchcraft]: https://github.com/shosca/django-rest-witchcraft
[drf-access-policy]: https://github.com/rsinger86/drf-access-policy
[drf-flex-fields]: https://github.com/rsinger86/drf-flex-fields
+[drf-typed-views]: https://github.com/rsinger86/drf-typed-views
[drf-action-serializer]: https://github.com/gregschmit/drf-action-serializer
[djangorestframework-dataclasses]: https://github.com/oxan/djangorestframework-dataclasses
[django-restql]: https://github.com/yezyilomo/django-restql
From e275b9036a274ad06d9ffa58b4c17293e0fe0564 Mon Sep 17 00:00:00 2001
From: Ryan P Kilby
Date: Wed, 29 Apr 2020 03:19:44 -0700
Subject: [PATCH 022/407] Allow context to be provided to get_serializer
(#7298)
* Test generics serializer behavior
* Allow context to be provided to get_serializer
---
rest_framework/generics.py | 2 +-
tests/test_generics.py | 30 ++++++++++++++++++++++++++++++
2 files changed, 31 insertions(+), 1 deletion(-)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index c39b02ab7..55cfafda4 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -106,7 +106,7 @@ class GenericAPIView(views.APIView):
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
- kwargs['context'] = self.get_serializer_context()
+ kwargs.setdefault('context', self.get_serializer_context())
return serializer_class(*args, **kwargs)
def get_serializer_class(self):
diff --git a/tests/test_generics.py b/tests/test_generics.py
index 0b91e3465..2907d2773 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -662,3 +662,33 @@ class GetObjectOr404Tests(TestCase):
def test_get_object_or_404_with_invalid_string_for_uuid(self):
with pytest.raises(Http404):
generics.get_object_or_404(UUIDForeignKeyTarget, pk='not-a-uuid')
+
+
+class TestSerializer(TestCase):
+
+ def test_serializer_class_not_provided(self):
+ class NoSerializerClass(generics.GenericAPIView):
+ pass
+
+ with pytest.raises(AssertionError) as excinfo:
+ NoSerializerClass().get_serializer_class()
+
+ assert str(excinfo.value) == (
+ "'NoSerializerClass' should either include a `serializer_class` "
+ "attribute, or override the `get_serializer_class()` method.")
+
+ def test_given_context_not_overridden(self):
+ context = object()
+
+ class View(generics.ListAPIView):
+ serializer_class = serializers.Serializer
+
+ def list(self, request):
+ response = Response()
+ response.serializer = self.get_serializer(context=context)
+ return response
+
+ response = View.as_view()(factory.get('/'))
+ serializer = response.serializer
+
+ assert serializer.context is context
From 5828d8f7ca167b11296733a2b54f9d6fca29b7b0 Mon Sep 17 00:00:00 2001
From: Rick van Hattem
Date: Wed, 29 Apr 2020 12:20:02 +0200
Subject: [PATCH 023/407] Changed hardcoded /tmp/ directory to autodetect from
system (#7292)
---
tests/test_model_serializer.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index fbb562792..0de628dc8 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -8,6 +8,7 @@ an appropriate set of serializer fields for each case.
import datetime
import decimal
import sys
+import tempfile
from collections import OrderedDict
import django
@@ -71,7 +72,7 @@ class RegularFieldsModel(models.Model):
time_field = models.TimeField()
url_field = models.URLField(max_length=100)
custom_field = CustomField()
- file_path_field = models.FilePathField(path='/tmp/')
+ file_path_field = models.FilePathField(path=tempfile.gettempdir())
def method(self):
return 'method'
@@ -191,8 +192,8 @@ class TestRegularFieldMappings(TestCase):
time_field = TimeField()
url_field = URLField(max_length=100)
custom_field = ModelField(model_field=)
- file_path_field = FilePathField(path='/tmp/')
- """)
+ file_path_field = FilePathField(path=%r)
+ """ % tempfile.gettempdir())
self.assertEqual(repr(TestSerializer()), expected)
From 900773ad06ed6b3e9dc67acd76c212a5b7146824 Mon Sep 17 00:00:00 2001
From: Ryan P Kilby
Date: Wed, 29 Apr 2020 03:21:42 -0700
Subject: [PATCH 024/407] Minor test improvements (#7297)
* Pass data via response instead of renderer_context
Instead of relying on internal implementation details to get a reference
to the response or view, attach these directly to the returned response.
* Remove unused method
---
tests/test_decorators.py | 4 ----
tests/test_renderers.py | 14 ++++++++++++--
tests/test_request.py | 10 ++++++----
tests/test_viewsets.py | 14 +++++++++-----
4 files changed, 27 insertions(+), 15 deletions(-)
diff --git a/tests/test_decorators.py b/tests/test_decorators.py
index e10f0e5c5..99ba13e60 100644
--- a/tests/test_decorators.py
+++ b/tests/test_decorators.py
@@ -22,10 +22,6 @@ class DecoratorTestCase(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
- def _finalize_response(self, request, response, *args, **kwargs):
- response.request = request
- return APIView.finalize_response(self, request, response, *args, **kwargs)
-
def test_api_view_incorrect(self):
"""
If @api_view is not applied correct, we should raise an assertion.
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
index c79c0a766..71c5fb3f6 100644
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -741,6 +741,11 @@ class AdminRendererTests(TestCase):
class DummyGenericViewsetLike(APIView):
lookup_field = 'test'
+ def get(self, request):
+ response = Response()
+ response.view = self
+ return response
+
def reverse_action(view, *args, **kwargs):
self.assertEqual(kwargs['kwargs']['test'], 1)
return '/example/'
@@ -749,7 +754,7 @@ class AdminRendererTests(TestCase):
view = DummyGenericViewsetLike.as_view()
request = factory.get('/')
response = view(request)
- view = response.renderer_context['view']
+ view = response.view
self.assertEqual(self.renderer.get_result_url({'test': 1}, view), '/example/')
self.assertIsNone(self.renderer.get_result_url({}, view))
@@ -760,11 +765,16 @@ class AdminRendererTests(TestCase):
class DummyView(APIView):
lookup_field = 'test'
+ def get(self, request):
+ response = Response()
+ response.view = self
+ return response
+
# get the view instance instead of the view function
view = DummyView.as_view()
request = factory.get('/')
response = view(request)
- view = response.renderer_context['view']
+ view = response.view
self.assertIsNone(self.renderer.get_result_url({'test': 1}, view))
self.assertIsNone(self.renderer.get_result_url({}, view))
diff --git a/tests/test_request.py b/tests/test_request.py
index ed1b2e867..be84fe5f9 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -137,7 +137,9 @@ class MockView(APIView):
class EchoView(APIView):
def post(self, request):
- return Response(status=status.HTTP_200_OK, data=request.data)
+ response = Response(status=status.HTTP_200_OK, data=request.data)
+ response._request = request # test client sets `request` input
+ return response
class FileUploadView(APIView):
@@ -306,7 +308,7 @@ class TestHttpRequest(TestCase):
`RawPostDataException` being raised.
"""
response = APIClient().post('/echo/', data={'a': 'b'}, format='json')
- request = response.renderer_context['request']
+ request = response._request
# ensure that request stream was consumed by json parser
assert request.content_type.startswith('application/json')
@@ -325,7 +327,7 @@ class TestHttpRequest(TestCase):
the duplicate stream parse exception.
"""
response = APIClient().post('/echo/', data={'a': 'b'})
- request = response.renderer_context['request']
+ request = response._request
# ensure that request stream was consumed by form parser
assert request.content_type.startswith('multipart/form-data')
@@ -333,7 +335,7 @@ class TestHttpRequest(TestCase):
# pass same HttpRequest to view, form data set on underlying request
response = EchoView.as_view()(request._request)
- request = response.renderer_context['request']
+ request = response._request
# ensure that request stream was consumed by form parser
assert request.content_type.startswith('multipart/form-data')
diff --git a/tests/test_viewsets.py b/tests/test_viewsets.py
index 1a621c518..465806a09 100644
--- a/tests/test_viewsets.py
+++ b/tests/test_viewsets.py
@@ -42,7 +42,9 @@ class ActionViewSet(GenericViewSet):
return response
def retrieve(self, request, *args, **kwargs):
- return Response()
+ response = Response()
+ response.view = self
+ return response
@action(detail=False)
def list_action(self, request, *args, **kwargs):
@@ -70,7 +72,9 @@ class ActionViewSet(GenericViewSet):
class ActionNamesViewSet(GenericViewSet):
def retrieve(self, request, *args, **kwargs):
- return Response()
+ response = Response()
+ response.view = self
+ return response
@action(detail=True)
def unnamed_action(self, request, *args, **kwargs):
@@ -209,7 +213,7 @@ class GetExtraActionUrlMapTests(TestCase):
def test_list_view(self):
response = self.client.get('/api/actions/')
- view = response.renderer_context['view']
+ view = response.view
expected = OrderedDict([
('Custom list action', 'http://testserver/api/actions/custom_list_action/'),
@@ -220,7 +224,7 @@ class GetExtraActionUrlMapTests(TestCase):
def test_detail_view(self):
response = self.client.get('/api/actions/1/')
- view = response.renderer_context['view']
+ view = response.view
expected = OrderedDict([
('Custom detail action', 'http://testserver/api/actions/1/custom_detail_action/'),
@@ -236,7 +240,7 @@ class GetExtraActionUrlMapTests(TestCase):
def test_action_names(self):
# Action 'name' and 'suffix' kwargs should be respected
response = self.client.get('/api/names/1/')
- view = response.renderer_context['view']
+ view = response.view
expected = OrderedDict([
('Custom Name', 'http://testserver/api/names/1/named_action/'),
From 4349ce1a542a1b83f7e19979831bbc427f92ad55 Mon Sep 17 00:00:00 2001
From: ADimeo
Date: Thu, 30 Apr 2020 14:14:27 +0200
Subject: [PATCH 025/407] Fix broken documentation link (#7305)
---
docs/api-guide/testing.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md
index dab0e264d..73de68a76 100644
--- a/docs/api-guide/testing.md
+++ b/docs/api-guide/testing.md
@@ -221,7 +221,7 @@ If you're using `RequestsClient` you'll want to ensure that test setup, and resu
## Headers & Authentication
Custom headers and authentication credentials can be provided in the same way
-as [when using a standard `requests.Session` instance](http://docs.python-requests.org/en/master/user/advanced/#session-objects).
+as [when using a standard `requests.Session` instance][session_objects].
from requests.auth import HTTPBasicAuth
@@ -414,3 +414,4 @@ For example, to add support for using `format='html'` in test requests, you migh
[requestfactory]: https://docs.djangoproject.com/en/stable/topics/testing/advanced/#django.test.client.RequestFactory
[configuration]: #configuration
[refresh_from_db_docs]: https://docs.djangoproject.com/en/1.11/ref/models/instances/#django.db.models.Model.refresh_from_db
+[session_objects]: https://requests.readthedocs.io/en/master/user/advanced/#session-objects
From d7777ea10ff40e0abf145df707b7701a65960249 Mon Sep 17 00:00:00 2001
From: Mathieu Pillard
Date: Fri, 1 May 2020 11:22:36 +0200
Subject: [PATCH 026/407] Pass custom code to PermissionDenied if permission
class had one set (#7306)
---
docs/api-guide/permissions.md | 2 +-
rest_framework/views.py | 12 ++++++++----
tests/test_permissions.py | 7 +++++++
3 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md
index 25baa4813..ac2924a83 100644
--- a/docs/api-guide/permissions.md
+++ b/docs/api-guide/permissions.md
@@ -231,7 +231,7 @@ If you need to test if a request is a read operation or a write operation, you s
---
-Custom permissions will raise a `PermissionDenied` exception if the test fails. To change the error message associated with the exception, implement a `message` attribute directly on your custom permission. Otherwise the `default_detail` attribute from `PermissionDenied` will be used.
+Custom permissions will raise a `PermissionDenied` exception if the test fails. To change the error message associated with the exception, implement a `message` attribute directly on your custom permission. Otherwise the `default_detail` attribute from `PermissionDenied` will be used. Similarly, to change the code identifier associated with the exception, implement a `code` attribute directly on your custom permission - otherwise the `default_code` attribute from `PermissionDenied` will be used.
from rest_framework import permissions
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 69db053d6..d1b5e4ed9 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -166,13 +166,13 @@ class APIView(View):
"""
raise exceptions.MethodNotAllowed(request.method)
- def permission_denied(self, request, message=None):
+ def permission_denied(self, request, message=None, code=None):
"""
If request is not permitted, determine what kind of exception to raise.
"""
if request.authenticators and not request.successful_authenticator:
raise exceptions.NotAuthenticated()
- raise exceptions.PermissionDenied(detail=message)
+ raise exceptions.PermissionDenied(detail=message, code=code)
def throttled(self, request, wait):
"""
@@ -331,7 +331,9 @@ class APIView(View):
for permission in self.get_permissions():
if not permission.has_permission(request, self):
self.permission_denied(
- request, message=getattr(permission, 'message', None)
+ request,
+ message=getattr(permission, 'message', None),
+ code=getattr(permission, 'code', None)
)
def check_object_permissions(self, request, obj):
@@ -342,7 +344,9 @@ class APIView(View):
for permission in self.get_permissions():
if not permission.has_object_permission(request, self, obj):
self.permission_denied(
- request, message=getattr(permission, 'message', None)
+ request,
+ message=getattr(permission, 'message', None),
+ code=getattr(permission, 'code', None)
)
def check_throttles(self, request):
diff --git a/tests/test_permissions.py b/tests/test_permissions.py
index b6178c0bb..d445f271d 100644
--- a/tests/test_permissions.py
+++ b/tests/test_permissions.py
@@ -438,6 +438,7 @@ class BasicPerm(permissions.BasePermission):
class BasicPermWithDetail(permissions.BasePermission):
message = 'Custom: You cannot access this resource'
+ code = 'permission_denied_custom'
def has_permission(self, request, view):
return False
@@ -450,6 +451,7 @@ class BasicObjectPerm(permissions.BasePermission):
class BasicObjectPermWithDetail(permissions.BasePermission):
message = 'Custom: You cannot access this resource'
+ code = 'permission_denied_custom'
def has_object_permission(self, request, view, obj):
return False
@@ -492,30 +494,35 @@ class CustomPermissionsTests(TestCase):
credentials = basic_auth_header('username', 'password')
self.request = factory.get('/1', format='json', HTTP_AUTHORIZATION=credentials)
self.custom_message = 'Custom: You cannot access this resource'
+ self.custom_code = 'permission_denied_custom'
def test_permission_denied(self):
response = denied_view(self.request, pk=1)
detail = response.data.get('detail')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertNotEqual(detail, self.custom_message)
+ self.assertNotEqual(detail.code, self.custom_code)
def test_permission_denied_with_custom_detail(self):
response = denied_view_with_detail(self.request, pk=1)
detail = response.data.get('detail')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(detail, self.custom_message)
+ self.assertEqual(detail.code, self.custom_code)
def test_permission_denied_for_object(self):
response = denied_object_view(self.request, pk=1)
detail = response.data.get('detail')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertNotEqual(detail, self.custom_message)
+ self.assertNotEqual(detail.code, self.custom_code)
def test_permission_denied_for_object_with_custom_detail(self):
response = denied_object_view_with_detail(self.request, pk=1)
detail = response.data.get('detail')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(detail, self.custom_message)
+ self.assertEqual(detail.code, self.custom_code)
class PermissionsCompositionTests(TestCase):
From 256f076df2ccf5f9fba264520ae368a40ffbea12 Mon Sep 17 00:00:00 2001
From: Santos Solorzano
Date: Fri, 1 May 2020 14:16:18 -0700
Subject: [PATCH 027/407] Update index.md
- Dropped Django 1.11, 2.0, 2.1 support
---
docs/index.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/index.md b/docs/index.md
index 899118896..65f907051 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -86,7 +86,7 @@ continued development by **[signing up for a paid plan][funding]**.
REST framework requires the following:
* Python (3.5, 3.6, 3.7, 3.8)
-* Django (1.11, 2.0, 2.1, 2.2, 3.0)
+* Django (2.2, 3.0)
We **highly recommend** and only officially support the latest patch release of
each Python and Django series.
From 46be2ffd34e2118e905fa3f0cbcab918d482134f Mon Sep 17 00:00:00 2001
From: Sardorbek Imomaliev
Date: Tue, 5 May 2020 20:04:35 +0700
Subject: [PATCH 028/407] Fix schema endpoint example (#7178)
* Fix schema endpoint example
Endpoint `schema` collides with `schema` class variable `schema` which is use for specification generation
* Update metadata.md
Co-authored-by: Tom Christie
---
docs/api-guide/metadata.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/api-guide/metadata.md b/docs/api-guide/metadata.md
index fdb778626..20708c6e3 100644
--- a/docs/api-guide/metadata.md
+++ b/docs/api-guide/metadata.md
@@ -71,7 +71,7 @@ If you have specific requirements for creating schema endpoints that are accesse
For example, the following additional route could be used on a viewset to provide a linkable schema endpoint.
@action(methods=['GET'], detail=False)
- def schema(self, request):
+ def api_schema(self, request):
meta = self.metadata_class()
data = meta.determine_metadata(request, self)
return Response(data)
From 56ff382b174c98fbe8a8dde74e32d32ab4e0f303 Mon Sep 17 00:00:00 2001
From: Asif Saif Uddin
Date: Tue, 5 May 2020 19:05:29 +0600
Subject: [PATCH 029/407] updated required versions of dependencies (#7312)
---
.travis.yml | 2 +-
requirements/requirements-codestyle.txt | 4 ++--
requirements/requirements-documentation.txt | 2 +-
requirements/requirements-optionals.txt | 4 ++--
requirements/requirements-packaging.txt | 6 +++---
requirements/requirements-testing.txt | 4 ++--
6 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index 2cdeee5b8..f1ec689f7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,6 @@
language: python
cache: pip
-dist: xenial
+dist: bionic
matrix:
fast_finish: true
include:
diff --git a/requirements/requirements-codestyle.txt b/requirements/requirements-codestyle.txt
index 482deac66..d9efde1d0 100644
--- a/requirements/requirements-codestyle.txt
+++ b/requirements/requirements-codestyle.txt
@@ -1,6 +1,6 @@
# PEP8 code linting, which we run on all commits.
-flake8==3.7.8
-flake8-tidy-imports==3.0.0
+flake8==3.7.9
+flake8-tidy-imports==4.1.0
pycodestyle==2.5.0
# Sort and lint imports
diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt
index 73158043e..e969ff471 100644
--- a/requirements/requirements-documentation.txt
+++ b/requirements/requirements-documentation.txt
@@ -1,2 +1,2 @@
# MkDocs to build our documentation.
-mkdocs==1.0.4
+mkdocs==1.1
diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt
index 14957a531..2b7a18a13 100644
--- a/requirements/requirements-optionals.txt
+++ b/requirements/requirements-optionals.txt
@@ -1,8 +1,8 @@
# Optional packages which may be used with REST framework.
-psycopg2-binary>=2.8.2, <2.9
+psycopg2-binary>=2.8.5, <2.9
markdown==3.1.1
pygments==2.4.2
-django-guardian==2.1.0
+django-guardian==2.2.0
django-filter>=2.2.0, <2.3
coreapi==2.3.1
coreschema==0.0.4
diff --git a/requirements/requirements-packaging.txt b/requirements/requirements-packaging.txt
index 48de9e768..091622fbe 100644
--- a/requirements/requirements-packaging.txt
+++ b/requirements/requirements-packaging.txt
@@ -1,8 +1,8 @@
# Wheel for PyPI installs.
-wheel==0.30.0
+wheel==0.34.2
# Twine for secured PyPI uploads.
-twine==1.11.0
+twine==3.1.1
# Transifex client for managing translation resources.
-transifex-client==0.11
+transifex-client==0.13.9
diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt
index 83ec9ab9e..ad246e857 100644
--- a/requirements/requirements-testing.txt
+++ b/requirements/requirements-testing.txt
@@ -1,4 +1,4 @@
# Pytest for running the tests.
-pytest>=5.0,<5.1
-pytest-django>=3.5.1,<3.6
+pytest>=5.4.1,<5.5
+pytest-django>=3.9.0,<3.10
pytest-cov>=2.7.1
From 4ac0fae75b3e9d2a4c011a87310c910762c84cb1 Mon Sep 17 00:00:00 2001
From: Daniil Okhlopkov <5613295+ohld@users.noreply.github.com>
Date: Mon, 11 May 2020 11:04:37 +0300
Subject: [PATCH 030/407] use @admin decorator (#7022)
---
rest_framework/authtoken/admin.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/rest_framework/authtoken/admin.py b/rest_framework/authtoken/admin.py
index 1a507249b..f0cf646f4 100644
--- a/rest_framework/authtoken/admin.py
+++ b/rest_framework/authtoken/admin.py
@@ -3,10 +3,8 @@ from django.contrib import admin
from rest_framework.authtoken.models import Token
+@admin.register(Token)
class TokenAdmin(admin.ModelAdmin):
list_display = ('key', 'user', 'created')
fields = ('user',)
ordering = ('-created',)
-
-
-admin.site.register(Token, TokenAdmin)
From 8a38991d6aa01c200ddc7bf46e2d945a197810d3 Mon Sep 17 00:00:00 2001
From: Ryan P Kilby
Date: Mon, 11 May 2020 02:55:39 -0700
Subject: [PATCH 031/407] Fix SearchFilter.must_call_distinict for
annotation+m2m (#7146)
* Test SearchFilter annotation+m2m distinct
* Fix SearchFilter annotation+m2m distinct
---
rest_framework/filters.py | 2 +-
tests/test_filters.py | 15 +++++++++++++++
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index 213182dad..366577519 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -85,7 +85,7 @@ class SearchFilter(BaseFilterBackend):
search_field = search_field[1:]
# Annotated fields do not need to be distinct
if isinstance(queryset, models.QuerySet) and search_field in queryset.query.annotations:
- return False
+ continue
parts = search_field.split(LOOKUP_SEP)
for part in parts:
field = opts.get_field(part)
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 30cedc7d7..e69537666 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -406,6 +406,21 @@ class SearchFilterAnnotatedFieldTests(TestCase):
assert len(response.data) == 1
assert response.data[0]['title_text'] == 'ABCDEF'
+ def test_must_call_distinct_subsequent_m2m_fields(self):
+ f = filters.SearchFilter()
+
+ queryset = SearchFilterModelM2M.objects.annotate(
+ title_text=Upper(
+ Concat(models.F('title'), models.F('text'))
+ )
+ ).all()
+
+ # Sanity check that m2m must call distinct
+ assert f.must_call_distinct(queryset, ['attributes'])
+
+ # Annotated field should not prevent m2m must call distinct
+ assert f.must_call_distinct(queryset, ['title_text', 'attributes'])
+
class OrderingFilterModel(models.Model):
title = models.CharField(max_length=20, verbose_name='verbose title')
From f07f48a5da0710ca807db372e0dabb045e8a380a Mon Sep 17 00:00:00 2001
From: Kamil Sampolski
Date: Mon, 11 May 2020 12:01:00 +0200
Subject: [PATCH 032/407] Updating documentation / switching from url to path
(#7237)
* Update README.md
We should use path instead of url in urlpatterns,
* Update index.md
We should use path instead of url in urlpatterns
* Update README.md
We should use path instead of url in urlpatterns
---
README.md | 6 +++---
docs/index.md | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index 41a344fcf..95020bb1d 100644
--- a/README.md
+++ b/README.md
@@ -89,7 +89,7 @@ Startup up a new project like so...
Now edit the `example/urls.py` module in your project:
```python
-from django.conf.urls import url, include
+from django.urls import path, include
from django.contrib.auth.models import User
from rest_framework import serializers, viewsets, routers
@@ -114,8 +114,8 @@ router.register(r'users', UserViewSet)
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
- url(r'^', include(router.urls)),
- url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
+ path('', include(router.urls)),
+ path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
```
diff --git a/docs/index.md b/docs/index.md
index 65f907051..0e5f9dc7e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -148,7 +148,7 @@ Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_
We're ready to create our API now.
Here's our project's root `urls.py` module:
- from django.conf.urls import url, include
+ from django.urls import path, include
from django.contrib.auth.models import User
from rest_framework import routers, serializers, viewsets
@@ -170,8 +170,8 @@ Here's our project's root `urls.py` module:
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
- url(r'^', include(router.urls)),
- url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
+ path('', include(router.urls)),
+ path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
You can now open the API in your browser at [http://127.0.0.1:8000/](http://127.0.0.1:8000/), and view your new 'users' API. If you use the login control in the top right corner you'll also be able to add, create and delete users from the system.
From 1260ed424a7c1dc5b488b7ab7248b8267cd5fde0 Mon Sep 17 00:00:00 2001
From: Asif Saif Uddin
Date: Mon, 11 May 2020 18:08:40 +0600
Subject: [PATCH 033/407] jquery 3.5.1 (#7313)
* jquery 3.5.1
---
rest_framework/static/rest_framework/js/jquery-3.4.1.min.js | 2 --
rest_framework/static/rest_framework/js/jquery-3.5.1.min.js | 2 ++
rest_framework/templates/rest_framework/admin.html | 2 +-
rest_framework/templates/rest_framework/base.html | 2 +-
rest_framework/templates/rest_framework/docs/error.html | 2 +-
rest_framework/templates/rest_framework/docs/index.html | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
delete mode 100644 rest_framework/static/rest_framework/js/jquery-3.4.1.min.js
create mode 100644 rest_framework/static/rest_framework/js/jquery-3.5.1.min.js
diff --git a/rest_framework/static/rest_framework/js/jquery-3.4.1.min.js b/rest_framework/static/rest_framework/js/jquery-3.4.1.min.js
deleted file mode 100644
index a1c07fd80..000000000
--- a/rest_framework/static/rest_framework/js/jquery-3.4.1.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/*! jQuery v3.4.1 | (c) JS Foundation and other contributors | jquery.org/license */
-!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],E=C.document,r=Object.getPrototypeOf,s=t.slice,g=t.concat,u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.4.1",k=function(e,t){return new k.fn.init(e,t)},p=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;function d(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp($),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+$),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),ne=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(m.childNodes),m.childNodes),t[m.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&((e?e.ownerDocument||e:m)!==C&&T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!A[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&U.test(t)){(s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=k),o=(l=h(t)).length;while(o--)l[o]="#"+s+" "+xe(l[o]);c=l.join(","),f=ee.test(t)&&ye(e.parentNode)||e}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){A(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[k]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:m;return r!==C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),m!==C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=k,!C.getElementsByName||!C.getElementsByName(k).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+k+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+k+"+*").length||v.push(".#.+[+~]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",$)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument===m&&y(m,e)?-1:t===C||t.ownerDocument===m&&y(m,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e===C?-1:t===C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]===m?-1:s[r]===m?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if((e.ownerDocument||e)!==C&&T(e),d.matchesSelector&&E&&!A[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){A(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=p[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&p(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?k.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?k.grep(e,function(e){return e===n!==r}):"string"!=typeof n?k.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(k.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||q,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:L.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof k?t[0]:t,k.merge(this,k.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),D.test(r[1])&&k.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(k):k.makeArray(e,this)}).prototype=k.fn,q=k(E);var H=/^(?:parents|prev(?:Until|All))/,O={children:!0,contents:!0,next:!0,prev:!0};function P(e,t){while((e=e[t])&&1!==e.nodeType);return e}k.fn.extend({has:function(e){var t=k(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?k.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;nx",y.noCloneChecked=!!me.cloneNode(!0).lastChild.defaultValue;var Te=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Ee=/^([^.]*)(?:\.(.+)|)/;function ke(){return!0}function Se(){return!1}function Ne(e,t){return e===function(){try{return E.activeElement}catch(e){}}()==("focus"===t)}function Ae(e,t,n,r,i,o){var a,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Ae(e,s,n,r,t[s],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Se;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return k().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=k.guid++)),e.each(function(){k.event.add(this,t,i,r,n)})}function De(e,i,o){o?(Q.set(e,i,!1),k.event.add(e,i,{namespace:!1,handler:function(e){var t,n,r=Q.get(this,i);if(1&e.isTrigger&&this[i]){if(r.length)(k.event.special[i]||{}).delegateType&&e.stopPropagation();else if(r=s.call(arguments),Q.set(this,i,r),t=o(this,i),this[i](),r!==(n=Q.get(this,i))||t?Q.set(this,i,!1):n={},r!==n)return e.stopImmediatePropagation(),e.preventDefault(),n.value}else r.length&&(Q.set(this,i,{value:k.event.trigger(k.extend(r[0],k.Event.prototype),r.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Q.get(e,i)&&k.event.add(e,i,ke)}k.event={global:{},add:function(t,e,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.get(t);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&k.find.matchesSelector(ie,i),n.guid||(n.guid=k.guid++),(u=v.events)||(u=v.events={}),(a=v.handle)||(a=v.handle=function(e){return"undefined"!=typeof k&&k.event.triggered!==e.type?k.event.dispatch.apply(t,arguments):void 0}),l=(e=(e||"").match(R)||[""]).length;while(l--)d=g=(s=Ee.exec(e[l])||[])[1],h=(s[2]||"").split(".").sort(),d&&(f=k.event.special[d]||{},d=(i?f.delegateType:f.bindType)||d,f=k.event.special[d]||{},c=k.extend({type:d,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&k.expr.match.needsContext.test(i),namespace:h.join(".")},o),(p=u[d])||((p=u[d]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(t,r,h,a)||t.addEventListener&&t.addEventListener(d,a)),f.add&&(f.add.call(t,c),c.handler.guid||(c.handler.guid=n.guid)),i?p.splice(p.delegateCount++,0,c):p.push(c),k.event.global[d]=!0)}},remove:function(e,t,n,r,i){var o,a,s,u,l,c,f,p,d,h,g,v=Q.hasData(e)&&Q.get(e);if(v&&(u=v.events)){l=(t=(t||"").match(R)||[""]).length;while(l--)if(d=g=(s=Ee.exec(t[l])||[])[1],h=(s[2]||"").split(".").sort(),d){f=k.event.special[d]||{},p=u[d=(r?f.delegateType:f.bindType)||d]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=p.length;while(o--)c=p[o],!i&&g!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(p.splice(o,1),c.selector&&p.delegateCount--,f.remove&&f.remove.call(e,c));a&&!p.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||k.removeEvent(e,d,v.handle),delete u[d])}else for(d in u)k.event.remove(e,d+t[l],n,r,!0);k.isEmptyObject(u)&&Q.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,o,a,s=k.event.fix(e),u=new Array(arguments.length),l=(Q.get(this,"events")||{})[s.type]||[],c=k.event.special[s.type]||{};for(u[0]=s,t=1;t\x20\t\r\n\f]*)[^>]*)\/>/gi,qe=/
-
+
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index 5d9d80b05..9207f049b 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -293,7 +293,7 @@
csrfToken: "{% if request %}{{ csrf_token }}{% endif %}"
};
-
+
diff --git a/rest_framework/templates/rest_framework/docs/error.html b/rest_framework/templates/rest_framework/docs/error.html
index 6afd25e7b..6afc4a88b 100644
--- a/rest_framework/templates/rest_framework/docs/error.html
+++ b/rest_framework/templates/rest_framework/docs/error.html
@@ -66,6 +66,6 @@ at rest_framework/docs/error.html
.
-
+