From 04e0c2b9ab6616a28148ce32e2d19858ccfe6c69 Mon Sep 17 00:00:00 2001 From: David Smith <39445562+smithdc1@users.noreply.github.com> Date: Tue, 13 Oct 2020 17:27:08 +0100 Subject: [PATCH 01/63] Bumped Markdown version to 3.3 (#7590) --- requirements/requirements-optionals.txt | 3 +- tests/test_description.py | 372 ++++++++++++------------ 2 files changed, 182 insertions(+), 193 deletions(-) diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 2b7a18a13f..739555667e 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,6 +1,7 @@ # Optional packages which may be used with REST framework. psycopg2-binary>=2.8.5, <2.9 -markdown==3.1.1 +markdown==3.3;python_version>="3.6" +markdown==3.2.2;python_version=="3.5" pygments==2.4.2 django-guardian==2.2.0 django-filter>=2.2.0, <2.3 diff --git a/tests/test_description.py b/tests/test_description.py index ae00fe4a97..9e7e4dc322 100644 --- a/tests/test_description.py +++ b/tests/test_description.py @@ -1,192 +1,180 @@ -from django.test import TestCase - -from rest_framework.compat import apply_markdown -from rest_framework.utils.formatting import dedent -from rest_framework.views import APIView - -# We check that docstrings get nicely un-indented. -DESCRIPTION = """an example docstring -==================== - -* list -* list - -another header --------------- - - code block - -indented - -# hash style header # - -``` json -[{ - "alpha": 1, - "beta: "this is a string" -}] -```""" - - -# If markdown is installed we also test it's working -# (and that our wrapped forces '=' to h2 and '-' to h3) -MARKED_DOWN_HILITE = """ -
[{
"alpha": 1,
\ - "beta: "this\ - is a \ -string"
}]
- -


""" - -MARKED_DOWN_NOT_HILITE = """ -

json -[{ - "alpha": 1, - "beta: "this is a string" -}]

""" - -# We support markdown < 2.1 and markdown >= 2.1 -MARKED_DOWN_lt_21 = """

an example docstring

- -

another header

-
code block
-
-

indented

-

hash style header

%s""" - -MARKED_DOWN_gte_21 = """

an example docstring

- -

another header

-
code block
-
-

indented

-

hash style header

%s""" - - -class TestViewNamesAndDescriptions(TestCase): - def test_view_name_uses_class_name(self): - """ - Ensure view names are based on the class name. - """ - class MockView(APIView): - pass - assert MockView().get_view_name() == 'Mock' - - def test_view_name_uses_name_attribute(self): - class MockView(APIView): - name = 'Foo' - assert MockView().get_view_name() == 'Foo' - - def test_view_name_uses_suffix_attribute(self): - class MockView(APIView): - suffix = 'List' - assert MockView().get_view_name() == 'Mock List' - - def test_view_name_preferences_name_over_suffix(self): - class MockView(APIView): - name = 'Foo' - suffix = 'List' - assert MockView().get_view_name() == 'Foo' - - def test_view_description_uses_docstring(self): - """Ensure view descriptions are based on the docstring.""" - class MockView(APIView): - """an example docstring - ==================== - - * list - * list - - another header - -------------- - - code block - - indented - - # hash style header # - - ``` json - [{ - "alpha": 1, - "beta: "this is a string" - }] - ```""" - - assert MockView().get_view_description() == DESCRIPTION - - def test_view_description_uses_description_attribute(self): - class MockView(APIView): - description = 'Foo' - assert MockView().get_view_description() == 'Foo' - - def test_view_description_allows_empty_description(self): - class MockView(APIView): - """Description.""" - description = '' - assert MockView().get_view_description() == '' - - def test_view_description_can_be_empty(self): - """ - Ensure that if a view has no docstring, - then it's description is the empty string. - """ - class MockView(APIView): - pass - assert MockView().get_view_description() == '' - - def test_view_description_can_be_promise(self): - """ - Ensure a view may have a docstring that is actually a lazily evaluated - class that can be converted to a string. - - See: https://github.com/encode/django-rest-framework/issues/1708 - """ - # use a mock object instead of gettext_lazy to ensure that we can't end - # up with a test case string in our l10n catalog - - class MockLazyStr: - def __init__(self, string): - self.s = string - - def __str__(self): - return self.s - - class MockView(APIView): - __doc__ = MockLazyStr("a gettext string") - - assert MockView().get_view_description() == 'a gettext string' - - def test_markdown(self): - """ - Ensure markdown to HTML works as expected. - """ - if apply_markdown: - md_applied = apply_markdown(DESCRIPTION) - gte_21_match = ( - md_applied == ( - MARKED_DOWN_gte_21 % MARKED_DOWN_HILITE) or - md_applied == ( - MARKED_DOWN_gte_21 % MARKED_DOWN_NOT_HILITE)) - lt_21_match = ( - md_applied == ( - MARKED_DOWN_lt_21 % MARKED_DOWN_HILITE) or - md_applied == ( - MARKED_DOWN_lt_21 % MARKED_DOWN_NOT_HILITE)) - assert gte_21_match or lt_21_match - - -def test_dedent_tabs(): - result = 'first string\n\nsecond string' - assert dedent(" first string\n\n second string") == result - assert dedent("first string\n\n second string") == result - assert dedent("\tfirst string\n\n\tsecond string") == result - assert dedent("first string\n\n\tsecond string") == result +import sys + +import pytest +from django.test import TestCase + +from rest_framework.compat import apply_markdown +from rest_framework.utils.formatting import dedent +from rest_framework.views import APIView + +# We check that docstrings get nicely un-indented. +DESCRIPTION = """an example docstring +==================== + +* list +* list + +another header +-------------- + + code block + +indented + +# hash style header # + +``` json +[{ + "alpha": 1, + "beta: "this is a string" +}] +```""" + + +# If markdown is installed we also test it's working +# (and that our wrapped forces '=' to h2 and '-' to h3) +MARKDOWN_BASE = """

an example docstring

+ +

another header

+
code block
+
+

indented

+

hash style header

%s""" + +MARKDOWN_gte_33 = """ +
[{
\ + "alpha":\ + 1,
\ + "beta: "this\ + is a \ +string"
}]\ +
+


""" + +MARKDOWN_lt_33 = """ +
[{
\ + "alpha":\ + 1,
\ + "beta: "this\ + is a\ + string"
}]\ +
+ +


""" + + +class TestViewNamesAndDescriptions(TestCase): + def test_view_name_uses_class_name(self): + """ + Ensure view names are based on the class name. + """ + class MockView(APIView): + pass + assert MockView().get_view_name() == 'Mock' + + def test_view_name_uses_name_attribute(self): + class MockView(APIView): + name = 'Foo' + assert MockView().get_view_name() == 'Foo' + + def test_view_name_uses_suffix_attribute(self): + class MockView(APIView): + suffix = 'List' + assert MockView().get_view_name() == 'Mock List' + + def test_view_name_preferences_name_over_suffix(self): + class MockView(APIView): + name = 'Foo' + suffix = 'List' + assert MockView().get_view_name() == 'Foo' + + def test_view_description_uses_docstring(self): + """Ensure view descriptions are based on the docstring.""" + class MockView(APIView): + """an example docstring + ==================== + + * list + * list + + another header + -------------- + + code block + + indented + + # hash style header # + + ``` json + [{ + "alpha": 1, + "beta: "this is a string" + }] + ```""" + + assert MockView().get_view_description() == DESCRIPTION + + def test_view_description_uses_description_attribute(self): + class MockView(APIView): + description = 'Foo' + assert MockView().get_view_description() == 'Foo' + + def test_view_description_allows_empty_description(self): + class MockView(APIView): + """Description.""" + description = '' + assert MockView().get_view_description() == '' + + def test_view_description_can_be_empty(self): + """ + Ensure that if a view has no docstring, + then it's description is the empty string. + """ + class MockView(APIView): + pass + assert MockView().get_view_description() == '' + + def test_view_description_can_be_promise(self): + """ + Ensure a view may have a docstring that is actually a lazily evaluated + class that can be converted to a string. + + See: https://github.com/encode/django-rest-framework/issues/1708 + """ + # use a mock object instead of gettext_lazy to ensure that we can't end + # up with a test case string in our l10n catalog + + class MockLazyStr: + def __init__(self, string): + self.s = string + + def __str__(self): + return self.s + + class MockView(APIView): + __doc__ = MockLazyStr("a gettext string") + + assert MockView().get_view_description() == 'a gettext string' + + @pytest.mark.skipif(not apply_markdown, reason="Markdown is not installed") + def test_markdown(self): + """ + Ensure markdown to HTML works as expected. + """ + # Markdown 3.3 is only supported on Python 3.6 and higher + if sys.version_info >= (3, 6): + assert apply_markdown(DESCRIPTION) == MARKDOWN_BASE % MARKDOWN_gte_33 + else: + assert apply_markdown(DESCRIPTION) == MARKDOWN_BASE % MARKDOWN_lt_33 + + +def test_dedent_tabs(): + result = 'first string\n\nsecond string' + assert dedent(" first string\n\n second string") == result + assert dedent("first string\n\n second string") == result + assert dedent("\tfirst string\n\n\tsecond string") == result + assert dedent("first string\n\n\tsecond string") == result From 9c29f5013f2741b215d537d12f7a6ddecda5677e Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 1 Nov 2020 07:42:30 -0800 Subject: [PATCH 02/63] Use Python 3.9 release in Travis configuration --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2c2724bf63..7a820766e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,8 +21,8 @@ matrix: - { python: "3.8", env: DJANGO=3.1 } - { python: "3.8", env: DJANGO=master } - - { python: "3.9-dev", env: DJANGO=3.1 } - - { python: "3.9-dev", env: DJANGO=master } + - { python: "3.9", env: DJANGO=3.1 } + - { python: "3.9", env: DJANGO=master } - { python: "3.8", env: TOXENV=base } - { python: "3.8", env: TOXENV=lint } From 56e45081235783812f90098916a6e0131b08bbaa Mon Sep 17 00:00:00 2001 From: "James S Blachly, MD" Date: Mon, 2 Nov 2020 03:45:43 -0500 Subject: [PATCH 03/63] Fix #7612 (#7622) --- 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 22cc3d8aa9..d5815127b6 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -152,7 +152,7 @@ A more complete example of extra actions: user = self.get_object() serializer = PasswordSerializer(data=request.data) if serializer.is_valid(): - user.set_password(serializer.data['password']) + user.set_password(serializer.validated_data['password']) user.save() return Response({'status': 'password set'}) else: From 606df838856bd3fe2c2a76bead15a8fb7234479e Mon Sep 17 00:00:00 2001 From: Megan Gross <16373770+144mdgross@users.noreply.github.com> Date: Thu, 5 Nov 2020 01:43:45 -0700 Subject: [PATCH 04/63] Update throttling.md (#7606) There were recent updates to the `@action` decorator calling a little more attention to the kwargs it accepts. I thought it would be useful to also provide an example in the throttling section of how those kwargs can be used to define/override throttle_classes through the action decorator as well. --- docs/api-guide/throttling.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 215c735bf4..a3e42cacf9 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -59,7 +59,7 @@ using the `APIView` class-based views. } return Response(content) -Or, if you're using the `@api_view` decorator with function based views. +If you're using the `@api_view` decorator with function based views you can use the following decorator. @api_view(['GET']) @throttle_classes([UserRateThrottle]) @@ -69,6 +69,16 @@ Or, if you're using the `@api_view` decorator with function based views. } return Response(content) +It's also possible to set throttle classes for routes that are created using the `@action` decorator. +Throttle classes set in this way will override any viewset level class settings. + + @action(detail=True, methods=["post"], throttle_classes=[UserRateThrottle]) + def example_adhoc_method(request, pk=None): + content = { + 'status': 'request was permitted' + } + return Response(content) + ## How clients are identified The `X-Forwarded-For` HTTP header and `REMOTE_ADDR` WSGI variable are used to uniquely identify client IP addresses for throttling. If the `X-Forwarded-For` header is present then it will be used, otherwise the value of the `REMOTE_ADDR` variable from the WSGI environment will be used. From 80444a0afb1c05b4c85a34de2bc6c3f9614bdc86 Mon Sep 17 00:00:00 2001 From: bhealy-indeed <62403057+bhealy-indeed@users.noreply.github.com> Date: Thu, 5 Nov 2020 15:21:30 -0600 Subject: [PATCH 05/63] nit: Typo fix (#7629) --- docs/api-guide/exceptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index d7d73a2f2b..fbf3097e0c 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -38,7 +38,7 @@ Might receive an error response indicating that the `DELETE` method is not allow Validation errors are handled slightly differently, and will include the field names as the keys in the response. If the validation error was not specific to a particular field then it will use the "non_field_errors" key, or whatever string value has been set for the `NON_FIELD_ERRORS_KEY` setting. -Any example validation error might look like this: +An example validation error might look like this: HTTP/1.1 400 Bad Request Content-Type: application/json From 6da94e5700dd505f0fe2d9f376fbc212d02febc2 Mon Sep 17 00:00:00 2001 From: Georg Lukas Date: Thu, 12 Nov 2020 09:48:18 +0100 Subject: [PATCH 06/63] docs: outline the difference between JSON and form parsers. Fix #7633 --- docs/api-guide/parsers.md | 2 +- docs/api-guide/requests.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index e8f03de8bd..dde77c3e0e 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -73,7 +73,7 @@ Or, if you're using the `@api_view` decorator with function based views. ## JSONParser -Parses `JSON` request content. +Parses `JSON` request content. `request.data` will be populated with a dictionary of data. **.media_type**: `application/json` diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md index 1c336953ca..e877c868df 100644 --- a/docs/api-guide/requests.md +++ b/docs/api-guide/requests.md @@ -23,7 +23,7 @@ REST framework's Request objects provide flexible request parsing that allows yo * It includes all parsed content, including *file and non-file* inputs. * It supports parsing the content of HTTP methods other than `POST`, meaning that you can access the content of `PUT` and `PATCH` requests. -* It supports REST framework's flexible request parsing, rather than just supporting form data. For example you can handle incoming JSON data in the same way that you handle incoming form data. +* It supports REST framework's flexible request parsing, rather than just supporting form data. For example you can handle incoming [JSON data] similarly to how you handle incoming [form data]. For more details see the [parsers documentation]. @@ -136,5 +136,7 @@ Note that due to implementation reasons the `Request` class does not inherit fro [cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion [parsers documentation]: parsers.md +[JSON data]: parsers.md#jsonparser +[form data]: parsers.md#formparser [authentication documentation]: authentication.md [browser enhancements documentation]: ../topics/browser-enhancements.md From 96993d817a6af9c037ece7253cfae49efc814f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20Ony=C5=9Bko?= Date: Thu, 12 Nov 2020 18:42:42 +0100 Subject: [PATCH 07/63] Changed url to django docs so it points to the stable version (#7628) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Karol Onyśko --- docs/api-guide/relations.md | 2 +- docs/topics/internationalization.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index d3d8b30b8c..f444125cff 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -603,6 +603,6 @@ The [rest-framework-generic-relations][drf-nested-relations] library provides re [generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1 [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations -[django-intermediary-manytomany]: https://docs.djangoproject.com/en/2.2/topics/db/models/#intermediary-manytomany +[django-intermediary-manytomany]: https://docs.djangoproject.com/en/stable/topics/db/models/#intermediary-manytomany [dealing-with-nested-objects]: https://www.django-rest-framework.org/api-guide/serializers/#dealing-with-nested-objects [to_internal_value]: https://www.django-rest-framework.org/api-guide/serializers/#to_internal_valueself-data diff --git a/docs/topics/internationalization.md b/docs/topics/internationalization.md index 7cfc6e247c..c20cf9e339 100644 --- a/docs/topics/internationalization.md +++ b/docs/topics/internationalization.md @@ -103,10 +103,10 @@ You can find more information on how the language preference is determined in th For API clients the most appropriate of these will typically be to use the `Accept-Language` header; Sessions and cookies will not be available unless using session authentication, and generally better practice to prefer an `Accept-Language` header for API clients rather than using language URL prefixes. [cite]: https://youtu.be/Wa0VfS2q94Y -[django-translation]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation +[django-translation]: https://docs.djangoproject.com/en/stable/topics/i18n/translation [custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling [transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/ [django-po-source]: https://raw.githubusercontent.com/encode/django-rest-framework/master/rest_framework/locale/en_US/LC_MESSAGES/django.po -[django-language-preference]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#how-django-discovers-language-preference -[django-locale-paths]: https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-LOCALE_PATHS -[django-locale-name]: https://docs.djangoproject.com/en/1.7/topics/i18n/#term-locale-name +[django-language-preference]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#how-django-discovers-language-preference +[django-locale-paths]: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-LOCALE_PATHS +[django-locale-name]: https://docs.djangoproject.com/en/stable/topics/i18n/#term-locale-name From 3ab8d4706eb6c04b331ca4d57778a1989c668f2f Mon Sep 17 00:00:00 2001 From: babaroga Date: Sat, 21 Nov 2020 11:53:39 -0500 Subject: [PATCH 08/63] changed unicode to str --- docs/api-guide/authentication.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 5878040a48..da932a06c8 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -60,8 +60,8 @@ using the `APIView` class-based views. def get(self, request, format=None): content = { - 'user': unicode(request.user), # `django.contrib.auth.User` instance. - 'auth': unicode(request.auth), # None + 'user': str(request.user), # `django.contrib.auth.User` instance. + 'auth': str(request.auth), # None } return Response(content) @@ -72,8 +72,8 @@ Or, if you're using the `@api_view` decorator with function based views. @permission_classes([IsAuthenticated]) def example_view(request, format=None): content = { - 'user': unicode(request.user), # `django.contrib.auth.User` instance. - 'auth': unicode(request.auth), # None + 'user': str(request.user), # `django.contrib.auth.User` instance. + 'auth': str(request.auth), # None } return Response(content) From bb133522efaf6ce3ae8fdf1dec6cd79566cfd166 Mon Sep 17 00:00:00 2001 From: Erwin Junge Date: Sun, 22 Nov 2020 09:33:17 +0100 Subject: [PATCH 09/63] Small documentation fix --- docs/api-guide/serializers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index fd5dbb0e67..f05fe7e7e9 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -282,7 +282,7 @@ If a nested representation may optionally accept the `None` value you should pas content = serializers.CharField(max_length=200) created = serializers.DateTimeField() -Similarly if a nested representation should be a list of items, you should pass the `many=True` flag to the nested serialized. +Similarly if a nested representation should be a list of items, you should pass the `many=True` flag to the nested serializer. class CommentSerializer(serializers.Serializer): user = UserSerializer(required=False) From 8351747d98b97907e6bb096914bf287a22c5314b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 21 Dec 2020 16:41:12 +0000 Subject: [PATCH 10/63] Update index.md --- docs/index.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/index.md b/docs/index.md index 0273da9f14..0e6bb48f2e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -190,11 +190,6 @@ For support please see the [REST framework discussion group][group], try the `# For priority support please sign up for a [professional or premium sponsorship plan](https://fund.django-rest-framework.org/topics/funding/). -For updates on REST framework development, you may also want to follow [the author][twitter] on Twitter. - - - - ## Security If you believe you’ve found something in Django REST framework which has security implications, please **do not raise the issue in a public forum**. From 3db88778893579e1d7609b584ef35409c8aa5a22 Mon Sep 17 00:00:00 2001 From: Adrian Coveney Date: Wed, 25 Jul 2018 10:53:43 +0100 Subject: [PATCH 11/63] Clarify documentation for TemplateHTMLRenderer Clarify that the response from a view may need to be modified to provide TemplateHTMLRenderer with a dict for it to use. --- docs/api-guide/renderers.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index ca3a29b82c..954fb3bb98 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -103,6 +103,16 @@ Unlike other renderers, the data passed to the `Response` does not need to be se The TemplateHTMLRenderer will create a `RequestContext`, using the `response.data` as the context dict, and determine a template name to use to render the context. +--- + +**Note:** When used with a view that makes use of a serializer the `Response` sent for rendering may not be a dictionay and will need to be wrapped in a dict before returning to allow the TemplateHTMLRenderer to render it. For example: + +``` +response.data = {'results': response.data} +``` + +--- + The template name is determined by (in order of preference): 1. An explicit `template_name` argument passed to the response. From 19655edbf782aa1fbdd7f8cd56ff9e0b7786ad3c Mon Sep 17 00:00:00 2001 From: Sebastian Jordan Date: Wed, 6 Jan 2021 14:13:34 +0100 Subject: [PATCH 12/63] Handle tuples same as lists in ValidationError detail context (#7647) --- rest_framework/exceptions.py | 6 ++++-- tests/test_validation_error.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 943dcc88c3..fee8f024f2 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -20,7 +20,7 @@ def _get_error_details(data, default_code=None): Descend into a nested data structure, forcing any lazy translation strings or strings into `ErrorDetail`. """ - if isinstance(data, list): + if isinstance(data, (list, tuple)): ret = [ _get_error_details(item, default_code) for item in data ] @@ -150,7 +150,9 @@ def __init__(self, detail=None, code=None): # For validation failures, we may collect many errors together, # so the details should always be coerced to a list if not already. - if not isinstance(detail, dict) and not isinstance(detail, list): + if isinstance(detail, tuple): + detail = list(detail) + elif not isinstance(detail, dict) and not isinstance(detail, list): detail = [detail] self.detail = _get_error_details(detail, code) diff --git a/tests/test_validation_error.py b/tests/test_validation_error.py index 562fe37e6b..341c4342a5 100644 --- a/tests/test_validation_error.py +++ b/tests/test_validation_error.py @@ -2,6 +2,7 @@ from rest_framework import serializers, status from rest_framework.decorators import api_view +from rest_framework.exceptions import ValidationError from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory @@ -99,3 +100,12 @@ def test_function_based_view_exception_handler(self): response = view(request) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data == self.expected_response_data + + +class TestValidationErrorConvertsTuplesToLists(TestCase): + def test_validation_error_details(self): + error = ValidationError(detail=('message1', 'message2')) + assert isinstance(error.detail, list) + assert len(error.detail) == 2 + assert str(error.detail[0]) == 'message1' + assert str(error.detail[1]) == 'message2' From 3e956df6eb7e3b645d334fec372ad7f8a487d765 Mon Sep 17 00:00:00 2001 From: Tonye Jack Date: Tue, 2 Feb 2021 20:54:21 -0500 Subject: [PATCH 13/63] Fixed test --- requirements/requirements-testing.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index ad246e8570..99463560e2 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -2,3 +2,4 @@ pytest>=5.4.1,<5.5 pytest-django>=3.9.0,<3.10 pytest-cov>=2.7.1 +six>=1.14.0 From 1ec0f86b585cd87e4b413aeaad1ecc947bacfef2 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Tue, 16 Feb 2021 18:17:29 +0600 Subject: [PATCH 14/63] Dj32 (#7713) adds django 3.2 line to the build matrix --- .gitignore | 2 ++ .travis.yml | 7 +++++-- tox.ini | 5 ++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 41768084c5..82e885edee 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ *.db *~ .* +*.py.bak + /site/ /htmlcov/ diff --git a/.travis.yml b/.travis.yml index 7a820766e5..f9f22336fc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,18 +10,20 @@ matrix: - { python: "3.6", env: DJANGO=2.2 } - { python: "3.6", env: DJANGO=3.0 } - { python: "3.6", env: DJANGO=3.1 } - - { python: "3.6", env: DJANGO=master } + - { python: "3.6", env: DJANGO=3.2 } - { python: "3.7", env: DJANGO=2.2 } - { python: "3.7", env: DJANGO=3.0 } - { python: "3.7", env: DJANGO=3.1 } - - { python: "3.7", env: DJANGO=master } + - { python: "3.7", env: DJANGO=3.2 } - { python: "3.8", env: DJANGO=3.0 } - { python: "3.8", env: DJANGO=3.1 } + - { python: "3.8", env: DJANGO=3.2 } - { python: "3.8", env: DJANGO=master } - { python: "3.9", env: DJANGO=3.1 } + - { python: "3.9", env: DJANGO=3.2 } - { python: "3.9", env: DJANGO=master } - { python: "3.8", env: TOXENV=base } @@ -38,6 +40,7 @@ matrix: allow_failures: - env: DJANGO=master + - env: DJANGO=3.2 install: - pip install tox tox-travis diff --git a/tox.ini b/tox.ini index df6387d5e1..544bab163c 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,8 @@ envlist = {py35,py36,py37}-django22, {py36,py37,py38}-django30, {py36,py37,py38,py39}-django31, - {py36,py37,py38,py39}-djangomaster, + {py36,py37,py38,py39}-django32, + {py38,py39}-djangomaster, base,dist,lint,docs, [travis:env] @@ -11,6 +12,7 @@ DJANGO = 2.2: django22 3.0: django30 3.1: django31 + 3.2: django32 master: djangomaster [testenv] @@ -23,6 +25,7 @@ deps = django22: Django>=2.2,<3.0 django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 + django32: Django>=3.2a1,<4.0 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 8f6d2d2f9c7d9bb91a2b894533c0233620fa360f Mon Sep 17 00:00:00 2001 From: Usoof Mansoor Date: Tue, 2 Mar 2021 11:26:31 +0400 Subject: [PATCH 15/63] Update docs link to Django OAuth Toolkit. (#7737) --- docs/api-guide/authentication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index da932a06c8..61687e6421 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -357,7 +357,7 @@ The following third party packages are also available. ## Django OAuth Toolkit -The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support and works with Python 3.4+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**. +The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support and works with Python 3.4+. The package is maintained by [jazzband][jazzband] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**. #### Installation & configuration @@ -448,7 +448,7 @@ There are currently two forks of this project. [djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth [oauth-1.0a]: https://oauth.net/core/1.0a/ [django-oauth-toolkit]: https://github.com/evonove/django-oauth-toolkit -[evonove]: https://github.com/evonove/ +[jazzband]: https://github.com/jazzband/ [oauthlib]: https://github.com/idan/oauthlib [djangorestframework-simplejwt]: https://github.com/davesque/django-rest-framework-simplejwt [etoccalino]: https://github.com/etoccalino/ From de7468d0b4c48007aed734fee22db0b79b22e70b Mon Sep 17 00:00:00 2001 From: Jonathan Mortensen <56177725+jmo-qap@users.noreply.github.com> Date: Wed, 3 Mar 2021 03:15:39 -0800 Subject: [PATCH 16/63] support multi db atomic_requests (#7739) --- rest_framework/views.py | 8 ++++---- tests/conftest.py | 4 ++++ tests/test_atomic_requests.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index d1b5e4ed90..5b06220691 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -3,7 +3,7 @@ """ from django.conf import settings from django.core.exceptions import PermissionDenied -from django.db import connection, models, transaction +from django.db import connections, models from django.http import Http404 from django.http.response import HttpResponseBase from django.utils.cache import cc_delim_re, patch_vary_headers @@ -63,9 +63,9 @@ def get_view_description(view, html=False): def set_rollback(): - atomic_requests = connection.settings_dict.get('ATOMIC_REQUESTS', False) - if atomic_requests and connection.in_atomic_block: - transaction.set_rollback(True) + for db in connections.all(): + if db.settings_dict['ATOMIC_REQUESTS'] and db.in_atomic_block: + db.set_rollback(True) def exception_handler(exc, context): diff --git a/tests/conftest.py b/tests/conftest.py index ac29e4a429..cc32cc6373 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,6 +24,10 @@ def pytest_configure(config): 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:' + }, + 'secondary': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:' } }, SITE_ID=1, diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py index 15b41e02f4..beda5cba19 100644 --- a/tests/test_atomic_requests.py +++ b/tests/test_atomic_requests.py @@ -130,6 +130,41 @@ def test_api_exception_rollback_transaction(self): assert BasicModel.objects.count() == 0 +@unittest.skipUnless( + connection.features.uses_savepoints, + "'atomic' requires transactions and savepoints." +) +class MultiDBTransactionAPIExceptionTests(TestCase): + databases = '__all__' + + def setUp(self): + self.view = APIExceptionView.as_view() + connections.databases['default']['ATOMIC_REQUESTS'] = True + connections.databases['secondary']['ATOMIC_REQUESTS'] = True + + def tearDown(self): + connections.databases['default']['ATOMIC_REQUESTS'] = False + connections.databases['secondary']['ATOMIC_REQUESTS'] = False + + def test_api_exception_rollback_transaction(self): + """ + Transaction is rollbacked by our transaction atomic block. + """ + request = factory.post('/') + num_queries = 4 if connection.features.can_release_savepoints else 3 + with self.assertNumQueries(num_queries): + # 1 - begin savepoint + # 2 - insert + # 3 - rollback savepoint + # 4 - release savepoint + with transaction.atomic(), transaction.atomic(using='secondary'): + response = self.view(request) + assert transaction.get_rollback() + assert transaction.get_rollback(using='secondary') + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert BasicModel.objects.count() == 0 + + @unittest.skipUnless( connection.features.uses_savepoints, "'atomic' requires transactions and savepoints." From 1deb8ae370df6c65b6ec3fadf71d9391236be06f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Mar 2021 15:06:42 +0000 Subject: [PATCH 17/63] Update FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index d7c23d6351..5a830ca53f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +github: encode custom: https://fund.django-rest-framework.org/topics/funding/ From 344235ab371dcd80e2ff2546bf673d586cde4310 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Mar 2021 15:32:19 +0000 Subject: [PATCH 18/63] Create config.yml --- .github/ISSUE_TEMPLATE/config.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..8bb8d8c210 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +contact_links: +- name: Discussions + url: https://github.com/encode/django-rest-framework/discussions + about: > + The "Discussions" forum is where you want to be headed too. Please only raise an issue if you've been advised to do so after discussion. Thank you! 🙏 From c9a00bdb2c0838f17e9a64f6ccab69d14d8ea6e7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Mar 2021 15:33:14 +0000 Subject: [PATCH 19/63] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 8bb8d8c210..fd0db4d66a 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,4 +2,6 @@ contact_links: - name: Discussions url: https://github.com/encode/django-rest-framework/discussions about: > - The "Discussions" forum is where you want to be headed too. Please only raise an issue if you've been advised to do so after discussion. Thank you! 🙏 + The "Discussions" forum is where you want to be headed too. + Please only raise an issue if you've been advised to do so after discussion. + Thank you! 🙏 From db0bb5ef42879a69e7262aebf0f42ce173248a61 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Mar 2021 15:39:22 +0000 Subject: [PATCH 20/63] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index fd0db4d66a..bf0c054a63 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,6 +2,4 @@ contact_links: - name: Discussions url: https://github.com/encode/django-rest-framework/discussions about: > - The "Discussions" forum is where you want to be headed too. - Please only raise an issue if you've been advised to do so after discussion. - Thank you! 🙏 + The "Discussions" forum is where you want to start. 💖 From 37b8d2018d4dbe2efc012f23baff1cca4df15675 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Mar 2021 15:42:38 +0000 Subject: [PATCH 21/63] Create 1-issue.md --- .github/ISSUE_TEMPLATE/1-issue.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/1-issue.md diff --git a/.github/ISSUE_TEMPLATE/1-issue.md b/.github/ISSUE_TEMPLATE/1-issue.md new file mode 100644 index 0000000000..0da1549534 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-issue.md @@ -0,0 +1,10 @@ +--- +name: Issue +about: Please only raise an issue if you've been advised to do so after discussion. Thanks! 🙏 +--- + +## Checklist + +- [ ] Raised initially as discussion #... +- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.) +- [ ] I have reduced the issue to the simplest possible case. From ee51145574c9d868baab87a65ac18878dee4ee12 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Mar 2021 15:43:57 +0000 Subject: [PATCH 22/63] Update config.yml --- .github/ISSUE_TEMPLATE/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index bf0c054a63..382fc521aa 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,3 +1,4 @@ +blank_issues_enabled: false contact_links: - name: Discussions url: https://github.com/encode/django-rest-framework/discussions From ec29ff8a8013dd3383344bb78eea479d025e2a87 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Mar 2021 15:45:40 +0000 Subject: [PATCH 23/63] Delete ISSUE_TEMPLATE.md --- ISSUE_TEMPLATE.md | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 ISSUE_TEMPLATE.md diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md deleted file mode 100644 index 566bf95436..0000000000 --- a/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,14 +0,0 @@ -## Checklist - -- [ ] I have verified that that issue exists against the `master` branch of Django REST framework. -- [ ] I have searched for similar issues in both open and closed tickets and cannot find a duplicate. -- [ ] This is not a usage question. (Those should be directed to the [discussion group](https://groups.google.com/forum/#!forum/django-rest-framework) instead.) -- [ ] This cannot be dealt with as a third party library. (We prefer new functionality to be [in the form of third party libraries](https://www.django-rest-framework.org/community/third-party-packages/#about-third-party-packages) where possible.) -- [ ] I have reduced the issue to the simplest possible case. -- [ ] I have included a failing test as a pull request. (If you are unable to do so we can still accept the issue.) - -## Steps to reproduce - -## Expected behavior - -## Actual behavior From ef112f5017bb6d3d6a331ff485dbf6a9209fb8b4 Mon Sep 17 00:00:00 2001 From: arcanemachine Date: Mon, 8 Mar 2021 04:46:35 -0700 Subject: [PATCH 24/63] Provide example for dict in ValidationError detail (#7788) Added a sentence describing the use of a dictionary as the `detail` argument of a ValidationError, and how it can be used to add field-level errors during object-level validation. --- docs/api-guide/exceptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index fbf3097e0c..e62a7e4f9d 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -222,7 +222,7 @@ By default this exception results in a response with the HTTP status code "429 T The `ValidationError` exception is slightly different from the other `APIException` classes: * The `detail` argument is mandatory, not optional. -* The `detail` argument may be a list or dictionary of error details, and may also be a nested data structure. +* The `detail` argument may be a list or dictionary of error details, and may also be a nested data structure. By using a dictionary, you can specify field-level errors while performing object-level validation in the `validate()` method of a serializer. For example. `raise serializers.ValidationError({'name': 'Please enter a valid name.'})` * By convention you should import the serializers module and use a fully qualified `ValidationError` style, in order to differentiate it from Django's built-in validation error. For example. `raise serializers.ValidationError('This field must be an integer value.')` The `ValidationError` class should be used for serializer and field validation, and by validator classes. It is also raised when calling `serializer.is_valid` with the `raise_exception` keyword argument: From 234527959d5ad6eef2bc0f8af1aa2e149fc8bc60 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Merx Date: Mon, 8 Mar 2021 13:08:26 +0100 Subject: [PATCH 25/63] Have options example in documenting-your-api.md to return a Response (#7639) It was returning data which is not correct. Closes #7638. Co-authored-by: Jean-Pierre Merx --- docs/topics/documenting-your-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md index cd7e5098fe..5eabeee7bb 100644 --- a/docs/topics/documenting-your-api.md +++ b/docs/topics/documenting-your-api.md @@ -202,7 +202,7 @@ You can modify the response behavior to `OPTIONS` requests by overriding the `op meta = self.metadata_class() data = meta.determine_metadata(request, self) data.pop('description') - return data + return Response(data=data, status=status.HTTP_200_OK) See [the Metadata docs][metadata-docs] for more details. From e32ebc41998ffd7f22f6e691badb86a709c89ba7 Mon Sep 17 00:00:00 2001 From: Alex Cotsarelis <57880995+alex-cots@users.noreply.github.com> Date: Mon, 8 Mar 2021 07:09:17 -0500 Subject: [PATCH 26/63] Docs: DjangoModelPermissions works on views with get_queryset() method. (#7693) Sentinel querysets not needed after v3.1.2 --- docs/api-guide/permissions.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index ade1462572..f694d6be50 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -169,7 +169,7 @@ This permission is suitable if you want to your API to allow read permissions to ## DjangoModelPermissions -This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property set. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. +This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property or `get_queryset()` method. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. * `POST` requests require the user to have the `add` permission on the model. * `PUT` and `PATCH` requests require the user to have the `change` permission on the model. @@ -179,12 +179,6 @@ The default behaviour can also be overridden to support custom model permissions To use custom model permissions, override `DjangoModelPermissions` and set the `.perms_map` property. Refer to the source code for details. -#### Using with views that do not include a `queryset` attribute. - -If you're using this permission with a view that uses an overridden `get_queryset()` method there may not be a `queryset` attribute on the view. In this case we suggest also marking the view with a sentinel queryset, so that this class can determine the required permissions. For example: - - queryset = User.objects.none() # Required for DjangoModelPermissions - ## DjangoModelPermissionsOrAnonReadOnly Similar to `DjangoModelPermissions`, but also allows unauthenticated users to have read-only access to the API. From b463878132004d33182b2f61be8209bfad79af7f Mon Sep 17 00:00:00 2001 From: Igor Polyakov Date: Tue, 9 Mar 2021 17:16:19 +0700 Subject: [PATCH 27/63] Commas added in README (#7730) To make it more comfortable for users to copy and paste snippets --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8af1466f8a..305f923898 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ router.register(r'users', UserViewSet) # Additionally, we include login URLs for the browsable API. urlpatterns = [ path('', include(router.urls)), - path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) + path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), ] ``` @@ -131,7 +131,7 @@ REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly', ] } ``` From e9a54e38e1c864919c79a8b88d83b8d61d477053 Mon Sep 17 00:00:00 2001 From: kuter Date: Tue, 9 Mar 2021 11:17:30 +0100 Subject: [PATCH 28/63] add support for Yes/No literals with BooleanField (#7701) --- rest_framework/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index fdfba13f26..b6c9ee5c52 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -704,7 +704,7 @@ class BooleanField(Field): initial = False TRUE_VALUES = { 't', 'T', - 'y', 'Y', 'yes', 'YES', + 'y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE', 'on', 'On', 'ON', '1', 1, @@ -712,7 +712,7 @@ class BooleanField(Field): } FALSE_VALUES = { 'f', 'F', - 'n', 'N', 'no', 'NO', + 'n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE', 'off', 'Off', 'OFF', '0', 0, 0.0, From 393f8679952b3e81b56db3e3c498aeb1f8849f52 Mon Sep 17 00:00:00 2001 From: Cas Ebbers <617080+CasEbbers@users.noreply.github.com> Date: Tue, 9 Mar 2021 11:21:11 +0100 Subject: [PATCH 29/63] Overlooked translation in search.html (#7551) --- rest_framework/templates/rest_framework/filters/search.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/templates/rest_framework/filters/search.html b/rest_framework/templates/rest_framework/filters/search.html index edb28d45d8..065c3889ac 100644 --- a/rest_framework/templates/rest_framework/filters/search.html +++ b/rest_framework/templates/rest_framework/filters/search.html @@ -5,7 +5,7 @@

{% trans "Search" %}

- +
From a89a6427d3af7045c8c35693cc830c8b76b8a00d Mon Sep 17 00:00:00 2001 From: Nathan Glover <15344788+nathanglover@users.noreply.github.com> Date: Tue, 9 Mar 2021 05:22:37 -0500 Subject: [PATCH 30/63] #7703 adding deprecations to release notes (#7716) * #7703 adding deprecations to release notes * #7703 - update link for gh6687 --- docs/community/release-notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index c981b9ac92..49fb655b01 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -177,6 +177,8 @@ Date: 28th September 2020 * Don't strict disallow redundant `SerializerMethodField` field name arguments. * Don't render extra actions in browable API if not authenticated. * Strip null characters from search parameters. +* Deprecate the `detail_route` decorator in favor of `action`, which accepts a `detail` bool. Use `@action(detail=True)` instead. [gh6687] +* Deprecate the `list_route` decorator in favor of `action`, which accepts a `detail` bool. Use `@action(detail=False)` instead. [gh6687] ## 3.9.x series @@ -2270,6 +2272,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh6680]: https://github.com/encode/django-rest-framework/issues/6680 [gh6317]: https://github.com/encode/django-rest-framework/issues/6317 +[gh6687]: https://github.com/encode/django-rest-framework/issues/6687 [gh6892]: https://github.com/encode/django-rest-framework/issues/6892 From 05512160abb4c2110afff9e82f8f523be68476cf Mon Sep 17 00:00:00 2001 From: David Kerkeslager Date: Tue, 9 Mar 2021 05:49:03 -0500 Subject: [PATCH 31/63] Respect allow_null=True on DecimalFields (#7718) * Handle None in to_representation() * Return None as '' in to_representation() when coerce_to_string=True * Handle '' as None in to_internal_value(), for symmetry with to_representation(), and because the empty concept doesn't make sense for Decimal. --- rest_framework/fields.py | 9 +++++++++ tests/test_fields.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index b6c9ee5c52..d91299484e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1063,6 +1063,9 @@ def to_internal_value(self, data): try: value = decimal.Decimal(data) except decimal.DecimalException: + if data == '' and self.allow_null: + return None + self.fail('invalid') if value.is_nan(): @@ -1112,6 +1115,12 @@ def validate_precision(self, value): def to_representation(self, value): coerce_to_string = getattr(self, 'coerce_to_string', api_settings.COERCE_DECIMAL_TO_STRING) + if value is None: + if coerce_to_string: + return '' + else: + return None + if not isinstance(value, decimal.Decimal): value = decimal.Decimal(str(value).strip()) diff --git a/tests/test_fields.py b/tests/test_fields.py index fdd570d8a6..5842553f02 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1090,6 +1090,9 @@ class TestDecimalField(FieldValues): '2E+1': Decimal('20'), } invalid_inputs = ( + (None, ["This field may not be null."]), + ('', ["A valid number is required."]), + (' ', ["A valid number is required."]), ('abc', ["A valid number is required."]), (Decimal('Nan'), ["A valid number is required."]), (Decimal('Snan'), ["A valid number is required."]), @@ -1115,6 +1118,32 @@ class TestDecimalField(FieldValues): field = serializers.DecimalField(max_digits=3, decimal_places=1) +class TestAllowNullDecimalField(FieldValues): + valid_inputs = { + None: None, + '': None, + ' ': None, + } + invalid_inputs = {} + outputs = { + None: '', + } + field = serializers.DecimalField(max_digits=3, decimal_places=1, allow_null=True) + + +class TestAllowNullNoStringCoercionDecimalField(FieldValues): + valid_inputs = { + None: None, + '': None, + ' ': None, + } + invalid_inputs = {} + outputs = { + None: None, + } + field = serializers.DecimalField(max_digits=3, decimal_places=1, allow_null=True, coerce_to_string=False) + + class TestMinMaxDecimalField(FieldValues): """ Valid and invalid values for `DecimalField` with min and max limits. From 95ae92ef23859b45d03bcc2facf04fab0acee09d Mon Sep 17 00:00:00 2001 From: Berkant Date: Tue, 9 Mar 2021 14:34:18 +0300 Subject: [PATCH 32/63] Fix #7706 (#7724) Handle non-dict values for NestedSerializer during BrowsableAPI rendering. --- rest_framework/utils/serializer_helpers.py | 4 ++-- tests/test_bound_fields.py | 27 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index b18fbe0df9..cd0373adcb 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -1,5 +1,5 @@ from collections import OrderedDict -from collections.abc import MutableMapping +from collections.abc import Mapping, MutableMapping from django.utils.encoding import force_str @@ -101,7 +101,7 @@ class NestedBoundField(BoundField): """ def __init__(self, field, value, errors, prefix=''): - if value is None or value == '': + if value is None or value == '' or not isinstance(value, Mapping): value = {} super().__init__(field, value, errors, prefix) diff --git a/tests/test_bound_fields.py b/tests/test_bound_fields.py index dc5ab542ff..dec8793c33 100644 --- a/tests/test_bound_fields.py +++ b/tests/test_bound_fields.py @@ -163,6 +163,33 @@ class ExampleSerializer(serializers.Serializer): rendered_packed = ''.join(rendered.split()) assert rendered_packed == expected_packed + def test_rendering_nested_fields_with_not_mappable_value(self): + from rest_framework.renderers import HTMLFormRenderer + + class Nested(serializers.Serializer): + text_field = serializers.CharField() + + class ExampleSerializer(serializers.Serializer): + nested = Nested() + + serializer = ExampleSerializer(data={'nested': 1}) + assert not serializer.is_valid() + renderer = HTMLFormRenderer() + for field in serializer: + rendered = renderer.render_field(field, {}) + expected_packed = ( + '
' + 'Nested' + '' + '' + '' + '' + '
' + ) + + rendered_packed = ''.join(rendered.split()) + assert rendered_packed == expected_packed + class TestJSONBoundField: def test_as_form_fields(self): From 82b8a64a02ccc4ff678ac9f9565f25463ecad871 Mon Sep 17 00:00:00 2001 From: Chris Guo <41265033+chrisguox@users.noreply.github.com> Date: Tue, 9 Mar 2021 19:49:19 +0800 Subject: [PATCH 33/63] docs: add example for caching (#7118) --- docs/api-guide/caching.md | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/docs/api-guide/caching.md b/docs/api-guide/caching.md index 96517b15ee..ab4f82cd2f 100644 --- a/docs/api-guide/caching.md +++ b/docs/api-guide/caching.md @@ -13,13 +13,13 @@ provided in Django. Django provides a [`method_decorator`][decorator] to use decorators with class based views. This can be used with -other cache decorators such as [`cache_page`][page] and -[`vary_on_cookie`][cookie]. +other cache decorators such as [`cache_page`][page], +[`vary_on_cookie`][cookie] and [`vary_on_headers`][headers]. ```python from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page -from django.views.decorators.vary import vary_on_cookie +from django.views.decorators.vary import vary_on_cookie, vary_on_headers from rest_framework.response import Response from rest_framework.views import APIView @@ -27,8 +27,7 @@ from rest_framework import viewsets class UserViewSet(viewsets.ViewSet): - - # Cache requested url for each user for 2 hours + # With cookie: cache requested url for each user for 2 hours @method_decorator(cache_page(60*60*2)) @method_decorator(vary_on_cookie) def list(self, request, format=None): @@ -38,8 +37,18 @@ class UserViewSet(viewsets.ViewSet): return Response(content) -class PostView(APIView): +class ProfileView(APIView): + # With auth: cache requested url for each user for 2 hours + @method_decorator(cache_page(60*60*2)) + @method_decorator(vary_on_headers("Authorization",)) + def get(self, request, format=None): + content = { + 'user_feed': request.user.get_user_feed() + } + return Response(content) + +class PostView(APIView): # Cache page for the requested url @method_decorator(cache_page(60*60*2)) def get(self, request, format=None): @@ -55,4 +64,5 @@ class PostView(APIView): [page]: https://docs.djangoproject.com/en/dev/topics/cache/#the-per-view-cache [cookie]: https://docs.djangoproject.com/en/dev/topics/http/decorators/#django.views.decorators.vary.vary_on_cookie +[headers]: https://docs.djangoproject.com/en/dev/topics/http/decorators/#django.views.decorators.vary.vary_on_headers [decorator]: https://docs.djangoproject.com/en/dev/topics/class-based-views/intro/#decorating-the-class From 747fef6134539c8ce9042b38639459c59e0440a5 Mon Sep 17 00:00:00 2001 From: Celia Oakley Date: Tue, 9 Mar 2021 22:51:52 +1100 Subject: [PATCH 34/63] Add django-rest-authemail to Third Party Packages (#7679) * Add django-rest-authemail to Authentication * Add django-rest-authemail to Third Party Packages --- docs/api-guide/authentication.md | 5 +++++ docs/community/third-party-packages.md | 2 ++ 2 files changed, 7 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 61687e6421..d13c5a2f0d 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -432,6 +432,10 @@ There are currently two forks of this project. [drfpasswordless][drfpasswordless] adds (Medium, Square Cash inspired) passwordless support to Django REST Framework's own TokenAuthentication scheme. Users log in and sign up with a token sent to a contact point like an email address or a mobile number. +## django-rest-authemail + +[django-rest-authemail][django-rest-authemail] provides a RESTful API interface for user signup and authentication. Email addresses are used for authentication, rather than usernames. API endpoints are available for signup, signup email verification, login, logout, password reset, password reset verification, email change, email change verification, password change, and user detail. A fully-functional example project and detailed instructions are included. + [cite]: https://jacobian.org/writing/rest-worst-practices/ [http401]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 [http403]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4 @@ -466,3 +470,4 @@ There are currently two forks of this project. [django-rest-framework-social-oauth2]: https://github.com/PhilipGarnero/django-rest-framework-social-oauth2 [django-rest-knox]: https://github.com/James1345/django-rest-knox [drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless +[django-rest-authemail]: https://github.com/celiao/django-rest-authemail diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index d4359890dd..88836cfc61 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -190,6 +190,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [django-rest-auth][django-rest-auth] - Provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc. * [drf-oidc-auth][drf-oidc-auth] - Implements OpenID Connect token authentication for DRF. * [drfpasswordless][drfpasswordless] - Adds (Medium, Square Cash inspired) passwordless logins and signups via email and mobile numbers. +* [django-rest-authemail][django-rest-authemail] - Provides a RESTful API for user signup and authentication using email addresses. ### Permissions @@ -362,3 +363,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-elasticsearch-dsl-drf]: https://github.com/barseghyanartur/django-elasticsearch-dsl-drf [django-api-client]: https://github.com/rhenter/django-api-client [drf-psq]: https://github.com/drf-psq/drf-psq +[django-rest-authemail]: https://github.com/celiao/django-rest-authemail From 4e0d6c411805743688bd25a2ad8021441a1ae1ac Mon Sep 17 00:00:00 2001 From: Vitor Hugo Date: Tue, 9 Mar 2021 08:54:58 -0300 Subject: [PATCH 35/63] Update default.css (#7643) When I apply a theme to the bootstrap used in the project, boolean inputs are out of line with the rest of the form. With this small payment, this no longer occurs. --- rest_framework/static/rest_framework/css/default.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css index 86fef17737..51ca3ba19e 100644 --- a/rest_framework/static/rest_framework/css/default.css +++ b/rest_framework/static/rest_framework/css/default.css @@ -40,7 +40,7 @@ td.nested > table { margin: 0; } -form select, form input, form textarea { +form select, form input:not([type=checkbox]), form textarea { width: 90%; } From 750bad0a58bdec85fa5a630a519277bac3b36535 Mon Sep 17 00:00:00 2001 From: Romain Rigaux Date: Tue, 9 Mar 2021 04:00:51 -0800 Subject: [PATCH 36/63] Actually use the loginUser arguments in the example (#7714) --- docs/topics/api-clients.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/api-clients.md b/docs/topics/api-clients.md index 9b61eaf427..b9f5e3ecd8 100644 --- a/docs/topics/api-clients.md +++ b/docs/topics/api-clients.md @@ -453,7 +453,7 @@ For example, using the "Django REST framework JWT" package function loginUser(username, password) { let action = ["api-token-auth", "obtain-token"]; - let params = {username: "example", email: "example@example.com"}; + let params = {username: username, password: password}; client.action(schema, action, params).then(function(result) { // On success, instantiate an authenticated client. let auth = window.coreapi.auth.TokenAuthentication({ From a3ae8ea77efa2fa9af69da5dfda9128ef94c0fde Mon Sep 17 00:00:00 2001 From: Dmitry Mugtasimov Date: Tue, 9 Mar 2021 15:06:12 +0300 Subject: [PATCH 37/63] Do not do `SELECT count(*) FROM ...` if pagination is not requested (#6098) * Do not do `SELECT count(*) FROM ...` if pagination is not requested * Update pagination.py Co-authored-by: Tom Christie --- rest_framework/pagination.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 60a57c8e4a..4db6461163 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -376,11 +376,11 @@ class LimitOffsetPagination(BasePagination): template = 'rest_framework/pagination/numbers.html' def paginate_queryset(self, queryset, request, view=None): - self.count = self.get_count(queryset) self.limit = self.get_limit(request) if self.limit is None: return None + self.count = self.get_count(queryset) self.offset = self.get_offset(request) self.request = request if self.count > self.limit and self.template is not None: From c69e2e4eaafd7270565f0ecab7635f8988bc0f6d Mon Sep 17 00:00:00 2001 From: PaulGilmartin Date: Wed, 10 Mar 2021 10:02:38 +0100 Subject: [PATCH 38/63] Add graphwrap to third-party-packages.md (#7819) * Add graphwrap to third-party-packages documentation * Fix typo in third party packages docs * Remove additional newline Co-authored-by: Paul Gilmartin Co-authored-by: Tom Christie --- 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 88836cfc61..93ed3e2ca7 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -215,6 +215,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [drf-action-serializer][drf-action-serializer] - Serializer providing per-action fields config for use with ViewSets to prevent having to write multiple serializers. * [djangorestframework-dataclasses][djangorestframework-dataclasses] - Serializer providing automatic field generation for Python dataclasses, like the built-in ModelSerializer does for models. * [django-restql][django-restql] - Turn your REST API into a GraphQL like API(It allows clients to control which fields will be sent in a response, uses GraphQL like syntax, supports read and write on both flat and nested fields). +* [graphwrap][graphwrap] - Transform your REST API into a fully compliant GraphQL API with just two lines of code. Leverages [Graphene-Django](https://docs.graphene-python.org/projects/django/en/latest/) to dynamically build, at runtime, a GraphQL ObjectType for each view in your API. ### Serializer fields @@ -364,3 +365,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-api-client]: https://github.com/rhenter/django-api-client [drf-psq]: https://github.com/drf-psq/drf-psq [django-rest-authemail]: https://github.com/celiao/django-rest-authemail +[graphwrap]: https://github.com/PaulGilmartin/graph_wrap From c603b98403e05070e01852f18fa5b11b11e366c8 Mon Sep 17 00:00:00 2001 From: Pierre Chiquet Date: Wed, 10 Mar 2021 13:03:15 +0100 Subject: [PATCH 39/63] Fix OpenAPISchema rendering for timedelta (#7641) * Add failing test when rendering to json a schema with timedelta * Fix JSONOpenAPIRenderer for fields with default=timedelta() * fix isort * fix test for python 3.5 Co-authored-by: Pierre Chiquet --- rest_framework/renderers.py | 3 ++- tests/schemas/test_openapi.py | 16 +++++++++++++++- tests/schemas/views.py | 2 ++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 3c4be8aeb0..5b7ba8a8c8 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -1063,7 +1063,8 @@ def ignore_aliases(self, data): class JSONOpenAPIRenderer(BaseRenderer): media_type = 'application/vnd.oai.openapi+json' charset = None + encoder_class = encoders.JSONEncoder format = 'openapi-json' def render(self, data, media_type=None, renderer_context=None): - return json.dumps(data, indent=2).encode('utf-8') + return json.dumps(data, cls=self.encoder_class, indent=2).encode('utf-8') diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 542c377b15..871eb1b302 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -11,7 +11,8 @@ from rest_framework.compat import uritemplate from rest_framework.parsers import JSONParser, MultiPartParser from rest_framework.renderers import ( - BaseRenderer, BrowsableAPIRenderer, JSONRenderer, OpenAPIRenderer + BaseRenderer, BrowsableAPIRenderer, JSONOpenAPIRenderer, JSONRenderer, + OpenAPIRenderer ) from rest_framework.request import Request from rest_framework.schemas.openapi import AutoSchema, SchemaGenerator @@ -992,6 +993,19 @@ def test_schema_construction(self): assert 'openapi' in schema assert 'paths' in schema + def test_schema_rendering_to_json(self): + patterns = [ + path('example/', views.ExampleGenericAPIView.as_view()), + ] + generator = SchemaGenerator(patterns=patterns) + + request = create_request('/') + schema = generator.get_schema(request=request) + ret = JSONOpenAPIRenderer().render(schema) + + assert b'"openapi": "' in ret + assert b'"default": "0.0"' in ret + def test_schema_with_no_paths(self): patterns = [] generator = SchemaGenerator(patterns=patterns) diff --git a/tests/schemas/views.py b/tests/schemas/views.py index 18b3beae4e..f1ed0bd4e3 100644 --- a/tests/schemas/views.py +++ b/tests/schemas/views.py @@ -1,4 +1,5 @@ import uuid +from datetime import timedelta from django.core.validators import ( DecimalValidator, MaxLengthValidator, MaxValueValidator, @@ -59,6 +60,7 @@ def get(self, *args, **kwargs): class ExampleSerializer(serializers.Serializer): date = serializers.DateField() datetime = serializers.DateTimeField() + duration = serializers.DurationField(default=timedelta()) hstore = serializers.HStoreField() uuid_field = serializers.UUIDField(default=uuid.uuid4) From 6f6d402d043acb6400736f882c6a48e3c7e773f4 Mon Sep 17 00:00:00 2001 From: sarath ak Date: Wed, 10 Mar 2021 18:02:10 +0530 Subject: [PATCH 40/63] Allow 'get_page' method for overriding #7626 (#7652) --- rest_framework/pagination.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 4db6461163..0f0aa9ccf9 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -198,7 +198,7 @@ def paginate_queryset(self, queryset, request, view=None): return None paginator = self.django_paginator_class(queryset, page_size) - page_number = request.query_params.get(self.page_query_param, 1) + page_number = self.get_page_number(request) if page_number in self.last_page_strings: page_number = paginator.num_pages @@ -217,6 +217,9 @@ def paginate_queryset(self, queryset, request, view=None): self.request = request return list(self.page) + def get_page_number(self, request): + return request.query_params.get(self.page_query_param, 1) + def get_paginated_response(self, data): return Response(OrderedDict([ ('count', self.page.paginator.count), From cef74d1726a73991e6805c1bf31ec7464dc738d1 Mon Sep 17 00:00:00 2001 From: John Alexis Munera Date: Wed, 10 Mar 2021 07:37:06 -0500 Subject: [PATCH 41/63] Add rest-framework-actions to Third Party Packages (#7688) This pull request adds rest-framework-actions to Third Party Packages, under Views rest-framework-actions can be found on PyPi here: https://pypi.org/project/rest-framework-actions/ Co-authored-by: Tom Christie --- 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 93ed3e2ca7..838122cbe6 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -227,6 +227,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [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. +* [rest-framework-actions][rest-framework-actions] - Provides control over each action in ViewSets. Serializers per action, method. ### Routers @@ -366,3 +367,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [drf-psq]: https://github.com/drf-psq/drf-psq [django-rest-authemail]: https://github.com/celiao/django-rest-authemail [graphwrap]: https://github.com/PaulGilmartin/graph_wrap +[rest-framework-actions]: https://github.com/AlexisMunera98/rest-framework-actions From 4f3cd8c7b0495276e7dc99cc06a1069082a37f9d Mon Sep 17 00:00:00 2001 From: Eshaan Bansal Date: Wed, 10 Mar 2021 18:10:45 +0530 Subject: [PATCH 42/63] add django-rest-durin to 3rd party auth libs [docs] (#7615) Co-authored-by: Tom Christie --- docs/api-guide/authentication.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index d13c5a2f0d..4497f73bd0 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -436,6 +436,12 @@ There are currently two forks of this project. [django-rest-authemail][django-rest-authemail] provides a RESTful API interface for user signup and authentication. Email addresses are used for authentication, rather than usernames. API endpoints are available for signup, signup email verification, login, logout, password reset, password reset verification, email change, email change verification, password change, and user detail. A fully-functional example project and detailed instructions are included. +## Django-Rest-Durin + +[Django-Rest-Durin][django-rest-durin] is built with the idea to have one library that does token auth for multiple Web/CLI/Mobile API clients via one interface but allows different token configuration for each API Client that consumes the API. It provides support for multiple tokens per user via custom models, views, permissions that work with Django-Rest-Framework. The token expiration time can be different per API client and is customizable via the Django Admin Interface. + +More information can be found in the [Documentation](https://django-rest-durin.readthedocs.io/en/latest/index.html). + [cite]: https://jacobian.org/writing/rest-worst-practices/ [http401]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 [http403]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4 @@ -471,3 +477,4 @@ There are currently two forks of this project. [django-rest-knox]: https://github.com/James1345/django-rest-knox [drfpasswordless]: https://github.com/aaronn/django-rest-framework-passwordless [django-rest-authemail]: https://github.com/celiao/django-rest-authemail +[django-rest-durin]: https://github.com/eshaan7/django-rest-durin From c78f99217673fbcdca23627817909d6419be0175 Mon Sep 17 00:00:00 2001 From: Feanil Patel Date: Wed, 10 Mar 2021 07:44:17 -0500 Subject: [PATCH 43/63] Make the doc on overriding the default permission classes more clear. (#7661) --- docs/api-guide/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index f694d6be50..08031bceb0 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -116,7 +116,7 @@ Or, if you're using the `@api_view` decorator with function based views. } return Response(content) -__Note:__ when you set new permission classes through class attribute or decorators you're telling the view to ignore the default list set over the __settings.py__ file. +__Note:__ when you set new permission classes via the class attribute or decorators you're telling the view to ignore the default list set in the __settings.py__ file. Provided they inherit from `rest_framework.permissions.BasePermission`, permissions can be composed using standard Python bitwise operators. For example, `IsAuthenticatedOrReadOnly` could be written: From ffe11d41bd357ab3acafebe9490bed89afc2039d Mon Sep 17 00:00:00 2001 From: Max Morlocke Date: Wed, 10 Mar 2021 07:45:47 -0500 Subject: [PATCH 44/63] upgrade pytest+pytest-django to eliminate dependencies on six (#7672) * upgrade to latest version of pytest+pytest-django to eliminate dependency on six * rollback pytest to 6.1 as py35 is dropped in 6.2 Co-authored-by: Tom Christie --- requirements/requirements-testing.txt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index 99463560e2..c5198dec54 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -1,5 +1,4 @@ # Pytest for running the tests. -pytest>=5.4.1,<5.5 -pytest-django>=3.9.0,<3.10 -pytest-cov>=2.7.1 -six>=1.14.0 +pytest>=6.1.1,<6.2 +pytest-django>=4.1.0,<4.2 +pytest-cov>=2.10.1 From c05cbe2da213ae6fef5ea66dbafb050b76923117 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Mar 2021 12:50:59 +0000 Subject: [PATCH 45/63] Update pagination.py Include `last_page_strings` logic *inside* the `get_page_number method. --- rest_framework/pagination.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 0f0aa9ccf9..87ff7d3d69 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -199,8 +199,6 @@ def paginate_queryset(self, queryset, request, view=None): paginator = self.django_paginator_class(queryset, page_size) page_number = self.get_page_number(request) - if page_number in self.last_page_strings: - page_number = paginator.num_pages try: self.page = paginator.page(page_number) @@ -218,7 +216,10 @@ def paginate_queryset(self, queryset, request, view=None): return list(self.page) def get_page_number(self, request): - return request.query_params.get(self.page_query_param, 1) + page_number = request.query_params.get(self.page_query_param, 1) + if page_number in self.last_page_strings: + page_number = paginator.num_pages + return page_number def get_paginated_response(self, data): return Response(OrderedDict([ From 39a98c80a6cb0b21fb6adb5ee97bc64b8b404433 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Mar 2021 09:21:22 +0000 Subject: [PATCH 46/63] Fix 'get_page_number' implementation --- rest_framework/pagination.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 87ff7d3d69..91da73de64 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -198,7 +198,7 @@ def paginate_queryset(self, queryset, request, view=None): return None paginator = self.django_paginator_class(queryset, page_size) - page_number = self.get_page_number(request) + page_number = self.get_page_number(request, paginator) try: self.page = paginator.page(page_number) @@ -215,7 +215,7 @@ def paginate_queryset(self, queryset, request, view=None): self.request = request return list(self.page) - def get_page_number(self, request): + def get_page_number(self, request, paginator): page_number = request.query_params.get(self.page_query_param, 1) if page_number in self.last_page_strings: page_number = paginator.num_pages From f0706190614541fd47aeb7576c2030b58907d68b Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 11 Mar 2021 11:26:11 +0100 Subject: [PATCH 47/63] Updated tox/travis to point to Django `main` branch. (#7827) --- .travis.yml | 6 +++--- tox.ini | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f9f22336fc..57a91e594a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,11 +20,11 @@ matrix: - { python: "3.8", env: DJANGO=3.0 } - { python: "3.8", env: DJANGO=3.1 } - { python: "3.8", env: DJANGO=3.2 } - - { python: "3.8", env: DJANGO=master } + - { python: "3.8", env: DJANGO=main } - { python: "3.9", env: DJANGO=3.1 } - { python: "3.9", env: DJANGO=3.2 } - - { python: "3.9", env: DJANGO=master } + - { python: "3.9", env: DJANGO=main } - { python: "3.8", env: TOXENV=base } - { python: "3.8", env: TOXENV=lint } @@ -39,7 +39,7 @@ matrix: - tox # test sdist allow_failures: - - env: DJANGO=master + - env: DJANGO=main - env: DJANGO=3.2 install: diff --git a/tox.ini b/tox.ini index 544bab163c..df16cf947f 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ envlist = {py36,py37,py38}-django30, {py36,py37,py38,py39}-django31, {py36,py37,py38,py39}-django32, - {py38,py39}-djangomaster, + {py38,py39}-djangomain, base,dist,lint,docs, [travis:env] @@ -13,7 +13,7 @@ DJANGO = 3.0: django30 3.1: django31 3.2: django32 - master: djangomaster + main: djangomain [testenv] commands = python -W error::DeprecationWarning -W error::PendingDeprecationWarning runtests.py --fast --coverage {posargs} @@ -26,7 +26,7 @@ deps = django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 django32: Django>=3.2a1,<4.0 - djangomaster: https://github.com/django/django/archive/master.tar.gz + djangomain: https://github.com/django/django/archive/main.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 883f6fe814acdbc35032143275ae78f6ff832d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Abac=C4=B1?= Date: Thu, 11 Mar 2021 14:39:06 +0300 Subject: [PATCH 48/63] Rename django-extra-fields to drf-extra-fields (#7833) --- docs/community/third-party-packages.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index 838122cbe6..32fc8a0f5e 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -220,7 +220,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque ### Serializer fields * [drf-compound-fields][drf-compound-fields] - Provides "compound" serializer fields, such as lists of simple values. -* [django-extra-fields][django-extra-fields] - Provides extra serializer fields. +* [drf-extra-fields][drf-extra-fields] - Provides extra serializer fields. * [django-versatileimagefield][django-versatileimagefield] - Provides a drop-in replacement for Django's stock `ImageField` that makes it easy to serve images in multiple sizes/renditions from a single field. For DRF-specific implementation docs, [click here][django-versatileimagefield-drf-docs]. ### Views @@ -311,7 +311,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [djangorestframework-gis]: https://github.com/djangonauts/django-rest-framework-gis [djangorestframework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore [drf-compound-fields]: https://github.com/estebistec/drf-compound-fields -[django-extra-fields]: https://github.com/Hipo/drf-extra-fields +[drf-extra-fields]: https://github.com/Hipo/drf-extra-fields [django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [wq.db.rest]: https://wq.io/docs/about-rest From ff625ecff5026e2f1e25014a0399afabb73753d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henryk=20Pl=C3=B6tz?= Date: Mon, 15 Mar 2021 11:28:45 +0100 Subject: [PATCH 49/63] Document object level permissions gotchas (#7446) * Document the limitation that object level permissions do not apply to object creation. See for example #6409. * Add overview of three different ways to restrict access --- docs/api-guide/permissions.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 08031bceb0..6912c375c2 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -70,6 +70,8 @@ For performance reasons the generic views will not automatically apply object le Often when you're using object level permissions you'll also want to [filter the queryset][filtering] appropriately, to ensure that users only have visibility onto instances that they are permitted to view. +Because the `get_object()` method is not called, object level permissions from the `has_object_permission()` method **are not applied** when creating objects. In order to restrict object creation you need to implement the permission check either in your Serializer class or override the `perform_create()` method of your ViewSet class. + ## Setting the permission policy The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example. @@ -272,6 +274,30 @@ Note that the generic views will check the appropriate object level permissions, Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the [filtering documentation][filtering] for more details. +# Overview of access restriction methods + +REST framework offers three different methods to customize access restrictions on a case-by-case basis. These apply in different scenarios and have different effects and limitations. + + * `queryset`/`get_queryset()`: Limits the general visibility of existing objects from the database. The queryset limits which objects will be listed and which objects can be modified or deleted. The `get_queryset()` method can apply different querysets based on the current action. + * `permission_classes`/`get_permissions()`: General permission checks based on the current action, request and targeted object. Object level permissions can only be applied to retrieve, modify and deletion actions. Permission checks for list and create will be applied to the entire object type. (In case of list: subject to restrictions in the queryset.) + * `serializer_class`/`get_serializer()`: Instance level restrictions that apply to all objects on input and output. The serializer may have access to the request context. The `get_serializer()` method can apply different serializers based on the current action. + +The following table lists the access restriction methods and the level of control they offer over which actions. + +| | `queryset` | `permission_classes` | `serializer_class` | +|------------------------------------|------------|----------------------|--------------------| +| Action: list | global | no | object-level* | +| Action: create | no | global | object-level | +| Action: retrieve | global | object-level | object-level | +| Action: update | global | object-level | object-level | +| Action: partial_update | global | object-level | object-level | +| Action: destroy | global | object-level | no | +| Can reference action in decision | no** | yes | no** | +| Can reference request in decision | no** | yes | yes | + + \* A Serializer class should not raise PermissionDenied in a list action, or the entire list would not be returned.
+ \** The `get_*()` methods have access to the current view and can return different Serializer or QuerySet instances based on the request or action. + --- # Third party packages From b256c46cb1470f818328941e0005134d38087220 Mon Sep 17 00:00:00 2001 From: Alex Hedlund Date: Mon, 15 Mar 2021 12:44:03 +0200 Subject: [PATCH 50/63] Render JSON fields with proper indentation in browsable API forms. (#6243) * Fix JSONBoundField usage on nested serializers (#6211) * Unify JSONBoundField as_form_field output between py2 and py3 When using json.dumps with indenting, in python2 the default formatting prints whitespace after commas (,) and python3 does not. This can be unified with the separators keyword argument. --- rest_framework/fields.py | 3 +++ rest_framework/utils/serializer_helpers.py | 9 ++++++++- tests/test_bound_fields.py | 23 +++++++++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index d91299484e..e4be54751d 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1764,6 +1764,9 @@ class JSONField(Field): 'invalid': _('Value must be valid JSON.') } + # Workaround for isinstance calls when importing the field isn't possible + _is_jsonfield = True + def __init__(self, *args, **kwargs): self.binary = kwargs.pop('binary', False) self.encoder = kwargs.pop('encoder', None) diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index cd0373adcb..4cd2ada314 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -87,7 +87,12 @@ def as_form_field(self): # value will be a JSONString, rather than a JSON primitive. if not getattr(value, 'is_json_string', False): try: - value = json.dumps(self.value, sort_keys=True, indent=4) + value = json.dumps( + self.value, + sort_keys=True, + indent=4, + separators=(',', ': '), + ) except (TypeError, ValueError): pass return self.__class__(self._field, value, self.errors, self._prefix) @@ -115,6 +120,8 @@ def __getitem__(self, key): error = self.errors.get(key) if isinstance(self.errors, dict) else None if hasattr(field, 'fields'): return NestedBoundField(field, value, error, prefix=self.name + '.') + elif getattr(field, '_is_jsonfield', False): + return JSONBoundField(field, value, error, prefix=self.name + '.') return BoundField(field, value, error, prefix=self.name + '.') def as_form_field(self): diff --git a/tests/test_bound_fields.py b/tests/test_bound_fields.py index dec8793c33..eee7d9b852 100644 --- a/tests/test_bound_fields.py +++ b/tests/test_bound_fields.py @@ -91,6 +91,10 @@ class ExampleSerializer(serializers.Serializer): assert rendered_packed == expected_packed +class CustomJSONField(serializers.JSONField): + pass + + class TestNestedBoundField: def test_nested_empty_bound_field(self): class Nested(serializers.Serializer): @@ -117,14 +121,31 @@ def test_as_form_fields(self): class Nested(serializers.Serializer): bool_field = serializers.BooleanField() null_field = serializers.IntegerField(allow_null=True) + json_field = serializers.JSONField() + custom_json_field = CustomJSONField() class ExampleSerializer(serializers.Serializer): nested = Nested() - serializer = ExampleSerializer(data={'nested': {'bool_field': False, 'null_field': None}}) + serializer = ExampleSerializer( + data={'nested': { + 'bool_field': False, 'null_field': None, + 'json_field': {'bool_item': True, 'number': 1, 'text_item': 'text'}, + 'custom_json_field': {'bool_item': True, 'number': 1, 'text_item': 'text'}, + }}) assert serializer.is_valid() assert serializer['nested']['bool_field'].as_form_field().value == '' assert serializer['nested']['null_field'].as_form_field().value == '' + assert serializer['nested']['json_field'].as_form_field().value == '''{ + "bool_item": true, + "number": 1, + "text_item": "text" +}''' + assert serializer['nested']['custom_json_field'].as_form_field().value == '''{ + "bool_item": true, + "number": 1, + "text_item": "text" +}''' def test_rendering_nested_fields_with_none_value(self): from rest_framework.renderers import HTMLFormRenderer From ce1568322af61a6b41ccc5dc2c631c6927ed5e71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Abac=C4=B1?= Date: Tue, 16 Mar 2021 15:53:39 +0300 Subject: [PATCH 51/63] Ordering filter bug with model property serializer field (#7609) * Add failing tests for ordering filter with model property * Fix get_default_valid_fields of OrderingFilter * Filter model properties in get_default_valid_fields of OrderingFilter --- rest_framework/filters.py | 12 ++++++++- tests/test_filters.py | 51 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 3665775195..1ffd9edc02 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -226,10 +226,20 @@ def get_default_valid_fields(self, queryset, view, context={}): ) raise ImproperlyConfigured(msg % self.__class__.__name__) + model_class = queryset.model + model_property_names = [ + # 'pk' is a property added in Django's Model class, however it is valid for ordering. + attr for attr in dir(model_class) if isinstance(getattr(model_class, attr), property) and attr != 'pk' + ] + return [ (field.source.replace('.', '__') or field_name, field.label) for field_name, field in serializer_class(context=context).fields.items() - if not getattr(field, 'write_only', False) and not field.source == '*' + if ( + not getattr(field, 'write_only', False) and + not field.source == '*' and + field.source not in model_property_names + ) ] def get_valid_fields(self, queryset, view, context={}): diff --git a/tests/test_filters.py b/tests/test_filters.py index 567e5f83fc..37ae4c7cf3 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -424,6 +424,10 @@ class OrderingFilterModel(models.Model): title = models.CharField(max_length=20, verbose_name='verbose title') text = models.CharField(max_length=100) + @property + def description(self): + return self.title + ": " + self.text + class OrderingFilterRelatedModel(models.Model): related_object = models.ForeignKey(OrderingFilterModel, related_name="relateds", on_delete=models.CASCADE) @@ -436,6 +440,17 @@ class Meta: fields = '__all__' +class OrderingFilterSerializerWithModelProperty(serializers.ModelSerializer): + class Meta: + model = OrderingFilterModel + fields = ( + "id", + "title", + "text", + "description" + ) + + class OrderingDottedRelatedSerializer(serializers.ModelSerializer): related_text = serializers.CharField(source='related_object.text') related_title = serializers.CharField(source='related_object.title') @@ -551,6 +566,42 @@ class OrderingListView(generics.ListAPIView): {'id': 1, 'title': 'zyx', 'text': 'abc'}, ] + def test_ordering_without_ordering_fields(self): + class OrderingListView(generics.ListAPIView): + queryset = OrderingFilterModel.objects.all() + serializer_class = OrderingFilterSerializerWithModelProperty + filter_backends = (filters.OrderingFilter,) + ordering = ('title',) + + view = OrderingListView.as_view() + + # Model field ordering works fine. + request = factory.get('/', {'ordering': 'text'}) + response = view(request) + assert response.data == [ + {'id': 1, 'title': 'zyx', 'text': 'abc', 'description': 'zyx: abc'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd', 'description': 'yxw: bcd'}, + {'id': 3, 'title': 'xwv', 'text': 'cde', 'description': 'xwv: cde'}, + ] + + # `incorrectfield` ordering works fine. + request = factory.get('/', {'ordering': 'foobar'}) + response = view(request) + assert response.data == [ + {'id': 3, 'title': 'xwv', 'text': 'cde', 'description': 'xwv: cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd', 'description': 'yxw: bcd'}, + {'id': 1, 'title': 'zyx', 'text': 'abc', 'description': 'zyx: abc'}, + ] + + # `description` is a Model property, which should be ignored. + request = factory.get('/', {'ordering': 'description'}) + response = view(request) + assert response.data == [ + {'id': 3, 'title': 'xwv', 'text': 'cde', 'description': 'xwv: cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd', 'description': 'yxw: bcd'}, + {'id': 1, 'title': 'zyx', 'text': 'abc', 'description': 'zyx: abc'}, + ] + def test_default_ordering(self): class OrderingListView(generics.ListAPIView): queryset = OrderingFilterModel.objects.all() From b0ca248d88240c58ff04554bf5df07a6dd8d9e8f Mon Sep 17 00:00:00 2001 From: Jeff Baumes Date: Tue, 16 Mar 2021 08:57:04 -0400 Subject: [PATCH 52/63] Correct the use of "to" (#7696) --- docs/tutorial/5-relationships-and-hyperlinked-apis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 4cd4e9bbd5..b0f3380859 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -143,7 +143,7 @@ We can change the default list style to use pagination, by modifying our `tutori Note that settings in REST framework are all namespaced into a single dictionary setting, named `REST_FRAMEWORK`, which helps keep them well separated from your other project settings. -We could also customize the pagination style if we needed too, but in this case we'll just stick with the default. +We could also customize the pagination style if we needed to, but in this case we'll just stick with the default. ## Browsing the API From 9c9ffb18f44062fd05f0b4e06b756c0a35230561 Mon Sep 17 00:00:00 2001 From: Jesse London Date: Tue, 16 Mar 2021 08:25:21 -0500 Subject: [PATCH 53/63] made Browsable API base template cachable: omit CSRF token when unnecessary (#7717) HTML responses generated by the Browsable API otherwise generate inconsistent ETAGs -- due to the presence of CSRF tokens in the response -- even when the API is read-only, (and as such when the response contains no resource-modifying forms, i.e. neither POST nor PUT forms, which might require the CSRF token). While the template was appropriately including CSRF tokens only within POST and PUT forms, its AJAX overlay included the CSRF token in *every* response, regardless of whether it would be needed. This change brings the logic of the `script` block into line with that of the rest of the template -- and such that read-only APIs (and really the Browsable API pages of *any* read-only resources) will not needlessly include the CSRF token, and will now be safely cachable -- by both back-end systems and by the user agent. --- .../templates/rest_framework/base.html | 2 +- tests/test_templates.py | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index a88e1591c6..4d057b6322 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -290,7 +290,7 @@

{{ name }}

diff --git a/tests/test_templates.py b/tests/test_templates.py index 0dba78ea22..195296e161 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -3,15 +3,23 @@ from django.shortcuts import render -def test_base_template_with_context(): - context = {'request': True, 'csrf_token': 'TOKEN'} - result = render({}, 'rest_framework/base.html', context=context) - assert re.search(r'\bcsrfToken: "TOKEN"', result.content.decode()) - - def test_base_template_with_no_context(): # base.html should be renderable with no context, # so it can be easily extended. result = render({}, 'rest_framework/base.html') # note that this response will not include a valid CSRF token assert re.search(r'\bcsrfToken: ""', result.content.decode()) + + +def test_base_template_with_simple_context(): + context = {'request': True, 'csrf_token': 'TOKEN'} + result = render({}, 'rest_framework/base.html', context=context) + # note that response will STILL not include a CSRF token + assert re.search(r'\bcsrfToken: ""', result.content.decode()) + + +def test_base_template_with_editing_context(): + context = {'request': True, 'post_form': object(), 'csrf_token': 'TOKEN'} + result = render({}, 'rest_framework/base.html', context=context) + # response includes a CSRF token in support of the POST form + assert re.search(r'\bcsrfToken: "TOKEN"', result.content.decode()) From a40bce50cda95652afd822d69d91a67b78bc05b0 Mon Sep 17 00:00:00 2001 From: Yuekui Date: Tue, 16 Mar 2021 06:29:13 -0700 Subject: [PATCH 54/63] No need to explictitly set None as default (#7373) --- docs/api-guide/filtering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index d305ede6ba..478e3bcf95 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -75,7 +75,7 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/ by filtering against a `username` query parameter in the URL. """ queryset = Purchase.objects.all() - username = self.request.query_params.get('username', None) + username = self.request.query_params.get('username') if username is not None: queryset = queryset.filter(purchaser__username=username) return queryset From 3e274146fcd6baffa82ac6e146e2c3ca35d447cd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Mar 2021 13:24:38 +0000 Subject: [PATCH 55/63] Fix WSGI signature for DjangoTestAdapter (#7846) Closes https://github.com/encode/django-rest-framework/issues/7132 --- rest_framework/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index f2581cacca..8ab0f2de19 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -79,7 +79,7 @@ def send(self, request, *args, **kwargs): """ raw_kwargs = {} - def start_response(wsgi_status, wsgi_headers): + def start_response(wsgi_status, wsgi_headers, exc_info=None): status, _, reason = wsgi_status.partition(' ') raw_kwargs['status'] = int(status) raw_kwargs['reason'] = reason From 7b53960c3bef7ffc8deb727639afd2ea118879b0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Mar 2021 13:24:55 +0000 Subject: [PATCH 56/63] Revert "made Browsable API base template cachable: omit CSRF token when unnecessary (#7717)" (#7847) This reverts commit 9c9ffb18f44062fd05f0b4e06b756c0a35230561. --- .../templates/rest_framework/base.html | 2 +- tests/test_templates.py | 20 ++++++------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 4d057b6322..a88e1591c6 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -290,7 +290,7 @@

{{ name }}

diff --git a/tests/test_templates.py b/tests/test_templates.py index 195296e161..0dba78ea22 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -3,23 +3,15 @@ from django.shortcuts import render +def test_base_template_with_context(): + context = {'request': True, 'csrf_token': 'TOKEN'} + result = render({}, 'rest_framework/base.html', context=context) + assert re.search(r'\bcsrfToken: "TOKEN"', result.content.decode()) + + def test_base_template_with_no_context(): # base.html should be renderable with no context, # so it can be easily extended. result = render({}, 'rest_framework/base.html') # note that this response will not include a valid CSRF token assert re.search(r'\bcsrfToken: ""', result.content.decode()) - - -def test_base_template_with_simple_context(): - context = {'request': True, 'csrf_token': 'TOKEN'} - result = render({}, 'rest_framework/base.html', context=context) - # note that response will STILL not include a CSRF token - assert re.search(r'\bcsrfToken: ""', result.content.decode()) - - -def test_base_template_with_editing_context(): - context = {'request': True, 'post_form': object(), 'csrf_token': 'TOKEN'} - result = render({}, 'rest_framework/base.html', context=context) - # response includes a CSRF token in support of the POST form - assert re.search(r'\bcsrfToken: "TOKEN"', result.content.decode()) From 67ebdd32cdab0ec9078c281d24971ccd9d119173 Mon Sep 17 00:00:00 2001 From: Aristotelis Mikropoulos Date: Wed, 17 Mar 2021 15:28:38 +0200 Subject: [PATCH 57/63] Reject PrimaryKeyRelatedField bool lookup values (#7597) * Reject PrimaryKeyRelatedField bool lookup values * Test PrimaryKeyRelatedField bool lookup rejection * Fix indentation in test --- rest_framework/relations.py | 2 ++ tests/test_relations.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index eaf27e1d96..cbdf233698 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -259,6 +259,8 @@ def to_internal_value(self, data): data = self.pk_field.to_internal_value(data) queryset = self.get_queryset() try: + if isinstance(data, bool): + raise TypeError return queryset.get(pk=data) except ObjectDoesNotExist: self.fail('does_not_exist', pk_value=data) diff --git a/tests/test_relations.py b/tests/test_relations.py index 92aeecf6c4..bb719a65a9 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -107,6 +107,12 @@ def test_pk_related_lookup_invalid_type(self): msg = excinfo.value.detail[0] assert msg == 'Incorrect type. Expected pk value, received BadType.' + def test_pk_related_lookup_bool(self): + with pytest.raises(serializers.ValidationError) as excinfo: + self.field.to_internal_value(True) + msg = excinfo.value.detail[0] + assert msg == 'Incorrect type. Expected pk value, received bool.' + def test_pk_representation(self): representation = self.field.to_representation(self.instance) assert representation == self.instance.pk From b25ac6c5e36403f62b13163a0190eaa48b586c47 Mon Sep 17 00:00:00 2001 From: Anton Zaslavskiy Date: Fri, 19 Mar 2021 14:46:09 +0300 Subject: [PATCH 58/63] Don't hit db to access user_id in TokenProxy (#7852) --- rest_framework/authtoken/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 540049295d..5a143d936c 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -46,7 +46,7 @@ class TokenProxy(Token): """ @property def pk(self): - return self.user.pk + return self.user_id class Meta: proxy = 'rest_framework.authtoken' in settings.INSTALLED_APPS From 0cddf097ca50344355db79b048b12c1805bbe180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hasan=20Talha=20Yaz=C4=B1c=C4=B1?= Date: Sun, 21 Mar 2021 10:53:09 +0100 Subject: [PATCH 59/63] Fix typo in docs (#7853) --- 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 d5815127b6..d4ab5a7317 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -185,7 +185,7 @@ The decorator allows you to override any viewset-level configuration such as `pe def set_password(self, request, pk=None): ... -The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$`. Use the `url_path` and `url_name` parameters to change the URL segement and the reverse URL name of the action. +The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$`. Use the `url_path` and `url_name` parameters to change the URL segment and the reverse URL name of the action. To view all extra actions, call the `.get_extra_actions()` method. From 7e3dd9cd1b7d9cd5d036b4b733937d93304b2dd8 Mon Sep 17 00:00:00 2001 From: Mohammad Ashraful Islam Date: Mon, 22 Mar 2021 18:07:48 +0600 Subject: [PATCH 60/63] Added fast-drf as a thirdparty package for making API development faster. (#7857) --- 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 32fc8a0f5e..046966594c 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -279,6 +279,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [djangorestframework-features][djangorestframework-features] - Advanced schema generation and more based on named features. * [django-elasticsearch-dsl-drf][django-elasticsearch-dsl-drf] - Integrate Elasticsearch DSL with Django REST framework. Package provides views, serializers, filter backends, pagination and other handy add-ons. * [django-api-client][django-api-client] - DRF client that groups the Endpoint response, for use in CBVs and FBV as if you were working with Django's Native Models.. +* [fast-drf] - A model based library for making API development faster and easier. [cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html [cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework @@ -368,3 +369,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-rest-authemail]: https://github.com/celiao/django-rest-authemail [graphwrap]: https://github.com/PaulGilmartin/graph_wrap [rest-framework-actions]: https://github.com/AlexisMunera98/rest-framework-actions +[fast-drf]: https://github.com/iashraful/fast-drf From 71e6c30034a1dd35a39ca74f86c371713e762c79 Mon Sep 17 00:00:00 2001 From: Joe Michelini <66066937+afolksetapart@users.noreply.github.com> Date: Mon, 22 Mar 2021 08:08:19 -0400 Subject: [PATCH 61/63] update SerializerMethodField example in docs (#7858) * update SerializerMethodField example * fix formatting --- docs/api-guide/fields.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 0492af9aa9..04f9939425 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -583,6 +583,7 @@ The serializer method referred to by the `method_name` argument should accept a class Meta: model = User + fields = '__all__' def get_days_since_joined(self, obj): return (now() - obj.date_joined).days From ebcb8d53108f1ebe56b9a7aa78bbe09b1079953c Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 25 Mar 2021 18:47:44 +0800 Subject: [PATCH 62/63] pick deque instead of list (#7849) Co-authored-by: Jack Zhang --- rest_framework/throttling.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 0ba2ba66b1..1374d44925 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -2,6 +2,7 @@ Provides various throttling policies. """ import time +from collections import deque from django.core.cache import cache as default_cache from django.core.exceptions import ImproperlyConfigured @@ -120,7 +121,7 @@ def allow_request(self, request, view): if self.key is None: return True - self.history = self.cache.get(self.key, []) + self.history = self.cache.get(self.key, deque()) self.now = self.timer() # Drop any requests from the history which have now passed the From 83ad265e138106c26745a49dce0576573f0d202c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Mar 2021 12:23:23 +0000 Subject: [PATCH 63/63] Version 3.12.3 (#7866) --- docs/community/release-notes.md | 16 ++++++++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 49fb655b01..72e6b466b5 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -38,6 +38,22 @@ You can determine your currently installed version using `pip show`: ### 3.12.2 +Date: 25th March 2021 + +* Properly handle ATOMIC_REQUESTS when multiple database configurations are used. [#7739] +* Bypass `COUNT` query when `LimitOffsetPagination` is configured but pagination params are not included on the request. [#6098] +* Respect `allow_null=True` on `DecimalField`. [#7718] +* Allow title cased `"Yes"`/`"No"` values with `BooleanField`. [#7739] +* Add `PageNumberPagination.get_page_number()` method for overriding behavior. [#7652] +* Fixed rendering of timedelta values in OpenAPI schemas, when present as default, min, or max fields. [#7641] +* Render JSONFields with indentation in browsable API forms. [#6243] +* Remove unnecessary database query in admin Token views. [#7852] +* Raise validation errors when bools are passed to `PrimaryKeyRelatedField` fields, instead of casting to ints. [#7597] +* Don't include model properties as automatically generated ordering fields with `OrderingFilter`. [#7609] +* Use `deque` instead of `list` for tracking throttling `.history`. [#7849] + +### 3.12.2 + Date: 13th October 2020 * Fix issue if `rest_framework.authtoken.models` is imported, but `rest_framework.authtoken` is not in INSTALLED_APPS. [#7571] diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 7ff188a5ad..eb5d605b9b 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -10,7 +10,7 @@ import django __title__ = 'Django REST framework' -__version__ = '3.12.2' +__version__ = '3.12.3' __author__ = 'Tom Christie' __license__ = 'BSD 3-Clause' __copyright__ = 'Copyright 2011-2019 Encode OSS Ltd'