[rhmessaging-commits] rhmessaging commits: r967 - in mgmt: bin and 10 other directories.

rhmessaging-commits at lists.jboss.org rhmessaging-commits at lists.jboss.org
Mon Oct 8 11:51:14 EDT 2007


Author: nunofsantos
Date: 2007-10-08 11:51:14 -0400 (Mon, 08 Oct 2007)
New Revision: 967

Added:
   mgmt/COPYING
   mgmt/LICENSE
   mgmt/bin/
   mgmt/bin/cumindev
   mgmt/bin/cumindev-etags
   mgmt/cumin-test-0/
   mgmt/cumin-test-0/bin
   mgmt/cumin-test-0/lib
   mgmt/cumin-test-0/python
   mgmt/cumin-test-0/resources
   mgmt/cumin/
   mgmt/cumin/bin/
   mgmt/cumin/bin/cumin-test
   mgmt/cumin/python/
   mgmt/cumin/python/cumin/
   mgmt/cumin/python/cumin/__init__.py
   mgmt/cumin/python/cumin/cluster.py
   mgmt/cumin/python/cumin/cluster.strings
   mgmt/cumin/python/cumin/demo.py
   mgmt/cumin/python/cumin/exchange.py
   mgmt/cumin/python/cumin/exchange.strings
   mgmt/cumin/python/cumin/model.py
   mgmt/cumin/python/cumin/page.py
   mgmt/cumin/python/cumin/page.strings
   mgmt/cumin/python/cumin/queue.py
   mgmt/cumin/python/cumin/queue.strings
   mgmt/cumin/python/cumin/realm.py
   mgmt/cumin/python/cumin/realm.strings
   mgmt/cumin/python/cumin/server.py
   mgmt/cumin/python/cumin/server.strings
   mgmt/cumin/python/cumin/virtualhost.py
   mgmt/cumin/python/cumin/virtualhost.strings
   mgmt/cumin/python/cumin/widgets.py
   mgmt/cumin/python/cumin/widgets.strings
   mgmt/cumin/python/wooly/
   mgmt/cumin/python/wooly/__init__.py
   mgmt/cumin/python/wooly/debug.py
   mgmt/cumin/python/wooly/devel.py
   mgmt/cumin/python/wooly/forms.py
   mgmt/cumin/python/wooly/forms.strings
   mgmt/cumin/python/wooly/model.py
   mgmt/cumin/python/wooly/pages.py
   mgmt/cumin/python/wooly/parameters.py
   mgmt/cumin/python/wooly/resources.py
   mgmt/cumin/python/wooly/server.py
   mgmt/cumin/python/wooly/widgets.py
   mgmt/cumin/python/wooly/widgets.strings
   mgmt/cumin/resources/
   mgmt/cumin/resources/ajax-test.html
   mgmt/cumin/resources/ajax.js
   mgmt/cumin/resources/exchange-20.png
   mgmt/cumin/resources/exchange-36.png
   mgmt/cumin/resources/exchange.svg
   mgmt/cumin/resources/logo.png
   mgmt/cumin/resources/logo.svg
   mgmt/cumin/resources/object-20.png
   mgmt/cumin/resources/object-36.png
   mgmt/cumin/resources/object.svg
   mgmt/cumin/resources/purple.png
   mgmt/cumin/resources/queue-20.png
   mgmt/cumin/resources/queue-36.png
   mgmt/cumin/resources/queue.svg
   mgmt/cumin/resources/radio-button-checked.png
   mgmt/cumin/resources/radio-button.png
   mgmt/cumin/resources/radio-buttons.svg
   mgmt/cumin/resources/wooly.js
   mgmt/etc/
   mgmt/etc/cumindev.el
   mgmt/etc/cumindev.profile
   mgmt/lib/
   mgmt/misc/
   mgmt/misc/templates.py
   mgmt/notes/
   mgmt/notes/Errors
   mgmt/notes/InterfaceQuestions
   mgmt/notes/ProtocolQuestions
   mgmt/notes/Todo
   mgmt/notes/WoolyOverview
   mgmt/notes/firebug-exception-bug.js
   mgmt/notes/firebug-swallows-exceptions.html
Log:
initial import of cumin code

Added: mgmt/COPYING
===================================================================
--- mgmt/COPYING	                        (rev 0)
+++ mgmt/COPYING	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,15 @@
+Copyright (C) 2007 Red Hat Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

Added: mgmt/LICENSE
===================================================================
--- mgmt/LICENSE	                        (rev 0)
+++ mgmt/LICENSE	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,280 @@
+                    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS

Added: mgmt/bin/cumindev
===================================================================
--- mgmt/bin/cumindev	                        (rev 0)
+++ mgmt/bin/cumindev	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+if [ -z "$CUMINDEV_HOME" ]; then
+    export CUMINDEV_HOME="${HOME}/cumindev"
+fi
+
+echo "CUMINDEV_HOME is ${CUMINDEV_HOME}"
+
+source "${CUMINDEV_HOME}/etc/cumindev.profile"
+
+cumindev-etags
+
+exec emacs -nw -l "${CUMINDEV_HOME}/etc/cumindev.el"


Property changes on: mgmt/bin/cumindev
___________________________________________________________________
Name: svn:executable
   + *

Added: mgmt/bin/cumindev-etags
===================================================================
--- mgmt/bin/cumindev-etags	                        (rev 0)
+++ mgmt/bin/cumindev-etags	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+output="${CUMINDEV_HOME}/etc/cumindev.tags"
+
+find "$CUMINDEV_HOME" -name \*.py -print | etags --output="$output" -
+find "$CUMINDEV_HOME" -name \*.strings -print \
+    | etags --append --output="$output" --regex='/^\[.*\][ \t]*$/\1/' -


Property changes on: mgmt/bin/cumindev-etags
___________________________________________________________________
Name: svn:executable
   + *

Added: mgmt/cumin/bin/cumin-test
===================================================================
--- mgmt/cumin/bin/cumin-test	                        (rev 0)
+++ mgmt/cumin/bin/cumin-test	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+import sys
+from cumin import CuminServer
+
+def load_args(argv):
+    args = dict()
+    key = None
+
+    for arg in sys.argv:
+        if arg.startswith("--"):
+            key = arg[2:]
+        elif key:
+            args[key] = arg
+            key = None
+
+    if key:
+        args[key] = None
+
+    return args
+
+if __name__ == "__main__":
+    args = load_args(sys.argv)
+    
+    port = int(args.get("port", 9090))
+    
+    if "profile" in args:
+	import profile, pstats
+
+	try:
+	    profile.run("CuminServer().run()", "cumin-test-stats")
+	    raise KeyboardInterrupt()
+	except KeyboardInterrupt:
+	    stats = pstats.Stats("cumin-test-stats")
+
+	    stats.sort_stats("cumulative").print_stats(15)
+	    stats.sort_stats("time").print_stats(15)
+
+	    stats.strip_dirs()
+
+	    #stats.print_callers("interpolate")
+	    #stats.print_callees("interpolate")
+    else:
+	CuminServer(port).run()


Property changes on: mgmt/cumin/bin/cumin-test
___________________________________________________________________
Name: svn:executable
   + *

Added: mgmt/cumin/python/cumin/__init__.py
===================================================================
--- mgmt/cumin/python/cumin/__init__.py	                        (rev 0)
+++ mgmt/cumin/python/cumin/__init__.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,78 @@
+import sys, os
+
+from random import randint
+from wooly import Application, Session, Page
+from wooly.pages import CssPage, ResourcePage
+from wooly.server import WebServer
+from wooly.devel import DevelPage
+from wooly.parameters import IntegerParameter
+
+from model import CuminModel
+from demo import DemoData
+from page import CuminPage
+from queue import QueueXmlPage
+
+class Cumin(Application):
+    def __init__(self, model):
+        super(Cumin, self).__init__()
+
+        try:
+            self.home = os.environ["CUMIN_HOME"]
+        except KeyError:
+            sys.exit(1)
+
+        self.add_resource_dir(os.path.join(self.home, "resources"))
+
+        self.model = model
+
+        self.cumin_page = CuminPage(self, "cumin.html")
+        self.set_default_page(self.cumin_page)
+
+        self.add_page(CssPage(self, "cumin.css"))
+        self.add_page(ResourcePage(self, "resource"))
+        self.add_page(CountPage(self, "count"))
+        self.add_page(RandomIntegerPage(self, "randint"))
+        self.add_page(DevelPage(self, "devel.html"))
+        self.add_page(QueueXmlPage(self, "queue.xml"))
+
+class RandomIntegerPage(Page):
+    def __init__(self, app, name):
+        super(RandomIntegerPage, self).__init__(app, name)
+
+        self.min = IntegerParameter(app, "min");
+        self.add_parameter(self.min);
+
+        self.max = IntegerParameter(app, "max");
+        self.add_parameter(self.max);
+
+    def get_content_type(self, session):
+        return Page.xml_content_type
+
+    def do_render(self, session, object):
+        int = randint(self.min.get(session), self.max.get(session))
+        return "%s<integer>%i</integer>" % (Page.xml_1_0_declaration, int)
+
+class CountPage(Page):
+    def __init__(self, app, name):
+        super(CountPage, self).__init__(app, name)
+
+        self.count = 0
+    
+    def get_content_type(self, session):
+        return Page.xml_content_type
+
+    def do_render(self, session, object):
+        self.count += 1
+        return "<count>%i</count>" % self.count
+
+class CuminServer(WebServer):
+    def __init__(self, port=9090):
+        model = CuminModel()
+
+        data = DemoData(model)
+        data.load()
+        data.start_updates()
+
+        app = Cumin(model)
+
+        super(CuminServer, self).__init__(app, port)

Added: mgmt/cumin/python/cumin/cluster.py
===================================================================
--- mgmt/cumin/python/cumin/cluster.py	                        (rev 0)
+++ mgmt/cumin/python/cumin/cluster.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,75 @@
+from wooly import *
+from wooly.widgets import *
+
+from server import *
+from queue import *
+from realm import *
+from exchange import *
+from widgets import *
+
+strings = StringCatalog(__file__)
+
+class ClusterSet(ItemSet):
+    def render_title(self, session, model):
+        return "Clusters (%i)" % len(model.get_clusters())
+
+    def get_items(self, session, model):
+        return sorted(model.get_clusters())
+
+    def render_item_href(self, session, cluster):
+        branch = session.branch()
+        self.page().show_cluster(branch, cluster).show_view(branch)
+        return branch.marshal()
+
+    def render_item_name(self, session, cluster):
+        return cluster.name
+
+class ClusterParameter(Parameter):
+    def do_unmarshal(self, string):
+        return self.app.model.get_cluster(int(string))
+
+    def do_marshal(self, cluster):
+        return str(cluster.id)
+
+class ClusterFrame(CuminFrame):
+    def __init__(self, app, name):
+        super(ClusterFrame, self).__init__(app, name)
+
+        self.param = ClusterParameter(app, "id")
+        self.add_parameter(self.param)
+        self.set_object_attribute(self.param)
+
+        self.view = ClusterView(app, "view")
+        self.add_child(self.view)
+
+    def set_cluster(self, session, cluster):
+        self.param.set(session, cluster)
+
+    def show_view(self, session):
+        return self.show_mode(session, self.view)
+
+    def render_href(self, session, cluster):
+        branch = session.branch()
+        return branch.marshal()
+
+    def render_title(self, session, cluster):
+        return "Cluster '%s'" % cluster.name
+
+class ClusterView(Widget):
+    def __init__(self, app, name):
+        super(ClusterView, self).__init__(app, name)
+
+        self.tabs = TabSet(app, "tabs")
+        self.add_child(self.tabs)
+
+        self.tabs.add_child(self.Servers(app, "servers"))
+
+    def render_title(self, session, cluster):
+        return "Cluster '%s'" % cluster.name
+
+    class Servers(ServerSet):
+        def render_title(self, session, cluster):
+            return "Servers (%i)" % len(cluster.server_items())
+
+        def get_items(self, session, cluster):
+            return sorted(cluster.server_items())

Added: mgmt/cumin/python/cumin/cluster.strings
===================================================================
--- mgmt/cumin/python/cumin/cluster.strings	                        (rev 0)
+++ mgmt/cumin/python/cumin/cluster.strings	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,18 @@
+[ClusterSet.html]
+<table class="ClusterSet mobjects">
+  <tr>
+    <th>Name</th>
+  </tr>
+  {items}
+</table>
+
+[ClusterSet.item_html]
+<tr>
+  <td><a href="{item_href}">{item_name}</a></td>
+</tr>
+
+[ClusterView.html]
+<div class="oblock">
+  <h1>{title}</h1>
+  {tabs}
+</div>

Added: mgmt/cumin/python/cumin/demo.py
===================================================================
--- mgmt/cumin/python/cumin/demo.py	                        (rev 0)
+++ mgmt/cumin/python/cumin/demo.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,115 @@
+from time import sleep
+from threading import Thread
+from random import sample, random, randint
+
+from model import *
+
+class DemoData(object):
+    def __init__(self, model):
+        self.model = model
+
+    def load(self):
+        # XXX need some locking in here
+
+        sgroups = dict()
+
+        for name in ("Geography", "Department"):
+            sgroup = ServerGroup(self.model)
+            sgroup.name = name
+            sgroups[name] = sgroup
+
+        for name in ("West Coast", "East Coast"):
+            sgroup = ServerGroup(self.model)
+            sgroup.name = name
+            sgroups["Geography"].add_child(sgroup)
+            sgroups[name] = sgroup
+
+        for name in ("Marketing", "Sales"):
+            sgroup = ServerGroup(self.model)
+            sgroup.name = name
+            sgroups["Department"].add_child(sgroup)
+            sgroups[name] = sgroup
+
+        clusters = list()
+
+        for cluster_count in range(3):
+            cluster = Cluster(self.model)
+            cluster.name = "cluster" + str(cluster_count)
+            clusters.append(cluster)
+
+        for server_count in range(12):
+            server = Server(self.model)
+            server.name = "server" + str(server_count)
+            server.set_cluster(clusters[server_count % 3])
+
+            vhost = VirtualHost(self.model)
+            vhost.name = "default"
+            server.add_virtual_host(vhost)
+            server.default_virtual_host = vhost
+
+            for name in ("test", "devel"):
+                vhost = VirtualHost(self.model)
+                vhost.name = name
+                server.add_virtual_host(vhost)
+
+            for vhost in server.virtual_host_items():
+                for name in ("realm0", "realm1", "realm2"):
+                    realm = Realm(self.model)
+                    realm.name = name
+                    vhost.add_realm(realm)
+
+                for name in ("amq.direct", "amq.fanout",
+                             "amq.topic", "amq.match"):
+                    exchange = Exchange(self.model)
+                    exchange.name = name
+                    vhost.add_exchange(exchange)
+
+                for queue_count in range(10):
+                    queue = Queue(self.model)
+                    queue.name = "queue" + str(queue_count)
+                    vhost.add_queue(queue)
+
+    def start_updates(self):
+        thread = UpdateThread(self.model)
+        thread.start()
+
+class UpdateThread(Thread):
+    def __init__(self, model):
+        super(UpdateThread, self).__init__()
+
+        self.model = model
+        self.setDaemon(True)
+    
+    def run(self):
+        while True:
+            sleep(1)
+
+            for server in self.model.get_servers():
+                for vhost in server.virtual_host_items():
+                    for queue in vhost.queue_items():
+                        queue.lock()
+                        try:
+                            queue.message_count += 1
+
+                            if random() < 0.01:
+                                queue.error_count += 1
+
+                            if random() < 0.01:
+                                queue.warning_count += 1
+                        finally:
+                            queue.unlock()
+
+if __name__ == "__main__":
+    import sys
+    
+    model = CuminModel()
+
+    data = DemoData(model)
+    data.load()
+
+    sys.stdout.write("<?xml version=\"1.0\"?><model>")
+
+    for server in model.get_servers():
+        server.write_xml(sys.stdout)
+
+    sys.stdout.write("</model>")

Added: mgmt/cumin/python/cumin/exchange.py
===================================================================
--- mgmt/cumin/python/cumin/exchange.py	                        (rev 0)
+++ mgmt/cumin/python/cumin/exchange.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,277 @@
+from wooly import *
+from wooly.widgets import *
+from wooly.forms import *
+from wooly.resources import *
+
+from model import *
+from widgets import *
+
+strings = StringCatalog(__file__)
+
+class ExchangeParameter(Parameter):
+    def do_unmarshal(self, string):
+        return self.app.model.get_exchange(int(string))
+
+    def do_marshal(self, exchange):
+        return str(exchange.id)
+
+class ExchangeInputSet(RadioInputSet):
+    def __init__(self, app, name, form):
+        super(ExchangeInputSet, self).__init__(app, name, form)
+        
+        param = ExchangeParameter(app, "param")
+        self.add_parameter(param)
+        self.set_parameter(param)
+
+    def get_items(self, session, vhost):
+        return sorted(vhost.exchange_items())
+
+    def render_item_value(self, session, exchange):
+        return exchange.id
+
+    def render_item_content(self, session, exchange):
+        return exchange.name
+
+    def render_item_checked_attr(self, session, exchange):
+        return exchange is self.param.get(session) and "checked=\"checked\""
+
+class ExchangeSet(ItemSet):
+    def render_title(self, session, vhost):
+        return "Exchanges (%s)" % len(vhost.exchange_items())
+
+    def get_items(self, session, vhost):
+        return sorted(vhost.exchange_items())
+
+    def render_item_href(self, session, exchange):
+        branch = session.branch()
+        self.page().show_exchange(branch, exchange).show_view(branch)
+        return branch.marshal()
+
+    def render_item_name(self, session, exchange):
+        return exchange.name
+
+    def render_item_flags(self, session, exchange):
+        flags = list()
+        return ", ".join(flags)
+
+    def render_item_config(self, session, exchange):
+        return "%i bindings" % len(exchange.binding_items())
+
+    def render_item_status(self, session, exchange):
+        return "2 errors"
+
+class ExchangeFrame(CuminFrame):
+    def __init__(self, app, name):
+        super(ExchangeFrame, self).__init__(app, name)
+
+        self.param = ExchangeParameter(app, "id")
+        self.add_parameter(self.param)
+        self.set_object_attribute(self.param)
+
+        self.view = ExchangeView(app, "view")
+        self.add_child(self.view)
+
+        self.edit = ExchangeEdit(app, "edit")
+        self.add_child(self.edit)
+
+        self.remove = ExchangeRemove(app, "remove")
+        self.add_child(self.remove)
+
+    def set_exchange(self, session, exchange):
+        return self.param.set(session, exchange)
+
+    def show_view(self, session):
+        return self.show_mode(session, self.view)
+
+    def show_edit(self, session):
+        return self.show_mode(session, self.edit)
+
+    def show_remove(self, session):
+        return self.show_mode(session, self.remove)
+
+    def render_href(self, session, exchange):
+        branch = session.branch()
+        self.show_view(branch)
+        return branch.marshal()
+
+    def render_title(self, session, exchange):
+        return "Exchange '%s'" % exchange.name
+
+class ExchangeView(Widget):
+    def __init__(self, app, name):
+        super(ExchangeView, self).__init__(app, name)
+
+        self.tabs = TabSet(app, "tabs")
+        self.add_child(self.tabs)
+
+        self.tabs.add_child(ExchangeBindingSet(app, "bindings"))
+        self.tabs.add_child(self.ExchangeLog(app, "log"))
+
+    class ExchangeLog(Widget):
+        def render_title(self, session, exchange):
+            return "Log Messages"
+    
+    def render_title(self, session, exchange):
+        return "Exchange '%s'" % exchange.name
+
+    def render_exchange_name(self, session, exchange):
+        return exchange.name
+
+    def render_type(self, session, exchange):
+        if exchange.type == "direct":
+            return "Direct"
+        elif exchange.type == "topic":
+            return "Topic"
+        elif exchange.type == "fanout":
+            return "Fan Out"
+        else:
+            raise Exception()
+
+    def render_edit_exchange_href(self, session, exchange):
+        branch = session.branch()
+        self.page().show_exchange(branch, exchange).show_edit(branch)
+        return branch.marshal()
+
+    def render_remove_exchange_href(self, session, exchange):
+        branch = session.branch()
+        self.page().show_exchange(branch, exchange).show_remove(branch)
+        return branch.marshal()
+
+class ExchangeBindingSet(ItemSet):
+    def render_title(self, session, exchange):
+        return "Bindings (%i)" % len(exchange.binding_items())
+
+    def get_items(self, session, exchange):
+        return sorted(exchange.binding_items())
+
+    def render_item_href(self, session, binding):
+        branch = session.branch()
+        self.page().show_queue(branch, binding.queue)
+        return branch.marshal()
+    
+    def render_item_name(self, session, binding):
+        return binding.get_queue().name
+
+    def render_item_routing_key(self, session, binding):
+        return binding.routing_key
+
+class ExchangeForm(CuminForm):
+    def __init__(self, app, name):
+        super(ExchangeForm, self).__init__(app, name)
+
+        self.exchange_name = TextInput(app, "exchange_name", self)
+        self.add_child(self.exchange_name)
+
+        self.type = Parameter(app, "type")
+        self.type.set_default("direct")
+        self.add_parameter(self.type)
+
+        self.direct = RadioInput(app, "direct", self)
+        self.direct.set_parameter(self.type)
+        self.direct.set_value("direct")
+        self.add_child(self.direct)
+
+        self.topic = RadioInput(app, "topic", self)
+        self.topic.set_parameter(self.type)
+        self.topic.set_value("topic")
+        self.add_child(self.topic)
+
+        self.fanout = RadioInput(app, "fanout", self)
+        self.fanout.set_parameter(self.type)
+        self.fanout.set_value("fanout")
+        self.add_child(self.fanout)
+
+    def validate(self, session):
+        valid = True
+
+        name = self.exchange_name.get(session)
+
+        if name == "":
+            valid = False
+            self.exchange_name.add_error(session, """
+            The exchange name is empty; it is required
+            """)
+        elif " " in name:
+            valid = False
+            self.exchange_name.add_error(session, """
+            The exchange name is invalid; allowed characters are
+            letters, digits, ".", and "_"
+            """)
+
+        return valid
+
+class ExchangeAdd(ExchangeForm):
+    def on_cancel(self, session, vhost):
+        branch = session.branch()
+        self.page().show_virtual_host(branch, vhost).show_view(branch)
+        session.set_redirect(branch.marshal())
+
+    def on_submit(self, session, vhost):
+        if self.validate(session):
+            exchange = Exchange(self.app.model)
+
+            exchange.lock()
+            try:
+                exchange.name = self.exchange_name.get(session)
+                exchange.type = self.type.get(session)
+            finally:
+                exchange.unlock()
+
+            vhost.add_exchange(exchange)
+
+            branch = session.branch()
+            self.page().show_exchange(branch, exchange).show_view(branch)
+            session.set_redirect(branch.marshal())
+
+    def render_title(self, session, vhost):
+        return "Add Exchange to Virtual Host '%s'" % vhost.name
+        
+class ExchangeEdit(ExchangeForm):
+    def on_cancel(self, session, exchange):
+        branch = session.branch()
+        self.page().show_exchange(branch, exchange).show_view(branch)
+        session.set_redirect(branch.marshal())
+
+    def on_submit(self, session, exchange):
+        if self.validate(session):
+            exchange.lock()
+            try:
+                exchange.name = self.exchange_name.get(session)
+                exchange.type = self.type.get(session)
+            finally:
+                exchange.unlock()
+
+            branch = session.branch()
+            self.page().show_exchange(branch, exchange).show_view(branch)
+            session.set_redirect(branch.marshal())
+
+    def on_display(self, session, exchange):
+        self.exchange_name.set(session, exchange.name)
+        self.type.set(session, exchange.type)
+
+    def render_title(self, session, exchange):
+        return "Edit Exchange '%s'" % exchange.name
+
+class ExchangeRemove(CuminConfirmForm):
+    def on_confirm(self, session, exchange):
+        vhost = exchange.get_virtual_host()
+
+        exchange.remove()
+
+        branch = session.branch()
+        self.page().show_virtual_host(branch, vhost).show_view(branch)
+        session.set_redirect(branch.marshal())
+
+    def on_cancel(self, session, exchange):
+        branch = session.branch()
+        self.page().show_exchange(branch, exchange).show_view(branch)
+        session.set_redirect(branch.marshal())
+
+    def render_title(self, session, exchange):
+        return "Remove Exchange '%s'" % exchange.name
+
+    def render_confirm_content(self, session, exchange):
+        return "Yes, Remove Exchange '%s'" % exchange.name
+
+    def render_cancel_content(self, session, exchange):
+        return "No, Cancel"

