diff --git a/README.md b/README.md index 6bd03d11b..921f50c8a 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,15 @@ continued development by [signing up for a paid plan][funding]. The initial aim is to provide a single full-time position on REST framework. *Every single sign-up makes a significant impact towards making that possible.* -[![][rover-img]][rover-url] [![][sentry-img]][sentry-url] [![][stream-img]][stream-url] [![][rollbar-img]][rollbar-url] [![][cadre-img]][cadre-url] -[![][load-impact-img]][load-impact-url] [![][kloudless-img]][kloudless-url] -[![][auklet-img]][auklet-url] +[![][release-history-img]][release-history-url] [![][lightson-img]][lightson-url] -Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover][rover-url], [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [Cadre][cadre-url], [Load Impact][load-impact-url], [Kloudless][kloudless-url], [Auklet][auklet-url], and [Lights On Software][lightson-url]. +Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Sentry][sentry-url], [Stream][stream-url], [Rollbar][rollbar-url], [Cadre][cadre-url], [Kloudless][kloudless-url], [Release History][release-history-url], and [Lights On Software][lightson-url]. --- @@ -202,7 +200,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou [cadre-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/cadre-readme.png [load-impact-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/load-impact-readme.png [kloudless-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/kloudless-readme.png -[auklet-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/auklet-readme.png +[release-history-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/release-history.png [lightson-img]: https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/lightson-readme.png [rover-url]: http://jobs.rover.com/ @@ -212,7 +210,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou [cadre-url]: https://cadre.com/ [load-impact-url]: https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf [kloudless-url]: https://hubs.ly/H0f30Lf0 -[auklet-url]: https://auklet.io/ +[release-history-url]: https://releasehistory.io [lightson-url]: https://lightsonsoftware.com [oauth1-section]: https://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 74ce2251d..ede4f15ad 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -306,10 +306,11 @@ A date and time representation. Corresponds to `django.db.models.fields.DateTimeField`. -**Signature:** `DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None)` +**Signature:** `DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None, default_timezone=None)` * `format` - A string representing the output format. If not specified, this defaults to the same value as the `DATETIME_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `datetime` objects should be returned by `to_representation`. In this case the datetime encoding will be determined by the renderer. * `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`. +* `default_timezone` - A `pytz.timezone` representing the timezone. If not specified and the `USE_TZ` setting is enabled, this defaults to the [current timezone][django-current-timezone]. If `USE_TZ` is disabled, then datetime objects will be naive. #### `DateTimeField` format strings. @@ -835,3 +836,4 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide [django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore [django-hstore]: https://github.com/djangonauts/django-hstore [python-decimal-rounding-modes]: https://docs.python.org/3/library/decimal.html#rounding-modes +[django-current-timezone]: https://docs.djangoproject.com/en/stable/topics/i18n/timezones/#default-time-zone-and-current-time-zone diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index aff267818..8a500f386 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -220,10 +220,13 @@ By default, the search parameter is named `'search`', but this may be overridden To dynamically change search fields based on request content, it's possible to subclass the `SearchFilter` and override the `get_search_fields()` function. For example, the following subclass will only search on `title` if the query parameter `title_only` is in the request: - class CustomSearchFilter(self, view, request): - if request.query_params.get('title_only'): - return ('title',) - return super(CustomSearchFilter, self).get_search_fields(view, request) + from rest_framework import filters + + class CustomSearchFilter(filters.SearchFilter): + def get_search_fields(self, view, request): + if request.query_params.get('title_only'): + return ('title',) + return super(CustomSearchFilter, self).get_search_fields(view, request) For more details, see the [Django documentation][search-django-admin]. diff --git a/docs/community/release-notes.md b/docs/community/release-notes.md index 288bf6d58..0f08342f5 100644 --- a/docs/community/release-notes.md +++ b/docs/community/release-notes.md @@ -62,7 +62,7 @@ You can determine your currently installed version using `pip show`: ### 3.9.1 -**Date**: [16th Janurary 2019][3.9.1-milestone] +**Date**: [16th January 2019][3.9.1-milestone] * Resolve XSS issue in browsable API. [#6330][gh6330] * Upgrade Bootstrap to 3.4.0 to resolve XSS issue. diff --git a/docs/img/premium/auklet-readme.png b/docs/img/premium/auklet-readme.png deleted file mode 100644 index f55f7a70e..000000000 Binary files a/docs/img/premium/auklet-readme.png and /dev/null differ diff --git a/docs/img/premium/release-history.png b/docs/img/premium/release-history.png new file mode 100644 index 000000000..b732b1ca2 Binary files /dev/null and b/docs/img/premium/release-history.png differ diff --git a/docs/index.md b/docs/index.md index c74b2caf0..9f5d3fa15 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,19 +66,17 @@ 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, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Auklet](https://auklet.io/), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Load Impact](https://loadimpact.com/?utm_campaign=Sponsorship%20links&utm_source=drf&utm_medium=drf), [Kloudless](https://hubs.ly/H0f30Lf0), and [Lights On Software](https://lightsonsoftware.com).* +*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), [Release History](https://releasehistory.io), [Rollbar](https://rollbar.com), [Cadre](https://cadre.com), [Kloudless](https://hubs.ly/H0f30Lf0), and [Lights On Software](https://lightsonsoftware.com).* --- diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index ec507df05..07ee8f208 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -150,7 +150,7 @@ At this point we've translated the model instance into Python native datatypes. content = JSONRenderer().render(serializer.data) content - # '{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}' + # b'{"id": 2, "title": "", "code": "print(\\"hello, world\\")\\n", "linenos": false, "language": "python", "style": "friendly"}' Deserialization is similar. First we parse a stream into Python native datatypes... diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt index fbddc4f20..a2a2fa753 100644 --- a/requirements/requirements-testing.txt +++ b/requirements/requirements-testing.txt @@ -1,4 +1,4 @@ # Pytest for running the tests. -pytest==3.6.2 -pytest-django==3.3.2 -pytest-cov==2.5.1 +pytest==4.3.0 +pytest-django==3.4.8 +pytest-cov==2.6.1 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1b3fe4230..c14a5a223 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1484,7 +1484,7 @@ class MultipleChoiceField(ChoiceField): return dictionary.get(self.field_name, empty) def to_internal_value(self, data): - if isinstance(data, type('')) or not hasattr(data, '__iter__'): + if isinstance(data, six.text_type) or not hasattr(data, '__iter__'): self.fail('not_a_list', input_type=type(data).__name__) if not self.allow_empty and len(data) == 0: self.fail('empty') @@ -1658,7 +1658,7 @@ class ListField(Field): """ if html.is_html_input(data): data = html.parse_html_list(data, default=[]) - if isinstance(data, (type(''), Mapping)) or not hasattr(data, '__iter__'): + if isinstance(data, (six.text_type, Mapping)) or not hasattr(data, '__iter__'): self.fail('not_a_list', input_type=type(data).__name__) if not self.allow_empty and len(data) == 0: self.fail('empty') diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 4c7f9ed5d..27e29676d 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -512,7 +512,7 @@ class ManyRelatedField(Field): return dictionary.get(self.field_name, empty) def to_internal_value(self, data): - if isinstance(data, type('')) or not hasattr(data, '__iter__'): + if isinstance(data, six.text_type) or not hasattr(data, '__iter__'): self.fail('not_a_list', input_type=type(data).__name__) if not self.allow_empty and len(data) == 0: self.fail('empty') diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py index 6e967d314..56b04d8df 100644 --- a/rest_framework/schemas/inspectors.py +++ b/rest_framework/schemas/inspectors.py @@ -50,8 +50,10 @@ def field_to_schema(field): description=description ) elif isinstance(field, serializers.ManyRelatedField): + related_field_schema = field_to_schema(field.child_relation) + return coreschema.Array( - items=coreschema.String(), + items=related_field_schema, title=title, description=description ) diff --git a/rest_framework/views.py b/rest_framework/views.py index ccc602982..98e651776 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -462,7 +462,7 @@ class APIView(View): renderer_format = getattr(request.accepted_renderer, 'format') use_plaintext_traceback = renderer_format not in ('html', 'api', 'admin') request.force_plaintext_errors(use_plaintext_traceback) - raise + raise exc # Note: Views are made CSRF exempt from within `as_view` as to prevent # accidental removal of this exemption in cases where `dispatch` needs to diff --git a/tests/test_pagination.py b/tests/test_pagination.py index e1804c2a1..26510fe98 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -2,6 +2,7 @@ import pytest from django.core.paginator import Paginator as DjangoPaginator from django.db import models from django.test import TestCase +from django.utils import six from rest_framework import ( exceptions, filters, generics, pagination, serializers, status @@ -204,7 +205,7 @@ class TestPageNumberPagination: ] } assert self.pagination.display_page_controls - assert isinstance(self.pagination.to_html(), type('')) + assert isinstance(self.pagination.to_html(), six.text_type) def test_second_page(self): request = Request(factory.get('/', {'page': 2})) @@ -310,7 +311,7 @@ class TestPageNumberPaginationOverride: ] } assert not self.pagination.display_page_controls - assert isinstance(self.pagination.to_html(), type('')) + assert isinstance(self.pagination.to_html(), six.text_type) def test_invalid_page(self): request = Request(factory.get('/', {'page': 'invalid'})) @@ -365,7 +366,7 @@ class TestLimitOffset: ] } assert self.pagination.display_page_controls - assert isinstance(self.pagination.to_html(), type('')) + assert isinstance(self.pagination.to_html(), six.text_type) def test_pagination_not_applied_if_limit_or_default_limit_not_set(self): class MockPagination(pagination.LimitOffsetPagination): @@ -628,7 +629,7 @@ class CursorPaginationTestsMixin: assert current == [1, 1, 1, 1, 1] assert next == [1, 2, 3, 4, 4] - assert isinstance(self.pagination.to_html(), type('')) + assert isinstance(self.pagination.to_html(), six.text_type) def test_cursor_pagination_with_page_size(self): (previous, current, next, previous_url, next_url) = self.get_pages('/?page_size=20') diff --git a/tests/test_schemas.py b/tests/test_schemas.py index f99eb55d1..fc7a8302d 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -24,7 +24,7 @@ from rest_framework.utils import formatting from rest_framework.views import APIView from rest_framework.viewsets import GenericViewSet, ModelViewSet -from .models import BasicModel, ForeignKeySource +from .models import BasicModel, ForeignKeySource, ManyToManySource factory = APIRequestFactory() @@ -701,6 +701,51 @@ class TestSchemaGeneratorWithForeignKey(TestCase): assert schema == expected +class ManyToManySourceSerializer(serializers.ModelSerializer): + class Meta: + model = ManyToManySource + fields = ('id', 'name', 'targets') + + +class ManyToManySourceView(generics.CreateAPIView): + queryset = ManyToManySource.objects.all() + serializer_class = ManyToManySourceSerializer + + +@unittest.skipUnless(coreapi, 'coreapi is not installed') +class TestSchemaGeneratorWithManyToMany(TestCase): + def setUp(self): + self.patterns = [ + url(r'^example/?$', ManyToManySourceView.as_view()), + ] + + def test_schema_for_regular_views(self): + """ + Ensure that AutoField many to many fields are output as Integer. + """ + generator = SchemaGenerator(title='Example API', patterns=self.patterns) + schema = generator.get_schema() + + expected = coreapi.Document( + url='', + title='Example API', + content={ + 'example': { + 'create': coreapi.Link( + url='/example/', + action='post', + encoding='application/json', + fields=[ + coreapi.Field('name', required=True, location='form', schema=coreschema.String(title='Name')), + coreapi.Field('targets', required=True, location='form', schema=coreschema.Array(title='Targets', items=coreschema.Integer())), + ] + ) + } + } + ) + assert schema == expected + + @unittest.skipUnless(coreapi, 'coreapi is not installed') class Test4605Regression(TestCase): def test_4605_regression(self): diff --git a/tests/test_validation.py b/tests/test_validation.py index eeac44bbd..b2d611e53 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -3,6 +3,7 @@ import re from django.core.validators import MaxValueValidator, RegexValidator from django.db import models from django.test import TestCase +from django.utils import six from rest_framework import generics, serializers, status from rest_framework.test import APIRequestFactory @@ -109,7 +110,7 @@ class TestAvoidValidation(TestCase): assert not serializer.is_valid() assert serializer.errors == { 'non_field_errors': [ - 'Invalid data. Expected a dictionary, but got %s.' % type('').__name__ + 'Invalid data. Expected a dictionary, but got %s.' % six.text_type.__name__ ] }