A monster checkin, with a variety of changes to the web
authorEric Price <ecprice@mit.edu>
Sat, 20 Oct 2007 12:28:32 +0000 (08:28 -0400)
committerEric Price <ecprice@mit.edu>
Sat, 20 Oct 2007 12:28:32 +0000 (08:28 -0400)
infrastructure.

Adds some support for javascript and asynchronous updates.

Also added prototype.

The interface is *really* *slow*, though.

svn path=/trunk/web/; revision=205

templates/Makefile [new file with mode: 0644]
templates/functions.py [new file with mode: 0644]
templates/functions.tmpl [new file with mode: 0644]
templates/info.tmpl
templates/list.tmpl
templates/main.py
templates/skeleton.py
templates/skeleton.tmpl
templates/static/prototype.js [new file with mode: 0644]
templates/static/style.css

diff --git a/templates/Makefile b/templates/Makefile
new file mode 100644 (file)
index 0000000..72f3626
--- /dev/null
@@ -0,0 +1,10 @@
+TEMPLATES=functions.tmpl skeleton.tmpl
+OUTPUTS=$(TEMPLATES:.tmpl=.py)
+
+all: ${OUTPUTS}
+
+${OUTPUTS}:${TEMPLATES}
+       cheetah compile $^
+
+clean:
+       @rm -f ${OUTPUTS} *.pyo *.pyc *.py.bak
diff --git a/templates/functions.py b/templates/functions.py
new file mode 100644 (file)
index 0000000..27caad7
--- /dev/null
@@ -0,0 +1,255 @@
+#!/usr/bin/env python
+
+
+
+
+##################################################
+## DEPENDENCIES
+import sys
+import os
+import os.path
+from os.path import getmtime, exists
+import time
+import types
+import __builtin__
+from Cheetah.Version import MinCompatibleVersion as RequiredCheetahVersion
+from Cheetah.Version import MinCompatibleVersionTuple as RequiredCheetahVersionTuple
+from Cheetah.Template import Template
+from Cheetah.DummyTransaction import DummyTransaction
+from Cheetah.NameMapper import NotFound, valueForName, valueFromSearchList, valueFromFrameOrSearchList
+from Cheetah.CacheRegion import CacheRegion
+import Cheetah.Filters as Filters
+import Cheetah.ErrorCatchers as ErrorCatchers
+
+##################################################
+## MODULE CONSTANTS
+try:
+    True, False
+except NameError:
+    True, False = (1==1), (1==0)
+VFFSL=valueFromFrameOrSearchList
+VFSL=valueFromSearchList
+VFN=valueForName
+currentTime=time.time
+__CHEETAH_version__ = '2.0rc8'
+__CHEETAH_versionTuple__ = (2, 0, 0, 'candidate', 8)
+__CHEETAH_genTime__ = 1192883107.2568331
+__CHEETAH_genTimestamp__ = 'Sat Oct 20 08:25:07 2007'
+__CHEETAH_src__ = 'functions.tmpl'
+__CHEETAH_srcLastModified__ = 'Sat Oct 20 07:34:59 2007'
+__CHEETAH_docstring__ = 'Autogenerated by CHEETAH: The Python-Powered Template Engine'
+
+if __CHEETAH_versionTuple__ < RequiredCheetahVersionTuple:
+    raise AssertionError(
+      'This template was compiled with Cheetah version'
+      ' %s. Templates compiled before version %s must be recompiled.'%(
+         __CHEETAH_version__, RequiredCheetahVersion))
+
+##################################################
+## CLASSES
+
+class functions(Template):
+
+    ##################################################
+    ## CHEETAH GENERATED METHODS
+
+
+    def __init__(self, *args, **KWs):
+
+        Template.__init__(self, *args, **KWs)
+        if not self._CHEETAH__instanceInitialized:
+            cheetahKWArgs = {}
+            allowedKWs = 'searchList namespaces filter filtersLib errorCatcher'.split()
+            for k,v in KWs.items():
+                if k in allowedKWs: cheetahKWArgs[k] = v
+            self._initCheetahInstance(**cheetahKWArgs)
+        
+
+    def cdromList(self, cdroms, default="", **KWS):
+
+
+
+        ## CHEETAH: generated from #def cdromList($cdroms, $default="") at line 1, col 1.
+        trans = KWS.get("trans")
+        if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)):
+            trans = self.transaction # is None unless self.awake() was called
+        if not trans:
+            trans = DummyTransaction()
+            _dummyTrans = True
+        else: _dummyTrans = False
+        write = trans.response().write
+        SL = self._CHEETAH__searchList
+        _filter = self._CHEETAH__currentFilter
+        
+        ########################################
+        ## START - generated method body
+        
+        write('''<select name="cdrom">
+  <option ''')
+        if VFFSL(SL,"default",True) : # generated from line 4, col 1
+            _v =  '' 
+            if _v is not None: write(_filter(_v))
+        else:
+            _v =  'selected'
+            if _v is not None: write(_filter(_v))
+        write(''' value="">None</option>
+''')
+        for cdrom in VFFSL(SL,"cdroms",True): # generated from line 6, col 3
+            write('''  <option ''')
+            if VFFSL(SL,"default",True) == VFFSL(SL,"cdrom.cdrom_id",True) : # generated from line 8, col 1
+                _v =  'selected' 
+                if _v is not None: write(_filter(_v))
+            else:
+                _v =  ''
+                if _v is not None: write(_filter(_v))
+            write(''' value="''')
+            _v = VFFSL(SL,"cdrom.cdrom_id",True) # '$cdrom.cdrom_id' on line 9, col 9
+            if _v is not None: write(_filter(_v, rawExpr='$cdrom.cdrom_id')) # from line 9, col 9.
+            write('''">
+    ''')
+            _v = VFFSL(SL,"cdrom.description",True) # '$cdrom.description' on line 10, col 5
+            if _v is not None: write(_filter(_v, rawExpr='$cdrom.description')) # from line 10, col 5.
+            write('''
+  </option>
+''')
+        write('''</select>
+''')
+        
+        ########################################
+        ## END - generated method body
+        
+        return _dummyTrans and trans.response().getvalue() or ""
+        
+
+    def addError(self, txt, **KWS):
+
+
+
+        ## CHEETAH: generated from #def addError(txt) at line 16, col 1.
+        trans = KWS.get("trans")
+        if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)):
+            trans = self.transaction # is None unless self.awake() was called
+        if not trans:
+            trans = DummyTransaction()
+            _dummyTrans = True
+        else: _dummyTrans = False
+        write = trans.response().write
+        SL = self._CHEETAH__searchList
+        _filter = self._CHEETAH__currentFilter
+        
+        ########################################
+        ## START - generated method body
+        
+        if VFFSL(SL,"varExists",False)('txt'): # generated from line 17, col 1
+            self._CHEETAH__globalSetVars["error_text"] = VFFSL(SL,"error_text",True) + '----\n' + VFFSL(SL,"txt",True)
+        else: # generated from line 19, col 1
+            self._CHEETAH__globalSetVars["error_text"] = VFFSL(SL,"txt",True)
+        
+        ########################################
+        ## END - generated method body
+        
+        return _dummyTrans and trans.response().getvalue() or ""
+        
+
+    def errorRow(self, value, err, **KWS):
+
+
+
+        ## CHEETAH: generated from #def errorRow($value, $err) at line 24, col 1.
+        trans = KWS.get("trans")
+        if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)):
+            trans = self.transaction # is None unless self.awake() was called
+        if not trans:
+            trans = DummyTransaction()
+            _dummyTrans = True
+        else: _dummyTrans = False
+        write = trans.response().write
+        SL = self._CHEETAH__searchList
+        _filter = self._CHEETAH__currentFilter
+        
+        ########################################
+        ## START - generated method body
+        
+        if VFFSL(SL,"err",True) and VFFSL(SL,"err.err_field",True) == VFFSL(SL,"value",True): # generated from line 25, col 1
+            write('''<tr>
+<td class="error" colspan="2">''')
+            _v = VFFSL(SL,"str",False)(VFFSL(SL,"err",True)) # '${str($err)}' on line 27, col 31
+            if _v is not None: write(_filter(_v, rawExpr='${str($err)}')) # from line 27, col 31.
+            write('''</td>
+</tr>
+''')
+        
+        ########################################
+        ## END - generated method body
+        
+        return _dummyTrans and trans.response().getvalue() or ""
+        
+
+    def respond(self, trans=None):
+
+
+
+        ## CHEETAH: main method generated for this template
+        if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)):
+            trans = self.transaction # is None unless self.awake() was called
+        if not trans:
+            trans = DummyTransaction()
+            _dummyTrans = True
+        else: _dummyTrans = False
+        write = trans.response().write
+        SL = self._CHEETAH__searchList
+        _filter = self._CHEETAH__currentFilter
+        
+        ########################################
+        ## START - generated method body
+        
+        write('''
+
+
+''')
+        _v = VFFSL(SL,"full_body",True) # '$full_body' on line 32, col 1
+        if _v is not None: write(_filter(_v, rawExpr='$full_body')) # from line 32, col 1.
+        
+        ########################################
+        ## END - generated method body
+        
+        return _dummyTrans and trans.response().getvalue() or ""
+        
+    ##################################################
+    ## CHEETAH GENERATED ATTRIBUTES
+
+
+    _CHEETAH__instanceInitialized = False
+
+    _CHEETAH_version = __CHEETAH_version__
+
+    _CHEETAH_versionTuple = __CHEETAH_versionTuple__
+
+    _CHEETAH_genTime = __CHEETAH_genTime__
+
+    _CHEETAH_genTimestamp = __CHEETAH_genTimestamp__
+
+    _CHEETAH_src = __CHEETAH_src__
+
+    _CHEETAH_srcLastModified = __CHEETAH_srcLastModified__
+
+    _mainCheetahMethod_for_functions= 'respond'
+
+## END CLASS DEFINITION
+
+if not hasattr(functions, '_initCheetahAttributes'):
+    templateAPIClass = getattr(functions, '_CHEETAH_templateClass', Template)
+    templateAPIClass._addCheetahPlumbingCodeToClass(functions)
+
+
+# CHEETAH was developed by Tavis Rudd and Mike Orr
+# with code, advice and input from many other volunteers.
+# For more information visit http://www.CheetahTemplate.org/
+
+##################################################
+## if run from command line:
+if __name__ == '__main__':
+    from Cheetah.TemplateCmdLineIface import CmdLineIface
+    CmdLineIface(templateObj=functions()).run()
+
+
diff --git a/templates/functions.tmpl b/templates/functions.tmpl
new file mode 100644 (file)
index 0000000..213b639
--- /dev/null
@@ -0,0 +1,32 @@
+#def cdromList($cdroms, $default="")
+<select name="cdrom">
+  <option #slurp
+#if $default then '' else 'selected'
+ value="">None</option>
+  #for $cdrom in $cdroms
+  <option #slurp
+#if $default == $cdrom.cdrom_id then 'selected' else ''
+ value="$cdrom.cdrom_id">
+    $cdrom.description
+  </option>
+  #end for
+</select>
+#end def
+
+#def addError(txt)
+#if $varExists('txt')
+#set global $error_text = $error_text + '----\n' + $txt
+#else
+#set global $error_text = $txt
+#end if
+#end def
+
+#def errorRow($value, $err)
+#if $err and $err.err_field == $value
+<tr>
+<td class="error" colspan="2">${str($err)}</td>
+</tr>
+#end if
+#end def
+
+$full_body
\ No newline at end of file
index 6ac0227..29b5907 100644 (file)
@@ -5,17 +5,27 @@
 Info on $machine.name
 #end def
 
 Info on $machine.name
 #end def
 
-
-#def body
-<h1>Info</h1>
+#def infoTable()
 <p>Info on ${machine.name}:</p>
 <table>
   #for $key, $value in $fields
   <tr><td>$key:</td><td>$value</td></tr>
   #end for
 </table>
 <p>Info on ${machine.name}:</p>
 <table>
   #for $key, $value in $fields
   <tr><td>$key:</td><td>$value</td></tr>
   #end for
 </table>
+#end def
 
 
-<p>Commands:</p>
+#def commands()
+<script>
+function commandButton(elt, action){
+  form = elt.up('', 4);
+  cdrom = Form.serialize(form, true).cdrom;
+  new Ajax.Request('command', {method: 'post', 
+  parameters: 'machine_id=$machine.machine_id&js=info&cdrom='+cdrom+'&action='+action,
+  onSuccess: replaceFunc
+  });
+  return false;
+}
+</script>
 <form action="command" action="POST">
   <input type="hidden" name="machine_id" value="$machine.machine_id"/>
   <table>
 <form action="command" action="POST">
   <input type="hidden" name="machine_id" value="$machine.machine_id"/>
   <table>
@@ -33,48 +43,74 @@ Info on $machine.name
     </td></tr>
       <tr>
        #if $on
     </td></tr>
       <tr>
        #if $on
-       <td><input type="submit" class="button" name="action" value="Power off"/></td>
-       <td><input type="submit" class="button" name="action" value="Shutdown"/></td>
-       <td><input type="submit" class="button" name="action" value="Reboot"/></td>
+       <td><input type="submit" class="button" name="action" value="Power off" onclick="return commandButton(this, 'Power off');"/></td>
+       <td><input type="submit" class="button" name="action" value="Shutdown" onclick="return commandButton(this, 'Shutdown');"/></td>
+       <td><input type="submit" class="button" name="action" value="Reboot" onclick="return commandButton(this, 'Reboot');"/></td>
        #else
        #else
-       <td><input type="submit" class="button" name="action" value="Power on"/></td>
+       <td><input type="submit" class="button" name="action" value="Power on" onclick="return commandButton(this, 'Power on');"/></td>
        #end if
       <td>Boot CD:</td>
        #end if
       <td>Boot CD:</td>
-      <td><select name="cdrom">
-         <option selected value="">None</option>
-         #for $cdrom in $cdroms
-         <option value="$cdrom.cdrom_id">
-           $cdrom.description
-         </option>
-         #end for
-      </select></td>
+      <td>$cdromList($cdroms)</td>
   </tr>
     <tr>
   </tr>
     <tr>
-      <td><input type="submit" class="button" name="action" value="Delete VM"/></td>
+      <td><input type="submit" class="button" name="action" value="Delete VM" onclick="return confirm('Are you sure that you want to delete this VM?');"/></td>
     </tr>
   </table>
 </form>
     </tr>
   </table>
 </form>
-<p>Change settings:
+#end def
+
+#def modifyForm()
+#if $err
+<p class="error">We had a problem with your request:</p>
+#else if $varExists('new_machine')
+<p>Successfully modified.</p>
+#end if
 #if $on
 (To edit ram, disk size, or machine name, turn off the machine first.)
 #end if
 </p>
 #if $on
 (To edit ram, disk size, or machine name, turn off the machine first.)
 #end if
 </p>
-<form action="modify" method="POST">
-  <input type="hidden" name="machine_id" value="$machine.machine_id"/>
+<form action="modify" method="POST" onsubmit="return jsFormSubmit('modify', this);">
+  <input type="hidden" name="machine_id" value="$defaults.machine_id"/>
   <table>
   <table>
-    <tr><td>Owner${helppopup("owner")}:</td><td><input type="text" name="owner", value="$machine.owner"/></td></tr>
-    <tr><td>Administrator${helppopup("administrator")}:</td><td><input type="text" name="administrator", value="$machine.administrator"/></td></tr>
-    <tr><td>Contact email:</td><td><input type="text" name="contact" value="$machine.contact"/></td></tr>
+    <tr><td>Owner${helppopup("owner")}:</td><td><input type="text" name="owner", value="$defaults.owner"/></td></tr>
+$errorRow('owner', $err)
+    <tr><td>Administrator${helppopup("administrator")}:</td><td><input type="text" name="administrator", value="$defaults.administrator"/></td></tr>
+$errorRow('administrator', $err)
+    <tr><td>Contact email:</td><td><input type="text" name="contact" value="$defaults.contact"/></td></tr>
+$errorRow('contact', $err)
 #if $machine.nics
 #if $machine.nics
-    <tr><td>Hostname:</td><td><input type="text" name="hostname" value="$machine.nics[0].hostname"/>.servers.csail.mit.edu</td></tr>
+    <tr><td>Hostname:</td><td><input type="text" name="hostname" value="$defaults.hostname"/>.servers.csail.mit.edu</td></tr>
 #end if
 #end if
+$errorRow('hostname', $err)
 #if not $on
 #if not $on
-    <tr><td>Machine Name:</td><td><input type="text" name="name" value="$machine.name"/></td></tr>
-    <tr><td>Ram:</td><td><input type="text" size=3 name="memory" value="$machine.memory"/>MB (max $max_mem)</td></tr>
-    <tr><td>Disk:</td><td><input type="text" size=3 name="disk" value="${machine.disks[0].size/1024.}"/>GB (max $max_disk)</td><td>WARNING: Modifying disk size may corrupt your data.</td></tr>
+    <tr><td>Machine Name:</td><td><input type="text" name="name" value="$defaults.name"/></td></tr>
+$errorRow('name', $err)
+    <tr><td>Ram:</td><td><input type="text" size=3 name="memory" value="$defaults.memory"/>MB (max $max_mem)</td></tr>
+$errorRow('memory', $err)
+    <tr><td>Disk:</td><td><input type="text" size=3 name="disk" value="$defaults.disk"/>GB (max $max_disk)</td><td>WARNING: Modifying disk size may corrupt your data.</td></tr>
+$errorRow('disk', $err)
+#else
+$errorRow('name', $err)
+$errorRow('memory', $err)
+$errorRow('disk', $err)
 #end if
     <tr><td><input type="submit" class="button" name="action" value="Change"/></td></tr>
   </table>
 </form>
 #end if
     <tr><td><input type="submit" class="button" name="action" value="Change"/></td></tr>
   </table>
 </form>
+#end def
+
+#def body
+<h1>Info</h1>
+<div id="info">
+  $infoTable()
+</div>
 
 
+<p>Commands:</p>
+<div id="commands">
+  $commands()
+</div>
+<p>Change settings:
+<div id="modify">
+  $modifyForm()
+</div>
 #end def
 #end def
index 05b5c2c..68f0858 100644 (file)
@@ -1,26 +1,62 @@
 #from skeleton import skeleton
 #extends skeleton
 
 #from skeleton import skeleton
 #extends skeleton
 
+
 #def title
 List of your VMs
 #end def
 
 #def title
 List of your VMs
 #end def
 
-#def body
-#if not $machines
-   
+#def createTable()
+#if $cant_add_vm
+<p>$cant_add_vm</p>
 #else
 #else
-    <p>You have the following VMs:</p>
-    <table>
-      <tr>
-       <td>Name</td>
-       <td>Memory</td>
-       <td>owner</td>
-       <td>IP</td>
-       <td>Hostname</td>
-       <td>Uptime</td>
-       <td>VNC</td>
-       <td></td>
-      #for $machine in $machines:
+<p>Create a new VM:</p>
+#if $err
+<p class="error">We had a problem with your request:</p>
+#else if $varExists('new_machine')
+<p>Congratulations! You successfully created a new VM called $new_machine.</p>
+#end if
+    <form action="create" method="POST" onsubmit="return jsFormSubmit('create', this);">
+      <table>
+      $errorRow('create', $err)
+       <tr>
+         <td>Name</td>
+         <td><input type="text" name="name" value="$defaults.name"/></td>
+       </tr>
+$errorRow('name', $err)
+       <tr>
+         <td>Memory</td>
+         <td><input type="text" name="memory" value="$defaults.memory" size=3/> megabytes ($max_memory max)</td>
+       </tr>
+$errorRow('memory', $err)
+       <tr>
+         <td>Disk</td>
+         <td><input type="text" name="disk" value="$defaults.disk" size=3/> gigabytes (${"%0.1f" % ($max_disk-0.05)} max)</td>
+       </tr>
+$errorRow('disk', $err)
+       <tr>
+         <td>HVM/ParaVM$helppopup('hvm_paravm')</td>
+         <td>
+#for $value, $name in (('hvm', 'HVM'), ('paravm', 'ParaVM'))
+   <input #slurp
+#if $defaults.vmtype == $value then 'checked' else ''
+ type="radio" name="vmtype" value="$value">$name</input>
+#end for
+         </td>
+       </tr>
+$errorRow('vmtype', $err)
+       <tr>
+         <td>Boot CD</td>
+         <td>$cdromList($cdroms, $defaults.cdrom)</td>
+       </tr>
+$errorRow('cdrom', $err)
+      </table>
+      <input type="submit" class="button" value="Create it!"/>
+    </form>
+#end if
+#end def
+
+#def machineRow($machine)
       <tr> 
        <td><a href="info?machine_id=$machine.machine_id">$machine.name</a></td>
        <td>${machine.memory}M</td>
       <tr> 
        <td><a href="info?machine_id=$machine.machine_id">$machine.name</a></td>
        <td>${machine.memory}M</td>
