Skip to content

Commit 67c855e

Browse files
authored
Merge pull request #1426 from zhaozigu/page_size
PageNumberPagination support page_size as API param
2 parents a20aedf + 31161a5 commit 67c855e

File tree

5 files changed

+187
-11
lines changed

5 files changed

+187
-11
lines changed

docs/docs/guides/response/pagination.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,14 @@ you can also set custom page_size value individually per view:
7979
def list_users(...
8080
```
8181

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

84+
Example query:
85+
```
86+
/api/users?page=2&page_size=20
87+
```
88+
89+
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.
8390

8491
## Accessing paginator parameters in view function
8592

ninja/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class Settings(BaseModel):
1111
"ninja.pagination.LimitOffsetPagination", alias="NINJA_PAGINATION_CLASS"
1212
)
1313
PAGINATION_PER_PAGE: int = Field(100, alias="NINJA_PAGINATION_PER_PAGE")
14+
PAGINATION_MAX_PER_PAGE_SIZE: int = Field(100, alias="NINJA_MAX_PER_PAGE_SIZE")
1415
PAGINATION_MAX_LIMIT: int = Field(inf, alias="NINJA_PAGINATION_MAX_LIMIT") # type: ignore
1516

1617
# Throttling

ninja/pagination.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,22 +122,34 @@ async def apaginate_queryset(
122122
class PageNumberPagination(AsyncPaginationBase):
123123
class Input(Schema):
124124
page: int = Field(1, ge=1)
125+
page_size: Optional[int] = Field(None, ge=1)
125126

126127
def __init__(
127-
self, page_size: int = settings.PAGINATION_PER_PAGE, **kwargs: Any
128+
self,
129+
page_size: int = settings.PAGINATION_PER_PAGE,
130+
max_page_size: int = settings.PAGINATION_MAX_PER_PAGE_SIZE,
131+
**kwargs: Any,
128132
) -> None:
129133
self.page_size = page_size
134+
self.max_page_size = max_page_size
130135
super().__init__(**kwargs)
131136

137+
def _get_page_size(self, requested_page_size: Optional[int]) -> int:
138+
if requested_page_size is None:
139+
return self.page_size
140+
141+
return min(requested_page_size, self.max_page_size)
142+
132143
def paginate_queryset(
133144
self,
134145
queryset: QuerySet,
135146
pagination: Input,
136147
**params: Any,
137148
) -> Any:
138-
offset = (pagination.page - 1) * self.page_size
149+
page_size = self._get_page_size(pagination.page_size)
150+
offset = (pagination.page - 1) * page_size
139151
return {
140-
"items": queryset[offset : offset + self.page_size],
152+
"items": queryset[offset : offset + page_size],
141153
"count": self._items_count(queryset),
142154
} # noqa: E203
143155

@@ -147,11 +159,14 @@ async def apaginate_queryset(
147159
pagination: Input,
148160
**params: Any,
149161
) -> Any:
150-
offset = (pagination.page - 1) * self.page_size
162+
page_size = self._get_page_size(pagination.page_size)
163+
offset = (pagination.page - 1) * page_size
164+
151165
if isinstance(queryset, QuerySet):
152-
items = [obj async for obj in queryset[offset : offset + self.page_size]]
166+
items = [obj async for obj in queryset[offset : offset + page_size]]
153167
else:
154-
items = queryset[offset : offset + self.page_size]
168+
items = queryset[offset : offset + page_size]
169+
155170
return {
156171
"items": items,
157172
"count": await self._aitems_count(queryset),

tests/test_pagination.py

Lines changed: 157 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ def items_9(request):
154154
return list(range(100))
155155

156156

157+
@api.get("/items_10", response=List[int])
158+
@paginate(PageNumberPagination, page_size=10, max_page_size=20)
159+
def items_10(request):
160+
return ITEMS
161+
162+
157163
client = TestClient(api)
158164

159165

@@ -260,7 +266,106 @@ def test_case4():
260266
"type": "integer",
261267
},
262268
"required": False,
263-
}
269+
},
270+
{
271+
"in": "query",
272+
"name": "page_size",
273+
"schema": {
274+
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
275+
"title": "Page Size",
276+
},
277+
"required": False,
278+
},
279+
]
280+
281+
282+
def test_case4_page_size():
283+
response = client.get("/items_4?page=2&page_size=20").json()
284+
assert response == {"items": ITEMS[20:40], "count": 100}
285+
286+
schema = api.get_openapi_schema()["paths"]["/api/items_4"]["get"]
287+
# print(schema)
288+
assert schema["parameters"] == [
289+
{
290+
"in": "query",
291+
"name": "page",
292+
"schema": {
293+
"title": "Page",
294+
"default": 1,
295+
"minimum": 1,
296+
"type": "integer",
297+
},
298+
"required": False,
299+
},
300+
{
301+
"in": "query",
302+
"name": "page_size",
303+
"schema": {
304+
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
305+
"title": "Page Size",
306+
},
307+
"required": False,
308+
},
309+
]
310+
311+
312+
def test_case4_no_page_param():
313+
response = client.get("/items_4?page_size=20").json()
314+
assert response == {"items": ITEMS[0:20], "count": 100}
315+
316+
schema = api.get_openapi_schema()["paths"]["/api/items_4"]["get"]
317+
# print(schema)
318+
assert schema["parameters"] == [
319+
{
320+
"in": "query",
321+
"name": "page",
322+
"schema": {
323+
"title": "Page",
324+
"default": 1,
325+
"minimum": 1,
326+
"type": "integer",
327+
},
328+
"required": False,
329+
},
330+
{
331+
"in": "query",
332+
"name": "page_size",
333+
"schema": {
334+
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
335+
"title": "Page Size",
336+
},
337+
"required": False,
338+
},
339+
]
340+
341+
342+
def test_case4_out_of_range():
343+
response = client.get("/items_4?page=2&page_size=100").json()
344+
assert response == {"items": [], "count": 100}
345+
346+
schema = api.get_openapi_schema()["paths"]["/api/items_4"]["get"]
347+
# print(schema)
348+
assert schema["parameters"] == [
349+
{
350+
"in": "query",
351+
"name": "page",
352+
"schema": {
353+
"title": "Page",
354+
"default": 1,
355+
"minimum": 1,
356+
"type": "integer",
357+
},
358+
"required": False,
359+
},
360+
{
361+
"in": "query",
362+
"name": "page_size",
363+
"schema": {
364+
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
365+
"title": "Page Size",
366+
},
367+
"required": False,
368+
},
264369
]
265370

266371

@@ -281,14 +386,23 @@ def test_case5_no_kwargs():
281386
"type": "integer",
282387
},
283388
"required": False,
284-
}
389+
},
390+
{
391+
"in": "query",
392+
"name": "page_size",
393+
"schema": {
394+
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
395+
"title": "Page Size",
396+
},
397+
"required": False,
398+
},
285399
]
286400

287401

288402
def test_case6_pass_param_kwargs():
289403
page = 11
290404
response = client.get(f"/items_6?page={page}").json()
291-
assert response == {"items": [{"page": 11}], "count": 101}
405+
assert response == {"items": [{"page": 11, "page_size": None}], "count": 101}
292406

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

@@ -303,7 +417,16 @@ def test_case6_pass_param_kwargs():
303417
"type": "integer",
304418
},
305419
"required": False,
306-
}
420+
},
421+
{
422+
"in": "query",
423+
"name": "page_size",
424+
"schema": {
425+
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
426+
"title": "Page Size",
427+
},
428+
"required": False,
429+
},
307430
]
308431

309432

@@ -335,6 +458,36 @@ def test_case9():
335458
}
336459

337460

461+
def test_case10_max_page_size():
462+
response = client.get("/items_10?page=2&page_size=30").json()
463+
assert response == {"items": ITEMS[20:40], "count": 100}
464+
465+
schema = api.get_openapi_schema()["paths"]["/api/items_5"]["get"]
466+
467+
assert schema["parameters"] == [
468+
{
469+
"in": "query",
470+
"name": "page",
471+
"schema": {
472+
"title": "Page",
473+
"default": 1,
474+
"minimum": 1,
475+
"type": "integer",
476+
},
477+
"required": False,
478+
},
479+
{
480+
"in": "query",
481+
"name": "page_size",
482+
"schema": {
483+
"anyOf": [{"minimum": 1, "type": "integer"}, {"type": "null"}],
484+
"title": "Page Size",
485+
},
486+
"required": False,
487+
},
488+
]
489+
490+
338491
@override_settings(NINJA_PAGINATION_MAX_LIMIT=1000)
339492
def test_10_max_limit_set():
340493
# reload to apply django settings

tests/test_pagination_async.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ async def items_page_number(request, **kwargs):
122122
client = TestAsyncClient(api)
123123

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

127127

128128
@pytest.mark.skipif(django.VERSION[:2] < (5, 0), reason="Requires Django 5.0+")

0 commit comments

Comments
 (0)