From a73d3c309f0736f46dd46c99e15e1102af26dacb Mon Sep 17 00:00:00 2001 From: Hendrik <30193551+verhoek@users.noreply.github.com> Date: Mon, 18 Nov 2019 13:35:36 +0100 Subject: [PATCH 01/40] Elaborated on nested relationships (#7051) --- docs/api-guide/relations.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 14f197b21..ef6efec5e 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -245,7 +245,9 @@ This field is always read-only. # Nested relationships -Nested relationships can be expressed by using serializers as fields. +As opposed to previously discussed _references_ to another entity, the referred entity can instead also be embedded or _nested_ +in the representation of the object that refers to it. +Such nested relationships can be expressed by using serializers as fields. If the field is used to represent a to-many relationship, you should add the `many=True` flag to the serializer field. From adaf97a739dc2c29589b25052daac04d5d706c1b Mon Sep 17 00:00:00 2001 From: Thomas Loiret Date: Wed, 20 Nov 2019 14:09:49 +0100 Subject: [PATCH 02/40] Remove the old reference to `JSONResponse` --- docs/tutorial/2-requests-and-responses.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index e3d21e864..b6433695a 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -33,9 +33,7 @@ The wrappers also provide behaviour such as returning `405 Method Not Allowed` r ## Pulling it all together -Okay, let's go ahead and start using these new components to write a few views. - -We don't need our `JSONResponse` class in `views.py` any more, so go ahead and delete that. Once that's done we can start refactoring our views slightly. +Okay, let's go ahead and start using these new components to refactor our views slightly. from rest_framework import status from rest_framework.decorators import api_view From fe840a34ff79f6fd996219ff8325b5f07cc3f62b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 21 Nov 2019 11:38:40 +0000 Subject: [PATCH 03/40] Escape hyperlink URLs on lookup (#7059) * Escape hyperlink URLs on lookup * Rename duplicate test --- rest_framework/relations.py | 2 +- tests/test_relations.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 9bde79b19..af4dd1804 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -344,7 +344,7 @@ class HyperlinkedRelatedField(RelatedField): if data.startswith(prefix): data = '/' + data[len(prefix):] - data = uri_to_iri(data) + data = uri_to_iri(parse.unquote(data)) try: match = resolve(data) diff --git a/tests/test_relations.py b/tests/test_relations.py index c89293415..86ed623ae 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -153,6 +153,7 @@ class TestHyperlinkedRelatedField(APISimpleTestCase): self.queryset = MockQueryset([ MockObject(pk=1, name='foobar'), MockObject(pk=2, name='bazABCqux'), + MockObject(pk=2, name='bazABC qux'), ]) self.field = serializers.HyperlinkedRelatedField( view_name='example', @@ -191,6 +192,10 @@ class TestHyperlinkedRelatedField(APISimpleTestCase): instance = self.field.to_internal_value('http://example.org/example/baz%41%42%43qux/') assert instance is self.queryset.items[1] + def test_hyperlinked_related_lookup_url_space_encoded_exists(self): + instance = self.field.to_internal_value('http://example.org/example/bazABC%20qux/') + assert instance is self.queryset.items[2] + def test_hyperlinked_related_lookup_does_not_exist(self): with pytest.raises(serializers.ValidationError) as excinfo: self.field.to_internal_value('http://example.org/example/doesnotexist/') From 7fbbfe2c60c314e79bf2179c76e4357f48045a2b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 21 Nov 2019 11:55:53 +0000 Subject: [PATCH 04/40] Django 3 compat (#7058) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First pass at Django 3.0 compat * Drop Guardian for 1.11 tests, since we're installing an incompatible version * Fix ROOT_URLCONF override in test case * Fix typo Co-Authored-By: Rémy HUBSCHER * Linting --- .travis.yml | 3 +++ requirements/requirements-optionals.txt | 2 +- tests/conftest.py | 29 ++++++++++++++----------- tests/test_relations.py | 7 ++++-- tox.ini | 3 +++ 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/.travis.yml b/.travis.yml index f89e77531..7266df2d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,13 +14,16 @@ matrix: - { 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=3.0 } - { 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=3.0 } - { python: "3.7", env: DJANGO=master } + - { python: "3.8", env: DJANGO=3.0 } - { python: "3.8", env: DJANGO=master } - { python: "3.8", env: TOXENV=base } diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index a33248d10..14957a531 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -2,7 +2,7 @@ psycopg2-binary>=2.8.2, <2.9 markdown==3.1.1 pygments==2.4.2 -django-guardian==1.5.0 +django-guardian==2.1.0 django-filter>=2.2.0, <2.3 coreapi==2.3.1 coreschema==0.0.4 diff --git a/tests/conftest.py b/tests/conftest.py index ac29e4a42..d28edeb8a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -67,19 +67,22 @@ def pytest_configure(config): ) # guardian is optional - try: - import guardian # NOQA - except ImportError: - pass - else: - settings.ANONYMOUS_USER_ID = -1 - settings.AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'guardian.backends.ObjectPermissionBackend', - ) - settings.INSTALLED_APPS += ( - 'guardian', - ) + # Note that for the test cases we're installing a version of django-guardian + # that's only compatible with Django 2.0+. + if django.VERSION >= (2, 0, 0): + try: + import guardian # NOQA + except ImportError: + pass + else: + settings.ANONYMOUS_USER_ID = -1 + settings.AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'guardian.backends.ObjectPermissionBackend', + ) + settings.INSTALLED_APPS += ( + 'guardian', + ) if config.getoption('--no-pkgroot'): sys.path.pop(0) diff --git a/tests/test_relations.py b/tests/test_relations.py index 86ed623ae..9f05e3b31 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -145,9 +145,12 @@ class TestProxiedPrimaryKeyRelatedField(APISimpleTestCase): assert representation == self.instance.pk.int -@override_settings(ROOT_URLCONF=[ +urlpatterns = [ url(r'^example/(?P.+)/$', lambda: None, name='example'), -]) +] + + +@override_settings(ROOT_URLCONF='tests.test_relations') class TestHyperlinkedRelatedField(APISimpleTestCase): def setUp(self): self.queryset = MockQueryset([ diff --git a/tox.ini b/tox.ini index 587c469b1..1153eae29 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = {py35,py36,py37}-django20, {py35,py36,py37}-django21 {py35,py36,py37}-django22 + {py36,py37,py38}-django30, {py36,py37,py38}-djangomaster, base,dist,lint,docs, @@ -13,6 +14,7 @@ DJANGO = 2.0: django20 2.1: django21 2.2: django22 + 3.0: django30 master: djangomaster [testenv] @@ -26,6 +28,7 @@ deps = django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 django22: Django>=2.2,<3.0 + django30: Django==3.0rc1 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 8001087e9e335140b8063a23916d9c05b615acd4 Mon Sep 17 00:00:00 2001 From: Oskar Persson Date: Thu, 21 Nov 2019 15:59:50 +0100 Subject: [PATCH 05/40] Fix typo in unsupported version error message (#7060) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c9d6443d5..24749992b 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ This will install the latest version of Django REST Framework which works on your version of Python. If you can't upgrade your pip (or Python), request an older version of Django REST Framework: - $ python -m pip install "django<3.10" + $ python -m pip install "djangorestframework<3.10" """.format(*(REQUIRED_PYTHON + CURRENT_PYTHON))) sys.exit(1) From 9325c3f6544c76173891786529ed5f4bc2be5876 Mon Sep 17 00:00:00 2001 From: Asif Saif Uddin Date: Tue, 3 Dec 2019 17:13:44 +0600 Subject: [PATCH 06/40] dj 3.0 (#7070) --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 1153eae29..9b8069174 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,7 @@ deps = django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 django22: Django>=2.2,<3.0 - django30: Django==3.0rc1 + django30: Django>=3.0,<3.1 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 070cff5a0356f6ead6380062f186a2582e460601 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2019 11:16:27 +0000 Subject: [PATCH 07/40] Drop `set_context()` (#7062) * Do not persist the context in validators Fixes encode/django-rest-framework#5760 * Drop set_context() in favour of 'requires_context = True' --- docs/api-guide/fields.md | 14 +++- docs/api-guide/validators.md | 16 +++-- rest_framework/fields.py | 61 +++++++++++++---- rest_framework/validators.py | 126 ++++++++++++++++------------------- tests/test_validators.py | 7 +- 5 files changed, 131 insertions(+), 93 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 29cb5aec9..e964458f9 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -50,7 +50,19 @@ If set, this gives the default value that will be used for the field if no input The `default` is not applied during partial update operations. In the partial update case only fields that are provided in the incoming data will have a validated value returned. -May be set to a function or other callable, in which case the value will be evaluated each time it is used. When called, it will receive no arguments. If the callable has a `set_context` method, that will be called each time before getting the value with the field instance as only argument. This works the same way as for [validators](validators.md#using-set_context). +May be set to a function or other callable, in which case the value will be evaluated each time it is used. When called, it will receive no arguments. If the callable has a `requires_context = True` attribute, then the serializer field will be passed as an argument. + +For example: + + class CurrentUserDefault: + """ + May be applied as a `default=...` value on a serializer field. + Returns the current user. + """ + requires_context = True + + def __call__(self, serializer_field): + return serializer_field.context['request'].user When serializing the instance, default will be used if the object attribute or dictionary key is not present in the instance. diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index 49685838a..009cd2468 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -291,13 +291,17 @@ To write a class-based validator, use the `__call__` method. Class-based validat message = 'This field must be a multiple of %d.' % self.base raise serializers.ValidationError(message) -#### Using `set_context()` +#### Accessing the context -In some advanced cases you might want a validator to be passed the serializer field it is being used with as additional context. You can do so by declaring a `set_context` method on a class-based validator. +In some advanced cases you might want a validator to be passed the serializer +field it is being used with as additional context. You can do so by setting +a `requires_context = True` attribute on the validator. The `__call__` method +will then be called with the `serializer_field` +or `serializer` as an additional argument. - def set_context(self, serializer_field): - # Determine if this is an update or a create operation. - # In `__call__` we can then use that information to modify the validation behavior. - self.is_update = serializer_field.parent.instance is not None + requires_context = True + + def __call__(self, value, serializer_field): + ... [cite]: https://docs.djangoproject.com/en/stable/ref/validators/ diff --git a/rest_framework/fields.py b/rest_framework/fields.py index ea8f47b2d..9507914e8 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -5,6 +5,7 @@ import functools import inspect import re import uuid +import warnings from collections import OrderedDict from collections.abc import Mapping @@ -249,19 +250,30 @@ class CreateOnlyDefault: for create operations, but that do not return any value for update operations. """ + requires_context = True + def __init__(self, default): self.default = default - def set_context(self, serializer_field): - self.is_update = serializer_field.parent.instance is not None - if callable(self.default) and hasattr(self.default, 'set_context') and not self.is_update: - self.default.set_context(serializer_field) - - def __call__(self): - if self.is_update: + def __call__(self, serializer_field): + is_update = serializer_field.parent.instance is not None + if is_update: raise SkipField() if callable(self.default): - return self.default() + if hasattr(self.default, 'set_context'): + warnings.warn( + "Method `set_context` on defaults is deprecated and will " + "no longer be called starting with 3.12. Instead set " + "`requires_context = True` on the class, and accept the " + "context as an additional argument.", + DeprecationWarning, stacklevel=2 + ) + self.default.set_context(self) + + if getattr(self.default, 'requires_context', False): + return self.default(serializer_field) + else: + return self.default() return self.default def __repr__(self): @@ -269,11 +281,10 @@ class CreateOnlyDefault: class CurrentUserDefault: - def set_context(self, serializer_field): - self.user = serializer_field.context['request'].user + requires_context = True - def __call__(self): - return self.user + def __call__(self, serializer_field): + return serializer_field.context['request'].user def __repr__(self): return '%s()' % self.__class__.__name__ @@ -489,8 +500,20 @@ class Field: raise SkipField() if callable(self.default): if hasattr(self.default, 'set_context'): + warnings.warn( + "Method `set_context` on defaults is deprecated and will " + "no longer be called starting with 3.12. Instead set " + "`requires_context = True` on the class, and accept the " + "context as an additional argument.", + DeprecationWarning, stacklevel=2 + ) self.default.set_context(self) - return self.default() + + if getattr(self.default, 'requires_context', False): + return self.default(self) + else: + return self.default() + return self.default def validate_empty_values(self, data): @@ -551,10 +574,20 @@ class Field: errors = [] for validator in self.validators: if hasattr(validator, 'set_context'): + warnings.warn( + "Method `set_context` on validators is deprecated and will " + "no longer be called starting with 3.12. Instead set " + "`requires_context = True` on the class, and accept the " + "context as an additional argument.", + DeprecationWarning, stacklevel=2 + ) validator.set_context(self) try: - validator(value) + if getattr(validator, 'requires_context', False): + validator(value, self) + else: + validator(value) except ValidationError as exc: # If the validation error contains a mapping of fields to # errors then simply raise it immediately rather than diff --git a/rest_framework/validators.py b/rest_framework/validators.py index 1cbe31b5e..2907312a9 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -37,6 +37,7 @@ class UniqueValidator: Should be applied to an individual field on the serializer. """ message = _('This field must be unique.') + requires_context = True def __init__(self, queryset, message=None, lookup='exact'): self.queryset = queryset @@ -44,37 +45,32 @@ class UniqueValidator: self.message = message or self.message self.lookup = lookup - def set_context(self, serializer_field): - """ - This hook is called by the serializer instance, - prior to the validation call being made. - """ - # Determine the underlying model field name. This may not be the - # same as the serializer field name if `source=<>` is set. - self.field_name = serializer_field.source_attrs[-1] - # Determine the existing instance, if this is an update operation. - self.instance = getattr(serializer_field.parent, 'instance', None) - - def filter_queryset(self, value, queryset): + def filter_queryset(self, value, queryset, field_name): """ Filter the queryset to all instances matching the given attribute. """ - filter_kwargs = {'%s__%s' % (self.field_name, self.lookup): value} + filter_kwargs = {'%s__%s' % (field_name, self.lookup): value} return qs_filter(queryset, **filter_kwargs) - def exclude_current_instance(self, queryset): + def exclude_current_instance(self, queryset, instance): """ If an instance is being updated, then do not include that instance itself as a uniqueness conflict. """ - if self.instance is not None: - return queryset.exclude(pk=self.instance.pk) + if instance is not None: + return queryset.exclude(pk=instance.pk) return queryset - def __call__(self, value): + def __call__(self, value, serializer_field): + # Determine the underlying model field name. This may not be the + # same as the serializer field name if `source=<>` is set. + field_name = serializer_field.source_attrs[-1] + # Determine the existing instance, if this is an update operation. + instance = getattr(serializer_field.parent, 'instance', None) + queryset = self.queryset - queryset = self.filter_queryset(value, queryset) - queryset = self.exclude_current_instance(queryset) + queryset = self.filter_queryset(value, queryset, field_name) + queryset = self.exclude_current_instance(queryset, instance) if qs_exists(queryset): raise ValidationError(self.message, code='unique') @@ -93,6 +89,7 @@ class UniqueTogetherValidator: """ message = _('The fields {field_names} must make a unique set.') missing_message = _('This field is required.') + requires_context = True def __init__(self, queryset, fields, message=None): self.queryset = queryset @@ -100,20 +97,12 @@ class UniqueTogetherValidator: self.serializer_field = None self.message = message or self.message - def set_context(self, serializer): - """ - This hook is called by the serializer instance, - prior to the validation call being made. - """ - # Determine the existing instance, if this is an update operation. - self.instance = getattr(serializer, 'instance', None) - - def enforce_required_fields(self, attrs): + def enforce_required_fields(self, attrs, instance): """ The `UniqueTogetherValidator` always forces an implied 'required' state on the fields it applies to. """ - if self.instance is not None: + if instance is not None: return missing_items = { @@ -124,16 +113,16 @@ class UniqueTogetherValidator: if missing_items: raise ValidationError(missing_items, code='required') - def filter_queryset(self, attrs, queryset): + def filter_queryset(self, attrs, queryset, instance): """ Filter the queryset to all instances matching the given attributes. """ # If this is an update, then any unprovided field should # have it's value set based on the existing instance attribute. - if self.instance is not None: + if instance is not None: for field_name in self.fields: if field_name not in attrs: - attrs[field_name] = getattr(self.instance, field_name) + attrs[field_name] = getattr(instance, field_name) # Determine the filter keyword arguments and filter the queryset. filter_kwargs = { @@ -142,20 +131,23 @@ class UniqueTogetherValidator: } return qs_filter(queryset, **filter_kwargs) - def exclude_current_instance(self, attrs, queryset): + def exclude_current_instance(self, attrs, queryset, instance): """ If an instance is being updated, then do not include that instance itself as a uniqueness conflict. """ - if self.instance is not None: - return queryset.exclude(pk=self.instance.pk) + if instance is not None: + return queryset.exclude(pk=instance.pk) return queryset - def __call__(self, attrs): - self.enforce_required_fields(attrs) + def __call__(self, attrs, serializer): + # Determine the existing instance, if this is an update operation. + instance = getattr(serializer, 'instance', None) + + self.enforce_required_fields(attrs, instance) queryset = self.queryset - queryset = self.filter_queryset(attrs, queryset) - queryset = self.exclude_current_instance(attrs, queryset) + queryset = self.filter_queryset(attrs, queryset, instance) + queryset = self.exclude_current_instance(attrs, queryset, instance) # Ignore validation if any field is None checked_values = [ @@ -177,6 +169,7 @@ class UniqueTogetherValidator: class BaseUniqueForValidator: message = None missing_message = _('This field is required.') + requires_context = True def __init__(self, queryset, field, date_field, message=None): self.queryset = queryset @@ -184,18 +177,6 @@ class BaseUniqueForValidator: self.date_field = date_field self.message = message or self.message - def set_context(self, serializer): - """ - This hook is called by the serializer instance, - prior to the validation call being made. - """ - # Determine the underlying model field names. These may not be the - # same as the serializer field names if `source=<>` is set. - self.field_name = serializer.fields[self.field].source_attrs[-1] - self.date_field_name = serializer.fields[self.date_field].source_attrs[-1] - # Determine the existing instance, if this is an update operation. - self.instance = getattr(serializer, 'instance', None) - def enforce_required_fields(self, attrs): """ The `UniqueForValidator` classes always force an implied @@ -209,23 +190,30 @@ class BaseUniqueForValidator: if missing_items: raise ValidationError(missing_items, code='required') - def filter_queryset(self, attrs, queryset): + def filter_queryset(self, attrs, queryset, field_name, date_field_name): raise NotImplementedError('`filter_queryset` must be implemented.') - def exclude_current_instance(self, attrs, queryset): + def exclude_current_instance(self, attrs, queryset, instance): """ If an instance is being updated, then do not include that instance itself as a uniqueness conflict. """ - if self.instance is not None: - return queryset.exclude(pk=self.instance.pk) + if instance is not None: + return queryset.exclude(pk=instance.pk) return queryset - def __call__(self, attrs): + def __call__(self, attrs, serializer): + # Determine the underlying model field names. These may not be the + # same as the serializer field names if `source=<>` is set. + field_name = serializer.fields[self.field].source_attrs[-1] + date_field_name = serializer.fields[self.date_field].source_attrs[-1] + # Determine the existing instance, if this is an update operation. + instance = getattr(serializer, 'instance', None) + self.enforce_required_fields(attrs) queryset = self.queryset - queryset = self.filter_queryset(attrs, queryset) - queryset = self.exclude_current_instance(attrs, queryset) + queryset = self.filter_queryset(attrs, queryset, field_name, date_field_name) + queryset = self.exclude_current_instance(attrs, queryset, instance) if qs_exists(queryset): message = self.message.format(date_field=self.date_field) raise ValidationError({ @@ -244,39 +232,39 @@ class BaseUniqueForValidator: class UniqueForDateValidator(BaseUniqueForValidator): message = _('This field must be unique for the "{date_field}" date.') - def filter_queryset(self, attrs, queryset): + def filter_queryset(self, attrs, queryset, field_name, date_field_name): value = attrs[self.field] date = attrs[self.date_field] filter_kwargs = {} - filter_kwargs[self.field_name] = value - filter_kwargs['%s__day' % self.date_field_name] = date.day - filter_kwargs['%s__month' % self.date_field_name] = date.month - filter_kwargs['%s__year' % self.date_field_name] = date.year + filter_kwargs[field_name] = value + filter_kwargs['%s__day' % date_field_name] = date.day + filter_kwargs['%s__month' % date_field_name] = date.month + filter_kwargs['%s__year' % date_field_name] = date.year return qs_filter(queryset, **filter_kwargs) class UniqueForMonthValidator(BaseUniqueForValidator): message = _('This field must be unique for the "{date_field}" month.') - def filter_queryset(self, attrs, queryset): + def filter_queryset(self, attrs, queryset, field_name, date_field_name): value = attrs[self.field] date = attrs[self.date_field] filter_kwargs = {} - filter_kwargs[self.field_name] = value - filter_kwargs['%s__month' % self.date_field_name] = date.month + filter_kwargs[field_name] = value + filter_kwargs['%s__month' % date_field_name] = date.month return qs_filter(queryset, **filter_kwargs) class UniqueForYearValidator(BaseUniqueForValidator): message = _('This field must be unique for the "{date_field}" year.') - def filter_queryset(self, attrs, queryset): + def filter_queryset(self, attrs, queryset, field_name, date_field_name): value = attrs[self.field] date = attrs[self.date_field] filter_kwargs = {} - filter_kwargs[self.field_name] = value - filter_kwargs['%s__year' % self.date_field_name] = date.year + filter_kwargs[field_name] = value + filter_kwargs['%s__year' % date_field_name] = date.year return qs_filter(queryset, **filter_kwargs) diff --git a/tests/test_validators.py b/tests/test_validators.py index fe31ba235..bb29a4305 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -361,8 +361,7 @@ class TestUniquenessTogetherValidation(TestCase): queryset = MockQueryset() validator = UniqueTogetherValidator(queryset, fields=('race_name', 'position')) - validator.instance = self.instance - validator.filter_queryset(attrs=data, queryset=queryset) + validator.filter_queryset(attrs=data, queryset=queryset, instance=self.instance) assert queryset.called_with == {'race_name': 'bar', 'position': 1} @@ -586,4 +585,6 @@ class ValidatorsTests(TestCase): validator = BaseUniqueForValidator(queryset=object(), field='foo', date_field='bar') with pytest.raises(NotImplementedError): - validator.filter_queryset(attrs=None, queryset=None) + validator.filter_queryset( + attrs=None, queryset=None, field_name='', date_field_name='' + ) From dff9759555eefef67c552f175d04bb7d8381e919 Mon Sep 17 00:00:00 2001 From: Kye Russell Date: Wed, 4 Dec 2019 17:29:01 +0800 Subject: [PATCH 08/40] Removed Eric S. Raymond quote from the release notes (#7073) --- docs/community/release-notes.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 283dbae67..4be05d56b 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -1,9 +1,5 @@ # Release Notes -> Release Early, Release Often -> -> — Eric S. Raymond, [The Cathedral and the Bazaar][cite]. - ## Versioning Minor version numbers (0.0.x) are used for changes that are API compatible. You should be able to upgrade between minor point releases without any other code changes. From 4d9f9eb192c5c1ffe4fa9210b90b9adbb00c3fdd Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Wed, 4 Dec 2019 21:24:49 +0100 Subject: [PATCH 09/40] Changed default widget for TextField with choices to select (#6892) --- rest_framework/utils/field_mapping.py | 3 ++- tests/test_model_serializer.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index b90c3eead..a25880d0f 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -91,7 +91,8 @@ def get_field_kwargs(field_name, model_field): if isinstance(model_field, models.SlugField): kwargs['allow_unicode'] = model_field.allow_unicode - if isinstance(model_field, models.TextField) or (postgres_fields and isinstance(model_field, postgres_fields.JSONField)): + if isinstance(model_field, models.TextField) and not model_field.choices or \ + (postgres_fields and isinstance(model_field, postgres_fields.JSONField)): kwargs['style'] = {'base_template': 'textarea.html'} if isinstance(model_field, models.AutoField) or not model_field.editable: diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 21ec82347..fbb562792 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -89,6 +89,7 @@ class FieldOptionsModel(models.Model): default_field = models.IntegerField(default=0) descriptive_field = models.IntegerField(help_text='Some help text', verbose_name='A label') choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES) + text_choices_field = models.TextField(choices=COLOR_CHOICES) class ChoicesModel(models.Model): @@ -211,6 +212,7 @@ class TestRegularFieldMappings(TestCase): default_field = IntegerField(required=False) descriptive_field = IntegerField(help_text='Some help text', label='A label') choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))) + text_choices_field = ChoiceField(choices=(('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))) """) self.assertEqual(repr(TestSerializer()), expected) From 95d4843abeecea96754a147f4f2cca33e620ad09 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 4 Dec 2019 14:14:43 -0800 Subject: [PATCH 10/40] Fix Django 3.0 deprecations (#7074) --- rest_framework/fields.py | 4 ++-- rest_framework/relations.py | 4 ++-- rest_framework/schemas/inspectors.py | 4 ++-- rest_framework/schemas/utils.py | 2 +- rest_framework/views.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 9507914e8..8c80d6bd5 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -23,7 +23,7 @@ from django.utils.dateparse import ( parse_date, parse_datetime, parse_duration, parse_time ) from django.utils.duration import duration_string -from django.utils.encoding import is_protected_type, smart_text +from django.utils.encoding import is_protected_type, smart_str from django.utils.formats import localize_input, sanitize_separators from django.utils.ipv6 import clean_ipv6_address from django.utils.timezone import utc @@ -1082,7 +1082,7 @@ class DecimalField(Field): instance. """ - data = smart_text(data).strip() + data = smart_str(data).strip() if self.localize: data = sanitize_separators(data) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index af4dd1804..3a2a8fb4b 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -6,7 +6,7 @@ from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.db.models import Manager from django.db.models.query import QuerySet from django.urls import NoReverseMatch, Resolver404, get_script_prefix, resolve -from django.utils.encoding import smart_text, uri_to_iri +from django.utils.encoding import smart_str, uri_to_iri from django.utils.translation import gettext_lazy as _ from rest_framework.fields import ( @@ -452,7 +452,7 @@ class SlugRelatedField(RelatedField): try: return self.get_queryset().get(**{self.slug_field: data}) except ObjectDoesNotExist: - self.fail('does_not_exist', slug_name=self.slug_field, value=smart_text(data)) + self.fail('does_not_exist', slug_name=self.slug_field, value=smart_str(data)) except (TypeError, ValueError): self.fail('invalid') diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py index 3b7e7f963..027472db1 100644 --- a/rest_framework/schemas/inspectors.py +++ b/rest_framework/schemas/inspectors.py @@ -6,7 +6,7 @@ See schemas.__init__.py for package overview. import re from weakref import WeakKeyDictionary -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str from rest_framework.settings import api_settings from rest_framework.utils import formatting @@ -82,7 +82,7 @@ class ViewInspector: method_docstring = getattr(view, method_name, None).__doc__ if method_docstring: # An explicit docstring on the method or action. - return self._get_description_section(view, method.lower(), formatting.dedent(smart_text(method_docstring))) + return self._get_description_section(view, method.lower(), formatting.dedent(smart_str(method_docstring))) else: return self._get_description_section(view, getattr(view, 'action', method.lower()), view.get_view_description()) diff --git a/rest_framework/schemas/utils.py b/rest_framework/schemas/utils.py index 6724eb428..60ed69829 100644 --- a/rest_framework/schemas/utils.py +++ b/rest_framework/schemas/utils.py @@ -4,7 +4,7 @@ utils.py # Shared helper functions See schemas.__init__.py for package overview. """ from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from rest_framework.mixins import RetrieveModelMixin diff --git a/rest_framework/views.py b/rest_framework/views.py index bec10560a..69db053d6 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -7,7 +7,7 @@ from django.db import connection, models, transaction from django.http import Http404 from django.http.response import HttpResponseBase from django.utils.cache import cc_delim_re, patch_vary_headers -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str from django.views.decorators.csrf import csrf_exempt from django.views.generic import View @@ -56,7 +56,7 @@ def get_view_description(view, html=False): if description is None: description = view.__class__.__doc__ or '' - description = formatting.dedent(smart_text(description)) + description = formatting.dedent(smart_str(description)) if html: return formatting.markup_description(description) return description From 90eaf51839e2bfc3ca897d1f87028ca3303aa097 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 4 Dec 2019 16:18:38 -0800 Subject: [PATCH 11/40] Update framework deprecation warnings (#7075) - Bump version numbers for deprecation warnings - Drop deprecated features --- rest_framework/__init__.py | 4 +-- rest_framework/routers.py | 24 ++----------- tests/test_routers.py | 73 +------------------------------------- 3 files changed, 6 insertions(+), 95 deletions(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 2b96e7336..fceee6817 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -25,9 +25,9 @@ ISO_8601 = 'iso-8601' default_app_config = 'rest_framework.apps.RestFrameworkConfig' -class RemovedInDRF311Warning(DeprecationWarning): +class RemovedInDRF312Warning(DeprecationWarning): pass -class RemovedInDRF312Warning(PendingDeprecationWarning): +class RemovedInDRF313Warning(PendingDeprecationWarning): pass diff --git a/rest_framework/routers.py b/rest_framework/routers.py index d8e19a2d7..657ad67bc 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -14,15 +14,13 @@ For example, you might have a `urls.py` that looks something like this: urlpatterns = router.urls """ import itertools -import warnings from collections import OrderedDict, namedtuple from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured from django.urls import NoReverseMatch -from django.utils.deprecation import RenameMethodsBase -from rest_framework import RemovedInDRF311Warning, views +from rest_framework import views from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.schemas import SchemaGenerator @@ -48,27 +46,11 @@ def flatten(list_of_lists): return itertools.chain(*list_of_lists) -class RenameRouterMethods(RenameMethodsBase): - renamed_methods = ( - ('get_default_base_name', 'get_default_basename', RemovedInDRF311Warning), - ) - - -class BaseRouter(metaclass=RenameRouterMethods): +class BaseRouter: def __init__(self): self.registry = [] - 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, RemovedInDRF311Warning, 2) - - assert not (basename and base_name), ( - "Do not provide both the `basename` and `base_name` arguments.") - - if basename is None: - basename = base_name - + def register(self, prefix, viewset, basename=None): if basename is None: basename = self.get_default_basename(viewset) self.registry.append((prefix, viewset, basename)) diff --git a/tests/test_routers.py b/tests/test_routers.py index 0f428e2a5..ff927ff33 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -1,4 +1,3 @@ -import warnings from collections import namedtuple import pytest @@ -8,9 +7,7 @@ from django.db import models from django.test import TestCase, override_settings from django.urls import resolve, reverse -from rest_framework import ( - RemovedInDRF311Warning, permissions, serializers, viewsets -) +from rest_framework import permissions, serializers, viewsets from rest_framework.compat import get_regex_pattern from rest_framework.decorators import action from rest_framework.response import Response @@ -488,71 +485,3 @@ class TestViewInitkwargs(URLPatternsTestCase, TestCase): initkwargs = match.func.initkwargs assert initkwargs['basename'] == 'routertestmodel' - - -class TestBaseNameRename(TestCase): - - def test_base_name_and_basename_assertion(self): - router = SimpleRouter() - - msg = "Do not provide both the `basename` and `base_name` arguments." - with warnings.catch_warnings(record=True) as w, \ - self.assertRaisesMessage(AssertionError, msg): - warnings.simplefilter('always') - router.register('mock', MockViewSet, 'mock', base_name='mock') - - msg = "The `base_name` argument is pending deprecation in favor of `basename`." - assert len(w) == 1 - assert str(w[0].message) == msg - - def test_base_name_argument_deprecation(self): - router = SimpleRouter() - - with pytest.warns(RemovedInDRF311Warning) as w: - warnings.simplefilter('always') - router.register('mock', MockViewSet, base_name='mock') - - msg = "The `base_name` argument is pending deprecation in favor of `basename`." - assert len(w) == 1 - assert str(w[0].message) == msg - assert router.registry == [ - ('mock', MockViewSet, 'mock'), - ] - - def test_basename_argument_no_warnings(self): - router = SimpleRouter() - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - router.register('mock', MockViewSet, basename='mock') - - assert len(w) == 0 - assert router.registry == [ - ('mock', MockViewSet, 'mock'), - ] - - def test_get_default_base_name_deprecation(self): - msg = "`CustomRouter.get_default_base_name` method should be renamed `get_default_basename`." - - # Class definition should raise a warning - with pytest.warns(RemovedInDRF311Warning) as w: - warnings.simplefilter('always') - - class CustomRouter(SimpleRouter): - def get_default_base_name(self, viewset): - return 'foo' - - assert len(w) == 1 - assert str(w[0].message) == msg - - # Deprecated method implementation should still be called - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - - router = CustomRouter() - router.register('mock', MockViewSet) - - assert len(w) == 0 - assert router.registry == [ - ('mock', MockViewSet, 'foo'), - ] From ebcd93163a5d0663d16a16d4691df1bbe965d42f Mon Sep 17 00:00:00 2001 From: Roy Segall Date: Tue, 10 Dec 2019 11:18:35 +0200 Subject: [PATCH 12/40] Adding I'm a teapot error code (#7081) --- rest_framework/status.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/status.py b/rest_framework/status.py index 06d090733..2561d7689 100644 --- a/rest_framework/status.py +++ b/rest_framework/status.py @@ -66,6 +66,7 @@ HTTP_414_REQUEST_URI_TOO_LONG = 414 HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415 HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE = 416 HTTP_417_EXPECTATION_FAILED = 417 +HTTP_418_IM_A_TEAPOT = 418 HTTP_422_UNPROCESSABLE_ENTITY = 422 HTTP_423_LOCKED = 423 HTTP_424_FAILED_DEPENDENCY = 424 From de9f1d56c45557638725bc18733387beac27ad1e Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 11 Dec 2019 00:44:08 -0800 Subject: [PATCH 13/40] Followup to set_context removal (#7076) * Raise framework-specific deprecation warnings - Use `RemovedInDRF313Warning` instead of DeprecationWarning - Update to follow deprecation policy * Pass serializer instead of model to validator The `UniqueTogetherValidator` may need to access attributes on the serializer instead of just the model instance. For example, this is useful for handling field sources. * Fix framework deprecation warning in test * Remove outdated validator attribute --- rest_framework/fields.py | 14 +++++++------- rest_framework/validators.py | 25 +++++++++---------------- tests/test_fields.py | 7 +++---- tests/test_validators.py | 7 ++++++- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 8c80d6bd5..11a291568 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -30,7 +30,7 @@ from django.utils.timezone import utc from django.utils.translation import gettext_lazy as _ from pytz.exceptions import InvalidTimeError -from rest_framework import ISO_8601 +from rest_framework import ISO_8601, RemovedInDRF313Warning from rest_framework.compat import ProhibitNullCharactersValidator from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.settings import api_settings @@ -263,10 +263,10 @@ class CreateOnlyDefault: if hasattr(self.default, 'set_context'): warnings.warn( "Method `set_context` on defaults is deprecated and will " - "no longer be called starting with 3.12. Instead set " + "no longer be called starting with 3.13. Instead set " "`requires_context = True` on the class, and accept the " "context as an additional argument.", - DeprecationWarning, stacklevel=2 + RemovedInDRF313Warning, stacklevel=2 ) self.default.set_context(self) @@ -502,10 +502,10 @@ class Field: if hasattr(self.default, 'set_context'): warnings.warn( "Method `set_context` on defaults is deprecated and will " - "no longer be called starting with 3.12. Instead set " + "no longer be called starting with 3.13. Instead set " "`requires_context = True` on the class, and accept the " "context as an additional argument.", - DeprecationWarning, stacklevel=2 + RemovedInDRF313Warning, stacklevel=2 ) self.default.set_context(self) @@ -576,10 +576,10 @@ class Field: if hasattr(validator, 'set_context'): warnings.warn( "Method `set_context` on validators is deprecated and will " - "no longer be called starting with 3.12. Instead set " + "no longer be called starting with 3.13. Instead set " "`requires_context = True` on the class, and accept the " "context as an additional argument.", - DeprecationWarning, stacklevel=2 + RemovedInDRF313Warning, stacklevel=2 ) validator.set_context(self) diff --git a/rest_framework/validators.py b/rest_framework/validators.py index 2907312a9..aa7937714 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -41,7 +41,6 @@ class UniqueValidator: def __init__(self, queryset, message=None, lookup='exact'): self.queryset = queryset - self.serializer_field = None self.message = message or self.message self.lookup = lookup @@ -94,15 +93,14 @@ class UniqueTogetherValidator: def __init__(self, queryset, fields, message=None): self.queryset = queryset self.fields = fields - self.serializer_field = None self.message = message or self.message - def enforce_required_fields(self, attrs, instance): + def enforce_required_fields(self, attrs, serializer): """ The `UniqueTogetherValidator` always forces an implied 'required' state on the fields it applies to. """ - if instance is not None: + if serializer.instance is not None: return missing_items = { @@ -113,16 +111,16 @@ class UniqueTogetherValidator: if missing_items: raise ValidationError(missing_items, code='required') - def filter_queryset(self, attrs, queryset, instance): + def filter_queryset(self, attrs, queryset, serializer): """ Filter the queryset to all instances matching the given attributes. """ # If this is an update, then any unprovided field should # have it's value set based on the existing instance attribute. - if instance is not None: + if serializer.instance is not None: for field_name in self.fields: if field_name not in attrs: - attrs[field_name] = getattr(instance, field_name) + attrs[field_name] = getattr(serializer.instance, field_name) # Determine the filter keyword arguments and filter the queryset. filter_kwargs = { @@ -141,13 +139,10 @@ class UniqueTogetherValidator: return queryset def __call__(self, attrs, serializer): - # Determine the existing instance, if this is an update operation. - instance = getattr(serializer, 'instance', None) - - self.enforce_required_fields(attrs, instance) + self.enforce_required_fields(attrs, serializer) queryset = self.queryset - queryset = self.filter_queryset(attrs, queryset, instance) - queryset = self.exclude_current_instance(attrs, queryset, instance) + queryset = self.filter_queryset(attrs, queryset, serializer) + queryset = self.exclude_current_instance(attrs, queryset, serializer.instance) # Ignore validation if any field is None checked_values = [ @@ -207,13 +202,11 @@ class BaseUniqueForValidator: # same as the serializer field names if `source=<>` is set. field_name = serializer.fields[self.field].source_attrs[-1] date_field_name = serializer.fields[self.date_field].source_attrs[-1] - # Determine the existing instance, if this is an update operation. - instance = getattr(serializer, 'instance', None) self.enforce_required_fields(attrs) queryset = self.queryset queryset = self.filter_queryset(attrs, queryset, field_name, date_field_name) - queryset = self.exclude_current_instance(attrs, queryset, instance) + queryset = self.exclude_current_instance(attrs, queryset, serializer.instance) if qs_exists(queryset): message = self.message.format(date_field=self.date_field) raise ValidationError({ diff --git a/tests/test_fields.py b/tests/test_fields.py index 1d302b730..0be1b1a7a 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -565,11 +565,10 @@ class TestCreateOnlyDefault: on the callable if possible """ class TestCallableDefault: - def set_context(self, serializer_field): - self.field = serializer_field + requires_context = True - def __call__(self): - return "success" if hasattr(self, 'field') else "failure" + def __call__(self, field=None): + return "success" if field is not None else "failure" class TestSerializer(serializers.Serializer): context_set = serializers.CharField(default=serializers.CreateOnlyDefault(TestCallableDefault())) diff --git a/tests/test_validators.py b/tests/test_validators.py index bb29a4305..5c4a62b31 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -357,11 +357,16 @@ class TestUniquenessTogetherValidation(TestCase): def filter(self, **kwargs): self.called_with = kwargs + class MockSerializer: + def __init__(self, instance): + self.instance = instance + data = {'race_name': 'bar'} queryset = MockQueryset() + serializer = MockSerializer(instance=self.instance) validator = UniqueTogetherValidator(queryset, fields=('race_name', 'position')) - validator.filter_queryset(attrs=data, queryset=queryset, instance=self.instance) + validator.filter_queryset(attrs=data, queryset=queryset, serializer=serializer) assert queryset.called_with == {'race_name': 'bar', 'position': 1} From f744da74d2878b480220ebaf9d8117ff9b79a947 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 12 Dec 2019 01:08:54 +0200 Subject: [PATCH 14/40] Improve the docstring on @action (#6951) --- rest_framework/decorators.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index eb1cad9e4..30b9d84d4 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -124,8 +124,23 @@ def action(methods=None, detail=None, url_path=None, url_name=None, **kwargs): """ Mark a ViewSet method as a routable action. - Set the `detail` boolean to determine if this action should apply to - instance/detail requests or collection/list requests. + `@action`-decorated functions will be endowed with a `mapping` property, + a `MethodMapper` that can be used to add additional method-based behaviors + on the routed action. + + :param methods: A list of HTTP method names this action responds to. + Defaults to GET only. + :param detail: Required. Determines whether this action applies to + instance/detail requests or collection/list requests. + :param url_path: Define the URL segment for this action. Defaults to the + name of the method decorated. + :param url_name: Define the internal (`reverse`) URL name for this action. + Defaults to the name of the method decorated with underscores + replaced with dashes. + :param kwargs: Additional properties to set on the view. This can be used + to override viewset-level *_classes settings, equivalent to + how the `@renderer_classes` etc. decorators work for function- + based API views. """ methods = ['get'] if (methods is None) else methods methods = [method.lower() for method in methods] @@ -144,6 +159,10 @@ def action(methods=None, detail=None, url_path=None, url_name=None, **kwargs): func.detail = detail func.url_path = url_path if url_path else func.__name__ func.url_name = url_name if url_name else func.__name__.replace('_', '-') + + # These kwargs will end up being passed to `ViewSet.as_view()` within + # the router, which eventually delegates to Django's CBV `View`, + # which assigns them as instance attributes for each request. func.kwargs = kwargs # Set descriptive arguments for viewsets From 236667b717309934e9c9cae91dbcb0abf4a5e04c Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 12 Dec 2019 05:02:30 -0800 Subject: [PATCH 15/40] Fix UniqueTogetherValidator with field sources (#7086) * Add failing tests for unique_together+source * Fix UniqueTogetherValidator source handling * Fix read-only+default+source handling * Update test to use functional serializer * Test UniqueTogetherValidator error+source --- rest_framework/serializers.py | 2 +- rest_framework/validators.py | 18 ++++++++----- tests/test_validators.py | 49 +++++++++++++++++++++++++++++++---- 3 files changed, 57 insertions(+), 12 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f5d9a5065..63fab3dc3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -448,7 +448,7 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass): default = field.get_default() except SkipField: continue - defaults[field.field_name] = default + defaults[field.source] = default return defaults diff --git a/rest_framework/validators.py b/rest_framework/validators.py index aa7937714..4681d4fb1 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -106,7 +106,7 @@ class UniqueTogetherValidator: missing_items = { field_name: self.missing_message for field_name in self.fields - if field_name not in attrs + if serializer.fields[field_name].source not in attrs } if missing_items: raise ValidationError(missing_items, code='required') @@ -115,17 +115,23 @@ class UniqueTogetherValidator: """ Filter the queryset to all instances matching the given attributes. """ + # field names => field sources + sources = [ + serializer.fields[field_name].source + for field_name in self.fields + ] + # If this is an update, then any unprovided field should # have it's value set based on the existing instance attribute. if serializer.instance is not None: - for field_name in self.fields: - if field_name not in attrs: - attrs[field_name] = getattr(serializer.instance, field_name) + for source in sources: + if source not in attrs: + attrs[source] = getattr(serializer.instance, source) # Determine the filter keyword arguments and filter the queryset. filter_kwargs = { - field_name: attrs[field_name] - for field_name in self.fields + source: attrs[source] + for source in sources } return qs_filter(queryset, **filter_kwargs) diff --git a/tests/test_validators.py b/tests/test_validators.py index 5c4a62b31..21c00073d 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -301,6 +301,49 @@ class TestUniquenessTogetherValidation(TestCase): ] } + def test_read_only_fields_with_default_and_source(self): + class ReadOnlySerializer(serializers.ModelSerializer): + name = serializers.CharField(source='race_name', default='test', read_only=True) + + class Meta: + model = UniquenessTogetherModel + fields = ['name', 'position'] + validators = [ + UniqueTogetherValidator( + queryset=UniquenessTogetherModel.objects.all(), + fields=['name', 'position'] + ) + ] + + serializer = ReadOnlySerializer(data={'position': 1}) + assert serializer.is_valid(raise_exception=True) + + def test_writeable_fields_with_source(self): + class WriteableSerializer(serializers.ModelSerializer): + name = serializers.CharField(source='race_name') + + class Meta: + model = UniquenessTogetherModel + fields = ['name', 'position'] + validators = [ + UniqueTogetherValidator( + queryset=UniquenessTogetherModel.objects.all(), + fields=['name', 'position'] + ) + ] + + serializer = WriteableSerializer(data={'name': 'test', 'position': 1}) + assert serializer.is_valid(raise_exception=True) + + # Validation error should use seriazlier field name, not source + serializer = WriteableSerializer(data={'position': 1}) + assert not serializer.is_valid() + assert serializer.errors == { + 'name': [ + 'This field is required.' + ] + } + def test_allow_explict_override(self): """ Ensure validators can be explicitly removed.. @@ -357,13 +400,9 @@ class TestUniquenessTogetherValidation(TestCase): def filter(self, **kwargs): self.called_with = kwargs - class MockSerializer: - def __init__(self, instance): - self.instance = instance - data = {'race_name': 'bar'} queryset = MockQueryset() - serializer = MockSerializer(instance=self.instance) + serializer = UniquenessTogetherSerializer(instance=self.instance) validator = UniqueTogetherValidator(queryset, fields=('race_name', 'position')) validator.filter_queryset(attrs=data, queryset=queryset, serializer=serializer) From 7c5459626d3850537e054a11d4fe0035a4f0de24 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Thu, 12 Dec 2019 13:03:34 +0000 Subject: [PATCH 16/40] Declare Django versions in install_requires (#7063) * Declare Django versions in install_requires Pip's dependency resolver (used in pipenv, pip-compile, poetry, etc.) can use this to infer whether there's a verison collision in what it's being asked to install or not. * No max --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 24749992b..65536885a 100755 --- a/setup.py +++ b/setup.py @@ -82,7 +82,7 @@ setup( author_email='tom@tomchristie.com', # SEE NOTE BELOW (*) packages=find_packages(exclude=['tests*']), include_package_data=True, - install_requires=[], + install_requires=["django>=1.11"], python_requires=">=3.5", zip_safe=False, classifiers=[ From b8c369c4cf4fef055cb69e60fefe4eb7f1821e62 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Thu, 12 Dec 2019 06:03:55 -0800 Subject: [PATCH 17/40] Fix serializer multiple inheritance bug (#6980) * Expand declared filtering tests - Test declared filter ordering - Test multiple inheritance * Fix serializer multiple inheritance bug * Improve field order test to check for field types --- rest_framework/serializers.py | 26 ++++++++++-------- tests/test_serializer.py | 50 +++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 63fab3dc3..18f4d0df6 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -298,18 +298,22 @@ class SerializerMetaclass(type): if isinstance(obj, Field)] fields.sort(key=lambda x: x[1]._creation_counter) - # If this class is subclassing another Serializer, add that Serializer's - # fields. Note that we loop over the bases in *reverse*. This is necessary - # in order to maintain the correct order of fields. - for base in reversed(bases): - if hasattr(base, '_declared_fields'): - fields = [ - (field_name, obj) for field_name, obj - in base._declared_fields.items() - if field_name not in attrs - ] + fields + # Ensures a base class field doesn't override cls attrs, and maintains + # field precedence when inheriting multiple parents. e.g. if there is a + # class C(A, B), and A and B both define 'field', use 'field' from A. + known = set(attrs) - return OrderedDict(fields) + def visit(name): + known.add(name) + return name + + base_fields = [ + (visit(name), f) + for base in bases if hasattr(base, '_declared_fields') + for name, f in base._declared_fields.items() if name not in known + ] + + return OrderedDict(base_fields + fields) def __new__(cls, name, bases, attrs): attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs) diff --git a/tests/test_serializer.py b/tests/test_serializer.py index fab0472b9..a58c46b2d 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -682,3 +682,53 @@ class TestDeclaredFieldInheritance: assert len(Parent().get_fields()) == 2 assert len(Child().get_fields()) == 2 assert len(Grandchild().get_fields()) == 2 + + def test_multiple_inheritance(self): + class A(serializers.Serializer): + field = serializers.CharField() + + class B(serializers.Serializer): + field = serializers.IntegerField() + + class TestSerializer(A, B): + pass + + fields = { + name: type(f) for name, f + in TestSerializer()._declared_fields.items() + } + assert fields == { + 'field': serializers.CharField, + } + + def test_field_ordering(self): + class Base(serializers.Serializer): + f1 = serializers.CharField() + f2 = serializers.CharField() + + class A(Base): + f3 = serializers.IntegerField() + + class B(serializers.Serializer): + f3 = serializers.CharField() + f4 = serializers.CharField() + + class TestSerializer(A, B): + f2 = serializers.IntegerField() + f5 = serializers.CharField() + + fields = { + name: type(f) for name, f + in TestSerializer()._declared_fields.items() + } + + # `IntegerField`s should be the 'winners' in field name conflicts + # - `TestSerializer.f2` should override `Base.F2` + # - `A.f3` should override `B.f3` + assert fields == { + 'f1': serializers.CharField, + 'f2': serializers.IntegerField, + 'f3': serializers.IntegerField, + 'f4': serializers.CharField, + 'f5': serializers.CharField, + } From 3c1428ff799e4fe19e1eb28ed63a7d54c6180f1d Mon Sep 17 00:00:00 2001 From: Jordan Ephron Date: Thu, 12 Dec 2019 06:09:34 -0800 Subject: [PATCH 18/40] Fix NotImplementedError for Field.to_internal_value and Field.to_representation (#6996) --- rest_framework/fields.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 11a291568..2c45ec6f4 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -605,8 +605,11 @@ class Field: Transform the *incoming* primitive data into a native value. """ raise NotImplementedError( - '{cls}.to_internal_value() must be implemented.'.format( - cls=self.__class__.__name__ + '{cls}.to_internal_value() must be implemented for field ' + '{field_name}. If you do not need to support write operations ' + 'you probably want to subclass `ReadOnlyField` instead.'.format( + cls=self.__class__.__name__, + field_name=self.field_name, ) ) @@ -615,9 +618,7 @@ class Field: Transform the *outgoing* native value into primitive data. """ raise NotImplementedError( - '{cls}.to_representation() must be implemented for field ' - '{field_name}. If you do not need to support write operations ' - 'you probably want to subclass `ReadOnlyField` instead.'.format( + '{cls}.to_representation() must be implemented for field {field_name}.'.format( cls=self.__class__.__name__, field_name=self.field_name, ) From de497a9bf12605b8b71bf7c21da57bc2a8238786 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 12 Dec 2019 14:31:40 +0000 Subject: [PATCH 19/40] Version 3.11 (#7083) * Version 3.11 * Added notes on OpenAPI changes for 3.11. * Minor docs tweaking * Update package version and supported versions * Use a lazy import for django.test.client.encode_mutlipart. Closes #7078 --- README.md | 4 +- docs/community/3.10-announcement.md | 2 +- docs/community/3.11-announcement.md | 117 ++++++++++++++++++++++++++++ docs/index.md | 6 +- mkdocs.yml | 1 + rest_framework/__init__.py | 2 +- rest_framework/renderers.py | 3 +- 7 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 docs/community/3.11-announcement.md diff --git a/README.md b/README.md index 8774bc854..9591bdc17 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ There is a live example API for testing purposes, [available here][sandbox]. # Requirements -* Python (3.5, 3.6, 3.7) -* Django (1.11, 2.0, 2.1, 2.2) +* Python (3.5, 3.6, 3.7, 3.8) +* Django (1.11, 2.0, 2.1, 2.2, 3.0) We **highly recommend** and only officially support the latest patch release of each Python and Django series. diff --git a/docs/community/3.10-announcement.md b/docs/community/3.10-announcement.md index 578e900dc..19748aa40 100644 --- a/docs/community/3.10-announcement.md +++ b/docs/community/3.10-announcement.md @@ -144,4 +144,4 @@ continued development by **[signing up for a paid plan][funding]**. [legacy-core-api-docs]:https://github.com/encode/django-rest-framework/blob/master/docs/coreapi/index.md [sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors -[funding]: community/funding.md +[funding]: funding.md diff --git a/docs/community/3.11-announcement.md b/docs/community/3.11-announcement.md new file mode 100644 index 000000000..83dd636d1 --- /dev/null +++ b/docs/community/3.11-announcement.md @@ -0,0 +1,117 @@ + + +# Django REST framework 3.11 + +The 3.11 release adds support for Django 3.0. + +* Our supported Python versions are now: 3.5, 3.6, 3.7, and 3.8. +* Our supported Django versions are now: 1.11, 2.0, 2.1, 2.2, and 3.0. + +This release will be the last to support Python 3.5 or Django 1.11. + +## OpenAPI Schema Generation Improvements + +The OpenAPI schema generation continues to mature. Some highlights in 3.11 +include: + +* Automatic mapping of Django REST Framework renderers and parsers into OpenAPI + request and response media-types. +* Improved mapping JSON schema mapping types, for example in HStoreFields, and + with large integer values. +* Porting of the old CoreAPI parsing of docstrings to form OpenAPI operation + descriptions. + +In this example view operation descriptions for the `get` and `post` methods will +be extracted from the class docstring: + +```python +class DocStringExampleListView(APIView): +""" +get: A description of my GET operation. +post: A description of my POST operation. +""" + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def get(self, request, *args, **kwargs): + ... + + def post(self, request, *args, **kwargs): + ... +``` + +## Validator / Default Context + +In some circumstances a Validator class or a Default class may need to access the serializer field with which it is called, or the `.context` with which the serializer was instantiated. In particular: + +* Uniqueness validators need to be able to determine the name of the field to which they are applied, in order to run an appropriate database query. +* The `CurrentUserDefault` needs to be able to determine the context with which the serializer was instantiated, in order to return the current user instance. + +Previous our approach to this was that implementations could include a `set_context` method, which would be called prior to validation. However this approach had issues with potential race conditions. We have now move this approach into a pending deprecation state. It will continue to function, but will be escalated to a deprecated state in 3.12, and removed entirely in 3.13. + +Instead, validators or defaults which require the serializer context, should include a `requires_context = True` attribute on the class. + +The `__call__` method should then include an additional `serializer_field` argument. + +Validator implementations will look like this: + +```python +class CustomValidator: + requires_context = True + + def __call__(self, value, serializer_field): + ... +``` + +Default implementations will look like this: + +```python +class CustomDefault: + requires_context = True + + def __call__(self, serializer_field): + ... +``` + +--- + +## Funding + +REST framework is a *collaboratively funded project*. If you use +REST framework commercially we strongly encourage you to invest in its +continued development by **[signing up for a paid plan][funding]**. + +*Every single sign-up helps us make REST framework long-term financially sustainable.* + + +
+ +*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [ESG](https://software.esg-usa.com/), [Rollbar](https://rollbar.com/?utm_source=django&utm_medium=sponsorship&utm_campaign=freetrial), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), [Lights On Software](https://lightsonsoftware.com), and [Retool](https://retool.com/?utm_source=djangorest&utm_medium=sponsorship).* + +[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors +[funding]: funding.md diff --git a/docs/index.md b/docs/index.md index e06b21dff..bccc1fb46 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,7 +52,7 @@ Some reasons you might want to use REST framework: * [Authentication policies][authentication] including packages for [OAuth1a][oauth1-section] and [OAuth2][oauth2-section]. * [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources. * Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers]. -* [Extensive documentation][index], and [great community support][group]. +* Extensive documentation, and [great community support][group]. * Used and trusted by internationally recognised companies including [Mozilla][mozilla], [Red Hat][redhat], [Heroku][heroku], and [Eventbrite][eventbrite]. --- @@ -85,8 +85,8 @@ continued development by **[signing up for a paid plan][funding]**. REST framework requires the following: -* Python (3.5, 3.6, 3.7) -* Django (1.11, 2.0, 2.1, 2.2) +* Python (3.5, 3.6, 3.7, 3.8) +* Django (1.11, 2.0, 2.1, 2.2, 3.0) We **highly recommend** and only officially support the latest patch release of each Python and Django series. diff --git a/mkdocs.yml b/mkdocs.yml index 83a345a3d..484971a71 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ nav: - 'Contributing to REST framework': 'community/contributing.md' - 'Project management': 'community/project-management.md' - 'Release Notes': 'community/release-notes.md' + - '3.11 Announcement': 'community/3.11-announcement.md' - '3.10 Announcement': 'community/3.10-announcement.md' - '3.9 Announcement': 'community/3.9-announcement.md' - '3.8 Announcement': 'community/3.8-announcement.md' diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index fceee6817..b6f3f65ce 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.10.3' +__version__ = '3.11.0' __author__ = 'Tom Christie' __license__ = 'BSD 3-Clause' __copyright__ = 'Copyright 2011-2019 Encode OSS Ltd' diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 9a6f3c3c5..29ac90ea8 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -16,7 +16,6 @@ from django.core.exceptions import ImproperlyConfigured from django.core.paginator import Page from django.http.multipartparser import parse_header from django.template import engines, loader -from django.test.client import encode_multipart from django.urls import NoReverseMatch from django.utils.html import mark_safe @@ -902,6 +901,8 @@ class MultiPartRenderer(BaseRenderer): BOUNDARY = 'BoUnDaRyStRiNg' def render(self, data, accepted_media_type=None, renderer_context=None): + from django.test.client import encode_multipart + if hasattr(data, 'items'): for key, value in data.items(): assert not isinstance(value, dict), ( From d985c7cbb999b2bc18a109249c583e91f4c27aec Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 16 Dec 2019 20:59:25 +0200 Subject: [PATCH 20/40] Remove a few no longer needed compat checks and references (#7092) * serializers: removes no longer needed compat checks UUIDField and DurationField are both supported in all supported Django versions. IPAddressField was removed in Django 1.9, which is no longer supported. * serializers: move related code closer together This way it's easier to see all of the mappings in one place. * serializers,docs: remove some DRF 2.x references The last release of DRF 2.x was 5 years ago, it seems fine to remove these references now. --- docs/api-guide/fields.md | 2 -- docs/api-guide/generic-views.md | 2 -- rest_framework/serializers.py | 36 ++++++--------------------------- 3 files changed, 6 insertions(+), 34 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index e964458f9..65c83b78e 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -597,8 +597,6 @@ The `.to_representation()` method is called to convert the initial datatype into The `to_internal_value()` method is called to restore a primitive datatype into its internal python representation. This method should raise a `serializers.ValidationError` if the data is invalid. -Note that the `WritableField` class that was present in version 2.x no longer exists. You should subclass `Field` and override `to_internal_value()` if the field supports data input. - ## Examples ### A Basic Custom Field diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index a2f19ff2e..a256eb2d9 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -175,8 +175,6 @@ You can also use these hooks to provide additional validation, by raising a `Val raise ValidationError('You have already signed up') serializer.save(user=self.request.user) -**Note**: These methods replace the old-style version 2.x `pre_save`, `post_save`, `pre_delete` and `post_delete` methods, which are no longer available. - **Other methods**: You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`. diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 18f4d0df6..8c2486bea 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -19,7 +19,6 @@ from collections.abc import Mapping from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured from django.core.exceptions import ValidationError as DjangoValidationError from django.db import models -from django.db.models import DurationField as ModelDurationField from django.db.models.fields import Field as DjangoModelField from django.utils import timezone from django.utils.functional import cached_property @@ -167,13 +166,6 @@ class BaseSerializer(Field): raise NotImplementedError('`create()` must be implemented.') def save(self, **kwargs): - assert not hasattr(self, 'save_object'), ( - 'Serializer `%s.%s` has old-style version 2 `.save_object()` ' - 'that is no longer compatible with REST framework 3. ' - 'Use the new-style `.create()` and `.update()` methods instead.' % - (self.__class__.__module__, self.__class__.__name__) - ) - assert hasattr(self, '_errors'), ( 'You must call `.is_valid()` before calling `.save()`.' ) @@ -217,13 +209,6 @@ class BaseSerializer(Field): return self.instance def is_valid(self, raise_exception=False): - assert not hasattr(self, 'restore_object'), ( - 'Serializer `%s.%s` has old-style version 2 `.restore_object()` ' - 'that is no longer compatible with REST framework 3. ' - 'Use the new-style `.create()` and `.update()` methods instead.' % - (self.__class__.__module__, self.__class__.__name__) - ) - assert hasattr(self, 'initial_data'), ( 'Cannot call `.is_valid()` as no `data=` keyword argument was ' 'passed when instantiating the serializer instance.' @@ -876,6 +861,7 @@ class ModelSerializer(Serializer): models.DateField: DateField, models.DateTimeField: DateTimeField, models.DecimalField: DecimalField, + models.DurationField: DurationField, models.EmailField: EmailField, models.Field: ModelField, models.FileField: FileField, @@ -890,11 +876,14 @@ class ModelSerializer(Serializer): models.TextField: CharField, models.TimeField: TimeField, models.URLField: URLField, + models.UUIDField: UUIDField, models.GenericIPAddressField: IPAddressField, models.FilePathField: FilePathField, } - if ModelDurationField is not None: - serializer_field_mapping[ModelDurationField] = DurationField + if postgres_fields: + serializer_field_mapping[postgres_fields.HStoreField] = HStoreField + serializer_field_mapping[postgres_fields.ArrayField] = ListField + serializer_field_mapping[postgres_fields.JSONField] = JSONField serializer_related_field = PrimaryKeyRelatedField serializer_related_to_field = SlugRelatedField serializer_url_field = HyperlinkedIdentityField @@ -1585,19 +1574,6 @@ class ModelSerializer(Serializer): return validators -if hasattr(models, 'UUIDField'): - ModelSerializer.serializer_field_mapping[models.UUIDField] = UUIDField - -# IPAddressField is deprecated in Django -if hasattr(models, 'IPAddressField'): - ModelSerializer.serializer_field_mapping[models.IPAddressField] = IPAddressField - -if postgres_fields: - ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = HStoreField - ModelSerializer.serializer_field_mapping[postgres_fields.ArrayField] = ListField - ModelSerializer.serializer_field_mapping[postgres_fields.JSONField] = JSONField - - class HyperlinkedModelSerializer(ModelSerializer): """ A type of `ModelSerializer` that uses hyperlinked relationships instead From 62ae241894fc49a7c6261cb1b6e3b9c98768ecf0 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 2 Jan 2020 16:52:05 +0200 Subject: [PATCH 21/40] Remove outdated comment in SerializerMethodField (#7110) Since 91ea13840699abca0e14e20732fd445d65c91914. --- rest_framework/fields.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 2c45ec6f4..3df7888a0 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1889,14 +1889,9 @@ class SerializerMethodField(Field): super().__init__(**kwargs) def bind(self, field_name, parent): - # In order to enforce a consistent style, we error if a redundant - # 'method_name' argument has been used. For example: - # my_field = serializer.SerializerMethodField(method_name='get_my_field') - default_method_name = 'get_{field_name}'.format(field_name=field_name) - - # The method name should default to `get_{field_name}`. + # The method name defaults to `get_{field_name}`. if self.method_name is None: - self.method_name = default_method_name + self.method_name = 'get_{field_name}'.format(field_name=field_name) super().bind(field_name, parent) From f3ed69374dc8b181bbde497a824b165e31f13457 Mon Sep 17 00:00:00 2001 From: Jihoon Park Date: Fri, 3 Jan 2020 22:28:36 +0900 Subject: [PATCH 22/40] Add missing punctuation marks and URL name (#7108) - trailing commas (as both Python and JavaScript allow them) - trailing semicolons in JavaScript - URL name `api-docs` --- docs/topics/api-clients.md | 54 +++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/topics/api-clients.md b/docs/topics/api-clients.md index 3fd560634..b020c380a 100644 --- a/docs/topics/api-clients.md +++ b/docs/topics/api-clients.md @@ -384,7 +384,7 @@ First, install the API documentation views. These will include the schema resour urlpatterns = [ ... - url(r'^docs/', include_docs_urls(title='My API service')) + url(r'^docs/', include_docs_urls(title='My API service'), name='api-docs'), ] Once the API documentation URLs are installed, you'll be able to include both the required JavaScript resources. Note that the ordering of these two lines is important, as the schema loading requires CoreAPI to already be installed. @@ -401,14 +401,14 @@ Once the API documentation URLs are installed, you'll be able to include both th The `coreapi` library, and the `schema` object will now both be available on the `window` instance. - const coreapi = window.coreapi - const schema = window.schema + const coreapi = window.coreapi; + const schema = window.schema; ## Instantiating a client In order to interact with the API you'll need a client instance. - var client = new coreapi.Client() + var client = new coreapi.Client(); Typically you'll also want to provide some authentication credentials when instantiating the client. @@ -421,9 +421,9 @@ the user to login, and then instantiate a client using session authentication: let auth = new coreapi.auth.SessionAuthentication({ csrfCookieName: 'csrftoken', - csrfHeaderName: 'X-CSRFToken' - }) - let client = new coreapi.Client({auth: auth}) + csrfHeaderName: 'X-CSRFToken', + }); + let client = new coreapi.Client({auth: auth}); The authentication scheme will handle including a CSRF header in any outgoing requests for unsafe HTTP methods. @@ -434,10 +434,10 @@ The `TokenAuthentication` class can be used to support REST framework's built-in `TokenAuthentication`, as well as OAuth and JWT schemes. let auth = new coreapi.auth.TokenAuthentication({ - scheme: 'JWT' - token: '' - }) - let client = new coreapi.Client({auth: auth}) + scheme: 'JWT', + token: '', + }); + let client = new coreapi.Client({auth: auth}); When using TokenAuthentication you'll probably need to implement a login flow using the CoreAPI client. @@ -448,20 +448,20 @@ request to an "obtain token" endpoint For example, using the "Django REST framework JWT" package // Setup some globally accessible state - window.client = new coreapi.Client() - window.loggedIn = false + window.client = new coreapi.Client(); + window.loggedIn = false; function loginUser(username, password) { - let action = ["api-token-auth", "obtain-token"] - let params = {username: "example", email: "example@example.com"} + let action = ["api-token-auth", "obtain-token"]; + let params = {username: "example", email: "example@example.com"}; client.action(schema, action, params).then(function(result) { // On success, instantiate an authenticated client. let auth = window.coreapi.auth.TokenAuthentication({ scheme: 'JWT', - token: result['token'] + token: result['token'], }) - window.client = coreapi.Client({auth: auth}) - window.loggedIn = true + window.client = coreapi.Client({auth: auth}); + window.loggedIn = true; }).catch(function (error) { // Handle error case where eg. user provides incorrect credentials. }) @@ -473,23 +473,23 @@ The `BasicAuthentication` class can be used to support HTTP Basic Authentication let auth = new coreapi.auth.BasicAuthentication({ username: '', - password: '' + password: '', }) - let client = new coreapi.Client({auth: auth}) + let client = new coreapi.Client({auth: auth}); ## Using the client Making requests: - let action = ["users", "list"] + let action = ["users", "list"]; client.action(schema, action).then(function(result) { // Return value is in 'result' }) Including parameters: - let action = ["users", "create"] - let params = {username: "example", email: "example@example.com"} + let action = ["users", "create"]; + let params = {username: "example", email: "example@example.com"}; client.action(schema, action, params).then(function(result) { // Return value is in 'result' }) @@ -512,12 +512,12 @@ The coreapi package is available on NPM. You'll either want to include the API schema in your codebase directly, by copying it from the `schema.js` resource, or else load the schema asynchronously. For example: - let client = new coreapi.Client() - let schema = null + let client = new coreapi.Client(); + let schema = null; client.get("https://api.example.org/").then(function(data) { // Load a CoreJSON API schema. - schema = data - console.log('schema loaded') + schema = data; + console.log('schema loaded'); }) [heroku-api]: https://devcenter.heroku.com/categories/platform-api From 07376f128cdeaf760967392253ffe1bafc3b2473 Mon Sep 17 00:00:00 2001 From: Bart Date: Fri, 3 Jan 2020 14:36:43 +0100 Subject: [PATCH 23/40] Grammar fix (#6933) --- docs/tutorial/2-requests-and-responses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index b6433695a..f2b369615 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -29,7 +29,7 @@ REST framework provides two wrappers you can use to write API views. These wrappers provide a few bits of functionality such as making sure you receive `Request` instances in your view, and adding context to `Response` objects so that content negotiation can be performed. -The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exception that occurs when accessing `request.data` with malformed input. +The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exceptions that occur when accessing `request.data` with malformed input. ## Pulling it all together From ced37a56cbc78fdd0679c76b4d26d6cd8d215536 Mon Sep 17 00:00:00 2001 From: Noam Date: Fri, 3 Jan 2020 15:49:46 +0200 Subject: [PATCH 24/40] Avoid outputting callable defaults to schema. (#7105) --- rest_framework/schemas/openapi.py | 2 +- tests/schemas/test_openapi.py | 16 ++++++++++++++++ tests/schemas/views.py | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 134df5043..fe688facc 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -393,7 +393,7 @@ class AutoSchema(ViewInspector): schema['writeOnly'] = True if field.allow_null: schema['nullable'] = True - if field.default and field.default != empty: # why don't they use None?! + if field.default and field.default != empty and not callable(field.default): # why don't they use None?! schema['default'] = field.default if field.help_text: schema['description'] = str(field.help_text) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 622f78cdd..03eb9de7a 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -571,6 +571,22 @@ class TestOperationIntrospection(TestCase): properties = response_schema['items']['properties'] assert properties['hstore']['type'] == 'object' + def test_serializer_callable_default(self): + path = '/' + method = 'GET' + view = create_view( + views.ExampleGenericAPIView, + method, + create_request(path), + ) + inspector = AutoSchema() + inspector.view = view + + responses = inspector._get_responses(path, method) + response_schema = responses['200']['content']['application/json']['schema'] + properties = response_schema['items']['properties'] + assert 'default' not in properties['uuid_field'] + def test_serializer_validators(self): path = '/' method = 'GET' diff --git a/tests/schemas/views.py b/tests/schemas/views.py index f8d143e71..e8307ccbd 100644 --- a/tests/schemas/views.py +++ b/tests/schemas/views.py @@ -58,6 +58,7 @@ class ExampleSerializer(serializers.Serializer): date = serializers.DateField() datetime = serializers.DateTimeField() hstore = serializers.HStoreField() + uuid_field = serializers.UUIDField(default=uuid.uuid4) class ExampleGenericAPIView(generics.GenericAPIView): From 430a5672582ef3984362d1189ac1250586e28b0d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 3 Jan 2020 13:50:26 +0000 Subject: [PATCH 25/40] Update openapi.py --- rest_framework/schemas/openapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index fe688facc..58788bc23 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -393,7 +393,7 @@ class AutoSchema(ViewInspector): schema['writeOnly'] = True if field.allow_null: schema['nullable'] = True - if field.default and field.default != empty and not callable(field.default): # why don't they use None?! + if field.default and field.default != empty and not callable(field.default): schema['default'] = field.default if field.help_text: schema['description'] = str(field.help_text) From 25ac7ba450e0a92b02c1bc0199e5d136514a5aea Mon Sep 17 00:00:00 2001 From: Frederico Lima Date: Fri, 3 Jan 2020 10:53:09 -0300 Subject: [PATCH 26/40] Add third party lib drf-viewset-profiler (#6993) --- docs/community/third-party-packages.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/community/third-party-packages.md b/docs/community/third-party-packages.md index 4d0043252..baa30fd0c 100644 --- a/docs/community/third-party-packages.md +++ b/docs/community/third-party-packages.md @@ -270,6 +270,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [django-rest-framework-condition][django-rest-framework-condition] - Decorators for managing HTTP cache headers for Django REST framework (ETag and Last-modified). * [django-rest-witchcraft][django-rest-witchcraft] - Provides DRF integration with SQLAlchemy with SQLAlchemy model serializers/viewsets and a bunch of other goodies * [djangorestframework-mvt][djangorestframework-mvt] - An extension for creating views that serve Postgres data as Map Box Vector Tiles. +* [drf-viewset-profiler][drf-viewset-profiler] - Lib to profile all methods from a viewset line by line. * [djangorestframework-features][djangorestframework-features] - Advanced schema generation and more based on named features. [cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html @@ -351,4 +352,5 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-restql]: https://github.com/yezyilomo/django-restql [djangorestframework-mvt]: https://github.com/corteva/djangorestframework-mvt [django-rest-framework-guardian]: https://github.com/rpkilby/django-rest-framework-guardian +[drf-viewset-profiler]: https://github.com/fvlima/drf-viewset-profiler [djangorestframework-features]: https://github.com/cloudcode-hungary/django-rest-framework-features/ From a9e55334e7d42c03929b33708cee6f0bd908e7c2 Mon Sep 17 00:00:00 2001 From: phankiewicz <12519354+phankiewicz@users.noreply.github.com> Date: Fri, 3 Jan 2020 14:59:32 +0100 Subject: [PATCH 27/40] Add X-CSRFToken HTTP header in swagger-ui example (#6968) --- docs/topics/documenting-your-api.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md index 5c806ea7e..5c5872650 100644 --- a/docs/topics/documenting-your-api.md +++ b/docs/topics/documenting-your-api.md @@ -45,7 +45,11 @@ this: SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset ], - layout: "BaseLayout" + layout: "BaseLayout", + requestInterceptor: (request) => { + request.headers['X-CSRFToken'] = "{{ csrf_token }}" + return request; + } }) From 165da5be0c8bd775156d6d1ac1fceb7eb325cbd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20=E2=9A=A1?= <1678423+Alex-CodeLab@users.noreply.github.com> Date: Fri, 3 Jan 2020 15:42:29 +0100 Subject: [PATCH 28/40] Documentation: make codeblocks easier to read. (#6896) --- docs_theme/css/default.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs_theme/css/default.css b/docs_theme/css/default.css index bb17a3a11..e9d7f23bf 100644 --- a/docs_theme/css/default.css +++ b/docs_theme/css/default.css @@ -74,6 +74,12 @@ pre { white-space: pre; } +code, pre { + font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif; + font-size: 13px; +} + + /* Preserve the spacing of the navbar across different screen sizes. */ .navbar-inner { /*padding: 5px 0;*/ @@ -432,3 +438,4 @@ ul.sponsor { margin: 0 !important; display: inline-block !important; } + From 373e521f3685c48eb22e6fbb1d7079f161a4a67b Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Mon, 6 Jan 2020 15:12:21 +0100 Subject: [PATCH 29/40] Make CharField prohibit surrogate characters (#7026) (#7067) * CharField: Detect and prohibit surrogate characters * CharField: Cover handling of surrogate characters --- rest_framework/fields.py | 2 ++ rest_framework/validators.py | 11 +++++++++++ tests/test_fields.py | 15 +++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 3df7888a0..958bebeef 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -36,6 +36,7 @@ from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.settings import api_settings from rest_framework.utils import html, humanize_datetime, json, representation from rest_framework.utils.formatting import lazy_format +from rest_framework.validators import ProhibitSurrogateCharactersValidator class empty: @@ -818,6 +819,7 @@ class CharField(Field): # ProhibitNullCharactersValidator is None on Django < 2.0 if ProhibitNullCharactersValidator is not None: self.validators.append(ProhibitNullCharactersValidator()) + self.validators.append(ProhibitSurrogateCharactersValidator()) def run_validation(self, data=empty): # Test for the empty string here so that it does not get validated, diff --git a/rest_framework/validators.py b/rest_framework/validators.py index 4681d4fb1..a5cb75a84 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -167,6 +167,17 @@ class UniqueTogetherValidator: ) +class ProhibitSurrogateCharactersValidator: + message = _('Surrogate characters are not allowed: U+{code_point:X}.') + code = 'surrogate_characters_not_allowed' + + def __call__(self, value): + for surrogate_character in (ch for ch in str(value) + if 0xD800 <= ord(ch) <= 0xDFFF): + message = self.message.format(code_point=ord(surrogate_character)) + raise ValidationError(message, code=self.code) + + class BaseUniqueForValidator: message = None missing_message = _('This field is required.') diff --git a/tests/test_fields.py b/tests/test_fields.py index 0be1b1a7a..a4b78fd51 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -758,6 +758,21 @@ class TestCharField(FieldValues): 'Null characters are not allowed.' ] + def test_surrogate_characters(self): + field = serializers.CharField() + + for code_point, expected_message in ( + (0xD800, 'Surrogate characters are not allowed: U+D800.'), + (0xDFFF, 'Surrogate characters are not allowed: U+DFFF.'), + ): + with pytest.raises(serializers.ValidationError) as exc_info: + field.run_validation(chr(code_point)) + assert exc_info.value.detail[0].code == 'surrogate_characters_not_allowed' + assert str(exc_info.value.detail[0]) == expected_message + + for code_point in (0xD800 - 1, 0xDFFF + 1): + field.run_validation(chr(code_point)) + def test_iterable_validators(self): """ Ensure `validators` parameter is compatible with reasonable iterables. From 442a20650254bd24165ddd69654b6f855df8f386 Mon Sep 17 00:00:00 2001 From: Danny Date: Sat, 11 Jan 2020 00:38:29 -0600 Subject: [PATCH 30/40] Fix full-text search docs (#7133) --- docs/api-guide/filtering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 1bdb6c52b..bad57b441 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -212,7 +212,7 @@ The search behavior may be restricted by prepending various characters to the `s * '^' Starts-with search. * '=' Exact matches. -* '@' Full-text search. (Currently only supported Django's MySQL backend.) +* '@' Full-text search. (Currently only supported Django's [PostgreSQL backend](https://docs.djangoproject.com/en/dev/ref/contrib/postgres/search/).) * '$' Regex search. For example: From 5f3f2ef10636e4083433289ee007e12cd61d8e8b Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Wed, 15 Jan 2020 02:52:29 -0800 Subject: [PATCH 31/40] Add note that APISettings is an internal class (#7144) --- rest_framework/settings.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rest_framework/settings.py b/rest_framework/settings.py index c4c0e7939..9eb4c5653 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -182,14 +182,19 @@ def import_from_string(val, setting_name): class APISettings: """ - A settings object, that allows API settings to be accessed as properties. - For example: + A settings object that allows REST Framework settings to be accessed as + properties. For example: from rest_framework.settings import api_settings print(api_settings.DEFAULT_RENDERER_CLASSES) Any setting with string import paths will be automatically resolved and return the class, rather than the string literal. + + Note: + This is an internal class that is only compatible with settings namespaced + under the REST_FRAMEWORK name. It is not intended to be used by 3rd-party + apps, and test helpers like `override_settings` may not work as expected. """ def __init__(self, user_settings=None, defaults=None, import_strings=None): if user_settings: From 62193e037859498bd8c87139ed63ebfd2cf8c324 Mon Sep 17 00:00:00 2001 From: Jonathan Longe Date: Wed, 15 Jan 2020 11:58:31 -0800 Subject: [PATCH 32/40] Add permissions to quickstart tutorial (#7113) --- docs/tutorial/quickstart.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index ee54816dc..505f7f91d 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -85,6 +85,7 @@ Right, we'd better write some views then. Open `tutorial/quickstart/views.py` a from django.contrib.auth.models import User, Group from rest_framework import viewsets + from rest_framework import permissions from tutorial.quickstart.serializers import UserSerializer, GroupSerializer @@ -94,6 +95,7 @@ Right, we'd better write some views then. Open `tutorial/quickstart/views.py` a """ queryset = User.objects.all().order_by('-date_joined') serializer_class = UserSerializer + permission_classes = [permissions.IsAuthenticated] class GroupViewSet(viewsets.ModelViewSet): @@ -102,6 +104,7 @@ Right, we'd better write some views then. Open `tutorial/quickstart/views.py` a """ queryset = Group.objects.all() serializer_class = GroupSerializer + permission_classes = [permissions.IsAuthenticated] Rather than write multiple views we're grouping together all the common behavior into classes called `ViewSets`. From 7bd730124c93f9309c6603ccfb56316d3f6934f0 Mon Sep 17 00:00:00 2001 From: David Runge Date: Wed, 15 Jan 2020 21:18:25 +0100 Subject: [PATCH 33/40] MANIFEST.in: Adding tests to sdist tarball. (#7145) --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 6f7cb8f13..262e3dc91 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include README.md include LICENSE.md +recursive-include tests/* * recursive-include rest_framework/static *.js *.css *.png *.ico *.eot *.svg *.ttf *.woff *.woff2 recursive-include rest_framework/templates *.html schema.js recursive-include rest_framework/locale *.mo From d0b957760549710b90300b9a3a373d175238884c Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 8 Jan 2020 20:37:23 +0100 Subject: [PATCH 34/40] Return valid OpenAPI schema even when empty. --- rest_framework/schemas/openapi.py | 10 +--------- tests/schemas/test_openapi.py | 9 +++++++++ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 58788bc23..aaeb2914c 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -35,12 +35,7 @@ class SchemaGenerator(BaseSchemaGenerator): def get_paths(self, request=None): result = {} - paths, view_endpoints = self._get_paths_and_endpoints(request) - - # Only generate the path prefix for paths that will be included - if not paths: - return None - + _, view_endpoints = self._get_paths_and_endpoints(request) for path, method, view in view_endpoints: if not self.has_view_permissions(path, method, view): continue @@ -62,9 +57,6 @@ class SchemaGenerator(BaseSchemaGenerator): self._initialise_endpoints() paths = self.get_paths(None if public else request) - if not paths: - return None - schema = { 'openapi': '3.0.2', 'info': self.get_info(), diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 03eb9de7a..83473be4b 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -707,6 +707,15 @@ class TestGenerator(TestCase): assert 'openapi' in schema assert 'paths' in schema + def test_schema_with_no_paths(self): + patterns = [] + generator = SchemaGenerator(patterns=patterns) + + request = create_request('/') + schema = generator.get_schema(request=request) + + assert schema['paths'] == {} + def test_schema_information(self): """Construction of the top level dictionary.""" patterns = [ From 3b88312c33118467c53d7628346b2ae348b4ca6e Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 8 Jan 2020 20:42:59 +0100 Subject: [PATCH 35/40] Call get_schema(), rather than sub-method in schema tests. --- tests/schemas/test_openapi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 83473be4b..8a723b85d 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -659,7 +659,7 @@ class TestGenerator(TestCase): generator = SchemaGenerator(patterns=patterns) generator._initialise_endpoints() - paths = generator.get_paths() + paths = generator.get_schema()["paths"] assert '/example/' in paths example_operations = paths['/example/'] @@ -676,7 +676,7 @@ class TestGenerator(TestCase): generator = SchemaGenerator(patterns=patterns) generator._initialise_endpoints() - paths = generator.get_paths() + paths = generator.get_schema()["paths"] assert '/v1/example/' in paths assert '/v1/example/{id}/' in paths @@ -689,7 +689,7 @@ class TestGenerator(TestCase): generator = SchemaGenerator(patterns=patterns, url='/api') generator._initialise_endpoints() - paths = generator.get_paths() + paths = generator.get_schema()["paths"] assert '/api/example/' in paths assert '/api/example/{id}/' in paths From 496947be3a8ac6e21b862dc7697d96f403b18ad6 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 8 Jan 2020 21:12:26 +0100 Subject: [PATCH 36/40] Inline unnecessary method in OpenAPI schema generator. --- rest_framework/schemas/openapi.py | 37 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index aaeb2914c..1df132ce3 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -32,31 +32,30 @@ class SchemaGenerator(BaseSchemaGenerator): return info - def get_paths(self, request=None): - result = {} - - _, view_endpoints = self._get_paths_and_endpoints(request) - for path, method, view in view_endpoints: - if not self.has_view_permissions(path, method, view): - continue - operation = view.schema.get_operation(path, method) - # Normalise path for any provided mount url. - if path.startswith('/'): - path = path[1:] - path = urljoin(self.url or '/', path) - - result.setdefault(path, {}) - result[path][method.lower()] = operation - - return result - def get_schema(self, request=None, public=False): """ Generate a OpenAPI schema. """ self._initialise_endpoints() - paths = self.get_paths(None if public else request) + # Iterate endpoints generating per method path operations. + # TODO: …and reference components. + paths = {} + _, view_endpoints = self._get_paths_and_endpoints(None if public else request) + for path, method, view in view_endpoints: + if not self.has_view_permissions(path, method, view): + continue + + operation = view.schema.get_operation(path, method) + # Normalise path for any provided mount url. + if path.startswith('/'): + path = path[1:] + path = urljoin(self.url or '/', path) + + paths.setdefault(path, {}) + paths[path][method.lower()] = operation + + # Compile final schema. schema = { 'openapi': '3.0.2', 'info': self.get_info(), From b1048984a7a839234ca604d199edbc9985c8a059 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Thu, 9 Jan 2020 21:07:52 -0500 Subject: [PATCH 37/40] Add failing test for `ListField` schema generation The `ListField` was generating a schema that contained `type=None` when a `ChoiceField` was the child, since we are not currently able to introspect the type of a `ChoiceField`. --- tests/schemas/test_openapi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 8a723b85d..6ad47359e 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -52,6 +52,8 @@ class TestFieldMapping(TestCase): (serializers.ListField(child=serializers.CharField()), {'items': {'type': 'string'}, 'type': 'array'}), (serializers.ListField(child=serializers.IntegerField(max_value=4294967295)), {'items': {'type': 'integer', 'format': 'int64'}, 'type': 'array'}), + (serializers.ListField(child=serializers.ChoiceField(choices=[('a', 'Choice A'), ('b', 'Choice B')])), + {'items': {'enum': ['a', 'b']}, 'type': 'array'}), (serializers.IntegerField(min_value=2147483648), {'type': 'integer', 'minimum': 2147483648, 'format': 'int64'}), ] From 98c8af5291ac366b3030c4091284091ca63943ac Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Thu, 9 Jan 2020 20:43:44 -0500 Subject: [PATCH 38/40] `ListField` mapping should map all options for the child Previously it was only mapping the `type` and `format`, even though for some field types (like a `MultipleChoiceField`) we map more than just these. And for some fields (like a `ChoiceField`) we do not map the `type` at all. --- rest_framework/schemas/openapi.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index 1df132ce3..cb5ea95a3 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -259,13 +259,7 @@ class AutoSchema(ViewInspector): 'items': {}, } if not isinstance(field.child, _UnvalidatedField): - map_field = self._map_field(field.child) - items = { - "type": map_field.get('type') - } - if 'format' in map_field: - items['format'] = map_field.get('format') - mapping['items'] = items + mapping['items'] = self._map_field(field.child) return mapping # DateField and DateTimeField type is string From f8f8b3a1f1c1463d1836dc2e9f6614460d03fed4 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Thu, 9 Jan 2020 21:31:54 -0500 Subject: [PATCH 39/40] Adjust test for ListField(IntegerField) The `maximum` is valid here within the schema but it was not previously being included because we were not copying over the entire schema for the generated `IntegerField` previously. --- tests/schemas/test_openapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/schemas/test_openapi.py b/tests/schemas/test_openapi.py index 6ad47359e..f734fd169 100644 --- a/tests/schemas/test_openapi.py +++ b/tests/schemas/test_openapi.py @@ -51,7 +51,7 @@ class TestFieldMapping(TestCase): (serializers.ListField(child=serializers.FloatField()), {'items': {'type': 'number'}, 'type': 'array'}), (serializers.ListField(child=serializers.CharField()), {'items': {'type': 'string'}, 'type': 'array'}), (serializers.ListField(child=serializers.IntegerField(max_value=4294967295)), - {'items': {'type': 'integer', 'format': 'int64'}, 'type': 'array'}), + {'items': {'type': 'integer', 'maximum': 4294967295, 'format': 'int64'}, 'type': 'array'}), (serializers.ListField(child=serializers.ChoiceField(choices=[('a', 'Choice A'), ('b', 'Choice B')])), {'items': {'enum': ['a', 'b']}, 'type': 'array'}), (serializers.IntegerField(min_value=2147483648), From e4a26ad58a0e3d13e7cb788b724398592b1543b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Domingues?= Date: Thu, 23 Jan 2020 14:53:47 +0000 Subject: [PATCH 40/40] Corrected _get_serializer() argument order. (#7156) --- rest_framework/schemas/openapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/schemas/openapi.py b/rest_framework/schemas/openapi.py index cb5ea95a3..3a7eb29a7 100644 --- a/rest_framework/schemas/openapi.py +++ b/rest_framework/schemas/openapi.py @@ -449,7 +449,7 @@ class AutoSchema(ViewInspector): media_types.append(renderer.media_type) return media_types - def _get_serializer(self, method, path): + def _get_serializer(self, path, method): view = self.view if not hasattr(view, 'get_serializer'):