Code

Ticket #4604: django-contrib-messages-e4da706e1152.diff

File django-contrib-messages-e4da706e1152.diff, 96.5 KB (added by tobias, 5 years ago)

diff showing changes in django-contrib-messages branch as of changeset e4da706e1152

Line 
1diff -r 1fb6476dce9d django/conf/global_settings.py
2--- a/django/conf/global_settings.py    Wed Dec 02 00:30:00 2009 +0000
3+++ b/django/conf/global_settings.py    Thu Dec 03 01:33:20 2009 -0500
4@@ -172,6 +172,7 @@
5     'django.core.context_processors.i18n',
6     'django.core.context_processors.media',
7 #    'django.core.context_processors.request',
8+    'django.contrib.messages.context_processors.messages',
9 )
10 
11 # Output to use in template system for invalid (e.g. misspelled) variables.
12@@ -308,6 +309,7 @@
13     'django.contrib.sessions.middleware.SessionMiddleware',
14     'django.middleware.csrf.CsrfViewMiddleware',
15     'django.contrib.auth.middleware.AuthenticationMiddleware',
16+    'django.contrib.messages.middleware.MessageMiddleware',
17 #     'django.middleware.http.ConditionalGetMiddleware',
18 #     'django.middleware.gzip.GZipMiddleware',
19 )
20diff -r 1fb6476dce9d django/conf/project_template/settings.py
21--- a/django/conf/project_template/settings.py  Wed Dec 02 00:30:00 2009 +0000
22+++ b/django/conf/project_template/settings.py  Thu Dec 03 01:33:20 2009 -0500
23@@ -62,6 +62,7 @@
24     'django.contrib.sessions.middleware.SessionMiddleware',
25     'django.middleware.csrf.CsrfViewMiddleware',
26     'django.contrib.auth.middleware.AuthenticationMiddleware',
27+    'django.contrib.messages.middleware.MessageMiddleware',
28 )
29 
30 ROOT_URLCONF = '{{ project_name }}.urls'
31@@ -77,4 +78,5 @@
32     'django.contrib.contenttypes',
33     'django.contrib.sessions',
34     'django.contrib.sites',
35+    'django.contrib.messages',
36 )
37diff -r 1fb6476dce9d django/contrib/admin/options.py
38--- a/django/contrib/admin/options.py   Wed Dec 02 00:30:00 2009 +0000
39+++ b/django/contrib/admin/options.py   Thu Dec 03 01:33:20 2009 -0500
40@@ -6,6 +6,7 @@
41 from django.contrib.admin import widgets
42 from django.contrib.admin import helpers
43 from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_ngettext, model_format_dict
44+from django.contrib import messages
45 from django.views.decorators.csrf import csrf_protect
46 from django.core.exceptions import PermissionDenied
47 from django.db import models, transaction
48@@ -541,9 +542,9 @@
49     def message_user(self, request, message):
50         """
51         Send a message to the user. The default implementation
52-        posts a message using the auth Message object.
53+        posts a message using the django.contrib.messages backend.
54         """
55-        request.user.message_set.create(message=message)
56+        messages.info(request, message)
57 
58     def save_form(self, request, form, change):
59         """
60diff -r 1fb6476dce9d django/contrib/admin/views/template.py
61--- a/django/contrib/admin/views/template.py    Wed Dec 02 00:30:00 2009 +0000
62+++ b/django/contrib/admin/views/template.py    Thu Dec 03 01:33:20 2009 -0500
63@@ -6,6 +6,7 @@
64 from django.conf import settings
65 from django.utils.importlib import import_module
66 from django.utils.translation import ugettext_lazy as _
67+from django.contrib import messages
68 
69 
70 def template_validator(request):
71@@ -23,7 +24,7 @@
72         form = TemplateValidatorForm(settings_modules, site_list,
73                                      data=request.POST)
74         if form.is_valid():
75-            request.user.message_set.create(message='The template is valid.')
76+            messages.info(request, 'The template is valid.')
77     else:
78         form = TemplateValidatorForm(settings_modules, site_list)
79     return render_to_response('admin/template_validator.html', {
80diff -r 1fb6476dce9d django/contrib/auth/admin.py
81--- a/django/contrib/auth/admin.py      Wed Dec 02 00:30:00 2009 +0000
82+++ b/django/contrib/auth/admin.py      Thu Dec 03 01:33:20 2009 -0500
83@@ -3,6 +3,7 @@
84 from django.contrib import admin
85 from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AdminPasswordChangeForm
86 from django.contrib.auth.models import User, Group
87+from django.contrib import messages
88 from django.core.exceptions import PermissionDenied
89 from django.http import HttpResponseRedirect, Http404
90 from django.shortcuts import render_to_response, get_object_or_404
91@@ -67,12 +68,13 @@
92                 msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': 'user', 'obj': new_user}
93                 self.log_addition(request, new_user)
94                 if "_addanother" in request.POST:
95-                    request.user.message_set.create(message=msg)
96+                    messages.success(request, msg)
97                     return HttpResponseRedirect(request.path)
98                 elif '_popup' in request.REQUEST:
99                     return self.response_add(request, new_user)
100                 else:
101-                    request.user.message_set.create(message=msg + ' ' + ugettext("You may edit it again below."))
102+                    messages.success(request, msg + ' ' +
103+                                     ugettext("You may edit it again below."))
104                     return HttpResponseRedirect('../%s/' % new_user.id)
105         else:
106             form = self.add_form()
107@@ -104,7 +106,7 @@
108             if form.is_valid():
109                 new_user = form.save()
110                 msg = ugettext('Password changed successfully.')
111-                request.user.message_set.create(message=msg)
112+                messages.success(request, msg)
113                 return HttpResponseRedirect('..')
114         else:
115             form = self.change_password_form(user)
116diff -r 1fb6476dce9d django/contrib/auth/models.py
117--- a/django/contrib/auth/models.py     Wed Dec 02 00:30:00 2009 +0000
118+++ b/django/contrib/auth/models.py     Thu Dec 03 01:33:20 2009 -0500
119@@ -288,6 +288,14 @@
120                 raise SiteProfileNotAvailable
121         return self._profile_cache
122 
123+    def _get_message_set(self):
124+        import warnings
125+        warnings.warn('The user messaging API is deprecated. Please update'
126+                      ' your code to use the new messages framework.',
127+                      category=PendingDeprecationWarning)
128+        return self._message_set
129+    message_set = property(_get_message_set)
130+
131 class Message(models.Model):
132     """
133     The message system is a lightweight way to queue messages for given
134@@ -297,7 +305,7 @@
135     actions. For example, "The poll Foo was created successfully." is a
136     message.
137     """
138-    user = models.ForeignKey(User)
139+    user = models.ForeignKey(User, related_name='_message_set')
140     message = models.TextField(_('message'))
141 
142     def __unicode__(self):
143diff -r 1fb6476dce9d django/contrib/messages/__init__.py
144--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
145+++ b/django/contrib/messages/__init__.py       Thu Dec 03 01:33:20 2009 -0500
146@@ -0,0 +1,2 @@
147+from api import *
148+from constants import *
149diff -r 1fb6476dce9d django/contrib/messages/api.py
150--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
151+++ b/django/contrib/messages/api.py    Thu Dec 03 01:33:20 2009 -0500
152@@ -0,0 +1,84 @@
153+from django.contrib.messages import constants
154+from django.utils.functional import lazy, memoize
155+
156+__all__ = (
157+    'add_message', 'get_messages',
158+    'debug', 'info', 'success', 'warning', 'error',
159+)
160+
161+
162+class AddMessageFailure(Exception):
163+    pass
164+
165+
166+def add_message(request, level, message, extra_tags='', fail_silently=False):
167+    """
168+    Attempts to add a message to the request using the 'messages' app, falling
169+    back to the user's message_set if MessageMiddleware hasn't been enabled.
170+    """
171+    if hasattr(request, '_messages'):
172+        return request._messages.add(level, message, extra_tags)
173+    if hasattr(request, 'user') and request.user.is_authenticated():
174+        return request.user.message_set.create(message=message)
175+    if not fail_silently:
176+        raise AddMessageFailure('Without the django.contrib.messages '
177+                                'middleware, messages can only be added to '
178+                                'authenticated users.')
179+
180+
181+def get_messages(request):
182+    """
183+    Returns the message storage on the request if it exists, otherwise returns
184+    user.message_set.all() as the old auth context processor did.
185+    """
186+    if hasattr(request, '_messages'):
187+        return request._messages
188+
189+    def get_user():
190+        if hasattr(request, 'user'):
191+            return request.user
192+        else:
193+            from django.contrib.auth.models import AnonymousUser
194+            return AnonymousUser()
195+
196+    return lazy(memoize(get_user().get_and_delete_messages, {}, 0), list)()
197+
198+
199+def debug(request, message, extra_tags='', fail_silently=False):
200+    """
201+    Adds a message with the ``DEBUG`` level.
202+    """
203+    add_message(request, constants.DEBUG, message, extra_tags=extra_tags,
204+                fail_silently=fail_silently)
205+
206+
207+def info(request, message, extra_tags='', fail_silently=False):
208+    """
209+    Adds a message with the ``INFO`` level.
210+    """
211+    add_message(request, constants.INFO, message, extra_tags=extra_tags,
212+                fail_silently=fail_silently)
213+
214+
215+def success(request, message, extra_tags='', fail_silently=False):
216+    """
217+    Adds a message with the ``SUCCESS`` level.
218+    """
219+    add_message(request, constants.SUCCESS, message, extra_tags=extra_tags,
220+                fail_silently=fail_silently)
221+
222+
223+def warning(request, message, extra_tags='', fail_silently=False):
224+    """
225+    Adds a message with the ``WARNING`` level.
226+    """
227+    add_message(request, constants.WARNING, message, extra_tags=extra_tags,
228+                fail_silently=fail_silently)
229+
230+
231+def error(request, message, extra_tags='', fail_silently=False):
232+    """
233+    Adds a message with the ``ERROR`` level.
234+    """
235+    add_message(request, constants.ERROR, message, extra_tags=extra_tags,
236+                fail_silently=fail_silently)
237diff -r 1fb6476dce9d django/contrib/messages/constants.py
238--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
239+++ b/django/contrib/messages/constants.py      Thu Dec 03 01:33:20 2009 -0500
240@@ -0,0 +1,13 @@
241+DEBUG = 10
242+INFO = 20
243+SUCCESS = 25
244+WARNING = 30
245+ERROR = 40
246+
247+DEFAULT_TAGS = {
248+    DEBUG: 'debug',
249+    INFO: 'info',
250+    SUCCESS: 'success',
251+    WARNING: 'warning',
252+    ERROR: 'error',
253+}
254diff -r 1fb6476dce9d django/contrib/messages/context_processors.py
255--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
256+++ b/django/contrib/messages/context_processors.py     Thu Dec 03 01:33:20 2009 -0500
257@@ -0,0 +1,8 @@
258+from django.contrib.messages.api import get_messages
259+
260+
261+def messages(request):
262+    """
263+    Returns a lazy 'messages' context variable.
264+    """
265+    return {'messages': get_messages(request)}
266diff -r 1fb6476dce9d django/contrib/messages/middleware.py
267--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
268+++ b/django/contrib/messages/middleware.py     Thu Dec 03 01:33:20 2009 -0500
269@@ -0,0 +1,26 @@
270+from django.conf import settings
271+from django.contrib.messages.storage import Storage
272+
273+
274+class MessageMiddleware(object):
275+    """
276+    Middleware that handles temporary messages.
277+    """
278+
279+    def process_request(self, request):
280+        request._messages = Storage(request)
281+
282+    def process_response(self, request, response):
283+        """
284+        Updates the storage backend (i.e., saves the messages).
285+       
286+        If not all messages could not be stored and ``DEBUG`` is ``True``, a
287+        ``ValueError`` is raised.
288+        """
289+        # A higher middleware layer may return a request which does not contain
290+        # messages storage, so make no assumption that it will be there.
291+        if hasattr(request, '_messages'):
292+            unstored_messages = request._messages.update(response)
293+            if unstored_messages and settings.DEBUG:
294+                raise ValueError('Not all temporary messages could be stored.')
295+        return response
296diff -r 1fb6476dce9d django/contrib/messages/models.py
297--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
298+++ b/django/contrib/messages/models.py Thu Dec 03 01:33:20 2009 -0500
299@@ -0,0 +1,1 @@
300+# Models module required so tests are discovered.
301diff -r 1fb6476dce9d django/contrib/messages/storage/__init__.py
302--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
303+++ b/django/contrib/messages/storage/__init__.py       Thu Dec 03 01:33:20 2009 -0500
304@@ -0,0 +1,26 @@
305+from django.conf import settings
306+from django.utils.importlib import import_module
307+
308+
309+def get_storage(import_path):
310+    """
311+    Imports the message storage class described by import_path, where
312+    import_path is the full Python path to the class.
313+    """
314+    try:
315+        dot = import_path.rindex('.')
316+    except ValueError:
317+        raise ImproperlyConfigured("%s isn't a Python path." % import_path)
318+    module, classname = import_path[:dot], import_path[dot+1:]
319+    try:
320+        mod = import_module(module)
321+    except ImportError, e:
322+        raise ImproperlyConfigured('Error importing module %s: "%s"' % (module, e))
323+    try:
324+        return getattr(mod, classname)
325+    except AttributeError:
326+        raise ImproperlyConfigured('Module "%s" does not define a "%s" class.' % (module, classname))
327+
328+
329+Storage = get_storage(getattr(settings, 'MESSAGE_STORAGE',
330+    'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'))
331diff -r 1fb6476dce9d django/contrib/messages/storage/base.py
332--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
333+++ b/django/contrib/messages/storage/base.py   Thu Dec 03 01:33:20 2009 -0500
334@@ -0,0 +1,179 @@
335+from django.conf import settings
336+from django.utils.encoding import force_unicode, StrAndUnicode
337+from django.contrib.messages import constants, utils
338+
339+
340+LEVEL_TAGS = utils.get_level_tags()
341+
342+
343+class Message(StrAndUnicode):
344+    """
345+    Represents an actual message that can be stored in any of the supported
346+    storage classes (typically session- or cookie-based) and rendered in a view
347+    or template.
348+    """
349+
350+    def __init__(self, level, message, extra_tags=None):
351+        self.level = int(level)
352+        self.message = message
353+        self.extra_tags = extra_tags
354+
355+    def _prepare(self):
356+        """
357+        Prepares the message for serialization by forcing the ``message``
358+        and ``extra_tags`` to unicode in case they are lazy translations.
359+       
360+        Known "safe" types (None, int, etc.) are not converted (see Django's
361+        ``force_unicode`` implementation for details).
362+        """
363+        self.message = force_unicode(self.message, strings_only=True)
364+        self.extra_tags = force_unicode(self.extra_tags, strings_only=True)
365+   
366+    def __eq__(self, other):
367+        return isinstance(other, Message) and self.level == other.level and \
368+                                              self.message == other.message
369+
370+    def __unicode__(self):
371+        return force_unicode(self.message)
372+
373+    def _get_tags(self):
374+        label_tag = force_unicode(LEVEL_TAGS.get(self.level, ''),
375+                                  strings_only=True)
376+        extra_tags = force_unicode(self.extra_tags, strings_only=True)
377+        if extra_tags and label_tag:
378+            return u' '.join([extra_tags, label_tag])
379+        elif extra_tags:
380+            return extra_tags
381+        elif label_tag:
382+            return label_tag
383+        return ''
384+    tags = property(_get_tags)
385+
386+
387+class BaseStorage(object):
388+    """
389+    This is the base backend for temporary message storage.
390+   
391+    This is not a complete class; to be a usable storage backend, it must be
392+    subclassed and the two methods ``_get`` and ``_store`` overridden.
393+    """
394+
395+    def __init__(self, request, *args, **kwargs):
396+        self.request = request
397+        self._queued_messages = []
398+        self.used = False
399+        self.added_new = False
400+        super(BaseStorage, self).__init__(*args, **kwargs)
401+
402+    def __len__(self):
403+        return len(self._loaded_messages) + len(self._queued_messages)
404+
405+    def __iter__(self):
406+        self.used = True
407+        if self._queued_messages:
408+            self._loaded_messages.extend(self._queued_messages)
409+            self._queued_messages = []
410+        return iter(self._loaded_messages)
411+
412+    def __contains__(self, item):
413+        return item in self._loaded_messages or item in self._queued_messages
414+
415+    @property
416+    def _loaded_messages(self):
417+        """
418+        Returns a list of loaded messages, retrieving them first if they have
419+        not been loaded yet.
420+        """
421+        if not hasattr(self, '_loaded_data'):
422+            messages, all_retrieved = self._get()
423+            self._loaded_data = messages or []
424+        return self._loaded_data
425+
426+    def _get(self, *args, **kwargs):
427+        """
428+        Retrieves a list of stored messages. Returns a tuple of the messages and
429+        a flag indicating whether or not all the messages originally intended
430+        to be stored in this storage were, in fact, stored and now retrieved.
431+       
432+        **This method must be implemented by a subclass.**
433+       
434+        If it is possible to tell if the backend was not used (as opposed to
435+        just containing no messages) then ``None`` should be returned.
436+        """
437+        raise NotImplementedError()
438+
439+    def _store(self, messages, response, *args, **kwargs):
440+        """
441+        Stores a list of messages, returning a list of any messages which could
442+        not be stored.
443+       
444+        One type of object must be able to be stored, ``Message``.
445+       
446+        **This method must be implemented by a subclass.**
447+        """
448+        raise NotImplementedError()
449+
450+    def _prepare_messages(self, messages):
451+        """
452+        Prepares a list of messages for storage.
453+        """
454+        for message in messages:
455+            message._prepare()
456+
457+    def update(self, response):
458+        """
459+        Stores all unread messages.
460+       
461+        If the backend has yet to be iterated, previously stored messages will
462+        be stored again. Otherwise, only messages added after the last
463+        iteration will be stored.
464+        """
465+        self._prepare_messages(self._queued_messages)
466+        if self.used:
467+            return self._store(self._queued_messages, response)
468+        elif self.added_new:
469+            messages = self._loaded_messages + self._queued_messages
470+            return self._store(messages, response)
471+
472+    def add(self, level, message, extra_tags=''):
473+        """
474+        Queues a message to be stored.
475+       
476+        The message is only queued if it contained something and its level is
477+        not less than the recording level (``self.level``).
478+        """
479+        if not message:
480+            return
481+        # Check that the message level is not less than the recording level.
482+        level = int(level)
483+        if level < self.level:
484+            return
485+        # Add the message.
486+        self.added_new = True
487+        message = Message(level, message, extra_tags=extra_tags)
488+        self._queued_messages.append(message)
489+
490+    def _get_level(self):
491+        """
492+        Returns the minimum recorded level.
493+       
494+        The default level is the ``MESSAGE_LEVEL`` setting. If this is
495+        not found, the ``INFO`` level is used.
496+        """
497+        if not hasattr(self, '_level'):
498+            self._level = getattr(settings, 'MESSAGE_LEVEL', constants.INFO)
499+        return self._level
500+
501+    def _set_level(self, value=None):
502+        """
503+        Sets a custom minimum recorded level.
504+       
505+        If set to ``None``, the default level will be used (see the
506+        ``_get_level`` method).
507+        """
508+        if value is None and hasattr(self, '_level'):
509+            del self._level
510+        else:
511+            self._level = int(value)
512+
513+    level = property(_get_level, _set_level, _set_level)
514diff -r 1fb6476dce9d django/contrib/messages/storage/cookie.py
515--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
516+++ b/django/contrib/messages/storage/cookie.py Thu Dec 03 01:33:20 2009 -0500
517@@ -0,0 +1,147 @@
518+import hmac
519+
520+from django.conf import settings
521+from django.utils.hashcompat import sha_constructor
522+from django.contrib.messages import constants
523+from django.contrib.messages.storage.base import BaseStorage, Message
524+try:
525+    import json   # Available in Python 2.6.
526+except ImportError:
527+    # Otherwise fall back to simplejson bundled with Django.
528+    from django.utils import simplejson as json
529+
530+
531+class MessageEncoder(json.JSONEncoder):
532+    """
533+    Compactly serializes instances of the ``Message`` class as JSON.
534+    """
535+    message_key = '__json_message'
536+
537+    def default(self, obj):
538+        if isinstance(obj, Message):
539+            message = [self.message_key, obj.level, obj.message]
540+            if obj.extra_tags:
541+                message.append(obj.extra_tags)
542+            return message
543+        return super(MessageEncoder, self).default(obj)
544+
545+
546+class MessageDecoder(json.JSONDecoder):
547+    """
548+    Decodes JSON that includes serialized ``Message`` instances.
549+    """
550+
551+    def process_messages(self, obj):
552+        if isinstance(obj, list) and obj:
553+            if obj[0] == MessageEncoder.message_key:
554+                return Message(*obj[1:])
555+            return [self.process_messages(item) for item in obj]
556+        if isinstance(obj, dict):
557+            return dict([(key, self.process_messages(value))
558+                         for key, value in obj.iteritems()])
559+        return obj
560+
561+    def decode(self, s, **kwargs):
562+        decoded = super(MessageDecoder, self).decode(s, **kwargs)
563+        return self.process_messages(decoded)
564+
565+
566+class CookieStorage(BaseStorage):
567+    """
568+    Stores messages in a cookie.
569+    """
570+    cookie_name = 'messages'
571+    max_cookie_size = 4096
572+    not_finished = '__messagesnotfinished__'
573+   
574+    def _get(self, *args, **kwargs):
575+        """
576+        Retrieves a list of messages from the messages cookie.  If the
577+        not_finished sentinel value is found at the end of the message list,
578+        remove it and return a result indicating that not all messages were
579+        retrieved by this storage.
580+        """
581+        data = self.request.COOKIES.get(self.cookie_name)
582+        messages = self._decode(data)
583+        all_retrieved = not (messages and messages[-1] == self.not_finished)
584+        if messages and not all_retrieved:
585+            # remove the sentinel value
586+            messages.pop()
587+        return messages, all_retrieved
588+
589+    def _update_cookie(self, encoded_data, response):
590+        """
591+        Either sets the cookie with the encoded data if there is any data to
592+        store, or deletes the cookie.
593+        """
594+        if encoded_data:
595+            response.set_cookie(self.cookie_name, encoded_data)
596+        else:
597+            response.delete_cookie(self.cookie_name)
598+
599+    def _store(self, messages, response, remove_oldest=True, *args, **kwargs):
600+        """
601+        Stores the messages to a cookie, returning a list of any messages which
602+        could not be stored.
603+       
604+        If the encoded data is larger than ``max_cookie_size``, removes
605+        messages until the data fits (these are the messages which are
606+        returned), and add the not_finished sentinel value to indicate as much.
607+        """
608+        unstored_messages = []
609+        encoded_data = self._encode(messages)
610+        if self.max_cookie_size:
611+            while encoded_data and len(encoded_data) > self.max_cookie_size:
612+                if remove_oldest:
613+                    unstored_messages.append(messages.pop(0))
614+                else:
615+                    unstored_messages.insert(0, messages.pop())
616+                encoded_data = self._encode(messages + [self.not_finished],
617+                                            encode_empty=unstored_messages)
618+        self._update_cookie(encoded_data, response)
619+        return unstored_messages
620+
621+    def _hash(self, value):
622+        """
623+        Creates an HMAC/SHA1 hash based on the value and the project setting's
624+        SECRET_KEY, modified to make it unique for the present purpose.
625+        """
626+        key = 'django.contrib.messages' + settings.SECRET_KEY
627+        return hmac.new(key, value, sha_constructor).hexdigest()
628+   
629+    def _encode(self, messages, encode_empty=False):
630+        """
631+        Returns an encoded version of the messages list which can be stored as
632+        plain text.
633+       
634+        Since the data will be retrieved from the client-side, the encoded data
635+        also contains a hash to ensure that the data was not tampered with.
636+        """
637+        if messages or encode_empty:
638+            encoder = MessageEncoder(separators=(',', ':'))
639+            value = encoder.encode(messages)
640+            return '%s$%s' % (self._hash(value), value)
641+
642+    def _decode(self, data):
643+        """
644+        Safely decodes a encoded text stream back into a list of messages.
645+       
646+        If the encoded text stream contained an invalid hash or was in an
647+        invalid format, ``None`` is returned.
648+        """
649+        if not data:
650+            return None
651+        bits = data.split('$', 1)
652+        if len(bits) == 2:
653+            hash, value = bits
654+            if hash == self._hash(value):
655+                try:
656+                    # If we get here (and the JSON decode works), everything is
657+                    # good. In any other case, drop back and return None.
658+                    return json.loads(value, cls=MessageDecoder)
659+                except ValueError:
660+                    pass
661+        # Mark the data as used (so it gets removed) since something was wrong
662+        # with the data.
663+        self.used = True
664+        return None
665diff -r 1fb6476dce9d django/contrib/messages/storage/fallback.py
666--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
667+++ b/django/contrib/messages/storage/fallback.py       Thu Dec 03 01:33:20 2009 -0500
668@@ -0,0 +1,62 @@
669+from django.contrib.messages.storage.base import BaseStorage
670+from django.contrib.messages.storage.cookie import CookieStorage
671+from django.contrib.messages.storage.session import SessionStorage
672+try:
673+    set
674+except NameError:
675+    from sets import Set as set   # Python 2.3
676+
677+
678+class FallbackStorage(BaseStorage):
679+    """
680+    Tries to store all messages in the first backend, storing any unstored
681+    messages in each subsequent backend backend.
682+    """
683+    storage_classes = (CookieStorage, SessionStorage)
684+
685+    def __init__(self, *args, **kwargs):
686+        super(FallbackStorage, self).__init__(*args, **kwargs)
687+        self.storages = [storage_class(*args, **kwargs)
688+                         for storage_class in self.storage_classes]
689+        self._used_storages = set()
690+
691+    def _get(self, *args, **kwargs):
692+        """
693+        Gets a single list of messages from all storage backends.
694+        """
695+        all_messages = []
696+        for storage in self.storages:
697+            messages, all_retrieved = storage._get()
698+            # If the backend hasn't been used, no more retrieval is necessary.
699+            if messages is None:
700+                break
701+            if messages:
702+                self._used_storages.add(storage)
703+            all_messages.extend(messages)
704+            # If this storage class contained all the messages, no further
705+            # retrieval is necessary
706+            if all_retrieved:
707+                break
708+        return all_messages, all_retrieved
709+
710+    def _store(self, messages, response, *args, **kwargs):
711+        """
712+        Stores the messages, returning any unstored messages after trying all
713+        backends.
714+       
715+        For each storage backend, any messages not stored are passed on to the
716+        next backend.
717+        """
718+        for storage in self.storages:
719+            if messages:
720+                messages = storage._store(
721+                    messages,
722+                    response,
723+                    remove_oldest=False,
724+                )
725+            # Even if there are no more messages, continue iterating to ensure
726+            # storages which contained messages are flushed.
727+            elif storage in self._used_storages:
728+                storage._store([], response)
729+                self._used_storages.remove(storage)
730+        return messages
731diff -r 1fb6476dce9d django/contrib/messages/storage/session.py
732--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
733+++ b/django/contrib/messages/storage/session.py        Thu Dec 03 01:33:20 2009 -0500
734@@ -0,0 +1,33 @@
735+from django.contrib.messages.storage.base import BaseStorage
736+
737+
738+class SessionStorage(BaseStorage):
739+    """
740+    Stores messages in the session (that is, django.contrib.sessions).
741+    """
742+    session_key = '_messages'
743+
744+    def __init__(self, request, *args, **kwargs):
745+        assert hasattr(request, 'session'), "The session-based temporary "\
746+            "message storage requires session middleware to be installed, "\
747+            "and come before the message middleware in the MIDDLEWARE_CLASSES "\
748+            "list."
749+        super(SessionStorage, self).__init__(request, *args, **kwargs)
750+
751+    def _get(self, *args, **kwargs):
752+        """
753+        Retrieves a list of messages from the request's session.  This storage
754+        always stores everything it is given, so return True for the
755+        all_retrieved flag.
756+        """
757+        return self.request.session.get(self.session_key), True
758+
759+    def _store(self, messages, response, *args, **kwargs):
760+        """
761+        Stores a list of messages to the request's session.
762+        """
763+        if messages:
764+            self.request.session[self.session_key] = messages
765+        else:
766+            self.request.session.pop(self.session_key, None)
767+        return []
768diff -r 1fb6476dce9d django/contrib/messages/storage/user_messages.py
769--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
770+++ b/django/contrib/messages/storage/user_messages.py  Thu Dec 03 01:33:20 2009 -0500
771@@ -0,0 +1,64 @@
772+"""
773+Storages used to assist in the deprecation of contrib.auth User messages.
774+
775+"""
776+from django.contrib.messages import constants
777+from django.contrib.messages.storage.base import BaseStorage, Message
778+from django.contrib.auth.models import User
779+from django.contrib.messages.storage.fallback import FallbackStorage
780+
781+
782+class UserMessagesStorage(BaseStorage):
783+    """
784+    Retrieves messages from the User, using the legacy user.message_set API.
785+   
786+    This storage is "read-only" insofar as it can only retrieve and delete
787+    messages, not store them.
788+    """
789+    session_key = '_messages'
790+
791+    def _get_messages_queryset(self):
792+        """
793+        Returns the QuerySet containing all user messages (or ``None`` if
794+        request.user is not a contrib.auth User).
795+        """
796+        user = getattr(self.request, 'user', None)
797+        if isinstance(user, User):
798+            return user._message_set.all()
799+
800+    def add(self, *args, **kwargs):
801+        raise NotImplementedError('This message storage is read-only.')
802+
803+    def _get(self, *args, **kwargs):
804+        """
805+        Retrieves a list of messages assigned to the User.  This backend never
806+        stores anything, so all_retrieved is assumed to be False.
807+        """
808+        queryset = self._get_messages_queryset()
809+        if queryset is None:
810+            # This is a read-only and optional storage, so to ensure other
811+            # storages will also be read if used with FallbackStorage an empty
812+            # list is returned rather than None.
813+            return [], False
814+        messages = []
815+        for user_message in queryset:
816+            messages.append(Message(constants.INFO, user_message.message))
817+        return messages, False
818+
819+    def _store(self, messages, *args, **kwargs):
820+        """
821+        Removes any messages assigned to the User and returns the list of
822+        messages (since no messages are stored in this read-only storage).
823+        """
824+        queryset = self._get_messages_queryset()
825+        if queryset is not None:
826+            queryset.delete()
827+        return messages
828+
829+
830+class LegacyFallbackStorage(FallbackStorage):
831+    """
832+    Works like ``FallbackStorage`` but also handles retrieving (and clearing)
833+    contrib.auth User messages.
834+    """
835+    storage_classes = (UserMessagesStorage,) + FallbackStorage.storage_classes
836diff -r 1fb6476dce9d django/contrib/messages/utils.py
837--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
838+++ b/django/contrib/messages/utils.py  Thu Dec 03 01:33:20 2009 -0500
839@@ -0,0 +1,11 @@
840+from django.conf import settings
841+from django.contrib.messages import constants
842+
843+
844+def get_level_tags():
845+    """
846+    Returns the message level tags.
847+    """
848+    level_tags = constants.DEFAULT_TAGS.copy()
849+    level_tags.update(getattr(settings, 'MESSAGE_TAGS', {}))
850+    return level_tags
851diff -r 1fb6476dce9d django/core/context_processors.py
852--- a/django/core/context_processors.py Wed Dec 02 00:30:00 2009 +0000
853+++ b/django/core/context_processors.py Thu Dec 03 01:33:20 2009 -0500
854@@ -10,6 +10,7 @@
855 from django.conf import settings
856 from django.middleware.csrf import get_token
857 from django.utils.functional import lazy, memoize, SimpleLazyObject
858+from django.contrib import messages
859 
860 def auth(request):
861     """
862@@ -37,8 +38,8 @@
863 
864     return {
865         'user': SimpleLazyObject(get_user),
866-        'messages': lazy(memoize(lambda: get_user().get_and_delete_messages(), {}, 0), list)(),
867-        'perms':  lazy(lambda: PermWrapper(get_user()), PermWrapper)(),
868+        'messages': messages.get_messages(request),
869+        'perms': lazy(lambda: PermWrapper(get_user()), PermWrapper)(),
870     }
871 
872 def csrf(request):
873diff -r 1fb6476dce9d django/views/generic/create_update.py
874--- a/django/views/generic/create_update.py     Wed Dec 02 00:30:00 2009 +0000
875+++ b/django/views/generic/create_update.py     Thu Dec 03 01:33:20 2009 -0500
876@@ -6,6 +6,7 @@
877 from django.utils.translation import ugettext
878 from django.contrib.auth.views import redirect_to_login
879 from django.views.generic import GenericViewError
880+from django.contrib import messages
881 
882 
883 def apply_extra_context(extra_context, context):
884@@ -110,8 +111,10 @@
885         form = form_class(request.POST, request.FILES)
886         if form.is_valid():
887             new_object = form.save()
888-            if request.user.is_authenticated():
889-                request.user.message_set.create(message=ugettext("The %(verbose_name)s was created successfully.") % {"verbose_name": model._meta.verbose_name})
890+           
891+            msg = ugettext("The %(verbose_name)s was created successfully.") %\
892+                                    {"verbose_name": model._meta.verbose_name}
893+            messages.success(request, msg, fail_silently=True)
894             return redirect(post_save_redirect, new_object)
895     else:
896         form = form_class()
897@@ -152,8 +155,9 @@
898         form = form_class(request.POST, request.FILES, instance=obj)
899         if form.is_valid():
900             obj = form.save()
901-            if request.user.is_authenticated():
902-                request.user.message_set.create(message=ugettext("The %(verbose_name)s was updated successfully.") % {"verbose_name": model._meta.verbose_name})
903+            msg = ugettext("The %(verbose_name)s was updated successfully.") %\
904+                                    {"verbose_name": model._meta.verbose_name}
905+            messages.success(request, msg, fail_silently=True)
906             return redirect(post_save_redirect, obj)
907     else:
908         form = form_class(instance=obj)
909@@ -194,8 +198,9 @@
910 
911     if request.method == 'POST':
912         obj.delete()
913-        if request.user.is_authenticated():
914-            request.user.message_set.create(message=ugettext("The %(verbose_name)s was deleted.") % {"verbose_name": model._meta.verbose_name})
915+        msg = ugettext("The %(verbose_name)s was deleted.") %\
916+                                    {"verbose_name": model._meta.verbose_name}
917+        messages.success(request, msg, fail_silently=True)
918         return HttpResponseRedirect(post_delete_redirect)
919     else:
920         if not template_name:
921diff -r 1fb6476dce9d docs/index.txt
922--- a/docs/index.txt    Wed Dec 02 00:30:00 2009 +0000
923+++ b/docs/index.txt    Thu Dec 03 01:33:20 2009 -0500
924@@ -170,6 +170,7 @@
925     * :ref:`Internationalization <topics-i18n>`
926     * :ref:`Jython support <howto-jython>`
927     * :ref:`"Local flavor" <ref-contrib-localflavor>`
928+    * :ref:`Messages <ref-contrib-messages>`
929     * :ref:`Pagination <topics-pagination>`
930     * :ref:`Redirects <ref-contrib-redirects>`
931     * :ref:`Serialization <topics-serialization>`
932diff -r 1fb6476dce9d docs/internals/deprecation.txt
933--- a/docs/internals/deprecation.txt    Wed Dec 02 00:30:00 2009 +0000
934+++ b/docs/internals/deprecation.txt    Thu Dec 03 01:33:20 2009 -0500
935@@ -28,6 +28,14 @@
936         * The many to many SQL generation functions on the database backends
937           will be removed.  These have been deprecated since the 1.2 release.
938 
939+        * The ``Message`` model (in ``django.contrib.auth``), its related
940+          manager in the ``User`` model (``user.message_set``), and the
941+          associated methods (``user.message_set.create()`` and
942+          ``user.get_and_delete_messages()``), which have
943+          been deprecated since the 1.2 release, will be removed.  The
944+          :ref:`messages framework <ref-contrib-messages>` should be used
945+          instead.
946+
947     * 2.0
948         * ``django.views.defaults.shortcut()``. This function has been moved
949           to ``django.contrib.contenttypes.views.shortcut()`` as part of the
950diff -r 1fb6476dce9d docs/ref/contrib/index.txt
951--- a/docs/ref/contrib/index.txt        Wed Dec 02 00:30:00 2009 +0000
952+++ b/docs/ref/contrib/index.txt        Thu Dec 03 01:33:20 2009 -0500
953@@ -34,6 +34,7 @@
954    formtools/index
955    humanize
956    localflavor
957+   messages
958    redirects
959    sitemaps
960    sites
961@@ -150,6 +151,17 @@
962 .. _Markdown: http://en.wikipedia.org/wiki/Markdown
963 .. _ReST (ReStructured Text): http://en.wikipedia.org/wiki/ReStructuredText
964 
965+messages
966+========
967+
968+.. versionchanged:: 1.2
969+    The messages framework was added.
970+
971+A framework for storing and retrieving temporary cookie- or session-based
972+messages
973+
974+See the :ref:`messages documentation <ref-contrib-messages>`.
975+
976 redirects
977 =========
978 
979diff -r 1fb6476dce9d docs/ref/contrib/messages.txt
980--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
981+++ b/docs/ref/contrib/messages.txt     Thu Dec 03 01:33:20 2009 -0500
982@@ -0,0 +1,381 @@
983+.. _ref-contrib-messages:
984+
985+======================
986+The messages framework
987+======================
988+
989+.. module:: django.contrib.messages
990+   :synopsis: Provides cookie- and session-based temporary message storage.
991+
992+Django provides full support for cookie- and session-based messaging, for
993+both anonymous and authenticated clients. The messages framework allows you
994+to temporarily store messages in one request and retrieve them for display
995+in a subsequent request (usually the next one). Every message is tagged
996+with a specific ``level`` that determines its priority (e.g., ``info``,
997+``warning``, or ``error``).
998+
999+.. versionadded:: 1.2
1000+   The messages framework was added.
1001+
1002+Enabling messages
1003+=================
1004+
1005+Messages are implemented through a :ref:`middleware <ref-middleware>`
1006+class and corresponding :ref:`context processor <ref-templates-api>`.
1007+
1008+To enable message functionality, do the following:
1009+
1010+    * Edit the :setting:`MIDDLEWARE_CLASSES` setting and make sure
1011+      it contains ``'django.contrib.messages.middleware.MessageMiddleware'``.
1012+
1013+      If you are using a :ref:`storage backend <storage-backends>` that relies
1014+      on :ref:`sessions <topics-http-sessions>` (the default),
1015+      ``'django.contrib.sessions.middleware.SessionMiddleware'`` must be
1016+      enabled and appear before ``MessageMiddleware`` in your
1017+      :setting:`MIDDLEWARE_CLASSES`.
1018+
1019+    * Edit the :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting and make sure
1020+      it contains ``'django.contrib.messages.context_processors.messages'``.
1021+
1022+    * Add ``'django.contrib.messages'`` to your :setting:`INSTALLED_APPS`
1023+      setting
1024+
1025+The default ``settings.py`` created by ``django-admin.py startproject`` has
1026+``MessageMiddleware`` activated and the ``django.contrib.messages`` app
1027+installed.  Also, the default value for :setting:`TEMPLATE_CONTEXT_PROCESSORS`
1028+contains ``'django.contrib.messages.context_processors.messages'``.
1029+
1030+If you don't want to use messages, you can remove the
1031+``MessageMiddleware`` line from :setting:`MIDDLEWARE_CLASSES`, the ``messages``
1032+context processor from :setting:`TEMPLATE_CONTEXT_PROCESSORS` and
1033+``'django.contrib.messages'`` from your :setting:`INSTALLED_APPS`.
1034+
1035+Configuring the message engine
1036+==============================
1037+
1038+By default, Django first tries to store messages in a lightweight cookie,
1039+and falls back to storing any messages that don't fit in the cookie in a
1040+session variable.
1041+
1042+.. _storage-backends:
1043+
1044+Storage backends
1045+----------------
1046+
1047+The messages framework can use different backends to store temporary messages.
1048+To change which backend is being used, add a `MESSAGE_STORAGE`_ to your
1049+settings, referencing the module and class of the storage class. For
1050+example::
1051+
1052+    MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
1053+
1054+The value should be the full path of the desired storage class.
1055+
1056+Four storage classes are included:
1057+
1058+``'django.contrib.messages.storage.session.SessionStorage'``
1059+    This class stores all messages inside of the request's session. It
1060+    requires Django's ``contrib.session`` application.
1061+
1062+``'django.contrib.messages.storage.cookie.CookieStorage'``
1063+    This class stores the message data in a cookie (signed with a secret hash to
1064+    prevent manipulation) to persist notifications across requests. Old messages
1065+    are dropped if the cookie data size would exceed 4096 bytes.
1066+
1067+``'django.contrib.messages.storage.fallback.FallbackStorage'``
1068+    This class first uses CookieStorage for all messages, falling back to using
1069+    SessionStorage for the messages that could not fit in a single cookie.
1070+
1071+    Since it is uses SessionStorage, it also requires Django's
1072+    ``contrib.session`` application.
1073+
1074+``'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'``
1075+    This is the default temporary storage class.
1076+
1077+    This class extends FallbackStorage and adds compatibility methods to
1078+    to retrieve any messages stored in the user Message model by code that
1079+    has not yet been updated to use the new API. This storage is temporary
1080+    (because it makes use of code that is pending deprecation) and will be
1081+    removed in Django 1.4. At that time, the default storage will become
1082+    ``django.contrib.messages.storage.fallback.FallbackStorage``. For more
1083+    information, see `LegacyFallbackStorage`_ below.
1084+
1085+To write your own storage class, subclass the ``BaseStorage`` class in
1086+``django.contrib.messages.storage.base`` and implement the ``_get`` and
1087+``_store`` methods.
1088+
1089+LegacyFallbackStorage
1090+^^^^^^^^^^^^^^^^^^^^^
1091+
1092+The ``LegacyFallbackStorage`` is a temporary tool to facilitate the transition
1093+from the deprecated ``user.message_set`` API and will be removed in Django 1.4
1094+according to Django's standard deprecation policy.  For more information, see
1095+the full :ref:`release process documentation <internals-release-process>`.
1096+
1097+In addition to the functionality in the ``FallbackStorage``, it adds a custom,
1098+read-only storage class that retrieves messages from the user ``Message`` model.
1099+Any messages that were stored in the ``Message`` model (e.g., by code that has
1100+not yet been updated to use the messages framework) will be retrieved first,
1101+followed by those stored in a cookie and in the session, if any.  Since messages
1102+stored in the ``Message`` model do not have a concept of levels, they will be
1103+assigned the ``INFO`` level by default.
1104+
1105+Message levels
1106+--------------
1107+
1108+The messages framework is based on a configurable level architecture similar
1109+to that of the Python logging module.  Message levels allow you to group
1110+messages by type so they can be filtered or displayed differently in views and
1111+templates.
1112+
1113+The built-in levels (which can be imported from ``django.contrib.messages``
1114+directly) are:
1115+
1116+=========== ========
1117+Constant    Purpose
1118+=========== ========
1119+``DEBUG``   Development-related messages that will be ignored (or removed) in a production deployment
1120+``INFO``    Informational messages for the user
1121+``SUCCESS`` An action was successful, e.g. "Your profile was updated successfully"
1122+``WARNING`` A failure did not occur but may be imminent
1123+``ERROR``   An action was **not** successful or some other failure occurred
1124+=========== ========
1125+
1126+The `MESSAGE_LEVEL`_ setting can be used to change the minimum recorded
1127+level. Attempts to add messages of a level less than this will be ignored.
1128+
1129+Message tags
1130+------------
1131+
1132+Message tags are a string representation of the message level plus any
1133+extra tags that were added directly in the view (see
1134+`Adding extra message tags`_ below for more details).  Tags are stored in a
1135+string and are separated by spaces.  Typically, message tags
1136+are used as CSS classes to customize message style based on message type. By
1137+default, each level has a single tag that's a lowercase version of its own
1138+constant:
1139+
1140+==============  ===========
1141+Level Constant  Tag
1142+==============  ===========
1143+``DEBUG``       ``debug``
1144+``INFO``        ``info``
1145+``SUCCESS``     ``success``
1146+``WARNING``     ``warning``
1147+``ERROR``       ``error``
1148+==============  ===========
1149+
1150+To change the default tags for a message level (either built-in or custom),
1151+set the `MESSAGE_TAGS`_ setting to a dictionary containing the levels
1152+you wish to change. As this extends the default tags, you only need to provide
1153+tags for the levels you wish to override::
1154+
1155+    from django.contrib.messages import constants as messages
1156+    MESSAGE_TAGS = {
1157+        messages.INFO: '',
1158+        50: 'critical',
1159+    }
1160+
1161+Using messages in views and templates
1162+=====================================
1163+
1164+Adding a message
1165+----------------
1166+
1167+To add a message, call::
1168+
1169+    from django.contrib import messages
1170+    messages.add_message(request, messages.INFO, 'Hello world.')
1171+
1172+Some shortcut methods provide a standard way to add messages with commonly
1173+used tags (which are usually represented as HTML classes for the message)::
1174+
1175+    messages.debug(request, '%s SQL statements were executed.' % count)
1176+    messages.info(request, 'Three credits remain in your account.')
1177+    messages.success(request, 'Profile details updated.')
1178+    messages.warning(request, 'Your account expires in three days.')
1179+    messages.error(request, 'Document deleted.')
1180+
1181+Displaying messages
1182+-------------------
1183+
1184+In your template, use something like::
1185+
1186+    {% if messages %}
1187+    <ul class="messages">
1188+        {% for message in messages %}
1189+        <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
1190+        {% endfor %}
1191+    </ul>
1192+    {% endif %}
1193+
1194+If you're using the context processor, your template should be rendered with a
1195+``RequestContext``. Otherwise, ensure ``messages`` is available to
1196+the template context.
1197+
1198+Creating custom message levels
1199+------------------------------
1200+
1201+Messages levels are nothing more than integers, so you can define your own
1202+level constants and use them to create more customized user feedback, e.g.::
1203+
1204+    CRITICAL = 50
1205+
1206+    def my_view(request):
1207+        messages.add_message(request, CRITICAL, 'A serious error occurred.')
1208+
1209+When creating custom message levels you should be careful to avoid overloading
1210+existing levels.  The values for the built-in levels are:
1211+
1212+==============  =====
1213+Level Constant  Value
1214+==============  =====
1215+``DEBUG``       10
1216+``INFO``        20
1217+``SUCCESS``     25
1218+``WARNING``     30
1219+``ERROR``       40
1220+==============  =====
1221+
1222+If you need to identify the custom levels in your HTML or CSS, you need to
1223+provide a mapping via the `MESSAGE_TAGS`_ setting.
1224+
1225+**Note:** If you are creating a reusable application, it is recommended to use
1226+only the built-in `message levels`_ and not rely on any custom levels.
1227+
1228+Changing the minimum recorded level per-request
1229+-----------------------------------------------
1230+
1231+The minimum recorded level can be set per request by changing the ``level``
1232+attribute of the messages storage instance::
1233+
1234+    from django.contrib import messages
1235+
1236+    # Change the messages level to ensure the debug message is added.
1237+    messages.get_messages(request).level = messages.DEBUG
1238+    messages.debug(request, 'Test message...')
1239+
1240+    # In another request, record only messages with a level of WARNING and higher
1241+    messages.get_messages(request).level = messages.WARNING
1242+    messages.success(request, 'Your profile was updated.') # ignored
1243+    messages.warning(request, 'Your account is about to expire.') # recorded
1244+
1245+    # Set the messages level back to default.
1246+    messages.get_messages(request).level = None
1247+
1248+For more information on how the minimum recorded level functions, see
1249+`Message levels`_ above.
1250+
1251+Adding extra message tags
1252+-------------------------
1253+
1254+For more direct control over message tags, you can optionally provide a string
1255+containing extra tags to any of the add methods::
1256+
1257+    messages.add_message(request, messages.INFO, 'Over 9000!',
1258+                         extra_tags='dragonball')
1259+    messages.error(request, 'Email box full', extra_tags='email')
1260+
1261+Extra tags are added before the default tag for that level and are space
1262+separated.
1263+
1264+Failing silently when the message framework is disabled
1265+-------------------------------------------------------
1266+
1267+If you're writing a reusable app (or other piece of code) and want to include
1268+messaging functionality, but don't want to require your users to enable it
1269+if they don't want to, you may pass an additional keyword argument
1270+``fail_silently=True`` to any of the ``add_message`` family of methods. For
1271+example::
1272+
1273+    messages.add_message(request, messages.SUCCESS, 'Profile details updated.',
1274+                         fail_silently=True)
1275+    messages.info(request, 'Hello world.', fail_silently=True)
1276+
1277+Internally, Django uses this functionality in the create, update, and delete
1278+:ref:`generic views <topics-generic-views>` so that they work even if the
1279+message framework is disabled.
1280+
1281+Expiration of messages
1282+======================
1283+
1284+The messages are marked to be cleared when the storage instance is iterated
1285+(and cleared when the response is processed).
1286+
1287+To avoid the messages being cleared, you can set the messages storage to
1288+``False`` after iterating::
1289+
1290+    storage = messages.get_messages(request)
1291+    for message in storage:
1292+        do_something_with(message)
1293+    storage.used = False
1294+
1295+Behavior of parallel requests
1296+=============================
1297+
1298+Due to the way cookies (and hence sessions) work, **the behavior of any
1299+backends that make use of cookies or sessions is undefined when the same
1300+client makes multiple requests that set or get messages in parallel**.  For
1301+example, if a client initiates a request that creates a message in one window
1302+(or tab) and then another that fetches any uniterated messages in another
1303+window, before the first window redirects, the message may appear in the
1304+second window instead of the first window where it may be expected.
1305+
1306+In short, when multiple simultaneous requests from the same client are
1307+involved, messages are not guaranteed to be delivered to the same window that
1308+created them nor, in some cases, at all.  Note that this is typically not a
1309+problem in most applications and will become a non-issue in HTML5, where each
1310+window/tab will have its own browsing context.
1311+
1312+Settings
1313+========
1314+
1315+A few :ref:`Django settings <ref-settings>` give you control over message behavior:
1316+
1317+MESSAGE_LEVEL
1318+-------------
1319+
1320+Default: ``messages.INFO``
1321+
1322+Important: If you override this in your settings file and rely on any of the built-in
1323+constants, you must import the constants module directly to avoid the potential
1324+for circular imports.
1325+
1326+This sets the minimum message that will be saved in the message storage.  See
1327+`Message levels`_ above for more details.
1328+
1329+MESSAGE_STORAGE
1330+---------------
1331+
1332+Default: ``'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'``
1333+
1334+Controls where Django stores message data. Valid values are:
1335+
1336+    * ``'django.contrib.messages.storage.fallback.FallbackStorage'``
1337+    * ``'django.contrib.messages.storage.session.SessionStorage'``
1338+    * ``'django.contrib.messages.storage.cookie.CookieStorage'``
1339+    * ``'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'``
1340+
1341+See `Storage backends`_ for more details.
1342+
1343+MESSAGE_TAGS
1344+------------
1345+
1346+Default::
1347+
1348+        {messages.DEBUG: 'debug',
1349+        messages.INFO: 'info',
1350+        messages.SUCCESS: 'success',
1351+        messages.WARNING: 'warning',
1352+        messages.ERROR: 'error',}
1353+
1354+Important: If you override this in your settings file and rely on any of the built-in
1355+constants, you must import the constants module directly to avoid the potential
1356+for circular imports.
1357+
1358+This sets the mapping of message level to message tag, which is typically
1359+rendered as a CSS class in HTML. If you specify a value, it will extend
1360+the default. This means you only have to specify those values which you need
1361+to override. See `Displaying messages`_ above for more details.
1362+
1363+.. _Django settings: ../settings/
1364diff -r 1fb6476dce9d docs/ref/middleware.txt
1365--- a/docs/ref/middleware.txt   Wed Dec 02 00:30:00 2009 +0000
1366+++ b/docs/ref/middleware.txt   Thu Dec 03 01:33:20 2009 -0500
1367@@ -139,6 +139,17 @@
1368 content for each user. See the :ref:`internationalization documentation
1369 <topics-i18n>`.
1370 
1371+Message middleware
1372+------------------
1373+
1374+.. module:: django.contrib.messages.middleware
1375+   :synopsis: Message middleware.
1376+
1377+.. class:: django.contrib.messages.middleware.MessageMiddleware
1378+
1379+Enables cookie- and session-based message support. See the
1380+:ref:`messages documentation <ref-contrib-messages>`.
1381+
1382 Session middleware
1383 ------------------
1384 
1385diff -r 1fb6476dce9d docs/ref/settings.txt
1386--- a/docs/ref/settings.txt     Wed Dec 02 00:30:00 2009 +0000
1387+++ b/docs/ref/settings.txt     Thu Dec 03 01:33:20 2009 -0500
1388@@ -812,6 +812,43 @@
1389 
1390 .. setting:: MIDDLEWARE_CLASSES
1391 
1392+MESSAGE_LEVEL
1393+-------------
1394+
1395+.. versionadded:: 1.2
1396+
1397+Default: `messages.INFO`
1398+
1399+Sets the minimum message level that will be recorded by the messages
1400+framework. See the :ref:`messages documentation <ref-contrib-messages>` for
1401+more details.
1402+
1403+MESSAGE_STORAGE
1404+---------------
1405+
1406+.. versionadded:: 1.2
1407+
1408+Default: ``'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'``
1409+
1410+Controls where Django stores message data.  See the
1411+:ref:`messages documentation <ref-contrib-messages>` for more details.
1412+
1413+MESSAGE_TAGS
1414+------------
1415+
1416+.. versionadded:: 1.2
1417+
1418+Default::
1419+
1420+        {messages.DEBUG: 'debug',
1421+        messages.INFO: 'info',
1422+        messages.SUCCESS: 'success',
1423+        messages.WARNING: 'warning',
1424+        messages.ERROR: 'error',}
1425+
1426+Sets the mapping of message levels to message tags. See the
1427+:ref:`messages documentation <ref-contrib-messages>` for more details.
1428+
1429 MIDDLEWARE_CLASSES
1430 ------------------
1431 
1432@@ -820,7 +857,8 @@
1433     ('django.middleware.common.CommonMiddleware',
1434      'django.contrib.sessions.middleware.SessionMiddleware',
1435      'django.middleware.csrf.CsrfViewMiddleware',
1436-     'django.contrib.auth.middleware.AuthenticationMiddleware',)
1437+     'django.contrib.auth.middleware.AuthenticationMiddleware',
1438+     'django.contrib.messages.middleware.MessageMiddleware',)
1439 
1440 A tuple of middleware classes to use. See :ref:`topics-http-middleware`.
1441 
1442@@ -1059,7 +1097,8 @@
1443     ("django.core.context_processors.auth",
1444     "django.core.context_processors.debug",
1445     "django.core.context_processors.i18n",
1446-    "django.core.context_processors.media")
1447+    "django.core.context_processors.media",
1448+    "django.contrib.messages.context_processors.messages")
1449 
1450 A tuple of callables that are used to populate the context in ``RequestContext``.
1451 These callables take a request object as their argument and return a dictionary
1452diff -r 1fb6476dce9d docs/ref/templates/api.txt
1453--- a/docs/ref/templates/api.txt        Wed Dec 02 00:30:00 2009 +0000
1454+++ b/docs/ref/templates/api.txt        Thu Dec 03 01:33:20 2009 -0500
1455@@ -311,7 +311,8 @@
1456     ("django.core.context_processors.auth",
1457     "django.core.context_processors.debug",
1458     "django.core.context_processors.i18n",
1459-    "django.core.context_processors.media")
1460+    "django.core.context_processors.media",
1461+    "django.contrib.messages.context_processors.messages")
1462 
1463 .. versionadded:: 1.2
1464    In addition to these, ``RequestContext`` always uses
1465@@ -320,6 +321,10 @@
1466    in case of accidental misconfiguration, it is deliberately hardcoded in and
1467    cannot be turned off by the :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting.
1468 
1469+.. versionadded:: 1.2
1470+   The ``'messages'`` context processor was added.  For more information, see
1471+   the :ref:`messages documentation <ref-contrib-messages>`.
1472+
1473 Each processor is applied in order. That means, if one processor adds a
1474 variable to the context and a second processor adds a variable with the same
1475 name, the second will override the first. The default processors are explained
1476@@ -365,17 +370,18 @@
1477       logged-in user (or an ``AnonymousUser`` instance, if the client isn't
1478       logged in).
1479 
1480-    * ``messages`` -- A list of messages (as strings) for the currently
1481-      logged-in user. Behind the scenes, this calls
1482-      ``request.user.get_and_delete_messages()`` for every request. That method
1483-      collects the user's messages and deletes them from the database.
1484-
1485-      Note that messages are set with ``user.message_set.create``.
1486+    * ``messages`` -- A list of messages (as strings) that have been set
1487+      via the :ref:`messages framework <ref-contrib-messages>`.
1488 
1489     * ``perms`` -- An instance of
1490       ``django.core.context_processors.PermWrapper``, representing the
1491       permissions that the currently logged-in user has.
1492 
1493+.. versionchanged:: 1.2
1494+   Prior to version 1.2, the ``messages`` variable was a lazy accessor for
1495+   ``user.get_and_delete_messages()``. It has been changed to include any
1496+   messages added via the :ref:`messages framework <ref-contrib-messages`.
1497+
1498 django.core.context_processors.debug
1499 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1500 
1501@@ -427,6 +433,25 @@
1502 :class:`~django.http.HttpRequest`. Note that this processor is not enabled by default;
1503 you'll have to activate it.
1504 
1505+django.contrib.messages.context_processors.messages
1506+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1507+
1508+If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every
1509+``RequestContext`` will contain a single additional variable:
1510+
1511+    * ``messages`` -- A list of messages (as strings) that have been set
1512+      via the user model (using ``user.message_set.create``) or through
1513+      the :ref:`messages framework <ref-contrib-messages>`.
1514+
1515+.. versionadded:: 1.2
1516+   This template context variable was previously supplied by the ``'auth'``
1517+   context processor.  For backwards compatibility the ``'auth'`` context
1518+   processor will continue to supply the ``messages`` variable until Django
1519+   1.4.  If you use the ``messages`` variable, your project will work with
1520+   either (or both) context processors, but it is recommended to add
1521+   ``django.contrib.messages.context_processors.messages`` so your project
1522+   will be prepared for the future upgrade.
1523+
1524 Writing your own context processors
1525 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1526 
1527diff -r 1fb6476dce9d docs/releases/1.2.txt
1528--- a/docs/releases/1.2.txt     Wed Dec 02 00:30:00 2009 +0000
1529+++ b/docs/releases/1.2.txt     Thu Dec 03 01:33:20 2009 -0500
1530@@ -72,7 +72,37 @@
1531 Features deprecated in 1.2
1532 ==========================
1533 
1534-None.
1535+User Messages API
1536+-----------------
1537+
1538+The API for storing messages in the user ``Message`` model (via
1539+``user.message_set.create``) is now deprecated and will be removed in Django
1540+1.4 according to the standard :ref:`release process <internals-release-process>`.
1541+
1542+To upgrade your code, you need to replace any instances of::
1543+
1544+    user.message_set.create('a message')
1545+
1546+with the following::
1547+
1548+    from django.contrib import messages
1549+    messages.add_message(request, messages.INFO, 'a message')
1550+
1551+Additionally, if you make use of the method, you need to replace the
1552+following::
1553+
1554+    for message in user.get_and_delete_messages():
1555+        ...
1556+   
1557+with::
1558+
1559+    from django.contrib import messages
1560+    for message in messages.get_messages(request):
1561+        ...
1562+   
1563+For more information, see the full
1564+:ref:`messages documentation <ref-contrib-messages>`. You should begin to
1565+update your code to use the new API immediately.
1566 
1567 What's new in Django 1.2
1568 ========================
1569@@ -107,3 +137,12 @@
1570 :ref:`memory<topic-email-memory-backend>` - you can even configure all
1571 email to be :ref:`thrown away<topic-email-console-backend>`.
1572 
1573+Messages Framework
1574+-------------------------
1575+
1576+Django now includes a robust and configurable
1577+:ref:`messages framework <ref-contrib-messages>` with built-in support for
1578+cookie- and session-based messaging, for both anonymous and authenticated
1579+clients. The messages framework replaces the deprecated user message API and
1580+allows you to temporarily store messages in one request and retrieve them for
1581+display in a subsequent request (usually the next one).
1582diff -r 1fb6476dce9d docs/topics/auth.txt
1583--- a/docs/topics/auth.txt      Wed Dec 02 00:30:00 2009 +0000
1584+++ b/docs/topics/auth.txt      Thu Dec 03 01:33:20 2009 -0500
1585@@ -23,6 +23,9 @@
1586       user.
1587     * Messages: A simple way to queue messages for given users.
1588 
1589+.. deprecated:: 1.2
1590+   The Messages component of the auth system will be removed in Django 1.4.
1591+
1592 Installation
1593 ============
1594 
1595@@ -1289,6 +1292,11 @@
1596 Messages
1597 ========
1598 
1599+.. deprecated:: 1.2
1600+   This functionality will be removed in Django 1.4.  It is recommended to use
1601+   the :ref:`messages framework <ref-contrib-messages>` for new projects
1602+   whenever possible.
1603+
1604 The message system is a lightweight way to queue messages for given users.
1605 
1606 A message is associated with a :class:`~django.contrib.auth.models.User`.
1607@@ -1334,13 +1342,16 @@
1608     </ul>
1609     {% endif %}
1610 
1611-Note that :class:`~django.template.context.RequestContext` calls
1612-:meth:`~django.contrib.auth.models.User.get_and_delete_messages` behind the
1613-scenes, so any messages will be deleted even if you don't display them.
1614+.. versionchanged:: 1.2
1615+   The ``messages`` template variable uses a backwards compatible method in the
1616+   :ref:`messages framework <ref-contrib-messages>` to retrieve messages from
1617+   both the user ``Message`` model and from the new framework.  Unlike in
1618+   previous revisions, the messages will not be erased unless they are actually
1619+   displayed.
1620 
1621 Finally, note that this messages framework only works with users in the user
1622 database. To send messages to anonymous users, use the
1623-:ref:`session framework <topics-http-sessions>`.
1624+:ref:`messages framework <ref-contrib-messages>`.
1625 
1626 .. _authentication-backends:
1627 
1628diff -r 1fb6476dce9d tests/regressiontests/message_tests/models.py
1629--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
1630+++ b/tests/regressiontests/message_tests/models.py     Thu Dec 03 01:33:20 2009 -0500
1631@@ -0,0 +1,1 @@
1632+# empty models so tests are discovered
1633\ No newline at end of file
1634diff -r 1fb6476dce9d tests/regressiontests/message_tests/templates/message_tests/show.html
1635--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
1636+++ b/tests/regressiontests/message_tests/templates/message_tests/show.html     Thu Dec 03 01:33:20 2009 -0500
1637@@ -0,0 +1,7 @@
1638+{% if messages %}
1639+<ul class="messages">
1640+        {% for message in messages %}
1641+        <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
1642+        {% endfor %}
1643+</ul>
1644+{% endif %}
1645\ No newline at end of file
1646diff -r 1fb6476dce9d tests/regressiontests/message_tests/tests/__init__.py
1647--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
1648+++ b/tests/regressiontests/message_tests/tests/__init__.py     Thu Dec 03 01:33:20 2009 -0500
1649@@ -0,0 +1,6 @@
1650+from regressiontests.message_tests.tests.cookie import CookieTest
1651+from regressiontests.message_tests.tests.fallback import FallbackTest
1652+from regressiontests.message_tests.tests.middleware import MiddlewareTest
1653+from regressiontests.message_tests.tests.session import SessionTest
1654+from regressiontests.message_tests.tests.user_messages import UserMessagesTest, \
1655+    LegacyFallbackTest
1656diff -r 1fb6476dce9d tests/regressiontests/message_tests/tests/base.py
1657--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
1658+++ b/tests/regressiontests/message_tests/tests/base.py Thu Dec 03 01:33:20 2009 -0500
1659@@ -0,0 +1,372 @@
1660+from django import http
1661+from django.test import TestCase
1662+from django.conf import settings
1663+from django.utils.translation import ugettext_lazy
1664+from django.contrib.messages import constants, utils
1665+from django.contrib.messages.storage import Storage, base
1666+from django.contrib.messages.storage.base import Message
1667+from django.core.urlresolvers import reverse
1668+from django.contrib.auth.models import User
1669+from django.contrib.messages.api import AddMessageFailure
1670+
1671+
1672+def add_level_messages(storage):
1673+    """
1674+    Adds 6 messages from different levels (including a custom one) to a storage
1675+    instance.
1676+    """
1677+    storage.add(constants.INFO, 'A generic info message')
1678+    storage.add(29, 'Some custom level')
1679+    storage.add(constants.DEBUG, 'A debugging message', extra_tags='extra-tag')
1680+    storage.add(constants.WARNING, 'A warning')
1681+    storage.add(constants.ERROR, 'An error')
1682+    storage.add(constants.SUCCESS, 'This was a triumph.')
1683+
1684+
1685+class BaseTest(TestCase):
1686+    storage_class = Storage
1687+    restore_settings = ['MESSAGE_LEVEL', 'MESSAGE_TAGS']
1688+    urls = 'regressiontests.message_tests.urls'
1689+    levels = {
1690+        'debug': constants.DEBUG,
1691+        'info': constants.INFO,
1692+        'success': constants.SUCCESS,
1693+        'warning': constants.WARNING,
1694+        'error': constants.ERROR,
1695+    }
1696+    def setUp(self):
1697+        self._remembered_settings = {}
1698+        for setting in self.restore_settings:
1699+            if hasattr(settings, setting):
1700+                self._remembered_settings[setting] = getattr(settings, setting)
1701+                delattr(settings._wrapped, setting)
1702+        # backup these manually because we do not want them deleted
1703+        self._middleware_classes = settings.MIDDLEWARE_CLASSES
1704+        self._template_context_processors = settings.TEMPLATE_CONTEXT_PROCESSORS
1705+        self._installed_apps = settings.INSTALLED_APPS
1706+       
1707+    def tearDown(self):
1708+        for setting in self.restore_settings:
1709+            self.restore_setting(setting)
1710+        # restore these manually (see above)
1711+        settings.MIDDLEWARE_CLASSES = self._middleware_classes
1712+        settings.TEMPLATE_CONTEXT_PROCESSORS = self._template_context_processors
1713+        settings.INSTALLED_APPS = self._installed_apps
1714+
1715+    def restore_setting(self, setting):
1716+        if setting in self._remembered_settings:
1717+            value = self._remembered_settings.pop(setting)
1718+            setattr(settings, setting, value)
1719+        elif hasattr(settings, setting):
1720+            delattr(settings._wrapped, setting)
1721+
1722+    def get_request(self):
1723+        return http.HttpRequest()
1724+
1725+    def get_response(self):
1726+        return http.HttpResponse()
1727+
1728+    def get_storage(self, data=None):
1729+        """
1730+        Returns the storage backend, setting its loaded data to the ``data``
1731+        argument.
1732+       
1733+        This method avoids the storage ``_get`` method from getting called so
1734+        that other parts of the storage backend can be tested independent of
1735+        the message retrieval logic.
1736+        """
1737+        storage = self.storage_class(self.get_request())
1738+        storage._loaded_data = data or []
1739+        return storage
1740+
1741+    def test_add(self):
1742+        storage = self.get_storage()
1743+        self.assertFalse(storage.added_new)
1744+        storage.add(constants.INFO, 'Test message 1')
1745+        self.assert_(storage.added_new)
1746+        storage.add(constants.INFO, 'Test message 2', extra_tags='tag')
1747+        self.assertEqual(len(storage), 2)
1748+
1749+    def test_add_lazy_translation(self):
1750+        storage = self.get_storage()
1751+        response = self.get_response()
1752+
1753+        storage.add(constants.INFO, ugettext_lazy('lazy message'))
1754+        storage.update(response)
1755+
1756+        storing = self.stored_messages_count(storage, response)
1757+        self.assertEqual(storing, 1)
1758+
1759+    def test_no_update(self):
1760+        storage = self.get_storage()
1761+        response = self.get_response()
1762+        storage.update(response)
1763+        storing = self.stored_messages_count(storage, response)
1764+        self.assertEqual(storing, 0)
1765+
1766+    def test_add_update(self):
1767+        storage = self.get_storage()
1768+        response = self.get_response()
1769+
1770+        storage.add(constants.INFO, 'Test message 1')
1771+        storage.add(constants.INFO, 'Test message 1', extra_tags='tag')
1772+        storage.update(response)
1773+
1774+        storing = self.stored_messages_count(storage, response)
1775+        self.assertEqual(storing, 2)
1776+
1777+    def test_existing_add_read_update(self):
1778+        storage = self.get_existing_storage()
1779+        response = self.get_response()
1780+
1781+        storage.add(constants.INFO, 'Test message 3')
1782+        list(storage)   # Simulates a read
1783+        storage.update(response)
1784+
1785+        storing = self.stored_messages_count(storage, response)
1786+        self.assertEqual(storing, 0)
1787+
1788+    def test_existing_read_add_update(self):
1789+        storage = self.get_existing_storage()
1790+        response = self.get_response()
1791+
1792+        list(storage)   # Simulates a read
1793+        storage.add(constants.INFO, 'Test message 3')
1794+        storage.update(response)
1795+
1796+        storing = self.stored_messages_count(storage, response)
1797+        self.assertEqual(storing, 1)
1798+
1799+    def test_full_request_response_cycle(self):
1800+        """
1801+        With the message middleware enabled, tests that messages are properly
1802+        stored and then retrieved across the full request/redirect/response
1803+        cycle.
1804+        """
1805+        settings.MESSAGE_LEVEL = constants.DEBUG
1806+        data = {
1807+            'messages': ['Test message %d' % x for x in xrange(10)],
1808+        }
1809+        show_url = reverse('regressiontests.message_tests.views.show')
1810+        for level in ('debug', 'info', 'success', 'warning', 'error'):
1811+            add_url = reverse('regressiontests.message_tests.views.add',
1812+                              args=(level,))
1813+            response = self.client.post(add_url, data, follow=True)
1814+            self.assertRedirects(response, show_url)
1815+            self.assertTrue('messages' in response.context)
1816+            messages = [
1817+                Message(self.levels[level], msg) for msg in data['messages']
1818+            ]
1819+            self.assertEqual(list(response.context['messages']), messages)
1820+            for msg in data['messages']:
1821+                self.assertContains(response, msg)
1822+
1823+    def test_multiple_posts(self):
1824+        """
1825+        Tests that messages persist properly when multiple POSTs are made
1826+        before a GET.
1827+        """
1828+        settings.MESSAGE_LEVEL = constants.DEBUG
1829+        data = {
1830+            'messages': ['Test message %d' % x for x in xrange(10)],
1831+        }
1832+        show_url = reverse('regressiontests.message_tests.views.show')
1833+        messages = []
1834+        for level in ('debug', 'info', 'success', 'warning', 'error'):
1835+            messages.extend(
1836+                [Message(self.levels[level], msg) for msg in data['messages']]
1837+            )
1838+            add_url = reverse('regressiontests.message_tests.views.add',
1839+                              args=(level,))
1840+            self.client.post(add_url, data)
1841+        response = self.client.get(show_url)
1842+        self.assertTrue('messages' in response.context)
1843+        self.assertEqual(list(response.context['messages']), messages)
1844+        for msg in data['messages']:
1845+            self.assertContains(response, msg)
1846+   
1847+    def test_middleware_disabled_auth_user(self):
1848+        """
1849+        Tests that the messages API successfully falls back to using
1850+        user.message_set to store messages directly when the middleware is
1851+        disabled.
1852+        """
1853+        settings.MESSAGE_LEVEL = constants.DEBUG
1854+        user = User.objects.create_user('test', 'test@example.com', 'test')
1855+        self.client.login(username='test', password='test')
1856+        settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
1857+        settings.INSTALLED_APPS.remove(
1858+            'django.contrib.messages',
1859+        )
1860+        settings.MIDDLEWARE_CLASSES = list(settings.MIDDLEWARE_CLASSES)
1861+        settings.MIDDLEWARE_CLASSES.remove(
1862+            'django.contrib.messages.middleware.MessageMiddleware',
1863+        )
1864+        settings.TEMPLATE_CONTEXT_PROCESSORS = \
1865+          list(settings.TEMPLATE_CONTEXT_PROCESSORS)
1866+        settings.TEMPLATE_CONTEXT_PROCESSORS.remove(
1867+            'django.contrib.messages.context_processors.messages',
1868+        )
1869+        data = {
1870+            'messages': ['Test message %d' % x for x in xrange(10)],
1871+        }
1872+        show_url = reverse('regressiontests.message_tests.views.show')
1873+        for level in ('debug', 'info', 'success', 'warning', 'error'):
1874+            add_url = reverse('regressiontests.message_tests.views.add',
1875+                              args=(level,))
1876+            response = self.client.post(add_url, data, follow=True)
1877+            self.assertRedirects(response, show_url)
1878+            self.assertTrue('messages' in response.context)
1879+            self.assertEqual(list(response.context['messages']), data['messages'])
1880+            for msg in data['messages']:
1881+                self.assertContains(response, msg)
1882+               
1883+    def test_middleware_disabled_anon_user(self):
1884+        """
1885+        Tests that, when the middleware is disabled and a user is not logged
1886+        in, an exception is raised when one attempts to store a message.
1887+        """
1888+        settings.MESSAGE_LEVEL = constants.DEBUG
1889+        settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
1890+        settings.INSTALLED_APPS.remove(
1891+            'django.contrib.messages',
1892+        )
1893+        settings.MIDDLEWARE_CLASSES = list(settings.MIDDLEWARE_CLASSES)
1894+        settings.MIDDLEWARE_CLASSES.remove(
1895+            'django.contrib.messages.middleware.MessageMiddleware',
1896+        )
1897+        settings.TEMPLATE_CONTEXT_PROCESSORS = \
1898+          list(settings.TEMPLATE_CONTEXT_PROCESSORS)
1899+        settings.TEMPLATE_CONTEXT_PROCESSORS.remove(
1900+            'django.contrib.messages.context_processors.messages',
1901+        )
1902+        data = {
1903+            'messages': ['Test message %d' % x for x in xrange(10)],
1904+        }
1905+        show_url = reverse('regressiontests.message_tests.views.show')
1906+        for level in ('debug', 'info', 'success', 'warning', 'error'):
1907+            add_url = reverse('regressiontests.message_tests.views.add',
1908+                              args=(level,))
1909+            self.assertRaises(AddMessageFailure, self.client.post, add_url, data, follow=True)
1910+
1911+    def test_middleware_disabled_anon_user_fail_silently(self):
1912+        """
1913+        Tests that, when the middleware is disabled and a user is not logged
1914+        in, an exception is raised when one attempts to store a message.
1915+        """
1916+        settings.MESSAGE_LEVEL = constants.DEBUG
1917+        settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
1918+        settings.INSTALLED_APPS.remove(
1919+            'django.contrib.messages',
1920+        )
1921+        settings.MIDDLEWARE_CLASSES = list(settings.MIDDLEWARE_CLASSES)
1922+        settings.MIDDLEWARE_CLASSES.remove(
1923+            'django.contrib.messages.middleware.MessageMiddleware',
1924+        )
1925+        settings.TEMPLATE_CONTEXT_PROCESSORS = \
1926+          list(settings.TEMPLATE_CONTEXT_PROCESSORS)
1927+        settings.TEMPLATE_CONTEXT_PROCESSORS.remove(
1928+            'django.contrib.messages.context_processors.messages',
1929+        )
1930+        data = {
1931+            'messages': ['Test message %d' % x for x in xrange(10)],
1932+            'fail_silently': True,
1933+        }
1934+        show_url = reverse('regressiontests.message_tests.views.show')
1935+        for level in ('debug', 'info', 'success', 'warning', 'error'):
1936+            add_url = reverse('regressiontests.message_tests.views.add',
1937+                              args=(level,))
1938+            response = self.client.post(add_url, data, follow=True)
1939+            self.assertRedirects(response, show_url)
1940+            self.assertTrue('messages' in response.context)
1941+            self.assertEqual(list(response.context['messages']), [])
1942+
1943+    def stored_messages_count(self, storage, response):
1944+        """
1945+        Returns the number of messages being stored after a
1946+        ``storage.update()`` call.
1947+        """
1948+        raise NotImplementedError('This method must be set by a subclass.')
1949+
1950+    def test_get(self):
1951+        raise NotImplementedError('This method must be set by a subclass.')
1952+
1953+    def get_existing_storage(self):
1954+        return self.get_storage([Message(constants.INFO, 'Test message 1'),
1955+                                 Message(constants.INFO, 'Test message 2',
1956+                                              extra_tags='tag')])
1957+
1958+    def test_existing_read(self):
1959+        """
1960+        Tests that reading the existing storage doesn't cause the data to be
1961+        lost.
1962+        """
1963+        storage = self.get_existing_storage()
1964+        self.assertFalse(storage.used)
1965+        # After iterating the storage engine directly, the used flag is set.
1966+        data = list(storage)
1967+        self.assert_(storage.used)
1968+        # The data does not disappear because it has been iterated.
1969+        self.assertEqual(data, list(storage))
1970+
1971+    def test_existing_add(self):
1972+        storage = self.get_existing_storage()
1973+        self.assertFalse(storage.added_new)
1974+        storage.add(constants.INFO, 'Test message 3')
1975+        self.assert_(storage.added_new)
1976+
1977+    def test_default_level(self):
1978+        storage = self.get_storage()
1979+        add_level_messages(storage)
1980+        self.assertEqual(len(storage), 5)
1981+
1982+    def test_low_level(self):
1983+        storage = self.get_storage()
1984+        storage.level = 5
1985+        add_level_messages(storage)
1986+        self.assertEqual(len(storage), 6)
1987+
1988+    def test_high_level(self):
1989+        storage = self.get_storage()
1990+        storage.level = 30
1991+        add_level_messages(storage)
1992+        self.assertEqual(len(storage), 2)
1993+
1994+    def test_settings_level(self):
1995+        settings.MESSAGE_LEVEL = 29
1996+        storage = self.get_storage()
1997+        add_level_messages(storage)
1998+        self.assertEqual(len(storage), 3)
1999+
2000+    def test_tags(self):
2001+        storage = self.get_storage()
2002+        storage.level = 0
2003+        add_level_messages(storage)
2004+        tags = [msg.tags for msg in storage]
2005+        self.assertEqual(tags,
2006+                         ['info', '', 'extra-tag debug', 'warning', 'error',
2007+                          'success'])
2008+
2009+    def test_custom_tags(self):
2010+        settings.MESSAGE_TAGS = {
2011+            constants.INFO: 'info',
2012+            constants.DEBUG: '',
2013+            constants.WARNING: '',
2014+            constants.ERROR: 'bad',
2015+            29: 'custom',
2016+        }
2017+        # LEVEL_TAGS is a constant defined in the
2018+        # django.contrib.messages.storage.base module, so after changing
2019+        # settings.MESSAGE_TAGS, we need to update that constant too.
2020+        base.LEVEL_TAGS = utils.get_level_tags()
2021+        try:
2022+            storage = self.get_storage()
2023+            storage.level = 0
2024+            add_level_messages(storage)
2025+            tags = [msg.tags for msg in storage]
2026+            self.assertEqual(tags,
2027+                         ['info', 'custom', 'extra-tag', '', 'bad', 'success'])
2028+        finally:
2029+            # Ensure the level tags constant is put back like we found it.
2030+            self.restore_setting('MESSAGE_TAGS')
2031+            base.LEVEL_TAGS = utils.get_level_tags()
2032diff -r 1fb6476dce9d tests/regressiontests/message_tests/tests/cookie.py
2033--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
2034+++ b/tests/regressiontests/message_tests/tests/cookie.py       Thu Dec 03 01:33:20 2009 -0500
2035@@ -0,0 +1,99 @@
2036+from django.contrib.messages import constants
2037+from regressiontests.message_tests.tests.base import BaseTest
2038+from django.contrib.messages.storage.cookie import CookieStorage, \
2039+                                            MessageEncoder, MessageDecoder
2040+from django.contrib.messages.storage.base import Message
2041+from django.utils import simplejson as json
2042+
2043+def set_cookie_data(storage, messages, invalid=False, encode_empty=False):
2044+    """
2045+    Sets ``request.COOKIES`` with the encoded data and removes the storage
2046+    backend's loaded data cache.
2047+    """
2048+    encoded_data = storage._encode(messages, encode_empty=encode_empty)
2049+    if invalid:
2050+        # Truncate the first character so that the hash is invalid.
2051+        encoded_data = encoded_data[1:]
2052+    storage.request.COOKIES = {CookieStorage.cookie_name: encoded_data}
2053+    if hasattr(storage, '_loaded_data'):
2054+        del storage._loaded_data
2055+
2056+
2057+def stored_cookie_messages_count(storage, response):
2058+    """
2059+    Returns an integer containing the number of messages stored.
2060+    """
2061+    # Get a list of cookies, excluding ones with a max-age of 0 (because
2062+    # they have been marked for deletion).
2063+    cookie = response.cookies.get(storage.cookie_name)
2064+    if not cookie or cookie['max-age'] == 0:
2065+        return 0
2066+    data = storage._decode(cookie.value)
2067+    if not data:
2068+        return 0
2069+    if data[-1] == CookieStorage.not_finished:
2070+        data.pop()
2071+    return len(data)
2072+
2073+
2074+class CookieTest(BaseTest):
2075+    storage_class = CookieStorage
2076+
2077+    def stored_messages_count(self, storage, response):
2078+        return stored_cookie_messages_count(storage, response)
2079+
2080+    def test_get(self):
2081+        storage = self.storage_class(self.get_request())
2082+        # Set initial data.
2083+        example_messages = ['test', 'me']
2084+        set_cookie_data(storage, example_messages)
2085+        # Test that the message actually contains what we expect.
2086+        self.assertEqual(list(storage), example_messages)
2087+
2088+    def test_get_bad_cookie(self):
2089+        request = self.get_request()
2090+        storage = self.storage_class(request)
2091+        # Set initial (invalid) data.
2092+        example_messages = ['test', 'me']
2093+        set_cookie_data(storage, example_messages, invalid=True)
2094+        # Test that the message actually contains what we expect.
2095+        self.assertEqual(list(storage), [])
2096+
2097+    def test_max_cookie_length(self):
2098+        """
2099+        Tests that, if the data exceeds what is allowed in a cookie, older
2100+        messages are removed before saving (and returned by the ``update``
2101+        method).
2102+        """
2103+        storage = self.get_storage()
2104+        response = self.get_response()
2105+
2106+        for i in range(5):
2107+            storage.add(constants.INFO, str(i) * 900)
2108+        unstored_messages = storage.update(response)
2109+
2110+        cookie_storing = self.stored_messages_count(storage, response)
2111+        self.assertEqual(cookie_storing, 4)
2112+
2113+        self.assertEqual(len(unstored_messages), 1)
2114+        self.assert_(unstored_messages[0].message == '0' * 900)
2115+   
2116+    def test_json_encoder_decoder(self):
2117+        """
2118+        Tests that an complex nested data structure containing Message
2119+        instances is properly encoded/decoded by the custom JSON
2120+        encoder/decoder classes.
2121+        """
2122+        messages = [
2123+            {
2124+                'message': Message(constants.INFO, 'Test message'),
2125+                'message_list': [
2126+                    Message(constants.INFO, 'message %s') for x in xrange(5)
2127+                ] + [{'another-message': Message(constants.ERROR, 'error')}],
2128+            },
2129+            Message(constants.INFO, 'message %s'),
2130+        ]
2131+        encoder = MessageEncoder(separators=(',', ':'))
2132+        value = encoder.encode(messages)
2133+        decoded_messages = json.loads(value, cls=MessageDecoder)
2134+        self.assertEqual(messages, decoded_messages)
2135diff -r 1fb6476dce9d tests/regressiontests/message_tests/tests/fallback.py
2136--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
2137+++ b/tests/regressiontests/message_tests/tests/fallback.py     Thu Dec 03 01:33:20 2009 -0500
2138@@ -0,0 +1,173 @@
2139+from django.contrib.messages import constants
2140+from django.contrib.messages.storage.fallback import FallbackStorage, \
2141+    CookieStorage
2142+from regressiontests.message_tests.tests.base import BaseTest
2143+from regressiontests.message_tests.tests.cookie import set_cookie_data, \
2144+    stored_cookie_messages_count
2145+from regressiontests.message_tests.tests.session import set_session_data, \
2146+    stored_session_messages_count
2147+
2148+
2149+class FallbackTest(BaseTest):
2150+    storage_class = FallbackStorage
2151+
2152+    def get_request(self):
2153+        self.session = {}
2154+        request = super(FallbackTest, self).get_request()
2155+        request.session = self.session
2156+        return request
2157+
2158+    def get_cookie_storage(self, storage):
2159+        return storage.storages[-2]
2160+
2161+    def get_session_storage(self, storage):
2162+        return storage.storages[-1]
2163+
2164+    def stored_cookie_messages_count(self, storage, response):
2165+        return stored_cookie_messages_count(self.get_cookie_storage(storage),
2166+                                            response)
2167+
2168+    def stored_session_messages_count(self, storage, response):
2169+        return stored_session_messages_count(self.get_session_storage(storage))
2170+
2171+    def stored_messages_count(self, storage, response):
2172+        """
2173+        Return the storage totals from both cookie and session backends.
2174+        """
2175+        total = (self.stored_cookie_messages_count(storage, response) +
2176+                 self.stored_session_messages_count(storage, response))
2177+        return total
2178+
2179+    def test_get(self):
2180+        request = self.get_request()
2181+        storage = self.storage_class(request)
2182+        cookie_storage = self.get_cookie_storage(storage)
2183+
2184+        # Set initial cookie data.
2185+        example_messages = [str(i) for i in range(5)]
2186+        set_cookie_data(cookie_storage, example_messages)
2187+
2188+        # Overwrite the _get method of the fallback storage to prove it is not
2189+        # used (it would cause a TypeError: 'NoneType' object is not callable).
2190+        self.get_session_storage(storage)._get = None
2191+
2192+        # Test that the message actually contains what we expect.
2193+        self.assertEqual(list(storage), example_messages)
2194+
2195+    def test_get_empty(self):
2196+        request = self.get_request()
2197+        storage = self.storage_class(request)
2198+
2199+        # Overwrite the _get method of the fallback storage to prove it is not
2200+        # used (it would cause a TypeError: 'NoneType' object is not callable).
2201+        self.get_session_storage(storage)._get = None
2202+
2203+        # Test that the message actually contains what we expect.
2204+        self.assertEqual(list(storage), [])
2205+
2206+    def test_get_fallback(self):
2207+        request = self.get_request()
2208+        storage = self.storage_class(request)
2209+        cookie_storage = self.get_cookie_storage(storage)
2210+        session_storage = self.get_session_storage(storage)
2211+
2212+        # Set initial cookie and session data.
2213+        example_messages = [str(i) for i in range(5)]
2214+        set_cookie_data(cookie_storage, example_messages[:4] +
2215+                        [CookieStorage.not_finished])
2216+        set_session_data(session_storage, example_messages[4:])
2217+
2218+        # Test that the message actually contains what we expect.
2219+        self.assertEqual(list(storage), example_messages)
2220+
2221+    def test_get_fallback_only(self):
2222+        request = self.get_request()
2223+        storage = self.storage_class(request)
2224+        cookie_storage = self.get_cookie_storage(storage)
2225+        session_storage = self.get_session_storage(storage)
2226+
2227+        # Set initial cookie and session data.
2228+        example_messages = [str(i) for i in range(5)]
2229+        set_cookie_data(cookie_storage, [CookieStorage.not_finished],
2230+                        encode_empty=True)
2231+        set_session_data(session_storage, example_messages)
2232+
2233+        # Test that the message actually contains what we expect.
2234+        self.assertEqual(list(storage), example_messages)
2235+
2236+    def test_flush_used_backends(self):
2237+        request = self.get_request()
2238+        storage = self.storage_class(request)
2239+        cookie_storage = self.get_cookie_storage(storage)
2240+        session_storage = self.get_session_storage(storage)
2241+
2242+        # Set initial cookie and session data.
2243+        set_cookie_data(cookie_storage, ['cookie', CookieStorage.not_finished])
2244+        set_session_data(session_storage, ['session'])
2245+
2246+        # When updating, previously used but no longer needed backends are
2247+        # flushed.
2248+        response = self.get_response()
2249+        list(storage)
2250+        storage.update(response)
2251+        session_storing = self.stored_session_messages_count(storage, response)
2252+        self.assertEqual(session_storing, 0)
2253+
2254+    def test_no_fallback(self):
2255+        """
2256+        Confirms that:
2257+       
2258+        (1) A short number of messages whose data size doesn't exceed what is
2259+        allowed in a cookie will all be stored in the CookieBackend.
2260+       
2261+        (2) If the CookieBackend can store all messages, the SessionBackend
2262+        won't be written to at all.
2263+        """
2264+        storage = self.get_storage()
2265+        response = self.get_response()
2266+
2267+        # Overwrite the _store method of the fallback storage to prove it isn't
2268+        # used (it would cause a TypeError: 'NoneType' object is not callable).
2269+        self.get_session_storage(storage)._store = None
2270+
2271+        for i in range(5):
2272+            storage.add(constants.INFO, str(i) * 100)
2273+        storage.update(response)
2274+
2275+        cookie_storing = self.stored_cookie_messages_count(storage, response)
2276+        self.assertEqual(cookie_storing, 5)
2277+        session_storing = self.stored_session_messages_count(storage, response)
2278+        self.assertEqual(session_storing, 0)
2279+
2280+    def test_session_fallback(self):
2281+        """
2282+        Confirms that, if the data exceeds what is allowed in a cookie, older
2283+        messages which did not "fit" are stored in the SessionBackend.
2284+        """
2285+        storage = self.get_storage()
2286+        response = self.get_response()
2287+
2288+        for i in range(5):
2289+            storage.add(constants.INFO, str(i) * 900)
2290+        storage.update(response)
2291+
2292+        cookie_storing = self.stored_cookie_messages_count(storage, response)
2293+        self.assertEqual(cookie_storing, 4)
2294+        session_storing = self.stored_session_messages_count(storage, response)
2295+        self.assertEqual(session_storing, 1)
2296+
2297+    def test_session_fallback_only(self):
2298+        """
2299+        Confirms that large messages, none of which fit in a cookie, are stored
2300+        in the SessionBackend (and nothing is stored in the CookieBackend).
2301+        """
2302+        storage = self.get_storage()
2303+        response = self.get_response()
2304+
2305+        storage.add(constants.INFO, 'x' * 5000)
2306+        storage.update(response)
2307+
2308+        cookie_storing = self.stored_cookie_messages_count(storage, response)
2309+        self.assertEqual(cookie_storing, 0)
2310+        session_storing = self.stored_session_messages_count(storage, response)
2311+        self.assertEqual(session_storing, 1)
2312diff -r 1fb6476dce9d tests/regressiontests/message_tests/tests/middleware.py
2313--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
2314+++ b/tests/regressiontests/message_tests/tests/middleware.py   Thu Dec 03 01:33:20 2009 -0500
2315@@ -0,0 +1,18 @@
2316+import unittest
2317+from django import http
2318+from django.contrib.messages.middleware import MessageMiddleware
2319+
2320+
2321+class MiddlewareTest(unittest.TestCase):
2322+
2323+    def setUp(self):
2324+        self.middleware = MessageMiddleware()
2325+
2326+    def test_response_without_messages(self):
2327+        """
2328+        Makes sure that the response middleware is tolerant of messages not
2329+        existing on request.
2330+        """
2331+        request = http.HttpRequest()
2332+        response = http.HttpResponse()
2333+        self.middleware.process_response(request, response)
2334diff -r 1fb6476dce9d tests/regressiontests/message_tests/tests/session.py
2335--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
2336+++ b/tests/regressiontests/message_tests/tests/session.py      Thu Dec 03 01:33:20 2009 -0500
2337@@ -0,0 +1,38 @@
2338+from regressiontests.message_tests.tests.base import BaseTest
2339+from django.contrib.messages.storage.session import SessionStorage
2340+
2341+
2342+def set_session_data(storage, messages):
2343+    """
2344+    Sets the messages into the backend request's session and remove the
2345+    backend's loaded data cache.
2346+    """
2347+    storage.request.session[storage.session_key] = messages
2348+    if hasattr(storage, '_loaded_data'):
2349+        del storage._loaded_data
2350+
2351+
2352+def stored_session_messages_count(storage):
2353+    data = storage.request.session.get(storage.session_key, [])
2354+    return len(data)
2355+
2356+
2357+class SessionTest(BaseTest):
2358+    storage_class = SessionStorage
2359+
2360+    def get_request(self):
2361+        self.session = {}
2362+        request = super(SessionTest, self).get_request()
2363+        request.session = self.session
2364+        return request
2365+
2366+    def stored_messages_count(self, storage, response):
2367+        return stored_session_messages_count(storage)
2368+
2369+    def test_get(self):
2370+        storage = self.storage_class(self.get_request())
2371+        # Set initial data.
2372+        example_messages = ['test', 'me']
2373+        set_session_data(storage, example_messages)
2374+        # Test that the message actually contains what we expect.
2375+        self.assertEqual(list(storage), example_messages)
2376diff -r 1fb6476dce9d tests/regressiontests/message_tests/tests/user_messages.py
2377--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
2378+++ b/tests/regressiontests/message_tests/tests/user_messages.py        Thu Dec 03 01:33:20 2009 -0500
2379@@ -0,0 +1,65 @@
2380+from django import http
2381+from django.contrib.auth.models import User
2382+from django.contrib.messages.storage.user_messages import UserMessagesStorage,\
2383+    LegacyFallbackStorage
2384+from regressiontests.message_tests.tests.cookie import set_cookie_data
2385+from regressiontests.message_tests.tests.fallback import FallbackTest
2386+from django.test import TestCase
2387+
2388+
2389+class UserMessagesTest(TestCase):
2390+
2391+    def setUp(self):
2392+        self.user = User.objects.create(username='tester')
2393+   
2394+    def test_add(self):
2395+        storage = UserMessagesStorage(http.HttpRequest())
2396+        self.assertRaises(NotImplementedError, storage.add, 'Test message 1')
2397+
2398+    def test_get_anonymous(self):
2399+        # Ensure that the storage still works if no user is attached to the
2400+        # request.
2401+        storage = UserMessagesStorage(http.HttpRequest())
2402+        self.assertEqual(len(storage), 0)
2403+
2404+    def test_get(self):
2405+        storage = UserMessagesStorage(http.HttpRequest())
2406+        storage.request.user = self.user
2407+        self.user.message_set.create(message='test message')
2408+
2409+        self.assertEqual(len(storage), 1)
2410+        self.assertEqual(list(storage)[0].message, 'test message')
2411+
2412+
2413+class LegacyFallbackTest(FallbackTest, TestCase):
2414+    storage_class = LegacyFallbackStorage
2415+
2416+    def setUp(self):
2417+        super(LegacyFallbackTest, self).setUp()
2418+        self.user = User.objects.create(username='tester')
2419+       
2420+    def get_request(self, *args, **kwargs):
2421+        request = super(LegacyFallbackTest, self).get_request(*args, **kwargs)
2422+        request.user = self.user
2423+        return request
2424+
2425+    def test_get_legacy_only(self):
2426+        request = self.get_request()
2427+        storage = self.storage_class(request)
2428+        self.user.message_set.create(message='user message')
2429+
2430+        # Test that the message actually contains what we expect.
2431+        self.assertEqual(len(storage), 1)
2432+        self.assertEqual(list(storage)[0].message, 'user message')
2433+
2434+    def test_get_legacy(self):
2435+        request = self.get_request()
2436+        storage = self.storage_class(request)
2437+        cookie_storage = self.get_cookie_storage(storage)
2438+        self.user.message_set.create(message='user message')
2439+        set_cookie_data(cookie_storage, ['cookie'])
2440+
2441+        # Test that the message actually contains what we expect.
2442+        self.assertEqual(len(storage), 2)
2443+        self.assertEqual(list(storage)[0].message, 'user message')
2444+        self.assertEqual(list(storage)[1], 'cookie')
2445diff -r 1fb6476dce9d tests/regressiontests/message_tests/urls.py
2446--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
2447+++ b/tests/regressiontests/message_tests/urls.py       Thu Dec 03 01:33:20 2009 -0500
2448@@ -0,0 +1,6 @@
2449+from django.conf.urls.defaults import *
2450+
2451+urlpatterns = patterns('regressiontests.message_tests.views',
2452+    ('^add/(debug|info|success|warning|error)/$', 'add'),
2453+    ('^show/$', 'show'),
2454+)
2455diff -r 1fb6476dce9d tests/regressiontests/message_tests/views.py
2456--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
2457+++ b/tests/regressiontests/message_tests/views.py      Thu Dec 03 01:33:20 2009 -0500
2458@@ -0,0 +1,23 @@
2459+from django.contrib import messages
2460+from django.core.urlresolvers import reverse
2461+from django.http import HttpResponseRedirect
2462+from django.shortcuts import render_to_response
2463+from django.template import RequestContext
2464+
2465+def add(request, message_type):
2466+    # don't default to False here, because we want to test that it defaults
2467+    # to False if unspecified
2468+    fail_silently = request.POST.get('fail_silently', None)
2469+    for msg in request.POST.getlist('messages'):
2470+        if fail_silently is not None:
2471+            getattr(messages, message_type)(request, msg,
2472+                                            fail_silently=fail_silently)
2473+        else:
2474+            getattr(messages, message_type)(request, msg)
2475+    show_url = reverse('regressiontests.message_tests.views.show')
2476+    return HttpResponseRedirect(show_url)
2477+
2478+
2479+def show(request):
2480+    return render_to_response('message_tests/show.html', {},
2481+                              context_instance=RequestContext(request))
2482\ No newline at end of file
2483diff -r 1fb6476dce9d tests/runtests.py
2484--- a/tests/runtests.py Wed Dec 02 00:30:00 2009 +0000
2485+++ b/tests/runtests.py Thu Dec 03 01:33:20 2009 -0500
2486@@ -28,6 +28,7 @@
2487     'django.contrib.flatpages',
2488     'django.contrib.redirects',
2489     'django.contrib.sessions',
2490+    'django.contrib.messages',
2491     'django.contrib.comments',
2492     'django.contrib.admin',
2493 ]
2494@@ -107,6 +108,7 @@
2495     settings.MIDDLEWARE_CLASSES = (
2496         'django.contrib.sessions.middleware.SessionMiddleware',
2497         'django.contrib.auth.middleware.AuthenticationMiddleware',
2498+        'django.contrib.messages.middleware.MessageMiddleware',
2499         'django.middleware.common.CommonMiddleware',
2500     )
2501     settings.SITE_ID = 1