Added: mgmt/cumin/python/cumin/exchange.strings
===================================================================
--- mgmt/cumin/python/cumin/exchange.strings	                        (rev 0)
+++ mgmt/cumin/python/cumin/exchange.strings	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,105 @@
+[ExchangeInputSet.item_html]
+<div class="field">
+  <input type="radio" name="{name}" value="{item_value}" tabindex="{tabindex}" {item_checked_attr}/>
+  {item_content}
+</div>
+
+[ExchangeSet.css]
+ul.ExchangeSet li:before {
+  content: url(resource?name=exchange-20.png);
+  vertical-align: -30%;
+  padding: 0 0.25em;
+}
+
+[ExchangeSet.html]
+<table class="ExchangeSet mobjects">
+  <tr>
+    <th>Name</th>
+    <th>Configuration</th>
+    <th>Status</th>
+  </tr>
+{items}
+</table>
+
+[ExchangeSet.item_html]
+<tr>
+  <td><a href="{item_href}">{item_name}</a></td>
+  <td>{item_config}</td>
+  <td>{item_status}</td>
+</tr>
+
+[ExchangeForm.html]
+<form id="{id}" class="ExchangeForm mform" method="post" action="?">
+  <div class="head">
+    <h1>{title}</h1>
+  </div>
+  <div class="body">
+    <span class="legend">Name</span>
+    <fieldset>
+      <div class="field">{exchange_name}</div>
+    </fieldset>
+    <span class="legend">Type</span>
+    <fieldset>
+      <div class="field">
+        {direct}
+        <em>Direct:</em> Route messages to queues by queue name
+      </div>
+      <div class="field">
+        {topic}
+        <em>Topic:</em> Route messages to queues by topic keyword match
+      </div>
+      <div class="field">
+        {fanout}
+        <em>Fan Out:</em> Lorem ipsum gloria dei ipso facto ad nauseum
+      </div>
+    </fieldset>
+{hidden_inputs}
+  </div>
+  <div class="foot">
+    <div style="display: block; float: left;"><button>Help</help></div>
+{cancel}
+{submit}
+  </div>
+</form>
+<script defer="defer">
+(function() {
+    // elements[0] is a fieldset, at least in firefox
+    var elem = wooly.doc().elem("{id}").node.elements[1];
+    elem.focus();
+    elem.select();
+}())
+</script>
+
+[ExchangeView.html]
+<div class="ExchangeView oblock">
+  <h1><img src="resource?name=exchange-36.png"> {title}</h1>
+
+  <dl class="properties">
+    <dt>Exchange Name</dt><dd>{exchange_name}</dd>
+    <dt>Type</dt><dd>{type}</dd>
+  </dl>
+
+  <ul class="actions">
+    <li><a href="{edit_exchange_href}">Edit Exchange</a></li>
+    <li><a href="{remove_exchange_href}">Remove Exchange</a></li>
+  </ul>
+
+  {tabs}
+</div>
+
+[ExchangeBindingSet.html]
+<table class="ExchangeBindingSet mobjects">
+  <tr>
+    <th>Queue</th>
+    <th>Routing Key</th>
+    <th></th>
+  </tr>
+{items}
+</table>
+
+[ExchangeBindingSet.item_html]
+<tr>
+  <td><a href="{item_href}">{item_name}</a></td>
+  <td>{item_routing_key}</td>
+  <td><a class="action" href="">Remove</a></td>
+</tr>

Added: mgmt/cumin/python/cumin/model.py
===================================================================
--- mgmt/cumin/python/cumin/model.py	                        (rev 0)
+++ mgmt/cumin/python/cumin/model.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,282 @@
+from wooly import *
+from wooly.model import *
+
+class CuminModel(Model):
+    def __init__(self):
+        super(CuminModel, self).__init__()
+        
+        self.cluster = ModelClass(self, "cluster")
+        self.server = ModelClass(self, "server")
+        self.server_group = ModelClass(self, "server_group")
+        self.virtual_host = ModelClass(self, "virtual_host")
+        self.virtual_host_group = ModelClass(self, "virtual_host_group")
+        self.queue = ModelClass(self, "queue")
+        self.exchange = ModelClass(self, "exchange")
+        self.realm = ModelClass(self, "realm")
+        self.binding = ModelClass(self, "binding")
+
+        assoc = ModelAssociation(self, "cluster_to_servers")
+        assoc.add_endpoint(self.server, "cluster", "0..1")
+        assoc.add_endpoint(self.cluster, "server", "0..n")
+
+        assoc = ModelAssociation(self, "server_to_virtual_hosts")
+        assoc.add_endpoint(self.server, "virtual_host", "0..n")
+        assoc.add_endpoint(self.virtual_host, "server", "0..1")
+
+        assoc = ModelAssociation(self, "server_groups_to_servers")
+        assoc.add_endpoint(self.server, "server_group", "0..n")
+        assoc.add_endpoint(self.server_group, "server", "0..n")
+
+        assoc = ModelAssociation(self, "server_groups_to_server_groups")
+        assoc.add_endpoint(self.server_group, "parent", "0..n")
+        assoc.add_endpoint(self.server_group, "child", "0..n")
+
+        assoc = ModelAssociation(self, "virtual_host_groups_to_virtual_hosts")
+        assoc.add_endpoint(self.virtual_host, "virtual_host_group", "0..n")
+        assoc.add_endpoint(self.virtual_host_group, "virtual_host", "0..n")
+
+        assoc = ModelAssociation \
+                (self, "virtual_host_groups_to_virtual_host_groups")
+        assoc.add_endpoint(self.virtual_host_group, "parent", "0..n")
+        assoc.add_endpoint(self.virtual_host_group, "child", "0..n")
+        
+        assoc = ModelAssociation(self, "virtual_host_to_queues")
+        assoc.add_endpoint(self.virtual_host, "queue", "0..n")
+        assoc.add_endpoint(self.queue, "virtual_host", "0..1")
+
+        assoc = ModelAssociation(self, "virtual_host_to_exchanges")
+        assoc.add_endpoint(self.virtual_host, "exchange", "0..n")
+        assoc.add_endpoint(self.exchange, "virtual_host", "0..1")
+
+        assoc = ModelAssociation(self, "virtual_host_to_realms")
+        assoc.add_endpoint(self.virtual_host, "realm", "0..n")
+        assoc.add_endpoint(self.realm, "virtual_host", "0..1")
+
+        assoc = ModelAssociation(self, "realms_to_queues")
+        assoc.add_endpoint(self.realm, "queue", "0..n")
+        assoc.add_endpoint(self.queue, "realm", "0..n")
+
+        assoc = ModelAssociation(self, "realms_to_exchanges")
+        assoc.add_endpoint(self.realm, "exchange", "0..n")
+        assoc.add_endpoint(self.exchange, "realm", "0..n")
+
+        assoc = ModelAssociation(self, "queue_to_bindings")
+        assoc.add_endpoint(self.queue, "binding", "0..n")
+        assoc.add_endpoint(self.binding, "queue", "0..1")
+
+        assoc = ModelAssociation(self, "exchange_to_bindings")
+        assoc.add_endpoint(self.exchange, "binding", "0..n")
+        assoc.add_endpoint(self.binding, "exchange", "0..1")
+
+    def get_cluster(self, id):
+        return self.get_index(self.cluster).get(id)
+
+    def get_clusters(self):
+        return self.get_index(self.cluster).values()
+
+    def get_server(self, id):
+        return self.get_index(self.server).get(id)
+
+    def get_servers(self):
+        return self.get_index(self.server).values()
+
+    def get_server_group(self, id):
+        return self.get_index(self.server_group).get(id)
+
+    def get_server_groups(self):
+        return self.get_index(self.server_group).values()
+    
+    def get_virtual_host(self, id):
+        return self.get_index(self.virtual_host).get(id)
+
+    def get_queue(self, id):
+        return self.get_index(self.queue).get(id)
+
+    def get_exchange(self, id):
+        return self.get_index(self.exchange).get(id)
+
+    def get_realm(self, id):
+        return self.get_index(self.realm).get(id)
+
+class Cluster(ModelObject):
+    def __init__(self, model):
+        super(Cluster, self).__init__(model, model.cluster)
+
+        self.name = None
+
+class Server(ModelObject):
+    def __init__(self, model):
+        super(Server, self).__init__(model, model.server)
+
+        self.name = None
+        self.default_virtual_host = None
+
+    def write_xml(self, writer):
+        writer.write("<server id=\"server-%i\">" % self.id)
+        writer.write("<name>" + self.name + "</name>")
+        writer.write("<default-virtual-host ref=\"virtual-host-%i\"/>" \
+                     % self.default_virtual_host.id)
+
+        for vhost in self.virtual_host_items():
+            vhost.write_xml(writer)
+
+        writer.write("</server>")
+
+class ServerGroup(ModelObject):
+    def __init__(self, model):
+        super(ServerGroup, self).__init__(model, model.server_group)
+
+        self.name = None
+
+class VirtualHost(ModelObject):
+    def __init__(self, model):
+        super(VirtualHost, self).__init__(model, model.virtual_host)
+
+        self.name = None
+
+        # XXX do this via associations? XXX this will leak a ref if
+        # the default exchange is removed
+
+        self.default_exchange = Exchange(model)
+        self.default_exchange.name = "default"
+        self.add_exchange(self.default_exchange)
+
+    def add_queue(self, queue):
+        self.do_add_queue(queue)
+
+        # Default binding
+
+        binding = Binding(self.model)
+        binding.routing_key = queue.name
+        binding.set_queue(queue)
+        binding.set_exchange(self.default_exchange)
+
+    def write_xml(self, writer):
+        writer.write("<virtual-host id=\"virtual-host-%i\">" % self.id)
+        writer.write("<name>%s</name>" % self.name)
+        writer.write("<default-exchange ref=\"exchange-%i\"/>" \
+                     % self.default_exchange.id)
+
+        for queue in self.queue_items():
+            queue.write_xml(writer)
+
+        for exchange in self.exchange_items():
+            exchange.write_xml(writer)
+
+        for realm in self.realm_items():
+            realm.write_xml(writer)
+
+        writer.write("</virtual-host>")
+
+class VirtualHostGroup(ModelObject):
+    def __init__(self, model):
+        super(VirtualHostGroup, self).__init__(model, model.virtual_host_group)
+
+        self.name = None
+
+class Realm(ModelObject):
+    def __init__(self, model):
+        model.lock()
+        
+        super(Realm, self).__init__(model, model.realm)
+
+        self.name = None
+
+        model.unlock()
+
+    def write_xml(self, writer):
+        writer.write("<realm id=\"realm-%i\">" % self.id)
+        writer.write("<name>%s</name>" % self.name)
+
+        for queue in self.queue_items():
+            writer.write("<queue ref=\"queue-%i\"/>" % queue.id)
+
+        for exchange in self.exchange_items():
+            writer.write("<exchange ref=\"exchange-%i\"/>" % exchange.id)
+
+        writer.write("</realm>")
+
+class Queue(ModelObject):
+    def __init__(self, model):
+        super(Queue, self).__init__(model, model.queue)
+
+        self.name = None
+        self.is_passive = False
+        self.is_durable = True
+        self.is_exclusive = False
+        self.is_auto_delete = False
+        self.latency_priority = "m" # h, m, or l
+
+        self.message_count = 41
+        self.error_count = 0
+        self.warning_count = 0
+
+    def remove(self):
+        for binding in self.binding_items().copy():
+            binding.remove()
+
+        super(Queue, self).remove()
+
+    def purge(self):
+        pass
+
+    def write_xml(self, writer):
+        writer.write("<queue id=\"queue-%i\">" % self.id)
+        writer.write("<name>%s</name>" % self.name)
+        writer.write("<latency-priority>%s</latency-priority>" \
+                     % self.latency_priority)
+        writer.write("<message-count>%i</message-count>" % self.message_count)
+        writer.write("<error-count>%i</error-count>" % self.error_count)
+        writer.write("<warning-count>%i</warning-count>" % self.warning_count)
+
+        for realm in self.realm_items():
+            writer.write("<realm ref=\"realm-%i\"/>" % realm.id)
+
+        for binding in self.binding_items():
+            binding.write_xml(writer)
+            
+        writer.write("</queue>")
+
+class Exchange(ModelObject):
+    def __init__(self, model):
+        super(Exchange, self).__init__(model, model.exchange)
+
+        self.name = None
+        self.type = "direct" # in ("direct", "topic", "fanout")
+        self.is_passive = False
+        self.is_durable = True
+        self.is_auto_delete = False
+        self.is_internal = False
+
+    def remove(self):
+        for binding in self.binding_items().copy():
+            binding.remove()
+
+        super(Exchange, self).remove()
+
+    def write_xml(self, writer):
+        writer.write("<exchange id=\"exchange-%i\">" % self.id)
+        writer.write("<name>%s</name>" % self.name)
+        #writer.write("<error-count>%i</error-count>" % self.error_count)
+        #writer.write("<warning-count>%i</warning-count>" % self.warning_count)
+
+        for realm in self.realm_items():
+            writer.write("<realm ref=\"realm-%i\"/>" % realm.id)
+
+        for binding in self.binding_items():
+            binding.write_xml(writer)
+            
+        writer.write("</exchange>")
+
+class Binding(ModelObject):
+    def __init__(self, model):
+        super(Binding, self).__init__(model, model.binding)
+
+        self.routing_key = None
+
+    def write_xml(self, writer):
+        writer.write("<binding id=\"binding-%i\">" % self.id)
+        writer.write("<exchange ref=\"exchange-%i\"/>" % self.exchange.id)
+        writer.write("<queue ref=\"queue-%i\"/>" % self.queue.id)
+        writer.write("<routing-key>%s</routing-key>" % self.routing_key)
+        writer.write("</binding>")

Added: mgmt/cumin/python/cumin/page.py
===================================================================
--- mgmt/cumin/python/cumin/page.py	                        (rev 0)
+++ mgmt/cumin/python/cumin/page.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,143 @@
+from wooly import *
+from wooly.debug import *
+from wooly.widgets import *
+from wooly.resources import *
+
+from server import *
+from cluster import *
+from widgets import *
+
+strings = StringCatalog(__file__)
+
+class CuminPage(Page):
+    def __init__(self, app, name):
+        super(CuminPage, self).__init__(app, name)
+
+        self.frames = self.FramesAttribute(app, "frames")
+        self.add_attribute(self.frames)
+
+        self.modal = Attribute(app, "modal")
+        self.add_attribute(self.modal)
+
+        self.citem = self.ContextItem(app, "citem")
+        self.add_child(self.citem)
+
+        self.main = MainFrame(app, "main")
+        self.add_child(self.main)
+
+    class FramesAttribute(Attribute):
+        def get_default(self, session):
+            return list()
+
+    def set_modal(self, session, modal):
+        self.modal.set(session, modal)
+
+    def save_session(self, session):
+        self.app.sessions.append(session)
+
+    def show_server(self, session, server):
+        return self.main.show_server(session, server)
+
+    def show_cluster(self, session, cluster):
+        return self.main.show_cluster(session, cluster)
+
+    def show_virtual_host(self, session, vhost):
+        frame = self.show_server(session, vhost.server)
+        return frame.show_virtual_host(session, vhost)
+
+    def show_queue(self, session, queue):
+        frame = self.show_virtual_host(session, queue.virtual_host)
+        return frame.show_queue(session, queue)
+
+    def show_exchange(self, session, exchange):
+        frame = self.show_virtual_host(session, exchange.virtual_host)
+        return frame.show_exchange(session, exchange)
+
+    def render_title(self, session, object):
+        return "Cumin"
+
+    def render_class(self, session, object):
+        return self.modal.get(session) and "modal"
+
+    def render_content(self, session, object):
+        return self.main.render(session, object)
+
+    def get_frames(self, session):
+        return self.frames.get(session)
+
+    def render_context_items(self, session, object):
+        writer = Writer()
+
+        for frame in self.get_frames(session):
+            writer.write(self.citem.render(session, frame))
+
+        return writer.to_string()
+
+    # XXX use a child template instead
+    class ContextItem(Widget):
+        def render_href(self, session, frame):
+            return frame.render_href(session, frame.get_object(session))
+
+        def render_content(self, session, frame):
+            return frame.render_title(session, frame.get_object(session))
+
+class MainFrame(CuminFrame):
+    def __init__(self, app, name):
+        super(MainFrame, self).__init__(app, name)
+
+        self.view = MainView(app, "view")
+        self.add_child(self.view)
+
+        self.server = ServerFrame(app, "server")
+        self.add_child(self.server)
+
+        self.cluster = ClusterFrame(app, "cluster")
+        self.add_child(self.cluster)
+
+    def get_object(self, session):
+        return self.app.model
+
+    def show_view(self, session):
+        return self.show_mode(session, self.view)
+
+    def show_server(self, session, server):
+        self.server.set_server(session, server)
+        return self.show_mode(session, self.server)
+
+    def show_cluster(self, session, cluster):
+        self.cluster.set_cluster(session, cluster)
+        return self.show_mode(session, self.cluster)
+
+    def render_href(self, session, model):
+        branch = session.branch()
+        self.show_view(branch)
+        return branch.marshal()
+
+    def render_title(self, session, model):
+        return "<img src=\"resource?name=logo.png\"/>"
+
+class MainView(Widget):
+    def __init__(self, app, name):
+        super(MainView, self).__init__(app, name)
+
+        self.tabs = TabSet(app, "tabs")
+        self.add_child(self.tabs)
+
+        self.tabs.add_child(self.ServerTab(app, "servers"))
+        self.tabs.add_child(ClusterSet(app, "clusters"))
+
+    def render_title(self, session, model):
+        return "Red Hat Messaging"
+
+    class ServerTab(TabSet):
+        def __init__(self, app, name):
+            super(MainView.ServerTab, self).__init__(app, name)
+
+            self.servers = ServerSet(app, "servers")
+            self.add_child(self.servers)
+
+            self.groups = ServerGroupTree(app, "groups")
+            self.add_child(self.groups)
+
+        def render_title(self, session, model):
+            return "Servers (%i)" % len(model.get_servers())