@@ -34,8 +70,8 @@ List of your VMs
        <td></td>
 #end if
 <td>#slurp
        <td></td>
 #end if
 <td>#slurp
-#if $uptimes[$machine]
-$uptimes[$machine]#slurp
+#if $machine.uptime
+$machine.uptime#slurp
 #else
 Off#slurp
 #end if
 #else
 Off#slurp
 #end if
@@ -48,60 +84,46 @@ $has_vnc[$machine]
 #end if
 </td>
        <td>
 #end if
 </td>
        <td>
-         <form action="command">
+         <form action="command" method="post" onsubmit="return rowFormSubmit(this, 'list');">
            <input type="hidden" name="machine_id"
                   value="$machine.machine_id"/>
            <input type="hidden" name="machine_id"
                   value="$machine.machine_id"/>
-#if $uptimes[$machine]
-           <input type="submit" class="button" name="action" value="Shutdown"/>
-#else
-           <input type="submit" class="button" name="action" value="Power on"/>
-#end if
+<input type="submit" class="button" name="action" value="#slurp
+#if $machine.uptime then 'Shutdown' else 'Power on'
+"/>
          </form>
        </td>
       </tr>
          </form>
        </td>
       </tr>
+#end def
+
+#def machineList($machines)
+    <table>
+      <tr>
+       <td>Name</td>
+       <td>Memory</td>
+       <td>owner</td>
+       <td>IP</td>
+       <td>Hostname</td>
+       <td>Uptime</td>
+       <td>VNC</td>
+       <td></td>
+      #for $machine in $machines:
+       $machineRow($machine)
       #end for
     </table>
       #end for
     </table>
-#end if
-#if $can_add_vm
-    <p>Create a new VM:</p>
-    <form action="create" method="POST">
-      <table>
-       <tr>
-         <td>Name</td>
-         <td><input type="text" name="name" value=""/></td>
-       </tr>
-       <tr>
-         <td>Memory</td>
-         <td><input type="text" name="memory" value="$default_mem" size=3/> megabytes ($max_mem max)</td>
-       </tr>
-       <tr>
-         <td>Disk</td>
-         <td><input type="text" name="disk" value="$default_disk" size=3/> gigabytes (${"%0.1f" % ($max_disk-0.05)} max)</td>
-       </tr>
-       <tr>
-         <td>HVM/ParaVM$helppopup('hvm_paravm')</td>
-         <td>
-           <input checked type="radio" name="vmtype" value="hvm">HVM</input>
-           <input type="radio" name="vmtype" value="paravm">ParaVM</input>
-         </td>
-       </tr>
-       <tr>
-         <td>Boot CD</td>
-         <td>
-           <select name="cdrom">
-             <option selected value="">None</option>
-             #for $cdrom in $cdroms
-             <option value="$cdrom.cdrom_id">
-               $cdrom.description
-             </option>
-             #end for
-           </select>
-         </td>
-       </tr>
-      </table>
-      <input type="submit" class="button" value="Create it!"/>
-    </form>
+#end def
+
+
+#def body
+#if not $machines
+<p>You don't currently control any VMs.</p>   
 #else
 #else
-<p>You are at the maximum number of VMs.</p>
+    <p>You have the following VMs:</p>
 #end if
 #end if
+    <p><a href="list" onclick="new Ajax.Updater('machinelist', 'list?js=machinelist', {method: 'get' });return false">refresh</a></p>
+    <div id="machinelist">
+    $machineList($machines)
+    </div>
+<div id="createtable">
+$createTable()
+</div>
 #end def
 #end def
index 915b5eb..6ec853d 100755 (executable)
@@ -1,27 +1,47 @@
 #!/usr/bin/python
 #!/usr/bin/python
+"""Main CGI script for web interface"""
 
 
-import sys
+import base64
+import cPickle
 import cgi
 import cgi
+import datetime
+import getafsgroups
+import hmac
 import os
 import os
+import random
+import re
+import sha
+import simplejson
 import string
 import subprocess
 import string
 import subprocess
-import re
+import sys
 import time
 import time
-import cPickle
-import base64
-import sha
-import hmac
-import datetime
-import StringIO
-import getafsgroups
+from StringIO import StringIO
+
+
+def revertStandardError():
+    """Move stderr to stdout, and return the contents of the old stderr."""
+    errio = sys.stderr
+    if not isinstance(errio, StringIO):
+        return None
+    sys.stderr = sys.stdout
+    errio.seek(0)
+    return errio.read()
+
+def printError():
+    """Revert stderr to stdout, and print the contents of stderr"""
+    if isinstance(sys.stderr, StringIO):
+        print revertStandardError()
+
+if __name__ == '__main__':
+    import atexit
+    atexit.register(printError)
+    sys.stderr = StringIO()
 
 
-errio = StringIO.StringIO()
-sys.stderr = errio
 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
 
 from Cheetah.Template import Template
 from sipb_xen_database import *
 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
 
 from Cheetah.Template import Template
 from sipb_xen_database import *
-import random
 
 class MyException(Exception):
     """Base class for my exceptions"""
 
 class MyException(Exception):
     """Base class for my exceptions"""
@@ -43,7 +63,14 @@ class CodeError(MyException):
     """Exception for internal errors or bad faith input."""
     pass
 
     """Exception for internal errors or bad faith input."""
     pass
 
+def helppopup(subj):
+    """Return HTML code for a (?) link to a specified help topic"""
+    return ('<span class="helplink"><a href="help?subject=' + subj + 
+            '&amp;simple=true" target="_blank" ' + 
+            'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
+
 class Global(object):
 class Global(object):
+    """Global state of the system, to avoid duplicate remctls to get state"""
     def __init__(self, user):
         self.user = user
 
     def __init__(self, user):
         self.user = user
 
@@ -53,16 +80,66 @@ class Global(object):
         return self._uptimes
     uptimes = property(__get_uptimes)
 
         return self._uptimes
     uptimes = property(__get_uptimes)
 
-g = None
+    def clear(self):
+        """Clear the state so future accesses reload it."""
+        for attr in ('_uptimes', ):
+            if hasattr(self, attr):
+                delattr(self, attr)
 
 
-def helppopup(subj):
-    """Return HTML code for a (?) link to a specified help topic"""
-    return '<span class="helplink"><a href="help?subject='+subj+'&amp;simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
-
-
-global_dict = {}
-global_dict['helppopup'] = helppopup
+g = None
 
 
+class User:
+    """User class (sort of useless, I admit)"""
+    def __init__(self, username, email):
+        self.username = username
+        self.email = email
+
+def makeErrorPre(old, addition):
+    if addition is None:
+        return
+    if old:
+        return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
+    else:
+        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
+
+Template.helppopup = staticmethod(helppopup)
+Template.err = None
+
+class JsonDict:
+    """Class to store a dictionary that will be converted to JSON"""
+    def __init__(self, **kws):
+        self.data = kws
+        if 'err' in kws:
+            err = kws['err']
+            del kws['err']
+            self.addError(err)
+
+    def __str__(self):
+        return simplejson.dumps(self.data)
+
+    def addError(self, text):
+        """Add stderr text to be displayed on the website."""
+        self.data['err'] = \
+            makeErrorPre(self.data.get('err'), text)
+
+class Defaults:
+    """Class to store default values for fields."""
+    memory = 256
+    disk = 4.0
+    cdrom = ''
+    name = ''
+    vmtype = 'hvm'
+    def __init__(self, max_memory=None, max_disk=None, **kws):
+        if max_memory is not None:
+            self.memory = min(self.memory, max_memory)
+        if max_disk is not None:
+            self.max_disk = min(self.disk, max_disk)
+        for key in kws:
+            setattr(self, key, kws[key])
+
+
+
+default_headers = {'Content-Type': 'text/html'}
 
 # ... and stolen from xend/uuid.py
 def randomUUID():
 
 # ... and stolen from xend/uuid.py
 def randomUUID():
@@ -119,11 +196,15 @@ def maxDisk(user, machine=None):
                       for x in machines if x != machine])
     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
 
                       for x in machines if x != machine])
     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
 
-def canAddVm(user):
+def cantAddVm(user):
     machines = getMachinesByOwner(user)
     active_machines = [x for x in machines if g.uptimes[x]]
     machines = getMachinesByOwner(user)
     active_machines = [x for x in machines if g.uptimes[x]]
-    return (len(machines) < MAX_VMS_TOTAL and
-            len(active_machines) < MAX_VMS_ACTIVE)
+    if len(machines) >= MAX_VMS_TOTAL:
+        return 'You have too many VMs to create a new one.'
+    if len(active_machines) >= MAX_VMS_ACTIVE:
+        return ('You already have the maximum number of VMs turned on.  '
+                'To create more, turn one off.')
+    return False
 
 def haveAccess(user, machine):
     """Return whether a user has adminstrative access to a machine"""
 
 def haveAccess(user, machine):
     """Return whether a user has adminstrative access to a machine"""
@@ -131,7 +212,8 @@ def haveAccess(user, machine):
         return True
     if user.username in (machine.administrator, machine.owner):
         return True
         return True
     if user.username in (machine.administrator, machine.owner):
         return True
-    if getafsgroups.checkAfsGroup(user.username, machine.administrator, 'athena.mit.edu'): #XXX Cell?
+    if getafsgroups.checkAfsGroup(user.username, machine.administrator, 
+                                  'athena.mit.edu'): #XXX Cell?
         return True
     if getafsgroups.checkLockerOwner(user.username, machine.owner):
         return True
         return True
     if getafsgroups.checkLockerOwner(user.username, machine.owner):
         return True
@@ -147,14 +229,14 @@ def error(op, user, fields, err, emsg):
     """Print an error page when a CodeError occurs"""
     d = dict(op=op, user=user, errorMessage=str(err),
              stderr=emsg)
     """Print an error page when a CodeError occurs"""
     d = dict(op=op, user=user, errorMessage=str(err),
              stderr=emsg)
-    return Template(file='error.tmpl', searchList=[d, global_dict]);
+    return Template(file='error.tmpl', searchList=[d]);
 
 def invalidInput(op, user, fields, err, emsg):
     """Print an error page when an InvalidInput exception occurs"""
     d = dict(op=op, user=user, err_field=err.err_field,
              err_value=str(err.err_value), stderr=emsg,
              errorMessage=str(err))
 
 def invalidInput(op, user, fields, err, emsg):
     """Print an error page when an InvalidInput exception occurs"""
     d = dict(op=op, user=user, err_field=err.err_field,
              err_value=str(err.err_value), stderr=emsg,
              errorMessage=str(err))
-    return Template(file='invalid.tmpl', searchList=[d, global_dict]);
+    return Template(file='invalid.tmpl', searchList=[d]);
 
 def validMachineName(name):
     """Check that name is valid for a machine name"""
 
 def validMachineName(name):
     """Check that name is valid for a machine name"""
@@ -193,11 +275,11 @@ def remctl(*args, **kws):
                          + list(args),
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE)
                          + list(args),
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE)
+    v = p.wait()
     if kws.get('err'):
     if kws.get('err'):
-        p.wait()
         return p.stdout.read(), p.stderr.read()
         return p.stdout.read(), p.stderr.read()
-    if p.wait():
-        print >> sys.stderr, 'Error on remctl', args, ':'
+    if v:
+        print >> sys.stderr, 'Error', v, 'on remctl', args, ':'
         print >> sys.stderr, p.stderr.read()
         raise CodeError('ERROR on remctl')
     return p.stdout.read()
         print >> sys.stderr, p.stderr.read()
         raise CodeError('ERROR on remctl')
     return p.stdout.read()
@@ -276,13 +358,16 @@ def statusInfo(machine):
 
     Gets and parses xm list --long
     """
 
     Gets and parses xm list --long
     """
-    value_string, err_string = remctl('control', machine.name, 'list-long', err=True)
+    value_string, err_string = remctl('control', machine.name, 'list-long', 
+                                      err=True)
     if 'Unknown command' in err_string:
     if 'Unknown command' in err_string:
-        raise CodeError("ERROR in remctl list-long %s is not registered" % (machine.name,))
+        raise CodeError("ERROR in remctl list-long %s is not registered" % 
+                        (machine.name,))
     elif 'does not exist' in err_string:
         return None
     elif err_string:
     elif 'does not exist' in err_string:
         return None
     elif err_string:
-        raise CodeError("ERROR in remctl list-long %s:  %s" % (machine.name, err_string))
+        raise CodeError("ERROR in remctl list-long %s:  %s" % 
+                        (machine.name, err_string))
     status = parseStatus(value_string)
     return status
 
     status = parseStatus(value_string)
     return status
 
@@ -307,9 +392,11 @@ def createVm(user, name, memory, disk, is_hvm, cdrom):
         if disk > maxDisk(user) * 1024:
             raise InvalidInput('disk', disk,
                                "Max %s" % maxDisk(user))
         if disk > maxDisk(user) * 1024:
             raise InvalidInput('disk', disk,
                                "Max %s" % maxDisk(user))
-        if not canAddVm(user):
-            raise InvalidInput('create', True, 'Unable to create more VMs')
-        res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
+        reason = cantAddVm(user)
+        if reason:
+            raise InvalidInput('create', True, reason)
+        res = meta.engine.execute('select nextval('
+                                  '\'"machines_machine_id_seq"\')')
         id = res.fetchone()[0]
         machine = Machine()
         machine.machine_id = id
         id = res.fetchone()[0]
         machine = Machine()
         machine.machine_id = id
@@ -325,10 +412,11 @@ def createVm(user, name, memory, disk, is_hvm, cdrom):
         ctx.current.save(machine)
         disk = Disk(machine.machine_id, 
                     'hda', disk)
         ctx.current.save(machine)
         disk = Disk(machine.machine_id, 
                     'hda', disk)
-        open = NIC.select_by(machine_id=None)
-        if not open: #No IPs left!
-            raise CodeError("No IP addresses left!  Contact sipb-xen-dev@mit.edu")
-        nic = open[0]
+        open_nics = NIC.select_by(machine_id=None)
+        if not open_nics: #No IPs left!
+            raise CodeError("No IP addresses left!  "
+                            "Contact sipb-xen-dev@mit.edu")
+        nic = open_nics[0]
         nic.machine_id = machine.machine_id
         nic.hostname = name
         ctx.current.save(nic)    
         nic.machine_id = machine.machine_id
         nic.hostname = name
         ctx.current.save(nic)    
@@ -377,16 +465,15 @@ def validDisk(user, disk, machine=None):
                            "Minimum %s GB" % MIN_DISK_SINGLE)
     return disk
 
                            "Minimum %s GB" % MIN_DISK_SINGLE)
     return disk
 
-def create(user, fields):
-    """Handler for create requests."""
+def parseCreate(user, fields):
     name = fields.getfirst('name')
     if not validMachineName(name):
     name = fields.getfirst('name')
     if not validMachineName(name):
-        raise InvalidInput('name', name)
+        raise InvalidInput('name', name, 'You must provide a machine name.')
     name = name.lower()
 
     if Machine.get_by(name=name):
         raise InvalidInput('name', name,
     name = name.lower()
 
     if Machine.get_by(name=name):
         raise InvalidInput('name', name,
-                           "Already exists")
+                           "Name already exists.")
     
     memory = fields.getfirst('memory')
     memory = validMemory(user, memory, on=True)
     
     memory = fields.getfirst('memory')
     memory = validMemory(user, memory, on=True)
@@ -401,21 +488,45 @@ def create(user, fields):
 
     cdrom = fields.getfirst('cdrom')
     if cdrom is not None and not CDROM.get(cdrom):
 
     cdrom = fields.getfirst('cdrom')
     if cdrom is not None and not CDROM.get(cdrom):
-        raise CodeError("Invalid cdrom type '%s'" % cdrom)    
-    
-    machine = createVm(user, name, memory, disk, is_hvm, cdrom)
-    d = dict(user=user,
-             machine=machine)
-    return Template(file='create.tmpl',
-                   searchList=[d, global_dict]);
+        raise CodeError("Invalid cdrom type '%s'" % cdrom)
+    return dict(user=user, name=name, memory=memory, disk=disk,
+                is_hvm=is_hvm, cdrom=cdrom)
 
 
-def listVms(user, fields):
-    """Handler for list requests."""
+def create(user, fields):
+    """Handler for create requests."""
+    js = fields.getfirst('js')
+    try:
+        parsed_fields = parseCreate(user, fields)
+        machine = createVm(**parsed_fields)
+    except InvalidInput, err:
+        if not js:
+            raise
+    else:
+        err = None
+        if not js:
+            d = dict(user=user,
+                     machine=machine)
+            return Template(file='create.tmpl', searchList=[d])
+    g.clear() #Changed global state
+    d = getListDict(user)
+    d['err'] = err
+    if err:
+        for field in fields.keys():
+            setattr(d['defaults'], field, fields.getfirst(field))
+    else:
+        d['new_machine'] = parsed_fields['name']
+    t = Template(file='list.tmpl', searchList=[d])
+    return JsonDict(createtable=t.createTable(),
+                    machinelist=t.machineList(d['machines']))
+
+
+def getListDict(user):
     machines = [m for m in Machine.select() if haveAccess(user, m)]    
     on = {}
     has_vnc = {}
     on = g.uptimes
     for m in machines:
     machines = [m for m in Machine.select() if haveAccess(user, m)]    
     on = {}
     has_vnc = {}
     on = g.uptimes
     for m in machines:
+        m.uptime = g.uptimes.get(m)
         if not on[m]:
             has_vnc[m] = 'Off'
         elif m.type.hvm:
         if not on[m]:
             has_vnc[m] = 'Off'
         elif m.type.hvm:
@@ -426,20 +537,38 @@ def listVms(user, fields):
     #         status = statusInfo(m)
     #         on[m.name] = status is not None
     #         has_vnc[m.name] = hasVnc(status)
     #         status = statusInfo(m)
     #         on[m.name] = status is not None
     #         has_vnc[m.name] = hasVnc(status)
-    max_mem=maxMemory(user)
-    max_disk=maxDisk(user)
+    max_memory = maxMemory(user)
+    max_disk = maxDisk(user)
+    defaults = Defaults(max_memory=max_memory,
+                        max_disk=max_disk,
+                        cdrom='gutsy-i386')
     d = dict(user=user,
     d = dict(user=user,
-             can_add_vm=canAddVm(user),
-             max_mem=max_mem,
+             cant_add_vm=cantAddVm(user),
+             max_memory=max_memory,
              max_disk=max_disk,
              max_disk=max_disk,
-             default_mem=max_mem,
-             default_disk=min(4.0, max_disk),
+             defaults=defaults,
              machines=machines,
              has_vnc=has_vnc,
              uptimes=g.uptimes,
              cdroms=CDROM.select())
              machines=machines,
              has_vnc=has_vnc,
              uptimes=g.uptimes,
              cdroms=CDROM.select())
-    return Template(file='list.tmpl', searchList=[d, global_dict])
+    return d
 
 
+def listVms(user, fields):
+    """Handler for list requests."""
+    d = getListDict(user)
+    t = Template(file='list.tmpl', searchList=[d])
+    js = fields.getfirst('js')
+    if not js:
+        return t
+    if js == 'machinelist':
+        return t.machineList(d['machines'])
+    elif js.startswith('machinerow-'):
+        request_machine_id = int(js.split('-')[1])
+        m = [x for x in d['machines'] if x.id == request_machine_id]
+        return t.machineRow(m)
+    elif js == 'createtable':
+        return t.createTable()
+            
 def testMachineId(user, machineId, exists=True):
     """Parse, validate and check authorization for a given machineId.
 
 def testMachineId(user, machineId, exists=True):
     """Parse, validate and check authorization for a given machineId.
 
