# 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>
127 lines
4 KiB
Python
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"
|