TurnKey Linux Virtual Appliance Library

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!

You can get future posts delivered by email or good old-fashioned RSS.
TurnKey also has a presence on Google+, Twitter and Facebook.

Comments

I cannot seem to get this

I cannot seem to get this going. I don;t need a custom login template - could you explain how to this without? When I try to use: url(r'^login/$', login, name='auth_login'), in my urls.py I get an error. Thanks!

Take 2

Have tried with some other options. With urlpatterns += patterns('', (r'^login/$', 'myproject.mypapp.views.login', {'template_name': 'myapp/templates/admin/login.html'}),) I can get the login to work (adding the name='' gives a syntax error), but the user_login_handle function is never called (I added a print statement to the code there as a double check). What else can I try here?
Alon Swartz's picture

More information required

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?

Thanks

Was looking for a simple example of how to user custom signals. This article was very usefull. Thanks

handler as class static method

Kinda sucks that you can use a @stasticmethod decorator to make the handler a class static. Django complains that Signal receivers must accept keyword arguments (**kwargs).

easy example

For those of you who couldnt get the article 'quite right'

Here's a quick example:

 

 

# in models.py
from django.db.models.signals import pre_delete
from events.models import Event
 
def do_something(sender, **kwargs):
    # the object which is saved can be accessed via kwargs 'instance' key.
    obj = kwargs['instance']
    print 'the object is now deleted'
    # ...do something else...
 
# here we connect a pre_delete signal for MyModel
# in other terms whenever an instance of MyModel is deleted
# the 'do_something' function will be called.
pre_delete.connect(do_something, sender=MyModel)

how to disconnect a signal

where should the code to disconnect a signal go..

whenever I write

USER_SAVE_UID='user_save_signal'
user_save_signal= django.dispatch.Signal(providing_args=["userid"])
def user_save(sender,**kwargs):
    user_id=kwargs['userid']
    logger.debug("user with id %d created",user_id)

user_save_signal.connect(user_save,sender=User,dispatch_uid= USER_SAVE_UID)

user_save_signal.disconnect(user_save,sender=User,dispatch_uid= USER_SAVE_UID)

 

user_save function is never called. But when I write only user_save_signal.connect(user_save,sender=User,dispatch_uid= USER_SAVE_UID) user_save function works

Post new comment

The content of this field is kept private and will not be shown publicly. If you have a Gravatar account, used to display your avatar.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <p> <span> <div> <h1> <h2> <h3> <h4> <h5> <h6> <img> <map> <area> <hr> <br> <br /> <ul> <ol> <li> <dl> <dt> <dd> <table> <tr> <td> <em> <b> <u> <i> <strong> <font> <del> <ins> <sub> <sup> <quote> <blockquote> <pre> <address> <code> <cite> <strike> <caption>

More information about formatting options

Leave this field empty. It's part of a security mechanism.
(Dear spammers: moderators are notified of all new posts. Spam is deleted immediately)