diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md
index 888390018..7767d3a39 100644
--- a/docs/api-guide/pagination.md
+++ b/docs/api-guide/pagination.md
@@ -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: ; rel="first", ; rel="prev", ; rel="next", ; 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
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index 0255cfc7f..2310cbebd 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -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:
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index dd7f70330..11a890a2c 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -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'] == (
+ '; rel="next", '
+ '; 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'] == (
+ '; rel="first", '
+ '; rel="prev", '
+ '; rel="next", '
+ '; 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'] == (
+ '; rel="first", '
+ '; 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`.