Added: mgmt/cumin/python/cumin/page.strings
===================================================================
--- mgmt/cumin/python/cumin/page.strings	                        (rev 0)
+++ mgmt/cumin/python/cumin/page.strings	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,310 @@
+[CuminPage.css]
+body {
+  margin: 0;
+  padding: 0;
+  background-color: #fff;
+  font-size: 0.9em;
+}
+
+body.modal #head {
+  opacity: 0.2;
+}
+
+body.modal {
+  background-color: #f7f7f7;
+}
+
+img {
+  border: none;
+}
+
+* {
+  text-decoration: none;
+}
+
+a {
+  color: #06c;
+}
+
+#head, #foot {
+  padding: 0.5em 0.75em 0.4em 0.75em;
+}
+
+#head {
+  background-color: #564979;
+}
+
+#body {
+  padding: 1em;
+}
+
+#logo {
+  vertical-align: -15%;
+}
+
+h1, h2 {
+  margin: 0;
+}
+
+h1 {
+  font-size: 1.1em;
+}
+
+h1 img {
+  vertical-align: -50%;
+  margin: 0 0.5em 0 0;
+}
+
+h2 {
+  font-size: 1em;
+  font-weight: normal;
+}
+
+.oblock {
+  padding: 0;
+  background-color: white;
+}
+
+.iblock {
+  margin: 0;
+  padding: 0 1em;
+}
+
+ul#context {
+  display: inline;
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+ul#context li {
+  display: inline;
+}
+
+ul#context li:after {
+  content: " > ";
+  font-weight: bold;
+  font-size: 0.8em;
+  color: #fff;
+}
+
+ul#context li:last-child:after {
+  content: "";
+}
+
+ul#context li a {
+  color: #ff9f00;
+}
+
+ul#context li:first-child a {
+  vertical-align: -15%;  
+}
+
+ul#context li:last-child a {
+  color: #fff;
+}
+
+ul.actions {
+  padding: 0;
+  margin: 1em 0;
+  list-style: none;
+}
+
+dl.properties {
+  margin: 1em 0;
+  width: 25em;
+}
+
+dl.properties dt, dd {
+  border-top: 1px dotted #ddd;
+  padding: 0.25em 0.5em;
+}
+
+dl.properties dt {
+  width: 10em;
+  float: left;
+  background-color: #f7f7f7;
+  margin-right: 0.5em;
+}
+
+ul.mobjects {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+}
+
+ul.mobjects li {
+  margin: 0;
+  border-top: 1px solid #ccc;
+  padding: 0.5em 0;
+}
+
+ul.mobjects li:first-child {
+  margin: 0;
+  border: none;
+}
+
+ul.mobjects li a.action {
+  float: right;
+}
+
+a.action:before, ul.actions li:before {
+  content: "\00BB \0020";
+  font-weight: bold;
+  color: #dc9f2e;
+}
+
+ul.mobjects .flags {
+  font-size: small;
+  font-style: italic;
+}
+
+ul.mobjects .config {
+  padding: 0 0 0 2em;
+}
+
+ul.mobjects .status {
+  padding: 0 0 0 2em;
+  color: #936;
+}
+
+table.mobjects {
+  width: 100%;
+  border-collapse: collapse;
+  margin: 0;
+}
+
+table.mobjects tr {
+  border-top: 1px dotted #ccc;
+  vertical-align: top;
+}
+
+table.mobjects td {
+  padding: 0.5em;
+}
+
+table.mobjects th {
+  padding: 0.25em 0.5em;
+}
+
+table.mobjects th {
+  text-align: left;
+  font-weight: normal;
+  background-color: #f7f7f7;
+}
+
+form.mform {
+  width: 50em;
+  border: 1px solid #ddd;
+  background-color: #fff;
+}
+
+form.mform fieldset {
+  border: none;
+  padding: 0.75em;
+}
+
+form.mform .legend {
+  font-weight: bold;
+}
+
+form.mform .head, .mform .body, .mform .foot {
+  padding: 0.5em 0.75em;
+  margin: 0;
+}
+
+form.mform .head {
+  font-weight: bold;
+  color: white;
+  background-color: #564979;
+}
+
+form.mform .foot {
+  text-align: right;
+  border-top: 1px solid #ddd;
+}
+
+form.mform .field {
+  margin: 0.25em 0;
+}
+
+form.mform .field input {
+  border-style: groove;
+}
+
+form.mform ul.errors {
+  list-style: none;
+  display: block;
+  float: right;
+  color: red;
+  padding: 0.25em 0.5em;
+  border: 1px solid red;
+  margin: 0 0.5em;
+  max-width: 20em;
+}
+
+form.mform button {
+  border-style: groove;
+  padding: 0.25em 0.5em;
+  margin: 0.5em;
+}
+
+ul.radiotabs {
+  list-style: none;
+  margin: 0.25em 0 1em 0;
+  padding: 0;
+}
+
+ul.radiotabs li {
+  display: inline;
+  margin: 0 1em 0 0;
+}
+
+ul.radiotabs li a:before {
+  content: url(resource?name=radio-button.png);
+  margin: 0 0.5em 0 0;
+  vertical-align: -15%;
+}
+
+ul.radiotabs li a.selected {
+  color: black;
+}
+
+ul.radiotabs li a.selected:before {
+  content: url(resource?name=radio-button-checked.png);
+}
+
+[CuminPage.html]
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
+  <head>
+    <title>{title}</title>
+    <link rel="stylesheet" type="text/css" href="cumin.css"/>
+    <script src="resource?name=wooly.js"> </script>
+  </head>
+  <body class="{class}">
+    <div id="head"><ul id="context">{context_items}</ul></div>
+    <div id="body">{content}</div>
+    <div id="foot">
+    </div>
+  </body>
+</html>
+
+[ContextItem.html]
+<li><a href="{href}">{content}</a></li>
+
+[MainView.html]
+<div class="oblock">
+  <h1>{title}</h1>
+  <ul class="actions">
+    <li><a href="">Add Server</a></li>
+    <li><a href="">Add Server Group</a></li>
+    <li><a href="">Add Cluster</a></li>
+  </ul>
+  {tabs}
+</div>
+
+[ServerTab.html]
+<ul class="ServerTab radiotabs tabs">{tabs}</ul>
+<div class="ServerTab mode">{mode}</div>

Added: mgmt/cumin/python/cumin/queue.py
===================================================================
--- mgmt/cumin/python/cumin/queue.py	                        (rev 0)
+++ mgmt/cumin/python/cumin/queue.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,416 @@
+from wooly import *
+from wooly.widgets import *
+from wooly.forms import *
+from wooly.resources import *
+
+from model import *
+from widgets import *
+from exchange import ExchangeInputSet
+
+strings = StringCatalog(__file__)
+
+class QueueXmlPage(Page):
+    def __init__(self, app, name):
+        super(QueueXmlPage, self).__init__(app, name)
+
+        self.queue = QueueParameter(app, "id")
+        self.add_parameter(self.queue)
+    
+    def get_content_type(self, session):
+        return Page.xml_content_type
+
+    def do_render(self, session, object):
+        writer = Writer()
+
+        writer.write(Page.xml_1_0_declaration)
+        self.queue.get(session).write_xml(writer)
+        
+        return writer.to_string()
+
+class QueueSet(ItemSet):
+    def render_title(self, session, vhost):
+        return "Queues (%s)" % len(vhost.queue_items())
+
+    def get_items(self, session, vhost):
+        return sorted(vhost.queue_items())
+
+    def render_item_href(self, session, queue):
+        branch = session.branch()
+        self.page().show_queue(branch, queue).show_view(branch)
+        return branch.marshal()
+
+    def render_item_name(self, session, queue):
+        return queue.name
+
+    def render_item_flags(self, session, queue):
+        flags = list()
+
+        if queue.is_durable:
+            flags.append("Durable")
+
+        if queue.is_auto_delete:
+            flags.append("Auto Delete")
+
+        return ", ".join(flags)
+
+    def render_item_config(self, session, queue):
+        bindings = list()
+        
+        for binding in sorted(queue.binding_items()):
+            name = binding.get_exchange().name
+            key = binding.routing_key
+
+            branch = session.branch()
+            self.page().show_exchange(branch, binding.get_exchange())
+            href = branch.marshal()
+
+            bindings.append("<a href=\"%s\">exchange '%s'</a> with key '%s'"
+                            % (href, name, key))
+
+        return ", ".join(bindings) 
+
+    def render_item_status(self, session, queue):
+        return "%i messages in queue<br/>%i errors, %i warnings" \
+               % (queue.message_count, queue.error_count, queue.warning_count)
+
+class QueueParameter(Parameter):
+    def do_unmarshal(self, string):
+        return self.app.model.get_queue(int(string))
+
+    def do_marshal(self, queue):
+        return str(queue.id)
+
+class QueueFrame(CuminFrame):
+    def __init__(self, app, name):
+        super(QueueFrame, self).__init__(app, name)
+
+        self.param = QueueParameter(app, "id")
+        self.add_parameter(self.param)
+        self.set_object_attribute(self.param)
+
+        self.view = QueueView(app, "view")
+        self.add_child(self.view)
+
+        self.edit = QueueEdit(app, "edit")
+        self.add_child(self.edit)
+
+        self.remove = QueueRemove(app, "remove")
+        self.add_child(self.remove)
+
+        self.binding_add = QueueBindingAdd(app, "binding_add")
+        self.add_child(self.binding_add)
+
+    def set_queue(self, session, queue):
+        return self.param.set(session, queue)
+
+    def show_view(self, session):
+        return self.show_mode(session, self.view)
+
+    def show_edit(self, session):
+        return self.show_mode(session, self.edit)
+
+    def show_remove(self, session):
+        return self.show_mode(session, self.remove)
+
+    def show_binding_add(self, session):
+        return self.show_mode(session, self.binding_add)
+
+    def render_href(self, session, queue):
+        branch = session.branch()
+        self.show_view(branch)
+        return branch.marshal()
+
+    def render_title(self, session, queue):
+        return "Queue '%s'" % queue.name
+
+class QueueStatus(Widget):
+    def render_class(self, session, queue):
+        if queue.error_count:
+            return "QueueStatus red"
+        elif queue.warning_count:
+            return "QueueStatus yellow"
+        else:
+            return "QueueStatus green"
+
+    def render_url(self, session, queue):
+        return "queue.xml?id=%i" % queue.id
+    
+    def render_message_info(self, session, queue):
+        return "%i %s in queue" % \
+               (queue.message_count,
+                queue.message_count == 1 and "message" or "messages")
+
+    def render_error_info(self, session, queue):
+        return "%i %s, %i %s" % \
+               (queue.error_count,
+                queue.error_count == 1 and "error" or "errors",
+                queue.warning_count,
+                queue.warning_count == 1 and "warning" or "warnings")
+
+class QueueView(Widget):
+    def __init__(self, app, name):
+        super(QueueView, self).__init__(app, name)
+
+        self.status = QueueStatus(app, "status")
+        self.add_child(self.status)
+
+        self.tabs = TabSet(app, "tabs")
+        self.add_child(self.tabs)
+
+        self.tabs.add_child(QueueBindingSet(app, "bindings"))
+        self.tabs.add_child(self.QueueLog(app, "log"))
+
+    class QueueLog(Widget):
+        def render_title(self, session, queue):
+            return "Log Messages"
+
+    def render_title(self, session, queue):
+        return "Queue '%s'" % queue.name
+
+    def render_queue_name(self, session, queue):
+        return queue.name
+
+    def render_latency_tuning(self, session, queue):
+        if queue.latency_priority == "h":
+            return "Lower Latency"
+        elif queue.latency_priority == "m":
+            return "Balanced"
+        elif queue.latency_priority == "l":
+            return "Higher Throughput"
+        else:
+            raise Exception()
+
+    def render_edit_queue_href(self, session, queue):
+        branch = session.branch()
+        self.page().show_queue(branch, queue).show_edit(branch)
+        return branch.marshal()
+
+    def render_remove_queue_href(self, session, queue):
+        branch = session.branch()
+        self.page().show_queue(branch, queue).show_remove(branch)
+        return branch.marshal()
+
+    def render_add_binding_href(self, session, queue):
+        branch = session.branch()
+        self.page().show_queue(branch, queue).show_binding_add(branch)
+        return branch.marshal()
+
+class QueueBindingSet(ItemSet):
+    def render_title(self, session, queue):
+        return "Bindings (%i)" % len(queue.binding_items())
+
+    def get_items(self, session, queue):
+        return sorted(queue.binding_items())
+
+    def render_item_href(self, session, binding):
+        branch = session.branch()
+        self.page().show_exchange(branch, binding.get_exchange())
+        return branch.marshal()
+    
+    def render_item_remove_href(self, session, binding):
+        branch = session.branch()
+        return branch.marshal()
+    
+    def render_item_name(self, session, binding):
+        return binding.get_exchange().name
+
+    def render_item_routing_key(self, session, binding):
+        return binding.routing_key
+
+class QueueForm(CuminForm):
+    def __init__(self, app, name):
+        super(QueueForm, self).__init__(app, name)
+
+        self.queue_name = TextInput(app, "queue_name", self)
+        self.add_child(self.queue_name)
+
+        # XXX Convert tuning stuff into single subwidget
+        
+        self.latency_priority = Parameter(app, "tuning")
+        self.latency_priority.set_default("m")
+        self.add_parameter(self.latency_priority)
+
+        self.latency = RadioInput(app, "latency", self)
+        self.latency.set_parameter(self.latency_priority)
+        self.latency.set_value("h")
+        self.add_child(self.latency)
+
+        self.balanced = RadioInput(app, "balanced", self)
+        self.balanced.set_parameter(self.latency_priority)
+        self.balanced.set_value("m")
+        self.add_child(self.balanced)
+
+        self.throughput = RadioInput(app, "throughput", self)
+        self.throughput.set_parameter(self.latency_priority)
+        self.throughput.set_value("l")
+        self.add_child(self.throughput)
+
+    def validate(self, session):
+        valid = True
+
+        name = self.queue_name.get(session)
+
+        if name == "":
+            valid = False
+            self.queue_name.add_error(session, """
+            The queue name is empty; it is required
+            """)
+        elif " " in name:
+            valid = False
+            self.queue_name.add_error(session, """
+            The queue name is invalid; allowed characters are
+            letters, digits, ".", and "_"
+            """)
+
+        return valid
+
+class QueueAdd(QueueForm):
+    def on_cancel(self, session, vhost):
+        branch = session.branch()
+        self.page().show_virtual_host(branch, vhost).show_view(branch)
+        session.set_redirect(branch.marshal())
+
+    def on_submit(self, session, vhost):
+        if self.validate(session):
+            queue = Queue(self.app.model)
+
+            queue.lock()
+            try:
+                queue.name = self.queue_name.get(session)
+                queue.latency_priority = self.latency_priority.get(session)
+            finally:
+                queue.unlock()
+                
+            vhost.add_queue(queue)
+            
+            branch = session.branch()
+            self.page().show_queue(branch, queue).show_view(branch)
+            session.set_redirect(branch.marshal())
+
+    def render_title(self, session, vhost):
+        return "Add Queue to Virtual Host '%s'" % vhost.name
+        
+class QueueEdit(QueueForm):
+    def on_cancel(self, session, queue):
+        branch = session.branch()
+        self.page().show_queue(branch, queue).show_view(branch)
+        session.set_redirect(branch.marshal())
+
+    def on_submit(self, session, queue):
+        if self.validate(session):
+            queue.lock()
+            try:
+                queue.name = self.queue_name.get(session)
+                queue.latency_priority = self.latency_priority.get(session)
+            finally:
+                queue.unlock()
+
+            branch = session.branch()
+            self.page().show_queue(branch, queue).show_view(branch)
+            session.set_redirect(branch.marshal())
+
+    def on_display(self, session, queue):
+        self.queue_name.set(session, queue.name)
+        self.latency_priority.set(session, queue.latency_priority)
+
+    def render_title(self, session, queue):
+        return "Edit Queue '%s'" % queue.name
+
+class QueueRemove(CuminConfirmForm):
+    def on_confirm(self, session, queue):
+        vhost = queue.get_virtual_host()
+
+        queue.remove()
+
+        branch = session.branch()
+        self.page().show_virtual_host(branch, vhost).show_view(branch)
+        session.set_redirect(branch.marshal())
+
+    def on_cancel(self, session, queue):
+        branch = session.branch()
+        self.page().show_queue(branch, queue).show_view(branch)
+        session.set_redirect(branch.marshal())
+
+    def render_title(self, session, queue):
+        return "Remove Queue '%s'" % queue.name
+
+    def render_confirm_content(self, session, queue):
+        return "Yes, Remove Queue '%s'" % queue.name
+
+    def render_cancel_content(self, session, queue):
+        return "No, Cancel"
+
+class QueueBindingAdd(CuminForm):
+    def __init__(self, app, name):
+        super(QueueBindingAdd, self).__init__(app, name)
+
+        self.exchanges = self.Exchanges(app, "exchanges", self)
+        self.add_child(self.exchanges)
+
+        self.routing_key = TextInput(app, "routing_key", self)
+        self.add_child(self.routing_key)
+    
+    def render_title(self, session, queue):
+        return "Add Binding to Queue '%s'" % queue.name
+
+    def on_cancel(self, session, queue):
+        branch = session.branch()
+        self.page().show_queue(branch, queue).show_view(branch)
+        session.set_redirect(branch.marshal())
+
+    def validate(self, session):
+        valid = True
+
+        if not self.routing_key.get(session):
+            valid = False
+            self.routing_key.add_error(session, """
+            The routing key is empty; it is required
+            """)
+
+        if not self.exchanges.get(session):
+            valid = False
+            self.exchanges.add_error(session, """
+            No exchange selected; it is required
+            """)
+
+        return valid
+
+    def on_submit(self, session, queue):
+        if self.validate(session):
+            binding = Binding(self.app.model)
+
+            binding.lock()
+            try:
+                binding.routing_key = self.routing_key.get(session)
+                binding.set_queue(queue)
+                binding.set_exchange(self.exchanges.get(session))
+            finally:
+                binding.unlock()
+
+            branch = session.branch()
+            self.page().show_queue(branch, queue).show_view(branch)
+            session.set_redirect(branch.marshal())
+
+    class Exchanges(ExchangeInputSet):
+        def get_items(self, session, queue):
+            return sorted(queue.virtual_host.exchange_items())
+
+class QueueBindingRemove(CuminConfirmForm):
+    def on_confirm(self, session, binding):
+        branch = session.branch()
+        self.page().show_queue(branch, binding.get_queue()).show_view(branch)
+        session.set_redirect(branch.marshal())
+
+    def on_cancel(self, session, binding):
+        branch = session.branch()
+        self.page().show_queue(branch, binding.get_queue()).show_view(branch)
+        session.set_redirect(branch.marshal())
+    
+    def render_title(self, session, binding):
+        return "Remove Binding"
+
+    def render_confirm_content(self, session, binding):
+        return "Yes, Remove Binding"
+
+    def render_cancel_content(self, session, binding):
+        return "No, Cancel"

