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]**.