Skip to content

Commit 7aea0d9

Browse files
authored
Merge pull request #317 from eadwinCode/pagination_with_filter
feat: Added Django Ninja FilterSchema support to Pagination Decorator And Model Controller
2 parents 1608a77 + 9c26473 commit 7aea0d9

File tree

7 files changed

+322
-10
lines changed

7 files changed

+322
-10
lines changed

docs/api_controller/model_controller/02_model_configuration.md

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,84 @@ class EventModelController(ModelControllerBase):
128128
)
129129
```
130130

131+
### **Pagination with Filtering**
132+
133+
You can combine pagination with Django Ninja's `FilterSchema` to automatically add filtering capabilities to your Model Controller's list endpoint:
134+
135+
```python
136+
from typing import Optional
137+
from ninja import FilterSchema
138+
from ninja_extra import ModelConfig, ModelPagination
139+
from ninja_extra.pagination import PageNumberPaginationExtra
140+
141+
# Define a FilterSchema for your model
142+
class EventFilterSchema(FilterSchema):
143+
title: Optional[str] = None
144+
category: Optional[str] = None
145+
start_date__gte: Optional[str] = None
146+
147+
@api_controller("/events")
148+
class EventModelController(ModelControllerBase):
149+
model_config = ModelConfig(
150+
model=Event,
151+
pagination=ModelPagination(
152+
klass=PageNumberPaginationExtra,
153+
filter_schema=EventFilterSchema, # Add filtering support
154+
paginator_kwargs={"page_size": 25}
155+
)
156+
)
157+
```
158+
159+
This configuration automatically applies the `FilterSchema` to the list endpoint, allowing users to filter results:
160+
161+
```
162+
GET /api/events/?title=Conference&category=Tech&page=1&page_size=25
163+
```
164+
165+
### **Advanced Filtering with Custom Lookups**
166+
167+
Use Django Ninja's `FilterLookup` annotation for more sophisticated filtering:
168+
169+
```python
170+
from typing import Annotated, Optional
171+
from ninja import FilterSchema, FilterLookup
172+
173+
class AdvancedEventFilterSchema(FilterSchema):
174+
# Case-insensitive search
175+
title: Annotated[Optional[str], FilterLookup("title__icontains")] = None
176+
177+
# Date range filtering
178+
start_date_after: Annotated[Optional[str], FilterLookup("start_date__gte")] = None
179+
start_date_before: Annotated[Optional[str], FilterLookup("start_date__lte")] = None
180+
181+
# Related field filtering
182+
category: Annotated[Optional[str], FilterLookup("category__name__iexact")] = None
183+
184+
# Multiple field search
185+
search: Annotated[
186+
Optional[str],
187+
FilterLookup([
188+
"title__icontains",
189+
"description__icontains",
190+
"location__icontains"
191+
])
192+
] = None
193+
194+
@api_controller("/events")
195+
class EventModelController(ModelControllerBase):
196+
model_config = ModelConfig(
197+
model=Event,
198+
pagination=ModelPagination(
199+
klass=PageNumberPaginationExtra,
200+
filter_schema=AdvancedEventFilterSchema,
201+
paginator_kwargs={"page_size": 50}
202+
)
203+
)
204+
```
205+
206+
!!! info "Learn More About FilterSchema"
207+
For comprehensive documentation on FilterSchema features, custom expressions, combining filters, and advanced filtering techniques, visit: [https://django-ninja.dev/guides/input/filtering/](https://django-ninja.dev/guides/input/filtering/)
208+
131209
## **Route Configuration**
132210

133211
You can customize individual route behavior using route info dictionaries. Each route type (`create_route_info`, `list_route_info`, `find_one_route_info`, `update_route_info`, `patch_route_info`, `delete_route_info`) accepts various configuration parameters.
@@ -316,7 +394,13 @@ class EventModelController(ModelControllerBase):
316394
- Consider your data size when choosing pagination class
317395
- Use appropriate page sizes for your use case
318396

319-
4. **Async Support**:
397+
4. **Filtering**:
398+
- Use `FilterSchema` to provide flexible filtering on list endpoints
399+
- Leverage `FilterLookup` for complex database lookups (e.g., `__icontains`, `__gte`)
400+
- Consider indexing filtered fields in your database for performance
401+
- Document available filters in your API documentation
402+
403+
5. **Async Support**:
320404
- Enable `async_routes` when using async database operations
321405
- Implement custom async services for complex operations
322406
- Consider performance implications of async operations

docs/tutorial/pagination.md

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44

55
## **Properties**
66

7-
`def paginate(func_or_pgn_class: Any = NOT_SET, **paginator_params: Any) -> Callable[..., Any]:`
7+
`def paginate(func_or_pgn_class: Any = NOT_SET, filter_schema: Optional[Type[FilterSchema]] = None, **paginator_params: Any) -> Callable[..., Any]:`
88

99
- func_or_pgn_class: Defines a route function or a Pagination Class. default: `ninja_extra.pagination.LimitOffsetPagination`
10+
- filter_schema: Optional FilterSchema class from Django Ninja for filtering querysets before pagination
1011
- paginator_params: extra parameters for initialising Pagination Class
1112

1213
### **Using Ninja LimitOffsetPagination**
@@ -75,3 +76,127 @@ api.register_controllers(UserController)
7576
```
7677

