Hooks¶
Hooks are methods on your serializer that run during create, update, delete and queries. Use them to fill fields, react to changes or restrict rows.
Follow the naming rule¶
Sync hooks use the plain name. Async hooks use the same name with an a
prefix:
| Sync | Async |
|---|---|
def post_create(self) |
async def apost_create(self) |
def custom_actions(self, payload) |
async def acustom_actions(self, payload) |
def queryset_request(cls, request) |
async def aqueryset_request(cls, request) |
Override one of the two. If you override only one, the other mode calls it for you.
The save hooks (before_save, after_save, on_create_before_save,
on_create_after_save, on_delete) are always plain def. They also run in
async viewsets.
Warning
A hook with the wrong kind stops your app at import time with
ImproperlyConfigured:
async def post_createraisesArticle.post_create is async; rename it to apost_create (sync hooks use the plain name).def apost_createraisesArticle.apost_create must be async.
Change the object around a save¶
from django.utils.text import slugify
from ninja_aio import ModelSerializer
class Article(ModelSerializer):
...
def before_save(self):
self.slug = slugify(self.title)
def on_create_after_save(self):
logger.info("Article %s created", self.pk)
| Hook | Runs |
|---|---|
on_create_before_save |
Before the first save only |
before_save |
Before every save |
on_create_after_save |
After the first save only |
after_save |
After every save |
on_delete |
After the object is deleted |
These hooks run on every save() and delete() call, including your own
code.
Check if a field changed¶
has_changed(field) compares the value on the object with the value in the
database:
def before_save(self):
if self.has_changed("is_published") and self.is_published:
self.published_at = timezone.now()
It returns False for objects that are not saved yet. In async code, use
await self.ahas_changed("is_published").
Run code after create¶
class Article(ModelSerializer):
def post_create(self):
AuditLog.objects.create(action="create", article=self)
class Article(ModelSerializer):
async def apost_create(self):
await AuditLog.objects.acreate(action="create", article=self)
post_create runs once, after the first save.
Handle custom fields¶
Fields in customs are accepted in the request body but not saved to the
model. Read them in custom_actions:
class Article(ModelSerializer):
...
class Schemas:
create = SchemaConfig(
fields=["title", "body", "category"],
customs=[("notify_editors", bool, False)],
)
def custom_actions(self, payload):
if payload.get("notify_editors"):
notify_editors(self.pk)
class Article(ModelSerializer):
...
class Schemas:
create = SchemaConfig(
fields=["title", "body", "category"],
customs=[("notify_editors", bool, False)],
)
async def acustom_actions(self, payload):
if payload.get("notify_editors"):
await notify_editors(self.pk)
payload is a dict with the custom fields, like {"notify_editors": True}.
It runs on create and update.
Restrict rows per request¶
queryset_request is a classmethod that builds the base queryset. It is the
only hook that receives the request. It is used by the list, and to load the
object for retrieve, update, delete and @on actions:
class Article(ModelSerializer):
@classmethod
def queryset_request(cls, request):
user = getattr(request, "auth", None)
if user and user.is_staff:
return cls.objects.all()
return cls.objects.filter(is_published=True)
class Article(ModelSerializer):
@classmethod
async def aqueryset_request(cls, request):
user = getattr(request, "auth", None)
if user and user.is_staff:
return cls.objects.all()
return cls.objects.filter(is_published=True)
request is None when you call the serializer from Python without passing
request=. Rows outside the queryset return 404.
React to changes with decorators¶
Mark any method with @on_create, @on_update or @on_delete:
from ninja_aio.models.hooks import on_create, on_delete, on_update
class Article(ModelSerializer):
...
@on_create
async def announce(self):
await send_new_article_email(self.pk)
@on_update("is_published")
def published_changed(self):
cache.delete(f"article:{self.pk}")
@on_update
def any_update(self):
logger.info("Article %s updated", self.pk)
@on_delete
def cleanup(self):
storage.delete(f"covers/{self.pk}.jpg")
| Decorator | Runs |
|---|---|
@on_create |
After an object is created |
@on_update |
After every update |
@on_update("status") |
After an update that sends and changes status |
@on_update("status", "views") |
After an update that sends and changes any of the fields |
@on_delete |
After an object is deleted |
The decorated methods can be def or async def. Both run in sync and async
viewsets. You can add many hooks for the same event: they run in the order you
define them.
When you call save() or delete() yourself, @on_create, @on_update and
@on_delete still run. @on_update("field") only runs through the framework
update, like PATCH or Article.update(). An update only touches the fields
the client sent, so @on_update("status") runs only when status was sent
and its value changed.
Note
On a ModelSerializer or Serializer, a method named on_delete hides the
@on_delete decorator for the lines after it in the class body. Define the
on_delete method after the decorated methods, or import the module:
from ninja_aio.models import hooks, then use @hooks.on_delete.
Know the order¶
Hooks run in this order, in sync and async, for a ModelSerializer and a
Serializer:
| Operation | Order |
|---|---|
| Create | on_create_before_save, before_save, save, on_create_after_save, after_save, custom_actions, post_create, @on_create, nested children |
| Update | set the sent fields, custom_actions, before_save, save, after_save, @on_update("field"), @on_update |
| Delete | delete, on_delete, @on_delete |
When the class has a hook that runs after the write, the write and its hooks
run in one database transaction. This covers post_create, custom_actions,
after_save, on_create_after_save, on_delete, the @on_create,
@on_update and @on_delete hooks, and nested writes. If a hook raises, the
change is rolled back, on the endpoints and when you call the serializer from
Python. To run code only after the commit, use Django's
transaction.on_commit() inside the hook.
Use hooks on a Serializer¶
A Serializer for a plain Django model has the same hooks. They receive the
instance as an argument:
from ninja_aio import Serializer
from ninja_aio.models.hooks import on_update
class ArticleSerializer(Serializer):
class Meta:
model = Article
class Schemas:
...
def post_create(self, instance):
AuditLog.objects.create(action="create", article=instance)
def custom_actions(self, payload, instance):
if payload.get("notify_editors"):
notify_editors(instance.pk)
@on_update("is_published")
async def published_changed(self, instance):
await clear_cache(instance.pk)
| Hook | Arguments |
|---|---|
post_create / apost_create |
instance |
before_save, after_save, on_create_before_save, on_create_after_save, on_delete |
instance |
custom_actions / acustom_actions |
payload, instance |
queryset_request / aqueryset_request |
request (classmethod) |
@on_create, @on_update, @on_delete methods |
instance |
They run in sync and async viewsets, and when you call the serializer from Python.
Hooks on the viewset¶
Viewsets have their own hooks that receive the request, like
on_before_operation and on_list_queryset. See
Run code before each operation.
For access rules, see Permissions.