Added: mgmt/cumin/python/cumin/queue.strings
===================================================================
--- mgmt/cumin/python/cumin/queue.strings	                        (rev 0)
+++ mgmt/cumin/python/cumin/queue.strings	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,199 @@
+[QueueSet.css]
+ul.QueueSet li:before {
+  content: url(resource?name=queue-20.png);
+  vertical-align: -30%;
+  padding: 0 0.25em;
+}
+
+[QueueSet.html]
+<table class="QueueSet mobjects">
+<tr>
+  <th>Name</th>
+  <th>Exchanges</th>
+  <th>Status</th>
+</tr>
+{items}
+</table>
+
+[QueueSet.item_html]
+<tr>
+  <td><a href="{item_href}">{item_name}</a></td>
+  <td>{item_config}</td>
+  <td>{item_status}</td>
+</tr>
+
+[QueueForm.html]
+<form id="{id}" class="QueueForm mform" method="post" action="?">
+  <div class="head">
+    <h1>{title}</h1>
+  </div>
+  <div class="body">
+    <span class="legend">Name</span>
+    <fieldset>
+      <div class="field">{queue_name}</div>
+    </fieldset>
+    <span class="legend">Latency Tuning</span>
+    <fieldset>
+      <div class="field">
+        {latency}
+        <em>Lower Latency:</em> Tune for shorter delays, with reduced volume
+      </div>
+      <div class="field">
+        {balanced}
+        <em>Balanced</em>
+      </div>
+      <div class="field">
+        {throughput}
+        <em>Higher Throughput:</em> Tune for increased volume, with longer
+        delays
+      </div>
+    </fieldset>
+{hidden_inputs}
+  </div>
+  <div class="foot">
+    <div style="display: block; float: left;"><button>Help</button></div>
+{cancel}
+{submit}
+  </div>
+</form>
+<script defer="defer">
+var id = "{id}";
+(function() {
+    // XXX elements[0] is a fieldset, at least in firefox
+    var elem = wooly.doc().elem(id).node.elements[1];
+    elem.focus();
+    elem.select();
+}())
+</script>
+
+[QueueStatus.css]
+.QueueStatus {
+  float: right;
+  margin: 1em;
+  padding: 0.75em 1em;
+  width: 15em;
+}
+
+.QueueStatus h2 {
+  font-weight: bold;
+  margin: 0 0 0.5em 0;
+}
+
+.QueueStatus.red {
+  border: 1px solid #c99;
+  background-color: #fcc;
+}
+
+.QueueStatus.yellow {
+  border: 1px solid #cc9;
+  background-color: #ffc;
+}
+
+.QueueStatus.green {
+  border: 1px solid #9c9;
+  background-color: #cfc;
+}
+
+[QueueStatus.html]
+<script defer="defer">
+(function() {
+    var updateStatus = function(xml, elem) {
+        var mcount = xml.elems("message-count").next().text().get();
+        var messages = mcount + " " + (mcount == "1" && "message" || "messages");
+
+        var ecount = xml.elems("error-count").next().text().get();
+        var errors = ecount + " " + (ecount == "1" && "error" || "errors");
+
+        var wcount = xml.elems("warning-count").next().text().get();
+        var warnings = wcount + " " + (wcount == "1" && "warning" || "warnings");
+
+        if (ecount != "0") {
+            elem.node.className = "QueueStatus red";
+        } else if (wcount != "0") {
+            elem.node.className = "QueueStatus yellow";
+        } else {
+            elem.node.className = "QueueStatus green";
+        }
+
+        var divs = elem.elems("div");
+        divs.next().set(messages + " in queue");
+        divs.next().set(errors + ", " + warnings);
+    }
+
+    wooly.setIntervalUpdate("{id}", "{url}", updateStatus, 3000);
+}())
+</script>
+<div class="{class}" id="{id}">
+  <h2>Queue Status</h2>
+
+  <div>{message_info}</div>
+  <div>{error_info}</div>
+</div>
+
+[QueueView.html]
+<div class="QueueView oblock">
+  {status}
+
+  <h1><img src="resource?name=queue-36.png"> {title}</h1>
+
+  <dl class="properties">
+    <dt>Queue Name</dt><dd>{queue_name}</dd>
+    <dt>Latency Tuning</dt><dd>{latency_tuning}</dd>
+  </dl>
+
+  <ul class="actions">
+    <li><a href="{edit_queue_href}">Edit Queue</a></li>
+    <li><a href="{remove_queue_href}">Remove Queue</a></li>
+    <li><a href="{add_binding_href}">Add Binding</a></li>
+  </ul>
+
+  {tabs}
+</div>
+
+[QueueBindingSet.html]
+<table class="QueueBindingSet mobjects">
+  <tr>
+    <th>Exchange</th>
+    <th>Routing Key</th>
+    <th></th>
+  </tr>
+{items}
+</table>
+
+[QueueBindingSet.item_html]
+<tr>
+  <td><a href="{item_href}">exchange '{item_name}'</a></td>
+  <td>{item_routing_key}</td>
+  <td><a class="action" href="{item_remove_href}">Remove</a></td>
+</tr>
+
+[QueueBindingAdd.html]
+<form id="{id}" class="QueueBindingAdd mform" method="post" action="?">
+  <div class="head">
+    <h1>{title}</h1>
+  </div>
+  <div class="body">
+    <span class="legend">Exchange</span>
+    <fieldset>{exchanges}</fieldset>
+    <span class="legend">Routing Key</span>
+    <fieldset>
+      <div class="field">{routing_key}</div>
+    </fieldset>
+{hidden_inputs}
+  </div>
+  <div class="foot">
+    <div style="display: block; float: left;"><button>Help</button></div>
+{cancel}
+{submit}
+  </div>
+</form>
+<script defer="defer">
+var id = "{id}";
+(function() {
+    // XXX elements[0] is a fieldset, at least in firefox
+    var elem = wooly.doc().elem(id).node.elements[1];
+    elem.focus();
+    elem.select();
+}())
+</script>
+

Added: mgmt/cumin/python/cumin/realm.py
===================================================================
--- mgmt/cumin/python/cumin/realm.py	                        (rev 0)
+++ mgmt/cumin/python/cumin/realm.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,56 @@
+from wooly import *
+from wooly.widgets import *
+from wooly.forms import *
+from wooly.resources import *
+
+from model import *
+from widgets import *
+
+strings = StringCatalog(__file__)
+
+class RealmSet(ItemSet):
+    def render_title(self, session, vhost):
+        return "Realms (%i)" % len(vhost.realm_items())
+
+    def get_items(self, session, vhost):
+        return sorted(vhost.realm_items())
+
+    def render_item_name(self, session, realm):
+        return realm.name
+
+class RealmParameter(Parameter):
+    def do_unmarshal(self, string):
+        return self.app.model.get_realm(int(string))
+
+    def do_marshal(self, queue):
+        return str(realm.id)
+
+class RealmInputSet(CheckboxInputSet):
+    def __init__(self, app, name, form):
+        super(RealmInputSet, self).__init__(app, name, form)
+        
+        param = ListParameter(app, "param", RealmParameter(app, "item"))
+        self.add_parameter(param)
+        self.set_parameter(param)
+
+    def get_items(self, session, vhost):
+        return sorted(vhost.realm_items())
+
+    # XXX just parked here
+    def do_process(self, session, queue):
+        for realm in self.get(session):
+            if realm not in queue.realm_items():
+                queue.add_realm(realm)
+
+        for realm in list(queue.realm_items()):
+            if realm not in self.get(session):
+                queue.remove_realm(realm)
+
+    def render_item_value(self, session, realm):
+        return realm.id
+
+    def render_item_content(self, session, realm):
+        return realm.name
+
+    def render_item_checked_attr(self, session, realm):
+        return realm in self.param.get(session) and "checked=\"checked\""

Added: mgmt/cumin/python/cumin/realm.strings
===================================================================
--- mgmt/cumin/python/cumin/realm.strings	                        (rev 0)
+++ mgmt/cumin/python/cumin/realm.strings	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,13 @@
+[RealmInputSet.item_html]
+<div class="field">
+  <input type="checkbox" name="{name}" value="{item_value}" tabindex="{tabindex}" {item_checked_attr}/>
+  {item_content}
+</div>
+
+[RealmSet.html]
+<ul class="RealmSet mobjects">{items}</ul>
+
+[RealmSet.item_html]
+<li>
+  <strong><a href="{item_href}">{item_name}</a></strong>
+</li>

Added: mgmt/cumin/python/cumin/server.py
===================================================================
--- mgmt/cumin/python/cumin/server.py	                        (rev 0)
+++ mgmt/cumin/python/cumin/server.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,146 @@
+from wooly import *
+from wooly.widgets import *
+
+from virtualhost import *
+from widgets import *
+
+strings = StringCatalog(__file__)
+
+class ServerSet(ItemSet):
+    def render_title(self, session, model):
+        return "Servers (%i)" % len(model.get_servers())
+
+    def get_items(self, session, model):
+        return sorted(model.get_servers())
+
+    def render_item_href(self, session, server):
+        branch = session.branch()
+        self.page().show_server(branch, server).show_view(branch)
+        return branch.marshal()
+
+    def render_item_name(self, session, server):
+        return server.name
+
+    def render_item_cluster_name(self, session, server):
+        cluster = server.get_cluster()
+
+        if cluster:
+            return cluster.name
+
+    def render_item_cluster_href(self, session, server):
+        cluster = server.get_cluster()
+
+        if cluster:
+            branch = session.branch()
+            self.page().show_cluster(branch, cluster).show_view(branch)
+            return branch.marshal()
+
+class ServerParameter(Parameter):
+    def do_unmarshal(self, string):
+        return self.app.model.get_server(int(string))
+
+    def do_marshal(self, server):
+        return str(server.id)
+
+class ServerFrame(CuminFrame):
+    def __init__(self, app, name):
+        super(ServerFrame, self).__init__(app, name)
+
+        self.param = ServerParameter(app, "id")
+        self.add_parameter(self.param)
+        self.set_object_attribute(self.param)
+
+        self.view = ServerView(app, "view")
+        self.add_child(self.view)
+
+        self.vhost = VirtualHostFrame(app, "vhost")
+        self.add_child(self.vhost)
+
+    def set_server(self, session, server):
+        self.param.set(session, server)
+
+    def show_view(self, session):
+        return self.show_mode(session, self.view)
+
+    def show_virtual_host(self, session, vhost):
+        self.vhost.set_virtual_host(session, vhost)
+        return self.show_mode(session, self.vhost)
+
+    def show_queue(self, session, queue):
+        vhost = self.show_virtual_host(session, queue.virtual_host)
+        return vhost.show_queue(session, queue)
+
+    def show_exchange(self, session, exchange):
+        vhost = self.show_virtual_host(session, exchange.virtual_host)
+        return vhost.show_exchange(session, exchange)
+
+    def render_href(self, session, server):
+        branch = session.branch()
+        self.show_mode(branch, self.view)
+        return branch.marshal()
+
+    def render_title(self, session, server):
+        return "Server '%s'" % server.name
+
+class ServerView(Widget):
+    def __init__(self, app, name):
+        super(ServerView, self).__init__(app, name)
+
+        self.tabs = TabSet(app, "tabs")
+        self.add_child(self.tabs)
+
+        self.vhosts = VirtualHostSet(app, "virtual_hosts")
+        self.tabs.add_child(self.vhosts)
+
+        self.log = self.ServerLog(app, "log")
+        self.tabs.add_child(self.log)
+
+    def render_title(self, session, server):
+        return "Server '%s'" % server.name
+
+    class ServerLog(Widget):
+        def render_title(self, session, server):
+            return "Log Messages"
+
+class ServerGroupTree(Widget):
+    def __init__(self, app, name):
+        super(ServerGroupTree, self).__init__(app, name)
+
+        self.item_tmpl = Template(self, "item_html")
+
+    def render_title(self, session, model):
+        return "Server Groups (%i)" % len(model.get_server_groups())
+
+    def get_root_items(self, session, model):
+        roots = list()
+
+        for group in model.get_server_groups():
+            if not group.parent_items():
+                roots.append(group)
+
+        return roots
+
+    def get_child_items(self, session, group):
+        return group.child_items()
+
+    def render_root_items(self, session, model):
+        roots = self.get_root_items(session, model)
+
+        if roots:
+            writer = Writer()
+
+            for root in roots:
+                self.item_tmpl.render(session, root, writer)
+
+            return writer.to_string()
+
+    def render_child_items(self, session, object):
+        writer = Writer()
+        
+        for child in self.get_child_items(session, object):
+            self.item_tmpl.render(session, child, writer)
+
+        return writer.to_string()
+
+    def render_item_name(self, session, group):
+        return group.name

Added: mgmt/cumin/python/cumin/server.strings
===================================================================
--- mgmt/cumin/python/cumin/server.strings	                        (rev 0)
+++ mgmt/cumin/python/cumin/server.strings	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,44 @@
+[ServerSet.html]
+<table class="ServerSet mobjects">
+  <tr>
+    <th>Name</th>
+    <th>Cluster</th>
+    <th>Status</th>
+  </tr>
+{items}
+</table>
+
+[ServerSet.item_html]
+<tr>
+  <td><a href="{item_href}">{item_name}</a></td>
+  <td><a href="{item_cluster_href}">{item_cluster_name}</a></td>
+  <td>0 errors, 0 warnings</td>
+</tr>
+
+[ServerView.html]
+<div class="oblock">
+  <h1>{title}</h1>
+
+  <ul class="actions">
+    <li><a href="">Shutdown</a></li>
+  </ul>
+  {tabs}
+</div>
+
+[ServerGroupTree.css]
+ul.ServerGroupTree, ul.ServerGroupTree ul {
+  list-style: square;
+  color: #ccc;
+  padding: 0 0 0 2em;
+}
+
+ul.ServerGroupTree {
+  padding: 0 0 0 1em;
+}
+
+[ServerGroupTree.html]
+<ul class="ServerGroupTree">{root_items}</ul>
+
+[ServerGroupTree.item_html]
+<li><a href="">{item_name}</a></li>
+<ul>{child_items}</ul>

Added: mgmt/cumin/python/cumin/virtualhost.py
===================================================================
--- mgmt/cumin/python/cumin/virtualhost.py	                        (rev 0)
+++ mgmt/cumin/python/cumin/virtualhost.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,117 @@
+from wooly import *
+from wooly.widgets import *
+
+from queue import *
+from realm import *
+from exchange import *
+from widgets import *
+
+strings = StringCatalog(__file__)
+
+class VirtualHostSet(ItemSet):
+    def render_title(self, session, server):
+        return "Virtual Hosts (%i)" % len(server.virtual_host_items())
+
+    def get_items(self, session, server):
+        return sorted(server.virtual_host_items())
+
+    def render_item_href(self, session, vhost):
+        branch = session.branch()
+        self.page().show_virtual_host(branch, vhost)
+        return branch.marshal()
+
+    def render_item_name(self, session, vhost):
+        return vhost.name
+
+class VirtualHostFrame(CuminFrame):
+    def __init__(self, app, name):
+        super(VirtualHostFrame, self).__init__(app, name)
+
+        self.param = self.VirtualHostParameter(app, "id")
+        self.add_parameter(self.param)
+        self.set_object_attribute(self.param)
+
+        self.view = VirtualHostView(app, "view")
+        self.add_child(self.view)
+
+        self.queue_add = QueueAdd(app, "queue_add")
+        self.add_child(self.queue_add)
+
+        self.queue = QueueFrame(app, "queue")
+        self.add_child(self.queue)
+
+        self.exchange_add = ExchangeAdd(app, "exchange_add")
+        self.add_child(self.exchange_add)
+
+        self.exchange = ExchangeFrame(app, "exchange")
+        self.add_child(self.exchange)
+
+    class VirtualHostParameter(Parameter):
+        def do_unmarshal(self, string):
+            return self.app.model.get_virtual_host(int(string))
+
+        def do_marshal(self, vhost):
+            return str(vhost.id)
+
+    def set_virtual_host(self, session, vhost):
+        return self.param.set(session, vhost)
+
+    def show_view(self, session):
+        return self.show_mode(session, self.view)
+
+    def show_queue_add(self, session):
+        return self.show_mode(session, self.queue_add)
+
+    def show_queue(self, session, queue):
+        self.queue.set_queue(session, queue)
+
+        return self.show_mode(session, self.queue)
+
+    def show_exchange_add(self, session):
+        return self.show_mode(session, self.exchange_add)
+
+    def show_exchange(self, session, exchange):
+        self.exchange.set_exchange(session, exchange)
+
+        return self.show_mode(session, self.exchange)
+
+    def render_href(self, session, vhost):
+        branch = session.branch()
+        self.show_view(branch)
+        return branch.marshal()
+
+    def render_title(self, session, vhost):
+        return "Virtual Host '%s'" % vhost.name
+
+class VirtualHostView(Widget):
+    def __init__(self, app, name):
+        super(VirtualHostView, self).__init__(app, name)
+
+        self.tabs = TabSet(app, "tabs")
+        self.add_child(self.tabs)
+
+        self.queues = QueueSet(app, "queues")
+        self.tabs.add_child(self.queues)
+
+        self.exchanges = ExchangeSet(app, "exchanges")
+        self.tabs.add_child(self.exchanges)
+
+        self.log = self.VirtualHostLog(app, "log")
+        self.tabs.add_child(self.log)
+
+    def render_title(self, session, vhost):
+        return "Virtual Host '%s'" % vhost.name
+
+    def render_add_queue_href(self, session, vhost):
+        branch = session.branch()
+        self.page().show_virtual_host(branch, vhost).show_queue_add(branch)
+        return branch.marshal()
+
+    def render_add_exchange_href(self, session, vhost):
+        branch = session.branch()
+        self.page().show_virtual_host(branch, vhost).show_exchange_add(branch)
+        return branch.marshal()
+
+    class VirtualHostLog(Widget):
+        def render_title(self, session, vhost):
+            return "Log Messages"

Added: mgmt/cumin/python/cumin/virtualhost.strings
===================================================================
--- mgmt/cumin/python/cumin/virtualhost.strings	                        (rev 0)
+++ mgmt/cumin/python/cumin/virtualhost.strings	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,26 @@
+[VirtualHostSet.html]
+<table class="VirtualHostSet mobjects">
+  <tr>
+    <th>Name</th>
+    <th>Configuration</th>
+    <th>Status</th>
+  </tr>
+{items}
+</table>
+
+[VirtualHostSet.item_html]
+<tr>
+  <td><a href="{item_href}">{item_name}</a></td>
+  <td>10 queues, 5 exchanges</td>
+  <td>2 errors, 10 warnings</td>
+</tr>
+
+[VirtualHostView.html]
+<div class="oblock">
+  <h1>{title}</h1>
+  <ul class="actions">
+    <li><a href="{add_queue_href}">Add Queue</a></li>
+    <li><a href="{add_exchange_href}">Add Exchange</a></li>
+  </ul>
+{tabs}
+</div>

Added: mgmt/cumin/python/cumin/widgets.py
===================================================================
--- mgmt/cumin/python/cumin/widgets.py	                        (rev 0)
+++ mgmt/cumin/python/cumin/widgets.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,102 @@
+from wooly import *
+from wooly.widgets import *
+from wooly.forms import *
+
+strings = StringCatalog(__file__)
+
+class CuminFrame(Frame, ModeSet):
+    def do_process(self, session, object):
+        self.page().get_frames(session).append(self)
+
+        super(CuminFrame, self).do_process(session, object)
+
+class CuminForm(Form):
+    def __init__(self, app, name):
+        super(CuminForm, self).__init__(app, name)
+        
+        self.cancel = self.Cancel(app, "cancel", self)
+        self.cancel.set_tab_index(201)
+        self.add_child(self.cancel)
+
+        self.submit = self.Submit(app, "submit", self)
+        self.submit.set_tab_index(200)
+        self.add_child(self.submit)
+
+    def do_process(self, session, object):        
+        self.page().set_modal(session, True)
+            
+        if self.cancel.get(session):
+            self.cancel.set(session, False)
+
+            self.on_cancel(session, object)
+        elif self.submit.get(session):
+            self.submit.set(session, False)
+
+            self.on_submit(session, object)
+        else:
+            self.on_display(session, object)
+
+    def on_cancel(self, session, object):
+        pass
+
+    def on_submit(self, session, object):
+        pass
+
+    def on_display(self, session, object):
+        pass
+
+    class Cancel(FormButton):
+        def render_content(self, session, object):
+            return "Cancel"
+
+    class Submit(FormButton):
+        def render_content(self, session, object):
+            return "Submit"
+    
+class CuminConfirmForm(Form):
+    def __init__(self, app, name):
+        super(CuminConfirmForm, self).__init__(app, name)
+
+        self.confirm = self.ConfirmButton(app, "confirm", self)
+        self.confirm.set_tab_index(101)
+        self.add_child(self.confirm)
+
+        self.cancel = self.CancelButton(app, "cancel", self)
+        self.add_child(self.cancel)
+
+    def do_process(self, session, object):
+        self.page().set_modal(session, True)
+            
+        if self.confirm.get(session):
+            self.confirm.set(session, False)
+
+            self.on_confirm(session, object)
+        elif self.cancel.get(session):
+            self.cancel.set(session, False)
+
+            self.on_cancel(session, object)
+        else:
+            self.on_display(session, object)
+
+    def on_cancel(self, session, object):
+        pass
+
+    def on_confirm(self, session, object):
+        pass
+
+    def on_display(self, session, object):
+        pass
+
+    def render_confirm_content(self, session, object):
+        return "Confirm"
+
+    def render_cancel_content(self, session, object):
+        return "Cancel"
+
+    class ConfirmButton(FormButton):
+        def render_content(self, session, object):
+            return self.parent.render_confirm_content(session, object)
+
+    class CancelButton(FormButton):
+        def render_content(self, session, object):
+            return self.parent.render_cancel_content(session, object)

