Commits

Mike Orr committed a634aee

Flash categories, contributed by Wichert Akkerman.

Comments (0)

Files changed (3)

     considered.
 * webhelpers.pylonslib:
   - is now a package.
+  - The ``Flash`` class now accepts severity categories, thanks to Wichert
+    Akkerman.  The docstring shows how to set up auto-fading messages using
+    Javascript a la Mac OSX's "Growl" feature.  This is backward compatible 
+    although you should delete existing sessions when upgrading from 0.6.x.
   - ``webhelpers.pylonslib.minify`` contains enhanced versions of
     ``javascript_link`` and ``stylesheet_link`` to minify (shrink) files for
     more efficient transmission.  (Untested.)

tests/test_pylonslib_flash.py

+from nose.plugins.skip import SkipTest
+from nose.tools import eq_
+
+from webhelpers.pylonslib import Flash, Message
+
+class FakeSession(dict):
+    def save(self):
+        pass
+
+class TestFlash(object):
+    def setUp(self):
+        try:
+            import pylons
+        except ImportError:
+            raise SkipTest()
+        self._orig_session = pylons.session
+        pylons.session = FakeSession()
+
+    def tearDown(self):
+        import pylons
+        pylons.session = self._orig_session
+
+    def test_flash(self):
+        MESSAGE1 = "Record deleted."
+        MESSAGE2 = "Hope you didn't need it."
+        flash = Flash()
+        flash(MESSAGE1)
+        flash(MESSAGE2, "warning")
+        messages = flash.pop_messages()
+        eq_(len(messages), 2)
+        eq_(messages[0].message, MESSAGE1)
+        eq_(messages[0].category, "notice")
+        eq_(messages[1].message, MESSAGE2)
+        eq_(messages[1].category, "warning")
+        messages = flash.pop_messages()
+        eq_(len(messages), 0)
+
+    def test_multiple_flashes(self):
+        MESSAGE = "Hello, world!"
+        DOOHICKEY_MESSAGE1 = "Added doohickey."
+        DOOHICKEY_MESSAGE2 = "Removed doohickey."
+        flash = Flash()
+        flash2 = Flash("doohickey")
+        flash(MESSAGE)
+        flash2(DOOHICKEY_MESSAGE1)
+        flash2(DOOHICKEY_MESSAGE2)
+        messages = flash.pop_messages()
+        messages2 = flash2.pop_messages()
+        eq_(len(messages), 1)
+        eq_(len(messages2), 2)
+        eq_(messages[0].message, MESSAGE)
+        eq_(messages2[0].message, DOOHICKEY_MESSAGE1)
+        messages = flash.pop_messages()
+        eq_(len(messages), 0)
+        messages2 = flash.pop_messages()
+        eq_(len(messages2), 0)

webhelpers/pylonslib/__init__.py

 # modules should be importable on any Python system for the standard
 # regression tests.
 
