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
 
-
-#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>
+#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>
@@ -33,48 +43,74 @@ Info on $machine.name
     </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
-       <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>
-      <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>
-      <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>
-<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>
-<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>
-    <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
-    <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
+$errorRow('hostname', $err)
 #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 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
index 05b5c2c..68f0858 100644 (file)
@@ -1,26 +1,62 @@
 #from skeleton import skeleton
 #extends skeleton
 
+
 #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
-    <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>
@@ -34,8 +70,8 @@ List of your VMs
        <td></td>
 #end if
 <td>#slurp
-#if $uptimes[$machine]
-$uptimes[$machine]#slurp
+#if $machine.uptime
+$machine.uptime#slurp
 #else
 Off#slurp
 #end if
@@ -48,60 +84,46 @@ $has_vnc[$machine]
 #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"/>
-#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>
+#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 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
-<p>You are at the maximum number of VMs.</p>
+    <p>You have the following VMs:</p>
 #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
index 915b5eb..6ec853d 100755 (executable)
@@ -1,27 +1,47 @@
 #!/usr/bin/python
+"""Main CGI script for web interface"""
 
-import sys
+import base64
+import cPickle
 import cgi
+import datetime
+import getafsgroups
+import hmac
 import os
+import random
+import re
+import sha
+import simplejson
 import string
 import subprocess
-import re
+import sys
 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 *
-import random
 
 class MyException(Exception):
     """Base class for my exceptions"""
@@ -43,7 +63,14 @@ class CodeError(MyException):
     """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):
+    """Global state of the system, to avoid duplicate remctls to get state"""
     def __init__(self, user):
         self.user = user
 
@@ -53,16 +80,66 @@ class Global(object):
         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():
@@ -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.)
 
-def canAddVm(user):
+def cantAddVm(user):
     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"""
@@ -131,7 +212,8 @@ def haveAccess(user, machine):
         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
@@ -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)
-    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))
-    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"""
@@ -193,11 +275,11 @@ def remctl(*args, **kws):
                          + list(args),
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE)
+    v = p.wait()
     if kws.get('err'):
-        p.wait()
         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()
@@ -276,13 +358,16 @@ def statusInfo(machine):
 
     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:
-        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:
-        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
 
@@ -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 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
@@ -325,10 +412,11 @@ def createVm(user, name, memory, disk, is_hvm, cdrom):
         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)    
@@ -377,16 +465,15 @@ def validDisk(user, disk, machine=None):
                            "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):
-        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,
-                           "Already exists")
+                           "Name already exists.")
     
     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):
-        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:
+        m.uptime = g.uptimes.get(m)
         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)
-    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,
-             can_add_vm=canAddVm(user),
-             max_mem=max_mem,
+             cant_add_vm=cantAddVm(user),
+             max_memory=max_memory,
              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())
-    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.
 
@@ -468,9 +597,12 @@ def vnc(user, fields):
 
     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
@@ -481,12 +613,12 @@ def vnc(user, fields):
 
     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.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)
     
@@ -499,8 +631,7 @@ def vnc(user, fields):
              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.
@@ -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])
-        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:
@@ -534,7 +666,8 @@ def getDiskInfo(data_dict, machine):
     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
 
@@ -542,7 +675,8 @@ def deleteVM(machine):
     """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
@@ -559,8 +693,7 @@ def deleteVM(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')
@@ -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)    
-    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:
-            remctl('control', machine.name, 'reboot', cdrom)
+            out, err = remctl('control', machine.name, 'reboot', cdrom,
+                              err=True)
         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',
-                               "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':
-        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':
-        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
@@ -591,7 +753,41 @@ def command(user, fields):
     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):
@@ -600,17 +796,15 @@ def testAdmin(user, admin, machine):
         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
-    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
-    #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
@@ -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):
-        raise InvalidInput('contact', contact, "Not a valid email")
+        raise InvalidInput('contact', contact, "Not a valid email.")
     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
-    raise InvalidInput('name', name, "Already taken")
+    raise InvalidInput('name', name, "Name is already taken.")
 
 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, "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
 
-def modify(user, fields):
-    """Handler for modifying attributes of a machine."""
-
+def modifyDict(user, fields):
     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
-        command="modify"
+        command = "modify"
 
         memory = fields.getfirst('memory')
         if memory is not None:
@@ -674,7 +867,8 @@ def modify(user, fields):
                 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)
@@ -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)
-    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')
     
-    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.""",
-                   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.""",
-                   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
-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:
-        subjects = sorted(mapping.keys())
+        subjects = sorted(help_mapping.keys())
         
     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))
-        uptime=None
-        cputime=None
+        uptime = None
+        cputime = None
     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')
-    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')
-    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:
@@ -807,10 +1039,17 @@ def info(user, fields):
             #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,
+             defaults=defaults,
              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)
-    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,
@@ -827,24 +1071,27 @@ mapping = dict(list=listVms,
                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()
-    class User:
-        username = "moo"
-        email = 'moo@cow.com'
-    u = User()
+    u = getUser()
     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"
@@ -856,39 +1103,42 @@ if __name__ == '__main__':
     if not operation:
         operation = 'list'
 
-    def badOperation(u, e):
-        raise CodeError("Unknown operation")
+
 
     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)
-        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:
-            output = str(output)
-            output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
+            output.addError(e)
+        printHeaders(headers)
         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'
-        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
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 functions import functions
 
 ##################################################
 ## MODULE CONSTANTS
@@ -33,10 +34,10 @@ VFN=valueForName
 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_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:
@@ -48,7 +49,7 @@ if __CHEETAH_versionTuple__ < RequiredCheetahVersionTuple:
 ##################################################
 ## CLASSES
 
-class skeleton(Template):
+class skeleton(functions):
 
     ##################################################
     ## CHEETAH GENERATED METHODS
@@ -56,7 +57,7 @@ class skeleton(Template):
 
     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()
@@ -65,11 +66,12 @@ class skeleton(Template):
             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:
@@ -83,14 +85,18 @@ class skeleton(Template):
         ########################################
         ## 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>''')
-        _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" />
+  <script type="text/javascript" src="static/prototype.js"></script>
   <script type="text/javascript">
 var helpWin = null;
 function closeWin(){
@@ -107,34 +113,104 @@ function helppopup(name){
    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>
-<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 ''')
-            _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>
+
+<div class="navigation">
 <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=''')
-                _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=''')
-                _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>
 ''')
-        _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('''
 ''')
-        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>.
 ''')
@@ -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 ""
         
+
+    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
 
@@ -165,7 +269,7 @@ Questions? Contact <a href="mailto:sipb-xen-dev@mit.edu">sipb-xen-dev@mit.edu</a
 
     _CHEETAH_srcLastModified = __CHEETAH_srcLastModified__
 
-    _mainCheetahMethod_for_skeleton= 'respond'
+    _mainCheetahMethod_for_skeleton= 'writeBody'
 
 ## 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" />
+  <script type="text/javascript" src="static/prototype.js"></script>
   <script type="text/javascript">
 var helpWin = null;
 function closeWin(){
@@ -19,11 +27,68 @@ function helppopup(name){
    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>
-<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>
+
+<div class="navigation">
 <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
+</div>
+
+<div id="loadingnotice" class="loadingnotice">LOADING</div>
+<div id="result" class="result"></div>
+
 $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 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;
 }
+
+#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