<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array"/>
  <modified type="array">
    <modified>
      <diff>@@ -1,16 +1,16 @@
-TRUNK
-  - builder is building 'cruise', 'test' or 'default' target, depending on what is defined in the project
-  - when build is running, OS variable CC_BUILD_ARTIFACTS is pointing to the build artifacts directory
-  - Bugfix: explicitly require 'rubygems' before trying to require 'rake', do not rely on RUBYOPT=-rubygems to be set.
-  - customizable Rake task and build command
---------------------------------------------------------------------------------
-* 0.2
-  - Unified command-line interface to web app, builder and add_project script
-  - Default port changed to 3333 (to avoid clashing with other Rails apps)
-  - Bugfix: builder invoking Rake with RAILS_ENV OS variable set to 'builder'
-  - Bugfix: not all Windows installations have rake.cmd
-  - Bugfix: CC.rb trying to build itself when a project has no Rakefile
-  
---------------------------------------------------------------------------------
-* 0.1 
-  - Initial release.
+TRUNK
+  - builder is building 'cruise', 'test' or 'default' target, depending on what is defined in the project
+  - when build is running, OS variable CC_BUILD_ARTIFACTS is pointing to the build artifacts directory
+  - Bugfix: explicitly require 'rubygems' before trying to require 'rake', do not rely on RUBYOPT=-rubygems to be set.
+  - customizable Rake task and build command
+--------------------------------------------------------------------------------
+* 0.2
+  - Unified command-line interface to web app, builder and add_project script
+  - Default port changed to 3333 (to avoid clashing with other Rails apps)
+  - Bugfix: builder invoking Rake with RAILS_ENV OS variable set to 'builder'
+  - Bugfix: not all Windows installations have rake.cmd
+  - Bugfix: CC.rb trying to build itself when a project has no Rakefile
+  
+--------------------------------------------------------------------------------
+* 0.1 
+  - Initial release.</diff>
      <filename>CHANGELOG</filename>
    </modified>
    <modified>
      <diff>@@ -1,201 +1,201 @@
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      &quot;License&quot; shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      &quot;Licensor&quot; shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      &quot;Legal Entity&quot; shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      &quot;control&quot; means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      &quot;You&quot; (or &quot;Your&quot;) shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      &quot;Source&quot; form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      &quot;Object&quot; form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      &quot;Work&quot; shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      &quot;Derivative Works&quot; shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      &quot;Contribution&quot; shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, &quot;submitted&quot;
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as &quot;Not a Contribution.&quot;
-
-      &quot;Contributor&quot; shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a &quot;NOTICE&quot; text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an &quot;AS IS&quot; BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets &quot;[]&quot;
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same &quot;printed page&quot; as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
-   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 &quot;AS IS&quot; 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.
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      &quot;License&quot; shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      &quot;Licensor&quot; shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      &quot;Legal Entity&quot; shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      &quot;control&quot; means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      &quot;You&quot; (or &quot;Your&quot;) shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      &quot;Source&quot; form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      &quot;Object&quot; form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      &quot;Work&quot; shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      &quot;Derivative Works&quot; shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      &quot;Contribution&quot; shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, &quot;submitted&quot;
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as &quot;Not a Contribution.&quot;
+
+      &quot;Contributor&quot; shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a &quot;NOTICE&quot; text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an &quot;AS IS&quot; BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets &quot;[]&quot;
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same &quot;printed page&quot; as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);
+   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 &quot;AS IS&quot; 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.</diff>
      <filename>LICENSE</filename>
    </modified>
    <modified>
      <diff>@@ -1,226 +1,226 @@
-== CruiseControl.rb
-
-=== What's this?
-CruiseControl.rb is a continuous integration tool. Its basic purpose in life is to alert members of a software project 
-when one of them checks into a source control system something that breaks the build.
-
-CC.rb is easy to install, pleasant to use and simple to hack. It's written in Ruby.
-
-
-=== The Concept
-
-See Martin Fowler's article about Continuous Integration: http://martinfowler.com/articles/continuousIntegration.html
-
-
-=== Basics
-
-CC.rb consists of a builder and a dashboard. 
-
-Builder is a daemon that polls source control every once in a while for new revisions.
-
-When someone performs a check in, the builder detects it, updates its own copy of the project, runs the build and
-notifies interested parties about build outcome.
-
-Dashboard is a web application to monitor and troubleshoot build status of projects.
-
-Each installation of CC.rb may have multiple projects, and multiple builders (one per project). There may also be
-multiple installations of CC.rb per computer.
-
-
-=== Prerequisites
-
-* Ruby 1.8.4 or later
-* Subversion client 1.3.2 or later
-* svn and ruby executables must both be on the PATH.
-
-
-=== Assumptions
-
-* CC.rb currently only works with Subversion.
-* To build your_project, builder goes to ~cruise/builds/your_project/work/ and executes Rake, trying to build 
-  'cruise', 'test' or 'default' target, whichever is defined (see &quot;What will it build?&quot; section for details).
-  We realize that this assumption is too narrow, and plan to allow customizable build command soon.
-
-
-=== Installation
-
-1. Download and unpack CruiseControl.rb (we'll call the place you unpack it &lt;cruise&gt;)
-
-2. From &lt;cruise&gt;, run &quot;cruise start&quot;.
-
-3. Browse to http://localhost:3333. If the dashboard is running fine and dandy, you will see a page with
-   CruiseControl.rb logo.
-
-4. Run &quot;cruise add your_project --url &lt;URL of your_project Subversion trunk&gt;&quot;. Optionally, you can specify username 
-   and password by adding &quot; --username &lt;your_user&gt; --password &lt;your_password&gt;&quot; to the command. This creates a directory 
-   for project's builds at &lt;cruise&gt;/builds/&lt;your_project&gt;/, and checks out your_project from subversion URL specified 
-   to &lt;cruise&gt;/builds/&lt;your_project&gt;/work/. 
-
-   Hint: Rakefile of your_project should be in &lt;cruise&gt;/builds/&lt;your_project&gt;/work/ directory, not anywhere under 
-   it. A common mistake is to specify in --url option the root of project's SVN repository instead of the trunk. 
-   Rakefile then ends up in &lt;cruise&gt;/builds/&lt;your_project&gt;/work/trunk/ and CC.rb does not see it there.
-
-5. Refresh the dashboard. It should now display your_project, with status &quot;Never built&quot;.
-
-6. Go to &lt;cruise&gt;/builds/&lt;your_project&gt;/work/ and make the build work. For a regular Rails app, this involves creating 
-   test database, editing database.yml, performing &quot;rake RAILSENV=test db:migrate&quot;
-   and making sure that &quot;rake test&quot; passes.
-
-7. Go back to &lt;cruise&gt; and run &quot;cruise build your_project&quot;. This should build your_project, place build outputs into
-   &lt;cruise&gt;/builds/&lt;your_project&gt;/build-&lt;revision-number&gt;/
- 
-   Hint: Monitor log/your_project_builder.log for any signs of trouble. Try to check in a change to your_project
-   and see if builder can detect and build it.  Check your_project status in the dashboard.
-
-   Hint: If you want to run your server on a different port, just use &quot;cruise start -p &lt;port&gt;&quot;. 
-
-   Hint: &quot;cruise help&quot; displays a list of commands available, &quot;cruise help &lt;command&gt;&quot; displays options for each command.
-
-
-=== Files and folders
-
-If CC.rb is unpacked into &lt;cruise&gt; directory, then:
-
-  &lt;cruise&gt;/builds/ is the projects directory.
-
-  &lt;cruise&gt;/builds/your_project/ is a directory for project called &quot;your_project&quot;.
-
-  &lt;cruise&gt;/builds/your_project/work/ is a local copy of the your_project's source code. Builder keeps it up to date 
-  with the source control repository and runs builds against it.
-
-    &lt;cruise&gt;/builds/your_project/build-123/ contains build status file, list of changed files, 
-    and other &quot;build artifacts&quot; created while building revision 123.
-    
-    &lt;cruise&gt;/builds/your_project/project_config.rb is builder configuration for your_project.
-
-  &lt;cruise&gt;/config/site_config.rb is the file where you can make centralized changes to the configuration of dashboard 
-  and all builders.
-
-
-=== What will it build by default?
-
-CC.rb assumes that your project is built by Rake, and Rakefile is in the root directory of the project. Just running 
-'default' Rake task is not good enough, because in a standard Rails application this task needs an up-to-date 
-development database to work. On a continuous build server there is usually no such thing. Therefore, CC.rb uses the 
-following logic to determine what to build:
-
-1. First, CC.rb loads all *.rake files from &lt;cruise&gt;/tasks/, and then the Rakefile of your_project. Then it invokes 
-   cc:build task (defined in &lt;cruise&gt;/tasks/cc_build.rake). That task sets RAILS_ENV to 'test' 
-   (unless your Rakefile already set it to something else), looks at other Rake tasks loaded, and 
-   decides what to do.
-
-2. If there is 'cruise' target, it simply invokes that target.
-
-3. If there is no 'cruise' target, then it will try to prepare the test database by executing 'db:test:purge'
-   and 'db:migrate' tasks (if they are defined in your build), then calling 'test' target.
-
-4. Finally, if there is no 'test' target, CC.rb will try to invoke 'default' target.
-
-Hint: if you define 'cruise' task in your_project, you should make it dependent on 'db:test:purge' and 
-'db:migrate', and/or whatever is necessary to bring test environment up to date.
-
-
-=== How to change what the build does?
-'cruise' may be the task for a quick build, but you may also want to run a long build with all acceptance tests 
-included. This can be done by assigning project.rake_task attribute in &lt;cruise&gt;/builds/your_project/project_config.rb:
-
-  Project.configure do |project|
-    ...
-    project.rake_task = 'Big_Bertha_build'
-    ...
-  end
-
-Or you may not want to deal with Rake at all, but build your project by shell script, make or Ant (yes, 
-CC.rb can cope with a Java project). This can be done by assigning project.build_command attribute in the 
-same &lt;cruise&gt;/builds/your_project/project_config.rb file, like this:
-  Project.configure do |project|
-    ...
-    project.build_command = 'my_build_script.sh'
-    ...
-  end
-
-Just like with Rake, CC.rb will change current working directory to &lt;cruise&gt;/builds/your_project/work/, invoke 
-specified command and look at the exit code to determine whether the build passed or failed.
-
-Note that you cannot specify both rake_task and build_command attributes project_config.rb. It doesn't make sense, 
-anyway.
-
-=== What should I do with custom build artifacts?
-
-  your_project may have a special build task, producing some output that you want to keep. Code coverage analysis is a 
-  good example. Before running the build, CC.rb sets OS variable CC_BUILD_ARTIFACTS to the directory where the build artifacts are 
-  collected. Make sure that your special task writes its output to that directory.
-  
-  Displaying custom task outputs on the dashboard is not implemented yet.
-  
-
-=== Email notification
-
-CruiseControl.rb can send email notices whenever build fails or is fixed. To make it happen, you need to tell it how 
-to send email, and who to send it to. Do the following:
-
-1. Configure SMTP server connection. Copy &lt;cruise&gt;/config/site_config.rb_example to ~cruise/config/site_config.rb, 
-   read it and edit according to your situation.
-
-2. Tell the builder, whom do you want to receive build notices. Copy ~cruise/config/project_config.rb_example to 
-   ~cruise/builds/your_project/project_config.rb and edit the list of email addresses there.
-
-
-=== Build scheduling
-
-By default, the builder polls Subversion every 10 seconds for new revisions. This can be changed by adding the 
-following line to the ~cruise/builds/your_project/project_config.rb:
-
-  Project.configure do |project|
-    ...
-    project.scheduler.polling_interval = 5.minutes
-    ...
-  end
-
-What if you want a scheduler with some interesting logic. Well, a default scheduler can be substituted by placing 
-your own scheduler implementation intpo the plugins directory and writing in 
-~cruise/builds/your_project/project_config.rb something like this:
-
-  Project.configure do |project|
-    ...
-    project.scheduler = MyCustomScheduler.new(project)
-    ...
-  end
-
-After initializing everything, and loading the project (step that includes evaluation of project_config.rb), the
-builder simply invokes project.scheduler.run. Look at app/models/polling_scheduler.rb to get the idea how 
-should scheduler interact with the project. 
-
-Beware that dashboard overwrites project_config.rb when you make changes to project configuration via web interface. 
-To play nicely with that, your custom scheduler must implement a memento() method, that 
-should return a line like this: &quot;  project.scheduler = MyCustomScheduler.new(project)&quot;. Again, look at 
-app/models/polling_scheduler.rb for the inspiration.
-
-=== Troubleshooting and support
-
-Beware, at the time of this writing, CC.rb is very young and not very stable. Good news is that it's simple (much, much 
-simpler than other CruiseControl incarnations). The dashboard is just a small Rails app, and the builder is little 
-more than a dumb, single-threaded Ruby script. Therefore, it's easy to debug. So, you are your own support hotline. 
-Don't forget to send us patches, please!
-
-OK, that was the pep talk. If you have an issue that you cannot fix on your own, email Alex or Jeremy.
-
-
-===Download the latest release from:
-
-* http://rubyforge.org/frs/?group_id=2918
-
-
-===Project web site
-
-* http://rubyforge.org/projects/cruisecontrolrb/
-
-
-===License:
-
-* Apache Software License 2.0
-
----
-Authors::
-* Alexey Verkhovsky, Jeremy Stell-Smith, Stephen Chu
-* (C) ThoughtWorks 2007
+== CruiseControl.rb
+
+=== What's this?
+CruiseControl.rb is a continuous integration tool. Its basic purpose in life is to alert members of a software project 
+when one of them checks into a source control system something that breaks the build.
+
+CC.rb is easy to install, pleasant to use and simple to hack. It's written in Ruby.
+
+
+=== The Concept
+
+See Martin Fowler's article about Continuous Integration: http://martinfowler.com/articles/continuousIntegration.html
+
+
+=== Basics
+
+CC.rb consists of a builder and a dashboard. 
+
+Builder is a daemon that polls source control every once in a while for new revisions.
+
+When someone performs a check in, the builder detects it, updates its own copy of the project, runs the build and
+notifies interested parties about build outcome.
+
+Dashboard is a web application to monitor and troubleshoot build status of projects.
+
+Each installation of CC.rb may have multiple projects, and multiple builders (one per project). There may also be
+multiple installations of CC.rb per computer.
+
+
+=== Prerequisites
+
+* Ruby 1.8.4 or later
+* Subversion client 1.3.2 or later
+* svn and ruby executables must both be on the PATH.
+
+
+=== Assumptions
+
+* CC.rb currently only works with Subversion.
+* To build your_project, builder goes to ~cruise/builds/your_project/work/ and executes Rake, trying to build 
+  'cruise', 'test' or 'default' target, whichever is defined (see &quot;What will it build?&quot; section for details).
+  We realize that this assumption is too narrow, and plan to allow customizable build command soon.
+
+
+=== Installation
+
+1. Download and unpack CruiseControl.rb (we'll call the place you unpack it &lt;cruise&gt;)
+
+2. From &lt;cruise&gt;, run &quot;cruise start&quot;.
+
+3. Browse to http://localhost:3333. If the dashboard is running fine and dandy, you will see a page with
+   CruiseControl.rb logo.
+
+4. Run &quot;cruise add your_project --url &lt;URL of your_project Subversion trunk&gt;&quot;. Optionally, you can specify username 
+   and password by adding &quot; --username &lt;your_user&gt; --password &lt;your_password&gt;&quot; to the command. This creates a directory 
+   for project's builds at &lt;cruise&gt;/builds/&lt;your_project&gt;/, and checks out your_project from subversion URL specified 
+   to &lt;cruise&gt;/builds/&lt;your_project&gt;/work/. 
+
+   Hint: Rakefile of your_project should be in &lt;cruise&gt;/builds/&lt;your_project&gt;/work/ directory, not anywhere under 
+   it. A common mistake is to specify in --url option the root of project's SVN repository instead of the trunk. 
+   Rakefile then ends up in &lt;cruise&gt;/builds/&lt;your_project&gt;/work/trunk/ and CC.rb does not see it there.
+
+5. Refresh the dashboard. It should now display your_project, with status &quot;Never built&quot;.
+
+6. Go to &lt;cruise&gt;/builds/&lt;your_project&gt;/work/ and make the build work. For a regular Rails app, this involves creating 
+   test database, editing database.yml, performing &quot;rake RAILSENV=test db:migrate&quot;
+   and making sure that &quot;rake test&quot; passes.
+
+7. Go back to &lt;cruise&gt; and run &quot;cruise build your_project&quot;. This should build your_project, place build outputs into
+   &lt;cruise&gt;/builds/&lt;your_project&gt;/build-&lt;revision-number&gt;/
+ 
+   Hint: Monitor log/your_project_builder.log for any signs of trouble. Try to check in a change to your_project
+   and see if builder can detect and build it.  Check your_project status in the dashboard.
+
+   Hint: If you want to run your server on a different port, just use &quot;cruise start -p &lt;port&gt;&quot;. 
+
+   Hint: &quot;cruise help&quot; displays a list of commands available, &quot;cruise help &lt;command&gt;&quot; displays options for each command.
+
+
+=== Files and folders
+
+If CC.rb is unpacked into &lt;cruise&gt; directory, then:
+
+  &lt;cruise&gt;/builds/ is the projects directory.
+
+  &lt;cruise&gt;/builds/your_project/ is a directory for project called &quot;your_project&quot;.
+
+  &lt;cruise&gt;/builds/your_project/work/ is a local copy of the your_project's source code. Builder keeps it up to date 
+  with the source control repository and runs builds against it.
+
+    &lt;cruise&gt;/builds/your_project/build-123/ contains build status file, list of changed files, 
+    and other &quot;build artifacts&quot; created while building revision 123.
+    
+    &lt;cruise&gt;/builds/your_project/project_config.rb is builder configuration for your_project.
+
+  &lt;cruise&gt;/config/site_config.rb is the file where you can make centralized changes to the configuration of dashboard 
+  and all builders.
+
+
+=== What will it build by default?
+
+CC.rb assumes that your project is built by Rake, and Rakefile is in the root directory of the project. Just running 
+'default' Rake task is not good enough, because in a standard Rails application this task needs an up-to-date 
+development database to work. On a continuous build server there is usually no such thing. Therefore, CC.rb uses the 
+following logic to determine what to build:
+
+1. First, CC.rb loads all *.rake files from &lt;cruise&gt;/tasks/, and then the Rakefile of your_project. Then it invokes 
+   cc:build task (defined in &lt;cruise&gt;/tasks/cc_build.rake). That task sets RAILS_ENV to 'test' 
+   (unless your Rakefile already set it to something else), looks at other Rake tasks loaded, and 
+   decides what to do.
+
+2. If there is 'cruise' target, it simply invokes that target.
+
+3. If there is no 'cruise' target, then it will try to prepare the test database by executing 'db:test:purge'
+   and 'db:migrate' tasks (if they are defined in your build), then calling 'test' target.
+
+4. Finally, if there is no 'test' target, CC.rb will try to invoke 'default' target.
+
+Hint: if you define 'cruise' task in your_project, you should make it dependent on 'db:test:purge' and 
+'db:migrate', and/or whatever is necessary to bring test environment up to date.
+
+
+=== How to change what the build does?
+'cruise' may be the task for a quick build, but you may also want to run a long build with all acceptance tests 
+included. This can be done by assigning project.rake_task attribute in &lt;cruise&gt;/builds/your_project/project_config.rb:
+
+  Project.configure do |project|
+    ...
+    project.rake_task = 'Big_Bertha_build'
+    ...
+  end
+
+Or you may not want to deal with Rake at all, but build your project by shell script, make or Ant (yes, 
+CC.rb can cope with a Java project). This can be done by assigning project.build_command attribute in the 
+same &lt;cruise&gt;/builds/your_project/project_config.rb file, like this:
+  Project.configure do |project|
+    ...
+    project.build_command = 'my_build_script.sh'
+    ...
+  end
+
+Just like with Rake, CC.rb will change current working directory to &lt;cruise&gt;/builds/your_project/work/, invoke 
+specified command and look at the exit code to determine whether the build passed or failed.
+
+Note that you cannot specify both rake_task and build_command attributes project_config.rb. It doesn't make sense, 
+anyway.
+
+=== What should I do with custom build artifacts?
+
+  your_project may have a special build task, producing some output that you want to keep. Code coverage analysis is a 
+  good example. Before running the build, CC.rb sets OS variable CC_BUILD_ARTIFACTS to the directory where the build artifacts are 
+  collected. Make sure that your special task writes its output to that directory.
+  
+  Displaying custom task outputs on the dashboard is not implemented yet.
+  
+
+=== Email notification
+
+CruiseControl.rb can send email notices whenever build fails or is fixed. To make it happen, you need to tell it how 
+to send email, and who to send it to. Do the following:
+
+1. Configure SMTP server connection. Copy &lt;cruise&gt;/config/site_config.rb_example to ~cruise/config/site_config.rb, 
+   read it and edit according to your situation.
+
+2. Tell the builder, whom do you want to receive build notices. Copy ~cruise/config/project_config.rb_example to 
+   ~cruise/builds/your_project/project_config.rb and edit the list of email addresses there.
+
+
+=== Build scheduling
+
+By default, the builder polls Subversion every 10 seconds for new revisions. This can be changed by adding the 
+following line to the ~cruise/builds/your_project/project_config.rb:
+
+  Project.configure do |project|
+    ...
+    project.scheduler.polling_interval = 5.minutes
+    ...
+  end
+
+What if you want a scheduler with some interesting logic. Well, a default scheduler can be substituted by placing 
+your own scheduler implementation intpo the plugins directory and writing in 
+~cruise/builds/your_project/project_config.rb something like this:
+
+  Project.configure do |project|
+    ...
+    project.scheduler = MyCustomScheduler.new(project)
+    ...
+  end
+
+After initializing everything, and loading the project (step that includes evaluation of project_config.rb), the
+builder simply invokes project.scheduler.run. Look at app/models/polling_scheduler.rb to get the idea how 
+should scheduler interact with the project. 
+
+Beware that dashboard overwrites project_config.rb when you make changes to project configuration via web interface. 
+To play nicely with that, your custom scheduler must implement a memento() method, that 
+should return a line like this: &quot;  project.scheduler = MyCustomScheduler.new(project)&quot;. Again, look at 
+app/models/polling_scheduler.rb for the inspiration.
+
+=== Troubleshooting and support
+
+Beware, at the time of this writing, CC.rb is very young and not very stable. Good news is that it's simple (much, much 
+simpler than other CruiseControl incarnations). The dashboard is just a small Rails app, and the builder is little 
+more than a dumb, single-threaded Ruby script. Therefore, it's easy to debug. So, you are your own support hotline. 
+Don't forget to send us patches, please!
+
+OK, that was the pep talk. If you have an issue that you cannot fix on your own, email Alex or Jeremy.
+
+
+===Download the latest release from:
+
+* http://rubyforge.org/frs/?group_id=2918
+
+
+===Project web site
+
+* http://rubyforge.org/projects/cruisecontrolrb/
+
+
+===License:
+
+* Apache Software License 2.0
+
+---
+Authors::
+* Alexey Verkhovsky, Jeremy Stell-Smith, Stephen Chu
+* (C) ThoughtWorks 2007</diff>
      <filename>README</filename>
    </modified>
    <modified>
      <diff>@@ -1,28 +1,28 @@
-# Add your own tasks in files placed in lib/tasks ending in .rake,
-# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-
-require(File.join(File.dirname(__FILE__), 'config', 'boot'))
-
-require 'rubygems'
-require 'rake'
-require 'rake/testtask'
-require 'rake/rdoctask'
-
-require 'rake/packagetask'
-require 'rake/gempackagetask'
-require 'rake/contrib/rubyforgepublisher'
-
-require File.dirname(__FILE__) + '/lib/cruisecontrol/version'
-
-PKG_NAME      = 'cruisecontrol'
-PKG_VERSION   = CruiseControl::VERSION::STRING
-PKG_FILE_NAME = &quot;#{PKG_NAME}-#{PKG_VERSION}&quot;
-
-RELEASE_NAME  = &quot;REL #{PKG_VERSION}&quot;
-
-RUBY_FORGE_PROJECT = &quot;cruisecontrolrb&quot;
-RUBY_FORGE_USER    = &quot;stellsmi&quot;
-
-
-require 'tasks/rails'
-
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rubygems'
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'rake/packagetask'
+require 'rake/gempackagetask'
+require 'rake/contrib/rubyforgepublisher'
+
+require File.dirname(__FILE__) + '/lib/cruisecontrol/version'
+
+PKG_NAME      = 'cruisecontrol'
+PKG_VERSION   = CruiseControl::VERSION::STRING
+PKG_FILE_NAME = &quot;#{PKG_NAME}-#{PKG_VERSION}&quot;
+
+RELEASE_NAME  = &quot;REL #{PKG_VERSION}&quot;
+
+RUBY_FORGE_PROJECT = &quot;cruisecontrolrb&quot;
+RUBY_FORGE_USER    = &quot;stellsmi&quot;
+
+
+require 'tasks/rails'
+</diff>
      <filename>Rakefile</filename>
    </modified>
    <modified>
      <diff>@@ -1,7 +1,7 @@
-# Filters added to this controller apply to all controllers in the application.
-# Likewise, all the methods added will be available for all controllers.
-
-class ApplicationController &lt; ActionController::Base
-  # Pick a unique cookie name to distinguish our session data from others'
-  session :session_key =&gt; '_ci_session_id'
-end
+# Filters added to this controller apply to all controllers in the application.
+# Likewise, all the methods added will be available for all controllers.
+
+class ApplicationController &lt; ActionController::Base
+  # Pick a unique cookie name to distinguish our session data from others'
+  session :session_key =&gt; '_ci_session_id'
+end</diff>
      <filename>app/controllers/application.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,59 +1,59 @@
-class ProjectsController &lt; ApplicationController
-  layout &quot;default&quot;
-  
-  def index
-    @projects = load_projects
-  end
-
-  def show
-    @project = find_project(load_projects)
-
-    if params.has_key? :build
-      @build = @project.builds.find {|build| build.label.to_s == params[:build]}
-    end
-
-    if !@build
-      @build = @project.last_build
-    end
-  end
-
-  def settings
-    @project = find_project(load_projects)
-  end
-
-  def add_email
-    projects = load_projects
-    @project = find_project(projects)
-
-    @project.add_email(params[:value])
-
-    projects.save_project(@project)
-    update_emails
-  end
-
-  def remove_email
-    projects = load_projects
-    @project = find_project(projects)
-
-    @project.delete_email(params[:value])
-
-    projects.save_project(@project)
-    update_emails
-  end
-
-  private
-
-  def update_emails
-    render :update do |page|
-      page.replace_html &quot;email_list&quot;, :partial =&gt; 'list'
-    end
-  end
-  
-  def load_projects
-    Projects.load_all
-  end
-
-  def find_project(projects)
-    projects.find {|p| p.name == params[:id] }
-  end
+class ProjectsController &lt; ApplicationController
+  layout &quot;default&quot;
+  
+  def index
+    @projects = load_projects
+  end
+
+  def show
+    @project = find_project(load_projects)
+
+    if params.has_key? :build
+      @build = @project.builds.find {|build| build.label.to_s == params[:build]}
+    end
+
+    if !@build
+      @build = @project.last_build
+    end
+  end
+
+  def settings
+    @project = find_project(load_projects)
+  end
+
+  def add_email
+    projects = load_projects
+    @project = find_project(projects)
+
+    @project.add_email(params[:value])
+
+    projects.save_project(@project)
+    update_emails
+  end
+
+  def remove_email
+    projects = load_projects
+    @project = find_project(projects)
+
+    @project.delete_email(params[:value])
+
+    projects.save_project(@project)
+    update_emails
+  end
+
+  private
+
+  def update_emails
+    render :update do |page|
+      page.replace_html &quot;email_list&quot;, :partial =&gt; 'list'
+    end
+  end
+  
+  def load_projects
+    Projects.load_all
+  end
+
+  def find_project(projects)
+    projects.find {|p| p.name == params[:id] }
+  end
 end
\ No newline at end of file</diff>
      <filename>app/controllers/projects_controller.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,18 +1,18 @@
-class TestController &lt; ApplicationController
-  layout &quot;default&quot;
-  
-  def test_mail
-  end
-
-  def send_test_mail
-    time = Time.now.strftime(&quot;%I:%M:%S&quot;)
-    email = BuildMailer.deliver_test(params[:email][:recipients])
-    render :text =&gt; email_result(&quot;green&quot;, &quot;Email Sent... (#{time})&quot;)
-  rescue
-    render :text =&gt; email_result(&quot;red&quot;, &quot;Error : #{$!} (%{time})&quot;)
-  end
-
-  def email_result(color, message)
-    &quot;&lt;font color='#{color}'&gt;#{message}&lt;/font&gt;&quot;
-  end
-end
+class TestController &lt; ApplicationController
+  layout &quot;default&quot;
+  
+  def test_mail
+  end
+
+  def send_test_mail
+    time = Time.now.strftime(&quot;%I:%M:%S&quot;)
+    email = BuildMailer.deliver_test(params[:email][:recipients])
+    render :text =&gt; email_result(&quot;green&quot;, &quot;Email Sent... (#{time})&quot;)
+  rescue
+    render :text =&gt; email_result(&quot;red&quot;, &quot;Error : #{$!} (%{time})&quot;)
+  end
+
+  def email_result(color, message)
+    &quot;&lt;font color='#{color}'&gt;#{message}&lt;/font&gt;&quot;
+  end
+end</diff>
      <filename>app/controllers/test_controller.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,6 +1,6 @@
-# Methods added to this helper will be available to all templates in the application.
-module ApplicationHelper
-  def color_for_status(build)
-    build.successful? ? 'green' : 'red'
-  end
-end
+# Methods added to this helper will be available to all templates in the application.
+module ApplicationHelper
+  def color_for_status(build)
+    build.successful? ? 'green' : 'red'
+  end
+end</diff>
      <filename>app/helpers/application_helper.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,11 +1,11 @@
-def format_changeset_log(log)
-  log.strip
-end
-
-def format_build_log(log)
-  convert_new_lines(log.gsub(/(\d+ tests, \d+ assertions, \d+ failures, \d+ errors)/, '&lt;div class=&quot;test-results&quot;&gt;\1&lt;/div&gt;'))
-end
-
-def convert_new_lines(value)
-  value.gsub(/\n/, &quot;&lt;br/&gt;\n&quot;)
+def format_changeset_log(log)
+  log.strip
+end
+
+def format_build_log(log)
+  convert_new_lines(log.gsub(/(\d+ tests, \d+ assertions, \d+ failures, \d+ errors)/, '&lt;div class=&quot;test-results&quot;&gt;\1&lt;/div&gt;'))
+end
+
+def convert_new_lines(value)
+  value.gsub(/\n/, &quot;&lt;br/&gt;\n&quot;)
 end
\ No newline at end of file</diff>
      <filename>app/helpers/projects_helper.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,150 +1,150 @@
-class Build
-  include CommandLine
-
-  class &lt;&lt; self
-    def nil
-      NilBuild.new
-    end
-  end
-
-  attr_reader :project, :label
-
-  def initialize(project, label)
-    @project, @label = project, label
-    FileUtils.mkdir_p(artifacts_directory)
-
-    @status = Status.new(artifacts_directory)
-  end
-
-  def run
-    build_log = artifact 'build.log'
-    # it's important to figure out build command before doing chdir, because it may be expanding some relative paths
-    build_command = self.command
-    in_clean_environment_on_local_copy do
-      execute build_command, :stdout =&gt; build_log, :stderr =&gt; build_log, :escape_quotes =&gt; false
-    end
-    @status.succeed!
-  rescue =&gt; e
-    Log.info &quot;==================== BUILD FAILED =========================&quot;
-    Log.info e.message
-    Log.info e.backtrace.join(&quot;\n&quot;)
-    @status.fail!
-  end
-  
-  def successful?
-    @status.succeeded?
-  end
-
-  def failed?
-    @status.failed?
-  end
-  
-  def status
-    @status.to_s
-  end
-  
-  def status=(value)
-    FileUtils.rm_f(Dir[&quot;#{artifacts_directory}/build_status = *&quot;])
-    FileUtils.touch(artifact(&quot;build_status = #{value}&quot;))
-    @status = value
-  end
-
-  def changeset
-    File.read(artifact('changeset.log')) rescue &quot;&quot;
-  end
-
-  def output
-    File.read(artifact('build.log')) rescue &quot;&quot;
-  end
-  
-  def coverage_reports
-    CoverageReportsRepository.new(artifacts_directory)
-  end
-  
-  def formatted_time
-    if time = @status.created_at
-      time.strftime('%I:%M %p %b %d, %Y')
-    else
-      '-'
-    end
-  end
-
-  def artifacts_directory
-    @artifacts_dir ||= File.join(@project.path, &quot;build-#{@label}&quot;)
-  end
-  
-  def artifact(file_name)
-    File.join(artifacts_directory, file_name)
-  end
-
-  def command
-    project.build_command or rake
-  end
-  
-  def rake_task
-    project.rake_task
-  end
-  
-  def rake
-    # Important note: --nosearch flag here prevents CC.rb from building itslef when a project has no Rakefile
-    %{ruby -e &quot;require 'rubygems' rescue nil; require 'rake'; load '#{File.expand_path(RAILS_ROOT)}/tasks/cc_build.rake'; ARGV &lt;&lt; '--nosearch' &lt;&lt; 'cc:build'; Rake.application.run&quot;}
-  end
-
-  def last
-    builds = @project.builds
-    builds.each_index do |i|
-      if builds[i].label == label
-        return i &gt; 0 ? builds[i - 1] : nil
-      end
-    end
-    nil
-  end
-
-  def in_clean_environment_on_local_copy(&amp;block)
-    old_rails_env = ENV['RAILS_ENV']
-    # If we don't clean RAILS_ENV OS variable, tests of the project we are building would be 
-    # executed under 'builder' Rails environment
-    ENV.delete('RAILS_ENV')
-    # set OS variable CC_BUILD_ARTIFACTS so that custom build tasks know where to redirect their products
-    ENV['CC_BUILD_ARTIFACTS'] = self.artifacts_directory
-    ENV['CC_RAKE_TASK'] = self.rake_task
-    begin
-      Dir.chdir(project.local_checkout, &amp;block)
-    ensure
-      ENV['RAILS_ENV'] = old_rails_env
-    end
-  end
-
-  private
-  
-  class CoverageReportsRepository
-    def initialize(artifacts_directory)
-      @artifacts_directory = artifacts_directory
-    end
-
-    def [](coverage_type)
-      File.read(&quot;#{@artifacts_directory}/coverage-#{coverage_type}.log&quot;) rescue &quot;&quot;
-    end
-  end
-
-  # TODO: Does it need to exist? Can't a Struct/OpenStruct be used instead of this class?
-  # Don't know how to put this class to use Status...
-  class NilBuild
-    attr_reader :project, :label, :status, :time, :changeset, :output
-
-    def initialize
-      @project = nil
-      @label = @time = @changeset = @output = '-'
-      @status = :never_built
-    end
-    
-    def formatted_time
-      '-'
-    end
-
-    def successful?
-      false
-    end
-  end
-
-end
+class Build
+  include CommandLine
+
+  class &lt;&lt; self
+    def nil
+      NilBuild.new
+    end
+  end
+
+  attr_reader :project, :label
+
+  def initialize(project, label)
+    @project, @label = project, label
+    FileUtils.mkdir_p(artifacts_directory)
+
+    @status = Status.new(artifacts_directory)
+  end
+
+  def run
+    build_log = artifact 'build.log'
+    # it's important to figure out build command before doing chdir, because it may be expanding some relative paths
+    build_command = self.command
+    in_clean_environment_on_local_copy do
+      execute build_command, :stdout =&gt; build_log, :stderr =&gt; build_log, :escape_quotes =&gt; false
+    end
+    @status.succeed!
+  rescue =&gt; e
+    Log.info &quot;==================== BUILD FAILED =========================&quot;
+    Log.info e.message
+    Log.info e.backtrace.join(&quot;\n&quot;)
+    @status.fail!
+  end
+  
+  def successful?
+    @status.succeeded?
+  end
+
+  def failed?
+    @status.failed?
+  end
+  
+  def status
+    @status.to_s
+  end
+  
+  def status=(value)
+    FileUtils.rm_f(Dir[&quot;#{artifacts_directory}/build_status = *&quot;])
+    FileUtils.touch(artifact(&quot;build_status = #{value}&quot;))
+    @status = value
+  end
+
+  def changeset
+    File.read(artifact('changeset.log')) rescue &quot;&quot;
+  end
+
+  def output
+    File.read(artifact('build.log')) rescue &quot;&quot;
+  end
+  
+  def coverage_reports
+    CoverageReportsRepository.new(artifacts_directory)
+  end
+  
+  def formatted_time
+    if time = @status.created_at
+      time.strftime('%I:%M %p %b %d, %Y')
+    else
+      '-'
+    end
+  end
+
+  def artifacts_directory
+    @artifacts_dir ||= File.join(@project.path, &quot;build-#{@label}&quot;)
+  end
+  
+  def artifact(file_name)
+    File.join(artifacts_directory, file_name)
+  end
+
+  def command
+    project.build_command or rake
+  end
+  
+  def rake_task
+    project.rake_task
+  end
+  
+  def rake
+    # Important note: --nosearch flag here prevents CC.rb from building itslef when a project has no Rakefile
+    %{ruby -e &quot;require 'rubygems' rescue nil; require 'rake'; load '#{File.expand_path(RAILS_ROOT)}/tasks/cc_build.rake'; ARGV &lt;&lt; '--nosearch' &lt;&lt; 'cc:build'; Rake.application.run&quot;}
+  end
+
+  def last
+    builds = @project.builds
+    builds.each_index do |i|
+      if builds[i].label == label
+        return i &gt; 0 ? builds[i - 1] : nil
+      end
+    end
+    nil
+  end
+
+  def in_clean_environment_on_local_copy(&amp;block)
+    old_rails_env = ENV['RAILS_ENV']
+    # If we don't clean RAILS_ENV OS variable, tests of the project we are building would be 
+    # executed under 'builder' Rails environment
+    ENV.delete('RAILS_ENV')
+    # set OS variable CC_BUILD_ARTIFACTS so that custom build tasks know where to redirect their products
+    ENV['CC_BUILD_ARTIFACTS'] = self.artifacts_directory
+    ENV['CC_RAKE_TASK'] = self.rake_task
+    begin
+      Dir.chdir(project.local_checkout, &amp;block)
+    ensure
+      ENV['RAILS_ENV'] = old_rails_env
+    end
+  end
+
+  private
+  
+  class CoverageReportsRepository
+    def initialize(artifacts_directory)
+      @artifacts_directory = artifacts_directory
+    end
+
+    def [](coverage_type)
+      File.read(&quot;#{@artifacts_directory}/coverage-#{coverage_type}.log&quot;) rescue &quot;&quot;
+    end
+  end
+
+  # TODO: Does it need to exist? Can't a Struct/OpenStruct be used instead of this class?
+  # Don't know how to put this class to use Status...
+  class NilBuild
+    attr_reader :project, :label, :status, :time, :changeset, :output
+
+    def initialize
+      @project = nil
+      @label = @time = @changeset = @output = '-'
+      @status = :never_built
+    end
+    
+    def formatted_time
+      '-'
+    end
+
+    def successful?
+      false
+    end
+  end
+
+end</diff>
      <filename>app/models/build.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,29 +1,29 @@
-class BuildMailer &lt; ActionMailer::Base
-
-  def build_failed(build, recipients, sent_at = Time.now)
-    @subject    = &quot;#{build.project.name} Build #{build.label} - FAILED&quot;
-    @body       = {:build_log =&gt; build.output}
-    @recipients = recipients
-    @from       = 'cruisecontrol@thoughtworks.com'
-    @sent_on    = sent_at
-    @headers    = {}
-  end
-
-  def build_fixed(build, recipients, sent_at = Time.now)
-    @subject    = &quot;#{build.project.name} Build #{build.label} - FIXED&quot;
-    @body       = {:build_log =&gt; build.output}
-    @recipients = recipients
-    @from       = 'cruisecontrol@thoughtworks.com'
-    @sent_on    = sent_at
-    @headers    = {}
-  end
-
-  def test(recipients, sent_at = Time.now)
-    @subject    = 'Test CI E-mail'
-    @body       = {}
-    @recipients = recipients
-    @from       = 'cruisecontrol@thoughtworks.com'
-    @sent_on    = sent_at
-    @headers    = {}
-  end
-end
+class BuildMailer &lt; ActionMailer::Base
+
+  def build_failed(build, recipients, sent_at = Time.now)
+    @subject    = &quot;#{build.project.name} Build #{build.label} - FAILED&quot;
+    @body       = {:build_log =&gt; build.output}
+    @recipients = recipients
+    @from       = 'cruisecontrol@thoughtworks.com'
+    @sent_on    = sent_at
+    @headers    = {}
+  end
+
+  def build_fixed(build, recipients, sent_at = Time.now)
+    @subject    = &quot;#{build.project.name} Build #{build.label} - FIXED&quot;
+    @body       = {:build_log =&gt; build.output}
+    @recipients = recipients
+    @from       = 'cruisecontrol@thoughtworks.com'
+    @sent_on    = sent_at
+    @headers    = {}
+  end
+
+  def test(recipients, sent_at = Time.now)
+    @subject    = 'Test CI E-mail'
+    @body       = {}
+    @recipients = recipients
+    @from       = 'cruisecontrol@thoughtworks.com'
+    @sent_on    = sent_at
+    @headers    = {}
+  end
+end</diff>
      <filename>app/models/build_mailer.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,6 +1,6 @@
-ChangesetEntry = Struct.new :operation, :file
-class ChangesetEntry
-  def to_s
-    &quot;  #{operation} #{file}&quot;
-  end
-end
+ChangesetEntry = Struct.new :operation, :file
+class ChangesetEntry
+  def to_s
+    &quot;  #{operation} #{file}&quot;
+  end
+end</diff>
      <filename>app/models/changeset_entry.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,10 +1,10 @@
-class Configuration
-
-  @builds_directory = File.expand_path(File.join(RAILS_ROOT, 'builds'))
-  @default_polling_interval = 10.seconds
-
-  class &lt;&lt; self
-    attr_accessor :builds_directory, :default_polling_interval
-  end
-
-end
+class Configuration
+
+  @builds_directory = File.expand_path(File.join(RAILS_ROOT, 'builds'))
+  @default_polling_interval = 10.seconds
+
+  class &lt;&lt; self
+    attr_accessor :builds_directory, :default_polling_interval
+  end
+
+end</diff>
      <filename>app/models/configuration.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,33 +1,33 @@
-class PollingScheduler
-
-  def initialize(project)
-    @project = project
-    @custom_polling_interval = nil
-  end
-
-  def run
-    @project.build_if_necessary or sleep(polling_interval) while(true)
-  end
-
-  def polling_interval
-    @custom_polling_interval or Configuration.default_polling_interval
-  end
-  
-  def polling_interval=(value)
-    begin
-      value = value.to_i
-    rescue 
-      raise &quot;Polling interval value #{value.inspect} could not be converted to a number of seconds&quot;
-    end
-    raise &quot;Polling interval of #{value} seconds is too small (min. 5 seconds)&quot; if value &lt; 5.seconds
-    raise &quot;Polling interval of #{value} seconds is too big (max. 24 hours)&quot; if value &gt; 24.hours
-    @custom_polling_interval = value
-  end
-
-  def memento
-    @custom_polling_interval ? 
-      &quot;  project.scheduler.polling_interval = #{@custom_polling_interval}.seconds&quot; : 
-      nil
-  end
-
-end
+class PollingScheduler
+
+  def initialize(project)
+    @project = project
+    @custom_polling_interval = nil
+  end
+
+  def run
+    @project.build_if_necessary or sleep(polling_interval) while(true)
+  end
+
+  def polling_interval
+    @custom_polling_interval or Configuration.default_polling_interval
+  end
+  
+  def polling_interval=(value)
+    begin
+      value = value.to_i
+    rescue 
+      raise &quot;Polling interval value #{value.inspect} could not be converted to a number of seconds&quot;
+    end
+    raise &quot;Polling interval of #{value} seconds is too small (min. 5 seconds)&quot; if value &lt; 5.seconds
+    raise &quot;Polling interval of #{value} seconds is too big (max. 24 hours)&quot; if value &gt; 24.hours
+    @custom_polling_interval = value
+  end
+
+  def memento
+    @custom_polling_interval ? 
+      &quot;  project.scheduler.polling_interval = #{@custom_polling_interval}.seconds&quot; : 
+      nil
+  end
+
+end</diff>
      <filename>app/models/polling_scheduler.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,164 +1,164 @@
-require 'fileutils'
-
-class Project
-  @@plugin_names = []
-
-  def self.plugin(plugin_name)
-    @@plugin_names &lt;&lt; plugin_name unless @@plugin_names.include? plugin_name
-  end
-
-  def self.load_or_create(dir)
-    config_file = File.expand_path(File.join(dir, 'project_config.rb'))
-    project = @project = Project.new(File.basename(dir), Subversion.new, dir + &quot;/work&quot;)
-    project.path = dir
-    begin
-      load config_file if File.exists? config_file
-      return project
-    rescue =&gt; e
-      raise &quot;Could not load #{config_file} : #{e.message} in #{e.backtrace.first}&quot;
-    ensure
-      @project = nil
-    end
-  end
-
-  def self.configure
-    raise &quot;No project is currently being created&quot; unless @project
-    yield @project
-  end
-
-  attr_reader :name, :plugins, :build_command, :rake_task
-  attr_accessor :source_control, :path, :local_checkout, :scheduler
-
-  def initialize(name, source_control, local_checkout = nil)
-    @name, @source_control, @local_checkout = name, source_control, local_checkout
-    @path = File.join(Configuration.builds_directory, @name)
-    @plugins = []
-    @plugins_by_name = {}
-    @@plugin_names.each do |plugin_name|
-      plugin_instance = plugin_name.to_s.camelize.constantize.new(self)
-      self.add_plugin(plugin_instance)
-    end
-    @scheduler = PollingScheduler.new(self)
-  end
-
-  def ==(another)
-    another.is_a?(Project) and another.name == self.name
-  end
-
-  def build_command=(value)
-    raise 'Cannot set build_command when rake_task is already defined' if @rake_task
-    @build_command = value
-  end
-
-  def rake_task=(value)
-    raise 'Cannot set rake_task when build_command is already defined' if @build_command
-    @rake_task = value
-  end
-
-  def builds
-    raise &quot;Project #{name.inspect} has no path&quot; unless @path
-
-    Dir[&quot;#{@path}/build-*/build_status = *&quot;].collect do |status_file|
-      dir = File.dirname(status_file)
-      number = File.basename(dir)[6..-1].to_i
-
-      Build.new(self, number)
-    end.sort_by { |build| build.label }
-  end
-
-  def last_build
-    builds.last || Build.nil
-  end
-
-  def memento
-    memento = []
-    memento &lt;&lt; &quot;  project.source_control = #{source_control.memento}&quot; unless source_control.memento.nil?
-    memento &lt;&lt; scheduler.memento
-    memento += notify(:memento)
-    return &lt;&lt;-EOL
-Project.configure do |project|
-#{memento.compact.join(&quot;\n&quot;)}
-end
-    EOL
-  end
-  
-  def build_if_necessary
-    notify(:polling_source_control)
-    begin
-      revisions = new_revisions()
-      if revisions.empty?
-        notify(:no_new_revisions_detected)
-        return nil
-      else
-        notify(:new_revisions_detected, revisions)
-        return build(revisions)
-      end
-    ensure
-      notify(:sleeping)
-    end
-  end
-
-  def new_revisions
-    b = builds
-    if b.empty?
-      [@source_control.latest_revision(self)]
-    else
-      @source_control.revisions_since(self, b.last.label.to_i)
-    end
-  end
-
-  def build(revisions = [@source_control.latest_revision(self)])
-    last_revision = revisions.last
-    build = Build.new(self, last_revision.number)
-    log_changeset(build.artifacts_directory, revisions)
-    @source_control.update(self, last_revision)
-    notify(:build_started, build)
-    build.run
-    notify(:build_finished, build)
-    build
-  end
-
-  def notify(event, *event_parameters)
-    @plugins.each do |plugin|
-      plugin.send(event, *event_parameters) if plugin.respond_to?(event)
-    end
-  end
-  
-  def log_changeset(artifacts_directory, revisions)
-    File.open(File.join(artifacts_directory, 'changeset.log'), 'w') do |f|
-      revisions.each { |rev| f &lt;&lt; rev.to_s &lt;&lt; &quot;\n&quot; }
-    end
-  end
-
-  def add_plugin(plugin, plugin_name = plugin.class)
-    @plugins &lt;&lt; plugin
-    plugin_name = plugin_name.to_s.underscore.to_sym
-    if self.respond_to?(plugin_name)
-      raise &quot;Cannot register an plugin with name #{plugin_name.inspect} &quot; +
-            &quot;because another plugin, or a method with the same name already exists&quot;
-    end
-    @plugins_by_name[plugin_name] = plugin
-    plugin
-  end
-
-  # access plugins by their names
-  def method_missing(method_name, *args, &amp;block)
-    @plugins_by_name.key?(method_name) ? @plugins_by_name[method_name] : super
-  end
-  
-  def respond_to?(method_name)
-    @plugins_by_name.key?(method_name) or super
-  end
-
-  def notify(sym, *args)
-    @plugins.collect do |plugin|
-      plugin.send(sym, *args) if plugin.respond_to?(sym)
-    end.compact
-  end
-
-end
-
-plugins = File.expand_path(File.dirname(__FILE__) + '/../../lib/cruise_plugins/*.rb')
-Dir[plugins].each do |plugin_file|
-  load plugin_file # this has to be loaded instead of required, for the dev build
-end
+require 'fileutils'
+
+class Project
+  @@plugin_names = []
+
+  def self.plugin(plugin_name)
+    @@plugin_names &lt;&lt; plugin_name unless @@plugin_names.include? plugin_name
+  end
+
+  def self.load_or_create(dir)
+    config_file = File.expand_path(File.join(dir, 'project_config.rb'))
+    project = @project = Project.new(File.basename(dir), Subversion.new, dir + &quot;/work&quot;)
+    project.path = dir
+    begin
+      load config_file if File.exists? config_file
+      return project
+    rescue =&gt; e
+      raise &quot;Could not load #{config_file} : #{e.message} in #{e.backtrace.first}&quot;
+    ensure
+      @project = nil
+    end
+  end
+
+  def self.configure
+    raise &quot;No project is currently being created&quot; unless @project
+    yield @project
+  end
+
+  attr_reader :name, :plugins, :build_command, :rake_task
+  attr_accessor :source_control, :path, :local_checkout, :scheduler
+
+  def initialize(name, source_control, local_checkout = nil)
+    @name, @source_control, @local_checkout = name, source_control, local_checkout
+    @path = File.join(Configuration.builds_directory, @name)
+    @plugins = []
+    @plugins_by_name = {}
+    @@plugin_names.each do |plugin_name|
+      plugin_instance = plugin_name.to_s.camelize.constantize.new(self)
+      self.add_plugin(plugin_instance)
+    end
+    @scheduler = PollingScheduler.new(self)
+  end
+
+  def ==(another)
+    another.is_a?(Project) and another.name == self.name
+  end
+
+  def build_command=(value)
+    raise 'Cannot set build_command when rake_task is already defined' if @rake_task
+    @build_command = value
+  end
+
+  def rake_task=(value)
+    raise 'Cannot set rake_task when build_command is already defined' if @build_command
+    @rake_task = value
+  end
+
+  def builds
+    raise &quot;Project #{name.inspect} has no path&quot; unless @path
+
+    Dir[&quot;#{@path}/build-*/build_status = *&quot;].collect do |status_file|
+      dir = File.dirname(status_file)
+      number = File.basename(dir)[6..-1].to_i
+
+      Build.new(self, number)
+    end.sort_by { |build| build.label }
+  end
+
+  def last_build
+    builds.last || Build.nil
+  end
+
+  def memento
+    memento = []
+    memento &lt;&lt; &quot;  project.source_control = #{source_control.memento}&quot; unless source_control.memento.nil?
+    memento &lt;&lt; scheduler.memento
+    memento += notify(:memento)
+    return &lt;&lt;-EOL
+Project.configure do |project|
+#{memento.compact.join(&quot;\n&quot;)}
+end
+    EOL
+  end
+  
+  def build_if_necessary
+    notify(:polling_source_control)
+    begin
+      revisions = new_revisions()
+      if revisions.empty?
+        notify(:no_new_revisions_detected)
+        return nil
+      else
+        notify(:new_revisions_detected, revisions)
+        return build(revisions)
+      end
+    ensure
+      notify(:sleeping)
+    end
+  end
+
+  def new_revisions
+    b = builds
+    if b.empty?
+      [@source_control.latest_revision(self)]
+    else
+      @source_control.revisions_since(self, b.last.label.to_i)
+    end
+  end
+
+  def build(revisions = [@source_control.latest_revision(self)])
+    last_revision = revisions.last
+    build = Build.new(self, last_revision.number)
+    log_changeset(build.artifacts_directory, revisions)
+    @source_control.update(self, last_revision)
+    notify(:build_started, build)
+    build.run
+    notify(:build_finished, build)
+    build
+  end
+
+  def notify(event, *event_parameters)
+    @plugins.each do |plugin|
+      plugin.send(event, *event_parameters) if plugin.respond_to?(event)
+    end
+  end
+  
+  def log_changeset(artifacts_directory, revisions)
+    File.open(File.join(artifacts_directory, 'changeset.log'), 'w') do |f|
+      revisions.each { |rev| f &lt;&lt; rev.to_s &lt;&lt; &quot;\n&quot; }
+    end
+  end
+
+  def add_plugin(plugin, plugin_name = plugin.class)
+    @plugins &lt;&lt; plugin
+    plugin_name = plugin_name.to_s.underscore.to_sym
+    if self.respond_to?(plugin_name)
+      raise &quot;Cannot register an plugin with name #{plugin_name.inspect} &quot; +
+            &quot;because another plugin, or a method with the same name already exists&quot;
+    end
+    @plugins_by_name[plugin_name] = plugin
+    plugin
+  end
+
+  # access plugins by their names
+  def method_missing(method_name, *args, &amp;block)
+    @plugins_by_name.key?(method_name) ? @plugins_by_name[method_name] : super
+  end
+  
+  def respond_to?(method_name)
+    @plugins_by_name.key?(method_name) or super
+  end
+
+  def notify(sym, *args)
+    @plugins.collect do |plugin|
+      plugin.send(sym, *args) if plugin.respond_to?(sym)
+    end.compact
+  end
+
+end
+
+plugins = File.expand_path(File.dirname(__FILE__) + '/../../lib/cruise_plugins/*.rb')
+Dir[plugins].each do |plugin_file|
+  load plugin_file # this has to be loaded instead of required, for the dev build
+end</diff>
      <filename>app/models/project.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,54 +1,54 @@
-require 'fileutils'
-
-class Projects
-
-  class &lt;&lt; self
-    def load_all(dir = Configuration.builds_directory)
-      Projects.new(dir).load_all
-    end
-
-    def load_project(dir)
-      project = Project.load_or_create(dir)
-      project.path = dir
-      project
-    end
-  end
-  
-  def initialize(dir = Configuration.builds_directory)
-    @dir = dir
-    @list = []
-  end
-
-  def load_all
-    @list = Dir[&quot;#{@dir}/*&quot;].find_all {|child| File.directory?(child)}.
-                             collect  {|child| Projects.load_project(child)}
-    self
-  end
-  
-  def &lt;&lt;(project)
-    raise &quot;project named #{project.name.inspect} already exists&quot; if @list.include?(project)
-    @list &lt;&lt; project
-    save_project(project)
-
-    work_dir = &quot;#{@dir}/#{project.name}/work&quot;
-    FileUtils.mkdir_p work_dir
-    project.source_control.checkout work_dir
-    
-    self
-  rescue
-    FileUtils.rm_rf &quot;#{@dir}/#{project.name}&quot;
-    raise
-  end
-
-  def save_project(project)
-    path = @dir + &quot;/&quot; + project.name
-    FileUtils::makedirs path
-    File.open(path + &quot;/project_config.rb&quot;, &quot;w&quot;) {|f| f &lt;&lt; project.memento}
-  end
-
-  # delegate everything else to the underlying @list
-  def method_missing(method, *args, &amp;block)
-    @list.send(method, *args, &amp;block)
-  end
-
-end
+require 'fileutils'
+
+class Projects
+
+  class &lt;&lt; self
+    def load_all(dir = Configuration.builds_directory)
+      Projects.new(dir).load_all
+    end
+
+    def load_project(dir)
+      project = Project.load_or_create(dir)
+      project.path = dir
+      project
+    end
+  end
+  
+  def initialize(dir = Configuration.builds_directory)
+    @dir = dir
+    @list = []
+  end
+
+  def load_all
+    @list = Dir[&quot;#{@dir}/*&quot;].find_all {|child| File.directory?(child)}.
+                             collect  {|child| Projects.load_project(child)}
+    self
+  end
+  
+  def &lt;&lt;(project)
+    raise &quot;project named #{project.name.inspect} already exists&quot; if @list.include?(project)
+    @list &lt;&lt; project
+    save_project(project)
+
+    work_dir = &quot;#{@dir}/#{project.name}/work&quot;
+    FileUtils.mkdir_p work_dir
+    project.source_control.checkout work_dir
+    
+    self
+  rescue
+    FileUtils.rm_rf &quot;#{@dir}/#{project.name}&quot;
+    raise
+  end
+
+  def save_project(project)
+    path = @dir + &quot;/&quot; + project.name
+    FileUtils::makedirs path
+    File.open(path + &quot;/project_config.rb&quot;, &quot;w&quot;) {|f| f &lt;&lt; project.memento}
+  end
+
+  # delegate everything else to the underlying @list
+  def method_missing(method, *args, &amp;block)
+    @list.send(method, *args, &amp;block)
+  end
+
+end</diff>
      <filename>app/models/projects.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,16 +1,16 @@
-Revision = Struct.new :number, :committed_by, :time, :message, :changeset
-
-class Revision
-  def to_s
-    &quot;Revision #{number} committed by #{committed_by} on #{time.strftime('%Y-%m-%d %H:%M:%S')}\n&quot; +
-    message +
-    &quot;\n&quot; +
-    changeset.collect { |entry| entry.to_s }.join(&quot;\n&quot;) +
-    &quot;\n&quot;
-  end
-  
-  def to_i
-    number.to_i
-  end
-
+Revision = Struct.new :number, :committed_by, :time, :message, :changeset
+
+class Revision
+  def to_s
+    &quot;Revision #{number} committed by #{committed_by} on #{time.strftime('%Y-%m-%d %H:%M:%S')}\n&quot; +
+    message +
+    &quot;\n&quot; +
+    changeset.collect { |entry| entry.to_s }.join(&quot;\n&quot;) +
+    &quot;\n&quot;
+  end
+  
+  def to_i
+    number.to_i
+  end
+
 end
\ No newline at end of file</diff>
      <filename>app/models/revision.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,63 +1,63 @@
-class Status
-  
-  def initialize(artifacts_directory)
-    @artifacts_directory = artifacts_directory
-  end
-  
-  def never_built?
-    read_latest_status == :never_built
-  end
-  
-  def succeeded?
-    read_latest_status == :success
-  end
-  
-  def failed?
-    read_latest_status == :failed
-  end
-
-  def succeed!
-    remove_status_file
-    touch_status_file(:success)
-  end
-  
-  def fail!
-    remove_status_file
-    touch_status_file(:failed)    
-  end
-  
-  def building!
-    remove_status_file
-    touch_status_file(:building)
-  end
-  
-  def created_at
-    if file = status_file
-      File.mtime(file)
-    end
-  end
-  
-  def to_s
-    read_latest_status.to_s
-  end
-    
-  private
-  
-    def read_latest_status
-      file = status_file
-      file ? File.basename(file)[15..-1].downcase.gsub('__', '').to_sym : :never_built
-    end
-  
-    def remove_status_file
-      FileUtils.rm_f(Dir[&quot;#{@artifacts_directory}/build_status = *&quot;])
-    end
-    
-    def touch_status_file(status)
-      FileUtils.touch(&quot;#{@artifacts_directory}/build_status = #{status}&quot;)
-    end
-    
-    def status_file
-      Dir[&quot;#{@artifacts_directory}/build_status = *&quot;].first
-    end
-  
+class Status
+  
+  def initialize(artifacts_directory)
+    @artifacts_directory = artifacts_directory
+  end
+  
+  def never_built?
+    read_latest_status == :never_built
+  end
+  
+  def succeeded?
+    read_latest_status == :success
+  end
+  
+  def failed?
+    read_latest_status == :failed
+  end
+
+  def succeed!
+    remove_status_file
+    touch_status_file(:success)
+  end
+  
+  def fail!
+    remove_status_file
+    touch_status_file(:failed)    
+  end
+  
+  def building!
+    remove_status_file
+    touch_status_file(:building)
+  end
+  
+  def created_at
+    if file = status_file
+      File.mtime(file)
+    end
+  end
+  
+  def to_s
+    read_latest_status.to_s
+  end
+    
+  private
+  
+    def read_latest_status
+      file = status_file
+      file ? File.basename(file)[15..-1].downcase.gsub('__', '').to_sym : :never_built
+    end
+  
+    def remove_status_file
+      FileUtils.rm_f(Dir[&quot;#{@artifacts_directory}/build_status = *&quot;])
+    end
+    
+    def touch_status_file(status)
+      FileUtils.touch(&quot;#{@artifacts_directory}/build_status = #{status}&quot;)
+    end
+    
+    def status_file
+      Dir[&quot;#{@artifacts_directory}/build_status = *&quot;].first
+    end
+  
 end
\ No newline at end of file</diff>
      <filename>app/models/status.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,89 +1,89 @@
-class Subversion
-
-  include CommandLine
-
-  attr_accessor :url, :username, :password
-
-  def initialize(options = {})
-    @url, @username, @password = options.delete(:url), options.delete(:username), options.delete(:password)
-    raise &quot;don't know how to handle '#{options.keys.first}'&quot; if options.length &gt; 0
-  end
-
-  def self.checkout(target_directory, options)
-    revision = options.delete(:revision)
-    Subversion.new(options).checkout(target_directory, revision)
-  end
-    
-  # FIXME: options should be passed either to the constructor, or to the method, not both
-  def checkout(target_directory, revision = nil)
-    @url or raise 'URL not specified'
-
-    cmd = &quot;svn --non-interactive co #{@url} #{target_directory}&quot;
-    cmd &lt;&lt; &quot; --username #{@username}&quot; if username
-    cmd &lt;&lt; &quot; --password #{@password}&quot; if password
-    cmd &lt;&lt; &quot; --revision #{revision_number(revision)}&quot; if revision
-
-    # need to read from command output, because otherwise tests break
-    execute(cmd) { |io| io.readlines }
-  end
-
-  def info(project)
-    result = Hash.new
-    Dir.chdir(project.local_checkout) do
-      execute 'svn --non-interactive info' do |io|
-        io.each_line do |line|
-          line.chomp!
-          next if line.empty?
-          match = line.match(/^([^:]+):\s*(.*)$/)
-          raise &quot;#{line.inspect} does not match 'name: value' pattern&quot; unless match
-          key, value = match[1..2]
-          result[key] = value
-        end
-      end
-    end
-    result
-  end
-
-  def latest_revision(project)
-    last_locally_known_revision = info(project)['Last Changed Rev']
-    svn_output = execute_in_local_copy(project, 
-        &quot;svn --non-interactive log --revision HEAD:#{last_locally_known_revision} --verbose&quot;)
-    SubversionLogParser.new.parse_log(svn_output).first
-  end
-
-  def current_revision(project)
-    info(project)[&quot;Revision&quot;].to_i
-  end
-
-  def revisions_since(project, revision_number)
-    svn_output = execute_in_local_copy(project, &quot;svn --non-interactive log --revision HEAD:#{revision_number} --verbose&quot;)
-    new_revisions = SubversionLogParser.new.parse_log(svn_output).reverse
-    new_revisions.delete_if { |r| r.number == revision_number }
-    new_revisions
-  end
-
-  def update(project, revision)
-    svn_output = execute_in_local_copy(project, &quot;svn --non-interactive update --revision #{revision_number(revision)}&quot;)
-    SubversionLogParser.new.parse_update(svn_output)
-  end
-
-  def revision_number(revision)
-    revision.respond_to?(:number) ? revision.number : revision.to_i
-  end
-
-  def memento
-    options = []
-    options &lt;&lt; &quot;:url =&gt; '#{url}'&quot; if url
-    options &lt;&lt; &quot;:username =&gt; '#{username}'&quot; if username
-    options &lt;&lt; &quot;:password =&gt; '#{password}'&quot; if password
-    
-    options.empty? ? nil : &quot;Subversion.new(#{options.join(&quot;, &quot;)})&quot;
-  end
-
-  def execute_in_local_copy(project, command)
-    Dir.chdir(project.local_checkout) do
-      execute(command) { |io| return io.readlines }
-    end
-  end
-
-end
+class Subversion
+
+  include CommandLine
+
+  attr_accessor :url, :username, :password
+
+  def initialize(options = {})
+    @url, @username, @password = options.delete(:url), options.delete(:username), options.delete(:password)
+    raise &quot;don't know how to handle '#{options.keys.first}'&quot; if options.length &gt; 0
+  end
+
+  def self.checkout(target_directory, options)
+    revision = options.delete(:revision)
+    Subversion.new(options).checkout(target_directory, revision)
+  end
+    
+  # FIXME: options should be passed either to the constructor, or to the method, not both
+  def checkout(target_directory, revision = nil)
+    @url or raise 'URL not specified'
+
+    cmd = &quot;svn --non-interactive co #{@url} #{target_directory}&quot;
+    cmd &lt;&lt; &quot; --username #{@username}&quot; if username
+    cmd &lt;&lt; &quot; --password #{@password}&quot; if password
+    cmd &lt;&lt; &quot; --revision #{revision_number(revision)}&quot; if revision
+
+    # need to read from command output, because otherwise tests break
+    execute(cmd) { |io| io.readlines }
+  end
+
+  def info(project)
+    result = Hash.new
+    Dir.chdir(project.local_checkout) do
+      execute 'svn --non-interactive info' do |io|
+        io.each_line do |line|
+          line.chomp!
+          next if line.empty?
+          match = line.match(/^([^:]+):\s*(.*)$/)
+          raise &quot;#{line.inspect} does not match 'name: value' pattern&quot; unless match
+          key, value = match[1..2]
+          result[key] = value
+        end
+      end
+    end
+    result
+  end
+
+  def latest_revision(project)
+    last_locally_known_revision = info(project)['Last Changed Rev']
+    svn_output = execute_in_local_copy(project, 
+        &quot;svn --non-interactive log --revision HEAD:#{last_locally_known_revision} --verbose&quot;)
+    SubversionLogParser.new.parse_log(svn_output).first
+  end
+
+  def current_revision(project)
+    info(project)[&quot;Revision&quot;].to_i
+  end
+
+  def revisions_since(project, revision_number)
+    svn_output = execute_in_local_copy(project, &quot;svn --non-interactive log --revision HEAD:#{revision_number} --verbose&quot;)
+    new_revisions = SubversionLogParser.new.parse_log(svn_output).reverse
+    new_revisions.delete_if { |r| r.number == revision_number }
+    new_revisions
+  end
+
+  def update(project, revision)
+    svn_output = execute_in_local_copy(project, &quot;svn --non-interactive update --revision #{revision_number(revision)}&quot;)
+    SubversionLogParser.new.parse_update(svn_output)
+  end
+
+  def revision_number(revision)
+    revision.respond_to?(:number) ? revision.number : revision.to_i
+  end
+
+  def memento
+    options = []
+    options &lt;&lt; &quot;:url =&gt; '#{url}'&quot; if url
+    options &lt;&lt; &quot;:username =&gt; '#{username}'&quot; if username
+    options &lt;&lt; &quot;:password =&gt; '#{password}'&quot; if password
+    
+    options.empty? ? nil : &quot;Subversion.new(#{options.join(&quot;, &quot;)})&quot;
+  end
+
+  def execute_in_local_copy(project, command)
+    Dir.chdir(project.local_checkout) do
+      execute(command) { |io| return io.readlines }
+    end
+  end
+
+end</diff>
      <filename>app/models/subversion.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,55 +1,55 @@
-require 'date'
-
-class SubversionLogParser
-
-  def parse_log(lines)
-    return [] if lines.empty?
-    lines.shift #ignore first dashed line
-
-    revisions = []
-    while not lines.empty?
-      revision = parse_revision(lines)
-      revisions &lt;&lt; revision
-    end
-
-  revisions
-  end
-
-  UPDATE_PATTERN = /^(...)  (\S.*)$/
-  def parse_update(lines)
-    lines[0..-2].collect do |line|
-      match = UPDATE_PATTERN.match(line)
-      if match
-        operation, file = match[1..2]
-        ChangesetEntry.new(operation, file)
-      else
-        nil
-      end
-    end.compact
-  end
-
-  private
-
-  REVISION_PATTERN = /^r(\d+) \| ([^ ]+) \| ([^|]+) \| .*$/
-  CHANGESET_PATTERN = /^\s*(\S+)\s+(.*)$/
-  def parse_revision(lines)
-    number, committed_by, time = REVISION_PATTERN.match(lines.shift)[1..3]
-    revision = Revision.new(number.to_i, committed_by, DateTime.parse(time), '', [])
-
-    line = lines.shift
-    if line =~ /^Changed paths:/
-      while (line = lines.shift) and not line.strip.empty?  do
-        match = CHANGESET_PATTERN.match(line)
-        raise &quot;Line #{line.inspect} does not like a changeset line from 'svn log --verbose'&quot; unless match
-        operation, file = match[1..2]
-        ChangesetEntry.new(operation.strip, file)
-        revision.changeset &lt;&lt; ChangesetEntry.new(operation, file)
-      end
-    end
-
-    revision.message &lt;&lt; line while (line = lines.shift).strip != '-' * 72
-    revision.message.strip!
-
-    revision
-  end
+require 'date'
+
+class SubversionLogParser
+
+  def parse_log(lines)
+    return [] if lines.empty?
+    lines.shift #ignore first dashed line
+
+    revisions = []
+    while not lines.empty?
+      revision = parse_revision(lines)
+      revisions &lt;&lt; revision
+    end
+
+  revisions
+  end
+
+  UPDATE_PATTERN = /^(...)  (\S.*)$/
+  def parse_update(lines)
+    lines[0..-2].collect do |line|
+      match = UPDATE_PATTERN.match(line)
+      if match
+        operation, file = match[1..2]
+        ChangesetEntry.new(operation, file)
+      else
+        nil
+      end
+    end.compact
+  end
+
+  private
+
+  REVISION_PATTERN = /^r(\d+) \| ([^ ]+) \| ([^|]+) \| .*$/
+  CHANGESET_PATTERN = /^\s*(\S+)\s+(.*)$/
+  def parse_revision(lines)
+    number, committed_by, time = REVISION_PATTERN.match(lines.shift)[1..3]
+    revision = Revision.new(number.to_i, committed_by, DateTime.parse(time), '', [])
+
+    line = lines.shift
+    if line =~ /^Changed paths:/
+      while (line = lines.shift) and not line.strip.empty?  do
+        match = CHANGESET_PATTERN.match(line)
+        raise &quot;Line #{line.inspect} does not like a changeset line from 'svn log --verbose'&quot; unless match
+        operation, file = match[1..2]
+        ChangesetEntry.new(operation.strip, file)
+        revision.changeset &lt;&lt; ChangesetEntry.new(operation, file)
+      end
+    end
+
+    revision.message &lt;&lt; line while (line = lines.shift).strip != '-' * 72
+    revision.message.strip!
+
+    revision
+  end
 end
\ No newline at end of file</diff>
      <filename>app/models/subversion_log_parser.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,39 +1,39 @@
-&lt;html&gt;
-&lt;head&gt;
-  &lt;title&gt;CruiseControl.rb&lt;/title&gt;
-  &lt;%= stylesheet_link_tag 'cruisecontrol', :media =&gt; 'all' %&gt;
-  &lt;%= javascript_include_tag :defaults %&gt;
-&lt;/head&gt;
-
-&lt;body class=&quot;wholepage&quot; topmargin=&quot;0&quot; leftmargin=&quot;0&quot; marginheight=&quot;0&quot; marginwidth=&quot;0&quot;&gt;
-
-  &lt;%= link_to image_tag(&quot;cc_logo.png&quot;), &quot;http://lipid.rubyforge.org/&quot; %&gt;
-
-  &lt;table class=&quot;TopControls&quot; border=&quot;0&quot; width=&quot;100%&quot; cellpadding=&quot;3&quot; cellspacing=&quot;0&quot; height=&quot;25&quot;&gt;
-    &lt;tr&gt;
-      &lt;td valign=&quot;middle&quot; align=&quot;left&quot;&gt;
-        &lt;div class=&quot;breadcrumbs&quot;&gt;&lt;%= @breadcrumbs %&gt;&lt;/div&gt;
-      &lt;/td&gt;
-      &lt;td valign=&quot;middle&quot; align=&quot;right&quot;&gt;&lt;p&gt;version : &lt;%= CruiseControl::VERSION::STRING %&gt;&lt;/p&gt;&lt;/td&gt;
-    &lt;/tr&gt;
-  &lt;/table&gt;
-
-  &lt;table border=&quot;0&quot; align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; width=&quot;100%&quot; bgcolor=&quot;#333399&quot;&gt;
-    &lt;tr&gt;
-      &lt;td valign=&quot;top&quot; id=&quot;LeftHandSide&quot; bgcolor=&quot;#eeeedd&quot; width=&quot;180&quot;&gt;
-        &lt;%= yield :left_side %&gt;
-      &lt;/td&gt;
-      &lt;td width=&quot;2&quot;&gt;&lt;img src=&quot;/images/shim.gif&quot; width=&quot;2&quot;/&gt;&lt;/td&gt;
-      &lt;td valign=&quot;top&quot; bgcolor=&quot;#ffffff&quot;&gt;
-        &lt;%= yield %&gt;
-      &lt;/td&gt;
-    &lt;/tr&gt;
-  &lt;/table&gt;
-
-  &lt;div align=&quot;right&quot;&gt;
-    &lt;%= link_to image_tag(&quot;tw_dev_logo.gif&quot;), &quot;http://www.thoughtworks.com/&quot; %&gt;
-    &lt;%= image_tag &quot;shim.gif&quot; %&gt;
-  &lt;/div&gt;
-&lt;/body&gt;
-&lt;/html&gt;
-
+&lt;html&gt;
+&lt;head&gt;
+  &lt;title&gt;CruiseControl.rb&lt;/title&gt;
+  &lt;%= stylesheet_link_tag 'cruisecontrol', :media =&gt; 'all' %&gt;
+  &lt;%= javascript_include_tag :defaults %&gt;
+&lt;/head&gt;
+
+&lt;body class=&quot;wholepage&quot; topmargin=&quot;0&quot; leftmargin=&quot;0&quot; marginheight=&quot;0&quot; marginwidth=&quot;0&quot;&gt;
+
+  &lt;%= link_to image_tag(&quot;cc_logo.png&quot;), &quot;http://lipid.rubyforge.org/&quot; %&gt;
+
+  &lt;table class=&quot;TopControls&quot; border=&quot;0&quot; width=&quot;100%&quot; cellpadding=&quot;3&quot; cellspacing=&quot;0&quot; height=&quot;25&quot;&gt;
+    &lt;tr&gt;
+      &lt;td valign=&quot;middle&quot; align=&quot;left&quot;&gt;
+        &lt;div class=&quot;breadcrumbs&quot;&gt;&lt;%= @breadcrumbs %&gt;&lt;/div&gt;
+      &lt;/td&gt;
+      &lt;td valign=&quot;middle&quot; align=&quot;right&quot;&gt;&lt;p&gt;version : &lt;%= CruiseControl::VERSION::STRING %&gt;&lt;/p&gt;&lt;/td&gt;
+    &lt;/tr&gt;
+  &lt;/table&gt;
+
+  &lt;table border=&quot;0&quot; align=&quot;center&quot; cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; width=&quot;100%&quot; bgcolor=&quot;#333399&quot;&gt;
+    &lt;tr&gt;
+      &lt;td valign=&quot;top&quot; id=&quot;LeftHandSide&quot; bgcolor=&quot;#eeeedd&quot; width=&quot;180&quot;&gt;
+        &lt;%= yield :left_side %&gt;
+      &lt;/td&gt;
+      &lt;td width=&quot;2&quot;&gt;&lt;img src=&quot;/images/shim.gif&quot; width=&quot;2&quot;/&gt;&lt;/td&gt;
+      &lt;td valign=&quot;top&quot; bgcolor=&quot;#ffffff&quot;&gt;
+        &lt;%= yield %&gt;
+      &lt;/td&gt;
+    &lt;/tr&gt;
+  &lt;/table&gt;
+
+  &lt;div align=&quot;right&quot;&gt;
+    &lt;%= link_to image_tag(&quot;tw_dev_logo.gif&quot;), &quot;http://www.thoughtworks.com/&quot; %&gt;
+    &lt;%= image_tag &quot;shim.gif&quot; %&gt;
+  &lt;/div&gt;
+&lt;/body&gt;
+&lt;/html&gt;
+</diff>
      <filename>app/views/layouts/default.rhtml</filename>
    </modified>
    <modified>
      <diff>@@ -1,19 +1,19 @@
-&lt;% @project.emails.each do |email| %&gt;
-  &lt;li&gt;
-    &lt;%= email %&gt;
-    &lt;%= link_to_remote('[x]', {:url =&gt; {:action =&gt; &quot;remove_email&quot;, :id =&gt; @project.name, :value =&gt; email}}) %&gt;
-    &lt;/div&gt;
-  &lt;/li&gt;
-&lt;% end %&gt;
-
-&lt;li id='new_email' class='read'&gt;
-
-  &lt;a class=&quot;likes-read&quot; href=&quot;#&quot; onclick=&quot;mark_for_edit_and_focus($('new_email'), $('new_email_text'))&quot;&gt;add&lt;/a&gt;
-  &lt;% form_remote_tag(:url =&gt; {:action =&gt; &quot;add_email&quot;, :id =&gt; @project.name},
-                      :html =&gt; {:id =&gt; 'new_email_form', :class =&gt; 'inplaceeditor-form, likes-edit'},
-                      :before =&gt; &quot;$('new_email').className='save'&quot;) do %&gt;
-    &lt;input type=&quot;text&quot; id=&quot;new_email_text&quot; name=&quot;value&quot; style=&quot;background-color:#ff9;&quot; class=&quot;editor-field&quot; size=&quot;50&quot;/&gt;
-    &lt;%= submit_tag &quot;ok&quot;, :class =&gt; &quot;editor_ok_button&quot; %&gt;
-  &lt;% end %&gt;
-  &lt;div id='new_email_saving' class='likes-save'&gt;Saving...&lt;/div&gt;
+&lt;% @project.emails.each do |email| %&gt;
+  &lt;li&gt;
+    &lt;%= email %&gt;
+    &lt;%= link_to_remote('[x]', {:url =&gt; {:action =&gt; &quot;remove_email&quot;, :id =&gt; @project.name, :value =&gt; email}}) %&gt;
+    &lt;/div&gt;
+  &lt;/li&gt;
+&lt;% end %&gt;
+
+&lt;li id='new_email' class='read'&gt;
+
+  &lt;a class=&quot;likes-read&quot; href=&quot;#&quot; onclick=&quot;mark_for_edit_and_focus($('new_email'), $('new_email_text'))&quot;&gt;add&lt;/a&gt;
+  &lt;% form_remote_tag(:url =&gt; {:action =&gt; &quot;add_email&quot;, :id =&gt; @project.name},
+                      :html =&gt; {:id =&gt; 'new_email_form', :class =&gt; 'inplaceeditor-form, likes-edit'},
+                      :before =&gt; &quot;$('new_email').className='save'&quot;) do %&gt;
+    &lt;input type=&quot;text&quot; id=&quot;new_email_text&quot; name=&quot;value&quot; style=&quot;background-color:#ff9;&quot; class=&quot;editor-field&quot; size=&quot;50&quot;/&gt;
+    &lt;%= submit_tag &quot;ok&quot;, :class =&gt; &quot;editor_ok_button&quot; %&gt;
+  &lt;% end %&gt;
+  &lt;div id='new_email_saving' class='likes-save'&gt;Saving...&lt;/div&gt;
 &lt;/li&gt;
\ No newline at end of file</diff>
      <filename>app/views/projects/_list.rhtml</filename>
    </modified>
    <modified>
      <diff>@@ -1,27 +1,27 @@
-  &lt;table id=&quot;Plugins&quot; cellpadding=&quot;3&quot; cellspacing=&quot;0&quot; border=&quot;0&quot;&gt;
-    &lt;tr&gt;
-      &lt;td&gt;&lt;img src=&quot;/ccnet/images/shim.gif&quot; width=&quot;180&quot; height=&quot;1&quot;&gt;&lt;/td&gt;
-    &lt;/tr&gt;
-    &lt;tr&gt;
-      &lt;td&gt;
-        &lt;table width=&quot;100%&quot;&gt;
-          &lt;tr&gt;
-            &lt;td&gt;&lt;a href=&quot;/ccnet/ViewFarmReport.aspx&quot; class=&quot;&quot;&gt;Farm Report&lt;/a&gt;&lt;/td&gt;
-          &lt;/tr&gt;
-
-          &lt;tr&gt;
-            &lt;td&gt;&lt;a href=&quot;/ccnet/CCTrayDownload.aspx&quot; class=&quot;&quot;&gt;Download CCTray&lt;/a&gt;&lt;/td&gt;
-          &lt;/tr&gt;
-          &lt;tr&gt;
-            &lt;td&gt;&lt;/td&gt;
-          &lt;/tr&gt;
-          &lt;tr&gt;
-            &lt;td&gt;&lt;b&gt;Servers&lt;/b&gt;&lt;/td&gt;
-          &lt;/tr&gt;
-          &lt;tr&gt;
-            &lt;td&gt;&lt;a href=&quot;/ccnet/server/local/ViewServerReport.aspx&quot; class=&quot;&quot;&gt;local&lt;/a&gt;&lt;/td&gt;
-          &lt;/tr&gt;
-        &lt;/table&gt;
-      &lt;/td&gt;
-    &lt;/tr&gt;
-  &lt;/table&gt;
+  &lt;table id=&quot;Plugins&quot; cellpadding=&quot;3&quot; cellspacing=&quot;0&quot; border=&quot;0&quot;&gt;
+    &lt;tr&gt;
+      &lt;td&gt;&lt;img src=&quot;/ccnet/images/shim.gif&quot; width=&quot;180&quot; height=&quot;1&quot;&gt;&lt;/td&gt;
+    &lt;/tr&gt;
+    &lt;tr&gt;
+      &lt;td&gt;
+        &lt;table width=&quot;100%&quot;&gt;
+          &lt;tr&gt;
+            &lt;td&gt;&lt;a href=&quot;/ccnet/ViewFarmReport.aspx&quot; class=&quot;&quot;&gt;Farm Report&lt;/a&gt;&lt;/td&gt;
+          &lt;/tr&gt;
+
+          &lt;tr&gt;
+            &lt;td&gt;&lt;a href=&quot;/ccnet/CCTrayDownload.aspx&quot; class=&quot;&quot;&gt;Download CCTray&lt;/a&gt;&lt;/td&gt;
+          &lt;/tr&gt;
+          &lt;tr&gt;
+            &lt;td&gt;&lt;/td&gt;
+          &lt;/tr&gt;
+          &lt;tr&gt;
+            &lt;td&gt;&lt;b&gt;Servers&lt;/b&gt;&lt;/td&gt;
+          &lt;/tr&gt;
+          &lt;tr&gt;
+            &lt;td&gt;&lt;a href=&quot;/ccnet/server/local/ViewServerReport.aspx&quot; class=&quot;&quot;&gt;local&lt;/a&gt;&lt;/td&gt;
+          &lt;/tr&gt;
+        &lt;/table&gt;
+      &lt;/td&gt;
+    &lt;/tr&gt;
+  &lt;/table&gt;</diff>
      <filename>app/views/projects/_plugins.rhtml</filename>
    </modified>
    <modified>
      <diff>@@ -1,25 +1,25 @@
-&lt;% @breadcrumbs = &quot;#{link_to(&quot;Dashboard&quot;, &quot;/&quot;)}&quot; %&gt;
-
-&lt;% content_for(:left_side) do %&gt;
-  &lt;%= link_to &quot;Test E-Mail&quot;, :controller =&gt; &quot;test&quot;, :action =&gt; &quot;test_mail&quot; %&gt;
-&lt;% end %&gt;
-
-&lt;table class=&quot;builds&quot;&gt;
-  &lt;tr&gt;
-    &lt;th&gt;Project Name&lt;/th&gt;
-    &lt;th&gt;Last Build Label&lt;/th&gt;
-    &lt;th&gt;Last Build Status&lt;/th&gt;
-    &lt;th&gt;Last Build Time&lt;/th&gt;
-    &lt;!--&lt;th&gt;Builder State&lt;/th&gt;--&gt;
-  &lt;/tr&gt;
-
-  &lt;% @projects.each do |project| %&gt;
-    &lt;tr class=&quot;build-&lt;%= project.last_build.status %&gt;&quot;&gt;
-      &lt;td&gt;&lt;%= link_to project.name, :action =&gt; 'show', :id =&gt; project.name %&gt;&lt;/td&gt;
-      &lt;td&gt;&lt;%= project.last_build.label %&gt;&lt;/td&gt;
-      &lt;td class=&quot;build-status&quot;&gt;&lt;%= project.last_build.status %&gt;&lt;/td&gt;
-      &lt;td&gt;&lt;%= project.last_build.formatted_time %&gt;&lt;/td&gt;
-      &lt;!--&lt;td&gt;Sleeping&lt;/td&gt;--&gt;&lt;!-- stopped / building / checking for modifications / sleeping --&gt;
-    &lt;/tr&gt;
-  &lt;% end %&gt;
-&lt;/table&gt;
+&lt;% @breadcrumbs = &quot;#{link_to(&quot;Dashboard&quot;, &quot;/&quot;)}&quot; %&gt;
+
+&lt;% content_for(:left_side) do %&gt;
+  &lt;%= link_to &quot;Test E-Mail&quot;, :controller =&gt; &quot;test&quot;, :action =&gt; &quot;test_mail&quot; %&gt;
+&lt;% end %&gt;
+
+&lt;table class=&quot;builds&quot;&gt;
+  &lt;tr&gt;
+    &lt;th&gt;Project Name&lt;/th&gt;
+    &lt;th&gt;Last Build Label&lt;/th&gt;
+    &lt;th&gt;Last Build Status&lt;/th&gt;
+    &lt;th&gt;Last Build Time&lt;/th&gt;
+    &lt;!--&lt;th&gt;Builder State&lt;/th&gt;--&gt;
+  &lt;/tr&gt;
+
+  &lt;% @projects.each do |project| %&gt;
+    &lt;tr class=&quot;build-&lt;%= project.last_build.status %&gt;&quot;&gt;
+      &lt;td&gt;&lt;%= link_to project.name, :action =&gt; 'show', :id =&gt; project.name %&gt;&lt;/td&gt;
+      &lt;td&gt;&lt;%= project.last_build.label %&gt;&lt;/td&gt;
+      &lt;td class=&quot;build-status&quot;&gt;&lt;%= project.last_build.status %&gt;&lt;/td&gt;
+      &lt;td&gt;&lt;%= project.last_build.formatted_time %&gt;&lt;/td&gt;
+      &lt;!--&lt;td&gt;Sleeping&lt;/td&gt;--&gt;&lt;!-- stopped / building / checking for modifications / sleeping --&gt;
+    &lt;/tr&gt;
+  &lt;% end %&gt;
+&lt;/table&gt;</diff>
      <filename>app/views/projects/index.rhtml</filename>
    </modified>
    <modified>
      <diff>@@ -1,11 +1,11 @@
-&lt;h1&gt;&lt;%= @project.name %&gt;&lt;/h1&gt;
-
-&lt;h2&gt;Notification&lt;/h2&gt;
-
-&lt;h3&gt;Email&lt;/h3&gt;
-
-&lt;p&gt;When the build fails, send an e-mail to :&lt;p&gt;
-
-&lt;ul id='email_list'&gt;
-  &lt;%= render :partial =&gt; 'list' %&gt;
-&lt;/ul&gt;
+&lt;h1&gt;&lt;%= @project.name %&gt;&lt;/h1&gt;
+
+&lt;h2&gt;Notification&lt;/h2&gt;
+
+&lt;h3&gt;Email&lt;/h3&gt;
+
+&lt;p&gt;When the build fails, send an e-mail to :&lt;p&gt;
+
+&lt;ul id='email_list'&gt;
+  &lt;%= render :partial =&gt; 'list' %&gt;
+&lt;/ul&gt;</diff>
      <filename>app/views/projects/settings.rhtml</filename>
    </modified>
    <modified>
      <diff>@@ -1,52 +1,52 @@
-&lt;% @breadcrumbs = &quot;#{link_to(&quot;Dashboard&quot;, &quot;/&quot;)} &gt; #{link_to(@project.name, :action =&gt; &quot;show&quot;, :id =&gt; @project.name)}&quot; %&gt;
-
-&lt;% content_for(:left_side) do %&gt;
-  &lt;div&gt;&lt;%= link_to(&quot;Latest&quot;, :action =&gt; &quot;show&quot;, :id =&gt; @project.name) %&gt;&lt;/div&gt;
-  &lt;div class=&quot;gray&quot;&gt;Next&lt;/div&gt;
-  &lt;div class=&quot;gray&quot;&gt;Prev&lt;/div&gt;
-  &lt;br/&gt;
-  &lt;div&gt;&lt;%= link_to(&quot;Settings&quot;, :action =&gt; &quot;settings&quot;, :id =&gt; @project.name) %&gt;&lt;/div&gt;
-  &lt;br/&gt;
-  &lt;br/&gt;
-  &lt;% @project.builds.reverse.each do |build| %&gt;
-    &lt;%= link_to &quot;&lt;div class='#{build.status}'&gt;build #{build.label}&lt;/div&gt;&quot;, :id =&gt; @project.name, :build =&gt; build.label %&gt;
-  &lt;% end %&gt;
-&lt;% end %&gt;
-
-&lt;h2&gt;BUILD &lt;%= @build.label %&gt;&lt;/h2&gt;
-
-&lt;table&gt;
-  &lt;tr&gt;
-    &lt;td class=&quot;label&quot;&gt;Project:&lt;/td&gt;
-    &lt;td&gt;&lt;%= @project.name %&gt;&lt;/td&gt;
-  &lt;/tr&gt;
-  &lt;tr&gt;
-    &lt;td class=&quot;label&quot;&gt;Label:&lt;/td&gt;
-    &lt;td&gt;&lt;%= @build.label %&gt;&lt;/td&gt;
-  &lt;/tr&gt;
-  &lt;tr&gt;
-    &lt;td class=&quot;label&quot;&gt;Status:&lt;/td&gt;
-    &lt;td class=&quot;build-status&quot;&gt;&lt;%= @build.status %&gt;&lt;/td&gt;
-  &lt;/tr&gt;
-  &lt;tr&gt;
-    &lt;td class=&quot;label&quot;&gt;Time:&lt;/td&gt;
-    &lt;td&gt;&lt;%= @build.formatted_time %&gt;&lt;/td&gt;
-  &lt;/tr&gt;
-  &lt;tr&gt;
-    &lt;td class=&quot;header-label&quot;&gt;Running time:&lt;/td&gt;
-    &lt;td class=&quot;header-data&quot;&gt;?????&lt;/td&gt;
-  &lt;/tr&gt;
-&lt;/table&gt;
-
-&lt;br/&gt;
-
-&lt;h3&gt;Build Changeset&lt;/h3&gt;
-&lt;pre class=&quot;changeset&quot;&gt;
-&lt;%= format_changeset_log(@build.changeset) %&gt;
-&lt;/pre&gt;
-
-&lt;h3&gt;Build Log&lt;/h3&gt;
-&lt;br/&gt;
-&lt;div class=&quot;logfile&quot;&gt;
-  &lt;%= format_build_log(@build.output) %&gt;
-&lt;/div&gt;
+&lt;% @breadcrumbs = &quot;#{link_to(&quot;Dashboard&quot;, &quot;/&quot;)} &gt; #{link_to(@project.name, :action =&gt; &quot;show&quot;, :id =&gt; @project.name)}&quot; %&gt;
+
+&lt;% content_for(:left_side) do %&gt;
+  &lt;div&gt;&lt;%= link_to(&quot;Latest&quot;, :action =&gt; &quot;show&quot;, :id =&gt; @project.name) %&gt;&lt;/div&gt;
+  &lt;div class=&quot;gray&quot;&gt;Next&lt;/div&gt;
+  &lt;div class=&quot;gray&quot;&gt;Prev&lt;/div&gt;
+  &lt;br/&gt;
+  &lt;div&gt;&lt;%= link_to(&quot;Settings&quot;, :action =&gt; &quot;settings&quot;, :id =&gt; @project.name) %&gt;&lt;/div&gt;
+  &lt;br/&gt;
+  &lt;br/&gt;
+  &lt;% @project.builds.reverse.each do |build| %&gt;
+    &lt;%= link_to &quot;&lt;div class='#{build.status}'&gt;build #{build.label}&lt;/div&gt;&quot;, :id =&gt; @project.name, :build =&gt; build.label %&gt;
+  &lt;% end %&gt;
+&lt;% end %&gt;
+
+&lt;h2&gt;BUILD &lt;%= @build.label %&gt;&lt;/h2&gt;
+
+&lt;table&gt;
+  &lt;tr&gt;
+    &lt;td class=&quot;label&quot;&gt;Project:&lt;/td&gt;
+    &lt;td&gt;&lt;%= @project.name %&gt;&lt;/td&gt;
+  &lt;/tr&gt;
+  &lt;tr&gt;
+    &lt;td class=&quot;label&quot;&gt;Label:&lt;/td&gt;
+    &lt;td&gt;&lt;%= @build.label %&gt;&lt;/td&gt;
+  &lt;/tr&gt;
+  &lt;tr&gt;
+    &lt;td class=&quot;label&quot;&gt;Status:&lt;/td&gt;
+    &lt;td class=&quot;build-status&quot;&gt;&lt;%= @build.status %&gt;&lt;/td&gt;
+  &lt;/tr&gt;
+  &lt;tr&gt;
+    &lt;td class=&quot;label&quot;&gt;Time:&lt;/td&gt;
+    &lt;td&gt;&lt;%= @build.formatted_time %&gt;&lt;/td&gt;
+  &lt;/tr&gt;
+  &lt;tr&gt;
+    &lt;td class=&quot;header-label&quot;&gt;Running time:&lt;/td&gt;
+    &lt;td class=&quot;header-data&quot;&gt;?????&lt;/td&gt;
+  &lt;/tr&gt;
+&lt;/table&gt;
+
+&lt;br/&gt;
+
+&lt;h3&gt;Build Changeset&lt;/h3&gt;
+&lt;pre class=&quot;changeset&quot;&gt;
+&lt;%= format_changeset_log(@build.changeset) %&gt;
+&lt;/pre&gt;
+
+&lt;h3&gt;Build Log&lt;/h3&gt;
+&lt;br/&gt;
+&lt;div class=&quot;logfile&quot;&gt;
+  &lt;%= format_build_log(@build.output) %&gt;
+&lt;/div&gt;</diff>
      <filename>app/views/projects/show.rhtml</filename>
    </modified>
    <modified>
      <diff>@@ -1,3 +1,3 @@
-&lt;ul&gt;
-  &lt;li&gt;&lt;%= link_to &quot;test e-mail&quot;, :action =&gt; &quot;test_mail&quot; %&gt;&lt;/li&gt;
+&lt;ul&gt;
+  &lt;li&gt;&lt;%= link_to &quot;test e-mail&quot;, :action =&gt; &quot;test_mail&quot; %&gt;&lt;/li&gt;
 &lt;/ul&gt;
\ No newline at end of file</diff>
      <filename>app/views/test/index.rhtml</filename>
    </modified>
    <modified>
      <diff>@@ -1,8 +1,8 @@
-&lt;p&gt;Send a test e-mail to :&lt;p&gt;
-
-&lt;%= form_remote_tag(:url =&gt; {:action =&gt; 'send_test_mail'}, :update =&gt; &quot;email_result&quot;) %&gt;
-  &lt;%= text_field &quot;email&quot;, &quot;recipients&quot; %&gt;
-  &lt;%= submit_tag &quot;send&quot; %&gt;
-&lt;%= end_form_tag %&gt;
-
-&lt;div id=&quot;email_result&quot;&gt;&lt;/div&gt;
+&lt;p&gt;Send a test e-mail to :&lt;p&gt;
+
+&lt;%= form_remote_tag(:url =&gt; {:action =&gt; 'send_test_mail'}, :update =&gt; &quot;email_result&quot;) %&gt;
+  &lt;%= text_field &quot;email&quot;, &quot;recipients&quot; %&gt;
+  &lt;%= submit_tag &quot;send&quot; %&gt;
+&lt;%= end_form_tag %&gt;
+
+&lt;div id=&quot;email_result&quot;&gt;&lt;/div&gt;</diff>
      <filename>app/views/test/test_mail.rhtml</filename>
    </modified>
    <modified>
      <diff>@@ -1,45 +1,45 @@
-# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb
-
-unless defined?(RAILS_ROOT)
-  root_path = File.join(File.dirname(__FILE__), '..')
-
-  unless RUBY_PLATFORM =~ /mswin32/
-    require 'pathname'
-    root_path = Pathname.new(root_path).cleanpath(true).to_s
-  end
-
-  RAILS_ROOT = root_path
-end
-
-unless defined?(Rails::Initializer)
-  if File.directory?(&quot;#{RAILS_ROOT}/vendor/rails&quot;)
-    require &quot;#{RAILS_ROOT}/vendor/rails/railties/lib/initializer&quot;
-  else
-    require 'rubygems'
-
-    environment_without_comments = IO.readlines(File.dirname(__FILE__) + '/environment.rb').reject { |l| l =~ /^#/ }.join
-    environment_without_comments =~ /[^#]RAILS_GEM_VERSION = '([\d.]+)'/
-    rails_gem_version = $1
-
-    if version = defined?(RAILS_GEM_VERSION) ? RAILS_GEM_VERSION : rails_gem_version
-      # Asking for 1.1.6 will give you 1.1.6.5206, if available -- makes it easier to use beta gems
-      rails_gem = Gem.cache.search('rails', &quot;~&gt;#{version}.0&quot;).sort_by { |g| g.version.version }.last
-
-      if rails_gem
-        require_gem &quot;rails&quot;, &quot;=#{rails_gem.version.version}&quot;
-        require rails_gem.full_gem_path + '/lib/initializer'
-      else
-        STDERR.puts %(Cannot find gem for Rails ~&gt;#{version}.0:
-    Install the missing gem with 'gem install -v=#{version} rails', or
-    change environment.rb to define RAILS_GEM_VERSION with your desired version.
-  )
-        exit 1
-      end
-    else
-      require_gem &quot;rails&quot;
-      require 'initializer'
-    end
-  end
-
-  Rails::Initializer.run(:set_load_path)
+# Don't change this file. Configuration is done in config/environment.rb and config/environments/*.rb
+
+unless defined?(RAILS_ROOT)
+  root_path = File.join(File.dirname(__FILE__), '..')
+
+  unless RUBY_PLATFORM =~ /mswin32/
+    require 'pathname'
+    root_path = Pathname.new(root_path).cleanpath(true).to_s
+  end
+
+  RAILS_ROOT = root_path
+end
+
+unless defined?(Rails::Initializer)
+  if File.directory?(&quot;#{RAILS_ROOT}/vendor/rails&quot;)
+    require &quot;#{RAILS_ROOT}/vendor/rails/railties/lib/initializer&quot;
+  else
+    require 'rubygems'
+
+    environment_without_comments = IO.readlines(File.dirname(__FILE__) + '/environment.rb').reject { |l| l =~ /^#/ }.join
+    environment_without_comments =~ /[^#]RAILS_GEM_VERSION = '([\d.]+)'/
+    rails_gem_version = $1
+
+    if version = defined?(RAILS_GEM_VERSION) ? RAILS_GEM_VERSION : rails_gem_version
+      # Asking for 1.1.6 will give you 1.1.6.5206, if available -- makes it easier to use beta gems
+      rails_gem = Gem.cache.search('rails', &quot;~&gt;#{version}.0&quot;).sort_by { |g| g.version.version }.last
+
+      if rails_gem
+        require_gem &quot;rails&quot;, &quot;=#{rails_gem.version.version}&quot;
+        require rails_gem.full_gem_path + '/lib/initializer'
+      else
+        STDERR.puts %(Cannot find gem for Rails ~&gt;#{version}.0:
+    Install the missing gem with 'gem install -v=#{version} rails', or
+    change environment.rb to define RAILS_GEM_VERSION with your desired version.
+  )
+        exit 1
+      end
+    else
+      require_gem &quot;rails&quot;
+      require 'initializer'
+    end
+  end
+
+  Rails::Initializer.run(:set_load_path)
 end
\ No newline at end of file</diff>
      <filename>config/boot.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,60 +1,60 @@
-# Be sure to restart your web server when you modify this file.
-
-# Uncomment below to force Rails into production mode when 
-# you don't control web/app server and can't set it the proper way
-# ENV['RAILS_ENV'] ||= 'production'
-
-# Specifies gem version of Rails to use when vendor/rails is not present
-RAILS_GEM_VERSION = '1.2.1' unless defined? RAILS_GEM_VERSION
-
-# Bootstrap the Rails environment, frameworks, and default configuration
-require File.join(File.dirname(__FILE__), 'boot')
-
-Rails::Initializer.run do |config|
-  # Settings in config/environments/* take precedence over those specified here
-
-
-  module ActiveRecord
-    # just so that WhinyNil doesn't complain about const missing
-    class Base
-      # and just so that ActiveRecordStore can load (even though we dont use it either
-      def self.before_save(*args) end 
-      # and just so controller generator can do its stuff 
-      def self.pluralize_table_names() true; end 
-      # and just so that Dispatcher#reset_application works
-      def self.reset_subclasses() end
-      # and just so that Dispatcher#prepare_application works
-      def self.verify_active_connections!() end
-      # and just so that Dispatcher#reset_application! works so Webrick (unlike Mongrel) stops bombing out
-      def self.clear_reloadable_connections!() end
-      # and just so that benchmarking's render() works 
-      def self.connected?() false; end
-      # and just so that Initializer#load_observers works
-      def self.instantiate_observers; end
-    end
-  end
-
-  
-  # Skip frameworks you're not going to use (only works if using vendor/rails)
-  config.frameworks -= [ :active_record, :action_web_service ]
-
-  # Only load the plugins named here, by default all plugins in vendor/plugins are loaded
-  # config.plugins = %W( exception_notification ssl_requirement )
-
-  # Add additional load paths for your own custom dirs
-  # config.load_paths += %W( #{RAILS_ROOT}/extras )
-
-  # Use the database for sessions instead of the file system
-  # (create the session table with 'rake db:sessions:create')
-  # config.action_controller.session_store = :active_record_store
-
-  # See Rails::Configuration for more options
-end
-
-# Include your application configuration below
-require 'cruisecontrol/version'
-
-# Local configuration, for example, details of the SMTP server for email notification, should be 
-# written in ./config/site_config.rb. See ./config/site_sonfig.rb_example for an example of what this file may 
-# look like.
-require 'site_config' if File.exists?(&quot;#{RAILS_ROOT}/config/site_config.rb&quot;)
+# Be sure to restart your web server when you modify this file.
+
+# Uncomment below to force Rails into production mode when 
+# you don't control web/app server and can't set it the proper way
+# ENV['RAILS_ENV'] ||= 'production'
+
+# Specifies gem version of Rails to use when vendor/rails is not present
+RAILS_GEM_VERSION = '1.2.1' unless defined? RAILS_GEM_VERSION
+
+# Bootstrap the Rails environment, frameworks, and default configuration
+require File.join(File.dirname(__FILE__), 'boot')
+
+Rails::Initializer.run do |config|
+  # Settings in config/environments/* take precedence over those specified here
+
+
+  module ActiveRecord
+    # just so that WhinyNil doesn't complain about const missing
+    class Base
+      # and just so that ActiveRecordStore can load (even though we dont use it either
+      def self.before_save(*args) end 
+      # and just so controller generator can do its stuff 
+      def self.pluralize_table_names() true; end 
+      # and just so that Dispatcher#reset_application works
+      def self.reset_subclasses() end
+      # and just so that Dispatcher#prepare_application works
+      def self.verify_active_connections!() end
+      # and just so that Dispatcher#reset_application! works so Webrick (unlike Mongrel) stops bombing out
+      def self.clear_reloadable_connections!() end
+      # and just so that benchmarking's render() works 
+      def self.connected?() false; end
+      # and just so that Initializer#load_observers works
+      def self.instantiate_observers; end
+    end
+  end
+
+  
+  # Skip frameworks you're not going to use (only works if using vendor/rails)
+  config.frameworks -= [ :active_record, :action_web_service ]
+
+  # Only load the plugins named here, by default all plugins in vendor/plugins are loaded
+  # config.plugins = %W( exception_notification ssl_requirement )
+
+  # Add additional load paths for your own custom dirs
+  # config.load_paths += %W( #{RAILS_ROOT}/extras )
+
+  # Use the database for sessions instead of the file system
+  # (create the session table with 'rake db:sessions:create')
+  # config.action_controller.session_store = :active_record_store
+
+  # See Rails::Configuration for more options
+end
+
+# Include your application configuration below
+require 'cruisecontrol/version'
+
+# Local configuration, for example, details of the SMTP server for email notification, should be 
+# written in ./config/site_config.rb. See ./config/site_sonfig.rb_example for an example of what this file may 
+# look like.
+require 'site_config' if File.exists?(&quot;#{RAILS_ROOT}/config/site_config.rb&quot;)</diff>
      <filename>config/environment.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,3 +1,3 @@
-config.cache_classes = true
-config.log_path = OPTIONS[:log_file_name] || 'log/builder_WITHOUT_A_NAME.log'
-config.log_level = OPTIONS[:verbose] ? :debug : :info
+config.cache_classes = true
+config.log_path = OPTIONS[:log_file_name] || 'log/builder_WITHOUT_A_NAME.log'
+config.log_level = OPTIONS[:verbose] ? :debug : :info</diff>
      <filename>config/environments/builder.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,21 +1,21 @@
-# Settings specified here will take precedence over those in config/environment.rb
-
-# In the development environment your application's code is reloaded on
-# every request.  This slows down response time but is perfect for development
-# since you don't have to restart the webserver when you make code changes.
-config.cache_classes = false
-
-# Log error messages when you accidentally call methods on nil.
-config.whiny_nils = true
-
-# Enable the breakpoint server that script/breakpointer connects to
-config.breakpoint_server = true
-
-# Show full error reports and disable caching
-config.action_controller.consider_all_requests_local = true
-config.action_controller.perform_caching             = false
-config.action_view.cache_template_extensions         = false
-config.action_view.debug_rjs                         = true
-
-# Don't care if the mailer can't send
-config.action_mailer.raise_delivery_errors = false
+# Settings specified here will take precedence over those in config/environment.rb
+
+# In the development environment your application's code is reloaded on
+# every request.  This slows down response time but is perfect for development
+# since you don't have to restart the webserver when you make code changes.
+config.cache_classes = false
+
+# Log error messages when you accidentally call methods on nil.
+config.whiny_nils = true
+
+# Enable the breakpoint server that script/breakpointer connects to
+config.breakpoint_server = true
+
+# Show full error reports and disable caching
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching             = false
+config.action_view.cache_template_extensions         = false
+config.action_view.debug_rjs                         = true
+
+# Don't care if the mailer can't send
+config.action_mailer.raise_delivery_errors = false</diff>
      <filename>config/environments/development.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,18 +1,18 @@
-# Settings specified here will take precedence over those in config/environment.rb
-
-# The production environment is meant for finished, &quot;live&quot; apps.
-# Code is not reloaded between requests
-config.cache_classes = true
-
-# Use a different logger for distributed setups
-# config.logger = SyslogLogger.new
-
-# Full error reports are disabled and caching is turned on
-config.action_controller.consider_all_requests_local = false
-config.action_controller.perform_caching             = true
-
-# Enable serving of images, stylesheets, and javascripts from an asset server
-# config.action_controller.asset_host                  = &quot;http://assets.example.com&quot;
-
-# Disable delivery errors, bad email addresses will be ignored
-# config.action_mailer.raise_delivery_errors = false
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The production environment is meant for finished, &quot;live&quot; apps.
+# Code is not reloaded between requests
+config.cache_classes = true
+
+# Use a different logger for distributed setups
+# config.logger = SyslogLogger.new
+
+# Full error reports are disabled and caching is turned on
+config.action_controller.consider_all_requests_local = false
+config.action_controller.perform_caching             = true
+
+# Enable serving of images, stylesheets, and javascripts from an asset server
+# config.action_controller.asset_host                  = &quot;http://assets.example.com&quot;
+
+# Disable delivery errors, bad email addresses will be ignored
+# config.action_mailer.raise_delivery_errors = false</diff>
      <filename>config/environments/production.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,19 +1,19 @@
-# Settings specified here will take precedence over those in config/environment.rb
-
-# The test environment is used exclusively to run your application's
-# test suite.  You never need to work with it otherwise.  Remember that
-# your test database is &quot;scratch space&quot; for the test suite and is wiped
-# and recreated between test runs.  Don't rely on the data there!
-config.cache_classes = true
-
-# Log error messages when you accidentally call methods on nil.
-config.whiny_nils = true
-
-# Show full error reports and disable caching
-config.action_controller.consider_all_requests_local = true
-config.action_controller.perform_caching             = false
-
-# Tell ActionMailer not to deliver emails to the real world.
-# The :test delivery method accumulates sent emails in the
-# ActionMailer::Base.deliveries array.
-config.action_mailer.delivery_method = :test
+# Settings specified here will take precedence over those in config/environment.rb
+
+# The test environment is used exclusively to run your application's
+# test suite.  You never need to work with it otherwise.  Remember that
+# your test database is &quot;scratch space&quot; for the test suite and is wiped
+# and recreated between test runs.  Don't rely on the data there!
+config.cache_classes = true
+
+# Log error messages when you accidentally call methods on nil.
+config.whiny_nils = true
+
+# Show full error reports and disable caching
+config.action_controller.consider_all_requests_local = true
+config.action_controller.perform_caching             = false
+
+# Tell ActionMailer not to deliver emails to the real world.
+# The :test delivery method accumulates sent emails in the
+# ActionMailer::Base.deliveries array.
+config.action_mailer.delivery_method = :test</diff>
      <filename>config/environments/test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,29 +1,29 @@
-ActionController::Routing::Routes.draw do |map|
-  # The priority is based upon order of creation: first created -&gt; highest priority.
-  
-  # Sample of regular route:
-  # map.connect 'products/:id', :controller =&gt; 'catalog', :action =&gt; 'view'
-  # Keep in mind you can assign values other than :controller and :action
-
-  # Sample of named route:
-  # map.purchase 'products/:id/purchase', :controller =&gt; 'catalog', :action =&gt; 'purchase'
-  # This route can be invoked with purchase_url(:id =&gt; product.id)
-  
-  # Sample resource route (maps HTTP verbs to controller actions automatically):
-  # map.resources :products
-
-  # Sample resource route with options:
-  # map.resources :products, :member =&gt; { :short =&gt; :get, :toggle =&gt; :post }, :collection =&gt; { :sold =&gt; :get }
-  
-  # map.connect 'build', :controller =&gt; 'build', :action =&gt; &quot;index&quot;
-  map.connect '', :controller =&gt; 'projects', :action =&gt; 'index'  
-
-  # You can have the root of your site routed with map.root
-  # map.root '', :controller =&gt; &quot;builds&quot;, :action =&gt; &quot;index&quot;
-  
-  # Allow downloading Web Service WSDL as a file with an extension instead of a file named 'wsdl'
-  # map.connect ':controller/service.wsdl', :action =&gt; 'wsdl'
-
-  # Install the default route as the lowest priority.
-  map.connect ':controller/:action/:id'
-end
+ActionController::Routing::Routes.draw do |map|
+  # The priority is based upon order of creation: first created -&gt; highest priority.
+  
+  # Sample of regular route:
+  # map.connect 'products/:id', :controller =&gt; 'catalog', :action =&gt; 'view'
+  # Keep in mind you can assign values other than :controller and :action
+
+  # Sample of named route:
+  # map.purchase 'products/:id/purchase', :controller =&gt; 'catalog', :action =&gt; 'purchase'
+  # This route can be invoked with purchase_url(:id =&gt; product.id)
+  
+  # Sample resource route (maps HTTP verbs to controller actions automatically):
+  # map.resources :products
+
+  # Sample resource route with options:
+  # map.resources :products, :member =&gt; { :short =&gt; :get, :toggle =&gt; :post }, :collection =&gt; { :sold =&gt; :get }
+  
+  # map.connect 'build', :controller =&gt; 'build', :action =&gt; &quot;index&quot;
+  map.connect '', :controller =&gt; 'projects', :action =&gt; 'index'  
+
+  # You can have the root of your site routed with map.root
+  # map.root '', :controller =&gt; &quot;builds&quot;, :action =&gt; &quot;index&quot;
+  
+  # Allow downloading Web Service WSDL as a file with an extension instead of a file named 'wsdl'
+  # map.connect ':controller/service.wsdl', :action =&gt; 'wsdl'
+
+  # Install the default route as the lowest priority.
+  map.connect ':controller/:action/:id'
+end</diff>
      <filename>config/routes.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,15 +1,15 @@
-uri        : http://rubyforge.org
-username   : stellsmi
-is_private : false
-group_ids:
-  lipid: 2866
-  cruisecontrolrb: 2918
-package_ids:
-  lipid: 3394
-  cruisecontrol: 3476
-type_ids:
-  .zip: 3000
-  .tgz: 5000
-  .gem: 1400
-processor_ids:
-  Any: 8000
+uri        : http://rubyforge.org
+username   : stellsmi
+is_private : false
+group_ids:
+  lipid: 2866
+  cruisecontrolrb: 2918
+package_ids:
+  lipid: 3394
+  cruisecontrol: 3476
+type_ids:
+  .zip: 3000
+  .tgz: 5000
+  .gem: 1400
+processor_ids:
+  Any: 8000</diff>
      <filename>config/rubyforge.yml</filename>
    </modified>
    <modified>
      <diff>@@ -1,32 +1,32 @@
-# site_config.rb contains examples of various configuration options for the local installation
-# of CruiseControl.rb.
-
-# EMAIL NOTIFICATION
-# ------------------
-
-# CruiseControl.rb can notify you about build status via email. It uses ActionMailer component of Ruby on Rails 
-# framework. Obviously, ActionMailer needs to know how to send out email messages. 
-# If you have an SMTP server on your network, and it needs no authentication, write this in your site_config.rb:
-# 
-# ActionMailer::Base.server_settings = {
-#   :address =&gt;        &quot;yoursmtpserver&quot;,
-#   :domain =&gt;         &quot;yourdomain.com&quot;,
-# }
-#
-# If you have no SMTP server at hand, you can configure email notification to use GMail SMTP server, as follows
-# (of course, you'll need to create a GMail account):
-#
-# ActionMailer::Base.server_settings = {
-#   :address =&gt;        &quot;smtp.gmail.com&quot;,
-#   :port =&gt;           587,
-#   :domain =&gt;         &quot;yourdomain.com&quot;,
-#   :authentication =&gt; :plain,
-#   :user_name =&gt;      &quot;yourgmailaccount&quot;,
-#   :password =&gt;       &quot;*****&quot;
-# }
-# 
-# The same approach works for other SMTP servers thet require authentication. Note that GMail's SMTP server runs on a 
-# non-standard port 587 (standard port for SMTP is 25).
-#
-# For further details about configuring email notification, see Ruby On Rails documentation for ActionMailer::Base.
-
+# site_config.rb contains examples of various configuration options for the local installation
+# of CruiseControl.rb.
+
+# EMAIL NOTIFICATION
+# ------------------
+
+# CruiseControl.rb can notify you about build status via email. It uses ActionMailer component of Ruby on Rails 
+# framework. Obviously, ActionMailer needs to know how to send out email messages. 
+# If you have an SMTP server on your network, and it needs no authentication, write this in your site_config.rb:
+# 
+# ActionMailer::Base.server_settings = {
+#   :address =&gt;        &quot;yoursmtpserver&quot;,
+#   :domain =&gt;         &quot;yourdomain.com&quot;,
+# }
+#
+# If you have no SMTP server at hand, you can configure email notification to use GMail SMTP server, as follows
+# (of course, you'll need to create a GMail account):
+#
+# ActionMailer::Base.server_settings = {
+#   :address =&gt;        &quot;smtp.gmail.com&quot;,
+#   :port =&gt;           587,
+#   :domain =&gt;         &quot;yourdomain.com&quot;,
+#   :authentication =&gt; :plain,
+#   :user_name =&gt;      &quot;yourgmailaccount&quot;,
+#   :password =&gt;       &quot;*****&quot;
+# }
+# 
+# The same approach works for other SMTP servers thet require authentication. Note that GMail's SMTP server runs on a 
+# non-standard port 587 (standard port for SMTP is 25).
+#
+# For further details about configuring email notification, see Ruby On Rails documentation for ActionMailer::Base.
+</diff>
      <filename>config/site_config.rb_example</filename>
    </modified>
    <modified>
      <diff>@@ -1,2 +1,2 @@
-Use this README file to introduce your application and point to useful places in the API for learning more.
+Use this README file to introduce your application and point to useful places in the API for learning more.
 Run &quot;rake appdoc&quot; to generate API documentation for your models and controllers.
\ No newline at end of file</diff>
      <filename>doc/README_FOR_APP</filename>
    </modified>
    <modified>
      <diff>@@ -1,90 +1,90 @@
-Bugs
-----
-* if project has no rake file, rake uses cruise's rake file and builds cruise... whoops...
-
-Before TW Release 2
--------------------
-* how do the builders get started?  the webapp will need to be able to start / restart / stop them eventually,
-  can we do this now?  how?  can we do it in a way that works on windows / mac / *nix?
-* builder
-  * should build if there is no build dir for the current revision - done
-    * this means, &quot;kicking&quot; the build is as simple as deleting the current build dir
-  * should reload project everytime it wakes up (so as to pick up changes from the webapp
-  * should sleep for at least 30 secs by default
-* webapp
-  * should have email settings / along w/ gmail stuff
-* should we just go from a rails gem?  why not?  if we're a gem ourselves...?
-
-If Possible...
---------------
-* get a new UI from someone like Anne Brent
-* create &quot;Add Project&quot; story - add a project from the web app, should be easy
-* we need working breadcrumbs on the UI - should be easy
-* output from builder should be pretty - minimizing information that is not helpful, and spitting out info that is
-* cleanup the test output
-* better reaction to exceptions in builder (see concerns)
-
-Build
------
-* Configurable build command(s);
-* Make the website a proper &quot;semantic XHTML + CSS&quot; job
-* Configurable polling interval
-
-Dev
----
-* Make rake call explicitly specified task OR default task otherwise
-* project.path setter after every call to Project.new is ugly. Move to the c'tor.
-* Error message when starting a builder for non-existant project is ugly (directory not found)
-* Check what happens with parsing &quot;svn log&quot; output if a project has svn:externals, and there is a new revision in there
-
-Concerns
---------
-? Look at handling of timezones when parsing/displaying revision time. Especially, SubversionLogParser.
-  It may prove an interesting story (hopefully, not).
-
-? Need VERY careful consideration about what to do with exceptions within the main build loop, 
-  especially, all steps of Project#build_revision. Target audience here is the &quot;build monkey&quot; and the 
-  design objective here is &quot;easy troubleshooting&quot;.
-    * this exception kills the builder :
-        svn: Can't connect to host 'rubyforge.org': A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
-        D:/src/lipid/lib/builder/lib/subversion_log_parser.rb:25:in `readline': end of file reached (EOFError)
-                from D:/src/lipid/lib/builder/lib/subversion_log_parser.rb:25:in `parse_log'
-                from D:/src/lipid/lib/builder/lib/subversion.rb:51:in `new_revisions'
-                from D:/src/lipid/lib/builder/lib/command_line.rb:88:in `e'
-                from D:/src/lipid/lib/builder/lib/command_line.rb:86:in `e'
-                from D:/src/lipid/lib/builder/lib/command_line.rb:73:in `execute'
-                from D:/src/lipid/lib/builder/lib/command_line.rb:72:in `execute'
-                from D:/src/lipid/lib/builder/lib/subversion.rb:50:in `new_revisions'
-                from D:/src/lipid/lib/builder/lib/subversion.rb:46:in `new_revisions'
-                from D:/src/lipid/lib/builder/lib/project.rb:26:in `new_revisions'
-                from D:/src/lipid/lib/builder/lib/project.rb:18:in `build_new_checkin'
-                from D:/src/lipid/lib/builder/lib/project.rb:17:in `build_new_checkin'
-                from D:/src/lipid/lib/builder/lib/schedule.rb:9:in `run'
-                from D:/src/lipid/lib/builder/lib/runner.rb:51
-                from d:/dev/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
-                from script/builder:2
-                
-? Builder should do something about local changes in the checked out copy of the project. Options are: ignore them (worst), 
-  write a warning, or overwrite these changes during 'svn update'
-  
-? What to do when we are building revision X, and there is already a directory from our previous attempt to 
-  build that same revision?
-
-  jss: delete it
-
-? Parser doesn't handle gracefully a situation when local copy is ahead of the repository (perhaps, after 
-  the repository was restored from a backup)
-    C:/projects/lipid/builds/passing_project/work averkhov$ svn log -r HEAD:BASE
-    svn: No such revision 7
-    ./subversion_log_parser.rb:7:in `readline': end of file reached (EOFError)
-            from ./subversion_log_parser.rb:7:in `parse'
-            ...
-
-? can subversion really be a connection to subversion?  so it can hold things like urls &amp; user &amp; pass?
-
-? we need to present times very readable manner in the timezone of one's browser
-  * 10:30 AM today, 1:30 PM yesterday, 3:00 AM 5 days ago, 5:30 PM May 3, 2006, etc
-
-? builds have NUMBERS not LABELS - I think - jss
-
+Bugs
+----
+* if project has no rake file, rake uses cruise's rake file and builds cruise... whoops...
+
+Before TW Release 2
+-------------------
+* how do the builders get started?  the webapp will need to be able to start / restart / stop them eventually,
+  can we do this now?  how?  can we do it in a way that works on windows / mac / *nix?
+* builder
+  * should build if there is no build dir for the current revision - done
+    * this means, &quot;kicking&quot; the build is as simple as deleting the current build dir
+  * should reload project everytime it wakes up (so as to pick up changes from the webapp
+  * should sleep for at least 30 secs by default
+* webapp
+  * should have email settings / along w/ gmail stuff
+* should we just go from a rails gem?  why not?  if we're a gem ourselves...?
+
+If Possible...
+--------------
+* get a new UI from someone like Anne Brent
+* create &quot;Add Project&quot; story - add a project from the web app, should be easy
+* we need working breadcrumbs on the UI - should be easy
+* output from builder should be pretty - minimizing information that is not helpful, and spitting out info that is
+* cleanup the test output
+* better reaction to exceptions in builder (see concerns)
+
+Build
+-----
+* Configurable build command(s);
+* Make the website a proper &quot;semantic XHTML + CSS&quot; job
+* Configurable polling interval
+
+Dev
+---
+* Make rake call explicitly specified task OR default task otherwise
+* project.path setter after every call to Project.new is ugly. Move to the c'tor.
+* Error message when starting a builder for non-existant project is ugly (directory not found)
+* Check what happens with parsing &quot;svn log&quot; output if a project has svn:externals, and there is a new revision in there
+
+Concerns
+--------
+? Look at handling of timezones when parsing/displaying revision time. Especially, SubversionLogParser.
+  It may prove an interesting story (hopefully, not).
+
+? Need VERY careful consideration about what to do with exceptions within the main build loop, 
+  especially, all steps of Project#build_revision. Target audience here is the &quot;build monkey&quot; and the 
+  design objective here is &quot;easy troubleshooting&quot;.
+    * this exception kills the builder :
+        svn: Can't connect to host 'rubyforge.org': A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
+        D:/src/lipid/lib/builder/lib/subversion_log_parser.rb:25:in `readline': end of file reached (EOFError)
+                from D:/src/lipid/lib/builder/lib/subversion_log_parser.rb:25:in `parse_log'
+                from D:/src/lipid/lib/builder/lib/subversion.rb:51:in `new_revisions'
+                from D:/src/lipid/lib/builder/lib/command_line.rb:88:in `e'
+                from D:/src/lipid/lib/builder/lib/command_line.rb:86:in `e'
+                from D:/src/lipid/lib/builder/lib/command_line.rb:73:in `execute'
+                from D:/src/lipid/lib/builder/lib/command_line.rb:72:in `execute'
+                from D:/src/lipid/lib/builder/lib/subversion.rb:50:in `new_revisions'
+                from D:/src/lipid/lib/builder/lib/subversion.rb:46:in `new_revisions'
+                from D:/src/lipid/lib/builder/lib/project.rb:26:in `new_revisions'
+                from D:/src/lipid/lib/builder/lib/project.rb:18:in `build_new_checkin'
+                from D:/src/lipid/lib/builder/lib/project.rb:17:in `build_new_checkin'
+                from D:/src/lipid/lib/builder/lib/schedule.rb:9:in `run'
+                from D:/src/lipid/lib/builder/lib/runner.rb:51
+                from d:/dev/ruby/lib/ruby/site_ruby/1.8/rubygems/custom_require.rb:27:in `require'
+                from script/builder:2
+                
+? Builder should do something about local changes in the checked out copy of the project. Options are: ignore them (worst), 
+  write a warning, or overwrite these changes during 'svn update'
+  
+? What to do when we are building revision X, and there is already a directory from our previous attempt to 
+  build that same revision?
+
+  jss: delete it
+
+? Parser doesn't handle gracefully a situation when local copy is ahead of the repository (perhaps, after 
+  the repository was restored from a backup)
+    C:/projects/lipid/builds/passing_project/work averkhov$ svn log -r HEAD:BASE
+    svn: No such revision 7
+    ./subversion_log_parser.rb:7:in `readline': end of file reached (EOFError)
+            from ./subversion_log_parser.rb:7:in `parse'
+            ...
+
+? can subversion really be a connection to subversion?  so it can hold things like urls &amp; user &amp; pass?
+
+? we need to present times very readable manner in the timezone of one's browser
+  * 10:30 AM today, 1:30 PM yesterday, 3:00 AM 5 days ago, 5:30 PM May 3, 2006, etc
+
+? builds have NUMBERS not LABELS - I think - jss
+
 ? Is 10 seconds a good default for polling interval? Methinks, it's not polite when dealing with rubyforge.org. Alex
\ No newline at end of file</diff>
      <filename>doc/tasks.txt</filename>
    </modified>
    <modified>
      <diff>@@ -1,173 +1,173 @@
-require 'platform'
-require 'English'
-
-# borrowed (with modifications) from the RSCM project
-module CommandLine
-
-  QUOTE_REPLACEMENT = (Platform.family == &quot;mswin32&quot;) ? '&quot;' : '\\&quot;'
-  LESS_THAN_REPLACEMENT = (Platform.family == &quot;mswin32&quot;) ? '&lt;' : '\\&lt;'
-
-  class OptionError &lt; StandardError; end
-  class ExecutionError &lt; StandardError
-    attr_reader :cmd, :dir, :exitstatus, :stderr
-    def initialize(cmd, full_cmd, dir, exitstatus, stderr)
-      @cmd, @full_cmd, @dir, @exitstatus, @stderr = cmd, full_cmd, dir, exitstatus, stderr
-    end
-    def to_s
-      &quot;\ndir : #{@dir}\n&quot; +
-      &quot;command : #{@cmd}\n&quot; +
-      &quot;executed command : #{@full_cmd}\n&quot; +
-      &quot;exitstatus: #{@exitstatus}\n&quot; +
-      &quot;STDERR TAIL START\n#{@stderr}\nSTDERR TAIL END\n&quot;
-    end
-  end
-
-  # Executes +cmd+.
-  # If the +:stdout+ and +:stderr+ options are specified, a line consisting
-  # of a prompt (including +cmd+) will be appended to the respective output streams will be appended
-  # to those files, followed by the output itself. Example:
-  #
-  #   CommandLine.execute(&quot;echo hello world&quot;, {:stdout =&gt; &quot;stdout.log&quot;, :stderr =&gt; &quot;stderr.log&quot;})
-  #
-  # will result in the following being written to stdout.log:
-  #
-  #   /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
-  #   hello world
-  #
-  # -and to stderr.log:
-  #   /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
-  #
-  # If a block is passed, the stdout io will be yielded to it (as with IO.popen). In this case the output
-  # will not be written to the stdout file (even if it's specified):
-  #
-  #   /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
-  #   [output captured and therefore not logged]
-  #
-  # If the exitstatus of the command is different from the value specified by the +:exitstatus+ option
-  # (which defaults to 0) then an ExecutionError is raised, its message containing the last 400 bytes of stderr
-  # (provided +:stderr+ was specified)
-  #
-  # You can also specify the +:dir+ option, which will cause the command to be executed in that directory
-  # (default is current directory).
-  #
-  # You can also specify a hash of environment variables in +:env+, which will add additional environment variables
-  # to the default environment.
-  #
-  # Finally, you can specify several commands within one by separating them with '&amp;&amp;' (as you would in a shell).
-  # This will result in several lines to be appended to the log (as if you had executed the commands separately).
-  #
-  # See the unit test for more examples.
-  def execute(cmd, options={}, &amp;proc)
-    raise &quot;Can't have newline in cmd&quot; if cmd =~ /\n/
-    options = {
-        :dir =&gt; Dir.pwd,
-        :escape_quotes =&gt; true,
-        :env =&gt; {},
-        :mode =&gt; 'r',
-        :exitstatus =&gt; 0
-      }.merge(options)
-
-    options[:stdout] = File.expand_path(options[:stdout]) if options[:stdout]
-    options[:stderr] = File.expand_path(options[:stderr]) if options[:stderr]
-
-    Dir.chdir(options[:dir]) do
-      return e(cmd, options, &amp;proc)
-    end
-  end
-  module_function :execute
-
-  private
-
-  def e(cmd, options, &amp;proc)
-    full_cmd = full_cmd(cmd, options, &amp;proc)
-
-    options[:env].each{|k,v| ENV[k]=v}
-    begin
-      Log.debug &quot;#{Platform.prompt} #{cmd}&quot; if options[:stdout].nil?
-      result = IO.popen(full_cmd, options[:mode]) do |io|
-        if proc
-          proc.call(io)
-        else
-          io.each_line do |line|
-            STDOUT.puts line if options[:stdout].nil?
-          end
-        end
-      end
-      exit_status = $CHILD_STATUS
-      raise &quot;$CHILD_STATUS is nil &quot; unless exit_status
-      verify_exit_code(exit_status, cmd, full_cmd, options)
-      return result
-    rescue Errno::ENOENT =&gt; e
-      unless options[:stderr].nil?
-        File.open(options[:stderr], &quot;a&quot;) {|io| io.write(e.message)}
-      else
-        STDERR.puts e.message
-        STDERR.puts e.backtrace.join(&quot;\n&quot;)
-      end
-      raise ExecutionError.new(cmd, full_cmd, options[:dir], nil, e.message)
-    end
-  end
-  module_function :e
-
-  def full_cmd(cmd, options, &amp;proc)
-    commands = cmd.split(&quot;&amp;&amp;&quot;).collect{|c| c.strip}
-    stdout_opt, stderr_opt = redirects(options)
-
-    capture_info_command = (block_given? &amp;&amp; options[:stdout]) ?
-        &quot;echo [output captured and therefore not logged] &gt;&gt; #{options[:stdout]} &amp;&amp; &quot; :
-        &quot;&quot;
-
-    full_cmd = commands.collect do |c|
-      escaped_command = options[:escape_quotes] ? c.gsub(/&quot;/, QUOTE_REPLACEMENT).gsub(/&lt;/, LESS_THAN_REPLACEMENT) : c
-      stdout_prompt_command = options[:stdout] ? &quot;echo #{Platform.prompt} #{escaped_command} &gt;&gt; #{options[:stdout]} &amp;&amp; &quot; : &quot;&quot;
-      stderr_prompt_command = options[:stderr] &amp;&amp; options[:stderr] != options[:stdout] ?
-                                &quot;echo #{Platform.prompt} #{escaped_command} &gt;&gt; #{options[:stderr]} &amp;&amp; &quot; :
-                                &quot;&quot;
-      redirected_command = block_given? ? &quot;#{c} #{stderr_opt}&quot; : &quot;#{c} #{stdout_opt} #{stderr_opt}&quot;
-
-      stdout_prompt_command + capture_info_command + stderr_prompt_command + redirected_command
-    end.join(&quot; &amp;&amp; &quot;)
-  end
-  module_function :full_cmd
-
-  def verify_exit_code(exit_status, cmd, full_cmd, options)
-    if exit_status.exitstatus != options[:exitstatus]
-      if options[:stderr] &amp;&amp; File.exist?(options[:stderr])
-        File.open(options[:stderr]) do |errio|
-          begin
-            errio.seek(-1200, IO::SEEK_END)
-          rescue Errno::EINVAL
-            # ignore - it just means we didn't have 400 bytes.
-          end
-          error_message = errio.read
-        end
-      else
-        error_message = &quot;#{options[:stderr]} doesn't exist&quot;
-      end
-      raise ExecutionError.new(cmd, full_cmd, options[:dir] || Dir.pwd, exit_status.exitstatus, error_message)
-    end
-  end
-  module_function :verify_exit_code
-
-  def redirects(options)
-    stdout_opt = options[:stdout] ? &quot;&gt;&gt; #{options[:stdout]}&quot; : &quot;&quot;
-
-    # redirecting stderr to stdout if they are the same file avoids a file lock conflict
-    stderr_opt =
-        case(options[:stderr])
-        when nil then ''
-        when options[:stdout] then '2&gt;&amp;1'
-        else &quot;2&gt;&gt; #{options[:stderr]}&quot;
-        end
-
-    # let's hope that nobody has slashes in directory names on their win32 file system
-    if Platform.family == 'mswin32'
-      stdout_opt.gsub!('/', '\\')
-      stderr_opt.gsub!('/', '\\')
-    end
-
-    [stdout_opt, stderr_opt]
-  end
-  module_function :redirects
-
+require 'platform'
+require 'English'
+
+# borrowed (with modifications) from the RSCM project
+module CommandLine
+
+  QUOTE_REPLACEMENT = (Platform.family == &quot;mswin32&quot;) ? '&quot;' : '\\&quot;'
+  LESS_THAN_REPLACEMENT = (Platform.family == &quot;mswin32&quot;) ? '&lt;' : '\\&lt;'
+
+  class OptionError &lt; StandardError; end
+  class ExecutionError &lt; StandardError
+    attr_reader :cmd, :dir, :exitstatus, :stderr
+    def initialize(cmd, full_cmd, dir, exitstatus, stderr)
+      @cmd, @full_cmd, @dir, @exitstatus, @stderr = cmd, full_cmd, dir, exitstatus, stderr
+    end
+    def to_s
+      &quot;\ndir : #{@dir}\n&quot; +
+      &quot;command : #{@cmd}\n&quot; +
+      &quot;executed command : #{@full_cmd}\n&quot; +
+      &quot;exitstatus: #{@exitstatus}\n&quot; +
+      &quot;STDERR TAIL START\n#{@stderr}\nSTDERR TAIL END\n&quot;
+    end
+  end
+
+  # Executes +cmd+.
+  # If the +:stdout+ and +:stderr+ options are specified, a line consisting
+  # of a prompt (including +cmd+) will be appended to the respective output streams will be appended
+  # to those files, followed by the output itself. Example:
+  #
+  #   CommandLine.execute(&quot;echo hello world&quot;, {:stdout =&gt; &quot;stdout.log&quot;, :stderr =&gt; &quot;stderr.log&quot;})
+  #
+  # will result in the following being written to stdout.log:
+  #
+  #   /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
+  #   hello world
+  #
+  # -and to stderr.log:
+  #   /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
+  #
+  # If a block is passed, the stdout io will be yielded to it (as with IO.popen). In this case the output
+  # will not be written to the stdout file (even if it's specified):
+  #
+  #   /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
+  #   [output captured and therefore not logged]
+  #
+  # If the exitstatus of the command is different from the value specified by the +:exitstatus+ option
+  # (which defaults to 0) then an ExecutionError is raised, its message containing the last 400 bytes of stderr
+  # (provided +:stderr+ was specified)
+  #
+  # You can also specify the +:dir+ option, which will cause the command to be executed in that directory
+  # (default is current directory).
+  #
+  # You can also specify a hash of environment variables in +:env+, which will add additional environment variables
+  # to the default environment.
+  #
+  # Finally, you can specify several commands within one by separating them with '&amp;&amp;' (as you would in a shell).
+  # This will result in several lines to be appended to the log (as if you had executed the commands separately).
+  #
+  # See the unit test for more examples.
+  def execute(cmd, options={}, &amp;proc)
+    raise &quot;Can't have newline in cmd&quot; if cmd =~ /\n/
+    options = {
+        :dir =&gt; Dir.pwd,
+        :escape_quotes =&gt; true,
+        :env =&gt; {},
+        :mode =&gt; 'r',
+        :exitstatus =&gt; 0
+      }.merge(options)
+
+    options[:stdout] = File.expand_path(options[:stdout]) if options[:stdout]
+    options[:stderr] = File.expand_path(options[:stderr]) if options[:stderr]
+
+    Dir.chdir(options[:dir]) do
+      return e(cmd, options, &amp;proc)
+    end
+  end
+  module_function :execute
+
+  private
+
+  def e(cmd, options, &amp;proc)
+    full_cmd = full_cmd(cmd, options, &amp;proc)
+
+    options[:env].each{|k,v| ENV[k]=v}
+    begin
+      Log.debug &quot;#{Platform.prompt} #{cmd}&quot; if options[:stdout].nil?
+      result = IO.popen(full_cmd, options[:mode]) do |io|
+        if proc
+          proc.call(io)
+        else
+          io.each_line do |line|
+            STDOUT.puts line if options[:stdout].nil?
+          end
+        end
+      end
+      exit_status = $CHILD_STATUS
+      raise &quot;$CHILD_STATUS is nil &quot; unless exit_status
+      verify_exit_code(exit_status, cmd, full_cmd, options)
+      return result
+    rescue Errno::ENOENT =&gt; e
+      unless options[:stderr].nil?
+        File.open(options[:stderr], &quot;a&quot;) {|io| io.write(e.message)}
+      else
+        STDERR.puts e.message
+        STDERR.puts e.backtrace.join(&quot;\n&quot;)
+      end
+      raise ExecutionError.new(cmd, full_cmd, options[:dir], nil, e.message)
+    end
+  end
+  module_function :e
+
+  def full_cmd(cmd, options, &amp;proc)
+    commands = cmd.split(&quot;&amp;&amp;&quot;).collect{|c| c.strip}
+    stdout_opt, stderr_opt = redirects(options)
+
+    capture_info_command = (block_given? &amp;&amp; options[:stdout]) ?
+        &quot;echo [output captured and therefore not logged] &gt;&gt; #{options[:stdout]} &amp;&amp; &quot; :
+        &quot;&quot;
+
+    full_cmd = commands.collect do |c|
+      escaped_command = options[:escape_quotes] ? c.gsub(/&quot;/, QUOTE_REPLACEMENT).gsub(/&lt;/, LESS_THAN_REPLACEMENT) : c
+      stdout_prompt_command = options[:stdout] ? &quot;echo #{Platform.prompt} #{escaped_command} &gt;&gt; #{options[:stdout]} &amp;&amp; &quot; : &quot;&quot;
+      stderr_prompt_command = options[:stderr] &amp;&amp; options[:stderr] != options[:stdout] ?
+                                &quot;echo #{Platform.prompt} #{escaped_command} &gt;&gt; #{options[:stderr]} &amp;&amp; &quot; :
+                                &quot;&quot;
+      redirected_command = block_given? ? &quot;#{c} #{stderr_opt}&quot; : &quot;#{c} #{stdout_opt} #{stderr_opt}&quot;
+
+      stdout_prompt_command + capture_info_command + stderr_prompt_command + redirected_command
+    end.join(&quot; &amp;&amp; &quot;)
+  end
+  module_function :full_cmd
+
+  def verify_exit_code(exit_status, cmd, full_cmd, options)
+    if exit_status.exitstatus != options[:exitstatus]
+      if options[:stderr] &amp;&amp; File.exist?(options[:stderr])
+        File.open(options[:stderr]) do |errio|
+          begin
+            errio.seek(-1200, IO::SEEK_END)
+          rescue Errno::EINVAL
+            # ignore - it just means we didn't have 400 bytes.
+          end
+          error_message = errio.read
+        end
+      else
+        error_message = &quot;#{options[:stderr]} doesn't exist&quot;
+      end
+      raise ExecutionError.new(cmd, full_cmd, options[:dir] || Dir.pwd, exit_status.exitstatus, error_message)
+    end
+  end
+  module_function :verify_exit_code
+
+  def redirects(options)
+    stdout_opt = options[:stdout] ? &quot;&gt;&gt; #{options[:stdout]}&quot; : &quot;&quot;
+
+    # redirecting stderr to stdout if they are the same file avoids a file lock conflict
+    stderr_opt =
+        case(options[:stderr])
+        when nil then ''
+        when options[:stdout] then '2&gt;&amp;1'
+        else &quot;2&gt;&gt; #{options[:stderr]}&quot;
+        end
+
+    # let's hope that nobody has slashes in directory names on their win32 file system
+    if Platform.family == 'mswin32'
+      stdout_opt.gsub!('/', '\\')
+      stderr_opt.gsub!('/', '\\')
+    end
+
+    [stdout_opt, stderr_opt]
+  end
+  module_function :redirects
+
 end
\ No newline at end of file</diff>
      <filename>lib/command_line.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,42 +1,42 @@
-class EmailNotifier
-  attr_accessor :emails
-  
-  def initialize(project = nil)
-    @emails = []
-  end
-
- def build_finished(build)
-    return if @emails.empty?
-
-    if build.failed?
-      BuildMailer.deliver_build_failed(build, @emails)
-    else
-      last_build = build.last
-      if last_build and last_build.failed?
-        BuildMailer.deliver_build_fixed(build, @emails)
-      end
-    end
-  end
-
-  def memento
-    &quot;  project.email_notifier.emails = [\n&quot; +
-    @emails.collect {|email| '    ' + email.to_s.strip.inspect }.join(&quot;,\n&quot;) + &quot;\n&quot; +
-    &quot;  ]&quot;
-  end
-end
-
-class Project
-  plugin :email_notifier
-
-  def emails
-    self.email_notifier.emails
-  end
-
-  def add_email(email)
-    emails &lt;&lt; email
-  end
-
-  def delete_email(email)
-    emails.delete(email)
-  end
-end
+class EmailNotifier
+  attr_accessor :emails
+  
+  def initialize(project = nil)
+    @emails = []
+  end
+
+ def build_finished(build)
+    return if @emails.empty?
+
+    if build.failed?
+      BuildMailer.deliver_build_failed(build, @emails)
+    else
+      last_build = build.last
+      if last_build and last_build.failed?
+        BuildMailer.deliver_build_fixed(build, @emails)
+      end
+    end
+  end
+
+  def memento
+    &quot;  project.email_notifier.emails = [\n&quot; +
+    @emails.collect {|email| '    ' + email.to_s.strip.inspect }.join(&quot;,\n&quot;) + &quot;\n&quot; +
+    &quot;  ]&quot;
+  end
+end
+
+class Project
+  plugin :email_notifier
+
+  def emails
+    self.email_notifier.emails
+  end
+
+  def add_email(email)
+    emails &lt;&lt; email
+  end
+
+  def delete_email(email)
+    emails.delete(email)
+  end
+end</diff>
      <filename>lib/cruise_plugins/email_notifier.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,10 +1,10 @@
-module CruiseControl
-  module VERSION #:nodoc:
-    unless defined? MAJOR
-      MAJOR = 0 
-      MINOR = 2
-
-      STRING = [MAJOR, MINOR].join('.')
-    end
-  end
-end
+module CruiseControl
+  module VERSION #:nodoc:
+    unless defined? MAJOR
+      MAJOR = 0 
+      MINOR = 2
+
+      STRING = [MAJOR, MINOR].join('.')
+    end
+  end
+end</diff>
      <filename>lib/cruisecontrol/version.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,17 +1,17 @@
-class Log
-
-  def self.verbose=(verbose)
-    @verbose = verbose
-  end
-
-  def self.event(description, severity = :info)
-    Log.send(severity.to_sym, &quot;[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] #{description}&quot;)
-    puts &quot;EVENT - #{description}&quot; if @verbose
-  end
-
-  def self.method_missing(method, *args, &amp;block)
-    RAILS_DEFAULT_LOGGER.send(method, *args, &amp;block)
-    puts &quot;#{method} - #{args}&quot; if @verbose
-  end
-  
+class Log
+
+  def self.verbose=(verbose)
+    @verbose = verbose
+  end
+
+  def self.event(description, severity = :info)
+    Log.send(severity.to_sym, &quot;[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] #{description}&quot;)
+    puts &quot;EVENT - #{description}&quot; if @verbose
+  end
+
+  def self.method_missing(method, *args, &amp;block)
+    RAILS_DEFAULT_LOGGER.send(method, *args, &amp;block)
+    puts &quot;#{method} - #{args}&quot; if @verbose
+  end
+  
 end
\ No newline at end of file</diff>
      <filename>lib/log.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,26 +1,26 @@
-require 'rbconfig'
-
-module Platform
-  def family
-    target_os = Config::CONFIG[&quot;target_os&quot;] or raise 'Cannot determine operating system'
-    case target_os
-    when /darwin/ then 'powerpc-darwin'
-    when /32/ then 'mswin32'
-    when /cyg/ then 'cygwin'
-    when /freebsd/ then 'freebsd'
-    when /linux/ then 'linux'
-    else raise &quot;Unknown OS: #{target_os}&quot;
-    end
-  end
-  module_function :family
-
-  def user
-    family == &quot;mswin32&quot; ? ENV['USERNAME'] : ENV['USER']
-  end
-  module_function :user
-
-  def prompt(dir=Dir.pwd)
-    prompt = &quot;#{dir.gsub(/\//, File::SEPARATOR)} #{user}$&quot;
-  end
-  module_function :prompt
+require 'rbconfig'
+
+module Platform
+  def family
+    target_os = Config::CONFIG[&quot;target_os&quot;] or raise 'Cannot determine operating system'
+    case target_os
+    when /darwin/ then 'powerpc-darwin'
+    when /32/ then 'mswin32'
+    when /cyg/ then 'cygwin'
+    when /freebsd/ then 'freebsd'
+    when /linux/ then 'linux'
+    else raise &quot;Unknown OS: #{target_os}&quot;
+    end
+  end
+  module_function :family
+
+  def user
+    family == &quot;mswin32&quot; ? ENV['USERNAME'] : ENV['USER']
+  end
+  module_function :user
+
+  def prompt(dir=Dir.pwd)
+    prompt = &quot;#{dir.gsub(/\//, File::SEPARATOR)} #{user}$&quot;
+  end
+  module_function :prompt
 end
\ No newline at end of file</diff>
      <filename>lib/platform.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,32 +1,32 @@
-class ProjectBlocker
-
-  @@pid_files = {}
-
-  def self.block(project)
-    raise &quot;Already holding a lock on project '#{project.name}'&quot; if @@pid_files.include?(project.name)
-    lock = File.open(pid_file(project), 'w')
-    locked = lock.flock(File::LOCK_EX | File::LOCK_NB)
-    if locked
-      @@pid_files[project.name] = lock
-    else
-      lock.close
-      raise &quot;Another process (probably another builder) holds a lock on project '#{project.name}'.\n&quot; + 
-            &quot;Look for a process with a lock on file #{pid_file(project)}&quot;
-    end
-  end
-  
-  def self.release(project)
-    lock = @@pid_files[project.name]
-    if lock
-      lock.flock(File::LOCK_UN | File::LOCK_NB)
-      lock.close
-      File.delete(lock.path)
-      @@pid_files.delete(project.name)
-    end
-  end
-  
-  def self.pid_file(project)
-    File.expand_path(File.join(project.path, &quot;builder.pid&quot;))
-  end
-
-end
+class ProjectBlocker
+
+  @@pid_files = {}
+
+  def self.block(project)
+    raise &quot;Already holding a lock on project '#{project.name}'&quot; if @@pid_files.include?(project.name)
+    lock = File.open(pid_file(project), 'w')
+    locked = lock.flock(File::LOCK_EX | File::LOCK_NB)
+    if locked
+      @@pid_files[project.name] = lock
+    else
+      lock.close
+      raise &quot;Another process (probably another builder) holds a lock on project '#{project.name}'.\n&quot; + 
+            &quot;Look for a process with a lock on file #{pid_file(project)}&quot;
+    end
+  end
+  
+  def self.release(project)
+    lock = @@pid_files[project.name]
+    if lock
+      lock.flock(File::LOCK_UN | File::LOCK_NB)
+      lock.close
+      File.delete(lock.path)
+      @@pid_files.delete(project.name)
+    end
+  end
+  
+  def self.pid_file(project)
+    File.expand_path(File.join(project.path, &quot;builder.pid&quot;))
+  end
+
+end</diff>
      <filename>lib/project_blocker.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,33 +1,33 @@
-class ProjectLogger
-
-  def initialize(project)
-  end
-
-  def build_started(build)
-    Log.event(&quot;Build #{build.label} started&quot;)
-  end
-  
-  def build_finished(build)
-    message = &quot;Build #{build.label} &quot; + (build.successful? ? 'finished SUCCESSFULLY' : 'FAILED')
-    Log.event(message)
-  end
-  
-  def sleeping
-    Log.event(&quot;Sleeping&quot;, :debug)
-  end
-
-  def polling_source_control
-    Log.event(&quot;Polling source control&quot;, :debug)
-  end
-  
-  def no_new_revisions_detected
-    Log.event(&quot;No new revisions detected&quot;, :debug)
-  end
-  
-  def new_revisions_detected(new_revisions)
-    Log.event(&quot;New revision #{new_revisions.last.number} detected&quot;)
-  end
-
-end
-
+class ProjectLogger
+
+  def initialize(project)
+  end
+
+  def build_started(build)
+    Log.event(&quot;Build #{build.label} started&quot;)
+  end
+  
+  def build_finished(build)
+    message = &quot;Build #{build.label} &quot; + (build.successful? ? 'finished SUCCESSFULLY' : 'FAILED')
+    Log.event(message)
+  end
+  
+  def sleeping
+    Log.event(&quot;Sleeping&quot;, :debug)
+  end
+
+  def polling_source_control
+    Log.event(&quot;Polling source control&quot;, :debug)
+  end
+  
+  def no_new_revisions_detected
+    Log.event(&quot;No new revisions detected&quot;, :debug)
+  end
+  
+  def new_revisions_detected(new_revisions)
+    Log.event(&quot;New revision #{new_revisions.last.number} detected&quot;)
+  end
+
+end
+
 Project.plugin :email_notifier
\ No newline at end of file</diff>
      <filename>lib/project_logger.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,4 +1,4 @@
-[:'test:recent', :'test:units', :'test:functionals', :'test:integration'].each do |task|
-  # Removes each of their db:test:prepare dependency
-  Rake::Task[task].prerequisites.clear
+[:'test:recent', :'test:units', :'test:functionals', :'test:integration'].each do |task|
+  # Removes each of their db:test:prepare dependency
+  Rake::Task[task].prerequisites.clear
 end
\ No newline at end of file</diff>
      <filename>lib/tasks/clear_database_prerequisites.rake</filename>
    </modified>
    <modified>
      <diff>@@ -1,32 +1,32 @@
-# Create compressed packages
-spec = Gem::Specification.new do |s|
-  s.platform = Gem::Platform::RUBY
-  s.name = PKG_NAME
-  s.summary = &quot;Continuous Integration Server for Ruby.&quot;
-  s.description = %q{Continuous Integration made easy.}
-  s.version = PKG_VERSION
-
-  s.author = &quot;ThoughtWorks&quot;
-  s.email = &quot;jeremystellsmith@gmail.com&quot;
-  s.rubyforge_project = RUBY_FORGE_PROJECT
-  s.homepage = &quot;http://#{RUBY_FORGE_PROJECT}.rubyforge.org&quot;
-
-  s.has_rdoc = false
-#  s.requirements &lt;&lt; 'none'
-  s.require_path = 'lib'
-#  s.autorequire = 'action_mailer'
-
-  s.default_executable = 'cruise'
-  s.executables = ['cruise']
-
-  s.files = [ &quot;Rakefile&quot;, &quot;README&quot;, &quot;CHANGELOG&quot;, &quot;LICENSE&quot; ] +
-            Dir.glob( &quot;{bin,app,config,lib,public,script,test}/**/*&quot; ) +
-            Dir.glob( &quot;{bin,app,config,lib,public,script,test}/**/.svn/*&quot; ) +
-            Dir.glob( &quot;vendor/**/*&quot; ).delete_if { |item| item.include?( &quot;\.svn&quot; ) }
-end
-
-Rake::GemPackageTask.new(spec) do |p|
-  p.gem_spec = spec
-  p.need_tar = true
-  p.need_zip = true
-end
+# Create compressed packages
+spec = Gem::Specification.new do |s|
+  s.platform = Gem::Platform::RUBY
+  s.name = PKG_NAME
+  s.summary = &quot;Continuous Integration Server for Ruby.&quot;
+  s.description = %q{Continuous Integration made easy.}
+  s.version = PKG_VERSION
+
+  s.author = &quot;ThoughtWorks&quot;
+  s.email = &quot;jeremystellsmith@gmail.com&quot;
+  s.rubyforge_project = RUBY_FORGE_PROJECT
+  s.homepage = &quot;http://#{RUBY_FORGE_PROJECT}.rubyforge.org&quot;
+
+  s.has_rdoc = false
+#  s.requirements &lt;&lt; 'none'
+  s.require_path = 'lib'
+#  s.autorequire = 'action_mailer'
+
+  s.default_executable = 'cruise'
+  s.executables = ['cruise']
+
+  s.files = [ &quot;Rakefile&quot;, &quot;README&quot;, &quot;CHANGELOG&quot;, &quot;LICENSE&quot; ] +
+            Dir.glob( &quot;{bin,app,config,lib,public,script,test}/**/*&quot; ) +
+            Dir.glob( &quot;{bin,app,config,lib,public,script,test}/**/.svn/*&quot; ) +
+            Dir.glob( &quot;vendor/**/*&quot; ).delete_if { |item| item.include?( &quot;\.svn&quot; ) }
+end
+
+Rake::GemPackageTask.new(spec) do |p|
+  p.gem_spec = spec
+  p.need_tar = true
+  p.need_zip = true
+end</diff>
      <filename>lib/tasks/package.rake</filename>
    </modified>
    <modified>
      <diff>@@ -1,4 +1,4 @@
-desc &quot;Publish the API documentation&quot;
-task :pgem =&gt; [:package] do
-  Rake::SshFilePublisher.new(&quot;davidhh@wrath.rubyonrails.org&quot;, &quot;public_html/gems/gems&quot;, &quot;pkg&quot;, &quot;#{PKG_FILE_NAME}.gem&quot;).upload
-end
+desc &quot;Publish the API documentation&quot;
+task :pgem =&gt; [:package] do
+  Rake::SshFilePublisher.new(&quot;davidhh@wrath.rubyonrails.org&quot;, &quot;public_html/gems/gems&quot;, &quot;pkg&quot;, &quot;#{PKG_FILE_NAME}.gem&quot;).upload
+end</diff>
      <filename>lib/tasks/pgem.rake</filename>
    </modified>
    <modified>
      <diff>@@ -1,15 +1,15 @@
-desc &quot;Publish the release files to RubyForge. May not work.&quot;
-task :release do
-#task :release =&gt; [ :package ] do
-  require 'rubyforge'
-
-  options = {&quot;cookie_jar&quot; =&gt; RubyForge::COOKIE_F}
-  puts &quot;Enter rubyforge password:&quot;
-  options[&quot;password&quot;] = $stdin.gets.strip
-  ruby_forge = RubyForge.new(File.dirname(__FILE__) + &quot;/config/rubyforge.yml&quot;, options)
-  ruby_forge.login
-
-  files = %w( tgz zip ).collect {|ext| &quot;pkg/#{PKG_FILE_NAME}.#{ext}&quot;}
-  puts &quot;Releasing #{files.collect{|f| File.basename(f)}.join(&quot;, &quot;)}...&quot;
-  ruby_forge.add_release(RUBY_FORGE_PROJECT, PKG_NAME, PKG_VERSION, *files)
+desc &quot;Publish the release files to RubyForge. May not work.&quot;
+task :release do
+#task :release =&gt; [ :package ] do
+  require 'rubyforge'
+
+  options = {&quot;cookie_jar&quot; =&gt; RubyForge::COOKIE_F}
+  puts &quot;Enter rubyforge password:&quot;
+  options[&quot;password&quot;] = $stdin.gets.strip
+  ruby_forge = RubyForge.new(File.dirname(__FILE__) + &quot;/config/rubyforge.yml&quot;, options)
+  ruby_forge.login
+
+  files = %w( tgz zip ).collect {|ext| &quot;pkg/#{PKG_FILE_NAME}.#{ext}&quot;}
+  puts &quot;Releasing #{files.collect{|f| File.basename(f)}.join(&quot;, &quot;)}...&quot;
+  ruby_forge.add_release(RUBY_FORGE_PROJECT, PKG_NAME, PKG_VERSION, *files)
 end
\ No newline at end of file</diff>
      <filename>lib/tasks/release.rake</filename>
    </modified>
    <modified>
      <diff>@@ -1,30 +1,30 @@
-&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD XHTML 1.0 Transitional//EN&quot;
-       &quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quot;&gt;
-
-&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot; xml:lang=&quot;en&quot; lang=&quot;en&quot;&gt;
-
-&lt;head&gt;
-  &lt;meta http-equiv=&quot;content-type&quot; content=&quot;text/html; charset=UTF-8&quot; /&gt;
-  &lt;title&gt;The page you were looking for doesn't exist (404)&lt;/title&gt;
-	&lt;style type=&quot;text/css&quot;&gt;
-		body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
-		div.dialog {
-			width: 25em;
-			padding: 0 4em;
-			margin: 4em auto 0 auto;
-			border: 1px solid #ccc;
-			border-right-color: #999;
-			border-bottom-color: #999;
-		}
-		h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
-	&lt;/style&gt;
-&lt;/head&gt;
-
-&lt;body&gt;
-  &lt;!-- This file lives in public/404.html --&gt;
-  &lt;div class=&quot;dialog&quot;&gt;
-    &lt;h1&gt;The page you were looking for doesn't exist.&lt;/h1&gt;
-    &lt;p&gt;You may have mistyped the address or the page may have moved.&lt;/p&gt;
-  &lt;/div&gt;
-&lt;/body&gt;
+&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD XHTML 1.0 Transitional//EN&quot;
+       &quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quot;&gt;
+
+&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot; xml:lang=&quot;en&quot; lang=&quot;en&quot;&gt;
+
+&lt;head&gt;
+  &lt;meta http-equiv=&quot;content-type&quot; content=&quot;text/html; charset=UTF-8&quot; /&gt;
+  &lt;title&gt;The page you were looking for doesn't exist (404)&lt;/title&gt;
+	&lt;style type=&quot;text/css&quot;&gt;
+		body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
+		div.dialog {
+			width: 25em;
+			padding: 0 4em;
+			margin: 4em auto 0 auto;
+			border: 1px solid #ccc;
+			border-right-color: #999;
+			border-bottom-color: #999;
+		}
+		h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+	&lt;/style&gt;
+&lt;/head&gt;
+
+&lt;body&gt;
+  &lt;!-- This file lives in public/404.html --&gt;
+  &lt;div class=&quot;dialog&quot;&gt;
+    &lt;h1&gt;The page you were looking for doesn't exist.&lt;/h1&gt;
+    &lt;p&gt;You may have mistyped the address or the page may have moved.&lt;/p&gt;
+  &lt;/div&gt;
+&lt;/body&gt;
 &lt;/html&gt;
\ No newline at end of file</diff>
      <filename>public/404.html</filename>
    </modified>
    <modified>
      <diff>@@ -1,30 +1,30 @@
-&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD XHTML 1.0 Transitional//EN&quot;
-       &quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quot;&gt;
-
-&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot; xml:lang=&quot;en&quot; lang=&quot;en&quot;&gt;
-
-&lt;head&gt;
-  &lt;meta http-equiv=&quot;content-type&quot; content=&quot;text/html; charset=UTF-8&quot; /&gt;
-  &lt;title&gt;We're sorry, but something went wrong&lt;/title&gt;
-	&lt;style type=&quot;text/css&quot;&gt;
-		body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
-		div.dialog {
-			width: 25em;
-			padding: 0 4em;
-			margin: 4em auto 0 auto;
-			border: 1px solid #ccc;
-			border-right-color: #999;
-			border-bottom-color: #999;
-		}
-		h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
-	&lt;/style&gt;
-&lt;/head&gt;
-
-&lt;body&gt;
-  &lt;!-- This file lives in public/500.html --&gt;
-  &lt;div class=&quot;dialog&quot;&gt;
-    &lt;h1&gt;We're sorry, but something went wrong.&lt;/h1&gt;
-    &lt;p&gt;We've been notified about this issue and we'll take a look at it shortly.&lt;/p&gt;
-  &lt;/div&gt;
-&lt;/body&gt;
+&lt;!DOCTYPE html PUBLIC &quot;-//W3C//DTD XHTML 1.0 Transitional//EN&quot;
+       &quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quot;&gt;
+
+&lt;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot; xml:lang=&quot;en&quot; lang=&quot;en&quot;&gt;
+
+&lt;head&gt;
+  &lt;meta http-equiv=&quot;content-type&quot; content=&quot;text/html; charset=UTF-8&quot; /&gt;
+  &lt;title&gt;We're sorry, but something went wrong&lt;/title&gt;
+	&lt;style type=&quot;text/css&quot;&gt;
+		body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
+		div.dialog {
+			width: 25em;
+			padding: 0 4em;
+			margin: 4em auto 0 auto;
+			border: 1px solid #ccc;
+			border-right-color: #999;
+			border-bottom-color: #999;
+		}
+		h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+	&lt;/style&gt;
+&lt;/head&gt;
+
+&lt;body&gt;
+  &lt;!-- This file lives in public/500.html --&gt;
+  &lt;div class=&quot;dialog&quot;&gt;
+    &lt;h1&gt;We're sorry, but something went wrong.&lt;/h1&gt;
+    &lt;p&gt;We've been notified about this issue and we'll take a look at it shortly.&lt;/p&gt;
+  &lt;/div&gt;
+&lt;/body&gt;
 &lt;/html&gt;
\ No newline at end of file</diff>
      <filename>public/500.html</filename>
    </modified>
    <modified>
      <diff>@@ -1,10 +1,10 @@
-#!/usr/bin/env ruby
-
-require File.dirname(__FILE__) + &quot;/../config/environment&quot; unless defined?(RAILS_ROOT)
-
-# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
-# &quot;/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher&quot; -- otherwise performance is severely impaired
-require &quot;dispatcher&quot;
-
-ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + &quot;/../config/environment&quot; unless defined?(RAILS_ROOT)
+
+# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
+# &quot;/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher&quot; -- otherwise performance is severely impaired
+require &quot;dispatcher&quot;
+
+ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
 Dispatcher.dispatch
\ No newline at end of file</diff>
      <filename>public/dispatch.cgi</filename>
    </modified>
    <modified>
      <diff>@@ -1,24 +1,24 @@
-#!/usr/bin/env ruby
-#
-# You may specify the path to the FastCGI crash log (a log of unhandled
-# exceptions which forced the FastCGI instance to exit, great for debugging)
-# and the number of requests to process before running garbage collection.
-#
-# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
-# and the GC period is nil (turned off).  A reasonable number of requests
-# could range from 10-100 depending on the memory footprint of your app.
-#
-# Example:
-#   # Default log path, normal GC behavior.
-#   RailsFCGIHandler.process!
-#
-#   # Default log path, 50 requests between GC.
-#   RailsFCGIHandler.process! nil, 50
-#
-#   # Custom log path, normal GC behavior.
-#   RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log'
-#
-require File.dirname(__FILE__) + &quot;/../config/environment&quot;
-require 'fcgi_handler'
-
-RailsFCGIHandler.process!
+#!/usr/bin/env ruby
+#
+# You may specify the path to the FastCGI crash log (a log of unhandled
+# exceptions which forced the FastCGI instance to exit, great for debugging)
+# and the number of requests to process before running garbage collection.
+#
+# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
+# and the GC period is nil (turned off).  A reasonable number of requests
+# could range from 10-100 depending on the memory footprint of your app.
+#
+# Example:
+#   # Default log path, normal GC behavior.
+#   RailsFCGIHandler.process!
+#
+#   # Default log path, 50 requests between GC.
+#   RailsFCGIHandler.process! nil, 50
+#
+#   # Custom log path, normal GC behavior.
+#   RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log'
+#
+require File.dirname(__FILE__) + &quot;/../config/environment&quot;
+require 'fcgi_handler'
+
+RailsFCGIHandler.process!</diff>
      <filename>public/dispatch.fcgi</filename>
    </modified>
    <modified>
      <diff>@@ -1,10 +1,10 @@
-#!/usr/bin/env ruby
-
-require File.dirname(__FILE__) + &quot;/../config/environment&quot; unless defined?(RAILS_ROOT)
-
-# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
-# &quot;/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher&quot; -- otherwise performance is severely impaired
-require &quot;dispatcher&quot;
-
-ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + &quot;/../config/environment&quot; unless defined?(RAILS_ROOT)
+
+# If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
+# &quot;/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher&quot; -- otherwise performance is severely impaired
+require &quot;dispatcher&quot;
+
+ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
 Dispatcher.dispatch
\ No newline at end of file</diff>
      <filename>public/dispatch.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,7 +1,7 @@
-// Place your application-specific JavaScript functions and classes here
-// This file is automatically included by javascript_include_tag :defaults
-
-function mark_for_edit_and_focus(to_mark, to_focus) {
-  to_mark.className = 'edit';
-  to_focus.focus();
+// Place your application-specific JavaScript functions and classes here
+// This file is automatically included by javascript_include_tag :defaults
+
+function mark_for_edit_and_focus(to_mark, to_focus) {
+  to_mark.className = 'edit';
+  to_focus.focus();
 }
\ No newline at end of file</diff>
      <filename>public/javascripts/application.js</filename>
    </modified>
    <modified>
      <diff>@@ -1,833 +1,833 @@
-// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
-//           (c) 2005, 2006 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
-//           (c) 2005, 2006 Jon Tirsen (http://www.tirsen.com)
-// Contributors:
-//  Richard Livsey
-//  Rahul Bhargava
-//  Rob Wills
-// 
-// script.aculo.us is freely distributable under the terms of an MIT-style license.
-// For details, see the script.aculo.us web site: http://script.aculo.us/
-
-// 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.
-
-if(typeof Effect == 'undefined')
-  throw(&quot;controls.js requires including script.aculo.us' effects.js library&quot;);
-
-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, &quot;blur&quot;, this.onBlur.bindAsEventListener(this));
-    Event.observe(this.element, &quot;keypress&quot;, this.onKeyPress.bindAsEventListener(this));
-  },
-
-  show: function() {
-    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
-    if(!this.iefix &amp;&amp; 
-      (navigator.appVersion.indexOf('MSIE')&gt;0) &amp;&amp;
-      (navigator.userAgent.indexOf('Opera')&lt;0) &amp;&amp;
-      (Element.getStyle(this.update, 'position')=='absolute')) {
-      new Insertion.After(this.update, 
-       '&lt;iframe id=&quot;' + this.update.id + '_iefix&quot; '+
-       'style=&quot;display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);&quot; ' +
-       'src=&quot;javascript:false;&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot;&gt;&lt;/iframe&gt;');
-      this.iefix = $(this.update.id+'_iefix');
-    }
-    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
-  },
-  
-  fixIEOverlapping: function() {
-    Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
-    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')&gt;0) Event.stop(event);
-         return;
-       case Event.KEY_DOWN:
-         this.markNext();
-         this.render();
-         if(navigator.appVersion.indexOf('AppleWebKit')&gt;0) Event.stop(event);
-         return;
-      }
-     else 
-       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 
-         (navigator.appVersion.indexOf('AppleWebKit') &gt; 0 &amp;&amp; event.keyCode == 0)) return;
-
-    this.changed = true;
-    this.hasFocus = true;
-
-    if(this.observer) clearTimeout(this.observer);
-      this.observer = 
-        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
-  },
-
-  activate: function() {
-    this.changed = false;
-    this.hasFocus = true;
-    this.getUpdatedChoices();
-  },
-
-  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 &gt; 0) {
-      for (var i = 0; i &lt; this.entryCount; i++)
-        this.index==i ? 
-          Element.addClassName(this.getEntry(i),&quot;selected&quot;) : 
-          Element.removeClassName(this.getEntry(i),&quot;selected&quot;);
-        
-      if(this.hasFocus) { 
-        this.show();
-        this.active = true;
-      }
-    } else {
-      this.active = false;
-      this.hide();
-    }
-  },
-  
-  markPrevious: function() {
-    if(this.index &gt; 0) this.index--
-      else this.index = this.entryCount-1;
-    this.getEntry(this.index).scrollIntoView(true);
-  },
-  
-  markNext: function() {
-    if(this.index &lt; this.entryCount-1) this.index++
-      else this.index = 0;
-    this.getEntry(this.index).scrollIntoView(false);
-  },
-  
-  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 = '';
-    if (this.options.select) {
-      var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
-      if(nodes.length&gt;0) value = Element.collectTextNodes(nodes[0], this.options.select);
-    } else
-      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 &amp;&amp; this.hasFocus) {
-      this.update.innerHTML = choices;
-      Element.cleanWhitespace(this.update);
-      Element.cleanWhitespace(this.update.down());
-
-      if(this.update.firstChild &amp;&amp; this.update.down().childNodes) {
-        this.entryCount = 
-          this.update.down().childNodes.length;
-        for (var i = 0; i &lt; this.entryCount; i++) {
-          var entry = this.getEntry(i);
-          entry.autocompleteIndex = i;
-          this.addObservers(entry);
-        }
-      } else { 
-        this.entryCount = 0;
-      }
-
-      this.stopIndicator();
-      this.index = 0;
-      
-      if(this.entryCount==1 &amp;&amp; this.options.autoSelect) {
-        this.selectEntry();
-        this.hide();
-      } else {
-        this.render();
-      }
-    }
-  },
-
-  addObservers: function(element) {
-    Event.observe(element, &quot;mouseover&quot;, this.onHover.bindAsEventListener(this));
-    Event.observe(element, &quot;click&quot;, this.onClick.bindAsEventListener(this));
-  },
-
-  onObserverEvent: function() {
-    this.changed = false;   
-    if(this.getToken().length&gt;=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&lt;this.options.tokens.length; i++) {
-      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
-      if (thisTokenPos &gt; 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 += '&amp;' + 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 &lt; instance.options.array.length &amp;&amp;  
-          ret.length &lt; 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 &amp;&amp; elem.length != entry.length) { 
-              ret.push(&quot;&lt;li&gt;&lt;strong&gt;&quot; + elem.substr(0, entry.length) + &quot;&lt;/strong&gt;&quot; + 
-                elem.substr(entry.length) + &quot;&lt;/li&gt;&quot;);
-              break;
-            } else if (entry.length &gt;= instance.options.partialChars &amp;&amp; 
-              instance.options.partialSearch &amp;&amp; foundPos != -1) {
-              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
-                partial.push(&quot;&lt;li&gt;&quot; + elem.substr(0, foundPos) + &quot;&lt;strong&gt;&quot; +
-                  elem.substr(foundPos, entry.length) + &quot;&lt;/strong&gt;&quot; + elem.substr(
-                  foundPos + entry.length) + &quot;&lt;/li&gt;&quot;);
-                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 &quot;&lt;ul&gt;&quot; + ret.join('') + &quot;&lt;/ul&gt;&quot;;
-      }
-    }, 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 = &quot;#FFFF99&quot;;
-Ajax.InPlaceEditor.prototype = {
-  initialize: function(element, url, options) {
-    this.url = url;
-    this.element = $(element);
-
-    this.options = Object.extend({
-      paramName: &quot;value&quot;,
-      okButton: true,
-      okText: &quot;ok&quot;,
-      cancelLink: true,
-      cancelText: &quot;cancel&quot;,
-      savingText: &quot;Saving...&quot;,
-      clickToEditText: &quot;Click to edit&quot;,
-      okText: &quot;ok&quot;,
-      rows: 1,
-      onComplete: function(transport, element) {
-        new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
-      },
-      onFailure: function(transport) {
-        alert(&quot;Error communicating with the server: &quot; + 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: &quot;#FFFFFF&quot;,
-      externalControl: null,
-      submitOnBlur: false,
-      ajaxOptions: {},
-      evalScripts: false
-    }, options || {});
-
-    if(!this.options.formId &amp;&amp; this.element.id) {
-      this.options.formId = this.element.id + &quot;-inplaceeditor&quot;;
-      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 = &quot;transparent&quot;;
-    }
-    
-    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);
-    if (!this.options.loadTextURL) 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(&quot;form&quot;);
-    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(&quot;br&quot;);
-      this.form.appendChild(br);
-    }
-
-    if (this.options.okButton) {
-      okButton = document.createElement(&quot;input&quot;);
-      okButton.type = &quot;submit&quot;;
-      okButton.value = this.options.okText;
-      okButton.className = 'editor_ok_button';
-      this.form.appendChild(okButton);
-    }
-
-    if (this.options.cancelLink) {
-      cancelLink = document.createElement(&quot;a&quot;);
-      cancelLink.href = &quot;#&quot;;
-      cancelLink.appendChild(document.createTextNode(this.options.cancelText));
-      cancelLink.onclick = this.onclickCancel.bind(this);
-      cancelLink.className = 'editor_cancel';      
-      this.form.appendChild(cancelLink);
-    }
-  },
-  hasHTMLLineBreaks: function(string) {
-    if (!this.options.handleLineBreaks) return false;
-    return string.match(/&lt;br/i) || string.match(/&lt;p&gt;/i);
-  },
-  convertHTMLLineBreaks: function(string) {
-    return string.replace(/&lt;br&gt;/gi, &quot;\n&quot;).replace(/&lt;br\/&gt;/gi, &quot;\n&quot;).replace(/&lt;\/p&gt;/gi, &quot;\n&quot;).replace(/&lt;p&gt;/gi, &quot;&quot;);
-  },
-  createEditField: function() {
-    var text;
-    if(this.options.loadTextURL) {
-      text = this.options.loadingText;
-    } else {
-      text = this.getText();
-    }
-
-    var obj = this;
-    
-    if (this.options.rows == 1 &amp;&amp; !this.hasHTMLLineBreaks(text)) {
-      this.options.textarea = false;
-      var textField = document.createElement(&quot;input&quot;);
-      textField.obj = this;
-      textField.type = &quot;text&quot;;
-      textField.name = this.options.paramName;
-      textField.value = text;
-      textField.style.backgroundColor = this.options.highlightcolor;
-      textField.className = 'editor_field';
-      var size = this.options.size || this.options.cols || 0;
-      if (size != 0) textField.size = size;
-      if (this.options.submitOnBlur)
-        textField.onblur = this.onSubmit.bind(this);
-      this.editField = textField;
-    } else {
-      this.options.textarea = true;
-      var textArea = document.createElement(&quot;textarea&quot;);
-      textArea.obj = this;
-      textArea.name = this.options.paramName;
-      textArea.value = this.convertHTMLLineBreaks(text);
-      textArea.rows = this.options.rows;
-      textArea.cols = this.options.cols || 40;
-      textArea.className = 'editor_field';      
-      if (this.options.submitOnBlur)
-        textArea.onblur = this.onSubmit.bind(this);
-      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();
-    Field.scrollFreeActivate(this.editField);
-  },
-  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();
-    
-    if (this.options.evalScripts) {
-      new Ajax.Request(
-        this.url, Object.extend({
-          parameters: this.options.callback(form, value),
-          onComplete: this.onComplete.bind(this),
-          onFailure: this.onFailure.bind(this),
-          asynchronous:true, 
-          evalScripts:true
-        }, this.options.ajaxOptions));
-    } else  {
-      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 &gt; 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);
-    }
-  }
-};
-
-Ajax.InPlaceCollectionEditor = Class.create();
-Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
-Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
-  createEditField: function() {
-    if (!this.cached_selectTag) {
-      var selectTag = document.createElement(&quot;select&quot;);
-      var collection = this.options.collection || [];
-      var optionTag;
-      collection.each(function(e,i) {
-        optionTag = document.createElement(&quot;option&quot;);
-        optionTag.value = (e instanceof Array) ? e[0] : e;
-        if((typeof this.options.value == 'undefined') &amp;&amp; 
-          ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true;
-        if(this.options.value==optionTag.value) optionTag.selected = true;
-        optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
-        selectTag.appendChild(optionTag);
-      }.bind(this));
-      this.cached_selectTag = selectTag;
-    }
-
-    this.editField = this.cached_selectTag;
-    if(this.options.loadTextURL) this.loadExternalText();
-    this.form.appendChild(this.editField);
-    this.options.callback = function(form, value) {
-      return &quot;value=&quot; + encodeURIComponent(value);
-    }
-  }
-});
-
-// Delayed observer, like Form.Element.Observer, 
-// but waits for delay after last key input
-// Ideal for live-search fields
-
-Form.Element.DelayedObserver = Class.create();
-Form.Element.DelayedObserver.prototype = {
-  initialize: function(element, delay, callback) {
-    this.delay     = delay || 0.5;
-    this.element   = $(element);
-    this.callback  = callback;
-    this.timer     = null;
-    this.lastValue = $F(this.element); 
-    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
-  },
-  delayedListener: function(event) {
-    if(this.lastValue == $F(this.element)) return;
-    if(this.timer) clearTimeout(this.timer);
-    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
-    this.lastValue = $F(this.element);
-  },
-  onTimerEvent: function() {
-    this.timer = null;
-    this.callback(this.element, $F(this.element));
-  }
-};
+// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005, 2006 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+//           (c) 2005, 2006 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+//  Richard Livsey
+//  Rahul Bhargava
+//  Rob Wills
+// 
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+// 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.
+
+if(typeof Effect == 'undefined')
+  throw(&quot;controls.js requires including script.aculo.us' effects.js library&quot;);
+
+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, &quot;blur&quot;, this.onBlur.bindAsEventListener(this));
+    Event.observe(this.element, &quot;keypress&quot;, this.onKeyPress.bindAsEventListener(this));
+  },
+
+  show: function() {
+    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+    if(!this.iefix &amp;&amp; 
+      (navigator.appVersion.indexOf('MSIE')&gt;0) &amp;&amp;
+      (navigator.userAgent.indexOf('Opera')&lt;0) &amp;&amp;
+      (Element.getStyle(this.update, 'position')=='absolute')) {
+      new Insertion.After(this.update, 
+       '&lt;iframe id=&quot;' + this.update.id + '_iefix&quot; '+
+       'style=&quot;display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);&quot; ' +
+       'src=&quot;javascript:false;&quot; frameborder=&quot;0&quot; scrolling=&quot;no&quot;&gt;&lt;/iframe&gt;');
+      this.iefix = $(this.update.id+'_iefix');
+    }
+    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+  },
+  
+  fixIEOverlapping: function() {
+    Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
+    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')&gt;0) Event.stop(event);
+         return;
+       case Event.KEY_DOWN:
+         this.markNext();
+         this.render();
+         if(navigator.appVersion.indexOf('AppleWebKit')&gt;0) Event.stop(event);
+         return;
+      }
+     else 
+       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN || 
+         (navigator.appVersion.indexOf('AppleWebKit') &gt; 0 &amp;&amp; event.keyCode == 0)) return;
+
+    this.changed = true;
+    this.hasFocus = true;
+
+    if(this.observer) clearTimeout(this.observer);
+      this.observer = 
+        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+  },
+
+  activate: function() {
+    this.changed = false;
+    this.hasFocus = true;
+    this.getUpdatedChoices();
+  },
+
+  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 &gt; 0) {
+      for (var i = 0; i &lt; this.entryCount; i++)
+        this.index==i ? 
+          Element.addClassName(this.getEntry(i),&quot;selected&quot;) : 
+          Element.removeClassName(this.getEntry(i),&quot;selected&quot;);
+        
+      if(this.hasFocus) { 
+        this.show();
+        this.active = true;
+      }
+    } else {
+      this.active = false;
+      this.hide();
+    }
+  },
+  
+  markPrevious: function() {
+    if(this.index &gt; 0) this.index--
+      else this.index = this.entryCount-1;
+    this.getEntry(this.index).scrollIntoView(true);
+  },
+  
+  markNext: function() {
+    if(this.index &lt; this.entryCount-1) this.index++
+      else this.index = 0;
+    this.getEntry(this.index).scrollIntoView(false);
+  },
+  
+  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 = '';
+    if (this.options.select) {
+      var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
+      if(nodes.length&gt;0) value = Element.collectTextNodes(nodes[0], this.options.select);
+    } else
+      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 &amp;&amp; this.hasFocus) {
+      this.update.innerHTML = choices;
+      Element.cleanWhitespace(this.update);
+      Element.cleanWhitespace(this.update.down());
+
+      if(this.update.firstChild &amp;&amp; this.update.down().childNodes) {
+        this.entryCount = 
+          this.update.down().childNodes.length;
+        for (var i = 0; i &lt; this.entryCount; i++) {
+          var entry = this.getEntry(i);
+          entry.autocompleteIndex = i;
+          this.addObservers(entry);
+        }
+      } else { 
+        this.entryCount = 0;
+      }
+
+      this.stopIndicator();
+      this.index = 0;
+      
+      if(this.entryCount==1 &amp;&amp; this.options.autoSelect) {
+        this.selectEntry();
+        this.hide();
+      } else {
+        this.render();
+      }
+    }
+  },
+
+  addObservers: function(element) {
+    Event.observe(element, &quot;mouseover&quot;, this.onHover.bindAsEventListener(this));
+    Event.observe(element, &quot;click&quot;, this.onClick.bindAsEventListener(this));
+  },
+
+  onObserverEvent: function() {
+    this.changed = false;   
+    if(this.getToken().length&gt;=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&lt;this.options.tokens.length; i++) {
+      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
+      if (thisTokenPos &gt; 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 += '&amp;' + 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 &lt; instance.options.array.length &amp;&amp;  
+          ret.length &lt; 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 &amp;&amp; elem.length != entry.length) { 
+              ret.push(&quot;&lt;li&gt;&lt;strong&gt;&quot; + elem.substr(0, entry.length) + &quot;&lt;/strong&gt;&quot; + 
+                elem.substr(entry.length) + &quot;&lt;/li&gt;&quot;);
+              break;
+            } else if (entry.length &gt;= instance.options.partialChars &amp;&amp; 
+              instance.options.partialSearch &amp;&amp; foundPos != -1) {
+              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+                partial.push(&quot;&lt;li&gt;&quot; + elem.substr(0, foundPos) + &quot;&lt;strong&gt;&quot; +
+                  elem.substr(foundPos, entry.length) + &quot;&lt;/strong&gt;&quot; + elem.substr(
+                  foundPos + entry.length) + &quot;&lt;/li&gt;&quot;);
+                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 &quot;&lt;ul&gt;&quot; + ret.join('') + &quot;&lt;/ul&gt;&quot;;
+      }
+    }, 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 = &quot;#FFFF99&quot;;
+Ajax.InPlaceEditor.prototype = {
+  initialize: function(element, url, options) {
+    this.url = url;
+    this.element = $(element);
+
+    this.options = Object.extend({
+      paramName: &quot;value&quot;,
+      okButton: true,
+      okText: &quot;ok&quot;,
+      cancelLink: true,
+      cancelText: &quot;cancel&quot;,
+      savingText: &quot;Saving...&quot;,
+      clickToEditText: &quot;Click to edit&quot;,
+      okText: &quot;ok&quot;,
+      rows: 1,
+      onComplete: function(transport, element) {
+        new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
+      },
+      onFailure: function(transport) {
+        alert(&quot;Error communicating with the server: &quot; + 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: &quot;#FFFFFF&quot;,
+      externalControl: null,
+      submitOnBlur: false,
+      ajaxOptions: {},
+      evalScripts: false
+    }, options || {});
+
+    if(!this.options.formId &amp;&amp; this.element.id) {
+      this.options.formId = this.element.id + &quot;-inplaceeditor&quot;;
+      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 = &quot;transparent&quot;;
+    }
+    
+    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);
+    if (!this.options.loadTextURL) 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(&quot;form&quot;);
+    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(&quot;br&quot;);
+      this.form.appendChild(br);
+    }
+
+    if (this.options.okButton) {
+      okButton = document.createElement(&quot;input&quot;);
+      okButton.type = &quot;submit&quot;;
+      okButton.value = this.options.okText;
+      okButton.className = 'editor_ok_button';
+      this.form.appendChild(okButton);
+    }
+
+    if (this.options.cancelLink) {
+      cancelLink = document.createElement(&quot;a&quot;);
+      cancelLink.href = &quot;#&quot;;
+      cancelLink.appendChild(document.createTextNode(this.options.cancelText));
+      cancelLink.onclick = this.onclickCancel.bind(this);
+      cancelLink.className = 'editor_cancel';      
+      this.form.appendChild(cancelLink);
+    }
+  },
+  hasHTMLLineBreaks: function(string) {
+    if (!this.options.handleLineBreaks) return false;
+    return string.match(/&lt;br/i) || string.match(/&lt;p&gt;/i);
+  },
+  convertHTMLLineBreaks: function(string) {
+    return string.replace(/&lt;br&gt;/gi, &quot;\n&quot;).replace(/&lt;br\/&gt;/gi, &quot;\n&quot;).replace(/&lt;\/p&gt;/gi, &quot;\n&quot;).replace(/&lt;p&gt;/gi, &quot;&quot;);
+  },
+  createEditField: function() {
+    var text;
+    if(this.options.loadTextURL) {
+      text = this.options.loadingText;
+    } else {
+      text = this.getText();
+    }
+
+    var obj = this;
+    
+    if (this.options.rows == 1 &amp;&amp; !this.hasHTMLLineBreaks(text)) {
+      this.options.textarea = false;
+      var textField = document.createElement(&quot;input&quot;);
+      textField.obj = this;
+      textField.type = &quot;text&quot;;
+      textField.name = this.options.paramName;
+      textField.value = text;
+      textField.style.backgroundColor = this.options.highlightcolor;
+      textField.className = 'editor_field';
+      var size = this.options.size || this.options.cols || 0;
+      if (size != 0) textField.size = size;
+      if (this.options.submitOnBlur)
+        textField.onblur = this.onSubmit.bind(this);
+      this.editField = textField;
+    } else {
+      this.options.textarea = true;
+      var textArea = document.createElement(&quot;textarea&quot;);
+      textArea.obj = this;
+      textArea.name = this.options.paramName;
+      textArea.value = this.convertHTMLLineBreaks(text);
+      textArea.rows = this.options.rows;
+      textArea.cols = this.options.cols || 40;
+      textArea.className = 'editor_field';      
+      if (this.options.submitOnBlur)
+        textArea.onblur = this.onSubmit.bind(this);
+      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();
+    Field.scrollFreeActivate(this.editField);
+  },
+  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();
+    
+    if (this.options.evalScripts) {
+      new Ajax.Request(
+        this.url, Object.extend({
+          parameters: this.options.callback(form, value),
+          onComplete: this.onComplete.bind(this),
+          onFailure: this.onFailure.bind(this),
+          asynchronous:true, 
+          evalScripts:true
+        }, this.options.ajaxOptions));
+    } else  {
+      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 &gt; 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);
+    }
+  }
+};
+
+Ajax.InPlaceCollectionEditor = Class.create();
+Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
+Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
+  createEditField: function() {
+    if (!this.cached_selectTag) {
+      var selectTag = document.createElement(&quot;select&quot;);
+      var collection = this.options.collection || [];
+      var optionTag;
+      collection.each(function(e,i) {
+        optionTag = document.createElement(&quot;option&quot;);
+        optionTag.value = (e instanceof Array) ? e[0] : e;
+        if((typeof this.options.value == 'undefined') &amp;&amp; 
+          ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true;
+        if(this.options.value==optionTag.value) optionTag.selected = true;
+        optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
+        selectTag.appendChild(optionTag);
+      }.bind(this));
+      this.cached_selectTag = selectTag;
+    }
+
+    this.editField = this.cached_selectTag;
+    if(this.options.loadTextURL) this.loadExternalText();
+    this.form.appendChild(this.editField);
+    this.options.callback = function(form, value) {
+      return &quot;value=&quot; + encodeURIComponent(value);
+    }
+  }
+});
+
+// Delayed observer, like Form.Element.Observer, 
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create();
+Form.Element.DelayedObserver.prototype = {
+  initialize: function(element, delay, callback) {
+    this.delay     = delay || 0.5;
+    this.element   = $(element);
+    this.callback  = callback;
+    this.timer     = null;
+    this.lastValue = $F(this.element); 
+    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
+  },
+  delayedListener: function(event) {
+    if(this.lastValue == $F(this.element)) return;
+    if(this.timer) clearTimeout(this.timer);
+    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
+    this.lastValue = $F(this.element);
+  },
+  onTimerEvent: function() {
+    this.timer = null;
+    this.callback(this.element, $F(this.element));
+  }
+};</diff>
      <filename>public/javascripts/controls.js</filename>
    </modified>
    <modified>
      <diff>@@ -1,942 +1,942 @@
-// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
-//           (c) 2005, 2006 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
-// 
-// script.aculo.us is freely distributable under the terms of an MIT-style license.
-// For details, see the script.aculo.us web site: http://script.aculo.us/
-
-if(typeof Effect == 'undefined')
-  throw(&quot;dragdrop.js requires including script.aculo.us' effects.js library&quot;);
-
-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,
-      tree:       false
-    }, arguments[1] || {});
-
-    // cache containers
-    if(options.containment) {
-      options._containers = [];
-      var containment = options.containment;
-      if((typeof containment == 'object') &amp;&amp; 
-        (containment.constructor == Array)) {
-        containment.each( function(c) { options._containers.push($(c)) });
-      } else {
-        options._containers.push($(containment));
-      }
-    }
-    
-    if(options.accept) options.accept = [options.accept].flatten();
-
-    Element.makePositioned(element); // fix IE
-    options.element = element;
-
-    this.drops.push(options);
-  },
-  
-  findDeepestChild: function(drops) {
-    deepest = drops[0];
-      
-    for (i = 1; i &lt; drops.length; ++i)
-      if (Element.isParent(drops[i].element, deepest.element))
-        deepest = drops[i];
-    
-    return deepest;
-  },
-
-  isContained: function(element, drop) {
-    var containmentNode;
-    if(drop.tree) {
-      containmentNode = element.treeNode; 
-    } else {
-      containmentNode = element.parentNode;
-    }
-    return drop._containers.detect(function(c) { return containmentNode == c });
-  },
-  
-  isAffected: function(point, element, drop) {
-    return (
-      (drop.element!=element) &amp;&amp;
-      ((!drop._containers) ||
-        this.isContained(element, drop)) &amp;&amp;
-      ((!drop.accept) ||
-        (Element.classNames(element).detect( 
-          function(v) { return drop.accept.include(v) } ) )) &amp;&amp;
-      Position.within(drop.element, point[0], point[1]) );
-  },
-
-  deactivate: function(drop) {
-    if(drop.hoverclass)
-      Element.removeClassName(drop.element, drop.hoverclass);
-    this.last_active = null;
-  },
-
-  activate: function(drop) {
-    if(drop.hoverclass)
-      Element.addClassName(drop.element, drop.hoverclass);
-    this.last_active = drop;
-  },
-
-  show: function(point, element) {
-    if(!this.drops.length) return;
-    var affected = [];
-    
-    if(this.last_active) this.deactivate(this.last_active);
-    this.drops.each( function(drop) {
-      if(Droppables.isAffected(point, element, drop))
-        affected.push(drop);
-    });
-        
-    if(affected.length&gt;0) {
-      drop = Droppables.findDeepestChild(affected);
-      Position.within(drop.element, point[0], point[1]);
-      if(drop.onHover)
-        drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
-      
-      Droppables.activate(drop);
-    }
-  },
-
-  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 = {
-  drags: [],
-  observers: [],
-  
-  register: function(draggable) {
-    if(this.drags.length == 0) {
-      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
-      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
-      this.eventKeypress  = this.keyPress.bindAsEventListener(this);
-      
-      Event.observe(document, &quot;mouseup&quot;, this.eventMouseUp);
-      Event.observe(document, &quot;mousemove&quot;, this.eventMouseMove);
-      Event.observe(document, &quot;keypress&quot;, this.eventKeypress);
-    }
-    this.drags.push(draggable);
-  },
-  
-  unregister: function(draggable) {
-    this.drags = this.drags.reject(function(d) { return d==draggable });
-    if(this.drags.length == 0) {
-      Event.stopObserving(document, &quot;mouseup&quot;, this.eventMouseUp);
-      Event.stopObserving(document, &quot;mousemove&quot;, this.eventMouseMove);
-      Event.stopObserving(document, &quot;keypress&quot;, this.eventKeypress);
-    }
-  },
-  
-  activate: function(draggable) {
-    if(draggable.options.delay) { 
-      this._timeout = setTimeout(function() { 
-        Draggables._timeout = null; 
-        window.focus(); 
-        Draggables.activeDraggable = draggable; 
-      }.bind(this), draggable.options.delay); 
-    } else {
-      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
-      this.activeDraggable = draggable;
-    }
-  },
-  
-  deactivate: function() {
-    this.activeDraggable = null;
-  },
-  
-  updateDrag: function(event) {
-    if(!this.activeDraggable) return;
-    var pointer = [Event.pointerX(event), Event.pointerY(event)];
-    // Mozilla-based browsers fire successive mousemove events with
-    // the same coordinates, prevent needless redrawing (moz bug?)
-    if(this._lastPointer &amp;&amp; (this._lastPointer.inspect() == pointer.inspect())) return;
-    this._lastPointer = pointer;
-    
-    this.activeDraggable.updateDrag(event, pointer);
-  },
-  
-  endDrag: function(event) {
-    if(this._timeout) { 
-      clearTimeout(this._timeout); 
-      this._timeout = null; 
-    }
-    if(!this.activeDraggable) return;
-    this._lastPointer = null;
-    this.activeDraggable.endDrag(event);
-    this.activeDraggable = null;
-  },
-  
-  keyPress: function(event) {
-    if(this.activeDraggable)
-      this.activeDraggable.keyPress(event);
-  },
-  
-  addObserver: function(observer) {
-    this.observers.push(observer);
-    this._cacheObserverCallbacks();
-  },
-  
-  removeObserver: function(element) {  // element instead of observer fixes mem leaks
-    this.observers = this.observers.reject( function(o) { return o.element==element });
-    this._cacheObserverCallbacks();
-  },
-  
-  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
-    if(this[eventName+'Count'] &gt; 0)
-      this.observers.each( function(o) {
-        if(o[eventName]) o[eventName](eventName, draggable, event);
-      });
-    if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
-  },
-  
-  _cacheObserverCallbacks: function() {
-    ['onStart','onEnd','onDrag'].each( function(eventName) {
-      Draggables[eventName+'Count'] = Draggables.observers.select(
-        function(o) { return o[eventName]; }
-      ).length;
-    });
-  }
-}
-
-/*--------------------------------------------------------------------------*/
-
-var Draggable = Class.create();
-Draggable._dragging    = {};
-
-Draggable.prototype = {
-  initialize: function(element) {
-    var defaults = {
-      handle: false,
-      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.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
-          queue: {scope:'_draggable', position:'end'}
-        });
-      },
-      endeffect: function(element) {
-        var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
-        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 
-          queue: {scope:'_draggable', position:'end'},
-          afterFinish: function(){ 
-            Draggable._dragging[element] = false 
-          }
-        }); 
-      },
-      zindex: 1000,
-      revert: false,
-      scroll: false,
-      scrollSensitivity: 20,
-      scrollSpeed: 15,
-      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
-      delay: 0
-    };
-    
-    if(!arguments[1] || typeof arguments[1].endeffect == 'undefined')
-      Object.extend(defaults, {
-        starteffect: function(element) {
-          element._opacity = Element.getOpacity(element);
-          Draggable._dragging[element] = true;
-          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 
-        }
-      });
-    
-    var options = Object.extend(defaults, arguments[1] || {});
-
-    this.element = $(element);
-    
-    if(options.handle &amp;&amp; (typeof options.handle == 'string'))
-      this.handle = this.element.down('.'+options.handle, 0);
-    
-    if(!this.handle) this.handle = $(options.handle);
-    if(!this.handle) this.handle = this.element;
-    
-    if(options.scroll &amp;&amp; !options.scroll.scrollTo &amp;&amp; !options.scroll.outerHTML) {
-      options.scroll = $(options.scroll);
-      this._isScrollChild = Element.childOf(this.element, options.scroll);
-    }
-
-    Element.makePositioned(this.element); // fix IE    
-
-    this.delta    = this.currentDelta();
-    this.options  = options;
-    this.dragging = false;   
-
-    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
-    Event.observe(this.handle, &quot;mousedown&quot;, this.eventMouseDown);
-    
-    Draggables.register(this);
-  },
-  
-  destroy: function() {
-    Event.stopObserving(this.handle, &quot;mousedown&quot;, this.eventMouseDown);
-    Draggables.unregister(this);
-  },
-  
-  currentDelta: function() {
-    return([
-      parseInt(Element.getStyle(this.element,'left') || '0'),
-      parseInt(Element.getStyle(this.element,'top') || '0')]);
-  },
-  
-  initDrag: function(event) {
-    if(typeof Draggable._dragging[this.element] != 'undefined' &amp;&amp;
-      Draggable._dragging[this.element]) return;
-    if(Event.isLeftClick(event)) {    
-      // abort on form elements, fixes a Firefox issue
-      var src = Event.element(event);
-      if(src.tagName &amp;&amp; (
-        src.tagName=='INPUT' ||
-        src.tagName=='SELECT' ||
-        src.tagName=='OPTION' ||
-        src.tagName=='BUTTON' ||
-        src.tagName=='TEXTAREA')) return;
-        
-      var pointer = [Event.pointerX(event), Event.pointerY(event)];
-      var pos     = Position.cumulativeOffset(this.element);
-      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
-      
-      Draggables.activate(this);
-      Event.stop(event);
-    }
-  },
-  
-  startDrag: function(event) {
-    this.dragging = true;
-    
-    if(this.options.zindex) {
-      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
-      this.element.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);
-    }
-    
-    if(this.options.scroll) {
-      if (this.options.scroll == window) {
-        var where = this._getWindowScroll(this.options.scroll);
-        this.originalScrollLeft = where.left;
-        this.originalScrollTop = where.top;
-      } else {
-        this.originalScrollLeft = this.options.scroll.scrollLeft;
-        this.originalScrollTop = this.options.scroll.scrollTop;
-      }
-    }
-    
-    Draggables.notify('onStart', this, event);
-        
-    if(this.options.starteffect) this.options.starteffect(this.element);
-  },
-  
-  updateDrag: function(event, pointer) {
-    if(!this.dragging) this.startDrag(event);
-    Position.prepare();
-    Droppables.show(pointer, this.element);
-    Draggables.notify('onDrag', this, event);
-    
-    this.draw(pointer);
-    if(this.options.change) this.options.change(this);
-    
-    if(this.options.scroll) {
-      this.stopScrolling();
-      
-      var p;
-      if (this.options.scroll == window) {
-        with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
-      } else {
-        p = Position.page(this.options.scroll);
-        p[0] += this.options.scroll.scrollLeft + Position.deltaX;
-        p[1] += this.options.scroll.scrollTop + Position.deltaY;
-        p.push(p[0]+this.options.scroll.offsetWidth);
-        p.push(p[1]+this.options.scroll.offsetHeight);
-      }
-      var speed = [0,0];
-      if(pointer[0] &lt; (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
-      if(pointer[1] &lt; (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
-      if(pointer[0] &gt; (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
-      if(pointer[1] &gt; (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
-      this.startScrolling(speed);
-    }
-    
-    // fix AppleWebKit rendering
-    if(navigator.appVersion.indexOf('AppleWebKit')&gt;0) window.scrollBy(0,0);
-    
-    Event.stop(event);
-  },
-  
-  finishDrag: function(event, success) {
-    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, event);
-
-    var revert = this.options.revert;
-    if(revert &amp;&amp; typeof revert == 'function') revert = revert(this.element);
-    
-    var d = this.currentDelta();
-    if(revert &amp;&amp; this.options.reverteffect) {
-      this.options.reverteffect(this.element, 
-        d[1]-this.delta[1], d[0]-this.delta[0]);
-    } else {
-      this.delta = d;
-    }
-
-    if(this.options.zindex)
-      this.element.style.zIndex = this.originalZ;
-
-    if(this.options.endeffect) 
-      this.options.endeffect(this.element);
-      
-    Draggables.deactivate(this);
-    Droppables.reset();
-  },
-  
-  keyPress: function(event) {
-    if(event.keyCode!=Event.KEY_ESC) return;
-    this.finishDrag(event, false);
-    Event.stop(event);
-  },
-  
-  endDrag: function(event) {
-    if(!this.dragging) return;
-    this.stopScrolling();
-    this.finishDrag(event, true);
-    Event.stop(event);
-  },
-  
-  draw: function(point) {
-    var pos = Position.cumulativeOffset(this.element);
-    if(this.options.ghosting) {
-      var r   = Position.realOffset(this.element);
-      pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
-    }
-    
-    var d = this.currentDelta();
-    pos[0] -= d[0]; pos[1] -= d[1];
-    
-    if(this.options.scroll &amp;&amp; (this.options.scroll != window &amp;&amp; this._isScrollChild)) {
-      pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
-      pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
-    }
-    
-    var p = [0,1].map(function(i){ 
-      return (point[i]-pos[i]-this.offset[i]) 
-    }.bind(this));
-    
-    if(this.options.snap) {
-      if(typeof this.options.snap == 'function') {
-        p = this.options.snap(p[0],p[1],this);
-      } else {
-      if(this.options.snap instanceof Array) {
-        p = p.map( function(v, i) {
-          return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
-      } else {
-        p = p.map( function(v) {
-          return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
-      }
-    }}
-    
-    var style = this.element.style;
-    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
-      style.left = p[0] + &quot;px&quot;;
-    if((!this.options.constraint) || (this.options.constraint=='vertical'))
-      style.top  = p[1] + &quot;px&quot;;
-    
-    if(style.visibility==&quot;hidden&quot;) style.visibility = &quot;&quot;; // fix gecko rendering
-  },
-  
-  stopScrolling: function() {
-    if(this.scrollInterval) {
-      clearInterval(this.scrollInterval);
-      this.scrollInterval = null;
-      Draggables._lastScrollPointer = null;
-    }
-  },
-  
-  startScrolling: function(speed) {
-    if(!(speed[0] || speed[1])) return;
-    this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
-    this.lastScrolled = new Date();
-    this.scrollInterval = setInterval(this.scroll.bind(this), 10);
-  },
-  
-  scroll: function() {
-    var current = new Date();
-    var delta = current - this.lastScrolled;
-    this.lastScrolled = current;
-    if(this.options.scroll == window) {
-      with (this._getWindowScroll(this.options.scroll)) {
-        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
-          var d = delta / 1000;
-          this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
-        }
-      }
-    } else {
-      this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
-      this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
-    }
-    
-    Position.prepare();
-    Droppables.show(Draggables._lastPointer, this.element);
-    Draggables.notify('onDrag', this);
-    if (this._isScrollChild) {
-      Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
-      Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
-      Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
-      if (Draggables._lastScrollPointer[0] &lt; 0)
-        Draggables._lastScrollPointer[0] = 0;
-      if (Draggables._lastScrollPointer[1] &lt; 0)
-        Draggables._lastScrollPointer[1] = 0;
-      this.draw(Draggables._lastScrollPointer);
-    }
-    
-    if(this.options.change) this.options.change(this);
-  },
-  
-  _getWindowScroll: function(w) {
-    var T, L, W, H;
-    with (w.document) {
-      if (w.document.documentElement &amp;&amp; documentElement.scrollTop) {
-        T = documentElement.scrollTop;
-        L = documentElement.scrollLeft;
-      } else if (w.document.body) {
-        T = body.scrollTop;
-        L = body.scrollLeft;
-      }
-      if (w.innerWidth) {
-        W = w.innerWidth;
-        H = w.innerHeight;
-      } else if (w.document.documentElement &amp;&amp; documentElement.clientWidth) {
-        W = documentElement.clientWidth;
-        H = documentElement.clientHeight;
-      } else {
-        W = body.offsetWidth;
-        H = body.offsetHeight
-      }
-    }
-    return { top: T, left: L, width: W, height: H };
-  }
-}
-
-/*--------------------------------------------------------------------------*/
-
-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 = {
-  SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
-  
-  sortables: {},
-  
-  _findRootElement: function(element) {
-    while (element.tagName != &quot;BODY&quot;) {  
-      if(element.id &amp;&amp; Sortable.sortables[element.id]) return element;
-      element = element.parentNode;
-    }
-  },
-
-  options: function(element) {
-    element = Sortable._findRootElement($(element));
-    if(!element) return;
-    return Sortable.sortables[element.id];
-  },
-  
-  destroy: function(element){
-    var s = Sortable.options(element);
-    
-    if(s) {
-      Draggables.removeObserver(s.element);
-      s.droppables.each(function(d){ Droppables.remove(d) });
-      s.draggables.invoke('destroy');
-      
-      delete Sortable.sortables[s.element.id];
-    }
-  },
-
-  create: function(element) {
-    element = $(element);
-    var options = Object.extend({ 
-      element:     element,
-      tag:         'li',       // assumes li children, override with tag: 'tagname'
-      dropOnEmpty: false,
-      tree:        false,
-      treeTag:     'ul',
-      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,
-      delay:       0,
-      hoverclass:  null,
-      ghosting:    false,
-      scroll:      false,
-      scrollSensitivity: 20,
-      scrollSpeed: 15,
-      format:      this.SERIALIZE_RULE,
-      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,
-      scroll:      options.scroll,
-      scrollSpeed: options.scrollSpeed,
-      scrollSensitivity: options.scrollSensitivity,
-      delay:       options.delay,
-      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,
-      tree:        options.tree,
-      hoverclass:  options.hoverclass,
-      onHover:     Sortable.onHover
-    }
-    
-    var options_for_tree = {
-      onHover:      Sortable.onEmptyHover,
-      overlap:      options.overlap,
-      containment:  options.containment,
-      hoverclass:   options.hoverclass
-    }
-
-    // fix for gecko engine
-    Element.cleanWhitespace(element); 
-
-    options.draggables = [];
-    options.droppables = [];
-
-    // drop on empty handling
-    if(options.dropOnEmpty || options.tree) {
-      Droppables.add(element, options_for_tree);
-      options.droppables.push(element);
-    }
-
-    (this.findElements(element, options) || []).each( function(e) {
-      // handles are per-draggable
-      var handle = options.handle ? 
-        $(e).down('.'+options.handle,0) : e;    
-      options.draggables.push(
-        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
-      Droppables.add(e, options_for_droppable);
-      if(options.tree) e.treeNode = element;
-      options.droppables.push(e);      
-    });
-    
-    if(options.tree) {
-      (Sortable.findTreeElements(element, options) || []).each( function(e) {
-        Droppables.add(e, options_for_tree);
-        e.treeNode = element;
-        options.droppables.push(e);
-      });
-    }
-
-    // keep reference
-    this.sortables[element.id] = options;
-
-    // for onupdate
-    Draggables.addObserver(new SortableObserver(element, options.onUpdate));
-
-  },
-
-  // return all suitable-for-sortable elements in a guaranteed order
-  findElements: function(element, options) {
-    return Element.findChildren(
-      element, options.only, options.tree ? true : false, options.tag);
-  },
-  
-  findTreeElements: function(element, options) {
-    return Element.findChildren(
-      element, options.only, options.tree ? true : false, options.treeTag);
-  },
-
-  onHover: function(element, dropon, overlap) {
-    if(Element.isParent(dropon, element)) return;
-
-    if(overlap &gt; .33 &amp;&amp; overlap &lt; .66 &amp;&amp; Sortable.options(dropon).tree) {
-      return;
-    } else if(overlap&gt;0.5) {
-      Sortable.mark(dropon, 'before');
-      if(dropon.previousSibling != element) {
-        var oldParentNode = element.parentNode;
-        element.style.visibility = &quot;hidden&quot;; // 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 = &quot;hidden&quot;; // 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, overlap) {
-    var oldParentNode = element.parentNode;
-    var droponOptions = Sortable.options(dropon);
-        
-    if(!Element.isParent(dropon, element)) {
-      var index;
-      
-      var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
-      var child = null;
-            
-      if(children) {
-        var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
-        
-        for (index = 0; index &lt; children.length; index += 1) {
-          if (offset - Element.offsetSize (children[index], droponOptions.overlap) &gt;= 0) {
-            offset -= Element.offsetSize (children[index], droponOptions.overlap);
-          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) &gt;= 0) {
-            child = index + 1 &lt; children.length ? children[index + 1] : null;
-            break;
-          } else {
-            child = children[index];
-            break;
-          }
-        }
-      }
-      
-      dropon.insertBefore(element, child);
-      
-      Sortable.options(oldParentNode).onChange(element);
-      droponOptions.onChange(element);
-    }
-  },
-
-  unmark: function() {
-    if(Sortable._marker) Sortable._marker.hide();
-  },
-
-  mark: function(dropon, position) {
-    // mark on ghosting only
-    var sortable = Sortable.options(dropon.parentNode);
-    if(sortable &amp;&amp; !sortable.ghosting) return; 
-
-    if(!Sortable._marker) {
-      Sortable._marker = 
-        ($('dropmarker') || Element.extend(document.createElement('DIV'))).
-          hide().addClassName('dropmarker').setStyle({position:'absolute'});
-      document.getElementsByTagName(&quot;body&quot;).item(0).appendChild(Sortable._marker);
-    }    
-    var offsets = Position.cumulativeOffset(dropon);
-    Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
-    
-    if(position=='after')
-      if(sortable.overlap == 'horizontal') 
-        Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
-      else
-        Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
-    
-    Sortable._marker.show();
-  },
-  
-  _tree: function(element, options, parent) {
-    var children = Sortable.findElements(element, options) || [];
-  
-    for (var i = 0; i &lt; children.length; ++i) {
-      var match = children[i].id.match(options.format);
-
-      if (!match) continue;
-      
-      var child = {
-        id: encodeURIComponent(match ? match[1] : null),
-        element: element,
-        parent: parent,
-        children: [],
-        position: parent.children.length,
-        container: $(children[i]).down(options.treeTag)
-      }
-      
-      /* Get the element containing the children and recurse over it */
-      if (child.container)
-        this._tree(child.container, options, child)
-      
-      parent.children.push (child);
-    }
-
-    return parent; 
-  },
-
-  tree: function(element) {
-    element = $(element);
-    var sortableOptions = this.options(element);
-    var options = Object.extend({
-      tag: sortableOptions.tag,
-      treeTag: sortableOptions.treeTag,
-      only: sortableOptions.only,
-      name: element.id,
-      format: sortableOptions.format
-    }, arguments[1] || {});
-    
-    var root = {
-      id: null,
-      parent: null,
-      children: [],
-      container: element,
-      position: 0
-    }
-    
-    return Sortable._tree(element, options, root);
-  },
-
-  /* Construct a [i] index for a particular node */
-  _constructIndex: function(node) {
-    var index = '';
-    do {
-      if (node.id) index = '[' + node.position + ']' + index;
-    } while ((node = node.parent) != null);
-    return index;
-  },
-
-  sequence: function(element) {
-    element = $(element);
-    var options = Object.extend(this.options(element), arguments[1] || {});
-    
-    return $(this.findElements(element, options) || []).map( function(item) {
-      return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
-    });
-  },
-
-  setSequence: function(element, new_sequence) {
-    element = $(element);
-    var options = Object.extend(this.options(element), arguments[2] || {});
-    
-    var nodeMap = {};
-    this.findElements(element, options).each( function(n) {
-        if (n.id.match(options.format))
-            nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
-        n.parentNode.removeChild(n);
-    });
-   
-    new_sequence.each(function(ident) {
-      var n = nodeMap[ident];
-      if (n) {
-        n[1].appendChild(n[0]);
-        delete nodeMap[ident];
-      }
-    });
-  },
-  
-  serialize: function(element) {
-    element = $(element);
-    var options = Object.extend(Sortable.options(element), arguments[1] || {});
-    var name = encodeURIComponent(
-      (arguments[1] &amp;&amp; arguments[1].name) ? arguments[1].name : element.id);
-    
-    if (options.tree) {
-      return Sortable.tree(element, arguments[1]).children.map( function (item) {
-        return [name + Sortable._constructIndex(item) + &quot;[id]=&quot; + 
-                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
-      }).flatten().join('&amp;');
-    } else {
-      return Sortable.sequence(element, arguments[1]).map( function(item) {
-        return name + &quot;[]=&quot; + encodeURIComponent(item);
-      }).join('&amp;');
-    }
-  }
-}
-
-// Returns true if child is contained within element
-Element.isParent = function(child, element) {
-  if (!child.parentNode || child == element) return false;
-  if (child.parentNode == element) return true;
-  return Element.isParent(child.parentNode, element);
-}
-
-Element.findChildren = function(element, only, recursive, tagName) {    
-  if(!element.hasChildNodes()) return null;
-  tagName = tagName.toUpperCase();
-  if(only) only = [only].flatten();
-  var elements = [];
-  $A(element.childNodes).each( function(e) {
-    if(e.tagName &amp;&amp; e.tagName.toUpperCase()==tagName &amp;&amp;
-      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
-        elements.push(e);
-    if(recursive) {
-      var grandchildren = Element.findChildren(e, only, recursive, tagName);
-      if(grandchildren) elements.push(grandchildren);
-    }
-  });
-
-  return (elements.length&gt;0 ? elements.flatten() : []);
-}
-
-Element.offsetSize = function (element, type) {
-  return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
-}
+// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005, 2006 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
+// 
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/
+
+if(typeof Effect == 'undefined')
+  throw(&quot;dragdrop.js requires including script.aculo.us' effects.js library&quot;);
+
+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,
+      tree:       false
+    }, arguments[1] || {});
+
+    // cache containers
+    if(options.containment) {
+      options._containers = [];
+      var containment = options.containment;
+      if((typeof containment == 'object') &amp;&amp; 
+        (containment.constructor == Array)) {
+        containment.each( function(c) { options._containers.push($(c)) });
+      } else {
+        options._containers.push($(containment));
+      }
+    }
+    
+    if(options.accept) options.accept = [options.accept].flatten();
+
+    Element.makePositioned(element); // fix IE
+    options.element = element;
+
+    this.drops.push(options);
+  },
+  
+  findDeepestChild: function(drops) {
+    deepest = drops[0];
+      
+    for (i = 1; i &lt; drops.length; ++i)
+      if (Element.isParent(drops[i].element, deepest.element))
+        deepest = drops[i];
+    
+    return deepest;
+  },
+
+  isContained: function(element, drop) {
+    var containmentNode;
+    if(drop.tree) {
+      containmentNode = element.treeNode; 
+    } else {
+      containmentNode = element.parentNode;
+    }
+    return drop._containers.detect(function(c) { return containmentNode == c });
+  },
+  
+  isAffected: function(point, element, drop) {
+    return (
+      (drop.element!=element) &amp;&amp;
+      ((!drop._containers) ||
+        this.isContained(element, drop)) &amp;&amp;
+      ((!drop.accept) ||
+        (Element.classNames(element).detect( 
+          function(v) { return drop.accept.include(v) } ) )) &amp;&amp;
+      Position.within(drop.element, point[0], point[1]) );
+  },
+
+  deactivate: function(drop) {
+    if(drop.hoverclass)
+      Element.removeClassName(drop.element, drop.hoverclass);
+    this.last_active = null;
+  },
+
+  activate: function(drop) {
+    if(drop.hoverclass)
+      Element.addClassName(drop.element, drop.hoverclass);
+    this.last_active = drop;
+  },
+
+  show: function(point, element) {
+    if(!this.drops.length) return;
+    var affected = [];
+    
+    if(this.last_active) this.deactivate(this.last_active);
+    this.drops.each( function(drop) {
+      if(Droppables.isAffected(point, element, drop))
+        affected.push(drop);
+    });
+        
+    if(affected.length&gt;0) {
+      drop = Droppables.findDeepestChild(affected);
+      Position.within(drop.element, point[0], point[1]);
+      if(drop.onHover)
+        drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+      
+      Droppables.activate(drop);
+    }
+  },
+
+  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 = {
+  drags: [],
+  observers: [],
+  
+  register: function(draggable) {
+    if(this.drags.length == 0) {
+      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
+      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
+      this.eventKeypress  = this.keyPress.bindAsEventListener(this);
+      
+      Event.observe(document, &quot;mouseup&quot;, this.eventMouseUp);
+      Event.observe(document, &quot;mousemove&quot;, this.eventMouseMove);
+      Event.observe(document, &quot;keypress&quot;, this.eventKeypress);
+    }
+    this.drags.push(draggable);
+  },
+  
+  unregister: function(draggable) {
+    this.drags = this.drags.reject(function(d) { return d==draggable });
+    if(this.drags.length == 0) {
+      Event.stopObserving(document, &quot;mouseup&quot;, this.eventMouseUp);
+      Event.stopObserving(document, &quot;mousemove&quot;, this.eventMouseMove);
+      Event.stopObserving(document, &quot;keypress&quot;, this.eventKeypress);
+    }
+  },
+  
+  activate: function(draggable) {
+    if(draggable.options.delay) { 
+      this._timeout = setTimeout(function() { 
+        Draggables._timeout = null; 
+        window.focus(); 
+        Draggables.activeDraggable = draggable; 
+      }.bind(this), draggable.options.delay); 
+    } else {
+      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
+      this.activeDraggable = draggable;
+    }
+  },
+  
+  deactivate: function() {
+    this.activeDraggable = null;
+  },
+  
+  updateDrag: function(event) {
+    if(!this.activeDraggable) return;
+    var pointer = [Event.pointerX(event), Event.pointerY(event)];
+    // Mozilla-based browsers fire successive mousemove events with
+    // the same coordinates, prevent needless redrawing (moz bug?)
+    if(this._lastPointer &amp;&amp; (this._lastPointer.inspect() == pointer.inspect())) return;
+    this._lastPointer = pointer;
+    
+    this.activeDraggable.updateDrag(event, pointer);
+  },
+  
+  endDrag: function(event) {
+    if(this._timeout) { 
+      clearTimeout(this._timeout); 
+      this._timeout = null; 
+    }
+    if(!this.activeDraggable) return;
+    this._lastPointer = null;
+    this.activeDraggable.endDrag(event);
+    this.activeDraggable = null;
+  },
+  
+  keyPress: function(event) {
+    if(this.activeDraggable)
+      this.activeDraggable.keyPress(event);
+  },
+  
+  addObserver: function(observer) {
+    this.observers.push(observer);
+    this._cacheObserverCallbacks();
+  },
+  
+  removeObserver: function(element) {  // element instead of observer fixes mem leaks
+    this.observers = this.observers.reject( function(o) { return o.element==element });
+    this._cacheObserverCallbacks();
+  },
+  
+  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
+    if(this[eventName+'Count'] &gt; 0)
+      this.observers.each( function(o) {
+        if(o[eventName]) o[eventName](eventName, draggable, event);
+      });
+    if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
+  },
+  
+  _cacheObserverCallbacks: function() {
+    ['onStart','onEnd','onDrag'].each( function(eventName) {
+      Draggables[eventName+'Count'] = Draggables.observers.select(
+        function(o) { return o[eventName]; }
+      ).length;
+    });
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Draggable = Class.create();
+Draggable._dragging    = {};
+
+Draggable.prototype = {
+  initialize: function(element) {
+    var defaults = {
+      handle: false,
+      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.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
+          queue: {scope:'_draggable', position:'end'}
+        });
+      },
+      endeffect: function(element) {
+        var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
+        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity, 
+          queue: {scope:'_draggable', position:'end'},
+          afterFinish: function(){ 
+            Draggable._dragging[element] = false 
+          }
+        }); 
+      },
+      zindex: 1000,
+      revert: false,
+      scroll: false,
+      scrollSensitivity: 20,
+      scrollSpeed: 15,
+      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
+      delay: 0
+    };
+    
+    if(!arguments[1] || typeof arguments[1].endeffect == 'undefined')
+      Object.extend(defaults, {
+        starteffect: function(element) {
+          element._opacity = Element.getOpacity(element);
+          Draggable._dragging[element] = true;
+          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7}); 
+        }
+      });
+    
+    var options = Object.extend(defaults, arguments[1] || {});
+
+    this.element = $(element);
+    
+    if(options.handle &amp;&amp; (typeof options.handle == 'string'))
+      this.handle = this.element.down('.'+options.handle, 0);
+    
+    if(!this.handle) this.handle = $(options.handle);
+    if(!this.handle) this.handle = this.element;
+    
+    if(options.scroll &amp;&amp; !options.scroll.scrollTo &amp;&amp; !options.scroll.outerHTML) {
+      options.scroll = $(options.scroll);
+      this._isScrollChild = Element.childOf(this.element, options.scroll);
+    }
+
+    Element.makePositioned(this.element); // fix IE    
+
+    this.delta    = this.currentDelta();
+    this.options  = options;
+    this.dragging = false;   
+
+    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
+    Event.observe(this.handle, &quot;mousedown&quot;, this.eventMouseDown);
+    
+    Draggables.register(this);
+  },
+  
+  destroy: function() {
+    Event.stopObserving(this.handle, &quot;mousedown&quot;, this.eventMouseDown);
+    Draggables.unregister(this);
+  },
+  
+  currentDelta: function() {
+    return([
+      parseInt(Element.getStyle(this.element,'left') || '0'),
+      parseInt(Element.getStyle(this.element,'top') || '0')]);
+  },
+  
+  initDrag: function(event) {
+    if(typeof Draggable._dragging[this.element] != 'undefined' &amp;&amp;
+      Draggable._dragging[this.element]) return;
+    if(Event.isLeftClick(event)) {    
+      // abort on form elements, fixes a Firefox issue
+      var src = Event.element(event);
+      if(src.tagName &amp;&amp; (
+        src.tagName=='INPUT' ||
+        src.tagName=='SELECT' ||
+        src.tagName=='OPTION' ||
+        src.tagName=='BUTTON' ||
+        src.tagName=='TEXTAREA')) return;
+        
+      var pointer = [Event.pointerX(event), Event.pointerY(event)];
+      var pos     = Position.cumulativeOffset(this.element);
+      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
+      
+      Draggables.activate(this);
+      Event.stop(event);
+    }
+  },
+  
+  startDrag: function(event) {
+    this.dragging = true;
+    
+    if(this.options.zindex) {
+      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
+      this.element.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);
+    }
+    
+    if(this.options.scroll) {
+      if (this.options.scroll == window) {
+        var where = this._getWindowScroll(this.options.scroll);
+        this.originalScrollLeft = where.left;
+        this.originalScrollTop = where.top;
+      } else {
+        this.originalScrollLeft = this.options.scroll.scrollLeft;
+        this.originalScrollTop = this.options.scroll.scrollTop;
+      }
+    }
+    
+    Draggables.notify('onStart', this, event);
+        
+    if(this.options.starteffect) this.options.starteffect(this.element);
+  },
+  
+  updateDrag: function(event, pointer) {
+    if(!this.dragging) this.startDrag(event);
+    Position.prepare();
+    Droppables.show(pointer, this.element);
+    Draggables.notify('onDrag', this, event);
+    
+    this.draw(pointer);
+    if(this.options.change) this.options.change(this);
+    
+    if(this.options.scroll) {
+      this.stopScrolling();
+      
+      var p;
+      if (this.options.scroll == window) {
+        with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
+      } else {
+        p = Position.page(this.options.scroll);
+        p[0] += this.options.scroll.scrollLeft + Position.deltaX;
+        p[1] += this.options.scroll.scrollTop + Position.deltaY;
+        p.push(p[0]+this.options.scroll.offsetWidth);
+        p.push(p[1]+this.options.scroll.offsetHeight);
+      }
+      var speed = [0,0];
+      if(pointer[0] &lt; (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
+      if(pointer[1] &lt; (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
+      if(pointer[0] &gt; (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
+      if(pointer[1] &gt; (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
+      this.startScrolling(speed);
+    }
+    
+    // fix AppleWebKit rendering
+    if(navigator.appVersion.indexOf('AppleWebKit')&gt;0) window.scrollBy(0,0);
+    
+    Event.stop(event);
+  },
+  
+  finishDrag: function(event, success) {
+    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, event);
+
+    var revert = this.options.revert;
+    if(revert &amp;&amp; typeof revert == 'function') revert = revert(this.element);
+    
+    var d = this.currentDelta();
+    if(revert &amp;&amp; this.options.reverteffect) {
+      this.options.reverteffect(this.element, 
+        d[1]-this.delta[1], d[0]-this.delta[0]);
+    } else {
+      this.delta = d;
+    }
+
+    if(this.options.zindex)
+      this.element.style.zIndex = this.originalZ;
+
+    if(this.options.endeffect) 
+      this.options.endeffect(this.element);
+      
+    Draggables.deactivate(this);
+    Droppables.reset();
+  },
+  
+  keyPress: function(event) {
+    if(event.keyCode!=Event.KEY_ESC) return;
+    this.finishDrag(event, false);
+    Event.stop(event);
+  },
+  
+  endDrag: function(event) {
+    if(!this.dragging) return;
+    this.stopScrolling();
+    this.finishDrag(event, true);
+    Event.stop(event);
+  },
+  
+  draw: function(point) {
+    var pos = Position.cumulativeOffset(this.element);
+    if(this.options.ghosting) {
+      var r   = Position.realOffset(this.element);
+      pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
+    }
+    
+    var d = this.currentDelta();
+    pos[0] -= d[0]; pos[1] -= d[1];
+    
+    if(this.options.scroll &amp;&amp; (this.options.scroll != window &amp;&amp; this._isScrollChild)) {
+      pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
+      pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
+    }
+    
+    var p = [0,1].map(function(i){ 
+      return (point[i]-pos[i]-this.offset[i]) 
+    }.bind(this));
+    
+    if(this.options.snap) {
+      if(typeof this.options.snap == 'function') {
+        p = this.options.snap(p[0],p[1],this);
+      } else {
+      if(this.options.snap instanceof Array) {
+        p = p.map( function(v, i) {
+          return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
+      } else {
+        p = p.map( function(v) {
+          return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
+      }
+    }}
+    
+    var style = this.element.style;
+    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+      style.left = p[0] + &quot;px&quot;;
+    if((!this.options.constraint) || (this.options.constraint=='vertical'))
+      style.top  = p[1] + &quot;px&quot;;
+    
+    if(style.visibility==&quot;hidden&quot;) style.visibility = &quot;&quot;; // fix gecko rendering
+  },
+  
+  stopScrolling: function() {
+    if(this.scrollInterval) {
+      clearInterval(this.scrollInterval);
+      this.scrollInterval = null;
+      Draggables._lastScrollPointer = null;
+    }
+  },
+  
+  startScrolling: function(speed) {
+    if(!(speed[0] || speed[1])) return;
+    this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
+    this.lastScrolled = new Date();
+    this.scrollInterval = setInterval(this.scroll.bind(this), 10);
+  },
+  
+  scroll: function() {
+    var current = new Date();
+    var delta = current - this.lastScrolled;
+    this.lastScrolled = current;
+    if(this.options.scroll == window) {
+      with (this._getWindowScroll(this.options.scroll)) {
+        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
+          var d = delta / 1000;
+          this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
+        }
+      }
+    } else {
+      this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
+      this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
+    }
+    
+    Position.prepare();
+    Droppables.show(Draggables._lastPointer, this.element);
+    Draggables.notify('onDrag', this);
+    if (this._isScrollChild) {
+      Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
+      Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
+      Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
+      if (Draggables._lastScrollPointer[0] &lt; 0)
+        Draggables._lastScrollPointer[0] = 0;
+      if (Draggables._lastScrollPointer[1] &lt; 0)
+        Draggables._lastScrollPointer[1] = 0;
+      this.draw(Draggables._lastScrollPointer);
+    }
+    
+    if(this.options.change) this.options.change(this);
+  },
+  
+  _getWindowScroll: function(w) {
+    var T, L, W, H;
+    with (w.document) {
+      if (w.document.documentElement &amp;&amp; documentElement.scrollTop) {
+        T = documentElement.scrollTop;
+        L = documentElement.scrollLeft;
+      } else if (w.document.body) {
+        T = body.scrollTop;
+        L = body.scrollLeft;
+      }
+      if (w.innerWidth) {
+        W = w.innerWidth;
+        H = w.innerHeight;
+      } else if (w.document.documentElement &amp;&amp; documentElement.clientWidth) {
+        W = documentElement.clientWidth;
+        H = documentElement.clientHeight;
+      } else {
+        W = body.offsetWidth;
+        H = body.offsetHeight
+      }
+    }
+    return { top: T, left: L, width: W, height: H };
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+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 = {
+  SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
+  
+  sortables: {},
+  
+  _findRootElement: function(element) {
+    while (element.tagName != &quot;BODY&quot;) {  
+      if(element.id &amp;&amp; Sortable.sortables[element.id]) return element;
+      element = element.parentNode;
+    }
+  },
+
+  options: function(element) {
+    element = Sortable._findRootElement($(element));
+    if(!element) return;
+    return Sortable.sortables[element.id];
+  },
+  
+  destroy: function(element){
+    var s = Sortable.options(element);
+    
+    if(s) {
+      Draggables.removeObserver(s.element);
+      s.droppables.each(function(d){ Droppables.remove(d) });
+      s.draggables.invoke('destroy');
+      
+      delete Sortable.sortables[s.element.id];
+    }
+  },
+
+  create: function(element) {
+    element = $(element);
+    var options = Object.extend({ 
+      element:     element,
+      tag:         'li',       // assumes li children, override with tag: 'tagname'
+      dropOnEmpty: false,
+      tree:        false,
+      treeTag:     'ul',
+      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,
+      delay:       0,
+      hoverclass:  null,
+      ghosting:    false,
+      scroll:      false,
+      scrollSensitivity: 20,
+      scrollSpeed: 15,
+      format:      this.SERIALIZE_RULE,
+      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,
+      scroll:      options.scroll,
+      scrollSpeed: options.scrollSpeed,
+      scrollSensitivity: options.scrollSensitivity,
+      delay:       options.delay,
+      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,
+      tree:        options.tree,
+      hoverclass:  options.hoverclass,
+      onHover:     Sortable.onHover
+    }
+    
+    var options_for_tree = {
+      onHover:      Sortable.onEmptyHover,
+      overlap:      options.overlap,
+      containment:  options.containment,
+      hoverclass:   options.hoverclass
+    }
+
+    // fix for gecko engine
+    Element.cleanWhitespace(element); 
+
+    options.draggables = [];
+    options.droppables = [];
+
+    // drop on empty handling
+    if(options.dropOnEmpty || options.tree) {
+      Droppables.add(element, options_for_tree);
+      options.droppables.push(element);
+    }
+
+    (this.findElements(element, options) || []).each( function(e) {
+      // handles are per-draggable
+      var handle = options.handle ? 
+        $(e).down('.'+options.handle,0) : e;    
+      options.draggables.push(
+        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
+      Droppables.add(e, options_for_droppable);
+      if(options.tree) e.treeNode = element;
+      options.droppables.push(e);      
+    });
+    
+    if(options.tree) {
+      (Sortable.findTreeElements(element, options) || []).each( function(e) {
+        Droppables.add(e, options_for_tree);
+        e.treeNode = element;
+        options.droppables.push(e);
+      });
+    }
+
+    // keep reference
+    this.sortables[element.id] = options;
+
+    // for onupdate
+    Draggables.addObserver(new SortableObserver(element, options.onUpdate));
+
+  },
+
+  // return all suitable-for-sortable elements in a guaranteed order
+  findElements: function(element, options) {
+    return Element.findChildren(
+      element, options.only, options.tree ? true : false, options.tag);
+  },
+  
+  findTreeElements: function(element, options) {
+    return Element.findChildren(
+      element, options.only, options.tree ? true : false, options.treeTag);
+  },
+
+  onHover: function(element, dropon, overlap) {
+    if(Element.isParent(dropon, element)) return;
+
+    if(overlap &gt; .33 &amp;&amp; overlap &lt; .66 &amp;&amp; Sortable.options(dropon).tree) {
+      return;
+    } else if(overlap&gt;0.5) {
+      Sortable.mark(dropon, 'before');
+      if(dropon.previousSibling != element) {
+        var oldParentNode = element.parentNode;
+        element.style.visibility = &quot;hidden&quot;; // 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 = &quot;hidden&quot;; // 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, overlap) {
+    var oldParentNode = element.parentNode;
+    var droponOptions = Sortable.options(dropon);
+        
+    if(!Element.isParent(dropon, element)) {
+      var index;
+      
+      var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
+      var child = null;
+            
+      if(children) {
+        var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
+        
+        for (index = 0; index &lt; children.length; index += 1) {
+          if (offset - Element.offsetSize (children[index], droponOptions.overlap) &gt;= 0) {
+            offset -= Element.offsetSize (children[index], droponOptions.overlap);
+          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) &gt;= 0) {
+            child = index + 1 &lt; children.length ? children[index + 1] : null;
+            break;
+          } else {
+            child = children[index];
+            break;
+          }
+        }
+      }
+      
+      dropon.insertBefore(element, child);
+      
+      Sortable.options(oldParentNode).onChange(element);
+      droponOptions.onChange(element);
+    }
+  },
+
+  unmark: function() {
+    if(Sortable._marker) Sortable._marker.hide();
+  },
+
+  mark: function(dropon, position) {
+    // mark on ghosting only
+    var sortable = Sortable.options(dropon.parentNode);
+    if(sortable &amp;&amp; !sortable.ghosting) return; 
+
+    if(!Sortable._marker) {
+      Sortable._marker = 
+        ($('dropmarker') || Element.extend(document.createElement('DIV'))).
+          hide().addClassName('dropmarker').setStyle({position:'absolute'});
+      document.getElementsByTagName(&quot;body&quot;).item(0).appendChild(Sortable._marker);
+    }    
+    var offsets = Position.cumulativeOffset(dropon);
+    Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
+    
+    if(position=='after')
+      if(sortable.overlap == 'horizontal') 
+        Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
+      else
+        Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
+    
+    Sortable._marker.show();
+  },
+  
+  _tree: function(element, options, parent) {
+    var children = Sortable.findElements(element, options) || [];
+  
+    for (var i = 0; i &lt; children.length; ++i) {
+      var match = children[i].id.match(options.format);
+
+      if (!match) continue;
+      
+      var child = {
+        id: encodeURIComponent(match ? match[1] : null),
+        element: element,
+        parent: parent,
+        children: [],
+        position: parent.children.length,
+        container: $(children[i]).down(options.treeTag)
+      }
+      
+      /* Get the element containing the children and recurse over it */
+      if (child.container)
+        this._tree(child.container, options, child)
+      
+      parent.children.push (child);
+    }
+
+    return parent; 
+  },
+
+  tree: function(element) {
+    element = $(element);
+    var sortableOptions = this.options(element);
+    var options = Object.extend({
+      tag: sortableOptions.tag,
+      treeTag: sortableOptions.treeTag,
+      only: sortableOptions.only,
+      name: element.id,
+      format: sortableOptions.format
+    }, arguments[1] || {});
+    
+    var root = {
+      id: null,
+      parent: null,
+      children: [],
+      container: element,
+      position: 0
+    }
+    
+    return Sortable._tree(element, options, root);
+  },
+
+  /* Construct a [i] index for a particular node */
+  _constructIndex: function(node) {
+    var index = '';
+    do {
+      if (node.id) index = '[' + node.position + ']' + index;
+    } while ((node = node.parent) != null);
+    return index;
+  },
+
+  sequence: function(element) {
+    element = $(element);
+    var options = Object.extend(this.options(element), arguments[1] || {});
+    
+    return $(this.findElements(element, options) || []).map( function(item) {
+      return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
+    });
+  },
+
+  setSequence: function(element, new_sequence) {
+    element = $(element);
+    var options = Object.extend(this.options(element), arguments[2] || {});
+    
+    var nodeMap = {};
+    this.findElements(element, options).each( function(n) {
+        if (n.id.match(options.format))
+            nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
+        n.parentNode.removeChild(n);
+    });
+   
+    new_sequence.each(function(ident) {
+      var n = nodeMap[ident];
+      if (n) {
+        n[1].appendChild(n[0]);
+        delete nodeMap[ident];
+      }
+    });
+  },
+  
+  serialize: function(element) {
+    element = $(element);
+    var options = Object.extend(Sortable.options(element), arguments[1] || {});
+    var name = encodeURIComponent(
+      (arguments[1] &amp;&amp; arguments[1].name) ? arguments[1].name : element.id);
+    
+    if (options.tree) {
+      return Sortable.tree(element, arguments[1]).children.map( function (item) {
+        return [name + Sortable._constructIndex(item) + &quot;[id]=&quot; + 
+                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
+      }).flatten().join('&amp;');
+    } else {
+      return Sortable.sequence(element, arguments[1]).map( function(item) {
+        return name + &quot;[]=&quot; + encodeURIComponent(item);
+      }).join('&amp;');
+    }
+  }
+}
+
+// Returns true if child is contained within element
+Element.isParent = function(child, element) {
+  if (!child.parentNode || child == element) return false;
+  if (child.parentNode == element) return true;
+  return Element.isParent(child.parentNode, element);
+}
+
+Element.findChildren = function(element, only, recursive, tagName) {    
+  if(!element.hasChildNodes()) return null;
+  tagName = tagName.toUpperCase();
+  if(only) only = [only].flatten();
+  var elements = [];
+  $A(element.childNodes).each( function(e) {
+    if(e.tagName &amp;&amp; e.tagName.toUpperCase()==tagName &amp;&amp;
+      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
+        elements.push(e);
+    if(recursive) {
+      var grandchildren = Element.findChildren(e, only, recursive, tagName);
+      if(grandchildren) elements.push(grandchildren);
+    }
+  });
+
+  return (elements.length&gt;0 ? elements.flatten() : []);
+}
+
+Element.offsetSize = function (element, type) {
+  return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
+}</diff>
      <filename>public/javascripts/dragdrop.js</filename>
    </modified>
    <modified>
      <diff>@@ -1,1088 +1,1088 @@
-// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
-// Contributors:
-//  Justin Palmer (http://encytemedia.com/)
-//  Mark Pilgrim (http://diveintomark.org/)
-//  Martin Bialasinki
-// 
-// script.aculo.us is freely distributable under the terms of an MIT-style license.
-// For details, see the script.aculo.us web site: http://script.aculo.us/ 
-
-// converts rgb() and #xxx to #xxxxxx format,  
-// returns self (or first argument) if not convertable  
-String.prototype.parseColor = function() {  
-  var 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&lt;3);  
-  } else {  
-    if(this.slice(0,1) == '#') {  
-      if(this.length==4) for(var i=1;i&lt;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.collectTextNodes = function(element) {  
-  return $A($(element).childNodes).collect( function(node) {
-    return (node.nodeType==3 ? node.nodeValue : 
-      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
-  }).flatten().join('');
-}
-
-Element.collectTextNodesIgnoreClass = function(element, className) {  
-  return $A($(element).childNodes).collect( function(node) {
-    return (node.nodeType==3 ? node.nodeValue : 
-      ((node.hasChildNodes() &amp;&amp; !Element.hasClassName(node,className)) ? 
-        Element.collectTextNodesIgnoreClass(node, className) : ''));
-  }).flatten().join('');
-}
-
-Element.setContentZoom = function(element, percent) {
-  element = $(element);  
-  element.setStyle({fontSize: (percent/100) + 'em'});   
-  if(navigator.appVersion.indexOf('AppleWebKit')&gt;0) window.scrollBy(0,0);
-  return element;
-}
-
-Element.getOpacity = function(element){
-  element = $(element);
-  var opacity;
-  if (opacity = element.getStyle('opacity'))  
-    return parseFloat(opacity);  
-  if (opacity = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))  
-    if(opacity[1]) return parseFloat(opacity[1]) / 100;  
-  return 1.0;  
-}
-
-Element.setOpacity = function(element, value){  
-  element= $(element);  
-  if (value == 1){
-    element.setStyle({ opacity: 
-      (/Gecko/.test(navigator.userAgent) &amp;&amp; !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 
-      0.999999 : 1.0 });
-    if(/MSIE/.test(navigator.userAgent) &amp;&amp; !window.opera)  
-      element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});  
-  } else {  
-    if(value &lt; 0.00001) value = 0;  
-    element.setStyle({opacity: value});
-    if(/MSIE/.test(navigator.userAgent) &amp;&amp; !window.opera)  
-      element.setStyle(
-        { filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
-            'alpha(opacity='+value*100+')' });  
-  }
-  return element;
-}  
- 
-Element.getInlineOpacity = function(element){  
-  return $(element).style.opacity || '';
-}  
-
-Element.forceRerendering = function(element) {
-  try {
-    element = $(element);
-    var n = document.createTextNode(' ');
-    element.appendChild(n);
-    element.removeChild(n);
-  } catch(e) { }
-};
-
-/*--------------------------------------------------------------------------*/
-
-Array.prototype.call = function() {
-  var args = arguments;
-  this.each(function(f){ f.apply(this, args) });
-}
-
-/*--------------------------------------------------------------------------*/
-
-var Effect = {
-  _elementDoesNotExistError: {
-    name: 'ElementDoesNotExistError',
-    message: 'The specified DOM element does not exist, but is required for this effect to operate'
-  },
-  tagifyText: function(element) {
-    if(typeof Builder == 'undefined')
-      throw(&quot;Effect.tagifyText requires including script.aculo.us' builder.js library&quot;);
-      
-    var tagifyStyle = 'position:relative';
-    if(/MSIE/.test(navigator.userAgent) &amp;&amp; !window.opera) 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')) &amp;&amp; 
-       (element.length))
-      elements = element;
-    else
-      elements = $(element).childNodes;
-      
-    var options = Object.extend({
-      speed: 0.1,
-      delay: 0.0
-    }, arguments[2] || {});
-    var masterDelay = options.delay;
-
-    $A(elements).each( function(element, index) {
-      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
-    });
-  },
-  PAIRS: {
-    'slide':  ['SlideDown','SlideUp'],
-    'blind':  ['BlindDown','BlindUp'],
-    'appear': ['Appear','Fade']
-  },
-  toggle: function(element, effect) {
-    element = $(element);
-    effect = (effect || 'appear').toLowerCase();
-    var options = Object.extend({
-      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
-    }, arguments[2] || {});
-    Effect[element.visible() ? 
-      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
-  }
-};
-
-var Effect2 = Effect; // deprecated
-
-/* ------------- transitions ------------- */
-
-Effect.Transitions = {
-  linear: Prototype.K,
-  sinoidal: function(pos) {
-    return (-Math.cos(pos*Math.PI)/2) + 0.5;
-  },
-  reverse: function(pos) {
-    return 1-pos;
-  },
-  flicker: function(pos) {
-    return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
-  },
-  wobble: function(pos) {
-    return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
-  },
-  pulse: function(pos, pulses) { 
-    pulses = pulses || 5; 
-    return (
-      Math.round((pos % (1/pulses)) * pulses) == 0 ? 
-            ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) : 
-        1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2))
-      );
-  },
-  none: function(pos) {
-    return 0;
-  },
-  full: function(pos) {
-    return 1;
-  }
-};
-
-/* ------------- core effects ------------- */
-
-Effect.ScopedQueue = Class.create();
-Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
-  initialize: function() {
-    this.effects  = [];
-    this.interval = null;
-  },
-  _each: function(iterator) {
-    this.effects._each(iterator);
-  },
-  add: function(effect) {
-    var timestamp = new Date().getTime();
-    
-    var position = (typeof effect.options.queue == 'string') ? 
-      effect.options.queue : effect.options.queue.position;
-    
-    switch(position) {
-      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 'with-last':
-        timestamp = this.effects.pluck('startOn').max() || timestamp;
-        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;
-
-    if(!effect.options.queue.limit || (this.effects.length &lt; effect.options.queue.limit))
-      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);
-  }
-});
-
-Effect.Queues = {
-  instances: $H(),
-  get: function(queueName) {
-    if(typeof queueName != 'string') return queueName;
-    
-    if(!this.instances[queueName])
-      this.instances[queueName] = new Effect.ScopedQueue();
-      
-    return this.instances[queueName];
-  }
-}
-Effect.Queue = Effect.Queues.get('global');
-
-Effect.DefaultOptions = {
-  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'
-}
-
-Effect.Base = function() {};
-Effect.Base.prototype = {
-  position: null,
-  start: function(options) {
-    this.options      = Object.extend(Object.extend({},Effect.DefaultOptions), 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.Queues.get(typeof this.options.queue == 'string' ? 
-        'global' : this.options.queue.scope).add(this);
-  },
-  loop: function(timePos) {
-    if(timePos &gt;= this.startOn) {
-      if(timePos &gt;= 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 &gt; 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.state == 'running') {
-      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.Queues.get(typeof this.options.queue == 'string' ? 
-        'global' : this.options.queue.scope).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);
-  },
-  inspect: function() {
-    return '#&lt;Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '&gt;';
-  }
-}
-
-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.Event = Class.create();
-Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), {
-  initialize: function() {
-    var options = Object.extend({
-      duration: 0
-    }, arguments[0] || {});
-    this.start(options);
-  },
-  update: Prototype.emptyFunction
-});
-
-Effect.Opacity = Class.create();
-Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
-  initialize: function(element) {
-    this.element = $(element);
-    if(!this.element) throw(Effect._elementDoesNotExistError);
-    // make this work on IE on elements without 'layout'
-    if(/MSIE/.test(navigator.userAgent) &amp;&amp; !window.opera &amp;&amp; (!this.element.currentStyle.hasLayout))
-      this.element.setStyle({zoom: 1});
-    var options = Object.extend({
-      from: this.element.getOpacity() || 0.0,
-      to:   1.0
-    }, arguments[1] || {});
-    this.start(options);
-  },
-  update: function(position) {
-    this.element.setOpacity(position);
-  }
-});
-
-Effect.Move = Class.create();
-Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
-  initialize: function(element) {
-    this.element = $(element);
-    if(!this.element) throw(Effect._elementDoesNotExistError);
-    var options = Object.extend({
-      x:    0,
-      y:    0,
-      mode: 'relative'
-    }, arguments[1] || {});
-    this.start(options);
-  },
-  setup: function() {
-    // Bug in Opera: Opera returns the &quot;real&quot; position of a static element or
-    // relative element that does not have top/left explicitly set.
-    // ==&gt; Always set top and left for position relative elements in your stylesheets 
-    // (to 0 if you do not need them) 
-    this.element.makePositioned();
-    this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
-    this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
-    if(this.options.mode == 'absolute') {
-      // absolute movement, so we need to calc deltaX and deltaY
-      this.options.x = this.options.x - this.originalLeft;
-      this.options.y = this.options.y - this.originalTop;
-    }
-  },
-  update: function(position) {
-    this.element.setStyle({
-      left: Math.round(this.options.x  * position + this.originalLeft) + 'px',
-      top:  Math.round(this.options.y  * position + this.originalTop)  + 'px'
-    });
-  }
-});
-
-// for backwards compatibility
-Effect.MoveBy = function(element, toTop, toLeft) {
-  return new Effect.Move(element, 
-    Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
-};
-
-Effect.Scale = Class.create();
-Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
-  initialize: function(element, percent) {
-    this.element = $(element);
-    if(!this.element) throw(Effect._elementDoesNotExistError);
-    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() {
-    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
-    this.elementPositioning = this.element.getStyle('position');
-    
-    this.originalStyle = {};
-    ['top','left','width','height','fontSize'].each( function(k) {
-      this.originalStyle[k] = this.element.style[k];
-    }.bind(this));
-      
-    this.originalTop  = this.element.offsetTop;
-    this.originalLeft = this.element.offsetLeft;
-    
-    var fontSize = this.element.getStyle('font-size') || '100%';
-    ['em','px','%','pt'].each( function(fontSizeType) {
-      if(fontSize.indexOf(fontSizeType)&gt;0) {
-        this.fontSize     = parseFloat(fontSize);
-        this.fontSizeType = fontSizeType;
-      }
-    }.bind(this));
-    
-    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
-    
-    this.dims = null;
-    if(this.options.scaleMode=='box')
-      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
-    if(/^content/.test(this.options.scaleMode))
-      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 &amp;&amp; this.fontSize)
-      this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
-    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
-  },
-  finish: function(position) {
-    if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
-  },
-  setDimensions: function(height, width) {
-    var d = {};
-    if(this.options.scaleX) d.width = Math.round(width) + 'px';
-    if(this.options.scaleY) d.height = Math.round(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) d.top = this.originalTop-topd + 'px';
-        if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
-      } else {
-        if(this.options.scaleY) d.top = -topd + 'px';
-        if(this.options.scaleX) d.left = -leftd + 'px';
-      }
-    }
-    this.element.setStyle(d);
-  }
-});
-
-Effect.Highlight = Class.create();
-Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
-  initialize: function(element) {
-    this.element = $(element);
-    if(!this.element) throw(Effect._elementDoesNotExistError);
-    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.getStyle('display')=='none') { this.cancel(); return; }
-    // Disable background image during the effect
-    this.oldStyle = {
-      backgroundImage: this.element.getStyle('background-image') };
-    this.element.setStyle({backgroundImage: 'none'});
-    if(!this.options.endcolor)
-      this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
-    if(!this.options.restorecolor)
-      this.options.restorecolor = this.element.getStyle('background-color');
-    // init color calculations
-    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
-    this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
-  },
-  update: function(position) {
-    this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
-      return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
-  },
-  finish: function() {
-    this.element.setStyle(Object.extend(this.oldStyle, {
-      backgroundColor: this.options.restorecolor
-    }));
-  }
-});
-
-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);
-    if(this.options.offset) offsets[1] += this.options.offset;
-    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] &gt; 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) {
-  element = $(element);
-  var oldOpacity = element.getInlineOpacity();
-  var options = Object.extend({
-  from: element.getOpacity() || 1.0,
-  to:   0.0,
-  afterFinishInternal: function(effect) { 
-    if(effect.options.to!=0) return;
-    effect.element.hide().setStyle({opacity: oldOpacity}); 
-  }}, arguments[1] || {});
-  return new Effect.Opacity(element,options);
-}
-
-Effect.Appear = function(element) {
-  element = $(element);
-  var options = Object.extend({
-  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
-  to:   1.0,
-  // force Safari to render floated elements properly
-  afterFinishInternal: function(effect) {
-    effect.element.forceRerendering();
-  },
-  beforeSetup: function(effect) {
-    effect.element.setOpacity(effect.options.from).show(); 
-  }}, arguments[1] || {});
-  return new Effect.Opacity(element,options);
-}
-
-Effect.Puff = function(element) {
-  element = $(element);
-  var oldStyle = { 
-    opacity: element.getInlineOpacity(), 
-    position: element.getStyle('position'),
-    top:  element.style.top,
-    left: element.style.left,
-    width: element.style.width,
-    height: element.style.height
-  };
-  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) {
-        Position.absolutize(effect.effects[0].element)
-      },
-      afterFinishInternal: function(effect) {
-         effect.effects[0].element.hide().setStyle(oldStyle); }
-     }, arguments[1] || {})
-   );
-}
-
-Effect.BlindUp = function(element) {
-  element = $(element);
-  element.makeClipping();
-  return new Effect.Scale(element, 0,
-    Object.extend({ scaleContent: false, 
-      scaleX: false, 
-      restoreAfterFinish: true,
-      afterFinishInternal: function(effect) {
-        effect.element.hide().undoClipping();
-      } 
-    }, arguments[1] || {})
-  );
-}
-
-Effect.BlindDown = function(element) {
-  element = $(element);
-  var elementDimensions = element.getDimensions();
-  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) {
-      effect.element.makeClipping().setStyle({height: '0px'}).show(); 
-    },  
-    afterFinishInternal: function(effect) {
-      effect.element.undoClipping();
-    }
-  }, arguments[1] || {}));
-}
-
-Effect.SwitchOff = function(element) {
-  element = $(element);
-  var oldOpacity = element.getInlineOpacity();
-  return new Effect.Appear(element, Object.extend({
-    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) { 
-          effect.element.makePositioned().makeClipping();
-        },
-        afterFinishInternal: function(effect) {
-          effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
-        }
-      })
-    }
-  }, arguments[1] || {}));
-}
-
-Effect.DropOut = function(element) {
-  element = $(element);
-  var oldStyle = {
-    top: element.getStyle('top'),
-    left: element.getStyle('left'),
-    opacity: element.getInlineOpacity() };
-  return new Effect.Parallel(
-    [ new Effect.Move(element, {x: 0, y: 100, sync: true }), 
-      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
-    Object.extend(
-      { duration: 0.5,
-        beforeSetup: function(effect) {
-          effect.effects[0].element.makePositioned(); 
-        },
-        afterFinishInternal: function(effect) {
-          effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
-        } 
-      }, arguments[1] || {}));
-}
-
-Effect.Shake = function(element) {
-  element = $(element);
-  var oldStyle = {
-    top: element.getStyle('top'),
-    left: element.getStyle('left') };
-    return new Effect.Move(element, 
-      { x:  20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
-    new Effect.Move(effect.element,
-      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
-    new Effect.Move(effect.element,
-      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
-    new Effect.Move(effect.element,
-      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
-    new Effect.Move(effect.element,
-      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
-    new Effect.Move(effect.element,
-      { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
-        effect.element.undoPositioned().setStyle(oldStyle);
-  }}) }}) }}) }}) }}) }});
-}
-
-Effect.SlideDown = function(element) {
-  element = $(element).cleanWhitespace();
-  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
-  var oldInnerBottom = element.down().getStyle('bottom');
-  var elementDimensions = element.getDimensions();
-  return new Effect.Scale(element, 100, Object.extend({ 
-    scaleContent: false, 
-    scaleX: false, 
-    scaleFrom: window.opera ? 0 : 1,
-    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
-    restoreAfterFinish: true,
-    afterSetup: function(effect) {
-      effect.element.makePositioned();
-      effect.element.down().makePositioned();
-      if(window.opera) effect.element.setStyle({top: ''});
-      effect.element.makeClipping().setStyle({height: '0px'}).show(); 
-    },
-    afterUpdateInternal: function(effect) {
-      effect.element.down().setStyle({bottom:
-        (effect.dims[0] - effect.element.clientHeight) + 'px' }); 
-    },
-    afterFinishInternal: function(effect) {
-      effect.element.undoClipping().undoPositioned();
-      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
-    }, arguments[1] || {})
-  );
-}
-
-Effect.SlideUp = function(element) {
-  element = $(element).cleanWhitespace();
-  var oldInnerBottom = element.down().getStyle('bottom');
-  return new Effect.Scale(element, window.opera ? 0 : 1,
-   Object.extend({ scaleContent: false, 
-    scaleX: false, 
-    scaleMode: 'box',
-    scaleFrom: 100,
-    restoreAfterFinish: true,
-    beforeStartInternal: function(effect) {
-      effect.element.makePositioned();
-      effect.element.down().makePositioned();
-      if(window.opera) effect.element.setStyle({top: ''});
-      effect.element.makeClipping().show();
-    },  
-    afterUpdateInternal: function(effect) {
-      effect.element.down().setStyle({bottom:
-        (effect.dims[0] - effect.element.clientHeight) + 'px' });
-    },
-    afterFinishInternal: function(effect) {
-      effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom});
-      effect.element.down().undoPositioned();
-    }
-   }, arguments[1] || {})
-  );
-}
-
-// Bug in opera makes the TD containing this element expand for a instance after finish 
-Effect.Squish = function(element) {
-  return new Effect.Scale(element, window.opera ? 1 : 0, { 
-    restoreAfterFinish: true,
-    beforeSetup: function(effect) {
-      effect.element.makeClipping(); 
-    },  
-    afterFinishInternal: function(effect) {
-      effect.element.hide().undoClipping(); 
-    }
-  });
-}
-
-Effect.Grow = function(element) {
-  element = $(element);
-  var options = Object.extend({
-    direction: 'center',
-    moveTransition: Effect.Transitions.sinoidal,
-    scaleTransition: Effect.Transitions.sinoidal,
-    opacityTransition: Effect.Transitions.full
-  }, arguments[1] || {});
-  var oldStyle = {
-    top: element.style.top,
-    left: element.style.left,
-    height: element.style.height,
-    width: element.style.width,
-    opacity: element.getInlineOpacity() };
-
-  var dims = element.getDimensions();    
-  var initialMoveX, initialMoveY;
-  var moveX, moveY;
-  
-  switch (options.direction) {
-    case 'top-left':
-      initialMoveX = initialMoveY = moveX = moveY = 0; 
-      break;
-    case 'top-right':
-      initialMoveX = dims.width;
-      initialMoveY = moveY = 0;
-      moveX = -dims.width;
-      break;
-    case 'bottom-left':
-      initialMoveX = moveX = 0;
-      initialMoveY = dims.height;
-      moveY = -dims.height;
-      break;
-    case 'bottom-right':
-      initialMoveX = dims.width;
-      initialMoveY = dims.height;
-      moveX = -dims.width;
-      moveY = -dims.height;
-      break;
-    case 'center':
-      initialMoveX = dims.width / 2;
-      initialMoveY = dims.height / 2;
-      moveX = -dims.width / 2;
-      moveY = -dims.height / 2;
-      break;
-  }
-  
-  return new Effect.Move(element, {
-    x: initialMoveX,
-    y: initialMoveY,
-    duration: 0.01, 
-    beforeSetup: function(effect) {
-      effect.element.hide().makeClipping().makePositioned();
-    },
-    afterFinishInternal: function(effect) {
-      new Effect.Parallel(
-        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
-          new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
-          new Effect.Scale(effect.element, 100, {
-            scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, 
-            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
-        ], Object.extend({
-             beforeSetup: function(effect) {
-               effect.effects[0].element.setStyle({height: '0px'}).show(); 
-             },
-             afterFinishInternal: function(effect) {
-               effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); 
-             }
-           }, options)
-      )
-    }
-  });
-}
-
-Effect.Shrink = function(element) {
-  element = $(element);
-  var options = Object.extend({
-    direction: 'center',
-    moveTransition: Effect.Transitions.sinoidal,
-    scaleTransition: Effect.Transitions.sinoidal,
-    opacityTransition: Effect.Transitions.none
-  }, arguments[1] || {});
-  var oldStyle = {
-    top: element.style.top,
-    left: element.style.left,
-    height: element.style.height,
-    width: element.style.width,
-    opacity: element.getInlineOpacity() };
-
-  var dims = element.getDimensions();
-  var moveX, moveY;
-  
-  switch (options.direction) {
-    case 'top-left':
-      moveX = moveY = 0;
-      break;
-    case 'top-right':
-      moveX = dims.width;
-      moveY = 0;
-      break;
-    case 'bottom-left':
-      moveX = 0;
-      moveY = dims.height;
-      break;
-    case 'bottom-right':
-      moveX = dims.width;
-      moveY = dims.height;
-      break;
-    case 'center':  
-      moveX = dims.width / 2;
-      moveY = dims.height / 2;
-      break;
-  }
-  
-  return new Effect.Parallel(
-    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
-      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
-      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
-    ], Object.extend({            
-         beforeStartInternal: function(effect) {
-           effect.effects[0].element.makePositioned().makeClipping(); 
-         },
-         afterFinishInternal: function(effect) {
-           effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
-       }, options)
-  );
-}
-
-Effect.Pulsate = function(element) {
-  element = $(element);
-  var options    = arguments[1] || {};
-  var oldOpacity = element.getInlineOpacity();
-  var transition = options.transition || Effect.Transitions.sinoidal;
-  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
-  reverser.bind(transition);
-  return new Effect.Opacity(element, 
-    Object.extend(Object.extend({  duration: 2.0, from: 0,
-      afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
-    }, options), {transition: reverser}));
-}
-
-Effect.Fold = function(element) {
-  element = $(element);
-  var oldStyle = {
-    top: element.style.top,
-    left: element.style.left,
-    width: element.style.width,
-    height: element.style.height };
-  element.makeClipping();
-  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) {
-        effect.element.hide().undoClipping().setStyle(oldStyle);
-      } });
-  }}, arguments[1] || {}));
-};
-
-Effect.Morph = Class.create();
-Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), {
-  initialize: function(element) {
-    this.element = $(element);
-    if(!this.element) throw(Effect._elementDoesNotExistError);
-    var options = Object.extend({
-      style: ''
-    }, arguments[1] || {});
-    this.start(options);
-  },
-  setup: function(){
-    function parseColor(color){
-      if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
-      color = color.parseColor();
-      return $R(0,2).map(function(i){
-        return parseInt( color.slice(i*2+1,i*2+3), 16 ) 
-      });
-    }
-    this.transforms = this.options.style.parseStyle().map(function(property){
-      var originalValue = this.element.getStyle(property[0]);
-      return $H({ 
-        style: property[0], 
-        originalValue: property[1].unit=='color' ? 
-          parseColor(originalValue) : parseFloat(originalValue || 0), 
-        targetValue: property[1].unit=='color' ? 
-          parseColor(property[1].value) : property[1].value,
-        unit: property[1].unit
-      });
-    }.bind(this)).reject(function(transform){
-      return (
-        (transform.originalValue == transform.targetValue) ||
-        (
-          transform.unit != 'color' &amp;&amp;
-          (isNaN(transform.originalValue) || isNaN(transform.targetValue))
-        )
-      )
-    });
-  },
-  update: function(position) {
-    var style = $H(), value = null;
-    this.transforms.each(function(transform){
-      value = transform.unit=='color' ?
-        $R(0,2).inject('#',function(m,v,i){
-          return m+(Math.round(transform.originalValue[i]+
-            (transform.targetValue[i] - transform.originalValue[i])*position)).toColorPart() }) : 
-        transform.originalValue + Math.round(
-          ((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit;
-      style[transform.style] = value;
-    });
-    this.element.setStyle(style);
-  }
-});
-
-Effect.Transform = Class.create();
-Object.extend(Effect.Transform.prototype, {
-  initialize: function(tracks){
-    this.tracks  = [];
-    this.options = arguments[1] || {};
-    this.addTracks(tracks);
-  },
-  addTracks: function(tracks){
-    tracks.each(function(track){
-      var data = $H(track).values().first();
-      this.tracks.push($H({
-        ids:     $H(track).keys().first(),
-        effect:  Effect.Morph,
-        options: { style: data }
-      }));
-    }.bind(this));
-    return this;
-  },
-  play: function(){
-    return new Effect.Parallel(
-      this.tracks.map(function(track){
-        var elements = [$(track.ids) || $$(track.ids)].flatten();
-        return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) });
-      }).flatten(),
-      this.options
-    );
-  }
-});
-
-Element.CSS_PROPERTIES = ['azimuth', 'backgroundAttachment', 'backgroundColor', 'backgroundImage', 
-  'backgroundPosition', 'backgroundRepeat', 'borderBottomColor', 'borderBottomStyle', 
-  'borderBottomWidth', 'borderCollapse', 'borderLeftColor', 'borderLeftStyle', 'borderLeftWidth',
-  'borderRightColor', 'borderRightStyle', 'borderRightWidth', 'borderSpacing', 'borderTopColor',
-  'borderTopStyle', 'borderTopWidth', 'bottom', 'captionSide', 'clear', 'clip', 'color', 'content',
-  'counterIncrement', 'counterReset', 'cssFloat', 'cueAfter', 'cueBefore', 'cursor', 'direction',
-  'display', 'elevation', 'emptyCells', 'fontFamily', 'fontSize', 'fontSizeAdjust', 'fontStretch',
-  'fontStyle', 'fontVariant', 'fontWeight', 'height', 'left', 'letterSpacing', 'lineHeight',
-  'listStyleImage', 'listStylePosition', 'listStyleType', 'marginBottom', 'marginLeft', 'marginRight',
-  'marginTop', 'markerOffset', 'marks', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'opacity',
-  'orphans', 'outlineColor', 'outlineOffset', 'outlineStyle', 'outlineWidth', 'overflowX', 'overflowY',
-  'paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop', 'page', 'pageBreakAfter', 'pageBreakBefore',
-  'pageBreakInside', 'pauseAfter', 'pauseBefore', 'pitch', 'pitchRange', 'position', 'quotes',
-  'richness', 'right', 'size', 'speakHeader', 'speakNumeral', 'speakPunctuation', 'speechRate', 'stress',
-  'tableLayout', 'textAlign', 'textDecoration', 'textIndent', 'textShadow', 'textTransform', 'top',
-  'unicodeBidi', 'verticalAlign', 'visibility', 'voiceFamily', 'volume', 'whiteSpace', 'widows',
-  'width', 'wordSpacing', 'zIndex'];
-  
-Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
-
-String.prototype.parseStyle = function(){
-  var element = Element.extend(document.createElement('div'));
-  element.innerHTML = '&lt;div style=&quot;' + this + '&quot;&gt;&lt;/div&gt;';
-  var style = element.down().style, styleRules = $H();
-  
-  Element.CSS_PROPERTIES.each(function(property){
-   if(style[property]) styleRules[property] = style[property]; 
-  });
-  
-  var result = $H();
-  
-  styleRules.each(function(pair){
-    var property = pair[0], value = pair[1], unit = null;
-    
-    if(value.parseColor('#zzzzzz') != '#zzzzzz') {
-      value = value.parseColor();
-      unit  = 'color';
-    } else if(Element.CSS_LENGTH.test(value)) 
-      var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/),
-          value = parseFloat(components[1]), unit = (components.length == 3) ? components[2] : null;
-    
-    result[property.underscore().dasherize()] = $H({ value:value, unit:unit });
-  }.bind(this));
-  
-  return result;
-};
-
-Element.morph = function(element, style) {
-  new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {}));
-  return element;
-};
-
-['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
- 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each( 
-  function(f) { Element.Methods[f] = Element[f]; }
-);
-
-Element.Methods.visualEffect = function(element, effect, options) {
-  s = effect.gsub(/_/, '-').camelize();
-  effect_class = s.charAt(0).toUpperCase() + s.substring(1);
-  new Effect[effect_class](element, options);
-  return $(element);
-};
-
+// Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// Contributors:
+//  Justin Palmer (http://encytemedia.com/)
+//  Mark Pilgrim (http://diveintomark.org/)
+//  Martin Bialasinki
+// 
+// script.aculo.us is freely distributable under the terms of an MIT-style license.
+// For details, see the script.aculo.us web site: http://script.aculo.us/ 
+
+// converts rgb() and #xxx to #xxxxxx format,  
+// returns self (or first argument) if not convertable  
+String.prototype.parseColor = function() {  
+  var 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&lt;3);  
+  } else {  
+    if(this.slice(0,1) == '#') {  
+      if(this.length==4) for(var i=1;i&lt;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.collectTextNodes = function(element) {  
+  return $A($(element).childNodes).collect( function(node) {
+    return (node.nodeType==3 ? node.nodeValue : 
+      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
+  }).flatten().join('');
+}
+
+Element.collectTextNodesIgnoreClass = function(element, className) {  
+  return $A($(element).childNodes).collect( function(node) {
+    return (node.nodeType==3 ? node.nodeValue : 
+      ((node.hasChildNodes() &amp;&amp; !Element.hasClassName(node,className)) ? 
+        Element.collectTextNodesIgnoreClass(node, className) : ''));
+  }).flatten().join('');
+}
+
+Element.setContentZoom = function(element, percent) {
+  element = $(element);  
+  element.setStyle({fontSize: (percent/100) + 'em'});   
+  if(navigator.appVersion.indexOf('AppleWebKit')&gt;0) window.scrollBy(0,0);
+  return element;
+}
+
+Element.getOpacity = function(element){
+  element = $(element);
+  var opacity;
+  if (opacity = element.getStyle('opacity'))  
+    return parseFloat(opacity);  
+  if (opacity = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))  
+    if(opacity[1]) return parseFloat(opacity[1]) / 100;  
+  return 1.0;  
+}
+
+Element.setOpacity = function(element, value){  
+  element= $(element);  
+  if (value == 1){
+    element.setStyle({ opacity: 
+      (/Gecko/.test(navigator.userAgent) &amp;&amp; !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 
+      0.999999 : 1.0 });
+    if(/MSIE/.test(navigator.userAgent) &amp;&amp; !window.opera)  
+      element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});  
+  } else {  
+    if(value &lt; 0.00001) value = 0;  
+    element.setStyle({opacity: value});
+    if(/MSIE/.test(navigator.userAgent) &amp;&amp; !window.opera)  
+      element.setStyle(
+        { filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
+            'alpha(opacity='+value*100+')' });  
+  }
+  return element;
+}  
+ 
+Element.getInlineOpacity = function(element){  
+  return $(element).style.opacity || '';
+}  
+
+Element.forceRerendering = function(element) {
+  try {
+    element = $(element);
+    var n = document.createTextNode(' ');
+    element.appendChild(n);
+    element.removeChild(n);
+  } catch(e) { }
+};
+
+/*--------------------------------------------------------------------------*/
+
+Array.prototype.call = function() {
+  var args = arguments;
+  this.each(function(f){ f.apply(this, args) });
+}
+
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+  _elementDoesNotExistError: {
+    name: 'ElementDoesNotExistError',
+    message: 'The specified DOM element does not exist, but is required for this effect to operate'
+  },
+  tagifyText: function(element) {
+    if(typeof Builder == 'undefined')
+      throw(&quot;Effect.tagifyText requires including script.aculo.us' builder.js library&quot;);
+      
+    var tagifyStyle = 'position:relative';
+    if(/MSIE/.test(navigator.userAgent) &amp;&amp; !window.opera) 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')) &amp;&amp; 
+       (element.length))
+      elements = element;
+    else
+      elements = $(element).childNodes;
+      
+    var options = Object.extend({
+      speed: 0.1,
+      delay: 0.0
+    }, arguments[2] || {});
+    var masterDelay = options.delay;
+
+    $A(elements).each( function(element, index) {
+      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
+    });
+  },
+  PAIRS: {
+    'slide':  ['SlideDown','SlideUp'],
+    'blind':  ['BlindDown','BlindUp'],
+    'appear': ['Appear','Fade']
+  },
+  toggle: function(element, effect) {
+    element = $(element);
+    effect = (effect || 'appear').toLowerCase();
+    var options = Object.extend({
+      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
+    }, arguments[2] || {});
+    Effect[element.visible() ? 
+      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
+  }
+};
+
+var Effect2 = Effect; // deprecated
+
+/* ------------- transitions ------------- */
+
+Effect.Transitions = {
+  linear: Prototype.K,
+  sinoidal: function(pos) {
+    return (-Math.cos(pos*Math.PI)/2) + 0.5;
+  },
+  reverse: function(pos) {
+    return 1-pos;
+  },
+  flicker: function(pos) {
+    return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
+  },
+  wobble: function(pos) {
+    return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
+  },
+  pulse: function(pos, pulses) { 
+    pulses = pulses || 5; 
+    return (
+      Math.round((pos % (1/pulses)) * pulses) == 0 ? 
+            ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) : 
+        1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2))
+      );
+  },
+  none: function(pos) {
+    return 0;
+  },
+  full: function(pos) {
+    return 1;
+  }
+};
+
+/* ------------- core effects ------------- */
+
+Effect.ScopedQueue = Class.create();
+Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
+  initialize: function() {
+    this.effects  = [];
+    this.interval = null;
+  },
+  _each: function(iterator) {
+    this.effects._each(iterator);
+  },
+  add: function(effect) {
+    var timestamp = new Date().getTime();
+    
+    var position = (typeof effect.options.queue == 'string') ? 
+      effect.options.queue : effect.options.queue.position;
+    
+    switch(position) {
+      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 'with-last':
+        timestamp = this.effects.pluck('startOn').max() || timestamp;
+        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;
+
+    if(!effect.options.queue.limit || (this.effects.length &lt; effect.options.queue.limit))
+      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);
+  }
+});
+
+Effect.Queues = {
+  instances: $H(),
+  get: function(queueName) {
+    if(typeof queueName != 'string') return queueName;
+    
+    if(!this.instances[queueName])
+      this.instances[queueName] = new Effect.ScopedQueue();
+      
+    return this.instances[queueName];
+  }
+}
+Effect.Queue = Effect.Queues.get('global');
+
+Effect.DefaultOptions = {
+  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'
+}
+
+Effect.Base = function() {};
+Effect.Base.prototype = {
+  position: null,
+  start: function(options) {
+    this.options      = Object.extend(Object.extend({},Effect.DefaultOptions), 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.Queues.get(typeof this.options.queue == 'string' ? 
+        'global' : this.options.queue.scope).add(this);
+  },
+  loop: function(timePos) {
+    if(timePos &gt;= this.startOn) {
+      if(timePos &gt;= 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 &gt; 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.state == 'running') {
+      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.Queues.get(typeof this.options.queue == 'string' ? 
+        'global' : this.options.queue.scope).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);
+  },
+  inspect: function() {
+    return '#&lt;Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '&gt;';
+  }
+}
+
+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.Event = Class.create();
+Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), {
+  initialize: function() {
+    var options = Object.extend({
+      duration: 0
+    }, arguments[0] || {});
+    this.start(options);
+  },
+  update: Prototype.emptyFunction
+});
+
+Effect.Opacity = Class.create();
+Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    if(!this.element) throw(Effect._elementDoesNotExistError);
+    // make this work on IE on elements without 'layout'
+    if(/MSIE/.test(navigator.userAgent) &amp;&amp; !window.opera &amp;&amp; (!this.element.currentStyle.hasLayout))
+      this.element.setStyle({zoom: 1});
+    var options = Object.extend({
+      from: this.element.getOpacity() || 0.0,
+      to:   1.0
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  update: function(position) {
+    this.element.setOpacity(position);
+  }
+});
+
+Effect.Move = Class.create();
+Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    if(!this.element) throw(Effect._elementDoesNotExistError);
+    var options = Object.extend({
+      x:    0,
+      y:    0,
+      mode: 'relative'
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  setup: function() {
+    // Bug in Opera: Opera returns the &quot;real&quot; position of a static element or
+    // relative element that does not have top/left explicitly set.
+    // ==&gt; Always set top and left for position relative elements in your stylesheets 
+    // (to 0 if you do not need them) 
+    this.element.makePositioned();
+    this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
+    this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
+    if(this.options.mode == 'absolute') {
+      // absolute movement, so we need to calc deltaX and deltaY
+      this.options.x = this.options.x - this.originalLeft;
+      this.options.y = this.options.y - this.originalTop;
+    }
+  },
+  update: function(position) {
+    this.element.setStyle({
+      left: Math.round(this.options.x  * position + this.originalLeft) + 'px',
+      top:  Math.round(this.options.y  * position + this.originalTop)  + 'px'
+    });
+  }
+});
+
+// for backwards compatibility
+Effect.MoveBy = function(element, toTop, toLeft) {
+  return new Effect.Move(element, 
+    Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
+};
+
+Effect.Scale = Class.create();
+Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
+  initialize: function(element, percent) {
+    this.element = $(element);
+    if(!this.element) throw(Effect._elementDoesNotExistError);
+    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() {
+    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
+    this.elementPositioning = this.element.getStyle('position');
+    
+    this.originalStyle = {};
+    ['top','left','width','height','fontSize'].each( function(k) {
+      this.originalStyle[k] = this.element.style[k];
+    }.bind(this));
+      
+    this.originalTop  = this.element.offsetTop;
+    this.originalLeft = this.element.offsetLeft;
+    
+    var fontSize = this.element.getStyle('font-size') || '100%';
+    ['em','px','%','pt'].each( function(fontSizeType) {
+      if(fontSize.indexOf(fontSizeType)&gt;0) {
+        this.fontSize     = parseFloat(fontSize);
+        this.fontSizeType = fontSizeType;
+      }
+    }.bind(this));
+    
+    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
+    
+    this.dims = null;
+    if(this.options.scaleMode=='box')
+      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
+    if(/^content/.test(this.options.scaleMode))
+      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 &amp;&amp; this.fontSize)
+      this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
+    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
+  },
+  finish: function(position) {
+    if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
+  },
+  setDimensions: function(height, width) {
+    var d = {};
+    if(this.options.scaleX) d.width = Math.round(width) + 'px';
+    if(this.options.scaleY) d.height = Math.round(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) d.top = this.originalTop-topd + 'px';
+        if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
+      } else {
+        if(this.options.scaleY) d.top = -topd + 'px';
+        if(this.options.scaleX) d.left = -leftd + 'px';
+      }
+    }
+    this.element.setStyle(d);
+  }
+});
+
+Effect.Highlight = Class.create();
+Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    if(!this.element) throw(Effect._elementDoesNotExistError);
+    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.getStyle('display')=='none') { this.cancel(); return; }
+    // Disable background image during the effect
+    this.oldStyle = {
+      backgroundImage: this.element.getStyle('background-image') };
+    this.element.setStyle({backgroundImage: 'none'});
+    if(!this.options.endcolor)
+      this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
+    if(!this.options.restorecolor)
+      this.options.restorecolor = this.element.getStyle('background-color');
+    // init color calculations
+    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
+    this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
+  },
+  update: function(position) {
+    this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
+      return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
+  },
+  finish: function() {
+    this.element.setStyle(Object.extend(this.oldStyle, {
+      backgroundColor: this.options.restorecolor
+    }));
+  }
+});
+
+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);
+    if(this.options.offset) offsets[1] += this.options.offset;
+    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] &gt; 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) {
+  element = $(element);
+  var oldOpacity = element.getInlineOpacity();
+  var options = Object.extend({
+  from: element.getOpacity() || 1.0,
+  to:   0.0,
+  afterFinishInternal: function(effect) { 
+    if(effect.options.to!=0) return;
+    effect.element.hide().setStyle({opacity: oldOpacity}); 
+  }}, arguments[1] || {});
+  return new Effect.Opacity(element,options);
+}
+
+Effect.Appear = function(element) {
+  element = $(element);
+  var options = Object.extend({
+  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
+  to:   1.0,
+  // force Safari to render floated elements properly
+  afterFinishInternal: function(effect) {
+    effect.element.forceRerendering();
+  },
+  beforeSetup: function(effect) {
+    effect.element.setOpacity(effect.options.from).show(); 
+  }}, arguments[1] || {});
+  return new Effect.Opacity(element,options);
+}
+
+Effect.Puff = function(element) {
+  element = $(element);
+  var oldStyle = { 
+    opacity: element.getInlineOpacity(), 
+    position: element.getStyle('position'),
+    top:  element.style.top,
+    left: element.style.left,
+    width: element.style.width,
+    height: element.style.height
+  };
+  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) {
+        Position.absolutize(effect.effects[0].element)
+      },
+      afterFinishInternal: function(effect) {
+         effect.effects[0].element.hide().setStyle(oldStyle); }
+     }, arguments[1] || {})
+   );
+}
+
+Effect.BlindUp = function(element) {
+  element = $(element);
+  element.makeClipping();
+  return new Effect.Scale(element, 0,
+    Object.extend({ scaleContent: false, 
+      scaleX: false, 
+      restoreAfterFinish: true,
+      afterFinishInternal: function(effect) {
+        effect.element.hide().undoClipping();
+      } 
+    }, arguments[1] || {})
+  );
+}
+
+Effect.BlindDown = function(element) {
+  element = $(element);
+  var elementDimensions = element.getDimensions();
+  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) {
+      effect.element.makeClipping().setStyle({height: '0px'}).show(); 
+    },  
+    afterFinishInternal: function(effect) {
+      effect.element.undoClipping();
+    }
+  }, arguments[1] || {}));
+}
+
+Effect.SwitchOff = function(element) {
+  element = $(element);
+  var oldOpacity = element.getInlineOpacity();
+  return new Effect.Appear(element, Object.extend({
+    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) { 
+          effect.element.makePositioned().makeClipping();
+        },
+        afterFinishInternal: function(effect) {
+          effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
+        }
+      })
+    }
+  }, arguments[1] || {}));
+}
+
+Effect.DropOut = function(element) {
+  element = $(element);
+  var oldStyle = {
+    top: element.getStyle('top'),
+    left: element.getStyle('left'),
+    opacity: element.getInlineOpacity() };
+  return new Effect.Parallel(
+    [ new Effect.Move(element, {x: 0, y: 100, sync: true }), 
+      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
+    Object.extend(
+      { duration: 0.5,
+        beforeSetup: function(effect) {
+          effect.effects[0].element.makePositioned(); 
+        },
+        afterFinishInternal: function(effect) {
+          effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
+        } 
+      }, arguments[1] || {}));
+}
+
+Effect.Shake = function(element) {
+  element = $(element);
+  var oldStyle = {
+    top: element.getStyle('top'),
+    left: element.getStyle('left') };
+    return new Effect.Move(element, 
+      { x:  20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x: -40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x:  40, y: 0, duration: 0.1,  afterFinishInternal: function(effect) {
+    new Effect.Move(effect.element,
+      { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
+        effect.element.undoPositioned().setStyle(oldStyle);
+  }}) }}) }}) }}) }}) }});
+}
+
+Effect.SlideDown = function(element) {
+  element = $(element).cleanWhitespace();
+  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
+  var oldInnerBottom = element.down().getStyle('bottom');
+  var elementDimensions = element.getDimensions();
+  return new Effect.Scale(element, 100, Object.extend({ 
+    scaleContent: false, 
+    scaleX: false, 
+    scaleFrom: window.opera ? 0 : 1,
+    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
+    restoreAfterFinish: true,
+    afterSetup: function(effect) {
+      effect.element.makePositioned();
+      effect.element.down().makePositioned();
+      if(window.opera) effect.element.setStyle({top: ''});
+      effect.element.makeClipping().setStyle({height: '0px'}).show(); 
+    },
+    afterUpdateInternal: function(effect) {
+      effect.element.down().setStyle({bottom:
+        (effect.dims[0] - effect.element.clientHeight) + 'px' }); 
+    },
+    afterFinishInternal: function(effect) {
+      effect.element.undoClipping().undoPositioned();
+      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
+    }, arguments[1] || {})
+  );
+}
+
+Effect.SlideUp = function(element) {
+  element = $(element).cleanWhitespace();
+  var oldInnerBottom = element.down().getStyle('bottom');
+  return new Effect.Scale(element, window.opera ? 0 : 1,
+   Object.extend({ scaleContent: false, 
+    scaleX: false, 
+    scaleMode: 'box',
+    scaleFrom: 100,
+    restoreAfterFinish: true,
+    beforeStartInternal: function(effect) {
+      effect.element.makePositioned();
+      effect.element.down().makePositioned();
+      if(window.opera) effect.element.setStyle({top: ''});
+      effect.element.makeClipping().show();
+    },  
+    afterUpdateInternal: function(effect) {
+      effect.element.down().setStyle({bottom:
+        (effect.dims[0] - effect.element.clientHeight) + 'px' });
+    },
+    afterFinishInternal: function(effect) {
+      effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom});
+      effect.element.down().undoPositioned();
+    }
+   }, arguments[1] || {})
+  );
+}
+
+// Bug in opera makes the TD containing this element expand for a instance after finish 
+Effect.Squish = function(element) {
+  return new Effect.Scale(element, window.opera ? 1 : 0, { 
+    restoreAfterFinish: true,
+    beforeSetup: function(effect) {
+      effect.element.makeClipping(); 
+    },  
+    afterFinishInternal: function(effect) {
+      effect.element.hide().undoClipping(); 
+    }
+  });
+}
+
+Effect.Grow = function(element) {
+  element = $(element);
+  var options = Object.extend({
+    direction: 'center',
+    moveTransition: Effect.Transitions.sinoidal,
+    scaleTransition: Effect.Transitions.sinoidal,
+    opacityTransition: Effect.Transitions.full
+  }, arguments[1] || {});
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    height: element.style.height,
+    width: element.style.width,
+    opacity: element.getInlineOpacity() };
+
+  var dims = element.getDimensions();    
+  var initialMoveX, initialMoveY;
+  var moveX, moveY;
+  
+  switch (options.direction) {
+    case 'top-left':
+      initialMoveX = initialMoveY = moveX = moveY = 0; 
+      break;
+    case 'top-right':
+      initialMoveX = dims.width;
+      initialMoveY = moveY = 0;
+      moveX = -dims.width;
+      break;
+    case 'bottom-left':
+      initialMoveX = moveX = 0;
+      initialMoveY = dims.height;
+      moveY = -dims.height;
+      break;
+    case 'bottom-right':
+      initialMoveX = dims.width;
+      initialMoveY = dims.height;
+      moveX = -dims.width;
+      moveY = -dims.height;
+      break;
+    case 'center':
+      initialMoveX = dims.width / 2;
+      initialMoveY = dims.height / 2;
+      moveX = -dims.width / 2;
+      moveY = -dims.height / 2;
+      break;
+  }
+  
+  return new Effect.Move(element, {
+    x: initialMoveX,
+    y: initialMoveY,
+    duration: 0.01, 
+    beforeSetup: function(effect) {
+      effect.element.hide().makeClipping().makePositioned();
+    },
+    afterFinishInternal: function(effect) {
+      new Effect.Parallel(
+        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
+          new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
+          new Effect.Scale(effect.element, 100, {
+            scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, 
+            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
+        ], Object.extend({
+             beforeSetup: function(effect) {
+               effect.effects[0].element.setStyle({height: '0px'}).show(); 
+             },
+             afterFinishInternal: function(effect) {
+               effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); 
+             }
+           }, options)
+      )
+    }
+  });
+}
+
+Effect.Shrink = function(element) {
+  element = $(element);
+  var options = Object.extend({
+    direction: 'center',
+    moveTransition: Effect.Transitions.sinoidal,
+    scaleTransition: Effect.Transitions.sinoidal,
+    opacityTransition: Effect.Transitions.none
+  }, arguments[1] || {});
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    height: element.style.height,
+    width: element.style.width,
+    opacity: element.getInlineOpacity() };
+
+  var dims = element.getDimensions();
+  var moveX, moveY;
+  
+  switch (options.direction) {
+    case 'top-left':
+      moveX = moveY = 0;
+      break;
+    case 'top-right':
+      moveX = dims.width;
+      moveY = 0;
+      break;
+    case 'bottom-left':
+      moveX = 0;
+      moveY = dims.height;
+      break;
+    case 'bottom-right':
+      moveX = dims.width;
+      moveY = dims.height;
+      break;
+    case 'center':  
+      moveX = dims.width / 2;
+      moveY = dims.height / 2;
+      break;
+  }
+  
+  return new Effect.Parallel(
+    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
+      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
+      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
+    ], Object.extend({            
+         beforeStartInternal: function(effect) {
+           effect.effects[0].element.makePositioned().makeClipping(); 
+         },
+         afterFinishInternal: function(effect) {
+           effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
+       }, options)
+  );
+}
+
+Effect.Pulsate = function(element) {
+  element = $(element);
+  var options    = arguments[1] || {};
+  var oldOpacity = element.getInlineOpacity();
+  var transition = options.transition || Effect.Transitions.sinoidal;
+  var reverser   = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
+  reverser.bind(transition);
+  return new Effect.Opacity(element, 
+    Object.extend(Object.extend({  duration: 2.0, from: 0,
+      afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
+    }, options), {transition: reverser}));
+}
+
+Effect.Fold = function(element) {
+  element = $(element);
+  var oldStyle = {
+    top: element.style.top,
+    left: element.style.left,
+    width: element.style.width,
+    height: element.style.height };
+  element.makeClipping();
+  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) {
+        effect.element.hide().undoClipping().setStyle(oldStyle);
+      } });
+  }}, arguments[1] || {}));
+};
+
+Effect.Morph = Class.create();
+Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), {
+  initialize: function(element) {
+    this.element = $(element);
+    if(!this.element) throw(Effect._elementDoesNotExistError);
+    var options = Object.extend({
+      style: ''
+    }, arguments[1] || {});
+    this.start(options);
+  },
+  setup: function(){
+    function parseColor(color){
+      if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
+      color = color.parseColor();
+      return $R(0,2).map(function(i){
+        return parseInt( color.slice(i*2+1,i*2+3), 16 ) 
+      });
+    }
+    this.transforms = this.options.style.parseStyle().map(function(property){
+      var originalValue = this.element.getStyle(property[0]);
+      return $H({ 
+        style: property[0], 
+        originalValue: property[1].unit=='color' ? 
+          parseColor(originalValue) : parseFloat(originalValue || 0), 
+        targetValue: property[1].unit=='color' ? 
+          parseColor(property[1].value) : property[1].value,
+        unit: property[1].unit
+      });
+    }.bind(this)).reject(function(transform){
+      return (
+        (transform.originalValue == transform.targetValue) ||
+        (
+          transform.unit != 'color' &amp;&amp;
+          (isNaN(transform.originalValue) || isNaN(transform.targetValue))
+        )
+      )
+    });
+  },
+  update: function(position) {
+    var style = $H(), value = null;
+    this.transforms.each(function(transform){
+      value = transform.unit=='color' ?
+        $R(0,2).inject('#',function(m,v,i){
+          return m+(Math.round(transform.originalValue[i]+
+            (transform.targetValue[i] - transform.originalValue[i])*position)).toColorPart() }) : 
+        transform.originalValue + Math.round(
+          ((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit;
+      style[transform.style] = value;
+    });
+    this.element.setStyle(style);
+  }
+});
+
+Effect.Transform = Class.create();
+Object.extend(Effect.Transform.prototype, {
+  initialize: function(tracks){
+    this.tracks  = [];
+    this.options = arguments[1] || {};
+    this.addTracks(tracks);
+  },
+  addTracks: function(tracks){
+    tracks.each(function(track){
+      var data = $H(track).values().first();
+      this.tracks.push($H({
+        ids:     $H(track).keys().first(),
+        effect:  Effect.Morph,
+        options: { style: data }
+      }));
+    }.bind(this));
+    return this;
+  },
+  play: function(){
+    return new Effect.Parallel(
+      this.tracks.map(function(track){
+        var elements = [$(track.ids) || $$(track.ids)].flatten();
+        return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) });
+      }).flatten(),
+      this.options
+    );
+  }
+});
+
+Element.CSS_PROPERTIES = ['azimuth', 'backgroundAttachment', 'backgroundColor', 'backgroundImage', 
+  'backgroundPosition', 'backgroundRepeat', 'borderBottomColor', 'borderBottomStyle', 
+  'borderBottomWidth', 'borderCollapse', 'borderLeftColor', 'borderLeftStyle', 'borderLeftWidth',
+  'borderRightColor', 'borderRightStyle', 'borderRightWidth', 'borderSpacing', 'borderTopColor',
+  'borderTopStyle', 'borderTopWidth', 'bottom', 'captionSide', 'clear', 'clip', 'color', 'content',
+  'counterIncrement', 'counterReset', 'cssFloat', 'cueAfter', 'cueBefore', 'cursor', 'direction',
+  'display', 'elevation', 'emptyCells', 'fontFamily', 'fontSize', 'fontSizeAdjust', 'fontStretch',
+  'fontStyle', 'fontVariant', 'fontWeight', 'height', 'left', 'letterSpacing', 'lineHeight',
+  'listStyleImage', 'listStylePosition', 'listStyleType', 'marginBottom', 'marginLeft', 'marginRight',
+  'marginTop', 'markerOffset', 'marks', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'opacity',
+  'orphans', 'outlineColor', 'outlineOffset', 'outlineStyle', 'outlineWidth', 'overflowX', 'overflowY',
+  'paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop', 'page', 'pageBreakAfter', 'pageBreakBefore',
+  'pageBreakInside', 'pauseAfter', 'pauseBefore', 'pitch', 'pitchRange', 'position', 'quotes',
+  'richness', 'right', 'size', 'speakHeader', 'speakNumeral', 'speakPunctuation', 'speechRate', 'stress',
+  'tableLayout', 'textAlign', 'textDecoration', 'textIndent', 'textShadow', 'textTransform', 'top',
+  'unicodeBidi', 'verticalAlign', 'visibility', 'voiceFamily', 'volume', 'whiteSpace', 'widows',
+  'width', 'wordSpacing', 'zIndex'];
+  
+Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
+
+String.prototype.parseStyle = function(){
+  var element = Element.extend(document.createElement('div'));
+  element.innerHTML = '&lt;div style=&quot;' + this + '&quot;&gt;&lt;/div&gt;';
+  var style = element.down().style, styleRules = $H();
+  
+  Element.CSS_PROPERTIES.each(function(property){
+   if(style[property]) styleRules[property] = style[property]; 
+  });
+  
+  var result = $H();
+  
+  styleRules.each(function(pair){
+    var property = pair[0], value = pair[1], unit = null;
+    
+    if(value.parseColor('#zzzzzz') != '#zzzzzz') {
+      value = value.parseColor();
+      unit  = 'color';
+    } else if(Element.CSS_LENGTH.test(value)) 
+      var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/),
+          value = parseFloat(components[1]), unit = (components.length == 3) ? components[2] : null;
+    
+    result[property.underscore().dasherize()] = $H({ value:value, unit:unit });
+  }.bind(this));
+  
+  return result;
+};
+
+Element.morph = function(element, style) {
+  new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {}));
+  return element;
+};
+
+['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
+ 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each( 
+  function(f) { Element.Methods[f] = Element[f]; }
+);
+
+Element.Methods.visualEffect = function(element, effect, options) {
+  s = effect.gsub(/_/, '-').camelize();
+  effect_class = s.charAt(0).toUpperCase() + s.substring(1);
+  new Effect[effect_class](element, options);
+  return $(element);
+};
+
 Element.addMethods();
\ No newline at end of file</diff>
      <filename>public/javascripts/effects.js</filename>
    </modified>
    <modified>
      <diff>@@ -1,2426 +1,2426 @@
-/*  Prototype JavaScript framework, version 1.5.0_rc2
- *  (c) 2005-2007 Sam Stephenson
- *
- *  Prototype is freely distributable under the terms of an MIT-style license.
- *  For details, see the Prototype web site: http://prototype.conio.net/
- *
-/*--------------------------------------------------------------------------*/
-
-var Prototype = {
-  Version: '1.5.0_rc2',
-  BrowserFeatures: {
-    XPath: !!document.evaluate
-  },
-
-  ScriptFragment: '(?:&lt;script.*?&gt;)((\n|\r|.)*?)(?:&lt;\/script&gt;)',
-  emptyFunction: function() {},
-  K: function(x) { return x }
-}
-
-var Class = {
-  create: function() {
-    return function() {
-      this.initialize.apply(this, arguments);
-    }
-  }
-}
-
-var Abstract = new Object();
-
-Object.extend = function(destination, source) {
-  for (var property in source) {
-    destination[property] = source[property];
-  }
-  return destination;
-}
-
-Object.extend(Object, {
-  inspect: function(object) {
-    try {
-      if (object === undefined) return 'undefined';
-      if (object === null) return 'null';
-      return object.inspect ? object.inspect() : object.toString();
-    } catch (e) {
-      if (e instanceof RangeError) return '...';
-      throw e;
-    }
-  },
-
-  keys: function(object) {
-    var keys = [];
-    for (var property in object)
-      keys.push(property);
-    return keys;
-  },
-
-  values: function(object) {
-    var values = [];
-    for (var property in object)
-      values.push(object[property]);
-    return values;
-  },
-
-  clone: function(object) {
-    return Object.extend({}, object);
-  }
-});
-
-Function.prototype.bind = function() {
-  var __method = this, args = $A(arguments), object = args.shift();
-  return function() {
-    return __method.apply(object, args.concat($A(arguments)));
-  }
-}
-
-Function.prototype.bindAsEventListener = function(object) {
-  var __method = this, args = $A(arguments), object = args.shift();
-  return function(event) {
-    return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments)));
-  }
-}
-
-Object.extend(Number.prototype, {
-  toColorPart: function() {
-    var digits = this.toString(16);
-    if (this &lt; 16) return '0' + digits;
-    return digits;
-  },
-
-  succ: function() {
-    return this + 1;
-  },
-
-  times: function(iterator) {
-    $R(0, this, true).each(iterator);
-    return this;
-  }
-});
-
-var Try = {
-  these: function() {
-    var returnValue;
-
-    for (var i = 0, length = arguments.length; i &lt; length; i++) {
-      var lambda = arguments[i];
-      try {
-        returnValue = lambda();
-        break;
-      } catch (e) {}
-    }
-
-    return returnValue;
-  }
-}
-
-/*--------------------------------------------------------------------------*/
-
-var PeriodicalExecuter = Class.create();
-PeriodicalExecuter.prototype = {
-  initialize: function(callback, frequency) {
-    this.callback = callback;
-    this.frequency = frequency;
-    this.currentlyExecuting = false;
-
-    this.registerCallback();
-  },
-
-  registerCallback: function() {
-    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
-  },
-
-  stop: function() {
-    if (!this.timer) return;
-    clearInterval(this.timer);
-    this.timer = null;
-  },
-
-  onTimerEvent: function() {
-    if (!this.currentlyExecuting) {
-      try {
-        this.currentlyExecuting = true;
-        this.callback(this);
-      } finally {
-        this.currentlyExecuting = false;
-      }
-    }
-  }
-}
-String.interpret = function(value){
-  return value == null ? '' : String(value);
-}
-
-Object.extend(String.prototype, {
-  gsub: function(pattern, replacement) {
-    var result = '', source = this, match;
-    replacement = arguments.callee.prepareReplacement(replacement);
-
-    while (source.length &gt; 0) {
-      if (match = source.match(pattern)) {
-        result += source.slice(0, match.index);
-        result += String.interpret(replacement(match));
-        source  = source.slice(match.index + match[0].length);
-      } else {
-        result += source, source = '';
-      }
-    }
-    return result;
-  },
-
-  sub: function(pattern, replacement, count) {
-    replacement = this.gsub.prepareReplacement(replacement);
-    count = count === undefined ? 1 : count;
-
-    return this.gsub(pattern, function(match) {
-      if (--count &lt; 0) return match[0];
-      return replacement(match);
-    });
-  },
-
-  scan: function(pattern, iterator) {
-    this.gsub(pattern, iterator);
-    return this;
-  },
-
-  truncate: function(length, truncation) {
-    length = length || 30;
-    truncation = truncation === undefined ? '...' : truncation;
-    return this.length &gt; length ?
-      this.slice(0, length - truncation.length) + truncation : this;
-  },
-
-  strip: function() {
-    return this.replace(/^\s+/, '').replace(/\s+$/, '');
-  },
-
-  stripTags: function() {
-    return this.replace(/&lt;\/?[^&gt;]+&gt;/gi, '');
-  },
-
-  stripScripts: function() {
-    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
-  },
-
-  extractScripts: function() {
-    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
-    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
-    return (this.match(matchAll) || []).map(function(scriptTag) {
-      return (scriptTag.match(matchOne) || ['', ''])[1];
-    });
-  },
-
-  evalScripts: function() {
-    return this.extractScripts().map(function(script) { return eval(script) });
-  },
-
-  escapeHTML: function() {
-    var 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] ? (div.childNodes.length &gt; 1 ?
-      $A(div.childNodes).inject('',function(memo,node){ return memo+node.nodeValue }) :
-      div.childNodes[0].nodeValue) : '';
-  },
-
-  toQueryParams: function(separator) {
-    var match = this.strip().match(/([^?#]*)(#.*)?$/);
-    if (!match) return {};
-
-    return match[1].split(separator || '&amp;').inject({}, function(hash, pair) {
-      if ((pair = pair.split('='))[0]) {
-        var name = decodeURIComponent(pair[0]);
-        var value = pair[1] ? decodeURIComponent(pair[1]) : undefined;
-
-        if (hash[name] !== undefined) {
-          if (hash[name].constructor != Array)
-            hash[name] = [hash[name]];
-          if (value) hash[name].push(value);
-        }
-        else hash[name] = value;
-      }
-      return hash;
-    });
-  },
-
-  toArray: function() {
-    return this.split('');
-  },
-
-  succ: function() {
-    return this.slice(0, this.length - 1) +
-      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
-  },
-
-  camelize: function() {
-    var parts = this.split('-'), len = parts.length;
-    if (len == 1) return parts[0];
-
-    var camelized = this.charAt(0) == '-'
-      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
-      : parts[0];
-
-    for (var i = 1; i &lt; len; i++)
-      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
-
-    return camelized;
-  },
-
-  capitalize: function(){
-    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
-  },
-
-  underscore: function() {
-    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
-  },
-
-  dasherize: function() {
-    return this.gsub(/_/,'-');
-  },
-
-  inspect: function(useDoubleQuotes) {
-    var escapedString = this.replace(/\\/g, '\\\\');
-    if (useDoubleQuotes)
-      return '&quot;' + escapedString.replace(/&quot;/g, '\\&quot;') + '&quot;';
-    else
-      return &quot;'&quot; + escapedString.replace(/'/g, '\\\'') + &quot;'&quot;;
-  }
-});
-
-String.prototype.gsub.prepareReplacement = function(replacement) {
-  if (typeof replacement == 'function') return replacement;
-  var template = new Template(replacement);
-  return function(match) { return template.evaluate(match) };
-}
-
-String.prototype.parseQuery = String.prototype.toQueryParams;
-
-var Template = Class.create();
-Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
-Template.prototype = {
-  initialize: function(template, pattern) {
-    this.template = template.toString();
-    this.pattern  = pattern || Template.Pattern;
-  },
-
-  evaluate: function(object) {
-    return this.template.gsub(this.pattern, function(match) {
-      var before = match[1];
-      if (before == '\\') return match[2];
-      return before + String.interpret(object[match[3]]);
-    });
-  }
-}
-
-var $break    = new Object();
-var $continue = new Object();
-
-var Enumerable = {
-  each: function(iterator) {
-    var index = 0;
-    try {
-      this._each(function(value) {
-        try {
-          iterator(value, index++);
-        } catch (e) {
-          if (e != $continue) throw e;
-        }
-      });
-    } catch (e) {
-      if (e != $break) throw e;
-    }
-    return this;
-  },
-
-  eachSlice: function(number, iterator) {
-    var index = -number, slices = [], array = this.toArray();
-    while ((index += number) &lt; array.length)
-      slices.push(array.slice(index, index+number));
-    return slices.map(iterator);
-  },
-
-  all: function(iterator) {
-    var result = true;
-    this.each(function(value, index) {
-      result = result &amp;&amp; !!(iterator || Prototype.K)(value, index);
-      if (!result) throw $break;
-    });
-    return result;
-  },
-
-  any: function(iterator) {
-    var result = false;
-    this.each(function(value, index) {
-      if (result = !!(iterator || Prototype.K)(value, index))
-        throw $break;
-    });
-    return result;
-  },
-
-  collect: function(iterator) {
-    var results = [];
-    this.each(function(value, index) {
-      results.push((iterator || Prototype.K)(value, index));
-    });
-    return results;
-  },
-
-  detect: function(iterator) {
-    var result;
-    this.each(function(value, index) {
-      if (iterator(value, index)) {
-        result = value;
-        throw $break;
-      }
-    });
-    return result;
-  },
-
-  findAll: function(iterator) {
-    var results = [];
-    this.each(function(value, index) {
-      if (iterator(value, index))
-        results.push(value);
-    });
-    return results;
-  },
-
-  grep: function(pattern, iterator) {
-    var results = [];
-    this.each(function(value, index) {
-      var stringValue = value.toString();
-      if (stringValue.match(pattern))
-        results.push((iterator || Prototype.K)(value, index));
-    })
-    return results;
-  },
-
-  include: function(object) {
-    var found = false;
-    this.each(function(value) {
-      if (value == object) {
-        found = true;
-        throw $break;
-      }
-    });
-    return found;
-  },
-
-  inGroupsOf: function(number, fillWith) {
-    fillWith = fillWith === undefined ? null : fillWith;
-    return this.eachSlice(number, function(slice) {
-      while(slice.length &lt; number) slice.push(fillWith);
-      return slice;
-    });
-  },
-
-  inject: function(memo, iterator) {
-    this.each(function(value, index) {
-      memo = iterator(memo, value, index);
-    });
-    return memo;
-  },
-
-  invoke: function(method) {
-    var args = $A(arguments).slice(1);
-    return this.map(function(value) {
-      return value[method].apply(value, args);
-    });
-  },
-
-  max: function(iterator) {
-    var result;
-    this.each(function(value, index) {
-      value = (iterator || Prototype.K)(value, index);
-      if (result == undefined || value &gt;= result)
-        result = value;
-    });
-    return result;
-  },
-
-  min: function(iterator) {
-    var result;
-    this.each(function(value, index) {
-      value = (iterator || Prototype.K)(value, index);
-      if (result == undefined || value &lt; result)
-        result = value;
-    });
-    return result;
-  },
-
-  partition: function(iterator) {
-    var trues = [], falses = [];
-    this.each(function(value, index) {
-      ((iterator || Prototype.K)(value, index) ?
-        trues : falses).push(value);
-    });
-    return [trues, falses];
-  },
-
-  pluck: function(property) {
-    var results = [];
-    this.each(function(value, index) {
-      results.push(value[property]);
-    });
-    return results;
-  },
-
-  reject: function(iterator) {
-    var results = [];
-    this.each(function(value, index) {
-      if (!iterator(value, index))
-        results.push(value);
-    });
-    return results;
-  },
-
-  sortBy: function(iterator) {
-    return this.map(function(value, index) {
-      return {value: value, criteria: iterator(value, index)};
-    }).sort(function(left, right) {
-      var a = left.criteria, b = right.criteria;
-      return a &lt; b ? -1 : a &gt; b ? 1 : 0;
-    }).pluck('value');
-  },
-
-  toArray: function() {
-    return this.map();
-  },
-
-  zip: function() {
-    var iterator = Prototype.K, args = $A(arguments);
-    if (typeof args.last() == 'function')
-      iterator = args.pop();
-
-    var collections = [this].concat(args).map($A);
-    return this.map(function(value, index) {
-      return iterator(collections.pluck(index));
-    });
-  },
-
-  size: function() {
-    return this.toArray().length;
-  },
-
-  inspect: function() {
-    return '#&lt;Enumerable:' + this.toArray().inspect() + '&gt;';
-  }
-}
-
-Object.extend(Enumerable, {
-  map:     Enumerable.collect,
-  find:    Enumerable.detect,
-  select:  Enumerable.findAll,
-  member:  Enumerable.include,
-  entries: Enumerable.toArray
-});
-var $A = Array.from = function(iterable) {
-  if (!iterable) return [];
-  if (iterable.toArray) {
-    return iterable.toArray();
-  } else {
-    var results = [];
-    for (var i = 0, length = iterable.length; i &lt; length; i++)
-      results.push(iterable[i]);
-    return results;
-  }
-}
-
-Object.extend(Array.prototype, Enumerable);
-
-if (!Array.prototype._reverse)
-  Array.prototype._reverse = Array.prototype.reverse;
-
-Object.extend(Array.prototype, {
-  _each: function(iterator) {
-    for (var i = 0, length = this.length; i &lt; length; i++)
-      iterator(this[i]);
-  },
-
-  clear: function() {
-    this.length = 0;
-    return this;
-  },
-
-  first: function() {
-    return this[0];
-  },
-
-  last: function() {
-    return this[this.length - 1];
-  },
-
-  compact: function() {
-    return this.select(function(value) {
-      return value != null;
-    });
-  },
-
-  flatten: function() {
-    return this.inject([], function(array, value) {
-      return array.concat(value &amp;&amp; value.constructor == Array ?
-        value.flatten() : [value]);
-    });
-  },
-
-  without: function() {
-    var values = $A(arguments);
-    return this.select(function(value) {
-      return !values.include(value);
-    });
-  },
-
-  indexOf: function(object) {
-    for (var i = 0, length = this.length; i &lt; length; i++)
-      if (this[i] == object) return i;
-    return -1;
-  },
-
-  reverse: function(inline) {
-    return (inline !== false ? this : this.toArray())._reverse();
-  },
-
-  reduce: function() {
-    return this.length &gt; 1 ? this : this[0];
-  },
-
-  uniq: function() {
-    return this.inject([], function(array, value) {
-      return array.include(value) ? array : array.concat([value]);
-    });
-  },
-
-  clone: function() {
-    return [].concat(this);
-  },
-
-  size: function() {
-    return this.length;
-  },
-
-  inspect: function() {
-    return '[' + this.map(Object.inspect).join(', ') + ']';
-  }
-});
-
-Array.prototype.toArray = Array.prototype.clone;
-
-function $w(string){
-  string = string.strip();
-  return string ? string.split(/\s+/) : [];
-}
-
-if(window.opera){
-  Array.prototype.concat = function(){
-    var array = [];
-    for(var i = 0, length = this.length; i &lt; length; i++) array.push(this[i]);
-    for(var i = 0, length = arguments.length; i &lt; length; i++) {
-      if(arguments[i].constructor == Array) {
-        for(var j = 0, arrayLength = arguments[i].length; j &lt; arrayLength; j++)
-          array.push(arguments[i][j]);
-      } else {
-        array.push(arguments[i]);
-      }
-    }
-    return array;
-  }
-}
-var Hash = {
-  _each: function(iterator) {
-    for (var key in this) {
-      var value = this[key];
-      if (typeof value == 'function') continue;
-
-      var pair = [key, value];
-      pair.key = key;
-      pair.value = value;
-      iterator(pair);
-    }
-  },
-
-  keys: function() {
-    return this.pluck('key');
-  },
-
-  values: function() {
-    return this.pluck('value');
-  },
-
-  merge: function(hash) {
-    return $H(hash).inject(this, function(mergedHash, pair) {
-      mergedHash[pair.key] = pair.value;
-      return mergedHash;
-    });
-  },
-
-  toQueryString: function() {
-    return this.map(function(pair) {
-      if (!pair.key) return null;
-
-      if (pair.value &amp;&amp; pair.value.constructor == Array) {
-        pair.value = pair.value.compact();
-
-        if (pair.value.length &lt; 2) {
-          pair.value = pair.value.reduce();
-        } else {
-          var key = encodeURIComponent(pair.key);
-          return pair.value.map(function(value) {
-            return key + '=' + encodeURIComponent(value);
-		  	  }).join('&amp;');
-        }
-      }
-
-      if (pair.value == undefined) pair[1] = '';
-      return pair.map(encodeURIComponent).join('=');
-    }).join('&amp;');
-  },
-
-  inspect: function() {
-    return '#&lt;Hash:{' + this.map(function(pair) {
-      return pair.map(Object.inspect).join(': ');
-    }).join(', ') + '}&gt;';
-  }
-}
-
-function $H(object) {
-  var hash = Object.extend({}, object || {});
-  Object.extend(hash, Enumerable);
-  Object.extend(hash, Hash);
-  return hash;
-}
-ObjectRange = Class.create();
-Object.extend(ObjectRange.prototype, Enumerable);
-Object.extend(ObjectRange.prototype, {
-  initialize: function(start, end, exclusive) {
-    this.start = start;
-    this.end = end;
-    this.exclusive = exclusive;
-  },
-
-  _each: function(iterator) {
-    var value = this.start;
-    while (this.include(value)) {
-      iterator(value);
-      value = value.succ();
-    }
-  },
-
-  include: function(value) {
-    if (value &lt; this.start)
-      return false;
-    if (this.exclusive)
-      return value &lt; this.end;
-    return value &lt;= this.end;
-  }
-});
-
-var $R = function(start, end, exclusive) {
-  return new ObjectRange(start, end, exclusive);
-}
-
-var Ajax = {
-  getTransport: function() {
-    return Try.these(
-      function() {return new XMLHttpRequest()},
-      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
-      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
-    ) || false;
-  },
-
-  activeRequestCount: 0
-}
-
-Ajax.Responders = {
-  responders: [],
-
-  _each: function(iterator) {
-    this.responders._each(iterator);
-  },
-
-  register: function(responder) {
-    if (!this.include(responder))
-      this.responders.push(responder);
-  },
-
-  unregister: function(responder) {
-    this.responders = this.responders.without(responder);
-  },
-
-  dispatch: function(callback, request, transport, json) {
-    this.each(function(responder) {
-      if (typeof responder[callback] == 'function') {
-        try {
-          responder[callback].apply(responder, [request, transport, json]);
-        } catch (e) {}
-      }
-    });
-  }
-};
-
-Object.extend(Ajax.Responders, Enumerable);
-
-Ajax.Responders.register({
-  onCreate: function() {
-    Ajax.activeRequestCount++;
-  },
-  onComplete: function() {
-    Ajax.activeRequestCount--;
-  }
-});
-
-Ajax.Base = function() {};
-Ajax.Base.prototype = {
-  setOptions: function(options) {
-    this.options = {
-      method:       'post',
-      asynchronous: true,
-      contentType:  'application/x-www-form-urlencoded',
-      encoding:     'UTF-8',
-      parameters:   ''
-    }
-    Object.extend(this.options, options || {});
-
-    this.options.method = this.options.method.toLowerCase();
-    this.options.parameters = $H(typeof this.options.parameters == 'string' ?
-      this.options.parameters.toQueryParams() : this.options.parameters);
-  }
-}
-
-Ajax.Request = Class.create();
-Ajax.Request.Events =
-  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
-
-Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
-  _complete: false,
-
-  initialize: function(url, options) {
-    this.transport = Ajax.getTransport();
-    this.setOptions(options);
-    this.request(url);
-  },
-
-  request: function(url) {
-    var params = this.options.parameters;
-    if (params.any()) params['_'] = '';
-
-    if (!['get', 'post'].include(this.options.method)) {
-      // simulate other verbs over post
-      params['_method'] = this.options.method;
-      this.options.method = 'post';
-    }
-
-    this.url = url;
-
-    // when GET, append parameters to URL
-    if (this.options.method == 'get' &amp;&amp; params.any())
-      this.url += (this.url.indexOf('?') &gt;= 0 ? '&amp;' : '?') +
-        params.toQueryString();
-
-    try {
-      Ajax.Responders.dispatch('onCreate', this, this.transport);
-
-      this.transport.open(this.options.method.toUpperCase(), this.url,
-        this.options.asynchronous);
-
-      if (this.options.asynchronous)
-        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
-
-      this.transport.onreadystatechange = this.onStateChange.bind(this);
-      this.setRequestHeaders();
-
-      var body = this.options.method == 'post' ?
-        (this.options.postBody || params.toQueryString()) : null;
-
-      this.transport.send(body);
-
-      /* Force Firefox to handle ready state 4 for synchronous requests */
-      if (!this.options.asynchronous &amp;&amp; this.transport.overrideMimeType)
-        this.onStateChange();
-
-    }
-    catch (e) {
-      this.dispatchException(e);
-    }
-  },
-
-  onStateChange: function() {
-    var readyState = this.transport.readyState;
-    if (readyState &gt; 1 &amp;&amp; !((readyState == 4) &amp;&amp; this._complete))
-      this.respondToReadyState(this.transport.readyState);
-  },
-
-  setRequestHeaders: function() {
-    var headers = {
-      'X-Requested-With': 'XMLHttpRequest',
-      'X-Prototype-Version': Prototype.Version,
-      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
-    };
-
-    if (this.options.method == 'post') {
-      headers['Content-type'] = this.options.contentType +
-        (this.options.encoding ? '; charset=' + this.options.encoding : '');
-
-      /* Force &quot;Connection: close&quot; for older Mozilla browsers to work
-       * around a bug where XMLHttpRequest sends an incorrect
-       * Content-length header. See Mozilla Bugzilla #246651.
-       */
-      if (this.transport.overrideMimeType &amp;&amp;
-          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] &lt; 2005)
-            headers['Connection'] = 'close';
-    }
-
-    // user-defined headers
-    if (typeof this.options.requestHeaders == 'object') {
-      var extras = this.options.requestHeaders;
-
-      if (typeof extras.push == 'function')
-        for (var i = 0, length = extras.length; i &lt; length; i += 2)
-          headers[extras[i]] = extras[i+1];
-      else
-        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
-    }
-
-    for (var name in headers)
-      this.transport.setRequestHeader(name, headers[name]);
-  },
-
-  success: function() {
-    return !this.transport.status
-        || (this.transport.status &gt;= 200 &amp;&amp; this.transport.status &lt; 300);
-  },
-
-  respondToReadyState: function(readyState) {
-    var state = Ajax.Request.Events[readyState];
-    var transport = this.transport, json = this.evalJSON();
-
-    if (state == 'Complete') {
-      try {
-        this._complete = true;
-        (this.options['on' + this.transport.status]
-         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
-         || Prototype.emptyFunction)(transport, json);
-      } catch (e) {
-        this.dispatchException(e);
-      }
-
-      if ((this.getHeader('Content-type') || 'text/javascript').strip().
-        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
-          this.evalResponse();
-    }
-
-    try {
-      (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
-      Ajax.Responders.dispatch('on' + state, this, transport, json);
-    } catch (e) {
-      this.dispatchException(e);
-    }
-
-    if (state == 'Complete') {
-      // avoid memory leak in MSIE: clean up
-      this.transport.onreadystatechange = Prototype.emptyFunction;
-    }
-  },
-
-  getHeader: function(name) {
-    try {
-      return this.transport.getResponseHeader(name);
-    } catch (e) { return null }
-  },
-
-  evalJSON: function() {
-    try {
-      var json = this.getHeader('X-JSON');
-      return json ? eval('(' + json + ')') : null;
-    } catch (e) { return null }
-  },
-
-  evalResponse: function() {
-    try {
-      return eval(this.transport.responseText);
-    } catch (e) {
-      this.dispatchException(e);
-    }
-  },
-
-  dispatchException: function(exception) {
-    (this.options.onException || Prototype.emptyFunction)(this, exception);
-    Ajax.Responders.dispatch('onException', this, exception);
-  }
-});
-
-Ajax.Updater = Class.create();
-
-Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
-  initialize: function(container, url, options) {
-    this.container = {
-      success: (container.success || container),
-      failure: (container.failure || (container.success ? null : container))
-    }
-
-    this.transport = Ajax.getTransport();
-    this.setOptions(options);
-
-    var onComplete = this.options.onComplete || Prototype.emptyFunction;
-    this.options.onComplete = (function(transport, param) {
-      this.updateContent();
-      onComplete(transport, param);
-    }).bind(this);
-
-    this.request(url);
-  },
-
-  updateContent: function() {
-    var receiver = this.container[this.success() ? 'success' : 'failure'];
-    var response = this.transport.responseText;
-
-    if (!this.options.evalScripts) response = response.stripScripts();
-
-    if (receiver = $(receiver)) {
-      if (this.options.insertion)
-        new this.options.insertion(receiver, response);
-      else
-        receiver.update(response);
-    }
-
-    if (this.success()) {
-      if (this.onComplete)
-        setTimeout(this.onComplete.bind(this), 10);
-    }
-  }
-});
-
-Ajax.PeriodicalUpdater = Class.create();
-Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
-  initialize: function(container, url, options) {
-    this.setOptions(options);
-    this.onComplete = this.options.onComplete;
-
-    this.frequency = (this.options.frequency || 2);
-    this.decay = (this.options.decay || 1);
-
-    this.updater = {};
-    this.container = container;
-    this.url = url;
-
-    this.start();
-  },
-
-  start: function() {
-    this.options.onComplete = this.updateComplete.bind(this);
-    this.onTimerEvent();
-  },
-
-  stop: function() {
-    this.updater.options.onComplete = undefined;
-    clearTimeout(this.timer);
-    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
-  },
-
-  updateComplete: function(request) {
-    if (this.options.decay) {
-      this.decay = (request.responseText == this.lastText ?
-        this.decay * this.options.decay : 1);
-
-      this.lastText = request.responseText;
-    }
-    this.timer = setTimeout(this.onTimerEvent.bind(this),
-      this.decay * this.frequency * 1000);
-  },
-
-  onTimerEvent: function() {
-    this.updater = new Ajax.Updater(this.container, this.url, this.options);
-  }
-});
-function $(element) {
-  if (arguments.length &gt; 1) {
-    for (var i = 0, elements = [], length = arguments.length; i &lt; length; i++)
-      elements.push($(arguments[i]));
-    return elements;
-  }
-  if (typeof element == 'string')
-    element = document.getElementById(element);
-  return Element.extend(element);
-}
-
-if (Prototype.BrowserFeatures.XPath) {
-  document._getElementsByXPath = function(expression, parentElement) {
-    var results = [];
-    var query = document.evaluate(expression, $(parentElement) || document,
-      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
-    for (var i = 0, length = query.snapshotLength; i &lt; length; i++)
-      results.push(query.snapshotItem(i));
-    return results;
-  }
-}
-
-document.getElementsByClassName = function(className, parentElement) {
-  if (Prototype.BrowserFeatures.XPath) {
-    var q = &quot;.//*[contains(concat(' ', @class, ' '), ' &quot; + className + &quot; ')]&quot;;
-    return document._getElementsByXPath(q, parentElement);
-  } else {
-    var children = ($(parentElement) || document.body).getElementsByTagName('*');
-    var elements = [], child;
-    for (var i = 0, length = children.length; i &lt; length; i++) {
-      child = children[i];
-      if (Element.hasClassName(child, className))
-        elements.push(Element.extend(child));
-    }
-    return elements;
-  }
-}
-
-/*--------------------------------------------------------------------------*/
-
-if (!window.Element)
-  var Element = new Object();
-
-Element.extend = function(element) {
-  if (!element || _nativeExtensions || element.nodeType == 3) return element;
-
-  if (!element._extended &amp;&amp; element.tagName &amp;&amp; element != window) {
-    var methods = Object.clone(Element.Methods), cache = Element.extend.cache;
-
-    if (element.tagName == 'FORM')
-      Object.extend(methods, Form.Methods);
-    if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName))
-      Object.extend(methods, Form.Element.Methods);
-
-    Object.extend(methods, Element.Methods.Simulated);
-
-    for (var property in methods) {
-      var value = methods[property];
-      if (typeof value == 'function' &amp;&amp; !(property in element))
-        element[property] = cache.findOrStore(value);
-    }
-  }
-
-  element._extended = true;
-  return element;
-}
-
-Element.extend.cache = {
-  findOrStore: function(value) {
-    return this[value] = this[value] || function() {
-      return value.apply(null, [this].concat($A(arguments)));
-    }
-  }
-}
-
-Element.Methods = {
-  visible: function(element) {
-    return $(element).style.display != 'none';
-  },
-
-  toggle: function(element) {
-    element = $(element);
-    Element[Element.visible(element) ? 'hide' : 'show'](element);
-    return element;
-  },
-
-  hide: function(element) {
-    $(element).style.display = 'none';
-    return element;
-  },
-
-  show: function(element) {
-    $(element).style.display = '';
-    return element;
-  },
-
-  remove: function(element) {
-    element = $(element);
-    element.parentNode.removeChild(element);
-    return element;
-  },
-
-  update: function(element, html) {
-    html = typeof html == 'undefined' ? '' : html.toString();
-    $(element).innerHTML = html.stripScripts();
-    setTimeout(function() {html.evalScripts()}, 10);
-    return element;
-  },
-
-  replace: function(element, html) {
-    element = $(element);
-    if (element.outerHTML) {
-      element.outerHTML = html.stripScripts();
-    } else {
-      var range = element.ownerDocument.createRange();
-      range.selectNodeContents(element);
-      element.parentNode.replaceChild(
-        range.createContextualFragment(html.stripScripts()), element);
-    }
-    setTimeout(function() {html.evalScripts()}, 10);
-    return element;
-  },
-
-  inspect: function(element) {
-    element = $(element);
-    var result = '&lt;' + element.tagName.toLowerCase();
-    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
-      var property = pair.first(), attribute = pair.last();
-      var value = (element[property] || '').toString();
-      if (value) result += ' ' + attribute + '=' + value.inspect(true);
-    });
-    return result + '&gt;';
-  },
-
-  recursivelyCollect: function(element, property) {
-    element = $(element);
-    var elements = [];
-    while (element = element[property])
-      if (element.nodeType == 1)
-        elements.push(Element.extend(element));
-    return elements;
-  },
-
-  ancestors: function(element) {
-    return $(element).recursivelyCollect('parentNode');
-  },
-
-  descendants: function(element) {
-    return $A($(element).getElementsByTagName('*'));
-  },
-
-  immediateDescendants: function(element) {
-    if (!(element = $(element).firstChild)) return [];
-    while (element &amp;&amp; element.nodeType != 1) element = element.nextSibling;
-    if (element) return [element].concat($(element).nextSiblings());
-    return [];
-  },
-
-  previousSiblings: function(element) {
-    return $(element).recursivelyCollect('previousSibling');
-  },
-
-  nextSiblings: function(element) {
-    return $(element).recursivelyCollect('nextSibling');
-  },
-
-  siblings: function(element) {
-    element = $(element);
-    return element.previousSiblings().reverse().concat(element.nextSiblings());
-  },
-
-  match: function(element, selector) {
-    if (typeof selector == 'string')
-      selector = new Selector(selector);
-    return selector.match($(element));
-  },
-
-  up: function(element, expression, index) {
-    return Selector.findElement($(element).ancestors(), expression, index);
-  },
-
-  down: function(element, expression, index) {
-    return Selector.findElement($(element).descendants(), expression, index);
-  },
-
-  previous: function(element, expression, index) {
-    return Selector.findElement($(element).previousSiblings(), expression, index);
-  },
-
-  next: function(element, expression, index) {
-    return Selector.findElement($(element).nextSiblings(), expression, index);
-  },
-
-  getElementsBySelector: function() {
-    var args = $A(arguments), element = $(args.shift());
-    return Selector.findChildElements(element, args);
-  },
-
-  getElementsByClassName: function(element, className) {
-    return document.getElementsByClassName(className, element);
-  },
-
-  readAttribute: function(element, name) {
-    return $(element).getAttribute(name);
-  },
-
-  getHeight: function(element) {
-    return $(element).offsetHeight;
-  },
-
-  classNames: function(element) {
-    return new Element.ClassNames(element);
-  },
-
-  hasClassName: function(element, className) {
-    if (!(element = $(element))) return;
-    var elementClassName = element.className;
-    if (elementClassName.length == 0) return false;
-    if (elementClassName == className ||
-        elementClassName.match(new RegExp(&quot;(^|\\s)&quot; + className + &quot;(\\s|$)&quot;)))
-      return true;
-    return false;
-  },
-
-  addClassName: function(element, className) {
-    if (!(element = $(element))) return;
-    Element.classNames(element).add(className);
-    return element;
-  },
-
-  removeClassName: function(element, className) {
-    if (!(element = $(element))) return;
-    Element.classNames(element).remove(className);
-    return element;
-  },
-
-  toggleClassName: function(element, className) {
-    if (!(element = $(element))) return;
-    Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className);
-    return element;
-  },
-
-  observe: function() {
-    Event.observe.apply(Event, arguments);
-    return $A(arguments).first();
-  },
-
-  stopObserving: function() {
-    Event.stopObserving.apply(Event, arguments);
-    return $A(arguments).first();
-  },
-
-  // removes whitespace-only text node children
-  cleanWhitespace: function(element) {
-    element = $(element);
-    var node = element.firstChild;
-    while (node) {
-      var nextNode = node.nextSibling;
-      if (node.nodeType == 3 &amp;&amp; !/\S/.test(node.nodeValue))
-        element.removeChild(node);
-      node = nextNode;
-    }
-    return element;
-  },
-
-  empty: function(element) {
-    return $(element).innerHTML.match(/^\s*$/);
-  },
-
-  childOf: function(element, ancestor) {
-    element = $(element), ancestor = $(ancestor);
-    while (element = element.parentNode)
-      if (element == ancestor) return true;
-    return false;
-  },
-
-  scrollTo: function(element) {
-    element = $(element);
-    var pos = Position.cumulativeOffset(element);
-    window.scrollTo(pos[0], pos[1]);
-    return element;
-  },
-
-  getStyle: function(element, style) {
-    element = $(element);
-    var camelizedStyle = (style == 'float' ?
-      (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat') : style).camelize();
-    var value = element.style[camelizedStyle];
-    if (!value) {
-      if (document.defaultView &amp;&amp; document.defaultView.getComputedStyle) {
-        var css = document.defaultView.getComputedStyle(element, null);
-        value = css ? css[camelizedStyle] : null;
-      } else if (element.currentStyle) {
-        value = element.currentStyle[camelizedStyle];
-      }
-    }
-
-    if((value == 'auto') &amp;&amp; ['width','height'].include(style) &amp;&amp; (element.getStyle('display') != 'none'))
-      value = element['offset'+style.capitalize()] + 'px';
-
-    if (window.opera &amp;&amp; ['left', 'top', 'right', 'bottom'].include(style))
-      if (Element.getStyle(element, 'position') == 'static') value = 'auto';
-    if(style == 'opacity') {
-      if(value) return parseFloat(value);
-      if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
-        if(value[1]) return parseFloat(value[1]) / 100;
-      return 1.0;
-    }
-    return value == 'auto' ? null : value;
-  },
-
-  setStyle: function(element, style) {
-    element = $(element);
-    for (var name in style) {
-      var value = style[name];
-      if(name == 'opacity') {
-        if (value == 1) {
-          value = (/Gecko/.test(navigator.userAgent) &amp;&amp;
-            !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0;
-          if(/MSIE/.test(navigator.userAgent) &amp;&amp; !window.opera)
-            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
-        } else {
-          if(value &lt; 0.00001) value = 0;
-          if(/MSIE/.test(navigator.userAgent) &amp;&amp; !window.opera)
-            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
-              'alpha(opacity='+value*100+')';
-        }
-      } else if(name == 'float') name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat';
-      element.style[name.camelize()] = value;
-    }
-    return element;
-  },
-
-  getDimensions: function(element) {
-    element = $(element);
-    if (Element.getStyle(element, 'display') != 'none')
-      return {width: element.offsetWidth, height: element.offsetHeight};
-
-    // All *Width and *Height properties give 0 on elements with display none,
-    // so enable the element temporarily
-    var els = element.style;
-    var originalVisibility = els.visibility;
-    var originalPosition = els.position;
-    els.visibility = 'hidden';
-    els.position = 'absolute';
-    els.display = '';
-    var originalWidth = element.clientWidth;
-    var originalHeight = element.clientHeight;
-    els.display = 'none';
-    els.position = originalPosition;
-    els.visibility = originalVisibility;
-    return {width: originalWidth, height: originalHeight};
-  },
-
-  makePositioned: function(element) {
-    element = $(element);
-    var pos = Element.getStyle(element, 'position');
-    if (pos == 'static' || !pos) {
-      element._madePositioned = true;
-      element.style.position = 'relative';
-      // Opera returns the offset relative to the positioning context, when an
-      // element is position relative but top and left have not been defined
-      if (window.opera) {
-        element.style.top = 0;
-        element.style.left = 0;
-      }
-    }
-    return element;
-  },
-
-  undoPositioned: function(element) {
-    element = $(element);
-    if (element._madePositioned) {
-      element._madePositioned = undefined;
-      element.style.position =
-        element.style.top =
-        element.style.left =
-        element.style.bottom =
-        element.style.right = '';
-    }
-    return element;
-  },
-
-  makeClipping: function(element) {
-    element = $(element);
-    if (element._overflow) return element;
-    element._overflow = element.style.overflow || 'auto';
-    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
-      element.style.overflow = 'hidden';
-    return element;
-  },
-
-  undoClipping: function(element) {
-    element = $(element);
-    if (!element._overflow) return element;
-    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
-    element._overflow = null;
-    return element;
-  }
-}
-
-Element.Methods.Simulated = {
-  hasAttribute: function(element, attribute) {
-    return $(element).getAttributeNode(attribute).specified;
-  }
-}
-
-// IE is missing .innerHTML support for TABLE-related elements
-if(document.all){
-  Element.Methods.update = function(element, html) {
-    element = $(element);
-    html = typeof html == 'undefined' ? '' : html.toString();
-    var tagName = element.tagName.toUpperCase();
-    if (['THEAD','TBODY','TR','TD'].include(tagName)) {
-      var div = document.createElement('div');
-      switch (tagName) {
-        case 'THEAD':
-        case 'TBODY':
-          div.innerHTML = '&lt;table&gt;&lt;tbody&gt;' +  html.stripScripts() + '&lt;/tbody&gt;&lt;/table&gt;';
-          depth = 2;
-          break;
-        case 'TR':
-          div.innerHTML = '&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;' +  html.stripScripts() + '&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;';
-          depth = 3;
-          break;
-        case 'TD':
-          div.innerHTML = '&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;' +  html.stripScripts() + '&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;';
-          depth = 4;
-      }
-      $A(element.childNodes).each(function(node){
-        element.removeChild(node)
-      });
-      depth.times(function(){ div = div.firstChild });
-
-      $A(div.childNodes).each(
-        function(node){ element.appendChild(node) });
-    } else {
-      element.innerHTML = html.stripScripts();
-    }
-    setTimeout(function() {html.evalScripts()}, 10);
-    return element;
-  }
-}
-
-Object.extend(Element, Element.Methods);
-
-var _nativeExtensions = false;
-
-if(/Konqueror|Safari|KHTML/.test(navigator.userAgent))
-  ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) {
-    var className = 'HTML' + tag + 'Element';
-    if(window[className]) return;
-    var klass = window[className] = {};
-    klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__;
-  });
-
-Element.addMethods = function(methods) {
-  Object.extend(Element.Methods, methods || {});
-
-  function copy(methods, destination, onlyIfAbsent) {
-    onlyIfAbsent = onlyIfAbsent || false;
-    var cache = Element.extend.cache;
-    for (var property in methods) {
-      var value = methods[property];
-      if (!onlyIfAbsent || !(property in destination))
-        destination[property] = cache.findOrStore(value);
-    }
-  }
-
-  if (typeof HTMLElement != 'undefined') {
-    copy(Element.Methods, HTMLElement.prototype);
-    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
-    copy(Form.Methods, HTMLFormElement.prototype);
-    [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) {
-      copy(Form.Element.Methods, klass.prototype);
-    });
-    _nativeExtensions = true;
-  }
-}
-
-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.stripScripts();
-
-    if (this.adjacency &amp;&amp; this.element.insertAdjacentHTML) {
-      try {
-        this.element.insertAdjacentHTML(this.adjacency, this.content);
-      } catch (e) {
-        var tagName = this.element.tagName.toUpperCase();
-        if (['TBODY', 'TR'].include(tagName)) {
-          this.insertContent(this.contentFromAnonymousTable());
-        } else {
-          throw e;
-        }
-      }
-    } else {
-      this.range = this.element.ownerDocument.createRange();
-      if (this.initializeRange) this.initializeRange();
-      this.insertContent([this.range.createContextualFragment(this.content)]);
-    }
-
-    setTimeout(function() {content.evalScripts()}, 10);
-  },
-
-  contentFromAnonymousTable: function() {
-    var div = document.createElement('div');
-    div.innerHTML = '&lt;table&gt;&lt;tbody&gt;' + this.content + '&lt;/tbody&gt;&lt;/table&gt;';
-    return $A(div.childNodes[0].childNodes[0].childNodes);
-  }
-}
-
-var Insertion = new Object();
-
-Insertion.Before = Class.create();
-Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
-  initializeRange: function() {
-    this.range.setStartBefore(this.element);
-  },
-
-  insertContent: function(fragments) {
-    fragments.each((function(fragment) {
-      this.element.parentNode.insertBefore(fragment, this.element);
-    }).bind(this));
-  }
-});
-
-Insertion.Top = Class.create();
-Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
-  initializeRange: function() {
-    this.range.selectNodeContents(this.element);
-    this.range.collapse(true);
-  },
-
-  insertContent: function(fragments) {
-    fragments.reverse(false).each((function(fragment) {
-      this.element.insertBefore(fragment, this.element.firstChild);
-    }).bind(this));
-  }
-});
-
-Insertion.Bottom = Class.create();
-Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
-  initializeRange: function() {
-    this.range.selectNodeContents(this.element);
-    this.range.collapse(this.element);
-  },
-
-  insertContent: function(fragments) {
-    fragments.each((function(fragment) {
-      this.element.appendChild(fragment);
-    }).bind(this));
-  }
-});
-
-Insertion.After = Class.create();
-Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
-  initializeRange: function() {
-    this.range.setStartAfter(this.element);
-  },
-
-  insertContent: function(fragments) {
-    fragments.each((function(fragment) {
-      this.element.parentNode.insertBefore(fragment,
-        this.element.nextSibling);
-    }).bind(this));
-  }
-});
-
-/*--------------------------------------------------------------------------*/
-
-Element.ClassNames = Class.create();
-Element.ClassNames.prototype = {
-  initialize: function(element) {
-    this.element = $(element);
-  },
-
-  _each: function(iterator) {
-    this.element.className.split(/\s+/).select(function(name) {
-      return name.length &gt; 0;
-    })._each(iterator);
-  },
-
-  set: function(className) {
-    this.element.className = className;
-  },
-
-  add: function(classNameToAdd) {
-    if (this.include(classNameToAdd)) return;
-    this.set($A(this).concat(classNameToAdd).join(' '));
-  },
-
-  remove: function(classNameToRemove) {
-    if (!this.include(classNameToRemove)) return;
-    this.set($A(this).without(classNameToRemove).join(' '));
-  },
-
-  toString: function() {
-    return $A(this).join(' ');
-  }
-}
-
-Object.extend(Element.ClassNames.prototype, Enumerable);
-var Selector = Class.create();
-Selector.prototype = {
-  initialize: function(expression) {
-    this.params = {classNames: []};
-    this.expression = expression.toString().strip();
-    this.parseExpression();
-    this.compileMatcher();
-  },
-
-  parseExpression: function() {
-    function abort(message) { throw 'Parse error in selector: ' + message; }
-
-    if (this.expression == '')  abort('empty expression');
-
-    var params = this.params, expr = this.expression, match, modifier, clause, rest;
-    while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:&quot;([^&quot;]*)&quot;|([^\]\s]*)))?\]$/i)) {
-      params.attributes = params.attributes || [];
-      params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
-      expr = match[1];
-    }
-
-    if (expr == '*') return this.params.wildcard = true;
-
-    while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
-      modifier = match[1], clause = match[2], rest = match[3];
-      switch (modifier) {
-        case '#':       params.id = clause; break;
-        case '.':       params.classNames.push(clause); break;
-        case '':
-        case undefined: params.tagName = clause.toUpperCase(); break;
-        default:        abort(expr.inspect());
-      }
-      expr = rest;
-    }
-
-    if (expr.length &gt; 0) abort(expr.inspect());
-  },
-
-  buildMatchExpression: function() {
-    var params = this.params, conditions = [], clause;
-
-    if (params.wildcard)
-      conditions.push('true');
-    if (clause = params.id)
-      conditions.push('element.getAttribute(&quot;id&quot;) == ' + clause.inspect());
-    if (clause = params.tagName)
-      conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
-    if ((clause = params.classNames).length &gt; 0)
-      for (var i = 0, length = clause.length; i &lt; length; i++)
-        conditions.push('Element.hasClassName(element, ' + clause[i].inspect() + ')');
-    if (clause = params.attributes) {
-      clause.each(function(attribute) {
-        var value = 'element.getAttribute(' + attribute.name.inspect() + ')';
-        var splitValueBy = function(delimiter) {
-          return value + ' &amp;&amp; ' + value + '.split(' + delimiter.inspect() + ')';
-        }
-
-        switch (attribute.operator) {
-          case '=':       conditions.push(value + ' == ' + attribute.value.inspect()); break;
-          case '~=':      conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
-          case '|=':      conditions.push(
-                            splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
-                          ); break;
-          case '!=':      conditions.push(value + ' != ' + attribute.value.inspect()); break;
-          case '':
-          case undefined: conditions.push(value + ' != null'); break;
-          default:        throw 'Unknown operator ' + attribute.operator + ' in selector';
-        }
-      });
-    }
-
-    return conditions.join(' &amp;&amp; ');
-  },
-
-  compileMatcher: function() {
-    this.match = new Function('element', 'if (!element.tagName) return false; \
-      return ' + this.buildMatchExpression());
-  },
-
-  findElements: function(scope) {
-    var element;
-
-    if (element = $(this.params.id))
-      if (this.match(element))
-        if (!scope || Element.childOf(element, scope))
-          return [element];
-
-    scope = (scope || document).getElementsByTagName(this.params.tagName || '*');
-
-    var results = [];
-    for (var i = 0, length = scope.length; i &lt; length; i++)
-      if (this.match(element = scope[i]))
-        results.push(Element.extend(element));
-
-    return results;
-  },
-
-  toString: function() {
-    return this.expression;
-  }
-}
-
-Object.extend(Selector, {
-  matchElements: function(elements, expression) {
-    var selector = new Selector(expression);
-    return elements.select(selector.match.bind(selector)).map(Element.extend);
-  },
-
-  findElement: function(elements, expression, index) {
-    if (typeof expression == 'number') index = expression, expression = false;
-    return Selector.matchElements(elements, expression || '*')[index || 0];
-  },
-
-  findChildElements: function(element, expressions) {
-    return expressions.map(function(expression) {
-      return expression.strip().split(/\s+/).inject([null], function(results, expr) {
-        var selector = new Selector(expr);
-        return results.inject([], function(elements, result) {
-          return elements.concat(selector.findElements(result || element));
-        });
-      });
-    }).flatten();
-  }
-});
-
-function $$() {
-  return Selector.findChildElements(document, $A(arguments));
-}
-var Form = {
-  reset: function(form) {
-    $(form).reset();
-    return form;
-  },
-
-  serializeElements: function(elements) {
-    return elements.inject([], function(queryComponents, element) {
-      var queryComponent = Form.Element.serialize(element);
-      if (queryComponent) queryComponents.push(queryComponent);
-      return queryComponents;
-    }).join('&amp;');
-  }
-};
-
-Form.Methods = {
-  serialize: function(form) {
-    return Form.serializeElements(Form.getElements(form));
-  },
-
-  getElements: function(form) {
-    return $A($(form).getElementsByTagName('*')).inject([],
-      function(elements, child) {
-        if (Form.Element.Serializers[child.tagName.toLowerCase()])
-          elements.push(Element.extend(child));
-        return elements;
-      }
-    );
-  },
-
-  getInputs: function(form, typeName, name) {
-    form = $(form);
-    var inputs = form.getElementsByTagName('input'), matchingInputs = [];
-
-    if (!typeName &amp;&amp; !name)
-      return $A(inputs).map(Element.extend);
-
-    for (var i = 0, length = inputs.length; i &lt; length; i++) {
-      var input = inputs[i];
-      if ((typeName &amp;&amp; input.type != typeName) ||
-          (name &amp;&amp; input.name != name))
-        continue;
-      matchingInputs.push(Element.extend(input));
-    }
-
-    return matchingInputs;
-  },
-
-  disable: function(form) {
-    form = $(form);
-    form.getElements().each(function(element) {
-      element.blur();
-      element.disabled = 'true';
-    });
-    return form;
-  },
-
-  enable: function(form) {
-    form = $(form);
-    form.getElements().each(function(element) {
-      element.disabled = '';
-    });
-    return form;
-  },
-
-  findFirstElement: function(form) {
-    return $(form).getElements().find(function(element) {
-      return element.type != 'hidden' &amp;&amp; !element.disabled &amp;&amp;
-        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
-    });
-  },
-
-  focusFirstElement: function(form) {
-    form = $(form);
-    form.findFirstElement().activate();
-    return form;
-  }
-}
-
-Object.extend(Form, Form.Methods);
-
-/*--------------------------------------------------------------------------*/
-
-Form.Element = {
-  focus: function(element) {
-    $(element).focus();
-    return element;
-  },
-
-  select: function(element) {
-    $(element).select();
-    return element;
-  }
-}
-
-Form.Element.Methods = {
-  serialize: function(element) {
-    element = $(element);
-    if (element.disabled) return '';
-    var method = element.tagName.toLowerCase();
-    var parameter = Form.Element.Serializers[method](element);
-
-    if (parameter) {
-      var key = encodeURIComponent(parameter[0]);
-      if (key.length == 0) return;
-
-      if (parameter[1].constructor != Array)
-        parameter[1] = [parameter[1]];
-
-      return parameter[1].map(function(value) {
-        return key + '=' + encodeURIComponent(value);
-      }).join('&amp;');
-    }
-  },
-
-  getValue: function(element) {
-    element = $(element);
-    var method = element.tagName.toLowerCase();
-    var parameter = Form.Element.Serializers[method](element);
-
-    if (parameter)
-      return parameter[1];
-  },
-
-  clear: function(element) {
-    $(element).value = '';
-    return element;
-  },
-
-  present: function(element) {
-    return $(element).value != '';
-  },
-
-  activate: function(element) {
-    element = $(element);
-    element.focus();
-    if (element.select &amp;&amp; ( element.tagName.toLowerCase() != 'input' ||
-      !['button', 'reset', 'submit'].include(element.type) ) )
-      element.select();
-    return element;
-  },
-
-  disable: function(element) {
-    element = $(element);
-    element.disabled = true;
-    return element;
-  },
-
-  enable: function(element) {
-    element = $(element);
-    element.blur();
-    element.disabled = false;
-    return element;
-  }
-}
-
-Object.extend(Form.Element, Form.Element.Methods);
-var Field = Form.Element;
-
-/*--------------------------------------------------------------------------*/
-
-Form.Element.Serializers = {
-  input: function(element) {
-    switch (element.type.toLowerCase()) {
-      case 'checkbox':
-      case 'radio':
-        return Form.Element.Serializers.inputSelector(element);
-      default:
-        return Form.Element.Serializers.textarea(element);
-    }
-    return false;
-  },
-
-  inputSelector: function(element) {
-    if (element.checked)
-      return [element.name, element.value];
-  },
-
-  textarea: function(element) {
-    return [element.name, element.value];
-  },
-
-  select: function(element) {
-    return Form.Element.Serializers[element.type == 'select-one' ?
-      'selectOne' : 'selectMany'](element);
-  },
-
-  selectOne: function(element) {
-    var value = '', opt, index = element.selectedIndex;
-    if (index &gt;= 0) {
-      opt = Element.extend(element.options[index]);
-      // Uses the new potential extension if hasAttribute isn't native.
-      value = opt.hasAttribute('value') ? opt.value : opt.text;
-    }
-    return [element.name, value];
-  },
-
-  selectMany: function(element) {
-    var value = [];
-    for (var i = 0, length = element.length; i &lt; length; i++) {
-      var opt = Element.extend(element.options[i]);
-      if (opt.selected)
-        // Uses the new potential extension if hasAttribute isn't native.
-        value.push(opt.hasAttribute('value') ? 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();
-    var changed = ('string' == typeof this.lastValue &amp;&amp; 'string' == typeof value
-      ? this.lastValue != value : String(this.lastValue) != String(value));
-    if (changed) {
-      this.callback(this.element, value);
-      this.lastValue = value;
-    }
-  }
-}
-
-Form.Element.Observer = Class.create();
-Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
-  getValue: function() {
-    return Form.Element.getValue(this.element);
-  }
-});
-
-Form.Observer = Class.create();
-Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
-  getValue: function() {
-    return Form.serialize(this.element);
-  }
-});
-
-/*--------------------------------------------------------------------------*/
-
-Abstract.EventObserver = function() {}
-Abstract.EventObserver.prototype = {
-  initialize: function(element, callback) {
-    this.element  = $(element);
-    this.callback = callback;
-
-    this.lastValue = this.getValue();
-    if (this.element.tagName.toLowerCase() == 'form')
-      this.registerFormCallbacks();
-    else
-      this.registerCallback(this.element);
-  },
-
-  onElementEvent: function() {
-    var value = this.getValue();
-    if (this.lastValue != value) {
-      this.callback(this.element, value);
-      this.lastValue = value;
-    }
-  },
-
-  registerFormCallbacks: function() {
-    Form.getElements(this.element).each(this.registerCallback.bind(this));
-  },
-
-  registerCallback: function(element) {
-    if (element.type) {
-      switch (element.type.toLowerCase()) {
-        case 'checkbox':
-        case 'radio':
-          Event.observe(element, 'click', this.onElementEvent.bind(this));
-          break;
-        default:
-          Event.observe(element, 'change', this.onElementEvent.bind(this));
-          break;
-      }
-    }
-  }
-}
-
-Form.Element.EventObserver = Class.create();
-Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
-  getValue: function() {
-    return Form.Element.getValue(this.element);
-  }
-});
-
-Form.EventObserver = Class.create();
-Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
-  getValue: function() {
-    return Form.serialize(this.element);
-  }
-});
-if (!window.Event) {
-  var Event = new Object();
-}
-
-Object.extend(Event, {
-  KEY_BACKSPACE: 8,
-  KEY_TAB:       9,
-  KEY_RETURN:   13,
-  KEY_ESC:      27,
-  KEY_LEFT:     37,
-  KEY_UP:       38,
-  KEY_RIGHT:    39,
-  KEY_DOWN:     40,
-  KEY_DELETE:   46,
-  KEY_HOME:     36,
-  KEY_END:      35,
-  KEY_PAGEUP:   33,
-  KEY_PAGEDOWN: 34,
-
-  element: function(event) {
-    return event.target || event.srcElement;
-  },
-
-  isLeftClick: function(event) {
-    return (((event.which) &amp;&amp; (event.which == 1)) ||
-            ((event.button) &amp;&amp; (event.button == 1)));
-  },
-
-  pointerX: function(event) {
-    return event.pageX || (event.clientX +
-      (document.documentElement.scrollLeft || document.body.scrollLeft));
-  },
-
-  pointerY: function(event) {
-    return event.pageY || (event.clientY +
-      (document.documentElement.scrollTop || document.body.scrollTop));
-  },
-
-  stop: function(event) {
-    if (event.preventDefault) {
-      event.preventDefault();
-      event.stopPropagation();
-    } else {
-      event.returnValue = false;
-      event.cancelBubble = true;
-    }
-  },
-
-  // find the first node with the given tagName, starting from the
-  // node the event was triggered on; traverses the DOM upwards
-  findElement: function(event, tagName) {
-    var element = Event.element(event);
-    while (element.parentNode &amp;&amp; (!element.tagName ||
-        (element.tagName.toUpperCase() != tagName.toUpperCase())))
-      element = element.parentNode;
-    return element;
-  },
-
-  observers: false,
-
-  _observeAndCache: function(element, name, observer, useCapture) {
-    if (!this.observers) this.observers = [];
-    if (element.addEventListener) {
-      this.observers.push([element, name, observer, useCapture]);
-      element.addEventListener(name, observer, useCapture);
-    } else if (element.attachEvent) {
-      this.observers.push([element, name, observer, useCapture]);
-      element.attachEvent('on' + name, observer);
-    }
-  },
-
-  unloadCache: function() {
-    if (!Event.observers) return;
-    for (var i = 0, length = Event.observers.length; i &lt; length; i++) {
-      Event.stopObserving.apply(this, Event.observers[i]);
-      Event.observers[i][0] = null;
-    }
-    Event.observers = false;
-  },
-
-  observe: function(element, name, observer, useCapture) {
-    element = $(element);
-    useCapture = useCapture || false;
-
-    if (name == 'keypress' &amp;&amp;
-        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
-        || element.attachEvent))
-      name = 'keydown';
-
-    Event._observeAndCache(element, name, observer, useCapture);
-  },
-
-  stopObserving: function(element, name, observer, useCapture) {
-    element = $(element);
-    useCapture = useCapture || false;
-
-    if (name == 'keypress' &amp;&amp;
-        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
-        || element.detachEvent))
-      name = 'keydown';
-
-    if (element.removeEventListener) {
-      element.removeEventListener(name, observer, useCapture);
-    } else if (element.detachEvent) {
-      try {
-        element.detachEvent('on' + name, observer);
-      } catch (e) {}
-    }
-  }
-});
-
-/* prevent memory leaks in IE */
-if (navigator.appVersion.match(/\bMSIE\b/))
-  Event.observe(window, 'unload', Event.unloadCache, false);
-var Position = {
-  // set to true if needed, warning: firefox performance problems
-  // NOT neeeded for page scrolling, only if draggable contained in
-  // scrollable elements
-  includeScrollOffsets: false,
-
-  // must be called before calling withinIncludingScrolloffset, every time the
-  // page is scrolled
-  prepare: function() {
-    this.deltaX =  window.pageXOffset
-                || document.documentElement.scrollLeft
-                || document.body.scrollLeft
-                || 0;
-    this.deltaY =  window.pageYOffset
-                || document.documentElement.scrollTop
-                || document.body.scrollTop
-                || 0;
-  },
-
-  realOffset: function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.scrollTop  || 0;
-      valueL += element.scrollLeft || 0;
-      element = element.parentNode;
-    } while (element);
-    return [valueL, valueT];
-  },
-
-  cumulativeOffset: function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-      element = element.offsetParent;
-    } while (element);
-    return [valueL, valueT];
-  },
-
-  positionedOffset: function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-      element = element.offsetParent;
-      if (element) {
-        if(element.tagName=='BODY') break;
-        var p = Element.getStyle(element, 'position');
-        if (p == 'relative' || p == 'absolute') break;
-      }
-    } while (element);
-    return [valueL, valueT];
-  },
-
-  offsetParent: function(element) {
-    if (element.offsetParent) return element.offsetParent;
-    if (element == document.body) return element;
-
-    while ((element = element.parentNode) &amp;&amp; element != document.body)
-      if (Element.getStyle(element, 'position') != 'static')
-        return element;
-
-    return document.body;
-  },
-
-  // caches x/y coordinate pair to use with overlap
-  within: function(element, x, y) {
-    if (this.includeScrollOffsets)
-      return this.withinIncludingScrolloffsets(element, x, y);
-    this.xcomp = x;
-    this.ycomp = y;
-    this.offset = this.cumulativeOffset(element);
-
-    return (y &gt;= this.offset[1] &amp;&amp;
-            y &lt;  this.offset[1] + element.offsetHeight &amp;&amp;
-            x &gt;= this.offset[0] &amp;&amp;
-            x &lt;  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 &gt;= this.offset[1] &amp;&amp;
-            this.ycomp &lt;  this.offset[1] + element.offsetHeight &amp;&amp;
-            this.xcomp &gt;= this.offset[0] &amp;&amp;
-            this.xcomp &lt;  this.offset[0] + element.offsetWidth);
-  },
-
-  // within must be called directly before
-  overlap: function(mode, element) {
-    if (!mode) return 0;
-    if (mode == 'vertical')
-      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
-        element.offsetHeight;
-    if (mode == 'horizontal')
-      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
-        element.offsetWidth;
-  },
-
-  page: function(forElement) {
-    var valueT = 0, valueL = 0;
-
-    var element = forElement;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-
-      // Safari fix
-      if (element.offsetParent==document.body)
-        if (Element.getStyle(element,'position')=='absolute') break;
-
-    } while (element = element.offsetParent);
-
-    element = forElement;
-    do {
-      if (!window.opera || element.tagName=='BODY') {
-        valueT -= element.scrollTop  || 0;
-        valueL -= element.scrollLeft || 0;
-      }
-    } while (element = element.parentNode);
-
-    return [valueL, valueT];
-  },
-
-  clone: function(source, target) {
-    var options = Object.extend({
-      setLeft:    true,
-      setTop:     true,
-      setWidth:   true,
-      setHeight:  true,
-      offsetTop:  0,
-      offsetLeft: 0
-    }, arguments[2] || {})
-
-    // find page position of source
-    source = $(source);
-    var p = Position.page(source);
-
-    // find coordinate system to use
-    target = $(target);
-    var delta = [0, 0];
-    var parent = null;
-    // delta [0,0] will do fine with position: fixed elements,
-    // position:absolute needs offsetParent deltas
-    if (Element.getStyle(target,'position') == 'absolute') {
-      parent = Position.offsetParent(target);
-      delta = Position.page(parent);
-    }
-
-    // correct by body offsets (fixes Safari)
-    if (parent == document.body) {
-      delta[0] -= document.body.offsetLeft;
-      delta[1] -= document.body.offsetTop;
-    }
-
-    // set position
-    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
-    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
-    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
-    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
-  },
-
-  absolutize: function(element) {
-    element = $(element);
-    if (element.style.position == 'absolute') return;
-    Position.prepare();
-
-    var offsets = Position.positionedOffset(element);
-    var top     = offsets[1];
-    var left    = offsets[0];
-    var width   = element.clientWidth;
-    var height  = element.clientHeight;
-
-    element._originalLeft   = left - parseFloat(element.style.left  || 0);
-    element._originalTop    = top  - parseFloat(element.style.top || 0);
-    element._originalWidth  = element.style.width;
-    element._originalHeight = element.style.height;
-
-    element.style.position = 'absolute';
-    element.style.top    = top + 'px';;
-    element.style.left   = left + 'px';;
-    element.style.width  = width + 'px';;
-    element.style.height = height + 'px';;
-  },
-
-  relativize: function(element) {
-    element = $(element);
-    if (element.style.position == 'relative') return;
-    Position.prepare();
-
-    element.style.position = 'relative';
-    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
-    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
-
-    element.style.top    = top + 'px';
-    element.style.left   = left + 'px';
-    element.style.height = element._originalHeight;
-    element.style.width  = element._originalWidth;
-  }
-}
-
-// Safari returns margins on body which is incorrect if the child is absolutely
-// positioned.  For performance reasons, redefine Position.cumulativeOffset for
-// KHTML/WebKit only.
-if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
-  Position.cumulativeOffset = function(element) {
-    var valueT = 0, valueL = 0;
-    do {
-      valueT += element.offsetTop  || 0;
-      valueL += element.offsetLeft || 0;
-      if (element.offsetParent == document.body)
-        if (Element.getStyle(element, 'position') == 'absolute') break;
-
-      element = element.offsetParent;
-    } while (element);
-
-    return [valueL, valueT];
-  }
-}
-
+/*  Prototype JavaScript framework, version 1.5.0_rc2
+ *  (c) 2005-2007 Sam Stephenson
+ *
+ *  Prototype is freely distributable under the terms of an MIT-style license.
+ *  For details, see the Prototype web site: http://prototype.conio.net/
+ *
+/*--------------------------------------------------------------------------*/
+
+var Prototype = {
+  Version: '1.5.0_rc2',
+  BrowserFeatures: {
+    XPath: !!document.evaluate
+  },
+
+  ScriptFragment: '(?:&lt;script.*?&gt;)((\n|\r|.)*?)(?:&lt;\/script&gt;)',
+  emptyFunction: function() {},
+  K: function(x) { return x }
+}
+
+var Class = {
+  create: function() {
+    return function() {
+      this.initialize.apply(this, arguments);
+    }
+  }
+}
+
+var Abstract = new Object();
+
+Object.extend = function(destination, source) {
+  for (var property in source) {
+    destination[property] = source[property];
+  }
+  return destination;
+}
+
+Object.extend(Object, {
+  inspect: function(object) {
+    try {
+      if (object === undefined) return 'undefined';
+      if (object === null) return 'null';
+      return object.inspect ? object.inspect() : object.toString();
+    } catch (e) {
+      if (e instanceof RangeError) return '...';
+      throw e;
+    }
+  },
+
+  keys: function(object) {
+    var keys = [];
+    for (var property in object)
+      keys.push(property);
+    return keys;
+  },
+
+  values: function(object) {
+    var values = [];
+    for (var property in object)
+      values.push(object[property]);
+    return values;
+  },
+
+  clone: function(object) {
+    return Object.extend({}, object);
+  }
+});
+
+Function.prototype.bind = function() {
+  var __method = this, args = $A(arguments), object = args.shift();
+  return function() {
+    return __method.apply(object, args.concat($A(arguments)));
+  }
+}
+
+Function.prototype.bindAsEventListener = function(object) {
+  var __method = this, args = $A(arguments), object = args.shift();
+  return function(event) {
+    return __method.apply(object, [( event || window.event)].concat(args).concat($A(arguments)));
+  }
+}
+
+Object.extend(Number.prototype, {
+  toColorPart: function() {
+    var digits = this.toString(16);
+    if (this &lt; 16) return '0' + digits;
+    return digits;
+  },
+
+  succ: function() {
+    return this + 1;
+  },
+
+  times: function(iterator) {
+    $R(0, this, true).each(iterator);
+    return this;
+  }
+});
+
+var Try = {
+  these: function() {
+    var returnValue;
+
+    for (var i = 0, length = arguments.length; i &lt; length; i++) {
+      var lambda = arguments[i];
+      try {
+        returnValue = lambda();
+        break;
+      } catch (e) {}
+    }
+
+    return returnValue;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+var PeriodicalExecuter = Class.create();
+PeriodicalExecuter.prototype = {
+  initialize: function(callback, frequency) {
+    this.callback = callback;
+    this.frequency = frequency;
+    this.currentlyExecuting = false;
+
+    this.registerCallback();
+  },
+
+  registerCallback: function() {
+    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
+  },
+
+  stop: function() {
+    if (!this.timer) return;
+    clearInterval(this.timer);
+    this.timer = null;
+  },
+
+  onTimerEvent: function() {
+    if (!this.currentlyExecuting) {
+      try {
+        this.currentlyExecuting = true;
+        this.callback(this);
+      } finally {
+        this.currentlyExecuting = false;
+      }
+    }
+  }
+}
+String.interpret = function(value){
+  return value == null ? '' : String(value);
+}
+
+Object.extend(String.prototype, {
+  gsub: function(pattern, replacement) {
+    var result = '', source = this, match;
+    replacement = arguments.callee.prepareReplacement(replacement);
+
+    while (source.length &gt; 0) {
+      if (match = source.match(pattern)) {
+        result += source.slice(0, match.index);
+        result += String.interpret(replacement(match));
+        source  = source.slice(match.index + match[0].length);
+      } else {
+        result += source, source = '';
+      }
+    }
+    return result;
+  },
+
+  sub: function(pattern, replacement, count) {
+    replacement = this.gsub.prepareReplacement(replacement);
+    count = count === undefined ? 1 : count;
+
+    return this.gsub(pattern, function(match) {
+      if (--count &lt; 0) return match[0];
+      return replacement(match);
+    });
+  },
+
+  scan: function(pattern, iterator) {
+    this.gsub(pattern, iterator);
+    return this;
+  },
+
+  truncate: function(length, truncation) {
+    length = length || 30;
+    truncation = truncation === undefined ? '...' : truncation;
+    return this.length &gt; length ?
+      this.slice(0, length - truncation.length) + truncation : this;
+  },
+
+  strip: function() {
+    return this.replace(/^\s+/, '').replace(/\s+$/, '');
+  },
+
+  stripTags: function() {
+    return this.replace(/&lt;\/?[^&gt;]+&gt;/gi, '');
+  },
+
+  stripScripts: function() {
+    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+  },
+
+  extractScripts: function() {
+    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
+    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+    return (this.match(matchAll) || []).map(function(scriptTag) {
+      return (scriptTag.match(matchOne) || ['', ''])[1];
+    });
+  },
+
+  evalScripts: function() {
+    return this.extractScripts().map(function(script) { return eval(script) });
+  },
+
+  escapeHTML: function() {
+    var 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] ? (div.childNodes.length &gt; 1 ?
+      $A(div.childNodes).inject('',function(memo,node){ return memo+node.nodeValue }) :
+      div.childNodes[0].nodeValue) : '';
+  },
+
+  toQueryParams: function(separator) {
+    var match = this.strip().match(/([^?#]*)(#.*)?$/);
+    if (!match) return {};
+
+    return match[1].split(separator || '&amp;').inject({}, function(hash, pair) {
+      if ((pair = pair.split('='))[0]) {
+        var name = decodeURIComponent(pair[0]);
+        var value = pair[1] ? decodeURIComponent(pair[1]) : undefined;
+
+        if (hash[name] !== undefined) {
+          if (hash[name].constructor != Array)
+            hash[name] = [hash[name]];
+          if (value) hash[name].push(value);
+        }
+        else hash[name] = value;
+      }
+      return hash;
+    });
+  },
+
+  toArray: function() {
+    return this.split('');
+  },
+
+  succ: function() {
+    return this.slice(0, this.length - 1) +
+      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+  },
+
+  camelize: function() {
+    var parts = this.split('-'), len = parts.length;
+    if (len == 1) return parts[0];
+
+    var camelized = this.charAt(0) == '-'
+      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
+      : parts[0];
+
+    for (var i = 1; i &lt; len; i++)
+      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);
+
+    return camelized;
+  },
+
+  capitalize: function(){
+    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+  },
+
+  underscore: function() {
+    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
+  },
+
+  dasherize: function() {
+    return this.gsub(/_/,'-');
+  },
+
+  inspect: function(useDoubleQuotes) {
+    var escapedString = this.replace(/\\/g, '\\\\');
+    if (useDoubleQuotes)
+      return '&quot;' + escapedString.replace(/&quot;/g, '\\&quot;') + '&quot;';
+    else
+      return &quot;'&quot; + escapedString.replace(/'/g, '\\\'') + &quot;'&quot;;
+  }
+});
+
+String.prototype.gsub.prepareReplacement = function(replacement) {
+  if (typeof replacement == 'function') return replacement;
+  var template = new Template(replacement);
+  return function(match) { return template.evaluate(match) };
+}
+
+String.prototype.parseQuery = String.prototype.toQueryParams;
+
+var Template = Class.create();
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+Template.prototype = {
+  initialize: function(template, pattern) {
+    this.template = template.toString();
+    this.pattern  = pattern || Template.Pattern;
+  },
+
+  evaluate: function(object) {
+    return this.template.gsub(this.pattern, function(match) {
+      var before = match[1];
+      if (before == '\\') return match[2];
+      return before + String.interpret(object[match[3]]);
+    });
+  }
+}
+
+var $break    = new Object();
+var $continue = new Object();
+
+var Enumerable = {
+  each: function(iterator) {
+    var index = 0;
+    try {
+      this._each(function(value) {
+        try {
+          iterator(value, index++);
+        } catch (e) {
+          if (e != $continue) throw e;
+        }
+      });
+    } catch (e) {
+      if (e != $break) throw e;
+    }
+    return this;
+  },
+
+  eachSlice: function(number, iterator) {
+    var index = -number, slices = [], array = this.toArray();
+    while ((index += number) &lt; array.length)
+      slices.push(array.slice(index, index+number));
+    return slices.map(iterator);
+  },
+
+  all: function(iterator) {
+    var result = true;
+    this.each(function(value, index) {
+      result = result &amp;&amp; !!(iterator || Prototype.K)(value, index);
+      if (!result) throw $break;
+    });
+    return result;
+  },
+
+  any: function(iterator) {
+    var result = false;
+    this.each(function(value, index) {
+      if (result = !!(iterator || Prototype.K)(value, index))
+        throw $break;
+    });
+    return result;
+  },
+
+  collect: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push((iterator || Prototype.K)(value, index));
+    });
+    return results;
+  },
+
+  detect: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      if (iterator(value, index)) {
+        result = value;
+        throw $break;
+      }
+    });
+    return result;
+  },
+
+  findAll: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  grep: function(pattern, iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      var stringValue = value.toString();
+      if (stringValue.match(pattern))
+        results.push((iterator || Prototype.K)(value, index));
+    })
+    return results;
+  },
+
+  include: function(object) {
+    var found = false;
+    this.each(function(value) {
+      if (value == object) {
+        found = true;
+        throw $break;
+      }
+    });
+    return found;
+  },
+
+  inGroupsOf: function(number, fillWith) {
+    fillWith = fillWith === undefined ? null : fillWith;
+    return this.eachSlice(number, function(slice) {
+      while(slice.length &lt; number) slice.push(fillWith);
+      return slice;
+    });
+  },
+
+  inject: function(memo, iterator) {
+    this.each(function(value, index) {
+      memo = iterator(memo, value, index);
+    });
+    return memo;
+  },
+
+  invoke: function(method) {
+    var args = $A(arguments).slice(1);
+    return this.map(function(value) {
+      return value[method].apply(value, args);
+    });
+  },
+
+  max: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (result == undefined || value &gt;= result)
+        result = value;
+    });
+    return result;
+  },
+
+  min: function(iterator) {
+    var result;
+    this.each(function(value, index) {
+      value = (iterator || Prototype.K)(value, index);
+      if (result == undefined || value &lt; result)
+        result = value;
+    });
+    return result;
+  },
+
+  partition: function(iterator) {
+    var trues = [], falses = [];
+    this.each(function(value, index) {
+      ((iterator || Prototype.K)(value, index) ?
+        trues : falses).push(value);
+    });
+    return [trues, falses];
+  },
+
+  pluck: function(property) {
+    var results = [];
+    this.each(function(value, index) {
+      results.push(value[property]);
+    });
+    return results;
+  },
+
+  reject: function(iterator) {
+    var results = [];
+    this.each(function(value, index) {
+      if (!iterator(value, index))
+        results.push(value);
+    });
+    return results;
+  },
+
+  sortBy: function(iterator) {
+    return this.map(function(value, index) {
+      return {value: value, criteria: iterator(value, index)};
+    }).sort(function(left, right) {
+      var a = left.criteria, b = right.criteria;
+      return a &lt; b ? -1 : a &gt; b ? 1 : 0;
+    }).pluck('value');
+  },
+
+  toArray: function() {
+    return this.map();
+  },
+
+  zip: function() {
+    var iterator = Prototype.K, args = $A(arguments);
+    if (typeof args.last() == 'function')
+      iterator = args.pop();
+
+    var collections = [this].concat(args).map($A);
+    return this.map(function(value, index) {
+      return iterator(collections.pluck(index));
+    });
+  },
+
+  size: function() {
+    return this.toArray().length;
+  },
+
+  inspect: function() {
+    return '#&lt;Enumerable:' + this.toArray().inspect() + '&gt;';
+  }
+}
+
+Object.extend(Enumerable, {
+  map:     Enumerable.collect,
+  find:    Enumerable.detect,
+  select:  Enumerable.findAll,
+  member:  Enumerable.include,
+  entries: Enumerable.toArray
+});
+var $A = Array.from = function(iterable) {
+  if (!iterable) return [];
+  if (iterable.toArray) {
+    return iterable.toArray();
+  } else {
+    var results = [];
+    for (var i = 0, length = iterable.length; i &lt; length; i++)
+      results.push(iterable[i]);
+    return results;
+  }
+}
+
+Object.extend(Array.prototype, Enumerable);
+
+if (!Array.prototype._reverse)
+  Array.prototype._reverse = Array.prototype.reverse;
+
+Object.extend(Array.prototype, {
+  _each: function(iterator) {
+    for (var i = 0, length = this.length; i &lt; length; i++)
+      iterator(this[i]);
+  },
+
+  clear: function() {
+    this.length = 0;
+    return this;
+  },
+
+  first: function() {
+    return this[0];
+  },
+
+  last: function() {
+    return this[this.length - 1];
+  },
+
+  compact: function() {
+    return this.select(function(value) {
+      return value != null;
+    });
+  },
+
+  flatten: function() {
+    return this.inject([], function(array, value) {
+      return array.concat(value &amp;&amp; value.constructor == Array ?
+        value.flatten() : [value]);
+    });
+  },
+
+  without: function() {
+    var values = $A(arguments);
+    return this.select(function(value) {
+      return !values.include(value);
+    });
+  },
+
+  indexOf: function(object) {
+    for (var i = 0, length = this.length; i &lt; length; i++)
+      if (this[i] == object) return i;
+    return -1;
+  },
+
+  reverse: function(inline) {
+    return (inline !== false ? this : this.toArray())._reverse();
+  },
+
+  reduce: function() {
+    return this.length &gt; 1 ? this : this[0];
+  },
+
+  uniq: function() {
+    return this.inject([], function(array, value) {
+      return array.include(value) ? array : array.concat([value]);
+    });
+  },
+
+  clone: function() {
+    return [].concat(this);
+  },
+
+  size: function() {
+    return this.length;
+  },
+
+  inspect: function() {
+    return '[' + this.map(Object.inspect).join(', ') + ']';
+  }
+});
+
+Array.prototype.toArray = Array.prototype.clone;
+
+function $w(string){
+  string = string.strip();
+  return string ? string.split(/\s+/) : [];
+}
+
+if(window.opera){
+  Array.prototype.concat = function(){
+    var array = [];
+    for(var i = 0, length = this.length; i &lt; length; i++) array.push(this[i]);
+    for(var i = 0, length = arguments.length; i &lt; length; i++) {
+      if(arguments[i].constructor == Array) {
+        for(var j = 0, arrayLength = arguments[i].length; j &lt; arrayLength; j++)
+          array.push(arguments[i][j]);
+      } else {
+        array.push(arguments[i]);
+      }
+    }
+    return array;
+  }
+}
+var Hash = {
+  _each: function(iterator) {
+    for (var key in this) {
+      var value = this[key];
+      if (typeof value == 'function') continue;
+
+      var pair = [key, value];
+      pair.key = key;
+      pair.value = value;
+      iterator(pair);
+    }
+  },
+
+  keys: function() {
+    return this.pluck('key');
+  },
+
+  values: function() {
+    return this.pluck('value');
+  },
+
+  merge: function(hash) {
+    return $H(hash).inject(this, function(mergedHash, pair) {
+      mergedHash[pair.key] = pair.value;
+      return mergedHash;
+    });
+  },
+
+  toQueryString: function() {
+    return this.map(function(pair) {
+      if (!pair.key) return null;
+
+      if (pair.value &amp;&amp; pair.value.constructor == Array) {
+        pair.value = pair.value.compact();
+
+        if (pair.value.length &lt; 2) {
+          pair.value = pair.value.reduce();
+        } else {
+          var key = encodeURIComponent(pair.key);
+          return pair.value.map(function(value) {
+            return key + '=' + encodeURIComponent(value);
+		  	  }).join('&amp;');
+        }
+      }
+
+      if (pair.value == undefined) pair[1] = '';
+      return pair.map(encodeURIComponent).join('=');
+    }).join('&amp;');
+  },
+
+  inspect: function() {
+    return '#&lt;Hash:{' + this.map(function(pair) {
+      return pair.map(Object.inspect).join(': ');
+    }).join(', ') + '}&gt;';
+  }
+}
+
+function $H(object) {
+  var hash = Object.extend({}, object || {});
+  Object.extend(hash, Enumerable);
+  Object.extend(hash, Hash);
+  return hash;
+}
+ObjectRange = Class.create();
+Object.extend(ObjectRange.prototype, Enumerable);
+Object.extend(ObjectRange.prototype, {
+  initialize: function(start, end, exclusive) {
+    this.start = start;
+    this.end = end;
+    this.exclusive = exclusive;
+  },
+
+  _each: function(iterator) {
+    var value = this.start;
+    while (this.include(value)) {
+      iterator(value);
+      value = value.succ();
+    }
+  },
+
+  include: function(value) {
+    if (value &lt; this.start)
+      return false;
+    if (this.exclusive)
+      return value &lt; this.end;
+    return value &lt;= this.end;
+  }
+});
+
+var $R = function(start, end, exclusive) {
+  return new ObjectRange(start, end, exclusive);
+}
+
+var Ajax = {
+  getTransport: function() {
+    return Try.these(
+      function() {return new XMLHttpRequest()},
+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
+      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
+    ) || false;
+  },
+
+  activeRequestCount: 0
+}
+
+Ajax.Responders = {
+  responders: [],
+
+  _each: function(iterator) {
+    this.responders._each(iterator);
+  },
+
+  register: function(responder) {
+    if (!this.include(responder))
+      this.responders.push(responder);
+  },
+
+  unregister: function(responder) {
+    this.responders = this.responders.without(responder);
+  },
+
+  dispatch: function(callback, request, transport, json) {
+    this.each(function(responder) {
+      if (typeof responder[callback] == 'function') {
+        try {
+          responder[callback].apply(responder, [request, transport, json]);
+        } catch (e) {}
+      }
+    });
+  }
+};
+
+Object.extend(Ajax.Responders, Enumerable);
+
+Ajax.Responders.register({
+  onCreate: function() {
+    Ajax.activeRequestCount++;
+  },
+  onComplete: function() {
+    Ajax.activeRequestCount--;
+  }
+});
+
+Ajax.Base = function() {};
+Ajax.Base.prototype = {
+  setOptions: function(options) {
+    this.options = {
+      method:       'post',
+      asynchronous: true,
+      contentType:  'application/x-www-form-urlencoded',
+      encoding:     'UTF-8',
+      parameters:   ''
+    }
+    Object.extend(this.options, options || {});
+
+    this.options.method = this.options.method.toLowerCase();
+    this.options.parameters = $H(typeof this.options.parameters == 'string' ?
+      this.options.parameters.toQueryParams() : this.options.parameters);
+  }
+}
+
+Ajax.Request = Class.create();
+Ajax.Request.Events =
+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
+  _complete: false,
+
+  initialize: function(url, options) {
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+    this.request(url);
+  },
+
+  request: function(url) {
+    var params = this.options.parameters;
+    if (params.any()) params['_'] = '';
+
+    if (!['get', 'post'].include(this.options.method)) {
+      // simulate other verbs over post
+      params['_method'] = this.options.method;
+      this.options.method = 'post';
+    }
+
+    this.url = url;
+
+    // when GET, append parameters to URL
+    if (this.options.method == 'get' &amp;&amp; params.any())
+      this.url += (this.url.indexOf('?') &gt;= 0 ? '&amp;' : '?') +
+        params.toQueryString();
+
+    try {
+      Ajax.Responders.dispatch('onCreate', this, this.transport);
+
+      this.transport.open(this.options.method.toUpperCase(), this.url,
+        this.options.asynchronous);
+
+      if (this.options.asynchronous)
+        setTimeout(function() { this.respondToReadyState(1) }.bind(this), 10);
+
+      this.transport.onreadystatechange = this.onStateChange.bind(this);
+      this.setRequestHeaders();
+
+      var body = this.options.method == 'post' ?
+        (this.options.postBody || params.toQueryString()) : null;
+
+      this.transport.send(body);
+
+      /* Force Firefox to handle ready state 4 for synchronous requests */
+      if (!this.options.asynchronous &amp;&amp; this.transport.overrideMimeType)
+        this.onStateChange();
+
+    }
+    catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  onStateChange: function() {
+    var readyState = this.transport.readyState;
+    if (readyState &gt; 1 &amp;&amp; !((readyState == 4) &amp;&amp; this._complete))
+      this.respondToReadyState(this.transport.readyState);
+  },
+
+  setRequestHeaders: function() {
+    var headers = {
+      'X-Requested-With': 'XMLHttpRequest',
+      'X-Prototype-Version': Prototype.Version,
+      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+    };
+
+    if (this.options.method == 'post') {
+      headers['Content-type'] = this.options.contentType +
+        (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+      /* Force &quot;Connection: close&quot; for older Mozilla browsers to work
+       * around a bug where XMLHttpRequest sends an incorrect
+       * Content-length header. See Mozilla Bugzilla #246651.
+       */
+      if (this.transport.overrideMimeType &amp;&amp;
+          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] &lt; 2005)
+            headers['Connection'] = 'close';
+    }
+
+    // user-defined headers
+    if (typeof this.options.requestHeaders == 'object') {
+      var extras = this.options.requestHeaders;
+
+      if (typeof extras.push == 'function')
+        for (var i = 0, length = extras.length; i &lt; length; i += 2)
+          headers[extras[i]] = extras[i+1];
+      else
+        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+    }
+
+    for (var name in headers)
+      this.transport.setRequestHeader(name, headers[name]);
+  },
+
+  success: function() {
+    return !this.transport.status
+        || (this.transport.status &gt;= 200 &amp;&amp; this.transport.status &lt; 300);
+  },
+
+  respondToReadyState: function(readyState) {
+    var state = Ajax.Request.Events[readyState];
+    var transport = this.transport, json = this.evalJSON();
+
+    if (state == 'Complete') {
+      try {
+        this._complete = true;
+        (this.options['on' + this.transport.status]
+         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+         || Prototype.emptyFunction)(transport, json);
+      } catch (e) {
+        this.dispatchException(e);
+      }
+
+      if ((this.getHeader('Content-type') || 'text/javascript').strip().
+        match(/^(text|application)\/(x-)?(java|ecma)script(;.*)?$/i))
+          this.evalResponse();
+    }
+
+    try {
+      (this.options['on' + state] || Prototype.emptyFunction)(transport, json);
+      Ajax.Responders.dispatch('on' + state, this, transport, json);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+
+    if (state == 'Complete') {
+      // avoid memory leak in MSIE: clean up
+      this.transport.onreadystatechange = Prototype.emptyFunction;
+    }
+  },
+
+  getHeader: function(name) {
+    try {
+      return this.transport.getResponseHeader(name);
+    } catch (e) { return null }
+  },
+
+  evalJSON: function() {
+    try {
+      var json = this.getHeader('X-JSON');
+      return json ? eval('(' + json + ')') : null;
+    } catch (e) { return null }
+  },
+
+  evalResponse: function() {
+    try {
+      return eval(this.transport.responseText);
+    } catch (e) {
+      this.dispatchException(e);
+    }
+  },
+
+  dispatchException: function(exception) {
+    (this.options.onException || Prototype.emptyFunction)(this, exception);
+    Ajax.Responders.dispatch('onException', this, exception);
+  }
+});
+
+Ajax.Updater = Class.create();
+
+Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
+  initialize: function(container, url, options) {
+    this.container = {
+      success: (container.success || container),
+      failure: (container.failure || (container.success ? null : container))
+    }
+
+    this.transport = Ajax.getTransport();
+    this.setOptions(options);
+
+    var onComplete = this.options.onComplete || Prototype.emptyFunction;
+    this.options.onComplete = (function(transport, param) {
+      this.updateContent();
+      onComplete(transport, param);
+    }).bind(this);
+
+    this.request(url);
+  },
+
+  updateContent: function() {
+    var receiver = this.container[this.success() ? 'success' : 'failure'];
+    var response = this.transport.responseText;
+
+    if (!this.options.evalScripts) response = response.stripScripts();
+
+    if (receiver = $(receiver)) {
+      if (this.options.insertion)
+        new this.options.insertion(receiver, response);
+      else
+        receiver.update(response);
+    }
+
+    if (this.success()) {
+      if (this.onComplete)
+        setTimeout(this.onComplete.bind(this), 10);
+    }
+  }
+});
+
+Ajax.PeriodicalUpdater = Class.create();
+Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
+  initialize: function(container, url, options) {
+    this.setOptions(options);
+    this.onComplete = this.options.onComplete;
+
+    this.frequency = (this.options.frequency || 2);
+    this.decay = (this.options.decay || 1);
+
+    this.updater = {};
+    this.container = container;
+    this.url = url;
+
+    this.start();
+  },
+
+  start: function() {
+    this.options.onComplete = this.updateComplete.bind(this);
+    this.onTimerEvent();
+  },
+
+  stop: function() {
+    this.updater.options.onComplete = undefined;
+    clearTimeout(this.timer);
+    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
+  },
+
+  updateComplete: function(request) {
+    if (this.options.decay) {
+      this.decay = (request.responseText == this.lastText ?
+        this.decay * this.options.decay : 1);
+
+      this.lastText = request.responseText;
+    }
+    this.timer = setTimeout(this.onTimerEvent.bind(this),
+      this.decay * this.frequency * 1000);
+  },
+
+  onTimerEvent: function() {
+    this.updater = new Ajax.Updater(this.container, this.url, this.options);
+  }
+});
+function $(element) {
+  if (arguments.length &gt; 1) {
+    for (var i = 0, elements = [], length = arguments.length; i &lt; length; i++)
+      elements.push($(arguments[i]));
+    return elements;
+  }
+  if (typeof element == 'string')
+    element = document.getElementById(element);
+  return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+  document._getElementsByXPath = function(expression, parentElement) {
+    var results = [];
+    var query = document.evaluate(expression, $(parentElement) || document,
+      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+    for (var i = 0, length = query.snapshotLength; i &lt; length; i++)
+      results.push(query.snapshotItem(i));
+    return results;
+  }
+}
+
+document.getElementsByClassName = function(className, parentElement) {
+  if (Prototype.BrowserFeatures.XPath) {
+    var q = &quot;.//*[contains(concat(' ', @class, ' '), ' &quot; + className + &quot; ')]&quot;;
+    return document._getElementsByXPath(q, parentElement);
+  } else {
+    var children = ($(parentElement) || document.body).getElementsByTagName('*');
+    var elements = [], child;
+    for (var i = 0, length = children.length; i &lt; length; i++) {
+      child = children[i];
+      if (Element.hasClassName(child, className))
+        elements.push(Element.extend(child));
+    }
+    return elements;
+  }
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!window.Element)
+  var Element = new Object();
+
+Element.extend = function(element) {
+  if (!element || _nativeExtensions || element.nodeType == 3) return element;
+
+  if (!element._extended &amp;&amp; element.tagName &amp;&amp; element != window) {
+    var methods = Object.clone(Element.Methods), cache = Element.extend.cache;
+
+    if (element.tagName == 'FORM')
+      Object.extend(methods, Form.Methods);
+    if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName))
+      Object.extend(methods, Form.Element.Methods);
+
+    Object.extend(methods, Element.Methods.Simulated);
+
+    for (var property in methods) {
+      var value = methods[property];
+      if (typeof value == 'function' &amp;&amp; !(property in element))
+        element[property] = cache.findOrStore(value);
+    }
+  }
+
+  element._extended = true;
+  return element;
+}
+
+Element.extend.cache = {
+  findOrStore: function(value) {
+    return this[value] = this[value] || function() {
+      return value.apply(null, [this].concat($A(arguments)));
+    }
+  }
+}
+
+Element.Methods = {
+  visible: function(element) {
+    return $(element).style.display != 'none';
+  },
+
+  toggle: function(element) {
+    element = $(element);
+    Element[Element.visible(element) ? 'hide' : 'show'](element);
+    return element;
+  },
+
+  hide: function(element) {
+    $(element).style.display = 'none';
+    return element;
+  },
+
+  show: function(element) {
+    $(element).style.display = '';
+    return element;
+  },
+
+  remove: function(element) {
+    element = $(element);
+    element.parentNode.removeChild(element);
+    return element;
+  },
+
+  update: function(element, html) {
+    html = typeof html == 'undefined' ? '' : html.toString();
+    $(element).innerHTML = html.stripScripts();
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  },
+
+  replace: function(element, html) {
+    element = $(element);
+    if (element.outerHTML) {
+      element.outerHTML = html.stripScripts();
+    } else {
+      var range = element.ownerDocument.createRange();
+      range.selectNodeContents(element);
+      element.parentNode.replaceChild(
+        range.createContextualFragment(html.stripScripts()), element);
+    }
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  },
+
+  inspect: function(element) {
+    element = $(element);
+    var result = '&lt;' + element.tagName.toLowerCase();
+    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+      var property = pair.first(), attribute = pair.last();
+      var value = (element[property] || '').toString();
+      if (value) result += ' ' + attribute + '=' + value.inspect(true);
+    });
+    return result + '&gt;';
+  },
+
+  recursivelyCollect: function(element, property) {
+    element = $(element);
+    var elements = [];
+    while (element = element[property])
+      if (element.nodeType == 1)
+        elements.push(Element.extend(element));
+    return elements;
+  },
+
+  ancestors: function(element) {
+    return $(element).recursivelyCollect('parentNode');
+  },
+
+  descendants: function(element) {
+    return $A($(element).getElementsByTagName('*'));
+  },
+
+  immediateDescendants: function(element) {
+    if (!(element = $(element).firstChild)) return [];
+    while (element &amp;&amp; element.nodeType != 1) element = element.nextSibling;
+    if (element) return [element].concat($(element).nextSiblings());
+    return [];
+  },
+
+  previousSiblings: function(element) {
+    return $(element).recursivelyCollect('previousSibling');
+  },
+
+  nextSiblings: function(element) {
+    return $(element).recursivelyCollect('nextSibling');
+  },
+
+  siblings: function(element) {
+    element = $(element);
+    return element.previousSiblings().reverse().concat(element.nextSiblings());
+  },
+
+  match: function(element, selector) {
+    if (typeof selector == 'string')
+      selector = new Selector(selector);
+    return selector.match($(element));
+  },
+
+  up: function(element, expression, index) {
+    return Selector.findElement($(element).ancestors(), expression, index);
+  },
+
+  down: function(element, expression, index) {
+    return Selector.findElement($(element).descendants(), expression, index);
+  },
+
+  previous: function(element, expression, index) {
+    return Selector.findElement($(element).previousSiblings(), expression, index);
+  },
+
+  next: function(element, expression, index) {
+    return Selector.findElement($(element).nextSiblings(), expression, index);
+  },
+
+  getElementsBySelector: function() {
+    var args = $A(arguments), element = $(args.shift());
+    return Selector.findChildElements(element, args);
+  },
+
+  getElementsByClassName: function(element, className) {
+    return document.getElementsByClassName(className, element);
+  },
+
+  readAttribute: function(element, name) {
+    return $(element).getAttribute(name);
+  },
+
+  getHeight: function(element) {
+    return $(element).offsetHeight;
+  },
+
+  classNames: function(element) {
+    return new Element.ClassNames(element);
+  },
+
+  hasClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    var elementClassName = element.className;
+    if (elementClassName.length == 0) return false;
+    if (elementClassName == className ||
+        elementClassName.match(new RegExp(&quot;(^|\\s)&quot; + className + &quot;(\\s|$)&quot;)))
+      return true;
+    return false;
+  },
+
+  addClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element).add(className);
+    return element;
+  },
+
+  removeClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element).remove(className);
+    return element;
+  },
+
+  toggleClassName: function(element, className) {
+    if (!(element = $(element))) return;
+    Element.classNames(element)[element.hasClassName(className) ? 'remove' : 'add'](className);
+    return element;
+  },
+
+  observe: function() {
+    Event.observe.apply(Event, arguments);
+    return $A(arguments).first();
+  },
+
+  stopObserving: function() {
+    Event.stopObserving.apply(Event, arguments);
+    return $A(arguments).first();
+  },
+
+  // removes whitespace-only text node children
+  cleanWhitespace: function(element) {
+    element = $(element);
+    var node = element.firstChild;
+    while (node) {
+      var nextNode = node.nextSibling;
+      if (node.nodeType == 3 &amp;&amp; !/\S/.test(node.nodeValue))
+        element.removeChild(node);
+      node = nextNode;
+    }
+    return element;
+  },
+
+  empty: function(element) {
+    return $(element).innerHTML.match(/^\s*$/);
+  },
+
+  childOf: function(element, ancestor) {
+    element = $(element), ancestor = $(ancestor);
+    while (element = element.parentNode)
+      if (element == ancestor) return true;
+    return false;
+  },
+
+  scrollTo: function(element) {
+    element = $(element);
+    var pos = Position.cumulativeOffset(element);
+    window.scrollTo(pos[0], pos[1]);
+    return element;
+  },
+
+  getStyle: function(element, style) {
+    element = $(element);
+    var camelizedStyle = (style == 'float' ?
+      (typeof element.style.styleFloat != 'undefined' ? 'styleFloat' : 'cssFloat') : style).camelize();
+    var value = element.style[camelizedStyle];
+    if (!value) {
+      if (document.defaultView &amp;&amp; document.defaultView.getComputedStyle) {
+        var css = document.defaultView.getComputedStyle(element, null);
+        value = css ? css[camelizedStyle] : null;
+      } else if (element.currentStyle) {
+        value = element.currentStyle[camelizedStyle];
+      }
+    }
+
+    if((value == 'auto') &amp;&amp; ['width','height'].include(style) &amp;&amp; (element.getStyle('display') != 'none'))
+      value = element['offset'+style.capitalize()] + 'px';
+
+    if (window.opera &amp;&amp; ['left', 'top', 'right', 'bottom'].include(style))
+      if (Element.getStyle(element, 'position') == 'static') value = 'auto';
+    if(style == 'opacity') {
+      if(value) return parseFloat(value);
+      if(value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+        if(value[1]) return parseFloat(value[1]) / 100;
+      return 1.0;
+    }
+    return value == 'auto' ? null : value;
+  },
+
+  setStyle: function(element, style) {
+    element = $(element);
+    for (var name in style) {
+      var value = style[name];
+      if(name == 'opacity') {
+        if (value == 1) {
+          value = (/Gecko/.test(navigator.userAgent) &amp;&amp;
+            !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ? 0.999999 : 1.0;
+          if(/MSIE/.test(navigator.userAgent) &amp;&amp; !window.opera)
+            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'');
+        } else {
+          if(value &lt; 0.00001) value = 0;
+          if(/MSIE/.test(navigator.userAgent) &amp;&amp; !window.opera)
+            element.style.filter = element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
+              'alpha(opacity='+value*100+')';
+        }
+      } else if(name == 'float') name = (typeof element.style.styleFloat != 'undefined') ? 'styleFloat' : 'cssFloat';
+      element.style[name.camelize()] = value;
+    }
+    return element;
+  },
+
+  getDimensions: function(element) {
+    element = $(element);
+    if (Element.getStyle(element, 'display') != 'none')
+      return {width: element.offsetWidth, height: element.offsetHeight};
+
+    // All *Width and *Height properties give 0 on elements with display none,
+    // so enable the element temporarily
+    var els = element.style;
+    var originalVisibility = els.visibility;
+    var originalPosition = els.position;
+    els.visibility = 'hidden';
+    els.position = 'absolute';
+    els.display = '';
+    var originalWidth = element.clientWidth;
+    var originalHeight = element.clientHeight;
+    els.display = 'none';
+    els.position = originalPosition;
+    els.visibility = originalVisibility;
+    return {width: originalWidth, height: originalHeight};
+  },
+
+  makePositioned: function(element) {
+    element = $(element);
+    var pos = Element.getStyle(element, 'position');
+    if (pos == 'static' || !pos) {
+      element._madePositioned = true;
+      element.style.position = 'relative';
+      // Opera returns the offset relative to the positioning context, when an
+      // element is position relative but top and left have not been defined
+      if (window.opera) {
+        element.style.top = 0;
+        element.style.left = 0;
+      }
+    }
+    return element;
+  },
+
+  undoPositioned: function(element) {
+    element = $(element);
+    if (element._madePositioned) {
+      element._madePositioned = undefined;
+      element.style.position =
+        element.style.top =
+        element.style.left =
+        element.style.bottom =
+        element.style.right = '';
+    }
+    return element;
+  },
+
+  makeClipping: function(element) {
+    element = $(element);
+    if (element._overflow) return element;
+    element._overflow = element.style.overflow || 'auto';
+    if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden')
+      element.style.overflow = 'hidden';
+    return element;
+  },
+
+  undoClipping: function(element) {
+    element = $(element);
+    if (!element._overflow) return element;
+    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
+    element._overflow = null;
+    return element;
+  }
+}
+
+Element.Methods.Simulated = {
+  hasAttribute: function(element, attribute) {
+    return $(element).getAttributeNode(attribute).specified;
+  }
+}
+
+// IE is missing .innerHTML support for TABLE-related elements
+if(document.all){
+  Element.Methods.update = function(element, html) {
+    element = $(element);
+    html = typeof html == 'undefined' ? '' : html.toString();
+    var tagName = element.tagName.toUpperCase();
+    if (['THEAD','TBODY','TR','TD'].include(tagName)) {
+      var div = document.createElement('div');
+      switch (tagName) {
+        case 'THEAD':
+        case 'TBODY':
+          div.innerHTML = '&lt;table&gt;&lt;tbody&gt;' +  html.stripScripts() + '&lt;/tbody&gt;&lt;/table&gt;';
+          depth = 2;
+          break;
+        case 'TR':
+          div.innerHTML = '&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;' +  html.stripScripts() + '&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;';
+          depth = 3;
+          break;
+        case 'TD':
+          div.innerHTML = '&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;' +  html.stripScripts() + '&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;';
+          depth = 4;
+      }
+      $A(element.childNodes).each(function(node){
+        element.removeChild(node)
+      });
+      depth.times(function(){ div = div.firstChild });
+
+      $A(div.childNodes).each(
+        function(node){ element.appendChild(node) });
+    } else {
+      element.innerHTML = html.stripScripts();
+    }
+    setTimeout(function() {html.evalScripts()}, 10);
+    return element;
+  }
+}
+
+Object.extend(Element, Element.Methods);
+
+var _nativeExtensions = false;
+
+if(/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+  ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) {
+    var className = 'HTML' + tag + 'Element';
+    if(window[className]) return;
+    var klass = window[className] = {};
+    klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__;
+  });
+
+Element.addMethods = function(methods) {
+  Object.extend(Element.Methods, methods || {});
+
+  function copy(methods, destination, onlyIfAbsent) {
+    onlyIfAbsent = onlyIfAbsent || false;
+    var cache = Element.extend.cache;
+    for (var property in methods) {
+      var value = methods[property];
+      if (!onlyIfAbsent || !(property in destination))
+        destination[property] = cache.findOrStore(value);
+    }
+  }
+
+  if (typeof HTMLElement != 'undefined') {
+    copy(Element.Methods, HTMLElement.prototype);
+    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
+    copy(Form.Methods, HTMLFormElement.prototype);
+    [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) {
+      copy(Form.Element.Methods, klass.prototype);
+    });
+    _nativeExtensions = true;
+  }
+}
+
+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.stripScripts();
+
+    if (this.adjacency &amp;&amp; this.element.insertAdjacentHTML) {
+      try {
+        this.element.insertAdjacentHTML(this.adjacency, this.content);
+      } catch (e) {
+        var tagName = this.element.tagName.toUpperCase();
+        if (['TBODY', 'TR'].include(tagName)) {
+          this.insertContent(this.contentFromAnonymousTable());
+        } else {
+          throw e;
+        }
+      }
+    } else {
+      this.range = this.element.ownerDocument.createRange();
+      if (this.initializeRange) this.initializeRange();
+      this.insertContent([this.range.createContextualFragment(this.content)]);
+    }
+
+    setTimeout(function() {content.evalScripts()}, 10);
+  },
+
+  contentFromAnonymousTable: function() {
+    var div = document.createElement('div');
+    div.innerHTML = '&lt;table&gt;&lt;tbody&gt;' + this.content + '&lt;/tbody&gt;&lt;/table&gt;';
+    return $A(div.childNodes[0].childNodes[0].childNodes);
+  }
+}
+
+var Insertion = new Object();
+
+Insertion.Before = Class.create();
+Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), {
+  initializeRange: function() {
+    this.range.setStartBefore(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment, this.element);
+    }).bind(this));
+  }
+});
+
+Insertion.Top = Class.create();
+Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(true);
+  },
+
+  insertContent: function(fragments) {
+    fragments.reverse(false).each((function(fragment) {
+      this.element.insertBefore(fragment, this.element.firstChild);
+    }).bind(this));
+  }
+});
+
+Insertion.Bottom = Class.create();
+Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), {
+  initializeRange: function() {
+    this.range.selectNodeContents(this.element);
+    this.range.collapse(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.appendChild(fragment);
+    }).bind(this));
+  }
+});
+
+Insertion.After = Class.create();
+Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), {
+  initializeRange: function() {
+    this.range.setStartAfter(this.element);
+  },
+
+  insertContent: function(fragments) {
+    fragments.each((function(fragment) {
+      this.element.parentNode.insertBefore(fragment,
+        this.element.nextSibling);
+    }).bind(this));
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+  initialize: function(element) {
+    this.element = $(element);
+  },
+
+  _each: function(iterator) {
+    this.element.className.split(/\s+/).select(function(name) {
+      return name.length &gt; 0;
+    })._each(iterator);
+  },
+
+  set: function(className) {
+    this.element.className = className;
+  },
+
+  add: function(classNameToAdd) {
+    if (this.include(classNameToAdd)) return;
+    this.set($A(this).concat(classNameToAdd).join(' '));
+  },
+
+  remove: function(classNameToRemove) {
+    if (!this.include(classNameToRemove)) return;
+    this.set($A(this).without(classNameToRemove).join(' '));
+  },
+
+  toString: function() {
+    return $A(this).join(' ');
+  }
+}
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+var Selector = Class.create();
+Selector.prototype = {
+  initialize: function(expression) {
+    this.params = {classNames: []};
+    this.expression = expression.toString().strip();
+    this.parseExpression();
+    this.compileMatcher();
+  },
+
+  parseExpression: function() {
+    function abort(message) { throw 'Parse error in selector: ' + message; }
+
+    if (this.expression == '')  abort('empty expression');
+
+    var params = this.params, expr = this.expression, match, modifier, clause, rest;
+    while (match = expr.match(/^(.*)\[([a-z0-9_:-]+?)(?:([~\|!]?=)(?:&quot;([^&quot;]*)&quot;|([^\]\s]*)))?\]$/i)) {
+      params.attributes = params.attributes || [];
+      params.attributes.push({name: match[2], operator: match[3], value: match[4] || match[5] || ''});
+      expr = match[1];
+    }
+
+    if (expr == '*') return this.params.wildcard = true;
+
+    while (match = expr.match(/^([^a-z0-9_-])?([a-z0-9_-]+)(.*)/i)) {
+      modifier = match[1], clause = match[2], rest = match[3];
+      switch (modifier) {
+        case '#':       params.id = clause; break;
+        case '.':       params.classNames.push(clause); break;
+        case '':
+        case undefined: params.tagName = clause.toUpperCase(); break;
+        default:        abort(expr.inspect());
+      }
+      expr = rest;
+    }
+
+    if (expr.length &gt; 0) abort(expr.inspect());
+  },
+
+  buildMatchExpression: function() {
+    var params = this.params, conditions = [], clause;
+
+    if (params.wildcard)
+      conditions.push('true');
+    if (clause = params.id)
+      conditions.push('element.getAttribute(&quot;id&quot;) == ' + clause.inspect());
+    if (clause = params.tagName)
+      conditions.push('element.tagName.toUpperCase() == ' + clause.inspect());
+    if ((clause = params.classNames).length &gt; 0)
+      for (var i = 0, length = clause.length; i &lt; length; i++)
+        conditions.push('Element.hasClassName(element, ' + clause[i].inspect() + ')');
+    if (clause = params.attributes) {
+      clause.each(function(attribute) {
+        var value = 'element.getAttribute(' + attribute.name.inspect() + ')';
+        var splitValueBy = function(delimiter) {
+          return value + ' &amp;&amp; ' + value + '.split(' + delimiter.inspect() + ')';
+        }
+
+        switch (attribute.operator) {
+          case '=':       conditions.push(value + ' == ' + attribute.value.inspect()); break;
+          case '~=':      conditions.push(splitValueBy(' ') + '.include(' + attribute.value.inspect() + ')'); break;
+          case '|=':      conditions.push(
+                            splitValueBy('-') + '.first().toUpperCase() == ' + attribute.value.toUpperCase().inspect()
+                          ); break;
+          case '!=':      conditions.push(value + ' != ' + attribute.value.inspect()); break;
+          case '':
+          case undefined: conditions.push(value + ' != null'); break;
+          default:        throw 'Unknown operator ' + attribute.operator + ' in selector';
+        }
+      });
+    }
+
+    return conditions.join(' &amp;&amp; ');
+  },
+
+  compileMatcher: function() {
+    this.match = new Function('element', 'if (!element.tagName) return false; \
+      return ' + this.buildMatchExpression());
+  },
+
+  findElements: function(scope) {
+    var element;
+
+    if (element = $(this.params.id))
+      if (this.match(element))
+        if (!scope || Element.childOf(element, scope))
+          return [element];
+
+    scope = (scope || document).getElementsByTagName(this.params.tagName || '*');
+
+    var results = [];
+    for (var i = 0, length = scope.length; i &lt; length; i++)
+      if (this.match(element = scope[i]))
+        results.push(Element.extend(element));
+
+    return results;
+  },
+
+  toString: function() {
+    return this.expression;
+  }
+}
+
+Object.extend(Selector, {
+  matchElements: function(elements, expression) {
+    var selector = new Selector(expression);
+    return elements.select(selector.match.bind(selector)).map(Element.extend);
+  },
+
+  findElement: function(elements, expression, index) {
+    if (typeof expression == 'number') index = expression, expression = false;
+    return Selector.matchElements(elements, expression || '*')[index || 0];
+  },
+
+  findChildElements: function(element, expressions) {
+    return expressions.map(function(expression) {
+      return expression.strip().split(/\s+/).inject([null], function(results, expr) {
+        var selector = new Selector(expr);
+        return results.inject([], function(elements, result) {
+          return elements.concat(selector.findElements(result || element));
+        });
+      });
+    }).flatten();
+  }
+});
+
+function $$() {
+  return Selector.findChildElements(document, $A(arguments));
+}
+var Form = {
+  reset: function(form) {
+    $(form).reset();
+    return form;
+  },
+
+  serializeElements: function(elements) {
+    return elements.inject([], function(queryComponents, element) {
+      var queryComponent = Form.Element.serialize(element);
+      if (queryComponent) queryComponents.push(queryComponent);
+      return queryComponents;
+    }).join('&amp;');
+  }
+};
+
+Form.Methods = {
+  serialize: function(form) {
+    return Form.serializeElements(Form.getElements(form));
+  },
+
+  getElements: function(form) {
+    return $A($(form).getElementsByTagName('*')).inject([],
+      function(elements, child) {
+        if (Form.Element.Serializers[child.tagName.toLowerCase()])
+          elements.push(Element.extend(child));
+        return elements;
+      }
+    );
+  },
+
+  getInputs: function(form, typeName, name) {
+    form = $(form);
+    var inputs = form.getElementsByTagName('input'), matchingInputs = [];
+
+    if (!typeName &amp;&amp; !name)
+      return $A(inputs).map(Element.extend);
+
+    for (var i = 0, length = inputs.length; i &lt; length; i++) {
+      var input = inputs[i];
+      if ((typeName &amp;&amp; input.type != typeName) ||
+          (name &amp;&amp; input.name != name))
+        continue;
+      matchingInputs.push(Element.extend(input));
+    }
+
+    return matchingInputs;
+  },
+
+  disable: function(form) {
+    form = $(form);
+    form.getElements().each(function(element) {
+      element.blur();
+      element.disabled = 'true';
+    });
+    return form;
+  },
+
+  enable: function(form) {
+    form = $(form);
+    form.getElements().each(function(element) {
+      element.disabled = '';
+    });
+    return form;
+  },
+
+  findFirstElement: function(form) {
+    return $(form).getElements().find(function(element) {
+      return element.type != 'hidden' &amp;&amp; !element.disabled &amp;&amp;
+        ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
+    });
+  },
+
+  focusFirstElement: function(form) {
+    form = $(form);
+    form.findFirstElement().activate();
+    return form;
+  }
+}
+
+Object.extend(Form, Form.Methods);
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element = {
+  focus: function(element) {
+    $(element).focus();
+    return element;
+  },
+
+  select: function(element) {
+    $(element).select();
+    return element;
+  }
+}
+
+Form.Element.Methods = {
+  serialize: function(element) {
+    element = $(element);
+    if (element.disabled) return '';
+    var method = element.tagName.toLowerCase();
+    var parameter = Form.Element.Serializers[method](element);
+
+    if (parameter) {
+      var key = encodeURIComponent(parameter[0]);
+      if (key.length == 0) return;
+
+      if (parameter[1].constructor != Array)
+        parameter[1] = [parameter[1]];
+
+      return parameter[1].map(function(value) {
+        return key + '=' + encodeURIComponent(value);
+      }).join('&amp;');
+    }
+  },
+
+  getValue: function(element) {
+    element = $(element);
+    var method = element.tagName.toLowerCase();
+    var parameter = Form.Element.Serializers[method](element);
+
+    if (parameter)
+      return parameter[1];
+  },
+
+  clear: function(element) {
+    $(element).value = '';
+    return element;
+  },
+
+  present: function(element) {
+    return $(element).value != '';
+  },
+
+  activate: function(element) {
+    element = $(element);
+    element.focus();
+    if (element.select &amp;&amp; ( element.tagName.toLowerCase() != 'input' ||
+      !['button', 'reset', 'submit'].include(element.type) ) )
+      element.select();
+    return element;
+  },
+
+  disable: function(element) {
+    element = $(element);
+    element.disabled = true;
+    return element;
+  },
+
+  enable: function(element) {
+    element = $(element);
+    element.blur();
+    element.disabled = false;
+    return element;
+  }
+}
+
+Object.extend(Form.Element, Form.Element.Methods);
+var Field = Form.Element;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+  input: function(element) {
+    switch (element.type.toLowerCase()) {
+      case 'checkbox':
+      case 'radio':
+        return Form.Element.Serializers.inputSelector(element);
+      default:
+        return Form.Element.Serializers.textarea(element);
+    }
+    return false;
+  },
+
+  inputSelector: function(element) {
+    if (element.checked)
+      return [element.name, element.value];
+  },
+
+  textarea: function(element) {
+    return [element.name, element.value];
+  },
+
+  select: function(element) {
+    return Form.Element.Serializers[element.type == 'select-one' ?
+      'selectOne' : 'selectMany'](element);
+  },
+
+  selectOne: function(element) {
+    var value = '', opt, index = element.selectedIndex;
+    if (index &gt;= 0) {
+      opt = Element.extend(element.options[index]);
+      // Uses the new potential extension if hasAttribute isn't native.
+      value = opt.hasAttribute('value') ? opt.value : opt.text;
+    }
+    return [element.name, value];
+  },
+
+  selectMany: function(element) {
+    var value = [];
+    for (var i = 0, length = element.length; i &lt; length; i++) {
+      var opt = Element.extend(element.options[i]);
+      if (opt.selected)
+        // Uses the new potential extension if hasAttribute isn't native.
+        value.push(opt.hasAttribute('value') ? 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();
+    var changed = ('string' == typeof this.lastValue &amp;&amp; 'string' == typeof value
+      ? this.lastValue != value : String(this.lastValue) != String(value));
+    if (changed) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  }
+}
+
+Form.Element.Observer = Class.create();
+Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.Observer = Class.create();
+Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = function() {}
+Abstract.EventObserver.prototype = {
+  initialize: function(element, callback) {
+    this.element  = $(element);
+    this.callback = callback;
+
+    this.lastValue = this.getValue();
+    if (this.element.tagName.toLowerCase() == 'form')
+      this.registerFormCallbacks();
+    else
+      this.registerCallback(this.element);
+  },
+
+  onElementEvent: function() {
+    var value = this.getValue();
+    if (this.lastValue != value) {
+      this.callback(this.element, value);
+      this.lastValue = value;
+    }
+  },
+
+  registerFormCallbacks: function() {
+    Form.getElements(this.element).each(this.registerCallback.bind(this));
+  },
+
+  registerCallback: function(element) {
+    if (element.type) {
+      switch (element.type.toLowerCase()) {
+        case 'checkbox':
+        case 'radio':
+          Event.observe(element, 'click', this.onElementEvent.bind(this));
+          break;
+        default:
+          Event.observe(element, 'change', this.onElementEvent.bind(this));
+          break;
+      }
+    }
+  }
+}
+
+Form.Element.EventObserver = Class.create();
+Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.Element.getValue(this.element);
+  }
+});
+
+Form.EventObserver = Class.create();
+Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), {
+  getValue: function() {
+    return Form.serialize(this.element);
+  }
+});
+if (!window.Event) {
+  var Event = new Object();
+}
+
+Object.extend(Event, {
+  KEY_BACKSPACE: 8,
+  KEY_TAB:       9,
+  KEY_RETURN:   13,
+  KEY_ESC:      27,
+  KEY_LEFT:     37,
+  KEY_UP:       38,
+  KEY_RIGHT:    39,
+  KEY_DOWN:     40,
+  KEY_DELETE:   46,
+  KEY_HOME:     36,
+  KEY_END:      35,
+  KEY_PAGEUP:   33,
+  KEY_PAGEDOWN: 34,
+
+  element: function(event) {
+    return event.target || event.srcElement;
+  },
+
+  isLeftClick: function(event) {
+    return (((event.which) &amp;&amp; (event.which == 1)) ||
+            ((event.button) &amp;&amp; (event.button == 1)));
+  },
+
+  pointerX: function(event) {
+    return event.pageX || (event.clientX +
+      (document.documentElement.scrollLeft || document.body.scrollLeft));
+  },
+
+  pointerY: function(event) {
+    return event.pageY || (event.clientY +
+      (document.documentElement.scrollTop || document.body.scrollTop));
+  },
+
+  stop: function(event) {
+    if (event.preventDefault) {
+      event.preventDefault();
+      event.stopPropagation();
+    } else {
+      event.returnValue = false;
+      event.cancelBubble = true;
+    }
+  },
+
+  // find the first node with the given tagName, starting from the
+  // node the event was triggered on; traverses the DOM upwards
+  findElement: function(event, tagName) {
+    var element = Event.element(event);
+    while (element.parentNode &amp;&amp; (!element.tagName ||
+        (element.tagName.toUpperCase() != tagName.toUpperCase())))
+      element = element.parentNode;
+    return element;
+  },
+
+  observers: false,
+
+  _observeAndCache: function(element, name, observer, useCapture) {
+    if (!this.observers) this.observers = [];
+    if (element.addEventListener) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.addEventListener(name, observer, useCapture);
+    } else if (element.attachEvent) {
+      this.observers.push([element, name, observer, useCapture]);
+      element.attachEvent('on' + name, observer);
+    }
+  },
+
+  unloadCache: function() {
+    if (!Event.observers) return;
+    for (var i = 0, length = Event.observers.length; i &lt; length; i++) {
+      Event.stopObserving.apply(this, Event.observers[i]);
+      Event.observers[i][0] = null;
+    }
+    Event.observers = false;
+  },
+
+  observe: function(element, name, observer, useCapture) {
+    element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &amp;&amp;
+        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+        || element.attachEvent))
+      name = 'keydown';
+
+    Event._observeAndCache(element, name, observer, useCapture);
+  },
+
+  stopObserving: function(element, name, observer, useCapture) {
+    element = $(element);
+    useCapture = useCapture || false;
+
+    if (name == 'keypress' &amp;&amp;
+        (navigator.appVersion.match(/Konqueror|Safari|KHTML/)
+        || element.detachEvent))
+      name = 'keydown';
+
+    if (element.removeEventListener) {
+      element.removeEventListener(name, observer, useCapture);
+    } else if (element.detachEvent) {
+      try {
+        element.detachEvent('on' + name, observer);
+      } catch (e) {}
+    }
+  }
+});
+
+/* prevent memory leaks in IE */
+if (navigator.appVersion.match(/\bMSIE\b/))
+  Event.observe(window, 'unload', Event.unloadCache, false);
+var Position = {
+  // set to true if needed, warning: firefox performance problems
+  // NOT neeeded for page scrolling, only if draggable contained in
+  // scrollable elements
+  includeScrollOffsets: false,
+
+  // must be called before calling withinIncludingScrolloffset, every time the
+  // page is scrolled
+  prepare: function() {
+    this.deltaX =  window.pageXOffset
+                || document.documentElement.scrollLeft
+                || document.body.scrollLeft
+                || 0;
+    this.deltaY =  window.pageYOffset
+                || document.documentElement.scrollTop
+                || document.body.scrollTop
+                || 0;
+  },
+
+  realOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.scrollTop  || 0;
+      valueL += element.scrollLeft || 0;
+      element = element.parentNode;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  cumulativeOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  positionedOffset: function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      element = element.offsetParent;
+      if (element) {
+        if(element.tagName=='BODY') break;
+        var p = Element.getStyle(element, 'position');
+        if (p == 'relative' || p == 'absolute') break;
+      }
+    } while (element);
+    return [valueL, valueT];
+  },
+
+  offsetParent: function(element) {
+    if (element.offsetParent) return element.offsetParent;
+    if (element == document.body) return element;
+
+    while ((element = element.parentNode) &amp;&amp; element != document.body)
+      if (Element.getStyle(element, 'position') != 'static')
+        return element;
+
+    return document.body;
+  },
+
+  // caches x/y coordinate pair to use with overlap
+  within: function(element, x, y) {
+    if (this.includeScrollOffsets)
+      return this.withinIncludingScrolloffsets(element, x, y);
+    this.xcomp = x;
+    this.ycomp = y;
+    this.offset = this.cumulativeOffset(element);
+
+    return (y &gt;= this.offset[1] &amp;&amp;
+            y &lt;  this.offset[1] + element.offsetHeight &amp;&amp;
+            x &gt;= this.offset[0] &amp;&amp;
+            x &lt;  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 &gt;= this.offset[1] &amp;&amp;
+            this.ycomp &lt;  this.offset[1] + element.offsetHeight &amp;&amp;
+            this.xcomp &gt;= this.offset[0] &amp;&amp;
+            this.xcomp &lt;  this.offset[0] + element.offsetWidth);
+  },
+
+  // within must be called directly before
+  overlap: function(mode, element) {
+    if (!mode) return 0;
+    if (mode == 'vertical')
+      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
+        element.offsetHeight;
+    if (mode == 'horizontal')
+      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
+        element.offsetWidth;
+  },
+
+  page: function(forElement) {
+    var valueT = 0, valueL = 0;
+
+    var element = forElement;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+
+      // Safari fix
+      if (element.offsetParent==document.body)
+        if (Element.getStyle(element,'position')=='absolute') break;
+
+    } while (element = element.offsetParent);
+
+    element = forElement;
+    do {
+      if (!window.opera || element.tagName=='BODY') {
+        valueT -= element.scrollTop  || 0;
+        valueL -= element.scrollLeft || 0;
+      }
+    } while (element = element.parentNode);
+
+    return [valueL, valueT];
+  },
+
+  clone: function(source, target) {
+    var options = Object.extend({
+      setLeft:    true,
+      setTop:     true,
+      setWidth:   true,
+      setHeight:  true,
+      offsetTop:  0,
+      offsetLeft: 0
+    }, arguments[2] || {})
+
+    // find page position of source
+    source = $(source);
+    var p = Position.page(source);
+
+    // find coordinate system to use
+    target = $(target);
+    var delta = [0, 0];
+    var parent = null;
+    // delta [0,0] will do fine with position: fixed elements,
+    // position:absolute needs offsetParent deltas
+    if (Element.getStyle(target,'position') == 'absolute') {
+      parent = Position.offsetParent(target);
+      delta = Position.page(parent);
+    }
+
+    // correct by body offsets (fixes Safari)
+    if (parent == document.body) {
+      delta[0] -= document.body.offsetLeft;
+      delta[1] -= document.body.offsetTop;
+    }
+
+    // set position
+    if(options.setLeft)   target.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
+    if(options.setTop)    target.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
+    if(options.setWidth)  target.style.width = source.offsetWidth + 'px';
+    if(options.setHeight) target.style.height = source.offsetHeight + 'px';
+  },
+
+  absolutize: function(element) {
+    element = $(element);
+    if (element.style.position == 'absolute') return;
+    Position.prepare();
+
+    var offsets = Position.positionedOffset(element);
+    var top     = offsets[1];
+    var left    = offsets[0];
+    var width   = element.clientWidth;
+    var height  = element.clientHeight;
+
+    element._originalLeft   = left - parseFloat(element.style.left  || 0);
+    element._originalTop    = top  - parseFloat(element.style.top || 0);
+    element._originalWidth  = element.style.width;
+    element._originalHeight = element.style.height;
+
+    element.style.position = 'absolute';
+    element.style.top    = top + 'px';;
+    element.style.left   = left + 'px';;
+    element.style.width  = width + 'px';;
+    element.style.height = height + 'px';;
+  },
+
+  relativize: function(element) {
+    element = $(element);
+    if (element.style.position == 'relative') return;
+    Position.prepare();
+
+    element.style.position = 'relative';
+    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
+    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);
+
+    element.style.top    = top + 'px';
+    element.style.left   = left + 'px';
+    element.style.height = element._originalHeight;
+    element.style.width  = element._originalWidth;
+  }
+}
+
+// Safari returns margins on body which is incorrect if the child is absolutely
+// positioned.  For performance reasons, redefine Position.cumulativeOffset for
+// KHTML/WebKit only.
+if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) {
+  Position.cumulativeOffset = function(element) {
+    var valueT = 0, valueL = 0;
+    do {
+      valueT += element.offsetTop  || 0;
+      valueL += element.offsetLeft || 0;
+      if (element.offsetParent == document.body)
+        if (Element.getStyle(element, 'position') == 'absolute') break;
+
+      element = element.offsetParent;
+    } while (element);
+
+    return [valueL, valueT];
+  }
+}
+
 Element.addMethods();
\ No newline at end of file</diff>
      <filename>public/javascripts/prototype.js</filename>
    </modified>
    <modified>
      <diff>@@ -1,209 +1,209 @@
-body, table, form, input, td, th, p, textarea, select {
-  font-family: verdana, helvetica, arial, sans-serif;
-  font-size: 11px;
-}
-
-img {
-  border: 0;
-}
-
-a:hover {
-  color: #FC0;
-}
-
-.main-panel {
-  color: #FC0;
-}
-
-.link {
-  color: #00F;
-}
-
-.buildresults-header {
-  color: #000;
-  font-weight: bold;
-}
-
-.buildresults-data {
-  color: #000;
-}
-
-.buildresults-data-failed {
-  color: #F30;
-}
-
-.stylesection {
-  margin-left: 4px;
-}
-
-.header-title {
-  font-size: 12px;
-  color: #000;
-  font-weight: bold;
-  padding-bottom: 10pt;
-}
-
-.header-label {
-  font-weight: bold;
-}
-
-.header-data {
-  color: #000;
-}
-
-.header-data-error {
-  color: #000;
-  white-space: pre;
-}
-
-.section-table {
-  margin-top: 10px;
-}
-
-.sectionheader {
-  background-color: #006;
-  color: #FFF;
-}
-
-.section-data {
-  font-size: 9px;
-  color: #000;
-}
-
-.section-oddrow {
-  background-color: #F0F7FF;
-}
-
-.section-evenrow {
-  background-color: #FFF;
-}
-
-.section-error {
-  font-size: 9px;
-  color: #F30;
-  white-space: pre;
-}
-
-.warning {
-  color: orange
-}
-
-.error {
-  color: red
-}
-
-.RecentBuildsPanel {
-  border-style: solid;
-  border-width: 1px;
-  border-color: #333399;
-  margin-bottom: 5px;
-}
-
-.RecentBuildsPanel th {
-  background-color: #333399;
-  text-align: center;
-  font-weight: bold;
-  padding: 3px;
-  color: ivory;
-}
-
-.RecentBuildsPanel td {
-  padding: 3px;
-  background-color: white;
-}
-
-.breadcrumbs {
-  color: darkgray;
-}
-
-.breadcrumbs a {
-  color: ivory;
-  text-decoration: none;
-}
-
-.breadcrumbs a:hover {
-  color: #FC0;
-}
-
-.ProjectGridHeader {
-  color: ivory;
-  font-weight: bold;
-}
-
-.ProjectGridHeader a {
-  color: ivory;
-  text-decoration: none;
-}
-
-.ProjectGridHeader a:hover {
-  color: #FC0;
-}
-
-.SideBar a {
-  text-decoration: none;
-  color: #333399;
-}
-
-.SideBar .build-passed-link {
-  color: Green;
-  text-decoration: none;
-}
-
-.SideBar .build-failed-link {
-  color: Red;
-  text-decoration: none;
-}
-
-.build-passed-link {
-  color: Green;
-  text-decoration: none;
-}
-
-.build-failed-link {
-  color: Red;
-  text-decoration: none;
-}
-
-.log {
-  overflow: auto;
-}
-
-.wholepage {
-  background-color: #000066;
-}
-
-.TopControls {
-  background-color: #333399;
-  color: ivory;
-}
-
-
-/* new stuff */
-
-.read .likes-save { display:none; }
-.read .likes-edit { display:none; }
-.edit .likes-read { display:none; }
-.edit .likes-save { display:none; }
-.save .likes-edit { display:none; }
-.save .likes-read { display:none; }
-
-.build-status {
-  font-weight: bold;
-  text-transform:uppercase;
-}
-
-.build-success .build-status { color: green;}
-.build-failed .build-status { color: red; }
-
-.success {
-  font-weight: bold;
-  color: green;
-}
-.failed {
-  font-weight: bold;
-  color: red;
-}
-
-.test-results {
-  font-weight: bold;
+body, table, form, input, td, th, p, textarea, select {
+  font-family: verdana, helvetica, arial, sans-serif;
+  font-size: 11px;
+}
+
+img {
+  border: 0;
+}
+
+a:hover {
+  color: #FC0;
+}
+
+.main-panel {
+  color: #FC0;
+}
+
+.link {
+  color: #00F;
+}
+
+.buildresults-header {
+  color: #000;
+  font-weight: bold;
+}
+
+.buildresults-data {
+  color: #000;
+}
+
+.buildresults-data-failed {
+  color: #F30;
+}
+
+.stylesection {
+  margin-left: 4px;
+}
+
+.header-title {
+  font-size: 12px;
+  color: #000;
+  font-weight: bold;
+  padding-bottom: 10pt;
+}
+
+.header-label {
+  font-weight: bold;
+}
+
+.header-data {
+  color: #000;
+}
+
+.header-data-error {
+  color: #000;
+  white-space: pre;
+}
+
+.section-table {
+  margin-top: 10px;
+}
+
+.sectionheader {
+  background-color: #006;
+  color: #FFF;
+}
+
+.section-data {
+  font-size: 9px;
+  color: #000;
+}
+
+.section-oddrow {
+  background-color: #F0F7FF;
+}
+
+.section-evenrow {
+  background-color: #FFF;
+}
+
+.section-error {
+  font-size: 9px;
+  color: #F30;
+  white-space: pre;
+}
+
+.warning {
+  color: orange
+}
+
+.error {
+  color: red
+}
+
+.RecentBuildsPanel {
+  border-style: solid;
+  border-width: 1px;
+  border-color: #333399;
+  margin-bottom: 5px;
+}
+
+.RecentBuildsPanel th {
+  background-color: #333399;
+  text-align: center;
+  font-weight: bold;
+  padding: 3px;
+  color: ivory;
+}
+
+.RecentBuildsPanel td {
+  padding: 3px;
+  background-color: white;
+}
+
+.breadcrumbs {
+  color: darkgray;
+}
+
+.breadcrumbs a {
+  color: ivory;
+  text-decoration: none;
+}
+
+.breadcrumbs a:hover {
+  color: #FC0;
+}
+
+.ProjectGridHeader {
+  color: ivory;
+  font-weight: bold;
+}
+
+.ProjectGridHeader a {
+  color: ivory;
+  text-decoration: none;
+}
+
+.ProjectGridHeader a:hover {
+  color: #FC0;
+}
+
+.SideBar a {
+  text-decoration: none;
+  color: #333399;
+}
+
+.SideBar .build-passed-link {
+  color: Green;
+  text-decoration: none;
+}
+
+.SideBar .build-failed-link {
+  color: Red;
+  text-decoration: none;
+}
+
+.build-passed-link {
+  color: Green;
+  text-decoration: none;
+}
+
+.build-failed-link {
+  color: Red;
+  text-decoration: none;
+}
+
+.log {
+  overflow: auto;
+}
+
+.wholepage {
+  background-color: #000066;
+}
+
+.TopControls {
+  background-color: #333399;
+  color: ivory;
+}
+
+
+/* new stuff */
+
+.read .likes-save { display:none; }
+.read .likes-edit { display:none; }
+.edit .likes-read { display:none; }
+.edit .likes-save { display:none; }
+.save .likes-edit { display:none; }
+.save .likes-read { display:none; }
+
+.build-status {
+  font-weight: bold;
+  text-transform:uppercase;
+}
+
+.build-success .build-status { color: green;}
+.build-failed .build-status { color: red; }
+
+.success {
+  font-weight: bold;
+  color: green;
+}
+.failed {
+  font-weight: bold;
+  color: red;
+}
+
+.test-results {
+  font-weight: bold;
 }
\ No newline at end of file</diff>
      <filename>public/stylesheets/cruisecontrol.css</filename>
    </modified>
    <modified>
      <diff>@@ -4,7 +4,7 @@ RAILS_DEFAULT_LOGGER = Logger.new(nil)
 require 'log'
 require 'command_line'
 
-VERBOTTEN_PATHS = ['.svn', 'scripts', 'builds', 'cruise.cmd', 'vendor', 'log']
+VERBOTTEN_PATHS = ['.svn', 'scripts', 'builds', 'cruise.cmd', 'vendor', 'log', '.iml', '.ipr', '.cmd', 'svn-repo']
 
 Dir['**/*'].each do |path|
   if File.file?(path) and not VERBOTTEN_PATHS.any? { |verbotten_path| path.include?(verbotten_path) }</diff>
      <filename>script/set_svn_eol_style</filename>
    </modified>
    <modified>
      <diff>@@ -1,36 +1,36 @@
-namespace :cc do
-
-  task 'build' do
-
-    ENV['RAILS_ENV'] ||= 'test'
-
-    # if custom rake task defined, invoke that
-    if ENV['CC_RAKE_TASK']
-      custom_task = ENV['CC_RAKE_TASK']
-      raise &quot;Custom rake task '#{custom_task}' not defined&quot; unless Rake.application.lookup(custom_task)
-      Rake::Task[custom_task].invoke
-    # if the project defines 'cruise' Rake task, that's all we need to do
-    elsif Rake.application.lookup('cruise')
-      Rake::Task['cruise'].invoke
-    else
-      # perform standard Rails database cleanup/preparation tasks if they are defined in project
-      # this is necessary because there is no up-to-date development database on a continuous integration box
-      if Rake.application.lookup('db:test:purge')
-        Rake::Task['db:test:purge'].invoke
-      end
-      if Rake.application.lookup('db:migrate')
-        Rake::Task['db:migrate'].invoke
-      end
-
-      # invoke 'test' or 'default' task
-      if Rake.application.lookup('test')
-      elsif Rake.application.lookup('default')
-        Rake::Task['default'].invoke
-      else
-        raise &quot;'cruise', test' or 'default' tasks not found. CruiseControl doesn't know what to build.&quot;
-      end
-      
-    end
-  end
-
-end
+namespace :cc do
+
+  task 'build' do
+
+    ENV['RAILS_ENV'] ||= 'test'
+
+    # if custom rake task defined, invoke that
+    if ENV['CC_RAKE_TASK']
+      custom_task = ENV['CC_RAKE_TASK']
+      raise &quot;Custom rake task '#{custom_task}' not defined&quot; unless Rake.application.lookup(custom_task)
+      Rake::Task[custom_task].invoke
+    # if the project defines 'cruise' Rake task, that's all we need to do
+    elsif Rake.application.lookup('cruise')
+      Rake::Task['cruise'].invoke
+    else
+      # perform standard Rails database cleanup/preparation tasks if they are defined in project
+      # this is necessary because there is no up-to-date development database on a continuous integration box
+      if Rake.application.lookup('db:test:purge')
+        Rake::Task['db:test:purge'].invoke
+      end
+      if Rake.application.lookup('db:migrate')
+        Rake::Task['db:migrate'].invoke
+      end
+
+      # invoke 'test' or 'default' task
+      if Rake.application.lookup('test')
+      elsif Rake.application.lookup('default')
+        Rake::Task['default'].invoke
+      else
+        raise &quot;'cruise', test' or 'default' tasks not found. CruiseControl doesn't know what to build.&quot;
+      end
+      
+    end
+  end
+
+end</diff>
      <filename>tasks/cc_build.rake</filename>
    </modified>
    <modified>
      <diff>@@ -1 +1 @@
-you should fix it
+you should fix it</diff>
      <filename>test/fixtures/build_mailer/build_failed</filename>
    </modified>
    <modified>
      <diff>@@ -1,112 +1,112 @@
-require File.dirname(__FILE__) + '/../test_helper'
-require 'projects_controller'
-
-# Re-raise errors caught by the controller.
-class ProjectsController
-  attr_accessor :load_projects
-  def rescue_action(e) raise end
-end
-
-class ProjectsControllerTest &lt; Test::Unit::TestCase
-  include FileSandbox
-
-  def setup
-    @controller = ProjectsController.new
-    @request    = ActionController::TestRequest.new
-    @response   = ActionController::TestResponse.new
-
-    setup_sandbox
-    @projects = new_project(&quot;one&quot;), new_project(&quot;two&quot;), new_project(&quot;three&quot;)
-    @controller.load_projects = @projects
-
-    @two = @projects[1]
-  end
-
-  def teardown
-    teardown_sandbox
-  end
-
-  def test_index
-    get :index
-    assert_equal @projects, assigns(:projects)
-  end
-
-  def test_show_with_build
-    @sandbox.new :file =&gt; &quot;two/build-24/build_status = pingpong&quot;
-    @sandbox.new :file =&gt; &quot;two/build-25/build_status = pingpong&quot;
-
-    get :show, :id =&gt; 'two'
-
-    assert_equal @two, assigns(:project)
-    assert_equal 25, assigns(:build).label
-  end
-
-  def test_show_specific_build
-    @sandbox.new :file =&gt; &quot;two/build-24/build_status = pingpong&quot;
-    @sandbox.new :file =&gt; &quot;two/build-25/build_status = pingpong&quot;
-
-    get :show, :id =&gt; 'two', :build =&gt; 24
-
-    assert_equal @two, assigns(:project)
-    assert_equal 24, assigns(:build).label
-  end
-
-  def test_show_with_no_build
-    get :show, :id =&gt; &quot;two&quot;
-
-    assert_equal @two, assigns(:project)
-    assert_equal Build::NilBuild, assigns(:build).class
-  end
-
-  def test_settings
-    get :settings, :id =&gt; &quot;two&quot;
-
-    assert_equal @two, assigns(:project)
-  end
-
-  def test_add_email
-    @projects.expects(:save_project).with(@two)
-
-    post :add_email, :id =&gt; &quot;two&quot;, :value =&gt; &quot;jss@gmail.com&quot;
-
-    assert_equal [&quot;jss@gmail.com&quot;], @two.emails
-
-    @projects.verify
-  end
-
-  def test_add_remove_email
-    @projects.stubs(:save_project)
-
-    post :add_email, :id =&gt; &quot;two&quot;, :value =&gt; &quot;jss@gmail.com&quot;
-
-    assert_equal [&quot;jss@gmail.com&quot;], @two.emails
-
-    post :add_email, :id =&gt; &quot;two&quot;, :value =&gt; &quot;art@gmail.com&quot;
-    post :add_email, :id =&gt; &quot;two&quot;, :value =&gt; &quot;stephan@gmail.com&quot;
-
-    assert_equal [&quot;jss@gmail.com&quot;, &quot;art@gmail.com&quot;, &quot;stephan@gmail.com&quot;], @two.emails
-
-    post :remove_email, :id =&gt; &quot;two&quot;, :value =&gt; &quot;art@gmail.com&quot;
-
-    assert_equal [&quot;jss@gmail.com&quot;, &quot;stephan@gmail.com&quot;], @two.emails
-  end
-
-#  def test_new
-#    get :new_project
-#  end
-#
-#  def test_create
-#    get :create_project, :id =&gt; &quot;myproject&quot;, :source_control =&gt; {:type =&gt; 'subversion',
-#                                                                 :url =&gt; &quot;http://svn/myproj&quot;,
-#                                                                 :username =&gt; &quot;foo&quot;,
-#                                                                 :password =&gt; &quot;bar&quot;}
-#  end
-
-  private
-
-  def new_project(name)
-    project = Project.new(name, Subversion.new)
-    project.path = file(name).name
-    project
-  end
-end
+require File.dirname(__FILE__) + '/../test_helper'
+require 'projects_controller'
+
+# Re-raise errors caught by the controller.
+class ProjectsController
+  attr_accessor :load_projects
+  def rescue_action(e) raise end
+end
+
+class ProjectsControllerTest &lt; Test::Unit::TestCase
+  include FileSandbox
+
+  def setup
+    @controller = ProjectsController.new
+    @request    = ActionController::TestRequest.new
+    @response   = ActionController::TestResponse.new
+
+    setup_sandbox
+    @projects = new_project(&quot;one&quot;), new_project(&quot;two&quot;), new_project(&quot;three&quot;)
+    @controller.load_projects = @projects
+
+    @two = @projects[1]
+  end
+
+  def teardown
+    teardown_sandbox
+  end
+
+  def test_index
+    get :index
+    assert_equal @projects, assigns(:projects)
+  end
+
+  def test_show_with_build
+    @sandbox.new :file =&gt; &quot;two/build-24/build_status = pingpong&quot;
+    @sandbox.new :file =&gt; &quot;two/build-25/build_status = pingpong&quot;
+
+    get :show, :id =&gt; 'two'
+
+    assert_equal @two, assigns(:project)
+    assert_equal 25, assigns(:build).label
+  end
+
+  def test_show_specific_build
+    @sandbox.new :file =&gt; &quot;two/build-24/build_status = pingpong&quot;
+    @sandbox.new :file =&gt; &quot;two/build-25/build_status = pingpong&quot;
+
+    get :show, :id =&gt; 'two', :build =&gt; 24
+
+    assert_equal @two, assigns(:project)
+    assert_equal 24, assigns(:build).label
+  end
+
+  def test_show_with_no_build
+    get :show, :id =&gt; &quot;two&quot;
+
+    assert_equal @two, assigns(:project)
+    assert_equal Build::NilBuild, assigns(:build).class
+  end
+
+  def test_settings
+    get :settings, :id =&gt; &quot;two&quot;
+
+    assert_equal @two, assigns(:project)
+  end
+
+  def test_add_email
+    @projects.expects(:save_project).with(@two)
+
+    post :add_email, :id =&gt; &quot;two&quot;, :value =&gt; &quot;jss@gmail.com&quot;
+
+    assert_equal [&quot;jss@gmail.com&quot;], @two.emails
+
+    @projects.verify
+  end
+
+  def test_add_remove_email
+    @projects.stubs(:save_project)
+
+    post :add_email, :id =&gt; &quot;two&quot;, :value =&gt; &quot;jss@gmail.com&quot;
+
+    assert_equal [&quot;jss@gmail.com&quot;], @two.emails
+
+    post :add_email, :id =&gt; &quot;two&quot;, :value =&gt; &quot;art@gmail.com&quot;
+    post :add_email, :id =&gt; &quot;two&quot;, :value =&gt; &quot;stephan@gmail.com&quot;
+
+    assert_equal [&quot;jss@gmail.com&quot;, &quot;art@gmail.com&quot;, &quot;stephan@gmail.com&quot;], @two.emails
+
+    post :remove_email, :id =&gt; &quot;two&quot;, :value =&gt; &quot;art@gmail.com&quot;
+
+    assert_equal [&quot;jss@gmail.com&quot;, &quot;stephan@gmail.com&quot;], @two.emails
+  end
+
+#  def test_new
+#    get :new_project
+#  end
+#
+#  def test_create
+#    get :create_project, :id =&gt; &quot;myproject&quot;, :source_control =&gt; {:type =&gt; 'subversion',
+#                                                                 :url =&gt; &quot;http://svn/myproj&quot;,
+#                                                                 :username =&gt; &quot;foo&quot;,
+#                                                                 :password =&gt; &quot;bar&quot;}
+#  end
+
+  private
+
+  def new_project(name)
+    project = Project.new(name, Subversion.new)
+    project.path = file(name).name
+    project
+  end
+end</diff>
      <filename>test/functional/projects_controller_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,144 +1,144 @@
-require File.dirname(__FILE__) + '/../test_helper'
-
-class IntegrationTest &lt; Test::Unit::TestCase
-  include FileSandbox
-
-  def test_checkout
-    # with_project calls svn.checkout
-    with_project 'passing_project' do |project, sandbox, svn|
-      assert File.exists?(&quot;passing_project/work/passing_test.rb&quot;)
-    end
-  end
-
-  def test_new_revisions
-    with_project('passing_project', :revision =&gt; 2) do |project, sandbox, svn|
-      expected_revisions = [
-          Revision.new(3, 'averkhov', DateTime.new(2007, 01, 11, 14, 01, 43, Rational(-7, 24)),
-                       'another revision',
-                       [ChangesetEntry.new('M', '/passing_project/revision_label.txt')]),
-          Revision.new(4, 'averkhov', DateTime.new(2007, 01, 11, 14, 02, 03, Rational(-7, 24)),
-                       'and one more revision, for good measure',
-                       [ChangesetEntry.new('M', '/passing_project/revision_label.txt')]),
-          Revision.new(7, 'averkhov', DateTime.new(2007, 01, 12, 18, 05, 26, Rational(-7, 24)),
-                       'Making both revision labels up to date',
-                       [ChangesetEntry.new('M', '/failing_project/revision_label.txt'),
-                        ChangesetEntry.new('M', '/passing_project/revision_label.txt')])
-          ]
-      assert_equal expected_revisions, svn.revisions_since(project, 2)
-    end
-  end
-
-  def test_new_revisions_should_return_an_empty_array_for_uptodate_local_copy
-    with_project 'passing_project' do |project, sandbox, svn|
-      assert_equal [],  svn.revisions_since(project, 7)
-    end
-  end
-
-  def test_build_if_necessary
-    with_project('passing_project', :revision =&gt; 2) do |project, sandbox, svn|
-      sandbox.new :file=&gt; 'passing_project/build-2/build_status = success'
-      assert_equal '2', File.read(&quot;#{sandbox.root}/passing_project/work/revision_label.txt&quot;).chomp
-
-      result = project.build_if_necessary
-
-      assert result.is_a?(Build)
-
-      assert_equal true, result.successful?
-
-      assert File.exists?(&quot;#{sandbox.root}/passing_project/build-7/build_status = success&quot;)
-      assert File.exists?(&quot;#{sandbox.root}/passing_project/build-7/changeset.log&quot;)
-      assert File.exists?(&quot;#{sandbox.root}/passing_project/build-7/build.log&quot;)
-    end
-  end
-
-  def test_build_if_necessary_for_a_failing_build
-    with_project('failing_project', :revision =&gt; 6) do |project, sandbox, svn|
-      result = project.build_if_necessary
-
-      assert result.is_a?(Build)
-      assert_equal true, result.failed?
-
-      assert file(&quot;failing_project/build-7/build_status = failed&quot;).exists?
-      assert_equal false, file(&quot;failing_project/build-7/build_status = success&quot;).exists?
-
-      assert file(&quot;failing_project/build-7/changeset.log&quot;).exists?
-      assert file(&quot;failing_project/build-7/build.log&quot;).exists?
-    end
-  end
-
-  def test_build_if_necessary_should_return_nil_when_no_changes_were_made
-    with_project 'passing_project' do |project, sandbox, svn|
-      sandbox.new :file=&gt;'passing_project/build-7/build_status = success'
-      result = project.build_if_necessary
-      assert_nil result
-      # test existence and contents of log files
-    end
-  end
-  
-  def test_builder_should_set_RAILS_ENV_to_test_and_invoke_db_migrate_and_test_instead_of_if_these_tasks_are_defined
-    with_project('project_with_db_migrate') do |project, sandbox, svn|
-      build = project.build
-      build_log = File.read(&quot;#{build.artifacts_directory}/build.log&quot;)
-      
-      assert build_log.include?(&quot;RAILS_ENV=test\ndb:migrate invoked\ndefault invoked\n&quot;), 
-          '&quot;RAILS_ENV=test\ndb:migrate invoked\ntest invoked\n&quot; not found in build log:' + &quot;\n&quot; + build_log
-    end
-    
-  end
-
-  def test_builder_should_clear_RAILS_ENV_and_invoke_cruise_if_this_task_is_defined
-    with_project('project_with_cruise_and_default_tasks') do |project, sandbox, svn|
-      build = project.build
-      build_log = File.read(&quot;#{build.artifacts_directory}/build.log&quot;)
-      
-      assert build_log.include?(&quot;RAILS_ENV=\&quot;test\&quot;\ncruise invoked\n&quot;), 
-          '&quot;RAILS_ENV=&quot;test&quot;\ncruise invoked\n&quot; not found in build log:' + &quot;\n&quot; + build_log
-    end
-    
-  end
-
-  def test_custom_build_command
-    with_project('project_with_cruise_and_default_tasks') do |project, sandbox, svn|
-      project.build_command = 'echo Vasya_was_here'
-
-      build = project.build
-      build_log = File.read(&quot;#{build.artifacts_directory}/build.log&quot;)
-      
-      assert build_log.include?(&quot;Vasya_was_here&quot;), 
-          '&quot;Vasya_was_here&quot; not found in build log:' + &quot;\n&quot; + build_log
-    end
-    
-  end
-
-  def test_custom_rake_task
-    with_project('project_with_custom_rake_task') do |project, sandbox, svn|
-      project.rake_task = 'my_build'
-
-      build = project.build
-      build_log = File.read(&quot;#{build.artifacts_directory}/build.log&quot;)
-      
-      assert build_log.include?(&quot;my_build invoked\n&quot;), 
-          '&quot;my_build invoked\n&quot; not found in build log:' + &quot;\n&quot; + build_log
-    end
-    
-  end
-
-
-  def fixture_repository_url
-    repository_path = File.expand_path(&quot;#{RAILS_ROOT}/test/fixtures/svn-repo&quot;)
-    urlified_path = repository_path.sub(/^[a-zA-Z]:/, '').gsub('\\', '/')
-    &quot;file://#{urlified_path}&quot;
-  end
-
-  def with_project(project_name, options = {}, &amp;block)
-    in_sandbox do |sandbox|
-      svn = Subversion.new :url =&gt; &quot;#{fixture_repository_url}/#{project_name}&quot;
-      svn.checkout &quot;#{sandbox.root}/#{project_name}/work&quot;, options[:revision]
-      
-      project = Project.new('passing_project', svn, &quot;#{sandbox.root}/#{project_name}/work&quot;)
-      project.path = &quot;#{sandbox.root}/#{project_name}&quot;
-
-      block.call(project, sandbox, svn)
-    end
-  end
+require File.dirname(__FILE__) + '/../test_helper'
+
+class IntegrationTest &lt; Test::Unit::TestCase
+  include FileSandbox
+
+  def test_checkout
+    # with_project calls svn.checkout
+    with_project 'passing_project' do |project, sandbox, svn|
+      assert File.exists?(&quot;passing_project/work/passing_test.rb&quot;)
+    end
+  end
+
+  def test_new_revisions
+    with_project('passing_project', :revision =&gt; 2) do |project, sandbox, svn|
+      expected_revisions = [
+          Revision.new(3, 'averkhov', DateTime.new(2007, 01, 11, 14, 01, 43, Rational(-7, 24)),
+                       'another revision',
+                       [ChangesetEntry.new('M', '/passing_project/revision_label.txt')]),
+          Revision.new(4, 'averkhov', DateTime.new(2007, 01, 11, 14, 02, 03, Rational(-7, 24)),
+                       'and one more revision, for good measure',
+                       [ChangesetEntry.new('M', '/passing_project/revision_label.txt')]),
+          Revision.new(7, 'averkhov', DateTime.new(2007, 01, 12, 18, 05, 26, Rational(-7, 24)),
+                       'Making both revision labels up to date',
+                       [ChangesetEntry.new('M', '/failing_project/revision_label.txt'),
+                        ChangesetEntry.new('M', '/passing_project/revision_label.txt')])
+          ]
+      assert_equal expected_revisions, svn.revisions_since(project, 2)
+    end
+  end
+
+  def test_new_revisions_should_return_an_empty_array_for_uptodate_local_copy
+    with_project 'passing_project' do |project, sandbox, svn|
+      assert_equal [],  svn.revisions_since(project, 7)
+    end
+  end
+
+  def test_build_if_necessary
+    with_project('passing_project', :revision =&gt; 2) do |project, sandbox, svn|
+      sandbox.new :file=&gt; 'passing_project/build-2/build_status = success'
+      assert_equal '2', File.read(&quot;#{sandbox.root}/passing_project/work/revision_label.txt&quot;).chomp
+
+      result = project.build_if_necessary
+
+      assert result.is_a?(Build)
+
+      assert_equal true, result.successful?
+
+      assert File.exists?(&quot;#{sandbox.root}/passing_project/build-7/build_status = success&quot;)
+      assert File.exists?(&quot;#{sandbox.root}/passing_project/build-7/changeset.log&quot;)
+      assert File.exists?(&quot;#{sandbox.root}/passing_project/build-7/build.log&quot;)
+    end
+  end
+
+  def test_build_if_necessary_for_a_failing_build
+    with_project('failing_project', :revision =&gt; 6) do |project, sandbox, svn|
+      result = project.build_if_necessary
+
+      assert result.is_a?(Build)
+      assert_equal true, result.failed?
+
+      assert file(&quot;failing_project/build-7/build_status = failed&quot;).exists?
+      assert_equal false, file(&quot;failing_project/build-7/build_status = success&quot;).exists?
+
+      assert file(&quot;failing_project/build-7/changeset.log&quot;).exists?
+      assert file(&quot;failing_project/build-7/build.log&quot;).exists?
+    end
+  end
+
+  def test_build_if_necessary_should_return_nil_when_no_changes_were_made
+    with_project 'passing_project' do |project, sandbox, svn|
+      sandbox.new :file=&gt;'passing_project/build-7/build_status = success'
+      result = project.build_if_necessary
+      assert_nil result
+      # test existence and contents of log files
+    end
+  end
+  
+  def test_builder_should_set_RAILS_ENV_to_test_and_invoke_db_migrate_and_test_instead_of_if_these_tasks_are_defined
+    with_project('project_with_db_migrate') do |project, sandbox, svn|
+      build = project.build
+      build_log = File.read(&quot;#{build.artifacts_directory}/build.log&quot;)
+      
+      assert build_log.include?(&quot;RAILS_ENV=test\ndb:migrate invoked\ndefault invoked\n&quot;), 
+          '&quot;RAILS_ENV=test\ndb:migrate invoked\ntest invoked\n&quot; not found in build log:' + &quot;\n&quot; + build_log
+    end
+    
+  end
+
+  def test_builder_should_clear_RAILS_ENV_and_invoke_cruise_if_this_task_is_defined
+    with_project('project_with_cruise_and_default_tasks') do |project, sandbox, svn|
+      build = project.build
+      build_log = File.read(&quot;#{build.artifacts_directory}/build.log&quot;)
+      
+      assert build_log.include?(&quot;RAILS_ENV=\&quot;test\&quot;\ncruise invoked\n&quot;), 
+          '&quot;RAILS_ENV=&quot;test&quot;\ncruise invoked\n&quot; not found in build log:' + &quot;\n&quot; + build_log
+    end
+    
+  end
+
+  def test_custom_build_command
+    with_project('project_with_cruise_and_default_tasks') do |project, sandbox, svn|
+      project.build_command = 'echo Vasya_was_here'
+
+      build = project.build
+      build_log = File.read(&quot;#{build.artifacts_directory}/build.log&quot;)
+      
+      assert build_log.include?(&quot;Vasya_was_here&quot;), 
+          '&quot;Vasya_was_here&quot; not found in build log:' + &quot;\n&quot; + build_log
+    end
+    
+  end
+
+  def test_custom_rake_task
+    with_project('project_with_custom_rake_task') do |project, sandbox, svn|
+      project.rake_task = 'my_build'
+
+      build = project.build
+      build_log = File.read(&quot;#{build.artifacts_directory}/build.log&quot;)
+      
+      assert build_log.include?(&quot;my_build invoked\n&quot;), 
+          '&quot;my_build invoked\n&quot; not found in build log:' + &quot;\n&quot; + build_log
+    end
+    
+  end
+
+
+  def fixture_repository_url
+    repository_path = File.expand_path(&quot;#{RAILS_ROOT}/test/fixtures/svn-repo&quot;)
+    urlified_path = repository_path.sub(/^[a-zA-Z]:/, '').gsub('\\', '/')
+    &quot;file://#{urlified_path}&quot;
+  end
+
+  def with_project(project_name, options = {}, &amp;block)
+    in_sandbox do |sandbox|
+      svn = Subversion.new :url =&gt; &quot;#{fixture_repository_url}/#{project_name}&quot;
+      svn.checkout &quot;#{sandbox.root}/#{project_name}/work&quot;, options[:revision]
+      
+      project = Project.new('passing_project', svn, &quot;#{sandbox.root}/#{project_name}/work&quot;)
+      project.path = &quot;#{sandbox.root}/#{project_name}&quot;
+
+      block.call(project, sandbox, svn)
+    end
+  end
 end
\ No newline at end of file</diff>
      <filename>test/integration/builder_integration_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,58 +1,58 @@
-ENV[&quot;RAILS_ENV&quot;] = &quot;test&quot;
-
-require File.expand_path(File.dirname(__FILE__) + &quot;/../config/environment&quot;)
-
-require_dependency 'application'
-
-# Make double-sure the RAILS_ENV is set to test,
-# so fixtures are loaded to the right database
-silence_warnings { RAILS_ENV = &quot;test&quot; }
-
-require 'test/unit'
-require 'action_controller/test_process'
-require 'action_controller/integration'
-require 'action_web_service/test_invoke'
-require 'breakpoint'
-require 'mocha'
-require 'stubba'
-require &quot;#{RAILS_ROOT}/vendor/file_sandbox/lib/file_sandbox&quot;
-
-ActionMailer::Base.delivery_method = :test
-ActionMailer::Base.perform_deliveries = true
-
-class Test::Unit::TestCase
-
-  def assert_raises(arg1 = nil, arg2 = nil)
-    expected_class = arg1.is_a?(Class) ? arg1 : nil
-    expected_message = arg1.is_a?(String) ? arg1 : arg2
-    begin 
-      yield
-      fail &quot;expected error was not raised&quot;
-    rescue Test::Unit::AssertionFailedError
-      raise
-    rescue =&gt; e
-      raise if e.message == &quot;expected error was not raised&quot;
-      assert_equal(expected_class, e.class, &quot;Unexpected error type raised&quot;) if expected_class
-      assert_equal(expected_message, e.message, &quot;Unexpected error message&quot;) if expected_message.is_a? String
-      assert_matched(expected_message, e.message, &quot;Unexpected error message&quot;) if expected_message.is_a? Regexp
-    end
-  end
-
-  def in_total_sandbox(&amp;block)
-    in_sandbox do |sandbox|
-      @dir = File.expand_path(sandbox.root)
-      @stdout = &quot;#{@dir}/stdout&quot;
-      @stderr = &quot;#{@dir}/stderr&quot;
-      @prompt = &quot;#{@dir} #{Platform.user}$&quot;
-      yield(sandbox)
-    end
-  end
-
-  def with_sandbox_project(&amp;block)
-    in_total_sandbox do |sandbox|
-      project = Project.new('my_project', nil, '.')
-      project.path = sandbox.root
-      yield(sandbox, project)
-    end
-  end
-end
+ENV[&quot;RAILS_ENV&quot;] = &quot;test&quot;
+
+require File.expand_path(File.dirname(__FILE__) + &quot;/../config/environment&quot;)
+
+require_dependency 'application'
+
+# Make double-sure the RAILS_ENV is set to test,
+# so fixtures are loaded to the right database
+silence_warnings { RAILS_ENV = &quot;test&quot; }
+
+require 'test/unit'
+require 'action_controller/test_process'
+require 'action_controller/integration'
+require 'action_web_service/test_invoke'
+require 'breakpoint'
+require 'mocha'
+require 'stubba'
+require &quot;#{RAILS_ROOT}/vendor/file_sandbox/lib/file_sandbox&quot;
+
+ActionMailer::Base.delivery_method = :test
+ActionMailer::Base.perform_deliveries = true
+
+class Test::Unit::TestCase
+
+  def assert_raises(arg1 = nil, arg2 = nil)
+    expected_class = arg1.is_a?(Class) ? arg1 : nil
+    expected_message = arg1.is_a?(String) ? arg1 : arg2
+    begin 
+      yield
+      fail &quot;expected error was not raised&quot;
+    rescue Test::Unit::AssertionFailedError
+      raise
+    rescue =&gt; e
+      raise if e.message == &quot;expected error was not raised&quot;
+      assert_equal(expected_class, e.class, &quot;Unexpected error type raised&quot;) if expected_class
+      assert_equal(expected_message, e.message, &quot;Unexpected error message&quot;) if expected_message.is_a? String
+      assert_matched(expected_message, e.message, &quot;Unexpected error message&quot;) if expected_message.is_a? Regexp
+    end
+  end
+
+  def in_total_sandbox(&amp;block)
+    in_sandbox do |sandbox|
+      @dir = File.expand_path(sandbox.root)
+      @stdout = &quot;#{@dir}/stdout&quot;
+      @stderr = &quot;#{@dir}/stderr&quot;
+      @prompt = &quot;#{@dir} #{Platform.user}$&quot;
+      yield(sandbox)
+    end
+  end
+
+  def with_sandbox_project(&amp;block)
+    in_total_sandbox do |sandbox|
+      project = Project.new('my_project', nil, '.')
+      project.path = sandbox.root
+      yield(sandbox, project)
+    end
+  end
+end</diff>
      <filename>test/test_helper.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,37 +1,37 @@
-require File.dirname(__FILE__) + '/../test_helper'
-
-class BuildMailerTest &lt; Test::Unit::TestCase
-  FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
-  CHARSET = &quot;utf-8&quot;
-
-  include ActionMailer::Quoting
-
-  def setup
-    ActionMailer::Base.delivery_method = :test
-    ActionMailer::Base.perform_deliveries = true
-    ActionMailer::Base.deliveries = []
-
-    @expected = TMail::Mail.new
-    @expected.set_content_type &quot;text&quot;, &quot;plain&quot;, { &quot;charset&quot; =&gt; CHARSET }
-    @expected.mime_version = '1.0'
-  end
-
-  def test_test
-    @expected.subject = 'Test CI E-mail'
-    @expected.body    = read_fixture('test')
-    @expected.from    = &quot;cruisecontrol@thoughtworks.com&quot;
-    @expected.date    = Time.now
-    @expected.to = &quot;Joe&quot;
-
-    assert_equal @expected.encoded, BuildMailer.create_test(&quot;Joe&quot;, @expected.date).encoded
-  end
-
-  private
-    def read_fixture(action)
-      IO.readlines(&quot;#{FIXTURES_PATH}/build_mailer/#{action}&quot;)
-    end
-
-    def encode(subject)
-      quoted_printable(subject, CHARSET)
-    end
-end
+require File.dirname(__FILE__) + '/../test_helper'
+
+class BuildMailerTest &lt; Test::Unit::TestCase
+  FIXTURES_PATH = File.dirname(__FILE__) + '/../fixtures'
+  CHARSET = &quot;utf-8&quot;
+
+  include ActionMailer::Quoting
+
+  def setup
+    ActionMailer::Base.delivery_method = :test
+    ActionMailer::Base.perform_deliveries = true
+    ActionMailer::Base.deliveries = []
+
+    @expected = TMail::Mail.new
+    @expected.set_content_type &quot;text&quot;, &quot;plain&quot;, { &quot;charset&quot; =&gt; CHARSET }
+    @expected.mime_version = '1.0'
+  end
+
+  def test_test
+    @expected.subject = 'Test CI E-mail'
+    @expected.body    = read_fixture('test')
+    @expected.from    = &quot;cruisecontrol@thoughtworks.com&quot;
+    @expected.date    = Time.now
+    @expected.to = &quot;Joe&quot;
+
+    assert_equal @expected.encoded, BuildMailer.create_test(&quot;Joe&quot;, @expected.date).encoded
+  end
+
+  private
+    def read_fixture(action)
+      IO.readlines(&quot;#{FIXTURES_PATH}/build_mailer/#{action}&quot;)
+    end
+
+    def encode(subject)
+      quoted_printable(subject, CHARSET)
+    end
+end</diff>
      <filename>test/unit/build_mailer_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,184 +1,184 @@
-require File.dirname(__FILE__) + '/../test_helper'
-
-class BuildTest &lt; Test::Unit::TestCase
-  include FileSandbox
-
-  def test_initialize_should_load_status_file_and_build_log
-    with_sandbox_project do |sandbox, project|
-      sandbox.new :file =&gt; &quot;build-2/build_status = success&quot;
-      sandbox.new :file =&gt; &quot;build-2/build.log&quot;, :with_content =&gt; &quot;some content&quot;
-      build = Build.new(project, 2)
-  
-      assert_equal 2, build.label
-      assert_equal true, build.successful?
-      assert_equal &quot;some content&quot;, build.output
-    end
-  end
-
-  def test_initialize_should_load__failed_status_file
-    with_sandbox_project do |sandbox, project|
-      sandbox.new :file =&gt; &quot;build-2/build_status = failed&quot;
-      build = Build.new(project, 2)
-  
-      assert_equal 2, build.label
-      assert_equal true, build.failed?
-    end
-  end
-
-  def test_output_grabs_log_file_when_file_exists
-    with_sandbox_project do |sandbox, project|
-      File.expects(:'read').with(&quot;#{project.path}/build-1/build.log&quot;).returns(['line 1', 'line 2'])
-      assert_equal ['line 1', 'line 2'], Build.new(project, 1).output
-    end
-  end
-  
-  def test_output_gives_empty_string_when_file_does_not_exist
-    with_sandbox_project do |sandbox, project|
-      File.expects(:'read').with(&quot;#{project.path}/build-1/build.log&quot;).raises(StandardError)
-      assert_equal &quot;&quot;, Build.new(project, 1).output
-    end
-  end
-  
-  def test_coverage_reports_reads_correct_coverage_log_file_by_correct_name
-    with_sandbox_project do |sandbox, project|
-      File.expects(:'read').with(&quot;#{project.path}/build-1/coverage-foo.log&quot;).returns(['line 1', 'line 2'])
-      assert_equal ['line 1', 'line 2'], Build.new(project, 1).coverage_reports[:foo]
-    end
-  end
-  
-  def test_coverage_reports_when_file_does_not_exist
-    with_sandbox_project do |sandbox, project|
-      File.expects(:'read').with(&quot;#{project.path}/build-1/coverage-units.log&quot;).raises(StandardError)
-      assert_equal &quot;&quot;, Build.new(project, 1).coverage_reports[:units]
-    end
-  end
-
-  def test_successful?
-    with_sandbox_project do |sandbox, project|
-      sandbox.new :file =&gt; &quot;build-1/build_status = success&quot;
-      sandbox.new :file =&gt; &quot;build-2/build_status = Success&quot;
-      sandbox.new :file =&gt; &quot;build-3/build_status = failure&quot;
-      sandbox.new :file =&gt; &quot;build-4/build_status = crap&quot;
-      sandbox.new :file =&gt; &quot;build-5/foo&quot;
-  
-      assert Build.new(project, 1).successful?
-      assert Build.new(project, 2).successful?
-      assert !Build.new(project, 3).successful?
-      assert !Build.new(project, 4).successful?
-      assert !Build.new(project, 5).successful?
-    end
-  end
-
-  def test_nil_build
-    assert_equal '-', Build.nil.time
-    assert_equal '-', Build.nil.label
-    assert_equal '-', Build.nil.output
-    assert_equal :never_built, Build.nil.status
-  end
-
-  def test_formatted_time_when_status_file_does_not_exist
-    with_sandbox_project do |sandbox, project|
-      assert_equal '-', Build.new(project, 1).formatted_time
-    end
-  end
-  
-  def test_formatted_time_when_status_file_exists
-    with_sandbox_project do |sandbox, project|
-      sandbox.new :file =&gt; &quot;build-1/build_status = success&quot;
-      Status.any_instance.expects(:created_at).returns(Time.local(2006, 5, 3, 14, 33, 50))
-      assert_equal &quot;02:33 PM May 03, 2006&quot;, Build.new(project, 1).formatted_time
-    end
-  end
-  
-  def test_run_successful_build
-    with_sandbox_project do |sandbox, project|
-      expected_build_directory = File.join(sandbox.root, 'build-123')
-  
-      FileUtils.expects(:mkdir_p).with(expected_build_directory).returns(expected_build_directory)
-  
-      build = Build.new(project, 123)
-  
-      expected_command = build.rake
-      expected_build_log = File.join(expected_build_directory, 'build.log')
-      expected_redirect_options = {
-          :stdout =&gt; expected_build_log,
-          :stderr =&gt; expected_build_log,
-          :escape_quotes =&gt; false
-        }
-      
-      build.expects(:execute).with(build.rake, expected_redirect_options).returns(&quot;hi, mom!&quot;)
-      Status.any_instance.expects(:'succeed!')
-  
-      build.run
-  
-      build.verify
-      FileUtils.verify
-    end
-  end
-
-  def test_run_unsuccessful_build
-    with_sandbox_project do |sandbox, project|
-      expected_build_directory = File.join(sandbox.root, 'build-123')
-  
-      FileUtils.expects(:mkdir_p).with(expected_build_directory).returns(expected_build_directory)
-  
-      build = Build.new(project, 123)
-  
-      expected_build_log = File.join(expected_build_directory, 'build.log')
-      expected_redirect_options = {
-        :stdout =&gt; expected_build_log,
-        :stderr =&gt; expected_build_log,
-        :escape_quotes =&gt; false
-      }
-  
-      build.expects(:execute).with(build.rake, expected_redirect_options).raises(CommandLine::ExecutionError)
-      Status.any_instance.expects(:'fail!')
-  
-      build.run
-  
-      build.verify
-      FileUtils.verify
-    end
-  end
-
-  def test_get_last_build
-    with_sandbox_project do |sandbox, project|
-      sandbox.new :file =&gt; &quot;build-1/build_status = success&quot;
-      sandbox.new :file =&gt; &quot;build-2/build_status = success&quot;
-  
-      one, two = Build.new(project, 1), Build.new(project, 2)
-  
-      assert_equal 1, one.label
-      assert_equal nil, one.last
-      assert_equal 1, two.last.label
-    end
-  end
-  
-  def test_status
-    with_sandbox_project do |sandbox, project|
-      Status.any_instance.expects(:to_s)
-      Build.new(project, 123).status
-    end
-  end
-
-  def test_build_command_customization
-    with_sandbox_project do |sandbox, project|
-      build_with_defaults = Build.new(project, '1')
-      assert_match(/cc_build.rake'; ARGV &lt;&lt; '--nosearch' &lt;&lt; 'cc:build'/, build_with_defaults.command)
-      assert_nil build_with_defaults.rake_task
-  
-      project.rake_task = 'my_build_task'
-      build_with_custom_rake_task = Build.new(project, '2')
-      assert_match(/cc_build.rake'; ARGV &lt;&lt; '--nosearch' &lt;&lt; 'cc:build'/, build_with_custom_rake_task.command)
-      assert_equal 'my_build_task', build_with_custom_rake_task.rake_task
-  
-      project.rake_task = nil
-      project.build_command = 'my_build_script.sh'
-      build_with_custom_script = Build.new(project, '3')
-      assert_equal 'my_build_script.sh', build_with_custom_script.command
-      assert_nil build_with_custom_script.rake_task
-    end
-
-  end
-
-end
+require File.dirname(__FILE__) + '/../test_helper'
+
+class BuildTest &lt; Test::Unit::TestCase
+  include FileSandbox
+
+  def test_initialize_should_load_status_file_and_build_log
+    with_sandbox_project do |sandbox, project|
+      sandbox.new :file =&gt; &quot;build-2/build_status = success&quot;
+      sandbox.new :file =&gt; &quot;build-2/build.log&quot;, :with_content =&gt; &quot;some content&quot;
+      build = Build.new(project, 2)
+  
+      assert_equal 2, build.label
+      assert_equal true, build.successful?
+      assert_equal &quot;some content&quot;, build.output
+    end
+  end
+
+  def test_initialize_should_load__failed_status_file
+    with_sandbox_project do |sandbox, project|
+      sandbox.new :file =&gt; &quot;build-2/build_status = failed&quot;
+      build = Build.new(project, 2)
+  
+      assert_equal 2, build.label
+      assert_equal true, build.failed?
+    end
+  end
+
+  def test_output_grabs_log_file_when_file_exists
+    with_sandbox_project do |sandbox, project|
+      File.expects(:'read').with(&quot;#{project.path}/build-1/build.log&quot;).returns(['line 1', 'line 2'])
+      assert_equal ['line 1', 'line 2'], Build.new(project, 1).output
+    end
+  end
+  
+  def test_output_gives_empty_string_when_file_does_not_exist
+    with_sandbox_project do |sandbox, project|
+      File.expects(:'read').with(&quot;#{project.path}/build-1/build.log&quot;).raises(StandardError)
+      assert_equal &quot;&quot;, Build.new(project, 1).output
+    end
+  end
+  
+  def test_coverage_reports_reads_correct_coverage_log_file_by_correct_name
+    with_sandbox_project do |sandbox, project|
+      File.expects(:'read').with(&quot;#{project.path}/build-1/coverage-foo.log&quot;).returns(['line 1', 'line 2'])
+      assert_equal ['line 1', 'line 2'], Build.new(project, 1).coverage_reports[:foo]
+    end
+  end
+  
+  def test_coverage_reports_when_file_does_not_exist
+    with_sandbox_project do |sandbox, project|
+      File.expects(:'read').with(&quot;#{project.path}/build-1/coverage-units.log&quot;).raises(StandardError)
+      assert_equal &quot;&quot;, Build.new(project, 1).coverage_reports[:units]
+    end
+  end
+
+  def test_successful?
+    with_sandbox_project do |sandbox, project|
+      sandbox.new :file =&gt; &quot;build-1/build_status = success&quot;
+      sandbox.new :file =&gt; &quot;build-2/build_status = Success&quot;
+      sandbox.new :file =&gt; &quot;build-3/build_status = failure&quot;
+      sandbox.new :file =&gt; &quot;build-4/build_status = crap&quot;
+      sandbox.new :file =&gt; &quot;build-5/foo&quot;
+  
+      assert Build.new(project, 1).successful?
+      assert Build.new(project, 2).successful?
+      assert !Build.new(project, 3).successful?
+      assert !Build.new(project, 4).successful?
+      assert !Build.new(project, 5).successful?
+    end
+  end
+
+  def test_nil_build
+    assert_equal '-', Build.nil.time
+    assert_equal '-', Build.nil.label
+    assert_equal '-', Build.nil.output
+    assert_equal :never_built, Build.nil.status
+  end
+
+  def test_formatted_time_when_status_file_does_not_exist
+    with_sandbox_project do |sandbox, project|
+      assert_equal '-', Build.new(project, 1).formatted_time
+    end
+  end
+  
+  def test_formatted_time_when_status_file_exists
+    with_sandbox_project do |sandbox, project|
+      sandbox.new :file =&gt; &quot;build-1/build_status = success&quot;
+      Status.any_instance.expects(:created_at).returns(Time.local(2006, 5, 3, 14, 33, 50))
+      assert_equal &quot;02:33 PM May 03, 2006&quot;, Build.new(project, 1).formatted_time
+    end
+  end
+  
+  def test_run_successful_build
+    with_sandbox_project do |sandbox, project|
+      expected_build_directory = File.join(sandbox.root, 'build-123')
+  
+      FileUtils.expects(:mkdir_p).with(expected_build_directory).returns(expected_build_directory)
+  
+      build = Build.new(project, 123)
+  
+      expected_command = build.rake
+      expected_build_log = File.join(expected_build_directory, 'build.log')
+      expected_redirect_options = {
+          :stdout =&gt; expected_build_log,
+          :stderr =&gt; expected_build_log,
+          :escape_quotes =&gt; false
+        }
+      
+      build.expects(:execute).with(build.rake, expected_redirect_options).returns(&quot;hi, mom!&quot;)
+      Status.any_instance.expects(:'succeed!')
+  
+      build.run
+  
+      build.verify
+      FileUtils.verify
+    end
+  end
+
+  def test_run_unsuccessful_build
+    with_sandbox_project do |sandbox, project|
+      expected_build_directory = File.join(sandbox.root, 'build-123')
+  
+      FileUtils.expects(:mkdir_p).with(expected_build_directory).returns(expected_build_directory)
+  
+      build = Build.new(project, 123)
+  
+      expected_build_log = File.join(expected_build_directory, 'build.log')
+      expected_redirect_options = {
+        :stdout =&gt; expected_build_log,
+        :stderr =&gt; expected_build_log,
+        :escape_quotes =&gt; false
+      }
+  
+      build.expects(:execute).with(build.rake, expected_redirect_options).raises(CommandLine::ExecutionError)
+      Status.any_instance.expects(:'fail!')
+  
+      build.run
+  
+      build.verify
+      FileUtils.verify
+    end
+  end
+
+  def test_get_last_build
+    with_sandbox_project do |sandbox, project|
+      sandbox.new :file =&gt; &quot;build-1/build_status = success&quot;
+      sandbox.new :file =&gt; &quot;build-2/build_status = success&quot;
+  
+      one, two = Build.new(project, 1), Build.new(project, 2)
+  
+      assert_equal 1, one.label
+      assert_equal nil, one.last
+      assert_equal 1, two.last.label
+    end
+  end
+  
+  def test_status
+    with_sandbox_project do |sandbox, project|
+      Status.any_instance.expects(:to_s)
+      Build.new(project, 123).status
+    end
+  end
+
+  def test_build_command_customization
+    with_sandbox_project do |sandbox, project|
+      build_with_defaults = Build.new(project, '1')
+      assert_match(/cc_build.rake'; ARGV &lt;&lt; '--nosearch' &lt;&lt; 'cc:build'/, build_with_defaults.command)
+      assert_nil build_with_defaults.rake_task
+  
+      project.rake_task = 'my_build_task'
+      build_with_custom_rake_task = Build.new(project, '2')
+      assert_match(/cc_build.rake'; ARGV &lt;&lt; '--nosearch' &lt;&lt; 'cc:build'/, build_with_custom_rake_task.command)
+      assert_equal 'my_build_task', build_with_custom_rake_task.rake_task
+  
+      project.rake_task = nil
+      project.build_command = 'my_build_script.sh'
+      build_with_custom_script = Build.new(project, '3')
+      assert_equal 'my_build_script.sh', build_with_custom_script.command
+      assert_nil build_with_custom_script.rake_task
+    end
+
+  end
+
+end</diff>
      <filename>test/unit/build_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,83 +1,83 @@
-require File.dirname(__FILE__) + '/../test_helper'
-require 'fileutils'
-
-class CommandLineTest &lt; Test::Unit::TestCase
-  include FileSandbox
-
-  def test_should_write_to_both_files_when_both_files_specified_and_no_block
-    in_total_sandbox do
-      CommandLine.execute(&quot;echo \&quot;&lt;hello\&quot; &amp;&amp; echo world&quot;, {:dir =&gt; @dir, :stdout =&gt; @stdout, :stderr =&gt; @stderr})
-      assert_match(/.* echo \&quot;&lt;hello\&quot;\s*\n.?\&lt;hello.?\s*\n.* echo world\s*\nworld/n, File.read(@stdout))
-      assert_match(/.* echo \&quot;&lt;hello\&quot;\s*\n.* echo world\s*/n, File.read(@stderr))
-    end
-  end
-
-  def test_should_not_write_to_stdout_file_when_no_stdout_specified
-    in_total_sandbox do
-      with_redirected_stdout do
-        CommandLine.execute(&quot;echo hello&quot;, {:dir =&gt; @dir, :stderr =&gt; @stderr})
-      end
-      assert_equal(&quot;hello&quot;, File.read(@stdout).strip)
-#      assert_equal(&quot;#{@prompt} echo hello\nhello&quot;, File.read(@stdout).strip)
-      assert_equal(&quot;#{@prompt} echo hello&quot;, File.read(@stderr).strip)
-    end
-  end
-
-  def test_should_only_write_command_to_stdout_when_block_specified
-    in_total_sandbox do
-      CommandLine.execute(&quot;echo hello&quot;, {:dir =&gt; @dir, :stdout =&gt; @stdout, :stderr =&gt; @stderr}) do |io|
-        assert_equal(&quot;hello&quot;, io.read.strip)
-      end
-      assert_match(/.* echo hello\s*\[output captured and therefore not logged\]/n, File.read(@stdout).strip)
-      assert_equal(&quot;#{@prompt} echo hello&quot;, File.read(@stderr).strip)
-    end
-  end
-
-  def test_should_raise_on_bad_command
-    in_total_sandbox do
-      assert_raise(CommandLine::ExecutionError) do
-        CommandLine.execute(&quot;xaswedf&quot;, {:dir =&gt; @dir, :stdout =&gt; @stdout, :stderr =&gt; @stderr})
-      end
-    end
-  end
-
-  def test_should_raise_on_bad_command_with_block
-    in_total_sandbox do
-      assert_raise(CommandLine::ExecutionError) do
-        CommandLine.execute(&quot;xaswedf&quot;, {:dir =&gt; @dir, :stdout =&gt; @stdout, :stderr =&gt; @stderr}) do |io|
-          io.each_line do |line|
-          end
-        end
-      end
-    end
-  end
-
-  def test_should_return_block_result
-    in_total_sandbox do
-      result = CommandLine.execute(&quot;echo hello&quot;, {:dir =&gt; @dir, :stdout =&gt; @stdout, :stderr =&gt; @stderr}) do |io|
-        io.read
-      end
-      assert_equal &quot;hello&quot;, result.strip
-    end
-  end
-
-  def test_execute_should_raise_when_return_code_is_not_zero
-    in_total_sandbox do
-      with_redirected_stdout do
-        assert_raise(CommandLine::ExecutionError) do
-          CommandLine.execute &quot;ruby -e 'exit(-1)'&quot;
-        end
-      end
-    end
-  end
-
-  def with_redirected_stdout
-    orgout = STDOUT.dup
-    STDOUT.reopen(@stdout)
-    begin
-      yield
-    ensure
-      STDOUT.reopen(orgout) rescue nil
-    end
-  end
+require File.dirname(__FILE__) + '/../test_helper'
+require 'fileutils'
+
+class CommandLineTest &lt; Test::Unit::TestCase
+  include FileSandbox
+
+  def test_should_write_to_both_files_when_both_files_specified_and_no_block
+    in_total_sandbox do
+      CommandLine.execute(&quot;echo \&quot;&lt;hello\&quot; &amp;&amp; echo world&quot;, {:dir =&gt; @dir, :stdout =&gt; @stdout, :stderr =&gt; @stderr})
+      assert_match(/.* echo \&quot;&lt;hello\&quot;\s*\n.?\&lt;hello.?\s*\n.* echo world\s*\nworld/n, File.read(@stdout))
+      assert_match(/.* echo \&quot;&lt;hello\&quot;\s*\n.* echo world\s*/n, File.read(@stderr))
+    end
+  end
+
+  def test_should_not_write_to_stdout_file_when_no_stdout_specified
+    in_total_sandbox do
+      with_redirected_stdout do
+        CommandLine.execute(&quot;echo hello&quot;, {:dir =&gt; @dir, :stderr =&gt; @stderr})
+      end
+      assert_equal(&quot;hello&quot;, File.read(@stdout).strip)
+#      assert_equal(&quot;#{@prompt} echo hello\nhello&quot;, File.read(@stdout).strip)
+      assert_equal(&quot;#{@prompt} echo hello&quot;, File.read(@stderr).strip)
+    end
+  end
+
+  def test_should_only_write_command_to_stdout_when_block_specified
+    in_total_sandbox do
+      CommandLine.execute(&quot;echo hello&quot;, {:dir =&gt; @dir, :stdout =&gt; @stdout, :stderr =&gt; @stderr}) do |io|
+        assert_equal(&quot;hello&quot;, io.read.strip)
+      end
+      assert_match(/.* echo hello\s*\[output captured and therefore not logged\]/n, File.read(@stdout).strip)
+      assert_equal(&quot;#{@prompt} echo hello&quot;, File.read(@stderr).strip)
+    end
+  end
+
+  def test_should_raise_on_bad_command
+    in_total_sandbox do
+      assert_raise(CommandLine::ExecutionError) do
+        CommandLine.execute(&quot;xaswedf&quot;, {:dir =&gt; @dir, :stdout =&gt; @stdout, :stderr =&gt; @stderr})
+      end
+    end
+  end
+
+  def test_should_raise_on_bad_command_with_block
+    in_total_sandbox do
+      assert_raise(CommandLine::ExecutionError) do
+        CommandLine.execute(&quot;xaswedf&quot;, {:dir =&gt; @dir, :stdout =&gt; @stdout, :stderr =&gt; @stderr}) do |io|
+          io.each_line do |line|
+          end
+        end
+      end
+    end
+  end
+
+  def test_should_return_block_result
+    in_total_sandbox do
+      result = CommandLine.execute(&quot;echo hello&quot;, {:dir =&gt; @dir, :stdout =&gt; @stdout, :stderr =&gt; @stderr}) do |io|
+        io.read
+      end
+      assert_equal &quot;hello&quot;, result.strip
+    end
+  end
+
+  def test_execute_should_raise_when_return_code_is_not_zero
+    in_total_sandbox do
+      with_redirected_stdout do
+        assert_raise(CommandLine::ExecutionError) do
+          CommandLine.execute &quot;ruby -e 'exit(-1)'&quot;
+        end
+      end
+    end
+  end
+
+  def with_redirected_stdout
+    orgout = STDOUT.dup
+    STDOUT.reopen(@stdout)
+    begin
+      yield
+    ensure
+      STDOUT.reopen(orgout) rescue nil
+    end
+  end
 end
\ No newline at end of file</diff>
      <filename>test/unit/command_line_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,63 +1,63 @@
-require File.dirname(__FILE__) + '/../test_helper'
-
-class EmailNotifierTest &lt; Test::Unit::TestCase
-  include FileSandbox
-  
-  BUILD_LOG = &lt;&lt;-EOL
-    blah blah blah
-    something built
-    tests passed / failed / etc
-  EOL
-
-  def setup
-    setup_sandbox
-
-    ActionMailer::Base.deliveries = []
-
-    @project = Project.new(&quot;myproj&quot;, nil, nil)
-    @project.path = @sandbox.root
-    @build = Build.new(@project, 5)
-
-    @notifier = @project.email_notifier
-    @notifier.emails = [&quot;jeremystellsmith@gmail.com&quot;, &quot;jeremy@thoughtworks.com&quot;]
-  end
-  
-  def teardown
-    teardown_sandbox
-  end
-
-  def test_do_nothing_with_passing_build
-    @notifier.build_finished(@build)
-
-    assert_equal [], ActionMailer::Base.deliveries
-  end
-
-  def test_send_email_with_failing_build
-    @build.expects(:failed?).returns(true)
-    @build.expects(:output).returns(BUILD_LOG)
-
-    @notifier.build_finished(@build)
-
-    mail = ActionMailer::Base.deliveries[0]
-
-    assert_equal @notifier.emails, mail.to
-    assert_equal &quot;myproj Build 5 - FAILED&quot;, mail.subject
-    assert_equal BUILD_LOG, mail.body
-  end
-
-  def test_send_email_with_fixed_build
-    last_build = Build.new(@project, 4)
-    last_build.expects(:failed?).returns(true)
-
-    @build.expects(:last).returns(last_build)
-    @build.expects(:output).returns(BUILD_LOG)
-
-    @notifier.build_finished(@build)
-
-    mail = ActionMailer::Base.deliveries[0]
-
-    assert_equal @notifier.emails, mail.to
-    assert_equal &quot;myproj Build 5 - FIXED&quot;, mail.subject
-    assert_equal BUILD_LOG, mail.body
-  end
+require File.dirname(__FILE__) + '/../test_helper'
+
+class EmailNotifierTest &lt; Test::Unit::TestCase
+  include FileSandbox
+  
+  BUILD_LOG = &lt;&lt;-EOL
+    blah blah blah
+    something built
+    tests passed / failed / etc
+  EOL
+
+  def setup
+    setup_sandbox
+
+    ActionMailer::Base.deliveries = []
+
+    @project = Project.new(&quot;myproj&quot;, nil, nil)
+    @project.path = @sandbox.root
+    @build = Build.new(@project, 5)
+
+    @notifier = @project.email_notifier
+    @notifier.emails = [&quot;jeremystellsmith@gmail.com&quot;, &quot;jeremy@thoughtworks.com&quot;]
+  end
+  
+  def teardown
+    teardown_sandbox
+  end
+
+  def test_do_nothing_with_passing_build
+    @notifier.build_finished(@build)
+
+    assert_equal [], ActionMailer::Base.deliveries
+  end
+
+  def test_send_email_with_failing_build
+    @build.expects(:failed?).returns(true)
+    @build.expects(:output).returns(BUILD_LOG)
+
+    @notifier.build_finished(@build)
+
+    mail = ActionMailer::Base.deliveries[0]
+
+    assert_equal @notifier.emails, mail.to
+    assert_equal &quot;myproj Build 5 - FAILED&quot;, mail.subject
+    assert_equal BUILD_LOG, mail.body
+  end
+
+  def test_send_email_with_fixed_build
+    last_build = Build.new(@project, 4)
+    last_build.expects(:failed?).returns(true)
+
+    @build.expects(:last).returns(last_build)
+    @build.expects(:output).returns(BUILD_LOG)
+
+    @notifier.build_finished(@build)
+
+    mail = ActionMailer::Base.deliveries[0]
+
+    assert_equal @notifier.emails, mail.to
+    assert_equal &quot;myproj Build 5 - FIXED&quot;, mail.subject
+    assert_equal BUILD_LOG, mail.body
+  end
 end
\ No newline at end of file</diff>
      <filename>test/unit/email_notifier_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,45 +1,45 @@
-require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
-
-class ProjectBlockerTest &lt; Test::Unit::TestCase
-  include FileSandbox
-  
-  def test_block_release
-    in_total_sandbox do |sandbox|
-      project = Object.new
-      project.stubs(:name).returns('foo')
-      project.stubs(:path).returns(sandbox.root)
-
-      begin
-        ProjectBlocker.block(project)
-        
-        expected_pid_file = &quot;#{sandbox.root}/builder.pid&quot;
-        assert File.file?(expected_pid_file)
-        assert_equal false, File.open(expected_pid_file, 'w') { |f| f.flock(File::LOCK_EX | File::LOCK_NB) }
-        assert_raises(&quot;Already holding a lock on project 'foo'&quot;) { ProjectBlocker.block(project) }
-
-        ProjectBlocker.release(project)
-  
-        assert_equal false, File.exists?(expected_pid_file)
-        assert_nothing_raised do 
-          File.open(expected_pid_file, 'w') { |f| f.puts 'blah' }
-        end
-        
-        lock = File.open(expected_pid_file, 'w')
-        begin
-          assert_equal 0, lock.flock(File::LOCK_EX | File::LOCK_NB)
-          
-          assert_raises(/^Another process (probably another builder) holds a lock on project 'foo'/) do
-            ProjectBlocker.block(project)
-          end
-        ensure
-          lock.flock(File::LOCK_UN) rescue nil
-          lock.close rescue nil
-        end
-        
-      ensure
-        ProjectBlocker.release(project) rescue nil
-      end
-    end
-  end
-  
+require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
+
+class ProjectBlockerTest &lt; Test::Unit::TestCase
+  include FileSandbox
+  
+  def test_block_release
+    in_total_sandbox do |sandbox|
+      project = Object.new
+      project.stubs(:name).returns('foo')
+      project.stubs(:path).returns(sandbox.root)
+
+      begin
+        ProjectBlocker.block(project)
+        
+        expected_pid_file = &quot;#{sandbox.root}/builder.pid&quot;
+        assert File.file?(expected_pid_file)
+        assert_equal false, File.open(expected_pid_file, 'w') { |f| f.flock(File::LOCK_EX | File::LOCK_NB) }
+        assert_raises(&quot;Already holding a lock on project 'foo'&quot;) { ProjectBlocker.block(project) }
+
+        ProjectBlocker.release(project)
+  
+        assert_equal false, File.exists?(expected_pid_file)
+        assert_nothing_raised do 
+          File.open(expected_pid_file, 'w') { |f| f.puts 'blah' }
+        end
+        
+        lock = File.open(expected_pid_file, 'w')
+        begin
+          assert_equal 0, lock.flock(File::LOCK_EX | File::LOCK_NB)
+          
+          assert_raises(/^Another process (probably another builder) holds a lock on project 'foo'/) do
+            ProjectBlocker.block(project)
+          end
+        ensure
+          lock.flock(File::LOCK_UN) rescue nil
+          lock.close rescue nil
+        end
+        
+      ensure
+        ProjectBlocker.release(project) rescue nil
+      end
+    end
+  end
+  
 end
\ No newline at end of file</diff>
      <filename>test/unit/project_blocker_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,67 +1,67 @@
-require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
-
-class ProjectLoggerTest &lt; Test::Unit::TestCase
-  
-  def setup
-    @logger = ProjectLogger.new(nil)
-    @mock_build = Object.new
-  end
-
-  def test_build_started
-    @mock_build.expects(:label).returns(123)
-    Log.expects(:event).with(&quot;Build 123 started&quot;)
-
-    @logger.build_started(@mock_build)
-
-    Log.verify
-  end
-  
-  def test_build_finished_with_success
-    @mock_build.expects(:label).returns(123)
-    @mock_build.expects(:successful?).returns(true)
-    Log.expects(:event).with(&quot;Build 123 finished SUCCESSFULLY&quot;)
-
-    @logger.build_finished(@mock_build)
-
-    Log.verify
-  end  
-
-  def test_build_finished_with_failure
-    @mock_build.expects(:label).returns(123)
-    @mock_build.expects(:successful?).returns(false)
-    Log.expects(:event).with(&quot;Build 123 FAILED&quot;)
-
-    @logger.build_finished(@mock_build)
-
-    Log.verify
-  end  
-  
-  def test_sleeping
-    Log.expects(:event).with(&quot;Sleeping&quot;, :debug)
-    @logger.sleeping
-    Log.verify
-  end
-
-  def test_polling_source_control
-    Log.expects(:event).with(&quot;Polling source control&quot;, :debug)
-    @logger.polling_source_control
-    Log.verify
-  end
-  
-  def test_no_new_revisions_detected
-    Log.expects(:event).with(&quot;No new revisions detected&quot;, :debug)
-    @logger.no_new_revisions_detected
-    Log.verify
-  end
-
-  def test_new_revisions_detected
-    @mock_revision = Object.new
-    @mock_revision.expects(:number).returns(9)
-    Log.expects(:event).with(&quot;New revision 9 detected&quot;)
-
-    @logger.new_revisions_detected([@mock_revision])
-
-    Log.verify
-  end
-
+require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
+
+class ProjectLoggerTest &lt; Test::Unit::TestCase
+  
+  def setup
+    @logger = ProjectLogger.new(nil)
+    @mock_build = Object.new
+  end
+
+  def test_build_started
+    @mock_build.expects(:label).returns(123)
+    Log.expects(:event).with(&quot;Build 123 started&quot;)
+
+    @logger.build_started(@mock_build)
+
+    Log.verify
+  end
+  
+  def test_build_finished_with_success
+    @mock_build.expects(:label).returns(123)
+    @mock_build.expects(:successful?).returns(true)
+    Log.expects(:event).with(&quot;Build 123 finished SUCCESSFULLY&quot;)
+
+    @logger.build_finished(@mock_build)
+
+    Log.verify
+  end  
+
+  def test_build_finished_with_failure
+    @mock_build.expects(:label).returns(123)
+    @mock_build.expects(:successful?).returns(false)
+    Log.expects(:event).with(&quot;Build 123 FAILED&quot;)
+
+    @logger.build_finished(@mock_build)
+
+    Log.verify
+  end  
+  
+  def test_sleeping
+    Log.expects(:event).with(&quot;Sleeping&quot;, :debug)
+    @logger.sleeping
+    Log.verify
+  end
+
+  def test_polling_source_control
+    Log.expects(:event).with(&quot;Polling source control&quot;, :debug)
+    @logger.polling_source_control
+    Log.verify
+  end
+  
+  def test_no_new_revisions_detected
+    Log.expects(:event).with(&quot;No new revisions detected&quot;, :debug)
+    @logger.no_new_revisions_detected
+    Log.verify
+  end
+
+  def test_new_revisions_detected
+    @mock_revision = Object.new
+    @mock_revision.expects(:number).returns(9)
+    Log.expects(:event).with(&quot;New revision 9 detected&quot;)
+
+    @logger.new_revisions_detected([@mock_revision])
+
+    Log.verify
+  end
+
 end
\ No newline at end of file</diff>
      <filename>test/unit/project_logger_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,198 +1,198 @@
-require 'date'
-require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
-
-class ProjectTest &lt; Test::Unit::TestCase
-  include FileSandbox
-  
-  def setup
-    @svn = Subversion.new(:url =&gt; 'file://foo', :username =&gt; 'bob', :password =&gt; 'cha')
-    @project = Project.new(&quot;lemmings&quot;, @svn)
-  end
-
-  def test_properties
-    assert_equal(&quot;lemmings&quot;, @project.name)
-    assert_equal(&quot;bob&quot;, @project.source_control.username)
-  end
-
-  def test_memento_with_all_defaults
-    default_project = Project.new(&quot;hamsters&quot;, Subversion.new)
-    
-    expected_result = &lt;&lt;-EOL
-Project.configure do |project|
-  project.email_notifier.emails = [
-
-  ]
-end
-    EOL
-    assert_equal expected_result, default_project.memento
-  end
-
-  def test_memento_with_email_notifications
-    expected_result = &lt;&lt;-EOL
-Project.configure do |project|
-  project.source_control = Subversion.new(:url =&gt; 'file://foo', :username =&gt; 'bob', :password =&gt; 'cha')
-  project.email_notifier.emails = [
-    &quot;jss@thoughtworks.com&quot;,
-    &quot;andrew@gmail.com&quot;,
-    &quot;bob@andrews.com&quot;
-  ]
-end
-    EOL
-
-    @project.email_notifier.emails &lt;&lt; &quot;jss@thoughtworks.com&quot; &lt;&lt; &quot;andrew@gmail.com&quot; &lt;&lt; &quot;bob@andrews.com&quot;
-    assert_equal expected_result, @project.memento
-  end
-
-  def test_memento_with_custom_polling_interval
-    expected_result = &lt;&lt;-EOL
-Project.configure do |project|
-  project.source_control = Subversion.new(:url =&gt; 'file://foo', :username =&gt; 'bob', :password =&gt; 'cha')
-  project.scheduler.polling_interval = 30.seconds
-  project.email_notifier.emails = [
-
-  ]
-end
-    EOL
-    
-    @project.scheduler.polling_interval = 30
-    assert_equal expected_result, @project.memento
-  end
-
-  def test_default_scheduler
-    assert_equal PollingScheduler, @project.scheduler.class
-  end
-
-  def test_builds
-    in_sandbox do |sandbox|
-      @project.path = sandbox.root
-
-      sandbox.new :file =&gt; &quot;build-1/build_status = success&quot;
-      sandbox.new :file =&gt; &quot;build-10/build_status = success&quot;
-      sandbox.new :file =&gt; &quot;build-3/build_status = failure&quot;
-      sandbox.new :file =&gt; &quot;build-5/build_status = success&quot;
-
-      assert_equal(&quot;1 - success, 3 - failure, 5 - success, 10 - success&quot;,
-                   @project.builds.collect {|b| &quot;#{b.label} - #{b.status}&quot;}.join(&quot;, &quot;))
-
-      assert_equal(10, @project.last_build.label)
-    end
-  end
-
-  def test_builds_should_return_empty_array_when_project_has_no_builds
-    in_sandbox do |sandbox|
-      @project.path = sandbox.root
-      assert_equal [], @project.builds
-    end
-  end
-
-  def test_should_build_with_no_logs
-    in_sandbox do |sandbox|
-      @project.path = sandbox.root
-
-      revision = new_revision(5)
-      build = new_mock_build(5)
-      build.stubs(:artifacts_directory).returns(sandbox.root)
-
-      @project.expects(:builds).returns([])
-      @svn.expects(:latest_revision).returns(revision)
-      @svn.expects(:update).with(@project, revision)
-
-      build.expects(:run)
-
-      @project.build_if_necessary
-
-      @svn.verify
-      build.verify
-    end
-  end
-
-  def test_build_if_necessary_should_generate_events
-    in_sandbox do |sandbox|
-      @project.path = sandbox.root
-
-      revision = new_revision(5)
-      build = new_mock_build(5)
-      build.stubs(:artifacts_directory).returns(sandbox.root)
-
-      @project.expects(:builds).returns([])
-      @svn.expects(:latest_revision).returns(revision)
-      @svn.expects(:update).with(@project, revision)
-
-      build.expects(:run)
-
-      # event expectations
-      listener = Object.new
-
-      listener.expects(:polling_source_control)
-      listener.expects(:new_revisions_detected).with([revision])
-      listener.expects(:build_started).with(build)
-      listener.expects(:build_finished).with(build)
-      listener.expects(:sleeping)
-
-      @project.add_plugin listener
-
-      @project.build_if_necessary
-
-      listener.verify
-    end
-  end
-
-  def test_should_build_when_logs_are_not_current
-    in_sandbox do |sandbox|
-      @project.path = sandbox.root
-
-      @project.expects(:builds).returns([Build.new(@project, 1)])
-      revision = new_revision(2)
-      build = new_mock_build(2)
-      build.stubs(:artifacts_directory).returns(sandbox.root)
-
-      @svn.expects(:revisions_since).with(@project, 1).returns([revision])
-      @svn.expects(:update).with(@project, revision)
-
-      build.expects(:run)
-
-      @project.build_if_necessary
-
-      @svn.verify
-      build.verify
-    end
-  end
-
-  def test_should_not_build_when_logs_are_current
-    in_sandbox do |sandbox|
-      @project.path = sandbox.root
-
-      @project.expects(:builds).returns([Build.new(@project, 2)])
-      revision = new_revision(2)
-
-      @svn.expects(:revisions_since).with(@project, 2).returns([])
-
-      @project.build_if_necessary
-
-      @svn.verify
-    end
-  end
-  
-  def test_either_rake_task_or_build_command_can_be_set_but_not_both
-    @project.rake_task = 'foo'
-    assert_raises(&quot;Cannot set build_command when rake_task is already defined&quot;) do
-      @project.build_command = 'foo'
-    end
-
-    @project.rake_task = nil
-    @project.build_command = 'foo'
-    assert_raises(&quot;Cannot set rake_task when build_command is already defined&quot;) do
-      @project.rake_task = 'foo'
-    end
-  end
-
-  def new_revision(number)
-    Revision.new(number, 'alex', DateTime.new(2005, 1, 1), 'message', [])
-  end
-
-  def new_mock_build(number)
-    build = Object.new
-    Build.expects(:new).with(@project, number).returns(build)
-    build
-  end
-end
+require 'date'
+require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
+
+class ProjectTest &lt; Test::Unit::TestCase
+  include FileSandbox
+  
+  def setup
+    @svn = Subversion.new(:url =&gt; 'file://foo', :username =&gt; 'bob', :password =&gt; 'cha')
+    @project = Project.new(&quot;lemmings&quot;, @svn)
+  end
+
+  def test_properties
+    assert_equal(&quot;lemmings&quot;, @project.name)
+    assert_equal(&quot;bob&quot;, @project.source_control.username)
+  end
+
+  def test_memento_with_all_defaults
+    default_project = Project.new(&quot;hamsters&quot;, Subversion.new)
+    
+    expected_result = &lt;&lt;-EOL
+Project.configure do |project|
+  project.email_notifier.emails = [
+
+  ]
+end
+    EOL
+    assert_equal expected_result, default_project.memento
+  end
+
+  def test_memento_with_email_notifications
+    expected_result = &lt;&lt;-EOL
+Project.configure do |project|
+  project.source_control = Subversion.new(:url =&gt; 'file://foo', :username =&gt; 'bob', :password =&gt; 'cha')
+  project.email_notifier.emails = [
+    &quot;jss@thoughtworks.com&quot;,
+    &quot;andrew@gmail.com&quot;,
+    &quot;bob@andrews.com&quot;
+  ]
+end
+    EOL
+
+    @project.email_notifier.emails &lt;&lt; &quot;jss@thoughtworks.com&quot; &lt;&lt; &quot;andrew@gmail.com&quot; &lt;&lt; &quot;bob@andrews.com&quot;
+    assert_equal expected_result, @project.memento
+  end
+
+  def test_memento_with_custom_polling_interval
+    expected_result = &lt;&lt;-EOL
+Project.configure do |project|
+  project.source_control = Subversion.new(:url =&gt; 'file://foo', :username =&gt; 'bob', :password =&gt; 'cha')
+  project.scheduler.polling_interval = 30.seconds
+  project.email_notifier.emails = [
+
+  ]
+end
+    EOL
+    
+    @project.scheduler.polling_interval = 30
+    assert_equal expected_result, @project.memento
+  end
+
+  def test_default_scheduler
+    assert_equal PollingScheduler, @project.scheduler.class
+  end
+
+  def test_builds
+    in_sandbox do |sandbox|
+      @project.path = sandbox.root
+
+      sandbox.new :file =&gt; &quot;build-1/build_status = success&quot;
+      sandbox.new :file =&gt; &quot;build-10/build_status = success&quot;
+      sandbox.new :file =&gt; &quot;build-3/build_status = failure&quot;
+      sandbox.new :file =&gt; &quot;build-5/build_status = success&quot;
+
+      assert_equal(&quot;1 - success, 3 - failure, 5 - success, 10 - success&quot;,
+                   @project.builds.collect {|b| &quot;#{b.label} - #{b.status}&quot;}.join(&quot;, &quot;))
+
+      assert_equal(10, @project.last_build.label)
+    end
+  end
+
+  def test_builds_should_return_empty_array_when_project_has_no_builds
+    in_sandbox do |sandbox|
+      @project.path = sandbox.root
+      assert_equal [], @project.builds
+    end
+  end
+
+  def test_should_build_with_no_logs
+    in_sandbox do |sandbox|
+      @project.path = sandbox.root
+
+      revision = new_revision(5)
+      build = new_mock_build(5)
+      build.stubs(:artifacts_directory).returns(sandbox.root)
+
+      @project.expects(:builds).returns([])
+      @svn.expects(:latest_revision).returns(revision)
+      @svn.expects(:update).with(@project, revision)
+
+      build.expects(:run)
+
+      @project.build_if_necessary
+
+      @svn.verify
+      build.verify
+    end
+  end
+
+  def test_build_if_necessary_should_generate_events
+    in_sandbox do |sandbox|
+      @project.path = sandbox.root
+
+      revision = new_revision(5)
+      build = new_mock_build(5)
+      build.stubs(:artifacts_directory).returns(sandbox.root)
+
+      @project.expects(:builds).returns([])
+      @svn.expects(:latest_revision).returns(revision)
+      @svn.expects(:update).with(@project, revision)
+
+      build.expects(:run)
+
+      # event expectations
+      listener = Object.new
+
+      listener.expects(:polling_source_control)
+      listener.expects(:new_revisions_detected).with([revision])
+      listener.expects(:build_started).with(build)
+      listener.expects(:build_finished).with(build)
+      listener.expects(:sleeping)
+
+      @project.add_plugin listener
+
+      @project.build_if_necessary
+
+      listener.verify
+    end
+  end
+
+  def test_should_build_when_logs_are_not_current
+    in_sandbox do |sandbox|
+      @project.path = sandbox.root
+
+      @project.expects(:builds).returns([Build.new(@project, 1)])
+      revision = new_revision(2)
+      build = new_mock_build(2)
+      build.stubs(:artifacts_directory).returns(sandbox.root)
+
+      @svn.expects(:revisions_since).with(@project, 1).returns([revision])
+      @svn.expects(:update).with(@project, revision)
+
+      build.expects(:run)
+
+      @project.build_if_necessary
+
+      @svn.verify
+      build.verify
+    end
+  end
+
+  def test_should_not_build_when_logs_are_current
+    in_sandbox do |sandbox|
+      @project.path = sandbox.root
+
+      @project.expects(:builds).returns([Build.new(@project, 2)])
+      revision = new_revision(2)
+
+      @svn.expects(:revisions_since).with(@project, 2).returns([])
+
+      @project.build_if_necessary
+
+      @svn.verify
+    end
+  end
+  
+  def test_either_rake_task_or_build_command_can_be_set_but_not_both
+    @project.rake_task = 'foo'
+    assert_raises(&quot;Cannot set build_command when rake_task is already defined&quot;) do
+      @project.build_command = 'foo'
+    end
+
+    @project.rake_task = nil
+    @project.build_command = 'foo'
+    assert_raises(&quot;Cannot set rake_task when build_command is already defined&quot;) do
+      @project.rake_task = 'foo'
+    end
+  end
+
+  def new_revision(number)
+    Revision.new(number, 'alex', DateTime.new(2005, 1, 1), 'message', [])
+  end
+
+  def new_mock_build(number)
+    build = Object.new
+    Build.expects(:new).with(@project, number).returns(build)
+    build
+  end
+end</diff>
      <filename>test/unit/project_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,131 +1,131 @@
-require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
-
-class ProjectsTest &lt; Test::Unit::TestCase
-  include FileSandbox
-
-  def setup
-    @svn = FakeSourceControl.new(&quot;bob&quot;)
-    @one = Project.new(&quot;one&quot;, @svn)
-    @two = Project.new(&quot;two&quot;, @svn)
-  end
-
-  def test_load_all
-    in_sandbox do |sandbox|
-      sandbox.new :file =&gt; &quot;one/project_config.rb&quot;, :with_content =&gt; @one.memento
-      sandbox.new :file =&gt; &quot;two/project_config.rb&quot;, :with_content =&gt; @two.memento
-
-      projects = Projects.new(sandbox.root)
-      projects.load_all
-
-      assert_equal(&quot;one&quot;, projects[0].name)
-      assert_equal(&quot;bob&quot;, projects[0].source_control.username)
-
-      assert_equal(&quot;two&quot;, projects[1].name)
-      assert_equal(&quot;bob&quot;, projects[1].source_control.username)
-    end
-  end
-
-  def test_add
-    in_sandbox do |sandbox|
-      projects = Projects.new(sandbox.root)
-      projects &lt;&lt; @one
-      projects &lt;&lt; @two
-
-      projects = Projects.new(sandbox.root)
-      projects.load_all
-
-      assert_equal(&quot;one&quot;, projects[0].name)
-      assert_equal(&quot;two&quot;, projects[1].name)
-    end
-  end
-
-  def test_add_checkouts_fresh_project
-    in_sandbox do |sandbox|
-      projects = Projects.new(sandbox.root)
-
-      projects &lt;&lt; @one
-
-      assert file('one/work').exists?
-      assert file('one/work/README').exists?
-      assert_equal @one.memento, file('one/project_config.rb').content
-    end
-  end
-
-  def test_add_cleans_up_after_itself_if_svn_throws_exception
-    in_sandbox do |sandbox|
-      projects = Projects.new(sandbox.root)
-      @svn.expects(:checkout).raises(&quot;svn error&quot;)
-
-      assert_raises('svn error') do
-        projects &lt;&lt; @one
-      end
-
-      assert !file('one/work').exists?
-      assert !file('one').exists?
-    end
-  end
-
-  def test_can_not_add_project_with_same_name
-    in_sandbox do |sandbox|
-      projects = Projects.new(sandbox.root)
-      projects &lt;&lt; @one
-      assert_raises('project named &quot;one&quot; already exists') do
-        projects &lt;&lt; @one
-      end
-    end
-  end
-
-  def test_load_project
-    in_sandbox do |sandbox|
-      sandbox.new :file =&gt; 'one/project_config.rb', :with_content =&gt; @one.memento
-
-      new_project = Projects.load_project(File.join(sandbox.root, 'one'))
-
-      assert_equal('one', new_project.name)
-      assert_equal('bob', new_project.source_control.username)
-      assert_equal(File.join(sandbox.root, 'one'), new_project.path)
-    end
-  end
-
-  def test_load_project_with_no_config
-    in_sandbox do |sandbox|
-      sandbox.new :file =&gt; &quot;myproject/builds-1/__success__&quot;
-
-      new_project = Projects.load_project(sandbox.root + '/myproject')
-
-      assert_equal(&quot;myproject&quot;, new_project.name)
-      assert_equal(Subversion, new_project.source_control.class)
-      assert_equal(sandbox.root + &quot;/myproject&quot;, new_project.path)
-    end
-  end
-
-  def test_each
-    in_sandbox do |sandbox|
-      projects = Projects.new(sandbox.root)
-      projects &lt;&lt; @one &lt;&lt; @two
-
-      out = &quot;&quot;
-      projects.each do |project|
-        out &lt;&lt; project.name
-      end
-
-      assert_equal(&quot;onetwo&quot;, out)
-    end
-  end
-
-  class FakeSourceControl
-    attr_reader :username
-    
-    def initialize(username)
-      @username = username
-    end
-
-    def checkout(dir)
-      File.open(&quot;#{dir}/README&quot;, &quot;w&quot;) {|f| f &lt;&lt; &quot;some text&quot;}
-    end
-
-    def memento
-      &quot;project.source_control = ProjectsTest::FakeSourceControl.new('#{@username}')&quot;
-    end
-  end
+require File.expand_path(File.dirname(__FILE__) + '/../test_helper')
+
+class ProjectsTest &lt; Test::Unit::TestCase
+  include FileSandbox
+
+  def setup
+    @svn = FakeSourceControl.new(&quot;bob&quot;)
+    @one = Project.new(&quot;one&quot;, @svn)
+    @two = Project.new(&quot;two&quot;, @svn)
+  end
+
+  def test_load_all
+    in_sandbox do |sandbox|
+      sandbox.new :file =&gt; &quot;one/project_config.rb&quot;, :with_content =&gt; @one.memento
+      sandbox.new :file =&gt; &quot;two/project_config.rb&quot;, :with_content =&gt; @two.memento
+
+      projects = Projects.new(sandbox.root)
+      projects.load_all
+
+      assert_equal(&quot;one&quot;, projects[0].name)
+      assert_equal(&quot;bob&quot;, projects[0].source_control.username)
+
+      assert_equal(&quot;two&quot;, projects[1].name)
+      assert_equal(&quot;bob&quot;, projects[1].source_control.username)
+    end
+  end
+
+  def test_add
+    in_sandbox do |sandbox|
+      projects = Projects.new(sandbox.root)
+      projects &lt;&lt; @one
+      projects &lt;&lt; @two
+
+      projects = Projects.new(sandbox.root)
+      projects.load_all
+
+      assert_equal(&quot;one&quot;, projects[0].name)
+      assert_equal(&quot;two&quot;, projects[1].name)
+    end
+  end
+
+  def test_add_checkouts_fresh_project
+    in_sandbox do |sandbox|
+      projects = Projects.new(sandbox.root)
+
+      projects &lt;&lt; @one
+
+      assert file('one/work').exists?
+      assert file('one/work/README').exists?
+      assert_equal @one.memento, file('one/project_config.rb').content
+    end
+  end
+
+  def test_add_cleans_up_after_itself_if_svn_throws_exception
+    in_sandbox do |sandbox|
+      projects = Projects.new(sandbox.root)
+      @svn.expects(:checkout).raises(&quot;svn error&quot;)
+
+      assert_raises('svn error') do
+        projects &lt;&lt; @one
+      end
+
+      assert !file('one/work').exists?
+      assert !file('one').exists?
+    end
+  end
+
+  def test_can_not_add_project_with_same_name
+    in_sandbox do |sandbox|
+      projects = Projects.new(sandbox.root)
+      projects &lt;&lt; @one
+      assert_raises('project named &quot;one&quot; already exists') do
+        projects &lt;&lt; @one
+      end
+    end
+  end
+
+  def test_load_project
+    in_sandbox do |sandbox|
+      sandbox.new :file =&gt; 'one/project_config.rb', :with_content =&gt; @one.memento
+
+      new_project = Projects.load_project(File.join(sandbox.root, 'one'))
+
+      assert_equal('one', new_project.name)
+      assert_equal('bob', new_project.source_control.username)
+      assert_equal(File.join(sandbox.root, 'one'), new_project.path)
+    end
+  end
+
+  def test_load_project_with_no_config
+    in_sandbox do |sandbox|
+      sandbox.new :file =&gt; &quot;myproject/builds-1/__success__&quot;
+
+      new_project = Projects.load_project(sandbox.root + '/myproject')
+
+      assert_equal(&quot;myproject&quot;, new_project.name)
+      assert_equal(Subversion, new_project.source_control.class)
+      assert_equal(sandbox.root + &quot;/myproject&quot;, new_project.path)
+    end
+  end
+
+  def test_each
+    in_sandbox do |sandbox|
+      projects = Projects.new(sandbox.root)
+      projects &lt;&lt; @one &lt;&lt; @two
+
+      out = &quot;&quot;
+      projects.each do |project|
+        out &lt;&lt; project.name
+      end
+
+      assert_equal(&quot;onetwo&quot;, out)
+    end
+  end
+
+  class FakeSourceControl
+    attr_reader :username
+    
+    def initialize(username)
+      @username = username
+    end
+
+    def checkout(dir)
+      File.open(&quot;#{dir}/README&quot;, &quot;w&quot;) {|f| f &lt;&lt; &quot;some text&quot;}
+    end
+
+    def memento
+      &quot;project.source_control = ProjectsTest::FakeSourceControl.new('#{@username}')&quot;
+    end
+  end
 end
\ No newline at end of file</diff>
      <filename>test/unit/projects_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,72 +1,72 @@
-require File.dirname(__FILE__) + '/../test_helper'
-
-class StatusTest &lt; Test::Unit::TestCase
-
-  def test_never_built_is_true_when_file_is_missing
-    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns([])
-    assert_equal true, Status.new(&quot;artifacts_directory&quot;).never_built?
-  end
-  
-  def test_never_built_is_false_when_file_exists
-    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns(['build_status = anything'])
-    assert_equal false, Status.new(&quot;artifacts_directory&quot;).never_built?
-  end
-
-  def test_succeeded_is_true_when_file_is___success__
-    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns(['build_status = success'])
-    assert_equal true, Status.new(&quot;artifacts_directory&quot;).succeeded?
-  end
-
-  def test_succeeded_is_false_when_file_is_not___success__
-    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns([])
-    assert_equal false, Status.new(&quot;artifacts_directory&quot;).succeeded?
-  end
-  
-  def test_succeed_creates_file___success__
-    Dir.stubs(:'[]').returns(['artifacts_directory/build_status = foo'])
-    FileUtils.expects(:rm_f).with([&quot;artifacts_directory/build_status = foo&quot;])
-    FileUtils.expects(:touch).with(&quot;artifacts_directory/build_status = success&quot;)
-    Status.new(&quot;artifacts_directory&quot;).succeed!
-  end
-  
-  def test_failed_is_true_when_file_is___failed__
-    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns(['build_status = failed'])
-    assert_equal true, Status.new(&quot;artifacts_directory&quot;).failed?
-  end
-
-  def test_failed_is_false_when_file_is_not___failed__
-    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns([])
-    assert_equal false, Status.new(&quot;artifacts_directory&quot;).failed?
-  end
-  
-  def test_fail_creates_file___failed__
-    Dir.stubs(:'[]').returns(['artifacts_directory/build_status = foo'])
-    FileUtils.expects(:rm_f).with([&quot;artifacts_directory/build_status = foo&quot;])
-    FileUtils.expects(:touch).with(&quot;artifacts_directory/build_status = failed&quot;)
-    Status.new(&quot;artifacts_directory&quot;).fail!    
-  end
-  
-  def test_build_creates_file___building__
-    Dir.stubs(:'[]').returns(['artifacts_directory/build_status = foo'])
-    FileUtils.expects(:rm_f).with([&quot;artifacts_directory/build_status = foo&quot;])
-    FileUtils.expects(:touch).with(&quot;artifacts_directory/build_status = building&quot;)
-    Status.new(&quot;artifacts_directory&quot;).building!
-  end
-  
-  def test_created_at_returns_creation_time_for_status_file
-    now = Time.now
-    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns([:some_file])
-    File.expects(:mtime).with(:some_file).returns(now)
-    assert_equal now, Status.new(&quot;artifacts_directory&quot;).created_at
-  end
-  
-  def test_created_at_returns_nil_when_file_not_exist
-    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns([])
-    assert_equal nil, Status.new(&quot;artifacts_directory&quot;).created_at    
-  end
-  
-  def test_to_s_returns_status_file_name_without_underscores
-    assert_equal 'never_built', Status.new(&quot;artifacts_directory&quot;).to_s
-  end
-  
+require File.dirname(__FILE__) + '/../test_helper'
+
+class StatusTest &lt; Test::Unit::TestCase
+
+  def test_never_built_is_true_when_file_is_missing
+    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns([])
+    assert_equal true, Status.new(&quot;artifacts_directory&quot;).never_built?
+  end
+  
+  def test_never_built_is_false_when_file_exists
+    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns(['build_status = anything'])
+    assert_equal false, Status.new(&quot;artifacts_directory&quot;).never_built?
+  end
+
+  def test_succeeded_is_true_when_file_is___success__
+    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns(['build_status = success'])
+    assert_equal true, Status.new(&quot;artifacts_directory&quot;).succeeded?
+  end
+
+  def test_succeeded_is_false_when_file_is_not___success__
+    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns([])
+    assert_equal false, Status.new(&quot;artifacts_directory&quot;).succeeded?
+  end
+  
+  def test_succeed_creates_file___success__
+    Dir.stubs(:'[]').returns(['artifacts_directory/build_status = foo'])
+    FileUtils.expects(:rm_f).with([&quot;artifacts_directory/build_status = foo&quot;])
+    FileUtils.expects(:touch).with(&quot;artifacts_directory/build_status = success&quot;)
+    Status.new(&quot;artifacts_directory&quot;).succeed!
+  end
+  
+  def test_failed_is_true_when_file_is___failed__
+    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns(['build_status = failed'])
+    assert_equal true, Status.new(&quot;artifacts_directory&quot;).failed?
+  end
+
+  def test_failed_is_false_when_file_is_not___failed__
+    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns([])
+    assert_equal false, Status.new(&quot;artifacts_directory&quot;).failed?
+  end
+  
+  def test_fail_creates_file___failed__
+    Dir.stubs(:'[]').returns(['artifacts_directory/build_status = foo'])
+    FileUtils.expects(:rm_f).with([&quot;artifacts_directory/build_status = foo&quot;])
+    FileUtils.expects(:touch).with(&quot;artifacts_directory/build_status = failed&quot;)
+    Status.new(&quot;artifacts_directory&quot;).fail!    
+  end
+  
+  def test_build_creates_file___building__
+    Dir.stubs(:'[]').returns(['artifacts_directory/build_status = foo'])
+    FileUtils.expects(:rm_f).with([&quot;artifacts_directory/build_status = foo&quot;])
+    FileUtils.expects(:touch).with(&quot;artifacts_directory/build_status = building&quot;)
+    Status.new(&quot;artifacts_directory&quot;).building!
+  end
+  
+  def test_created_at_returns_creation_time_for_status_file
+    now = Time.now
+    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns([:some_file])
+    File.expects(:mtime).with(:some_file).returns(now)
+    assert_equal now, Status.new(&quot;artifacts_directory&quot;).created_at
+  end
+  
+  def test_created_at_returns_nil_when_file_not_exist
+    Dir.expects(:'[]').with(&quot;artifacts_directory/build_status = *&quot;).returns([])
+    assert_equal nil, Status.new(&quot;artifacts_directory&quot;).created_at    
+  end
+  
+  def test_to_s_returns_status_file_name_without_underscores
+    assert_equal 'never_built', Status.new(&quot;artifacts_directory&quot;).to_s
+  end
+  
 end
\ No newline at end of file</diff>
      <filename>test/unit/status_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,124 +1,124 @@
-require File.dirname(__FILE__) + '/../test_helper'
-require 'revision'
-require 'changeset_entry'
-
-class SubversionLogParserTest &lt; Test::Unit::TestCase
-
-SIMPLE_LOG_ENTRY = &lt;&lt;EOF
-------------------------------------------------------------------------
-r359 | aslak | 2006-05-22 13:23:29 -0600 (Mon, 22 May 2006) | 1 line
-Changed paths:
-   A /trunk/foo.txt
-
-versioning
-------------------------------------------------------------------------
-EOF
-
-LOG_ENTRY_WITH_MULTIPLE_ENTRIES = &lt;&lt;EOF
-------------------------------------------------------------------------
-r359 | aslak | 2006-05-22 13:23:29 -0600 (Mon, 22 May 2006) | 1 line
-Changed paths:
-   A /trunk/foo.txt
-   D /trunk/bar.exe
-
-versioning
-------------------------------------------------------------------------
-r358 | joe | 2006-05-22 13:20:05 -0600 (Mon, 22 May 2006) | 1 line
-Changed paths:
-   A /trunk/bar.exe
-
-Added Rakefile for packaging of svn ruby bindings (swig) in prebuilt gems for di
-fferent platforms
-------------------------------------------------------------------------
-EOF
-
-UPDATE_OUTPUT = &lt;&lt;EOF
-A    failing_project
-D    failing_project\\Rakefile
-U*   failing_project\\failing_test.rb
-G    failing_project\\revision_label.txt
-C B  passing_project\\revision_label.txt
-?    foo.txt
-
-Fetching external item into 'vendor\rails'
-Updated external to revision 5875.
-
-Updated to revision 46.
-EOF
-
-  def test_can_parse_SIMPLE_LOG_ENTRY
-    expected_result = [Revision.new(359, 'aslak', DateTime.parse('2006-05-22 13:23:29 -0600'), 'versioning',
-                                    [ChangesetEntry.new('A', '/trunk/foo.txt')])]
-    assert_equal expected_result, parse_log(SIMPLE_LOG_ENTRY)
-  end
-
-  def test_can_parse_LOG_ENTRY_WITH_MULTIPLE_ENTRIES
-    expected = [
-      Revision.new(359, 'aslak', DateTime.parse('2006-05-22 13:23:29 -0600'), 'versioning',
-                   [ChangesetEntry.new('A', '/trunk/foo.txt'), ChangesetEntry.new('D', '/trunk/bar.exe')]),
-      Revision.new(358, 'joe',   DateTime.parse('2006-05-22 13:20:05 -0600'),
-                   &quot;Added Rakefile for packaging of svn ruby bindings (swig) in prebuilt gems for different platforms&quot;,
-                   [ChangesetEntry.new('A', '/trunk/bar.exe')])
-    ]
-
-    assert_equal expected, parse_log(LOG_ENTRY_WITH_MULTIPLE_ENTRIES)
-  end
-
-DV_LOG = &lt;&lt;-EOL
-------------------------------------------------------------------------
-r127 | (no author) | 2007-01-02 06:56:05 +0300 (Tue, 02 Jan 2007) | 1 line
-Changed paths:
-   M /waypoints/app/controllers/categories_controller.rb
-   M /waypoints/app/controllers/mywaypoints_controller.rb
-   M /waypoints/app/models/category.rb
-   A /waypoints/app/views/categories/_form.rhtml
-   A /waypoints/app/views/categories/_list_stripes.rhtml
-   A /waypoints/app/views/categories/edit.rhtml
-   A /waypoints/app/views/categories/list.rhtml
-   A /waypoints/app/views/categories/new.rhtml
-   A /waypoints/app/views/layouts/_guest_left_tab.rhtml
-   A /waypoints/app/views/layouts/_user_left_tab.rhtml
-   M /waypoints/app/views/layouts/application.rhtml
-   M /waypoints/app/views/layouts/mywaypoints_show.rhtml
-   M /waypoints/app/views/layouts/waypoints_show.rhtml
-   M /waypoints/app/views/mywaypoints/list.rhtml
-   M /waypoints/public/stylesheets/scaffold.css
-
-categories added
-------------------------------------------------------------------------
-EOL
-
-  def test_DV
-    parse_log(LOG_ENTRY_WITH_MULTIPLE_ENTRIES)
-  end
-
-  def test_can_parse_UPDATE_OUTPUT
-    expected_result = [
-      ChangesetEntry.new('A  ', 'failing_project'),
-      ChangesetEntry.new('D  ', 'failing_project\Rakefile'),
-      ChangesetEntry.new('U* ', 'failing_project\\failing_test.rb'),
-      ChangesetEntry.new('G  ', 'failing_project\\revision_label.txt'),
-      ChangesetEntry.new('C B', 'passing_project\\revision_label.txt'),
-      ChangesetEntry.new('?  ', 'foo.txt')]
-
-    assert_equal expected_result, parse_update(UPDATE_OUTPUT)
-  end
-
-  def test_revision_and_changeset_should_know_how_to_convert_to_string
-    expected_result = &lt;&lt;-EOL
-Revision 359 committed by aslak on #{DateTime.parse(&quot;2006-05-22 13:23:29 -0600&quot;).strftime('%Y-%m-%d %H:%M:%S')}
-versioning
-  A /trunk/foo.txt
-    EOL
-    assert_equal expected_result, parse_log(SIMPLE_LOG_ENTRY)[0].to_s
-  end
-
-  def parse_log(log_entry)
-    SubversionLogParser.new.parse_log(log_entry.split(&quot;\n&quot;))
-  end
-
-  def parse_update(log_entry)
-    SubversionLogParser.new.parse_update(log_entry.split(&quot;\n&quot;))
-  end
-
+require File.dirname(__FILE__) + '/../test_helper'
+require 'revision'
+require 'changeset_entry'
+
+class SubversionLogParserTest &lt; Test::Unit::TestCase
+
+SIMPLE_LOG_ENTRY = &lt;&lt;EOF
+------------------------------------------------------------------------
+r359 | aslak | 2006-05-22 13:23:29 -0600 (Mon, 22 May 2006) | 1 line
+Changed paths:
+   A /trunk/foo.txt
+
+versioning
+------------------------------------------------------------------------
+EOF
+
+LOG_ENTRY_WITH_MULTIPLE_ENTRIES = &lt;&lt;EOF
+------------------------------------------------------------------------
+r359 | aslak | 2006-05-22 13:23:29 -0600 (Mon, 22 May 2006) | 1 line
+Changed paths:
+   A /trunk/foo.txt
+   D /trunk/bar.exe
+
+versioning
+------------------------------------------------------------------------
+r358 | joe | 2006-05-22 13:20:05 -0600 (Mon, 22 May 2006) | 1 line
+Changed paths:
+   A /trunk/bar.exe
+
+Added Rakefile for packaging of svn ruby bindings (swig) in prebuilt gems for di
+fferent platforms
+------------------------------------------------------------------------
+EOF
+
+UPDATE_OUTPUT = &lt;&lt;EOF
+A    failing_project
+D    failing_project\\Rakefile
+U*   failing_project\\failing_test.rb
+G    failing_project\\revision_label.txt
+C B  passing_project\\revision_label.txt
+?    foo.txt
+
+Fetching external item into 'vendor\rails'
+Updated external to revision 5875.
+
+Updated to revision 46.
+EOF
+
+  def test_can_parse_SIMPLE_LOG_ENTRY
+    expected_result = [Revision.new(359, 'aslak', DateTime.parse('2006-05-22 13:23:29 -0600'), 'versioning',
+                                    [ChangesetEntry.new('A', '/trunk/foo.txt')])]
+    assert_equal expected_result, parse_log(SIMPLE_LOG_ENTRY)
+  end
+
+  def test_can_parse_LOG_ENTRY_WITH_MULTIPLE_ENTRIES
+    expected = [
+      Revision.new(359, 'aslak', DateTime.parse('2006-05-22 13:23:29 -0600'), 'versioning',
+                   [ChangesetEntry.new('A', '/trunk/foo.txt'), ChangesetEntry.new('D', '/trunk/bar.exe')]),
+      Revision.new(358, 'joe',   DateTime.parse('2006-05-22 13:20:05 -0600'),
+                   &quot;Added Rakefile for packaging of svn ruby bindings (swig) in prebuilt gems for different platforms&quot;,
+                   [ChangesetEntry.new('A', '/trunk/bar.exe')])
+    ]
+
+    assert_equal expected, parse_log(LOG_ENTRY_WITH_MULTIPLE_ENTRIES)
+  end
+
+DV_LOG = &lt;&lt;-EOL
+------------------------------------------------------------------------
+r127 | (no author) | 2007-01-02 06:56:05 +0300 (Tue, 02 Jan 2007) | 1 line
+Changed paths:
+   M /waypoints/app/controllers/categories_controller.rb
+   M /waypoints/app/controllers/mywaypoints_controller.rb
+   M /waypoints/app/models/category.rb
+   A /waypoints/app/views/categories/_form.rhtml
+   A /waypoints/app/views/categories/_list_stripes.rhtml
+   A /waypoints/app/views/categories/edit.rhtml
+   A /waypoints/app/views/categories/list.rhtml
+   A /waypoints/app/views/categories/new.rhtml
+   A /waypoints/app/views/layouts/_guest_left_tab.rhtml
+   A /waypoints/app/views/layouts/_user_left_tab.rhtml
+   M /waypoints/app/views/layouts/application.rhtml
+   M /waypoints/app/views/layouts/mywaypoints_show.rhtml
+   M /waypoints/app/views/layouts/waypoints_show.rhtml
+   M /waypoints/app/views/mywaypoints/list.rhtml
+   M /waypoints/public/stylesheets/scaffold.css
+
+categories added
+------------------------------------------------------------------------
+EOL
+
+  def test_DV
+    parse_log(LOG_ENTRY_WITH_MULTIPLE_ENTRIES)
+  end
+
+  def test_can_parse_UPDATE_OUTPUT
+    expected_result = [
+      ChangesetEntry.new('A  ', 'failing_project'),
+      ChangesetEntry.new('D  ', 'failing_project\Rakefile'),
+      ChangesetEntry.new('U* ', 'failing_project\\failing_test.rb'),
+      ChangesetEntry.new('G  ', 'failing_project\\revision_label.txt'),
+      ChangesetEntry.new('C B', 'passing_project\\revision_label.txt'),
+      ChangesetEntry.new('?  ', 'foo.txt')]
+
+    assert_equal expected_result, parse_update(UPDATE_OUTPUT)
+  end
+
+  def test_revision_and_changeset_should_know_how_to_convert_to_string
+    expected_result = &lt;&lt;-EOL
+Revision 359 committed by aslak on #{DateTime.parse(&quot;2006-05-22 13:23:29 -0600&quot;).strftime('%Y-%m-%d %H:%M:%S')}
+versioning
+  A /trunk/foo.txt
+    EOL
+    assert_equal expected_result, parse_log(SIMPLE_LOG_ENTRY)[0].to_s
+  end
+
+  def parse_log(log_entry)
+    SubversionLogParser.new.parse_log(log_entry.split(&quot;\n&quot;))
+  end
+
+  def parse_update(log_entry)
+    SubversionLogParser.new.parse_update(log_entry.split(&quot;\n&quot;))
+  end
+
 end
\ No newline at end of file</diff>
      <filename>test/unit/subversion_log_parser_test.rb</filename>
    </modified>
    <modified>
      <diff>@@ -1,158 +1,158 @@
-require File.dirname(__FILE__) + '/../test_helper'
-require 'stringio'
-
-class SubversionTest &lt; Test::Unit::TestCase
-
-  LOG_ENTRY = &lt;&lt;-EOF
-------------------------------------------------------------------------
-r18 | alexeyv | 2007-01-11 13:58:58 -0700 (Thu, 11 Jan 2007) | 1 line
-
-Goofin around with integration test for the builder
-------------------------------------------------------------------------
-r17 | alexeyv | 2007-01-11 12:44:54 -0700 (Thu, 11 Jan 2007) | 1 line
-
-Moved builder from vendor, made bulder's integration tests
-talk to a subversion repository in the local file system
-------------------------------------------------------------------------
-r15 | stellsmi | 2007-01-11 10:37:32 -0700 (Thu, 11 Jan 2007) | 1 line
-
-integration test does a checkout
-------------------------------------------------------------------------
-  EOF
-
-  EMPTY_LOG = &lt;&lt;-EOF
-------------------------------------------------------------------------
-  EOF
-
-  def test_options
-    svn = Subversion.new(:url =&gt; &quot;file://foo&quot;, :username =&gt; &quot;bob&quot;, :password =&gt; 'cha')
-
-    assert_equal(&quot;file://foo&quot;, svn.url)
-    assert_equal(&quot;bob&quot;, svn.username)
-    assert_equal(&quot;cha&quot;, svn.password)
-  end
-
-  def test_only_except_known_options
-    assert_raises(&quot;don't know how to handle 'sugar'&quot;) do
-      Subversion.new(:sugar =&gt; &quot;1/2 cup&quot;)
-    end
-  end
-
-  def test_memento
-    svn = Subversion.new(:url =&gt; &quot;file://foo&quot;, :username =&gt; &quot;bob&quot;, :password =&gt; 'cha')
-
-    assert_equal(
-        &quot;Subversion.new(:url =&gt; 'file://foo', :username =&gt; 'bob', :password =&gt; 'cha')&quot;,
-        svn.memento)
-  end
-
-  def test_memento_for_all_defaults
-    assert_nil Subversion.new.memento
-  end
-  
-  def test_update_with_revision_number
-    revision_number = 10
-
-    svn = Subversion.new
-    svn.expects(:execute).with(&quot;svn --non-interactive update --revision #{revision_number}&quot;).returns(&quot;your mom&quot;)
-
-    svn.update(dummy_project, Revision.new(revision_number))
-    svn.verify
-  end
-
-  def test_latest_revision
-    svn = Subversion.new
-
-    svn.expects(:info).with(dummy_project).returns('Last Changed Rev' =&gt; '10')
-    svn.expects(:execute).with(&quot;svn --non-interactive log --revision HEAD:10 --verbose&quot;).yields(StringIO.new(LOG_ENTRY))
-
-    revision = svn.latest_revision(dummy_project)
-
-    svn.verify
-    assert_equal 18, revision.number
-  end
-
-  def test_revisions_since_should_reverse_the_log_entries_and_skip_the_one_corresponding_to_current_revision
-    svn = Subversion.new
-
-    svn.expects(:execute).with(&quot;svn --non-interactive log --revision HEAD:15 --verbose&quot;).yields(StringIO.new(LOG_ENTRY))
-
-    revisions = svn.revisions_since(dummy_project, 15)
-
-    svn.verify
-    assert_equal [17, 18], numbers(revisions)
-  end
-
-  def test_revisions_since_should_return_all_revisions_when_curreent_revision_is_not_in_the_log_output
-    svn = Subversion.new
-
-    svn.expects(:execute).with(&quot;svn --non-interactive log --revision HEAD:14 --verbose&quot;).yields(StringIO.new(LOG_ENTRY))
-
-    revisions = svn.revisions_since(dummy_project, 14)
-
-    svn.verify
-
-    assert_equal [15, 17, 18], numbers(revisions)
-  end
-
-  def test_revisions_since_should_return_an_empty_array_for_empty_log_output
-    svn = Subversion.new
-
-    svn.expects(:execute).with(&quot;svn --non-interactive log --revision HEAD:14 --verbose&quot;).yields(StringIO.new(EMPTY_LOG))
-
-    revisions = svn.revisions_since(dummy_project, 14)
-
-    svn.verify
-
-    assert_equal [], numbers(revisions)
-  end
-
-  def test_checkout_with_no_user_password
-    svn = Subversion.new(:url =&gt; 'http://foo.com/svn/project')
-    svn.expects(:execute).with(&quot;svn --non-interactive co http://foo.com/svn/project .&quot;)
-
-    svn.checkout('.')
-
-    svn.verify
-  end
-
-  def test_checkout_with_user_password
-    svn = Subversion.new(:url =&gt; 'http://foo.com/svn/project', :username =&gt; 'jer', :password =&gt; &quot;crap&quot;)
-    svn.expects(:execute).with(&quot;svn --non-interactive co http://foo.com/svn/project . --username jer --password crap&quot;)
-
-    svn.checkout('.')
-
-    svn.verify
-  end
-
-  def test_checkout_with_revision
-    svn = Subversion.new(:url =&gt; 'http://foo.com/svn/project')
-    svn.expects(:execute).with(&quot;svn --non-interactive co http://foo.com/svn/project . --revision 5&quot;)
-
-    svn.checkout('.', Revision.new(5))
-
-    svn.verify
-  end
-
-  def test_checkout_requires_url
-    assert_raises('URL not specified') { Subversion.new.checkout('.') }
-  end
-
-  def test_new_does_not_allow_random_params
-    assert_raises(&quot;don't know how to handle 'lollipop'&quot;) do
-      Subversion.new(:url =&gt; 'http://foo.com/svn/project', :lollipop =&gt; 'http://foo.com/svn/project')
-    end
-  end
-
-  def numbers(revisions)
-    revisions.map { |r|
-      r.number
-    }
-  end
-
-  DummyProject = Struct.new :local_checkout
-  def dummy_project
-    DummyProject.new('.')
-  end
-
-end
+require File.dirname(__FILE__) + '/../test_helper'
+require 'stringio'
+
+class SubversionTest &lt; Test::Unit::TestCase
+
+  LOG_ENTRY = &lt;&lt;-EOF
+------------------------------------------------------------------------
+r18 | alexeyv | 2007-01-11 13:58:58 -0700 (Thu, 11 Jan 2007) | 1 line
+
+Goofin around with integration test for the builder
+------------------------------------------------------------------------
+r17 | alexeyv | 2007-01-11 12:44:54 -0700 (Thu, 11 Jan 2007) | 1 line
+
+Moved builder from vendor, made bulder's integration tests
+talk to a subversion repository in the local file system
+------------------------------------------------------------------------
+r15 | stellsmi | 2007-01-11 10:37:32 -0700 (Thu, 11 Jan 2007) | 1 line
+
+integration test does a checkout
+------------------------------------------------------------------------
+  EOF
+
+  EMPTY_LOG = &lt;&lt;-EOF
+------------------------------------------------------------------------
+  EOF
+
+  def test_options
+    svn = Subversion.new(:url =&gt; &quot;file://foo&quot;, :username =&gt; &quot;bob&quot;, :password =&gt; 'cha')
+
+    assert_equal(&quot;file://foo&quot;, svn.url)
+    assert_equal(&quot;bob&quot;, svn.username)
+    assert_equal(&quot;cha&quot;, svn.password)
+  end
+
+  def test_only_except_known_options
+    assert_raises(&quot;don't know how to handle 'sugar'&quot;) do
+      Subversion.new(:sugar =&gt; &quot;1/2 cup&quot;)
+    end
+  end
+
+  def test_memento
+    svn = Subversion.new(:url =&gt; &quot;file://foo&quot;, :username =&gt; &quot;bob&quot;, :password =&gt; 'cha')
+
+    assert_equal(
+        &quot;Subversion.new(:url =&gt; 'file://foo', :username =&gt; 'bob', :password =&gt; 'cha')&quot;,
+        svn.memento)
+  end
+
+  def test_memento_for_all_defaults
+    assert_nil Subversion.new.memento
+  end
+  
+  def test_update_with_revision_number
+    revision_number = 10
+
+    svn = Subversion.new
+    svn.expects(:execute).with(&quot;svn --non-interactive update --revision #{revision_number}&quot;).returns(&quot;your mom&quot;)
+
+    svn.update(dummy_project, Revision.new(revision_number))
+    svn.verify
+  end
+
+  def test_latest_revision
+    svn = Subversion.new
+
+    svn.expects(:info).with(dummy_project).returns('Last Changed Rev' =&gt; '10')
+    svn.expects(:execute).with(&quot;svn --non-interactive log --revision HEAD:10 --verbose&quot;).yields(StringIO.new(LOG_ENTRY))
+
+    revision = svn.latest_revision(dummy_project)
+
+    svn.verify
+    assert_equal 18, revision.number
+  end
+
+  def test_revisions_since_should_reverse_the_log_entries_and_skip_the_one_corresponding_to_current_revision
+    svn = Subversion.new
+
+    svn.expects(:execute).with(&quot;svn --non-interactive log --revision HEAD:15 --verbose&quot;).yields(StringIO.new(LOG_ENTRY))
+
+    revisions = svn.revisions_since(dummy_project, 15)
+
+    svn.verify
+    assert_equal [17, 18], numbers(revisions)
+  end
+
+  def test_revisions_since_should_return_all_revisions_when_curreent_revision_is_not_in_the_log_output
+    svn = Subversion.new
+
+    svn.expects(:execute).with(&quot;svn --non-interactive log --revision HEAD:14 --verbose&quot;).yields(StringIO.new(LOG_ENTRY))
+
+    revisions = svn.revisions_since(dummy_project, 14)
+
+    svn.verify
+
+    assert_equal [15, 17, 18], numbers(revisions)
+  end
+
+  def test_revisions_since_should_return_an_empty_array_for_empty_log_output
+    svn = Subversion.new
+
+    svn.expects(:execute).with(&quot;svn --non-interactive log --revision HEAD:14 --verbose&quot;).yields(StringIO.new(EMPTY_LOG))
+
+    revisions = svn.revisions_since(dummy_project, 14)
+
+    svn.verify
+
+    assert_equal [], numbers(revisions)
+  end
+
+  def test_checkout_with_no_user_password
+    svn = Subversion.new(:url =&gt; 'http://foo.com/svn/project')
+    svn.expects(:execute).with(&quot;svn --non-interactive co http://foo.com/svn/project .&quot;)
+
+    svn.checkout('.')
+
+    svn.verify
+  end
+
+  def test_checkout_with_user_password
+    svn = Subversion.new(:url =&gt; 'http://foo.com/svn/project', :username =&gt; 'jer', :password =&gt; &quot;crap&quot;)
+    svn.expects(:execute).with(&quot;svn --non-interactive co http://foo.com/svn/project . --username jer --password crap&quot;)
+
+    svn.checkout('.')
+
+    svn.verify
+  end
+
+  def test_checkout_with_revision
+    svn = Subversion.new(:url =&gt; 'http://foo.com/svn/project')
+    svn.expects(:execute).with(&quot;svn --non-interactive co http://foo.com/svn/project . --revision 5&quot;)
+
+    svn.checkout('.', Revision.new(5))
+
+    svn.verify
+  end
+
+  def test_checkout_requires_url
+    assert_raises('URL not specified') { Subversion.new.checkout('.') }
+  end
+
+  def test_new_does_not_allow_random_params
+    assert_raises(&quot;don't know how to handle 'lollipop'&quot;) do
+      Subversion.new(:url =&gt; 'http://foo.com/svn/project', :lollipop =&gt; 'http://foo.com/svn/project')
+    end
+  end
+
+  def numbers(revisions)
+    revisions.map { |r|
+      r.number
+    }
+  end
+
+  DummyProject = Struct.new :local_checkout
+  def dummy_project
+    DummyProject.new('.')
+  end
+
+end</diff>
      <filename>test/unit/subversion_test.rb</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>54efc205cb42c78824d7b793c8a3b4c47d5dc7df</id>
    </parent>
  </parents>
  <author>
    <name>Alexey Verkhovsky</name>
    <email>alexey.verkhovsky@gmail.com</email>
  </author>
  <url>http://github.com/willbryant/cruisecontrolrb/commit/a91a0c2af337a88a0a233744bb048a4a5f015130</url>
  <id>a91a0c2af337a88a0a233744bb048a4a5f015130</id>
  <committed-date>2007-01-25T12:54:40-08:00</committed-date>
  <authored-date>2007-01-25T12:54:40-08:00</authored-date>
  <message>r44: setting eol-style to native where appropriate</message>
  <tree>2e455cf04784bff9814cea7937cb072f3c543668</tree>
  <committer>
    <name>Alexey Verkhovsky</name>
    <email>alexey.verkhovsky@gmail.com</email>
  </committer>
</commit>
