Where developers come to connect, share, build and be inspired.

15

Django Signals - an Extremely Simplified Explanation for Beginners.

1184 views

A lot of this write-up is based on my own (rather limited) understanding of the concept. I am, in no way, an expert and have barely begun to learn programming. I couldn't find any good articles explaning Signals as a concept and hence wrote this article hoping to help others who might be struggling similarly.

There may (and will) be glaring mistakes and downright incorrect assumptions & statements in the post and I implore you to correct me wherever necessary so that I may change the post as and where required for others to read and learn later on.


"F**king signals, how do they work?!"

When django receives a request for content at a specific url, the request-routing mechanism picks up the appropriate view, as defined by your urls.py, to generate content. For a typical GET request, this involves extracting relevant information from the database and passing it to the template which constructs the HTML to be displayed and sends it to the requesting user. Quite trivial, really.

In case of a POST request, however, the server receives data from the user. There may be a case where you need/want to modify this data to suit your requirements before committing/storing it to your database.

Consider, for example, the situation where you need to generate a profile for every new user who signs up to your site. By default, Django provides a basic User model via the the django.contrib.auth module. Any extra information can be added either by way of customising your User model or creating a separate UserProfile model in a separate app.

Okay, I choose the LATTER option.

Alright, let's say our UserProfile model looks like this:

GENDER_CHOICES = (
('M', 'Male'),
('F', 'Female'),
('P', 'Prefer not to answer'),
)
class UserProfile(models.Model):
    user = models.OneToOneField(User, related_name='profile')
    nickname = models.TextField(max_length=64, null=True, blank=True)
    dob = models.DateField(null=True, blank=True)
    gender = models.CharField(max_length=1, 
                              choices=GENDER_CHOICES, default='M')
    bio = models.TextField(max_length=1024, null=True, blank=True)
    [...]

Now, we need to ensure that a corresponding UserProfile instance is created (or already exists) each time a User instance is saved to the database. Hey, we could override the save method on the User model and achieve this, right? Maybe, the code might look something like this:

def save(self, *args, *kwargs):
    u = super(User, self).save(*args, **kwargs)
    UserProfile.objects.get_or_create(user_id=u.id)
    return u # save needs to return a `User` object, remember!

BUT wait! The User model comes from django.contrib.auth which is a part of the django installation itself! Do you really want to override that? I certainly wouldn't recommend that!

So, what now?

Okay, if there was a way we could somehow 'listen' to Django's internal mechanisms and figure out a point in the process where we can 'hook in' our little code snippet, it would make our lives so much easier. In fact, wouldn't it be great if Django 'announced' whenever such a point in the process was reached? Wouldn't it be great if Django could 'announce' had finished creating a new user? That way, you could simply wait for such an 'announcement' to happen and write your code to act upon it only when such an 'announcement' happens.

Well, we're in luck because Django does exactly that. These announcements are called 'Signals'. Django 'emits' specific signals to indicate that it has reached a particular step in its code-execution. These signals provide an 'entry point' into its code-execution mechanism allowing you to execute your own code, at the point where the signal is emitted. Django also provides you with the identity of the sender (and other relevant, extra data) so that we can fine-tune our 'hook-in' code to the best possible extent!

Excellent! That still doesn't explain HOW, though...

For our scenario, we have a very convenient signal called post_save that is emitted whenever a model instance gets saved to the database - even the User model! Therefore, all we need to do is 'receive' this signal and hook in our own code to be executed at that point in the process, i.e. the point where a new user instance has just been saved to the database!

from django.dispatch import receiver
from django.core.signals import post_save
from django.contrib.auth.models import User

@receiver(post_save, sender=User)
def ensure_profile_exists(sender, **kwargs):
    if kwargs.get('created', False):
        UserProfile.objects.get_or_create(user=kwargs.get('instance'))

Confused? Don't worry. Let's try to read the code line-by-line to understand what it says.

> @receiver(post_save, sender=User)

(NB: I'm hoping the three import lines are kinda obvious.)

The first line of the snippet is a decorator called @receiver. This decorator is simply a shortcut to a wrapper that invokes a connection to the post_save signal. The decorator can also takes extra arguments which are passed onto the signal. For instance, we are specifying the sender=User argument in this case to ensure that the receiver is invoked only when the signal is sent by the User model.

What the decorator essentially states is this: "Keep an eye out for any instance of a User model being saved to the database and tell Django that we have a receiver here which is waiting to execute code when such an event happens."

> def ensure_profile_exists(sender, **kwargs):

We are now defining a function that will be called when the post_save signal is intercepted. For purposes of easy understanding and code-readability, we have named this function ensure_profile_exists and we are passing the Signal sender as an argument to this function. We are also passing extra keyword arguments to the funtion and we'll shortly see how beneficial these will be.

>     if kwargs.get('created', False):
          UserProfile.objects.get_or_create(user=kwargs.get('instance'))

To understand this, we first need to understand what kind of extra data gets sent along with the post_save signal. So, let's anthropomorphize the situation for a bit.

Imagine instructing a person at a printing press (where a certain book is being printed) by telling them, "Tell me when each published copy of a book comes out of the press." By doing this, you have ensured that this person will approach you and inform you whenever a book copy appears at the end of printing press production line. Now, imagine that this person is extra-efficient and also brings with them, a copy of the published book that came out of the press.

You now have two pieces of information - the first being that a book has just finished publishing and the second being the actual physical copy of the book that was brought to you to do with as you please.

The kwargs variable is an example of the post_save signal being extra-efficient - it sends out highly-relevant information pertaining to the Signal-sender. Typically, the post_save signal sends out a copy of the saved instance and a boolean variable called created that indicates whether a new instance was created or an older instance was saved/updated. There are a few other things that are bundled into the kwargs dictionary but it is these two variables that we shall use to carry out our ultimate task - that of creating a UserProfile whenever a new User is created.

This part of our code-snippet, therefore, simply checks for the (boolean-)value of the created key in the kwargs dictionary (assuming it to be False, by default) and creates a UserProfile whenever it finds that a new user has been created...

... which is precisely what we have wanted all along!

Hmm... But where should all this code live? In which file?

As per the official Django documentation on Signals:

You can put signal handling and registration code anywhere you like. However, you’ll need to make sure that the module it’s in gets imported early on so that the signal handling gets registered before any signals need to be sent. This makes your app’s models.py a good place to put registration of signal handlers.

Note that models.py is the recommended location. That certainly doesn't mean it is the location to dump all your signal registrations and handlers. If you want to be adventurous, you could write your code in a separate file (say signals.py or something similar) and import it at the exact point where your signals need to be registered/handled.

Whatever you do, wherever you choose to put your code, make sure that your signal handler has been properly registered and is being invoked correctly and at the right time whenever & wherever required.

Good luck!


POINTS TO NOTE

  1. Notice that we didn't add any extra information to the UserProfile - we merely created an empty (but associated) instance and left it to be modified later. Since we have defined most of them have been defined to accept null values, our code will still work. However, if you so wanted, you could use other APIs (internal, as well as external) to acquire the relevant data for the attributes (i.e. the nickname, bio, dob, gender, etc.) and pre-fill them while creating the UserProfile - for example, by extracting the relevant data from a social network profile.

  2. Always check the arguments provided by a signal whenever it is sent. Not all signals send the same arguments and some third-party apps may send arguments that will be useful to you in more ways than one. Try and make good use of these arguments in your receiver functions as best as you can.

Comments

Add a comment