Blog Tags: 

Django Signals: Be lazy, let stuff happen magically

When I first learned about Django signals, it gave me the same warm fuzzy feeling I got when I started using RSS. Define what I'm interested in, sit back, relax, and let the information come to me.

You can do the same in Django, but instead of getting news type notifications, you define what stuff should happen when other stuff happens, and best of all, the so-called stuff is global throughout your project, not just within applications (supporting decoupled apps).

For example:

  • Before a blog comment is published, check it for spam.
  • After a user logs in successfully, update his twitter status.

What you can do with signals are plentiful, and up to your imagination, so lets get into it.

What are Django signals?

In a nutshell, signals allow certain senders to notify a set of receivers that some action has taken place. They're especially useful when many pieces of code may be interested in the same events.

This might remind you of Event driven programming, and rightfully so, the core concepts are very similar.

Django provides a set of built-in signals that let user defined code get notified by Django itself of certain actions, for example:

# Sent before or after a model's save() method is called.
django.db.models.signals.pre_save | post_save

# Sent before or after a model's delete() method is called.
django.db.models.signals.pre_delete | post_delete

# Sent when a ManyToManyField on a model is changed.
django.db.models.signals.m2m_changed

# Sent when Django starts or finishes an HTTP request.
django.core.signals.request_started | request_finished

Built-in signals are really useful, but what I really like is the ability to define custom signals, and due to the way the "signal dispatcher" works, it allows decoupled applications to be notified when actions occur elsewhere in the framework.

In other words, one of your apps can send a signal when something happens, and a different app can listen for the signal and do something when it's received.
 

Using Django signals

Defining and sending a signal

All signals are django.dispatch.Signal instances. The providing_args is a list of the names of arguments the signal will provide to listeners.

Application: foo

signals.py

from django.dispatch import Signal
user_login = Signal(providing_args=["request", "user"])

views.py

from foo import signals

def login(request):
    ...
    if request.user.is_authenticated():
        signals.user_login.send(sender=None, request=request, user=request.user)
    ...

In the above example, a signal will be sent once the user logs in successfully.

Note: The above is just for exemplary purposes, it should be very rare to create your own authentication system as opposed to leveraging django.contrib.auth or one of the great apps out there.

Just as a side note, sender is usually defined as self when sending the signal from a class, such as a model class. This gives the intercepting handler instant access to the related class instance.
 

Listening to signals

To receive a signal, you need to register a receiver function that gets called when the signal is sent.

Application: bar

tasks.py

from foo.signals import user_login

def user_login_handler(sender, **kwargs):
    """signal intercept for user_login"""
    user = kwargs['user']
    ...

user_login.connect(user_login_handler)

Now, when a user logs in successfully and the signal is sent, it will be intercepted and the handler can then do what ever is required. Note that multiple receivers can be registered for a single signal.
 

Where should the code live?

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 apps models.py a good place to put registration of signal handlers.
 

Integrating with django.contrib.auth

Seeing as I opened the door to this, let me digress a little.

The login and logout methods of django.contrib.auth do not send signals, and is discussed in this ticket. The main reason is that signals are synchronous (discussed in the next section) and would slow down the login/logout process.

If you do need authentication related signals, there are a few ways of accomplishing it, each with their own pros and cons, such as:

  • Patch django.contrib.auth
  • Create a custom auth backend
  • Create a wrapping view

Creating a wrapping view provides the most flexibility, while writing less code and leveraging great code that already exists.

Application: foo

urls.py

from django.conf.urls.defaults import *
from foo.views import login

urlpatterns = patterns('',
   url(r'^login/$', login,
       {'template_name': 'fooapp/login.html'}, name='auth_login'),
   )

views.py

from django.contrib.auth.views import login as auth_login
from foo import signals

def login(request, **kwargs):
    """workaround wrapper for auth.login that sends user_login signal"""
    response = auth_login(request, **kwargs)
    if request.user.is_authenticated():
        signals.user_login.send(sender=None, request=request, user=request.user)

    return response

The above will wrap the auth.login method, and send a signal when a user successfully logs in. We are also passing a custom login template, but of course you don't need to.
 

Signals are synchronous

As I mentioned above, signals are synchronous, or blocking (just like everything else in Django). This means that the request will not be returned until all signal handlers are done.

There have been multiple requests to add asynchronous support to Django, namely via the Python threading module, but I doubt it will happen anytime soon, if at all. In my opinion it comes down to separation of concerns, and there is a whole other world called message queuing, namely AMQP which is designed for these sort of things.

In an upcoming post I will discuss implementing AMQP (Advanced Message Queuing Protocol) with RabbitMQ and Celery.
 

Ever needed to use signaling in your Django webapp? Post a comment!

Comments

Alon Swartz's picture

Since version 1.0 (not sure what version you are using), Django has supported Naming URL patterns. It's not required for signaling, but it's really useful when using the same view function in multiple URL patterns, and specifying urls in templates.

Regarding your problem, could you post some code snippets? I'm not sure how productive it would be to make wild guesses. But, just for fun, you mentioned user_login_handle, my example was user_login_handler, could it be a typo?

Pages

Add new comment