Sunday 17 May 2009

Signals: Letting People Know When You've Got Something To Say

This week marks a break away from the topic of templates and instead will be focussing on signals.


Signals are Django's way of allowing you to listen out for certain events that either occur by default in the Django code or that have been specified by an application developer. The signals provided by default are explained in a fair amount of detail in the Django Built-in Signal Reference (djangoproject.com) so I won't go into too much depth on what they do, but they are as follows (skip to the first example):



pre_init
Occurs at the start of the process of initialising a model;

post_init
Occurs at the end of the process of initialising a model;

pre_save
Occurs at the start of the process of saving a model;

post_save
Occurs at the end of the process of saving a model;

pre_delete
Occurs at the start of the process of deleting a model;

post_delete
Occurs at the end of the process of deleting a model;

class_prepared
Internally used, occurring when a model class has been registered with Django;

post_syncdb
Occurs after syncdb installs an application;

request_started
Occurs just before Django starts to process a request;

request_finished
Occurs just after Django processes a request;

got_request_exception
Occurs when Django encounters an exception whilst processing a request;

template_rendered
Occurs only whilst running tests when Django renders a template;

comment_will_be_posted
Occurs if you have Django's comment app installed and a comment is just about to be saved;

comment_was_posted
Occurs if you have Django's comment app installed and a comment has just been saved;

comment_was_flagged
Occurs if you have Django's comment app installed and a comment has had a flag set (i.e. a moderator just approved it).


For my first example this week I'm going to be looking at the post_save signal and how it can be used to ensure that a User object always has a profile instance associated with it.


# File: test_signal/models.py
from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save

class UserProfile(models.Model):
user = models.ForeignKey(User, unique=True)
allow_our_emails = models.BooleanField(default=False)
allow_other_emails = models.BooleanField(default=False)

def ensure_profile_exists(sender, **kwargs):
if kwargs.get('created', False):
UserProfile.objects.create(user=kwargs.get('instance'))

post_save.connect(ensure_profile_exists, sender=User)

# File: (somewhere in) settings.py
AUTH_PROFILE_MODULE = "test_signal"

As you can see this defines a fairly simple profile model (supposedly for storing a user's e-mail preferences), the key part of this comes from the function ensure_profile_exists. With all signals it can only be guaranteed that the sender keyword argument will be present therefore we also need to catch everything else using **kwargs. Inside the function we use the fact that post_save is supposed to provide the arguments created and instance to check if the save was for a new object and if so to assign the created instance to the user property of a freshly created UserProfile object.


Finally we connect our function to the post_save signal with instructions to only receive signals when the sending class is User


If the test_signal app is now added to the list of installed apps in settings.py then all new users created will now have an associated profile created also.


The second example for this week demonstrates how you can create your very own signals for code to listen to.


# File: test_signal/signals.py
from django.dispatch import Signal

form_post_received = Signal(providing_args=["path","arguments",]

Here we tell Django that we want form_post_received to be a signal and that it should provide the arguments path and arguments. Now if we add the following:


# File: test_signal/views.py
from signals import form_post_received
from django.conf import settings

def view1(request):
if request.method == "POST":
form_post_received.send(sender=request, path=request.path, arguments=request.POST)
# Continue to do stuff with the form.

def view2(request):
if request.method == "POST":
form_post_received.send(sender=request, path=request.path, arguments=request.POST)
# Continue to do stuff with the form.

def log_form_received(sender, **kwargs):
print "Form Received at %(path)s with arguments %(arguments)s" % kwargs

if settings.DEBUG:
form_post_received.connect(log_form_received)

When pointing views at view1 and view2 whenever the server is in debug mode extra information should be output to the terminal informing us of whenever a form has been posted to either view.


This is done by calling send on our signal along with an argument for sender, in this case we use the request that triggered the view, along with values for each of the other arguments we specified. Django then passes them on to each function that has connected to our signal, which in this case is our logging function (but only when the server is in debug mode, otherwise connect will never be called).


That concludes this weeks tutorial on signals. Next week we shall be looking at the useful functions Django has built in for performing pagination.

UPDATE:There is now a second part to this tutorial talking about more advanced uses of signals. Check it out at Signals - Part 2: Return to sender

7 comments:

  1. I had a similar usage in my app for a signal to make sure a user profile is created. What i still didn't fully get WRT registration is:

    1. When i define user-specific models (i.e) models that are only accessible for a specific user - do i link them to User or to UserProfile - and why?
    2. When do i present the UserProfile form, it is part of the registration form/process or is it presented when the user first logs in (after registering)? What are the pros and cons?

    ReplyDelete
  2. Hi avivgr,

    Thank you for your questions.

    1. Personally I would link the model to the User as this way it boosts the chances of the model being reusable by others (if it's part of a potentially stand alone app) as well as allowing you to change the model that is used for the profile without having to update any of your other code.

    2. I guess there are a couple of ways that this could be done, firstly you could ask for the information on the sign up page (or potentially the page after) by using a custom form on your view and making sure the data is created there. Alternatively you can use some custom middleware to detect if a logged in user has any 'required' fields missing and redirect them to a form to fill in the information at that point. I would tend to lean towards asking for any required information on the sign up page so that it's got out of the way as early as possible. Anything extra can always be put on a secondary page which the user can safely skip (otherwise without some middleware to catch them there's not much you can do to stop them clicking elsewhere on your site). Finally the middleware option is probably best left until you decide that you require some extra information from your existing users at which point it can catch their requests and ensure that their info is up-to-date before letting them get to their destination, this should be used with caution though as it can get quite annoying to click to go somewhere and end up in a completely different place.

    I hope this helps answer your questions or at least nudges you in the right direction.

    -- G

    ReplyDelete