summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt5
-rw-r--r--TODO.txt2
-rw-r--r--docs/api.rst1
-rw-r--r--docs/api/flash.rst36
-rw-r--r--docs/api/interfaces.rst1
-rw-r--r--docs/index.rst1
-rw-r--r--docs/latexindex.rst2
-rw-r--r--docs/narr/flash.rst123
-rw-r--r--docs/narr/sessions.rst4
-rw-r--r--pyramid/flash.py32
-rw-r--r--pyramid/interfaces.py40
-rw-r--r--pyramid/session.py13
-rw-r--r--pyramid/tests/test_flash.py81
-rw-r--r--pyramid/tests/test_session.py55
14 files changed, 394 insertions, 2 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index b4f367607..9fb26b589 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -9,6 +9,9 @@ Features
generate a URL, overriding the default logic. See the new "Generating The
URL Of A Resource" section within the Resources narrative chapter.
+- Added flash messaging, as described in the "Flash Messaging" narrative
+ documentation chapter.
+
Documentation
-------------
@@ -33,6 +36,8 @@ Documentation
- Added "Finding a Resource With a Class or Interface in Lineage" to
Resources narrative chapter.
+- Added a "Flash Messaging" narrative documentation chapter.
+
1.0a7 (2010-12-20)
==================
diff --git a/TODO.txt b/TODO.txt
index c0dd920b1..f90c52538 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -6,8 +6,6 @@ Must-Have (before 1.0)
- Narrative docs for ``Configurator.include`` and ``Configurator.commit``.
-- Provide a .flash API on session object.
-
- Consider deprecations for ``model`` and ``resource`` APIs.
Should-Have
diff --git a/docs/api.rst b/docs/api.rst
index b650c8ded..4808a08b3 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -15,6 +15,7 @@ documentation is organized alphabetically by module name.
api/config
api/events
api/exceptions
+ api/flash
api/httpexceptions
api/i18n
api/interfaces
diff --git a/docs/api/flash.rst b/docs/api/flash.rst
new file mode 100644
index 000000000..94907958d
--- /dev/null
+++ b/docs/api/flash.rst
@@ -0,0 +1,36 @@
+.. _flash_module:
+
+:mod:`pyramid.flash`
+--------------------
+
+Flash Category Constants
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+The following attributes represent constants for use as flash messaging
+category values (see :ref:`flash_chapter`).
+
+.. attribute:: DEBUG
+
+ An alternate spelling for the string ``debug``. Represents development
+ debug messages.
+
+.. attribute:: INFO
+
+ An alternate spelling for the string ``info``. Represents messages that
+ are informational for user consumption.
+
+.. attribute:: SUCCESS
+
+ An alternate spelling for the string ``success``. Represents messages that
+ tell the user about a successful action.
+
+.. attribute:: WARNING
+
+ An alternate spelling for the string ``warning``. Represents messages
+ that tell the user about a condition that is not a success, but is neither
+ an error.
+
+.. attribute:: ERROR
+
+ An alternate spelling for the string ``success``. Represents messages
+ that tell the user about an unsuccessful action.
diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst
index b3c14e5f7..dab64ba15 100644
--- a/docs/api/interfaces.rst
+++ b/docs/api/interfaces.rst
@@ -35,3 +35,4 @@ Other Interfaces
.. autointerface:: ITemplateRenderer
+ .. autointerface:: IFlashMessages
diff --git a/docs/index.rst b/docs/index.rst
index 4f484b0f9..fbf9de810 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -48,6 +48,7 @@ Narrative documentation in chapter form explaining how to use
narr/static
narr/webob
narr/sessions
+ narr/flash
narr/security
narr/hybrid
narr/i18n
diff --git a/docs/latexindex.rst b/docs/latexindex.rst
index f55e30aa8..6a1992ba4 100644
--- a/docs/latexindex.rst
+++ b/docs/latexindex.rst
@@ -41,6 +41,7 @@ Narrative Documentation
narr/static
narr/webob
narr/sessions
+ narr/flash
narr/security
narr/hybrid
narr/i18n
@@ -88,6 +89,7 @@ API Reference
api/config
api/events
api/exceptions
+ api/flash
api/httpexceptions
api/i18n
api/interfaces
diff --git a/docs/narr/flash.rst b/docs/narr/flash.rst
new file mode 100644
index 000000000..331793533
--- /dev/null
+++ b/docs/narr/flash.rst
@@ -0,0 +1,123 @@
+.. _flash_chapter:
+
+Flash Messages
+==============
+
+"Flash messages" are simply a queue of message strings stored in the
+:term:`session`. To use flash messaging, you must enable a :term:`session
+factory` as described in :ref:`using_the_default_session_factory` or
+:ref:`using_alternate_session_factories`.
+
+Flash messaging has two main uses: to display a status message only once to
+the user after performing an internal redirect, and to allow generic code to
+log messages for single-time display without having direct access to an HTML
+template. The user interface consists of two methods of the :term:`session`
+object.
+
+Using the ``session.flash`` Method
+----------------------------------
+
+To add a message to a flash queue, use a session object's ``flash`` method:
+
+.. code-block:: python
+ :linenos:
+
+ request.session.flash('mymessage')
+
+The ``.flash`` method appends a message to the queue, creating the queue if
+necessary. The message is not modified in any way.
+
+The ``category`` argument names a category or level. The library defines
+several default category names: ``debug``, ``info``, ``success``, ``warning``
+and ``error``. The default category level is ``info``.
+
+The ``queue_name`` argument allows you to define multiple message
+queues. This can be used to display different kinds of messages in different
+places on a page. You cam pass any name for your queue, but it must be a
+string. The default value is the empty string, which chooses the default
+queue. Each queue is independent, and can be popped by ``unflash``
+separately.
+
+Constant names for flash message category names are importable from the
+:mod:`pyramid.flash` module as ``DEBUG``, ``INFO``, ``SUCCESS``, ``WARNING``
+and ``ERROR``, which respectively name ``debug``, ``info``, ``success``,
+``warning`` and ``error`` strings. For example you can do this:
+
+.. code-block:: python
+
+ from pyramid import flash
+ request.session.flash(msg, flash.DEBUG)
+
+Or you can use the literal name ``debug``:
+
+.. code-block:: python
+
+ request.session.flash(msg, 'debug')
+
+Both examples do the same thing. The meanings of flash category names are
+detailed in :mod:`pyramid.flash`.
+
+To pop a particular queue of messages from the flash object, use the session
+object's ``unflash`` method.
+
+.. code-block:: python
+ :linenos:
+
+ >>> request.session.flash('info message', 'info')
+ >>> messages = request.session.unflash()
+ >>> messages['info']
+ ['info message']
+
+Using the ``session.unflash`` Method
+------------------------------------
+
+Once one or more messages has been added to a flash queue by the
+``session.flash`` API, the ``session.unflash`` API can be used to pop that
+queue and return it for use.
+
+For example some code that runs in a view callable might call the
+``session.flash`` API:
+
+.. code-block:: python
+ :linenos:
+
+ request.session.flash('mymessage')
+
+A corresponding ``session.unflash`` might be called on a subsequent request:
+
+.. code-block:: python
+ :linenos:
+
+ messages = request.session.unflash()
+
+Calling ``session.unflash`` again like above without a corresponding call to
+``session.flash`` will return an empty ``messages`` object, because the queue
+has already been popped.
+
+The ``messages`` object returned from ``unflash`` is a dictionary-like
+object. Its keys are category names, and its values are sequences of
+strings. For ease of use, the dict-like object returned by ``unflash`` isn't
+a "plain" dict: it's an object which has several helper methods, each named
+after a particular flash category level. These methods return all messages
+related to the category name:
+
+.. code-block:: python
+ :linenos:
+
+ >>> request.session.flash('debug message', 'debug')
+ >>> request.session.flash('info message', 'info')
+ >>> messages = request.session.unflash()
+ >>> info_messages = messages.debug()
+ ['debug message']
+ >>> info_messages = messages.info()
+ ['info message']
+
+The full API of the ``messages`` object returned by ``unflash`` is documented
+in :class:`pyramid.interfaces.IFlashMessages`.
+
+.. The ``ignore_duplicate`` flag tells whether to suppress duplicate
+.. messages. If true, and another message with identical text exists in the
+.. queue, don't add the new message. But if the existing message has a
+.. different category than the new message, change its category to match the
+.. new message.
+
diff --git a/docs/narr/sessions.rst b/docs/narr/sessions.rst
index 7a0a03384..de9add3b7 100644
--- a/docs/narr/sessions.rst
+++ b/docs/narr/sessions.rst
@@ -10,6 +10,8 @@ A :term:`session` is a namespace which is valid for some period of
continual activity that can be used to represent a user's interaction
with a web application.
+.. _using_the_default_session_factory:
+
Using The Default Session Factory
---------------------------------
@@ -131,6 +133,8 @@ Some gotchas:
single: pyramid_beaker
single: Beaker
+.. _using_alternate_session_factories:
+
Using Alternate Session Factories
---------------------------------
diff --git a/pyramid/flash.py b/pyramid/flash.py
new file mode 100644
index 000000000..9e9f44634
--- /dev/null
+++ b/pyramid/flash.py
@@ -0,0 +1,32 @@
+from zope.interface import implements
+
+from pyramid.interfaces import IFlashMessages
+
+# flash message categories
+DEBUG = 'debug' # development messages
+INFO = 'info' # informational messages
+SUCCESS = 'success' # a message indicating success
+WARNING = 'warning' # not an error, but not a success
+ERROR = 'error' # an action was unsuccessful
+
+class FlashMessages(dict):
+ implements(IFlashMessages)
+ def custom(self, name):
+ messages = self.get(name, [])
+ return messages
+
+ def debug(self):
+ return self.get(DEBUG, [])
+
+ def info(self):
+ return self.get(INFO, [])
+
+ def success(self):
+ return self.get(SUCCESS, [])
+
+ def warning(self):
+ return self.get(WARNING, [])
+
+ def error(self):
+ return self.get(ERROR, [])
+
diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py
index d9b06abae..3ab833be8 100644
--- a/pyramid/interfaces.py
+++ b/pyramid/interfaces.py
@@ -120,6 +120,34 @@ class ITemplateRenderer(IRenderer):
accepts arbitrary keyword arguments and returns a string or
unicode object """
+class IFlashMessages(Interface):
+ """ Dictionary-like object which maps flash category names to lists of
+ flash messages. Also supports an API for obtaining classes of flash
+ message lists."""
+ def custom(name):
+ """ Return a sequence of custom-category flash messages or an empty
+ list if no messages of this custom category existed in the queue."""
+
+ def debug():
+ """ Return a sequence of flash.DEBUG category flash messages or an
+ empty list if no flash.DEBUG messages existed in the queue."""
+
+ def info():
+ """ Return a sequence of flash.INFO category flash messages or an
+ empty list if no flash.INFO messages existed in the queue."""
+
+ def success():
+ """ Return a sequence of flash.SUCCESS category flash messages or an
+ empty list if no flash.SUCCESS messages existed in the queue."""
+
+ def warning():
+ """ Return a sequence of flash.WARNING category flash messages or an
+ empty list if no flash.WARNING messages existed in the queue."""
+
+ def error():
+ """ Return a sequence of flash.ERROR category flash messages or an
+ empty list if no flash.ERROR messages existed in the queue."""
+
# internal interfaces
class IRequest(Interface):
@@ -460,6 +488,18 @@ class ISession(Interface):
the sessioning machinery to notice the mutation of the
internal dictionary."""
+ def flash(msg, category='info', queue_name=''):
+ """ Push a flash message onto the stack related to the category and
+ queue name. Multiple flash message queues can be managed by passing
+ an optional ``queue_name``. Default category names are 'debug',
+ 'info', 'success', 'warning', and 'error' (these have constant names
+ importable from the ``pyramid.flash`` module). A custom category
+ name is also permitted."""
+
+ def unflash(queue_name=''):
+ """ Pop a queue from the flash message storage. This method returns
+ an object which implements ``pyramid.interfaces.IFlashMessages``"""
+
# mapping methods
def __getitem__(key):
diff --git a/pyramid/session.py b/pyramid/session.py
index 3ea3dbcf8..8c3bd1897 100644
--- a/pyramid/session.py
+++ b/pyramid/session.py
@@ -18,6 +18,7 @@ import base64
from zope.interface import implements
from pyramid.interfaces import ISession
+from pyramid import flash
def manage_accessed(wrapped):
""" Decorator which causes a cookie to be set when a wrapped
@@ -167,6 +168,18 @@ def UnencryptedCookieSessionFactoryConfig(
__setitem__ = manage_accessed(dict.__setitem__)
__delitem__ = manage_accessed(dict.__delitem__)
+ # flash API methods
+ @manage_accessed
+ def flash(self, msg, category=flash.INFO, queue_name=''):
+ storage = self.setdefault('_f_' + queue_name, {})
+ category = storage.setdefault(category, [])
+ category.append(msg)
+
+ @manage_accessed
+ def unflash(self, queue_name=''):
+ storage = self.pop('_f_' + queue_name, {})
+ return flash.FlashMessages(storage)
+
# non-API methods
def _set_cookie(self, response):
if not self._cookie_on_exception:
diff --git a/pyramid/tests/test_flash.py b/pyramid/tests/test_flash.py
new file mode 100644
index 000000000..cce01d45b
--- /dev/null
+++ b/pyramid/tests/test_flash.py
@@ -0,0 +1,81 @@
+import unittest
+
+class TestFlashMessages(unittest.TestCase):
+ def _getTargetClass(self):
+ from pyramid.flash import FlashMessages
+ return FlashMessages
+
+ def _makeOne(self, *arg, **kw):
+ cls = self._getTargetClass()
+ return cls(*arg, **kw)
+
+ def test_class_conforms(self):
+ from zope.interface.verify import verifyClass
+ from pyramid.interfaces import IFlashMessages
+ verifyClass(IFlashMessages, self._getTargetClass())
+
+ def test_instance_conforms(self):
+ from zope.interface.verify import verifyObject
+ from pyramid.interfaces import IFlashMessages
+ messages = self._makeOne()
+ verifyObject(IFlashMessages, messages)
+
+ def test_debug_filled(self):
+ from pyramid import flash
+ expected = ['one', 'two']
+ messages = self._makeOne({flash.DEBUG:expected})
+ self.assertEqual(messages.debug(), expected)
+
+ def test_debug_empty(self):
+ messages = self._makeOne()
+ self.assertEqual(messages.debug(), [])
+
+ def test_info_filled(self):
+ from pyramid import flash
+ expected = ['one', 'two']
+ messages = self._makeOne({flash.INFO:expected})
+ self.assertEqual(messages.info(), expected)
+
+ def test_info_empty(self):
+ messages = self._makeOne()
+ self.assertEqual(messages.info(), [])
+
+ def test_success_filled(self):
+ from pyramid import flash
+ expected = ['one', 'two']
+ messages = self._makeOne({flash.SUCCESS:expected})
+ self.assertEqual(messages.success(), expected)
+
+ def test_success_empty(self):
+ messages = self._makeOne()
+ self.assertEqual(messages.success(), [])
+
+ def test_warning_filled(self):
+ from pyramid import flash
+ expected = ['one', 'two']
+ messages = self._makeOne({flash.WARNING:expected})
+ self.assertEqual(messages.warning(), expected)
+
+ def test_warning_empty(self):
+ messages = self._makeOne()
+ self.assertEqual(messages.warning(), [])
+
+ def test_error_filled(self):
+ from pyramid import flash
+ expected = ['one', 'two']
+ messages = self._makeOne({flash.ERROR:expected})
+ self.assertEqual(messages.error(), expected)
+
+ def test_error_empty(self):
+ messages = self._makeOne()
+ self.assertEqual(messages.error(), [])
+
+ def test_custom_filled(self):
+ expected = ['one', 'two']
+ messages = self._makeOne({'custom':expected})
+ self.assertEqual(messages.custom('custom'), expected)
+
+ def test_custom_empty(self):
+ messages = self._makeOne()
+ self.assertEqual(messages.custom('custom'), [])
+
diff --git a/pyramid/tests/test_session.py b/pyramid/tests/test_session.py
index 1be010849..bfaa6cd97 100644
--- a/pyramid/tests/test_session.py
+++ b/pyramid/tests/test_session.py
@@ -112,6 +112,61 @@ class TestUnencryptedCookieSession(unittest.TestCase):
self.assertEqual(secure, 'secure')
self.assertEqual(httponly, 'HttpOnly')
+ def test_flash_default(self):
+ request = testing.DummyRequest()
+ session = self._makeOne(request)
+ session.flash('msg1')
+ session.flash('msg2')
+ self.assertEqual(dict(session['_f_']),
+ {'info':['msg1', 'msg2']})
+
+ def test_flash_mixed(self):
+ from pyramid import flash
+ request = testing.DummyRequest()
+ session = self._makeOne(request)
+ session.flash('warn1', flash.WARNING)
+ session.flash('warn2', flash.WARNING)
+ session.flash('err1', flash.ERROR)
+ session.flash('err2', flash.ERROR)
+ self.assertEqual(dict(session['_f_']),
+ {flash.WARNING:['warn1', 'warn2'],
+ flash.ERROR:['err1', 'err2']})
+
+ def test_flash_with_nondefault_queue(self):
+ from pyramid import flash
+ request = testing.DummyRequest()
+ session = self._makeOne(request)
+ session.flash('one_1', queue_name='one')
+ session.flash('one_2', queue_name='one')
+ session.flash('two_1', queue_name='two')
+ session.flash('two_2', queue_name='two')
+ self.assertEqual(dict(session['_f_one']),
+ {flash.INFO:['one_1', 'one_2']})
+ self.assertEqual(dict(session['_f_two']),
+ {flash.INFO:['two_1', 'two_2']})
+
+ def test_unflash_default_queue(self):
+ from pyramid import flash
+ from pyramid.interfaces import IFlashMessages
+ request = testing.DummyRequest()
+ session = self._makeOne(request)
+ storage = {flash.INFO:['one', 'two']}
+ session['_f_'] = storage
+ result = session.unflash()
+ self.assertEqual(dict(result), storage)
+ self.failUnless(IFlashMessages.providedBy(result))
+
+ def test_unflash_nodefault_queue(self):
+ from pyramid import flash
+ from pyramid.interfaces import IFlashMessages
+ request = testing.DummyRequest()
+ session = self._makeOne(request)
+ storage = {flash.INFO:['one', 'two']}
+ session['_f_one'] = storage
+ result = session.unflash('one')
+ self.assertEqual(dict(result), storage)
+ self.failUnless(IFlashMessages.providedBy(result))
+
class Test_manage_accessed(unittest.TestCase):
def _makeOne(self, wrapped):
from pyramid.session import manage_accessed