Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PageNumberPagination support page_size as API param #1426

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions docs/docs/guides/response/pagination.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,14 @@ you can also set custom page_size value individually per view:
def list_users(...
```

In addition to the `page` parameter, you can also use the `page_size` parameter to dynamically adjust the number of records displayed per page:

Example query:
```
/api/users?page=2&page_size=20
```

This allows you to temporarily override the page size setting in your request. The request will use the specified `page_size` value if provided. Otherwise, it will use either the value specified in the decorator or the value from `PAGINATION_MAX_PER_PAGE_SIZE` in settings.py if no decorator value is set.

## Accessing paginator parameters in view function

Expand Down
1 change: 1 addition & 0 deletions ninja/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Settings(BaseModel):
"ninja.pagination.LimitOffsetPagination", alias="NINJA_PAGINATION_CLASS"
)
PAGINATION_PER_PAGE: int = Field(100, alias="NINJA_PAGINATION_PER_PAGE")
PAGINATION_MAX_PER_PAGE_SIZE: int = Field(100, alias="NINJA_MAX_PER_PAGE_SIZE")
PAGINATION_MAX_LIMIT: int = Field(inf, alias="NINJA_PAGINATION_MAX_LIMIT") # type: ignore

# Throttling
Expand Down
23 changes: 18 additions & 5 deletions ninja/pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,22 +116,34 @@ async def apaginate_queryset(
class PageNumberPagination(AsyncPaginationBase):
class Input(Schema):
page: int = Field(1, ge=1)
page_size: Optional[int] = Field(None, ge=1)

def __init__(
self, page_size: int = settings.PAGINATION_PER_PAGE, **kwargs: Any
self,
page_size: int = settings.PAGINATION_PER_PAGE,
max_page_size: int = settings.PAGINATION_MAX_PER_PAGE_SIZE,
**kwargs: Any,
) -> None:
self.page_size = page_size
self.max_page_size = max_page_size
super().__init__(**kwargs)

def _get_page_size(self, requested_page_size: Optional[int]) -> int:
if requested_page_size is None:
return self.page_size

return min(requested_page_size, self.max_page_size)

def paginate_queryset(
self,
queryset: QuerySet,
pagination: Input,
**params: Any,
) -> Any:
offset = (pagination.page - 1) * self.page_size
page_size = self._get_page_size(pagination.page_size)
offset = (pagination.page - 1) * page_size
return {
"items": queryset[offset : offset + self.page_size],
"items": queryset[offset : offset + page_size],
"count": self._items_count(queryset),
} # noqa: E203

Expand All @@ -141,9 +153,10 @@ async def apaginate_queryset(
pagination: Input,
**params: Any,
) -> Any:
offset = (pagination.page - 1) * self.page_size
page_size = self._get_page_size(pagination.page_size)
offset = (pagination.page - 1) * page_size
return {
"items": queryset[offset : offset + self.page_size],
"items": queryset[offset : offset + page_size],
"count": await self._aitems_count(queryset),
} # noqa: E203

Expand Down
161 changes: 157 additions & 4 deletions tests/test_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ def items_9(request):
return list(range(100))


@api.get("/items_10", response=List[int])
@paginate(PageNumberPagination, page_size=10, max_page_size=20)
def items_10(request):
return ITEMS


client = TestClient(api)


Expand Down Expand Up @@ -260,7 +266,106 @@ def test_case4():
"type": "integer",
},
"required": False,
}
},
{
"in": "query",
"name": "page_size",
"schema": {
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
"title": "Page Size",
},
"required": False,
},
]


def test_case4_page_size():
response = client.get("/items_4?page=2&page_size=20").json()
assert response == {"items": ITEMS[20:40], "count": 100}

schema = api.get_openapi_schema()["paths"]["/api/items_4"]["get"]
# print(schema)
assert schema["parameters"] == [
{
"in": "query",
"name": "page",
"schema": {
"title": "Page",
"default": 1,
"minimum": 1,
"type": "integer",
},
"required": False,
},
{
"in": "query",
"name": "page_size",
"schema": {
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
"title": "Page Size",
},
"required": False,
},
]


def test_case4_no_page_param():
response = client.get("/items_4?page_size=20").json()
assert response == {"items": ITEMS[0:20], "count": 100}

schema = api.get_openapi_schema()["paths"]["/api/items_4"]["get"]
# print(schema)
assert schema["parameters"] == [
{
"in": "query",
"name": "page",
"schema": {
"title": "Page",
"default": 1,
"minimum": 1,
"type": "integer",
},
"required": False,
},
{
"in": "query",
"name": "page_size",
"schema": {
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
"title": "Page Size",
},
"required": False,
},
]


def test_case4_out_of_range():
response = client.get("/items_4?page=2&page_size=100").json()
assert response == {"items": [], "count": 100}

schema = api.get_openapi_schema()["paths"]["/api/items_4"]["get"]
# print(schema)
assert schema["parameters"] == [
{
"in": "query",
"name": "page",
"schema": {
"title": "Page",
"default": 1,
"minimum": 1,
"type": "integer",
},
"required": False,
},
{
"in": "query",
"name": "page_size",
"schema": {
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
"title": "Page Size",
},
"required": False,
},
]


Expand All @@ -281,14 +386,23 @@ def test_case5_no_kwargs():
"type": "integer",
},
"required": False,
}
},
{
"in": "query",
"name": "page_size",
"schema": {
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
"title": "Page Size",
},
"required": False,
},
]


def test_case6_pass_param_kwargs():
page = 11
response = client.get(f"/items_6?page={page}").json()
assert response == {"items": [{"page": 11}], "count": 101}
assert response == {"items": [{"page": 11, "page_size": None}], "count": 101}

schema = api.get_openapi_schema()["paths"]["/api/items_6"]["get"]

Expand All @@ -303,7 +417,16 @@ def test_case6_pass_param_kwargs():
"type": "integer",
},
"required": False,
}
},
{
"in": "query",
"name": "page_size",
"schema": {
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
"title": "Page Size",
},
"required": False,
},
]


Expand Down Expand Up @@ -335,6 +458,36 @@ def test_case9():
}


def test_case10_max_page_size():
response = client.get("/items_10?page=2&page_size=30").json()
assert response == {"items": ITEMS[20:40], "count": 100}

schema = api.get_openapi_schema()["paths"]["/api/items_5"]["get"]

assert schema["parameters"] == [
{
"in": "query",
"name": "page",
"schema": {
"title": "Page",
"default": 1,
"minimum": 1,
"type": "integer",
},
"required": False,
},
{
"in": "query",
"name": "page_size",
"schema": {
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
"title": "Page Size",
},
"required": False,
},
]


@override_settings(NINJA_PAGINATION_MAX_LIMIT=1000)
def test_10_max_limit_set():
# reload to apply django settings
Expand Down
2 changes: 1 addition & 1 deletion tests/test_pagination_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,4 @@ async def items_page_number(request, **kwargs):
client = TestAsyncClient(api)

response = await client.get("/items_page_number?page=11")
assert response.json() == {"items": [{"page": 11}], "count": 101}
assert response.json() == {"items": [{"page": 11, "page_size": None}], "count": 101}