diff options
53 files changed, 2286 insertions, 76 deletions
@@ -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 @@ -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 |