diff --git a/README.md b/README.md index 573ce11..39e30e4 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ Other pagination classes that are available are: - `LinkHeaderCursorPagination`: This is similar to the normal [`CursorPagination`](cursor-pagination) class but using the `Link` header to return only the `next` and/or `prev` links. The `first` and `last` links are unavailable. - `LinkHeaderLinkResponseCursorPagination`: This is similar to `LinkHeaderCursorPagination`, but in addition to the `next` and/or `prev` URL's being in the `Link` header, the content of the response body is updated to include them as well. The body will be an object with the keys `next` (the next page's URL or None), `previous` (the previous page's URL or None), and `results` (the original content of the body). +- `LinkHeaderLimitOffsetPagination`: [Uses the `LimitOffsetPagination` pagination class from DRF](https://www.django-rest-framework.org/api-guide/pagination/#limitoffsetpagination) to support `offset` and `limit` parameters instead of `page` to indicate offset into the queryset. ## Configuration diff --git a/drf_link_header_pagination/__init__.py b/drf_link_header_pagination/__init__.py index 2bc1265..aa7ebff 100644 --- a/drf_link_header_pagination/__init__.py +++ b/drf_link_header_pagination/__init__.py @@ -1,9 +1,10 @@ -from rest_framework.pagination import CursorPagination, PageNumberPagination +from rest_framework.pagination import CursorPagination, PageNumberPagination, LimitOffsetPagination from rest_framework.response import Response from rest_framework.utils.urls import remove_query_param, replace_query_param __all__ = [ "LinkHeaderPagination", + "LinkHeaderLimitOffsetPagination", "LinkHeaderCursorPagination", "LinkHeaderLinkResponseCursorPagination", ] @@ -36,6 +37,35 @@ def get_paginated_response(self, data): return Response(data, headers=self.get_headers()) +class LinkHeaderLimitOffsetPagination(LinkHeaderMixin, LimitOffsetPagination): + """ + Link header pagination with offset/limit links. Implements the regular + `LimitOffsetPagination` module with `Link: ` headers instead. + """ + def get_first_link(self): + url = self.request.build_absolute_uri() + url = replace_query_param(url, self.limit_query_param, self.limit) + + return remove_query_param(url, self.offset_query_param) + + def get_last_link(self): + if not self.get_next_link(): + return None + + # We need to adjust for 0 offset, otherwise we'll get the last link + # to an empty page if count % limit == 0 (i.e. the "pages" line up + # exactly) + offset = max(0, self.count - ((self.count - 1) % self.limit) - 1) + + url = self.request.build_absolute_uri() + url = replace_query_param(url, self.limit_query_param, self.limit) + + return replace_query_param(url, self.offset_query_param, offset) + + def get_paginated_response_schema(self, schema): + return schema + + class LinkHeaderPagination(LinkHeaderMixin, PageNumberPagination): """Inform the user of pagination links via response headers, similar to what's described in diff --git a/tests/test_link_header_limit_offset_pagination.py b/tests/test_link_header_limit_offset_pagination.py new file mode 100644 index 0000000..5da6197 --- /dev/null +++ b/tests/test_link_header_limit_offset_pagination.py @@ -0,0 +1,107 @@ +import pytest +from rest_framework import exceptions +from rest_framework.pagination import PAGE_BREAK, PageLink +from rest_framework.request import Request +from rest_framework.test import APIRequestFactory + +import drf_link_header_pagination + +factory = APIRequestFactory() + + +class TestLinkHeaderLimitOffsetPagination: + """ + Unit tests for `pagination.LinkHeaderLimitOffsetPagination`. + """ + def setup(self): + class ExamplePagination(drf_link_header_pagination.LinkHeaderLimitOffsetPagination): + default_limit = 4 + + 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_offset(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] + assert response.data == [1, 2, 3, 4] + assert response["Link"] == ( + '; rel="next", ' + '; rel="first", ' + '; rel="last"' + ) + + assert context == { + "previous_url": None, + "next_url": "http://testserver/?limit=4&offset=4", + "page_links": [ + PageLink("http://testserver/", 1, True, False), + PageLink("http://testserver/?offset=4", 2, False, False), + PageLink("http://testserver/?offset=8", 3, False, False), + PAGE_BREAK, + PageLink("http://testserver/?offset=96", 25, False, False), + ], + } + assert self.pagination.display_page_controls + assert isinstance(self.pagination.to_html(), type("")) + + def test_second_page(self): + request = Request(factory.get("/", {"offset": 4})) + queryset = self.paginate_queryset(request) + response = self.get_paginated_response(queryset) + context = self.get_html_context() + + assert queryset == [5, 6, 7, 8] + assert response.data == [5, 6, 7, 8] + assert response["Link"] == ( + '; rel="prev", ' + '; rel="next", ' + '; rel="first", ' + '; rel="last"' + ) + assert context == { + "previous_url": "http://testserver/?limit=4", + "next_url": "http://testserver/?limit=4&offset=8", + "page_links": [ + PageLink("http://testserver/", 1, False, False), + PageLink("http://testserver/?offset=4", 2, True, False), + PageLink("http://testserver/?offset=8", 3, False, False), + PAGE_BREAK, + PageLink("http://testserver/?offset=96", 25, False, False), + ], + } + + def test_last_page(self): + request = Request(factory.get("/", {"offset": "96"})) + queryset = self.paginate_queryset(request) + response = self.get_paginated_response(queryset) + context = self.get_html_context() + assert queryset == [97, 98, 99, 100] + assert response.data == [97, 98, 99, 100] + assert response["Link"] == ( + '; rel="prev", ' + '; rel="first"' + ) + assert context == { + "previous_url": "http://testserver/?limit=4&offset=92", + "next_url": None, + "page_links": [ + PageLink("http://testserver/", 1, False, False), + PAGE_BREAK, + PageLink("http://testserver/?offset=88", 23, False, False), + PageLink("http://testserver/?offset=92", 24, False, False), + PageLink("http://testserver/?offset=96", 25, True, False), + ], + }