+class Message(object):
+    def __init__(self, category, message):
+        self.category=category
+        self.message=message
+
+    def __str__(self):
+        return self.message
+
+    __unicode__ = __str__
+
+
 class Flash(object):
     """Accumulate a list of messages to show at the next page request.
 
             padding: 4px;
             list-style: none;
             }
+
+    Multiple flash objects
+    ======================
+
+    You can define multiple flash objects in your application to display
+    different kinds of messages at different places on the page.  For instance,
+    you might use the main flash object for general messages, and a second
+    flash object for "Added dookickey" / "Removed doohickey" messages next to a
+    doohickey manager.
+
+    Message categories
+    ==================
+
+    WebHelpers 1.0 adds message categories.  These work like severity levels
+    in Python's logging system.  The standard categories are "*warning*",
+    "*notice*", "*error*", and "*success*", with the default being "*notice*".
+    The category is available in the message's ``.category`` attribute, and is
+    normally used to set the container's CSS class.  Unlike the logging system,
+    the flash object does not filter out messages below a certain level; it
+    returns all messages set. 
+
+    You can change the standard categories by overriding the ``.categories``
+    and ``.default_category`` class attributes.
+
+    Note that messages are _not_ grouped by category, nor is it possible to 
+    pop one category of messages while leaving the others.  If you want to 
+    group different kinds of messages together, you should use multiple flash
+    objects.
+
+    Category example
+    ----------------
+
+    Lets show a standard way of using flash messages in your site: we will
+    demonstrate *self-healing messages* (similar to what Growl does on OSX)
+    to show messages in a site.
+
+    To send a message from python just call the flash helper method::
+
+       from myapp.lib.helpers import flash
+
+       flash(u"Settings have been saved")
+
+    This will tell the system to show a message in the rendered page. If you need
+    more control you can specify a message category as well: one of *warning*,
+    *notice*, *error* or *success*. The default category is *notice*. For example::
+
+       from myapp.lib.helpers import flash
+
+       flash(u"Failed to send confirmation email", "warning")
+
+    We will use a very simple markup style: messages will be placed in a ``div``
+    with id ``selfHealingFeedback`` at the end of the document body. The messages
+    are standard paragraphs with a class indicating the message category. For
+    example::
+
+      <html>
+        <body>
+          <div id="content">
+            ...
+            ...
+          </div>
+          <div id="selfHealingFeedback">
+            <p class="success">Succesfully updated your settings</p>
+            <p class="warning">Failed to send confirmation email</p>
+          </div>
+        </body>
+      </html>
+
+    This can easily created from a template. If you are using Genshi this
+    should work:
+
+      <div id="selfHealingFeedback">
+        <p class="notice" py:for="message in h.flash.pop_messages()"
+           py:attrs="{'class' : message.category}" py:content="message">
+          This is a notice.
+        </p>
+      </div>
+
+    The needed CSS is very simple::
+
+        #selfHealingFeedback {
+            position: fixed;
+            top: 20px;
+            left: 20px;
+            z-index: 2000;
+        }
+
+        #selfHealingFeedback p {
+            margin-bottom: 10px;
+            width: 250px;
+            opacity: 0.93;
+        }
+
+        p.notice,p.error,p.success,p.warning {
+            border: 3px solid silver;
+            padding: 10px;
+            -webkit-border-radius: 3px;
+            -moz-border-radius: 3px;
+            border-radius: 3px;
+            -webkit-box-shadow: 0 0 5px silver;
+        }
+
+    Choosing different colours for the categories is left as an excersize
+    for the reader.
+
+    Next we create the javascript that will manage the needed behaviour (this
+    implementation is based on jQuery)::
+
+        function _SetupMessage(el) {
+            var remover = function () {
+                msg.animate({opacity: 0}, "slow")
+                   .slideUp("slow", function() { msg.remove() }); };
+
+            msg.data("healtimer", setTimeout(remover, 10000))
+               .click(function() { clearTimeout(msg.data("healtimer")); remover(); });
+        }
+
+        function ShowMessage(message, category) {
+            if (!category)
+                category="notice";
+
+            var container = $("#selfHealingFeedback");
+
+            if (!container.length)
+                container=$("<div id='selfHealingFeedback'/>").appendTo("body");
+
+            var msg = $("<p/>").addClass(category).html(message);
+            SetupMessage(msg);
+            msg.appendTo(container);
+        }
+
+        $(document).ready(function() {
+            $("#selfHealingFeedback p").each(function() { SetupMessage($(this)); });
+        }
+
+    The ``SetupMessage`` function configures the desired behaviour: a message
+    disappears after 10 seconds, or if you click on it. Removal is done using
+    a simple animation to avoid messages jumping around on the screen.
+
+    This function is called for all messages as soon as the document has fully
+    loaded. The ``ShowMessage`` function works exactly like the ``flash`` method
+    in python: you can call it with a message and optionally a category and it
+    will pop up a new message.
+
+    JSON integration
+    ----------------
+
+    It is not unusal to perform a remote task using a JSON call and show a
+    result message to the user. This can easily be done using a simple wrapper
+    around the ShowMessage method::
+
+        function ShowJSONResponse(info) {
+            if (!info.message)
+                return;
+
+            ShowMessage(info.message, info.message_category);
+        }
+
+    You can use this direct as the success callback for the jQuery AJAX method::
+
+       $.ajax({type: "POST",
+               url:  "http://your.domain/call/json",
+               dataType: "json",
+               success: ShowJSONResponse
+       });
+
+    if you need to perform extra work in your callback method you can call
+    it yourself as well, for example::
+
+       <form action="http://your.domain/call/form">
+         <input type="hidden" name="json_url" value="http://your.domain/call/json">
+         <button>Submit</button>
+       </form>
+
+       <sript type="text/javascript">
+          $(document).ready(function() {
+              $("button").click(function() {
+                  var button = $(this);
+
+                  button.addClass("processing");
+                  $.ajax({type: "POST",
+                          url:  this.form["json_url"].value,
+                          dataType: "json",
+                          success: function(data, status) {
+                              button.removeClass("processing");
+                              ShowJSONResponse(data);
+                           },
+                           error: function(request, status, error) {
+                              button.removeClass("processing");
+                              ShowMessage("JSON call failed", "error");
+                           }
+                  });
+
+                  return false;
+              });
+          });
+       </script>
+
+    This sets up a simple form which can be submitted normally by non-javascript
+    enabled browsers. If a user does hava javascript an AJAX call will be made
+    to the server and the result will be shown in a message. While the call is
+    active the button will be marked with a *processing* class.
+
+    The server can return a message by including a ``message`` field in its
+    response. Optionally a ``message_category`` field can also be included
+    which will be used to determine the message category. For example::
+
+        @jsonify
+        def handler(self):
+           ..
+           ..
+           return dict(message=u"Settings succesfully updated") 
     """
+    
+    # List of allowed categories.  If None, allow any category.
+    categories = ["warning", "notice", "error", "success"]
+    
+    # Default category if none is specified.
+    default_category = "notice"
+
     def __init__(self, session_key="flash"):
         self.session_key = session_key
 
-    def __call__(self, message):
+    def __call__(self, message, category=None):
+        if not category:
+            category = self.default_category
+        elif self.categories and category not in self.categories:
+            raise ValueError("unrecognized category '%s'" % category)
+        # Don't store Message objects in the session, to avoid unpickling
+        # errors in edge cases.
         from pylons import session
-        session.setdefault(self.session_key, []).append(message)
+        session.setdefault(self.session_key, []).append((category, message))
         session.save()
 
     def pop_messages(self):
         from pylons import session
         messages = session.pop(self.session_key, [])
         session.save()
-        return messages
+        return [Message(*m) for m in messages]