Towards a Standard for Django Session Messages

June 19th, 2009 by tobias

Django needs a standard way in which session-specific messages can be created and retrieved for display to the user. For years we’ve been surviving using user.message_set to store messages that are really specific to the current session, not the user, or using the latest and greatest Django snippet, pluggable app, or custom crafted middleware to handle messages in a more appropriate way.

While this has been discussed at length in Ticket #4604 as well as on Django Snippets, here are a few reasons that user.message_set is the wrong implementation:

  • No message_set exists for AnonymousUsers in Django, so you can’t display any messages to them.
  • What happens when the same user is logged in from two different browsers and completing two different tasks, simultaneously? When using user.message_set to store feedback for the user, the messages will be distributed on a first come first served basis, with no regard for what session actually generated what feedback. For this reason it’s bad to get in the habit of using user.message_set for messages like “Article updated successfully,” or other messages that really have no context outside the current session.

I’ve outlined a few characteristics below that I believe would make up a solid session messaging contrib app. Please feel free to comment if I missed anything, or if you’ve got beef with any of my points. This is in many ways a work in progress, so I’ll update it as often as I can.

  • Standards. The implementation ought to make it clear how multiple messages are to be stored and retrieved for display to the user. Maybe you need to push multiple messages onto the stack from a single view, or your app performs multiple redirects through different views.
  • Persistence. In the case where your app redirects through multiple views, it’s not acceptable for session messages to disappear. The implementation needs to provide facilities for determining whether or not the messages were actually displayed, and delay purging the message list if necessary.
  • Flexibility. Support the case where a large number of independent, pluggable apps do messaging in the same project (sometimes for the same request), but don’t require it. Display all the messages created by all the apps, but don’t break (or lose messages) if one of the apps doesn’t happen to use the messaging implementation.
  • Efficiency. Avoid storing messages in the database (or another persistent store) if possible. While it’s possible to use memcache as a session backend, this isn’t always possible. One potential implementation would be to store shorter messages directly in a cookie, but provide a fallback to session-based storage for longer messages.

Here’s the implementation we use at Caktus, which is far from complete but it does address some of these points. This code is based on a number of snippets as well as attachments to the above referenced ticket. It could be improved by purging each message independently when it is actually retrieved and adding facilities for cookie-based storage. While I haven’t used it yet, django-notify looks a lot better than this and I’m excited about trying it out.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
from django.utils.encoding import StrAndUnicode
from django.contrib.sessions.backends.base import SessionBase
 
MESSAGES_NAME = '_messages'
 
SessionBase.get_messages = lambda self: self[MESSAGES_NAME]
 
def _session_get_and_delete_messages(self):
    messages = self.pop(MESSAGES_NAME, [])
    self[MESSAGES_NAME] = []
    return messages
SessionBase.get_and_delete_messages = \
  _session_get_and_delete_messages
 
def _session_create_message(self, message):
    self[MESSAGES_NAME].append(message)
    self.modified = True
SessionBase.create_message = _session_create_message
 
class SessionMessagesMiddleware(object):
    """
    To store messages or other user feedback in the session, add this
    class to your middleware.
 
    In your views, call request.session.create_message('the message') to
    add a message to the session.
 
    In your template(s), do this:
 
        {% if request.messages %}
            {% for message in request.messages %}<li>{{ message|escape }}</li>{% endfor %}
        {% endif %}
 
    Messages will NOT be erased from the session if you never access request.messages.
    """
 
    class LazyMessages(StrAndUnicode):
        """
        A lazy proxy for session messages.
        """
        def __init__(self, session):
            self.session = session
            super(SessionMessagesMiddleware.LazyMessages, self).__init__()
 
        def __iter__(self):
            return iter(self.messages)
 
        def __len__(self):
            return len(self.messages)
 
        def __nonzero__(self):
            return bool(self.messages)
 
        def __unicode__(self):
            return unicode(self.messages)
 
        def __getitem__(self, *args, **kwargs):
            return self.messages.__getitem__(*args, **kwargs)
 
        def _get_messages(self):
            if not hasattr(self, '_messages'):
                self._messages = self.session.get_and_delete_messages()
            return self._messages
        messages = property(_get_messages)
 
    def process_request(self, request):
        if not hasattr(request, 'session'):
            raise AttributeError('Request has no attribute "session".  Make sure session middleware is running before SessionMessages middleware.')
 
        if MESSAGES_NAME not in request.session:
            request.session[MESSAGES_NAME] = []
 
        request.messages = \
          SessionMessagesMiddleware.LazyMessages(request.session)

11 Responses to “Towards a Standard for Django Session Messages”

  1. Simon Willison Says:

    Couldn’t agree more that Django needs a standard solution for this, but I don’t think the solution should rely on sessions. In most cases, there’s no need to round trip to the database (or another persistent store) at all for this - a signed cookie that’s deleted when the message is viewed should be adequate for short messages, and for long messages the cookie can contain a key which references the longer message string which is stored on the server.

  2. tobias Says:

    That sounds perfectly reasonable, maybe even better. I think django-notify has an option for cookie storage.

    That said, I’ve had good luck with memcache as a session backend, which avoids the round trip to the database. And assuming the database is out of the picture, the session is also just a key that references a larger “message” stored on the server.

    Still, your point is well taken. “Session Messages” was probably a poor title choice for this article.

  3. Sean Patrick Hogan Says:

    How time flies. I remember when I opened the ticket two years ago.

    Simon is right that there’s no reason to make a round trip to the database to support this feature. However, many would argue (like Cal Henderson) that we don’t need to hit the database for sessions either. With 4096 bytes we can store and sign quite a bit of data in there.

    Not that Rails is the gold standard in these things, but they do use sessions for this storage. I don’t personally have any stake in a certain method, but I would be happy to see it done.

    I’m glad to see the conversation pick up on the ticket again. Hopefully it will make the 1.2 release and thanks to everyone working to get 1.1 out!

  4. tobias Says:

    I added a point about efficiency and avoiding the database hit if possible. Thanks for the suggestions Simon and Sean.

  5. Vinilios Says:

    how about django-flash ?
    http://github.com/danielfm/django-flash/tree/master

    we have used in a couple of my company projects, it does good job

  6. tobias Says:

    My understanding is that django-flash currently does not meet the conditions outlined above, but I think the author is working on some of them.

    See conversation on this ticket:
    http://code.djangoproject.com/ticket/4604

  7. SmileyChris Says:

    Yep, django-notify comes with two temporary message storage backends. The first uses sessions, the other uses a signed cookie, just like Simon wants :)

    The signed cookie backend doesn’t handle longer messages being stored server side, but another backend which handles that would be easy to write and I’d be happy to include it too (probably even make it default - it’s seems the best of both scenarios)

  8. tobias Says:

    I’d be happy to contribute the combo backend..traveling right now but I’ll look at it later this week.

  9. tobias Says:

    The combo backend has been written (finally):

    http://code.google.com/p/django-notify/issues/detail?id=5

  10. bbb Says:

    I needed this functionality and started to hack around the apparent lack of it–then thought twice. Googling a bit led me to Ticket #4604, and to here/django-notify. *Exactly* what I was looking for and works seamlessly. Props to the contributors, thanks!

  11. tobias Says:

    A messages backend has been integrated in Django:

    http://code.djangoproject.com/changeset/11804

    It’s only in trunk at this point, but it’ll be released as part of Django 1.2. The documentation can be found here:

    http://docs.djangoproject.com/en/dev/ref/contrib/messages/

    Enjoy!

Leave a Reply