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

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

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

  • django/conf/global_settings.py

    diff -r 1fb6476dce9d 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)
  • django/conf/project_template/settings.py

    diff -r 1fb6476dce9d 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 1fb6476dce9d 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 1fb6476dce9d 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 1fb6476dce9d 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 1fb6476dce9d 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 1fb6476dce9d django/contrib/messages/__init__.py
    - +  
     1from api import *
     2from constants import *
  • new file django/contrib/messages/api.py

    diff -r 1fb6476dce9d 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 AddMessageFailure(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 AddMessageFailure('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 1fb6476dce9d 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 1fb6476dce9d 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 1fb6476dce9d django/contrib/messages/middleware.py
    - +  
     1from django.conf import settings
     2from django.contrib.messages.storage import Storage
     3
     4
     5class MessageMiddleware(object):
     6    """
     7    Middleware that handles temporary messages.
     8    """
     9
     10    def process_request(self, request):
     11        request._messages = 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 1fb6476dce9d django/contrib/messages/models.py
    - +  
     1# Models module required so tests are discovered.
  • new file django/contrib/messages/storage/__init__.py

    diff -r 1fb6476dce9d django/contrib/messages/storage/__init__.py
    - +  
     1from django.conf import settings
     2from django.utils.importlib import import_module
     3
     4
     5def get_storage(import_path):
     6    """
     7    Imports the message storage class described by import_path, where
     8    import_path is the full Python path to the class.
     9    """
     10    try:
     11        dot = import_path.rindex('.')
     12    except ValueError:
     13        raise ImproperlyConfigured("%s isn't a Python path." % import_path)
     14    module, classname = import_path[:dot], import_path[dot+1:]
     15    try:
     16        mod = import_module(module)
     17    except ImportError, e:
     18        raise ImproperlyConfigured('Error importing module %s: "%s"' % (module, e))
     19    try:
     20        return getattr(mod, classname)
     21    except AttributeError:
     22        raise ImproperlyConfigured('Module "%s" does not define a "%s" class.' % (module, classname))
     23
     24
     25Storage = get_storage(getattr(settings, 'MESSAGE_STORAGE',
     26    'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'))
  • new file django/contrib/messages/storage/base.py

    diff -r 1fb6476dce9d 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 and
     95        a flag indicating whether or not all the messages originally intended
     96        to be stored in this storage were, in fact, stored and now retrieved.
     97       
     98        **This method must be implemented by a subclass.**
     99       
     100        If it is possible to tell if the backend was not used (as opposed to
     101        just containing no messages) then ``None`` should be returned.
     102        """
     103        raise NotImplementedError()
     104
     105    def _store(self, messages, response, *args, **kwargs):
     106        """
     107        Stores a list of messages, returning a list of any messages which could
     108        not be stored.
     109       
     110        One type of object must be able to be stored, ``Message``.
     111       
     112        **This method must be implemented by a subclass.**
     113        """
     114        raise NotImplementedError()
     115
     116    def _prepare_messages(self, messages):
     117        """
     118        Prepares a list of messages for storage.
     119        """
     120        for message in messages:
     121            message._prepare()
     122
     123    def update(self, response):
     124        """
     125        Stores all unread messages.
     126       
     127        If the backend has yet to be iterated, previously stored messages will
     128        be stored again. Otherwise, only messages added after the last
     129        iteration will be stored.
     130        """
     131        self._prepare_messages(self._queued_messages)
     132        if self.used:
     133            return self._store(self._queued_messages, response)
     134        elif self.added_new:
     135            messages = self._loaded_messages + self._queued_messages
     136            return self._store(messages, response)
     137
     138    def add(self, level, message, extra_tags=''):
     139        """
     140        Queues a message to be stored.
     141       
     142        The message is only queued if it contained something and its level is
     143        not less than the recording level (``self.level``).
     144        """
     145        if not message:
     146            return
     147        # Check that the message level is not less than the recording level.
     148        level = int(level)
     149        if level < self.level:
     150            return
     151        # Add the message.
     152        self.added_new = True
     153        message = Message(level, message, extra_tags=extra_tags)
     154        self._queued_messages.append(message)
     155
     156    def _get_level(self):
     157        """
     158        Returns the minimum recorded level.
     159       
     160        The default level is the ``MESSAGE_LEVEL`` setting. If this is
     161        not found, the ``INFO`` level is used.
     162        """
     163        if not hasattr(self, '_level'):
     164            self._level = getattr(settings, 'MESSAGE_LEVEL', constants.INFO)
     165        return self._level
     166
     167    def _set_level(self, value=None):
     168        """
     169        Sets a custom minimum recorded level.
     170       
     171        If set to ``None``, the default level will be used (see the
     172        ``_get_level`` method).
     173        """
     174        if value is None and hasattr(self, '_level'):
     175            del self._level
     176        else:
     177            self._level = int(value)
     178
     179    level = property(_get_level, _set_level, _set_level)
  • new file django/contrib/messages/storage/cookie.py

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

    diff -r 1fb6476dce9d 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(
     53                    messages,
     54                    response,
     55                    remove_oldest=False,
     56                )
     57            # Even if there are no more messages, continue iterating to ensure
     58            # storages which contained messages are flushed.
     59            elif storage in self._used_storages:
     60                storage._store([], response)
     61                self._used_storages.remove(storage)
     62        return messages
  • new file django/contrib/messages/storage/session.py

    diff -r 1fb6476dce9d 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 MIDDLEWARE_CLASSES "\
     14            "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 1fb6476dce9d 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/utils.py

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

    diff -r 1fb6476dce9d 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
     150Enables cookie- and session-based message support. See the
     151:ref:`messages documentation <ref-contrib-messages>`.
     152
    142153Session middleware
    143154------------------
    144155
  • docs/ref/settings.txt

    diff -r 1fb6476dce9d 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
     
    10591097    ("django.core.context_processors.auth",
    10601098    "django.core.context_processors.debug",
    10611099    "django.core.context_processors.i18n",
    1062     "django.core.context_processors.media")
     1100    "django.core.context_processors.media",
     1101    "django.contrib.messages.context_processors.messages")
    10631102
    10641103A tuple of callables that are used to populate the context in ``RequestContext``.
    10651104These callables take a request object as their argument and return a dictionary
  • docs/ref/templates/api.txt

    diff -r 1fb6476dce9d 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 1fb6476dce9d docs/releases/1.2.txt
    a b  
    7272Features deprecated in 1.2
    7373==========================
    7474
    75 None.
     75User Messages API
     76-----------------
     77
     78The API for storing messages in the user ``Message`` model (via
     79``user.message_set.create``) is now deprecated and will be removed in Django
     801.4 according to the standard :ref:`release process <internals-release-process>`.
     81
     82To upgrade your code, you need to replace any instances of::
     83
     84    user.message_set.create('a message')
     85
     86with the following::
     87
     88    from django.contrib import messages
     89    messages.add_message(request, messages.INFO, 'a message')
     90
     91Additionally, if you make use of the method, you need to replace the
     92following::
     93
     94    for message in user.get_and_delete_messages():
     95        ...
     96   
     97with::
     98
     99    from django.contrib import messages
     100    for message in messages.get_messages(request):
     101        ...
     102   
     103For more information, see the full
     104:ref:`messages documentation <ref-contrib-messages>`. You should begin to
     105update your code to use the new API immediately.
    76106
    77107What's new in Django 1.2
    78108========================
     
    107137:ref:`memory<topic-email-memory-backend>` - you can even configure all
    108138email to be :ref:`thrown away<topic-email-console-backend>`.
    109139
     140Messages Framework
     141-------------------------
     142
     143Django now includes a robust and configurable
     144:ref:`messages framework <ref-contrib-messages>` with built-in support for
     145cookie- and session-based messaging, for both anonymous and authenticated
     146clients. The messages framework replaces the deprecated user message API and
     147allows you to temporarily store messages in one request and retrieve them for
     148display in a subsequent request (usually the next one).
  • docs/topics/auth.txt

    diff -r 1fb6476dce9d 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.  It is recommended to use
     1297   the :ref:`messages framework <ref-contrib-messages>` for new projects
     1298   whenever possible.
     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
  • new file tests/regressiontests/message_tests/models.py

    diff -r 1fb6476dce9d tests/regressiontests/message_tests/models.py
    - +  
     1# empty models so tests are discovered
     2 No newline at end of file
  • new file tests/regressiontests/message_tests/templates/message_tests/show.html

    diff -r 1fb6476dce9d tests/regressiontests/message_tests/templates/message_tests/show.html
    - +  
     1{% if messages %}
     2<ul class="messages">
     3        {% for message in messages %}
     4        <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
     5        {% endfor %}
     6</ul>
     7{% endif %}
     8 No newline at end of file
  • new file tests/regressiontests/message_tests/tests/__init__.py

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

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

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

    diff -r 1fb6476dce9d tests/regressiontests/message_tests/tests/fallback.py
    - +  
     1from django.contrib.messages import constants
     2from django.contrib.messages.storage.fallback import FallbackStorage, \
     3    CookieStorage
     4from regressiontests.message_tests.tests.base import BaseTest
     5from regressiontests.message_tests.tests.cookie import set_cookie_data, \
     6    stored_cookie_messages_count
     7from regressiontests.message_tests.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 tests/regressiontests/message_tests/tests/middleware.py

    diff -r 1fb6476dce9d tests/regressiontests/message_tests/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 tests/regressiontests/message_tests/tests/session.py

    diff -r 1fb6476dce9d tests/regressiontests/message_tests/tests/session.py
    - +  
     1from regressiontests.message_tests.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 tests/regressiontests/message_tests/tests/user_messages.py

    diff -r 1fb6476dce9d tests/regressiontests/message_tests/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 regressiontests.message_tests.tests.cookie import set_cookie_data
     6from regressiontests.message_tests.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 tests/regressiontests/message_tests/urls.py

    diff -r 1fb6476dce9d tests/regressiontests/message_tests/urls.py
    - +  
     1from django.conf.urls.defaults import *
     2
     3urlpatterns = patterns('regressiontests.message_tests.views',
     4    ('^add/(debug|info|success|warning|error)/$', 'add'),
     5    ('^show/$', 'show'),
     6)
  • new file tests/regressiontests/message_tests/views.py

    diff -r 1fb6476dce9d tests/regressiontests/message_tests/views.py
    - +  
     1from django.contrib import messages
     2from django.core.urlresolvers import reverse
     3from django.http import HttpResponseRedirect
     4from django.shortcuts import render_to_response
     5from django.template import RequestContext
     6
     7def add(request, message_type):
     8    # don't default to False here, because we want to test that it defaults
     9    # to False if unspecified
     10    fail_silently = request.POST.get('fail_silently', None)
     11    for msg in request.POST.getlist('messages'):
     12        if fail_silently is not None:
     13            getattr(messages, message_type)(request, msg,
     14                                            fail_silently=fail_silently)
     15        else:
     16            getattr(messages, message_type)(request, msg)
     17    show_url = reverse('regressiontests.message_tests.views.show')
     18    return HttpResponseRedirect(show_url)
     19
     20
     21def show(request):
     22    return render_to_response('message_tests/show.html', {},
     23                              context_instance=RequestContext(request))
     24 No newline at end of file
  • tests/runtests.py

    diff -r 1fb6476dce9d 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