|
| 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. |
0 commit comments