Django Signals (pre_save and post_save)

Dec. 26, 2022


0
8 min read
420

Django, with its experienced developer community and regular updates, has become a reliable framework to build stable and maintainable websites. Not to forget the easy-to-understand documentation it provides, makes it easier for beginners to get their way started in Django

Without wasting much time on what Django is, let’s jump right into one of its most crucial topics- Signals

What are Signals

Signals, as the name suggests, allow applications to get notified when a certain event occurs. Suppose you want to notify the author of an article whenever someone comments or reacts to the article, but there are several places in your codebase to comment on an article or react to the article. How do you do it? You guessed it right, using signals. Signal hooks some piece of code to be executed as soon as a specific model’s save method is triggered.

To have a clearer idea of the above example, let’s define the models.

from django.db import models
from django.contrib.auth.models import User

#Artical Model
class Article(models.Model):
    title = models.CharField(max_length=50)
    body = models.TextField()
    auther = models.ForeignKey(User, on_delete=models.CASCADE)

#Comment Model
class ArticleComment(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    artical = models.ForeignKey(Article, on_delete=models.CASCADE)
    comment = models.TextField()

#Rating Model
class ArticleReaction(models.Model):
    CHOICES = (
        ('1', 'Like'),
        ('2', 'Love'),
        ('3', 'Wow'),
        ('4', 'Hug'),
        ('5', 'Sad'),
        ('6', 'Angry')
    )
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    artical = models.ForeignKey(Article, on_delete=models.CASCADE)
    reaction = models.IntegerChoices(choices=CHOICES, default=1, max_length='10')

When to use Signals

Signals are best used when multiple pieces of code are interested in the same model instance events. To understand model instance events in simpler terms, it means row creation, updating or deletion events.

How to use Signals

While there are many ways to use signals, my favourite way is to use the ‘@receiver’ decorator.

Here is an example:

from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import ArticleComment

@receiver(post_save, sender=ArticleComment)
def notify_author(sender, instance, created, **kwargs):
    if created:
        #call your function to notify instance.article.author

This might seem a bit confusing right now if you are a beginner, but let me help you out here.

The receiver decorator tells the code that the function(‘notify_user’ here) is about to receive a signal(post_save signal in this case). Now there are many types of Django signals, post_save(django.db.models.signals.post_save) is one of them. Let’s go through some of the important signals separately.

1. Tips to use post_save

As the name suggests, it is called just after the Django model save() function has done its job. The sender parameter here defines the Model from which we are trying to receive the signal. So now if I am to convert the above python code into English, I would say:

Call the function ‘notifiy_author’ after the istance of ‘ArticleComments’ is saved

Whenever we use signals, we get to use some variables. One of those variables, as you can see in the above code snippet is ‘instance’. This is nothing but the instance of the model ArticleComments which is being saved. So if I want to fetch the author of the comment, I can easily use:

instance.article.author
def notify_author(sender, instance, created, **kwargs):

 

Now a perk about using post_save signal is that it provides us with a variable named created. This is a flag, which returns True if this post_save signal is called when a new instance(or row) of a model was created.

The following points should be kept in mind while using post_save:

  1. There are no special signals like pre_create or post_create. Whenever we call the Model function to create(), it calls the save signals.
  2. You cannot modify the value of any instance’s fields(for ex: ‘instance.comment’ here) inside post_save, without calling Django save() method again, which is not(never) a good practice to call inside a signal of the same model(Can you guess why?)

2. Use pre_save like a pro

pre_save(django.db.models.signals.pre_save) is provoked just before the model save() method is called, or you could say the model save method is called only after pre_save is called and done its job free of errors.

We know how to notify the author when a new comment is posted(the same way you can create a signal on the creation of ArticleReaction too), but what if I want to notify the author when the reaction is modified? pre_save comes in handy for similar cases.

@receiver(post_save, sender=ArticleReaction)
def notity_author_on_reaction(sender, instance, **kwargs):

    #if instance/row is being created, then do nothing
    if instance.id is None:
        pass

    #else if it is being modified
    else:
        current = instance
        privious = ArticleReaction.objects.get(id=instance.id)

        #if previous reaction is not equal to current reaction
        if privious.reaction != current.reaction:
            #notify instance.article.author

We do not get ‘created’ variable to use in pre_save. No worries, we have a workaround for that too.

All the model instances(or rows) have an auto-generated primary key which is name ‘id’. This id is numerically incremented as instances of the model are created. But the ‘id’ can only be generated/assigned after the creation of the row. Hence in pre_save, if we try to access the instance.id before the instance(or row) is created, it will return us None. So the condition on line 10 in the above snippet is nothing but a replica of ‘created’ from post_save.

#if instance/row is being created, then do nothing
if instance.id is None:

"""(Notice how I am using ‘row’ to refer an instance of a model. It will help you to have a clearer imagination of what’s happenning behind the scenes. Similarly, attributes/firelds can be referred with another term ‘column’)"""

Our problem statement was to notify the author if the reaction is changed. Let’s try to backtrack it. To find if the reaction was changed, we need the current reaction and the previous reaction. If the current reaction and the previous reaction do not match, then it concludes that the reaction was changed and hence we need to notify the author about it. But how exactly can we get the previous and the current reactions? To understand that, we have to know one VERY IMPORTANT property about the variable instance in post_save and pre_save

"""The post_save’s instance has the attributes with values which are already saved in your model, but the pre_save’s instance has the attributes with values which are yet to be saved in your model."""

To make the above line clear, let’s take an example. Some user named ‘Abdulla’ has already reacted with a ‘Like’. Abdulla has a habit of reading everything twice. After reading the article again, he decided to change his reaction to ‘Love’. 

Let’s dive back into our code again. Pre_save is called as soon as Ravi changed the reaction. Since the instance(or row) was already created, we move directly to the else condition in Line 14. Using the above-highlighted property, we can say that the variable ‘instance’ will have the new value ‘Love’ for its attribute ‘reaction’. Let’s name this modified variable ‘instance’ as ‘created’ (Line 15). Now we just need to get the old reaction. Remember that the ‘Love’ reaction has not been saved in our model yet(since we are still in pre_save). So what will we get if we try to get a model instance of ArticleReaction with the id provided by pre_save? We will get the instance with the reaction ‘Like’. Now let’s name this as ‘previous’(line 16). As we now have both old and new instances, we can easily compare these reactions and code are conditions accordingly.

current = instance
privious = ArticleReaction.objects.get(id=instance.id)

Since pre_save is called right before the model save() method, you can also modify the instance’s fields as per the requirement. For example, if we are using pre_save for ArticleComments and we want to check and remove any abusive words from comments before it is saved in our model, we can do it like this:

if is_abuse(instance.comment):
    instance.comment = remove_abusive(instance.comment)

Assuming that is_abusive and remove_abusive is your custom method. Also notice that we are not calling instance.save() after changing the instance.comment. Can you tell me why?

3. Other Signals

There many more signals you can use, which you can find here, but this blog was just an attempt to give you an idea of how you can use signals effectively.

Before we finish

Even though signals come in handy when you want to perform actions behind the scenes, you have to be very careful about how you use them.

  1. Do not compromise speed: putting too much load on pre_save or post_save signals might make the model save() method slow.
  2. Lost in the loop: Calling the save() method for the sender model inside the post_save or pre_save will keep calling the signals repetitively.
  3. Remember the use cases: We use post_save when we are interested more in the creation of a model instance without modifying the values, while we use pre_save when we are more into monitoring the change in the model instance’s value, or if we are into modifying the instance’s attribute’s values ourselves.
  4. update() and save signals don’t get along: Django model ‘update()’ method does not invoke any kind of pre_save or post_save signals.
django Python coding django-signal Appreciate you stopping by my post! 😊

Add a comment


Note: If you use these tags, write your text inside the HTML tag.
Login Required