Get Even More Visitors To Your Blog, Upgrade To A Business Listing >>

Learn Django Signals

Django Signals are Explained

Photo by Tobias Cornille on Unsplash

Django Signals allow us to send signals that will trigger other events within the application during an event (at any time stage of that event). They are comprised of senders that send out some information to receivers whenever some event has occurred.

Event handlers. Image by the author.

Thanks to signals, we can customize what needs to be done before an event happens or what to do after it happens. Moreover, wherever this event takes place in the application, the signals successfully establish the send-receive operations. This allows for example to define a pre-post operation defined in a view class in one place instead of copying it for use in many different places.

You can reach the official Django Signals document from here and the Github repo of the demonstration project that will be explained below from here.

There are many built-in signals in Django:

  • Model Signals
  • Management Signals
  • Request / Response Signals
  • Test Signals
  • Database Wrappers

Let’s start with the Model Signals.

Model Signals

These signals wrap up the model creation process. Whenever an action is taken on any model, we can customize what happens before and after that action. Model signals are;

  • pre_init
  • post_init
  • pre_save
  • post_save
  • pre_delete
  • post_delete
  • m2m_changed
  • class_prepared

We have signals for instantiations, creations, deletions, many to many relations and class preparations.

Pre & Post Save Signals

Let’s start with the pre_save & post_save signals because it would be a more understandable start. To keep this example simple, I have created a basic customer model.

Customer Model

Whenever a create or save method of any instance of this model is called, a signal is automatically sent called pre_save before the operation and it sends the instance itself. After the event is completed, another signal called post_save is sent automatically. It also sends a flag called created in addition to the instance. If the instance is a new row or record in the database, then the created flag is equal to True (if the create method is used); or, if the instance is already in the database (if the save method is used), then it is set to False.

pre & post signals on save

We can connect these signals in two ways. First, we can do it by sending the handler function via connect method of the signal. In the second method, we can use decorators.

connection via connect

You must override ready method of Config class of your app in the app.py and import the signals.

Override ready function of Config class.

And also add your app (by indicating the config class) in the settings.py

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'home.apps.HomeConfig',
]

I added a record from the admin panel and what we get in the signals are (remember that I’ve printed args and kwargs):

PRE HANDLER
() {'signal': , 'sender': , 'instance': , 'raw': False, 'using': 'default', 'update_fields': None}

POST HANDLER
() {'signal': , 'sender': , 'instance': , 'created': True, 'update_fields': None, 'raw': False, 'using': 'default'}

We’ve got keyword arguments, the signal itself, the sender model, the actual instance, and whether or not it’s been created.

As a second way, we can use decorators. A decorator called receiver is imported. It takes signal and sender arguments.

signals with decorators

I created a new record from the admin panel and the outcome is:

INSIDE PRE HANDLER
INSIDE POST HANDLER
New record Messi is created.

You can fill and customize the signal handler functions according to your business. I will continue with the use of decorators in the next examples.

Pre & Post Init Signals

These signals are triggered whenever a Django model is instantiated.

pre & post init signals

Just refreshed the admin page;

INIT PRE HANDLER
() {'signal': , 'sender': , 'args': (4, 'Messi', 'Paris', 35), 'kwargs': {}}
INIT POST HANDLER
() {'signal': , 'sender': , 'instance': }
INIT PRE HANDLER
() {'signal': , 'sender': , 'args': (3, 'Zidane', 'Algeria', 33), 'kwargs': {}}
INIT POST HANDLER
() {'signal': , 'sender': , 'instance': }

Pre & Post Delete Signals

Similar to others, we can also define a signal in the delete operation.

DELETE PRE HANDLER
() {'signal': , 'sender': , 'instance': , 'using': 'default', 'origin': ]>}
DELETE POST HANDLER
() {'signal': , 'sender': , 'instance': , 'using': 'default', 'origin': ]>}

M2M Change Signal

This works whenever a ManyToManyField is changed. So, let’s create another class to bind a many-to-many relation.

models.py

Here I set up a structure like the one below. When a new customer is created, the post-handler function adds this new user to the “A” segment by default. Another handler function triggered by the m2m change here automatically runs and prints.

m2m change signal
INSIDE POST HANDLER
New record Salah is created.
INSIDE CUSTOMER CHANGE
() {'signal': , 'sender': , 'action': 'pre_add', 'instance': , 'reverse': False, 'model': , 'pk_set': {8}, 'using': 'default'}
INSIDE CUSTOMER CHANGE
() {'signal': , 'sender': , 'action': 'post_add', 'instance': , 'reverse': False, 'model': , 'pk_set': {8}, 'using': 'default'}
Salah added into A

