summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Gaynor <alex.gaynor@gmail.com>2009-12-09 17:23:16 +0000
committerAlex Gaynor <alex.gaynor@gmail.com>2009-12-09 17:23:16 +0000
commitbee835fa445dfc6eca624a63fdea8db7e9a5493d (patch)
treefe37aed51fd365bde4280bea5998b267696f5b2b
parent2e900d47f284afcb663b6687f9a4cca7c7e2eeb4 (diff)
downloaddjango-bee835fa445dfc6eca624a63fdea8db7e9a5493d.tar.gz
[soc2009/multidb] Merged up to trunk r11804.
git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/multidb@11805 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--AUTHORS3
-rw-r--r--TODO1
-rw-r--r--django/conf/__init__.py2
-rw-r--r--django/conf/global_settings.py12
-rw-r--r--django/conf/project_template/settings.py2
-rw-r--r--django/contrib/admin/options.py5
-rw-r--r--django/contrib/admin/sites.py2
-rw-r--r--django/contrib/admin/views/template.py3
-rw-r--r--django/contrib/auth/admin.py8
-rw-r--r--django/contrib/auth/models.py10
-rw-r--r--django/contrib/contenttypes/generic.py5
-rw-r--r--django/contrib/messages/__init__.py2
-rw-r--r--django/contrib/messages/api.py84
-rw-r--r--django/contrib/messages/constants.py13
-rw-r--r--django/contrib/messages/context_processors.py8
-rw-r--r--django/contrib/messages/middleware.py26
-rw-r--r--django/contrib/messages/models.py1
-rw-r--r--django/contrib/messages/storage/__init__.py31
-rw-r--r--django/contrib/messages/storage/base.py181
-rw-r--r--django/contrib/messages/storage/cookie.py143
-rw-r--r--django/contrib/messages/storage/fallback.py59
-rw-r--r--django/contrib/messages/storage/session.py33
-rw-r--r--django/contrib/messages/storage/user_messages.py64
-rw-r--r--django/contrib/messages/tests/__init__.py6
-rw-r--r--django/contrib/messages/tests/base.py375
-rw-r--r--django/contrib/messages/tests/cookie.py100
-rw-r--r--django/contrib/messages/tests/fallback.py173
-rw-r--r--django/contrib/messages/tests/middleware.py18
-rw-r--r--django/contrib/messages/tests/session.py38
-rw-r--r--django/contrib/messages/tests/urls.py39
-rw-r--r--django/contrib/messages/tests/user_messages.py65
-rw-r--r--django/contrib/messages/utils.py11
-rw-r--r--django/core/context_processors.py5
-rw-r--r--django/core/mail/__init__.py2
-rw-r--r--django/db/models/query.py30
-rw-r--r--django/db/models/sql/query.py1
-rw-r--r--django/views/generic/create_update.py17
-rw-r--r--docs/index.txt1
-rw-r--r--docs/internals/contributing.txt41
-rw-r--r--docs/internals/deprecation.txt8
-rw-r--r--docs/ref/contrib/csrf.txt2
-rw-r--r--docs/ref/contrib/index.txt12
-rw-r--r--docs/ref/contrib/messages.txt405
-rw-r--r--docs/ref/middleware.txt14
-rw-r--r--docs/ref/settings.txt53
-rw-r--r--docs/ref/templates/api.txt39
-rw-r--r--docs/releases/1.2.txt117
-rw-r--r--docs/topics/auth.txt19
-rw-r--r--docs/topics/email.txt54
-rw-r--r--tests/modeltests/basic/models.py8
-rw-r--r--tests/regressiontests/backends/tests.py2
-rw-r--r--tests/regressiontests/generic_inline_admin/tests.py7
-rwxr-xr-xtests/runtests.py2
53 files changed, 2286 insertions, 76 deletions
diff --git a/AUTHORS b/AUTHORS
index ae34fbecae..27fe91bcc5 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -60,6 +60,7 @@ answer newbie questions, and generally made Django that much better:
Ned Batchelder <http://www.nedbatchelder.com/>
batiste@dosimple.ch
Batman
+ Chris Beaven <http://smileychris.tactful.co.nz/>
Brian Beck <http://blog.brianbeck.com/>
Shannon -jj Behrens <http://jjinux.blogspot.com/>
Esdras Beleza <linux@esdrasbeleza.com>
@@ -299,6 +300,7 @@ answer newbie questions, and generally made Django that much better:
Jason McBrayer <http://www.carcosa.net/jason/>
Kevin McConnell <kevin.mcconnell@gmail.com>
mccutchen@gmail.com
+ Tobias McNulty <http://www.caktusgroup.com/blog>
Christian Metts
michael.mcewan@gmail.com
michal@plovarna.cz
@@ -391,7 +393,6 @@ answer newbie questions, and generally made Django that much better:
Jozko Skrablin <jozko.skrablin@gmail.com>
Ben Slavin <benjamin.slavin@gmail.com>
sloonz <simon.lipp@insa-lyon.fr>
- SmileyChris <smileychris@gmail.com>
Warren Smith <warren@wandrsmith.net>
smurf@smurf.noris.de
Vsevolod Solovyov
diff --git a/TODO b/TODO
index 4ddbeb6a50..4a0aad16cb 100644
--- a/TODO
+++ b/TODO
@@ -7,6 +7,7 @@ Required for v1.2
* Finalize the sql.Query internals
* Clean up the use of db.backend.query_class()
* Verify it still works with GeoDjango
+ * Modify the admin interface to support multiple databases (doh).
Optional for v1.2
~~~~~~~~~~~~~~~~~
diff --git a/django/conf/__init__.py b/django/conf/__init__.py
index 7fbfd26534..8dd028414a 100644
--- a/django/conf/__init__.py
+++ b/django/conf/__init__.py
@@ -127,7 +127,7 @@ class UserSettingsHolder(object):
return getattr(self.default_settings, name)
def __dir__(self):
- return dir(self) + dir(self.default_settings)
+ return self.__dict__.keys() + dir(self.default_settings)
# For Python < 2.6:
__members__ = property(lambda self: self.__dir__())
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 0f96eadf45..9a08348fa5 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -175,6 +175,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.i18n',
'django.core.context_processors.media',
# 'django.core.context_processors.request',
+ 'django.contrib.messages.context_processors.messages',
)
# Output to use in template system for invalid (e.g. misspelled) variables.
@@ -311,6 +312,7 @@ MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
# 'django.middleware.http.ConditionalGetMiddleware',
# 'django.middleware.gzip.GZipMiddleware',
)
@@ -396,6 +398,16 @@ CSRF_FAILURE_VIEW = 'django.views.csrf.csrf_failure'
CSRF_COOKIE_NAME = 'csrftoken'
CSRF_COOKIE_DOMAIN = None
+############
+# MESSAGES #
+############
+
+# Class to use as messges backend
+MESSAGE_STORAGE = 'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'
+
+# Default values of MESSAGE_LEVEL and MESSAGE_TAGS are defined within
+# django.contrib.messages to avoid imports in this settings file.
+
###########
# TESTING #
###########
diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py
index 7cba5de26d..6a46f7a820 100644
--- a/django/conf/project_template/settings.py
+++ b/django/conf/project_template/settings.py
@@ -66,6 +66,7 @@ MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
)
ROOT_URLCONF = '{{ project_name }}.urls'
@@ -81,4 +82,5 @@ INSTALLED_APPS = (
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
+ 'django.contrib.messages',
)
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 0c663a1c80..aab5ddbb6c 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.admin import widgets
from django.contrib.admin import helpers
from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_ngettext, model_format_dict
+from django.contrib import messages
from django.views.decorators.csrf import csrf_protect
from django.core.exceptions import PermissionDenied
from django.db import models, transaction
@@ -541,9 +542,9 @@ class ModelAdmin(BaseModelAdmin):
def message_user(self, request, message):
"""
Send a message to the user. The default implementation
- posts a message using the auth Message object.
+ posts a message using the django.contrib.messages backend.
"""
- request.user.message_set.create(message=message)
+ messages.info(request, message)
def save_form(self, request, form, change):
"""
diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
index 52ef57370d..68621e333e 100644
--- a/django/contrib/admin/sites.py
+++ b/django/contrib/admin/sites.py
@@ -452,7 +452,7 @@ class AdminSite(object):
import warnings
warnings.warn(
"AdminSite.root() is deprecated; use include(admin.site.urls) instead.",
- PendingDeprecationWarning
+ DeprecationWarning
)
#
diff --git a/django/contrib/admin/views/template.py b/django/contrib/admin/views/template.py
index e1f64957c6..f6fa384fc5 100644
--- a/django/contrib/admin/views/template.py
+++ b/django/contrib/admin/views/template.py
@@ -6,6 +6,7 @@ from django.contrib.sites.models import Site
from django.conf import settings
from django.utils.importlib import import_module
from django.utils.translation import ugettext_lazy as _
+from django.contrib import messages
def template_validator(request):
@@ -23,7 +24,7 @@ def template_validator(request):
form = TemplateValidatorForm(settings_modules, site_list,
data=request.POST)
if form.is_valid():
- request.user.message_set.create(message='The template is valid.')
+ messages.info(request, 'The template is valid.')
else:
form = TemplateValidatorForm(settings_modules, site_list)
return render_to_response('admin/template_validator.html', {
diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py
index 34691c00cd..b581abc8b9 100644
--- a/django/contrib/auth/admin.py
+++ b/django/contrib/auth/admin.py
@@ -3,6 +3,7 @@ from django.conf import settings
from django.contrib import admin
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AdminPasswordChangeForm
from django.contrib.auth.models import User, Group
+from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseRedirect, Http404
from django.shortcuts import render_to_response, get_object_or_404
@@ -67,12 +68,13 @@ class UserAdmin(admin.ModelAdmin):
msg = _('The %(name)s "%(obj)s" was added successfully.') % {'name': 'user', 'obj': new_user}
self.log_addition(request, new_user)
if "_addanother" in request.POST:
- request.user.message_set.create(message=msg)
+ messages.success(request, msg)
return HttpResponseRedirect(request.path)
elif '_popup' in request.REQUEST:
return self.response_add(request, new_user)
else:
- request.user.message_set.create(message=msg + ' ' + ugettext("You may edit it again below."))
+ messages.success(request, msg + ' ' +
+ ugettext("You may edit it again below."))
return HttpResponseRedirect('../%s/' % new_user.id)
else:
form = self.add_form()
@@ -104,7 +106,7 @@ class UserAdmin(admin.ModelAdmin):
if form.is_valid():
new_user = form.save()
msg = ugettext('Password changed successfully.')
- request.user.message_set.create(message=msg)
+ messages.success(request, msg)
return HttpResponseRedirect('..')
else:
form = self.change_password_form(user)
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py
index e337bec262..55fbb39bec 100644
--- a/django/contrib/auth/models.py
+++ b/django/contrib/auth/models.py
@@ -288,6 +288,14 @@ class User(models.Model):
raise SiteProfileNotAvailable
return self._profile_cache
+ def _get_message_set(self):
+ import warnings
+ warnings.warn('The user messaging API is deprecated. Please update'
+ ' your code to use the new messages framework.',
+ category=PendingDeprecationWarning)
+ return self._message_set
+ message_set = property(_get_message_set)
+
class Message(models.Model):
"""
The message system is a lightweight way to queue messages for given
@@ -297,7 +305,7 @@ class Message(models.Model):
actions. For example, "The poll Foo was created successfully." is a
message.
"""
- user = models.ForeignKey(User)
+ user = models.ForeignKey(User, related_name='_message_set')
message = models.TextField(_('message'))
def __unicode__(self):
diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py
index f6c463db04..93e85b7965 100644
--- a/django/contrib/contenttypes/generic.py
+++ b/django/contrib/contenttypes/generic.py
@@ -317,10 +317,13 @@ class BaseGenericInlineFormSet(BaseModelFormSet):
from django.contrib.contenttypes.models import ContentType
if self.instance is None or self.instance.pk is None:
return self.model._default_manager.none()
- return self.model._default_manager.filter(**{
+ qs = self.model._default_manager.filter(**{
self.ct_field.name: ContentType.objects.get_for_model(self.instance),
self.ct_fk_field.name: self.instance.pk,
})
+ if not qs.ordered:
+ qs = qs.order_by(self.model._meta.pk.name)
+ return qs
def save_new(self, form, commit=True):
# Avoid a circular import.
diff --git a/django/contrib/messages/__init__.py b/django/contrib/messages/__init__.py
new file mode 100644
index 0000000000..36a990cc6a
--- /dev/null
+++ b/django/contrib/messages/__init__.py
@@ -0,0 +1,2 @@
+from api import *
+from constants import *
diff --git a/django/contrib/messages/api.py b/django/contrib/messages/api.py
new file mode 100644
index 0000000000..4164f2785b
--- /dev/null
+++ b/django/contrib/messages/api.py
@@ -0,0 +1,84 @@
+from django.contrib.messages import constants
+from django.utils.functional import lazy, memoize
+
+__all__ = (
+ 'add_message', 'get_messages',
+ 'debug', 'info', 'success', 'warning', 'error',
+)
+
+
+class MessageFailure(Exception):
+ pass
+
+
+def add_message(request, level, message, extra_tags='', fail_silently=False):
+ """
+ Attempts to add a message to the request using the 'messages' app, falling
+ back to the user's message_set if MessageMiddleware hasn't been enabled.
+ """
+ if hasattr(request, '_messages'):
+ return request._messages.add(level, message, extra_tags)
+ if hasattr(request, 'user') and request.user.is_authenticated():
+ return request.user.message_set.create(message=message)
+ if not fail_silently:
+ raise MessageFailure('Without the django.contrib.messages '
+ 'middleware, messages can only be added to '
+ 'authenticated users.')
+
+
+def get_messages(request):
+ """
+ Returns the message storage on the request if it exists, otherwise returns
+ user.message_set.all() as the old auth context processor did.
+ """
+ if hasattr(request, '_messages'):
+ return request._messages
+
+ def get_user():
+ if hasattr(request, 'user'):
+ return request.user
+ else:
+ from django.contrib.auth.models import AnonymousUser
+ return AnonymousUser()
+
+ return lazy(memoize(get_user().get_and_delete_messages, {}, 0), list)()
+
+
+def debug(request, message, extra_tags='', fail_silently=False):
+ """
+ Adds a message with the ``DEBUG`` level.
+ """
+ add_message(request, constants.DEBUG, message, extra_tags=extra_tags,
+ fail_silently=fail_silently)
+
+
+def info(request, message, extra_tags='', fail_silently=False):
+ """
+ Adds a message with the ``INFO`` level.
+ """
+ add_message(request, constants.INFO, message, extra_tags=extra_tags,
+ fail_silently=fail_silently)
+
+
+def success(request, message, extra_tags='', fail_silently=False):
+ """
+ Adds a message with the ``SUCCESS`` level.
+ """
+ add_message(request, constants.SUCCESS, message, extra_tags=extra_tags,
+ fail_silently=fail_silently)
+
+
+def warning(request, message, extra_tags='', fail_silently=False):
+ """
+ Adds a message with the ``WARNING`` level.
+ """
+ add_message(request, constants.WARNING, message, extra_tags=extra_tags,
+ fail_silently=fail_silently)
+
+
+def error(request, message, extra_tags='', fail_silently=False):
+ """
+ Adds a message with the ``ERROR`` level.
+ """
+ add_message(request, constants.ERROR, message, extra_tags=extra_tags,
+ fail_silently=fail_silently)
diff --git a/django/contrib/messages/constants.py b/django/contrib/messages/constants.py
new file mode 100644
index 0000000000..bf83bfa13b
--- /dev/null
+++ b/django/contrib/messages/constants.py
@@ -0,0 +1,13 @@
+DEBUG = 10
+INFO = 20
+SUCCESS = 25
+WARNING = 30
+ERROR = 40
+
+DEFAULT_TAGS = {
+ DEBUG: 'debug',
+ INFO: 'info',
+ SUCCESS: 'success',
+ WARNING: 'warning',
+ ERROR: 'error',
+}
diff --git a/django/contrib/messages/context_processors.py b/django/contrib/messages/context_processors.py
new file mode 100644
index 0000000000..26cbcd5853
--- /dev/null
+++ b/django/contrib/messages/context_processors.py
@@ -0,0 +1,8 @@
+from django.contrib.messages.api import get_messages
+
+
+def messages(request):
+ """
+ Returns a lazy 'messages' context variable.
+ """
+ return {'messages': get_messages(request)}
diff --git a/django/contrib/messages/middleware.py b/django/contrib/messages/middleware.py
new file mode 100644
index 0000000000..a84dc6cb11
--- /dev/null
+++ b/django/contrib/messages/middleware.py
@@ -0,0 +1,26 @@
+from django.conf import settings
+from django.contrib.messages.storage import default_storage
+
+
+class MessageMiddleware(object):
+ """
+ Middleware that handles temporary messages.
+ """
+
+ def process_request(self, request):
+ request._messages = default_storage(request)
+
+ def process_response(self, request, response):
+ """
+ Updates the storage backend (i.e., saves the messages).
+
+ If not all messages could not be stored and ``DEBUG`` is ``True``, a
+ ``ValueError`` is raised.
+ """
+ # A higher middleware layer may return a request which does not contain
+ # messages storage, so make no assumption that it will be there.
+ if hasattr(request, '_messages'):
+ unstored_messages = request._messages.update(response)
+ if unstored_messages and settings.DEBUG:
+ raise ValueError('Not all temporary messages could be stored.')
+ return response
diff --git a/django/contrib/messages/models.py b/django/contrib/messages/models.py
new file mode 100644
index 0000000000..4f656f5082
--- /dev/null
+++ b/django/contrib/messages/models.py
@@ -0,0 +1 @@
+# Models module required so tests are discovered.
diff --git a/django/contrib/messages/storage/__init__.py b/django/contrib/messages/storage/__init__.py
new file mode 100644
index 0000000000..ce3971b25c
--- /dev/null
+++ b/django/contrib/messages/storage/__init__.py
@@ -0,0 +1,31 @@
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.importlib import import_module
+
+
+def get_storage(import_path):
+ """
+ Imports the message storage class described by import_path, where
+ import_path is the full Python path to the class.
+ """
+ try:
+ dot = import_path.rindex('.')
+ except ValueError:
+ raise ImproperlyConfigured("%s isn't a Python path." % import_path)
+ module, classname = import_path[:dot], import_path[dot + 1:]
+ try:
+ mod = import_module(module)
+ except ImportError, e:
+ raise ImproperlyConfigured('Error importing module %s: "%s"' %
+ (module, e))
+ try:
+ return getattr(mod, classname)
+ except AttributeError:
+ raise ImproperlyConfigured('Module "%s" does not define a "%s" '
+ 'class.' % (module, classname))
+
+
+# Callable with the same interface as the storage classes i.e. accepts a
+# 'request' object. It is wrapped in a lambda to stop 'settings' being used at
+# the module level
+default_storage = lambda request: get_storage(settings.MESSAGE_STORAGE)(request)
diff --git a/django/contrib/messages/storage/base.py b/django/contrib/messages/storage/base.py
new file mode 100644
index 0000000000..65e8526b5c
--- /dev/null
+++ b/django/contrib/messages/storage/base.py
@@ -0,0 +1,181 @@
+from django.conf import settings
+from django.utils.encoding import force_unicode, StrAndUnicode
+from django.contrib.messages import constants, utils
+
+
+LEVEL_TAGS = utils.get_level_tags()
+
+
+class Message(StrAndUnicode):
+ """
+ Represents an actual message that can be stored in any of the supported
+ storage classes (typically session- or cookie-based) and rendered in a view
+ or template.
+ """
+
+ def __init__(self, level, message, extra_tags=None):
+ self.level = int(level)
+ self.message = message
+ self.extra_tags = extra_tags
+
+ def _prepare(self):
+ """
+ Prepares the message for serialization by forcing the ``message``
+ and ``extra_tags`` to unicode in case they are lazy translations.
+
+ Known "safe" types (None, int, etc.) are not converted (see Django's
+ ``force_unicode`` implementation for details).
+ """
+ self.message = force_unicode(self.message, strings_only=True)
+ self.extra_tags = force_unicode(self.extra_tags, strings_only=True)
+
+ def __eq__(self, other):
+ return isinstance(other, Message) and self.level == other.level and \
+ self.message == other.message
+
+ def __unicode__(self):
+ return force_unicode(self.message)
+
+ def _get_tags(self):
+ label_tag = force_unicode(LEVEL_TAGS.get(self.level, ''),
+ strings_only=True)
+ extra_tags = force_unicode(self.extra_tags, strings_only=True)
+ if extra_tags and label_tag:
+ return u' '.join([extra_tags, label_tag])
+ elif extra_tags:
+ return extra_tags
+ elif label_tag:
+ return label_tag
+ return ''
+ tags = property(_get_tags)
+
+
+class BaseStorage(object):
+ """
+ This is the base backend for temporary message storage.
+
+ This is not a complete class; to be a usable storage backend, it must be
+ subclassed and the two methods ``_get`` and ``_store`` overridden.
+ """
+
+ def __init__(self, request, *args, **kwargs):
+ self.request = request
+ self._queued_messages = []
+ self.used = False
+ self.added_new = False
+ super(BaseStorage, self).__init__(*args, **kwargs)
+
+ def __len__(self):
+ return len(self._loaded_messages) + len(self._queued_messages)
+
+ def __iter__(self):
+ self.used = True
+ if self._queued_messages:
+ self._loaded_messages.extend(self._queued_messages)
+ self._queued_messages = []
+ return iter(self._loaded_messages)
+
+ def __contains__(self, item):
+ return item in self._loaded_messages or item in self._queued_messages
+
+ @property
+ def _loaded_messages(self):
+ """
+ Returns a list of loaded messages, retrieving them first if they have
+ not been loaded yet.
+ """
+ if not hasattr(self, '_loaded_data'):
+ messages, all_retrieved = self._get()
+ self._loaded_data = messages or []
+ return self._loaded_data
+
+ def _get(self, *args, **kwargs):
+ """
+ Retrieves a list of stored messages. Returns a tuple of the messages
+ and a flag indicating whether or not all the messages originally
+ intended to be stored in this storage were, in fact, stored and
+ retrieved; e.g., ``(messages, all_retrieved)``.
+
+ **This method must be implemented by a subclass.**
+
+ If it is possible to tell if the backend was not used (as opposed to
+ just containing no messages) then ``None`` should be returned in
+ place of ``messages``.
+ """
+ raise NotImplementedError()
+
+ def _store(self, messages, response, *args, **kwargs):
+ """
+ Stores a list of messages, returning a list of any messages which could
+ not be stored.
+
+ One type of object must be able to be stored, ``Message``.
+
+ **This method must be implemented by a subclass.**
+ """
+ raise NotImplementedError()
+
+ def _prepare_messages(self, messages):
+ """
+ Prepares a list of messages for storage.
+ """
+ for message in messages:
+ message._prepare()
+
+ def update(self, response):
+ """
+ Stores all unread messages.
+
+ If the backend has yet to be iterated, previously stored messages will
+ be stored again. Otherwise, only messages added after the last
+ iteration will be stored.
+ """
+ self._prepare_messages(self._queued_messages)
+ if self.used:
+ return self._store(self._queued_messages, response)
+ elif self.added_new:
+ messages = self._loaded_messages + self._queued_messages
+ return self._store(messages, response)
+
+ def add(self, level, message, extra_tags=''):
+ """
+ Queues a message to be stored.
+
+ The message is only queued if it contained something and its level is
+ not less than the recording level (``self.level``).
+ """
+ if not message:
+ return
+ # Check that the message level is not less than the recording level.
+ level = int(level)
+ if level < self.level:
+ return
+ # Add the message.
+ self.added_new = True
+ message = Message(level, message, extra_tags=extra_tags)
+ self._queued_messages.append(message)
+
+ def _get_level(self):
+ """
+ Returns the minimum recorded level.
+
+ The default level is the ``MESSAGE_LEVEL`` setting. If this is
+ not found, the ``INFO`` level is used.
+ """
+ if not hasattr(self, '_level'):
+ self._level = getattr(settings, 'MESSAGE_LEVEL', constants.INFO)
+ return self._level
+
+ def _set_level(self, value=None):
+ """
+ Sets a custom minimum recorded level.
+
+ If set to ``None``, the default level will be used (see the
+ ``_get_level`` method).
+ """
+ if value is None and hasattr(self, '_level'):
+ del self._level
+ else:
+ self._level = int(value)
+
+ level = property(_get_level, _set_level, _set_level)
diff --git a/django/contrib/messages/storage/cookie.py b/django/contrib/messages/storage/cookie.py
new file mode 100644
index 0000000000..248791d3e7
--- /dev/null
+++ b/django/contrib/messages/storage/cookie.py
@@ -0,0 +1,143 @@
+import hmac
+
+from django.conf import settings
+from django.utils.hashcompat import sha_constructor
+from django.contrib.messages import constants
+from django.contrib.messages.storage.base import BaseStorage, Message
+from django.utils import simplejson as json
+
+
+class MessageEncoder(json.JSONEncoder):
+ """
+ Compactly serializes instances of the ``Message`` class as JSON.
+ """
+ message_key = '__json_message'
+
+ def default(self, obj):
+ if isinstance(obj, Message):
+ message = [self.message_key, obj.level, obj.message]
+ if obj.extra_tags:
+ message.append(obj.extra_tags)
+ return message
+ return super(MessageEncoder, self).default(obj)
+
+
+class MessageDecoder(json.JSONDecoder):
+ """
+ Decodes JSON that includes serialized ``Message`` instances.
+ """
+
+ def process_messages(self, obj):
+ if isinstance(obj, list) and obj:
+ if obj[0] == MessageEncoder.message_key:
+ return Message(*obj[1:])
+ return [self.process_messages(item) for item in obj]
+ if isinstance(obj, dict):
+ return dict([(key, self.process_messages(value))
+ for key, value in obj.iteritems()])
+ return obj
+
+ def decode(self, s, **kwargs):
+ decoded = super(MessageDecoder, self).decode(s, **kwargs)
+ return self.process_messages(decoded)
+
+
+class CookieStorage(BaseStorage):
+ """
+ Stores messages in a cookie.
+ """
+ cookie_name = 'messages'
+ max_cookie_size = 4096
+ not_finished = '__messagesnotfinished__'
+
+ def _get(self, *args, **kwargs):
+ """
+ Retrieves a list of messages from the messages cookie. If the
+ not_finished sentinel value is found at the end of the message list,
+ remove it and return a result indicating that not all messages were
+ retrieved by this storage.
+ """
+ data = self.request.COOKIES.get(self.cookie_name)
+ messages = self._decode(data)
+ all_retrieved = not (messages and messages[-1] == self.not_finished)
+ if messages and not all_retrieved:
+ # remove the sentinel value
+ messages.pop()
+ return messages, all_retrieved
+
+ def _update_cookie(self, encoded_data, response):
+ """
+ Either sets the cookie with the encoded data if there is any data to
+ store, or deletes the cookie.
+ """
+ if encoded_data:
+ response.set_cookie(self.cookie_name, encoded_data)
+ else:
+ response.delete_cookie(self.cookie_name)
+
+ def _store(self, messages, response, remove_oldest=True, *args, **kwargs):
+ """
+ Stores the messages to a cookie, returning a list of any messages which
+ could not be stored.
+
+ If the encoded data is larger than ``max_cookie_size``, removes
+ messages until the data fits (these are the messages which are
+ returned), and add the not_finished sentinel value to indicate as much.
+ """
+ unstored_messages = []
+ encoded_data = self._encode(messages)
+ if self.max_cookie_size:
+ while encoded_data and len(encoded_data) > self.max_cookie_size:
+ if remove_oldest:
+ unstored_messages.append(messages.pop(0))
+ else:
+ unstored_messages.insert(0, messages.pop())
+ encoded_data = self._encode(messages + [self.not_finished],
+ encode_empty=unstored_messages)
+ self._update_cookie(encoded_data, response)
+ return unstored_messages
+
+ def _hash(self, value):
+ """
+ Creates an HMAC/SHA1 hash based on the value and the project setting's
+ SECRET_KEY, modified to make it unique for the present purpose.
+ """
+ key = 'django.contrib.messages' + settings.SECRET_KEY
+ return hmac.new(key, value, sha_constructor).hexdigest()
+
+ def _encode(self, messages, encode_empty=False):
+ """
+ Returns an encoded version of the messages list which can be stored as
+ plain text.
+
+ Since the data will be retrieved from the client-side, the encoded data
+ also contains a hash to ensure that the data was not tampered with.
+ """
+ if messages or encode_empty:
+ encoder = MessageEncoder(separators=(',', ':'))
+ value = encoder.encode(messages)
+ return '%s$%s' % (self._hash(value), value)
+
+ def _decode(self, data):
+ """
+ Safely decodes a encoded text stream back into a list of messages.
+
+ If the encoded text stream contained an invalid hash or was in an
+ invalid format, ``None`` is returned.
+ """
+ if not data:
+ return None
+ bits = data.split('$', 1)
+ if len(bits) == 2:
+ hash, value = bits
+ if hash == self._hash(value):
+ try:
+ # If we get here (and the JSON decode works), everything is
+ # good. In any other case, drop back and return None.
+ return json.loads(value, cls=MessageDecoder)
+ except ValueError:
+ pass
+ # Mark the data as used (so it gets removed) since something was wrong
+ # with the data.
+ self.used = True
+ return None
diff --git a/django/contrib/messages/storage/fallback.py b/django/contrib/messages/storage/fallback.py
new file mode 100644
index 0000000000..94e2decb79
--- /dev/null
+++ b/django/contrib/messages/storage/fallback.py
@@ -0,0 +1,59 @@
+from django.contrib.messages.storage.base import BaseStorage
+from django.contrib.messages.storage.cookie import CookieStorage
+from django.contrib.messages.storage.session import SessionStorage
+try:
+ set
+except NameError:
+ from sets import Set as set # Python 2.3
+
+
+class FallbackStorage(BaseStorage):
+ """
+ Tries to store all messages in the first backend, storing any unstored
+ messages in each subsequent backend backend.
+ """
+ storage_classes = (CookieStorage, SessionStorage)
+
+ def __init__(self, *args, **kwargs):
+ super(FallbackStorage, self).__init__(*args, **kwargs)
+ self.storages = [storage_class(*args, **kwargs)
+ for storage_class in self.storage_classes]
+ self._used_storages = set()
+
+ def _get(self, *args, **kwargs):
+ """
+ Gets a single list of messages from all storage backends.
+ """
+ all_messages = []
+ for storage in self.storages:
+ messages, all_retrieved = storage._get()
+ # If the backend hasn't been used, no more retrieval is necessary.
+ if messages is None:
+ break
+ if messages:
+ self._used_storages.add(storage)
+ all_messages.extend(messages)
+ # If this storage class contained all the messages, no further
+ # retrieval is necessary
+ if all_retrieved:
+ break
+ return all_messages, all_retrieved
+
+ def _store(self, messages, response, *args, **kwargs):
+ """
+ Stores the messages, returning any unstored messages after trying all
+ backends.
+
+ For each storage backend, any messages not stored are passed on to the
+ next backend.
+ """
+ for storage in self.storages:
+ if messages:
+ messages = storage._store(messages, response,
+ remove_oldest=False)
+ # Even if there are no more messages, continue iterating to ensure
+ # storages which contained messages are flushed.
+ elif storage in self._used_storages:
+ storage._store([], response)
+ self._used_storages.remove(storage)
+ return messages
diff --git a/django/contrib/messages/storage/session.py b/django/contrib/messages/storage/session.py
new file mode 100644
index 0000000000..225dfda289
--- /dev/null
+++ b/django/contrib/messages/storage/session.py
@@ -0,0 +1,33 @@
+from django.contrib.messages.storage.base import BaseStorage
+
+
+class SessionStorage(BaseStorage):
+ """
+ Stores messages in the session (that is, django.contrib.sessions).
+ """
+ session_key = '_messages'
+
+ def __init__(self, request, *args, **kwargs):
+ assert hasattr(request, 'session'), "The session-based temporary "\
+ "message storage requires session middleware to be installed, "\
+ "and come before the message middleware in the "\
+ "MIDDLEWARE_CLASSES list."
+ super(SessionStorage, self).__init__(request, *args, **kwargs)
+
+ def _get(self, *args, **kwargs):
+ """
+ Retrieves a list of messages from the request's session. This storage
+ always stores everything it is given, so return True for the
+ all_retrieved flag.
+ """
+ return self.request.session.get(self.session_key), True
+
+ def _store(self, messages, response, *args, **kwargs):
+ """
+ Stores a list of messages to the request's session.
+ """
+ if messages:
+ self.request.session[self.session_key] = messages
+ else:
+ self.request.session.pop(self.session_key, None)
+ return []
diff --git a/django/contrib/messages/storage/user_messages.py b/django/contrib/messages/storage/user_messages.py
new file mode 100644
index 0000000000..17f0c55290
--- /dev/null
+++ b/django/contrib/messages/storage/user_messages.py
@@ -0,0 +1,64 @@
+"""
+Storages used to assist in the deprecation of contrib.auth User messages.
+
+"""
+from django.contrib.messages import constants
+from django.contrib.messages.storage.base import BaseStorage, Message
+from django.contrib.auth.models import User
+from django.contrib.messages.storage.fallback import FallbackStorage
+
+
+class UserMessagesStorage(BaseStorage):
+ """
+ Retrieves messages from the User, using the legacy user.message_set API.
+
+ This storage is "read-only" insofar as it can only retrieve and delete
+ messages, not store them.
+ """
+ session_key = '_messages'
+
+ def _get_messages_queryset(self):
+ """
+ Returns the QuerySet containing all user messages (or ``None`` if
+ request.user is not a contrib.auth User).
+ """
+ user = getattr(self.request, 'user', None)
+ if isinstance(user, User):
+ return user._message_set.all()
+
+ def add(self, *args, **kwargs):
+ raise NotImplementedError('This message storage is read-only.')
+
+ def _get(self, *args, **kwargs):
+ """
+ Retrieves a list of messages assigned to the User. This backend never
+ stores anything, so all_retrieved is assumed to be False.
+ """
+ queryset = self._get_messages_queryset()
+ if queryset is None:
+ # This is a read-only and optional storage, so to ensure other
+ # storages will also be read if used with FallbackStorage an empty
+ # list is returned rather than None.
+ return [], False
+ messages = []
+ for user_message in queryset:
+ messages.append(Message(constants.INFO, user_message.message))
+ return messages, False
+
+ def _store(self, messages, *args, **kwargs):
+ """
+ Removes any messages assigned to the User and returns the list of
+ messages (since no messages are stored in this read-only storage).
+ """
+ queryset = self._get_messages_queryset()
+ if queryset is not None:
+ queryset.delete()
+ return messages
+
+
+class LegacyFallbackStorage(FallbackStorage):
+ """
+ Works like ``FallbackStorage`` but also handles retrieving (and clearing)
+ contrib.auth User messages.
+ """
+ storage_classes = (UserMessagesStorage,) + FallbackStorage.storage_classes
diff --git a/django/contrib/messages/tests/__init__.py b/django/contrib/messages/tests/__init__.py
new file mode 100644
index 0000000000..84581e00e4
--- /dev/null
+++ b/django/contrib/messages/tests/__init__.py
@@ -0,0 +1,6 @@
+from django.contrib.messages.tests.cookie import CookieTest
+from django.contrib.messages.tests.fallback import FallbackTest
+from django.contrib.messages.tests.middleware import MiddlewareTest
+from django.contrib.messages.tests.session import SessionTest
+from django.contrib.messages.tests.user_messages import \
+ UserMessagesTest, LegacyFallbackTest
diff --git a/django/contrib/messages/tests/base.py b/django/contrib/messages/tests/base.py
new file mode 100644
index 0000000000..45e061aedc
--- /dev/null
+++ b/django/contrib/messages/tests/base.py
@@ -0,0 +1,375 @@
+from django import http
+from django.test import TestCase
+from django.conf import settings
+from django.utils.translation import ugettext_lazy
+from django.contrib.messages import constants, utils
+from django.contrib.messages.storage import default_storage, base
+from django.contrib.messages.storage.base import Message
+from django.core.urlresolvers import reverse
+from django.contrib.auth.models import User
+from django.contrib.messages.api import MessageFailure
+
+
+def add_level_messages(storage):
+ """
+ Adds 6 messages from different levels (including a custom one) to a storage
+ instance.
+ """
+ storage.add(constants.INFO, 'A generic info message')
+ storage.add(29, 'Some custom level')
+ storage.add(constants.DEBUG, 'A debugging message', extra_tags='extra-tag')
+ storage.add(constants.WARNING, 'A warning')
+ storage.add(constants.ERROR, 'An error')
+ storage.add(constants.SUCCESS, 'This was a triumph.')
+
+
+class BaseTest(TestCase):
+ storage_class = default_storage
+ restore_settings = ['MESSAGE_LEVEL', 'MESSAGE_TAGS']
+ urls = 'django.contrib.messages.tests.urls'
+ levels = {
+ 'debug': constants.DEBUG,
+ 'info': constants.INFO,
+ 'success': constants.SUCCESS,
+ 'warning': constants.WARNING,
+ 'error': constants.ERROR,
+ }
+
+ def setUp(self):
+ self._remembered_settings = {}
+ for setting in self.restore_settings:
+ if hasattr(settings, setting):
+ self._remembered_settings[setting] = getattr(settings, setting)
+ delattr(settings._wrapped, setting)
+ # backup these manually because we do not want them deleted
+ self._middleware_classes = settings.MIDDLEWARE_CLASSES
+ self._template_context_processors = \
+ settings.TEMPLATE_CONTEXT_PROCESSORS
+ self._installed_apps = settings.INSTALLED_APPS
+
+ def tearDown(self):
+ for setting in self.restore_settings:
+ self.restore_setting(setting)
+ # restore these manually (see above)
+ settings.MIDDLEWARE_CLASSES = self._middleware_classes
+ settings.TEMPLATE_CONTEXT_PROCESSORS = \
+ self._template_context_processors
+ settings.INSTALLED_APPS = self._installed_apps
+
+ def restore_setting(self, setting):
+ if setting in self._remembered_settings:
+ value = self._remembered_settings.pop(setting)
+ setattr(settings, setting, value)
+ elif hasattr(settings, setting):
+ delattr(settings._wrapped, setting)
+
+ def get_request(self):
+ return http.HttpRequest()
+
+ def get_response(self):
+ return http.HttpResponse()
+
+ def get_storage(self, data=None):
+ """
+ Returns the storage backend, setting its loaded data to the ``data``
+ argument.
+
+ This method avoids the storage ``_get`` method from getting called so
+ that other parts of the storage backend can be tested independent of
+ the message retrieval logic.
+ """
+ storage = self.storage_class(self.get_request())
+ storage._loaded_data = data or []
+ return storage
+
+ def test_add(self):
+ storage = self.get_storage()
+ self.assertFalse(storage.added_new)
+ storage.add(constants.INFO, 'Test message 1')
+ self.assert_(storage.added_new)
+ storage.add(constants.INFO, 'Test message 2', extra_tags='tag')
+ self.assertEqual(len(storage), 2)
+
+ def test_add_lazy_translation(self):
+ storage = self.get_storage()
+ response = self.get_response()
+
+ storage.add(constants.INFO, ugettext_lazy('lazy message'))
+ storage.update(response)
+
+ storing = self.stored_messages_count(storage, response)
+ self.assertEqual(storing, 1)
+
+ def test_no_update(self):
+ storage = self.get_storage()
+ response = self.get_response()
+ storage.update(response)
+ storing = self.stored_messages_count(storage, response)
+ self.assertEqual(storing, 0)
+
+ def test_add_update(self):
+ storage = self.get_storage()
+ response = self.get_response()
+
+ storage.add(constants.INFO, 'Test message 1')
+ storage.add(constants.INFO, 'Test message 1', extra_tags='tag')
+ storage.update(response)
+
+ storing = self.stored_messages_count(storage, response)
+ self.assertEqual(storing, 2)
+
+ def test_existing_add_read_update(self):
+ storage = self.get_existing_storage()
+ response = self.get_response()
+
+ storage.add(constants.INFO, 'Test message 3')
+ list(storage) # Simulates a read
+ storage.update(response)
+
+ storing = self.stored_messages_count(storage, response)
+ self.assertEqual(storing, 0)
+
+ def test_existing_read_add_update(self):
+ storage = self.get_existing_storage()
+ response = self.get_response()
+
+ list(storage) # Simulates a read
+ storage.add(constants.INFO, 'Test message 3')
+ storage.update(response)
+
+ storing = self.stored_messages_count(storage, response)
+ self.assertEqual(storing, 1)
+
+ def test_full_request_response_cycle(self):
+ """
+ With the message middleware enabled, tests that messages are properly
+ stored and then retrieved across the full request/redirect/response
+ cycle.
+ """
+ settings.MESSAGE_LEVEL = constants.DEBUG
+ data = {
+ 'messages': ['Test message %d' % x for x in xrange(10)],
+ }
+ show_url = reverse('django.contrib.messages.tests.urls.show')
+ for level in ('debug', 'info', 'success', 'warning', 'error'):
+ add_url = reverse('django.contrib.messages.tests.urls.add',
+ args=(level,))
+ response = self.client.post(add_url, data, follow=True)
+ self.assertRedirects(response, show_url)
+ self.assertTrue('messages' in response.context)
+ messages = [Message(self.levels[level], msg) for msg in
+ data['messages']]
+ self.assertEqual(list(response.context['messages']), messages)
+ for msg in data['messages']:
+ self.assertContains(response, msg)
+
+ def test_multiple_posts(self):
+ """
+ Tests that messages persist properly when multiple POSTs are made
+ before a GET.
+ """
+ settings.MESSAGE_LEVEL = constants.DEBUG
+ data = {
+ 'messages': ['Test message %d' % x for x in xrange(10)],
+ }
+ show_url = reverse('django.contrib.messages.tests.urls.show')
+ messages = []
+ for level in ('debug', 'info', 'success', 'warning', 'error'):
+ messages.extend([Message(self.levels[level], msg) for msg in
+ data['messages']])
+ add_url = reverse('django.contrib.messages.tests.urls.add',
+ args=(level,))
+ self.client.post(add_url, data)
+ response = self.client.get(show_url)
+ self.assertTrue('messages' in response.context)
+ self.assertEqual(list(response.context['messages']), messages)
+ for msg in data['messages']:
+ self.assertContains(response, msg)
+
+ def test_middleware_disabled_auth_user(self):
+ """
+ Tests that the messages API successfully falls back to using
+ user.message_set to store messages directly when the middleware is
+ disabled.
+ """
+ settings.MESSAGE_LEVEL = constants.DEBUG
+ user = User.objects.create_user('test', 'test@example.com', 'test')
+ self.client.login(username='test', password='test')
+ settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
+ settings.INSTALLED_APPS.remove(
+ 'django.contrib.messages',
+ )
+ settings.MIDDLEWARE_CLASSES = list(settings.MIDDLEWARE_CLASSES)
+ settings.MIDDLEWARE_CLASSES.remove(
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ )
+ settings.TEMPLATE_CONTEXT_PROCESSORS = \
+ list(settings.TEMPLATE_CONTEXT_PROCESSORS)
+ settings.TEMPLATE_CONTEXT_PROCESSORS.remove(
+ 'django.contrib.messages.context_processors.messages',
+ )
+ data = {
+ 'messages': ['Test message %d' % x for x in xrange(10)],
+ }
+ show_url = reverse('django.contrib.messages.tests.urls.show')
+ for level in ('debug', 'info', 'success', 'warning', 'error'):
+ add_url = reverse('django.contrib.messages.tests.urls.add',
+ args=(level,))
+ response = self.client.post(add_url, data, follow=True)
+ self.assertRedirects(response, show_url)
+ self.assertTrue('messages' in response.context)
+ self.assertEqual(list(response.context['messages']),
+ data['messages'])
+ for msg in data['messages']:
+ self.assertContains(response, msg)
+
+ def test_middleware_disabled_anon_user(self):
+ """
+ Tests that, when the middleware is disabled and a user is not logged
+ in, an exception is raised when one attempts to store a message.
+ """
+ settings.MESSAGE_LEVEL = constants.DEBUG
+ settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
+ settings.INSTALLED_APPS.remove(
+ 'django.contrib.messages',
+ )
+ settings.MIDDLEWARE_CLASSES = list(settings.MIDDLEWARE_CLASSES)
+ settings.MIDDLEWARE_CLASSES.remove(
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ )
+ settings.TEMPLATE_CONTEXT_PROCESSORS = \
+ list(settings.TEMPLATE_CONTEXT_PROCESSORS)
+ settings.TEMPLATE_CONTEXT_PROCESSORS.remove(
+ 'django.contrib.messages.context_processors.messages',
+ )
+ data = {
+ 'messages': ['Test message %d' % x for x in xrange(10)],
+ }
+ show_url = reverse('django.contrib.messages.tests.urls.show')
+ for level in ('debug', 'info', 'success', 'warning', 'error'):
+ add_url = reverse('django.contrib.messages.tests.urls.add',
+ args=(level,))
+ self.assertRaises(MessageFailure, self.client.post, add_url,
+ data, follow=True)
+
+ def test_middleware_disabled_anon_user_fail_silently(self):
+ """
+ Tests that, when the middleware is disabled and a user is not logged
+ in, an exception is not raised if 'fail_silently' = True
+ """
+ settings.MESSAGE_LEVEL = constants.DEBUG
+ settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
+ settings.INSTALLED_APPS.remove(
+ 'django.contrib.messages',
+ )
+ settings.MIDDLEWARE_CLASSES = list(settings.MIDDLEWARE_CLASSES)
+ settings.MIDDLEWARE_CLASSES.remove(
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ )
+ settings.TEMPLATE_CONTEXT_PROCESSORS = \
+ list(settings.TEMPLATE_CONTEXT_PROCESSORS)
+ settings.TEMPLATE_CONTEXT_PROCESSORS.remove(
+ 'django.contrib.messages.context_processors.messages',
+ )
+ data = {
+ 'messages': ['Test message %d' % x for x in xrange(10)],
+ 'fail_silently': True,
+ }
+ show_url = reverse('django.contrib.messages.tests.urls.show')
+ for level in ('debug', 'info', 'success', 'warning', 'error'):
+ add_url = reverse('django.contrib.messages.tests.urls.add',
+ args=(level,))
+ response = self.client.post(add_url, data, follow=True)
+ self.assertRedirects(response, show_url)
+ self.assertTrue('messages' in response.context)
+ self.assertEqual(list(response.context['messages']), [])
+
+ def stored_messages_count(self, storage, response):
+ """
+ Returns the number of messages being stored after a
+ ``storage.update()`` call.
+ """
+ raise NotImplementedError('This method must be set by a subclass.')
+
+ def test_get(self):
+ raise NotImplementedError('This method must be set by a subclass.')
+
+ def get_existing_storage(self):
+ return self.get_storage([Message(constants.INFO, 'Test message 1'),
+ Message(constants.INFO, 'Test message 2',
+ extra_tags='tag')])
+
+ def test_existing_read(self):
+ """
+ Tests that reading the existing storage doesn't cause the data to be
+ lost.
+ """
+ storage = self.get_existing_storage()
+ self.assertFalse(storage.used)
+ # After iterating the storage engine directly, the used flag is set.
+ data = list(storage)
+ self.assert_(storage.used)
+ # The data does not disappear because it has been iterated.
+ self.assertEqual(data, list(storage))
+
+ def test_existing_add(self):
+ storage = self.get_existing_storage()
+ self.assertFalse(storage.added_new)
+ storage.add(constants.INFO, 'Test message 3')
+ self.assert_(storage.added_new)
+
+ def test_default_level(self):
+ storage = self.get_storage()
+ add_level_messages(storage)
+ self.assertEqual(len(storage), 5)
+
+ def test_low_level(self):
+ storage = self.get_storage()
+ storage.level = 5
+ add_level_messages(storage)
+ self.assertEqual(len(storage), 6)
+
+ def test_high_level(self):
+ storage = self.get_storage()
+ storage.level = 30
+ add_level_messages(storage)
+ self.assertEqual(len(storage), 2)
+
+ def test_settings_level(self):
+ settings.MESSAGE_LEVEL = 29
+ storage = self.get_storage()
+ add_level_messages(storage)
+ self.assertEqual(len(storage), 3)
+
+ def test_tags(self):
+ storage = self.get_storage()
+ storage.level = 0
+ add_level_messages(storage)
+ tags = [msg.tags for msg in storage]
+ self.assertEqual(tags,
+ ['info', '', 'extra-tag debug', 'warning', 'error',
+ 'success'])
+
+ def test_custom_tags(self):
+ settings.MESSAGE_TAGS = {
+ constants.INFO: 'info',
+ constants.DEBUG: '',
+ constants.WARNING: '',
+ constants.ERROR: 'bad',
+ 29: 'custom',
+ }
+ # LEVEL_TAGS is a constant defined in the
+ # django.contrib.messages.storage.base module, so after changing
+ # settings.MESSAGE_TAGS, we need to update that constant too.
+ base.LEVEL_TAGS = utils.get_level_tags()
+ try:
+ storage = self.get_storage()
+ storage.level = 0
+ add_level_messages(storage)
+ tags = [msg.tags for msg in storage]
+ self.assertEqual(tags,
+ ['info', 'custom', 'extra-tag', '', 'bad', 'success'])
+ finally:
+ # Ensure the level tags constant is put back like we found it.
+ self.restore_setting('MESSAGE_TAGS')
+ base.LEVEL_TAGS = utils.get_level_tags()
diff --git a/django/contrib/messages/tests/cookie.py b/django/contrib/messages/tests/cookie.py
new file mode 100644
index 0000000000..0a0bca0568
--- /dev/null
+++ b/django/contrib/messages/tests/cookie.py
@@ -0,0 +1,100 @@
+from django.contrib.messages import constants
+from django.contrib.messages.tests.base import BaseTest
+from django.contrib.messages.storage.cookie import CookieStorage, \
+ MessageEncoder, MessageDecoder
+from django.contrib.messages.storage.base import Message
+from django.utils import simplejson as json
+
+
+def set_cookie_data(storage, messages, invalid=False, encode_empty=False):
+ """
+ Sets ``request.COOKIES`` with the encoded data and removes the storage
+ backend's loaded data cache.
+ """
+ encoded_data = storage._encode(messages, encode_empty=encode_empty)
+ if invalid:
+ # Truncate the first character so that the hash is invalid.
+ encoded_data = encoded_data[1:]
+ storage.request.COOKIES = {CookieStorage.cookie_name: encoded_data}
+ if hasattr(storage, '_loaded_data'):
+ del storage._loaded_data
+
+
+def stored_cookie_messages_count(storage, response):
+ """
+ Returns an integer containing the number of messages stored.
+ """
+ # Get a list of cookies, excluding ones with a max-age of 0 (because
+ # they have been marked for deletion).
+ cookie = response.cookies.get(storage.cookie_name)
+ if not cookie or cookie['max-age'] == 0:
+ return 0
+ data = storage._decode(cookie.value)
+ if not data:
+ return 0
+ if data[-1] == CookieStorage.not_finished:
+ data.pop()
+ return len(data)
+
+
+class CookieTest(BaseTest):
+ storage_class = CookieStorage
+
+ def stored_messages_count(self, storage, response):
+ return stored_cookie_messages_count(storage, response)
+
+ def test_get(self):
+ storage = self.storage_class(self.get_request())
+ # Set initial data.
+ example_messages = ['test', 'me']
+ set_cookie_data(storage, example_messages)
+ # Test that the message actually contains what we expect.
+ self.assertEqual(list(storage), example_messages)
+
+ def test_get_bad_cookie(self):
+ request = self.get_request()
+ storage = self.storage_class(request)
+ # Set initial (invalid) data.
+ example_messages = ['test', 'me']
+ set_cookie_data(storage, example_messages, invalid=True)
+ # Test that the message actually contains what we expect.
+ self.assertEqual(list(storage), [])
+
+ def test_max_cookie_length(self):
+ """
+ Tests that, if the data exceeds what is allowed in a cookie, older
+ messages are removed before saving (and returned by the ``update``
+ method).
+ """
+ storage = self.get_storage()
+ response = self.get_response()
+
+ for i in range(5):
+ storage.add(constants.INFO, str(i) * 900)
+ unstored_messages = storage.update(response)
+
+ cookie_storing = self.stored_messages_count(storage, response)
+ self.assertEqual(cookie_storing, 4)
+
+ self.assertEqual(len(unstored_messages), 1)
+ self.assert_(unstored_messages[0].message == '0' * 900)
+
+ def test_json_encoder_decoder(self):
+ """
+ Tests that an complex nested data structure containing Message
+ instances is properly encoded/decoded by the custom JSON
+ encoder/decoder classes.
+ """
+ messages = [
+ {
+ 'message': Message(constants.INFO, 'Test message'),
+ 'message_list': [Message(constants.INFO, 'message %s') \
+ for x in xrange(5)] + [{'another-message': \
+ Message(constants.ERROR, 'error')}],
+ },
+ Message(constants.INFO, 'message %s'),
+ ]
+ encoder = MessageEncoder(separators=(',', ':'))
+ value = encoder.encode(messages)
+ decoded_messages = json.loads(value, cls=MessageDecoder)
+ self.assertEqual(messages, decoded_messages)
diff --git a/django/contrib/messages/tests/fallback.py b/django/contrib/messages/tests/fallback.py
new file mode 100644
index 0000000000..794cfc4428
--- /dev/null
+++ b/django/contrib/messages/tests/fallback.py
@@ -0,0 +1,173 @@
+from django.contrib.messages import constants
+from django.contrib.messages.storage.fallback import FallbackStorage, \
+ CookieStorage
+from django.contrib.messages.tests.base import BaseTest
+from django.contrib.messages.tests.cookie import set_cookie_data, \
+ stored_cookie_messages_count
+from django.contrib.messages.tests.session import set_session_data, \
+ stored_session_messages_count
+
+
+class FallbackTest(BaseTest):
+ storage_class = FallbackStorage
+
+ def get_request(self):
+ self.session = {}
+ request = super(FallbackTest, self).get_request()
+ request.session = self.session
+ return request
+
+ def get_cookie_storage(self, storage):
+ return storage.storages[-2]
+
+ def get_session_storage(self, storage):
+ return storage.storages[-1]
+
+ def stored_cookie_messages_count(self, storage, response):
+ return stored_cookie_messages_count(self.get_cookie_storage(storage),
+ response)
+
+ def stored_session_messages_count(self, storage, response):
+ return stored_session_messages_count(self.get_session_storage(storage))
+
+ def stored_messages_count(self, storage, response):
+ """
+ Return the storage totals from both cookie and session backends.
+ """
+ total = (self.stored_cookie_messages_count(storage, response) +
+ self.stored_session_messages_count(storage, response))
+ return total
+
+ def test_get(self):
+ request = self.get_request()
+ storage = self.storage_class(request)
+ cookie_storage = self.get_cookie_storage(storage)
+
+ # Set initial cookie data.
+ example_messages = [str(i) for i in range(5)]
+ set_cookie_data(cookie_storage, example_messages)
+
+ # Overwrite the _get method of the fallback storage to prove it is not
+ # used (it would cause a TypeError: 'NoneType' object is not callable).
+ self.get_session_storage(storage)._get = None
+
+ # Test that the message actually contains what we expect.
+ self.assertEqual(list(storage), example_messages)
+
+ def test_get_empty(self):
+ request = self.get_request()
+ storage = self.storage_class(request)
+
+ # Overwrite the _get method of the fallback storage to prove it is not
+ # used (it would cause a TypeError: 'NoneType' object is not callable).
+ self.get_session_storage(storage)._get = None
+
+ # Test that the message actually contains what we expect.
+ self.assertEqual(list(storage), [])
+
+ def test_get_fallback(self):
+ request = self.get_request()
+ storage = self.storage_class(request)
+ cookie_storage = self.get_cookie_storage(storage)
+ session_storage = self.get_session_storage(storage)
+
+ # Set initial cookie and session data.
+ example_messages = [str(i) for i in range(5)]
+ set_cookie_data(cookie_storage, example_messages[:4] +
+ [CookieStorage.not_finished])
+ set_session_data(session_storage, example_messages[4:])
+
+ # Test that the message actually contains what we expect.
+ self.assertEqual(list(storage), example_messages)
+
+ def test_get_fallback_only(self):
+ request = self.get_request()
+ storage = self.storage_class(request)
+ cookie_storage = self.get_cookie_storage(storage)
+ session_storage = self.get_session_storage(storage)
+
+ # Set initial cookie and session data.
+ example_messages = [str(i) for i in range(5)]
+ set_cookie_data(cookie_storage, [CookieStorage.not_finished],
+ encode_empty=True)
+ set_session_data(session_storage, example_messages)
+
+ # Test that the message actually contains what we expect.
+ self.assertEqual(list(storage), example_messages)
+
+ def test_flush_used_backends(self):
+ request = self.get_request()
+ storage = self.storage_class(request)
+ cookie_storage = self.get_cookie_storage(storage)
+ session_storage = self.get_session_storage(storage)
+
+ # Set initial cookie and session data.
+ set_cookie_data(cookie_storage, ['cookie', CookieStorage.not_finished])
+ set_session_data(session_storage, ['session'])
+
+ # When updating, previously used but no longer needed backends are
+ # flushed.
+ response = self.get_response()
+ list(storage)
+ storage.update(response)
+ session_storing = self.stored_session_messages_count(storage, response)
+ self.assertEqual(session_storing, 0)
+
+ def test_no_fallback(self):
+ """
+ Confirms that:
+
+ (1) A short number of messages whose data size doesn't exceed what is
+ allowed in a cookie will all be stored in the CookieBackend.
+
+ (2) If the CookieBackend can store all messages, the SessionBackend
+ won't be written to at all.
+ """
+ storage = self.get_storage()
+ response = self.get_response()
+
+ # Overwrite the _store method of the fallback storage to prove it isn't
+ # used (it would cause a TypeError: 'NoneType' object is not callable).
+ self.get_session_storage(storage)._store = None
+
+ for i in range(5):
+ storage.add(constants.INFO, str(i) * 100)
+ storage.update(response)
+
+ cookie_storing = self.stored_cookie_messages_count(storage, response)
+ self.assertEqual(cookie_storing, 5)
+ session_storing = self.stored_session_messages_count(storage, response)
+ self.assertEqual(session_storing, 0)
+
+ def test_session_fallback(self):
+ """
+ Confirms that, if the data exceeds what is allowed in a cookie,
+ messages which did not fit are stored in the SessionBackend.
+ """
+ storage = self.get_storage()
+ response = self.get_response()
+
+ for i in range(5):
+ storage.add(constants.INFO, str(i) * 900)
+ storage.update(response)
+
+ cookie_storing = self.stored_cookie_messages_count(storage, response)
+ self.assertEqual(cookie_storing, 4)
+ session_storing = self.stored_session_messages_count(storage, response)
+ self.assertEqual(session_storing, 1)
+
+ def test_session_fallback_only(self):
+ """
+ Confirms that large messages, none of which fit in a cookie, are stored
+ in the SessionBackend (and nothing is stored in the CookieBackend).
+ """
+ storage = self.get_storage()
+ response = self.get_response()
+
+ storage.add(constants.INFO, 'x' * 5000)
+ storage.update(response)
+
+ cookie_storing = self.stored_cookie_messages_count(storage, response)
+ self.assertEqual(cookie_storing, 0)
+ session_storing = self.stored_session_messages_count(storage, response)
+ self.assertEqual(session_storing, 1)
diff --git a/django/contrib/messages/tests/middleware.py b/django/contrib/messages/tests/middleware.py
new file mode 100644
index 0000000000..654217a224
--- /dev/null
+++ b/django/contrib/messages/tests/middleware.py
@@ -0,0 +1,18 @@
+import unittest
+from django import http
+from django.contrib.messages.middleware import MessageMiddleware
+
+
+class MiddlewareTest(unittest.TestCase):
+
+ def setUp(self):
+ self.middleware = MessageMiddleware()
+
+ def test_response_without_messages(self):
+ """
+ Makes sure that the response middleware is tolerant of messages not
+ existing on request.
+ """
+ request = http.HttpRequest()
+ response = http.HttpResponse()
+ self.middleware.process_response(request, response)
diff --git a/django/contrib/messages/tests/session.py b/django/contrib/messages/tests/session.py
new file mode 100644
index 0000000000..741f53136d
--- /dev/null
+++ b/django/contrib/messages/tests/session.py
@@ -0,0 +1,38 @@
+from django.contrib.messages.tests.base import BaseTest
+from django.contrib.messages.storage.session import SessionStorage
+
+
+def set_session_data(storage, messages):
+ """
+ Sets the messages into the backend request's session and remove the
+ backend's loaded data cache.
+ """
+ storage.request.session[storage.session_key] = messages
+ if hasattr(storage, '_loaded_data'):
+ del storage._loaded_data
+
+
+def stored_session_messages_count(storage):
+ data = storage.request.session.get(storage.session_key, [])
+ return len(data)
+
+
+class SessionTest(BaseTest):
+ storage_class = SessionStorage
+
+ def get_request(self):
+ self.session = {}
+ request = super(SessionTest, self).get_request()
+ request.session = self.session
+ return request
+
+ def stored_messages_count(self, storage, response):
+ return stored_session_messages_count(storage)
+
+ def test_get(self):
+ storage = self.storage_class(self.get_request())
+ # Set initial data.
+ example_messages = ['test', 'me']
+ set_session_data(storage, example_messages)
+ # Test that the message actually contains what we expect.
+ self.assertEqual(list(storage), example_messages)
diff --git a/django/contrib/messages/tests/urls.py b/django/contrib/messages/tests/urls.py
new file mode 100644
index 0000000000..6252adcfb1
--- /dev/null
+++ b/django/contrib/messages/tests/urls.py
@@ -0,0 +1,39 @@
+from django.conf.urls.defaults import *
+from django.contrib import messages
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect, HttpResponse
+from django.shortcuts import render_to_response
+from django.template import RequestContext, Template
+
+
+def add(request, message_type):
+ # don't default to False here, because we want to test that it defaults
+ # to False if unspecified
+ fail_silently = request.POST.get('fail_silently', None)
+ for msg in request.POST.getlist('messages'):
+ if fail_silently is not None:
+ getattr(messages, message_type)(request, msg,
+ fail_silently=fail_silently)
+ else:
+ getattr(messages, message_type)(request, msg)
+ show_url = reverse('django.contrib.messages.tests.urls.show')
+ return HttpResponseRedirect(show_url)
+
+
+def show(request):
+ t = Template("""{% if messages %}
+<ul class="messages">
+ {% for message in messages %}
+ <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
+ {{ message }}
+ </li>
+ {% endfor %}
+</ul>
+{% endif %}""")
+ return HttpResponse(t.render(RequestContext(request)))
+
+
+urlpatterns = patterns('',
+ ('^add/(debug|info|success|warning|error)/$', add),
+ ('^show/$', show),
+)
diff --git a/django/contrib/messages/tests/user_messages.py b/django/contrib/messages/tests/user_messages.py
new file mode 100644
index 0000000000..8d7aeb76bb
--- /dev/null
+++ b/django/contrib/messages/tests/user_messages.py
@@ -0,0 +1,65 @@
+from django import http
+from django.contrib.auth.models import User
+from django.contrib.messages.storage.user_messages import UserMessagesStorage,\
+ LegacyFallbackStorage
+from django.contrib.messages.tests.cookie import set_cookie_data
+from django.contrib.messages.tests.fallback import FallbackTest
+from django.test import TestCase
+
+
+class UserMessagesTest(TestCase):
+
+ def setUp(self):
+ self.user = User.objects.create(username='tester')
+
+ def test_add(self):
+ storage = UserMessagesStorage(http.HttpRequest())
+ self.assertRaises(NotImplementedError, storage.add, 'Test message 1')
+
+ def test_get_anonymous(self):
+ # Ensure that the storage still works if no user is attached to the
+ # request.
+ storage = UserMessagesStorage(http.HttpRequest())
+ self.assertEqual(len(storage), 0)
+
+ def test_get(self):
+ storage = UserMessagesStorage(http.HttpRequest())
+ storage.request.user = self.user
+ self.user.message_set.create(message='test message')
+
+ self.assertEqual(len(storage), 1)
+ self.assertEqual(list(storage)[0].message, 'test message')
+
+
+class LegacyFallbackTest(FallbackTest, TestCase):
+ storage_class = LegacyFallbackStorage
+
+ def setUp(self):
+ super(LegacyFallbackTest, self).setUp()
+ self.user = User.objects.create(username='tester')
+
+ def get_request(self, *args, **kwargs):
+ request = super(LegacyFallbackTest, self).get_request(*args, **kwargs)
+ request.user = self.user
+ return request
+
+ def test_get_legacy_only(self):
+ request = self.get_request()
+ storage = self.storage_class(request)
+ self.user.message_set.create(message='user message')
+
+ # Test that the message actually contains what we expect.
+ self.assertEqual(len(storage), 1)
+ self.assertEqual(list(storage)[0].message, 'user message')
+
+ def test_get_legacy(self):
+ request = self.get_request()
+ storage = self.storage_class(request)
+ cookie_storage = self.get_cookie_storage(storage)
+ self.user.message_set.create(message='user message')
+ set_cookie_data(cookie_storage, ['cookie'])
+
+ # Test that the message actually contains what we expect.
+ self.assertEqual(len(storage), 2)
+ self.assertEqual(list(storage)[0].message, 'user message')
+ self.assertEqual(list(storage)[1], 'cookie')
diff --git a/django/contrib/messages/utils.py b/django/contrib/messages/utils.py
new file mode 100644
index 0000000000..838860b497
--- /dev/null
+++ b/django/contrib/messages/utils.py
@@ -0,0 +1,11 @@
+from django.conf import settings
+from django.contrib.messages import constants
+
+
+def get_level_tags():
+ """
+ Returns the message level tags.
+ """
+ level_tags = constants.DEFAULT_TAGS.copy()
+ level_tags.update(getattr(settings, 'MESSAGE_TAGS', {}))
+ return level_tags
diff --git a/django/core/context_processors.py b/django/core/context_processors.py
index b950dba0f6..eedf601217 100644
--- a/django/core/context_processors.py
+++ b/django/core/context_processors.py
@@ -10,6 +10,7 @@ RequestContext.
from django.conf import settings
from django.middleware.csrf import get_token
from django.utils.functional import lazy, memoize, SimpleLazyObject
+from django.contrib import messages
def auth(request):
"""
@@ -37,8 +38,8 @@ def auth(request):
return {
'user': SimpleLazyObject(get_user),
- 'messages': lazy(memoize(lambda: get_user().get_and_delete_messages(), {}, 0), list)(),
- 'perms': lazy(lambda: PermWrapper(get_user()), PermWrapper)(),
+ 'messages': messages.get_messages(request),
+ 'perms': lazy(lambda: PermWrapper(get_user()), PermWrapper)(),
}
def csrf(request):
diff --git a/django/core/mail/__init__.py b/django/core/mail/__init__.py
index b02575793d..9a629035cf 100644
--- a/django/core/mail/__init__.py
+++ b/django/core/mail/__init__.py
@@ -105,6 +105,6 @@ class SMTPConnection(_SMTPConnection):
import warnings
warnings.warn(
'mail.SMTPConnection is deprecated; use mail.get_connection() instead.',
- DeprecationWarning
+ PendingDeprecationWarning
)
super(SMTPConnection, self).__init__(*args, **kwds)
diff --git a/django/db/models/query.py b/django/db/models/query.py
index 747f5c118c..eba6f2f142 100644
--- a/django/db/models/query.py
+++ b/django/db/models/query.py
@@ -110,6 +110,36 @@ class QuerySet(object):
return False
return True
+ def __contains__(self, val):
+ # The 'in' operator works without this method, due to __iter__. This
+ # implementation exists only to shortcut the creation of Model
+ # instances, by bailing out early if we find a matching element.
+ pos = 0
+ if self._result_cache is not None:
+ if val in self._result_cache:
+ return True
+ elif self._iter is None:
+ # iterator is exhausted, so we have our answer
+ return False
+ # remember not to check these again:
+ pos = len(self._result_cache)
+ else:
+ # We need to start filling the result cache out. The following
+ # ensures that self._iter is not None and self._result_cache is not
+ # None
+ it = iter(self)
+
+ # Carry on, one result at a time.
+ while True:
+ if len(self._result_cache) <= pos:
+ self._fill_cache(num=1)
+ if self._iter is None:
+ # we ran out of items
+ return False
+ if self._result_cache[pos] == val:
+ return True
+ pos += 1
+
def __getitem__(self, k):
"""
Retrieves an item or slice from the set of results.
diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py
index d1217bf048..0a53fc3bae 100644
--- a/django/db/models/sql/query.py
+++ b/django/db/models/sql/query.py
@@ -22,6 +22,7 @@ from django.db.models.sql.expressions import SQLEvaluator
from django.db.models.sql.where import WhereNode, Constraint, EverythingNode, AND, OR
from django.core.exceptions import FieldError
+
__all__ = ['Query']
class Query(object):
diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py
index f1f718e79d..76575f9f89 100644
--- a/django/views/generic/create_update.py
+++ b/django/views/generic/create_update.py
@@ -6,6 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
from django.utils.translation import ugettext
from django.contrib.auth.views import redirect_to_login
from django.views.generic import GenericViewError
+from django.contrib import messages
def apply_extra_context(extra_context, context):
@@ -110,8 +111,10 @@ def create_object(request, model=None, template_name=None,
form = form_class(request.POST, request.FILES)
if form.is_valid():
new_object = form.save()
- if request.user.is_authenticated():
- request.user.message_set.create(message=ugettext("The %(verbose_name)s was created successfully.") % {"verbose_name": model._meta.verbose_name})
+
+ msg = ugettext("The %(verbose_name)s was created successfully.") %\
+ {"verbose_name": model._meta.verbose_name}
+ messages.success(request, msg, fail_silently=True)
return redirect(post_save_redirect, new_object)
else:
form = form_class()
@@ -152,8 +155,9 @@ def update_object(request, model=None, object_id=None, slug=None,
form = form_class(request.POST, request.FILES, instance=obj)
if form.is_valid():
obj = form.save()
- if request.user.is_authenticated():
- request.user.message_set.create(message=ugettext("The %(verbose_name)s was updated successfully.") % {"verbose_name": model._meta.verbose_name})
+ msg = ugettext("The %(verbose_name)s was updated successfully.") %\
+ {"verbose_name": model._meta.verbose_name}
+ messages.success(request, msg, fail_silently=True)
return redirect(post_save_redirect, obj)
else:
form = form_class(instance=obj)
@@ -194,8 +198,9 @@ def delete_object(request, model, post_delete_redirect, object_id=None,
if request.method == 'POST':
obj.delete()
- if request.user.is_authenticated():
- request.user.message_set.create(message=ugettext("The %(verbose_name)s was deleted.") % {"verbose_name": model._meta.verbose_name})
+ msg = ugettext("The %(verbose_name)s was deleted.") %\
+ {"verbose_name": model._meta.verbose_name}
+ messages.success(request, msg, fail_silently=True)
return HttpResponseRedirect(post_delete_redirect)
else:
if not template_name:
diff --git a/docs/index.txt b/docs/index.txt
index 12d93526ae..4b14b74917 100644
--- a/docs/index.txt
+++ b/docs/index.txt
@@ -171,6 +171,7 @@ Other batteries included
* :ref:`Internationalization <topics-i18n>`
* :ref:`Jython support <howto-jython>`
* :ref:`"Local flavor" <ref-contrib-localflavor>`
+ * :ref:`Messages <ref-contrib-messages>`
* :ref:`Pagination <topics-pagination>`
* :ref:`Redirects <ref-contrib-redirects>`
* :ref:`Serialization <topics-serialization>`
diff --git a/docs/internals/contributing.txt b/docs/internals/contributing.txt
index 5bd4b0f6f1..4649e4c8b4 100644
--- a/docs/internals/contributing.txt
+++ b/docs/internals/contributing.txt
@@ -426,6 +426,47 @@ translated, here's what to do:
.. _Django i18n mailing list: http://groups.google.com/group/django-i18n/
+Django conventions
+==================
+
+Various Django-specific code issues are detailed in this section.
+
+Use of ``django.conf.settings``
+-------------------------------
+
+Modules should not in general use settings stored in ``django.conf.settings`` at
+the top level (i.e. evaluated when the module is imported). The explanation for
+this is as follows:
+
+Manual configuration of settings (i.e. not relying on the
+``DJANGO_SETTINGS_MODULE`` environment variable) is allowed and possible as
+follows::
+
+ from django.conf import settings
+
+ settings.configure({}, SOME_SETTING='foo')
+
+However, if any setting is accessed before the ``settings.configure`` line, this
+will not work. (Internally, ``setttings`` is a ``LazyObject`` which configures
+itself automatically when the settings are accessed if it has not already been
+configured).
+
+So, if there is a module containg some code as follows::
+
+ from django.conf import settings
+ from django.core.urlresolvers import get_callable
+
+ default_foo_view = get_callable(settings.FOO_VIEW)
+
+...then importing this module will cause the settings object to be configured.
+That means that the ability for third parties to import the module at the top
+level is incompatible with the ability to configure the settings object
+manually, or makes it very difficult in some circumstances.
+
+Instead of the above code, a level of laziness or indirection must be used, such
+as :class:`django.utils.functional.LazyObject`, :func:`django.utils.functional.lazy` or
+``lambda``.
+
Coding style
============
diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt
index 3ab71e97b4..685bc4a396 100644
--- a/docs/internals/deprecation.txt
+++ b/docs/internals/deprecation.txt
@@ -40,6 +40,14 @@ their deprecation, as per the :ref:`Django deprecation policy
multiple databases. In 1.4, the support functions that allow methods
with the old prototype to continue working will be removed.
+ * The ``Message`` model (in ``django.contrib.auth``), its related
+ manager in the ``User`` model (``user.message_set``), and the
+ associated methods (``user.message_set.create()`` and
+ ``user.get_and_delete_messages()``), which have
+ been deprecated since the 1.2 release, will be removed. The
+ :ref:`messages framework <ref-contrib-messages>` should be used
+ instead.
+
* 2.0
* ``django.views.defaults.shortcut()``. This function has been moved
to ``django.contrib.contenttypes.views.shortcut()`` as part of the
diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt
index b76ec2f4f0..ea76fc3739 100644
--- a/docs/ref/contrib/csrf.txt
+++ b/docs/ref/contrib/csrf.txt
@@ -153,6 +153,8 @@ launch a CSRF attack on your site against that user. The
``@csrf_response_exempt`` decorator can be used to fix this, but only if the
page doesn't also contain internal forms that require the token.
+.. _ref-csrf-upgrading-notes:
+
Upgrading notes
---------------
diff --git a/docs/ref/contrib/index.txt b/docs/ref/contrib/index.txt
index 4f401d6836..a8e1427678 100644
--- a/docs/ref/contrib/index.txt
+++ b/docs/ref/contrib/index.txt
@@ -34,6 +34,7 @@ those packages have.
formtools/index
humanize
localflavor
+ messages
redirects
sitemaps
sites
@@ -150,6 +151,17 @@ read the source code in django/contrib/markup/templatetags/markup.py.
.. _Markdown: http://en.wikipedia.org/wiki/Markdown
.. _ReST (ReStructured Text): http://en.wikipedia.org/wiki/ReStructuredText
+messages
+========
+
+.. versionchanged:: 1.2
+ The messages framework was added.
+
+A framework for storing and retrieving temporary cookie- or session-based
+messages
+
+See the :ref:`messages documentation <ref-contrib-messages>`.
+
redirects
=========
diff --git a/docs/ref/contrib/messages.txt b/docs/ref/contrib/messages.txt
new file mode 100644
index 0000000000..20b509388c
--- /dev/null
+++ b/docs/ref/contrib/messages.txt
@@ -0,0 +1,405 @@
+.. _ref-contrib-messages:
+
+======================
+The messages framework
+======================
+
+.. module:: django.contrib.messages
+ :synopsis: Provides cookie- and session-based temporary message storage.
+
+Django provides full support for cookie- and session-based messaging, for
+both anonymous and authenticated clients. The messages framework allows you
+to temporarily store messages in one request and retrieve them for display
+in a subsequent request (usually the next one). Every message is tagged
+with a specific ``level`` that determines its priority (e.g., ``info``,
+``warning``, or ``error``).
+
+.. versionadded:: 1.2
+ The messages framework was added.
+
+Enabling messages
+=================
+
+Messages are implemented through a :ref:`middleware <ref-middleware>`
+class and corresponding :ref:`context processor <ref-templates-api>`.
+
+To enable message functionality, do the following:
+
+ * Edit the :setting:`MIDDLEWARE_CLASSES` setting and make sure
+ it contains ``'django.contrib.messages.middleware.MessageMiddleware'``.
+
+ If you are using a :ref:`storage backend <message-storage-backends>` that
+ relies on :ref:`sessions <topics-http-sessions>` (the default),
+ ``'django.contrib.sessions.middleware.SessionMiddleware'`` must be
+ enabled and appear before ``MessageMiddleware`` in your
+ :setting:`MIDDLEWARE_CLASSES`.
+
+ * Edit the :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting and make sure
+ it contains ``'django.contrib.messages.context_processors.messages'``.
+
+ * Add ``'django.contrib.messages'`` to your :setting:`INSTALLED_APPS`
+ setting
+
+The default ``settings.py`` created by ``django-admin.py startproject`` has
+``MessageMiddleware`` activated and the ``django.contrib.messages`` app
+installed. Also, the default value for :setting:`TEMPLATE_CONTEXT_PROCESSORS`
+contains ``'django.contrib.messages.context_processors.messages'``.
+
+If you don't want to use messages, you can remove the
+``MessageMiddleware`` line from :setting:`MIDDLEWARE_CLASSES`, the ``messages``
+context processor from :setting:`TEMPLATE_CONTEXT_PROCESSORS` and
+``'django.contrib.messages'`` from your :setting:`INSTALLED_APPS`.
+
+Configuring the message engine
+==============================
+
+.. _message-storage-backends:
+
+Storage backends
+----------------
+
+The messages framework can use different backends to store temporary messages.
+To change which backend is being used, add a `MESSAGE_STORAGE`_ to your
+settings, referencing the module and class of the storage class. For
+example::
+
+ MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
+
+The value should be the full path of the desired storage class.
+
+Four storage classes are included:
+
+``'django.contrib.messages.storage.session.SessionStorage'``
+ This class stores all messages inside of the request's session. It
+ requires Django's ``contrib.session`` application.
+
+``'django.contrib.messages.storage.cookie.CookieStorage'``
+ This class stores the message data in a cookie (signed with a secret hash
+ to prevent manipulation) to persist notifications across requests. Old
+ messages are dropped if the cookie data size would exceed 4096 bytes.
+
+``'django.contrib.messages.storage.fallback.FallbackStorage'``
+ This class first uses CookieStorage for all messages, falling back to using
+ SessionStorage for the messages that could not fit in a single cookie.
+
+ Since it is uses SessionStorage, it also requires Django's
+ ``contrib.session`` application.
+
+``'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'``
+ This is the default temporary storage class.
+
+ This class extends FallbackStorage and adds compatibility methods to
+ to retrieve any messages stored in the user Message model by code that
+ has not yet been updated to use the new API. This storage is temporary
+ (because it makes use of code that is pending deprecation) and will be
+ removed in Django 1.4. At that time, the default storage will become
+ ``django.contrib.messages.storage.fallback.FallbackStorage``. For more
+ information, see `LegacyFallbackStorage`_ below.
+
+To write your own storage class, subclass the ``BaseStorage`` class in
+``django.contrib.messages.storage.base`` and implement the ``_get`` and
+``_store`` methods.
+
+LegacyFallbackStorage
+^^^^^^^^^^^^^^^^^^^^^
+
+The ``LegacyFallbackStorage`` is a temporary tool to facilitate the transition
+from the deprecated ``user.message_set`` API and will be removed in Django 1.4
+according to Django's standard deprecation policy. For more information, see
+the full :ref:`release process documentation <internals-release-process>`.
+
+In addition to the functionality in the ``FallbackStorage``, it adds a custom,
+read-only storage class that retrieves messages from the user ``Message``
+model. Any messages that were stored in the ``Message`` model (e.g., by code
+that has not yet been updated to use the messages framework) will be retrieved
+first, followed by those stored in a cookie and in the session, if any. Since
+messages stored in the ``Message`` model do not have a concept of levels, they
+will be assigned the ``INFO`` level by default.
+
+Message levels
+--------------
+
+The messages framework is based on a configurable level architecture similar
+to that of the Python logging module. Message levels allow you to group
+messages by type so they can be filtered or displayed differently in views and
+templates.
+
+The built-in levels (which can be imported from ``django.contrib.messages``
+directly) are:
+
+=========== ========
+Constant Purpose
+=========== ========
+``DEBUG`` Development-related messages that will be ignored (or removed) in a production deployment
+``INFO`` Informational messages for the user
+``SUCCESS`` An action was successful, e.g. "Your profile was updated successfully"
+``WARNING`` A failure did not occur but may be imminent
+``ERROR`` An action was **not** successful or some other failure occurred
+=========== ========
+
+The `MESSAGE_LEVEL`_ setting can be used to change the minimum recorded
+level. Attempts to add messages of a level less than this will be ignored.
+
+Message tags
+------------
+
+Message tags are a string representation of the message level plus any
+extra tags that were added directly in the view (see
+`Adding extra message tags`_ below for more details). Tags are stored in a
+string and are separated by spaces. Typically, message tags
+are used as CSS classes to customize message style based on message type. By
+default, each level has a single tag that's a lowercase version of its own
+constant:
+
+============== ===========
+Level Constant Tag
+============== ===========
+``DEBUG`` ``debug``
+``INFO`` ``info``
+``SUCCESS`` ``success``
+``WARNING`` ``warning``
+``ERROR`` ``error``
+============== ===========
+
+To change the default tags for a message level (either built-in or custom),
+set the `MESSAGE_TAGS`_ setting to a dictionary containing the levels
+you wish to change. As this extends the default tags, you only need to provide
+tags for the levels you wish to override::
+
+ from django.contrib.messages import constants as messages
+ MESSAGE_TAGS = {
+ messages.INFO: '',
+ 50: 'critical',
+ }
+
+Using messages in views and templates
+=====================================
+
+Adding a message
+----------------
+
+To add a message, call::
+
+ from django.contrib import messages
+ messages.add_message(request, messages.INFO, 'Hello world.')
+
+Some shortcut methods provide a standard way to add messages with commonly
+used tags (which are usually represented as HTML classes for the message)::
+
+ messages.debug(request, '%s SQL statements were executed.' % count)
+ messages.info(request, 'Three credits remain in your account.')
+ messages.success(request, 'Profile details updated.')
+ messages.warning(request, 'Your account expires in three days.')
+ messages.error(request, 'Document deleted.')
+
+Displaying messages
+-------------------
+
+In your template, use something like::
+
+ {% if messages %}
+ <ul class="messages">
+ {% for message in messages %}
+ <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
+ {% endfor %}
+ </ul>
+ {% endif %}
+
+If you're using the context processor, your template should be rendered with a
+``RequestContext``. Otherwise, ensure ``messages`` is available to
+the template context.
+
+Creating custom message levels
+------------------------------
+
+Messages levels are nothing more than integers, so you can define your own
+level constants and use them to create more customized user feedback, e.g.::
+
+ CRITICAL = 50
+
+ def my_view(request):
+ messages.add_message(request, CRITICAL, 'A serious error occurred.')
+
+When creating custom message levels you should be careful to avoid overloading
+existing levels. The values for the built-in levels are:
+
+.. _message-level-constants:
+
+============== =====
+Level Constant Value
+============== =====
+``DEBUG`` 10
+``INFO`` 20
+``SUCCESS`` 25
+``WARNING`` 30
+``ERROR`` 40
+============== =====
+
+If you need to identify the custom levels in your HTML or CSS, you need to
+provide a mapping via the `MESSAGE_TAGS`_ setting.
+
+.. note::
+ If you are creating a reusable application, it is recommended to use
+ only the built-in `message levels`_ and not rely on any custom levels.
+
+Changing the minimum recorded level per-request
+-----------------------------------------------
+
+The minimum recorded level can be set per request by changing the ``level``
+attribute of the messages storage instance::
+
+ from django.contrib import messages
+
+ # Change the messages level to ensure the debug message is added.
+ messages.get_messages(request).level = messages.DEBUG
+ messages.debug(request, 'Test message...')
+
+ # In another request, record only messages with a level of WARNING and higher
+ messages.get_messages(request).level = messages.WARNING
+ messages.success(request, 'Your profile was updated.') # ignored
+ messages.warning(request, 'Your account is about to expire.') # recorded
+
+ # Set the messages level back to default.
+ messages.get_messages(request).level = None
+
+For more information on how the minimum recorded level functions, see
+`Message levels`_ above.
+
+Adding extra message tags
+-------------------------
+
+For more direct control over message tags, you can optionally provide a string
+containing extra tags to any of the add methods::
+
+ messages.add_message(request, messages.INFO, 'Over 9000!',
+ extra_tags='dragonball')
+ messages.error(request, 'Email box full', extra_tags='email')
+
+Extra tags are added before the default tag for that level and are space
+separated.
+
+Failing silently when the message framework is disabled
+-------------------------------------------------------
+
+If you're writing a reusable app (or other piece of code) and want to include
+messaging functionality, but don't want to require your users to enable it
+if they don't want to, you may pass an additional keyword argument
+``fail_silently=True`` to any of the ``add_message`` family of methods. For
+example::
+
+ messages.add_message(request, messages.SUCCESS, 'Profile details updated.',
+ fail_silently=True)
+ messages.info(request, 'Hello world.', fail_silently=True)
+
+Internally, Django uses this functionality in the create, update, and delete
+:ref:`generic views <topics-generic-views>` so that they work even if the
+message framework is disabled.
+
+.. note::
+ Setting ``fail_silently=True`` only hides the ``MessageFailure`` that would
+ otherwise occur when the messages framework disabled and one attempts to
+ use one of the ``add_message`` family of methods. It does not hide failures
+ that may occur for other reasons.
+
+Expiration of messages
+======================
+
+The messages are marked to be cleared when the storage instance is iterated
+(and cleared when the response is processed).
+
+To avoid the messages being cleared, you can set the messages storage to
+``False`` after iterating::
+
+ storage = messages.get_messages(request)
+ for message in storage:
+ do_something_with(message)
+ storage.used = False
+
+Behavior of parallel requests
+=============================
+
+Due to the way cookies (and hence sessions) work, **the behavior of any
+backends that make use of cookies or sessions is undefined when the same
+client makes multiple requests that set or get messages in parallel**. For
+example, if a client initiates a request that creates a message in one window
+(or tab) and then another that fetches any uniterated messages in another
+window, before the first window redirects, the message may appear in the
+second window instead of the first window where it may be expected.
+
+In short, when multiple simultaneous requests from the same client are
+involved, messages are not guaranteed to be delivered to the same window that
+created them nor, in some cases, at all. Note that this is typically not a
+problem in most applications and will become a non-issue in HTML5, where each
+window/tab will have its own browsing context.
+
+Settings
+========
+
+A few :ref:`Django settings <ref-settings>` give you control over message
+behavior:
+
+MESSAGE_LEVEL
+-------------
+
+Default: ``messages.INFO``
+
+This sets the minimum message that will be saved in the message storage. See
+`Message levels`_ above for more details.
+
+.. admonition:: Important
+
+ If you override ``MESSAGE_LEVEL`` in your settings file and rely on any of
+ the built-in constants, you must import the constants module directly to
+ avoid the potential for circular imports, e.g.::
+
+ from django.contrib.messages import constants as message_constants
+ MESSAGE_LEVEL = message_constants.DEBUG
+
+ If desired, you may specify the numeric values for the constants directly
+ according to the values in the above :ref:`constants table
+ <message-level-constants>`.
+
+MESSAGE_STORAGE
+---------------
+
+Default: ``'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'``
+
+Controls where Django stores message data. Valid values are:
+
+ * ``'django.contrib.messages.storage.fallback.FallbackStorage'``
+ * ``'django.contrib.messages.storage.session.SessionStorage'``
+ * ``'django.contrib.messages.storage.cookie.CookieStorage'``
+ * ``'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'``
+
+See `Storage backends`_ for more details.
+
+MESSAGE_TAGS
+------------
+
+Default::
+
+ {messages.DEBUG: 'debug',
+ messages.INFO: 'info',
+ messages.SUCCESS: 'success',
+ messages.WARNING: 'warning',
+ messages.ERROR: 'error',}
+
+This sets the mapping of message level to message tag, which is typically
+rendered as a CSS class in HTML. If you specify a value, it will extend
+the default. This means you only have to specify those values which you need
+to override. See `Displaying messages`_ above for more details.
+
+.. admonition:: Important
+
+ If you override ``MESSAGE_TAGS`` in your settings file and rely on any of
+ the built-in constants, you must import the ``constants`` module directly to
+ avoid the potential for circular imports, e.g.::
+
+ from django.contrib.messages import constants as message_constants
+ MESSAGE_TAGS = {message_constants.INFO: ''}
+
+ If desired, you may specify the numeric values for the constants directly
+ according to the values in the above :ref:`constants table
+ <message-level-constants>`.
+
+.. _Django settings: ../settings/
diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt
index b0b26cc227..b8d3e475c3 100644
--- a/docs/ref/middleware.txt
+++ b/docs/ref/middleware.txt
@@ -139,6 +139,20 @@ Enables language selection based on data from the request. It customizes
content for each user. See the :ref:`internationalization documentation
<topics-i18n>`.
+Message middleware
+------------------
+
+.. module:: django.contrib.messages.middleware
+ :synopsis: Message middleware.
+
+.. class:: django.contrib.messages.middleware.MessageMiddleware
+
+.. versionadded:: 1.2
+ ``MessageMiddleware`` was added.
+
+Enables cookie- and session-based message support. See the
+:ref:`messages documentation <ref-contrib-messages>`.
+
Session middleware
------------------
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index 7989c9f25d..11cb821b98 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -896,6 +896,43 @@ Bad: ``"http://www.example.com/static"``
.. setting:: MIDDLEWARE_CLASSES
+MESSAGE_LEVEL
+-------------
+
+.. versionadded:: 1.2
+
+Default: `messages.INFO`
+
+Sets the minimum message level that will be recorded by the messages
+framework. See the :ref:`messages documentation <ref-contrib-messages>` for
+more details.
+
+MESSAGE_STORAGE
+---------------
+
+.. versionadded:: 1.2
+
+Default: ``'django.contrib.messages.storage.user_messages.LegacyFallbackStorage'``
+
+Controls where Django stores message data. See the
+:ref:`messages documentation <ref-contrib-messages>` for more details.
+
+MESSAGE_TAGS
+------------
+
+.. versionadded:: 1.2
+
+Default::
+
+ {messages.DEBUG: 'debug',
+ messages.INFO: 'info',
+ messages.SUCCESS: 'success',
+ messages.WARNING: 'warning',
+ messages.ERROR: 'error',}
+
+Sets the mapping of message levels to message tags. See the
+:ref:`messages documentation <ref-contrib-messages>` for more details.
+
MIDDLEWARE_CLASSES
------------------
@@ -904,10 +941,16 @@ Default::
('django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',)
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',)
A tuple of middleware classes to use. See :ref:`topics-http-middleware`.
+.. versionchanged:: 1.2
+ ``'django.contrib.messages.middleware.MessageMiddleware'`` was added to the
+ default. For more information, see the :ref:`messages documentation
+ <ref-contrib-messages>`.
+
.. setting:: MONTH_DAY_FORMAT
MONTH_DAY_FORMAT
@@ -1155,12 +1198,18 @@ Default::
("django.core.context_processors.auth",
"django.core.context_processors.debug",
"django.core.context_processors.i18n",
- "django.core.context_processors.media")
+ "django.core.context_processors.media",
+ "django.contrib.messages.context_processors.messages")
A tuple of callables that are used to populate the context in ``RequestContext``.
These callables take a request object as their argument and return a dictionary
of items to be merged into the context.
+.. versionchanged:: 1.2
+ ``"django.contrib.messages.context_processors.messages"`` was added to the
+ default. For more information, see the :ref:`messages documentation
+ <ref-contrib-messages>`.
+
.. setting:: TEMPLATE_DEBUG
TEMPLATE_DEBUG
diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt
index 1b6eeb7014..077325b48e 100644
--- a/docs/ref/templates/api.txt
+++ b/docs/ref/templates/api.txt
@@ -311,7 +311,8 @@ and return a dictionary of items to be merged into the context. By default,
("django.core.context_processors.auth",
"django.core.context_processors.debug",
"django.core.context_processors.i18n",
- "django.core.context_processors.media")
+ "django.core.context_processors.media",
+ "django.contrib.messages.context_processors.messages")
.. versionadded:: 1.2
In addition to these, ``RequestContext`` always uses
@@ -320,6 +321,10 @@ and return a dictionary of items to be merged into the context. By default,
in case of accidental misconfiguration, it is deliberately hardcoded in and
cannot be turned off by the :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting.
+.. versionadded:: 1.2
+ The ``'messages'`` context processor was added. For more information, see
+ the :ref:`messages documentation <ref-contrib-messages>`.
+
Each processor is applied in order. That means, if one processor adds a
variable to the context and a second processor adds a variable with the same
name, the second will override the first. The default processors are explained
@@ -365,17 +370,18 @@ If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every
logged-in user (or an ``AnonymousUser`` instance, if the client isn't
logged in).
- * ``messages`` -- A list of messages (as strings) for the currently
- logged-in user. Behind the scenes, this calls
- ``request.user.get_and_delete_messages()`` for every request. That method
- collects the user's messages and deletes them from the database.
-
- Note that messages are set with ``user.message_set.create``.
+ * ``messages`` -- A list of messages (as strings) that have been set
+ via the :ref:`messages framework <ref-contrib-messages>`.
* ``perms`` -- An instance of
``django.core.context_processors.PermWrapper``, representing the
permissions that the currently logged-in user has.
+.. versionchanged:: 1.2
+ Prior to version 1.2, the ``messages`` variable was a lazy accessor for
+ ``user.get_and_delete_messages()``. It has been changed to include any
+ messages added via the :ref:`messages framework <ref-contrib-messages`.
+
django.core.context_processors.debug
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -427,6 +433,25 @@ If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every
:class:`~django.http.HttpRequest`. Note that this processor is not enabled by default;
you'll have to activate it.
+django.contrib.messages.context_processors.messages
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every
+``RequestContext`` will contain a single additional variable:
+
+ * ``messages`` -- A list of messages (as strings) that have been set
+ via the user model (using ``user.message_set.create``) or through
+ the :ref:`messages framework <ref-contrib-messages>`.
+
+.. versionadded:: 1.2
+ This template context variable was previously supplied by the ``'auth'``
+ context processor. For backwards compatibility the ``'auth'`` context
+ processor will continue to supply the ``messages`` variable until Django
+ 1.4. If you use the ``messages`` variable, your project will work with
+ either (or both) context processors, but it is recommended to add
+ ``django.contrib.messages.context_processors.messages`` so your project
+ will be prepared for the future upgrade.
+
Writing your own context processors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/docs/releases/1.2.txt b/docs/releases/1.2.txt
index 3bf4ec59a7..2f4b82fc33 100644
--- a/docs/releases/1.2.txt
+++ b/docs/releases/1.2.txt
@@ -26,13 +26,13 @@ There have been large changes to the way that CSRF protection works, detailed in
changes that developers must be aware of:
* ``CsrfResponseMiddleware`` and ``CsrfMiddleware`` have been deprecated, and
- will be removed completely in Django 1.4, in favour of a template tag that
+ will be removed completely in Django 1.4, in favor of a template tag that
should be inserted into forms.
* All contrib apps use a ``csrf_protect`` decorator to protect the view. This
requires the use of the csrf_token template tag in the template, so if you
- have used custom templates for contrib views, you MUST READ THE UPGRADE
- INSTRUCTIONS to fix those templates.
+ have used custom templates for contrib views, you MUST READ THE :ref:`UPGRADE
+ INSTRUCTIONS <ref-csrf-upgrading-notes>` to fix those templates.
* ``CsrfViewMiddleware`` is included in :setting:`MIDDLEWARE_CLASSES` by
default. This turns on CSRF protection by default, so that views that accept
@@ -42,8 +42,8 @@ changes that developers must be aware of:
* All of the CSRF has moved from contrib to core (with backwards compatible
imports in the old locations, which are deprecated).
-LazyObject
-----------
+``LazyObject``
+--------------
``LazyObject`` is an undocumented utility class used for lazily wrapping other
objects of unknown type. In Django 1.1 and earlier, it handled introspection in
@@ -214,7 +214,86 @@ argument to resolve database-specific values.
Features deprecated in 1.2
==========================
-None.
+CSRF response rewriting middleware
+----------------------------------
+
+``CsrfResponseMiddleware``, the middleware that automatically inserted CSRF
+tokens into POST forms in outgoing pages, has been deprecated in favor of a
+template tag method (see above), and will be removed completely in Django
+1.4. ``CsrfMiddleware``, which includes the functionality of
+``CsrfResponseMiddleware`` and ``CsrfViewMiddleware`` has likewise been
+deprecated.
+
+Also, the CSRF module has moved from contrib to core, and the old imports are
+deprecated, as described in the :ref:`upgrading notes <ref-csrf-upgrading-notes>`.
+
+``SMTPConnection``
+------------------
+
+The ``SMTPConnection`` class has been deprecated in favor of a generic
+E-mail backend API. Old code that explicitly instantiated an instance
+of an SMTPConnection::
+
+ from django.core.mail import SMTPConnection
+ connection = SMTPConnection()
+ messages = get_notification_email()
+ connection.send_messages(messages)
+
+should now call :meth:`~django.core.mail.get_connection()` to
+instantiate a generic e-mail connection::
+
+ from django.core.mail import get_connection
+ connection = get_connection()
+ messages = get_notification_email()
+ connection.send_messages(messages)
+
+Depending on the value of the :setting:`EMAIL_BACKEND` setting, this
+may not return an SMTP connection. If you explicitly require an SMTP
+connection with which to send e-mail, you can explicitly request an
+SMTP connection::
+
+ from django.core.mail import get_connection
+ connection = get_connection('django.core.mail.backends.smtp')
+ messages = get_notification_email()
+ connection.send_messages(messages)
+
+If your call to construct an instance of ``SMTPConnection`` required
+additional arguments, those arguments can be passed to the
+:meth:`~django.core.mail.get_connection()` call::
+
+ connection = get_connection('django.core.mail.backends.smtp', hostname='localhost', port=1234)
+
+User Messages API
+-----------------
+
+The API for storing messages in the user ``Message`` model (via
+``user.message_set.create``) is now deprecated and will be removed in Django
+1.4 according to the standard :ref:`release process <internals-release-process>`.
+
+To upgrade your code, you need to replace any instances of::
+
+ user.message_set.create('a message')
+
+with the following::
+
+ from django.contrib import messages
+ messages.add_message(request, messages.INFO, 'a message')
+
+Additionally, if you make use of the method, you need to replace the
+following::
+
+ for message in user.get_and_delete_messages():
+ ...
+
+with::
+
+ from django.contrib import messages
+ for message in messages.get_messages(request):
+ ...
+
+For more information, see the full
+:ref:`messages documentation <ref-contrib-messages>`. You should begin to
+update your code to use the new API immediately.
What's new in Django 1.2
========================
@@ -231,23 +310,33 @@ malicious site in their browser. A related type of attack, 'login
CSRF', where an attacking site tricks a user's browser into logging
into a site with someone else's credentials, is also covered.
-Email Backends
---------------
+E-mail Backends
+---------------
-You can now :ref:`configure the way that Django sends email
-<topic-email-backends>`. Instead of using SMTP to send all email, you
-can now choose a configurable email backend to send messages. If your
+You can now :ref:`configure the way that Django sends e-mail
+<topic-email-backends>`. Instead of using SMTP to send all e-mail, you
+can now choose a configurable e-mail backend to send messages. If your
hosting provider uses a sandbox or some other non-SMTP technique for
-sending mail, you can now construct an email backend that will allow
+sending mail, you can now construct an e-mail backend that will allow
Django's standard :ref:`mail sending methods<topics-email>` to use
those facilities.
This also makes it easier to debug mail sending - Django ships with
-backend implementations that allow you to send email to a
+backend implementations that allow you to send e-mail to a
:ref:`file<topic-email-file-backend>`, to the
:ref:`console<topic-email-console-backend>`, or to
:ref:`memory<topic-email-memory-backend>` - you can even configure all
-email to be :ref:`thrown away<topic-email-console-backend>`.
+e-mail to be :ref:`thrown away<topic-email-dummy-backend>`.
+
+Messages Framework
+------------------
+
+Django now includes a robust and configurable :ref:`messages framework
+<ref-contrib-messages>` with built-in support for cookie- and session-based
+messaging, for both anonymous and authenticated clients. The messages framework
+replaces the deprecated user message API and allows you to temporarily store
+messages in one request and retrieve them for display in a subsequent request
+(usually the next one).
Support for multiple databases
------------------------------
diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt
index 33461a0858..ebd31e4e20 100644
--- a/docs/topics/auth.txt
+++ b/docs/topics/auth.txt
@@ -23,6 +23,9 @@ The auth system consists of:
user.
* Messages: A simple way to queue messages for given users.
+.. deprecated:: 1.2
+ The Messages component of the auth system will be removed in Django 1.4.
+
Installation
============
@@ -1289,6 +1292,11 @@ messages.
Messages
========
+.. deprecated:: 1.2
+ This functionality will be removed in Django 1.4. You should use the
+ :ref:`messages framework <ref-contrib-messages>` for all new projects and
+ begin to update your existing code immediately.
+
The message system is a lightweight way to queue messages for given users.
A message is associated with a :class:`~django.contrib.auth.models.User`.
@@ -1334,13 +1342,16 @@ logged-in user and his/her messages are made available in the
</ul>
{% endif %}
-Note that :class:`~django.template.context.RequestContext` calls
-:meth:`~django.contrib.auth.models.User.get_and_delete_messages` behind the
-scenes, so any messages will be deleted even if you don't display them.
+.. versionchanged:: 1.2
+ The ``messages`` template variable uses a backwards compatible method in the
+ :ref:`messages framework <ref-contrib-messages>` to retrieve messages from
+ both the user ``Message`` model and from the new framework. Unlike in
+ previous revisions, the messages will not be erased unless they are actually
+ displayed.
Finally, note that this messages framework only works with users in the user
database. To send messages to anonymous users, use the
-:ref:`session framework <topics-http-sessions>`.
+:ref:`messages framework <ref-contrib-messages>`.
.. _authentication-backends:
diff --git a/docs/topics/email.txt b/docs/topics/email.txt
index ccc993b2e8..eee77cb4a0 100644
--- a/docs/topics/email.txt
+++ b/docs/topics/email.txt
@@ -10,7 +10,7 @@ Sending e-mail
Although Python makes sending e-mail relatively easy via the `smtplib
library`_, Django provides a couple of light wrappers over it. These wrappers
are provided to make sending e-mail extra quick, to make it easy to test
-email sending during development, and to provide support for platforms that
+e-mail sending during development, and to provide support for platforms that
can't use SMTP.
The code lives in the ``django.core.mail`` module.
@@ -64,7 +64,7 @@ are required.
* ``auth_password``: The optional password to use to authenticate to the
SMTP server. If this isn't provided, Django will use the value of the
``EMAIL_HOST_PASSWORD`` setting.
- * ``connection``: The optional email backend to use to send the mail.
+ * ``connection``: The optional e-mail backend to use to send the mail.
If unspecified, an instance of the default backend will be used.
See the documentation on :ref:`E-mail backends <topic-email-backends>`
for more details.
@@ -215,8 +215,8 @@ message itself. The :ref:`e-mail backend <topic-email-backends>` is then
responsible for sending the e-mail.
For convenience, :class:`~django.core.mail.EmailMessage` provides a simple
-``send()`` method for sending a single email. If you need to send multiple
-messages, the email backend API :ref:`provides an alternative
+``send()`` method for sending a single e-mail. If you need to send multiple
+messages, the e-mail backend API :ref:`provides an alternative
<topics-sending-multiple-emails>`.
EmailMessage Objects
@@ -264,7 +264,7 @@ For example::
The class has the following methods:
* ``send(fail_silently=False)`` sends the message. If a connection was
- specified when the email was constructed, that connection will be used.
+ specified when the e-mail was constructed, that connection will be used.
Otherwise, an instance of the default backend will be instantiated and
used. If the keyword argument ``fail_silently`` is ``True``, exceptions
raised while sending the message will be quashed.
@@ -358,9 +358,9 @@ The actual sending of an e-mail is handled by the e-mail backend.
The e-mail backend class has the following methods:
- * ``open()`` instantiates an long-lived email-sending connection.
+ * ``open()`` instantiates an long-lived e-mail-sending connection.
- * ``close()`` closes the current email-sending connection.
+ * ``close()`` closes the current e-mail-sending connection.
* ``send_messages(email_messages)`` sends a list of
:class:`~django.core.mail.EmailMessage` objects. If the connection is
@@ -379,11 +379,11 @@ instance of the e-mail backend that you can use.
.. function:: get_connection(backend=None, fail_silently=False, *args, **kwargs)
By default, a call to ``get_connection()`` will return an instance of the
-email backend specified in :setting:`EMAIL_BACKEND`. If you specify the
+e-mail backend specified in :setting:`EMAIL_BACKEND`. If you specify the
``backend`` argument, an instance of that backend will be instantiated.
The ``fail_silently`` argument controls how the backend should handle errors.
-If ``fail_silently`` is True, exceptions during the email sending process
+If ``fail_silently`` is True, exceptions during the e-mail sending process
will be silently ignored.
All other arguments are passed directly to the constructor of the
@@ -391,8 +391,8 @@ e-mail backend.
Django ships with several e-mail sending backends. With the exception of the
SMTP backend (which is the default), these backends are only useful during
-testing and development. If you have special email sending requirements, you
-can :ref:`write your own email backend <topic-custom-email-backend>`.
+testing and development. If you have special e-mail sending requirements, you
+can :ref:`write your own e-mail backend <topic-custom-email-backend>`.
.. _topic-email-smtp-backend:
@@ -414,8 +414,8 @@ want to specify it explicitly, put the following in your settings::
Prior to version 1.2, Django provided a
:class:`~django.core.mail.SMTPConnection` class. This class provided a way
- to directly control the use of SMTP to send email. This class has been
- deprecated in favor of the generic email backend API.
+ to directly control the use of SMTP to send e-mail. This class has been
+ deprecated in favor of the generic e-mail backend API.
For backwards compatibility :class:`~django.core.mail.SMTPConnection` is
still available in ``django.core.mail`` as an alias for the SMTP backend.
@@ -508,15 +508,15 @@ implementation.
.. _topics-sending-multiple-emails:
-Sending multiple emails
------------------------
+Sending multiple e-mails
+------------------------
Establishing and closing an SMTP connection (or any other network connection,
-for that matter) is an expensive process. If you have a lot of emails to send,
+for that matter) is an expensive process. If you have a lot of e-mails to send,
it makes sense to reuse an SMTP connection, rather than creating and
-destroying a connection every time you want to send an email.
+destroying a connection every time you want to send an e-mail.
-There are two ways you tell an email backend to reuse a connection.
+There are two ways you tell an e-mail backend to reuse a connection.
Firstly, you can use the ``send_messages()`` method. ``send_messages()`` takes
a list of :class:`~django.core.mail.EmailMessage` instances (or subclasses),
@@ -524,11 +524,11 @@ and sends them all using a single connection.
For example, if you have a function called ``get_notification_email()`` that
returns a list of :class:`~django.core.mail.EmailMessage` objects representing
-some periodic e-mail you wish to send out, you could send these emails using
+some periodic e-mail you wish to send out, you could send these e-mails using
a single call to send_messages::
from django.core import mail
- connection = mail.get_connection() # Use default email connection
+ connection = mail.get_connection() # Use default e-mail connection
messages = get_notification_email()
connection.send_messages(messages)
@@ -536,7 +536,7 @@ In this example, the call to ``send_messages()`` opens a connection on the
backend, sends the list of messages, and then closes the connection again.
The second approach is to use the ``open()`` and ``close()`` methods on the
-email backend to manually control the connection. ``send_messages()`` will not
+e-mail backend to manually control the connection. ``send_messages()`` will not
manually open or close the connection if it is already open, so if you
manually open the connection, you can control when it is closed. For example::
@@ -546,10 +546,10 @@ manually open the connection, you can control when it is closed. For example::
# Manually open the connection
connection.open()
- # Construct an email message that uses the connection
+ # Construct an e-mail message that uses the connection
email1 = mail.EmailMessage('Hello', 'Body goes here', 'from@example.com',
['to1@example.com'], connection=connection)
- email1.send() # Send the email
+ email1.send() # Send the e-mail
# Construct two more messages
email2 = mail.EmailMessage('Hello', 'Body goes here', 'from@example.com',
@@ -557,7 +557,7 @@ manually open the connection, you can control when it is closed. For example::
email3 = mail.EmailMessage('Hello', 'Body goes here', 'from@example.com',
['to3@example.com'])
- # Send the two emails in a single call -
+ # Send the two e-mails in a single call -
connection.send_messages([email2, email3])
# The connection was already open so send_messages() doesn't close it.
# We need to manually close the connection.
@@ -574,10 +574,10 @@ people under the right conditions, and that those e-mails will contain the
correct content.
The easiest way to test your project's use of e-mail is to use the ``console``
-email backend. This backend redirects all email to stdout, allowing you to
+e-mail backend. This backend redirects all e-mail to stdout, allowing you to
inspect the content of mail.
-The ``file`` email backend can also be useful during development -- this backend
+The ``file`` e-mail backend can also be useful during development -- this backend
dumps the contents of every SMTP connection to a file that can be inspected
at your leisure.
@@ -604,7 +604,7 @@ SMTPConnection
.. deprecated:: 1.2
-The ``SMTPConnection`` class has been deprecated in favor of the generic email
+The ``SMTPConnection`` class has been deprecated in favor of the generic e-mail
backend API.
For backwards compatibility ``SMTPConnection`` is still available in
diff --git a/tests/modeltests/basic/models.py b/tests/modeltests/basic/models.py
index 28b44a582f..c86cb3ab25 100644
--- a/tests/modeltests/basic/models.py
+++ b/tests/modeltests/basic/models.py
@@ -211,6 +211,14 @@ True
>>> Article.objects.get(id__exact=8) == Article.objects.get(id__exact=7)
False
+# You can use 'in' to test for membership...
+>>> a8 in Article.objects.all()
+True
+
+# ... but there will often be more efficient ways if that is all you need:
+>>> Article.objects.filter(id=a8.id).exists()
+True
+
# dates() returns a list of available dates of the given scope for the given field.
>>> Article.objects.dates('pub_date', 'year')
[datetime.datetime(2005, 1, 1, 0, 0)]
diff --git a/tests/regressiontests/backends/tests.py b/tests/regressiontests/backends/tests.py
index e4fa82403a..ad00f079ba 100644
--- a/tests/regressiontests/backends/tests.py
+++ b/tests/regressiontests/backends/tests.py
@@ -18,7 +18,7 @@ class Callproc(unittest.TestCase):
return True
else:
return True
-
+
class LongString(unittest.TestCase):
def test_long_string(self):
diff --git a/tests/regressiontests/generic_inline_admin/tests.py b/tests/regressiontests/generic_inline_admin/tests.py
index 60a370fb2d..d28cd16e1d 100644
--- a/tests/regressiontests/generic_inline_admin/tests.py
+++ b/tests/regressiontests/generic_inline_admin/tests.py
@@ -81,6 +81,11 @@ class GenericAdminViewTest(TestCase):
inline_formset = generic_inlineformset_factory(Media,
exclude=('url',))
+ # Regression test for #12340.
+ e = Episode.objects.get(name='This Week in Django')
+ formset = inline_formset(instance=e)
+ self.failUnless(formset.get_queryset().ordered)
+
class GenericInlineAdminParametersTest(TestCase):
fixtures = ['users.xml']
@@ -139,4 +144,4 @@ class GenericInlineAdminParametersTest(TestCase):
e = self._create_object(EpisodeExclude)
response = self.client.get('/generic_inline_admin/admin/generic_inline_admin/episodeexclude/%s/' % e.pk)
formset = response.context['inline_admin_formsets'][0].formset
- self.failIf('url' in formset.forms[0], 'The formset has excluded "url" field.') \ No newline at end of file
+ self.failIf('url' in formset.forms[0], 'The formset has excluded "url" field.')
diff --git a/tests/runtests.py b/tests/runtests.py
index 1408a0e0a3..86e83fe1ea 100755
--- a/tests/runtests.py
+++ b/tests/runtests.py
@@ -28,6 +28,7 @@ ALWAYS_INSTALLED_APPS = [
'django.contrib.flatpages',
'django.contrib.redirects',
'django.contrib.sessions',
+ 'django.contrib.messages',
'django.contrib.comments',
'django.contrib.admin',
]
@@ -106,6 +107,7 @@ def django_tests(verbosity, interactive, test_labels):
settings.MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.common.CommonMiddleware',
)
settings.SITE_ID = 1