From f02b7f1329012aafca3851df3340b719c28d4586 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 10 Jul 2017 14:20:23 -0400 Subject: [PATCH 01/20] Add failing test for #4655 --- tests/test_filters.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_filters.py b/tests/test_filters.py index b2de80998..f803d0957 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -645,6 +645,48 @@ class SearchFilterM2MTests(TestCase): ) +class Blog(models.Model): + name = models.CharField(max_length=20) + + +class Entry(models.Model): + blog = models.ForeignKey(Blog, on_delete=models.CASCADE) + headline = models.CharField(max_length=120) + pub_date = models.DateField(null=True) + + +class BlogSerializer(serializers.ModelSerializer): + class Meta: + model = Blog + fields = '__all__' + + +class SearchFilterToManyTests(TestCase): + + @classmethod + def setUpTestData(cls): + b1 = Blog.objects.create(name='Blog 1') + b2 = Blog.objects.create(name='Blog 2') + + Entry.objects.create(blog=b1, headline='Something about Lennon', pub_date=datetime.date(1979, 1, 1)) + Entry.objects.create(blog=b1, headline='Another thing about Lennon', pub_date=datetime.date(1979, 6, 1)) + + Entry.objects.create(blog=b2, headline='Something unrelated', pub_date=datetime.date(1979, 1, 1)) + Entry.objects.create(blog=b2, headline='Retrospective on Lennon', pub_date=datetime.date(1990, 6, 1)) + + def test_multiple_filter_conditions(self): + class SearchListView(generics.ListAPIView): + queryset = Blog.objects.all() + serializer_class = BlogSerializer + filter_backends = (filters.SearchFilter,) + search_fields = ('=name', 'entry__headline', '=entry__pub_date__year') + + view = SearchListView.as_view() + request = factory.get('/', {'search': 'Lennon,1979'}) + response = view(request) + assert len(response.data) == 1 + + class OrderingFilterModel(models.Model): title = models.CharField(max_length=20, verbose_name='verbose title') text = models.CharField(max_length=100) From d1cfec8d871a473f4c1e4b45ead959b07d813988 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 10 Jul 2017 14:24:58 -0400 Subject: [PATCH 02/20] Fix SearchFilter to-many behavior by ANDing cond's --- rest_framework/filters.py | 4 +++- tests/test_filters.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index bdab97b58..63ebf05ef 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -140,12 +140,14 @@ class SearchFilter(BaseFilterBackend): ] base = queryset + conditions = [] for search_term in search_terms: queries = [ models.Q(**{orm_lookup: search_term}) for orm_lookup in orm_lookups ] - queryset = queryset.filter(reduce(operator.or_, queries)) + conditions.append(reduce(operator.or_, queries)) + queryset = queryset.filter(reduce(operator.and_, conditions)) if self.must_call_distinct(queryset, search_fields): # Filtering against a many-to-many field requires us to diff --git a/tests/test_filters.py b/tests/test_filters.py index f803d0957..6df0a3169 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -5,6 +5,7 @@ import unittest import warnings from decimal import Decimal +import django import pytest from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured @@ -668,12 +669,15 @@ class SearchFilterToManyTests(TestCase): b1 = Blog.objects.create(name='Blog 1') b2 = Blog.objects.create(name='Blog 2') + # Multiple entries on Lennon published in 1979 - distinct should deduplicate Entry.objects.create(blog=b1, headline='Something about Lennon', pub_date=datetime.date(1979, 1, 1)) Entry.objects.create(blog=b1, headline='Another thing about Lennon', pub_date=datetime.date(1979, 6, 1)) + # Entry on Lennon *and* a separate entry in 1979 - should not match Entry.objects.create(blog=b2, headline='Something unrelated', pub_date=datetime.date(1979, 1, 1)) Entry.objects.create(blog=b2, headline='Retrospective on Lennon', pub_date=datetime.date(1990, 6, 1)) + @unittest.skipIf(django.VERSION < (1, 9), "Django 1.8 does not support transforms") def test_multiple_filter_conditions(self): class SearchListView(generics.ListAPIView): queryset = Blog.objects.all() From 2a1fd3b45a2d1f5fa61056fcf151f9345e6a3327 Mon Sep 17 00:00:00 2001 From: Tommy Beadle Date: Wed, 12 Jul 2017 11:45:41 -0400 Subject: [PATCH 03/20] Add link to third-party package for LinkHeaderPagination. (#5270) --- docs/api-guide/pagination.md | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 888390018..d767e45de 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -242,29 +242,6 @@ We'd then need to setup the custom class in our configuration: Note that if you care about how the ordering of keys is displayed in responses in the browsable API you might choose to use an `OrderedDict` when constructing the body of paginated responses, but this is optional. -## Header based pagination - -Let's modify the built-in `PageNumberPagination` style, so that instead of include the pagination links in the body of the response, we'll instead include a `Link` header, in a [similar style to the GitHub API][github-link-pagination]. - - class LinkHeaderPagination(pagination.PageNumberPagination): - def get_paginated_response(self, data): - next_url = self.get_next_link() - previous_url = self.get_previous_link() - - if next_url is not None and previous_url is not None: - link = '<{next_url}>; rel="next", <{previous_url}>; rel="prev"' - elif next_url is not None: - link = '<{next_url}>; rel="next"' - elif previous_url is not None: - link = '<{previous_url}>; rel="prev"' - else: - link = '' - - link = link.format(next_url=next_url, previous_url=previous_url) - headers = {'Link': link} if link else {} - - return Response(data, headers=headers) - ## Using your custom pagination class To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting: @@ -328,10 +305,15 @@ The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagination` class which allows to choose pagination class with a query parameter. +## link-header-pagination + +The [`django-rest-framework-link-header-pagination` package][drf-link-header-pagination] includes a `LinkHeaderPagination` class which provides pagination via an HTTP `Link` header as desribed in [Github's developer documentation](github-link-pagination). + [cite]: https://docs.djangoproject.com/en/stable/topics/pagination/ [github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/ [link-header]: ../img/link-header-pagination.png [drf-extensions]: http://chibisov.github.io/drf-extensions/docs/ [paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin [drf-proxy-pagination]: https://github.com/tuffnatty/drf-proxy-pagination +[drf-link-header-pagination]: https://github.com/tbeadle/django-rest-framework-link-header-pagination [disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api From 089887d56e4ff7584ce4e65ed490029a55bd8616 Mon Sep 17 00:00:00 2001 From: Erick Delfin Date: Sun, 16 Jul 2017 09:12:29 -0700 Subject: [PATCH 04/20] Simplified chained comparisons and minor code fixes (#5276) --- rest_framework/pagination.py | 4 ++-- rest_framework/status.py | 10 +++++----- rest_framework/utils/formatting.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 0255cfc7f..a4a5230ef 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -31,7 +31,7 @@ def _positive_int(integer_string, strict=False, cutoff=None): if ret < 0 or (ret == 0 and strict): raise ValueError() if cutoff: - ret = min(ret, cutoff) + return min(ret, cutoff) return ret @@ -95,7 +95,7 @@ def _get_displayed_page_numbers(current, final): # Now sort the page numbers and drop anything outside the limits. included = [ idx for idx in sorted(list(included)) - if idx > 0 and idx <= final + if 0 < idx <= final ] # Finally insert any `...` breaks diff --git a/rest_framework/status.py b/rest_framework/status.py index c016b63c6..d4522df3d 100644 --- a/rest_framework/status.py +++ b/rest_framework/status.py @@ -9,23 +9,23 @@ from __future__ import unicode_literals def is_informational(code): - return code >= 100 and code <= 199 + return 100 <= code <= 199 def is_success(code): - return code >= 200 and code <= 299 + return 200 <= code <= 299 def is_redirect(code): - return code >= 300 and code <= 399 + return 300 <= code <= 399 def is_client_error(code): - return code >= 400 and code <= 499 + return 400 <= code <= 499 def is_server_error(code): - return code >= 500 and code <= 599 + return 500 <= code <= 599 HTTP_100_CONTINUE = 100 diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 78cb37e56..aa805f14e 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -52,8 +52,8 @@ def camelcase_to_spaces(content): Translate 'CamelCaseNames' to 'Camel Case Names'. Used when generating names from view classes. """ - camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))' - content = re.sub(camelcase_boundry, ' \\1', content).strip() + camelcase_boundary = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))' + content = re.sub(camelcase_boundary, ' \\1', content).strip() return ' '.join(content.split('_')).title() From 2e534b31c1273bbbe13236e162ced755faf4183f Mon Sep 17 00:00:00 2001 From: Anna Ossowski Date: Tue, 18 Jul 2017 01:14:49 -0700 Subject: [PATCH 05/20] Removed Micropyramid as a sponsor (#5280) --- README.md | 1 - docs/index.md | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index d710f3d4a..5a8857a71 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ The initial aim is to provide a single full-time position on REST framework. -

