June 19, 2009
by Tobias McNulty
0 comments
Categories:
Technical

Towards a Standard for Django Session Messages

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.

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)
blog comments powered by Disqus