@@ -468,9 +597,12 @@ def vnc(user, fields):
 
     You might want iptables like:
 
 
     You might want iptables like:
 
-    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 
-    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp --dport 10003 -j SNAT --to-source 18.187.7.142 
-    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
+    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
+      --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 
+    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
+      --dport 10003 -j SNAT --to-source 18.187.7.142 
+    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
+      --dport 10003 -j ACCEPT
 
     Remember to enable iptables!
     echo 1 > /proc/sys/net/ipv4/ip_forward
 
     Remember to enable iptables!
     echo 1 > /proc/sys/net/ipv4/ip_forward
@@ -481,12 +613,12 @@ def vnc(user, fields):
 
     data = {}
     data["user"] = user.username
 
     data = {}
     data["user"] = user.username
-    data["machine"]=machine.name
-    data["expires"]=time.time()+(5*60)
-    pickledData = cPickle.dumps(data)
+    data["machine"] = machine.name
+    data["expires"] = time.time()+(5*60)
+    pickled_data = cPickle.dumps(data)
     m = hmac.new(TOKEN_KEY, digestmod=sha)
     m = hmac.new(TOKEN_KEY, digestmod=sha)
-    m.update(pickledData)
-    token = {'data': pickledData, 'digest': m.digest()}
+    m.update(pickled_data)
+    token = {'data': pickled_data, 'digest': m.digest()}
     token = cPickle.dumps(token)
     token = base64.urlsafe_b64encode(token)
     
     token = cPickle.dumps(token)
     token = base64.urlsafe_b64encode(token)
     
@@ -499,8 +631,7 @@ def vnc(user, fields):
              machine=machine,
              hostname=os.environ.get('SERVER_NAME', 'localhost'),
              authtoken=token)
              machine=machine,
              hostname=os.environ.get('SERVER_NAME', 'localhost'),
              authtoken=token)
-    return Template(file='vnc.tmpl',
-                   searchList=[d, global_dict])
+    return Template(file='vnc.tmpl', searchList=[d])
 
 def getNicInfo(data_dict, machine):
     """Helper function for info, get data on nics for a machine.
 
 def getNicInfo(data_dict, machine):
     """Helper function for info, get data on nics for a machine.
@@ -516,7 +647,8 @@ def getNicInfo(data_dict, machine):
     nic_fields = []
     for i in range(len(machine.nics)):
         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
     nic_fields = []
     for i in range(len(machine.nics)):
         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
-        data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
+        data_dict['nic%s_hostname' % i] = (machine.nics[i].hostname + 
+                                           '.servers.csail.mit.edu')
         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
         data_dict['nic%s_ip' % i] = machine.nics[i].ip
     if len(machine.nics) == 1:
         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
         data_dict['nic%s_ip' % i] = machine.nics[i].ip
     if len(machine.nics) == 1:
@@ -534,7 +666,8 @@ def getDiskInfo(data_dict, machine):
     disk_fields = []
     for disk in machine.disks:
         name = disk.guest_device_name
     disk_fields = []
     for disk in machine.disks:
         name = disk.guest_device_name
-        disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
+        disk_fields.extend([(x % name, y % name) for x, y in 
+                            disk_fields_template])
         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
     return disk_fields
 
         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
     return disk_fields
 
@@ -542,7 +675,8 @@ def deleteVM(machine):
     """Delete a VM."""
     remctl('control', machine.name, 'destroy', err=True)
     transaction = ctx.current.create_transaction()
     """Delete a VM."""
     remctl('control', machine.name, 'destroy', err=True)
     transaction = ctx.current.create_transaction()
-    delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
+    delete_disk_pairs = [(machine.name, d.guest_device_name) 
+                         for d in machine.disks]
     try:
         for nic in machine.nics:
             nic.machine_id = None
     try:
         for nic in machine.nics:
             nic.machine_id = None
@@ -559,8 +693,7 @@ def deleteVM(machine):
         remctl('web', 'lvremove', mname, dname)
     unregisterMachine(machine)
 
         remctl('web', 'lvremove', mname, dname)
     unregisterMachine(machine)
 
-def command(user, fields):
-    """Handler for running commands like boot and delete on a VM."""
+def commandResult(user, fields):
     print >> sys.stderr, time.time()-start_time
     machine = testMachineId(user, fields.getfirst('machine_id'))
     action = fields.getfirst('action')
     print >> sys.stderr, time.time()-start_time
     machine = testMachineId(user, fields.getfirst('machine_id'))
     action = fields.getfirst('action')
@@ -568,22 +701,51 @@ def command(user, fields):
     print >> sys.stderr, time.time()-start_time
     if cdrom is not None and not CDROM.get(cdrom):
         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
     print >> sys.stderr, time.time()-start_time
     if cdrom is not None and not CDROM.get(cdrom):
         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
-    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
+    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 
+                      'Delete VM'):
         raise CodeError("Invalid action '%s'" % action)
     if action == 'Reboot':
         if cdrom is not None:
         raise CodeError("Invalid action '%s'" % action)
     if action == 'Reboot':
         if cdrom is not None:
-            remctl('control', machine.name, 'reboot', cdrom)
+            out, err = remctl('control', machine.name, 'reboot', cdrom,
+                              err=True)
         else:
         else:
-            remctl('control', machine.name, 'reboot')
+            out, err = remctl('control', machine.name, 'reboot',
+                              err=True)
+        if err:
+            if re.match("Error: Domain '.*' does not exist.", err):
+                raise InvalidInput("action", "reboot", 
+                                   "Machine is not on")
+            else:
+                print >> sys.stderr, 'Error on reboot:'
+                print >> sys.stderr, err
+                raise CodeError('ERROR on remctl')
+                
     elif action == 'Power on':
         if maxMemory(user) < machine.memory:
             raise InvalidInput('action', 'Power on',
     elif action == 'Power on':
         if maxMemory(user) < machine.memory:
             raise InvalidInput('action', 'Power on',
-                               "You don't have enough free RAM quota to turn on this machine")
+                               "You don't have enough free RAM quota "
+                               "to turn on this machine.")
         bootMachine(machine, cdrom)
     elif action == 'Power off':
         bootMachine(machine, cdrom)
     elif action == 'Power off':
-        remctl('control', machine.name, 'destroy')
+        out, err = remctl('control', machine.name, 'destroy', err=True)
+        if err:
+            if re.match("Error: Domain '.*' does not exist.", err):
+                raise InvalidInput("action", "Power off", 
+                                   "Machine is not on.")
+            else:
+                print >> sys.stderr, 'Error on power off:'
+                print >> sys.stderr, err
+                raise CodeError('ERROR on remctl')
     elif action == 'Shutdown':
     elif action == 'Shutdown':
-        remctl('control', machine.name, 'shutdown')
+        out, err = remctl('control', machine.name, 'shutdown', err=True)
+        if err:
+            if re.match("Error: Domain '.*' does not exist.", err):
+                raise InvalidInput("action", "Shutdown", 
+                                   "Machine is not on.")
+            else:
+                print >> sys.stderr, 'Error on Shutdown:'
+                print >> sys.stderr, err
+                raise CodeError('ERROR on remctl')
     elif action == 'Delete VM':
         deleteVM(machine)
     print >> sys.stderr, time.time()-start_time
     elif action == 'Delete VM':
         deleteVM(machine)
     print >> sys.stderr, time.time()-start_time
@@ -591,7 +753,41 @@ def command(user, fields):
     d = dict(user=user,
              command=action,
              machine=machine)
     d = dict(user=user,
              command=action,
              machine=machine)
-    return Template(file="command.tmpl", searchList=[d, global_dict])
+    return d
+
+def command(user, fields):
+    """Handler for running commands like boot and delete on a VM."""
+    js = fields.getfirst('js')
+    try:
+        d = commandResult(user, fields)
+    except InvalidInput, err:
+        if not js:
+            raise
+        result = None
+    else:
+        err = None
+        result = 'Success!'
+        if not js:
+            return Template(file='command.tmpl', searchList=[d])
+    if js == 'list':
+        g.clear() #Changed global state
+        d = getListDict(user)
+        t = Template(file='list.tmpl', searchList=[d])
+        return JsonDict(createtable=t.createTable(),
+                        machinelist=t.machineList(d['machines']),
+                        result=result,
+                        err=err)
+    elif js == 'info':
+        machine = testMachineId(user, fields.getfirst('machine_id'))
+        d = infoDict(user, machine)
+        t = Template(file='info.tmpl', searchList=[d])
+        return JsonDict(info=t.infoTable(),
+                        commands=t.commands(),
+                        modify=t.modifyForm(),
+                        result=result,
+                        err=err)
+    else:
+        raise InvalidInput('js', js, 'Not a known js type.')
 
 def testAdmin(user, admin, machine):
     if admin in (None, machine.administrator):
 
 def testAdmin(user, admin, machine):
     if admin in (None, machine.administrator):
@@ -600,17 +796,15 @@ def testAdmin(user, admin, machine):
         return admin
     if getafsgroups.checkAfsGroup(user.username, admin, 'athena.mit.edu'):
         return admin
         return admin
     if getafsgroups.checkAfsGroup(user.username, admin, 'athena.mit.edu'):
         return admin
-    if getafsgroups.checkAfsGroup(user.username, 'system:'+admin, 'athena.mit.edu'):
+    if getafsgroups.checkAfsGroup(user.username, 'system:'+admin,
+                                  'athena.mit.edu'):
         return 'system:'+admin
         return 'system:'+admin
-    raise InvalidInput('admin', admin, 
-                       'You must control the group you move it to')
+    raise InvalidInput('administrator', admin, 
+                       'You must control the group you move it to.')
     
 def testOwner(user, owner, machine):
     if owner in (None, machine.owner):
         return None
     
 def testOwner(user, owner, machine):
     if owner in (None, machine.owner):
         return None
-    #XXX should you be able to transfer ownership if you don't already own it?
-    #if not owns(user, machine):
-    #    raise InvalidInput('owner', owner, "You don't own this machine, so you can't  transfer ownership")
     value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
     if value == True:
         return owner
     value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
     if value == True:
         return owner
@@ -620,7 +814,7 @@ def testContact(user, contact, machine=None):
     if contact in (None, machine.contact):
         return None
     if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
     if contact in (None, machine.contact):
         return None
     if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
-        raise InvalidInput('contact', contact, "Not a valid email")
+        raise InvalidInput('contact', contact, "Not a valid email.")
     return contact
 
 def testDisk(user, disksize, machine=None):
     return contact
 
 def testDisk(user, disksize, machine=None):
@@ -631,7 +825,7 @@ def testName(user, name, machine=None):
         return None
     if not Machine.select_by(name=name):
         return name
         return None
     if not Machine.select_by(name=name):
         return name
-    raise InvalidInput('name', name, "Already taken")
+    raise InvalidInput('name', name, "Name is already taken.")
 
 def testHostname(user, hostname, machine):
     for nic in machine.nics:
 
 def testHostname(user, hostname, machine):
     for nic in machine.nics:
@@ -642,12 +836,11 @@ def testHostname(user, hostname, machine):
         raise InvalidInput('hostname', hostname,
                            "Already exists")
     if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
         raise InvalidInput('hostname', hostname,
                            "Already exists")
     if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
-        raise InvalidInput('hostname', hostname, "Not a valid hostname; must only use number, letters, and dashes.")
+        raise InvalidInput('hostname', hostname, "Not a valid hostname; "
+                           "must only use number, letters, and dashes.")
     return hostname
 
     return hostname
 
-def modify(user, fields):
-    """Handler for modifying attributes of a machine."""
-
+def modifyDict(user, fields):
     olddisk = {}
     transaction = ctx.current.create_transaction()
     try:
     olddisk = {}
     transaction = ctx.current.create_transaction()
     try:
@@ -658,7 +851,7 @@ def modify(user, fields):
         hostname = testHostname(owner, fields.getfirst('hostname'), machine)
         name = testName(user, fields.getfirst('name'), machine)
         oldname = machine.name
         hostname = testHostname(owner, fields.getfirst('hostname'), machine)
         name = testName(user, fields.getfirst('name'), machine)
         oldname = machine.name
-        command="modify"
+        command = "modify"
 
         memory = fields.getfirst('memory')
         if memory is not None:
 
         memory = fields.getfirst('memory')
         if memory is not None:
@@ -674,7 +867,8 @@ def modify(user, fields):
                 disk.size = disksize
                 ctx.current.save(disk)
         
                 disk.size = disksize
                 ctx.current.save(disk)
         
-        # XXX first NIC gets hostname on change?  Interface doesn't support more.
+        # XXX first NIC gets hostname on change?  
+        # Interface doesn't support more.
         for nic in machine.nics[:1]:
             nic.hostname = hostname
             ctx.current.save(nic)
         for nic in machine.nics[:1]:
             nic.hostname = hostname
             ctx.current.save(nic)
@@ -699,58 +893,94 @@ def modify(user, fields):
         for disk in machine.disks:
             remctl("web", "lvrename", oldname, disk.guest_device_name, name)
         remctl("web", "moveregister", oldname, name)
         for disk in machine.disks:
             remctl("web", "lvrename", oldname, disk.guest_device_name, name)
         remctl("web", "moveregister", oldname, name)
-    d = dict(user=user,
-             command=command,
-             machine=machine)
-    return Template(file="command.tmpl", searchList=[d, global_dict])    
-
+    return dict(user=user,
+                command=command,
+                machine=machine)
+    
+def modify(user, fields):
+    """Handler for modifying attributes of a machine."""
+    js = fields.getfirst('js')
+    try:
+        modify_dict = modifyDict(user, fields)
+    except InvalidInput, err:
+        if not js:
+            raise
+        result = ''
+        machine = testMachineId(user, fields.getfirst('machine_id'))
+    else:
+        machine = modify_dict['machine']
+        result='Success!'
+        err = None
+        if not js:
+            return Template(file='command.tmpl', searchList=[modify_dict])
+    info_dict = infoDict(user, machine)
+    info_dict['err'] = err
+    if err:
+        for field in fields.keys():
+            setattr(info_dict['defaults'], field, fields.getfirst(field))
+    t = Template(file='info.tmpl', searchList=[info_dict])
+    return JsonDict(info=t.infoTable(),
+                    commands=t.commands(),
+                    modify=t.modifyForm(),
+                    result=result,
+                    err=err)
+    
 
 
-def help(user, fields):
+def helpHandler(user, fields):
     """Handler for help messages."""
     simple = fields.getfirst('simple')
     subjects = fields.getlist('subject')
     
     """Handler for help messages."""
     simple = fields.getfirst('simple')
     subjects = fields.getlist('subject')
     
-    mapping = dict(paravm_console="""
+    help_mapping = dict(paravm_console="""
 ParaVM machines do not support console access over VNC.  To access
 these machines, you either need to boot with a liveCD and ssh in or
 hope that the sipb-xen maintainers add support for serial consoles.""",
 ParaVM machines do not support console access over VNC.  To access
 these machines, you either need to boot with a liveCD and ssh in or
 hope that the sipb-xen maintainers add support for serial consoles.""",
-                   hvm_paravm="""
+                        hvm_paravm="""
 HVM machines use the virtualization features of the processor, while
 ParaVM machines use Xen's emulation of virtualization features.  You
 want an HVM virtualized machine.""",
 HVM machines use the virtualization features of the processor, while
 ParaVM machines use Xen's emulation of virtualization features.  You
 want an HVM virtualized machine.""",
-                   cpu_weight="""Don't ask us!  We're as mystified as you are.""",
-                   owner="""The owner field is used to determine <a href="help?subject=quotas">quotas</a>.  It must be the name
-of a locker that you are an AFS administrator of.  In particular, you
-or an AFS group you are a member of must have AFS rlidwka bits on the
+                        cpu_weight="""
+Don't ask us!  We're as mystified as you are.""",
+                        owner="""
+The owner field is used to determine <a
+href="help?subject=quotas">quotas</a>.  It must be the name of a
+locker that you are an AFS administrator of.  In particular, you or an
+AFS group you are a member of must have AFS rlidwka bits on the
 locker.  You can check see who administers the LOCKER locker using the
 locker.  You can check see who administers the LOCKER locker using the
-command 'fs la /mit/LOCKER' on Athena.)  See also <a href="help?subject=administrator">administrator</a>.""",
-                   administrator="""The administrator field determines who can access the console and power on and off the machine.  This can be either a user or a moira group.""",
-                   quotas="""Quotas are determined on a per-locker basis.  Each 
-quota may have a maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4 active machines."""
-
+command 'fs la /mit/LOCKER' on Athena.)  See also <a
+href="help?subject=administrator">administrator</a>.""",
+                        administrator="""
+The administrator field determines who can access the console and
+power on and off the machine.  This can be either a user or a moira
+group.""",
+                        quotas="""
+Quotas are determined on a per-locker basis.  Each quota may have a
+maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
+active machines."""
                    )
     
     if not subjects:
                    )
     
     if not subjects:
-        subjects = sorted(mapping.keys())
+        subjects = sorted(help_mapping.keys())
         
     d = dict(user=user,
              simple=simple,
              subjects=subjects,
         
     d = dict(user=user,
              simple=simple,
              subjects=subjects,
-             mapping=mapping)
+             mapping=help_mapping)
     
     
-    return Template(file="help.tmpl", searchList=[d, global_dict])
+    return Template(file="help.tmpl", searchList=[d])
     
 
     
 
-def info(user, fields):
-    """Handler for info on a single VM."""
-    machine = testMachineId(user, fields.getfirst('machine_id'))
+def badOperation(u, e):
+    raise CodeError("Unknown operation")
+
+def infoDict(user, machine):
     status = statusInfo(machine)
     has_vnc = hasVnc(status)
     if status is None:
         main_status = dict(name=machine.name,
                            memory=str(machine.memory))
     status = statusInfo(machine)
     has_vnc = hasVnc(status)
     if status is None:
         main_status = dict(name=machine.name,
                            memory=str(machine.memory))
-        uptime=None
-        cputime=None
+        uptime = None
+        cputime = None
     else:
         main_status = dict(status[1:])
         start_time = float(main_status.get('start_time', 0))
     else:
         main_status = dict(status[1:])
         start_time = float(main_status.get('start_time', 0))
@@ -788,11 +1018,13 @@ def info(user, fields):
 
     nic_fields = getNicInfo(machine_info, machine)
     nic_point = display_fields.index('NIC_INFO')
 
     nic_fields = getNicInfo(machine_info, machine)
     nic_point = display_fields.index('NIC_INFO')
-    display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
+    display_fields = (display_fields[:nic_point] + nic_fields + 
+                      display_fields[nic_point+1:])
 
     disk_fields = getDiskInfo(machine_info, machine)
     disk_point = display_fields.index('DISK_INFO')
 
     disk_fields = getDiskInfo(machine_info, machine)
     disk_point = display_fields.index('DISK_INFO')
-    display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
+    display_fields = (display_fields[:disk_point] + disk_fields + 
+                      display_fields[disk_point+1:])
     
     main_status['memory'] += ' MB'
     for field, disp in display_fields:
     
     main_status['memory'] += ' MB'
     for field, disp in display_fields:
@@ -807,10 +1039,17 @@ def info(user, fields):
             #fields.append((disp, None))
     max_mem = maxMemory(user, machine)
     max_disk = maxDisk(user, machine)
             #fields.append((disp, None))
     max_mem = maxMemory(user, machine)
     max_disk = maxDisk(user, machine)
+    defaults=Defaults()
+    for name in 'machine_id name administrator owner memory contact'.split():
+        setattr(defaults, name, getattr(machine, name))
+    if machine.nics:
+        defaults.hostname = machine.nics[0].hostname
+    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
     d = dict(user=user,
              cdroms=CDROM.select(),
              on=status is not None,
              machine=machine,
     d = dict(user=user,
              cdroms=CDROM.select(),
              on=status is not None,
              machine=machine,
+             defaults=defaults,
              has_vnc=has_vnc,
              uptime=str(uptime),
              ram=machine.memory,
              has_vnc=has_vnc,
              uptime=str(uptime),
              ram=machine.memory,
@@ -818,8 +1057,13 @@ def info(user, fields):
              max_disk=max_disk,
              owner_help=helppopup("owner"),
              fields = fields)
              max_disk=max_disk,
              owner_help=helppopup("owner"),
              fields = fields)
