From 72b4310129c440834283ccff427165700f7fbc5e Mon Sep 17 00:00:00 2001 From: Steve Lacey Date: Fri, 27 Feb 2015 15:53:40 +0000 Subject: [PATCH 1/7] Improved on LinkHeaderPagination example - Fixes Link header syntax, which if I understand [RFC5988](http://www.rfc-editor.org/rfc/rfc5988.txt) correctly, was incorrect - Adds first and last page ``s as and `X-Total-Count` as per http://blog.mwaysolutions.com/2014/06/05/10-best-practices-for-better-restful-api - Adds `X-Total-Count` as per http://blog.mwaysolutions.com/2014/06/05/10-best-practices-for-better-restful-api - Adds imports and python codeblock for easy reading --- docs/api-guide/pagination.md | 46 ++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index bae579a6d..49602abff 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -51,7 +51,8 @@ You can then apply your new style to a view using the `.pagination_class` attrib Or apply the style globally, using the `DEFAULT_PAGINATION_CLASS` settings key. For example: REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'apps.core.pagination.StandardResultsSetPagination' } + 'DEFAULT_PAGINATION_CLASS': 'apps.core.pagination.StandardResultsSetPagination' + } --- @@ -84,24 +85,45 @@ Note that the `paginate_queryset` method may set state on the pagination instanc 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]. +```py + from rest_framework.pagination import PageNumberPagination + from rest_framework.response import Response + from rest_framework.utils.urls import remove_query_param, replace_query_param + + class LinkHeaderPagination(pagination.PageNumberPagination): def get_paginated_response(self, data): - next_url = self.get_next_link() previous_url = self.get_previous_link() + link = '<{}>; rel="{}"' - 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 = '' + first = self.get_first_link() + prev = self.get_previous_link() + next = self.get_next_link() + last = self.get_last_link() - link = link.format(next_url=next_url, previous_url=previous_url) - headers = {'Link': link} if link else {} + links = [ + link.format(first, 'first'), + link.format(prev, 'prev') if prev else None, + link.format(next, 'next') if next else None, + link.format(last, 'last'), + ] + + headers = { + 'Link': ", ".join([link for link in links if link]), + 'X-Total-Count': self.page.paginator.count + } return Response(data, headers=headers) + def get_first_link(self): + url = self.request.build_absolute_uri() + return remove_query_param(url, self.page_query_param) + + def get_last_link(self): + url = self.request.build_absolute_uri() + page_number = self.page.paginator.num_pages + return replace_query_param(url, self.page_query_param, page_number) +``` + ## Using your custom pagination class To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting: From ace82b78d66af4e13badcfb01d4f066ee4520f31 Mon Sep 17 00:00:00 2001 From: Steve Lacey Date: Fri, 27 Feb 2015 16:32:15 +0000 Subject: [PATCH 2/7] Added _url to vars and reordered --- docs/api-guide/pagination.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 49602abff..4b64256ba 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -93,18 +93,18 @@ Let's modify the built-in `PageNumberPagination` style, so that instead of inclu class LinkHeaderPagination(pagination.PageNumberPagination): def get_paginated_response(self, data): + first_url = self.get_first_link() + prev_url = self.get_previous_link() + next_url = self.get_next_link() + last_url = self.get_last_link() + link = '<{}>; rel="{}"' - first = self.get_first_link() - prev = self.get_previous_link() - next = self.get_next_link() - last = self.get_last_link() - links = [ - link.format(first, 'first'), - link.format(prev, 'prev') if prev else None, - link.format(next, 'next') if next else None, - link.format(last, 'last'), + link.format(first_url, 'first'), + link.format(prev_url, 'prev') if prev_url else None, + link.format(next_url, 'next') if next_url else None, + link.format(last_url, 'last'), ] headers = { From 29c8146c008ac92f8681d79a8dfeae076a494865 Mon Sep 17 00:00:00 2001 From: Steve Lacey Date: Fri, 27 Feb 2015 16:33:00 +0000 Subject: [PATCH 3/7] Stripped indent now using a code block --- docs/api-guide/pagination.md | 56 ++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 4b64256ba..45c96eb24 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -86,42 +86,42 @@ Note that the `paginate_queryset` method may set state on the pagination instanc 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]. ```py - from rest_framework.pagination import PageNumberPagination - from rest_framework.response import Response - from rest_framework.utils.urls import remove_query_param, replace_query_param +from rest_framework.pagination import PageNumberPagination +from rest_framework.response import Response +from rest_framework.utils.urls import remove_query_param, replace_query_param - class LinkHeaderPagination(pagination.PageNumberPagination): - def get_paginated_response(self, data): - first_url = self.get_first_link() - prev_url = self.get_previous_link() - next_url = self.get_next_link() - last_url = self.get_last_link() +class LinkHeaderPagination(pagination.PageNumberPagination): + def get_paginated_response(self, data): + first_url = self.get_first_link() + prev_url = self.get_previous_link() + next_url = self.get_next_link() + last_url = self.get_last_link() - link = '<{}>; rel="{}"' + link = '<{}>; rel="{}"' - links = [ - link.format(first_url, 'first'), - link.format(prev_url, 'prev') if prev_url else None, - link.format(next_url, 'next') if next_url else None, - link.format(last_url, 'last'), - ] + links = [ + link.format(first_url, 'first'), + link.format(prev_url, 'prev') if prev_url else None, + link.format(next_url, 'next') if next_url else None, + link.format(last_url, 'last'), + ] - headers = { - 'Link': ", ".join([link for link in links if link]), - 'X-Total-Count': self.page.paginator.count - } + headers = { + 'Link': ", ".join([link for link in links if link]), + 'X-Total-Count': self.page.paginator.count + } - return Response(data, headers=headers) + return Response(data, headers=headers) - def get_first_link(self): - url = self.request.build_absolute_uri() - return remove_query_param(url, self.page_query_param) + def get_first_link(self): + url = self.request.build_absolute_uri() + return remove_query_param(url, self.page_query_param) - def get_last_link(self): - url = self.request.build_absolute_uri() - page_number = self.page.paginator.num_pages - return replace_query_param(url, self.page_query_param, page_number) + def get_last_link(self): + url = self.request.build_absolute_uri() + page_number = self.page.paginator.num_pages + return replace_query_param(url, self.page_query_param, page_number) ``` ## Using your custom pagination class From 3dc83fb4588136634926735aa5f3b023409df886 Mon Sep 17 00:00:00 2001 From: Steve Lacey Date: Fri, 27 Feb 2015 16:35:21 +0000 Subject: [PATCH 4/7] More python blocks --- docs/api-guide/pagination.md | 54 +++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 45c96eb24..e49edea8e 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -21,9 +21,11 @@ Pagination is only performed automatically if you're using the generic views or The default pagination style may be set globally, using the `DEFAULT_PAGINATION_CLASS` settings key. For example, to use the built-in limit/offset pagination, you would do: - REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination' - } +```py +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination' +} +``` You can also set the pagination class on an individual view by using the `pagination_class` attribute. Typically you'll want to use the same pagination style throughout your API, although you might want to vary individual aspects of the pagination, such as default or maximum page size, on a per-view basis. @@ -31,28 +33,34 @@ You can also set the pagination class on an individual view by using the `pagina If you want to modify particular aspects of the pagination style, you'll want to override one of the pagination classes, and set the attributes that you want to change. - class LargeResultsSetPagination(PageNumberPagination): - paginate_by = 1000 - paginate_by_param = 'page_size' - max_paginate_by = 10000 +```py +class LargeResultsSetPagination(PageNumberPagination): + paginate_by = 1000 + paginate_by_param = 'page_size' + max_paginate_by = 10000 - class StandardResultsSetPagination(PageNumberPagination): - paginate_by = 100 - paginate_by_param = 'page_size' - max_paginate_by = 1000 +class StandardResultsSetPagination(PageNumberPagination): + paginate_by = 100 + paginate_by_param = 'page_size' + max_paginate_by = 1000 +``` You can then apply your new style to a view using the `.pagination_class` attribute: - class BillingRecordsView(generics.ListAPIView): - queryset = Billing.objects.all() - serializer = BillingRecordsSerializer - pagination_class = LargeResultsSetPagination +```py +class BillingRecordsView(generics.ListAPIView): + queryset = Billing.objects.all() + serializer = BillingRecordsSerializer + pagination_class = LargeResultsSetPagination +``` Or apply the style globally, using the `DEFAULT_PAGINATION_CLASS` settings key. For example: - REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'apps.core.pagination.StandardResultsSetPagination' - } +```py +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'apps.core.pagination.StandardResultsSetPagination' +} +``` --- @@ -128,10 +136,12 @@ class LinkHeaderPagination(pagination.PageNumberPagination): To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting: - REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'my_project.apps.core.pagination.LinkHeaderPagination', - 'PAGINATE_BY': 10 - } +```py +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'my_project.apps.core.pagination.LinkHeaderPagination', + 'PAGINATE_BY': 10 +} +``` API responses for list endpoints will now include a `Link` header, instead of including the pagination links as part of the body of the response, for example: From f3efdce9ede3f594b93f233d330d40608e56100e Mon Sep 17 00:00:00 2001 From: Steve Lacey Date: Fri, 27 Feb 2015 17:40:15 +0000 Subject: [PATCH 5/7] Adjusted pagination X headers in example, moved out get_first/last_link methods --- docs/api-guide/pagination.md | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index e49edea8e..c3b19752d 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -101,35 +101,30 @@ from rest_framework.utils.urls import remove_query_param, replace_query_param class LinkHeaderPagination(pagination.PageNumberPagination): def get_paginated_response(self, data): + headers = { + 'X-Page': self.page.number, + 'X-Per-Page': self.page.paginator.per_page, + 'X-Total': self.page.paginator.count, + 'X-Total-Pages': self.page.paginator.num_pages + } + first_url = self.get_first_link() prev_url = self.get_previous_link() next_url = self.get_next_link() last_url = self.get_last_link() link = '<{}>; rel="{}"' + links = [] - links = [ - link.format(first_url, 'first'), - link.format(prev_url, 'prev') if prev_url else None, - link.format(next_url, 'next') if next_url else None, - link.format(last_url, 'last'), - ] + if first_url: links.append(link.format(first_url, 'first')) + if prev_url: links.append(link.format(prev_url, 'prev')) + if next_url: links.append(link.format(next_url, 'next')) + if last_url: links.append(link.format(last_url, 'last')) - headers = { - 'Link': ", ".join([link for link in links if link]), - 'X-Total-Count': self.page.paginator.count - } + if links: + headers['Link'] = ", ".join(links) return Response(data, headers=headers) - - def get_first_link(self): - url = self.request.build_absolute_uri() - return remove_query_param(url, self.page_query_param) - - def get_last_link(self): - url = self.request.build_absolute_uri() - page_number = self.page.paginator.num_pages - return replace_query_param(url, self.page_query_param, page_number) ``` ## Using your custom pagination class From 5f24aeb829251840e0d844b6c57a6085d84b931d Mon Sep 17 00:00:00 2001 From: Steve Lacey Date: Fri, 27 Feb 2015 17:43:04 +0000 Subject: [PATCH 6/7] Moved get_first/last_link methods out to PageNumberPaginator --- rest_framework/pagination.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 496500ba5..7d0e4057a 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -287,12 +287,11 @@ class PageNumberPagination(BasePagination): return self.paginate_by - def get_next_link(self): - if not self.page.has_next(): + def get_first_link(self): + if not self.page.has_previous(): return None url = self.request.build_absolute_uri() - page_number = self.page.next_page_number() - return replace_query_param(url, self.page_query_param, page_number) + return remove_query_param(url, self.page_query_param) def get_previous_link(self): if not self.page.has_previous(): @@ -303,6 +302,20 @@ class PageNumberPagination(BasePagination): return remove_query_param(url, self.page_query_param) return replace_query_param(url, self.page_query_param, page_number) + def get_next_link(self): + if not self.page.has_next(): + return None + url = self.request.build_absolute_uri() + page_number = self.page.next_page_number() + return replace_query_param(url, self.page_query_param, page_number) + + def get_last_link(self): + if not self.page.has_next(): + return None + url = self.request.build_absolute_uri() + page_number = self.page.paginator.num_pages + return replace_query_param(url, self.page_query_param, page_number) + def get_html_context(self): base_url = self.request.build_absolute_uri() From a5208e8c12b031605d78e813c4d9f70f6ae22851 Mon Sep 17 00:00:00 2001 From: Steve Lacey Date: Mon, 2 Mar 2015 09:18:04 +0000 Subject: [PATCH 7/7] Update pagination.md --- docs/api-guide/pagination.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index c3b19752d..8ec000e2e 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -96,10 +96,9 @@ Let's modify the built-in `PageNumberPagination` style, so that instead of inclu ```py from rest_framework.pagination import PageNumberPagination from rest_framework.response import Response -from rest_framework.utils.urls import remove_query_param, replace_query_param -class LinkHeaderPagination(pagination.PageNumberPagination): +class LinkHeaderPagination(PageNumberPagination): def get_paginated_response(self, data): headers = { 'X-Page': self.page.number,