Messages Framework
1. Basics
Django's messages framework makes sending one-time messages to users simple. After setting up the messages framework in your Django project (which is setup by default in the standard Django project) it is as simple as adding a message with a single call in your views.
# views.py
from django.contrib import messages
...
def some_view(request):
...
# there are two ways to add a message in view code
# (a) using the add_message method,
messages.add_message(request, messages.INFO, 'Hello!')
# (b) using one of the convenience methods for the message level (info in this case).
messages.info(request, 'Hello!')
...
...
The above code adds the message "Hello!" to the messages storage backend to be displayed to the user later during the current request or during the next request. Messages are stored with the level information attached to them for later use. The messages above are stored with the level of 'INFO'. When a message is displayed it is removed from the storage backend.
In order to display messages to the user the messages need to be looped over in a template. The following template code is one way to do this:
<!-- base.html -->
...
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
...
Messages are added to the messages storage at specific levels. These levels are similar to the logging levels in Python, but are not the same. Each message level corresponds to an integer value. Below are the default levels and their values along with the tag that will be attached (which is also the convenience method names).
Level Constant | Tag | Value |
---|---|---|
DEBUG |
debug | 10 |
INFO |
info | 20 |
SUCCESS |
success | 25 |
WARNING |
warning | 30 |
ERROR |
error | 40 |
The messages framework is setup by default in the standard Django project that you get from running startproject
with django-admin
. If you have setup your project using another method you might need to make the following changes in your code in order to make the messages framework available.
- Make sure
django.contrib.messages
is in theINSTALLED_APPS
list in your settings. - Make sure
django.contrib.sessions.middleware.SessionMiddleware
anddjango.contrib.messages.middleware.MessageMiddleware
are both in theMIDDLEWARE
list in your settings. Note that the session middleware needs to come before the messages middleware. - Make sure the
context_processors
list in theTEMPLATES
settings containsdjango.contrib.messages.context_processors.messages
.
For more information about Django's messages framework see the Django documentation. Also a Google search for django messages framework
results in many helpful resources for learning more.
2. Deep Dive: Code Walk-through
MessageMiddleware
The messages middleware is the place to start as we take a closer look at the implementation for this feature. The middleware is one of the things that is required to make sure you are able to use the messages framework. The code can be found on github.
There are two methods in the MessageMiddleware
class that are executed for each request/response. The first is the process_request
method which is executed on each request as it comes into Django.
def process_request(self, request):
request._messages = default_storage(request)
This method adds a new _messages
attribute to the request object for the current request. The value of that attribute is the return value from calling the default_storage
function. The default_storage
function is also very simple, it instantiates the messages storage backend, set in the projects settings, with the request object (default_storage
code).
The other method on the MessageMiddleware
class we need to take a look at is the process_response
method.
def process_response(self, request, response):
...
# A higher middleware layer may return a request which does not contain
# messages storage, so make no assumption that it will be there.
if hasattr(request, '_messages'):
unstored_messages = request._messages.update(response)
...
return response
Before each response is sent back from Django to the user's browser the response is sent to the update
method of the messages storage backend instance. The implementation for this method is found in the BaseStorage
class that is extended from for each of the message storage backends (see the code here). This method makes sure that any messages that where not read for this response will be saved/stored for later requests. The actual implementation of what that means depends on the messages storage backend implementation. The _store
method is called from the update
method and is storage backend dependent.
The end result of this means that before a response is sent back to the user's browser, all unread messages are stored for later use.
messages.context_processors
The messages context processor (code) makes sure that every context that is sent to a template to be rendered gets the following context variables added:
messages
- A list of the messages that haven't been displayed yet.
DEFAULT_MESSAGE_LEVELS
- A dict of the default message levels where the keys are the level names and the values are the level integer values (similar to the message level table above).
Message Storage
The messages framework stores the messages in what is known as a storage backend. These backends must extend from the BaseStorage
class found in storage/base.py in the messages framework code. The BaseStorage
class' doc string states that all children classes that implement a new storage backend must implement two methods: _get
and _store
.
# message storage backend _get method signature.
def _get(self, *args, **kwargs):
"""
Retrieve a list of stored messages. Return a tuple of the messages
and a flag indicating whether or not all the messages originally
intended to be stored in this storage were, in fact, stored and
retrieved; e.g., ``(messages, all_retrieved)``.
"""
...
# messages storage backend _store method signature.
def _store(self, messages, response, *args, **kwargs):
"""
Store a list of messages and return a list of any messages which could
not be stored.
"""
...
The _get
method must return a tuple where the first element is a list of the stored messages and the second element is a flag indicating whether or not all of the messages where stored and retrieved.
The _store
method must store a list of messages and return a list of the messages that couldn't be stored.
The BaseStorage
class also provides two main methods that define how all storage backends work. First the add
method. This method is responsible for adding messages to the messages system. It takes a message level, the message and optional extra tags, instantiates a new Message
and appends that message to an internal list of messages (this is only adding the messages in memory, actual storage in the backend happens later when the update
method is called from the process_response
method of the MessagesMiddleware
, described above).
def add(self, level, message, extra_tags=''):
...
if not message:
return
if level < self.level:
return
self.add_new = True
message = Message(level, message, extra_tags=extra_tags)
self._queued_messages.append(message)
The first thing we can see is that the add
method returns right away and doesn't queue anything or even raise errors if the message
argument is falsy or the level
argument is less than the current message level set. Otherwise, a new Message
instance is created and appended to the _queued_messages
attribute of the storage backend.
From this we can see that messages are not stored based on the message level set. Often it is assumed that messages are stored and when displaying them the message level can be set to determine which ones get shown, but that is not the case as we can see from the implementation.
The update
method is called by the MessagesMiddleware
's process_response
method. It is responsible for calling the messages storage backend's _store
method.
def update(self, response):
...
if self.used:
return self._store(self._queued_messages, response)
elif self.added_new:
messages = self._loaded_messages + self._queued_messages
return self._store(messages, response)
The self.used
is set to true on the storage backend instance if the messages have been iterated over. The self.added_new
is set to true inside the add
method that we saw above. So the update
method only stores the queued messages if the backend was iterated over, otherwise it stores everything (_loaded_messages
contains messages that were stored during a previous request in the storage backend).
All messages are stored by instantiating the Message
class. This class takes in its constructor the level, message, and extra tags. The message argument is cast to a string if the Message
instance is cast to a string (see the __str__
method on Message
) or when the messages are iterated over (see the _prepare
method on Message
). Seeing this in the implementation means that we can store in the messages framework objects other than strings as long as they can be cast to a string and that casting results in the messages that we want stored.
In order to get a better understanding of how the messages storage backends work, we can take a look at one of the more simple storage backends provided by Django, the SessionStorage
class. Like we already learned, a storage backend only needs to implement the _get
and _store
methods, so we should be able to see how the SessionStorage
backend is implemented by just looking at those two methods.
...
class SessionStorage(BaseStorage):
...
session_key = '_messages'
...
def _get(self, *args, **kwargs):
...
return self.deserialize_messages(self.request.session.get(self.session_key)), True
def _store(self, messages, response, *args, **kwargs):
...
if messages:
self.request.session[self.session_key] = self.serialize_messages(messages)
else:
self.request.session.pop(self.session_key, None)
return []
...
The SessionStorage
backend does make use of a few helper methods to serialize and deserialize the messages before and after storing them in the session, but other than that the session storage backend implementation is fairly straight forward. The _get
method pulls any messages out of the session (self.request.session.get(self.session_key)
) and the_store
method puts messages into the session (self.request.session[self.session_key] = messages
).
Messages API and Constants
The final piece to the messages framework puzzle is the interface used by the Django developer. When you import from django.contrib import messages
you are importing everything in the constants.py
file and the api.py
file.
The constants.py
file contains the message level constants that are used throughout the code.
The api.py
file contains the add_message
function, set_level
function, and the convenience functions for adding messages at the specific levels (debug
, info
, success
, warning
, error
).
The add_message
function is the gateway function. In most Django developer code, the add_message
function is the only thing that is specifically interacted with in the framework.
...
def add_message(request, level, message, extra_tags='', fail_silently=False):
...
try:
messages = request._messages
except AttributeError:
... # code here uses the fail_silently argument to determine if an exception is raised
else:
return messages.add(level, message, extra_tags)
...
Remember that the _messages
attribute on the request object is an instance of the storage backend. So the add_message
function simply delegates to the storage backend's add
method as seen previously.
The set_level
function allows you to change the minimum message level that should be store.
def set_level(request, level):
...
if not hasattr(request, '_messages'):
return False
request._messages.level = level
return True
This function will return True
or False
indicating if it was able to change the message level. It simply changes the level attribute on the storage backend instance found in the _messages
attribute of the request object.
Finally, there are several convenience functions provided to make it simpler to add messages at specific levels. These simple functions all do the exact same thing.
def debug(request, message, extra_tags='', fail_silently=False):
"""Add a message with the ``DEBUG`` level."""
add_message(request, constants.DEBUG, message, extra_tags=extra_tags,
fail_silently=fail_silently)
All of the convenience functions are the same here. The only differences being the name, the doc string, and which constant level value that is provided as the second argument of the add_message
call.
3. Deep Dive: Language Features
try-except-else
Python has the concept of an else
block in connection with a try-except
block. This is for code that should be executed if no exceptions were raised.
try:
... # some code that could raise an exception
except Exception as e:
... # do something when an exception is raised
else:
... # code that is executed if the try block completes and no exceptions were raised
We saw this in the api.py
file in the add_message
function. The try
block was used to get access to the current instance of the message storage backend (request._message
) and so the else
block is executed if that access doesn't fail.
Custom containers
Python has double underscore (dunder) methods that can be defined on classes to make them work within built-in constructs and operators. You can create your own class and if you also define the __len__
, __iter__
, and __contains__
methods you have also defined a new type of container object.
The __len__
method is used to return the length of the container. This is used by the built-in len
function.
The __iter__
method is used to return an iterator over the contents of the container. This is used by looping constructs.
The __contains__
method is used to determine if an object is in the container. This is used by the in
operator.
The BaseStorage
class implements these methods and it is why we can iterate over the storage backend instance in the templates and determine who many messages there are.
4. Deep Dive: Software Architectural Features
Inheritance
In object oriented programming (OOP) inheritance is used to share code between objects and build upon the functionality provided in other classes. In Python this syntax is:
class MyBackend(BaseStorage):
...
This means that the MyBackend
class extends the BaseStorage
class. All of the methods and attributes defined on the BaseStorage
class are now available to the MyBackend
class. In this example the BaseStorage
class is referred to as the parent class of MyBackend
.
In the messages framework we see the use of inheritance by defining the BaseStorage
parent class that all the child classes must inherit from. There is no need to write any other code than the two methods that must get implemented (_get
and _store
). All other interface code is shared among all storage backends, because they must all extend from the same parent class.
Singleton pattern
In OOP it is sometimes common to make sure there is only ever one instance of a class. This is called the Singleton pattern. Code is usually written in the class constructors to make sure that it is only able to be instantiated once.
In Python this can be accomplished by simply writing a module (file). Even if the module is imported more than once of under different names, there will only be one instance of it. This acts as a singleton.
The messages API is implemented as a singleton. You import from django.contrib import messages
, which pulls in the module. Functions defined in the api.py
file are accessed as methods on an instance.
To illustrate this more simply we can do the following. We will create a file called single.py
with the following contents.
x = 1
def calc(num):
return num * x
Then we will start our REPL and do the following.
>>> import single
>>> single.x
1
>>> single.calc(2)
2
>>> single.x = 10
>>> single.x
10
>>> single.calc(2)
20
Note that single
here looks just like an instance of a class with attributes that can be changed and methods that can be called.
Now, in that same REPL session, if we import the single
module again but using a different name, we can see that we will always just have one instance of the module.
...
>>> import single as s
>>> s.x
10
5. Hands-on Exercises
Implement a new messages storage backend that uses Django's caching framework.
The hints section below is to help you come up with a solution to this problem. The possible solution section below shows one possible solution to this problem. Try out your own solution first with the help of the hints before taking a look at the possible solution section.
Hints
- You can get access to the low-level Django cache framework by importing it
from django.core.cache import cache
. See Django's low-level cache documentation. - You can set and get values from the cache framework using the
set
andget
methods. - Remember that you probably want to store messages on a per user basis.
- Don't forget to change the
MESSAGE_STORAGE
setting to point to your custom backend.
Possible Solution
Here is one possible solution. One major problem with this solution is that it will only store messages in the cache based on the username of the user. This is a problem in that if you are storing messages in views that don't require authentication, than any user that is not authenticated will have a blank username and all messages will be stored in the same place for those users. So, this implementation is not perfect, but it will work in authenticated views.
from django.contrib.messages.storage.base import BaseStorage
from django.core.cache import cache
class CacheStorage(BaseStorage):
def cache_key(self):
return f'messages-for-{self.request.user.username}'
def _get(self, *args, **kwargs):
return (cache.get(self.cache_key()) or [], True)
def _store(self, messages, response, *args, **kwargs):
if messages:
cache.set(self.cache_key(), messages)
else:
cache.set(self.cache_key(), [])
return []
6. Contribute
Resources
- contrib.messages documentation
- contrib.messages open tickets (refer to Understanding Django's Ticketing System for more details)