-    return Template(file='info.tmpl',
-                   searchList=[d, global_dict])
+    return d
+
+def info(user, fields):
+    """Handler for info on a single VM."""
+    machine = testMachineId(user, fields.getfirst('machine_id'))
+    d = infoDict(user, machine)
+    return Template(file='info.tmpl', searchList=[d])
 
 mapping = dict(list=listVms,
                vnc=vnc,
 
 mapping = dict(list=listVms,
                vnc=vnc,
@@ -827,24 +1071,27 @@ mapping = dict(list=listVms,
                modify=modify,
                info=info,
                create=create,
                modify=modify,
                info=info,
                create=create,
-               help=help)
+               help=helpHandler)
+
+def printHeaders(headers):
+    for key, value in headers.iteritems():
+        print '%s: %s' % (key, value)
+    print
+
+
+def getUser():
+    """Return the current user based on the SSL environment variables"""
+    if 'SSL_CLIENT_S_DN_Email' in os.environ:
+        username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
+        return User(username, os.environ['SSL_CLIENT_S_DN_Email'])
+    else:
+        return User('moo', 'nobody')
 
 if __name__ == '__main__':
     start_time = time.time()
     fields = cgi.FieldStorage()
 
 if __name__ == '__main__':
     start_time = time.time()
     fields = cgi.FieldStorage()
-    class User:
-        username = "moo"
-        email = 'moo@cow.com'
-    u = User()
+    u = getUser()
     g = Global(u)
     g = Global(u)
-    if 'SSL_CLIENT_S_DN_Email' in os.environ:
-        username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
-        u.username = username
-        u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
-    else:
-        u.username = 'moo'
-        u.email = 'nobody'
-    connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
     operation = os.environ.get('PATH_INFO', '')
     if not operation:
         print "Status: 301 Moved Permanently"
     operation = os.environ.get('PATH_INFO', '')
     if not operation:
         print "Status: 301 Moved Permanently"
@@ -856,39 +1103,42 @@ if __name__ == '__main__':
     if not operation:
         operation = 'list'
 
     if not operation:
         operation = 'list'
 
-    def badOperation(u, e):
-        raise CodeError("Unknown operation")
+
 
     fun = mapping.get(operation, badOperation)
 
     fun = mapping.get(operation, badOperation)
-    if fun not in (help, ):
-        connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
+
+    if fun not in (helpHandler, ):
+        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
     try:
         output = fun(u, fields)
     try:
         output = fun(u, fields)
-        print 'Content-Type: text/html\n'
-        sys.stderr=sys.stdout
-        errio.seek(0)
-        e = errio.read()
+
+        headers = dict(default_headers)
+        if isinstance(output, tuple):
+            new_headers, output = output
+            headers.update(new_headers)
+
+        e = revertStandardError()
         if e:
         if e:
-            output = str(output)
-            output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
+            output.addError(e)
+        printHeaders(headers)
         print output
         print output
-    except CodeError, err:
-        print 'Content-Type: text/html\n'
-        sys.stderr=sys.stdout
-        errio.seek(0)
-        e = errio.read()
-        print error(operation, u, fields, err, e)
-    except InvalidInput, err:
-        print 'Content-Type: text/html\n'
-        sys.stderr=sys.stdout
-        errio.seek(0)
-        e = errio.read()
-        print invalidInput(operation, u, fields, err, e)
-    except:
+    except Exception, err:
+        if not fields.has_key('js'):
+            if isinstance(err, CodeError):
+                print 'Content-Type: text/html\n'
+                e = revertStandardError()
+                print error(operation, u, fields, err, e)
+                sys.exit(1)
+            if isinstance(err, InvalidInput):
+                print 'Content-Type: text/html\n'
+                e = revertStandardError()
+                print invalidInput(operation, u, fields, err, e)
+                sys.exit(1)
         print 'Content-Type: text/plain\n'
         print 'Content-Type: text/plain\n'
-        sys.stderr=sys.stdout
-        errio.seek(0)
-        e = errio.read()
+        print 'Uh-oh!  We experienced an error.'
+        print 'Please email sipb-xen@mit.edu with the contents of this page.'
+        print '----'
+        e = revertStandardError()
         print e
         print '----'
         raise
         print e
         print '----'
         raise
index e3444ec..b2643b4 100644 (file)
@@ -20,6 +20,7 @@ from Cheetah.NameMapper import NotFound, valueForName, valueFromSearchList, valu
 from Cheetah.CacheRegion import CacheRegion
 import Cheetah.Filters as Filters
 import Cheetah.ErrorCatchers as ErrorCatchers
 from Cheetah.CacheRegion import CacheRegion
 import Cheetah.Filters as Filters
 import Cheetah.ErrorCatchers as ErrorCatchers
+from functions import functions
 
 ##################################################
 ## MODULE CONSTANTS
 
 ##################################################
 ## MODULE CONSTANTS
@@ -33,10 +34,10 @@ VFN=valueForName
 currentTime=time.time
 __CHEETAH_version__ = '2.0rc8'
 __CHEETAH_versionTuple__ = (2, 0, 0, 'candidate', 8)
 currentTime=time.time
 __CHEETAH_version__ = '2.0rc8'
 __CHEETAH_versionTuple__ = (2, 0, 0, 'candidate', 8)
-__CHEETAH_genTime__ = 1192082083.444865
-__CHEETAH_genTimestamp__ = 'Thu Oct 11 01:54:43 2007'
+__CHEETAH_genTime__ = 1192883107.2947011
+__CHEETAH_genTimestamp__ = 'Sat Oct 20 08:25:07 2007'
 __CHEETAH_src__ = 'skeleton.tmpl'
 __CHEETAH_src__ = 'skeleton.tmpl'
-__CHEETAH_srcLastModified__ = 'Thu Oct 11 01:54:41 2007'
+__CHEETAH_srcLastModified__ = 'Sat Oct 20 08:22:11 2007'
 __CHEETAH_docstring__ = 'Autogenerated by CHEETAH: The Python-Powered Template Engine'
 
 if __CHEETAH_versionTuple__ < RequiredCheetahVersionTuple:
 __CHEETAH_docstring__ = 'Autogenerated by CHEETAH: The Python-Powered Template Engine'
 
 if __CHEETAH_versionTuple__ < RequiredCheetahVersionTuple:
@@ -48,7 +49,7 @@ if __CHEETAH_versionTuple__ < RequiredCheetahVersionTuple:
 ##################################################
 ## CLASSES
 
 ##################################################
 ## CLASSES
 
-class skeleton(Template):
+class skeleton(functions):
 
     ##################################################
     ## CHEETAH GENERATED METHODS
 
     ##################################################
     ## CHEETAH GENERATED METHODS
@@ -56,7 +57,7 @@ class skeleton(Template):
 
     def __init__(self, *args, **KWs):
 
 
     def __init__(self, *args, **KWs):
 
-        Template.__init__(self, *args, **KWs)
+        functions.__init__(self, *args, **KWs)
         if not self._CHEETAH__instanceInitialized:
             cheetahKWArgs = {}
             allowedKWs = 'searchList namespaces filter filtersLib errorCatcher'.split()
         if not self._CHEETAH__instanceInitialized:
             cheetahKWArgs = {}
             allowedKWs = 'searchList namespaces filter filtersLib errorCatcher'.split()
@@ -65,11 +66,12 @@ class skeleton(Template):
             self._initCheetahInstance(**cheetahKWArgs)
         
 
             self._initCheetahInstance(**cheetahKWArgs)
         
 
-    def respond(self, trans=None):
+    def full_body(self, **KWS):
 
 
 
 
 
 
-        ## CHEETAH: main method generated for this template
+        ## CHEETAH: generated from #def full_body at line 4, col 1.
+        trans = KWS.get("trans")
         if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)):
             trans = self.transaction # is None unless self.awake() was called
         if not trans:
         if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)):
             trans = self.transaction # is None unless self.awake() was called
         if not trans:
@@ -83,14 +85,18 @@ class skeleton(Template):
         ########################################
         ## START - generated method body
         
         ########################################
         ## START - generated method body
         
-        write('''<html>
+        write('''<!DOCTYPE html
+PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
 <head><title>''')
 <head><title>''')
-        _v = VFFSL(SL,"title",True) # '$title' on line 2, col 14
-        if _v is not None: write(_filter(_v, rawExpr='$title')) # from line 2, col 14.
+        _v = VFFSL(SL,"title",True) # '$title' on line 9, col 14
+        if _v is not None: write(_filter(_v, rawExpr='$title')) # from line 9, col 14.
         write('''</title>
   <link href="static/favicon.ico" type="image/x-icon" rel="shortcut icon">
   <link rel="stylesheet" href="static/style.css" type="text/css" />
   <link rel="stylesheet" href="static/layout.css" type="text/css" media="screen" />
         write('''</title>
   <link href="static/favicon.ico" type="image/x-icon" rel="shortcut icon">
   <link rel="stylesheet" href="static/style.css" type="text/css" />
   <link rel="stylesheet" href="static/layout.css" type="text/css" media="screen" />
+  <script type="text/javascript" src="static/prototype.js"></script>
   <script type="text/javascript">
 var helpWin = null;
 function closeWin(){
   <script type="text/javascript">
 var helpWin = null;
 function closeWin(){
@@ -107,34 +113,104 @@ function helppopup(name){
    if (window.focus){helpWin.focus();}
    return false;
 }
    if (window.focus){helpWin.focus();}
    return false;
 }
+
+Ajax.Responders.register({
+  onCreate: function(){
+    if (Ajax.activeRequestCount > 0) {
+       document.getElementById("loadingnotice").style.display = \'block\';
+    }
+  }, 
+  onComplete: function(){
+    if (Ajax.activeRequestCount == 0) {
+      document.getElementById("loadingnotice").style.display = \'none\';
+    } 
+  }
+});
+function replaceFunc(transport) {
+  try {
+    d = transport.responseText.evalJSON();
+  } catch (e) {
+    $(\'body\').innerHTML = \'<pre>\'+transport.responseText+\'</pre>\'
+    return;
+  }
+  for(key in d) {
+    $(key).innerHTML = d[key];
+  }
+}
+
+function jsFormSubmit(location, elt){
+  new Ajax.Request(location, {method: \'post\', 
+  parameters: Form.serialize(elt)+\'&js=true\', 
+  onSuccess: replaceFunc,
+  onComplete: function() {Form.enable(elt);}
+  });
+  Form.disable(elt);
+  return false;
+}
+
+function rowFormSubmit(elt, retpage){
+  new Ajax.Request(\'command\', {method: \'post\', 
+  parameters: Form.serialize(elt)+\'&js=\'+retpage, 
+  onSuccess: replaceFunc
+  });
+  return false;
+}
+
+window.onload = {
+  //Fix bug with disabled elements
+  $''')
+        _v = 'form' # "$('form')" on line 75, col 4
+        if _v is not None: write(_filter(_v, rawExpr="$('form')")) # from line 75, col 4.
+        write('''.each(Form.enable);
+}
+
 </script>
 </head>
 </script>
 </head>
-<body>
+<body id="body">
+
+<div id="err">
 ''')
 ''')
-        if not VFFSL(SL,"varExists",False)('simple') or not VFFSL(SL,"simple",True): # generated from line 25, col 1
+        if VFFSL(SL,"varExists",False)('error_text'): # generated from line 83, col 1
+            write('''<p>STDERR:</p><pre>''')
+            _v = VFFSL(SL,"error_text",True) # '$error_text' on line 84, col 20
+            if _v is not None: write(_filter(_v, rawExpr='$error_text')) # from line 84, col 20.
+            write('''</pre>
+''')
+        write('''</div>
+
+''')
+        if not VFFSL(SL,"varExists",False)('simple') or not VFFSL(SL,"simple",True): # generated from line 88, col 1
             write('''<p>[You are logged in as ''')
             write('''<p>[You are logged in as ''')
-            _v = VFFSL(SL,"user.username",True) # '$user.username' on line 26, col 26
-            if _v is not None: write(_filter(_v, rawExpr='$user.username')) # from line 26, col 26.
+            _v = VFFSL(SL,"user.username",True) # '$user.username' on line 89, col 26
+            if _v is not None: write(_filter(_v, rawExpr='$user.username')) # from line 89, col 26.
             write('''.]</p>
             write('''.]</p>
+
+<div class="navigation">
 <p><a href="list">List</a> 
 ''')
 <p><a href="list">List</a> 
 ''')
-            if VFFSL(SL,"varExists",False)('machine'): # generated from line 28, col 1
+            if VFFSL(SL,"varExists",False)('machine'): # generated from line 93, col 1
                 write('''<a href="info?machine_id=''')
                 write('''<a href="info?machine_id=''')
-                _v = VFFSL(SL,"machine.machine_id",True) # '$machine.machine_id' on line 29, col 26
-                if _v is not None: write(_filter(_v, rawExpr='$machine.machine_id')) # from line 29, col 26.
+                _v = VFFSL(SL,"machine.machine_id",True) # '$machine.machine_id' on line 94, col 26
+                if _v is not None: write(_filter(_v, rawExpr='$machine.machine_id')) # from line 94, col 26.
                 write('''">Info</a>
 <a href="vnc?machine_id=''')
                 write('''">Info</a>
 <a href="vnc?machine_id=''')
-                _v = VFFSL(SL,"machine.machine_id",True) # '$machine.machine_id' on line 30, col 25
-                if _v is not None: write(_filter(_v, rawExpr='$machine.machine_id')) # from line 30, col 25.
+                _v = VFFSL(SL,"machine.machine_id",True) # '$machine.machine_id' on line 95, col 25
+                if _v is not None: write(_filter(_v, rawExpr='$machine.machine_id')) # from line 95, col 25.
                 write('''">Console</a>
 ''')
             write('''<a href="help">Help</a></p>
 ''')
                 write('''">Console</a>
 ''')
             write('''<a href="help">Help</a></p>
 ''')
-        _v = VFFSL(SL,"body",True) # '$body' on line 34, col 1
-        if _v is not None: write(_filter(_v, rawExpr='$body')) # from line 34, col 1.
+        write('''</div>
+
+<div id="loadingnotice" class="loadingnotice">LOADING</div>
+<div id="result" class="result"></div>
+
+''')
+        _v = VFFSL(SL,"body",True) # '$body' on line 104, col 1
+        if _v is not None: write(_filter(_v, rawExpr='$body')) # from line 104, col 1.
         write('''
 ''')
         write('''
 ''')
-        if not VFFSL(SL,"varExists",False)('simple') or not VFFSL(SL,"simple",True): # generated from line 35, col 1
+        if not VFFSL(SL,"varExists",False)('simple') or not VFFSL(SL,"simple",True): # generated from line 105, col 1
             write('''<hr />
 Questions? Contact <a href="mailto:sipb-xen-dev@mit.edu">sipb-xen-dev@mit.edu</a>.
 ''')
             write('''<hr />
 Questions? Contact <a href="mailto:sipb-xen-dev@mit.edu">sipb-xen-dev@mit.edu</a>.
 ''')
@@ -147,6 +223,34 @@ Questions? Contact <a href="mailto:sipb-xen-dev@mit.edu">sipb-xen-dev@mit.edu</a
         
         return _dummyTrans and trans.response().getvalue() or ""
         
         
         return _dummyTrans and trans.response().getvalue() or ""
         
+
+    def writeBody(self, **KWS):
+
+
+
+        ## CHEETAH: main method generated for this template
+        trans = KWS.get("trans")
+        if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)):
+            trans = self.transaction # is None unless self.awake() was called
+        if not trans:
+            trans = DummyTransaction()
+            _dummyTrans = True
+        else: _dummyTrans = False
+        write = trans.response().write
+        SL = self._CHEETAH__searchList
+        _filter = self._CHEETAH__currentFilter
+        
+        ########################################
+        ## START - generated method body
+        
+        write('''
+''')
+        
+        ########################################
+        ## END - generated method body
+        
+        return _dummyTrans and trans.response().getvalue() or ""
+        
     ##################################################
     ## CHEETAH GENERATED ATTRIBUTES
 
     ##################################################
     ## CHEETAH GENERATED ATTRIBUTES
 
@@ -165,7 +269,7 @@ Questions? Contact <a href="mailto:sipb-xen-dev@mit.edu">sipb-xen-dev@mit.edu</a
 
     _CHEETAH_srcLastModified = __CHEETAH_srcLastModified__
 
 
     _CHEETAH_srcLastModified = __CHEETAH_srcLastModified__
 
-    _mainCheetahMethod_for_skeleton= 'respond'
+    _mainCheetahMethod_for_skeleton= 'writeBody'
 
 ## END CLASS DEFINITION
 
 
 ## END CLASS DEFINITION
 
index 1269786..7964aca 100644 (file)
@@ -1,8 +1,16 @@
+#from functions import functions
+#extends functions
+
+#def full_body
+<!DOCTYPE html
+PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html>
 <head><title>$title</title>
   <link href="static/favicon.ico" type="image/x-icon" rel="shortcut icon">
   <link rel="stylesheet" href="static/style.css" type="text/css" />
   <link rel="stylesheet" href="static/layout.css" type="text/css" media="screen" />
 <html>
 <head><title>$title</title>
   <link href="static/favicon.ico" type="image/x-icon" rel="shortcut icon">
   <link rel="stylesheet" href="static/style.css" type="text/css" />
   <link rel="stylesheet" href="static/layout.css" type="text/css" media="screen" />