Added: mgmt/cumin/python/cumin/widgets.strings
===================================================================
--- mgmt/cumin/python/cumin/widgets.strings	                        (rev 0)
+++ mgmt/cumin/python/cumin/widgets.strings	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,14 @@
+[CuminConfirmForm.html]
+<form id="{id}" class="QueueForm mform" method="post" action="?">
+  <div class="head">
+    <h1>{title}</h1>
+  </div>
+  <div class="body">
+    <div>{confirm}</div>
+    <div>{cancel}</div>
+{hidden_inputs}
+  </div>
+</form>
+<script>
+wooly.doc().elem("{id}").node.elements[1].focus();
+</script>

Added: mgmt/cumin/python/wooly/__init__.py
===================================================================
--- mgmt/cumin/python/wooly/__init__.py	                        (rev 0)
+++ mgmt/cumin/python/wooly/__init__.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,639 @@
+import sys, os
+from cStringIO import StringIO
+from urllib import quote_plus, unquote_plus
+from copy import copy
+from time import time
+from datetime import datetime
+
+from resources import ResourceFinder, StringCatalog
+
+class Widget(object):
+    html = "{content}"
+    
+    def __init__(self, app, name):
+        self.app = app
+        self.name = name
+        self.parent = None
+        self.children = list()
+        self.attributes = list()
+        self.parameters = list()
+        self.template = Template(self, "html")
+
+        self.cached_path = None
+        self.cached_page = None
+        self.child_index = None
+
+        app.add_widget(self)
+
+        self.strings = None
+
+    def path(self):
+        if not self.cached_path:
+            if not self.parent:
+                self.cached_path = ""
+            elif not self.parent.parent:
+                self.cached_path = self.name;
+            else:
+                self.cached_path = self.parent.path() + "." + self.name
+
+        return self.cached_path
+
+    def page(self):
+        if not self.cached_page:
+            if not self.parent:
+                self.cached_page = self
+            else:
+                self.cached_page = self.parent.page()
+
+        return self.cached_page
+
+    def add_child(self, widget):
+        self.children.append(widget)
+        widget.parent = self
+
+    def get_child(self, name):
+        if not self.child_index:
+            self.child_index = dict()
+
+            for child in self.children:
+                self.child_index[child.name] = child
+
+        return self.child_index.get(name, None)
+
+    def add_attribute(self, attribute):
+        self.attributes.append(attribute)
+        attribute.widget = self
+
+    def add_parameter(self, parameter):
+        self.parameters.append(parameter)
+        parameter.widget = self
+
+    def get_string(self, key):
+        for cls in self.__class__.__mro__:
+            str = None
+            module = sys.modules[cls.__module__]
+
+            strs = module.__dict__.get("strings")
+
+            if strs:
+                str = strs.get(cls.__name__ + "." + key)
+
+                if str:
+                    return str
+
+            str = cls.__dict__.get(key)
+
+            if str:
+                return str
+
+    def scope(self, session, params):
+        params.update(self.parameters)
+
+        for child in self.children:
+            child.scope(session, params)
+
+    def process(self, session, object):
+        if session.debug:
+            call = WidgetCall(session.process_stack, self, session, object)
+            call.open()
+
+        self.do_process(session, object)
+
+        if session.debug:
+            call.close()
+
+    def do_process(self, session, object):
+        for child in self.children:
+            child.process(session, object)
+
+    def render(self, session, object):
+        if session.debug:
+            call = WidgetCall(session.render_stack, self, session, object)
+            call.open()
+
+        string = self.do_render(session, object)
+
+        if session.debug:
+            call.close()
+
+        return string
+
+    def do_render(self, session, object):
+        writer = Writer()
+
+        self.template.render(session, object, writer)
+
+        return writer.to_string()
+
+    def render_id(self, session, object):
+        return self.path()
+
+    def render_class(self, session, object):
+        return None
+
+    def render_href(self, session, object):
+        return None
+
+    def render_title(self, session, object):
+        return None
+
+    def render_content(self, session, object):
+        writer = Writer()
+
+        for child in self.children:
+            writer.write(child.render(session, object))
+
+        return writer.to_string()
+
+    def __str__(self):
+        return "%s '%s'" % (self.__class__.__name__, self.path())
+
+class Page(Widget):
+    xml_content_type = "text/xml"
+    html_content_type = "text/html"
+    xml_1_0_declaration = """<?xml version="1.0"?>"""
+    xhtml_1_1_doctype = """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">"""
+    xhtml_namespace = "http://www.w3.org/1999/xhtml"
+    
+    def __init__(self, app, name):
+        super(Page, self).__init__(app, name)
+
+    def get_last_modified(self, session):
+        return datetime.utcnow()
+
+    def get_content_type(self, session):
+        return "text/html"
+
+    def save_session(self, session):
+        pass
+
+class Frame(Widget):
+    def __init__(self, app, name):
+        super(Frame, self).__init__(app, name)
+
+        self.object = None
+
+    def set_object_attribute(self, attribute):
+        self.object = attribute
+
+    def get_object(self, session):
+        if self.object:
+            return self.object.get(session)
+
+    def do_process(self, session, object):
+        new_object = self.get_object(session)
+        super(Frame, self).do_process(session, new_object)
+
+    def do_render(self, session, object):
+        new_object = self.get_object(session)
+        return super(Frame, self).do_render(session, new_object)
+
+class Application(object):
+    def __init__(self):
+        self.pages = dict()
+        self.default_page = None
+
+        # Registration lists XXX get rid of?
+        self.widgets = list()
+        self.parameters = list()
+
+        self.finder = ResourceFinder()
+        self.cached_css = None
+
+        self.sessions = list()
+
+    def add_page(self, page):
+        if page.parent:
+            raise Exception("Page '%s' is not a root widget" % page.name)
+
+        self.pages[page.name] = page
+
+    def get_page(self, name):
+        return self.pages.get(name, self.default_page)
+
+    def set_default_page(self, page):
+        self.default_page = page
+
+    def add_widget(self, widget):
+        self.widgets.append(widget)
+
+    def get_widget(self, key):
+        # XXX not fast
+        for widget in self.widgets:
+            if widget.path() == key:
+                return widget
+
+    def add_parameter(self, param):
+        self.parameters.append(param)
+
+    def get_parameter(self, key):
+        # XXX not fast
+        for param in self.parameters:
+            # XXX I'm not sure the param.widget test is what I want
+            if param.widget and param.path() == key:
+                return param
+
+    def add_resource_dir(self, dir):
+        self.finder.add_dir(dir)
+
+    def get_resource(self, name):
+        return self.finder.find(name)
+
+    def get_css(self):
+        if not self.cached_css:
+            writer = Writer()
+
+            for widget in self.widgets:
+                css = widget.get_string("css")
+
+                if css:
+                    writer.write(css)
+                    writer.write("\r\n") # HTTP newline
+
+            self.cached_css = writer.to_string()
+
+        return self.cached_css
+
+    def clear_caches(self):
+        self.cached_css = None
+
+class Attribute(object):
+    def __init__(self, app, name):
+        self.app = app
+        self.name = name
+        self.widget = None
+        self.default = None
+        self.is_required = True
+
+    def path(self):
+        if not self.widget:
+            raise Exception("Parameter has no widget")
+
+        if not self.widget.parent:
+            return self.name
+        else:
+            path = self.widget.path() + "." + self.name
+
+        return path
+
+    def set_required(self, is_required):
+        self.is_required = is_required
+
+    def validate(self, session):
+        value = self.get(session)
+        
+        if value == None and self.is_required:
+            raise Exception("%s not set" % str(self))
+
+    def get(self, session):
+        value = session.get(self.path())
+
+        # Use strict test because empty collections are False
+        if value == None:
+            default = self.get_default(session)
+
+            if default != None:
+                value = self.set(session, default)
+
+        return value
+
+    def add(self, session, value):
+        self.set(session, value)
+
+    def set(self, session, value):
+        return session.set(self.path(), value)
+
+    def get_default(self, session):
+        return self.default
+
+    def set_default(self, default):
+        self.default = default
+
+    def __str__(self):
+        return "%s '%s'" % (self.__class__.__name__, self.path())
+
+class Parameter(Attribute):
+    def __init__(self, app, name):
+        super(Parameter, self).__init__(app, name)
+
+        app.add_parameter(self)
+
+    def marshal(self, object):
+        if object == None:
+            string = ""
+        else:
+            string = self.do_marshal(object)
+
+        return string
+
+    def do_marshal(self, object):
+        return str(object)
+
+    def unmarshal(self, string):
+        return self.do_unmarshal(string)
+
+    def do_unmarshal(self, string):
+        return string
+
+class Session(object):
+    def __init__(self, app, trunk=None):
+        self.app = app
+        self.trunk = trunk
+        self.page = None
+        self.values = dict()
+        self.errors = dict() # widget => list of str
+        self.redirect = None
+
+        self.debug = True
+
+        self.process_stack = list()
+        self.render_stack = list()
+
+    def print_process_calls(self, out=sys.stdout):
+        if self.process_stack:
+            self.process_stack[0].write(out)
+
+    def print_render_calls(self, out=sys.stdout):
+        if self.render_stack:
+            self.render_stack[0].write(out)
+
+    def branch(self):
+        return Session(self.app, self)
+
+    def get_page(self):
+        if self.page:
+            page = self.page
+        elif self.trunk:
+            page = self.trunk.get_page()
+        else:
+            page = None
+
+        return page
+
+    def set_page(self, page):
+        self.page = page
+
+    def get(self, key):
+        if key in self.values:
+            value = self.values[key]
+        elif self.trunk:
+            value = self.trunk.get(key)
+        else:
+            value = None
+
+        return value
+
+    def set(self, key, value):
+        # XXX interesting idea for debugging: catch where things get set
+        #if key.startswith("server.vhost.queue.edit.durable"):
+        #    raise Exception()
+
+        self.values[key] = value
+
+        return value
+
+    def unset(self, key):
+        if key in self.values:
+            del self.values[key]
+
+    # XXX this is a little out of line with other session methods in
+    # that it uses widget as a key rather than having a method through
+    # widget to do that
+    def get_errors(self, widget):
+        return self.errors.get(widget)
+
+    def add_error(self, widget, error):
+        errors = self.errors.setdefault(widget, list())
+
+        errors.append(error)
+
+    def set_redirect(self, redirect):
+        self.redirect = redirect
+
+    def marshal(self):
+        return self.marshal_page() + "?" + self.marshal_url_vars()
+
+    def marshal_page(self):
+        return self.get_page().name
+
+    def marshal_url_vars(self, separator=";"):
+        params = set()
+        self.get_page().scope(self, params)
+        vars = list()
+
+        for param in params:
+            key = param.path()
+            value = self.get(key)
+            default = param.get_default(self)
+
+            if value not in (default, None):
+                skey = quote_plus(key)
+                svalue = quote_plus(param.marshal(value))
+
+                vars.append("%s=%s" % (skey, svalue))
+
+        return separator.join(vars)
+
+    def unmarshal(self, string):
+        if string.startswith("/"):
+            raise Exception("Illegal session string '" + string + "'")
+
+        elems = string.split("?")
+
+        self.unmarshal_page(elems[0])
+
+        try:
+            self.unmarshal_url_vars(elems[1])
+        except IndexError:
+            pass
+
+    def unmarshal_page(self, string):
+        self.set_page(self.app.get_page(string))
+
+    def unmarshal_url_vars(self, string, separator=";"):
+        vars = string.split(separator)
+
+        for var in vars:
+            try:
+                skey, svalue = var.split("=")
+
+                key = unquote_plus(skey)
+                value = unquote_plus(svalue)
+
+                param = self.app.get_parameter(key)
+
+                if param:
+                    param.add(self, param.unmarshal(value))
+            except ValueError:
+                pass
+
+class StringIOWriter(object):
+    def __init__(self):
+        self.writer = StringIO()
+
+    def write(self, string):
+        self.writer.write(string)
+
+    def to_string(self):
+        string = self.writer.getvalue()
+
+        self.writer.close()
+
+        return string
+
+class ListWriter(object):
+    def __init__(self):
+        self.strings = list()
+
+    def write(self, string):
+        self.strings.append(string)
+
+    def to_string(self):
+        return "".join(self.strings)
+
+class Writer(StringIOWriter):
+    pass
+
+class Template(object):
+    def __init__(self, widget, key):
+        self.widget = widget
+        self.key = key
+        self.fragments = None
+
+    def compile(self):
+        text = self.widget.get_string(self.key)
+
+        if not text:
+            raise Exception("Template '%s.%s' not found" \
+                            % (self.widget.__class__.__name__, self.key))
+
+        return self.resolve(self.parse(text))
+
+    def parse(self, text):
+        strings = list()
+
+        start = 0
+        end = text.find("{")
+
+        while True:
+            if (end == -1):
+                strings.append(text[start:])
+                break
+
+            strings.append(text[start:end])
+
+            ccurly = text.find("}", end + 1)
+
+            if ccurly == -1:
+                start = end
+                end = -1
+            else:
+                ocurly = text.find("{", end + 1)
+
+                if ocurly == -1:
+                    start = end
+                    end = ccurly + 1
+                elif ocurly < ccurly:
+                    start = end
+                    end = ocurly
+                else:
+                    strings.append("{" + text[end + 1:ccurly] + "}")
+                        
+                    start = ccurly + 1
+                    end = ocurly
+
+        return strings
+
+    def resolve(self, strings):
+        fragments = list()
+        
+        for string in strings:
+            if string.startswith("{") and string.endswith("}"):
+                # Might be a placeholder; look for a matching render
+                # method
+                key = string[1:-1]
+                method = self.find_method("render_" + key)
+
+                if method:
+                    fragments.append(method)
+                else:
+                    child = self.widget.get_child(key)
+
+                    if child:
+                        fragments.append(child)
+                    else:
+                        fragments.append(string)
+            else:
+                fragments.append(string)
+
+        return fragments
+
+    # XXX Ought to make sure what we're returning is a method.
+    def find_method(self, name):
+        for cls in self.widget.__class__.__mro__:
+            method = getattr(cls, name, None)
+
+            if method:
+                return method
+
+    def render(self, session, object, writer):
+        if not self.fragments:
+            self.fragments = self.compile()
+
+        for elem in self.fragments:
+            if type(elem) is str:
+                writer.write(elem)
+            elif callable(elem):
+                writer.write(str(elem(self.widget, session, object) or ""))
+            else:
+                writer.write(str(elem.render(session, object) or ""))
+
+class WidgetCall(object):
+    def __init__(self, stack, widget, session, object):
+        self.stack = stack
+        self.widget = widget
+        self.session = session
+        self.session_values = copy(session.values)
+        self.object = object
+
+        self.caller = None
+        self.callees = list()
+
+        self.start = None
+        self.end = None
+
+    def open(self):
+        if self.stack:
+            self.caller = self.stack[-1]
+            self.caller.callees.append(self)
+
+        self.stack.append(self)
+
+        self.start = time()
+
+    def close(self):
+        self.end = time()
+
+        if len(self.stack) > 1:
+            self.stack.pop()
+
+    def write(self, writer):
+        writer.write(str(self.widget))
+        writer.write(" (call %i, caller %i)" % (id(self), id(self.caller)))
+        writer.write(os.linesep)
+
+        writer.write("  session: " + str(self.session))
+        writer.write(os.linesep)
+
+        for item in self.session_values.iteritems():
+            writer.write("    value: %s = %s" % item)
+            writer.write(os.linesep)
+
+        writer.write("   object: " + str(self.object))
+        writer.write(os.linesep)
+        
+        writer.write("    times: %f, %f" % (self.start, self.end or -1))
+        writer.write(os.linesep)
+
+        for call in self.callees:
+            call.write(writer)

Added: mgmt/cumin/python/wooly/debug.py
===================================================================
--- mgmt/cumin/python/wooly/debug.py	                        (rev 0)
+++ mgmt/cumin/python/wooly/debug.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,158 @@
+from wooly import *
+from wooly.widgets import *
+
+class WidgetInspector(Widget):
+    css = """
+    .WidgetInspector {
+      background-color: white;
+      padding: 1em;
+      border: 1px solid #ccc;
+      min-height: 60em;
+    }
+
+    .WidgetInspector .list {
+      float: left;
+      width: 20em;
+      background-color: #f7f7f7;
+      padding: 0.75em 1em;
+      border: 1px dashed #ccc;
+      margin: 0 1em 0 0;
+    }
+
+    """
+
+    html = """
+    <div class="WidgetInspector">
+      <div class="list">{widgets}</div>
+      {view}
+    </div>
+    """
+    
+    def __init__(self, app, name):
+        super(WidgetInspector, self).__init__(app, name)
+
+        self.widgets = WidgetList(app, "widgets")
+        self.add_child(self.widgets)
+
+        self.view = WidgetView(app, "view")
+        self.add_child(self.view)
+
+class WidgetView(Widget):
+    css = """
+    """
+
+    html = """
+    <h2>{title}</h2>
+    """
+
+    def render_title(self, session, object):
+        return self.parent.widgets.get_selected_widget(session).name
+
+class WidgetList(Widget):
+    css = """
+    .WidgetList ul {
+      list-style: none;
+      margin: 0;
+      padding: 0 0 0 1.5em;
+    }
+
+    .WidgetList .selected {
+      font-weight: bold;
+    }
+    """
+
+    html = """
+    <div class="WidgetList"><ul>{widgets}</ul></div>
+    """
+
+    def __init__(self, app, name):
+        super(WidgetList, self).__init__(app, name)
+
+        self.widget = Parameter(app, "widget")
+        self.add_parameter(self.widget)
+
+        self.item = self.WidgetItem(app, "item")
+        self.add_child(self.item)
+
+    def do_process(self, session, object):
+        print "do_process"
+        
+        if not self.get_selected_widget(session):
+            self.set_selected_widget(session, self.page())
+
+    def get_selected_widget(self, session):
+        return self.app.get_widget(self.widget.get(session))
+
+    def set_selected_widget(self, session, widget):
+        self.widget.set(session, widget.path())
+    
+    def render_title(self, session, object):
+        return "Widgets"
+
+    def render_widgets(self, session, object):
+        return self.item.render(session, self.page())
+
+    class WidgetItem(Widget):
+        html = """
+        <li class="{selected}">{class} <a href="{href}">{name}</a></li>
+        {children}
+        """
+
+        def render_selected(self, session, widget):
+            if widget == self.parent.get_selected_widget(session):
+                return "selected"
+
+        def render_href(self, session, widget):
+            branch = session.branch()
+            self.parent.set_selected_widget(branch, widget)
+            return branch.marshal()
+
+        def render_class(self, session, widget):
+            return widget.__class__.__name__
+
+        def render_name(self, session, widget):
+            return widget.name
+
+        def render_children(self, session, widget):
+            if len(widget.children):
+                writer = Writer()
+                writer.write("<ul>")
+
+                for child in widget.children:
+                    writer.write(self.parent.item.render(session, child))
+
+                writer.write("</ul>")
+
+                return writer.to_string()
+
+    def xxx_render_widget(self, session, widget, depth, writer):
+        spacer = ".   "
+        indent = spacer * depth
+
+        try:
+            cum_time = session.render_times[widget] * 1000
+            self_time = cum_time
+            for child in widget.children:
+                self_time -= session.render_times[child] * 1000
+
+            if widget.parent and cum_time > 0.2 * session.render_times[widget.parent] * 1000:
+                hot = "hot"
+            else:
+                hot = ""
+
+            stime = " (<span class='wooly debug timing %s'>%f, %f</span>)" % \
+                    (hot, cum_time, self_time)
+        except KeyError:
+            stime = ""
+
+        writer.write("%s%s <b>%s</b>%s\n" % \
+                     (indent, widget.__class__.__name__, widget.name, \
+                      stime))
+
+        for param in widget.parameters:
+            writer.write("%s<span class='wooly debug param'>param %s <strong>%s</strong> = %s</span>\n" % \
+                         (indent + spacer, param.__class__.__name__, \
+                          param.name, param.get(session)))
+
+        for child in widget.children:
+            self.render_widget(session, child, depth + 1, writer)

