mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-04 12:30:11 +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
|
||||
|
||||
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.
|
||||
|
||||
## 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:
|
||||
|
@ -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
|
||||
[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
|
||||
[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
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
A limit/offset based style. For example:
|
||||
|
|
|
@ -263,6 +263,106 @@ class TestPageNumberPagination:
|
|||
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:
|
||||
"""
|
||||
Unit tests for `pagination.PageNumberPagination`.
|
||||
|
|
Loading…
Reference in New Issue
Block a user