Permalink
Browse files

Fixed #4604 - Configurable message passing system, supporting anonymo…

…us users

This deprecates User.message_set in favour of a configurable messaging
system, with backends provided for cookie storage, session storage and
backward compatibility.

Many thanks to Tobias McNulty for the bulk of the work here, with
contributions from Chris Beaven (SmileyChris) and lots of code review from
Russell Keith-Magee, and input from many others.  Also credit to the authors
of various messaging systems for Django whose ideas may have been pinched
:-)



git-svn-id: http://code.djangoproject.com/svn/django/trunk@11804 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent eeb10d5 commit 25020ddb05543fff1c37d77b49bd937fd2bbb170 @spookylukey spookylukey committed Dec 9, 2009
Showing with 2,100 additions and 29 deletions.
  1. +2 −1 AUTHORS
  2. +12 −0 django/conf/global_settings.py
  3. +2 −0 django/conf/project_template/settings.py
  4. +3 −2 django/contrib/admin/options.py
  5. +2 −1 django/contrib/admin/views/template.py
  6. +5 −3 django/contrib/auth/admin.py
  7. +9 −1 django/contrib/auth/models.py
  8. +2 −0 django/contrib/messages/__init__.py
  9. +84 −0 django/contrib/messages/api.py
  10. +13 −0 django/contrib/messages/constants.py
  11. +8 −0 django/contrib/messages/context_processors.py
  12. +26 −0 django/contrib/messages/middleware.py
  13. +1 −0 django/contrib/messages/models.py
  14. +31 −0 django/contrib/messages/storage/__init__.py
  15. +181 −0 django/contrib/messages/storage/base.py
  16. +143 −0 django/contrib/messages/storage/cookie.py
  17. +59 −0 django/contrib/messages/storage/fallback.py
  18. +33 −0 django/contrib/messages/storage/session.py
  19. +64 −0 django/contrib/messages/storage/user_messages.py
  20. +6 −0 django/contrib/messages/tests/__init__.py
  21. +375 −0 django/contrib/messages/tests/base.py
  22. +100 −0 django/contrib/messages/tests/cookie.py
  23. +173 −0 django/contrib/messages/tests/fallback.py
  24. +18 −0 django/contrib/messages/tests/middleware.py
  25. +38 −0 django/contrib/messages/tests/session.py
  26. +39 −0 django/contrib/messages/tests/urls.py
  27. +65 −0 django/contrib/messages/tests/user_messages.py
  28. +11 −0 django/contrib/messages/utils.py
  29. +3 −2 django/core/context_processors.py
  30. +11 −6 django/views/generic/create_update.py
  31. +1 −0 docs/index.txt
  32. +8 −0 docs/internals/deprecation.txt
  33. +12 −0 docs/ref/contrib/index.txt
  34. +405 −0 docs/ref/contrib/messages.txt
  35. +14 −0 docs/ref/middleware.txt
  36. +51 −2 docs/ref/settings.txt
  37. +32 −7 docs/ref/templates/api.txt
  38. +41 −0 docs/releases/1.2.txt
  39. +15 −4 docs/topics/auth.txt
  40. +2 −0 tests/runtests.py
View
@@ -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
@@ -172,6 +172,7 @@
'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.
@@ -308,6 +309,7 @@
'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',
)
@@ -393,6 +395,16 @@
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 #
###########
@@ -62,6 +62,7 @@
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
)
ROOT_URLCONF = '{{ project_name }}.urls'
@@ -77,4 +78,5 @@
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
+ 'django.contrib.messages',
)
@@ -6,6 +6,7 @@
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 @@ def construct_change_message(self, request, form, formsets):
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):
"""
@@ -6,6 +6,7 @@
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', {
@@ -3,6 +3,7 @@
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 @@ def add_view(self, request):
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 @@ def user_change_password(self, request, id):
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)
@@ -288,6 +288,14 @@ def get_profile(self):
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):
@@ -0,0 +1,2 @@
+from api import *
+from constants import *
@@ -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)
@@ -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',
+}
@@ -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)}
@@ -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
@@ -0,0 +1 @@
+# Models module required so tests are discovered.
@@ -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)
Oops, something went wrong.

0 comments on commit 25020dd

Please sign in to comment.