Ticket #4604: django-contrib-messages.diff

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

final patch from django-contrib-messages branch

  • AUTHORS

    diff -r 70e75e8cd224 AUTHORS
    a b  
    6060    Ned Batchelder <http://www.nedbatchelder.com/>
    6161    batiste@dosimple.ch
    6262    Batman
     63    Chris Beaven <http://smileychris.tactful.co.nz/>
    6364    Brian Beck <http://blog.brianbeck.com/>
    6465    Shannon -jj Behrens <http://jjinux.blogspot.com/>
    6566    Esdras Beleza <linux@esdrasbeleza.com>
     
    299300    Jason McBrayer <http://www.carcosa.net/jason/>
    300301    Kevin McConnell <kevin.mcconnell@gmail.com>
    301302    mccutchen@gmail.com
     303    Tobias McNulty <http://www.caktusgroup.com/blog>
    302304    Christian Metts
    303305    michael.mcewan@gmail.com
    304306    michal@plovarna.cz
     
    391393    Jozko Skrablin <jozko.skrablin@gmail.com>
    392394    Ben Slavin <benjamin.slavin@gmail.com>
    393395    sloonz <simon.lipp@insa-lyon.fr>
    394     SmileyChris <smileychris@gmail.com>
    395396    Warren Smith <warren@wandrsmith.net>
    396397    smurf@smurf.noris.de
    397398    Vsevolod Solovyov
  • django/conf/global_settings.py

    diff -r 70e75e8cd224 django/conf/global_settings.py
    a b  
    172172    'django.core.context_processors.i18n',
    173173    'django.core.context_processors.media',
    174174#    'django.core.context_processors.request',
     175    'django.contrib.messages.context_processors.messages',
    175176)
    176177
    177178# Output to use in template system for invalid (e.g. misspelled) variables.
     
    308309    'django.contrib.sessions.middleware.SessionMiddleware',
    309310    'django.middleware.csrf.CsrfViewMiddleware',
    310311    'django.contrib.auth.middleware.AuthenticationMiddleware',
     312    'django.contrib.messages.middleware.MessageMiddleware',
    311313#     'django.middleware.http.ConditionalGetMiddleware',
    312314#     'django.middleware.gzip.GZipMiddleware',
    313315)
     
    393395CSRF_COOKIE_NAME = 'csrftoken'
    394396CSRF_COOKIE_DOMAIN = None
    395397
     398############
     399# MESSAGES #
     400############
     401
     402# Class to use as messges backend
     403MESSAGE_STORAGE = 'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'
     404
     405# Default values of MESSAGE_LEVEL and MESSAGE_TAGS are defined within
     406# django.contrib.messages to avoid imports in this settings file.
     407
    396408###########
    397409# TESTING #
    398410###########
  • django/conf/project_template/settings.py

    diff -r 70e75e8cd224 django/conf/project_template/settings.py
    a b  
    6262    'django.contrib.sessions.middleware.SessionMiddleware',
    6363    'django.middleware.csrf.CsrfViewMiddleware',
    6464    'django.contrib.auth.middleware.AuthenticationMiddleware',
     65    'django.contrib.messages.middleware.MessageMiddleware',
    6566)
    6667
    6768ROOT_URLCONF = '{{ project_name }}.urls'
     
    7778    'django.contrib.contenttypes',
    7879    'django.contrib.sessions',
    7980    'django.contrib.sites',
     81    'django.contrib.messages',
    8082)
  • django/contrib/admin/options.py

    diff -r 70e75e8cd224 django/contrib/admin/options.py
    a b  
    66from django.contrib.admin import widgets
    77from django.contrib.admin import helpers
    88from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_ngettext, model_format_dict
     9from django.contrib import messages
    910from django.views.decorators.csrf import csrf_protect
    1011from django.core.exceptions import PermissionDenied
    1112from django.db import models, transaction
     
    541542    def message_user(self, request, message):
    542543        """
    543544        Send a message to the user. The default implementation
    544         posts a message using the auth Message object.
     545        posts a message using the django.contrib.messages backend.
    545546        """
    546         request.user.message_set.create(message=message)
     547        messages.info(request, message)
    547548
    548549    def save_form(self, request, form, change):
    549550        """
  • django/contrib/admin/views/template.py

    diff -r 70e75e8cd224 django/contrib/admin/views/template.py
    a b  
    66from django.conf import settings
    77from django.utils.importlib import import_module
    88from django.utils.translation import ugettext_lazy as _
     9from django.contrib import messages
    910
    1011
    1112def template_validator(request):
     
    2324        form = TemplateValidatorForm(settings_modules, site_list,
    2425                                     data=request.POST)
    2526        if form.is_valid():
    26             request.user.message_set.create(message='The template is valid.')
     27            messages.info(request, 'The template is valid.')
    2728    else:
    2829        form = TemplateValidatorForm(settings_modules, site_list)
    2930    return render_to_response('admin/template_validator.html', {
  • django/contrib/auth/admin.py

    diff -r 70e75e8cd224 django/contrib/auth/admin.py
    a b  
    33from django.contrib import admin
    44from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AdminPasswordChangeForm
    55from django.contrib.auth.models import User, Group
     6from django.contrib import messages
    67from django.core.exceptions import PermissionDenied
    78from django.http import HttpResponseRedirect, Http404
    89from django.shortcuts import render_to_response, get_object_or_404
     
    6768                msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': 'user', 'obj': new_user}
    6869                self.log_addition(request, new_user)
    6970                if "_addanother" in request.POST:
    70                     request.user.message_set.create(message=msg)
     71                    messages.success(request, msg)
    7172                    return HttpResponseRedirect(request.path)
    7273                elif '_popup' in request.REQUEST:
    7374                    return self.response_add(request, new_user)
    7475                else:
    75                     request.user.message_set.create(message=msg + ' ' + ugettext("You may edit it again below."))
     76                    messages.success(request, msg + ' ' +
     77                                     ugettext("You may edit it again below."))
    7678                    return HttpResponseRedirect('../%s/' % new_user.id)
    7779        else:
    7880            form = self.add_form()
     
    104106            if form.is_valid():
    105107                new_user = form.save()
    106108                msg = ugettext('Password changed successfully.')
    107                 request.user.message_set.create(message=msg)
     109                messages.success(request, msg)
    108110                return HttpResponseRedirect('..')
    109111        else:
    110112            form = self.change_password_form(user)
  • django/contrib/auth/models.py

    diff -r 70e75e8cd224 django/contrib/auth/models.py
    a b  
    288288                raise SiteProfileNotAvailable
    289289        return self._profile_cache
    290290
     291    def _get_message_set(self):
     292        import warnings
     293        warnings.warn('The user messaging API is deprecated. Please update'
     294                      ' your code to use the new messages framework.',
     295                      category=PendingDeprecationWarning)
     296        return self._message_set
     297    message_set = property(_get_message_set)
     298
    291299class Message(models.Model):
    292300    """
    293301    The message system is a lightweight way to queue messages for given
     
    297305    actions. For example, "The poll Foo was created successfully." is a
    298306    message.
    299307    """
    300     user = models.ForeignKey(User)
     308    user = models.ForeignKey(User, related_name='_message_set')
    301309    message = models.TextField(_('message'))
    302310
    303311    def __unicode__(self):
  • new file django/contrib/messages/__init__.py

    diff -r 70e75e8cd224 django/contrib/messages/__init__.py
    - +  
     1from api import *
     2from constants import *
  • new file django/contrib/messages/api.py

    diff -r 70e75e8cd224 django/contrib/messages/api.py
    - +  
     1from django.contrib.messages import constants
     2from django.utils.functional import lazy, memoize
     3
     4__all__ = (
     5    'add_message', 'get_messages',
     6    'debug', 'info', 'success', 'warning', 'error',
     7)
     8
     9
     10class MessageFailure(Exception):
     11    pass
     12
     13
     14def add_message(request, level, message, extra_tags='', fail_silently=False):
     15    """
     16    Attempts to add a message to the request using the 'messages' app, falling
     17    back to the user's message_set if MessageMiddleware hasn't been enabled.
     18    """
     19    if hasattr(request, '_messages'):
     20        return request._messages.add(level, message, extra_tags)
     21    if hasattr(request, 'user') and request.user.is_authenticated():
     22        return request.user.message_set.create(message=message)
     23    if not fail_silently:
     24        raise MessageFailure('Without the django.contrib.messages '
     25                                'middleware, messages can only be added to '
     26                                'authenticated users.')
     27
     28
     29def get_messages(request):
     30    """
     31    Returns the message storage on the request if it exists, otherwise returns
     32    user.message_set.all() as the old auth context processor did.
     33    """
     34    if hasattr(request, '_messages'):
     35        return request._messages
     36
     37    def get_user():
     38        if hasattr(request, 'user'):
     39            return request.user
     40        else:
     41            from django.contrib.auth.models import AnonymousUser
     42            return AnonymousUser()
     43
     44    return lazy(memoize(get_user().get_and_delete_messages, {}, 0), list)()
     45
     46
     47def debug(request, message, extra_tags='', fail_silently=False):
     48    """
     49    Adds a message with the ``DEBUG`` level.
     50    """
     51    add_message(request, constants.DEBUG, message, extra_tags=extra_tags,
     52                fail_silently=fail_silently)
     53
     54
     55def info(request, message, extra_tags='', fail_silently=False):
     56    """
     57    Adds a message with the ``INFO`` level.
     58    """
     59    add_message(request, constants.INFO, message, extra_tags=extra_tags,
     60                fail_silently=fail_silently)
     61
     62
     63def success(request, message, extra_tags='', fail_silently=False):
     64    """
     65    Adds a message with the ``SUCCESS`` level.
     66    """
     67    add_message(request, constants.SUCCESS, message, extra_tags=extra_tags,
     68                fail_silently=fail_silently)
     69
     70
     71def warning(request, message, extra_tags='', fail_silently=False):
     72    """
     73    Adds a message with the ``WARNING`` level.
     74    """
     75    add_message(request, constants.WARNING, message, extra_tags=extra_tags,
     76                fail_silently=fail_silently)
     77
     78
     79def error(request, message, extra_tags='', fail_silently=False):
     80    """
     81    Adds a message with the ``ERROR`` level.
     82    """
     83    add_message(request, constants.ERROR, message, extra_tags=extra_tags,
     84                fail_silently=fail_silently)
  • new file django/contrib/messages/constants.py

    diff -r 70e75e8cd224 django/contrib/messages/constants.py
    - +  
     1DEBUG = 10
     2INFO = 20
     3SUCCESS = 25
     4WARNING = 30
     5ERROR = 40
     6
     7DEFAULT_TAGS = {
     8    DEBUG: 'debug',
     9    INFO: 'info',
     10    SUCCESS: 'success',
     11    WARNING: 'warning',
     12    ERROR: 'error',
     13}
  • new file django/contrib/messages/context_processors.py

    diff -r 70e75e8cd224 django/contrib/messages/context_processors.py
    - +  
     1from django.contrib.messages.api import get_messages
     2
     3
     4def messages(request):
     5    """
     6    Returns a lazy 'messages' context variable.
     7    """
     8    return {'messages': get_messages(request)}
  • new file django/contrib/messages/middleware.py

    diff -r 70e75e8cd224 django/contrib/messages/middleware.py
    - +  
     1from django.conf import settings
     2from django.contrib.messages.storage import default_storage
     3
     4
     5class MessageMiddleware(object):
     6    """
     7    Middleware that handles temporary messages.
     8    """
     9
     10    def process_request(self, request):
     11        request._messages = default_storage(request)
     12
     13    def process_response(self, request, response):
     14        """
     15        Updates the storage backend (i.e., saves the messages).
     16
     17        If not all messages could not be stored and ``DEBUG`` is ``True``, a
     18        ``ValueError`` is raised.
     19        """
     20        # A higher middleware layer may return a request which does not contain
     21        # messages storage, so make no assumption that it will be there.
     22        if hasattr(request, '_messages'):
     23            unstored_messages = request._messages.update(response)
     24            if unstored_messages and settings.DEBUG:
     25                raise ValueError('Not all temporary messages could be stored.')
     26        return response
  • new file django/contrib/messages/models.py

    diff -r 70e75e8cd224 django/contrib/messages/models.py
    - +  
     1# Models module required so tests are discovered.
  • new file django/contrib/messages/storage/__init__.py

    diff -r 70e75e8cd224 django/contrib/messages/storage/__init__.py
    - +  
     1from django.conf import settings
     2from django.core.exceptions import ImproperlyConfigured
     3from django.utils.importlib import import_module
     4
     5
     6def get_storage(import_path):
     7    """
     8    Imports the message storage class described by import_path, where
     9    import_path is the full Python path to the class.
     10    """
     11    try:
     12        dot = import_path.rindex('.')
     13    except ValueError:
     14        raise ImproperlyConfigured("%s isn't a Python path." % import_path)
     15    module, classname = import_path[:dot], import_path[dot + 1:]
     16    try:
     17        mod = import_module(module)
     18    except ImportError, e:
     19        raise ImproperlyConfigured('Error importing module %s: "%s"' %
     20                                   (module, e))
     21    try:
     22        return getattr(mod, classname)
     23    except AttributeError:
     24        raise ImproperlyConfigured('Module "%s" does not define a "%s" '
     25                                   'class.' % (module, classname))
     26
     27
     28# Callable with the same interface as the storage classes i.e.  accepts a
     29# 'request' object.  It is wrapped in a lambda to stop 'settings' being used at
     30# the module level
     31default_storage = lambda request: get_storage(settings.MESSAGE_STORAGE)(request)
  • new file django/contrib/messages/storage/base.py

    diff -r 70e75e8cd224 django/contrib/messages/storage/base.py
    - +  
     1from django.conf import settings
     2from django.utils.encoding import force_unicode, StrAndUnicode
     3from django.contrib.messages import constants, utils
     4
     5
     6LEVEL_TAGS = utils.get_level_tags()
     7
     8
     9class Message(StrAndUnicode):
     10    """
     11    Represents an actual message that can be stored in any of the supported
     12    storage classes (typically session- or cookie-based) and rendered in a view
     13    or template.
     14    """
     15
     16    def __init__(self, level, message, extra_tags=None):
     17        self.level = int(level)
     18        self.message = message
     19        self.extra_tags = extra_tags
     20
     21    def _prepare(self):
     22        """
     23        Prepares the message for serialization by forcing the ``message``
     24        and ``extra_tags`` to unicode in case they are lazy translations.
     25
     26        Known "safe" types (None, int, etc.) are not converted (see Django's
     27        ``force_unicode`` implementation for details).
     28        """
     29        self.message = force_unicode(self.message, strings_only=True)
     30        self.extra_tags = force_unicode(self.extra_tags, strings_only=True)
     31
     32    def __eq__(self, other):
     33        return isinstance(other, Message) and self.level == other.level and \
     34                                              self.message == other.message
     35
     36    def __unicode__(self):
     37        return force_unicode(self.message)
     38
     39    def _get_tags(self):
     40        label_tag = force_unicode(LEVEL_TAGS.get(self.level, ''),
     41                                  strings_only=True)
     42        extra_tags = force_unicode(self.extra_tags, strings_only=True)
     43        if extra_tags and label_tag:
     44            return u' '.join([extra_tags, label_tag])
     45        elif extra_tags:
     46            return extra_tags
     47        elif label_tag:
     48            return label_tag
     49        return ''
     50    tags = property(_get_tags)
     51
     52
     53class BaseStorage(object):
     54    """
     55    This is the base backend for temporary message storage.
     56
     57    This is not a complete class; to be a usable storage backend, it must be
     58    subclassed and the two methods ``_get`` and ``_store`` overridden.
     59    """
     60
     61    def __init__(self, request, *args, **kwargs):
     62        self.request = request
     63        self._queued_messages = []
     64        self.used = False
     65        self.added_new = False
     66        super(BaseStorage, self).__init__(*args, **kwargs)
     67
     68    def __len__(self):
     69        return len(self._loaded_messages) + len(self._queued_messages)
     70
     71    def __iter__(self):
     72        self.used = True
     73        if self._queued_messages:
     74            self._loaded_messages.extend(self._queued_messages)
     75            self._queued_messages = []
     76        return iter(self._loaded_messages)
     77
     78    def __contains__(self, item):
     79        return item in self._loaded_messages or item in self._queued_messages
     80
     81    @property
     82    def _loaded_messages(self):
     83        """
     84        Returns a list of loaded messages, retrieving them first if they have
     85        not been loaded yet.
     86        """
     87        if not hasattr(self, '_loaded_data'):
     88            messages, all_retrieved = self._get()
     89            self._loaded_data = messages or []
     90        return self._loaded_data
     91
     92    def _get(self, *args, **kwargs):
     93        """
     94        Retrieves a list of stored messages. Returns a tuple of the messages
     95        and a flag indicating whether or not all the messages originally
     96        intended to be stored in this storage were, in fact, stored and
     97        retrieved; e.g., ``(messages, all_retrieved)``.
     98
     99        **This method must be implemented by a subclass.**
     100
     101        If it is possible to tell if the backend was not used (as opposed to
     102        just containing no messages) then ``None`` should be returned in
     103        place of ``messages``.
     104        """
     105        raise NotImplementedError()
     106
     107    def _store(self, messages, response, *args, **kwargs):
     108        """
     109        Stores a list of messages, returning a list of any messages which could
     110        not be stored.
     111
     112        One type of object must be able to be stored, ``Message``.
     113
     114        **This method must be implemented by a subclass.**
     115        """
     116        raise NotImplementedError()
     117
     118    def _prepare_messages(self, messages):
     119        """
     120        Prepares a list of messages for storage.
     121        """
     122        for message in messages:
     123            message._prepare()
     124
     125    def update(self, response):
     126        """
     127        Stores all unread messages.
     128
     129        If the backend has yet to be iterated, previously stored messages will
     130        be stored again. Otherwise, only messages added after the last
     131        iteration will be stored.
     132        """
     133        self._prepare_messages(self._queued_messages)
     134        if self.used:
     135            return self._store(self._queued_messages, response)
     136        elif self.added_new:
     137            messages = self._loaded_messages + self._queued_messages
     138            return self._store(messages, response)
     139
     140    def add(self, level, message, extra_tags=''):
     141        """
     142        Queues a message to be stored.
     143
     144        The message is only queued if it contained something and its level is
     145        not less than the recording level (``self.level``).
     146        """
     147        if not message:
     148            return
     149        # Check that the message level is not less than the recording level.
     150        level = int(level)
     151        if level < self.level:
     152            return
     153        # Add the message.
     154        self.added_new = True
     155        message = Message(level, message, extra_tags=extra_tags)
     156        self._queued_messages.append(message)
     157
     158    def _get_level(self):
     159        """
     160        Returns the minimum recorded level.
     161
     162        The default level is the ``MESSAGE_LEVEL`` setting. If this is
     163        not found, the ``INFO`` level is used.
     164        """
     165        if not hasattr(self, '_level'):
     166            self._level = getattr(settings, 'MESSAGE_LEVEL', constants.INFO)
     167        return self._level
     168
     169    def _set_level(self, value=None):
     170        """
     171        Sets a custom minimum recorded level.
     172
     173        If set to ``None``, the default level will be used (see the
     174        ``_get_level`` method).
     175        """
     176        if value is None and hasattr(self, '_level'):
     177            del self._level
     178        else:
     179            self._level = int(value)
     180
     181    level = property(_get_level, _set_level, _set_level)
  • new file django/contrib/messages/storage/cookie.py

    diff -r 70e75e8cd224 django/contrib/messages/storage/cookie.py
    - +  
     1import hmac
     2
     3from django.conf import settings
     4from django.utils.hashcompat import sha_constructor
     5from django.contrib.messages import constants
     6from django.contrib.messages.storage.base import BaseStorage, Message
     7from django.utils import simplejson as json
     8
     9
     10class MessageEncoder(json.JSONEncoder):
     11    """
     12    Compactly serializes instances of the ``Message`` class as JSON.
     13    """
     14    message_key = '__json_message'
     15
     16    def default(self, obj):
     17        if isinstance(obj, Message):
     18            message = [self.message_key, obj.level, obj.message]
     19            if obj.extra_tags:
     20                message.append(obj.extra_tags)
     21            return message
     22        return super(MessageEncoder, self).default(obj)
     23
     24
     25class MessageDecoder(json.JSONDecoder):
     26    """
     27    Decodes JSON that includes serialized ``Message`` instances.
     28    """
     29
     30    def process_messages(self, obj):
     31        if isinstance(obj, list) and obj:
     32            if obj[0] == MessageEncoder.message_key:
     33                return Message(*obj[1:])
     34            return [self.process_messages(item) for item in obj]
     35        if isinstance(obj, dict):
     36            return dict([(key, self.process_messages(value))
     37                         for key, value in obj.iteritems()])
     38        return obj
     39
     40    def decode(self, s, **kwargs):
     41        decoded = super(MessageDecoder, self).decode(s, **kwargs)
     42        return self.process_messages(decoded)
     43
     44
     45class CookieStorage(BaseStorage):
     46    """
     47    Stores messages in a cookie.
     48    """
     49    cookie_name = 'messages'
     50    max_cookie_size = 4096
     51    not_finished = '__messagesnotfinished__'
     52
     53    def _get(self, *args, **kwargs):
     54        """
     55        Retrieves a list of messages from the messages cookie.  If the
     56        not_finished sentinel value is found at the end of the message list,
     57        remove it and return a result indicating that not all messages were
     58        retrieved by this storage.
     59        """
     60        data = self.request.COOKIES.get(self.cookie_name)
     61        messages = self._decode(data)
     62        all_retrieved = not (messages and messages[-1] == self.not_finished)
     63        if messages and not all_retrieved:
     64            # remove the sentinel value
     65            messages.pop()
     66        return messages, all_retrieved
     67
     68    def _update_cookie(self, encoded_data, response):
     69        """
     70        Either sets the cookie with the encoded data if there is any data to
     71        store, or deletes the cookie.
     72        """
     73        if encoded_data:
     74            response.set_cookie(self.cookie_name, encoded_data)
     75        else:
     76            response.delete_cookie(self.cookie_name)
     77
     78    def _store(self, messages, response, remove_oldest=True, *args, **kwargs):
     79        """
     80        Stores the messages to a cookie, returning a list of any messages which
     81        could not be stored.
     82
     83        If the encoded data is larger than ``max_cookie_size``, removes
     84        messages until the data fits (these are the messages which are
     85        returned), and add the not_finished sentinel value to indicate as much.
     86        """
     87        unstored_messages = []
     88        encoded_data = self._encode(messages)
     89        if self.max_cookie_size:
     90            while encoded_data and len(encoded_data) > self.max_cookie_size:
     91                if remove_oldest:
     92                    unstored_messages.append(messages.pop(0))
     93                else:
     94                    unstored_messages.insert(0, messages.pop())
     95                encoded_data = self._encode(messages + [self.not_finished],
     96                                            encode_empty=unstored_messages)
     97        self._update_cookie(encoded_data, response)
     98        return unstored_messages
     99
     100    def _hash(self, value):
     101        """
     102        Creates an HMAC/SHA1 hash based on the value and the project setting's
     103        SECRET_KEY, modified to make it unique for the present purpose.
     104        """
     105        key = 'django.contrib.messages' + settings.SECRET_KEY
     106        return hmac.new(key, value, sha_constructor).hexdigest()
     107
     108    def _encode(self, messages, encode_empty=False):
     109        """
     110        Returns an encoded version of the messages list which can be stored as
     111        plain text.
     112
     113        Since the data will be retrieved from the client-side, the encoded data
     114        also contains a hash to ensure that the data was not tampered with.
     115        """
     116        if messages or encode_empty:
     117            encoder = MessageEncoder(separators=(',', ':'))
     118            value = encoder.encode(messages)
     119            return '%s$%s' % (self._hash(value), value)
     120
     121    def _decode(self, data):
     122        """
     123        Safely decodes a encoded text stream back into a list of messages.
     124
     125        If the encoded text stream contained an invalid hash or was in an
     126        invalid format, ``None`` is returned.
     127        """
     128        if not data:
     129            return None
     130        bits = data.split('$', 1)
     131        if len(bits) == 2:
     132            hash, value = bits
     133            if hash == self._hash(value):
     134                try:
     135                    # If we get here (and the JSON decode works), everything is
     136                    # good. In any other case, drop back and return None.
     137                    return json.loads(value, cls=MessageDecoder)
     138                except ValueError:
     139                    pass
     140        # Mark the data as used (so it gets removed) since something was wrong
     141        # with the data.
     142        self.used = True
     143        return None
  • new file django/contrib/messages/storage/fallback.py

    diff -r 70e75e8cd224 django/contrib/messages/storage/fallback.py
    - +  
     1from django.contrib.messages.storage.base import BaseStorage
     2from django.contrib.messages.storage.cookie import CookieStorage
     3from django.contrib.messages.storage.session import SessionStorage
     4try:
     5    set
     6except NameError:
     7    from sets import Set as set   # Python 2.3
     8
     9
     10class FallbackStorage(BaseStorage):
     11    """
     12    Tries to store all messages in the first backend, storing any unstored
     13    messages in each subsequent backend backend.
     14    """
     15    storage_classes = (CookieStorage, SessionStorage)
     16
     17    def __init__(self, *args, **kwargs):
     18        super(FallbackStorage, self).__init__(*args, **kwargs)
     19        self.storages = [storage_class(*args, **kwargs)
     20                         for storage_class in self.storage_classes]
     21        self._used_storages = set()
     22
     23    def _get(self, *args, **kwargs):
     24        """
     25        Gets a single list of messages from all storage backends.
     26        """
     27        all_messages = []
     28        for storage in self.storages:
     29            messages, all_retrieved = storage._get()
     30            # If the backend hasn't been used, no more retrieval is necessary.
     31            if messages is None:
     32                break
     33            if messages:
     34                self._used_storages.add(storage)
     35            all_messages.extend(messages)
     36            # If this storage class contained all the messages, no further
     37            # retrieval is necessary
     38            if all_retrieved:
     39                break
     40        return all_messages, all_retrieved
     41
     42    def _store(self, messages, response, *args, **kwargs):
     43        """
     44        Stores the messages, returning any unstored messages after trying all
     45        backends.
     46
     47        For each storage backend, any messages not stored are passed on to the
     48        next backend.
     49        """
     50        for storage in self.storages:
     51            if messages:
     52                messages = storage._store(messages, response,
     53                                          remove_oldest=False)
     54            # Even if there are no more messages, continue iterating to ensure
     55            # storages which contained messages are flushed.
     56            elif storage in self._used_storages:
     57                storage._store([], response)
     58                self._used_storages.remove(storage)
     59        return messages
  • new file django/contrib/messages/storage/session.py

    diff -r 70e75e8cd224 django/contrib/messages/storage/session.py
    - +  
     1from django.contrib.messages.storage.base import BaseStorage
     2
     3
     4class SessionStorage(BaseStorage):
     5    """
     6    Stores messages in the session (that is, django.contrib.sessions).
     7    """
     8    session_key = '_messages'
     9
     10    def __init__(self, request, *args, **kwargs):
     11        assert hasattr(request, 'session'), "The session-based temporary "\
     12            "message storage requires session middleware to be installed, "\
     13            "and come before the message middleware in the "\
     14            "MIDDLEWARE_CLASSES list."
     15        super(SessionStorage, self).__init__(request, *args, **kwargs)
     16
     17    def _get(self, *args, **kwargs):
     18        """
     19        Retrieves a list of messages from the request's session.  This storage
     20        always stores everything it is given, so return True for the
     21        all_retrieved flag.
     22        """
     23        return self.request.session.get(self.session_key), True
     24
     25    def _store(self, messages, response, *args, **kwargs):
     26        """
     27        Stores a list of messages to the request's session.
     28        """
     29        if messages:
     30            self.request.session[self.session_key] = messages
     31        else:
     32            self.request.session.pop(self.session_key, None)
     33        return []
  • new file django/contrib/messages/storage/user_messages.py

    diff -r 70e75e8cd224 django/contrib/messages/storage/user_messages.py
    - +  
     1"""
     2Storages used to assist in the deprecation of contrib.auth User messages.
     3
     4"""
     5from django.contrib.messages import constants
     6from django.contrib.messages.storage.base import BaseStorage, Message
     7from django.contrib.auth.models import User
     8from django.contrib.messages.storage.fallback import FallbackStorage
     9
     10
     11class UserMessagesStorage(BaseStorage):
     12    """
     13    Retrieves messages from the User, using the legacy user.message_set API.
     14
     15    This storage is "read-only" insofar as it can only retrieve and delete
     16    messages, not store them.
     17    """
     18    session_key = '_messages'
     19
     20    def _get_messages_queryset(self):
     21        """
     22        Returns the QuerySet containing all user messages (or ``None`` if
     23        request.user is not a contrib.auth User).
     24        """
     25        user = getattr(self.request, 'user', None)
     26        if isinstance(user, User):
     27            return user._message_set.all()
     28
     29    def add(self, *args, **kwargs):
     30        raise NotImplementedError('This message storage is read-only.')
     31
     32    def _get(self, *args, **kwargs):
     33        """
     34        Retrieves a list of messages assigned to the User.  This backend never
     35        stores anything, so all_retrieved is assumed to be False.
     36        """
     37        queryset = self._get_messages_queryset()
     38        if queryset is None:
     39            # This is a read-only and optional storage, so to ensure other
     40            # storages will also be read if used with FallbackStorage an empty
     41            # list is returned rather than None.
     42            return [], False
     43        messages = []
     44        for user_message in queryset:
     45            messages.append(Message(constants.INFO, user_message.message))
     46        return messages, False
     47
     48    def _store(self, messages, *args, **kwargs):
     49        """
     50        Removes any messages assigned to the User and returns the list of
     51        messages (since no messages are stored in this read-only storage).
     52        """
     53        queryset = self._get_messages_queryset()
     54        if queryset is not None:
     55            queryset.delete()
     56        return messages
     57
     58
     59class LegacyFallbackStorage(FallbackStorage):
     60    """
     61    Works like ``FallbackStorage`` but also handles retrieving (and clearing)
     62    contrib.auth User messages.
     63    """
     64    storage_classes = (UserMessagesStorage,) + FallbackStorage.storage_classes
  • new file django/contrib/messages/tests/__init__.py

    diff -r 70e75e8cd224 django/contrib/messages/tests/__init__.py
    - +  
     1from django.contrib.messages.tests.cookie import CookieTest
     2from django.contrib.messages.tests.fallback import FallbackTest
     3from django.contrib.messages.tests.middleware import MiddlewareTest
     4from django.contrib.messages.tests.session import SessionTest
     5from django.contrib.messages.tests.user_messages import \
     6                                           UserMessagesTest, LegacyFallbackTest
  • new file django/contrib/messages/tests/base.py

    diff -r 70e75e8cd224 django/contrib/messages/tests/base.py
    - +  
     1from django import http
     2from django.test import TestCase
     3from django.conf import settings
     4from django.utils.translation import ugettext_lazy
     5from django.contrib.messages import constants, utils
     6from django.contrib.messages.storage import default_storage, base
     7from django.contrib.messages.storage.base import Message
     8from django.core.urlresolvers import reverse
     9from django.contrib.auth.models import User
     10from django.contrib.messages.api import MessageFailure
     11
     12
     13def add_level_messages(storage):
     14    """
     15    Adds 6 messages from different levels (including a custom one) to a storage
     16    instance.
     17    """
     18    storage.add(constants.INFO, 'A generic info message')
     19    storage.add(29, 'Some custom level')
     20    storage.add(constants.DEBUG, 'A debugging message', extra_tags='extra-tag')
     21    storage.add(constants.WARNING, 'A warning')
     22    storage.add(constants.ERROR, 'An error')
     23    storage.add(constants.SUCCESS, 'This was a triumph.')
     24
     25
     26class BaseTest(TestCase):
     27    storage_class = default_storage
     28    restore_settings = ['MESSAGE_LEVEL', 'MESSAGE_TAGS']
     29    urls = 'django.contrib.messages.tests.urls'
     30    levels = {
     31        'debug': constants.DEBUG,
     32        'info': constants.INFO,
     33        'success': constants.SUCCESS,
     34        'warning': constants.WARNING,
     35        'error': constants.ERROR,
     36    }
     37
     38    def setUp(self):
     39        self._remembered_settings = {}
     40        for setting in self.restore_settings:
     41            if hasattr(settings, setting):
     42                self._remembered_settings[setting] = getattr(settings, setting)
     43                delattr(settings._wrapped, setting)
     44        # backup these manually because we do not want them deleted
     45        self._middleware_classes = settings.MIDDLEWARE_CLASSES
     46        self._template_context_processors = \
     47           settings.TEMPLATE_CONTEXT_PROCESSORS
     48        self._installed_apps = settings.INSTALLED_APPS
     49
     50    def tearDown(self):
     51        for setting in self.restore_settings:
     52            self.restore_setting(setting)
     53        # restore these manually (see above)
     54        settings.MIDDLEWARE_CLASSES = self._middleware_classes
     55        settings.TEMPLATE_CONTEXT_PROCESSORS = \
     56           self._template_context_processors
     57        settings.INSTALLED_APPS = self._installed_apps
     58
     59    def restore_setting(self, setting):
     60        if setting in self._remembered_settings:
     61            value = self._remembered_settings.pop(setting)
     62            setattr(settings, setting, value)
     63        elif hasattr(settings, setting):
     64            delattr(settings._wrapped, setting)
     65
     66    def get_request(self):
     67        return http.HttpRequest()
     68
     69    def get_response(self):
     70        return http.HttpResponse()
     71
     72    def get_storage(self, data=None):
     73        """
     74        Returns the storage backend, setting its loaded data to the ``data``
     75        argument.
     76
     77        This method avoids the storage ``_get`` method from getting called so
     78        that other parts of the storage backend can be tested independent of
     79        the message retrieval logic.
     80        """
     81        storage = self.storage_class(self.get_request())
     82        storage._loaded_data = data or []
     83        return storage
     84
     85    def test_add(self):
     86        storage = self.get_storage()
     87        self.assertFalse(storage.added_new)
     88        storage.add(constants.INFO, 'Test message 1')
     89        self.assert_(storage.added_new)
     90        storage.add(constants.INFO, 'Test message 2', extra_tags='tag')
     91        self.assertEqual(len(storage), 2)
     92
     93    def test_add_lazy_translation(self):
     94        storage = self.get_storage()
     95        response = self.get_response()
     96
     97        storage.add(constants.INFO, ugettext_lazy('lazy message'))
     98        storage.update(response)
     99
     100        storing = self.stored_messages_count(storage, response)
     101        self.assertEqual(storing, 1)
     102
     103    def test_no_update(self):
     104        storage = self.get_storage()
     105        response = self.get_response()
     106        storage.update(response)
     107        storing = self.stored_messages_count(storage, response)
     108        self.assertEqual(storing, 0)
     109
     110    def test_add_update(self):
     111        storage = self.get_storage()
     112        response = self.get_response()
     113
     114        storage.add(constants.INFO, 'Test message 1')
     115        storage.add(constants.INFO, 'Test message 1', extra_tags='tag')
     116        storage.update(response)
     117
     118        storing = self.stored_messages_count(storage, response)
     119        self.assertEqual(storing, 2)
     120
     121    def test_existing_add_read_update(self):
     122        storage = self.get_existing_storage()
     123        response = self.get_response()
     124
     125        storage.add(constants.INFO, 'Test message 3')
     126        list(storage)   # Simulates a read
     127        storage.update(response)
     128
     129        storing = self.stored_messages_count(storage, response)
     130        self.assertEqual(storing, 0)
     131
     132    def test_existing_read_add_update(self):
     133        storage = self.get_existing_storage()
     134        response = self.get_response()
     135
     136        list(storage)   # Simulates a read
     137        storage.add(constants.INFO, 'Test message 3')
     138        storage.update(response)
     139
     140        storing = self.stored_messages_count(storage, response)
     141        self.assertEqual(storing, 1)
     142
     143    def test_full_request_response_cycle(self):
     144        """
     145        With the message middleware enabled, tests that messages are properly
     146        stored and then retrieved across the full request/redirect/response
     147        cycle.
     148        """
     149        settings.MESSAGE_LEVEL = constants.DEBUG
     150        data = {
     151            'messages': ['Test message %d' % x for x in xrange(10)],
     152        }
     153        show_url = reverse('django.contrib.messages.tests.urls.show')
     154        for level in ('debug', 'info', 'success', 'warning', 'error'):
     155            add_url = reverse('django.contrib.messages.tests.urls.add',
     156                              args=(level,))
     157            response = self.client.post(add_url, data, follow=True)
     158            self.assertRedirects(response, show_url)
     159            self.assertTrue('messages' in response.context)
     160            messages = [Message(self.levels[level], msg) for msg in
     161                                                         data['messages']]
     162            self.assertEqual(list(response.context['messages']), messages)
     163            for msg in data['messages']:
     164                self.assertContains(response, msg)
     165
     166    def test_multiple_posts(self):
     167        """
     168        Tests that messages persist properly when multiple POSTs are made
     169        before a GET.
     170        """
     171        settings.MESSAGE_LEVEL = constants.DEBUG
     172        data = {
     173            'messages': ['Test message %d' % x for x in xrange(10)],
     174        }
     175        show_url = reverse('django.contrib.messages.tests.urls.show')
     176        messages = []
     177        for level in ('debug', 'info', 'success', 'warning', 'error'):
     178            messages.extend([Message(self.levels[level], msg) for msg in
     179                                                             data['messages']])
     180            add_url = reverse('django.contrib.messages.tests.urls.add',
     181                              args=(level,))
     182            self.client.post(add_url, data)
     183        response = self.client.get(show_url)
     184        self.assertTrue('messages' in response.context)
     185        self.assertEqual(list(response.context['messages']), messages)
     186        for msg in data['messages']:
     187            self.assertContains(response, msg)
     188
     189    def test_middleware_disabled_auth_user(self):
     190        """
     191        Tests that the messages API successfully falls back to using
     192        user.message_set to store messages directly when the middleware is
     193        disabled.
     194        """
     195        settings.MESSAGE_LEVEL = constants.DEBUG
     196        user = User.objects.create_user('test', 'test@example.com', 'test')
     197        self.client.login(username='test', password='test')
     198        settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
     199        settings.INSTALLED_APPS.remove(
     200            'django.contrib.messages',
     201        )
     202        settings.MIDDLEWARE_CLASSES = list(settings.MIDDLEWARE_CLASSES)
     203        settings.MIDDLEWARE_CLASSES.remove(
     204            'django.contrib.messages.middleware.MessageMiddleware',
     205        )
     206        settings.TEMPLATE_CONTEXT_PROCESSORS = \
     207          list(settings.TEMPLATE_CONTEXT_PROCESSORS)
     208        settings.TEMPLATE_CONTEXT_PROCESSORS.remove(
     209            'django.contrib.messages.context_processors.messages',
     210        )
     211        data = {
     212            'messages': ['Test message %d' % x for x in xrange(10)],
     213        }
     214        show_url = reverse('django.contrib.messages.tests.urls.show')
     215        for level in ('debug', 'info', 'success', 'warning', 'error'):
     216            add_url = reverse('django.contrib.messages.tests.urls.add',
     217                              args=(level,))
     218            response = self.client.post(add_url, data, follow=True)
     219            self.assertRedirects(response, show_url)
     220            self.assertTrue('messages' in response.context)
     221            self.assertEqual(list(response.context['messages']),
     222                             data['messages'])
     223            for msg in data['messages']:
     224                self.assertContains(response, msg)
     225
     226    def test_middleware_disabled_anon_user(self):
     227        """
     228        Tests that, when the middleware is disabled and a user is not logged
     229        in, an exception is raised when one attempts to store a message.
     230        """
     231        settings.MESSAGE_LEVEL = constants.DEBUG
     232        settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
     233        settings.INSTALLED_APPS.remove(
     234            'django.contrib.messages',
     235        )
     236        settings.MIDDLEWARE_CLASSES = list(settings.MIDDLEWARE_CLASSES)
     237        settings.MIDDLEWARE_CLASSES.remove(
     238            'django.contrib.messages.middleware.MessageMiddleware',
     239        )
     240        settings.TEMPLATE_CONTEXT_PROCESSORS = \
     241          list(settings.TEMPLATE_CONTEXT_PROCESSORS)
     242        settings.TEMPLATE_CONTEXT_PROCESSORS.remove(
     243            'django.contrib.messages.context_processors.messages',
     244        )
     245        data = {
     246            'messages': ['Test message %d' % x for x in xrange(10)],
     247        }
     248        show_url = reverse('django.contrib.messages.tests.urls.show')
     249        for level in ('debug', 'info', 'success', 'warning', 'error'):
     250            add_url = reverse('django.contrib.messages.tests.urls.add',
     251                              args=(level,))
     252            self.assertRaises(MessageFailure, self.client.post, add_url,
     253                              data, follow=True)
     254
     255    def test_middleware_disabled_anon_user_fail_silently(self):
     256        """
     257        Tests that, when the middleware is disabled and a user is not logged
     258        in, an exception is raised when one attempts to store a message.
     259        """
     260        settings.MESSAGE_LEVEL = constants.DEBUG
     261        settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
     262        settings.INSTALLED_APPS.remove(
     263            'django.contrib.messages',
     264        )
     265        settings.MIDDLEWARE_CLASSES = list(settings.MIDDLEWARE_CLASSES)
     266        settings.MIDDLEWARE_CLASSES.remove(
     267            'django.contrib.messages.middleware.MessageMiddleware',
     268        )
     269        settings.TEMPLATE_CONTEXT_PROCESSORS = \
     270          list(settings.TEMPLATE_CONTEXT_PROCESSORS)
     271        settings.TEMPLATE_CONTEXT_PROCESSORS.remove(
     272            'django.contrib.messages.context_processors.messages',
     273        )
     274        data = {
     275            'messages': ['Test message %d' % x for x in xrange(10)],
     276            'fail_silently': True,
     277        }
     278        show_url = reverse('django.contrib.messages.tests.urls.show')
     279        for level in ('debug', 'info', 'success', 'warning', 'error'):
     280            add_url = reverse('django.contrib.messages.tests.urls.add',
     281                              args=(level,))
     282            response = self.client.post(add_url, data, follow=True)
     283            self.assertRedirects(response, show_url)
     284            self.assertTrue('messages' in response.context)
     285            self.assertEqual(list(response.context['messages']), [])
     286
     287    def stored_messages_count(self, storage, response):
     288        """
     289        Returns the number of messages being stored after a
     290        ``storage.update()`` call.
     291        """
     292        raise NotImplementedError('This method must be set by a subclass.')
     293
     294    def test_get(self):
     295        raise NotImplementedError('This method must be set by a subclass.')
     296
     297    def get_existing_storage(self):
     298        return self.get_storage([Message(constants.INFO, 'Test message 1'),
     299                                 Message(constants.INFO, 'Test message 2',
     300                                              extra_tags='tag')])
     301
     302    def test_existing_read(self):
     303        """
     304        Tests that reading the existing storage doesn't cause the data to be
     305        lost.
     306        """
     307        storage = self.get_existing_storage()
     308        self.assertFalse(storage.used)
     309        # After iterating the storage engine directly, the used flag is set.
     310        data = list(storage)
     311        self.assert_(storage.used)
     312        # The data does not disappear because it has been iterated.
     313        self.assertEqual(data, list(storage))
     314
     315    def test_existing_add(self):
     316        storage = self.get_existing_storage()
     317        self.assertFalse(storage.added_new)
     318        storage.add(constants.INFO, 'Test message 3')
     319        self.assert_(storage.added_new)
     320
     321    def test_default_level(self):
     322        storage = self.get_storage()
     323        add_level_messages(storage)
     324        self.assertEqual(len(storage), 5)
     325
     326    def test_low_level(self):
     327        storage = self.get_storage()
     328        storage.level = 5
     329        add_level_messages(storage)
     330        self.assertEqual(len(storage), 6)
     331
     332    def test_high_level(self):
     333        storage = self.get_storage()
     334        storage.level = 30
     335        add_level_messages(storage)
     336        self.assertEqual(len(storage), 2)
     337
     338    def test_settings_level(self):
     339        settings.MESSAGE_LEVEL = 29
     340        storage = self.get_storage()
     341        add_level_messages(storage)
     342        self.assertEqual(len(storage), 3)
     343
     344    def test_tags(self):
     345        storage = self.get_storage()
     346        storage.level = 0
     347        add_level_messages(storage)
     348        tags = [msg.tags for msg in storage]
     349        self.assertEqual(tags,
     350                         ['info', '', 'extra-tag debug', 'warning', 'error',
     351                          'success'])
     352
     353    def test_custom_tags(self):
     354        settings.MESSAGE_TAGS = {
     355            constants.INFO: 'info',
     356            constants.DEBUG: '',
     357            constants.WARNING: '',
     358            constants.ERROR: 'bad',
     359            29: 'custom',
     360        }
     361        # LEVEL_TAGS is a constant defined in the
     362        # django.contrib.messages.storage.base module, so after changing
     363        # settings.MESSAGE_TAGS, we need to update that constant too.
     364        base.LEVEL_TAGS = utils.get_level_tags()
     365        try:
     366            storage = self.get_storage()
     367            storage.level = 0
     368            add_level_messages(storage)
     369            tags = [msg.tags for msg in storage]
     370            self.assertEqual(tags,
     371                         ['info', 'custom', 'extra-tag', '', 'bad', 'success'])
     372        finally:
     373            # Ensure the level tags constant is put back like we found it.
     374            self.restore_setting('MESSAGE_TAGS')
     375            base.LEVEL_TAGS = utils.get_level_tags()
  • new file django/contrib/messages/tests/cookie.py

    diff -r 70e75e8cd224 django/contrib/messages/tests/cookie.py
    - +  
     1from django.contrib.messages import constants
     2from django.contrib.messages.tests.base import BaseTest
     3from django.contrib.messages.storage.cookie import CookieStorage, \
     4                                            MessageEncoder, MessageDecoder
     5from django.contrib.messages.storage.base import Message
     6from django.utils import simplejson as json
     7
     8
     9def set_cookie_data(storage, messages, invalid=False, encode_empty=False):
     10    """
     11    Sets ``request.COOKIES`` with the encoded data and removes the storage
     12    backend's loaded data cache.
     13    """
     14    encoded_data = storage._encode(messages, encode_empty=encode_empty)
     15    if invalid:
     16        # Truncate the first character so that the hash is invalid.
     17        encoded_data = encoded_data[1:]
     18    storage.request.COOKIES = {CookieStorage.cookie_name: encoded_data}
     19    if hasattr(storage, '_loaded_data'):
     20        del storage._loaded_data
     21
     22
     23def stored_cookie_messages_count(storage, response):
     24    """
     25    Returns an integer containing the number of messages stored.
     26    """
     27    # Get a list of cookies, excluding ones with a max-age of 0 (because
     28    # they have been marked for deletion).
     29    cookie = response.cookies.get(storage.cookie_name)
     30    if not cookie or cookie['max-age'] == 0:
     31        return 0
     32    data = storage._decode(cookie.value)
     33    if not data:
     34        return 0
     35    if data[-1] == CookieStorage.not_finished:
     36        data.pop()
     37    return len(data)
     38
     39
     40class CookieTest(BaseTest):
     41    storage_class = CookieStorage
     42
     43    def stored_messages_count(self, storage, response):
     44        return stored_cookie_messages_count(storage, response)
     45
     46    def test_get(self):
     47        storage = self.storage_class(self.get_request())
     48        # Set initial data.
     49        example_messages = ['test', 'me']
     50        set_cookie_data(storage, example_messages)
     51        # Test that the message actually contains what we expect.
     52        self.assertEqual(list(storage), example_messages)
     53
     54    def test_get_bad_cookie(self):
     55        request = self.get_request()
     56        storage = self.storage_class(request)
     57        # Set initial (invalid) data.
     58        example_messages = ['test', 'me']
     59        set_cookie_data(storage, example_messages, invalid=True)
     60        # Test that the message actually contains what we expect.
     61        self.assertEqual(list(storage), [])
     62
     63    def test_max_cookie_length(self):
     64        """
     65        Tests that, if the data exceeds what is allowed in a cookie, older
     66        messages are removed before saving (and returned by the ``update``
     67        method).
     68        """
     69        storage = self.get_storage()
     70        response = self.get_response()
     71
     72        for i in range(5):
     73            storage.add(constants.INFO, str(i) * 900)
     74        unstored_messages = storage.update(response)
     75
     76        cookie_storing = self.stored_messages_count(storage, response)
     77        self.assertEqual(cookie_storing, 4)
     78
     79        self.assertEqual(len(unstored_messages), 1)
     80        self.assert_(unstored_messages[0].message == '0' * 900)
     81
     82    def test_json_encoder_decoder(self):
     83        """
     84        Tests that an complex nested data structure containing Message
     85        instances is properly encoded/decoded by the custom JSON
     86        encoder/decoder classes.
     87        """
     88        messages = [
     89            {
     90                'message': Message(constants.INFO, 'Test message'),
     91                'message_list': [Message(constants.INFO, 'message %s') \
     92                                 for x in xrange(5)] + [{'another-message': \
     93                                 Message(constants.ERROR, 'error')}],
     94            },
     95            Message(constants.INFO, 'message %s'),
     96        ]
     97        encoder = MessageEncoder(separators=(',', ':'))
     98        value = encoder.encode(messages)
     99        decoded_messages = json.loads(value, cls=MessageDecoder)
     100        self.assertEqual(messages, decoded_messages)
  • new file django/contrib/messages/tests/fallback.py

    diff -r 70e75e8cd224 django/contrib/messages/tests/fallback.py
    - +  
     1from django.contrib.messages import constants
     2from django.contrib.messages.storage.fallback import FallbackStorage, \
     3    CookieStorage
     4from django.contrib.messages.tests.base import BaseTest
     5from django.contrib.messages.tests.cookie import set_cookie_data, \
     6    stored_cookie_messages_count
     7from django.contrib.messages.tests.session import set_session_data, \
     8    stored_session_messages_count
     9
     10
     11class FallbackTest(BaseTest):
     12    storage_class = FallbackStorage
     13
     14    def get_request(self):
     15        self.session = {}
     16        request = super(FallbackTest, self).get_request()
     17        request.session = self.session
     18        return request
     19
     20    def get_cookie_storage(self, storage):
     21        return storage.storages[-2]
     22
     23    def get_session_storage(self, storage):
     24        return storage.storages[-1]
     25
     26    def stored_cookie_messages_count(self, storage, response):
     27        return stored_cookie_messages_count(self.get_cookie_storage(storage),
     28                                            response)
     29
     30    def stored_session_messages_count(self, storage, response):
     31        return stored_session_messages_count(self.get_session_storage(storage))
     32
     33    def stored_messages_count(self, storage, response):
     34        """
     35        Return the storage totals from both cookie and session backends.
     36        """
     37        total = (self.stored_cookie_messages_count(storage, response) +
     38                 self.stored_session_messages_count(storage, response))
     39        return total
     40
     41    def test_get(self):
     42        request = self.get_request()
     43        storage = self.storage_class(request)
     44        cookie_storage = self.get_cookie_storage(storage)
     45
     46        # Set initial cookie data.
     47        example_messages = [str(i) for i in range(5)]
     48        set_cookie_data(cookie_storage, example_messages)
     49
     50        # Overwrite the _get method of the fallback storage to prove it is not
     51        # used (it would cause a TypeError: 'NoneType' object is not callable).
     52        self.get_session_storage(storage)._get = None
     53
     54        # Test that the message actually contains what we expect.
     55        self.assertEqual(list(storage), example_messages)
     56
     57    def test_get_empty(self):
     58        request = self.get_request()
     59        storage = self.storage_class(request)
     60
     61        # Overwrite the _get method of the fallback storage to prove it is not
     62        # used (it would cause a TypeError: 'NoneType' object is not callable).
     63        self.get_session_storage(storage)._get = None
     64
     65        # Test that the message actually contains what we expect.
     66        self.assertEqual(list(storage), [])
     67
     68    def test_get_fallback(self):
     69        request = self.get_request()
     70        storage = self.storage_class(request)
     71        cookie_storage = self.get_cookie_storage(storage)
     72        session_storage = self.get_session_storage(storage)
     73
     74        # Set initial cookie and session data.
     75        example_messages = [str(i) for i in range(5)]
     76        set_cookie_data(cookie_storage, example_messages[:4] +
     77                        [CookieStorage.not_finished])
     78        set_session_data(session_storage, example_messages[4:])
     79
     80        # Test that the message actually contains what we expect.
     81        self.assertEqual(list(storage), example_messages)
     82
     83    def test_get_fallback_only(self):
     84        request = self.get_request()
     85        storage = self.storage_class(request)
     86        cookie_storage = self.get_cookie_storage(storage)
     87        session_storage = self.get_session_storage(storage)
     88
     89        # Set initial cookie and session data.
     90        example_messages = [str(i) for i in range(5)]
     91        set_cookie_data(cookie_storage, [CookieStorage.not_finished],
     92                        encode_empty=True)
     93        set_session_data(session_storage, example_messages)
     94
     95        # Test that the message actually contains what we expect.
     96        self.assertEqual(list(storage), example_messages)
     97
     98    def test_flush_used_backends(self):
     99        request = self.get_request()
     100        storage = self.storage_class(request)
     101        cookie_storage = self.get_cookie_storage(storage)
     102        session_storage = self.get_session_storage(storage)
     103
     104        # Set initial cookie and session data.
     105        set_cookie_data(cookie_storage, ['cookie', CookieStorage.not_finished])
     106        set_session_data(session_storage, ['session'])
     107
     108        # When updating, previously used but no longer needed backends are
     109        # flushed.
     110        response = self.get_response()
     111        list(storage)
     112        storage.update(response)
     113        session_storing = self.stored_session_messages_count(storage, response)
     114        self.assertEqual(session_storing, 0)
     115
     116    def test_no_fallback(self):
     117        """
     118        Confirms that:
     119
     120        (1) A short number of messages whose data size doesn't exceed what is
     121        allowed in a cookie will all be stored in the CookieBackend.
     122
     123        (2) If the CookieBackend can store all messages, the SessionBackend
     124        won't be written to at all.
     125        """
     126        storage = self.get_storage()
     127        response = self.get_response()
     128
     129        # Overwrite the _store method of the fallback storage to prove it isn't
     130        # used (it would cause a TypeError: 'NoneType' object is not callable).
     131        self.get_session_storage(storage)._store = None
     132
     133        for i in range(5):
     134            storage.add(constants.INFO, str(i) * 100)
     135        storage.update(response)
     136
     137        cookie_storing = self.stored_cookie_messages_count(storage, response)
     138        self.assertEqual(cookie_storing, 5)
     139        session_storing = self.stored_session_messages_count(storage, response)
     140        self.assertEqual(session_storing, 0)
     141
     142    def test_session_fallback(self):
     143        """
     144        Confirms that, if the data exceeds what is allowed in a cookie, older
     145        messages which did not "fit" are stored in the SessionBackend.
     146        """
     147        storage = self.get_storage()
     148        response = self.get_response()
     149
     150        for i in range(5):
     151            storage.add(constants.INFO, str(i) * 900)
     152        storage.update(response)
     153
     154        cookie_storing = self.stored_cookie_messages_count(storage, response)
     155        self.assertEqual(cookie_storing, 4)
     156        session_storing = self.stored_session_messages_count(storage, response)
     157        self.assertEqual(session_storing, 1)
     158
     159    def test_session_fallback_only(self):
     160        """
     161        Confirms that large messages, none of which fit in a cookie, are stored
     162        in the SessionBackend (and nothing is stored in the CookieBackend).
     163        """
     164        storage = self.get_storage()
     165        response = self.get_response()
     166
     167        storage.add(constants.INFO, 'x' * 5000)
     168        storage.update(response)
     169
     170        cookie_storing = self.stored_cookie_messages_count(storage, response)
     171        self.assertEqual(cookie_storing, 0)
     172        session_storing = self.stored_session_messages_count(storage, response)
     173        self.assertEqual(session_storing, 1)
  • new file django/contrib/messages/tests/middleware.py

    diff -r 70e75e8cd224 django/contrib/messages/tests/middleware.py
    - +  
     1import unittest
     2from django import http
     3from django.contrib.messages.middleware import MessageMiddleware
     4
     5
     6class MiddlewareTest(unittest.TestCase):
     7
     8    def setUp(self):
     9        self.middleware = MessageMiddleware()
     10
     11    def test_response_without_messages(self):
     12        """
     13        Makes sure that the response middleware is tolerant of messages not
     14        existing on request.
     15        """
     16        request = http.HttpRequest()
     17        response = http.HttpResponse()
     18        self.middleware.process_response(request, response)
  • new file django/contrib/messages/tests/session.py

    diff -r 70e75e8cd224 django/contrib/messages/tests/session.py
    - +  
     1from django.contrib.messages.tests.base import BaseTest
     2from django.contrib.messages.storage.session import SessionStorage
     3
     4
     5def set_session_data(storage, messages):
     6    """
     7    Sets the messages into the backend request's session and remove the
     8    backend's loaded data cache.
     9    """
     10    storage.request.session[storage.session_key] = messages
     11    if hasattr(storage, '_loaded_data'):
     12        del storage._loaded_data
     13
     14
     15def stored_session_messages_count(storage):
     16    data = storage.request.session.get(storage.session_key, [])
     17    return len(data)
     18
     19
     20class SessionTest(BaseTest):
     21    storage_class = SessionStorage
     22
     23    def get_request(self):
     24        self.session = {}
     25        request = super(SessionTest, self).get_request()
     26        request.session = self.session
     27        return request
     28
     29    def stored_messages_count(self, storage, response):
     30        return stored_session_messages_count(storage)
     31
     32    def test_get(self):
     33        storage = self.storage_class(self.get_request())
     34        # Set initial data.
     35        example_messages = ['test', 'me']
     36        set_session_data(storage, example_messages)
     37        # Test that the message actually contains what we expect.
     38        self.assertEqual(list(storage), example_messages)
  • new file django/contrib/messages/tests/urls.py

    diff -r 70e75e8cd224 django/contrib/messages/tests/urls.py
    - +  
     1from django.conf.urls.defaults import *
     2from django.contrib import messages
     3from django.core.urlresolvers import reverse
     4from django.http import HttpResponseRedirect, HttpResponse
     5from django.shortcuts import render_to_response
     6from django.template import RequestContext, Template
     7
     8
     9def add(request, message_type):
     10    # don't default to False here, because we want to test that it defaults
     11    # to False if unspecified
     12    fail_silently = request.POST.get('fail_silently', None)
     13    for msg in request.POST.getlist('messages'):
     14        if fail_silently is not None:
     15            getattr(messages, message_type)(request, msg,
     16                                            fail_silently=fail_silently)
     17        else:
     18            getattr(messages, message_type)(request, msg)
     19    show_url = reverse('django.contrib.messages.tests.urls.show')
     20    return HttpResponseRedirect(show_url)
     21
     22
     23def show(request):
     24    t = Template("""{% if messages %}
     25<ul class="messages">
     26    {% for message in messages %}
     27    <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
     28        {{ message }}
     29    </li>
     30    {% endfor %}
     31</ul>
     32{% endif %}""")
     33    return HttpResponse(t.render(RequestContext(request)))
     34
     35
     36urlpatterns = patterns('',
     37    ('^add/(debug|info|success|warning|error)/$', add),
     38    ('^show/$', show),
     39)
  • new file django/contrib/messages/tests/user_messages.py

    diff -r 70e75e8cd224 django/contrib/messages/tests/user_messages.py
    - +  
     1from django import http
     2from django.contrib.auth.models import User
     3from django.contrib.messages.storage.user_messages import UserMessagesStorage,\
     4    LegacyFallbackStorage
     5from django.contrib.messages.tests.cookie import set_cookie_data
     6from django.contrib.messages.tests.fallback import FallbackTest
     7from django.test import TestCase
     8
     9
     10class UserMessagesTest(TestCase):
     11
     12    def setUp(self):
     13        self.user = User.objects.create(username='tester')
     14
     15    def test_add(self):
     16        storage = UserMessagesStorage(http.HttpRequest())
     17        self.assertRaises(NotImplementedError, storage.add, 'Test message 1')
     18
     19    def test_get_anonymous(self):
     20        # Ensure that the storage still works if no user is attached to the
     21        # request.
     22        storage = UserMessagesStorage(http.HttpRequest())
     23        self.assertEqual(len(storage), 0)
     24
     25    def test_get(self):
     26        storage = UserMessagesStorage(http.HttpRequest())
     27        storage.request.user = self.user
     28        self.user.message_set.create(message='test message')
     29
     30        self.assertEqual(len(storage), 1)
     31        self.assertEqual(list(storage)[0].message, 'test message')
     32
     33
     34class LegacyFallbackTest(FallbackTest, TestCase):
     35    storage_class = LegacyFallbackStorage
     36
     37    def setUp(self):
     38        super(LegacyFallbackTest, self).setUp()
     39        self.user = User.objects.create(username='tester')
     40
     41    def get_request(self, *args, **kwargs):
     42        request = super(LegacyFallbackTest, self).get_request(*args, **kwargs)
     43        request.user = self.user
     44        return request
     45
     46    def test_get_legacy_only(self):
     47        request = self.get_request()
     48        storage = self.storage_class(request)
     49        self.user.message_set.create(message='user message')
     50
     51        # Test that the message actually contains what we expect.
     52        self.assertEqual(len(storage), 1)
     53        self.assertEqual(list(storage)[0].message, 'user message')
     54
     55    def test_get_legacy(self):
     56        request = self.get_request()
     57        storage = self.storage_class(request)
     58        cookie_storage = self.get_cookie_storage(storage)
     59        self.user.message_set.create(message='user message')
     60        set_cookie_data(cookie_storage, ['cookie'])
     61
     62        # Test that the message actually contains what we expect.
     63        self.assertEqual(len(storage), 2)
     64        self.assertEqual(list(storage)[0].message, 'user message')
     65        self.assertEqual(list(storage)[1], 'cookie')
  • new file django/contrib/messages/utils.py

    diff -r 70e75e8cd224 django/contrib/messages/utils.py
    - +  
     1from django.conf import settings
     2from django.contrib.messages import constants
     3
     4
     5def get_level_tags():
     6    """
     7    Returns the message level tags.
     8    """
     9    level_tags = constants.DEFAULT_TAGS.copy()
     10    level_tags.update(getattr(settings, 'MESSAGE_TAGS', {}))
     11    return level_tags
  • django/core/context_processors.py

    diff -r 70e75e8cd224 django/core/context_processors.py
    a b  
    1010from django.conf import settings
    1111from django.middleware.csrf import get_token
    1212from django.utils.functional import lazy, memoize, SimpleLazyObject
     13from django.contrib import messages
    1314
    1415def auth(request):
    1516    """
     
    3738
    3839    return {
    3940        'user': SimpleLazyObject(get_user),
    40         'messages': lazy(memoize(lambda: get_user().get_and_delete_messages(), {}, 0), list)(),
    41         'perms':  lazy(lambda: PermWrapper(get_user()), PermWrapper)(),
     41        'messages': messages.get_messages(request),
     42        'perms': lazy(lambda: PermWrapper(get_user()), PermWrapper)(),
    4243    }
    4344
    4445def csrf(request):
  • django/views/generic/create_update.py

    diff -r 70e75e8cd224 django/views/generic/create_update.py
    a b  
    66from django.utils.translation import ugettext
    77from django.contrib.auth.views import redirect_to_login
    88from django.views.generic import GenericViewError
     9from django.contrib import messages
    910
    1011
    1112def apply_extra_context(extra_context, context):
     
    110111        form = form_class(request.POST, request.FILES)
    111112        if form.is_valid():
    112113            new_object = form.save()
    113             if request.user.is_authenticated():
    114                 request.user.message_set.create(message=ugettext("The %(verbose_name)s was created successfully.") % {"verbose_name": model._meta.verbose_name})
     114           
     115            msg = ugettext("The %(verbose_name)s was created successfully.") %\
     116                                    {"verbose_name": model._meta.verbose_name}
     117            messages.success(request, msg, fail_silently=True)
    115118            return redirect(post_save_redirect, new_object)
    116119    else:
    117120        form = form_class()
     
    152155        form = form_class(request.POST, request.FILES, instance=obj)
    153156        if form.is_valid():
    154157            obj = form.save()
    155             if request.user.is_authenticated():
    156                 request.user.message_set.create(message=ugettext("The %(verbose_name)s was updated successfully.") % {"verbose_name": model._meta.verbose_name})
     158            msg = ugettext("The %(verbose_name)s was updated successfully.") %\
     159                                    {"verbose_name": model._meta.verbose_name}
     160            messages.success(request, msg, fail_silently=True)
    157161            return redirect(post_save_redirect, obj)
    158162    else:
    159163        form = form_class(instance=obj)
     
    194198
    195199    if request.method == 'POST':
    196200        obj.delete()
    197         if request.user.is_authenticated():
    198             request.user.message_set.create(message=ugettext("The %(verbose_name)s was deleted.") % {"verbose_name": model._meta.verbose_name})
     201        msg = ugettext("The %(verbose_name)s was deleted.") %\
     202                                    {"verbose_name": model._meta.verbose_name}
     203        messages.success(request, msg, fail_silently=True)
    199204        return HttpResponseRedirect(post_delete_redirect)
    200205    else:
    201206        if not template_name:
  • docs/index.txt

    diff -r 70e75e8cd224 docs/index.txt
    a b  
    170170    * :ref:`Internationalization <topics-i18n>`
    171171    * :ref:`Jython support <howto-jython>`
    172172    * :ref:`"Local flavor" <ref-contrib-localflavor>`
     173    * :ref:`Messages <ref-contrib-messages>`
    173174    * :ref:`Pagination <topics-pagination>`
    174175    * :ref:`Redirects <ref-contrib-redirects>`
    175176    * :ref:`Serialization <topics-serialization>`
  • docs/internals/deprecation.txt

    diff -r 70e75e8cd224 docs/internals/deprecation.txt
    a b  
    2828        * The many to many SQL generation functions on the database backends
    2929          will be removed.  These have been deprecated since the 1.2 release.
    3030
     31        * The ``Message`` model (in ``django.contrib.auth``), its related
     32          manager in the ``User`` model (``user.message_set``), and the
     33          associated methods (``user.message_set.create()`` and
     34          ``user.get_and_delete_messages()``), which have
     35          been deprecated since the 1.2 release, will be removed.  The
     36          :ref:`messages framework <ref-contrib-messages>` should be used
     37          instead.
     38
    3139    * 2.0
    3240        * ``django.views.defaults.shortcut()``. This function has been moved
    3341          to ``django.contrib.contenttypes.views.shortcut()`` as part of the
  • docs/ref/contrib/index.txt

    diff -r 70e75e8cd224 docs/ref/contrib/index.txt
    a b  
    3434   formtools/index
    3535   humanize
    3636   localflavor
     37   messages
    3738   redirects
    3839   sitemaps
    3940   sites
     
    150151.. _Markdown: http://en.wikipedia.org/wiki/Markdown
    151152.. _ReST (ReStructured Text): http://en.wikipedia.org/wiki/ReStructuredText
    152153
     154messages
     155========
     156
     157.. versionchanged:: 1.2
     158    The messages framework was added.
     159
     160A framework for storing and retrieving temporary cookie- or session-based
     161messages
     162
     163See the :ref:`messages documentation <ref-contrib-messages>`.
     164
    153165redirects
    154166=========
    155167
  • new file docs/ref/contrib/messages.txt

    diff -r 70e75e8cd224 docs/ref/contrib/messages.txt
    - +  
     1.. _ref-contrib-messages:
     2
     3======================
     4The messages framework
     5======================
     6
     7.. module:: django.contrib.messages
     8   :synopsis: Provides cookie- and session-based temporary message storage.
     9
     10Django provides full support for cookie- and session-based messaging, for
     11both anonymous and authenticated clients. The messages framework allows you
     12to temporarily store messages in one request and retrieve them for display
     13in a subsequent request (usually the next one). Every message is tagged
     14with a specific ``level`` that determines its priority (e.g., ``info``,
     15``warning``, or ``error``).
     16
     17.. versionadded:: 1.2
     18   The messages framework was added.
     19
     20Enabling messages
     21=================
     22
     23Messages are implemented through a :ref:`middleware <ref-middleware>`
     24class and corresponding :ref:`context processor <ref-templates-api>`.
     25
     26To enable message functionality, do the following:
     27
     28    * Edit the :setting:`MIDDLEWARE_CLASSES` setting and make sure
     29      it contains ``'django.contrib.messages.middleware.MessageMiddleware'``.
     30
     31      If you are using a :ref:`storage backend <message-storage-backends>` that
     32      relies on :ref:`sessions <topics-http-sessions>` (the default),
     33      ``'django.contrib.sessions.middleware.SessionMiddleware'`` must be
     34      enabled and appear before ``MessageMiddleware`` in your
     35      :setting:`MIDDLEWARE_CLASSES`.
     36
     37    * Edit the :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting and make sure
     38      it contains ``'django.contrib.messages.context_processors.messages'``.
     39
     40    * Add ``'django.contrib.messages'`` to your :setting:`INSTALLED_APPS`
     41      setting
     42
     43The default ``settings.py`` created by ``django-admin.py startproject`` has
     44``MessageMiddleware`` activated and the ``django.contrib.messages`` app
     45installed.  Also, the default value for :setting:`TEMPLATE_CONTEXT_PROCESSORS`
     46contains ``'django.contrib.messages.context_processors.messages'``.
     47
     48If you don't want to use messages, you can remove the
     49``MessageMiddleware`` line from :setting:`MIDDLEWARE_CLASSES`, the ``messages``
     50context processor from :setting:`TEMPLATE_CONTEXT_PROCESSORS` and
     51``'django.contrib.messages'`` from your :setting:`INSTALLED_APPS`.
     52
     53Configuring the message engine
     54==============================
     55
     56.. _message-storage-backends:
     57
     58Storage backends
     59----------------
     60
     61The messages framework can use different backends to store temporary messages.
     62To change which backend is being used, add a `MESSAGE_STORAGE`_ to your
     63settings, referencing the module and class of the storage class. For
     64example::
     65
     66    MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
     67
     68The value should be the full path of the desired storage class.
     69
     70Four storage classes are included:
     71
     72``'django.contrib.messages.storage.session.SessionStorage'``
     73    This class stores all messages inside of the request's session. It
     74    requires Django's ``contrib.session`` application.
     75
     76``'django.contrib.messages.storage.cookie.CookieStorage'``
     77    This class stores the message data in a cookie (signed with a secret hash
     78    to prevent manipulation) to persist notifications across requests. Old
     79    messages are dropped if the cookie data size would exceed 4096 bytes.
     80
     81``'django.contrib.messages.storage.fallback.FallbackStorage'``
     82    This class first uses CookieStorage for all messages, falling back to using
     83    SessionStorage for the messages that could not fit in a single cookie.
     84
     85    Since it is uses SessionStorage, it also requires Django's
     86    ``contrib.session`` application.
     87
     88``'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'``
     89    This is the default temporary storage class.
     90
     91    This class extends FallbackStorage and adds compatibility methods to
     92    to retrieve any messages stored in the user Message model by code that
     93    has not yet been updated to use the new API. This storage is temporary
     94    (because it makes use of code that is pending deprecation) and will be
     95    removed in Django 1.4. At that time, the default storage will become
     96    ``django.contrib.messages.storage.fallback.FallbackStorage``. For more
     97    information, see `LegacyFallbackStorage`_ below.
     98
     99To write your own storage class, subclass the ``BaseStorage`` class in
     100``django.contrib.messages.storage.base`` and implement the ``_get`` and
     101``_store`` methods.
     102
     103LegacyFallbackStorage
     104^^^^^^^^^^^^^^^^^^^^^
     105
     106The ``LegacyFallbackStorage`` is a temporary tool to facilitate the transition
     107from the deprecated ``user.message_set`` API and will be removed in Django 1.4
     108according to Django's standard deprecation policy.  For more information, see
     109the full :ref:`release process documentation <internals-release-process>`.
     110
     111In addition to the functionality in the ``FallbackStorage``, it adds a custom,
     112read-only storage class that retrieves messages from the user ``Message``
     113model. Any messages that were stored in the ``Message`` model (e.g., by code
     114that has not yet been updated to use the messages framework) will be retrieved
     115first, followed by those stored in a cookie and in the session, if any.  Since
     116messages stored in the ``Message`` model do not have a concept of levels, they
     117will be assigned the ``INFO`` level by default.
     118
     119Message levels
     120--------------
     121
     122The messages framework is based on a configurable level architecture similar
     123to that of the Python logging module.  Message levels allow you to group
     124messages by type so they can be filtered or displayed differently in views and
     125templates.
     126
     127The built-in levels (which can be imported from ``django.contrib.messages``
     128directly) are:
     129
     130=========== ========
     131Constant    Purpose
     132=========== ========
     133``DEBUG``   Development-related messages that will be ignored (or removed) in a production deployment
     134``INFO``    Informational messages for the user
     135``SUCCESS`` An action was successful, e.g. "Your profile was updated successfully"
     136``WARNING`` A failure did not occur but may be imminent
     137``ERROR``   An action was **not** successful or some other failure occurred
     138=========== ========
     139
     140The `MESSAGE_LEVEL`_ setting can be used to change the minimum recorded
     141level. Attempts to add messages of a level less than this will be ignored.
     142
     143Message tags
     144------------
     145
     146Message tags are a string representation of the message level plus any
     147extra tags that were added directly in the view (see
     148`Adding extra message tags`_ below for more details).  Tags are stored in a
     149string and are separated by spaces.  Typically, message tags
     150are used as CSS classes to customize message style based on message type. By
     151default, each level has a single tag that's a lowercase version of its own
     152constant:
     153
     154==============  ===========
     155Level Constant  Tag
     156==============  ===========
     157``DEBUG``       ``debug``
     158``INFO``        ``info``
     159``SUCCESS``     ``success``
     160``WARNING``     ``warning``
     161``ERROR``       ``error``
     162==============  ===========
     163
     164To change the default tags for a message level (either built-in or custom),
     165set the `MESSAGE_TAGS`_ setting to a dictionary containing the levels
     166you wish to change. As this extends the default tags, you only need to provide
     167tags for the levels you wish to override::
     168
     169    from django.contrib.messages import constants as messages
     170    MESSAGE_TAGS = {
     171        messages.INFO: '',
     172        50: 'critical',
     173    }
     174
     175Using messages in views and templates
     176=====================================
     177
     178Adding a message
     179----------------
     180
     181To add a message, call::
     182
     183    from django.contrib import messages
     184    messages.add_message(request, messages.INFO, 'Hello world.')
     185
     186Some shortcut methods provide a standard way to add messages with commonly
     187used tags (which are usually represented as HTML classes for the message)::
     188
     189    messages.debug(request, '%s SQL statements were executed.' % count)
     190    messages.info(request, 'Three credits remain in your account.')
     191    messages.success(request, 'Profile details updated.')
     192    messages.warning(request, 'Your account expires in three days.')
     193    messages.error(request, 'Document deleted.')
     194
     195Displaying messages
     196-------------------
     197
     198In your template, use something like::
     199
     200    {% if messages %}
     201    <ul class="messages">
     202        {% for message in messages %}
     203        <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
     204        {% endfor %}
     205    </ul>
     206    {% endif %}
     207
     208If you're using the context processor, your template should be rendered with a
     209``RequestContext``. Otherwise, ensure ``messages`` is available to
     210the template context.
     211
     212Creating custom message levels
     213------------------------------
     214
     215Messages levels are nothing more than integers, so you can define your own
     216level constants and use them to create more customized user feedback, e.g.::
     217
     218    CRITICAL = 50
     219
     220    def my_view(request):
     221        messages.add_message(request, CRITICAL, 'A serious error occurred.')
     222
     223When creating custom message levels you should be careful to avoid overloading
     224existing levels.  The values for the built-in levels are:
     225
     226.. _message-level-constants:
     227
     228==============  =====
     229Level Constant  Value
     230==============  =====
     231``DEBUG``       10
     232``INFO``        20
     233``SUCCESS``     25
     234``WARNING``     30
     235``ERROR``       40
     236==============  =====
     237
     238If you need to identify the custom levels in your HTML or CSS, you need to
     239provide a mapping via the `MESSAGE_TAGS`_ setting.
     240
     241.. note::
     242   If you are creating a reusable application, it is recommended to use
     243   only the built-in `message levels`_ and not rely on any custom levels.
     244
     245Changing the minimum recorded level per-request
     246-----------------------------------------------
     247
     248The minimum recorded level can be set per request by changing the ``level``
     249attribute of the messages storage instance::
     250
     251    from django.contrib import messages
     252
     253    # Change the messages level to ensure the debug message is added.
     254    messages.get_messages(request).level = messages.DEBUG
     255    messages.debug(request, 'Test message...')
     256
     257    # In another request, record only messages with a level of WARNING and higher
     258    messages.get_messages(request).level = messages.WARNING
     259    messages.success(request, 'Your profile was updated.') # ignored
     260    messages.warning(request, 'Your account is about to expire.') # recorded
     261
     262    # Set the messages level back to default.
     263    messages.get_messages(request).level = None
     264
     265For more information on how the minimum recorded level functions, see
     266`Message levels`_ above.
     267
     268Adding extra message tags
     269-------------------------
     270
     271For more direct control over message tags, you can optionally provide a string
     272containing extra tags to any of the add methods::
     273
     274    messages.add_message(request, messages.INFO, 'Over 9000!',
     275                         extra_tags='dragonball')
     276    messages.error(request, 'Email box full', extra_tags='email')
     277
     278Extra tags are added before the default tag for that level and are space
     279separated.
     280
     281Failing silently when the message framework is disabled
     282-------------------------------------------------------
     283
     284If you're writing a reusable app (or other piece of code) and want to include
     285messaging functionality, but don't want to require your users to enable it
     286if they don't want to, you may pass an additional keyword argument
     287``fail_silently=True`` to any of the ``add_message`` family of methods. For
     288example::
     289
     290    messages.add_message(request, messages.SUCCESS, 'Profile details updated.',
     291                         fail_silently=True)
     292    messages.info(request, 'Hello world.', fail_silently=True)
     293
     294Internally, Django uses this functionality in the create, update, and delete
     295:ref:`generic views <topics-generic-views>` so that they work even if the
     296message framework is disabled.
     297
     298.. note::
     299   Setting ``fail_silently=True`` only hides the ``MessageFailure`` that would
     300   otherwise occur when the messages framework disabled and one attempts to
     301   use one of the ``add_message`` family of methods.  It does not hide failures
     302   that may occur for other reasons.
     303
     304Expiration of messages
     305======================
     306
     307The messages are marked to be cleared when the storage instance is iterated
     308(and cleared when the response is processed).
     309
     310To avoid the messages being cleared, you can set the messages storage to
     311``False`` after iterating::
     312
     313    storage = messages.get_messages(request)
     314    for message in storage:
     315        do_something_with(message)
     316    storage.used = False
     317
     318Behavior of parallel requests
     319=============================
     320
     321Due to the way cookies (and hence sessions) work, **the behavior of any
     322backends that make use of cookies or sessions is undefined when the same
     323client makes multiple requests that set or get messages in parallel**.  For
     324example, if a client initiates a request that creates a message in one window
     325(or tab) and then another that fetches any uniterated messages in another
     326window, before the first window redirects, the message may appear in the
     327second window instead of the first window where it may be expected.
     328
     329In short, when multiple simultaneous requests from the same client are
     330involved, messages are not guaranteed to be delivered to the same window that
     331created them nor, in some cases, at all.  Note that this is typically not a
     332problem in most applications and will become a non-issue in HTML5, where each
     333window/tab will have its own browsing context.
     334
     335Settings
     336========
     337
     338A few :ref:`Django settings <ref-settings>` give you control over message
     339behavior:
     340
     341MESSAGE_LEVEL
     342-------------
     343
     344Default: ``messages.INFO``
     345
     346This sets the minimum message that will be saved in the message storage.  See
     347`Message levels`_ above for more details.
     348
     349.. admonition:: Important
     350
     351   If you override ``MESSAGE_LEVEL`` in your settings file and rely on any of
     352   the built-in constants, you must import the constants module directly to
     353   avoid the potential for circular imports, e.g.::
     354
     355       from django.contrib.messages import constants as message_constants
     356       MESSAGE_LEVEL = message_constants.DEBUG
     357
     358   If desired, you may specify the numeric values for the constants directly
     359   according to the values in the above :ref:`constants table
     360   <message-level-constants>`.
     361
     362MESSAGE_STORAGE
     363---------------
     364
     365Default: ``'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'``
     366
     367Controls where Django stores message data. Valid values are:
     368
     369    * ``'django.contrib.messages.storage.fallback.FallbackStorage'``
     370    * ``'django.contrib.messages.storage.session.SessionStorage'``
     371    * ``'django.contrib.messages.storage.cookie.CookieStorage'``
     372    * ``'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'``
     373
     374See `Storage backends`_ for more details.
     375
     376MESSAGE_TAGS
     377------------
     378
     379Default::
     380
     381        {messages.DEBUG: 'debug',
     382        messages.INFO: 'info',
     383        messages.SUCCESS: 'success',
     384        messages.WARNING: 'warning',
     385        messages.ERROR: 'error',}
     386
     387This sets the mapping of message level to message tag, which is typically
     388rendered as a CSS class in HTML. If you specify a value, it will extend
     389the default. This means you only have to specify those values which you need
     390to override. See `Displaying messages`_ above for more details.
     391
     392.. admonition:: Important
     393
     394   If you override ``MESSAGE_TAGS`` in your settings file and rely on any of
     395   the built-in constants, you must import the ``constants`` module directly to
     396   avoid the potential for circular imports, e.g.::
     397
     398       from django.contrib.messages import constants as message_constants
     399       MESSAGE_TAGS = {message_constants.INFO: ''}
     400
     401   If desired, you may specify the numeric values for the constants directly
     402   according to the values in the above :ref:`constants table
     403   <message-level-constants>`.
     404
     405.. _Django settings: ../settings/
  • docs/ref/middleware.txt

    diff -r 70e75e8cd224 docs/ref/middleware.txt
    a b  
    139139content for each user. See the :ref:`internationalization documentation
    140140<topics-i18n>`.
    141141
     142Message middleware
     143------------------
     144
     145.. module:: django.contrib.messages.middleware
     146   :synopsis: Message middleware.
     147
     148.. class:: django.contrib.messages.middleware.MessageMiddleware
     149
     150.. versionadded:: 1.2
     151   ``MessageMiddleware`` was added.
     152   
     153Enables cookie- and session-based message support. See the
     154:ref:`messages documentation <ref-contrib-messages>`.
     155
    142156Session middleware
    143157------------------
    144158
  • docs/ref/settings.txt

    diff -r 70e75e8cd224 docs/ref/settings.txt
    a b  
    812812
    813813.. setting:: MIDDLEWARE_CLASSES
    814814
     815MESSAGE_LEVEL
     816-------------
     817
     818.. versionadded:: 1.2
     819
     820Default: `messages.INFO`
     821
     822Sets the minimum message level that will be recorded by the messages
     823framework. See the :ref:`messages documentation <ref-contrib-messages>` for
     824more details.
     825
     826MESSAGE_STORAGE
     827---------------
     828
     829.. versionadded:: 1.2
     830
     831Default: ``'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'``
     832
     833Controls where Django stores message data.  See the
     834:ref:`messages documentation <ref-contrib-messages>` for more details.
     835
     836MESSAGE_TAGS
     837------------
     838
     839.. versionadded:: 1.2
     840
     841Default::
     842
     843        {messages.DEBUG: 'debug',
     844        messages.INFO: 'info',
     845        messages.SUCCESS: 'success',
     846        messages.WARNING: 'warning',
     847        messages.ERROR: 'error',}
     848
     849Sets the mapping of message levels to message tags. See the
     850:ref:`messages documentation <ref-contrib-messages>` for more details.
     851
    815852MIDDLEWARE_CLASSES
    816853------------------
    817854
     
    820857    ('django.middleware.common.CommonMiddleware',
    821858     'django.contrib.sessions.middleware.SessionMiddleware',
    822859     'django.middleware.csrf.CsrfViewMiddleware',
    823      'django.contrib.auth.middleware.AuthenticationMiddleware',)
     860     'django.contrib.auth.middleware.AuthenticationMiddleware',
     861     'django.contrib.messages.middleware.MessageMiddleware',)
    824862
    825863A tuple of middleware classes to use. See :ref:`topics-http-middleware`.
    826864
     865.. versionchanged:: 1.2
     866   ``'django.contrib.messages.middleware.MessageMiddleware'`` was added to the
     867   default.  For more information, see the :ref:`messages documentation
     868   <ref-contrib-messages>`.
     869
    827870.. setting:: MONTH_DAY_FORMAT
    828871
    829872MONTH_DAY_FORMAT
     
    10591102    ("django.core.context_processors.auth",
    10601103    "django.core.context_processors.debug",
    10611104    "django.core.context_processors.i18n",
    1062     "django.core.context_processors.media")
     1105    "django.core.context_processors.media",
     1106    "django.contrib.messages.context_processors.messages")
    10631107
    10641108A tuple of callables that are used to populate the context in ``RequestContext``.
    10651109These callables take a request object as their argument and return a dictionary
    10661110of items to be merged into the context.
    10671111
     1112.. versionchanged:: 1.2
     1113   ``"django.contrib.messages.context_processors.messages"`` was added to the
     1114   default.  For more information, see the :ref:`messages documentation
     1115   <ref-contrib-messages>`.
     1116
    10681117.. setting:: TEMPLATE_DEBUG
    10691118
    10701119TEMPLATE_DEBUG
  • docs/ref/templates/api.txt

    diff -r 70e75e8cd224 docs/ref/templates/api.txt
    a b  
    311311    ("django.core.context_processors.auth",
    312312    "django.core.context_processors.debug",
    313313    "django.core.context_processors.i18n",
    314     "django.core.context_processors.media")
     314    "django.core.context_processors.media",
     315    "django.contrib.messages.context_processors.messages")
    315316
    316317.. versionadded:: 1.2
    317318   In addition to these, ``RequestContext`` always uses
     
    320321   in case of accidental misconfiguration, it is deliberately hardcoded in and
    321322   cannot be turned off by the :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting.
    322323
     324.. versionadded:: 1.2
     325   The ``'messages'`` context processor was added.  For more information, see
     326   the :ref:`messages documentation <ref-contrib-messages>`.
     327
    323328Each processor is applied in order. That means, if one processor adds a
    324329variable to the context and a second processor adds a variable with the same
    325330name, the second will override the first. The default processors are explained
     
    365370      logged-in user (or an ``AnonymousUser`` instance, if the client isn't
    366371      logged in).
    367372
    368     * ``messages`` -- A list of messages (as strings) for the currently
    369       logged-in user. Behind the scenes, this calls
    370       ``request.user.get_and_delete_messages()`` for every request. That method
    371       collects the user's messages and deletes them from the database.
    372 
    373       Note that messages are set with ``user.message_set.create``.
     373    * ``messages`` -- A list of messages (as strings) that have been set
     374      via the :ref:`messages framework <ref-contrib-messages>`.
    374375
    375376    * ``perms`` -- An instance of
    376377      ``django.core.context_processors.PermWrapper``, representing the
    377378      permissions that the currently logged-in user has.
    378379
     380.. versionchanged:: 1.2
     381   Prior to version 1.2, the ``messages`` variable was a lazy accessor for
     382   ``user.get_and_delete_messages()``. It has been changed to include any
     383   messages added via the :ref:`messages framework <ref-contrib-messages`.
     384
    379385django.core.context_processors.debug
    380386~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    381387
     
    427433:class:`~django.http.HttpRequest`. Note that this processor is not enabled by default;
    428434you'll have to activate it.
    429435
     436django.contrib.messages.context_processors.messages
     437~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     438
     439If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every
     440``RequestContext`` will contain a single additional variable:
     441
     442    * ``messages`` -- A list of messages (as strings) that have been set
     443      via the user model (using ``user.message_set.create``) or through
     444      the :ref:`messages framework <ref-contrib-messages>`.
     445
     446.. versionadded:: 1.2
     447   This template context variable was previously supplied by the ``'auth'``
     448   context processor.  For backwards compatibility the ``'auth'`` context
     449   processor will continue to supply the ``messages`` variable until Django
     450   1.4.  If you use the ``messages`` variable, your project will work with
     451   either (or both) context processors, but it is recommended to add
     452   ``django.contrib.messages.context_processors.messages`` so your project
     453   will be prepared for the future upgrade.
     454
    430455Writing your own context processors
    431456~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    432457
  • docs/releases/1.2.txt

    diff -r 70e75e8cd224 docs/releases/1.2.txt
    a b  
    121121:meth:`~django.core.mail.get_connection()` call::
    122122
    123123    connection = get_connection('django.core.mail.backends.smtp', hostname='localhost', port=1234)
     124   
     125User Messages API
     126-----------------
     127
     128The API for storing messages in the user ``Message`` model (via
     129``user.message_set.create``) is now deprecated and will be removed in Django
     1301.4 according to the standard :ref:`release process <internals-release-process>`.
     131
     132To upgrade your code, you need to replace any instances of::
     133
     134    user.message_set.create('a message')
     135
     136with the following::
     137
     138    from django.contrib import messages
     139    messages.add_message(request, messages.INFO, 'a message')
     140
     141Additionally, if you make use of the method, you need to replace the
     142following::
     143
     144    for message in user.get_and_delete_messages():
     145        ...
     146   
     147with::
     148
     149    from django.contrib import messages
     150    for message in messages.get_messages(request):
     151        ...
     152   
     153For more information, see the full
     154:ref:`messages documentation <ref-contrib-messages>`. You should begin to
     155update your code to use the new API immediately.
    124156
    125157What's new in Django 1.2
    126158========================
     
    155187:ref:`memory<topic-email-memory-backend>` - you can even configure all
    156188e-mail to be :ref:`thrown away<topic-email-dummy-backend>`.
    157189
     190Messages Framework
     191-------------------------
     192
     193Django now includes a robust and configurable
     194:ref:`messages framework <ref-contrib-messages>` with built-in support for
     195cookie- and session-based messaging, for both anonymous and authenticated
     196clients. The messages framework replaces the deprecated user message API and
     197allows you to temporarily store messages in one request and retrieve them for
     198display in a subsequent request (usually the next one).
  • docs/topics/auth.txt

    diff -r 70e75e8cd224 docs/topics/auth.txt
    a b  
    2323      user.
    2424    * Messages: A simple way to queue messages for given users.
    2525
     26.. deprecated:: 1.2
     27   The Messages component of the auth system will be removed in Django 1.4.
     28 
    2629Installation
    2730============
    2831
     
    12891292Messages
    12901293========
    12911294
     1295.. deprecated:: 1.2
     1296   This functionality will be removed in Django 1.4.  You should use the
     1297   :ref:`messages framework <ref-contrib-messages>` for all new projects and
     1298   begin to update your existing code immediately.
     1299
    12921300The message system is a lightweight way to queue messages for given users.
    12931301
    12941302A message is associated with a :class:`~django.contrib.auth.models.User`.
     
    13341342    </ul>
    13351343    {% endif %}
    13361344
    1337 Note that :class:`~django.template.context.RequestContext` calls
    1338 :meth:`~django.contrib.auth.models.User.get_and_delete_messages` behind the
    1339 scenes, so any messages will be deleted even if you don't display them.
     1345.. versionchanged:: 1.2
     1346   The ``messages`` template variable uses a backwards compatible method in the
     1347   :ref:`messages framework <ref-contrib-messages>` to retrieve messages from
     1348   both the user ``Message`` model and from the new framework.  Unlike in
     1349   previous revisions, the messages will not be erased unless they are actually
     1350   displayed.
    13401351
    13411352Finally, note that this messages framework only works with users in the user
    13421353database. To send messages to anonymous users, use the
    1343 :ref:`session framework <topics-http-sessions>`.
     1354:ref:`messages framework <ref-contrib-messages>`.
    13441355
    13451356.. _authentication-backends:
    13461357
  • tests/runtests.py

    diff -r 70e75e8cd224 tests/runtests.py
    a b  
    2828    'django.contrib.flatpages',
    2929    'django.contrib.redirects',
    3030    'django.contrib.sessions',
     31    'django.contrib.messages',
    3132    'django.contrib.comments',
    3233    'django.contrib.admin',
    3334]
     
    107108    settings.MIDDLEWARE_CLASSES = (
    108109        'django.contrib.sessions.middleware.SessionMiddleware',
    109110        'django.contrib.auth.middleware.AuthenticationMiddleware',
     111        'django.contrib.messages.middleware.MessageMiddleware',
    110112        'django.middleware.common.CommonMiddleware',
    111113    )
    112114    settings.SITE_ID = 1
Back to Top