diff options
| -rw-r--r-- | CHANGES.txt | 5 | ||||
| -rw-r--r-- | TODO.txt | 2 | ||||
| -rw-r--r-- | docs/api.rst | 1 | ||||
| -rw-r--r-- | docs/api/flash.rst | 36 | ||||
| -rw-r--r-- | docs/api/interfaces.rst | 1 | ||||
| -rw-r--r-- | docs/index.rst | 1 | ||||
| -rw-r--r-- | docs/latexindex.rst | 2 | ||||
| -rw-r--r-- | docs/narr/flash.rst | 123 | ||||
| -rw-r--r-- | docs/narr/sessions.rst | 4 | ||||
| -rw-r--r-- | pyramid/flash.py | 32 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 40 | ||||
| -rw-r--r-- | pyramid/session.py | 13 | ||||
| -rw-r--r-- | pyramid/tests/test_flash.py | 81 | ||||
| -rw-r--r-- | pyramid/tests/test_session.py | 55 |
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) ================== @@ -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 |
