Add LinkHeaderPagination class.

This expands on the "Header based pagination" example given in the
documentation but includes it in the app since it is a commonly used
pagination style.
This commit is contained in:
Tommy Beadle 2017-07-11 15:55:54 -04:00
parent 8a8389bd4b
commit bce4f73743
3 changed files with 180 additions and 23 deletions

View File

@ -110,6 +110,40 @@ To set these attributes you should override the `PageNumberPagination` class, an
--- ---
## LinkHeaderPagination
This pagination style accepts a single page number in the request query parameters. The response uses an HTTP header called `Link` to provide the URLs for the next, previous, first, and last pages as described in [Github's Developer Documentation][github-link-pagination]. If you are using Python's [Requests][requests] library to make the request, this header is automatically parsed for you as described [here][requests-link-header].
**Request**:
GET https://api.example.org/accounts/?page=4
**Response**:
HTTP 200 OK
Link: <https://api.example.org/accounts/>; rel="first", <https://api.example.org/accounts/?page=3>; rel="prev", <https://api.example.org/accounts/?page=5>; rel="next", <https://api.example.org/accounts/?page=11>; rel="last"
[
]
#### Setup
To enable the `LinkHeaderPagination` style globally, use the following configuration, modifying the `PAGE_SIZE` as desired:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LinkHeaderPagination',
'PAGE_SIZE': 100
}
On `GenericAPIView` subclasses you may also set the `pagination_class` attribute to select `LinkHeaderPagination` on a per-view basis.
#### Configuration
The configuration is the same as for `PageNumberPagination` described above.
---
## LimitOffsetPagination ## LimitOffsetPagination
This pagination style mirrors the syntax used when looking up multiple database records. The client includes both a "limit" and an This pagination style mirrors the syntax used when looking up multiple database records. The client includes both a "limit" and an
@ -242,29 +276,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. 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 ## Using your custom pagination class
To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting: To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting:
@ -335,3 +346,5 @@ The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagi
[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin [paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
[drf-proxy-pagination]: https://github.com/tuffnatty/drf-proxy-pagination [drf-proxy-pagination]: https://github.com/tuffnatty/drf-proxy-pagination
[disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api [disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api
[requests]: http://docs.python-requests.org
[requests-link-header]: http://docs.python-requests.org/en/master/user/advanced/#link-headers

View File

@ -316,6 +316,50 @@ class PageNumberPagination(BasePagination):
return fields return fields
class LinkHeaderPagination(PageNumberPagination):
""" Inform the user of pagination links via response headers, similar to
what's described in
https://developer.github.com/guides/traversing-with-pagination/.
"""
def get_paginated_response(self, data):
next_url = self.get_next_link()
previous_url = self.get_previous_link()
first_url = self.get_first_link()
last_url = self.get_last_link()
links = []
for url, label in (
(first_url, 'first'),
(previous_url, 'prev'),
(next_url, 'next'),
(last_url, 'last'),
):
if url is not None:
links.append('<{}>; rel="{}"'.format(url, label))
headers = {'Link': ', '.join(links)} if links else {}
return Response(data, headers=headers)
def get_first_link(self):
if not self.page.has_previous():
return None
else:
url = self.request.build_absolute_uri()
return remove_query_param(url, self.page_query_param)
def get_last_link(self):
if not self.page.has_next():
return None
else:
url = self.request.build_absolute_uri()
return replace_query_param(
url,
self.page_query_param,
self.page.paginator.num_pages,
)
class LimitOffsetPagination(BasePagination): class LimitOffsetPagination(BasePagination):
""" """
A limit/offset based style. For example: A limit/offset based style. For example:

View File

@ -263,6 +263,106 @@ class TestPageNumberPagination:
self.paginate_queryset(request) self.paginate_queryset(request)
class TestLinkHeaderPagination:
"""
Unit tests for `pagination.LinkHeaderPagination`.
"""
def setup(self):
class ExamplePagination(pagination.LinkHeaderPagination):
page_size = 5
self.pagination = ExamplePagination()
self.queryset = range(1, 101)
def paginate_queryset(self, request):
return list(self.pagination.paginate_queryset(self.queryset, request))
def get_paginated_response(self, queryset):
return self.pagination.get_paginated_response(queryset)
def get_html_context(self):
return self.pagination.get_html_context()
def test_no_page_number(self):
request = Request(factory.get('/'))
queryset = self.paginate_queryset(request)
response = self.get_paginated_response(queryset)
context = self.get_html_context()
assert queryset == [1, 2, 3, 4, 5]
assert response.data == [1, 2, 3, 4, 5]
assert response['Link'] == (
'<http://testserver/?page=2>; rel="next", '
'<http://testserver/?page=20>; rel="last"'
)
assert context == {
'previous_url': None,
'next_url': 'http://testserver/?page=2',
'page_links': [
PageLink('http://testserver/', 1, True, False),
PageLink('http://testserver/?page=2', 2, False, False),
PageLink('http://testserver/?page=3', 3, False, False),
PAGE_BREAK,
PageLink('http://testserver/?page=20', 20, False, False),
]
}
assert self.pagination.display_page_controls
assert isinstance(self.pagination.to_html(), type(''))
def test_second_page(self):
request = Request(factory.get('/', {'page': 2}))
queryset = self.paginate_queryset(request)
response = self.get_paginated_response(queryset)
context = self.get_html_context()
assert queryset == [6, 7, 8, 9, 10]
assert response.data == [6, 7, 8, 9, 10]
assert response['Link'] == (
'<http://testserver/>; rel="first", '
'<http://testserver/>; rel="prev", '
'<http://testserver/?page=3>; rel="next", '
'<http://testserver/?page=20>; rel="last"'
)
assert context == {
'previous_url': 'http://testserver/',
'next_url': 'http://testserver/?page=3',
'page_links': [
PageLink('http://testserver/', 1, False, False),
PageLink('http://testserver/?page=2', 2, True, False),
PageLink('http://testserver/?page=3', 3, False, False),
PAGE_BREAK,
PageLink('http://testserver/?page=20', 20, False, False),
]
}
def test_last_page(self):
request = Request(factory.get('/', {'page': 'last'}))
queryset = self.paginate_queryset(request)
response = self.get_paginated_response(queryset)
context = self.get_html_context()
assert queryset == [96, 97, 98, 99, 100]
assert response.data == [96, 97, 98, 99, 100]
assert response['Link'] == (
'<http://testserver/>; rel="first", '
'<http://testserver/?page=19>; rel="prev"'
)
assert context == {
'previous_url': 'http://testserver/?page=19',
'next_url': None,
'page_links': [
PageLink('http://testserver/', 1, False, False),
PAGE_BREAK,
PageLink('http://testserver/?page=18', 18, False, False),
PageLink('http://testserver/?page=19', 19, False, False),
PageLink('http://testserver/?page=20', 20, True, False),
]
}
def test_invalid_page(self):
request = Request(factory.get('/', {'page': 'invalid'}))
with pytest.raises(exceptions.NotFound):
self.paginate_queryset(request)
class TestPageNumberPaginationOverride: class TestPageNumberPaginationOverride:
""" """
Unit tests for `pagination.PageNumberPagination`. Unit tests for `pagination.PageNumberPagination`.