7778
![Preview](../images/pagination_example.gif)
79+
80+
## **Pagination with Filtering**
81+
82+
You can combine pagination with Django Ninja's `FilterSchema` to provide both filtering and pagination capabilities. Filters are applied to the queryset **before** pagination.
83+
84+
!!! info "Learn More About FilterSchema"
85+
For comprehensive information about FilterSchema features, custom expressions, combining filters, and advanced filtering techniques, see the official Django Ninja documentation: [https://django-ninja.dev/guides/input/filtering/](https://django-ninja.dev/guides/input/filtering/)
86+
87+
### **Basic Filtering Example**
88+
89+
```python
90+
from typing import Optional
91+
from ninja import FilterSchema
92+
from ninja_extra.pagination import paginate, PageNumberPaginationExtra, PaginatedResponseSchema
93+
from ninja_extra import api_controller, route, NinjaExtraAPI
94+
from ninja import ModelSchema
95+
from myapp.models import Book
96+
97+
98+
class BookSchema(ModelSchema):
99+
class Config:
100+
model = Book
101+
model_fields = ['id', 'title', 'author', 'price', 'published_date']
102+
103+
104+
# Define a FilterSchema for your model
105+
class BookFilterSchema(FilterSchema):
106+
title: Optional[str] = None
107+
author: Optional[str] = None
108+
min_price: Optional[float] = None
109+
110+
111+
@api_controller('/books')
112+
class BookController:
113+
@route.get('', response=PaginatedResponseSchema[BookSchema])
114+
@paginate(PageNumberPaginationExtra, filter_schema=BookFilterSchema, page_size=20)
115+
def list_books(self):
116+
return Book.objects.all()
117+
118+
119+
api = NinjaExtraAPI(title='Books API')
120+
api.register_controllers(BookController)
121+
```
122+
123+
**Example API calls:**
124+
- `GET /api/books/?page=1&page_size=20` - Paginated results
125+
- `GET /api/books/?title=Python&page=1` - Filter by title and paginate
126+
- `GET /api/books/?author=John&min_price=10&page=2` - Multiple filters with pagination
127+
128+
### **Advanced Filtering with Custom Lookups**
129+
130+
Use Django Ninja's `FilterLookup` annotation for more complex filtering:
131+
132+
```python
133+
from typing import Annotated, Optional
134+
from ninja import FilterSchema, FilterLookup
135+
136+
137+
class AdvancedBookFilterSchema(FilterSchema):
138+
# Case-insensitive containment search
139+
title: Annotated[Optional[str], FilterLookup("title__icontains")] = None
140+
141+
# Exact match on author name
142+
author: Annotated[Optional[str], FilterLookup("author__name__iexact")] = None
143+
144+
# Greater than or equal to price
145+
min_price: Annotated[Optional[float], FilterLookup("price__gte")] = None
146+
147+
# Less than or equal to price
148+
max_price: Annotated[Optional[float], FilterLookup("price__lte")] = None
149+
150+
# Date range filtering
151+
published_after: Annotated[Optional[str], FilterLookup("published_date__gte")] = None
152+
153+
154+
@api_controller('/books')
155+
class BookController:
156+
@route.get('', response=PaginatedResponseSchema[BookSchema])
157+
@paginate(PageNumberPaginationExtra, filter_schema=AdvancedBookFilterSchema, page_size=20)
158+
def list_books(self):
159+
return Book.objects.all()
160+
```
161+
162+
### **Using FilterSchema with Model Controllers**
163+
164+
FilterSchema can also be integrated with Model Controllers through the `ModelPagination` configuration:
165+
166+
```python
167+
from ninja import FilterSchema
168+
from ninja_extra.controllers import ModelControllerBase
169+
from ninja_extra.controllers.model import ModelConfig, ModelPagination
170+
from ninja_extra.pagination import PageNumberPaginationExtra
171+
from myapp.models import Book
172+
173+
174+
class BookFilterSchema(FilterSchema):
175+
title: Optional[str] = None
176+
author__name: Optional[str] = None
177+
price__gte: Optional[float] = None
178+
179+
180+
class BookModelController(ModelControllerBase):
181+
model_config = ModelConfig(
182+
model=Book,
183+
pagination=ModelPagination(
184+
klass=PageNumberPaginationExtra,
185+
filter_schema=BookFilterSchema,
186+
paginator_kwargs={"page_size": 25}
187+
)
188+
)
189+
```
190+
191+
This configuration automatically applies the FilterSchema to the `list` endpoint of your Model Controller.
192+
193+
### **How It Works**
194+
195+
1. **Query Parameters**: When a request is made, both filter and pagination parameters are extracted from query parameters
196+
2. **Filtering**: The FilterSchema validates and applies filters to the queryset first
197+
3. **Pagination**: The filtered results are then paginated according to the pagination parameters
198+
4. **Response**: The paginated response includes filtered results with pagination metadata
199+
200+
### **OpenAPI Schema**
201+
202+
When using FilterSchema with pagination, the OpenAPI documentation automatically includes both filter parameters and pagination parameters, making your API self-documenting and easy to use.

ninja_extra/controllers/model/builder.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ def _register_list_endpoint(self) -> None:
155155
)
156156
if self._config.pagination.paginator_kwargs: # pragma: no cover
157157
paginate_kwargs.update(self._config.pagination.paginator_kwargs)
158+
# Add filter_schema if provided
159+
if self._config.pagination.filter_schema:
160+
paginate_kwargs["filter_schema"] = self._config.pagination.filter_schema
158161

