Django Signals Example

signals

Have you read the Django Signals Documentation and wondered, “Okay, how the heck do you actually use it? How about an example?” If so, then you’ve come to the right place.

Will demo by example adding signals for pre_save, pre_delete to raise an exception if a READ_ONLY_FILE exists to prevent the DB from changing. Then, add signals for post_save and post_delete to print the change to stdout. Using print as an example, a more useful thing would be to write it to syslog or something – say you want to log every DB change that occurs.

The code output is available at https://github.com/djangopractice/demo_signals

This starts with setting up a very basic Django app. It should feel very familiar, borrowing from the Django tutorial Polls as the starting point.

Create New Django Project with Polls App

pip install Django
django-admin.py startproject demo_signals
cd demo_signals
django-admin.py startapp polls

Add polls to INSTALLED_APPS

Edit the django_signals/settings.py file, add ‘polls’ to INSTALLED_APPS

...
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'polls',
)
...

Add READ_ONLY_FILE to settings

Edit the django_signals/settings.py file, append READ_ONLY_FILE to the end

...
READ_ONLY_FILE = os.path.join(BASE_DIR, 'readonly')

We’re going to make our signal raise an exception and refuse to write to the DB if this file exists. This is just one use case of signals. Another could be if there is something critical downstream that needs to be available post save, you could check to be sure it’s available before you save the instance to the DB. Are there other use cases other than check something and prevent the write? Maybe. I haven’t used it for anything but that so far.

Create the Question and Choice models

Edit the polls/models.py file and make it look like this:

from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):
    question = models.ForeignKey(Question)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

Create the polls/signals.py file

I hope that it’s self evident what this will do once it’s hooked up. Note that it won’t do anything just by mere existence of this file. We’ll connect the dots in the next two instructions.

from django.db.models.signals import pre_save, pre_delete, post_save, post_delete
from django.dispatch import receiver
from django.conf import settings
from polls.models import Question, Choice
import os.path


@receiver(pre_delete, sender=Question)
@receiver(pre_delete, sender=Choice)
@receiver(pre_save, sender=Question)
@receiver(pre_save, sender=Choice)
def model_pre_change(sender, **kwargs):
    if os.path.isfile(settings.READ_ONLY_FILE):
        raise ReadOnlyException('Model in read only mode, cannot save')


@receiver(post_save, sender=Question)
@receiver(post_save, sender=Choice)
def model_post_save(sender, **kwargs):
    print('Saved: {}'.format(kwargs['instance'].__dict__))


@receiver(post_delete, sender=Question)
@receiver(post_delete, sender=Choice)
def model_post_delete(sender, **kwargs):
    print('Deleted: {}'.format(kwargs['instance'].__dict__))


class ReadOnlyException(Exception):
    pass

Create the polls/apps.py file

This is the first tricky part. There mere existence of this file also does nothing. We’ll point to this in the next step. This imports polls.signals in the ready method. By extending the AppConfig class from Django, we’re telling Django to import polls.signals when the Polls app is loaded.

from django.apps import AppConfig


class PollsConfig(AppConfig):
    name = 'polls'
    verbose_name = 'Polls Application'

    def ready(self):
        import polls.signals

Edit the polls/__init__.py file

default_app_config = 'polls.apps.PollsConfig'

This default_app_config is some Django magic that tells Django to the use polls.apps.PollsConfig class when loading the Polls app.

Make migrations and migrate

$ python manage.py makemigrations polls
Migrations for 'polls':
  0001_initial.py:
    - Create model Choice
    - Create model Question
    - Add field question to choice

$ python manage.py migrate
Operations to perform:
  Apply all migrations: auth, admin, sessions, contenttypes, polls
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying sessions.0001_initial... OK

Try it out in the shell