Added: mgmt/cumin/python/wooly/devel.py
===================================================================
--- mgmt/cumin/python/wooly/devel.py	                        (rev 0)
+++ mgmt/cumin/python/wooly/devel.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,42 @@
+import sys, os
+
+from wooly import *
+
+class DevelPage(Page):
+    html = """
+    <html>
+      <head>
+        <title>{title}</title>
+      </head>
+      <body>{rtrace}</body>
+    </html>
+    """
+
+    def __init__(self, app, name):
+        super(DevelPage, self).__init__(app, name)
+
+        self.render_trace = self.RenderTrace(app, "rtrace")
+        self.add_child(self.render_trace)
+
+    class RenderTrace(Widget):
+        def do_render(self, session, object):
+            writer = Writer()
+
+            writer.write("<ul>")
+
+            if self.app.sessions:
+                call = self.app.sessions[-1].render_stack[0]
+                self.render_call(session, call, writer)
+
+            writer.write("</ul>")
+
+            return writer.to_string()
+
+        def render_call(self, session, call, writer):
+            writer.write("<li>%s</li>" % str(call.widget))
+            writer.write("<ul>")
+
+            for c in call.callees:
+                self.render_call(session, c, writer)
+
+            writer.write("</ul>")

Added: mgmt/cumin/python/wooly/forms.py
===================================================================
--- mgmt/cumin/python/wooly/forms.py	                        (rev 0)
+++ mgmt/cumin/python/wooly/forms.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,177 @@
+from wooly import *
+from parameters import *
+from resources import *
+from widgets import ItemSet
+
+strings = StringCatalog(__file__)
+
+class Form(Widget):
+    def __init__(self, app, name):
+        super(Form, self).__init__(app, name)
+
+        self.form_params = set()
+
+    def add_form_parameter(self, param):
+        self.form_params.add(param)
+
+    def render_hidden_inputs(self, session, object):
+        writer = Writer()
+
+        params = set()
+        session.get_page().scope(session, params)
+        params.difference_update(self.form_params)
+
+        for param in params:
+            key = param.path()
+            value = session.get(key)
+            default = param.get_default(session)
+
+            if value not in (default, None):
+                writer.write("<input type='hidden' name='%s' value='%s'/>" \
+                             % (key, param.marshal(value)))
+
+        return writer.to_string()
+
+# XXX implement me
+#class FormInputItemSet(FormInput, ItemSet):
+#    pass
+
+class FormInput(Widget):
+    def __init__(self, app, name, form):
+        super(FormInput, self).__init__(app, name)
+
+        self.form = form
+        self.param = None
+
+        self.tab_index = 100
+
+    def set_parameter(self, param):
+        if self.param:
+            raise Exception("Parameter already set")
+
+        self.param = param;
+        self.form.add_form_parameter(self.param)
+
+    def get_parameter(self):
+        return self.param
+
+    def get(self, session):
+        return self.param.get(session)
+
+    def set(self, session, value):
+        return self.param.set(session, value)
+
+    def get_default(self, session):
+        return self.param.get_default(session)
+
+    def set_default(self, default):
+        self.param.set_default(default)
+
+    def add_error(self, session, error):
+        session.add_error(self, error)
+
+    def get_errors(self, session):
+        return session.get_errors(self)
+
+    def set_tab_index(self, tab_index):
+        self.tab_index = tab_index
+
+    def render_name(self, session, object):
+        return self.param.path()
+
+    def render_value(self, session, object):
+        return self.param.marshal(self.param.get(session))
+
+    # XXX do this proper
+    def render_errors(self, session, object):
+        errors = self.get_errors(session)
+
+        if errors:
+            return "<ul class=\"errors\"><li>" + \
+                   "</li><li>".join(errors) + \
+                   "</li></ul>"
+
+    def render_tabindex(self, session, object):
+        return self.tab_index
+
+class TextInput(FormInput):
+    def __init__(self, app, name, form):
+        super(TextInput, self).__init__(app, name, form)
+
+        self.set_parameter(Parameter(app, "param"))
+        self.add_parameter(self.get_parameter())
+
+        self.size = 32
+
+    def set_size(self, size):
+        self.size = size
+
+    def render_size(self, session, object):
+        return self.size
+
+class CheckboxInput(FormInput):
+    def __init__(self, app, name, form):
+        super(CheckboxInput, self).__init__(app, name, form)
+
+        self.set_parameter(VoidBooleanParameter(app, "param"))
+        self.add_parameter(self.get_parameter())
+
+    def render_checked_attr(self, session, object):
+        return self.get(session) and "checked=\"checked\""
+
+class RadioInput(FormInput):
+    def __init__(self, app, name, form):
+        super(RadioInput, self).__init__(app, name, form)
+
+        self.value = None
+
+    def set_value(self, value):
+        self.value = value
+
+    def render_value(self, session, object):
+        return self.value
+
+    def render_checked_attr(self, session, object):
+        value = self.get(session)
+
+        return value and value == self.value and "checked=\"checked\""
+
+class FormButton(FormInput):
+    def __init__(self, app, name, form):
+        FormInput.__init__(self, app, name, form)
+
+        self.set_parameter(BooleanParameter(app, "param"))
+        self.add_parameter(self.get_parameter())
+
+    def do_process(self, session, object):
+        if self.get(session):
+            self.set(session, False)
+
+            self.on_submit(session, object)
+
+    def on_submit(self, session, object):
+        pass
+
+    def render_value(self, session, object):
+        branch = session.branch()
+
+        self.set(branch, True)
+
+        return super(FormButton, self).render_value(branch, object)
+
+class CheckboxInputSet(FormInput, ItemSet):
+    def __init__(self, app, name, form):
+        super(CheckboxInputSet, self).__init__(app, name, form)
+
+    def render_item_value(self, session, object):
+        return None
+
+    def render_item_checked_attr(self, session, object):
+        return None
+
+class RadioInputSet(FormInput, ItemSet):
+    def render_item_value(self, session, object):
+        return None
+
+    def render_item_checked_attr(self, session, object):
+        return None

Added: mgmt/cumin/python/wooly/forms.strings
===================================================================
--- mgmt/cumin/python/wooly/forms.strings	                        (rev 0)
+++ mgmt/cumin/python/wooly/forms.strings	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,26 @@
+[FormButton.html]
+<button type="submit" name="{name}" value="{value}" tabindex="{tabindex}">{content}</button>
+
+[TextInput.html]
+{errors}
+<input type="text" name="{name}" value="{value}" tabindex="{tabindex}" size="{size}"/>
+
+[CheckboxInput.html]
+<input type="checkbox" name="{name}" value="{value}" tabindex="{tabindex}" {checked_attr}/>
+
+[RadioInput.html]
+<input type="radio" name="{name}" value="{value}" tabindex="{tabindex}" {checked_attr}/>
+
+[CheckboxInputSet.html]
+{errors}{items}
+
+[CheckboxInputSet.item_html]
+<input type="checkbox" name="{name}" value="{item_value}" tabindex="{tabindex}" {item_checked_attr}/>
+{item_content}
+
+[RadioInputSet.html]
+{errors}{items}
+
+[RadioInputSet.item_html]
+<input type="radio" name="{name}" value="{item_value}" tabindex="{tabindex}" {item_checked_attr}/>
+{item_content}

Added: mgmt/cumin/python/wooly/model.py
===================================================================
--- mgmt/cumin/python/wooly/model.py	                        (rev 0)
+++ mgmt/cumin/python/wooly/model.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,301 @@
+from new import instancemethod
+from threading import RLock
+
+class Model(object):
+    def __init__(self):
+        self.indexes = dict()
+        self.mclasses = dict()
+        self.__lock = RLock();
+
+    def get_index(self, cls):
+        return self.indexes.setdefault(cls, dict())
+
+    def lock(self):
+        self.__lock.acquire()
+
+    def unlock(self):
+        self.__lock.release()
+
+class ModelClass(object):
+    def __init__(self, model, name):
+        self.model = model
+        self.name = name
+        self.endpoints = set()
+
+class ModelAssociation(object):
+    def __init__(self, model, name):
+        self.model = model
+        self.name = name
+        self.endpoints = set()
+
+    def add_endpoint(self, mclass, name, multiplicity):
+        if not isinstance(mclass, ModelClass):
+            raise TypeError()
+
+        if multiplicity not in ("0..1", "0..n"):
+            raise Exception("Multiplicity not recognized")
+
+        if len(self.endpoints) > 1:
+            raise Exception("Too many endpoints")
+        
+        endpoint = self.Endpoint()
+        endpoint.name = name
+        endpoint.multiplicity = multiplicity
+
+        self.endpoints.add(endpoint)
+        endpoint.association = self
+
+        mclass.endpoints.add(endpoint)
+        endpoint.mclass = mclass
+
+        return endpoint
+
+    class Endpoint(object):
+        def __init__(self):
+            self.association = None
+            self.mclass = None
+
+            self.name = None
+            self.multiplicity = None
+
+        def other(self):
+            for end in self.association.endpoints:
+                if end is not self:
+                    return end
+
+        def is_scalar(self):
+            return self.multiplicity == "0..1"
+
+        def set_scalar(self, this, that):
+            this.__dict__[self.name] = that
+
+        def get_scalar(self, this):
+            return this.__dict__[self.name]
+
+        def items(self, this):
+            return this.__dict__[self.name + "_set"]
+
+        def add(self, this, that):
+            if self.is_scalar():
+                self.set_scalar(this, that)
+            else:
+                self.items(this).add(that)
+
+        def remove(self, this, that):
+            if self.is_scalar():
+                print "is_scalar"
+                self.set_scalar(this, None)
+            else:
+                print "items(%s).remove(%s)" % (this.id, that.id)
+                self.items(this).remove(that)
+
+        def object_items(self, this):
+            return self.items(this)
+
+        def add_object(self, this, that):
+            this.lock()
+            try:
+                self.add(this, that)
+
+                that.lock()
+                try:
+                    self.other().add(that, this)
+                finally:
+                    that.unlock()
+            finally:
+                this.unlock()
+
+        def remove_object(self, this, that):
+            print "remove_object"
+            
+            this.lock()
+            try:
+                self.remove(this, that)
+
+                that.lock()
+                try:
+                    self.other().remove(that, this)
+                finally:
+                    that.unlock()
+            finally:
+                this.unlock()
+
+        def get_object(self, this):
+            return self.get_scalar(this)
+
+        def set_object(self, this, new_that):
+            this.lock()
+            try:
+                old_that = self.get_scalar(this)
+
+                if old_that != None:
+                    old_that.lock()
+                    try:
+                        self.other().remove(old_that, this)
+                    finally:
+                        old_that.unlock()
+
+                self.set_scalar(this, new_that)
+
+                if new_that != None:
+                    new_that.lock()
+                    try:
+                        self.other().add(new_that, this)
+                    finally:
+                        new_that.unlock()
+            finally:
+                this.unlock()
+            
+class ModelObject(object):
+    sequence = 100
+
+    def __init__(self, model, mclass):
+        self.model = model
+        self.mclass = mclass
+        self.__lock = RLock()
+
+        for end in self.mclass.endpoints:
+            if end.is_scalar():
+                self.add_scalar_attributes(end)
+            else:
+                self.add_set_attributes(end)
+
+        self.lock()
+        try:
+            self.__class__.sequence += 1
+            self.id = self.__class__.sequence
+            model.get_index(self.mclass)[self.id] = self
+        finally:
+            self.unlock()
+
+    def lock(self):
+        self.__lock.acquire()
+
+    def unlock(self):
+        self.__lock.release()
+
+    def setmethod(self, name, func):
+        if not hasattr(self, name):
+            method = instancemethod(func, self, self.__class__)
+            setattr(self, name, method)
+
+    def add_set_attributes(this, end):
+        setattr(this, end.name + "_set", set())
+
+        def items_method(self): return end.object_items(this)
+        this.setmethod(end.name + "_items", items_method)
+
+        def do_add_method(self, that):
+            if that == None:
+                raise Exception()
+            
+            end.add_object(this, that)
+
+        this.setmethod("do_add_" + end.name, do_add_method)
+
+        def add_method(self, that): do_add_method(self, that)
+        this.setmethod("add_" + end.name, add_method)
+
+        def do_remove_method(self, that):
+            if that == None:
+                raise Exception()
+            
+            end.remove_object(this, that)
+
+        this.setmethod("do_remove_" + end.name, do_remove_method)
+
+        def remove_method(self, that): do_remove_method(self, that)
+        this.setmethod("remove_" + end.name, remove_method)
+
+    def add_scalar_attributes(this, end):
+        end.set_scalar(this, None)
+
+        def get_method(self): return end.get_object(this)
+        this.setmethod("get_" + end.name, get_method)
+
+        def set_method(self, that): end.set_object(this, that)
+        this.setmethod("set_" + end.name, set_method)
+
+    # Note that this doesn't go through the add_someobject methods
+    def remove(self):
+        self.lock()
+        try:
+            for end in self.mclass.endpoints:
+                this = self
+                
+                if end.is_scalar():
+                    end.set_object(this, None)
+                else:
+                    for that in end.items(this).copy():
+                        end.remove_object(this, that)
+
+            del self.model.get_index(self.mclass)[self.id]
+        finally:
+            self.unlock()
+
+    def __str__(self):
+        return self.__class__.__name__ + "(" + str(self.id) + ")"
+
+class TestModel(Model):
+    def __init__(self):
+        super(TestModel, self).__init__()
+
+        self.order = ModelClass(self, self.Order)
+        self.item = ModelClass(self, self.Item)
+
+        self.assoc = ModelAssociation(self, "order_item")
+        self.assoc.add_endpoint(self.order, "item", "0..n")
+        self.assoc.add_endpoint(self.item, "order", "0..1")
+
+    class Order(ModelObject):
+        def __init__(self, model):
+            super(TestModel.Order, self).__init__(model, model.order)
+
+    class Item(ModelObject):
+        def __init__(self, model):
+            super(TestModel.Item, self).__init__(model, model.item)
+
+    def test(self):
+        def results(heading):
+            print heading
+            for item in order.item_set:
+                print "  item", item, "item.order", item.order
+
+        item0 = self.Item(self)
+        item1 = self.Item(self)
+        item2 = self.Item(self)
+
+        order = self.Order(self)
+
+        order.add_item(item0)
+        order.add_item(item1)
+        order.add_item(item2)
+
+        results("beginning state")
+
+        order.remove_item(item0)
+
+        results("remove item0 from order.items")
+
+        item1.set_order(order)
+
+        results("remove order from item1.order")
+
+        order.add_item(item1)
+
+        results("a")
+
+        item1.set_order(None)
+
+        results("b")
+
+        item1.set_order(order)
+
+        results("c")
+
+        item1.remove()
+
+        results("d")
+
+if __name__ == "__main__":
+    TestModel().test()

Added: mgmt/cumin/python/wooly/pages.py
===================================================================
--- mgmt/cumin/python/wooly/pages.py	                        (rev 0)
+++ mgmt/cumin/python/wooly/pages.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,54 @@
+from datetime import datetime
+
+from wooly import Page, Parameter
+
+class CssPage(Page):
+    def __init__(self, app, name):
+        super(CssPage, self).__init__(app, name)
+
+        self.then = datetime.utcnow()
+
+    def get_last_modified(self, session):
+        return self.then
+
+    def get_content_type(self, session):
+        return "text/css"
+
+    def do_render(self, session, object):
+        return self.app.get_css()
+
+class ResourcePage(Page):
+    def __init__(self, app, name):
+        super(ResourcePage, self).__init__(app, name)
+
+        self.rname = Parameter(app, "name")
+        self.add_parameter(self.rname)
+
+        self.then = datetime.utcnow()
+
+    def get_last_modified(self, session):
+        return self.then
+
+    def get_content_type(self, session):
+        name = self.rname.get(session)
+
+        if name:
+            if name.endswith(".png"):
+                type = "image/png"
+            elif name.endswith(".jpeg"):
+                type = "image/jpeg"
+            elif name.endswith(".html"):
+                type = "text/html"
+            else:
+                type = "text/plain"
+
+            return type
+
+    def do_render(self, session, object):
+        name = self.rname.get(session)
+
+        if name:
+            resource = self.app.get_resource(name)
+
+            if resource:
+                return resource.read()

Added: mgmt/cumin/python/wooly/parameters.py
===================================================================
--- mgmt/cumin/python/wooly/parameters.py	                        (rev 0)
+++ mgmt/cumin/python/wooly/parameters.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,53 @@
+from copy import copy
+
+from wooly import *
+
+# XXX the marshal side of this is not implemented
+class ListParameter(Parameter):
+    def __init__(self, app, name, param):
+        super(ListParameter, self).__init__(app, name)
+
+        self.param = param
+        self.default = list()
+
+    def get_default(self, session):
+        return copy(self.default)
+
+    def add(self, session, value):
+        lst = self.get(session)
+        lst.append(value)
+        return lst
+
+    def do_unmarshal(self, string):
+        return self.param.do_unmarshal(string)
+
+class IntegerParameter(Parameter):
+    def do_unmarshal(self, string):
+        return int(string)
+
+class BooleanParameter(Parameter):
+    def __init__(self, model, name):
+        Parameter.__init__(self, model, name)
+
+        self.set_default(False)
+
+    def do_unmarshal(self, string):
+        return string == "t"
+
+    def do_marshal(self, object):
+        return object and "t" or "f"
+
+class VoidBooleanParameter(Parameter):
+    def set_default(self, default):
+        raise Exception("Unsupported operation")
+
+    def get(self, session):
+        return self.path() in session.values
+
+    def set(self, session, value):
+        key = self.path()
+
+        if value:
+            session.set(key, None)
+        else:
+            session.unset(key)

Added: mgmt/cumin/python/wooly/resources.py
===================================================================
--- mgmt/cumin/python/wooly/resources.py	                        (rev 0)
+++ mgmt/cumin/python/wooly/resources.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,71 @@
+import os
+import wooly
+
+class StringCatalog(object):
+    def __init__(self, file):
+        self.strings = None
+        self.path = os.path.splitext(file)[0] + ".strings"
+
+    def clear(self):
+        self.strings = None
+
+    def load(self):
+        try:
+            file = open(self.path)
+            self.strings = parse_catalog_file(file)
+        finally:
+            file.close()
+
+    def get(self, key):
+        if not self.strings:
+            self.load()
+        
+        return self.strings.get(key)
+
+def parse_catalog_file(file):
+    strings = dict()
+    key = None
+    writer = wooly.Writer()
+
+    for line in file:
+        line = line.rstrip()
+
+        if line.startswith("[") and line.endswith("]"):
+            if key:
+                strings[key] = writer.to_string().strip()
+
+            writer = wooly.Writer()
+
+            key = line[1:-1]
+
+            continue
+
+        writer.write(line)
+        writer.write("\r\n")
+
+    strings[key] = writer.to_string().strip()
+
+    return strings
+
+class ResourceFinder(object):
+    def __init__(self):
+        self.dirs = list()
+
+    def add_dir(self, dir):
+        self.dirs.append(dir)
+
+    def find(self, name):
+        file = None
+
+        for dir in self.dirs:
+            try:
+                path = os.path.join(dir, name)
+
+                # XXX +1 exposing template resolution is useful
+                #print "Looking for resource '%s'" % path
+
+                file = open(path, "r")
+            except IOError:
+                pass
+
+        return file

Added: mgmt/cumin/python/wooly/server.py
===================================================================
--- mgmt/cumin/python/wooly/server.py	                        (rev 0)
+++ mgmt/cumin/python/wooly/server.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,112 @@
+from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+from traceback import print_exc
+from datetime import datetime
+from time import strptime
+
+from wooly import *
+from devel import DevelPage
+
+class WebServer(object):
+    def __init__(self, app, port=8080):
+        self.app = app
+        self.port = port
+
+    def run(self):
+        server = HTTPServer(("", self.port), self.RequestHandler)
+
+        # XXX hack, because HTTPServer and python conspire to make
+        # this hard
+        server.app = self.app
+
+        print "Cumin server started on port %s" % (self.port)
+        server.serve_forever()
+
+    class RequestHandler(BaseHTTPRequestHandler):
+        http_date = "%a, %d %b %Y %H:%M:%S %Z"
+
+        def do_GET(self):
+            session = Session(self.server.app)
+            session.unmarshal(self.path[1:])
+
+            self.service(session)
+
+        def do_POST(self):
+            session = Session(self.server.app)
+            session.unmarshal_page(self.path.split("?")[0])
+
+            content_type = str(self.headers.getheader("content-type"))
+
+            if content_type == "application/x-www-form-urlencoded":
+                length = int(self.headers.getheader("content-length"))
+                vars = self.rfile.read(length)
+            else:
+                raise Exception("Content type '%s' is not supported" \
+                                % content_type)
+
+            if vars:
+                session.unmarshal_url_vars(vars, "&")
+
+            self.service(session)
+
+        def service(self, session):
+            page = session.get_page()
+
+            try:
+                page.process(session, None)
+            except:
+                self.error(session)
+                return
+
+            if session.redirect:
+                self.send_response(303)
+                self.send_header("Location", session.redirect)
+                self.end_headers()
+                return 
+
+            ims = self.headers.getheader("if-modified-since")
+            modified = page.get_last_modified(session).replace \
+                       (microsecond=0)
+
+            if ims:
+                since = datetime(*strptime(str(ims), self.http_date)[0:6])
+
+                if modified <= since:
+                    self.send_response(304)
+                    self.end_headers()
+                    return
+
+            try:
+                response = page.render(session, None)
+            except:
+                self.error(session)
+                return
+
+            self.send_response(200)
+
+            type = page.get_content_type(session)
+
+            if type:
+                self.send_header("Content-Type", type)
+
+            if modified:
+                ts = modified.strftime("%a, %d %b %Y %H:%M:%S GMT")
+                self.send_header("Last-Modified", ts)
+
+            self.end_headers()
+
+            self.wfile.write(response)
+
+            page.save_session(session)
+
+        def error(self, session):
+            self.send_response(500)
+            self.send_header("Content-Type", "text/plain")
+            self.end_headers()
+
+            self.wfile.write("APPLICATION ERROR\n")
+            self.wfile.write("\n----- python trace -----\n")
+            print_exc(None, self.wfile)
+            self.wfile.write("\n----- process trace -----\n")
+            session.print_process_calls(self.wfile)
+            self.wfile.write("\n----- render trace -----\n")
+            session.print_render_calls(self.wfile)

