Model APIController
Model Controllers dynamically generate CRUD (Create, Read, Update, Delete) operations for a Django ORM model within a controller, based on specified configurations.
Model Controllers extend the ControllerBase class and introduce two configuration variables, namely model_config and model_service.
model_configis responsible for defining configurations related to routes and schema generationmodel_servicerefers to a class that manages CRUD (Create, Read, Update, Delete) operations for the specified model.
For example, consider the definition of an Event model in Django:
from django.db import models
class Category(models.Model):
title = models.CharField(max_length=100)
class Event(models.Model):
title = models.CharField(max_length=100)
category = models.OneToOneField(
Category, null=True, blank=True, on_delete=models.SET_NULL, related_name='events'
)
start_date = models.DateField()
end_date = models.DateField()
def __str__(self):
return self.title
Now, let's create a ModelController for the Event model. In the api.py file, we define an EventModelController:
from ninja_extra import (
ModelConfig,
ModelControllerBase,
ModelSchemaConfig,
api_controller,
NinjaExtraAPI
)
from .models import Event
@api_controller("/events")
class EventModelController(ModelControllerBase):
model_config = ModelConfig(
model=Event,
schema_config=ModelSchemaConfig(
read_only_fields=["id", "category"],
# if you want to extra configuration to the generated schemas
# extra_config_dict={
# 'title': 'EventCustomTitle',
# 'populate_by_name': True
# }
),
)
api = NinjaExtraAPI()
api.register_controllers(EventModelController)
It's important to note that Model Controllers rely on the ninja-schema package for automatic schema generation. To install the package, use the following command:
pip install ninja-schema
After installation, you can access the auto-generated API documentation by visiting http://localhost:8000/api/docs.
This documentation provides a detailed overview of the available routes, schemas, and functionalities
exposed by the EventModelController for the Event model.
Model Configuration
The ModelConfig is a Pydantic schema designed for validating and configuring the behavior of Model Controllers. Key configuration options include:
- model: A mandatory field representing the Django model type associated with the Model Controller.
- async_routes: Indicates if controller routes should be created as
asynchronousroute functions - allowed_routes: A list specifying the API actions permissible for generation in the Model Controller. The default value is
["create", "find_one", "update", "patch", "delete", "list"]. - create_schema: An optional Pydantic schema outlining the data input types for a
createorPOSToperation in the Model Controller. The default isNone. If not provided, theModelControllerwill generate a new schema based on theschema_configoption. - update_schema: An optional Pydantic schema detailing the data input types for an
updateorPUToperation in the Model Controller. The default isNone. If not provided, thecreate_schemawill be used if available, or a new schema will be generated based on theschema_configoption. - retrieve_schema: An optional Pydantic schema output defining the data output types for various operations. The default is
None. If not provided, theModelControllerwill generate a schema based on theschema_configoption. - patch_schema: An optional Pydantic schema output specifying the data input types for
patch/PATCHoperations. The default isNone. If not provided, theModelControllerwill generate a schema with all its fields optional. - schema_config: Another mandatory field describing the schema generation approach required by Model Controller operations. Configuration options encompass:
include: A list of Fields to be included. The default is__all__.exclude: A list of Fields to be excluded. The default is[].optional: A list of Fields to be enforced as optional. The default is[pk].depth: The depth for nesting schema generation.read_only_fields: A list of fields to be excluded when generating input schemas for create, update, and patch operations.write_only_fields: A list of fields to be excluded when generating output schemas for find_one and list operations.extra_config_dict: A dictionary of extra configuration to be added to the generated schemas. Options must be valid Pydantic configuration options.
-
pagination: A requisite for the model
list/GEToperation to prevent sending100_000items at once in a request. The pagination configuration mandates aModelPaginationPydantic schema object for setup. Options encompass:klass: The pagination class of typePaginationBase. The default isPageNumberPaginationExtra.paginator_kwargs: A dictionary value forPaginationBaseinitialization. The default is None.pagination_schema: A Pydantic generic schema that combines withretrieve_schemato generate a response schema for thelist/GEToperation.
For instance, if opting for
ninjapagination likeLimitOffsetPagination:from ninja.pagination import LimitOffsetPagination from ninja_extra.schemas import NinjaPaginationResponseSchema from ninja_extra import ( ModelConfig, ModelControllerBase, api_controller, ModelPagination ) @api_controller("/events") class EventModelController(ModelControllerBase): model_config = ModelConfig( model=Event, pagination=ModelPagination( klass=LimitOffsetPagination, pagination_schema=NinjaPaginationResponseSchema ), )
More on Model Controller Operations
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.
For example, you can create a read-only controller like this:
from ninja_extra import api_controller, ModelControllerBase, ModelConfig, ModelSchemaConfig
from .models import Event
@api_controller("/events")
class EventModelController(ModelControllerBase):
model_config = ModelConfig(
model=Event,
allowed_routes=['find_one', 'list'],
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
)
GET/{id} and GET/ routes for listing.
You can also add more endpoints to the existing EventModelController. For example:
from ninja_extra import api_controller, http_get, ModelControllerBase, ModelConfig, ModelSchemaConfig
from .models import Event
@api_controller("/events")
class EventModelController(ModelControllerBase):
model_config = ModelConfig(
model=Event,
allowed_routes=['find_one', 'list'],
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
)
@http_get('/subtract',)
def subtract(self, a: int, b: int):
"""Subtracts a from b"""
return {"result": a - b}
Model Service
Every model controller has a ModelService instance created during runtime to manage model interaction with the controller.
Usually, these model service actions would have been part of the model controller,
but they are abstracted to a service to allow a more dynamic approach.
class ModelService(ModelServiceBase):
"""
Model Service for Model Controller model CRUD operations with simple logic for simple models.
It's advised to override this class if you have a complex model.
"""
def __init__(self, model: Type[DjangoModel]) -> None:
self.model = model
# ... (other CRUD methods)
Using Custom Model Service
Overriding a ModelService in a Model Controller is more important than overriding a route operation.
The default ModelService used in the Model Controller is designed for simple Django models.
It's advised to override the ModelService if you have a complex model.
For example, if you want to change the way the Event model is being saved:
from ninja_extra import ModelService
class EventModelService(ModelService):
def create(self, schema: PydanticModel, **kwargs: Any) -> Any:
data = schema.dict(by_alias=True)
data.update(kwargs)
instance = self.model._default_manager.create(**data)
return instance
api.py
from ninja_extra import (
ModelConfig,
ModelControllerBase,
ModelSchemaConfig,
api_controller,
)
from .service import EventModelService
from .models import Event
@api_controller("/events")
class EventModelController(ModelControllerBase):
service = EventModelService(model=Event)
model_config = ModelConfig(
model=Event,
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
)
Enable Async Routes
In model_config, set async_routes to True
from ninja_extra import (
ModelConfig,
ModelControllerBase,
ModelSchemaConfig,
api_controller,
)
from .service import EventModelService
from .models import Event
@api_controller("/events")
class EventModelController(ModelControllerBase):
service = EventModelService(model=Event)
model_config = ModelConfig(
model=Event,
async_routes=True,
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
)
By setting the async_routes parameter to True in the model_config, the ModelController
dynamically switches between ModelAsyncEndpointFactory and
ModelEndpointFactory to generate
either asynchronous or synchronous endpoints based on the configuration.
ModelController and ModelService Together
It's also possible to merge the controller and the model service together if needed:
For example, using the EventModelService we created
from ninja_extra import (
ModelConfig,
ModelControllerBase,
ModelSchemaConfig,
api_controller,
)
from .service import EventModelService
from .models import Event
@api_controller("/events")
class EventModelController(ModelControllerBase, EventModelService):
model_config = ModelConfig(
model=Event,
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
)
def __init__(self):
EventModelService.__init__(self, model=Event)
self.service = self # This will expose the functions to the service attribute
Model Endpoint Factory
The ModelEndpointFactory is a factory class used by the Model Controller to generate endpoints seamlessly.
It can also be used directly in any NinjaExtra Controller for the same purpose.
For example, if we want to add an Event to a new Category, we can do so as follows:
from typing import Any
from pydantic import BaseModel
from ninja_extra import (
ModelConfig,
ModelControllerBase,
ModelSchemaConfig,
api_controller,
ModelEndpointFactory
)
from .models import Event, Category
class CreateCategorySchema(BaseModel):
title: str
class CategorySchema(BaseModel):
id: str
title: str
@api_controller("/events")
class EventModelController(ModelControllerBase):
model_config = ModelConfig(
model=Event,
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
)
add_event_to_new_category = ModelEndpointFactory.create(
path="/{int:event_id}/new-category",
schema_in=CreateCategorySchema,
schema_out=CategorySchema,
custom_handler=lambda self, data, **kw: self.handle_add_event_to_new_category(data, **kw)
)
def handle_add_event_to_new_category(
self, data: CreateCategorySchema, event_id: int, **kw: Any
) -> Category:
event = self.service.get_one(pk=event_id)
category = Category.objects.create(title=data.title)
event.category = category
event.save()
return category
In the above example, we created an endpoint POST /{int:event_id}/new-category using ModelEndpointFactory.create
and passed in input and output schemas along with a custom handler.
By passing in a custom_handler, the generated route function will delegate its handling action to the provided
custom_handler instead of calling service.create.
Async Model Endpoint Factory
The ModelAsyncEndpointFactory shares the same API interface as ModelEndpointFactory
but is specifically designed for generating asynchronous endpoints.
we can create same example as with ModelEndpointFactory,
For example:
from typing import Any
from pydantic import BaseModel
from ninja_extra import (
ModelConfig,
ModelControllerBase,
ModelSchemaConfig,
api_controller,
ModelAsyncEndpointFactory
)
from .models import Event, Category
class CreateCategorySchema(BaseModel):
title: str
class CategorySchema(BaseModel):
id: str
title: str
@api_controller("/events")
class EventModelController(ModelControllerBase):
model_config = ModelConfig(
model=Event,
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
)
add_event_to_new_category = ModelAsyncEndpointFactory.create(
path="/{int:event_id}/new-category",
schema_in=CreateCategorySchema,
schema_out=CategorySchema,
custom_handler=lambda self, data, **kw: self.handle_add_event_to_new_category(data, **kw)
)
async def handle_add_event_to_new_category(
self, data: CreateCategorySchema, event_id: int, **kw: Any
) -> Category:
event = await self.service.get_one_async(pk=event_id)
category = Category.objects.create(title=data.title)
event.category = category
event.save()
return category
add_event_to_new_category as an asynchronous function and converted
handle_add_event_to_new_category to asynchronous function as well.
QueryGetter and ObjectGetter
ModelEndpointFactory exposes a more flexible way to get a model object or get a queryset filter in the case of
ModelEndpointFactory.find_one and ModelEndpointFactory.list, respectively.
For example, to retrieve the category of an event (not practical but for illustration):
from ninja_extra import (
ModelConfig,
ModelControllerBase,
ModelSchemaConfig,
api_controller,
ModelEndpointFactory
)
from .models import Event, Category
@api_controller("/events")
class EventModelController(ModelControllerBase):
model_config = ModelConfig(
model=Event,
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
)
get_event_category = ModelEndpointFactory.find_one(
path="/{int:event_id}/category",
schema_out=CategorySchema,
lookup_param='event_id',
object_getter=lambda self, pk, **kw: self.service.get_one(pk=pk).category
)
get_event_category endpoint using ModelEndpointFactory.find_one and
provided an object_getter as a callback for fetching the model based on the event_id.
And the lookup_param indicates the key in kwargs that defines the pk value used to get object model incase
there is no object_getter handler implemented.
On the other hand, you can have a case where you need to list events by category_id:
from ninja_extra import (
ModelConfig,
ModelControllerBase,
ModelSchemaConfig,
api_controller,
ModelEndpointFactory
)
from .models import Event, Category
@api_controller("/events")
class EventModelController(ModelControllerBase):
model_config = ModelConfig(
model=Event,
schema_config=ModelSchemaConfig(read_only_fields=["id", "category"]),
)
get_events_by_category = ModelEndpointFactory.list(
path="/category/{int:category_id}/",
schema_out=model_config.retrieve_schema,
lookup_param='category_id',
queryset_getter=lambda self, **kw: Category.objects.filter(pk=kw['category_id']).first().events.all()
)
ModelEndpointFactory.list and queryset_getter, you can quickly set up a list endpoint that returns events belonging to a category.
Note that our queryset_getter may fail if an invalid ID is supplied, as this is just an illustration.
Also, keep in mind that model_config settings like create_schema, retrieve_schema, patch_schema, and update_schema
are all available after ModelConfig instantiation.
Path and Query Parameters
In ModelEndpointFactory, path parameters are parsed to identify both path and query parameters.
These parameters are then created as fields within the Ninja input schema and resolved during the request,
passing them as kwargs to the handler.
For example,
list_post_tags = ModelEndpointFactory.list(
path="/{int:id}/tags/{post_id}?query=int&query1=int",
schema_out=model_config.retrieve_schema,
queryset_getter=lambda self, **kw: self.list_post_tags_query(**kw)
)
def list_post_tags_query(self, **kwargs):
assert kwargs['id']
assert kwargs['query']
assert kwargs['query1']
post_id = kwargs['post_id']
return Post.objects.filter(id=post_id).first().tags.all()
In this example, the path /{int:id}/tags/{post_id}?query=int&query1=int generates two path parameters ['id:int', 'post_id:str']
and two query parameters ['query:int', 'query1:int'].
These parameters are bundled into the Ninja input schema and resolved during the request, passing them as kwargs to the route handler.
Note that when path and query parameters are defined they are added to ninja schema input as a required field and, not optional.
Also, path and query data types must be compatible with Django URL converters.