Class Prepared Signal

Whenever a new model class is introduced to the application, these kinds of signals are triggered.

#a new class
class
NewModelClass(models.Model):
name = models.CharField(max_length=10)

As soon as I restart the server:

INSIDE CLASS PREPARED
() {'signal': , 'sender': .Migration'>}

Management Signals

These signals are triggered before and after migrations.

  • pre_migrate
  • post_migrate

Pre & Post Migration Signals

pre & post migrate signals

After running “python manage.py migrate” command:

INSIDE PRE MIGRATE
() {'signal': , 'sender': , 'app_config': , 'verbosity': 1, 'interactive': True, 'using': 'default', 'stdout': , 'apps': , 'plan': [(, False)]}
...
Running migrations:
Applying home.0003_newclass2_newmodelclass... OK
INSIDE POST MIGRATE
() {'signal': , 'sender': , 'app_config': , 'verbosity': 1, 'interactive': True, 'using': 'default', 'stdout': , 'apps': , 'plan': [(, False)]}
...

Request / Response Signals

These signals are sent during the processing of a request.

  • request_started
  • request_finished
  • got_request_exception

Start & Finish Request Signals

They are invoked before and after Django processing an HTTP request.

Request Signals

Just refreshed the page;

INSIDE REQUEST STARTED
() {'signal': , 'sender': , 'environ': {'CONDA_PYTHON_EXE': '/Users/okanyenigun/miniforge3/bin/python', 'PWD': '/Users/okanyenigun/Desktop/codes/demonstrations/signalexp', 'APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL': 'true', 'MallocNanoZone': '0', 'MANPATH': '/opt/homebrew/share/man:/usr/share/man:/usr/local/share/man:/opt/homebrew/share/man:', 'INFOPATH': '/opt/homebrew/share/info:/opt/homebrew/share/info:', 'HOMEBREW_PREFIX': '/opt/homebrew', 'USER': 'okanyenigun', 'COMMAND_MODE': 'unix2003', '__CFBundleIdentifier': 'com.microsoft.VSCode', 'LC_CTYPE': 'UTF-8', 'LOGNAME': 'okanyenigun', 'TERM': 'xterm-256color', 'PATH': '/Users/okanyenigun/Desktop/codes/demonstrations/signalexp/.env/bin:/Library/Frameworks/Python.framework/Versions/3.9/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/Library/Frameworks/Python.framework/Versions/3.10/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/okanyenigun/miniforge3/condabin:/Library/Frameworks/Python.framework/Versions/3.9/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/Library/Frameworks/Python.framework/Versions/3.10/bin', 'CONDA_EXE': '/Users/okanyenigun/miniforge3/bin/conda', 'CONDA_SHLVL': '0', 'TERM_SESSION_ID': 'DC48AB6E-6177-46AC-AA58-D5CFF4C05D1B', 'HOMEBREW_CELLAR': '/opt/homebrew/Cellar', 'SHLVL': '3', 'HOMEBREW_REPOSITORY': '/opt/homebrew', 'TERM_PROGRAM_VERSION': '1.70.1', 'SSH_AUTH_SOCK': '/private/tmp/com.apple.launchd.Vvm1tkaUzt/Listeners', 'HOME': '/Users/okanyenigun', 'SHELL': '/bin/zsh', '__CF_USER_TEXT_ENCODING': '0x1F5:0x0:0x0', 'TERM_PROGRAM': 'vscode', 'TMPDIR': '/var/folders/9d/xym4108s4255j4wn5n1n7n3c0000gn/T/', 'XPC_SERVICE_NAME': '0', 'XPC_FLAGS': '0x0', 'ORIGINAL_XDG_CURRENT_DESKTOP': 'undefined', 'LANG': 'en_US.UTF-8', 'COLORTERM': 'truecolor', 'GIT_ASKPASS': '/Applications/Visual Studio Code.app/Contents/Resources/app/extensions/git/dist/askpass.sh', 'VSCODE_GIT_ASKPASS_NODE': '/Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/MacOS/Code Helper', 'VSCODE_GIT_ASKPASS_EXTRA_ARGS': '--ms-enable-electron-run-as-node', 'VSCODE_GIT_ASKPASS_MAIN': '/Applications/Visual Studio Code.app/Contents/Resources/app/extensions/git/dist/askpass-main.js', 'VSCODE_GIT_IPC_HANDLE': '/var/folders/9d/xym4108s4255j4wn5n1n7n3c0000gn/T/vscode-git-8b80efcd54.sock', 'VSCODE_INJECTION': '1', 'ZDOTDIR': '/var/folders/9d/xym4108s4255j4wn5n1n7n3c0000gn/T/vscode-zsh', 'OLDPWD': '/Users/okanyenigun/Desktop/codes/demonstrations/signalexp', '_CE_M': '', '_CE_CONDA': '', 'VIRTUAL_ENV': '/Users/okanyenigun/Desktop/codes/demonstrations/signalexp/.env', 'PS1': '(.env) %n@%m %1~ %# ', '_': '/Users/okanyenigun/Desktop/codes/demonstrations/signalexp/.env/bin/python', 'DJANGO_SETTINGS_MODULE': 'signalexp.settings', 'TZ': 'UTC', 'RUN_MAIN': 'true', 'SERVER_NAME': '1.0.0.127.in-addr.arpa', 'GATEWAY_INTERFACE': 'CGI/1.1', 'SERVER_PORT': '8000', 'REMOTE_HOST': '', 'CONTENT_LENGTH': '', 'SCRIPT_NAME': '', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'WSGIServer/0.2', 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/', 'QUERY_STRING': '', 'REMOTE_ADDR': '127.0.0.1', 'CONTENT_TYPE': 'text/plain', 'HTTP_HOST': '127.0.0.1:8000', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'HTTP_UPGRADE_INSECURE_REQUESTS': '1', 'HTTP_COOKIE': 'csrftoken=P9O78nww1woIc7wmKP0HuIpYblxGFi26; sessionid=bmkx4dbnvrxtr0jo3wuenszneyeb8s4b', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.5 Safari/605.1.15', 'HTTP_ACCEPT_LANGUAGE': 'en-US,en;q=0.9', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate', 'HTTP_CONNECTION': 'keep-alive', 'wsgi.input': , 'wsgi.errors': <_io.textiowrapper name="<stderr>" mode="w" encoding="utf-8">, 'wsgi.version': (1, 0), 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.multithread': True, 'wsgi.multiprocess': False, 'wsgi.file_wrapper': }}
[25/Aug/2022 15:16:41] "GET / HTTP/1.1" 200 291
INSIDE REQUEST FINISHED
() {'signal': , 'sender': }

Request Exception Signal

I added a line to the View class that will give an error. The request_started signal is sent first when I refresh the page. Then an exception signal was sent because an error occurred. In the end, the request completion signal was thrown.

request exception signal
INSIDE REQUEST STARTED
() {'signal':
INSIDE REQUEST EXCEPTION
() {'signal': , 'sender': None, 'request': }
Internal Server Error: /
Traceback (most recent call last):
File ..... ERROR MESSAGE
ValueError: invalid literal for int() with base 10: 'asd'
[25/Aug/2022 15:19:32] "GET / HTTP/1.1" 500 69854
INSIDE REQUEST FINISHED
() {'signal': , 'sender': }

Test Signals

These signals are triggered when running tests. I will not go into detail about them. (maybe later…)

  • setting_changed
  • template_rendered

Database Wrappers

  • connection_created: it is thrown when a connection to a database is established.

Conclusion

Django signals are easy to implement and have very good cost/performance ratios. They are great methods for defining new operations to wrap specific events. In this way, operations are standardized. You don’t have to carry the same code snippets everywhere with copy-paste. In addition, when considered within the scope of separation of concerns, you also separate the responsibilities and get rid of crowded classes.

Thank you for reading and if you want to be notified of my new posts, please subscribe and follow. Feel free to write me with any kind of comments, suggestions, etc.

Read More…

  • Learn Django Models
  • How to Implement “Please, Wait!” Animation Using Ajax in Django
  • ANOVA with Python

References

https://docs.djangoproject.com/en/4.1/ref/signals/#m2m-changed

Signals | Django documentation | Django

If this post was helpful, please click the clap 👏 button below a few times to show your support for the author 👇

🚀Developers: Learn and grow by keeping up with what matters, JOIN FAUN.


Learn Django Signals was originally published in FAUN Publication on Medium, where people are continuing the conversation by highlighting and responding to this story.

Share the post

Learn Django Signals

×

Subscribe to Top Digital Transformation Strategies For Business Development: How To Effectively Grow Your Business In The Digital Age

Get updates delivered right to your inbox!

Thank you for your subscription

×