oncall-engine/engine/common/api_helpers/paginators.py
Michael Derynck f79445fbcb
Fix pagination behavior when page # exceeds search results (#4817)
# What this PR does
Change pagination to return last available page if the page number
exceeds the pages available instead of returning 404. This came up from
if the user is on a page other than the first and they enter a search
and the number of search results are smaller than what would be needed
to reach the current page number it would give a blank page and 404.

## Which issue(s) this PR closes

Related to [issue link here]

<!--
*Note*: If you want the issue to be auto-closed once the PR is merged,
change "Related to" to "Closes" in the line above.
If you have more than one GitHub issue that this PR closes, be sure to
preface
each issue link with a [closing
keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue).
This ensures that the issue(s) are auto-closed once the PR has been
merged.
-->

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] Added the relevant release notes label (see labels prefixed w/
`release:`). These labels dictate how your PR will
    show up in the autogenerated release notes.

---------

Co-authored-by: Rares Mardare <rares.mardare@grafana.com>
2024-08-19 13:59:01 +00:00

127 lines
4 KiB
Python

import typing
from django.core.paginator import EmptyPage
from rest_framework.pagination import BasePagination, CursorPagination, PageNumberPagination
from rest_framework.response import Response
from common.api_helpers.utils import create_engine_url
PaginatedData = typing.List[typing.Any]
class BasePaginatedResponseData(typing.TypedDict):
next: str | None
previous: str | None
results: PaginatedData
page_size: int
class PageBasedPaginationResponseData(BasePaginatedResponseData):
count: int
current_page_number: int
total_pages: int
class BasePathPrefixedPagination(BasePagination):
max_page_size = 100
page_query_param = "page"
page_size_query_param = "perpage"
def paginate_queryset(self, queryset, request, view=None):
request.build_absolute_uri = lambda: create_engine_url(request.get_full_path())
return super().paginate_queryset(queryset, request, view)
class PathPrefixedPagePagination(BasePathPrefixedPagination, PageNumberPagination):
def get_paginated_response(self, data: PaginatedData) -> Response:
response = super().get_paginated_response(data)
response.data.update(
{
"page_size": self.get_page_size(self.request),
"current_page_number": self.page.number,
"total_pages": self.page.paginator.num_pages,
}
)
return response
def get_paginated_response_schema(self, schema):
paginated_schema = super().get_paginated_response_schema(schema)
paginated_schema["properties"].update(
{
"page_size": {"type": "integer"},
"current_page_number": {"type": "integer"},
"total_pages": {"type": "integer"},
}
)
return paginated_schema
def paginate_queryset(self, queryset, request, view=None):
request.build_absolute_uri = lambda: create_engine_url(request.get_full_path())
per_page = request.query_params.get(self.page_size_query_param, self.page_size)
try:
per_page = int(per_page)
except ValueError:
per_page = self.page_size
if per_page < 1:
per_page = self.page_size
paginator = self.django_paginator_class(queryset, per_page)
page_number = request.query_params.get(self.page_query_param, 1)
try:
page_number = int(page_number)
except ValueError:
page_number = 1
if page_number < 1:
page_number = 1
try:
self.page = self.get_page(page_number, paginator)
except EmptyPage:
self.page = paginator.page(paginator.num_pages)
if paginator.num_pages > 1 and self.template is not None:
self.display_page_controls = True
self.request = request
return list(self.page)
def get_page(self, page_number, paginator):
try:
return paginator.page(page_number)
except EmptyPage:
return paginator.page(paginator.num_pages)
class PathPrefixedCursorPagination(BasePathPrefixedPagination, CursorPagination):
def get_paginated_response(self, data: PaginatedData) -> Response:
response = super().get_paginated_response(data)
response.data.update({"page_size": self.page_size})
return response
def get_paginated_response_schema(self, schema):
paginated_schema = super().get_paginated_response_schema(schema)
paginated_schema["properties"].update({"page_size": {"type": "integer"}})
return paginated_schema
class HundredPageSizePaginator(PathPrefixedPagePagination):
page_size = 100
class FiftyPageSizePaginator(PathPrefixedPagePagination):
page_size = 50
class TwentyFivePageSizePaginator(PathPrefixedPagePagination):
page_size = 25
class FifteenPageSizePaginator(PathPrefixedPagePagination):
page_size = 15
class AlertGroupCursorPaginator(PathPrefixedCursorPagination):
page_size = 25
ordering = "-started_at"