From dd9afaf0569fae039b0279bf60092724664987a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:55:18 +0000 Subject: [PATCH 1/3] Initial plan From 40e488bc518ecf3e555f01ef526aeb99f6bf64de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:04:47 +0000 Subject: [PATCH 2/3] fix(api): honor page param in paginator for public work-item lists --- .../plane/tests/unit/utils/test_paginator.py | 80 +++++++++++++++++++ apps/api/plane/utils/paginator.py | 26 ++++-- 2 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 apps/api/plane/tests/unit/utils/test_paginator.py diff --git a/apps/api/plane/tests/unit/utils/test_paginator.py b/apps/api/plane/tests/unit/utils/test_paginator.py new file mode 100644 index 00000000000..0d89752d0f4 --- /dev/null +++ b/apps/api/plane/tests/unit/utils/test_paginator.py @@ -0,0 +1,80 @@ +# 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): + return CursorResult( + results=[{"page_offset": cursor.offset}], + next=Cursor(limit, cursor.offset + 1, False, False), + 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 diff --git a/apps/api/plane/utils/paginator.py b/apps/api/plane/utils/paginator.py index 5ae4d38150c..a8a502f5087 100644 --- a/apps/api/plane/utils/paginator.py +++ b/apps/api/plane/utils/paginator.py @@ -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="Invalid page parameter.") + + if page_number < 1: + raise ParseError(detail="Invalid page parameter.") + + input_cursor = cursor_cls(per_page, page_number - 1, False) + else: + input_cursor = cursor_cls(per_page, 0, False) if not paginator: if group_by_field_name: @@ -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, From 59a18593efaf33570d19412096284fbaa772b914 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:06:29 +0000 Subject: [PATCH 3/3] test(api): refine paginator page validation and cursor fixture --- apps/api/plane/tests/unit/utils/test_paginator.py | 3 ++- apps/api/plane/utils/paginator.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/api/plane/tests/unit/utils/test_paginator.py b/apps/api/plane/tests/unit/utils/test_paginator.py index 0d89752d0f4..c91f34be933 100644 --- a/apps/api/plane/tests/unit/utils/test_paginator.py +++ b/apps/api/plane/tests/unit/utils/test_paginator.py @@ -10,9 +10,10 @@ 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, False), + next=Cursor(limit, cursor.offset + 1, False, has_next), prev=Cursor(limit, cursor.offset - 1, True, cursor.offset > 0), hits=2, max_hits=2, diff --git a/apps/api/plane/utils/paginator.py b/apps/api/plane/utils/paginator.py index a8a502f5087..93641a060aa 100644 --- a/apps/api/plane/utils/paginator.py +++ b/apps/api/plane/utils/paginator.py @@ -686,10 +686,10 @@ def paginate( try: page_number = int(page_value) except ValueError: - raise ParseError(detail="Invalid page parameter.") + raise ParseError(detail="Page parameter must be a valid integer.") if page_number < 1: - raise ParseError(detail="Invalid page parameter.") + raise ParseError(detail="Page parameter must be greater than or equal to 1.") input_cursor = cursor_cls(per_page, page_number - 1, False) else: