Skip to content

Commit

Permalink
feat: LimitOffsetPagination support (#6)
Browse files Browse the repository at this point in the history
Add LimitOffsetPagination support
  • Loading branch information
matslindh authored Sep 19, 2022
1 parent 7a47b88 commit 9d84feb
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
32 changes: 31 additions & 1 deletion drf_link_header_pagination/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
Expand Down Expand Up @@ -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
Expand Down
107 changes: 107 additions & 0 deletions tests/test_link_header_limit_offset_pagination.py
Original file line number Diff line number Diff line change
@@ -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"] == (
'<http://testserver/?limit=4&offset=4>; rel="next", '
'<http://testserver/?limit=4>; rel="first", '
'<http://testserver/?limit=4&offset=96>; 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"] == (
'<http://testserver/?limit=4>; rel="prev", '
'<http://testserver/?limit=4&offset=8>; rel="next", '
'<http://testserver/?limit=4>; rel="first", '
'<http://testserver/?limit=4&offset=96>; 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"] == (
'<http://testserver/?limit=4&offset=92>; rel="prev", '
'<http://testserver/?limit=4>; 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),
],
}

0 comments on commit 9d84feb

Please sign in to comment.