From f54a220d8f3c8649b84188c7e2a6dc2ef3be6f4c Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 31 Jan 2019 11:36:40 +0100 Subject: [PATCH 58/97] Corrected coreapi CLI code example generation. (#6428) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove “> “ when rendering template. Closes #6333. --- .../templates/rest_framework/docs/langs/shell.html | 2 +- tests/test_renderers.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/rest_framework/templates/rest_framework/docs/langs/shell.html b/rest_framework/templates/rest_framework/docs/langs/shell.html index 24137e4ae..e5f2a0322 100644 --- a/rest_framework/templates/rest_framework/docs/langs/shell.html +++ b/rest_framework/templates/rest_framework/docs/langs/shell.html @@ -3,4 +3,4 @@ $ coreapi get {{ document.url }}{% if schema_format %} --format {{ schema_format }}{% endif %} # Interact with the API endpoint -$ coreapi action {% if section_key %}{{ section_key }} {% endif %}{{ link_key }}{% for field in link.fields %} -p {{ field.name }}=...{% endfor %}{% endcode %} +$ coreapi action {% if section_key %}{{ section_key }} {% endif %}{{ link_key|cut:"> " }}{% for field in link.fields %} -p {{ field.name }}=...{% endfor %}{% endcode %} diff --git a/tests/test_renderers.py b/tests/test_renderers.py index a68ece734..8518a3f7c 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -9,6 +9,7 @@ from django.conf.urls import include, url from django.core.cache import cache from django.db import models from django.http.request import HttpRequest +from django.template import loader from django.test import TestCase, override_settings from django.utils import six from django.utils.safestring import SafeText @@ -827,6 +828,16 @@ class TestDocumentationRenderer(TestCase): html = renderer.render(document, accepted_media_type="text/html", renderer_context={"request": request}) assert '

Data Endpoint API

' in html + def test_shell_code_example_rendering(self): + template = loader.get_template('rest_framework/docs/langs/shell.html') + context = { + 'document': coreapi.Document(url='https://api.example.org/'), + 'link_key': 'testcases > list', + 'link': coreapi.Link(url='/data/', action='get', fields=[]), + } + html = template.render(context) + assert 'testcases list' in html + @pytest.mark.skipif(not coreapi, reason='coreapi is not installed') class TestSchemaJSRenderer(TestCase): From bd9a799e166b1d44c9db444a95765ad0dce2aa8f Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 31 Jan 2019 15:28:01 +0100 Subject: [PATCH 59/97] Fixed SchemaView to reset renderer on exception. (#6429) Fixes #6258. --- rest_framework/schemas/views.py | 8 ++++++++ tests/test_schemas.py | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/rest_framework/schemas/views.py b/rest_framework/schemas/views.py index 845b68ea6..f5e327a94 100644 --- a/rest_framework/schemas/views.py +++ b/rest_framework/schemas/views.py @@ -31,3 +31,11 @@ class SchemaView(APIView): if schema is None: raise exceptions.PermissionDenied() return Response(schema) + + def handle_exception(self, exc): + # Schema renderers do not render exceptions, so re-perform content + # negotiation with default renderers. + self.renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + neg = self.perform_content_negotiation(self.request, force=True) + self.request.accepted_renderer, self.request.accepted_media_type = neg + return super(SchemaView, self).handle_exception(exc) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 8e097f9f4..d3bd43073 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -1304,3 +1304,13 @@ class TestAutoSchemaAllowsFilters(object): def test_FOO(self): assert not self._test('FOO') + + +@pytest.mark.skipif(not coreapi, reason='coreapi is not installed') +def test_schema_handles_exception(): + schema_view = get_schema_view(permission_classes=[DenyAllUsingPermissionDenied]) + request = factory.get('/') + response = schema_view(request) + response.render() + assert response.status_code == 403 + assert "You do not have permission to perform this action." in str(response.content) From 190f6201cbbac18803479020dae922c5ae3e90f2 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 31 Jan 2019 15:59:19 +0100 Subject: [PATCH 60/97] Update Django Guardian dependency. (#6430) * Update Django Guardian dependency. * Skip testing Guardian on PY2. See https://github.com/django-guardian/django-guardian/issues/602 --- requirements/requirements-optionals.txt | 2 +- rest_framework/compat.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index cd0f1f62b..c800a5891 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,7 +1,7 @@ # Optional packages which may be used with REST framework. psycopg2-binary==2.7.5 markdown==2.6.11 -django-guardian==1.4.9 +django-guardian==1.5.0 django-filter==1.1.0 coreapi==2.3.1 coreschema==0.0.4 diff --git a/rest_framework/compat.py b/rest_framework/compat.py index fffc17938..5a4bcdf66 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -161,6 +161,10 @@ def is_guardian_installed(): """ django-guardian is optional and only imported if in INSTALLED_APPS. """ + if six.PY2: + # Guardian 1.5.0, for Django 2.2 is NOT compatible with Python 2.7. + # Remove when dropping PY2. + return False return 'guardian' in settings.INSTALLED_APPS From 2b62941bb4d48e27960690bdea1a343177dd5d0e Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Thu, 31 Jan 2019 21:50:36 +0600 Subject: [PATCH 61/97] Added testing against Django 2.2a1. (#6422) * Added testing against Django 2.2a1. * Allow failures for Django 2.2 --- .travis.yml | 4 ++++ tox.ini | 3 +++ 2 files changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index c0373077e..c9febbdf9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,15 +12,18 @@ matrix: - { python: "3.5", env: DJANGO=1.11 } - { python: "3.5", env: DJANGO=2.0 } - { python: "3.5", env: DJANGO=2.1 } + - { python: "3.5", env: DJANGO=2.2 } - { python: "3.5", env: DJANGO=master } - { python: "3.6", env: DJANGO=1.11 } - { python: "3.6", env: DJANGO=2.0 } - { python: "3.6", env: DJANGO=2.1 } + - { python: "3.6", env: DJANGO=2.2 } - { python: "3.6", env: DJANGO=master } - { python: "3.7", env: DJANGO=2.0 } - { python: "3.7", env: DJANGO=2.1 } + - { python: "3.7", env: DJANGO=2.2 } - { python: "3.7", env: DJANGO=master } - { python: "3.7", env: TOXENV=base } @@ -37,6 +40,7 @@ matrix: allow_failures: - env: DJANGO=master + - env: DJANGO=2.2 install: - pip install tox tox-venv tox-travis diff --git a/tox.ini b/tox.ini index 968ec1ef1..65b941628 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = {py27,py34,py35,py36}-django111, {py34,py35,py36,py37}-django20, {py35,py36,py37}-django21 + {py35,py36,py37}-django22 {py35,py36,py37}-djangomaster, base,dist,lint,docs, @@ -11,6 +12,7 @@ DJANGO = 1.11: django111 2.0: django20 2.1: django21 + 2.2: django22 master: djangomaster [testenv] @@ -23,6 +25,7 @@ deps = django111: Django>=1.11,<2.0 django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 + django22: Django>=2.2a1,<3.0 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 63e352586b679d554e1e9ef708e153d858e7e3bf Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 31 Jan 2019 17:16:43 +0100 Subject: [PATCH 62/97] Drop testing Python 3.5 against Django master. (#6431) Not supported in Django 3.0. --- .travis.yml | 1 - tox.ini | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c9febbdf9..796ef0502 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,6 @@ matrix: - { python: "3.5", env: DJANGO=2.0 } - { python: "3.5", env: DJANGO=2.1 } - { python: "3.5", env: DJANGO=2.2 } - - { python: "3.5", env: DJANGO=master } - { python: "3.6", env: DJANGO=1.11 } - { python: "3.6", env: DJANGO=2.0 } diff --git a/tox.ini b/tox.ini index 65b941628..cf6799a0a 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ envlist = {py34,py35,py36,py37}-django20, {py35,py36,py37}-django21 {py35,py36,py37}-django22 - {py35,py36,py37}-djangomaster, + {py36,py37}-djangomaster, base,dist,lint,docs, [travis:env] From 7310411533873717305f08fcc5d45426cb0f01d9 Mon Sep 17 00:00:00 2001 From: Daniel Roseman Date: Fri, 1 Feb 2019 18:50:27 +0000 Subject: [PATCH 63/97] Updated example models to use `__str__` in relations docs. (#6433) --- docs/api-guide/relations.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 8683347cb..8665e80f6 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -46,12 +46,12 @@ In order to explain the various types of relational fields, we'll use a couple o unique_together = ('album', 'order') ordering = ['order'] - def __unicode__(self): + def __str__(self): return '%d: %s' % (self.order, self.title) ## StringRelatedField -`StringRelatedField` may be used to represent the target of the relationship using its `__unicode__` method. +`StringRelatedField` may be used to represent the target of the relationship using its `__str__` method. For example, the following serializer. @@ -510,7 +510,7 @@ For example, given the following model for a tag, which has a generic relationsh object_id = models.PositiveIntegerField() tagged_object = GenericForeignKey('content_type', 'object_id') - def __unicode__(self): + def __str__(self): return self.tag_name And the following two models, which may have associated tags: From 7c6e34c14f54f1bf0d46ff34dc9a823215987c95 Mon Sep 17 00:00:00 2001 From: jhtimmins Date: Sat, 2 Feb 2019 05:49:58 -0800 Subject: [PATCH 64/97] Fix typo: 'what' to 'that' (#6437) --- docs/api-guide/requests.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md index 35b976c66..28450f082 100644 --- a/docs/api-guide/requests.md +++ b/docs/api-guide/requests.md @@ -50,7 +50,7 @@ The request exposes some properties that allow you to determine the result of th ## .accepted_renderer -The renderer instance what was selected by the content negotiation stage. +The renderer instance that was selected by the content negotiation stage. ## .accepted_media_type From 3c5c61f33bd451496385aec237cb5502db8d6a6a Mon Sep 17 00:00:00 2001 From: carlfarrington <33500423+carlfarrington@users.noreply.github.com> Date: Wed, 6 Feb 2019 09:35:04 +0000 Subject: [PATCH 65/97] fix for a couple of missing words (#6444) --- docs/api-guide/permissions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 8a4cb63c6..e04b1199b 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -10,9 +10,9 @@ Together with [authentication] and [throttling], permissions determine whether a Permission checks are always run at the very start of the view, before any other code is allowed to proceed. Permission checks will typically use the authentication information in the `request.user` and `request.auth` properties to determine if the incoming request should be permitted. -Permissions are used to grant or deny access different classes of users to different parts of the API. +Permissions are used to grant or deny access for different classes of users to different parts of the API. -The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user. This corresponds the `IsAuthenticated` class in REST framework. +The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user. This corresponds to the `IsAuthenticated` class in REST framework. A slightly less strict style of permission would be to allow full access to authenticated users, but allow read-only access to unauthenticated users. This corresponds to the `IsAuthenticatedOrReadOnly` class in REST framework. From abf07e672e94f2ce7d42e690222f527ba918ad40 Mon Sep 17 00:00:00 2001 From: Tanner Prestegard Date: Wed, 6 Feb 2019 14:26:09 -0600 Subject: [PATCH 66/97] Fix throttling documentation for specifying alternate caches (#6446) --- docs/api-guide/throttling.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index de66396a8..dade47460 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -82,8 +82,10 @@ The throttle classes provided by REST framework use Django's cache backend. You If you need to use a cache other than `'default'`, you can do so by creating a custom throttle class and setting the `cache` attribute. For example: + from django.core.cache import caches + class CustomAnonRateThrottle(AnonRateThrottle): - cache = get_cache('alternate') + cache = caches['alternate'] You'll need to remember to also set your custom throttle class in the `'DEFAULT_THROTTLE_CLASSES'` settings key, or using the `throttle_classes` view attribute. From dc6b3bf42e53c6bfdb597a67de931913c8bd0255 Mon Sep 17 00:00:00 2001 From: briwa Date: Thu, 7 Feb 2019 16:10:11 +0800 Subject: [PATCH 67/97] Fix tutorial instruction to also add pyyaml (#6443) --- docs/tutorial/7-schemas-and-client-libraries.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/7-schemas-and-client-libraries.md b/docs/tutorial/7-schemas-and-client-libraries.md index 412759834..203d81ea5 100644 --- a/docs/tutorial/7-schemas-and-client-libraries.md +++ b/docs/tutorial/7-schemas-and-client-libraries.md @@ -29,9 +29,10 @@ automatically generated schemas. Since we're using viewsets and routers, we can simply use the automatic schema generation. You'll need to install the `coreapi` python package in order to include an -API schema. +API schema, and `pyyaml` to render the schema into the commonly used +YAML-based OpenAPI format. - $ pip install coreapi + $ pip install coreapi pyyaml We can now include a schema for our API, by including an autogenerated schema view in our URL configuration. From 9f66fc9a7ca51ad5535cb48e199cf2fbe9eecec6 Mon Sep 17 00:00:00 2001 From: johnthagen Date: Wed, 13 Feb 2019 19:00:16 -0500 Subject: [PATCH 68/97] Fix typo in caching docs --- docs/api-guide/caching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/caching.md b/docs/api-guide/caching.md index ff51aed06..5342345e4 100644 --- a/docs/api-guide/caching.md +++ b/docs/api-guide/caching.md @@ -13,7 +13,7 @@ provided in Django. Django provides a [`method_decorator`][decorator] to use decorators with class based views. This can be used with -with other cache decorators such as [`cache_page`][page] and +other cache decorators such as [`cache_page`][page] and [`vary_on_cookie`][cookie]. ```python From 3b996c6dc27c95749e1c3c2028e3d6adf3993052 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 12:01:36 +0100 Subject: [PATCH 69/97] Correct 3rd-party-packages link in issue template. Closes #6457 --- ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 8f2391d29..566bf9543 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -3,7 +3,7 @@ - [ ] 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/topics/third-party-resources/#about-third-party-packages) where possible.) +- [ ] 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.) From 606dd492279a856fa1eaf3487c1e66b36840a9c3 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 08:48:36 +0100 Subject: [PATCH 70/97] Update tox to use Django 2.2b2. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index cf6799a0a..4226f1a92 100644 --- a/tox.ini +++ b/tox.ini @@ -25,7 +25,7 @@ deps = django111: Django>=1.11,<2.0 django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 - django22: Django>=2.2a1,<3.0 + django22: Django>=2.2b1,<3.0 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From cb4cbb61f259b1fad659c714d64681d5add76c10 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 08:50:27 +0100 Subject: [PATCH 71/97] Fix search filter tests against Django 2.2. Django 2.2 enables foreign key constraint checking on SQLite. --- tests/test_filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index a7d9a07c1..2d4eb132e 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -221,7 +221,7 @@ class SearchFilterM2MTests(TestCase): # ... for idx in range(3): label = 'w' * (idx + 1) - AttributeModel(label=label) + AttributeModel.objects.create(label=label) for idx in range(10): title = 'z' * (idx + 1) From 481ae69df3b59d628689b03bbf18c6002d11a073 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 09:30:53 +0100 Subject: [PATCH 72/97] Add migration for CustomToken test model. Move authentication tests to sub-app to enable this. --- tests/authentication/__init__.py | 0 .../authentication/migrations/0001_initial.py | 24 +++++++++++++++++++ tests/authentication/migrations/__init__.py | 0 tests/authentication/models.py | 10 ++++++++ .../test_authentication.py | 20 +++++++--------- tests/conftest.py | 1 + 6 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 tests/authentication/__init__.py create mode 100644 tests/authentication/migrations/0001_initial.py create mode 100644 tests/authentication/migrations/__init__.py create mode 100644 tests/authentication/models.py rename tests/{ => authentication}/test_authentication.py (97%) diff --git a/tests/authentication/__init__.py b/tests/authentication/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/authentication/migrations/0001_initial.py b/tests/authentication/migrations/0001_initial.py new file mode 100644 index 000000000..cfc887240 --- /dev/null +++ b/tests/authentication/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='CustomToken', + fields=[ + ('key', models.CharField(max_length=40, primary_key=True, serialize=False)), + ('user', models.OneToOneField(on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/tests/authentication/migrations/__init__.py b/tests/authentication/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/authentication/models.py b/tests/authentication/models.py new file mode 100644 index 000000000..b8d1fd5a6 --- /dev/null +++ b/tests/authentication/models.py @@ -0,0 +1,10 @@ +# coding: utf-8 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import models + + +class CustomToken(models.Model): + key = models.CharField(max_length=40, primary_key=True) + user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) diff --git a/tests/test_authentication.py b/tests/authentication/test_authentication.py similarity index 97% rename from tests/test_authentication.py rename to tests/authentication/test_authentication.py index f2714acb5..793773542 100644 --- a/tests/test_authentication.py +++ b/tests/authentication/test_authentication.py @@ -8,7 +8,6 @@ import pytest from django.conf import settings from django.conf.urls import include, url from django.contrib.auth.models import User -from django.db import models from django.http import HttpResponse from django.test import TestCase, override_settings from django.utils import six @@ -26,14 +25,11 @@ from rest_framework.response import Response from rest_framework.test import APIClient, APIRequestFactory from rest_framework.views import APIView +from .models import CustomToken + factory = APIRequestFactory() -class CustomToken(models.Model): - key = models.CharField(max_length=40, primary_key=True) - user = models.OneToOneField(User, on_delete=models.CASCADE) - - class CustomTokenAuthentication(TokenAuthentication): model = CustomToken @@ -87,7 +83,7 @@ urlpatterns = [ ] -@override_settings(ROOT_URLCONF='tests.test_authentication') +@override_settings(ROOT_URLCONF=__name__) class BasicAuthTests(TestCase): """Basic authentication""" def setUp(self): @@ -169,7 +165,7 @@ class BasicAuthTests(TestCase): assert response.status_code == status.HTTP_401_UNAUTHORIZED -@override_settings(ROOT_URLCONF='tests.test_authentication') +@override_settings(ROOT_URLCONF=__name__) class SessionAuthTests(TestCase): """User session authentication""" def setUp(self): @@ -370,7 +366,7 @@ class BaseTokenAuthTests(object): assert response.status_code == status.HTTP_401_UNAUTHORIZED -@override_settings(ROOT_URLCONF='tests.test_authentication') +@override_settings(ROOT_URLCONF=__name__) class TokenAuthTests(BaseTokenAuthTests, TestCase): model = Token path = '/token/' @@ -429,13 +425,13 @@ class TokenAuthTests(BaseTokenAuthTests, TestCase): assert response.data['token'] == self.key -@override_settings(ROOT_URLCONF='tests.test_authentication') +@override_settings(ROOT_URLCONF=__name__) class CustomTokenAuthTests(BaseTokenAuthTests, TestCase): model = CustomToken path = '/customtoken/' -@override_settings(ROOT_URLCONF='tests.test_authentication') +@override_settings(ROOT_URLCONF=__name__) class CustomKeywordTokenAuthTests(BaseTokenAuthTests, TestCase): model = Token path = '/customkeywordtoken/' @@ -549,7 +545,7 @@ class BasicAuthenticationUnitTests(TestCase): authentication.authenticate = old_authenticate -@override_settings(ROOT_URLCONF='tests.test_authentication', +@override_settings(ROOT_URLCONF=__name__, AUTHENTICATION_BACKENDS=('django.contrib.auth.backends.RemoteUserBackend',)) class RemoteUserAuthenticationUnitTests(TestCase): def setUp(self): diff --git a/tests/conftest.py b/tests/conftest.py index 27558c02b..1c0c6dda7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,6 +56,7 @@ def pytest_configure(config): 'django.contrib.staticfiles', 'rest_framework', 'rest_framework.authtoken', + 'tests.authentication', 'tests.importable', 'tests', ), From 59fcbc6dd553cbbbd60a3dd7c57fb72fde8e7f68 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 14:29:57 +0100 Subject: [PATCH 73/97] Add migration for generic relations Tag model. --- tests/conftest.py | 1 + tests/generic_relations/__init__.py | 0 .../migrations/0001_initial.py | 36 +++++++++++++++ .../generic_relations/migrations/__init__.py | 0 tests/generic_relations/models.py | 46 +++++++++++++++++++ .../test_generic_relations.py} | 44 +----------------- 6 files changed, 84 insertions(+), 43 deletions(-) create mode 100644 tests/generic_relations/__init__.py create mode 100644 tests/generic_relations/migrations/0001_initial.py create mode 100644 tests/generic_relations/migrations/__init__.py create mode 100644 tests/generic_relations/models.py rename tests/{test_relations_generic.py => generic_relations/test_generic_relations.py} (63%) diff --git a/tests/conftest.py b/tests/conftest.py index 1c0c6dda7..ac29e4a42 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,6 +57,7 @@ def pytest_configure(config): 'rest_framework', 'rest_framework.authtoken', 'tests.authentication', + 'tests.generic_relations', 'tests.importable', 'tests', ), diff --git a/tests/generic_relations/__init__.py b/tests/generic_relations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/generic_relations/migrations/0001_initial.py b/tests/generic_relations/migrations/0001_initial.py new file mode 100644 index 000000000..ea04d8d67 --- /dev/null +++ b/tests/generic_relations/migrations/0001_initial.py @@ -0,0 +1,36 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Bookmark', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('url', models.URLField()), + ], + ), + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ], + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tag', models.SlugField()), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=models.CASCADE, to='contenttypes.ContentType')), + ], + ), + ] diff --git a/tests/generic_relations/migrations/__init__.py b/tests/generic_relations/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/generic_relations/models.py b/tests/generic_relations/models.py new file mode 100644 index 000000000..55bc243cb --- /dev/null +++ b/tests/generic_relations/models.py @@ -0,0 +1,46 @@ +from __future__ import unicode_literals + +from django.contrib.contenttypes.fields import ( + GenericForeignKey, GenericRelation +) +from django.contrib.contenttypes.models import ContentType +from django.db import models +from django.utils.encoding import python_2_unicode_compatible + + +@python_2_unicode_compatible +class Tag(models.Model): + """ + Tags have a descriptive slug, and are attached to an arbitrary object. + """ + tag = models.SlugField() + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + tagged_item = GenericForeignKey('content_type', 'object_id') + + def __str__(self): + return self.tag + + +@python_2_unicode_compatible +class Bookmark(models.Model): + """ + A URL bookmark that may have multiple tags attached. + """ + url = models.URLField() + tags = GenericRelation(Tag) + + def __str__(self): + return 'Bookmark: %s' % self.url + + +@python_2_unicode_compatible +class Note(models.Model): + """ + A textual note that may have multiple tags attached. + """ + text = models.TextField() + tags = GenericRelation(Tag) + + def __str__(self): + return 'Note: %s' % self.text diff --git a/tests/test_relations_generic.py b/tests/generic_relations/test_generic_relations.py similarity index 63% rename from tests/test_relations_generic.py rename to tests/generic_relations/test_generic_relations.py index a3798b0a3..c8de332e1 100644 --- a/tests/test_relations_generic.py +++ b/tests/generic_relations/test_generic_relations.py @@ -1,52 +1,10 @@ from __future__ import unicode_literals -from django.contrib.contenttypes.fields import ( - GenericForeignKey, GenericRelation -) -from django.contrib.contenttypes.models import ContentType -from django.db import models from django.test import TestCase -from django.utils.encoding import python_2_unicode_compatible from rest_framework import serializers - -@python_2_unicode_compatible -class Tag(models.Model): - """ - Tags have a descriptive slug, and are attached to an arbitrary object. - """ - tag = models.SlugField() - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField() - tagged_item = GenericForeignKey('content_type', 'object_id') - - def __str__(self): - return self.tag - - -@python_2_unicode_compatible -class Bookmark(models.Model): - """ - A URL bookmark that may have multiple tags attached. - """ - url = models.URLField() - tags = GenericRelation(Tag) - - def __str__(self): - return 'Bookmark: %s' % self.url - - -@python_2_unicode_compatible -class Note(models.Model): - """ - A textual note that may have multiple tags attached. - """ - text = models.TextField() - tags = GenericRelation(Tag) - - def __str__(self): - return 'Note: %s' % self.text +from .models import Bookmark, Note, Tag class TestGenericRelations(TestCase): From 1c5466eae772212eaf1df1832870cf64b77c9dc5 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 15:23:15 +0100 Subject: [PATCH 74/97] Remove Django 2.2 from allowed failure. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 796ef0502..9543cb452 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,6 @@ matrix: allow_failures: - env: DJANGO=master - - env: DJANGO=2.2 install: - pip install tox tox-venv tox-travis From 65f5c11a5b8aca5a6aca14c7ad85293fa941c37d Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 14 Feb 2019 15:38:21 +0100 Subject: [PATCH 75/97] Document support for Django 2.2. --- README.md | 2 +- docs/index.md | 2 +- setup.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 02f8ca275..0309ee2bd 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ There is a live example API for testing purposes, [available here][sandbox]. # Requirements * Python (2.7, 3.4, 3.5, 3.6, 3.7) -* Django (1.11, 2.0, 2.1) +* Django (1.11, 2.0, 2.1, 2.2) We **highly recommend** and only officially support the latest patch release of each Python and Django series. diff --git a/docs/index.md b/docs/index.md index b5ef5f5a6..c74b2caf0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -87,7 +87,7 @@ continued development by **[signing up for a paid plan][funding]**. REST framework requires the following: * Python (2.7, 3.4, 3.5, 3.6, 3.7) -* Django (1.11, 2.0, 2.1) +* Django (1.11, 2.0, 2.1, 2.2) We **highly recommend** and only officially support the latest patch release of each Python and Django series. diff --git a/setup.py b/setup.py index 341f33990..96384ab46 100755 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ setup( 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', 'Framework :: Django :: 2.1', + 'Framework :: Django :: 2.2', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', From e8b4bb1471b9fbd5895a63cc23b3b53ff45b3df1 Mon Sep 17 00:00:00 2001 From: kuter Date: Thu, 14 Feb 2019 17:51:10 +0100 Subject: [PATCH 76/97] Added tests for generateschema management command. (#6442) --- tests/test_generateschema.py | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/test_generateschema.py diff --git a/tests/test_generateschema.py b/tests/test_generateschema.py new file mode 100644 index 000000000..915c6ea05 --- /dev/null +++ b/tests/test_generateschema.py @@ -0,0 +1,88 @@ +from __future__ import unicode_literals + +import pytest +from django.conf.urls import url +from django.core.management import call_command +from django.test import TestCase +from django.test.utils import override_settings +from django.utils import six + +from rest_framework.compat import coreapi +from rest_framework.utils import formatting, json +from rest_framework.views import APIView + + +class FooView(APIView): + def get(self, request): + pass + + +urlpatterns = [ + url(r'^$', FooView.as_view()) +] + + +@override_settings(ROOT_URLCONF='tests.test_generateschema') +@pytest.mark.skipif(not coreapi, reason='coreapi is not installed') +class GenerateSchemaTests(TestCase): + """Tests for management command generateschema.""" + + def setUp(self): + self.out = six.StringIO() + + @pytest.mark.skipif(six.PY2, reason='PyYAML unicode output is malformed on PY2.') + def test_renders_default_schema_with_custom_title_url_and_description(self): + expected_out = """info: + description: Sample description + title: SampleAPI + version: '' + openapi: 3.0.0 + paths: + /: + get: + operationId: list + servers: + - url: http://api.sample.com/ + """ + call_command('generateschema', + '--title=SampleAPI', + '--url=http://api.sample.com', + '--description=Sample description', + stdout=self.out) + + self.assertIn(formatting.dedent(expected_out), self.out.getvalue()) + + def test_renders_openapi_json_schema(self): + expected_out = { + "openapi": "3.0.0", + "info": { + "version": "", + "title": "", + "description": "" + }, + "servers": [ + { + "url": "" + } + ], + "paths": { + "/": { + "get": { + "operationId": "list" + } + } + } + } + call_command('generateschema', + '--format=openapi-json', + stdout=self.out) + out_json = json.loads(self.out.getvalue()) + + self.assertDictEqual(out_json, expected_out) + + def test_renders_corejson_schema(self): + expected_out = """{"_type":"document","":{"list":{"_type":"link","url":"/","action":"get"}}}""" + call_command('generateschema', + '--format=corejson', + stdout=self.out) + self.assertIn(expected_out, self.out.getvalue()) From de3929fb3334262bbf57980fff41d3f8c6f2b8b4 Mon Sep 17 00:00:00 2001 From: Rohit Gupta Date: Fri, 15 Feb 2019 15:27:02 +0530 Subject: [PATCH 77/97] Add Python 3.7 to classifiers. (#6458) --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 96384ab46..cb850a3ae 100755 --- a/setup.py +++ b/setup.py @@ -72,6 +72,7 @@ setup( 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', 'Topic :: Internet :: WWW/HTTP', ] ) From f9401f5ff0c4738d02ea5d7576435913582006ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ufl?= Date: Sat, 16 Feb 2019 15:47:13 +0100 Subject: [PATCH 78/97] Fix Python 3 compat in documentation --- docs/api-guide/serializers.md | 12 ++++++------ docs/community/3.0-announcement.md | 2 +- docs/tutorial/1-serialization.md | 14 +++++++------- docs/tutorial/2-requests-and-responses.md | 10 +++++----- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 3ef930c64..e25053936 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -152,7 +152,7 @@ When deserializing data, you always need to call `is_valid()` before attempting serializer.is_valid() # False serializer.errors - # {'email': [u'Enter a valid e-mail address.'], 'created': [u'This field is required.']} + # {'email': ['Enter a valid e-mail address.'], 'created': ['This field is required.']} Each key in the dictionary will be the field name, and the values will be lists of strings of any error messages corresponding to that field. The `non_field_errors` key may also be present, and will list any general validation errors. The name of the `non_field_errors` key may be customized using the `NON_FIELD_ERRORS_KEY` REST framework setting. @@ -253,7 +253,7 @@ When passing data to a serializer instance, the unmodified data will be made ava By default, serializers must be passed values for all required fields or they will raise validation errors. You can use the `partial` argument in order to allow partial updates. # Update `comment` with partial data - serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) + serializer = CommentSerializer(comment, data={'content': 'foo bar'}, partial=True) ## Dealing with nested objects @@ -293,7 +293,7 @@ When dealing with nested representations that support deserializing the data, an serializer.is_valid() # False serializer.errors - # {'user': {'email': [u'Enter a valid e-mail address.']}, 'created': [u'This field is required.']} + # {'user': {'email': ['Enter a valid e-mail address.']}, 'created': ['This field is required.']} Similarly, the `.validated_data` property will include nested data structures. @@ -415,7 +415,7 @@ You can provide arbitrary additional context by passing a `context` argument whe serializer = AccountSerializer(account, context={'request': request}) serializer.data - # {'id': 6, 'owner': u'denvercoder9', 'created': datetime.datetime(2013, 2, 12, 09, 44, 56, 678870), 'details': 'http://example.com/accounts/6/details'} + # {'id': 6, 'owner': 'denvercoder9', 'created': datetime.datetime(2013, 2, 12, 09, 44, 56, 678870), 'details': 'http://example.com/accounts/6/details'} The context dictionary can be used within any serializer field logic, such as a custom `.to_representation()` method, by accessing the `self.context` attribute. @@ -1094,10 +1094,10 @@ This would then allow you to do the following: >>> model = User >>> fields = ('id', 'username', 'email') >>> - >>> print UserSerializer(user) + >>> print(UserSerializer(user)) {'id': 2, 'username': 'jonwatts', 'email': 'jon@example.com'} >>> - >>> print UserSerializer(user, fields=('id', 'email')) + >>> print(UserSerializer(user, fields=('id', 'email'))) {'id': 2, 'email': 'jon@example.com'} ## Customizing the default fields diff --git a/docs/community/3.0-announcement.md b/docs/community/3.0-announcement.md index 13be1e3cd..dc118d70c 100644 --- a/docs/community/3.0-announcement.md +++ b/docs/community/3.0-announcement.md @@ -389,7 +389,7 @@ You can include `expiry_date` as a field option on a `ModelSerializer` class. These fields will be mapped to `serializers.ReadOnlyField()` instances. >>> serializer = InvitationSerializer() - >>> print repr(serializer) + >>> print(repr(serializer)) InvitationSerializer(): to_email = EmailField(max_length=75) message = CharField(max_length=1000) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 387f99eda..ec507df05 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -137,20 +137,20 @@ Okay, once we've got a few imports out of the way, let's create a couple of code snippet = Snippet(code='foo = "bar"\n') snippet.save() - snippet = Snippet(code='print "hello, world"\n') + snippet = Snippet(code='print("hello, world")\n') snippet.save() We've now got a few snippet instances to play with. Let's take a look at serializing one of those instances. serializer = SnippetSerializer(snippet) serializer.data - # {'id': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'} + # {'id': 2, 'title': '', 'code': 'print("hello, world")\n', 'linenos': False, 'language': 'python', 'style': 'friendly'} At this point we've translated the model instance into Python native datatypes. To finalize the serialization process we render the data into `json`. content = JSONRenderer().render(serializer.data) content - # '{"id": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}' + # '{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}' Deserialization is similar. First we parse a stream into Python native datatypes... @@ -165,7 +165,7 @@ Deserialization is similar. First we parse a stream into Python native datatype serializer.is_valid() # True serializer.validated_data - # OrderedDict([('title', ''), ('code', 'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]) + # OrderedDict([('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]) serializer.save() # @@ -175,7 +175,7 @@ We can also serialize querysets instead of model instances. To do so we simply serializer = SnippetSerializer(Snippet.objects.all(), many=True) serializer.data - # [OrderedDict([('id', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', u''), ('code', u'print "hello, world"'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])] + # [OrderedDict([('id', 1), ('title', ''), ('code', 'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', ''), ('code', 'print("hello, world")\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', ''), ('code', 'print("hello, world")'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])] ## Using ModelSerializers @@ -338,7 +338,7 @@ Finally, we can get a list of all of the snippets: { "id": 2, "title": "", - "code": "print \"hello, world\"\n", + "code": "print(\"hello, world\")\n", "linenos": false, "language": "python", "style": "friendly" @@ -354,7 +354,7 @@ Or we can get a particular snippet by referencing its id: { "id": 2, "title": "", - "code": "print \"hello, world\"\n", + "code": "print(\"hello, world\")\n", "linenos": false, "language": "python", "style": "friendly" diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 4a9b0dbf7..e3d21e864 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -143,7 +143,7 @@ We can get a list of all of the snippets, as before. { "id": 2, "title": "", - "code": "print \"hello, world\"\n", + "code": "print(\"hello, world\")\n", "linenos": false, "language": "python", "style": "friendly" @@ -163,24 +163,24 @@ Or by appending a format suffix: Similarly, we can control the format of the request that we send, using the `Content-Type` header. # POST using form data - http --form POST http://127.0.0.1:8000/snippets/ code="print 123" + http --form POST http://127.0.0.1:8000/snippets/ code="print(123)" { "id": 3, "title": "", - "code": "print 123", + "code": "print(123)", "linenos": false, "language": "python", "style": "friendly" } # POST using JSON - http --json POST http://127.0.0.1:8000/snippets/ code="print 456" + http --json POST http://127.0.0.1:8000/snippets/ code="print(456)" { "id": 4, "title": "", - "code": "print 456", + "code": "print(456)", "linenos": false, "language": "python", "style": "friendly" From eb3180173ed119192c57a6ab52097025c00148e3 Mon Sep 17 00:00:00 2001 From: jeffrey k eliasen Date: Tue, 19 Feb 2019 03:15:03 -0800 Subject: [PATCH 79/97] Made templates compatible with session-based CSRF. (#6207) --- rest_framework/static/rest_framework/js/csrf.js | 2 +- rest_framework/templates/rest_framework/admin.html | 2 +- rest_framework/templates/rest_framework/base.html | 2 +- tests/test_templates.py | 12 +++++++++++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/rest_framework/static/rest_framework/js/csrf.js b/rest_framework/static/rest_framework/js/csrf.js index 97c8d0124..6e4bf39a7 100644 --- a/rest_framework/static/rest_framework/js/csrf.js +++ b/rest_framework/static/rest_framework/js/csrf.js @@ -38,7 +38,7 @@ function sameOrigin(url) { !(/^(\/\/|http:|https:).*/.test(url)); } -var csrftoken = getCookie(window.drf.csrfCookieName); +var csrftoken = window.drf.csrfToken; $.ajaxSetup({ beforeSend: function(xhr, settings) { diff --git a/rest_framework/templates/rest_framework/admin.html b/rest_framework/templates/rest_framework/admin.html index 66d8431f1..f058b2694 100644 --- a/rest_framework/templates/rest_framework/admin.html +++ b/rest_framework/templates/rest_framework/admin.html @@ -247,7 +247,7 @@ diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index e8a13674e..6d740f2b5 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -290,7 +290,7 @@ diff --git a/tests/test_templates.py b/tests/test_templates.py index a296395f6..19f511b96 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -1,7 +1,17 @@ +import re + 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('utf-8')) + + def test_base_template_with_no_context(): # base.html should be renderable with no context, # so it can be easily extended. - render({}, 'rest_framework/base.html') + 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('utf-8')) From 6de33effd6d0e1cc9ad2dc3a2a4d0487583a116f Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 19 Feb 2019 16:18:55 +0100 Subject: [PATCH 80/97] =?UTF-8?q?Doc=E2=80=99d=20requirement=20to=20implem?= =?UTF-8?q?ent=20has=5Fobject=5Fpermission()=20(#6462)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …when using provided permission classes. Closes #6402. --- docs/api-guide/permissions.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index e04b1199b..a797da9ac 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -48,6 +48,19 @@ For example: self.check_object_permissions(self.request, obj) return obj +--- + +**Note**: With the exception of `DjangoObjectPermissions`, the provided +permission classes in `rest_framework.permssions` **do not** implement the +methods necessary to check object permissions. + +If you wish to use the provided permission classes in order to check object +permissions, **you must** subclass them and implement the +`has_object_permission()` method described in the [_Custom +permissions_](#custom-permissions) section (below). + +--- + #### Limitations of object level permissions For performance reasons the generic views will not automatically apply object level permissions to each instance in a queryset when returning a list of objects. From 1ece516d2d0d942d9de513f85d601afcccf67ebd Mon Sep 17 00:00:00 2001 From: Si Feng Date: Tue, 19 Feb 2019 07:38:20 -0800 Subject: [PATCH 81/97] Adjusted field `validators` to accept iterables. (#6282) Closes 6280. --- rest_framework/fields.py | 4 ++-- rest_framework/serializers.py | 4 ++-- tests/test_fields.py | 19 +++++++++++++++++++ tests/test_serializer.py | 34 +++++++++++++++++++++++++++++++++- 4 files changed, 56 insertions(+), 5 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 562e52b22..2cbfd22bb 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -350,7 +350,7 @@ class Field(object): self.default_empty_html = default if validators is not None: - self.validators = validators[:] + self.validators = list(validators) # These are set up by `.bind()` when the field is added to a serializer. self.field_name = None @@ -410,7 +410,7 @@ class Field(object): self._validators = validators def get_validators(self): - return self.default_validators[:] + return list(self.default_validators) def get_initial(self): """ diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index eae08a34c..9830edb3f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -393,7 +393,7 @@ class Serializer(BaseSerializer): # Used by the lazily-evaluated `validators` property. meta = getattr(self, 'Meta', None) validators = getattr(meta, 'validators', None) - return validators[:] if validators else [] + return list(validators) if validators else [] def get_initial(self): if hasattr(self, 'initial_data'): @@ -1480,7 +1480,7 @@ class ModelSerializer(Serializer): # If the validators have been declared explicitly then use that. validators = getattr(getattr(self, 'Meta', None), 'validators', None) if validators is not None: - return validators[:] + return list(validators) # Otherwise use the default set of validators. return ( diff --git a/tests/test_fields.py b/tests/test_fields.py index 9a1d04979..12c936b22 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -740,6 +740,25 @@ class TestCharField(FieldValues): 'Null characters are not allowed.' ] + def test_iterable_validators(self): + """ + Ensure `validators` parameter is compatible with reasonable iterables. + """ + value = 'example' + + for validators in ([], (), set()): + field = serializers.CharField(validators=validators) + field.run_validation(value) + + def raise_exception(value): + raise exceptions.ValidationError('Raised error') + + for validators in ([raise_exception], (raise_exception,), set([raise_exception])): + field = serializers.CharField(validators=validators) + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation(value) + assert exc_info.value.detail == ['Raised error'] + class TestEmailField(FieldValues): """ diff --git a/tests/test_serializer.py b/tests/test_serializer.py index efa1adf0e..6e4ff22b2 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -10,7 +10,7 @@ from collections import Mapping import pytest from django.db import models -from rest_framework import fields, relations, serializers +from rest_framework import exceptions, fields, relations, serializers from rest_framework.compat import unicode_repr from rest_framework.fields import Field @@ -183,6 +183,38 @@ class TestSerializer: assert serializer.validated_data.coords[1] == 50.941357 assert serializer.errors == {} + def test_iterable_validators(self): + """ + Ensure `validators` parameter is compatible with reasonable iterables. + """ + data = {'char': 'abc', 'integer': 123} + + for validators in ([], (), set()): + class ExampleSerializer(serializers.Serializer): + char = serializers.CharField(validators=validators) + integer = serializers.IntegerField() + + serializer = ExampleSerializer(data=data) + assert serializer.is_valid() + assert serializer.validated_data == data + assert serializer.errors == {} + + def raise_exception(value): + raise exceptions.ValidationError('Raised error') + + for validators in ([raise_exception], (raise_exception,), set([raise_exception])): + class ExampleSerializer(serializers.Serializer): + char = serializers.CharField(validators=validators) + integer = serializers.IntegerField() + + serializer = ExampleSerializer(data=data) + assert not serializer.is_valid() + assert serializer.data == data + assert serializer.validated_data == {} + assert serializer.errors == {'char': [ + exceptions.ErrorDetail(string='Raised error', code='invalid') + ]} + class TestValidateMethod: def test_non_field_error_validate_method(self): From d110454d4c15fe6617a112e846b89e09ed6c95b2 Mon Sep 17 00:00:00 2001 From: Allan Reyes Date: Tue, 19 Feb 2019 08:18:14 -0800 Subject: [PATCH 82/97] Added SearchFilter.get_search_fields() hook. (#6279) --- docs/api-guide/filtering.md | 7 +++++++ rest_framework/filters.py | 10 +++++++++- tests/test_filters.py | 25 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 1a04ad5e3..5d1f6e49a 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -218,6 +218,13 @@ For example: By default, the search parameter is named `'search`', but this may be overridden with the `SEARCH_PARAM` setting. +To dynamically change search fields based on request content, it's possible to subclass the `SearchFilter` and override the `get_search_fields()` function. For example, the following subclass will only search on `title` if the query parameter `title_only` is in the request: + + class CustomSearchFilter(self, view, request): + if request.query_params.get('title_only'): + return ('title',) + return super(CustomSearchFilter, self).get_search_fields(view, request) + For more details, see the [Django documentation][search-django-admin]. --- diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 0627bd8c4..53d49ae45 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -53,6 +53,14 @@ class SearchFilter(BaseFilterBackend): search_title = _('Search') search_description = _('A search term.') + def get_search_fields(self, view, request): + """ + Search fields are obtained from the view, but the request is always + passed to this method. Sub-classes can override this method to + dynamically change the search fields based on request content. + """ + return getattr(view, 'search_fields', None) + def get_search_terms(self, request): """ Search terms are set by a ?search=... query parameter, @@ -90,7 +98,7 @@ class SearchFilter(BaseFilterBackend): return False def filter_queryset(self, request, queryset, view): - search_fields = getattr(view, 'search_fields', None) + search_fields = self.get_search_fields(view, request) search_terms = self.get_search_terms(request) if not search_fields or not search_terms: diff --git a/tests/test_filters.py b/tests/test_filters.py index 2d4eb132e..39a96f994 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -156,6 +156,31 @@ class SearchFilterTests(TestCase): reload_module(filters) + def test_search_with_filter_subclass(self): + class CustomSearchFilter(filters.SearchFilter): + # Filter that dynamically changes search fields + def get_search_fields(self, view, request): + if request.query_params.get('title_only'): + return ('$title',) + return super(CustomSearchFilter, self).get_search_fields(view, request) + + class SearchListView(generics.ListAPIView): + queryset = SearchFilterModel.objects.all() + serializer_class = SearchFilterSerializer + filter_backends = (CustomSearchFilter,) + search_fields = ('$title', '$text') + + view = SearchListView.as_view() + request = factory.get('/', {'search': '^\w{3}$'}) + response = view(request) + assert len(response.data) == 10 + + request = factory.get('/', {'search': '^\w{3}$', 'title_only': 'true'}) + response = view(request) + assert response.data == [ + {'id': 3, 'title': 'zzz', 'text': 'cde'} + ] + class AttributeModel(models.Model): label = models.CharField(max_length=32) From d932baa64660012cb7e5b86ba0f985fee8d5e57c Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 22 Feb 2019 11:11:52 +0100 Subject: [PATCH 83/97] Corrected link to ajax-form library. Closes #6465. --- docs/topics/browser-enhancements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/browser-enhancements.md b/docs/topics/browser-enhancements.md index 0e79a66e1..fa07b6064 100644 --- a/docs/topics/browser-enhancements.md +++ b/docs/topics/browser-enhancements.md @@ -81,7 +81,7 @@ was later [dropped from the spec][html5]. There remains as well as how to support content types other than form-encoded data. [cite]: https://www.amazon.com/RESTful-Web-Services-Leonard-Richardson/dp/0596529260 -[ajax-form]: https://github.com/encode/ajax-form +[ajax-form]: https://github.com/tomchristie/ajax-form [rails]: https://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work [html5]: https://www.w3.org/TR/html5-diff/#changes-2010-06-24 [put_delete]: http://amundsen.com/examples/put-delete-forms/ From 286cf57a8d22aafd51054a40a5cf8a58edfc8226 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Fri, 22 Feb 2019 10:58:01 -0800 Subject: [PATCH 84/97] Update filtering docs (#6467) --- docs/api-guide/filtering.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 5d1f6e49a..aff267818 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -127,7 +127,7 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering """ model = Product serializer_class = ProductSerializer - filter_class = ProductFilter + filterset_class = ProductFilter def get_queryset(self): user = self.request.user @@ -305,9 +305,9 @@ A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectP **permissions.py**: class CustomObjectPermissions(permissions.DjangoObjectPermissions): - """ - Similar to `DjangoObjectPermissions`, but adding 'view' permissions. - """ + """ + Similar to `DjangoObjectPermissions`, but adding 'view' permissions. + """ perms_map = { 'GET': ['%(app_label)s.view_%(model_name)s'], 'OPTIONS': ['%(app_label)s.view_%(model_name)s'], @@ -321,11 +321,11 @@ A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectP **views.py**: class EventViewSet(viewsets.ModelViewSet): - """ - Viewset that only lists events if user has 'view' permissions, and only - allows operations on individual events if user has appropriate 'view', 'add', - 'change' or 'delete' permissions. - """ + """ + Viewset that only lists events if user has 'view' permissions, and only + allows operations on individual events if user has appropriate 'view', 'add', + 'change' or 'delete' permissions. + """ queryset = Event.objects.all() serializer_class = EventSerializer filter_backends = (filters.DjangoObjectPermissionsFilter,) From 07c5c968ce8b06c7ebbcc91a070aa492510611b2 Mon Sep 17 00:00:00 2001 From: Charlie Hornsby Date: Mon, 25 Feb 2019 10:17:04 +0200 Subject: [PATCH 85/97] Fix DeprecationWarning when accessing collections.abc classes via collections (#6268) * Use compat version of collections.abc.Mapping Since the Mapping class will no longer be available to import directly from the collections module in Python 3.8, we should use the compatibility helper introduced in #6154 in the fields module. * Alias and use compat version of collections.abc.MutableMapping Since the MutableMapping class will no longer be available to import directly from the collections module in Python 3.8, we should create an alias for it in the compat module and use that instead. --- rest_framework/compat.py | 4 ++-- rest_framework/fields.py | 7 +++---- rest_framework/utils/serializer_helpers.py | 5 ++--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 5a4bcdf66..59217c587 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -12,10 +12,10 @@ from django.views.generic import View try: # Python 3 - from collections.abc import Mapping # noqa + from collections.abc import Mapping, MutableMapping # noqa except ImportError: # Python 2.7 - from collections import Mapping # noqa + from collections import Mapping, MutableMapping # noqa try: from django.urls import ( # noqa diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 2cbfd22bb..1b8387714 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals -import collections import copy import datetime import decimal @@ -33,7 +32,7 @@ from pytz.exceptions import InvalidTimeError from rest_framework import ISO_8601 from rest_framework.compat import ( - MaxLengthValidator, MaxValueValidator, MinLengthValidator, + Mapping, MaxLengthValidator, MaxValueValidator, MinLengthValidator, MinValueValidator, ProhibitNullCharactersValidator, unicode_repr, unicode_to_repr ) @@ -96,7 +95,7 @@ def get_attribute(instance, attrs): """ for attr in attrs: try: - if isinstance(instance, collections.Mapping): + if isinstance(instance, Mapping): instance = instance[attr] else: instance = getattr(instance, attr) @@ -1661,7 +1660,7 @@ class ListField(Field): """ if html.is_html_input(data): data = html.parse_html_list(data, default=[]) - if isinstance(data, type('')) or isinstance(data, collections.Mapping) or not hasattr(data, '__iter__'): + if isinstance(data, type('')) or isinstance(data, Mapping) or not hasattr(data, '__iter__'): self.fail('not_a_list', input_type=type(data).__name__) if not self.allow_empty and len(data) == 0: self.fail('empty') diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index 6b662a66c..c24e51d09 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -1,11 +1,10 @@ from __future__ import unicode_literals -import collections from collections import OrderedDict from django.utils.encoding import force_text -from rest_framework.compat import unicode_to_repr +from rest_framework.compat import MutableMapping, unicode_to_repr from rest_framework.utils import json @@ -130,7 +129,7 @@ class NestedBoundField(BoundField): return self.__class__(self._field, values, self.errors, self._prefix) -class BindingDict(collections.MutableMapping): +class BindingDict(MutableMapping): """ This dict-like object is used to store fields on a serializer. From 8a29c53226f63733775a232fea0c2c65cb50b6b0 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 25 Feb 2019 11:49:29 +0100 Subject: [PATCH 86/97] Allowed Q objects in limit_choices_to introspection. (#6472) Closes #6470. --- rest_framework/utils/field_mapping.py | 4 +++- tests/models.py | 7 +++++++ tests/test_relations_pk.py | 21 +++++++++++++++++---- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 991f20f17..f11b4b94e 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -251,7 +251,9 @@ def get_relation_kwargs(field_name, relation_info): limit_choices_to = model_field and model_field.get_limit_choices_to() if limit_choices_to: - kwargs['queryset'] = kwargs['queryset'].filter(**limit_choices_to) + if not isinstance(limit_choices_to, models.Q): + limit_choices_to = models.Q(**limit_choices_to) + kwargs['queryset'] = kwargs['queryset'].filter(limit_choices_to) if has_through_model: kwargs['read_only'] = True diff --git a/tests/models.py b/tests/models.py index 55f250e04..17bf23cda 100644 --- a/tests/models.py +++ b/tests/models.py @@ -59,6 +59,13 @@ class ForeignKeySourceWithLimitedChoices(RESTFrameworkModel): on_delete=models.CASCADE) +class ForeignKeySourceWithQLimitedChoices(RESTFrameworkModel): + target = models.ForeignKey(ForeignKeyTarget, help_text='Target', + verbose_name='Target', + limit_choices_to=models.Q(name__startswith="limited-"), + on_delete=models.CASCADE) + + # Nullable ForeignKey class NullableForeignKeySource(RESTFrameworkModel): name = models.CharField(max_length=100) diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py index 31b6bb867..2cffb62e6 100644 --- a/tests/test_relations_pk.py +++ b/tests/test_relations_pk.py @@ -5,10 +5,11 @@ from django.utils import six from rest_framework import serializers from tests.models import ( - ForeignKeySource, ForeignKeySourceWithLimitedChoices, ForeignKeyTarget, - ManyToManySource, ManyToManyTarget, NullableForeignKeySource, - NullableOneToOneSource, NullableUUIDForeignKeySource, OneToOnePKSource, - OneToOneTarget, UUIDForeignKeyTarget + ForeignKeySource, ForeignKeySourceWithLimitedChoices, + ForeignKeySourceWithQLimitedChoices, ForeignKeyTarget, ManyToManySource, + ManyToManyTarget, NullableForeignKeySource, NullableOneToOneSource, + NullableUUIDForeignKeySource, OneToOnePKSource, OneToOneTarget, + UUIDForeignKeyTarget ) @@ -378,6 +379,18 @@ class PKForeignKeyTests(TestCase): queryset = ForeignKeySourceWithLimitedChoicesSerializer().fields["target"].get_queryset() assert len(queryset) == 1 + def test_queryset_size_with_Q_limited_choices(self): + limited_target = ForeignKeyTarget(name="limited-target") + limited_target.save() + + class QLimitedChoicesSerializer(serializers.ModelSerializer): + class Meta: + model = ForeignKeySourceWithQLimitedChoices + fields = ("id", "target") + + queryset = QLimitedChoicesSerializer().fields["target"].get_queryset() + assert len(queryset) == 1 + class PKNullableForeignKeyTests(TestCase): def setUp(self): From 94fbfcb6fdc8f9755a9f005bd005b00be3309ff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Massart?= Date: Mon, 25 Feb 2019 20:47:02 +0800 Subject: [PATCH 87/97] Added lazy evaluation to composed permissions. (#6463) Refs #6402. --- rest_framework/compat.py | 11 +++++ rest_framework/permissions.py | 8 ++-- tests/test_permissions.py | 87 ++++++++++++++++++++++++++++++++++- 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 59217c587..9422e6ad5 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -5,6 +5,8 @@ versions of Django/Python, and compatibility wrappers around optional packages. from __future__ import unicode_literals +import sys + from django.conf import settings from django.core import validators from django.utils import six @@ -34,6 +36,11 @@ try: except ImportError: ProhibitNullCharactersValidator = None +try: + from unittest import mock +except ImportError: + mock = None + def get_original_route(urlpattern): """ @@ -314,3 +321,7 @@ class MinLengthValidator(CustomValidatorMessage, validators.MinLengthValidator): class MaxLengthValidator(CustomValidatorMessage, validators.MaxLengthValidator): pass + + +# Version Constants. +PY36 = sys.version_info >= (3, 6) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index ac616e202..69432d79a 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -44,13 +44,13 @@ class AND: def has_permission(self, request, view): return ( - self.op1.has_permission(request, view) & + self.op1.has_permission(request, view) and self.op2.has_permission(request, view) ) def has_object_permission(self, request, view, obj): return ( - self.op1.has_object_permission(request, view, obj) & + self.op1.has_object_permission(request, view, obj) and self.op2.has_object_permission(request, view, obj) ) @@ -62,13 +62,13 @@ class OR: def has_permission(self, request, view): return ( - self.op1.has_permission(request, view) | + self.op1.has_permission(request, view) or self.op2.has_permission(request, view) ) def has_object_permission(self, request, view, obj): return ( - self.op1.has_object_permission(request, view, obj) | + self.op1.has_object_permission(request, view, obj) or self.op2.has_object_permission(request, view, obj) ) diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 381ec448c..f9d53430f 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -5,6 +5,7 @@ import unittest import warnings import django +import pytest from django.contrib.auth.models import AnonymousUser, Group, Permission, User from django.db import models from django.test import TestCase @@ -14,7 +15,7 @@ from rest_framework import ( HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers, status, views ) -from rest_framework.compat import is_guardian_installed +from rest_framework.compat import PY36, is_guardian_installed, mock from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.routers import DefaultRouter from rest_framework.test import APIRequestFactory @@ -600,3 +601,87 @@ class PermissionsCompositionTests(TestCase): permissions.IsAuthenticated ) assert composed_perm().has_permission(request, None) is True + + @pytest.mark.skipif(not PY36, reason="assert_called_once() not available") + def test_or_lazyness(self): + request = factory.get('/1', format='json') + request.user = AnonymousUser() + + with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: + composed_perm = (permissions.AllowAny | permissions.IsAuthenticated) + hasperm = composed_perm().has_permission(request, None) + self.assertIs(hasperm, True) + mock_allow.assert_called_once() + mock_deny.assert_not_called() + + with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: + composed_perm = (permissions.IsAuthenticated | permissions.AllowAny) + hasperm = composed_perm().has_permission(request, None) + self.assertIs(hasperm, True) + mock_deny.assert_called_once() + mock_allow.assert_called_once() + + @pytest.mark.skipif(not PY36, reason="assert_called_once() not available") + def test_object_or_lazyness(self): + request = factory.get('/1', format='json') + request.user = AnonymousUser() + + with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny: + composed_perm = (permissions.AllowAny | permissions.IsAuthenticated) + hasperm = composed_perm().has_object_permission(request, None, None) + self.assertIs(hasperm, True) + mock_allow.assert_called_once() + mock_deny.assert_not_called() + + with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny: + composed_perm = (permissions.IsAuthenticated | permissions.AllowAny) + hasperm = composed_perm().has_object_permission(request, None, None) + self.assertIs(hasperm, True) + mock_deny.assert_called_once() + mock_allow.assert_called_once() + + @pytest.mark.skipif(not PY36, reason="assert_called_once() not available") + def test_and_lazyness(self): + request = factory.get('/1', format='json') + request.user = AnonymousUser() + + with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: + composed_perm = (permissions.AllowAny & permissions.IsAuthenticated) + hasperm = composed_perm().has_permission(request, None) + self.assertIs(hasperm, False) + mock_allow.assert_called_once() + mock_deny.assert_called_once() + + with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny: + composed_perm = (permissions.IsAuthenticated & permissions.AllowAny) + hasperm = composed_perm().has_permission(request, None) + self.assertIs(hasperm, False) + mock_allow.assert_not_called() + mock_deny.assert_called_once() + + @pytest.mark.skipif(not PY36, reason="assert_called_once() not available") + def test_object_and_lazyness(self): + request = factory.get('/1', format='json') + request.user = AnonymousUser() + + with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny: + composed_perm = (permissions.AllowAny & permissions.IsAuthenticated) + hasperm = composed_perm().has_object_permission(request, None, None) + self.assertIs(hasperm, False) + mock_allow.assert_called_once() + mock_deny.assert_called_once() + + with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow: + with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny: + composed_perm = (permissions.IsAuthenticated & permissions.AllowAny) + hasperm = composed_perm().has_object_permission(request, None, None) + self.assertIs(hasperm, False) + mock_allow.assert_not_called() + mock_deny.assert_called_once() From 739b0a272a66d9beb7afb7490f201d71d7cdc910 Mon Sep 17 00:00:00 2001 From: Xtreak Date: Mon, 25 Feb 2019 19:52:45 +0530 Subject: [PATCH 88/97] Fix DeprecationWarning in tests when accessing collections.abc classes via collections (#6473) --- tests/test_renderers.py | 4 ++-- tests/test_serializer.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 8518a3f7c..b4c41b148 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import re -from collections import MutableMapping, OrderedDict +from collections import OrderedDict import pytest from django.conf.urls import include, url @@ -16,7 +16,7 @@ from django.utils.safestring import SafeText from django.utils.translation import ugettext_lazy as _ from rest_framework import permissions, serializers, status -from rest_framework.compat import coreapi +from rest_framework.compat import MutableMapping, coreapi from rest_framework.decorators import action from rest_framework.renderers import ( AdminRenderer, BaseRenderer, BrowsableAPIRenderer, DocumentationRenderer, diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 6e4ff22b2..0f1e81965 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -5,13 +5,12 @@ import inspect import pickle import re import unittest -from collections import Mapping import pytest from django.db import models from rest_framework import exceptions, fields, relations, serializers -from rest_framework.compat import unicode_repr +from rest_framework.compat import Mapping, unicode_repr from rest_framework.fields import Field from .models import ( From 2daf6f13414f1a5d363b5bc4a2ce3ba294a7766c Mon Sep 17 00:00:00 2001 From: Adrien Brunet Date: Mon, 25 Feb 2019 15:33:40 +0100 Subject: [PATCH 89/97] Add negation ~ operator to permissions composition (#6361) --- docs/api-guide/permissions.md | 2 +- rest_framework/permissions.py | 24 ++++++++++++++++++++++++ tests/test_permissions.py | 25 ++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index a797da9ac..6a1297e60 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -134,7 +134,7 @@ Provided they inherit from `rest_framework.permissions.BasePermission`, permissi } return Response(content) -__Note:__ it only supports & -and- and | -or-. +__Note:__ it supports & (and), | (or) and ~ (not). --- diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 69432d79a..5d75f54ba 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -24,6 +24,19 @@ class OperationHolderMixin: def __ror__(self, other): return OperandHolder(OR, other, self) + def __invert__(self): + return SingleOperandHolder(NOT, self) + + +class SingleOperandHolder(OperationHolderMixin): + def __init__(self, operator_class, op1_class): + self.operator_class = operator_class + self.op1_class = op1_class + + def __call__(self, *args, **kwargs): + op1 = self.op1_class(*args, **kwargs) + return self.operator_class(op1) + class OperandHolder(OperationHolderMixin): def __init__(self, operator_class, op1_class, op2_class): @@ -73,6 +86,17 @@ class OR: ) +class NOT: + def __init__(self, op1): + self.op1 = op1 + + def has_permission(self, request, view): + return not self.op1.has_permission(request, view) + + def has_object_permission(self, request, view, obj): + return not self.op1.has_object_permission(request, view, obj) + + class BasePermissionMetaclass(OperationHolderMixin, type): pass diff --git a/tests/test_permissions.py b/tests/test_permissions.py index f9d53430f..807006858 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -580,7 +580,19 @@ class PermissionsCompositionTests(TestCase): composed_perm = permissions.IsAuthenticated | permissions.AllowAny assert composed_perm().has_permission(request, None) is True - def test_several_levels(self): + def test_not_false(self): + request = factory.get('/1', format='json') + request.user = AnonymousUser() + composed_perm = ~permissions.IsAuthenticated + assert composed_perm().has_permission(request, None) is True + + def test_not_true(self): + request = factory.get('/1', format='json') + request.user = self.user + composed_perm = ~permissions.AllowAny + assert composed_perm().has_permission(request, None) is False + + def test_several_levels_without_negation(self): request = factory.get('/1', format='json') request.user = self.user composed_perm = ( @@ -591,6 +603,17 @@ class PermissionsCompositionTests(TestCase): ) assert composed_perm().has_permission(request, None) is True + def test_several_levels_and_precedence_with_negation(self): + request = factory.get('/1', format='json') + request.user = self.user + composed_perm = ( + permissions.IsAuthenticated & + ~ permissions.IsAdminUser & + permissions.IsAuthenticated & + ~(permissions.IsAdminUser & permissions.IsAdminUser) + ) + assert composed_perm().has_permission(request, None) is True + def test_several_levels_and_precedence(self): request = factory.get('/1', format='json') request.user = self.user From 317174b163d80aaa8be44b8d3bf073d5c50acb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20G=C3=B3rski?= Date: Mon, 25 Feb 2019 16:59:25 +0100 Subject: [PATCH 90/97] Avoided calling distinct on annotated fields in SearchFilter. (#6240) Fixes #6094 --- rest_framework/filters.py | 3 +++ tests/test_filters.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 53d49ae45..7989ace34 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -85,6 +85,9 @@ class SearchFilter(BaseFilterBackend): opts = queryset.model._meta if search_field[0] in self.lookup_prefixes: search_field = search_field[1:] + # Annotated fields do not need to be distinct + if isinstance(queryset, models.QuerySet) and search_field in queryset.query.annotations: + return False parts = search_field.split(LOOKUP_SEP) for part in parts: field = opts.get_field(part) diff --git a/tests/test_filters.py b/tests/test_filters.py index 39a96f994..088d25436 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -5,6 +5,7 @@ import datetime import pytest from django.core.exceptions import ImproperlyConfigured from django.db import models +from django.db.models.functions import Concat, Upper from django.test import TestCase from django.test.utils import override_settings from django.utils.six.moves import reload_module @@ -329,6 +330,38 @@ class SearchFilterToManyTests(TestCase): assert len(response.data) == 1 +class SearchFilterAnnotatedSerializer(serializers.ModelSerializer): + title_text = serializers.CharField() + + class Meta: + model = SearchFilterModel + fields = ('title', 'text', 'title_text') + + +class SearchFilterAnnotatedFieldTests(TestCase): + @classmethod + def setUpTestData(cls): + SearchFilterModel.objects.create(title='abc', text='def') + SearchFilterModel.objects.create(title='ghi', text='jkl') + + def test_search_in_annotated_field(self): + class SearchListView(generics.ListAPIView): + queryset = SearchFilterModel.objects.annotate( + title_text=Upper( + Concat(models.F('title'), models.F('text')) + ) + ).all() + serializer_class = SearchFilterAnnotatedSerializer + filter_backends = (filters.SearchFilter,) + search_fields = ('title_text',) + + view = SearchListView.as_view() + request = factory.get('/', {'search': 'ABCDEF'}) + response = view(request) + assert len(response.data) == 1 + assert response.data[0]['title_text'] == 'ABCDEF' + + class OrderingFilterModel(models.Model): title = models.CharField(max_length=20, verbose_name='verbose title') text = models.CharField(max_length=100) From 1dc81acb4d351ef954291391b9151fa9c524ea43 Mon Sep 17 00:00:00 2001 From: Ramon de Jezus Date: Thu, 28 Feb 2019 15:18:58 +0100 Subject: [PATCH 91/97] Fixed a typo in pagination docs. (#6475) --- docs/api-guide/pagination.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 7ae351a7f..99612ef46 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -311,7 +311,7 @@ The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagi ## link-header-pagination -The [`django-rest-framework-link-header-pagination` package][drf-link-header-pagination] includes a `LinkHeaderPagination` class which provides pagination via an HTTP `Link` header as desribed in [Github's developer documentation](github-link-pagination). +The [`django-rest-framework-link-header-pagination` package][drf-link-header-pagination] includes a `LinkHeaderPagination` class which provides pagination via an HTTP `Link` header as described in [Github's developer documentation](github-link-pagination). [cite]: https://docs.djangoproject.com/en/stable/topics/pagination/ [link-header]: ../img/link-header-pagination.png From 31bf59708121127994bdbcd038f4f76bb28059d7 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 1 Mar 2019 12:48:12 +0100 Subject: [PATCH 92/97] Updated note on BooleanField required kwarg generation. Closes #6474. --- docs/api-guide/fields.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 8d25d6c78..74ce2251d 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -124,7 +124,14 @@ A boolean representation. When using HTML encoded form input be aware that omitting a value will always be treated as setting a field to `False`, even if it has a `default=True` option specified. This is because HTML checkbox inputs represent the unchecked state by omitting the value, so REST framework treats omission as if it is an empty checkbox input. -Note that default `BooleanField` instances will be generated with a `required=False` option (since Django `models.BooleanField` is always `blank=True`). If you want to change this behaviour explicitly declare the `BooleanField` on the serializer class. +Note that Django 2.1 removed the `blank` kwarg from `models.BooleanField`. +Prior to Django 2.1 `models.BooleanField` fields were always `blank=True`. Thus +since Django 2.1 default `serializers.BooleanField` instances will be generated +without the `required` kwarg (i.e. equivalent to `required=True`) whereas with +previous versions of Django, default `BooleanField` instances will be generated +with a `required=False` option. If you want to control this behaviour manually, +explicitly declare the `BooleanField` on the serializer class, or use the +`extra_kwargs` option to set the `required` flag. Corresponds to `django.db.models.fields.BooleanField`. From a216d02ce0661510d456cfd327fadc3acb9ec960 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 2 Mar 2019 12:48:03 -0800 Subject: [PATCH 93/97] Merge multiple isinstance() calls to one (#6481) https://docs.python.org/3/library/functions.html#isinstance > If classinfo is a tuple of type objects (or recursively, other such > tuples), return true if object is an instance of any of the types. --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1b8387714..b5fafeaa3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1660,7 +1660,7 @@ class ListField(Field): """ if html.is_html_input(data): data = html.parse_html_list(data, default=[]) - if isinstance(data, type('')) or isinstance(data, Mapping) or not hasattr(data, '__iter__'): + if isinstance(data, (type(''), Mapping)) or not hasattr(data, '__iter__'): self.fail('not_a_list', input_type=type(data).__name__) if not self.allow_empty and len(data) == 0: self.fail('empty') From 94593b3a503472c7d1ca97b8e191ef5c8a3c00ff Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 3 Mar 2019 09:20:45 +0100 Subject: [PATCH 94/97] =?UTF-8?q?Introduce=20RemovedInDRF=E2=80=A6Warning?= =?UTF-8?q?=20classes=20to=20simplify=20deprecations.=20(#6480)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #6290. --- docs/community/release-notes.md | 4 ++-- rest_framework/__init__.py | 8 ++++++++ rest_framework/decorators.py | 5 +++-- rest_framework/filters.py | 3 ++- rest_framework/routers.py | 12 +++++++----- tests/test_decorators.py | 8 ++++---- tests/test_permissions.py | 6 +++--- tests/test_routers.py | 8 +++++--- 8 files changed, 34 insertions(+), 20 deletions(-) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 0efc7fa6c..62ad95723 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -18,9 +18,9 @@ REST framework releases follow a formal deprecation policy, which is in line wit The timeline for deprecation of a feature present in version 1.0 would work as follows: -* Version 1.1 would remain **fully backwards compatible** with 1.0, but would raise `PendingDeprecationWarning` warnings if you use the feature that are due to be deprecated. These warnings are **silent by default**, but can be explicitly enabled when you're ready to start migrating any required changes. For example if you start running your tests using `python -Wd manage.py test`, you'll be warned of any API changes you need to make. +* Version 1.1 would remain **fully backwards compatible** with 1.0, but would raise `RemovedInDRF13Warning` warnings, subclassing `PendingDeprecationWarning`, if you use the feature that are due to be deprecated. These warnings are **silent by default**, but can be explicitly enabled when you're ready to start migrating any required changes. For example if you start running your tests using `python -Wd manage.py test`, you'll be warned of any API changes you need to make. -* Version 1.2 would escalate these warnings to `DeprecationWarning`, which is loud by default. +* Version 1.2 would escalate these warnings to subclass `DeprecationWarning`, which is loud by default. * Version 1.3 would remove the deprecated bits of API entirely. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index b9da046ae..eacc8dca0 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -23,3 +23,11 @@ HTTP_HEADER_ENCODING = 'iso-8859-1' ISO_8601 = 'iso-8601' default_app_config = 'rest_framework.apps.RestFrameworkConfig' + + +class RemovedInDRF310Warning(DeprecationWarning): + pass + + +class RemovedInDRF311Warning(PendingDeprecationWarning): + pass diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index f6d557d11..30bfcc4e5 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -14,6 +14,7 @@ import warnings from django.forms.utils import pretty_name from django.utils import six +from rest_framework import RemovedInDRF310Warning from rest_framework.views import APIView @@ -225,7 +226,7 @@ def detail_route(methods=None, **kwargs): warnings.warn( "`detail_route` is deprecated and will be removed in 3.10 in favor of " "`action`, which accepts a `detail` bool. Use `@action(detail=True)` instead.", - DeprecationWarning, stacklevel=2 + RemovedInDRF310Warning, stacklevel=2 ) def decorator(func): @@ -243,7 +244,7 @@ def list_route(methods=None, **kwargs): warnings.warn( "`list_route` is deprecated and will be removed in 3.10 in favor of " "`action`, which accepts a `detail` bool. Use `@action(detail=False)` instead.", - DeprecationWarning, stacklevel=2 + RemovedInDRF310Warning, stacklevel=2 ) def decorator(func): diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 7989ace34..bb1b86586 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -17,6 +17,7 @@ from django.utils import six from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ +from rest_framework import RemovedInDRF310Warning from rest_framework.compat import ( coreapi, coreschema, distinct, is_guardian_installed ) @@ -299,7 +300,7 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend): warnings.warn( "`DjangoObjectPermissionsFilter` has been deprecated and moved to " "the 3rd-party django-rest-framework-guardian package.", - DeprecationWarning, stacklevel=2 + RemovedInDRF310Warning, stacklevel=2 ) assert is_guardian_installed(), 'Using DjangoObjectPermissionsFilter, but django-guardian is not installed' diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 2c24f9099..1cacea181 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -25,7 +25,9 @@ from django.urls import NoReverseMatch from django.utils import six from django.utils.deprecation import RenameMethodsBase -from rest_framework import views +from rest_framework import ( + RemovedInDRF310Warning, RemovedInDRF311Warning, views +) from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.schemas import SchemaGenerator @@ -43,7 +45,7 @@ class DynamicDetailRoute(object): "`DynamicDetailRoute` is deprecated and will be removed in 3.10 " "in favor of `DynamicRoute`, which accepts a `detail` boolean. Use " "`DynamicRoute(url, name, True, initkwargs)` instead.", - DeprecationWarning, stacklevel=2 + RemovedInDRF310Warning, stacklevel=2 ) return DynamicRoute(url, name, True, initkwargs) @@ -54,7 +56,7 @@ class DynamicListRoute(object): "`DynamicListRoute` is deprecated and will be removed in 3.10 in " "favor of `DynamicRoute`, which accepts a `detail` boolean. Use " "`DynamicRoute(url, name, False, initkwargs)` instead.", - DeprecationWarning, stacklevel=2 + RemovedInDRF310Warning, stacklevel=2 ) return DynamicRoute(url, name, False, initkwargs) @@ -77,7 +79,7 @@ def flatten(list_of_lists): class RenameRouterMethods(RenameMethodsBase): renamed_methods = ( - ('get_default_base_name', 'get_default_basename', PendingDeprecationWarning), + ('get_default_base_name', 'get_default_basename', RemovedInDRF311Warning), ) @@ -88,7 +90,7 @@ class BaseRouter(six.with_metaclass(RenameRouterMethods)): def register(self, prefix, viewset, basename=None, base_name=None): if base_name is not None: msg = "The `base_name` argument is pending deprecation in favor of `basename`." - warnings.warn(msg, PendingDeprecationWarning, 2) + warnings.warn(msg, RemovedInDRF311Warning, 2) assert not (basename and base_name), ( "Do not provide both the `basename` and `base_name` arguments.") diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 9c6a899bf..13dd41ff3 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import pytest from django.test import TestCase -from rest_framework import status +from rest_framework import RemovedInDRF310Warning, status from rest_framework.authentication import BasicAuthentication from rest_framework.decorators import ( action, api_view, authentication_classes, detail_route, list_route, @@ -290,7 +290,7 @@ class ActionDecoratorTestCase(TestCase): raise NotImplementedError def test_detail_route_deprecation(self): - with pytest.warns(DeprecationWarning) as record: + with pytest.warns(RemovedInDRF310Warning) as record: @detail_route() def view(request): raise NotImplementedError @@ -303,7 +303,7 @@ class ActionDecoratorTestCase(TestCase): ) def test_list_route_deprecation(self): - with pytest.warns(DeprecationWarning) as record: + with pytest.warns(RemovedInDRF310Warning) as record: @list_route() def view(request): raise NotImplementedError @@ -317,7 +317,7 @@ class ActionDecoratorTestCase(TestCase): def test_route_url_name_from_path(self): # pre-3.8 behavior was to base the `url_name` off of the `url_path` - with pytest.warns(DeprecationWarning): + with pytest.warns(RemovedInDRF310Warning): @list_route(url_path='foo_bar') def view(request): raise NotImplementedError diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 807006858..2fabdfa05 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -12,8 +12,8 @@ from django.test import TestCase from django.urls import ResolverMatch from rest_framework import ( - HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers, - status, views + HTTP_HEADER_ENCODING, RemovedInDRF310Warning, authentication, generics, + permissions, serializers, status, views ) from rest_framework.compat import PY36, is_guardian_installed, mock from rest_framework.filters import DjangoObjectPermissionsFilter @@ -427,7 +427,7 @@ class ObjectPermissionsIntegrationTests(TestCase): message = ("`DjangoObjectPermissionsFilter` has been deprecated and moved " "to the 3rd-party django-rest-framework-guardian package.") self.assertEqual(len(w), 1) - self.assertIs(w[-1].category, DeprecationWarning) + self.assertIs(w[-1].category, RemovedInDRF310Warning) self.assertEqual(str(w[-1].message), message) def test_can_read_list_permissions(self): diff --git a/tests/test_routers.py b/tests/test_routers.py index c74055347..a3a731f93 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -10,7 +10,9 @@ from django.db import models from django.test import TestCase, override_settings from django.urls import resolve, reverse -from rest_framework import permissions, serializers, viewsets +from rest_framework import ( + RemovedInDRF311Warning, permissions, serializers, viewsets +) from rest_framework.compat import get_regex_pattern from rest_framework.decorators import action from rest_framework.response import Response @@ -508,7 +510,7 @@ class TestBaseNameRename(TestCase): def test_base_name_argument_deprecation(self): router = SimpleRouter() - with pytest.warns(PendingDeprecationWarning) as w: + with pytest.warns(RemovedInDRF311Warning) as w: warnings.simplefilter('always') router.register('mock', MockViewSet, base_name='mock') @@ -535,7 +537,7 @@ class TestBaseNameRename(TestCase): msg = "`CustomRouter.get_default_base_name` method should be renamed `get_default_basename`." # Class definition should raise a warning - with pytest.warns(PendingDeprecationWarning) as w: + with pytest.warns(RemovedInDRF311Warning) as w: warnings.simplefilter('always') class CustomRouter(SimpleRouter): From 7eac86688a73503c6668568ad564f6b2539049f0 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 3 Mar 2019 10:39:08 -0800 Subject: [PATCH 95/97] Remove executable bit from static assets (#6484) These files are simply static assets and do not require an executable bit. They are never intended to be executed as standalone scripts. --- docs_theme/css/bootstrap-responsive.css | 0 docs_theme/css/bootstrap.css | 0 docs_theme/js/bootstrap-2.1.1-min.js | 0 .../docs/css/jquery.json-view.min.css | 0 .../rest_framework/docs/js/jquery.json-view.min.js | 0 .../rest_framework/fonts/fontawesome-webfont.eot | Bin .../rest_framework/fonts/fontawesome-webfont.svg | 0 .../rest_framework/fonts/fontawesome-webfont.ttf | Bin .../rest_framework/fonts/fontawesome-webfont.woff | Bin 9 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 docs_theme/css/bootstrap-responsive.css mode change 100755 => 100644 docs_theme/css/bootstrap.css mode change 100755 => 100644 docs_theme/js/bootstrap-2.1.1-min.js mode change 100755 => 100644 rest_framework/static/rest_framework/docs/css/jquery.json-view.min.css mode change 100755 => 100644 rest_framework/static/rest_framework/docs/js/jquery.json-view.min.js mode change 100755 => 100644 rest_framework/static/rest_framework/fonts/fontawesome-webfont.eot mode change 100755 => 100644 rest_framework/static/rest_framework/fonts/fontawesome-webfont.svg mode change 100755 => 100644 rest_framework/static/rest_framework/fonts/fontawesome-webfont.ttf mode change 100755 => 100644 rest_framework/static/rest_framework/fonts/fontawesome-webfont.woff diff --git a/docs_theme/css/bootstrap-responsive.css b/docs_theme/css/bootstrap-responsive.css old mode 100755 new mode 100644 diff --git a/docs_theme/css/bootstrap.css b/docs_theme/css/bootstrap.css old mode 100755 new mode 100644 diff --git a/docs_theme/js/bootstrap-2.1.1-min.js b/docs_theme/js/bootstrap-2.1.1-min.js old mode 100755 new mode 100644 diff --git a/rest_framework/static/rest_framework/docs/css/jquery.json-view.min.css b/rest_framework/static/rest_framework/docs/css/jquery.json-view.min.css old mode 100755 new mode 100644 diff --git a/rest_framework/static/rest_framework/docs/js/jquery.json-view.min.js b/rest_framework/static/rest_framework/docs/js/jquery.json-view.min.js old mode 100755 new mode 100644 diff --git a/rest_framework/static/rest_framework/fonts/fontawesome-webfont.eot b/rest_framework/static/rest_framework/fonts/fontawesome-webfont.eot old mode 100755 new mode 100644 diff --git a/rest_framework/static/rest_framework/fonts/fontawesome-webfont.svg b/rest_framework/static/rest_framework/fonts/fontawesome-webfont.svg old mode 100755 new mode 100644 diff --git a/rest_framework/static/rest_framework/fonts/fontawesome-webfont.ttf b/rest_framework/static/rest_framework/fonts/fontawesome-webfont.ttf old mode 100755 new mode 100644 diff --git a/rest_framework/static/rest_framework/fonts/fontawesome-webfont.woff b/rest_framework/static/rest_framework/fonts/fontawesome-webfont.woff old mode 100755 new mode 100644 From ac4c78967ad96ccf6c46e6464b6e9298cfbb7734 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 3 Mar 2019 15:33:16 +0100 Subject: [PATCH 96/97] Update version for v3.9.2 release. --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index eacc8dca0..55c06982d 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.9.1' +__version__ = '3.9.2' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2019 Encode OSS Ltd' From 0ab527a3df39e344904f1e6543d76b6ce9b45c61 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 3 Mar 2019 15:46:57 +0100 Subject: [PATCH 97/97] Updated release notes for v3.9.2 --- docs/community/release-notes.md | 39 +++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 62ad95723..288bf6d58 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -40,9 +40,25 @@ You can determine your currently installed version using `pip show`: ## 3.9.x series -### 3.9.2 - IN DEVELOPMENT +### 3.9.2 -... +**Date**: [3rd March 2019][3.9.1-milestone] + +* Routers: invalidate `_urls` cache on `register()` [#6407][gh6407] +* Deferred schema renderer creation to avoid requiring pyyaml. [#6416][gh6416] +* Added 'request_forms' block to base.html [#6340][gh6340] +* Fixed SchemaView to reset renderer on exception. [#6429][gh6429] +* Update Django Guardian dependency. [#6430][gh6430] +* Ensured support for Django 2.2 [#6422][gh6422] & [#6455][gh6455] +* Made templates compatible with session-based CSRF. [#6207][gh6207] +* Adjusted field `validators` to accept non-list iterables. [#6282][gh6282] +* Added SearchFilter.get_search_fields() hook. [#6279][gh6279] +* Fix DeprecationWarning when accessing collections.abc classes via collections [#6268][gh6268] +* Allowed Q objects in limit_choices_to introspection. [#6472][gh6472] +* Added lazy evaluation to composed permissions. [#6463][gh6463] +* Add negation ~ operator to permissions composition [#6361][gh6361] +* Avoided calling distinct on annotated fields in SearchFilter. [#6240][gh6240] +* Introduced `RemovedInDRF…Warning` classes to simplify deprecations. [#6480][gh6480] ### 3.9.1 @@ -1149,6 +1165,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.8.2-milestone]: https://github.com/encode/django-rest-framework/milestone/68?closed=1 [3.9.0-milestone]: https://github.com/encode/django-rest-framework/milestone/66?closed=1 [3.9.1-milestone]: https://github.com/encode/django-rest-framework/milestone/70?closed=1 +[3.9.1-milestone]: https://github.com/encode/django-rest-framework/milestone/71?closed=1 [gh2013]: https://github.com/encode/django-rest-framework/issues/2013 @@ -2071,3 +2088,21 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh6330]: https://github.com/encode/django-rest-framework/issues/6330 [gh6299]: https://github.com/encode/django-rest-framework/issues/6299 [gh6371]: https://github.com/encode/django-rest-framework/issues/6371 + + +[gh6480]: https://github.com/encode/django-rest-framework/issues/6480 +[gh6240]: https://github.com/encode/django-rest-framework/issues/6240 +[gh6361]: https://github.com/encode/django-rest-framework/issues/6361 +[gh6463]: https://github.com/encode/django-rest-framework/issues/6463 +[gh6472]: https://github.com/encode/django-rest-framework/issues/6472 +[gh6268]: https://github.com/encode/django-rest-framework/issues/6268 +[gh6279]: https://github.com/encode/django-rest-framework/issues/6279 +[gh6282]: https://github.com/encode/django-rest-framework/issues/6282 +[gh6207]: https://github.com/encode/django-rest-framework/issues/6207 +[gh6455]: https://github.com/encode/django-rest-framework/issues/6455 +[gh6422]: https://github.com/encode/django-rest-framework/issues/6422 +[gh6430]: https://github.com/encode/django-rest-framework/issues/6430 +[gh6429]: https://github.com/encode/django-rest-framework/issues/6429 +[gh6340]: https://github.com/encode/django-rest-framework/issues/6340 +[gh6416]: https://github.com/encode/django-rest-framework/issues/6416 +[gh6407]: https://github.com/encode/django-rest-framework/issues/6407