Skip to content

Commit f2f2923

Browse files
committed
Code refactor and Added ModelController Doc
1 parent 8363066 commit f2f2923

File tree

10 files changed

+647
-150
lines changed

10 files changed

+647
-150
lines changed

docs/model_controller/index.md

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
# **Model APIController**
2+
!!! Note
3+
`ModelController` is only available from **v0.19.5**
4+
5+
**Model Controllers** inherit from the `ControllerBase` class and provide two important variables: `model_config` and `model_service`.
6+
These variables guide the route generation, schema generation, and model operations.
7+
8+
For instance, let's create a ModelController for an Event model defined as follows:
9+
10+
Model Controllers inherits from `ControllerBase` class and provides and provides two important variables `model_config` and `model_service`,
11+
which guides its route generation, schema generation or model operations.
12+
13+
For example, Let's create `ModelController` for an `Event` model defined below,
14+
15+
```python
16+
from django.db import models
17+
18+
class Category(models.Model):
19+
title = models.CharField(max_length=100)
20+
21+
class Event(models.Model):
22+
title = models.CharField(max_length=100)
23+
category = models.OneToOneField(
24+
Category, null=True, blank=True, on_delete=models.SET_NULL, related_name='events'
25+
)
26+
start_date = models.DateField()
27+
end_date = models.DateField()
28+
29+
def __str__(self):
30+
return self.title
31+
32+
```
33+
Then, in `api.py`, we create an `EventModelController`:
34+
35+
```python
36+
from ninja_extra import (
37+
ModelConfig,
38+
ModelControllerBase,
39+
ModelSchemaConfig,
40+
api_controller,
41+
NinjaExtraAPI
42+
)
43+
from .models import Event
44+
45+
@api_controller("/events")
46+
class EventModelController(ModelControllerBase):
47+
model_config = ModelConfig(
48+
model=Event,
49+
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
50+
)
51+
52+
api = NinjaExtraAPI()
53+
api.register_controllers(EventModelController)
54+
55+
```
56+
57+
Model Controllers require the `ninja-schema` package for auto schema generation, which can be installed with:
58+
```shell
59+
pip install ninja-schema
60+
```
61+
62+
## **Model Configuration**
63+
`ModelConfig` is a Pydantic schema used for validating and configuring Model Controller behaviors. Configuration options include:
64+
65+
- **model**: A required field that holds the Django model type for the Model Controller.
66+
- **allowed_routes**: A list of API actions allowed to be generated in the Model Controller. The default value is `["create", "find_one", "update", "patch", "delete", "list"]`.
67+
- **create_schema**: An optional Pydantic schema that describes the data input types for a `create` or `POST` operation in the Model Controller. The default value is `None`. If not provided, the `ModelController` will create a new schema based on the `schema_config` option.
68+
- **update_schema**: An optional Pydantic schema that describes the data input types for an `update` or `PUT` operation in the Model Controller. The default value is `None`. If not provided, the `create_schema` will be used if available, or a new schema will be generated based on the `schema_config` option.
69+
- **retrieve_schema**: An optional Pydantic schema output that describes the data output types for various operations. The default value is `None`. If not provided, the `ModelController` will generate a schema based on the `schema_config` option.
70+
- **patch_schema**: An optional Pydantic schema output that describes the data input types for `patch/PATCH` operations. The default value is `None`. If not provided, the `ModelController` will generate a schema with all of its fields optional.
71+
- **schema_config**: This is also a required field that describes how schema should be generated as required by Model Controller operations. Configuration options include:
72+
- `include`: List of Fields to be included. The default is `__all__`.
73+
- `exclude`: List of Fields to be excluded. The default is `[]`.
74+
- `optional`: List of Fields to be forced as optional. The default is `[pk]`.
75+
- `depth`: Depth to nest schema generation.
76+
- `read_only_fields`: List of fields to be excluded when generating input schemas for create, update, and patch operations.
77+
- `write_only_fields`: List of fields to be excluded when generating output schemas for find_one and list operations.
78+
- **pagination**: This is required for the model `list/GET` operation to avoid sending `100_000` items at once in a request. The pagination configuration requires a `ModelPagination` Pydantic schema object to be configured. Options include:
79+
- `klass`: The pagination class of type `PaginationBase`. The default is `PageNumberPaginationExtra`.
80+
- `paginator_kwargs`: A dictionary value for `PaginationBase` initialization. The default is None.
81+
- `pagination_schema`: A Pydantic generic schema that will be combined with `retrieve_schema` to generate a response schema for `list/GET `operation.
82+
83+
For example, if you want to use `ninja` pagination like `LimitOffsetPagination`:
84+
85+
```python
86+
from ninja.pagination import LimitOffsetPagination
87+
from ninja_extra.schemas import NinjaPaginationResponseSchema
88+
from ninja_extra import (
89+
ModelConfig,
90+
ModelControllerBase,
91+
api_controller,
92+
ModelPagination
93+
)
94+
95+
@api_controller("/events")
96+
class EventModelController(ModelControllerBase):
97+
model_config = ModelConfig(
98+
model=Event,
99+
pagination=ModelPagination(
100+
klass=LimitOffsetPagination,
101+
pagination_schema=NinjaPaginationResponseSchema
102+
),
103+
)
104+
105+
```
106+
107+
## **More on Model Controller Operations**
108+
In NinjaExtra Model Controller, the controller's behavior can be controlled by what is provided in the `allowed_routes` list within the `model_config` option.
109+
110+
For example, you can create a read-only controller like this:
111+
112+
```python
113+
from ninja_extra import api_controller, ModelControllerBase, ModelConfig, ModelSchemaConfig
114+
from .models import Event
115+
116+
@api_controller("/events")
117+
class EventModelController(ModelControllerBase):
118+
model_config = ModelConfig(
119+
model=Event,
120+
allowed_routes=['find_one', 'list'],
121+
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
122+
)
123+
124+
```
125+
This will only create `GET/{id}` and `GET/` routes for listing.
126+
127+
You can also add more endpoints to the existing `EventModelController`. For example:
128+
129+
```python
130+
from ninja_extra import api_controller, http_get, ModelControllerBase, ModelConfig, ModelSchemaConfig
131+
from .models import Event
132+
133+
@api_controller("/events")
134+
class EventModelController(ModelControllerBase):
135+
model_config = ModelConfig(
136+
model=Event,
137+
allowed_routes=['find_one', 'list'],
138+
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
139+
)
140+
141+
@http_get('/subtract',)
142+
def subtract(self, a: int, b: int):
143+
"""Subtracts a from b"""
144+
return {"result": a - b}
145+
146+
```
147+
148+
## **Model Service**
149+
Every model controller has a `ModelService` instance created during runtime to manage model interaction with the controller.
150+
Usually, these model service actions would have been part of the model controller,
151+
but they are abstracted to a service to allow a more dynamic approach.
152+
153+
```python
154+
class ModelService(ModelServiceBase):
155+
"""
156+
Model Service for Model Controller model CRUD operations with simple logic for simple models.
157+
158+
It's advised to override this class if you have a complex model.
159+
"""
160+
def __init__(self, model: Type[DjangoModel]) -> None:
161+
self.model = model
162+
163+
# ... (other CRUD methods)
164+
165+
```
166+
These actions are called based on the ongoing action on the model controller or
167+
based on the request being handled by the model controller.
168+
169+
### **Using Custom Model Service**
170+
Overriding a `ModelService` in a Model Controller is more important than overriding a route operation.
171+
The default `ModelService` used in the Model Controller is designed for simple Django models.
172+
It's advised to override the `ModelService` if you have a complex model.
173+
174+
For example, if you want to change the way the `Event` model is being saved:
175+
```python
176+
from ninja_extra import ModelService
177+
178+
class EventModelService(ModelService):
179+
def create(self, schema: PydanticModel, **kwargs: Any) -> Any:
180+
data = schema.dict(by_alias=True)
181+
data.update(kwargs)
182+
183+
instance = self.model._default_manager.create(**data)
184+
return instance
185+
186+
187+
```
188+
And then in `api.py`
189+
```python
190+
from ninja_extra import (
191+
ModelConfig,
192+
ModelControllerBase,
193+
ModelSchemaConfig,
194+
api_controller,
195+
)
196+
from .service import EventModelService
197+
from .models import Event
198+
199+
@api_controller("/events")
200+
class EventModelController(ModelControllerBase):
201+
service = EventModelService(model=Event)
202+
model_config = ModelConfig(
203+
model=Event,
204+
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
205+
)
206+
207+
```
208+
### **ModelController and ModelService Together**
209+
It's also possible to merge the controller and the model service together if needed:
210+
211+
For example, using the `EventModelService` we created
212+
```python
213+
from ninja_extra import (
214+
ModelConfig,
215+
ModelControllerBase,
216+
ModelSchemaConfig,
217+
api_controller,
218+
)
219+
from .service import EventModelService
220+
from .models import Event
221+
222+
@api_controller("/events")
223+
class EventModelController(ModelControllerBase, EventModelService):
224+
model_config = ModelConfig(
225+
model=Event,
226+
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
227+
)
228+
229+
def __init__(self):
230+
EventModelService.__init__(self, model=Event)
231+
self.service = self # This will expose the functions to the service attribute
232+
233+
```
234+
235+
## **Model Endpoint Factory**
236+
The `ModelEndpointFactory` is a factory class used by the Model Controller to generate endpoints seamlessly.
237+
It can also be used directly in any NinjaExtra Controller for the same purpose.
238+
239+
For example, if we want to add an `Event` to a new `Category`, we can do so as follows:
240+
```python
241+
from typing import Any
242+
from pydantic import BaseModel
243+
from ninja_extra import (
244+
ModelConfig,
245+
ModelControllerBase,
246+
ModelSchemaConfig,
247+
api_controller,
248+
ModelEndpointFactory
249+
)
250+
from .models import Event, Category
251+
252+
class CreateCategorySchema(BaseModel):
253+
title: str
254+
255+
class CategorySchema(BaseModel):
256+
id: str
257+
title: str
258+
259+
@api_controller("/events")
260+
class EventModelController(ModelControllerBase):
261+
model_config = ModelConfig(
262+
model=Event,
263+
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
264+
)
265+
266+
add_event_to_new_category = ModelEndpointFactory.create(
267+
path="/{int:event_id}/new-category",
268+
schema_in=CreateCategorySchema,
269+
schema_out=CategorySchema,
270+
custom_handler=lambda self, data, **kw: self.handle_add_event_to_new_category(data, **kw)
271+
)
272+
273+
def handle_add_event_to_new_category(self, data: CreateCategorySchema, **kw: Any) -> Category:
274+
event = self.service.get_one(pk=kw['event_id'])
275+
category = Category.objects.create(title=data.title)
276+
event.category = category
277+
event.save()
278+
return category
279+
280+
```
281+
282+
In the above example, we created an endpoint `POST /{int:event_id}/new-category` using `ModelEndpointFactory.create`
283+
and passed in input and output schemas along with a custom handler.
284+
By passing in a `custom_handler`, the generated route function will delegate its handling action to the provided
285+
`custom_handler` instead of calling `service.create`.
286+
287+
### **QueryGetter and ObjectGetter**
288+
`ModelEndpointFactory` exposes a more flexible way to get a model object or get a queryset filter in the case of
289+
`ModelEndpointFactory.find_one` and `ModelEndpointFactory.list`, respectively.
290+
291+
For example, to retrieve the category of an event (not practical but for illustration):
292+
```python
293+
from ninja_extra import (
294+
ModelConfig,
295+
ModelControllerBase,
296+
ModelSchemaConfig,
297+
api_controller,
298+
ModelEndpointFactory
299+
)
300+
from .models import Event, Category
301+
302+
@api_controller("/events")
303+
class EventModelController(ModelControllerBase):
304+
model_config = ModelConfig(
305+
model=Event,
306+
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
307+
)
308+
309+
get_event_category = ModelEndpointFactory.find_one(
310+
path="/{int:event_id}/category",
311+
schema_out=CategorySchema,
312+
lookup_param='event_id',
313+
object_getter=lambda self, pk, **kw: self.service.get_one(pk=pk).category
314+
)
315+
316+
```
317+
In the above example, we created a `get_event_category` endpoint using `ModelEndpointFactory.find_one` and
318+
provided an `object_getter` as a callback for fetching the model based on the `event_id`.
319+
320+
On the other hand, you can have a case where you need to list events by `category_id`:
321+
```python
322+
from ninja_extra import (
323+
ModelConfig,
324+
ModelControllerBase,
325+
ModelSchemaConfig,
326+
api_controller,
327+
ModelEndpointFactory
328+
)
329+
from .models import Event, Category
330+
331+
@api_controller("/events")
332+
class EventModelController(ModelControllerBase):
333+
model_config = ModelConfig(
334+
model=Event,
335+
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
336+
)
337+
338+
get_events_by_category = ModelEndpointFactory.list(
339+
path="/category/{int:category_id}/",
340+
schema_out=model_config.retrieve_schema,
341+
lookup_param='category_id',
342+
queryset_getter=lambda self, **kw: Category.objects.filter(pk=kw['category_id']).first().events.all()
343+
)
344+
345+
```
346+
By using `ModelEndpointFactory.list` and `queryset_getter`, you can quickly set up a list endpoint that returns events belonging to a category.
347+
Note that our `queryset_getter` may fail if an invalid ID is supplied, as this is just an illustration.
348+
349+
Also, keep in mind that `model_config` settings like `create_schema`, `retrieve_schema`, `patch_schema`, and `update_schema`
350+
are all available after ModelConfig instantiation.
351+
352+
### **Path and Query Parameters**
353+
354+
In `ModelEndpointFactory`, path parameters are parsed to identify both `path` and `query` parameters.
355+
These parameters are then created as fields within the Ninja input schema and resolved during the request,
356+
passing them as kwargs to the handler.
357+
358+
For example,
359+
```python
360+
list_post_tags = ModelEndpointFactory.list(
361+
path="/{int:id}/tags/{post_id}?query=int&query1=int",
362+
schema_out=model_config.retrieve_schema,
363+
queryset_getter=lambda self, **kw: self.list_post_tags_query(**kw)
364+
)
365+
366+
def list_post_tags_query(self, **kwargs):
367+
assert kwargs['id']
368+
assert kwargs['query']
369+
assert kwargs['query1']
370+
post_id = kwargs['post_id']
371+
return Post.objects.filter(id=post_id).first().tags.all()
372+
373+
```
374+
375+
In this example, the path `/{int:id}/tags/{post_id}?query=int&query1=int` generates two path parameters `['id:int', 'post_id:str']`
376+
and two query parameters `['query:int', 'query1:int']`.
377+
These parameters are bundled into the Ninja input schema and resolved during the request, passing them as kwargs to the route handler.
378+
379+
Note that when `path` and `query` parameters are defined they are added to ninja schema input as a required field and, not optional.
380+
Also, path and query data types must be compatible with Django URL converters.

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ nav:
3737
- Index: api_controller/index.md
3838
- Controller Routes: api_controller/api_controller_route.md
3939
- Controller Permissions: api_controller/api_controller_permission.md
40+
- ModelAPIController: model_controller/index.md
4041
- Usage:
4142
- Quick Tutorial: tutorial/index.md
4243
- Authentication: tutorial/authentication.md

0 commit comments

Comments
 (0)