You are here
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
More information required
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