$ python manage.py shell
Python 3.4.2 (default, Oct 16 2014, 05:21:12)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.51)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from polls.models import Question
>>> from django.utils import timezone
>>> question = Question(question_text='What is your favorite food?', pub_date=timezone.now())
>>> question.save()
Saved: {'pub_date': datetime.datetime(2015, 1, 19, 22, 50, 54, 627393, tzinfo=<UTC>),
'question_text': 'What is your favorite food?', 'id': 1}
>>> question.pub_date = timezone.now()
>>> question.save()
Saved: {'pub_date': datetime.datetime(2015, 1, 19, 22, 51, 21, 106598, tzinfo=<UTC>),
'question_text': 'What is your favorite food?', 'id': 1}
>>> question.delete()
Deleted: {'pub_date': datetime.datetime(2015, 1, 19, 22, 51, 21, 106598, tzinfo=<UTC>),
'question_text': 'What is your favorite food?', 'id': 1}
>>> question = Question(question_text='What is your favorite color?', pub_date=timezone.now())
>>> question.save()
Saved: {'pub_date': datetime.datetime(2015, 1, 19, 22, 51, 53, 689325, tzinfo=<UTC>),
'question_text': 'What is your favorite color?', 'id': 2}
>>> exit()

This time with readonly file:

$ touch readonly

$ python manage.py shell
Python 3.4.2 (default, Oct 16 2014, 05:21:12)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.51)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from polls.models import Question
>>> from django.utils import timezone
>>> question = Question(question_text='What is your favorite food?', pub_date=timezone.now())
>>> question.save()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/dkoopman/.virtualenvs/demo_signals/lib/python3.4/site-packages/django/db/models/base.py", line 589, in save
    force_update=force_update, update_fields=update_fields)
  File "/Users/dkoopman/.virtualenvs/demo_signals/lib/python3.4/site-packages/django/db/models/base.py", line 613, in save_base
    update_fields=update_fields)
  File "/Users/dkoopman/.virtualenvs/demo_signals/lib/python3.4/site-packages/django/dispatch/dispatcher.py", line 198, in send
    response = receiver(signal=self, sender=sender, **named)
  File "/Users/dkoopman/repos/demo_signals/polls/signals.py", line 14, in model_pre_change
    raise ReadOnlyException('Model in read only mode, cannot save')
polls.signals.ReadOnlyException: Model in read only mode, cannot save
>>> question = Question.objects.get(pk=2)
>>> question.pub_date = timezone.now()
>>> question.save()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/dkoopman/.virtualenvs/demo_signals/lib/python3.4/site-packages/django/db/models/base.py", line 589, in save
    force_update=force_update, update_fields=update_fields)
  File "/Users/dkoopman/.virtualenvs/demo_signals/lib/python3.4/site-packages/django/db/models/base.py", line 613, in save_base
    update_fields=update_fields)
  File "/Users/dkoopman/.virtualenvs/demo_signals/lib/python3.4/site-packages/django/dispatch/dispatcher.py", line 198, in send
    response = receiver(signal=self, sender=sender, **named)
  File "/Users/dkoopman/repos/demo_signals/polls/signals.py", line 14, in model_pre_change
    raise ReadOnlyException('Model in read only mode, cannot save')
polls.signals.ReadOnlyException: Model in read only mode, cannot save
>>> question.delete()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/dkoopman/.virtualenvs/demo_signals/lib/python3.4/site-packages/django/db/models/base.py", line 739, in delete
    collector.delete()
  File "/Users/dkoopman/.virtualenvs/demo_signals/lib/python3.4/site-packages/django/db/models/deletion.py", line 257, in delete
    sender=model, instance=obj, using=self.using
  File "/Users/dkoopman/.virtualenvs/demo_signals/lib/python3.4/site-packages/django/dispatch/dispatcher.py", line 198, in send
    response = receiver(signal=self, sender=sender, **named)
  File "/Users/dkoopman/repos/demo_signals/polls/signals.py", line 14, in model_pre_change
    raise ReadOnlyException('Model in read only mode, cannot save')
polls.signals.ReadOnlyException: Model in read only mode, cannot save