diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 58bf286f6..b52dd90d9 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -165,7 +165,7 @@ If all you need is simple equality-based filtering, you can set a `filter_fields class ProductList(generics.ListAPIView): queryset = Product.objects.all() serializer_class = ProductSerializer - filter_backends = (filters.DjangoFilterBackend,) + filter_backends = (DjangoFilterBackend,) filter_fields = ('category', 'in_stock') This will automatically create a `FilterSet` class for the given fields, and will allow you to make requests such as: diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index a1666c011..a8fc6afef 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -51,7 +51,7 @@ Typically we wouldn't do this, but would instead register the viewset with a rou from rest_framework.routers import DefaultRouter router = DefaultRouter() - router.register(r'users', UserViewSet) + router.register(r'users', UserViewSet, base_name='user') urlpatterns = router.urls Rather than writing your own viewsets, you'll often want to use the existing base classes that provide a default set of behavior. For example: diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index dd7054f75..7bdd7b0b1 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,33 @@ You can determine your currently installed version using `pip freeze`: ## 3.6.x series +### 3.6.3 + +**Date**: [12th May 2017][3.6.3-milestone] + +* Raise 404 if a URL lookup results in ValidationError. ([#5126][gh5126]) +* Honor http_method_names on class based view, when generating API schemas. ([#5085][gh5085]) +* Allow overridden `get_limit` in LimitOffsetPagination to return all records. ([#4437][gh4437]) +* Fix partial update for the ListSerializer. ([#4222][gh4222]) +* Render JSONField control correctly in browsable API. ([#4999][gh4999], [#5042][gh5042]) +* Raise validation errors for invalid datetime in given timezone. ([#4987][gh4987]) +* Support restricting doc & schema shortcuts to a subset of urls. ([#4979][gh4979]) +* Resolve SchemaGenerator error with paginators that have no `page_size` attribute. ([#5086][gh5086], [#3692][gh3692]) +* Resolve HyperlinkedRelatedField exception on string with %20 instead of space. ([#4748][gh4748], [#5078][gh5078]) +* Customizable schema generator classes. ([#5082][gh5082]) +* Update existing vary headers in response instead of overwriting them. ([#5047][gh5047]) +* Support passing `.as_view()` to view instance. ([#5053][gh5053]) +* Use correct exception handler when settings overridden on a view. ([#5055][gh5055], [#5054][gh5054]) +* Update Boolean field to support 'yes' and 'no' values. ([#5038][gh5038]) +* Fix unique validator for ChoiceField. ([#5004][gh5004], [#5026][gh5026], [#5028][gh5028]) +* JavaScript cleanups in API Docs. ([#5001][gh5001]) +* Include URL path regexs in API schemas where valid. ([#5014][gh5014]) +* Correctly set scheme in coreapi TokenAuthentication. ([#5000][gh5000], [#4994][gh4994]) +* HEAD requests on ViewSets should not return 405. ([#4705][gh4705], [#4973][gh4973], [#4864][gh4864]) +* Support usage of 'source' in `extra_kwargs`. ([#4688][gh4688]) +* Fix invalid content type for schema.js ([#4968][gh4968]) +* Fix DjangoFilterBackend inheritance issues. ([#5089][gh5089], [#5117][gh5117]) + ### 3.6.2 **Date**: [10th March 2017][3.6.2-milestone] @@ -688,6 +715,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.6.0-milestone]: https://github.com/encode/django-rest-framework/issues?q=milestone%3A%223.6.0+Release%22 [3.6.1-milestone]: https://github.com/encode/django-rest-framework/issues?q=milestone%3A%223.6.1+Release%22 [3.6.2-milestone]: https://github.com/encode/django-rest-framework/issues?q=milestone%3A%223.6.2+Release%22 +[3.6.3-milestone]: https://github.com/encode/django-rest-framework/issues?q=milestone%3A%223.6.3+Release%22 [gh2013]: https://github.com/encode/django-rest-framework/issues/2013 @@ -1298,3 +1326,37 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh4955]: https://github.com/encode/django-rest-framework/issues/4955 [gh4956]: https://github.com/encode/django-rest-framework/issues/4956 [gh4949]: https://github.com/encode/django-rest-framework/issues/4949 + + +[gh5126]: https://github.com/encode/django-rest-framework/issues/5126 +[gh5085]: https://github.com/encode/django-rest-framework/issues/5085 +[gh4437]: https://github.com/encode/django-rest-framework/issues/4437 +[gh4222]: https://github.com/encode/django-rest-framework/issues/4222 +[gh4999]: https://github.com/encode/django-rest-framework/issues/4999 +[gh5042]: https://github.com/encode/django-rest-framework/issues/5042 +[gh4987]: https://github.com/encode/django-rest-framework/issues/4987 +[gh4979]: https://github.com/encode/django-rest-framework/issues/4979 +[gh5086]: https://github.com/encode/django-rest-framework/issues/5086 +[gh3692]: https://github.com/encode/django-rest-framework/issues/3692 +[gh4748]: https://github.com/encode/django-rest-framework/issues/4748 +[gh5078]: https://github.com/encode/django-rest-framework/issues/5078 +[gh5082]: https://github.com/encode/django-rest-framework/issues/5082 +[gh5047]: https://github.com/encode/django-rest-framework/issues/5047 +[gh5053]: https://github.com/encode/django-rest-framework/issues/5053 +[gh5055]: https://github.com/encode/django-rest-framework/issues/5055 +[gh5054]: https://github.com/encode/django-rest-framework/issues/5054 +[gh5038]: https://github.com/encode/django-rest-framework/issues/5038 +[gh5004]: https://github.com/encode/django-rest-framework/issues/5004 +[gh5026]: https://github.com/encode/django-rest-framework/issues/5026 +[gh5028]: https://github.com/encode/django-rest-framework/issues/5028 +[gh5001]: https://github.com/encode/django-rest-framework/issues/5001 +[gh5014]: https://github.com/encode/django-rest-framework/issues/5014 +[gh5000]: https://github.com/encode/django-rest-framework/issues/5000 +[gh4994]: https://github.com/encode/django-rest-framework/issues/4994 +[gh4705]: https://github.com/encode/django-rest-framework/issues/4705 +[gh4973]: https://github.com/encode/django-rest-framework/issues/4973 +[gh4864]: https://github.com/encode/django-rest-framework/issues/4864 +[gh4688]: https://github.com/encode/django-rest-framework/issues/4688 +[gh4968]: https://github.com/encode/django-rest-framework/issues/4968 +[gh5089]: https://github.com/encode/django-rest-framework/issues/5089 +[gh5117]: https://github.com/encode/django-rest-framework/issues/5117 diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 83fe6d955..e8ba50851 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,6 +1,6 @@ # Optional packages which may be used with REST framework. markdown==2.6.4 django-guardian==1.4.8 -django-filter==1.0.2 +django-filter==1.0.4 coreapi==2.2.4 coreschema==0.0.4 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index edcf9b52d..c0b5c4c04 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.6.2' +__version__ = '3.6.3' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2017 Tom Christie' diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index b91a8454f..7590fdb75 100644 --- a/rest_framework/authtoken/serializers.py +++ b/rest_framework/authtoken/serializers.py @@ -6,7 +6,11 @@ from rest_framework import serializers class AuthTokenSerializer(serializers.Serializer): username = serializers.CharField(label=_("Username")) - password = serializers.CharField(label=_("Password"), style={'input_type': 'password'}) + password = serializers.CharField( + label=_("Password"), + style={'input_type': 'password'}, + trim_whitespace=False + ) def validate(self, attrs): username = attrs.get('username') diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 429b79c77..aea9d3a57 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -11,6 +11,7 @@ from functools import reduce from django.core.exceptions import ImproperlyConfigured from django.db import models from django.db.models.constants import LOOKUP_SEP +from django.db.models.sql.constants import ORDER_PATTERN from django.template import loader from django.utils import six from django.utils.encoding import force_text @@ -268,7 +269,7 @@ class OrderingFilter(BaseFilterBackend): def remove_invalid_fields(self, queryset, fields, view, request): valid_fields = [item[0] for item in self.get_valid_fields(queryset, view, {'request': request})] - return [term for term in fields if term.lstrip('-') in valid_fields] + return [term for term in fields if term.lstrip('-') in valid_fields and ORDER_PATTERN.match(term)] def filter_queryset(self, request, queryset, view): ordering = self.get_ordering(request, queryset, view) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index afab8f71f..875f9454b 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -606,7 +606,7 @@ class SchemaGenerator(object): return [] pagination = getattr(view, 'pagination_class', None) - if not pagination or not getattr(pagination, 'page_size', None): + if not pagination: return [] paginator = view.pagination_class() diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5bd9b6473..e27610178 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1010,7 +1010,7 @@ class ModelSerializer(Serializer): continue extra_field_kwargs = extra_kwargs.get(field_name, {}) - source = extra_field_kwargs.get('source') or field_name + source = extra_field_kwargs.get('source', '*') != '*' or field_name # Determine the serializer field class and keyword arguments. field_class, field_kwargs = self.build_field( diff --git a/rest_framework/static/rest_framework/docs/js/api.js b/rest_framework/static/rest_framework/docs/js/api.js index 0a4fb7e6c..045e95edd 100644 --- a/rest_framework/static/rest_framework/docs/js/api.js +++ b/rest_framework/static/rest_framework/docs/js/api.js @@ -23,7 +23,7 @@ function formEntries (form) { // Polyfill for new FormData(form).entries() var formData = new FormData(form) if (formData.entries !== undefined) { - return formData.entries() + return Array.from(formData.entries()) } var entries = [] @@ -59,6 +59,8 @@ $(function () { var $selectedAuthentication = $('#selected-authentication') var $authControl = $('#auth-control') var $authTokenModal = $('#auth_token_modal') + var $authBasicModal = $('#auth_basic_modal') + var $authSessionModal = $('#auth_session_modal') // Language Control $('#language-control li').click(function (event) { @@ -260,8 +262,8 @@ $(function () { event.preventDefault() window.auth = null $selectedAuthentication.text('none') - $authControl.children().removeClass('active') - $authControl.find("[data-auth='none']").addClass('active') + $authControl.find("[data-auth]").closest('li').removeClass('active') + $authControl.find("[data-auth='none']").closest('li').addClass('active') }) // Authentication: token @@ -276,8 +278,8 @@ $(function () { 'token': token } $selectedAuthentication.text('token') - $authControl.children().removeClass('active') - $authControl.find("[data-auth='token']").addClass('active') + $authControl.find("[data-auth]").closest('li').removeClass('active') + $authControl.find("[data-auth='token']").closest('li').addClass('active') $authTokenModal.modal('hide') }) @@ -293,9 +295,9 @@ $(function () { 'password': password } $selectedAuthentication.text('basic') - $authControl.children().removeClass('active') - $authControl.find("[data-auth='basic']").addClass('active') - $authTokenModal.modal('hide') + $authControl.find("[data-auth]").closest('li').removeClass('active') + $authControl.find("[data-auth='basic']").closest('li').addClass('active') + $authBasicModal.modal('hide') }) // Authentication: session @@ -305,8 +307,8 @@ $(function () { 'type': 'session' } $selectedAuthentication.text('session') - $authControl.children().removeClass('active') - $authControl.find("[data-auth='session']").addClass('active') - $authTokenModal.modal('hide') + $authControl.find("[data-auth]").closest('li').removeClass('active') + $authControl.find("[data-auth='session']").closest('li').addClass('active') + $authSessionModal.modal('hide') }) }) diff --git a/rest_framework/templates/rest_framework/docs/index.html b/rest_framework/templates/rest_framework/docs/index.html index fd3d61820..84ef03749 100644 --- a/rest_framework/templates/rest_framework/docs/index.html +++ b/rest_framework/templates/rest_framework/docs/index.html @@ -49,7 +49,7 @@ }; $('#selected-authentication').text('session'); $('#auth-control').children().removeClass('active'); - $('#auth-control').find("[data-auth='session']").addClass('active'); + $('#auth-control').find("[data-auth='session']").closest('li').addClass('active'); {% endif %} diff --git a/runtests.py b/runtests.py index e97ac0367..5e8460c85 100755 --- a/runtests.py +++ b/runtests.py @@ -12,7 +12,7 @@ PYTEST_ARGS = { 'fast': ['tests', '--tb=short', '-q', '-s', '-rw'], } -FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501'] +FLAKE8_ARGS = ['rest_framework', 'tests'] ISORT_ARGS = ['--recursive', '--check-only', '-o' 'uritemplate', '-p', 'tests', 'rest_framework', 'tests'] diff --git a/setup.cfg b/setup.cfg index fd8b0682b..509abd58b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,6 @@ universal = 1 [metadata] license_file = LICENSE.md + +[flake8] +ignore = E501 diff --git a/tests/test_authtoken.py b/tests/test_authtoken.py index 04eeb2f63..54ac1848d 100644 --- a/tests/test_authtoken.py +++ b/tests/test_authtoken.py @@ -27,3 +27,9 @@ class AuthTokenTests(TestCase): def test_validate_raise_error_if_no_credentials_provided(self): with pytest.raises(ValidationError): AuthTokenSerializer().validate({}) + + def test_whitespace_in_password(self): + data = {'username': self.user.username, 'password': 'test pass '} + self.user.set_password(data['password']) + self.user.save() + assert AuthTokenSerializer(data=data).is_valid() diff --git a/tests/test_filters.py b/tests/test_filters.py index d2c11d258..b2de80998 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -764,6 +764,23 @@ class OrderingFilterTests(TestCase): {'id': 1, 'title': 'zyx', 'text': 'abc'}, ] + def test_incorrecturl_extrahyphens_ordering(self): + class OrderingListView(generics.ListAPIView): + queryset = OrderingFilterModel.objects.all() + serializer_class = OrderingFilterSerializer + filter_backends = (filters.OrderingFilter,) + ordering = ('title',) + ordering_fields = ('text',) + + view = OrderingListView.as_view() + request = factory.get('/', {'ordering': '--text'}) + response = view(request) + assert response.data == [ + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + ] + def test_incorrectfield_ordering(self): class OrderingListView(generics.ListAPIView): queryset = OrderingFilterModel.objects.all() @@ -883,6 +900,7 @@ class OrderingFilterTests(TestCase): queryset = OrderingFilterModel.objects.all() filter_backends = (filters.OrderingFilter,) ordering = ('title',) + # note: no ordering_fields and serializer_class specified def get_serializer_class(self): diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 08fc3b42d..4aa0fa35e 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -524,6 +524,37 @@ class TestRelationalFieldMappings(TestCase): """) self.assertEqual(unicode_repr(TestSerializer()), expected) + def test_nested_hyperlinked_relations_starred_source(self): + class TestSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = RelationalModel + depth = 1 + fields = '__all__' + + extra_kwargs = { + 'url': { + 'source': '*', + }} + + expected = dedent(""" + TestSerializer(): + url = HyperlinkedIdentityField(source='*', view_name='relationalmodel-detail') + foreign_key = NestedSerializer(read_only=True): + url = HyperlinkedIdentityField(view_name='foreignkeytargetmodel-detail') + name = CharField(max_length=100) + one_to_one = NestedSerializer(read_only=True): + url = HyperlinkedIdentityField(view_name='onetoonetargetmodel-detail') + name = CharField(max_length=100) + many_to_many = NestedSerializer(many=True, read_only=True): + url = HyperlinkedIdentityField(view_name='manytomanytargetmodel-detail') + name = CharField(max_length=100) + through = NestedSerializer(many=True, read_only=True): + url = HyperlinkedIdentityField(view_name='throughtargetmodel-detail') + name = CharField(max_length=100) + """) + self.maxDiff = None + self.assertEqual(unicode_repr(TestSerializer()), expected) + def test_nested_unique_together_relations(self): class TestSerializer(serializers.HyperlinkedModelSerializer): class Meta: