From d54b2af025379800f7f3b8d1bc72833700cde1db Mon Sep 17 00:00:00 2001 From: <> Date: Wed, 4 Dec 2024 05:30:05 +0000 Subject: [PATCH] Deployed 58c5fe3 with MkDocs version: 1.6.1 --- .nojekyll | 0 404.html | 915 +++ .../api_controller_permission/index.html | 1233 +++ .../api_controller_route/index.html | 1125 +++ api_controller/index.html | 1184 +++ api_controller/model_controller/index.html | 1592 ++++ assets/images/favicon.png | Bin 0 -> 1870 bytes assets/javascripts/bundle.83f73b43.min.js | 16 + assets/javascripts/bundle.83f73b43.min.js.map | 7 + assets/javascripts/lunr/min/lunr.ar.min.js | 1 + assets/javascripts/lunr/min/lunr.da.min.js | 18 + assets/javascripts/lunr/min/lunr.de.min.js | 18 + assets/javascripts/lunr/min/lunr.du.min.js | 18 + assets/javascripts/lunr/min/lunr.el.min.js | 1 + assets/javascripts/lunr/min/lunr.es.min.js | 18 + assets/javascripts/lunr/min/lunr.fi.min.js | 18 + assets/javascripts/lunr/min/lunr.fr.min.js | 18 + assets/javascripts/lunr/min/lunr.he.min.js | 1 + assets/javascripts/lunr/min/lunr.hi.min.js | 1 + assets/javascripts/lunr/min/lunr.hu.min.js | 18 + assets/javascripts/lunr/min/lunr.hy.min.js | 1 + assets/javascripts/lunr/min/lunr.it.min.js | 18 + assets/javascripts/lunr/min/lunr.ja.min.js | 1 + assets/javascripts/lunr/min/lunr.jp.min.js | 1 + assets/javascripts/lunr/min/lunr.kn.min.js | 1 + assets/javascripts/lunr/min/lunr.ko.min.js | 1 + assets/javascripts/lunr/min/lunr.multi.min.js | 1 + assets/javascripts/lunr/min/lunr.nl.min.js | 18 + assets/javascripts/lunr/min/lunr.no.min.js | 18 + assets/javascripts/lunr/min/lunr.pt.min.js | 18 + assets/javascripts/lunr/min/lunr.ro.min.js | 18 + assets/javascripts/lunr/min/lunr.ru.min.js | 18 + assets/javascripts/lunr/min/lunr.sa.min.js | 1 + .../lunr/min/lunr.stemmer.support.min.js | 1 + assets/javascripts/lunr/min/lunr.sv.min.js | 18 + assets/javascripts/lunr/min/lunr.ta.min.js | 1 + assets/javascripts/lunr/min/lunr.te.min.js | 1 + assets/javascripts/lunr/min/lunr.th.min.js | 1 + assets/javascripts/lunr/min/lunr.tr.min.js | 18 + assets/javascripts/lunr/min/lunr.vi.min.js | 1 + assets/javascripts/lunr/min/lunr.zh.min.js | 1 + assets/javascripts/lunr/tinyseg.js | 206 + assets/javascripts/lunr/wordcut.js | 6708 +++++++++++++++++ .../workers/search.6ce7567c.min.js | 42 + .../workers/search.6ce7567c.min.js.map | 7 + assets/stylesheets/main.6f8fc17f.min.css | 1 + assets/stylesheets/main.6f8fc17f.min.css.map | 1 + assets/stylesheets/palette.06af60db.min.css | 1 + .../stylesheets/palette.06af60db.min.css.map | 1 + contribution/index.html | 1145 +++ images/benchmark.png | Bin 0 -> 51900 bytes images/custom_exception.gif | Bin 0 -> 1100522 bytes images/pagination_example.gif | Bin 0 -> 5136836 bytes images/ui_swagger_preview_readme.gif | Bin 0 -> 1478750 bytes index.html | 1188 +++ route_context/index.html | 1173 +++ search/search_index.json | 1 + service_module_injector/index.html | 1456 ++++ settings/index.html | 1008 +++ sitemap.xml | 95 + sitemap.xml.gz | Bin 0 -> 388 bytes tutorial/authentication/index.html | 1130 +++ tutorial/body_request/index.html | 1054 +++ tutorial/custom_exception/index.html | 982 +++ tutorial/form/index.html | 1031 +++ tutorial/index.html | 1160 +++ tutorial/ordering/index.html | 1159 +++ tutorial/pagination/index.html | 1146 +++ tutorial/path/index.html | 967 +++ tutorial/query/index.html | 971 +++ tutorial/schema/index.html | 1044 +++ tutorial/searching/index.html | 1169 +++ tutorial/testing/index.html | 1005 +++ tutorial/throttling/index.html | 1269 ++++ tutorial/versioning/index.html | 1107 +++ 75 files changed, 34586 insertions(+) create mode 100644 .nojekyll create mode 100644 404.html create mode 100644 api_controller/api_controller_permission/index.html create mode 100644 api_controller/api_controller_route/index.html create mode 100644 api_controller/index.html create mode 100644 api_controller/model_controller/index.html create mode 100644 assets/images/favicon.png create mode 100644 assets/javascripts/bundle.83f73b43.min.js create mode 100644 assets/javascripts/bundle.83f73b43.min.js.map create mode 100644 assets/javascripts/lunr/min/lunr.ar.min.js create mode 100644 assets/javascripts/lunr/min/lunr.da.min.js create mode 100644 assets/javascripts/lunr/min/lunr.de.min.js create mode 100644 assets/javascripts/lunr/min/lunr.du.min.js create mode 100644 assets/javascripts/lunr/min/lunr.el.min.js create mode 100644 assets/javascripts/lunr/min/lunr.es.min.js create mode 100644 assets/javascripts/lunr/min/lunr.fi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.fr.min.js create mode 100644 assets/javascripts/lunr/min/lunr.he.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hu.min.js create mode 100644 assets/javascripts/lunr/min/lunr.hy.min.js create mode 100644 assets/javascripts/lunr/min/lunr.it.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ja.min.js create mode 100644 assets/javascripts/lunr/min/lunr.jp.min.js create mode 100644 assets/javascripts/lunr/min/lunr.kn.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ko.min.js create mode 100644 assets/javascripts/lunr/min/lunr.multi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.nl.min.js create mode 100644 assets/javascripts/lunr/min/lunr.no.min.js create mode 100644 assets/javascripts/lunr/min/lunr.pt.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ro.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ru.min.js create mode 100644 assets/javascripts/lunr/min/lunr.sa.min.js create mode 100644 assets/javascripts/lunr/min/lunr.stemmer.support.min.js create mode 100644 assets/javascripts/lunr/min/lunr.sv.min.js create mode 100644 assets/javascripts/lunr/min/lunr.ta.min.js create mode 100644 assets/javascripts/lunr/min/lunr.te.min.js create mode 100644 assets/javascripts/lunr/min/lunr.th.min.js create mode 100644 assets/javascripts/lunr/min/lunr.tr.min.js create mode 100644 assets/javascripts/lunr/min/lunr.vi.min.js create mode 100644 assets/javascripts/lunr/min/lunr.zh.min.js create mode 100644 assets/javascripts/lunr/tinyseg.js create mode 100644 assets/javascripts/lunr/wordcut.js create mode 100644 assets/javascripts/workers/search.6ce7567c.min.js create mode 100644 assets/javascripts/workers/search.6ce7567c.min.js.map create mode 100644 assets/stylesheets/main.6f8fc17f.min.css create mode 100644 assets/stylesheets/main.6f8fc17f.min.css.map create mode 100644 assets/stylesheets/palette.06af60db.min.css create mode 100644 assets/stylesheets/palette.06af60db.min.css.map create mode 100644 contribution/index.html create mode 100644 images/benchmark.png create mode 100644 images/custom_exception.gif create mode 100644 images/pagination_example.gif create mode 100644 images/ui_swagger_preview_readme.gif create mode 100644 index.html create mode 100644 route_context/index.html create mode 100644 search/search_index.json create mode 100644 service_module_injector/index.html create mode 100644 settings/index.html create mode 100644 sitemap.xml create mode 100644 sitemap.xml.gz create mode 100644 tutorial/authentication/index.html create mode 100644 tutorial/body_request/index.html create mode 100644 tutorial/custom_exception/index.html create mode 100644 tutorial/form/index.html create mode 100644 tutorial/index.html create mode 100644 tutorial/ordering/index.html create mode 100644 tutorial/pagination/index.html create mode 100644 tutorial/path/index.html create mode 100644 tutorial/query/index.html create mode 100644 tutorial/schema/index.html create mode 100644 tutorial/searching/index.html create mode 100644 tutorial/testing/index.html create mode 100644 tutorial/throttling/index.html create mode 100644 tutorial/versioning/index.html diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/404.html b/404.html new file mode 100644 index 00000000..34590878 --- /dev/null +++ b/404.html @@ -0,0 +1,915 @@ + + + +
+ + + + + + + + + + + + + + + + +The concept of this permission system came from Django DRF.
+Permission checks are always run at the very start of the route function, before any other code is allowed to proceed.
+Permission checks will typically use the authentication information in the request.user
and request.auth
properties to determine if the incoming request should be permitted.
Permissions are used to grant or deny access for different classes of users to different parts of the API.
+The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user.
+This corresponds to the IsAuthenticated
class in Django Ninja Extra.
A slightly less strict style of permission would be to allow full access to authenticated users, but allow read-only access to unauthenticated users.
+This corresponds to the IsAuthenticatedOrReadOnly
class in Django Ninja Extra.
During the handling of a request, the has_permission
method is automatically invoked for all the permissions specified
+in the permission list of the route function. However, has_object_permission
is not triggered since
+it requires an object for permission validation. As a result of that, has_object_permission
method for permissions are
+invoked when attempting to retrieve an object using the get_object_or_exception
+or get_object_or_none
methods within the controller.
To implement a custom permission, override BasePermission
and implement either, or both, of the following methods:
.has_permission(self, request: HttpRequest, controller: "APIController")
+.has_object_permission(self, request: HttpRequest, controller: "APIController", obj: Any)
+
Example
+from ninja_extra import permissions, api_controller, http_get
+
+class ReadOnly(permissions.BasePermission):
+ def has_permission(self, request, view):
+ return request.method in permissions.SAFE_METHODS
+
+@api_controller(permissions=[permissions.IsAuthenticated | ReadOnly])
+class PermissionController:
+ @http_get('/must_be_authenticated', permissions=[permissions.IsAuthenticated])
+ def must_be_authenticated(self, word: str):
+ return dict(says=word)
+
permissions.IsAuthenticated & ReadOnly
permissions.IsAuthenticated | ReadOnly
~(permissions.IsAuthenticated & ReadOnly)
The Ninja-Extra permission system provides flexibility in defining permissions either as an instance of a permission class or as a type.
+In the example below, the ReadOnly
class is defined as a subclass of permissions.BasePermission
and
+its instance is then passed to the permissions
parameter within the api_controller
decorator.
from ninja_extra import permissions, api_controller, ControllerBase
+
+class ReadOnly(permissions.BasePermission):
+ def has_permission(self, request, view):
+ return request.method in permissions.SAFE_METHODS
+
+@api_controller(permissions=[permissions.IsAuthenticated | ReadOnly()])
+class SampleController(ControllerBase):
+ pass
+
In the provided example, the UserWithPermission
class is utilized to assess different permissions for distinct controllers or route functions.
For instance: +
from ninja_extra import permissions, api_controller, ControllerBase, http_post, http_delete
+
+class UserWithPermission(permissions.BasePermission):
+ def __init__(self, permission: str) -> None:
+ self._permission = permission
+
+ def has_permission(self, request, view):
+ return request.user.has_perm(self._permission)
+
+
+@api_controller('/blog')
+class BlogController(ControllerBase):
+ @http_post('/', permissions=[permissions.IsAuthenticated & UserWithPermission('blog.add')])
+ def add_blog(self):
+ pass
+
+ @http_delete('/', permissions=[permissions.IsAuthenticated & UserWithPermission('blog.delete')])
+ def delete_blog(self):
+ pass
+
In this scenario, the UserWithPermission
class is employed to verify whether the user possesses the blog.add
+permission to access the add_blog
action and blog.delete
permission for the delete_blog
action within the BlogController
.
+The permissions are explicitly configured for each route function, allowing fine-grained control over user access based on specific permissions.
The AllowAny
permission class grants unrestricted access, irrespective of whether the request is authenticated or unauthenticated. While not mandatory, using this permission class is optional, as you can achieve the same outcome by employing an empty list or tuple for the permissions setting.
+However, specifying the AllowAny
class can be beneficial as it explicitly communicates the intention of allowing unrestricted access.
The IsAuthenticated
permission class denies permission to unauthenticated users and grants permission to authenticated users.
This permission is appropriate if you intend to restrict API access solely to registered users.
+The IsAdminUser
permission class denies permission to any user, except when user.is_staff
is True
,
+in which case permission is granted.
This permission is suitable if you intend to restrict API access to a +specific subset of trusted administrators.
+The IsAuthenticatedOrReadOnly
permission class allows authenticated users to perform any request.
+For unauthenticated users, requests will only be permitted if the method is one of the "safe" methods: GET, HEAD, or OPTIONS.
This permission is appropriate if you want your API to grant read permissions to anonymous users while restricting write permissions to authenticated users.
+ + + + + + + + + + + + + +The route
class is a function decorator designed to annotate a Controller class function as an endpoint with a specific HTTP method.
For instance: +
from ninja_extra import route, api_controller
+
+@api_controller
+class MyController:
+ @route.get('/test')
+ def test(self):
+ return {'message': 'test'}
+
route
provides predefined methods that simplify the creation of various operations, and their names align with the respective HTTP methods:
+route.get
route.post
route.put
route.delete
route.patch
route.generic(methods=['POST', 'PATCH'])
Here's a summarized description of the parameters for the route
class in NinjaExtra:
path
: A required unique endpoint path string.
methods
: A collection of required HTTP methods for the endpoint, e.g., ['POST', 'PUT']
.
auth
: Defines the authentication method for the endpoint. Default: NOT_SET
response
: Defines the response format as dict[status_code, schema]
or Schema
. It is used to validate the returned response.Default: NOT_SET
operation_id
: An optional unique identifier distinguishing operations in path view.Default: NOT_SET
summary
: An optional summary describing the endpoint. Default: None
description
: An optional description providing additional details about the endpoint. Default: None
tags
: A list of strings for grouping the endpoint for documentation purposes. Default: None
deprecated
: An optional boolean parameter indicating if the endpoint is deprecated. Default: None
by_alias
: An optional parameter applied to filter the response
schema object. Default: False
exclude_unset
: An optional parameter applied to filter the response
schema object. Default: False
exclude_defaults
: An optional parameter applied to filter the response
schema object. Default: False
exclude_none
: An optional parameter applied to filter the response
schema object. Default: False
include_in_schema
: Indicates whether the endpoint should appear on the Swagger documentation. Default: True
url_name
: Gives a name to the endpoint that can be resolved using the reverse
function in Django. Default: None
permissions
: Defines a collection of route permission classes for the endpoint. Default: None
These parameters serve a similar purpose to those used in creating an endpoint in Django-Ninja +but have been abstracted to apply to Controller classes in NinjaExtra.
+In Django-Ninja-Extra, the route
class supports the definition of asynchronous endpoints, similar to Django-Ninja.
+This feature is available for Django versions greater than 3.0.
For example:
+import asyncio
+from ninja_extra import http_get, api_controller
+
+@api_controller
+class MyController:
+ @http_get("/say-after")
+ async def say_after(self, delay: int, word: str):
+ await asyncio.sleep(delay)
+ return {'saying': word}
+
In this illustration, the say_after
endpoint is defined as an asynchronous function using the async
+keyword, allowing for asynchronous operations within the endpoint.
Info
+Read more on Django-Ninja Async Support
+Ninja-Extra APIController is responsible for handling incoming requests and returning responses to the client.
+In Ninja-Extra, there are major components to creating a controller model
+The ControllerBase
class is the base class for all controllers in Django Ninja Extra.
+It provides the core functionality for handling requests, validating input, and returning responses in a class-based approach.
The class includes properties and methods that are common to all controllers, such as the request
object, permission_classes
, and response
object which are part of the RouteContext
.
+The request object contains information about the incoming request, such as headers, query parameters, and body data.
+The permission_classes property is used to define the permissions required to access the controller's routes,
+while the response object is used to construct the final response that is returned to the client.
In addition to the core properties, the ControllerBase
class also includes a number of utility methods that can be used to handle common tasks such as object permission checking (check_object_permission
), creating quick responses (create_response
), and fetching data from database (get_object_or_exception
).
+These methods can be overridden in subclasses to provide custom behavior.
The ControllerBase class also includes a dependency injection system that allows for easy access to other services and objects within the application, such as the repository services etc.
+from ninja_extra import ControllerBase, api_controller
+
+@api_controller('/users')
+class UserControllerBase(ControllerBase):
+ ...
+
The api_controller
decorator is used to define a class-based controller in Django Ninja Extra.
+It is applied to a ControllerBase class and takes several arguments to configure the routes and functionality of the controller.
The first argument, prefix_or_class
, is either a prefix string for grouping all routes registered under the controller or the class object that the decorator is applied on.
The second argument, auth
, is a list of all Django Ninja Auth classes that should be applied to the controller's routes.
The third argument, tags
, is a list of strings for OPENAPI tags purposes.
The fourth argument, permissions
, is a list of all permissions that should be applied to the controller's routes.
The fifth argument, auto_import
, defaults to true, which automatically adds your controller to auto import list.
for example:
+import typing
+from ninja_extra import api_controller, ControllerBase, permissions, route
+from django.contrib.auth.models import User
+from ninja.security import APIKeyQuery
+from ninja import ModelSchema
+
+
+class UserSchema(ModelSchema):
+ class Config:
+ model = User
+ model_fields = ['username', 'email', 'first_name']
+
+
+@api_controller('users/', auth=[APIKeyQuery()], permissions=[permissions.IsAuthenticated])
+class UsersController(ControllerBase):
+ @route.get('', response={200: typing.List[UserSchema]})
+ def get_users(self):
+ # Logic to handle GET request to the /users endpoint
+ users = User.objects.all()
+ return users
+
+ @route.post('create/', response={200: UserSchema})
+ def create_user(self, payload: UserSchema):
+ # Logic to handle POST request to the /users endpoint
+ new_user = User.objects.create(
+ username=payload.username,
+ email=payload.email,
+ first_name=payload.first_name,
+ )
+ new_user.set_password('password')
+ return new_user
+
In the above code, we have defined a controller called UsersController
using the api_controller
decorator.
+The decorator is applied to the class and takes two arguments, the URL endpoint /users
and auth
and permission
classes.
+And get_users
and create_user
are route function that handles GET /users
and POST /users/create
incoming request.
Info
+Inheriting from ControllerBase class gives you more IDE intellisense support.
+Let's create an APIController to properly manage Django user model
+import uuid
+from ninja import ModelSchema
+from ninja_extra import (
+ http_get, http_post, http_generic, http_delete,
+ api_controller, status, ControllerBase, pagination
+)
+from ninja_extra.controllers.response import Detail
+from django.contrib.auth import get_user_model
+
+
+class UserSchema(ModelSchema):
+ class Config:
+ model = get_user_model()
+ model_fields = ['username', 'email', 'first_name']
+
+
+@api_controller('/users')
+class UsersController(ControllerBase):
+ user_model = get_user_model()
+
+ @http_post()
+ def create_user(self, user: UserSchema):
+ # just simulating created user
+ return dict(id=uuid.uuid4())
+
+ @http_generic('/{int:user_id}', methods=['put', 'patch'], response=UserSchema)
+ def update_user(self, user_id: int):
+ """ Django Ninja will serialize Django ORM model to schema provided as `response`"""
+ user = self.get_object_or_exception(self.user_model, id=user_id)
+ return user
+
+ @http_delete('/{int:user_id}', response=Detail(status_code=status.HTTP_204_NO_CONTENT))
+ def delete_user(self, user_id: int):
+ user = self.get_object_or_exception(self.user_model, id=user_id)
+ user.delete()
+ return self.create_response('', status_code=status.HTTP_204_NO_CONTENT)
+
+ @http_get("", response=pagination.PaginatedResponseSchema[UserSchema])
+ @pagination.paginate(pagination.PageNumberPaginationExtra, page_size=50)
+ def list_user(self):
+ return self.user_model.objects.all()
+
+ @http_get('/{user_id}', response=UserSchema)
+ def get_user_by_id(self, user_id: int):
+ user = self.get_object_or_exception(self.user_model, id=user_id)
+ return user
+
In the example above, the UsersController
class defines several methods that correspond to different HTTP methods,
+such as create_user
, update_user
, delete_user
, list_user
and get_user_by_id
.
+These methods are decorated with http_post
, http_generic
, http_delete
, http_get
decorators respectively.
The create_user
method uses http_post
decorator and accepts a user argument of type UserSchema
,
+which is a ModelSchema
that is used to validate and serialize the input data.
+The method is used to create a new user in the system and return an ID
of the user.
The update_user
method uses http_generic
decorator and accepts a user_id
argument of type int.
+The decorator is configured to handle both PUT
and PATCH
methods and
+provides a response argument of type UserSchema
which will be used to serialize the user object.
The delete_user
method uses http_delete
decorator and accepts a user_id
argument of type int and a response argument of type
+Detail which will be used to return a 204 status code with an empty body on success.
The list_user
method uses http_get
decorator and decorated with pagination.paginate
decorator that paginate the results of the method using PageNumberPaginationExtra
class with page_size=50.
+It also provides a response argument of type pagination.PaginatedResponseSchema[UserSchema]
which will be used to serialize and paginate the list of users returned by the method.
The get_user_by_id
method uses http_get
decorator and accepts a user_id
argument of type int and a response argument of type UserSchema which will be used to serialize the user object.
The UsersController also use self.get_object_or_exception(self.user_model, id=user_id)
which is a helper method that will raise an exception if the user object is not found.
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_config
is responsible for defining configurations related to routes and schema generationmodel_service
refers 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"]),
+ )
+
+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.
The ModelConfig
is a Pydantic schema designed for validating and configuring the behavior of Model Controllers. Key configuration options include:
asynchronous
route functions["create", "find_one", "update", "patch", "delete", "list"]
.create
or POST
operation in the Model Controller. The default is None
. If not provided, the ModelController
will generate a new schema based on the schema_config
option.update
or PUT
operation in the Model Controller. The default 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.None
. If not provided, the ModelController
will generate a schema based on the schema_config
option.patch/PATCH
operations. The default is None
. If not provided, the ModelController
will generate a schema with all its fields optional.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.pagination: A requisite for the model list/GET
operation to prevent sending 100_000
items at once in a request. The pagination configuration mandates a ModelPagination
Pydantic schema object for setup. Options encompass:
klass
: The pagination class of type PaginationBase
. The default is PageNumberPaginationExtra
.paginator_kwargs
: A dictionary value for PaginationBase
initialization. The default is None.pagination_schema
: A Pydantic generic schema that combines with retrieve_schema
to generate a response schema for the list/GET
operation.For instance, if opting for ninja
pagination like LimitOffsetPagination
:
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
+ ),
+ )
+
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}
+
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)
+
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"]),
+ )
+
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.
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
+
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, **kw: Any) -> Category:
+ event = self.service.get_one(pk=kw['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
.
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, **kw: Any) -> Category:
+ event = await self.service.get_one_async(pk=kw['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.
+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.
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.