159162
list_items = self._route_factory.list(
160163
path="/",

ninja_extra/controllers/model/endpoints.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from django.db.models import Model as DjangoModel
55
from django.db.models import QuerySet
6+
from ninja import FilterSchema
67
from ninja.constants import NOT_SET, NOT_SET_TYPE
78
from ninja.pagination import PaginationBase
89
from ninja.params import Body
@@ -392,6 +393,7 @@ def list(
392393
pagination_class: t.Optional[
393394
t.Type[PaginationBase]
394395
] = PageNumberPaginationExtra,
396+
filter_schema: t.Optional[t.Type[FilterSchema]] = None,
395397
**paginate_kwargs: t.Any,
396398
) -> ModelEndpointFunction:
397399
"""
@@ -408,7 +410,9 @@ def _setup(model_controller_type: t.Type["ModelControllerBase"]) -> t.Callable:
408410
)
409411

410412
if pagination_response_schema and pagination_class:
411-
list_items = paginate(pagination_class, **paginate_kwargs)(list_items)
413+
list_items = paginate(
414+
pagination_class, filter_schema=filter_schema, **paginate_kwargs
415+
)(list_items)
412416
return route.get(
413417
working_path,
414418
response=response

ninja_extra/pagination/decorator.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import inspect
22
import logging
3-
from typing import Any, Callable, Type, cast, overload
3+
from typing import Any, Callable, Optional, Type, cast, overload
44

5+
from ninja import FilterSchema
56
from ninja.constants import NOT_SET
67
from ninja.pagination import PaginationBase
78
from ninja.signature import is_async
@@ -22,13 +23,17 @@ def paginate() -> Callable[..., Any]: # pragma: no cover
2223

2324
@overload
2425
def paginate(
25-
func_or_pgn_class: Any = NOT_SET, **paginator_params: Any
26+
func_or_pgn_class: Any = NOT_SET,
27+
filter_schema: Optional[Type[FilterSchema]] = None,
28+
**paginator_params: Any,
2629
) -> Callable[..., Any]: # pragma: no cover
2730
...
2831

2932

3033
def paginate(
31-
func_or_pgn_class: Any = NOT_SET, **paginator_params: Any
34+
func_or_pgn_class: Any = NOT_SET,
35+
filter_schema: Optional[Type[FilterSchema]] = None,
36+
**paginator_params: Any,
3237
) -> Callable[..., Any]:
3338
isfunction = inspect.isfunction(func_or_pgn_class)
3439
is_not_set = func_or_pgn_class == NOT_SET
@@ -38,20 +43,25 @@ def paginate(
3843
)
3944

4045
if isfunction:
41-
return _inject_pagination(func_or_pgn_class, pagination_class)
46+
return _inject_pagination(
47+
func_or_pgn_class, pagination_class, filter_schema=filter_schema
48+
)
4249

4350
if not is_not_set:
4451
pagination_class = func_or_pgn_class
4552

4653
def wrapper(func: Callable[..., Any]) -> Any:
47-
return _inject_pagination(func, pagination_class, **paginator_params)
54+
return _inject_pagination(
55+
func, pagination_class, filter_schema=filter_schema, **paginator_params
56+
)
4857

4958
return wrapper
5059

5160

5261
def _inject_pagination(
5362
func: Callable[..., Any],
5463
paginator_class: Type[PaginationBase],
64+
filter_schema: Optional[Type[FilterSchema]] = None,
5565
**paginator_params: Any,
5666
) -> Callable[..., Any]:
5767
paginator: PaginationBase = paginator_class(**paginator_params)
@@ -61,7 +71,10 @@ def _inject_pagination(
6171
if is_async(func):
6272
paginator_operation_class = AsyncPaginatorOperation
6373
paginator_operation = paginator_operation_class(
64-
paginator=paginator, view_func=func, paginator_kwargs_name=paginator_kwargs_name
74+
paginator=paginator,
75+
view_func=func,
76+
paginator_kwargs_name=paginator_kwargs_name,
77+
filter_schema=filter_schema,
6578
)
6679

6780
return paginator_operation.as_view

0 commit comments

Comments
 (0)