mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-04 20:40:14 +03:00
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:
parent
8a8389bd4b
commit
bce4f73743
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user