*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), [Machinalis](https://hello.machinalis.co.uk/), [Rollbar](https://rollbar.com), and [MicroPyramid](https://micropyramid.com/django-rest-framework-development-services/).* diff --git a/docs/index.md b/docs/index.md index 23433a6b7..fbb5bc1bb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -75,11 +75,10 @@ continued development by **[signing up for a paid plan][funding]**.
  • Stream
  • Machinalis
  • Rollbar
  • -
  • MicroPyramid
  • -*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), [Machinalis](https://hello.machinalis.co.uk/), [Rollbar](https://rollbar.com), and [MicroPyramid](https://micropyramid.com/django-rest-framework-development-services/).* +*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), [Machinalis](https://hello.machinalis.co.uk/), and [Rollbar](https://rollbar.com).* --- From 0c9c951c1d144c397a47ff25883ae573e61d5ef4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 18 Jul 2017 09:15:22 +0100 Subject: [PATCH 06/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a8857a71..cef407af4 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The initial aim is to provide a single full-time position on REST framework.

    -*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), [Machinalis](https://hello.machinalis.co.uk/), [Rollbar](https://rollbar.com), and [MicroPyramid](https://micropyramid.com/django-rest-framework-development-services/).* +*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), [Machinalis](https://hello.machinalis.co.uk/), and [Rollbar].* --- From 2d4226c2bd1bd959cfb9a8887b347435b63909c9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 18 Jul 2017 09:16:13 +0100 Subject: [PATCH 07/20] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cef407af4..814da6e92 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The initial aim is to provide a single full-time position on REST framework.

    -*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), [Machinalis](https://hello.machinalis.co.uk/), and [Rollbar].* +*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), [Machinalis](https://hello.machinalis.co.uk/), and [Rollbar](https://rollbar.com/).* --- From 9ec89141ee1cacbf2b10400597242e664a7656a7 Mon Sep 17 00:00:00 2001 From: minusf Date: Thu, 20 Jul 2017 23:42:51 +0200 Subject: [PATCH 08/20] typo: may -> many --- rest_framework/exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index e84074a07..aac31c453 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -129,8 +129,8 @@ class ValidationError(APIException): if code is None: code = self.default_code - # For validation failures, we may collect may errors together, so the - # details should always be coerced to a list if not already. + # For validation failures, we may collect many errors together, + # so the details should always be coerced to a list if not already. if not isinstance(detail, dict) and not isinstance(detail, list): detail = [detail] From ad501d41f5db7267e1a4484d0afcafe9ba24800e Mon Sep 17 00:00:00 2001 From: Didi Hoffmann Date: Mon, 31 Jul 2017 14:50:36 +0200 Subject: [PATCH 09/20] Added the django user model So it is possible to copy paste the example and it will work out of the box. --- docs/api-guide/views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index c0c4f67e4..abf5351c7 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -19,10 +19,10 @@ REST framework provides an `APIView` class, which subclasses Django's `View` cla Using the `APIView` class is pretty much the same as using a regular `View` class, as usual, the incoming request is dispatched to an appropriate handler method such as `.get()` or `.post()`. Additionally, a number of attributes may be set on the class that control various aspects of the API policy. For example: - from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import authentication, permissions + from django.contrib.auth.models import User class ListUsers(APIView): """ From 1368d3677ccf8756f771696dff04eb721819171e Mon Sep 17 00:00:00 2001 From: Didi Hoffmann Date: Mon, 31 Jul 2017 15:00:26 +0200 Subject: [PATCH 10/20] Added the separator line back Accidentally also deleted a line. --- docs/api-guide/views.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index abf5351c7..4fa36d0fc 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -19,6 +19,7 @@ REST framework provides an `APIView` class, which subclasses Django's `View` cla Using the `APIView` class is pretty much the same as using a regular `View` class, as usual, the incoming request is dispatched to an appropriate handler method such as `.get()` or `.post()`. Additionally, a number of attributes may be set on the class that control various aspects of the API policy. For example: + from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import authentication, permissions From 21074a02b435cb7cba990b10423fe1823e2007da Mon Sep 17 00:00:00 2001 From: Anna Ossowski Date: Fri, 4 Aug 2017 21:23:54 +0200 Subject: [PATCH 11/20] Changed monthly report link (#5308) * Removed Micropyramid as a sponsor * Changed monthly report link --- docs/topics/funding.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/funding.md b/docs/topics/funding.md index 6c196faf0..259b69fd2 100644 --- a/docs/topics/funding.md +++ b/docs/topics/funding.md @@ -329,7 +329,7 @@ For further enquires please contact From 26ebb88306abf2da0840d424772fbc3020e2e832 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Mon, 7 Aug 2017 11:52:09 -0400 Subject: [PATCH 12/20] Revert 3288 (#5313) * Add regression test for #2505. Thanks @pySilver! * Add regression test for #5087 * Revert "Cached the field's root and context property." This reverts commit 792005806b50f8aad086a76ff5a742c66a98428e. --- rest_framework/fields.py | 5 ++--- tests/test_fields.py | 10 ++++++++++ tests/test_serializer.py | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 14b264ff9..9cc9ab03f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -26,7 +26,6 @@ from django.utils.dateparse import ( from django.utils.duration import duration_string from django.utils.encoding import is_protected_type, smart_text from django.utils.formats import localize_input, sanitize_separators -from django.utils.functional import cached_property from django.utils.ipv6 import clean_ipv6_address from django.utils.timezone import utc from django.utils.translation import ugettext_lazy as _ @@ -586,7 +585,7 @@ class Field(object): message_string = msg.format(**kwargs) raise ValidationError(message_string, code=key) - @cached_property + @property def root(self): """ Returns the top-level serializer for this field. @@ -596,7 +595,7 @@ class Field(object): root = root.parent return root - @cached_property + @property def context(self): """ Returns the context as passed to the root serializer on initialization. diff --git a/tests/test_fields.py b/tests/test_fields.py index 38dc5f7a7..d6b233227 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -502,6 +502,16 @@ class TestCreateOnlyDefault: assert serializer.validated_data['context_set'] == 'success' +class Test5087Regression: + def test_parent_binding(self): + parent = serializers.Serializer() + field = serializers.CharField() + + assert field.root is field + field.bind('name', parent) + assert field.root is parent + + # Tests for field input and output values. # ---------------------------------------- diff --git a/tests/test_serializer.py b/tests/test_serializer.py index f76cec9c3..91430a193 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -469,6 +469,22 @@ class TestSerializerValidationWithCompiledRegexField: assert serializer.errors == {} +class Test2505Regression: + def test_serializer_context(self): + class NestedSerializer(serializers.Serializer): + def __init__(self, *args, **kwargs): + super(NestedSerializer, self).__init__(*args, **kwargs) + # .context should not cache + self.context + + class ParentSerializer(serializers.Serializer): + nested = NestedSerializer() + + serializer = ParentSerializer(data={}, context={'foo': 'bar'}) + assert serializer.context == {'foo': 'bar'} + assert serializer.fields['nested'].context == {'foo': 'bar'} + + class Test4606Regression: def setup(self): class ExampleSerializer(serializers.Serializer): From 1f2e2dea9658d671abcad1187048186749a1fb7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Va=C5=A1ek=20Dohnal?= Date: Tue, 8 Aug 2017 16:01:19 +0200 Subject: [PATCH 13/20] Fixed typo in docs/Documenting your API (#5316) --- docs/topics/documenting-your-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md index 9a87c17c1..da1dbe358 100644 --- a/docs/topics/documenting-your-api.md +++ b/docs/topics/documenting-your-api.md @@ -42,7 +42,7 @@ For example: class UserList(generics.ListAPIView): """ Return a list of all the existing users. - """" + """ If a view supports multiple methods, you should split your documentation using `method:` style delimiters. From 96d6a9a6da1318d392cba4d6f82d1250cb1b6a21 Mon Sep 17 00:00:00 2001 From: "Yury V. Zaytsev" Date: Wed, 9 Aug 2017 21:19:43 +0200 Subject: [PATCH 14/20] Docs: update link in pagination.py (#5321) The blog post referenced in the documentation has been since moved to a new location. --- rest_framework/pagination.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index a4a5230ef..61e0a80b0 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -473,7 +473,7 @@ class CursorPagination(BasePagination): """ The cursor pagination implementation is necessarily complex. For an overview of the position/offset style we use, see this post: - http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api + http://cra.mr/2011/03/08/building-cursors-for-the-disqus-api """ cursor_query_param = 'cursor' cursor_query_description = _('The pagination cursor value.') From 9b5a6bea67114e0d2cdb0f8e4ae1eb1cf05d2fed Mon Sep 17 00:00:00 2001 From: Steven Johns Date: Thu, 10 Aug 2017 05:21:23 +1000 Subject: [PATCH 15/20] Fix typo on `fields.md` (#5320) > keeps you error messages to > keeps your error messages and > more cleanly separated from your code to > cleaner and more separated from your code --- docs/api-guide/fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 28d06f25c..b93ac389b 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -641,7 +641,7 @@ The `.fail()` method is a shortcut for raising `ValidationError` that takes a me return Color(red, green, blue) -This style keeps you error messages more cleanly separated from your code, and should be preferred. +This style keeps your error messages cleaner and more separated from your code, and should be preferred. # Third party packages From e80b78d1cbbadb341e788f64d87e70af5f53c421 Mon Sep 17 00:00:00 2001 From: Alexander Dutton Date: Fri, 11 Aug 2017 10:35:00 +0100 Subject: [PATCH 16/20] RemoteUserAuthentication, docs, and tests (#5306) RemoteUserAuthentication, docs, and tests --- docs/api-guide/authentication.md | 22 ++++++++++++++++++++++ rest_framework/authentication.py | 21 +++++++++++++++++++++ tests/test_authentication.py | 26 +++++++++++++++++++++++--- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 05b8523f8..a1c24f954 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -239,6 +239,28 @@ If you're using an AJAX style API with SessionAuthentication, you'll need to mak CSRF validation in REST framework works slightly differently to standard Django due to the need to support both session and non-session based authentication to the same views. This means that only authenticated requests require CSRF tokens, and anonymous requests may be sent without CSRF tokens. This behaviour is not suitable for login views, which should always have CSRF validation applied. + +## RemoteUserAuthentication + +This authentication scheme allows you to delegate authentication to your web server, which sets the `REMOTE_USER` +environment variable. + +To use it, you must have `django.contrib.auth.backends.RemoteUserBackend` (or a subclass) in your +`AUTHENTICATION_BACKENDS` setting. By default, `RemoteUserBackend` creates `User` objects for usernames that don't +already exist. To change this and other behaviour, consult the +[Django documentation](https://docs.djangoproject.com/en/stable/howto/auth-remote-user/). + +If successfully authenticated, `RemoteUserAuthentication` provides the following credentials: + +* `request.user` will be a Django `User` instance. +* `request.auth` will be `None`. + +Consult your web server's documentation for information about configuring an authentication method, e.g.: + +* [Apache Authentication How-To](https://httpd.apache.org/docs/2.4/howto/auth.html) +* [NGINX (Restricting Access)](https://www.nginx.com/resources/admin-guide/#restricting_access) + + # Custom authentication To implement a custom authentication scheme, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise. diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index cb9608a3c..606f572ab 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -201,3 +201,24 @@ class TokenAuthentication(BaseAuthentication): def authenticate_header(self, request): return self.keyword + + +class RemoteUserAuthentication(BaseAuthentication): + """ + REMOTE_USER authentication. + + To use this, set up your web server to perform authentication, which will + set the REMOTE_USER environment variable. You will need to have + 'django.contrib.auth.backends.RemoteUserBackend in your + AUTHENTICATION_BACKENDS setting + """ + + # Name of request header to grab username from. This will be the key as + # used in the request.META dictionary, i.e. the normalization of headers to + # all uppercase and the addition of "HTTP_" prefix apply. + header = "REMOTE_USER" + + def authenticate(self, request): + user = authenticate(remote_user=request.META.get(self.header)) + if user and user.is_active: + return (user, None) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index f2e0fb424..fdbc28a2a 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -16,9 +16,8 @@ from rest_framework import ( HTTP_HEADER_ENCODING, exceptions, permissions, renderers, status ) from rest_framework.authentication import ( - BaseAuthentication, BasicAuthentication, SessionAuthentication, - TokenAuthentication -) + BaseAuthentication, BasicAuthentication, RemoteUserAuthentication, SessionAuthentication, + TokenAuthentication) from rest_framework.authtoken.models import Token from rest_framework.authtoken.views import obtain_auth_token from rest_framework.compat import is_authenticated @@ -64,6 +63,10 @@ urlpatterns = [ r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication]) ), + url( + r'^remote-user/$', + MockView.as_view(authentication_classes=[RemoteUserAuthentication]) + ), url( r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication]) @@ -523,3 +526,20 @@ class BasicAuthenticationUnitTests(TestCase): auth.authenticate_credentials('foo', 'bar') assert 'User inactive or deleted.' in str(error) authentication.authenticate = old_authenticate + + +@override_settings(ROOT_URLCONF='tests.test_authentication', + AUTHENTICATION_BACKENDS=('django.contrib.auth.backends.RemoteUserBackend',)) +class RemoteUserAuthenticationUnitTests(TestCase): + def setUp(self): + self.username = 'john' + self.email = 'lennon@thebeatles.com' + self.password = 'password' + self.user = User.objects.create_user( + self.username, self.email, self.password + ) + + def test_remote_user_works(self): + response = self.client.post('/remote-user/', + REMOTE_USER=self.username) + self.assertEqual(response.status_code, status.HTTP_200_OK) From 1799b569d8def2eccc26854eae4d5994793729d2 Mon Sep 17 00:00:00 2001 From: Andrea Grandi Date: Sun, 13 Aug 2017 11:08:44 +0100 Subject: [PATCH 17/20] Add documentation to create user token with the new command --- docs/api-guide/authentication.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index a1c24f954..a7a24029b 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -222,6 +222,21 @@ It is also possible to create Tokens manually through admin interface. In case y TokenAdmin.raw_id_fields = ('user',) +#### Using Django manage.py command + +Since version 3.6.4 it's possible to generate a user token using the following command: + + ./manage.py drf_create_token + +this command will return the API token for the given user, creating it if it doesn't exist: + + Generated token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b for user user1 + +In case you want to regenerate the token (for example if it has been compromised or leaked) you can pass an additional parameter: + + ./manage.py drf_create_token -r + + ## SessionAuthentication This authentication scheme uses Django's default session backend for authentication. Session authentication is appropriate for AJAX clients that are running in the same session context as your website. From 52e0f3ae08808a6a4f3b2b710f0c0030bcb4b34e Mon Sep 17 00:00:00 2001 From: Berker Peksag Date: Mon, 14 Aug 2017 15:04:52 +0300 Subject: [PATCH 18/20] Fix indentation of code example in chapter 7 (#5329) --- docs/tutorial/7-schemas-and-client-libraries.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/tutorial/7-schemas-and-client-libraries.md b/docs/tutorial/7-schemas-and-client-libraries.md index 4d7193986..26ee86871 100644 --- a/docs/tutorial/7-schemas-and-client-libraries.md +++ b/docs/tutorial/7-schemas-and-client-libraries.md @@ -36,15 +36,15 @@ API schema. We can now include a schema for our API, by including an autogenerated schema view in our URL configuration. -``` - from rest_framework.schemas import get_schema_view +```python +from rest_framework.schemas import get_schema_view - schema_view = get_schema_view(title='Pastebin API') +schema_view = get_schema_view(title='Pastebin API') - urlpatterns = [ -    url(r'^schema/$', schema_view), - ... - ] +urlpatterns = [ +    url(r'^schema/$', schema_view), + ... +] ``` If you visit the API root endpoint in a browser you should now see `corejson` From bf4b3ff0d5d0f5c267803ff0d519b27df043b0f7 Mon Sep 17 00:00:00 2001 From: Oliver Eidel Date: Mon, 14 Aug 2017 14:09:47 +0200 Subject: [PATCH 19/20] Fix token param in force_authenticate example (#5284) related_name for Token is `auth_token`, not `token`. --- docs/api-guide/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 753c77e2f..2b93080b3 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -82,7 +82,7 @@ For example, when forcibly authenticating using a token, you might do something user = User.objects.get(username='olivia') request = factory.get('/accounts/django-superstars/') - force_authenticate(request, user=user, token=user.token) + force_authenticate(request, user=user, token=user.auth_token) --- From 3110635685088bcc5c2115704341860c3e161394 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 15 Aug 2017 15:33:06 +0200 Subject: [PATCH 20/20] Update JS constructor syntax (#5332) Fixes #5247 --- docs/topics/api-clients.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/topics/api-clients.md b/docs/topics/api-clients.md index 6e0df69e5..73434416d 100644 --- a/docs/topics/api-clients.md +++ b/docs/topics/api-clients.md @@ -408,7 +408,7 @@ The `coreapi` library, and the `schema` object will now both be available on the In order to interact with the API you'll need a client instance. - var client = coreapi.Client() + var client = new coreapi.Client() Typically you'll also want to provide some authentication credentials when instantiating the client. @@ -423,7 +423,7 @@ the user to login, and then instantiate a client using session authentication: csrfCookieName: 'csrftoken', csrfHeaderName: 'X-CSRFToken' }) - let client = coreapi.Client({auth: auth}) + let client = new coreapi.Client({auth: auth}) The authentication scheme will handle including a CSRF header in any outgoing requests for unsafe HTTP methods. @@ -437,7 +437,7 @@ The `TokenAuthentication` class can be used to support REST framework's built-in scheme: 'JWT' token: '' }) - let client = coreapi.Client({auth: auth}) + let client = new coreapi.Client({auth: auth}) When using TokenAuthentication you'll probably need to implement a login flow using the CoreAPI client. @@ -448,7 +448,7 @@ request to an "obtain token" endpoint For example, using the "Django REST framework JWT" package // Setup some globally accessible state - window.client = coreapi.Client() + window.client = new coreapi.Client() window.loggedIn = false function loginUser(username, password) { @@ -475,7 +475,7 @@ The `BasicAuthentication` class can be used to support HTTP Basic Authentication username: '', password: '' }) - let client = coreapi.Client({auth: auth}) + let client = new coreapi.Client({auth: auth}) ## Using the client