Added: mgmt/cumin/python/wooly/widgets.py
===================================================================
--- mgmt/cumin/python/wooly/widgets.py	                        (rev 0)
+++ mgmt/cumin/python/wooly/widgets.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,147 @@
+from wooly import *
+from parameters import *
+from resources import *
+
+strings = StringCatalog(__file__)
+
+class ModeSet(Widget):
+    def __init__(self, app, name):
+        super(ModeSet, self).__init__(app, name)
+
+        self.mode = Parameter(app, "mode")
+        self.add_parameter(self.mode)
+
+    def add_child(self, mode):
+        super(ModeSet, self).add_child(mode)
+
+        if not self.mode.default:
+            self.mode.set_default(mode.name)
+
+    def get_selected_mode(self, session):
+        return self.get_child(self.mode.get(session))
+
+    def set_selected_mode(self, session, mode):
+        self.mode.set(session, mode.name)
+
+    def show_mode(self, session, mode):
+        self.set_selected_mode(session, mode)
+
+        return mode
+
+    def scope(self, session, params):
+        params.update(self.parameters)
+
+        mode = self.get_selected_mode(session)
+
+        if mode:
+            mode.scope(session, params)
+
+    def do_process(self, session, object):
+        mode = self.get_selected_mode(session)
+
+        if mode:
+            mode.process(session, object)
+
+    def do_render(self, session, object):
+        mode = self.get_selected_mode(session)
+
+        if mode:
+            return mode.render(session, object)
+
+class TabSet(ModeSet):
+    def __init__(self, app, name):
+        super(TabSet, self).__init__(app, name)
+
+    def do_render(self, session, object):
+        writer = Writer()
+
+        self.template.render(session, object, writer)
+
+        return writer.to_string()
+
+    # XXX make this use an item template
+    def render_tabs(self, session, object):
+        writer = Writer()
+        str = """<li><a href="%s" class="%s">%s</a></li>"""
+
+        for mode in self.children:
+            branch = session.branch()
+            self.set_selected_mode(branch, mode)
+            href = branch.marshal()
+
+            smode = self.get_selected_mode(session)
+            selected = smode == mode and "selected" or ""
+
+            content = mode.render_title(session, object)
+
+            writer.write(str % (href, selected, content))
+
+        return writer.to_string()
+
+    def render_mode(self, session, object):
+        mode = self.get_selected_mode(session)
+
+        if mode:
+            return mode.render(session, object)
+
+class Link(Widget):
+    def update_session(self, session, object):
+        pass
+
+    def render_href(self, session, object):
+        branch = session.branch()
+
+        self.update_session(branch, object)
+        
+        return branch.marshal()
+
+class Toggle(Link):
+    def __init__(self, app, name):
+        super(Toggle, self).__init__(app, name)
+
+        self.toggled = BooleanParameter(app, "param")
+        self.add_parameter(self.toggled)
+
+    def get(self, session):
+        return self.toggled.get(session)
+
+    def set(self, session, toggled):
+        self.toggled.set(session, toggled)
+
+    def do_process(self, session, object):
+        if self.get(session):
+            self.on_click(session, object)
+
+    def on_click(self, session, object):
+        pass
+
+    def render_href(self, session, object):
+        branch = session.branch()
+        self.set(branch, not self.get(session))
+        return branch.marshal()
+
+    def render_state(self, session, object):
+        return self.toggled.get(session) and "on" or "off"
+
+class ItemSet(Widget):
+    def __init__(self, app, name):
+        super(ItemSet, self).__init__(app, name)
+
+        self.item_tmpl = Template(self, "item_html")
+
+    def get_items(self, session, object):
+        return None
+
+    def render_items(self, session, object):
+        items = self.get_items(session, object)
+
+        if items:
+            writer = Writer()
+            
+            for item in items:
+                self.item_tmpl.render(session, item, writer)
+
+            return writer.to_string()
+
+    def render_item_content(self, session, item):
+        return None

Added: mgmt/cumin/python/wooly/widgets.strings
===================================================================
--- mgmt/cumin/python/wooly/widgets.strings	                        (rev 0)
+++ mgmt/cumin/python/wooly/widgets.strings	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,60 @@
+[TabSet.css]
+ul.TabSet.tabs {
+  padding: 0;
+  margin: 1em 0 0 0;
+  list-style: none;
+}
+
+.TabSet.tabs li {
+  display: inline;
+}
+
+.TabSet.tabs li a {
+  padding: 0.25em 0.5em;
+  border-top: 1px solid #ccc;
+  border-right: 1px solid #ccc;
+  background-color: #f7f7f7;
+  color: #000;
+  line-height: 1.5em;
+}
+
+.TabSet.tabs li:first-child a {
+  border-left: 1px solid #ccc;
+}
+
+.TabSet.tabs li a.selected {
+  background-color: #fff;
+  position: relative;
+  z-index: 2;
+}
+
+.TabSet.mode {
+  background-color: white;
+  padding: 1em;
+  border: 1px solid #ccc;
+  margin: 0;
+  background-color: #fff;
+  position: relative;
+  z-index: 1;
+}
+
+[TabSet.html]
+<ul class="TabSet tabs">{tabs}</ul>
+<div class="TabSet mode">{mode}</div>
+
+[Link.html]
+<a href="{href}">{content}</a>
+
+[Toggle.css]
+.Toggle.on {
+  font-weight: bold;
+}
+
+[Toggle.html]
+<a href="{href}" class="Toggle {state}">{content}</a>
+
+[ItemSet.html]
+<ul class="ItemSet">{items}</ul>
+
+[ItemSet.item_html]
+<li>{item_content}</li>

Added: mgmt/cumin/resources/ajax-test.html
===================================================================
--- mgmt/cumin/resources/ajax-test.html	                        (rev 0)
+++ mgmt/cumin/resources/ajax-test.html	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,32 @@
+<html>
+  <head>
+    <title>test</title>
+    <script>
+      var xhr = new XMLHttpRequest()
+
+      function getCount() {
+          xhr.open("get", "http://localhost:8080/count", true)
+          xhr.onreadystatechange = updateCount
+          xhr.send(null)
+      }
+
+      function updateCount() {
+          if (xhr.readyState == 4 && xhr.status == 200) {
+              countNode = xhr.responseXML.getElementsByTagName("count")[0]
+              count = countNode.firstChild.nodeValue
+
+              var body = document.getElementsByTagName("body")
+
+              var div = document.createElement("div")
+              div.appendChild(document.createTextNode(count))
+
+              body[0].appendChild(div)
+          }
+      }
+
+      setInterval(getCount, 5000)
+    </script>
+  </head>
+  <body>
+  </body>
+</html>

Added: mgmt/cumin/resources/ajax.js
===================================================================
--- mgmt/cumin/resources/ajax.js	                        (rev 0)
+++ mgmt/cumin/resources/ajax.js	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,67 @@
+// Rename this to wooly.js
+
+wooly = {};
+wooly.updaters = {};
+
+function WoolyUpdater(id, url, callback, interval) {
+    this.id = id;
+    this.url = url;
+    this.interval = interval;
+    this.callback = callback;
+    this.request = new XMLHttpRequest();
+
+    this.init = function() {
+        setInterval(this.fetch, this.interval);
+    }
+
+    this.fetch = function() {
+        this.request.open("get", this.url, true);
+        this.request.onreadystatechange = this.update
+        this.send(null)
+    }
+
+    this.update = function() {
+        if (request.readyState == 4 && request.status == 200) {
+            elem = document.getElementById(this.id)
+            this.callback(xml, elem)
+        }
+    }
+}    
+
+function AjaxRequest() {
+    try {
+        this.request = window.XMLHttpRequest();
+    } catch (e) {
+        try {
+            this.request = new ActiveXObject("Msxml2.XMLHTTP");
+        } catch (ie) {
+            try {
+                this.request = new ActiveXObject("Microsoft.XMLHTTP");
+            } catch (iie) {
+                throw new Error("XMLHttpRequest not found");
+            }
+        }
+    }
+
+    // XXX configure with an xpath expression?  pg 515 in the big js
+    // book
+
+    this.get = function(url, callback) {
+        this.request.open("GET", url, true);
+
+        // XXX set some headers
+        //this.request.setRequestHeader("If-Modified-Since",
+        //                              lastRequested.toString());
+
+        this.onreadystatechange = function() {
+            if (this.request.readyState == 4 && this.request.status == 200) {
+                callback(request.responseXML)
+            }
+        }
+
+        this.request.send(null)
+    }
+}
+
+req = new AjaxRequest();
+req.get("queue-xml?id={{id}}", updateStatus)
\ No newline at end of file

Added: mgmt/cumin/resources/exchange-20.png
===================================================================
(Binary files differ)


Property changes on: mgmt/cumin/resources/exchange-20.png
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: mgmt/cumin/resources/exchange-36.png
===================================================================
(Binary files differ)


Property changes on: mgmt/cumin/resources/exchange-36.png
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: mgmt/cumin/resources/exchange.svg
===================================================================
--- mgmt/cumin/resources/exchange.svg	                        (rev 0)
+++ mgmt/cumin/resources/exchange.svg	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="744.09448819"
+   height="1052.3622047"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.45.1"
+   sodipodi:docbase="/home/justin/cumindev/cumin/resources"
+   sodipodi:docname="exchange.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="549.10742"
+     inkscape:cy="503.26882"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     inkscape:window-width="1418"
+     inkscape:window-height="956"
+     inkscape:window-x="29"
+     inkscape:window-y="45" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <g
+       id="g3181"
+       inkscape:export-filename="/home/justin/cumindev/cumin/resources/exchange-20.png"
+       inkscape:export-xdpi="3.506865"
+       inkscape:export-ydpi="3.506865">
+      <path
+         sodipodi:nodetypes="cccccccccccccc"
+         d="M 313.86973,832.3721 C 312.11118,709.27939 279.68924,569.91252 207.1601,445.66059 L 168.9849,468.05869 L 205.55073,331.59313 L 342.95181,368.40964 L 305.42972,390.07303 C 344.62364,462.24138 365.78921,497.41554 391.10471,587.93301 C 409.27317,551.38754 456.55215,499.0514 474.28581,483.53568 L 444.90847,454.15833 L 585.197,454.15833 L 585.197,590.33585 L 554.87823,560.01708 C 484.07919,636.62814 448.21282,711.11613 446.96398,832.3721 C 446.96398,832.3721 446.4501,832.3721 313.86973,832.3721 z "
+         style="fill:#ffaa66;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:12.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         id="path2164" />
+      <path
+         transform="translate(47.55914,-118.77957)"
+         d="M 296 598.36218 A 54 54 0 1 1  188,598.36218 A 54 54 0 1 1  296 598.36218 z"
+         sodipodi:ry="54"
+         sodipodi:rx="54"
+         sodipodi:cy="598.36218"
+         sodipodi:cx="242"
+         id="path2190"
+         style="fill:#ff5577;fill-opacity:1;stroke:#000000;stroke-width:12.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:type="arc" />
+      <path
+         transform="translate(229.66129,-27.66129)"
+         d="M 296 598.36218 A 54 54 0 1 1  188,598.36218 A 54 54 0 1 1  296 598.36218 z"
+         sodipodi:ry="54"
+         sodipodi:rx="54"
+         sodipodi:cy="598.36218"
+         sodipodi:cx="242"
+         id="path3165"
+         style="fill:#77aaff;fill-opacity:1;stroke:#000000;stroke-width:12.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:type="arc" />
+    </g>
+  </g>
+</svg>

Added: mgmt/cumin/resources/logo.png
===================================================================
(Binary files differ)


Property changes on: mgmt/cumin/resources/logo.png
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: mgmt/cumin/resources/logo.svg
===================================================================
--- mgmt/cumin/resources/logo.svg	                        (rev 0)
+++ mgmt/cumin/resources/logo.svg	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="744.09448819"
+   height="1052.3622047"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.45"
+   sodipodi:modified="TRUE">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.35"
+     inkscape:cx="375"
+     inkscape:cy="520"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <rect
+       style="fill:#564979;fill-opacity:1;stroke:#bfdce8;stroke-width:0.07982907;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="rect2160"
+       width="39.203495"
+       height="22.692081"
+       x="176.57945"
+       y="149.64551" />
+    <g
+       id="g3141"
+       inkscape:export-filename="/home/jross/cumindev/cumin/resources/logo.png"
+       inkscape:export-xdpi="98.580963"
+       inkscape:export-ydpi="98.580963">
+      <text
+         transform="scale(1.0989239,0.9099811)"
+         sodipodi:linespacing="125%"
+         id="text3133"
+         y="178.24783"
+         x="164.23683"
+         style="font-size:10.9197731px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ff9f00;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"
+         xml:space="preserve"><tspan
+           y="178.24783"
+           x="164.23683"
+           id="tspan3135"
+           sodipodi:role="line">RHM</tspan></text>
+      <text
+         transform="scale(1.2362181,0.8089188)"
+         sodipodi:linespacing="125%"
+         id="text3137"
+         y="205.76395"
+         x="146.27437"
+         style="font-size:7.40763378px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-family:Bitstream Vera Sans"
+         xml:space="preserve"><tspan
+           y="205.76395"
+           x="146.27437"
+           id="tspan3139"
+           sodipodi:role="line">mgmt</tspan></text>
+    </g>
+  </g>
+</svg>

Added: mgmt/cumin/resources/object-20.png
===================================================================
(Binary files differ)


Property changes on: mgmt/cumin/resources/object-20.png
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: mgmt/cumin/resources/object-36.png
===================================================================
(Binary files differ)


Property changes on: mgmt/cumin/resources/object-36.png
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: mgmt/cumin/resources/object.svg
===================================================================
--- mgmt/cumin/resources/object.svg	                        (rev 0)
+++ mgmt/cumin/resources/object.svg	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="744.09448819"
+   height="1052.3622047"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.45.1"
+   sodipodi:docbase="/home/justin/cumindev/cumin/resources"
+   sodipodi:docname="object.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.35"
+     inkscape:cx="375"
+     inkscape:cy="520"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     inkscape:window-width="814"
+     inkscape:window-height="619"
+     inkscape:window-x="0"
+     inkscape:window-y="25" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <g
+       id="g3139"
+       transform="translate(-194.28571,31.428571)"
+       inkscape:export-filename="/home/justin/cumindev/cumin/resources/object-20.png"
+       inkscape:export-xdpi="3.4734666"
+       inkscape:export-ydpi="3.4734666">
+      <rect
+         y="100.93361"
+         x="317.14285"
+         height="505.71429"
+         width="505.71429"
+         id="rect2166"
+         style="fill:#ffaa66;fill-opacity:1;stroke:#000000;stroke-width:12.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <rect
+         transform="matrix(0.7071068,-0.7071068,0.7071068,0.7071068,0,0)"
+         y="438.85617"
+         x="79.941772"
+         height="147.90282"
+         width="147.90282"
+         id="rect2160"
+         style="fill:#77aaff;fill-opacity:1;stroke:#000000;stroke-width:12.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <path
+         transform="translate(485.71428,-20)"
+         d="M 282.85715 492.36218 A 88.571434 88.571434 0 1 1  105.71429,492.36218 A 88.571434 88.571434 0 1 1  282.85715 492.36218 z"
+         sodipodi:ry="88.571434"
+         sodipodi:rx="88.571434"
+         sodipodi:cy="492.36218"
+         sodipodi:cx="194.28572"
+         id="path2162"
+         style="fill:#ff5577;fill-opacity:1;stroke:#000000;stroke-width:12.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:type="arc" />
+    </g>
+  </g>
+</svg>

Added: mgmt/cumin/resources/purple.png
===================================================================
(Binary files differ)


Property changes on: mgmt/cumin/resources/purple.png
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: mgmt/cumin/resources/queue-20.png
===================================================================
(Binary files differ)


Property changes on: mgmt/cumin/resources/queue-20.png
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: mgmt/cumin/resources/queue-36.png
===================================================================
(Binary files differ)


Property changes on: mgmt/cumin/resources/queue-36.png
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: mgmt/cumin/resources/queue.svg
===================================================================
--- mgmt/cumin/resources/queue.svg	                        (rev 0)
+++ mgmt/cumin/resources/queue.svg	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="744.09448819"
+   height="1052.3622047"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.45.1"
+   sodipodi:docbase="/home/justin/cumindev/cumin/resources"
+   sodipodi:docname="queue.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.68801116"
+     inkscape:cx="441.43343"
+     inkscape:cy="526.18109"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     inkscape:window-width="814"
+     inkscape:window-height="892"
+     inkscape:window-x="33"
+     inkscape:window-y="41" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <g
+       id="g3146">
+      <path
+         transform="matrix(0.9063078,0.4226182,-0.4226182,0.9063078,86.342292,293.33708)"
+         d="M 552.93207 223.99181 A 195.27341 52.415497 0 1 1  162.38525,223.99181 A 195.27341 52.415497 0 1 1  552.93207 223.99181 z"
+         sodipodi:ry="52.415497"
+         sodipodi:rx="195.27341"
+         sodipodi:cy="223.99181"
+         sodipodi:cx="357.65866"
+         id="path3134"
+         style="fill:#ffaa66;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:12.50000018;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:type="arc" />
+      <path
+         transform="matrix(0.9063078,0.4226182,-0.4226182,0.9063078,116.01145,231.71374)"
+         d="M 552.93207 223.99181 A 195.27341 52.415497 0 1 1  162.38525,223.99181 A 195.27341 52.415497 0 1 1  552.93207 223.99181 z"
+         sodipodi:ry="52.415497"
+         sodipodi:rx="195.27341"
+         sodipodi:cy="223.99181"
+         sodipodi:cx="357.65866"
+         id="path4105"
+         style="fill:#ffaa66;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:12.50000018;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:type="arc" />
+      <path
+         transform="matrix(0.9063078,0.4226182,-0.4226182,0.9063078,145.68064,170.09034)"
+         d="M 552.93207 223.99181 A 195.27341 52.415497 0 1 1  162.38525,223.99181 A 195.27341 52.415497 0 1 1  552.93207 223.99181 z"
+         sodipodi:ry="52.415497"
+         sodipodi:rx="195.27341"
+         sodipodi:cy="223.99181"
+         sodipodi:cx="357.65866"
+         id="path4109"
+         style="fill:#ffaa66;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:12.50000018;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:type="arc" />
+      <path
+         transform="matrix(0.9063078,0.4226182,-0.4226182,0.9063078,175.34983,108.467)"
+         d="M 552.93207 223.99181 A 195.27341 52.415497 0 1 1  162.38525,223.99181 A 195.27341 52.415497 0 1 1  552.93207 223.99181 z"
+         sodipodi:ry="52.415497"
+         sodipodi:rx="195.27341"
+         sodipodi:cy="223.99181"
+         sodipodi:cx="357.65866"
+         id="path4113"
+         style="fill:#ffaa66;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:12.50000018;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:type="arc" />
+      <path
+         sodipodi:nodetypes="cccccccc"
+         id="path5088"
+         d="M 454.10516,344.26002 L 399.27203,461.85003 L 359.20085,443.16454 L 406.83279,566.23776 L 529.50338,522.5779 L 489.4322,503.89241 L 544.76381,385.23338 L 454.10516,344.26002 z "
+         style="fill:#ff5577;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:12.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <path
+         sodipodi:nodetypes="cccccccc"
+         id="path4117"
+         d="M 310.19505,419.15001 L 365.02818,301.56001 L 324.95699,282.87451 L 449.85377,240.25274 L 495.25952,362.28789 L 455.18834,343.60238 L 399.85673,462.26141 L 310.19505,419.15001 z "
+         style="fill:#77aaff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:12.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+  </g>
+</svg>

Added: mgmt/cumin/resources/radio-button-checked.png
===================================================================
(Binary files differ)