+  <script type="text/javascript" src="static/prototype.js"></script>
   <script type="text/javascript">
 var helpWin = null;
 function closeWin(){
   <script type="text/javascript">
 var helpWin = null;
 function closeWin(){
@@ -19,11 +27,68 @@ function helppopup(name){
    if (window.focus){helpWin.focus();}
    return false;
 }
    if (window.focus){helpWin.focus();}
    return false;
 }
+
+Ajax.Responders.register({
+  onCreate: function(){
+    if (Ajax.activeRequestCount > 0) {
+       document.getElementById("loadingnotice").style.display = 'block';
+    }
+  }, 
+  onComplete: function(){
+    if (Ajax.activeRequestCount == 0) {
+      document.getElementById("loadingnotice").style.display = 'none';
+    } 
+  }
+});
+function replaceFunc(transport) {
+  try {
+    d = transport.responseText.evalJSON();
+  } catch (e) {
+    \$('body').innerHTML = '<pre>'+transport.responseText+'</pre>'
+    return;
+  }
+  for(key in d) {
+    \$(key).innerHTML = d[key];
+  }
+}
+
+function jsFormSubmit(location, elt){
+  new Ajax.Request(location, {method: 'post', 
+  parameters: Form.serialize(elt)+'&js=true', 
+  onSuccess: replaceFunc,
+  onComplete: function() {Form.enable(elt);}
+  });
+  Form.disable(elt);
+  return false;
+}
+
+function rowFormSubmit(elt, retpage){
+  new Ajax.Request('command', {method: 'post', 
+  parameters: Form.serialize(elt)+'&js='+retpage, 
+  onSuccess: replaceFunc
+  });
+  return false;
+}
+
+window.onload = {
+  //Fix bug with disabled elements
+  $$('form').each(Form.enable);
+}
+
 </script>
 </head>
 </script>
 </head>
-<body>
+<body id="body">
+
+<div id="err">
+#if $varExists('error_text')
+<p>STDERR:</p><pre>$error_text</pre>
+#end if
+</div>
+
 #if not $varExists('simple') or not $simple
 <p>[You are logged in as $user.username.]</p>
 #if not $varExists('simple') or not $simple
 <p>[You are logged in as $user.username.]</p>
+
+<div class="navigation">
 <p><a href="list">List</a> 
 #if $varExists('machine')
 <a href="info?machine_id=$machine.machine_id">Info</a>
 <p><a href="list">List</a> 
 #if $varExists('machine')
 <a href="info?machine_id=$machine.machine_id">Info</a>
@@ -31,6 +96,11 @@ function helppopup(name){
 #end if
 <a href="help">Help</a></p>
 #end if
 #end if
 <a href="help">Help</a></p>
 #end if
+</div>
+
+<div id="loadingnotice" class="loadingnotice">LOADING</div>
+<div id="result" class="result"></div>
+
 $body
 #if not $varExists('simple') or not $simple
 <hr />
 $body
 #if not $varExists('simple') or not $simple
 <hr />
@@ -38,3 +108,4 @@ Questions? Contact <a href="mailto:sipb-xen-dev@mit.edu">sipb-xen-dev@mit.edu</a
 #end if
 </body>
 </html>
 #end if
 </body>
 </html>
+#end def
diff --git a/templates/static/prototype.js b/templates/static/prototype.js
new file mode 100644 (file)
index 0000000..a3f21ac
--- /dev/null
@@ -0,0 +1,3277 @@
+/*  Prototype JavaScript framework, version 1.5.1.1
+ *  (c) 2005-2007 Sam Stephenson
+ *
+ *  Prototype is freely distributable under the terms of an MIT-style license.
+ *  For details, see the Prototype web site: http://www.prototypejs.org/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+  Version: '1.5.1.1',
+
+  Browser: {
+    IE:     !!(window.attachEvent && !window.opera),
+    Opera:  !!window.opera,
+    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1
+  },
+
+  BrowserFeatures: {
+    XPath: !!document.evaluate,
+    ElementExtensions: !!window.HTMLElement,
+    SpecificElementExtensions:
+      (document.createElement('div').__proto__ !==
+       document.createElement('form').__proto__)
+  },
+
+  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
+  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+  emptyFunction: function() { },
+  K: function(x) { return x }
+}
+
+var Class = {
+  create: function() {
+    return function() {
+      this.initialize.apply(this, arguments);
+    }
+  }
+}
+
+var Abstract = new Object();
+
+Object.extend = function(destination, source) {
+  for (var property in source) {
+    destination[property] = source[property];
+  }
+  return destination;
+}
+
+Object.extend(Object, {
+  inspect: function(object) {
+    try {
+      if (object === undefined) return 'undefined';
+      if (object === null) return 'null';
+      return object.inspect ? object.inspect() : object.toString();
+    } catch (e) {
+      if (e instanceof RangeError) return '...';
+      throw e;
+    }
+  },
+
+  toJSON: function(object) {
+    var type = typeof object;
+    switch(type) {
+      case 'undefined':
+      case 'function':
+      case 'unknown': return;
+      case 'boolean': return object.toString();
+    }
+    if (object === null) return 'null';
+    if (object.toJSON) return object.toJSON();
+    if (object.ownerDocument === document) return;
+    var results = [];
+    for (var property in object) {
+      var value = Object.toJSON(object[property]);
+      if (value !== undefined)
+        results.push(property.toJSON() + ': ' + value);
+    }
+    return '{' + results.join(', ') + '}';
+  },
+
+  keys: function(object) {
+    var keys = [];
+    for (var property in object)
+      keys.push(property);
+    return keys;
+  },
+
+  values: function(object) {
+    var values = [];
+    for (var property in object)
+      values.push(object[property]);
+    return values;
+  },
+
+  clone: function(object) {
+    return Object.extend({}, object);
+  }
+});
+
+Function.prototype.bind = function() {
+  var __method = this, args = $A(arguments), object = args.shift();
+  return function() {
+    return __method.apply(object, args.concat($A(arguments)));
+  }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+  var __method = this, args = $A(arguments), object = args.shift();
+  return function(event) {
+    return __method.apply(object, [event || window.event].concat(args));
+  }
+}
+
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    return this.toPaddedString(2, 16);
+  },
+
+  succ: function() {
+    return this + 1;
+  },
+
+  times: function(iterator) {
+    $R(0, this, true).each(iterator);
+    return this;
+  },
+
+  toPaddedString: function(length, radix) {
+    var string = this.toString(radix || 10);
+    return '0'.times(length - string.length) + string;
+  },
+
+  toJSON: function() {
+    return isFinite(this) ? this.toString() : 'null';
+  }
+});
+
+Date.prototype.toJSON = function() {
+  return '"' + this.getFullYear() + '-' +
+    (this.getMonth() + 1).toPaddedString(2) + '-' +
+    this.getDate().toPaddedString(2) + 'T' +
+    this.getHours().toPaddedString(2) + ':' +
+    this.getMinutes().toPaddedString(2) + ':' +
+    this.getSeconds().toPaddedString(2) + '"';
+};
+
+var Try = {
+  these: function() {
+    var returnValue;
+
+    for (var i = 0, length = arguments.length; i < length; i++) {
+      var lambda = arguments[i];
+      try {
+        returnValue = lambda();
+        break;
+      } catch (e) {}
+    }
+
+    return returnValue;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create();
+PeriodicalExecuter.prototype = {
+  initialize: function(callback, frequency) {
+    this.callback = callback;
+    this.frequency = frequency;
+    this.currentlyExecuting = false;
+
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  stop: function() {
+    if (!this.timer) return;
+    clearInterval(this.timer);
+    this.timer = null;
+  },
+
+  onTimerEvent: function() {
+    if (!this.currentlyExecuting) {
+      try {
+        this.currentlyExecuting = true;
+        this.callback(this);
+      } finally {
+        this.currentlyExecuting = false;
+      }
+    }
+  }
+}
+Object.extend(String, {
+  interpret: function(value) {
+    return value == null ? '' : String(value);
+  },
+  specialChar: {
+    '\b': '\\b',
+    '\t': '\\t',
+    '\n': '\\n',
+    '\f': '\\f',
+    '\r': '\\r',
+    '\\': '\\\\'
+  }
+});
+
+Object.extend(String.prototype, {
+  gsub: function(pattern, replacement) {
+    var result = '', source = this, match;
+    replacement = arguments.callee.prepareReplacement(replacement);
+
+    while (source.length > 0) {
+      if (match = source.match(pattern)) {
+        result += source.slice(0, match.index);
+        result += String.interpret(replacement(match));
+        source  = source.slice(match.index + match[0].length);
+      } else {
+        result += source, source = '';
+      }
+    }
+    return result;
+  },
+
+  sub: function(pattern, replacement, count) {
+    replacement = this.gsub.prepareReplacement(replacement);
+    count = count === undefined ? 1 : count;
+
+    return this.gsub(pattern, function(match) {
+      if (--count < 0) return match[0];
+      return replacement(match);
+    });
+  },
+
+  scan: function(pattern, iterator) {
+    this.gsub(pattern, iterator);
+    return this;
+  },
+
+  truncate: function(length, truncation) {
+    length = length || 30;
+    truncation = truncation === undefined ? '...' : truncation;
+    return this.length > length ?
+      this.slice(0, length - truncation.length) + truncation : this;
+  },
+
+  strip: function() {
+    return this.replace(/^\s+/, '').replace(/\s+$/, '');
+  },
+
+  stripTags: function() {
+    return this.replace(/<\/?[^>]+>/gi, '');
+  },
+
+  stripScripts: function() {
+    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+  },
+
+  extractScripts: function() {
+    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+    return (this.match(matchAll) || []).map(function(scriptTag) {
+      return (scriptTag.match(matchOne) || ['', ''])[1];
+    });
+  },
+
+  evalScripts: function() {
+    return this.extractScripts().map(function(script) { return eval(script) });
+  },
+
+  escapeHTML: function() {
+    var self = arguments.callee;
+    self.text.data = this;
+    return self.div.innerHTML;
+  },
+
+  unescapeHTML: function() {
+    var div = document.createElement('div');
+    div.innerHTML = this.stripTags();
+    return div.childNodes[0] ? (div.childNodes.length > 1 ?
+      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
+      div.childNodes[0].nodeValue) : '';
+  },
+
+  toQueryParams: function(separator) {
+    var match = this.strip().match(/([^?#]*)(#.*)?$/);
+    if (!match) return {};
+
+    return match[1].split(separator || '&').inject({}, function(hash, pair) {
+      if ((pair = pair.split('='))[0]) {
+        var key = decodeURIComponent(pair.shift());
+        var value = pair.length > 1 ? pair.join('=') : pair[0];
+        if (value != undefined) value = decodeURIComponent(value);
+
+        if (key in hash) {
+          if (hash[key].constructor != Array) hash[key] = [hash[key]];
+          hash[key].push(value);
+        }
+        else hash[key] = value;
+      }
+      return hash;
+    });
+  },
+
+  toArray: function() {
+    return this.split('');
+  },
+
+  succ: function() {
+    return this.slice(0, this.length - 1) +
+      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+  },
+
+  times: function(count) {
+    var result = '';
+    for (var i = 0; i < count; i++) result += this;
+    return result;
+  },
+
+  camelize: function() {
+    var parts = this.split('-'), len = parts.length;
+    if (len == 1) return parts[0];
+
+    var camelized = this.charAt(0) == '-'
+      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+      : parts[0];
+
+    for (var i = 1; i < len; i++)
+      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+    return camelized;
+  },
+
+  capitalize: function() {
+    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+  },
+
+  underscore: function() {
+    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+  },
+
+  dasherize: function() {
+    return this.gsub(/_/,'-');
+  },
+
+  inspect: function(useDoubleQuotes) {
+    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
+      var character = String.specialChar[match[0]];
+      return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
+    });
+    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+  },
+
+  toJSON: function() {
+    return this.inspect(true);
+  },
+
+  unfilterJSON: function(filter) {
+    return this.sub(filter || Prototype.JSONFilter, '#{1}');
+  },
+
+  isJSON: function() {
+    var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
+    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
+  },
+
+  evalJSON: function(sanitize) {
+    var json = this.unfilterJSON();
+    try {
+      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+    } catch (e) { }
+    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
+  },
+
+  include: function(pattern) {
+    return this.indexOf(pattern) > -1;
+  },
+
+  startsWith: function(pattern) {
+    return this.indexOf(pattern) === 0;
+  },
+
+  endsWith: function(pattern) {
+    var d = this.length - pattern.length;
+    return d >= 0 && this.lastIndexOf(pattern) === d;
+  },
+
+  empty: function() {
+    return this == '';
+  },
+
+  blank: function() {
+    return /^\s*$/.test(this);
+  }
+});
+
+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
+  escapeHTML: function() {
+    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
+  },
+  unescapeHTML: function() {
+    return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
+  }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+  if (typeof replacement == 'function') return replacement;
+  var template = new Template(replacement);
+  return function(match) { return template.evaluate(match) };
+}
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+Object.extend(String.prototype.escapeHTML, {
+  div:  document.createElement('div'),
+  text: document.createTextNode('')
+});
+
+with (String.prototype.escapeHTML) div.appendChild(text);
+
+var Template = Class.create();
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+Template.prototype = {
+  initialize: function(template, pattern) {
+    this.template = template.toString();
+    this.pattern  = pattern || Template.Pattern;
+  },
+
+  evaluate: function(object) {
+    return this.template.gsub(this.pattern, function(match) {
+      var before = match[1];
+      if (before == '\\') return match[2];
+      return before + String.interpret(object[match[3]]);
+    });
+  }
+}
+
+var $break = {}, $continue = new Error('"throw $continue" is deprecated, use "return" instead');
+
+var Enumerable = {
+  each: function(iterator) {
+    var index = 0;
+    try {
+      this._each(function(value) {
+        iterator(value, index++);
+      });
+    } catch (e) {
+      if (e != $break) throw e;
+    }
+    return this;
+  },
+
+  eachSlice: function(number, iterator) {
+    var index = -number, slices = [], array = this.toArray();
+    while ((index += number) < array.length)
+      slices.push(array.slice(index, index+number));
+    return slices.map(iterator);
+  },
+
+  all: function(iterator) {
+    var result = true;
+    this.each(function(value, index) {
+      result = result && !!(iterator || Prototype.K)(value, index);
+      if (!result) throw $break;
+    });
+    return result;
+  },
+
+  any: function(iterator) {
+    var result = false;
+    this.each(function(value, index) {
+      if (result = !!(iterator || Prototype.K)(value, index))
+        throw $break;
+    });
+    return result;
+  },
+
+  collect: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push((iterator || Prototype.K)(value, index));
+    });
+    return results;
+  },
+
+  detect: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      if (iterator(value, index)) {
+        result = value;
+        throw $break;
+      }
+    });
+    return result;
+  },
+
+  findAll: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  grep: function(pattern, iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      var stringValue = value.toString();
+      if (stringValue.match(pattern))
+        results.push((iterator || Prototype.K)(value, index));
+    })
+    return results;
+  },
+
+  include: function(object) {
+    var found = false;
+    this.each(function(value) {
+      if (value == object) {
+        found = true;
+        throw $break;
+      }
+    });
+    return found;
+  },
+
+  inGroupsOf: function(number, fillWith) {
+    fillWith = fillWith === undefined ? null : fillWith;
+    return this.eachSlice(number, function(slice) {
+      while(slice.length < number) slice.push(fillWith);
+      return slice;
+    });
+  },
+
+  inject: function(memo, iterator) {
+    this.each(function(value, index) {
+      memo = iterator(memo, value, index);
+    });
+    return memo;
+  },
+
+  invoke: function(method) {
+    var args = $A(arguments).slice(1);
+    return this.map(function(value) {
+      return value[method].apply(value, args);
+    });
+  },
+
+  max: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (result == undefined || value >= result)
+        result = value;
+    });
+    return result;
+  },
+
+  min: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (result == undefined || value < result)
+        result = value;
+    });
+    return result;
+  },
+
+  partition: function(iterator) {
+    var trues = [], falses = [];
+    this.each(function(value, index) {
+      ((iterator || Prototype.K)(value, index) ?
+        trues : falses).push(value);
+    });
+    return [trues, falses];
+  },
+
+  pluck: function(property) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push(value[property]);
+    });
+    return results;
+  },
+
+  reject: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (!iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  sortBy: function(iterator) {
+    return this.map(function(value, index) {
+      return {value: value, criteria: iterator(value, index)};
+    }).sort(function(left, right) {
+      var a = left.criteria, b = right.criteria;
+      return a < b ? -1 : a > b ? 1 : 0;
+    }).pluck('value');
+  },
+
+  toArray: function() {
+    return this.map();
+  },
+
+  zip: function() {
+    var iterator = Prototype.K, args = $A(arguments);
+    if (typeof args.last() == 'function')
+      iterator = args.pop();
+
+    var collections = [this].concat(args).map($A);
+    return this.map(function(value, index) {
+      return iterator(collections.pluck(index));
+    });
+  },
+
+  size: function() {
+    return this.toArray().length;
+  },
+
+  inspect: function() {
+    return '#<Enumerable:' + this.toArray().inspect() + '>';
+  }
+}
+
+Object.extend(Enumerable, {
+  map:     Enumerable.collect,
+  find:    Enumerable.detect,
+  select:  Enumerable.findAll,
+  member:  Enumerable.include,
+  entries: Enumerable.toArray
+});
+var $A = Array.from = function(iterable) {
+  if (!iterable) return [];
+  if (iterable.toArray) {
+    return iterable.toArray();
+  } else {
+    var results = [];
+    for (var i = 0, length = iterable.length; i < length; i++)
+      results.push(iterable[i]);
+    return results;
+  }
+}
+
+if (Prototype.Browser.WebKit) {
+  $A = Array.from = function(iterable) {
+    if (!iterable) return [];
+    if (!(typeof iterable == 'function' && iterable == '[object NodeList]') &&
+      iterable.toArray) {
+      return iterable.toArray();
+    } else {
+      var results = [];
+      for (var i = 0, length = iterable.length; i < length; i++)
+        results.push(iterable[i]);
+      return results;
+    }
+  }
+}
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse)
+  Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+  _each: function(iterator) {
+    for (var i = 0, length = this.length; i < length; i++)
+      iterator(this[i]);
+  },
+
+  clear: function() {
+    this.length = 0;
+    return this;
+  },
+
+  first: function() {
+    return this[0];
+  },
+
+  last: function() {
+    return this[this.length - 1];
+  },
+
+  compact: function() {
+    return this.select(function(value) {
+      return value != null;
+    });
+  },
+
+  flatten: function() {
+    return this.inject([], function(array, value) {
+      return array.concat(value && value.constructor == Array ?
+        value.flatten() : [value]);
+    });
+  },
+
+  without: function() {
+    var values = $A(arguments);
+    return this.select(function(value) {
+      return !values.include(value);
+    });
+  },
+
+  indexOf: function(object) {
+    for (var i = 0, length = this.length; i < length; i++)
+      if (this[i] == object) return i;
+    return -1;
+  },
+
+  reverse: function(inline) {
+    return (inline !== false ? this : this.toArray())._reverse();
+  },
+
+  reduce: function() {
+    return this.length > 1 ? this : this[0];
+  },
+
+  uniq: function(sorted) {
+    return this.inject([], function(array, value, index) {
+      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
+        array.push(value);
+      return array;
+    });
+  },
+
+  clone: function() {
+    return [].concat(this);
+  },
+
+  size: function() {
+    return this.length;
+  },
+
+  inspect: function() {
+    return '[' + this.map(Object.inspect).join(', ') + ']';
+  },
+
+  toJSON: function() {
+    var results = [];
+    this.each(function(object) {
+      var value = Object.toJSON(object);
+      if (value !== undefined) results.push(value);
+    });
+    return '[' + results.join(', ') + ']';
+  }
+});
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string) {
+  string = string.strip();
+  return string ? string.split(/\s+/) : [];
+}
+
+if (Prototype.Browser.Opera){
+  Array.prototype.concat = function() {
+    var array = [];
+    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
+    for (var i = 0, length = arguments.length; i < length; i++) {
+      if (arguments[i].constructor == Array) {
+        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
+          array.push(arguments[i][j]);
+      } else {
+        array.push(arguments[i]);
+      }
+    }
+    return array;
+  }
+}
+var Hash = function(object) {
+  if (object instanceof Hash) this.merge(object);
+  else Object.extend(this, object || {});
+};
+
+Object.extend(Hash, {
+  toQueryString: function(obj) {
+    var parts = [];
+    parts.add = arguments.callee.addPair;
+
+    this.prototype._each.call(obj, function(pair) {
+      if (!pair.key) return;
+      var value = pair.value;
+
+      if (value && typeof value == 'object') {
+        if (value.constructor == Array) value.each(function(value) {
+          parts.add(pair.key, value);
+        });
+        return;
+      }
+      parts.add(pair.key, value);
+    });
+
+    return parts.join('&');
+  },
+
+  toJSON: function(object) {
+    var results = [];
+    this.prototype._each.call(object, function(pair) {
+      var value = Object.toJSON(pair.value);
+      if (value !== undefined) results.push(pair.key.toJSON() + ': ' + value);
+    });
+    return '{' + results.join(', ') + '}';
+  }
+});
+
+Hash.toQueryString.addPair = function(key, value, prefix) {
+  key = encodeURIComponent(key);
+  if (value === undefined) this.push(key);
+  else this.push(key + '=' + (value == null ? '' : encodeURIComponent(value)));
+}
+
+Object.extend(Hash.prototype, Enumerable);
+Object.extend(Hash.prototype, {
+  _each: function(iterator) {
+    for (var key in this) {
+      var value = this[key];
+      if (value && value == Hash.prototype[key]) continue;
+
+      var pair = [key, value];
+      pair.key = key;
+      pair.value = value;
+      iterator(pair);
+    }
+  },
+
+  keys: function() {
+    return this.pluck('key');
+  },
+
+  values: function() {
+    return this.pluck('value');
+  },
+
+  merge: function(hash) {
+    return $H(hash).inject(this, function(mergedHash, pair) {
+      mergedHash[pair.key] = pair.value;
+      return mergedHash;
+    });
+  },
+
+  remove: function() {
+    var result;
+    for(var i = 0, length = arguments.length; i < length; i++) {
+      var value = this[arguments[i]];
+      if (value !== undefined){
+        if (result === undefined) result = value;
+        else {
+          if (result.constructor != Array) result = [result];
+          result.push(value)
+        }
+      }
+      delete this[arguments[i]];
+    }
+    return result;
+  },
+
+  toQueryString: function() {
+    return Hash.toQueryString(this);
+  },
+
+  inspect: function() {
+    return '#<Hash:{' + this.map(function(pair) {
+      return pair.map(Object.inspect).join(': ');
+    }).join(', ') + '}>';
+  },
+
+  toJSON: function() {
+    return Hash.toJSON(this);
+  }
+});
+
+function $H(object) {
+  if (object instanceof Hash) return object;
+  return new Hash(object);
+};
+
+// Safari iterates over shadowed properties
+if (function() {
+  var i = 0, Test = function(value) { this.key = value };
+  Test.prototype.key = 'foo';
+  for (var property in new Test('bar')) i++;
+  return i > 1;
+}()) Hash.prototype._each = function(iterator) {
+  var cache = [];
+  for (var key in this) {
+    var value = this[key];
+    if ((value && value == Hash.prototype[key]) || cache.include(key)) continue;
+    cache.push(key);
+    var pair = [key, value];
+    pair.key = key;
+    pair.value = value;
+    iterator(pair);
+  }
+};
+ObjectRange = Class.create();
+Object.extend(ObjectRange.prototype, Enumerable);
+Object.extend(ObjectRange.prototype, {
+  initialize: function(start, end, exclusive) {
+    this.start = start;
+    this.end = end;
+    this.exclusive = exclusive;
+  },
+
+  _each: function(iterator) {
+    var value = this.start;
+    while (this.include(value)) {
+      iterator(value);
+      value = value.succ();
+    }
+  },
+
+  include: function(value) {
+    if (value < this.start)
+      return false;
+    if (this.exclusive)
+      return value < this.end;
+    return value <= this.end;
+  }
+});
+
+var $R = function(start, end, exclusive) {
+  return new ObjectRange(start, end, exclusive);
+}
+
+var Ajax = {
+  getTransport: function() {
+    return Try.these(
+      function() {return new XMLHttpRequest()},
+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+    ) || false;
+  },
+
+  activeRequestCount: 0
+}
+
+Ajax.Responders = {
+  responders: [],
+
+  _each: function(iterator) {
+    this.responders._each(iterator);
+  },
+
+  register: function(responder) {
+    if (!this.include(responder))
+      this.responders.push(responder);
+  },
+
+  unregister: function(responder) {
+    this.responders = this.responders.without(responder);
+  },
+
+  dispatch: function(callback, request, transport, json) {
+    this.each(function(responder) {
+      if (typeof responder[callback] == 'function') {
+        try {
+          responder[callback].apply(responder, [request, transport, json]);
+        } catch (e) {}
+      }
+    });
+  }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+  onCreate: function() {
+    Ajax.activeRequestCount++;
+  },
+  onComplete: function() {
+    Ajax.activeRequestCount--;
+  }
+});
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+  setOptions: function(options) {
+    this.options = {
+      method:       'post',
+      asynchronous: true,
+      contentType:  'application/x-www-form-urlencoded',
+      encoding:     'UTF-8',
+      parameters:   ''
+    }
+    Object.extend(this.options, options || {});
+
+    this.options.method = this.options.method.toLowerCase();
+    if (typeof this.options.parameters == 'string')
+      this.options.parameters = this.options.parameters.toQueryParams();
+  }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+  _complete: false,
+
+  initialize: function(url, options) {
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+    this.request(url);
+  },
+
+  request: function(url) {
+    this.url = url;
+    this.method = this.options.method;
+    var params = Object.clone(this.options.parameters);
+
+    if (!['get', 'post'].include(this.method)) {
+      // simulate other verbs over post
+      params['_method'] = this.method;
+      this.method = 'post';
+    }
+
+    this.parameters = params;
+
+    if (params = Hash.toQueryString(params)) {
+      // when GET, append parameters to URL
+      if (this.method == 'get')
+        this.url += (this.url.include('?') ? '&' : '?') + params;
+      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+        params += '&_=';
+    }
+
+    try {
+      if (this.options.onCreate) this.options.onCreate(this.transport);
+      Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+      this.transport.open(this.method.toUpperCase(), this.url,
+        this.options.asynchronous);
+
+      if (this.options.asynchronous)
+        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
+
+      this.transport.onreadystatechange = this.onStateChange.bind(this);
+      this.setRequestHeaders();
+
+      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+      this.transport.send(this.body);
+
+      /* Force Firefox to handle ready state 4 for synchronous requests */
+      if (!this.options.asynchronous && this.transport.overrideMimeType)
+        this.onStateChange();
+
+    }
+    catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  onStateChange: function() {
+    var readyState = this.transport.readyState;
+    if (readyState > 1 && !((readyState == 4) && this._complete))
+      this.respondToReadyState(this.transport.readyState);
+  },
+
+  setRequestHeaders: function() {
+    var headers = {
+      'X-Requested-With': 'XMLHttpRequest',
+      'X-Prototype-Version': Prototype.Version,
+      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+    };
+
+    if (this.method == 'post') {
+      headers['Content-type'] = this.options.contentType +
+        (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+      /* Force "Connection: close" for older Mozilla browsers to work
+       * around a bug where XMLHttpRequest sends an incorrect
+       * Content-length header. See Mozilla Bugzilla #246651.
+       */
+      if (this.transport.overrideMimeType &&
+          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
+            headers['Connection'] = 'close';
+    }
+
+    // user-defined headers
+    if (typeof this.options.requestHeaders == 'object') {
+      var extras = this.options.requestHeaders;
+
+      if (typeof extras.push == 'function')
+        for (var i = 0, length = extras.length; i < length; i += 2)
+          headers[extras[i]] = extras[i+1];
+      else
+        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+    }
+
+    for (var name in headers)
+      this.transport.setRequestHeader(name, headers[name]);
+  },
+
+  success: function() {
+    return !this.transport.status
+        || (this.transport.status >= 200 && this.transport.status < 300);
+  },
+
+  respondToReadyState: function(readyState) {
+    var state = Ajax.Request.Events[readyState];
+    var transport = this.transport, json = this.evalJSON();
+
+    if (state == 'Complete') {
+      try {
+        this._complete = true;
+        (this.options['on' + this.transport.status]
+         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+         || Prototype.emptyFunction)(transport, json);
+      } catch (e) {
+        this.dispatchException(e);
+      }
+
+      var contentType = this.getHeader('Content-type');
+      if (contentType && contentType.strip().
+        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
+          this.evalResponse();
+    }
+
+    try {
+      (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
+      Ajax.Responders.dispatch('on' + state, this, transport, json);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+
+    if (state == 'Complete') {
+      // avoid memory leak in MSIE: clean up
+      this.transport.onreadystatechange = Prototype.emptyFunction;
+    }
+  },
+
+  getHeader: function(name) {
+    try {
+      return this.transport.getResponseHeader(name);
+    } catch (e) { return null }
+  },
+
+  evalJSON: function() {
+    try {
+      var json = this.getHeader('X-JSON');
+      return json ? json.evalJSON() : null;
+    } catch (e) { return null }
+  },
+
+  evalResponse: function() {
+    try {
+      return eval((this.transport.responseText || '').unfilterJSON());
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  dispatchException: function(exception) {
+    (this.options.onException || Prototype.emptyFunction)(this, exception);
+    Ajax.Responders.dispatch('onException', this, exception);
+  }
+});
+
+Ajax.Updater = Class.create();
+
+Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
+  initialize: function(container, url, options) {
+    this.container = {
+      success: (container.success || container),
+      failure: (container.failure || (container.success ? null : container))
+    }
+
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+
+    var onComplete = this.options.onComplete || Prototype.emptyFunction;
+    this.options.onComplete = (function(transport, param) {
+      this.updateContent();
+      onComplete(transport, param);
+    }).bind(this);
+
+    this.request(url);
+  },
+
+  updateContent: function() {
+    var receiver = this.container[this.success() ? 'success' : 'failure'];
+    var response = this.transport.responseText;
+
+    if (!this.options.evalScripts) response = response.stripScripts();
+
+    if (receiver = $(receiver)) {
+      if (this.options.insertion)
+        new this.options.insertion(receiver, response);
+      else
+        receiver.update(response);
+    }
+
+    if (this.success()) {
+      if (this.onComplete)
+        setTimeout(this.onComplete.bind(this), 10);
+    }
+  }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
+  initialize: function(container, url, options) {
+    this.setOptions(options);
+    this.onComplete = this.options.onComplete;
+
+    this.frequency = (this.options.frequency || 2);
+    this.decay = (this.options.decay || 1);
+
+    this.updater = {};
+    this.container = container;
+    this.url = url;
+
+    this.start();
+  },
+
+  start: function() {
+    this.options.onComplete = this.updateComplete.bind(this);
+    this.onTimerEvent();
+  },
+
+  stop: function() {
+    this.updater.options.onComplete = undefined;
+    clearTimeout(this.timer);
+    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+  },
+
+  updateComplete: function(request) {
+    if (this.options.decay) {
+      this.decay = (request.responseText == this.lastText ?
+        this.decay * this.options.decay : 1);
+
+      this.lastText = request.responseText;
+    }
+    this.timer = setTimeout(this.onTimerEvent.bind(this),
+      this.decay * this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    this.updater = new Ajax.Updater(this.container, this.url, this.options);
+  }
+});
+function $(element) {
+  if (arguments.length > 1) {
+    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+      elements.push($(arguments[i]));
+    return elements;
+  }
+  if (typeof element == 'string')
+    element = document.getElementById(element);
+  return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+  document._getElementsByXPath = function(expression, parentElement) {
+    var results = [];
+    var query = document.evaluate(expression, $(parentElement) || document,
+      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+    for (var i = 0, length = query.snapshotLength; i < length; i++)
+      results.push(query.snapshotItem(i));
+    return results;
+  };
+
+  document.getElementsByClassName = function(className, parentElement) {
+    var q = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
+    return document._getElementsByXPath(q, parentElement);
+  }
+
+} else document.getElementsByClassName = function(className, parentElement) {
+  var children = ($(parentElement) || document.body).getElementsByTagName('*');
+  var elements = [], child, pattern = new RegExp("(^|\\s)" + className + "(\\s|$)");
+  for (var i = 0, length = children.length; i < length; i++) {
+    child = children[i];
+    var elementClassName = child.className;
+    if (elementClassName.length == 0) continue;
+    if (elementClassName == className || elementClassName.match(pattern))
+      elements.push(Element.extend(child));
+  }
+  return elements;
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element) var Element = {};
+
+Element.extend = function(element) {
+  var F = Prototype.BrowserFeatures;
+  if (!element || !element.tagName || element.nodeType == 3 ||
+   element._extended || F.SpecificElementExtensions || element == window)
+    return element;
+
+  var methods = {}, tagName = element.tagName, cache = Element.extend.cache,
+   T = Element.Methods.ByTag;
+
+  // extend methods for all tags (Safari doesn't need this)
+  if (!F.ElementExtensions) {
+    Object.extend(methods, Element.Methods),
+    Object.extend(methods, Element.Methods.Simulated);
+  }
+
+  // extend methods for specific tags
+  if (T[tagName]) Object.extend(methods, T[tagName]);
+
+  for (var property in methods) {
+    var value = methods[property];
+    if (typeof value == 'function' && !(property in element))
+      element[property] = cache.findOrStore(value);
+  }
+
+  element._extended = Prototype.emptyFunction;
+  return element;
+};
+
+Element.extend.cache = {
+  findOrStore: function(value) {
+    return this[value] = this[value] || function() {
+      return value.apply(null, [this].concat($A(arguments)));
+    }
+  }
+};
+
+Element.Methods = {
+  visible: function(element) {
+    return $(element).style.display != 'none';
+  },
+
+  toggle: function(element) {
+    element = $(element);
+    Element[Element.visible(element) ? 'hide' : 'show'](element);
+    return element;
+  },
+
+  hide: function(element) {
+    $(element).style.display = 'none';
+    return element;
+  },
+
+  show: function(element) {
+    $(element).style.display = '';
+    return element;
+  },
+
+  remove: function(element) {
+    element = $(element);
+    element.parentNode.removeChild(element);
+    return element;
+  },
+
+  update: function(element, html) {
+    html = typeof html == 'undefined' ? '' : html.toString();
+    $(element).innerHTML = html.stripScripts();
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  },
+
+  replace: function(element, html) {
+    element = $(element);
+    html = typeof html == 'undefined' ? '' : html.toString();
+    if (element.outerHTML) {
+      element.outerHTML = html.stripScripts();
+    } else {
+      var range = element.ownerDocument.createRange();
+      range.selectNodeContents(element);
+      element.parentNode.replaceChild(
+        range.createContextualFragment(html.stripScripts()), element);
+    }
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  },
+
+  inspect: function(element) {
+    element = $(element);
+    var result = '<' + element.tagName.toLowerCase();
+    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+      var property = pair.first(), attribute = pair.last();
+      var value = (element[property] || '').toString();
+      if (value) result += ' ' + attribute + '=' + value.inspect(true);
+    });
+    return result + '>';
+  },
+
+  recursivelyCollect: function(element, property) {
+    element = $(element);
+    var elements = [];
+    while (element = element[property])
+      if (element.nodeType == 1)
+        elements.push(Element.extend(element));
+    return elements;
+  },
+
+  ancestors: function(element) {
+    return $(element).recursivelyCollect('parentNode');
+  },
+
+  descendants: function(element) {
+    return $A($(element).getElementsByTagName('*')).each(Element.extend);
+  },
+
+  firstDescendant: function(element) {
+    element = $(element).firstChild;
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    return $(element);
+  },
+
+  immediateDescendants: function(element) {
+    if (!(element = $(element).firstChild)) return [];
+    while (element && element.nodeType != 1) element = element.nextSibling;
+    if (element) return [element].concat($(element).nextSiblings());
+    return [];
+  },
+
+  previousSiblings: function(element) {
+    return $(element).recursivelyCollect('previousSibling');
+  },
+
+  nextSiblings: function(element) {
+    return $(element).recursivelyCollect('nextSibling');
+  },
+
+  siblings: function(element) {
+    element = $(element);
+    return element.previousSiblings().reverse().concat(element.nextSiblings());
+  },
+
+  match: function(element, selector) {
+    if (typeof selector == 'string')
+      selector = new Selector(selector);
+    return selector.match($(element));
+  },
+
+  up: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(element.parentNode);
+    var ancestors = element.ancestors();
+    return expression ? Selector.findElement(ancestors, expression, index) :
+      ancestors[index || 0];
+  },
+
+  down: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return element.firstDescendant();
+    var descendants = element.descendants();
+    return expression ? Selector.findElement(descendants, expression, index) :
+      descendants[index || 0];
+  },
+
+  previous: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
+    var previousSiblings = element.previousSiblings();
+    return expression ? Selector.findElement(previousSiblings, expression, index) :
+      previousSiblings[index || 0];
+  },
+
+  next: function(element, expression, index) {
+    element = $(element);
+    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
+    var nextSiblings = element.nextSiblings();
+    return expression ? Selector.findElement(nextSiblings, expression, index) :
+      nextSiblings[index || 0];
+  },
+
+  getElementsBySelector: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element, args);
+  },
+
+  getElementsByClassName: function(element, className) {
+    return document.getElementsByClassName(className, element);
+  },
+
+  readAttribute: function(element, name) {
+    element = $(element);
+    if (Prototype.Browser.IE) {
+      if (!element.attributes) return null;
+      var t = Element._attributeTranslations;
+      if (t.values[name]) return t.values[name](element, name);
+      if (t.names[name])  name = t.names[name];
+      var attribute = element.attributes[name];
+      return attribute ? attribute.nodeValue : null;
+    }
+    return element.getAttribute(name);
+  },
+
+  getHeight: function(element) {
+    return $(element).getDimensions().height;
+  },
+
+  getWidth: function(element) {
+    return $(element).getDimensions().width;
+  },
+
+  classNames: function(element) {
+    return new Element.ClassNames(element);
+  },
+
+  hasClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    var elementClassName = element.className;
+    if (elementClassName.length == 0) return false;
+    if (elementClassName == className ||
+        elementClassName.match(new RegExp("(^|\\s)" + className + "(\\s|$)")))
+      return true;
+    return false;
+  },
+
+  addClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element).add(className);
+    return element;
+  },
+
+  removeClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element).remove(className);
+    return element;
+  },
+
+  toggleClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className);
+    return element;
+  },
+
+  observe: function() {
+    Event.observe.apply(Event, arguments);
+    return $A(arguments).first();
+  },
+
+  stopObserving: function() {
+    Event.stopObserving.apply(Event, arguments);
+    return $A(arguments).first();
+  },
+
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    element = $(element);
+    var node = element.firstChild;
+    while (node) {
+      var nextNode = node.nextSibling;
+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+        element.removeChild(node);
+      node = nextNode;
+    }
+    return element;
+  },
+
+  empty: function(element) {
+    return $(element).innerHTML.blank();
+  },
+
+  descendantOf: function(element, ancestor) {
+    element = $(element), ancestor = $(ancestor);
+    while (element = element.parentNode)
+      if (element == ancestor) return true;
+    return false;
+  },
+
+  scrollTo: function(element) {
+    element = $(element);
+    var pos = Position.cumulativeOffset(element);
+    window.scrollTo(pos[0], pos[1]);
+    return element;
+  },
+
+  getStyle: function(element, style) {
+    element = $(element);
+    style = style == 'float' ? 'cssFloat' : style.camelize();
+    var value = element.style[style];
+    if (!value) {
+      var css = document.defaultView.getComputedStyle(element, null);
+      value = css ? css[style] : null;
+    }
+    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
+    return value == 'auto' ? null : value;
+  },
+
+  getOpacity: function(element) {
+    return $(element).getStyle('opacity');
+  },
+
+  setStyle: function(element, styles, camelized) {
+    element = $(element);
+    var elementStyle = element.style;
+
+    for (var property in styles)
+      if (property == 'opacity') element.setOpacity(styles[property])
+      else
+        elementStyle[(property == 'float' || property == 'cssFloat') ?
+          (elementStyle.styleFloat === undefined ? 'cssFloat' : 'styleFloat') :
+          (camelized ? property : property.camelize())] = styles[property];
+
+    return element;
+  },
+
+  setOpacity: function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1 || value === '') ? '' :
+      (value < 0.00001) ? 0 : value;
+    return element;
+  },
+
+  getDimensions: function(element) {
+    element = $(element);
+    var display = $(element).getStyle('display');
+    if (display != 'none' && display != null) // Safari bug
+      return {width: element.offsetWidth, height: element.offsetHeight};
+
+    // All *Width and *Height properties give 0 on elements with display none,
+    // so enable the element temporarily
+    var els = element.style;
+    var originalVisibility = els.visibility;
+    var originalPosition = els.position;
+    var originalDisplay = els.display;
+    els.visibility = 'hidden';
+    els.position = 'absolute';
+    els.display = 'block';
+    var originalWidth = element.clientWidth;
+    var originalHeight = element.clientHeight;
+    els.display = originalDisplay;
+    els.position = originalPosition;
+    els.visibility = originalVisibility;
+    return {width: originalWidth, height: originalHeight};
+  },
+
+  makePositioned: function(element) {
+    element = $(element);
+    var pos = Element.getStyle(element, 'position');
+    if (pos == 'static' || !pos) {
+      element._madePositioned = true;
+      element.style.position = 'relative';
+      // Opera returns the offset relative to the positioning context, when an
+      // element is position relative but top and left have not been defined
+      if (window.opera) {
+        element.style.top = 0;
+        element.style.left = 0;
+      }
+    }
+    return element;
+  },
+
+  undoPositioned: function(element) {
+    element = $(element);
+    if (element._madePositioned) {
+      element._madePositioned = undefined;
+      element.style.position =
+        element.style.top =
+        element.style.left =
+        element.style.bottom =
+        element.style.right = '';
+    }
+    return element;
+  },
+
+  makeClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return element;
+    element._overflow = element.style.overflow || 'auto';
+    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+      element.style.overflow = 'hidden';
+    return element;
+  },
+
+  undoClipping: function(element) {
+    element = $(element);
+    if (!element._overflow) return element;
+    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+    element._overflow = null;
+    return element;
+  }
+};
+
+Object.extend(Element.Methods, {
+  childOf: Element.Methods.descendantOf,
+  childElements: Element.Methods.immediateDescendants
+});
+
+if (Prototype.Browser.Opera) {
+  Element.Methods._getStyle = Element.Methods.getStyle;
+  Element.Methods.getStyle = function(element, style) {
+    switch(style) {
+      case 'left':
+      case 'top':
+      case 'right':
+      case 'bottom':
+        if (Element._getStyle(element, 'position') == 'static') return null;
+      default: return Element._getStyle(element, style);
+    }
+  };
+}
+else if (Prototype.Browser.IE) {
+  Element.Methods.getStyle = function(element, style) {
+    element = $(element);
+    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
+    var value = element.style[style];
+    if (!value && element.currentStyle) value = element.currentStyle[style];
+
+    if (style == 'opacity') {
+      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+        if (value[1]) return parseFloat(value[1]) / 100;
+      return 1.0;
+    }
+
+    if (value == 'auto') {
+      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
+        return element['offset'+style.capitalize()] + 'px';
+      return null;
+    }
+    return value;
+  };
+
+  Element.Methods.setOpacity = function(element, value) {
+    element = $(element);
+    var filter = element.getStyle('filter'), style = element.style;
+    if (value == 1 || value === '') {
+      style.filter = filter.replace(/alpha\([^\)]*\)/gi,'');
+      return element;
+    } else if (value < 0.00001) value = 0;
+    style.filter = filter.replace(/alpha\([^\)]*\)/gi, '') +
+      'alpha(opacity=' + (value * 100) + ')';
+    return element;
+  };
+
+  // IE is missing .innerHTML support for TABLE-related elements
+  Element.Methods.update = function(element, html) {
+    element = $(element);
+    html = typeof html == 'undefined' ? '' : html.toString();
+    var tagName = element.tagName.toUpperCase();
+    if (['THEAD','TBODY','TR','TD'].include(tagName)) {
+      var div = document.createElement('div');
+      switch (tagName) {
+        case 'THEAD':
+        case 'TBODY':
+          div.innerHTML = '<table><tbody>' +  html.stripScripts() + '</tbody></table>';
+          depth = 2;
+          break;
+        case 'TR':
+          div.innerHTML = '<table><tbody><tr>' +  html.stripScripts() + '</tr></tbody></table>';
+          depth = 3;
+          break;
+        case 'TD':
+          div.innerHTML = '<table><tbody><tr><td>' +  html.stripScripts() + '</td></tr></tbody></table>';
+          depth = 4;
+      }
+      $A(element.childNodes).each(function(node) { element.removeChild(node) });
+      depth.times(function() { div = div.firstChild });
+      $A(div.childNodes).each(function(node) { element.appendChild(node) });
+    } else {
+      element.innerHTML = html.stripScripts();
+    }
+    setTimeout(function() { html.evalScripts() }, 10);
+    return element;
+  }
+}
+else if (Prototype.Browser.Gecko) {
+  Element.Methods.setOpacity = function(element, value) {
+    element = $(element);
+    element.style.opacity = (value == 1) ? 0.999999 :
+      (value === '') ? '' : (value < 0.00001) ? 0 : value;
+    return element;
+  };
+}
+
+Element._attributeTranslations = {
+  names: {
+    colspan:   "colSpan",
+    rowspan:   "rowSpan",
+    valign:    "vAlign",
+    datetime:  "dateTime",
+    accesskey: "accessKey",
+    tabindex:  "tabIndex",
+    enctype:   "encType",
+    maxlength: "maxLength",
+    readonly:  "readOnly",
+    longdesc:  "longDesc"
+  },
+  values: {
+    _getAttr: function(element, attribute) {
+      return element.getAttribute(attribute, 2);
+    },
+    _flag: function(element, attribute) {
+      return $(element).hasAttribute(attribute) ? attribute : null;
+    },
+    style: function(element) {
+      return element.style.cssText.toLowerCase();
+    },
+    title: function(element) {
+      var node = element.getAttributeNode('title');
+      return node.specified ? node.nodeValue : null;
+    }
+  }
+};
+
+(function() {
+  Object.extend(this, {
+    href: this._getAttr,
+    src:  this._getAttr,
+    type: this._getAttr,
+    disabled: this._flag,
+    checked:  this._flag,
+    readonly: this._flag,
+    multiple: this._flag
+  });
+}).call(Element._attributeTranslations.values);
+
+Element.Methods.Simulated = {
+  hasAttribute: function(element, attribute) {
+    var t = Element._attributeTranslations, node;
+    attribute = t.names[attribute] || attribute;
+    node = $(element).getAttributeNode(attribute);
+    return node && node.specified;
+  }
+};
+
+Element.Methods.ByTag = {};
+
+Object.extend(Element, Element.Methods);
+
+if (!Prototype.BrowserFeatures.ElementExtensions &&
+ document.createElement('div').__proto__) {
+  window.HTMLElement = {};
+  window.HTMLElement.prototype = document.createElement('div').__proto__;
+  Prototype.BrowserFeatures.ElementExtensions = true;
+}
+
+Element.hasAttribute = function(element, attribute) {
+  if (element.hasAttribute) return element.hasAttribute(attribute);
+  return Element.Methods.Simulated.hasAttribute(element, attribute);
+};
+
+Element.addMethods = function(methods) {
+  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+  if (!methods) {
+    Object.extend(Form, Form.Methods);
+    Object.extend(Form.Element, Form.Element.Methods);
+    Object.extend(Element.Methods.ByTag, {
+      "FORM":     Object.clone(Form.Methods),
+      "INPUT":    Object.clone(Form.Element.Methods),
+      "SELECT":   Object.clone(Form.Element.Methods),
+      "TEXTAREA": Object.clone(Form.Element.Methods)
+    });
+  }
+
+  if (arguments.length == 2) {
+    var tagName = methods;
+    methods = arguments[1];
+  }
+
+  if (!tagName) Object.extend(Element.Methods, methods || {});
+  else {
+    if (tagName.constructor == Array) tagName.each(extend);
+    else extend(tagName);
+  }
+
+  function extend(tagName) {
+    tagName = tagName.toUpperCase();
+    if (!Element.Methods.ByTag[tagName])
+      Element.Methods.ByTag[tagName] = {};
+    Object.extend(Element.Methods.ByTag[tagName], methods);
+  }
+
+  function copy(methods, destination, onlyIfAbsent) {
+    onlyIfAbsent = onlyIfAbsent || false;
+    var cache = Element.extend.cache;
+    for (var property in methods) {
+      var value = methods[property];
+      if (!onlyIfAbsent || !(property in destination))
+        destination[property] = cache.findOrStore(value);
+    }
+  }
+
+  function findDOMClass(tagName) {
+    var klass;
+    var trans = {
+      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
+      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
+      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
+      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
+      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
+      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
+      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
+      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
+      "FrameSet", "IFRAME": "IFrame"
+    };
+    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
+    if (window[klass]) return window[klass];
+    klass = 'HTML' + tagName + 'Element';
+    if (window[klass]) return window[klass];
+    klass = 'HTML' + tagName.capitalize() + 'Element';
+    if (window[klass]) return window[klass];
+
+    window[klass] = {};
+    window[klass].prototype = document.createElement(tagName).__proto__;
+    return window[klass];
+  }
+
+  if (F.ElementExtensions) {
+    copy(Element.Methods, HTMLElement.prototype);
+    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+  }
+
+  if (F.SpecificElementExtensions) {
+    for (var tag in Element.Methods.ByTag) {
+      var klass = findDOMClass(tag);
+      if (typeof klass == "undefined") continue;
+      copy(T[tag], klass.prototype);
+    }
+  }
+
+  Object.extend(Element, Element.Methods);
+  delete Element.ByTag;
+};
+
+var Toggle = { display: Element.toggle };
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.Insertion = function(adjacency) {
+  this.adjacency = adjacency;
+}
+
+Abstract.Insertion.prototype = {
+  initialize: function(element, content) {
+    this.element = $(element);
+    this.content = content.stripScripts();
+
+    if (this.adjacency && this.element.insertAdjacentHTML) {
+      try {
+        this.element.insertAdjacentHTML(this.adjacency, this.content);
+      } catch (e) {
+        var tagName = this.element.tagName.toUpperCase();
+        if (['TBODY', 'TR'].include(tagName)) {
+          this.insertContent(this.contentFromAnonymousTable());
+        } else {
+          throw e;
+        }
+      }
+    } else {
+      this.range = this.element.ownerDocument.createRange();
+      if (this.initializeRange) this.initializeRange();
+      this.insertContent([this.range.createContextualFragment(this.content)]);
+    }
+
+    setTimeout(function() {content.evalScripts()}, 10);
+  },
+
+  contentFromAnonymousTable: function() {
+    var div = document.createElement('div');
+    div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>';
+    return $A(div.childNodes[0].childNodes[0].childNodes);
+  }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
+  initializeRange: function() {
+    this.range.setStartBefore(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment, this.element);
+    }).bind(this));
+  }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(true);
+  },
+
+  insertContent: function(fragments) {
+    fragments.reverse(false).each((function(fragment) {
+      this.element.insertBefore(fragment, this.element.firstChild);
+    }).bind(this));
+  }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.appendChild(fragment);
+    }).bind(this));
+  }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
+  initializeRange: function() {
+    this.range.setStartAfter(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment,
+        this.element.nextSibling);
+    }).bind(this));
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+  initialize: function(element) {
+    this.element = $(element);
+  },
+
+  _each: function(iterator) {
+    this.element.className.split(/\s+/).select(function(name) {
+      return name.length > 0;
+    })._each(iterator);
+  },
+
+  set: function(className) {
+    this.element.className = className;
+  },
+
+  add: function(classNameToAdd) {
+    if (this.include(classNameToAdd)) return;
+    this.set($A(this).concat(classNameToAdd).join(' '));
+  },
+
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set($A(this).without(classNameToRemove).join(' '));
+  },
+
+  toString: function() {
+    return $A(this).join(' ');
+  }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,
+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
+ * license.  Please see http://www.yui-ext.com/ for more information. */
+
+var Selector = Class.create();
+
+Selector.prototype = {
+  initialize: function(expression) {
+    this.expression = expression.strip();
+    this.compileMatcher();
+  },
+
+  compileMatcher: function() {
+    // Selectors with namespaced attributes can't use the XPath version
+    if (Prototype.BrowserFeatures.XPath && !(/\[[\w-]*?:/).test(this.expression))
+      return this.compileXPathMatcher();
+
+    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
+        c = Selector.criteria, le, p, m;
+
+    if (Selector._cache[e]) {
+      this.matcher = Selector._cache[e]; return;
+    }
+    this.matcher = ["this.matcher = function(root) {",
+                    "var r = root, h = Selector.handlers, c = false, n;"];
+
+    while (e && le != e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        p = ps[i];
+        if (m = e.match(p)) {
+          this.matcher.push(typeof c[i] == 'function' ? c[i](m) :
+             new Template(c[i]).evaluate(m));
+          e = e.replace(m[0], '');
+          break;
+        }
+      }
+    }
+
+    this.matcher.push("return h.unique(n);\n}");
+    eval(this.matcher.join('\n'));
+    Selector._cache[this.expression] = this.matcher;
+  },
+
+  compileXPathMatcher: function() {
+    var e = this.expression, ps = Selector.patterns,
+        x = Selector.xpath, le,  m;
+
+    if (Selector._cache[e]) {
+      this.xpath = Selector._cache[e]; return;
+    }
+
+    this.matcher = ['.//*'];
+    while (e && le != e && (/\S/).test(e)) {
+      le = e;
+      for (var i in ps) {
+        if (m = e.match(ps[i])) {
+          this.matcher.push(typeof x[i] == 'function' ? x[i](m) :
+            new Template(x[i]).evaluate(m));
+          e = e.replace(m[0], '');
+          break;
+        }
+      }
+    }
+
+    this.xpath = this.matcher.join('');
+    Selector._cache[this.expression] = this.xpath;
+  },
+
+  findElements: function(root) {
+    root = root || document;
+    if (this.xpath) return document._getElementsByXPath(this.xpath, root);
+    return this.matcher(root);
+  },
+
+  match: function(element) {
+    return this.findElements(document).include(element);
+  },
+
+  toString: function() {
+    return this.expression;
+  },
+
+  inspect: function() {
+    return "#<Selector:" + this.expression.inspect() + ">";
+  }
+};
+
+Object.extend(Selector, {
+  _cache: {},
+
+  xpath: {
+    descendant:   "//*",
+    child:        "/*",
+    adjacent:     "/following-sibling::*[1]",
+    laterSibling: '/following-sibling::*',
+    tagName:      function(m) {
+      if (m[1] == '*') return '';
+      return "[local-name()='" + m[1].toLowerCase() +
+             "' or local-name()='" + m[1].toUpperCase() + "']";
+    },
+    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
+    id:           "[@id='#{1}']",
+    attrPresence: "[@#{1}]",
+    attr: function(m) {
+      m[3] = m[5] || m[6];
+      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
+    },
+    pseudo: function(m) {
+      var h = Selector.xpath.pseudos[m[1]];
+      if (!h) return '';
+      if (typeof h === 'function') return h(m);
+      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
+    },
+    operators: {
+      '=':  "[@#{1}='#{3}']",
+      '!=': "[@#{1}!='#{3}']",
+      '^=': "[starts-with(@#{1}, '#{3}')]",
+      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
+      '*=': "[contains(@#{1}, '#{3}')]",
+      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
+      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
+    },
+    pseudos: {
+      'first-child': '[not(preceding-sibling::*)]',
+      'last-child':  '[not(following-sibling::*)]',
+      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
+      'empty':       "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",
+      'checked':     "[@checked]",
+      'disabled':    "[@disabled]",
+      'enabled':     "[not(@disabled)]",
+      'not': function(m) {
+        var e = m[6], p = Selector.patterns,
+            x = Selector.xpath, le, m, v;
+
+        var exclusion = [];
+        while (e && le != e && (/\S/).test(e)) {
+          le = e;
+          for (var i in p) {
+            if (m = e.match(p[i])) {
+              v = typeof x[i] == 'function' ? x[i](m) : new Template(x[i]).evaluate(m);
+              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
+              e = e.replace(m[0], '');
+              break;
+            }
+          }
+        }
+        return "[not(" + exclusion.join(" and ") + ")]";
+      },
+      'nth-child':      function(m) {
+        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
+      },
+      'nth-last-child': function(m) {
+        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
+      },
+      'nth-of-type':    function(m) {
+        return Selector.xpath.pseudos.nth("position() ", m);
+      },
+      'nth-last-of-type': function(m) {
+        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
+      },
+      'first-of-type':  function(m) {
+        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
+      },
+      'last-of-type':   function(m) {
+        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
+      },
+      'only-of-type':   function(m) {
+        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
+      },
+      nth: function(fragment, m) {
+        var mm, formula = m[6], predicate;
+        if (formula == 'even') formula = '2n+0';
+        if (formula == 'odd')  formula = '2n+1';
+        if (mm = formula.match(/^(\d+)$/)) // digit only
+          return '[' + fragment + "= " + mm[1] + ']';
+        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+          if (mm[1] == "-") mm[1] = -1;
+          var a = mm[1] ? Number(mm[1]) : 1;
+          var b = mm[2] ? Number(mm[2]) : 0;
+          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
+          "((#{fragment} - #{b}) div #{a} >= 0)]";
+          return new Template(predicate).evaluate({
+            fragment: fragment, a: a, b: b });
+        }
+      }
+    }
+  },
+
+  criteria: {
+    tagName:      'n = h.tagName(n, r, "#{1}", c);   c = false;',
+    className:    'n = h.className(n, r, "#{1}", c); c = false;',
+    id:           'n = h.id(n, r, "#{1}", c);        c = false;',
+    attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;',
+    attr: function(m) {
+      m[3] = (m[5] || m[6]);
+      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m);
+    },
+    pseudo:       function(m) {
+      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
+      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
+    },
+    descendant:   'c = "descendant";',
+    child:        'c = "child";',
+    adjacent:     'c = "adjacent";',
+    laterSibling: 'c = "laterSibling";'
+  },
+
+  patterns: {
+    // combinators must be listed first
+    // (and descendant needs to be last combinator)
+    laterSibling: /^\s*~\s*/,
+    child:        /^\s*>\s*/,
+    adjacent:     /^\s*\+\s*/,
+    descendant:   /^\s/,
+
+    // selectors follow
+    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
+    id:           /^#([\w\-\*]+)(\b|$)/,
+    className:    /^\.([\w\-\*]+)(\b|$)/,
+    pseudo:       /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|\s|(?=:))/,
+    attrPresence: /^\[([\w]+)\]/,
+    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\]]*?)\4|([^'"][^\]]*?)))?\]/
+  },
+
+  handlers: {
+    // UTILITY FUNCTIONS
+    // joins two collections
+    concat: function(a, b) {
+      for (var i = 0, node; node = b[i]; i++)
+        a.push(node);
+      return a;
+    },
+
+    // marks an array of nodes for counting
+    mark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._counted = true;
+      return nodes;
+    },
+
+    unmark: function(nodes) {
+      for (var i = 0, node; node = nodes[i]; i++)
+        node._counted = undefined;
+      return nodes;
+    },
+
+    // mark each child node with its position (for nth calls)
+    // "ofType" flag indicates whether we're indexing for nth-of-type
+    // rather than nth-child
+    index: function(parentNode, reverse, ofType) {
+      parentNode._counted = true;
+      if (reverse) {
+        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
+          node = nodes[i];
+          if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+        }
+      } else {
+        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
+          if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++;
+      }
+    },
+
+    // filters out duplicates and extends all nodes
+    unique: function(nodes) {
+      if (nodes.length == 0) return nodes;
+      var results = [], n;
+      for (var i = 0, l = nodes.length; i < l; i++)
+        if (!(n = nodes[i])._counted) {
+          n._counted = true;
+          results.push(Element.extend(n));
+        }
+      return Selector.handlers.unmark(results);
+    },
+
+    // COMBINATOR FUNCTIONS
+    descendant: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        h.concat(results, node.getElementsByTagName('*'));
+      return results;
+    },
+
+    child: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        for (var j = 0, children = [], child; child = node.childNodes[j]; j++)
+          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
+      }
+      return results;
+    },
+
+    adjacent: function(nodes) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        var next = this.nextElementSibling(node);
+        if (next) results.push(next);
+      }
+      return results;
+    },
+
+    laterSibling: function(nodes) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        h.concat(results, Element.nextSiblings(node));
+      return results;
+    },
+
+    nextElementSibling: function(node) {
+      while (node = node.nextSibling)
+             if (node.nodeType == 1) return node;
+      return null;
+    },
+
+    previousElementSibling: function(node) {
+      while (node = node.previousSibling)
+        if (node.nodeType == 1) return node;
+      return null;
+    },
+
+    // TOKEN FUNCTIONS
+    tagName: function(nodes, root, tagName, combinator) {
+      tagName = tagName.toUpperCase();
+      var results = [], h = Selector.handlers;
+      if (nodes) {
+        if (combinator) {
+          // fastlane for ordinary descendant combinators
+          if (combinator == "descendant") {
+            for (var i = 0, node; node = nodes[i]; i++)
+              h.concat(results, node.getElementsByTagName(tagName));
+            return results;
+          } else nodes = this[combinator](nodes);
+          if (tagName == "*") return nodes;
+        }
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node.tagName.toUpperCase() == tagName) results.push(node);
+        return results;
+      } else return root.getElementsByTagName(tagName);
+    },
+
+    id: function(nodes, root, id, combinator) {
+      var targetNode = $(id), h = Selector.handlers;
+      if (!nodes && root == document) return targetNode ? [targetNode] : [];
+      if (nodes) {
+        if (combinator) {
+          if (combinator == 'child') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (targetNode.parentNode == node) return [targetNode];
+          } else if (combinator == 'descendant') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (Element.descendantOf(targetNode, node)) return [targetNode];
+          } else if (combinator == 'adjacent') {
+            for (var i = 0, node; node = nodes[i]; i++)
+              if (Selector.handlers.previousElementSibling(targetNode) == node)
+                return [targetNode];
+          } else nodes = h[combinator](nodes);
+        }
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node == targetNode) return [targetNode];
+        return [];
+      }
+      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
+    },
+
+    className: function(nodes, root, className, combinator) {
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      return Selector.handlers.byClassName(nodes, root, className);
+    },
+
+    byClassName: function(nodes, root, className) {
+      if (!nodes) nodes = Selector.handlers.descendant([root]);
+      var needle = ' ' + className + ' ';
+      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
+        nodeClassName = node.className;
+        if (nodeClassName.length == 0) continue;
+        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
+          results.push(node);
+      }
+      return results;
+    },
+
+    attrPresence: function(nodes, root, attr) {
+      var results = [];
+      for (var i = 0, node; node = nodes[i]; i++)
+        if (Element.hasAttribute(node, attr)) results.push(node);
+      return results;
+    },
+
+    attr: function(nodes, root, attr, value, operator) {
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      var handler = Selector.operators[operator], results = [];
+      for (var i = 0, node; node = nodes[i]; i++) {
+        var nodeValue = Element.readAttribute(node, attr);
+        if (nodeValue === null) continue;
+        if (handler(nodeValue, value)) results.push(node);
+      }
+      return results;
+    },
+
+    pseudo: function(nodes, name, value, root, combinator) {
+      if (nodes && combinator) nodes = this[combinator](nodes);
+      if (!nodes) nodes = root.getElementsByTagName("*");
+      return Selector.pseudos[name](nodes, value, root);
+    }
+  },
+
+  pseudos: {
+    'first-child': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        if (Selector.handlers.previousElementSibling(node)) continue;
+          results.push(node);
+      }
+      return results;
+    },
+    'last-child': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        if (Selector.handlers.nextElementSibling(node)) continue;
+          results.push(node);
+      }
+      return results;
+    },
+    'only-child': function(nodes, value, root) {
+      var h = Selector.handlers;
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
+          results.push(node);
+      return results;
+    },
+    'nth-child':        function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root);
+    },
+    'nth-last-child':   function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, true);
+    },
+    'nth-of-type':      function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, false, true);
+    },
+    'nth-last-of-type': function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, formula, root, true, true);
+    },
+    'first-of-type':    function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, "1", root, false, true);
+    },
+    'last-of-type':     function(nodes, formula, root) {
+      return Selector.pseudos.nth(nodes, "1", root, true, true);
+    },
+    'only-of-type':     function(nodes, formula, root) {
+      var p = Selector.pseudos;
+      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
+    },
+
+    // handles the an+b logic
+    getIndices: function(a, b, total) {
+      if (a == 0) return b > 0 ? [b] : [];
+      return $R(1, total).inject([], function(memo, i) {
+        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
+        return memo;
+      });
+    },
+
+    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
+    nth: function(nodes, formula, root, reverse, ofType) {
+      if (nodes.length == 0) return [];
+      if (formula == 'even') formula = '2n+0';
+      if (formula == 'odd')  formula = '2n+1';
+      var h = Selector.handlers, results = [], indexed = [], m;
+      h.mark(nodes);
+      for (var i = 0, node; node = nodes[i]; i++) {
+        if (!node.parentNode._counted) {
+          h.index(node.parentNode, reverse, ofType);
+          indexed.push(node.parentNode);
+        }
+      }
+      if (formula.match(/^\d+$/)) { // just a number
+        formula = Number(formula);
+        for (var i = 0, node; node = nodes[i]; i++)
+          if (node.nodeIndex == formula) results.push(node);
+      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
+        if (m[1] == "-") m[1] = -1;
+        var a = m[1] ? Number(m[1]) : 1;
+        var b = m[2] ? Number(m[2]) : 0;
+        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
+        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
+          for (var j = 0; j < l; j++)
+            if (node.nodeIndex == indices[j]) results.push(node);
+        }
+      }
+      h.unmark(nodes);
+      h.unmark(indexed);
+      return results;
+    },
+
+    'empty': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++) {
+        // IE treats comments as element nodes
+        if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;
+        results.push(node);
+      }
+      return results;
+    },
+
+    'not': function(nodes, selector, root) {
+      var h = Selector.handlers, selectorType, m;
+      var exclusions = new Selector(selector).findElements(root);
+      h.mark(exclusions);
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!node._counted) results.push(node);
+      h.unmark(exclusions);
+      return results;
+    },
+
+    'enabled': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (!node.disabled) results.push(node);
+      return results;
+    },
+
+    'disabled': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (node.disabled) results.push(node);
+      return results;
+    },
+
+    'checked': function(nodes, value, root) {
+      for (var i = 0, results = [], node; node = nodes[i]; i++)
+        if (node.checked) results.push(node);
+      return results;
+    }
+  },
+
+  operators: {
+    '=':  function(nv, v) { return nv == v; },
+    '!=': function(nv, v) { return nv != v; },
+    '^=': function(nv, v) { return nv.startsWith(v); },
+    '$=': function(nv, v) { return nv.endsWith(v); },
+    '*=': function(nv, v) { return nv.include(v); },
+    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
+    '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }
+  },
+
+  matchElements: function(elements, expression) {
+    var matches = new Selector(expression).findElements(), h = Selector.handlers;
+    h.mark(matches);
+    for (var i = 0, results = [], element; element = elements[i]; i++)
+      if (element._counted) results.push(element);
+    h.unmark(matches);
+    return results;
+  },
+
+  findElement: function(elements, expression, index) {
+    if (typeof expression == 'number') {
+      index = expression; expression = false;
+    }
+    return Selector.matchElements(elements, expression || '*')[index || 0];
+  },
+
+  findChildElements: function(element, expressions) {
+    var exprs = expressions.join(','), expressions = [];
+    exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
+      expressions.push(m[1].strip());
+    });
+    var results = [], h = Selector.handlers;
+    for (var i = 0, l = expressions.length, selector; i < l; i++) {
+      selector = new Selector(expressions[i].strip());
+      h.concat(results, selector.findElements(element));
+    }
+    return (l > 1) ? h.unique(results) : results;
+  }
+});
+
+function $$() {
+  return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+  reset: function(form) {
+    $(form).reset();
+    return form;
+  },
+
+  serializeElements: function(elements, getHash) {
+    var data = elements.inject({}, function(result, element) {
+      if (!element.disabled && element.name) {
+        var key = element.name, value = $(element).getValue();
+        if (value != null) {
+               if (key in result) {
+            if (result[key].constructor != Array) result[key] = [result[key]];
+            result[key].push(value);
+          }
+          else result[key] = value;
+        }
+      }
+      return result;
+    });
+
+    return getHash ? data : Hash.toQueryString(data);
+  }
+};
+
+Form.Methods = {
+  serialize: function(form, getHash) {
+    return Form.serializeElements(Form.getElements(form), getHash);
+  },
+
+  getElements: function(form) {
+    return $A($(form).getElementsByTagName('*')).inject([],
+      function(elements, child) {
+        if (Form.Element.Serializers[child.tagName.toLowerCase()])
+          elements.push(Element.extend(child));
+        return elements;
+      }
+    );
+  },
+
+  getInputs: function(form, typeName, name) {
+    form = $(form);
+    var inputs = form.getElementsByTagName('input');
+
+    if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+      var input = inputs[i];
+      if ((typeName && input.type != typeName) || (name && input.name != name))
+        continue;
+      matchingInputs.push(Element.extend(input));
+    }
+
+    return matchingInputs;
+  },
+
+  disable: function(form) {
+    form = $(form);
+    Form.getElements(form).invoke('disable');
+    return form;
+  },
+
+  enable: function(form) {
+    form = $(form);
+    Form.getElements(form).invoke('enable');
+    return form;
+  },
+
+  findFirstElement: function(form) {
+    return $(form).getElements().find(function(element) {
+      return element.type != 'hidden' && !element.disabled &&
+        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    });
+  },
+
+  focusFirstElement: function(form) {
+    form = $(form);
+    form.findFirstElement().activate();
+    return form;
+  },
+
+  request: function(form, options) {
+    form = $(form), options = Object.clone(options || {});
+
+    var params = options.parameters;
+    options.parameters = form.serialize(true);
+
+    if (params) {
+      if (typeof params == 'string') params = params.toQueryParams();
+      Object.extend(options.parameters, params);
+    }
+
+    if (form.hasAttribute('method') && !options.method)
+      options.method = form.method;
+
+    return new Ajax.Request(form.readAttribute('action'), options);
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+  focus: function(element) {
+    $(element).focus();
+    return element;
+  },
+
+  select: function(element) {
+    $(element).select();
+    return element;
+  }
+}
+
+Form.Element.Methods = {
+  serialize: function(element) {
+    element = $(element);
+    if (!element.disabled && element.name) {
+      var value = element.getValue();
+      if (value != undefined) {
+        var pair = {};
+        pair[element.name] = value;
+        return Hash.toQueryString(pair);
+      }
+    }
+    return '';
+  },
+
+  getValue: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    return Form.Element.Serializers[method](element);
+  },
+
+  clear: function(element) {
+    $(element).value = '';
+    return element;
+  },
+
+  present: function(element) {
+    return $(element).value != '';
+  },
+
+  activate: function(element) {
+    element = $(element);
+    try {
+      element.focus();
+      if (element.select && (element.tagName.toLowerCase() != 'input' ||
+        !['button', 'reset', 'submit'].include(element.type)))
+        element.select();
+    } catch (e) {}
+    return element;
+  },
+
+  disable: function(element) {
+    element = $(element);
+    element.blur();
+    element.disabled = true;
+    return element;
+  },
+
+  enable: function(element) {
+    element = $(element);
+    element.disabled = false;
+    return element;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Field = Form.Element;
+var $F = Form.Element.Methods.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+  input: function(element) {
+    switch (element.type.toLowerCase()) {
+      case 'checkbox':
+      case 'radio':
+        return Form.Element.Serializers.inputSelector(element);
+      default:
+        return Form.Element.Serializers.textarea(element);
+    }
+  },
+
+  inputSelector: function(element) {
+    return element.checked ? element.value : null;
+  },
+
+  textarea: function(element) {
+    return element.value;
+  },
+
+  select: function(element) {
+    return this[element.type == 'select-one' ?
+      'selectOne' : 'selectMany'](element);
+  },
+
+  selectOne: function(element) {
+    var index = element.selectedIndex;
+    return index >= 0 ? this.optionValue(element.options[index]) : null;
+  },
+
+  selectMany: function(element) {
+    var values, length = element.length;
+    if (!length) return null;
+
+    for (var i = 0, values = []; i < length; i++) {
+      var opt = element.options[i];
+      if (opt.selected) values.push(this.optionValue(opt));
+    }
+    return values;
+  },
+
+  optionValue: function(opt) {
+    // extend element because hasAttribute may not be native
+    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.TimedObserver = function() {}
+Abstract.TimedObserver.prototype = {
+  initialize: function(element, frequency, callback) {
+    this.frequency = frequency;
+    this.element   = $(element);
+    this.callback  = callback;
+
+    this.lastValue = this.getValue();
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    var value = this.getValue();
+    var changed = ('string' == typeof this.lastValue && 'string' == typeof value
+      ? this.lastValue != value : String(this.lastValue) != String(value));
+    if (changed) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = function() {}
+Abstract.EventObserver.prototype = {
+  initialize: function(element, callback) {
+    this.element  = $(element);
+    this.callback = callback;
+
+    this.lastValue = this.getValue();
+    if (this.element.tagName.toLowerCase() == 'form')
+      this.registerFormCallbacks();
+    else
+      this.registerCallback(this.element);
+  },
+
+  onElementEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  },
+
+  registerFormCallbacks: function() {
+    Form.getElements(this.element).each(this.registerCallback.bind(this));
+  },
+
+  registerCallback: function(element) {
+    if (element.type) {
+      switch (element.type.toLowerCase()) {
+        case 'checkbox':
+        case 'radio':
+          Event.observe(element, 'click', this.onElementEvent.bind(this));
+          break;
+        default:
+          Event.observe(element, 'change', this.onElementEvent.bind(this));
+          break;
+      }
+    }
+  }
+}
+
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+if (!window.Event) {
+  var Event = new Object();
+}
+
+Object.extend(Event, {
+  KEY_BACKSPACE: 8,
+  KEY_TAB:       9,
+  KEY_RETURN:   13,
+  KEY_ESC:      27,
+  KEY_LEFT:     37,
+  KEY_UP:       38,
+  KEY_RIGHT:    39,
+  KEY_DOWN:     40,
+  KEY_DELETE:   46,
+  KEY_HOME:     36,
+  KEY_END:      35,
+  KEY_PAGEUP:   33,
+  KEY_PAGEDOWN: 34,
+
+  element: function(event) {
+    return $(event.target || event.srcElement);
+  },
+
+  isLeftClick: function(event) {
+    return (((event.which) && (event.which == 1)) ||
+            ((event.button) && (event.button == 1)));
+  },
+
+  pointerX: function(event) {
+    return event.pageX || (event.clientX +
+      (document.documentElement.scrollLeft || document.body.scrollLeft));
+  },
+
+  pointerY: function(event) {
+    return event.pageY || (event.clientY +
+      (document.documentElement.scrollTop || document.body.scrollTop));
+  },
+
+  stop: function(event) {
+    if (event.preventDefault) {
+      event.preventDefault();
+      event.stopPropagation();
+    } else {
+      event.returnValue = false;
+      event.cancelBubble = true;
+    }
+  },
+
+  // find the first node with the given tagName, starting from the
+  // node the event was triggered on; traverses the DOM upwards
+  findElement: function(event, tagName) {
+    var element = Event.element(event);
+    while (element.parentNode && (!element.tagName ||
+        (element.tagName.toUpperCase() != tagName.toUpperCase())))
+      element = element.parentNode;
+    return element;
+  },
+
+  observers: false,
+
+  _observeAndCache: function(element, name, observer, useCapture) {
+    if (!this.observers) this.observers = [];
+    if (element.addEventListener) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.addEventListener(name, observer, useCapture);
+    } else if (element.attachEvent) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.attachEvent('on' + name, observer);
+    }
+  },
+
+  unloadCache: function() {
+    if (!Event.observers) return;
+    for (var i = 0, length = Event.observers.length; i < length; i++) {
+      Event.stopObserving.apply(this, Event.observers[i]);
+      Event.observers[i][0] = null;
+    }
+    Event.observers = false;
+  },
+
+  observe: function(element, name, observer, useCapture) {
+    element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+      (Prototype.Browser.WebKit || element.attachEvent))
+      name = 'keydown';
+
+    Event._observeAndCache(element, name, observer, useCapture);
+  },
+
+  stopObserving: function(element, name, observer, useCapture) {
+    element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &&
+        (Prototype.Browser.WebKit || element.attachEvent))
+      name = 'keydown';
+
+    if (element.removeEventListener) {
+      element.removeEventListener(name, observer, useCapture);
+    } else if (element.detachEvent) {
+      try {
+        element.detachEvent('on' + name, observer);
+      } catch (e) {}
+    }
+  }
+});
+
+/* prevent memory leaks in IE */
+if (Prototype.Browser.IE)
+  Event.observe(window, 'unload', Event.unloadCache, false);
+var Position = {
+  // set to true if needed, warning: firefox performance problems
+  // NOT neeeded for page scrolling, only if draggable contained in
+  // scrollable elements
+  includeScrollOffsets: false,
+
+  // must be called before calling withinIncludingScrolloffset, every time the
+  // page is scrolled
+  prepare: function() {
+    this.deltaX =  window.pageXOffset
+                || document.documentElement.scrollLeft
+                || document.body.scrollLeft
+                || 0;
+    this.deltaY =  window.pageYOffset
+                || document.documentElement.scrollTop
+                || document.body.scrollTop
+                || 0;
+  },
+
+  realOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.scrollTop  || 0;
+      valueL += element.scrollLeft || 0;
+      element = element.parentNode;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  cumulativeOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  positionedOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+      if (element) {
+        if(element.tagName=='BODY') break;
+        var p = Element.getStyle(element, 'position');
+        if (p == 'relative' || p == 'absolute') break;
+      }
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  offsetParent: function(element) {
+    if (element.offsetParent) return element.offsetParent;
+    if (element == document.body) return element;
+
+    while ((element = element.parentNode) && element != document.body)
+      if (Element.getStyle(element, 'position') != 'static')
+        return element;
+
+    return document.body;
+  },
+
+  // caches x/y coordinate pair to use with overlap
+  within: function(element, x, y) {
+    if (this.includeScrollOffsets)
+      return this.withinIncludingScrolloffsets(element, x, y);
+    this.xcomp = x;
+    this.ycomp = y;
+    this.offset = this.cumulativeOffset(element);
+
+    return (y >= this.offset[1] &&
+            y <  this.offset[1] + element.offsetHeight &&
+            x >= this.offset[0] &&
+            x <  this.offset[0] + element.offsetWidth);
+  },
+
+  withinIncludingScrolloffsets: function(element, x, y) {
+    var offsetcache = this.realOffset(element);
+
+    this.xcomp = x + offsetcache[0] - this.deltaX;
+    this.ycomp = y + offsetcache[1] - this.deltaY;
+    this.offset = this.cumulativeOffset(element);
+
+    return (this.ycomp >= this.offset[1] &&
+            this.ycomp <  this.offset[1] + element.offsetHeight &&
+            this.xcomp >= this.offset[0] &&
+            this.xcomp <  this.offset[0] + element.offsetWidth);
+  },
+
+  // within must be called directly before
+  overlap: function(mode, element) {
+    if (!mode) return 0;
+    if (mode == 'vertical')
+      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+        element.offsetHeight;
+    if (mode == 'horizontal')
+      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+        element.offsetWidth;
+  },
+
+  page: function(forElement) {
+    var valueT = 0, valueL = 0;
+
+    var element = forElement;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+
+      // Safari fix
+      if (element.offsetParent == document.body)
+        if (Element.getStyle(element,'position')=='absolute') break;
+
+    } while (element = element.offsetParent);
+
+    element = forElement;
+    do {
+      if (!window.opera || element.tagName=='BODY') {
+        valueT -= element.scrollTop  || 0;
+        valueL -= element.scrollLeft || 0;
+      }
+    } while (element = element.parentNode);
+
+    return [valueL, valueT];
+  },
+
+  clone: function(source, target) {
+    var options = Object.extend({
+      setLeft:    true,
+      setTop:     true,
+      setWidth:   true,
+      setHeight:  true,
+      offsetTop:  0,
+      offsetLeft: 0
+    }, arguments[2] || {})
+
+    // find page position of source
+    source = $(source);
+    var p = Position.page(source);
+
+    // find coordinate system to use
+    target = $(target);
+    var delta = [0, 0];
+    var parent = null;
+    // delta [0,0] will do fine with position: fixed elements,
+    // position:absolute needs offsetParent deltas
+    if (Element.getStyle(target,'position') == 'absolute') {
+      parent = Position.offsetParent(target);
+      delta = Position.page(parent);
+    }
+
+    // correct by body offsets (fixes Safari)
+    if (parent == document.body) {
+      delta[0] -= document.body.offsetLeft;
+      delta[1] -= document.body.offsetTop;
+    }
+
+    // set position
+    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
+    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
+    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
+    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
+  },
+
+  absolutize: function(element) {
+    element = $(element);
+    if (element.style.position == 'absolute') return;
+    Position.prepare();
+
+    var offsets = Position.positionedOffset(element);
+    var top     = offsets[1];
+    var left    = offsets[0];
+    var width   = element.clientWidth;
+    var height  = element.clientHeight;
+
+    element._originalLeft   = left - parseFloat(element.style.left  || 0);
+    element._originalTop    = top  - parseFloat(element.style.top || 0);
+    element._originalWidth  = element.style.width;
+    element._originalHeight = element.style.height;
+
+    element.style.position = 'absolute';
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.width  = width + 'px';
+    element.style.height = height + 'px';
+  },
+
+  relativize: function(element) {
+    element = $(element);
+    if (element.style.position == 'relative') return;
+    Position.prepare();
+
+    element.style.position = 'relative';
+    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
+    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.height = element._originalHeight;
+    element.style.width  = element._originalWidth;
+  }
+}
+
+// Safari returns margins on body which is incorrect if the child is absolutely
+// positioned.  For performance reasons, redefine Position.cumulativeOffset for
+// KHTML/WebKit only.
+if (Prototype.Browser.WebKit) {
+  Position.cumulativeOffset = function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      if (element.offsetParent == document.body)
+        if (Element.getStyle(element, 'position') == 'absolute') break;
+
+      element = element.offsetParent;
+    } while (element);
+
+    return [valueL, valueT];
+  }
+}
+
+Element.addMethods();
\ No newline at end of file
index 65f6966..f311640 100644 (file)
@@ -66,3 +66,22 @@ var {
 .index table td a {
     font-family: "Bitstream Vera Sans Mono", "Luxi Mono", "Courier New", monospace;
 }
 .index table td a {
     font-family: "Bitstream Vera Sans Mono", "Luxi Mono", "Courier New", monospace;
 }
+
+#loadingnotice {
+    padding: 2px;
+    display: none;
+    color: #FFFFFF;
+    background: #CC2200;
+    position: fixed;
+    right: 10px;
+    top: 5px;
+}
+
+.error {
+  color: #FF0000;
+  padding: 0.25em;
+}
+
+td.error {
+  border: 1px solid red;
+}
\ No newline at end of file