Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions apps/api/plane/tests/unit/utils/test_paginator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.

import pytest
from rest_framework.test import APIRequestFactory

from plane.utils.paginator import BasePaginator, Cursor, CursorResult


class OffsetEchoPaginator:
def get_result(self, limit=1000, cursor=None):
has_next = cursor.offset < 1
return CursorResult(
results=[{"page_offset": cursor.offset}],
next=Cursor(limit, cursor.offset + 1, False, has_next),
prev=Cursor(limit, cursor.offset - 1, True, cursor.offset > 0),
hits=2,
max_hits=2,
)


@pytest.mark.unit
class TestBasePaginatorPageSupport:
def setup_method(self):
self.base_paginator = BasePaginator()
self.factory = APIRequestFactory()

@pytest.mark.parametrize(
("page", "expected_offset"),
[
("1", 0),
("2", 1),
],
)
def test_paginate_honors_explicit_page_parameter(self, page, expected_offset):
request = self.factory.get("/", {"page": page, "per_page": "10"})

response = self.base_paginator.paginate(request=request, paginator=OffsetEchoPaginator())

assert response.data["results"][0]["page_offset"] == expected_offset

def test_paginate_returns_no_next_cursor_on_last_page(self):
request = self.factory.get("/", {"page": "2", "per_page": "10"})

response = self.base_paginator.paginate(request=request, paginator=OffsetEchoPaginator())

assert response.data["next_page_results"] is False
assert response.data["next_cursor"] is None

def test_following_next_cursor_metadata_terminates(self):
class TwoPagePaginator:
def get_result(self, limit=1000, cursor=None):
has_next = cursor.offset == 0
return CursorResult(
results=[{"page_offset": cursor.offset}],
next=Cursor(limit, cursor.offset + 1, False, has_next),
prev=Cursor(limit, cursor.offset - 1, True, cursor.offset > 0),
hits=3,
max_hits=2,
)

seen_offsets = []
next_cursor = None
paginator = TwoPagePaginator()

for _ in range(5):
params = {"per_page": "2"}
if next_cursor:
params["cursor"] = next_cursor

request = self.factory.get("/", params)
response = self.base_paginator.paginate(request=request, paginator=paginator)

seen_offsets.append(response.data["results"][0]["page_offset"])
next_cursor = response.data["next_cursor"]
if not next_cursor:
break

assert seen_offsets == [0, 1]
assert next_cursor is None
26 changes: 21 additions & 5 deletions apps/api/plane/utils/paginator.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,10 +674,26 @@ def paginate(
per_page = self.get_per_page(request, default_per_page, max_per_page)
# Convert the cursor value to integer and float from string
input_cursor = None
try:
input_cursor = cursor_cls.from_string(request.GET.get(self.cursor_name, f"{per_page}:0:0"))
except ValueError:
raise ParseError(detail="Invalid cursor parameter.")
input_cursor_value = request.GET.get(self.cursor_name)
page_value = request.GET.get("page")

if input_cursor_value is not None:
try:
input_cursor = cursor_cls.from_string(input_cursor_value)
except ValueError:
raise ParseError(detail="Invalid cursor parameter.")
elif page_value is not None:
try:
page_number = int(page_value)
except ValueError:
raise ParseError(detail="Page parameter must be a valid integer.")

if page_number < 1:
raise ParseError(detail="Page parameter must be greater than or equal to 1.")

input_cursor = cursor_cls(per_page, page_number - 1, False)
else:
input_cursor = cursor_cls(per_page, 0, False)
Comment thread
akoweicollinxx marked this conversation as resolved.

if not paginator:
if group_by_field_name:
Expand Down Expand Up @@ -718,7 +734,7 @@ def paginate(
"grouped_by": group_by_field_name,
"sub_grouped_by": sub_group_by_field_name,
"total_count": (cursor_result.hits),
"next_cursor": str(cursor_result.next),
"next_cursor": str(cursor_result.next) if cursor_result.next.has_results else None,
"prev_cursor": str(cursor_result.prev),
"next_page_results": cursor_result.next.has_results,
"prev_page_results": cursor_result.prev.has_results,
Comment on lines 736 to 740
Expand Down