Property changes on: mgmt/cumin/resources/radio-button-checked.png
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: mgmt/cumin/resources/radio-button.png
===================================================================
(Binary files differ)


Property changes on: mgmt/cumin/resources/radio-button.png
___________________________________________________________________
Name: svn:mime-type
   + application/octet-stream

Added: mgmt/cumin/resources/radio-buttons.svg
===================================================================
--- mgmt/cumin/resources/radio-buttons.svg	                        (rev 0)
+++ mgmt/cumin/resources/radio-buttons.svg	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="744.09448819"
+   height="1052.3622047"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.45"
+   sodipodi:docbase="/home/jross/cumindev/cumin/resources"
+   sodipodi:docname="radio-buttons.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape"
+   sodipodi:modified="TRUE">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     gridtolerance="10000"
+     guidetolerance="10"
+     objecttolerance="10"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="2.4431138"
+     inkscape:cx="261"
+     inkscape:cy="591.5"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     inkscape:window-width="814"
+     inkscape:window-height="620"
+     inkscape:window-x="423"
+     inkscape:window-y="159" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <g
+       id="g3167"
+       inkscape:export-filename="/home/jross/cumindev/cumin/resources/radio-button-checked.png"
+       inkscape:export-xdpi="90"
+       inkscape:export-ydpi="90">
+      <path
+         transform="matrix(0.5990025,0,0,0.5990025,87.699389,185.92954)"
+         d="M 243.24474 452.02853 A 12.020815 12.020815 0 1 1  219.20311,452.02853 A 12.020815 12.020815 0 1 1  243.24474 452.02853 z"
+         sodipodi:ry="12.020815"
+         sodipodi:rx="12.020815"
+         sodipodi:cy="452.02853"
+         sodipodi:cx="231.22392"
+         id="path2160"
+         style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1.66944211;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+         sodipodi:type="arc" />
+      <path
+         transform="matrix(0.5990025,0,0,0.5990025,86.746382,184.65886)"
+         d="M 239.88597 454.23822 A 6.9826794 6.9826794 0 1 1  225.92061,454.23822 A 6.9826794 6.9826794 0 1 1  239.88597 454.23822 z"
+         sodipodi:ry="6.9826794"
+         sodipodi:rx="6.9826794"
+         sodipodi:cy="454.23822"
+         sodipodi:cx="232.90329"
+         id="path3133"
+         style="fill:#4e9fdd;fill-opacity:1;stroke:#bfdce8;stroke-opacity:1;stroke-width:1.66944211;stroke-miterlimit:4;stroke-dasharray:none"
+         sodipodi:type="arc" />
+    </g>
+    <path
+       sodipodi:type="arc"
+       style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#cccccc;stroke-width:1.66944206;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       id="path3163"
+       sodipodi:cx="231.22392"
+       sodipodi:cy="452.02853"
+       sodipodi:rx="12.020815"
+       sodipodi:ry="12.020815"
+       d="M 243.24474 452.02853 A 12.020815 12.020815 0 1 1  219.20311,452.02853 A 12.020815 12.020815 0 1 1  243.24474 452.02853 z"
+       transform="matrix(0.5990025,0,0,0.5990025,121.41051,185.92954)"
+       inkscape:export-filename="/home/jross/cumindev/cumin/resources/radio-button.png"
+       inkscape:export-xdpi="90"
+       inkscape:export-ydpi="90" />
+  </g>
+</svg>

Added: mgmt/cumin/resources/wooly.js
===================================================================
--- mgmt/cumin/resources/wooly.js	                        (rev 0)
+++ mgmt/cumin/resources/wooly.js	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,221 @@
+var wooly;
+
+(function() {
+    wooly = new Wooly();
+
+    function assert() {
+        for (var i = 0; i < arguments.length; i++) {
+            if (!arguments[i]) {
+                throw new Error("Assertion failure in " + arguments.callee.caller.prototype);
+            }
+        }
+    }
+
+    function log() {
+        if (wooly.console) {
+            wooly.console.log.apply(wooly.console, arguments);
+        }
+    }
+
+    function dir() {
+        if (wooly.console) {
+            wooly.console.dir.apply(wooly.console, arguments);
+        }
+    }
+
+    function Wooly() {
+        this.request = new XMLHttpRequest();
+        
+        this.assert = assert;
+        this.log = log;
+        this.dir = dir;
+
+        if (window.console) {
+            this.console = window.console;
+        }
+
+        this.setIntervalUpdate = function(id, url, callback, interval) {
+            var req = this.request;
+
+            function fetch() {
+                req.open("get", url, true);
+                req.onreadystatechange = update;
+                req.send(null);
+            }
+
+            var timerid = window.setInterval(fetch, interval);
+
+            function update() {
+                try {
+                    if (req.readyState == 4 && req.status == 200) {
+                        //dir(req);
+
+                        var elem = wooly.doc().elem(id);
+
+                        callback(wooly.doc(req.responseXML), elem);
+                    }
+                } catch (e) {
+                    log(e);
+                    // XXX might want to retry for a bit before we do
+                    // this
+                    window.clearInterval(timerid);
+                    throw e;
+                }
+            }
+        }
+
+        this._doc = new WoolyDocument(document);
+
+        this.doc = function(doc) {
+            if (doc) {
+                return new WoolyDocument(doc);
+            } else {
+                return this._doc;
+            }
+        }
+    }
+
+    function WoolyDocument(node) {
+        assert(node);
+
+        this.node = node;
+
+        this.elem = function(id) {
+            var node = this.node.getElementById(id);
+
+            if (node) {
+                return new WoolyElement(this, node);
+            }
+        }
+
+        this.elems = function(tag) {
+            var nodes = this.node.getElementsByTagName(tag);
+            
+            return new WoolyIterator(this, WoolyElement,
+                                     nodes, 1, tag);
+        }
+    }
+
+    function WoolyIterator(doc, nodeClass, nodes, nodeType, nodeName) {
+        assert(doc);
+        assert(doc instanceof WoolyDocument);
+        assert(nodeClass);
+        assert(nodes);
+        assert(nodes instanceof NodeList);
+        assert(nodeType);
+        assert(typeof nodeType == "number");
+        if (nodeName) assert(typeof nodeName == "string");
+
+        this.doc = doc;
+        this.nodes = nodes;
+        this.nodeClass = nodeClass;
+        this.nodeType = nodeType;
+        this.nodeName = nodeName;
+
+        this.lastIndex = -1;
+
+        this.next = function() {
+            var node;
+
+            for (var i = this.lastIndex + 1; i < this.nodes.length; i++) {
+                node = this.nodes[i];
+
+                if (this.nodeType == null
+                    || node.nodeType == this.nodeType) {
+                    if (this.nodeName == null
+                        || node.nodeName.toLowerCase() == this.nodeName) {
+                        this.lastIndex = i;
+                        return new this.nodeClass(this.doc, node);
+                    }
+                }
+            }
+
+            return null;
+        }
+    }
+
+    function WoolyElement(doc, node) {
+        assert(doc);
+        assert(doc instanceof WoolyDocument);
+        assert(node);
+        assert(node instanceof Node, node.nodeType == 1);
+
+        this.doc = doc;
+        this.node = node;
+
+        this.clear = function() {
+            var child = this.node.firstChild;
+            var next;
+
+            while (child) {
+                next = child.nextSibling;
+                this.node.removeChild(child);
+                child = next;
+            }
+
+            return this;
+        }
+
+        this.add = function(content) {
+            if (typeof content == "string") {
+                this.add(new WoolyText(this.doc, null).set(content));
+            } else if (content.hasOwnProperty("node")) {
+                this.node.appendChild(content.node);
+            } else {
+                throw new Error("Content is of unexpected type");
+            }
+
+            return this;
+        }
+
+        this.set = function(content) {
+            this.clear().add(content);
+
+            return this;
+        }
+
+        this.elems = function(name) {
+            return new WoolyIterator(this.doc, WoolyElement,
+                                     this.node.childNodes,
+                                     1, name);
+        }
+
+        this.text = function() {
+            var children = this.node.childNodes;
+
+            for (var i = 0; i < children.length; i++) {
+                var child = children[i];
+
+                if (child.nodeType == 3) return new WoolyText(this.doc, child);
+            }
+
+            return null;
+        }
+    }
+
+    function WoolyText(doc, node) {
+        assert(doc);
+        assert(doc instanceof WoolyDocument);
+        if (node) assert(node instanceof Node, node.nodeType == 3);
+
+        this.doc = doc;
+
+        if (node == null) {
+            this.node = doc.node.createTextNode("");
+        } else {
+            this.node = node;
+        }
+
+        this.get = function() {
+            return this.node.data
+        }
+
+        this.set = function(data) {
+            assert(typeof data == "string");
+
+            this.node.data = data
+
+            return this;
+        }
+    }
+}())

Added: mgmt/cumin-test-0/bin
===================================================================
--- mgmt/cumin-test-0/bin	                        (rev 0)
+++ mgmt/cumin-test-0/bin	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1 @@
+link ../cumin/bin
\ No newline at end of file


Property changes on: mgmt/cumin-test-0/bin
___________________________________________________________________
Name: svn:special
   + *

Added: mgmt/cumin-test-0/lib
===================================================================
--- mgmt/cumin-test-0/lib	                        (rev 0)
+++ mgmt/cumin-test-0/lib	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1 @@
+link ../lib
\ No newline at end of file


Property changes on: mgmt/cumin-test-0/lib
___________________________________________________________________
Name: svn:special
   + *

Added: mgmt/cumin-test-0/python
===================================================================
--- mgmt/cumin-test-0/python	                        (rev 0)
+++ mgmt/cumin-test-0/python	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1 @@
+link ../cumin/python
\ No newline at end of file


Property changes on: mgmt/cumin-test-0/python
___________________________________________________________________
Name: svn:special
   + *

Added: mgmt/cumin-test-0/resources
===================================================================
--- mgmt/cumin-test-0/resources	                        (rev 0)
+++ mgmt/cumin-test-0/resources	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1 @@
+link ../cumin/resources
\ No newline at end of file


Property changes on: mgmt/cumin-test-0/resources
___________________________________________________________________
Name: svn:special
   + *

Added: mgmt/etc/cumindev.el
===================================================================
--- mgmt/etc/cumindev.el	                        (rev 0)
+++ mgmt/etc/cumindev.el	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,10 @@
+(setq cumindev-home (getenv "CUMINDEV_HOME"))
+
+(if cumindev-home
+    (progn
+      ;(desktop-read cumindev-home)
+      (shell "dev")
+      (setq tags-file-name (concat cumindev-home "/etc/cumindev.tags"))
+      (setq grep-command (concat "find " cumindev-home " -name \\*.py -print | xargs fgrep -n "))
+      (setq compile-command "cumin-test"))
+  (display-warning 'cumindev "Environment variable CUMINDEV_HOME not set" :error))

Added: mgmt/etc/cumindev.profile
===================================================================
--- mgmt/etc/cumindev.profile	                        (rev 0)
+++ mgmt/etc/cumindev.profile	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,11 @@
+if [ -z "$CUMINDEV_HOME" ]; then 
+    export CUMINDEV_HOME="$PWD"
+fi
+
+export CUMIN_HOME="$CUMINDEV_HOME"/cumin-test-0
+
+export PYTHONPATH="$CUMIN_HOME"/lib:"$CUMIN_HOME"/python
+
+export CUMINDEV_ORIGINAL_PATH="$PATH"
+
+export PATH="$CUMIN_HOME"/bin:"$CUMINDEV_HOME"/bin:"$CUMINDEV_ORIGINAL_PATH"

Added: mgmt/misc/templates.py
===================================================================
--- mgmt/misc/templates.py	                        (rev 0)
+++ mgmt/misc/templates.py	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,113 @@
+text0 = """
+<form id="{id}" class="QueueForm mform" method="post" action="?">
+  <div class="head">
+    <h1>{title}</h1>
+  </div>
+  <div class="body">
+    <span class="legend">Name</span>
+    <fieldset>
+      <div class="field">{queue_name}</div>
+    </fieldset>
+    <span class="legend">Latency Tuning</span>
+    <fieldset>
+      <div class="field">
+        {latency}
+        <em>Lower Latency:</em> Tune for shorter delays, with reduced volume
+      </div>
+      <div class="field">
+        {balanced}
+        <em>Balanced</em>
+      </div>
+      <div class="field">
+        {throughput}
+        <em>Higher Throughput:</em> Tune for increased volume, with longer
+        delays
+      </div>
+    </fieldset>
+    <span class="legend">Realms</span>
+    <fieldset>{realms}</fieldset>
+{hidden_inputs}
+  </div>
+  <div class="foot">
+    <div style="display: block; float: left;"><button>Help</button></div>
+{cancel}
+{submit}
+  </div>
+</form>
+<script defer="defer">
+var id = "{id}";
+(function() {
+    // XXX elements[0] is a fieldset, at least in firefox
+    var elem = wooly.doc().elem(id).node.elements[1];
+    elem.focus();
+    elem.select();
+}())
+</script>
+"""
+
+def parse(text):
+    fragments = list()
+
+    start = 0
+    end = text.find("{")
+
+    while True:
+        if (end == -1):
+            fragments.append(text[start:])
+            break
+
+        fragments.append(text[start:end])
+
+        ccurly = text.find("}", end + 1)
+
+        if ccurly == -1:
+            start = end
+            end = -1
+        else:
+            ocurly = text.find("{", end + 1)
+
+            if ocurly == -1:
+                start = end
+                end = ccurly + 1
+            elif ocurly < ccurly:
+                start = end
+                end = ocurly
+            else:
+                fragments.append("{" + text[end + 1:ccurly] + "}")
+                    
+                start = ccurly + 1
+                end = ocurly
+
+    return fragments
+
+if __name__ == "__main__":
+    from time import clock
+    from cStringIO import StringIO
+    
+    texts = ["x{y}z}a{b}c",
+             "x{y",
+             "x}y",
+             "{{{",
+             "}{}{",
+             "x{y{z}"]
+
+    for text in texts:
+        print text, parse(text)
+
+    frags = None
+    start = clock()
+
+    for i in range(10000):
+        frags = parse(text0)
+
+    print clock() - start
+
+    start = clock()
+        
+    for i in range(10000):
+        buffer = StringIO()
+        
+        for frag in frags:
+            buffer.write(frag)
+
+    print clock() - start

Added: mgmt/notes/Errors
===================================================================
--- mgmt/notes/Errors	                        (rev 0)
+++ mgmt/notes/Errors	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,3 @@
+[justin at localhost cumindev]$ cumindev-etags
+/home/justin/cumindev/cumin/python/cumin/.#model.py: No such file or directory
+

Added: mgmt/notes/InterfaceQuestions
===================================================================
--- mgmt/notes/InterfaceQuestions	                        (rev 0)
+++ mgmt/notes/InterfaceQuestions	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,5 @@
+ * Should the default exchange appear in the UI at all?
+
+ * Is there any place in the UI for transient queues and exchanges?
+   It would seem not, except perhaps in status information.
+

Added: mgmt/notes/ProtocolQuestions
===================================================================
--- mgmt/notes/ProtocolQuestions	                        (rev 0)
+++ mgmt/notes/ProtocolQuestions	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,19 @@
+ * Why use null for the default exchange name?  It would seem
+   consistent and more convenient in developing software to use a name
+   such as "amq.default"
+
+ * Can the routing key of the default binding to the default exchange
+   for new queues be changed?  How different is the default exchange
+   from other types of exchanges?  The docs suggest that the default
+   binding is for "point and shoot" use of the broker, so it would
+   make sense if the default exchange only supported simple
+   queue-named routing keys, no?
+
+ * I didn't run into anything that spelled out the multiplicity of
+   bindings vis a vis queues.  I assumed that queues and exchanges
+   support many bindings.
+
+ * Can a queue, for instance, be in more than one realm?
+
+ * It strikes me that UML would answer many of my questions.
+

Added: mgmt/notes/Todo
===================================================================
--- mgmt/notes/Todo	                        (rev 0)
+++ mgmt/notes/Todo	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,69 @@
+Big picture
+
+ * A more-or-less complete demonstration of an admin UI
+
+Higher
+
+Lower
+
+ * more form inputs, non scalar ones too
+
+ * Add an error banner to form
+
+ * use wsgiref instead of BaseHTTPServer
+
+ * Make sure HTTPServer handles concurrent requests; need to look at
+   documentation
+
+   - It does not, and it's not easily changed.  Will need to switch to
+     wsgi stuff, I believe.
+
+ * When in queue mode, make the context nav go back to the right tab
+
+ * Change declared charset to iso-8859???, not utf-8, since it's
+   important to be honest
+
+ * Only do render timing conditionally
+
+ * Make debug and devel things contingent upon a start-time variable
+   "--debug"
+
+ * Make it a little simpler to express hrefs
+
+ * Add disabling to form inputs, and disable renaming of exchanges
+   that start with "amq." and the default exchange
+
+ * Make form help buttons pop up a (for now, empty) help page
+
+ * Icons: machine looking thing for vhost.  For Cumin in general, I'm
+   not sure.  The hammer and screwdriver?
+
+ * Make item counts in tab labels a little grayer, that is, less
+   intense than the name
+
+ * If debug is enabled, append a comment to the response containing
+   render and process traces
+
+ * Write a little test comparing wooly.Template to string.Template to
+   using the % operator
+
+ * Add ability to send a test message to a queue
+
+ * Add favicon and a mapping in the server to serve it
+
+ * Create a model.dtd
+
+ * Add scalar get/setters to ModelObject
+
+ * Separate wooly stuff into its own devel subdir
+
+ * When there is nothing in a set, render a [None]
+
+ * Add creation dates to some objects
+
+ * cumindev: Bind .strings to html-mode
+
+ * cumindev: add a cumin-test function and bind it to C-c C-c
+
+ * Consider adding a set_object to Frame, instead of having
+   set_somethingspecific on each frame.  

Added: mgmt/notes/WoolyOverview
===================================================================
--- mgmt/notes/WoolyOverview	                        (rev 0)
+++ mgmt/notes/WoolyOverview	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,40 @@
+Widget
+ - A display object
+ - Usually bound to a Template
+ - May have state represented in Parameters
+ - Lives in a tree of widgets
+ - Has a process method
+   - Manipulates state, both ui state and model state
+   - Called in the process phase, before rendering
+ - Has a render method
+   - Produces HTML
+   - Called after all processing is done
+
+Template
+ - A string with placeholders such as {foo}
+ - A placeholder {foo} resolves to: widget.render_foo(...)
+ - Or it resolves to: widget.get_child("foo").render(...)
+
+Application
+ - The static state of the app
+ - Holds Pages
+
+Page
+ - A top-level widget with some extra methods for producing HTTP
+   responses
+ - The root widget of all widget trees is a page
+
+Parameter
+ - Represents state for the life of the request/response
+ - Attached to a widget
+ - Stores its session-level state on a Session
+ - Marshals and unmarshals itself to url params
+
+Session
+ - The main source of state
+ - Can be "branched" for producing "future states", URLs
+
+ModeSet
+ - A widget that shows only one of its children
+ - Used for producing various UI behaviors, tabs for instance
+ - Generally useful for controlling visibility

Added: mgmt/notes/firebug-exception-bug.js
===================================================================
--- mgmt/notes/firebug-exception-bug.js	                        (rev 0)
+++ mgmt/notes/firebug-exception-bug.js	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,23 @@
+var scratchy;
+
+(function() {
+    scratchy = new Scratchy();
+
+    function Scratchy() {
+        this.x = new Object();
+
+        this.itchy = function(callback, interval) {
+            var x = this.x;
+
+            setTimeout(show, 1000);
+
+            function show() {
+                try {
+                    callback();
+                } catch (e) {
+                    throw e;
+                }
+            }
+        }
+    }
+}());

Added: mgmt/notes/firebug-swallows-exceptions.html
===================================================================
--- mgmt/notes/firebug-swallows-exceptions.html	                        (rev 0)
+++ mgmt/notes/firebug-swallows-exceptions.html	2007-10-08 15:51:14 UTC (rev 967)
@@ -0,0 +1,42 @@
+<html>
+<head>
+<title>Test</title>
+<script>
+var scratchy;
+
+(function() {
+    scratchy = new Scratchy();
+
+    function Scratchy() {
+        this.x = true;
+
+        this.itchy = function(callback) {
+            var y = this.x;
+
+            setTimeout(a, 1000);
+
+            function a() {
+                b();
+            }
+
+            function b() {
+                if (y) callback();
+            }
+        }
+    }
+}());
+</script>
+</head>
+<body>
+Test
+<script defer="defer">
+(function() {
+    var code = function() {
+        throw Error();
+    }
+
+    scratchy.itchy(code);
+}());
+</script>
+</body>
+</html>
\ No newline at end of file




More information about the rhmessaging-commits mailing list