[Bps-public-commit] wifty branch, master, created. 94e56c4dee5911b28346972be17d675bee181f7e

Kevin Falcone falcone at bestpractical.com
Wed Mar 31 15:40:51 EDT 2010


The branch, master has been created
        at  94e56c4dee5911b28346972be17d675bee181f7e (commit)

- Log -----------------------------------------------------------------
commit 38eaa832eb93c3c3af24f0117f5e188533a2c362
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:26:13 2005 +0000

    (empty commit message)

commit 655563731fb5704ff52337d72cd0063384bdb884
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:29:37 2005 +0000

    Merge  into 'trunk'
    
    
    r18591 at truegrounds (orig r2266):  alexmv | 2005-11-08 16:34:35 -0500
     r7059 at zoq-fot-pik:  chmrr | 2005-11-08 16:33:37 -0500
      * Look ma, no helpers.  Temporary replacements dropped in
    
    r18592 at truegrounds (orig r2267):  alexmv | 2005-11-08 16:34:53 -0500
     r7060 at zoq-fot-pik:  chmrr | 2005-11-08 16:33:55 -0500
      * Continuations working epsilon better
    
    
    
    r18566 at truegrounds (orig r2262):  root | 2005-11-08 12:49:06 -0500
    * Task edit style updates
    
    
    
    r18568 at truegrounds (orig r2263):  root | 2005-11-08 13:11:27 -0500
    * CSS hackery
    
    
    
    r18602 at truegrounds (orig r2272):  alexmv | 2005-11-08 18:19:49 -0500
     r7070 at zoq-fot-pik:  chmrr | 2005-11-08 18:19:08 -0500
      * Hackish API for making linksandbuttonsthat pop the stack
      * First steps toward preserving J:C arg when needed
    
    r18603 at truegrounds (orig r2273):  alexmv | 2005-11-09 14:55:48 -0500
     r7081 at zoq-fot-pik:  chmrr | 2005-11-09 14:55:22 -0500
      * Jifty::Web->(button|link) both take parameters
      * Jifty::Action->button takes parameters, but calls Jifty::Web->button
      * Started another doc pass
    
    
    
    * Linkified
    
    
    r18655 at truegrounds (orig r2274):  alexmv | 2005-11-09 17:22:58 -0500
     r7083 at zoq-fot-pik:  chmrr | 2005-11-09 17:22:27 -0500
      * Some docs, notably a bunch of continuations
    
    
    
    
    
    * Now guessed configu is the base of the config file, not an alternative to it
    
    r18660 at truegrounds (orig r2276):  alexmv | 2005-11-10 15:50:10 -0500
     r7085 at zoq-fot-pik:  chmrr | 2005-11-09 18:25:39 -0500
      * Don't explode if I don't recognize the continuation, silently fail
    
    r18661 at truegrounds (orig r2277):  alexmv | 2005-11-10 15:50:23 -0500
     r7086 at zoq-fot-pik:  chmrr | 2005-11-10 15:49:53 -0500
      * Coderefs through continuations
    
    r18662 at truegrounds (orig r2278):  alexmv | 2005-11-10 15:50:32 -0500
    
    
    
    * started to add users
    
    * Config file loading cleanups
    
    
    * new_article became new_entry
    
    * Refactored "config" to its own package to clean out Jifty::
    
    
    * checkpoint of new blogdemo pseudocode
    
    r18706 at truegrounds (orig r2285):  alexmv | 2005-11-10 16:51:50 -0500
     r7100 at zoq-fot-pik:  chmrr | 2005-11-10 16:51:16 -0500
      * First pass at a "gosub" call
    
    r18707 at truegrounds (orig r2286):  alexmv | 2005-11-10 16:51:56 -0500
    
    
    
    * jesse and alex's hit list
    
    * made Jifty::Handle its own subclass
    
    r18718 at truegrounds (orig r2292):  alexmv | 2005-11-11 13:15:51 -0500
     r7112 at zoq-fot-pik:  chmrr | 2005-11-11 13:15:18 -0500
      * Validation rototill.  XML is now generated by /validator.xml not by
        random function calls.
    
    
    
    * Tiny style cleanups to render_messages
    
    
    * checkpoint
    
    * A bit of Jifty script command help cleanup
    
    * makefile deps update
    
    r18914 at truegrounds (orig r2321):  alexmv | 2005-11-15 18:25:25 -0500
     r7213 at zoq-fot-pik:  chmrr | 2005-11-15 18:24:42 -0500
      * No defaults from arguments which aren't columns
      * Save continuations after validation
      * Only redirect if not validating or success
    
    r18933 at truegrounds (orig r2331):  alexmv | 2005-11-17 15:16:39 -0500
     r7218 at zoq-fot-pik:  chmrr | 2005-11-17 15:11:57 -0500
      * Automagic password_confirm
    
    r18934 at truegrounds (orig r2332):  alexmv | 2005-11-17 15:16:47 -0500
     r7219 at zoq-fot-pik:  chmrr | 2005-11-17 15:12:23 -0500
      * Remove all references to bin/schema
    
    r18935 at truegrounds (orig r2333):  alexmv | 2005-11-17 15:16:53 -0500
     r7220 at zoq-fot-pik:  chmrr | 2005-11-17 15:12:45 -0500
      * Fix current_user bug ( was grabbing framework all the time)
    
    
    
    * Cleanups and warning avoidance and more correct monikerization
    
    * moved serial and mason out of the base class
    
    * Switched "framework" to "web"
    
    * Added the other new wiki components
    
    
    * Removed Text::Markdown from our dist, as it's on CPAN now
    
    * Recent changes
    
    * Added menu structure
    
    * Render menus if you are not logged in
    
    * Reverted men stuff
    
    
    * Reverted the change to nav
    
    r19038 at truegrounds (orig r2356):  alexmv | 2005-11-21 15:38:27 -0500
     r7288 at zoq-fot-pik:  chmrr | 2005-11-21 15:37:14 -0500
      * Pull mandatory from column
      * Move jifty stuff out of btdt_behavior.js
      * autocomplete cleanups
      * multiplace frobing
    
    
    
    r19050 at truegrounds (orig r2364):  alexmv | 2005-11-22 02:27:49 -0500
     r7299 at zoq-fot-pik:  chmrr | 2005-11-22 02:26:32 -0500
      * s/Jifty->web->form->add_action/Jifty->web->new_action/g
      * Paging on groups, news, invitations
      * 'Accept all' button for invites
    
    
    
    r19055 at truegrounds (orig r2367):  alexmv | 2005-11-22 16:15:53 -0500
     r7308 at zoq-fot-pik:  chmrr | 2005-11-22 16:14:59 -0500
      * Revert r2327
    
    r19056 at truegrounds (orig r2368):  alexmv | 2005-11-22 16:32:30 -0500
     r7312 at zoq-fot-pik:  chmrr | 2005-11-22 16:30:15 -0500
      * Updates only need to validate arguments that get submitted
    
    
    
    * Continuations - holy grail
    
    *removed crack
    
    r19055 at truegrounds (orig r2367):  alexmv | 2005-11-22 16:15:53 -0500
     r7308 at zoq-fot-pik:  chmrr | 2005-11-22 16:14:59 -0500
      * Revert r2327
    
    r19056 at truegrounds (orig r2368):  alexmv | 2005-11-22 16:32:30 -0500
     r7312 at zoq-fot-pik:  chmrr | 2005-11-22 16:30:15 -0500
      * Updates only need to validate arguments that get submitted
    
    r19065 at truegrounds (orig r2373):  alexmv | 2005-11-22 18:18:06 -0500
     r7319 at zoq-fot-pik:  chmrr | 2005-11-22 18:17:02 -0500
      * JiftyWiki -> Wifty because I like my tab completion, dammit
    
    r19066 at truegrounds (orig r2374):  alexmv | 2005-11-22 18:18:17 -0500
    
    
    
    * more wifty rename
    
    More rename
    
    * All sorts of updates to Wifty. It's starting to look like a real app
    
    
    * ClassLoader now autocreates ::Record, ::Collection and $AppName base classes
    
    * added rico
    
    * cleaning up css a bit
    
    
    * Added IE7. it almost improves IE
    
    r19347 at truegrounds (orig r2380):  alexmv | 2005-11-25 15:37:10 -0500
     r7348 at zoq-fot-pik:  chmrr | 2005-11-25 15:34:12 -0500
      * validate_argument(s?) -> _validate_argument$1 to avoid possible infinite look with arguments named "arguments" or "argument"
    
    r19348 at truegrounds (orig r2381):  alexmv | 2005-11-25 15:37:21 -0500
     r7349 at zoq-fot-pik:  chmrr | 2005-11-25 15:34:50 -0500
      * Use *latest* possible match when auto-guessing config
    
    
    
    * We really don't want to pull in mandatoryness from tasks
    
    
    * don't run the quicksearch if the user doesn't want to.
    
    * Don't create a task if the user doesn't put in a summary.
    
    * If the moniker the test looks for isn't there, carp.
    
    
    * more explicit test info
    
    * Look Ma! full support for SQLite
    
    * Switched to Markdown
    
    r19533 at truegrounds (orig r2400):  alexmv | 2005-11-29 02:05:55 -0500
     r7379 at zoq-fot-pik:  chmrr | 2005-11-29 02:04:44 -0500
      * Jifty::Web::Form::Link gutted; most of it now in Jifty::Web::Form::Clickable
      * Elements have key bindings, ids, labels, and classes
      * Changed everything to use new link syntax; "button" is gone
    
      * Tests for low-level continuations
      * Only make new continuations during CALL if we absolutely need to
      * Snip J:C-* out of request when it is saved into a continuation, so
        we don't infinite loop
    
      * Continuations non-raw API needs tests
      * Lots of docs needed
    
    r19534 at truegrounds (orig r2401):  alexmv | 2005-11-29 02:06:06 -0500
    
    
    
    * Added a BPS logo
    
    r19537 at truegrounds (orig r2402):  root | 2005-11-29 04:15:12 -0500
    * Fixed to cope with API changes (removed web->submit)
    
    r19538 at truegrounds (orig r2403):  root | 2005-11-29 04:21:42 -0500
    * "ApplicationName" not "Name"
    
    r19539 at truegrounds (orig r2404):  alexmv | 2005-11-29 15:18:17 -0500
     r7387 at zoq-fot-pik:  chmrr | 2005-11-29 12:52:14 -0500
      * Continuations don't need to specify paths for their requests all
        the time (eg, counter demo)
    
    r19540 at truegrounds (orig r2405):  alexmv | 2005-11-29 15:18:36 -0500
     r7388 at zoq-fot-pik:  chmrr | 2005-11-29 15:17:26 -0500
      * More Jifty continuation tests, using the API this time
      * Small contoinuation bugfixes
    
    r19541 at truegrounds (orig r2406):  alexmv | 2005-11-29 15:41:10 -0500
     r7391 at zoq-fot-pik:  chmrr | 2005-11-29 15:40:33 -0500
      * Login as continuations
    
    
    
    r19549 at truegrounds (orig r2407):  alexmv | 2005-11-29 18:57:52 -0500
     r7393 at zoq-fot-pik:  chmrr | 2005-11-29 18:57:08 -0500
      * Use new submit syntax
      * No more Rico (JS bugs)
      * New form syntax
      * Half-broken task review
    
    r19642 at truegrounds (orig r2408):  alexmv | 2005-11-30 16:48:44 -0500
     r7402 at zoq-fot-pik:  chmrr | 2005-11-30 16:47:48 -0500
      * Task review using continuations
    
    r19643 at truegrounds (orig r2409):  alexmv | 2005-11-30 16:48:52 -0500
     r7403 at zoq-fot-pik:  chmrr | 2005-11-30 16:48:10 -0500
      * Forgot control loop for task review

diff --git a/bin/jifty b/bin/jifty
new file mode 100755
index 0000000..4f653a5
--- /dev/null
+++ b/bin/jifty
@@ -0,0 +1,13 @@
+#!/usr/bin/perl
+use warnings;
+use strict;
+use File::Basename qw(dirname); 
+
+BEGIN {
+    my $dir = dirname(__FILE__); 
+    push @INC, "$dir/../lib";
+    push @INC, "$dir/../../Jifty/lib";
+}
+
+use Jifty::Script;
+Jifty::Script->dispatch();
diff --git a/etc/config.yml b/etc/config.yml
new file mode 100644
index 0000000..5812a3d
--- /dev/null
+++ b/etc/config.yml
@@ -0,0 +1,15 @@
+framework:
+  LogConfig: etc/btdt.log4perl.conf
+  Database:
+    Driver: Pg
+    Host: localhost
+    User: postgres
+    Version: 0.0.8
+    Password: ''
+    RequireSSL: 0
+#  Mailer: IO
+#  MailerArgs:
+#    - %log/mail.log%
+  SiteConfig: etc/site_config.yml
+application: 
+  MaxWurbles: 9
diff --git a/lib/Wifty/Bootstrap.pm b/lib/Wifty/Bootstrap.pm
new file mode 100644
index 0000000..f05c0d4
--- /dev/null
+++ b/lib/Wifty/Bootstrap.pm
@@ -0,0 +1,15 @@
+package Wifty::Bootstrap;
+
+use Wifty::Model::Page;
+sub run {
+    my $self = shift;
+
+    my $index = Wifty::Model::Page->new();
+    $index->create( name => 'home',
+                    content=> 'Welcome to your Wifty');
+
+
+}
+
+
+1;
diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
new file mode 100644
index 0000000..b6541f8
--- /dev/null
+++ b/lib/Wifty/Model/Page.pm
@@ -0,0 +1,106 @@
+package Wifty::Model::Page::Schema;
+use Jifty::DBI::Schema;
+
+column name => 
+    type is 'text',
+    is mandatory,
+    is distinct;
+
+column content =>
+    type is 'text',
+    label is 'Page content',
+    render_as 'textarea';
+
+column updated =>
+    type is 'timestamp',
+    since '0.0.6';
+
+
+#column revisions => refers_to Wifty::Model::Revision by 'page';
+
+package Wifty::Model::Page;
+use base qw/Wifty::Record/;
+use Text::Markdown;
+use HTML::Scrubber;
+
+sub wiki_content {
+    my $self     = shift;
+    my $content  = $self->content();
+    my $scrubber = HTML::Scrubber->new();
+
+    $scrubber->default(
+        0,
+        {   '*'   => 0,
+            id    => 1,
+            class => 1,
+            href  => qr{^(?:http:|ftp:|https:|/)}i,
+
+            # Match http, ftp and relative urls
+            face   => 1,
+            size   => 1,
+            target => 1
+        }
+    );
+
+    $scrubber->deny(qw[*]);
+    $scrubber->allow(
+        qw[A B U P BR I HR BR SMALL EM FONT SPAN DIV UL OL LI DL DT DD]);
+    $scrubber->comment(0);
+    return ( markdown( $scrubber->scrub( $self->content ) ) );
+
+}
+
+
+sub create {
+    my $self = shift;
+    my %args = (@_);
+    my $now = DateTime->now();
+    $args{'updated'} =  $now->ymd." ".$now->hms;
+    my ($id) = $self->SUPER::create(%args);
+    if ($self->id) { 
+        $self->_add_revision(%args);
+    }
+    return($id);
+}
+
+=head2 _add_revision 
+
+Adds a revision for this page. Called by  create and set_content
+
+=over
+
+=item content
+
+=back
+
+=cut
+
+sub _add_revision {
+    my $self = shift;
+    my %args = (@_);
+
+    my $rev = Wifty::Model::Revision->new();
+    $rev->create(
+        page    => $self->id,
+        content => $args{'content'}
+    );
+
+}
+
+sub set_content {
+    my $self = shift;
+    my $content = shift;
+    my ($val, $msg) = $self->SUPER::set_content($content);
+    $self->_add_revision(content =>$content );
+    return ($val,$msg);
+}
+
+sub _set {
+    my $self = shift;
+    my ($val,$msg) = $self->SUPER::_set(@_);
+    my $now = DateTime->now();
+    $self->SUPER::_set( column => 'updated', value =>   $now->ymd." ".$now->hms);
+    return ($val,$msg);
+}
+
+1;
diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
new file mode 100644
index 0000000..646519b
--- /dev/null
+++ b/lib/Wifty/Model/Revision.pm
@@ -0,0 +1,33 @@
+package Wifty::Model::Revision::Schema;
+use Jifty::DBI::Schema;
+
+column page  => 
+    refers_to Wifty::Model::Revision;
+
+column content =>
+    type is 'text',
+    render_as 'textarea';
+
+column created => 
+    type is 'timestamp';
+
+package Wifty::Model::Revision;
+use base qw/Wifty::Record/;
+
+use DateTime;
+
+
+sub since { '0.0.5' }
+
+
+sub create {
+    my $self = shift;
+    my %args = (@_);
+
+    my $now = DateTime->now();
+    $args{'created'} =  $now->ymd." ".$now->hms;
+    $self->SUPER::create(%args);
+
+}
+
+1;
diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
new file mode 100644
index 0000000..61cd92a
--- /dev/null
+++ b/lib/Wifty/Model/User.pm
@@ -0,0 +1,32 @@
+package Wifty::Model::User::Schema;
+use Jifty::DBI::Schema;
+
+column name => 
+    type is 'text',
+    is mandatory,
+    is distinct;
+
+column email =>
+    type is 'text',
+    is mandatory,
+    is distinct;
+
+column password =>,
+    type is 'text',
+    render_as 'password';
+
+
+
+package Wifty::Model::User;
+use base qw/Wifty::Record/;
+
+sub since {'0.0.7'}
+
+sub create {
+    my $self = shift;
+    my %args = (@_);
+    my ($id) = $self->SUPER::create(%args);
+    return($id);
+}
+
+1;
diff --git a/web/static/css/.base.css.swp b/web/static/css/.base.css.swp
new file mode 100644
index 0000000..79c4cf5
Binary files /dev/null and b/web/static/css/.base.css.swp differ
diff --git a/web/static/css/base.css b/web/static/css/base.css
new file mode 100644
index 0000000..df6ab06
--- /dev/null
+++ b/web/static/css/base.css
@@ -0,0 +1,111 @@
+body { 
+    background-color: #dddddd;
+
+
+}
+
+h1 {
+ background: green;
+ color: white;
+ padding: 0.2em;
+ border: 0;
+ margin: 0;
+ margin: -10px 0  0 -10px ;
+ margin-right: -10px;
+
+}
+
+a {
+ color: black;
+ font-style: bold;
+}
+
+#jifty-wait-message {
+ display: none;
+}
+
+div#menu {
+ background: green;
+
+}
+
+div#sidebar {
+ float: left;
+
+
+}
+
+div#content {
+    background: #ffffff;
+   padding: 2em;
+}
+
+
+ul.menu {
+    display: block;
+    border-bottom: 2px solid black;
+    padding-bottom: 4px;
+    width: 100%;
+    margin-left: 0px;
+    margin-right: 0px;
+
+}
+
+ul.menu li {
+
+    background: #770077;
+    color:#ffffff;
+    text-align: center;
+    display: inline;
+    padding: 0.4em 0.4em 4px 0.4em;
+    border-bottom: 2px solid black;
+;
+     
+}
+
+ul.menu li a {
+ color: #ffffff;
+
+}
+
+div#salutation {
+    float: right;
+    font-style: italic;
+}
+
+textarea.content {
+  height: 50em;
+  background: #ddd;
+  border: 1px solid black;
+  padding: 5px;
+}
+
+label {
+  position: absolute;
+  top: 3.4em;
+  left: 1em;
+  font-size: 2em;
+}
+
+input[type=submit] {
+    border: 1px solid black;
+    font-size: 2em;
+    margin: 5px;
+        
+}
+
+div#syntax {
+    float: right;
+    background: white;
+    border: 1px solid #333;
+    padding: 3px;
+    font-size: 0.8em;
+    width: 25%;
+    position: absolute;
+    top: 10em;
+    right: 2em;
+}
+
+
+
+
diff --git a/web/static/css/main.css b/web/static/css/main.css
new file mode 100644
index 0000000..db0e44c
--- /dev/null
+++ b/web/static/css/main.css
@@ -0,0 +1 @@
+ at import "base.css";
diff --git a/web/static/js/behaviour.js b/web/static/js/behaviour.js
new file mode 100644
index 0000000..bc5504f
--- /dev/null
+++ b/web/static/js/behaviour.js
@@ -0,0 +1,254 @@
+/*
+   Behaviour v1.1 by Ben Nolan, June 2005. Based largely on the work
+   of Simon Willison (see comments by Simon below).
+
+   Description:
+   	
+   	Uses css selectors to apply javascript behaviours to enable
+   	unobtrusive javascript in html documents.
+   	
+   Usage:   
+   
+	var myrules = {
+		'b.someclass' : function(element){
+			element.onclick = function(){
+				alert(this.innerHTML);
+			}
+		},
+		'#someid u' : function(element){
+			element.onmouseover = function(){
+				this.innerHTML = "BLAH!";
+			}
+		}
+	};
+	
+	Behaviour.register(myrules);
+	
+	// Call Behaviour.apply() to re-apply the rules (if you
+	// update the dom, etc).
+
+   License:
+   
+   	My stuff is BSD licensed. Not sure about Simon's.
+   	
+   More information:
+   	
+   	http://ripcord.co.nz/behaviour/
+   
+*/   
+
+var Behaviour = {
+	list : new Array,
+	
+	register : function(sheet){
+		Behaviour.list.push(sheet);
+	},
+	
+	start : function(){
+		Behaviour.addLoadEvent(function(){
+			Behaviour.apply();
+		});
+	},
+	
+	apply : function(){
+		for (h=0;sheet=Behaviour.list[h];h++){
+			for (selector in sheet){
+				list = document.getElementsBySelector(selector);
+				
+				if (!list){
+					continue;
+				}
+
+				for (i=0;element=list[i];i++){
+					sheet[selector](element);
+				}
+			}
+		}
+	},
+	
+	addLoadEvent : function(func){
+		var oldonload = window.onload;
+		
+		if (typeof window.onload != 'function') {
+			window.onload = func;
+		} else {
+			window.onload = function() {
+				oldonload();
+				func();
+			}
+		}
+	}
+}
+
+Behaviour.start();
+
+/*
+   The following code is Copyright (C) Simon Willison 2004.
+
+   document.getElementsBySelector(selector)
+   - returns an array of element objects from the current document
+     matching the CSS selector. Selectors can contain element names, 
+     class names and ids and can be nested. For example:
+     
+       elements = document.getElementsBySelect('div#main p a.external')
+     
+     Will return an array of all 'a' elements with 'external' in their 
+     class attribute that are contained inside 'p' elements that are 
+     contained inside the 'div' element which has id="main"
+
+   New in version 0.4: Support for CSS2 and CSS3 attribute selectors:
+   See http://www.w3.org/TR/css3-selectors/#attribute-selectors
+
+   Version 0.4 - Simon Willison, March 25th 2003
+   -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows
+   -- Opera 7 fails 
+*/
+
+function getAllChildren(e) {
+  // Returns all children of element. Workaround required for IE5/Windows. Ugh.
+  return e.all ? e.all : e.getElementsByTagName('*');
+}
+
+document.getElementsBySelector = function(selector) {
+  // Attempt to fail gracefully in lesser browsers
+  if (!document.getElementsByTagName) {
+    return new Array();
+  }
+  // Split selector in to tokens
+  var tokens = selector.split(' ');
+  var currentContext = new Array(document);
+  for (var i = 0; i < tokens.length; i++) {
+    token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');;
+    if (token.indexOf('#') > -1) {
+      // Token is an ID selector
+      var bits = token.split('#');
+      var tagName = bits[0];
+      var id = bits[1];
+      var element = document.getElementById(id);
+      if (tagName && element.nodeName.toLowerCase() != tagName) {
+        // tag with that ID not found, return false
+        return new Array();
+      }
+      // Set currentContext to contain just this element
+      currentContext = new Array(element);
+      continue; // Skip to next token
+    }
+    if (token.indexOf('.') > -1) {
+      // Token contains a class selector
+      var bits = token.split('.');
+      var tagName = bits[0];
+      var className = bits[1];
+      if (!tagName) {
+        tagName = '*';
+      }
+      // Get elements matching tag, filter them for class selector
+      var found = new Array;
+      var foundCount = 0;
+      for (var h = 0; h < currentContext.length; h++) {
+        var elements;
+        if (tagName == '*') {
+            elements = getAllChildren(currentContext[h]);
+        } else {
+            elements = currentContext[h].getElementsByTagName(tagName);
+        }
+        for (var j = 0; j < elements.length; j++) {
+          found[foundCount++] = elements[j];
+        }
+      }
+      currentContext = new Array;
+      var currentContextIndex = 0;
+      for (var k = 0; k < found.length; k++) {
+        if (found[k].className && found[k].className.match(new RegExp('\\b'+className+'\\b'))) {
+          currentContext[currentContextIndex++] = found[k];
+        }
+      }
+      continue; // Skip to next token
+    }
+    // Code to deal with attribute selectors
+    if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) {
+      var tagName = RegExp.$1;
+      var attrName = RegExp.$2;
+      var attrOperator = RegExp.$3;
+      var attrValue = RegExp.$4;
+      if (!tagName) {
+        tagName = '*';
+      }
+      // Grab all of the tagName elements within current context
+      var found = new Array;
+      var foundCount = 0;
+      for (var h = 0; h < currentContext.length; h++) {
+        var elements;
+        if (tagName == '*') {
+            elements = getAllChildren(currentContext[h]);
+        } else {
+            elements = currentContext[h].getElementsByTagName(tagName);
+        }
+        for (var j = 0; j < elements.length; j++) {
+          found[foundCount++] = elements[j];
+        }
+      }
+      currentContext = new Array;
+      var currentContextIndex = 0;
+      var checkFunction; // This function will be used to filter the elements
+      switch (attrOperator) {
+        case '=': // Equality
+          checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); };
+          break;
+        case '~': // Match one of space seperated words 
+          checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b'+attrValue+'\\b'))); };
+          break;
+        case '|': // Match start with value followed by optional hyphen
+          checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?'))); };
+          break;
+        case '^': // Match starts with value
+          checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) == 0); };
+          break;
+        case '$': // Match ends with value - fails with "Warning" in Opera 7
+          checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); };
+          break;
+        case '*': // Match ends with value
+          checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); };
+          break;
+        default :
+          // Just test for existence of attribute
+          checkFunction = function(e) { return e.getAttribute(attrName); };
+      }
+      currentContext = new Array;
+      var currentContextIndex = 0;
+      for (var k = 0; k < found.length; k++) {
+        if (checkFunction(found[k])) {
+          currentContext[currentContextIndex++] = found[k];
+        }
+      }
+      // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue);
+      continue; // Skip to next token
+    }
+    
+    if (!currentContext[0]){
+    	return;
+    }
+    
+    // If we get here, token is JUST an element (not a class or ID selector)
+    tagName = token;
+    var found = new Array;
+    var foundCount = 0;
+    for (var h = 0; h < currentContext.length; h++) {
+      var elements = currentContext[h].getElementsByTagName(tagName);
+      for (var j = 0; j < elements.length; j++) {
+        found[foundCount++] = elements[j];
+      }
+    }
+    currentContext = found;
+  }
+  return currentContext;
+}
+
+/* That revolting regular expression explained 
+/^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
+  \---/  \---/\-------------/    \-------/
+    |      |         |               |
+    |      |         |           The value
+    |      |    ~,|,^,$,* or =
+    |   Attribute 
+   Tag
+*/
diff --git a/web/static/js/bps_util.js b/web/static/js/bps_util.js
new file mode 100644
index 0000000..330c8a5
--- /dev/null
+++ b/web/static/js/bps_util.js
@@ -0,0 +1,89 @@
+// XXX TODO This library should likely be refactored to use behaviour
+
+function focusElementById(id) {
+    var e = document.getElementById(id);
+    if (e) e.focus();
+}
+
+function openCalWindow(field) {
+    var objWindow = window.open('/helpers/calendar.html?field='+field, 'Calendar', 'height=200,width=235,scrollbars=1');
+    objWindow.focus();
+}
+
+function updateParentField(field, value) {
+    if (window.opener) {
+        window.opener.document.getElementById(field).value = value;
+        window.close();
+    }
+}
+
+function createCalendarLink(input) {
+    var e = document.getElementById(input);
+    if (e) {
+        var link = document.createElement('a');
+        link.setAttribute('href', '#');
+        link.setAttribute('onclick', "openCalWindow('"+input+"'); return false;");
+        
+        var text = document.createTextNode('Calendar');
+        link.appendChild(text);
+        
+        var space = document.createTextNode(' ');
+        
+        e.parentNode.insertBefore(link, e.nextSibling);
+        e.parentNode.insertBefore(space, e.nextSibling);
+        
+        return true;
+    }
+    return false;
+}
+
+// onload handlers
+
+var onLoadStack     = new Array();
+var onLoadLastStack = new Array();
+var onLoadExecuted  = 0;
+
+function onLoadHook(commandStr) {
+    if(typeof(commandStr) == "string") {
+        onLoadStack[onLoadStack.length] = commandStr;
+        return true;
+    }
+    return false;
+}
+
+// some things *really* need to be done after everything else
+function onLoadLastHook(commandStr) {
+    if(typeof(commandStr) == "string"){
+        onLoadLastStack[onLoadLastStack.length] = commandStr;
+        return true;
+    }
+    return false;
+}
+
+function doOnLoadHooks() {
+    if(onLoadExecuted) return;
+    for (var x=0; x < onLoadStack.length; x++) { 
+        eval(onLoadStack[x]);
+    }
+    for (var x=0; x < onLoadLastStack.length; x++) { 
+        eval(onLoadLastStack[x]); 
+    }
+    onLoadExecuted = 1;
+}
+
+
+if (typeof window.onload != 'function') {
+    window.onload = doOnLoadHooks;
+} else {
+    var oldonload = window.onload;
+    
+    window.onload = function() {
+        oldonload();
+        doOnLoadHooks();
+    }
+}
+
+function jifty_button_click() {
+
+1;
+}
diff --git a/web/static/js/btdt_behaviour.js b/web/static/js/btdt_behaviour.js
new file mode 100644
index 0000000..7c3f3ec
--- /dev/null
+++ b/web/static/js/btdt_behaviour.js
@@ -0,0 +1,36 @@
+/* Uses Behaviour v1.0 (behaviour.js); see
+        http://ripcord.co.nz/behaviour/
+
+   IMPORTANT: if you make DOM changes that mean that an element
+              ought to gain or lose a behaviour, call Behaviour.apply()!
+   (Actually, that *won't* make something lose a behaviour, so if that's necessary
+    you'll need to have an empty "fallback".  Ie, if "div#foo a" should have a special
+    onclick and other "a" shouldn't, then there ought to be an explicit "a" style
+    that sets onclick to a trivial function, if DOM changes will ever happen.)
+   (Also, with the current behaviour.js, the order of application of styles is undefined,
+    so you can't really do cascading.  I've suggested to the author that he change it;
+    if he doesn't, but we need it, it's an easy change to make the sheets arrays instead
+    of Objects (hashes).  For now this can be dealt with by loading multiple sheets (register
+    calls), though.)
+*/
+
+
+
+/*    'textarea.bigbox' : function(elt) {
+  new Form.Element.Observer( elt.id,
+     1,
+         function( element, value ) {
+         new Ajax.Updater( elt.id+'-observer',
+         '/fragments/parsetext',
+         { parameters: Form.Element.getAction(elt).serialize(),
+           onComplete: function () { Behaviour.apply() } }
+       )
+     }
+         );
+    },
+*/
+
+var myrules = {
+                };
+        
+        Behaviour.register(myrules);
diff --git a/web/static/js/combobox.js b/web/static/js/combobox.js
new file mode 100644
index 0000000..90fcddf
--- /dev/null
+++ b/web/static/js/combobox.js
@@ -0,0 +1,233 @@
+function ComboBox_InitWith(n) {
+    if ( typeof( window.addEventListener ) != "undefined" ) {
+        window.addEventListener("load", ComboBox_Init(n), false);
+    } else if ( typeof( window.attachEvent ) != "undefined" ) {
+        window.attachEvent("onload", ComboBox_Init(n));
+    } else {
+        ComboBox_Init(n)();
+    }
+}
+function ComboBox_Init(n) {
+    return function () {
+        if ( ComboBox_UplevelBrowser( n ) ) {
+            ComboBox_Load( n );
+        }
+    }
+}
+function ComboBox_UplevelBrowser( n ) {
+    if( typeof( document.getElementById ) == "undefined" ) return false;
+    var combo = document.getElementById( n + "_Container" );
+    if( combo == null || typeof( combo ) == "undefined" ) return false;
+    if( typeof( combo.style ) == "undefined" ) return false;
+    if( typeof( combo.innerHTML ) == "undefined" ) return false;
+    return true;
+}
+function ComboBox_Load( comboId ) {
+    var combo  = document.getElementById( comboId + "_Container" );
+    var button = document.getElementById( comboId + "_Button" );
+    var list   = document.getElementById( comboId + "_List" );
+    var text   = document.getElementById( comboId );
+    
+    
+    combo.List = list;
+    combo.Button = button;
+    combo.Text = text;
+    
+    button.Container = combo;
+    button.Toggle = ComboBox_ToggleList;
+    button.onclick = button.Toggle;
+    button.onmouseover = function(e) { this.Container.List.DisableBlur(e); };
+    button.onmouseout = function(e) { this.Container.List.EnableBlur(e); };
+    button.innerHTML = "\u25BC";
+    button.onselectstart = function(e){ return false; };
+    button.style.height = ( list.offsetHeight - 4 ) + "px";
+    
+    text.Container = combo;
+    text.TypeDown = ComboBox_TextTypeDown;
+    text.KeyAccess = ComboBox_TextKeyAccess;
+    text.onkeyup = function(e) { this.KeyAccess(e); this.TypeDown(e); };
+    text.style.width = ( list.offsetWidth ) + "px";
+    
+    list.Container = combo;
+    list.Show = ComboBox_ShowList;
+    list.Hide = ComboBox_HideList;
+    list.EnableBlur = ComboBox_ListEnableBlur;
+    list.DisableBlur = ComboBox_ListDisableBlur;
+    list.Select = ComboBox_ListItemSelect;
+    list.ClearSelection = ComboBox_ListClearSelection;
+    list.KeyAccess = ComboBox_ListKeyAccess;
+    list.FireTextChange = ComboBox_ListFireTextChange;
+    list.onchange = null;
+    list.onclick = function(e){ this.Select(e); this.ClearSelection(); this.FireTextChange(); };
+    list.onkeyup = function(e) { this.KeyAccess(e); };
+    list.EnableBlur(null);
+    list.style.position = "absolute";
+    list.size = ComboBox_GetListSize( list );
+    list.IsShowing = true;
+    list.Hide();
+    
+}
+function ComboBox_InitEvent( e ) {
+    if( typeof( e ) == "undefined" && typeof( window.event ) != "undefined" ) e = window.event;
+    if( e == null ) e = new Object();
+    return e;
+}
+function ComboBox_ListClearSelection() {
+            if ( typeof( this.Container.Text.createTextRange ) == "undefined" ) return;
+    var rNew = this.Container.Text.createTextRange();
+    rNew.moveStart('character', this.Container.Text.value.length) ;
+    rNew.select();
+}
+function ComboBox_GetListSize( theList ) {
+    ComboBox_EnsureListSize( theList );
+    return theList.listSize;
+}
+function ComboBox_EnsureListSize( theList ) {
+    if ( typeof( theList.listSize ) == "undefined" ) {
+        if( typeof( theList.getAttribute ) != "undefined" ) {
+            if( theList.getAttribute( "listSize" ) != null && theList.getAttribute( "listSize" ) != "" ) {
+                theList.listSize = theList.getAttribute( "listSize" );
+                return;
+            }
+        }
+        if( theList.options.length > 0 ) {
+            theList.listSize = theList.options.length;
+            return;
+        }
+        theList.listSize = 4;
+    }
+}
+function ComboBox_ListKeyAccess(e) { //Make enter/space and escape do the right thing :)
+    e = ComboBox_InitEvent( e );
+    if( e.keyCode == 13 || e.keyCode == 32 ) {
+        this.Select();
+        return;
+    }
+    if( e.keyCode == 27 ) {
+        this.Hide();
+        this.Container.Text.focus();
+        return;
+    }
+}
+function ComboBox_TextKeyAccess(e) { //Make alt+arrow expand the list
+    e = ComboBox_InitEvent( e );
+    if( e.altKey && (e.keyCode == 38 || e.keyCode == 40) ) {
+            this.Container.List.Show();
+    }
+}
+function ComboBox_TextTypeDown(e) { //Make the textbox do a type-down on the list
+    e = ComboBox_InitEvent( e );
+    var items = this.Container.List.options;
+    if( this.value == "" ) return;
+    var ctrlKeys = Array( 8, 46, 37, 38, 39, 40, 33, 34, 35, 36, 45, 16, 20 );
+    for( var i = 0; i < ctrlKeys.length; i++ ) {
+        if( e.keyCode == ctrlKeys[i] ) return;
+    }
+    for( var i = 0; i < items.length; i++ ) {
+        var item = items[i];
+        if( item.text.toLowerCase().indexOf( this.value.toLowerCase() ) == 0 ) {
+            this.Container.List.selectedIndex = i;
+            if ( typeof( this.Container.Text.createTextRange ) != "undefined" ) {
+                                    this.Container.List.Select();
+                            }
+            break;
+        }
+    }
+}
+function ComboBox_ListFireTextChange() {
+    var textOnChange = this.Container.Text.onchange;
+            if ( textOnChange != null && typeof(textOnChange) == "function" ) {
+                    textOnChange();
+            }
+}
+function ComboBox_ListEnableBlur(e) {
+    this.onblur = this.Hide;
+}
+function ComboBox_ListDisableBlur(e) {
+    this.onblur = null;
+}
+function ComboBox_ListItemSelect(e) {
+    if( this.options.length > 0 ) {
+        var text = this.Container.Text;
+        var oldValue = text.value;
+        var newValue = this.options[ this.selectedIndex ].value;
+        text.value = newValue;
+        if ( typeof( text.createTextRange ) != "undefined" ) {
+            if (newValue != oldValue) {
+                var rNew = text.createTextRange();
+                rNew.moveStart('character', oldValue.length) ;
+                rNew.select();
+            }
+        }
+    }
+    this.Hide();
+    this.Container.Text.focus();
+}
+function ComboBox_ToggleList(e) {
+    if( this.Container.List.IsShowing == true ) {
+        this.Container.List.Hide();
+    } else {
+        this.Container.List.Show();
+    }
+}
+function ComboBox_ShowList(e) {
+    if ( !this.IsShowing && !this.disabled ) {
+        this.style.width = ( this.Container.offsetWidth ) + "px";
+        this.style.top = ( this.Container.offsetHeight + ComboBox_RecursiveOffsetTop(this.Container,true) ) + "px";
+        this.style.left = ( ComboBox_RecursiveOffsetLeft(this.Container,true) + 1 ) + "px";
+        ComboBox_SetVisibility(this,true);
+        this.focus();
+        this.IsShowing = true;
+    }
+}
+function ComboBox_HideList(e) {
+    if( this.IsShowing ) {
+                    ComboBox_SetVisibility(this,false);
+        this.IsShowing = false;
+    }
+}
+function ComboBox_SetVisibility(theList,isVisible) {
+    var isIE = ( typeof( theList.dataSrc ) != "undefined" ); // dataSrc is an IE-only property which is unlikely to be supported elsewhere
+    var ua = navigator.userAgent.toLowerCase(); 
+    var isSafari = (ua.indexOf('safari') != - 1);
+    if ( isIE || isSafari) {
+        if ( isVisible ) {
+            theList.style.visibility = "visible";
+        } else {
+            theList.style.visibility = "hidden";
+        }
+    } else { 
+        if ( isVisible ) {
+            theList.style.display = "block";
+        } else {
+            theList.style.display = "none";
+        }
+    }
+}
+function ComboBox_RecursiveOffsetTop(thisObject,isFirst) {
+    if(thisObject.offsetParent) {
+        if ( thisObject.style.position == "absolute" && !isFirst && typeof(document.designMode) != "undefined" ) {
+            return 0;
+        }
+        return (thisObject.offsetTop + ComboBox_RecursiveOffsetTop(thisObject.offsetParent,false));
+    } else {
+        return thisObject.offsetTop;
+    }
+}
+function ComboBox_RecursiveOffsetLeft(thisObject,isFirst) {
+    if(thisObject.offsetParent) {
+        if ( thisObject.style.position == "absolute" && !isFirst && typeof(document.designMode) != "undefined" ) {
+            return 0;
+        }
+        return (thisObject.offsetLeft + ComboBox_RecursiveOffsetLeft(thisObject.offsetParent,false));
+    } else {
+        return thisObject.offsetLeft;
+    }
+}
+function ComboBox_SimpleAttach(selectElement,textElement) {
+    textElement.value = selectElement.options[ selectElement.options.selectedIndex ].value;
+    var textOnChange = textElement.onchange;
+    if ( textOnChange != null && typeof( textOnChange ) == "function" ) {
+        textOnChange();
+    }
+}
diff --git a/web/static/js/dom-drag.js b/web/static/js/dom-drag.js
new file mode 100644
index 0000000..9fb7118
--- /dev/null
+++ b/web/static/js/dom-drag.js
@@ -0,0 +1,9 @@
+/**************************************************
+ * dom-drag.js
+ * 09.25.2001
+ * www.youngpup.net
+ **************************************************
+ * 10.28.2001 - fixed minor bug where events
+ * sometimes fired off the handle, not the root.
+ **************************************************/
+var Drag = { obj : null, init : function(o, oRoot, minX, maxX, minY, maxY, bSwapHorzRef, bSwapVertRef, fXMapper, fYMapper){ o.onmousedown = Drag.start; o.hmode = bSwapHorzRef ? false : true ; o.vmode = bSwapVertRef ? false : true ; o.root = oRoot && oRoot != null ? oRoot : o ; if (o.hmode && isNaN(parseInt(o.root.style.left ))) o.root.style.left = "0px"; if (o.vmode && isNaN(parseInt(o.root.style.top ))) o.root.style.top = "0px"; if (!o.hmode && isNaN(parseInt(o.root.style.right ))) o.root.style.right = "0px"; if (!o.vmode && isNaN(parseInt(o.root.style.bottom))) o.root.style.bottom = "0px"; o.minX = typeof minX != 'undefined' ? minX : null; o.minY = typeof minY != 'undefined' ? minY : null; o.maxX = typeof maxX != 'undefined' ? maxX : null; o.maxY = typeof maxY != 'undefined' ? maxY : null; o.xMapper = fXMapper ? fXMapper : null; o.yMapper = fYMapper ? fYMapper : null; o.root.onDragStart = new Function(); o.root.onDragEnd = new Function(); o.root.onDrag = new Function();}, 
 start : function(e){ var o = Drag.obj = this; e = Drag.fixE(e); var y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom); var x = parseInt(o.hmode ? o.root.style.left : o.root.style.right ); o.root.onDragStart(x, y); o.lastMouseX = e.clientX; o.lastMouseY = e.clientY; if (o.hmode) { if (o.minX != null) o.minMouseX = e.clientX - x + o.minX; if (o.maxX != null) o.maxMouseX = o.minMouseX + o.maxX - o.minX;} else { if (o.minX != null) o.maxMouseX = -o.minX + e.clientX + x; if (o.maxX != null) o.minMouseX = -o.maxX + e.clientX + x;} if (o.vmode) { if (o.minY != null) o.minMouseY = e.clientY - y + o.minY; if (o.maxY != null) o.maxMouseY = o.minMouseY + o.maxY - o.minY;} else { if (o.minY != null) o.maxMouseY = -o.minY + e.clientY + y; if (o.maxY != null) o.minMouseY = -o.maxY + e.clientY + y;} document.onmousemove = Drag.drag; document.onmouseup = Drag.end; return false;}, drag : function(e){ e = Drag.fixE(e); var o = Drag.obj; var ey = e.clientY; var ex = e.clientX; var
  y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom); var x = parseInt(o.hmode ? o.root.style.left : o.root.style.right ); var nx, ny; if (o.minX != null) ex = o.hmode ? Math.max(ex, o.minMouseX) : Math.min(ex, o.maxMouseX); if (o.maxX != null) ex = o.hmode ? Math.min(ex, o.maxMouseX) : Math.max(ex, o.minMouseX); if (o.minY != null) ey = o.vmode ? Math.max(ey, o.minMouseY) : Math.min(ey, o.maxMouseY); if (o.maxY != null) ey = o.vmode ? Math.min(ey, o.maxMouseY) : Math.max(ey, o.minMouseY); nx = x + ((ex - o.lastMouseX) * (o.hmode ? 1 : -1)); ny = y + ((ey - o.lastMouseY) * (o.vmode ? 1 : -1)); if(o.xMapper){ nx = o.xMapper(y) } else if (o.yMapper) { ny = o.yMapper(x); } Drag.obj.root.style[o.hmode ? "left" : "right"] = nx + "px"; Drag.obj.root.style[o.vmode ? "top" : "bottom"] = ny + "px"; Drag.obj.lastMouseX = ex; Drag.obj.lastMouseY = ey; Drag.obj.root.onDrag(nx, ny); return false;}, end : function(){ document.onmousemove = null; document.onmouseup = null; Drag.
 obj.root.onDragEnd( parseInt(Drag.obj.root.style[Drag.obj.hmode ? "left" : "right"]), parseInt(Drag.obj.root.style[Drag.obj.vmode ? "top" : "bottom"])); Drag.obj = null;}, fixE : function(e){ if (typeof e == 'undefined') e = window.event; if (typeof e.layerX == 'undefined') e.layerX = e.offsetX; if (typeof e.layerY == 'undefined') e.layerY = e.offsetY; return e;} };
\ No newline at end of file
diff --git a/web/static/js/ie7/README.txt b/web/static/js/ie7/README.txt
new file mode 100644
index 0000000..e546ce4
--- /dev/null
+++ b/web/static/js/ie7/README.txt
@@ -0,0 +1,34 @@
+Installation
+------------
+
+Follow these simple instructions to get IE7 working immediately on your server:
+
+ * download the latest IE7 ZIP file (https://sourceforge.net/project/showfiles.php?group_id=109983&package_id=119707)
+
+ * extract the contents to a directory on your server (keep the folder names used in the ZIP)
+
+ * you will now have an IE7 directory on your server
+
+ * include the IE7 JavaScript library in the page you wish to test
+
+   <!-- compliance patch for microsoft browsers -->
+   <!--[if lt IE 7]><script src="/ie7/ie7-standard-p.js" type="text/javascript"></script><![endif]-->
+
+ * make sure this also points to the same directory
+
+ * open the page in your web browser
+
+ * the page should now be IE7 enabled.
+
+ * if you are using the PNG solution then be aware that it operates on files
+   names "something-trans.png"
+
+ * see this page for more configuration and usage options:
+   http://dean.edwards.name/IE7/usage/
+
+You may extract the contents of the ZIP file to your hard disk if you do not have access to a web server.
+
+
+Enjoy ;-)
+
+Dean Edwards, 23rd May 2005
diff --git a/web/static/js/ie7/blank.gif b/web/static/js/ie7/blank.gif
new file mode 100644
index 0000000..a4fe2e6
Binary files /dev/null and b/web/static/js/ie7/blank.gif differ
diff --git a/web/static/js/ie7/ie7-base64.php b/web/static/js/ie7/ie7-base64.php
new file mode 100644
index 0000000..530392d
--- /dev/null
+++ b/web/static/js/ie7/ie7-base64.php
@@ -0,0 +1,7 @@
+<?php
+$data = split(";", $_SERVER["REDIRECT_QUERY_STRING"]);
+$type = $data[0];
+$data = split(",", $data[1]);
+header("Content-type: ".$type);
+echo base64_decode($data[1]);
+?>
\ No newline at end of file
diff --git a/web/static/js/ie7/ie7-content.htc b/web/static/js/ie7/ie7-content.htc
new file mode 100644
index 0000000..cc480cb
--- /dev/null
+++ b/web/static/js/ie7/ie7-content.htc
@@ -0,0 +1,14 @@
+<html>
+<!--
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+-->
+<head>
+<object id="dummy" width="0" height="0"></object>
+<base id="base">
+<style type="text/css">html,body,img{margin:0;}img{vertical-align:top}#dummy{display:inline}</style>
+<script type="text/javascript">public_description=new function(){var l=false;this.ie7_anon=true;this.load=function(o,c,u){if(l)return;l=true;base.href=o.document.URL;dummy.style.cssText=c;var _0=o.parentElement;var _1=Boolean(dummy.currentStyle.display=="inline");function r(){o.runtimeStyle.width=(_1)?image.offsetWidth:"100%";o.runtimeStyle.height=body.offsetHeight};image.onreadystatechange=function(){if(this.readyState=="complete")_2()};image.src=u;function _2(){function copy(p){try{body.style[p]=_0.currentStyle[p]}catch(i){}};for(var j in body.currentStyle)copy(j);body.style.width="";body.style.height="";body.style.border="none";body.style.padding="0";body.style.margin="0";body.style.textIndent="";body.style.position="static";while(_0&&_0.currentStyle.backgroundColor=="transparent"){_0=_0.parentElement}if(_0)document.body.style.backgroundColor=_0.currentStyle.backgroundColor;body.runtimeStyle.cssText=c;body.runtimeStyle.margin="0";if(_1)body.runtimeStyle.width="";r()}}};</
 script>
+</head>
+<body><span id="body"><img id="image"></span></body>
+</html>
diff --git a/web/static/js/ie7/ie7-core.js b/web/static/js/ie7/ie7-core.js
new file mode 100644
index 0000000..c3dbcef
--- /dev/null
+++ b/web/static/js/ie7/ie7-core.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('x(!1M.1j)z 6(){1l{1M.1j=8;4 1W=8.2m=z 26;8.O=6(){7"1j 2q 0.9 (5G)"};4 36=/36/.B(42.41.40);4 1G=(36)?6(m){1M.1G(1j+"\\n\\n"+m)}:1W;4 2t=5F.2t.1g(/5E (\\d\\.\\d)/)[1];4 2z=K.5D!="5C";x(/5B/.B(42.41.40)||2t<5||!/^5A/.B(K.1J.2A))7;4 1H=K.39=="1H";4 1t,5z;4 1J=K.1J,2s,3X,17=K.17;4 5y="!";4 22={};4 1u=1C;1j.2m=6(n,s){x(!22[n]){x(1u)1k("s="+2o(s));22[n]=z s()}};4 R=/^[\\w\\.]+[^:]*$/;6 1I(h,p){x(R.B(h))h=(p||"")+h;7 h};6 2e(h,p){h=1I(h,p);7 h.1d(0,h.3n("/")+1)};4 s=K.3Z[K.3Z.y-1];1l{1k(s.3z)}1i(i){}4 1R=2e(s.5x);4 1F;1l{4 l=(5w()>=5)?"5v":"5u";1F=z 5t(l+".5s")}1i(i){}4 2w={};6 2y(h,p){1l{h=1I(h,p);x(!2w[h]){1F.5r("5q",h,1C);1F.5p();x(1F.3Y==0||1F.3Y==5o){2w[h]=1
 F.5n}}}1i(i){1G("2x [1]: 30 5m 5l "+h)}37{7 2w[h]||""}};4 5k=1I("5j.5i",1R);6 1E(V){x(V!=1L){V.2v=13.16.2v;V.12=13.16.12}7 V};1E.12=6(p,c){x(!p)p={};x(!c)c=p.J;x(c=={}.J)c=z 26("8.2v()");c.Y=z 26("7 8");c.Y.16=z 8.Y;c.Y.16.12(p);c.16=z c.Y;c.Y.16.J=c.16.J=c;c.1r=8;c.12=F.32;c.2u=8.2u;7 c};1E.Y=z 26("7 8");1E.Y.16={J:1E,2v:6(){7 F.32.5h.1r.2k(8,F)},12:6(V){x(8==8.J.16&&8.J.12){7 8.J.Y.16.12(V)}D(4 i 5g V){2K(i){1o"J":1o"O":1o"Y":2X}x(2V V[i]=="6"&&V[i]!=8[i]){V[i].1r=8[i]}8[i]=V[i]}x(V.O!=8.O&&V.O!={}.O){V.O.1r=8.O;8.O=V.O}7 8}};6 13(){};8.13=1E.12({J:13,O:6(){7"[5f "+(8.J.2Z||"5e")+"]"},5d:6(1h){7 8.J==1h||1h.2u(8.J)}});13.2Z="13";13.1r=1L;13.2u=6(1h){1f(1h&&1h.1r!=8)1h=1h.1r;7 3J(1h)};13.Y.1r=1E;2a 8.13;4 3A=13.12({J:6(){8.5c=[];8.1p=[]},1s:1W});x(2t<5.5)1k(2y("Z-5b.3a",1R));4 35=1C;1j.1s=6(){1l{x(35)7;35=1H=1c;2s=K.2s;3X=(2z)?2s:1J;x(1K&&1t)1t.2k();15.2k();1n();1G("1u 5a")}1i(e){1G("2x [2]: "+e.38)}};4 1p=[];6 2C(r){1p.11(r)};6 1n(){H.3P();x(1K&&1t)1t.1n();15.1n();D(4 i=0;
 i<1p.y;i++)1p[i]()};6 23(){4 E=0,R=1,L=2;4 G=/\\(/g,S=/\\$\\d/,I=/^\\$\\d+$/,T=/([\'"])\\1\\+(.*)\\+\\1\\1$/,3Q=/\\\\./g,Q=/\'/,3W=/\\25[^\\25]*\\25/g;4 1X=8;8.18=6(e,r){x(!r)r="";4 l=(34(2o(e)).1g(G)||"").y+1;x(S.B(r)){x(I.B(r)){r=3e(r.1d(1))-1}1b{4 i=l;4 q=Q.B(34(r))?\'"\':"\'";1f(i)r=r.2S("$"+i--).2p(q+"+a[o+"+i+"]+"+q);r=z 26("a,o","7"+q+r.19(T,"$1")+q)}}3V(e||"/^$/",r,l)};8.1U=6(s){24.y=0;7 3R(3S(s,8.2r).19(z 1Z(1D,8.33?"2I":"g"),3T),8.2r).19(3W,"")};8.59=6(){1D.y=0};4 24=[];4 1D=[];4 3U=6(){7"("+2o(8[E]).1d(1,-1)+")"};1D.O=6(){7 8.2p("|")};6 3V(){F.O=3U;1D[1D.y]=F}6 3T(){x(!F[0])7"";4 i=1,j=0,p;1f(p=1D[j++]){x(F[i]){4 r=p[R];2K(2V r){1o"6":7 r(F,i);1o"58":7 F[r+i]}4 d=(F[i].57(1X.2r)==-1)?"":"\\25"+F[i]+"\\25";7 d+r}1b i+=p[L]}};6 3S(s,e){7 e?s.19(z 1Z("\\\\"+e+"(.)","g"),6(m,c){24[24.y]=c;7 e}):s};6 3R(s,e){4 i=0;7 e?s.19(z 1Z("\\\\"+e,"g"),6(){7 e+(24[i++]||"")}):s};6 34(s){7 s.19(3Q,"")}};23.16={J:23,33:1C,2r:""};13.12(23.16);4 1V=23.12({33:1c});4 H=6(){4 2q="2.0.2"
 ;4 C=/\\s*,\\s*/;4 H=6(s,14){1l{4 m=[];4 u=F.32.2Q&&!14;4 b=(14)?(14.J==3G)?14:[14]:[K];4 31=3D(s).2S(C),i;D(i=0;i<31.y;i++){s=2R(31[i]);x(3K&&s.1d(0,3).2p("")==" *#"){s=s.1d(2);14=3H([],b,s[1])}1b 14=b;4 j=0,t,f,a,c="";1f(j<s.y){t=s[j++];f=s[j++];c+=t+f;a="";x(s[j]=="("){1f(s[j++]!=")"&&j<s.y){a+=s[j]}a=a.1d(0,-1);c+="("+a+")"}14=(u&&1P[c])?1P[c]:3F(14,t,f,a);x(u)1P[c]=14}m=m.3t(14)}2a H.30;7 m}1i(e){H.30=e;7[]}};H.O=6(){7"6 H() {\\n  [2q "+2q+"]\\n}"};4 1P={};H.2Q=1C;H.3P=6(s){x(s){s=2R(s).2p("");2a 1P[s]}1b 1P={}};4 22={};4 1u=1C;H.2m=6(n,s){x(1u)1k("s="+2o(s));22[n]=z s()};H.Y=6(c){7 c?1k(c):8};4 1B={};4 2n={};4 56={1g:/\\[([\\w-]+(\\|[\\w-]+)?)\\s*(\\W?=)?\\s*([^\\]]*)\\]/};4 55=[];1B[" "]=6(r,f,t,n){4 e,i,j;D(i=0;i<f.y;i++){4 s=2l(f[i],t,n);D(j=0;(e=s[j]);j++){x(1q(e)&&2T(e,n))r.11(e)}}};1B["#"]=6(r,f,i){4 e,j;D(j=0;(e=f[j]);j++)x(e.1a==i)r.11(e)};1B["."]=6(r,f,c){c=z 1Z("(^|\\\\s)"+c+"(\\\\s|$)");4 e,i;D(i=0;(e=f[i]);i++)x(c.B(e.2Z))r.11(e)};1B[":"]=6(r,f,p,a){4 t=2n[
 p],e,i;x(t)D(i=0;(e=f[i]);i++)x(t(e,a))r.11(e)};2n["21"]=6(e){4 d=2U(e);x(d.2Y)D(4 i=0;i<d.2Y.y;i++){x(d.2Y[i]==e)7 1c}};2n["2N"]=6(e){};4 1q=6(e){7(e&&e.3B==1&&e.2P!="!")?e:1L};4 3N=6(e){1f(e&&(e=e.54)&&!1q(e))2X;7 e};4 2W=6(e){1f(e&&(e=e.53)&&!1q(e))2X;7 e};4 3L=6(e){7 1q(e.3O)||2W(e.3O)};4 52=6(e){7 1q(e.3M)||3N(e.3M)};4 51=6(e){4 c=[];e=3L(e);1f(e){c.11(e);e=2W(e)}7 c};4 3K=1c;4 2O=6(e){4 d=2U(e);7(2V d.3I=="50")?/\\.4Z$/i.B(d.4Y):3J(d.3I=="4X 4W")};4 2U=6(e){7 e.4V||e.K};4 2l=6(e,t){7(t=="*"&&e.1A)?e.1A:e.2l(t)};4 4U=6(e,t,n){x(t=="*")7 1q(e);x(!2T(e,n))7 1C;x(!2O(e))t=t.4T();7 e.2P==t};4 2T=6(e,n){7!n||(n=="*")||(e.4S==n)};4 4R=6(e){7 e.4Q};6 3H(r,f,1a){4 m,i,j;D(i=0;i<f.y;i++){x(m=f[i].1A.4P(1a)){x(m.1a==1a)r.11(m);1b x(m.y!=1L){D(j=0;j<m.y;j++){x(m[j].1a==1a)r.11(m[j])}}}}7 r};x(![].11)3G.16.11=6(){D(4 i=0;i<F.y;i++){8[8.y]=F[i]}7 8.y};4 N=/\\|/;6 3F(14,t,f,a){x(N.B(f)){f=f.2S(N);a=f[0];f=f[1]}4 r=[];x(1B[t]){1B[t](r,14,f,a)}7 r};4 S=/^[^\\s>+~]/;4 3E=/[\\s#.:>+~()@]
 |[^\\s#.:>+~()@]+/g;6 2R(s){x(S.B(s))s=" "+s;7 s.1g(3E)||[]};4 W=/\\s*([\\s>+~(),]|^|$)\\s*/g;4 I=/([\\s>+~,]|[^(]\\+|^)([#.:@])/g;4 3D=6(s){7 s.19(W,"$1").19(I,"$1*$2")};4 1y={O:6(){7"\'"},1g:/^(\'[^\']*\')|("[^"]*")$/,B:6(s){7 8.1g.B(s)},18:6(s){7 8.B(s)?s:8+s+8},3C:6(s){7 8.B(s)?s.1d(1,-1):s}};4 1N=6(t){7 1y.3C(t)};4 E=/([\\/()[\\]?{}|*+-])/g;6 4O(s){7 s.19(E,"\\\\$1")};1u=1c;7 H}();H.2Q=1c;H.2m("Z",6(){1q=6(e){7(e&&e.3B==1&&e.2P!="!"&&!e.3d)?e:1L}});H.Y("1N=F[1]",3k);4 1K=!H.Y("2O(F[1])",1J);4 2h=":21{Z-21:21}:2N{Z-21:2N}"+(1K?"":"*{4N:0}");4 15=z(3A.12({2F:z 1V,1O:"",1w:"",2L:[],1s:6(){8.2M();8.2g()},2g:6(){15.1Y.X=2h+8.1O+8.1w},3y:6(){4 20=K.2l("1e"),s;D(4 i=20.y-1;(s=20[i]);i--){x(!s.2H&&!s.Z){8.2L.11(s.3z)}}},2k:6(){8.3y();8.2g();z 28("1O");8.3u()},3w:6(e,r){8.2F.18(e,r)},1n:6(){4 R=/3v\\d+/g;4 s=2h.1g(/[{,]/g).y;4 20=s+(8.1O.X.1g(/\\{/g)||"").y;4 3x=8.1Y.4M,r;4 2j,c,2i,e,i,j,k,1a;D(i=s;i<20;i++){r=3x[i];x(r&&(2j=r.1e.X.1g(R))){2i=H(r.4L);x(2i.y)D(j=0;j<2j.y;j++){1a=
 2j[j];c=15.1p[1a.1d(10)][2];D(k=0;(e=2i[k]);k++){x(e.1v[1a])c(e)}}}}},2C:6(p,t,h,r){t=z 1Z("([{;\\\\s])"+p+"\\\\s*:\\\\s*"+t+"[^;}]*");4 i=8.1p.y;x(r)r=p+":"+r;8.3w(t,6(m,o){7(r?m[o+1]+r:m[o])+";Z-"+m[o].1d(1)+";3v"+i+":1"});8.1p.11(F);7 i},1N:6(s){7 s.X||""},2M:6(){x(1H||!1K)K.2M();1b K.4K("<1e Z=1c></1e>");8.1Y=17[17.y-1];8.1Y.Z=1c;8.1Y.X=2h},3u:6(){D(4 i=0;i<17.y;i++){x(!17[i].Z&&17[i].X){17[i].X=""}}}}));6 28(m){8.1z=m;8.1S();15[m]=8;15.2g()};13.12({J:28,O:6(){7"@1z "+8.1z+"{"+8.X+"}"},1n:1W,1S:6(){8.X="";8.1N();8.3m();8.X=3j(8.X);f={}},1N:6(){4 3r=[].3t(15.2L);4 M=/@1z\\s+([^{]*)\\{([^@]+\\})\\s*\\}/2I;4 A=/\\4J\\b|^$/i,S=/\\4I\\b/i,P=/\\4H\\b/i;6 3q(c,m){2f.v=m;7 c.19(M,2f)};6 2f(4G,m,c){m=2J(m);2K(m){1o"1O":1o"1w":x(m!=2f.v)7"";1o"1A":7 c}7""};6 2J(m){x(A.B(m))7"1A";1b x(S.B(m))7(P.B(m))?"1A":"1O";1b x(P.B(m))7"1w"};4 1X=8;6 2G(s,p,m,l){4 c="";x(!l){m=2J(s.1z);l=0}x(m=="1A"||m==1X.1z){x(l<3){D(4 i=0;i<s.3s.y;i++){c+=2G(s.3s[i],2e(s.2d,p),m,l+1)}}c+=3l(s.2d?3p(s,p):3r.
 3h()||"");c=3q(c,1X.1z)}7 c};4 f={};6 3p(s,p){4 u=1I(s.2d,p);x(f[u])7"";f[u]=(s.2H)?"":3o(15.1N(s,p),2e(s.2d,p));7 f[u]};4 U=/(4F\\s*\\(\\s*[\'"]?)([\\w\\.]+[^:\\)]*[\'"]?\\))/2I;6 3o(c,p){7 c.19(U,"$1"+p.1d(0,p.3n("/")+1)+"$2")};D(4 i=0;i<17.y;i++){x(!17[i].2H&&!17[i].Z){8.X+=2G(17[i])}}},3m:6(){8.X=15.2F.1U(8.X)},1n:1W});4 1y=H.Y("1y");4 2b=[];6 3l(c){7 1x.1U(2c.1U(c))};6 2E(m,o){7 1y+(2b.11(m[o])-1)+1y};6 3k(v){7 1y.B(v)?1k(2b[1k(v)]):v};4 1x=z 1V;1x.18(/\\/\\*[^*]*\\*+([^\\/][^*]*\\*+)*\\//);1x.18(/\'[^\']*\'/,2E);1x.18(/"[^"]*"/,2E);1x.18(/\\s+/," ");1x.18(/@(4E|4D)[^;\\n]+[;\\n]|<!\\-\\-|\\-\\->/);4 2c=z 1V;2c.18(/\\\\\'/,"\\\\4C");2c.18(/\\\\"/,"\\\\4B");4 2D=z 1V;2D.18(/\'(\\d+)\'/,3i);6 3j(c){7 2D.1U(c)};6 3i(m,o){7 2b[m[o+1]]};4 2B=[];6 4A(h){2C(h);1Q(1M,"4z",h)};6 1Q(e,t,h){e.4y(t,h);2B.11(F)};6 3g(e,t,h){1l{e.4x(t,h)}1i(i){}};1Q(1M,"4w",6(){4 h;1f(h=2B.3h()){3g(h[0],h[1],h[2])}});6 4v(h,e,c){x(!h.29)h.29={};x(c)h.29[e.2A]=e;1b 2a h.29[e.2A];7 c};1Q(1M,"4u",6(){x(
 !15.1w)z 28("1w");15.1w.1n()});4 3f=/^\\d+(4t)?$/i;4 4s=/^\\d+%$/;4 4r=6(e,v){x(3f.B(v))7 3e(v);4 s=e.1e.1m;4 r=e.1T.1m;e.1T.1m=e.1v.1m;e.1e.1m=v||0;v=e.1e.4q;e.1e.1m=s;e.1T.1m=r;7 v};6 4p(t){4 e=K.4o(t||"4n");e.1e.X="3c:4m;4l:0;4k:4j;4i:4h;4g:4f(0 0 0 0);1m:-4e";e.3d=1c;7 e};4 27="Z-";6 4d(e){7 e.1v["Z-3c"]=="4c"};6 4b(e,p){7 e.1v[27+p]||e.1v[p]};6 4a(e,p,v){x(e.1v[27+p]==1L){e.1T[27+p]=e.1v[p]}e.1T[p]=v};6 49(o,c,u){4 t=48(6(){1l{x(!o.1S)7;o.1S(o,c,u);3b(t)}1i(i){3b(t)}},10)};1u=1c;x(2z)1k(2y("Z-47.3a",1R));15.1s();x(1K&&1t)1t.1s();x(1H)1j.1s();1b{1J.46(1I("Z-1S.45",1R));1Q(K,"44",6(){x(K.39=="1H")43(1j.1s,0)})}}1i(e){1G("2x [0]: "+e.38)}37{}};',62,353,'||||var||function|return|this|||||||||||||||||||||||||if|length|new||test||for||arguments||cssQuery||constructor|document||||toString|||||||that||cssText|valueOf|ie7||push|specialize|Common|fr|ie7CSS|prototype|styleSheets|add|replace|id|else|true|slice|style|while|match|klass|catch|IE7|eval|try|left|recalc|case|recalcs|this
 Element|ancestor|init|ie7HTML|loaded|currentStyle|print|encoder|Quote|media|all|selectors|false|_0|ICommon|httpRequest|alert|complete|makePath|documentElement|isHTML|null|window|getText|screen|cache|addEventHandler|path|load|runtimeStyle|exec|Parser|DUMMY|self|styleSheet|RegExp|st|link|modules|ParseMaster|_1|x01|Function|_2|StyleSheet|elements|delete|_3|safeString|href|getPath|_4|refresh|HEADER|el|ca|apply|getElementsByTagName|addModule|pseudoClasses|String|join|version|escapeChar|body|appVersion|ancestorOf|inherit|_5|Error|loadFile|quirksMode|uniqueID|_6|addRecalc|decoder|_7|parser|_8|disabled|gi|_9|switch|styles|createStyleSheet|visited|isXML|tagName|caching|_10|split|compareNamespace|getDocument|typeof|nextElementSibling|continue|links|className|error|se|callee|ignoreCase|_11|_12|ie7_debug|finally|description|readyState|js|clearInterval|position|ie7_anon|parseInt|PIXEL|removeEventHandler|pop|_13|decode|getString|_14|parse|lastIndexOf|_15|_16|_17|_18|imports|concat|trash|i
 e7_recalc|addFix|ru|getInlineStyles|innerHTML|Fix|nodeType|remove|parseSelector|ST|select|Array|_19|mimeType|Boolean|isMSIE|firstElementChild|lastChild|previousElementSibling|firstChild|clearCache|ES|_20|_21|_22|_23|_24|DE|viewport|status|scripts|search|location|top|setTimeout|onreadystatechange|htc|addBehavior|quirks|setInterval|addTimer|setOverrideStyle|getDefinedStyle|fixed|isFixed|9999|rect|clip|none|border|block|display|padding|absolute|object|createElement|createTempElement|pixelLeft|getPixelValue|PERCENT|px|onbeforeprint|register|onunload|detachEvent|attachEvent|onresize|addResize|x22|x27|import|namespace|url|ma|bprint|bscreen|ball|write|selectorText|rules|margin|regEscape|item|innerText|getTextContent|scopeName|toUpperCase|compareTagName|ownerDocument|Document|XML|URL|xml|unknown|childElements|lastElementChild|nextSibling|previousSibling|attributeSelectors|AttributeSelector|indexOf|number|reset|successfully|ie5|fixes|instanceOf|Object|common|in|caller|gif|blank|BLANK
 _GIF|file|loading|responseText|200|send|GET|open|XMLHTTP|ActiveXObject|Microsoft|Msxml2|ScriptEngineMajorVersion|src|ANON|ie7Layout|ms_|ie7_off|CSS1Compat|compatMode|MSIE|navigator|alpha'.split('|'),0,{}))
diff --git a/web/static/js/ie7/ie7-css-strict.js b/web/static/js/ie7/ie7-css-strict.js
new file mode 100644
index 0000000..0c7e330
--- /dev/null
+++ b/web/static/js/ie7/ie7-css-strict.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+IE7.addModule("ie7-css-strict",function(){if(!modules["ie7-css2-selectors"])return;StyleSheet.prototype.specialize({parse:function(){this.inherit();var r=[].concat(this.rules);r.sort(ie7CSS.Rule.compare);this.cssText=r.join("\n")},createRule:function(s,c){var m;if(m=s.match(ie7CSS.PseudoElement.MATCH))return new ie7CSS.PseudoElement(m[1],m[2],c);else if(m=s.match(ie7CSS.DynamicRule.MATCH))return new ie7CSS.DynamicRule(s,m[1],m[2],m[3],c);else return new ie7CSS.Rule(s,c)}});ie7CSS.specialize({apply:function(){this.inherit();this.Rule.MATCH=/([^{}]+)(\{[^{}]*\})/g}});ie7CSS.Rule.compare=function(r1,r2){return r1.specificity-r2.specificity};var N=[],I=/#/g,C=/[.:\[]/g,T=/^\w|[\s>+~]\w/g;ie7CSS.Rule.score=function(s){return(s.match(I)||N).length*10000+(s.match(C)||N).length*100+(s.match(T)||N).length};ie7CSS.Rule.simple=function(){return""};ie7CSS.Rule.prototype.specialize({specificity:0,init:function(){this.specificity=ie7CSS.Rule.score(this.selector)}})});
diff --git a/web/static/js/ie7/ie7-css2-selectors.js b/web/static/js/ie7/ie7-css2-selectors.js
new file mode 100644
index 0000000..bb08da3
--- /dev/null
+++ b/web/static/js/ie7/ie7-css2-selectors.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+IE7.addModule("ie7-css2-selectors",function(){cssQuery.addModule("css-level2",function(){selectors[">"]=function(r,f,t,n){var e,i,j;for(i=0;i<f.length;i++){var s=childElements(f[i]);for(j=0;(e=s[j]);j++)if(compareTagName(e,t,n))r.push(e)}};selectors["+"]=function(r,f,t,n){for(var i=0;i<f.length;i++){var e=nextElementSibling(f[i]);if(e&&compareTagName(e,t,n))r.push(e)}};selectors["@"]=function(r,f,a){var t=attributeSelectors[a].test;var e,i;for(i=0;(e=f[i]);i++)if(t(e))r.push(e)};pseudoClasses["first-child"]=function(e){return!previousElementSibling(e)};pseudoClasses["lang"]=function(e,c){c=new RegExp("^"+c,"i");while(e&&!e.getAttribute("lang"))e=e.parentNode;return e&&c.test(e.getAttribute("lang"))};AttributeSelector.NS_IE=/\\:/g;AttributeSelector.PREFIX="@";AttributeSelector.tests={};AttributeSelector.replace=function(m,a,n,c,v){var k=this.PREFIX+m;if(!attributeSelectors[k]){a=this.create(a,c||"",v||"");attributeSelectors[k]=a;attributeSelectors.push(a)}return attributeSele
 ctors[k].id};AttributeSelector.parse=function(s){s=s.replace(this.NS_IE,"|");var m;while(m=s.match(this.match)){var r=this.replace(m[0],m[1],m[2],m[3],m[4]);s=s.replace(this.match,r)}return s};AttributeSelector.create=function(p,t,v){var a={};a.id=this.PREFIX+attributeSelectors.length;a.name=p;t=this.tests[t];t=t?t(this.getAttribute(p),getText(v)):false;a.test=new Function("e","return "+t);return a};AttributeSelector.getAttribute=function(n){switch(n.toLowerCase()){case"id":return"e.id";case"class":return"e.className";case"for":return"e.htmlFor";case"href":if(isMSIE){return"String((e.outerHTML.match(/href=\\x22?([^\\s\\x22]*)\\x22?/)||[])[1]||'')"}}return"e.getAttribute('"+n.replace(N,":")+"')"};AttributeSelector.tests[""]=function(a){return a};AttributeSelector.tests["="]=function(a,v){return a+"=="+Quote.add(v)};AttributeSelector.tests["~="]=function(a,v){return"/(^| )"+regEscape(v)+"( |$)/.test("+a+")"};AttributeSelector.tests["|="]=function(a,v){return"/^"+regEscape(v)+"
 (-|$)/.test("+a+")"};var _6=parseSelector;parseSelector=function(s){return _6(AttributeSelector.parse(s))}});var AttributeSelector=cssQuery.valueOf("AttributeSelector");var H=/a(#[\w-]+)?(\.[\w-]+)?:(hover|active)/i;var B1=/\s*\{\s*/,B2=/\s*\}\s*/,C=/\s*\,\s*/;var F=/(.*)(:first-(line|letter))/;StyleSheet.prototype.specialize({parse:function(){this.inherit();var o=ie7CSS.rules.length;var ru=this.cssText.split(B2),r;var se,c,i,j;for(i=0;i<ru.length;i++){r=ru[i].split(B1);se=r[0].split(C);c=r[1];for(j=0;j<se.length;j++){se[j]=c?this.createRule(se[j],c):""}ru[i]=se.join("\n")}this.cssText=ru.join("\n");this.rules=ie7CSS.rules.slice(o)},recalc:function(){var r,i;for(i=0;(r=this.rules[i]);i++)r.recalc()},createRule:function(s,c){if(ie7CSS.UNKNOWN.test(s)){var m;if(m=s.match(PseudoElement.MATCH)){return new PseudoElement(m[1],m[2],c)}else if(m=s.match(DynamicRule.MATCH)){if(!isHTML||!H.test(m)||DynamicRule.COMPLEX.test(m)){return new DynamicRule(s,m[1],m[2],m[3],c)}}else return ne
 w Rule(s,c)}return s+" {"+c+"}"}});ie7CSS.specialize({rules:[],pseudoClasses:cssQuery.valueOf("pseudoClasses"),dynamicPseudoClasses:{},cache:cssQuery.valueOf("cache"),Rule:Rule,DynamicRule:DynamicRule,PseudoElement:PseudoElement,DynamicPseudoClass:DynamicPseudoClass,apply:function(){var p=this.pseudoClasses+"|before|after|"+this.dynamicPseudoClasses;p=p.replace(/(link|visited)\|/g,"");this.UNKNOWN=new RegExp("[>+~\[]|([:.])[\\w-()]+\\1|:("+p+")");var c="[^\\s(]+\\s*[+~]|@\\d+|:(";Rule.COMPLEX=new RegExp(c+p+")","g");DynamicRule.COMPLEX=new RegExp(c+this.pseudoClasses+")","g");DynamicRule.MATCH=new RegExp("(.*):("+this.dynamicPseudoClasses+")(.*)");PseudoElement.MATCH=/(.*):(before|after).*/;this.inherit()},recalc:function(){this.screen.recalc();this.inherit()},getText:function(s,p){return httpRequest?(loadFile(s.href,p)||s.cssText):this.inherit(s)},addEventHandler:function(e,t,h){addEventHandler(e,t,h)}});function Rule(s,c){this.id=ie7CSS.rules.length;this.className=Rule.PRE
 FIX+this.id;s=(s).match(F)||s||"*";this.selector=s[1]||s;this.selectorText=Rule.simple(this.selector)+"."+this.className+(s[2]||"");this.cssText=c;this.MATCH=new RegExp("\\s"+this.className+"(\\s|$)","g");ie7CSS.rules.push(this);this.init()};Common.specialize({constructor:Rule,toString:function(){return this.selectorText+" {"+this.cssText+"}"},init:DUMMY,add:function(e){e.className+=" "+this.className},remove:function(e){e.className=e.className.replace(this.MATCH,"$1")},recalc:function(){var m=ie7CSS.cache[" *."+this.className]=cssQuery(this.selector);for(i=0;i<m.length;i++)this.add(m[i])}});Rule.PREFIX="ie7_class";Rule.CHILD=/>/g;Rule.simple=function(s){s=AttributeSelector.parse(s);return s.replace(this.COMPLEX,"").replace(this.CHILD," ")};function DynamicRule(s,a,d,t,c){this.attach=a||"*";this.dynamicPseudoClass=ie7CSS.dynamicPseudoClasses[d];this.target=t;this.inherit(s,c)};Rule.specialize({constructor:DynamicRule,recalc:function(){var m=cssQuery(this.attach);for(var i=0;
 i<m.length;i++){var t=(this.target)?cssQuery(this.target,m[i]):[m[i]];if(t.length)this.dynamicPseudoClass.apply(m[i],t,this)}}});var A=/^attr/;var U=/^url\s*\(\s*([^)]*)\)$/;var M={before0:"beforeBegin",before1:"afterBegin",after0:"afterEnd",after1:"beforeEnd"};var _5=makePath("ie7-content.htc",path)+"?";HEADER+=".ie7_anon{display:none}";function PseudoElement(s,p,c){this.position=p;var co=c.match(PseudoElement.CONTENT),m,e;if(co){co=co[1];m=co.split(/\s+/);for(var i=0;(e=m[i]);i++){m[i]=A.test(e)?{attr:e.slice(5,-1)}:(e.charAt(0)=="'")?getString(e):decode(e)}co=m}this.content=co;this.inherit(s,decode(c))};Rule.specialize({constructor:PseudoElement,toString:function(){return"."+this.className+"{display:inline}"},init:function(){this.match=cssQuery(this.selector);for(var i=0;i<this.match.length;i++){var r=this.match[i].runtimeStyle;if(!r[this.position])r[this.position]={cssText:""};r[this.position].cssText+=";"+this.cssText;if(this.content!=null)r[this.position].content=this.
 content}},recalc:function(){if(this.content==null)return;for(var i=0;i<this.match.length;i++){this.create(this.match[i])}},create:function(t){var g=t.runtimeStyle[this.position];if(g){var c=[].concat(g.content||"");for(var j=0;j<c.length;j++){if(typeof c[j]=="object"){c[j]=t.getAttribute(c[j].attr)}}c=c.join("");var u=c.match(U);var h=PseudoElement[u?"OBJECT":"ANON"].replace(/%1/,this.className);var cs=g.cssText.replace(/'/g,'"');var po=M[this.position+Number(t.canHaveChildren)];if(u){var p=document.createElement(h);t.insertAdjacentElement(po,p);p.data=_5;addTimer(p,cs,Quote.remove(u[1]))}else{h=h.replace(/%2/,cs).replace(/%3/,c);t.insertAdjacentHTML(po,h)}t.runtimeStyle[this.position]=null}}});PseudoElement.CONTENT=/content\s*:\s*([^;]*)(;|$)/;PseudoElement.OBJECT="<object class='ie7_anon %1' ie7_anon width=100% height=0 type=text/x-scriptlet>";PseudoElement.ANON="<ie7:! class='ie7_anon %1' ie7_anon style='%2'>%3</ie7:!>";function DynamicPseudoClass(n,a){this.name=n;this.ap
 ply=a;this.instances={};ie7CSS.dynamicPseudoClasses[n]=this};Common.specialize({constructor:DynamicPseudoClass,register:function(i){var c=i[2];i.id=c.id+i[0].uniqueID;if(!this.instances[i.id]){var t=i[1],j;for(j=0;j<t.length;j++)c.add(t[j]);this.instances[i.id]=i}},unregister:function(i){if(this.instances[i.id]){var c=i[2];var t=i[1],j;for(j=0;j<t.length;j++)c.remove(t[j]);delete this.instances[i.id]}}});ie7CSS.pseudoClasses.toString=function(){var t=[],p;for(p in this){if(this[p].length>1)p+="\\([^)]*\\)";t.push(p)}return t.join("|")};ie7CSS.pseudoClasses["link"]=function(e){return e.currentStyle["ie7-link"]=="link"};ie7CSS.pseudoClasses["visited"]=function(e){return e.currentStyle["ie7-link"]=="visited"};var _4=(appVersion<5.5)?"onmouseover":"onmouseenter";var _3=(appVersion<5.5)?"onmouseout":"onmouseleave";ie7CSS.dynamicPseudoClasses.toString=ie7CSS.pseudoClasses.toString;var _0=new DynamicPseudoClass("hover",function(e){var i=arguments;ie7CSS.addEventHandler(e,_4,functio
 n(){_0.register(i)});ie7CSS.addEventHandler(e,_3,function(){_0.unregister(i)})});var _1=new DynamicPseudoClass("focus",function(e){var i=arguments;ie7CSS.addEventHandler(e,"onfocus",function(){_1.unregister(i);_1.register(i)});ie7CSS.addEventHandler(e,"onblur",function(){_1.unregister(i)});if(e==document.activeElement){_1.register(i)}});var _2=new DynamicPseudoClass("active",function(e){var i=arguments;ie7CSS.addEventHandler(e,"onmousedown",function(){_2.register(i)})});addEventHandler(document,"onmouseup",function(){var i=_2.instances,j;for(j in i)_2.unregister(i[j]);i=_0.instances;for(j in i)if(!i[j][0].contains(event.srcElement))_0.unregister(i[j])});ICommon(AttributeSelector);AttributeSelector.specialize({getAttribute:function(n){switch(n.toLowerCase()){case"class":return"e.className.replace(/\\b\\s*ie7_class\\d+/g,'')";case"src":return"(e.pngSrc||e.src)"}return this.inherit(n)}});encoder.add(/::/,":");safeString.add(/\\([\da-fA-F]{1,4})/,function(m,o){m=m[o+1];return"\\
 u"+"0000".slice(m.length)+m})});
diff --git a/web/static/js/ie7/ie7-css3-selectors.js b/web/static/js/ie7/ie7-css3-selectors.js
new file mode 100644
index 0000000..7337b82
--- /dev/null
+++ b/web/static/js/ie7/ie7-css3-selectors.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+IE7.addModule("ie7-css3-selectors",function(){cssQuery.addModule("css-level3",function(){selectors["~"]=function(r,f,t,n){var e,i;for(i=0;(e=f[i]);i++){while(e=nextElementSibling(e)){if(compareTagName(e,t,n))r.push(e)}}};pseudoClasses["contains"]=function(e,t){t=new RegExp(regEscape(getText(t)));return t.test(getTextContent(e))};pseudoClasses["root"]=function(e){return e==getDocument(e).documentElement};pseudoClasses["empty"]=function(e){var n,i;for(i=0;(n=e.childNodes[i]);i++){if(thisElement(n)||n.nodeType==3)return false}return true};pseudoClasses["last-child"]=function(e){return!nextElementSibling(e)};pseudoClasses["only-child"]=function(e){e=e.parentNode;return firstElementChild(e)==lastElementChild(e)};pseudoClasses["not"]=function(e,s){var n=cssQuery(s,getDocument(e));for(var i=0;i<n.length;i++){if(n[i]==e)return false}return true};pseudoClasses["nth-child"]=function(e,a){return nthChild(e,a,previousElementSibling)};pseudoClasses["nth-last-child"]=function(e,a){return 
 nthChild(e,a,nextElementSibling)};pseudoClasses["target"]=function(e){return e.id==location.hash.slice(1)};pseudoClasses["checked"]=function(e){return e.checked};pseudoClasses["enabled"]=function(e){return e.disabled===false};pseudoClasses["disabled"]=function(e){return e.disabled};pseudoClasses["indeterminate"]=function(e){return e.indeterminate};AttributeSelector.tests["^="]=function(a,v){return"/^"+regEscape(v)+"/.test("+a+")"};AttributeSelector.tests["$="]=function(a,v){return"/"+regEscape(v)+"$/.test("+a+")"};AttributeSelector.tests["*="]=function(a,v){return"/"+regEscape(v)+"/.test("+a+")"};function nthChild(e,a,t){switch(a){case"n":return true;case"even":a="2n";break;case"odd":a="2n+1"}var ch=childElements(e.parentNode);function _5(i){var i=(t==nextElementSibling)?ch.length-i:i-1;return ch[i]==e};if(!isNaN(a))return _5(a);a=a.split("n");var m=parseInt(a[0]);var s=parseInt(a[1]);if((isNaN(m)||m==1)&&s==0)return true;if(m==0&&!isNaN(s))return _5(s);if(isNaN(s))s=0;var c
 =1;while(e=t(e))c++;if(isNaN(m)||m==1)return(t==nextElementSibling)?(c<=s):(s>=c);return(c%m)==s}});var firstElementChild=cssQuery.valueOf("firstElementChild");ie7CSS.pseudoClasses["root"]=function(e){return(e==viewport)||(!isHTML&&e==firstElementChild(body))};var _4=new ie7CSS.DynamicPseudoClass("checked",function(e){if(typeof e.checked!="boolean")return;var i=arguments;ie7CSS.addEventHandler(e,"onpropertychange",function(){if(event.propertyName=="checked"){if(e.checked)_4.register(i);else _4.unregister(i)}});if(e.checked)_4.register(i)});var _3=new ie7CSS.DynamicPseudoClass("enabled",function(e){if(typeof e.disabled!="boolean")return;var i=arguments;ie7CSS.addEventHandler(e,"onpropertychange",function(){if(event.propertyName=="disabled"){if(!e.isDisabled)_3.register(i);else _3.unregister(i)}});if(!e.isDisabled)_3.register(i)});var _2=new ie7CSS.DynamicPseudoClass("disabled",function(e){if(typeof e.disabled!="boolean")return;var i=arguments;ie7CSS.addEventHandler(e,"onprope
 rtychange",function(){if(event.propertyName=="disabled"){if(e.isDisabled)_2.register(i);else _2.unregister(i)}});if(e.isDisabled)_2.register(i)});var _1=new ie7CSS.DynamicPseudoClass("indeterminate",function(e){if(typeof e.indeterminate!="boolean")return;var i=arguments;ie7CSS.addEventHandler(e,"onpropertychange",function(){if(event.propertyName=="indeterminate"){if(e.indeterminate)_1.register(i);else _1.unregister(i)}});ie7CSS.addEventHandler(e,"onclick",function(){_1.unregister(i)})});var _0=new ie7CSS.DynamicPseudoClass("target",function(e){var i=arguments;if(!e.tabIndex)e.tabIndex=0;ie7CSS.addEventHandler(document,"onpropertychange",function(){if(event.propertyName=="activeElement"){if(e.id==location.hash.slice(1))_0.register(i);else _0.unregister(i)}});if(e.id==location.hash.slice(1))_0.register(i)});decoder.add(/\|/,"\\:")});
diff --git a/web/static/js/ie7/ie7-dhtml.js b/web/static/js/ie7/ie7-dhtml.js
new file mode 100644
index 0000000..d768063
--- /dev/null
+++ b/web/static/js/ie7/ie7-dhtml.js
@@ -0,0 +1,57 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+IE7.addModule("ie7-dhtml", function() {
+
+/* ---------------------------------------------------------------------
+  This module is still in development and should not be used.
+--------------------------------------------------------------------- */
+
+ie7CSS.specialize("recalc", function() {
+	this.inherit();
+	for (var i = 0; i < this.recalcs.length; i++) {
+		var $recalc = this.recalcs[i];
+		for (var j = 0; i < $recalc[3].length; i++) {
+			_addPropertyChangeHandler($recalc[3][j], _getPropertyName($recalc[2]), $recalc[1]);
+		}
+	}
+});
+
+// constants
+var _PATTERNS = {
+	width: "(width|paddingLeft|paddingRight|borderLeftWidth|borderRightWidth|borderLeftStyle|borderRightStyle)",
+	height:	"(height|paddingTop|paddingBottom|borderTopHeight|borderBottomHeight|borderTopStyle|borderBottomStyle)"
+};
+var _PROPERTY_NAMES = {
+	width: "fixedWidth",
+	height: "fixedHeight",
+	right: "width",
+	bottom: "height"
+};
+var _DASH_LETTER = /-(\w)/g;
+var _PROPERTY_NAME = /\w+/;
+
+function _addPropertyChangeHandler($element, $propertyName, $fix) {
+	addEventHandler($element, "onpropertychange", function() {
+		if (_getPattern($propertyName).test(event.propertyName)) {
+			_reset($element, $propertyName);
+			$fix($element);
+		}
+	});
+};
+function _upper($match, $letter) {return $letter.toUpperCase()};
+function _getPropertyName($pattern) {
+	return String(String($pattern).toLowerCase().replace(_DASH_LETTER, _upper).match(_PROPERTY_NAME));
+};
+function _getPattern($propertyName) {
+	return eval("/^style." + (_PATTERNS[$propertyName] || $propertyName) + "$/");
+};
+function _reset($element, $propertyName) {
+	$element.runtimeStyle[$propertyName] = "";
+	$propertyName = _PROPERTY_NAMES[$propertyName]
+	if ($propertyName) $element.runtimeStyle[$propertyName] = "";
+};
+
+});
diff --git a/web/static/js/ie7/ie7-dynamic-attributes.js b/web/static/js/ie7/ie7-dynamic-attributes.js
new file mode 100644
index 0000000..e066911
--- /dev/null
+++ b/web/static/js/ie7/ie7-dynamic-attributes.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+IE7.addModule("ie7-dynamic-attributes",function(){if(!modules["ie7-css2-selectors"])return;var attributeSelectors=cssQuery.valueOf("attributeSelectors");var parseSelector=cssQuery.valueOf("parseSelector");function DynamicAttribute(s,a,d,t,c){this.attach=a||"*";parseSelector(d);this.dynamicAttribute=attributeSelectors["@"+d];this.target=t;this.inherit(s,c)};ie7CSS.Rule.specialize({constructor:DynamicAttribute,recalc:function(){var m=cssQuery(this.attach);for(var i=0;i<m.length;i++){var t=(this.target)?cssQuery(this.target,m[i]):[m[i]];if(t.length)this.apply(m[i],t)}},apply:function(e,t){var self=this;addEventHandler(e,"onpropertychange",function(){if(event.propertyName==self.dynamicAttribute.name)self.test(e,t)});this.test(e,t)},test:function(e,t){var a=this.dynamicAttribute.test(e)?"add":"remove";for(var i=0;(e=t[i]);i++)this[a](e)}});DynamicAttribute.MATCH=/(.*)(\[[^\]]*\])(.*)/;StyleSheet.prototype.specialize({createRule:function(s,c){var m;if(m=s.match(DynamicAttribute.MA
 TCH)){return new DynamicAttribute(s,m[1],m[2],m[3],c)}else return this.inherit(s,c)}})});
diff --git a/web/static/js/ie7/ie7-fixed.js b/web/static/js/ie7/ie7-fixed.js
new file mode 100644
index 0000000..10d629f
--- /dev/null
+++ b/web/static/js/ie7/ie7-fixed.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+IE7.addModule("ie7-fixed",function(){ie7CSS.addRecalc("position","fixed",_6,"absolute");ie7CSS.addRecalc("background(-attachment)?","[^};]*fixed",_7);var _10=(quirksMode)?"body":"documentElement";var _8=function(){if(body.currentStyle.backgroundAttachment!="fixed"){if(body.currentStyle.backgroundImage=="none"){body.runtimeStyle.backgroundRepeat="no-repeat";body.runtimeStyle.backgroundImage="url("+BLANK_GIF+")"}body.runtimeStyle.backgroundAttachment="fixed"}_8=DUMMY};var _0=createTempElement("img");function _1(f){return _2.exec(String(f))};var _2=new ParseMaster;_2.add(/Left/,"Top");_2.add(/left/,"top");_2.add(/Width/,"Height");_2.add(/width/,"height");_2.add(/right/,"bottom");_2.add(/X/,"Y");function _3(e){return(e)?isFixed(e)||_3(e.parentElement):false};function setExpression(e,p,ex){setTimeout("document.all."+e.uniqueID+".runtimeStyle.setExpression('"+p+"','"+ex+"')",0)};function _7(e){if(register(_7,e,e.currentStyle.backgroundAttachment=="fixed"&&!e.contains(body))){_8();
 backgroundLeft(e);backgroundTop(e);_9(e)}};function _9(e){_0.src=e.currentStyle.backgroundImage.slice(5,-2);var p=(e.canHaveChildren)?e:e.parentElement;p.appendChild(_0);setOffsetLeft(e);setOffsetTop(e);p.removeChild(_0)};function backgroundLeft(e){e.style.backgroundPositionX=e.currentStyle.backgroundPositionX;if(!_3(e)){var ex="(parseInt(runtimeStyle.offsetLeft)+document."+_10+".scrollLeft)||0";setExpression(e,"backgroundPositionX",ex)}};eval(_1(backgroundLeft));function setOffsetLeft(e){var p=_3(e)?"backgroundPositionX":"offsetLeft";e.runtimeStyle[p]=getOffsetLeft(e,e.style.backgroundPositionX)-e.getBoundingClientRect().left-e.clientLeft+2};eval(_1(setOffsetLeft));function getOffsetLeft(e,p){switch(p){case"left":case"top":return 0;case"right":case"bottom":return viewport.clientWidth-_0.offsetWidth;case"center":return(viewport.clientWidth-_0.offsetWidth)/2;default:if(PERCENT.test(p)){return parseInt((viewport.clientWidth-_0.offsetWidth)*parseFloat(p)/100)}_0.style.left=p;re
 turn _0.offsetLeft}};eval(_1(getOffsetLeft));function _6(e){if(register(_6,e,isFixed(e))){setOverrideStyle(e,"position","absolute");setOverrideStyle(e,"left",e.currentStyle.left);setOverrideStyle(e,"top",e.currentStyle.top);_8();if(ie7Layout)ie7Layout.fixRight(e);_5(e)}};function _5(e,r){positionTop(e,r);positionLeft(e,r,true);if(!e.runtimeStyle.autoLeft&&e.currentStyle.marginLeft=="auto"&&e.currentStyle.right!="auto"){var l=viewport.clientWidth-getPixelWidth(e,e.currentStyle.right)-getPixelWidth(e,e.runtimeStyle._12)-e.clientWidth;if(e.currentStyle.marginRight=="auto")l=parseInt(l/2);if(_3(e.offsetParent))e.runtimeStyle.pixelLeft+=l;else e.runtimeStyle.shiftLeft=l}clipWidth(e);clipHeight(e)};function clipWidth(e){if(e.currentStyle.width!="auto"){var r=e.getBoundingClientRect();var w=e.offsetWidth-viewport.clientWidth+r.left-2;if(w>=0){w=Math.max(getPixelValue(e,e.currentStyle.width)-w,0);setOverrideStyle(e,"width",w)}}};eval(_1(clipWidth));function positionLeft(e,r){if(!r&&
 PERCENT.test(e.currentStyle.width)){e.runtimeStyle.fixWidth=e.currentStyle.width}if(e.runtimeStyle.fixWidth){e.runtimeStyle.width=getPixelWidth(e,e.runtimeStyle.fixWidth)}if(r){if(!e.runtimeStyle.autoLeft)return}else{e.runtimeStyle.shiftLeft=0;e.runtimeStyle._12=e.currentStyle.left;e.runtimeStyle.autoLeft=e.currentStyle.right!="auto"&&e.currentStyle.left=="auto"}e.runtimeStyle.left="";e.runtimeStyle.screenLeft=getScreenLeft(e);e.runtimeStyle.pixelLeft=e.runtimeStyle.screenLeft;if(!r&&!_3(e.offsetParent)){var ex="runtimeStyle.screenLeft+runtimeStyle.shiftLeft+document."+_10+".scrollLeft";setExpression(e,"pixelLeft",ex)}};eval(_1(positionLeft));function getScreenLeft(e){var s=e.offsetLeft,n=1;if(e.runtimeStyle.autoLeft){s=viewport.clientWidth-e.offsetWidth-getPixelWidth(e,e.currentStyle.right)}if(e.currentStyle.marginLeft!="auto"){s-=getPixelWidth(e,e.currentStyle.marginLeft)}while(e=e.offsetParent){if(e.currentStyle.position!="static")n=-1;s+=e.offsetLeft*n}return s};eval(_1(
 getScreenLeft));function getPixelWidth(e,v){if(PERCENT.test(v))return parseInt(parseFloat(v)/100*viewport.clientWidth);return getPixelValue(e,v)};eval(_1(getPixelWidth));function _11(){var e=_7.elements;for(var i in e)_9(e[i]);e=_6.elements;for(i in e){_5(e[i],true);_5(e[i],true)}_4=0};var _4;addResize(function(){if(!_4)_4=setTimeout(_11,0)})});
\ No newline at end of file
diff --git a/web/static/js/ie7/ie7-graphics.js b/web/static/js/ie7/ie7-graphics.js
new file mode 100644
index 0000000..7e63c47
--- /dev/null
+++ b/web/static/js/ie7/ie7-graphics.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+IE7.addModule("ie7-graphics",function(){if(appVersion<5.5)return;var A="DXImageTransform.Microsoft.AlphaImageLoader";var F="progid:"+A+"(src='%1',sizingMethod='scale')";var _3=new RegExp((window.IE7_PNG_SUFFIX||"-trans.png")+"$","i");var _0=[];function _2(e){var f=e.filters[A];if(f){f.src=e.src;f.enabled=true}else{e.runtimeStyle.filter=F.replace(/%1/,e.src);_0.push(e)}e.src=BLANK_GIF};function _5(e){e.src=e.pngSrc;e.filters[A].enabled=false};ie7CSS.addFix(/opacity\s*:\s*([\d.]+)/,function(m,o){return"zoom:1;filter:progid:DXImageTransform.Microsoft.Alpha(opacity="+((parseFloat(m[o+1])*100)||1)+")"});var B=/background(-image)?\s*:\s*([^\(};]*)url\(([^\)]+)\)([^;}]*)/;ie7CSS.addFix(B,function(m,o){var u=getString(m[o+3]);return _3.test(u)?"filter:"+F.replace(/scale/,"crop").replace(/%1/,u)+";zoom:1;background"+(m[o+1]||"")+":"+(m[o+2]||"")+"none"+(m[o+4]||""):m[o]});if(ie7HTML){ie7HTML.addRecalc("img,input",function(e){if(e.tagName=="INPUT"&&e.type!="image")return;_4(e);addEven
 tHandler(e,"onpropertychange",function(){if(!_1&&event.propertyName=="src"&&e.src.indexOf(BLANK_GIF)==-1)_4(e)})});var B64=/^data:.*;base64/i;var _7=makePath("ie7-base64.php",path);function _4(e){if(_3.test(e.src)){var i=new Image(e.width,e.height);i.onload=function(){e.width=i.width;e.height=i.height;i=null};i.src=e.src;e.pngSrc=e.src;_2(e)}else if(B64.test(e.src)){e.src=_7+"?"+e.src.slice(5)}};var I=/^image/i;var _6=makePath("ie7-object.htc",path);ie7HTML.addRecalc("object",function(e){if(I.test(e.type)){var o=document.createElement("<object type=text/x-scriptlet>");o.style.width=e.currentStyle.width;o.style.height=e.currentStyle.height;o.data=_6;var u=makePath(e.data,getPath(location.href));e.parentNode.replaceChild(o,e);cssQuery.clearCache("object");addTimer(o,"",u);return o}})}var _1=false;addEventHandler(window,"onbeforeprint",function(){_1=true;for(var i=0;i<_0.length;i++)_5(_0[i])});addEventHandler(window,"onafterprint",function(){for(var i=0;i<_0.length;i++)_2(_0[i]
 );_1=false})});
diff --git a/web/static/js/ie7/ie7-html4.js b/web/static/js/ie7/ie7-html4.js
new file mode 100644
index 0000000..3fcd891
--- /dev/null
+++ b/web/static/js/ie7/ie7-html4.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+IE7.addModule("ie7-html4",function(){if(!isHTML)return;HEADER+="h1{font-size:2em}h2{font-size:1.5em;}h3{font-size:1.17em;}"+"h4{font-size:1em}h5{font-size:.83em}h6{font-size:.67em}";var _0={};ie7HTML=new(Fix.specialize({init:DUMMY,addFix:function(){this.fixes.push(arguments)},apply:function(){for(var i=0;i<this.fixes.length;i++){var m=cssQuery(this.fixes[i][0]);var f=this.fixes[i][1]||_1;for(var j=0;j<m.length;j++)f(m[j])}},addRecalc:function(){this.recalcs.push(arguments)},recalc:function(){for(var i=0;i<this.recalcs.length;i++){var m=cssQuery(this.recalcs[i][0]);var r=this.recalcs[i][1],e;var k=Math.pow(2,i);for(var j=0;(e=m[j]);j++){var u=e.uniqueID;if((_0[u]&k)==0){e=r(e)||e;_0[u]|=k}}}}}));ie7HTML.addFix("abbr");ie7HTML.addRecalc("label",function(e){if(!e.htmlFor){var f=cssQuery("input,textarea",e)[0];if(f){addEventHandler(e,"onclick",function(){f.click()})}}});ie7HTML.addRecalc("button,input",function(e){if(e.tagName=="BUTTON"){var m=e.outerHTML.match(/ value="([^"]*)"
 /i);e.runtimeStyle.value=(m)?m[1]:""}if(e.type=="submit"){addEventHandler(e,"onclick",function(){e.runtimeStyle.clicked=true;setTimeout("document.all."+e.uniqueID+".runtimeStyle.clicked=false",1)})}});var U=/^(submit|reset|button)$/;ie7HTML.addRecalc("form",function(e){addEventHandler(e,"onsubmit",function(){for(var i=0;i<e.length;i++){if(_2(e[i])){e[i].disabled=true;setTimeout("document.all."+e[i].uniqueID+".disabled=false",1)}else if(e[i].tagName=="BUTTON"&&e[i].type=="submit"){setTimeout("document.all."+e[i].uniqueID+".value='"+e[i].value+"'",1);e[i].value=e[i].runtimeStyle.value}}})});function _2(e){return U.test(e.type)&&!e.disabled&&!e.runtimeStyle.clicked};ie7HTML.addRecalc("img",function(e){if(e.alt&&!e.title)e.title=""});var P=(appVersion<5.5)?"HTML:":"";function _1(e){var f=document.createElement("<"+P+e.outerHTML.slice(1));if(e.outerHTML.slice(-2)!="/>"){var en="</"+e.tagName+">",n;while((n=e.nextSibling)&&n.outerHTML!=en){f.appendChild(n)}if(n)n.removeNode()}e.pa
 rentNode.replaceChild(f,e)}});
diff --git a/web/static/js/ie7/ie7-ie5.js b/web/static/js/ie7/ie7-ie5.js
new file mode 100644
index 0000000..a07fd98
--- /dev/null
+++ b/web/static/js/ie7/ie7-ie5.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+if(appVersion<5.5){ANON="HTML:!";var ap=function(f,o,a){f.apply(o,a)};if(''.replace(/^/,String)){var _0=String.prototype.replace;var _1=function(e,r){var m,n="",s=this;while(s&&(m=e.exec(s))){n+=s.slice(0,m.index)+ap(r,this,m);s=s.slice(m.lastIndex)}return n+s};String.prototype.replace=function(e,r){this.replace=(typeof r=="function")?_1:_0;return this.replace(e,r)}}if(!Function.apply){var APPLY="apply-"+Number(new Date);ap=function(f,o,a){var r;o[APPLY]=f;switch(a.length){case 0:r=o[APPLY]();break;case 1:r=o[APPLY](a[0]);break;case 2:r=o[APPLY](a[0],a[1]);break;case 3:r=o[APPLY](a[0],a[1],a[2]);break;case 4:r=o[APPLY](a[0],a[1],a[2],a[3]);break;default:var aa=[],i=a.length-1;do aa[i]="a["+i+"]";while(i--);eval("r=o[APPLY]("+aa+")")}delete o[APPLY];return r};ICommon.valueOf.prototype.inherit=function(){return ap(arguments.callee.caller.ancestor,this,arguments)}}if(![].push)Array.prototype.push=function(){for(var i=0;i<arguments.length;i++){this[this.length]=arguments[i]}retu
 rn this.length};if(![].pop)Array.prototype.pop=function(){var i=this[this.length-1];this.length--;return i};if(isHTML){HEADER+="address,blockquote,body,dd,div,dt,fieldset,form,"+"frame,frameset,h1,h2,h3,h4,h5,h6,iframe,noframes,object,p,"+"hr,applet,center,dir,menu,pre,dl,li,ol,ul{display:block}"}}
diff --git a/web/static/js/ie7/ie7-layout.js b/web/static/js/ie7/ie7-layout.js
new file mode 100644
index 0000000..d1b64eb
--- /dev/null
+++ b/web/static/js/ie7/ie7-layout.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+IE7.addModule("ie7-layout",function(){ie7Layout=this;HEADER+="*{boxSizing:content-box}";this.hasLayout=(appVersion<5.5)?function(e){return e.clientWidth}:function(e){return e.currentStyle.hasLayout};this.boxSizing=function(e){if(!ie7Layout.hasLayout(e)){e.style.height="0cm";if(e.currentStyle.verticalAlign=="auto")e.runtimeStyle.verticalAlign="top";_1(e)}};function _1(e){if(e!=viewport&&e.currentStyle.position!="absolute"){collapseMarginTop(e);collapseMarginBottom(e)}};var firstElementChild=cssQuery.valueOf("firstElementChild");var lastElementChild=cssQuery.valueOf("lastElementChild");function collapseMarginTop(e){if(!e.runtimeStyle.marginTop){var p=e.parentElement;if(p&&ie7Layout.hasLayout(p)&&e==firstElementChild(p))return;var f=firstElementChild(e);if(f&&f.currentStyle.styleFloat=="none"&&ie7Layout.hasLayout(f)){collapseMarginTop(f);m=_3(e,e.currentStyle.marginTop);c=_3(f,f.currentStyle.marginTop);if(m<0||c<0){e.runtimeStyle.marginTop=m+c}else{e.runtimeStyle.marginTop=Math
 .max(c,m)}f.runtimeStyle.marginTop="0px"}}};eval(String(collapseMarginTop).replace(/Top/g,"Bottom").replace(/first/g,"last"));function _3(e,v){return(v=="auto")?0:getPixelValue(e,v)};var U=/^[.\d][\w%]*$/,A=/^(auto|0cm)$/,N="[.\\d]";var applyWidth,applyHeight;function borderBox(e){applyWidth(e);applyHeight(e)};function fixWidth(H){applyWidth=function(e){if(!PERCENT.test(e.currentStyle.width))fixWidth(e);_1(e)};function fixWidth(e,v){if(!e.runtimeStyle.fixedWidth){if(!v)v=e.currentStyle.width;e.runtimeStyle.fixedWidth=(U.test(v))?Math.max(0,getFixedWidth(e,v)):v;setOverrideStyle(e,"width",e.runtimeStyle.fixedWidth)}};function layoutWidth(e){if(!isFixed(e)){var l=e.offsetParent;while(l&&!ie7Layout.hasLayout(l))l=l.offsetParent}return(l||viewport).clientWidth};function getPixelWidth(e,v){if(PERCENT.test(v))return parseInt(parseFloat(v)/100*layoutWidth(e));return getPixelValue(e,v)};var getFixedWidth=function(e,v){var b=e.currentStyle["box-sizing"]=="border-box";var a=0;if(quirk
 sMode&&!b)a+=getBorderWidth(e)+getPaddingWidth(e);else if(!quirksMode&&b)a-=getBorderWidth(e)+getPaddingWidth(e);return getPixelWidth(e,v)+a};function getBorderWidth(e){return e.offsetWidth-e.clientWidth};function getPaddingWidth(e){return getPixelWidth(e,e.currentStyle.paddingLeft)+getPixelWidth(e,e.currentStyle.paddingRight)};eval(String(getPaddingWidth).replace(/padding/g,"margin").replace(/Padding/g,"Margin"));HEADER+="*{minWidth:none;maxWidth:none;min-width:none;max-width:none}";function minWidth(e){if(e.currentStyle["min-width"]!=null){e.style.minWidth=e.currentStyle["min-width"]}if(register(minWidth,e,e.currentStyle.minWidth!="none")){ie7Layout.boxSizing(e);fixWidth(e);resizeWidth(e)}};eval(String(minWidth).replace(/min/g,"max"));ie7Layout.minWidth=minWidth;ie7Layout.maxWidth=maxWidth;function resizeWidth(e){var r=e.getBoundingClientRect();var w=r.right-r.left;if(e.currentStyle.minWidth!="none"&&w<=getFixedWidth(e,e.currentStyle.minWidth)){e.runtimeStyle.width=getFixe
 dWidth(e,e.currentStyle.minWidth)}else if(e.currentStyle.maxWidth!="none"&&w>=getFixedWidth(e,e.currentStyle.maxWidth)){e.runtimeStyle.width=getFixedWidth(e,e.currentStyle.maxWidth)}else{e.runtimeStyle.width=e.runtimeStyle.fixedWidth}};function fixRight(e){if(register(fixRight,e,/^(fixed|absolute)$/.test(e.currentStyle.position)&&getDefinedStyle(e,"left")!="auto"&&getDefinedStyle(e,"right")!="auto"&&A.test(getDefinedStyle(e,"width")))){resizeRight(e);ie7Layout.boxSizing(e)}};ie7Layout.fixRight=fixRight;function resizeRight(e){var l=getPixelWidth(e,e.runtimeStyle._4||e.currentStyle.left);var w=layoutWidth(e)-getPixelWidth(e,e.currentStyle.right)-l-getMarginWidth(e);if(parseInt(e.runtimeStyle.width)==w)return;e.runtimeStyle.width="";if(isFixed(e)||H||e.offsetWidth<w){if(!quirksMode)w-=getBorderWidth(e)+getPaddingWidth(e);if(w<0)w=0;e.runtimeStyle.fixedWidth=w;setOverrideStyle(e,"width",w)}};var _2=0;addResize(function(){var i,w=(_2<viewport.clientWidth);_2=viewport.clientWidth
 ;for(i in minWidth.elements){var e=minWidth.elements[i];var f=(parseInt(e.runtimeStyle.width)==getFixedWidth(e,e.currentStyle.minWidth));if(w&&f)e.runtimeStyle.width="";if(w==f)resizeWidth(e)}for(i in maxWidth.elements){var e=maxWidth.elements[i];var f=(parseInt(e.runtimeStyle.width)==getFixedWidth(e,e.currentStyle.maxWidth));if(!w&&f)e.runtimeStyle.width="";if(w!=f)resizeWidth(e)}for(i in fixRight.elements)resizeRight(fixRight.elements[i])});if(window.IE7_BOX_MODEL!==false){ie7CSS.addRecalc("width",N,quirksMode?applyWidth:_1)}ie7CSS.addRecalc("min-width",N,minWidth);ie7CSS.addRecalc("max-width",N,maxWidth);ie7CSS.addRecalc("right",N,fixRight)};ie7CSS.addRecalc("border-spacing",N,function(e){if(e.currentStyle.borderCollapse!="collapse"){e.cellSpacing=getPixelValue(e,e.currentStyle["border-spacing"])}});ie7CSS.addRecalc("box-sizing","content-box",this.boxSizing);ie7CSS.addRecalc("box-sizing","border-box",borderBox);var _0=new ParseMaster;_0.add(/Width/,"Height");_0.add(/width
 /,"height");_0.add(/Left/,"Top");_0.add(/left/,"top");_0.add(/Right/,"Bottom");_0.add(/right/,"bottom");eval(_0.exec(String(fixWidth)));fixWidth();fixHeight(true)});
diff --git a/web/static/js/ie7/ie7-load.htc b/web/static/js/ie7/ie7-load.htc
new file mode 100644
index 0000000..a6f1e7f
--- /dev/null
+++ b/web/static/js/ie7/ie7-load.htc
@@ -0,0 +1 @@
+<component lightweight="true"><attach event="ondocumentready" onevent="IE7.init()"/></component>
diff --git a/web/static/js/ie7/ie7-object.htc b/web/static/js/ie7/ie7-object.htc
new file mode 100644
index 0000000..392409e
--- /dev/null
+++ b/web/static/js/ie7/ie7-object.htc
@@ -0,0 +1,12 @@
+<html>
+<!--
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+-->
+<head>
+<style type="text/css">body{margin:0}</style>
+<script type="text/javascript">public_description=new function(){var l=false;this.ie7_anon=true;this.load=function(o,c,u){if(l)return;l=true;function _0(t,p){t.style[p]=o.currentStyle[p]};var p=o;while(p&&p.currentStyle.backgroundColor=="transparent"){p=p.parentElement}if(p)body.style.backgroundColor=p.currentStyle.backgroundColor;_0(body,"backgroundImage");_0(body,"backgroundRepeat");_0(body,"backgroundPositionX");_0(body,"backgroundPositionY");_0(body,"fontFamily");_0(body,"fontSize");_0(wrapper,"paddingTop");_0(wrapper,"paddingRight");_0(wrapper,"paddingBottom");_0(wrapper,"paddingLeft");image.width=o.clientWidth;image.height=o.clientHeight;var B64=/^data:.*;base64/i,P=/.png$/i;if(B64.test(u))u="ie7-base64.php"+"?"+u.slice(5);if(P.test(u)&&!/MSIE 5.0/.test(navigator.userAgent)){image.src="blank.gif";image.style.filter="progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+u+"',sizingMethod='scale')"}else{image.src=u}o.style.width=body.scrollWidth;o.style.height=body.s
 crollHeight}};</script>
+</head>
+<body id="body"><div id="wrapper"><img id="image"></div></body>
+</html>
diff --git a/web/static/js/ie7/ie7-overflow.js b/web/static/js/ie7/ie7-overflow.js
new file mode 100644
index 0000000..ad2e030
--- /dev/null
+++ b/web/static/js/ie7/ie7-overflow.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+IE7.addModule("ie7-overflow",function(){var S={backgroundColor:"transparent",backgroundImage:"none",backgroundPositionX:null,backgroundPositionY:null,backgroundRepeat:null,borderTopWidth:0,borderRightWidth:0,borderBottomWidth:0,borderLeftStyle:"none",borderTopStyle:"none",borderRightStyle:"none",borderBottomStyle:"none",borderLeftWidth:0,height:null,marginTop:0,marginBottom:0,marginRight:0,marginLeft:0,width:"100%"};function _3(p,s,t){t.style[p]=s.currentStyle[p];if(S[p]!=null){s.runtimeStyle[p]=S[p]}};ie7CSS.addRecalc("overflow","visible",function(e){if(e.parentNode.ie7_wrapper)return;if(ie7Layout&&e.currentStyle["max-height"]!="auto"){ie7Layout.maxHeight(e)}if(e.currentStyle.marginLeft=="auto")e.style.marginLeft=0;if(e.currentStyle.marginRight=="auto")e.style.marginRight=0;var w=document.createElement(ANON);w.ie7_wrapper=true;for(var p in S)_3(p,e,w);w.style.display="block";w.style.position="relative";e.runtimeStyle.position="absolute";e.parentNode.insertBefore(w,e);w.appe
 ndChild(e)});cssQuery.addModule("ie7-overflow",function(){function _0(e){return(e&&e.ie7_wrapper)?e.firstChild:e};var _2=previousElementSibling;previousElementSibling=function(e){return _0(_2(e))};var _1=nextElementSibling;nextElementSibling=function(e){return _0(_1(e))};selectors[" "]=function(r,f,t,n){var e,i,j;for(i=0;i<f.length;i++){var s=getElementsByTagName(f[i],t,n);for(j=0;(e=_0(s[j]));j++){if(thisElement(e)&&(!n||compareNamespace(e,n)))r.push(e)}}};selectors[">"]=function(r,f,t,n){var e,i,j;for(i=0;i<f.length;i++){var s=childElements(f[i]);for(j=0;(e=_0(s[j]));j++){if(compareTagName(e,t,n))r.push(e)}}}})});
diff --git a/web/static/js/ie7/ie7-quirks.js b/web/static/js/ie7/ie7-quirks.js
new file mode 100644
index 0000000..a1314dd
--- /dev/null
+++ b/web/static/js/ie7/ie7-quirks.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+IE7.addModule("ie7-quirks",function(){if(quirksMode){var F="xx-small,x-small,small,medium,large,x-large,xx-large".split(",");for(var i=0;i<F.length;i++){F[F[i]]=F[i-1]||"0.67em"}ie7CSS.addFix(new RegExp("(font(-size)?\\s*:\\s*)([\\w\\-\\.]+)"),function(m,o){return m[o+1]+(F[m[o+3]]||m[o+3])});if(appVersion<6){var N=/^\-/,L=/(em|ex)$/i;var EM=/em$/i,EX=/ex$/i;function _2(e){var s=1;_0.style.fontFamily=e.currentStyle.fontFamily;_0.style.lineHeight=e.currentStyle.lineHeight;while(e!=body){var f=e.currentStyle["ie7-font-size"];if(f){if(EM.test(f))s*=parseFloat(f);else if(PERCENT.test(f))s*=(parseFloat(f)/100);else if(EX.test(f))s*=(parseFloat(f)/2);else{_0.style.fontSize=f;return 1}}e=e.parentElement}return s};var _0=createTempElement();getPixelValue=function(e,v){if(PIXEL.test(v||0))return parseInt(v||0);var scale=N.test(v)?-1:1;if(L.test(v))scale*=_2(e);_0.style.width=(scale<0)?v.slice(1):v;body.appendChild(_0);v=scale*_0.offsetWidth;_0.removeNode();return parseInt(v)};HEADER=
 HEADER.replace(/(font(-size)?\s*:\s*([^\s;}\/]*))/gi,"ie7-font-size:$3;$1");ie7CSS.addFix(/cursor\s*:\s*pointer/,"cursor:hand");ie7CSS.addFix(/display\s*:\s*list-item/,"display:block")}function getPaddingWidth(e){return getPixelValue(e,e.currentStyle.paddingLeft)+getPixelValue(e,e.currentStyle.paddingRight)};function _1(e){if(appVersion<5.5&&ie7Layout)ie7Layout.boxSizing(e.parentElement);var p=e.parentElement;var m=p.offsetWidth-e.offsetWidth-getPaddingWidth(p);var a=(e.currentStyle["ie7-margin"]&&e.currentStyle.marginRight=="auto")||e.currentStyle["ie7-margin-right"]=="auto";switch(p.currentStyle.textAlign){case"right":m=(a)?parseInt(m/2):0;e.runtimeStyle.marginRight=parseInt(m)+"px";break;case"center":if(a)m=0;default:if(a)m=parseInt(m/2);e.runtimeStyle.marginLeft=parseInt(m)+"px"}};ie7CSS.addRecalc("margin(-left|-right)?","[^};]*auto",function(e){if(register(_1,e,e.parentElement&&e.currentStyle.display=="block"&&e.currentStyle.marginLeft=="auto"&&e.currentStyle.position!=
 "absolute")){_1(e)}});addResize(function(){for(var i in _1.elements){e=_1.elements[i];e.runtimeStyle.marginLeft=e.runtimeStyle.marginRight="";_1(e)}})}});
diff --git a/web/static/js/ie7/ie7-recalc.js b/web/static/js/ie7/ie7-recalc.js
new file mode 100644
index 0000000..10cb839
--- /dev/null
+++ b/web/static/js/ie7/ie7-recalc.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+IE7.addModule("ie7-recalc",function(){C=/\sie7_class\d+/g;function _0(e){e.className=e.className.replace(C,"")};function _1(e){e.runtimeStyle.cssText=""};ie7CSS.specialize({elements:{},handlers:[],reset:function(){this.removeEventHandlers();var e=this.elements;for(var i in e)_1(e[i]);this.elements={};if(this.Rule){var e=this.Rule.elements;for(var i in e)_0(e[i]);this.Rule.elements={}}},reload:function(){ie7CSS.rules=[];this.getInlineStyles();this.screen.load();if(this.print)this.print.load();this.refresh();this.trash()},addRecalc:function(p,t,h,r){this.inherit(p,t,function(e){h(e);ie7CSS.elements[e.uniqueID]=e},r)},recalc:function(){this.reset();this.inherit()},addEventHandler:function(e,t,h){e.attachEvent(t,h);this.handlers.push(arguments)},removeEventHandlers:function(){var h;while(h=this.handlers.pop()){removeEventHandler(h[0],h[1],h[2])}},getInlineStyles:function(){var st=document.getElementsByTagName("style"),s;for(var i=st.length-1;(s=st[i]);i--){if(!s.disabled&&!s.ie7
 ){var c=s.c||s.innerHTML;this.styles.push(c);s.c=c}}},trash:function(){var s,i;for(i=0;i<styleSheets.length;i++){s=styleSheets[i];if(!s.ie7&&!s.c){s.c=s.cssText}}this.inherit()},getText:function(s){return s.c||this.inherit(s)}});addEventHandler(window,"onunload",function(){ie7CSS.removeEventHandlers()});if(ie7CSS.Rule){ie7CSS.Rule.elements={};ie7CSS.Rule.prototype.specialize({add:function(e){this.inherit(e);ie7CSS.Rule.elements[e.uniqueID]=e}});ie7CSS.PseudoElement.hash={};ie7CSS.PseudoElement.prototype.specialize({create:function(t){var k=this.selector+":"+t.uniqueID;if(!ie7CSS.PseudoElement.hash[k]){ie7CSS.PseudoElement.hash[k]=true;this.inherit(t)}}})}if(isHTML&&ie7HTML){ie7HTML.specialize({elements:{},addRecalc:function(s,h){this.inherit(s,function(e){if(!ie7HTML.elements[e.uniqueID]){h(e);ie7HTML.elements[e.uniqueID]=e}})}})}document.recalc=function(reload){if(ie7CSS.screen){if(reload)ie7CSS.reload();recalc()}}});
diff --git a/web/static/js/ie7/ie7-server.css b/web/static/js/ie7/ie7-server.css
new file mode 100644
index 0000000..50ca605
--- /dev/null
+++ b/web/static/js/ie7/ie7-server.css
@@ -0,0 +1,44 @@
+body, td, dd {font: 10pt Verdana, Arial, Helvetica, sans-serif; color: black;}
+body {margin: 8px; background: #333;}
+h1 {margin: 0;}
+h1 a:hover {background-color: transparent;}
+h2 {font-size: 1.75em;}
+h3 {font-size: 1.1em;}
+p.footnote {font-family: "Times New Roman", Times, serif; font-style: italic;}
+a:active {color: #ff0000;}
+a:link {color: #0a6cce;}
+a:visited {color: #0a6cce;}
+code, *.code {font-family: monospace; font-size: 100%; font-style: normal; white-space: nowrap;
+ padding: 0 1px; background: #f2f3f8; border: #d6d9e9 1px solid;}
+code.box {display: block; padding: 10px; margin: 0.5em 0;}
+ul {list-style-type: square;}
+dd {margin: .2em 0 .5em 1em;}
+dl.library dt {display: list-item; margin-left: 3em; list-style-type: square;}
+dl.library dd {font-style: italic; margin-left: 3em;}
+dt {font-weight: bold;}
+dt.pack {color: brown;}
+a img {border-style: none;}
+hr {height: 1px; color: #000; border-style: solid;}
+hr.short {height: 2px; width: 100px;}
+div.document {background: #eef; padding: 20px 20px 5px 20px; width: 600px; border: 1px solid black;}
+hr {border-bottom-width: 0px;}
+div.header hr {color: #0a6cce; background-color: #0a6cce;}
+div.footer hr {color: #898e79; background-color: #898e79; }
+div.header, div.header a:link, div.header a:visited, h3 a:link, h3 a:visited {text-decoration: none;}
+a:hover {color: #fff; background-color: #0a6cce; text-decoration: none;}
+div.footer a:hover {background-color: transparent; text-decoration: none;}
+div.header .menu {text-align: right;}
+div.content {min-height: 100px;}
+div.footer {font-size: x-small; margin-top: 8px;}
+div.footnote {font-family: "times new roman", times; font-style: italic; margin-top: 10px;}
+#license {margin-top: 5px; font-size: xx-small;}
+table {border-top: 1px solid #000; border-left: 1px solid #000;}
+th {background-color: #fff; text-align: left;}
+th, td {border-right: 1px solid #000; border-bottom: 1px solid #000;}
+th.small {width: 100px;}
+th.medium {width: 200px;}
+th.large {width: 270px;}
+th.x-large {width: 408px;}
+table.fixed {table-layout: fixed;}
+span.comment {color: #666;}
+
diff --git a/web/static/js/ie7/ie7-squish.js b/web/static/js/ie7/ie7-squish.js
new file mode 100644
index 0000000..e5a1972
--- /dev/null
+++ b/web/static/js/ie7/ie7-squish.js
@@ -0,0 +1,45 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+IE7.addModule("ie7-squish", function() {
+
+/* ---------------------------------------------------------------------
+
+  Squish some IE bugs!
+
+  Some of these bug fixes may have adverse effects so they are
+  not included in the standard library. Add your own if you want.
+
+  -dean
+
+--------------------------------------------------------------------- */
+
+// @NOTE: ie7Layout.boxSizing is the same as the "Holly Hack"
+
+// "doubled margin" bug
+// http://www.positioniseverything.net/explorer/doubled-margin.html
+ie7CSS.addFix(/float\s*:\s*(left|right)/, "display:inline;$1");
+
+if (ie7Layout) {
+	// "peekaboo" bug
+	// http://www.positioniseverything.net/explorer/peekaboo.html
+	if (appVersion >= 6) ie7CSS.addRecalc("float", "left|right", function($element) {
+		ie7Layout.boxSizing($element.parentElement);
+		// "doubled margin" bug
+		$element.runtimeStyle.display = "inline";
+	});
+
+	// "unscrollable content" bug
+	// http://www.positioniseverything.net/explorer/unscrollable.html
+	ie7CSS.addRecalc("position", "absolute|fixed", function($element) {
+		if ($element.offsetParent && $element.offsetParent.currentStyle.position == "relative")
+			ie7Layout.boxSizing($element.offsetParent);
+	});
+}
+
+//# // get rid of Microsoft's pesky image toolbar
+//# if (!complete) document.write('<meta http-equiv="imagetoolbar" content="no">');
+
+});
diff --git a/web/static/js/ie7/ie7-standard-p.js b/web/static/js/ie7/ie7-standard-p.js
new file mode 100644
index 0000000..6db85f5
--- /dev/null
+++ b/web/static/js/ie7/ie7-standard-p.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('y(!26.1F)11 7(){2C{26.1F=8;6 2s=8.24=11 3b;8.1g=7(){z"1F 4x 0.9 (ad)"};6 5T=/5T/.Z(2y.5h.7C);6 31=(5T)?7(m){26.31(1F+"\\n\\n"+m)}:2s;6 29=ac.29.19(/ab (\\d\\.\\d)/)[1];6 2m=16.aa!="a9";y(/a8/.Z(2y.5h.7C)||29<5||!/^a7/.Z(16.2F.2a))z;6 33=16.5W=="33";6 1s,1K;6 2F=16.2F,1X,1J,1R=16.1R;6 4E="!";6 3Q={};6 2G=1z;1F.24=7(n,s){y(!3Q[n]){y(2G)1k("s="+23(s));3Q[n]=11 s()}};6 R=/^[\\w\\.]+[^:]*$/;7 1Z(h,p){y(R.Z(h))h=(p||"")+h;z h};7 3F(h,p){h=1Z(h,p);z h.1q(0,h.7a("/")+1)};6 s=16.7B[16.7B.K-1];2C{1k(s.7j)}2j(i){}6 2k=3F(s.1l);6 2v;2C{6 l=(a6()>=5)?"a5":"5n";2v=11 a4(l+".a3")}2j(i){}6 4A={};7 3T(h,p){2C{h=1Z(h,p);y(!4A[h]){2v.a2("a1",h,1z);2v.a0();y(2v.7A==0||2v.7A=
 =9Z){4A[h]=2v.9Y}}}2j(i){31("4B [1]: 5O 9X 9W "+h)}5U{z 4A[h]||""}};6 4i=1Z("9V.9U",2k);7 2o(1w){y(1w!=1U){1w.1T=1t.1C.1T;1w.1e=1t.1C.1e}z 1w};2o.1e=7(p,c){y(!p)p={};y(!c)c=p.1h;y(c=={}.1h)c=11 3b("8.1T()");c.1i=11 3b("z 8");c.1i.1C=11 8.1i;c.1i.1C.1e(p);c.1C=11 c.1i;c.1i.1C.1h=c.1C.1h=c;c.2E=8;c.1e=1a.5P;c.4z=8.4z;z c};2o.1i=11 3b("z 8");2o.1i.1C={1h:2o,1T:7(){z 1a.5P.9T.2E.2q(8,1a)},1e:7(1w){y(8==8.1h.1C&&8.1h.1e){z 8.1h.1i.1C.1e(1w)}O(6 i 28 1w){34(i){1m"1h":1m"1g":1m"1i":5M}y(3Y 1w[i]=="7"&&1w[i]!=8[i]){1w[i].2E=8[i]}8[i]=1w[i]}y(1w.1g!=8.1g&&1w.1g!={}.1g){1w.1g.2E=8.1g;8.1g=1w.1g}z 8}};7 1t(){};8.1t=2o.1e({1h:1t,1g:7(){z"[9S "+(8.1h.1x||"9R")+"]"},9Q:7(2i){z 8.1h==2i||2i.4z(8.1h)}});1t.1x="1t";1t.2E=1U;1t.4z=7(2i){1D(2i&&2i.2E!=8)2i=2i.2E;z 7q(2i)};1t.1i.2E=2o;3u 8.1t;6 5x=1t.1e({1h:7(){8.3L=[];8.1Q=[]},1S:2s});y(29<5.5)1k(3T("17-9P.5X",2k));6 5S=1z;1F.1S=7(){2C{y(5S)z;5S=33=1o;1X=16.1X;1J=(2m)?1X:2F;y(2l&&1s)1s.2q();V.2q();1u();31("2G 9O")}2j(e){31("4B [2]: "+e.5V)}};6
  1Q=[];7 1n(r){1Q.1b(r)};7 1u(){14.5g();y(2l&&1s)1s.1u();V.1u();O(6 i=0;i<1Q.K;i++)1Q[i]()};7 2U(){6 E=0,R=1,L=2;6 G=/\\(/g,S=/\\$\\d/,I=/^\\$\\d+$/,T=/([\'"])\\1\\+(.*)\\+\\1\\1$/,7t=/\\\\./g,Q=/\'/,7z=/\\3S[^\\3S]*\\3S/g;6 3N=8;8.15=7(e,r){y(!r)r="";6 l=(5R(23(e)).19(G)||"").K+1;y(S.Z(r)){y(I.Z(r)){r=25(r.1q(1))-1}1d{6 i=l;6 q=Q.Z(5R(r))?\'"\':"\'";1D(i)r=r.2O("$"+i--).2p(q+"+a[o+"+i+"]+"+q);r=11 3b("a,o","z"+q+r.13(T,"$1")+q)}}7y(e||"/^$/",r,l)};8.2V=7(s){3R.K=0;z 7u(7v(s,8.4y).13(11 1N(30,8.5Q?"5D":"g"),7w),8.4y).13(7z,"")};8.72=7(){30.K=0};6 3R=[];6 30=[];6 7x=7(){z"("+23(8[E]).1q(1,-1)+")"};30.1g=7(){z 8.2p("|")};7 7y(){1a.1g=7x;30[30.K]=1a}7 7w(){y(!1a[0])z"";6 i=1,j=0,p;1D(p=30[j++]){y(1a[i]){6 r=p[R];34(3Y r){1m"7":z r(1a,i);1m"9N":z 1a[r+i]}6 d=(1a[i].6F(3N.4y)==-1)?"":"\\3S"+1a[i]+"\\3S";z d+r}1d i+=p[L]}};7 7v(s,e){z e?s.13(11 1N("\\\\"+e+"(.)","g"),7(m,c){3R[3R.K]=c;z e}):s};7 7u(s,e){6 i=0;z e?s.13(11 1N("\\\\"+e,"g"),7(){z e+(3R[i++]||"")}):s};7 5R(s){z s.13(7
 t,"")}};2U.1C={1h:2U,5Q:1z,4y:""};1t.1e(2U.1C);6 3M=2U.1e({5Q:1o});6 14=7(){6 4x="2.0.2";6 C=/\\s*,\\s*/;6 14=7(s,1E){2C{6 m=[];6 u=1a.5P.5I&&!1E;6 b=(1E)?(1E.1h==7n)?1E:[1E]:[16];6 2f=45(s).2O(C),i;O(i=0;i<2f.K;i++){s=5J(2f[i]);y(4P&&s.1q(0,3).2p("")==" *#"){s=s.1q(2);1E=7o([],b,s[1])}1d 1E=b;6 j=0,t,f,a,c="";1D(j<s.K){t=s[j++];f=s[j++];c+=t+f;a="";y(s[j]=="("){1D(s[j++]!=")"&&j<s.K){a+=s[j]}a=a.1q(0,-1);c+="("+a+")"}1E=(u&&2e[c])?2e[c]:7m(1E,t,f,a);y(u)2e[c]=1E}m=m.4J(1E)}3u 14.5O;z m}2j(e){14.5O=e;z[]}};14.1g=7(){z"7 14() {\\n  [4x "+4x+"]\\n}"};6 2e={};14.5I=1z;14.5g=7(s){y(s){s=5J(s).2p("");3u 2e[s]}1d 2e={}};6 3Q={};6 2G=1z;14.24=7(n,s){y(2G)1k("s="+23(s));3Q[n]=11 s()};14.1i=7(c){z c?1k(c):8};6 1V={};6 1B={};6 1p={19:/\\[([\\w-]+(\\|[\\w-]+)?)\\s*(\\W?=)?\\s*([^\\]]*)\\]/};6 2R=[];1V[" "]=7(r,f,t,n){6 e,i,j;O(i=0;i<f.K;i++){6 s=4w(f[i],t,n);O(j=0;(e=s[j]);j++){y(2D(e)&&5K(e,n))r.1b(e)}}};1V["#"]=7(r,f,i){6 e,j;O(j=0;(e=f[j]);j++)y(e.1c==i)r.1b(e)};1V["."]=7(r,f,c){c=1
 1 1N("(^|\\\\s)"+c+"(\\\\s|$)");6 e,i;O(i=0;(e=f[i]);i++)y(c.Z(e.1x))r.1b(e)};1V[":"]=7(r,f,p,a){6 t=1B[p],e,i;y(t)O(i=0;(e=f[i]);i++)y(t(e,a))r.1b(e)};1B["21"]=7(e){6 d=5L(e);y(d.5N)O(6 i=0;i<d.5N.K;i++){y(d.5N[i]==e)z 1o}};1B["37"]=7(e){};6 2D=7(e){z(e&&e.7k==1&&e.2W!="!")?e:1U};6 4S=7(e){1D(e&&(e=e.9M)&&!2D(e))5M;z e};6 47=7(e){1D(e&&(e=e.6W)&&!2D(e))5M;z e};6 3l=7(e){z 2D(e.7s)||47(e.7s)};6 5t=7(e){z 2D(e.7r)||4S(e.7r)};6 6q=7(e){6 c=[];e=3l(e);1D(e){c.1b(e);e=47(e)}z c};6 4P=1o;6 5H=7(e){6 d=5L(e);z(3Y d.7p=="9L")?/\\.9K$/i.Z(d.9J):7q(d.7p=="9I 9H")};6 5L=7(e){z e.9G||e.16};6 4w=7(e,t){z(t=="*"&&e.1Y)?e.1Y:e.4w(t)};6 4T=7(e,t,n){y(t=="*")z 2D(e);y(!5K(e,n))z 1z;y(!5H(e))t=t.9F();z e.2W==t};6 5K=7(e,n){z!n||(n=="*")||(e.9E==n)};6 9D=7(e){z e.9C};7 7o(r,f,1c){6 m,i,j;O(i=0;i<f.K;i++){y(m=f[i].1Y.9B(1c)){y(m.1c==1c)r.1b(m);1d y(m.K!=1U){O(j=0;j<m.K;j++){y(m[j].1c==1c)r.1b(m[j])}}}}z r};y(![].1b)7n.1C.1b=7(){O(6 i=0;i<1a.K;i++){8[8.K]=1a[i]}z 8.K};6 N=/\\|/;7 7m(1E,t,f,a){y
 (N.Z(f)){f=f.2O(N);a=f[0];f=f[1]}6 r=[];y(1V[t]){1V[t](r,1E,f,a)}z r};6 S=/^[^\\s>+~]/;6 7l=/[\\s#.:>+~()@]|[^\\s#.:>+~()@]+/g;7 5J(s){y(S.Z(s))s=" "+s;z s.19(7l)||[]};6 W=/\\s*([\\s>+~(),]|^|$)\\s*/g;6 I=/([\\s>+~,]|[^(]\\+|^)([#.:@])/g;6 45=7(s){z s.13(W,"$1").13(I,"$1*$2")};6 2c={1g:7(){z"\'"},19:/^(\'[^\']*\')|("[^"]*")$/,Z:7(s){z 8.19.Z(s)},15:7(s){z 8.Z(s)?s:8+s+8},3v:7(s){z 8.Z(s)?s.1q(1,-1):s}};6 2w=7(t){z 2c.3v(t)};6 E=/([\\/()[\\]?{}|*+-])/g;7 4O(s){z s.13(E,"\\\\$1")};2G=1o;z 14}();14.5I=1o;14.24("17",7(){2D=7(e){z(e&&e.7k==1&&e.2W!="!"&&!e.2K)?e:1U}});14.1i("2w=1a[1]",42);6 2l=!14.1i("5H(1a[1])",2F);6 2r=":21{17-21:21}:37{17-21:37}"+(2l?"":"*{6Q:0}");6 V=11(5x.1e({5B:11 3M,2P:"",2Y:"",5F:[],1S:7(){8.5G();8.4t()},4t:7(){V.3O.18=2r+8.2P+8.2Y},7i:7(){6 3P=16.4w("1r"),s;O(6 i=3P.K-1;(s=3P[i]);i--){y(!s.3m&&!s.17){8.5F.1b(s.7j)}}},2q:7(){8.7i();8.4t();11 3y("2P");8.7g()},3i:7(e,r){8.5B.15(e,r)},1u:7(){6 R=/7h\\d+/g;6 s=2r.19(/[{,]/g).K;6 3P=s+(8.2P.18.19(/\\{/g)||"").
 K;6 2Q=8.3O.2t,r;6 4v,c,4u,e,i,j,k,1c;O(i=s;i<3P;i++){r=2Q[i];y(r&&(4v=r.1r.18.19(R))){4u=14(r.4M);y(4u.K)O(j=0;j<4v.K;j++){1c=4v[j];c=V.1Q[1c.1q(10)][2];O(k=0;(e=4u[k]);k++){y(e.D[1c])c(e)}}}}},1n:7(p,t,h,r){t=11 1N("([{;\\\\s])"+p+"\\\\s*:\\\\s*"+t+"[^;}]*");6 i=8.1Q.K;y(r)r=p+":"+r;8.3i(t,7(m,o){z(r?m[o+1]+r:m[o])+";17-"+m[o].1q(1)+";7h"+i+":1"});8.1Q.1b(1a);z i},2w:7(s){z s.18||""},5G:7(){y(33||!2l)16.5G();1d 16.9A("<1r 17=1o></1r>");8.3O=1R[1R.K-1];8.3O.17=1o;8.3O.18=2r},7g:7(){O(6 i=0;i<1R.K;i++){y(!1R[i].17&&1R[i].18){1R[i].18=""}}}}));7 3y(m){8.2Z=m;8.3q();V[m]=8;V.4t()};1t.1e({1h:3y,1g:7(){z"@2Z "+8.2Z+"{"+8.18+"}"},1u:2s,3q:7(){8.18="";8.2w();8.38();8.18=41(8.18);f={}},2w:7(){6 7e=[].4J(V.5F);6 M=/@2Z\\s+([^{]*)\\{([^@]+\\})\\s*\\}/5D;6 A=/\\9z\\b|^$/i,S=/\\9y\\b/i,P=/\\9x\\b/i;7 7d(c,m){4s.v=m;z c.13(M,4s)};7 4s(9w,m,c){m=5E(m);34(m){1m"2P":1m"2Y":y(m!=4s.v)z"";1m"1Y":z c}z""};7 5E(m){y(A.Z(m))z"1Y";1d y(S.Z(m))z(P.Z(m))?"1Y":"2P";1d y(P.Z(m))z"2Y"};6 3N=8;7 5C(s,
 p,m,l){6 c="";y(!l){m=5E(s.2Z);l=0}y(m=="1Y"||m==3N.2Z){y(l<3){O(6 i=0;i<s.7f.K;i++){c+=5C(s.7f[i],3F(s.2u,p),m,l+1)}}c+=79(s.2u?7c(s,p):7e.77()||"");c=7d(c,3N.2Z)}z c};6 f={};7 7c(s,p){6 u=1Z(s.2u,p);y(f[u])z"";f[u]=(s.3m)?"":7b(V.2w(s,p),3F(s.2u,p));z f[u]};6 U=/(43\\s*\\(\\s*[\'"]?)([\\w\\.]+[^:\\)]*[\'"]?\\))/5D;7 7b(c,p){z c.13(U,"$1"+p.1q(0,p.7a("/")+1)+"$2")};O(6 i=0;i<1R.K;i++){y(!1R[i].3m&&!1R[i].17){8.18+=5C(1R[i])}}},38:7(){8.18=V.5B.2V(8.18)},1u:2s});6 2c=14.1i("2c");6 4r=[];7 79(c){z 2n.2V(3r.2V(c))};7 5A(m,o){z 2c+(4r.1b(m[o])-1)+2c};7 42(v){z 2c.Z(v)?1k(4r[1k(v)]):v};6 2n=11 3M;2n.15(/\\/\\*[^*]*\\*+([^\\/][^*]*\\*+)*\\//);2n.15(/\'[^\']*\'/,5A);2n.15(/"[^"]*"/,5A);2n.15(/\\s+/," ");2n.15(/@(9v|9u)[^;\\n]+[;\\n]|<!\\-\\-|\\-\\->/);6 3r=11 3M;3r.15(/\\\\\'/,"\\\\9t");3r.15(/\\\\"/,"\\\\46");6 5z=11 3M;5z.15(/\'(\\d+)\'/,78);7 41(c){z 5z.2V(c)};7 78(m,o){z 4r[m[o+1]]};6 5y=[];7 4U(h){1n(h);1j(26,"9s",h)};7 1j(e,t,h){e.9r(t,h);5y.1b(1a)};7 76(e,t,h){2C{e.9q(t,h)}
 2j(i){}};1j(26,"9p",7(){6 h;1D(h=5y.77()){76(h[0],h[1],h[2])}});7 20(h,e,c){y(!h.1O)h.1O={};y(c)h.1O[e.2a]=e;1d 3u h.1O[e.2a];z c};1j(26,"6z",7(){y(!V.2Y)11 3y("2Y");V.2Y.1u()});6 75=/^\\d+(9o)?$/i;6 3d=/^\\d+%$/;6 3c=7(e,v){y(75.Z(v))z 25(v);6 s=e.1r.1f;6 r=e.J.1f;e.J.1f=e.D.1f;e.1r.1f=v||0;v=e.1r.4e;e.1r.1f=s;e.J.1f=r;z v};7 6x(t){6 e=16.3X(t||"2M");e.1r.18="1y:3C;6R:0;4K:9n;3G:1M;9m:9l(0 0 0 0);1f:-9k";e.2K=1o;z e};6 4q="17-";7 3D(e){z e.D["17-1y"]=="2z"};7 4o(e,p){z e.D[4q+p]||e.D[p]};7 2T(e,p,v){y(e.D[4q+p]==1U){e.J[4q+p]=e.D[p]}e.J[p]=v};7 4H(o,c,u){6 t=9j(7(){2C{y(!o.3q)z;o.3q(o,c,u);74(t)}2j(i){74(t)}},10)};1F.24("17-9i",7(){y(!2l)z;2r+="9h{3p-3o:9g}9f{3p-3o:1.9e;}9d{3p-3o:1.9c;}"+"9b{3p-3o:9a}99{3p-3o:.98}97{3p-3o:.96}";6 5w={};1s=11(5x.1e({1S:2s,3i:7(){8.3L.1b(1a)},2q:7(){O(6 i=0;i<8.3L.K;i++){6 m=14(8.3L[i][0]);6 f=8.3L[i][1]||6X;O(6 j=0;j<m.K;j++)f(m[j])}},1n:7(){8.1Q.1b(1a)},1u:7(){O(6 i=0;i<8.1Q.K;i++){6 m=14(8.1Q[i][0]);6 r=8.1Q[i][1],e;6 k=4g.95(2,i);O(6 j=0;
 (e=m[j]);j++){6 u=e.2a;y((5w[u]&k)==0){e=r(e)||e;5w[u]|=k}}}}}));1s.3i("94");1s.1n("93",7(e){y(!e.6o){6 f=14("5l,92",e)[0];y(f){1j(e,"73",7(){f.91()})}}});1s.1n("71,5l",7(e){y(e.2W=="70"){6 m=e.3z.19(/ 3n="([^"]*)"/i);e.J.3n=(m)?m[1]:""}y(e.2L=="5v"){1j(e,"73",7(){e.J.5u=1o;32("16.1Y."+e.2a+".J.5u=1z",1)})}});6 U=/^(5v|72|71)$/;1s.1n("90",7(e){1j(e,"8Z",7(){O(6 i=0;i<e.K;i++){y(6Z(e[i])){e[i].3m=1o;32("16.1Y."+e[i].2a+".3m=1z",1)}1d y(e[i].2W=="70"&&e[i].2L=="5v"){32("16.1Y."+e[i].2a+".3n=\'"+e[i].3n+"\'",1);e[i].3n=e[i].J.3n}}})});7 6Z(e){z U.Z(e.2L)&&!e.3m&&!e.J.5u};1s.1n("5d",7(e){y(e.8Y&&!e.6Y)e.6Y=""});6 P=(29<5.5)?"8X:":"";7 6X(e){6 f=16.3X("<"+P+e.3z.1q(1));y(e.3z.1q(-2)!="/>"){6 6V="</"+e.2W+">",n;1D((n=e.6W)&&n.3z!=6V){f.6t(n)}y(n)n.8W()}e.4R.6A(f,e)}});1F.24("17-8V",7(){1K=8;2r+="*{3H:22-2X}";8.3j=(29<5.5)?7(e){z e.1I}:7(e){z e.D.3j};8.3H=7(e){y(!1K.3j(e)){e.1r.2b="6T";y(e.D.6U=="1P")e.J.6U="2y";4k(e)}};7 4k(e){y(e!=1J&&e.D.1y!="3C"){4p(e);8U(e)}};6 3l=14.1i("3l");
 6 5t=14.1i("5t");7 4p(e){y(!e.J.3k){6 p=e.59;y(p&&1K.3j(p)&&e==3l(p))z;6 f=3l(e);y(f&&f.D.8T=="1M"&&1K.3j(f)){4p(f);m=5s(e,e.D.3k);c=5s(f,f.D.3k);y(m<0||c<0){e.J.3k=m+c}1d{e.J.3k=4g.3g(c,m)}f.J.3k="8S"}}};1k(23(4p).13(/5c/g,"6N").13(/4N/g,"8R"));7 5s(e,v){z(v=="1P")?0:3c(e,v)};6 U=/^[.\\d][\\w%]*$/,A=/^(1P|6T)$/,N="[.\\\\d]";6 4l,6S;7 6O(e){4l(e);6S(e)};7 2g(H){4l=7(e){y(!3d.Z(e.D.12))2g(e);4k(e)};7 2g(e,v){y(!e.J.3J){y(!v)v=e.D.12;e.J.3J=(U.Z(v))?4g.3g(0,2B(e,v)):v;2T(e,"12",e.J.3J)}};7 5r(e){y(!3D(e)){6 l=e.3B;1D(l&&!1K.3j(l))l=l.3B}z(l||1J).1I};7 1H(e,v){y(3d.Z(v))z 25(4c(v)/3w*5r(e));z 3c(e,v)};6 2B=7(e,v){6 b=e.D["2X-5o"]=="3G-2X";6 a=0;y(2m&&!b)a+=4n(e)+3K(e);1d y(!2m&&b)a-=4n(e)+3K(e);z 1H(e,v)+a};7 4n(e){z e.2S-e.1I};7 3K(e){z 1H(e,e.D.8Q)+1H(e,e.D.8P)};1k(23(3K).13(/6R/g,"6Q").13(/8O/g,"8N"));2r+="*{1A:1M;27:1M;3I-12:1M;3g-12:1M}";7 1A(e){y(e.D["3I-12"]!=1U){e.1r.1A=e.D["3I-12"]}y(20(1A,e,e.D.1A!="1M")){1K.3H(e);2g(e);4m(e)}};1k(23(1A).13(/3I/g,"3g"));1K.1A=1A;1K.27
 =27;7 4m(e){6 r=e.54();6 w=r.1W-r.1f;y(e.D.1A!="1M"&&w<=2B(e,e.D.1A)){e.J.12=2B(e,e.D.1A)}1d y(e.D.27!="1M"&&w>=2B(e,e.D.27)){e.J.12=2B(e,e.D.27)}1d{e.J.12=e.J.3J}};7 2x(e){y(20(2x,e,/^(2z|3C)$/.Z(e.D.1y)&&4o(e,"1f")!="1P"&&4o(e,"1W")!="1P"&&A.Z(4o(e,"12")))){5p(e);1K.3H(e)}};1K.2x=2x;7 5p(e){6 l=1H(e,e.J.52||e.D.1f);6 w=5r(e)-1H(e,e.D.1W)-l-8M(e);y(25(e.J.12)==w)z;e.J.12="";y(3D(e)||H||e.2S<w){y(!2m)w-=4n(e)+3K(e);y(w<0)w=0;e.J.3J=w;2T(e,"12",w)}};6 5q=0;4U(7(){6 i,w=(5q<1J.1I);5q=1J.1I;O(i 28 1A.1O){6 e=1A.1O[i];6 f=(25(e.J.12)==2B(e,e.D.1A));y(w&&f)e.J.12="";y(w==f)4m(e)}O(i 28 27.1O){6 e=27.1O[i];6 f=(25(e.J.12)==2B(e,e.D.27));y(!w&&f)e.J.12="";y(w!=f)4m(e)}O(i 28 2x.1O)5p(2x.1O[i])});y(26.8L!==1z){V.1n("12",N,2m?4l:4k)}V.1n("3I-12",N,1A);V.1n("3g-12",N,27);V.1n("1W",N,2x)};V.1n("3G-6P",N,7(e){y(e.D.8K!="8J"){e.8I=3c(e,e.D["3G-6P"])}});V.1n("2X-5o","22-2X",8.3H);V.1n("2X-5o","3G-2X",6O);6 1v=11 2U;1v.15(/6v/,"6u");1v.15(/12/,"2b");1v.15(/6w/,"5c");1v.15(/1f/,"2y");1v.15(
 /8H/,"6N");1v.15(/1W/,"56");1k(1v.2V(23(2g)));2g();8G(1o)});1F.24("17-8F",7(){y(29<5.5)z;6 A="6J.5n.8E";6 F="6K:"+A+"(1l=\'%1\',8D=\'6H\')";6 5j=11 1N((26.8C||"-8B.8A")+"$","i");6 3h=[];7 5f(e){6 f=e.6M[A];y(f){f.1l=e.1l;f.6L=1o}1d{e.J.5m=F.13(/%1/,e.1l);3h.1b(e)}e.1l=4i};7 6y(e){e.1l=e.4D;e.6M[A].6L=1z};V.3i(/6I\\s*:\\s*([\\d.]+)/,7(m,o){z"6G:1;5m:6K:6J.5n.8z(6I="+((4c(m[o+1])*3w)||1)+")"});6 B=/5e(-5i)?\\s*:\\s*([^\\(};]*)43\\(([^\\)]+)\\)([^;}]*)/;V.3i(B,7(m,o){6 u=42(m[o+3]);z 5j.Z(u)?"5m:"+F.13(/6H/,"8y").13(/%1/,u)+";6G:1;5e"+(m[o+1]||"")+":"+(m[o+2]||"")+"1M"+(m[o+4]||""):m[o]});y(1s){1s.1n("5d,5l",7(e){y(e.2W=="8x"&&e.2L!="5i")z;5k(e);1j(e,"8w",7(){y(!4j&&60.8v=="1l"&&e.1l.6F(4i)==-1)5k(e)})});6 6D=/^3W:.*;6E/i;6 6C=1Z("17-6E.8u",2k);7 5k(e){y(5j.Z(e.1l)){6 i=11 8t(e.12,e.2b);i.8s=7(){e.12=i.12;e.2b=i.2b;i=1U};i.1l=e.1l;e.4D=e.1l;5f(e)}1d y(6D.Z(e.1l)){e.1l=6C+"?"+e.1l.1q(5)}};6 I=/^5i/i;6 6B=1Z("17-2M.4C",2k);1s.1n("2M",7(e){y(I.Z(e.2L)){6 o=16.3X("<2M 2L=68/x-67>")
 ;o.1r.12=e.D.12;o.1r.2b=e.D.2b;o.3W=6B;6 u=1Z(e.3W,3F(5h.2u));e.4R.6A(o,e);14.5g("2M");4H(o,"",u);z o}})}6 4j=1z;1j(26,"6z",7(){4j=1o;O(6 i=0;i<3h.K;i++)6y(3h[i])});1j(26,"8r",7(){O(6 i=0;i<3h.K;i++)5f(3h[i]);4j=1z})});1F.24("17-2z",7(){V.1n("1y","2z",4a,"3C");V.1n("5e(-8q)?","[^};]*2z",4b);6 4Z=(2m)?"1X":"2F";6 4h=7(){y(1X.D.5b!="2z"){y(1X.D.5a=="1M"){1X.J.8p="8o-8n";1X.J.5a="43("+4i+")"}1X.J.5b="2z"}4h=2s};6 2h=6x("5d");7 1v(f){z 2A.2V(23(f))};6 2A=11 2U;2A.15(/6w/,"5c");2A.15(/1f/,"2y");2A.15(/6v/,"6u");2A.15(/12/,"2b");2A.15(/1W/,"56");2A.15(/X/,"Y");7 3f(e){z(e)?3D(e)||3f(e.59):1z};7 4f(e,p,3e){32("16.1Y."+e.2a+".J.4f(\'"+p+"\',\'"+3e+"\')",0)};7 4b(e){y(20(4b,e,e.D.5b=="2z"&&!e.61(1X))){4h();58(e);8m(e);4V(e)}};7 4V(e){2h.1l=e.D.5a.1q(5,-2);6 p=(e.6c)?e:e.59;p.6t(2h);57(e);8l(e);p.8k(2h)};7 58(e){e.1r.3E=e.D.3E;y(!3f(e)){6 3e="(25(J.3A)+16."+4Z+".6s)||0";4f(e,"3E",3e)}};1k(1v(58));7 57(e){6 p=3f(e)?"3E":"3A";e.J[p]=55(e,e.1r.3E)-e.54().1f-e.8j+2};1k(1v(57));7 55(e,p){3
 4(p){1m"1f":1m"2y":z 0;1m"1W":1m"56":z 1J.1I-2h.2S;1m"8i":z(1J.1I-2h.2S)/2;8h:y(3d.Z(p)){z 25((1J.1I-2h.2S)*4c(p)/3w)}2h.1r.1f=p;z 2h.3A}};1k(1v(55));7 4a(e){y(20(4a,e,3D(e))){2T(e,"1y","3C");2T(e,"1f",e.D.1f);2T(e,"2y",e.D.2y);4h();y(1K)1K.2x(e);49(e)}};7 49(e,r){8g(e,r);4Y(e,r,1o);y(!e.J.4d&&e.D.4X=="1P"&&e.D.1W!="1P"){6 l=1J.1I-1H(e,e.D.1W)-1H(e,e.J.52)-e.1I;y(e.D.8f=="1P")l=25(l/2);y(3f(e.3B))e.J.4e+=l;1d e.J.50=l}53(e);8e(e)};7 53(e){y(e.D.12!="1P"){6 r=e.54();6 w=e.2S-1J.1I+r.1f-2;y(w>=0){w=4g.3g(3c(e,e.D.12)-w,0);2T(e,"12",w)}}};1k(1v(53));7 4Y(e,r){y(!r&&3d.Z(e.D.12)){e.J.2g=e.D.12}y(e.J.2g){e.J.12=1H(e,e.J.2g)}y(r){y(!e.J.4d)z}1d{e.J.50=0;e.J.52=e.D.1f;e.J.4d=e.D.1W!="1P"&&e.D.1f=="1P"}e.J.1f="";e.J.51=4W(e);e.J.4e=e.J.51;y(!r&&!3f(e.3B)){6 3e="J.51+J.50+16."+4Z+".6s";4f(e,"4e",3e)}};1k(1v(4Y));7 4W(e){6 s=e.3A,n=1;y(e.J.4d){s=1J.1I-e.2S-1H(e,e.D.1W)}y(e.D.4X!="1P"){s-=1H(e,e.D.4X)}1D(e=e.3B){y(e.D.1y!="8d")n=-1;s+=e.3A*n}z s};1k(1v(4W));7 1H(e,v){y(3d.Z(v))z 25(4c(
 v)/3w*1J.1I);z 3c(e,v)};1k(1v(1H));7 6r(){6 e=4b.1O;O(6 i 28 e)4V(e[i]);e=4a.1O;O(i 28 e){49(e[i],1o);49(e[i],1o)}48=0};6 48;4U(7(){y(!48)48=32(6r,0)})});1F.24("17-8c-1V",7(){14.24("8b-8a",7(){1V[">"]=7(r,f,t,n){6 e,i,j;O(i=0;i<f.K;i++){6 s=6q(f[i]);O(j=0;(e=s[j]);j++)y(4T(e,t,n))r.1b(e)}};1V["+"]=7(r,f,t,n){O(6 i=0;i<f.K;i++){6 e=47(f[i]);y(e&&4T(e,t,n))r.1b(e)}};1V["@"]=7(r,f,a){6 t=2R[a].Z;6 e,i;O(i=0;(e=f[i]);i++)y(t(e))r.1b(e)};1B["4N-89"]=7(e){z!4S(e)};1B["4Q"]=7(e,c){c=11 1N("^"+c,"i");1D(e&&!e.2H("4Q"))e=e.4R;z e&&c.Z(e.2H("4Q"))};1p.6p=/\\\\:/g;1p.3x="@";1p.3a={};1p.13=7(m,a,n,c,v){6 k=8.3x+m;y(!2R[k]){a=8.3Z(a,c||"",v||"");2R[k]=a;2R.1b(a)}z 2R[k].1c};1p.38=7(s){s=s.13(8.6p,"|");6 m;1D(m=s.19(8.19)){6 r=8.13(m[0],m[1],m[2],m[3],m[4]);s=s.13(8.19,r)}z s};1p.3Z=7(p,t,v){6 a={};a.1c=8.3x+2R.K;a.66=p;t=8.3a[t];t=t?t(8.2H(p),2w(v)):1z;a.Z=11 3b("e","z "+t);z a};1p.2H=7(n){34(n.5Z()){1m"1c":z"e.1c";1m"3U":z"e.1x";1m"O":z"e.6o";1m"2u":y(4P){z"23((e.3z.19(/2u=\\\\46?([^\\\
 \s\\\\46]*)\\\\46?/)||[])[1]||\'\')"}}z"e.2H(\'"+n.13(N,":")+"\')"};1p.3a[""]=7(a){z a};1p.3a["="]=7(a,v){z a+"=="+2c.15(v)};1p.3a["~="]=7(a,v){z"/(^| )"+4O(v)+"( |$)/.Z("+a+")"};1p.3a["|="]=7(a,v){z"/^"+4O(v)+"(-|$)/.Z("+a+")"};6 6n=45;45=7(s){z 6n(1p.38(s))}});6 1p=14.1i("1p");6 H=/a(#[\\w-]+)?(\\.[\\w-]+)?:(65|62)/i;6 6l=/\\s*\\{\\s*/,6m=/\\s*\\}\\s*/,C=/\\s*\\,\\s*/;6 F=/(.*)(:4N-(88|87))/;3y.1C.1e({38:7(){8.1T();6 o=V.2t.K;6 2Q=8.18.2O(6m),r;6 2f,c,i,j;O(i=0;i<2Q.K;i++){r=2Q[i].2O(6l);2f=r[0].2O(C);c=r[1];O(j=0;j<2f.K;j++){2f[j]=c?8.6k(2f[j],c):""}2Q[i]=2f.2p("\\n")}8.18=2Q.2p("\\n");8.2t=V.2t.1q(o)},1u:7(){6 r,i;O(i=0;(r=8.2t[i]);i++)r.1u()},6k:7(s,c){y(V.6j.Z(s)){6 m;y(m=s.19(1L.39)){z 11 1L(m[1],m[2],c)}1d y(m=s.19(2d.39)){y(!2l||!H.Z(m)||2d.44.Z(m)){z 11 2d(s,m[1],m[2],m[3],c)}}1d z 11 1G(s,c)}z s+" {"+c+"}"}});V.1e({2t:[],1B:14.1i("1B"),36:{},2e:14.1i("2e"),1G:1G,2d:2d,1L:1L,2J:2J,2q:7(){6 p=8.1B+"|6i|6h|"+8.36;p=p.13(/(21|37)\\|/g,"");8.6j=11 1N("[>+~\\[]|([:.])[\
 \\\w-()]+\\\\1|:("+p+")");6 c="[^\\\\s(]+\\\\s*[+~]|@\\\\d+|:(";1G.44=11 1N(c+p+")","g");2d.44=11 1N(c+8.1B+")","g");2d.39=11 1N("(.*):("+8.36+")(.*)");1L.39=/(.*):(6i|6h).*/;8.1T()},1u:7(){8.2P.1u();8.1T()},2w:7(s,p){z 2v?(3T(s.2u,p)||s.18):8.1T(s)},1j:7(e,t,h){1j(e,t,h)}});7 1G(s,c){8.1c=V.2t.K;8.1x=1G.3x+8.1c;s=(s).19(F)||s||"*";8.40=s[1]||s;8.4M=1G.6g(8.40)+"."+8.1x+(s[2]||"");8.18=c;8.39=11 1N("\\\\s"+8.1x+"(\\\\s|$)","g");V.2t.1b(8);8.1S()};1t.1e({1h:1G,1g:7(){z 8.4M+" {"+8.18+"}"},1S:2s,15:7(e){e.1x+=" "+8.1x},3v:7(e){e.1x=e.1x.13(8.39,"$1")},1u:7(){6 m=V.2e[" *."+8.1x]=14(8.40);O(i=0;i<m.K;i++)8.15(m[i])}});1G.3x="5Y";1G.6f=/>/g;1G.6g=7(s){s=1p.38(s);z s.13(8.44,"").13(8.6f," ")};7 2d(s,a,d,t,c){8.6e=a||"*";8.6d=V.36[d];8.4L=t;8.1T(s,c)};1G.1e({1h:2d,1u:7(){6 m=14(8.6e);O(6 i=0;i<m.K;i++){6 t=(8.4L)?14(8.4L,m[i]):[m[i]];y(t.K)8.6d.2q(m[i],t,8)}}});6 A=/^4I/;6 U=/^43\\s*\\(\\s*([^)]*)\\)$/;6 M={86:"85",84:"83",82:"81",80:"7Z"};6 6b=1Z("17-22.4C",2k)+"?";2r+=".2K{4K:1M
 }";7 1L(s,p,c){8.1y=p;6 2N=c.19(1L.6a),m,e;y(2N){2N=2N[1];m=2N.2O(/\\s+/);O(6 i=0;(e=m[i]);i++){m[i]=A.Z(e)?{4I:e.1q(5,-1)}:(e.7Y(0)=="\'")?42(e):41(e)}2N=m}8.22=2N;8.1T(s,41(c))};1G.1e({1h:1L,1g:7(){z"."+8.1x+"{4K:7X}"},1S:7(){8.19=14(8.40);O(6 i=0;i<8.19.K;i++){6 r=8.19[i].J;y(!r[8.1y])r[8.1y]={18:""};r[8.1y].18+=";"+8.18;y(8.22!=1U)r[8.1y].22=8.22}},1u:7(){y(8.22==1U)z;O(6 i=0;i<8.19.K;i++){8.3Z(8.19[i])}},3Z:7(t){6 g=t.J[8.1y];y(g){6 c=[].4J(g.22||"");O(6 j=0;j<c.K;j++){y(3Y c[j]=="2M"){c[j]=t.2H(c[j].4I)}}c=c.2p("");6 u=c.19(U);6 h=1L[u?"69":"4E"].13(/%1/,8.1x);6 4G=g.18.13(/\'/g,\'"\');6 4F=M[8.1y+7W(t.6c)];y(u){6 p=16.3X(h);t.7V(4F,p);p.3W=6b;4H(p,4G,2c.3v(u[1]))}1d{h=h.13(/%2/,4G).13(/%3/,c);t.7U(4F,h)}t.J[8.1y]=1U}}});1L.6a=/22\\s*:\\s*([^;]*)(;|$)/;1L.69="<2M 3U=\'2K %1\' 2K 12=3w% 2b=0 2L=68/x-67>";1L.4E="<17:! 3U=\'2K %1\' 2K 1r=\'%2\'>%3</17:!>";7 2J(n,a){8.66=n;8.2q=a;8.2I={};V.36[n]=8};1t.1e({1h:2J,20:7(i){6 c=i[2];i.1c=c.1c+i[0].2a;y(!8.2I[i.1c]){6 t=i[1],j;O
 (j=0;j<t.K;j++)c.15(t[j]);8.2I[i.1c]=i}},35:7(i){y(8.2I[i.1c]){6 c=i[2];6 t=i[1],j;O(j=0;j<t.K;j++)c.3v(t[j]);3u 8.2I[i.1c]}}});V.1B.1g=7(){6 t=[],p;O(p 28 8){y(8[p].K>1)p+="\\\\([^)]*\\\\)";t.1b(p)}z t.2p("|")};V.1B["21"]=7(e){z e.D["17-21"]=="21"};V.1B["37"]=7(e){z e.D["17-21"]=="37"};6 64=(29<5.5)?"7T":"7S";6 63=(29<5.5)?"7R":"7Q";V.36.1g=V.1B.1g;6 3s=11 2J("65",7(e){6 i=1a;V.1j(e,64,7(){3s.20(i)});V.1j(e,63,7(){3s.35(i)})});6 3t=11 2J("7P",7(e){6 i=1a;V.1j(e,"7O",7(){3t.35(i);3t.20(i)});V.1j(e,"7N",7(){3t.35(i)});y(e==16.7M){3t.20(i)}});6 3V=11 2J("62",7(e){6 i=1a;V.1j(e,"7L",7(){3V.20(i)})});1j(16,"7K",7(){6 i=3V.2I,j;O(j 28 i)3V.35(i[j]);i=3s.2I;O(j 28 i)y(!i[j][0].61(60.7J))3s.35(i[j])});2o(1p);1p.1e({2H:7(n){34(n.5Z()){1m"3U":z"e.1x.13(/\\\\b\\\\s*5Y\\\\d+/g,\'\')";1m"1l":z"(e.4D||e.1l)"}z 8.1T(n)}});2n.15(/::/,":");3r.15(/\\\\([\\7I-7H-F]{1,4})/,7(m,o){m=m[o+1];z"\\\\u"+"7G".1q(m.K)+m})});2G=1o;y(2m)1k(3T("17-7F.5X",2k));V.1S();y(2l&&1s)1s.1S();y(33)1F.1S();1d{2F.7E
 (1Z("17-3q.4C",2k));1j(16,"7D",7(){y(16.5W=="33")32(1F.1S,0)})}}2j(e){31("4B [0]: "+e.5V)}5U{}};',62,634,'||||||var|function|this||||||||||||||||||||||||||if|return||||currentStyle||||||runtimeStyle|length||||for|||||||ie7CSS||||test||new|width|replace|cssQuery|add|document|ie7|cssText|match|arguments|push|id|else|specialize|left|toString|constructor|valueOf|addEventHandler|eval|src|case|addRecalc|true|AttributeSelector|slice|style|ie7HTML|Common|recalc|_0|that|className|position|false|minWidth|pseudoClasses|prototype|while|fr|IE7|Rule|getPixelWidth|clientWidth|viewport|ie7Layout|PseudoElement|none|RegExp|elements|auto|recalcs|styleSheets|init|inherit|null|selectors|right|body|all|makePath|register|link|content|String|addModule|parseInt|window|maxWidth|in|appVersion|uniqueID|height|Quote|DynamicRule|cache|se|fixWidth|_1|klass|catch|path|isHTML|quirksMode|encoder|ICommon|join|apply|HEADER|DUMMY|rules|href|httpRequest|getText|fixRight|top|fixed|_2|getFixedWidth|try|thisElement
 |ancestor|documentElement|loaded|getAttribute|instances|DynamicPseudoClass|ie7_anon|type|object|co|split|screen|ru|attributeSelectors|offsetWidth|setOverrideStyle|ParseMaster|exec|tagName|box|print|media|_3|alert|setTimeout|complete|switch|unregister|dynamicPseudoClasses|visited|parse|MATCH|tests|Function|getPixelValue|PERCENT|ex|_4|max|_5|addFix|hasLayout|marginTop|firstElementChild|disabled|value|size|font|load|safeString|_6|_7|delete|remove|100|PREFIX|StyleSheet|outerHTML|offsetLeft|offsetParent|absolute|isFixed|backgroundPositionX|getPath|border|boxSizing|min|fixedWidth|getPaddingWidth|fixes|Parser|self|styleSheet|st|modules|_8|x01|loadFile|class|_9|data|createElement|typeof|create|selector|decode|getString|url|COMPLEX|parseSelector|x22|nextElementSibling|_10|_11|_12|_13|parseFloat|autoLeft|pixelLeft|setExpression|Math|_14|BLANK_GIF|_15|_16|applyWidth|resizeWidth|getBorderWidth|getDefinedStyle|collapseMarginTop|_17|_18|_19|refresh|el|ca|getElementsByTagName|version|escap
 eChar|ancestorOf|_20|Error|htc|pngSrc|ANON|po|cs|addTimer|attr|concat|display|target|selectorText|first|regEscape|isMSIE|lang|parentNode|previousElementSibling|compareTagName|addResize|_21|getScreenLeft|marginLeft|positionLeft|_22|shiftLeft|screenLeft|_23|clipWidth|getBoundingClientRect|getOffsetLeft|bottom|setOffsetLeft|backgroundLeft|parentElement|backgroundImage|backgroundAttachment|Top|img|background|_24|clearCache|location|image|_25|_26|input|filter|Microsoft|sizing|resizeRight|_27|layoutWidth|_28|lastElementChild|clicked|submit|_29|Fix|_30|decoder|_31|parser|_32|gi|_33|styles|createStyleSheet|isXML|caching|_34|compareNamespace|getDocument|continue|links|error|callee|ignoreCase|_35|_36|ie7_debug|finally|description|readyState|js|ie7_class|toLowerCase|event|contains|active|_37|_38|hover|name|scriptlet|text|OBJECT|CONTENT|_39|canHaveChildren|dynamicPseudoClass|attach|CHILD|simple|after|before|UNKNOWN|createRule|B1|B2|_40|htmlFor|NS_IE|childElements|_41|scrollLeft|appendCh
 ild|Height|Width|Left|createTempElement|_42|onbeforeprint|replaceChild|_43|_44|B64|base64|indexOf|zoom|scale|opacity|DXImageTransform|progid|enabled|filters|Bottom|borderBox|spacing|margin|padding|applyHeight|0cm|verticalAlign|en|nextSibling|_45|title|_46|BUTTON|button|reset|onclick|clearInterval|PIXEL|removeEventHandler|pop|_47|_48|lastIndexOf|_49|_50|_51|_52|imports|trash|ie7_recalc|getInlineStyles|innerHTML|nodeType|ST|select|Array|_53|mimeType|Boolean|lastChild|firstChild|ES|_54|_55|_56|_57|_58|DE|status|scripts|search|onreadystatechange|addBehavior|quirks|0000|fA|da|srcElement|onmouseup|onmousedown|activeElement|onblur|onfocus|focus|onmouseleave|onmouseout|onmouseenter|onmouseover|insertAdjacentHTML|insertAdjacentElement|Number|inline|charAt|beforeEnd|after1|afterEnd|after0|afterBegin|before1|beforeBegin|before0|letter|line|child|level2|css|css2|static|clipHeight|marginRight|positionTop|default|center|clientLeft|removeChild|setOffsetTop|backgroundTop|repeat|no|backgroun
 dRepeat|attachment|onafterprint|onload|Image|php|propertyName|onpropertychange|INPUT|crop|Alpha|png|trans|IE7_PNG_SUFFIX|sizingMethod|AlphaImageLoader|graphics|fixHeight|Right|cellSpacing|collapse|borderCollapse|IE7_BOX_MODEL|getMarginWidth|Margin|Padding|paddingRight|paddingLeft|last|0px|styleFloat|collapseMarginBottom|layout|removeNode|HTML|alt|onsubmit|form|click|textarea|label|abbr|pow|67em|h6|83em|h5|1em|h4|17em|h3|5em|h2|2em|h1|html4|setInterval|9999|rect|clip|block|px|onunload|detachEvent|attachEvent|onresize|x27|import|namespace|ma|bprint|bscreen|ball|write|item|innerText|getTextContent|scopeName|toUpperCase|ownerDocument|Document|XML|URL|xml|unknown|previousSibling|number|successfully|ie5|instanceOf|Object|common|caller|gif|blank|file|loading|responseText|200|send|GET|open|XMLHTTP|ActiveXObject|Msxml2|ScriptEngineMajorVersion|ms_|ie7_off|CSS1Compat|compatMode|MSIE|navigator|alpha'.split('|'),0,{}))
diff --git a/web/static/js/ie7/ie7-xml-extras.js b/web/static/js/ie7/ie7-xml-extras.js
new file mode 100644
index 0000000..97846f6
--- /dev/null
+++ b/web/static/js/ie7/ie7-xml-extras.js
@@ -0,0 +1,6 @@
+/*
+	IE7, version 0.9 (alpha) (2005-08-19)
+	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
+	License: http://creativecommons.org/licenses/LGPL/2.1/
+*/
+function XMLHttpRequest(){var l=(ScriptEngineMajorVersion()>=5)?"Msxml2":"Microsoft";return new ActiveXObject(l+".XMLHTTP")};function DOMParser(){};DOMParser.prototype={toString:function(){return"[object DOMParser]"},parseFromString:function(s,c){var x=new ActiveXObject("Microsoft.XMLDOM");x.loadXML(s);return x},parseFromStream:new Function,baseURI:""};function XMLSerializer(){};XMLSerializer.prototype={toString:function(){return"[object XMLSerializer]"},serializeToString:function(r){return r.xml||r.outerHTML},serializeToStream:new Function};
diff --git a/web/static/js/ie7/ie7.gif b/web/static/js/ie7/ie7.gif
new file mode 100644
index 0000000..64a2c2d
Binary files /dev/null and b/web/static/js/ie7/ie7.gif differ
diff --git a/web/static/js/ie7/test-trans.png b/web/static/js/ie7/test-trans.png
new file mode 100644
index 0000000..e187e2c
Binary files /dev/null and b/web/static/js/ie7/test-trans.png differ
diff --git a/web/static/js/ie7/test.html b/web/static/js/ie7/test.html
new file mode 100644
index 0000000..ab78f46
--- /dev/null
+++ b/web/static/js/ie7/test.html
@@ -0,0 +1,100 @@
+<html xmlns:html="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+<title>IE7 Test Page</title>
+<meta name="author" content="Dean Edwards"/>
+<!-- compliance patch for microsoft browsers -->
+<!--[if lt IE 7]>
+<script src="ie7-standard-p.js" type="text/javascript"></script>
+<script src="ie7-css3-selectors.js" type="text/javascript"></script>
+<script src="ie7-css-strict.js" type="text/javascript"></script>
+<![endif]-->
+<style type="text/css">
+ body {background-color: #ccc;}
+ img {border: none;}
+ h1 {font-family: monospace;}
+ h2 {background-color: black; color: white; font-style: normal;}
+ h3 {margin: 0.1em 0;}
+</style>
+</head>
+
+<body>
+<div class="document">
+<div class="header">
+<h1>IE7 { css2: auto; }</h1>
+<hr />
+</div>
+
+<div class="content">
+
+<h2>Black & White Test</h2>
+
+<h3>Legend</h3>
+<style type="text/css">
+ div.legend {height: 20px; font-weight: bold; text-indent: 4px;}
+ #fail {background-color: black; color: white;}
+ #pass {background-color: white; color: black;}
+</style>
+<div class="legend" id="pass">PASS</div>
+<div class="legend" id="fail">FAIL</div>
+
+<hr />
+
+<h3>ie7-html4.js</h3>
+<style type="text/css">
+ #ie7-html4 {background-color: black; height: 20px;}
+ #ie7-html4 abbr {display: block; background-color: white; height: 20px;}
+</style>
+<div id="ie7-html4"><abbr> </abbr></div>
+
+<h3>ie7-layout.js</h3>
+<style type="text/css">
+ #ie7-layout {background-color: black; height: 20px; overflow: hidden;}
+ #ie7-layout div.box {position: relative; top: -40px; background-color: white;
+   height: 40px; border-top: 20px black solid;}
+</style>
+<div id="ie7-layout"><div class="box"></div></div>
+
+<h3>ie7-graphics.js</h3>
+<style type="text/css">
+ #ie7-graphics {background-color: white; height: 20px;}
+ #ie7-graphics div.box {height: 20px; background: url(test-trans.png);}
+</style>
+<div id="ie7-graphics"><div class="box"></div></div>
+
+<h3>ie7-fixed.js</h3>
+<style type="text/css">
+ #ie7-fixed {background-color: white; height: 20px;}
+ #ie7-fixed div.box {position: fixed; top: -20px; background-color: black; height: 20px;}
+</style>
+<div id="ie7-fixed"><div class="box"></div></div>
+
+<h3>ie7-css2-selectors.js</h3>
+<style type="text/css">
+ #ie7-css2-selectors {background-color: black; height: 20px;}
+ #ie7-css2-selectors > span {display: block; background-color: white; height: 20px;}
+</style>
+<div id="ie7-css2-selectors"><span>&nbsp</span></div>
+
+<h3>ie7-css3-selectors.js</h3>
+<style type="text/css">
+ #ie7-css3-selectors {background-color: black; height: 20px;}
+ #ie7-css3-selectors:empty {background-color: white;}
+</style>
+<div id="ie7-css3-selectors"></div>
+
+<h3>ie7-css-strict.js</h3>
+<style type="text/css">
+ #ie7-css-strict {background-color: black; height: 20px;}
+ #ie7-css-strict > span.strict {display: block; background-color: white; height: 20px;}
+ #ie7-css-strict > span {display: block; background-color: black}
+</style>
+<div id="ie7-css-strict"><span class="strict"></span></div>
+</div>
+
+<div class="footer">
+<hr />
+<a href="http://dean.edwards.name/IE7/"><img src="ie7.gif" width="80" height="15" alt="IE7 Enhanced"/></a>
+</div>
+</div>
+</body>
+</html>
diff --git a/web/static/js/jifty.js b/web/static/js/jifty.js
new file mode 100644
index 0000000..e674eb4
--- /dev/null
+++ b/web/static/js/jifty.js
@@ -0,0 +1,333 @@
+/* An empty class so we can create things inside it */
+var Jifty = Class.create();
+
+/* General methods for dealing with forms, actions, and fields */
+/* Actions */
+var Action = Class.create();
+Action.prototype = {
+    // New takes the moniker, a string
+    initialize: function(moniker) {
+        this.moniker = moniker;
+
+        this.register = $('J:A-' + this.moniker);  // Simple case -- no ordering information
+        if (! this.register) {
+            // We need to go looking
+            var elements = document.getElementsByTagName('input');
+            for (var i = 0; i < elements.length; i++) {
+                if (Form.Element.getMoniker(elements[i]) == this.moniker) {
+                    this.register = elements[i];
+                    break;
+                }
+            }
+        }
+
+        this.form = Form.Element.getForm(this.register);
+        this.actionClass = this.register.value;
+    },
+
+    // Returns an Array of all fields in this Action
+    fields: function() {
+        var elements = new Array;
+        var possible = Form.getElements(this.form);
+
+        for (var i = 0; i < possible.length; i++) {
+            if (Form.Element.getMoniker(possible[i]) == this.moniker)
+                elements.push(possible[i]);
+        }
+        return elements;
+    },
+
+    // Serialize and return all fields needed for this action
+    serialize: function() {
+        var fields = this.fields();
+        var serialized = new Array;
+
+        for (var i = 0; i < fields.length; i++) {
+            serialized.push(Form.Element.serialize(fields[i].id));
+        }
+        return serialized.join('&');
+    },
+
+    // Validate the action
+    validate: function() {
+        show_wait_message();
+        var id = this.register.id;
+
+        new Ajax.Request(
+            '/validator.xml',  // Right now, the URL is actually completely irrelevant
+            {
+                asynchronous: 1,
+                method: "get",
+                parameters: this.serialize() + "&J:VALIDATE=1",
+                onComplete:
+                    function (request) {
+                        var response  = request.responseXML.documentElement;
+                        for (var action = response.firstChild; action != null; action = action.nextSibling) {
+                            if ((action.nodeName != 'action') || (action.getAttribute("id") != id))
+                                continue;
+                            for (var field = action.firstChild; field != null; field = field.nextSibling) {
+                                // Possibilities for field.nodeName: it could be #text (whitespace),
+                                // or 'blank' (the field was blank, don't mess with the error div), or 'ok'
+                                // (clear the error div!) or 'error' (fill in the error div!)
+                                if (field.nodeName == 'error') {
+                                    var err_div = document.getElementById(field.getAttribute("id"));
+                                    if (err_div != null) {
+                                        err_div.innerHTML = field.firstChild.data;
+                                    }
+                                } else if (field.nodeName == 'ok') {
+                                    var err_div = document.getElementById(field.getAttribute("id"));
+                                    if (err_div != null) {
+                                        err_div.innerHTML = '';
+                                    }
+                                }
+                            }
+                        }
+                        return true;
+                    }
+            }
+        ); 
+        hide_wait_message();
+        return false;
+    },
+
+    submit: function() {
+        show_wait_message();
+        new Ajax.Request(
+            '/empty',
+            { parameters: this.serialize() }
+        );
+        hide_wait_message();
+    }
+};
+
+
+
+/* Forms */
+// Return an Array of Actions that are in this form
+Form.getActions = function (element) {
+    var elements = new Array;
+    var possible = Form.getElements(element);
+
+    for (var i = 0; i < possible.length; i++) {
+        if (Form.Element.isRegistration(possible[i]))
+            elements.push(new Action(Form.Element.getMoniker(possible[i])));
+    }
+
+    return elements;
+};
+
+/* Fields */
+// Get the moniker for this form element
+// Takes an element or an element id
+Form.Element.getMoniker = function (element) {
+     // if we have an element id, get the element itself
+     if (typeof(element) == "string") {
+         element = $(element);    
+    }
+    if (/^J:A:F(:F)*-[^-]+-.+$/.test(element.name)) {
+        var bits = element.name.match(/^J:A:F(?::F)*-[^-]+-(.+)$/);
+        return bits[1];
+    } else if (/^J:A-(\d+-)?.+$/.test(element.name)) {
+        var bits = element.name.match(/^J:A-(?:\d+-)?(.+)$/);
+        return bits[1];
+    } else {
+        return null;
+    }
+};
+
+// Get the Action for this form element
+// Takes an element or an element id
+Form.Element.getAction = function (element) {
+        // if we have an element id, get the element itself
+        if (typeof(element) == "string") {
+            element = $(element);    
+            }
+    var moniker = Form.Element.getMoniker(element);
+    return new Action(moniker);
+}
+
+// Returns true if this form element is the registration for its action
+Form.Element.isRegistration = function (element) {
+    return /^J:A-/.test(element.name)
+};
+
+// Validates the action this form element is part of
+Form.Element.validate = function (element) {
+    Form.Element.getAction(element).validate();
+};
+
+// Form elements should AJAX validate if the CSS says so
+Behaviour.register({
+    'input.ajaxvalidation': function(elt) {
+        elt.onblur = function () {
+            Form.Element.validate(this);
+        } 
+    }
+});
+
+// Look up the form that this element is part of -- this is sometimes
+// more complicated than you'd think because the form may not exist
+// anymore, or the element may have been inserted into a new form.
+// Hence, we may need to walk the DOM.
+Form.Element.getForm = function (element) {
+    if (element.form)
+        return element.form;
+
+    for (var elt = element.parentNode; elt != null; elt = elt.parentNode) {
+        if (elt.nodeName == 'FORM') {
+            element.form = elt;
+            return elt;
+        }
+    }
+    return null;
+}
+
+function serialize(thing) {
+    var serialized = new Array;
+    for (n in thing) {
+        if (typeof(thing[n]) == "string" && thing[n].length)
+            serialized.push(encodeURIComponent(n) + '=' + 
+                            encodeURIComponent(thing[n]));
+    }
+    return serialized.join('&');
+}
+
+var fragments = {};
+var current_args = {};
+function region(name, args, path) {
+    fragments[name] = {name: name, args: args, path: path};
+    current_args[name] = {};
+}
+
+function update_region() {
+    show_wait_message();
+    arguments = arguments[0];
+    var name = arguments['name'];
+
+    var args = {};
+    for (var n in fragments[name].args) {
+        args[n] = fragments[name].args[n];
+    }
+    for (var n in current_args) {
+        if (typeof(current_args[n]) == "string") {
+            args[n] = current_args[n];
+            var parsed = n.match(/J:NV-region-(.*?)\.(.*)/);
+            
+            if ((parsed != null) && (parsed.length == 3) && (parsed[1] == name)) {
+                args[parsed[2]] = current_args[n];
+            }
+        }
+    }
+    for (var n in arguments['args']) {
+        args[n] = arguments['args'][n];
+        if (n.indexOf('J:NV-') != 0) {
+            current_args['J:NV-region-'+name+'.'+n] = args[n];
+            args['J:NV-region-'+name+'.'+n] = args[n];
+        }
+    }
+    var path;
+    if (arguments['fragment'] != null) {
+        path = arguments['fragment'];
+    } else {
+        path = fragments[name].path;
+    }
+    args['J:NV-region-'+name] = path;
+    current_args['J:NV-region-'+name] = path;
+
+    for (var i = 0; i < document.forms.length; i++) {
+        var form = document.forms[i];
+        for (var n in args) {
+            if ((typeof(args[n]) == "string") && (/^J:NV-/.test(n))) {
+                if (form[n]) {
+                    form[n].value = args[n];
+                } else {
+                    var hidden = document.createElement('input');
+                    hidden.setAttribute('type',  'hidden');
+                    hidden.setAttribute('name',  n);
+                    hidden.setAttribute('id',    n);
+                    hidden.setAttribute('value', args[n]);
+                    form.appendChild(hidden);
+                }
+            }
+        }
+    }
+
+    args['J-NAME'] = name;
+    args['J-PATH'] = document.URL;
+
+    var query = serialize(args);
+    if (arguments['submit']) {
+        var a = new Action(arguments['submit']);
+        query = query + '&' + a.serialize();
+    }
+
+    new Ajax.Updater('region-' + name,
+                     path,
+                     { parameters: query,
+                       onComplete: function () { Behaviour.apply();
+                        hide_wait_message();
+                       },
+                       evalScripts: true }
+                    );
+}
+
+function trace( msg ){
+  if( typeof( jsTrace ) != 'undefined' ){
+    jsTrace.send( msg );
+  }
+}
+
+
+function show_wait_message (){
+        chunk = document.getElementById('jifty-wait-message');
+        if (chunk) {  chunk.style.display= 'block';}
+}
+
+function hide_wait_message (){
+
+        chunk = document.getElementById('jifty-wait-message');
+         if (chunk) { chunk.style.display = "none";}
+}
+
+
+
+Jifty.Autocompleter = Class.create();
+Object.extend(Object.extend(Jifty.Autocompleter.prototype, Ajax.Autocompleter.prototype), {
+  initialize: function(element, update, url, options) {
+          this.baseInitialize(element, update, options);
+    this.options.asynchronous  = true;
+    this.options.onComplete    = this.onComplete.bind(this);
+    this.options.defaultParams = this.options.parameters || null;
+    this.url                   = url;
+  },
+
+  getUpdatedChoices: function() {
+    entry = encodeURIComponent("J:A-autocomplete")
+        + "=" +encodeURIComponent("Jifty::Action::Autocomplete");
+
+    entry += '&' + encodeURIComponent("J:A:F-argument-autocomplete") 
+        + "=" + encodeURIComponent(this.options.paramName);
+      
+    entry += '&' + encodeURIComponent("J:A:F-action-autocomplete") 
+        + "=" + encodeURIComponent(
+                        Form.Element.getMoniker(this.options.paramName)
+                        );
+
+    entry += '&'+ encodeURIComponent("J:ACTIONS") + '=' + encodeURIComponent("autocomplete");
+
+
+    this.options.parameters = this.options.callback ?
+      this.options.callback(this.element, entry) : entry;
+
+    if(this.options.defaultParams)
+      this.options.parameters += '&' + this.options.defaultParams;
+      
+    var action =  Form.Element.getAction(this.options.paramName);
+      this.options.parameters += '&' + action.serialize();
+
+    new Ajax.Request(this.url, this.options);
+  }
+
+
+});
+
diff --git a/web/static/js/jsTrace.js b/web/static/js/jsTrace.js
new file mode 100644
index 0000000..5e8c93e
--- /dev/null
+++ b/web/static/js/jsTrace.js
@@ -0,0 +1,12 @@
+/*------------------------------------------------------------------------------
+Function:       jsTrace()
+Author:         Aaron Gustafson (aaron at easy-designs dot net)
+Creation Date:  26 October 2005
+Version:        1.0
+Homepage:       http://www.easy-designs.net/code/jsTrace/
+License:        Creative Commons Attribution-ShareAlike 2.0 License
+                http://creativecommons.org/licenses/by-sa/2.0/
+Note:           If you change or improve on this script, please let us know by 
+                emailing the author (above) with a link to your demo page.
+------------------------------------------------------------------------------*/
+var jsTrace = { debugging_on: false, window: null, viewport: null, init: function(){ if( !document.getElementsByTagName || !document.getElementById || !document.createElement || !document.createTextNode ) return; jsTrace.createWindow(); jsTrace.debugging_on = true;}, createWindow: function(){ jsTrace.window = document.createElement( 'div' ); jsTrace.window.style.background = '#000'; jsTrace.window.style.font = '80% "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, sans-serif'; jsTrace.window.style.padding = '2px'; jsTrace.window.style.position = 'absolute'; jsTrace.window.style.top = '50px'; jsTrace.window.style.left = '700px'; jsTrace.window.style.height = '360px'; jsTrace.window.style.zIndex = '100'; jsTrace.window.style.minHeight = '150px'; jsTrace.window.style.width = '190px'; jsTrace.window.style.minWidth = '150px'; var x = document.createElement('span'); x.style.border = '1px solid #000'; x.style.cursor = 'pointer'; x.style.color = '#000'; x.style.display = 'bl
 ock'; x.style.lineHeight = '.5em'; x.style.padding = '0 0 3px'; x.style.position = 'absolute'; x.style.top = '4px'; x.style.right = '4px'; jsTrace.addEvent( x, 'click', function(){ jsTrace.kill();} ); x.setAttribute( 'title', 'Close jsTrace Debugger' ); x.appendChild( document.createTextNode( 'x' ) ); jsTrace.window.appendChild( x ); var sh = document.createElement('div'); sh.style.position = 'absolute'; sh.style.bottom = '3px'; sh.style.right = '3px'; var sg = document.createElement('span'); sg.style.border = '5px solid #ccc'; sg.style.borderLeftColor = sg.style.borderTopColor = '#000'; sg.style.cursor = 'pointer'; sg.style.color = '#ccc'; sg.style.display = 'block'; sg.style.height = '0'; sg.style.width = '0'; sg.style.overflow = 'hidden'; sg.setAttribute( 'title', 'Resize the jsTrace Debugger' ); if( typeof( Drag ) != 'undefined' ){ sg.xFrom = 0; sg.yFrom = 0; Drag.init( sg, null, null, null, null, null, true, true ); sg.onDrag = function( x, y ){ jsTrace.resizeX( x, this
  ); jsTrace.resizeY( y, this );}; sh.appendChild( sg ); jsTrace.window.appendChild( sh );} var tools = document.createElement( 'div' ); tools.style.fontSize = '.7em'; tools.style.fontVariant = 'small-caps'; tools.style.lineHeight = '10px'; tools.style.position = 'absolute'; tools.style.bottom = '5px'; tools.style.left = '3px'; var dl = document.createElement( 'span' ); dl.style.color = '#ccc'; dl.style.padding = '0 10px 0 0'; dl.style.overflow = 'hidden'; dl.style.cursor = 'pointer'; dl.setAttribute( 'title', 'Add a Delimeter' ); dl.appendChild( document.createTextNode( 'delimit' ) ); jsTrace.addEvent( dl, 'click', function(){ jsTrace.sendDelimeter();} ); tools.appendChild( dl ); var cl = document.createElement( 'span' ); cl.style.color = '#ccc'; cl.style.padding = '0 10px 0 0'; cl.style.overflow = 'hidden'; cl.style.cursor = 'pointer'; cl.setAttribute( 'title', 'Add a Delimeter' ); cl.appendChild( document.createTextNode( 'clear' ) ); jsTrace.addEvent( cl, 'click', function
 (){ jsTrace.clearWindow();} ); tools.appendChild( cl ); jsTrace.window.appendChild( tools ); var header = document.createElement( 'h3' ); header.style.background = '#ccc'; header.style.color = '#000'; header.style.cursor = 'pointer'; header.style.fontSize = '1em'; header.style.fontVariant = 'small-caps'; header.style.margin = '0 0 2px'; header.style.padding = '5px 10px'; header.style.lineHeight = '15px'; header.appendChild( document.createTextNode( 'jsTrace Debugger' ) ); jsTrace.window.appendChild( header ); jsTrace.viewport = document.createElement( 'pre' ); jsTrace.viewport.style.border = '1px solid #ccc'; jsTrace.viewport.style.color = '#ebebeb'; jsTrace.viewport.style.fontSize = '1.2em'; jsTrace.viewport.style.margin = '0'; jsTrace.viewport.style.padding = '0 3px'; jsTrace.viewport.style.position = 'absolute'; jsTrace.viewport.style.top = '30px'; jsTrace.viewport.style.left = '2px'; jsTrace.viewport.style.overflow = 'auto'; jsTrace.viewport.style.width = ( parseInt( jsT
 race.window.style.width ) - 8 ) + 'px'; jsTrace.viewport.style.height = ( parseInt( jsTrace.window.style.height ) - 45 ) + 'px'; jsTrace.window.appendChild( jsTrace.viewport ); document.getElementsByTagName( 'body' )[0].appendChild( jsTrace.window ); if( typeof( Drag ) != 'undefined' ){ Drag.init( header, jsTrace.window );} }, resizeX: function( x, grip ){ var width = parseInt( jsTrace.window.style.width ); var newWidth = Math.abs( width - ( x - grip.xFrom ) ) + 'px'; if( parseInt( newWidth ) < parseInt( jsTrace.window.style.minWidth ) ) newWidth = jsTrace.window.style.minWidth; jsTrace.window.style.width = newWidth; grip.xFrom = x; jsTrace.viewport.style.width = ( parseInt( jsTrace.window.style.width ) - 8 ) + 'px';}, resizeY: function( y, grip ){ var height = parseInt( jsTrace.window.style.height ); var newHeight = Math.abs( height - ( y - grip.yFrom ) ) + 'px'; if( parseInt( newHeight ) < parseInt( jsTrace.window.style.minHeight ) ) newHeight = jsTrace.window.style.minHei
 ght; jsTrace.window.style.height = newHeight; grip.yFrom = y; jsTrace.viewport.style.height = ( parseInt( jsTrace.window.style.height ) - 45 ) + 'px';}, send: function( text ){ text = text + "<br />"; jsTrace.viewport.innerHTML += text;}, sendDelimeter: function(){ jsTrace.send( '<span style="color: #f00">--------------------</span>' );}, clearWindow: function(){ jsTrace.viewport.innerHTML = '';}, kill: function() { jsTrace.window.parentNode.removeChild( jsTrace.window ); jsTrace.debugging_on = false;}, addEvent: function( obj, type, fn ){ if (obj.addEventListener) obj.addEventListener( type, fn, false ); else if (obj.attachEvent) { obj["e"+type+fn] = fn; obj[type+fn] = function() { obj["e"+type+fn]( window.event );}; obj.attachEvent( "on"+type, obj[type+fn] );} }, removeEvent: function ( obj, type, fn ) { if (obj.removeEventListener) obj.removeEventListener( type, fn, false ); else if (obj.detachEvent) { obj.detachEvent( "on"+type, obj[type+fn] ); obj[type+fn] = null; obj["
 e"+type+fn] = null;} } }; jsTrace.addEvent( window, 'load', jsTrace.init ); 
\ No newline at end of file
diff --git a/web/static/js/key_bindings.js b/web/static/js/key_bindings.js
new file mode 100644
index 0000000..c480f0c
--- /dev/null
+++ b/web/static/js/key_bindings.js
@@ -0,0 +1,69 @@
+// Copyright 2004-2005, Best Practical Solutions, LLC
+// This Library is licensed to you under the same terms as Perl 5.x
+
+var bindings = Array;
+
+document.onkeydown = doClick;
+function doClick(e) {
+    var targ;
+        if (!e) var e = window.event;
+            if (e.target) targ = e.target;
+            else if (e.srcElement) targ = e.srcElement;
+        if (targ.nodeType == 3) // defeat Safari bug
+                targ = targ.parentNode;
+   
+   // safari or mozilla
+   if ( ( ! e.metaKey && ! e.altKey &&  ! e.ctrlKey )
+        && (
+        (targ == document.body) || 
+       (targ ==  document.getElementsByTagName('html')[0])  
+        ) ){
+	var code = String.fromCharCode(e.keyCode);
+    var binding = getKeyBinding(code);
+   if (binding) {
+   if (binding["action"] == "goto") {
+        document.location = (binding["data"]);
+    } 
+   else if (binding["action"] == "focus") {
+      var elements = document.getElementsByName(binding["data"]);
+       elements[0].focus();
+    }
+   else if (binding["action"] == "click") {
+      var elements = document.getElementsByName(binding["data"]);
+       elements[0].click();
+    }
+
+ }     
+
+}
+}
+
+function addKeyBinding(key, action, data, label) {
+    var binding = new Array;
+    binding["action"] = action;
+    binding["data"] = data;
+    binding["label"] = label;
+    bindings[key] = binding;
+}
+
+
+function getKeyBinding(key) {
+    return(bindings[key]);
+}
+
+
+function writeKeyBindingLegend() {
+    var content = '';
+    for  (var key in bindings) {
+    if ( bindings[key]['label']) {
+    content = content + '<dt>'+key + '</dt>' +'<dd>'+bindings[key]['label'] +'</dd>'; 
+    }
+    }
+    if (content) {
+    document.write('<div class="keybindings">');
+    document.write('<dl class="keybindings">');
+    document.write(content);
+    document.write('</dl>');
+    document.write('</div>');
+    }
+}
diff --git a/web/static/js/prototype.js b/web/static/js/prototype.js
new file mode 100644
index 0000000..fbdab91
--- /dev/null
+++ b/web/static/js/prototype.js
@@ -0,0 +1,1038 @@
+/*  Prototype JavaScript framework, version 1.3.1
+ *  (c) 2005 Sam Stephenson <sam at conio.net>
+ *
+ *  THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff
+ *  against the source tree, available from the Prototype darcs repository. 
+ *
+ *  Prototype is freely distributable under the terms of an MIT-style license.
+ *
+ *  For details, see the Prototype web site: http://prototype.conio.net/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+  Version: '1.3.1',
+  emptyFunction: function() {}
+}
+
+var Class = {
+  create: function() {
+    return function() { 
+      this.initialize.apply(this, arguments);
+    }
+  }
+}
+
+var Abstract = new Object();
+
+Object.extend = function(destination, source) {
+  for (property in source) {
+    destination[property] = source[property];
+  }
+  return destination;
+}
+
+Object.prototype.extend = function(object) {
+  return Object.extend.apply(this, [this, object]);
+}
+
+Function.prototype.bind = function(object) {
+  var __method = this;
+  return function() {
+    __method.apply(object, arguments);
+  }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+  var __method = this;
+  return function(event) {
+    __method.call(object, event || window.event);
+  }
+}
+
+Number.prototype.toColorPart = function() {
+  var digits = this.toString(16);
+  if (this < 16) return '0' + digits;
+  return digits;
+}
+
+var Try = {
+  these: function() {
+    var returnValue;
+
+    for (var i = 0; i < arguments.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() {
+    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    if (!this.currentlyExecuting) {
+      try { 
+        this.currentlyExecuting = true;
+        this.callback(); 
+      } finally { 
+        this.currentlyExecuting = false;
+      }
+    }
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+function $() {
+  var elements = new Array();
+
+  for (var i = 0; i < arguments.length; i++) {
+    var element = arguments[i];
+    if (typeof element == 'string')
+      element = document.getElementById(element);
+
+    if (arguments.length == 1) 
+      return element;
+
+    elements.push(element);
+  }
+
+  return elements;
+}
+
+if (!Array.prototype.push) {
+  Array.prototype.push = function() {
+		var startLength = this.length;
+		for (var i = 0; i < arguments.length; i++)
+      this[startLength + i] = arguments[i];
+	  return this.length;
+  }
+}
+
+if (!Function.prototype.apply) {
+  // Based on code from http://www.youngpup.net/
+  Function.prototype.apply = function(object, parameters) {
+    var parameterStrings = new Array();
+    if (!object)     object = window;
+    if (!parameters) parameters = new Array();
+    
+    for (var i = 0; i < parameters.length; i++)
+      parameterStrings[i] = 'parameters[' + i + ']';
+    
+    object.__apply__ = this;
+    var result = eval('object.__apply__(' + 
+      parameterStrings.join(', ') + ')');
+    object.__apply__ = null;
+    
+    return result;
+  }
+}
+
+String.prototype.extend({
+  stripTags: function() {
+    return this.replace(/<\/?[^>]+>/gi, '');
+  },
+
+  escapeHTML: function() {
+    var div = document.createElement('div');
+    var text = document.createTextNode(this);
+    div.appendChild(text);
+    return div.innerHTML;
+  },
+
+  unescapeHTML: function() {
+    var div = document.createElement('div');
+    div.innerHTML = this.stripTags();
+    return div.childNodes[0].nodeValue;
+  }
+});
+
+var Ajax = {
+  getTransport: function() {
+    return Try.these(
+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')},
+      function() {return new XMLHttpRequest()}
+    ) || false;
+  }
+}
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+  setOptions: function(options) {
+    this.options = {
+      method:       'post',
+      asynchronous: true,
+      parameters:   ''
+    }.extend(options || {});
+  },
+
+  responseIsSuccess: function() {
+    return this.transport.status == undefined
+        || this.transport.status == 0 
+        || (this.transport.status >= 200 && this.transport.status < 300);
+  },
+
+  responseIsFailure: function() {
+    return !this.responseIsSuccess();
+  }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events = 
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = (new Ajax.Base()).extend({
+  initialize: function(url, options) {
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+    this.request(url);
+  },
+
+  request: function(url) {
+    var parameters = this.options.parameters || '';
+    if (parameters.length > 0) parameters += '&_=';
+
+    try {
+      if (this.options.method == 'get')
+        url += '?' + parameters;
+
+      this.transport.open(this.options.method, url,
+        this.options.asynchronous);
+
+      if (this.options.asynchronous) {
+        this.transport.onreadystatechange = this.onStateChange.bind(this);
+        setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10);
+      }
+
+      this.setRequestHeaders();
+
+      var body = this.options.postBody ? this.options.postBody : parameters;
+      this.transport.send(this.options.method == 'post' ? body : null);
+
+    } catch (e) {
+    }
+  },
+
+  setRequestHeaders: function() {
+    var requestHeaders = 
+      ['X-Requested-With', 'XMLHttpRequest',
+       'X-Prototype-Version', Prototype.Version];
+
+    if (this.options.method == 'post') {
+      requestHeaders.push('Content-type', 
+        'application/x-www-form-urlencoded');
+
+      /* Force "Connection: close" for Mozilla browsers to work around
+       * a bug where XMLHttpReqeuest sends an incorrect Content-length
+       * header. See Mozilla Bugzilla #246651. 
+       */
+      if (this.transport.overrideMimeType)
+        requestHeaders.push('Connection', 'close');
+    }
+
+    if (this.options.requestHeaders)
+      requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
+
+    for (var i = 0; i < requestHeaders.length; i += 2)
+      this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
+  },
+
+  onStateChange: function() {
+    var readyState = this.transport.readyState;
+    if (readyState != 1)
+      this.respondToReadyState(this.transport.readyState);
+  },
+
+  respondToReadyState: function(readyState) {
+    var event = Ajax.Request.Events[readyState];
+
+    if (event == 'Complete')
+      (this.options['on' + this.transport.status]
+       || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
+       || Prototype.emptyFunction)(this.transport);
+
+    (this.options['on' + event] || Prototype.emptyFunction)(this.transport);
+
+    /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
+    if (event == 'Complete')
+      this.transport.onreadystatechange = Prototype.emptyFunction;
+  }
+});
+
+Ajax.Updater = Class.create();
+Ajax.Updater.ScriptFragment = '(?:<script.*?>)((\n|.)*?)(?:<\/script>)';
+
+Ajax.Updater.prototype.extend(Ajax.Request.prototype).extend({
+  initialize: function(container, url, options) {
+    this.containers = {
+      success: container.success ? $(container.success) : $(container),
+      failure: 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() {
+      this.updateContent();
+      onComplete(this.transport);
+    }).bind(this);
+
+    this.request(url);
+  },
+
+  updateContent: function() {
+    var receiver = this.responseIsSuccess() ?
+      this.containers.success : this.containers.failure;
+
+    var match    = new RegExp(Ajax.Updater.ScriptFragment, 'img');
+    var response = this.transport.responseText.replace(match, '');
+    var scripts  = this.transport.responseText.match(match);
+
+    if (receiver) {
+      if (this.options.insertion) {
+        new this.options.insertion(receiver, response);
+      } else {
+        receiver.innerHTML = response;
+      }
+    }
+
+    if (this.responseIsSuccess()) {
+      if (this.onComplete)
+        setTimeout((function() {this.onComplete(
+          this.transport)}).bind(this), 10);
+    }
+
+    if (this.options.evalScripts && scripts) {
+      match = new RegExp(Ajax.Updater.ScriptFragment, 'im');
+      setTimeout((function() {
+        for (var i = 0; i < scripts.length; i++)
+          eval(scripts[i].match(match)[1]);
+      }).bind(this), 10);
+    }
+  }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = (new Ajax.Base()).extend({
+  initialize: function(container, url, options) {
+    this.setOptions(options);
+    this.onComplete = this.options.onComplete;
+
+    this.frequency = (this.options.frequency || 2);
+    this.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.onComplete = undefined;
+    clearTimeout(this.timer);
+    (this.onComplete || Ajax.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);
+  }
+});
+
+document.getElementsByClassName = function(className) {
+  var children = document.getElementsByTagName('*') || document.all;
+  var elements = new Array();
+  
+  for (var i = 0; i < children.length; i++) {
+    var child = children[i];
+    var classNames = child.className.split(' ');
+    for (var j = 0; j < classNames.length; j++) {
+      if (classNames[j] == className) {
+        elements.push(child);
+        break;
+      }
+    }
+  }
+  
+  return elements;
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element) {
+  var Element = new Object();
+}
+
+Object.extend(Element, {
+  toggle: function() {
+    for (var i = 0; i < arguments.length; i++) {
+      var element = $(arguments[i]);
+      element.style.display = 
+        (element.style.display == 'none' ? '' : 'none');
+    }
+  },
+
+  hide: function() {
+    for (var i = 0; i < arguments.length; i++) {
+      var element = $(arguments[i]);
+      element.style.display = 'none';
+    }
+  },
+
+  show: function() {
+    for (var i = 0; i < arguments.length; i++) {
+      var element = $(arguments[i]);
+      element.style.display = '';
+    }
+  },
+
+  remove: function(element) {
+    element = $(element);
+    element.parentNode.removeChild(element);
+  },
+   
+  getHeight: function(element) {
+    element = $(element);
+    return element.offsetHeight; 
+  },
+
+  hasClassName: function(element, className) {
+    element = $(element);
+    if (!element)
+      return;
+    var a = element.className.split(' ');
+    for (var i = 0; i < a.length; i++) {
+      if (a[i] == className)
+        return true;
+    }
+    return false;
+  },
+
+  addClassName: function(element, className) {
+    element = $(element);
+    Element.removeClassName(element, className);
+    element.className += ' ' + className;
+  },
+
+  removeClassName: function(element, className) {
+    element = $(element);
+    if (!element)
+      return;
+    var newClassName = '';
+    var a = element.className.split(' ');
+    for (var i = 0; i < a.length; i++) {
+      if (a[i] != className) {
+        if (i > 0)
+          newClassName += ' ';
+        newClassName += a[i];
+      }
+    }
+    element.className = newClassName;
+  },
+  
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    var element = $(element);
+    for (var i = 0; i < element.childNodes.length; i++) {
+      var node = element.childNodes[i];
+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) 
+        Element.remove(node);
+    }
+  }
+});
+
+var Toggle = new Object();
+Toggle.display = Element.toggle;
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.Insertion = function(adjacency) {
+  this.adjacency = adjacency;
+}
+
+Abstract.Insertion.prototype = {
+  initialize: function(element, content) {
+    this.element = $(element);
+    this.content = content;
+    
+    if (this.adjacency && this.element.insertAdjacentHTML) {
+      this.element.insertAdjacentHTML(this.adjacency, this.content);
+    } else {
+      this.range = this.element.ownerDocument.createRange();
+      if (this.initializeRange) this.initializeRange();
+      this.fragment = this.range.createContextualFragment(this.content);
+      this.insertContent();
+    }
+  }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = (new Abstract.Insertion('beforeBegin')).extend({
+  initializeRange: function() {
+    this.range.setStartBefore(this.element);
+  },
+  
+  insertContent: function() {
+    this.element.parentNode.insertBefore(this.fragment, this.element);
+  }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = (new Abstract.Insertion('afterBegin')).extend({
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(true);
+  },
+  
+  insertContent: function() {  
+    this.element.insertBefore(this.fragment, this.element.firstChild);
+  }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = (new Abstract.Insertion('beforeEnd')).extend({
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(this.element);
+  },
+  
+  insertContent: function() {
+    this.element.appendChild(this.fragment);
+  }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = (new Abstract.Insertion('afterEnd')).extend({
+  initializeRange: function() {
+    this.range.setStartAfter(this.element);
+  },
+  
+  insertContent: function() {
+    this.element.parentNode.insertBefore(this.fragment, 
+      this.element.nextSibling);
+  }
+});
+
+var Field = {
+  clear: function() {
+    for (var i = 0; i < arguments.length; i++)
+      $(arguments[i]).value = '';
+  },
+
+  focus: function(element) {
+    $(element).focus();
+  },
+  
+  present: function() {
+    for (var i = 0; i < arguments.length; i++)
+      if ($(arguments[i]).value == '') return false;
+    return true;
+  },
+  
+  select: function(element) {
+    $(element).select();
+  },
+   
+  activate: function(element) {
+    $(element).focus();
+    $(element).select();
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Form = {
+  serialize: function(form) {
+    var elements = Form.getElements($(form));
+    var queryComponents = new Array();
+    
+    for (var i = 0; i < elements.length; i++) {
+      var queryComponent = Form.Element.serialize(elements[i]);
+      if (queryComponent)
+        queryComponents.push(queryComponent);
+    }
+    
+    return queryComponents.join('&');
+  },
+  
+  getElements: function(form) {
+    var form = $(form);
+    var elements = new Array();
+
+    for (tagName in Form.Element.Serializers) {
+      var tagElements = form.getElementsByTagName(tagName);
+      for (var j = 0; j < tagElements.length; j++)
+        elements.push(tagElements[j]);
+    }
+    return elements;
+  },
+  
+  getInputs: function(form, typeName, name) {
+    var form = $(form);
+    var inputs = form.getElementsByTagName('input');
+    
+    if (!typeName && !name)
+      return inputs;
+      
+    var matchingInputs = new Array();
+    for (var i = 0; i < inputs.length; i++) {
+      var input = inputs[i];
+      if ((typeName && input.type != typeName) ||
+          (name && input.name != name)) 
+        continue;
+      matchingInputs.push(input);
+    }
+
+    return matchingInputs;
+  },
+
+  disable: function(form) {
+    var elements = Form.getElements(form);
+    for (var i = 0; i < elements.length; i++) {
+      var element = elements[i];
+      element.blur();
+      element.disabled = 'true';
+    }
+  },
+
+  enable: function(form) {
+    var elements = Form.getElements(form);
+    for (var i = 0; i < elements.length; i++) {
+      var element = elements[i];
+      element.disabled = '';
+    }
+  },
+
+  focusFirstElement: function(form) {
+    var form = $(form);
+    var elements = Form.getElements(form);
+    for (var i = 0; i < elements.length; i++) {
+      var element = elements[i];
+      if (element.type != 'hidden' && !element.disabled) {
+        Field.activate(element);
+        break;
+      }
+    }
+  },
+
+  reset: function(form) {
+    $(form).reset();
+  }
+}
+
+Form.Element = {
+  serialize: function(element) {
+    var element = $(element);
+    var method = element.tagName.toLowerCase();
+    var parameter = Form.Element.Serializers[method](element);
+    
+    if (parameter)
+      return encodeURIComponent(parameter[0]) + '=' + 
+        encodeURIComponent(parameter[1]);                   
+  },
+  
+  getValue: function(element) {
+    var element = $(element);
+    var method = element.tagName.toLowerCase();
+    var parameter = Form.Element.Serializers[method](element);
+    
+    if (parameter) 
+      return parameter[1];
+  }
+}
+
+Form.Element.Serializers = {
+  input: function(element) {
+    switch (element.type.toLowerCase()) {
+      case 'submit':
+      case 'hidden':
+      case 'password':
+      case 'text':
+        return Form.Element.Serializers.textarea(element);
+      case 'checkbox':  
+      case 'radio':
+        return Form.Element.Serializers.inputSelector(element);
+    }
+    return false;
+  },
+
+  inputSelector: function(element) {
+    if (element.checked)
+      return [element.name, element.value];
+  },
+
+  textarea: function(element) {
+    return [element.name, element.value];
+  },
+
+  select: function(element) {
+    var value = '';
+    if (element.type == 'select-one') {
+      var index = element.selectedIndex;
+      if (index >= 0)
+        value = element.options[index].value || element.options[index].text;
+    } else {
+      value = new Array();
+      for (var i = 0; i < element.length; i++) {
+        var opt = element.options[i];
+        if (opt.selected)
+          value.push(opt.value || opt.text);
+      }
+    }
+    return [element.name, value];
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var $F = Form.Element.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+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();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = (new Abstract.TimedObserver()).extend({
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = (new Abstract.TimedObserver()).extend({
+  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() {
+    var elements = Form.getElements(this.element);
+    for (var i = 0; i < elements.length; i++)
+      this.registerCallback(elements[i]);
+  },
+  
+  registerCallback: function(element) {
+    if (element.type) {
+      switch (element.type.toLowerCase()) {
+        case 'checkbox':  
+        case 'radio':
+          element.target = this;
+          element.prev_onclick = element.onclick || Prototype.emptyFunction;
+          element.onclick = function() {
+            this.prev_onclick(); 
+            this.target.onElementEvent();
+          }
+          break;
+        case 'password':
+        case 'text':
+        case 'textarea':
+        case 'select-one':
+        case 'select-multiple':
+          element.target = this;
+          element.prev_onchange = element.onchange || Prototype.emptyFunction;
+          element.onchange = function() {
+            this.prev_onchange(); 
+            this.target.onElementEvent();
+          }
+          break;
+      }
+    }    
+  }
+}
+
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = (new Abstract.EventObserver()).extend({
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = (new Abstract.EventObserver()).extend({
+  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,
+
+  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;
+    }
+  },
+
+  // 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; i < Event.observers.length; i++) {
+      Event.stopObserving.apply(this, Event.observers[i]);
+      Event.observers[i][0] = null;
+    }
+    Event.observers = false;
+  },
+
+  observe: function(element, name, observer, useCapture) {
+    var element = $(element);
+    useCapture = useCapture || false;
+    
+    if (name == 'keypress' &&
+        ((navigator.appVersion.indexOf('AppleWebKit') > 0) 
+        || element.attachEvent))
+      name = 'keydown';
+    
+    this._observeAndCache(element, name, observer, useCapture);
+  },
+
+  stopObserving: function(element, name, observer, useCapture) {
+    var element = $(element);
+    useCapture = useCapture || false;
+    
+    if (name == 'keypress' &&
+        ((navigator.appVersion.indexOf('AppleWebKit') > 0) 
+        || element.detachEvent))
+      name = 'keydown';
+    
+    if (element.removeEventListener) {
+      element.removeEventListener(name, observer, useCapture);
+    } else if (element.detachEvent) {
+      element.detachEvent('on' + name, observer);
+    }
+  }
+});
+
+/* prevent memory leaks in 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];
+  },
+
+  // 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;
+  },
+
+  clone: function(source, target) {
+    source = $(source);
+    target = $(target);
+    target.style.position = 'absolute';
+    var offsets = this.cumulativeOffset(source);
+    target.style.top    = offsets[1] + 'px';
+    target.style.left   = offsets[0] + 'px';
+    target.style.width  = source.offsetWidth + 'px';
+    target.style.height = source.offsetHeight + 'px';
+  }
+}
diff --git a/web/static/js/rico.js b/web/static/js/rico.js
new file mode 100644
index 0000000..a1f9113
--- /dev/null
+++ b/web/static/js/rico.js
@@ -0,0 +1,2666 @@
+/**
+  *
+  *  Copyright 2005 Sabre Airline Solutions
+  *
+  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
+  *  file except in compliance with the License. You may obtain a copy of the License at
+  *
+  *         http://www.apache.org/licenses/LICENSE-2.0
+  *
+  *  Unless required by applicable law or agreed to in writing, software distributed under the
+  *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+  *  either express or implied. See the License for the specific language governing permissions
+  *  and limitations under the License.
+  **/
+
+
+//-------------------- rico.js
+var Rico = {
+  Version: '1.1-beta2'
+}
+
+Rico.ArrayExtensions = new Array();
+
+if (Object.prototype.extend) {
+   // in prototype.js...
+   Rico.ArrayExtensions[ Rico.ArrayExtensions.length ] = Object.prototype.extend;
+}
+
+if (Array.prototype.push) {
+   // in prototype.js...
+   Rico.ArrayExtensions[ Rico.ArrayExtensions.length ] = Array.prototype.push;
+}
+
+if (!Array.prototype.remove) {
+   Array.prototype.remove = function(dx) {
+      if( isNaN(dx) || dx > this.length )
+         return false;
+      for( var i=0,n=0; i<this.length; i++ )
+         if( i != dx )
+            this[n++]=this[i];
+      this.length-=1;
+   };
+  Rico.ArrayExtensions[ Rico.ArrayExtensions.length ] = Array.prototype.remove;
+}
+
+if (!Array.prototype.removeItem) {
+   Array.prototype.removeItem = function(item) {
+      for ( var i = 0 ; i < this.length ; i++ )
+         if ( this[i] == item ) {
+            this.remove(i);
+            break;
+         }
+   };
+  Rico.ArrayExtensions[ Rico.ArrayExtensions.length ] = Array.prototype.removeItem;
+}
+
+if (!Array.prototype.indices) {
+   Array.prototype.indices = function() {
+      var indexArray = new Array();
+      for ( index in this ) {
+         var ignoreThis = false;
+         for ( var i = 0 ; i < Rico.ArrayExtensions.length ; i++ ) {
+            if ( this[index] == Rico.ArrayExtensions[i] ) {
+               ignoreThis = true;
+               break;
+            }
+         }
+         if ( !ignoreThis )
+            indexArray[ indexArray.length ] = index;
+      }
+      return indexArray;
+   }
+  Rico.ArrayExtensions[ Rico.ArrayExtensions.length ] = Array.prototype.indices;
+}
+
+// Create the loadXML method and xml getter for Mozilla
+if ( window.DOMParser &&
+	  window.XMLSerializer &&
+	  window.Node && Node.prototype && Node.prototype.__defineGetter__ ) {
+
+   if (!Document.prototype.loadXML) {
+      Document.prototype.loadXML = function (s) {
+         var doc2 = (new DOMParser()).parseFromString(s, "text/xml");
+         while (this.hasChildNodes())
+            this.removeChild(this.lastChild);
+
+         for (var i = 0; i < doc2.childNodes.length; i++) {
+            this.appendChild(this.importNode(doc2.childNodes[i], true));
+         }
+      };
+	}
+
+	Document.prototype.__defineGetter__( "xml",
+	   function () {
+		   return (new XMLSerializer()).serializeToString(this);
+	   }
+	 );
+}
+
+document.getElementsByTagAndClassName = function(tagName, className) {
+  if ( tagName == null )
+     tagName = '*';
+
+  var children = document.getElementsByTagName(tagName) || document.all;
+  var elements = new Array();
+
+  if ( className == null )
+    return children;
+
+  for (var i = 0; i < children.length; i++) {
+    var child = children[i];
+    var classNames = child.className.split(' ');
+    for (var j = 0; j < classNames.length; j++) {
+      if (classNames[j] == className) {
+        elements.push(child);
+        break;
+      }
+    }
+  }
+
+  return elements;
+}
+
+
+//-------------------- ricoAccordion.js
+
+Rico.Accordion = Class.create();
+
+Rico.Accordion.prototype = {
+
+   initialize: function(container, options) {
+      this.container            = $(container);
+      this.lastExpandedTab      = null;
+      this.accordionTabs        = new Array();
+      this.setOptions(options);
+      this._attachBehaviors();
+
+      this.container.style.borderBottom = '1px solid ' + this.options.borderColor;
+
+      // set the initial visual state...
+      for ( var i=1 ; i < this.accordionTabs.length ; i++ )
+      {
+         this.accordionTabs[i].collapse();
+         this.accordionTabs[i].content.style.display = 'none';
+      }
+      this.lastExpandedTab = this.accordionTabs[0];
+      this.lastExpandedTab.content.style.height = this.options.panelHeight + "px";
+      this.lastExpandedTab.showExpanded();
+      this.lastExpandedTab.titleBar.style.fontWeight = this.options.expandedFontWeight;
+   },
+
+   setOptions: function(options) {
+      this.options = {
+         expandedBg          : '#63699c',
+         hoverBg             : '#63699c',
+         collapsedBg         : '#6b79a5',
+         expandedTextColor   : '#ffffff',
+         expandedFontWeight  : 'bold',
+         hoverTextColor      : '#ffffff',
+         collapsedTextColor  : '#ced7ef',
+         collapsedFontWeight : 'normal',
+         hoverTextColor      : '#ffffff',
+         borderColor         : '#1f669b',
+         panelHeight         : 200,
+         onHideTab           : null,
+         onShowTab           : null
+      }.extend(options || {});
+   },
+
+   showTabByIndex: function( anIndex, animate ) {
+      var doAnimate = arguments.length == 1 ? true : animate;
+      this.showTab( this.accordionTabs[anIndex], doAnimate );
+   },
+
+   showTab: function( accordionTab, animate ) {
+
+      var doAnimate = arguments.length == 1 ? true : animate;
+
+      if ( this.options.onHideTab )
+         this.options.onHideTab(this.lastExpandedTab);
+
+      this.lastExpandedTab.showCollapsed(); 
+      var accordion = this;
+      var lastExpandedTab = this.lastExpandedTab;
+
+      this.lastExpandedTab.content.style.height = (this.options.panelHeight - 1) + 'px';
+      accordionTab.content.style.display = '';
+
+      accordionTab.titleBar.style.fontWeight = this.options.expandedFontWeight;
+
+      if ( doAnimate ) {
+         new Effect.AccordionSize( this.lastExpandedTab.content,
+                                   accordionTab.content,
+                                   1,
+                                   this.options.panelHeight,
+                                   100, 10,
+                                   { complete: function() {accordion.showTabDone(lastExpandedTab)} } );
+         this.lastExpandedTab = accordionTab;
+      }
+      else {
+         this.lastExpandedTab.content.style.height = "1px";
+         accordionTab.content.style.height = this.options.panelHeight + "px";
+         this.lastExpandedTab = accordionTab;
+         this.showTabDone(lastExpandedTab);
+      }
+   },
+
+   showTabDone: function(collapsedTab) {
+      collapsedTab.content.style.display = 'none';
+      this.lastExpandedTab.showExpanded();
+      if ( this.options.onShowTab )
+         this.options.onShowTab(this.lastExpandedTab);
+   },
+
+   _attachBehaviors: function() {
+      var panels = this._getDirectChildrenByTag(this.container, 'DIV');
+      for ( var i = 0 ; i < panels.length ; i++ ) {
+
+         var tabChildren = this._getDirectChildrenByTag(panels[i],'DIV');
+         if ( tabChildren.length != 2 )
+            continue; // unexpected
+
+         var tabTitleBar   = tabChildren[0];
+         var tabContentBox = tabChildren[1];
+         this.accordionTabs.push( new Rico.Accordion.Tab(this,tabTitleBar,tabContentBox) );
+      }
+   },
+
+   _getDirectChildrenByTag: function(e, tagName) {
+      var kids = new Array();
+      var allKids = e.childNodes;
+      for( var i = 0 ; i < allKids.length ; i++ )
+         if ( allKids[i] && allKids[i].tagName && allKids[i].tagName == tagName )
+            kids.push(allKids[i]);
+      return kids;
+   }
+
+};
+
+Rico.Accordion.Tab = Class.create();
+
+Rico.Accordion.Tab.prototype = {
+
+   initialize: function(accordion, titleBar, content) {
+      this.accordion = accordion;
+      this.titleBar  = titleBar;
+      this.content   = content;
+      this._attachBehaviors();
+   },
+
+   collapse: function() {
+      this.showCollapsed();
+      this.content.style.height = "1px";
+   },
+
+   showCollapsed: function() {
+      this.expanded = false;
+      this.titleBar.style.backgroundColor = this.accordion.options.collapsedBg;
+      this.titleBar.style.color           = this.accordion.options.collapsedTextColor;
+      this.titleBar.style.fontWeight      = this.accordion.options.collapsedFontWeight;
+      this.content.style.overflow = "hidden";
+   },
+
+   showExpanded: function() {
+      this.expanded = true;
+      this.titleBar.style.backgroundColor = this.accordion.options.expandedBg;
+      this.titleBar.style.color           = this.accordion.options.expandedTextColor;
+      this.content.style.overflow         = "visible";
+   },
+
+   titleBarClicked: function(e) {
+      if ( this.accordion.lastExpandedTab == this )
+         return;
+      this.accordion.showTab(this);
+   },
+
+   hover: function(e) {
+		this.titleBar.style.backgroundColor = this.accordion.options.hoverBg;
+		this.titleBar.style.color           = this.accordion.options.hoverTextColor;
+   },
+
+   unhover: function(e) {
+      if ( this.expanded ) {
+         this.titleBar.style.backgroundColor = this.accordion.options.expandedBg;
+         this.titleBar.style.color           = this.accordion.options.expandedTextColor;
+      }
+      else {
+         this.titleBar.style.backgroundColor = this.accordion.options.collapsedBg;
+         this.titleBar.style.color           = this.accordion.options.collapsedTextColor;
+      }
+   },
+
+   _attachBehaviors: function() {
+      this.content.style.border = "1px solid " + this.accordion.options.borderColor;
+      this.content.style.borderTopWidth    = "0px";
+      this.content.style.borderBottomWidth = "0px";
+      this.content.style.margin            = "0px";
+
+      this.titleBar.onclick     = this.titleBarClicked.bindAsEventListener(this);
+      this.titleBar.onmouseover = this.hover.bindAsEventListener(this);
+      this.titleBar.onmouseout  = this.unhover.bindAsEventListener(this);
+   }
+
+};
+
+
+//-------------------- ricoAjaxEngine.js
+
+Rico.AjaxEngine = Class.create();
+
+Rico.AjaxEngine.prototype = {
+
+   initialize: function() {
+      this.ajaxElements = new Array();
+      this.ajaxObjects  = new Array();
+      this.requestURLS  = new Array();
+   },
+
+   registerAjaxElement: function( anId, anElement ) {
+      if ( arguments.length == 1 )
+         anElement = $(anId);
+      this.ajaxElements[anId] = anElement;
+   },
+
+   registerAjaxObject: function( anId, anObject ) {
+      this.ajaxObjects[anId] = anObject;
+   },
+
+   registerRequest: function (requestLogicalName, requestURL) {
+      this.requestURLS[requestLogicalName] = requestURL;
+   },
+
+   sendRequest: function(requestName) {
+      var requestURL = this.requestURLS[requestName];
+      if ( requestURL == null )
+         return;
+
+      var queryString = "";
+      if ( arguments.length > 1 )
+         queryString = this._createQueryString(arguments, 1);
+
+      new Ajax.Request(requestURL, this._requestOptions(queryString));
+   },
+
+   sendRequestWithData: function(requestName, xmlDocument) {
+      var requestURL = this.requestURLS[requestName];
+      if ( requestURL == null )
+         return;
+
+      var queryString = "";
+      if ( arguments.length > 2 )
+         queryString = this._createQueryString(arguments, 2);
+
+      new Ajax.Request(requestURL + "?" + queryString, this._requestOptions(null,xmlDocument));
+   },
+
+   sendRequestAndUpdate: function(requestName,container,options) {
+      var requestURL = this.requestURLS[requestName];
+      if ( requestURL == null )
+         return;
+
+      var queryString = "";
+      if ( arguments.length > 3 )
+         queryString = this._createQueryString(arguments, 3);
+
+      var updaterOptions = this._requestOptions(queryString);
+      updaterOptions.onComplete = null;
+      updaterOptions.extend(options);
+
+      new Ajax.Updater(container, requestURL, updaterOptions);
+   },
+
+   sendRequestWithDataAndUpdate: function(requestName,xmlDocument,container,options) {
+      var requestURL = this.requestURLS[requestName];
+      if ( requestURL == null )
+         return;
+
+      var queryString = "";
+      if ( arguments.length > 4 )
+         queryString = this._createQueryString(arguments, 4);
+
+
+      var updaterOptions = this._requestOptions(queryString,xmlDocument);
+      updaterOptions.onComplete = null;
+      updaterOptions.extend(options);
+
+      new Ajax.Updater(container, requestURL + "?" + queryString, updaterOptions);
+   },
+
+   // Private -- not part of intended engine API --------------------------------------------------------------------
+
+   _requestOptions: function(queryString,xmlDoc) {
+      var self = this;
+
+      var requestHeaders = ['X-Rico-Version', Rico.Version ];
+      var sendMethod = "post"
+      if ( arguments[1] )
+         requestHeaders.push( 'Content-type', 'text/xml' );
+      else
+         sendMethod = "get";
+
+      return { requestHeaders: requestHeaders,
+               parameters:     queryString,
+               postBody:       arguments[1] ? xmlDoc : null,
+               method:         sendMethod,
+               onComplete:     self._onRequestComplete.bind(self) };
+   },
+
+   _createQueryString: function( theArgs, offset ) {
+      var queryString = ""
+      for ( var i = offset ; i < theArgs.length ; i++ ) {
+          if ( i != offset )
+            queryString += "&";
+
+          var anArg = theArgs[i];
+
+          if ( anArg.name != undefined && anArg.value != undefined ) {
+            queryString += anArg.name +  "=" + escape(anArg.value);
+          }
+          else {
+             var ePos  = anArg.indexOf('=');
+             var argName  = anArg.substring( 0, ePos );
+             var argValue = anArg.substring( ePos + 1 );
+             queryString += argName + "=" + escape(argValue);
+          }
+      }
+
+      return queryString;
+   },
+
+   _onRequestComplete : function(request) {
+
+      //!!TODO: error handling infrastructure?? 
+      if (request.status != 200)
+        return;
+
+      var response = request.responseXML.getElementsByTagName("ajax-response");
+      if (response == null || response.length != 1)
+         return;
+      this._processAjaxResponse( response[0].childNodes );
+   },
+
+   _processAjaxResponse: function( xmlResponseElements ) {
+      for ( var i = 0 ; i < xmlResponseElements.length ; i++ ) {
+         var responseElement = xmlResponseElements[i];
+
+         // only process nodes of type element.....
+         if ( responseElement.nodeType != 1 )
+            continue;
+
+         var responseType = responseElement.getAttribute("type");
+         var responseId   = responseElement.getAttribute("id");
+
+         if ( responseType == "object" )
+            this._processAjaxObjectUpdate( this.ajaxObjects[ responseId ], responseElement );
+         else if ( responseType == "element" )
+            this._processAjaxElementUpdate( this.ajaxElements[ responseId ], responseElement );
+         else
+            alert('unrecognized AjaxResponse type : ' + responseType );
+      }
+   },
+
+   _processAjaxObjectUpdate: function( ajaxObject, responseElement ) {
+      ajaxObject.ajaxUpdate( responseElement );
+   },
+
+   _processAjaxElementUpdate: function( ajaxElement, responseElement ) {
+      ajaxElement.innerHTML = RicoUtil.getContentAsString(responseElement);
+   }
+
+}
+
+var ajaxEngine = new Rico.AjaxEngine();
+
+
+//-------------------- ricoColor.js
+Rico.Color = Class.create();
+
+Rico.Color.prototype = {
+
+   initialize: function(red, green, blue) {
+      this.rgb = { r: red, g : green, b : blue };
+   },
+
+   setRed: function(r) {
+      this.rgb.r = r;
+   },
+
+   setGreen: function(g) {
+      this.rgb.g = g;
+   },
+
+   setBlue: function(b) {
+      this.rgb.b = b;
+   },
+
+   setHue: function(h) {
+
+      // get an HSB model, and set the new hue...
+      var hsb = this.asHSB();
+      hsb.h = h;
+
+      // convert back to RGB...
+      this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, hsb.b);
+   },
+
+   setSaturation: function(s) {
+      // get an HSB model, and set the new hue...
+      var hsb = this.asHSB();
+      hsb.s = s;
+
+      // convert back to RGB and set values...
+      this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, hsb.b);
+   },
+
+   setBrightness: function(b) {
+      // get an HSB model, and set the new hue...
+      var hsb = this.asHSB();
+      hsb.b = b;
+
+      // convert back to RGB and set values...
+      this.rgb = Rico.Color.HSBtoRGB( hsb.h, hsb.s, hsb.b );
+   },
+
+   darken: function(percent) {
+      var hsb  = this.asHSB();
+      this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, Math.max(hsb.b - percent,0));
+   },
+
+   brighten: function(percent) {
+      var hsb  = this.asHSB();
+      this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, Math.min(hsb.b + percent,1));
+   },
+
+   blend: function(other) {
+      this.rgb.r = Math.floor((this.rgb.r + other.rgb.r)/2);
+      this.rgb.g = Math.floor((this.rgb.g + other.rgb.g)/2);
+      this.rgb.b = Math.floor((this.rgb.b + other.rgb.b)/2);
+   },
+
+   isBright: function() {
+      var hsb = this.asHSB();
+      return this.asHSB().b > 0.5;
+   },
+
+   isDark: function() {
+      return ! this.isBright();
+   },
+
+   asRGB: function() {
+      return "rgb(" + this.rgb.r + "," + this.rgb.g + "," + this.rgb.b + ")";
+   },
+
+   asHex: function() {
+      return "#" + this.rgb.r.toColorPart() + this.rgb.g.toColorPart() + this.rgb.b.toColorPart();
+   },
+
+   asHSB: function() {
+      return Rico.Color.RGBtoHSB(this.rgb.r, this.rgb.g, this.rgb.b);
+   },
+
+   toString: function() {
+      return this.asHex();
+   }
+
+};
+
+Rico.Color.createFromHex = function(hexCode) {
+
+   if ( hexCode.indexOf('#') == 0 )
+      hexCode = hexCode.substring(1);
+   var red   = hexCode.substring(0,2);
+   var green = hexCode.substring(2,4);
+   var blue  = hexCode.substring(4,6);
+   return new Rico.Color( parseInt(red,16), parseInt(green,16), parseInt(blue,16) );
+}
+
+/**
+ * Factory method for creating a color from the background of
+ * an HTML element.
+ */
+Rico.Color.createColorFromBackground = function(elem) {
+
+   var actualColor = RicoUtil.getElementsComputedStyle($(elem), "backgroundColor", "background-color");
+
+   if ( actualColor == "transparent" && elem.parent )
+      return Rico.Color.createColorFromBackground(elem.parent);
+
+   if ( actualColor == null )
+      return new Rico.Color(255,255,255);
+
+   if ( actualColor.indexOf("rgb(") == 0 ) {
+      var colors = actualColor.substring(4, actualColor.length - 1 );
+      var colorArray = colors.split(",");
+      return new Rico.Color( parseInt( colorArray[0] ),
+                            parseInt( colorArray[1] ),
+                            parseInt( colorArray[2] )  );
+
+   }
+   else if ( actualColor.indexOf("#") == 0 ) {
+      var redPart   = parseInt(actualColor.substring(1,3), 16);
+      var greenPart = parseInt(actualColor.substring(3,5), 16);
+      var bluePart  = parseInt(actualColor.substring(5), 16);
+      return new Rico.Color( redPart, greenPart, bluePart );
+   }
+   else
+      return new Rico.Color(255,255,255);
+}
+
+Rico.Color.HSBtoRGB = function(hue, saturation, brightness) {
+
+   var red   = 0;
+	var green = 0;
+	var blue  = 0;
+
+   if (saturation == 0) {
+      red = parseInt(brightness * 255.0 + 0.5);
+	   green = red;
+	   blue = red;
+	}
+	else {
+      var h = (hue - Math.floor(hue)) * 6.0;
+      var f = h - Math.floor(h);
+      var p = brightness * (1.0 - saturation);
+      var q = brightness * (1.0 - saturation * f);
+      var t = brightness * (1.0 - (saturation * (1.0 - f)));
+
+      switch (parseInt(h)) {
+         case 0:
+            red   = (brightness * 255.0 + 0.5);
+            green = (t * 255.0 + 0.5);
+            blue  = (p * 255.0 + 0.5);
+            break;
+         case 1:
+            red   = (q * 255.0 + 0.5);
+            green = (brightness * 255.0 + 0.5);
+            blue  = (p * 255.0 + 0.5);
+            break;
+         case 2:
+            red   = (p * 255.0 + 0.5);
+            green = (brightness * 255.0 + 0.5);
+            blue  = (t * 255.0 + 0.5);
+            break;
+         case 3:
+            red   = (p * 255.0 + 0.5);
+            green = (q * 255.0 + 0.5);
+            blue  = (brightness * 255.0 + 0.5);
+            break;
+         case 4:
+            red   = (t * 255.0 + 0.5);
+            green = (p * 255.0 + 0.5);
+            blue  = (brightness * 255.0 + 0.5);
+            break;
+          case 5:
+            red   = (brightness * 255.0 + 0.5);
+            green = (p * 255.0 + 0.5);
+            blue  = (q * 255.0 + 0.5);
+            break;
+	    }
+	}
+
+   return { r : parseInt(red), g : parseInt(green) , b : parseInt(blue) };
+}
+
+Rico.Color.RGBtoHSB = function(r, g, b) {
+
+   var hue;
+   var saturaton;
+   var brightness;
+
+   var cmax = (r > g) ? r : g;
+   if (b > cmax)
+      cmax = b;
+
+   var cmin = (r < g) ? r : g;
+   if (b < cmin)
+      cmin = b;
+
+   brightness = cmax / 255.0;
+   if (cmax != 0)
+      saturation = (cmax - cmin)/cmax;
+   else
+      saturation = 0;
+
+   if (saturation == 0)
+      hue = 0;
+   else {
+      var redc   = (cmax - r)/(cmax - cmin);
+    	var greenc = (cmax - g)/(cmax - cmin);
+    	var bluec  = (cmax - b)/(cmax - cmin);
+
+    	if (r == cmax)
+    	   hue = bluec - greenc;
+    	else if (g == cmax)
+    	   hue = 2.0 + redc - bluec;
+      else
+    	   hue = 4.0 + greenc - redc;
+
+    	hue = hue / 6.0;
+    	if (hue < 0)
+    	   hue = hue + 1.0;
+   }
+
+   return { h : hue, s : saturation, b : brightness };
+}
+
+
+//-------------------- ricoCorner.js
+
+Rico.Corner = {
+
+   round: function(e, options) {
+      var e = $(e);
+      this._setOptions(options);
+
+      var color = this.options.color;
+      if ( this.options.color == "fromElement" )
+         color = this._background(e);
+
+      var bgColor = this.options.bgColor;
+      if ( this.options.bgColor == "fromParent" )
+         bgColor = this._background(e.offsetParent);
+
+      this._roundCornersImpl(e, color, bgColor);
+   },
+
+   _roundCornersImpl: function(e, color, bgColor) {
+      if(this.options.border)
+         this._renderBorder(e,bgColor);
+      if(this._isTopRounded())
+         this._roundTopCorners(e,color,bgColor);
+      if(this._isBottomRounded())
+         this._roundBottomCorners(e,color,bgColor);
+   },
+
+   _renderBorder: function(el,bgColor) {
+      var borderValue = "1px solid " + this._borderColor(bgColor);
+      var borderL = "border-left: "  + borderValue;
+      var borderR = "border-right: " + borderValue;
+      var style   = "style='" + borderL + ";" + borderR +  "'";
+      el.innerHTML = "<div " + style + ">" + el.innerHTML + "</div>"
+   },
+
+   _roundTopCorners: function(el, color, bgColor) {
+      var corner = this._createCorner(bgColor);
+      for(var i=0 ; i < this.options.numSlices ; i++ )
+         corner.appendChild(this._createCornerSlice(color,bgColor,i,"top"));
+      el.style.paddingTop = 0;
+      el.insertBefore(corner,el.firstChild);
+   },
+
+   _roundBottomCorners: function(el, color, bgColor) {
+      var corner = this._createCorner(bgColor);
+      for(var i=(this.options.numSlices-1) ; i >= 0 ; i-- )
+         corner.appendChild(this._createCornerSlice(color,bgColor,i,"bottom"));
+      el.style.paddingBottom = 0;
+      el.appendChild(corner);
+   },
+
+   _createCorner: function(bgColor) {
+      var corner = document.createElement("div");
+      corner.style.backgroundColor = (this._isTransparent() ? "transparent" : bgColor);
+      return corner;
+   },
+
+   _createCornerSlice: function(color,bgColor, n, position) {
+      var slice = document.createElement("span");
+
+      var inStyle = slice.style;
+      inStyle.backgroundColor = color;
+      inStyle.display  = "block";
+      inStyle.height   = "1px";
+      inStyle.overflow = "hidden";
+      inStyle.fontSize = "1px";
+
+      var borderColor = this._borderColor(color,bgColor);
+      if ( this.options.border && n == 0 ) {
+         inStyle.borderTopStyle    = "solid";
+         inStyle.borderTopWidth    = "1px";
+         inStyle.borderLeftWidth   = "0px";
+         inStyle.borderRightWidth  = "0px";
+         inStyle.borderBottomWidth = "0px";
+         inStyle.height            = "0px"; // assumes css compliant box model
+         inStyle.borderColor       = borderColor;
+      }
+      else if(borderColor) {
+         inStyle.borderColor = borderColor;
+         inStyle.borderStyle = "solid";
+         inStyle.borderWidth = "0px 1px";
+      }
+
+      if ( !this.options.compact && (n == (this.options.numSlices-1)) )
+         inStyle.height = "2px";
+
+      this._setMargin(slice, n, position);
+      this._setBorder(slice, n, position);
+
+      return slice;
+   },
+
+   _setOptions: function(options) {
+      this.options = {
+         corners : "all",
+         color   : "fromElement",
+         bgColor : "fromParent",
+         blend   : true,
+         border  : false,
+         compact : false
+      }.extend(options || {});
+
+      this.options.numSlices = this.options.compact ? 2 : 4;
+      if ( this._isTransparent() )
+         this.options.blend = false;
+   },
+
+   _whichSideTop: function() {
+      if ( this._hasString(this.options.corners, "all", "top") )
+         return "";
+
+      if ( this.options.corners.indexOf("tl") >= 0 && this.options.corners.indexOf("tr") >= 0 )
+         return "";
+
+      if (this.options.corners.indexOf("tl") >= 0)
+         return "left";
+      else if (this.options.corners.indexOf("tr") >= 0)
+          return "right";
+      return "";
+   },
+
+   _whichSideBottom: function() {
+      if ( this._hasString(this.options.corners, "all", "bottom") )
+         return "";
+
+      if ( this.options.corners.indexOf("bl")>=0 && this.options.corners.indexOf("br")>=0 )
+         return "";
+
+      if(this.options.corners.indexOf("bl") >=0)
+         return "left";
+      else if(this.options.corners.indexOf("br")>=0)
+         return "right";
+      return "";
+   },
+
+   _borderColor : function(color,bgColor) {
+      if ( color == "transparent" )
+         return bgColor;
+      else if ( this.options.border )
+         return this.options.border;
+      else if ( this.options.blend )
+         return this._blend( bgColor, color );
+      else
+         return "";
+   },
+
+
+   _setMargin: function(el, n, corners) {
+      var marginSize = this._marginSize(n);
+      var whichSide = corners == "top" ? this._whichSideTop() : this._whichSideBottom();
+
+      if ( whichSide == "left" ) {
+         el.style.marginLeft = marginSize + "px"; el.style.marginRight = "0px";
+      }
+      else if ( whichSide == "right" ) {
+         el.style.marginRight = marginSize + "px"; el.style.marginLeft  = "0px";
+      }
+      else {
+         el.style.marginLeft = marginSize + "px"; el.style.marginRight = marginSize + "px";
+      }
+   },
+
+   _setBorder: function(el,n,corners) {
+      var borderSize = this._borderSize(n);
+      var whichSide = corners == "top" ? this._whichSideTop() : this._whichSideBottom();
+
+      if ( whichSide == "left" ) {
+         el.style.borderLeftWidth = borderSize + "px"; el.style.borderRightWidth = "0px";
+      }
+      else if ( whichSide == "right" ) {
+         el.style.borderRightWidth = borderSize + "px"; el.style.borderLeftWidth  = "0px";
+      }
+      else {
+         el.style.borderLeftWidth = borderSize + "px"; el.style.borderRightWidth = borderSize + "px";
+      }
+   },
+
+   _marginSize: function(n) {
+      if ( this._isTransparent() )
+         return 0;
+
+      var marginSizes          = [ 5, 3, 2, 1 ];
+      var blendedMarginSizes   = [ 3, 2, 1, 0 ];
+      var compactMarginSizes   = [ 2, 1 ];
+      var smBlendedMarginSizes = [ 1, 0 ];
+
+      if ( this.options.compact && this.options.blend )
+         return smBlendedMarginSizes[n];
+      else if ( this.options.compact )
+         return compactMarginSizes[n];
+      else if ( this.options.blend )
+         return blendedMarginSizes[n];
+      else
+         return marginSizes[n];
+   },
+
+   _borderSize: function(n) {
+      var transparentBorderSizes = [ 5, 3, 2, 1 ];
+      var blendedBorderSizes     = [ 2, 1, 1, 1 ];
+      var compactBorderSizes     = [ 1, 0 ];
+      var actualBorderSizes      = [ 0, 2, 0, 0 ];
+
+      if ( this.options.compact && (this.options.blend || this._isTransparent()) )
+         return 1;
+      else if ( this.options.compact )
+         return compactBorderSizes[n];
+      else if ( this.options.blend )
+         return blendedBorderSizes[n];
+      else if ( this.options.border )
+         return actualBorderSizes[n];
+      else if ( this._isTransparent() )
+         return transparentBorderSizes[n];
+      return 0;
+   },
+
+   _hasString: function(str) { for(var i=1 ; i<arguments.length ; i++) if (str.indexOf(arguments[i]) >= 0) return true; return false; },
+   _blend: function(c1, c2) { var cc1 = Rico.Color.createFromHex(c1); cc1.blend(Rico.Color.createFromHex(c2)); return cc1; },
+   _background: function(el) { try { return Rico.Color.createColorFromBackground(el).asHex(); } catch(err) { return "#ffffff"; } },
+   _isTransparent: function() { return this.options.color == "transparent"; },
+   _isTopRounded: function() { return this._hasString(this.options.corners, "all", "top", "tl", "tr"); },
+   _isBottomRounded: function() { return this._hasString(this.options.corners, "all", "bottom", "bl", "br"); },
+   _hasSingleTextChild: function(el) { return el.childNodes.length == 1 && el.childNodes[0].nodeType == 3; }
+}
+
+
+//-------------------- ricoDragAndDrop.js
+Rico.DragAndDrop = Class.create();
+
+Rico.DragAndDrop.prototype = {
+
+   initialize: function() {
+      this.dropZones                = new Array();
+      this.draggables               = new Array();
+      this.currentDragObjects       = new Array();
+      this.dragElement              = null;
+      this.lastSelectedDraggable    = null;
+      this.currentDragObjectVisible = false;
+      this.interestedInMotionEvents = false;
+   },
+
+   registerDropZone: function(aDropZone) {
+      this.dropZones[ this.dropZones.length ] = aDropZone;
+   },
+
+   deregisterDropZone: function(aDropZone) {
+      var newDropZones = new Array();
+      var j = 0;
+      for ( var i = 0 ; i < this.dropZones.length ; i++ ) {
+         if ( this.dropZones[i] != aDropZone )
+            newDropZones[j++] = this.dropZones[i];
+      }
+
+      this.dropZones = newDropZones;
+   },
+
+   clearDropZones: function() {
+      this.dropZones = new Array();
+   },
+
+   registerDraggable: function( aDraggable ) {
+      this.draggables[ this.draggables.length ] = aDraggable;
+      this._addMouseDownHandler( aDraggable );
+   },
+
+   clearSelection: function() {
+      for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
+         this.currentDragObjects[i].deselect();
+      this.currentDragObjects = new Array();
+      this.lastSelectedDraggable = null;
+   },
+
+   hasSelection: function() {
+      return this.currentDragObjects.length > 0;
+   },
+
+   setStartDragFromElement: function( e, mouseDownElement ) {
+      this.origPos = RicoUtil.toDocumentPosition(mouseDownElement);
+      this.startx = e.screenX - this.origPos.x
+      this.starty = e.screenY - this.origPos.y
+      //this.startComponentX = e.layerX ? e.layerX : e.offsetX;
+      //this.startComponentY = e.layerY ? e.layerY : e.offsetY;
+      //this.adjustedForDraggableSize = false;
+
+      this.interestedInMotionEvents = this.hasSelection();
+      this._terminateEvent(e);
+   },
+
+   updateSelection: function( draggable, extendSelection ) {
+      if ( ! extendSelection )
+         this.clearSelection();
+
+      if ( draggable.isSelected() ) {
+         this.currentDragObjects.removeItem(draggable);
+         draggable.deselect();
+         if ( draggable == this.lastSelectedDraggable )
+            this.lastSelectedDraggable = null;
+      }
+      else {
+         this.currentDragObjects[ this.currentDragObjects.length ] = draggable;
+         draggable.select();
+         this.lastSelectedDraggable = draggable;
+      }
+   },
+
+   _mouseDownHandler: function(e) {
+      if ( arguments.length == 0 )
+         e = event;
+
+      // if not button 1 ignore it...
+      var nsEvent = e.which != undefined;
+      if ( (nsEvent && e.which != 1) || (!nsEvent && e.button != 1))
+         return;
+
+      var eventTarget      = e.target ? e.target : e.srcElement;
+      var draggableObject  = eventTarget.draggable;
+
+      var candidate = eventTarget;
+      while (draggableObject == null && candidate.parentNode) {
+         candidate = candidate.parentNode;
+         draggableObject = candidate.draggable;
+      }
+   
+      if ( draggableObject == null )
+         return;
+
+      this.updateSelection( draggableObject, e.ctrlKey );
+
+      // clear the drop zones postion cache...
+      if ( this.hasSelection() )
+         for ( var i = 0 ; i < this.dropZones.length ; i++ )
+            this.dropZones[i].clearPositionCache();
+
+      this.setStartDragFromElement( e, draggableObject.getMouseDownHTMLElement() );
+   },
+
+
+   _mouseMoveHandler: function(e) {
+      var nsEvent = e.which != undefined;
+      if ( !this.interestedInMotionEvents ) {
+         this._terminateEvent(e);
+         return;
+      }
+
+      if ( ! this.hasSelection() )
+         return;
+
+      if ( ! this.currentDragObjectVisible )
+         this._startDrag(e);
+
+      if ( !this.activatedDropZones )
+         this._activateRegisteredDropZones();
+
+      //if ( !this.adjustedForDraggableSize )
+      //   this._adjustForDraggableSize(e);
+
+      this._updateDraggableLocation(e);
+      this._updateDropZonesHover(e);
+
+      this._terminateEvent(e);
+   },
+
+   _makeDraggableObjectVisible: function(e)
+   {
+      if ( !this.hasSelection() )
+         return;
+
+      var dragElement;
+      if ( this.currentDragObjects.length > 1 )
+         dragElement = this.currentDragObjects[0].getMultiObjectDragGUI(this.currentDragObjects);
+      else
+         dragElement = this.currentDragObjects[0].getSingleObjectDragGUI();
+
+      // go ahead and absolute position it...
+      if ( RicoUtil.getElementsComputedStyle(dragElement, "position")  != "absolute" )
+         dragElement.style.position = "absolute";
+
+      // need to parent him into the document...
+      if ( dragElement.parentNode == null || dragElement.parentNode.nodeType == 11 )
+         document.body.appendChild(dragElement);
+
+      this.dragElement = dragElement;
+      this._updateDraggableLocation(e);
+
+      this.currentDragObjectVisible = true;
+   },
+
+   /**
+   _adjustForDraggableSize: function(e) {
+      var dragElementWidth  = this.dragElement.offsetWidth;
+      var dragElementHeight = this.dragElement.offsetHeight;
+      if ( this.startComponentX > dragElementWidth )
+         this.startx -= this.startComponentX - dragElementWidth + 2;
+      if ( e.offsetY ) {
+         if ( this.startComponentY > dragElementHeight )
+            this.starty -= this.startComponentY - dragElementHeight + 2;
+      }
+      this.adjustedForDraggableSize = true;
+   },
+   **/
+
+   _updateDraggableLocation: function(e) {
+      var dragObjectStyle = this.dragElement.style;
+      dragObjectStyle.left = (e.screenX - this.startx) + "px"
+      dragObjectStyle.top  = (e.screenY - this.starty) + "px";
+   },
+
+   _updateDropZonesHover: function(e) {
+      var n = this.dropZones.length;
+      for ( var i = 0 ; i < n ; i++ ) {
+         if ( ! this._mousePointInDropZone( e, this.dropZones[i] ) )
+            this.dropZones[i].hideHover();
+      }
+
+      for ( var i = 0 ; i < n ; i++ ) {
+         if ( this._mousePointInDropZone( e, this.dropZones[i] ) ) {
+            if ( this.dropZones[i].canAccept(this.currentDragObjects) )
+               this.dropZones[i].showHover();
+         }
+      }
+   },
+
+   _startDrag: function(e) {
+      for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
+         this.currentDragObjects[i].startDrag();
+
+      this._makeDraggableObjectVisible(e);
+   },
+
+   _mouseUpHandler: function(e) {
+      if ( ! this.hasSelection() )
+         return;
+
+      var nsEvent = e.which != undefined;
+      if ( (nsEvent && e.which != 1) || (!nsEvent && e.button != 1))
+         return;
+
+      this.interestedInMotionEvents = false;
+
+      if ( this.dragElement == null ) {
+         this._terminateEvent(e);
+         return;
+      }
+
+      if ( this._placeDraggableInDropZone(e) )
+         this._completeDropOperation(e);
+      else {
+         this._terminateEvent(e);
+         new Effect.Position( this.dragElement,
+                              this.origPos.x,
+                              this.origPos.y,
+                              200,
+                              20,
+                              { complete : this._doCancelDragProcessing.bind(this) } );
+      }
+   },
+
+   _completeDropOperation: function(e) {
+      if ( this.dragElement != this.currentDragObjects[0].getMouseDownHTMLElement() ) {
+         if ( this.dragElement.parentNode != null )
+            this.dragElement.parentNode.removeChild(this.dragElement);
+      }
+
+      this._deactivateRegisteredDropZones();
+      this._endDrag();
+      this.clearSelection();
+      this.dragElement = null;
+      this.currentDragObjectVisible = false;
+      this._terminateEvent(e);
+   },
+
+   _doCancelDragProcessing: function() {
+      this._cancelDrag();
+
+      if ( this.dragElement != this.currentDragObjects[0].getMouseDownHTMLElement() ) {
+         if ( this.dragElement.parentNode != null ) {
+            this.dragElement.parentNode.removeChild(this.dragElement);
+         }
+      }
+
+      this._deactivateRegisteredDropZones();
+      this.dragElement = null;
+      this.currentDragObjectVisible = false;
+   },
+
+   _placeDraggableInDropZone: function(e) {
+      var foundDropZone = false;
+      var n = this.dropZones.length;
+      for ( var i = 0 ; i < n ; i++ ) {
+         if ( this._mousePointInDropZone( e, this.dropZones[i] ) ) {
+            if ( this.dropZones[i].canAccept(this.currentDragObjects) ) {
+               this.dropZones[i].hideHover();
+               this.dropZones[i].accept(this.currentDragObjects);
+               foundDropZone = true;
+               break;
+            }
+         }
+      }
+
+      return foundDropZone;
+   },
+
+   _cancelDrag: function() {
+      for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
+         this.currentDragObjects[i].cancelDrag();
+   },
+
+   _endDrag: function() {
+      for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
+         this.currentDragObjects[i].endDrag();
+   },
+
+   _mousePointInDropZone: function( e, dropZone ) {
+
+      var absoluteRect = dropZone.getAbsoluteRect();
+
+      return e.clientX  > absoluteRect.left  &&
+             e.clientX  < absoluteRect.right &&
+             e.clientY  > absoluteRect.top   &&
+             e.clientY  < absoluteRect.bottom;
+   },
+
+   _addMouseDownHandler: function( aDraggable )
+   {
+      var htmlElement = aDraggable.getMouseDownHTMLElement();
+      if ( htmlElement != null ) {
+         htmlElement.draggable = aDraggable;
+         this._addMouseDownEvent( htmlElement );
+      }
+   },
+
+   _activateRegisteredDropZones: function() {
+      var n = this.dropZones.length;
+      for ( var i = 0 ; i < n ; i++ ) {
+         var dropZone = this.dropZones[i];
+         if ( dropZone.canAccept(this.currentDragObjects) )
+            dropZone.activate();
+      }
+
+      this.activatedDropZones = true;
+   },
+
+   _deactivateRegisteredDropZones: function() {
+      var n = this.dropZones.length;
+      for ( var i = 0 ; i < n ; i++ )
+         this.dropZones[i].deactivate();
+      this.activatedDropZones = false;
+   },
+
+   _addMouseDownEvent: function( htmlElement ) {
+      if ( typeof document.implementation != "undefined" &&
+         document.implementation.hasFeature("HTML",   "1.0") &&
+         document.implementation.hasFeature("Events", "2.0") &&
+         document.implementation.hasFeature("CSS",    "2.0") ) {
+         htmlElement.addEventListener("mousedown", this._mouseDownHandler.bindAsEventListener(this), false);
+      }
+      else {
+         htmlElement.attachEvent( "onmousedown", this._mouseDownHandler.bindAsEventListener(this) );
+      }
+   },
+
+   _terminateEvent: function(e) {
+      if ( e.stopPropagation != undefined )
+         e.stopPropagation();
+      else if ( e.cancelBubble != undefined )
+         e.cancelBubble = true;
+
+      if ( e.preventDefault != undefined )
+         e.preventDefault();
+      else
+         e.returnValue = false;
+   },
+
+   initializeEventHandlers: function() {
+      if ( typeof document.implementation != "undefined" &&
+         document.implementation.hasFeature("HTML",   "1.0") &&
+         document.implementation.hasFeature("Events", "2.0") &&
+         document.implementation.hasFeature("CSS",    "2.0") ) {
+         document.addEventListener("mouseup",   this._mouseUpHandler.bindAsEventListener(this),  false);
+         document.addEventListener("mousemove", this._mouseMoveHandler.bindAsEventListener(this), false);
+      }
+      else {
+         document.attachEvent( "onmouseup",   this._mouseUpHandler.bindAsEventListener(this) );
+         document.attachEvent( "onmousemove", this._mouseMoveHandler.bindAsEventListener(this) );
+      }
+   }
+}
+
+var dndMgr = new Rico.DragAndDrop();
+dndMgr.initializeEventHandlers();
+
+
+//-------------------- ricoDraggable.js
+Rico.Draggable = Class.create();
+
+Rico.Draggable.prototype = {
+
+   initialize: function( type, htmlElement ) {
+      this.type          = type;
+      this.htmlElement   = $(htmlElement);
+      this.selected      = false;
+   },
+
+   /**
+    *   Returns the HTML element that should have a mouse down event
+    *   added to it in order to initiate a drag operation
+    *
+    **/
+   getMouseDownHTMLElement: function() {
+      return this.htmlElement;
+   },
+
+   select: function() {
+      this.selected = true;
+
+      if ( this.showingSelected )
+         return;
+
+      var htmlElement = this.getMouseDownHTMLElement();
+
+      var color = Rico.Color.createColorFromBackground(htmlElement);
+      color.isBright() ? color.darken(0.033) : color.brighten(0.033);
+
+      this.saveBackground = RicoUtil.getElementsComputedStyle(htmlElement, "backgroundColor", "background-color");
+      htmlElement.style.backgroundColor = color.asHex();
+      this.showingSelected = true;
+   },
+
+   deselect: function() {
+      this.selected = false;
+      if ( !this.showingSelected )
+         return;
+
+      var htmlElement = this.getMouseDownHTMLElement();
+
+      htmlElement.style.backgroundColor = this.saveBackground;
+      this.showingSelected = false;
+   },
+
+   isSelected: function() {
+      return this.selected;
+   },
+
+   startDrag: function() {
+   },
+
+   cancelDrag: function() {
+   },
+
+   endDrag: function() {
+   },
+
+   getSingleObjectDragGUI: function() {
+      return this.htmlElement;
+   },
+
+   getMultiObjectDragGUI: function( draggables ) {
+      return this.htmlElement;
+   },
+
+   getDroppedGUI: function() {
+      return this.htmlElement;
+   },
+
+   toString: function() {
+      return this.type + ":" + this.htmlElement + ":";
+   }
+
+}
+
+
+//-------------------- ricoDropzone.js
+Rico.Dropzone = Class.create();
+
+Rico.Dropzone.prototype = {
+
+   initialize: function( htmlElement ) {
+      this.htmlElement  = $(htmlElement);
+      this.absoluteRect = null;
+   },
+
+   getHTMLElement: function() {
+      return this.htmlElement;
+   },
+
+   clearPositionCache: function() {
+      this.absoluteRect = null;
+   },
+
+   getAbsoluteRect: function() {
+      if ( this.absoluteRect == null ) {
+         var htmlElement = this.getHTMLElement();
+         var pos = RicoUtil.toViewportPosition(htmlElement);
+
+         this.absoluteRect = {
+            top:    pos.y,
+            left:   pos.x,
+            bottom: pos.y + htmlElement.offsetHeight,
+            right:  pos.x + htmlElement.offsetWidth
+         };
+      }
+      return this.absoluteRect;
+   },
+
+   activate: function() {
+      var htmlElement = this.getHTMLElement();
+      if (htmlElement == null  || this.showingActive)
+         return;
+
+      this.showingActive = true;
+      this.saveBackgroundColor = htmlElement.style.backgroundColor;
+
+      var fallbackColor = "#ffea84";
+      var currentColor = Rico.Color.createColorFromBackground(htmlElement);
+      if ( currentColor == null )
+         htmlElement.style.backgroundColor = fallbackColor;
+      else {
+         currentColor.isBright() ? currentColor.darken(0.2) : currentColor.brighten(0.2);
+         htmlElement.style.backgroundColor = currentColor.asHex();
+      }
+   },
+
+   deactivate: function() {
+      var htmlElement = this.getHTMLElement();
+      if (htmlElement == null || !this.showingActive)
+         return;
+
+      htmlElement.style.backgroundColor = this.saveBackgroundColor;
+      this.showingActive = false;
+      this.saveBackgroundColor = null;
+   },
+
+   showHover: function() {
+      var htmlElement = this.getHTMLElement();
+      if ( htmlElement == null || this.showingHover )
+         return;
+
+      this.saveBorderWidth = htmlElement.style.borderWidth;
+      this.saveBorderStyle = htmlElement.style.borderStyle;
+      this.saveBorderColor = htmlElement.style.borderColor;
+
+      this.showingHover = true;
+      htmlElement.style.borderWidth = "1px";
+      htmlElement.style.borderStyle = "solid";
+      //htmlElement.style.borderColor = "#ff9900";
+      htmlElement.style.borderColor = "#ffff00";
+   },
+
+   hideHover: function() {
+      var htmlElement = this.getHTMLElement();
+      if ( htmlElement == null || !this.showingHover )
+         return;
+
+      htmlElement.style.borderWidth = this.saveBorderWidth;
+      htmlElement.style.borderStyle = this.saveBorderStyle;
+      htmlElement.style.borderColor = this.saveBorderColor;
+      this.showingHover = false;
+   },
+
+   canAccept: function(draggableObjects) {
+      return true;
+   },
+
+   accept: function(draggableObjects) {
+      var htmlElement = this.getHTMLElement();
+      if ( htmlElement == null )
+         return;
+
+      n = draggableObjects.length;
+      for ( var i = 0 ; i < n ; i++ )
+      {
+         var theGUI = draggableObjects[i].getDroppedGUI();
+         if ( RicoUtil.getElementsComputedStyle( theGUI, "position" ) == "absolute" )
+         {
+            theGUI.style.position = "static";
+            theGUI.style.top = "";
+            theGUI.style.top = "";
+         }
+         htmlElement.appendChild(theGUI);
+      }
+   }
+}
+
+
+//-------------------- ricoEffects.js
+
+/**
+  *  Use the Effect namespace for effects.  If using scriptaculous effects
+  *  this will already be defined, otherwise we'll just create an empty
+  *  object for it...
+ **/
+if ( window.Effect == undefined )
+   Effect = {};
+
+Effect.SizeAndPosition = Class.create();
+Effect.SizeAndPosition.prototype = {
+
+   initialize: function(element, x, y, w, h, duration, steps, options) {
+      this.element = $(element);
+      this.x = x;
+      this.y = y;
+      this.w = w;
+      this.h = h;
+      this.duration = duration;
+      this.steps    = steps;
+      this.options  = arguments[7] || {};
+
+      this.sizeAndPosition();
+   },
+
+   sizeAndPosition: function() {
+      if (this.isFinished()) {
+         if(this.options.complete) this.options.complete(this);
+         return;
+      }
+
+      if (this.timer)
+         clearTimeout(this.timer);
+
+      var stepDuration = Math.round(this.duration/this.steps) ;
+
+      // Get original values: x,y = top left corner;  w,h = width height
+      var currentX = this.element.offsetLeft;
+      var currentY = this.element.offsetTop;
+      var currentW = this.element.offsetWidth;
+      var currentH = this.element.offsetHeight;
+
+      // If values not set, or zero, we do not modify them, and take original as final as well
+      this.x = (this.x) ? this.x : currentX;
+      this.y = (this.y) ? this.y : currentY;
+      this.w = (this.w) ? this.w : currentW;
+      this.h = (this.h) ? this.h : currentH;
+
+      // how much do we need to modify our values for each step?
+      var difX = this.steps >  0 ? (this.x - currentX)/this.steps : 0;
+      var difY = this.steps >  0 ? (this.y - currentY)/this.steps : 0;
+      var difW = this.steps >  0 ? (this.w - currentW)/this.steps : 0;
+      var difH = this.steps >  0 ? (this.h - currentH)/this.steps : 0;
+
+      this.moveBy(difX, difY);
+      this.resizeBy(difW, difH);
+
+      this.duration -= stepDuration;
+      this.steps--;
+
+      this.timer = setTimeout(this.sizeAndPosition.bind(this), stepDuration);
+   },
+
+   isFinished: function() {
+      return this.steps <= 0;
+   },
+
+   moveBy: function( difX, difY ) {
+      var currentLeft = this.element.offsetLeft;
+      var currentTop  = this.element.offsetTop;
+      var intDifX     = parseInt(difX);
+      var intDifY     = parseInt(difY);
+
+      var style = this.element.style;
+      if ( intDifX != 0 )
+         style.left = (currentLeft + intDifX) + "px";
+      if ( intDifY != 0 )
+         style.top  = (currentTop + intDifY) + "px";
+   },
+
+   resizeBy: function( difW, difH ) {
+      var currentWidth  = this.element.offsetWidth;
+      var currentHeight = this.element.offsetHeight;
+      var intDifW       = parseInt(difW);
+      var intDifH       = parseInt(difH);
+
+      var style = this.element.style;
+      if ( intDifW != 0 )
+         style.width   = (currentWidth  + intDifW) + "px";
+      if ( intDifH != 0 )
+         style.height  = (currentHeight + intDifH) + "px";
+   }
+}
+
+Effect.Size = Class.create();
+Effect.Size.prototype = {
+
+   initialize: function(element, w, h, duration, steps, options) {
+      new Effect.SizeAndPosition(element, null, null, w, h, duration, steps, options);
+  }
+}
+
+Effect.Position = Class.create();
+Effect.Position.prototype = {
+
+   initialize: function(element, x, y, duration, steps, options) {
+      new Effect.SizeAndPosition(element, x, y, null, null, duration, steps, options);
+  }
+}
+
+Effect.Round = Class.create();
+Effect.Round.prototype = {
+
+   initialize: function(tagName, className, options) {
+      var elements = document.getElementsByTagAndClassName(tagName,className);
+      for ( var i = 0 ; i < elements.length ; i++ )
+         Rico.Corner.round( elements[i], options );
+   }
+};
+
+Effect.FadeTo = Class.create();
+Effect.FadeTo.prototype = {
+
+   initialize: function( element, opacity, duration, steps, options) {
+      this.element  = $(element);
+      this.opacity  = opacity;
+      this.duration = duration;
+      this.steps    = steps;
+      this.options  = arguments[4] || {};
+      this.fadeTo();
+   },
+
+   fadeTo: function() {
+      if (this.isFinished()) {
+         if(this.options.complete) this.options.complete(this);
+         return;
+      }
+
+      if (this.timer)
+         clearTimeout(this.timer);
+
+      var stepDuration = Math.round(this.duration/this.steps) ;
+      var currentOpacity = this.getElementOpacity();
+      var delta = this.steps > 0 ? (this.opacity - currentOpacity)/this.steps : 0;
+
+      this.changeOpacityBy(delta);
+      this.duration -= stepDuration;
+      this.steps--;
+
+      this.timer = setTimeout(this.fadeTo.bind(this), stepDuration);
+   },
+
+   changeOpacityBy: function(v) {
+      var currentOpacity = this.getElementOpacity();
+      var newOpacity = Math.max(0, Math.min(currentOpacity+v, 1));
+      this.element.ricoOpacity = newOpacity;
+
+      this.element.style.filter = "alpha(opacity:"+Math.round(newOpacity*100)+")";
+      this.element.style.opacity = newOpacity; /*//*/;
+   },
+
+   isFinished: function() {
+      return this.steps <= 0;
+   },
+
+   getElementOpacity: function() {
+      if ( this.element.ricoOpacity == undefined ) {
+         var opacity;
+         if ( this.element.currentStyle ) {
+            opacity = this.element.currentStyle.opacity;
+         }
+         else if ( document.defaultView.getComputedStyle != undefined ) {
+            var computedStyle = document.defaultView.getComputedStyle;
+            opacity = computedStyle(this.element, null).getPropertyValue('opacity');
+         }
+
+         this.element.ricoOpacity = opacity != undefined ? opacity : 1.0;
+      }
+
+      return parseFloat(this.element.ricoOpacity);
+   }
+}
+
+Effect.AccordionSize = Class.create();
+
+Effect.AccordionSize.prototype = {
+
+   initialize: function(e1, e2, start, end, duration, steps, options) {
+      this.e1       = $(e1);
+      this.e2       = $(e2);
+      this.start    = start;
+      this.end      = end;
+      this.duration = duration;
+      this.steps    = steps;
+      this.options  = arguments[6] || {};
+
+      this.accordionSize();
+   },
+
+   accordionSize: function() {
+
+      if (this.isFinished()) {
+         // just in case there are round errors or such...
+         this.e1.style.height = this.start + "px";
+         this.e2.style.height = this.end + "px";
+
+         if(this.options.complete)
+            this.options.complete(this);
+         return;
+      }
+
+      if (this.timer)
+         clearTimeout(this.timer);
+
+      var stepDuration = Math.round(this.duration/this.steps) ;
+
+      var diff = this.steps > 0 ? (parseInt(this.e1.offsetHeight) - this.start)/this.steps : 0;
+      this.resizeBy(diff);
+
+      this.duration -= stepDuration;
+      this.steps--;
+
+      this.timer = setTimeout(this.accordionSize.bind(this), stepDuration);
+   },
+
+   isFinished: function() {
+      return this.steps <= 0;
+   },
+
+   resizeBy: function(diff) {
+      var h1Height = this.e1.offsetHeight;
+      var h2Height = this.e2.offsetHeight;
+      var intDiff = parseInt(diff);
+      if ( diff != 0 ) {
+         this.e1.style.height = (h1Height - intDiff) + "px";
+         this.e2.style.height = (h2Height + intDiff) + "px";
+      }
+   }
+
+};
+
+
+//-------------------- ricoLiveGrid.js
+
+// Rico.LiveGridMetaData -----------------------------------------------------
+
+Rico.LiveGridMetaData = Class.create();
+
+Rico.LiveGridMetaData.prototype = {
+
+   initialize: function( pageSize, totalRows, columnCount, options ) {
+      this.pageSize  = pageSize;
+      this.totalRows = totalRows;
+      this.setOptions(options);
+      this.scrollArrowHeight = 16;
+      this.columnCount = columnCount;
+   },
+
+   setOptions: function(options) {
+      this.options = {
+         largeBufferSize    : 7.0,   // 7 pages
+         nearLimitFactor    : 0.2    // 20% of buffer
+      }.extend(options || {});
+   },
+
+   getPageSize: function() {
+      return this.pageSize;
+   },
+
+   getTotalRows: function() {
+      return this.totalRows;
+   },
+
+   setTotalRows: function(n) {
+      this.totalRows = n;
+   },
+
+   getLargeBufferSize: function() {
+      return parseInt(this.options.largeBufferSize * this.pageSize);
+   },
+
+   getLimitTolerance: function() {
+      return parseInt(this.getLargeBufferSize() * this.options.nearLimitFactor);
+   }
+};
+
+// Rico.LiveGridScroller -----------------------------------------------------
+
+Rico.LiveGridScroller = Class.create();
+
+Rico.LiveGridScroller.prototype = {
+
+   initialize: function(liveGrid, viewPort) {
+      this.isIE = navigator.userAgent.toLowerCase().indexOf("msie") >= 0;
+      this.liveGrid = liveGrid;
+      this.metaData = liveGrid.metaData;
+      this.createScrollBar();
+      this.scrollTimeout = null;
+      this.lastScrollPos = 0;
+      this.viewPort = viewPort;
+      this.rows = new Array();
+   },
+
+   isUnPlugged: function() {
+      return this.scrollerDiv.onscroll == null;
+   },
+
+   plugin: function() {
+      this.scrollerDiv.onscroll = this.handleScroll.bindAsEventListener(this);
+   },
+
+   unplug: function() {
+      this.scrollerDiv.onscroll = null;
+   },
+
+   sizeIEHeaderHack: function() {
+      if ( !this.isIE ) return;
+      var headerTable = $(this.liveGrid.tableId + "_header");
+      if ( headerTable )
+         headerTable.rows[0].cells[0].style.width =
+            (headerTable.rows[0].cells[0].offsetWidth + 1) + "px";
+   },
+
+   createScrollBar: function() {
+      var visibleHeight = this.liveGrid.viewPort.visibleHeight();
+      // create the outer div...
+      this.scrollerDiv  = document.createElement("div");
+      var scrollerStyle = this.scrollerDiv.style;
+      scrollerStyle.borderRight = "1px solid #ababab"; // hard coded color!!!
+      scrollerStyle.position    = "relative";
+      scrollerStyle.left        = this.isIE ? "-6px" : "-3px";
+      scrollerStyle.width       = "19px";
+      scrollerStyle.height      = visibleHeight + "px";
+      scrollerStyle.overflow    = "auto";
+
+      // create the inner div...
+      this.heightDiv = document.createElement("div");
+      this.heightDiv.style.width  = "1px";
+
+      this.heightDiv.style.height = parseInt(visibleHeight *
+                        this.metaData.getTotalRows()/this.metaData.getPageSize()) + "px" ;
+      this.scrollerDiv.appendChild(this.heightDiv);
+      this.scrollerDiv.onscroll = this.handleScroll.bindAsEventListener(this);
+
+     var table = this.liveGrid.table;
+     table.parentNode.parentNode.insertBefore( this.scrollerDiv, table.parentNode.nextSibling );
+   },
+
+   updateSize: function() {
+      var table = this.liveGrid.table;
+      var visibleHeight = this.viewPort.visibleHeight();
+      this.heightDiv.style.height = parseInt(visibleHeight *
+                                  this.metaData.getTotalRows()/this.metaData.getPageSize()) + "px";
+   },
+
+   rowToPixel: function(rowOffset) {
+      return (rowOffset / this.metaData.getTotalRows()) * this.heightDiv.offsetHeight
+   },
+   
+   moveScroll: function(rowOffset) {
+      this.scrollerDiv.scrollTop = this.rowToPixel(rowOffset);
+      if ( this.metaData.options.onscroll )
+         this.metaData.options.onscroll( this.liveGrid, rowOffset );    
+   },
+
+   handleScroll: function() {
+     if ( this.scrollTimeout )
+         clearTimeout( this.scrollTimeout );
+
+      var contentOffset = parseInt(this.scrollerDiv.scrollTop / this.viewPort.rowHeight);
+      this.liveGrid.requestContentRefresh(contentOffset);
+      this.viewPort.scrollTo(this.scrollerDiv.scrollTop);
+      
+      if ( this.metaData.options.onscroll )
+         this.metaData.options.onscroll( this.liveGrid, contentOffset );
+
+      this.scrollTimeout = setTimeout( this.scrollIdle.bind(this), 1200 );
+   },
+
+   scrollIdle: function() {
+      if ( this.metaData.options.onscrollidle )
+         this.metaData.options.onscrollidle();
+   }
+};
+
+// Rico.LiveGridBuffer -----------------------------------------------------
+
+Rico.LiveGridBuffer = Class.create();
+
+Rico.LiveGridBuffer.prototype = {
+
+   initialize: function(metaData, viewPort) {
+      this.startPos = 0;
+      this.size     = 0;
+      this.metaData = metaData;
+      this.rows     = new Array();
+      this.updateInProgress = false;
+      this.viewPort = viewPort;
+      this.maxBufferSize = metaData.getLargeBufferSize() * 2;
+      this.maxFetchSize = metaData.getLargeBufferSize();
+      this.lastOffset = 0;
+   },
+
+   getBlankRow: function() {
+      if (!this.blankRow ) {
+         this.blankRow = new Array();
+         for ( var i=0; i < this.metaData.columnCount ; i++ ) 
+            this.blankRow[i] = " ";
+     }
+     return this.blankRow;
+   },
+   
+   loadRows: function(ajaxResponse) {
+      var rowsElement = ajaxResponse.getElementsByTagName('rows')[0];
+      this.updateUI = rowsElement.getAttribute("update_ui") == "true"
+      var newRows = new Array()
+      var trs = rowsElement.getElementsByTagName("tr");
+      for ( var i=0 ; i < trs.length; i++ ) {
+         var row = newRows[i] = new Array(); 
+         var cells = trs[i].getElementsByTagName("td");
+         for ( var j=0; j < cells.length ; j++ ) {
+            var cell = cells[j];
+            var convertSpaces = cell.getAttribute("convert_spaces") == "true";
+            var cellContent = RicoUtil.getContentAsString(cell);
+            row[j] = convertSpaces ? this.convertSpaces(cellContent) : cellContent;
+            if (!row[j]) 
+               row[j] = ' ';
+         }
+      }
+      return newRows;
+   },
+      
+   update: function(ajaxResponse, start) {
+     var newRows = this.loadRows(ajaxResponse);
+      if (this.rows.length == 0) { // initial load
+         this.rows = newRows;
+         this.size = this.rows.length;
+         this.startPos = start;
+         return;
+      }
+      if (start > this.startPos) { //appending
+         if (this.startPos + this.rows.length < start) {
+            this.rows =  newRows;
+            this.startPos = start;//
+         } else {
+              this.rows = this.rows.concat( newRows.slice(0, newRows.length));
+            if (this.rows.length > this.maxBufferSize) {
+               var fullSize = this.rows.length;
+               this.rows = this.rows.slice(this.rows.length - this.maxBufferSize, this.rows.length)
+               this.startPos = this.startPos +  (fullSize - this.rows.length);
+            }
+         }
+      } else { //prepending
+         if (start + newRows.length < this.startPos) {
+            this.rows =  newRows;
+         } else {
+            this.rows = newRows.slice(0, this.startPos).concat(this.rows);
+            if (this.rows.length > this.maxBufferSize) 
+               this.rows = this.rows.slice(0, this.maxBufferSize)
+         }
+         this.startPos =  start;
+      }
+      this.size = this.rows.length;
+   },
+   
+   clear: function() {
+      this.rows = new Array();
+      this.startPos = 0;
+      this.size = 0;
+   },
+
+   isOverlapping: function(start, size) {
+      return ((start < this.endPos()) && (this.startPos < start + size)) || (this.endPos() == 0)
+   },
+
+   isInRange: function(position) {
+      return (position >= this.startPos) && (position + this.metaData.getPageSize() <= this.endPos()); 
+             //&& this.size()  != 0;
+   },
+
+   isNearingTopLimit: function(position) {
+      return position - this.startPos < this.metaData.getLimitTolerance();
+   },
+
+   endPos: function() {
+      return this.startPos + this.rows.length;
+   },
+   
+   isNearingBottomLimit: function(position) {
+      return this.endPos() - (position + this.metaData.getPageSize()) < this.metaData.getLimitTolerance();
+   },
+
+   isAtTop: function() {
+      return this.startPos == 0;
+   },
+
+   isAtBottom: function() {
+      return this.endPos() == this.metaData.getTotalRows();
+   },
+
+   isNearingLimit: function(position) {
+      return ( !this.isAtTop()    && this.isNearingTopLimit(position)) ||
+             ( !this.isAtBottom() && this.isNearingBottomLimit(position) )
+   },
+
+   getFetchSize: function(offset) {
+      var adjustedOffset = this.getFetchOffset(offset);
+      var adjustedSize = 0;
+      if (adjustedOffset >= this.startPos) { //apending
+         var endFetchOffset = this.maxFetchSize  + adjustedOffset;
+         if (endFetchOffset > this.metaData.totalRows)
+            endFetchOffset = this.metaData.totalRows;
+         adjustedSize = endFetchOffset - adjustedOffset;   
+      } else {//prepending
+         var adjustedSize = this.startPos - adjustedOffset;
+         if (adjustedSize > this.maxFetchSize)
+            adjustedSize = this.maxFetchSize;
+      }
+      return adjustedSize;
+   }, 
+
+   getFetchOffset: function(offset) {
+      var adjustedOffset = offset;
+      if (offset > this.startPos)  //apending
+         adjustedOffset = (offset > this.endPos()) ? offset :  this.endPos(); 
+      else { //prepending
+         if (offset + this.maxFetchSize >= this.startPos) {
+            var adjustedOffset = this.startPos - this.maxFetchSize;
+            if (adjustedOffset < 0)
+               adjustedOffset = 0;
+         }
+      }
+      this.lastOffset = adjustedOffset;
+      return adjustedOffset;
+   },
+
+   getRows: function(start, count) {
+      var begPos = start - this.startPos
+      var endPos = begPos + count
+
+      // er? need more data...
+      if ( endPos > this.size )
+         endPos = this.size
+
+      var results = new Array()
+      var index = 0;
+      for ( var i=begPos ; i < endPos; i++ ) {
+         results[index++] = this.rows[i]
+      }
+      return results
+   },
+
+   convertSpaces: function(s) {
+      return s.split(" ").join(" ");
+   }
+
+};
+
+
+//Rico.GridViewPort --------------------------------------------------
+Rico.GridViewPort = Class.create();
+
+Rico.GridViewPort.prototype = {
+
+   initialize: function(table, rowHeight, visibleRows, buffer, liveGrid) {
+      this.lastDisplayedStartPos = 0;
+      this.div = table.parentNode;
+      this.table = table
+      this.rowHeight = rowHeight;
+      this.div.style.height = this.rowHeight * visibleRows;
+      this.div.style.overflow = "hidden";
+      this.buffer = buffer;
+      this.liveGrid = liveGrid;
+      this.visibleRows = visibleRows + 1;
+      this.lastPixelOffset = 0;
+      this.startPos = 0;
+   },
+
+   populateRow: function(htmlRow, row) {
+      for (var j=0; j < row.length; j++) {
+         htmlRow.cells[j].innerHTML = row[j]
+      }
+   },
+   
+   bufferChanged: function() {
+      this.refreshContents( parseInt(this.lastPixelOffset / this.rowHeight));
+   },
+   
+   clearRows: function() {
+      if (!this.isBlank) {
+         for (var i=0; i < this.visibleRows; i++)
+            this.populateRow(this.table.rows[i], this.buffer.getBlankRow());
+         this.isBlank = true;
+      }
+   },
+   
+   clearContents: function() {   
+      this.clearRows();
+      this.scrollTo(0);
+      this.startPos = 0;
+      this.lastStartPos = -1;   
+   },
+   
+   refreshContents: function(startPos) {
+      if (startPos == this.lastRowPos && !this.isPartialBlank && !this.isBlank) {
+         return;
+      }
+      if ((startPos + this.visibleRows < this.buffer.startPos)  
+          || (this.buffer.startPos + this.buffer.size < startPos) 
+          || (this.buffer.size == 0)) {
+         this.clearRows();
+         return;
+      }
+      this.isBlank = false;
+      var viewPrecedesBuffer = this.buffer.startPos > startPos
+      var contentStartPos = viewPrecedesBuffer ? this.buffer.startPos: startPos;
+   
+      var contentEndPos = (this.buffer.startPos + this.buffer.size < startPos + this.visibleRows) 
+                                 ? this.buffer.startPos + this.buffer.size
+                                 : startPos + this.visibleRows;       
+      var rowSize = contentEndPos - contentStartPos;
+      var rows = this.buffer.getRows(contentStartPos, rowSize ); 
+      var blankSize = this.visibleRows - rowSize;
+      var blankOffset = viewPrecedesBuffer ? 0: rowSize;
+      var contentOffset = viewPrecedesBuffer ? blankSize: 0;
+
+      for (var i=0; i < rows.length; i++) {//initialize what we have
+        this.populateRow(this.table.rows[i + contentOffset], rows[i]);
+      }       
+      for (var i=0; i < blankSize; i++) {// blank out the rest 
+        this.populateRow(this.table.rows[i + blankOffset], this.buffer.getBlankRow());
+      }
+      this.isPartialBlank = blankSize > 0;
+      this.lastRowPos = startPos;   
+   },
+
+   scrollTo: function(pixelOffset) {      
+      if (this.lastPixelOffset == pixelOffset)
+         return;
+
+      this.refreshContents(parseInt(pixelOffset / this.rowHeight))
+      this.div.scrollTop = pixelOffset % this.rowHeight        
+      
+      this.lastPixelOffset = pixelOffset;
+   },
+   
+   visibleHeight: function() {
+      return parseInt(this.div.style.height);
+   }
+   
+};
+
+
+Rico.LiveGridRequest = Class.create();
+Rico.LiveGridRequest.prototype = {
+   initialize: function( requestOffset, options ) {
+      this.requestOffset = requestOffset;
+   }
+};
+
+// Rico.LiveGrid -----------------------------------------------------
+
+Rico.LiveGrid = Class.create();
+
+Rico.LiveGrid.prototype = {
+
+   initialize: function( tableId, visibleRows, totalRows, url, options ) {
+      if ( options == null )
+         options = {};
+
+      this.tableId     = tableId; 
+      this.table       = $(tableId);
+      var columnCount  = this.table.rows[0].cells.length
+      this.metaData    = new Rico.LiveGridMetaData(visibleRows, totalRows, columnCount, options);
+      this.buffer      = new Rico.LiveGridBuffer(this.metaData);
+
+      var rowCount = this.table.rows.length;
+      this.viewPort =  new Rico.GridViewPort(this.table, 
+                                            this.table.offsetHeight/rowCount,
+                                            visibleRows,
+                                            this.buffer, this);
+      this.scroller    = new Rico.LiveGridScroller(this,this.viewPort);
+      
+      this.additionalParms       = options.requestParameters || [];
+      
+      options.sortHandler = this.sortHandler.bind(this);
+
+      if ( $(tableId + '_header') )
+         this.sort = new Rico.LiveGridSort(tableId + '_header', options)
+
+      this.processingRequest = null;
+      this.unprocessedRequest = null;
+
+      this.initAjax(url);
+      if ( options.prefetchBuffer || options.prefetchOffset > 0) {
+         var offset = 0;
+         if (options.offset ) {
+            offset = options.offset;            
+            this.scroller.moveScroll(offset);
+            this.viewPort.scrollTo(this.scroller.rowToPixel(offset));            
+         }
+         if (options.sortCol) {
+             this.sortCol = options.sortCol;
+             this.sortDir = options.sortDir;
+         }
+         this.requestContentRefresh(offset);
+      }
+   },
+
+   resetContents: function() {
+      this.scroller.moveScroll(0);
+      this.buffer.clear();
+      this.viewPort.clearContents();
+   },
+   
+   sortHandler: function(column) {
+      this.sortCol = column.name;
+      this.sortDir = column.currentSort;
+
+      this.resetContents();
+      this.requestContentRefresh(0) 
+   },
+   
+   setRequestParams: function() {
+      this.additionalParms = [];
+      for ( var i=0 ; i < arguments.length ; i++ )
+         this.additionalParms[i] = arguments[i];
+   },
+
+   setTotalRows: function( newTotalRows ) {
+      this.resetContents();
+      this.metaData.setTotalRows(newTotalRows);
+      this.scroller.updateSize();
+   },
+
+   initAjax: function(url) {
+      ajaxEngine.registerRequest( this.tableId + '_request', url );
+      ajaxEngine.registerAjaxObject( this.tableId + '_updater', this );
+   },
+
+   invokeAjax: function() {
+   },
+
+   handleTimedOut: function() {
+      //server did not respond in 4 seconds... assume that there could have been
+      //an error or something, and allow requests to be processed again...
+      this.processingRequest = null;
+      this.processQueuedRequest();
+   },
+
+   fetchBuffer: function(offset) {
+      if ( this.buffer.isInRange(offset) &&
+         !this.buffer.isNearingLimit(offset)) {
+         return;
+      }
+      if (this.processingRequest) {
+          this.unprocessedRequest = new Rico.LiveGridRequest(offset);
+         return;
+      }
+      var bufferStartPos = this.buffer.getFetchOffset(offset);
+      this.processingRequest = new Rico.LiveGridRequest(offset);
+      this.processingRequest.bufferOffset = bufferStartPos;   
+      var fetchSize = this.buffer.getFetchSize(offset);
+      var partialLoaded = false;
+      var callParms = []; 
+      callParms.push(this.tableId + '_request');
+      callParms.push('id='        + this.tableId);
+      callParms.push('page_size=' + fetchSize);
+      callParms.push('offset='    + bufferStartPos);
+      if ( this.sortCol) {
+         callParms.push('sort_col='    + this.sortCol);
+         callParms.push('sort_dir='    + this.sortDir);
+      }
+      
+      for( var i=0 ; i < this.additionalParms.length ; i++ )
+         callParms.push(this.additionalParms[i]);
+      ajaxEngine.sendRequest.apply( ajaxEngine, callParms );
+        
+      this.timeoutHandler = setTimeout( this.handleTimedOut.bind(this), 20000 ); //todo: make as option
+   },
+
+   requestContentRefresh: function(contentOffset) {
+      this.fetchBuffer(contentOffset);
+   },
+
+   ajaxUpdate: function(ajaxResponse) {
+      try {
+         clearTimeout( this.timeoutHandler );
+         this.buffer.update(ajaxResponse,this.processingRequest.bufferOffset);
+         this.viewPort.bufferChanged();
+      }
+      catch(err) {}
+      finally {this.processingRequest = null; }
+      this.processQueuedRequest();
+   },
+
+   processQueuedRequest: function() {
+      if (this.unprocessedRequest != null) {
+         this.requestContentRefresh(this.unprocessedRequest.requestOffset);
+         this.unprocessedRequest = null
+      }  
+   }
+ 
+};
+
+
+//-------------------- ricoLiveGridSort.js
+Rico.LiveGridSort = Class.create();
+
+Rico.LiveGridSort.prototype = {
+
+   initialize: function(headerTableId, options) {
+      this.headerTableId = headerTableId;
+      this.headerTable   = $(headerTableId);
+      this.setOptions(options);
+      this.applySortBehavior();
+
+      if ( this.options.sortCol ) {
+         this.setSortUI( this.options.sortCol, this.options.sortDir );
+      }
+   },
+
+   setSortUI: function( columnName, sortDirection ) {
+      var cols = this.options.columns;
+      for ( var i = 0 ; i < cols.length ; i++ ) {
+         if ( cols[i].name == columnName ) {
+            this.setColumnSort(i, sortDirection);
+            break;
+         }
+      }
+   },
+
+   setOptions: function(options) {
+      this.options = {
+         sortAscendImg:    'images/sort_asc.gif',
+         sortDescendImg:   'images/sort_desc.gif',
+         imageWidth:       9,
+         imageHeight:      5,
+         ajaxSortURLParms: []
+      }.extend(options);
+
+      // preload the images...
+      new Image().src = this.options.sortAscendImg;
+      new Image().src = this.options.sortDescendImg;
+
+      this.sort = options.sortHandler;
+      if ( !this.options.columns )
+         this.options.columns = this.introspectForColumnInfo();
+      else {
+         // allow client to pass { columns: [ ["a", true], ["b", false] ] }
+         // and convert to an array of Rico.TableColumn objs...
+         this.options.columns = this.convertToTableColumns(this.options.columns);
+      }
+   },
+
+   applySortBehavior: function() {
+      var headerRow   = this.headerTable.rows[0];
+      var headerCells = headerRow.cells;
+      for ( var i = 0 ; i < headerCells.length ; i++ ) {
+         this.addSortBehaviorToColumn( i, headerCells[i] );
+      }
+   },
+
+   addSortBehaviorToColumn: function( n, cell ) {
+      if ( this.options.columns[n].isSortable() ) {
+         cell.id            = this.headerTableId + '_' + n;
+         cell.style.cursor  = 'pointer';
+         cell.onclick       = this.headerCellClicked.bindAsEventListener(this);
+         cell.innerHTML     = cell.innerHTML + '<span id="' + this.headerTableId + '_img_' + n + '">'
+                           + '   </span>';
+      }
+   },
+
+   // event handler....
+   headerCellClicked: function(evt) {
+      var eventTarget = evt.target ? evt.target : evt.srcElement;
+      var cellId = eventTarget.id;
+      var columnNumber = parseInt(cellId.substring( cellId.lastIndexOf('_') + 1 ));
+      var sortedColumnIndex = this.getSortedColumnIndex();
+      if ( sortedColumnIndex != -1 ) {
+         if ( sortedColumnIndex != columnNumber ) {
+            this.removeColumnSort(sortedColumnIndex);
+            this.setColumnSort(columnNumber, Rico.TableColumn.SORT_ASC);
+         }
+         else
+            this.toggleColumnSort(sortedColumnIndex);
+      }
+      else
+         this.setColumnSort(columnNumber, Rico.TableColumn.SORT_ASC);
+
+      if (this.options.sortHandler) {
+         this.options.sortHandler(this.options.columns[columnNumber]);
+      }
+   },
+
+   removeColumnSort: function(n) {
+      this.options.columns[n].setUnsorted();
+      this.setSortImage(n);
+   },
+
+   setColumnSort: function(n, direction) {
+      this.options.columns[n].setSorted(direction);
+      this.setSortImage(n);
+   },
+
+   toggleColumnSort: function(n) {
+      this.options.columns[n].toggleSort();
+      this.setSortImage(n);
+   },
+
+   setSortImage: function(n) {
+      var sortDirection = this.options.columns[n].getSortDirection();
+
+      var sortImageSpan = $( this.headerTableId + '_img_' + n );
+      if ( sortDirection == Rico.TableColumn.UNSORTED )
+         sortImageSpan.innerHTML = '  ';
+      else if ( sortDirection == Rico.TableColumn.SORT_ASC )
+         sortImageSpan.innerHTML = '  <img width="'  + this.options.imageWidth    + '" ' +
+                                                     'height="'+ this.options.imageHeight   + '" ' +
+                                                     'src="'   + this.options.sortAscendImg + '"/>';
+      else if ( sortDirection == Rico.TableColumn.SORT_DESC )
+         sortImageSpan.innerHTML = '  <img width="'  + this.options.imageWidth    + '" ' +
+                                                     'height="'+ this.options.imageHeight   + '" ' +
+                                                     'src="'   + this.options.sortDescendImg + '"/>';
+   },
+
+   getSortedColumnIndex: function() {
+      var cols = this.options.columns;
+      for ( var i = 0 ; i < cols.length ; i++ ) {
+         if ( cols[i].isSorted() )
+            return i;
+      }
+
+      return -1;
+   },
+
+   introspectForColumnInfo: function() {
+      var columns = new Array();
+      var headerRow   = this.headerTable.rows[0];
+      var headerCells = headerRow.cells;
+      for ( var i = 0 ; i < headerCells.length ; i++ )
+         columns.push( new Rico.TableColumn( this.deriveColumnNameFromCell(headerCells[i],i), true ) );
+      return columns;
+   },
+
+   convertToTableColumns: function(cols) {
+      var columns = new Array();
+      for ( var i = 0 ; i < cols.length ; i++ )
+         columns.push( new Rico.TableColumn( cols[i][0], cols[i][1] ) );
+   },
+
+   deriveColumnNameFromCell: function(cell,columnNumber) {
+      var cellContent = cell.innerText != undefined ? cell.innerText : cell.textContent;
+      return cellContent ? cellContent.toLowerCase().split(' ').join('_') : "col_" + columnNumber;
+   }
+};
+
+Rico.TableColumn = Class.create();
+
+Rico.TableColumn.UNSORTED  = 0;
+Rico.TableColumn.SORT_ASC  = "ASC";
+Rico.TableColumn.SORT_DESC = "DESC";
+
+Rico.TableColumn.prototype = {
+   initialize: function(name, sortable) {
+      this.name        = name;
+      this.sortable    = sortable;
+      this.currentSort = Rico.TableColumn.UNSORTED;
+   },
+
+   isSortable: function() {
+      return this.sortable;
+   },
+
+   isSorted: function() {
+      return this.currentSort != Rico.TableColumn.UNSORTED;
+   },
+
+   getSortDirection: function() {
+      return this.currentSort;
+   },
+
+   toggleSort: function() {
+      if ( this.currentSort == Rico.TableColumn.UNSORTED || this.currentSort == Rico.TableColumn.SORT_DESC )
+         this.currentSort = Rico.TableColumn.SORT_ASC;
+      else if ( this.currentSort == Rico.TableColumn.SORT_ASC )
+         this.currentSort = Rico.TableColumn.SORT_DESC;
+   },
+
+   setUnsorted: function(direction) {
+      this.setSorted(Rico.TableColumn.UNSORTED);
+   },
+
+   setSorted: function(direction) {
+      // direction must by one of Rico.TableColumn.UNSORTED, .SORT_ASC, or .SET_DESC...
+      this.currentSort = direction;
+   }
+
+};
+
+
+//-------------------- ricoUtil.js
+
+var RicoUtil = {
+
+   getElementsComputedStyle: function ( htmlElement, cssProperty, mozillaEquivalentCSS) {
+      if ( arguments.length == 2 )
+         mozillaEquivalentCSS = cssProperty;
+
+      var el = $(htmlElement);
+      if ( el.currentStyle )
+         return el.currentStyle[cssProperty];
+      else
+         return document.defaultView.getComputedStyle(el, null).getPropertyValue(mozillaEquivalentCSS);
+   },
+
+   createXmlDocument : function() {
+      if (document.implementation && document.implementation.createDocument) {
+         var doc = document.implementation.createDocument("", "", null);
+
+         if (doc.readyState == null) {
+            doc.readyState = 1;
+            doc.addEventListener("load", function () {
+               doc.readyState = 4;
+               if (typeof doc.onreadystatechange == "function")
+                  doc.onreadystatechange();
+            }, false);
+         }
+
+         return doc;
+      }
+
+      if (window.ActiveXObject)
+          return Try.these(
+            function() { return new ActiveXObject('MSXML2.DomDocument')   },
+            function() { return new ActiveXObject('Microsoft.DomDocument')},
+            function() { return new ActiveXObject('MSXML.DomDocument')    },
+            function() { return new ActiveXObject('MSXML3.DomDocument')   }
+          ) || false;
+
+      return null;
+   },
+
+   getContentAsString: function( parentNode ) {
+      return parentNode.xml != undefined ? 
+         this._getContentAsStringIE(parentNode) :
+         this._getContentAsStringMozilla(parentNode);
+   },
+
+   _getContentAsStringIE: function(parentNode) {
+      var contentStr = "";
+      for ( var i = 0 ; i < parentNode.childNodes.length ; i++ )
+         contentStr += parentNode.childNodes[i].xml;
+      return contentStr;
+   },
+
+   _getContentAsStringMozilla: function(parentNode) {
+      var xmlSerializer = new XMLSerializer();
+      var contentStr = "";
+      for ( var i = 0 ; i < parentNode.childNodes.length ; i++ )
+         contentStr += xmlSerializer.serializeToString(parentNode.childNodes[i]);
+      return contentStr;
+   },
+
+   toViewportPosition: function(element) {
+      return this._toAbsolute(element,true);
+   },
+
+   toDocumentPosition: function(element) {
+      return this._toAbsolute(element,false);
+   },
+
+   /**
+    *  Compute the elements position in terms of the window viewport
+    *  so that it can be compared to the position of the mouse (dnd)
+    *  This is additions of all the offsetTop,offsetLeft values up the
+    *  offsetParent hierarchy, ...taking into account any scrollTop,
+    *  scrollLeft values along the way...
+    *
+    * IE has a bug reporting a correct offsetLeft of elements within a
+    * a relatively positioned parent!!!
+    **/
+   _toAbsolute: function(element,accountForDocScroll) {
+
+      if ( navigator.userAgent.toLowerCase().indexOf("msie") == -1 )
+         return this._toAbsoluteMozilla(element,accountForDocScroll);
+
+      var x = 0;
+      var y = 0;
+      var parent = element;
+      while ( parent ) {
+
+         var borderXOffset = 0;
+         var borderYOffset = 0;
+         if ( parent != element ) {
+            var borderXOffset = parseInt(this.getElementsComputedStyle(parent, "borderLeftWidth" ));
+            var borderYOffset = parseInt(this.getElementsComputedStyle(parent, "borderTopWidth" ));
+            borderXOffset = isNaN(borderXOffset) ? 0 : borderXOffset;
+            borderYOffset = isNaN(borderYOffset) ? 0 : borderYOffset;
+         }
+
+         x += parent.offsetLeft - parent.scrollLeft + borderXOffset;
+         y += parent.offsetTop - parent.scrollTop + borderYOffset;
+         parent = parent.offsetParent;
+      }
+
+      if ( accountForDocScroll ) {
+         x -= this.docScrollLeft();
+         y -= this.docScrollTop();
+      }
+
+      return { x:x, y:y };
+   },
+
+   /**
+    *  Mozilla did not report all of the parents up the hierarchy via the
+    *  offsetParent property that IE did.  So for the calculation of the
+    *  offsets we use the offsetParent property, but for the calculation of
+    *  the scrollTop/scrollLeft adjustments we navigate up via the parentNode
+    *  property instead so as to get the scroll offsets...
+    *
+    **/
+   _toAbsoluteMozilla: function(element,accountForDocScroll) {
+      var x = 0;
+      var y = 0;
+      var parent = element;
+      while ( parent ) {
+         x += parent.offsetLeft;
+         y += parent.offsetTop;
+         parent = parent.offsetParent;
+      }
+
+      parent = element;
+      while ( parent &&
+              parent != document.body &&
+              parent != document.documentElement ) {
+         if ( parent.scrollLeft  )
+            x -= parent.scrollLeft;
+         if ( parent.scrollTop )
+            y -= parent.scrollTop;
+         parent = parent.parentNode;
+      }
+
+      if ( accountForDocScroll ) {
+         x -= this.docScrollLeft();
+         y -= this.docScrollTop();
+      }
+
+      return { x:x, y:y };
+   },
+
+   docScrollLeft: function() {
+      if ( window.pageXOffset )
+         return window.pageXOffset;
+      else if ( document.documentElement && document.documentElement.scrollLeft )
+         return document.documentElement.scrollLeft;
+      else if ( document.body )
+         return document.body.scrollLeft;
+      else
+         return 0;
+   },
+
+   docScrollTop: function() {
+      if ( window.pageYOffset )
+         return window.pageYOffset;
+      else if ( document.documentElement && document.documentElement.scrollTop )
+         return document.documentElement.scrollTop;
+      else if ( document.body )
+         return document.body.scrollTop;
+      else
+         return 0;
+   }
+
+};
diff --git a/web/static/js/scriptaculous/builder.js b/web/static/js/scriptaculous/builder.js
new file mode 100644
index 0000000..5e00f45
--- /dev/null
+++ b/web/static/js/scriptaculous/builder.js
@@ -0,0 +1,97 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// See scriptaculous.js for full license.
+
+var Builder = {
+  NODEMAP: {
+    AREA: 'map',
+    CAPTION: 'table',
+    COL: 'table',
+    COLGROUP: 'table',
+    LEGEND: 'fieldset',
+    OPTGROUP: 'select',
+    OPTION: 'select',
+    PARAM: 'object',
+    TBODY: 'table',
+    TD: 'table',
+    TFOOT: 'table',
+    TH: 'table',
+    THEAD: 'table',
+    TR: 'table'
+  },
+  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
+  //       due to a Firefox bug
+  node: function(elementName) {
+    elementName = elementName.toUpperCase();
+    
+    // try innerHTML approach
+    var parentTag = this.NODEMAP[elementName] || 'div';
+    var parentElement = document.createElement(parentTag);
+    parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
+    var element = parentElement.firstChild || null;
+      
+    // see if browser added wrapping tags
+    if(element && (element.tagName != elementName))
+      element = element.getElementsByTagName(elementName)[0];
+    
+    // fallback to createElement approach
+    if(!element) element = document.createElement(elementName);
+    
+    // abort if nothing could be created
+    if(!element) return;
+
+    // attributes (or text)
+    if(arguments[1])
+      if(this._isStringOrNumber(arguments[1]) ||
+        (arguments[1] instanceof Array)) {
+          this._children(element, arguments[1]);
+        } else {
+          var attrs = this._attributes(arguments[1]);
+          if(attrs.length) {
+            parentElement.innerHTML = "<" +elementName + " " +
+              attrs + "></" + elementName + ">";
+            element = parentElement.firstChild || null;
+            // workaround firefox 1.0.X bug
+            if(!element) {
+              element = document.createElement(elementName);
+              for(attr in arguments[1]) 
+                element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
+            }
+            if(element.tagName != elementName)
+              element = parentElement.getElementsByTagName(elementName)[0];
+            }
+        } 
+
+    // text, or array of children
+    if(arguments[2])
+      this._children(element, arguments[2]);
+
+     return element;
+  },
+  _text: function(text) {
+     return document.createTextNode(text);
+  },
+  _attributes: function(attributes) {
+    var attrs = [];
+    for(attribute in attributes)
+      attrs.push((attribute=='className' ? 'class' : attribute) +
+          '="' + attributes[attribute].toString().escapeHTML() + '"');
+    return attrs.join(" ");
+  },
+  _children: function(element, children) {
+    if(typeof children=='object') { // array can hold nodes and text
+      children.flatten().each( function(e) {
+        if(typeof e=='object')
+          element.appendChild(e)
+        else
+          if(Builder._isStringOrNumber(e))
+            element.appendChild(Builder._text(e));
+      });
+    } else
+      if(Builder._isStringOrNumber(children)) 
+         element.appendChild(Builder._text(children));
+  },
+  _isStringOrNumber: function(param) {
+    return(typeof param=='string' || typeof param=='number');
+  }
+}
\ No newline at end of file
diff --git a/web/static/js/scriptaculous/controls.js b/web/static/js/scriptaculous/controls.js
new file mode 100644
index 0000000..18bdf20
--- /dev/null
+++ b/web/static/js/scriptaculous/controls.js
@@ -0,0 +1,721 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+//  Richard Livsey
+//  Rahul Bhargava
+//  Rob Wills
+// 
+// See scriptaculous.js for full license.
+
+// Autocompleter.Base handles all the autocompletion functionality 
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least, 
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method 
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most 
+// useful when one of the tokens is \n (a newline), as it 
+// allows smart autocompletion after linebreaks.
+
+var Autocompleter = {}
+Autocompleter.Base = function() {};
+Autocompleter.Base.prototype = {
+  baseInitialize: function(element, update, options) {
+    this.element     = $(element); 
+    this.update      = $(update);  
+    this.hasFocus    = false; 
+    this.changed     = false; 
+    this.active      = false; 
+    this.index       = 0;     
+    this.entryCount  = 0;
+
+    if (this.setOptions)
+      this.setOptions(options);
+    else
+      this.options = options || {};
+
+    this.options.paramName    = this.options.paramName || this.element.name;
+    this.options.tokens       = this.options.tokens || [];
+    this.options.frequency    = this.options.frequency || 0.4;
+    this.options.minChars     = this.options.minChars || 1;
+    this.options.onShow       = this.options.onShow || 
+    function(element, update){ 
+      if(!update.style.position || update.style.position=='absolute') {
+        update.style.position = 'absolute';
+        Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
+      }
+      Effect.Appear(update,{duration:0.15});
+    };
+    this.options.onHide = this.options.onHide || 
+    function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+    if (typeof(this.options.tokens) == 'string') 
+      this.options.tokens = new Array(this.options.tokens);
+
+    this.observer = null;
+    
+    this.element.setAttribute('autocomplete','off');
+
+    Element.hide(this.update);
+
+    Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
+    Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
+  },
+
+  show: function() {
+    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+    if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && (Element.getStyle(this.update, 'position')=='absolute')) {
+      new Insertion.After(this.update, 
+       '<iframe id="' + this.update.id + '_iefix" '+
+       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+      this.iefix = $(this.update.id+'_iefix');
+    }
+    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+  },
+  
+  fixIEOverlapping: function() {
+    Position.clone(this.update, this.iefix);
+    this.iefix.style.zIndex = 1;
+    this.update.style.zIndex = 2;
+    Element.show(this.iefix);
+  },
+
+  hide: function() {
+    this.stopIndicator();
+    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+    if(this.iefix) Element.hide(this.iefix);
+  },
+
+  startIndicator: function() {
+    if(this.options.indicator) Element.show(this.options.indicator);
+  },
+
+  stopIndicator: function() {
+    if(this.options.indicator) Element.hide(this.options.indicator);
+  },
+
+  onKeyPress: function(event) {
+    if(this.active)
+      switch(event.keyCode) {
+       case Event.KEY_TAB:
+       case Event.KEY_RETURN:
+         this.selectEntry();
+         Event.stop(event);
+       case Event.KEY_ESC:
+         this.hide();
+         this.active = false;
+         Event.stop(event);
+         return;
+       case Event.KEY_LEFT:
+       case Event.KEY_RIGHT:
+         return;
+       case Event.KEY_UP:
+         this.markPrevious();
+         this.render();
+         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         return;
+       case Event.KEY_DOWN:
+         this.markNext();
+         this.render();
+         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         return;
+      }
+     else 
+      if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 
+        return;
+
+    this.changed = true;
+    this.hasFocus = true;
+
+    if(this.observer) clearTimeout(this.observer);
+      this.observer = 
+        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+  },
+
+  onHover: function(event) {
+    var element = Event.findElement(event, 'LI');
+    if(this.index != element.autocompleteIndex) 
+    {
+        this.index = element.autocompleteIndex;
+        this.render();
+    }
+    Event.stop(event);
+  },
+  
+  onClick: function(event) {
+    var element = Event.findElement(event, 'LI');
+    this.index = element.autocompleteIndex;
+    this.selectEntry();
+    this.hide();
+  },
+  
+  onBlur: function(event) {
+    // needed to make click events working
+    setTimeout(this.hide.bind(this), 250);
+    this.hasFocus = false;
+    this.active = false;     
+  }, 
+  
+  render: function() {
+    if(this.entryCount > 0) {
+      for (var i = 0; i < this.entryCount; i++)
+        this.index==i ? 
+          Element.addClassName(this.getEntry(i),"selected") : 
+          Element.removeClassName(this.getEntry(i),"selected");
+        
+      if(this.hasFocus) { 
+        this.show();
+        this.active = true;
+      }
+    } else {
+      this.active = false;
+      this.hide();
+    }
+  },
+  
+  markPrevious: function() {
+    if(this.index > 0) this.index--
+      else this.index = this.entryCount-1;
+  },
+  
+  markNext: function() {
+    if(this.index < this.entryCount-1) this.index++
+      else this.index = 0;
+  },
+  
+  getEntry: function(index) {
+    return this.update.firstChild.childNodes[index];
+  },
+  
+  getCurrentEntry: function() {
+    return this.getEntry(this.index);
+  },
+  
+  selectEntry: function() {
+    this.active = false;
+    this.updateElement(this.getCurrentEntry());
+  },
+
+  updateElement: function(selectedElement) {
+    if (this.options.updateElement) {
+      this.options.updateElement(selectedElement);
+      return;
+    }
+
+    var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+    var lastTokenPos = this.findLastToken();
+    if (lastTokenPos != -1) {
+      var newValue = this.element.value.substr(0, lastTokenPos + 1);
+      var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
+      if (whitespace)
+        newValue += whitespace[0];
+      this.element.value = newValue + value;
+    } else {
+      this.element.value = value;
+    }
+    this.element.focus();
+    
+    if (this.options.afterUpdateElement)
+      this.options.afterUpdateElement(this.element, selectedElement);
+  },
+
+  updateChoices: function(choices) {
+    if(!this.changed && this.hasFocus) {
+      this.update.innerHTML = choices;
+      Element.cleanWhitespace(this.update);
+      Element.cleanWhitespace(this.update.firstChild);
+
+      if(this.update.firstChild && this.update.firstChild.childNodes) {
+        this.entryCount = 
+          this.update.firstChild.childNodes.length;
+        for (var i = 0; i < this.entryCount; i++) {
+          var entry = this.getEntry(i);
+          entry.autocompleteIndex = i;
+          this.addObservers(entry);
+        }
+      } else { 
+        this.entryCount = 0;
+      }
+
+      this.stopIndicator();
+
+      this.index = 0;
+      this.render();
+    }
+  },
+
+  addObservers: function(element) {
+    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+  },
+
+  onObserverEvent: function() {
+    this.changed = false;   
+    if(this.getToken().length>=this.options.minChars) {
+      this.startIndicator();
+      this.getUpdatedChoices();
+    } else {
+      this.active = false;
+      this.hide();
+    }
+  },
+
+  getToken: function() {
+    var tokenPos = this.findLastToken();
+    if (tokenPos != -1)
+      var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
+    else
+      var ret = this.element.value;
+
+    return /\n/.test(ret) ? '' : ret;
+  },
+
+  findLastToken: function() {
+    var lastTokenPos = -1;
+
+    for (var i=0; i<this.options.tokens.length; i++) {
+      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
+      if (thisTokenPos > lastTokenPos)
+        lastTokenPos = thisTokenPos;
+    }
+    return lastTokenPos;
+  }
+}
+
+Ajax.Autocompleter = Class.create();
+Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
+  initialize: function(element, update, url, options) {
+	  this.baseInitialize(element, update, options);
+    this.options.asynchronous  = true;
+    this.options.onComplete    = this.onComplete.bind(this);
+    this.options.defaultParams = this.options.parameters || null;
+    this.url                   = url;
+  },
+
+  getUpdatedChoices: function() {
+    entry = encodeURIComponent(this.options.paramName) + '=' + 
+      encodeURIComponent(this.getToken());
+
+    this.options.parameters = this.options.callback ?
+      this.options.callback(this.element, entry) : entry;
+
+    if(this.options.defaultParams) 
+      this.options.parameters += '&' + this.options.defaultParams;
+
+    new Ajax.Request(this.url, this.options);
+  },
+
+  onComplete: function(request) {
+    this.updateChoices(request.responseText);
+  }
+
+});
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partialSearch - If false, the autocompleter will match entered
+//                    text only at the beginning of strings in the 
+//                    autocomplete array. Defaults to true, which will
+//                    match text at the beginning of any *word* in the
+//                    strings in the autocomplete array. If you want to
+//                    search anywhere in the string, additionally set
+//                    the option fullSearch to true (default: off).
+//
+// - fullSsearch - Search anywhere in autocomplete array strings.
+//
+// - partialChars - How many characters to enter before triggering
+//                   a partial match (unlike minChars, which defines
+//                   how many characters are required to do any match
+//                   at all). Defaults to 2.
+//
+// - ignoreCase - Whether to ignore case when autocompleting.
+//                 Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector' 
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create();
+Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
+  initialize: function(element, update, array, options) {
+    this.baseInitialize(element, update, options);
+    this.options.array = array;
+  },
+
+  getUpdatedChoices: function() {
+    this.updateChoices(this.options.selector(this));
+  },
+
+  setOptions: function(options) {
+    this.options = Object.extend({
+      choices: 10,
+      partialSearch: true,
+      partialChars: 2,
+      ignoreCase: true,
+      fullSearch: false,
+      selector: function(instance) {
+        var ret       = []; // Beginning matches
+        var partial   = []; // Inside matches
+        var entry     = instance.getToken();
+        var count     = 0;
+
+        for (var i = 0; i < instance.options.array.length &&  
+          ret.length < instance.options.choices ; i++) { 
+
+          var elem = instance.options.array[i];
+          var foundPos = instance.options.ignoreCase ? 
+            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
+            elem.indexOf(entry);
+
+          while (foundPos != -1) {
+            if (foundPos == 0 && elem.length != entry.length) { 
+              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
+                elem.substr(entry.length) + "</li>");
+              break;
+            } else if (entry.length >= instance.options.partialChars && 
+              instance.options.partialSearch && foundPos != -1) {
+              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+                  foundPos + entry.length) + "</li>");
+                break;
+              }
+            }
+
+            foundPos = instance.options.ignoreCase ? 
+              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
+              elem.indexOf(entry, foundPos + 1);
+
+          }
+        }
+        if (partial.length)
+          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
+        return "<ul>" + ret.join('') + "</ul>";
+      }
+    }, options || {});
+  }
+});
+
+// AJAX in-place editor
+//
+// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
+
+// Use this if you notice weird scrolling problems on some browsers,
+// the DOM might be a bit confused when this gets called so do this
+// waits 1 ms (with setTimeout) until it does the activation
+Field.scrollFreeActivate = function(field) {
+  setTimeout(function() {
+    Field.activate(field);
+  }, 1);
+}
+
+Ajax.InPlaceEditor = Class.create();
+Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
+Ajax.InPlaceEditor.prototype = {
+  initialize: function(element, url, options) {
+    this.url = url;
+    this.element = $(element);
+
+    this.options = Object.extend({
+      okText: "ok",
+      cancelText: "cancel",
+      savingText: "Saving...",
+      clickToEditText: "Click to edit",
+      okText: "ok",
+      rows: 1,
+      onComplete: function(transport, element) {
+        new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
+      },
+      onFailure: function(transport) {
+        alert("Error communicating with the server: " + transport.responseText.stripTags());
+      },
+      callback: function(form) {
+        return Form.serialize(form);
+      },
+      handleLineBreaks: true,
+      loadingText: 'Loading...',
+      savingClassName: 'inplaceeditor-saving',
+      loadingClassName: 'inplaceeditor-loading',
+      formClassName: 'inplaceeditor-form',
+      highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
+      highlightendcolor: "#FFFFFF",
+      externalControl:	null,
+      ajaxOptions: {}
+    }, options || {});
+
+    if(!this.options.formId && this.element.id) {
+      this.options.formId = this.element.id + "-inplaceeditor";
+      if ($(this.options.formId)) {
+        // there's already a form with that name, don't specify an id
+        this.options.formId = null;
+      }
+    }
+    
+    if (this.options.externalControl) {
+      this.options.externalControl = $(this.options.externalControl);
+    }
+    
+    this.originalBackground = Element.getStyle(this.element, 'background-color');
+    if (!this.originalBackground) {
+      this.originalBackground = "transparent";
+    }
+    
+    this.element.title = this.options.clickToEditText;
+    
+    this.onclickListener = this.enterEditMode.bindAsEventListener(this);
+    this.mouseoverListener = this.enterHover.bindAsEventListener(this);
+    this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
+    Event.observe(this.element, 'click', this.onclickListener);
+    Event.observe(this.element, 'mouseover', this.mouseoverListener);
+    Event.observe(this.element, 'mouseout', this.mouseoutListener);
+    if (this.options.externalControl) {
+      Event.observe(this.options.externalControl, 'click', this.onclickListener);
+      Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
+      Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
+    }
+  },
+  enterEditMode: function(evt) {
+    if (this.saving) return;
+    if (this.editing) return;
+    this.editing = true;
+    this.onEnterEditMode();
+    if (this.options.externalControl) {
+      Element.hide(this.options.externalControl);
+    }
+    Element.hide(this.element);
+    this.createForm();
+    this.element.parentNode.insertBefore(this.form, this.element);
+    Field.scrollFreeActivate(this.editField);
+    // stop the event to avoid a page refresh in Safari
+    if (evt) {
+      Event.stop(evt);
+    }
+    return false;
+  },
+  createForm: function() {
+    this.form = document.createElement("form");
+    this.form.id = this.options.formId;
+    Element.addClassName(this.form, this.options.formClassName)
+    this.form.onsubmit = this.onSubmit.bind(this);
+
+    this.createEditField();
+
+    if (this.options.textarea) {
+      var br = document.createElement("br");
+      this.form.appendChild(br);
+    }
+
+    okButton = document.createElement("input");
+    okButton.type = "submit";
+    okButton.value = this.options.okText;
+    this.form.appendChild(okButton);
+
+    cancelLink = document.createElement("a");
+    cancelLink.href = "#";
+    cancelLink.appendChild(document.createTextNode(this.options.cancelText));
+    cancelLink.onclick = this.onclickCancel.bind(this);
+    this.form.appendChild(cancelLink);
+  },
+  hasHTMLLineBreaks: function(string) {
+    if (!this.options.handleLineBreaks) return false;
+    return string.match(/<br/i) || string.match(/<p>/i);
+  },
+  convertHTMLLineBreaks: function(string) {
+    return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
+  },
+  createEditField: function() {
+    var text;
+    if(this.options.loadTextURL) {
+      text = this.options.loadingText;
+    } else {
+      text = this.getText();
+    }
+    
+    if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
+      this.options.textarea = false;
+      var textField = document.createElement("input");
+      textField.type = "text";
+      textField.name = "value";
+      textField.value = text;
+      textField.style.backgroundColor = this.options.highlightcolor;
+      var size = this.options.size || this.options.cols || 0;
+      if (size != 0) textField.size = size;
+      this.editField = textField;
+    } else {
+      this.options.textarea = true;
+      var textArea = document.createElement("textarea");
+      textArea.name = "value";
+      textArea.value = this.convertHTMLLineBreaks(text);
+      textArea.rows = this.options.rows;
+      textArea.cols = this.options.cols || 40;
+      this.editField = textArea;
+    }
+    
+    if(this.options.loadTextURL) {
+      this.loadExternalText();
+    }
+    this.form.appendChild(this.editField);
+  },
+  getText: function() {
+    return this.element.innerHTML;
+  },
+  loadExternalText: function() {
+    Element.addClassName(this.form, this.options.loadingClassName);
+    this.editField.disabled = true;
+    new Ajax.Request(
+      this.options.loadTextURL,
+      Object.extend({
+        asynchronous: true,
+        onComplete: this.onLoadedExternalText.bind(this)
+      }, this.options.ajaxOptions)
+    );
+  },
+  onLoadedExternalText: function(transport) {
+    Element.removeClassName(this.form, this.options.loadingClassName);
+    this.editField.disabled = false;
+    this.editField.value = transport.responseText.stripTags();
+  },
+  onclickCancel: function() {
+    this.onComplete();
+    this.leaveEditMode();
+    return false;
+  },
+  onFailure: function(transport) {
+    this.options.onFailure(transport);
+    if (this.oldInnerHTML) {
+      this.element.innerHTML = this.oldInnerHTML;
+      this.oldInnerHTML = null;
+    }
+    return false;
+  },
+  onSubmit: function() {
+    // onLoading resets these so we need to save them away for the Ajax call
+    var form = this.form;
+    var value = this.editField.value;
+    
+    // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
+    // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
+    // to be displayed indefinitely
+    this.onLoading();
+    
+    new Ajax.Updater(
+      { 
+        success: this.element,
+         // don't update on failure (this could be an option)
+        failure: null
+      },
+      this.url,
+      Object.extend({
+        parameters: this.options.callback(form, value),
+        onComplete: this.onComplete.bind(this),
+        onFailure: this.onFailure.bind(this)
+      }, this.options.ajaxOptions)
+    );
+    // stop the event to avoid a page refresh in Safari
+    if (arguments.length > 1) {
+      Event.stop(arguments[0]);
+    }
+    return false;
+  },
+  onLoading: function() {
+    this.saving = true;
+    this.removeForm();
+    this.leaveHover();
+    this.showSaving();
+  },
+  showSaving: function() {
+    this.oldInnerHTML = this.element.innerHTML;
+    this.element.innerHTML = this.options.savingText;
+    Element.addClassName(this.element, this.options.savingClassName);
+    this.element.style.backgroundColor = this.originalBackground;
+    Element.show(this.element);
+  },
+  removeForm: function() {
+    if(this.form) {
+      if (this.form.parentNode) Element.remove(this.form);
+      this.form = null;
+    }
+  },
+  enterHover: function() {
+    if (this.saving) return;
+    this.element.style.backgroundColor = this.options.highlightcolor;
+    if (this.effect) {
+      this.effect.cancel();
+    }
+    Element.addClassName(this.element, this.options.hoverClassName)
+  },
+  leaveHover: function() {
+    if (this.options.backgroundColor) {
+      this.element.style.backgroundColor = this.oldBackground;
+    }
+    Element.removeClassName(this.element, this.options.hoverClassName)
+    if (this.saving) return;
+    this.effect = new Effect.Highlight(this.element, {
+      startcolor: this.options.highlightcolor,
+      endcolor: this.options.highlightendcolor,
+      restorecolor: this.originalBackground
+    });
+  },
+  leaveEditMode: function() {
+    Element.removeClassName(this.element, this.options.savingClassName);
+    this.removeForm();
+    this.leaveHover();
+    this.element.style.backgroundColor = this.originalBackground;
+    Element.show(this.element);
+    if (this.options.externalControl) {
+      Element.show(this.options.externalControl);
+    }
+    this.editing = false;
+    this.saving = false;
+    this.oldInnerHTML = null;
+    this.onLeaveEditMode();
+  },
+  onComplete: function(transport) {
+    this.leaveEditMode();
+    this.options.onComplete.bind(this)(transport, this.element);
+  },
+  onEnterEditMode: function() {},
+  onLeaveEditMode: function() {},
+  dispose: function() {
+    if (this.oldInnerHTML) {
+      this.element.innerHTML = this.oldInnerHTML;
+    }
+    this.leaveEditMode();
+    Event.stopObserving(this.element, 'click', this.onclickListener);
+    Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
+    Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
+    if (this.options.externalControl) {
+      Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
+      Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
+      Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
+    }
+  }
+};
diff --git a/web/static/js/scriptaculous/dragdrop.js b/web/static/js/scriptaculous/dragdrop.js
new file mode 100644
index 0000000..7ca95f6
--- /dev/null
+++ b/web/static/js/scriptaculous/dragdrop.js
@@ -0,0 +1,519 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// 
+// Element.Class part Copyright (c) 2005 by Rick Olson
+// 
+// See scriptaculous.js for full license.
+
+/*--------------------------------------------------------------------------*/
+
+var Droppables = {
+  drops: [],
+
+  remove: function(element) {
+    this.drops = this.drops.reject(function(d) { return d.element==element });
+  },
+
+  add: function(element) {
+    element = $(element);
+    var options = Object.extend({
+      greedy:     true,
+      hoverclass: null  
+    }, arguments[1] || {});
+
+    // cache containers
+    if(options.containment) {
+      options._containers = [];
+      var containment = options.containment;
+      if((typeof containment == 'object') && 
+        (containment.constructor == Array)) {
+        containment.each( function(c) { options._containers.push($(c)) });
+      } else {
+        options._containers.push($(containment));
+      }
+    }
+
+    Element.makePositioned(element); // fix IE
+    options.element = element;
+
+    this.drops.push(options);
+  },
+
+  isContained: function(element, drop) {
+    var parentNode = element.parentNode;
+    return drop._containers.detect(function(c) { return parentNode == c });
+  },
+
+  isAffected: function(pX, pY, element, drop) {
+    return (
+      (drop.element!=element) &&
+      ((!drop._containers) ||
+        this.isContained(element, drop)) &&
+      ((!drop.accept) ||
+        (Element.Class.has_any(element, drop.accept))) &&
+      Position.within(drop.element, pX, pY) );
+  },
+
+  deactivate: function(drop) {
+    if(drop.hoverclass)
+      Element.Class.remove(drop.element, drop.hoverclass);
+    this.last_active = null;
+  },
+
+  activate: function(drop) {
+    if(this.last_active) this.deactivate(this.last_active);
+    if(drop.hoverclass)
+      Element.Class.add(drop.element, drop.hoverclass);
+    this.last_active = drop;
+  },
+
+  show: function(event, element) {
+    if(!this.drops.length) return;
+    var pX = Event.pointerX(event);
+    var pY = Event.pointerY(event);
+    Position.prepare();
+
+    var i = this.drops.length-1; do {
+      var drop = this.drops[i];
+      if(this.isAffected(pX, pY, element, drop)) {
+        if(drop.onHover)
+           drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+        if(drop.greedy) { 
+          this.activate(drop);
+          return;
+        }
+      }
+    } while (i--);
+    
+    if(this.last_active) this.deactivate(this.last_active);
+  },
+
+  fire: function(event, element) {
+    if(!this.last_active) return;
+    Position.prepare();
+
+    if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
+      if (this.last_active.onDrop) 
+        this.last_active.onDrop(element, this.last_active.element, event);
+  },
+
+  reset: function() {
+    if(this.last_active)
+      this.deactivate(this.last_active);
+  }
+}
+
+var Draggables = {
+  observers: [],
+  addObserver: function(observer) {
+    this.observers.push(observer);    
+  },
+  removeObserver: function(element) {  // element instead of obsever fixes mem leaks
+    this.observers = this.observers.reject( function(o) { return o.element==element });
+  },
+  notify: function(eventName, draggable) {  // 'onStart', 'onEnd'
+    this.observers.invoke(eventName, draggable);
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Draggable = Class.create();
+Draggable.prototype = {
+  initialize: function(element) {
+    var options = Object.extend({
+      handle: false,
+      starteffect: function(element) { 
+        new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); 
+      },
+      reverteffect: function(element, top_offset, left_offset) {
+        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
+        new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
+      },
+      endeffect: function(element) { 
+         new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); 
+      },
+      zindex: 1000,
+      revert: false
+    }, arguments[1] || {});
+
+    this.element      = $(element);
+    if(options.handle && (typeof options.handle == 'string'))
+      this.handle = Element.Class.childrenWith(this.element, options.handle)[0];
+      
+    if(!this.handle) this.handle = $(options.handle);
+    if(!this.handle) this.handle = this.element;
+
+    Element.makePositioned(this.element); // fix IE    
+
+    this.offsetX      = 0;
+    this.offsetY      = 0;
+    this.originalLeft = this.currentLeft();
+    this.originalTop  = this.currentTop();
+    this.originalX    = this.element.offsetLeft;
+    this.originalY    = this.element.offsetTop;
+
+    this.options      = options;
+
+    this.active       = false;
+    this.dragging     = false;   
+
+    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
+    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
+    this.eventMouseMove = this.update.bindAsEventListener(this);
+    this.eventKeypress  = this.keyPress.bindAsEventListener(this);
+    
+    this.registerEvents();
+  },
+  destroy: function() {
+    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
+    this.unregisterEvents();
+  },
+  registerEvents: function() {
+    Event.observe(document, "mouseup", this.eventMouseUp);
+    Event.observe(document, "mousemove", this.eventMouseMove);
+    Event.observe(document, "keypress", this.eventKeypress);
+    Event.observe(this.handle, "mousedown", this.eventMouseDown);
+  },
+  unregisterEvents: function() {
+    //if(!this.active) return;
+    //Event.stopObserving(document, "mouseup", this.eventMouseUp);
+    //Event.stopObserving(document, "mousemove", this.eventMouseMove);
+    //Event.stopObserving(document, "keypress", this.eventKeypress);
+  },
+  currentLeft: function() {
+    return parseInt(this.element.style.left || '0');
+  },
+  currentTop: function() {
+    return parseInt(this.element.style.top || '0')
+  },
+  startDrag: function(event) {
+    if(Event.isLeftClick(event)) {
+      
+      // abort on form elements, fixes a Firefox issue
+      var src = Event.element(event);
+      if(src.tagName && (
+        src.tagName=='INPUT' ||
+        src.tagName=='SELECT' ||
+        src.tagName=='BUTTON' ||
+        src.tagName=='TEXTAREA')) return;
+      
+      // this.registerEvents();
+      this.active = true;
+      var pointer = [Event.pointerX(event), Event.pointerY(event)];
+      var offsets = Position.cumulativeOffset(this.element);
+      this.offsetX =  (pointer[0] - offsets[0]);
+      this.offsetY =  (pointer[1] - offsets[1]);
+      Event.stop(event);
+    }
+  },
+  finishDrag: function(event, success) {
+    // this.unregisterEvents();
+
+    this.active = false;
+    this.dragging = false;
+
+    if(this.options.ghosting) {
+      Position.relativize(this.element);
+      Element.remove(this._clone);
+      this._clone = null;
+    }
+
+    if(success) Droppables.fire(event, this.element);
+    Draggables.notify('onEnd', this);
+
+    var revert = this.options.revert;
+    if(revert && typeof revert == 'function') revert = revert(this.element);
+
+    if(revert && this.options.reverteffect) {
+      this.options.reverteffect(this.element, 
+      this.currentTop()-this.originalTop,
+      this.currentLeft()-this.originalLeft);
+    } else {
+      this.originalLeft = this.currentLeft();
+      this.originalTop  = this.currentTop();
+    }
+
+    if(this.options.zindex)
+      this.element.style.zIndex = this.originalZ;
+
+    if(this.options.endeffect) 
+      this.options.endeffect(this.element);
+
+
+    Droppables.reset();
+  },
+  keyPress: function(event) {
+    if(this.active) {
+      if(event.keyCode==Event.KEY_ESC) {
+        this.finishDrag(event, false);
+        Event.stop(event);
+      }
+    }
+  },
+  endDrag: function(event) {
+    if(this.active && this.dragging) {
+      this.finishDrag(event, true);
+      Event.stop(event);
+    }
+    this.active = false;
+    this.dragging = false;
+  },
+  draw: function(event) {
+    var pointer = [Event.pointerX(event), Event.pointerY(event)];
+    var offsets = Position.cumulativeOffset(this.element);
+    offsets[0] -= this.currentLeft();
+    offsets[1] -= this.currentTop();
+    var style = this.element.style;
+    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+      style.left = (pointer[0] - offsets[0] - this.offsetX) + "px";
+    if((!this.options.constraint) || (this.options.constraint=='vertical'))
+      style.top  = (pointer[1] - offsets[1] - this.offsetY) + "px";
+    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
+  },
+  update: function(event) {
+   if(this.active) {
+      if(!this.dragging) {
+        var style = this.element.style;
+        this.dragging = true;
+        
+        if(Element.getStyle(this.element,'position')=='') 
+          style.position = "relative";
+        
+        if(this.options.zindex) {
+          this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+          style.zIndex = this.options.zindex;
+        }
+
+        if(this.options.ghosting) {
+          this._clone = this.element.cloneNode(true);
+          Position.absolutize(this.element);
+          this.element.parentNode.insertBefore(this._clone, this.element);
+        }
+
+        Draggables.notify('onStart', this);
+        if(this.options.starteffect) this.options.starteffect(this.element);
+      }
+
+      Droppables.show(event, this.element);
+      this.draw(event);
+      if(this.options.change) this.options.change(this);
+
+      // fix AppleWebKit rendering
+      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); 
+
+      Event.stop(event);
+   }
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var SortableObserver = Class.create();
+SortableObserver.prototype = {
+  initialize: function(element, observer) {
+    this.element   = $(element);
+    this.observer  = observer;
+    this.lastValue = Sortable.serialize(this.element);
+  },
+  onStart: function() {
+    this.lastValue = Sortable.serialize(this.element);
+  },
+  onEnd: function() {
+    Sortable.unmark();
+    if(this.lastValue != Sortable.serialize(this.element))
+      this.observer(this.element)
+  }
+}
+
+var Sortable = {
+  sortables: new Array(),
+  options: function(element){
+    element = $(element);
+    return this.sortables.detect(function(s) { return s.element == element });
+  },
+  destroy: function(element){
+    element = $(element);
+    this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
+      Draggables.removeObserver(s.element);
+      s.droppables.each(function(d){ Droppables.remove(d) });
+      s.draggables.invoke('destroy');
+    });
+    this.sortables = this.sortables.reject(function(s) { return s.element == element });
+  },
+  create: function(element) {
+    element = $(element);
+    var options = Object.extend({ 
+      element:     element,
+      tag:         'li',       // assumes li children, override with tag: 'tagname'
+      dropOnEmpty: false,
+      tree:        false,      // fixme: unimplemented
+      overlap:     'vertical', // one of 'vertical', 'horizontal'
+      constraint:  'vertical', // one of 'vertical', 'horizontal', false
+      containment: element,    // also takes array of elements (or id's); or false
+      handle:      false,      // or a CSS class
+      only:        false,
+      hoverclass:  null,
+      ghosting:    false,
+      format:      null,
+      onChange:    Prototype.emptyFunction,
+      onUpdate:    Prototype.emptyFunction
+    }, arguments[1] || {});
+
+    // clear any old sortable with same element
+    this.destroy(element);
+
+    // build options for the draggables
+    var options_for_draggable = {
+      revert:      true,
+      ghosting:    options.ghosting,
+      constraint:  options.constraint,
+      handle:      options.handle };
+
+    if(options.starteffect)
+      options_for_draggable.starteffect = options.starteffect;
+
+    if(options.reverteffect)
+      options_for_draggable.reverteffect = options.reverteffect;
+    else
+      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
+        element.style.top  = 0;
+        element.style.left = 0;
+      };
+
+    if(options.endeffect)
+      options_for_draggable.endeffect = options.endeffect;
+
+    if(options.zindex)
+      options_for_draggable.zindex = options.zindex;
+
+    // build options for the droppables  
+    var options_for_droppable = {
+      overlap:     options.overlap,
+      containment: options.containment,
+      hoverclass:  options.hoverclass,
+      onHover:     Sortable.onHover,
+      greedy:      !options.dropOnEmpty
+    }
+
+    // fix for gecko engine
+    Element.cleanWhitespace(element); 
+
+    options.draggables = [];
+    options.droppables = [];
+
+    // make it so
+
+    // drop on empty handling
+    if(options.dropOnEmpty) {
+      Droppables.add(element,
+        {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
+      options.droppables.push(element);
+    }
+
+    (this.findElements(element, options) || []).each( function(e) {
+      // handles are per-draggable
+      var handle = options.handle ? 
+        Element.Class.childrenWith(e, options.handle)[0] : e;    
+      options.draggables.push(
+        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
+      Droppables.add(e, options_for_droppable);
+      options.droppables.push(e);      
+    });
+
+    // keep reference
+    this.sortables.push(options);
+
+    // for onupdate
+    Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+  },
+
+  // return all suitable-for-sortable elements in a guaranteed order
+  findElements: function(element, options) {
+    if(!element.hasChildNodes()) return null;
+    var elements = [];
+    $A(element.childNodes).each( function(e) {
+      if(e.tagName && e.tagName==options.tag.toUpperCase() &&
+        (!options.only || (Element.Class.has(e, options.only))))
+          elements.push(e);
+      if(options.tree) {
+        var grandchildren = this.findElements(e, options);
+        if(grandchildren) elements.push(grandchildren);
+      }
+    });
+
+    return (elements.length>0 ? elements.flatten() : null);
+  },
+
+  onHover: function(element, dropon, overlap) {
+    if(overlap>0.5) {
+      Sortable.mark(dropon, 'before');
+      if(dropon.previousSibling != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = "hidden"; // fix gecko rendering
+        dropon.parentNode.insertBefore(element, dropon);
+        if(dropon.parentNode!=oldParentNode) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    } else {
+      Sortable.mark(dropon, 'after');
+      var nextElement = dropon.nextSibling || null;
+      if(nextElement != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = "hidden"; // fix gecko rendering
+        dropon.parentNode.insertBefore(element, nextElement);
+        if(dropon.parentNode!=oldParentNode) 
+          Sortable.options(oldParentNode).onChange(element);
+        Sortable.options(dropon.parentNode).onChange(element);
+      }
+    }
+  },
+
+  onEmptyHover: function(element, dropon) {
+    if(element.parentNode!=dropon) {
+      var oldParentNode = element.parentNode;
+      dropon.appendChild(element);
+      Sortable.options(oldParentNode).onChange(element);
+      Sortable.options(dropon).onChange(element);
+    }
+  },
+
+  unmark: function() {
+    if(Sortable._marker) Element.hide(Sortable._marker);
+  },
+
+  mark: function(dropon, position) {
+    // mark on ghosting only
+    var sortable = Sortable.options(dropon.parentNode);
+    if(sortable && !sortable.ghosting) return; 
+
+    if(!Sortable._marker) {
+      Sortable._marker = $('dropmarker') || document.createElement('DIV');
+      Element.hide(Sortable._marker);
+      Element.Class.add(Sortable._marker, 'dropmarker');
+      Sortable._marker.style.position = 'absolute';
+      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+    }    
+    var offsets = Position.cumulativeOffset(dropon);
+    Sortable._marker.style.top  = offsets[1] + 'px';
+    if(position=='after') Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
+    Sortable._marker.style.left = offsets[0] + 'px';
+    Element.show(Sortable._marker);
+  },
+
+  serialize: function(element) {
+    element = $(element);
+    var sortableOptions = this.options(element);
+    var options = Object.extend({
+      tag:  sortableOptions.tag,
+      only: sortableOptions.only,
+      name: element.id,
+      format: sortableOptions.format || /^[^_]*_(.*)$/
+    }, arguments[1] || {});
+    return $(this.findElements(element, options) || []).collect( function(item) {
+      return (encodeURIComponent(options.name) + "[]=" + 
+              encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
+    }).join("&");
+  }
+} 
\ No newline at end of file
diff --git a/web/static/js/scriptaculous/effects.js b/web/static/js/scriptaculous/effects.js
new file mode 100644
index 0000000..3f92992
--- /dev/null
+++ b/web/static/js/scriptaculous/effects.js
@@ -0,0 +1,992 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+//  Justin Palmer (http://encytemedia.com/)
+//  Mark Pilgrim (http://diveintomark.org/)
+//  Martin Bialasinki
+// 
+// See scriptaculous.js for full license.  
+
+/* ------------- element ext -------------- */  
+ 
+// converts rgb() and #xxx to #xxxxxx format,  
+// returns self (or first argument) if not convertable  
+String.prototype.parseColor = function() {  
+  color = "#";  
+  if(this.slice(0,4) == "rgb(") {  
+    var cols = this.slice(4,this.length-1).split(',');  
+    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);  
+  } else {  
+    if(this.slice(0,1) == '#') {  
+      if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();  
+      if(this.length==7) color = this.toLowerCase();  
+    }  
+  }  
+  return(color.length==7 ? color : (arguments[0] || this));  
+}  
+
+Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {  
+  var children = $(element).childNodes;  
+  var text     = "";  
+  var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i");  
+ 
+  for (var i = 0; i < children.length; i++) {  
+    if(children[i].nodeType==3) {  
+      text+=children[i].nodeValue;  
+    } else {  
+      if((!children[i].className.match(classtest)) && children[i].hasChildNodes())  
+        text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass);  
+    }  
+  }  
+ 
+  return text;
+}
+
+Element.setContentZoom = function(element, percent) {  
+  element = $(element);  
+  element.style.fontSize = (percent/100) + "em";   
+  if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);  
+}
+
+Element.getOpacity = function(element){  
+  var opacity;  
+  if (opacity = Element.getStyle(element, "opacity"))  
+    return parseFloat(opacity);  
+  if (opacity = (Element.getStyle(element, "filter") || '').match(/alpha\(opacity=(.*)\)/))  
+    if(opacity[1]) return parseFloat(opacity[1]) / 100;  
+  return 1.0;  
+}
+
+Element.setOpacity = function(element, value){  
+  element= $(element);  
+  var els = element.style;  
+  if (value == 1){  
+    els.opacity = '0.999999';  
+    if(/MSIE/.test(navigator.userAgent))  
+      els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'');  
+  } else {  
+    if(value < 0.00001) value = 0;  
+    els.opacity = value;  
+    if(/MSIE/.test(navigator.userAgent))  
+      els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +  
+        "alpha(opacity="+value*100+")";  
+  }   
+}  
+ 
+Element.getInlineOpacity = function(element){  
+  element= $(element);  
+  var op;  
+  op = element.style.opacity;  
+  if (typeof op != "undefined" && op != "") return op;  
+  return "";  
+}  
+ 
+Element.setInlineOpacity = function(element, value){  
+  element= $(element);  
+  var els = element.style;  
+  els.opacity = value;  
+}  
+ 
+/*--------------------------------------------------------------------------*/  
+ 
+Element.Class = {  
+    // Element.toggleClass(element, className) toggles the class being on/off  
+    // Element.toggleClass(element, className1, className2) toggles between both classes,  
+    //   defaulting to className1 if neither exist  
+    toggle: function(element, className) {  
+      if(Element.Class.has(element, className)) {  
+        Element.Class.remove(element, className);  
+        if(arguments.length == 3) Element.Class.add(element, arguments[2]);  
+      } else {  
+        Element.Class.add(element, className);  
+        if(arguments.length == 3) Element.Class.remove(element, arguments[2]);  
+      }  
+    },  
+ 
+    // gets space-delimited classnames of an element as an array  
+    get: function(element) {  
+      return $(element).className.split(' ');  
+    },  
+ 
+    // functions adapted from original functions by Gavin Kistner  
+    remove: function(element) {  
+      element = $(element);  
+      var removeClasses = arguments;  
+      $R(1,arguments.length-1).each( function(index) {  
+        element.className =  
+          element.className.split(' ').reject(  
+            function(klass) { return (klass == removeClasses[index]) } ).join(' ');  
+      });  
+    },  
+ 
+    add: function(element) {  
+      element = $(element);  
+      for(var i = 1; i < arguments.length; i++) {  
+        Element.Class.remove(element, arguments[i]);  
+        element.className += (element.className.length > 0 ? ' ' : '') + arguments[i];  
+      }  
+    },  
+ 
+    // returns true if all given classes exist in said element  
+    has: function(element) {  
+      element = $(element);  
+      if(!element || !element.className) return false;  
+      var regEx;  
+      for(var i = 1; i < arguments.length; i++) {  
+        if((typeof arguments[i] == 'object') &&  
+          (arguments[i].constructor == Array)) {  
+          for(var j = 0; j < arguments[i].length; j++) {  
+            regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)");  
+            if(!regEx.test(element.className)) return false;  
+          }  
+        } else {  
+          regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)");  
+          if(!regEx.test(element.className)) return false;  
+        }  
+      }  
+      return true;  
+    },  
+ 
+    // expects arrays of strings and/or strings as optional paramters  
+    // Element.Class.has_any(element, ['classA','classB','classC'], 'classD')  
+    has_any: function(element) {  
+      element = $(element);  
+      if(!element || !element.className) return false;  
+      var regEx;  
+      for(var i = 1; i < arguments.length; i++) {  
+        if((typeof arguments[i] == 'object') &&  
+          (arguments[i].constructor == Array)) {  
+          for(var j = 0; j < arguments[i].length; j++) {  
+            regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)");  
+            if(regEx.test(element.className)) return true;  
+          }  
+        } else {  
+          regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)");  
+          if(regEx.test(element.className)) return true;  
+        }  
+      }  
+      return false;  
+    },  
+ 
+    childrenWith: function(element, className) {  
+      var children = $(element).getElementsByTagName('*');  
+      var elements = new Array();  
+ 
+      for (var i = 0; i < children.length; i++)  
+        if (Element.Class.has(children[i], className))  
+          elements.push(children[i]);  
+ 
+      return elements;  
+    }  
+}  
+ 
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+  tagifyText: function(element) {
+    var tagifyStyle = "position:relative";
+    if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ";zoom:1";
+    element = $(element);
+    $A(element.childNodes).each( function(child) {
+      if(child.nodeType==3) {
+        child.nodeValue.toArray().each( function(character) {
+          element.insertBefore(
+            Builder.node('span',{style: tagifyStyle},
+              character == " " ? String.fromCharCode(160) : character), 
+              child);
+        });
+        Element.remove(child);
+      }
+    });
+  },
+  multiple: function(element, effect) {
+    var elements;
+    if(((typeof element == 'object') || 
+        (typeof element == 'function')) && 
+       (element.length))
+      elements = element;
+    else
+      elements = $(element).childNodes;
+      
+    var options = Object.extend({
+      speed: 0.1,
+      delay: 0.0
+    }, arguments[2] || {});
+    var speed = options.speed;
+    var delay = options.delay;
+
+    $A(elements).each( function(element, index) {
+      new effect(element, Object.extend(options, { delay: delay + index * speed }));
+    });
+  }
+};
+
+var Effect2 = Effect; // deprecated
+
+/* ------------- transitions ------------- */
+
+Effect.Transitions = {}
+
+Effect.Transitions.linear = function(pos) {
+  return pos;
+}
+Effect.Transitions.sinoidal = function(pos) {
+  return (-Math.cos(pos*Math.PI)/2) + 0.5;
+}
+Effect.Transitions.reverse  = function(pos) {
+  return 1-pos;
+}
+Effect.Transitions.flicker = function(pos) {
+  return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
+}
+Effect.Transitions.wobble = function(pos) {
+  return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+}
+Effect.Transitions.pulse = function(pos) {
+  return (Math.floor(pos*10) % 2 == 0 ? 
+    (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
+}
+Effect.Transitions.none = function(pos) {
+  return 0;
+}
+Effect.Transitions.full = function(pos) {
+  return 1;
+}
+
+/* ------------- core effects ------------- */
+
+Effect.Queue = {
+  effects:  [],
+  _each: function(iterator) {
+    this.effects._each(iterator);
+  },
+  interval: null,
+  add: function(effect) {
+    var timestamp = new Date().getTime();
+    
+    switch(effect.options.queue) {
+      case 'front':
+        // move unstarted effects after this effect  
+        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
+            e.startOn  += effect.finishOn;
+            e.finishOn += effect.finishOn;
+          });
+        break;
+      case 'end':
+        // start effect after last queued effect has finished
+        timestamp = this.effects.pluck('finishOn').max() || timestamp;
+        break;
+    }
+    
+    effect.startOn  += timestamp;
+    effect.finishOn += timestamp;
+    this.effects.push(effect);
+    if(!this.interval) 
+      this.interval = setInterval(this.loop.bind(this), 40);
+  },
+  remove: function(effect) {
+    this.effects = this.effects.reject(function(e) { return e==effect });
+    if(this.effects.length == 0) {
+      clearInterval(this.interval);
+      this.interval = null;
+    }
+  },
+  loop: function() {
+    var timePos = new Date().getTime();
+    this.effects.invoke('loop', timePos);
+  }
+}
+Object.extend(Effect.Queue, Enumerable);
+
+Effect.Base = function() {};
+Effect.Base.prototype = {
+  position: null,
+  setOptions: function(options) {
+    this.options = Object.extend({
+      transition: Effect.Transitions.sinoidal,
+      duration:   1.0,   // seconds
+      fps:        25.0,  // max. 25fps due to Effect.Queue implementation
+      sync:       false, // true for combining
+      from:       0.0,
+      to:         1.0,
+      delay:      0.0,
+      queue:      'parallel'
+    }, options || {});
+  },
+  start: function(options) {
+    this.setOptions(options || {});
+    this.currentFrame = 0;
+    this.state        = 'idle';
+    this.startOn      = this.options.delay*1000;
+    this.finishOn     = this.startOn + (this.options.duration*1000);
+    this.event('beforeStart');
+    if(!this.options.sync) Effect.Queue.add(this);
+  },
+  loop: function(timePos) {
+    if(timePos >= this.startOn) {
+      if(timePos >= this.finishOn) {
+        this.render(1.0);
+        this.cancel();
+        this.event('beforeFinish');
+        if(this.finish) this.finish(); 
+        this.event('afterFinish');
+        return;  
+      }
+      var pos   = (timePos - this.startOn) / (this.finishOn - this.startOn);
+      var frame = Math.round(pos * this.options.fps * this.options.duration);
+      if(frame > this.currentFrame) {
+        this.render(pos);
+        this.currentFrame = frame;
+      }
+    }
+  },
+  render: function(pos) {
+    if(this.state == 'idle') {
+      this.state = 'running';
+      this.event('beforeSetup');
+      if(this.setup) this.setup();
+      this.event('afterSetup');
+    }
+    if(this.options.transition) pos = this.options.transition(pos);
+    pos *= (this.options.to-this.options.from);
+    pos += this.options.from;
+    this.position = pos;
+    this.event('beforeUpdate');
+    if(this.update) this.update(pos);
+    this.event('afterUpdate');
+  },
+  cancel: function() {
+    if(!this.options.sync) Effect.Queue.remove(this);
+    this.state = 'finished';
+  },
+  event: function(eventName) {
+    if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
+    if(this.options[eventName]) this.options[eventName](this);
+  }
+}
+
+Effect.Parallel = Class.create();
+Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
+  initialize: function(effects) {
+    this.effects = effects || [];
+    this.start(arguments[1]);
+  },
+  update: function(position) {
+    this.effects.invoke('render', position);
+  },
+  finish: function(position) {
+    this.effects.each( function(effect) {
+      effect.render(1.0);
+      effect.cancel();
+      effect.event('beforeFinish');
+      if(effect.finish) effect.finish(position);
+      effect.event('afterFinish');
+    });
+  }
+});
+
+Effect.Opacity = Class.create();
+Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    // make this work on IE on elements without 'layout'
+    if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout))
+      this.element.style.zoom = 1;
+    var options = Object.extend({
+      from: Element.getOpacity(this.element) || 0.0,
+      to:   1.0
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  update: function(position) {
+    Element.setOpacity(this.element, position);
+  }
+});
+
+Effect.MoveBy = Class.create();
+Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), {
+  initialize: function(element, toTop, toLeft) {
+    this.element      = $(element);
+    this.toTop        = toTop;
+    this.toLeft       = toLeft;
+    this.start(arguments[3]);
+  },
+  setup: function() {
+    // Bug in Opera: Opera returns the "real" position of a static element or
+    // relative element that does not have top/left explicitly set.
+    // ==> Always set top and left for position relative elements in your stylesheets 
+    // (to 0 if you do not need them)
+    
+    Element.makePositioned(this.element);
+    this.originalTop  = parseFloat(Element.getStyle(this.element,'top')  || '0');
+    this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0');
+  },
+  update: function(position) {
+    var topd  = this.toTop  * position + this.originalTop;
+    var leftd = this.toLeft * position + this.originalLeft;
+    this.setPosition(topd, leftd);
+  },
+  setPosition: function(topd, leftd) {
+    this.element.style.top  = topd  + "px";
+    this.element.style.left = leftd + "px";
+  }
+});
+
+Effect.Scale = Class.create();
+Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
+  initialize: function(element, percent) {
+    this.element = $(element)
+    var options = Object.extend({
+      scaleX: true,
+      scaleY: true,
+      scaleContent: true,
+      scaleFromCenter: false,
+      scaleMode: 'box',        // 'box' or 'contents' or {} with provided values
+      scaleFrom: 100.0,
+      scaleTo:   percent
+    }, arguments[2] || {});
+    this.start(options);
+  },
+  setup: function() {
+    var effect = this;
+    
+    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+    this.elementPositioning = Element.getStyle(this.element,'position');
+    
+    effect.originalStyle = {};
+    ['top','left','width','height','fontSize'].each( function(k) {
+      effect.originalStyle[k] = effect.element.style[k];
+    });
+      
+    this.originalTop  = this.element.offsetTop;
+    this.originalLeft = this.element.offsetLeft;
+    
+    var fontSize = Element.getStyle(this.element,'font-size') || "100%";
+    ['em','px','%'].each( function(fontSizeType) {
+      if(fontSize.indexOf(fontSizeType)>0) {
+        effect.fontSize     = parseFloat(fontSize);
+        effect.fontSizeType = fontSizeType;
+      }
+    });
+    
+    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+    
+    this.dims = null;
+    if(this.options.scaleMode=='box')
+      this.dims = [this.element.clientHeight, this.element.clientWidth];
+    if(this.options.scaleMode=='content')
+      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
+    if(!this.dims)
+      this.dims = [this.options.scaleMode.originalHeight,
+                   this.options.scaleMode.originalWidth];
+  },
+  update: function(position) {
+    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
+    if(this.options.scaleContent && this.fontSize)
+      this.element.style.fontSize = this.fontSize*currentScale + this.fontSizeType;
+    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+  },
+  finish: function(position) {
+    if (this.restoreAfterFinish) {
+      var effect = this;
+      ['top','left','width','height','fontSize'].each( function(k) {
+        effect.element.style[k] = effect.originalStyle[k];
+      });
+    }
+  },
+  setDimensions: function(height, width) {
+    var els = this.element.style;
+    if(this.options.scaleX) els.width = width + 'px';
+    if(this.options.scaleY) els.height = height + 'px';
+    if(this.options.scaleFromCenter) {
+      var topd  = (height - this.dims[0])/2;
+      var leftd = (width  - this.dims[1])/2;
+      if(this.elementPositioning == 'absolute') {
+        if(this.options.scaleY) els.top = this.originalTop-topd + "px";
+        if(this.options.scaleX) els.left = this.originalLeft-leftd + "px";
+      } else {
+        if(this.options.scaleY) els.top = -topd + "px";
+        if(this.options.scaleX) els.left = -leftd + "px";
+      }
+    }
+  }
+});
+
+Effect.Highlight = Class.create();
+Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    var options = Object.extend({
+      startcolor:   "#ffff99"
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  setup: function() {
+    // Prevent executing on elements not in the layout flow
+    if(this.element.style.display=='none') { this.cancel(); return; }
+    // Disable background image during the effect
+    this.oldBgImage = this.element.style.backgroundImage;
+    this.element.style.backgroundImage = "none";
+    if(!this.options.endcolor)
+      this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff');
+    if (typeof this.options.restorecolor == "undefined")
+      this.options.restorecolor = this.element.style.backgroundColor;
+    // init color calculations
+    this.colors_base = [
+      parseInt(this.options.startcolor.slice(1,3),16),
+      parseInt(this.options.startcolor.slice(3,5),16),
+      parseInt(this.options.startcolor.slice(5),16) ];
+    this.colors_delta = [
+      parseInt(this.options.endcolor.slice(1,3),16)-this.colors_base[0],
+      parseInt(this.options.endcolor.slice(3,5),16)-this.colors_base[1],
+      parseInt(this.options.endcolor.slice(5),16)-this.colors_base[2]];
+  },
+  update: function(position) {
+    var effect = this; var colors = $R(0,2).map( function(i){ 
+      return Math.round(effect.colors_base[i]+(effect.colors_delta[i]*position))
+    });
+    this.element.style.backgroundColor = "#" +
+      colors[0].toColorPart() + colors[1].toColorPart() + colors[2].toColorPart();
+  },
+  finish: function() {
+    this.element.style.backgroundColor = this.options.restorecolor;
+    this.element.style.backgroundImage = this.oldBgImage;
+  }
+});
+
+Effect.ScrollTo = Class.create();
+Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    this.start(arguments[1] || {});
+  },
+  setup: function() {
+    Position.prepare();
+    var offsets = Position.cumulativeOffset(this.element);
+    var max = window.innerHeight ? 
+      window.height - window.innerHeight :
+      document.body.scrollHeight - 
+        (document.documentElement.clientHeight ? 
+          document.documentElement.clientHeight : document.body.clientHeight);
+    this.scrollStart = Position.deltaY;
+    this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
+  },
+  update: function(position) {
+    Position.prepare();
+    window.scrollTo(Position.deltaX, 
+      this.scrollStart + (position*this.delta));
+  }
+});
+
+/* ------------- combination effects ------------- */
+
+Effect.Fade = function(element) {
+  var oldOpacity = Element.getInlineOpacity(element);
+  var options = Object.extend({
+  from: Element.getOpacity(element) || 1.0,
+  to:   0.0,
+  afterFinishInternal: function(effect) 
+    { if (effect.options.to == 0) {
+        Element.hide(effect.element);
+        Element.setInlineOpacity(effect.element, oldOpacity);
+      }  
+    }
+  }, arguments[1] || {});
+  return new Effect.Opacity(element,options);
+}
+
+Effect.Appear = function(element) {
+  var options = Object.extend({
+  from: (Element.getStyle(element, "display") == "none" ? 0.0 : Element.getOpacity(element) || 0.0),
+  to:   1.0,
+  beforeSetup: function(effect)  
+    { Element.setOpacity(effect.element, effect.options.from);
+      Element.show(effect.element); }
+  }, arguments[1] || {});
+  return new Effect.Opacity(element,options);
+}
+
+Effect.Puff = function(element) {
+  element = $(element);
+  var oldOpacity = Element.getInlineOpacity(element);
+  var oldPosition = element.style.position;
+  return new Effect.Parallel(
+   [ new Effect.Scale(element, 200, 
+      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 
+     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 
+     Object.extend({ duration: 1.0, 
+      beforeSetupInternal: function(effect) 
+       { effect.effects[0].element.style.position = 'absolute'; },
+      afterFinishInternal: function(effect)
+       { Element.hide(effect.effects[0].element);
+         effect.effects[0].element.style.position = oldPosition;
+         Element.setInlineOpacity(effect.effects[0].element, oldOpacity); }
+     }, arguments[1] || {})
+   );
+}
+
+Effect.BlindUp = function(element) {
+  element = $(element);
+  Element.makeClipping(element);
+  return new Effect.Scale(element, 0, 
+    Object.extend({ scaleContent: false, 
+      scaleX: false, 
+      restoreAfterFinish: true,
+      afterFinishInternal: function(effect)
+        { 
+          Element.hide(effect.element);
+          Element.undoClipping(effect.element);
+        } 
+    }, arguments[1] || {})
+  );
+}
+
+Effect.BlindDown = function(element) {
+  element = $(element);
+  var oldHeight = element.style.height;
+  var elementDimensions = Element.getDimensions(element);
+  return new Effect.Scale(element, 100, 
+    Object.extend({ scaleContent: false, 
+      scaleX: false,
+      scaleFrom: 0,
+      scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+      restoreAfterFinish: true,
+      afterSetup: function(effect) {
+        Element.makeClipping(effect.element);
+        effect.element.style.height = "0px";
+        Element.show(effect.element); 
+      },  
+      afterFinishInternal: function(effect) {
+        Element.undoClipping(effect.element);
+        effect.element.style.height = oldHeight;
+      }
+    }, arguments[1] || {})
+  );
+}
+
+Effect.SwitchOff = function(element) {
+  element = $(element);
+  var oldOpacity = Element.getInlineOpacity(element);
+  return new Effect.Appear(element, { 
+    duration: 0.4,
+    from: 0,
+    transition: Effect.Transitions.flicker,
+    afterFinishInternal: function(effect) {
+      new Effect.Scale(effect.element, 1, { 
+        duration: 0.3, scaleFromCenter: true,
+        scaleX: false, scaleContent: false, restoreAfterFinish: true,
+        beforeSetup: function(effect) { 
+          Element.makePositioned(effect.element); 
+          Element.makeClipping(effect.element);
+        },
+        afterFinishInternal: function(effect) { 
+          Element.hide(effect.element); 
+          Element.undoClipping(effect.element);
+          Element.undoPositioned(effect.element);
+          Element.setInlineOpacity(effect.element, oldOpacity);
+        }
+      })
+    }
+  });
+}
+
+Effect.DropOut = function(element) {
+  element = $(element);
+  var oldTop = element.style.top;
+  var oldLeft = element.style.left;
+  var oldOpacity = Element.getInlineOpacity(element);
+  return new Effect.Parallel(
+    [ new Effect.MoveBy(element, 100, 0, { sync: true }), 
+      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+    Object.extend(
+      { duration: 0.5,
+        beforeSetup: function(effect) { 
+          Element.makePositioned(effect.effects[0].element); },
+        afterFinishInternal: function(effect) { 
+          Element.hide(effect.effects[0].element); 
+          Element.undoPositioned(effect.effects[0].element);
+          effect.effects[0].element.style.left = oldLeft;
+          effect.effects[0].element.style.top = oldTop;
+          Element.setInlineOpacity(effect.effects[0].element, oldOpacity); } 
+      }, arguments[1] || {}));
+}
+
+Effect.Shake = function(element) {
+  element = $(element);
+  var oldTop = element.style.top;
+  var oldLeft = element.style.left;
+  return new Effect.MoveBy(element, 0, 20, 
+    { duration: 0.05, afterFinishInternal: function(effect) {
+  new Effect.MoveBy(effect.element, 0, -40, 
+    { duration: 0.1, afterFinishInternal: function(effect) {
+  new Effect.MoveBy(effect.element, 0, 40, 
+    { duration: 0.1, afterFinishInternal: function(effect) {
+  new Effect.MoveBy(effect.element, 0, -40, 
+    { duration: 0.1, afterFinishInternal: function(effect) {
+  new Effect.MoveBy(effect.element, 0, 40, 
+    { duration: 0.1, afterFinishInternal: function(effect) {
+  new Effect.MoveBy(effect.element, 0, -20, 
+    { duration: 0.05, afterFinishInternal: function(effect) {
+        Element.undoPositioned(effect.element);
+        effect.element.style.left = oldLeft;
+        effect.element.style.top = oldTop;
+  }}) }}) }}) }}) }}) }});
+}
+
+Effect.SlideDown = function(element) {
+  element = $(element);
+  Element.cleanWhitespace(element);
+  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+  var oldInnerBottom = element.firstChild.style.bottom;
+  var elementDimensions = Element.getDimensions(element);
+  return new Effect.Scale(element, 100, 
+   Object.extend({ scaleContent: false, 
+    scaleX: false, 
+    scaleFrom: 0,
+    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},    
+    restoreAfterFinish: true,
+    afterSetup: function(effect) {
+      Element.makePositioned(effect.element.firstChild);
+      if (window.opera) effect.element.firstChild.style.top = "";
+      Element.makeClipping(effect.element);
+      element.style.height = '0';
+      Element.show(element); 
+    },  
+    afterUpdateInternal: function(effect) { 
+      effect.element.firstChild.style.bottom = 
+        (effect.dims[0] - effect.element.clientHeight) + 'px'; },
+    afterFinishInternal: function(effect) { 
+      Element.undoClipping(effect.element); 
+      Element.undoPositioned(effect.element.firstChild);
+      effect.element.firstChild.style.bottom = oldInnerBottom; }
+    }, arguments[1] || {})
+  );
+}
+  
+Effect.SlideUp = function(element) {
+  element = $(element);
+  Element.cleanWhitespace(element);
+  var oldInnerBottom = element.firstChild.style.bottom;
+  return new Effect.Scale(element, 0, 
+   Object.extend({ scaleContent: false, 
+    scaleX: false, 
+    scaleMode: 'box',
+    scaleFrom: 100,
+    restoreAfterFinish: true,
+    beforeStartInternal: function(effect) { 
+      Element.makePositioned(effect.element.firstChild);
+      if (window.opera) effect.element.firstChild.style.top = "";
+      Element.makeClipping(effect.element);
+      Element.show(element); 
+    },  
+    afterUpdateInternal: function(effect) { 
+     effect.element.firstChild.style.bottom = 
+       (effect.dims[0] - effect.element.clientHeight) + 'px'; },
+    afterFinishInternal: function(effect) { 
+        Element.hide(effect.element);
+        Element.undoClipping(effect.element); 
+        Element.undoPositioned(effect.element.firstChild);
+        effect.element.firstChild.style.bottom = oldInnerBottom; }
+   }, arguments[1] || {})
+  );
+}
+
+Effect.Squish = function(element) {
+  // Bug in opera makes the TD containing this element expand for a instance after finish 
+  return new Effect.Scale(element, window.opera ? 1 : 0, 
+    { restoreAfterFinish: true,
+      beforeSetup: function(effect) { 
+        Element.makeClipping(effect.element); },  
+      afterFinishInternal: function(effect) { 
+        Element.hide(effect.element); 
+        Element.undoClipping(effect.element); } 
+  });
+}
+
+Effect.Grow = function(element) {
+  element = $(element);
+  var options = arguments[1] || {};
+  
+  var elementDimensions = Element.getDimensions(element);
+  var originalWidth = elementDimensions.width;
+  var originalHeight = elementDimensions.height;
+  var oldTop = element.style.top;
+  var oldLeft = element.style.left;
+  var oldHeight = element.style.height;
+  var oldWidth = element.style.width;
+  var oldOpacity = Element.getInlineOpacity(element);
+  
+  var direction = options.direction || 'center';
+  var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
+  var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
+  var opacityTransition = options.opacityTransition || Effect.Transitions.full;
+  
+  var initialMoveX, initialMoveY;
+  var moveX, moveY;
+  
+  switch (direction) {
+    case 'top-left':
+      initialMoveX = initialMoveY = moveX = moveY = 0; 
+      break;
+    case 'top-right':
+      initialMoveX = originalWidth;
+      initialMoveY = moveY = 0;
+      moveX = -originalWidth;
+      break;
+    case 'bottom-left':
+      initialMoveX = moveX = 0;
+      initialMoveY = originalHeight;
+      moveY = -originalHeight;
+      break;
+    case 'bottom-right':
+      initialMoveX = originalWidth;
+      initialMoveY = originalHeight;
+      moveX = -originalWidth;
+      moveY = -originalHeight;
+      break;
+    case 'center':
+      initialMoveX = originalWidth / 2;
+      initialMoveY = originalHeight / 2;
+      moveX = -originalWidth / 2;
+      moveY = -originalHeight / 2;
+      break;
+  }
+  
+  return new Effect.MoveBy(element, initialMoveY, initialMoveX, { 
+    duration: 0.01, 
+    beforeSetup: function(effect) { 
+      Element.hide(effect.element);
+      Element.makeClipping(effect.element);
+      Element.makePositioned(effect.element);
+    },
+    afterFinishInternal: function(effect) {
+      new Effect.Parallel(
+        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }),
+          new Effect.MoveBy(effect.element, moveY, moveX, { sync: true, transition: moveTransition }),
+          new Effect.Scale(effect.element, 100, {
+            scaleMode: { originalHeight: originalHeight, originalWidth: originalWidth }, 
+            sync: true, scaleFrom: window.opera ? 1 : 0, transition: scaleTransition, restoreAfterFinish: true})
+        ], Object.extend({
+             beforeSetup: function(effect) {
+              effect.effects[0].element.style.height = 0;
+              Element.show(effect.effects[0].element);
+             },              
+             afterFinishInternal: function(effect) {
+               var el = effect.effects[0].element;
+               var els = el.style;
+               Element.undoClipping(el); 
+               Element.undoPositioned(el);
+               els.top = oldTop;
+               els.left = oldLeft;
+               els.height = oldHeight;
+               els.width = originalWidth + 'px';
+               Element.setInlineOpacity(el, oldOpacity);
+             }
+           }, options)
+      )
+    }
+  });
+}
+
+Effect.Shrink = function(element) {
+  element = $(element);
+  var options = arguments[1] || {};
+  
+  var originalWidth = element.clientWidth;
+  var originalHeight = element.clientHeight;
+  var oldTop = element.style.top;
+  var oldLeft = element.style.left;
+  var oldHeight = element.style.height;
+  var oldWidth = element.style.width;
+  var oldOpacity = Element.getInlineOpacity(element);
+
+  var direction = options.direction || 'center';
+  var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
+  var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
+  var opacityTransition = options.opacityTransition || Effect.Transitions.none;
+  
+  var moveX, moveY;
+  
+  switch (direction) {
+    case 'top-left':
+      moveX = moveY = 0;
+      break;
+    case 'top-right':
+      moveX = originalWidth;
+      moveY = 0;
+      break;
+    case 'bottom-left':
+      moveX = 0;
+      moveY = originalHeight;
+      break;
+    case 'bottom-right':
+      moveX = originalWidth;
+      moveY = originalHeight;
+      break;
+    case 'center':  
+      moveX = originalWidth / 2;
+      moveY = originalHeight / 2;
+      break;
+  }
+  
+  return new Effect.Parallel(
+    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: opacityTransition }),
+      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: scaleTransition, restoreAfterFinish: true}),
+      new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition })
+    ], Object.extend({            
+         beforeStartInternal: function(effect) { 
+           Element.makePositioned(effect.effects[0].element);
+           Element.makeClipping(effect.effects[0].element);
+         },
+         afterFinishInternal: function(effect) {
+           var el = effect.effects[0].element;
+           var els = el.style;
+           Element.hide(el);
+           Element.undoClipping(el); 
+           Element.undoPositioned(el);
+           els.top = oldTop;
+           els.left = oldLeft;
+           els.height = oldHeight;
+           els.width = oldWidth;
+           Element.setInlineOpacity(el, oldOpacity);
+         }
+       }, options)
+  );
+}
+
+Effect.Pulsate = function(element) {
+  element = $(element);
+  var options    = arguments[1] || {};
+  var oldOpacity = Element.getInlineOpacity(element);
+  var transition = options.transition || Effect.Transitions.sinoidal;
+  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
+  reverser.bind(transition);
+  return new Effect.Opacity(element, 
+    Object.extend(Object.extend({  duration: 3.0, from: 0,
+      afterFinishInternal: function(effect) { Element.setInlineOpacity(effect.element, oldOpacity); }
+    }, options), {transition: reverser}));
+}
+
+Effect.Fold = function(element) {
+  element = $(element);
+  var originalTop = element.style.top;
+  var originalLeft = element.style.left;
+  var originalWidth = element.style.width;
+  var originalHeight = element.style.height;
+  Element.makeClipping(element);
+  return new Effect.Scale(element, 5, Object.extend({   
+    scaleContent: false,
+    scaleX: false,
+    afterFinishInternal: function(effect) {
+    new Effect.Scale(element, 1, { 
+      scaleContent: false, 
+      scaleY: false,
+      afterFinishInternal: function(effect) { 
+        Element.hide(effect.element);  
+        Element.undoClipping(effect.element); 
+        effect.element.style.top = originalTop;
+        effect.element.style.left = originalLeft;
+        effect.element.style.width = originalWidth;
+        effect.element.style.height = originalHeight;
+      } });
+  }}, arguments[1] || {}));
+}
diff --git a/web/static/js/scriptaculous/scriptaculous.js b/web/static/js/scriptaculous/scriptaculous.js
new file mode 100644
index 0000000..940d886
--- /dev/null
+++ b/web/static/js/scriptaculous/scriptaculous.js
@@ -0,0 +1,48 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// 
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+// 
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var Scriptaculous = {
+  Version: '1.5_rc4',
+  require: function(libraryName) {
+    // inserting via DOM fails in Safari 2.0, so brute force approach
+    document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
+  },
+  load: function() {
+    if((typeof Prototype=='undefined') ||
+      parseFloat(Prototype.Version.split(".")[0] + "." +
+                 Prototype.Version.split(".")[1]) < 1.4)
+      throw("script.aculo.us requires the Prototype JavaScript framework >= 1.4.0");
+    var scriptTags = document.getElementsByTagName("script");
+    for(var i=0;i<scriptTags.length;i++) {
+      if(scriptTags[i].src && scriptTags[i].src.match(/scriptaculous\.js(\?.*)?$/)) {
+        var path = scriptTags[i].src.replace(/scriptaculous\.js(\?.*)?$/,'');
+        this.require(path + 'builder.js');
+        this.require(path + 'effects.js');
+        this.require(path + 'dragdrop.js');
+        this.require(path + 'controls.js');
+        this.require(path + 'slider.js');
+        break;
+      }
+    }
+  }
+}
+
+Scriptaculous.load();
\ No newline at end of file
diff --git a/web/static/js/scriptaculous/slider.js b/web/static/js/scriptaculous/slider.js
new file mode 100644
index 0000000..736ed41
--- /dev/null
+++ b/web/static/js/scriptaculous/slider.js
@@ -0,0 +1,226 @@
+// Copyright (c) 2005 Marty Haught
+// 
+// See scriptaculous.js for full license.
+
+if(!Control) var Control = {};
+Control.Slider = Class.create();
+
+// options:
+//  axis: 'vertical', or 'horizontal' (default)
+//
+// callbacks:
+//  onChange(value)
+//  onSlide(value)
+Control.Slider.prototype = {
+  initialize: function(handle, track, options) {
+    var slider = this;
+    
+    if(handle instanceof Array) {
+      this.handles = handle.collect( function(e) { return $(e) });
+    } else {
+      this.handles = [$(handle)];
+    }
+    
+    this.track   = $(track);
+    this.options = options || {};
+
+    this.axis      = this.options.axis || 'horizontal';
+    this.increment = this.options.increment || 1;
+    this.step      = parseInt(this.options.step || '1');
+    this.range     = this.options.range || $R(0,1);
+    
+    this.value     = 0; // assure backwards compat
+    this.values    = this.handles.map( function() { return 0 });
+    this.spans     = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false;
+    this.restricted = this.options.restricted || false;
+
+    this.maximum   = this.options.maximum || this.range.end;
+    this.minimum   = this.options.minimum || this.range.start;
+
+    // Will be used to align the handle onto the track, if necessary
+    this.alignX = parseInt(this.options.alignX || '0');
+    this.alignY = parseInt(this.options.alignY || '0');
+    
+    this.trackLength = this.maximumOffset() - this.minimumOffset();
+
+    this.active   = false;
+    this.dragging = false;
+    this.disabled = false;
+
+    if(this.options.disabled) this.setDisabled();
+
+    // Allowed values array
+    this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false;
+    if(this.allowedValues) {
+      this.minimum = this.allowedValues.min();
+      this.maximum = this.allowedValues.max();
+    }
+
+    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
+    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
+    this.eventMouseMove = this.update.bindAsEventListener(this);
+
+    // Initialize handles
+    this.handles.each( function(h,i) {
+      slider.setValue(parseInt(slider.options.sliderValue || slider.range.start), i);
+      Element.makePositioned(h); // fix IE
+      Event.observe(h, "mousedown", slider.eventMouseDown);
+    });
+    
+    Event.observe(document, "mouseup", this.eventMouseUp);
+    Event.observe(document, "mousemove", this.eventMouseMove);
+  },
+  dispose: function() {
+    var slider = this;    
+    Event.stopObserving(document, "mouseup", this.eventMouseUp);
+    Event.stopObserving(document, "mousemove", this.eventMouseMove);
+    this.handles.each( function(h) {
+      Event.stopObserving(h, "mousedown", slider.eventMouseDown);
+    });
+  },
+  setDisabled: function(){
+    this.disabled = true;
+  },
+  setEnabled: function(){
+    this.disabled = false;
+  },  
+  getNearestValue: function(value){
+    if(this.allowedValues){
+      if(value >= this.allowedValues.max()) return(this.allowedValues.max());
+      if(value <= this.allowedValues.min()) return(this.allowedValues.min());
+      
+      var offset = Math.abs(this.allowedValues[0] - value);
+      var newValue = this.allowedValues[0];
+      this.allowedValues.each( function(v) {
+        var currentOffset = Math.abs(v - value);
+        if(currentOffset <= offset){
+          newValue = v;
+          offset = currentOffset;
+        } 
+      });
+      return newValue;
+    }
+    if(value > this.range.end) return this.range.end;
+    if(value < this.range.start) return this.range.start;
+    return value;
+  },
+  setValue: function(sliderValue, handleIdx){
+    if(!this.active) {
+      this.activeHandle    = this.handles[handleIdx];
+      this.activeHandleIdx = handleIdx;
+    }
+    handleIdx = handleIdx || this.activeHandleIdx || 0;
+    if(this.restricted) {
+      if((handleIdx>0) && (sliderValue<this.values[handleIdx-1]))
+        sliderValue = this.values[handleIdx-1];
+      if((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1]))
+        sliderValue = this.values[handleIdx+1];
+    }
+    sliderValue = this.getNearestValue(sliderValue);
+    this.values[handleIdx] = sliderValue;
+    this.value = this.values[0]; // assure backwards compat
+    
+    this.handles[handleIdx].style[ this.isVertical() ? 'top' : 'left'] = 
+      this.translateToPx(sliderValue);
+    
+    this.drawSpans();
+    this.updateFinished();
+  },
+  setValueBy: function(delta, handleIdx) {
+    this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta, 
+      handleIdx || this.activeHandleIdx || 0);
+  },
+  translateToPx: function(value) {
+    return Math.round((this.trackLength / (this.range.end - this.range.start)) * (value - this.range.start)) + "px";
+  },
+  translateToValue: function(offset) {
+    return ((offset/this.trackLength) * (this.range.end - this.range.start)) + this.range.start;
+  },
+  getRange: function(range) {
+    var v = this.values.sortBy(Prototype.K); 
+    range = range || 0;
+    return $R(v[range],v[range+1]);
+  },
+  minimumOffset: function(){
+    return(this.isVertical() ? this.alignY : this.alignX);
+  },
+  maximumOffset: function(){
+    return(this.isVertical() ?
+      this.track.offsetHeight - this.alignY : this.track.offsetWidth - this.alignX);
+  },  
+  isVertical:  function(){
+    return (this.axis == 'vertical');
+  },
+  drawSpans: function() {
+    var slider = this;
+    if(this.spans)
+      $R(0, this.spans.length-1).each(function(r) { slider.setSpan(r, slider.getRange(r)) });
+  },
+  setSpan: function(span, range) {
+    if(this.isVertical()) {
+      this.spans[span].style.top = this.translateToPx(range.start);
+      this.spans[span].style.height = this.translateToPx(range.end - range.start);
+    } else {
+      this.spans[span].style.left = this.translateToPx(range.start);
+      this.spans[span].style.width = this.translateToPx(range.end - range.start);
+    }
+  },
+  startDrag: function(event) {
+    if(Event.isLeftClick(event)) {
+      if(!this.disabled){
+        this.active = true;
+        
+        // find the handle (prevents issues with Safari)
+        var handle = Event.element(event);
+        while((this.handles.indexOf(handle) == -1) && handle.parentNode) 
+          handle = handle.parentNode;
+        
+        this.activeHandle    = handle;
+        this.activeHandleIdx = this.handles.indexOf(this.activeHandle);
+        
+        var pointer  = [Event.pointerX(event), Event.pointerY(event)];
+        var offsets  = Position.cumulativeOffset(this.activeHandle);
+        this.offsetX = (pointer[0] - offsets[0]);
+        this.offsetY = (pointer[1] - offsets[1]);
+        
+      }
+      Event.stop(event);
+    }
+  },
+  update: function(event) {
+   if(this.active) {
+      if(!this.dragging) {
+        this.dragging = true;
+        if(this.activeHandle.style.position=="") style.position = "relative";
+      }
+      this.draw(event);
+      // fix AppleWebKit rendering
+      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
+      Event.stop(event);
+   }
+  },
+  draw: function(event) {
+    var pointer = [Event.pointerX(event), Event.pointerY(event)];
+    var offsets = Position.cumulativeOffset(this.track);
+    pointer[0] -= this.offsetX + offsets[0];
+    pointer[1] -= this.offsetY + offsets[1];
+    this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] ));
+    if(this.options.onSlide) this.options.onSlide(this.values.length>1 ? this.values : this.value, this);
+  },
+  endDrag: function(event) {
+    if(this.active && this.dragging) {
+      this.finishDrag(event, true);
+      Event.stop(event);
+    }
+    this.active = false;
+    this.dragging = false;
+  },  
+  finishDrag: function(event, success) {
+    this.active = false;
+    this.dragging = false;
+    this.updateFinished();
+  },
+  updateFinished: function() {
+    if(this.options.onChange) this.options.onChange(this.values.length>1 ? this.values : this.value, this);
+  }
+}
diff --git a/web/static/js/scriptaculous/unittest.js b/web/static/js/scriptaculous/unittest.js
new file mode 100644
index 0000000..20941ad
--- /dev/null
+++ b/web/static/js/scriptaculous/unittest.js
@@ -0,0 +1,363 @@
+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
+//           (c) 2005 Michael Schuerig (http://www.schuerig.de/michael/)
+//
+// See scriptaculous.js for full license.
+
+// experimental, Firefox-only
+Event.simulateMouse = function(element, eventName) {
+  var options = Object.extend({
+    pointerX: 0,
+    pointerY: 0,
+    buttons: 0
+  }, arguments[2] || {});
+  var oEvent = document.createEvent("MouseEvents");
+  oEvent.initMouseEvent(eventName, true, true, document.defaultView, 
+    options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY, 
+    false, false, false, false, 0, $(element));
+  
+  if(this.mark) Element.remove(this.mark);
+  this.mark = document.createElement('div');
+  this.mark.appendChild(document.createTextNode(" "));
+  document.body.appendChild(this.mark);
+  this.mark.style.position = 'absolute';
+  this.mark.style.top = options.pointerY + "px";
+  this.mark.style.left = options.pointerX + "px";
+  this.mark.style.width = "5px";
+  this.mark.style.height = "5px;";
+  this.mark.style.borderTop = "1px solid red;"
+  this.mark.style.borderLeft = "1px solid red;"
+  
+  if(this.step)
+    alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
+  
+  $(element).dispatchEvent(oEvent);
+};
+
+// Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2.
+// You need to downgrade to 1.0.4 for now to get this working
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
+Event.simulateKey = function(element, eventName) {
+  var options = Object.extend({
+    ctrlKey: false,
+    altKey: false,
+    shiftKey: false,
+    metaKey: false,
+    keyCode: 0,
+    charCode: 0
+  }, arguments[2] || {});
+
+  var oEvent = document.createEvent("KeyEvents");
+  oEvent.initKeyEvent(eventName, true, true, window, 
+    options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
+    options.keyCode, options.charCode );
+  $(element).dispatchEvent(oEvent);
+};
+
+Event.simulateKeys = function(element, command) {
+  for(var i=0; i<command.length; i++) {
+    Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)});
+  }
+};
+
+var Test = {}
+Test.Unit = {};
+
+// security exception workaround
+Test.Unit.inspect = function(obj) {
+  var info = [];
+
+  if(typeof obj=="string" || 
+     typeof obj=="number") {
+    return obj;
+  } else {
+    for(property in obj)
+      if(typeof obj[property]!="function")
+        info.push(property + ' => ' + 
+          (typeof obj[property] == "string" ?
+            '"' + obj[property] + '"' :
+            obj[property]));
+  }
+
+  return ("'" + obj + "' #" + typeof obj + 
+    ": {" + info.join(", ") + "}");
+}
+
+Test.Unit.Logger = Class.create();
+Test.Unit.Logger.prototype = {
+  initialize: function(log) {
+    this.log = $(log);
+    if (this.log) {
+      this._createLogTable();
+    }
+  },
+  start: function(testName) {
+    if (!this.log) return;
+    this.testName = testName;
+    this.lastLogLine = document.createElement('tr');
+    this.statusCell = document.createElement('td');
+    this.nameCell = document.createElement('td');
+    this.nameCell.appendChild(document.createTextNode(testName));
+    this.messageCell = document.createElement('td');
+    this.lastLogLine.appendChild(this.statusCell);
+    this.lastLogLine.appendChild(this.nameCell);
+    this.lastLogLine.appendChild(this.messageCell);
+    this.loglines.appendChild(this.lastLogLine);
+  },
+  finish: function(status, summary) {
+    if (!this.log) return;
+    this.lastLogLine.className = status;
+    this.statusCell.innerHTML = status;
+    this.messageCell.innerHTML = this._toHTML(summary);
+  },
+  message: function(message) {
+    if (!this.log) return;
+    this.messageCell.innerHTML = this._toHTML(message);
+  },
+  summary: function(summary) {
+    if (!this.log) return;
+    this.logsummary.innerHTML = this._toHTML(summary);
+  },
+  _createLogTable: function() {
+    this.log.innerHTML =
+    '<div id="logsummary"></div>' +
+    '<table id="logtable">' +
+    '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
+    '<tbody id="loglines"></tbody>' +
+    '</table>';
+    this.logsummary = $('logsummary')
+    this.loglines = $('loglines');
+  },
+  _toHTML: function(txt) {
+    return txt.escapeHTML().replace(/\n/g,"<br/>");
+  }
+}
+
+Test.Unit.Runner = Class.create();
+Test.Unit.Runner.prototype = {
+  initialize: function(testcases) {
+    this.options = Object.extend({
+      testLog: 'testlog'
+    }, arguments[1] || {});
+    this.options.resultsURL = this.parseResultsURLQueryParameter();
+    if (this.options.testLog) {
+      this.options.testLog = $(this.options.testLog) || null;
+    }
+    if(this.options.tests) {
+      this.tests = [];
+      for(var i = 0; i < this.options.tests.length; i++) {
+        if(/^test/.test(this.options.tests[i])) {
+          this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"]));
+        }
+      }
+    } else {
+      if (this.options.test) {
+        this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])];
+      } else {
+        this.tests = [];
+        for(var testcase in testcases) {
+          if(/^test/.test(testcase)) {
+            this.tests.push(new Test.Unit.Testcase(testcase, testcases[testcase], testcases["setup"], testcases["teardown"]));
+          }
+        }
+      }
+    }
+    this.currentTest = 0;
+    this.logger = new Test.Unit.Logger(this.options.testLog);
+    setTimeout(this.runTests.bind(this), 1000);
+  },
+  parseResultsURLQueryParameter: function() {
+    return window.location.search.parseQuery()["resultsURL"];
+  },
+  // Returns:
+  //  "ERROR" if there was an error,
+  //  "FAILURE" if there was a failure, or
+  //  "SUCCESS" if there was neither
+  getResult: function() {
+    var hasFailure = false;
+    for(var i=0;i<this.tests.length;i++) {
+      if (this.tests[i].errors > 0) {
+        return "ERROR";
+      }
+      if (this.tests[i].failures > 0) {
+        hasFailure = true;
+      }
+    }
+    if (hasFailure) {
+      return "FAILURE";
+    } else {
+      return "SUCCESS";
+    }
+  },
+  postResults: function() {
+    if (this.options.resultsURL) {
+      new Ajax.Request(this.options.resultsURL, 
+        { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false });
+    }
+  },
+  runTests: function() {
+    var test = this.tests[this.currentTest];
+    if (!test) {
+      // finished!
+      this.postResults();
+      this.logger.summary(this.summary());
+      return;
+    }
+    if(!test.isWaiting) {
+      this.logger.start(test.name);
+    }
+    test.run();
+    if(test.isWaiting) {
+      this.logger.message("Waiting for " + test.timeToWait + "ms");
+      setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
+    } else {
+      this.logger.finish(test.status(), test.summary());
+      this.currentTest++;
+      // tail recursive, hopefully the browser will skip the stackframe
+      this.runTests();
+    }
+  },
+  summary: function() {
+    var assertions = 0;
+    var failures = 0;
+    var errors = 0;
+    var messages = [];
+    for(var i=0;i<this.tests.length;i++) {
+      assertions +=   this.tests[i].assertions;
+      failures   +=   this.tests[i].failures;
+      errors     +=   this.tests[i].errors;
+    }
+    return (
+      this.tests.length + " tests, " + 
+      assertions + " assertions, " + 
+      failures   + " failures, " +
+      errors     + " errors");
+  }
+}
+
+Test.Unit.Assertions = Class.create();
+Test.Unit.Assertions.prototype = {
+  initialize: function() {
+    this.assertions = 0;
+    this.failures   = 0;
+    this.errors     = 0;
+    this.messages   = [];
+  },
+  summary: function() {
+    return (
+      this.assertions + " assertions, " + 
+      this.failures   + " failures, " +
+      this.errors     + " errors" + "\n" +
+      this.messages.join("\n"));
+  },
+  pass: function() {
+    this.assertions++;
+  },
+  fail: function(message) {
+    this.failures++;
+    this.messages.push("Failure: " + message);
+  },
+  error: function(error) {
+    this.errors++;
+    this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) +")");
+  },
+  status: function() {
+    if (this.failures > 0) return 'failed';
+    if (this.errors > 0) return 'error';
+    return 'passed';
+  },
+  assert: function(expression) {
+    var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
+    try { expression ? this.pass() : 
+      this.fail(message); }
+    catch(e) { this.error(e); }
+  },
+  assertEqual: function(expected, actual) {
+    var message = arguments[2] || "assertEqual";
+    try { (expected == actual) ? this.pass() :
+      this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 
+        '", actual "' + Test.Unit.inspect(actual) + '"'); }
+    catch(e) { this.error(e); }
+  },
+  assertNotEqual: function(expected, actual) {
+    var message = arguments[2] || "assertNotEqual";
+    try { (expected != actual) ? this.pass() : 
+      this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); }
+    catch(e) { this.error(e); }
+  },
+  assertNull: function(obj) {
+    var message = arguments[1] || 'assertNull'
+    try { (obj==null) ? this.pass() : 
+      this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
+    catch(e) { this.error(e); }
+  },
+  assertHidden: function(element) {
+    var message = arguments[1] || 'assertHidden';
+    this.assertEqual("none", element.style.display, message);
+  },
+  assertNotNull: function(object) {
+    var message = arguments[1] || 'assertNotNull';
+    this.assert(object != null, message);
+  },
+  assertInstanceOf: function(expected, actual) {
+    var message = arguments[2] || 'assertInstanceOf';
+    try { 
+      (actual instanceof expected) ? this.pass() : 
+      this.fail(message + ": object was not an instance of the expected type"); }
+    catch(e) { this.error(e); } 
+  },
+  assertNotInstanceOf: function(expected, actual) {
+    var message = arguments[2] || 'assertNotInstanceOf';
+    try { 
+      !(actual instanceof expected) ? this.pass() : 
+      this.fail(message + ": object was an instance of the not expected type"); }
+    catch(e) { this.error(e); } 
+  },
+  _isVisible: function(element) {
+    element = $(element);
+    if(!element.parentNode) return true;
+    this.assertNotNull(element);
+    if(element.style && Element.getStyle(element, 'display') == 'none')
+      return false;
+    
+    return this._isVisible(element.parentNode);
+  },
+  assertNotVisible: function(element) {
+    this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1]));
+  },
+  assertVisible: function(element) {
+    this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1]));
+  }
+}
+
+Test.Unit.Testcase = Class.create();
+Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), {
+  initialize: function(name, test, setup, teardown) {
+    Test.Unit.Assertions.prototype.initialize.bind(this)();
+    this.name           = name;
+    this.test           = test || function() {};
+    this.setup          = setup || function() {};
+    this.teardown       = teardown || function() {};
+    this.isWaiting      = false;
+    this.timeToWait     = 1000;
+  },
+  wait: function(time, nextPart) {
+    this.isWaiting = true;
+    this.test = nextPart;
+    this.timeToWait = time;
+  },
+  run: function() {
+    try {
+      try {
+        if (!this.isWaiting) this.setup.bind(this)();
+        this.isWaiting = false;
+        this.test.bind(this)();
+      } finally {
+        if(!this.isWaiting) {
+          this.teardown.bind(this)();
+        }
+      }
+    }
+    catch(e) { this.error(e); }
+  }
+});
\ No newline at end of file
diff --git a/web/templates/_elements/header b/web/templates/_elements/header
new file mode 100644
index 0000000..e8942d3
--- /dev/null
+++ b/web/templates/_elements/header
@@ -0,0 +1,25 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+  <meta name="robots" content="all" />
+  
+  <title><% $title %></title>
+  
+  <link rel="stylesheet" type="text/css" href="/css/main.css" media="all" />
+  
+  <script type="text/javascript" src="/js/prototype.js"></script>
+  <script type="text/javascript" src="/js/rico.js"></script>
+  <script type="text/javascript" src="/js/behaviour.js"></script>
+  <script type="text/javascript" src="/js/jifty.js"></script>
+  <script type="text/javascript" src="/js/btdt_behaviour.js"></script>
+  <script type="text/javascript" src="/js/bps_util.js"></script>
+  <script type="text/javascript" src="/js/combobox.js"></script>
+  <script type="text/javascript" src="/js/key_bindings.js"></script>
+</head>
+<%args>
+$title => ""
+</%args>
+<%init>
+$r->content_type('text/html; charset=utf-8');
+</%init>
diff --git a/web/templates/_elements/markup b/web/templates/_elements/markup
new file mode 100644
index 0000000..a281a2a
--- /dev/null
+++ b/web/templates/_elements/markup
@@ -0,0 +1,143 @@
+<div id="syntax">
+<h2>Wiki Syntax</h2>
+
+<h3>Phrase Emphasis</h3>
+
+<pre><code>*italic*   **bold**
+_italic_   __bold__
+</code></pre>
+
+<h3>Links</h3>
+
+<p>Inline:</p>
+
+<pre><code>Show me a [wiki page](WikiPage)</code></pre>
+
+<pre><code>An [example](http://url.com/ "Title")
+</code></pre>
+
+<p>Reference-style labels (titles are optional):</p>
+
+<pre><code>An [example][id]. Then, anywhere
+else in the doc, define the link:
+
+  [id]: http://example.com/  "Title"
+</code></pre>
+
+<h3>Images</h3>
+
+<p>Inline (titles are optional):</p>
+
+<pre><code>![alt text](/path/img.jpg "Title")
+</code></pre>
+
+<p>Reference-style:</p>
+
+<pre><code>![alt text][id]
+
+[id]: /url/to/img.jpg "Title"
+</code></pre>
+
+<h3>Headers</h3>
+
+<p>Setext-style:</p>
+
+<pre><code>Header 1
+========
+
+Header 2
+--------
+</code></pre>
+
+<p>atx-style (closing #'s are optional):</p>
+
+<pre><code># Header 1 #
+
+## Header 2 ##
+
+###### Header 6
+</code></pre>
+
+<h3>Lists</h3>
+
+<p>Ordered, without paragraphs:</p>
+
+<pre><code>1.  Foo
+2.  Bar
+
+</code></pre>
+
+<p>Unordered, with paragraphs:</p>
+
+<pre><code>*   A list item.
+
+    With multiple paragraphs.
+
+*   Bar
+</code></pre>
+
+<p>You can nest them:</p>
+
+<pre><code>*   Abacus
+    * answer
+*   Bubbles
+    1.  bunk
+    2.  bupkis
+        * BELITTLER
+    3. burper
+*   Cunning
+</code></pre>
+
+<h3>Blockquotes</h3>
+
+<pre><code>> Email-style angle brackets
+> are used for blockquotes.
+
+> > And, they can be nested.
+
+> #### Headers in blockquotes
+> 
+> * You can quote a list.
+> * Etc.
+</code></pre>
+
+<h3>Code Spans</h3>
+
+<pre><code>`<code>` spans are delimited
+by backticks.
+
+You can include literal backticks
+like `` `this` ``.
+</code></pre>
+
+<h3>Preformatted Code Blocks</h3>
+
+<p>Indent every line of a code block 
+by at least 4 spaces or 1 tab.</p>
+
+<pre><code>This is a normal paragraph.
+
+    This is a preformatted
+    code block.
+</code></pre>
+
+<h3>Horizontal Rules</h3>
+
+<p>Three or more dashes or asterisks:</p>
+
+<pre><code>---
+
+* * *
+
+- - - -
+</code></pre>
+
+<h3>Manual Line Breaks</h3>
+
+<p>End a line with two or more spaces:</p>
+
+<pre><code>Roses are red,   
+Violets are blue.
+</code></pre>
+<address>(Thanks to <a href="http://daringfireball.net/projects/markdown/dingus">Daring Fireball</a>)</address>
+</div> 
diff --git a/web/templates/_elements/nav b/web/templates/_elements/nav
new file mode 100644
index 0000000..c773de9
--- /dev/null
+++ b/web/templates/_elements/nav
@@ -0,0 +1,7 @@
+<%init>
+my $top = Jifty->web->navigation;
+$top->child(Home       => url => "/", sort_order => 1);
+$top->child(Recent       => url => "/recent", label => "Recent Changes", sort_order => 2);
+
+return();
+</%init>
diff --git a/web/templates/_elements/sidebar b/web/templates/_elements/sidebar
new file mode 100644
index 0000000..2869f05
--- /dev/null
+++ b/web/templates/_elements/sidebar
@@ -0,0 +1,26 @@
+<div id="salutation">
+% if (Jifty->web->current_user->id) {
+Hiya, <span class="user"><%Jifty->web->current_user->name%></span>.
+% }  else {
+You're not currently signed in. 
+% }
+</div>
+<ul class="menu">
+% $m->comp(".menu", item => $_) for (sort { $a->sort_order <=> $b->sort_order} Jifty->web->navigation->children);
+</ul>
+<%def .menu>
+<%args>
+$item
+</%args>
+  <li><% 
+    Jifty->web->link(
+        url   => $item->url,
+        label => $item->label,
+        class => $item->active ? "active" : ""
+    ) %></li>
+% if (my @kids = $item->children) {
+<ul class="menu submenu">
+% $m->comp(".menu", item => $_) for @kids;
+</ul>
+% }
+</%def>
diff --git a/web/templates/_elements/wrapper b/web/templates/_elements/wrapper
new file mode 100644
index 0000000..0ec63a4
--- /dev/null
+++ b/web/templates/_elements/wrapper
@@ -0,0 +1,27 @@
+<& header, title => $title &>
+<body>
+  <div id="headers">
+    <%Jifty->web->link( url => "/", label => Jifty->config->framework('ApplicationName'))%>
+    <h1 class="title"><% $title %></h1>
+  </div>
+  <& sidebar &>
+  <div id="content">
+    <a name="content"></a>
+    <% Jifty->web->render_messages %>
+    <% $m->content |n%>
+    <div id="keybindings">
+       <script><!--
+       writeKeyBindingLegend();
+       --></script>
+    </div>
+  </div>
+  <div id="jifty-wait-message">Loading...</div>
+</body>
+</html>
+<%args>
+$title => ""
+</%args>
+<%init>
+$m->comp('nav');
+
+</%init>
diff --git a/web/templates/autohandler b/web/templates/autohandler
new file mode 100644
index 0000000..0a4476b
--- /dev/null
+++ b/web/templates/autohandler
@@ -0,0 +1,25 @@
+<%init>
+Jifty->web->handle_request();
+
+if ($m->base_comp->path =~ m|/_elements/|) {
+    # Requesting an internal component by hand -- naughty
+    $m->redirect("/errors/requested_private_component");
+#} elsif (not Jifty->web->current_user->id and $m->request_comp->path !~ m{^/(?:welcome|dhandler|css|js|images|validator\.xml)} ) {
+#    # Not logged in, trying to access a protected page
+#    $m->notes->{'login-nextpage'} =  $m->{top_path};
+#    Jifty->web->redirect('/welcome/');
+}
+</%init>
+<%$m->call_next()%>
+<%def .setup_actions>
+<%init>
+Jifty->web->allow_actions(qr/.*/);
+# this method turns around and calls the setup_actions method 
+# it's called by Jifty::Web->setup_page_actions.
+my $delegate = $m->fetch_comp($m->next_comp->path);
+if ($delegate and $delegate->method_exists('setup_actions')) {
+    $delegate->call_method('setup_actions');
+}
+
+</%init>
+</%def>
diff --git a/web/templates/create/dhandler b/web/templates/create/dhandler
new file mode 100644
index 0000000..fcbbe89
--- /dev/null
+++ b/web/templates/create/dhandler
@@ -0,0 +1,13 @@
+<%init>
+my $page = $m->dhandler_arg;
+my $action = Jifty->web->new_action( class => 'CreatePage');
+</%init>
+<&|/_elements/wrapper, title => 'New page: '. $page&>
+<% Jifty->web->form->start %>
+<% Jifty->web->form->next_page( url => '/view/'.$page) %>
+<% $action->form_field('name', render_as => 'hidden', default_value => $page) %>
+<% $action->form_field('content')%>
+<% Jifty->web->form->submit( label => 'Save')%>
+<% Jifty->web->form->end %>
+<& /_elements/markup &>
+</&>
diff --git a/web/templates/dhandler b/web/templates/dhandler
new file mode 100644
index 0000000..8a2fe56
--- /dev/null
+++ b/web/templates/dhandler
@@ -0,0 +1,47 @@
+<&| /_elements/wrapper, title => "Something's not quite right" &>
+
+<div id="overview">
+
+<p>You got to a page that we don't think exists.  Anyway, the software has logged this error. Sorry about this.</p>
+
+<p><%Jifty->web->link( url => "/", label => 'Go back home...')%></p>
+
+</div>
+</&>
+%# XXX TODO ACTUALLY LOG THIS.
+<%doc>
+Used as a poor man's 404 handler
+</%doc>
+<%init>
+
+# This code loads up any static file and displays it if it would 404 from dynamic content. Failing that, actually 404
+my $file = $m->dhandler_arg;
+my $type = "application/octet-stream";
+if ( $file =~ /\.(gif|png|jpe?g)$/i ) {
+    $type = "image/$1";
+    $type =~ s/jpg/jpeg/gi;
+} elsif ($file =~ /\.css$/i ) {
+    $type ='text/css';
+} elsif ($file =~ /\.js$/i) {
+    $type = 'application/x-javascript';
+}
+my $image = Jifty::Util->absolute_path( Jifty->config->framework('Web')->{'StaticRoot'}
+        || "static" )
+    . "/"
+    . $file;
+
+if ( ( -f $image && -r $image ) ) {
+    $r->header_out( 'Cache-Control' => 'max-age=3600, must-revalidate' );
+    $r->content_type($type);
+    open( FILE, "<$image" ) || die;
+    {
+        local $/ = \16384;
+        $m->out($_) while (<FILE>);
+        close(FILE);
+    }
+    $m->abort;
+}
+
+Jifty->log->error("404: user tried to get to ".$m->dhandler_arg);
+$r->header_out( Status => '404');
+</%init>
diff --git a/web/templates/edit/dhandler b/web/templates/edit/dhandler
new file mode 100644
index 0000000..4a5240d
--- /dev/null
+++ b/web/templates/edit/dhandler
@@ -0,0 +1,16 @@
+<%init>
+my $name = $m->dhandler_arg();
+my $page = Wifty::Model::Page->new();
+$page->load_by_cols( name => $name );
+my $viewer = Jifty->web->new_action( class => 'UpdatePage', record => $page );
+my $top = Jifty->web->navigation;
+$top->child(Show       => url =>  '/view/'.$page->name,, label => 'Show Page',  sort_order => 5);
+</%init>
+<&|/_elements/wrapper, title => 'Edit: '.$page->name &>
+<% Jifty->web->form->start %>
+<% Jifty->web->form->next_page( url => '/view/'.$page->name) %>
+<% $viewer->form_field('content') %>
+<% Jifty->web->form->submit( label => 'Save') %>
+<% Jifty->web->form->end%>
+<& /_elements/markup &>
+</&>
diff --git a/web/templates/favicon.ico b/web/templates/favicon.ico
new file mode 100644
index 0000000..e69de29
diff --git a/web/templates/index.html b/web/templates/index.html
new file mode 100644
index 0000000..e5cb700
--- /dev/null
+++ b/web/templates/index.html
@@ -0,0 +1 @@
+% Jifty->web->redirect( '/view/HomePage');
diff --git a/web/templates/pages b/web/templates/pages
new file mode 100644
index 0000000..35f05bc
--- /dev/null
+++ b/web/templates/pages
@@ -0,0 +1,12 @@
+<%init>
+my $pages = Wifty::Model::PageCollection->new();
+$pages->unlimit();
+
+</%init>
+<&|/_elements/wrapper, title => 'These are the pages on your wiki!' &>
+<ul id="pagelist">
+% while (my $page = $pages->next) {
+<li><% Jifty->web->link( label => $page->name, url => '/view/'.$page->name)%></li>
+% } 
+</ul>
+</&>
diff --git a/web/templates/recent b/web/templates/recent
new file mode 100644
index 0000000..433fb35
--- /dev/null
+++ b/web/templates/recent
@@ -0,0 +1,14 @@
+<%init>
+my $then = DateTime->from_epoch(epoch => (time - (86400*7)));
+my $pages = Wifty::Model::PageCollection->new();
+$pages->limit( column => 'updated', operator => '>', value => $then->ymd );
+$pages->order_by( column => 'updated', order => 'desc');
+</%init>
+<&|/_elements/wrapper, title => 'Updated this week' &>
+<dl id="recentudates">
+% while (my $page = $pages->next) {
+<dt><% Jifty->web->link( label => $page->name, url => '/view/'.$page->name)%></dt>
+<dd><%$page->updated%></dd>
+% } 
+</dl>
+</&>
diff --git a/web/templates/view/dhandler b/web/templates/view/dhandler
new file mode 100644
index 0000000..63c0f18
--- /dev/null
+++ b/web/templates/view/dhandler
@@ -0,0 +1,14 @@
+<%init>
+my $name = $m->dhandler_arg();
+my $page = Wifty::Model::Page->new();
+$page->load_by_cols( name => $name);
+unless ($page->id) {
+    Jifty->web->redirect( '/create/'.$name);
+    # XXX TODO: should this use goto or gosub or whatever we're calling it this week?
+}
+my $top = Jifty->web->navigation;
+$top->child(Edit       => url =>  '/edit/'.$page->name , sort_order => 5);
+</%init>
+<&|/_elements/wrapper, title => $page->name &>
+<% $page->wiki_content |n %>
+</&>

commit ba981322c1fc3a42078619f1d60c2ce53099f93d
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:30:12 2005 +0000

    The 'page' of a revision is a *Page* not a *Revision*

diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index 646519b..bd5344e 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -2,7 +2,7 @@ package Wifty::Model::Revision::Schema;
 use Jifty::DBI::Schema;
 
 column page  => 
-    refers_to Wifty::Model::Revision;
+    refers_to Wifty::Model::Page;
 
 column content =>
     type is 'text',

commit ac379948b2d1e1f77cc4af2a566b3aa0d13be041
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:30:41 2005 +0000

    Switched to asking for autopackaged dependencies

diff --git a/bin/jifty b/bin/jifty
index 4f653a5..0ba9b77 100755
--- a/bin/jifty
+++ b/bin/jifty
@@ -6,6 +6,7 @@ use File::Basename qw(dirname);
 BEGIN {
     my $dir = dirname(__FILE__); 
     push @INC, "$dir/../lib";
+    push @INC, "$dir/../../Jifty/deps";
     push @INC, "$dir/../../Jifty/lib";
 }
 

commit e71375c9a2572dc8998cbd327bab80770fcdf1fa
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:30:54 2005 +0000

    Turn off admin mode on Wifty and Hiveminder

diff --git a/etc/config.yml b/etc/config.yml
index 5812a3d..ba02778 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -1,4 +1,5 @@
 framework:
+  AdminMode: 0
   LogConfig: etc/btdt.log4perl.conf
   Database:
     Driver: Pg

commit 5703cdda805a3234798ba1e72a300147581990f8
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:31:13 2005 +0000

    Use less code (especially the stuff that we can pull from the default app

diff --git a/lib/Wifty/Bootstrap.pm b/lib/Wifty/Bootstrap.pm
index f05c0d4..5026527 100644
--- a/lib/Wifty/Bootstrap.pm
+++ b/lib/Wifty/Bootstrap.pm
@@ -1,15 +1,16 @@
 package Wifty::Bootstrap;
 
 use Wifty::Model::Page;
+
 sub run {
     my $self = shift;
 
     my $index = Wifty::Model::Page->new();
-    $index->create( name => 'home',
-                    content=> 'Welcome to your Wifty');
-
+    $index->create(
+        name    => 'HomePage',
+        content => 'Welcome to your Wifty'
+    );
 
 }
 
-
 1;
diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index b6541f8..6af8ae8 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -50,17 +50,16 @@ sub wiki_content {
 
 }
 
-
 sub create {
     my $self = shift;
     my %args = (@_);
-    my $now = DateTime->now();
-    $args{'updated'} =  $now->ymd." ".$now->hms;
+    my $now  = DateTime->now();
+    $args{'updated'} = $now->ymd . " " . $now->hms;
     my ($id) = $self->SUPER::create(%args);
-    if ($self->id) { 
+    if ( $self->id ) {
         $self->_add_revision(%args);
     }
-    return($id);
+    return ($id);
 }
 
 =head2 _add_revision 
@@ -88,19 +87,22 @@ sub _add_revision {
 }
 
 sub set_content {
-    my $self = shift;
+    my $self    = shift;
     my $content = shift;
-    my ($val, $msg) = $self->SUPER::set_content($content);
-    $self->_add_revision(content =>$content );
-    return ($val,$msg);
+    my ( $val, $msg ) = $self->SUPER::set_content($content);
+    $self->_add_revision( content => $content );
+    return ( $val, $msg );
 }
 
 sub _set {
     my $self = shift;
-    my ($val,$msg) = $self->SUPER::_set(@_);
+    my ( $val, $msg ) = $self->SUPER::_set(@_);
     my $now = DateTime->now();
-    $self->SUPER::_set( column => 'updated', value =>   $now->ymd." ".$now->hms);
-    return ($val,$msg);
+    $self->SUPER::_set(
+        column => 'updated',
+        value  => $now->ymd . " " . $now->hms
+    );
+    return ( $val, $msg );
 }
 
 1;
diff --git a/web/static/css/base.css b/web/static/css/app-base.css
similarity index 100%
rename from web/static/css/base.css
rename to web/static/css/app-base.css
diff --git a/web/static/css/main.css b/web/static/css/main.css
deleted file mode 100644
index db0e44c..0000000
--- a/web/static/css/main.css
+++ /dev/null
@@ -1 +0,0 @@
- at import "base.css";
diff --git a/web/static/js/behaviour.js b/web/static/js/behaviour.js
deleted file mode 100644
index bc5504f..0000000
--- a/web/static/js/behaviour.js
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
-   Behaviour v1.1 by Ben Nolan, June 2005. Based largely on the work
-   of Simon Willison (see comments by Simon below).
-
-   Description:
-   	
-   	Uses css selectors to apply javascript behaviours to enable
-   	unobtrusive javascript in html documents.
-   	
-   Usage:   
-   
-	var myrules = {
-		'b.someclass' : function(element){
-			element.onclick = function(){
-				alert(this.innerHTML);
-			}
-		},
-		'#someid u' : function(element){
-			element.onmouseover = function(){
-				this.innerHTML = "BLAH!";
-			}
-		}
-	};
-	
-	Behaviour.register(myrules);
-	
-	// Call Behaviour.apply() to re-apply the rules (if you
-	// update the dom, etc).
-
-   License:
-   
-   	My stuff is BSD licensed. Not sure about Simon's.
-   	
-   More information:
-   	
-   	http://ripcord.co.nz/behaviour/
-   
-*/   
-
-var Behaviour = {
-	list : new Array,
-	
-	register : function(sheet){
-		Behaviour.list.push(sheet);
-	},
-	
-	start : function(){
-		Behaviour.addLoadEvent(function(){
-			Behaviour.apply();
-		});
-	},
-	
-	apply : function(){
-		for (h=0;sheet=Behaviour.list[h];h++){
-			for (selector in sheet){
-				list = document.getElementsBySelector(selector);
-				
-				if (!list){
-					continue;
-				}
-
-				for (i=0;element=list[i];i++){
-					sheet[selector](element);
-				}
-			}
-		}
-	},
-	
-	addLoadEvent : function(func){
-		var oldonload = window.onload;
-		
-		if (typeof window.onload != 'function') {
-			window.onload = func;
-		} else {
-			window.onload = function() {
-				oldonload();
-				func();
-			}
-		}
-	}
-}
-
-Behaviour.start();
-
-/*
-   The following code is Copyright (C) Simon Willison 2004.
-
-   document.getElementsBySelector(selector)
-   - returns an array of element objects from the current document
-     matching the CSS selector. Selectors can contain element names, 
-     class names and ids and can be nested. For example:
-     
-       elements = document.getElementsBySelect('div#main p a.external')
-     
-     Will return an array of all 'a' elements with 'external' in their 
-     class attribute that are contained inside 'p' elements that are 
-     contained inside the 'div' element which has id="main"
-
-   New in version 0.4: Support for CSS2 and CSS3 attribute selectors:
-   See http://www.w3.org/TR/css3-selectors/#attribute-selectors
-
-   Version 0.4 - Simon Willison, March 25th 2003
-   -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows
-   -- Opera 7 fails 
-*/
-
-function getAllChildren(e) {
-  // Returns all children of element. Workaround required for IE5/Windows. Ugh.
-  return e.all ? e.all : e.getElementsByTagName('*');
-}
-
-document.getElementsBySelector = function(selector) {
-  // Attempt to fail gracefully in lesser browsers
-  if (!document.getElementsByTagName) {
-    return new Array();
-  }
-  // Split selector in to tokens
-  var tokens = selector.split(' ');
-  var currentContext = new Array(document);
-  for (var i = 0; i < tokens.length; i++) {
-    token = tokens[i].replace(/^\s+/,'').replace(/\s+$/,'');;
-    if (token.indexOf('#') > -1) {
-      // Token is an ID selector
-      var bits = token.split('#');
-      var tagName = bits[0];
-      var id = bits[1];
-      var element = document.getElementById(id);
-      if (tagName && element.nodeName.toLowerCase() != tagName) {
-        // tag with that ID not found, return false
-        return new Array();
-      }
-      // Set currentContext to contain just this element
-      currentContext = new Array(element);
-      continue; // Skip to next token
-    }
-    if (token.indexOf('.') > -1) {
-      // Token contains a class selector
-      var bits = token.split('.');
-      var tagName = bits[0];
-      var className = bits[1];
-      if (!tagName) {
-        tagName = '*';
-      }
-      // Get elements matching tag, filter them for class selector
-      var found = new Array;
-      var foundCount = 0;
-      for (var h = 0; h < currentContext.length; h++) {
-        var elements;
-        if (tagName == '*') {
-            elements = getAllChildren(currentContext[h]);
-        } else {
-            elements = currentContext[h].getElementsByTagName(tagName);
-        }
-        for (var j = 0; j < elements.length; j++) {
-          found[foundCount++] = elements[j];
-        }
-      }
-      currentContext = new Array;
-      var currentContextIndex = 0;
-      for (var k = 0; k < found.length; k++) {
-        if (found[k].className && found[k].className.match(new RegExp('\\b'+className+'\\b'))) {
-          currentContext[currentContextIndex++] = found[k];
-        }
-      }
-      continue; // Skip to next token
-    }
-    // Code to deal with attribute selectors
-    if (token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/)) {
-      var tagName = RegExp.$1;
-      var attrName = RegExp.$2;
-      var attrOperator = RegExp.$3;
-      var attrValue = RegExp.$4;
-      if (!tagName) {
-        tagName = '*';
-      }
-      // Grab all of the tagName elements within current context
-      var found = new Array;
-      var foundCount = 0;
-      for (var h = 0; h < currentContext.length; h++) {
-        var elements;
-        if (tagName == '*') {
-            elements = getAllChildren(currentContext[h]);
-        } else {
-            elements = currentContext[h].getElementsByTagName(tagName);
-        }
-        for (var j = 0; j < elements.length; j++) {
-          found[foundCount++] = elements[j];
-        }
-      }
-      currentContext = new Array;
-      var currentContextIndex = 0;
-      var checkFunction; // This function will be used to filter the elements
-      switch (attrOperator) {
-        case '=': // Equality
-          checkFunction = function(e) { return (e.getAttribute(attrName) == attrValue); };
-          break;
-        case '~': // Match one of space seperated words 
-          checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('\\b'+attrValue+'\\b'))); };
-          break;
-        case '|': // Match start with value followed by optional hyphen
-          checkFunction = function(e) { return (e.getAttribute(attrName).match(new RegExp('^'+attrValue+'-?'))); };
-          break;
-        case '^': // Match starts with value
-          checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) == 0); };
-          break;
-        case '$': // Match ends with value - fails with "Warning" in Opera 7
-          checkFunction = function(e) { return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); };
-          break;
-        case '*': // Match ends with value
-          checkFunction = function(e) { return (e.getAttribute(attrName).indexOf(attrValue) > -1); };
-          break;
-        default :
-          // Just test for existence of attribute
-          checkFunction = function(e) { return e.getAttribute(attrName); };
-      }
-      currentContext = new Array;
-      var currentContextIndex = 0;
-      for (var k = 0; k < found.length; k++) {
-        if (checkFunction(found[k])) {
-          currentContext[currentContextIndex++] = found[k];
-        }
-      }
-      // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue);
-      continue; // Skip to next token
-    }
-    
-    if (!currentContext[0]){
-    	return;
-    }
-    
-    // If we get here, token is JUST an element (not a class or ID selector)
-    tagName = token;
-    var found = new Array;
-    var foundCount = 0;
-    for (var h = 0; h < currentContext.length; h++) {
-      var elements = currentContext[h].getElementsByTagName(tagName);
-      for (var j = 0; j < elements.length; j++) {
-        found[foundCount++] = elements[j];
-      }
-    }
-    currentContext = found;
-  }
-  return currentContext;
-}
-
-/* That revolting regular expression explained 
-/^(\w+)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/
-  \---/  \---/\-------------/    \-------/
-    |      |         |               |
-    |      |         |           The value
-    |      |    ~,|,^,$,* or =
-    |   Attribute 
-   Tag
-*/
diff --git a/web/static/js/bps_util.js b/web/static/js/bps_util.js
deleted file mode 100644
index 330c8a5..0000000
--- a/web/static/js/bps_util.js
+++ /dev/null
@@ -1,89 +0,0 @@
-// XXX TODO This library should likely be refactored to use behaviour
-
-function focusElementById(id) {
-    var e = document.getElementById(id);
-    if (e) e.focus();
-}
-
-function openCalWindow(field) {
-    var objWindow = window.open('/helpers/calendar.html?field='+field, 'Calendar', 'height=200,width=235,scrollbars=1');
-    objWindow.focus();
-}
-
-function updateParentField(field, value) {
-    if (window.opener) {
-        window.opener.document.getElementById(field).value = value;
-        window.close();
-    }
-}
-
-function createCalendarLink(input) {
-    var e = document.getElementById(input);
-    if (e) {
-        var link = document.createElement('a');
-        link.setAttribute('href', '#');
-        link.setAttribute('onclick', "openCalWindow('"+input+"'); return false;");
-        
-        var text = document.createTextNode('Calendar');
-        link.appendChild(text);
-        
-        var space = document.createTextNode(' ');
-        
-        e.parentNode.insertBefore(link, e.nextSibling);
-        e.parentNode.insertBefore(space, e.nextSibling);
-        
-        return true;
-    }
-    return false;
-}
-
-// onload handlers
-
-var onLoadStack     = new Array();
-var onLoadLastStack = new Array();
-var onLoadExecuted  = 0;
-
-function onLoadHook(commandStr) {
-    if(typeof(commandStr) == "string") {
-        onLoadStack[onLoadStack.length] = commandStr;
-        return true;
-    }
-    return false;
-}
-
-// some things *really* need to be done after everything else
-function onLoadLastHook(commandStr) {
-    if(typeof(commandStr) == "string"){
-        onLoadLastStack[onLoadLastStack.length] = commandStr;
-        return true;
-    }
-    return false;
-}
-
-function doOnLoadHooks() {
-    if(onLoadExecuted) return;
-    for (var x=0; x < onLoadStack.length; x++) { 
-        eval(onLoadStack[x]);
-    }
-    for (var x=0; x < onLoadLastStack.length; x++) { 
-        eval(onLoadLastStack[x]); 
-    }
-    onLoadExecuted = 1;
-}
-
-
-if (typeof window.onload != 'function') {
-    window.onload = doOnLoadHooks;
-} else {
-    var oldonload = window.onload;
-    
-    window.onload = function() {
-        oldonload();
-        doOnLoadHooks();
-    }
-}
-
-function jifty_button_click() {
-
-1;
-}
diff --git a/web/static/js/btdt_behaviour.js b/web/static/js/btdt_behaviour.js
deleted file mode 100644
index 7c3f3ec..0000000
--- a/web/static/js/btdt_behaviour.js
+++ /dev/null
@@ -1,36 +0,0 @@
-/* Uses Behaviour v1.0 (behaviour.js); see
-        http://ripcord.co.nz/behaviour/
-
-   IMPORTANT: if you make DOM changes that mean that an element
-              ought to gain or lose a behaviour, call Behaviour.apply()!
-   (Actually, that *won't* make something lose a behaviour, so if that's necessary
-    you'll need to have an empty "fallback".  Ie, if "div#foo a" should have a special
-    onclick and other "a" shouldn't, then there ought to be an explicit "a" style
-    that sets onclick to a trivial function, if DOM changes will ever happen.)
-   (Also, with the current behaviour.js, the order of application of styles is undefined,
-    so you can't really do cascading.  I've suggested to the author that he change it;
-    if he doesn't, but we need it, it's an easy change to make the sheets arrays instead
-    of Objects (hashes).  For now this can be dealt with by loading multiple sheets (register
-    calls), though.)
-*/
-
-
-
-/*    'textarea.bigbox' : function(elt) {
-  new Form.Element.Observer( elt.id,
-     1,
-         function( element, value ) {
-         new Ajax.Updater( elt.id+'-observer',
-         '/fragments/parsetext',
-         { parameters: Form.Element.getAction(elt).serialize(),
-           onComplete: function () { Behaviour.apply() } }
-       )
-     }
-         );
-    },
-*/
-
-var myrules = {
-                };
-        
-        Behaviour.register(myrules);
diff --git a/web/static/js/combobox.js b/web/static/js/combobox.js
deleted file mode 100644
index 90fcddf..0000000
--- a/web/static/js/combobox.js
+++ /dev/null
@@ -1,233 +0,0 @@
-function ComboBox_InitWith(n) {
-    if ( typeof( window.addEventListener ) != "undefined" ) {
-        window.addEventListener("load", ComboBox_Init(n), false);
-    } else if ( typeof( window.attachEvent ) != "undefined" ) {
-        window.attachEvent("onload", ComboBox_Init(n));
-    } else {
-        ComboBox_Init(n)();
-    }
-}
-function ComboBox_Init(n) {
-    return function () {
-        if ( ComboBox_UplevelBrowser( n ) ) {
-            ComboBox_Load( n );
-        }
-    }
-}
-function ComboBox_UplevelBrowser( n ) {
-    if( typeof( document.getElementById ) == "undefined" ) return false;
-    var combo = document.getElementById( n + "_Container" );
-    if( combo == null || typeof( combo ) == "undefined" ) return false;
-    if( typeof( combo.style ) == "undefined" ) return false;
-    if( typeof( combo.innerHTML ) == "undefined" ) return false;
-    return true;
-}
-function ComboBox_Load( comboId ) {
-    var combo  = document.getElementById( comboId + "_Container" );
-    var button = document.getElementById( comboId + "_Button" );
-    var list   = document.getElementById( comboId + "_List" );
-    var text   = document.getElementById( comboId );
-    
-    
-    combo.List = list;
-    combo.Button = button;
-    combo.Text = text;
-    
-    button.Container = combo;
-    button.Toggle = ComboBox_ToggleList;
-    button.onclick = button.Toggle;
-    button.onmouseover = function(e) { this.Container.List.DisableBlur(e); };
-    button.onmouseout = function(e) { this.Container.List.EnableBlur(e); };
-    button.innerHTML = "\u25BC";
-    button.onselectstart = function(e){ return false; };
-    button.style.height = ( list.offsetHeight - 4 ) + "px";
-    
-    text.Container = combo;
-    text.TypeDown = ComboBox_TextTypeDown;
-    text.KeyAccess = ComboBox_TextKeyAccess;
-    text.onkeyup = function(e) { this.KeyAccess(e); this.TypeDown(e); };
-    text.style.width = ( list.offsetWidth ) + "px";
-    
-    list.Container = combo;
-    list.Show = ComboBox_ShowList;
-    list.Hide = ComboBox_HideList;
-    list.EnableBlur = ComboBox_ListEnableBlur;
-    list.DisableBlur = ComboBox_ListDisableBlur;
-    list.Select = ComboBox_ListItemSelect;
-    list.ClearSelection = ComboBox_ListClearSelection;
-    list.KeyAccess = ComboBox_ListKeyAccess;
-    list.FireTextChange = ComboBox_ListFireTextChange;
-    list.onchange = null;
-    list.onclick = function(e){ this.Select(e); this.ClearSelection(); this.FireTextChange(); };
-    list.onkeyup = function(e) { this.KeyAccess(e); };
-    list.EnableBlur(null);
-    list.style.position = "absolute";
-    list.size = ComboBox_GetListSize( list );
-    list.IsShowing = true;
-    list.Hide();
-    
-}
-function ComboBox_InitEvent( e ) {
-    if( typeof( e ) == "undefined" && typeof( window.event ) != "undefined" ) e = window.event;
-    if( e == null ) e = new Object();
-    return e;
-}
-function ComboBox_ListClearSelection() {
-            if ( typeof( this.Container.Text.createTextRange ) == "undefined" ) return;
-    var rNew = this.Container.Text.createTextRange();
-    rNew.moveStart('character', this.Container.Text.value.length) ;
-    rNew.select();
-}
-function ComboBox_GetListSize( theList ) {
-    ComboBox_EnsureListSize( theList );
-    return theList.listSize;
-}
-function ComboBox_EnsureListSize( theList ) {
-    if ( typeof( theList.listSize ) == "undefined" ) {
-        if( typeof( theList.getAttribute ) != "undefined" ) {
-            if( theList.getAttribute( "listSize" ) != null && theList.getAttribute( "listSize" ) != "" ) {
-                theList.listSize = theList.getAttribute( "listSize" );
-                return;
-            }
-        }
-        if( theList.options.length > 0 ) {
-            theList.listSize = theList.options.length;
-            return;
-        }
-        theList.listSize = 4;
-    }
-}
-function ComboBox_ListKeyAccess(e) { //Make enter/space and escape do the right thing :)
-    e = ComboBox_InitEvent( e );
-    if( e.keyCode == 13 || e.keyCode == 32 ) {
-        this.Select();
-        return;
-    }
-    if( e.keyCode == 27 ) {
-        this.Hide();
-        this.Container.Text.focus();
-        return;
-    }
-}
-function ComboBox_TextKeyAccess(e) { //Make alt+arrow expand the list
-    e = ComboBox_InitEvent( e );
-    if( e.altKey && (e.keyCode == 38 || e.keyCode == 40) ) {
-            this.Container.List.Show();
-    }
-}
-function ComboBox_TextTypeDown(e) { //Make the textbox do a type-down on the list
-    e = ComboBox_InitEvent( e );
-    var items = this.Container.List.options;
-    if( this.value == "" ) return;
-    var ctrlKeys = Array( 8, 46, 37, 38, 39, 40, 33, 34, 35, 36, 45, 16, 20 );
-    for( var i = 0; i < ctrlKeys.length; i++ ) {
-        if( e.keyCode == ctrlKeys[i] ) return;
-    }
-    for( var i = 0; i < items.length; i++ ) {
-        var item = items[i];
-        if( item.text.toLowerCase().indexOf( this.value.toLowerCase() ) == 0 ) {
-            this.Container.List.selectedIndex = i;
-            if ( typeof( this.Container.Text.createTextRange ) != "undefined" ) {
-                                    this.Container.List.Select();
-                            }
-            break;
-        }
-    }
-}
-function ComboBox_ListFireTextChange() {
-    var textOnChange = this.Container.Text.onchange;
-            if ( textOnChange != null && typeof(textOnChange) == "function" ) {
-                    textOnChange();
-            }
-}
-function ComboBox_ListEnableBlur(e) {
-    this.onblur = this.Hide;
-}
-function ComboBox_ListDisableBlur(e) {
-    this.onblur = null;
-}
-function ComboBox_ListItemSelect(e) {
-    if( this.options.length > 0 ) {
-        var text = this.Container.Text;
-        var oldValue = text.value;
-        var newValue = this.options[ this.selectedIndex ].value;
-        text.value = newValue;
-        if ( typeof( text.createTextRange ) != "undefined" ) {
-            if (newValue != oldValue) {
-                var rNew = text.createTextRange();
-                rNew.moveStart('character', oldValue.length) ;
-                rNew.select();
-            }
-        }
-    }
-    this.Hide();
-    this.Container.Text.focus();
-}
-function ComboBox_ToggleList(e) {
-    if( this.Container.List.IsShowing == true ) {
-        this.Container.List.Hide();
-    } else {
-        this.Container.List.Show();
-    }
-}
-function ComboBox_ShowList(e) {
-    if ( !this.IsShowing && !this.disabled ) {
-        this.style.width = ( this.Container.offsetWidth ) + "px";
-        this.style.top = ( this.Container.offsetHeight + ComboBox_RecursiveOffsetTop(this.Container,true) ) + "px";
-        this.style.left = ( ComboBox_RecursiveOffsetLeft(this.Container,true) + 1 ) + "px";
-        ComboBox_SetVisibility(this,true);
-        this.focus();
-        this.IsShowing = true;
-    }
-}
-function ComboBox_HideList(e) {
-    if( this.IsShowing ) {
-                    ComboBox_SetVisibility(this,false);
-        this.IsShowing = false;
-    }
-}
-function ComboBox_SetVisibility(theList,isVisible) {
-    var isIE = ( typeof( theList.dataSrc ) != "undefined" ); // dataSrc is an IE-only property which is unlikely to be supported elsewhere
-    var ua = navigator.userAgent.toLowerCase(); 
-    var isSafari = (ua.indexOf('safari') != - 1);
-    if ( isIE || isSafari) {
-        if ( isVisible ) {
-            theList.style.visibility = "visible";
-        } else {
-            theList.style.visibility = "hidden";
-        }
-    } else { 
-        if ( isVisible ) {
-            theList.style.display = "block";
-        } else {
-            theList.style.display = "none";
-        }
-    }
-}
-function ComboBox_RecursiveOffsetTop(thisObject,isFirst) {
-    if(thisObject.offsetParent) {
-        if ( thisObject.style.position == "absolute" && !isFirst && typeof(document.designMode) != "undefined" ) {
-            return 0;
-        }
-        return (thisObject.offsetTop + ComboBox_RecursiveOffsetTop(thisObject.offsetParent,false));
-    } else {
-        return thisObject.offsetTop;
-    }
-}
-function ComboBox_RecursiveOffsetLeft(thisObject,isFirst) {
-    if(thisObject.offsetParent) {
-        if ( thisObject.style.position == "absolute" && !isFirst && typeof(document.designMode) != "undefined" ) {
-            return 0;
-        }
-        return (thisObject.offsetLeft + ComboBox_RecursiveOffsetLeft(thisObject.offsetParent,false));
-    } else {
-        return thisObject.offsetLeft;
-    }
-}
-function ComboBox_SimpleAttach(selectElement,textElement) {
-    textElement.value = selectElement.options[ selectElement.options.selectedIndex ].value;
-    var textOnChange = textElement.onchange;
-    if ( textOnChange != null && typeof( textOnChange ) == "function" ) {
-        textOnChange();
-    }
-}
diff --git a/web/static/js/dom-drag.js b/web/static/js/dom-drag.js
deleted file mode 100644
index 9fb7118..0000000
--- a/web/static/js/dom-drag.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/**************************************************
- * dom-drag.js
- * 09.25.2001
- * www.youngpup.net
- **************************************************
- * 10.28.2001 - fixed minor bug where events
- * sometimes fired off the handle, not the root.
- **************************************************/
-var Drag = { obj : null, init : function(o, oRoot, minX, maxX, minY, maxY, bSwapHorzRef, bSwapVertRef, fXMapper, fYMapper){ o.onmousedown = Drag.start; o.hmode = bSwapHorzRef ? false : true ; o.vmode = bSwapVertRef ? false : true ; o.root = oRoot && oRoot != null ? oRoot : o ; if (o.hmode && isNaN(parseInt(o.root.style.left ))) o.root.style.left = "0px"; if (o.vmode && isNaN(parseInt(o.root.style.top ))) o.root.style.top = "0px"; if (!o.hmode && isNaN(parseInt(o.root.style.right ))) o.root.style.right = "0px"; if (!o.vmode && isNaN(parseInt(o.root.style.bottom))) o.root.style.bottom = "0px"; o.minX = typeof minX != 'undefined' ? minX : null; o.minY = typeof minY != 'undefined' ? minY : null; o.maxX = typeof maxX != 'undefined' ? maxX : null; o.maxY = typeof maxY != 'undefined' ? maxY : null; o.xMapper = fXMapper ? fXMapper : null; o.yMapper = fYMapper ? fYMapper : null; o.root.onDragStart = new Function(); o.root.onDragEnd = new Function(); o.root.onDrag = new Function();}, 
 start : function(e){ var o = Drag.obj = this; e = Drag.fixE(e); var y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom); var x = parseInt(o.hmode ? o.root.style.left : o.root.style.right ); o.root.onDragStart(x, y); o.lastMouseX = e.clientX; o.lastMouseY = e.clientY; if (o.hmode) { if (o.minX != null) o.minMouseX = e.clientX - x + o.minX; if (o.maxX != null) o.maxMouseX = o.minMouseX + o.maxX - o.minX;} else { if (o.minX != null) o.maxMouseX = -o.minX + e.clientX + x; if (o.maxX != null) o.minMouseX = -o.maxX + e.clientX + x;} if (o.vmode) { if (o.minY != null) o.minMouseY = e.clientY - y + o.minY; if (o.maxY != null) o.maxMouseY = o.minMouseY + o.maxY - o.minY;} else { if (o.minY != null) o.maxMouseY = -o.minY + e.clientY + y; if (o.maxY != null) o.minMouseY = -o.maxY + e.clientY + y;} document.onmousemove = Drag.drag; document.onmouseup = Drag.end; return false;}, drag : function(e){ e = Drag.fixE(e); var o = Drag.obj; var ey = e.clientY; var ex = e.clientX; var
  y = parseInt(o.vmode ? o.root.style.top : o.root.style.bottom); var x = parseInt(o.hmode ? o.root.style.left : o.root.style.right ); var nx, ny; if (o.minX != null) ex = o.hmode ? Math.max(ex, o.minMouseX) : Math.min(ex, o.maxMouseX); if (o.maxX != null) ex = o.hmode ? Math.min(ex, o.maxMouseX) : Math.max(ex, o.minMouseX); if (o.minY != null) ey = o.vmode ? Math.max(ey, o.minMouseY) : Math.min(ey, o.maxMouseY); if (o.maxY != null) ey = o.vmode ? Math.min(ey, o.maxMouseY) : Math.max(ey, o.minMouseY); nx = x + ((ex - o.lastMouseX) * (o.hmode ? 1 : -1)); ny = y + ((ey - o.lastMouseY) * (o.vmode ? 1 : -1)); if(o.xMapper){ nx = o.xMapper(y) } else if (o.yMapper) { ny = o.yMapper(x); } Drag.obj.root.style[o.hmode ? "left" : "right"] = nx + "px"; Drag.obj.root.style[o.vmode ? "top" : "bottom"] = ny + "px"; Drag.obj.lastMouseX = ex; Drag.obj.lastMouseY = ey; Drag.obj.root.onDrag(nx, ny); return false;}, end : function(){ document.onmousemove = null; document.onmouseup = null; Drag.
 obj.root.onDragEnd( parseInt(Drag.obj.root.style[Drag.obj.hmode ? "left" : "right"]), parseInt(Drag.obj.root.style[Drag.obj.vmode ? "top" : "bottom"])); Drag.obj = null;}, fixE : function(e){ if (typeof e == 'undefined') e = window.event; if (typeof e.layerX == 'undefined') e.layerX = e.offsetX; if (typeof e.layerY == 'undefined') e.layerY = e.offsetY; return e;} };
\ No newline at end of file
diff --git a/web/static/js/ie7/README.txt b/web/static/js/ie7/README.txt
deleted file mode 100644
index e546ce4..0000000
--- a/web/static/js/ie7/README.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-Installation
-------------
-
-Follow these simple instructions to get IE7 working immediately on your server:
-
- * download the latest IE7 ZIP file (https://sourceforge.net/project/showfiles.php?group_id=109983&package_id=119707)
-
- * extract the contents to a directory on your server (keep the folder names used in the ZIP)
-
- * you will now have an IE7 directory on your server
-
- * include the IE7 JavaScript library in the page you wish to test
-
-   <!-- compliance patch for microsoft browsers -->
-   <!--[if lt IE 7]><script src="/ie7/ie7-standard-p.js" type="text/javascript"></script><![endif]-->
-
- * make sure this also points to the same directory
-
- * open the page in your web browser
-
- * the page should now be IE7 enabled.
-
- * if you are using the PNG solution then be aware that it operates on files
-   names "something-trans.png"
-
- * see this page for more configuration and usage options:
-   http://dean.edwards.name/IE7/usage/
-
-You may extract the contents of the ZIP file to your hard disk if you do not have access to a web server.
-
-
-Enjoy ;-)
-
-Dean Edwards, 23rd May 2005
diff --git a/web/static/js/ie7/blank.gif b/web/static/js/ie7/blank.gif
deleted file mode 100644
index a4fe2e6..0000000
Binary files a/web/static/js/ie7/blank.gif and /dev/null differ
diff --git a/web/static/js/ie7/ie7-base64.php b/web/static/js/ie7/ie7-base64.php
deleted file mode 100644
index 530392d..0000000
--- a/web/static/js/ie7/ie7-base64.php
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-$data = split(";", $_SERVER["REDIRECT_QUERY_STRING"]);
-$type = $data[0];
-$data = split(",", $data[1]);
-header("Content-type: ".$type);
-echo base64_decode($data[1]);
-?>
\ No newline at end of file
diff --git a/web/static/js/ie7/ie7-content.htc b/web/static/js/ie7/ie7-content.htc
deleted file mode 100644
index cc480cb..0000000
--- a/web/static/js/ie7/ie7-content.htc
+++ /dev/null
@@ -1,14 +0,0 @@
-<html>
-<!--
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
--->
-<head>
-<object id="dummy" width="0" height="0"></object>
-<base id="base">
-<style type="text/css">html,body,img{margin:0;}img{vertical-align:top}#dummy{display:inline}</style>
-<script type="text/javascript">public_description=new function(){var l=false;this.ie7_anon=true;this.load=function(o,c,u){if(l)return;l=true;base.href=o.document.URL;dummy.style.cssText=c;var _0=o.parentElement;var _1=Boolean(dummy.currentStyle.display=="inline");function r(){o.runtimeStyle.width=(_1)?image.offsetWidth:"100%";o.runtimeStyle.height=body.offsetHeight};image.onreadystatechange=function(){if(this.readyState=="complete")_2()};image.src=u;function _2(){function copy(p){try{body.style[p]=_0.currentStyle[p]}catch(i){}};for(var j in body.currentStyle)copy(j);body.style.width="";body.style.height="";body.style.border="none";body.style.padding="0";body.style.margin="0";body.style.textIndent="";body.style.position="static";while(_0&&_0.currentStyle.backgroundColor=="transparent"){_0=_0.parentElement}if(_0)document.body.style.backgroundColor=_0.currentStyle.backgroundColor;body.runtimeStyle.cssText=c;body.runtimeStyle.margin="0";if(_1)body.runtimeStyle.width="";r()}}};</
 script>
-</head>
-<body><span id="body"><img id="image"></span></body>
-</html>
diff --git a/web/static/js/ie7/ie7-core.js b/web/static/js/ie7/ie7-core.js
deleted file mode 100644
index c3dbcef..0000000
--- a/web/static/js/ie7/ie7-core.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('x(!1M.1j)z 6(){1l{1M.1j=8;4 1W=8.2m=z 26;8.O=6(){7"1j 2q 0.9 (5G)"};4 36=/36/.B(42.41.40);4 1G=(36)?6(m){1M.1G(1j+"\\n\\n"+m)}:1W;4 2t=5F.2t.1g(/5E (\\d\\.\\d)/)[1];4 2z=K.5D!="5C";x(/5B/.B(42.41.40)||2t<5||!/^5A/.B(K.1J.2A))7;4 1H=K.39=="1H";4 1t,5z;4 1J=K.1J,2s,3X,17=K.17;4 5y="!";4 22={};4 1u=1C;1j.2m=6(n,s){x(!22[n]){x(1u)1k("s="+2o(s));22[n]=z s()}};4 R=/^[\\w\\.]+[^:]*$/;6 1I(h,p){x(R.B(h))h=(p||"")+h;7 h};6 2e(h,p){h=1I(h,p);7 h.1d(0,h.3n("/")+1)};4 s=K.3Z[K.3Z.y-1];1l{1k(s.3z)}1i(i){}4 1R=2e(s.5x);4 1F;1l{4 l=(5w()>=5)?"5v":"5u";1F=z 5t(l+".5s")}1i(i){}4 2w={};6 2y(h,p){1l{h=1I(h,p);x(!2w[h]){1F.5r("5q",h,1C);1F.5p();x(1F.3Y==0||1F.3Y==5o){2w[h]=1
 F.5n}}}1i(i){1G("2x [1]: 30 5m 5l "+h)}37{7 2w[h]||""}};4 5k=1I("5j.5i",1R);6 1E(V){x(V!=1L){V.2v=13.16.2v;V.12=13.16.12}7 V};1E.12=6(p,c){x(!p)p={};x(!c)c=p.J;x(c=={}.J)c=z 26("8.2v()");c.Y=z 26("7 8");c.Y.16=z 8.Y;c.Y.16.12(p);c.16=z c.Y;c.Y.16.J=c.16.J=c;c.1r=8;c.12=F.32;c.2u=8.2u;7 c};1E.Y=z 26("7 8");1E.Y.16={J:1E,2v:6(){7 F.32.5h.1r.2k(8,F)},12:6(V){x(8==8.J.16&&8.J.12){7 8.J.Y.16.12(V)}D(4 i 5g V){2K(i){1o"J":1o"O":1o"Y":2X}x(2V V[i]=="6"&&V[i]!=8[i]){V[i].1r=8[i]}8[i]=V[i]}x(V.O!=8.O&&V.O!={}.O){V.O.1r=8.O;8.O=V.O}7 8}};6 13(){};8.13=1E.12({J:13,O:6(){7"[5f "+(8.J.2Z||"5e")+"]"},5d:6(1h){7 8.J==1h||1h.2u(8.J)}});13.2Z="13";13.1r=1L;13.2u=6(1h){1f(1h&&1h.1r!=8)1h=1h.1r;7 3J(1h)};13.Y.1r=1E;2a 8.13;4 3A=13.12({J:6(){8.5c=[];8.1p=[]},1s:1W});x(2t<5.5)1k(2y("Z-5b.3a",1R));4 35=1C;1j.1s=6(){1l{x(35)7;35=1H=1c;2s=K.2s;3X=(2z)?2s:1J;x(1K&&1t)1t.2k();15.2k();1n();1G("1u 5a")}1i(e){1G("2x [2]: "+e.38)}};4 1p=[];6 2C(r){1p.11(r)};6 1n(){H.3P();x(1K&&1t)1t.1n();15.1n();D(4 i=0;
 i<1p.y;i++)1p[i]()};6 23(){4 E=0,R=1,L=2;4 G=/\\(/g,S=/\\$\\d/,I=/^\\$\\d+$/,T=/([\'"])\\1\\+(.*)\\+\\1\\1$/,3Q=/\\\\./g,Q=/\'/,3W=/\\25[^\\25]*\\25/g;4 1X=8;8.18=6(e,r){x(!r)r="";4 l=(34(2o(e)).1g(G)||"").y+1;x(S.B(r)){x(I.B(r)){r=3e(r.1d(1))-1}1b{4 i=l;4 q=Q.B(34(r))?\'"\':"\'";1f(i)r=r.2S("$"+i--).2p(q+"+a[o+"+i+"]+"+q);r=z 26("a,o","7"+q+r.19(T,"$1")+q)}}3V(e||"/^$/",r,l)};8.1U=6(s){24.y=0;7 3R(3S(s,8.2r).19(z 1Z(1D,8.33?"2I":"g"),3T),8.2r).19(3W,"")};8.59=6(){1D.y=0};4 24=[];4 1D=[];4 3U=6(){7"("+2o(8[E]).1d(1,-1)+")"};1D.O=6(){7 8.2p("|")};6 3V(){F.O=3U;1D[1D.y]=F}6 3T(){x(!F[0])7"";4 i=1,j=0,p;1f(p=1D[j++]){x(F[i]){4 r=p[R];2K(2V r){1o"6":7 r(F,i);1o"58":7 F[r+i]}4 d=(F[i].57(1X.2r)==-1)?"":"\\25"+F[i]+"\\25";7 d+r}1b i+=p[L]}};6 3S(s,e){7 e?s.19(z 1Z("\\\\"+e+"(.)","g"),6(m,c){24[24.y]=c;7 e}):s};6 3R(s,e){4 i=0;7 e?s.19(z 1Z("\\\\"+e,"g"),6(){7 e+(24[i++]||"")}):s};6 34(s){7 s.19(3Q,"")}};23.16={J:23,33:1C,2r:""};13.12(23.16);4 1V=23.12({33:1c});4 H=6(){4 2q="2.0.2"
 ;4 C=/\\s*,\\s*/;4 H=6(s,14){1l{4 m=[];4 u=F.32.2Q&&!14;4 b=(14)?(14.J==3G)?14:[14]:[K];4 31=3D(s).2S(C),i;D(i=0;i<31.y;i++){s=2R(31[i]);x(3K&&s.1d(0,3).2p("")==" *#"){s=s.1d(2);14=3H([],b,s[1])}1b 14=b;4 j=0,t,f,a,c="";1f(j<s.y){t=s[j++];f=s[j++];c+=t+f;a="";x(s[j]=="("){1f(s[j++]!=")"&&j<s.y){a+=s[j]}a=a.1d(0,-1);c+="("+a+")"}14=(u&&1P[c])?1P[c]:3F(14,t,f,a);x(u)1P[c]=14}m=m.3t(14)}2a H.30;7 m}1i(e){H.30=e;7[]}};H.O=6(){7"6 H() {\\n  [2q "+2q+"]\\n}"};4 1P={};H.2Q=1C;H.3P=6(s){x(s){s=2R(s).2p("");2a 1P[s]}1b 1P={}};4 22={};4 1u=1C;H.2m=6(n,s){x(1u)1k("s="+2o(s));22[n]=z s()};H.Y=6(c){7 c?1k(c):8};4 1B={};4 2n={};4 56={1g:/\\[([\\w-]+(\\|[\\w-]+)?)\\s*(\\W?=)?\\s*([^\\]]*)\\]/};4 55=[];1B[" "]=6(r,f,t,n){4 e,i,j;D(i=0;i<f.y;i++){4 s=2l(f[i],t,n);D(j=0;(e=s[j]);j++){x(1q(e)&&2T(e,n))r.11(e)}}};1B["#"]=6(r,f,i){4 e,j;D(j=0;(e=f[j]);j++)x(e.1a==i)r.11(e)};1B["."]=6(r,f,c){c=z 1Z("(^|\\\\s)"+c+"(\\\\s|$)");4 e,i;D(i=0;(e=f[i]);i++)x(c.B(e.2Z))r.11(e)};1B[":"]=6(r,f,p,a){4 t=2n[
 p],e,i;x(t)D(i=0;(e=f[i]);i++)x(t(e,a))r.11(e)};2n["21"]=6(e){4 d=2U(e);x(d.2Y)D(4 i=0;i<d.2Y.y;i++){x(d.2Y[i]==e)7 1c}};2n["2N"]=6(e){};4 1q=6(e){7(e&&e.3B==1&&e.2P!="!")?e:1L};4 3N=6(e){1f(e&&(e=e.54)&&!1q(e))2X;7 e};4 2W=6(e){1f(e&&(e=e.53)&&!1q(e))2X;7 e};4 3L=6(e){7 1q(e.3O)||2W(e.3O)};4 52=6(e){7 1q(e.3M)||3N(e.3M)};4 51=6(e){4 c=[];e=3L(e);1f(e){c.11(e);e=2W(e)}7 c};4 3K=1c;4 2O=6(e){4 d=2U(e);7(2V d.3I=="50")?/\\.4Z$/i.B(d.4Y):3J(d.3I=="4X 4W")};4 2U=6(e){7 e.4V||e.K};4 2l=6(e,t){7(t=="*"&&e.1A)?e.1A:e.2l(t)};4 4U=6(e,t,n){x(t=="*")7 1q(e);x(!2T(e,n))7 1C;x(!2O(e))t=t.4T();7 e.2P==t};4 2T=6(e,n){7!n||(n=="*")||(e.4S==n)};4 4R=6(e){7 e.4Q};6 3H(r,f,1a){4 m,i,j;D(i=0;i<f.y;i++){x(m=f[i].1A.4P(1a)){x(m.1a==1a)r.11(m);1b x(m.y!=1L){D(j=0;j<m.y;j++){x(m[j].1a==1a)r.11(m[j])}}}}7 r};x(![].11)3G.16.11=6(){D(4 i=0;i<F.y;i++){8[8.y]=F[i]}7 8.y};4 N=/\\|/;6 3F(14,t,f,a){x(N.B(f)){f=f.2S(N);a=f[0];f=f[1]}4 r=[];x(1B[t]){1B[t](r,14,f,a)}7 r};4 S=/^[^\\s>+~]/;4 3E=/[\\s#.:>+~()@]
 |[^\\s#.:>+~()@]+/g;6 2R(s){x(S.B(s))s=" "+s;7 s.1g(3E)||[]};4 W=/\\s*([\\s>+~(),]|^|$)\\s*/g;4 I=/([\\s>+~,]|[^(]\\+|^)([#.:@])/g;4 3D=6(s){7 s.19(W,"$1").19(I,"$1*$2")};4 1y={O:6(){7"\'"},1g:/^(\'[^\']*\')|("[^"]*")$/,B:6(s){7 8.1g.B(s)},18:6(s){7 8.B(s)?s:8+s+8},3C:6(s){7 8.B(s)?s.1d(1,-1):s}};4 1N=6(t){7 1y.3C(t)};4 E=/([\\/()[\\]?{}|*+-])/g;6 4O(s){7 s.19(E,"\\\\$1")};1u=1c;7 H}();H.2Q=1c;H.2m("Z",6(){1q=6(e){7(e&&e.3B==1&&e.2P!="!"&&!e.3d)?e:1L}});H.Y("1N=F[1]",3k);4 1K=!H.Y("2O(F[1])",1J);4 2h=":21{Z-21:21}:2N{Z-21:2N}"+(1K?"":"*{4N:0}");4 15=z(3A.12({2F:z 1V,1O:"",1w:"",2L:[],1s:6(){8.2M();8.2g()},2g:6(){15.1Y.X=2h+8.1O+8.1w},3y:6(){4 20=K.2l("1e"),s;D(4 i=20.y-1;(s=20[i]);i--){x(!s.2H&&!s.Z){8.2L.11(s.3z)}}},2k:6(){8.3y();8.2g();z 28("1O");8.3u()},3w:6(e,r){8.2F.18(e,r)},1n:6(){4 R=/3v\\d+/g;4 s=2h.1g(/[{,]/g).y;4 20=s+(8.1O.X.1g(/\\{/g)||"").y;4 3x=8.1Y.4M,r;4 2j,c,2i,e,i,j,k,1a;D(i=s;i<20;i++){r=3x[i];x(r&&(2j=r.1e.X.1g(R))){2i=H(r.4L);x(2i.y)D(j=0;j<2j.y;j++){1a=
 2j[j];c=15.1p[1a.1d(10)][2];D(k=0;(e=2i[k]);k++){x(e.1v[1a])c(e)}}}}},2C:6(p,t,h,r){t=z 1Z("([{;\\\\s])"+p+"\\\\s*:\\\\s*"+t+"[^;}]*");4 i=8.1p.y;x(r)r=p+":"+r;8.3w(t,6(m,o){7(r?m[o+1]+r:m[o])+";Z-"+m[o].1d(1)+";3v"+i+":1"});8.1p.11(F);7 i},1N:6(s){7 s.X||""},2M:6(){x(1H||!1K)K.2M();1b K.4K("<1e Z=1c></1e>");8.1Y=17[17.y-1];8.1Y.Z=1c;8.1Y.X=2h},3u:6(){D(4 i=0;i<17.y;i++){x(!17[i].Z&&17[i].X){17[i].X=""}}}}));6 28(m){8.1z=m;8.1S();15[m]=8;15.2g()};13.12({J:28,O:6(){7"@1z "+8.1z+"{"+8.X+"}"},1n:1W,1S:6(){8.X="";8.1N();8.3m();8.X=3j(8.X);f={}},1N:6(){4 3r=[].3t(15.2L);4 M=/@1z\\s+([^{]*)\\{([^@]+\\})\\s*\\}/2I;4 A=/\\4J\\b|^$/i,S=/\\4I\\b/i,P=/\\4H\\b/i;6 3q(c,m){2f.v=m;7 c.19(M,2f)};6 2f(4G,m,c){m=2J(m);2K(m){1o"1O":1o"1w":x(m!=2f.v)7"";1o"1A":7 c}7""};6 2J(m){x(A.B(m))7"1A";1b x(S.B(m))7(P.B(m))?"1A":"1O";1b x(P.B(m))7"1w"};4 1X=8;6 2G(s,p,m,l){4 c="";x(!l){m=2J(s.1z);l=0}x(m=="1A"||m==1X.1z){x(l<3){D(4 i=0;i<s.3s.y;i++){c+=2G(s.3s[i],2e(s.2d,p),m,l+1)}}c+=3l(s.2d?3p(s,p):3r.
 3h()||"");c=3q(c,1X.1z)}7 c};4 f={};6 3p(s,p){4 u=1I(s.2d,p);x(f[u])7"";f[u]=(s.2H)?"":3o(15.1N(s,p),2e(s.2d,p));7 f[u]};4 U=/(4F\\s*\\(\\s*[\'"]?)([\\w\\.]+[^:\\)]*[\'"]?\\))/2I;6 3o(c,p){7 c.19(U,"$1"+p.1d(0,p.3n("/")+1)+"$2")};D(4 i=0;i<17.y;i++){x(!17[i].2H&&!17[i].Z){8.X+=2G(17[i])}}},3m:6(){8.X=15.2F.1U(8.X)},1n:1W});4 1y=H.Y("1y");4 2b=[];6 3l(c){7 1x.1U(2c.1U(c))};6 2E(m,o){7 1y+(2b.11(m[o])-1)+1y};6 3k(v){7 1y.B(v)?1k(2b[1k(v)]):v};4 1x=z 1V;1x.18(/\\/\\*[^*]*\\*+([^\\/][^*]*\\*+)*\\//);1x.18(/\'[^\']*\'/,2E);1x.18(/"[^"]*"/,2E);1x.18(/\\s+/," ");1x.18(/@(4E|4D)[^;\\n]+[;\\n]|<!\\-\\-|\\-\\->/);4 2c=z 1V;2c.18(/\\\\\'/,"\\\\4C");2c.18(/\\\\"/,"\\\\4B");4 2D=z 1V;2D.18(/\'(\\d+)\'/,3i);6 3j(c){7 2D.1U(c)};6 3i(m,o){7 2b[m[o+1]]};4 2B=[];6 4A(h){2C(h);1Q(1M,"4z",h)};6 1Q(e,t,h){e.4y(t,h);2B.11(F)};6 3g(e,t,h){1l{e.4x(t,h)}1i(i){}};1Q(1M,"4w",6(){4 h;1f(h=2B.3h()){3g(h[0],h[1],h[2])}});6 4v(h,e,c){x(!h.29)h.29={};x(c)h.29[e.2A]=e;1b 2a h.29[e.2A];7 c};1Q(1M,"4u",6(){x(
 !15.1w)z 28("1w");15.1w.1n()});4 3f=/^\\d+(4t)?$/i;4 4s=/^\\d+%$/;4 4r=6(e,v){x(3f.B(v))7 3e(v);4 s=e.1e.1m;4 r=e.1T.1m;e.1T.1m=e.1v.1m;e.1e.1m=v||0;v=e.1e.4q;e.1e.1m=s;e.1T.1m=r;7 v};6 4p(t){4 e=K.4o(t||"4n");e.1e.X="3c:4m;4l:0;4k:4j;4i:4h;4g:4f(0 0 0 0);1m:-4e";e.3d=1c;7 e};4 27="Z-";6 4d(e){7 e.1v["Z-3c"]=="4c"};6 4b(e,p){7 e.1v[27+p]||e.1v[p]};6 4a(e,p,v){x(e.1v[27+p]==1L){e.1T[27+p]=e.1v[p]}e.1T[p]=v};6 49(o,c,u){4 t=48(6(){1l{x(!o.1S)7;o.1S(o,c,u);3b(t)}1i(i){3b(t)}},10)};1u=1c;x(2z)1k(2y("Z-47.3a",1R));15.1s();x(1K&&1t)1t.1s();x(1H)1j.1s();1b{1J.46(1I("Z-1S.45",1R));1Q(K,"44",6(){x(K.39=="1H")43(1j.1s,0)})}}1i(e){1G("2x [0]: "+e.38)}37{}};',62,353,'||||var||function|return|this|||||||||||||||||||||||||if|length|new||test||for||arguments||cssQuery||constructor|document||||toString|||||||that||cssText|valueOf|ie7||push|specialize|Common|fr|ie7CSS|prototype|styleSheets|add|replace|id|else|true|slice|style|while|match|klass|catch|IE7|eval|try|left|recalc|case|recalcs|this
 Element|ancestor|init|ie7HTML|loaded|currentStyle|print|encoder|Quote|media|all|selectors|false|_0|ICommon|httpRequest|alert|complete|makePath|documentElement|isHTML|null|window|getText|screen|cache|addEventHandler|path|load|runtimeStyle|exec|Parser|DUMMY|self|styleSheet|RegExp|st|link|modules|ParseMaster|_1|x01|Function|_2|StyleSheet|elements|delete|_3|safeString|href|getPath|_4|refresh|HEADER|el|ca|apply|getElementsByTagName|addModule|pseudoClasses|String|join|version|escapeChar|body|appVersion|ancestorOf|inherit|_5|Error|loadFile|quirksMode|uniqueID|_6|addRecalc|decoder|_7|parser|_8|disabled|gi|_9|switch|styles|createStyleSheet|visited|isXML|tagName|caching|_10|split|compareNamespace|getDocument|typeof|nextElementSibling|continue|links|className|error|se|callee|ignoreCase|_11|_12|ie7_debug|finally|description|readyState|js|clearInterval|position|ie7_anon|parseInt|PIXEL|removeEventHandler|pop|_13|decode|getString|_14|parse|lastIndexOf|_15|_16|_17|_18|imports|concat|trash|i
 e7_recalc|addFix|ru|getInlineStyles|innerHTML|Fix|nodeType|remove|parseSelector|ST|select|Array|_19|mimeType|Boolean|isMSIE|firstElementChild|lastChild|previousElementSibling|firstChild|clearCache|ES|_20|_21|_22|_23|_24|DE|viewport|status|scripts|search|location|top|setTimeout|onreadystatechange|htc|addBehavior|quirks|setInterval|addTimer|setOverrideStyle|getDefinedStyle|fixed|isFixed|9999|rect|clip|none|border|block|display|padding|absolute|object|createElement|createTempElement|pixelLeft|getPixelValue|PERCENT|px|onbeforeprint|register|onunload|detachEvent|attachEvent|onresize|addResize|x22|x27|import|namespace|url|ma|bprint|bscreen|ball|write|selectorText|rules|margin|regEscape|item|innerText|getTextContent|scopeName|toUpperCase|compareTagName|ownerDocument|Document|XML|URL|xml|unknown|childElements|lastElementChild|nextSibling|previousSibling|attributeSelectors|AttributeSelector|indexOf|number|reset|successfully|ie5|fixes|instanceOf|Object|common|in|caller|gif|blank|BLANK
 _GIF|file|loading|responseText|200|send|GET|open|XMLHTTP|ActiveXObject|Microsoft|Msxml2|ScriptEngineMajorVersion|src|ANON|ie7Layout|ms_|ie7_off|CSS1Compat|compatMode|MSIE|navigator|alpha'.split('|'),0,{}))
diff --git a/web/static/js/ie7/ie7-css-strict.js b/web/static/js/ie7/ie7-css-strict.js
deleted file mode 100644
index 0c7e330..0000000
--- a/web/static/js/ie7/ie7-css-strict.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-IE7.addModule("ie7-css-strict",function(){if(!modules["ie7-css2-selectors"])return;StyleSheet.prototype.specialize({parse:function(){this.inherit();var r=[].concat(this.rules);r.sort(ie7CSS.Rule.compare);this.cssText=r.join("\n")},createRule:function(s,c){var m;if(m=s.match(ie7CSS.PseudoElement.MATCH))return new ie7CSS.PseudoElement(m[1],m[2],c);else if(m=s.match(ie7CSS.DynamicRule.MATCH))return new ie7CSS.DynamicRule(s,m[1],m[2],m[3],c);else return new ie7CSS.Rule(s,c)}});ie7CSS.specialize({apply:function(){this.inherit();this.Rule.MATCH=/([^{}]+)(\{[^{}]*\})/g}});ie7CSS.Rule.compare=function(r1,r2){return r1.specificity-r2.specificity};var N=[],I=/#/g,C=/[.:\[]/g,T=/^\w|[\s>+~]\w/g;ie7CSS.Rule.score=function(s){return(s.match(I)||N).length*10000+(s.match(C)||N).length*100+(s.match(T)||N).length};ie7CSS.Rule.simple=function(){return""};ie7CSS.Rule.prototype.specialize({specificity:0,init:function(){this.specificity=ie7CSS.Rule.score(this.selector)}})});
diff --git a/web/static/js/ie7/ie7-css2-selectors.js b/web/static/js/ie7/ie7-css2-selectors.js
deleted file mode 100644
index bb08da3..0000000
--- a/web/static/js/ie7/ie7-css2-selectors.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-IE7.addModule("ie7-css2-selectors",function(){cssQuery.addModule("css-level2",function(){selectors[">"]=function(r,f,t,n){var e,i,j;for(i=0;i<f.length;i++){var s=childElements(f[i]);for(j=0;(e=s[j]);j++)if(compareTagName(e,t,n))r.push(e)}};selectors["+"]=function(r,f,t,n){for(var i=0;i<f.length;i++){var e=nextElementSibling(f[i]);if(e&&compareTagName(e,t,n))r.push(e)}};selectors["@"]=function(r,f,a){var t=attributeSelectors[a].test;var e,i;for(i=0;(e=f[i]);i++)if(t(e))r.push(e)};pseudoClasses["first-child"]=function(e){return!previousElementSibling(e)};pseudoClasses["lang"]=function(e,c){c=new RegExp("^"+c,"i");while(e&&!e.getAttribute("lang"))e=e.parentNode;return e&&c.test(e.getAttribute("lang"))};AttributeSelector.NS_IE=/\\:/g;AttributeSelector.PREFIX="@";AttributeSelector.tests={};AttributeSelector.replace=function(m,a,n,c,v){var k=this.PREFIX+m;if(!attributeSelectors[k]){a=this.create(a,c||"",v||"");attributeSelectors[k]=a;attributeSelectors.push(a)}return attributeSele
 ctors[k].id};AttributeSelector.parse=function(s){s=s.replace(this.NS_IE,"|");var m;while(m=s.match(this.match)){var r=this.replace(m[0],m[1],m[2],m[3],m[4]);s=s.replace(this.match,r)}return s};AttributeSelector.create=function(p,t,v){var a={};a.id=this.PREFIX+attributeSelectors.length;a.name=p;t=this.tests[t];t=t?t(this.getAttribute(p),getText(v)):false;a.test=new Function("e","return "+t);return a};AttributeSelector.getAttribute=function(n){switch(n.toLowerCase()){case"id":return"e.id";case"class":return"e.className";case"for":return"e.htmlFor";case"href":if(isMSIE){return"String((e.outerHTML.match(/href=\\x22?([^\\s\\x22]*)\\x22?/)||[])[1]||'')"}}return"e.getAttribute('"+n.replace(N,":")+"')"};AttributeSelector.tests[""]=function(a){return a};AttributeSelector.tests["="]=function(a,v){return a+"=="+Quote.add(v)};AttributeSelector.tests["~="]=function(a,v){return"/(^| )"+regEscape(v)+"( |$)/.test("+a+")"};AttributeSelector.tests["|="]=function(a,v){return"/^"+regEscape(v)+"
 (-|$)/.test("+a+")"};var _6=parseSelector;parseSelector=function(s){return _6(AttributeSelector.parse(s))}});var AttributeSelector=cssQuery.valueOf("AttributeSelector");var H=/a(#[\w-]+)?(\.[\w-]+)?:(hover|active)/i;var B1=/\s*\{\s*/,B2=/\s*\}\s*/,C=/\s*\,\s*/;var F=/(.*)(:first-(line|letter))/;StyleSheet.prototype.specialize({parse:function(){this.inherit();var o=ie7CSS.rules.length;var ru=this.cssText.split(B2),r;var se,c,i,j;for(i=0;i<ru.length;i++){r=ru[i].split(B1);se=r[0].split(C);c=r[1];for(j=0;j<se.length;j++){se[j]=c?this.createRule(se[j],c):""}ru[i]=se.join("\n")}this.cssText=ru.join("\n");this.rules=ie7CSS.rules.slice(o)},recalc:function(){var r,i;for(i=0;(r=this.rules[i]);i++)r.recalc()},createRule:function(s,c){if(ie7CSS.UNKNOWN.test(s)){var m;if(m=s.match(PseudoElement.MATCH)){return new PseudoElement(m[1],m[2],c)}else if(m=s.match(DynamicRule.MATCH)){if(!isHTML||!H.test(m)||DynamicRule.COMPLEX.test(m)){return new DynamicRule(s,m[1],m[2],m[3],c)}}else return ne
 w Rule(s,c)}return s+" {"+c+"}"}});ie7CSS.specialize({rules:[],pseudoClasses:cssQuery.valueOf("pseudoClasses"),dynamicPseudoClasses:{},cache:cssQuery.valueOf("cache"),Rule:Rule,DynamicRule:DynamicRule,PseudoElement:PseudoElement,DynamicPseudoClass:DynamicPseudoClass,apply:function(){var p=this.pseudoClasses+"|before|after|"+this.dynamicPseudoClasses;p=p.replace(/(link|visited)\|/g,"");this.UNKNOWN=new RegExp("[>+~\[]|([:.])[\\w-()]+\\1|:("+p+")");var c="[^\\s(]+\\s*[+~]|@\\d+|:(";Rule.COMPLEX=new RegExp(c+p+")","g");DynamicRule.COMPLEX=new RegExp(c+this.pseudoClasses+")","g");DynamicRule.MATCH=new RegExp("(.*):("+this.dynamicPseudoClasses+")(.*)");PseudoElement.MATCH=/(.*):(before|after).*/;this.inherit()},recalc:function(){this.screen.recalc();this.inherit()},getText:function(s,p){return httpRequest?(loadFile(s.href,p)||s.cssText):this.inherit(s)},addEventHandler:function(e,t,h){addEventHandler(e,t,h)}});function Rule(s,c){this.id=ie7CSS.rules.length;this.className=Rule.PRE
 FIX+this.id;s=(s).match(F)||s||"*";this.selector=s[1]||s;this.selectorText=Rule.simple(this.selector)+"."+this.className+(s[2]||"");this.cssText=c;this.MATCH=new RegExp("\\s"+this.className+"(\\s|$)","g");ie7CSS.rules.push(this);this.init()};Common.specialize({constructor:Rule,toString:function(){return this.selectorText+" {"+this.cssText+"}"},init:DUMMY,add:function(e){e.className+=" "+this.className},remove:function(e){e.className=e.className.replace(this.MATCH,"$1")},recalc:function(){var m=ie7CSS.cache[" *."+this.className]=cssQuery(this.selector);for(i=0;i<m.length;i++)this.add(m[i])}});Rule.PREFIX="ie7_class";Rule.CHILD=/>/g;Rule.simple=function(s){s=AttributeSelector.parse(s);return s.replace(this.COMPLEX,"").replace(this.CHILD," ")};function DynamicRule(s,a,d,t,c){this.attach=a||"*";this.dynamicPseudoClass=ie7CSS.dynamicPseudoClasses[d];this.target=t;this.inherit(s,c)};Rule.specialize({constructor:DynamicRule,recalc:function(){var m=cssQuery(this.attach);for(var i=0;
 i<m.length;i++){var t=(this.target)?cssQuery(this.target,m[i]):[m[i]];if(t.length)this.dynamicPseudoClass.apply(m[i],t,this)}}});var A=/^attr/;var U=/^url\s*\(\s*([^)]*)\)$/;var M={before0:"beforeBegin",before1:"afterBegin",after0:"afterEnd",after1:"beforeEnd"};var _5=makePath("ie7-content.htc",path)+"?";HEADER+=".ie7_anon{display:none}";function PseudoElement(s,p,c){this.position=p;var co=c.match(PseudoElement.CONTENT),m,e;if(co){co=co[1];m=co.split(/\s+/);for(var i=0;(e=m[i]);i++){m[i]=A.test(e)?{attr:e.slice(5,-1)}:(e.charAt(0)=="'")?getString(e):decode(e)}co=m}this.content=co;this.inherit(s,decode(c))};Rule.specialize({constructor:PseudoElement,toString:function(){return"."+this.className+"{display:inline}"},init:function(){this.match=cssQuery(this.selector);for(var i=0;i<this.match.length;i++){var r=this.match[i].runtimeStyle;if(!r[this.position])r[this.position]={cssText:""};r[this.position].cssText+=";"+this.cssText;if(this.content!=null)r[this.position].content=this.
 content}},recalc:function(){if(this.content==null)return;for(var i=0;i<this.match.length;i++){this.create(this.match[i])}},create:function(t){var g=t.runtimeStyle[this.position];if(g){var c=[].concat(g.content||"");for(var j=0;j<c.length;j++){if(typeof c[j]=="object"){c[j]=t.getAttribute(c[j].attr)}}c=c.join("");var u=c.match(U);var h=PseudoElement[u?"OBJECT":"ANON"].replace(/%1/,this.className);var cs=g.cssText.replace(/'/g,'"');var po=M[this.position+Number(t.canHaveChildren)];if(u){var p=document.createElement(h);t.insertAdjacentElement(po,p);p.data=_5;addTimer(p,cs,Quote.remove(u[1]))}else{h=h.replace(/%2/,cs).replace(/%3/,c);t.insertAdjacentHTML(po,h)}t.runtimeStyle[this.position]=null}}});PseudoElement.CONTENT=/content\s*:\s*([^;]*)(;|$)/;PseudoElement.OBJECT="<object class='ie7_anon %1' ie7_anon width=100% height=0 type=text/x-scriptlet>";PseudoElement.ANON="<ie7:! class='ie7_anon %1' ie7_anon style='%2'>%3</ie7:!>";function DynamicPseudoClass(n,a){this.name=n;this.ap
 ply=a;this.instances={};ie7CSS.dynamicPseudoClasses[n]=this};Common.specialize({constructor:DynamicPseudoClass,register:function(i){var c=i[2];i.id=c.id+i[0].uniqueID;if(!this.instances[i.id]){var t=i[1],j;for(j=0;j<t.length;j++)c.add(t[j]);this.instances[i.id]=i}},unregister:function(i){if(this.instances[i.id]){var c=i[2];var t=i[1],j;for(j=0;j<t.length;j++)c.remove(t[j]);delete this.instances[i.id]}}});ie7CSS.pseudoClasses.toString=function(){var t=[],p;for(p in this){if(this[p].length>1)p+="\\([^)]*\\)";t.push(p)}return t.join("|")};ie7CSS.pseudoClasses["link"]=function(e){return e.currentStyle["ie7-link"]=="link"};ie7CSS.pseudoClasses["visited"]=function(e){return e.currentStyle["ie7-link"]=="visited"};var _4=(appVersion<5.5)?"onmouseover":"onmouseenter";var _3=(appVersion<5.5)?"onmouseout":"onmouseleave";ie7CSS.dynamicPseudoClasses.toString=ie7CSS.pseudoClasses.toString;var _0=new DynamicPseudoClass("hover",function(e){var i=arguments;ie7CSS.addEventHandler(e,_4,functio
 n(){_0.register(i)});ie7CSS.addEventHandler(e,_3,function(){_0.unregister(i)})});var _1=new DynamicPseudoClass("focus",function(e){var i=arguments;ie7CSS.addEventHandler(e,"onfocus",function(){_1.unregister(i);_1.register(i)});ie7CSS.addEventHandler(e,"onblur",function(){_1.unregister(i)});if(e==document.activeElement){_1.register(i)}});var _2=new DynamicPseudoClass("active",function(e){var i=arguments;ie7CSS.addEventHandler(e,"onmousedown",function(){_2.register(i)})});addEventHandler(document,"onmouseup",function(){var i=_2.instances,j;for(j in i)_2.unregister(i[j]);i=_0.instances;for(j in i)if(!i[j][0].contains(event.srcElement))_0.unregister(i[j])});ICommon(AttributeSelector);AttributeSelector.specialize({getAttribute:function(n){switch(n.toLowerCase()){case"class":return"e.className.replace(/\\b\\s*ie7_class\\d+/g,'')";case"src":return"(e.pngSrc||e.src)"}return this.inherit(n)}});encoder.add(/::/,":");safeString.add(/\\([\da-fA-F]{1,4})/,function(m,o){m=m[o+1];return"\\
 u"+"0000".slice(m.length)+m})});
diff --git a/web/static/js/ie7/ie7-css3-selectors.js b/web/static/js/ie7/ie7-css3-selectors.js
deleted file mode 100644
index 7337b82..0000000
--- a/web/static/js/ie7/ie7-css3-selectors.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-IE7.addModule("ie7-css3-selectors",function(){cssQuery.addModule("css-level3",function(){selectors["~"]=function(r,f,t,n){var e,i;for(i=0;(e=f[i]);i++){while(e=nextElementSibling(e)){if(compareTagName(e,t,n))r.push(e)}}};pseudoClasses["contains"]=function(e,t){t=new RegExp(regEscape(getText(t)));return t.test(getTextContent(e))};pseudoClasses["root"]=function(e){return e==getDocument(e).documentElement};pseudoClasses["empty"]=function(e){var n,i;for(i=0;(n=e.childNodes[i]);i++){if(thisElement(n)||n.nodeType==3)return false}return true};pseudoClasses["last-child"]=function(e){return!nextElementSibling(e)};pseudoClasses["only-child"]=function(e){e=e.parentNode;return firstElementChild(e)==lastElementChild(e)};pseudoClasses["not"]=function(e,s){var n=cssQuery(s,getDocument(e));for(var i=0;i<n.length;i++){if(n[i]==e)return false}return true};pseudoClasses["nth-child"]=function(e,a){return nthChild(e,a,previousElementSibling)};pseudoClasses["nth-last-child"]=function(e,a){return 
 nthChild(e,a,nextElementSibling)};pseudoClasses["target"]=function(e){return e.id==location.hash.slice(1)};pseudoClasses["checked"]=function(e){return e.checked};pseudoClasses["enabled"]=function(e){return e.disabled===false};pseudoClasses["disabled"]=function(e){return e.disabled};pseudoClasses["indeterminate"]=function(e){return e.indeterminate};AttributeSelector.tests["^="]=function(a,v){return"/^"+regEscape(v)+"/.test("+a+")"};AttributeSelector.tests["$="]=function(a,v){return"/"+regEscape(v)+"$/.test("+a+")"};AttributeSelector.tests["*="]=function(a,v){return"/"+regEscape(v)+"/.test("+a+")"};function nthChild(e,a,t){switch(a){case"n":return true;case"even":a="2n";break;case"odd":a="2n+1"}var ch=childElements(e.parentNode);function _5(i){var i=(t==nextElementSibling)?ch.length-i:i-1;return ch[i]==e};if(!isNaN(a))return _5(a);a=a.split("n");var m=parseInt(a[0]);var s=parseInt(a[1]);if((isNaN(m)||m==1)&&s==0)return true;if(m==0&&!isNaN(s))return _5(s);if(isNaN(s))s=0;var c
 =1;while(e=t(e))c++;if(isNaN(m)||m==1)return(t==nextElementSibling)?(c<=s):(s>=c);return(c%m)==s}});var firstElementChild=cssQuery.valueOf("firstElementChild");ie7CSS.pseudoClasses["root"]=function(e){return(e==viewport)||(!isHTML&&e==firstElementChild(body))};var _4=new ie7CSS.DynamicPseudoClass("checked",function(e){if(typeof e.checked!="boolean")return;var i=arguments;ie7CSS.addEventHandler(e,"onpropertychange",function(){if(event.propertyName=="checked"){if(e.checked)_4.register(i);else _4.unregister(i)}});if(e.checked)_4.register(i)});var _3=new ie7CSS.DynamicPseudoClass("enabled",function(e){if(typeof e.disabled!="boolean")return;var i=arguments;ie7CSS.addEventHandler(e,"onpropertychange",function(){if(event.propertyName=="disabled"){if(!e.isDisabled)_3.register(i);else _3.unregister(i)}});if(!e.isDisabled)_3.register(i)});var _2=new ie7CSS.DynamicPseudoClass("disabled",function(e){if(typeof e.disabled!="boolean")return;var i=arguments;ie7CSS.addEventHandler(e,"onprope
 rtychange",function(){if(event.propertyName=="disabled"){if(e.isDisabled)_2.register(i);else _2.unregister(i)}});if(e.isDisabled)_2.register(i)});var _1=new ie7CSS.DynamicPseudoClass("indeterminate",function(e){if(typeof e.indeterminate!="boolean")return;var i=arguments;ie7CSS.addEventHandler(e,"onpropertychange",function(){if(event.propertyName=="indeterminate"){if(e.indeterminate)_1.register(i);else _1.unregister(i)}});ie7CSS.addEventHandler(e,"onclick",function(){_1.unregister(i)})});var _0=new ie7CSS.DynamicPseudoClass("target",function(e){var i=arguments;if(!e.tabIndex)e.tabIndex=0;ie7CSS.addEventHandler(document,"onpropertychange",function(){if(event.propertyName=="activeElement"){if(e.id==location.hash.slice(1))_0.register(i);else _0.unregister(i)}});if(e.id==location.hash.slice(1))_0.register(i)});decoder.add(/\|/,"\\:")});
diff --git a/web/static/js/ie7/ie7-dhtml.js b/web/static/js/ie7/ie7-dhtml.js
deleted file mode 100644
index d768063..0000000
--- a/web/static/js/ie7/ie7-dhtml.js
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-IE7.addModule("ie7-dhtml", function() {
-
-/* ---------------------------------------------------------------------
-  This module is still in development and should not be used.
---------------------------------------------------------------------- */
-
-ie7CSS.specialize("recalc", function() {
-	this.inherit();
-	for (var i = 0; i < this.recalcs.length; i++) {
-		var $recalc = this.recalcs[i];
-		for (var j = 0; i < $recalc[3].length; i++) {
-			_addPropertyChangeHandler($recalc[3][j], _getPropertyName($recalc[2]), $recalc[1]);
-		}
-	}
-});
-
-// constants
-var _PATTERNS = {
-	width: "(width|paddingLeft|paddingRight|borderLeftWidth|borderRightWidth|borderLeftStyle|borderRightStyle)",
-	height:	"(height|paddingTop|paddingBottom|borderTopHeight|borderBottomHeight|borderTopStyle|borderBottomStyle)"
-};
-var _PROPERTY_NAMES = {
-	width: "fixedWidth",
-	height: "fixedHeight",
-	right: "width",
-	bottom: "height"
-};
-var _DASH_LETTER = /-(\w)/g;
-var _PROPERTY_NAME = /\w+/;
-
-function _addPropertyChangeHandler($element, $propertyName, $fix) {
-	addEventHandler($element, "onpropertychange", function() {
-		if (_getPattern($propertyName).test(event.propertyName)) {
-			_reset($element, $propertyName);
-			$fix($element);
-		}
-	});
-};
-function _upper($match, $letter) {return $letter.toUpperCase()};
-function _getPropertyName($pattern) {
-	return String(String($pattern).toLowerCase().replace(_DASH_LETTER, _upper).match(_PROPERTY_NAME));
-};
-function _getPattern($propertyName) {
-	return eval("/^style." + (_PATTERNS[$propertyName] || $propertyName) + "$/");
-};
-function _reset($element, $propertyName) {
-	$element.runtimeStyle[$propertyName] = "";
-	$propertyName = _PROPERTY_NAMES[$propertyName]
-	if ($propertyName) $element.runtimeStyle[$propertyName] = "";
-};
-
-});
diff --git a/web/static/js/ie7/ie7-dynamic-attributes.js b/web/static/js/ie7/ie7-dynamic-attributes.js
deleted file mode 100644
index e066911..0000000
--- a/web/static/js/ie7/ie7-dynamic-attributes.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-IE7.addModule("ie7-dynamic-attributes",function(){if(!modules["ie7-css2-selectors"])return;var attributeSelectors=cssQuery.valueOf("attributeSelectors");var parseSelector=cssQuery.valueOf("parseSelector");function DynamicAttribute(s,a,d,t,c){this.attach=a||"*";parseSelector(d);this.dynamicAttribute=attributeSelectors["@"+d];this.target=t;this.inherit(s,c)};ie7CSS.Rule.specialize({constructor:DynamicAttribute,recalc:function(){var m=cssQuery(this.attach);for(var i=0;i<m.length;i++){var t=(this.target)?cssQuery(this.target,m[i]):[m[i]];if(t.length)this.apply(m[i],t)}},apply:function(e,t){var self=this;addEventHandler(e,"onpropertychange",function(){if(event.propertyName==self.dynamicAttribute.name)self.test(e,t)});this.test(e,t)},test:function(e,t){var a=this.dynamicAttribute.test(e)?"add":"remove";for(var i=0;(e=t[i]);i++)this[a](e)}});DynamicAttribute.MATCH=/(.*)(\[[^\]]*\])(.*)/;StyleSheet.prototype.specialize({createRule:function(s,c){var m;if(m=s.match(DynamicAttribute.MA
 TCH)){return new DynamicAttribute(s,m[1],m[2],m[3],c)}else return this.inherit(s,c)}})});
diff --git a/web/static/js/ie7/ie7-fixed.js b/web/static/js/ie7/ie7-fixed.js
deleted file mode 100644
index 10d629f..0000000
--- a/web/static/js/ie7/ie7-fixed.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-IE7.addModule("ie7-fixed",function(){ie7CSS.addRecalc("position","fixed",_6,"absolute");ie7CSS.addRecalc("background(-attachment)?","[^};]*fixed",_7);var _10=(quirksMode)?"body":"documentElement";var _8=function(){if(body.currentStyle.backgroundAttachment!="fixed"){if(body.currentStyle.backgroundImage=="none"){body.runtimeStyle.backgroundRepeat="no-repeat";body.runtimeStyle.backgroundImage="url("+BLANK_GIF+")"}body.runtimeStyle.backgroundAttachment="fixed"}_8=DUMMY};var _0=createTempElement("img");function _1(f){return _2.exec(String(f))};var _2=new ParseMaster;_2.add(/Left/,"Top");_2.add(/left/,"top");_2.add(/Width/,"Height");_2.add(/width/,"height");_2.add(/right/,"bottom");_2.add(/X/,"Y");function _3(e){return(e)?isFixed(e)||_3(e.parentElement):false};function setExpression(e,p,ex){setTimeout("document.all."+e.uniqueID+".runtimeStyle.setExpression('"+p+"','"+ex+"')",0)};function _7(e){if(register(_7,e,e.currentStyle.backgroundAttachment=="fixed"&&!e.contains(body))){_8();
 backgroundLeft(e);backgroundTop(e);_9(e)}};function _9(e){_0.src=e.currentStyle.backgroundImage.slice(5,-2);var p=(e.canHaveChildren)?e:e.parentElement;p.appendChild(_0);setOffsetLeft(e);setOffsetTop(e);p.removeChild(_0)};function backgroundLeft(e){e.style.backgroundPositionX=e.currentStyle.backgroundPositionX;if(!_3(e)){var ex="(parseInt(runtimeStyle.offsetLeft)+document."+_10+".scrollLeft)||0";setExpression(e,"backgroundPositionX",ex)}};eval(_1(backgroundLeft));function setOffsetLeft(e){var p=_3(e)?"backgroundPositionX":"offsetLeft";e.runtimeStyle[p]=getOffsetLeft(e,e.style.backgroundPositionX)-e.getBoundingClientRect().left-e.clientLeft+2};eval(_1(setOffsetLeft));function getOffsetLeft(e,p){switch(p){case"left":case"top":return 0;case"right":case"bottom":return viewport.clientWidth-_0.offsetWidth;case"center":return(viewport.clientWidth-_0.offsetWidth)/2;default:if(PERCENT.test(p)){return parseInt((viewport.clientWidth-_0.offsetWidth)*parseFloat(p)/100)}_0.style.left=p;re
 turn _0.offsetLeft}};eval(_1(getOffsetLeft));function _6(e){if(register(_6,e,isFixed(e))){setOverrideStyle(e,"position","absolute");setOverrideStyle(e,"left",e.currentStyle.left);setOverrideStyle(e,"top",e.currentStyle.top);_8();if(ie7Layout)ie7Layout.fixRight(e);_5(e)}};function _5(e,r){positionTop(e,r);positionLeft(e,r,true);if(!e.runtimeStyle.autoLeft&&e.currentStyle.marginLeft=="auto"&&e.currentStyle.right!="auto"){var l=viewport.clientWidth-getPixelWidth(e,e.currentStyle.right)-getPixelWidth(e,e.runtimeStyle._12)-e.clientWidth;if(e.currentStyle.marginRight=="auto")l=parseInt(l/2);if(_3(e.offsetParent))e.runtimeStyle.pixelLeft+=l;else e.runtimeStyle.shiftLeft=l}clipWidth(e);clipHeight(e)};function clipWidth(e){if(e.currentStyle.width!="auto"){var r=e.getBoundingClientRect();var w=e.offsetWidth-viewport.clientWidth+r.left-2;if(w>=0){w=Math.max(getPixelValue(e,e.currentStyle.width)-w,0);setOverrideStyle(e,"width",w)}}};eval(_1(clipWidth));function positionLeft(e,r){if(!r&&
 PERCENT.test(e.currentStyle.width)){e.runtimeStyle.fixWidth=e.currentStyle.width}if(e.runtimeStyle.fixWidth){e.runtimeStyle.width=getPixelWidth(e,e.runtimeStyle.fixWidth)}if(r){if(!e.runtimeStyle.autoLeft)return}else{e.runtimeStyle.shiftLeft=0;e.runtimeStyle._12=e.currentStyle.left;e.runtimeStyle.autoLeft=e.currentStyle.right!="auto"&&e.currentStyle.left=="auto"}e.runtimeStyle.left="";e.runtimeStyle.screenLeft=getScreenLeft(e);e.runtimeStyle.pixelLeft=e.runtimeStyle.screenLeft;if(!r&&!_3(e.offsetParent)){var ex="runtimeStyle.screenLeft+runtimeStyle.shiftLeft+document."+_10+".scrollLeft";setExpression(e,"pixelLeft",ex)}};eval(_1(positionLeft));function getScreenLeft(e){var s=e.offsetLeft,n=1;if(e.runtimeStyle.autoLeft){s=viewport.clientWidth-e.offsetWidth-getPixelWidth(e,e.currentStyle.right)}if(e.currentStyle.marginLeft!="auto"){s-=getPixelWidth(e,e.currentStyle.marginLeft)}while(e=e.offsetParent){if(e.currentStyle.position!="static")n=-1;s+=e.offsetLeft*n}return s};eval(_1(
 getScreenLeft));function getPixelWidth(e,v){if(PERCENT.test(v))return parseInt(parseFloat(v)/100*viewport.clientWidth);return getPixelValue(e,v)};eval(_1(getPixelWidth));function _11(){var e=_7.elements;for(var i in e)_9(e[i]);e=_6.elements;for(i in e){_5(e[i],true);_5(e[i],true)}_4=0};var _4;addResize(function(){if(!_4)_4=setTimeout(_11,0)})});
\ No newline at end of file
diff --git a/web/static/js/ie7/ie7-graphics.js b/web/static/js/ie7/ie7-graphics.js
deleted file mode 100644
index 7e63c47..0000000
--- a/web/static/js/ie7/ie7-graphics.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-IE7.addModule("ie7-graphics",function(){if(appVersion<5.5)return;var A="DXImageTransform.Microsoft.AlphaImageLoader";var F="progid:"+A+"(src='%1',sizingMethod='scale')";var _3=new RegExp((window.IE7_PNG_SUFFIX||"-trans.png")+"$","i");var _0=[];function _2(e){var f=e.filters[A];if(f){f.src=e.src;f.enabled=true}else{e.runtimeStyle.filter=F.replace(/%1/,e.src);_0.push(e)}e.src=BLANK_GIF};function _5(e){e.src=e.pngSrc;e.filters[A].enabled=false};ie7CSS.addFix(/opacity\s*:\s*([\d.]+)/,function(m,o){return"zoom:1;filter:progid:DXImageTransform.Microsoft.Alpha(opacity="+((parseFloat(m[o+1])*100)||1)+")"});var B=/background(-image)?\s*:\s*([^\(};]*)url\(([^\)]+)\)([^;}]*)/;ie7CSS.addFix(B,function(m,o){var u=getString(m[o+3]);return _3.test(u)?"filter:"+F.replace(/scale/,"crop").replace(/%1/,u)+";zoom:1;background"+(m[o+1]||"")+":"+(m[o+2]||"")+"none"+(m[o+4]||""):m[o]});if(ie7HTML){ie7HTML.addRecalc("img,input",function(e){if(e.tagName=="INPUT"&&e.type!="image")return;_4(e);addEven
 tHandler(e,"onpropertychange",function(){if(!_1&&event.propertyName=="src"&&e.src.indexOf(BLANK_GIF)==-1)_4(e)})});var B64=/^data:.*;base64/i;var _7=makePath("ie7-base64.php",path);function _4(e){if(_3.test(e.src)){var i=new Image(e.width,e.height);i.onload=function(){e.width=i.width;e.height=i.height;i=null};i.src=e.src;e.pngSrc=e.src;_2(e)}else if(B64.test(e.src)){e.src=_7+"?"+e.src.slice(5)}};var I=/^image/i;var _6=makePath("ie7-object.htc",path);ie7HTML.addRecalc("object",function(e){if(I.test(e.type)){var o=document.createElement("<object type=text/x-scriptlet>");o.style.width=e.currentStyle.width;o.style.height=e.currentStyle.height;o.data=_6;var u=makePath(e.data,getPath(location.href));e.parentNode.replaceChild(o,e);cssQuery.clearCache("object");addTimer(o,"",u);return o}})}var _1=false;addEventHandler(window,"onbeforeprint",function(){_1=true;for(var i=0;i<_0.length;i++)_5(_0[i])});addEventHandler(window,"onafterprint",function(){for(var i=0;i<_0.length;i++)_2(_0[i]
 );_1=false})});
diff --git a/web/static/js/ie7/ie7-html4.js b/web/static/js/ie7/ie7-html4.js
deleted file mode 100644
index 3fcd891..0000000
--- a/web/static/js/ie7/ie7-html4.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-IE7.addModule("ie7-html4",function(){if(!isHTML)return;HEADER+="h1{font-size:2em}h2{font-size:1.5em;}h3{font-size:1.17em;}"+"h4{font-size:1em}h5{font-size:.83em}h6{font-size:.67em}";var _0={};ie7HTML=new(Fix.specialize({init:DUMMY,addFix:function(){this.fixes.push(arguments)},apply:function(){for(var i=0;i<this.fixes.length;i++){var m=cssQuery(this.fixes[i][0]);var f=this.fixes[i][1]||_1;for(var j=0;j<m.length;j++)f(m[j])}},addRecalc:function(){this.recalcs.push(arguments)},recalc:function(){for(var i=0;i<this.recalcs.length;i++){var m=cssQuery(this.recalcs[i][0]);var r=this.recalcs[i][1],e;var k=Math.pow(2,i);for(var j=0;(e=m[j]);j++){var u=e.uniqueID;if((_0[u]&k)==0){e=r(e)||e;_0[u]|=k}}}}}));ie7HTML.addFix("abbr");ie7HTML.addRecalc("label",function(e){if(!e.htmlFor){var f=cssQuery("input,textarea",e)[0];if(f){addEventHandler(e,"onclick",function(){f.click()})}}});ie7HTML.addRecalc("button,input",function(e){if(e.tagName=="BUTTON"){var m=e.outerHTML.match(/ value="([^"]*)"
 /i);e.runtimeStyle.value=(m)?m[1]:""}if(e.type=="submit"){addEventHandler(e,"onclick",function(){e.runtimeStyle.clicked=true;setTimeout("document.all."+e.uniqueID+".runtimeStyle.clicked=false",1)})}});var U=/^(submit|reset|button)$/;ie7HTML.addRecalc("form",function(e){addEventHandler(e,"onsubmit",function(){for(var i=0;i<e.length;i++){if(_2(e[i])){e[i].disabled=true;setTimeout("document.all."+e[i].uniqueID+".disabled=false",1)}else if(e[i].tagName=="BUTTON"&&e[i].type=="submit"){setTimeout("document.all."+e[i].uniqueID+".value='"+e[i].value+"'",1);e[i].value=e[i].runtimeStyle.value}}})});function _2(e){return U.test(e.type)&&!e.disabled&&!e.runtimeStyle.clicked};ie7HTML.addRecalc("img",function(e){if(e.alt&&!e.title)e.title=""});var P=(appVersion<5.5)?"HTML:":"";function _1(e){var f=document.createElement("<"+P+e.outerHTML.slice(1));if(e.outerHTML.slice(-2)!="/>"){var en="</"+e.tagName+">",n;while((n=e.nextSibling)&&n.outerHTML!=en){f.appendChild(n)}if(n)n.removeNode()}e.pa
 rentNode.replaceChild(f,e)}});
diff --git a/web/static/js/ie7/ie7-ie5.js b/web/static/js/ie7/ie7-ie5.js
deleted file mode 100644
index a07fd98..0000000
--- a/web/static/js/ie7/ie7-ie5.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-if(appVersion<5.5){ANON="HTML:!";var ap=function(f,o,a){f.apply(o,a)};if(''.replace(/^/,String)){var _0=String.prototype.replace;var _1=function(e,r){var m,n="",s=this;while(s&&(m=e.exec(s))){n+=s.slice(0,m.index)+ap(r,this,m);s=s.slice(m.lastIndex)}return n+s};String.prototype.replace=function(e,r){this.replace=(typeof r=="function")?_1:_0;return this.replace(e,r)}}if(!Function.apply){var APPLY="apply-"+Number(new Date);ap=function(f,o,a){var r;o[APPLY]=f;switch(a.length){case 0:r=o[APPLY]();break;case 1:r=o[APPLY](a[0]);break;case 2:r=o[APPLY](a[0],a[1]);break;case 3:r=o[APPLY](a[0],a[1],a[2]);break;case 4:r=o[APPLY](a[0],a[1],a[2],a[3]);break;default:var aa=[],i=a.length-1;do aa[i]="a["+i+"]";while(i--);eval("r=o[APPLY]("+aa+")")}delete o[APPLY];return r};ICommon.valueOf.prototype.inherit=function(){return ap(arguments.callee.caller.ancestor,this,arguments)}}if(![].push)Array.prototype.push=function(){for(var i=0;i<arguments.length;i++){this[this.length]=arguments[i]}retu
 rn this.length};if(![].pop)Array.prototype.pop=function(){var i=this[this.length-1];this.length--;return i};if(isHTML){HEADER+="address,blockquote,body,dd,div,dt,fieldset,form,"+"frame,frameset,h1,h2,h3,h4,h5,h6,iframe,noframes,object,p,"+"hr,applet,center,dir,menu,pre,dl,li,ol,ul{display:block}"}}
diff --git a/web/static/js/ie7/ie7-layout.js b/web/static/js/ie7/ie7-layout.js
deleted file mode 100644
index d1b64eb..0000000
--- a/web/static/js/ie7/ie7-layout.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-IE7.addModule("ie7-layout",function(){ie7Layout=this;HEADER+="*{boxSizing:content-box}";this.hasLayout=(appVersion<5.5)?function(e){return e.clientWidth}:function(e){return e.currentStyle.hasLayout};this.boxSizing=function(e){if(!ie7Layout.hasLayout(e)){e.style.height="0cm";if(e.currentStyle.verticalAlign=="auto")e.runtimeStyle.verticalAlign="top";_1(e)}};function _1(e){if(e!=viewport&&e.currentStyle.position!="absolute"){collapseMarginTop(e);collapseMarginBottom(e)}};var firstElementChild=cssQuery.valueOf("firstElementChild");var lastElementChild=cssQuery.valueOf("lastElementChild");function collapseMarginTop(e){if(!e.runtimeStyle.marginTop){var p=e.parentElement;if(p&&ie7Layout.hasLayout(p)&&e==firstElementChild(p))return;var f=firstElementChild(e);if(f&&f.currentStyle.styleFloat=="none"&&ie7Layout.hasLayout(f)){collapseMarginTop(f);m=_3(e,e.currentStyle.marginTop);c=_3(f,f.currentStyle.marginTop);if(m<0||c<0){e.runtimeStyle.marginTop=m+c}else{e.runtimeStyle.marginTop=Math
 .max(c,m)}f.runtimeStyle.marginTop="0px"}}};eval(String(collapseMarginTop).replace(/Top/g,"Bottom").replace(/first/g,"last"));function _3(e,v){return(v=="auto")?0:getPixelValue(e,v)};var U=/^[.\d][\w%]*$/,A=/^(auto|0cm)$/,N="[.\\d]";var applyWidth,applyHeight;function borderBox(e){applyWidth(e);applyHeight(e)};function fixWidth(H){applyWidth=function(e){if(!PERCENT.test(e.currentStyle.width))fixWidth(e);_1(e)};function fixWidth(e,v){if(!e.runtimeStyle.fixedWidth){if(!v)v=e.currentStyle.width;e.runtimeStyle.fixedWidth=(U.test(v))?Math.max(0,getFixedWidth(e,v)):v;setOverrideStyle(e,"width",e.runtimeStyle.fixedWidth)}};function layoutWidth(e){if(!isFixed(e)){var l=e.offsetParent;while(l&&!ie7Layout.hasLayout(l))l=l.offsetParent}return(l||viewport).clientWidth};function getPixelWidth(e,v){if(PERCENT.test(v))return parseInt(parseFloat(v)/100*layoutWidth(e));return getPixelValue(e,v)};var getFixedWidth=function(e,v){var b=e.currentStyle["box-sizing"]=="border-box";var a=0;if(quirk
 sMode&&!b)a+=getBorderWidth(e)+getPaddingWidth(e);else if(!quirksMode&&b)a-=getBorderWidth(e)+getPaddingWidth(e);return getPixelWidth(e,v)+a};function getBorderWidth(e){return e.offsetWidth-e.clientWidth};function getPaddingWidth(e){return getPixelWidth(e,e.currentStyle.paddingLeft)+getPixelWidth(e,e.currentStyle.paddingRight)};eval(String(getPaddingWidth).replace(/padding/g,"margin").replace(/Padding/g,"Margin"));HEADER+="*{minWidth:none;maxWidth:none;min-width:none;max-width:none}";function minWidth(e){if(e.currentStyle["min-width"]!=null){e.style.minWidth=e.currentStyle["min-width"]}if(register(minWidth,e,e.currentStyle.minWidth!="none")){ie7Layout.boxSizing(e);fixWidth(e);resizeWidth(e)}};eval(String(minWidth).replace(/min/g,"max"));ie7Layout.minWidth=minWidth;ie7Layout.maxWidth=maxWidth;function resizeWidth(e){var r=e.getBoundingClientRect();var w=r.right-r.left;if(e.currentStyle.minWidth!="none"&&w<=getFixedWidth(e,e.currentStyle.minWidth)){e.runtimeStyle.width=getFixe
 dWidth(e,e.currentStyle.minWidth)}else if(e.currentStyle.maxWidth!="none"&&w>=getFixedWidth(e,e.currentStyle.maxWidth)){e.runtimeStyle.width=getFixedWidth(e,e.currentStyle.maxWidth)}else{e.runtimeStyle.width=e.runtimeStyle.fixedWidth}};function fixRight(e){if(register(fixRight,e,/^(fixed|absolute)$/.test(e.currentStyle.position)&&getDefinedStyle(e,"left")!="auto"&&getDefinedStyle(e,"right")!="auto"&&A.test(getDefinedStyle(e,"width")))){resizeRight(e);ie7Layout.boxSizing(e)}};ie7Layout.fixRight=fixRight;function resizeRight(e){var l=getPixelWidth(e,e.runtimeStyle._4||e.currentStyle.left);var w=layoutWidth(e)-getPixelWidth(e,e.currentStyle.right)-l-getMarginWidth(e);if(parseInt(e.runtimeStyle.width)==w)return;e.runtimeStyle.width="";if(isFixed(e)||H||e.offsetWidth<w){if(!quirksMode)w-=getBorderWidth(e)+getPaddingWidth(e);if(w<0)w=0;e.runtimeStyle.fixedWidth=w;setOverrideStyle(e,"width",w)}};var _2=0;addResize(function(){var i,w=(_2<viewport.clientWidth);_2=viewport.clientWidth
 ;for(i in minWidth.elements){var e=minWidth.elements[i];var f=(parseInt(e.runtimeStyle.width)==getFixedWidth(e,e.currentStyle.minWidth));if(w&&f)e.runtimeStyle.width="";if(w==f)resizeWidth(e)}for(i in maxWidth.elements){var e=maxWidth.elements[i];var f=(parseInt(e.runtimeStyle.width)==getFixedWidth(e,e.currentStyle.maxWidth));if(!w&&f)e.runtimeStyle.width="";if(w!=f)resizeWidth(e)}for(i in fixRight.elements)resizeRight(fixRight.elements[i])});if(window.IE7_BOX_MODEL!==false){ie7CSS.addRecalc("width",N,quirksMode?applyWidth:_1)}ie7CSS.addRecalc("min-width",N,minWidth);ie7CSS.addRecalc("max-width",N,maxWidth);ie7CSS.addRecalc("right",N,fixRight)};ie7CSS.addRecalc("border-spacing",N,function(e){if(e.currentStyle.borderCollapse!="collapse"){e.cellSpacing=getPixelValue(e,e.currentStyle["border-spacing"])}});ie7CSS.addRecalc("box-sizing","content-box",this.boxSizing);ie7CSS.addRecalc("box-sizing","border-box",borderBox);var _0=new ParseMaster;_0.add(/Width/,"Height");_0.add(/width
 /,"height");_0.add(/Left/,"Top");_0.add(/left/,"top");_0.add(/Right/,"Bottom");_0.add(/right/,"bottom");eval(_0.exec(String(fixWidth)));fixWidth();fixHeight(true)});
diff --git a/web/static/js/ie7/ie7-load.htc b/web/static/js/ie7/ie7-load.htc
deleted file mode 100644
index a6f1e7f..0000000
--- a/web/static/js/ie7/ie7-load.htc
+++ /dev/null
@@ -1 +0,0 @@
-<component lightweight="true"><attach event="ondocumentready" onevent="IE7.init()"/></component>
diff --git a/web/static/js/ie7/ie7-object.htc b/web/static/js/ie7/ie7-object.htc
deleted file mode 100644
index 392409e..0000000
--- a/web/static/js/ie7/ie7-object.htc
+++ /dev/null
@@ -1,12 +0,0 @@
-<html>
-<!--
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
--->
-<head>
-<style type="text/css">body{margin:0}</style>
-<script type="text/javascript">public_description=new function(){var l=false;this.ie7_anon=true;this.load=function(o,c,u){if(l)return;l=true;function _0(t,p){t.style[p]=o.currentStyle[p]};var p=o;while(p&&p.currentStyle.backgroundColor=="transparent"){p=p.parentElement}if(p)body.style.backgroundColor=p.currentStyle.backgroundColor;_0(body,"backgroundImage");_0(body,"backgroundRepeat");_0(body,"backgroundPositionX");_0(body,"backgroundPositionY");_0(body,"fontFamily");_0(body,"fontSize");_0(wrapper,"paddingTop");_0(wrapper,"paddingRight");_0(wrapper,"paddingBottom");_0(wrapper,"paddingLeft");image.width=o.clientWidth;image.height=o.clientHeight;var B64=/^data:.*;base64/i,P=/.png$/i;if(B64.test(u))u="ie7-base64.php"+"?"+u.slice(5);if(P.test(u)&&!/MSIE 5.0/.test(navigator.userAgent)){image.src="blank.gif";image.style.filter="progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+u+"',sizingMethod='scale')"}else{image.src=u}o.style.width=body.scrollWidth;o.style.height=body.s
 crollHeight}};</script>
-</head>
-<body id="body"><div id="wrapper"><img id="image"></div></body>
-</html>
diff --git a/web/static/js/ie7/ie7-overflow.js b/web/static/js/ie7/ie7-overflow.js
deleted file mode 100644
index ad2e030..0000000
--- a/web/static/js/ie7/ie7-overflow.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-IE7.addModule("ie7-overflow",function(){var S={backgroundColor:"transparent",backgroundImage:"none",backgroundPositionX:null,backgroundPositionY:null,backgroundRepeat:null,borderTopWidth:0,borderRightWidth:0,borderBottomWidth:0,borderLeftStyle:"none",borderTopStyle:"none",borderRightStyle:"none",borderBottomStyle:"none",borderLeftWidth:0,height:null,marginTop:0,marginBottom:0,marginRight:0,marginLeft:0,width:"100%"};function _3(p,s,t){t.style[p]=s.currentStyle[p];if(S[p]!=null){s.runtimeStyle[p]=S[p]}};ie7CSS.addRecalc("overflow","visible",function(e){if(e.parentNode.ie7_wrapper)return;if(ie7Layout&&e.currentStyle["max-height"]!="auto"){ie7Layout.maxHeight(e)}if(e.currentStyle.marginLeft=="auto")e.style.marginLeft=0;if(e.currentStyle.marginRight=="auto")e.style.marginRight=0;var w=document.createElement(ANON);w.ie7_wrapper=true;for(var p in S)_3(p,e,w);w.style.display="block";w.style.position="relative";e.runtimeStyle.position="absolute";e.parentNode.insertBefore(w,e);w.appe
 ndChild(e)});cssQuery.addModule("ie7-overflow",function(){function _0(e){return(e&&e.ie7_wrapper)?e.firstChild:e};var _2=previousElementSibling;previousElementSibling=function(e){return _0(_2(e))};var _1=nextElementSibling;nextElementSibling=function(e){return _0(_1(e))};selectors[" "]=function(r,f,t,n){var e,i,j;for(i=0;i<f.length;i++){var s=getElementsByTagName(f[i],t,n);for(j=0;(e=_0(s[j]));j++){if(thisElement(e)&&(!n||compareNamespace(e,n)))r.push(e)}}};selectors[">"]=function(r,f,t,n){var e,i,j;for(i=0;i<f.length;i++){var s=childElements(f[i]);for(j=0;(e=_0(s[j]));j++){if(compareTagName(e,t,n))r.push(e)}}}})});
diff --git a/web/static/js/ie7/ie7-quirks.js b/web/static/js/ie7/ie7-quirks.js
deleted file mode 100644
index a1314dd..0000000
--- a/web/static/js/ie7/ie7-quirks.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-IE7.addModule("ie7-quirks",function(){if(quirksMode){var F="xx-small,x-small,small,medium,large,x-large,xx-large".split(",");for(var i=0;i<F.length;i++){F[F[i]]=F[i-1]||"0.67em"}ie7CSS.addFix(new RegExp("(font(-size)?\\s*:\\s*)([\\w\\-\\.]+)"),function(m,o){return m[o+1]+(F[m[o+3]]||m[o+3])});if(appVersion<6){var N=/^\-/,L=/(em|ex)$/i;var EM=/em$/i,EX=/ex$/i;function _2(e){var s=1;_0.style.fontFamily=e.currentStyle.fontFamily;_0.style.lineHeight=e.currentStyle.lineHeight;while(e!=body){var f=e.currentStyle["ie7-font-size"];if(f){if(EM.test(f))s*=parseFloat(f);else if(PERCENT.test(f))s*=(parseFloat(f)/100);else if(EX.test(f))s*=(parseFloat(f)/2);else{_0.style.fontSize=f;return 1}}e=e.parentElement}return s};var _0=createTempElement();getPixelValue=function(e,v){if(PIXEL.test(v||0))return parseInt(v||0);var scale=N.test(v)?-1:1;if(L.test(v))scale*=_2(e);_0.style.width=(scale<0)?v.slice(1):v;body.appendChild(_0);v=scale*_0.offsetWidth;_0.removeNode();return parseInt(v)};HEADER=
 HEADER.replace(/(font(-size)?\s*:\s*([^\s;}\/]*))/gi,"ie7-font-size:$3;$1");ie7CSS.addFix(/cursor\s*:\s*pointer/,"cursor:hand");ie7CSS.addFix(/display\s*:\s*list-item/,"display:block")}function getPaddingWidth(e){return getPixelValue(e,e.currentStyle.paddingLeft)+getPixelValue(e,e.currentStyle.paddingRight)};function _1(e){if(appVersion<5.5&&ie7Layout)ie7Layout.boxSizing(e.parentElement);var p=e.parentElement;var m=p.offsetWidth-e.offsetWidth-getPaddingWidth(p);var a=(e.currentStyle["ie7-margin"]&&e.currentStyle.marginRight=="auto")||e.currentStyle["ie7-margin-right"]=="auto";switch(p.currentStyle.textAlign){case"right":m=(a)?parseInt(m/2):0;e.runtimeStyle.marginRight=parseInt(m)+"px";break;case"center":if(a)m=0;default:if(a)m=parseInt(m/2);e.runtimeStyle.marginLeft=parseInt(m)+"px"}};ie7CSS.addRecalc("margin(-left|-right)?","[^};]*auto",function(e){if(register(_1,e,e.parentElement&&e.currentStyle.display=="block"&&e.currentStyle.marginLeft=="auto"&&e.currentStyle.position!=
 "absolute")){_1(e)}});addResize(function(){for(var i in _1.elements){e=_1.elements[i];e.runtimeStyle.marginLeft=e.runtimeStyle.marginRight="";_1(e)}})}});
diff --git a/web/static/js/ie7/ie7-recalc.js b/web/static/js/ie7/ie7-recalc.js
deleted file mode 100644
index 10cb839..0000000
--- a/web/static/js/ie7/ie7-recalc.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-IE7.addModule("ie7-recalc",function(){C=/\sie7_class\d+/g;function _0(e){e.className=e.className.replace(C,"")};function _1(e){e.runtimeStyle.cssText=""};ie7CSS.specialize({elements:{},handlers:[],reset:function(){this.removeEventHandlers();var e=this.elements;for(var i in e)_1(e[i]);this.elements={};if(this.Rule){var e=this.Rule.elements;for(var i in e)_0(e[i]);this.Rule.elements={}}},reload:function(){ie7CSS.rules=[];this.getInlineStyles();this.screen.load();if(this.print)this.print.load();this.refresh();this.trash()},addRecalc:function(p,t,h,r){this.inherit(p,t,function(e){h(e);ie7CSS.elements[e.uniqueID]=e},r)},recalc:function(){this.reset();this.inherit()},addEventHandler:function(e,t,h){e.attachEvent(t,h);this.handlers.push(arguments)},removeEventHandlers:function(){var h;while(h=this.handlers.pop()){removeEventHandler(h[0],h[1],h[2])}},getInlineStyles:function(){var st=document.getElementsByTagName("style"),s;for(var i=st.length-1;(s=st[i]);i--){if(!s.disabled&&!s.ie7
 ){var c=s.c||s.innerHTML;this.styles.push(c);s.c=c}}},trash:function(){var s,i;for(i=0;i<styleSheets.length;i++){s=styleSheets[i];if(!s.ie7&&!s.c){s.c=s.cssText}}this.inherit()},getText:function(s){return s.c||this.inherit(s)}});addEventHandler(window,"onunload",function(){ie7CSS.removeEventHandlers()});if(ie7CSS.Rule){ie7CSS.Rule.elements={};ie7CSS.Rule.prototype.specialize({add:function(e){this.inherit(e);ie7CSS.Rule.elements[e.uniqueID]=e}});ie7CSS.PseudoElement.hash={};ie7CSS.PseudoElement.prototype.specialize({create:function(t){var k=this.selector+":"+t.uniqueID;if(!ie7CSS.PseudoElement.hash[k]){ie7CSS.PseudoElement.hash[k]=true;this.inherit(t)}}})}if(isHTML&&ie7HTML){ie7HTML.specialize({elements:{},addRecalc:function(s,h){this.inherit(s,function(e){if(!ie7HTML.elements[e.uniqueID]){h(e);ie7HTML.elements[e.uniqueID]=e}})}})}document.recalc=function(reload){if(ie7CSS.screen){if(reload)ie7CSS.reload();recalc()}}});
diff --git a/web/static/js/ie7/ie7-server.css b/web/static/js/ie7/ie7-server.css
deleted file mode 100644
index 50ca605..0000000
--- a/web/static/js/ie7/ie7-server.css
+++ /dev/null
@@ -1,44 +0,0 @@
-body, td, dd {font: 10pt Verdana, Arial, Helvetica, sans-serif; color: black;}
-body {margin: 8px; background: #333;}
-h1 {margin: 0;}
-h1 a:hover {background-color: transparent;}
-h2 {font-size: 1.75em;}
-h3 {font-size: 1.1em;}
-p.footnote {font-family: "Times New Roman", Times, serif; font-style: italic;}
-a:active {color: #ff0000;}
-a:link {color: #0a6cce;}
-a:visited {color: #0a6cce;}
-code, *.code {font-family: monospace; font-size: 100%; font-style: normal; white-space: nowrap;
- padding: 0 1px; background: #f2f3f8; border: #d6d9e9 1px solid;}
-code.box {display: block; padding: 10px; margin: 0.5em 0;}
-ul {list-style-type: square;}
-dd {margin: .2em 0 .5em 1em;}
-dl.library dt {display: list-item; margin-left: 3em; list-style-type: square;}
-dl.library dd {font-style: italic; margin-left: 3em;}
-dt {font-weight: bold;}
-dt.pack {color: brown;}
-a img {border-style: none;}
-hr {height: 1px; color: #000; border-style: solid;}
-hr.short {height: 2px; width: 100px;}
-div.document {background: #eef; padding: 20px 20px 5px 20px; width: 600px; border: 1px solid black;}
-hr {border-bottom-width: 0px;}
-div.header hr {color: #0a6cce; background-color: #0a6cce;}
-div.footer hr {color: #898e79; background-color: #898e79; }
-div.header, div.header a:link, div.header a:visited, h3 a:link, h3 a:visited {text-decoration: none;}
-a:hover {color: #fff; background-color: #0a6cce; text-decoration: none;}
-div.footer a:hover {background-color: transparent; text-decoration: none;}
-div.header .menu {text-align: right;}
-div.content {min-height: 100px;}
-div.footer {font-size: x-small; margin-top: 8px;}
-div.footnote {font-family: "times new roman", times; font-style: italic; margin-top: 10px;}
-#license {margin-top: 5px; font-size: xx-small;}
-table {border-top: 1px solid #000; border-left: 1px solid #000;}
-th {background-color: #fff; text-align: left;}
-th, td {border-right: 1px solid #000; border-bottom: 1px solid #000;}
-th.small {width: 100px;}
-th.medium {width: 200px;}
-th.large {width: 270px;}
-th.x-large {width: 408px;}
-table.fixed {table-layout: fixed;}
-span.comment {color: #666;}
-
diff --git a/web/static/js/ie7/ie7-squish.js b/web/static/js/ie7/ie7-squish.js
deleted file mode 100644
index e5a1972..0000000
--- a/web/static/js/ie7/ie7-squish.js
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-IE7.addModule("ie7-squish", function() {
-
-/* ---------------------------------------------------------------------
-
-  Squish some IE bugs!
-
-  Some of these bug fixes may have adverse effects so they are
-  not included in the standard library. Add your own if you want.
-
-  -dean
-
---------------------------------------------------------------------- */
-
-// @NOTE: ie7Layout.boxSizing is the same as the "Holly Hack"
-
-// "doubled margin" bug
-// http://www.positioniseverything.net/explorer/doubled-margin.html
-ie7CSS.addFix(/float\s*:\s*(left|right)/, "display:inline;$1");
-
-if (ie7Layout) {
-	// "peekaboo" bug
-	// http://www.positioniseverything.net/explorer/peekaboo.html
-	if (appVersion >= 6) ie7CSS.addRecalc("float", "left|right", function($element) {
-		ie7Layout.boxSizing($element.parentElement);
-		// "doubled margin" bug
-		$element.runtimeStyle.display = "inline";
-	});
-
-	// "unscrollable content" bug
-	// http://www.positioniseverything.net/explorer/unscrollable.html
-	ie7CSS.addRecalc("position", "absolute|fixed", function($element) {
-		if ($element.offsetParent && $element.offsetParent.currentStyle.position == "relative")
-			ie7Layout.boxSizing($element.offsetParent);
-	});
-}
-
-//# // get rid of Microsoft's pesky image toolbar
-//# if (!complete) document.write('<meta http-equiv="imagetoolbar" content="no">');
-
-});
diff --git a/web/static/js/ie7/ie7-standard-p.js b/web/static/js/ie7/ie7-standard-p.js
deleted file mode 100644
index 6db85f5..0000000
--- a/web/static/js/ie7/ie7-standard-p.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('y(!26.1F)11 7(){2C{26.1F=8;6 2s=8.24=11 3b;8.1g=7(){z"1F 4x 0.9 (ad)"};6 5T=/5T/.Z(2y.5h.7C);6 31=(5T)?7(m){26.31(1F+"\\n\\n"+m)}:2s;6 29=ac.29.19(/ab (\\d\\.\\d)/)[1];6 2m=16.aa!="a9";y(/a8/.Z(2y.5h.7C)||29<5||!/^a7/.Z(16.2F.2a))z;6 33=16.5W=="33";6 1s,1K;6 2F=16.2F,1X,1J,1R=16.1R;6 4E="!";6 3Q={};6 2G=1z;1F.24=7(n,s){y(!3Q[n]){y(2G)1k("s="+23(s));3Q[n]=11 s()}};6 R=/^[\\w\\.]+[^:]*$/;7 1Z(h,p){y(R.Z(h))h=(p||"")+h;z h};7 3F(h,p){h=1Z(h,p);z h.1q(0,h.7a("/")+1)};6 s=16.7B[16.7B.K-1];2C{1k(s.7j)}2j(i){}6 2k=3F(s.1l);6 2v;2C{6 l=(a6()>=5)?"a5":"5n";2v=11 a4(l+".a3")}2j(i){}6 4A={};7 3T(h,p){2C{h=1Z(h,p);y(!4A[h]){2v.a2("a1",h,1z);2v.a0();y(2v.7A==0||2v.7A=
 =9Z){4A[h]=2v.9Y}}}2j(i){31("4B [1]: 5O 9X 9W "+h)}5U{z 4A[h]||""}};6 4i=1Z("9V.9U",2k);7 2o(1w){y(1w!=1U){1w.1T=1t.1C.1T;1w.1e=1t.1C.1e}z 1w};2o.1e=7(p,c){y(!p)p={};y(!c)c=p.1h;y(c=={}.1h)c=11 3b("8.1T()");c.1i=11 3b("z 8");c.1i.1C=11 8.1i;c.1i.1C.1e(p);c.1C=11 c.1i;c.1i.1C.1h=c.1C.1h=c;c.2E=8;c.1e=1a.5P;c.4z=8.4z;z c};2o.1i=11 3b("z 8");2o.1i.1C={1h:2o,1T:7(){z 1a.5P.9T.2E.2q(8,1a)},1e:7(1w){y(8==8.1h.1C&&8.1h.1e){z 8.1h.1i.1C.1e(1w)}O(6 i 28 1w){34(i){1m"1h":1m"1g":1m"1i":5M}y(3Y 1w[i]=="7"&&1w[i]!=8[i]){1w[i].2E=8[i]}8[i]=1w[i]}y(1w.1g!=8.1g&&1w.1g!={}.1g){1w.1g.2E=8.1g;8.1g=1w.1g}z 8}};7 1t(){};8.1t=2o.1e({1h:1t,1g:7(){z"[9S "+(8.1h.1x||"9R")+"]"},9Q:7(2i){z 8.1h==2i||2i.4z(8.1h)}});1t.1x="1t";1t.2E=1U;1t.4z=7(2i){1D(2i&&2i.2E!=8)2i=2i.2E;z 7q(2i)};1t.1i.2E=2o;3u 8.1t;6 5x=1t.1e({1h:7(){8.3L=[];8.1Q=[]},1S:2s});y(29<5.5)1k(3T("17-9P.5X",2k));6 5S=1z;1F.1S=7(){2C{y(5S)z;5S=33=1o;1X=16.1X;1J=(2m)?1X:2F;y(2l&&1s)1s.2q();V.2q();1u();31("2G 9O")}2j(e){31("4B [2]: "+e.5V)}};6
  1Q=[];7 1n(r){1Q.1b(r)};7 1u(){14.5g();y(2l&&1s)1s.1u();V.1u();O(6 i=0;i<1Q.K;i++)1Q[i]()};7 2U(){6 E=0,R=1,L=2;6 G=/\\(/g,S=/\\$\\d/,I=/^\\$\\d+$/,T=/([\'"])\\1\\+(.*)\\+\\1\\1$/,7t=/\\\\./g,Q=/\'/,7z=/\\3S[^\\3S]*\\3S/g;6 3N=8;8.15=7(e,r){y(!r)r="";6 l=(5R(23(e)).19(G)||"").K+1;y(S.Z(r)){y(I.Z(r)){r=25(r.1q(1))-1}1d{6 i=l;6 q=Q.Z(5R(r))?\'"\':"\'";1D(i)r=r.2O("$"+i--).2p(q+"+a[o+"+i+"]+"+q);r=11 3b("a,o","z"+q+r.13(T,"$1")+q)}}7y(e||"/^$/",r,l)};8.2V=7(s){3R.K=0;z 7u(7v(s,8.4y).13(11 1N(30,8.5Q?"5D":"g"),7w),8.4y).13(7z,"")};8.72=7(){30.K=0};6 3R=[];6 30=[];6 7x=7(){z"("+23(8[E]).1q(1,-1)+")"};30.1g=7(){z 8.2p("|")};7 7y(){1a.1g=7x;30[30.K]=1a}7 7w(){y(!1a[0])z"";6 i=1,j=0,p;1D(p=30[j++]){y(1a[i]){6 r=p[R];34(3Y r){1m"7":z r(1a,i);1m"9N":z 1a[r+i]}6 d=(1a[i].6F(3N.4y)==-1)?"":"\\3S"+1a[i]+"\\3S";z d+r}1d i+=p[L]}};7 7v(s,e){z e?s.13(11 1N("\\\\"+e+"(.)","g"),7(m,c){3R[3R.K]=c;z e}):s};7 7u(s,e){6 i=0;z e?s.13(11 1N("\\\\"+e,"g"),7(){z e+(3R[i++]||"")}):s};7 5R(s){z s.13(7
 t,"")}};2U.1C={1h:2U,5Q:1z,4y:""};1t.1e(2U.1C);6 3M=2U.1e({5Q:1o});6 14=7(){6 4x="2.0.2";6 C=/\\s*,\\s*/;6 14=7(s,1E){2C{6 m=[];6 u=1a.5P.5I&&!1E;6 b=(1E)?(1E.1h==7n)?1E:[1E]:[16];6 2f=45(s).2O(C),i;O(i=0;i<2f.K;i++){s=5J(2f[i]);y(4P&&s.1q(0,3).2p("")==" *#"){s=s.1q(2);1E=7o([],b,s[1])}1d 1E=b;6 j=0,t,f,a,c="";1D(j<s.K){t=s[j++];f=s[j++];c+=t+f;a="";y(s[j]=="("){1D(s[j++]!=")"&&j<s.K){a+=s[j]}a=a.1q(0,-1);c+="("+a+")"}1E=(u&&2e[c])?2e[c]:7m(1E,t,f,a);y(u)2e[c]=1E}m=m.4J(1E)}3u 14.5O;z m}2j(e){14.5O=e;z[]}};14.1g=7(){z"7 14() {\\n  [4x "+4x+"]\\n}"};6 2e={};14.5I=1z;14.5g=7(s){y(s){s=5J(s).2p("");3u 2e[s]}1d 2e={}};6 3Q={};6 2G=1z;14.24=7(n,s){y(2G)1k("s="+23(s));3Q[n]=11 s()};14.1i=7(c){z c?1k(c):8};6 1V={};6 1B={};6 1p={19:/\\[([\\w-]+(\\|[\\w-]+)?)\\s*(\\W?=)?\\s*([^\\]]*)\\]/};6 2R=[];1V[" "]=7(r,f,t,n){6 e,i,j;O(i=0;i<f.K;i++){6 s=4w(f[i],t,n);O(j=0;(e=s[j]);j++){y(2D(e)&&5K(e,n))r.1b(e)}}};1V["#"]=7(r,f,i){6 e,j;O(j=0;(e=f[j]);j++)y(e.1c==i)r.1b(e)};1V["."]=7(r,f,c){c=1
 1 1N("(^|\\\\s)"+c+"(\\\\s|$)");6 e,i;O(i=0;(e=f[i]);i++)y(c.Z(e.1x))r.1b(e)};1V[":"]=7(r,f,p,a){6 t=1B[p],e,i;y(t)O(i=0;(e=f[i]);i++)y(t(e,a))r.1b(e)};1B["21"]=7(e){6 d=5L(e);y(d.5N)O(6 i=0;i<d.5N.K;i++){y(d.5N[i]==e)z 1o}};1B["37"]=7(e){};6 2D=7(e){z(e&&e.7k==1&&e.2W!="!")?e:1U};6 4S=7(e){1D(e&&(e=e.9M)&&!2D(e))5M;z e};6 47=7(e){1D(e&&(e=e.6W)&&!2D(e))5M;z e};6 3l=7(e){z 2D(e.7s)||47(e.7s)};6 5t=7(e){z 2D(e.7r)||4S(e.7r)};6 6q=7(e){6 c=[];e=3l(e);1D(e){c.1b(e);e=47(e)}z c};6 4P=1o;6 5H=7(e){6 d=5L(e);z(3Y d.7p=="9L")?/\\.9K$/i.Z(d.9J):7q(d.7p=="9I 9H")};6 5L=7(e){z e.9G||e.16};6 4w=7(e,t){z(t=="*"&&e.1Y)?e.1Y:e.4w(t)};6 4T=7(e,t,n){y(t=="*")z 2D(e);y(!5K(e,n))z 1z;y(!5H(e))t=t.9F();z e.2W==t};6 5K=7(e,n){z!n||(n=="*")||(e.9E==n)};6 9D=7(e){z e.9C};7 7o(r,f,1c){6 m,i,j;O(i=0;i<f.K;i++){y(m=f[i].1Y.9B(1c)){y(m.1c==1c)r.1b(m);1d y(m.K!=1U){O(j=0;j<m.K;j++){y(m[j].1c==1c)r.1b(m[j])}}}}z r};y(![].1b)7n.1C.1b=7(){O(6 i=0;i<1a.K;i++){8[8.K]=1a[i]}z 8.K};6 N=/\\|/;7 7m(1E,t,f,a){y
 (N.Z(f)){f=f.2O(N);a=f[0];f=f[1]}6 r=[];y(1V[t]){1V[t](r,1E,f,a)}z r};6 S=/^[^\\s>+~]/;6 7l=/[\\s#.:>+~()@]|[^\\s#.:>+~()@]+/g;7 5J(s){y(S.Z(s))s=" "+s;z s.19(7l)||[]};6 W=/\\s*([\\s>+~(),]|^|$)\\s*/g;6 I=/([\\s>+~,]|[^(]\\+|^)([#.:@])/g;6 45=7(s){z s.13(W,"$1").13(I,"$1*$2")};6 2c={1g:7(){z"\'"},19:/^(\'[^\']*\')|("[^"]*")$/,Z:7(s){z 8.19.Z(s)},15:7(s){z 8.Z(s)?s:8+s+8},3v:7(s){z 8.Z(s)?s.1q(1,-1):s}};6 2w=7(t){z 2c.3v(t)};6 E=/([\\/()[\\]?{}|*+-])/g;7 4O(s){z s.13(E,"\\\\$1")};2G=1o;z 14}();14.5I=1o;14.24("17",7(){2D=7(e){z(e&&e.7k==1&&e.2W!="!"&&!e.2K)?e:1U}});14.1i("2w=1a[1]",42);6 2l=!14.1i("5H(1a[1])",2F);6 2r=":21{17-21:21}:37{17-21:37}"+(2l?"":"*{6Q:0}");6 V=11(5x.1e({5B:11 3M,2P:"",2Y:"",5F:[],1S:7(){8.5G();8.4t()},4t:7(){V.3O.18=2r+8.2P+8.2Y},7i:7(){6 3P=16.4w("1r"),s;O(6 i=3P.K-1;(s=3P[i]);i--){y(!s.3m&&!s.17){8.5F.1b(s.7j)}}},2q:7(){8.7i();8.4t();11 3y("2P");8.7g()},3i:7(e,r){8.5B.15(e,r)},1u:7(){6 R=/7h\\d+/g;6 s=2r.19(/[{,]/g).K;6 3P=s+(8.2P.18.19(/\\{/g)||"").
 K;6 2Q=8.3O.2t,r;6 4v,c,4u,e,i,j,k,1c;O(i=s;i<3P;i++){r=2Q[i];y(r&&(4v=r.1r.18.19(R))){4u=14(r.4M);y(4u.K)O(j=0;j<4v.K;j++){1c=4v[j];c=V.1Q[1c.1q(10)][2];O(k=0;(e=4u[k]);k++){y(e.D[1c])c(e)}}}}},1n:7(p,t,h,r){t=11 1N("([{;\\\\s])"+p+"\\\\s*:\\\\s*"+t+"[^;}]*");6 i=8.1Q.K;y(r)r=p+":"+r;8.3i(t,7(m,o){z(r?m[o+1]+r:m[o])+";17-"+m[o].1q(1)+";7h"+i+":1"});8.1Q.1b(1a);z i},2w:7(s){z s.18||""},5G:7(){y(33||!2l)16.5G();1d 16.9A("<1r 17=1o></1r>");8.3O=1R[1R.K-1];8.3O.17=1o;8.3O.18=2r},7g:7(){O(6 i=0;i<1R.K;i++){y(!1R[i].17&&1R[i].18){1R[i].18=""}}}}));7 3y(m){8.2Z=m;8.3q();V[m]=8;V.4t()};1t.1e({1h:3y,1g:7(){z"@2Z "+8.2Z+"{"+8.18+"}"},1u:2s,3q:7(){8.18="";8.2w();8.38();8.18=41(8.18);f={}},2w:7(){6 7e=[].4J(V.5F);6 M=/@2Z\\s+([^{]*)\\{([^@]+\\})\\s*\\}/5D;6 A=/\\9z\\b|^$/i,S=/\\9y\\b/i,P=/\\9x\\b/i;7 7d(c,m){4s.v=m;z c.13(M,4s)};7 4s(9w,m,c){m=5E(m);34(m){1m"2P":1m"2Y":y(m!=4s.v)z"";1m"1Y":z c}z""};7 5E(m){y(A.Z(m))z"1Y";1d y(S.Z(m))z(P.Z(m))?"1Y":"2P";1d y(P.Z(m))z"2Y"};6 3N=8;7 5C(s,
 p,m,l){6 c="";y(!l){m=5E(s.2Z);l=0}y(m=="1Y"||m==3N.2Z){y(l<3){O(6 i=0;i<s.7f.K;i++){c+=5C(s.7f[i],3F(s.2u,p),m,l+1)}}c+=79(s.2u?7c(s,p):7e.77()||"");c=7d(c,3N.2Z)}z c};6 f={};7 7c(s,p){6 u=1Z(s.2u,p);y(f[u])z"";f[u]=(s.3m)?"":7b(V.2w(s,p),3F(s.2u,p));z f[u]};6 U=/(43\\s*\\(\\s*[\'"]?)([\\w\\.]+[^:\\)]*[\'"]?\\))/5D;7 7b(c,p){z c.13(U,"$1"+p.1q(0,p.7a("/")+1)+"$2")};O(6 i=0;i<1R.K;i++){y(!1R[i].3m&&!1R[i].17){8.18+=5C(1R[i])}}},38:7(){8.18=V.5B.2V(8.18)},1u:2s});6 2c=14.1i("2c");6 4r=[];7 79(c){z 2n.2V(3r.2V(c))};7 5A(m,o){z 2c+(4r.1b(m[o])-1)+2c};7 42(v){z 2c.Z(v)?1k(4r[1k(v)]):v};6 2n=11 3M;2n.15(/\\/\\*[^*]*\\*+([^\\/][^*]*\\*+)*\\//);2n.15(/\'[^\']*\'/,5A);2n.15(/"[^"]*"/,5A);2n.15(/\\s+/," ");2n.15(/@(9v|9u)[^;\\n]+[;\\n]|<!\\-\\-|\\-\\->/);6 3r=11 3M;3r.15(/\\\\\'/,"\\\\9t");3r.15(/\\\\"/,"\\\\46");6 5z=11 3M;5z.15(/\'(\\d+)\'/,78);7 41(c){z 5z.2V(c)};7 78(m,o){z 4r[m[o+1]]};6 5y=[];7 4U(h){1n(h);1j(26,"9s",h)};7 1j(e,t,h){e.9r(t,h);5y.1b(1a)};7 76(e,t,h){2C{e.9q(t,h)}
 2j(i){}};1j(26,"9p",7(){6 h;1D(h=5y.77()){76(h[0],h[1],h[2])}});7 20(h,e,c){y(!h.1O)h.1O={};y(c)h.1O[e.2a]=e;1d 3u h.1O[e.2a];z c};1j(26,"6z",7(){y(!V.2Y)11 3y("2Y");V.2Y.1u()});6 75=/^\\d+(9o)?$/i;6 3d=/^\\d+%$/;6 3c=7(e,v){y(75.Z(v))z 25(v);6 s=e.1r.1f;6 r=e.J.1f;e.J.1f=e.D.1f;e.1r.1f=v||0;v=e.1r.4e;e.1r.1f=s;e.J.1f=r;z v};7 6x(t){6 e=16.3X(t||"2M");e.1r.18="1y:3C;6R:0;4K:9n;3G:1M;9m:9l(0 0 0 0);1f:-9k";e.2K=1o;z e};6 4q="17-";7 3D(e){z e.D["17-1y"]=="2z"};7 4o(e,p){z e.D[4q+p]||e.D[p]};7 2T(e,p,v){y(e.D[4q+p]==1U){e.J[4q+p]=e.D[p]}e.J[p]=v};7 4H(o,c,u){6 t=9j(7(){2C{y(!o.3q)z;o.3q(o,c,u);74(t)}2j(i){74(t)}},10)};1F.24("17-9i",7(){y(!2l)z;2r+="9h{3p-3o:9g}9f{3p-3o:1.9e;}9d{3p-3o:1.9c;}"+"9b{3p-3o:9a}99{3p-3o:.98}97{3p-3o:.96}";6 5w={};1s=11(5x.1e({1S:2s,3i:7(){8.3L.1b(1a)},2q:7(){O(6 i=0;i<8.3L.K;i++){6 m=14(8.3L[i][0]);6 f=8.3L[i][1]||6X;O(6 j=0;j<m.K;j++)f(m[j])}},1n:7(){8.1Q.1b(1a)},1u:7(){O(6 i=0;i<8.1Q.K;i++){6 m=14(8.1Q[i][0]);6 r=8.1Q[i][1],e;6 k=4g.95(2,i);O(6 j=0;
 (e=m[j]);j++){6 u=e.2a;y((5w[u]&k)==0){e=r(e)||e;5w[u]|=k}}}}}));1s.3i("94");1s.1n("93",7(e){y(!e.6o){6 f=14("5l,92",e)[0];y(f){1j(e,"73",7(){f.91()})}}});1s.1n("71,5l",7(e){y(e.2W=="70"){6 m=e.3z.19(/ 3n="([^"]*)"/i);e.J.3n=(m)?m[1]:""}y(e.2L=="5v"){1j(e,"73",7(){e.J.5u=1o;32("16.1Y."+e.2a+".J.5u=1z",1)})}});6 U=/^(5v|72|71)$/;1s.1n("90",7(e){1j(e,"8Z",7(){O(6 i=0;i<e.K;i++){y(6Z(e[i])){e[i].3m=1o;32("16.1Y."+e[i].2a+".3m=1z",1)}1d y(e[i].2W=="70"&&e[i].2L=="5v"){32("16.1Y."+e[i].2a+".3n=\'"+e[i].3n+"\'",1);e[i].3n=e[i].J.3n}}})});7 6Z(e){z U.Z(e.2L)&&!e.3m&&!e.J.5u};1s.1n("5d",7(e){y(e.8Y&&!e.6Y)e.6Y=""});6 P=(29<5.5)?"8X:":"";7 6X(e){6 f=16.3X("<"+P+e.3z.1q(1));y(e.3z.1q(-2)!="/>"){6 6V="</"+e.2W+">",n;1D((n=e.6W)&&n.3z!=6V){f.6t(n)}y(n)n.8W()}e.4R.6A(f,e)}});1F.24("17-8V",7(){1K=8;2r+="*{3H:22-2X}";8.3j=(29<5.5)?7(e){z e.1I}:7(e){z e.D.3j};8.3H=7(e){y(!1K.3j(e)){e.1r.2b="6T";y(e.D.6U=="1P")e.J.6U="2y";4k(e)}};7 4k(e){y(e!=1J&&e.D.1y!="3C"){4p(e);8U(e)}};6 3l=14.1i("3l");
 6 5t=14.1i("5t");7 4p(e){y(!e.J.3k){6 p=e.59;y(p&&1K.3j(p)&&e==3l(p))z;6 f=3l(e);y(f&&f.D.8T=="1M"&&1K.3j(f)){4p(f);m=5s(e,e.D.3k);c=5s(f,f.D.3k);y(m<0||c<0){e.J.3k=m+c}1d{e.J.3k=4g.3g(c,m)}f.J.3k="8S"}}};1k(23(4p).13(/5c/g,"6N").13(/4N/g,"8R"));7 5s(e,v){z(v=="1P")?0:3c(e,v)};6 U=/^[.\\d][\\w%]*$/,A=/^(1P|6T)$/,N="[.\\\\d]";6 4l,6S;7 6O(e){4l(e);6S(e)};7 2g(H){4l=7(e){y(!3d.Z(e.D.12))2g(e);4k(e)};7 2g(e,v){y(!e.J.3J){y(!v)v=e.D.12;e.J.3J=(U.Z(v))?4g.3g(0,2B(e,v)):v;2T(e,"12",e.J.3J)}};7 5r(e){y(!3D(e)){6 l=e.3B;1D(l&&!1K.3j(l))l=l.3B}z(l||1J).1I};7 1H(e,v){y(3d.Z(v))z 25(4c(v)/3w*5r(e));z 3c(e,v)};6 2B=7(e,v){6 b=e.D["2X-5o"]=="3G-2X";6 a=0;y(2m&&!b)a+=4n(e)+3K(e);1d y(!2m&&b)a-=4n(e)+3K(e);z 1H(e,v)+a};7 4n(e){z e.2S-e.1I};7 3K(e){z 1H(e,e.D.8Q)+1H(e,e.D.8P)};1k(23(3K).13(/6R/g,"6Q").13(/8O/g,"8N"));2r+="*{1A:1M;27:1M;3I-12:1M;3g-12:1M}";7 1A(e){y(e.D["3I-12"]!=1U){e.1r.1A=e.D["3I-12"]}y(20(1A,e,e.D.1A!="1M")){1K.3H(e);2g(e);4m(e)}};1k(23(1A).13(/3I/g,"3g"));1K.1A=1A;1K.27
 =27;7 4m(e){6 r=e.54();6 w=r.1W-r.1f;y(e.D.1A!="1M"&&w<=2B(e,e.D.1A)){e.J.12=2B(e,e.D.1A)}1d y(e.D.27!="1M"&&w>=2B(e,e.D.27)){e.J.12=2B(e,e.D.27)}1d{e.J.12=e.J.3J}};7 2x(e){y(20(2x,e,/^(2z|3C)$/.Z(e.D.1y)&&4o(e,"1f")!="1P"&&4o(e,"1W")!="1P"&&A.Z(4o(e,"12")))){5p(e);1K.3H(e)}};1K.2x=2x;7 5p(e){6 l=1H(e,e.J.52||e.D.1f);6 w=5r(e)-1H(e,e.D.1W)-l-8M(e);y(25(e.J.12)==w)z;e.J.12="";y(3D(e)||H||e.2S<w){y(!2m)w-=4n(e)+3K(e);y(w<0)w=0;e.J.3J=w;2T(e,"12",w)}};6 5q=0;4U(7(){6 i,w=(5q<1J.1I);5q=1J.1I;O(i 28 1A.1O){6 e=1A.1O[i];6 f=(25(e.J.12)==2B(e,e.D.1A));y(w&&f)e.J.12="";y(w==f)4m(e)}O(i 28 27.1O){6 e=27.1O[i];6 f=(25(e.J.12)==2B(e,e.D.27));y(!w&&f)e.J.12="";y(w!=f)4m(e)}O(i 28 2x.1O)5p(2x.1O[i])});y(26.8L!==1z){V.1n("12",N,2m?4l:4k)}V.1n("3I-12",N,1A);V.1n("3g-12",N,27);V.1n("1W",N,2x)};V.1n("3G-6P",N,7(e){y(e.D.8K!="8J"){e.8I=3c(e,e.D["3G-6P"])}});V.1n("2X-5o","22-2X",8.3H);V.1n("2X-5o","3G-2X",6O);6 1v=11 2U;1v.15(/6v/,"6u");1v.15(/12/,"2b");1v.15(/6w/,"5c");1v.15(/1f/,"2y");1v.15(
 /8H/,"6N");1v.15(/1W/,"56");1k(1v.2V(23(2g)));2g();8G(1o)});1F.24("17-8F",7(){y(29<5.5)z;6 A="6J.5n.8E";6 F="6K:"+A+"(1l=\'%1\',8D=\'6H\')";6 5j=11 1N((26.8C||"-8B.8A")+"$","i");6 3h=[];7 5f(e){6 f=e.6M[A];y(f){f.1l=e.1l;f.6L=1o}1d{e.J.5m=F.13(/%1/,e.1l);3h.1b(e)}e.1l=4i};7 6y(e){e.1l=e.4D;e.6M[A].6L=1z};V.3i(/6I\\s*:\\s*([\\d.]+)/,7(m,o){z"6G:1;5m:6K:6J.5n.8z(6I="+((4c(m[o+1])*3w)||1)+")"});6 B=/5e(-5i)?\\s*:\\s*([^\\(};]*)43\\(([^\\)]+)\\)([^;}]*)/;V.3i(B,7(m,o){6 u=42(m[o+3]);z 5j.Z(u)?"5m:"+F.13(/6H/,"8y").13(/%1/,u)+";6G:1;5e"+(m[o+1]||"")+":"+(m[o+2]||"")+"1M"+(m[o+4]||""):m[o]});y(1s){1s.1n("5d,5l",7(e){y(e.2W=="8x"&&e.2L!="5i")z;5k(e);1j(e,"8w",7(){y(!4j&&60.8v=="1l"&&e.1l.6F(4i)==-1)5k(e)})});6 6D=/^3W:.*;6E/i;6 6C=1Z("17-6E.8u",2k);7 5k(e){y(5j.Z(e.1l)){6 i=11 8t(e.12,e.2b);i.8s=7(){e.12=i.12;e.2b=i.2b;i=1U};i.1l=e.1l;e.4D=e.1l;5f(e)}1d y(6D.Z(e.1l)){e.1l=6C+"?"+e.1l.1q(5)}};6 I=/^5i/i;6 6B=1Z("17-2M.4C",2k);1s.1n("2M",7(e){y(I.Z(e.2L)){6 o=16.3X("<2M 2L=68/x-67>")
 ;o.1r.12=e.D.12;o.1r.2b=e.D.2b;o.3W=6B;6 u=1Z(e.3W,3F(5h.2u));e.4R.6A(o,e);14.5g("2M");4H(o,"",u);z o}})}6 4j=1z;1j(26,"6z",7(){4j=1o;O(6 i=0;i<3h.K;i++)6y(3h[i])});1j(26,"8r",7(){O(6 i=0;i<3h.K;i++)5f(3h[i]);4j=1z})});1F.24("17-2z",7(){V.1n("1y","2z",4a,"3C");V.1n("5e(-8q)?","[^};]*2z",4b);6 4Z=(2m)?"1X":"2F";6 4h=7(){y(1X.D.5b!="2z"){y(1X.D.5a=="1M"){1X.J.8p="8o-8n";1X.J.5a="43("+4i+")"}1X.J.5b="2z"}4h=2s};6 2h=6x("5d");7 1v(f){z 2A.2V(23(f))};6 2A=11 2U;2A.15(/6w/,"5c");2A.15(/1f/,"2y");2A.15(/6v/,"6u");2A.15(/12/,"2b");2A.15(/1W/,"56");2A.15(/X/,"Y");7 3f(e){z(e)?3D(e)||3f(e.59):1z};7 4f(e,p,3e){32("16.1Y."+e.2a+".J.4f(\'"+p+"\',\'"+3e+"\')",0)};7 4b(e){y(20(4b,e,e.D.5b=="2z"&&!e.61(1X))){4h();58(e);8m(e);4V(e)}};7 4V(e){2h.1l=e.D.5a.1q(5,-2);6 p=(e.6c)?e:e.59;p.6t(2h);57(e);8l(e);p.8k(2h)};7 58(e){e.1r.3E=e.D.3E;y(!3f(e)){6 3e="(25(J.3A)+16."+4Z+".6s)||0";4f(e,"3E",3e)}};1k(1v(58));7 57(e){6 p=3f(e)?"3E":"3A";e.J[p]=55(e,e.1r.3E)-e.54().1f-e.8j+2};1k(1v(57));7 55(e,p){3
 4(p){1m"1f":1m"2y":z 0;1m"1W":1m"56":z 1J.1I-2h.2S;1m"8i":z(1J.1I-2h.2S)/2;8h:y(3d.Z(p)){z 25((1J.1I-2h.2S)*4c(p)/3w)}2h.1r.1f=p;z 2h.3A}};1k(1v(55));7 4a(e){y(20(4a,e,3D(e))){2T(e,"1y","3C");2T(e,"1f",e.D.1f);2T(e,"2y",e.D.2y);4h();y(1K)1K.2x(e);49(e)}};7 49(e,r){8g(e,r);4Y(e,r,1o);y(!e.J.4d&&e.D.4X=="1P"&&e.D.1W!="1P"){6 l=1J.1I-1H(e,e.D.1W)-1H(e,e.J.52)-e.1I;y(e.D.8f=="1P")l=25(l/2);y(3f(e.3B))e.J.4e+=l;1d e.J.50=l}53(e);8e(e)};7 53(e){y(e.D.12!="1P"){6 r=e.54();6 w=e.2S-1J.1I+r.1f-2;y(w>=0){w=4g.3g(3c(e,e.D.12)-w,0);2T(e,"12",w)}}};1k(1v(53));7 4Y(e,r){y(!r&&3d.Z(e.D.12)){e.J.2g=e.D.12}y(e.J.2g){e.J.12=1H(e,e.J.2g)}y(r){y(!e.J.4d)z}1d{e.J.50=0;e.J.52=e.D.1f;e.J.4d=e.D.1W!="1P"&&e.D.1f=="1P"}e.J.1f="";e.J.51=4W(e);e.J.4e=e.J.51;y(!r&&!3f(e.3B)){6 3e="J.51+J.50+16."+4Z+".6s";4f(e,"4e",3e)}};1k(1v(4Y));7 4W(e){6 s=e.3A,n=1;y(e.J.4d){s=1J.1I-e.2S-1H(e,e.D.1W)}y(e.D.4X!="1P"){s-=1H(e,e.D.4X)}1D(e=e.3B){y(e.D.1y!="8d")n=-1;s+=e.3A*n}z s};1k(1v(4W));7 1H(e,v){y(3d.Z(v))z 25(4c(
 v)/3w*1J.1I);z 3c(e,v)};1k(1v(1H));7 6r(){6 e=4b.1O;O(6 i 28 e)4V(e[i]);e=4a.1O;O(i 28 e){49(e[i],1o);49(e[i],1o)}48=0};6 48;4U(7(){y(!48)48=32(6r,0)})});1F.24("17-8c-1V",7(){14.24("8b-8a",7(){1V[">"]=7(r,f,t,n){6 e,i,j;O(i=0;i<f.K;i++){6 s=6q(f[i]);O(j=0;(e=s[j]);j++)y(4T(e,t,n))r.1b(e)}};1V["+"]=7(r,f,t,n){O(6 i=0;i<f.K;i++){6 e=47(f[i]);y(e&&4T(e,t,n))r.1b(e)}};1V["@"]=7(r,f,a){6 t=2R[a].Z;6 e,i;O(i=0;(e=f[i]);i++)y(t(e))r.1b(e)};1B["4N-89"]=7(e){z!4S(e)};1B["4Q"]=7(e,c){c=11 1N("^"+c,"i");1D(e&&!e.2H("4Q"))e=e.4R;z e&&c.Z(e.2H("4Q"))};1p.6p=/\\\\:/g;1p.3x="@";1p.3a={};1p.13=7(m,a,n,c,v){6 k=8.3x+m;y(!2R[k]){a=8.3Z(a,c||"",v||"");2R[k]=a;2R.1b(a)}z 2R[k].1c};1p.38=7(s){s=s.13(8.6p,"|");6 m;1D(m=s.19(8.19)){6 r=8.13(m[0],m[1],m[2],m[3],m[4]);s=s.13(8.19,r)}z s};1p.3Z=7(p,t,v){6 a={};a.1c=8.3x+2R.K;a.66=p;t=8.3a[t];t=t?t(8.2H(p),2w(v)):1z;a.Z=11 3b("e","z "+t);z a};1p.2H=7(n){34(n.5Z()){1m"1c":z"e.1c";1m"3U":z"e.1x";1m"O":z"e.6o";1m"2u":y(4P){z"23((e.3z.19(/2u=\\\\46?([^\\\
 \s\\\\46]*)\\\\46?/)||[])[1]||\'\')"}}z"e.2H(\'"+n.13(N,":")+"\')"};1p.3a[""]=7(a){z a};1p.3a["="]=7(a,v){z a+"=="+2c.15(v)};1p.3a["~="]=7(a,v){z"/(^| )"+4O(v)+"( |$)/.Z("+a+")"};1p.3a["|="]=7(a,v){z"/^"+4O(v)+"(-|$)/.Z("+a+")"};6 6n=45;45=7(s){z 6n(1p.38(s))}});6 1p=14.1i("1p");6 H=/a(#[\\w-]+)?(\\.[\\w-]+)?:(65|62)/i;6 6l=/\\s*\\{\\s*/,6m=/\\s*\\}\\s*/,C=/\\s*\\,\\s*/;6 F=/(.*)(:4N-(88|87))/;3y.1C.1e({38:7(){8.1T();6 o=V.2t.K;6 2Q=8.18.2O(6m),r;6 2f,c,i,j;O(i=0;i<2Q.K;i++){r=2Q[i].2O(6l);2f=r[0].2O(C);c=r[1];O(j=0;j<2f.K;j++){2f[j]=c?8.6k(2f[j],c):""}2Q[i]=2f.2p("\\n")}8.18=2Q.2p("\\n");8.2t=V.2t.1q(o)},1u:7(){6 r,i;O(i=0;(r=8.2t[i]);i++)r.1u()},6k:7(s,c){y(V.6j.Z(s)){6 m;y(m=s.19(1L.39)){z 11 1L(m[1],m[2],c)}1d y(m=s.19(2d.39)){y(!2l||!H.Z(m)||2d.44.Z(m)){z 11 2d(s,m[1],m[2],m[3],c)}}1d z 11 1G(s,c)}z s+" {"+c+"}"}});V.1e({2t:[],1B:14.1i("1B"),36:{},2e:14.1i("2e"),1G:1G,2d:2d,1L:1L,2J:2J,2q:7(){6 p=8.1B+"|6i|6h|"+8.36;p=p.13(/(21|37)\\|/g,"");8.6j=11 1N("[>+~\\[]|([:.])[\
 \\\w-()]+\\\\1|:("+p+")");6 c="[^\\\\s(]+\\\\s*[+~]|@\\\\d+|:(";1G.44=11 1N(c+p+")","g");2d.44=11 1N(c+8.1B+")","g");2d.39=11 1N("(.*):("+8.36+")(.*)");1L.39=/(.*):(6i|6h).*/;8.1T()},1u:7(){8.2P.1u();8.1T()},2w:7(s,p){z 2v?(3T(s.2u,p)||s.18):8.1T(s)},1j:7(e,t,h){1j(e,t,h)}});7 1G(s,c){8.1c=V.2t.K;8.1x=1G.3x+8.1c;s=(s).19(F)||s||"*";8.40=s[1]||s;8.4M=1G.6g(8.40)+"."+8.1x+(s[2]||"");8.18=c;8.39=11 1N("\\\\s"+8.1x+"(\\\\s|$)","g");V.2t.1b(8);8.1S()};1t.1e({1h:1G,1g:7(){z 8.4M+" {"+8.18+"}"},1S:2s,15:7(e){e.1x+=" "+8.1x},3v:7(e){e.1x=e.1x.13(8.39,"$1")},1u:7(){6 m=V.2e[" *."+8.1x]=14(8.40);O(i=0;i<m.K;i++)8.15(m[i])}});1G.3x="5Y";1G.6f=/>/g;1G.6g=7(s){s=1p.38(s);z s.13(8.44,"").13(8.6f," ")};7 2d(s,a,d,t,c){8.6e=a||"*";8.6d=V.36[d];8.4L=t;8.1T(s,c)};1G.1e({1h:2d,1u:7(){6 m=14(8.6e);O(6 i=0;i<m.K;i++){6 t=(8.4L)?14(8.4L,m[i]):[m[i]];y(t.K)8.6d.2q(m[i],t,8)}}});6 A=/^4I/;6 U=/^43\\s*\\(\\s*([^)]*)\\)$/;6 M={86:"85",84:"83",82:"81",80:"7Z"};6 6b=1Z("17-22.4C",2k)+"?";2r+=".2K{4K:1M
 }";7 1L(s,p,c){8.1y=p;6 2N=c.19(1L.6a),m,e;y(2N){2N=2N[1];m=2N.2O(/\\s+/);O(6 i=0;(e=m[i]);i++){m[i]=A.Z(e)?{4I:e.1q(5,-1)}:(e.7Y(0)=="\'")?42(e):41(e)}2N=m}8.22=2N;8.1T(s,41(c))};1G.1e({1h:1L,1g:7(){z"."+8.1x+"{4K:7X}"},1S:7(){8.19=14(8.40);O(6 i=0;i<8.19.K;i++){6 r=8.19[i].J;y(!r[8.1y])r[8.1y]={18:""};r[8.1y].18+=";"+8.18;y(8.22!=1U)r[8.1y].22=8.22}},1u:7(){y(8.22==1U)z;O(6 i=0;i<8.19.K;i++){8.3Z(8.19[i])}},3Z:7(t){6 g=t.J[8.1y];y(g){6 c=[].4J(g.22||"");O(6 j=0;j<c.K;j++){y(3Y c[j]=="2M"){c[j]=t.2H(c[j].4I)}}c=c.2p("");6 u=c.19(U);6 h=1L[u?"69":"4E"].13(/%1/,8.1x);6 4G=g.18.13(/\'/g,\'"\');6 4F=M[8.1y+7W(t.6c)];y(u){6 p=16.3X(h);t.7V(4F,p);p.3W=6b;4H(p,4G,2c.3v(u[1]))}1d{h=h.13(/%2/,4G).13(/%3/,c);t.7U(4F,h)}t.J[8.1y]=1U}}});1L.6a=/22\\s*:\\s*([^;]*)(;|$)/;1L.69="<2M 3U=\'2K %1\' 2K 12=3w% 2b=0 2L=68/x-67>";1L.4E="<17:! 3U=\'2K %1\' 2K 1r=\'%2\'>%3</17:!>";7 2J(n,a){8.66=n;8.2q=a;8.2I={};V.36[n]=8};1t.1e({1h:2J,20:7(i){6 c=i[2];i.1c=c.1c+i[0].2a;y(!8.2I[i.1c]){6 t=i[1],j;O
 (j=0;j<t.K;j++)c.15(t[j]);8.2I[i.1c]=i}},35:7(i){y(8.2I[i.1c]){6 c=i[2];6 t=i[1],j;O(j=0;j<t.K;j++)c.3v(t[j]);3u 8.2I[i.1c]}}});V.1B.1g=7(){6 t=[],p;O(p 28 8){y(8[p].K>1)p+="\\\\([^)]*\\\\)";t.1b(p)}z t.2p("|")};V.1B["21"]=7(e){z e.D["17-21"]=="21"};V.1B["37"]=7(e){z e.D["17-21"]=="37"};6 64=(29<5.5)?"7T":"7S";6 63=(29<5.5)?"7R":"7Q";V.36.1g=V.1B.1g;6 3s=11 2J("65",7(e){6 i=1a;V.1j(e,64,7(){3s.20(i)});V.1j(e,63,7(){3s.35(i)})});6 3t=11 2J("7P",7(e){6 i=1a;V.1j(e,"7O",7(){3t.35(i);3t.20(i)});V.1j(e,"7N",7(){3t.35(i)});y(e==16.7M){3t.20(i)}});6 3V=11 2J("62",7(e){6 i=1a;V.1j(e,"7L",7(){3V.20(i)})});1j(16,"7K",7(){6 i=3V.2I,j;O(j 28 i)3V.35(i[j]);i=3s.2I;O(j 28 i)y(!i[j][0].61(60.7J))3s.35(i[j])});2o(1p);1p.1e({2H:7(n){34(n.5Z()){1m"3U":z"e.1x.13(/\\\\b\\\\s*5Y\\\\d+/g,\'\')";1m"1l":z"(e.4D||e.1l)"}z 8.1T(n)}});2n.15(/::/,":");3r.15(/\\\\([\\7I-7H-F]{1,4})/,7(m,o){m=m[o+1];z"\\\\u"+"7G".1q(m.K)+m})});2G=1o;y(2m)1k(3T("17-7F.5X",2k));V.1S();y(2l&&1s)1s.1S();y(33)1F.1S();1d{2F.7E
 (1Z("17-3q.4C",2k));1j(16,"7D",7(){y(16.5W=="33")32(1F.1S,0)})}}2j(e){31("4B [0]: "+e.5V)}5U{}};',62,634,'||||||var|function|this||||||||||||||||||||||||||if|return||||currentStyle||||||runtimeStyle|length||||for|||||||ie7CSS||||test||new|width|replace|cssQuery|add|document|ie7|cssText|match|arguments|push|id|else|specialize|left|toString|constructor|valueOf|addEventHandler|eval|src|case|addRecalc|true|AttributeSelector|slice|style|ie7HTML|Common|recalc|_0|that|className|position|false|minWidth|pseudoClasses|prototype|while|fr|IE7|Rule|getPixelWidth|clientWidth|viewport|ie7Layout|PseudoElement|none|RegExp|elements|auto|recalcs|styleSheets|init|inherit|null|selectors|right|body|all|makePath|register|link|content|String|addModule|parseInt|window|maxWidth|in|appVersion|uniqueID|height|Quote|DynamicRule|cache|se|fixWidth|_1|klass|catch|path|isHTML|quirksMode|encoder|ICommon|join|apply|HEADER|DUMMY|rules|href|httpRequest|getText|fixRight|top|fixed|_2|getFixedWidth|try|thisElement
 |ancestor|documentElement|loaded|getAttribute|instances|DynamicPseudoClass|ie7_anon|type|object|co|split|screen|ru|attributeSelectors|offsetWidth|setOverrideStyle|ParseMaster|exec|tagName|box|print|media|_3|alert|setTimeout|complete|switch|unregister|dynamicPseudoClasses|visited|parse|MATCH|tests|Function|getPixelValue|PERCENT|ex|_4|max|_5|addFix|hasLayout|marginTop|firstElementChild|disabled|value|size|font|load|safeString|_6|_7|delete|remove|100|PREFIX|StyleSheet|outerHTML|offsetLeft|offsetParent|absolute|isFixed|backgroundPositionX|getPath|border|boxSizing|min|fixedWidth|getPaddingWidth|fixes|Parser|self|styleSheet|st|modules|_8|x01|loadFile|class|_9|data|createElement|typeof|create|selector|decode|getString|url|COMPLEX|parseSelector|x22|nextElementSibling|_10|_11|_12|_13|parseFloat|autoLeft|pixelLeft|setExpression|Math|_14|BLANK_GIF|_15|_16|applyWidth|resizeWidth|getBorderWidth|getDefinedStyle|collapseMarginTop|_17|_18|_19|refresh|el|ca|getElementsByTagName|version|escap
 eChar|ancestorOf|_20|Error|htc|pngSrc|ANON|po|cs|addTimer|attr|concat|display|target|selectorText|first|regEscape|isMSIE|lang|parentNode|previousElementSibling|compareTagName|addResize|_21|getScreenLeft|marginLeft|positionLeft|_22|shiftLeft|screenLeft|_23|clipWidth|getBoundingClientRect|getOffsetLeft|bottom|setOffsetLeft|backgroundLeft|parentElement|backgroundImage|backgroundAttachment|Top|img|background|_24|clearCache|location|image|_25|_26|input|filter|Microsoft|sizing|resizeRight|_27|layoutWidth|_28|lastElementChild|clicked|submit|_29|Fix|_30|decoder|_31|parser|_32|gi|_33|styles|createStyleSheet|isXML|caching|_34|compareNamespace|getDocument|continue|links|error|callee|ignoreCase|_35|_36|ie7_debug|finally|description|readyState|js|ie7_class|toLowerCase|event|contains|active|_37|_38|hover|name|scriptlet|text|OBJECT|CONTENT|_39|canHaveChildren|dynamicPseudoClass|attach|CHILD|simple|after|before|UNKNOWN|createRule|B1|B2|_40|htmlFor|NS_IE|childElements|_41|scrollLeft|appendCh
 ild|Height|Width|Left|createTempElement|_42|onbeforeprint|replaceChild|_43|_44|B64|base64|indexOf|zoom|scale|opacity|DXImageTransform|progid|enabled|filters|Bottom|borderBox|spacing|margin|padding|applyHeight|0cm|verticalAlign|en|nextSibling|_45|title|_46|BUTTON|button|reset|onclick|clearInterval|PIXEL|removeEventHandler|pop|_47|_48|lastIndexOf|_49|_50|_51|_52|imports|trash|ie7_recalc|getInlineStyles|innerHTML|nodeType|ST|select|Array|_53|mimeType|Boolean|lastChild|firstChild|ES|_54|_55|_56|_57|_58|DE|status|scripts|search|onreadystatechange|addBehavior|quirks|0000|fA|da|srcElement|onmouseup|onmousedown|activeElement|onblur|onfocus|focus|onmouseleave|onmouseout|onmouseenter|onmouseover|insertAdjacentHTML|insertAdjacentElement|Number|inline|charAt|beforeEnd|after1|afterEnd|after0|afterBegin|before1|beforeBegin|before0|letter|line|child|level2|css|css2|static|clipHeight|marginRight|positionTop|default|center|clientLeft|removeChild|setOffsetTop|backgroundTop|repeat|no|backgroun
 dRepeat|attachment|onafterprint|onload|Image|php|propertyName|onpropertychange|INPUT|crop|Alpha|png|trans|IE7_PNG_SUFFIX|sizingMethod|AlphaImageLoader|graphics|fixHeight|Right|cellSpacing|collapse|borderCollapse|IE7_BOX_MODEL|getMarginWidth|Margin|Padding|paddingRight|paddingLeft|last|0px|styleFloat|collapseMarginBottom|layout|removeNode|HTML|alt|onsubmit|form|click|textarea|label|abbr|pow|67em|h6|83em|h5|1em|h4|17em|h3|5em|h2|2em|h1|html4|setInterval|9999|rect|clip|block|px|onunload|detachEvent|attachEvent|onresize|x27|import|namespace|ma|bprint|bscreen|ball|write|item|innerText|getTextContent|scopeName|toUpperCase|ownerDocument|Document|XML|URL|xml|unknown|previousSibling|number|successfully|ie5|instanceOf|Object|common|caller|gif|blank|file|loading|responseText|200|send|GET|open|XMLHTTP|ActiveXObject|Msxml2|ScriptEngineMajorVersion|ms_|ie7_off|CSS1Compat|compatMode|MSIE|navigator|alpha'.split('|'),0,{}))
diff --git a/web/static/js/ie7/ie7-xml-extras.js b/web/static/js/ie7/ie7-xml-extras.js
deleted file mode 100644
index 97846f6..0000000
--- a/web/static/js/ie7/ie7-xml-extras.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
-	IE7, version 0.9 (alpha) (2005-08-19)
-	Copyright: 2004-2005, Dean Edwards (http://dean.edwards.name/)
-	License: http://creativecommons.org/licenses/LGPL/2.1/
-*/
-function XMLHttpRequest(){var l=(ScriptEngineMajorVersion()>=5)?"Msxml2":"Microsoft";return new ActiveXObject(l+".XMLHTTP")};function DOMParser(){};DOMParser.prototype={toString:function(){return"[object DOMParser]"},parseFromString:function(s,c){var x=new ActiveXObject("Microsoft.XMLDOM");x.loadXML(s);return x},parseFromStream:new Function,baseURI:""};function XMLSerializer(){};XMLSerializer.prototype={toString:function(){return"[object XMLSerializer]"},serializeToString:function(r){return r.xml||r.outerHTML},serializeToStream:new Function};
diff --git a/web/static/js/ie7/ie7.gif b/web/static/js/ie7/ie7.gif
deleted file mode 100644
index 64a2c2d..0000000
Binary files a/web/static/js/ie7/ie7.gif and /dev/null differ
diff --git a/web/static/js/ie7/test-trans.png b/web/static/js/ie7/test-trans.png
deleted file mode 100644
index e187e2c..0000000
Binary files a/web/static/js/ie7/test-trans.png and /dev/null differ
diff --git a/web/static/js/ie7/test.html b/web/static/js/ie7/test.html
deleted file mode 100644
index ab78f46..0000000
--- a/web/static/js/ie7/test.html
+++ /dev/null
@@ -1,100 +0,0 @@
-<html xmlns:html="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head>
-<title>IE7 Test Page</title>
-<meta name="author" content="Dean Edwards"/>
-<!-- compliance patch for microsoft browsers -->
-<!--[if lt IE 7]>
-<script src="ie7-standard-p.js" type="text/javascript"></script>
-<script src="ie7-css3-selectors.js" type="text/javascript"></script>
-<script src="ie7-css-strict.js" type="text/javascript"></script>
-<![endif]-->
-<style type="text/css">
- body {background-color: #ccc;}
- img {border: none;}
- h1 {font-family: monospace;}
- h2 {background-color: black; color: white; font-style: normal;}
- h3 {margin: 0.1em 0;}
-</style>
-</head>
-
-<body>
-<div class="document">
-<div class="header">
-<h1>IE7 { css2: auto; }</h1>
-<hr />
-</div>
-
-<div class="content">
-
-<h2>Black & White Test</h2>
-
-<h3>Legend</h3>
-<style type="text/css">
- div.legend {height: 20px; font-weight: bold; text-indent: 4px;}
- #fail {background-color: black; color: white;}
- #pass {background-color: white; color: black;}
-</style>
-<div class="legend" id="pass">PASS</div>
-<div class="legend" id="fail">FAIL</div>
-
-<hr />
-
-<h3>ie7-html4.js</h3>
-<style type="text/css">
- #ie7-html4 {background-color: black; height: 20px;}
- #ie7-html4 abbr {display: block; background-color: white; height: 20px;}
-</style>
-<div id="ie7-html4"><abbr> </abbr></div>
-
-<h3>ie7-layout.js</h3>
-<style type="text/css">
- #ie7-layout {background-color: black; height: 20px; overflow: hidden;}
- #ie7-layout div.box {position: relative; top: -40px; background-color: white;
-   height: 40px; border-top: 20px black solid;}
-</style>
-<div id="ie7-layout"><div class="box"></div></div>
-
-<h3>ie7-graphics.js</h3>
-<style type="text/css">
- #ie7-graphics {background-color: white; height: 20px;}
- #ie7-graphics div.box {height: 20px; background: url(test-trans.png);}
-</style>
-<div id="ie7-graphics"><div class="box"></div></div>
-
-<h3>ie7-fixed.js</h3>
-<style type="text/css">
- #ie7-fixed {background-color: white; height: 20px;}
- #ie7-fixed div.box {position: fixed; top: -20px; background-color: black; height: 20px;}
-</style>
-<div id="ie7-fixed"><div class="box"></div></div>
-
-<h3>ie7-css2-selectors.js</h3>
-<style type="text/css">
- #ie7-css2-selectors {background-color: black; height: 20px;}
- #ie7-css2-selectors > span {display: block; background-color: white; height: 20px;}
-</style>
-<div id="ie7-css2-selectors"><span>&nbsp</span></div>
-
-<h3>ie7-css3-selectors.js</h3>
-<style type="text/css">
- #ie7-css3-selectors {background-color: black; height: 20px;}
- #ie7-css3-selectors:empty {background-color: white;}
-</style>
-<div id="ie7-css3-selectors"></div>
-
-<h3>ie7-css-strict.js</h3>
-<style type="text/css">
- #ie7-css-strict {background-color: black; height: 20px;}
- #ie7-css-strict > span.strict {display: block; background-color: white; height: 20px;}
- #ie7-css-strict > span {display: block; background-color: black}
-</style>
-<div id="ie7-css-strict"><span class="strict"></span></div>
-</div>
-
-<div class="footer">
-<hr />
-<a href="http://dean.edwards.name/IE7/"><img src="ie7.gif" width="80" height="15" alt="IE7 Enhanced"/></a>
-</div>
-</div>
-</body>
-</html>
diff --git a/web/static/js/jifty.js b/web/static/js/jifty.js
deleted file mode 100644
index e674eb4..0000000
--- a/web/static/js/jifty.js
+++ /dev/null
@@ -1,333 +0,0 @@
-/* An empty class so we can create things inside it */
-var Jifty = Class.create();
-
-/* General methods for dealing with forms, actions, and fields */
-/* Actions */
-var Action = Class.create();
-Action.prototype = {
-    // New takes the moniker, a string
-    initialize: function(moniker) {
-        this.moniker = moniker;
-
-        this.register = $('J:A-' + this.moniker);  // Simple case -- no ordering information
-        if (! this.register) {
-            // We need to go looking
-            var elements = document.getElementsByTagName('input');
-            for (var i = 0; i < elements.length; i++) {
-                if (Form.Element.getMoniker(elements[i]) == this.moniker) {
-                    this.register = elements[i];
-                    break;
-                }
-            }
-        }
-
-        this.form = Form.Element.getForm(this.register);
-        this.actionClass = this.register.value;
-    },
-
-    // Returns an Array of all fields in this Action
-    fields: function() {
-        var elements = new Array;
-        var possible = Form.getElements(this.form);
-
-        for (var i = 0; i < possible.length; i++) {
-            if (Form.Element.getMoniker(possible[i]) == this.moniker)
-                elements.push(possible[i]);
-        }
-        return elements;
-    },
-
-    // Serialize and return all fields needed for this action
-    serialize: function() {
-        var fields = this.fields();
-        var serialized = new Array;
-
-        for (var i = 0; i < fields.length; i++) {
-            serialized.push(Form.Element.serialize(fields[i].id));
-        }
-        return serialized.join('&');
-    },
-
-    // Validate the action
-    validate: function() {
-        show_wait_message();
-        var id = this.register.id;
-
-        new Ajax.Request(
-            '/validator.xml',  // Right now, the URL is actually completely irrelevant
-            {
-                asynchronous: 1,
-                method: "get",
-                parameters: this.serialize() + "&J:VALIDATE=1",
-                onComplete:
-                    function (request) {
-                        var response  = request.responseXML.documentElement;
-                        for (var action = response.firstChild; action != null; action = action.nextSibling) {
-                            if ((action.nodeName != 'action') || (action.getAttribute("id") != id))
-                                continue;
-                            for (var field = action.firstChild; field != null; field = field.nextSibling) {
-                                // Possibilities for field.nodeName: it could be #text (whitespace),
-                                // or 'blank' (the field was blank, don't mess with the error div), or 'ok'
-                                // (clear the error div!) or 'error' (fill in the error div!)
-                                if (field.nodeName == 'error') {
-                                    var err_div = document.getElementById(field.getAttribute("id"));
-                                    if (err_div != null) {
-                                        err_div.innerHTML = field.firstChild.data;
-                                    }
-                                } else if (field.nodeName == 'ok') {
-                                    var err_div = document.getElementById(field.getAttribute("id"));
-                                    if (err_div != null) {
-                                        err_div.innerHTML = '';
-                                    }
-                                }
-                            }
-                        }
-                        return true;
-                    }
-            }
-        ); 
-        hide_wait_message();
-        return false;
-    },
-
-    submit: function() {
-        show_wait_message();
-        new Ajax.Request(
-            '/empty',
-            { parameters: this.serialize() }
-        );
-        hide_wait_message();
-    }
-};
-
-
-
-/* Forms */
-// Return an Array of Actions that are in this form
-Form.getActions = function (element) {
-    var elements = new Array;
-    var possible = Form.getElements(element);
-
-    for (var i = 0; i < possible.length; i++) {
-        if (Form.Element.isRegistration(possible[i]))
-            elements.push(new Action(Form.Element.getMoniker(possible[i])));
-    }
-
-    return elements;
-};
-
-/* Fields */
-// Get the moniker for this form element
-// Takes an element or an element id
-Form.Element.getMoniker = function (element) {
-     // if we have an element id, get the element itself
-     if (typeof(element) == "string") {
-         element = $(element);    
-    }
-    if (/^J:A:F(:F)*-[^-]+-.+$/.test(element.name)) {
-        var bits = element.name.match(/^J:A:F(?::F)*-[^-]+-(.+)$/);
-        return bits[1];
-    } else if (/^J:A-(\d+-)?.+$/.test(element.name)) {
-        var bits = element.name.match(/^J:A-(?:\d+-)?(.+)$/);
-        return bits[1];
-    } else {
-        return null;
-    }
-};
-
-// Get the Action for this form element
-// Takes an element or an element id
-Form.Element.getAction = function (element) {
-        // if we have an element id, get the element itself
-        if (typeof(element) == "string") {
-            element = $(element);    
-            }
-    var moniker = Form.Element.getMoniker(element);
-    return new Action(moniker);
-}
-
-// Returns true if this form element is the registration for its action
-Form.Element.isRegistration = function (element) {
-    return /^J:A-/.test(element.name)
-};
-
-// Validates the action this form element is part of
-Form.Element.validate = function (element) {
-    Form.Element.getAction(element).validate();
-};
-
-// Form elements should AJAX validate if the CSS says so
-Behaviour.register({
-    'input.ajaxvalidation': function(elt) {
-        elt.onblur = function () {
-            Form.Element.validate(this);
-        } 
-    }
-});
-
-// Look up the form that this element is part of -- this is sometimes
-// more complicated than you'd think because the form may not exist
-// anymore, or the element may have been inserted into a new form.
-// Hence, we may need to walk the DOM.
-Form.Element.getForm = function (element) {
-    if (element.form)
-        return element.form;
-
-    for (var elt = element.parentNode; elt != null; elt = elt.parentNode) {
-        if (elt.nodeName == 'FORM') {
-            element.form = elt;
-            return elt;
-        }
-    }
-    return null;
-}
-
-function serialize(thing) {
-    var serialized = new Array;
-    for (n in thing) {
-        if (typeof(thing[n]) == "string" && thing[n].length)
-            serialized.push(encodeURIComponent(n) + '=' + 
-                            encodeURIComponent(thing[n]));
-    }
-    return serialized.join('&');
-}
-
-var fragments = {};
-var current_args = {};
-function region(name, args, path) {
-    fragments[name] = {name: name, args: args, path: path};
-    current_args[name] = {};
-}
-
-function update_region() {
-    show_wait_message();
-    arguments = arguments[0];
-    var name = arguments['name'];
-
-    var args = {};
-    for (var n in fragments[name].args) {
-        args[n] = fragments[name].args[n];
-    }
-    for (var n in current_args) {
-        if (typeof(current_args[n]) == "string") {
-            args[n] = current_args[n];
-            var parsed = n.match(/J:NV-region-(.*?)\.(.*)/);
-            
-            if ((parsed != null) && (parsed.length == 3) && (parsed[1] == name)) {
-                args[parsed[2]] = current_args[n];
-            }
-        }
-    }
-    for (var n in arguments['args']) {
-        args[n] = arguments['args'][n];
-        if (n.indexOf('J:NV-') != 0) {
-            current_args['J:NV-region-'+name+'.'+n] = args[n];
-            args['J:NV-region-'+name+'.'+n] = args[n];
-        }
-    }
-    var path;
-    if (arguments['fragment'] != null) {
-        path = arguments['fragment'];
-    } else {
-        path = fragments[name].path;
-    }
-    args['J:NV-region-'+name] = path;
-    current_args['J:NV-region-'+name] = path;
-
-    for (var i = 0; i < document.forms.length; i++) {
-        var form = document.forms[i];
-        for (var n in args) {
-            if ((typeof(args[n]) == "string") && (/^J:NV-/.test(n))) {
-                if (form[n]) {
-                    form[n].value = args[n];
-                } else {
-                    var hidden = document.createElement('input');
-                    hidden.setAttribute('type',  'hidden');
-                    hidden.setAttribute('name',  n);
-                    hidden.setAttribute('id',    n);
-                    hidden.setAttribute('value', args[n]);
-                    form.appendChild(hidden);
-                }
-            }
-        }
-    }
-
-    args['J-NAME'] = name;
-    args['J-PATH'] = document.URL;
-
-    var query = serialize(args);
-    if (arguments['submit']) {
-        var a = new Action(arguments['submit']);
-        query = query + '&' + a.serialize();
-    }
-
-    new Ajax.Updater('region-' + name,
-                     path,
-                     { parameters: query,
-                       onComplete: function () { Behaviour.apply();
-                        hide_wait_message();
-                       },
-                       evalScripts: true }
-                    );
-}
-
-function trace( msg ){
-  if( typeof( jsTrace ) != 'undefined' ){
-    jsTrace.send( msg );
-  }
-}
-
-
-function show_wait_message (){
-        chunk = document.getElementById('jifty-wait-message');
-        if (chunk) {  chunk.style.display= 'block';}
-}
-
-function hide_wait_message (){
-
-        chunk = document.getElementById('jifty-wait-message');
-         if (chunk) { chunk.style.display = "none";}
-}
-
-
-
-Jifty.Autocompleter = Class.create();
-Object.extend(Object.extend(Jifty.Autocompleter.prototype, Ajax.Autocompleter.prototype), {
-  initialize: function(element, update, url, options) {
-          this.baseInitialize(element, update, options);
-    this.options.asynchronous  = true;
-    this.options.onComplete    = this.onComplete.bind(this);
-    this.options.defaultParams = this.options.parameters || null;
-    this.url                   = url;
-  },
-
-  getUpdatedChoices: function() {
-    entry = encodeURIComponent("J:A-autocomplete")
-        + "=" +encodeURIComponent("Jifty::Action::Autocomplete");
-
-    entry += '&' + encodeURIComponent("J:A:F-argument-autocomplete") 
-        + "=" + encodeURIComponent(this.options.paramName);
-      
-    entry += '&' + encodeURIComponent("J:A:F-action-autocomplete") 
-        + "=" + encodeURIComponent(
-                        Form.Element.getMoniker(this.options.paramName)
-                        );
-
-    entry += '&'+ encodeURIComponent("J:ACTIONS") + '=' + encodeURIComponent("autocomplete");
-
-
-    this.options.parameters = this.options.callback ?
-      this.options.callback(this.element, entry) : entry;
-
-    if(this.options.defaultParams)
-      this.options.parameters += '&' + this.options.defaultParams;
-      
-    var action =  Form.Element.getAction(this.options.paramName);
-      this.options.parameters += '&' + action.serialize();
-
-    new Ajax.Request(this.url, this.options);
-  }
-
-
-});
-
diff --git a/web/static/js/jsTrace.js b/web/static/js/jsTrace.js
deleted file mode 100644
index 5e8c93e..0000000
--- a/web/static/js/jsTrace.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/*------------------------------------------------------------------------------
-Function:       jsTrace()
-Author:         Aaron Gustafson (aaron at easy-designs dot net)
-Creation Date:  26 October 2005
-Version:        1.0
-Homepage:       http://www.easy-designs.net/code/jsTrace/
-License:        Creative Commons Attribution-ShareAlike 2.0 License
-                http://creativecommons.org/licenses/by-sa/2.0/
-Note:           If you change or improve on this script, please let us know by 
-                emailing the author (above) with a link to your demo page.
-------------------------------------------------------------------------------*/
-var jsTrace = { debugging_on: false, window: null, viewport: null, init: function(){ if( !document.getElementsByTagName || !document.getElementById || !document.createElement || !document.createTextNode ) return; jsTrace.createWindow(); jsTrace.debugging_on = true;}, createWindow: function(){ jsTrace.window = document.createElement( 'div' ); jsTrace.window.style.background = '#000'; jsTrace.window.style.font = '80% "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, sans-serif'; jsTrace.window.style.padding = '2px'; jsTrace.window.style.position = 'absolute'; jsTrace.window.style.top = '50px'; jsTrace.window.style.left = '700px'; jsTrace.window.style.height = '360px'; jsTrace.window.style.zIndex = '100'; jsTrace.window.style.minHeight = '150px'; jsTrace.window.style.width = '190px'; jsTrace.window.style.minWidth = '150px'; var x = document.createElement('span'); x.style.border = '1px solid #000'; x.style.cursor = 'pointer'; x.style.color = '#000'; x.style.display = 'bl
 ock'; x.style.lineHeight = '.5em'; x.style.padding = '0 0 3px'; x.style.position = 'absolute'; x.style.top = '4px'; x.style.right = '4px'; jsTrace.addEvent( x, 'click', function(){ jsTrace.kill();} ); x.setAttribute( 'title', 'Close jsTrace Debugger' ); x.appendChild( document.createTextNode( 'x' ) ); jsTrace.window.appendChild( x ); var sh = document.createElement('div'); sh.style.position = 'absolute'; sh.style.bottom = '3px'; sh.style.right = '3px'; var sg = document.createElement('span'); sg.style.border = '5px solid #ccc'; sg.style.borderLeftColor = sg.style.borderTopColor = '#000'; sg.style.cursor = 'pointer'; sg.style.color = '#ccc'; sg.style.display = 'block'; sg.style.height = '0'; sg.style.width = '0'; sg.style.overflow = 'hidden'; sg.setAttribute( 'title', 'Resize the jsTrace Debugger' ); if( typeof( Drag ) != 'undefined' ){ sg.xFrom = 0; sg.yFrom = 0; Drag.init( sg, null, null, null, null, null, true, true ); sg.onDrag = function( x, y ){ jsTrace.resizeX( x, this
  ); jsTrace.resizeY( y, this );}; sh.appendChild( sg ); jsTrace.window.appendChild( sh );} var tools = document.createElement( 'div' ); tools.style.fontSize = '.7em'; tools.style.fontVariant = 'small-caps'; tools.style.lineHeight = '10px'; tools.style.position = 'absolute'; tools.style.bottom = '5px'; tools.style.left = '3px'; var dl = document.createElement( 'span' ); dl.style.color = '#ccc'; dl.style.padding = '0 10px 0 0'; dl.style.overflow = 'hidden'; dl.style.cursor = 'pointer'; dl.setAttribute( 'title', 'Add a Delimeter' ); dl.appendChild( document.createTextNode( 'delimit' ) ); jsTrace.addEvent( dl, 'click', function(){ jsTrace.sendDelimeter();} ); tools.appendChild( dl ); var cl = document.createElement( 'span' ); cl.style.color = '#ccc'; cl.style.padding = '0 10px 0 0'; cl.style.overflow = 'hidden'; cl.style.cursor = 'pointer'; cl.setAttribute( 'title', 'Add a Delimeter' ); cl.appendChild( document.createTextNode( 'clear' ) ); jsTrace.addEvent( cl, 'click', function
 (){ jsTrace.clearWindow();} ); tools.appendChild( cl ); jsTrace.window.appendChild( tools ); var header = document.createElement( 'h3' ); header.style.background = '#ccc'; header.style.color = '#000'; header.style.cursor = 'pointer'; header.style.fontSize = '1em'; header.style.fontVariant = 'small-caps'; header.style.margin = '0 0 2px'; header.style.padding = '5px 10px'; header.style.lineHeight = '15px'; header.appendChild( document.createTextNode( 'jsTrace Debugger' ) ); jsTrace.window.appendChild( header ); jsTrace.viewport = document.createElement( 'pre' ); jsTrace.viewport.style.border = '1px solid #ccc'; jsTrace.viewport.style.color = '#ebebeb'; jsTrace.viewport.style.fontSize = '1.2em'; jsTrace.viewport.style.margin = '0'; jsTrace.viewport.style.padding = '0 3px'; jsTrace.viewport.style.position = 'absolute'; jsTrace.viewport.style.top = '30px'; jsTrace.viewport.style.left = '2px'; jsTrace.viewport.style.overflow = 'auto'; jsTrace.viewport.style.width = ( parseInt( jsT
 race.window.style.width ) - 8 ) + 'px'; jsTrace.viewport.style.height = ( parseInt( jsTrace.window.style.height ) - 45 ) + 'px'; jsTrace.window.appendChild( jsTrace.viewport ); document.getElementsByTagName( 'body' )[0].appendChild( jsTrace.window ); if( typeof( Drag ) != 'undefined' ){ Drag.init( header, jsTrace.window );} }, resizeX: function( x, grip ){ var width = parseInt( jsTrace.window.style.width ); var newWidth = Math.abs( width - ( x - grip.xFrom ) ) + 'px'; if( parseInt( newWidth ) < parseInt( jsTrace.window.style.minWidth ) ) newWidth = jsTrace.window.style.minWidth; jsTrace.window.style.width = newWidth; grip.xFrom = x; jsTrace.viewport.style.width = ( parseInt( jsTrace.window.style.width ) - 8 ) + 'px';}, resizeY: function( y, grip ){ var height = parseInt( jsTrace.window.style.height ); var newHeight = Math.abs( height - ( y - grip.yFrom ) ) + 'px'; if( parseInt( newHeight ) < parseInt( jsTrace.window.style.minHeight ) ) newHeight = jsTrace.window.style.minHei
 ght; jsTrace.window.style.height = newHeight; grip.yFrom = y; jsTrace.viewport.style.height = ( parseInt( jsTrace.window.style.height ) - 45 ) + 'px';}, send: function( text ){ text = text + "<br />"; jsTrace.viewport.innerHTML += text;}, sendDelimeter: function(){ jsTrace.send( '<span style="color: #f00">--------------------</span>' );}, clearWindow: function(){ jsTrace.viewport.innerHTML = '';}, kill: function() { jsTrace.window.parentNode.removeChild( jsTrace.window ); jsTrace.debugging_on = false;}, addEvent: function( obj, type, fn ){ if (obj.addEventListener) obj.addEventListener( type, fn, false ); else if (obj.attachEvent) { obj["e"+type+fn] = fn; obj[type+fn] = function() { obj["e"+type+fn]( window.event );}; obj.attachEvent( "on"+type, obj[type+fn] );} }, removeEvent: function ( obj, type, fn ) { if (obj.removeEventListener) obj.removeEventListener( type, fn, false ); else if (obj.detachEvent) { obj.detachEvent( "on"+type, obj[type+fn] ); obj[type+fn] = null; obj["
 e"+type+fn] = null;} } }; jsTrace.addEvent( window, 'load', jsTrace.init ); 
\ No newline at end of file
diff --git a/web/static/js/key_bindings.js b/web/static/js/key_bindings.js
deleted file mode 100644
index c480f0c..0000000
--- a/web/static/js/key_bindings.js
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright 2004-2005, Best Practical Solutions, LLC
-// This Library is licensed to you under the same terms as Perl 5.x
-
-var bindings = Array;
-
-document.onkeydown = doClick;
-function doClick(e) {
-    var targ;
-        if (!e) var e = window.event;
-            if (e.target) targ = e.target;
-            else if (e.srcElement) targ = e.srcElement;
-        if (targ.nodeType == 3) // defeat Safari bug
-                targ = targ.parentNode;
-   
-   // safari or mozilla
-   if ( ( ! e.metaKey && ! e.altKey &&  ! e.ctrlKey )
-        && (
-        (targ == document.body) || 
-       (targ ==  document.getElementsByTagName('html')[0])  
-        ) ){
-	var code = String.fromCharCode(e.keyCode);
-    var binding = getKeyBinding(code);
-   if (binding) {
-   if (binding["action"] == "goto") {
-        document.location = (binding["data"]);
-    } 
-   else if (binding["action"] == "focus") {
-      var elements = document.getElementsByName(binding["data"]);
-       elements[0].focus();
-    }
-   else if (binding["action"] == "click") {
-      var elements = document.getElementsByName(binding["data"]);
-       elements[0].click();
-    }
-
- }     
-
-}
-}
-
-function addKeyBinding(key, action, data, label) {
-    var binding = new Array;
-    binding["action"] = action;
-    binding["data"] = data;
-    binding["label"] = label;
-    bindings[key] = binding;
-}
-
-
-function getKeyBinding(key) {
-    return(bindings[key]);
-}
-
-
-function writeKeyBindingLegend() {
-    var content = '';
-    for  (var key in bindings) {
-    if ( bindings[key]['label']) {
-    content = content + '<dt>'+key + '</dt>' +'<dd>'+bindings[key]['label'] +'</dd>'; 
-    }
-    }
-    if (content) {
-    document.write('<div class="keybindings">');
-    document.write('<dl class="keybindings">');
-    document.write(content);
-    document.write('</dl>');
-    document.write('</div>');
-    }
-}
diff --git a/web/static/js/prototype.js b/web/static/js/prototype.js
deleted file mode 100644
index fbdab91..0000000
--- a/web/static/js/prototype.js
+++ /dev/null
@@ -1,1038 +0,0 @@
-/*  Prototype JavaScript framework, version 1.3.1
- *  (c) 2005 Sam Stephenson <sam at conio.net>
- *
- *  THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff
- *  against the source tree, available from the Prototype darcs repository. 
- *
- *  Prototype is freely distributable under the terms of an MIT-style license.
- *
- *  For details, see the Prototype web site: http://prototype.conio.net/
- *
-/*--------------------------------------------------------------------------*/
-
-var Prototype = {
-  Version: '1.3.1',
-  emptyFunction: function() {}
-}
-
-var Class = {
-  create: function() {
-    return function() { 
-      this.initialize.apply(this, arguments);
-    }
-  }
-}
-
-var Abstract = new Object();
-
-Object.extend = function(destination, source) {
-  for (property in source) {
-    destination[property] = source[property];
-  }
-  return destination;
-}
-
-Object.prototype.extend = function(object) {
-  return Object.extend.apply(this, [this, object]);
-}
-
-Function.prototype.bind = function(object) {
-  var __method = this;
-  return function() {
-    __method.apply(object, arguments);
-  }
-}
-
-Function.prototype.bindAsEventListener = function(object) {
-  var __method = this;
-  return function(event) {
-    __method.call(object, event || window.event);
-  }
-}
-
-Number.prototype.toColorPart = function() {
-  var digits = this.toString(16);
-  if (this < 16) return '0' + digits;
-  return digits;
-}
-
-var Try = {
-  these: function() {
-    var returnValue;
-
-    for (var i = 0; i < arguments.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() {
-    setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
-  },
-
-  onTimerEvent: function() {
-    if (!this.currentlyExecuting) {
-      try { 
-        this.currentlyExecuting = true;
-        this.callback(); 
-      } finally { 
-        this.currentlyExecuting = false;
-      }
-    }
-  }
-}
-
-/*--------------------------------------------------------------------------*/
-
-function $() {
-  var elements = new Array();
-
-  for (var i = 0; i < arguments.length; i++) {
-    var element = arguments[i];
-    if (typeof element == 'string')
-      element = document.getElementById(element);
-
-    if (arguments.length == 1) 
-      return element;
-
-    elements.push(element);
-  }
-
-  return elements;
-}
-
-if (!Array.prototype.push) {
-  Array.prototype.push = function() {
-		var startLength = this.length;
-		for (var i = 0; i < arguments.length; i++)
-      this[startLength + i] = arguments[i];
-	  return this.length;
-  }
-}
-
-if (!Function.prototype.apply) {
-  // Based on code from http://www.youngpup.net/
-  Function.prototype.apply = function(object, parameters) {
-    var parameterStrings = new Array();
-    if (!object)     object = window;
-    if (!parameters) parameters = new Array();
-    
-    for (var i = 0; i < parameters.length; i++)
-      parameterStrings[i] = 'parameters[' + i + ']';
-    
-    object.__apply__ = this;
-    var result = eval('object.__apply__(' + 
-      parameterStrings.join(', ') + ')');
-    object.__apply__ = null;
-    
-    return result;
-  }
-}
-
-String.prototype.extend({
-  stripTags: function() {
-    return this.replace(/<\/?[^>]+>/gi, '');
-  },
-
-  escapeHTML: function() {
-    var div = document.createElement('div');
-    var text = document.createTextNode(this);
-    div.appendChild(text);
-    return div.innerHTML;
-  },
-
-  unescapeHTML: function() {
-    var div = document.createElement('div');
-    div.innerHTML = this.stripTags();
-    return div.childNodes[0].nodeValue;
-  }
-});
-
-var Ajax = {
-  getTransport: function() {
-    return Try.these(
-      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
-      function() {return new ActiveXObject('Microsoft.XMLHTTP')},
-      function() {return new XMLHttpRequest()}
-    ) || false;
-  }
-}
-
-Ajax.Base = function() {};
-Ajax.Base.prototype = {
-  setOptions: function(options) {
-    this.options = {
-      method:       'post',
-      asynchronous: true,
-      parameters:   ''
-    }.extend(options || {});
-  },
-
-  responseIsSuccess: function() {
-    return this.transport.status == undefined
-        || this.transport.status == 0 
-        || (this.transport.status >= 200 && this.transport.status < 300);
-  },
-
-  responseIsFailure: function() {
-    return !this.responseIsSuccess();
-  }
-}
-
-Ajax.Request = Class.create();
-Ajax.Request.Events = 
-  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
-
-Ajax.Request.prototype = (new Ajax.Base()).extend({
-  initialize: function(url, options) {
-    this.transport = Ajax.getTransport();
-    this.setOptions(options);
-    this.request(url);
-  },
-
-  request: function(url) {
-    var parameters = this.options.parameters || '';
-    if (parameters.length > 0) parameters += '&_=';
-
-    try {
-      if (this.options.method == 'get')
-        url += '?' + parameters;
-
-      this.transport.open(this.options.method, url,
-        this.options.asynchronous);
-
-      if (this.options.asynchronous) {
-        this.transport.onreadystatechange = this.onStateChange.bind(this);
-        setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10);
-      }
-
-      this.setRequestHeaders();
-
-      var body = this.options.postBody ? this.options.postBody : parameters;
-      this.transport.send(this.options.method == 'post' ? body : null);
-
-    } catch (e) {
-    }
-  },
-
-  setRequestHeaders: function() {
-    var requestHeaders = 
-      ['X-Requested-With', 'XMLHttpRequest',
-       'X-Prototype-Version', Prototype.Version];
-
-    if (this.options.method == 'post') {
-      requestHeaders.push('Content-type', 
-        'application/x-www-form-urlencoded');
-
-      /* Force "Connection: close" for Mozilla browsers to work around
-       * a bug where XMLHttpReqeuest sends an incorrect Content-length
-       * header. See Mozilla Bugzilla #246651. 
-       */
-      if (this.transport.overrideMimeType)
-        requestHeaders.push('Connection', 'close');
-    }
-
-    if (this.options.requestHeaders)
-      requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
-
-    for (var i = 0; i < requestHeaders.length; i += 2)
-      this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
-  },
-
-  onStateChange: function() {
-    var readyState = this.transport.readyState;
-    if (readyState != 1)
-      this.respondToReadyState(this.transport.readyState);
-  },
-
-  respondToReadyState: function(readyState) {
-    var event = Ajax.Request.Events[readyState];
-
-    if (event == 'Complete')
-      (this.options['on' + this.transport.status]
-       || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
-       || Prototype.emptyFunction)(this.transport);
-
-    (this.options['on' + event] || Prototype.emptyFunction)(this.transport);
-
-    /* Avoid memory leak in MSIE: clean up the oncomplete event handler */
-    if (event == 'Complete')
-      this.transport.onreadystatechange = Prototype.emptyFunction;
-  }
-});
-
-Ajax.Updater = Class.create();
-Ajax.Updater.ScriptFragment = '(?:<script.*?>)((\n|.)*?)(?:<\/script>)';
-
-Ajax.Updater.prototype.extend(Ajax.Request.prototype).extend({
-  initialize: function(container, url, options) {
-    this.containers = {
-      success: container.success ? $(container.success) : $(container),
-      failure: 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() {
-      this.updateContent();
-      onComplete(this.transport);
-    }).bind(this);
-
-    this.request(url);
-  },
-
-  updateContent: function() {
-    var receiver = this.responseIsSuccess() ?
-      this.containers.success : this.containers.failure;
-
-    var match    = new RegExp(Ajax.Updater.ScriptFragment, 'img');
-    var response = this.transport.responseText.replace(match, '');
-    var scripts  = this.transport.responseText.match(match);
-
-    if (receiver) {
-      if (this.options.insertion) {
-        new this.options.insertion(receiver, response);
-      } else {
-        receiver.innerHTML = response;
-      }
-    }
-
-    if (this.responseIsSuccess()) {
-      if (this.onComplete)
-        setTimeout((function() {this.onComplete(
-          this.transport)}).bind(this), 10);
-    }
-
-    if (this.options.evalScripts && scripts) {
-      match = new RegExp(Ajax.Updater.ScriptFragment, 'im');
-      setTimeout((function() {
-        for (var i = 0; i < scripts.length; i++)
-          eval(scripts[i].match(match)[1]);
-      }).bind(this), 10);
-    }
-  }
-});
-
-Ajax.PeriodicalUpdater = Class.create();
-Ajax.PeriodicalUpdater.prototype = (new Ajax.Base()).extend({
-  initialize: function(container, url, options) {
-    this.setOptions(options);
-    this.onComplete = this.options.onComplete;
-
-    this.frequency = (this.options.frequency || 2);
-    this.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.onComplete = undefined;
-    clearTimeout(this.timer);
-    (this.onComplete || Ajax.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);
-  }
-});
-
-document.getElementsByClassName = function(className) {
-  var children = document.getElementsByTagName('*') || document.all;
-  var elements = new Array();
-  
-  for (var i = 0; i < children.length; i++) {
-    var child = children[i];
-    var classNames = child.className.split(' ');
-    for (var j = 0; j < classNames.length; j++) {
-      if (classNames[j] == className) {
-        elements.push(child);
-        break;
-      }
-    }
-  }
-  
-  return elements;
-}
-
-/*--------------------------------------------------------------------------*/
-
-if (!window.Element) {
-  var Element = new Object();
-}
-
-Object.extend(Element, {
-  toggle: function() {
-    for (var i = 0; i < arguments.length; i++) {
-      var element = $(arguments[i]);
-      element.style.display = 
-        (element.style.display == 'none' ? '' : 'none');
-    }
-  },
-
-  hide: function() {
-    for (var i = 0; i < arguments.length; i++) {
-      var element = $(arguments[i]);
-      element.style.display = 'none';
-    }
-  },
-
-  show: function() {
-    for (var i = 0; i < arguments.length; i++) {
-      var element = $(arguments[i]);
-      element.style.display = '';
-    }
-  },
-
-  remove: function(element) {
-    element = $(element);
-    element.parentNode.removeChild(element);
-  },
-   
-  getHeight: function(element) {
-    element = $(element);
-    return element.offsetHeight; 
-  },
-
-  hasClassName: function(element, className) {
-    element = $(element);
-    if (!element)
-      return;
-    var a = element.className.split(' ');
-    for (var i = 0; i < a.length; i++) {
-      if (a[i] == className)
-        return true;
-    }
-    return false;
-  },
-
-  addClassName: function(element, className) {
-    element = $(element);
-    Element.removeClassName(element, className);
-    element.className += ' ' + className;
-  },
-
-  removeClassName: function(element, className) {
-    element = $(element);
-    if (!element)
-      return;
-    var newClassName = '';
-    var a = element.className.split(' ');
-    for (var i = 0; i < a.length; i++) {
-      if (a[i] != className) {
-        if (i > 0)
-          newClassName += ' ';
-        newClassName += a[i];
-      }
-    }
-    element.className = newClassName;
-  },
-  
-  // removes whitespace-only text node children
-  cleanWhitespace: function(element) {
-    var element = $(element);
-    for (var i = 0; i < element.childNodes.length; i++) {
-      var node = element.childNodes[i];
-      if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) 
-        Element.remove(node);
-    }
-  }
-});
-
-var Toggle = new Object();
-Toggle.display = Element.toggle;
-
-/*--------------------------------------------------------------------------*/
-
-Abstract.Insertion = function(adjacency) {
-  this.adjacency = adjacency;
-}
-
-Abstract.Insertion.prototype = {
-  initialize: function(element, content) {
-    this.element = $(element);
-    this.content = content;
-    
-    if (this.adjacency && this.element.insertAdjacentHTML) {
-      this.element.insertAdjacentHTML(this.adjacency, this.content);
-    } else {
-      this.range = this.element.ownerDocument.createRange();
-      if (this.initializeRange) this.initializeRange();
-      this.fragment = this.range.createContextualFragment(this.content);
-      this.insertContent();
-    }
-  }
-}
-
-var Insertion = new Object();
-
-Insertion.Before = Class.create();
-Insertion.Before.prototype = (new Abstract.Insertion('beforeBegin')).extend({
-  initializeRange: function() {
-    this.range.setStartBefore(this.element);
-  },
-  
-  insertContent: function() {
-    this.element.parentNode.insertBefore(this.fragment, this.element);
-  }
-});
-
-Insertion.Top = Class.create();
-Insertion.Top.prototype = (new Abstract.Insertion('afterBegin')).extend({
-  initializeRange: function() {
-    this.range.selectNodeContents(this.element);
-    this.range.collapse(true);
-  },
-  
-  insertContent: function() {  
-    this.element.insertBefore(this.fragment, this.element.firstChild);
-  }
-});
-
-Insertion.Bottom = Class.create();
-Insertion.Bottom.prototype = (new Abstract.Insertion('beforeEnd')).extend({
-  initializeRange: function() {
-    this.range.selectNodeContents(this.element);
-    this.range.collapse(this.element);
-  },
-  
-  insertContent: function() {
-    this.element.appendChild(this.fragment);
-  }
-});
-
-Insertion.After = Class.create();
-Insertion.After.prototype = (new Abstract.Insertion('afterEnd')).extend({
-  initializeRange: function() {
-    this.range.setStartAfter(this.element);
-  },
-  
-  insertContent: function() {
-    this.element.parentNode.insertBefore(this.fragment, 
-      this.element.nextSibling);
-  }
-});
-
-var Field = {
-  clear: function() {
-    for (var i = 0; i < arguments.length; i++)
-      $(arguments[i]).value = '';
-  },
-
-  focus: function(element) {
-    $(element).focus();
-  },
-  
-  present: function() {
-    for (var i = 0; i < arguments.length; i++)
-      if ($(arguments[i]).value == '') return false;
-    return true;
-  },
-  
-  select: function(element) {
-    $(element).select();
-  },
-   
-  activate: function(element) {
-    $(element).focus();
-    $(element).select();
-  }
-}
-
-/*--------------------------------------------------------------------------*/
-
-var Form = {
-  serialize: function(form) {
-    var elements = Form.getElements($(form));
-    var queryComponents = new Array();
-    
-    for (var i = 0; i < elements.length; i++) {
-      var queryComponent = Form.Element.serialize(elements[i]);
-      if (queryComponent)
-        queryComponents.push(queryComponent);
-    }
-    
-    return queryComponents.join('&');
-  },
-  
-  getElements: function(form) {
-    var form = $(form);
-    var elements = new Array();
-
-    for (tagName in Form.Element.Serializers) {
-      var tagElements = form.getElementsByTagName(tagName);
-      for (var j = 0; j < tagElements.length; j++)
-        elements.push(tagElements[j]);
-    }
-    return elements;
-  },
-  
-  getInputs: function(form, typeName, name) {
-    var form = $(form);
-    var inputs = form.getElementsByTagName('input');
-    
-    if (!typeName && !name)
-      return inputs;
-      
-    var matchingInputs = new Array();
-    for (var i = 0; i < inputs.length; i++) {
-      var input = inputs[i];
-      if ((typeName && input.type != typeName) ||
-          (name && input.name != name)) 
-        continue;
-      matchingInputs.push(input);
-    }
-
-    return matchingInputs;
-  },
-
-  disable: function(form) {
-    var elements = Form.getElements(form);
-    for (var i = 0; i < elements.length; i++) {
-      var element = elements[i];
-      element.blur();
-      element.disabled = 'true';
-    }
-  },
-
-  enable: function(form) {
-    var elements = Form.getElements(form);
-    for (var i = 0; i < elements.length; i++) {
-      var element = elements[i];
-      element.disabled = '';
-    }
-  },
-
-  focusFirstElement: function(form) {
-    var form = $(form);
-    var elements = Form.getElements(form);
-    for (var i = 0; i < elements.length; i++) {
-      var element = elements[i];
-      if (element.type != 'hidden' && !element.disabled) {
-        Field.activate(element);
-        break;
-      }
-    }
-  },
-
-  reset: function(form) {
-    $(form).reset();
-  }
-}
-
-Form.Element = {
-  serialize: function(element) {
-    var element = $(element);
-    var method = element.tagName.toLowerCase();
-    var parameter = Form.Element.Serializers[method](element);
-    
-    if (parameter)
-      return encodeURIComponent(parameter[0]) + '=' + 
-        encodeURIComponent(parameter[1]);                   
-  },
-  
-  getValue: function(element) {
-    var element = $(element);
-    var method = element.tagName.toLowerCase();
-    var parameter = Form.Element.Serializers[method](element);
-    
-    if (parameter) 
-      return parameter[1];
-  }
-}
-
-Form.Element.Serializers = {
-  input: function(element) {
-    switch (element.type.toLowerCase()) {
-      case 'submit':
-      case 'hidden':
-      case 'password':
-      case 'text':
-        return Form.Element.Serializers.textarea(element);
-      case 'checkbox':  
-      case 'radio':
-        return Form.Element.Serializers.inputSelector(element);
-    }
-    return false;
-  },
-
-  inputSelector: function(element) {
-    if (element.checked)
-      return [element.name, element.value];
-  },
-
-  textarea: function(element) {
-    return [element.name, element.value];
-  },
-
-  select: function(element) {
-    var value = '';
-    if (element.type == 'select-one') {
-      var index = element.selectedIndex;
-      if (index >= 0)
-        value = element.options[index].value || element.options[index].text;
-    } else {
-      value = new Array();
-      for (var i = 0; i < element.length; i++) {
-        var opt = element.options[i];
-        if (opt.selected)
-          value.push(opt.value || opt.text);
-      }
-    }
-    return [element.name, value];
-  }
-}
-
-/*--------------------------------------------------------------------------*/
-
-var $F = Form.Element.getValue;
-
-/*--------------------------------------------------------------------------*/
-
-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();
-    if (this.lastValue != value) {
-      this.callback(this.element, value);
-      this.lastValue = value;
-    }
-  }
-}
-
-Form.Element.Observer = Class.create();
-Form.Element.Observer.prototype = (new Abstract.TimedObserver()).extend({
-  getValue: function() {
-    return Form.Element.getValue(this.element);
-  }
-});
-
-Form.Observer = Class.create();
-Form.Observer.prototype = (new Abstract.TimedObserver()).extend({
-  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() {
-    var elements = Form.getElements(this.element);
-    for (var i = 0; i < elements.length; i++)
-      this.registerCallback(elements[i]);
-  },
-  
-  registerCallback: function(element) {
-    if (element.type) {
-      switch (element.type.toLowerCase()) {
-        case 'checkbox':  
-        case 'radio':
-          element.target = this;
-          element.prev_onclick = element.onclick || Prototype.emptyFunction;
-          element.onclick = function() {
-            this.prev_onclick(); 
-            this.target.onElementEvent();
-          }
-          break;
-        case 'password':
-        case 'text':
-        case 'textarea':
-        case 'select-one':
-        case 'select-multiple':
-          element.target = this;
-          element.prev_onchange = element.onchange || Prototype.emptyFunction;
-          element.onchange = function() {
-            this.prev_onchange(); 
-            this.target.onElementEvent();
-          }
-          break;
-      }
-    }    
-  }
-}
-
-Form.Element.EventObserver = Class.create();
-Form.Element.EventObserver.prototype = (new Abstract.EventObserver()).extend({
-  getValue: function() {
-    return Form.Element.getValue(this.element);
-  }
-});
-
-Form.EventObserver = Class.create();
-Form.EventObserver.prototype = (new Abstract.EventObserver()).extend({
-  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,
-
-  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;
-    }
-  },
-
-  // 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; i < Event.observers.length; i++) {
-      Event.stopObserving.apply(this, Event.observers[i]);
-      Event.observers[i][0] = null;
-    }
-    Event.observers = false;
-  },
-
-  observe: function(element, name, observer, useCapture) {
-    var element = $(element);
-    useCapture = useCapture || false;
-    
-    if (name == 'keypress' &&
-        ((navigator.appVersion.indexOf('AppleWebKit') > 0) 
-        || element.attachEvent))
-      name = 'keydown';
-    
-    this._observeAndCache(element, name, observer, useCapture);
-  },
-
-  stopObserving: function(element, name, observer, useCapture) {
-    var element = $(element);
-    useCapture = useCapture || false;
-    
-    if (name == 'keypress' &&
-        ((navigator.appVersion.indexOf('AppleWebKit') > 0) 
-        || element.detachEvent))
-      name = 'keydown';
-    
-    if (element.removeEventListener) {
-      element.removeEventListener(name, observer, useCapture);
-    } else if (element.detachEvent) {
-      element.detachEvent('on' + name, observer);
-    }
-  }
-});
-
-/* prevent memory leaks in 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];
-  },
-
-  // 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;
-  },
-
-  clone: function(source, target) {
-    source = $(source);
-    target = $(target);
-    target.style.position = 'absolute';
-    var offsets = this.cumulativeOffset(source);
-    target.style.top    = offsets[1] + 'px';
-    target.style.left   = offsets[0] + 'px';
-    target.style.width  = source.offsetWidth + 'px';
-    target.style.height = source.offsetHeight + 'px';
-  }
-}
diff --git a/web/static/js/rico.js b/web/static/js/rico.js
deleted file mode 100644
index a1f9113..0000000
--- a/web/static/js/rico.js
+++ /dev/null
@@ -1,2666 +0,0 @@
-/**
-  *
-  *  Copyright 2005 Sabre Airline Solutions
-  *
-  *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
-  *  file except in compliance with the License. You may obtain a copy of the License at
-  *
-  *         http://www.apache.org/licenses/LICENSE-2.0
-  *
-  *  Unless required by applicable law or agreed to in writing, software distributed under the
-  *  License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
-  *  either express or implied. See the License for the specific language governing permissions
-  *  and limitations under the License.
-  **/
-
-
-//-------------------- rico.js
-var Rico = {
-  Version: '1.1-beta2'
-}
-
-Rico.ArrayExtensions = new Array();
-
-if (Object.prototype.extend) {
-   // in prototype.js...
-   Rico.ArrayExtensions[ Rico.ArrayExtensions.length ] = Object.prototype.extend;
-}
-
-if (Array.prototype.push) {
-   // in prototype.js...
-   Rico.ArrayExtensions[ Rico.ArrayExtensions.length ] = Array.prototype.push;
-}
-
-if (!Array.prototype.remove) {
-   Array.prototype.remove = function(dx) {
-      if( isNaN(dx) || dx > this.length )
-         return false;
-      for( var i=0,n=0; i<this.length; i++ )
-         if( i != dx )
-            this[n++]=this[i];
-      this.length-=1;
-   };
-  Rico.ArrayExtensions[ Rico.ArrayExtensions.length ] = Array.prototype.remove;
-}
-
-if (!Array.prototype.removeItem) {
-   Array.prototype.removeItem = function(item) {
-      for ( var i = 0 ; i < this.length ; i++ )
-         if ( this[i] == item ) {
-            this.remove(i);
-            break;
-         }
-   };
-  Rico.ArrayExtensions[ Rico.ArrayExtensions.length ] = Array.prototype.removeItem;
-}
-
-if (!Array.prototype.indices) {
-   Array.prototype.indices = function() {
-      var indexArray = new Array();
-      for ( index in this ) {
-         var ignoreThis = false;
-         for ( var i = 0 ; i < Rico.ArrayExtensions.length ; i++ ) {
-            if ( this[index] == Rico.ArrayExtensions[i] ) {
-               ignoreThis = true;
-               break;
-            }
-         }
-         if ( !ignoreThis )
-            indexArray[ indexArray.length ] = index;
-      }
-      return indexArray;
-   }
-  Rico.ArrayExtensions[ Rico.ArrayExtensions.length ] = Array.prototype.indices;
-}
-
-// Create the loadXML method and xml getter for Mozilla
-if ( window.DOMParser &&
-	  window.XMLSerializer &&
-	  window.Node && Node.prototype && Node.prototype.__defineGetter__ ) {
-
-   if (!Document.prototype.loadXML) {
-      Document.prototype.loadXML = function (s) {
-         var doc2 = (new DOMParser()).parseFromString(s, "text/xml");
-         while (this.hasChildNodes())
-            this.removeChild(this.lastChild);
-
-         for (var i = 0; i < doc2.childNodes.length; i++) {
-            this.appendChild(this.importNode(doc2.childNodes[i], true));
-         }
-      };
-	}
-
-	Document.prototype.__defineGetter__( "xml",
-	   function () {
-		   return (new XMLSerializer()).serializeToString(this);
-	   }
-	 );
-}
-
-document.getElementsByTagAndClassName = function(tagName, className) {
-  if ( tagName == null )
-     tagName = '*';
-
-  var children = document.getElementsByTagName(tagName) || document.all;
-  var elements = new Array();
-
-  if ( className == null )
-    return children;
-
-  for (var i = 0; i < children.length; i++) {
-    var child = children[i];
-    var classNames = child.className.split(' ');
-    for (var j = 0; j < classNames.length; j++) {
-      if (classNames[j] == className) {
-        elements.push(child);
-        break;
-      }
-    }
-  }
-
-  return elements;
-}
-
-
-//-------------------- ricoAccordion.js
-
-Rico.Accordion = Class.create();
-
-Rico.Accordion.prototype = {
-
-   initialize: function(container, options) {
-      this.container            = $(container);
-      this.lastExpandedTab      = null;
-      this.accordionTabs        = new Array();
-      this.setOptions(options);
-      this._attachBehaviors();
-
-      this.container.style.borderBottom = '1px solid ' + this.options.borderColor;
-
-      // set the initial visual state...
-      for ( var i=1 ; i < this.accordionTabs.length ; i++ )
-      {
-         this.accordionTabs[i].collapse();
-         this.accordionTabs[i].content.style.display = 'none';
-      }
-      this.lastExpandedTab = this.accordionTabs[0];
-      this.lastExpandedTab.content.style.height = this.options.panelHeight + "px";
-      this.lastExpandedTab.showExpanded();
-      this.lastExpandedTab.titleBar.style.fontWeight = this.options.expandedFontWeight;
-   },
-
-   setOptions: function(options) {
-      this.options = {
-         expandedBg          : '#63699c',
-         hoverBg             : '#63699c',
-         collapsedBg         : '#6b79a5',
-         expandedTextColor   : '#ffffff',
-         expandedFontWeight  : 'bold',
-         hoverTextColor      : '#ffffff',
-         collapsedTextColor  : '#ced7ef',
-         collapsedFontWeight : 'normal',
-         hoverTextColor      : '#ffffff',
-         borderColor         : '#1f669b',
-         panelHeight         : 200,
-         onHideTab           : null,
-         onShowTab           : null
-      }.extend(options || {});
-   },
-
-   showTabByIndex: function( anIndex, animate ) {
-      var doAnimate = arguments.length == 1 ? true : animate;
-      this.showTab( this.accordionTabs[anIndex], doAnimate );
-   },
-
-   showTab: function( accordionTab, animate ) {
-
-      var doAnimate = arguments.length == 1 ? true : animate;
-
-      if ( this.options.onHideTab )
-         this.options.onHideTab(this.lastExpandedTab);
-
-      this.lastExpandedTab.showCollapsed(); 
-      var accordion = this;
-      var lastExpandedTab = this.lastExpandedTab;
-
-      this.lastExpandedTab.content.style.height = (this.options.panelHeight - 1) + 'px';
-      accordionTab.content.style.display = '';
-
-      accordionTab.titleBar.style.fontWeight = this.options.expandedFontWeight;
-
-      if ( doAnimate ) {
-         new Effect.AccordionSize( this.lastExpandedTab.content,
-                                   accordionTab.content,
-                                   1,
-                                   this.options.panelHeight,
-                                   100, 10,
-                                   { complete: function() {accordion.showTabDone(lastExpandedTab)} } );
-         this.lastExpandedTab = accordionTab;
-      }
-      else {
-         this.lastExpandedTab.content.style.height = "1px";
-         accordionTab.content.style.height = this.options.panelHeight + "px";
-         this.lastExpandedTab = accordionTab;
-         this.showTabDone(lastExpandedTab);
-      }
-   },
-
-   showTabDone: function(collapsedTab) {
-      collapsedTab.content.style.display = 'none';
-      this.lastExpandedTab.showExpanded();
-      if ( this.options.onShowTab )
-         this.options.onShowTab(this.lastExpandedTab);
-   },
-
-   _attachBehaviors: function() {
-      var panels = this._getDirectChildrenByTag(this.container, 'DIV');
-      for ( var i = 0 ; i < panels.length ; i++ ) {
-
-         var tabChildren = this._getDirectChildrenByTag(panels[i],'DIV');
-         if ( tabChildren.length != 2 )
-            continue; // unexpected
-
-         var tabTitleBar   = tabChildren[0];
-         var tabContentBox = tabChildren[1];
-         this.accordionTabs.push( new Rico.Accordion.Tab(this,tabTitleBar,tabContentBox) );
-      }
-   },
-
-   _getDirectChildrenByTag: function(e, tagName) {
-      var kids = new Array();
-      var allKids = e.childNodes;
-      for( var i = 0 ; i < allKids.length ; i++ )
-         if ( allKids[i] && allKids[i].tagName && allKids[i].tagName == tagName )
-            kids.push(allKids[i]);
-      return kids;
-   }
-
-};
-
-Rico.Accordion.Tab = Class.create();
-
-Rico.Accordion.Tab.prototype = {
-
-   initialize: function(accordion, titleBar, content) {
-      this.accordion = accordion;
-      this.titleBar  = titleBar;
-      this.content   = content;
-      this._attachBehaviors();
-   },
-
-   collapse: function() {
-      this.showCollapsed();
-      this.content.style.height = "1px";
-   },
-
-   showCollapsed: function() {
-      this.expanded = false;
-      this.titleBar.style.backgroundColor = this.accordion.options.collapsedBg;
-      this.titleBar.style.color           = this.accordion.options.collapsedTextColor;
-      this.titleBar.style.fontWeight      = this.accordion.options.collapsedFontWeight;
-      this.content.style.overflow = "hidden";
-   },
-
-   showExpanded: function() {
-      this.expanded = true;
-      this.titleBar.style.backgroundColor = this.accordion.options.expandedBg;
-      this.titleBar.style.color           = this.accordion.options.expandedTextColor;
-      this.content.style.overflow         = "visible";
-   },
-
-   titleBarClicked: function(e) {
-      if ( this.accordion.lastExpandedTab == this )
-         return;
-      this.accordion.showTab(this);
-   },
-
-   hover: function(e) {
-		this.titleBar.style.backgroundColor = this.accordion.options.hoverBg;
-		this.titleBar.style.color           = this.accordion.options.hoverTextColor;
-   },
-
-   unhover: function(e) {
-      if ( this.expanded ) {
-         this.titleBar.style.backgroundColor = this.accordion.options.expandedBg;
-         this.titleBar.style.color           = this.accordion.options.expandedTextColor;
-      }
-      else {
-         this.titleBar.style.backgroundColor = this.accordion.options.collapsedBg;
-         this.titleBar.style.color           = this.accordion.options.collapsedTextColor;
-      }
-   },
-
-   _attachBehaviors: function() {
-      this.content.style.border = "1px solid " + this.accordion.options.borderColor;
-      this.content.style.borderTopWidth    = "0px";
-      this.content.style.borderBottomWidth = "0px";
-      this.content.style.margin            = "0px";
-
-      this.titleBar.onclick     = this.titleBarClicked.bindAsEventListener(this);
-      this.titleBar.onmouseover = this.hover.bindAsEventListener(this);
-      this.titleBar.onmouseout  = this.unhover.bindAsEventListener(this);
-   }
-
-};
-
-
-//-------------------- ricoAjaxEngine.js
-
-Rico.AjaxEngine = Class.create();
-
-Rico.AjaxEngine.prototype = {
-
-   initialize: function() {
-      this.ajaxElements = new Array();
-      this.ajaxObjects  = new Array();
-      this.requestURLS  = new Array();
-   },
-
-   registerAjaxElement: function( anId, anElement ) {
-      if ( arguments.length == 1 )
-         anElement = $(anId);
-      this.ajaxElements[anId] = anElement;
-   },
-
-   registerAjaxObject: function( anId, anObject ) {
-      this.ajaxObjects[anId] = anObject;
-   },
-
-   registerRequest: function (requestLogicalName, requestURL) {
-      this.requestURLS[requestLogicalName] = requestURL;
-   },
-
-   sendRequest: function(requestName) {
-      var requestURL = this.requestURLS[requestName];
-      if ( requestURL == null )
-         return;
-
-      var queryString = "";
-      if ( arguments.length > 1 )
-         queryString = this._createQueryString(arguments, 1);
-
-      new Ajax.Request(requestURL, this._requestOptions(queryString));
-   },
-
-   sendRequestWithData: function(requestName, xmlDocument) {
-      var requestURL = this.requestURLS[requestName];
-      if ( requestURL == null )
-         return;
-
-      var queryString = "";
-      if ( arguments.length > 2 )
-         queryString = this._createQueryString(arguments, 2);
-
-      new Ajax.Request(requestURL + "?" + queryString, this._requestOptions(null,xmlDocument));
-   },
-
-   sendRequestAndUpdate: function(requestName,container,options) {
-      var requestURL = this.requestURLS[requestName];
-      if ( requestURL == null )
-         return;
-
-      var queryString = "";
-      if ( arguments.length > 3 )
-         queryString = this._createQueryString(arguments, 3);
-
-      var updaterOptions = this._requestOptions(queryString);
-      updaterOptions.onComplete = null;
-      updaterOptions.extend(options);
-
-      new Ajax.Updater(container, requestURL, updaterOptions);
-   },
-
-   sendRequestWithDataAndUpdate: function(requestName,xmlDocument,container,options) {
-      var requestURL = this.requestURLS[requestName];
-      if ( requestURL == null )
-         return;
-
-      var queryString = "";
-      if ( arguments.length > 4 )
-         queryString = this._createQueryString(arguments, 4);
-
-
-      var updaterOptions = this._requestOptions(queryString,xmlDocument);
-      updaterOptions.onComplete = null;
-      updaterOptions.extend(options);
-
-      new Ajax.Updater(container, requestURL + "?" + queryString, updaterOptions);
-   },
-
-   // Private -- not part of intended engine API --------------------------------------------------------------------
-
-   _requestOptions: function(queryString,xmlDoc) {
-      var self = this;
-
-      var requestHeaders = ['X-Rico-Version', Rico.Version ];
-      var sendMethod = "post"
-      if ( arguments[1] )
-         requestHeaders.push( 'Content-type', 'text/xml' );
-      else
-         sendMethod = "get";
-
-      return { requestHeaders: requestHeaders,
-               parameters:     queryString,
-               postBody:       arguments[1] ? xmlDoc : null,
-               method:         sendMethod,
-               onComplete:     self._onRequestComplete.bind(self) };
-   },
-
-   _createQueryString: function( theArgs, offset ) {
-      var queryString = ""
-      for ( var i = offset ; i < theArgs.length ; i++ ) {
-          if ( i != offset )
-            queryString += "&";
-
-          var anArg = theArgs[i];
-
-          if ( anArg.name != undefined && anArg.value != undefined ) {
-            queryString += anArg.name +  "=" + escape(anArg.value);
-          }
-          else {
-             var ePos  = anArg.indexOf('=');
-             var argName  = anArg.substring( 0, ePos );
-             var argValue = anArg.substring( ePos + 1 );
-             queryString += argName + "=" + escape(argValue);
-          }
-      }
-
-      return queryString;
-   },
-
-   _onRequestComplete : function(request) {
-
-      //!!TODO: error handling infrastructure?? 
-      if (request.status != 200)
-        return;
-
-      var response = request.responseXML.getElementsByTagName("ajax-response");
-      if (response == null || response.length != 1)
-         return;
-      this._processAjaxResponse( response[0].childNodes );
-   },
-
-   _processAjaxResponse: function( xmlResponseElements ) {
-      for ( var i = 0 ; i < xmlResponseElements.length ; i++ ) {
-         var responseElement = xmlResponseElements[i];
-
-         // only process nodes of type element.....
-         if ( responseElement.nodeType != 1 )
-            continue;
-
-         var responseType = responseElement.getAttribute("type");
-         var responseId   = responseElement.getAttribute("id");
-
-         if ( responseType == "object" )
-            this._processAjaxObjectUpdate( this.ajaxObjects[ responseId ], responseElement );
-         else if ( responseType == "element" )
-            this._processAjaxElementUpdate( this.ajaxElements[ responseId ], responseElement );
-         else
-            alert('unrecognized AjaxResponse type : ' + responseType );
-      }
-   },
-
-   _processAjaxObjectUpdate: function( ajaxObject, responseElement ) {
-      ajaxObject.ajaxUpdate( responseElement );
-   },
-
-   _processAjaxElementUpdate: function( ajaxElement, responseElement ) {
-      ajaxElement.innerHTML = RicoUtil.getContentAsString(responseElement);
-   }
-
-}
-
-var ajaxEngine = new Rico.AjaxEngine();
-
-
-//-------------------- ricoColor.js
-Rico.Color = Class.create();
-
-Rico.Color.prototype = {
-
-   initialize: function(red, green, blue) {
-      this.rgb = { r: red, g : green, b : blue };
-   },
-
-   setRed: function(r) {
-      this.rgb.r = r;
-   },
-
-   setGreen: function(g) {
-      this.rgb.g = g;
-   },
-
-   setBlue: function(b) {
-      this.rgb.b = b;
-   },
-
-   setHue: function(h) {
-
-      // get an HSB model, and set the new hue...
-      var hsb = this.asHSB();
-      hsb.h = h;
-
-      // convert back to RGB...
-      this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, hsb.b);
-   },
-
-   setSaturation: function(s) {
-      // get an HSB model, and set the new hue...
-      var hsb = this.asHSB();
-      hsb.s = s;
-
-      // convert back to RGB and set values...
-      this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, hsb.b);
-   },
-
-   setBrightness: function(b) {
-      // get an HSB model, and set the new hue...
-      var hsb = this.asHSB();
-      hsb.b = b;
-
-      // convert back to RGB and set values...
-      this.rgb = Rico.Color.HSBtoRGB( hsb.h, hsb.s, hsb.b );
-   },
-
-   darken: function(percent) {
-      var hsb  = this.asHSB();
-      this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, Math.max(hsb.b - percent,0));
-   },
-
-   brighten: function(percent) {
-      var hsb  = this.asHSB();
-      this.rgb = Rico.Color.HSBtoRGB(hsb.h, hsb.s, Math.min(hsb.b + percent,1));
-   },
-
-   blend: function(other) {
-      this.rgb.r = Math.floor((this.rgb.r + other.rgb.r)/2);
-      this.rgb.g = Math.floor((this.rgb.g + other.rgb.g)/2);
-      this.rgb.b = Math.floor((this.rgb.b + other.rgb.b)/2);
-   },
-
-   isBright: function() {
-      var hsb = this.asHSB();
-      return this.asHSB().b > 0.5;
-   },
-
-   isDark: function() {
-      return ! this.isBright();
-   },
-
-   asRGB: function() {
-      return "rgb(" + this.rgb.r + "," + this.rgb.g + "," + this.rgb.b + ")";
-   },
-
-   asHex: function() {
-      return "#" + this.rgb.r.toColorPart() + this.rgb.g.toColorPart() + this.rgb.b.toColorPart();
-   },
-
-   asHSB: function() {
-      return Rico.Color.RGBtoHSB(this.rgb.r, this.rgb.g, this.rgb.b);
-   },
-
-   toString: function() {
-      return this.asHex();
-   }
-
-};
-
-Rico.Color.createFromHex = function(hexCode) {
-
-   if ( hexCode.indexOf('#') == 0 )
-      hexCode = hexCode.substring(1);
-   var red   = hexCode.substring(0,2);
-   var green = hexCode.substring(2,4);
-   var blue  = hexCode.substring(4,6);
-   return new Rico.Color( parseInt(red,16), parseInt(green,16), parseInt(blue,16) );
-}
-
-/**
- * Factory method for creating a color from the background of
- * an HTML element.
- */
-Rico.Color.createColorFromBackground = function(elem) {
-
-   var actualColor = RicoUtil.getElementsComputedStyle($(elem), "backgroundColor", "background-color");
-
-   if ( actualColor == "transparent" && elem.parent )
-      return Rico.Color.createColorFromBackground(elem.parent);
-
-   if ( actualColor == null )
-      return new Rico.Color(255,255,255);
-
-   if ( actualColor.indexOf("rgb(") == 0 ) {
-      var colors = actualColor.substring(4, actualColor.length - 1 );
-      var colorArray = colors.split(",");
-      return new Rico.Color( parseInt( colorArray[0] ),
-                            parseInt( colorArray[1] ),
-                            parseInt( colorArray[2] )  );
-
-   }
-   else if ( actualColor.indexOf("#") == 0 ) {
-      var redPart   = parseInt(actualColor.substring(1,3), 16);
-      var greenPart = parseInt(actualColor.substring(3,5), 16);
-      var bluePart  = parseInt(actualColor.substring(5), 16);
-      return new Rico.Color( redPart, greenPart, bluePart );
-   }
-   else
-      return new Rico.Color(255,255,255);
-}
-
-Rico.Color.HSBtoRGB = function(hue, saturation, brightness) {
-
-   var red   = 0;
-	var green = 0;
-	var blue  = 0;
-
-   if (saturation == 0) {
-      red = parseInt(brightness * 255.0 + 0.5);
-	   green = red;
-	   blue = red;
-	}
-	else {
-      var h = (hue - Math.floor(hue)) * 6.0;
-      var f = h - Math.floor(h);
-      var p = brightness * (1.0 - saturation);
-      var q = brightness * (1.0 - saturation * f);
-      var t = brightness * (1.0 - (saturation * (1.0 - f)));
-
-      switch (parseInt(h)) {
-         case 0:
-            red   = (brightness * 255.0 + 0.5);
-            green = (t * 255.0 + 0.5);
-            blue  = (p * 255.0 + 0.5);
-            break;
-         case 1:
-            red   = (q * 255.0 + 0.5);
-            green = (brightness * 255.0 + 0.5);
-            blue  = (p * 255.0 + 0.5);
-            break;
-         case 2:
-            red   = (p * 255.0 + 0.5);
-            green = (brightness * 255.0 + 0.5);
-            blue  = (t * 255.0 + 0.5);
-            break;
-         case 3:
-            red   = (p * 255.0 + 0.5);
-            green = (q * 255.0 + 0.5);
-            blue  = (brightness * 255.0 + 0.5);
-            break;
-         case 4:
-            red   = (t * 255.0 + 0.5);
-            green = (p * 255.0 + 0.5);
-            blue  = (brightness * 255.0 + 0.5);
-            break;
-          case 5:
-            red   = (brightness * 255.0 + 0.5);
-            green = (p * 255.0 + 0.5);
-            blue  = (q * 255.0 + 0.5);
-            break;
-	    }
-	}
-
-   return { r : parseInt(red), g : parseInt(green) , b : parseInt(blue) };
-}
-
-Rico.Color.RGBtoHSB = function(r, g, b) {
-
-   var hue;
-   var saturaton;
-   var brightness;
-
-   var cmax = (r > g) ? r : g;
-   if (b > cmax)
-      cmax = b;
-
-   var cmin = (r < g) ? r : g;
-   if (b < cmin)
-      cmin = b;
-
-   brightness = cmax / 255.0;
-   if (cmax != 0)
-      saturation = (cmax - cmin)/cmax;
-   else
-      saturation = 0;
-
-   if (saturation == 0)
-      hue = 0;
-   else {
-      var redc   = (cmax - r)/(cmax - cmin);
-    	var greenc = (cmax - g)/(cmax - cmin);
-    	var bluec  = (cmax - b)/(cmax - cmin);
-
-    	if (r == cmax)
-    	   hue = bluec - greenc;
-    	else if (g == cmax)
-    	   hue = 2.0 + redc - bluec;
-      else
-    	   hue = 4.0 + greenc - redc;
-
-    	hue = hue / 6.0;
-    	if (hue < 0)
-    	   hue = hue + 1.0;
-   }
-
-   return { h : hue, s : saturation, b : brightness };
-}
-
-
-//-------------------- ricoCorner.js
-
-Rico.Corner = {
-
-   round: function(e, options) {
-      var e = $(e);
-      this._setOptions(options);
-
-      var color = this.options.color;
-      if ( this.options.color == "fromElement" )
-         color = this._background(e);
-
-      var bgColor = this.options.bgColor;
-      if ( this.options.bgColor == "fromParent" )
-         bgColor = this._background(e.offsetParent);
-
-      this._roundCornersImpl(e, color, bgColor);
-   },
-
-   _roundCornersImpl: function(e, color, bgColor) {
-      if(this.options.border)
-         this._renderBorder(e,bgColor);
-      if(this._isTopRounded())
-         this._roundTopCorners(e,color,bgColor);
-      if(this._isBottomRounded())
-         this._roundBottomCorners(e,color,bgColor);
-   },
-
-   _renderBorder: function(el,bgColor) {
-      var borderValue = "1px solid " + this._borderColor(bgColor);
-      var borderL = "border-left: "  + borderValue;
-      var borderR = "border-right: " + borderValue;
-      var style   = "style='" + borderL + ";" + borderR +  "'";
-      el.innerHTML = "<div " + style + ">" + el.innerHTML + "</div>"
-   },
-
-   _roundTopCorners: function(el, color, bgColor) {
-      var corner = this._createCorner(bgColor);
-      for(var i=0 ; i < this.options.numSlices ; i++ )
-         corner.appendChild(this._createCornerSlice(color,bgColor,i,"top"));
-      el.style.paddingTop = 0;
-      el.insertBefore(corner,el.firstChild);
-   },
-
-   _roundBottomCorners: function(el, color, bgColor) {
-      var corner = this._createCorner(bgColor);
-      for(var i=(this.options.numSlices-1) ; i >= 0 ; i-- )
-         corner.appendChild(this._createCornerSlice(color,bgColor,i,"bottom"));
-      el.style.paddingBottom = 0;
-      el.appendChild(corner);
-   },
-
-   _createCorner: function(bgColor) {
-      var corner = document.createElement("div");
-      corner.style.backgroundColor = (this._isTransparent() ? "transparent" : bgColor);
-      return corner;
-   },
-
-   _createCornerSlice: function(color,bgColor, n, position) {
-      var slice = document.createElement("span");
-
-      var inStyle = slice.style;
-      inStyle.backgroundColor = color;
-      inStyle.display  = "block";
-      inStyle.height   = "1px";
-      inStyle.overflow = "hidden";
-      inStyle.fontSize = "1px";
-
-      var borderColor = this._borderColor(color,bgColor);
-      if ( this.options.border && n == 0 ) {
-         inStyle.borderTopStyle    = "solid";
-         inStyle.borderTopWidth    = "1px";
-         inStyle.borderLeftWidth   = "0px";
-         inStyle.borderRightWidth  = "0px";
-         inStyle.borderBottomWidth = "0px";
-         inStyle.height            = "0px"; // assumes css compliant box model
-         inStyle.borderColor       = borderColor;
-      }
-      else if(borderColor) {
-         inStyle.borderColor = borderColor;
-         inStyle.borderStyle = "solid";
-         inStyle.borderWidth = "0px 1px";
-      }
-
-      if ( !this.options.compact && (n == (this.options.numSlices-1)) )
-         inStyle.height = "2px";
-
-      this._setMargin(slice, n, position);
-      this._setBorder(slice, n, position);
-
-      return slice;
-   },
-
-   _setOptions: function(options) {
-      this.options = {
-         corners : "all",
-         color   : "fromElement",
-         bgColor : "fromParent",
-         blend   : true,
-         border  : false,
-         compact : false
-      }.extend(options || {});
-
-      this.options.numSlices = this.options.compact ? 2 : 4;
-      if ( this._isTransparent() )
-         this.options.blend = false;
-   },
-
-   _whichSideTop: function() {
-      if ( this._hasString(this.options.corners, "all", "top") )
-         return "";
-
-      if ( this.options.corners.indexOf("tl") >= 0 && this.options.corners.indexOf("tr") >= 0 )
-         return "";
-
-      if (this.options.corners.indexOf("tl") >= 0)
-         return "left";
-      else if (this.options.corners.indexOf("tr") >= 0)
-          return "right";
-      return "";
-   },
-
-   _whichSideBottom: function() {
-      if ( this._hasString(this.options.corners, "all", "bottom") )
-         return "";
-
-      if ( this.options.corners.indexOf("bl")>=0 && this.options.corners.indexOf("br")>=0 )
-         return "";
-
-      if(this.options.corners.indexOf("bl") >=0)
-         return "left";
-      else if(this.options.corners.indexOf("br")>=0)
-         return "right";
-      return "";
-   },
-
-   _borderColor : function(color,bgColor) {
-      if ( color == "transparent" )
-         return bgColor;
-      else if ( this.options.border )
-         return this.options.border;
-      else if ( this.options.blend )
-         return this._blend( bgColor, color );
-      else
-         return "";
-   },
-
-
-   _setMargin: function(el, n, corners) {
-      var marginSize = this._marginSize(n);
-      var whichSide = corners == "top" ? this._whichSideTop() : this._whichSideBottom();
-
-      if ( whichSide == "left" ) {
-         el.style.marginLeft = marginSize + "px"; el.style.marginRight = "0px";
-      }
-      else if ( whichSide == "right" ) {
-         el.style.marginRight = marginSize + "px"; el.style.marginLeft  = "0px";
-      }
-      else {
-         el.style.marginLeft = marginSize + "px"; el.style.marginRight = marginSize + "px";
-      }
-   },
-
-   _setBorder: function(el,n,corners) {
-      var borderSize = this._borderSize(n);
-      var whichSide = corners == "top" ? this._whichSideTop() : this._whichSideBottom();
-
-      if ( whichSide == "left" ) {
-         el.style.borderLeftWidth = borderSize + "px"; el.style.borderRightWidth = "0px";
-      }
-      else if ( whichSide == "right" ) {
-         el.style.borderRightWidth = borderSize + "px"; el.style.borderLeftWidth  = "0px";
-      }
-      else {
-         el.style.borderLeftWidth = borderSize + "px"; el.style.borderRightWidth = borderSize + "px";
-      }
-   },
-
-   _marginSize: function(n) {
-      if ( this._isTransparent() )
-         return 0;
-
-      var marginSizes          = [ 5, 3, 2, 1 ];
-      var blendedMarginSizes   = [ 3, 2, 1, 0 ];
-      var compactMarginSizes   = [ 2, 1 ];
-      var smBlendedMarginSizes = [ 1, 0 ];
-
-      if ( this.options.compact && this.options.blend )
-         return smBlendedMarginSizes[n];
-      else if ( this.options.compact )
-         return compactMarginSizes[n];
-      else if ( this.options.blend )
-         return blendedMarginSizes[n];
-      else
-         return marginSizes[n];
-   },
-
-   _borderSize: function(n) {
-      var transparentBorderSizes = [ 5, 3, 2, 1 ];
-      var blendedBorderSizes     = [ 2, 1, 1, 1 ];
-      var compactBorderSizes     = [ 1, 0 ];
-      var actualBorderSizes      = [ 0, 2, 0, 0 ];
-
-      if ( this.options.compact && (this.options.blend || this._isTransparent()) )
-         return 1;
-      else if ( this.options.compact )
-         return compactBorderSizes[n];
-      else if ( this.options.blend )
-         return blendedBorderSizes[n];
-      else if ( this.options.border )
-         return actualBorderSizes[n];
-      else if ( this._isTransparent() )
-         return transparentBorderSizes[n];
-      return 0;
-   },
-
-   _hasString: function(str) { for(var i=1 ; i<arguments.length ; i++) if (str.indexOf(arguments[i]) >= 0) return true; return false; },
-   _blend: function(c1, c2) { var cc1 = Rico.Color.createFromHex(c1); cc1.blend(Rico.Color.createFromHex(c2)); return cc1; },
-   _background: function(el) { try { return Rico.Color.createColorFromBackground(el).asHex(); } catch(err) { return "#ffffff"; } },
-   _isTransparent: function() { return this.options.color == "transparent"; },
-   _isTopRounded: function() { return this._hasString(this.options.corners, "all", "top", "tl", "tr"); },
-   _isBottomRounded: function() { return this._hasString(this.options.corners, "all", "bottom", "bl", "br"); },
-   _hasSingleTextChild: function(el) { return el.childNodes.length == 1 && el.childNodes[0].nodeType == 3; }
-}
-
-
-//-------------------- ricoDragAndDrop.js
-Rico.DragAndDrop = Class.create();
-
-Rico.DragAndDrop.prototype = {
-
-   initialize: function() {
-      this.dropZones                = new Array();
-      this.draggables               = new Array();
-      this.currentDragObjects       = new Array();
-      this.dragElement              = null;
-      this.lastSelectedDraggable    = null;
-      this.currentDragObjectVisible = false;
-      this.interestedInMotionEvents = false;
-   },
-
-   registerDropZone: function(aDropZone) {
-      this.dropZones[ this.dropZones.length ] = aDropZone;
-   },
-
-   deregisterDropZone: function(aDropZone) {
-      var newDropZones = new Array();
-      var j = 0;
-      for ( var i = 0 ; i < this.dropZones.length ; i++ ) {
-         if ( this.dropZones[i] != aDropZone )
-            newDropZones[j++] = this.dropZones[i];
-      }
-
-      this.dropZones = newDropZones;
-   },
-
-   clearDropZones: function() {
-      this.dropZones = new Array();
-   },
-
-   registerDraggable: function( aDraggable ) {
-      this.draggables[ this.draggables.length ] = aDraggable;
-      this._addMouseDownHandler( aDraggable );
-   },
-
-   clearSelection: function() {
-      for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
-         this.currentDragObjects[i].deselect();
-      this.currentDragObjects = new Array();
-      this.lastSelectedDraggable = null;
-   },
-
-   hasSelection: function() {
-      return this.currentDragObjects.length > 0;
-   },
-
-   setStartDragFromElement: function( e, mouseDownElement ) {
-      this.origPos = RicoUtil.toDocumentPosition(mouseDownElement);
-      this.startx = e.screenX - this.origPos.x
-      this.starty = e.screenY - this.origPos.y
-      //this.startComponentX = e.layerX ? e.layerX : e.offsetX;
-      //this.startComponentY = e.layerY ? e.layerY : e.offsetY;
-      //this.adjustedForDraggableSize = false;
-
-      this.interestedInMotionEvents = this.hasSelection();
-      this._terminateEvent(e);
-   },
-
-   updateSelection: function( draggable, extendSelection ) {
-      if ( ! extendSelection )
-         this.clearSelection();
-
-      if ( draggable.isSelected() ) {
-         this.currentDragObjects.removeItem(draggable);
-         draggable.deselect();
-         if ( draggable == this.lastSelectedDraggable )
-            this.lastSelectedDraggable = null;
-      }
-      else {
-         this.currentDragObjects[ this.currentDragObjects.length ] = draggable;
-         draggable.select();
-         this.lastSelectedDraggable = draggable;
-      }
-   },
-
-   _mouseDownHandler: function(e) {
-      if ( arguments.length == 0 )
-         e = event;
-
-      // if not button 1 ignore it...
-      var nsEvent = e.which != undefined;
-      if ( (nsEvent && e.which != 1) || (!nsEvent && e.button != 1))
-         return;
-
-      var eventTarget      = e.target ? e.target : e.srcElement;
-      var draggableObject  = eventTarget.draggable;
-
-      var candidate = eventTarget;
-      while (draggableObject == null && candidate.parentNode) {
-         candidate = candidate.parentNode;
-         draggableObject = candidate.draggable;
-      }
-   
-      if ( draggableObject == null )
-         return;
-
-      this.updateSelection( draggableObject, e.ctrlKey );
-
-      // clear the drop zones postion cache...
-      if ( this.hasSelection() )
-         for ( var i = 0 ; i < this.dropZones.length ; i++ )
-            this.dropZones[i].clearPositionCache();
-
-      this.setStartDragFromElement( e, draggableObject.getMouseDownHTMLElement() );
-   },
-
-
-   _mouseMoveHandler: function(e) {
-      var nsEvent = e.which != undefined;
-      if ( !this.interestedInMotionEvents ) {
-         this._terminateEvent(e);
-         return;
-      }
-
-      if ( ! this.hasSelection() )
-         return;
-
-      if ( ! this.currentDragObjectVisible )
-         this._startDrag(e);
-
-      if ( !this.activatedDropZones )
-         this._activateRegisteredDropZones();
-
-      //if ( !this.adjustedForDraggableSize )
-      //   this._adjustForDraggableSize(e);
-
-      this._updateDraggableLocation(e);
-      this._updateDropZonesHover(e);
-
-      this._terminateEvent(e);
-   },
-
-   _makeDraggableObjectVisible: function(e)
-   {
-      if ( !this.hasSelection() )
-         return;
-
-      var dragElement;
-      if ( this.currentDragObjects.length > 1 )
-         dragElement = this.currentDragObjects[0].getMultiObjectDragGUI(this.currentDragObjects);
-      else
-         dragElement = this.currentDragObjects[0].getSingleObjectDragGUI();
-
-      // go ahead and absolute position it...
-      if ( RicoUtil.getElementsComputedStyle(dragElement, "position")  != "absolute" )
-         dragElement.style.position = "absolute";
-
-      // need to parent him into the document...
-      if ( dragElement.parentNode == null || dragElement.parentNode.nodeType == 11 )
-         document.body.appendChild(dragElement);
-
-      this.dragElement = dragElement;
-      this._updateDraggableLocation(e);
-
-      this.currentDragObjectVisible = true;
-   },
-
-   /**
-   _adjustForDraggableSize: function(e) {
-      var dragElementWidth  = this.dragElement.offsetWidth;
-      var dragElementHeight = this.dragElement.offsetHeight;
-      if ( this.startComponentX > dragElementWidth )
-         this.startx -= this.startComponentX - dragElementWidth + 2;
-      if ( e.offsetY ) {
-         if ( this.startComponentY > dragElementHeight )
-            this.starty -= this.startComponentY - dragElementHeight + 2;
-      }
-      this.adjustedForDraggableSize = true;
-   },
-   **/
-
-   _updateDraggableLocation: function(e) {
-      var dragObjectStyle = this.dragElement.style;
-      dragObjectStyle.left = (e.screenX - this.startx) + "px"
-      dragObjectStyle.top  = (e.screenY - this.starty) + "px";
-   },
-
-   _updateDropZonesHover: function(e) {
-      var n = this.dropZones.length;
-      for ( var i = 0 ; i < n ; i++ ) {
-         if ( ! this._mousePointInDropZone( e, this.dropZones[i] ) )
-            this.dropZones[i].hideHover();
-      }
-
-      for ( var i = 0 ; i < n ; i++ ) {
-         if ( this._mousePointInDropZone( e, this.dropZones[i] ) ) {
-            if ( this.dropZones[i].canAccept(this.currentDragObjects) )
-               this.dropZones[i].showHover();
-         }
-      }
-   },
-
-   _startDrag: function(e) {
-      for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
-         this.currentDragObjects[i].startDrag();
-
-      this._makeDraggableObjectVisible(e);
-   },
-
-   _mouseUpHandler: function(e) {
-      if ( ! this.hasSelection() )
-         return;
-
-      var nsEvent = e.which != undefined;
-      if ( (nsEvent && e.which != 1) || (!nsEvent && e.button != 1))
-         return;
-
-      this.interestedInMotionEvents = false;
-
-      if ( this.dragElement == null ) {
-         this._terminateEvent(e);
-         return;
-      }
-
-      if ( this._placeDraggableInDropZone(e) )
-         this._completeDropOperation(e);
-      else {
-         this._terminateEvent(e);
-         new Effect.Position( this.dragElement,
-                              this.origPos.x,
-                              this.origPos.y,
-                              200,
-                              20,
-                              { complete : this._doCancelDragProcessing.bind(this) } );
-      }
-   },
-
-   _completeDropOperation: function(e) {
-      if ( this.dragElement != this.currentDragObjects[0].getMouseDownHTMLElement() ) {
-         if ( this.dragElement.parentNode != null )
-            this.dragElement.parentNode.removeChild(this.dragElement);
-      }
-
-      this._deactivateRegisteredDropZones();
-      this._endDrag();
-      this.clearSelection();
-      this.dragElement = null;
-      this.currentDragObjectVisible = false;
-      this._terminateEvent(e);
-   },
-
-   _doCancelDragProcessing: function() {
-      this._cancelDrag();
-
-      if ( this.dragElement != this.currentDragObjects[0].getMouseDownHTMLElement() ) {
-         if ( this.dragElement.parentNode != null ) {
-            this.dragElement.parentNode.removeChild(this.dragElement);
-         }
-      }
-
-      this._deactivateRegisteredDropZones();
-      this.dragElement = null;
-      this.currentDragObjectVisible = false;
-   },
-
-   _placeDraggableInDropZone: function(e) {
-      var foundDropZone = false;
-      var n = this.dropZones.length;
-      for ( var i = 0 ; i < n ; i++ ) {
-         if ( this._mousePointInDropZone( e, this.dropZones[i] ) ) {
-            if ( this.dropZones[i].canAccept(this.currentDragObjects) ) {
-               this.dropZones[i].hideHover();
-               this.dropZones[i].accept(this.currentDragObjects);
-               foundDropZone = true;
-               break;
-            }
-         }
-      }
-
-      return foundDropZone;
-   },
-
-   _cancelDrag: function() {
-      for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
-         this.currentDragObjects[i].cancelDrag();
-   },
-
-   _endDrag: function() {
-      for ( var i = 0 ; i < this.currentDragObjects.length ; i++ )
-         this.currentDragObjects[i].endDrag();
-   },
-
-   _mousePointInDropZone: function( e, dropZone ) {
-
-      var absoluteRect = dropZone.getAbsoluteRect();
-
-      return e.clientX  > absoluteRect.left  &&
-             e.clientX  < absoluteRect.right &&
-             e.clientY  > absoluteRect.top   &&
-             e.clientY  < absoluteRect.bottom;
-   },
-
-   _addMouseDownHandler: function( aDraggable )
-   {
-      var htmlElement = aDraggable.getMouseDownHTMLElement();
-      if ( htmlElement != null ) {
-         htmlElement.draggable = aDraggable;
-         this._addMouseDownEvent( htmlElement );
-      }
-   },
-
-   _activateRegisteredDropZones: function() {
-      var n = this.dropZones.length;
-      for ( var i = 0 ; i < n ; i++ ) {
-         var dropZone = this.dropZones[i];
-         if ( dropZone.canAccept(this.currentDragObjects) )
-            dropZone.activate();
-      }
-
-      this.activatedDropZones = true;
-   },
-
-   _deactivateRegisteredDropZones: function() {
-      var n = this.dropZones.length;
-      for ( var i = 0 ; i < n ; i++ )
-         this.dropZones[i].deactivate();
-      this.activatedDropZones = false;
-   },
-
-   _addMouseDownEvent: function( htmlElement ) {
-      if ( typeof document.implementation != "undefined" &&
-         document.implementation.hasFeature("HTML",   "1.0") &&
-         document.implementation.hasFeature("Events", "2.0") &&
-         document.implementation.hasFeature("CSS",    "2.0") ) {
-         htmlElement.addEventListener("mousedown", this._mouseDownHandler.bindAsEventListener(this), false);
-      }
-      else {
-         htmlElement.attachEvent( "onmousedown", this._mouseDownHandler.bindAsEventListener(this) );
-      }
-   },
-
-   _terminateEvent: function(e) {
-      if ( e.stopPropagation != undefined )
-         e.stopPropagation();
-      else if ( e.cancelBubble != undefined )
-         e.cancelBubble = true;
-
-      if ( e.preventDefault != undefined )
-         e.preventDefault();
-      else
-         e.returnValue = false;
-   },
-
-   initializeEventHandlers: function() {
-      if ( typeof document.implementation != "undefined" &&
-         document.implementation.hasFeature("HTML",   "1.0") &&
-         document.implementation.hasFeature("Events", "2.0") &&
-         document.implementation.hasFeature("CSS",    "2.0") ) {
-         document.addEventListener("mouseup",   this._mouseUpHandler.bindAsEventListener(this),  false);
-         document.addEventListener("mousemove", this._mouseMoveHandler.bindAsEventListener(this), false);
-      }
-      else {
-         document.attachEvent( "onmouseup",   this._mouseUpHandler.bindAsEventListener(this) );
-         document.attachEvent( "onmousemove", this._mouseMoveHandler.bindAsEventListener(this) );
-      }
-   }
-}
-
-var dndMgr = new Rico.DragAndDrop();
-dndMgr.initializeEventHandlers();
-
-
-//-------------------- ricoDraggable.js
-Rico.Draggable = Class.create();
-
-Rico.Draggable.prototype = {
-
-   initialize: function( type, htmlElement ) {
-      this.type          = type;
-      this.htmlElement   = $(htmlElement);
-      this.selected      = false;
-   },
-
-   /**
-    *   Returns the HTML element that should have a mouse down event
-    *   added to it in order to initiate a drag operation
-    *
-    **/
-   getMouseDownHTMLElement: function() {
-      return this.htmlElement;
-   },
-
-   select: function() {
-      this.selected = true;
-
-      if ( this.showingSelected )
-         return;
-
-      var htmlElement = this.getMouseDownHTMLElement();
-
-      var color = Rico.Color.createColorFromBackground(htmlElement);
-      color.isBright() ? color.darken(0.033) : color.brighten(0.033);
-
-      this.saveBackground = RicoUtil.getElementsComputedStyle(htmlElement, "backgroundColor", "background-color");
-      htmlElement.style.backgroundColor = color.asHex();
-      this.showingSelected = true;
-   },
-
-   deselect: function() {
-      this.selected = false;
-      if ( !this.showingSelected )
-         return;
-
-      var htmlElement = this.getMouseDownHTMLElement();
-
-      htmlElement.style.backgroundColor = this.saveBackground;
-      this.showingSelected = false;
-   },
-
-   isSelected: function() {
-      return this.selected;
-   },
-
-   startDrag: function() {
-   },
-
-   cancelDrag: function() {
-   },
-
-   endDrag: function() {
-   },
-
-   getSingleObjectDragGUI: function() {
-      return this.htmlElement;
-   },
-
-   getMultiObjectDragGUI: function( draggables ) {
-      return this.htmlElement;
-   },
-
-   getDroppedGUI: function() {
-      return this.htmlElement;
-   },
-
-   toString: function() {
-      return this.type + ":" + this.htmlElement + ":";
-   }
-
-}
-
-
-//-------------------- ricoDropzone.js
-Rico.Dropzone = Class.create();
-
-Rico.Dropzone.prototype = {
-
-   initialize: function( htmlElement ) {
-      this.htmlElement  = $(htmlElement);
-      this.absoluteRect = null;
-   },
-
-   getHTMLElement: function() {
-      return this.htmlElement;
-   },
-
-   clearPositionCache: function() {
-      this.absoluteRect = null;
-   },
-
-   getAbsoluteRect: function() {
-      if ( this.absoluteRect == null ) {
-         var htmlElement = this.getHTMLElement();
-         var pos = RicoUtil.toViewportPosition(htmlElement);
-
-         this.absoluteRect = {
-            top:    pos.y,
-            left:   pos.x,
-            bottom: pos.y + htmlElement.offsetHeight,
-            right:  pos.x + htmlElement.offsetWidth
-         };
-      }
-      return this.absoluteRect;
-   },
-
-   activate: function() {
-      var htmlElement = this.getHTMLElement();
-      if (htmlElement == null  || this.showingActive)
-         return;
-
-      this.showingActive = true;
-      this.saveBackgroundColor = htmlElement.style.backgroundColor;
-
-      var fallbackColor = "#ffea84";
-      var currentColor = Rico.Color.createColorFromBackground(htmlElement);
-      if ( currentColor == null )
-         htmlElement.style.backgroundColor = fallbackColor;
-      else {
-         currentColor.isBright() ? currentColor.darken(0.2) : currentColor.brighten(0.2);
-         htmlElement.style.backgroundColor = currentColor.asHex();
-      }
-   },
-
-   deactivate: function() {
-      var htmlElement = this.getHTMLElement();
-      if (htmlElement == null || !this.showingActive)
-         return;
-
-      htmlElement.style.backgroundColor = this.saveBackgroundColor;
-      this.showingActive = false;
-      this.saveBackgroundColor = null;
-   },
-
-   showHover: function() {
-      var htmlElement = this.getHTMLElement();
-      if ( htmlElement == null || this.showingHover )
-         return;
-
-      this.saveBorderWidth = htmlElement.style.borderWidth;
-      this.saveBorderStyle = htmlElement.style.borderStyle;
-      this.saveBorderColor = htmlElement.style.borderColor;
-
-      this.showingHover = true;
-      htmlElement.style.borderWidth = "1px";
-      htmlElement.style.borderStyle = "solid";
-      //htmlElement.style.borderColor = "#ff9900";
-      htmlElement.style.borderColor = "#ffff00";
-   },
-
-   hideHover: function() {
-      var htmlElement = this.getHTMLElement();
-      if ( htmlElement == null || !this.showingHover )
-         return;
-
-      htmlElement.style.borderWidth = this.saveBorderWidth;
-      htmlElement.style.borderStyle = this.saveBorderStyle;
-      htmlElement.style.borderColor = this.saveBorderColor;
-      this.showingHover = false;
-   },
-
-   canAccept: function(draggableObjects) {
-      return true;
-   },
-
-   accept: function(draggableObjects) {
-      var htmlElement = this.getHTMLElement();
-      if ( htmlElement == null )
-         return;
-
-      n = draggableObjects.length;
-      for ( var i = 0 ; i < n ; i++ )
-      {
-         var theGUI = draggableObjects[i].getDroppedGUI();
-         if ( RicoUtil.getElementsComputedStyle( theGUI, "position" ) == "absolute" )
-         {
-            theGUI.style.position = "static";
-            theGUI.style.top = "";
-            theGUI.style.top = "";
-         }
-         htmlElement.appendChild(theGUI);
-      }
-   }
-}
-
-
-//-------------------- ricoEffects.js
-
-/**
-  *  Use the Effect namespace for effects.  If using scriptaculous effects
-  *  this will already be defined, otherwise we'll just create an empty
-  *  object for it...
- **/
-if ( window.Effect == undefined )
-   Effect = {};
-
-Effect.SizeAndPosition = Class.create();
-Effect.SizeAndPosition.prototype = {
-
-   initialize: function(element, x, y, w, h, duration, steps, options) {
-      this.element = $(element);
-      this.x = x;
-      this.y = y;
-      this.w = w;
-      this.h = h;
-      this.duration = duration;
-      this.steps    = steps;
-      this.options  = arguments[7] || {};
-
-      this.sizeAndPosition();
-   },
-
-   sizeAndPosition: function() {
-      if (this.isFinished()) {
-         if(this.options.complete) this.options.complete(this);
-         return;
-      }
-
-      if (this.timer)
-         clearTimeout(this.timer);
-
-      var stepDuration = Math.round(this.duration/this.steps) ;
-
-      // Get original values: x,y = top left corner;  w,h = width height
-      var currentX = this.element.offsetLeft;
-      var currentY = this.element.offsetTop;
-      var currentW = this.element.offsetWidth;
-      var currentH = this.element.offsetHeight;
-
-      // If values not set, or zero, we do not modify them, and take original as final as well
-      this.x = (this.x) ? this.x : currentX;
-      this.y = (this.y) ? this.y : currentY;
-      this.w = (this.w) ? this.w : currentW;
-      this.h = (this.h) ? this.h : currentH;
-
-      // how much do we need to modify our values for each step?
-      var difX = this.steps >  0 ? (this.x - currentX)/this.steps : 0;
-      var difY = this.steps >  0 ? (this.y - currentY)/this.steps : 0;
-      var difW = this.steps >  0 ? (this.w - currentW)/this.steps : 0;
-      var difH = this.steps >  0 ? (this.h - currentH)/this.steps : 0;
-
-      this.moveBy(difX, difY);
-      this.resizeBy(difW, difH);
-
-      this.duration -= stepDuration;
-      this.steps--;
-
-      this.timer = setTimeout(this.sizeAndPosition.bind(this), stepDuration);
-   },
-
-   isFinished: function() {
-      return this.steps <= 0;
-   },
-
-   moveBy: function( difX, difY ) {
-      var currentLeft = this.element.offsetLeft;
-      var currentTop  = this.element.offsetTop;
-      var intDifX     = parseInt(difX);
-      var intDifY     = parseInt(difY);
-
-      var style = this.element.style;
-      if ( intDifX != 0 )
-         style.left = (currentLeft + intDifX) + "px";
-      if ( intDifY != 0 )
-         style.top  = (currentTop + intDifY) + "px";
-   },
-
-   resizeBy: function( difW, difH ) {
-      var currentWidth  = this.element.offsetWidth;
-      var currentHeight = this.element.offsetHeight;
-      var intDifW       = parseInt(difW);
-      var intDifH       = parseInt(difH);
-
-      var style = this.element.style;
-      if ( intDifW != 0 )
-         style.width   = (currentWidth  + intDifW) + "px";
-      if ( intDifH != 0 )
-         style.height  = (currentHeight + intDifH) + "px";
-   }
-}
-
-Effect.Size = Class.create();
-Effect.Size.prototype = {
-
-   initialize: function(element, w, h, duration, steps, options) {
-      new Effect.SizeAndPosition(element, null, null, w, h, duration, steps, options);
-  }
-}
-
-Effect.Position = Class.create();
-Effect.Position.prototype = {
-
-   initialize: function(element, x, y, duration, steps, options) {
-      new Effect.SizeAndPosition(element, x, y, null, null, duration, steps, options);
-  }
-}
-
-Effect.Round = Class.create();
-Effect.Round.prototype = {
-
-   initialize: function(tagName, className, options) {
-      var elements = document.getElementsByTagAndClassName(tagName,className);
-      for ( var i = 0 ; i < elements.length ; i++ )
-         Rico.Corner.round( elements[i], options );
-   }
-};
-
-Effect.FadeTo = Class.create();
-Effect.FadeTo.prototype = {
-
-   initialize: function( element, opacity, duration, steps, options) {
-      this.element  = $(element);
-      this.opacity  = opacity;
-      this.duration = duration;
-      this.steps    = steps;
-      this.options  = arguments[4] || {};
-      this.fadeTo();
-   },
-
-   fadeTo: function() {
-      if (this.isFinished()) {
-         if(this.options.complete) this.options.complete(this);
-         return;
-      }
-
-      if (this.timer)
-         clearTimeout(this.timer);
-
-      var stepDuration = Math.round(this.duration/this.steps) ;
-      var currentOpacity = this.getElementOpacity();
-      var delta = this.steps > 0 ? (this.opacity - currentOpacity)/this.steps : 0;
-
-      this.changeOpacityBy(delta);
-      this.duration -= stepDuration;
-      this.steps--;
-
-      this.timer = setTimeout(this.fadeTo.bind(this), stepDuration);
-   },
-
-   changeOpacityBy: function(v) {
-      var currentOpacity = this.getElementOpacity();
-      var newOpacity = Math.max(0, Math.min(currentOpacity+v, 1));
-      this.element.ricoOpacity = newOpacity;
-
-      this.element.style.filter = "alpha(opacity:"+Math.round(newOpacity*100)+")";
-      this.element.style.opacity = newOpacity; /*//*/;
-   },
-
-   isFinished: function() {
-      return this.steps <= 0;
-   },
-
-   getElementOpacity: function() {
-      if ( this.element.ricoOpacity == undefined ) {
-         var opacity;
-         if ( this.element.currentStyle ) {
-            opacity = this.element.currentStyle.opacity;
-         }
-         else if ( document.defaultView.getComputedStyle != undefined ) {
-            var computedStyle = document.defaultView.getComputedStyle;
-            opacity = computedStyle(this.element, null).getPropertyValue('opacity');
-         }
-
-         this.element.ricoOpacity = opacity != undefined ? opacity : 1.0;
-      }
-
-      return parseFloat(this.element.ricoOpacity);
-   }
-}
-
-Effect.AccordionSize = Class.create();
-
-Effect.AccordionSize.prototype = {
-
-   initialize: function(e1, e2, start, end, duration, steps, options) {
-      this.e1       = $(e1);
-      this.e2       = $(e2);
-      this.start    = start;
-      this.end      = end;
-      this.duration = duration;
-      this.steps    = steps;
-      this.options  = arguments[6] || {};
-
-      this.accordionSize();
-   },
-
-   accordionSize: function() {
-
-      if (this.isFinished()) {
-         // just in case there are round errors or such...
-         this.e1.style.height = this.start + "px";
-         this.e2.style.height = this.end + "px";
-
-         if(this.options.complete)
-            this.options.complete(this);
-         return;
-      }
-
-      if (this.timer)
-         clearTimeout(this.timer);
-
-      var stepDuration = Math.round(this.duration/this.steps) ;
-
-      var diff = this.steps > 0 ? (parseInt(this.e1.offsetHeight) - this.start)/this.steps : 0;
-      this.resizeBy(diff);
-
-      this.duration -= stepDuration;
-      this.steps--;
-
-      this.timer = setTimeout(this.accordionSize.bind(this), stepDuration);
-   },
-
-   isFinished: function() {
-      return this.steps <= 0;
-   },
-
-   resizeBy: function(diff) {
-      var h1Height = this.e1.offsetHeight;
-      var h2Height = this.e2.offsetHeight;
-      var intDiff = parseInt(diff);
-      if ( diff != 0 ) {
-         this.e1.style.height = (h1Height - intDiff) + "px";
-         this.e2.style.height = (h2Height + intDiff) + "px";
-      }
-   }
-
-};
-
-
-//-------------------- ricoLiveGrid.js
-
-// Rico.LiveGridMetaData -----------------------------------------------------
-
-Rico.LiveGridMetaData = Class.create();
-
-Rico.LiveGridMetaData.prototype = {
-
-   initialize: function( pageSize, totalRows, columnCount, options ) {
-      this.pageSize  = pageSize;
-      this.totalRows = totalRows;
-      this.setOptions(options);
-      this.scrollArrowHeight = 16;
-      this.columnCount = columnCount;
-   },
-
-   setOptions: function(options) {
-      this.options = {
-         largeBufferSize    : 7.0,   // 7 pages
-         nearLimitFactor    : 0.2    // 20% of buffer
-      }.extend(options || {});
-   },
-
-   getPageSize: function() {
-      return this.pageSize;
-   },
-
-   getTotalRows: function() {
-      return this.totalRows;
-   },
-
-   setTotalRows: function(n) {
-      this.totalRows = n;
-   },
-
-   getLargeBufferSize: function() {
-      return parseInt(this.options.largeBufferSize * this.pageSize);
-   },
-
-   getLimitTolerance: function() {
-      return parseInt(this.getLargeBufferSize() * this.options.nearLimitFactor);
-   }
-};
-
-// Rico.LiveGridScroller -----------------------------------------------------
-
-Rico.LiveGridScroller = Class.create();
-
-Rico.LiveGridScroller.prototype = {
-
-   initialize: function(liveGrid, viewPort) {
-      this.isIE = navigator.userAgent.toLowerCase().indexOf("msie") >= 0;
-      this.liveGrid = liveGrid;
-      this.metaData = liveGrid.metaData;
-      this.createScrollBar();
-      this.scrollTimeout = null;
-      this.lastScrollPos = 0;
-      this.viewPort = viewPort;
-      this.rows = new Array();
-   },
-
-   isUnPlugged: function() {
-      return this.scrollerDiv.onscroll == null;
-   },
-
-   plugin: function() {
-      this.scrollerDiv.onscroll = this.handleScroll.bindAsEventListener(this);
-   },
-
-   unplug: function() {
-      this.scrollerDiv.onscroll = null;
-   },
-
-   sizeIEHeaderHack: function() {
-      if ( !this.isIE ) return;
-      var headerTable = $(this.liveGrid.tableId + "_header");
-      if ( headerTable )
-         headerTable.rows[0].cells[0].style.width =
-            (headerTable.rows[0].cells[0].offsetWidth + 1) + "px";
-   },
-
-   createScrollBar: function() {
-      var visibleHeight = this.liveGrid.viewPort.visibleHeight();
-      // create the outer div...
-      this.scrollerDiv  = document.createElement("div");
-      var scrollerStyle = this.scrollerDiv.style;
-      scrollerStyle.borderRight = "1px solid #ababab"; // hard coded color!!!
-      scrollerStyle.position    = "relative";
-      scrollerStyle.left        = this.isIE ? "-6px" : "-3px";
-      scrollerStyle.width       = "19px";
-      scrollerStyle.height      = visibleHeight + "px";
-      scrollerStyle.overflow    = "auto";
-
-      // create the inner div...
-      this.heightDiv = document.createElement("div");
-      this.heightDiv.style.width  = "1px";
-
-      this.heightDiv.style.height = parseInt(visibleHeight *
-                        this.metaData.getTotalRows()/this.metaData.getPageSize()) + "px" ;
-      this.scrollerDiv.appendChild(this.heightDiv);
-      this.scrollerDiv.onscroll = this.handleScroll.bindAsEventListener(this);
-
-     var table = this.liveGrid.table;
-     table.parentNode.parentNode.insertBefore( this.scrollerDiv, table.parentNode.nextSibling );
-   },
-
-   updateSize: function() {
-      var table = this.liveGrid.table;
-      var visibleHeight = this.viewPort.visibleHeight();
-      this.heightDiv.style.height = parseInt(visibleHeight *
-                                  this.metaData.getTotalRows()/this.metaData.getPageSize()) + "px";
-   },
-
-   rowToPixel: function(rowOffset) {
-      return (rowOffset / this.metaData.getTotalRows()) * this.heightDiv.offsetHeight
-   },
-   
-   moveScroll: function(rowOffset) {
-      this.scrollerDiv.scrollTop = this.rowToPixel(rowOffset);
-      if ( this.metaData.options.onscroll )
-         this.metaData.options.onscroll( this.liveGrid, rowOffset );    
-   },
-
-   handleScroll: function() {
-     if ( this.scrollTimeout )
-         clearTimeout( this.scrollTimeout );
-
-      var contentOffset = parseInt(this.scrollerDiv.scrollTop / this.viewPort.rowHeight);
-      this.liveGrid.requestContentRefresh(contentOffset);
-      this.viewPort.scrollTo(this.scrollerDiv.scrollTop);
-      
-      if ( this.metaData.options.onscroll )
-         this.metaData.options.onscroll( this.liveGrid, contentOffset );
-
-      this.scrollTimeout = setTimeout( this.scrollIdle.bind(this), 1200 );
-   },
-
-   scrollIdle: function() {
-      if ( this.metaData.options.onscrollidle )
-         this.metaData.options.onscrollidle();
-   }
-};
-
-// Rico.LiveGridBuffer -----------------------------------------------------
-
-Rico.LiveGridBuffer = Class.create();
-
-Rico.LiveGridBuffer.prototype = {
-
-   initialize: function(metaData, viewPort) {
-      this.startPos = 0;
-      this.size     = 0;
-      this.metaData = metaData;
-      this.rows     = new Array();
-      this.updateInProgress = false;
-      this.viewPort = viewPort;
-      this.maxBufferSize = metaData.getLargeBufferSize() * 2;
-      this.maxFetchSize = metaData.getLargeBufferSize();
-      this.lastOffset = 0;
-   },
-
-   getBlankRow: function() {
-      if (!this.blankRow ) {
-         this.blankRow = new Array();
-         for ( var i=0; i < this.metaData.columnCount ; i++ ) 
-            this.blankRow[i] = " ";
-     }
-     return this.blankRow;
-   },
-   
-   loadRows: function(ajaxResponse) {
-      var rowsElement = ajaxResponse.getElementsByTagName('rows')[0];
-      this.updateUI = rowsElement.getAttribute("update_ui") == "true"
-      var newRows = new Array()
-      var trs = rowsElement.getElementsByTagName("tr");
-      for ( var i=0 ; i < trs.length; i++ ) {
-         var row = newRows[i] = new Array(); 
-         var cells = trs[i].getElementsByTagName("td");
-         for ( var j=0; j < cells.length ; j++ ) {
-            var cell = cells[j];
-            var convertSpaces = cell.getAttribute("convert_spaces") == "true";
-            var cellContent = RicoUtil.getContentAsString(cell);
-            row[j] = convertSpaces ? this.convertSpaces(cellContent) : cellContent;
-            if (!row[j]) 
-               row[j] = ' ';
-         }
-      }
-      return newRows;
-   },
-      
-   update: function(ajaxResponse, start) {
-     var newRows = this.loadRows(ajaxResponse);
-      if (this.rows.length == 0) { // initial load
-         this.rows = newRows;
-         this.size = this.rows.length;
-         this.startPos = start;
-         return;
-      }
-      if (start > this.startPos) { //appending
-         if (this.startPos + this.rows.length < start) {
-            this.rows =  newRows;
-            this.startPos = start;//
-         } else {
-              this.rows = this.rows.concat( newRows.slice(0, newRows.length));
-            if (this.rows.length > this.maxBufferSize) {
-               var fullSize = this.rows.length;
-               this.rows = this.rows.slice(this.rows.length - this.maxBufferSize, this.rows.length)
-               this.startPos = this.startPos +  (fullSize - this.rows.length);
-            }
-         }
-      } else { //prepending
-         if (start + newRows.length < this.startPos) {
-            this.rows =  newRows;
-         } else {
-            this.rows = newRows.slice(0, this.startPos).concat(this.rows);
-            if (this.rows.length > this.maxBufferSize) 
-               this.rows = this.rows.slice(0, this.maxBufferSize)
-         }
-         this.startPos =  start;
-      }
-      this.size = this.rows.length;
-   },
-   
-   clear: function() {
-      this.rows = new Array();
-      this.startPos = 0;
-      this.size = 0;
-   },
-
-   isOverlapping: function(start, size) {
-      return ((start < this.endPos()) && (this.startPos < start + size)) || (this.endPos() == 0)
-   },
-
-   isInRange: function(position) {
-      return (position >= this.startPos) && (position + this.metaData.getPageSize() <= this.endPos()); 
-             //&& this.size()  != 0;
-   },
-
-   isNearingTopLimit: function(position) {
-      return position - this.startPos < this.metaData.getLimitTolerance();
-   },
-
-   endPos: function() {
-      return this.startPos + this.rows.length;
-   },
-   
-   isNearingBottomLimit: function(position) {
-      return this.endPos() - (position + this.metaData.getPageSize()) < this.metaData.getLimitTolerance();
-   },
-
-   isAtTop: function() {
-      return this.startPos == 0;
-   },
-
-   isAtBottom: function() {
-      return this.endPos() == this.metaData.getTotalRows();
-   },
-
-   isNearingLimit: function(position) {
-      return ( !this.isAtTop()    && this.isNearingTopLimit(position)) ||
-             ( !this.isAtBottom() && this.isNearingBottomLimit(position) )
-   },
-
-   getFetchSize: function(offset) {
-      var adjustedOffset = this.getFetchOffset(offset);
-      var adjustedSize = 0;
-      if (adjustedOffset >= this.startPos) { //apending
-         var endFetchOffset = this.maxFetchSize  + adjustedOffset;
-         if (endFetchOffset > this.metaData.totalRows)
-            endFetchOffset = this.metaData.totalRows;
-         adjustedSize = endFetchOffset - adjustedOffset;   
-      } else {//prepending
-         var adjustedSize = this.startPos - adjustedOffset;
-         if (adjustedSize > this.maxFetchSize)
-            adjustedSize = this.maxFetchSize;
-      }
-      return adjustedSize;
-   }, 
-
-   getFetchOffset: function(offset) {
-      var adjustedOffset = offset;
-      if (offset > this.startPos)  //apending
-         adjustedOffset = (offset > this.endPos()) ? offset :  this.endPos(); 
-      else { //prepending
-         if (offset + this.maxFetchSize >= this.startPos) {
-            var adjustedOffset = this.startPos - this.maxFetchSize;
-            if (adjustedOffset < 0)
-               adjustedOffset = 0;
-         }
-      }
-      this.lastOffset = adjustedOffset;
-      return adjustedOffset;
-   },
-
-   getRows: function(start, count) {
-      var begPos = start - this.startPos
-      var endPos = begPos + count
-
-      // er? need more data...
-      if ( endPos > this.size )
-         endPos = this.size
-
-      var results = new Array()
-      var index = 0;
-      for ( var i=begPos ; i < endPos; i++ ) {
-         results[index++] = this.rows[i]
-      }
-      return results
-   },
-
-   convertSpaces: function(s) {
-      return s.split(" ").join(" ");
-   }
-
-};
-
-
-//Rico.GridViewPort --------------------------------------------------
-Rico.GridViewPort = Class.create();
-
-Rico.GridViewPort.prototype = {
-
-   initialize: function(table, rowHeight, visibleRows, buffer, liveGrid) {
-      this.lastDisplayedStartPos = 0;
-      this.div = table.parentNode;
-      this.table = table
-      this.rowHeight = rowHeight;
-      this.div.style.height = this.rowHeight * visibleRows;
-      this.div.style.overflow = "hidden";
-      this.buffer = buffer;
-      this.liveGrid = liveGrid;
-      this.visibleRows = visibleRows + 1;
-      this.lastPixelOffset = 0;
-      this.startPos = 0;
-   },
-
-   populateRow: function(htmlRow, row) {
-      for (var j=0; j < row.length; j++) {
-         htmlRow.cells[j].innerHTML = row[j]
-      }
-   },
-   
-   bufferChanged: function() {
-      this.refreshContents( parseInt(this.lastPixelOffset / this.rowHeight));
-   },
-   
-   clearRows: function() {
-      if (!this.isBlank) {
-         for (var i=0; i < this.visibleRows; i++)
-            this.populateRow(this.table.rows[i], this.buffer.getBlankRow());
-         this.isBlank = true;
-      }
-   },
-   
-   clearContents: function() {   
-      this.clearRows();
-      this.scrollTo(0);
-      this.startPos = 0;
-      this.lastStartPos = -1;   
-   },
-   
-   refreshContents: function(startPos) {
-      if (startPos == this.lastRowPos && !this.isPartialBlank && !this.isBlank) {
-         return;
-      }
-      if ((startPos + this.visibleRows < this.buffer.startPos)  
-          || (this.buffer.startPos + this.buffer.size < startPos) 
-          || (this.buffer.size == 0)) {
-         this.clearRows();
-         return;
-      }
-      this.isBlank = false;
-      var viewPrecedesBuffer = this.buffer.startPos > startPos
-      var contentStartPos = viewPrecedesBuffer ? this.buffer.startPos: startPos;
-   
-      var contentEndPos = (this.buffer.startPos + this.buffer.size < startPos + this.visibleRows) 
-                                 ? this.buffer.startPos + this.buffer.size
-                                 : startPos + this.visibleRows;       
-      var rowSize = contentEndPos - contentStartPos;
-      var rows = this.buffer.getRows(contentStartPos, rowSize ); 
-      var blankSize = this.visibleRows - rowSize;
-      var blankOffset = viewPrecedesBuffer ? 0: rowSize;
-      var contentOffset = viewPrecedesBuffer ? blankSize: 0;
-
-      for (var i=0; i < rows.length; i++) {//initialize what we have
-        this.populateRow(this.table.rows[i + contentOffset], rows[i]);
-      }       
-      for (var i=0; i < blankSize; i++) {// blank out the rest 
-        this.populateRow(this.table.rows[i + blankOffset], this.buffer.getBlankRow());
-      }
-      this.isPartialBlank = blankSize > 0;
-      this.lastRowPos = startPos;   
-   },
-
-   scrollTo: function(pixelOffset) {      
-      if (this.lastPixelOffset == pixelOffset)
-         return;
-
-      this.refreshContents(parseInt(pixelOffset / this.rowHeight))
-      this.div.scrollTop = pixelOffset % this.rowHeight        
-      
-      this.lastPixelOffset = pixelOffset;
-   },
-   
-   visibleHeight: function() {
-      return parseInt(this.div.style.height);
-   }
-   
-};
-
-
-Rico.LiveGridRequest = Class.create();
-Rico.LiveGridRequest.prototype = {
-   initialize: function( requestOffset, options ) {
-      this.requestOffset = requestOffset;
-   }
-};
-
-// Rico.LiveGrid -----------------------------------------------------
-
-Rico.LiveGrid = Class.create();
-
-Rico.LiveGrid.prototype = {
-
-   initialize: function( tableId, visibleRows, totalRows, url, options ) {
-      if ( options == null )
-         options = {};
-
-      this.tableId     = tableId; 
-      this.table       = $(tableId);
-      var columnCount  = this.table.rows[0].cells.length
-      this.metaData    = new Rico.LiveGridMetaData(visibleRows, totalRows, columnCount, options);
-      this.buffer      = new Rico.LiveGridBuffer(this.metaData);
-
-      var rowCount = this.table.rows.length;
-      this.viewPort =  new Rico.GridViewPort(this.table, 
-                                            this.table.offsetHeight/rowCount,
-                                            visibleRows,
-                                            this.buffer, this);
-      this.scroller    = new Rico.LiveGridScroller(this,this.viewPort);
-      
-      this.additionalParms       = options.requestParameters || [];
-      
-      options.sortHandler = this.sortHandler.bind(this);
-
-      if ( $(tableId + '_header') )
-         this.sort = new Rico.LiveGridSort(tableId + '_header', options)
-
-      this.processingRequest = null;
-      this.unprocessedRequest = null;
-
-      this.initAjax(url);
-      if ( options.prefetchBuffer || options.prefetchOffset > 0) {
-         var offset = 0;
-         if (options.offset ) {
-            offset = options.offset;            
-            this.scroller.moveScroll(offset);
-            this.viewPort.scrollTo(this.scroller.rowToPixel(offset));            
-         }
-         if (options.sortCol) {
-             this.sortCol = options.sortCol;
-             this.sortDir = options.sortDir;
-         }
-         this.requestContentRefresh(offset);
-      }
-   },
-
-   resetContents: function() {
-      this.scroller.moveScroll(0);
-      this.buffer.clear();
-      this.viewPort.clearContents();
-   },
-   
-   sortHandler: function(column) {
-      this.sortCol = column.name;
-      this.sortDir = column.currentSort;
-
-      this.resetContents();
-      this.requestContentRefresh(0) 
-   },
-   
-   setRequestParams: function() {
-      this.additionalParms = [];
-      for ( var i=0 ; i < arguments.length ; i++ )
-         this.additionalParms[i] = arguments[i];
-   },
-
-   setTotalRows: function( newTotalRows ) {
-      this.resetContents();
-      this.metaData.setTotalRows(newTotalRows);
-      this.scroller.updateSize();
-   },
-
-   initAjax: function(url) {
-      ajaxEngine.registerRequest( this.tableId + '_request', url );
-      ajaxEngine.registerAjaxObject( this.tableId + '_updater', this );
-   },
-
-   invokeAjax: function() {
-   },
-
-   handleTimedOut: function() {
-      //server did not respond in 4 seconds... assume that there could have been
-      //an error or something, and allow requests to be processed again...
-      this.processingRequest = null;
-      this.processQueuedRequest();
-   },
-
-   fetchBuffer: function(offset) {
-      if ( this.buffer.isInRange(offset) &&
-         !this.buffer.isNearingLimit(offset)) {
-         return;
-      }
-      if (this.processingRequest) {
-          this.unprocessedRequest = new Rico.LiveGridRequest(offset);
-         return;
-      }
-      var bufferStartPos = this.buffer.getFetchOffset(offset);
-      this.processingRequest = new Rico.LiveGridRequest(offset);
-      this.processingRequest.bufferOffset = bufferStartPos;   
-      var fetchSize = this.buffer.getFetchSize(offset);
-      var partialLoaded = false;
-      var callParms = []; 
-      callParms.push(this.tableId + '_request');
-      callParms.push('id='        + this.tableId);
-      callParms.push('page_size=' + fetchSize);
-      callParms.push('offset='    + bufferStartPos);
-      if ( this.sortCol) {
-         callParms.push('sort_col='    + this.sortCol);
-         callParms.push('sort_dir='    + this.sortDir);
-      }
-      
-      for( var i=0 ; i < this.additionalParms.length ; i++ )
-         callParms.push(this.additionalParms[i]);
-      ajaxEngine.sendRequest.apply( ajaxEngine, callParms );
-        
-      this.timeoutHandler = setTimeout( this.handleTimedOut.bind(this), 20000 ); //todo: make as option
-   },
-
-   requestContentRefresh: function(contentOffset) {
-      this.fetchBuffer(contentOffset);
-   },
-
-   ajaxUpdate: function(ajaxResponse) {
-      try {
-         clearTimeout( this.timeoutHandler );
-         this.buffer.update(ajaxResponse,this.processingRequest.bufferOffset);
-         this.viewPort.bufferChanged();
-      }
-      catch(err) {}
-      finally {this.processingRequest = null; }
-      this.processQueuedRequest();
-   },
-
-   processQueuedRequest: function() {
-      if (this.unprocessedRequest != null) {
-         this.requestContentRefresh(this.unprocessedRequest.requestOffset);
-         this.unprocessedRequest = null
-      }  
-   }
- 
-};
-
-
-//-------------------- ricoLiveGridSort.js
-Rico.LiveGridSort = Class.create();
-
-Rico.LiveGridSort.prototype = {
-
-   initialize: function(headerTableId, options) {
-      this.headerTableId = headerTableId;
-      this.headerTable   = $(headerTableId);
-      this.setOptions(options);
-      this.applySortBehavior();
-
-      if ( this.options.sortCol ) {
-         this.setSortUI( this.options.sortCol, this.options.sortDir );
-      }
-   },
-
-   setSortUI: function( columnName, sortDirection ) {
-      var cols = this.options.columns;
-      for ( var i = 0 ; i < cols.length ; i++ ) {
-         if ( cols[i].name == columnName ) {
-            this.setColumnSort(i, sortDirection);
-            break;
-         }
-      }
-   },
-
-   setOptions: function(options) {
-      this.options = {
-         sortAscendImg:    'images/sort_asc.gif',
-         sortDescendImg:   'images/sort_desc.gif',
-         imageWidth:       9,
-         imageHeight:      5,
-         ajaxSortURLParms: []
-      }.extend(options);
-
-      // preload the images...
-      new Image().src = this.options.sortAscendImg;
-      new Image().src = this.options.sortDescendImg;
-
-      this.sort = options.sortHandler;
-      if ( !this.options.columns )
-         this.options.columns = this.introspectForColumnInfo();
-      else {
-         // allow client to pass { columns: [ ["a", true], ["b", false] ] }
-         // and convert to an array of Rico.TableColumn objs...
-         this.options.columns = this.convertToTableColumns(this.options.columns);
-      }
-   },
-
-   applySortBehavior: function() {
-      var headerRow   = this.headerTable.rows[0];
-      var headerCells = headerRow.cells;
-      for ( var i = 0 ; i < headerCells.length ; i++ ) {
-         this.addSortBehaviorToColumn( i, headerCells[i] );
-      }
-   },
-
-   addSortBehaviorToColumn: function( n, cell ) {
-      if ( this.options.columns[n].isSortable() ) {
-         cell.id            = this.headerTableId + '_' + n;
-         cell.style.cursor  = 'pointer';
-         cell.onclick       = this.headerCellClicked.bindAsEventListener(this);
-         cell.innerHTML     = cell.innerHTML + '<span id="' + this.headerTableId + '_img_' + n + '">'
-                           + '   </span>';
-      }
-   },
-
-   // event handler....
-   headerCellClicked: function(evt) {
-      var eventTarget = evt.target ? evt.target : evt.srcElement;
-      var cellId = eventTarget.id;
-      var columnNumber = parseInt(cellId.substring( cellId.lastIndexOf('_') + 1 ));
-      var sortedColumnIndex = this.getSortedColumnIndex();
-      if ( sortedColumnIndex != -1 ) {
-         if ( sortedColumnIndex != columnNumber ) {
-            this.removeColumnSort(sortedColumnIndex);
-            this.setColumnSort(columnNumber, Rico.TableColumn.SORT_ASC);
-         }
-         else
-            this.toggleColumnSort(sortedColumnIndex);
-      }
-      else
-         this.setColumnSort(columnNumber, Rico.TableColumn.SORT_ASC);
-
-      if (this.options.sortHandler) {
-         this.options.sortHandler(this.options.columns[columnNumber]);
-      }
-   },
-
-   removeColumnSort: function(n) {
-      this.options.columns[n].setUnsorted();
-      this.setSortImage(n);
-   },
-
-   setColumnSort: function(n, direction) {
-      this.options.columns[n].setSorted(direction);
-      this.setSortImage(n);
-   },
-
-   toggleColumnSort: function(n) {
-      this.options.columns[n].toggleSort();
-      this.setSortImage(n);
-   },
-
-   setSortImage: function(n) {
-      var sortDirection = this.options.columns[n].getSortDirection();
-
-      var sortImageSpan = $( this.headerTableId + '_img_' + n );
-      if ( sortDirection == Rico.TableColumn.UNSORTED )
-         sortImageSpan.innerHTML = '  ';
-      else if ( sortDirection == Rico.TableColumn.SORT_ASC )
-         sortImageSpan.innerHTML = '  <img width="'  + this.options.imageWidth    + '" ' +
-                                                     'height="'+ this.options.imageHeight   + '" ' +
-                                                     'src="'   + this.options.sortAscendImg + '"/>';
-      else if ( sortDirection == Rico.TableColumn.SORT_DESC )
-         sortImageSpan.innerHTML = '  <img width="'  + this.options.imageWidth    + '" ' +
-                                                     'height="'+ this.options.imageHeight   + '" ' +
-                                                     'src="'   + this.options.sortDescendImg + '"/>';
-   },
-
-   getSortedColumnIndex: function() {
-      var cols = this.options.columns;
-      for ( var i = 0 ; i < cols.length ; i++ ) {
-         if ( cols[i].isSorted() )
-            return i;
-      }
-
-      return -1;
-   },
-
-   introspectForColumnInfo: function() {
-      var columns = new Array();
-      var headerRow   = this.headerTable.rows[0];
-      var headerCells = headerRow.cells;
-      for ( var i = 0 ; i < headerCells.length ; i++ )
-         columns.push( new Rico.TableColumn( this.deriveColumnNameFromCell(headerCells[i],i), true ) );
-      return columns;
-   },
-
-   convertToTableColumns: function(cols) {
-      var columns = new Array();
-      for ( var i = 0 ; i < cols.length ; i++ )
-         columns.push( new Rico.TableColumn( cols[i][0], cols[i][1] ) );
-   },
-
-   deriveColumnNameFromCell: function(cell,columnNumber) {
-      var cellContent = cell.innerText != undefined ? cell.innerText : cell.textContent;
-      return cellContent ? cellContent.toLowerCase().split(' ').join('_') : "col_" + columnNumber;
-   }
-};
-
-Rico.TableColumn = Class.create();
-
-Rico.TableColumn.UNSORTED  = 0;
-Rico.TableColumn.SORT_ASC  = "ASC";
-Rico.TableColumn.SORT_DESC = "DESC";
-
-Rico.TableColumn.prototype = {
-   initialize: function(name, sortable) {
-      this.name        = name;
-      this.sortable    = sortable;
-      this.currentSort = Rico.TableColumn.UNSORTED;
-   },
-
-   isSortable: function() {
-      return this.sortable;
-   },
-
-   isSorted: function() {
-      return this.currentSort != Rico.TableColumn.UNSORTED;
-   },
-
-   getSortDirection: function() {
-      return this.currentSort;
-   },
-
-   toggleSort: function() {
-      if ( this.currentSort == Rico.TableColumn.UNSORTED || this.currentSort == Rico.TableColumn.SORT_DESC )
-         this.currentSort = Rico.TableColumn.SORT_ASC;
-      else if ( this.currentSort == Rico.TableColumn.SORT_ASC )
-         this.currentSort = Rico.TableColumn.SORT_DESC;
-   },
-
-   setUnsorted: function(direction) {
-      this.setSorted(Rico.TableColumn.UNSORTED);
-   },
-
-   setSorted: function(direction) {
-      // direction must by one of Rico.TableColumn.UNSORTED, .SORT_ASC, or .SET_DESC...
-      this.currentSort = direction;
-   }
-
-};
-
-
-//-------------------- ricoUtil.js
-
-var RicoUtil = {
-
-   getElementsComputedStyle: function ( htmlElement, cssProperty, mozillaEquivalentCSS) {
-      if ( arguments.length == 2 )
-         mozillaEquivalentCSS = cssProperty;
-
-      var el = $(htmlElement);
-      if ( el.currentStyle )
-         return el.currentStyle[cssProperty];
-      else
-         return document.defaultView.getComputedStyle(el, null).getPropertyValue(mozillaEquivalentCSS);
-   },
-
-   createXmlDocument : function() {
-      if (document.implementation && document.implementation.createDocument) {
-         var doc = document.implementation.createDocument("", "", null);
-
-         if (doc.readyState == null) {
-            doc.readyState = 1;
-            doc.addEventListener("load", function () {
-               doc.readyState = 4;
-               if (typeof doc.onreadystatechange == "function")
-                  doc.onreadystatechange();
-            }, false);
-         }
-
-         return doc;
-      }
-
-      if (window.ActiveXObject)
-          return Try.these(
-            function() { return new ActiveXObject('MSXML2.DomDocument')   },
-            function() { return new ActiveXObject('Microsoft.DomDocument')},
-            function() { return new ActiveXObject('MSXML.DomDocument')    },
-            function() { return new ActiveXObject('MSXML3.DomDocument')   }
-          ) || false;
-
-      return null;
-   },
-
-   getContentAsString: function( parentNode ) {
-      return parentNode.xml != undefined ? 
-         this._getContentAsStringIE(parentNode) :
-         this._getContentAsStringMozilla(parentNode);
-   },
-
-   _getContentAsStringIE: function(parentNode) {
-      var contentStr = "";
-      for ( var i = 0 ; i < parentNode.childNodes.length ; i++ )
-         contentStr += parentNode.childNodes[i].xml;
-      return contentStr;
-   },
-
-   _getContentAsStringMozilla: function(parentNode) {
-      var xmlSerializer = new XMLSerializer();
-      var contentStr = "";
-      for ( var i = 0 ; i < parentNode.childNodes.length ; i++ )
-         contentStr += xmlSerializer.serializeToString(parentNode.childNodes[i]);
-      return contentStr;
-   },
-
-   toViewportPosition: function(element) {
-      return this._toAbsolute(element,true);
-   },
-
-   toDocumentPosition: function(element) {
-      return this._toAbsolute(element,false);
-   },
-
-   /**
-    *  Compute the elements position in terms of the window viewport
-    *  so that it can be compared to the position of the mouse (dnd)
-    *  This is additions of all the offsetTop,offsetLeft values up the
-    *  offsetParent hierarchy, ...taking into account any scrollTop,
-    *  scrollLeft values along the way...
-    *
-    * IE has a bug reporting a correct offsetLeft of elements within a
-    * a relatively positioned parent!!!
-    **/
-   _toAbsolute: function(element,accountForDocScroll) {
-
-      if ( navigator.userAgent.toLowerCase().indexOf("msie") == -1 )
-         return this._toAbsoluteMozilla(element,accountForDocScroll);
-
-      var x = 0;
-      var y = 0;
-      var parent = element;
-      while ( parent ) {
-
-         var borderXOffset = 0;
-         var borderYOffset = 0;
-         if ( parent != element ) {
-            var borderXOffset = parseInt(this.getElementsComputedStyle(parent, "borderLeftWidth" ));
-            var borderYOffset = parseInt(this.getElementsComputedStyle(parent, "borderTopWidth" ));
-            borderXOffset = isNaN(borderXOffset) ? 0 : borderXOffset;
-            borderYOffset = isNaN(borderYOffset) ? 0 : borderYOffset;
-         }
-
-         x += parent.offsetLeft - parent.scrollLeft + borderXOffset;
-         y += parent.offsetTop - parent.scrollTop + borderYOffset;
-         parent = parent.offsetParent;
-      }
-
-      if ( accountForDocScroll ) {
-         x -= this.docScrollLeft();
-         y -= this.docScrollTop();
-      }
-
-      return { x:x, y:y };
-   },
-
-   /**
-    *  Mozilla did not report all of the parents up the hierarchy via the
-    *  offsetParent property that IE did.  So for the calculation of the
-    *  offsets we use the offsetParent property, but for the calculation of
-    *  the scrollTop/scrollLeft adjustments we navigate up via the parentNode
-    *  property instead so as to get the scroll offsets...
-    *
-    **/
-   _toAbsoluteMozilla: function(element,accountForDocScroll) {
-      var x = 0;
-      var y = 0;
-      var parent = element;
-      while ( parent ) {
-         x += parent.offsetLeft;
-         y += parent.offsetTop;
-         parent = parent.offsetParent;
-      }
-
-      parent = element;
-      while ( parent &&
-              parent != document.body &&
-              parent != document.documentElement ) {
-         if ( parent.scrollLeft  )
-            x -= parent.scrollLeft;
-         if ( parent.scrollTop )
-            y -= parent.scrollTop;
-         parent = parent.parentNode;
-      }
-
-      if ( accountForDocScroll ) {
-         x -= this.docScrollLeft();
-         y -= this.docScrollTop();
-      }
-
-      return { x:x, y:y };
-   },
-
-   docScrollLeft: function() {
-      if ( window.pageXOffset )
-         return window.pageXOffset;
-      else if ( document.documentElement && document.documentElement.scrollLeft )
-         return document.documentElement.scrollLeft;
-      else if ( document.body )
-         return document.body.scrollLeft;
-      else
-         return 0;
-   },
-
-   docScrollTop: function() {
-      if ( window.pageYOffset )
-         return window.pageYOffset;
-      else if ( document.documentElement && document.documentElement.scrollTop )
-         return document.documentElement.scrollTop;
-      else if ( document.body )
-         return document.body.scrollTop;
-      else
-         return 0;
-   }
-
-};
diff --git a/web/static/js/scriptaculous/builder.js b/web/static/js/scriptaculous/builder.js
deleted file mode 100644
index 5e00f45..0000000
--- a/web/static/js/scriptaculous/builder.js
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
-//
-// See scriptaculous.js for full license.
-
-var Builder = {
-  NODEMAP: {
-    AREA: 'map',
-    CAPTION: 'table',
-    COL: 'table',
-    COLGROUP: 'table',
-    LEGEND: 'fieldset',
-    OPTGROUP: 'select',
-    OPTION: 'select',
-    PARAM: 'object',
-    TBODY: 'table',
-    TD: 'table',
-    TFOOT: 'table',
-    TH: 'table',
-    THEAD: 'table',
-    TR: 'table'
-  },
-  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
-  //       due to a Firefox bug
-  node: function(elementName) {
-    elementName = elementName.toUpperCase();
-    
-    // try innerHTML approach
-    var parentTag = this.NODEMAP[elementName] || 'div';
-    var parentElement = document.createElement(parentTag);
-    parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
-    var element = parentElement.firstChild || null;
-      
-    // see if browser added wrapping tags
-    if(element && (element.tagName != elementName))
-      element = element.getElementsByTagName(elementName)[0];
-    
-    // fallback to createElement approach
-    if(!element) element = document.createElement(elementName);
-    
-    // abort if nothing could be created
-    if(!element) return;
-
-    // attributes (or text)
-    if(arguments[1])
-      if(this._isStringOrNumber(arguments[1]) ||
-        (arguments[1] instanceof Array)) {
-          this._children(element, arguments[1]);
-        } else {
-          var attrs = this._attributes(arguments[1]);
-          if(attrs.length) {
-            parentElement.innerHTML = "<" +elementName + " " +
-              attrs + "></" + elementName + ">";
-            element = parentElement.firstChild || null;
-            // workaround firefox 1.0.X bug
-            if(!element) {
-              element = document.createElement(elementName);
-              for(attr in arguments[1]) 
-                element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
-            }
-            if(element.tagName != elementName)
-              element = parentElement.getElementsByTagName(elementName)[0];
-            }
-        } 
-
-    // text, or array of children
-    if(arguments[2])
-      this._children(element, arguments[2]);
-
-     return element;
-  },
-  _text: function(text) {
-     return document.createTextNode(text);
-  },
-  _attributes: function(attributes) {
-    var attrs = [];
-    for(attribute in attributes)
-      attrs.push((attribute=='className' ? 'class' : attribute) +
-          '="' + attributes[attribute].toString().escapeHTML() + '"');
-    return attrs.join(" ");
-  },
-  _children: function(element, children) {
-    if(typeof children=='object') { // array can hold nodes and text
-      children.flatten().each( function(e) {
-        if(typeof e=='object')
-          element.appendChild(e)
-        else
-          if(Builder._isStringOrNumber(e))
-            element.appendChild(Builder._text(e));
-      });
-    } else
-      if(Builder._isStringOrNumber(children)) 
-         element.appendChild(Builder._text(children));
-  },
-  _isStringOrNumber: function(param) {
-    return(typeof param=='string' || typeof param=='number');
-  }
-}
\ No newline at end of file
diff --git a/web/static/js/scriptaculous/controls.js b/web/static/js/scriptaculous/controls.js
deleted file mode 100644
index 18bdf20..0000000
--- a/web/static/js/scriptaculous/controls.js
+++ /dev/null
@@ -1,721 +0,0 @@
-// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
-//           (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
-//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
-// Contributors:
-//  Richard Livsey
-//  Rahul Bhargava
-//  Rob Wills
-// 
-// See scriptaculous.js for full license.
-
-// Autocompleter.Base handles all the autocompletion functionality 
-// that's independent of the data source for autocompletion. This
-// includes drawing the autocompletion menu, observing keyboard
-// and mouse events, and similar.
-//
-// Specific autocompleters need to provide, at the very least, 
-// a getUpdatedChoices function that will be invoked every time
-// the text inside the monitored textbox changes. This method 
-// should get the text for which to provide autocompletion by
-// invoking this.getToken(), NOT by directly accessing
-// this.element.value. This is to allow incremental tokenized
-// autocompletion. Specific auto-completion logic (AJAX, etc)
-// belongs in getUpdatedChoices.
-//
-// Tokenized incremental autocompletion is enabled automatically
-// when an autocompleter is instantiated with the 'tokens' option
-// in the options parameter, e.g.:
-// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
-// will incrementally autocomplete with a comma as the token.
-// Additionally, ',' in the above example can be replaced with
-// a token array, e.g. { tokens: [',', '\n'] } which
-// enables autocompletion on multiple tokens. This is most 
-// useful when one of the tokens is \n (a newline), as it 
-// allows smart autocompletion after linebreaks.
-
-var Autocompleter = {}
-Autocompleter.Base = function() {};
-Autocompleter.Base.prototype = {
-  baseInitialize: function(element, update, options) {
-    this.element     = $(element); 
-    this.update      = $(update);  
-    this.hasFocus    = false; 
-    this.changed     = false; 
-    this.active      = false; 
-    this.index       = 0;     
-    this.entryCount  = 0;
-
-    if (this.setOptions)
-      this.setOptions(options);
-    else
-      this.options = options || {};
-
-    this.options.paramName    = this.options.paramName || this.element.name;
-    this.options.tokens       = this.options.tokens || [];
-    this.options.frequency    = this.options.frequency || 0.4;
-    this.options.minChars     = this.options.minChars || 1;
-    this.options.onShow       = this.options.onShow || 
-    function(element, update){ 
-      if(!update.style.position || update.style.position=='absolute') {
-        update.style.position = 'absolute';
-        Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
-      }
-      Effect.Appear(update,{duration:0.15});
-    };
-    this.options.onHide = this.options.onHide || 
-    function(element, update){ new Effect.Fade(update,{duration:0.15}) };
-
-    if (typeof(this.options.tokens) == 'string') 
-      this.options.tokens = new Array(this.options.tokens);
-
-    this.observer = null;
-    
-    this.element.setAttribute('autocomplete','off');
-
-    Element.hide(this.update);
-
-    Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
-    Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
-  },
-
-  show: function() {
-    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
-    if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && (Element.getStyle(this.update, 'position')=='absolute')) {
-      new Insertion.After(this.update, 
-       '<iframe id="' + this.update.id + '_iefix" '+
-       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
-       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
-      this.iefix = $(this.update.id+'_iefix');
-    }
-    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
-  },
-  
-  fixIEOverlapping: function() {
-    Position.clone(this.update, this.iefix);
-    this.iefix.style.zIndex = 1;
-    this.update.style.zIndex = 2;
-    Element.show(this.iefix);
-  },
-
-  hide: function() {
-    this.stopIndicator();
-    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
-    if(this.iefix) Element.hide(this.iefix);
-  },
-
-  startIndicator: function() {
-    if(this.options.indicator) Element.show(this.options.indicator);
-  },
-
-  stopIndicator: function() {
-    if(this.options.indicator) Element.hide(this.options.indicator);
-  },
-
-  onKeyPress: function(event) {
-    if(this.active)
-      switch(event.keyCode) {
-       case Event.KEY_TAB:
-       case Event.KEY_RETURN:
-         this.selectEntry();
-         Event.stop(event);
-       case Event.KEY_ESC:
-         this.hide();
-         this.active = false;
-         Event.stop(event);
-         return;
-       case Event.KEY_LEFT:
-       case Event.KEY_RIGHT:
-         return;
-       case Event.KEY_UP:
-         this.markPrevious();
-         this.render();
-         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
-         return;
-       case Event.KEY_DOWN:
-         this.markNext();
-         this.render();
-         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
-         return;
-      }
-     else 
-      if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 
-        return;
-
-    this.changed = true;
-    this.hasFocus = true;
-
-    if(this.observer) clearTimeout(this.observer);
-      this.observer = 
-        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
-  },
-
-  onHover: function(event) {
-    var element = Event.findElement(event, 'LI');
-    if(this.index != element.autocompleteIndex) 
-    {
-        this.index = element.autocompleteIndex;
-        this.render();
-    }
-    Event.stop(event);
-  },
-  
-  onClick: function(event) {
-    var element = Event.findElement(event, 'LI');
-    this.index = element.autocompleteIndex;
-    this.selectEntry();
-    this.hide();
-  },
-  
-  onBlur: function(event) {
-    // needed to make click events working
-    setTimeout(this.hide.bind(this), 250);
-    this.hasFocus = false;
-    this.active = false;     
-  }, 
-  
-  render: function() {
-    if(this.entryCount > 0) {
-      for (var i = 0; i < this.entryCount; i++)
-        this.index==i ? 
-          Element.addClassName(this.getEntry(i),"selected") : 
-          Element.removeClassName(this.getEntry(i),"selected");
-        
-      if(this.hasFocus) { 
-        this.show();
-        this.active = true;
-      }
-    } else {
-      this.active = false;
-      this.hide();
-    }
-  },
-  
-  markPrevious: function() {
-    if(this.index > 0) this.index--
-      else this.index = this.entryCount-1;
-  },
-  
-  markNext: function() {
-    if(this.index < this.entryCount-1) this.index++
-      else this.index = 0;
-  },
-  
-  getEntry: function(index) {
-    return this.update.firstChild.childNodes[index];
-  },
-  
-  getCurrentEntry: function() {
-    return this.getEntry(this.index);
-  },
-  
-  selectEntry: function() {
-    this.active = false;
-    this.updateElement(this.getCurrentEntry());
-  },
-
-  updateElement: function(selectedElement) {
-    if (this.options.updateElement) {
-      this.options.updateElement(selectedElement);
-      return;
-    }
-
-    var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
-    var lastTokenPos = this.findLastToken();
-    if (lastTokenPos != -1) {
-      var newValue = this.element.value.substr(0, lastTokenPos + 1);
-      var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
-      if (whitespace)
-        newValue += whitespace[0];
-      this.element.value = newValue + value;
-    } else {
-      this.element.value = value;
-    }
-    this.element.focus();
-    
-    if (this.options.afterUpdateElement)
-      this.options.afterUpdateElement(this.element, selectedElement);
-  },
-
-  updateChoices: function(choices) {
-    if(!this.changed && this.hasFocus) {
-      this.update.innerHTML = choices;
-      Element.cleanWhitespace(this.update);
-      Element.cleanWhitespace(this.update.firstChild);
-
-      if(this.update.firstChild && this.update.firstChild.childNodes) {
-        this.entryCount = 
-          this.update.firstChild.childNodes.length;
-        for (var i = 0; i < this.entryCount; i++) {
-          var entry = this.getEntry(i);
-          entry.autocompleteIndex = i;
-          this.addObservers(entry);
-        }
-      } else { 
-        this.entryCount = 0;
-      }
-
-      this.stopIndicator();
-
-      this.index = 0;
-      this.render();
-    }
-  },
-
-  addObservers: function(element) {
-    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
-    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
-  },
-
-  onObserverEvent: function() {
-    this.changed = false;   
-    if(this.getToken().length>=this.options.minChars) {
-      this.startIndicator();
-      this.getUpdatedChoices();
-    } else {
-      this.active = false;
-      this.hide();
-    }
-  },
-
-  getToken: function() {
-    var tokenPos = this.findLastToken();
-    if (tokenPos != -1)
-      var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
-    else
-      var ret = this.element.value;
-
-    return /\n/.test(ret) ? '' : ret;
-  },
-
-  findLastToken: function() {
-    var lastTokenPos = -1;
-
-    for (var i=0; i<this.options.tokens.length; i++) {
-      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
-      if (thisTokenPos > lastTokenPos)
-        lastTokenPos = thisTokenPos;
-    }
-    return lastTokenPos;
-  }
-}
-
-Ajax.Autocompleter = Class.create();
-Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
-  initialize: function(element, update, url, options) {
-	  this.baseInitialize(element, update, options);
-    this.options.asynchronous  = true;
-    this.options.onComplete    = this.onComplete.bind(this);
-    this.options.defaultParams = this.options.parameters || null;
-    this.url                   = url;
-  },
-
-  getUpdatedChoices: function() {
-    entry = encodeURIComponent(this.options.paramName) + '=' + 
-      encodeURIComponent(this.getToken());
-
-    this.options.parameters = this.options.callback ?
-      this.options.callback(this.element, entry) : entry;
-
-    if(this.options.defaultParams) 
-      this.options.parameters += '&' + this.options.defaultParams;
-
-    new Ajax.Request(this.url, this.options);
-  },
-
-  onComplete: function(request) {
-    this.updateChoices(request.responseText);
-  }
-
-});
-
-// The local array autocompleter. Used when you'd prefer to
-// inject an array of autocompletion options into the page, rather
-// than sending out Ajax queries, which can be quite slow sometimes.
-//
-// The constructor takes four parameters. The first two are, as usual,
-// the id of the monitored textbox, and id of the autocompletion menu.
-// The third is the array you want to autocomplete from, and the fourth
-// is the options block.
-//
-// Extra local autocompletion options:
-// - choices - How many autocompletion choices to offer
-//
-// - partialSearch - If false, the autocompleter will match entered
-//                    text only at the beginning of strings in the 
-//                    autocomplete array. Defaults to true, which will
-//                    match text at the beginning of any *word* in the
-//                    strings in the autocomplete array. If you want to
-//                    search anywhere in the string, additionally set
-//                    the option fullSearch to true (default: off).
-//
-// - fullSsearch - Search anywhere in autocomplete array strings.
-//
-// - partialChars - How many characters to enter before triggering
-//                   a partial match (unlike minChars, which defines
-//                   how many characters are required to do any match
-//                   at all). Defaults to 2.
-//
-// - ignoreCase - Whether to ignore case when autocompleting.
-//                 Defaults to true.
-//
-// It's possible to pass in a custom function as the 'selector' 
-// option, if you prefer to write your own autocompletion logic.
-// In that case, the other options above will not apply unless
-// you support them.
-
-Autocompleter.Local = Class.create();
-Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
-  initialize: function(element, update, array, options) {
-    this.baseInitialize(element, update, options);
-    this.options.array = array;
-  },
-
-  getUpdatedChoices: function() {
-    this.updateChoices(this.options.selector(this));
-  },
-
-  setOptions: function(options) {
-    this.options = Object.extend({
-      choices: 10,
-      partialSearch: true,
-      partialChars: 2,
-      ignoreCase: true,
-      fullSearch: false,
-      selector: function(instance) {
-        var ret       = []; // Beginning matches
-        var partial   = []; // Inside matches
-        var entry     = instance.getToken();
-        var count     = 0;
-
-        for (var i = 0; i < instance.options.array.length &&  
-          ret.length < instance.options.choices ; i++) { 
-
-          var elem = instance.options.array[i];
-          var foundPos = instance.options.ignoreCase ? 
-            elem.toLowerCase().indexOf(entry.toLowerCase()) : 
-            elem.indexOf(entry);
-
-          while (foundPos != -1) {
-            if (foundPos == 0 && elem.length != entry.length) { 
-              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + 
-                elem.substr(entry.length) + "</li>");
-              break;
-            } else if (entry.length >= instance.options.partialChars && 
-              instance.options.partialSearch && foundPos != -1) {
-              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
-                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
-                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
-                  foundPos + entry.length) + "</li>");
-                break;
-              }
-            }
-
-            foundPos = instance.options.ignoreCase ? 
-              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : 
-              elem.indexOf(entry, foundPos + 1);
-
-          }
-        }
-        if (partial.length)
-          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
-        return "<ul>" + ret.join('') + "</ul>";
-      }
-    }, options || {});
-  }
-});
-
-// AJAX in-place editor
-//
-// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
-
-// Use this if you notice weird scrolling problems on some browsers,
-// the DOM might be a bit confused when this gets called so do this
-// waits 1 ms (with setTimeout) until it does the activation
-Field.scrollFreeActivate = function(field) {
-  setTimeout(function() {
-    Field.activate(field);
-  }, 1);
-}
-
-Ajax.InPlaceEditor = Class.create();
-Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
-Ajax.InPlaceEditor.prototype = {
-  initialize: function(element, url, options) {
-    this.url = url;
-    this.element = $(element);
-
-    this.options = Object.extend({
-      okText: "ok",
-      cancelText: "cancel",
-      savingText: "Saving...",
-      clickToEditText: "Click to edit",
-      okText: "ok",
-      rows: 1,
-      onComplete: function(transport, element) {
-        new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
-      },
-      onFailure: function(transport) {
-        alert("Error communicating with the server: " + transport.responseText.stripTags());
-      },
-      callback: function(form) {
-        return Form.serialize(form);
-      },
-      handleLineBreaks: true,
-      loadingText: 'Loading...',
-      savingClassName: 'inplaceeditor-saving',
-      loadingClassName: 'inplaceeditor-loading',
-      formClassName: 'inplaceeditor-form',
-      highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
-      highlightendcolor: "#FFFFFF",
-      externalControl:	null,
-      ajaxOptions: {}
-    }, options || {});
-
-    if(!this.options.formId && this.element.id) {
-      this.options.formId = this.element.id + "-inplaceeditor";
-      if ($(this.options.formId)) {
-        // there's already a form with that name, don't specify an id
-        this.options.formId = null;
-      }
-    }
-    
-    if (this.options.externalControl) {
-      this.options.externalControl = $(this.options.externalControl);
-    }
-    
-    this.originalBackground = Element.getStyle(this.element, 'background-color');
-    if (!this.originalBackground) {
-      this.originalBackground = "transparent";
-    }
-    
-    this.element.title = this.options.clickToEditText;
-    
-    this.onclickListener = this.enterEditMode.bindAsEventListener(this);
-    this.mouseoverListener = this.enterHover.bindAsEventListener(this);
-    this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
-    Event.observe(this.element, 'click', this.onclickListener);
-    Event.observe(this.element, 'mouseover', this.mouseoverListener);
-    Event.observe(this.element, 'mouseout', this.mouseoutListener);
-    if (this.options.externalControl) {
-      Event.observe(this.options.externalControl, 'click', this.onclickListener);
-      Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
-      Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
-    }
-  },
-  enterEditMode: function(evt) {
-    if (this.saving) return;
-    if (this.editing) return;
-    this.editing = true;
-    this.onEnterEditMode();
-    if (this.options.externalControl) {
-      Element.hide(this.options.externalControl);
-    }
-    Element.hide(this.element);
-    this.createForm();
-    this.element.parentNode.insertBefore(this.form, this.element);
-    Field.scrollFreeActivate(this.editField);
-    // stop the event to avoid a page refresh in Safari
-    if (evt) {
-      Event.stop(evt);
-    }
-    return false;
-  },
-  createForm: function() {
-    this.form = document.createElement("form");
-    this.form.id = this.options.formId;
-    Element.addClassName(this.form, this.options.formClassName)
-    this.form.onsubmit = this.onSubmit.bind(this);
-
-    this.createEditField();
-
-    if (this.options.textarea) {
-      var br = document.createElement("br");
-      this.form.appendChild(br);
-    }
-
-    okButton = document.createElement("input");
-    okButton.type = "submit";
-    okButton.value = this.options.okText;
-    this.form.appendChild(okButton);
-
-    cancelLink = document.createElement("a");
-    cancelLink.href = "#";
-    cancelLink.appendChild(document.createTextNode(this.options.cancelText));
-    cancelLink.onclick = this.onclickCancel.bind(this);
-    this.form.appendChild(cancelLink);
-  },
-  hasHTMLLineBreaks: function(string) {
-    if (!this.options.handleLineBreaks) return false;
-    return string.match(/<br/i) || string.match(/<p>/i);
-  },
-  convertHTMLLineBreaks: function(string) {
-    return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
-  },
-  createEditField: function() {
-    var text;
-    if(this.options.loadTextURL) {
-      text = this.options.loadingText;
-    } else {
-      text = this.getText();
-    }
-    
-    if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
-      this.options.textarea = false;
-      var textField = document.createElement("input");
-      textField.type = "text";
-      textField.name = "value";
-      textField.value = text;
-      textField.style.backgroundColor = this.options.highlightcolor;
-      var size = this.options.size || this.options.cols || 0;
-      if (size != 0) textField.size = size;
-      this.editField = textField;
-    } else {
-      this.options.textarea = true;
-      var textArea = document.createElement("textarea");
-      textArea.name = "value";
-      textArea.value = this.convertHTMLLineBreaks(text);
-      textArea.rows = this.options.rows;
-      textArea.cols = this.options.cols || 40;
-      this.editField = textArea;
-    }
-    
-    if(this.options.loadTextURL) {
-      this.loadExternalText();
-    }
-    this.form.appendChild(this.editField);
-  },
-  getText: function() {
-    return this.element.innerHTML;
-  },
-  loadExternalText: function() {
-    Element.addClassName(this.form, this.options.loadingClassName);
-    this.editField.disabled = true;
-    new Ajax.Request(
-      this.options.loadTextURL,
-      Object.extend({
-        asynchronous: true,
-        onComplete: this.onLoadedExternalText.bind(this)
-      }, this.options.ajaxOptions)
-    );
-  },
-  onLoadedExternalText: function(transport) {
-    Element.removeClassName(this.form, this.options.loadingClassName);
-    this.editField.disabled = false;
-    this.editField.value = transport.responseText.stripTags();
-  },
-  onclickCancel: function() {
-    this.onComplete();
-    this.leaveEditMode();
-    return false;
-  },
-  onFailure: function(transport) {
-    this.options.onFailure(transport);
-    if (this.oldInnerHTML) {
-      this.element.innerHTML = this.oldInnerHTML;
-      this.oldInnerHTML = null;
-    }
-    return false;
-  },
-  onSubmit: function() {
-    // onLoading resets these so we need to save them away for the Ajax call
-    var form = this.form;
-    var value = this.editField.value;
-    
-    // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
-    // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
-    // to be displayed indefinitely
-    this.onLoading();
-    
-    new Ajax.Updater(
-      { 
-        success: this.element,
-         // don't update on failure (this could be an option)
-        failure: null
-      },
-      this.url,
-      Object.extend({
-        parameters: this.options.callback(form, value),
-        onComplete: this.onComplete.bind(this),
-        onFailure: this.onFailure.bind(this)
-      }, this.options.ajaxOptions)
-    );
-    // stop the event to avoid a page refresh in Safari
-    if (arguments.length > 1) {
-      Event.stop(arguments[0]);
-    }
-    return false;
-  },
-  onLoading: function() {
-    this.saving = true;
-    this.removeForm();
-    this.leaveHover();
-    this.showSaving();
-  },
-  showSaving: function() {
-    this.oldInnerHTML = this.element.innerHTML;
-    this.element.innerHTML = this.options.savingText;
-    Element.addClassName(this.element, this.options.savingClassName);
-    this.element.style.backgroundColor = this.originalBackground;
-    Element.show(this.element);
-  },
-  removeForm: function() {
-    if(this.form) {
-      if (this.form.parentNode) Element.remove(this.form);
-      this.form = null;
-    }
-  },
-  enterHover: function() {
-    if (this.saving) return;
-    this.element.style.backgroundColor = this.options.highlightcolor;
-    if (this.effect) {
-      this.effect.cancel();
-    }
-    Element.addClassName(this.element, this.options.hoverClassName)
-  },
-  leaveHover: function() {
-    if (this.options.backgroundColor) {
-      this.element.style.backgroundColor = this.oldBackground;
-    }
-    Element.removeClassName(this.element, this.options.hoverClassName)
-    if (this.saving) return;
-    this.effect = new Effect.Highlight(this.element, {
-      startcolor: this.options.highlightcolor,
-      endcolor: this.options.highlightendcolor,
-      restorecolor: this.originalBackground
-    });
-  },
-  leaveEditMode: function() {
-    Element.removeClassName(this.element, this.options.savingClassName);
-    this.removeForm();
-    this.leaveHover();
-    this.element.style.backgroundColor = this.originalBackground;
-    Element.show(this.element);
-    if (this.options.externalControl) {
-      Element.show(this.options.externalControl);
-    }
-    this.editing = false;
-    this.saving = false;
-    this.oldInnerHTML = null;
-    this.onLeaveEditMode();
-  },
-  onComplete: function(transport) {
-    this.leaveEditMode();
-    this.options.onComplete.bind(this)(transport, this.element);
-  },
-  onEnterEditMode: function() {},
-  onLeaveEditMode: function() {},
-  dispose: function() {
-    if (this.oldInnerHTML) {
-      this.element.innerHTML = this.oldInnerHTML;
-    }
-    this.leaveEditMode();
-    Event.stopObserving(this.element, 'click', this.onclickListener);
-    Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
-    Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
-    if (this.options.externalControl) {
-      Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
-      Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
-      Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
-    }
-  }
-};
diff --git a/web/static/js/scriptaculous/dragdrop.js b/web/static/js/scriptaculous/dragdrop.js
deleted file mode 100644
index 7ca95f6..0000000
--- a/web/static/js/scriptaculous/dragdrop.js
+++ /dev/null
@@ -1,519 +0,0 @@
-// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
-// 
-// Element.Class part Copyright (c) 2005 by Rick Olson
-// 
-// See scriptaculous.js for full license.
-
-/*--------------------------------------------------------------------------*/
-
-var Droppables = {
-  drops: [],
-
-  remove: function(element) {
-    this.drops = this.drops.reject(function(d) { return d.element==element });
-  },
-
-  add: function(element) {
-    element = $(element);
-    var options = Object.extend({
-      greedy:     true,
-      hoverclass: null  
-    }, arguments[1] || {});
-
-    // cache containers
-    if(options.containment) {
-      options._containers = [];
-      var containment = options.containment;
-      if((typeof containment == 'object') && 
-        (containment.constructor == Array)) {
-        containment.each( function(c) { options._containers.push($(c)) });
-      } else {
-        options._containers.push($(containment));
-      }
-    }
-
-    Element.makePositioned(element); // fix IE
-    options.element = element;
-
-    this.drops.push(options);
-  },
-
-  isContained: function(element, drop) {
-    var parentNode = element.parentNode;
-    return drop._containers.detect(function(c) { return parentNode == c });
-  },
-
-  isAffected: function(pX, pY, element, drop) {
-    return (
-      (drop.element!=element) &&
-      ((!drop._containers) ||
-        this.isContained(element, drop)) &&
-      ((!drop.accept) ||
-        (Element.Class.has_any(element, drop.accept))) &&
-      Position.within(drop.element, pX, pY) );
-  },
-
-  deactivate: function(drop) {
-    if(drop.hoverclass)
-      Element.Class.remove(drop.element, drop.hoverclass);
-    this.last_active = null;
-  },
-
-  activate: function(drop) {
-    if(this.last_active) this.deactivate(this.last_active);
-    if(drop.hoverclass)
-      Element.Class.add(drop.element, drop.hoverclass);
-    this.last_active = drop;
-  },
-
-  show: function(event, element) {
-    if(!this.drops.length) return;
-    var pX = Event.pointerX(event);
-    var pY = Event.pointerY(event);
-    Position.prepare();
-
-    var i = this.drops.length-1; do {
-      var drop = this.drops[i];
-      if(this.isAffected(pX, pY, element, drop)) {
-        if(drop.onHover)
-           drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
-        if(drop.greedy) { 
-          this.activate(drop);
-          return;
-        }
-      }
-    } while (i--);
-    
-    if(this.last_active) this.deactivate(this.last_active);
-  },
-
-  fire: function(event, element) {
-    if(!this.last_active) return;
-    Position.prepare();
-
-    if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active))
-      if (this.last_active.onDrop) 
-        this.last_active.onDrop(element, this.last_active.element, event);
-  },
-
-  reset: function() {
-    if(this.last_active)
-      this.deactivate(this.last_active);
-  }
-}
-
-var Draggables = {
-  observers: [],
-  addObserver: function(observer) {
-    this.observers.push(observer);    
-  },
-  removeObserver: function(element) {  // element instead of obsever fixes mem leaks
-    this.observers = this.observers.reject( function(o) { return o.element==element });
-  },
-  notify: function(eventName, draggable) {  // 'onStart', 'onEnd'
-    this.observers.invoke(eventName, draggable);
-  }
-}
-
-/*--------------------------------------------------------------------------*/
-
-var Draggable = Class.create();
-Draggable.prototype = {
-  initialize: function(element) {
-    var options = Object.extend({
-      handle: false,
-      starteffect: function(element) { 
-        new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); 
-      },
-      reverteffect: function(element, top_offset, left_offset) {
-        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
-        new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur});
-      },
-      endeffect: function(element) { 
-         new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); 
-      },
-      zindex: 1000,
-      revert: false
-    }, arguments[1] || {});
-
-    this.element      = $(element);
-    if(options.handle && (typeof options.handle == 'string'))
-      this.handle = Element.Class.childrenWith(this.element, options.handle)[0];
-      
-    if(!this.handle) this.handle = $(options.handle);
-    if(!this.handle) this.handle = this.element;
-
-    Element.makePositioned(this.element); // fix IE    
-
-    this.offsetX      = 0;
-    this.offsetY      = 0;
-    this.originalLeft = this.currentLeft();
-    this.originalTop  = this.currentTop();
-    this.originalX    = this.element.offsetLeft;
-    this.originalY    = this.element.offsetTop;
-
-    this.options      = options;
-
-    this.active       = false;
-    this.dragging     = false;   
-
-    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
-    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
-    this.eventMouseMove = this.update.bindAsEventListener(this);
-    this.eventKeypress  = this.keyPress.bindAsEventListener(this);
-    
-    this.registerEvents();
-  },
-  destroy: function() {
-    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
-    this.unregisterEvents();
-  },
-  registerEvents: function() {
-    Event.observe(document, "mouseup", this.eventMouseUp);
-    Event.observe(document, "mousemove", this.eventMouseMove);
-    Event.observe(document, "keypress", this.eventKeypress);
-    Event.observe(this.handle, "mousedown", this.eventMouseDown);
-  },
-  unregisterEvents: function() {
-    //if(!this.active) return;
-    //Event.stopObserving(document, "mouseup", this.eventMouseUp);
-    //Event.stopObserving(document, "mousemove", this.eventMouseMove);
-    //Event.stopObserving(document, "keypress", this.eventKeypress);
-  },
-  currentLeft: function() {
-    return parseInt(this.element.style.left || '0');
-  },
-  currentTop: function() {
-    return parseInt(this.element.style.top || '0')
-  },
-  startDrag: function(event) {
-    if(Event.isLeftClick(event)) {
-      
-      // abort on form elements, fixes a Firefox issue
-      var src = Event.element(event);
-      if(src.tagName && (
-        src.tagName=='INPUT' ||
-        src.tagName=='SELECT' ||
-        src.tagName=='BUTTON' ||
-        src.tagName=='TEXTAREA')) return;
-      
-      // this.registerEvents();
-      this.active = true;
-      var pointer = [Event.pointerX(event), Event.pointerY(event)];
-      var offsets = Position.cumulativeOffset(this.element);
-      this.offsetX =  (pointer[0] - offsets[0]);
-      this.offsetY =  (pointer[1] - offsets[1]);
-      Event.stop(event);
-    }
-  },
-  finishDrag: function(event, success) {
-    // this.unregisterEvents();
-
-    this.active = false;
-    this.dragging = false;
-
-    if(this.options.ghosting) {
-      Position.relativize(this.element);
-      Element.remove(this._clone);
-      this._clone = null;
-    }
-
-    if(success) Droppables.fire(event, this.element);
-    Draggables.notify('onEnd', this);
-
-    var revert = this.options.revert;
-    if(revert && typeof revert == 'function') revert = revert(this.element);
-
-    if(revert && this.options.reverteffect) {
-      this.options.reverteffect(this.element, 
-      this.currentTop()-this.originalTop,
-      this.currentLeft()-this.originalLeft);
-    } else {
-      this.originalLeft = this.currentLeft();
-      this.originalTop  = this.currentTop();
-    }
-
-    if(this.options.zindex)
-      this.element.style.zIndex = this.originalZ;
-
-    if(this.options.endeffect) 
-      this.options.endeffect(this.element);
-
-
-    Droppables.reset();
-  },
-  keyPress: function(event) {
-    if(this.active) {
-      if(event.keyCode==Event.KEY_ESC) {
-        this.finishDrag(event, false);
-        Event.stop(event);
-      }
-    }
-  },
-  endDrag: function(event) {
-    if(this.active && this.dragging) {
-      this.finishDrag(event, true);
-      Event.stop(event);
-    }
-    this.active = false;
-    this.dragging = false;
-  },
-  draw: function(event) {
-    var pointer = [Event.pointerX(event), Event.pointerY(event)];
-    var offsets = Position.cumulativeOffset(this.element);
-    offsets[0] -= this.currentLeft();
-    offsets[1] -= this.currentTop();
-    var style = this.element.style;
-    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
-      style.left = (pointer[0] - offsets[0] - this.offsetX) + "px";
-    if((!this.options.constraint) || (this.options.constraint=='vertical'))
-      style.top  = (pointer[1] - offsets[1] - this.offsetY) + "px";
-    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
-  },
-  update: function(event) {
-   if(this.active) {
-      if(!this.dragging) {
-        var style = this.element.style;
-        this.dragging = true;
-        
-        if(Element.getStyle(this.element,'position')=='') 
-          style.position = "relative";
-        
-        if(this.options.zindex) {
-          this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
-          style.zIndex = this.options.zindex;
-        }
-
-        if(this.options.ghosting) {
-          this._clone = this.element.cloneNode(true);
-          Position.absolutize(this.element);
-          this.element.parentNode.insertBefore(this._clone, this.element);
-        }
-
-        Draggables.notify('onStart', this);
-        if(this.options.starteffect) this.options.starteffect(this.element);
-      }
-
-      Droppables.show(event, this.element);
-      this.draw(event);
-      if(this.options.change) this.options.change(this);
-
-      // fix AppleWebKit rendering
-      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); 
-
-      Event.stop(event);
-   }
-  }
-}
-
-/*--------------------------------------------------------------------------*/
-
-var SortableObserver = Class.create();
-SortableObserver.prototype = {
-  initialize: function(element, observer) {
-    this.element   = $(element);
-    this.observer  = observer;
-    this.lastValue = Sortable.serialize(this.element);
-  },
-  onStart: function() {
-    this.lastValue = Sortable.serialize(this.element);
-  },
-  onEnd: function() {
-    Sortable.unmark();
-    if(this.lastValue != Sortable.serialize(this.element))
-      this.observer(this.element)
-  }
-}
-
-var Sortable = {
-  sortables: new Array(),
-  options: function(element){
-    element = $(element);
-    return this.sortables.detect(function(s) { return s.element == element });
-  },
-  destroy: function(element){
-    element = $(element);
-    this.sortables.findAll(function(s) { return s.element == element }).each(function(s){
-      Draggables.removeObserver(s.element);
-      s.droppables.each(function(d){ Droppables.remove(d) });
-      s.draggables.invoke('destroy');
-    });
-    this.sortables = this.sortables.reject(function(s) { return s.element == element });
-  },
-  create: function(element) {
-    element = $(element);
-    var options = Object.extend({ 
-      element:     element,
-      tag:         'li',       // assumes li children, override with tag: 'tagname'
-      dropOnEmpty: false,
-      tree:        false,      // fixme: unimplemented
-      overlap:     'vertical', // one of 'vertical', 'horizontal'
-      constraint:  'vertical', // one of 'vertical', 'horizontal', false
-      containment: element,    // also takes array of elements (or id's); or false
-      handle:      false,      // or a CSS class
-      only:        false,
-      hoverclass:  null,
-      ghosting:    false,
-      format:      null,
-      onChange:    Prototype.emptyFunction,
-      onUpdate:    Prototype.emptyFunction
-    }, arguments[1] || {});
-
-    // clear any old sortable with same element
-    this.destroy(element);
-
-    // build options for the draggables
-    var options_for_draggable = {
-      revert:      true,
-      ghosting:    options.ghosting,
-      constraint:  options.constraint,
-      handle:      options.handle };
-
-    if(options.starteffect)
-      options_for_draggable.starteffect = options.starteffect;
-
-    if(options.reverteffect)
-      options_for_draggable.reverteffect = options.reverteffect;
-    else
-      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
-        element.style.top  = 0;
-        element.style.left = 0;
-      };
-
-    if(options.endeffect)
-      options_for_draggable.endeffect = options.endeffect;
-
-    if(options.zindex)
-      options_for_draggable.zindex = options.zindex;
-
-    // build options for the droppables  
-    var options_for_droppable = {
-      overlap:     options.overlap,
-      containment: options.containment,
-      hoverclass:  options.hoverclass,
-      onHover:     Sortable.onHover,
-      greedy:      !options.dropOnEmpty
-    }
-
-    // fix for gecko engine
-    Element.cleanWhitespace(element); 
-
-    options.draggables = [];
-    options.droppables = [];
-
-    // make it so
-
-    // drop on empty handling
-    if(options.dropOnEmpty) {
-      Droppables.add(element,
-        {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false});
-      options.droppables.push(element);
-    }
-
-    (this.findElements(element, options) || []).each( function(e) {
-      // handles are per-draggable
-      var handle = options.handle ? 
-        Element.Class.childrenWith(e, options.handle)[0] : e;    
-      options.draggables.push(
-        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
-      Droppables.add(e, options_for_droppable);
-      options.droppables.push(e);      
-    });
-
-    // keep reference
-    this.sortables.push(options);
-
-    // for onupdate
-    Draggables.addObserver(new SortableObserver(element, options.onUpdate));
-
-  },
-
-  // return all suitable-for-sortable elements in a guaranteed order
-  findElements: function(element, options) {
-    if(!element.hasChildNodes()) return null;
-    var elements = [];
-    $A(element.childNodes).each( function(e) {
-      if(e.tagName && e.tagName==options.tag.toUpperCase() &&
-        (!options.only || (Element.Class.has(e, options.only))))
-          elements.push(e);
-      if(options.tree) {
-        var grandchildren = this.findElements(e, options);
-        if(grandchildren) elements.push(grandchildren);
-      }
-    });
-
-    return (elements.length>0 ? elements.flatten() : null);
-  },
-
-  onHover: function(element, dropon, overlap) {
-    if(overlap>0.5) {
-      Sortable.mark(dropon, 'before');
-      if(dropon.previousSibling != element) {
-        var oldParentNode = element.parentNode;
-        element.style.visibility = "hidden"; // fix gecko rendering
-        dropon.parentNode.insertBefore(element, dropon);
-        if(dropon.parentNode!=oldParentNode) 
-          Sortable.options(oldParentNode).onChange(element);
-        Sortable.options(dropon.parentNode).onChange(element);
-      }
-    } else {
-      Sortable.mark(dropon, 'after');
-      var nextElement = dropon.nextSibling || null;
-      if(nextElement != element) {
-        var oldParentNode = element.parentNode;
-        element.style.visibility = "hidden"; // fix gecko rendering
-        dropon.parentNode.insertBefore(element, nextElement);
-        if(dropon.parentNode!=oldParentNode) 
-          Sortable.options(oldParentNode).onChange(element);
-        Sortable.options(dropon.parentNode).onChange(element);
-      }
-    }
-  },
-
-  onEmptyHover: function(element, dropon) {
-    if(element.parentNode!=dropon) {
-      var oldParentNode = element.parentNode;
-      dropon.appendChild(element);
-      Sortable.options(oldParentNode).onChange(element);
-      Sortable.options(dropon).onChange(element);
-    }
-  },
-
-  unmark: function() {
-    if(Sortable._marker) Element.hide(Sortable._marker);
-  },
-
-  mark: function(dropon, position) {
-    // mark on ghosting only
-    var sortable = Sortable.options(dropon.parentNode);
-    if(sortable && !sortable.ghosting) return; 
-
-    if(!Sortable._marker) {
-      Sortable._marker = $('dropmarker') || document.createElement('DIV');
-      Element.hide(Sortable._marker);
-      Element.Class.add(Sortable._marker, 'dropmarker');
-      Sortable._marker.style.position = 'absolute';
-      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
-    }    
-    var offsets = Position.cumulativeOffset(dropon);
-    Sortable._marker.style.top  = offsets[1] + 'px';
-    if(position=='after') Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
-    Sortable._marker.style.left = offsets[0] + 'px';
-    Element.show(Sortable._marker);
-  },
-
-  serialize: function(element) {
-    element = $(element);
-    var sortableOptions = this.options(element);
-    var options = Object.extend({
-      tag:  sortableOptions.tag,
-      only: sortableOptions.only,
-      name: element.id,
-      format: sortableOptions.format || /^[^_]*_(.*)$/
-    }, arguments[1] || {});
-    return $(this.findElements(element, options) || []).collect( function(item) {
-      return (encodeURIComponent(options.name) + "[]=" + 
-              encodeURIComponent(item.id.match(options.format) ? item.id.match(options.format)[1] : ''));
-    }).join("&");
-  }
-} 
\ No newline at end of file
diff --git a/web/static/js/scriptaculous/effects.js b/web/static/js/scriptaculous/effects.js
deleted file mode 100644
index 3f92992..0000000
--- a/web/static/js/scriptaculous/effects.js
+++ /dev/null
@@ -1,992 +0,0 @@
-// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
-// Contributors:
-//  Justin Palmer (http://encytemedia.com/)
-//  Mark Pilgrim (http://diveintomark.org/)
-//  Martin Bialasinki
-// 
-// See scriptaculous.js for full license.  
-
-/* ------------- element ext -------------- */  
- 
-// converts rgb() and #xxx to #xxxxxx format,  
-// returns self (or first argument) if not convertable  
-String.prototype.parseColor = function() {  
-  color = "#";  
-  if(this.slice(0,4) == "rgb(") {  
-    var cols = this.slice(4,this.length-1).split(',');  
-    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);  
-  } else {  
-    if(this.slice(0,1) == '#') {  
-      if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();  
-      if(this.length==7) color = this.toLowerCase();  
-    }  
-  }  
-  return(color.length==7 ? color : (arguments[0] || this));  
-}  
-
-Element.collectTextNodesIgnoreClass = function(element, ignoreclass) {  
-  var children = $(element).childNodes;  
-  var text     = "";  
-  var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i");  
- 
-  for (var i = 0; i < children.length; i++) {  
-    if(children[i].nodeType==3) {  
-      text+=children[i].nodeValue;  
-    } else {  
-      if((!children[i].className.match(classtest)) && children[i].hasChildNodes())  
-        text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass);  
-    }  
-  }  
- 
-  return text;
-}
-
-Element.setContentZoom = function(element, percent) {  
-  element = $(element);  
-  element.style.fontSize = (percent/100) + "em";   
-  if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);  
-}
-
-Element.getOpacity = function(element){  
-  var opacity;  
-  if (opacity = Element.getStyle(element, "opacity"))  
-    return parseFloat(opacity);  
-  if (opacity = (Element.getStyle(element, "filter") || '').match(/alpha\(opacity=(.*)\)/))  
-    if(opacity[1]) return parseFloat(opacity[1]) / 100;  
-  return 1.0;  
-}
-
-Element.setOpacity = function(element, value){  
-  element= $(element);  
-  var els = element.style;  
-  if (value == 1){  
-    els.opacity = '0.999999';  
-    if(/MSIE/.test(navigator.userAgent))  
-      els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'');  
-  } else {  
-    if(value < 0.00001) value = 0;  
-    els.opacity = value;  
-    if(/MSIE/.test(navigator.userAgent))  
-      els.filter = Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +  
-        "alpha(opacity="+value*100+")";  
-  }   
-}  
- 
-Element.getInlineOpacity = function(element){  
-  element= $(element);  
-  var op;  
-  op = element.style.opacity;  
-  if (typeof op != "undefined" && op != "") return op;  
-  return "";  
-}  
- 
-Element.setInlineOpacity = function(element, value){  
-  element= $(element);  
-  var els = element.style;  
-  els.opacity = value;  
-}  
- 
-/*--------------------------------------------------------------------------*/  
- 
-Element.Class = {  
-    // Element.toggleClass(element, className) toggles the class being on/off  
-    // Element.toggleClass(element, className1, className2) toggles between both classes,  
-    //   defaulting to className1 if neither exist  
-    toggle: function(element, className) {  
-      if(Element.Class.has(element, className)) {  
-        Element.Class.remove(element, className);  
-        if(arguments.length == 3) Element.Class.add(element, arguments[2]);  
-      } else {  
-        Element.Class.add(element, className);  
-        if(arguments.length == 3) Element.Class.remove(element, arguments[2]);  
-      }  
-    },  
- 
-    // gets space-delimited classnames of an element as an array  
-    get: function(element) {  
-      return $(element).className.split(' ');  
-    },  
- 
-    // functions adapted from original functions by Gavin Kistner  
-    remove: function(element) {  
-      element = $(element);  
-      var removeClasses = arguments;  
-      $R(1,arguments.length-1).each( function(index) {  
-        element.className =  
-          element.className.split(' ').reject(  
-            function(klass) { return (klass == removeClasses[index]) } ).join(' ');  
-      });  
-    },  
- 
-    add: function(element) {  
-      element = $(element);  
-      for(var i = 1; i < arguments.length; i++) {  
-        Element.Class.remove(element, arguments[i]);  
-        element.className += (element.className.length > 0 ? ' ' : '') + arguments[i];  
-      }  
-    },  
- 
-    // returns true if all given classes exist in said element  
-    has: function(element) {  
-      element = $(element);  
-      if(!element || !element.className) return false;  
-      var regEx;  
-      for(var i = 1; i < arguments.length; i++) {  
-        if((typeof arguments[i] == 'object') &&  
-          (arguments[i].constructor == Array)) {  
-          for(var j = 0; j < arguments[i].length; j++) {  
-            regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)");  
-            if(!regEx.test(element.className)) return false;  
-          }  
-        } else {  
-          regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)");  
-          if(!regEx.test(element.className)) return false;  
-        }  
-      }  
-      return true;  
-    },  
- 
-    // expects arrays of strings and/or strings as optional paramters  
-    // Element.Class.has_any(element, ['classA','classB','classC'], 'classD')  
-    has_any: function(element) {  
-      element = $(element);  
-      if(!element || !element.className) return false;  
-      var regEx;  
-      for(var i = 1; i < arguments.length; i++) {  
-        if((typeof arguments[i] == 'object') &&  
-          (arguments[i].constructor == Array)) {  
-          for(var j = 0; j < arguments[i].length; j++) {  
-            regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)");  
-            if(regEx.test(element.className)) return true;  
-          }  
-        } else {  
-          regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)");  
-          if(regEx.test(element.className)) return true;  
-        }  
-      }  
-      return false;  
-    },  
- 
-    childrenWith: function(element, className) {  
-      var children = $(element).getElementsByTagName('*');  
-      var elements = new Array();  
- 
-      for (var i = 0; i < children.length; i++)  
-        if (Element.Class.has(children[i], className))  
-          elements.push(children[i]);  
- 
-      return elements;  
-    }  
-}  
- 
-/*--------------------------------------------------------------------------*/
-
-var Effect = {
-  tagifyText: function(element) {
-    var tagifyStyle = "position:relative";
-    if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ";zoom:1";
-    element = $(element);
-    $A(element.childNodes).each( function(child) {
-      if(child.nodeType==3) {
-        child.nodeValue.toArray().each( function(character) {
-          element.insertBefore(
-            Builder.node('span',{style: tagifyStyle},
-              character == " " ? String.fromCharCode(160) : character), 
-              child);
-        });
-        Element.remove(child);
-      }
-    });
-  },
-  multiple: function(element, effect) {
-    var elements;
-    if(((typeof element == 'object') || 
-        (typeof element == 'function')) && 
-       (element.length))
-      elements = element;
-    else
-      elements = $(element).childNodes;
-      
-    var options = Object.extend({
-      speed: 0.1,
-      delay: 0.0
-    }, arguments[2] || {});
-    var speed = options.speed;
-    var delay = options.delay;
-
-    $A(elements).each( function(element, index) {
-      new effect(element, Object.extend(options, { delay: delay + index * speed }));
-    });
-  }
-};
-
-var Effect2 = Effect; // deprecated
-
-/* ------------- transitions ------------- */
-
-Effect.Transitions = {}
-
-Effect.Transitions.linear = function(pos) {
-  return pos;
-}
-Effect.Transitions.sinoidal = function(pos) {
-  return (-Math.cos(pos*Math.PI)/2) + 0.5;
-}
-Effect.Transitions.reverse  = function(pos) {
-  return 1-pos;
-}
-Effect.Transitions.flicker = function(pos) {
-  return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
-}
-Effect.Transitions.wobble = function(pos) {
-  return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
-}
-Effect.Transitions.pulse = function(pos) {
-  return (Math.floor(pos*10) % 2 == 0 ? 
-    (pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
-}
-Effect.Transitions.none = function(pos) {
-  return 0;
-}
-Effect.Transitions.full = function(pos) {
-  return 1;
-}
-
-/* ------------- core effects ------------- */
-
-Effect.Queue = {
-  effects:  [],
-  _each: function(iterator) {
-    this.effects._each(iterator);
-  },
-  interval: null,
-  add: function(effect) {
-    var timestamp = new Date().getTime();
-    
-    switch(effect.options.queue) {
-      case 'front':
-        // move unstarted effects after this effect  
-        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
-            e.startOn  += effect.finishOn;
-            e.finishOn += effect.finishOn;
-          });
-        break;
-      case 'end':
-        // start effect after last queued effect has finished
-        timestamp = this.effects.pluck('finishOn').max() || timestamp;
-        break;
-    }
-    
-    effect.startOn  += timestamp;
-    effect.finishOn += timestamp;
-    this.effects.push(effect);
-    if(!this.interval) 
-      this.interval = setInterval(this.loop.bind(this), 40);
-  },
-  remove: function(effect) {
-    this.effects = this.effects.reject(function(e) { return e==effect });
-    if(this.effects.length == 0) {
-      clearInterval(this.interval);
-      this.interval = null;
-    }
-  },
-  loop: function() {
-    var timePos = new Date().getTime();
-    this.effects.invoke('loop', timePos);
-  }
-}
-Object.extend(Effect.Queue, Enumerable);
-
-Effect.Base = function() {};
-Effect.Base.prototype = {
-  position: null,
-  setOptions: function(options) {
-    this.options = Object.extend({
-      transition: Effect.Transitions.sinoidal,
-      duration:   1.0,   // seconds
-      fps:        25.0,  // max. 25fps due to Effect.Queue implementation
-      sync:       false, // true for combining
-      from:       0.0,
-      to:         1.0,
-      delay:      0.0,
-      queue:      'parallel'
-    }, options || {});
-  },
-  start: function(options) {
-    this.setOptions(options || {});
-    this.currentFrame = 0;
-    this.state        = 'idle';
-    this.startOn      = this.options.delay*1000;
-    this.finishOn     = this.startOn + (this.options.duration*1000);
-    this.event('beforeStart');
-    if(!this.options.sync) Effect.Queue.add(this);
-  },
-  loop: function(timePos) {
-    if(timePos >= this.startOn) {
-      if(timePos >= this.finishOn) {
-        this.render(1.0);
-        this.cancel();
-        this.event('beforeFinish');
-        if(this.finish) this.finish(); 
-        this.event('afterFinish');
-        return;  
-      }
-      var pos   = (timePos - this.startOn) / (this.finishOn - this.startOn);
-      var frame = Math.round(pos * this.options.fps * this.options.duration);
-      if(frame > this.currentFrame) {
-        this.render(pos);
-        this.currentFrame = frame;
-      }
-    }
-  },
-  render: function(pos) {
-    if(this.state == 'idle') {
-      this.state = 'running';
-      this.event('beforeSetup');
-      if(this.setup) this.setup();
-      this.event('afterSetup');
-    }
-    if(this.options.transition) pos = this.options.transition(pos);
-    pos *= (this.options.to-this.options.from);
-    pos += this.options.from;
-    this.position = pos;
-    this.event('beforeUpdate');
-    if(this.update) this.update(pos);
-    this.event('afterUpdate');
-  },
-  cancel: function() {
-    if(!this.options.sync) Effect.Queue.remove(this);
-    this.state = 'finished';
-  },
-  event: function(eventName) {
-    if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
-    if(this.options[eventName]) this.options[eventName](this);
-  }
-}
-
-Effect.Parallel = Class.create();
-Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
-  initialize: function(effects) {
-    this.effects = effects || [];
-    this.start(arguments[1]);
-  },
-  update: function(position) {
-    this.effects.invoke('render', position);
-  },
-  finish: function(position) {
-    this.effects.each( function(effect) {
-      effect.render(1.0);
-      effect.cancel();
-      effect.event('beforeFinish');
-      if(effect.finish) effect.finish(position);
-      effect.event('afterFinish');
-    });
-  }
-});
-
-Effect.Opacity = Class.create();
-Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
-  initialize: function(element) {
-    this.element = $(element);
-    // make this work on IE on elements without 'layout'
-    if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout))
-      this.element.style.zoom = 1;
-    var options = Object.extend({
-      from: Element.getOpacity(this.element) || 0.0,
-      to:   1.0
-    }, arguments[1] || {});
-    this.start(options);
-  },
-  update: function(position) {
-    Element.setOpacity(this.element, position);
-  }
-});
-
-Effect.MoveBy = Class.create();
-Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), {
-  initialize: function(element, toTop, toLeft) {
-    this.element      = $(element);
-    this.toTop        = toTop;
-    this.toLeft       = toLeft;
-    this.start(arguments[3]);
-  },
-  setup: function() {
-    // Bug in Opera: Opera returns the "real" position of a static element or
-    // relative element that does not have top/left explicitly set.
-    // ==> Always set top and left for position relative elements in your stylesheets 
-    // (to 0 if you do not need them)
-    
-    Element.makePositioned(this.element);
-    this.originalTop  = parseFloat(Element.getStyle(this.element,'top')  || '0');
-    this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0');
-  },
-  update: function(position) {
-    var topd  = this.toTop  * position + this.originalTop;
-    var leftd = this.toLeft * position + this.originalLeft;
-    this.setPosition(topd, leftd);
-  },
-  setPosition: function(topd, leftd) {
-    this.element.style.top  = topd  + "px";
-    this.element.style.left = leftd + "px";
-  }
-});
-
-Effect.Scale = Class.create();
-Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
-  initialize: function(element, percent) {
-    this.element = $(element)
-    var options = Object.extend({
-      scaleX: true,
-      scaleY: true,
-      scaleContent: true,
-      scaleFromCenter: false,
-      scaleMode: 'box',        // 'box' or 'contents' or {} with provided values
-      scaleFrom: 100.0,
-      scaleTo:   percent
-    }, arguments[2] || {});
-    this.start(options);
-  },
-  setup: function() {
-    var effect = this;
-    
-    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
-    this.elementPositioning = Element.getStyle(this.element,'position');
-    
-    effect.originalStyle = {};
-    ['top','left','width','height','fontSize'].each( function(k) {
-      effect.originalStyle[k] = effect.element.style[k];
-    });
-      
-    this.originalTop  = this.element.offsetTop;
-    this.originalLeft = this.element.offsetLeft;
-    
-    var fontSize = Element.getStyle(this.element,'font-size') || "100%";
-    ['em','px','%'].each( function(fontSizeType) {
-      if(fontSize.indexOf(fontSizeType)>0) {
-        effect.fontSize     = parseFloat(fontSize);
-        effect.fontSizeType = fontSizeType;
-      }
-    });
-    
-    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
-    
-    this.dims = null;
-    if(this.options.scaleMode=='box')
-      this.dims = [this.element.clientHeight, this.element.clientWidth];
-    if(this.options.scaleMode=='content')
-      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
-    if(!this.dims)
-      this.dims = [this.options.scaleMode.originalHeight,
-                   this.options.scaleMode.originalWidth];
-  },
-  update: function(position) {
-    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
-    if(this.options.scaleContent && this.fontSize)
-      this.element.style.fontSize = this.fontSize*currentScale + this.fontSizeType;
-    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
-  },
-  finish: function(position) {
-    if (this.restoreAfterFinish) {
-      var effect = this;
-      ['top','left','width','height','fontSize'].each( function(k) {
-        effect.element.style[k] = effect.originalStyle[k];
-      });
-    }
-  },
-  setDimensions: function(height, width) {
-    var els = this.element.style;
-    if(this.options.scaleX) els.width = width + 'px';
-    if(this.options.scaleY) els.height = height + 'px';
-    if(this.options.scaleFromCenter) {
-      var topd  = (height - this.dims[0])/2;
-      var leftd = (width  - this.dims[1])/2;
-      if(this.elementPositioning == 'absolute') {
-        if(this.options.scaleY) els.top = this.originalTop-topd + "px";
-        if(this.options.scaleX) els.left = this.originalLeft-leftd + "px";
-      } else {
-        if(this.options.scaleY) els.top = -topd + "px";
-        if(this.options.scaleX) els.left = -leftd + "px";
-      }
-    }
-  }
-});
-
-Effect.Highlight = Class.create();
-Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
-  initialize: function(element) {
-    this.element = $(element);
-    var options = Object.extend({
-      startcolor:   "#ffff99"
-    }, arguments[1] || {});
-    this.start(options);
-  },
-  setup: function() {
-    // Prevent executing on elements not in the layout flow
-    if(this.element.style.display=='none') { this.cancel(); return; }
-    // Disable background image during the effect
-    this.oldBgImage = this.element.style.backgroundImage;
-    this.element.style.backgroundImage = "none";
-    if(!this.options.endcolor)
-      this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff');
-    if (typeof this.options.restorecolor == "undefined")
-      this.options.restorecolor = this.element.style.backgroundColor;
-    // init color calculations
-    this.colors_base = [
-      parseInt(this.options.startcolor.slice(1,3),16),
-      parseInt(this.options.startcolor.slice(3,5),16),
-      parseInt(this.options.startcolor.slice(5),16) ];
-    this.colors_delta = [
-      parseInt(this.options.endcolor.slice(1,3),16)-this.colors_base[0],
-      parseInt(this.options.endcolor.slice(3,5),16)-this.colors_base[1],
-      parseInt(this.options.endcolor.slice(5),16)-this.colors_base[2]];
-  },
-  update: function(position) {
-    var effect = this; var colors = $R(0,2).map( function(i){ 
-      return Math.round(effect.colors_base[i]+(effect.colors_delta[i]*position))
-    });
-    this.element.style.backgroundColor = "#" +
-      colors[0].toColorPart() + colors[1].toColorPart() + colors[2].toColorPart();
-  },
-  finish: function() {
-    this.element.style.backgroundColor = this.options.restorecolor;
-    this.element.style.backgroundImage = this.oldBgImage;
-  }
-});
-
-Effect.ScrollTo = Class.create();
-Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
-  initialize: function(element) {
-    this.element = $(element);
-    this.start(arguments[1] || {});
-  },
-  setup: function() {
-    Position.prepare();
-    var offsets = Position.cumulativeOffset(this.element);
-    var max = window.innerHeight ? 
-      window.height - window.innerHeight :
-      document.body.scrollHeight - 
-        (document.documentElement.clientHeight ? 
-          document.documentElement.clientHeight : document.body.clientHeight);
-    this.scrollStart = Position.deltaY;
-    this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
-  },
-  update: function(position) {
-    Position.prepare();
-    window.scrollTo(Position.deltaX, 
-      this.scrollStart + (position*this.delta));
-  }
-});
-
-/* ------------- combination effects ------------- */
-
-Effect.Fade = function(element) {
-  var oldOpacity = Element.getInlineOpacity(element);
-  var options = Object.extend({
-  from: Element.getOpacity(element) || 1.0,
-  to:   0.0,
-  afterFinishInternal: function(effect) 
-    { if (effect.options.to == 0) {
-        Element.hide(effect.element);
-        Element.setInlineOpacity(effect.element, oldOpacity);
-      }  
-    }
-  }, arguments[1] || {});
-  return new Effect.Opacity(element,options);
-}
-
-Effect.Appear = function(element) {
-  var options = Object.extend({
-  from: (Element.getStyle(element, "display") == "none" ? 0.0 : Element.getOpacity(element) || 0.0),
-  to:   1.0,
-  beforeSetup: function(effect)  
-    { Element.setOpacity(effect.element, effect.options.from);
-      Element.show(effect.element); }
-  }, arguments[1] || {});
-  return new Effect.Opacity(element,options);
-}
-
-Effect.Puff = function(element) {
-  element = $(element);
-  var oldOpacity = Element.getInlineOpacity(element);
-  var oldPosition = element.style.position;
-  return new Effect.Parallel(
-   [ new Effect.Scale(element, 200, 
-      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 
-     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 
-     Object.extend({ duration: 1.0, 
-      beforeSetupInternal: function(effect) 
-       { effect.effects[0].element.style.position = 'absolute'; },
-      afterFinishInternal: function(effect)
-       { Element.hide(effect.effects[0].element);
-         effect.effects[0].element.style.position = oldPosition;
-         Element.setInlineOpacity(effect.effects[0].element, oldOpacity); }
-     }, arguments[1] || {})
-   );
-}
-
-Effect.BlindUp = function(element) {
-  element = $(element);
-  Element.makeClipping(element);
-  return new Effect.Scale(element, 0, 
-    Object.extend({ scaleContent: false, 
-      scaleX: false, 
-      restoreAfterFinish: true,
-      afterFinishInternal: function(effect)
-        { 
-          Element.hide(effect.element);
-          Element.undoClipping(effect.element);
-        } 
-    }, arguments[1] || {})
-  );
-}
-
-Effect.BlindDown = function(element) {
-  element = $(element);
-  var oldHeight = element.style.height;
-  var elementDimensions = Element.getDimensions(element);
-  return new Effect.Scale(element, 100, 
-    Object.extend({ scaleContent: false, 
-      scaleX: false,
-      scaleFrom: 0,
-      scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
-      restoreAfterFinish: true,
-      afterSetup: function(effect) {
-        Element.makeClipping(effect.element);
-        effect.element.style.height = "0px";
-        Element.show(effect.element); 
-      },  
-      afterFinishInternal: function(effect) {
-        Element.undoClipping(effect.element);
-        effect.element.style.height = oldHeight;
-      }
-    }, arguments[1] || {})
-  );
-}
-
-Effect.SwitchOff = function(element) {
-  element = $(element);
-  var oldOpacity = Element.getInlineOpacity(element);
-  return new Effect.Appear(element, { 
-    duration: 0.4,
-    from: 0,
-    transition: Effect.Transitions.flicker,
-    afterFinishInternal: function(effect) {
-      new Effect.Scale(effect.element, 1, { 
-        duration: 0.3, scaleFromCenter: true,
-        scaleX: false, scaleContent: false, restoreAfterFinish: true,
-        beforeSetup: function(effect) { 
-          Element.makePositioned(effect.element); 
-          Element.makeClipping(effect.element);
-        },
-        afterFinishInternal: function(effect) { 
-          Element.hide(effect.element); 
-          Element.undoClipping(effect.element);
-          Element.undoPositioned(effect.element);
-          Element.setInlineOpacity(effect.element, oldOpacity);
-        }
-      })
-    }
-  });
-}
-
-Effect.DropOut = function(element) {
-  element = $(element);
-  var oldTop = element.style.top;
-  var oldLeft = element.style.left;
-  var oldOpacity = Element.getInlineOpacity(element);
-  return new Effect.Parallel(
-    [ new Effect.MoveBy(element, 100, 0, { sync: true }), 
-      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
-    Object.extend(
-      { duration: 0.5,
-        beforeSetup: function(effect) { 
-          Element.makePositioned(effect.effects[0].element); },
-        afterFinishInternal: function(effect) { 
-          Element.hide(effect.effects[0].element); 
-          Element.undoPositioned(effect.effects[0].element);
-          effect.effects[0].element.style.left = oldLeft;
-          effect.effects[0].element.style.top = oldTop;
-          Element.setInlineOpacity(effect.effects[0].element, oldOpacity); } 
-      }, arguments[1] || {}));
-}
-
-Effect.Shake = function(element) {
-  element = $(element);
-  var oldTop = element.style.top;
-  var oldLeft = element.style.left;
-  return new Effect.MoveBy(element, 0, 20, 
-    { duration: 0.05, afterFinishInternal: function(effect) {
-  new Effect.MoveBy(effect.element, 0, -40, 
-    { duration: 0.1, afterFinishInternal: function(effect) {
-  new Effect.MoveBy(effect.element, 0, 40, 
-    { duration: 0.1, afterFinishInternal: function(effect) {
-  new Effect.MoveBy(effect.element, 0, -40, 
-    { duration: 0.1, afterFinishInternal: function(effect) {
-  new Effect.MoveBy(effect.element, 0, 40, 
-    { duration: 0.1, afterFinishInternal: function(effect) {
-  new Effect.MoveBy(effect.element, 0, -20, 
-    { duration: 0.05, afterFinishInternal: function(effect) {
-        Element.undoPositioned(effect.element);
-        effect.element.style.left = oldLeft;
-        effect.element.style.top = oldTop;
-  }}) }}) }}) }}) }}) }});
-}
-
-Effect.SlideDown = function(element) {
-  element = $(element);
-  Element.cleanWhitespace(element);
-  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
-  var oldInnerBottom = element.firstChild.style.bottom;
-  var elementDimensions = Element.getDimensions(element);
-  return new Effect.Scale(element, 100, 
-   Object.extend({ scaleContent: false, 
-    scaleX: false, 
-    scaleFrom: 0,
-    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},    
-    restoreAfterFinish: true,
-    afterSetup: function(effect) {
-      Element.makePositioned(effect.element.firstChild);
-      if (window.opera) effect.element.firstChild.style.top = "";
-      Element.makeClipping(effect.element);
-      element.style.height = '0';
-      Element.show(element); 
-    },  
-    afterUpdateInternal: function(effect) { 
-      effect.element.firstChild.style.bottom = 
-        (effect.dims[0] - effect.element.clientHeight) + 'px'; },
-    afterFinishInternal: function(effect) { 
-      Element.undoClipping(effect.element); 
-      Element.undoPositioned(effect.element.firstChild);
-      effect.element.firstChild.style.bottom = oldInnerBottom; }
-    }, arguments[1] || {})
-  );
-}
-  
-Effect.SlideUp = function(element) {
-  element = $(element);
-  Element.cleanWhitespace(element);
-  var oldInnerBottom = element.firstChild.style.bottom;
-  return new Effect.Scale(element, 0, 
-   Object.extend({ scaleContent: false, 
-    scaleX: false, 
-    scaleMode: 'box',
-    scaleFrom: 100,
-    restoreAfterFinish: true,
-    beforeStartInternal: function(effect) { 
-      Element.makePositioned(effect.element.firstChild);
-      if (window.opera) effect.element.firstChild.style.top = "";
-      Element.makeClipping(effect.element);
-      Element.show(element); 
-    },  
-    afterUpdateInternal: function(effect) { 
-     effect.element.firstChild.style.bottom = 
-       (effect.dims[0] - effect.element.clientHeight) + 'px'; },
-    afterFinishInternal: function(effect) { 
-        Element.hide(effect.element);
-        Element.undoClipping(effect.element); 
-        Element.undoPositioned(effect.element.firstChild);
-        effect.element.firstChild.style.bottom = oldInnerBottom; }
-   }, arguments[1] || {})
-  );
-}
-
-Effect.Squish = function(element) {
-  // Bug in opera makes the TD containing this element expand for a instance after finish 
-  return new Effect.Scale(element, window.opera ? 1 : 0, 
-    { restoreAfterFinish: true,
-      beforeSetup: function(effect) { 
-        Element.makeClipping(effect.element); },  
-      afterFinishInternal: function(effect) { 
-        Element.hide(effect.element); 
-        Element.undoClipping(effect.element); } 
-  });
-}
-
-Effect.Grow = function(element) {
-  element = $(element);
-  var options = arguments[1] || {};
-  
-  var elementDimensions = Element.getDimensions(element);
-  var originalWidth = elementDimensions.width;
-  var originalHeight = elementDimensions.height;
-  var oldTop = element.style.top;
-  var oldLeft = element.style.left;
-  var oldHeight = element.style.height;
-  var oldWidth = element.style.width;
-  var oldOpacity = Element.getInlineOpacity(element);
-  
-  var direction = options.direction || 'center';
-  var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
-  var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
-  var opacityTransition = options.opacityTransition || Effect.Transitions.full;
-  
-  var initialMoveX, initialMoveY;
-  var moveX, moveY;
-  
-  switch (direction) {
-    case 'top-left':
-      initialMoveX = initialMoveY = moveX = moveY = 0; 
-      break;
-    case 'top-right':
-      initialMoveX = originalWidth;
-      initialMoveY = moveY = 0;
-      moveX = -originalWidth;
-      break;
-    case 'bottom-left':
-      initialMoveX = moveX = 0;
-      initialMoveY = originalHeight;
-      moveY = -originalHeight;
-      break;
-    case 'bottom-right':
-      initialMoveX = originalWidth;
-      initialMoveY = originalHeight;
-      moveX = -originalWidth;
-      moveY = -originalHeight;
-      break;
-    case 'center':
-      initialMoveX = originalWidth / 2;
-      initialMoveY = originalHeight / 2;
-      moveX = -originalWidth / 2;
-      moveY = -originalHeight / 2;
-      break;
-  }
-  
-  return new Effect.MoveBy(element, initialMoveY, initialMoveX, { 
-    duration: 0.01, 
-    beforeSetup: function(effect) { 
-      Element.hide(effect.element);
-      Element.makeClipping(effect.element);
-      Element.makePositioned(effect.element);
-    },
-    afterFinishInternal: function(effect) {
-      new Effect.Parallel(
-        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }),
-          new Effect.MoveBy(effect.element, moveY, moveX, { sync: true, transition: moveTransition }),
-          new Effect.Scale(effect.element, 100, {
-            scaleMode: { originalHeight: originalHeight, originalWidth: originalWidth }, 
-            sync: true, scaleFrom: window.opera ? 1 : 0, transition: scaleTransition, restoreAfterFinish: true})
-        ], Object.extend({
-             beforeSetup: function(effect) {
-              effect.effects[0].element.style.height = 0;
-              Element.show(effect.effects[0].element);
-             },              
-             afterFinishInternal: function(effect) {
-               var el = effect.effects[0].element;
-               var els = el.style;
-               Element.undoClipping(el); 
-               Element.undoPositioned(el);
-               els.top = oldTop;
-               els.left = oldLeft;
-               els.height = oldHeight;
-               els.width = originalWidth + 'px';
-               Element.setInlineOpacity(el, oldOpacity);
-             }
-           }, options)
-      )
-    }
-  });
-}
-
-Effect.Shrink = function(element) {
-  element = $(element);
-  var options = arguments[1] || {};
-  
-  var originalWidth = element.clientWidth;
-  var originalHeight = element.clientHeight;
-  var oldTop = element.style.top;
-  var oldLeft = element.style.left;
-  var oldHeight = element.style.height;
-  var oldWidth = element.style.width;
-  var oldOpacity = Element.getInlineOpacity(element);
-
-  var direction = options.direction || 'center';
-  var moveTransition = options.moveTransition || Effect.Transitions.sinoidal;
-  var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal;
-  var opacityTransition = options.opacityTransition || Effect.Transitions.none;
-  
-  var moveX, moveY;
-  
-  switch (direction) {
-    case 'top-left':
-      moveX = moveY = 0;
-      break;
-    case 'top-right':
-      moveX = originalWidth;
-      moveY = 0;
-      break;
-    case 'bottom-left':
-      moveX = 0;
-      moveY = originalHeight;
-      break;
-    case 'bottom-right':
-      moveX = originalWidth;
-      moveY = originalHeight;
-      break;
-    case 'center':  
-      moveX = originalWidth / 2;
-      moveY = originalHeight / 2;
-      break;
-  }
-  
-  return new Effect.Parallel(
-    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: opacityTransition }),
-      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: scaleTransition, restoreAfterFinish: true}),
-      new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition })
-    ], Object.extend({            
-         beforeStartInternal: function(effect) { 
-           Element.makePositioned(effect.effects[0].element);
-           Element.makeClipping(effect.effects[0].element);
-         },
-         afterFinishInternal: function(effect) {
-           var el = effect.effects[0].element;
-           var els = el.style;
-           Element.hide(el);
-           Element.undoClipping(el); 
-           Element.undoPositioned(el);
-           els.top = oldTop;
-           els.left = oldLeft;
-           els.height = oldHeight;
-           els.width = oldWidth;
-           Element.setInlineOpacity(el, oldOpacity);
-         }
-       }, options)
-  );
-}
-
-Effect.Pulsate = function(element) {
-  element = $(element);
-  var options    = arguments[1] || {};
-  var oldOpacity = Element.getInlineOpacity(element);
-  var transition = options.transition || Effect.Transitions.sinoidal;
-  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
-  reverser.bind(transition);
-  return new Effect.Opacity(element, 
-    Object.extend(Object.extend({  duration: 3.0, from: 0,
-      afterFinishInternal: function(effect) { Element.setInlineOpacity(effect.element, oldOpacity); }
-    }, options), {transition: reverser}));
-}
-
-Effect.Fold = function(element) {
-  element = $(element);
-  var originalTop = element.style.top;
-  var originalLeft = element.style.left;
-  var originalWidth = element.style.width;
-  var originalHeight = element.style.height;
-  Element.makeClipping(element);
-  return new Effect.Scale(element, 5, Object.extend({   
-    scaleContent: false,
-    scaleX: false,
-    afterFinishInternal: function(effect) {
-    new Effect.Scale(element, 1, { 
-      scaleContent: false, 
-      scaleY: false,
-      afterFinishInternal: function(effect) { 
-        Element.hide(effect.element);  
-        Element.undoClipping(effect.element); 
-        effect.element.style.top = originalTop;
-        effect.element.style.left = originalLeft;
-        effect.element.style.width = originalWidth;
-        effect.element.style.height = originalHeight;
-      } });
-  }}, arguments[1] || {}));
-}
diff --git a/web/static/js/scriptaculous/scriptaculous.js b/web/static/js/scriptaculous/scriptaculous.js
deleted file mode 100644
index 940d886..0000000
--- a/web/static/js/scriptaculous/scriptaculous.js
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
-// 
-// Permission is hereby granted, free of charge, to any person obtaining
-// a copy of this software and associated documentation files (the
-// "Software"), to deal in the Software without restriction, including
-// without limitation the rights to use, copy, modify, merge, publish,
-// distribute, sublicense, and/or sell copies of the Software, and to
-// permit persons to whom the Software is furnished to do so, subject to
-// the following conditions:
-// 
-// The above copyright notice and this permission notice shall be
-// included in all copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-var Scriptaculous = {
-  Version: '1.5_rc4',
-  require: function(libraryName) {
-    // inserting via DOM fails in Safari 2.0, so brute force approach
-    document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
-  },
-  load: function() {
-    if((typeof Prototype=='undefined') ||
-      parseFloat(Prototype.Version.split(".")[0] + "." +
-                 Prototype.Version.split(".")[1]) < 1.4)
-      throw("script.aculo.us requires the Prototype JavaScript framework >= 1.4.0");
-    var scriptTags = document.getElementsByTagName("script");
-    for(var i=0;i<scriptTags.length;i++) {
-      if(scriptTags[i].src && scriptTags[i].src.match(/scriptaculous\.js(\?.*)?$/)) {
-        var path = scriptTags[i].src.replace(/scriptaculous\.js(\?.*)?$/,'');
-        this.require(path + 'builder.js');
-        this.require(path + 'effects.js');
-        this.require(path + 'dragdrop.js');
-        this.require(path + 'controls.js');
-        this.require(path + 'slider.js');
-        break;
-      }
-    }
-  }
-}
-
-Scriptaculous.load();
\ No newline at end of file
diff --git a/web/static/js/scriptaculous/slider.js b/web/static/js/scriptaculous/slider.js
deleted file mode 100644
index 736ed41..0000000
--- a/web/static/js/scriptaculous/slider.js
+++ /dev/null
@@ -1,226 +0,0 @@
-// Copyright (c) 2005 Marty Haught
-// 
-// See scriptaculous.js for full license.
-
-if(!Control) var Control = {};
-Control.Slider = Class.create();
-
-// options:
-//  axis: 'vertical', or 'horizontal' (default)
-//
-// callbacks:
-//  onChange(value)
-//  onSlide(value)
-Control.Slider.prototype = {
-  initialize: function(handle, track, options) {
-    var slider = this;
-    
-    if(handle instanceof Array) {
-      this.handles = handle.collect( function(e) { return $(e) });
-    } else {
-      this.handles = [$(handle)];
-    }
-    
-    this.track   = $(track);
-    this.options = options || {};
-
-    this.axis      = this.options.axis || 'horizontal';
-    this.increment = this.options.increment || 1;
-    this.step      = parseInt(this.options.step || '1');
-    this.range     = this.options.range || $R(0,1);
-    
-    this.value     = 0; // assure backwards compat
-    this.values    = this.handles.map( function() { return 0 });
-    this.spans     = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false;
-    this.restricted = this.options.restricted || false;
-
-    this.maximum   = this.options.maximum || this.range.end;
-    this.minimum   = this.options.minimum || this.range.start;
-
-    // Will be used to align the handle onto the track, if necessary
-    this.alignX = parseInt(this.options.alignX || '0');
-    this.alignY = parseInt(this.options.alignY || '0');
-    
-    this.trackLength = this.maximumOffset() - this.minimumOffset();
-
-    this.active   = false;
-    this.dragging = false;
-    this.disabled = false;
-
-    if(this.options.disabled) this.setDisabled();
-
-    // Allowed values array
-    this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false;
-    if(this.allowedValues) {
-      this.minimum = this.allowedValues.min();
-      this.maximum = this.allowedValues.max();
-    }
-
-    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
-    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
-    this.eventMouseMove = this.update.bindAsEventListener(this);
-
-    // Initialize handles
-    this.handles.each( function(h,i) {
-      slider.setValue(parseInt(slider.options.sliderValue || slider.range.start), i);
-      Element.makePositioned(h); // fix IE
-      Event.observe(h, "mousedown", slider.eventMouseDown);
-    });
-    
-    Event.observe(document, "mouseup", this.eventMouseUp);
-    Event.observe(document, "mousemove", this.eventMouseMove);
-  },
-  dispose: function() {
-    var slider = this;    
-    Event.stopObserving(document, "mouseup", this.eventMouseUp);
-    Event.stopObserving(document, "mousemove", this.eventMouseMove);
-    this.handles.each( function(h) {
-      Event.stopObserving(h, "mousedown", slider.eventMouseDown);
-    });
-  },
-  setDisabled: function(){
-    this.disabled = true;
-  },
-  setEnabled: function(){
-    this.disabled = false;
-  },  
-  getNearestValue: function(value){
-    if(this.allowedValues){
-      if(value >= this.allowedValues.max()) return(this.allowedValues.max());
-      if(value <= this.allowedValues.min()) return(this.allowedValues.min());
-      
-      var offset = Math.abs(this.allowedValues[0] - value);
-      var newValue = this.allowedValues[0];
-      this.allowedValues.each( function(v) {
-        var currentOffset = Math.abs(v - value);
-        if(currentOffset <= offset){
-          newValue = v;
-          offset = currentOffset;
-        } 
-      });
-      return newValue;
-    }
-    if(value > this.range.end) return this.range.end;
-    if(value < this.range.start) return this.range.start;
-    return value;
-  },
-  setValue: function(sliderValue, handleIdx){
-    if(!this.active) {
-      this.activeHandle    = this.handles[handleIdx];
-      this.activeHandleIdx = handleIdx;
-    }
-    handleIdx = handleIdx || this.activeHandleIdx || 0;
-    if(this.restricted) {
-      if((handleIdx>0) && (sliderValue<this.values[handleIdx-1]))
-        sliderValue = this.values[handleIdx-1];
-      if((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1]))
-        sliderValue = this.values[handleIdx+1];
-    }
-    sliderValue = this.getNearestValue(sliderValue);
-    this.values[handleIdx] = sliderValue;
-    this.value = this.values[0]; // assure backwards compat
-    
-    this.handles[handleIdx].style[ this.isVertical() ? 'top' : 'left'] = 
-      this.translateToPx(sliderValue);
-    
-    this.drawSpans();
-    this.updateFinished();
-  },
-  setValueBy: function(delta, handleIdx) {
-    this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta, 
-      handleIdx || this.activeHandleIdx || 0);
-  },
-  translateToPx: function(value) {
-    return Math.round((this.trackLength / (this.range.end - this.range.start)) * (value - this.range.start)) + "px";
-  },
-  translateToValue: function(offset) {
-    return ((offset/this.trackLength) * (this.range.end - this.range.start)) + this.range.start;
-  },
-  getRange: function(range) {
-    var v = this.values.sortBy(Prototype.K); 
-    range = range || 0;
-    return $R(v[range],v[range+1]);
-  },
-  minimumOffset: function(){
-    return(this.isVertical() ? this.alignY : this.alignX);
-  },
-  maximumOffset: function(){
-    return(this.isVertical() ?
-      this.track.offsetHeight - this.alignY : this.track.offsetWidth - this.alignX);
-  },  
-  isVertical:  function(){
-    return (this.axis == 'vertical');
-  },
-  drawSpans: function() {
-    var slider = this;
-    if(this.spans)
-      $R(0, this.spans.length-1).each(function(r) { slider.setSpan(r, slider.getRange(r)) });
-  },
-  setSpan: function(span, range) {
-    if(this.isVertical()) {
-      this.spans[span].style.top = this.translateToPx(range.start);
-      this.spans[span].style.height = this.translateToPx(range.end - range.start);
-    } else {
-      this.spans[span].style.left = this.translateToPx(range.start);
-      this.spans[span].style.width = this.translateToPx(range.end - range.start);
-    }
-  },
-  startDrag: function(event) {
-    if(Event.isLeftClick(event)) {
-      if(!this.disabled){
-        this.active = true;
-        
-        // find the handle (prevents issues with Safari)
-        var handle = Event.element(event);
-        while((this.handles.indexOf(handle) == -1) && handle.parentNode) 
-          handle = handle.parentNode;
-        
-        this.activeHandle    = handle;
-        this.activeHandleIdx = this.handles.indexOf(this.activeHandle);
-        
-        var pointer  = [Event.pointerX(event), Event.pointerY(event)];
-        var offsets  = Position.cumulativeOffset(this.activeHandle);
-        this.offsetX = (pointer[0] - offsets[0]);
-        this.offsetY = (pointer[1] - offsets[1]);
-        
-      }
-      Event.stop(event);
-    }
-  },
-  update: function(event) {
-   if(this.active) {
-      if(!this.dragging) {
-        this.dragging = true;
-        if(this.activeHandle.style.position=="") style.position = "relative";
-      }
-      this.draw(event);
-      // fix AppleWebKit rendering
-      if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
-      Event.stop(event);
-   }
-  },
-  draw: function(event) {
-    var pointer = [Event.pointerX(event), Event.pointerY(event)];
-    var offsets = Position.cumulativeOffset(this.track);
-    pointer[0] -= this.offsetX + offsets[0];
-    pointer[1] -= this.offsetY + offsets[1];
-    this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] ));
-    if(this.options.onSlide) this.options.onSlide(this.values.length>1 ? this.values : this.value, this);
-  },
-  endDrag: function(event) {
-    if(this.active && this.dragging) {
-      this.finishDrag(event, true);
-      Event.stop(event);
-    }
-    this.active = false;
-    this.dragging = false;
-  },  
-  finishDrag: function(event, success) {
-    this.active = false;
-    this.dragging = false;
-    this.updateFinished();
-  },
-  updateFinished: function() {
-    if(this.options.onChange) this.options.onChange(this.values.length>1 ? this.values : this.value, this);
-  }
-}
diff --git a/web/static/js/scriptaculous/unittest.js b/web/static/js/scriptaculous/unittest.js
deleted file mode 100644
index 20941ad..0000000
--- a/web/static/js/scriptaculous/unittest.js
+++ /dev/null
@@ -1,363 +0,0 @@
-// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
-//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
-//           (c) 2005 Michael Schuerig (http://www.schuerig.de/michael/)
-//
-// See scriptaculous.js for full license.
-
-// experimental, Firefox-only
-Event.simulateMouse = function(element, eventName) {
-  var options = Object.extend({
-    pointerX: 0,
-    pointerY: 0,
-    buttons: 0
-  }, arguments[2] || {});
-  var oEvent = document.createEvent("MouseEvents");
-  oEvent.initMouseEvent(eventName, true, true, document.defaultView, 
-    options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY, 
-    false, false, false, false, 0, $(element));
-  
-  if(this.mark) Element.remove(this.mark);
-  this.mark = document.createElement('div');
-  this.mark.appendChild(document.createTextNode(" "));
-  document.body.appendChild(this.mark);
-  this.mark.style.position = 'absolute';
-  this.mark.style.top = options.pointerY + "px";
-  this.mark.style.left = options.pointerX + "px";
-  this.mark.style.width = "5px";
-  this.mark.style.height = "5px;";
-  this.mark.style.borderTop = "1px solid red;"
-  this.mark.style.borderLeft = "1px solid red;"
-  
-  if(this.step)
-    alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options));
-  
-  $(element).dispatchEvent(oEvent);
-};
-
-// Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2.
-// You need to downgrade to 1.0.4 for now to get this working
-// See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much
-Event.simulateKey = function(element, eventName) {
-  var options = Object.extend({
-    ctrlKey: false,
-    altKey: false,
-    shiftKey: false,
-    metaKey: false,
-    keyCode: 0,
-    charCode: 0
-  }, arguments[2] || {});
-
-  var oEvent = document.createEvent("KeyEvents");
-  oEvent.initKeyEvent(eventName, true, true, window, 
-    options.ctrlKey, options.altKey, options.shiftKey, options.metaKey,
-    options.keyCode, options.charCode );
-  $(element).dispatchEvent(oEvent);
-};
-
-Event.simulateKeys = function(element, command) {
-  for(var i=0; i<command.length; i++) {
-    Event.simulateKey(element,'keypress',{charCode:command.charCodeAt(i)});
-  }
-};
-
-var Test = {}
-Test.Unit = {};
-
-// security exception workaround
-Test.Unit.inspect = function(obj) {
-  var info = [];
-
-  if(typeof obj=="string" || 
-     typeof obj=="number") {
-    return obj;
-  } else {
-    for(property in obj)
-      if(typeof obj[property]!="function")
-        info.push(property + ' => ' + 
-          (typeof obj[property] == "string" ?
-            '"' + obj[property] + '"' :
-            obj[property]));
-  }
-
-  return ("'" + obj + "' #" + typeof obj + 
-    ": {" + info.join(", ") + "}");
-}
-
-Test.Unit.Logger = Class.create();
-Test.Unit.Logger.prototype = {
-  initialize: function(log) {
-    this.log = $(log);
-    if (this.log) {
-      this._createLogTable();
-    }
-  },
-  start: function(testName) {
-    if (!this.log) return;
-    this.testName = testName;
-    this.lastLogLine = document.createElement('tr');
-    this.statusCell = document.createElement('td');
-    this.nameCell = document.createElement('td');
-    this.nameCell.appendChild(document.createTextNode(testName));
-    this.messageCell = document.createElement('td');
-    this.lastLogLine.appendChild(this.statusCell);
-    this.lastLogLine.appendChild(this.nameCell);
-    this.lastLogLine.appendChild(this.messageCell);
-    this.loglines.appendChild(this.lastLogLine);
-  },
-  finish: function(status, summary) {
-    if (!this.log) return;
-    this.lastLogLine.className = status;
-    this.statusCell.innerHTML = status;
-    this.messageCell.innerHTML = this._toHTML(summary);
-  },
-  message: function(message) {
-    if (!this.log) return;
-    this.messageCell.innerHTML = this._toHTML(message);
-  },
-  summary: function(summary) {
-    if (!this.log) return;
-    this.logsummary.innerHTML = this._toHTML(summary);
-  },
-  _createLogTable: function() {
-    this.log.innerHTML =
-    '<div id="logsummary"></div>' +
-    '<table id="logtable">' +
-    '<thead><tr><th>Status</th><th>Test</th><th>Message</th></tr></thead>' +
-    '<tbody id="loglines"></tbody>' +
-    '</table>';
-    this.logsummary = $('logsummary')
-    this.loglines = $('loglines');
-  },
-  _toHTML: function(txt) {
-    return txt.escapeHTML().replace(/\n/g,"<br/>");
-  }
-}
-
-Test.Unit.Runner = Class.create();
-Test.Unit.Runner.prototype = {
-  initialize: function(testcases) {
-    this.options = Object.extend({
-      testLog: 'testlog'
-    }, arguments[1] || {});
-    this.options.resultsURL = this.parseResultsURLQueryParameter();
-    if (this.options.testLog) {
-      this.options.testLog = $(this.options.testLog) || null;
-    }
-    if(this.options.tests) {
-      this.tests = [];
-      for(var i = 0; i < this.options.tests.length; i++) {
-        if(/^test/.test(this.options.tests[i])) {
-          this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"]));
-        }
-      }
-    } else {
-      if (this.options.test) {
-        this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])];
-      } else {
-        this.tests = [];
-        for(var testcase in testcases) {
-          if(/^test/.test(testcase)) {
-            this.tests.push(new Test.Unit.Testcase(testcase, testcases[testcase], testcases["setup"], testcases["teardown"]));
-          }
-        }
-      }
-    }
-    this.currentTest = 0;
-    this.logger = new Test.Unit.Logger(this.options.testLog);
-    setTimeout(this.runTests.bind(this), 1000);
-  },
-  parseResultsURLQueryParameter: function() {
-    return window.location.search.parseQuery()["resultsURL"];
-  },
-  // Returns:
-  //  "ERROR" if there was an error,
-  //  "FAILURE" if there was a failure, or
-  //  "SUCCESS" if there was neither
-  getResult: function() {
-    var hasFailure = false;
-    for(var i=0;i<this.tests.length;i++) {
-      if (this.tests[i].errors > 0) {
-        return "ERROR";
-      }
-      if (this.tests[i].failures > 0) {
-        hasFailure = true;
-      }
-    }
-    if (hasFailure) {
-      return "FAILURE";
-    } else {
-      return "SUCCESS";
-    }
-  },
-  postResults: function() {
-    if (this.options.resultsURL) {
-      new Ajax.Request(this.options.resultsURL, 
-        { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false });
-    }
-  },
-  runTests: function() {
-    var test = this.tests[this.currentTest];
-    if (!test) {
-      // finished!
-      this.postResults();
-      this.logger.summary(this.summary());
-      return;
-    }
-    if(!test.isWaiting) {
-      this.logger.start(test.name);
-    }
-    test.run();
-    if(test.isWaiting) {
-      this.logger.message("Waiting for " + test.timeToWait + "ms");
-      setTimeout(this.runTests.bind(this), test.timeToWait || 1000);
-    } else {
-      this.logger.finish(test.status(), test.summary());
-      this.currentTest++;
-      // tail recursive, hopefully the browser will skip the stackframe
-      this.runTests();
-    }
-  },
-  summary: function() {
-    var assertions = 0;
-    var failures = 0;
-    var errors = 0;
-    var messages = [];
-    for(var i=0;i<this.tests.length;i++) {
-      assertions +=   this.tests[i].assertions;
-      failures   +=   this.tests[i].failures;
-      errors     +=   this.tests[i].errors;
-    }
-    return (
-      this.tests.length + " tests, " + 
-      assertions + " assertions, " + 
-      failures   + " failures, " +
-      errors     + " errors");
-  }
-}
-
-Test.Unit.Assertions = Class.create();
-Test.Unit.Assertions.prototype = {
-  initialize: function() {
-    this.assertions = 0;
-    this.failures   = 0;
-    this.errors     = 0;
-    this.messages   = [];
-  },
-  summary: function() {
-    return (
-      this.assertions + " assertions, " + 
-      this.failures   + " failures, " +
-      this.errors     + " errors" + "\n" +
-      this.messages.join("\n"));
-  },
-  pass: function() {
-    this.assertions++;
-  },
-  fail: function(message) {
-    this.failures++;
-    this.messages.push("Failure: " + message);
-  },
-  error: function(error) {
-    this.errors++;
-    this.messages.push(error.name + ": "+ error.message + "(" + Test.Unit.inspect(error) +")");
-  },
-  status: function() {
-    if (this.failures > 0) return 'failed';
-    if (this.errors > 0) return 'error';
-    return 'passed';
-  },
-  assert: function(expression) {
-    var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"';
-    try { expression ? this.pass() : 
-      this.fail(message); }
-    catch(e) { this.error(e); }
-  },
-  assertEqual: function(expected, actual) {
-    var message = arguments[2] || "assertEqual";
-    try { (expected == actual) ? this.pass() :
-      this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 
-        '", actual "' + Test.Unit.inspect(actual) + '"'); }
-    catch(e) { this.error(e); }
-  },
-  assertNotEqual: function(expected, actual) {
-    var message = arguments[2] || "assertNotEqual";
-    try { (expected != actual) ? this.pass() : 
-      this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); }
-    catch(e) { this.error(e); }
-  },
-  assertNull: function(obj) {
-    var message = arguments[1] || 'assertNull'
-    try { (obj==null) ? this.pass() : 
-      this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); }
-    catch(e) { this.error(e); }
-  },
-  assertHidden: function(element) {
-    var message = arguments[1] || 'assertHidden';
-    this.assertEqual("none", element.style.display, message);
-  },
-  assertNotNull: function(object) {
-    var message = arguments[1] || 'assertNotNull';
-    this.assert(object != null, message);
-  },
-  assertInstanceOf: function(expected, actual) {
-    var message = arguments[2] || 'assertInstanceOf';
-    try { 
-      (actual instanceof expected) ? this.pass() : 
-      this.fail(message + ": object was not an instance of the expected type"); }
-    catch(e) { this.error(e); } 
-  },
-  assertNotInstanceOf: function(expected, actual) {
-    var message = arguments[2] || 'assertNotInstanceOf';
-    try { 
-      !(actual instanceof expected) ? this.pass() : 
-      this.fail(message + ": object was an instance of the not expected type"); }
-    catch(e) { this.error(e); } 
-  },
-  _isVisible: function(element) {
-    element = $(element);
-    if(!element.parentNode) return true;
-    this.assertNotNull(element);
-    if(element.style && Element.getStyle(element, 'display') == 'none')
-      return false;
-    
-    return this._isVisible(element.parentNode);
-  },
-  assertNotVisible: function(element) {
-    this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1]));
-  },
-  assertVisible: function(element) {
-    this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1]));
-  }
-}
-
-Test.Unit.Testcase = Class.create();
-Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), {
-  initialize: function(name, test, setup, teardown) {
-    Test.Unit.Assertions.prototype.initialize.bind(this)();
-    this.name           = name;
-    this.test           = test || function() {};
-    this.setup          = setup || function() {};
-    this.teardown       = teardown || function() {};
-    this.isWaiting      = false;
-    this.timeToWait     = 1000;
-  },
-  wait: function(time, nextPart) {
-    this.isWaiting = true;
-    this.test = nextPart;
-    this.timeToWait = time;
-  },
-  run: function() {
-    try {
-      try {
-        if (!this.isWaiting) this.setup.bind(this)();
-        this.isWaiting = false;
-        this.test.bind(this)();
-      } finally {
-        if(!this.isWaiting) {
-          this.teardown.bind(this)();
-        }
-      }
-    }
-    catch(e) { this.error(e); }
-  }
-});
\ No newline at end of file
diff --git a/web/templates/_elements/header b/web/templates/_elements/header
deleted file mode 100644
index e8942d3..0000000
--- a/web/templates/_elements/header
+++ /dev/null
@@ -1,25 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
-<head>
-  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
-  <meta name="robots" content="all" />
-  
-  <title><% $title %></title>
-  
-  <link rel="stylesheet" type="text/css" href="/css/main.css" media="all" />
-  
-  <script type="text/javascript" src="/js/prototype.js"></script>
-  <script type="text/javascript" src="/js/rico.js"></script>
-  <script type="text/javascript" src="/js/behaviour.js"></script>
-  <script type="text/javascript" src="/js/jifty.js"></script>
-  <script type="text/javascript" src="/js/btdt_behaviour.js"></script>
-  <script type="text/javascript" src="/js/bps_util.js"></script>
-  <script type="text/javascript" src="/js/combobox.js"></script>
-  <script type="text/javascript" src="/js/key_bindings.js"></script>
-</head>
-<%args>
-$title => ""
-</%args>
-<%init>
-$r->content_type('text/html; charset=utf-8');
-</%init>
diff --git a/web/templates/_elements/markup b/web/templates/_elements/markup
index a281a2a..b6b8cca 100644
--- a/web/templates/_elements/markup
+++ b/web/templates/_elements/markup
@@ -3,58 +3,17 @@
 
 <h3>Phrase Emphasis</h3>
 
-<pre><code>*italic*   **bold**
-_italic_   __bold__
-</code></pre>
+<code> <b>**bold**</b> <i>_italic_</i> </code>
 
 <h3>Links</h3>
 
-<p>Inline:</p>
-
-<pre><code>Show me a [wiki page](WikiPage)</code></pre>
-
-<pre><code>An [example](http://url.com/ "Title")
-</code></pre>
-
-<p>Reference-style labels (titles are optional):</p>
-
-<pre><code>An [example][id]. Then, anywhere
-else in the doc, define the link:
-
-  [id]: http://example.com/  "Title"
-</code></pre>
-
-<h3>Images</h3>
-
-<p>Inline (titles are optional):</p>
-
-<pre><code>![alt text](/path/img.jpg "Title")
-</code></pre>
-
-<p>Reference-style:</p>
-
-<pre><code>![alt text][id]
-
-[id]: /url/to/img.jpg "Title"
-</code></pre>
+<code>Show me a [wiki page](WikiPage)</code>
+<code>An [example](http://url.com/ "Title")</code>
 
 <h3>Headers</h3>
 
-<p>Setext-style:</p>
-
-<pre><code>Header 1
-========
-
-Header 2
---------
-</code></pre>
-
-<p>atx-style (closing #'s are optional):</p>
-
-<pre><code># Header 1 #
-
-## Header 2 ##
-
+<pre><code># Header 1
+## Header 2
 ###### Header 6
 </code></pre>
 
@@ -64,7 +23,6 @@ Header 2
 
 <pre><code>1.  Foo
 2.  Bar
-
 </code></pre>
 
 <p>Unordered, with paragraphs:</p>
@@ -73,47 +31,17 @@ Header 2
 
     With multiple paragraphs.
 
-*   Bar
-</code></pre>
-
-<p>You can nest them:</p>
-
-<pre><code>*   Abacus
-    * answer
-*   Bubbles
-    1.  bunk
-    2.  bupkis
-        * BELITTLER
-    3. burper
-*   Cunning
-</code></pre>
-
-<h3>Blockquotes</h3>
-
-<pre><code>> Email-style angle brackets
-> are used for blockquotes.
-
-> > And, they can be nested.
-
-> #### Headers in blockquotes
-> 
-> * You can quote a list.
-> * Etc.
-</code></pre>
+*   Bar</code></pre>
 
 <h3>Code Spans</h3>
 
-<pre><code>`<code>` spans are delimited
-by backticks.
-
-You can include literal backticks
-like `` `this` ``.
-</code></pre>
+<p><code>`<code>`</code> spans are 
+delimited by backticks.</p>
 
 <h3>Preformatted Code Blocks</h3>
 
 <p>Indent every line of a code block 
-by at least 4 spaces or 1 tab.</p>
+by at least 4 spaces.</p>
 
 <pre><code>This is a normal paragraph.
 
@@ -123,21 +51,7 @@ by at least 4 spaces or 1 tab.</p>
 
 <h3>Horizontal Rules</h3>
 
-<p>Three or more dashes or asterisks:</p>
-
-<pre><code>---
-
-* * *
+<p>Three or more dashes: <code>---</code></p>
 
-- - - -
-</code></pre>
-
-<h3>Manual Line Breaks</h3>
-
-<p>End a line with two or more spaces:</p>
-
-<pre><code>Roses are red,   
-Violets are blue.
-</code></pre>
 <address>(Thanks to <a href="http://daringfireball.net/projects/markdown/dingus">Daring Fireball</a>)</address>
 </div> 
diff --git a/web/templates/_elements/nav b/web/templates/_elements/nav
index c773de9..5baff86 100644
--- a/web/templates/_elements/nav
+++ b/web/templates/_elements/nav
@@ -1,7 +1,16 @@
 <%init>
 my $top = Jifty->web->navigation;
-$top->child(Home       => url => "/", sort_order => 1);
-$top->child(Recent       => url => "/recent", label => "Recent Changes", sort_order => 2);
+$top->child( 
+    Home => 
+        url => "/", 
+        sort_order => 1 
+);
+$top->child(
+    Recent  =>
+        url => "/recent",
+        label      => "Recent Changes",
+        sort_order => 2
+);
 
 return();
 </%init>
diff --git a/web/templates/_elements/sidebar b/web/templates/_elements/sidebar
deleted file mode 100644
index 2869f05..0000000
--- a/web/templates/_elements/sidebar
+++ /dev/null
@@ -1,26 +0,0 @@
-<div id="salutation">
-% if (Jifty->web->current_user->id) {
-Hiya, <span class="user"><%Jifty->web->current_user->name%></span>.
-% }  else {
-You're not currently signed in. 
-% }
-</div>
-<ul class="menu">
-% $m->comp(".menu", item => $_) for (sort { $a->sort_order <=> $b->sort_order} Jifty->web->navigation->children);
-</ul>
-<%def .menu>
-<%args>
-$item
-</%args>
-  <li><% 
-    Jifty->web->link(
-        url   => $item->url,
-        label => $item->label,
-        class => $item->active ? "active" : ""
-    ) %></li>
-% if (my @kids = $item->children) {
-<ul class="menu submenu">
-% $m->comp(".menu", item => $_) for @kids;
-</ul>
-% }
-</%def>
diff --git a/web/templates/_elements/wrapper b/web/templates/_elements/wrapper
deleted file mode 100644
index 0ec63a4..0000000
--- a/web/templates/_elements/wrapper
+++ /dev/null
@@ -1,27 +0,0 @@
-<& header, title => $title &>
-<body>
-  <div id="headers">
-    <%Jifty->web->link( url => "/", label => Jifty->config->framework('ApplicationName'))%>
-    <h1 class="title"><% $title %></h1>
-  </div>
-  <& sidebar &>
-  <div id="content">
-    <a name="content"></a>
-    <% Jifty->web->render_messages %>
-    <% $m->content |n%>
-    <div id="keybindings">
-       <script><!--
-       writeKeyBindingLegend();
-       --></script>
-    </div>
-  </div>
-  <div id="jifty-wait-message">Loading...</div>
-</body>
-</html>
-<%args>
-$title => ""
-</%args>
-<%init>
-$m->comp('nav');
-
-</%init>
diff --git a/web/templates/autohandler b/web/templates/autohandler
deleted file mode 100644
index 0a4476b..0000000
--- a/web/templates/autohandler
+++ /dev/null
@@ -1,25 +0,0 @@
-<%init>
-Jifty->web->handle_request();
-
-if ($m->base_comp->path =~ m|/_elements/|) {
-    # Requesting an internal component by hand -- naughty
-    $m->redirect("/errors/requested_private_component");
-#} elsif (not Jifty->web->current_user->id and $m->request_comp->path !~ m{^/(?:welcome|dhandler|css|js|images|validator\.xml)} ) {
-#    # Not logged in, trying to access a protected page
-#    $m->notes->{'login-nextpage'} =  $m->{top_path};
-#    Jifty->web->redirect('/welcome/');
-}
-</%init>
-<%$m->call_next()%>
-<%def .setup_actions>
-<%init>
-Jifty->web->allow_actions(qr/.*/);
-# this method turns around and calls the setup_actions method 
-# it's called by Jifty::Web->setup_page_actions.
-my $delegate = $m->fetch_comp($m->next_comp->path);
-if ($delegate and $delegate->method_exists('setup_actions')) {
-    $delegate->call_method('setup_actions');
-}
-
-</%init>
-</%def>
diff --git a/web/templates/dhandler b/web/templates/dhandler
deleted file mode 100644
index 8a2fe56..0000000
--- a/web/templates/dhandler
+++ /dev/null
@@ -1,47 +0,0 @@
-<&| /_elements/wrapper, title => "Something's not quite right" &>
-
-<div id="overview">
-
-<p>You got to a page that we don't think exists.  Anyway, the software has logged this error. Sorry about this.</p>
-
-<p><%Jifty->web->link( url => "/", label => 'Go back home...')%></p>
-
-</div>
-</&>
-%# XXX TODO ACTUALLY LOG THIS.
-<%doc>
-Used as a poor man's 404 handler
-</%doc>
-<%init>
-
-# This code loads up any static file and displays it if it would 404 from dynamic content. Failing that, actually 404
-my $file = $m->dhandler_arg;
-my $type = "application/octet-stream";
-if ( $file =~ /\.(gif|png|jpe?g)$/i ) {
-    $type = "image/$1";
-    $type =~ s/jpg/jpeg/gi;
-} elsif ($file =~ /\.css$/i ) {
-    $type ='text/css';
-} elsif ($file =~ /\.js$/i) {
-    $type = 'application/x-javascript';
-}
-my $image = Jifty::Util->absolute_path( Jifty->config->framework('Web')->{'StaticRoot'}
-        || "static" )
-    . "/"
-    . $file;
-
-if ( ( -f $image && -r $image ) ) {
-    $r->header_out( 'Cache-Control' => 'max-age=3600, must-revalidate' );
-    $r->content_type($type);
-    open( FILE, "<$image" ) || die;
-    {
-        local $/ = \16384;
-        $m->out($_) while (<FILE>);
-        close(FILE);
-    }
-    $m->abort;
-}
-
-Jifty->log->error("404: user tried to get to ".$m->dhandler_arg);
-$r->header_out( Status => '404');
-</%init>
diff --git a/web/templates/edit/dhandler b/web/templates/edit/dhandler
index 4a5240d..7eff1f2 100644
--- a/web/templates/edit/dhandler
+++ b/web/templates/edit/dhandler
@@ -4,13 +4,18 @@ my $page = Wifty::Model::Page->new();
 $page->load_by_cols( name => $name );
 my $viewer = Jifty->web->new_action( class => 'UpdatePage', record => $page );
 my $top = Jifty->web->navigation;
-$top->child(Show       => url =>  '/view/'.$page->name,, label => 'Show Page',  sort_order => 5);
+$top->child(
+    Show    =>
+        url => '/view/' . $page->name,
+        label      => 'Show Page',
+        sort_order => 5
+);
 </%init>
 <&|/_elements/wrapper, title => 'Edit: '.$page->name &>
 <% Jifty->web->form->start %>
 <% Jifty->web->form->next_page( url => '/view/'.$page->name) %>
 <% $viewer->form_field('content') %>
 <% Jifty->web->form->submit( label => 'Save') %>
-<% Jifty->web->form->end%>
+<% Jifty->web->form->end %>
 <& /_elements/markup &>
 </&>
diff --git a/web/templates/favicon.ico b/web/templates/favicon.ico
deleted file mode 100644
index e69de29..0000000
diff --git a/web/templates/pages b/web/templates/pages
index 35f05bc..aeddbd4 100644
--- a/web/templates/pages
+++ b/web/templates/pages
@@ -1,12 +1,17 @@
 <%init>
 my $pages = Wifty::Model::PageCollection->new();
 $pages->unlimit();
-
 </%init>
 <&|/_elements/wrapper, title => 'These are the pages on your wiki!' &>
 <ul id="pagelist">
 % while (my $page = $pages->next) {
-<li><% Jifty->web->link( label => $page->name, url => '/view/'.$page->name)%></li>
+<li><% 
+        Jifty->web->link(
+            label => $page->name,
+            url   => '/view/' . $page->name
+            );
+
+    %></li>
 % } 
 </ul>
 </&>
diff --git a/web/templates/recent b/web/templates/recent
index 433fb35..d002c6f 100644
--- a/web/templates/recent
+++ b/web/templates/recent
@@ -1,8 +1,8 @@
 <%init>
-my $then = DateTime->from_epoch(epoch => (time - (86400*7)));
+my $then = DateTime->from_epoch( epoch => ( time - ( 86400 * 7 ) ) );
 my $pages = Wifty::Model::PageCollection->new();
 $pages->limit( column => 'updated', operator => '>', value => $then->ymd );
-$pages->order_by( column => 'updated', order => 'desc');
+$pages->order_by( column => 'updated', order => 'desc' );
 </%init>
 <&|/_elements/wrapper, title => 'Updated this week' &>
 <dl id="recentudates">
diff --git a/web/templates/view/dhandler b/web/templates/view/dhandler
index 63c0f18..242ca75 100644
--- a/web/templates/view/dhandler
+++ b/web/templates/view/dhandler
@@ -1,13 +1,12 @@
 <%init>
 my $name = $m->dhandler_arg();
 my $page = Wifty::Model::Page->new();
-$page->load_by_cols( name => $name);
-unless ($page->id) {
-    Jifty->web->redirect( '/create/'.$name);
-    # XXX TODO: should this use goto or gosub or whatever we're calling it this week?
+$page->load_by_cols( name => $name );
+unless ( $page->id ) {
+    Jifty->web->redirect( '/create/' . $name );
 }
 my $top = Jifty->web->navigation;
-$top->child(Edit       => url =>  '/edit/'.$page->name , sort_order => 5);
+$top->child( Edit => url => '/edit/' . $page->name, sort_order => 5 );
 </%init>
 <&|/_elements/wrapper, title => $page->name &>
 <% $page->wiki_content |n %>

commit 0057bb6bd47c20f5a06d01411b919706ee4d3965
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:31:33 2005 +0000

    Page history!

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 6af8ae8..996f439 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -16,16 +16,25 @@ column updated =>
     since '0.0.6';
 
 
-#column revisions => refers_to Wifty::Model::Revision by 'page';
+column revisions =>
+    refers_to Wifty::Model::RevisionCollection by 'page';
 
 package Wifty::Model::Page;
 use base qw/Wifty::Record/;
+use Wifty::Model::RevisionCollection;
 use Text::Markdown;
 use HTML::Scrubber;
 
+=head2 wiki_content [CONTENT]
+
+Wikify either the content of a scalar passed in as an argument or
+this page's "content" attribute.
+
+=cut
+
 sub wiki_content {
     my $self     = shift;
-    my $content  = $self->content();
+    my $content  = shift ||$self->content();
     my $scrubber = HTML::Scrubber->new();
 
     $scrubber->default(
@@ -46,7 +55,7 @@ sub wiki_content {
     $scrubber->allow(
         qw[A B U P BR I HR BR SMALL EM FONT SPAN DIV UL OL LI DL DT DD]);
     $scrubber->comment(0);
-    return ( markdown( $scrubber->scrub( $self->content ) ) );
+    return ( markdown( $scrubber->scrub( $content ) ) );
 
 }
 
diff --git a/web/templates/_elements/page_nav b/web/templates/_elements/page_nav
new file mode 100644
index 0000000..8840b35
--- /dev/null
+++ b/web/templates/_elements/page_nav
@@ -0,0 +1,30 @@
+<%init>
+my $subpath =  $page . ($rev ? "/rev/$rev" : '');
+my $top = Jifty->web->navigation;
+my $this = $top->child( 
+    This => 
+        url => "/view/".$subpath,
+        label => $page,
+        sort_order => 5
+);
+
+$this->child(
+    View =>
+        url => '/view/'.$subpath
+);
+
+$this->child(
+    Edit =>
+        url => '/edit/'.$subpath
+);
+
+$this->child(
+    History =>
+        url => '/history/'.$subpath
+    );
+
+</%init>
+<%args>
+$page => 'HomePage'
+$rev => undef
+</%args>
diff --git a/web/templates/edit/dhandler b/web/templates/edit/dhandler
index 7eff1f2..08d463f 100644
--- a/web/templates/edit/dhandler
+++ b/web/templates/edit/dhandler
@@ -1,20 +1,30 @@
 <%init>
-my $name = $m->dhandler_arg();
+my $arg = $m->dhandler_arg();
+my ($name,$rev);
+if ($arg =~ qr{^(.*?)/?(\d?)$}) {
+    $name = $1;
+    $rev = $2;
+}
 my $page = Wifty::Model::Page->new();
 $page->load_by_cols( name => $name );
+
+my $revision = Wifty::Model::Revision->new();
+if ($rev) {
+$revision->load_by_cols( page  => $page->id, id=> $rev);
+}
+
 my $viewer = Jifty->web->new_action( class => 'UpdatePage', record => $page );
-my $top = Jifty->web->navigation;
-$top->child(
-    Show    =>
-        url => '/view/' . $page->name,
-        label      => 'Show Page',
-        sort_order => 5
-);
+$m->comp('/_elements/page_nav', page => $page->name, rev => $rev);
+
 </%init>
 <&|/_elements/wrapper, title => 'Edit: '.$page->name &>
 <% Jifty->web->form->start %>
 <% Jifty->web->form->next_page( url => '/view/'.$page->name) %>
+% if ($revision->id) {
+<% $viewer->form_field('content', default_value => $revision->content )%>
+% } else { 
 <% $viewer->form_field('content') %>
+% }
 <% Jifty->web->form->submit( label => 'Save') %>
 <% Jifty->web->form->end %>
 <& /_elements/markup &>
diff --git a/web/templates/history/dhandler b/web/templates/history/dhandler
new file mode 100755
index 0000000..a8c8aa3
--- /dev/null
+++ b/web/templates/history/dhandler
@@ -0,0 +1,20 @@
+<%init>
+my $name = $m->dhandler_arg();
+my $page = Wifty::Model::Page->new();
+$page->load_by_cols( name => $name );
+Jifty->web->redirect( '/create/' . $name ) unless ( $page->id );
+
+my $revisions = $page->revisions;
+$revisions->order_by( column => 'id', order => 'desc');
+$m->comp('/_elements/page_nav', page => $page->name);
+</%init>
+<&|/_elements/wrapper, title => $revisions->count ." revisions of " .$page->name &>
+<ul>
+% while (my $rev = $revisions->next) {
+<dt><% Jifty->web->link( label => $rev->created, 
+                          url => '/view/'.$page->name.'/'.$rev->id
+                        ) %></dt>
+<dd><%length($rev->content)%> bytes</dd>
+% }
+</ul>
+</&>
diff --git a/web/templates/view/dhandler b/web/templates/view/dhandler
index 242ca75..d6a528f 100644
--- a/web/templates/view/dhandler
+++ b/web/templates/view/dhandler
@@ -1,13 +1,28 @@
 <%init>
-my $name = $m->dhandler_arg();
+my $arg = $m->dhandler_arg();
+my ($name,$rev);
+if ($arg =~ qr{^(.*?)/?(\d?)$}) {
+    $name = $1;
+    $rev = $2;
+}
 my $page = Wifty::Model::Page->new();
 $page->load_by_cols( name => $name );
+
+my $revision = Wifty::Model::Revision->new();
+if ($rev) {
+    $revision->load_by_cols( page => $page->id, id => $rev);
+}
+
 unless ( $page->id ) {
     Jifty->web->redirect( '/create/' . $name );
 }
-my $top = Jifty->web->navigation;
-$top->child( Edit => url => '/edit/' . $page->name, sort_order => 5 );
+
+$m->comp('/_elements/page_nav', page => $page->name, rev => $rev);
 </%init>
 <&|/_elements/wrapper, title => $page->name &>
+% if ($revision->id) {
+<% $page->wiki_content($revision->content) |n%>
+% } else {
 <% $page->wiki_content |n %>
+% }
 </&>

commit 169b04efaac5e3432d9970961e1662d5a4bfecbb
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:31:48 2005 +0000

    oop. regex

diff --git a/web/templates/edit/dhandler b/web/templates/edit/dhandler
index 08d463f..e378789 100644
--- a/web/templates/edit/dhandler
+++ b/web/templates/edit/dhandler
@@ -1,7 +1,7 @@
 <%init>
 my $arg = $m->dhandler_arg();
 my ($name,$rev);
-if ($arg =~ qr{^(.*?)/?(\d?)$}) {
+if ($arg =~ qr{^(.*?)/?(\d*?)$}) {
     $name = $1;
     $rev = $2;
 }
diff --git a/web/templates/view/dhandler b/web/templates/view/dhandler
index d6a528f..8394e2b 100644
--- a/web/templates/view/dhandler
+++ b/web/templates/view/dhandler
@@ -1,7 +1,7 @@
 <%init>
 my $arg = $m->dhandler_arg();
 my ($name,$rev);
-if ($arg =~ qr{^(.*?)/?(\d?)$}) {
+if ($arg =~ qr{^(.*?)/?(\d*?)$}) {
     $name = $1;
     $rev = $2;
 }

commit 2590de549d7272d5c7d72c0f0653910f9e284923
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:32:01 2005 +0000

    nav

diff --git a/web/templates/_elements/page_nav b/web/templates/_elements/page_nav
index 8840b35..ecdc2b4 100644
--- a/web/templates/_elements/page_nav
+++ b/web/templates/_elements/page_nav
@@ -1,5 +1,5 @@
 <%init>
-my $subpath =  $page . ($rev ? "/rev/$rev" : '');
+my $subpath =  $page . ($rev ? "/$rev" : '');
 my $top = Jifty->web->navigation;
 my $this = $top->child( 
     This => 
@@ -20,7 +20,7 @@ $this->child(
 
 $this->child(
     History =>
-        url => '/history/'.$subpath
+        url => '/history/'.$page
     );
 
 </%init>

commit 0f0c11709236c0471f31dc09d76d17f1f19948d9
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:32:15 2005 +0000

    as of date

diff --git a/web/templates/edit/dhandler b/web/templates/edit/dhandler
index e378789..133554f 100644
--- a/web/templates/edit/dhandler
+++ b/web/templates/edit/dhandler
@@ -17,7 +17,7 @@ my $viewer = Jifty->web->new_action( class => 'UpdatePage', record => $page );
 $m->comp('/_elements/page_nav', page => $page->name, rev => $rev);
 
 </%init>
-<&|/_elements/wrapper, title => 'Edit: '.$page->name &>
+<&|/_elements/wrapper, title => 'Edit: '.$page->name . ($revision->id ? " as of ".$revision->date : '')  &>
 <% Jifty->web->form->start %>
 <% Jifty->web->form->next_page( url => '/view/'.$page->name) %>
 % if ($revision->id) {
diff --git a/web/templates/view/dhandler b/web/templates/view/dhandler
index 8394e2b..0f6c6fe 100644
--- a/web/templates/view/dhandler
+++ b/web/templates/view/dhandler
@@ -19,7 +19,7 @@ unless ( $page->id ) {
 
 $m->comp('/_elements/page_nav', page => $page->name, rev => $rev);
 </%init>
-<&|/_elements/wrapper, title => $page->name &>
+<&|/_elements/wrapper, title => $page->name . ($revision->id ? " as of ".$revision->date : '') &>
 % if ($revision->id) {
 <% $page->wiki_content($revision->content) |n%>
 % } else {

commit 9b5181b4c098f2efcc2427f98056a87c9cc96fac
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:32:27 2005 +0000

    as of date

diff --git a/web/templates/edit/dhandler b/web/templates/edit/dhandler
index 133554f..53b818d 100644
--- a/web/templates/edit/dhandler
+++ b/web/templates/edit/dhandler
@@ -17,7 +17,7 @@ my $viewer = Jifty->web->new_action( class => 'UpdatePage', record => $page );
 $m->comp('/_elements/page_nav', page => $page->name, rev => $rev);
 
 </%init>
-<&|/_elements/wrapper, title => 'Edit: '.$page->name . ($revision->id ? " as of ".$revision->date : '')  &>
+<&|/_elements/wrapper, title => 'Edit: '.$page->name . ($revision->id ? " as of ".$revision->created : '')  &>
 <% Jifty->web->form->start %>
 <% Jifty->web->form->next_page( url => '/view/'.$page->name) %>
 % if ($revision->id) {
diff --git a/web/templates/view/dhandler b/web/templates/view/dhandler
index 0f6c6fe..088d45b 100644
--- a/web/templates/view/dhandler
+++ b/web/templates/view/dhandler
@@ -19,7 +19,7 @@ unless ( $page->id ) {
 
 $m->comp('/_elements/page_nav', page => $page->name, rev => $rev);
 </%init>
-<&|/_elements/wrapper, title => $page->name . ($revision->id ? " as of ".$revision->date : '') &>
+<&|/_elements/wrapper, title => $page->name . ($revision->id ? " as of ".$revision->created : '') &>
 % if ($revision->id) {
 <% $page->wiki_content($revision->content) |n%>
 % } else {

commit 45db201a4be3ae528cb969d2830fb27fbdf57fef
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:32:40 2005 +0000

    goto latest

diff --git a/web/templates/_elements/page_nav b/web/templates/_elements/page_nav
index ecdc2b4..61993fd 100644
--- a/web/templates/_elements/page_nav
+++ b/web/templates/_elements/page_nav
@@ -8,20 +8,11 @@ my $this = $top->child(
         sort_order => 5
 );
 
-$this->child(
-    View =>
-        url => '/view/'.$subpath
-);
-
-$this->child(
-    Edit =>
-        url => '/edit/'.$subpath
-);
 
-$this->child(
-    History =>
-        url => '/history/'.$page
-    );
+$this->child( View => url => '/view/'.$subpath);
+$this->child( Edit => url => '/edit/'.$subpath);
+$this->child( History => url => '/history/'.$page);
+$this->child( Latest => url => '/view/'.$page) if ($rev);
 
 </%init>
 <%args>

commit 2c864ec5e9b5181aff6bb0245086c89ba2d0a4f1
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:32:54 2005 +0000

    move around the header styles

diff --git a/web/static/css/app-base.css b/web/static/css/app-base.css
index df6ab06..49800c3 100644
--- a/web/static/css/app-base.css
+++ b/web/static/css/app-base.css
@@ -4,7 +4,7 @@ body {
 
 }
 
-h1 {
+div#headers h1 {
  background: green;
  color: white;
  padding: 0.2em;

commit fc17cfa282b8e70c3f7adfa1175a5926ceeb0f72
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:33:08 2005 +0000

    updated wifty model class to include password support needed by new currentuser stuff

diff --git a/lib/Wifty/Action/Login.pm b/lib/Wifty/Action/Login.pm
new file mode 100644
index 0000000..2d5e4f8
--- /dev/null
+++ b/lib/Wifty/Action/Login.pm
@@ -0,0 +1,94 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Wifty::Action::Login
+
+=cut
+
+package Wifty::Action::Login;
+use base qw/Wifty::Action Jifty::Action/;
+
+=head2 arguments
+
+Return the email and password form fields
+
+=cut
+
+sub arguments { 
+    return( { email => { label => 'Email address',
+                           mandatory => 1,
+                           ajax_validates => 1,
+                            }  ,
+
+              password => { type => 'password',
+                            label => 'Password',
+                            mandatory => 1
+                        },
+              remember => { type => 'checkbox',
+                            label => 'Remember me?',
+                            hints => 'If you want, your browser can remember your login for you',
+                            default => 0,
+                          }
+          });
+
+}
+
+=head2 validate_email ADDRESS
+
+Makes sure that the email submitted is a legal email address and that there's a user in the database with it.
+
+
+=cut
+
+sub validate_email {
+    my $self  = shift;
+    my $email = shift;
+
+    unless ( $email =~ /\S\@\S/ ) {
+        return $self->validation_error(email => "That doesn't look like an email address." );
+    }
+
+    my $u = Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser);
+    $u->load_by_cols( email => $email );
+    return $self->validation_error(email => 'No account has that email address.') unless ($u->id);
+
+
+    return $self->validation_ok('email');
+}
+
+=head2 take_action
+
+Actually check the user's password. If it's right, log them in.
+Otherwise, throw an error.
+
+
+=cut
+
+sub take_action {
+    my $self = shift;
+    my $user = Wifty::CurrentUser->new( email => $self->argument_value('email'));
+
+    unless ( $user->id  && $user->password_is($self->argument_value('password'))) {
+        $self->result->error( 'You may have mistyped your email address or password. Give it another shot?' );
+        return;
+    }
+
+    unless ($user->user_object->email_confirmed) {
+        $self->result->error( q{You haven't <a href="/welcome/confirm.html">confirmed your account</a> yet.} );
+        return;
+    }
+
+    # Set up our login message
+    $self->result->message("Welcome back, " . $user->user_object->name . "." );
+
+    # Actually do the signin thing.
+    Jifty->web->current_user($user);
+    Jifty->web->session->expires($self->argument_value('remember') ? '+1y' : undef);
+    Jifty->web->session->set_cookie;
+
+    return 1;
+}
+
+1;
diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index 61cd92a..93d1651 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -15,6 +15,9 @@ column password =>,
     type is 'text',
     render_as 'password';
 
+column email_confirmed =>
+    type is 'boolean',
+    since '0.0.10';
 
 
 package Wifty::Model::User;
@@ -29,4 +32,30 @@ sub create {
     return($id);
 }
 
+
+=head2 password_is STRING
+
+Returns true if and only if the current user's password matches STRING
+
+=cut
+
+
+sub password_is {
+    my $self = shift;
+    my $string = shift;
+    warn "Checking $string";
+    return 1 if ($self->_value('password') eq $string);
+    return 0;
+}
+
+=head2 password
+
+Never display a password
+
+=cut
+
+sub password {
+    return undef;
+
+}
 1;

commit 2729ccbefb26575356e483196c33c2a98cb95a2d
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:33:21 2005 +0000

    Wifty::CurrentUser

diff --git a/lib/Wifty/CurrentUser.pm b/lib/Wifty/CurrentUser.pm
new file mode 100755
index 0000000..78e50fa
--- /dev/null
+++ b/lib/Wifty/CurrentUser.pm
@@ -0,0 +1,38 @@
+use warnings;
+use strict;
+
+
+package Wifty::CurrentUser;
+
+use base qw(Jifty::CurrentUser);
+
+=head2 new PARAMHASH
+
+Instantiate a new current user object, loading the user by paramhash:
+
+   my $item = Wifty::Model::Item->new( Wifty::CurrentUser->new(email => 'user at site'));
+
+if you give the param 
+    _bootstrap => 1
+
+your object will be marked as a bootstrap user. You can use that to do an endrun around acls.
+
+=cut
+
+
+
+sub _init {
+    my $self = shift;
+    my %args = (@_);
+
+    if (delete $args{'_bootstrap'} ) {
+        $self->is_bootstrap_user(1);
+    } elsif (keys %args) {
+        $self->user_object(Wifty::Model::User->new(current_user => $self));
+        $self->user_object->load_by_cols(%args);
+    }
+    $self->SUPER::_init(%args);
+}
+
+
+1;

commit 832077c5f3ff99f91eb7027712d7b50c4e8e9ed7
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:33:36 2005 +0000

    login page

diff --git a/etc/config.yml b/etc/config.yml
index ba02778..b821631 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -1,11 +1,11 @@
 framework:
-  AdminMode: 0
+  AdminMode: 1
   LogConfig: etc/btdt.log4perl.conf
   Database:
     Driver: Pg
     Host: localhost
     User: postgres
-    Version: 0.0.8
+    Version: 0.0.10
     Password: ''
     RequireSSL: 0
 #  Mailer: IO
diff --git a/web/templates/_elements/nav b/web/templates/_elements/nav
index 5baff86..e89df55 100644
--- a/web/templates/_elements/nav
+++ b/web/templates/_elements/nav
@@ -1,16 +1,11 @@
 <%init>
 my $top = Jifty->web->navigation;
-$top->child( 
-    Home => 
-        url => "/", 
-        sort_order => 1 
-);
-$top->child(
-    Recent  =>
-        url => "/recent",
-        label      => "Recent Changes",
-        sort_order => 2
-);
+$top->child( Home => url => "/", sort_order => 1 );
+$top->child( Recent  => url => "/recent", label      => "Recent Changes", sort_order => 2);
+ if (Jifty->config->framework('AdminMode') ) {
+     $top->child(Administration       => url => "/__jifty/admin/", sort_order => 998);
+     $top->child(OnlineDocs       => url => "/__jifty/online_docs/", label => 'Online docs',  sort_order => 999);
+}
 
 return();
 </%init>
diff --git a/web/templates/login b/web/templates/login
new file mode 100755
index 0000000..e29d3f8
--- /dev/null
+++ b/web/templates/login
@@ -0,0 +1,18 @@
+<%init> 
+if (Jifty->web->current_user->id) {
+    $m->comp('/_elements/logged_in_already');
+    return();
+}
+my $action = Jifty->web->new_action(class => 'Login', moniker => 'loginbox' );
+
+my $next = Jifty->web->request->continuation || Jifty::Continuation->new(request => Jifty::Request->new(path => "/"));
+</%init>
+<&|/_elements/wrapper, title => 'Login' &>
+<h2>Login</h2>
+<% Jifty->web->form->start(call => $next, name => "loginbox") %>
+<% $action->form_field('email') %>
+<% $action->form_field('password') %>
+<% $action->form_field('remember') %>
+<% Jifty->web->form->submit(label => 'Login', submit => $action) %>
+<% Jifty->web->form->end %>
+</&>

commit 3b0de9a62c20b24b77618b15144266009f55a64b
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:33:51 2005 +0000

    Fixed some of the worst style errors

diff --git a/web/static/css/app-base.css b/web/static/css/app-base.css
index 49800c3..638d94b 100644
--- a/web/static/css/app-base.css
+++ b/web/static/css/app-base.css
@@ -24,50 +24,11 @@ a {
  display: none;
 }
 
-div#menu {
- background: green;
-
-}
-
-div#sidebar {
- float: left;
-
-
-}
-
 div#content {
     background: #ffffff;
    padding: 2em;
 }
 
-
-ul.menu {
-    display: block;
-    border-bottom: 2px solid black;
-    padding-bottom: 4px;
-    width: 100%;
-    margin-left: 0px;
-    margin-right: 0px;
-
-}
-
-ul.menu li {
-
-    background: #770077;
-    color:#ffffff;
-    text-align: center;
-    display: inline;
-    padding: 0.4em 0.4em 4px 0.4em;
-    border-bottom: 2px solid black;
-;
-     
-}
-
-ul.menu li a {
- color: #ffffff;
-
-}
-
 div#salutation {
     float: right;
     font-style: italic;
@@ -80,16 +41,10 @@ textarea.content {
   padding: 5px;
 }
 
-label {
-  position: absolute;
-  top: 3.4em;
-  left: 1em;
-  font-size: 2em;
-}
 
 input[type=submit] {
     border: 1px solid black;
-    font-size: 2em;
+    font-size: 1.5em;
     margin: 5px;
         
 }

commit 258a05a50d19f932f9b21426866957509b482416
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:34:03 2005 +0000

    ./lib/Jifty/DefaultApp moved to ./share
     * Added magic symlink to ./lib/Jifty/auto/Jifty to make share dir
       accessible during dev work
     * Re-spec application root dir

diff --git a/bin/jifty b/bin/jifty
index 0ba9b77..1e40ae7 100755
--- a/bin/jifty
+++ b/bin/jifty
@@ -5,9 +5,9 @@ use File::Basename qw(dirname);
 
 BEGIN {
     my $dir = dirname(__FILE__); 
-    push @INC, "$dir/../lib";
+    unshift @INC, "$dir/../../Jifty/lib";
+    unshift @INC, "$dir/../lib";
     push @INC, "$dir/../../Jifty/deps";
-    push @INC, "$dir/../../Jifty/lib";
 }
 
 use Jifty::Script;

commit 7eb8241953bf8fc723c589f498e677189e0651e8
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:34:16 2005 +0000

    ACL cleanups. HM should now be acled correctly

diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index bd5344e..b1fc3a6 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -13,7 +13,7 @@ column created =>
 
 package Wifty::Model::Revision;
 use base qw/Wifty::Record/;
-
+use Jifty::RightsFrom column => 'page';
 use DateTime;
 
 

commit 273f72ddec35508bbe9a7564c5ee9b304eb3c3ed
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:34:30 2005 +0000

    By default, Jifty applications let superuser and bootstrap users do aaaaaaanything.
      And regular users can't do anything.

diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index 93d1651..af89ea5 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -43,7 +43,6 @@ Returns true if and only if the current user's password matches STRING
 sub password_is {
     my $self = shift;
     my $string = shift;
-    warn "Checking $string";
     return 1 if ($self->_value('password') eq $string);
     return 0;
 }
@@ -58,4 +57,21 @@ sub password {
     return undef;
 
 }
+
+
+sub current_user_can {
+    my $self = shift;
+    my $right = shift;
+    my %args = (@_);
+
+    if ($right eq 'read')  {
+
+    } elsif ($right eq 'write') {
+
+    }
+
+    return $self->SUPER::current_user_can($right, %args);
+}
+
+
 1;

commit bbac78c301525b206e2fac8c12691297f4aa31ad
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:34:43 2005 +0000

    Remove Jifty::Script::Command (pushed into App::CLI::Command)
     * Make Jifty::Script::App build basic framework
     * Extract most of Jifty::Script::Help into App::CLI::Command::Help
     * Allow --port option to Jifty::Script::Server

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 996f439..88d3002 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -25,6 +25,7 @@ use Wifty::Model::RevisionCollection;
 use Text::Markdown;
 use HTML::Scrubber;
 
+
 =head2 wiki_content [CONTENT]
 
 Wikify either the content of a scalar passed in as an argument or
@@ -55,7 +56,7 @@ sub wiki_content {
     $scrubber->allow(
         qw[A B U P BR I HR BR SMALL EM FONT SPAN DIV UL OL LI DL DT DD]);
     $scrubber->comment(0);
-    return ( markdown( $scrubber->scrub( $content ) ) );
+    return ( $scrubber->scrub( markdown( $content ) ) );
 
 }
 
diff --git a/web/templates/view/dhandler b/web/templates/view/dhandler
index 088d45b..5ce5e8b 100644
--- a/web/templates/view/dhandler
+++ b/web/templates/view/dhandler
@@ -1,4 +1,5 @@
 <%init>
+use Wifty::Model::Page;
 my $arg = $m->dhandler_arg();
 my ($name,$rev);
 if ($arg =~ qr{^(.*?)/?(\d*?)$}) {

commit 31a443aeb7b908bab232e9e6ef6b34fa150cbe87
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:34:57 2005 +0000

    Wifty does acls now

diff --git a/lib/Wifty/Bootstrap.pm b/lib/Wifty/Bootstrap.pm
index 5026527..4f95051 100644
--- a/lib/Wifty/Bootstrap.pm
+++ b/lib/Wifty/Bootstrap.pm
@@ -5,7 +5,7 @@ use Wifty::Model::Page;
 sub run {
     my $self = shift;
 
-    my $index = Wifty::Model::Page->new();
+    my $index = Wifty::Model::Page->new( current_user => Wifty::CurrentUser->superuser () );
     $index->create(
         name    => 'HomePage',
         content => 'Welcome to your Wifty'

commit 72653728486cbb96520571f5d4447f92ed31cf9a
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:35:13 2005 +0000

    First draft. we _clearly_ need to refactor out the email validator, if nothing else.

diff --git a/lib/Wifty/Action/ConfirmAccount.pm b/lib/Wifty/Action/ConfirmAccount.pm
new file mode 100644
index 0000000..0498579
--- /dev/null
+++ b/lib/Wifty/Action/ConfirmAccount.pm
@@ -0,0 +1,62 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Wifty::Action::ConfirmEmail - Confirm a user's email address
+
+=head1 DESCRIPTION
+
+This is the link in a user's email to confirm that their email
+email is really theirs.  It is not really meant to be rendered on any
+web page, but is used by the confirmation notification.
+
+Note that the use of C<insecure_url_auth_token> here is insecure and wrong!
+(XXX TODO FIXME) If an attacker knew the token calculation algorithm (including
+the non-random salt), they could easily do email confirmation without needed
+to actually have access to the email account, since the algorithm only depends on
+the email address, requested password, and non-random salt.
+
+=cut
+
+package Wifty::Action::ConfirmEmail;
+use base qw/Wifty::Action Jifty::Action/;
+
+use Wifty::Model::User;
+
+
+=head2 actions
+
+A null sub, because the superclass wants to make sure we fill in actions
+
+=cut
+
+sub actions {}
+
+=head2 take_action
+
+Set their confirmed status.
+
+=cut
+
+sub take_action {
+    my $self = shift;
+    my $u = Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser);
+    $u->load_by_cols( email => Jifty->web->current_user->user_object->email );
+
+    if ($u->email_confirmed) {
+        $self->result->error(email => "You have already confirmed your account.");
+        $self->result->success(1);  # but the action is still a success
+    }
+
+    $u->set_email_confirmed('1');
+
+    # Set up our login message
+    $self->result->message( "Welcome to Wifty, " . $u->name . ". Your email address has now been confirmed." );
+
+    # Actually do the login thing.
+    Jifty->web->current_user(Wifty::CurrentUser->new(id => $u->id));
+    return 1;
+}
+
+1;
diff --git a/lib/Wifty/Action/RecoverPassword.pm b/lib/Wifty/Action/RecoverPassword.pm
new file mode 100644
index 0000000..7f6f0c3
--- /dev/null
+++ b/lib/Wifty/Action/RecoverPassword.pm
@@ -0,0 +1,7 @@
+
+use warnings;
+use strict;
+
+package Wifty::Action::RecoverPassword;
+
+1;
diff --git a/lib/Wifty/Action/ResetLostPassword.pm b/lib/Wifty/Action/ResetLostPassword.pm
new file mode 100755
index 0000000..813a781
--- /dev/null
+++ b/lib/Wifty/Action/ResetLostPassword.pm
@@ -0,0 +1,70 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Wifty::Action::ResetPassword - Confirm and reset a lost password
+
+=head1 DESCRIPTION
+
+This is the action run by the link in a user's email to confirm that their email
+address is really theirs, when claiming that they lost their password.  
+
+
+=cut
+
+package Wifty::Action::ResetPassword;
+use base qw/Wifty::Action Jifty::Action/;
+
+use Wifty::Model::User;
+
+=head2 arguments
+
+ConfirmEmail has the following fields: address, code, password, and password_confirm.
+Note that it can get the first two from the confirm dhandler.
+
+=cut
+
+sub arguments { 
+    return( { 
+              password => { type => 'password', sticky => 0 },
+              password_confirm => { type => 'password', sticky => 0, label => 'type your password again' },
+          }); 
+}
+
+=head2 take_action
+
+Resets the password.
+
+=cut
+
+sub take_action {
+    my $self = shift;
+    my $u = Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser);
+    $u->load_by_cols( email => Jifty->web->current_user->user_object->email );
+
+    unless ($u) {
+        $self->result->error( "You don't exist. I'm not sure how this happened. Really, really sorry. Please email us!");
+    } 
+
+    my $pass = $self->argument_value('password');
+    my $pass_c = $self->argument_value('password_confirm');
+
+    # Trying to set a password (ie, submitted the form)
+    unless (defined $pass and defined $pass_c and length $pass and $pass eq $pass_c) {
+        $self->result->error("It looks like you didn't enter the same password into both boxes. Give it another shot?");
+        return;
+    } 
+
+    unless ($u->set_password($pass)) {
+        $self->result->error("There was an error setting your password.");
+        return;
+    } 
+    # Log in!
+    $self->result->message( "Your password has been reset.  Welcome back." );
+    Jifty->web->current_user(Wifty::CurrentUser->new(id => $u->id));
+    return 1;
+
+}
+
+1;
diff --git a/lib/Wifty/Action/SendAccountConfrimation.pm b/lib/Wifty/Action/SendAccountConfrimation.pm
new file mode 100755
index 0000000..dc30667
--- /dev/null
+++ b/lib/Wifty/Action/SendAccountConfrimation.pm
@@ -0,0 +1,78 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Wifty::Action::ResendConfirmation
+
+=cut
+
+package Wifty::Action::ResendConfirmation;
+use base qw/Wifty::Action Jifty::Action/;
+
+__PACKAGE__->mk_accessors(qw(user_object));
+
+use Wifty::Model::User;
+
+=head2 arguments
+
+The field for C<ResendConfirmation> is:
+
+=over 4
+
+=item address: the email address
+
+=back
+
+=cut
+
+sub arguments {
+    return ( { address => { label     => 'email address', mandatory => 1, default_value => "", }, });
+}
+
+=head2 setup
+
+Create an empty user object to work with
+
+=cut
+
+sub setup {
+    my $self = shift;
+    
+    $self->user_object(Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser));
+}
+
+=head2 validate_address
+
+Make sure their email address is an unconfirmed user.
+
+=cut
+
+sub validate_address {
+    my $self  = shift;
+    my $email = shift;
+
+    return $self->validation_error(address => "That doesn't look like an email address." ) unless ( $email =~ /\S\@\S/ );
+
+    $self->user_object(Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser));
+    $self->user_object->load_by_cols( email => $email );
+    return $self->validation_error(address => "It doesn't look like there's an account by that name.") unless ($self->user_object->id);
+
+    return $self->validation_error(address => "It looks like you're already confirmed.") if ($self->user_object->email_confirmed);
+
+    return $self->validation_ok('address');
+}
+
+=head2 take_action
+
+Create a new unconfirmed user and send out a confirmation email.
+
+=cut
+
+sub take_action {
+    my $self = shift;
+    Wifty::Notification::ConfirmAddress->new( to => $self->user_object )->send;
+    return $self->result->message("Confirmation resent.");
+}
+
+1;
diff --git a/lib/Wifty/Action/SendPasswordReminder.pm b/lib/Wifty/Action/SendPasswordReminder.pm
new file mode 100755
index 0000000..dde794c
--- /dev/null
+++ b/lib/Wifty/Action/SendPasswordReminder.pm
@@ -0,0 +1,87 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Wifty::Action::SendLostPasswordConfirmation
+
+=cut
+
+package Wifty::Action::SendLostPasswordConfirmation;
+use base qw/Wifty::Action Jifty::Action/;
+
+__PACKAGE__->mk_accessors(qw(user_object));
+
+use Wifty::Model::User;
+
+=head2 arguments
+
+The field for C<SendLostPasswordConfirmation> is:
+
+=over 4
+
+=item address: the email address
+
+=back
+
+=cut
+
+sub arguments {
+    return (
+        {
+            address => {
+                label     => 'email address',
+                mandatory => 1,
+            },
+        }
+    );
+
+}
+
+=head2 setup
+
+Create an empty user object to work with
+
+=cut
+
+sub setup {
+    my $self = shift;
+    
+    # Make a blank user object
+    $self->user_object(Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser));
+}
+
+=head2 validate_address
+
+Make sure there's actually an account by that name.
+
+=cut
+
+sub validate_address {
+    my $self  = shift;
+    my $email = shift;
+
+        return $self->validation_error(address => "That doesn't look like an email address." )
+    unless ( $email =~ /\S\@\S/ ) 
+
+    $self->user_object(Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser));
+    $self->user_object->load_by_cols( email => $email );
+        return $self->validation_error(address => "It doesn't look like there's an account by that name.")
+    unless ($self->user_object->id);
+
+    return $self->validation_ok('address');
+}
+
+=head2 take_action
+
+Send out a confirmation email giving a link to a password-reset form.
+
+=cut
+
+sub take_action {
+    my $self = shift;
+    Wifty::Notification::ConfirmLostPassword->new( to => $self->user_object )->send;
+    return $self->result->message("A link to reset your password has been sent to your email account.");
+}
+
+1;
diff --git a/lib/Wifty/Action/Signup.pm b/lib/Wifty/Action/Signup.pm
new file mode 100644
index 0000000..1fc264c
--- /dev/null
+++ b/lib/Wifty/Action/Signup.pm
@@ -0,0 +1,111 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Wifty::Action::Signup
+
+=cut
+
+package Wifty::Action::Signup;
+use Wifty::Action::CreateUser;
+use base qw/Wifty::Action::CreateUser/;
+
+
+use Wifty::Model::User;
+
+=head2 arguments
+
+
+The fields for C<Signup> are:
+
+=over 4
+
+=item email: the email address
+
+=item password and password_confirm: the requested password
+
+=item name: your full name
+
+=back
+
+=cut
+
+sub arguments {
+    my $self = shift;
+    my $args = $self->SUPER::arguments;
+
+    my %fields = ( 
+        email                        => 1,
+        likes_ticky_boxes            => 1,
+        name                         => 1,
+        never_email                  => 1,
+        notification_email_frequency => 1,
+        password                     => 1,
+        password_confirm             => 1,
+    );
+
+    for ( keys %$args ) { delete $args->{$_} unless ( $fields{$_} ); }
+    $args->{'email'}{'ajax_validates'} = 1;
+    $args->{'password_confirm'}{'label'} = "Type that again?";
+    return $args;
+}
+
+
+=head2 validate_email
+
+Make sure their email address looks sane
+
+=cut
+
+sub validate_email {
+    my $self  = shift;
+    my $email = shift;
+
+    unless ( $email =~ /\S\@\S/ ) {
+        return $self->validation_error(email => "That doesn't look like an email address." );
+    }
+
+    my $u = Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser);
+    $u->load_by_cols( email => $email );
+    if ($u->id) {
+      return $self->validation_error(email => 'It looks like you already have an account. Perhaps you want to <a href="/welcome/">sign in</a> instead?');
+    }
+
+    return $self->validation_ok('email');
+}
+
+
+
+=head2 take_action
+
+Overrides the virtual C<take_action> method on L<Jifty::Action> to call
+the appropriate C<Jifty::Record>'s C<create> method when the action is
+run, thus creating a new object in the database.
+
+Makes sure that the user only specifies things we want them to.
+
+=cut
+
+sub take_action {
+    my $self   = shift;
+    my $record = Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser);
+
+    my %values;
+    $values{$_} = $self->argument_value($_)
+      for grep { defined $self->record->column($_) and defined $self->argument_value($_) } $self->argument_names;
+    
+    my ($id) = $record->create(%values);
+    # Handle errors?
+    unless ( $record->id ) {
+        $self->result->error("Something bad happened and we couldn't create your account.  Try again later");
+        return;
+    }
+
+    $self->result->message( "Welcome to Wifty, " . $record->name .".");
+
+
+    return 1;
+}
+
+1;

commit c4a48ede445d81debfe3c637f76e71b8422fa782
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:35:27 2005 +0000

    Fix compiler error in SendPasswordReminder
     * Fix possible undef warning
     * Very premissive ACLs

diff --git a/lib/Wifty/Action/SendPasswordReminder.pm b/lib/Wifty/Action/SendPasswordReminder.pm
index dde794c..4c643d6 100755
--- a/lib/Wifty/Action/SendPasswordReminder.pm
+++ b/lib/Wifty/Action/SendPasswordReminder.pm
@@ -61,8 +61,8 @@ sub validate_address {
     my $self  = shift;
     my $email = shift;
 
-        return $self->validation_error(address => "That doesn't look like an email address." )
-    unless ( $email =~ /\S\@\S/ ) 
+    return $self->validation_error(address => "That doesn't look like an email address." )
+      unless ( $email =~ /\S\@\S/ );
 
     $self->user_object(Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser));
     $self->user_object->load_by_cols( email => $email );
diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 88d3002..2b7d02b 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -56,7 +56,7 @@ sub wiki_content {
     $scrubber->allow(
         qw[A B U P BR I HR BR SMALL EM FONT SPAN DIV UL OL LI DL DT DD]);
     $scrubber->comment(0);
-    return ( $scrubber->scrub( markdown( $content ) ) );
+    return ( $scrubber->scrub( markdown( $content || '') ) );
 
 }
 
@@ -115,4 +115,19 @@ sub _set {
     return ( $val, $msg );
 }
 
+sub current_user_can {
+    my $self = shift;
+    my ($type) = @_;
+
+    # TODO FIXME: For now..
+    return 1;
+
+    # We probably want something like this eventually:
+    if ($type eq "read") {
+        return 1;
+    } else {
+        return $self->current_user->id;
+    }
+}
+
 1;

commit ce6d4960ee06b679a8389439a7e1e82641aa62b5
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:35:40 2005 +0000

    No, this order is still wrong, but less wrong than the other way.
    I think.  The problem is that scrubber escapes < and > once, and then
    markdown escapes the &'s therein.

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 2b7d02b..59e7384 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -56,7 +56,7 @@ sub wiki_content {
     $scrubber->allow(
         qw[A B U P BR I HR BR SMALL EM FONT SPAN DIV UL OL LI DL DT DD]);
     $scrubber->comment(0);
-    return ( $scrubber->scrub( markdown( $content || '') ) );
+    return ( markdown( $scrubber->scrub( $content || '') ) );
 
 }
 

commit eed56064f973164948a159028cb24f4860d9f1be
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:35:51 2005 +0000

    Only init the log4perl object once
     * Updated deps in META.yaml
     * Output new testing code for model classes
     * Make generated Makefile.PL actually work
     * SchemaTool only shows errors

diff --git a/etc/config.yml b/etc/config.yml
index b821631..a589501 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -1,6 +1,5 @@
 framework:
   AdminMode: 1
-  LogConfig: etc/btdt.log4perl.conf
   Database:
     Driver: Pg
     Host: localhost

commit 37e821d912bd81a11cfe0c38ad15ec3d0f64f621
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:36:04 2005 +0000

    Makefile and tests

diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644
index 0000000..89bf3d4
--- /dev/null
+++ b/Makefile.PL
@@ -0,0 +1,6 @@
+use inc::Module::Install;
+name('Wifty');
+version('0.01');
+requires('Jifty');
+
+WriteAll;
diff --git a/t/00-model-Page.t b/t/00-model-Page.t
new file mode 100644
index 0000000..617861e
--- /dev/null
+++ b/t/00-model-Page.t
@@ -0,0 +1,49 @@
+#!/usr/bin/perl -w
+use warnings;
+use strict;
+
+=head1 DESCRIPTION
+
+A basic test harness for the Page model.
+
+=cut
+
+use Jifty::Test tests => 11;
+
+# Make sure we can load the model
+use_ok('Wifty::Model::Page');
+
+# Grab a system use
+my $system_user = Wifty::CurrentUser->superuser;
+ok($system_user, "Found a system user");
+
+# Try testing a create
+my $o = Wifty::Model::Page->new(current_user => $system_user);
+my ($id) = $o->create(name => "Something");
+ok($id, "Page create returned success");
+ok($o->id, "New Page has valid id set");
+is($o->id, $id, "Create returned the right id");
+
+# And another
+$o->create(name => "Something else");
+ok($o->id, "Page create returned another value");
+isnt($o->id, $id, "And it is different from the previous one");
+
+# Searches in general
+my $collection =  Wifty::Model::PageCollection->new(current_user => $system_user);
+$collection->unlimit;
+is($collection->count, 3, "Finds three records");
+
+# Searches in specific
+$collection->limit(column => 'id', value => $o->id);
+is($collection->count, 1, "Finds one record with specific id");
+
+# Delete one of them
+$o->delete;
+$collection->redo_search;
+is($collection->count, 0, "Deleted row is gone");
+
+# And the other one is still there
+$collection->unlimit;
+is($collection->count, 2, "Still two left");
+
diff --git a/t/00-model-Revision.t b/t/00-model-Revision.t
new file mode 100644
index 0000000..9da02b8
--- /dev/null
+++ b/t/00-model-Revision.t
@@ -0,0 +1,49 @@
+#!/usr/bin/perl -w
+use warnings;
+use strict;
+
+=head1 DESCRIPTION
+
+A basic test harness for the Revision model.
+
+=cut
+
+use Jifty::Test tests => 11;
+
+# Make sure we can load the model
+use_ok('Wifty::Model::Revision');
+
+# Grab a system use
+my $system_user = Wifty::CurrentUser->superuser;
+ok($system_user, "Found a system user");
+
+# Try testing a create
+my $o = Wifty::Model::Revision->new(current_user => $system_user);
+my ($id) = $o->create();
+ok($id, "Revision create returned success");
+ok($o->id, "New Revision has valid id set");
+is($o->id, $id, "Create returned the right id");
+
+# And another
+$o->create();
+ok($o->id, "Revision create returned another value");
+isnt($o->id, $id, "And it is different from the previous one");
+
+# Searches in general
+my $collection =  Wifty::Model::RevisionCollection->new(current_user => $system_user);
+$collection->unlimit;
+is($collection->count, 3, "Finds three records");
+
+# Searches in specific
+$collection->limit(column => 'id', value => $o->id);
+is($collection->count, 1, "Finds one record with specific id");
+
+# Delete one of them
+$o->delete;
+$collection->redo_search;
+is($collection->count, 0, "Deleted row is gone");
+
+# And the other one is still there
+$collection->unlimit;
+is($collection->count, 2, "Still two left");
+
diff --git a/t/00-model-User.t b/t/00-model-User.t
new file mode 100644
index 0000000..06b838a
--- /dev/null
+++ b/t/00-model-User.t
@@ -0,0 +1,49 @@
+#!/usr/bin/perl -w
+use warnings;
+use strict;
+
+=head1 DESCRIPTION
+
+A basic test harness for the User model.
+
+=cut
+
+use Jifty::Test tests => 11;
+
+# Make sure we can load the model
+use_ok('Wifty::Model::User');
+
+# Grab a system use
+my $system_user = Wifty::CurrentUser->superuser;
+ok($system_user, "Found a system user");
+
+# Try testing a create
+my $o = Wifty::Model::User->new(current_user => $system_user);
+my ($id) = $o->create(email => 'an at email', name => 'name');
+ok($id, "User create returned success");
+ok($o->id, "New User has valid id set");
+is($o->id, $id, "Create returned the right id");
+
+# And another
+$o->create(email => 'some at mail', name => 'another');
+ok($o->id, "User create returned another value");
+isnt($o->id, $id, "And it is different from the previous one");
+
+# Searches in general
+my $collection =  Wifty::Model::UserCollection->new(current_user => $system_user);
+$collection->unlimit;
+is($collection->count, 2, "Finds two records");
+
+# Searches in specific
+$collection->limit(column => 'id', value => $o->id);
+is($collection->count, 1, "Finds one record with specific id");
+
+# Delete one of them
+$o->delete;
+$collection->redo_search;
+is($collection->count, 0, "Deleted row is gone");
+
+# And the other one is still there
+$collection->unlimit;
+is($collection->count, 1, "Still one left");
+

commit 3c7dbb168fa6e9fdcbfb54b7f7b0176737482873
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:36:17 2005 +0000

    Updated how delegated acl decisions happen

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 59e7384..e584723 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -115,18 +115,22 @@ sub _set {
     return ( $val, $msg );
 }
 
+
+=head2 current_user_can ACTION
+
+Let everybody create, read and update pages, but not delete the.
+
+=cut
+
 sub current_user_can {
     my $self = shift;
-    my ($type) = @_;
-
-    # TODO FIXME: For now..
-    return 1;
+    my $type = shift;
 
     # We probably want something like this eventually:
-    if ($type eq "read") {
+    if ($type =~ /(?:create|read|update)/i) {
         return 1;
     } else {
-        return $self->current_user->id;
+        return $self->SUPER::current_user_can($type, @_);
     }
 }
 
diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index b1fc3a6..7d44259 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -30,4 +30,24 @@ sub create {
 
 }
 
+=head2 current_user_can RIGHT
+
+We're using L<Jifty::RightsFrom> to pass off ACL decisions to this
+update's page.  But we need to make sure that page history entries aren't
+editable, except by superusers. So we override C<current_user_can>
+to give the arguments a brief massage before handing off to
+C<urrent_user_can> (which we inherit).
+
+=cut
+
+sub current_user_can {
+    my $self = shift;
+    my $right = shift;
+    
+    if ($right ne 'read' and not $self->current_user->is_superuser) {
+        return 0;
+    }
+    $self->SUPER::current_user_can($right, @_);
+
+}
 1;

commit 3e3618025f43de516c690910ad3473b305e4212f
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:36:31 2005 +0000

    revisions are created by the superuser

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index e584723..63d1e27 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -88,7 +88,7 @@ sub _add_revision {
     my $self = shift;
     my %args = (@_);
 
-    my $rev = Wifty::Model::Revision->new();
+    my $rev = Wifty::Model::Revision->new( current_user => Wifty::CurrentUser->superuser);
     $rev->create(
         page    => $self->id,
         content => $args{'content'}

commit 43139ed8c3f03fd9e82bbb1efcb974361215066e
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:36:46 2005 +0000

    Wifty really lets you login

diff --git a/lib/Wifty/Action/Login.pm b/lib/Wifty/Action/Login.pm
index 2d5e4f8..ed12a5a 100644
--- a/lib/Wifty/Action/Login.pm
+++ b/lib/Wifty/Action/Login.pm
@@ -76,7 +76,7 @@ sub take_action {
     }
 
     unless ($user->user_object->email_confirmed) {
-        $self->result->error( q{You haven't <a href="/welcome/confirm.html">confirmed your account</a> yet.} );
+        $self->result->error( q{You haven't confirmed your account yet.} );
         return;
     }
 
diff --git a/lib/Wifty/Action/Signup.pm b/lib/Wifty/Action/Signup.pm
index 1fc264c..6e73fc2 100644
--- a/lib/Wifty/Action/Signup.pm
+++ b/lib/Wifty/Action/Signup.pm
@@ -36,11 +36,8 @@ sub arguments {
     my $args = $self->SUPER::arguments;
 
     my %fields = ( 
+        name                        => 1,
         email                        => 1,
-        likes_ticky_boxes            => 1,
-        name                         => 1,
-        never_email                  => 1,
-        notification_email_frequency => 1,
         password                     => 1,
         password_confirm             => 1,
     );
@@ -62,14 +59,13 @@ sub validate_email {
     my $self  = shift;
     my $email = shift;
 
-    unless ( $email =~ /\S\@\S/ ) {
-        return $self->validation_error(email => "That doesn't look like an email address." );
-    }
+        return $self->validation_error(email => "That doesn't look like an email address." )
+    unless ( $email =~ /\S\@\S/ ) ;
 
     my $u = Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser);
     $u->load_by_cols( email => $email );
     if ($u->id) {
-      return $self->validation_error(email => 'It looks like you already have an account. Perhaps you want to <a href="/welcome/">sign in</a> instead?');
+      return $self->validation_error(email => 'It looks like you already have an account. Perhaps you want to <a href="/login">sign in</a> instead?');
     }
 
     return $self->validation_ok('email');
@@ -98,11 +94,11 @@ sub take_action {
     my ($id) = $record->create(%values);
     # Handle errors?
     unless ( $record->id ) {
-        $self->result->error("Something bad happened and we couldn't create your account.  Try again later");
+        $self->result->error("Something bad happened and we couldn't create your account.  Try again later. We're really, really sorry.");
         return;
     }
 
-    $self->result->message( "Welcome to Wifty, " . $record->name .".");
+    $self->result->message( "Welcome to Wifty, " . $record->name .". We've sent a confirmation message to your email box.");
 
 
     return 1;
diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index af89ea5..d48e537 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -22,14 +22,19 @@ column email_confirmed =>
 
 package Wifty::Model::User;
 use base qw/Wifty::Record/;
+use Wifty::Notification::ConfirmAddress;
 
 sub since {'0.0.7'}
 
 sub create {
     my $self = shift;
     my %args = (@_);
-    my ($id) = $self->SUPER::create(%args);
-    return($id);
+    my (@ret) = $self->SUPER::create(%args);
+
+    if ($self->id and not $self->email_confirmed) {
+        Wifty::Notification::ConfirmAddress->new( to => $self )->send;
+    }
+    return (@ret);
 }
 
 
@@ -63,10 +68,10 @@ sub current_user_can {
     my $self = shift;
     my $right = shift;
     my %args = (@_);
-
+    return(1);
     if ($right eq 'read')  {
 
-    } elsif ($right eq 'write') {
+    } elsif ($right eq 'update') {
 
     }
 
diff --git a/web/templates/login b/web/templates/login
index e29d3f8..becc8aa 100755
--- a/web/templates/login
+++ b/web/templates/login
@@ -15,4 +15,5 @@ my $next = Jifty->web->request->continuation || Jifty::Continuation->new(request
 <% $action->form_field('remember') %>
 <% Jifty->web->form->submit(label => 'Login', submit => $action) %>
 <% Jifty->web->form->end %>
+<% Jifty->web->tangent( label => q{Don't have an account?}, url => '/signup' )%>
 </&>

commit b2a2c780478608838ce790eb64ceaaf01d3bbf08
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:36:59 2005 +0000

    Checkpoint

diff --git a/lib/Wifty/Action/ConfirmAccount.pm b/lib/Wifty/Action/ConfirmAccount.pm
index 0498579..bbd6317 100644
--- a/lib/Wifty/Action/ConfirmAccount.pm
+++ b/lib/Wifty/Action/ConfirmAccount.pm
@@ -11,7 +11,7 @@ This is the link in a user's email to confirm that their email
 email is really theirs.  It is not really meant to be rendered on any
 web page, but is used by the confirmation notification.
 
-Note that the use of C<insecure_url_auth_token> here is insecure and wrong!
+Note that the use of C<auth_token> here is insecure and wrong!
 (XXX TODO FIXME) If an attacker knew the token calculation algorithm (including
 the non-random salt), they could easily do email confirmation without needed
 to actually have access to the email account, since the algorithm only depends on

commit 48e2e1cbccb2c87d14cb057fd4577da45a79363b
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:37:13 2005 +0000

    users can log in and log out of wifty and sign up for accounts.

diff --git a/etc/config.yml b/etc/config.yml
index a589501..514c1e8 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -4,7 +4,7 @@ framework:
     Driver: Pg
     Host: localhost
     User: postgres
-    Version: 0.0.10
+    Version: 0.0.15
     Password: ''
     RequireSSL: 0
 #  Mailer: IO
diff --git a/lib/Wifty/Action/ConfirmAccount.pm b/lib/Wifty/Action/ConfirmEmail.pm
similarity index 78%
rename from lib/Wifty/Action/ConfirmAccount.pm
rename to lib/Wifty/Action/ConfirmEmail.pm
index bbd6317..c3261a7 100644
--- a/lib/Wifty/Action/ConfirmAccount.pm
+++ b/lib/Wifty/Action/ConfirmEmail.pm
@@ -11,12 +11,6 @@ This is the link in a user's email to confirm that their email
 email is really theirs.  It is not really meant to be rendered on any
 web page, but is used by the confirmation notification.
 
-Note that the use of C<auth_token> here is insecure and wrong!
-(XXX TODO FIXME) If an attacker knew the token calculation algorithm (including
-the non-random salt), they could easily do email confirmation without needed
-to actually have access to the email account, since the algorithm only depends on
-the email address, requested password, and non-random salt.
-
 =cut
 
 package Wifty::Action::ConfirmEmail;
diff --git a/lib/Wifty/Action/Logout.pm b/lib/Wifty/Action/Logout.pm
new file mode 100644
index 0000000..697b9f6
--- /dev/null
+++ b/lib/Wifty/Action/Logout.pm
@@ -0,0 +1,35 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Wifty::Action::Logout
+
+=cut
+
+package Wifty::Action::Logout;
+use base qw/Wifty::Action Jifty::Action/;
+
+=head2 arguments
+
+Return the email and password form fields
+
+=cut
+
+sub arguments { 
+    return( { });
+}
+
+=head2 take_action
+
+Nuke the current user object
+
+=cut
+
+sub take_action {
+    my $self = shift;
+    Jifty->web->current_user(undef);
+    return 1;
+}
+
+1;
diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index d48e537..992c231 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -19,6 +19,11 @@ column email_confirmed =>
     type is 'boolean',
     since '0.0.10';
 
+column auth_token => 
+    type is 'text',
+    since '0.0.15';
+
+
 
 package Wifty::Model::User;
 use base qw/Wifty::Record/;
@@ -78,5 +83,25 @@ sub current_user_can {
     return $self->SUPER::current_user_can($right, %args);
 }
 
+=head2 auth_token
+
+Returns the user's unique authentication token. If the user 
+doesn't have one, sets one and returns it.
+
+=cut
+
+
+sub auth_token {
+    my $self = shift;
+    return undef unless ($self->current_user_can( read => 'auth_token'));
+    my $value = $self->_value('auth_token') ;
+    unless ($value) {
+            my $digest =Digest::MD5->new();
+            $digest->add(rand(100));
+            $self->__set('auth_token' => $digest->b64digest);
+            return $digest->b64digest;
+    }
+
+}
 
 1;
diff --git a/lib/Wifty/Notification/ConfirmAddress.pm b/lib/Wifty/Notification/ConfirmAddress.pm
new file mode 100755
index 0000000..3fb3c3a
--- /dev/null
+++ b/lib/Wifty/Notification/ConfirmAddress.pm
@@ -0,0 +1,53 @@
+use warnings;
+use strict;
+
+package Wifty::Notification::ConfirmAddress;
+use base qw/Wifty::Notification/;
+
+=head1 NAME
+
+Hiveminder::Notification::ConfirmAddress
+
+=head1 ARGUMENTS
+
+C<to>, a L<Wifty::Model::User> whose address we are confirming.
+
+=cut
+
+=head2 setup
+
+Sets up the fields of the message.
+
+=cut
+
+sub setup {
+    my $self = shift;
+
+    unless (UNIVERSAL::isa($self->to, "Wifty::Model::User")) {
+	$self->log->error((ref $self) . " called with invalid user argument");
+	return;
+    } 
+   
+
+    my $letme = Jifty::LetMe->new();
+    $letme->email($self->to->email);
+    $letme->path('confirm_email'); 
+    my $confirm_url = $letme->as_url;
+
+    $self->subject( "Welcome to Wifty!" ); 
+    
+
+    $self->body(<<"END_BODY");
+
+You're getting this message because you (or somebody claiming to be you)
+signed up for a Wiki running Wifty.
+
+Before you can use Wifty, we need to make sure that we got your email
+address right.  Click on the link below to get started:
+
+$confirm_url
+
+END_BODY
+}
+
+1;
diff --git a/web/templates/_elements/sidebar b/web/templates/_elements/sidebar
new file mode 100644
index 0000000..41f4859
--- /dev/null
+++ b/web/templates/_elements/sidebar
@@ -0,0 +1,34 @@
+<div id="salutation">
+% if (Jifty->web->current_user->id and Jifty->web->current_user->user_object) {
+Hiya, <span class="user"><%Jifty->web->current_user->user_object->name%></span>.<br />
+(<% Jifty->web->tangent( label => q{Logout}, url => '/logout' )%>)
+% }  else {
+<% Jifty->web->tangent( label => q{You're not currently signed in.}, url => '/login' )%>
+% }
+</div>
+<div id="navigation">
+<ul id="menu">
+<%perl>
+
+
+$m->comp( ".menu", item => $_ )
+    for ( sort { $a->sort_order <=> $b->sort_order }
+    Jifty->web->navigation->children );
+</%perl>
+</ul>
+</div>
+<%def .menu>
+<%args>
+$item
+</%args>
+  <li <%  $item->active ? 'class="active"' : '' |n %>><% 
+    Jifty->web->link(
+        url   => $item->url,
+        label => $item->label,
+    ) %></li>
+% if (my @kids = $item->children) {
+<ul id="submenu">
+% $m->comp(".menu", item => $_) for @kids;
+</ul>
+% }
+</%def>
diff --git a/web/templates/_elements/wrapper b/web/templates/_elements/wrapper
new file mode 100644
index 0000000..15be078
--- /dev/null
+++ b/web/templates/_elements/wrapper
@@ -0,0 +1,31 @@
+<& header, title => $title &>
+<body>
+  <div id="headers">
+    <%Jifty->web->link( url => "/", label => Jifty->config->framework('ApplicationName'))%>
+    <h1 class="title"><% $title %></h1>
+  </div>
+  <& sidebar &>
+  <div id="content">
+    <a name="content"></a>
+% if (Jifty->config->framework('AdminMode') ) {
+<div class="warning admin_mode">
+Alert: Jifty <% Jifty->web->tangent( label => 'administration mode' , url => '/__jifty/admin/')%> is enabled.
+</div>
+
+% }
+    <% Jifty->web->render_messages %>
+    <% $m->content |n%>
+    <div id="keybindings">
+       <script><!--
+       writeKeyBindingLegend();
+       --></script>
+    </div>
+  </div>
+  <div id="jifty-wait-message" style="display: none">Loading...</div>
+</body>
+</html>
+<%args>
+$title => ""
+</%args>
+<%init>
+</%init>
diff --git a/web/templates/let/confirm_email b/web/templates/let/confirm_email
new file mode 100755
index 0000000..6ad94a4
--- /dev/null
+++ b/web/templates/let/confirm_email
@@ -0,0 +1,14 @@
+<%method setup_actions>
+<%perl>
+Jifty->web->allow_actions( 'Wifty::Action::ConfirmEmail'); 
+Jifty->web->request->add_action(
+    moniker => 'confirm_email',
+    class   => 'Wifty::Action::ConfirmEmail',
+);
+Jifty->web->request->add_action(
+    moniker   => 'next_page',
+    class     => 'Jifty::Action::Redirect',
+    arguments => {url => "/"},
+);
+</%perl>
+</%method>
diff --git a/web/templates/login b/web/templates/login
index becc8aa..b149ebc 100755
--- a/web/templates/login
+++ b/web/templates/login
@@ -1,13 +1,13 @@
 <%init> 
-if (Jifty->web->current_user->id) {
-    $m->comp('/_elements/logged_in_already');
-    return();
-}
 my $action = Jifty->web->new_action(class => 'Login', moniker => 'loginbox' );
 
 my $next = Jifty->web->request->continuation || Jifty::Continuation->new(request => Jifty::Request->new(path => "/"));
 </%init>
 <&|/_elements/wrapper, title => 'Login' &>
+
+
+
+% if (not Jifty->web->current_user->id) {
 <h2>Login</h2>
 <% Jifty->web->form->start(call => $next, name => "loginbox") %>
 <% $action->form_field('email') %>
@@ -16,4 +16,10 @@ my $next = Jifty->web->request->continuation || Jifty::Continuation->new(request
 <% Jifty->web->form->submit(label => 'Login', submit => $action) %>
 <% Jifty->web->form->end %>
 <% Jifty->web->tangent( label => q{Don't have an account?}, url => '/signup' )%>
+% }
+% else {
+
+You're alrady logged in.
+
+%}
 </&>
diff --git a/web/templates/logout b/web/templates/logout
new file mode 100755
index 0000000..ba5f717
--- /dev/null
+++ b/web/templates/logout
@@ -0,0 +1,12 @@
+<&| /_elements/wrapper, title => "Logged out" &>
+<p>Ok, you're now logged out. Have a good day.</p>
+</&>
+
+
+<%method setup_actions>
+<%perl>
+    Jifty->web->request->add_action( moniker => 'logout',
+        class => 'Wifty::Action::Logout'
+    );
+</%perl>
+</%method>
diff --git a/web/templates/signup b/web/templates/signup
new file mode 100755
index 0000000..2c08fff
--- /dev/null
+++ b/web/templates/signup
@@ -0,0 +1,19 @@
+<%init> 
+if (Jifty->web->current_user->id) {
+    $m->comp('/_elements/logged_in_already');
+    return();
+}
+my $action = Jifty->web->new_action(class => 'Signup', moniker => 'signupbox' );
+
+my $next = Jifty->web->request->continuation || Jifty::Continuation->new(request => Jifty::Request->new(path => "/"));
+</%init>
+<&|/_elements/wrapper, title => 'Signup' &>
+<h2>Signup</h2>
+<% Jifty->web->form->start(call => $next, name => "signupbox") %>
+<% $action->form_field('email') %>
+<% $action->form_field('name') %>
+<% $action->form_field('password') %>
+<% $action->form_field('password_confirm') %>
+<% Jifty->web->form->submit(label => 'Signup', submit => $action) %>
+<% Jifty->web->form->end %>
+</&>

commit 816471197e8f6c344d50f12ac7a626f92a1185d8
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:37:27 2005 +0000

    Mostly have recording of who-performs-an-update

diff --git a/etc/config.yml b/etc/config.yml
index 514c1e8..641dc5c 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -4,7 +4,7 @@ framework:
     Driver: Pg
     Host: localhost
     User: postgres
-    Version: 0.0.15
+    Version: 0.0.18
     Password: ''
     RequireSSL: 0
 #  Mailer: IO
diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 63d1e27..1cdcbb9 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -3,18 +3,23 @@ use Jifty::DBI::Schema;
 
 column name => 
     type is 'text',
+    label is 'Page name',
     is mandatory,
     is distinct;
 
 column content =>
     type is 'text',
-    label is 'Page content',
+    label is 'Content',
     render_as 'textarea';
 
 column updated =>
     type is 'timestamp',
+    label is 'Last updated',
     since '0.0.6';
 
+column updated_by =>
+    refers_to Wifty::Model::User,
+    since '0.0.16';
 
 column revisions =>
     refers_to Wifty::Model::RevisionCollection by 'page';
@@ -65,6 +70,7 @@ sub create {
     my %args = (@_);
     my $now  = DateTime->now();
     $args{'updated'} = $now->ymd . " " . $now->hms;
+    $args{'updated_by'} = ( $self->current_user? $self->current_user->user_object : undef );
     my ($id) = $self->SUPER::create(%args);
     if ( $self->id ) {
         $self->_add_revision(%args);
@@ -91,7 +97,8 @@ sub _add_revision {
     my $rev = Wifty::Model::Revision->new( current_user => Wifty::CurrentUser->superuser);
     $rev->create(
         page    => $self->id,
-        content => $args{'content'}
+        content => $args{'content'},
+        by      => $args{'updated_by'}
     );
 
 }
@@ -100,7 +107,9 @@ sub set_content {
     my $self    = shift;
     my $content = shift;
     my ( $val, $msg ) = $self->SUPER::set_content($content);
-    $self->_add_revision( content => $content );
+    $self->_add_revision( content => $content,
+                    updated_by =>( $self->current_user? $self->current_user->user_object : undef )
+                );
     return ( $val, $msg );
 }
 
@@ -112,6 +121,14 @@ sub _set {
         column => 'updated',
         value  => $now->ymd . " " . $now->hms
     );
+
+    $self->SUPER::_set(
+        column => 'updated_by',
+        value  => 
+                    ( $self->current_user? $self->current_user->user_object : undef )
+        
+    );
+
     return ( $val, $msg );
 }
 
diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index 7d44259..d6318bd 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -1,21 +1,21 @@
 package Wifty::Model::Revision::Schema;
 use Jifty::DBI::Schema;
 
-column page  => 
-    refers_to Wifty::Model::Page;
+column page  => refers_to Wifty::Model::Page;
 
-column content =>
-    type is 'text',
-    render_as 'textarea';
+column content => type is 'text', render_as 'textarea';
+
+column created => type is 'timestamp';
+
+column by => refers_to Wifty::Model::User, since '0.0.18';
 
-column created => 
-    type is 'timestamp';
 
 package Wifty::Model::Revision;
 use base qw/Wifty::Record/;
 use Jifty::RightsFrom column => 'page';
 use DateTime;
-
+use Wifty::Model::User;
+use Wifty::Model::Page;
 
 sub since { '0.0.5' }
 
diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index 992c231..bcf4a70 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -3,24 +3,29 @@ use Jifty::DBI::Schema;
 
 column name => 
     type is 'text',
+    label is 'Name',
     is mandatory,
     is distinct;
 
 column email =>
     type is 'text',
+    label is 'Email address',
     is mandatory,
     is distinct;
 
 column password =>,
     type is 'text',
+    label is 'Password',
     render_as 'password';
 
 column email_confirmed =>
+    label is 'Email address confirmed?',
     type is 'boolean',
     since '0.0.10';
 
 column auth_token => 
     type is 'text',
+    render_as 'Password',
     since '0.0.15';
 
 

commit 44838be9a6bb3f8ac2dd36425747a79e72f95cbd
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Dec 25 01:37:40 2005 +0000

    Updated user model

diff --git a/etc/config.yml b/etc/config.yml
index 641dc5c..5f6e456 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -1,10 +1,13 @@
 framework:
-  AdminMode: 1
+  AdminMode: 0
+  Web:
+    Port: 80
+    BaseURL: http://jifty.org
   Database:
     Driver: Pg
     Host: localhost
     User: postgres
-    Version: 0.0.18
+    Version: 0.0.19
     Password: ''
     RequireSSL: 0
 #  Mailer: IO
diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index bcf4a70..1f437d9 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -21,7 +21,7 @@ column password =>,
 column email_confirmed =>
     label is 'Email address confirmed?',
     type is 'boolean',
-    since '0.0.10';
+    since '0.0.19';
 
 column auth_token => 
     type is 'text',

commit 94f16d73756face316164254a2925acbb80cc7d0
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Mon Dec 26 02:57:46 2005 +0000

    Upgrades to token handling, acls for users enabled.

diff --git a/etc/config.yml b/etc/config.yml
index 5f6e456..eec1480 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -1,5 +1,6 @@
 framework:
   AdminMode: 0
+  ApplicationName: Wifty
   Web:
     Port: 80
     BaseURL: http://jifty.org
diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index 1f437d9..a0b23ee 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -73,19 +73,39 @@ sub password {
 
 }
 
+=head2 current_user_can
 
-sub current_user_can {
-    my $self = shift;
-    my $right = shift;
-    my %args = (@_);
-    return(1);
-    if ($right eq 'read')  {
+Allows the current user to see all their own attributes and
+everyone else to see their username.
+
+Allows the current user to update any of their own attributes
+except whether or not their email has been confirmed.
 
-    } elsif ($right eq 'update') {
+Passes everything else off to the superclass.
 
+=cut
+
+
+sub current_user_can {
+    my $self  = shift;
+    my $right = shift;
+    my %args  = (@_);
+    Carp::confess if ($right eq 'read' and not $args{'column'});
+    if (    $right eq 'read'
+        and $self->id == $self->current_user->id )
+    {
+        return 1;
+    } elsif ( $right eq 'read' and $args{'column'} eq 'name' ) {
+        return (1);
+
+    } elsif ( $right eq 'update'
+        and $self->id == $self->current_user->id
+        and $args{'column'} ne 'email_confirmed' )
+    {
+        return (1);
     }
 
-    return $self->SUPER::current_user_can($right, %args);
+    return $self->SUPER::current_user_can( $right, %args );
 }
 
 =head2 auth_token
@@ -98,14 +118,14 @@ doesn't have one, sets one and returns it.
 
 sub auth_token {
     my $self = shift;
-    return undef unless ($self->current_user_can( read => 'auth_token'));
+    return undef unless ($self->current_user_can( read => column =>  'auth_token'));
     my $value = $self->_value('auth_token') ;
     unless ($value) {
             my $digest =Digest::MD5->new();
             $digest->add(rand(100));
-            $self->__set('auth_token' => $digest->b64digest);
-            return $digest->b64digest;
+            $self->__set(column => 'auth_token', value => $digest->b64digest);
     }
+    return $self->_value('auth_token') ;
 
 }
 
diff --git a/t/00-model-User.t b/t/00-model-User.t
index 06b838a..e09777f 100644
--- a/t/00-model-User.t
+++ b/t/00-model-User.t
@@ -8,7 +8,7 @@ A basic test harness for the User model.
 
 =cut
 
-use Jifty::Test tests => 11;
+use Jifty::Test tests => 12;
 
 # Make sure we can load the model
 use_ok('Wifty::Model::User');
@@ -24,6 +24,8 @@ ok($id, "User create returned success");
 ok($o->id, "New User has valid id set");
 is($o->id, $id, "Create returned the right id");
 
+ok($o->auth_token, "We have an auth token! ".$o->auth_token);
+
 # And another
 $o->create(email => 'some at mail', name => 'another');
 ok($o->id, "User create returned another value");

commit 0e19cd0fb9b8cdaefd36cc48a35534874ebec788
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Thu Jan 5 19:50:42 2006 +0000

    Create now uses the dispatcher!

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
new file mode 100644
index 0000000..40531cb
--- /dev/null
+++ b/lib/Wifty/Dispatcher.pm
@@ -0,0 +1,8 @@
+package Wifty::Dispatcher;
+use Jifty::Dispatcher -base;
+
+under 'create/*', run {
+    set page => $1;
+    set action => Jifty->web->new_action( class => 'CreatePage' );
+};
+1;
diff --git a/web/templates/autohandler b/web/templates/autohandler
new file mode 100644
index 0000000..b0bf4c2
--- /dev/null
+++ b/web/templates/autohandler
@@ -0,0 +1,29 @@
+<%init>
+$r->content_type('text/html; charset=utf-8');
+Jifty->web->handle_request();
+
+if ($m->base_comp->path =~ m|/_elements/|) {
+    # Requesting an internal component by hand -- naughty
+    $m->redirect("/errors/requested_private_component");
+#} elsif (not Jifty->web->current_user->id and $m->request_comp->path !~ m{^/(?:welcome|dhandler|css|js|images|validator\.xml)} ) {
+#    # Not logged in, trying to access a protected page
+#    $m->notes->{'login-nextpage'} =  $m->{top_path};
+#    Jifty->web->redirect('/welcome/');
+}
+$m->comp('/_elements/nav');
+require Wifty::Dispatcher;
+Wifty::Dispatcher->handle_request();
+return;
+</%init>
+<%def .setup_actions>
+<%init>
+Jifty->web->allow_actions(qr/.*/);
+# this method turns around and calls the setup_actions method 
+# it's called by Jifty::Web->setup_page_actions.
+my $delegate = $m->fetch_comp($m->next_comp->path);
+if ($delegate and $delegate->method_exists('setup_actions')) {
+    $delegate->call_method('setup_actions');
+}
+
+</%init>
+</%def>
diff --git a/web/templates/create/dhandler b/web/templates/create/dhandler
index fcbbe89..d8a3afe 100644
--- a/web/templates/create/dhandler
+++ b/web/templates/create/dhandler
@@ -1,7 +1,3 @@
-<%init>
-my $page = $m->dhandler_arg;
-my $action = Jifty->web->new_action( class => 'CreatePage');
-</%init>
 <&|/_elements/wrapper, title => 'New page: '. $page&>
 <% Jifty->web->form->start %>
 <% Jifty->web->form->next_page( url => '/view/'.$page) %>
@@ -11,3 +7,7 @@ my $action = Jifty->web->new_action( class => 'CreatePage');
 <% Jifty->web->form->end %>
 <& /_elements/markup &>
 </&>
+<%args>
+$action
+$page
+</%args>

commit 0c06037f6099182d00a9e0eb7889eac9664a0779
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Thu Jan 5 22:48:00 2006 +0000

    Wifty now uses the dispatcher

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 40531cb..178ee21 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -1,8 +1,92 @@
 package Wifty::Dispatcher;
 use Jifty::Dispatcher -base;
 
-under 'create/*', run {
-    set page => $1;
-    set action => Jifty->web->new_action( class => 'CreatePage' );
+on '/', run {
+    redirect( '/view/HomePage');
 };
+
+under '/create/*', run {
+     set page => $1;
+     set action => Jifty->web->new_action( class => 'CreatePage' );
+};
+
+
+under ['view/*', 'edit/*'], run {
+    my ( $name, $rev );
+    warn "hey";
+    if ( $1 =~ qr{^(.*?)/?(\d*?)$} ) {
+        $name = $1;
+        $rev  = $2;
+    }
+    my $page = Wifty::Model::Page->new();
+    $page->load_by_cols( name => $name );
+    Jifty->web->redirect( '/create/' . $name ) unless ( $page->id );
+    my $revision = Wifty::Model::Revision->new();
+    $revision->load_by_cols( page => $page->id, id => $rev ) if ($rev);
+    set page => $page;
+    set revision => $revision;
+    set viewer => Jifty->web->new_action( class => 'UpdatePage', record => $page );
+};
+
+under 'history/*', run {
+    my $name = $1;
+    my $page = Wifty::Model::Page->new();
+    $page->load_by_cols( name => $name );
+    redirect( '/create/' . $name ) unless ( $page->id );
+
+    my $revisions = $page->revisions;
+    $revisions->order_by( column => 'id', order => 'desc');
+
+    set page => $page;
+    set revisions => $revisions;
+
+};
+
+
+on 'pages', run {
+    my $pages = Wifty::Model::PageCollection->new();
+    $pages->unlimit();
+    set pages => $pages;
+};
+
+on 'recent', run {
+    my $then = DateTime->from_epoch( epoch => ( time - ( 86400 * 7 ) ) );
+    my $pages = Wifty::Model::PageCollection->new();
+    $pages->limit(
+        column   => 'updated',
+        operator => '>',
+        value    => $then->ymd
+    );
+    $pages->order_by( column => 'updated', order => 'desc' );
+    set pages => $pages;
+};
+
+on 'signup', run {
+    redirect('/') if ( Jifty->web->current_user->id );
+    set 'action' =>
+        Jifty->web->new_action( class => 'Signup', moniker => 'signupbox' );
+
+    set 'next' => Jifty->web->request->continuation
+        || Jifty::Continuation->new(
+        request => Jifty::Request->new( path => "/" ) );
+
+};
+
+on 'login', run {
+    set 'action' =>
+        Jifty->web->new_action( class => 'Login', moniker => 'loginbox' );
+    set 'next' => Jifty->web->request->continuation
+        || Jifty::Continuation->new(
+        request => Jifty::Request->new( path => "/" ) );
+
+};
+
+on 'logout', run {
+    Jifty->web->request->add_action(
+        moniker => 'logout',
+        class   => 'Wifty::Action::Logout'
+    );
+};
+
+    
 1;
diff --git a/web/templates/_elements/wrapper b/web/templates/_elements/wrapper
index 15be078..9569bf8 100644
--- a/web/templates/_elements/wrapper
+++ b/web/templates/_elements/wrapper
@@ -27,5 +27,3 @@ Alert: Jifty <% Jifty->web->tangent( label => 'administration mode' , url => '/_
 <%args>
 $title => ""
 </%args>
-<%init>
-</%init>
diff --git a/web/templates/create/dhandler b/web/templates/create/dhandler
index d8a3afe..91e61d8 100644
--- a/web/templates/create/dhandler
+++ b/web/templates/create/dhandler
@@ -8,6 +8,6 @@
 <& /_elements/markup &>
 </&>
 <%args>
-$action
-$page
+$action => undef
+$page => undef
 </%args>
diff --git a/web/templates/edit/dhandler b/web/templates/edit/dhandler
index 53b818d..380691e 100644
--- a/web/templates/edit/dhandler
+++ b/web/templates/edit/dhandler
@@ -1,22 +1,9 @@
-<%init>
-my $arg = $m->dhandler_arg();
-my ($name,$rev);
-if ($arg =~ qr{^(.*?)/?(\d*?)$}) {
-    $name = $1;
-    $rev = $2;
-}
-my $page = Wifty::Model::Page->new();
-$page->load_by_cols( name => $name );
-
-my $revision = Wifty::Model::Revision->new();
-if ($rev) {
-$revision->load_by_cols( page  => $page->id, id=> $rev);
-}
-
-my $viewer = Jifty->web->new_action( class => 'UpdatePage', record => $page );
-$m->comp('/_elements/page_nav', page => $page->name, rev => $rev);
-
-</%init>
+<%args>
+$page
+$revision
+$viewer 
+</%args>
+<&/_elements/page_nav, page => $page->name, rev => $revision->id &>
 <&|/_elements/wrapper, title => 'Edit: '.$page->name . ($revision->id ? " as of ".$revision->created : '')  &>
 <% Jifty->web->form->start %>
 <% Jifty->web->form->next_page( url => '/view/'.$page->name) %>
diff --git a/web/templates/history/dhandler b/web/templates/history/dhandler
index a8c8aa3..41d133b 100755
--- a/web/templates/history/dhandler
+++ b/web/templates/history/dhandler
@@ -1,13 +1,8 @@
-<%init>
-my $name = $m->dhandler_arg();
-my $page = Wifty::Model::Page->new();
-$page->load_by_cols( name => $name );
-Jifty->web->redirect( '/create/' . $name ) unless ( $page->id );
-
-my $revisions = $page->revisions;
-$revisions->order_by( column => 'id', order => 'desc');
-$m->comp('/_elements/page_nav', page => $page->name);
-</%init>
+<%args>
+$page
+$revisions
+</%args>
+<& /_elements/page_nav, page => $page->name &>
 <&|/_elements/wrapper, title => $revisions->count ." revisions of " .$page->name &>
 <ul>
 % while (my $rev = $revisions->next) {
diff --git a/web/templates/index.html b/web/templates/index.html
deleted file mode 100644
index e5cb700..0000000
--- a/web/templates/index.html
+++ /dev/null
@@ -1 +0,0 @@
-% Jifty->web->redirect( '/view/HomePage');
diff --git a/web/templates/login b/web/templates/login
index b149ebc..ebd8869 100755
--- a/web/templates/login
+++ b/web/templates/login
@@ -1,12 +1,5 @@
-<%init> 
-my $action = Jifty->web->new_action(class => 'Login', moniker => 'loginbox' );
-
-my $next = Jifty->web->request->continuation || Jifty::Continuation->new(request => Jifty::Request->new(path => "/"));
-</%init>
 <&|/_elements/wrapper, title => 'Login' &>
 
-
-
 % if (not Jifty->web->current_user->id) {
 <h2>Login</h2>
 <% Jifty->web->form->start(call => $next, name => "loginbox") %>
@@ -18,8 +11,6 @@ my $next = Jifty->web->request->continuation || Jifty::Continuation->new(request
 <% Jifty->web->tangent( label => q{Don't have an account?}, url => '/signup' )%>
 % }
 % else {
-
-You're alrady logged in.
-
-%}
+You're already logged in.
+% }
 </&>
diff --git a/web/templates/logout b/web/templates/logout
index ba5f717..f6eee9d 100755
--- a/web/templates/logout
+++ b/web/templates/logout
@@ -1,12 +1,3 @@
 <&| /_elements/wrapper, title => "Logged out" &>
 <p>Ok, you're now logged out. Have a good day.</p>
 </&>
-
-
-<%method setup_actions>
-<%perl>
-    Jifty->web->request->add_action( moniker => 'logout',
-        class => 'Wifty::Action::Logout'
-    );
-</%perl>
-</%method>
diff --git a/web/templates/pages b/web/templates/pages
index aeddbd4..c157fd1 100644
--- a/web/templates/pages
+++ b/web/templates/pages
@@ -1,7 +1,6 @@
-<%init>
-my $pages = Wifty::Model::PageCollection->new();
-$pages->unlimit();
-</%init>
+<%args>
+$pages
+</%args>
 <&|/_elements/wrapper, title => 'These are the pages on your wiki!' &>
 <ul id="pagelist">
 % while (my $page = $pages->next) {
diff --git a/web/templates/recent b/web/templates/recent
index d002c6f..2d1941e 100644
--- a/web/templates/recent
+++ b/web/templates/recent
@@ -1,9 +1,6 @@
-<%init>
-my $then = DateTime->from_epoch( epoch => ( time - ( 86400 * 7 ) ) );
-my $pages = Wifty::Model::PageCollection->new();
-$pages->limit( column => 'updated', operator => '>', value => $then->ymd );
-$pages->order_by( column => 'updated', order => 'desc' );
-</%init>
+<%args>
+$pages
+</%args>
 <&|/_elements/wrapper, title => 'Updated this week' &>
 <dl id="recentudates">
 % while (my $page = $pages->next) {
diff --git a/web/templates/signup b/web/templates/signup
index 2c08fff..1d006db 100755
--- a/web/templates/signup
+++ b/web/templates/signup
@@ -1,12 +1,7 @@
-<%init> 
-if (Jifty->web->current_user->id) {
-    $m->comp('/_elements/logged_in_already');
-    return();
-}
-my $action = Jifty->web->new_action(class => 'Signup', moniker => 'signupbox' );
-
-my $next = Jifty->web->request->continuation || Jifty::Continuation->new(request => Jifty::Request->new(path => "/"));
-</%init>
+<%args>
+$action
+$next
+</%args>
 <&|/_elements/wrapper, title => 'Signup' &>
 <h2>Signup</h2>
 <% Jifty->web->form->start(call => $next, name => "signupbox") %>
diff --git a/web/templates/view/dhandler b/web/templates/view/dhandler
index 5ce5e8b..f81e26e 100644
--- a/web/templates/view/dhandler
+++ b/web/templates/view/dhandler
@@ -1,25 +1,8 @@
-<%init>
-use Wifty::Model::Page;
-my $arg = $m->dhandler_arg();
-my ($name,$rev);
-if ($arg =~ qr{^(.*?)/?(\d*?)$}) {
-    $name = $1;
-    $rev = $2;
-}
-my $page = Wifty::Model::Page->new();
-$page->load_by_cols( name => $name );
-
-my $revision = Wifty::Model::Revision->new();
-if ($rev) {
-    $revision->load_by_cols( page => $page->id, id => $rev);
-}
-
-unless ( $page->id ) {
-    Jifty->web->redirect( '/create/' . $name );
-}
-
-$m->comp('/_elements/page_nav', page => $page->name, rev => $rev);
-</%init>
+<%args>
+$page
+$revision
+</%args>
+<& /_elements/page_nav, page => $page->name, rev => $revision->id &>
 <&|/_elements/wrapper, title => $page->name . ($revision->id ? " as of ".$revision->created : '') &>
 % if ($revision->id) {
 <% $page->wiki_content($revision->content) |n%>

commit 5fb201082269c8e2f16e8cd06009e25157d01cbe
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Thu Jan 5 22:49:09 2006 +0000

    removed a warning

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 178ee21..5ec5f28 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -13,7 +13,6 @@ under '/create/*', run {
 
 under ['view/*', 'edit/*'], run {
     my ( $name, $rev );
-    warn "hey";
     if ( $1 =~ qr{^(.*?)/?(\d*?)$} ) {
         $name = $1;
         $rev  = $2;

commit 6c8ed75f95e7164d3505b1898147c4ec61e9aafb
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Fri Jan 6 13:38:18 2006 +0000

    checkpoint
    
    * wifty checkpoint

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 5ec5f28..ca16fc6 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -1,9 +1,7 @@
 package Wifty::Dispatcher;
 use Jifty::Dispatcher -base;
 
-on '/', run {
-    redirect( '/view/HomePage');
-};
+on '/', redirect( '/view/HomePage');
 
 under '/create/*', run {
      set page => $1;
@@ -11,9 +9,9 @@ under '/create/*', run {
 };
 
 
-under ['view/*', 'edit/*'], run {
+on qr{(view|edit)/(.*)}, run {
     my ( $name, $rev );
-    if ( $1 =~ qr{^(.*?)/?(\d*?)$} ) {
+    if ( $2 =~ qr{^(.*?)/?(\d*?)$} ) {
         $name = $1;
         $rev  = $2;
     }
@@ -25,9 +23,10 @@ under ['view/*', 'edit/*'], run {
     set page => $page;
     set revision => $revision;
     set viewer => Jifty->web->new_action( class => 'UpdatePage', record => $page );
+    show("/view");
 };
 
-under 'history/*', run {
+on 'history/*', run {
     my $name = $1;
     my $page = Wifty::Model::Page->new();
     $page->load_by_cols( name => $name );
diff --git a/web/templates/create/dhandler b/web/templates/create
similarity index 100%
rename from web/templates/create/dhandler
rename to web/templates/create
diff --git a/web/templates/edit/dhandler b/web/templates/edit
similarity index 100%
rename from web/templates/edit/dhandler
rename to web/templates/edit
diff --git a/web/templates/history/dhandler b/web/templates/history
similarity index 100%
rename from web/templates/history/dhandler
rename to web/templates/history
diff --git a/web/templates/view/dhandler b/web/templates/view
similarity index 100%
rename from web/templates/view/dhandler
rename to web/templates/view

commit 2a77fc37a71b4ebe0f8c04e98cf314ca8308acbc
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Fri Jan 6 22:49:25 2006 +0000

    sync wifty to the dispatcher

diff --git a/etc/config.yml b/etc/config.yml
index eec1480..338de16 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -1,6 +1,8 @@
 framework:
   AdminMode: 0
   ApplicationName: Wifty
+  LogConfig: etc/log4perl.conf
+
   Web:
     Port: 80
     BaseURL: http://jifty.org
diff --git a/etc/log4perl.conf b/etc/log4perl.conf
new file mode 100644
index 0000000..b48ff34
--- /dev/null
+++ b/etc/log4perl.conf
@@ -0,0 +1,34 @@
+log4perl.rootLogger=DEBUG, ShowInfo, LogToFile, ErrorsToFile
+
+# Disable debugging output for certain modules; comment these lines to have them
+# show up again
+
+log4perl.logger.Jifty::MasonInterp = INFO, ShowInfo, LogToFile
+
+# If you want to make DEBUG level for Jifty::Some::Module show up on the screen
+# (and not just the log file), add the
+# following line:
+#log4perl.logger.Jifty::Some::Module=DEBUG, ShowCategoryDebug
+
+log4perl.appender.ShowInfo=Log::Log4perl::Appender::ScreenColoredLevels
+log4perl.appender.ShowInfo.layout=Log::Log4perl::Layout::PatternLayout
+log4perl.appender.ShowInfo.layout.ConversionPattern=%d %p> %F{1}:%L %M - %m%n
+#log4perl.appender.ShowInfo.Threshold=INFO
+
+log4perl.appender.LogToFile=Log::Log4perl::Appender::File
+log4perl.appender.LogToFile.filename= sub { Jifty::Util->absolute_path("log/server.log") }
+log4perl.appender.LogToFile.mode=append
+log4perl.appender.LogToFile.layout=Log::Log4perl::Layout::PatternLayout
+log4perl.appender.LogToFile.layout.ConversionPattern=%d %p> %F{1}:%L %M - %m%n
+
+log4perl.appender.ErrorsToFile=Log::Log4perl::Appender::File
+log4perl.appender.ErrorsToFile.filename= sub { Jifty::Util->absolute_path("log/error.log") }
+log4perl.appender.ErrorsToFile.mode=append
+log4perl.appender.ErrorsToFile.layout=Log::Log4perl::Layout::PatternLayout
+log4perl.appender.ErrorsToFile.layout.ConversionPattern=%d %p> %F{1}:%L %M - %m%n
+log4perl.appender.ErrorsToFile.Threshold=WARN
+
+log4perl.appender.ShowCategoryDebug=Log::Log4perl::Appender::ScreenColoredLevels
+log4perl.appender.ShowCategoryDebug.layout=Log::Log4perl::Layout::PatternLayout
+log4perl.appender.ShowCategoryDebug.layout.ConversionPattern=%d %p> %F{1}:%L %M - %m%n
+
diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index ca16fc6..212500d 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -3,14 +3,16 @@ use Jifty::Dispatcher -base;
 
 on '/', redirect( '/view/HomePage');
 
-under '/create/*', run {
+on '/create/*', run {
      set page => $1;
      set action => Jifty->web->new_action( class => 'CreatePage' );
+     show("/create");
 };
 
 
 on qr{(view|edit)/(.*)}, run {
     my ( $name, $rev );
+    my $page_name = $1;
     if ( $2 =~ qr{^(.*?)/?(\d*?)$} ) {
         $name = $1;
         $rev  = $2;
@@ -23,7 +25,7 @@ on qr{(view|edit)/(.*)}, run {
     set page => $page;
     set revision => $revision;
     set viewer => Jifty->web->new_action( class => 'UpdatePage', record => $page );
-    show("/view");
+    show("/$page_name");
 };
 
 on 'history/*', run {
@@ -37,7 +39,7 @@ on 'history/*', run {
 
     set page => $page;
     set revisions => $revisions;
-
+    show('/history');
 };
 
 
@@ -79,7 +81,7 @@ on 'login', run {
 
 };
 
-on 'logout', run {
+before 'logout', run {
     Jifty->web->request->add_action(
         moniker => 'logout',
         class   => 'Wifty::Action::Logout'
diff --git a/web/templates/autohandler b/web/templates/autohandler
index b0bf4c2..8009934 100644
--- a/web/templates/autohandler
+++ b/web/templates/autohandler
@@ -1,6 +1,6 @@
 <%init>
 $r->content_type('text/html; charset=utf-8');
-Jifty->web->handle_request();
+#Jifty->web->handle_request();
 
 if ($m->base_comp->path =~ m|/_elements/|) {
     # Requesting an internal component by hand -- naughty
@@ -17,6 +17,7 @@ return;
 </%init>
 <%def .setup_actions>
 <%init>
+# XXX TODO: move all this into the dispatcher
 Jifty->web->allow_actions(qr/.*/);
 # this method turns around and calls the setup_actions method 
 # it's called by Jifty::Web->setup_page_actions.

commit d22433343ccfd78a92a2e8e14254c67a2573a16c
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Jan 8 15:35:09 2006 +0000

    Switch to SQLite for testing purposes
    * Turn off Log4Perl
    * Comment out 'by' column since SQLite treats it as reserved

diff --git a/Makefile.PL b/Makefile.PL
index 89bf3d4..456dedf 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -2,5 +2,6 @@ use inc::Module::Install;
 name('Wifty');
 version('0.01');
 requires('Jifty');
-
+requires('Text::Markdown');
+requires('HTML::Scrubber');
 WriteAll;
diff --git a/etc/config.yml b/etc/config.yml
index 338de16..a4654c2 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -1,13 +1,14 @@
 framework:
   AdminMode: 0
   ApplicationName: Wifty
-  LogConfig: etc/log4perl.conf
+  #LogConfig: etc/log4perl.conf
 
   Web:
     Port: 80
     BaseURL: http://jifty.org
   Database:
-    Driver: Pg
+  #  Driver: Pg
+    Driver: SQLite
     Host: localhost
     User: postgres
     Version: 0.0.19
diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 1cdcbb9..3ee988f 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -98,7 +98,7 @@ sub _add_revision {
     $rev->create(
         page    => $self->id,
         content => $args{'content'},
-        by      => $args{'updated_by'}
+        # by      => $args{'updated_by'}
     );
 
 }
diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index d6318bd..8291192 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -7,7 +7,7 @@ column content => type is 'text', render_as 'textarea';
 
 column created => type is 'timestamp';
 
-column by => refers_to Wifty::Model::User, since '0.0.18';
+#column by => refers_to Wifty::Model::User, since '0.0.18';
 
 
 package Wifty::Model::Revision;

commit 60ce2ebf84b7a6913b9df336822ce40a89104d94
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Jan 8 15:36:58 2006 +0000

    typoed the yaml comments

diff --git a/etc/config.yml b/etc/config.yml
index a4654c2..1405062 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -1,13 +1,11 @@
 framework:
   AdminMode: 0
   ApplicationName: Wifty
-  #LogConfig: etc/log4perl.conf
 
   Web:
     Port: 80
     BaseURL: http://jifty.org
   Database:
-  #  Driver: Pg
     Driver: SQLite
     Host: localhost
     User: postgres

commit 6ff5d0dd7c669627cdca6c54ee5251cc924dffa4
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Wed Jan 11 18:56:16 2006 +0000

    Site-specific stuff should go in the siteconfig
     * Avoid double-escaping <'s by doing markdown then scrub; loosen
       scrubbing to allow all of markdown through

diff --git a/etc/config.yml b/etc/config.yml
index 1405062..d256e28 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -2,9 +2,6 @@ framework:
   AdminMode: 0
   ApplicationName: Wifty
 
-  Web:
-    Port: 80
-    BaseURL: http://jifty.org
   Database:
     Driver: SQLite
     Host: localhost
@@ -16,5 +13,3 @@ framework:
 #  MailerArgs:
 #    - %log/mail.log%
   SiteConfig: etc/site_config.yml
-application: 
-  MaxWurbles: 9
diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 3ee988f..4ad0cdb 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -40,7 +40,7 @@ this page's "content" attribute.
 
 sub wiki_content {
     my $self     = shift;
-    my $content  = shift ||$self->content();
+    my $content  = shift || $self->content() || '';
     my $scrubber = HTML::Scrubber->new();
 
     $scrubber->default(
@@ -48,7 +48,7 @@ sub wiki_content {
         {   '*'   => 0,
             id    => 1,
             class => 1,
-            href  => qr{^(?:http:|ftp:|https:|/)}i,
+            href  => qr{^(?:(?:\w+$)|http:|ftp:|https:|/)}i,
 
             # Match http, ftp and relative urls
             face   => 1,
@@ -59,9 +59,12 @@ sub wiki_content {
 
     $scrubber->deny(qw[*]);
     $scrubber->allow(
-        qw[A B U P BR I HR BR SMALL EM FONT SPAN DIV UL OL LI DL DT DD]);
+        qw[H1 H2 H3 H4 H5 A STRONG EM CODE PRE B U P BR I HR BR SPAN DIV UL OL LI DL DT DD]);
     $scrubber->comment(0);
-    return ( markdown( $scrubber->scrub( $content || '') ) );
+
+    $content = markdown( $content );
+    $content = $scrubber->scrub( $content );
+    return ( $content );
 
 }
 

commit dd6bf9bb12d0d09a7cc3d22f07068d04fccbc1fc
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Wed Jan 11 21:59:34 2006 +0000

    Make /login compile
     * Remove extraneous lines from autohandler

diff --git a/web/templates/autohandler b/web/templates/autohandler
index 8009934..f3ac548 100644
--- a/web/templates/autohandler
+++ b/web/templates/autohandler
@@ -1,14 +1,7 @@
 <%init>
-$r->content_type('text/html; charset=utf-8');
-#Jifty->web->handle_request();
-
 if ($m->base_comp->path =~ m|/_elements/|) {
     # Requesting an internal component by hand -- naughty
     $m->redirect("/errors/requested_private_component");
-#} elsif (not Jifty->web->current_user->id and $m->request_comp->path !~ m{^/(?:welcome|dhandler|css|js|images|validator\.xml)} ) {
-#    # Not logged in, trying to access a protected page
-#    $m->notes->{'login-nextpage'} =  $m->{top_path};
-#    Jifty->web->redirect('/welcome/');
 }
 $m->comp('/_elements/nav');
 require Wifty::Dispatcher;
diff --git a/web/templates/login b/web/templates/login
index ebd8869..52d75b5 100755
--- a/web/templates/login
+++ b/web/templates/login
@@ -1,3 +1,7 @@
+<%args>
+$action => undef
+$next => undef
+</%args>
 <&|/_elements/wrapper, title => 'Login' &>
 
 % if (not Jifty->web->current_user->id) {

commit 58f84da7862163cd03322a319423a64f8c7d73ab
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Mon Jan 23 22:29:35 2006 +0000

    Clean up to work with most recent dispatcher, etc

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 212500d..b366a33 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -1,15 +1,24 @@
 package Wifty::Dispatcher;
 use Jifty::Dispatcher -base;
 
-on '/', redirect( '/view/HomePage');
+# Generic restrictions
+under '/', run {
+    Jifty->web->deny_actions('Wifty::ConfirmEmail');
+};
+
+# Default page
+on '/', run {
+    redirect( '/view/HomePage');
+};
 
+# Create a page
 on '/create/*', run {
      set page => $1;
      set action => Jifty->web->new_action( class => 'CreatePage' );
      show("/create");
 };
 
-
+# View or edit a page
 on qr{(view|edit)/(.*)}, run {
     my ( $name, $rev );
     my $page_name = $1;
@@ -28,6 +37,7 @@ on qr{(view|edit)/(.*)}, run {
     show("/$page_name");
 };
 
+# View page history
 on 'history/*', run {
     my $name = $1;
     my $page = Wifty::Model::Page->new();
@@ -42,13 +52,14 @@ on 'history/*', run {
     show('/history');
 };
 
-
+# List pages
 on 'pages', run {
     my $pages = Wifty::Model::PageCollection->new();
     $pages->unlimit();
     set pages => $pages;
 };
 
+# Show recent edits
 on 'recent', run {
     my $then = DateTime->from_epoch( epoch => ( time - ( 86400 * 7 ) ) );
     my $pages = Wifty::Model::PageCollection->new();
@@ -61,6 +72,7 @@ on 'recent', run {
     set pages => $pages;
 };
 
+# Sign up for an account
 on 'signup', run {
     redirect('/') if ( Jifty->web->current_user->id );
     set 'action' =>
@@ -72,6 +84,7 @@ on 'signup', run {
 
 };
 
+# Login
 on 'login', run {
     set 'action' =>
         Jifty->web->new_action( class => 'Login', moniker => 'loginbox' );
@@ -81,6 +94,7 @@ on 'login', run {
 
 };
 
+# Log out
 before 'logout', run {
     Jifty->web->request->add_action(
         moniker => 'logout',
diff --git a/web/templates/autohandler b/web/templates/autohandler
index f3ac548..6497077 100644
--- a/web/templates/autohandler
+++ b/web/templates/autohandler
@@ -4,20 +4,5 @@ if ($m->base_comp->path =~ m|/_elements/|) {
     $m->redirect("/errors/requested_private_component");
 }
 $m->comp('/_elements/nav');
-require Wifty::Dispatcher;
-Wifty::Dispatcher->handle_request();
-return;
+$m->call_next();
 </%init>
-<%def .setup_actions>
-<%init>
-# XXX TODO: move all this into the dispatcher
-Jifty->web->allow_actions(qr/.*/);
-# this method turns around and calls the setup_actions method 
-# it's called by Jifty::Web->setup_page_actions.
-my $delegate = $m->fetch_comp($m->next_comp->path);
-if ($delegate and $delegate->method_exists('setup_actions')) {
-    $delegate->call_method('setup_actions');
-}
-
-</%init>
-</%def>

commit f51437acf71c1c668b5778f778a9f09df1c36177
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Mon Jan 30 05:11:22 2006 +0000

    Removed now-unneeded components

diff --git a/web/templates/_elements/wrapper b/web/templates/_elements/wrapper
deleted file mode 100644
index 9569bf8..0000000
--- a/web/templates/_elements/wrapper
+++ /dev/null
@@ -1,29 +0,0 @@
-<& header, title => $title &>
-<body>
-  <div id="headers">
-    <%Jifty->web->link( url => "/", label => Jifty->config->framework('ApplicationName'))%>
-    <h1 class="title"><% $title %></h1>
-  </div>
-  <& sidebar &>
-  <div id="content">
-    <a name="content"></a>
-% if (Jifty->config->framework('AdminMode') ) {
-<div class="warning admin_mode">
-Alert: Jifty <% Jifty->web->tangent( label => 'administration mode' , url => '/__jifty/admin/')%> is enabled.
-</div>
-
-% }
-    <% Jifty->web->render_messages %>
-    <% $m->content |n%>
-    <div id="keybindings">
-       <script><!--
-       writeKeyBindingLegend();
-       --></script>
-    </div>
-  </div>
-  <div id="jifty-wait-message" style="display: none">Loading...</div>
-</body>
-</html>
-<%args>
-$title => ""
-</%args>
diff --git a/web/templates/autohandler b/web/templates/autohandler
deleted file mode 100644
index 6497077..0000000
--- a/web/templates/autohandler
+++ /dev/null
@@ -1,8 +0,0 @@
-<%init>
-if ($m->base_comp->path =~ m|/_elements/|) {
-    # Requesting an internal component by hand -- naughty
-    $m->redirect("/errors/requested_private_component");
-}
-$m->comp('/_elements/nav');
-$m->call_next();
-</%init>

commit cc26f9412a6fe86ed898abf59bfed16a39cf7fe7
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Mon Jan 30 05:23:11 2006 +0000

    Wifty view/edit dispatcher rules were too greedy

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index b366a33..e1c5034 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -19,7 +19,7 @@ on '/create/*', run {
 };
 
 # View or edit a page
-on qr{(view|edit)/(.*)}, run {
+on qr{^/(view|edit)/(.*)}, run {
     my ( $name, $rev );
     my $page_name = $1;
     if ( $2 =~ qr{^(.*?)/?(\d*?)$} ) {

commit b47d2fa3fbb0c0414ad33f9582b614bf431fadbf
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Fri Feb 17 22:37:06 2006 +0000

    We're not importing the 'markdown' sub from Text::Markdown, so
       provide the package name, too

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 4ad0cdb..66d0e27 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -62,7 +62,7 @@ sub wiki_content {
         qw[H1 H2 H3 H4 H5 A STRONG EM CODE PRE B U P BR I HR BR SPAN DIV UL OL LI DL DT DD]);
     $scrubber->comment(0);
 
-    $content = markdown( $content );
+    $content = Text::Markdown::markdown( $content );
     $content = $scrubber->scrub( $content );
     return ( $content );
 

commit 4cfd8b2b2d52c3b3189c19d72c7eca161e2458b2
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Sat Feb 25 18:38:32 2006 +0000

    Fix because of update to jifty

diff --git a/web/templates/_elements/sidebar b/web/templates/_elements/sidebar
index 41f4859..697f839 100644
--- a/web/templates/_elements/sidebar
+++ b/web/templates/_elements/sidebar
@@ -7,28 +7,5 @@ Hiya, <span class="user"><%Jifty->web->current_user->user_object->name%></span>.
 % }
 </div>
 <div id="navigation">
-<ul id="menu">
-<%perl>
-
-
-$m->comp( ".menu", item => $_ )
-    for ( sort { $a->sort_order <=> $b->sort_order }
-    Jifty->web->navigation->children );
-</%perl>
-</ul>
+<& /_elements/menu &>
 </div>
-<%def .menu>
-<%args>
-$item
-</%args>
-  <li <%  $item->active ? 'class="active"' : '' |n %>><% 
-    Jifty->web->link(
-        url   => $item->url,
-        label => $item->label,
-    ) %></li>
-% if (my @kids = $item->children) {
-<ul id="submenu">
-% $m->comp(".menu", item => $_) for @kids;
-</ul>
-% }
-</%def>

commit cfee2d5723e7ef1749ff7b41254d884ea0e256fc
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Sat Mar 25 20:01:06 2006 +0000

    Fix LetMes -- the dispatcher code needs to be abstracted out still, though

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index e1c5034..d6dd43e 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -91,7 +91,6 @@ on 'login', run {
     set 'next' => Jifty->web->request->continuation
         || Jifty::Continuation->new(
         request => Jifty::Request->new( path => "/" ) );
-
 };
 
 # Log out
@@ -102,5 +101,26 @@ before 'logout', run {
     );
 };
 
-    
+
+## LetMes
+before qr'^/let/(.*)' => run {
+    Jifty->web->deny_actions(qr/.*/);
+
+    my $let_me = Jifty::LetMe->new();
+    $let_me->from_token($1);
+    redirect '/error/let_me/invalid_token' unless $let_me->validate;
+
+    Jifty->web->temporary_current_user($let_me->validated_current_user);
+
+    my %args = %{$let_me->args};
+    set $_ => $args{$_} for keys %args;
+    set let_me => $let_me;
+};
+
+on qr'^/let/', => run {
+    my $let_me = get 'let_me';
+    show '/let/' . $let_me->path;
+};
+
+
 1;
diff --git a/web/templates/_elements/sidebar b/web/templates/_elements/sidebar
index 697f839..e849b39 100644
--- a/web/templates/_elements/sidebar
+++ b/web/templates/_elements/sidebar
@@ -1,7 +1,7 @@
 <div id="salutation">
 % if (Jifty->web->current_user->id and Jifty->web->current_user->user_object) {
 Hiya, <span class="user"><%Jifty->web->current_user->user_object->name%></span>.<br />
-(<% Jifty->web->tangent( label => q{Logout}, url => '/logout' )%>)
+(<% Jifty->web->link( label => q{Logout}, url => '/logout' )%>)
 % }  else {
 <% Jifty->web->tangent( label => q{You're not currently signed in.}, url => '/login' )%>
 % }
diff --git a/web/templates/let/confirm_email b/web/templates/let/confirm_email
index 6ad94a4..8da28d2 100755
--- a/web/templates/let/confirm_email
+++ b/web/templates/let/confirm_email
@@ -1,14 +1,8 @@
-<%method setup_actions>
-<%perl>
+<%init>
 Jifty->web->allow_actions( 'Wifty::Action::ConfirmEmail'); 
-Jifty->web->request->add_action(
+Jifty->web->new_action(
     moniker => 'confirm_email',
     class   => 'Wifty::Action::ConfirmEmail',
-);
-Jifty->web->request->add_action(
-    moniker   => 'next_page',
-    class     => 'Jifty::Action::Redirect',
-    arguments => {url => "/"},
-);
-</%perl>
-</%method>
+)->run;
+Jifty->web->redirect("/");
+</%init>

commit f6d11cb26b5ada50b4d6a12f14b0c88dc2fcfee0
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Wed Apr 5 00:19:15 2006 +0000

    New allow and deny actions API

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index d6dd43e..6480c3e 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -3,7 +3,7 @@ use Jifty::Dispatcher -base;
 
 # Generic restrictions
 under '/', run {
-    Jifty->web->deny_actions('Wifty::ConfirmEmail');
+    Jifty->api->deny('ConfirmEmail');
 };
 
 # Default page
@@ -104,7 +104,7 @@ before 'logout', run {
 
 ## LetMes
 before qr'^/let/(.*)' => run {
-    Jifty->web->deny_actions(qr/.*/);
+    Jifty->api->deny(qr/^Wifty::Dispatcher/);
 
     my $let_me = Jifty::LetMe->new();
     $let_me->from_token($1);
diff --git a/web/templates/let/confirm_email b/web/templates/let/confirm_email
index 8da28d2..86a69a1 100755
--- a/web/templates/let/confirm_email
+++ b/web/templates/let/confirm_email
@@ -1,5 +1,5 @@
 <%init>
-Jifty->web->allow_actions( 'Wifty::Action::ConfirmEmail'); 
+Jifty->api->allow( 'ConfirmEmail'); 
 Jifty->web->new_action(
     moniker => 'confirm_email',
     class   => 'Wifty::Action::ConfirmEmail',

commit edf57d2bfee8fda9938c87daf214841cfe18dd8b
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Thu Apr 6 19:56:50 2006 +0000

    Remove backup file
     * Specify template and static directories

diff --git a/etc/config.yml b/etc/config.yml
index d256e28..eb96db9 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -13,3 +13,7 @@ framework:
 #  MailerArgs:
 #    - %log/mail.log%
   SiteConfig: etc/site_config.yml
+
+  Web:
+    StaticRoot: share/web/static
+    TemplateRoot: share/web/templates
diff --git a/web/static/css/.base.css.swp b/web/static/css/.base.css.swp
deleted file mode 100644
index 79c4cf5..0000000
Binary files a/web/static/css/.base.css.swp and /dev/null differ

commit 85a981a0ce25005445597f597bd778db43728cd8
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Thu Apr 6 19:57:03 2006 +0000

    Move files into share directory

diff --git a/web/static/css/app-base.css b/share/web/static/css/app-base.css
similarity index 100%
rename from web/static/css/app-base.css
rename to share/web/static/css/app-base.css
diff --git a/web/templates/_elements/markup b/share/web/templates/_elements/markup
similarity index 100%
rename from web/templates/_elements/markup
rename to share/web/templates/_elements/markup
diff --git a/web/templates/_elements/nav b/share/web/templates/_elements/nav
similarity index 100%
rename from web/templates/_elements/nav
rename to share/web/templates/_elements/nav
diff --git a/web/templates/_elements/page_nav b/share/web/templates/_elements/page_nav
similarity index 100%
rename from web/templates/_elements/page_nav
rename to share/web/templates/_elements/page_nav
diff --git a/web/templates/_elements/sidebar b/share/web/templates/_elements/sidebar
similarity index 100%
rename from web/templates/_elements/sidebar
rename to share/web/templates/_elements/sidebar
diff --git a/web/templates/create b/share/web/templates/create
similarity index 100%
rename from web/templates/create
rename to share/web/templates/create
diff --git a/web/templates/edit b/share/web/templates/edit
similarity index 100%
rename from web/templates/edit
rename to share/web/templates/edit
diff --git a/web/templates/history b/share/web/templates/history
old mode 100755
new mode 100644
similarity index 100%
rename from web/templates/history
rename to share/web/templates/history
diff --git a/web/templates/let/confirm_email b/share/web/templates/let/confirm_email
old mode 100755
new mode 100644
similarity index 100%
rename from web/templates/let/confirm_email
rename to share/web/templates/let/confirm_email
diff --git a/web/templates/login b/share/web/templates/login
old mode 100755
new mode 100644
similarity index 100%
rename from web/templates/login
rename to share/web/templates/login
diff --git a/web/templates/logout b/share/web/templates/logout
old mode 100755
new mode 100644
similarity index 100%
rename from web/templates/logout
rename to share/web/templates/logout
diff --git a/web/templates/pages b/share/web/templates/pages
similarity index 100%
rename from web/templates/pages
rename to share/web/templates/pages
diff --git a/web/templates/recent b/share/web/templates/recent
similarity index 100%
rename from web/templates/recent
rename to share/web/templates/recent
diff --git a/web/templates/signup b/share/web/templates/signup
old mode 100755
new mode 100644
similarity index 100%
rename from web/templates/signup
rename to share/web/templates/signup
diff --git a/web/templates/view b/share/web/templates/view
similarity index 100%
rename from web/templates/view
rename to share/web/templates/view

commit fa4cba8ff720c97dd72d054794907e9e4461b458
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun Apr 23 21:48:10 2006 +0000

    We were using an illegal method call

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 66d0e27..76f4fea 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -109,7 +109,7 @@ sub _add_revision {
 sub set_content {
     my $self    = shift;
     my $content = shift;
-    my ( $val, $msg ) = $self->SUPER::set_content($content);
+    my ( $val, $msg ) = $self->_set(column => 'content', value => $content);
     $self->_add_revision( content => $content,
                     updated_by =>( $self->current_user? $self->current_user->user_object : undef )
                 );

commit e4df55d05b057c9be536806a636dfe80c7a1181f
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Wed May 3 20:36:36 2006 +0000

    Diffs of revisions

diff --git a/Makefile.PL b/Makefile.PL
index 456dedf..e79fd94 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -4,4 +4,5 @@ version('0.01');
 requires('Jifty');
 requires('Text::Markdown');
 requires('HTML::Scrubber');
+requires('Text::Diff::HTML');
 WriteAll;
diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index 8291192..46e36f9 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -30,6 +30,50 @@ sub create {
 
 }
 
+sub previous {
+    my $self = shift;
+    return undef unless $self->id;
+
+    my $revisions = Wifty::Model::RevisionCollection->new;
+    $revisions->limit(
+        column         => 'page',
+        value          => $self->page->id,
+        quote_value    => 0,
+        case_sensitive => 1
+    );
+    $revisions->limit(
+        column         => 'id',
+        operator       => '<',
+        value          => $self->id,
+        quote_value    => 0,
+        case_sensitive => 1
+    );
+    $revisions->order_by( { column => 'id' } );
+    return $revisions->last;
+}
+
+sub next {
+    my $self = shift;
+    return undef unless $self->id;
+
+    my $revisions = Wifty::Model::RevisionCollection->new;
+    $revisions->limit(
+        column         => 'page',
+        value          => $self->page->id,
+        quote_value    => 0,
+        case_sensitive => 1
+    );
+    $revisions->limit(
+        column         => 'id',
+        operator       => '>',
+        value          => $self->id,
+        quote_value    => 0,
+        case_sensitive => 1
+    );
+    $revisions->order_by( { column => 'id' } );
+    return $revisions->first;
+}
+
 =head2 current_user_can RIGHT
 
 We're using L<Jifty::RightsFrom> to pass off ACL decisions to this
diff --git a/share/web/static/css/app-base.css b/share/web/static/css/app-base.css
index 638d94b..de47f2e 100644
--- a/share/web/static/css/app-base.css
+++ b/share/web/static/css/app-base.css
@@ -61,6 +61,8 @@ div#syntax {
     right: 2em;
 }
 
-
-
-
+.file span { display: block; clear: both; }
+.file .fileheader, .file .hunkheader {color: #888; }
+.file .hunk .ctx { background: #eee;}
+.file .hunk ins { background: #dfd; text-decoration: none; display: block; }
+.file .hunk del { background: #fdd; text-decoration: none; display: block; }
diff --git a/share/web/templates/_elements/diff b/share/web/templates/_elements/diff
new file mode 100644
index 0000000..858d4c0
--- /dev/null
+++ b/share/web/templates/_elements/diff
@@ -0,0 +1,24 @@
+<%args>
+$page
+$from =>undef
+$to => undef
+</%args>
+<%init>
+
+$to   ||= $page->revisions->last;
+$from ||= $to->previous || Wifty::Model::Revision->new;
+
+my $before = $to->previous;
+my $after  = $to->next;
+
+use Text::Diff ();
+my $diff = Text::Diff::diff(\($from->content), \($to->content), { STYLE => 'Text::Diff::HTML' });
+
+</%init>
+% if ($before) {
+<span style="float: left"><% Jifty->web->link(url => "/view/".$page->name."/".$before->id, label => "Previous revision") %></span>
+% }
+% if ($after) {
+<span style="float: right"><% Jifty->web->link(url => "/view/".$page->name."/".$after->id, label => "Next revision") %></span>
+% }
+<pre><% $diff |n%></pre><hr />
diff --git a/share/web/templates/view b/share/web/templates/view
index f81e26e..35c7668 100644
--- a/share/web/templates/view
+++ b/share/web/templates/view
@@ -5,6 +5,8 @@ $revision
 <& /_elements/page_nav, page => $page->name, rev => $revision->id &>
 <&|/_elements/wrapper, title => $page->name . ($revision->id ? " as of ".$revision->created : '') &>
 % if ($revision->id) {
+<& _elements/diff, page => $page, to => $revision &>
+
 <% $page->wiki_content($revision->content) |n%>
 % } else {
 <% $page->wiki_content |n %>

commit 324075cf48863db338e1c29adde74fe2ad07d47f
Author: Eric Wilhelm <ewilhelm at cpan.org>
Date:   Thu May 4 18:45:24 2006 +0000

    share/web/templates/_elements/markup - made 'Wiki Syntax Help' a DHTML flyout
    share/web/static/css/app-base.css - width tweak

diff --git a/share/web/static/css/app-base.css b/share/web/static/css/app-base.css
index de47f2e..0416cc6 100644
--- a/share/web/static/css/app-base.css
+++ b/share/web/static/css/app-base.css
@@ -55,7 +55,7 @@ div#syntax {
     border: 1px solid #333;
     padding: 3px;
     font-size: 0.8em;
-    width: 25%;
+    width: 20em;
     position: absolute;
     top: 10em;
     right: 2em;
diff --git a/share/web/templates/_elements/markup b/share/web/templates/_elements/markup
index b6b8cca..2fcea61 100644
--- a/share/web/templates/_elements/markup
+++ b/share/web/templates/_elements/markup
@@ -1,5 +1,32 @@
+<script>
+   // javascript flyout by Eric Wilhelm
+   // TODO use images for minimize/maximize button
+   var targetDiv = 'syntax_content';
+   var divCont;
+   var effectDone = false;
+   var minimize = '-';
+   var maximize = '+';
+
+   function toggleEffect() {
+      if ( !effectDone ) {
+         effectDone = true;
+	     divCont = $(targetDiv).innerHTML;
+         $(targetDiv).innerHTML = '';
+		 $('toggle').innerHTML = maximize;
+      }
+      else {
+         effectDone = false;
+         $(targetDiv).innerHTML = divCont;
+		 $('toggle').innerHTML = minimize;
+      }
+   }
+
+</script>
 <div id="syntax">
-<h2>Wiki Syntax</h2>
+<div><a href="javascript:toggleEffect()"><b>Wiki Syntax Help</b></a>
+<a id="toggle" style="color:#5b5b5b;text-decoration:none;" href="javascript:toggleEffect()"></a>
+</div>
+<div id="syntax_content">
 
 <h3>Phrase Emphasis</h3>
 
@@ -54,4 +81,8 @@ by at least 4 spaces.</p>
 <p>Three or more dashes: <code>---</code></p>
 
 <address>(Thanks to <a href="http://daringfireball.net/projects/markdown/dingus">Daring Fireball</a>)</address>
+</div>
 </div> 
+<script>
+toggleEffect();
+</script>

commit 94aa491e5756101ba8b49668a022e5a15175d4b9
Author: Eric Wilhelm <ewilhelm at cpan.org>
Date:   Thu May 4 19:31:23 2006 +0000

    share/web/templates/_elements/markup - just using scriptaculous Element.toggle() for now (has no hooks for minimize/maximize icon switching?)

diff --git a/share/web/templates/_elements/markup b/share/web/templates/_elements/markup
index 2fcea61..7bfa695 100644
--- a/share/web/templates/_elements/markup
+++ b/share/web/templates/_elements/markup
@@ -1,30 +1,5 @@
-<script>
-   // javascript flyout by Eric Wilhelm
-   // TODO use images for minimize/maximize button
-   var targetDiv = 'syntax_content';
-   var divCont;
-   var effectDone = false;
-   var minimize = '-';
-   var maximize = '+';
-
-   function toggleEffect() {
-      if ( !effectDone ) {
-         effectDone = true;
-	     divCont = $(targetDiv).innerHTML;
-         $(targetDiv).innerHTML = '';
-		 $('toggle').innerHTML = maximize;
-      }
-      else {
-         effectDone = false;
-         $(targetDiv).innerHTML = divCont;
-		 $('toggle').innerHTML = minimize;
-      }
-   }
-
-</script>
 <div id="syntax">
-<div><a href="javascript:toggleEffect()"><b>Wiki Syntax Help</b></a>
-<a id="toggle" style="color:#5b5b5b;text-decoration:none;" href="javascript:toggleEffect()"></a>
+<div><a href="javascript:;" onclick="Element.toggle('syntax_content');return(false);"><b>Wiki Syntax Help</b></a>
 </div>
 <div id="syntax_content">
 
@@ -84,5 +59,8 @@ by at least 4 spaces.</p>
 </div>
 </div> 
 <script>
-toggleEffect();
+   // javascript flyout by Eric Wilhelm
+   // TODO use images for minimize/maximize button
+   // Is there a way to add a callback?
+   Element.toggle('syntax_content');
 </script>

commit 3fa93108747e6903145536d9612faa33fca75bdf
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun May 7 22:27:10 2006 +0000

    Cleaned up the edit page after the CSS change

diff --git a/share/web/templates/edit b/share/web/templates/edit
index 380691e..f0c7bee 100644
--- a/share/web/templates/edit
+++ b/share/web/templates/edit
@@ -7,11 +7,7 @@ $viewer
 <&|/_elements/wrapper, title => 'Edit: '.$page->name . ($revision->id ? " as of ".$revision->created : '')  &>
 <% Jifty->web->form->start %>
 <% Jifty->web->form->next_page( url => '/view/'.$page->name) %>
-% if ($revision->id) {
-<% $viewer->form_field('content', default_value => $revision->content )%>
-% } else { 
-<% $viewer->form_field('content') %>
-% }
+<% $viewer->form_field('content', ($revision->id ? (default_value => $revision->content) : (undef, undef)), rows=> 30, cols => 80 )%>
 <% Jifty->web->form->submit( label => 'Save') %>
 <% Jifty->web->form->end %>
 <& /_elements/markup &>

commit b31a1b9b0a59f64d4ee6304a50d59eb06f4e194c
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Tue May 9 22:51:15 2006 +0000

    Typo fix

diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index 46e36f9..8b9eadd 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -80,7 +80,7 @@ We're using L<Jifty::RightsFrom> to pass off ACL decisions to this
 update's page.  But we need to make sure that page history entries aren't
 editable, except by superusers. So we override C<current_user_can>
 to give the arguments a brief massage before handing off to
-C<urrent_user_can> (which we inherit).
+C<current_user_can> (which we inherit).
 
 =cut
 

commit be8d64ea2cca86b22ac68005d810bdc14ba701be
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Wed May 10 22:14:08 2006 +0000

    Fixing the saving of updated_by by Wifty, and keeping track of
    revisions' authors.

diff --git a/etc/config.yml b/etc/config.yml
index eb96db9..b92a460 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -6,7 +6,7 @@ framework:
     Driver: SQLite
     Host: localhost
     User: postgres
-    Version: 0.0.19
+    Version: 0.0.20
     Password: ''
     RequireSSL: 0
 #  Mailer: IO
diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 76f4fea..4b4bf18 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -99,9 +99,9 @@ sub _add_revision {
 
     my $rev = Wifty::Model::Revision->new( current_user => Wifty::CurrentUser->superuser);
     $rev->create(
-        page    => $self->id,
-        content => $args{'content'},
-        # by      => $args{'updated_by'}
+        page			=> $self->id,
+        content			=> $args{'content'},
+        created_by      => $args{'updated_by'}
     );
 
 }
@@ -127,9 +127,7 @@ sub _set {
 
     $self->SUPER::_set(
         column => 'updated_by',
-        value  => 
-                    ( $self->current_user? $self->current_user->user_object : undef )
-        
+        value  => ( $self->current_user? $self->current_user->user_object->id : undef )
     );
 
     return ( $val, $msg );
@@ -138,7 +136,7 @@ sub _set {
 
 =head2 current_user_can ACTION
 
-Let everybody create, read and update pages, but not delete the.
+Let everybody create, read and update pages, but not delete them.
 
 =cut
 
diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index 8b9eadd..7fe23b3 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -7,7 +7,7 @@ column content => type is 'text', render_as 'textarea';
 
 column created => type is 'timestamp';
 
-#column by => refers_to Wifty::Model::User, since '0.0.18';
+column created_by => refers_to Wifty::Model::User, since '0.0.20';
 
 
 package Wifty::Model::Revision;
diff --git a/share/web/templates/history b/share/web/templates/history
index 41d133b..34c9db3 100644
--- a/share/web/templates/history
+++ b/share/web/templates/history
@@ -8,7 +8,13 @@ $revisions
 % while (my $rev = $revisions->next) {
 <dt><% Jifty->web->link( label => $rev->created, 
                           url => '/view/'.$page->name.'/'.$rev->id
-                        ) %></dt>
+                        ) %>
+% if($rev->created_by) {
+  (<% $rev->created_by->name %>)
+% } else {
+  (Anonymous)
+% }
+</dt>
 <dd><%length($rev->content)%> bytes</dd>
 % }
 </ul>
diff --git a/share/web/templates/recent b/share/web/templates/recent
index 2d1941e..dd18b2b 100644
--- a/share/web/templates/recent
+++ b/share/web/templates/recent
@@ -5,7 +5,13 @@ $pages
 <dl id="recentudates">
 % while (my $page = $pages->next) {
 <dt><% Jifty->web->link( label => $page->name, url => '/view/'.$page->name)%></dt>
-<dd><%$page->updated%></dd>
-% } 
+<dd><%$page->updated%>
+% if($page->updated_by) {
+  (<% $page->updated_by->name %>)
+% } else {
+  (Anonymous)
+% }
+</dd>
+% }
 </dl>
 </&>

commit 445c15cb1d666862ecf957d0f5fc92e28ee4ce34
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Sat May 13 20:25:50 2006 +0000

    Fixing dependencies between Wifty models

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 4b4bf18..cdb4f44 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -1,5 +1,6 @@
 package Wifty::Model::Page::Schema;
 use Jifty::DBI::Schema;
+use Wifty::Model::User;
 
 column name => 
     type is 'text',

commit 8c0bdf25b8a092b0e147223356bcdb369dfbf4e2
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Sat May 13 21:04:11 2006 +0000

    Adding some additional Wifty Model tests

diff --git a/t/01-models.t b/t/01-models.t
new file mode 100644
index 0000000..289db28
--- /dev/null
+++ b/t/01-models.t
@@ -0,0 +1,42 @@
+#!/usr/bin/perl -w
+use warnings;
+use strict;
+
+=head1 DESCRIPTION
+
+A slightly more complicated test harness for the interactions between
+model classes.
+
+=cut
+
+
+use Jifty::Test tests => 9;
+
+use_ok('Wifty::Model::Page');
+use_ok('Wifty::Model::User');
+use_ok('Wifty::Model::Revision');
+
+my $system_user = Wifty::CurrentUser->superuser;
+
+my $user = Wifty::Model::User->new(current_user => $system_user);
+$user->create(email => 'test at email', name => 'Test User');
+ok($user, "Created a user model object");
+
+my $current_user = Wifty::CurrentUser->new(id => $user->id);
+ok($current_user, "Created a Wifty::CurrentUser");
+
+my $page = Wifty::Model::Page->new(current_user => $current_user);
+$page->create(name => "TestPage", content => "Test Content");
+is($page->updated_by->id, $user->id, "Model::Page set updated_by correctly");
+
+$page->set(content => "Second test");
+
+my $revs = Wifty::Model::RevisionCollection->new(current_user => $current_user);
+$revs->limit(column => "page", value => $page->id);
+
+is($revs->count, 1, "Model::Page stored a revision");
+
+my $revision = $revs->next;
+
+is($revision->page->id, $page->id, "Revision is of the correct page");
+is($revision->created_by->id, $current_user->id, "Revision has the correct creator");

commit 9cba41808a3e0b52dd736d07a329da40986d8818
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Mon May 15 19:24:30 2006 +0000

    Commenting some tests

diff --git a/t/01-models.t b/t/01-models.t
index 289db28..95fb190 100644
--- a/t/01-models.t
+++ b/t/01-models.t
@@ -18,6 +18,7 @@ use_ok('Wifty::Model::Revision');
 
 my $system_user = Wifty::CurrentUser->superuser;
 
+# Create a test user
 my $user = Wifty::Model::User->new(current_user => $system_user);
 $user->create(email => 'test at email', name => 'Test User');
 ok($user, "Created a user model object");
@@ -25,12 +26,14 @@ ok($user, "Created a user model object");
 my $current_user = Wifty::CurrentUser->new(id => $user->id);
 ok($current_user, "Created a Wifty::CurrentUser");
 
+#Create a page and check it
 my $page = Wifty::Model::Page->new(current_user => $current_user);
 $page->create(name => "TestPage", content => "Test Content");
 is($page->updated_by->id, $user->id, "Model::Page set updated_by correctly");
 
 $page->set(content => "Second test");
 
+# Make sure the page is creating revisions
 my $revs = Wifty::Model::RevisionCollection->new(current_user => $current_user);
 $revs->limit(column => "page", value => $page->id);
 

commit 32f066f6104ce3ee0f79921b0056bff9008d9b55
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Mon May 15 19:24:33 2006 +0000

    Removing an errant semicolon in the pages template

diff --git a/share/web/templates/pages b/share/web/templates/pages
index c157fd1..c39f5ae 100644
--- a/share/web/templates/pages
+++ b/share/web/templates/pages
@@ -8,7 +8,7 @@ $pages
         Jifty->web->link(
             label => $page->name,
             url   => '/view/' . $page->name
-            );
+            )
 
     %></li>
 % } 

commit 7eb648940d236a1e0d74f9124bca0f96e3c77a88
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun May 28 04:49:25 2006 +0000

    (empty commit message)

commit 0f75399b1a8c2b46676d1c8fe2f5c831dd56945a
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sun May 28 04:49:34 2006 +0000

    CSS tweaks from beppu

diff --git a/share/web/static/css/app-base.css b/share/web/static/css/app-base.css
index 0416cc6..275e4d7 100644
--- a/share/web/static/css/app-base.css
+++ b/share/web/static/css/app-base.css
@@ -1,6 +1,6 @@
 body { 
     background-color: #dddddd;
-
+    font-size: 85%;
 
 }
 
@@ -15,6 +15,10 @@ div#headers h1 {
 
 }
 
+div#headers { margin-left: 10px; }
+div#headers a { position: absolute; top: 0.5em; right: 1em; }
+
+
 a {
  color: black;
  font-style: bold;

commit 98764189e5a270fc8ef9b1f490a5d9b5a6161574
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Sat Jul 1 20:35:24 2006 +0000

    Jifty::Test -> Wifty::Test

diff --git a/t/01-models.t b/t/01-models.t
index 95fb190..962aebb 100644
--- a/t/01-models.t
+++ b/t/01-models.t
@@ -10,7 +10,7 @@ model classes.
 =cut
 
 
-use Jifty::Test tests => 9;
+use Wifty::Test tests => 9;
 
 use_ok('Wifty::Model::Page');
 use_ok('Wifty::Model::User');

commit b7dbbb26216a4b71cd6d68ca149b2b85085ca8e8
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Sat Jul 1 20:35:47 2006 +0000

    Adding some test users

diff --git a/lib/Wifty/Test.pm b/lib/Wifty/Test.pm
new file mode 100644
index 0000000..3aeb25e
--- /dev/null
+++ b/lib/Wifty/Test.pm
@@ -0,0 +1,53 @@
+use warnings;
+use strict;
+
+package Wifty::Test;
+use base qw/Jifty::Test/;
+
+=head2 setup
+
+Set up for testing. Calls L<Jifty::Test/setup> and L</setup_db>.
+
+=cut
+
+sub setup {
+    my $class = shift;
+    $class->SUPER::setup;
+    $class->setup_db;
+}
+
+=head2 setup_db
+
+Add two users to the database:
+
+Some User <someuser at localhost>, password 'sekrit'
+Other User <otheruser at localhost>, password 'motdepasse'
+
+This should be kept in sync with C<t/0-test-database>.
+
+=cut
+
+sub setup_db {
+    my $class = shift;
+
+    my $admin = Wifty::CurrentUser->superuser;
+
+    my $someuser = Wifty::Model::User->new(current_user => $admin);
+    $someuser->create(
+        name            => 'Some User',
+        email           => 'someuser at localhost',
+        password        => 'sekrit',
+        email_confirmed => 1,
+       );
+
+    my $otheruser = Wifty::Model::User->new(current_user => $admin);
+    $otheruser->create(
+        name            => 'Other User',
+        email           => 'otheruser at localhost',
+        password        => 'motdepasse',
+        email_confirmed => 1,
+       );
+
+}
+
+1;

commit 126512772ae2b649da7c2031c0673c13162c26b1
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Sat Jul 1 20:36:12 2006 +0000

    Test the users created in Wifty::Test

diff --git a/t/0-test-database.t b/t/0-test-database.t
new file mode 100644
index 0000000..d836aa1
--- /dev/null
+++ b/t/0-test-database.t
@@ -0,0 +1,35 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+=head1 DESCRIPTION
+
+Test the models set up by L<Wifty::Test>
+
+=cut
+
+use Wifty::Test no_plan => 1;
+
+my $admin = Wifty::CurrentUser->superuser;
+
+my $users = Wifty::Model::UserCollection->new(current_user => $admin);
+isa_ok($users, 'Wifty::Model::UserCollection');
+
+$users->unlimit;
+
+is($users->count, 2, "Got two users");
+
+my $user = $users->next;
+
+isa_ok($user, 'Wifty::Model::User');
+is($user->name, 'Some User', 'name ok');
+is($user->email, 'someuser at localhost', 'email ok');
+ok($user->password_is('sekrit'), 'password ok');
+is($user->email_confirmed, '1');
+
+$user = $users->next;
+isa_ok($user, 'Wifty::Model::User');
+is($user->name, 'Other User', 'name ok');
+is($user->email, 'otheruser at localhost', 'email ok');
+ok($user->password_is('motdepasse'), 'password ok');
+is($user->email_confirmed, '1');

commit 5c45f9708825f0637d129b3fb9e1338621506962
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Sat Jul 1 20:36:39 2006 +0000

    Don't you love plans?

diff --git a/t/0-test-database.t b/t/0-test-database.t
index d836aa1..39d523b 100644
--- a/t/0-test-database.t
+++ b/t/0-test-database.t
@@ -8,7 +8,7 @@ Test the models set up by L<Wifty::Test>
 
 =cut
 
-use Wifty::Test no_plan => 1;
+use Wifty::Test test => 12;
 
 my $admin = Wifty::CurrentUser->superuser;
 

commit b472c8fc8c97ceb66ae8263a48013ed7c4777e36
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Sat Jul 1 20:37:02 2006 +0000

    This was breaking tests

diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index a0b23ee..9a61bf1 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -90,12 +90,11 @@ sub current_user_can {
     my $self  = shift;
     my $right = shift;
     my %args  = (@_);
-    Carp::confess if ($right eq 'read' and not $args{'column'});
     if (    $right eq 'read'
         and $self->id == $self->current_user->id )
     {
         return 1;
-    } elsif ( $right eq 'read' and $args{'column'} eq 'name' ) {
+    } elsif ( $right eq 'read' and $args{'column'} and $args{'column'} eq 'name' ) {
         return (1);
 
     } elsif ( $right eq 'update'

commit e1388eb49af73d8b8e138ba2cc0c34d824844336
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Sat Jul 1 20:37:28 2006 +0000

    Fixing t/0-test-database and adding login tests

diff --git a/t/0-test-database.t b/t/0-test-database.t
index 39d523b..3a5330d 100644
--- a/t/0-test-database.t
+++ b/t/0-test-database.t
@@ -8,7 +8,7 @@ Test the models set up by L<Wifty::Test>
 
 =cut
 
-use Wifty::Test test => 12;
+use Wifty::Test tests => 12;
 
 my $admin = Wifty::CurrentUser->superuser;
 
diff --git a/t/02-login.t b/t/02-login.t
new file mode 100644
index 0000000..87bf108
--- /dev/null
+++ b/t/02-login.t
@@ -0,0 +1,55 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+=head1 DESCRIPTION
+
+Test that we can log in to Wifty
+
+=cut
+
+use constant PER_TRIAL => 3;
+
+use Wifty::Test tests => 5 + PER_TRIAL * 4;
+use Jifty::Test::WWW::Mechanize;
+
+my $server = Wifty::Test->make_server;
+
+my $URL = $server->started_ok;
+
+ok($URL, "Started a test server");
+
+my $mech = Jifty::Test::WWW::Mechanize->new();
+
+$mech->get_ok($URL, "Got the homepage");
+ok($mech->find_link(text_regex => qr/currently signed in/), 'Got the signin link');
+$mech->follow_link_ok(text_regex => qr/currently signed in/);
+
+sub try_login {
+    my $mech = shift;
+    my $user = shift;
+    my $pass = shift;
+    
+    {
+        local $Test::Builder::Level = $Test::Builder::Level;
+        $Test::Builder::Level++;
+        $mech->fill_in_action_ok('loginbox', email => $user, password => $pass);
+        $mech->submit_html_ok();
+    }
+}
+
+# Try logging in with a bad user
+try_login($mech, 'baduser at localhost', 'notmypassword');
+$mech->content_contains('No account has that email address', "Login failed with bad username");
+
+# With a blank password
+try_login($mech, 'someuser at localost', '');
+$mech->content_contains('need to fill in this field','Login fails with no password');
+
+# With the wrong password
+try_login($mech, 'someuser at localhost', 'badmemory');
+$mech->content_contains('may have mistyped','Login fails with wrong password');
+
+# Try a correct login
+try_login($mech, 'someuser at localhost', 'sekrit');
+$mech->content_contains('Welcome back','Logged in');

commit e7159328976f0d6c84b03159e6e7fe357237088b
Author: Audrey Tang <audreyt at audreyt.org>
Date:   Fri Jul 21 06:53:18 2006 +0000

    Wifty: Updated actions to use declarative parameters.

diff --git a/lib/Wifty/Action/Login.pm b/lib/Wifty/Action/Login.pm
index ed12a5a..09643bc 100644
--- a/lib/Wifty/Action/Login.pm
+++ b/lib/Wifty/Action/Login.pm
@@ -8,32 +8,27 @@ Wifty::Action::Login
 =cut
 
 package Wifty::Action::Login;
-use base qw/Wifty::Action Jifty::Action/;
-
-=head2 arguments
-
-Return the email and password form fields
-
-=cut
-
-sub arguments { 
-    return( { email => { label => 'Email address',
-                           mandatory => 1,
-                           ajax_validates => 1,
-                            }  ,
-
-              password => { type => 'password',
-                            label => 'Password',
-                            mandatory => 1
-                        },
-              remember => { type => 'checkbox',
-                            label => 'Remember me?',
-                            hints => 'If you want, your browser can remember your login for you',
-                            default => 0,
-                          }
-          });
-
-}
+use base qw/Wifty::Action/;
+use Jifty::Param::Schema;
+use Jifty::Action schema {
+
+param email =>
+    label is 'Email address',
+    is mandatory,
+    ajax validates;
+
+param password =>
+    type is 'password',
+    label is 'Password',
+    is mandatory;
+
+param remember =>
+    type is 'checkbox',
+    label is 'Remember me?',
+    hints is 'If you want, your browser can remember your login for you',
+    default is 0;
+
+};
 
 =head2 validate_email ADDRESS
 
diff --git a/lib/Wifty/Action/Logout.pm b/lib/Wifty/Action/Logout.pm
index 697b9f6..3a35611 100644
--- a/lib/Wifty/Action/Logout.pm
+++ b/lib/Wifty/Action/Logout.pm
@@ -10,16 +10,6 @@ Wifty::Action::Logout
 package Wifty::Action::Logout;
 use base qw/Wifty::Action Jifty::Action/;
 
-=head2 arguments
-
-Return the email and password form fields
-
-=cut
-
-sub arguments { 
-    return( { });
-}
-
 =head2 take_action
 
 Nuke the current user object
diff --git a/lib/Wifty/Action/ResetLostPassword.pm b/lib/Wifty/Action/ResetLostPassword.pm
index 813a781..a98292f 100755
--- a/lib/Wifty/Action/ResetLostPassword.pm
+++ b/lib/Wifty/Action/ResetLostPassword.pm
@@ -14,23 +14,22 @@ address is really theirs, when claiming that they lost their password.
 =cut
 
 package Wifty::Action::ResetPassword;
-use base qw/Wifty::Action Jifty::Action/;
-
 use Wifty::Model::User;
+use base qw/Wifty::Action/;
 
-=head2 arguments
+use Jifty::Param::Schema;
+use Jifty::Action schema {
 
-ConfirmEmail has the following fields: address, code, password, and password_confirm.
-Note that it can get the first two from the confirm dhandler.
+param password =>
+    type is 'password',
+    ! is sticky;
 
-=cut
+param password_confirm =>
+    type is 'password',
+    label is 'type your password again',
+    ! is sticky;
 
-sub arguments { 
-    return( { 
-              password => { type => 'password', sticky => 0 },
-              password_confirm => { type => 'password', sticky => 0, label => 'type your password again' },
-          }); 
-}
+};
 
 =head2 take_action
 
diff --git a/lib/Wifty/Action/SendAccountConfrimation.pm b/lib/Wifty/Action/SendAccountConfrimation.pm
index dc30667..1073862 100755
--- a/lib/Wifty/Action/SendAccountConfrimation.pm
+++ b/lib/Wifty/Action/SendAccountConfrimation.pm
@@ -8,27 +8,21 @@ Wifty::Action::ResendConfirmation
 =cut
 
 package Wifty::Action::ResendConfirmation;
-use base qw/Wifty::Action Jifty::Action/;
-
-__PACKAGE__->mk_accessors(qw(user_object));
 
 use Wifty::Model::User;
+use base qw/Wifty::Action/;
 
-=head2 arguments
-
-The field for C<ResendConfirmation> is:
-
-=over 4
-
-=item address: the email address
+__PACKAGE__->mk_accessors(qw(user_object));
 
-=back
+use Jifty::Param::Schema;
+use Jifty::Action schema {
 
-=cut
+param address =>
+    label is 'email address',
+    is mandatory,
+    default is '';
 
-sub arguments {
-    return ( { address => { label     => 'email address', mandatory => 1, default_value => "", }, });
-}
+};
 
 =head2 setup
 

commit 72dbbffdaa4e2723cd0069c1fd3f0abb9b021acf
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Wed Jul 26 14:56:17 2006 +0000

    Fixing history display with anonymous edits

diff --git a/share/web/templates/history b/share/web/templates/history
index 34c9db3..f44c6b4 100644
--- a/share/web/templates/history
+++ b/share/web/templates/history
@@ -9,7 +9,7 @@ $revisions
 <dt><% Jifty->web->link( label => $rev->created, 
                           url => '/view/'.$page->name.'/'.$rev->id
                         ) %>
-% if($rev->created_by) {
+% if($rev->created_by->id) {
   (<% $rev->created_by->name %>)
 % } else {
   (Anonymous)

commit 903410aa12ede77243fd55cbbdcbec5c788e6611
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Wed Jul 26 14:56:52 2006 +0000

    Adding simple access controls -- the ability to require users to be logged in to edit and create pages

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 6480c3e..e9bbfcd 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -15,7 +15,13 @@ on '/', run {
 on '/create/*', run {
      set page => $1;
      set action => Jifty->web->new_action( class => 'CreatePage' );
-     show("/create");
+
+     my $p = Wifty::Model::Page->new();
+     if($p->current_user_can('create')) {
+         show("/create");
+     } else {
+         show("/no_such_page");
+     }
 };
 
 # View or edit a page
diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index cdb4f44..54f3d7a 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -137,7 +137,9 @@ sub _set {
 
 =head2 current_user_can ACTION
 
-Let everybody create, read and update pages, but not delete them.
+Let everybody read pages. If RequireAuth is set in the app config,
+only allow logged-in users to create and edit pages. Otherwise, allow
+anyone.
 
 =cut
 
@@ -145,12 +147,17 @@ sub current_user_can {
     my $self = shift;
     my $type = shift;
 
-    # We probably want something like this eventually:
-    if ($type =~ /(?:create|read|update)/i) {
+    if ($type eq 'create' || $type eq 'update') {
+        return 0 if
+         Jifty->config->app('RequireAuth')
+           && !$self->current_user->is_superuser
+           && !$self->current_user->id;
+        return 1;
+    } elsif($type eq 'read') {
         return 1;
-    } else {
-        return $self->SUPER::current_user_can($type, @_);
     }
+
+    return $self->SUPER::current_user_can($type, @_);
 }
 
 1;
diff --git a/share/web/templates/_elements/page_nav b/share/web/templates/_elements/page_nav
index 61993fd..4b27a7f 100644
--- a/share/web/templates/_elements/page_nav
+++ b/share/web/templates/_elements/page_nav
@@ -8,9 +8,11 @@ my $this = $top->child(
         sort_order => 5
 );
 
+my $page_obj = Wifty::Model::Page->new();
+$page_obj->load_by_cols(name => $page);
 
 $this->child( View => url => '/view/'.$subpath);
-$this->child( Edit => url => '/edit/'.$subpath);
+$this->child( Edit => url => '/edit/'.$subpath) if $page_obj->current_user_can('update');
 $this->child( History => url => '/history/'.$page);
 $this->child( Latest => url => '/view/'.$page) if ($rev);
 
diff --git a/share/web/templates/edit b/share/web/templates/edit
index f0c7bee..d8ca040 100644
--- a/share/web/templates/edit
+++ b/share/web/templates/edit
@@ -3,12 +3,23 @@ $page
 $revision
 $viewer 
 </%args>
+<%init>
+my $can_edit = $page->current_user_can('update');
+</%init>
 <&/_elements/page_nav, page => $page->name, rev => $revision->id &>
 <&|/_elements/wrapper, title => 'Edit: '.$page->name . ($revision->id ? " as of ".$revision->created : '')  &>
 <% Jifty->web->form->start %>
+% unless($can_edit) {
+  <p> You don't have permission to edit this page. Perhaps
+  <% Jifty->web->tangent(url => '/login', label => 'logging in') %>
+  would help. In the mean time, though, you're welcome to view and
+  copy the source of this page. </p>
+% }
 <% Jifty->web->form->next_page( url => '/view/'.$page->name) %>
 <% $viewer->form_field('content', ($revision->id ? (default_value => $revision->content) : (undef, undef)), rows=> 30, cols => 80 )%>
+% if($can_edit) {
 <% Jifty->web->form->submit( label => 'Save') %>
+% }
 <% Jifty->web->form->end %>
 <& /_elements/markup &>
 </&>
diff --git a/share/web/templates/no_such_page b/share/web/templates/no_such_page
new file mode 100644
index 0000000..fbdc076
--- /dev/null
+++ b/share/web/templates/no_such_page
@@ -0,0 +1,11 @@
+<&|/_elements/wrapper, title => 'No such page: '. $page&>
+
+  <p>Unfortunately, you've tried to reach a page that doesn't exist
+    yet, and you don't have permissions to create pages. If you
+    <% Jifty->web->tangent(url => '/login', label => 'login') %>,
+    you'll be able to create new pages of your own.</p>
+    
+</&>
+<%args>
+$page => undef
+</%args>

commit c94d216a00e5ff9585d2a1a1a9ad09b16e9c0dac
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Wed Jul 26 14:57:30 2006 +0000

    Adding commented-out RequireAuth to config.yml

diff --git a/etc/config.yml b/etc/config.yml
index b92a460..c48dc8d 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -17,3 +17,5 @@ framework:
   Web:
     StaticRoot: share/web/static
     TemplateRoot: share/web/templates
+application:
+#  RequireAuth: 1

commit 0f1bac5e31f479d9c507ddca9147f666b1d449ac
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Wed Jul 26 15:44:35 2006 +0000

    Fixing recent for anonymous users

diff --git a/share/web/templates/recent b/share/web/templates/recent
index dd18b2b..08bc6f0 100644
--- a/share/web/templates/recent
+++ b/share/web/templates/recent
@@ -6,7 +6,7 @@ $pages
 % while (my $page = $pages->next) {
 <dt><% Jifty->web->link( label => $page->name, url => '/view/'.$page->name)%></dt>
 <dd><%$page->updated%>
-% if($page->updated_by) {
+% if($page->updated_by->id) {
   (<% $page->updated_by->name %>)
 % } else {
   (Anonymous)

commit 368fcc465fa1d2ada1a62ddff543de44cd88e3d2
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Mon Jul 31 05:59:48 2006 +0000

    Add WikiName preference
    * Give wifty a big layout/styling overhaul to make it look nicer by default (and more like a wiki)

diff --git a/etc/config.yml b/etc/config.yml
index c48dc8d..b1a8d98 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -19,3 +19,4 @@ framework:
     TemplateRoot: share/web/templates
 application:
 #  RequireAuth: 1
+  WikiName: A Wiki
diff --git a/share/web/static/css/app-base.css b/share/web/static/css/app-base.css
index 275e4d7..d6aeb11 100644
--- a/share/web/static/css/app-base.css
+++ b/share/web/static/css/app-base.css
@@ -1,72 +1,132 @@
 body { 
-    background-color: #dddddd;
-    font-size: 85%;
+    background-color: #ddd;
+    font-size: 76%;
+    margin: 0 1.5em 1.5em 1.5em;
+}
 
+a {
+    color: #c12d06;
 }
 
-div#headers h1 {
- background: green;
- color: white;
- padding: 0.2em;
- border: 0;
- margin: 0;
- margin: -10px 0  0 -10px ;
- margin-right: -10px;
+#salutation {
+    position: absolute;
+    top: 0.5em;
+    right: 1.8em;
+    font-family: sans-serif;
+    font-size: 0.9em;
+}
 
+#header {
+    margin-top: 2.3em;
 }
 
-div#headers { margin-left: 10px; }
-div#headers a { position: absolute; top: 0.5em; right: 1em; }
+#wikiheader {
+    float: right;
+    text-align: right;
+    padding-right: 1px;
+    width: 28%;
+}
 
+#wikiname {
+    margin: 0;
+    font-size: 1.7em;
+}
 
-a {
- color: black;
- font-style: bold;
+#wikiname a {
+    color: black;
+    text-decoration: none;
 }
 
-#jifty-wait-message {
- display: none;
+#pageheader {
+    margin-top: 1em;
+    width: 70%;
+    padding-left: 1px;
 }
 
-div#content {
-    background: #ffffff;
-   padding: 2em;
+h1, h2, h3, h4, h5, h6 {
+    font-family: sans-serif;
 }
 
-div#salutation {
-    float: right;
-    font-style: italic;
+h1 { font-size: 1.5em; }
+h2 { font-size: 1.3em; }
+h3 { font-size: 1.2em; }
+h4 { font-size: 1.1em; }
+
+#pagename {
+    margin: 0.2em 0 0 0;
+    font-size: 2em;
 }
 
-textarea.content {
-  height: 50em;
-  background: #ddd;
-  border: 1px solid black;
-  padding: 5px;
+#content {
+    background: white;
+    padding: 0.3em 1em 0.5em 1em;
+    border: 1px solid #ccc;
+    clear: both;
+    margin-top: 0.5em;
+    font-family: sans-serif;
+    font-size: 1.1em;
 }
 
+* html #content { margin-top: 0; }
 
-input[type=submit] {
-    border: 1px solid black;
-    font-size: 1.5em;
-    margin: 5px;
-        
+#update #content, #create #content {
+    position: relative;
 }
 
-div#syntax {
+#content h1,
+#content h2,
+#content h3,
+#content h4
+{
+    margin: 0.2em 0;
+}
+
+#syntax {
     float: right;
     background: white;
-    border: 1px solid #333;
-    padding: 3px;
-    font-size: 0.8em;
-    width: 20em;
+    border: 1px solid #888;
+    padding: 0.2em 0.4em;
+    font-size: 0.9em;
+    width: 25%;
     position: absolute;
-    top: 10em;
-    right: 2em;
+    top: 1em;
+    right: 1em;
+}
+
+#syntax code, #syntax tt {
+    font-size: 1.2em;
+}
+
+.diff {
+    font-size: 1.1em;
+    overflow: auto;
+}
+
+.file span {
+    display: block;
+    clear: both;
+}
+
+.file .fileheader, .file .hunkheader {
+    color: #666;
 }
 
-.file span { display: block; clear: both; }
-.file .fileheader, .file .hunkheader {color: #888; }
-.file .hunk .ctx { background: #eee;}
-.file .hunk ins { background: #dfd; text-decoration: none; display: block; }
-.file .hunk del { background: #fdd; text-decoration: none; display: block; }
+.file .hunk .ctx {
+    background: #eee;
+}
+
+.file .hunk ins {
+    background: #dfd;
+    text-decoration: none;
+    display: block;
+}
+
+.file .hunk del {
+    background: #fdd;
+    text-decoration: none;
+    display: block;
+}
+
+#jifty-wait-message {
+    display: none;
+}
diff --git a/share/web/static/css/app.css b/share/web/static/css/app.css
new file mode 100644
index 0000000..2f5ed0f
--- /dev/null
+++ b/share/web/static/css/app.css
@@ -0,0 +1,96 @@
+div.argument-content {
+    width: 70%;
+}
+
+* html div.form_field,
+* html form .submit_button { position: relative; }
+
+label.argument-content {
+    display: none !important;
+}
+
+textarea.argument-content {
+    width: 100%;
+    font-size: 1.2em;
+}
+
+* html textarea.argument-content {
+    font-size: 1em;
+}
+
+form .submit_button input {
+    margin-left: 0;
+    font-size: 1.1em;
+    color: white;
+    background: #3d4286;
+    border: 1px outset #3d4286;
+}
+
+#recentupdates dt {
+    float: left;
+    clear: left;
+    width: 45%;
+}
+
+#recentupdates dd {
+    margin-left: 2em;
+    padding-left: 0;
+    margin-bottom: 0.5em;
+    float: left;
+    width: 50%;
+}
+
+* html #recentupdates dt,
+* html #recentupdates dd { position: relative; }
+
+#history dd {
+    font-size: 0.95em;
+    color: #444;
+    margin-left: 2em;
+    padding-left: 0;
+    margin-bottom: 0.5em;
+}
+
+hr {
+    border: none;
+    border-top: 1px solid #777;
+}
+
+.revision_nav {
+    margin-top: 0.5em;
+}
+
+/* Login box */
+
+#login-box form {
+    padding-bottom: 0;
+}
+
+#login-box label {
+    width: 10em;
+}
+
+#login-box input.text,
+#login-box input.password {
+    font-size: 1.1em;
+}
+
+#login-box span.error {
+    font-size: 0.8em;
+}
+
+#login-box .argument-remember .hints {
+    float: left;
+    clear: none;
+    width: 50%;
+    font-size: 0.8em;
+    padding: 0 0 0 0.3em;
+}
+
+* html #login-box .argument-remember .hints {
+    width: auto;
+}
+
+#login-box input.argument-remember {
+    float: left;
+}
diff --git a/share/web/static/css/base.css b/share/web/static/css/base.css
new file mode 100644
index 0000000..27e44e2
--- /dev/null
+++ b/share/web/static/css/base.css
@@ -0,0 +1,68 @@
+.error {
+    color: #a00000;
+}
+
+.warning {
+    color: #00a0a0;
+}
+
+hr.clear {
+    clear: both;
+    visibility: hidden;
+    height: 0;
+    padding: 0;
+    margin: 0;
+} 
+
+.messages .message {
+    display: block;
+}
+
+div#messages,  div#errors {
+     background-color: rgb(240,234,183);
+     border: 1px solid rgb(230,224,173);
+     margin-top: 10px;
+     margin-bottom: 10px;
+     padding: 5px;
+     font-size: 1.2em;
+}
+
+div.spacer {
+    clear: both;
+}
+
+.next-page, .prev-page {
+    display: block;
+    float: left;
+    margin: 0.5em 0;
+    padding: 0.2em 0.5em 0.5em 0.5em;
+    border-top: 1px solid gray;
+}
+
+.next-page { padding-right: 1em; }
+.prev-page { padding-left: 1em; }
+
+div#jifty-wait-message {
+    color: red;
+    background: black;
+    font-size: 2em;
+    position: fixed;
+    top: 10px;
+    right: 10px;
+    z-index: 42;
+}
+
+div.warning {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    background-color: red;
+    color: white;
+    padding: .5em;
+    border-bottom: 1px solid #000;
+}
+
+div.warning a {
+    color: white;
+}
diff --git a/share/web/static/css/forms.css b/share/web/static/css/forms.css
new file mode 100644
index 0000000..981390f
--- /dev/null
+++ b/share/web/static/css/forms.css
@@ -0,0 +1,107 @@
+/* buttons */
+
+input.button {
+    margin-top: 0.6em;
+    padding: 0.15em 1em;
+    font-weight: bold;
+}
+
+* html input.button {
+    padding: 0 0.1em;
+}
+
+/* fields */
+
+input.text, input.date, input.password, input.combo-text, textarea, select {
+    border-top: 1px solid #7c7c7c;
+    border-left: 1px solid #c3c3c3;
+    border-right: 1px solid #c3c3c3;
+    border-bottom: 1px solid #ddd;
+    background: #fff url(/static/images/css/fieldbg.gif) repeat-x top;
+    padding: 0.2em;
+    font-size: 1em;
+}
+
+label, span.label {
+    font-size: 0.9em;
+}
+
+.form_field .hints {
+    font-size: 0.9em;
+    color: #777;
+}
+
+/* layout */
+
+.form_field {
+    clear: both;
+}
+
+label, span.label {
+    display: block;
+    width: 20%;
+    float: left;
+    text-align: right;
+    margin-right: 0.5em;
+}
+
+* html label, * html span.label {
+    width: 22%;
+}
+
+form .hints {
+    display: block;
+    clear: both;
+}
+
+html>body form .hints {
+    padding: 0.2em 0 0.2em 21%;
+}
+
+* html form .hints {
+    padding-left: 11.5%;
+}
+
+form .error {
+    display: block;
+    clear: both;
+}
+
+.form_field {
+    padding: 0.3em 0 0 0;
+}
+
+.inline .hints {
+    padding-left: 0;
+}
+
+.inline label, .inline span.label {
+    float: none;
+    width: auto;
+    text-align: left;
+    margin-right: auto;
+    font-weight: bold;
+}
+
+.inline .form_field {
+    float: left;
+    clear: none;
+    margin-right: 0.5em;
+}
+
+.inline .button {
+    margin-top: 1.1em;
+}
+
+.button_line {
+    border-top: 1px solid #ccc;
+    padding-right: 5em;
+    margin-top: 1.5em;
+    clear: both;
+    direction: rtl;
+}
+
+form .line {
+    clear: both;
+}
+
diff --git a/share/web/static/css/keybindings.css b/share/web/static/css/keybindings.css
new file mode 100644
index 0000000..87f864e
--- /dev/null
+++ b/share/web/static/css/keybindings.css
@@ -0,0 +1,25 @@
+div#keybindings {
+    color: #666666;
+    margin-top: 2em;
+}
+
+dl.keybindings .keybinding {
+    display: inline;
+}
+
+dl.keybindings dt  {
+    margin: 0;
+    font-weight: bold;
+    display: inline;
+
+}
+dl.keybindings dt:after  {
+    content: ":";
+
+}
+dl.keybindings dd  {
+    margin-right: 1.5em;
+    margin-left: 0.5em;
+    display: inline;
+    white-space: nowrap;
+}
diff --git a/share/web/static/css/main.css b/share/web/static/css/main.css
new file mode 100644
index 0000000..f1e909f
--- /dev/null
+++ b/share/web/static/css/main.css
@@ -0,0 +1,9 @@
+ at import "app-base.css";
+ at import "base.css";
+ at import "nav.css";
+ at import "keybindings.css";
+ at import "forms.css";
+ at import "halos.css";
+ at import "app.css";
+ at import "autocomplete.css";
+ at import "notices.css";
diff --git a/share/web/static/css/nav.css b/share/web/static/css/nav.css
new file mode 100644
index 0000000..b71722f
--- /dev/null
+++ b/share/web/static/css/nav.css
@@ -0,0 +1,21 @@
+ul.menu {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+    font-size: 1.2em;
+    font-family: sans-serif;
+}
+
+ul.menu li {
+    display: inline;
+    padding-right: 1em;
+}
+
+#pageheader ul.menu {
+    margin-top: -0.1em;
+}
+
+#wikiheader ul.menu li {
+    padding-right: 0;
+    padding-left: 1em;
+}
\ No newline at end of file
diff --git a/share/web/templates/_elements/diff b/share/web/templates/_elements/diff
index 858d4c0..0e874d7 100644
--- a/share/web/templates/_elements/diff
+++ b/share/web/templates/_elements/diff
@@ -15,10 +15,16 @@ use Text::Diff ();
 my $diff = Text::Diff::diff(\($from->content), \($to->content), { STYLE => 'Text::Diff::HTML' });
 
 </%init>
+<div class="revision_nav">
 % if ($before) {
-<span style="float: left"><% Jifty->web->link(url => "/view/".$page->name."/".$before->id, label => "Previous revision") %></span>
+<span class="prev"><% Jifty->web->link(url => "/view/".$page->name."/".$before->id, label => "Previous revision") %></span>
+% }
+% if ( $before and $after ) {
+ | 
 % }
 % if ($after) {
-<span style="float: right"><% Jifty->web->link(url => "/view/".$page->name."/".$after->id, label => "Next revision") %></span>
+<span class="next"><% Jifty->web->link(url => "/view/".$page->name."/".$after->id, label => "Next revision") %></span>
 % }
-<pre><% $diff |n%></pre><hr />
+</div>
+<pre class="diff"><% $diff |n%></pre>
+<hr />
diff --git a/share/web/templates/_elements/header b/share/web/templates/_elements/header
new file mode 100644
index 0000000..d89c29d
--- /dev/null
+++ b/share/web/templates/_elements/header
@@ -0,0 +1,18 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+  <meta name="robots" content="all" />
+  
+  <title><% _( $title ) %> - <% _( $wikiname ) %></title>
+  
+  <% Jifty->web->include_css %>
+  <% Jifty->web->include_javascript %> 
+</head>
+<%args>
+$title => ""
+$wikiname => ""
+</%args>
+<%init>
+$r->content_type('text/html; charset=utf-8');
+</%init>
diff --git a/share/web/templates/_elements/markup b/share/web/templates/_elements/markup
index 7bfa695..cd83f25 100644
--- a/share/web/templates/_elements/markup
+++ b/share/web/templates/_elements/markup
@@ -1,5 +1,5 @@
 <div id="syntax">
-<div><a href="javascript:;" onclick="Element.toggle('syntax_content');return(false);"><b>Wiki Syntax Help</b></a>
+<div><a href="#" onclick="Element.toggle('syntax_content');return(false);"><b>Wiki Syntax Help</b></a>
 </div>
 <div id="syntax_content">
 
diff --git a/share/web/templates/_elements/nav b/share/web/templates/_elements/nav
index e89df55..69b953c 100644
--- a/share/web/templates/_elements/nav
+++ b/share/web/templates/_elements/nav
@@ -1,11 +1,12 @@
 <%init>
 my $top = Jifty->web->navigation;
-$top->child( Home => url => "/", sort_order => 1 );
-$top->child( Recent  => url => "/recent", label      => "Recent Changes", sort_order => 2);
- if (Jifty->config->framework('AdminMode') ) {
-     $top->child(Administration       => url => "/__jifty/admin/", sort_order => 998);
-     $top->child(OnlineDocs       => url => "/__jifty/online_docs/", label => 'Online docs',  sort_order => 999);
+$top->child( Home   => url => "/", sort_order => 1 );
+$top->child( Recent => url => "/recent", label => "Recent Changes", sort_order => 2 );
+
+if ( Jifty->config->framework('AdminMode') ) {
+    $top->child( Administration => url => "/__jifty/admin/", sort_order => 998);
+    $top->child( OnlineDocs     => url => "/__jifty/online_docs/", label => 'Online docs', sort_order => 999);
 }
 
-return();
+return;
 </%init>
diff --git a/share/web/templates/_elements/page_nav b/share/web/templates/_elements/page_nav
index 4b27a7f..a6dd135 100644
--- a/share/web/templates/_elements/page_nav
+++ b/share/web/templates/_elements/page_nav
@@ -1,20 +1,14 @@
 <%init>
 my $subpath =  $page . ($rev ? "/$rev" : '');
-my $top = Jifty->web->navigation;
-my $this = $top->child( 
-    This => 
-        url => "/view/".$subpath,
-        label => $page,
-        sort_order => 5
-);
+my $top = Jifty->web->page_navigation;
 
 my $page_obj = Wifty::Model::Page->new();
 $page_obj->load_by_cols(name => $page);
 
-$this->child( View => url => '/view/'.$subpath);
-$this->child( Edit => url => '/edit/'.$subpath) if $page_obj->current_user_can('update');
-$this->child( History => url => '/history/'.$page);
-$this->child( Latest => url => '/view/'.$page) if ($rev);
+$top->child( View => url => '/view/'.$subpath);
+$top->child( Edit => url => '/edit/'.$subpath);
+$top->child( History => url => '/history/'.$page);
+$top->child( Latest => url => '/view/'.$page) if ($rev);
 
 </%init>
 <%args>
diff --git a/share/web/templates/_elements/salutation b/share/web/templates/_elements/salutation
new file mode 100644
index 0000000..bfd36c6
--- /dev/null
+++ b/share/web/templates/_elements/salutation
@@ -0,0 +1,9 @@
+<div id="salutation">
+% if ( Jifty->web->current_user->id and Jifty->web->current_user->user_object ) {
+    Hiya, <span class="user"><% Jifty->web->current_user->user_object->name %></span>.
+    (<% Jifty->web->link( label => q{Logout}, url => '/logout' )%>)
+% }  else {
+    You're not currently signed in.
+    <% Jifty->web->tangent( label => q{Sign in}, url => '/login' ) %>.
+% }
+</div>
diff --git a/share/web/templates/_elements/sidebar b/share/web/templates/_elements/sidebar
deleted file mode 100644
index e849b39..0000000
--- a/share/web/templates/_elements/sidebar
+++ /dev/null
@@ -1,11 +0,0 @@
-<div id="salutation">
-% if (Jifty->web->current_user->id and Jifty->web->current_user->user_object) {
-Hiya, <span class="user"><%Jifty->web->current_user->user_object->name%></span>.<br />
-(<% Jifty->web->link( label => q{Logout}, url => '/logout' )%>)
-% }  else {
-<% Jifty->web->tangent( label => q{You're not currently signed in.}, url => '/login' )%>
-% }
-</div>
-<div id="navigation">
-<& /_elements/menu &>
-</div>
diff --git a/share/web/templates/_elements/wrapper b/share/web/templates/_elements/wrapper
new file mode 100644
index 0000000..2ba5a29
--- /dev/null
+++ b/share/web/templates/_elements/wrapper
@@ -0,0 +1,49 @@
+<& /_elements/header, title => $title, wikiname => $wikiname &>
+% Jifty->handler->stash->{'in_body'} = 1;
+<body<% $id && qq[ id="$id"]|n%>>
+% if (Jifty->config->framework('AdminMode') ) {
+  <div class="warning admin_mode">
+  <%_('Alert')%>: <% Jifty->web->tangent( label => _('Administration mode is enabled'),
+                                          url => '/__jifty/admin/') %>.
+  </div>
+% }
+
+  <div id="header">
+    <div id="wikiheader">
+      <h1 id="wikiname">
+        <% Jifty->web->link( url => "/", label => _($wikiname) ) %>
+      </h1>
+
+      <% Jifty->web->navigation->render_as_menu %>
+    </div>
+
+    <div id="pageheader">
+      <h1 id="pagename"><% _($title) %></h1>
+
+      <% Jifty->web->page_navigation->render_as_menu %>
+    </div>
+  </div>
+  
+  <& /_elements/salutation &>
+  
+  <hr class="clear" />
+  
+  <div id="content">
+    <% Jifty->web->render_messages %>
+    <% $m->content |n%>
+    <& /_elements/keybindings &>
+    <hr class="clear" />
+  </div>
+  
+  <div id="jifty-wait-message" style="display: none"><%_('Loading...')%></div>
+% Jifty::Mason::Halo->render_component_tree() if Jifty->config->framework('DevelMode');
+</body>
+</html>
+% Jifty->handler->stash->{'in_body'} = 0;
+<%args>
+$title => ""
+$id => ''
+</%args>
+<%init>
+my $wikiname = Jifty->config->app('WikiName') || "Wifty";
+</%init>
diff --git a/share/web/templates/create b/share/web/templates/create
index 91e61d8..002545b 100644
--- a/share/web/templates/create
+++ b/share/web/templates/create
@@ -1,9 +1,15 @@
-<&|/_elements/wrapper, title => 'New page: '. $page&>
+<&|/_elements/wrapper, title => 'New page: '. $page, id => 'create'&>
 <% Jifty->web->form->start %>
+<div class="form_wrapper">
 <% Jifty->web->form->next_page( url => '/view/'.$page) %>
 <% $action->form_field('name', render_as => 'hidden', default_value => $page) %>
-<% $action->form_field('content')%>
-<% Jifty->web->form->submit( label => 'Save')%>
+<div class="inline">
+<% $action->form_field('content', rows => 30)%>
+</div>
+<div class="line">
+<% Jifty->web->form->submit( label => 'Create' )%>
+</div>
+</div>
 <% Jifty->web->form->end %>
 <& /_elements/markup &>
 </&>
diff --git a/share/web/templates/edit b/share/web/templates/edit
index d8ca040..9e8084c 100644
--- a/share/web/templates/edit
+++ b/share/web/templates/edit
@@ -7,19 +7,25 @@ $viewer
 my $can_edit = $page->current_user_can('update');
 </%init>
 <&/_elements/page_nav, page => $page->name, rev => $revision->id &>
-<&|/_elements/wrapper, title => 'Edit: '.$page->name . ($revision->id ? " as of ".$revision->created : '')  &>
+<&|/_elements/wrapper, title => 'Edit: '.$page->name . ($revision->id ? " as of ".$revision->created : ''), id => "update"  &>
 <% Jifty->web->form->start %>
+<div class="form_wrapper">
+<div class="inline">
 % unless($can_edit) {
-  <p> You don't have permission to edit this page. Perhaps
+  <p style="width: 70%"> You don't have permission to edit this page. Perhaps
   <% Jifty->web->tangent(url => '/login', label => 'logging in') %>
   would help. In the mean time, though, you're welcome to view and
   copy the source of this page. </p>
 % }
 <% Jifty->web->form->next_page( url => '/view/'.$page->name) %>
-<% $viewer->form_field('content', ($revision->id ? (default_value => $revision->content) : (undef, undef)), rows=> 30, cols => 80 )%>
+<% $viewer->form_field('content', ($revision->id ? (default_value => $revision->content) : (undef, undef)), rows => 30 )%>
+</div>
 % if($can_edit) {
+<div class="line">
 <% Jifty->web->form->submit( label => 'Save') %>
+</div>
 % }
+</div>
 <% Jifty->web->form->end %>
 <& /_elements/markup &>
 </&>
diff --git a/share/web/templates/history b/share/web/templates/history
index f44c6b4..d5741c1 100644
--- a/share/web/templates/history
+++ b/share/web/templates/history
@@ -4,7 +4,7 @@ $revisions
 </%args>
 <& /_elements/page_nav, page => $page->name &>
 <&|/_elements/wrapper, title => $revisions->count ." revisions of " .$page->name &>
-<ul>
+<dl id="history">
 % while (my $rev = $revisions->next) {
 <dt><% Jifty->web->link( label => $rev->created, 
                           url => '/view/'.$page->name.'/'.$rev->id
@@ -17,5 +17,5 @@ $revisions
 </dt>
 <dd><%length($rev->content)%> bytes</dd>
 % }
-</ul>
+</dl>
 </&>
diff --git a/share/web/templates/login b/share/web/templates/login
index 52d75b5..5884b65 100644
--- a/share/web/templates/login
+++ b/share/web/templates/login
@@ -4,17 +4,23 @@ $next => undef
 </%args>
 <&|/_elements/wrapper, title => 'Login' &>
 
-% if (not Jifty->web->current_user->id) {
-<h2>Login</h2>
+% if ( not Jifty->web->current_user->id ) {
+<div id="login-box">
 <% Jifty->web->form->start(call => $next, name => "loginbox") %>
 <% $action->form_field('email') %>
 <% $action->form_field('password') %>
 <% $action->form_field('remember') %>
 <% Jifty->web->form->submit(label => 'Login', submit => $action) %>
 <% Jifty->web->form->end %>
-<% Jifty->web->tangent( label => q{Don't have an account?}, url => '/signup' )%>
+</div>
+
+<p><% Jifty->web->tangent( label => q{Don't have an account?}, url => '/signup' )%></p>
+
 % }
 % else {
-You're already logged in.
+<p>
+You're already logged in as <% Jifty->web->current_user->user_object->name %>.
+If this isn't you, <% Jifty->web->tangent( url => '/logout', label => 'click here') %>.
+</p>
 % }
 </&>
diff --git a/share/web/templates/recent b/share/web/templates/recent
index 08bc6f0..0464768 100644
--- a/share/web/templates/recent
+++ b/share/web/templates/recent
@@ -2,7 +2,7 @@
 $pages
 </%args>
 <&|/_elements/wrapper, title => 'Updated this week' &>
-<dl id="recentudates">
+<dl id="recentupdates">
 % while (my $page = $pages->next) {
 <dt><% Jifty->web->link( label => $page->name, url => '/view/'.$page->name)%></dt>
 <dd><%$page->updated%>
diff --git a/share/web/templates/signup b/share/web/templates/signup
index 1d006db..70103a1 100644
--- a/share/web/templates/signup
+++ b/share/web/templates/signup
@@ -3,7 +3,7 @@ $action
 $next
 </%args>
 <&|/_elements/wrapper, title => 'Signup' &>
-<h2>Signup</h2>
+<p>Just a few bits of information are all that's needed.</p>
 <% Jifty->web->form->start(call => $next, name => "signupbox") %>
 <% $action->form_field('email') %>
 <% $action->form_field('name') %>
diff --git a/share/web/templates/view b/share/web/templates/view
index 35c7668..0a4e256 100644
--- a/share/web/templates/view
+++ b/share/web/templates/view
@@ -5,7 +5,7 @@ $revision
 <& /_elements/page_nav, page => $page->name, rev => $revision->id &>
 <&|/_elements/wrapper, title => $page->name . ($revision->id ? " as of ".$revision->created : '') &>
 % if ($revision->id) {
-<& _elements/diff, page => $page, to => $revision &>
+<& /_elements/diff, page => $page, to => $revision &>
 
 <% $page->wiki_content($revision->content) |n%>
 % } else {
diff --git a/t/02-login.t b/t/02-login.t
index 87bf108..74c5d94 100644
--- a/t/02-login.t
+++ b/t/02-login.t
@@ -22,8 +22,8 @@ ok($URL, "Started a test server");
 my $mech = Jifty::Test::WWW::Mechanize->new();
 
 $mech->get_ok($URL, "Got the homepage");
-ok($mech->find_link(text_regex => qr/currently signed in/), 'Got the signin link');
-$mech->follow_link_ok(text_regex => qr/currently signed in/);
+ok($mech->find_link(text_regex => qr/Sign in/), 'Got the signin link');
+$mech->follow_link_ok(text_regex => qr/Sign in/);
 
 sub try_login {
     my $mech = shift;

commit 6cfc5b727093028b6e5782ae53d43e8226f06eeb
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Mon Aug 28 19:12:49 2006 +0000

    This makes Wifty admin mode slightly less hideous.

diff --git a/share/web/static/css/forms.css b/share/web/static/css/forms.css
index 981390f..338f291 100644
--- a/share/web/static/css/forms.css
+++ b/share/web/static/css/forms.css
@@ -105,3 +105,12 @@ form .line {
     clear: both;
 }
 
+/* So the admin ui is one row per line */
+
+.jifty_admin.item.inline {
+     clear: both;
+}
+
+.jifty_admin .editlink {
+    float: right;
+}

commit a03a86f5481c3462d3b85e6670415e4501e9ff46
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Wed Aug 30 06:22:28 2006 +0000

    Step one of aligning packages and filenames

diff --git a/lib/Wifty/Action/ResetLostPassword.pm b/lib/Wifty/Action/ResetLostPassword.pm
index a98292f..927d7c8 100755
--- a/lib/Wifty/Action/ResetLostPassword.pm
+++ b/lib/Wifty/Action/ResetLostPassword.pm
@@ -3,7 +3,7 @@ use strict;
 
 =head1 NAME
 
-Wifty::Action::ResetPassword - Confirm and reset a lost password
+Wifty::Action::ResetLostPassword - Confirm and reset a lost password
 
 =head1 DESCRIPTION
 
@@ -13,7 +13,7 @@ address is really theirs, when claiming that they lost their password.
 
 =cut
 
-package Wifty::Action::ResetPassword;
+package Wifty::Action::ResetLostPassword;
 use Wifty::Model::User;
 use base qw/Wifty::Action/;
 
diff --git a/lib/Wifty/Action/SendAccountConfrimation.pm b/lib/Wifty/Action/SendAccountConfrimation.pm
index 1073862..265a8c2 100755
--- a/lib/Wifty/Action/SendAccountConfrimation.pm
+++ b/lib/Wifty/Action/SendAccountConfrimation.pm
@@ -3,11 +3,11 @@ use strict;
 
 =head1 NAME
 
-Wifty::Action::ResendConfirmation
+Wifty::Action::SendAccountConfirmation
 
 =cut
 
-package Wifty::Action::ResendConfirmation;
+package Wifty::Action::SendAccountConfirmation;
 
 use Wifty::Model::User;
 use base qw/Wifty::Action/;
diff --git a/lib/Wifty/Action/SendPasswordReminder.pm b/lib/Wifty/Action/SendPasswordReminder.pm
index 4c643d6..0ae5ab4 100755
--- a/lib/Wifty/Action/SendPasswordReminder.pm
+++ b/lib/Wifty/Action/SendPasswordReminder.pm
@@ -3,11 +3,11 @@ use strict;
 
 =head1 NAME
 
-Wifty::Action::SendLostPasswordConfirmation
+Wifty::Action::SendPasswordReminder
 
 =cut
 
-package Wifty::Action::SendLostPasswordConfirmation;
+package Wifty::Action::SendPasswordReminder;
 use base qw/Wifty::Action Jifty::Action/;
 
 __PACKAGE__->mk_accessors(qw(user_object));
@@ -16,7 +16,7 @@ use Wifty::Model::User;
 
 =head2 arguments
 
-The field for C<SendLostPasswordConfirmation> is:
+The field for C<SendPasswordReminder> is:
 
 =over 4
 

commit b003dd5a514cb9abddaa165472b8b53f82bc3c66
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Wed Aug 30 06:22:36 2006 +0000

    Step two of aligning packages and filenames

diff --git a/lib/Wifty/Action/SendAccountConfrimation.pm b/lib/Wifty/Action/SendAccountConfirmation.pm
similarity index 100%
rename from lib/Wifty/Action/SendAccountConfrimation.pm
rename to lib/Wifty/Action/SendAccountConfirmation.pm

commit 9b432651278e2e84f4037a248be195fb787e38dc
Author: John Peacock <jpeacock at cpan.org>
Date:   Wed Aug 30 23:51:23 2006 +0000

    Update Wifty to use the Login plugin.
    Delete unnecessary files.

diff --git a/etc/config.yml b/etc/config.yml
index b1a8d98..4fad165 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -1,6 +1,7 @@
 framework:
   AdminMode: 0
   ApplicationName: Wifty
+  AdminEmail: 'wifty at example.com'
 
   Database:
     Driver: SQLite
@@ -9,7 +10,10 @@ framework:
     Version: 0.0.20
     Password: ''
     RequireSSL: 0
-#  Mailer: IO
+  Plugins:
+    - Login: {}
+  Mailer: SMTP
+  MailerArgs: ['69.17.117.59']
 #  MailerArgs:
 #    - %log/mail.log%
   SiteConfig: etc/site_config.yml
diff --git a/lib/Wifty/Action/ConfirmEmail.pm b/lib/Wifty/Action/ConfirmEmail.pm
deleted file mode 100644
index c3261a7..0000000
--- a/lib/Wifty/Action/ConfirmEmail.pm
+++ /dev/null
@@ -1,56 +0,0 @@
-use warnings;
-use strict;
-
-=head1 NAME
-
-Wifty::Action::ConfirmEmail - Confirm a user's email address
-
-=head1 DESCRIPTION
-
-This is the link in a user's email to confirm that their email
-email is really theirs.  It is not really meant to be rendered on any
-web page, but is used by the confirmation notification.
-
-=cut
-
-package Wifty::Action::ConfirmEmail;
-use base qw/Wifty::Action Jifty::Action/;
-
-use Wifty::Model::User;
-
-
-=head2 actions
-
-A null sub, because the superclass wants to make sure we fill in actions
-
-=cut
-
-sub actions {}
-
-=head2 take_action
-
-Set their confirmed status.
-
-=cut
-
-sub take_action {
-    my $self = shift;
-    my $u = Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser);
-    $u->load_by_cols( email => Jifty->web->current_user->user_object->email );
-
-    if ($u->email_confirmed) {
-        $self->result->error(email => "You have already confirmed your account.");
-        $self->result->success(1);  # but the action is still a success
-    }
-
-    $u->set_email_confirmed('1');
-
-    # Set up our login message
-    $self->result->message( "Welcome to Wifty, " . $u->name . ". Your email address has now been confirmed." );
-
-    # Actually do the login thing.
-    Jifty->web->current_user(Wifty::CurrentUser->new(id => $u->id));
-    return 1;
-}
-
-1;
diff --git a/lib/Wifty/Action/Login.pm b/lib/Wifty/Action/Login.pm
deleted file mode 100644
index 09643bc..0000000
--- a/lib/Wifty/Action/Login.pm
+++ /dev/null
@@ -1,89 +0,0 @@
-use warnings;
-use strict;
-
-=head1 NAME
-
-Wifty::Action::Login
-
-=cut
-
-package Wifty::Action::Login;
-use base qw/Wifty::Action/;
-use Jifty::Param::Schema;
-use Jifty::Action schema {
-
-param email =>
-    label is 'Email address',
-    is mandatory,
-    ajax validates;
-
-param password =>
-    type is 'password',
-    label is 'Password',
-    is mandatory;
-
-param remember =>
-    type is 'checkbox',
-    label is 'Remember me?',
-    hints is 'If you want, your browser can remember your login for you',
-    default is 0;
-
-};
-
-=head2 validate_email ADDRESS
-
-Makes sure that the email submitted is a legal email address and that there's a user in the database with it.
-
-
-=cut
-
-sub validate_email {
-    my $self  = shift;
-    my $email = shift;
-
-    unless ( $email =~ /\S\@\S/ ) {
-        return $self->validation_error(email => "That doesn't look like an email address." );
-    }
-
-    my $u = Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser);
-    $u->load_by_cols( email => $email );
-    return $self->validation_error(email => 'No account has that email address.') unless ($u->id);
-
-
-    return $self->validation_ok('email');
-}
-
-=head2 take_action
-
-Actually check the user's password. If it's right, log them in.
-Otherwise, throw an error.
-
-
-=cut
-
-sub take_action {
-    my $self = shift;
-    my $user = Wifty::CurrentUser->new( email => $self->argument_value('email'));
-
-    unless ( $user->id  && $user->password_is($self->argument_value('password'))) {
-        $self->result->error( 'You may have mistyped your email address or password. Give it another shot?' );
-        return;
-    }
-
-    unless ($user->user_object->email_confirmed) {
-        $self->result->error( q{You haven't confirmed your account yet.} );
-        return;
-    }
-
-    # Set up our login message
-    $self->result->message("Welcome back, " . $user->user_object->name . "." );
-
-    # Actually do the signin thing.
-    Jifty->web->current_user($user);
-    Jifty->web->session->expires($self->argument_value('remember') ? '+1y' : undef);
-    Jifty->web->session->set_cookie;
-
-    return 1;
-}
-
-1;
diff --git a/lib/Wifty/Action/Logout.pm b/lib/Wifty/Action/Logout.pm
deleted file mode 100644
index 3a35611..0000000
--- a/lib/Wifty/Action/Logout.pm
+++ /dev/null
@@ -1,25 +0,0 @@
-use warnings;
-use strict;
-
-=head1 NAME
-
-Wifty::Action::Logout
-
-=cut
-
-package Wifty::Action::Logout;
-use base qw/Wifty::Action Jifty::Action/;
-
-=head2 take_action
-
-Nuke the current user object
-
-=cut
-
-sub take_action {
-    my $self = shift;
-    Jifty->web->current_user(undef);
-    return 1;
-}
-
-1;
diff --git a/lib/Wifty/Action/RecoverPassword.pm b/lib/Wifty/Action/RecoverPassword.pm
deleted file mode 100644
index 7f6f0c3..0000000
--- a/lib/Wifty/Action/RecoverPassword.pm
+++ /dev/null
@@ -1,7 +0,0 @@
-
-use warnings;
-use strict;
-
-package Wifty::Action::RecoverPassword;
-
-1;
diff --git a/lib/Wifty/Action/ResetLostPassword.pm b/lib/Wifty/Action/ResetLostPassword.pm
deleted file mode 100755
index 927d7c8..0000000
--- a/lib/Wifty/Action/ResetLostPassword.pm
+++ /dev/null
@@ -1,69 +0,0 @@
-use warnings;
-use strict;
-
-=head1 NAME
-
-Wifty::Action::ResetLostPassword - Confirm and reset a lost password
-
-=head1 DESCRIPTION
-
-This is the action run by the link in a user's email to confirm that their email
-address is really theirs, when claiming that they lost their password.  
-
-
-=cut
-
-package Wifty::Action::ResetLostPassword;
-use Wifty::Model::User;
-use base qw/Wifty::Action/;
-
-use Jifty::Param::Schema;
-use Jifty::Action schema {
-
-param password =>
-    type is 'password',
-    ! is sticky;
-
-param password_confirm =>
-    type is 'password',
-    label is 'type your password again',
-    ! is sticky;
-
-};
-
-=head2 take_action
-
-Resets the password.
-
-=cut
-
-sub take_action {
-    my $self = shift;
-    my $u = Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser);
-    $u->load_by_cols( email => Jifty->web->current_user->user_object->email );
-
-    unless ($u) {
-        $self->result->error( "You don't exist. I'm not sure how this happened. Really, really sorry. Please email us!");
-    } 
-
-    my $pass = $self->argument_value('password');
-    my $pass_c = $self->argument_value('password_confirm');
-
-    # Trying to set a password (ie, submitted the form)
-    unless (defined $pass and defined $pass_c and length $pass and $pass eq $pass_c) {
-        $self->result->error("It looks like you didn't enter the same password into both boxes. Give it another shot?");
-        return;
-    } 
-
-    unless ($u->set_password($pass)) {
-        $self->result->error("There was an error setting your password.");
-        return;
-    } 
-    # Log in!
-    $self->result->message( "Your password has been reset.  Welcome back." );
-    Jifty->web->current_user(Wifty::CurrentUser->new(id => $u->id));
-    return 1;
-
-}
-
-1;
diff --git a/lib/Wifty/Action/SendAccountConfirmation.pm b/lib/Wifty/Action/SendAccountConfirmation.pm
deleted file mode 100755
index 265a8c2..0000000
--- a/lib/Wifty/Action/SendAccountConfirmation.pm
+++ /dev/null
@@ -1,72 +0,0 @@
-use warnings;
-use strict;
-
-=head1 NAME
-
-Wifty::Action::SendAccountConfirmation
-
-=cut
-
-package Wifty::Action::SendAccountConfirmation;
-
-use Wifty::Model::User;
-use base qw/Wifty::Action/;
-
-__PACKAGE__->mk_accessors(qw(user_object));
-
-use Jifty::Param::Schema;
-use Jifty::Action schema {
-
-param address =>
-    label is 'email address',
-    is mandatory,
-    default is '';
-
-};
-
-=head2 setup
-
-Create an empty user object to work with
-
-=cut
-
-sub setup {
-    my $self = shift;
-    
-    $self->user_object(Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser));
-}
-
-=head2 validate_address
-
-Make sure their email address is an unconfirmed user.
-
-=cut
-
-sub validate_address {
-    my $self  = shift;
-    my $email = shift;
-
-    return $self->validation_error(address => "That doesn't look like an email address." ) unless ( $email =~ /\S\@\S/ );
-
-    $self->user_object(Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser));
-    $self->user_object->load_by_cols( email => $email );
-    return $self->validation_error(address => "It doesn't look like there's an account by that name.") unless ($self->user_object->id);
-
-    return $self->validation_error(address => "It looks like you're already confirmed.") if ($self->user_object->email_confirmed);
-
-    return $self->validation_ok('address');
-}
-
-=head2 take_action
-
-Create a new unconfirmed user and send out a confirmation email.
-
-=cut
-
-sub take_action {
-    my $self = shift;
-    Wifty::Notification::ConfirmAddress->new( to => $self->user_object )->send;
-    return $self->result->message("Confirmation resent.");
-}
-
-1;
diff --git a/lib/Wifty/Action/SendPasswordReminder.pm b/lib/Wifty/Action/SendPasswordReminder.pm
deleted file mode 100755
index 0ae5ab4..0000000
--- a/lib/Wifty/Action/SendPasswordReminder.pm
+++ /dev/null
@@ -1,87 +0,0 @@
-use warnings;
-use strict;
-
-=head1 NAME
-
-Wifty::Action::SendPasswordReminder
-
-=cut
-
-package Wifty::Action::SendPasswordReminder;
-use base qw/Wifty::Action Jifty::Action/;
-
-__PACKAGE__->mk_accessors(qw(user_object));
-
-use Wifty::Model::User;
-
-=head2 arguments
-
-The field for C<SendPasswordReminder> is:
-
-=over 4
-
-=item address: the email address
-
-=back
-
-=cut
-
-sub arguments {
-    return (
-        {
-            address => {
-                label     => 'email address',
-                mandatory => 1,
-            },
-        }
-    );
-
-}
-
-=head2 setup
-
-Create an empty user object to work with
-
-=cut
-
-sub setup {
-    my $self = shift;
-    
-    # Make a blank user object
-    $self->user_object(Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser));
-}
-
-=head2 validate_address
-
-Make sure there's actually an account by that name.
-
-=cut
-
-sub validate_address {
-    my $self  = shift;
-    my $email = shift;
-
-    return $self->validation_error(address => "That doesn't look like an email address." )
-      unless ( $email =~ /\S\@\S/ );
-
-    $self->user_object(Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser));
-    $self->user_object->load_by_cols( email => $email );
-        return $self->validation_error(address => "It doesn't look like there's an account by that name.")
-    unless ($self->user_object->id);
-
-    return $self->validation_ok('address');
-}
-
-=head2 take_action
-
-Send out a confirmation email giving a link to a password-reset form.
-
-=cut
-
-sub take_action {
-    my $self = shift;
-    Wifty::Notification::ConfirmLostPassword->new( to => $self->user_object )->send;
-    return $self->result->message("A link to reset your password has been sent to your email account.");
-}
-
-1;
diff --git a/lib/Wifty/Action/Signup.pm b/lib/Wifty/Action/Signup.pm
deleted file mode 100644
index 6e73fc2..0000000
--- a/lib/Wifty/Action/Signup.pm
+++ /dev/null
@@ -1,107 +0,0 @@
-use warnings;
-use strict;
-
-=head1 NAME
-
-Wifty::Action::Signup
-
-=cut
-
-package Wifty::Action::Signup;
-use Wifty::Action::CreateUser;
-use base qw/Wifty::Action::CreateUser/;
-
-
-use Wifty::Model::User;
-
-=head2 arguments
-
-
-The fields for C<Signup> are:
-
-=over 4
-
-=item email: the email address
-
-=item password and password_confirm: the requested password
-
-=item name: your full name
-
-=back
-
-=cut
-
-sub arguments {
-    my $self = shift;
-    my $args = $self->SUPER::arguments;
-
-    my %fields = ( 
-        name                        => 1,
-        email                        => 1,
-        password                     => 1,
-        password_confirm             => 1,
-    );
-
-    for ( keys %$args ) { delete $args->{$_} unless ( $fields{$_} ); }
-    $args->{'email'}{'ajax_validates'} = 1;
-    $args->{'password_confirm'}{'label'} = "Type that again?";
-    return $args;
-}
-
-
-=head2 validate_email
-
-Make sure their email address looks sane
-
-=cut
-
-sub validate_email {
-    my $self  = shift;
-    my $email = shift;
-
-        return $self->validation_error(email => "That doesn't look like an email address." )
-    unless ( $email =~ /\S\@\S/ ) ;
-
-    my $u = Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser);
-    $u->load_by_cols( email => $email );
-    if ($u->id) {
-      return $self->validation_error(email => 'It looks like you already have an account. Perhaps you want to <a href="/login">sign in</a> instead?');
-    }
-
-    return $self->validation_ok('email');
-}
-
-
-
-=head2 take_action
-
-Overrides the virtual C<take_action> method on L<Jifty::Action> to call
-the appropriate C<Jifty::Record>'s C<create> method when the action is
-run, thus creating a new object in the database.
-
-Makes sure that the user only specifies things we want them to.
-
-=cut
-
-sub take_action {
-    my $self   = shift;
-    my $record = Wifty::Model::User->new(current_user => Wifty::CurrentUser->superuser);
-
-    my %values;
-    $values{$_} = $self->argument_value($_)
-      for grep { defined $self->record->column($_) and defined $self->argument_value($_) } $self->argument_names;
-    
-    my ($id) = $record->create(%values);
-    # Handle errors?
-    unless ( $record->id ) {
-        $self->result->error("Something bad happened and we couldn't create your account.  Try again later. We're really, really sorry.");
-        return;
-    }
-
-    $self->result->message( "Welcome to Wifty, " . $record->name .". We've sent a confirmation message to your email box.");
-
-
-    return 1;
-}
-
-1;
diff --git a/lib/Wifty/CurrentUser.pm b/lib/Wifty/CurrentUser.pm
deleted file mode 100755
index 78e50fa..0000000
--- a/lib/Wifty/CurrentUser.pm
+++ /dev/null
@@ -1,38 +0,0 @@
-use warnings;
-use strict;
-
-
-package Wifty::CurrentUser;
-
-use base qw(Jifty::CurrentUser);
-
-=head2 new PARAMHASH
-
-Instantiate a new current user object, loading the user by paramhash:
-
-   my $item = Wifty::Model::Item->new( Wifty::CurrentUser->new(email => 'user at site'));
-
-if you give the param 
-    _bootstrap => 1
-
-your object will be marked as a bootstrap user. You can use that to do an endrun around acls.
-
-=cut
-
-
-
-sub _init {
-    my $self = shift;
-    my %args = (@_);
-
-    if (delete $args{'_bootstrap'} ) {
-        $self->is_bootstrap_user(1);
-    } elsif (keys %args) {
-        $self->user_object(Wifty::Model::User->new(current_user => $self));
-        $self->user_object->load_by_cols(%args);
-    }
-    $self->SUPER::_init(%args);
-}
-
-
-1;
diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index e9bbfcd..00f57d8 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -78,55 +78,5 @@ on 'recent', run {
     set pages => $pages;
 };
 
-# Sign up for an account
-on 'signup', run {
-    redirect('/') if ( Jifty->web->current_user->id );
-    set 'action' =>
-        Jifty->web->new_action( class => 'Signup', moniker => 'signupbox' );
-
-    set 'next' => Jifty->web->request->continuation
-        || Jifty::Continuation->new(
-        request => Jifty::Request->new( path => "/" ) );
-
-};
-
-# Login
-on 'login', run {
-    set 'action' =>
-        Jifty->web->new_action( class => 'Login', moniker => 'loginbox' );
-    set 'next' => Jifty->web->request->continuation
-        || Jifty::Continuation->new(
-        request => Jifty::Request->new( path => "/" ) );
-};
-
-# Log out
-before 'logout', run {
-    Jifty->web->request->add_action(
-        moniker => 'logout',
-        class   => 'Wifty::Action::Logout'
-    );
-};
-
-
-## LetMes
-before qr'^/let/(.*)' => run {
-    Jifty->api->deny(qr/^Wifty::Dispatcher/);
-
-    my $let_me = Jifty::LetMe->new();
-    $let_me->from_token($1);
-    redirect '/error/let_me/invalid_token' unless $let_me->validate;
-
-    Jifty->web->temporary_current_user($let_me->validated_current_user);
-
-    my %args = %{$let_me->args};
-    set $_ => $args{$_} for keys %args;
-    set let_me => $let_me;
-};
-
-on qr'^/let/', => run {
-    my $let_me = get 'let_me';
-    show '/let/' . $let_me->path;
-};
-
 
 1;
diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 54f3d7a..ad43928 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -128,7 +128,9 @@ sub _set {
 
     $self->SUPER::_set(
         column => 'updated_by',
-        value  => ( $self->current_user? $self->current_user->user_object->id : undef )
+        value  => (   $self->current_user->user_object 
+		    ? $self->current_user->user_object->id 
+		    : undef )
     );
 
     return ( $val, $msg );
diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index 9a61bf1..a97d6dd 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -1,131 +1,4 @@
-package Wifty::Model::User::Schema;
-use Jifty::DBI::Schema;
-
-column name => 
-    type is 'text',
-    label is 'Name',
-    is mandatory,
-    is distinct;
-
-column email =>
-    type is 'text',
-    label is 'Email address',
-    is mandatory,
-    is distinct;
-
-column password =>,
-    type is 'text',
-    label is 'Password',
-    render_as 'password';
-
-column email_confirmed =>
-    label is 'Email address confirmed?',
-    type is 'boolean',
-    since '0.0.19';
-
-column auth_token => 
-    type is 'text',
-    render_as 'Password',
-    since '0.0.15';
-
-
-
 package Wifty::Model::User;
-use base qw/Wifty::Record/;
-use Wifty::Notification::ConfirmAddress;
-
-sub since {'0.0.7'}
-
-sub create {
-    my $self = shift;
-    my %args = (@_);
-    my (@ret) = $self->SUPER::create(%args);
-
-    if ($self->id and not $self->email_confirmed) {
-        Wifty::Notification::ConfirmAddress->new( to => $self )->send;
-    }
-    return (@ret);
-}
-
-
-=head2 password_is STRING
-
-Returns true if and only if the current user's password matches STRING
-
-=cut
-
-
-sub password_is {
-    my $self = shift;
-    my $string = shift;
-    return 1 if ($self->_value('password') eq $string);
-    return 0;
-}
-
-=head2 password
-
-Never display a password
-
-=cut
-
-sub password {
-    return undef;
-
-}
-
-=head2 current_user_can
-
-Allows the current user to see all their own attributes and
-everyone else to see their username.
-
-Allows the current user to update any of their own attributes
-except whether or not their email has been confirmed.
-
-Passes everything else off to the superclass.
-
-=cut
-
-
-sub current_user_can {
-    my $self  = shift;
-    my $right = shift;
-    my %args  = (@_);
-    if (    $right eq 'read'
-        and $self->id == $self->current_user->id )
-    {
-        return 1;
-    } elsif ( $right eq 'read' and $args{'column'} and $args{'column'} eq 'name' ) {
-        return (1);
-
-    } elsif ( $right eq 'update'
-        and $self->id == $self->current_user->id
-        and $args{'column'} ne 'email_confirmed' )
-    {
-        return (1);
-    }
-
-    return $self->SUPER::current_user_can( $right, %args );
-}
-
-=head2 auth_token
-
-Returns the user's unique authentication token. If the user 
-doesn't have one, sets one and returns it.
-
-=cut
-
-
-sub auth_token {
-    my $self = shift;
-    return undef unless ($self->current_user_can( read => column =>  'auth_token'));
-    my $value = $self->_value('auth_token') ;
-    unless ($value) {
-            my $digest =Digest::MD5->new();
-            $digest->add(rand(100));
-            $self->__set(column => 'auth_token', value => $digest->b64digest);
-    }
-    return $self->_value('auth_token') ;
-
-}
+use base qw/Jifty::Plugin::Login::Model::User/;
 
 1;
diff --git a/lib/Wifty/Notification/ConfirmAddress.pm b/lib/Wifty/Notification/ConfirmAddress.pm
deleted file mode 100755
index 3fb3c3a..0000000
--- a/lib/Wifty/Notification/ConfirmAddress.pm
+++ /dev/null
@@ -1,53 +0,0 @@
-use warnings;
-use strict;
-
-package Wifty::Notification::ConfirmAddress;
-use base qw/Wifty::Notification/;
-
-=head1 NAME
-
-Hiveminder::Notification::ConfirmAddress
-
-=head1 ARGUMENTS
-
-C<to>, a L<Wifty::Model::User> whose address we are confirming.
-
-=cut
-
-=head2 setup
-
-Sets up the fields of the message.
-
-=cut
-
-sub setup {
-    my $self = shift;
-
-    unless (UNIVERSAL::isa($self->to, "Wifty::Model::User")) {
-	$self->log->error((ref $self) . " called with invalid user argument");
-	return;
-    } 
-   
-
-    my $letme = Jifty::LetMe->new();
-    $letme->email($self->to->email);
-    $letme->path('confirm_email'); 
-    my $confirm_url = $letme->as_url;
-
-    $self->subject( "Welcome to Wifty!" ); 
-    
-
-    $self->body(<<"END_BODY");
-
-You're getting this message because you (or somebody claiming to be you)
-signed up for a Wiki running Wifty.
-
-Before you can use Wifty, we need to make sure that we got your email
-address right.  Click on the link below to get started:
-
-$confirm_url
-
-END_BODY
-}
-
-1;

commit 3f3bff22674bd70b995becaebc1cfb16e3924d42
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Sun Sep 3 02:59:55 2006 +0000

    Trying out a new idea for a Jifty idiom with Wifty.
    
    Make a Wifty::Form::Field::WikiPage J::W::Form::Field subclass, and
    have C<content> fields C<render_as> it. Then, rendering pages by
    calling C<form_value> for C<content> field of an appropriate
    C<UpdatePage> action. Note that this also makes admin mode look nicer.

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 00f57d8..aae97fe 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -39,7 +39,11 @@ on qr{^/(view|edit)/(.*)}, run {
     $revision->load_by_cols( page => $page->id, id => $rev ) if ($rev);
     set page => $page;
     set revision => $revision;
-    set viewer => Jifty->web->new_action( class => 'UpdatePage', record => $page );
+    my $viewer = Jifty->web->new_action( class => 'UpdatePage', record => $page );
+    if($rev) {
+        $viewer->argument_value(content => $revision->content);
+    }
+    set viewer => $viewer;
     show("/$page_name");
 };
 
diff --git a/lib/Wifty/Form/Field/WikiPage.pm b/lib/Wifty/Form/Field/WikiPage.pm
new file mode 100644
index 0000000..99fa3a2
--- /dev/null
+++ b/lib/Wifty/Form/Field/WikiPage.pm
@@ -0,0 +1,80 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+Wifty::Form::Field::WikiPage
+
+=head1 DESCRIPTION
+
+A L<Jifty::Web::Form::Field> subclass that renders itself as a text
+field on update, and wikifies itself on read-only display.
+
+=cut
+
+package Wifty::Form::Field::WikiPage;
+use base qw(Jifty::Web::Form::Field::Textarea);
+
+=head2 render_value
+
+Render a wikified view of this field's content.
+
+=cut
+
+sub render_value {
+    my $self = shift;
+    my $field;
+    my $field = '<span';
+    $field .= qq! class="@{[ $self->classes ]}"> !;
+    $field .= $self->wiki_content;
+    $field .= qq!</span>\n!;
+    Jifty->web->out($field);
+    return '';
+    
+}
+
+
+=head2 wiki_content
+
+Wikify this field's C<current_value>
+
+=cut
+
+
+sub wiki_content {
+    my $self     = shift;
+    my $content  = $self->current_value;
+    my $scrubber = HTML::Scrubber->new();
+
+    $scrubber->default(
+        0,
+        {   '*'   => 0,
+            id    => 1,
+            class => 1,
+            href  => qr{^(?:(?:\w+$)|http:|ftp:|https:|/)}i,
+
+            # Match http, ftp and relative urls
+            face   => 1,
+            size   => 1,
+            target => 1
+        }
+    );
+
+    $scrubber->deny(qw[*]);
+    $scrubber->allow(
+        qw[H1 H2 H3 H4 H5 A STRONG EM CODE PRE B U P BR I HR BR SPAN DIV UL OL LI DL DT DD]);
+    $scrubber->comment(0);
+
+    $content = Text::Markdown::markdown( $content );
+    $content = $scrubber->scrub( $content );
+    return ( $content );
+
+}
+
+=head1 SEE ALSO
+
+L<Text::Markdown>, L<Jifty::Web::Form::Field::Textarea>
+
+=cut
+
+1;
diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index ad43928..f39c5d4 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -11,7 +11,7 @@ column name =>
 column content =>
     type is 'text',
     label is 'Content',
-    render_as 'textarea';
+    render_as 'Wifty::Form::Field::WikiPage';
 
 column updated =>
     type is 'timestamp',
@@ -32,43 +32,6 @@ use Text::Markdown;
 use HTML::Scrubber;
 
 
-=head2 wiki_content [CONTENT]
-
-Wikify either the content of a scalar passed in as an argument or
-this page's "content" attribute.
-
-=cut
-
-sub wiki_content {
-    my $self     = shift;
-    my $content  = shift || $self->content() || '';
-    my $scrubber = HTML::Scrubber->new();
-
-    $scrubber->default(
-        0,
-        {   '*'   => 0,
-            id    => 1,
-            class => 1,
-            href  => qr{^(?:(?:\w+$)|http:|ftp:|https:|/)}i,
-
-            # Match http, ftp and relative urls
-            face   => 1,
-            size   => 1,
-            target => 1
-        }
-    );
-
-    $scrubber->deny(qw[*]);
-    $scrubber->allow(
-        qw[H1 H2 H3 H4 H5 A STRONG EM CODE PRE B U P BR I HR BR SPAN DIV UL OL LI DL DT DD]);
-    $scrubber->comment(0);
-
-    $content = Text::Markdown::markdown( $content );
-    $content = $scrubber->scrub( $content );
-    return ( $content );
-
-}
-
 sub create {
     my $self = shift;
     my %args = (@_);
diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index 7fe23b3..f75545e 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -3,7 +3,7 @@ use Jifty::DBI::Schema;
 
 column page  => refers_to Wifty::Model::Page;
 
-column content => type is 'text', render_as 'textarea';
+column content => type is 'text', render_as 'Wifty::Form::Field::WikiPage';
 
 column created => type is 'timestamp';
 
diff --git a/share/web/templates/edit b/share/web/templates/edit
index 9e8084c..c036545 100644
--- a/share/web/templates/edit
+++ b/share/web/templates/edit
@@ -18,7 +18,7 @@ my $can_edit = $page->current_user_can('update');
   copy the source of this page. </p>
 % }
 <% Jifty->web->form->next_page( url => '/view/'.$page->name) %>
-<% $viewer->form_field('content', ($revision->id ? (default_value => $revision->content) : (undef, undef)), rows => 30 )%>
+<% $viewer->form_field('content', rows => 30 )%>
 </div>
 % if($can_edit) {
 <div class="line">

commit 673ffc3e3e50c94dc35ebbc0775f77f0b8537dd7
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Sun Sep 3 03:00:44 2006 +0000

    Oops, forgot the view template in the last commit.

diff --git a/share/web/templates/view b/share/web/templates/view
index 0a4e256..75133b9 100644
--- a/share/web/templates/view
+++ b/share/web/templates/view
@@ -1,14 +1,15 @@
 <%args>
 $page
 $revision
+$viewer
 </%args>
 <& /_elements/page_nav, page => $page->name, rev => $revision->id &>
 <&|/_elements/wrapper, title => $page->name . ($revision->id ? " as of ".$revision->created : '') &>
+
 % if ($revision->id) {
 <& /_elements/diff, page => $page, to => $revision &>
-
-<% $page->wiki_content($revision->content) |n%>
-% } else {
-<% $page->wiki_content |n %>
 % }
+
+<% $viewer->form_value('content', label => "") %>
+
 </&>

commit d1645d29ab81f9507b39615509244e13c3f33c4f
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Sun Sep 3 03:01:09 2006 +0000

    Some Wifty admin mode styling.

diff --git a/share/web/static/css/forms.css b/share/web/static/css/forms.css
index 338f291..125bec9 100644
--- a/share/web/static/css/forms.css
+++ b/share/web/static/css/forms.css
@@ -113,4 +113,11 @@ form .line {
 
 .jifty_admin .editlink {
     float: right;
+    border-left: 1px solid black;
+    border-bottom: 1px solid black;
+    padding: 0 0 10px 10px;
+}
+
+.jifty_admin hr {
+    clear: both;
 }

commit e1671a4fb01e227d5ee3d7adc79e714403424aab
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Sun Sep 3 03:32:10 2006 +0000

    Moving the '30 rows' into WikiPage.pm, so it affects admin mode as well.

diff --git a/lib/Wifty/Form/Field/WikiPage.pm b/lib/Wifty/Form/Field/WikiPage.pm
index 99fa3a2..261813a 100644
--- a/lib/Wifty/Form/Field/WikiPage.pm
+++ b/lib/Wifty/Form/Field/WikiPage.pm
@@ -71,6 +71,14 @@ sub wiki_content {
 
 }
 
+=head2 rows
+
+C<WikiPage> forms have 30 rows in their textarea by default
+
+=cut
+
+sub rows { 30 };
+
 =head1 SEE ALSO
 
 L<Text::Markdown>, L<Jifty::Web::Form::Field::Textarea>
diff --git a/share/web/templates/edit b/share/web/templates/edit
index c036545..c7b0ab3 100644
--- a/share/web/templates/edit
+++ b/share/web/templates/edit
@@ -18,7 +18,7 @@ my $can_edit = $page->current_user_can('update');
   copy the source of this page. </p>
 % }
 <% Jifty->web->form->next_page( url => '/view/'.$page->name) %>
-<% $viewer->form_field('content', rows => 30 )%>
+<% $viewer->form_field('content')%>
 </div>
 % if($can_edit) {
 <div class="line">

commit 8af14b855fb20beb1685076b130c49becd90ea24
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Sun Sep 3 04:19:28 2006 +0000

    Some search CSS fixes for Wifty.

diff --git a/share/web/static/css/app.css b/share/web/static/css/app.css
index 2f5ed0f..e265b24 100644
--- a/share/web/static/css/app.css
+++ b/share/web/static/css/app.css
@@ -9,6 +9,14 @@ label.argument-content {
     display: none !important;
 }
 
+.jifty_admin label.argument-content {
+    display: inline !important;
+}
+
+.jifty_admin div.argument-content {
+    width: 100%;
+}
+
 textarea.argument-content {
     width: 100%;
     font-size: 1.2em;

commit 587dc0d0b3d6f675c80fc2381e9176c417d3cb45
Author: John Peacock <jpeacock at cpan.org>
Date:   Tue Oct 10 09:17:58 2006 +0000

    Remove duplicate "my $field;" line.

diff --git a/lib/Wifty/Form/Field/WikiPage.pm b/lib/Wifty/Form/Field/WikiPage.pm
index 261813a..6c7bec5 100644
--- a/lib/Wifty/Form/Field/WikiPage.pm
+++ b/lib/Wifty/Form/Field/WikiPage.pm
@@ -23,7 +23,6 @@ Render a wikified view of this field's content.
 
 sub render_value {
     my $self = shift;
-    my $field;
     my $field = '<span';
     $field .= qq! class="@{[ $self->classes ]}"> !;
     $field .= $self->wiki_content;

commit 53468fd7b768704754ed53a4ab6186946bc3aee3
Author: John Peacock <jpeacock at cpan.org>
Date:   Wed Oct 11 01:00:23 2006 +0000

    Can't see the salutation link when Admin mode is enabled (red text on
    slightly different red background).  Black at least shows up. ;-)

diff --git a/share/web/static/css/app-base.css b/share/web/static/css/app-base.css
index d6aeb11..72446d2 100644
--- a/share/web/static/css/app-base.css
+++ b/share/web/static/css/app-base.css
@@ -16,6 +16,10 @@ a {
     font-size: 0.9em;
 }
 
+#salutation a {
+    color: black;
+}
+
 #header {
     margin-top: 2.3em;
 }

commit dd5f2e511f7cf96b0210ecb5f73849519a6d683c
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Thu Oct 19 19:52:03 2006 +0000

    Backing out the SMTP mailer

diff --git a/etc/config.yml b/etc/config.yml
index 4fad165..df6b32e 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -12,10 +12,9 @@ framework:
     RequireSSL: 0
   Plugins:
     - Login: {}
-  Mailer: SMTP
-  MailerArgs: ['69.17.117.59']
-#  MailerArgs:
-#    - %log/mail.log%
+  Mailer: IO
+  MailerArgs:
+    - %log/mail.log%
   SiteConfig: etc/site_config.yml
 
   Web:

commit f4035a321b58e9ca653ca48a6cb5d68f81def410
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Thu Oct 19 20:58:13 2006 +0000

    Adding a simplistic search page to Wifty

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index aae97fe..e19d01f 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -69,6 +69,19 @@ on 'pages', run {
     set pages => $pages;
 };
 
+on 'search', run {
+    my $search = Jifty->web->response->result('search');
+    my $collection = undef;
+    if($search) {
+        $collection = $search->content('search');
+    }
+    my $action =  Jifty->web->new_action(class => 'SearchPage', moniker => 'search');
+    $action->sticky_on_success(1);
+
+    set search => $action;
+    set pages => $collection;
+};
+
 # Show recent edits
 on 'recent', run {
     my $then = DateTime->from_epoch( epoch => ( time - ( 86400 * 7 ) ) );
diff --git a/share/web/templates/_elements/nav b/share/web/templates/_elements/nav
index 69b953c..808bd8c 100644
--- a/share/web/templates/_elements/nav
+++ b/share/web/templates/_elements/nav
@@ -2,6 +2,7 @@
 my $top = Jifty->web->navigation;
 $top->child( Home   => url => "/", sort_order => 1 );
 $top->child( Recent => url => "/recent", label => "Recent Changes", sort_order => 2 );
+$top->child( Search => url => "/search", label => "Search", sort_order => 3 );
 
 if ( Jifty->config->framework('AdminMode') ) {
     $top->child( Administration => url => "/__jifty/admin/", sort_order => 998);

commit b2eca96edab7ffe51bf420d54ae980272328ef53
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Thu Oct 19 21:04:41 2006 +0000

    Actually adding the search page, and restyling it slightly

diff --git a/share/web/static/css/app.css b/share/web/static/css/app.css
index e265b24..290e344 100644
--- a/share/web/static/css/app.css
+++ b/share/web/static/css/app.css
@@ -34,13 +34,13 @@ form .submit_button input {
     border: 1px outset #3d4286;
 }
 
-#recentupdates dt {
+.pagelist dt {
     float: left;
     clear: left;
     width: 45%;
 }
 
-#recentupdates dd {
+.pagelist dd {
     margin-left: 2em;
     padding-left: 0;
     margin-bottom: 0.5em;
@@ -48,8 +48,8 @@ form .submit_button input {
     width: 50%;
 }
 
-* html #recentupdates dt,
-* html #recentupdates dd { position: relative; }
+* html .pagelist dt,
+* html .pagelist dd { position: relative; }
 
 #history dd {
     font-size: 0.95em;
diff --git a/share/web/templates/recent b/share/web/templates/_elements/page_list
similarity index 76%
copy from share/web/templates/recent
copy to share/web/templates/_elements/page_list
index 0464768..f4bfd15 100644
--- a/share/web/templates/recent
+++ b/share/web/templates/_elements/page_list
@@ -1,8 +1,8 @@
 <%args>
 $pages
+$id
 </%args>
-<&|/_elements/wrapper, title => 'Updated this week' &>
-<dl id="recentupdates">
+<dl id="<%$id%>" class="pagelist">
 % while (my $page = $pages->next) {
 <dt><% Jifty->web->link( label => $page->name, url => '/view/'.$page->name)%></dt>
 <dd><%$page->updated%>
@@ -14,4 +14,3 @@ $pages
 </dd>
 % }
 </dl>
-</&>
diff --git a/share/web/templates/pages b/share/web/templates/pages
index c39f5ae..4495c8e 100644
--- a/share/web/templates/pages
+++ b/share/web/templates/pages
@@ -2,15 +2,5 @@
 $pages
 </%args>
 <&|/_elements/wrapper, title => 'These are the pages on your wiki!' &>
-<ul id="pagelist">
-% while (my $page = $pages->next) {
-<li><% 
-        Jifty->web->link(
-            label => $page->name,
-            url   => '/view/' . $page->name
-            )
-
-    %></li>
-% } 
-</ul>
+<& /_elements/page_list, pages => $pages, id => 'allpages' &>
 </&>
diff --git a/share/web/templates/recent b/share/web/templates/recent
index 0464768..071ac48 100644
--- a/share/web/templates/recent
+++ b/share/web/templates/recent
@@ -2,16 +2,5 @@
 $pages
 </%args>
 <&|/_elements/wrapper, title => 'Updated this week' &>
-<dl id="recentupdates">
-% while (my $page = $pages->next) {
-<dt><% Jifty->web->link( label => $page->name, url => '/view/'.$page->name)%></dt>
-<dd><%$page->updated%>
-% if($page->updated_by->id) {
-  (<% $page->updated_by->name %>)
-% } else {
-  (Anonymous)
-% }
-</dd>
-% }
-</dl>
+<& /_elements/page_list, pages => $pages, id => 'recentupdates' &>
 </&>
diff --git a/share/web/templates/search b/share/web/templates/search
new file mode 100644
index 0000000..6d77876
--- /dev/null
+++ b/share/web/templates/search
@@ -0,0 +1,19 @@
+<%args>
+$pages
+$search
+</%args>
+<%init>
+warn $search;
+</%init>
+<&|/_elements/wrapper, title => 'Search' &>
+<% Jifty->web->form->start %>  
+  <div id="searchbox" class="inline">
+    <% $search->form_field('contains', label => 'Find pages containing:') %>
+    <% $search->button(label => 'Search') %>
+  </div>
+  
+<% Jifty->web->form->end %>  
+% if($pages) {  
+<& /_elements/page_list, pages => $pages, id => 'searchresults' &>
+% }
+</&>

commit af80295d518132ef9038f6d1b3a7ad8fca375a9e
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Thu Oct 19 21:41:45 2006 +0000

    Adding a search bar to the menu

diff --git a/share/web/static/css/app-base.css b/share/web/static/css/app-base.css
index 72446d2..71e20ef 100644
--- a/share/web/static/css/app-base.css
+++ b/share/web/static/css/app-base.css
@@ -28,7 +28,12 @@ a {
     float: right;
     text-align: right;
     padding-right: 1px;
-    width: 28%;
+    width: 50%;
+}
+
+#wikiheader label {
+    display: inline;
+    float: none;
 }
 
 #wikiname {
diff --git a/share/web/templates/_elements/wrapper b/share/web/templates/_elements/wrapper
index 2ba5a29..67b2e13 100644
--- a/share/web/templates/_elements/wrapper
+++ b/share/web/templates/_elements/wrapper
@@ -15,6 +15,7 @@
       </h1>
 
       <% Jifty->web->navigation->render_as_menu %>
+      <& /_elements/search_box &>
     </div>
 
     <div id="pageheader">

commit 605c6fdb9e9aa81c048e9a7288f98c16553bf835
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Thu Oct 19 21:42:18 2006 +0000

    Added support for Kwiki style markup, wiki logos and a kwiki (most recent version) importer

diff --git a/Makefile.PL b/Makefile.PL
index e79fd94..8273f33 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -5,4 +5,5 @@ requires('Jifty');
 requires('Text::Markdown');
 requires('HTML::Scrubber');
 requires('Text::Diff::HTML');
+recommends('Text::KwikiFormatish');
 WriteAll;
diff --git a/bin/import_kwiki b/bin/import_kwiki
new file mode 100644
index 0000000..fa05624
--- /dev/null
+++ b/bin/import_kwiki
@@ -0,0 +1,53 @@
+#!/usr/bin/perl
+
+use warnings;
+use strict;
+
+use Text::KwikiFormatish;
+
+use Jifty;
+Jifty->new();
+
+get_pages('/tmp/svkwiki/data/database');
+
+
+
+sub import_page {
+    my $path = shift;
+    my $name = shift;
+    my $page = Wifty::Model::Page->new(current_user => Wifty::CurrentUser->superuser);
+    open( my $file, "<", $path)||die $!;
+    my @content = <$file>;
+   # chomp(@content);
+    close $file;
+    my $content = join("\n", at content);
+       return unless $content; 
+    my      $html = Text::KwikiFormatish::format( $content);
+
+     $name =~ s/\s*//g;
+    $page->load_by_cols(name =>$name);
+    if ($page->id) {
+        $page->set_content($content);
+    }
+    else {
+   my ($ret)=  $page->create( name => $name, content => $content);
+   warn $ret;
+   }
+}
+
+sub get_pages {
+    my $src = shift;
+    File::Find::find(
+        {   wanted => sub {
+                   &import_page($File::Find::name, $_) 
+            },
+            follow => 0
+            
+        },
+
+        $src
+    );
+
+    
+
+}
diff --git a/etc/config.yml b/etc/config.yml
index df6b32e..b84a8a3 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -23,3 +23,7 @@ framework:
 application:
 #  RequireAuth: 1
   WikiName: A Wiki
+  Formatter: Markdown
+  # The formatter options are "Markdown" and "Kwiki"
+  # Logo: http://svk.bestpractical.com/svk-logo.png
+  # The logo points to the url to a logo image
diff --git a/lib/Wifty/Form/Field/WikiPage.pm b/lib/Wifty/Form/Field/WikiPage.pm
index 6c7bec5..cee01e1 100644
--- a/lib/Wifty/Form/Field/WikiPage.pm
+++ b/lib/Wifty/Form/Field/WikiPage.pm
@@ -15,6 +15,10 @@ field on update, and wikifies itself on read-only display.
 package Wifty::Form::Field::WikiPage;
 use base qw(Jifty::Web::Form::Field::Textarea);
 
+use HTML::Scrubber;
+
+
+
 =head2 render_value
 
 Render a wikified view of this field's content.
@@ -50,7 +54,7 @@ sub wiki_content {
         {   '*'   => 0,
             id    => 1,
             class => 1,
-            href  => qr{^(?:(?:\w+$)|http:|ftp:|https:|/)}i,
+            href  => qr{^(?:(?:\w+$)|http:|ftp:|https:|\.?/)}i,
 
             # Match http, ftp and relative urls
             face   => 1,
@@ -64,8 +68,15 @@ sub wiki_content {
         qw[H1 H2 H3 H4 H5 A STRONG EM CODE PRE B U P BR I HR BR SPAN DIV UL OL LI DL DT DD]);
     $scrubber->comment(0);
 
-    $content = Text::Markdown::markdown( $content );
-    $content = $scrubber->scrub( $content );
+    if (Jifty->config->app('Formatter') eq 'Markdown' ) {
+            require Text::Markdown;
+            $content = Text::Markdown::markdown( $content );
+    }
+    elsif (Jifty->config->app('Formatter') eq 'Kwiki') {
+        require Text::KwikiFormatish;
+        $content = Text::KwikiFormatish::format( $content);
+    }
+    #$content = $scrubber->scrub( $content );
     return ( $content );
 
 }
diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index f39c5d4..96a31df 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -28,8 +28,6 @@ column revisions =>
 package Wifty::Model::Page;
 use base qw/Wifty::Record/;
 use Wifty::Model::RevisionCollection;
-use Text::Markdown;
-use HTML::Scrubber;
 
 
 sub create {
diff --git a/share/web/static/css/app-base.css b/share/web/static/css/app-base.css
index 71e20ef..94becf6 100644
--- a/share/web/static/css/app-base.css
+++ b/share/web/static/css/app-base.css
@@ -24,6 +24,10 @@ a {
     margin-top: 2.3em;
 }
 
+#logo {
+    float: right;
+}
+
 #wikiheader {
     float: right;
     text-align: right;
diff --git a/share/web/templates/_elements/markup b/share/web/templates/_elements/markup
index cd83f25..24e4c9c 100644
--- a/share/web/templates/_elements/markup
+++ b/share/web/templates/_elements/markup
@@ -1,3 +1,7 @@
+<%init>
+return undef unless (Jifty->config->app('Formatter') eq 'Markdown');
+</%init>
+
 <div id="syntax">
 <div><a href="#" onclick="Element.toggle('syntax_content');return(false);"><b>Wiki Syntax Help</b></a>
 </div>
diff --git a/share/web/templates/_elements/wrapper b/share/web/templates/_elements/wrapper
index 67b2e13..f06fbe2 100644
--- a/share/web/templates/_elements/wrapper
+++ b/share/web/templates/_elements/wrapper
@@ -7,7 +7,9 @@
                                           url => '/__jifty/admin/') %>.
   </div>
 % }
-
+    <div id="logo">
+   <% Jifty->config->app('Logo') ? '<img src="'.Jifty->config->app('Logo').'" alt="" />' : '' |n %>
+   </div>
   <div id="header">
     <div id="wikiheader">
       <h1 id="wikiname">

commit c838c52bc714617d5c95ae13ba2699909f1d24f1
Author: Nelson Elhage <nelhage at bestpractical.com>
Date:   Thu Oct 19 21:46:37 2006 +0000

    Actually adding the search box component

diff --git a/share/web/templates/_elements/search_box b/share/web/templates/_elements/search_box
new file mode 100644
index 0000000..c487d97
--- /dev/null
+++ b/share/web/templates/_elements/search_box
@@ -0,0 +1,10 @@
+<%init>
+my $action =  Jifty->web->new_action(class => 'SearchPage', moniker => 'search');
+$action->sticky_on_success(1);
+</%init>
+<span>
+<% Jifty->web->form->start %>
+<% Jifty->web->form->next_page(url => '/search') %>
+<% $action->form_field('contains', label => 'Search:') %>
+<% Jifty->web->form->end %>
+</span>

commit 6a82a76c7f38ec1e9a73d5319d471a5a5ed2063a
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Wed Nov 1 04:49:39 2006 +0000

    importer updates for svk wiki conversion

diff --git a/bin/import_kwiki b/bin/import_kwiki
index fa05624..68011b4 100644
--- a/bin/import_kwiki
+++ b/bin/import_kwiki
@@ -1,4 +1,4 @@
-#!/usr/bin/perl
+#!/opt/local/bin/perl
 
 use warnings;
 use strict;
@@ -8,7 +8,7 @@ use Text::KwikiFormatish;
 use Jifty;
 Jifty->new();
 
-get_pages('/tmp/svkwiki/data/database');
+get_pages(shift @ARGV);
 
 
 
@@ -21,17 +21,17 @@ sub import_page {
    # chomp(@content);
     close $file;
     my $content = join("\n", at content);
-       return unless $content; 
+       return unless length($content) > 4; 
     my      $html = Text::KwikiFormatish::format( $content);
 
-     $name =~ s/\s*//g;
+    #$name =~ s/\s*//g;
     $page->load_by_cols(name =>$name);
     if ($page->id) {
         $page->set_content($content);
     }
     else {
    my ($ret)=  $page->create( name => $name, content => $content);
-   warn $ret;
+   warn $ret. ": $name";
    }
 }
 
diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index e19d01f..2b7ee8d 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -83,7 +83,7 @@ on 'search', run {
 };
 
 # Show recent edits
-on 'recent', run {
+on 'recent*', run {
     my $then = DateTime->from_epoch( epoch => ( time - ( 86400 * 7 ) ) );
     my $pages = Wifty::Model::PageCollection->new();
     $pages->limit(

commit 37cf4fccdcca55653ddce8758ebf37733dc14735
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Wed Nov 1 14:39:17 2006 +0000

    page numbers ending in digits were triggering the wrong regex

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 2b7ee8d..0cd5b94 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -28,7 +28,7 @@ on '/create/*', run {
 on qr{^/(view|edit)/(.*)}, run {
     my ( $name, $rev );
     my $page_name = $1;
-    if ( $2 =~ qr{^(.*?)/?(\d*?)$} ) {
+    if ( $2 =~ qr{^(.*?)(/\d*)?$} ) {
         $name = $1;
         $rev  = $2;
     }

commit 2fb98886582f72f3b9d388160dadc191885457de
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Wed Nov 1 17:58:40 2006 +0000

    better chomp() import

diff --git a/bin/import_kwiki b/bin/import_kwiki
index 68011b4..20bc777 100644
--- a/bin/import_kwiki
+++ b/bin/import_kwiki
@@ -18,7 +18,7 @@ sub import_page {
     my $page = Wifty::Model::Page->new(current_user => Wifty::CurrentUser->superuser);
     open( my $file, "<", $path)||die $!;
     my @content = <$file>;
-   # chomp(@content);
+    chomp(@content);
     close $file;
     my $content = join("\n", at content);
        return unless length($content) > 4; 

commit b0ecb51eea8af3ba9bba081f6d1c9960113ff19b
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Thu Nov 2 22:28:33 2006 +0000

    Regex messup made it impossible to view history

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 0cd5b94..ac6563d 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -28,7 +28,7 @@ on '/create/*', run {
 on qr{^/(view|edit)/(.*)}, run {
     my ( $name, $rev );
     my $page_name = $1;
-    if ( $2 =~ qr{^(.*?)(/\d*)?$} ) {
+    if ( $2 =~ qr{^(.*?)(?:/(\d*))?$} ) {
         $name = $1;
         $rev  = $2;
     }

commit 5d5d1d5afa59efad2568e3c28b5231b8b0948e10
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Mon Nov 13 00:12:25 2006 +0000

    First draft of declarative templates for Wifty. Only works on the Template::Declare branch of jifty

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
new file mode 100644
index 0000000..59077b3
--- /dev/null
+++ b/lib/Wifty/View.pm
@@ -0,0 +1,642 @@
+use warnings;
+use strict;
+
+
+=head1 NAME
+
+Wifty::View
+
+=head1 DESCRIPTION
+
+This code is only useful on the new Jifty "Declarative tempaltes" branch. It shouldn't get in the way 
+if you're running a traditional (0.610 or before) Jifty.
+
+=cut
+
+package Wifty::View;
+use base qw/Jifty::View::Declare::Templates/;
+# includes my application's plugins' View libraries as superclasses.
+use Template::Declare::Tags;
+use Jifty::View::Declare::Templates;
+
+private template page_list => sub {
+
+    # actually creates: sub _jifty_ui_template_page_list
+    #
+    my (  $pages, $id ) = get(qw(pages id));
+    with( id => $id, class => "pagelist" ), dl {
+
+        while ( my $page = $pages->next ) {
+            dt {
+                hyperlink(
+                    label => $page->name,
+                    url   => '/view/' . $page->name
+                );
+            };
+            dd {
+                outs( $page->updated );
+                outs(
+                    ' - ('
+                        . (
+                          $page->updated_by->id
+                        ? $page->updated_by->name
+                        : _('Anonymous')
+                        )
+                        . ')'
+                );
+            };
+        }
+    };
+};
+
+private template nav => sub {
+    my $top  = Jifty->web->navigation;
+    $top->child( Home => url => "/", sort_order => 1 );
+    $top->child(
+        Recent  =>
+            url => "/recent",
+        label      => "Recent Changes",
+        sort_order => 2
+    );
+    $top->child(
+        Search  =>
+            url => "/search",
+        label      => "Search",
+        sort_order => 3
+    );
+
+    if ( Jifty->config->framework('AdminMode') ) {
+        $top->child(
+            Administration =>
+                url        => "/__jifty/admin/",
+            sort_order => 998
+        );
+        $top->child(
+            OnlineDocs =>
+                url    => "/__jifty/online_docs/",
+            label      => 'Online docs',
+            sort_order => 999
+        );
+    }
+
+};
+
+private template page_nav => sub {
+    my %args = (page => 'HomePage', rev => undef, @_);
+    my $page = $args{'page'};
+    my $rev = $args{'rev'};
+    
+    $page ||= 'HomePage';
+    my $subpath = $page . ( $rev ? "/$rev" : '' );
+    my $top     = Jifty->web->page_navigation;
+
+    my $page_obj = Wifty::Model::Page->new();
+    $page_obj->load_by_cols( name => $page );
+
+    $top->child( View => url => '/view/' . $subpath );
+    $top->child( Edit => url => '/edit/' . $subpath );
+    $top->child( History => url => '/history/' . $page );
+    $top->child( Latest => url => '/view/' . $page ) if ($rev);
+
+};
+
+private template wrapper => sub {
+    # it's actually called with args.
+    my ($args, $coderef ) = (@_);
+    my $title    = $args->{title};
+    my $id       = $args->{id};
+    my $wikiname = Jifty->config->app('WikiName') || "Wifty";
+
+    show('nav');
+    show( 'header', title => $args->{'title'}, wikiname => $wikiname );
+
+    with( id => $args->{id} ), body {
+
+        if ( Jifty->config->framework('AdminMode') ) {
+            with( class => 'warning admin_mode' ), div {
+                _('Alert') . ":"
+                    . tangent(
+                    label => _('Administration mode is enabled'),
+                    url   => '/__jifty/admin/'
+                    )
+                    . ".";
+                }
+        }
+        with( id => 'logo' ), div {
+            Jifty->config->app('Logo')
+                ? '<img src="' . Jifty->config->app('Logo') . '" alt="" />'
+                : '';
+        };
+        with( id => 'header' ), div {
+            with( id => 'wikiheader' ), div {
+                with( id => "wikiname" ), h1 {
+                    hyperlink( url => "/", label => _($wikiname) );
+                };
+                outs(Jifty->web->navigation->render_as_menu);
+                show('search_box');
+
+            };
+            with( id => 'pageheader' ), div {
+                with( id => "pagename" ), h1 {
+                    _( $args->{title} );
+                };
+
+                outs (Jifty->web->page_navigation->render_as_menu);
+                }
+        };
+
+        show('salutation');
+
+        with( class => "clear" ),   hr  {};
+        with( id    => 'content' ), div {
+            Jifty->web->render_messages;
+            my $buf = '';
+            {
+            local $Template::Declare::Tags::BUFFER ='';
+            $coderef->();
+            $buf = $Template::Declare::Tags::BUFFER;
+            warn "My buffer is $buf";
+            }
+            outs($buf);
+            with( class => "clear" ), hr { };
+
+            }
+        }
+
+};
+
+
+private template search_box => sub {
+    my $action = new_action( class => 'SearchPage' );
+    $action->sticky_on_success(1);
+    span {
+        form {
+
+            form_next_page( url => '/search' );
+            param( $action, 'contains', label => 'Search:' );
+            }
+        };
+};
+
+private template salutation => sub {
+    with (id => 'salutation'),
+        div {
+
+        if (    Jifty->web->current_user->id and Jifty->web->current_user->user_object ) {
+            outs('Hiya, ');
+            with class => 'user',
+                span { Jifty->web->current_user->user_object->name };
+            outs('(' . hyperlink( label => q{Logout}, url => '/logout' ) .')');
+        } else {
+            outs("You're not currently signed in.") .  tangent( label => q{Sign in}, url => '/login' ) . "."; }
+        }
+};
+
+
+private template diff => sub {
+    my %args = ( page => undef, from => undef, to => undef, @_);
+
+    my $to   =  $args{'to'} ||$args{page}->revisions->last;
+    my $from = $args{'from'}|| $to->previous || Wifty::Model::Revision->new;
+
+    my $before = $to->previous;
+    my $after  = $to->next;
+
+    use Text::Diff ();
+    my $diff = Text::Diff::diff(
+        \( $from->content ),
+        \( $to->content ),
+        { STYLE => 'Text::Diff::HTML' }
+    );
+
+    with( class => 'revision_nav' ), div {
+        if ($before) {
+            span {
+                with class => "prev";
+                hyperlink(
+                    url   => "/view/" . $args{page}->name . "/" . $before->id,
+                    label => "Previous revision"
+                );
+            };
+        }
+        outs('|') if ( $before and $after );
+
+        if ($after) {
+            with( class => "next" ), span {
+                hyperlink(
+                    url   => "/view/" . $args{'page'}->name . "/" . $after->id,
+                    label => "Next revision"
+                );
+            };
+        }
+    };
+    with class => "diff", pre {
+        $diff;
+        };
+    hr {}
+
+};
+
+template create => sub {
+    my ( $action, $page ) = get(qw(action page));
+    show(
+        'wrapper',
+        {title => 'New page: ' . $page, id => 'create' }, 
+        sub {p{
+            form {
+                with( class => 'form_wrapper' ), div {
+                    form_next_page( url => '/view/' . $page );
+                        param($action => 'name', render_as     => 'hidden', default_value => $page);
+                    with( class => 'inline' ), div { param ($action => 'content', rows => 30 ); };
+                    with( class => 'line' ), div { form_submit( label => 'Create' );
+                    };
+                };
+            };
+            show('markup');
+        };
+        }
+    );
+};
+
+template edit => sub {
+    my ( $page, $revision, $viewer ) = get(qw(page revision viewer));
+    my $can_edit = $page->current_user_can('update');
+    show( 'page_nav', page => $page->name, rev => $revision->id );
+    show(
+        'wrapper',
+        {   title => 'Edit: ' . $page->name . ( $revision->id ? " as of " . $revision->created : '' ), id => "update" },
+        sub {
+            form {
+                with( class => 'form_wrapper' ), div {
+                    with( class => 'inline' ), div {
+                        unless ($can_edit) { with( style => "width: 70%" ), p { q{You don't have permission to edit this page. Perhaps} . tangent( url   => '/login', label => 'logging in') . q{would help. In the mean time, though, you're welcome to view and} . q{copy the source of this page.}; } }
+                        form_next_page( url => '/view/' . $page->name );
+                        param($viewer => 'content');
+                        if ($can_edit) { with( class => 'line' ), div { form_submit( label => 'Save' ); } }
+                    };
+                };
+                show('markup');
+                };
+            
+            }
+
+
+    );
+};
+
+template history => sub {
+    my ( $page, $revisions ) = get(qw(page revisions));
+    # XXX TODO, this isn't right
+    show( 'page_nav', page => $page->name );
+    show(
+        'wrapper',
+        { title => $revisions->count . " revisions of " . $page->name },
+        sub {
+            with( id => "history" ),
+
+                dl {
+                while ( my $rev = $revisions->next ) {
+                    dt {
+                        hyperlink(
+                            label => $rev->created,
+                            url   => '/view/' . $page->name . '/' . $rev->id
+                        );
+                        if ( $rev->created_by->id ) {
+                            '(' . $rev->created_by->name . ')';
+                        } else {
+                            '(Anonymous)';
+                        }
+                    };
+                    dd { length( $rev->content ) . ' bytes' };
+                }
+                };
+        }
+    );
+
+};
+
+template login => sub {
+    my ( $action, $next, ) = get(qw(action next));
+    show(
+        'wrapper',
+        { title => 'Login' },
+        sub {
+            if ( not current_user->id ) {
+                with( id => 'login-box' ), div {
+                    with( call => $next, name => "loginbox" ), form {
+                        param($action => 'email');
+                        param($action => 'password');
+                        param($action => 'remember');
+                        form_submit(
+                            label  => 'Login',
+                            submit => $action
+                        );
+                    };
+                };
+
+                p {
+                    tangent(
+                        label => q{Don't have an account?},
+                        url   => '/signup'
+                    );
+                };
+
+            } else {
+                p {
+                    "You're already logged in as "
+                        . current_user->user_object->name . "."
+                        . "If this isn't you, "
+                        . tangent(
+                        url   => '/logout',
+                        label => 'click here'
+                        )
+                        . ".";
+                    }
+            }
+        }
+    );
+};
+
+template logout => sub {
+    show(
+        'wrapper',
+        { title => "Logged out" },
+        sub {
+            p { _("Ok, you're now logged out. Have a good day.") };
+        }
+    );
+};
+
+template no_such_page => sub {
+    my (  $page ) = get(qw(page));
+    show(
+        'wrapper',
+        { title => 'No such page: ' . $page },
+        sub {
+
+            p {
+                q{Unfortunately, you've tried to reach a page that doesn't exist }
+                    . q{yet, and you don't have permissions to create pages. If you }
+                    . tangent( url => '/login', label => 'login' )
+                    . q{, you'll be able to create new pages of your own.}
+
+                }
+
+        }
+    );
+};
+
+template pages => sub {
+    my ($pages ) = get(qw(pages));
+    show(
+        'wrapper',
+        { title => 'These are the pages on your wiki!' },
+        sub {
+            show( 'page_list', pages => $pages, id => 'allpages' );
+        }
+    );
+
+};
+
+template recent => sub {
+    my ( $pages ) = get(qw(pages));
+    show(
+        'wrapper',
+        { title => 'Updated this week' },
+        sub {
+            show( 'page_list', pages => $pages, id => 'recentupdates' );
+        }
+    );
+
+};
+
+template recent_atom => sub {
+    my ( $pages) = get(qw(pages));
+    use XML::Atom::SimpleFeed;
+    use Data::UUID;
+    my $feed = XML::Atom::SimpleFeed->new(
+        title   => 'Recently changed pages',
+        link    => Jifty->web->url,
+        updated => '2009-12-31T00:00:00Z',
+        author  => 'John Doe',
+        id      => 'urn:uuid:' . Data::UViewD->new->create_str()
+    );
+
+    while ( my $page = $pages->next ) {
+
+        $feed->add_entry(
+            title   => $page->name,
+            link    => Jifty->web->url . '/view/' . $page->name,
+            id      => 'urn:uuid:' . Data::UViewD->new->create_str(),
+            summary => $page->content,
+            updated => $page->updated
+        );
+    }
+    $feed->print;
+};
+
+template search => sub {
+    my ( $pages, $search ) = get(qw(pages search));
+    show( 'wrapper',
+        { title => 'Search' },
+        sub {
+            form {
+                with( id    => "searchbox", class => 'inline'), div {
+                    param($search => 'contains', label => 'Find pages containing:' );
+                    form_submit( label => 'Search', submit => $search);
+                    };
+
+            };
+            if ($pages) {
+                show( 'page_list' => pages => $pages, id => 'searchresults' );
+            }
+        }
+    );
+};
+
+template signup => sub {
+    my (  $action, $next ) = get(qw(action next));
+    show(
+        'wrapper',
+        { title => 'Signup' },
+        sub {
+            p {q{Just a few bits of information are all that's needed.}};
+            with( call => $next, name => "signupbox" ), form {
+                param ($action => 'email');
+                param ($action => 'name');
+                param ($action => 'password');
+                param ($action => 'password_confirm');
+                form_submit( label => 'Signup', submit => $action );
+            };
+        }
+    );
+
+};
+
+template view => sub {
+    my ( $page, $revision, $viewer ) = get(qw(page revision viewer));
+    show( 'page_nav', page => $page->name, rev => $revision->id );
+    show(
+        'wrapper',
+        {   title => $page->name
+                . ( $revision->id ? " as of " . $revision->created : '' )
+        },
+        sub {
+            if ( $revision->id ) {
+                show( 'diff', page => $page, to => $revision );
+            }
+
+            param($viewer => 'content', label => '', render_mode => 'read');
+            #$viewer->form_value( 'content', label => "" );
+
+        }
+    );
+
+};
+
+private template header => sub {
+    my %args = ( title=> undef, wikiname => undef, @_);
+    
+    my (  $title, $wikiname ) = ($args{'title'}, $args{'wikiname'});
+    # $HTML::Mason::r->content_type('text/html; charset=utf-8');
+    outs(
+        '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
+    );
+
+    with(
+        xmlns      => "http://www.w3.org/1999/xhtml",
+        'xml:lang' => "en"
+        ),
+        html {
+        head {
+            with(
+                'http-equiv' => "content-type",
+                'content'    => "text/html; charset=utf-8"
+                ),
+                meta {};
+            with(
+                name    => "robots",
+                content => "all"
+                ),
+                meta {};
+            title { _($title) . ' - ' . _($wikiname) };
+
+            Jifty->web->include_css;
+            Jifty->web->include_javascript;
+
+            }
+        }
+};
+
+template markup => sub {
+    return undef unless ( Jifty->config->app('Formatter') eq 'Markdown' );
+
+    with( id => 'syntax' ), div {
+        div {
+            with(
+                href    => "#",
+                onclick => "Element.toggle('syntax_content');return(false);"
+                ),
+                a {
+                b {'Wiki Syntax Help'};
+                }
+        };
+        with( id => 'syntax_content' ), div {
+            h3   {'Phrase Emphasis'};
+            code {
+                b { '**bold**'; };
+                i {'_italic_'};
+            };
+
+            h3 {'Links'};
+
+            code {'Show me a [wiki page](WikiPage)'};
+            code {'An [example](http://url.com/ "Title")'};
+
+            h3 {'Headers'};
+
+            pre {
+                code {
+                    join( "\n",
+                        '# Header 1',
+                        '## Header 2',
+                        '###### Header 6' );
+                    }
+            };
+
+            h3 {'Lists'};
+
+            p {'Ordered, without paragraphs:'};
+
+            pre {
+                code {
+                    join( "\n", '1.  Foo', '2.  Bar' );
+                    }
+            };
+
+            p {' Unordered, with paragraphs:'};
+
+            pre {
+                code {
+                    join( "\n",
+                        '*   A list item.',
+                        'With multiple paragraphs.',
+                        '*   Bar' );
+                };
+
+                h3 {'Code Spans'};
+
+                p {
+                    code {'`<code>`'}
+                        . 'spans are delimited by backticks.';
+                };
+
+                h3 {'Preformatted Code Blocks'};
+
+                p {'Indent every line of a code block by at least 4 spaces.'};
+
+                pre {
+                    code {
+                        'This is a normal paragraph.' . "\n\n" . "\n"
+                            . '    This is a preformatted' . "\n"
+                            . '    code block.';
+                    };
+                };
+
+                h3 {'Horizontal Rules'};
+
+                p {
+                    'Three or more dashes: ' . code {'---'};
+                };
+
+                address {
+                    '(Thanks to <a href="http://daringfireball.net/projects/markdown/dingus">Daring Fireball</a>)';
+                    }
+                }
+        };
+        script {
+            qq{
+   // javascript flyout by Eric Wilhelm
+   // TODO use images for minimize/maximize button
+   // Is there a way to add a callback?
+   Element.toggle('syntax_content');
+   };
+        };
+    };
+};
+
+package Wifty::View::let;
+use Template::Declare::Tags;
+
+# /let/confirm_email
+
+template confirm_email => sub {
+    Jifty->api->allow('ConfirmEmail');
+    new_action(
+        moniker => 'confirm_email',
+        class   => 'Wifty::Action::ConfirmEmail'
+    )->run;
+    redirect("/");
+};
+
+1;

commit 194cffbf5a778fb41b445d7b44b93702e0ec991c
Author: Audrey Tang <audreyt at audreyt.org>
Date:   Sun Dec 3 01:59:09 2006 +0000

    Wifty::View - Replace old syntax:
    
        with( x => 1, y => 2 ), tag {
            ...;
        };
    
      with new syntax:
    
        tag {{ x is 1, y is 2 }
            ...;
        };

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index 59077b3..bb82830 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -20,12 +20,10 @@ use Template::Declare::Tags;
 use Jifty::View::Declare::Templates;
 
 private template page_list => sub {
-
     # actually creates: sub _jifty_ui_template_page_list
     #
-    my (  $pages, $id ) = get(qw(pages id));
-    with( id => $id, class => "pagelist" ), dl {
-
+    my ( $pages, $id ) = get(qw(pages id));
+    dl {{ id is $id, class is "pagelist" }
         while ( my $page = $pages->next ) {
             dt {
                 hyperlink(
@@ -110,10 +108,9 @@ private template wrapper => sub {
     show('nav');
     show( 'header', title => $args->{'title'}, wikiname => $wikiname );
 
-    with( id => $args->{id} ), body {
-
+    body {{ id is $args->{id} }
         if ( Jifty->config->framework('AdminMode') ) {
-            with( class => 'warning admin_mode' ), div {
+            div {{ class is 'warning admin_mode' }
                 _('Alert') . ":"
                     . tangent(
                     label => _('Administration mode is enabled'),
@@ -122,22 +119,22 @@ private template wrapper => sub {
                     . ".";
                 }
         }
-        with( id => 'logo' ), div {
+        div {{ id is 'logo' }
             Jifty->config->app('Logo')
                 ? '<img src="' . Jifty->config->app('Logo') . '" alt="" />'
                 : '';
         };
-        with( id => 'header' ), div {
-            with( id => 'wikiheader' ), div {
-                with( id => "wikiname" ), h1 {
+        div {{ id is 'header' }
+            div {{ id is 'wikiheader' }
+                h1 {{ id is 'wikiname' }
                     hyperlink( url => "/", label => _($wikiname) );
                 };
                 outs(Jifty->web->navigation->render_as_menu);
                 show('search_box');
 
             };
-            with( id => 'pageheader' ), div {
-                with( id => "pagename" ), h1 {
+            div {{ id is 'pageheader' }
+                h1 {{ id is 'pagename' }
                     _( $args->{title} );
                 };
 
@@ -147,18 +144,18 @@ private template wrapper => sub {
 
         show('salutation');
 
-        with( class => "clear" ),   hr  {};
-        with( id    => 'content' ), div {
+        hr {{ class is 'clear' }};
+        div {{ id is 'content' }
             Jifty->web->render_messages;
             my $buf = '';
             {
             local $Template::Declare::Tags::BUFFER ='';
             $coderef->();
             $buf = $Template::Declare::Tags::BUFFER;
-            warn "My buffer is $buf";
+            #warn "My buffer is $buf";
             }
             outs($buf);
-            with( class => "clear" ), hr { };
+            hr {{ class is 'clear' }};
 
             }
         }
@@ -171,21 +168,18 @@ private template search_box => sub {
     $action->sticky_on_success(1);
     span {
         form {
-
             form_next_page( url => '/search' );
-            param( $action, 'contains', label => 'Search:' );
+                render_param( $action, 'contains', label => 'Search:' );
             }
         };
 };
 
 private template salutation => sub {
-    with (id => 'salutation'),
-        div {
+    div {{ id is 'salutation' }
 
         if (    Jifty->web->current_user->id and Jifty->web->current_user->user_object ) {
             outs('Hiya, ');
-            with class => 'user',
-                span { Jifty->web->current_user->user_object->name };
+            span {{ class is 'user' } Jifty->web->current_user->user_object->name };
             outs('(' . hyperlink( label => q{Logout}, url => '/logout' ) .')');
         } else {
             outs("You're not currently signed in.") .  tangent( label => q{Sign in}, url => '/login' ) . "."; }
@@ -209,10 +203,9 @@ private template diff => sub {
         { STYLE => 'Text::Diff::HTML' }
     );
 
-    with( class => 'revision_nav' ), div {
+    div {{ class is 'revision_nav' }
         if ($before) {
-            span {
-                with class => "prev";
+            span {{ class is "prev" }
                 hyperlink(
                     url   => "/view/" . $args{page}->name . "/" . $before->id,
                     label => "Previous revision"
@@ -222,7 +215,7 @@ private template diff => sub {
         outs('|') if ( $before and $after );
 
         if ($after) {
-            with( class => "next" ), span {
+            span {{ class is "next" }
                 hyperlink(
                     url   => "/view/" . $args{'page'}->name . "/" . $after->id,
                     label => "Next revision"
@@ -230,9 +223,7 @@ private template diff => sub {
             };
         }
     };
-    with class => "diff", pre {
-        $diff;
-        };
+    pre {{ class is 'diff' } $diff };
     hr {}
 
 };
@@ -244,11 +235,14 @@ template create => sub {
         {title => 'New page: ' . $page, id => 'create' }, 
         sub {p{
             form {
-                with( class => 'form_wrapper' ), div {
+                div {{ class is 'form_wrapper' }
                     form_next_page( url => '/view/' . $page );
-                        param($action => 'name', render_as     => 'hidden', default_value => $page);
-                    with( class => 'inline' ), div { param ($action => 'content', rows => 30 ); };
-                    with( class => 'line' ), div { form_submit( label => 'Create' );
+                        render_param($action => 'name', render_as     => 'hidden', default_value => $page);
+                    div {{ class is 'inline' }
+                        render_param($action => 'content', rows => 30 );
+                    };
+                    div {{ class is 'inline' }
+                        form_submit( label => 'Create' );
                     };
                 };
             };
@@ -267,12 +261,12 @@ template edit => sub {
         {   title => 'Edit: ' . $page->name . ( $revision->id ? " as of " . $revision->created : '' ), id => "update" },
         sub {
             form {
-                with( class => 'form_wrapper' ), div {
-                    with( class => 'inline' ), div {
-                        unless ($can_edit) { with( style => "width: 70%" ), p { q{You don't have permission to edit this page. Perhaps} . tangent( url   => '/login', label => 'logging in') . q{would help. In the mean time, though, you're welcome to view and} . q{copy the source of this page.}; } }
+                div {{ class is 'form_wrapper' }
+                    div {{ class is 'inline' }
+                        unless ($can_edit) { p {{ style is "width: 70%" } q{You don't have permission to edit this page. Perhaps} . tangent( url   => '/login', label => 'logging in') . q{would help. In the mean time, though, you're welcome to view and} . q{copy the source of this page.}; } }
                         form_next_page( url => '/view/' . $page->name );
-                        param($viewer => 'content');
-                        if ($can_edit) { with( class => 'line' ), div { form_submit( label => 'Save' ); } }
+                        render_param($viewer => 'content');
+                        if ($can_edit) { div {{ class is 'line' } form_submit( label => 'Save' ); } }
                     };
                 };
                 show('markup');
@@ -292,9 +286,7 @@ template history => sub {
         'wrapper',
         { title => $revisions->count . " revisions of " . $page->name },
         sub {
-            with( id => "history" ),
-
-                dl {
+            dl {{ id is 'history' }
                 while ( my $rev = $revisions->next ) {
                     dt {
                         hyperlink(
@@ -322,11 +314,11 @@ template login => sub {
         { title => 'Login' },
         sub {
             if ( not current_user->id ) {
-                with( id => 'login-box' ), div {
-                    with( call => $next, name => "loginbox" ), form {
-                        param($action => 'email');
-                        param($action => 'password');
-                        param($action => 'remember');
+                div {{ id is 'login-box' }
+                    form {{ call is $next, name is "loginbox" }
+                        render_param($action => 'email');
+                        render_param($action => 'password');
+                        render_param($action => 'remember');
                         form_submit(
                             label  => 'Login',
                             submit => $action
@@ -441,8 +433,8 @@ template search => sub {
         { title => 'Search' },
         sub {
             form {
-                with( id    => "searchbox", class => 'inline'), div {
-                    param($search => 'contains', label => 'Find pages containing:' );
+                div {{ id is "searchbox", class is 'inline' }
+                    render_param($search => 'contains', label => 'Find pages containing:' );
                     form_submit( label => 'Search', submit => $search);
                     };
 
@@ -455,17 +447,17 @@ template search => sub {
 };
 
 template signup => sub {
-    my (  $action, $next ) = get(qw(action next));
+    my ( $action, $next ) = get(qw(action next));
     show(
         'wrapper',
         { title => 'Signup' },
         sub {
             p {q{Just a few bits of information are all that's needed.}};
-            with( call => $next, name => "signupbox" ), form {
-                param ($action => 'email');
-                param ($action => 'name');
-                param ($action => 'password');
-                param ($action => 'password_confirm');
+            form {{ call is $next, name is "signupbox" }
+                render_param($action => 'email');
+                render_param($action => 'name');
+                render_param($action => 'password');
+                render_param($action => 'password_confirm');
                 form_submit( label => 'Signup', submit => $action );
             };
         }
@@ -486,7 +478,7 @@ template view => sub {
                 show( 'diff', page => $page, to => $revision );
             }
 
-            param($viewer => 'content', label => '', render_mode => 'read');
+            render_param($viewer => 'content', label => '', render_mode => 'read');
             #$viewer->form_value( 'content', label => "" );
 
         }
@@ -514,11 +506,7 @@ private template header => sub {
                 'content'    => "text/html; charset=utf-8"
                 ),
                 meta {};
-            with(
-                name    => "robots",
-                content => "all"
-                ),
-                meta {};
+            meta {{ name is 'robots', content is 'all' }};
             title { _($title) . ' - ' . _($wikiname) };
 
             Jifty->web->include_css;
@@ -531,17 +519,14 @@ private template header => sub {
 template markup => sub {
     return undef unless ( Jifty->config->app('Formatter') eq 'Markdown' );
 
-    with( id => 'syntax' ), div {
+    div {{ id is 'syntax' }
         div {
-            with(
-                href    => "#",
-                onclick => "Element.toggle('syntax_content');return(false);"
-                ),
-                a {
-                b {'Wiki Syntax Help'};
-                }
+            a {{
+                href    is "#",
+                onclick is "Element.toggle('syntax_content');return(false);"
+            } b {'Wiki Syntax Help'}; }
         };
-        with( id => 'syntax_content' ), div {
+        div {{ id is 'syntax_content' }
             h3   {'Phrase Emphasis'};
             code {
                 b { '**bold**'; };

commit 9186ba91f8a80b1075104db2d398b1da4f8bcf11
Author: Audrey Tang <audreyt at audreyt.org>
Date:   Sun Dec 3 15:59:00 2006 +0000

    Wifty::View - Squash all with() now that we can represent
      http-equiv and xml:lang with "is" syntax.

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index bb82830..cceb15f 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -495,17 +495,9 @@ private template header => sub {
         '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
     );
 
-    with(
-        xmlns      => "http://www.w3.org/1999/xhtml",
-        'xml:lang' => "en"
-        ),
-        html {
+    html {{ xmlns is "http://www.w3.org/1999/xhtml", xml__lang is "en" }
         head {
-            with(
-                'http-equiv' => "content-type",
-                'content'    => "text/html; charset=utf-8"
-                ),
-                meta {};
+            meta {{ http_equiv is "content-type", content is "text/html; charset=utf-8" }};
             meta {{ name is 'robots', content is 'all' }};
             title { _($title) . ' - ' . _($wikiname) };
 

commit 2c5c197a507456d7cc6e5fbd505ddb6bf836c08e
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Fri Jan 26 15:17:00 2007 +0000

    modernizing model column declarations

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 96a31df..e46b053 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -1,7 +1,14 @@
-package Wifty::Model::Page::Schema;
+
+package Wifty::Model::Page;
+use warnings;
+use strict;
+
+use base qw/Wifty::Record/;
 use Jifty::DBI::Schema;
 use Wifty::Model::User;
 
+use Jifty::Record schema {
+
 column name => 
     type is 'text',
     label is 'Page name',
@@ -24,9 +31,9 @@ column updated_by =>
 
 column revisions =>
     refers_to Wifty::Model::RevisionCollection by 'page';
+};
+
 
-package Wifty::Model::Page;
-use base qw/Wifty::Record/;
 use Wifty::Model::RevisionCollection;
 
 
diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index f75545e..0a89354 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -1,17 +1,23 @@
-package Wifty::Model::Revision::Schema;
+
+package Wifty::Model::Revision;
+use warnings;
+use strict;
+
+
+use base qw/Wifty::Record/;
+
 use Jifty::DBI::Schema;
 
+use Jifty::Record schema {
 column page  => refers_to Wifty::Model::Page;
 
 column content => type is 'text', render_as 'Wifty::Form::Field::WikiPage';
 
 column created => type is 'timestamp';
-
 column created_by => refers_to Wifty::Model::User, since '0.0.20';
+};
 
 
-package Wifty::Model::Revision;
-use base qw/Wifty::Record/;
 use Jifty::RightsFrom column => 'page';
 use DateTime;
 use Wifty::Model::User;

commit b4a9f1ebac8484e865434585de70634897691ba8
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sat Jan 27 03:30:51 2007 +0000

    cleaned up a warning

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index e46b053..28c255c 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -75,6 +75,8 @@ sub _add_revision {
 
 }
 
+{
+no warnings qw'redefine';
 sub set_content {
     my $self    = shift;
     my $content = shift;
@@ -84,6 +86,7 @@ sub set_content {
                 );
     return ( $val, $msg );
 }
+};
 
 sub _set {
     my $self = shift;

commit f4e2f9283f9b50d6b7a7912fc6e3806f3160da08
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Sat Jan 27 03:31:20 2007 +0000

    unfixing the warning avoidance pending a better core test

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 28c255c..e46b053 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -75,8 +75,6 @@ sub _add_revision {
 
 }
 
-{
-no warnings qw'redefine';
 sub set_content {
     my $self    = shift;
     my $content = shift;
@@ -86,7 +84,6 @@ sub set_content {
                 );
     return ( $val, $msg );
 }
-};
 
 sub _set {
     my $self = shift;

commit 3e786d57668e8d9532d4586ade22b8f5d94924c3
Author: Kevin Falcone <falcone at bestpractical.com>
Date:   Wed Feb 21 22:31:41 2007 +0000

    move use of the RevisionCollection object before the schema
      declaration so we can refers_to it

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index e46b053..c6cf3b4 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -6,6 +6,7 @@ use strict;
 use base qw/Wifty::Record/;
 use Jifty::DBI::Schema;
 use Wifty::Model::User;
+use Wifty::Model::RevisionCollection;
 
 use Jifty::Record schema {
 
@@ -33,10 +34,6 @@ column revisions =>
     refers_to Wifty::Model::RevisionCollection by 'page';
 };
 
-
-use Wifty::Model::RevisionCollection;
-
-
 sub create {
     my $self = shift;
     my %args = (@_);

commit 65a99da68ff8381f6d6ccc70e25f621dc97b0abd
Author: Kevin Falcone <falcone at bestpractical.com>
Date:   Wed Feb 21 22:32:18 2007 +0000

    if the set_content fails, don't create a transaction

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index c6cf3b4..5064538 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -76,9 +76,12 @@ sub set_content {
     my $self    = shift;
     my $content = shift;
     my ( $val, $msg ) = $self->_set(column => 'content', value => $content);
-    $self->_add_revision( content => $content,
-                    updated_by =>( $self->current_user? $self->current_user->user_object : undef )
-                );
+
+    if ($val) {
+        $self->_add_revision( content => $content,
+                        updated_by =>( $self->current_user? $self->current_user->user_object : undef )
+                    );
+    }
     return ( $val, $msg );
 }
 

commit 084d2d011ecf98456fed50e09263894b0a0e5708
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Wed Feb 21 22:36:52 2007 +0000

    numeric pagenames lost badly

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index ac6563d..9de8e77 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -28,7 +28,7 @@ on '/create/*', run {
 on qr{^/(view|edit)/(.*)}, run {
     my ( $name, $rev );
     my $page_name = $1;
-    if ( $2 =~ qr{^(.*?)(?:/(\d*))?$} ) {
+    if ( $2 =~ qr{^(.*?)(?:/(\d+))?$} ) {
         $name = $1;
         $rev  = $2;
     }

commit 509ca19ef364b98cae49e48b8e4f707fe2c1b46f
Author: Chia-liang Kao <clkao at bestpractical.com>
Date:   Tue Apr 3 06:50:59 2007 +0000

    merge from trs' local
    
    Move Wifty::View so that it isn't automatically loaded.  It's not ready for use yet (still contains much early TD syntax).

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View-not-ready-yet.pm
similarity index 100%
rename from lib/Wifty/View.pm
rename to lib/Wifty/View-not-ready-yet.pm

commit a62ee0f7450a6f3a95ab75b80b7fb6749530e2a5
Author: Kevin Riggle <kevinr at bestpractical.com>
Date:   Fri Apr 27 03:38:35 2007 +0000

    bin/jifty was out of date

diff --git a/bin/jifty b/bin/jifty
index 1e40ae7..8f0e01c 100755
--- a/bin/jifty
+++ b/bin/jifty
@@ -1,14 +1,15 @@
-#!/usr/bin/perl
+#!/usr/bin/env perl
 use warnings;
 use strict;
 use File::Basename qw(dirname); 
+use UNIVERSAL::require;
 
 BEGIN {
-    my $dir = dirname(__FILE__); 
-    unshift @INC, "$dir/../../Jifty/lib";
-    unshift @INC, "$dir/../lib";
-    push @INC, "$dir/../../Jifty/deps";
+    Jifty::Util->require or die $UNIVERSAL::require::ERROR;
+    my $root = Jifty::Util->app_root;
+    unshift @INC, "$root/lib" if ($root);
 }
 
 use Jifty::Script;
+local $SIG{INT} = sub { warn "Stopped\n"; exit; };
 Jifty::Script->dispatch();

commit d1428d7c52a880f83af58075a568fb2b3896bace
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Fri Oct 26 20:42:12 2007 +0000

    fixing for text wikiformat bogosity with newlines

diff --git a/lib/Wifty/Form/Field/WikiPage.pm b/lib/Wifty/Form/Field/WikiPage.pm
index cee01e1..fcc1760 100644
--- a/lib/Wifty/Form/Field/WikiPage.pm
+++ b/lib/Wifty/Form/Field/WikiPage.pm
@@ -63,6 +63,8 @@ sub wiki_content {
         }
     );
 
+    $content =~ s/(?:\n\r|\r\n|\r)/\n/g;
+
     $scrubber->deny(qw[*]);
     $scrubber->allow(
         qw[H1 H2 H3 H4 H5 A STRONG EM CODE PRE B U P BR I HR BR SPAN DIV UL OL LI DL DT DD]);

commit dcf36825f275bfd641891ba964fab7e46ccaaf62
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Oct 28 03:36:27 2007 +0000

    fetch only one record from the DB

diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index 0a89354..88fd8eb 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -54,8 +54,9 @@ sub previous {
         quote_value    => 0,
         case_sensitive => 1
     );
-    $revisions->order_by( { column => 'id' } );
-    return $revisions->last;
+    $revisions->order_by( { column => 'id', order => 'desc' } );
+    $revisions->rows_per_page(1);
+    return $revisions->first;
 }
 
 sub next {
@@ -77,6 +78,7 @@ sub next {
         case_sensitive => 1
     );
     $revisions->order_by( { column => 'id' } );
+    $revisions->rows_per_page(1);
     return $revisions->first;
 }
 

commit 2fe256e2665e2a7db15e6937cb84e4440898ba5d
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Nov 30 00:51:15 2008 +0000

    switch from deprecated Login plugin to User + Auth::Password plugins

diff --git a/etc/config.yml b/etc/config.yml
index b84a8a3..470e5a9 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -1,4 +1,7 @@
+---
 framework:
+  ConfigFileVersion: 4
+
   AdminMode: 0
   ApplicationName: Wifty
   AdminEmail: 'wifty at example.com'
@@ -11,7 +14,12 @@ framework:
     Password: ''
     RequireSSL: 0
   Plugins:
-    - Login: {}
+    - SkeletonApp: {}
+    - CompressedCSSandJS: {}
+    - User: {}
+    - Authentication::Password:
+        login_by: email
+
   Mailer: IO
   MailerArgs:
     - %log/mail.log%
diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index a97d6dd..bc483eb 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -1,4 +1,13 @@
 package Wifty::Model::User;
-use base qw/Jifty::Plugin::Login::Model::User/;
+
+use Jifty::DBI::Schema;
+use Wifty::Record schema {
+    # column definitions
+};
+
+# import columns: name, email and email_confirmed
+use Jifty::Plugin::User::Mixin::Model::User;
+# import columns: password, auth_token
+use Jifty::Plugin::Authentication::Password::Mixin::Model::User;
 
 1;

commit c77871b3cc2654d30071b24dc3d997780cfb8c11
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Nov 30 00:53:17 2008 +0000

    remove some templates which are part of old Login plugin

diff --git a/share/web/templates/let/confirm_email b/share/web/templates/let/confirm_email
deleted file mode 100644
index 86a69a1..0000000
--- a/share/web/templates/let/confirm_email
+++ /dev/null
@@ -1,8 +0,0 @@
-<%init>
-Jifty->api->allow( 'ConfirmEmail'); 
-Jifty->web->new_action(
-    moniker => 'confirm_email',
-    class   => 'Wifty::Action::ConfirmEmail',
-)->run;
-Jifty->web->redirect("/");
-</%init>
diff --git a/share/web/templates/login b/share/web/templates/login
deleted file mode 100644
index 5884b65..0000000
--- a/share/web/templates/login
+++ /dev/null
@@ -1,26 +0,0 @@
-<%args>
-$action => undef
-$next => undef
-</%args>
-<&|/_elements/wrapper, title => 'Login' &>
-
-% if ( not Jifty->web->current_user->id ) {
-<div id="login-box">
-<% Jifty->web->form->start(call => $next, name => "loginbox") %>
-<% $action->form_field('email') %>
-<% $action->form_field('password') %>
-<% $action->form_field('remember') %>
-<% Jifty->web->form->submit(label => 'Login', submit => $action) %>
-<% Jifty->web->form->end %>
-</div>
-
-<p><% Jifty->web->tangent( label => q{Don't have an account?}, url => '/signup' )%></p>
-
-% }
-% else {
-<p>
-You're already logged in as <% Jifty->web->current_user->user_object->name %>.
-If this isn't you, <% Jifty->web->tangent( url => '/logout', label => 'click here') %>.
-</p>
-% }
-</&>
diff --git a/share/web/templates/logout b/share/web/templates/logout
deleted file mode 100644
index f6eee9d..0000000
--- a/share/web/templates/logout
+++ /dev/null
@@ -1,3 +0,0 @@
-<&| /_elements/wrapper, title => "Logged out" &>
-<p>Ok, you're now logged out. Have a good day.</p>
-</&>
diff --git a/share/web/templates/signup b/share/web/templates/signup
deleted file mode 100644
index 70103a1..0000000
--- a/share/web/templates/signup
+++ /dev/null
@@ -1,14 +0,0 @@
-<%args>
-$action
-$next
-</%args>
-<&|/_elements/wrapper, title => 'Signup' &>
-<p>Just a few bits of information are all that's needed.</p>
-<% Jifty->web->form->start(call => $next, name => "signupbox") %>
-<% $action->form_field('email') %>
-<% $action->form_field('name') %>
-<% $action->form_field('password') %>
-<% $action->form_field('password_confirm') %>
-<% Jifty->web->form->submit(label => 'Signup', submit => $action) %>
-<% Jifty->web->form->end %>
-</&>

commit f982223dff6fe6a4d49ca41529cf77533d356037
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Nov 30 00:53:41 2008 +0000

    fix test

diff --git a/t/02-login.t b/t/02-login.t
index 74c5d94..1136987 100644
--- a/t/02-login.t
+++ b/t/02-login.t
@@ -33,18 +33,21 @@ sub try_login {
     {
         local $Test::Builder::Level = $Test::Builder::Level;
         $Test::Builder::Level++;
-        $mech->fill_in_action_ok('loginbox', email => $user, password => $pass);
+        $mech->fill_in_action_ok(
+            $mech->moniker_for("Wifty::Action::Login"),
+            email => $user, password => $pass
+        );
         $mech->submit_html_ok();
     }
 }
 
 # Try logging in with a bad user
 try_login($mech, 'baduser at localhost', 'notmypassword');
-$mech->content_contains('No account has that email address', "Login failed with bad username");
+$mech->content_contains("It doesn't look like there's an account by that name", "Login failed with bad username");
 
 # With a blank password
 try_login($mech, 'someuser at localost', '');
-$mech->content_contains('need to fill in this field','Login fails with no password');
+$mech->content_contains('Please fill in this field','Login fails with no password');
 
 # With the wrong password
 try_login($mech, 'someuser at localhost', 'badmemory');

commit 97d54ccc03432784677a9cdd69723ee9bcb9119d
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Nov 30 03:12:32 2008 +0000

    we will not need these templates

diff --git a/lib/Wifty/View-not-ready-yet.pm b/lib/Wifty/View-not-ready-yet.pm
index cceb15f..58fc261 100644
--- a/lib/Wifty/View-not-ready-yet.pm
+++ b/lib/Wifty/View-not-ready-yet.pm
@@ -307,58 +307,6 @@ template history => sub {
 
 };
 
-template login => sub {
-    my ( $action, $next, ) = get(qw(action next));
-    show(
-        'wrapper',
-        { title => 'Login' },
-        sub {
-            if ( not current_user->id ) {
-                div {{ id is 'login-box' }
-                    form {{ call is $next, name is "loginbox" }
-                        render_param($action => 'email');
-                        render_param($action => 'password');
-                        render_param($action => 'remember');
-                        form_submit(
-                            label  => 'Login',
-                            submit => $action
-                        );
-                    };
-                };
-
-                p {
-                    tangent(
-                        label => q{Don't have an account?},
-                        url   => '/signup'
-                    );
-                };
-
-            } else {
-                p {
-                    "You're already logged in as "
-                        . current_user->user_object->name . "."
-                        . "If this isn't you, "
-                        . tangent(
-                        url   => '/logout',
-                        label => 'click here'
-                        )
-                        . ".";
-                    }
-            }
-        }
-    );
-};
-
-template logout => sub {
-    show(
-        'wrapper',
-        { title => "Logged out" },
-        sub {
-            p { _("Ok, you're now logged out. Have a good day.") };
-        }
-    );
-};
-
 template no_such_page => sub {
     my (  $page ) = get(qw(page));
     show(
@@ -446,25 +394,6 @@ template search => sub {
     );
 };
 
-template signup => sub {
-    my ( $action, $next ) = get(qw(action next));
-    show(
-        'wrapper',
-        { title => 'Signup' },
-        sub {
-            p {q{Just a few bits of information are all that's needed.}};
-            form {{ call is $next, name is "signupbox" }
-                render_param($action => 'email');
-                render_param($action => 'name');
-                render_param($action => 'password');
-                render_param($action => 'password_confirm');
-                form_submit( label => 'Signup', submit => $action );
-            };
-        }
-    );
-
-};
-
 template view => sub {
     my ( $page, $revision, $viewer ) = get(qw(page revision viewer));
     show( 'page_nav', page => $page->name, rev => $revision->id );
@@ -602,18 +531,4 @@ template markup => sub {
     };
 };
 
-package Wifty::View::let;
-use Template::Declare::Tags;
-
-# /let/confirm_email
-
-template confirm_email => sub {
-    Jifty->api->allow('ConfirmEmail');
-    new_action(
-        moniker => 'confirm_email',
-        class   => 'Wifty::Action::ConfirmEmail'
-    )->run;
-    redirect("/");
-};
-
 1;

commit ebd40518d454a1e38f97c074cb422bdbfc1aa703
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Nov 30 03:15:24 2008 +0000

    replace _elements/page_nav with a function in dispatcher

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 9de8e77..690fee0 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -35,6 +35,9 @@ on qr{^/(view|edit)/(.*)}, run {
     my $page = Wifty::Model::Page->new();
     $page->load_by_cols( name => $name );
     Jifty->web->redirect( '/create/' . $name ) unless ( $page->id );
+
+    setup_page_nav($name, $rev);
+
     my $revision = Wifty::Model::Revision->new();
     $revision->load_by_cols( page => $page->id, id => $rev ) if ($rev);
     set page => $page;
@@ -54,6 +57,8 @@ on 'history/*', run {
     $page->load_by_cols( name => $name );
     redirect( '/create/' . $name ) unless ( $page->id );
 
+    setup_page_nav($name);
+
     my $revisions = $page->revisions;
     $revisions->order_by( column => 'id', order => 'desc');
 
@@ -95,5 +100,15 @@ on 'recent*', run {
     set pages => $pages;
 };
 
+sub setup_page_nav {
+    my ($page, $rev) = @_;
+
+    my $subpath =  $page . ($rev ? "/$rev" : '');
+    my $top = Jifty->web->page_navigation;
+    $top->child( View => url => '/view/'.$subpath);
+    $top->child( Edit => url => '/edit/'.$subpath);
+    $top->child( History => url => '/history/'.$page);
+    $top->child( Latest => url => '/view/'.$page) if $rev;
+}
 
 1;
diff --git a/share/web/templates/_elements/page_nav b/share/web/templates/_elements/page_nav
deleted file mode 100644
index a6dd135..0000000
--- a/share/web/templates/_elements/page_nav
+++ /dev/null
@@ -1,17 +0,0 @@
-<%init>
-my $subpath =  $page . ($rev ? "/$rev" : '');
-my $top = Jifty->web->page_navigation;
-
-my $page_obj = Wifty::Model::Page->new();
-$page_obj->load_by_cols(name => $page);
-
-$top->child( View => url => '/view/'.$subpath);
-$top->child( Edit => url => '/edit/'.$subpath);
-$top->child( History => url => '/history/'.$page);
-$top->child( Latest => url => '/view/'.$page) if ($rev);
-
-</%init>
-<%args>
-$page => 'HomePage'
-$rev => undef
-</%args>
diff --git a/share/web/templates/edit b/share/web/templates/edit
index c7b0ab3..37d4512 100644
--- a/share/web/templates/edit
+++ b/share/web/templates/edit
@@ -6,7 +6,6 @@ $viewer
 <%init>
 my $can_edit = $page->current_user_can('update');
 </%init>
-<&/_elements/page_nav, page => $page->name, rev => $revision->id &>
 <&|/_elements/wrapper, title => 'Edit: '.$page->name . ($revision->id ? " as of ".$revision->created : ''), id => "update"  &>
 <% Jifty->web->form->start %>
 <div class="form_wrapper">
diff --git a/share/web/templates/history b/share/web/templates/history
index d5741c1..5fa39a8 100644
--- a/share/web/templates/history
+++ b/share/web/templates/history
@@ -2,7 +2,6 @@
 $page
 $revisions
 </%args>
-<& /_elements/page_nav, page => $page->name &>
 <&|/_elements/wrapper, title => $revisions->count ." revisions of " .$page->name &>
 <dl id="history">
 % while (my $rev = $revisions->next) {
diff --git a/share/web/templates/view b/share/web/templates/view
index 75133b9..cf08c0f 100644
--- a/share/web/templates/view
+++ b/share/web/templates/view
@@ -3,7 +3,6 @@ $page
 $revision
 $viewer
 </%args>
-<& /_elements/page_nav, page => $page->name, rev => $revision->id &>
 <&|/_elements/wrapper, title => $page->name . ($revision->id ? " as of ".$revision->created : '') &>
 
 % if ($revision->id) {

commit 02921c220d34e95871dd79ca1e8c98bbe24642da
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Nov 30 03:19:18 2008 +0000

    replace _elements/nav with a dispatcher rule

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 690fee0..fa9e563 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -6,6 +6,13 @@ under '/', run {
     Jifty->api->deny('ConfirmEmail');
 };
 
+before '*', run {
+    my $top = Jifty->web->navigation;
+    $top->child( Home   => url => "/", sort_order => 1 );
+    $top->child( Recent => url => "/recent", label => "Recent Changes", sort_order => 2 );
+    $top->child( Search => url => "/search", label => "Search", sort_order => 3 );
+};
+
 # Default page
 on '/', run {
     redirect( '/view/HomePage');
diff --git a/share/web/templates/_elements/nav b/share/web/templates/_elements/nav
deleted file mode 100644
index 808bd8c..0000000
--- a/share/web/templates/_elements/nav
+++ /dev/null
@@ -1,13 +0,0 @@
-<%init>
-my $top = Jifty->web->navigation;
-$top->child( Home   => url => "/", sort_order => 1 );
-$top->child( Recent => url => "/recent", label => "Recent Changes", sort_order => 2 );
-$top->child( Search => url => "/search", label => "Search", sort_order => 3 );
-
-if ( Jifty->config->framework('AdminMode') ) {
-    $top->child( Administration => url => "/__jifty/admin/", sort_order => 998);
-    $top->child( OnlineDocs     => url => "/__jifty/online_docs/", label => 'Online docs', sort_order => 999);
-}
-
-return;
-</%init>

commit 76aa1d95c0e91066f7e7dfb2dec3b66d79917ed3
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Dec 1 05:05:00 2008 +0000

    remove empty dir

commit 4989ab7aa115ae9731cf5425f6a4ca9359a75e0c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Dec 1 05:09:49 2008 +0000

    replace all mason templates with TD, a little changes in page layout

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
new file mode 100644
index 0000000..e0beb53
--- /dev/null
+++ b/lib/Wifty/View.pm
@@ -0,0 +1,291 @@
+use warnings;
+use strict;
+
+package Wifty::View;
+use Jifty::View::Declare -base;
+
+template 'view' => page {
+    my ( $page, $revision, $viewer ) = get(qw(page revision viewer));
+    my $rev = $revision->id;
+    my $title = $rev
+        ? _('%1 as of %2', $page->name, $revision->created)
+        : $page->name;
+    { title is $title }
+    show( 'diff', page => $page, to => $revision ) if $rev;
+    render_param($viewer => 'content', label => '', render_mode => 'read');
+};
+
+template 'edit' => page {
+    my ( $page, $revision, $viewer ) = get(qw(page revision viewer));
+
+    my $title = $revision->id
+        ? _('Edit page %1 as of %2', $page->name, $revision->created)
+        : _('Edit page %1');
+    { title is $title }
+
+    my $can_edit = $page->current_user_can('update');
+
+    show('markup');
+
+    form { div { attr { class is 'form_wrapper' };
+        div { attr { class is 'inline' };
+            unless ( $can_edit ) {
+            }
+            form_next_page url => '/view/'.$page->name;
+            render_action $viewer, ['content'];
+        };
+        if ( $can_edit ) {
+            div { attr { class is 'line' };
+                form_submit label => _('Save')
+            }
+        }
+    } };
+};
+
+template create => page {
+    my ($action, $page) = get(qw(action page));
+
+    { title is _("New page '%1'", $page), id is 'create' };
+
+    div {
+        show('markup');
+
+        form { div { attr { class is 'form_wrapper' };
+            form_next_page url => '/view/' . $page;
+            render_param $action, 'name',
+                render_as => 'hidden',
+                default_value => $page;
+
+            render_param $action, 'content', rows => 30;
+            form_submit( label => _('Create') );
+        } }
+    };
+};
+
+template no_such_page => page {
+    my ($page) = get(qw(page));
+
+    { title is _("No '%1' page", $page) }
+
+    p { 
+        q{Unfortunately, you've tried to reach a page that doesn't exist }
+        . q{yet, and you don't have permissions to create pages. If you }
+        . tangent( url => '/login', label => 'login' )
+        . q{, you'll be able to create new pages of your own.}
+    }
+};
+
+template history => page {
+    my ( $page, $revisions ) = get(qw(page revisions));
+    { title is $revisions->count . " revisions of " . $page->name }
+
+    dl { { id is 'history' }
+        while ( my $rev = $revisions->next ) {
+            dt {
+                hyperlink(
+                    label => $rev->created,
+                    url   => '/view/' . $page->name . '/' . $rev->id
+                );
+                if ( $rev->created_by->id ) {
+                    '(' . $rev->created_by->name . ')';
+                } else {
+                    '(Anonymous)';
+                }
+            };
+            dd { length( $rev->content ) . ' bytes' };
+        }
+    };
+};
+
+template recent => page {
+    my ($pages) = get(qw(pages));
+    { title is _('Updated this week') }
+
+    show( 'page_list', pages => $pages, id => 'recentupdates' );
+};
+
+template pages => page {
+    my ($pages ) = get(qw(pages));
+    { title is _('These are the pages on your wiki!') }
+
+    show( 'page_list', pages => $pages, id => 'allpages' );
+};
+
+template search => page {
+    my ( $pages, $search ) = get(qw(pages search));
+
+    form { div { { id is "searchbox", class is 'inline' }
+        render_param $search => 'contains', label => _('Find pages containing:');
+        form_submit label => 'Search', submit => $search;
+    }; };
+    if ( $pages ) {
+        show( 'page_list' => pages => $pages, id => 'searchresults' );
+    }
+};
+
+private template 'search_box' => sub {
+    my $action = new_action(class => 'SearchPage', moniker => 'search');
+    $action->sticky_on_success(1);
+    span { form {
+        form_next_page url => '/search';
+        render_param $action, 'contains', label => 'Search:';
+    } };
+};
+
+private template 'menu' => sub {
+    my $wikiname = Jifty->config->app('WikiName') || "Wifty";
+    h1 { attr { id is 'wikiname' }
+        Jifty->web->link( url => "/", label => _($wikiname) )
+    }
+    div { attr { id => "navigation" };
+        Jifty->web->navigation->render_as_menu;
+    };
+};
+
+private template 'heading_in_wrapper' => sub {
+    h1 { attr { class => 'title' }; outs_raw(get('title')) };
+    Jifty->web->page_navigation->render_as_menu;
+#    show('/search_box');
+    hr { {class is 'clear'} }
+};
+
+
+private template markup => sub {
+    return undef unless Jifty->config->app('Formatter') eq 'Markdown';
+
+    div {{ id is 'syntax' }
+        div {
+            a {{
+                href    is "#",
+                onclick is 'jQuery("syntax_content").toggle();return(false);'
+            } b {_('Wiki Syntax Help')} }
+        };
+        div {{ id is 'syntax_content' }
+            h3   {'Phrase Emphasis'};
+            code {
+                b {'**bold**'; };
+                i {'_italic_'};
+            };
+
+            h3 {'Links'};
+
+            code {'Show me a [wiki page](WikiPage)'};
+            code {'An [example](http://url.com/ "Title")'};
+
+            h3 {'Headers'};
+
+            code { pre { join "\n",
+                '# Header 1', '## Header 1', '###### Header 6'
+            } };
+
+            h3 {'Lists'};
+
+            p {'Ordered, without paragraphs:'};
+
+            code { pre { join "\n", '1. Foo', '2. Bar' } };
+
+            p {'Unordered, with paragraphs:'};
+
+            code { pre { join "\n",
+                '*   A list item.', 
+                'With multiple paragraphs.',
+                '*   Bar',
+            } };
+
+            h3 {'Code Spans'};
+
+            p { code {'`<code>`'}; outs(' - spans are delimited by backticks') };
+
+            h3 {'Preformatted Code Blocks'};
+
+            p {'Indent every line of a code block by at least 4 spaces.'};
+
+            code {
+                pre {
+                    'This is a normal paragraph.' . "\n\n" . "\n"
+                        . '    This is a preformatted' . "\n"
+                        . '    code block.';
+                };
+            };
+
+            h3 {'Horizontal Rules'};
+
+            p {
+                outs('Three or more dashes: '); code {'---'};
+            };
+
+            address {
+                outs_raw '(Thanks to <a href="http://daringfireball.net/projects/markdown/dingus">Daring Fireball</a>)';
+            }
+        };
+        script { outs_raw 'jQuery("syntax_content").toggle();' };
+    };
+};
+
+private template page_list => sub {
+    my ( $pages, $id ) = get(qw(pages id));
+    dl {{ id is $id, class is "pagelist" }
+        while ( my $page = $pages->next ) {
+            dt {
+                hyperlink(
+                    label => $page->name,
+                    url   => '/view/' . $page->name
+                );
+            };
+            dd {
+                outs( $page->updated );
+                outs(
+                    ' - ('
+                        . (
+                          $page->updated_by->id
+                        ? $page->updated_by->name
+                        : _('Anonymous')
+                        )
+                        . ')'
+                );
+            };
+        }
+    };
+};
+
+private template diff => sub {
+    my ($page, $from, $to) = get(qw(page from to));
+
+    $to ||= $page->revisions->last;
+    $from ||= $to->previous || Wifty::Model::Revision->new;
+
+    my $before = $to->previous;
+    my $after  = $to->next;
+
+    use Text::Diff ();
+    my $diff = Text::Diff::diff(
+        \( $from->content ),
+        \( $to->content ),
+        { STYLE => 'Text::Diff::HTML' }
+    );
+
+    div {{ class is 'revision_nav' }
+        if ($before) {
+            span {{ class is "prev" }
+                hyperlink(
+                    url   => "/view/" . $page->name . "/" . $before->id,
+                    label => _("Previous revision")
+                );
+            };
+        }
+        outs('|') if $before and $after;
+        if ($after) {
+            span {{ class is "next" }
+                hyperlink(
+                    url   => "/view/" . $page->name . "/" . $after->id,
+                    label => _("Next revision")
+                );
+            };
+        }
+    };
+    pre {{ class is 'diff' } outs_raw($diff) };
+    hr {}
+};
+
+
+1;
diff --git a/lib/Wifty/View/Page.pm b/lib/Wifty/View/Page.pm
new file mode 100644
index 0000000..c896e24
--- /dev/null
+++ b/lib/Wifty/View/Page.pm
@@ -0,0 +1,21 @@
+use strict;
+use warnings;
+
+package Wifty::View::Page;
+use base qw(Jifty::View::Declare::Page);
+use Jifty::View::Declare::Helpers;
+
+sub render_body {
+    my ($self, $body_code) = @_;
+
+    my $logo = Jifty->config->app('Logo');
+    return $self->SUPER::render_body( $body_code ) unless $logo;
+
+    return $self->SUPER::render_body( sub {
+        div { attr { id is "logo" } img { src is $logo, alt is '' } };
+        $body_code->();
+    });
+}
+
+1;
+

commit cf6f22515b5fd043088b66611450a6b35648ba51
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Dec 1 05:11:12 2008 +0000

    delete all implemented TD templates from View-not-ready-yet

diff --git a/lib/Wifty/View-not-ready-yet.pm b/lib/Wifty/View-not-ready-yet.pm
index 58fc261..04a4022 100644
--- a/lib/Wifty/View-not-ready-yet.pm
+++ b/lib/Wifty/View-not-ready-yet.pm
@@ -19,337 +19,6 @@ use base qw/Jifty::View::Declare::Templates/;
 use Template::Declare::Tags;
 use Jifty::View::Declare::Templates;
 
-private template page_list => sub {
-    # actually creates: sub _jifty_ui_template_page_list
-    #
-    my ( $pages, $id ) = get(qw(pages id));
-    dl {{ id is $id, class is "pagelist" }
-        while ( my $page = $pages->next ) {
-            dt {
-                hyperlink(
-                    label => $page->name,
-                    url   => '/view/' . $page->name
-                );
-            };
-            dd {
-                outs( $page->updated );
-                outs(
-                    ' - ('
-                        . (
-                          $page->updated_by->id
-                        ? $page->updated_by->name
-                        : _('Anonymous')
-                        )
-                        . ')'
-                );
-            };
-        }
-    };
-};
-
-private template nav => sub {
-    my $top  = Jifty->web->navigation;
-    $top->child( Home => url => "/", sort_order => 1 );
-    $top->child(
-        Recent  =>
-            url => "/recent",
-        label      => "Recent Changes",
-        sort_order => 2
-    );
-    $top->child(
-        Search  =>
-            url => "/search",
-        label      => "Search",
-        sort_order => 3
-    );
-
-    if ( Jifty->config->framework('AdminMode') ) {
-        $top->child(
-            Administration =>
-                url        => "/__jifty/admin/",
-            sort_order => 998
-        );
-        $top->child(
-            OnlineDocs =>
-                url    => "/__jifty/online_docs/",
-            label      => 'Online docs',
-            sort_order => 999
-        );
-    }
-
-};
-
-private template page_nav => sub {
-    my %args = (page => 'HomePage', rev => undef, @_);
-    my $page = $args{'page'};
-    my $rev = $args{'rev'};
-    
-    $page ||= 'HomePage';
-    my $subpath = $page . ( $rev ? "/$rev" : '' );
-    my $top     = Jifty->web->page_navigation;
-
-    my $page_obj = Wifty::Model::Page->new();
-    $page_obj->load_by_cols( name => $page );
-
-    $top->child( View => url => '/view/' . $subpath );
-    $top->child( Edit => url => '/edit/' . $subpath );
-    $top->child( History => url => '/history/' . $page );
-    $top->child( Latest => url => '/view/' . $page ) if ($rev);
-
-};
-
-private template wrapper => sub {
-    # it's actually called with args.
-    my ($args, $coderef ) = (@_);
-    my $title    = $args->{title};
-    my $id       = $args->{id};
-    my $wikiname = Jifty->config->app('WikiName') || "Wifty";
-
-    show('nav');
-    show( 'header', title => $args->{'title'}, wikiname => $wikiname );
-
-    body {{ id is $args->{id} }
-        if ( Jifty->config->framework('AdminMode') ) {
-            div {{ class is 'warning admin_mode' }
-                _('Alert') . ":"
-                    . tangent(
-                    label => _('Administration mode is enabled'),
-                    url   => '/__jifty/admin/'
-                    )
-                    . ".";
-                }
-        }
-        div {{ id is 'logo' }
-            Jifty->config->app('Logo')
-                ? '<img src="' . Jifty->config->app('Logo') . '" alt="" />'
-                : '';
-        };
-        div {{ id is 'header' }
-            div {{ id is 'wikiheader' }
-                h1 {{ id is 'wikiname' }
-                    hyperlink( url => "/", label => _($wikiname) );
-                };
-                outs(Jifty->web->navigation->render_as_menu);
-                show('search_box');
-
-            };
-            div {{ id is 'pageheader' }
-                h1 {{ id is 'pagename' }
-                    _( $args->{title} );
-                };
-
-                outs (Jifty->web->page_navigation->render_as_menu);
-                }
-        };
-
-        show('salutation');
-
-        hr {{ class is 'clear' }};
-        div {{ id is 'content' }
-            Jifty->web->render_messages;
-            my $buf = '';
-            {
-            local $Template::Declare::Tags::BUFFER ='';
-            $coderef->();
-            $buf = $Template::Declare::Tags::BUFFER;
-            #warn "My buffer is $buf";
-            }
-            outs($buf);
-            hr {{ class is 'clear' }};
-
-            }
-        }
-
-};
-
-
-private template search_box => sub {
-    my $action = new_action( class => 'SearchPage' );
-    $action->sticky_on_success(1);
-    span {
-        form {
-            form_next_page( url => '/search' );
-                render_param( $action, 'contains', label => 'Search:' );
-            }
-        };
-};
-
-private template salutation => sub {
-    div {{ id is 'salutation' }
-
-        if (    Jifty->web->current_user->id and Jifty->web->current_user->user_object ) {
-            outs('Hiya, ');
-            span {{ class is 'user' } Jifty->web->current_user->user_object->name };
-            outs('(' . hyperlink( label => q{Logout}, url => '/logout' ) .')');
-        } else {
-            outs("You're not currently signed in.") .  tangent( label => q{Sign in}, url => '/login' ) . "."; }
-        }
-};
-
-
-private template diff => sub {
-    my %args = ( page => undef, from => undef, to => undef, @_);
-
-    my $to   =  $args{'to'} ||$args{page}->revisions->last;
-    my $from = $args{'from'}|| $to->previous || Wifty::Model::Revision->new;
-
-    my $before = $to->previous;
-    my $after  = $to->next;
-
-    use Text::Diff ();
-    my $diff = Text::Diff::diff(
-        \( $from->content ),
-        \( $to->content ),
-        { STYLE => 'Text::Diff::HTML' }
-    );
-
-    div {{ class is 'revision_nav' }
-        if ($before) {
-            span {{ class is "prev" }
-                hyperlink(
-                    url   => "/view/" . $args{page}->name . "/" . $before->id,
-                    label => "Previous revision"
-                );
-            };
-        }
-        outs('|') if ( $before and $after );
-
-        if ($after) {
-            span {{ class is "next" }
-                hyperlink(
-                    url   => "/view/" . $args{'page'}->name . "/" . $after->id,
-                    label => "Next revision"
-                );
-            };
-        }
-    };
-    pre {{ class is 'diff' } $diff };
-    hr {}
-
-};
-
-template create => sub {
-    my ( $action, $page ) = get(qw(action page));
-    show(
-        'wrapper',
-        {title => 'New page: ' . $page, id => 'create' }, 
-        sub {p{
-            form {
-                div {{ class is 'form_wrapper' }
-                    form_next_page( url => '/view/' . $page );
-                        render_param($action => 'name', render_as     => 'hidden', default_value => $page);
-                    div {{ class is 'inline' }
-                        render_param($action => 'content', rows => 30 );
-                    };
-                    div {{ class is 'inline' }
-                        form_submit( label => 'Create' );
-                    };
-                };
-            };
-            show('markup');
-        };
-        }
-    );
-};
-
-template edit => sub {
-    my ( $page, $revision, $viewer ) = get(qw(page revision viewer));
-    my $can_edit = $page->current_user_can('update');
-    show( 'page_nav', page => $page->name, rev => $revision->id );
-    show(
-        'wrapper',
-        {   title => 'Edit: ' . $page->name . ( $revision->id ? " as of " . $revision->created : '' ), id => "update" },
-        sub {
-            form {
-                div {{ class is 'form_wrapper' }
-                    div {{ class is 'inline' }
-                        unless ($can_edit) { p {{ style is "width: 70%" } q{You don't have permission to edit this page. Perhaps} . tangent( url   => '/login', label => 'logging in') . q{would help. In the mean time, though, you're welcome to view and} . q{copy the source of this page.}; } }
-                        form_next_page( url => '/view/' . $page->name );
-                        render_param($viewer => 'content');
-                        if ($can_edit) { div {{ class is 'line' } form_submit( label => 'Save' ); } }
-                    };
-                };
-                show('markup');
-                };
-            
-            }
-
-
-    );
-};
-
-template history => sub {
-    my ( $page, $revisions ) = get(qw(page revisions));
-    # XXX TODO, this isn't right
-    show( 'page_nav', page => $page->name );
-    show(
-        'wrapper',
-        { title => $revisions->count . " revisions of " . $page->name },
-        sub {
-            dl {{ id is 'history' }
-                while ( my $rev = $revisions->next ) {
-                    dt {
-                        hyperlink(
-                            label => $rev->created,
-                            url   => '/view/' . $page->name . '/' . $rev->id
-                        );
-                        if ( $rev->created_by->id ) {
-                            '(' . $rev->created_by->name . ')';
-                        } else {
-                            '(Anonymous)';
-                        }
-                    };
-                    dd { length( $rev->content ) . ' bytes' };
-                }
-                };
-        }
-    );
-
-};
-
-template no_such_page => sub {
-    my (  $page ) = get(qw(page));
-    show(
-        'wrapper',
-        { title => 'No such page: ' . $page },
-        sub {
-
-            p {
-                q{Unfortunately, you've tried to reach a page that doesn't exist }
-                    . q{yet, and you don't have permissions to create pages. If you }
-                    . tangent( url => '/login', label => 'login' )
-                    . q{, you'll be able to create new pages of your own.}
-
-                }
-
-        }
-    );
-};
-
-template pages => sub {
-    my ($pages ) = get(qw(pages));
-    show(
-        'wrapper',
-        { title => 'These are the pages on your wiki!' },
-        sub {
-            show( 'page_list', pages => $pages, id => 'allpages' );
-        }
-    );
-
-};
-
-template recent => sub {
-    my ( $pages ) = get(qw(pages));
-    show(
-        'wrapper',
-        { title => 'Updated this week' },
-        sub {
-            show( 'page_list', pages => $pages, id => 'recentupdates' );
-        }
-    );
-
-};
-
 template recent_atom => sub {
     my ( $pages) = get(qw(pages));
     use XML::Atom::SimpleFeed;
@@ -375,46 +44,6 @@ template recent_atom => sub {
     $feed->print;
 };
 
-template search => sub {
-    my ( $pages, $search ) = get(qw(pages search));
-    show( 'wrapper',
-        { title => 'Search' },
-        sub {
-            form {
-                div {{ id is "searchbox", class is 'inline' }
-                    render_param($search => 'contains', label => 'Find pages containing:' );
-                    form_submit( label => 'Search', submit => $search);
-                    };
-
-            };
-            if ($pages) {
-                show( 'page_list' => pages => $pages, id => 'searchresults' );
-            }
-        }
-    );
-};
-
-template view => sub {
-    my ( $page, $revision, $viewer ) = get(qw(page revision viewer));
-    show( 'page_nav', page => $page->name, rev => $revision->id );
-    show(
-        'wrapper',
-        {   title => $page->name
-                . ( $revision->id ? " as of " . $revision->created : '' )
-        },
-        sub {
-            if ( $revision->id ) {
-                show( 'diff', page => $page, to => $revision );
-            }
-
-            render_param($viewer => 'content', label => '', render_mode => 'read');
-            #$viewer->form_value( 'content', label => "" );
-
-        }
-    );
-
-};
-
 private template header => sub {
     my %args = ( title=> undef, wikiname => undef, @_);
     
@@ -437,98 +66,4 @@ private template header => sub {
         }
 };
 
-template markup => sub {
-    return undef unless ( Jifty->config->app('Formatter') eq 'Markdown' );
-
-    div {{ id is 'syntax' }
-        div {
-            a {{
-                href    is "#",
-                onclick is "Element.toggle('syntax_content');return(false);"
-            } b {'Wiki Syntax Help'}; }
-        };
-        div {{ id is 'syntax_content' }
-            h3   {'Phrase Emphasis'};
-            code {
-                b { '**bold**'; };
-                i {'_italic_'};
-            };
-
-            h3 {'Links'};
-
-            code {'Show me a [wiki page](WikiPage)'};
-            code {'An [example](http://url.com/ "Title")'};
-
-            h3 {'Headers'};
-
-            pre {
-                code {
-                    join( "\n",
-                        '# Header 1',
-                        '## Header 2',
-                        '###### Header 6' );
-                    }
-            };
-
-            h3 {'Lists'};
-
-            p {'Ordered, without paragraphs:'};
-
-            pre {
-                code {
-                    join( "\n", '1.  Foo', '2.  Bar' );
-                    }
-            };
-
-            p {' Unordered, with paragraphs:'};
-
-            pre {
-                code {
-                    join( "\n",
-                        '*   A list item.',
-                        'With multiple paragraphs.',
-                        '*   Bar' );
-                };
-
-                h3 {'Code Spans'};
-
-                p {
-                    code {'`<code>`'}
-                        . 'spans are delimited by backticks.';
-                };
-
-                h3 {'Preformatted Code Blocks'};
-
-                p {'Indent every line of a code block by at least 4 spaces.'};
-
-                pre {
-                    code {
-                        'This is a normal paragraph.' . "\n\n" . "\n"
-                            . '    This is a preformatted' . "\n"
-                            . '    code block.';
-                    };
-                };
-
-                h3 {'Horizontal Rules'};
-
-                p {
-                    'Three or more dashes: ' . code {'---'};
-                };
-
-                address {
-                    '(Thanks to <a href="http://daringfireball.net/projects/markdown/dingus">Daring Fireball</a>)';
-                    }
-                }
-        };
-        script {
-            qq{
-   // javascript flyout by Eric Wilhelm
-   // TODO use images for minimize/maximize button
-   // Is there a way to add a callback?
-   Element.toggle('syntax_content');
-   };
-        };
-    };
-};
-
 1;

commit af8a97feaf21879bd2393c8eb473b05ae15dc5f0
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Dec 1 05:11:56 2008 +0000

    delete all mason templates we are on TD

diff --git a/share/web/templates/_elements/diff b/share/web/templates/_elements/diff
deleted file mode 100644
index 0e874d7..0000000
--- a/share/web/templates/_elements/diff
+++ /dev/null
@@ -1,30 +0,0 @@
-<%args>
-$page
-$from =>undef
-$to => undef
-</%args>
-<%init>
-
-$to   ||= $page->revisions->last;
-$from ||= $to->previous || Wifty::Model::Revision->new;
-
-my $before = $to->previous;
-my $after  = $to->next;
-
-use Text::Diff ();
-my $diff = Text::Diff::diff(\($from->content), \($to->content), { STYLE => 'Text::Diff::HTML' });
-
-</%init>
-<div class="revision_nav">
-% if ($before) {
-<span class="prev"><% Jifty->web->link(url => "/view/".$page->name."/".$before->id, label => "Previous revision") %></span>
-% }
-% if ( $before and $after ) {
- | 
-% }
-% if ($after) {
-<span class="next"><% Jifty->web->link(url => "/view/".$page->name."/".$after->id, label => "Next revision") %></span>
-% }
-</div>
-<pre class="diff"><% $diff |n%></pre>
-<hr />
diff --git a/share/web/templates/_elements/header b/share/web/templates/_elements/header
deleted file mode 100644
index d89c29d..0000000
--- a/share/web/templates/_elements/header
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
-<head>
-  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
-  <meta name="robots" content="all" />
-  
-  <title><% _( $title ) %> - <% _( $wikiname ) %></title>
-  
-  <% Jifty->web->include_css %>
-  <% Jifty->web->include_javascript %> 
-</head>
-<%args>
-$title => ""
-$wikiname => ""
-</%args>
-<%init>
-$r->content_type('text/html; charset=utf-8');
-</%init>
diff --git a/share/web/templates/_elements/markup b/share/web/templates/_elements/markup
deleted file mode 100644
index 24e4c9c..0000000
--- a/share/web/templates/_elements/markup
+++ /dev/null
@@ -1,70 +0,0 @@
-<%init>
-return undef unless (Jifty->config->app('Formatter') eq 'Markdown');
-</%init>
-
-<div id="syntax">
-<div><a href="#" onclick="Element.toggle('syntax_content');return(false);"><b>Wiki Syntax Help</b></a>
-</div>
-<div id="syntax_content">
-
-<h3>Phrase Emphasis</h3>
-
-<code> <b>**bold**</b> <i>_italic_</i> </code>
-
-<h3>Links</h3>
-
-<code>Show me a [wiki page](WikiPage)</code>
-<code>An [example](http://url.com/ "Title")</code>
-
-<h3>Headers</h3>
-
-<pre><code># Header 1
-## Header 2
-###### Header 6
-</code></pre>
-
-<h3>Lists</h3>
-
-<p>Ordered, without paragraphs:</p>
-
-<pre><code>1.  Foo
-2.  Bar
-</code></pre>
-
-<p>Unordered, with paragraphs:</p>
-
-<pre><code>*   A list item.
-
-    With multiple paragraphs.
-
-*   Bar</code></pre>
-
-<h3>Code Spans</h3>
-
-<p><code>`<code>`</code> spans are 
-delimited by backticks.</p>
-
-<h3>Preformatted Code Blocks</h3>
-
-<p>Indent every line of a code block 
-by at least 4 spaces.</p>
-
-<pre><code>This is a normal paragraph.
-
-    This is a preformatted
-    code block.
-</code></pre>
-
-<h3>Horizontal Rules</h3>
-
-<p>Three or more dashes: <code>---</code></p>
-
-<address>(Thanks to <a href="http://daringfireball.net/projects/markdown/dingus">Daring Fireball</a>)</address>
-</div>
-</div> 
-<script>
-   // javascript flyout by Eric Wilhelm
-   // TODO use images for minimize/maximize button
-   // Is there a way to add a callback?
-   Element.toggle('syntax_content');
-</script>
diff --git a/share/web/templates/_elements/page_list b/share/web/templates/_elements/page_list
deleted file mode 100644
index f4bfd15..0000000
--- a/share/web/templates/_elements/page_list
+++ /dev/null
@@ -1,16 +0,0 @@
-<%args>
-$pages
-$id
-</%args>
-<dl id="<%$id%>" class="pagelist">
-% while (my $page = $pages->next) {
-<dt><% Jifty->web->link( label => $page->name, url => '/view/'.$page->name)%></dt>
-<dd><%$page->updated%>
-% if($page->updated_by->id) {
-  (<% $page->updated_by->name %>)
-% } else {
-  (Anonymous)
-% }
-</dd>
-% }
-</dl>
diff --git a/share/web/templates/_elements/salutation b/share/web/templates/_elements/salutation
deleted file mode 100644
index bfd36c6..0000000
--- a/share/web/templates/_elements/salutation
+++ /dev/null
@@ -1,9 +0,0 @@
-<div id="salutation">
-% if ( Jifty->web->current_user->id and Jifty->web->current_user->user_object ) {
-    Hiya, <span class="user"><% Jifty->web->current_user->user_object->name %></span>.
-    (<% Jifty->web->link( label => q{Logout}, url => '/logout' )%>)
-% }  else {
-    You're not currently signed in.
-    <% Jifty->web->tangent( label => q{Sign in}, url => '/login' ) %>.
-% }
-</div>
diff --git a/share/web/templates/_elements/search_box b/share/web/templates/_elements/search_box
deleted file mode 100644
index c487d97..0000000
--- a/share/web/templates/_elements/search_box
+++ /dev/null
@@ -1,10 +0,0 @@
-<%init>
-my $action =  Jifty->web->new_action(class => 'SearchPage', moniker => 'search');
-$action->sticky_on_success(1);
-</%init>
-<span>
-<% Jifty->web->form->start %>
-<% Jifty->web->form->next_page(url => '/search') %>
-<% $action->form_field('contains', label => 'Search:') %>
-<% Jifty->web->form->end %>
-</span>
diff --git a/share/web/templates/_elements/wrapper b/share/web/templates/_elements/wrapper
deleted file mode 100644
index f06fbe2..0000000
--- a/share/web/templates/_elements/wrapper
+++ /dev/null
@@ -1,52 +0,0 @@
-<& /_elements/header, title => $title, wikiname => $wikiname &>
-% Jifty->handler->stash->{'in_body'} = 1;
-<body<% $id && qq[ id="$id"]|n%>>
-% if (Jifty->config->framework('AdminMode') ) {
-  <div class="warning admin_mode">
-  <%_('Alert')%>: <% Jifty->web->tangent( label => _('Administration mode is enabled'),
-                                          url => '/__jifty/admin/') %>.
-  </div>
-% }
-    <div id="logo">
-   <% Jifty->config->app('Logo') ? '<img src="'.Jifty->config->app('Logo').'" alt="" />' : '' |n %>
-   </div>
-  <div id="header">
-    <div id="wikiheader">
-      <h1 id="wikiname">
-        <% Jifty->web->link( url => "/", label => _($wikiname) ) %>
-      </h1>
-
-      <% Jifty->web->navigation->render_as_menu %>
-      <& /_elements/search_box &>
-    </div>
-
-    <div id="pageheader">
-      <h1 id="pagename"><% _($title) %></h1>
-
-      <% Jifty->web->page_navigation->render_as_menu %>
-    </div>
-  </div>
-  
-  <& /_elements/salutation &>
-  
-  <hr class="clear" />
-  
-  <div id="content">
-    <% Jifty->web->render_messages %>
-    <% $m->content |n%>
-    <& /_elements/keybindings &>
-    <hr class="clear" />
-  </div>
-  
-  <div id="jifty-wait-message" style="display: none"><%_('Loading...')%></div>
-% Jifty::Mason::Halo->render_component_tree() if Jifty->config->framework('DevelMode');
-</body>
-</html>
-% Jifty->handler->stash->{'in_body'} = 0;
-<%args>
-$title => ""
-$id => ''
-</%args>
-<%init>
-my $wikiname = Jifty->config->app('WikiName') || "Wifty";
-</%init>
diff --git a/share/web/templates/create b/share/web/templates/create
deleted file mode 100644
index 002545b..0000000
--- a/share/web/templates/create
+++ /dev/null
@@ -1,19 +0,0 @@
-<&|/_elements/wrapper, title => 'New page: '. $page, id => 'create'&>
-<% Jifty->web->form->start %>
-<div class="form_wrapper">
-<% Jifty->web->form->next_page( url => '/view/'.$page) %>
-<% $action->form_field('name', render_as => 'hidden', default_value => $page) %>
-<div class="inline">
-<% $action->form_field('content', rows => 30)%>
-</div>
-<div class="line">
-<% Jifty->web->form->submit( label => 'Create' )%>
-</div>
-</div>
-<% Jifty->web->form->end %>
-<& /_elements/markup &>
-</&>
-<%args>
-$action => undef
-$page => undef
-</%args>
diff --git a/share/web/templates/edit b/share/web/templates/edit
deleted file mode 100644
index 37d4512..0000000
--- a/share/web/templates/edit
+++ /dev/null
@@ -1,30 +0,0 @@
-<%args>
-$page
-$revision
-$viewer 
-</%args>
-<%init>
-my $can_edit = $page->current_user_can('update');
-</%init>
-<&|/_elements/wrapper, title => 'Edit: '.$page->name . ($revision->id ? " as of ".$revision->created : ''), id => "update"  &>
-<% Jifty->web->form->start %>
-<div class="form_wrapper">
-<div class="inline">
-% unless($can_edit) {
-  <p style="width: 70%"> You don't have permission to edit this page. Perhaps
-  <% Jifty->web->tangent(url => '/login', label => 'logging in') %>
-  would help. In the mean time, though, you're welcome to view and
-  copy the source of this page. </p>
-% }
-<% Jifty->web->form->next_page( url => '/view/'.$page->name) %>
-<% $viewer->form_field('content')%>
-</div>
-% if($can_edit) {
-<div class="line">
-<% Jifty->web->form->submit( label => 'Save') %>
-</div>
-% }
-</div>
-<% Jifty->web->form->end %>
-<& /_elements/markup &>
-</&>
diff --git a/share/web/templates/history b/share/web/templates/history
deleted file mode 100644
index 5fa39a8..0000000
--- a/share/web/templates/history
+++ /dev/null
@@ -1,20 +0,0 @@
-<%args>
-$page
-$revisions
-</%args>
-<&|/_elements/wrapper, title => $revisions->count ." revisions of " .$page->name &>
-<dl id="history">
-% while (my $rev = $revisions->next) {
-<dt><% Jifty->web->link( label => $rev->created, 
-                          url => '/view/'.$page->name.'/'.$rev->id
-                        ) %>
-% if($rev->created_by->id) {
-  (<% $rev->created_by->name %>)
-% } else {
-  (Anonymous)
-% }
-</dt>
-<dd><%length($rev->content)%> bytes</dd>
-% }
-</dl>
-</&>
diff --git a/share/web/templates/no_such_page b/share/web/templates/no_such_page
deleted file mode 100644
index fbdc076..0000000
--- a/share/web/templates/no_such_page
+++ /dev/null
@@ -1,11 +0,0 @@
-<&|/_elements/wrapper, title => 'No such page: '. $page&>
-
-  <p>Unfortunately, you've tried to reach a page that doesn't exist
-    yet, and you don't have permissions to create pages. If you
-    <% Jifty->web->tangent(url => '/login', label => 'login') %>,
-    you'll be able to create new pages of your own.</p>
-    
-</&>
-<%args>
-$page => undef
-</%args>
diff --git a/share/web/templates/pages b/share/web/templates/pages
deleted file mode 100644
index 4495c8e..0000000
--- a/share/web/templates/pages
+++ /dev/null
@@ -1,6 +0,0 @@
-<%args>
-$pages
-</%args>
-<&|/_elements/wrapper, title => 'These are the pages on your wiki!' &>
-<& /_elements/page_list, pages => $pages, id => 'allpages' &>
-</&>
diff --git a/share/web/templates/recent b/share/web/templates/recent
deleted file mode 100644
index 071ac48..0000000
--- a/share/web/templates/recent
+++ /dev/null
@@ -1,6 +0,0 @@
-<%args>
-$pages
-</%args>
-<&|/_elements/wrapper, title => 'Updated this week' &>
-<& /_elements/page_list, pages => $pages, id => 'recentupdates' &>
-</&>
diff --git a/share/web/templates/search b/share/web/templates/search
deleted file mode 100644
index 6d77876..0000000
--- a/share/web/templates/search
+++ /dev/null
@@ -1,19 +0,0 @@
-<%args>
-$pages
-$search
-</%args>
-<%init>
-warn $search;
-</%init>
-<&|/_elements/wrapper, title => 'Search' &>
-<% Jifty->web->form->start %>  
-  <div id="searchbox" class="inline">
-    <% $search->form_field('contains', label => 'Find pages containing:') %>
-    <% $search->button(label => 'Search') %>
-  </div>
-  
-<% Jifty->web->form->end %>  
-% if($pages) {  
-<& /_elements/page_list, pages => $pages, id => 'searchresults' &>
-% }
-</&>
diff --git a/share/web/templates/view b/share/web/templates/view
deleted file mode 100644
index cf08c0f..0000000
--- a/share/web/templates/view
+++ /dev/null
@@ -1,14 +0,0 @@
-<%args>
-$page
-$revision
-$viewer
-</%args>
-<&|/_elements/wrapper, title => $page->name . ($revision->id ? " as of ".$revision->created : '') &>
-
-% if ($revision->id) {
-<& /_elements/diff, page => $page, to => $revision &>
-% }
-
-<% $viewer->form_value('content', label => "") %>
-
-</&>

commit 45763595867e66c91116bb2e9eb65dde568dc737
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Dec 1 05:12:32 2008 +0000

    fix css a little

diff --git a/share/web/static/css/app-base.css b/share/web/static/css/app-base.css
index 94becf6..f4cdd23 100644
--- a/share/web/static/css/app-base.css
+++ b/share/web/static/css/app-base.css
@@ -102,7 +102,6 @@ h4 { font-size: 1.1em; }
     font-size: 0.9em;
     width: 25%;
     position: absolute;
-    top: 1em;
     right: 1em;
 }
 

commit d52aed342b1663cc108450b329e2dd4786ef41ef
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Dec 1 05:18:07 2008 +0000

    adjust default config a little

diff --git a/etc/config.yml b/etc/config.yml
index 470e5a9..2ca00cd 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -29,9 +29,14 @@ framework:
     StaticRoot: share/web/static
     TemplateRoot: share/web/templates
 application:
-#  RequireAuth: 1
+  # name of this wiki
   WikiName: A Wiki
-  Formatter: Markdown
+
+  # can anonymous users change wiki?
+  RequireAuth: 1
+
   # The formatter options are "Markdown" and "Kwiki"
-  # Logo: http://svk.bestpractical.com/svk-logo.png
+  Formatter: Markdown
+
   # The logo points to the url to a logo image
+  # Logo: http://www.bestpractical.com/images/svk-logo.png

commit 367731ac3db9b407233cb98b48ff7836866e66c0
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 6 14:00:12 2008 +0000

    split diff temlate into:
    ** diff - just render a diff from rev x to y
    ** diff/with_nav - render diff with prev/next navigation buttons
    ** helpers/diff - helper for regions with toggle visibility link

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index e0beb53..6c13c36 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -11,7 +11,7 @@ template 'view' => page {
         ? _('%1 as of %2', $page->name, $revision->created)
         : $page->name;
     { title is $title }
-    show( 'diff', page => $page, to => $revision ) if $rev;
+    show( 'diff/with_nav', page => $page, to => $revision ) if $rev;
     render_param($viewer => 'content', label => '', render_mode => 'read');
 };
 
@@ -248,21 +248,55 @@ private template page_list => sub {
     };
 };
 
-private template diff => sub {
-    my ($page, $from, $to) = get(qw(page from to));
-
-    $to ||= $page->revisions->last;
-    $from ||= $to->previous || Wifty::Model::Revision->new;
+template 'helpers/diff' => sub {
+    my ($from, $to, $show) = get(qw(from to show));
+    hyperlink
+        label => $show? _('hide diff') : _('show diff'),
+        onclick => {
+            refresh_self => 1,
+            args => {
+                show => !$show,
+                from => $from,
+                to => $to
+            },
+        },
+    ;
+    if ( $show ) {
+        # XXX: check why show(x, key => $value, key => $value)
+        # doesn't work 
+        set(
+            to => Wifty::Model::Revision->load_by_cols(id => $to),
+            from => Wifty::Model::Revision->load_by_cols(id => $from),
+        );
+        show('/diff');
+    }
+};
 
-    my $before = $to->previous;
-    my $after  = $to->next;
+private template 'diff' => sub {
+    my ($from, $to) = get(qw(from to));
+    if ( $to && !$from ) {
+        $from = $to->previous;
+    }
+    elsif ( !$to && $from ) {
+        $to = $from->next;
+    }
 
     use Text::Diff ();
     my $diff = Text::Diff::diff(
-        \( $from->content ),
-        \( $to->content ),
+        \( $from? $from->content : '' ),
+        \( $to ? $to->content : '' ),
         { STYLE => 'Text::Diff::HTML' }
     );
+    pre {{ class is 'diff' } outs_raw($diff) };
+};
+
+private template 'diff/with_nav' => sub {
+    my ($page, $from, $to) = get(qw(page from to));
+
+    $to ||= $page->revisions->last;
+
+    my $before = $to->previous;
+    my $after  = $to->next;
 
     div {{ class is 'revision_nav' }
         if ($before) {
@@ -283,7 +317,8 @@ private template diff => sub {
             };
         }
     };
-    pre {{ class is 'diff' } outs_raw($diff) };
+    set(to => $to);
+    show('diff');
     hr {}
 };
 

commit 964b9375d4543de01f9d0b35aae07b52ad7b104c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 6 14:01:07 2008 +0000

    fetch revisions once

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index 6c13c36..ce3cd57 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -77,6 +77,7 @@ template no_such_page => page {
 
 template history => page {
     my ( $page, $revisions ) = get(qw(page revisions));
+    $revisions->do_search; # avoid count+fetch
     { title is $revisions->count . " revisions of " . $page->name }
 
     dl { { id is 'history' }

commit 969ff34cc021001885af0172847862b375605acf
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 6 14:04:36 2008 +0000

    string inside if block don't work without outs
    * dl for history is not suitable - use ul
    * add diff region
    * needs CSS

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index ce3cd57..c2a501a 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -80,21 +80,24 @@ template history => page {
     $revisions->do_search; # avoid count+fetch
     { title is $revisions->count . " revisions of " . $page->name }
 
-    dl { { id is 'history' }
-        while ( my $rev = $revisions->next ) {
-            dt {
-                hyperlink(
-                    label => $rev->created,
-                    url   => '/view/' . $page->name . '/' . $rev->id
-                );
-                if ( $rev->created_by->id ) {
-                    '(' . $rev->created_by->name . ')';
-                } else {
-                    '(Anonymous)';
-                }
-            };
-            dd { length( $rev->content ) . ' bytes' };
-        }
+    ul { { id is 'history' }
+        while ( my $rev = $revisions->next ) { li {
+            hyperlink(
+                label => $rev->created,
+                url   => '/view/' . $page->name . '/' . $rev->id
+            );
+            if ( $rev->created_by->id ) {
+                outs(' ', '(' . $rev->created_by->name . ')');
+            } else {
+                outs(' ', _('(Anonymous)'));
+            }
+            outs( ' ', _('%1 bytes', length $rev->content ) );
+            render_region(
+                'revision-'. $rev->id .'-diff',
+                path => '/helpers/diff',
+                defaults => { page => $page->id, to => $rev->id },
+            )
+        } }
     };
 };
 

commit 38ad981e65b619da0568ae95d2b170e1fb259f44
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 6 14:15:24 2008 +0000

    allow to read users' data

diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index bc483eb..9d514b1 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -10,4 +10,15 @@ use Jifty::Plugin::User::Mixin::Model::User;
 # import columns: password, auth_token
 use Jifty::Plugin::Authentication::Password::Mixin::Model::User;
 
+sub current_user_can {
+    my $self = shift;
+    my $type = shift;
+
+    if ( $type eq 'read' ) {
+        return 1;
+    }
+
+    return $self->SUPER::current_user_can($type, @_);
+}
+
 1;

commit 41cc3fa398bdf2330df2aec87d759278721f1a2f
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 6 15:50:09 2008 +0000

    typo

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index c2a501a..1dbd8ec 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -322,7 +322,7 @@ private template 'diff/with_nav' => sub {
         }
     };
     set(to => $to);
-    show('diff');
+    show('/diff');
     hr {}
 };
 

commit d84528fccfbb8796c2792d1aa7caa1f461a38140
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 6 16:17:17 2008 +0000

    friendly_name method

diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index 9d514b1..010ae66 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -10,6 +10,12 @@ use Jifty::Plugin::User::Mixin::Model::User;
 # import columns: password, auth_token
 use Jifty::Plugin::Authentication::Password::Mixin::Model::User;
 
+sub friendly_name {
+    my $self = shift;
+    return _('Anonymous') unless $self->id;
+    return $self->name;
+}
+
 sub current_user_can {
     my $self = shift;
     my $type = shift;
diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index 1dbd8ec..35ad043 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -86,11 +86,7 @@ template history => page {
                 label => $rev->created,
                 url   => '/view/' . $page->name . '/' . $rev->id
             );
-            if ( $rev->created_by->id ) {
-                outs(' ', '(' . $rev->created_by->name . ')');
-            } else {
-                outs(' ', _('(Anonymous)'));
-            }
+            outs( ' (' . $rev->created_by->friendly_name . ')' );
             outs( ' ', _('%1 bytes', length $rev->content ) );
             render_region(
                 'revision-'. $rev->id .'-diff',
@@ -238,15 +234,7 @@ private template page_list => sub {
             };
             dd {
                 outs( $page->updated );
-                outs(
-                    ' - ('
-                        . (
-                          $page->updated_by->id
-                        ? $page->updated_by->name
-                        : _('Anonymous')
-                        )
-                        . ')'
-                );
+                outs( ' - ('. $page->updated_by->friendly_name .')' );
             };
         }
     };

commit 7f09136bb3d3967dab8143f22f0acd4f80a23ab8
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 6 16:17:51 2008 +0000

    fine tune current_user_can in the User model

diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index 010ae66..0115d97 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -19,12 +19,15 @@ sub friendly_name {
 sub current_user_can {
     my $self = shift;
     my $type = shift;
+    my $column = shift;
 
     if ( $type eq 'read' ) {
-        return 1;
+        return 1 if $column eq 'name';
+        my $cu = $self->current_user;
+        return 1 if $self->id && ($cu->id||0) == $self->id;
     }
 
-    return $self->SUPER::current_user_can($type, @_);
+    return $self->SUPER::current_user_can($type, $column, @_);
 }
 
 1;

commit 34ed26d9a5c6b30c18da0f3d5f5551a38de1dc15
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Dec 7 03:08:36 2008 +0000

    column passed as named argument into rights checker

diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index 0115d97..fd1e28f 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -19,15 +19,15 @@ sub friendly_name {
 sub current_user_can {
     my $self = shift;
     my $type = shift;
-    my $column = shift;
+    my %args = @_;
 
     if ( $type eq 'read' ) {
-        return 1 if $column eq 'name';
+        return 1 if $args{'column'} eq 'name';
         my $cu = $self->current_user;
         return 1 if $self->id && ($cu->id||0) == $self->id;
     }
 
-    return $self->SUPER::current_user_can($type, $column, @_);
+    return $self->SUPER::current_user_can($type, %args);
 }
 
 1;

commit 479cadd8201860e24e5c905714807b796afa7acd
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Dec 7 03:09:31 2008 +0000

    tweak css a little, history is ul now, inline region

diff --git a/share/web/static/css/app.css b/share/web/static/css/app.css
index 290e344..9f95109 100644
--- a/share/web/static/css/app.css
+++ b/share/web/static/css/app.css
@@ -51,14 +51,24 @@ form .submit_button input {
 * html .pagelist dt,
 * html .pagelist dd { position: relative; }
 
-#history dd {
+#history {
+    margin-left: 0em;
+    padding-left: 1em;
+    list-style-type: none;
+}
+
+#history li {
     font-size: 0.95em;
-    color: #444;
-    margin-left: 2em;
+    margin-left: 0em;
     padding-left: 0;
     margin-bottom: 0.5em;
 }
 
+#history div.jifty-region {
+    display: inline;
+    margin-left: 0.5em;
+}
+
 hr {
     border: none;
     border-top: 1px solid #777;

commit 653e80cf9acdff9e3d69200f1a0fa8282fdc49cc
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Dec 7 06:29:39 2008 +0000

    add revision method in the Page model

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 5064538..14f7c8d 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -104,6 +104,18 @@ sub _set {
     return ( $val, $msg );
 }
 
+sub revision {
+    my $self = shift;
+    my $rev = shift;
+    return undef unless $self->id;
+    return undef unless $rev;
+
+    my $res = new Wifty::Model::Revision;
+    $res->load_by_cols( page => $self->id, id => $rev );
+    return undef unless $res->id;
+    return $res;
+}
+
 
 =head2 current_user_can ACTION
 

commit f7f851aafb676582fcf6e2931b792ecaa44c23ff
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Dec 7 06:31:56 2008 +0000

    tidy revision model

diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index 88fd8eb..85ea17a 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -9,12 +9,20 @@ use base qw/Wifty::Record/;
 use Jifty::DBI::Schema;
 
 use Jifty::Record schema {
-column page  => refers_to Wifty::Model::Page;
-
-column content => type is 'text', render_as 'Wifty::Form::Field::WikiPage';
-
-column created => type is 'timestamp';
-column created_by => refers_to Wifty::Model::User, since '0.0.20';
+    column page =>
+        refers_to Wifty::Model::Page
+    ;
+    column content =>
+        type is 'text',
+        render_as 'Wifty::Form::Field::WikiPage'
+    ;
+    column created =>
+        type is 'timestamp'
+    ;
+    column created_by =>
+        refers_to Wifty::Model::User,
+        since '0.0.20'
+    ;
 };
 
 
@@ -77,7 +85,7 @@ sub next {
         quote_value    => 0,
         case_sensitive => 1
     );
-    $revisions->order_by( { column => 'id' } );
+    $revisions->order_by( { column => 'id', order => 'asc' } );
     $revisions->rows_per_page(1);
     return $revisions->first;
 }

commit e9c49491a59d77ac75f69f95ba1c86eced6df9de
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Dec 7 06:33:04 2008 +0000

    don't use diff with navigation
    * fix diff shower

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index 35ad043..b217a27 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -6,12 +6,10 @@ use Jifty::View::Declare -base;
 
 template 'view' => page {
     my ( $page, $revision, $viewer ) = get(qw(page revision viewer));
-    my $rev = $revision->id;
-    my $title = $rev
+    my $title = $revision->id
         ? _('%1 as of %2', $page->name, $revision->created)
         : $page->name;
     { title is $title }
-    show( 'diff/with_nav', page => $page, to => $revision ) if $rev;
     render_param($viewer => 'content', label => '', render_mode => 'read');
 };
 
@@ -266,10 +264,10 @@ template 'helpers/diff' => sub {
 
 private template 'diff' => sub {
     my ($from, $to) = get(qw(from to));
-    if ( $to && !$from ) {
+    if ( $to && !($from && $from->id) ) {
         $from = $to->previous;
     }
-    elsif ( !$to && $from ) {
+    elsif ( !($to && $to->id) && $from ) {
         $to = $from->next;
     }
 

commit 6986699ce72ca26a8a44db54fa9b020d16d36d93
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Dec 7 06:34:10 2008 +0000

    add history navigation into page nav

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index fa9e563..c6729fc 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -33,25 +33,23 @@ on '/create/*', run {
 
 # View or edit a page
 on qr{^/(view|edit)/(.*)}, run {
-    my ( $name, $rev );
     my $page_name = $1;
-    if ( $2 =~ qr{^(.*?)(?:/(\d+))?$} ) {
-        $name = $1;
-        $rev  = $2;
-    }
+    my ( $name, $rev ) = ($2 =~ qr{^(.*?)(?:/(\d+))?$});
+
     my $page = Wifty::Model::Page->new();
     $page->load_by_cols( name => $name );
-    Jifty->web->redirect( '/create/' . $name ) unless ( $page->id );
+    Jifty->web->redirect( '/create/' . $name )
+        unless $page->id;
+
+    $rev = $page->revision($rev);
 
-    setup_page_nav($name, $rev);
+    setup_page_nav($page_name, $page, $rev);
 
-    my $revision = Wifty::Model::Revision->new();
-    $revision->load_by_cols( page => $page->id, id => $rev ) if ($rev);
     set page => $page;
-    set revision => $revision;
+    set revision => $rev || new Wifty::Model::Revision;
     my $viewer = Jifty->web->new_action( class => 'UpdatePage', record => $page );
-    if($rev) {
-        $viewer->argument_value(content => $revision->content);
+    if ( $rev ) {
+        $viewer->argument_value(content => $rev->content);
     }
     set viewer => $viewer;
     show("/$page_name");
@@ -64,7 +62,7 @@ on 'history/*', run {
     $page->load_by_cols( name => $name );
     redirect( '/create/' . $name ) unless ( $page->id );
 
-    setup_page_nav($name);
+    setup_page_nav('view', $page);
 
     my $revisions = $page->revisions;
     $revisions->order_by( column => 'id', order => 'desc');
@@ -108,14 +106,23 @@ on 'recent*', run {
 };
 
 sub setup_page_nav {
-    my ($page, $rev) = @_;
+    my ($prefix, $page, $rev) = @_;
 
-    my $subpath =  $page . ($rev ? "/$rev" : '');
+    my $name = $page->name;
+
+    my $subpath = $name;
+    $subpath .= '/'. $rev->id if $rev;
     my $top = Jifty->web->page_navigation;
-    $top->child( View => url => '/view/'.$subpath);
-    $top->child( Edit => url => '/edit/'.$subpath);
-    $top->child( History => url => '/history/'.$page);
-    $top->child( Latest => url => '/view/'.$page) if $rev;
+    $top->child( View => url => '/view/'. $subpath);
+    $top->child( Edit => url => '/edit/'. $subpath);
+    if ( my $prev = ($rev? $rev : $page->revisions->last)->previous ) {
+        $top->child( Older => url => join '/', '', $prefix, $name, $prev->id );
+    }
+    $top->child( History => url => '/history/'. $name);
+    if ( $rev and my $next = $rev->next ) {
+        $top->child( Newer => url => join '/',  '', $prefix, $name, $next->id );
+        $top->child( Latest => url => join '/', '', $prefix, $name );
+    }
 }
 
 1;

commit f99d814b7957c174f37bf87cd460d1824d284706
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Dec 7 06:43:09 2008 +0000

    localize nav labels

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index c6729fc..0de9625 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -8,9 +8,9 @@ under '/', run {
 
 before '*', run {
     my $top = Jifty->web->navigation;
-    $top->child( Home   => url => "/", sort_order => 1 );
-    $top->child( Recent => url => "/recent", label => "Recent Changes", sort_order => 2 );
-    $top->child( Search => url => "/search", label => "Search", sort_order => 3 );
+    $top->child( Home   => url => "/", label => _("Home") );
+    $top->child( Recent => url => "/recent", label => _("Recent Changes") );
+    $top->child( Search => url => "/search", label => _("Search") );
 };
 
 # Default page
@@ -113,15 +113,27 @@ sub setup_page_nav {
     my $subpath = $name;
     $subpath .= '/'. $rev->id if $rev;
     my $top = Jifty->web->page_navigation;
-    $top->child( View => url => '/view/'. $subpath);
-    $top->child( Edit => url => '/edit/'. $subpath);
+    $top->child( View => url => '/view/'. $subpath, label => _('View') );
+    $top->child( Edit => url => '/edit/'. $subpath, label => _('Edit') );
     if ( my $prev = ($rev? $rev : $page->revisions->last)->previous ) {
-        $top->child( Older => url => join '/', '', $prefix, $name, $prev->id );
+        $top->child(
+            Older => label => _('Previous Version'),
+            url => join '/', '', $prefix, $name, $prev->id
+        );
     }
-    $top->child( History => url => '/history/'. $name);
+    $top->child(
+        History => label => _('History'),
+        url => '/history/'. $name
+    );
     if ( $rev and my $next = $rev->next ) {
-        $top->child( Newer => url => join '/',  '', $prefix, $name, $next->id );
-        $top->child( Latest => url => join '/', '', $prefix, $name );
+        $top->child(
+            Newer => label => _('Next Version'),
+            url => join '/',  '', $prefix, $name, $next->id
+        );
+        $top->child(
+            Latest => label => _('Latest'),
+            url => join '/', '', $prefix, $name
+        );
     }
 }
 

commit 7104f615413aa41347005eb9ce37955e129a8c8b
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Dec 7 07:07:44 2008 +0000

    add ru.po

diff --git a/share/po/ru.po b/share/po/ru.po
new file mode 100644
index 0000000..f0c04e7
--- /dev/null
+++ b/share/po/ru.po
@@ -0,0 +1,116 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Ruslan Zakirov <ruz at bestpractical.com>\n"
+"Language-Team: LANGUAGE <LL at li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: lib/Wifty/View-not-ready-yet.pm:59
+msgid " name is 'robots', content is 'all' "
+msgstr ""
+
+#: lib/Wifty/View-not-ready-yet.pm:58
+msgid ""
+" xmlns is \"http://www.w3.org/1999/xhtml\", xml__lang is \"en\" }\n"
+"        head {\n"
+"            meta {{ http_equiv is \"content-type\", content is \"text/html; charset=utf-8\" "
+msgstr ""
+
+#: lib/Wifty/View.pm:10
+#. ($page->name, $revision->created)
+msgid "%1 as of %2"
+msgstr "%1 от %2"
+
+#: lib/Wifty/View.pm:88
+#. (length $rev->content)
+msgid "%1 bytes"
+msgstr "%1 байт"
+
+#: lib/Wifty/Model/User.pm:15
+msgid "Anonymous"
+msgstr "Аноним"
+
+#: lib/Wifty/View.pm:21
+msgid "Edit page %1"
+msgstr "Изменить страницу %1"
+
+#: lib/Wifty/View.pm:20
+#. ($page->name, $revision->created)
+msgid "Edit page %1 as of %2"
+msgstr "Изменить страницу %1 от %2"
+
+#: lib/Wifty/View.pm:116
+msgid "Find pages containing:"
+msgstr "Найти страницы содержащие:"
+
+#: lib/Wifty/Dispatcher.pm:125
+msgid "History"
+msgstr "История"
+
+#: lib/Wifty/Dispatcher.pm:134
+msgid "Latest"
+msgstr "Последняя"
+
+#: lib/Wifty/View.pm:46
+#. ($page)
+msgid "New page '%1'"
+msgstr "Новая страница '%1'"
+
+#: lib/Wifty/Dispatcher.pm:130
+msgid "Next Version"
+msgstr "Следующая Версия"
+
+#: lib/Wifty/View.pm:305
+msgid "Next revision"
+msgstr ""
+
+#: lib/Wifty/View.pm:66
+#. ($page)
+msgid "No '%1' page"
+msgstr "Нет страницы '%1'"
+
+#: lib/Wifty/Dispatcher.pm:120
+msgid "Previous Version"
+msgstr "Предыдущая версия"
+
+#: lib/Wifty/View.pm:296
+msgid "Previous revision"
+msgstr ""
+
+#: lib/Wifty/Dispatcher.pm:12
+msgid "Recent Changes"
+msgstr "Последние Изменения"
+
+#: lib/Wifty/View.pm:107
+msgid "These are the pages on your wiki!"
+msgstr "Все страницы на этой wiki!"
+
+#: lib/Wifty/View.pm:100
+msgid "Updated this week"
+msgstr "Обновлены на этой неделе"
+
+#: lib/Wifty/Dispatcher.pm:116
+msgid "View"
+msgstr "Просмотреть"
+
+#: lib/Wifty/View.pm:159
+msgid "Wiki Syntax Help"
+msgstr "Справка по wiki-синтаксису"
+
+#: lib/Wifty/View.pm:244
+msgid "hide diff"
+msgstr "скрыть изменения"
+
+#: lib/Wifty/View.pm:244
+msgid "show diff"
+msgstr "показать изменения"
diff --git a/share/po/wifty.pot b/share/po/wifty.pot
new file mode 100644
index 0000000..639125f
--- /dev/null
+++ b/share/po/wifty.pot
@@ -0,0 +1,116 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL at ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
+"Language-Team: LANGUAGE <LL at li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: lib/Wifty/View-not-ready-yet.pm:59
+msgid " name is 'robots', content is 'all' "
+msgstr ""
+
+#: lib/Wifty/View-not-ready-yet.pm:58
+msgid ""
+" xmlns is \"http://www.w3.org/1999/xhtml\", xml__lang is \"en\" }\n"
+"        head {\n"
+"            meta {{ http_equiv is \"content-type\", content is \"text/html; charset=utf-8\" "
+msgstr ""
+
+#: lib/Wifty/View.pm:10
+#. ($page->name, $revision->created)
+msgid "%1 as of %2"
+msgstr ""
+
+#: lib/Wifty/View.pm:88
+#. (length $rev->content)
+msgid "%1 bytes"
+msgstr ""
+
+#: lib/Wifty/Model/User.pm:15
+msgid "Anonymous"
+msgstr ""
+
+#: lib/Wifty/View.pm:21
+msgid "Edit page %1"
+msgstr ""
+
+#: lib/Wifty/View.pm:20
+#. ($page->name, $revision->created)
+msgid "Edit page %1 as of %2"
+msgstr ""
+
+#: lib/Wifty/View.pm:116
+msgid "Find pages containing:"
+msgstr ""
+
+#: lib/Wifty/Dispatcher.pm:125
+msgid "History"
+msgstr ""
+
+#: lib/Wifty/Dispatcher.pm:134
+msgid "Latest"
+msgstr ""
+
+#: lib/Wifty/View.pm:46
+#. ($page)
+msgid "New page '%1'"
+msgstr ""
+
+#: lib/Wifty/Dispatcher.pm:130
+msgid "Next Version"
+msgstr ""
+
+#: lib/Wifty/View.pm:305
+msgid "Next revision"
+msgstr ""
+
+#: lib/Wifty/View.pm:66
+#. ($page)
+msgid "No '%1' page"
+msgstr ""
+
+#: lib/Wifty/Dispatcher.pm:120
+msgid "Previous Version"
+msgstr ""
+
+#: lib/Wifty/View.pm:296
+msgid "Previous revision"
+msgstr ""
+
+#: lib/Wifty/Dispatcher.pm:12
+msgid "Recent Changes"
+msgstr ""
+
+#: lib/Wifty/View.pm:107
+msgid "These are the pages on your wiki!"
+msgstr ""
+
+#: lib/Wifty/View.pm:100
+msgid "Updated this week"
+msgstr ""
+
+#: lib/Wifty/Dispatcher.pm:116
+msgid "View"
+msgstr ""
+
+#: lib/Wifty/View.pm:159
+msgid "Wiki Syntax Help"
+msgstr ""
+
+#: lib/Wifty/View.pm:244
+msgid "hide diff"
+msgstr ""
+
+#: lib/Wifty/View.pm:244
+msgid "show diff"
+msgstr ""

commit 5e43509b70b1f30636106c36747c72070c4541f5
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Dec 12 16:25:31 2008 +0000

    add feeds

diff --git a/lib/Wifty/View/Feeds.pm b/lib/Wifty/View/Feeds.pm
new file mode 100644
index 0000000..919b9e2
--- /dev/null
+++ b/lib/Wifty/View/Feeds.pm
@@ -0,0 +1,61 @@
+use warnings;
+use strict;
+
+package Wifty::View::Feeds;
+use Jifty::View::Declare -base;
+
+use XML::Atom::SimpleFeed;
+use Data::UUID;
+
+# XXX: don't know how to redispatch to private template
+# right from dispatcher
+template 'atom/recent' => sub {
+    set(type => 'full');
+    show('../atom');
+};
+
+template 'atom/recent/diff' => sub {
+    set(type => 'diff');
+    show('../../atom');
+};
+
+template 'atom/recent/headlines' => sub {
+    set(type => 'headlines');
+    show('../../atom');
+};
+
+# XXX: id rendering is not correct
+private template 'atom' => sub {
+    my ($pages, $type) = get(qw(pages type));
+    my $wikiname = Jifty->config->app('WikiName');
+    my $title = $wikiname
+        ? _('Recently changed pages on %1 wiki', $wikiname)
+        : _('Recently changed pages on some wiki');
+    my $feed = XML::Atom::SimpleFeed->new(
+        title   => $title,
+        link    => Jifty->web->url,
+        id      => 'urn:uuid:' . Data::UUID->new->create_str()
+    );
+
+    while ( my $page = $pages->next ) {
+        my $summary = '';
+        if ( !$type || $type eq 'full' ) {
+            $summary = $page->viewer->form_field('content')->wiki_content;
+        }
+        elsif ( $type eq 'diff' ) {
+            $summary = '<pre>'. $page->revisions->last->diff_from .'</pre>';
+        }
+
+        $feed->add_entry(
+            id      => 'urn:uuid:' . Data::UUID->new->create_str(),
+            link    => Jifty->web->url . '/view/' . $page->name,
+            title   => $page->name,
+            author  => $page->updated_by->friendly_name,
+            updated => $page->updated,
+            summary => $summary,
+        );
+    }
+    $feed->print;
+};
+
+1;

commit 7e08edf853ed4854de3a5eaf86295aa95d97177a
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Dec 12 16:27:37 2008 +0000

    add feeds to the dispatcher

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 0de9625..babb431 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -93,17 +93,24 @@ on 'search', run {
 };
 
 # Show recent edits
+under 'feeds/atom/recent', run {
+    set pages => recent_changes();
+};
 on 'recent*', run {
+    set pages => recent_changes();
+};
+
+sub recent_changes {
     my $then = DateTime->from_epoch( epoch => ( time - ( 86400 * 7 ) ) );
     my $pages = Wifty::Model::PageCollection->new();
     $pages->limit(
         column   => 'updated',
         operator => '>',
-        value    => $then->ymd
+        value    => $then->ymd,
     );
     $pages->order_by( column => 'updated', order => 'desc' );
-    set pages => $pages;
-};
+    return $pages;
+}
 
 sub setup_page_nav {
     my ($prefix, $page, $rev) = @_;

commit 8325a813f926d38ff4663a4d1af9dd2c0a9e66bf
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Dec 12 16:29:33 2008 +0000

    add viewer methods in Revison and Page models
    * add diff_to, diff_from, _diff methods in Revision model

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 14f7c8d..5612125 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -85,6 +85,11 @@ sub set_content {
     return ( $val, $msg );
 }
 
+sub viewer {
+    my $self = shift;
+    return Jifty->web->new_action( class => 'UpdatePage', record => $self );
+}
+
 sub _set {
     my $self = shift;
     my ( $val, $msg ) = $self->SUPER::_set(@_);
diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index 85ea17a..53413bd 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -90,6 +90,42 @@ sub next {
     return $revisions->first;
 }
 
+sub diff_from {
+    my $to = shift;
+    my $from = shift;
+    unless ( $from && $from->id ) {
+        $from = $to->previous;
+    }
+    return $to->_diff( $from, $to, @_ );
+}
+
+sub diff_to {
+    my $from = shift;
+    my $to = shift;
+    unless ( $to && $to->id ) {
+        $to = $from->next;
+    }
+    return $from->_diff( $from, $to, @_ );
+}
+
+sub _diff {
+    my $self = shift;
+    my ($from, $to, %opt) = @_;
+    require Text::Diff;
+    return Text::Diff::diff(
+        \( $from? $from->content : '' ),
+        \( $to ? $to->content : '' ),
+        { STYLE => 'Text::Diff::HTML', %opt }
+    );
+}
+
+sub viewer {
+    my $self = shift;
+    my $viewer = $self->page->viewer;
+    $viewer->argument_value( content => $self->content );
+    return $viewer;
+}
+
 =head2 current_user_can RIGHT
 
 We're using L<Jifty::RightsFrom> to pass off ACL decisions to this

commit 95819ea95b2b0d0503108fc46a74019d2654c66b
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Dec 12 16:31:21 2008 +0000

    use a new viewer method

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index babb431..6aae73e 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -47,11 +47,7 @@ on qr{^/(view|edit)/(.*)}, run {
 
     set page => $page;
     set revision => $rev || new Wifty::Model::Revision;
-    my $viewer = Jifty->web->new_action( class => 'UpdatePage', record => $page );
-    if ( $rev ) {
-        $viewer->argument_value(content => $rev->content);
-    }
-    set viewer => $viewer;
+    set viewer => $rev? $rev->viewer: $page->viewer;
     show("/$page_name");
 };
 

commit bc2cd450cdf18caa8049f5cd6df1e4ffef069e5f
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Dec 12 16:33:27 2008 +0000

    use ViewDeclarePage

diff --git a/etc/config.yml b/etc/config.yml
index 2ca00cd..23e9ace 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -19,6 +19,7 @@ framework:
     - User: {}
     - Authentication::Password:
         login_by: email
+    - ViewDeclarePage: {}
 
   Mailer: IO
   MailerArgs:

commit e990591a50d79a58869533678a53a5780e69606d
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Dec 12 16:34:44 2008 +0000

    switch to new page plugin

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index b217a27..11e2482 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -130,24 +130,6 @@ private template 'search_box' => sub {
     } };
 };
 
-private template 'menu' => sub {
-    my $wikiname = Jifty->config->app('WikiName') || "Wifty";
-    h1 { attr { id is 'wikiname' }
-        Jifty->web->link( url => "/", label => _($wikiname) )
-    }
-    div { attr { id => "navigation" };
-        Jifty->web->navigation->render_as_menu;
-    };
-};
-
-private template 'heading_in_wrapper' => sub {
-    h1 { attr { class => 'title' }; outs_raw(get('title')) };
-    Jifty->web->page_navigation->render_as_menu;
-#    show('/search_box');
-    hr { {class is 'clear'} }
-};
-
-
 private template markup => sub {
     return undef unless Jifty->config->app('Formatter') eq 'Markdown';
 
diff --git a/lib/Wifty/View/Page.pm b/lib/Wifty/View/Page.pm
index c896e24..971a6dd 100644
--- a/lib/Wifty/View/Page.pm
+++ b/lib/Wifty/View/Page.pm
@@ -2,20 +2,43 @@ use strict;
 use warnings;
 
 package Wifty::View::Page;
-use base qw(Jifty::View::Declare::Page);
+use base qw(Jifty::Plugin::ViewDeclarePage::Page);
 use Jifty::View::Declare::Helpers;
 
-sub render_body {
-    my ($self, $body_code) = @_;
+sub render_page {
+    my $self = shift;
 
-    my $logo = Jifty->config->app('Logo');
-    return $self->SUPER::render_body( $body_code ) unless $logo;
+    if ( my $logo = Jifty->config->app('Logo') ) {
+        div { attr { id is "logo" } 
+            img { src is $logo, alt is '' }
+        };
+    }
 
-    return $self->SUPER::render_body( sub {
-        div { attr { id is "logo" } img { src is $logo, alt is '' } };
-        $body_code->();
-    });
+    return $self->SUPER::render_page( @_ );
 }
 
-1;
+sub render_navigation {
+    my $self = shift;
+    my $wikiname = Jifty->config->app('WikiName') || "Wifty";
+    h1 { attr { id is 'wikiname' }
+        Jifty->web->link( url => "/", label => _($wikiname) )
+    };
+    return $self->SUPER::render_navigation( @_ );
+}
+
+sub render_title_inhead {
+    my $self = shift;
+    my $title = shift;
+    my $wikiname = Jifty->config->app('WikiName') || "Wifty";
+    return $self->SUPER::render_title_inhead( $title .' - '. $wikiname );
+}
 
+sub render_title_inpage {
+    my $self = shift;
+    $self->SUPER::render_title_inpage( @_ );
+#    show('/search_box');
+    hr { {class is 'clear'} };
+    return '';
+}
+
+1;

commit b2aaa93d9ed91c1518a5c3e90d8ab874add3eb23
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Dec 12 16:36:04 2008 +0000

    actually bind feeds into templates tree

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index 11e2482..f364257 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -4,6 +4,9 @@ use strict;
 package Wifty::View;
 use Jifty::View::Declare -base;
 
+require Wifty::View::Feeds;
+alias Wifty::View::Feeds under 'feeds/';
+
 template 'view' => page {
     my ( $page, $revision, $viewer ) = get(qw(page revision viewer));
     my $title = $revision->id

commit 242972de5867c1f1cab8b4b5c8d6a17bf382086a
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Dec 12 16:37:01 2008 +0000

    use new *diff* methods

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index f364257..f5f6965 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -249,20 +249,14 @@ template 'helpers/diff' => sub {
 
 private template 'diff' => sub {
     my ($from, $to) = get(qw(from to));
-    if ( $to && !($from && $from->id) ) {
-        $from = $to->previous;
+    if ( $to && $to->id ) {
+        pre {{ class is 'diff' } outs_raw( $to->diff_from( $from ) ) };
     }
-    elsif ( !($to && $to->id) && $from ) {
-        $to = $from->next;
+    elsif ( $from && $from->id ) {
+        pre {{ class is 'diff' } outs_raw( $from->diff_to( $to ) ) };
+    } else {
+        die "illegal arguments for diff";
     }
-
-    use Text::Diff ();
-    my $diff = Text::Diff::diff(
-        \( $from? $from->content : '' ),
-        \( $to ? $to->content : '' ),
-        { STYLE => 'Text::Diff::HTML' }
-    );
-    pre {{ class is 'diff' } outs_raw($diff) };
 };
 
 private template 'diff/with_nav' => sub {

commit 0626e65376935833579275e48c7aa119fd50f38e
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Dec 12 16:44:22 2008 +0000

    announce atom feeds right from the page

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index f5f6965..2d6d83e 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -101,8 +101,23 @@ template history => page {
 template recent => page {
     my ($pages) = get(qw(pages));
     { title is _('Updated this week') }
-
     show( 'page_list', pages => $pages, id => 'recentupdates' );
+
+    add rel "alternate",
+        type => "application/atom+xml",
+        title => _('Updated this week') .' '. _('(full content)'),
+        href => '/feeds/atom/recent',
+    ;
+    add rel "alternate",
+        type => "application/atom+xml",
+        title => _('Updated this week') .' '. _('(headlines)'),
+        href => '/feeds/atom/recent/headlines',
+    ;
+    add rel "alternate",
+        type => "application/atom+xml",
+        title => _('Updated this week') .' '. _('(diffs)'),
+        href => '/feeds/atom/recent/diffs',
+    ;
 };
 
 template pages => page {

commit 3457f38ce9b666aab2f8a27e636a93a8ba7995ad
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 13 23:37:00 2008 +0000

    add created/created_by on Page

diff --git a/etc/config.yml b/etc/config.yml
index 23e9ace..7f93d3a 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -10,7 +10,7 @@ framework:
     Driver: SQLite
     Host: localhost
     User: postgres
-    Version: 0.0.20
+    Version: 0.0.21
     Password: ''
     RequireSSL: 0
   Plugins:
diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 5612125..4a53260 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -30,6 +30,14 @@ column updated_by =>
     refers_to Wifty::Model::User,
     since '0.0.16';
 
+column created =>
+    type is 'timestamp',
+    since '0.0.21';
+
+column created_by =>
+    refers_to Wifty::Model::User,
+    since '0.0.21';
+
 column revisions =>
     refers_to Wifty::Model::RevisionCollection by 'page';
 };
@@ -38,8 +46,9 @@ sub create {
     my $self = shift;
     my %args = (@_);
     my $now  = DateTime->now();
-    $args{'updated'} = $now->ymd . " " . $now->hms;
-    $args{'updated_by'} = ( $self->current_user? $self->current_user->user_object : undef );
+    $args{'created'} = $args{'updated'} = $now->ymd . " " . $now->hms;
+    $args{'created_by'} = $args{'updated_by'}
+        = $self->current_user? $self->current_user->user_object : undef;
     my ($id) = $self->SUPER::create(%args);
     if ( $self->id ) {
         $self->_add_revision(%args);
diff --git a/lib/Wifty/Upgrade.pm b/lib/Wifty/Upgrade.pm
new file mode 100644
index 0000000..b59f1f2
--- /dev/null
+++ b/lib/Wifty/Upgrade.pm
@@ -0,0 +1,24 @@
+use strict;
+use warnings;
+
+package Wifty::Upgrade;
+
+use base qw(Jifty::Upgrade);
+use Jifty::Upgrade qw( since rename );
+
+since '0.0.21' => sub {
+    my $pages = Wifty::Model::PageCollection->new(
+        current_user => Jifty->app_class('CurrentUser')->superuser
+    );
+    $pages->unlimit;
+
+    while ( my $page = $pages->next ) {
+        my $first_rev = $page->revisions->first;
+        my ($status, $msg) = $page->set_created( $first_rev? $first_rev->created : $page->updated );
+        Jifty->log->error("Couldn't set created:". $msg) unless $status;
+        ($status, $msg) = $page->set_created_by( $first_rev? $first_rev->created_by : $page->updated_by );
+        Jifty->log->error("Couldn't set created_by:". $msg) unless $status;
+    }
+};
+
+1;

commit db0f755504f7075318ef87fd37a052e25ff239a4
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 13 23:38:07 2008 +0000

    add PageCollection class with recently_{updated,created}

diff --git a/lib/Wifty/Model/PageCollection.pm b/lib/Wifty/Model/PageCollection.pm
new file mode 100644
index 0000000..22899c5
--- /dev/null
+++ b/lib/Wifty/Model/PageCollection.pm
@@ -0,0 +1,26 @@
+use strict;
+use warnings;
+
+package Wifty::Model::PageCollection;
+use base qw(Jifty::Collection);
+
+sub recently_created { return (shift)->_recently('created', @_) }
+sub recently_updated { return (shift)->_recently('updated', @_) }
+
+sub _recently {
+    my $proto = shift;
+    my $self = ref($proto)? $proto : new $proto;
+    my $column = shift;
+    my $time = shift || 7*24*60*60;
+
+    my $then = DateTime->from_epoch( epoch => time - $time );
+    $self->limit(
+        column   => $column,
+        operator => '>',
+        value    => $then->ymd,
+    );
+    $self->order_by( column => $column, order => 'desc' );
+    return $self;
+}
+
+1;

commit 4e1a1c55e3e936fec8717856e81e4d83ee2dceb9
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 13 23:39:17 2008 +0000

    remove old experimental View, we have all this implemented

diff --git a/lib/Wifty/View-not-ready-yet.pm b/lib/Wifty/View-not-ready-yet.pm
deleted file mode 100644
index 04a4022..0000000
--- a/lib/Wifty/View-not-ready-yet.pm
+++ /dev/null
@@ -1,69 +0,0 @@
-use warnings;
-use strict;
-
-
-=head1 NAME
-
-Wifty::View
-
-=head1 DESCRIPTION
-
-This code is only useful on the new Jifty "Declarative tempaltes" branch. It shouldn't get in the way 
-if you're running a traditional (0.610 or before) Jifty.
-
-=cut
-
-package Wifty::View;
-use base qw/Jifty::View::Declare::Templates/;
-# includes my application's plugins' View libraries as superclasses.
-use Template::Declare::Tags;
-use Jifty::View::Declare::Templates;
-
-template recent_atom => sub {
-    my ( $pages) = get(qw(pages));
-    use XML::Atom::SimpleFeed;
-    use Data::UUID;
-    my $feed = XML::Atom::SimpleFeed->new(
-        title   => 'Recently changed pages',
-        link    => Jifty->web->url,
-        updated => '2009-12-31T00:00:00Z',
-        author  => 'John Doe',
-        id      => 'urn:uuid:' . Data::UViewD->new->create_str()
-    );
-
-    while ( my $page = $pages->next ) {
-
-        $feed->add_entry(
-            title   => $page->name,
-            link    => Jifty->web->url . '/view/' . $page->name,
-            id      => 'urn:uuid:' . Data::UViewD->new->create_str(),
-            summary => $page->content,
-            updated => $page->updated
-        );
-    }
-    $feed->print;
-};
-
-private template header => sub {
-    my %args = ( title=> undef, wikiname => undef, @_);
-    
-    my (  $title, $wikiname ) = ($args{'title'}, $args{'wikiname'});
-    # $HTML::Mason::r->content_type('text/html; charset=utf-8');
-    outs(
-        '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
-    );
-
-    html {{ xmlns is "http://www.w3.org/1999/xhtml", xml__lang is "en" }
-        head {
-            meta {{ http_equiv is "content-type", content is "text/html; charset=utf-8" }};
-            meta {{ name is 'robots', content is 'all' }};
-            title { _($title) . ' - ' . _($wikiname) };
-
-            Jifty->web->include_css;
-            Jifty->web->include_javascript;
-
-            }
-        }
-};
-
-1;

commit bd2178cad7872d7c3f2ef214d08eefbee0a333a4
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 13 23:43:31 2008 +0000

    split recent into recent changes and recent additions

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 6aae73e..1c30dc4 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -8,9 +8,10 @@ under '/', run {
 
 before '*', run {
     my $top = Jifty->web->navigation;
-    $top->child( Home   => url => "/", label => _("Home") );
-    $top->child( Recent => url => "/recent", label => _("Recent Changes") );
-    $top->child( Search => url => "/search", label => _("Search") );
+    $top->child( Home   => url => "/",                 label => _("Home") );
+    $top->child( Recent => url => "/recent/changes",   label => _("Recent Changes") );
+    $top->child( New    => url => "/recent/additions", label => _("New") );
+    $top->child( Search => url => "/search",           label => _("Search") );
 };
 
 # Default page
@@ -88,26 +89,31 @@ on 'search', run {
     set pages => $collection;
 };
 
-# Show recent edits
+# Show recent
+
+# backwards compat
+on '/recent', run {
+    redirect('/recent/changes');
+};
+
 under 'feeds/atom/recent', run {
     set pages => recent_changes();
 };
-on 'recent*', run {
-    set pages => recent_changes();
+on qr{^/recent/(.+)}, run {
+    my $type = $1;
+    if ( $type eq 'changes' ) {
+        set title => _('Updated this week');
+        set pages => Wifty::Model::PageCollection->recently_updated;
+    } elsif ( $type eq 'additions' ) {
+        set title => _('Created this week');
+        set pages => Wifty::Model::PageCollection->recently_created;
+    } else {
+        redirect('/recent/changes');
+    }
+    set( type => $type );
+    show('/recent');
 };
 
-sub recent_changes {
-    my $then = DateTime->from_epoch( epoch => ( time - ( 86400 * 7 ) ) );
-    my $pages = Wifty::Model::PageCollection->new();
-    $pages->limit(
-        column   => 'updated',
-        operator => '>',
-        value    => $then->ymd,
-    );
-    $pages->order_by( column => 'updated', order => 'desc' );
-    return $pages;
-}
-
 sub setup_page_nav {
     my ($prefix, $page, $rev) = @_;
 
diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index 2d6d83e..2d45c61 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -99,25 +99,14 @@ template history => page {
 };
 
 template recent => page {
-    my ($pages) = get(qw(pages));
-    { title is _('Updated this week') }
-    show( 'page_list', pages => $pages, id => 'recentupdates' );
-
-    add rel "alternate",
-        type => "application/atom+xml",
-        title => _('Updated this week') .' '. _('(full content)'),
-        href => '/feeds/atom/recent',
-    ;
-    add rel "alternate",
-        type => "application/atom+xml",
-        title => _('Updated this week') .' '. _('(headlines)'),
-        href => '/feeds/atom/recent/headlines',
-    ;
-    add rel "alternate",
-        type => "application/atom+xml",
-        title => _('Updated this week') .' '. _('(diffs)'),
-        href => '/feeds/atom/recent/diffs',
-    ;
+    my ($pages, $title, $type) = get(qw(pages title type));
+
+    title is $title;
+
+    set( id => 'recent-'. $type ); show( 'page_list' );
+
+    set( path => "recent/$type" );
+    show('/feeds/pages_links');
 };
 
 template pages => page {

commit 45b618fdf1050568f8b383d028715a5f1fa7e834
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Dec 15 14:44:43 2008 +0000

    delete overriden main.css so we get new things from jifty
      like jGrowl messages

diff --git a/share/web/static/css/main.css b/share/web/static/css/main.css
deleted file mode 100644
index f1e909f..0000000
--- a/share/web/static/css/main.css
+++ /dev/null
@@ -1,9 +0,0 @@
- at import "app-base.css";
- at import "base.css";
- at import "nav.css";
- at import "keybindings.css";
- at import "forms.css";
- at import "halos.css";
- at import "app.css";
- at import "autocomplete.css";
- at import "notices.css";

commit 9ff2906b7a051c87ee00b746e1401f3c34112e49
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Dec 15 14:47:55 2008 +0000

    get rid of some css files

diff --git a/share/web/static/css/app.css b/share/web/static/css/app.css
index 9f95109..c36034f 100644
--- a/share/web/static/css/app.css
+++ b/share/web/static/css/app.css
@@ -1,3 +1,12 @@
+
+hr.clear {
+    clear: both;
+    visibility: hidden;
+    height: 0;
+    padding: 0;
+    margin: 0;
+}
+
 div.argument-content {
     width: 70%;
 }
@@ -112,3 +121,4 @@ hr {
 #login-box input.argument-remember {
     float: left;
 }
+
diff --git a/share/web/static/css/base.css b/share/web/static/css/base.css
deleted file mode 100644
index 27e44e2..0000000
--- a/share/web/static/css/base.css
+++ /dev/null
@@ -1,68 +0,0 @@
-.error {
-    color: #a00000;
-}
-
-.warning {
-    color: #00a0a0;
-}
-
-hr.clear {
-    clear: both;
-    visibility: hidden;
-    height: 0;
-    padding: 0;
-    margin: 0;
-} 
-
-.messages .message {
-    display: block;
-}
-
-div#messages,  div#errors {
-     background-color: rgb(240,234,183);
-     border: 1px solid rgb(230,224,173);
-     margin-top: 10px;
-     margin-bottom: 10px;
-     padding: 5px;
-     font-size: 1.2em;
-}
-
-div.spacer {
-    clear: both;
-}
-
-.next-page, .prev-page {
-    display: block;
-    float: left;
-    margin: 0.5em 0;
-    padding: 0.2em 0.5em 0.5em 0.5em;
-    border-top: 1px solid gray;
-}
-
-.next-page { padding-right: 1em; }
-.prev-page { padding-left: 1em; }
-
-div#jifty-wait-message {
-    color: red;
-    background: black;
-    font-size: 2em;
-    position: fixed;
-    top: 10px;
-    right: 10px;
-    z-index: 42;
-}
-
-div.warning {
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    background-color: red;
-    color: white;
-    padding: .5em;
-    border-bottom: 1px solid #000;
-}
-
-div.warning a {
-    color: white;
-}
diff --git a/share/web/static/css/forms.css b/share/web/static/css/forms.css
deleted file mode 100644
index 125bec9..0000000
--- a/share/web/static/css/forms.css
+++ /dev/null
@@ -1,123 +0,0 @@
-/* buttons */
-
-input.button {
-    margin-top: 0.6em;
-    padding: 0.15em 1em;
-    font-weight: bold;
-}
-
-* html input.button {
-    padding: 0 0.1em;
-}
-
-/* fields */
-
-input.text, input.date, input.password, input.combo-text, textarea, select {
-    border-top: 1px solid #7c7c7c;
-    border-left: 1px solid #c3c3c3;
-    border-right: 1px solid #c3c3c3;
-    border-bottom: 1px solid #ddd;
-    background: #fff url(/static/images/css/fieldbg.gif) repeat-x top;
-    padding: 0.2em;
-    font-size: 1em;
-}
-
-label, span.label {
-    font-size: 0.9em;
-}
-
-.form_field .hints {
-    font-size: 0.9em;
-    color: #777;
-}
-
-/* layout */
-
-.form_field {
-    clear: both;
-}
-
-label, span.label {
-    display: block;
-    width: 20%;
-    float: left;
-    text-align: right;
-    margin-right: 0.5em;
-}
-
-* html label, * html span.label {
-    width: 22%;
-}
-
-form .hints {
-    display: block;
-    clear: both;
-}
-
-html>body form .hints {
-    padding: 0.2em 0 0.2em 21%;
-}
-
-* html form .hints {
-    padding-left: 11.5%;
-}
-
-form .error {
-    display: block;
-    clear: both;
-}
-
-.form_field {
-    padding: 0.3em 0 0 0;
-}
-
-.inline .hints {
-    padding-left: 0;
-}
-
-.inline label, .inline span.label {
-    float: none;
-    width: auto;
-    text-align: left;
-    margin-right: auto;
-    font-weight: bold;
-}
-
-.inline .form_field {
-    float: left;
-    clear: none;
-    margin-right: 0.5em;
-}
-
-.inline .button {
-    margin-top: 1.1em;
-}
-
-.button_line {
-    border-top: 1px solid #ccc;
-    padding-right: 5em;
-    margin-top: 1.5em;
-    clear: both;
-    direction: rtl;
-}
-
-form .line {
-    clear: both;
-}
-
-/* So the admin ui is one row per line */
-
-.jifty_admin.item.inline {
-     clear: both;
-}
-
-.jifty_admin .editlink {
-    float: right;
-    border-left: 1px solid black;
-    border-bottom: 1px solid black;
-    padding: 0 0 10px 10px;
-}
-
-.jifty_admin hr {
-    clear: both;
-}
diff --git a/share/web/static/css/keybindings.css b/share/web/static/css/keybindings.css
deleted file mode 100644
index 87f864e..0000000
--- a/share/web/static/css/keybindings.css
+++ /dev/null
@@ -1,25 +0,0 @@
-div#keybindings {
-    color: #666666;
-    margin-top: 2em;
-}
-
-dl.keybindings .keybinding {
-    display: inline;
-}
-
-dl.keybindings dt  {
-    margin: 0;
-    font-weight: bold;
-    display: inline;
-
-}
-dl.keybindings dt:after  {
-    content: ":";
-
-}
-dl.keybindings dd  {
-    margin-right: 1.5em;
-    margin-left: 0.5em;
-    display: inline;
-    white-space: nowrap;
-}
diff --git a/share/web/static/css/nav.css b/share/web/static/css/nav.css
index b71722f..2fba9e8 100644
--- a/share/web/static/css/nav.css
+++ b/share/web/static/css/nav.css
@@ -18,4 +18,4 @@ ul.menu li {
 #wikiheader ul.menu li {
     padding-right: 0;
     padding-left: 1em;
-}
\ No newline at end of file
+}

commit eaee8ab839a9036b6d4f142ebd21ee0dab0b847c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Dec 15 14:49:23 2008 +0000

    return back default wifty layout we had before

diff --git a/lib/Wifty/View/Page.pm b/lib/Wifty/View/Page.pm
index 971a6dd..44293ac 100644
--- a/lib/Wifty/View/Page.pm
+++ b/lib/Wifty/View/Page.pm
@@ -8,13 +8,38 @@ use Jifty::View::Declare::Helpers;
 sub render_page {
     my $self = shift;
 
+    my $wikiname = Jifty->config->app('WikiName') || "Wifty";
+
     if ( my $logo = Jifty->config->app('Logo') ) {
         div { attr { id is "logo" } 
             img { src is $logo, alt is '' }
         };
     }
 
-    return $self->SUPER::render_page( @_ );
+    Template::Declare->new_buffer_frame;
+    $self->instrument_content;
+    my $content = Template::Declare->end_buffer_frame->data;
+
+    div { attr { id is "header" }
+        div { attr { id is "wikiheader" }
+            $self->render_navigation;
+        }
+        div { attr { id is "pageheader" }
+            h1 { attr { id is "pagename" }; outs( $self->_title ) };
+            Jifty->web->page_navigation->render_as_menu;
+        }
+    };
+    $self->render_salutation;
+    hr { attr { class is 'clear' } };
+    
+    Jifty->web->render_messages;
+
+    div { attr { id is "content" };
+        outs_raw( $content );
+        hr { attr { class is 'clear' } };
+    };
+    $self->render_jifty_page_detritus;
+    return '';
 }
 
 sub render_navigation {
@@ -23,9 +48,17 @@ sub render_navigation {
     h1 { attr { id is 'wikiname' }
         Jifty->web->link( url => "/", label => _($wikiname) )
     };
-    return $self->SUPER::render_navigation( @_ );
+    $self->SUPER::render_navigation( @_ );
+    show('/search_box');
+    return '';
 }
 
+=head2 render_title_inhead
+
+Adds " - <wikiname>" after page title.
+
+=cut
+
 sub render_title_inhead {
     my $self = shift;
     my $title = shift;
@@ -33,12 +66,6 @@ sub render_title_inhead {
     return $self->SUPER::render_title_inhead( $title .' - '. $wikiname );
 }
 
-sub render_title_inpage {
-    my $self = shift;
-    $self->SUPER::render_title_inpage( @_ );
-#    show('/search_box');
-    hr { {class is 'clear'} };
-    return '';
-}
+sub render_title_inpage { return '' }
 
 1;

commit 8c964d6def0d799cd82b87fd1268b1be2827eb9c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon Dec 15 15:25:24 2008 +0000

    rework feeds:
    ** headlines are default
    ** cover feeds for new pages
    * under/on doesn't work the way it should, use just on

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 1c30dc4..ffb54c0 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -92,14 +92,8 @@ on 'search', run {
 # Show recent
 
 # backwards compat
-on '/recent', run {
-    redirect('/recent/changes');
-};
-
-under 'feeds/atom/recent', run {
-    set pages => recent_changes();
-};
-on qr{^/recent/(.+)}, run {
+on 'recent' => run { redirect('/recent/changes') };
+on qr{^/recent/(changes|additions)}, run {
     my $type = $1;
     if ( $type eq 'changes' ) {
         set title => _('Updated this week');
@@ -107,13 +101,32 @@ on qr{^/recent/(.+)}, run {
     } elsif ( $type eq 'additions' ) {
         set title => _('Created this week');
         set pages => Wifty::Model::PageCollection->recently_created;
-    } else {
-        redirect('/recent/changes');
     }
-    set( type => $type );
+    set type => $type;
     show('/recent');
 };
 
+on 'feeds/atom/recent' => run { redirect('/feeds/atom/recent/changes/headlines') };
+on qr{^/feeds/atom/recent/(changes|additions)(?:/(full|headlines|diff))?$} => run {
+    my $wikiname = Jifty->config->app('WikiName');
+    my $show = $1;
+    my $show_as = $2 || 'headlines';
+    my ($pages, $title);
+    if ( $show eq 'changes' ) {
+        $pages = Wifty::Model::PageCollection->recently_updated;
+        $title = $wikiname
+            ? _('Recently changed pages on %1 wiki', $wikiname)
+            : _('Recently changed pages on some wiki');
+    } else {
+        $pages = Wifty::Model::PageCollection->recently_created;
+        $title = $wikiname
+            ? _('Recently added pages on %1 wiki', $wikiname)
+            : _('Recently added pages on some wiki');
+    }
+    set( title => $title ); set( pages => $pages ); set( show_as => $show_as );
+    show('/feeds/atom/pages');
+};
+
 sub setup_page_nav {
     my ($prefix, $page, $rev) = @_;
 
diff --git a/lib/Wifty/View/Feeds.pm b/lib/Wifty/View/Feeds.pm
index 919b9e2..0a62f52 100644
--- a/lib/Wifty/View/Feeds.pm
+++ b/lib/Wifty/View/Feeds.pm
@@ -7,30 +7,31 @@ use Jifty::View::Declare -base;
 use XML::Atom::SimpleFeed;
 use Data::UUID;
 
-# XXX: don't know how to redispatch to private template
-# right from dispatcher
-template 'atom/recent' => sub {
-    set(type => 'full');
-    show('../atom');
-};
-
-template 'atom/recent/diff' => sub {
-    set(type => 'diff');
-    show('../../atom');
-};
-
-template 'atom/recent/headlines' => sub {
-    set(type => 'headlines');
-    show('../../atom');
+private template 'pages_links' => sub {
+    my ($title, $path) = get(qw(title path));
+
+    add rel "alternate",
+        type => "application/atom+xml",
+        title => $title .' '. _('(headlines)'),
+        href => "/feeds/atom/$path/headlines",
+    ;
+    add rel "alternate",
+        type => "application/atom+xml",
+        title => $title .' '. _('(full content)'),
+        href => "/feeds/atom/$path/full",
+    ;
+    add rel "alternate",
+        type => "application/atom+xml",
+        title => $title .' '. _('(diffs)'),
+        href => "/feeds/atom/$path/diffs",
+    ;
 };
 
 # XXX: id rendering is not correct
-private template 'atom' => sub {
-    my ($pages, $type) = get(qw(pages type));
-    my $wikiname = Jifty->config->app('WikiName');
-    my $title = $wikiname
-        ? _('Recently changed pages on %1 wiki', $wikiname)
-        : _('Recently changed pages on some wiki');
+# XXX: don't know how to dispatch to private template
+template 'atom/pages' => sub {
+    my ($pages, $title, $show_as) = get(qw(pages title show_as));
+    $show_as ||= 'headlines';
     my $feed = XML::Atom::SimpleFeed->new(
         title   => $title,
         link    => Jifty->web->url,
@@ -39,10 +40,10 @@ private template 'atom' => sub {
 
     while ( my $page = $pages->next ) {
         my $summary = '';
-        if ( !$type || $type eq 'full' ) {
+        if ( $show_as eq 'full' ) {
             $summary = $page->viewer->form_field('content')->wiki_content;
         }
-        elsif ( $type eq 'diff' ) {
+        elsif ( $show_as eq 'diff' ) {
             $summary = '<pre>'. $page->revisions->last->diff_from .'</pre>';
         }
 

commit f8cdc037d6394a29ea35bb5be93a99969ac08847
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Mon Dec 15 15:32:08 2008 +0000

    Add a requirement on XML::Atom::SimpleFeed

diff --git a/Makefile.PL b/Makefile.PL
index 8273f33..6fcb19b 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -5,5 +5,6 @@ requires('Jifty');
 requires('Text::Markdown');
 requires('HTML::Scrubber');
 requires('Text::Diff::HTML');
+requires('XML::Atom::SimpleFeed');
 recommends('Text::KwikiFormatish');
 WriteAll;

commit a21d14398ca55e2059afccca13e2e7dffc95ab6e
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Dec 16 23:43:45 2008 +0000

    'title is' => page_title

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index 2d45c61..b03364a 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -9,20 +9,21 @@ alias Wifty::View::Feeds under 'feeds/';
 
 template 'view' => page {
     my ( $page, $revision, $viewer ) = get(qw(page revision viewer));
-    my $title = $revision->id
+    page_title is
+        $revision->id
         ? _('%1 as of %2', $page->name, $revision->created)
         : $page->name;
-    { title is $title }
+
     render_param($viewer => 'content', label => '', render_mode => 'read');
 };
 
 template 'edit' => page {
     my ( $page, $revision, $viewer ) = get(qw(page revision viewer));
 
-    my $title = $revision->id
+    page_title is
+        $revision->id
         ? _('Edit page %1 as of %2', $page->name, $revision->created)
         : _('Edit page %1');
-    { title is $title }
 
     my $can_edit = $page->current_user_can('update');
 
@@ -46,7 +47,8 @@ template 'edit' => page {
 template create => page {
     my ($action, $page) = get(qw(action page));
 
-    { title is _("New page '%1'", $page), id is 'create' };
+    id is 'create';
+    page_title is _("New page '%1'", $page);
 
     div {
         show('markup');
@@ -66,7 +68,7 @@ template create => page {
 template no_such_page => page {
     my ($page) = get(qw(page));
 
-    { title is _("No '%1' page", $page) }
+    page_title is _("No '%1' page", $page);
 
     p { 
         q{Unfortunately, you've tried to reach a page that doesn't exist }
@@ -79,7 +81,8 @@ template no_such_page => page {
 template history => page {
     my ( $page, $revisions ) = get(qw(page revisions));
     $revisions->do_search; # avoid count+fetch
-    { title is $revisions->count . " revisions of " . $page->name }
+
+    page_title is _('%1 revision(s) of %2', $revisions->count, $page->name);
 
     ul { { id is 'history' }
         while ( my $rev = $revisions->next ) { li {
@@ -101,7 +104,7 @@ template history => page {
 template recent => page {
     my ($pages, $title, $type) = get(qw(pages title type));
 
-    title is $title;
+    page_title is $title;
 
     set( id => 'recent-'. $type ); show( 'page_list' );
 
@@ -111,7 +114,8 @@ template recent => page {
 
 template pages => page {
     my ($pages ) = get(qw(pages));
-    { title is _('These are the pages on your wiki!') }
+
+    page_title is _('These are the pages on your wiki!');
 
     show( 'page_list', pages => $pages, id => 'allpages' );
 };
@@ -119,6 +123,8 @@ template pages => page {
 template search => page {
     my ( $pages, $search ) = get(qw(pages search));
 
+    page_title is _('Search');
+
     form { div { { id is "searchbox", class is 'inline' }
         render_param $search => 'contains', label => _('Find pages containing:');
         form_submit label => 'Search', submit => $search;

commit 6c382fc5b08e1f42bbe398138f1759998cd2e05c
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Dec 16 23:49:06 2008 +0000

    publish feeds in head and content where we want them to be
      right from templates using new page class plugin
    * add simple css

diff --git a/lib/Wifty/View/Feeds.pm b/lib/Wifty/View/Feeds.pm
index 0a62f52..f54851b 100644
--- a/lib/Wifty/View/Feeds.pm
+++ b/lib/Wifty/View/Feeds.pm
@@ -10,21 +10,24 @@ use Data::UUID;
 private template 'pages_links' => sub {
     my ($title, $path) = get(qw(title path));
 
-    add rel "alternate",
-        type => "application/atom+xml",
-        title => $title .' '. _('(headlines)'),
-        href => "/feeds/atom/$path/headlines",
-    ;
-    add rel "alternate",
-        type => "application/atom+xml",
-        title => $title .' '. _('(full content)'),
-        href => "/feeds/atom/$path/full",
-    ;
-    add rel "alternate",
-        type => "application/atom+xml",
-        title => $title .' '. _('(diffs)'),
-        href => "/feeds/atom/$path/diffs",
-    ;
+    ul { attr { class is 'atom-feeds' };
+        li { add rel "alternate",
+            type => "application/atom+xml",
+            title => $title .' '. _('(headlines)'),
+            href => "/feeds/atom/$path/headlines",
+        }
+        li { add rel "alternate",
+            type => "application/atom+xml",
+            title => $title .' '. _('(full content)'),
+            href => "/feeds/atom/$path/full",
+        }
+        li { add rel "alternate",
+            type => "application/atom+xml",
+            title => $title .' '. _('(diffs)'),
+            href => "/feeds/atom/$path/diffs",
+        }
+    };
+    return '';
 };
 
 # XXX: id rendering is not correct
diff --git a/lib/Wifty/View/Page.pm b/lib/Wifty/View/Page.pm
index 44293ac..448e157 100644
--- a/lib/Wifty/View/Page.pm
+++ b/lib/Wifty/View/Page.pm
@@ -68,4 +68,18 @@ sub render_title_inhead {
 
 sub render_title_inpage { return '' }
 
+sub render_link_inpage {
+    my $self = shift;
+    my %link = @_;
+    if ( ($link{rel}||'') eq 'alternate' && ($link{type}||'') eq 'application/atom+xml' ) {
+        a { attr { href => $link{'href'} };
+            img { attr {
+                src => '/static/images/feed-icon-14x14.png',
+                width => 14, heigth => 14,
+                title => $link{'title'}
+            } }
+        }
+    }
+    return '';
+}
 1;
diff --git a/share/web/static/css/app.css b/share/web/static/css/app.css
index c36034f..1a346ec 100644
--- a/share/web/static/css/app.css
+++ b/share/web/static/css/app.css
@@ -122,3 +122,20 @@ hr {
     float: left;
 }
 
+ul.atom-feeds {
+    clear: both;
+    margin-left: 0em;
+    padding-left: 0em;
+    list-style-type: none;
+}
+
+ul.atom-feeds a img {
+    border: none;
+}
+
+ul.atom-feeds li {
+    margin-left: 1em;
+    padding-left: 0;
+    display: inline;
+}
+

commit 043197f6c03975c182325fc4fee443a7ca6d542f
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Dec 16 23:50:18 2008 +0000

    add standard feed image

diff --git a/share/web/static/images/feed-icon-14x14.png b/share/web/static/images/feed-icon-14x14.png
new file mode 100755
index 0000000..b3c949d
Binary files /dev/null and b/share/web/static/images/feed-icon-14x14.png differ

commit 90d8bb4f5774e2a2baf62abc3d4da3b93f48ae12
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Dec 19 22:20:37 2008 +0000

    make diff/diffs the same

diff --git a/lib/Wifty/View/Feeds.pm b/lib/Wifty/View/Feeds.pm
index f54851b..d5a2661 100644
--- a/lib/Wifty/View/Feeds.pm
+++ b/lib/Wifty/View/Feeds.pm
@@ -46,7 +46,7 @@ template 'atom/pages' => sub {
         if ( $show_as eq 'full' ) {
             $summary = $page->viewer->form_field('content')->wiki_content;
         }
-        elsif ( $show_as eq 'diff' ) {
+        elsif ( $show_as eq 'diff' or $show_as eq 'diffs' ) {
             $summary = '<pre>'. $page->revisions->last->diff_from .'</pre>';
         }
 

commit 92d43f5f78efb276049ce4d1dbd90c3bcc04efca
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Dec 19 22:21:33 2008 +0000

    the same for urls

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index ffb54c0..c3a9a88 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -107,7 +107,7 @@ on qr{^/recent/(changes|additions)}, run {
 };
 
 on 'feeds/atom/recent' => run { redirect('/feeds/atom/recent/changes/headlines') };
-on qr{^/feeds/atom/recent/(changes|additions)(?:/(full|headlines|diff))?$} => run {
+on qr{^/feeds/atom/recent/(changes|additions)(?:/(full|headlines?|diffs?))?$} => run {
     my $wikiname = Jifty->config->app('WikiName');
     my $show = $1;
     my $show_as = $2 || 'headlines';

commit a8535dac8f77be215aa5070145d4bb61a9b437cc
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Dec 19 22:57:54 2008 +0000

    generating different ids each time is totally incorrect, unique
      url is better

diff --git a/lib/Wifty/View/Feeds.pm b/lib/Wifty/View/Feeds.pm
index d5a2661..971653f 100644
--- a/lib/Wifty/View/Feeds.pm
+++ b/lib/Wifty/View/Feeds.pm
@@ -30,7 +30,6 @@ private template 'pages_links' => sub {
     return '';
 };
 
-# XXX: id rendering is not correct
 # XXX: don't know how to dispatch to private template
 template 'atom/pages' => sub {
     my ($pages, $title, $show_as) = get(qw(pages title show_as));
@@ -38,22 +37,24 @@ template 'atom/pages' => sub {
     my $feed = XML::Atom::SimpleFeed->new(
         title   => $title,
         link    => Jifty->web->url,
-        id      => 'urn:uuid:' . Data::UUID->new->create_str()
     );
 
     while ( my $page = $pages->next ) {
+        my $last_rev = $page->revisions->last;
         my $summary = '';
         if ( $show_as eq 'full' ) {
             $summary = $page->viewer->form_field('content')->wiki_content;
         }
         elsif ( $show_as eq 'diff' or $show_as eq 'diffs' ) {
-            $summary = '<pre>'. $page->revisions->last->diff_from .'</pre>';
+            $summary = {
+                content => $last_rev->diff_from,
+                type => 'xhtml',
+            };
         }
 
         $feed->add_entry(
-            id      => 'urn:uuid:' . Data::UUID->new->create_str(),
-            link    => Jifty->web->url . '/view/' . $page->name,
             title   => $page->name,
+            link    => Jifty->web->url . '/view/' . $page->name .'/'. $last_rev->id,
             author  => $page->updated_by->friendly_name,
             updated => $page->updated,
             summary => $summary,

commit 1e751baa9e0db298f0357663bcb54a5e9e9342f5
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Fri Dec 19 23:02:18 2008 +0000

    update config
    ** if we have log4perl.conf in repo then we should use it
    ** describe View:*
    ** no more dependecy on SkelletonApp
    ** path to po files

diff --git a/etc/config.yml b/etc/config.yml
index 7f93d3a..1760bf1 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -1,20 +1,25 @@
 ---
 framework:
   ConfigFileVersion: 4
-
-  AdminMode: 0
+  
   ApplicationName: Wifty
   AdminEmail: 'wifty at example.com'
 
+  AdminMode: 0
+  DevelMode: 1
+  LogConfig: etc/log4perl.conf
+
   Database:
+    AutoUpgrade: 1
+    CheckSchema: 1
     Driver: SQLite
     Host: localhost
     User: postgres
     Version: 0.0.21
     Password: ''
     RequireSSL: 0
+
   Plugins:
-    - SkeletonApp: {}
     - CompressedCSSandJS: {}
     - User: {}
     - Authentication::Password:
@@ -26,6 +31,15 @@ framework:
     - %log/mail.log%
   SiteConfig: etc/site_config.yml
 
+  L10N: 
+    PoDir: share/po
+
+  View: 
+    FallbackHandler: Jifty::View::Declare::Handler
+    Handlers: 
+      - Jifty::View::Static::Handler
+      - Jifty::View::Declare::Handler
+
   Web:
     StaticRoot: share/web/static
     TemplateRoot: share/web/templates

commit 5e7abdaac58134b8d191a82672e1389361e448e4
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 20 00:33:49 2008 +0000

    return back description for users that can not edit pages

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index b03364a..fdf4779 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -31,8 +31,12 @@ template 'edit' => page {
 
     form { div { attr { class is 'form_wrapper' };
         div { attr { class is 'inline' };
-            unless ( $can_edit ) {
-            }
+            unless ( $can_edit ) { p {
+                outs(_("You don't have permission to edit this page."));
+                outs(' '. _("Perhaps logging in would help."));
+                outs(' '. _("In the mean time, though, you're welcome to view and copy the source of this page."). ' ');
+                tangent(url => '/login', label => _('Login'));
+            } }
             form_next_page url => '/view/'.$page->name;
             render_action $viewer, ['content'];
         };

commit f205047021dac03ddf8dafa9f2cbbf6124b6d59f
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 20 00:42:06 2008 +0000

    'attr is ...' inside 'attr {}' makes no sense, prefer attr
    * add back style="wifth: 70%"

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index fdf4779..8d94321 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -29,9 +29,9 @@ template 'edit' => page {
 
     show('markup');
 
-    form { div { attr { class is 'form_wrapper' };
-        div { attr { class is 'inline' };
-            unless ( $can_edit ) { p {
+    form { div { attr { class => 'form_wrapper' };
+        div { attr { class => 'inline' };
+            unless ( $can_edit ) { p { attr { style => "width: 70%" };
                 outs(_("You don't have permission to edit this page."));
                 outs(' '. _("Perhaps logging in would help."));
                 outs(' '. _("In the mean time, though, you're welcome to view and copy the source of this page."). ' ');
@@ -41,7 +41,7 @@ template 'edit' => page {
             render_action $viewer, ['content'];
         };
         if ( $can_edit ) {
-            div { attr { class is 'line' };
+            div { attr { class => 'line' };
                 form_submit label => _('Save')
             }
         }
@@ -57,7 +57,7 @@ template create => page {
     div {
         show('markup');
 
-        form { div { attr { class is 'form_wrapper' };
+        form { div { attr { class => 'form_wrapper' };
             form_next_page url => '/view/' . $page;
             render_param $action, 'name',
                 render_as => 'hidden',

commit f7d6ffee727b0f00b8bee30a9f02a3bc3041035a
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Dec 20 01:09:01 2008 +0000

    use __set method in Upgrade script to avoid updating 'updated'
      and 'upgrated_by' columns in _set method during upgrade

diff --git a/lib/Wifty/Upgrade.pm b/lib/Wifty/Upgrade.pm
index b59f1f2..244445b 100644
--- a/lib/Wifty/Upgrade.pm
+++ b/lib/Wifty/Upgrade.pm
@@ -14,10 +14,18 @@ since '0.0.21' => sub {
 
     while ( my $page = $pages->next ) {
         my $first_rev = $page->revisions->first;
-        my ($status, $msg) = $page->set_created( $first_rev? $first_rev->created : $page->updated );
-        Jifty->log->error("Couldn't set created:". $msg) unless $status;
-        ($status, $msg) = $page->set_created_by( $first_rev? $first_rev->created_by : $page->updated_by );
-        Jifty->log->error("Couldn't set created_by:". $msg) unless $status;
+        my $created = $first_rev? $first_rev->created : $page->updated;
+        if ( $created ) {
+            my ($status, $msg) = $page->__set( column => 'created', value => $created );
+            Jifty->log->error("Couldn't set created:". $msg)
+                unless $status;
+        }
+        my $created_by = ( $first_rev? $first_rev->created_by : $page->updated_by )->id;
+        if ( $created_by ) {
+            my ($status, $msg) = $page->__set( column => 'created_by', value => $created_by );
+            Jifty->log->error("Couldn't set created_by:". $msg)
+                unless $status;
+        }
     }
 };
 

commit 1e99679cdcc56fc2d8ce529c1c02f691aafb2eb6
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Dec 23 02:05:55 2008 +0000

    we never used scrubber, delete unused code

diff --git a/lib/Wifty/Form/Field/WikiPage.pm b/lib/Wifty/Form/Field/WikiPage.pm
index fcc1760..715537d 100644
--- a/lib/Wifty/Form/Field/WikiPage.pm
+++ b/lib/Wifty/Form/Field/WikiPage.pm
@@ -47,21 +47,6 @@ Wikify this field's C<current_value>
 sub wiki_content {
     my $self     = shift;
     my $content  = $self->current_value;
-    my $scrubber = HTML::Scrubber->new();
-
-    $scrubber->default(
-        0,
-        {   '*'   => 0,
-            id    => 1,
-            class => 1,
-            href  => qr{^(?:(?:\w+$)|http:|ftp:|https:|\.?/)}i,
-
-            # Match http, ftp and relative urls
-            face   => 1,
-            size   => 1,
-            target => 1
-        }
-    );
 
     $content =~ s/(?:\n\r|\r\n|\r)/\n/g;
 
@@ -71,16 +56,14 @@ sub wiki_content {
     $scrubber->comment(0);
 
     if (Jifty->config->app('Formatter') eq 'Markdown' ) {
-            require Text::Markdown;
-            $content = Text::Markdown::markdown( $content );
+        require Text::Markdown;
+        $content = Text::Markdown::markdown( $content );
     }
     elsif (Jifty->config->app('Formatter') eq 'Kwiki') {
         require Text::KwikiFormatish;
-        $content = Text::KwikiFormatish::format( $content);
+        $content = Text::KwikiFormatish::format( $content );
     }
-    #$content = $scrubber->scrub( $content );
     return ( $content );
-
 }
 
 =head2 rows

commit 8e09eb0320516f77d3180a6ac76b59c2fc460e96
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue Dec 23 02:06:49 2008 +0000

    delete all notes on scrubber

diff --git a/lib/Wifty/Form/Field/WikiPage.pm b/lib/Wifty/Form/Field/WikiPage.pm
index 715537d..43f4b03 100644
--- a/lib/Wifty/Form/Field/WikiPage.pm
+++ b/lib/Wifty/Form/Field/WikiPage.pm
@@ -50,11 +50,6 @@ sub wiki_content {
 
     $content =~ s/(?:\n\r|\r\n|\r)/\n/g;
 
-    $scrubber->deny(qw[*]);
-    $scrubber->allow(
-        qw[H1 H2 H3 H4 H5 A STRONG EM CODE PRE B U P BR I HR BR SPAN DIV UL OL LI DL DT DD]);
-    $scrubber->comment(0);
-
     if (Jifty->config->app('Formatter') eq 'Markdown' ) {
         require Text::Markdown;
         $content = Text::Markdown::markdown( $content );

commit b13470ea8143145d7b6b7eed778bf53d0fbfa4b2
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Dec 24 17:54:33 2008 +0000

    generate url once

diff --git a/lib/Wifty/View/Feeds.pm b/lib/Wifty/View/Feeds.pm
index 971653f..30b79d5 100644
--- a/lib/Wifty/View/Feeds.pm
+++ b/lib/Wifty/View/Feeds.pm
@@ -34,9 +34,10 @@ private template 'pages_links' => sub {
 template 'atom/pages' => sub {
     my ($pages, $title, $show_as) = get(qw(pages title show_as));
     $show_as ||= 'headlines';
+
+    my $url = Jifty->web->url;
     my $feed = XML::Atom::SimpleFeed->new(
-        title   => $title,
-        link    => Jifty->web->url,
+        title => $title, link => $url
     );
 
     while ( my $page = $pages->next ) {
@@ -54,7 +55,7 @@ template 'atom/pages' => sub {
 
         $feed->add_entry(
             title   => $page->name,
-            link    => Jifty->web->url . '/view/' . $page->name .'/'. $last_rev->id,
+            link    => $url . '/view/' . $page->name .'/'. $last_rev->id,
             author  => $page->updated_by->friendly_name,
             updated => $page->updated,
             summary => $summary,

commit 97f18fe1b3296afd4521c90a8d19b2047f392fe7
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Wed Dec 24 17:56:21 2008 +0000

    send HTTP headers manually :(

diff --git a/lib/Wifty/View/Feeds.pm b/lib/Wifty/View/Feeds.pm
index 30b79d5..aacf8f7 100644
--- a/lib/Wifty/View/Feeds.pm
+++ b/lib/Wifty/View/Feeds.pm
@@ -35,6 +35,11 @@ template 'atom/pages' => sub {
     my ($pages, $title, $show_as) = get(qw(pages title show_as));
     $show_as ||= 'headlines';
 
+    # XXX: investigation required. Why Jifty doesn't send HTTP headers
+    # for us?
+    Jifty->handler->apache->content_type('application/atom+xml');
+    Jifty->handler->send_http_header;
+
     my $url = Jifty->web->url;
     my $feed = XML::Atom::SimpleFeed->new(
         title => $title, link => $url

commit bd8e63900bef7b5915a41716700ce62de1c957af
Author: Thomas Sibley <trs at bestpractical.com>
Date:   Thu Dec 25 00:21:24 2008 +0000

    Use at least a little bit of text to differentiate the feed types

diff --git a/lib/Wifty/View/Page.pm b/lib/Wifty/View/Page.pm
index 448e157..504142b 100644
--- a/lib/Wifty/View/Page.pm
+++ b/lib/Wifty/View/Page.pm
@@ -72,12 +72,14 @@ sub render_link_inpage {
     my $self = shift;
     my %link = @_;
     if ( ($link{rel}||'') eq 'alternate' && ($link{type}||'') eq 'application/atom+xml' ) {
+        my ($type) = $link{'href'} =~ m{/(\w+)$};
         a { attr { href => $link{'href'} };
             img { attr {
                 src => '/static/images/feed-icon-14x14.png',
                 width => 14, heigth => 14,
                 title => $link{'title'}
-            } }
+            } };
+            outs(" " . ucfirst $type);
         }
     }
     return '';

commit 86d952defa24dcf03913f931161c6676e92cf918
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sun Dec 28 23:43:31 2008 +0000

    render diff for feeds as text and wrap into pre

diff --git a/lib/Wifty/View/Feeds.pm b/lib/Wifty/View/Feeds.pm
index aacf8f7..d422353 100644
--- a/lib/Wifty/View/Feeds.pm
+++ b/lib/Wifty/View/Feeds.pm
@@ -53,7 +53,10 @@ template 'atom/pages' => sub {
         }
         elsif ( $show_as eq 'diff' or $show_as eq 'diffs' ) {
             $summary = {
-                content => $last_rev->diff_from,
+                content =>
+                    '<pre>'. Jifty->web->escape( 
+                        $last_rev->diff_from( undef, STYLE => 'Text::Diff::Unified' )
+                    ) .'</pre>',
                 type => 'xhtml',
             };
         }

commit e134a8619c5ddb15ce65f9844752f924a1b48e4a
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Sat Jan 3 22:04:35 2009 +0000

    use prefix '/view/', however to make it work we need a new
      release of the kwiki formatter:
      https://rt.cpan.org/Public/Bug/Display.html?id=42090

diff --git a/lib/Wifty/Form/Field/WikiPage.pm b/lib/Wifty/Form/Field/WikiPage.pm
index 43f4b03..f1805e8 100644
--- a/lib/Wifty/Form/Field/WikiPage.pm
+++ b/lib/Wifty/Form/Field/WikiPage.pm
@@ -55,8 +55,12 @@ sub wiki_content {
         $content = Text::Markdown::markdown( $content );
     }
     elsif (Jifty->config->app('Formatter') eq 'Kwiki') {
+        # XXX: we need a new release of Text::KwikiFormatish
+        # https://rt.cpan.org/Public/Bug/Display.html?id=42090
         require Text::KwikiFormatish;
-        $content = Text::KwikiFormatish::format( $content );
+        $content = Text::KwikiFormatish::format(
+            $content, prefix => '/view/',
+        );
     }
     return ( $content );
 }

commit ee0c988b7ed1ec5c3059fcf7014d895af5a92ad7
Author: Jesse Vincent <jesse at bestpractical.com>
Date:   Mon Jan 26 19:07:49 2009 +0000

    Added a basic robots.txt

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index 8d94321..ed2c6cd 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -7,6 +7,14 @@ use Jifty::View::Declare -base;
 require Wifty::View::Feeds;
 alias Wifty::View::Feeds under 'feeds/';
 
+template 'robots.txt' => sub {
+outs_raw('User-agent: *
+Disallow: /history
+Disallow: /search
+');
+
+};
+
 template 'view' => page {
     my ( $page, $revision, $viewer ) = get(qw(page revision viewer));
     page_title is

commit e5d7bf123abf59615de75249f7dd90f589d1b903
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon May 11 17:33:20 2009 +0000

    bump version of Wifty

diff --git a/Makefile.PL b/Makefile.PL
index 6fcb19b..f61b9b4 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -1,6 +1,6 @@
 use inc::Module::Install;
 name('Wifty');
-version('0.01');
+version('0.02');
 requires('Jifty');
 requires('Text::Markdown');
 requires('HTML::Scrubber');

commit 6aa10d42ad51be9860052a9d993512076bcf1a51
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Mon May 11 18:57:43 2009 +0000

    Move to new Template::Declare buffer API

diff --git a/lib/Wifty/View/Page.pm b/lib/Wifty/View/Page.pm
index 504142b..1d33b32 100644
--- a/lib/Wifty/View/Page.pm
+++ b/lib/Wifty/View/Page.pm
@@ -16,9 +16,9 @@ sub render_page {
         };
     }
 
-    Template::Declare->new_buffer_frame;
+    Template::Declare->buffer->push( private => 1 );
     $self->instrument_content;
-    my $content = Template::Declare->end_buffer_frame->data;
+    my $content = Template::Declare->buffer->pop;
 
     div { attr { id is "header" }
         div { attr { id is "wikiheader" }

commit 45043a7d10a0666ab1bad19921fa4cc4e1caf3fd
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Mon May 11 20:19:26 2009 +0000

    add Users' stats I had in the checkout for months

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index c3a9a88..a01a08e 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -11,6 +11,13 @@ before '*', run {
     $top->child( Home   => url => "/",                 label => _("Home") );
     $top->child( Recent => url => "/recent/changes",   label => _("Recent Changes") );
     $top->child( New    => url => "/recent/additions", label => _("New") );
+    if ( Jifty->web->current_user->id ) {
+        $top->child(
+            Stats => url =>
+            "/user/". Jifty->web->escape_uri(Jifty->web->current_user->username),
+            label => _("Stats"),
+        );
+    }
     $top->child( Search => url => "/search",           label => _("Search") );
 };
 
@@ -127,6 +134,14 @@ on qr{^/feeds/atom/recent/(changes|additions)(?:/(full|headlines?|diffs?))?$} =>
     show('/feeds/atom/pages');
 };
 
+on 'user/*' => run {
+    my $user = Wifty::Model::User->load_by_cols( name => URI::Escape::uri_unescape($1) );
+    abort(404) unless $user && $user->id;
+
+    set(user => $user);
+    show('/user/stats');
+};
+
 sub setup_page_nav {
     my ($prefix, $page, $rev) = @_;
 
diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index ed2c6cd..38738d9 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -7,6 +7,9 @@ use Jifty::View::Declare -base;
 require Wifty::View::Feeds;
 alias Wifty::View::Feeds under 'feeds/';
 
+require Wifty::View::Users;
+alias Wifty::View::Users under 'user/';
+
 template 'robots.txt' => sub {
 outs_raw('User-agent: *
 Disallow: /history
@@ -102,7 +105,9 @@ template history => page {
                 label => $rev->created,
                 url   => '/view/' . $page->name . '/' . $rev->id
             );
-            outs( ' (' . $rev->created_by->friendly_name . ')' );
+            outs(' (');
+            user($rev->created_by);
+            outs(')');
             outs( ' ', _('%1 bytes', length $rev->content ) );
             render_region(
                 'revision-'. $rev->id .'-diff',
@@ -228,23 +233,48 @@ private template markup => sub {
 };
 
 private template page_list => sub {
-    my ( $pages, $id ) = get(qw(pages id));
-    dl {{ id is $id, class is "pagelist" }
-        while ( my $page = $pages->next ) {
-            dt {
-                hyperlink(
-                    label => $page->name,
-                    url   => '/view/' . $page->name
-                );
-            };
-            dd {
-                outs( $page->updated );
-                outs( ' - ('. $page->updated_by->friendly_name .')' );
-            };
-        }
+    my ($pages, $id, $hide) = get(qw(pages id hide));
+    my %hide = map {$_ => 1} @{ $hide || [] };
+
+    table { attr { id => $id, class => "pagelist" };
+        unless ( $hide{'header'} ) { row {
+            th {_('Page')};
+            th {_('Updated')} unless $hide{'updated'};
+            th {_('Created')} unless $hide{'created'};
+        } }
+        while ( my $page = $pages->next ) { row {
+            cell { hyperlink(
+                label => $page->name,
+                url   => '/view/' . $page->name
+            ) };
+            unless ( $hide{'updated'} ) {
+                cell { date_user( $page->updated, $page->updated_by ) }
+            }
+            unless ( $hide{'created'} ) {
+                cell { date_user( $page->created, $page->created_by ) }
+            }
+        } }
     };
 };
 
+sub date_user {
+    my ($date, $user) = @_;
+    return outs( _('%1 by %2', $date, $user->friendly_name ) )
+        unless $user->id;
+
+    return a { attr { href => '/user/'. $user->name };
+        _('%1 by %2', $date, $user->friendly_name)
+    }
+}
+
+sub user {
+    my $user = shift;
+    return $user->friendly_name unless $user->id;
+    return a { attr { href => '/user/'. $user->name };
+        $user->friendly_name
+    }
+}
+
 template 'helpers/diff' => sub {
     my ($from, $to, $show) = get(qw(from to show));
     hyperlink
diff --git a/lib/Wifty/View/Users.pm b/lib/Wifty/View/Users.pm
new file mode 100644
index 0000000..998d6e5
--- /dev/null
+++ b/lib/Wifty/View/Users.pm
@@ -0,0 +1,31 @@
+use warnings;
+use strict;
+
+package Wifty::View::Users;
+use Jifty::View::Declare -base;
+
+template stats => page {
+    my ($user) = get('user');
+    page_title is _('Statistics of user %1', $user->friendly_name );
+
+    set(type => 'updated'); show('recently');
+    set(type => 'created'); show('recently');
+};
+
+private template recently => sub {
+    my ($user, $type) = get('user', 'type');
+    
+    my $method = 'recently_'. $type;
+    my $pages = Wifty::Model::PageCollection->$method;
+    $pages->limit( column => $type .'_by', value => $user->id );
+
+    h1 { $type eq 'updated'? _('Recenly updated') : _('Recently created') };
+    set(
+        pages => $pages,
+        id => 'recent-user-updates',
+        hide => [$type],
+    );
+    show('/page_list');
+};
+
+1;

commit 6acd1b80a19a48c0b31a362bf1785dc7b5558893
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 06:15:22 2009 +0000

    add admin column to the users table

diff --git a/lib/Wifty/Model/User.pm b/lib/Wifty/Model/User.pm
index fd1e28f..b7866c0 100644
--- a/lib/Wifty/Model/User.pm
+++ b/lib/Wifty/Model/User.pm
@@ -3,6 +3,12 @@ package Wifty::Model::User;
 use Jifty::DBI::Schema;
 use Wifty::Record schema {
     # column definitions
+    column admin =>
+        type is 'integer',
+        is mandatory,
+        default is 0,
+        since '0.0.22',
+    ;
 };
 
 # import columns: name, email and email_confirmed

commit 165d2dfe90e4eea084ef5cebac22d0d68c4c7550
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 06:18:05 2009 +0000

    add BlackList model

diff --git a/lib/Wifty/Model/BlackList.pm b/lib/Wifty/Model/BlackList.pm
new file mode 100644
index 0000000..2527cbf
--- /dev/null
+++ b/lib/Wifty/Model/BlackList.pm
@@ -0,0 +1,95 @@
+package Wifty::Model::BlackList;
+use warnings;
+use strict;
+
+use List::Compare;
+
+use base qw/Wifty::Record/;
+use Jifty::DBI::Schema;
+use Wifty::Model::User;
+use Wifty::Model::RevisionCollection;
+
+use Jifty::Record schema {
+    column type =>
+        type is 'varchar(32)',
+        label is 'value',
+        is mandatory,
+    ;
+
+    column value =>
+        type is 'varchar(255)',
+        label is 'value',
+        is mandatory,
+    ;
+
+    column created =>
+        type is 'timestamp',
+    ;
+
+    column created_by =>
+        refers_to Wifty::Model::User,
+    ;
+};
+
+sub since { '0.0.23' }
+
+sub create {
+    my $self = shift;
+    my %args = (@_);
+    my $now  = DateTime->now();
+    $args{'created'}    ||= $now->ymd . " " . $now->hms;
+    $args{'created_by'} ||= $self->current_user? $self->current_user->user_object : undef;
+    return $self->SUPER::create(%args);
+}
+
+=head2 current_user_can ACTION
+
+=cut
+
+sub current_user_can {
+    my $self = shift;
+    my $type = shift;
+
+    return 1 if $self->current_user->is_superuser;
+    return 0 unless $self->current_user->id;
+    return 1 if $self->current_user->user_object->admin;
+    return 0;
+}
+
+sub update_list {
+    my $self = shift;
+    my %args = (@_);
+
+    my $current = Jifty->app_class('Model::BlackListCollection')->new;
+    $current->limit( column => 'type', value => $args{'type'} );
+
+    my $values = delete $args{'values'} || [];
+    unless ( @$values ) {
+        while ( my $e = $current->next ) {
+            my ($status, $msg) = $e->delete;
+            return ($status, $msg) unless $status;
+        }
+        return (1, "Done");
+    }
+
+    my $now  = DateTime->now();
+    $args{'created'}    ||= $now->ymd . " " . $now->hms;
+    $args{'created_by'} ||= $self->current_user? $self->current_user->user_object : undef;
+
+    my %current = map { $_->value => $_ } @$current;
+
+    my $lc = List::Compare->new(
+        '--unsorted', $values, [keys %current]
+    );
+    foreach my $e ( map $current{$_}, $lc->get_Ronly ) {
+        my ($status, $msg) = $e->delete;
+        return ($status, $msg) unless $status;
+    }
+    foreach my $e ( $lc->get_Lonly ) {
+        my ($status, $msg) = $self->create( %args, value => $e );
+        return ($status, $msg) unless $status;
+    }
+    return (1, 'Done');
+}
+
+1;

commit 52d7cdda198444b8c1d82472b3e348c8bbd2e1cf
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 06:19:18 2009 +0000

    add EditIPsBlackList action

diff --git a/lib/Wifty/Action/EditIPsBlackList.pm b/lib/Wifty/Action/EditIPsBlackList.pm
new file mode 100644
index 0000000..e03d653
--- /dev/null
+++ b/lib/Wifty/Action/EditIPsBlackList.pm
@@ -0,0 +1,82 @@
+use strict;
+use warnings;
+
+=head1 NAME
+
+Wifty::Action::EditIPsBlackList
+
+=cut
+
+package Wifty::Action::EditIPsBlackList;
+use base qw/Wifty::Action Jifty::Action/;
+
+use Regexp::Common qw(RE_net_IPv4);
+my $re_ip = $RE{net}{IPv4};
+
+use Jifty::Param::Schema;
+use Jifty::Action schema {
+    param 'ips' =>
+        label is 'Block IPs',
+        render as 'Textarea',
+        default is defer {
+            my $list = Jifty->app_class('Model::BlackListCollection')->new;
+            $list->limit( column => 'type', value => 'IP' );
+            $list->order_by({ column => 'value', order => 'asc' });
+            return join "\n", map $_->value, @$list;
+        },
+    ;
+};
+
+sub canonicalize_ips {
+    my $self = shift;
+    my $ips = shift;
+
+    my @ips;
+    my @not_ips;
+
+    foreach my $part ( grep /\S/, split /[^0-9.]+/, $ips ) {
+        unless ( $part =~ /^$re_ip$/ ) {
+            push @not_ips, $part;
+        } else {
+            push @ips, $part;
+        }
+    }
+    
+    $self->canonicalization_note('ips' => "Some values have been dropped as don't look like IP")
+        if @not_ips;
+
+    return \@ips;
+}
+
+=head2 take_action
+
+=cut
+
+sub take_action {
+    my $self = shift;
+
+    my $ips = $self->argument_value('ips');
+
+    my ($status, $msg) = Jifty->app_class('Model::BlackList')->new->update_list(
+        type   => 'IP',
+        values => $ips,
+    );
+    Jifty->log->error("$status $msg");
+    return $self->result->error( $msg )
+        unless $status;
+
+    return $self->report_success;
+}
+
+=head2 report_success
+
+=cut
+
+sub report_success {
+    my $self = shift;
+    # Your success message here
+    $self->result->message('Success');
+}
+
+1;
+

commit af2911a6f942d18cd618a37ae9d6ef2da15c7527
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 06:22:32 2009 +0000

    extend behaviour of current_user_can in array context
    * fail the check if IP is blacklisted

diff --git a/lib/Wifty/Model/Page.pm b/lib/Wifty/Model/Page.pm
index 4a53260..7b45dea 100644
--- a/lib/Wifty/Model/Page.pm
+++ b/lib/Wifty/Model/Page.pm
@@ -144,10 +144,17 @@ sub current_user_can {
     my $type = shift;
 
     if ($type eq 'create' || $type eq 'update') {
-        return 0 if
+        return wantarray? (0, 'require_auth'): 0 if
          Jifty->config->app('RequireAuth')
            && !$self->current_user->is_superuser
            && !$self->current_user->id;
+
+        if ( my $ip = $ENV{'REMOTE_HOST'} ) {
+            my $block = Jifty->app_class('Model::BlackList')->load_by_cols(
+                type => 'IP', value => $ip
+            );
+            return wantarray? (0, 'black_ip'): 0 if $block && $block->id;
+        }
         return 1;
     } elsif($type eq 'read') {
         return 1;

commit c2a50922419ccb9b2c0b17d0ddd87eb1e2cab9e8
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 06:23:39 2009 +0000

    add /admin path

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index 38738d9..7f64afb 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -28,6 +28,14 @@ template 'view' => page {
     render_param($viewer => 'content', label => '', render_mode => 'read');
 };
 
+template 'admin' => page {
+    page_title is 'Admin wiki';
+    form {
+        render_action( new_action( class => 'EditIPsBlackList') );
+        form_submit(label => _("Update"));
+    }
+};
+
 template 'edit' => page {
     my ( $page, $revision, $viewer ) = get(qw(page revision viewer));
 

commit 59a57fbfd6a95e30bfcdc2f37ab40273544afb91
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 06:24:46 2009 +0000

    dispatch /admin, mention in the menu

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index a01a08e..e06820a 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -1,6 +1,9 @@
 package Wifty::Dispatcher;
 use Jifty::Dispatcher -base;
 
+use strict;
+use warnings;
+
 # Generic restrictions
 under '/', run {
     Jifty->api->deny('ConfirmEmail');
@@ -11,16 +14,26 @@ before '*', run {
     $top->child( Home   => url => "/",                 label => _("Home") );
     $top->child( Recent => url => "/recent/changes",   label => _("Recent Changes") );
     $top->child( New    => url => "/recent/additions", label => _("New") );
-    if ( Jifty->web->current_user->id ) {
+    my $cu = Jifty->web->current_user;
+    if ( $cu->id ) {
         $top->child(
             Stats => url =>
-            "/user/". Jifty->web->escape_uri(Jifty->web->current_user->username),
+            "/user/". Jifty->web->escape_uri($cu->username),
             label => _("Stats"),
         );
+        $top->child(
+            Admin => url => "/admin/", label => _("Administration"),
+        ) if $cu->user_object->admin;
     }
     $top->child( Search => url => "/search",           label => _("Search") );
 };
 
+before qr{^/admin\b}, run {
+    my $cu = Jifty->web->current_user;
+    abort(403) unless $cu->id;
+    abort(403) unless $cu->user_object->admin;
+};
+
 # Default page
 on '/', run {
     redirect( '/view/HomePage');

commit e902df149b53fd95defa6932a72f98c23b2241ca
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 06:31:34 2009 +0000

    redispatch errors

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index e06820a..9e78edb 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -155,6 +155,15 @@ on 'user/*' => run {
     show('/user/stats');
 };
 
+sub error {
+    my ($action, $reason) = @_;
+    foreach my $page ( map { "error/$_" } "$action/$reason", "$reason", "$action", "" ) {
+        next unless $Jifty::Dispatcher::Dispatcher->template_exists($page);
+        show($page);
+        return;
+    }
+}
+
 sub setup_page_nav {
     my ($prefix, $page, $rev) = @_;
 

commit 890ec91db454afd1568c811e38ecf26ea5ce312a
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 06:32:57 2009 +0000

    use new extended current_user_can and error helper to
      issue better errors

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index 9e78edb..d2667b6 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -41,15 +41,16 @@ on '/', run {
 
 # Create a page
 on '/create/*', run {
-     set page => $1;
-     set action => Jifty->web->new_action( class => 'CreatePage' );
-
-     my $p = Wifty::Model::Page->new();
-     if($p->current_user_can('create')) {
-         show("/create");
-     } else {
-         show("/no_such_page");
-     }
+    set page => $1;
+    set action => Jifty->web->new_action( class => 'CreatePage' );
+
+    my $p = Wifty::Model::Page->new;
+    my ($can, $reason) = $p->current_user_can('create');
+    unless ( $can ) {
+        error( create => $reason);
+    } else {
+        show("/create");
+    }
 };
 
 # View or edit a page

commit 4c7e1bebc23117cf0c3a37967b9471ae32bebe20
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 06:35:47 2009 +0000

    show logging in hint only when user is not logged in

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index 7f64afb..3cd0489 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -52,9 +52,11 @@ template 'edit' => page {
         div { attr { class => 'inline' };
             unless ( $can_edit ) { p { attr { style => "width: 70%" };
                 outs(_("You don't have permission to edit this page."));
-                outs(' '. _("Perhaps logging in would help."));
                 outs(' '. _("In the mean time, though, you're welcome to view and copy the source of this page."). ' ');
-                tangent(url => '/login', label => _('Login'));
+                unless ( Jifty->web->current_user->id ) {
+                    outs(' '. _("Perhaps logging in would help."));
+                    tangent(url => '/login', label => _('Login'));
+                }
             } }
             form_next_page url => '/view/'.$page->name;
             render_action $viewer, ['content'];

commit e5b8760b1988b9dca7f2ddfc87870253ca4e5bf1
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 06:38:13 2009 +0000

    move page_not_found error into /error/... space

diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index 3cd0489..4f9b991 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -90,19 +90,6 @@ template create => page {
     };
 };
 
-template no_such_page => page {
-    my ($page) = get(qw(page));
-
-    page_title is _("No '%1' page", $page);
-
-    p { 
-        q{Unfortunately, you've tried to reach a page that doesn't exist }
-        . q{yet, and you don't have permissions to create pages. If you }
-        . tangent( url => '/login', label => 'login' )
-        . q{, you'll be able to create new pages of your own.}
-    }
-};
-
 template history => page {
     my ( $page, $revisions ) = get(qw(page revisions));
     $revisions->do_search; # avoid count+fetch
@@ -353,5 +340,26 @@ private template 'diff/with_nav' => sub {
     hr {}
 };
 
+template 'error/create/require_auth' => page {
+    my ($page) = get(qw(page));
+
+    page_title is _("No '%1' page", $page);
+
+    p { 
+        q{Unfortunately, you've tried to reach a page that doesn't exist }
+        . q{yet, and you don't have permissions to create pages. If you }
+        . tangent( url => '/login', label => 'login' )
+        . q{, you'll be able to create new pages of your own.}
+    }
+};
+
+template 'error/black_ip' => page {
+    page_title is _("You're blacklisted");
+
+    p {
+        q{Unfortunately, your IP address has been blocked.}
+        .q{ You can not change any content on this wiki.}
+    }
+};
 
 1;

commit 166cf4bbf45945e4f175d6973465ea7237622d2d
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 07:25:02 2009 +0000

    update DB version

diff --git a/etc/config.yml b/etc/config.yml
index 1760bf1..9a253f8 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -10,12 +10,12 @@ framework:
   LogConfig: etc/log4perl.conf
 
   Database:
+    Version: 0.0.23
     AutoUpgrade: 1
     CheckSchema: 1
     Driver: SQLite
     Host: localhost
     User: postgres
-    Version: 0.0.21
     Password: ''
     RequireSSL: 0
 

commit 41d53386158b2e38f98bad39005c482db6d19530
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 07:58:40 2009 +0000

    add RevisionCollection with methods abstracted from Revision

diff --git a/lib/Wifty/Model/RevisionCollection.pm b/lib/Wifty/Model/RevisionCollection.pm
new file mode 100644
index 0000000..1474b95
--- /dev/null
+++ b/lib/Wifty/Model/RevisionCollection.pm
@@ -0,0 +1,54 @@
+use strict;
+use warnings;
+
+package Wifty::Model::RevisionCollection;
+use base qw(Jifty::Collection);
+
+use Scalar::Util qw(blessed);
+
+sub limit_by_page {
+    my $self = shift;
+    my $page = shift;
+    if ( blessed $page ) {
+        $page = $page->can('page')? $page->page->id : $page->id;
+    }
+    return $self->limit(
+        @_,
+        column         => 'page',
+        value          => $page,
+        quote_value    => 0,
+        case_sensitive => 1
+    );
+}
+
+sub newer_than {
+    my $self = shift;
+    my $rev = shift;
+    $rev = $rev->id if blessed $rev;
+
+    return $self->limit(
+        @_,
+        column         => 'id',
+        operator       => '>',
+        value          => $rev,
+        quote_value    => 0,
+        case_sensitive => 1
+    );
+}
+
+sub older_than {
+    my $self = shift;
+    my $rev = shift;
+    $rev = $rev->id if blessed $rev;
+
+    return $self->limit(
+        @_,
+        column         => 'id',
+        operator       => '<',
+        value          => $rev,
+        quote_value    => 0,
+        case_sensitive => 1
+    );
+}
+
+1;

commit dfd77a30ce5506d6cb37c54e5613942922b1f2d6
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 08:00:49 2009 +0000

    use new methods in the collection class

diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index 53413bd..c97321b 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -33,7 +33,6 @@ use Wifty::Model::Page;
 
 sub since { '0.0.5' }
 
-
 sub create {
     my $self = shift;
     my %args = (@_);
@@ -49,19 +48,8 @@ sub previous {
     return undef unless $self->id;
 
     my $revisions = Wifty::Model::RevisionCollection->new;
-    $revisions->limit(
-        column         => 'page',
-        value          => $self->page->id,
-        quote_value    => 0,
-        case_sensitive => 1
-    );
-    $revisions->limit(
-        column         => 'id',
-        operator       => '<',
-        value          => $self->id,
-        quote_value    => 0,
-        case_sensitive => 1
-    );
+    $revisions->limit_by_page($self);
+    $revisions->older_than($self);
     $revisions->order_by( { column => 'id', order => 'desc' } );
     $revisions->rows_per_page(1);
     return $revisions->first;
@@ -72,19 +60,8 @@ sub next {
     return undef unless $self->id;
 
     my $revisions = Wifty::Model::RevisionCollection->new;
-    $revisions->limit(
-        column         => 'page',
-        value          => $self->page->id,
-        quote_value    => 0,
-        case_sensitive => 1
-    );
-    $revisions->limit(
-        column         => 'id',
-        operator       => '>',
-        value          => $self->id,
-        quote_value    => 0,
-        case_sensitive => 1
-    );
+    $revisions->limit_by_page($self);
+    $revisions->newer_than($self);
     $revisions->order_by( { column => 'id', order => 'asc' } );
     $revisions->rows_per_page(1);
     return $revisions->first;
@@ -144,6 +121,6 @@ sub current_user_can {
         return 0;
     }
     $self->SUPER::current_user_can($right, @_);
-
 }
+
 1;

commit c79fc28a941f2db13c3420633516f0163f99e9e2
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 08:02:08 2009 +0000

    add ip to revisions

diff --git a/etc/config.yml b/etc/config.yml
index 9a253f8..2dd4db3 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -10,7 +10,7 @@ framework:
   LogConfig: etc/log4perl.conf
 
   Database:
-    Version: 0.0.23
+    Version: 0.0.24
     AutoUpgrade: 1
     CheckSchema: 1
     Driver: SQLite
diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index c97321b..f2bc1e7 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -17,11 +17,15 @@ use Jifty::Record schema {
         render_as 'Wifty::Form::Field::WikiPage'
     ;
     column created =>
-        type is 'timestamp'
+        type is 'timestamp',
     ;
     column created_by =>
         refers_to Wifty::Model::User,
-        since '0.0.20'
+        since '0.0.20',
+    ;
+    column ip =>
+        type is 'varchar(15)',
+        since '0.0.24',
     ;
 };
 
@@ -38,7 +42,8 @@ sub create {
     my %args = (@_);
 
     my $now = DateTime->now();
-    $args{'created'} =  $now->ymd." ".$now->hms;
+    $args{'created'} ||=  $now->ymd." ".$now->hms;
+    $args{'ip'} ||= $ENV{'REMOTE_HOST'};
     $self->SUPER::create(%args);
 
 }

commit 48ab906c6a89ed51ac0099c7b80203201f6db3a6
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue May 12 23:15:57 2009 +0000

    Add List::Compare dep

diff --git a/Makefile.PL b/Makefile.PL
index f61b9b4..1dec409 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -6,5 +6,6 @@ requires('Text::Markdown');
 requires('HTML::Scrubber');
 requires('Text::Diff::HTML');
 requires('XML::Atom::SimpleFeed');
+requires('List::Compare');
 recommends('Text::KwikiFormatish');
 WriteAll;

commit 57cab1bc1e3f85b7b7c810ef2dd2295cd053c480
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 23:19:56 2009 +0000

    jifty still need mason handler for some stuff

diff --git a/etc/config.yml b/etc/config.yml
index 2dd4db3..c8a3ce4 100644
--- a/etc/config.yml
+++ b/etc/config.yml
@@ -39,6 +39,7 @@ framework:
     Handlers: 
       - Jifty::View::Static::Handler
       - Jifty::View::Declare::Handler
+      - Jifty::View::Mason::Handler
 
   Web:
     StaticRoot: share/web/static

commit f3eb1ab4a580ae74586e670e4723b9ff69049484
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 23:21:03 2009 +0000

    explicit return on abort for clarity

diff --git a/lib/Wifty/Dispatcher.pm b/lib/Wifty/Dispatcher.pm
index d2667b6..00c2d98 100644
--- a/lib/Wifty/Dispatcher.pm
+++ b/lib/Wifty/Dispatcher.pm
@@ -30,8 +30,8 @@ before '*', run {
 
 before qr{^/admin\b}, run {
     my $cu = Jifty->web->current_user;
-    abort(403) unless $cu->id;
-    abort(403) unless $cu->user_object->admin;
+    return abort(403) unless $cu->id;
+    return abort(403) unless $cu->user_object->admin;
 };
 
 # Default page

commit 3c154ddf567cd035bd892556898aaba9d361ffe2
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 23:22:14 2009 +0000

    only admins can see IPs
    * show IP in the history

diff --git a/lib/Wifty/Model/Revision.pm b/lib/Wifty/Model/Revision.pm
index f2bc1e7..e1970c9 100644
--- a/lib/Wifty/Model/Revision.pm
+++ b/lib/Wifty/Model/Revision.pm
@@ -121,11 +121,22 @@ C<current_user_can> (which we inherit).
 sub current_user_can {
     my $self = shift;
     my $right = shift;
-    
-    if ($right ne 'read' and not $self->current_user->is_superuser) {
-        return 0;
+    my %args = @_;
+
+    return 1 if $self->current_user->is_superuser;
+
+    if ( $right eq 'read' ) {
+        return 0
+            if $args{'column'}
+            && $args{'column'} eq 'ip'
+            && !(
+                $self->current_user->id
+                && $self->current_user->user_object->admin
+            );
+        return 1;
     }
-    $self->SUPER::current_user_can($right, @_);
+
+    $self->SUPER::current_user_can($right, %args);
 }
 
 1;
diff --git a/lib/Wifty/View.pm b/lib/Wifty/View.pm
index 4f9b991..4af28d8 100644
--- a/lib/Wifty/View.pm
+++ b/lib/Wifty/View.pm
@@ -104,6 +104,9 @@ template history => page {
             );
             outs(' (');
             user($rev->created_by);
+            if ( my $ip = $rev->ip ) { # only admins can see IPs
+                outs( ' - '. $ip );
+            }
             outs(')');
             outs( ' ', _('%1 bytes', length $rev->content ) );
             render_region(

commit 5faf176969022fbb5b2019d9249aca161c392c6b
Author: Ruslan Zakirov <ruz at bestpractical.com>
Date:   Tue May 12 23:23:46 2009 +0000

    announce new requirements in the makefile

diff --git a/Makefile.PL b/Makefile.PL
index 1dec409..5059089 100644
--- a/Makefile.PL
+++ b/Makefile.PL
@@ -7,5 +7,7 @@ requires('HTML::Scrubber');
 requires('Text::Diff::HTML');
 requires('XML::Atom::SimpleFeed');
 requires('List::Compare');
+requires('Regexp::Common');
+requires('Scalar::Util');
 recommends('Text::KwikiFormatish');
 WriteAll;

commit 94e56c4dee5911b28346972be17d675bee181f7e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Mar 19 00:39:28 2010 +0000

    update login tests

diff --git a/t/02-login.t b/t/02-login.t
index 1136987..3dc21bd 100644
--- a/t/02-login.t
+++ b/t/02-login.t
@@ -22,8 +22,8 @@ ok($URL, "Started a test server");
 my $mech = Jifty::Test::WWW::Mechanize->new();
 
 $mech->get_ok($URL, "Got the homepage");
-ok($mech->find_link(text_regex => qr/Sign in/), 'Got the signin link');
-$mech->follow_link_ok(text_regex => qr/Sign in/);
+ok($mech->find_link(text_regex => qr/Login/), 'Got the login link');
+$mech->follow_link_ok(text_regex => qr/Login/);
 
 sub try_login {
     my $mech = shift;
@@ -47,7 +47,7 @@ $mech->content_contains("It doesn't look like there's an account by that name",
 
 # With a blank password
 try_login($mech, 'someuser at localost', '');
-$mech->content_contains('Please fill in this field','Login fails with no password');
+$mech->content_contains("fill in the 'password' field",'Login fails with no password');
 
 # With the wrong password
 try_login($mech, 'someuser at localhost', 'badmemory');
@@ -56,3 +56,4 @@ $mech->content_contains('may have mistyped','Login fails with wrong password');
 # Try a correct login
 try_login($mech, 'someuser at localhost', 'sekrit');
 $mech->content_contains('Welcome back','Logged in');
+

-----------------------------------------------------------------------



More information about the Bps-public-commit mailing list