warvox - VoIP based wardialing tool, forked from rapid7/warvox.
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
---
(DIR) commit acb500fd3b153e34fce50504796aab6458f717d8
(DIR) parent 7ab6be136efbdbc5e12e4e7b44269b575d55878b
(HTM) Author: HD Moore <hd_moore@rapid7.com>
Date: Wed, 3 Nov 2010 08:28:02 +0000
Diffstat:
M Makefile | 12 +++++++++++-
M bin/verify_install.rb | 19 +++++++++++++++++++
M bin/warvox.rb | 30 +++++++++++++++---------------
M docs/BUGS | 6 ++++--
M docs/ChangeLog | 8 ++++++--
M docs/LICENSE | 9 +++------
M docs/README | 6 +++++-
M etc/sigs/01.default.rb | 10 +++++-----
M etc/sigs/99.default.rb | 8 ++++----
M lib/warvox.rb | 2 +-
M lib/warvox/jobs.rb | 5 +++++
M lib/warvox/jobs/analysis.rb | 8 ++++++--
M lib/warvox/phone.rb | 6 +++++-
M src/ruby-kissfft/main.c | 17 +++++++----------
A web/Gemfile | 30 ++++++++++++++++++++++++++++++
A web/Gemfile.lock | 74 +++++++++++++++++++++++++++++++
A web/README | 256 +++++++++++++++++++++++++++++++
A web/Rakefile | 7 +++++++
A web/app/controllers/analyze_contro… | 148 +++++++++++++++++++++++++++++++
A web/app/controllers/application_co… | 54 +++++++++++++++++++++++++++++++
A web/app/controllers/dial_jobs_cont… | 123 +++++++++++++++++++++++++++++++
A web/app/controllers/dial_results_c… | 204 +++++++++++++++++++++++++++++++
A web/app/controllers/home_controlle… | 16 ++++++++++++++++
A web/app/controllers/providers_cont… | 91 +++++++++++++++++++++++++++++++
A web/app/helpers/analyze_helper.rb | 2 ++
A web/app/helpers/application_helper… | 70 +++++++++++++++++++++++++++++++
A web/app/helpers/dial_jobs_helper.rb | 2 ++
A web/app/helpers/dial_results_helpe… | 2 ++
A web/app/helpers/home_helper.rb | 2 ++
A web/app/helpers/providers_helper.rb | 2 ++
A web/app/models/dial_job.rb | 30 ++++++++++++++++++++++++++++++
A web/app/models/dial_result.rb | 4 ++++
A web/app/models/provider.rb | 7 +++++++
A web/app/views/analyze/index.html.e… | 38 +++++++++++++++++++++++++++++++
A web/app/views/analyze/show.html.erb | 9 +++++++++
A web/app/views/analyze/view.html.erb | 62 +++++++++++++++++++++++++++++++
A web/app/views/dial_jobs/edit.html.… | 24 ++++++++++++++++++++++++
A web/app/views/dial_jobs/index.html… | 107 +++++++++++++++++++++++++++++++
A web/app/views/dial_jobs/new.html.e… | 35 +++++++++++++++++++++++++++++++
A web/app/views/dial_jobs/run.html.e… | 5 +++++
A web/app/views/dial_jobs/show.html.… | 39 +++++++++++++++++++++++++++++++
A web/app/views/dial_results/analyze… | 25 +++++++++++++++++++++++++
A web/app/views/dial_results/edit.ht… | 48 +++++++++++++++++++++++++++++++
A web/app/views/dial_results/index.h… | 41 +++++++++++++++++++++++++++++++
A web/app/views/dial_results/new.htm… | 43 ++++++++++++++++++++++++++++++
A web/app/views/dial_results/show.ht… | 48 +++++++++++++++++++++++++++++++
A web/app/views/dial_results/view.ht… | 44 +++++++++++++++++++++++++++++++
A web/app/views/home/about.html.erb | 142 +++++++++++++++++++++++++++++++
A web/app/views/home/index.html.erb | 45 +++++++++++++++++++++++++++++++
A web/app/views/layouts/application.… | 0
A web/app/views/layouts/warvox.html.… | 37 +++++++++++++++++++++++++++++++
A web/app/views/providers/edit.html.… | 38 +++++++++++++++++++++++++++++++
A web/app/views/providers/index.html… | 72 +++++++++++++++++++++++++++++++
A web/app/views/providers/new.html.e… | 34 +++++++++++++++++++++++++++++++
A web/app/views/providers/show.html.… | 38 +++++++++++++++++++++++++++++++
A web/app/views/shared/_footer.html.… | 5 +++++
A web/app/views/shared/_header.html.… | 17 +++++++++++++++++
A web/config.ru | 4 ++++
A web/config/application.rb | 49 +++++++++++++++++++++++++++++++
A web/config/boot.rb | 13 +++++++++++++
A web/config/database.yml | 22 ++++++++++++++++++++++
A web/config/environment.rb | 5 +++++
A web/config/environments/developmen… | 26 ++++++++++++++++++++++++++
A web/config/environments/production… | 49 +++++++++++++++++++++++++++++++
A web/config/environments/test.rb | 35 +++++++++++++++++++++++++++++++
A web/config/initializers/backtrace_… | 7 +++++++
A web/config/initializers/inflection… | 10 ++++++++++
A web/config/initializers/mime_types… | 5 +++++
A web/config/initializers/secret_tok… | 7 +++++++
A web/config/initializers/session_st… | 8 ++++++++
A web/config/initializers/warvox.rb | 0
A web/config/locales/en.yml | 5 +++++
A web/config/routes.rb | 23 +++++++++++++++++++++++
A web/db/migrate/20090228195925_crea… | 18 ++++++++++++++++++
A web/db/migrate/20090228200035_crea… | 20 ++++++++++++++++++++
A web/db/migrate/20090228200141_crea… | 21 +++++++++++++++++++++
A web/db/migrate/20090301084459_add_… | 9 +++++++++
A web/db/migrate/20090303204859_add_… | 9 +++++++++
A web/db/migrate/20090303204917_add_… | 9 +++++++++
A web/db/migrate/20090303225838_add_… | 9 +++++++++
A web/db/migrate/20090304013815_add_… | 9 +++++++++
A web/db/migrate/20090304013839_add_… | 9 +++++++++
A web/db/migrate/20090304013909_add_… | 9 +++++++++
A web/db/migrate/20090304014018_add_… | 9 +++++++++
A web/db/migrate/20090304014033_add_… | 9 +++++++++
A web/db/migrate/20090522202032_add_… | 9 +++++++++
A web/db/migrate/20090526031826_add_… | 11 +++++++++++
A web/db/schema.rb | 65 +++++++++++++++++++++++++++++++
A web/db/seeds.rb | 7 +++++++
A web/public/404.html | 26 ++++++++++++++++++++++++++
A web/public/422.html | 26 ++++++++++++++++++++++++++
A web/public/500.html | 26 ++++++++++++++++++++++++++
A web/public/FusionCharts/FCF_Area2D… | 0
A web/public/FusionCharts/FCF_Bar2D.… | 0
A web/public/FusionCharts/FCF_Candle… | 0
A web/public/FusionCharts/FCF_Column… | 0
A web/public/FusionCharts/FCF_Column… | 0
A web/public/FusionCharts/FCF_Doughn… | 0
A web/public/FusionCharts/FCF_Funnel… | 0
A web/public/FusionCharts/FCF_Gantt.… | 0
A web/public/FusionCharts/FCF_Line.s… | 0
A web/public/FusionCharts/FCF_MSArea… | 0
A web/public/FusionCharts/FCF_MSBar2… | 0
A web/public/FusionCharts/FCF_MSColu… | 0
A web/public/FusionCharts/FCF_MSColu… | 0
A web/public/FusionCharts/FCF_MSColu… | 0
A web/public/FusionCharts/FCF_MSColu… | 0
A web/public/FusionCharts/FCF_MSLine… | 0
A web/public/FusionCharts/FCF_Pie2D.… | 0
A web/public/FusionCharts/FCF_Pie3D.… | 0
A web/public/FusionCharts/FCF_Stacke… | 0
A web/public/FusionCharts/FCF_Stacke… | 0
A web/public/FusionCharts/FCF_Stacke… | 0
A web/public/FusionCharts/FCF_Stacke… | 0
A web/public/favicon.ico | 0
A web/public/images/balloon.png | 0
A web/public/images/bluefade.jpg | 0
A web/public/images/close.gif | 0
A web/public/images/left-round.png | 0
A web/public/images/loading.gif | 0
A web/public/images/logo.png | 0
A web/public/images/logo_raw.xcf | 0
A web/public/images/musicplayer.swf | 0
A web/public/images/rails.png | 0
A web/public/images/right-round.png | 0
A web/public/images/round_bot.png | 0
A web/public/images/round_top.png | 0
A web/public/javascripts/FusionChart… | 362 +++++++++++++++++++++++++++++++
A web/public/javascripts/application… | 2 ++
A web/public/javascripts/controls.js | 966 +++++++++++++++++++++++++++++++
A web/public/javascripts/custom.js | 0
A web/public/javascripts/dragdrop.js | 975 +++++++++++++++++++++++++++++++
A web/public/javascripts/effects.js | 1124 +++++++++++++++++++++++++++++++
A web/public/javascripts/lightbox.js | 426 +++++++++++++++++++++++++++++++
A web/public/javascripts/prototype.js | 6001 +++++++++++++++++++++++++++++++
A web/public/javascripts/rails.js | 175 +++++++++++++++++++++++++++++++
A web/public/robots.txt | 5 +++++
A web/public/stylesheets/global.css | 556 ++++++++++++++++++++++++++++++
A web/public/stylesheets/ie7.css | 3 +++
A web/public/stylesheets/lightbox.css | 27 +++++++++++++++++++++++++++
A web/public/stylesheets/overlay.png | 0
A web/public/stylesheets/scaffold.css | 54 +++++++++++++++++++++++++++++++
A web/script/rails | 6 ++++++
A web/test/fixtures/dial_jobs.yml | 11 +++++++++++
A web/test/fixtures/dial_results.yml | 11 +++++++++++
A web/test/fixtures/providers.yml | 11 +++++++++++
A web/test/functional/analyze_contro… | 8 ++++++++
A web/test/functional/dial_jobs_cont… | 8 ++++++++
A web/test/functional/dial_results_c… | 8 ++++++++
A web/test/functional/home_controlle… | 8 ++++++++
A web/test/functional/providers_cont… | 8 ++++++++
A web/test/performance/browsing_test… | 9 +++++++++
A web/test/test_helper.rb | 13 +++++++++++++
A web/test/unit/dial_job_test.rb | 8 ++++++++
A web/test/unit/dial_result_test.rb | 8 ++++++++
A web/test/unit/helpers/analyze_help… | 4 ++++
A web/test/unit/helpers/dial_jobs_he… | 4 ++++
A web/test/unit/helpers/dial_results… | 4 ++++
A web/test/unit/helpers/home_helper_… | 4 ++++
A web/test/unit/helpers/providers_he… | 4 ++++
A web/test/unit/provider_test.rb | 8 ++++++++
A web/vendor/plugins/dynamic_form/MI… | 20 ++++++++++++++++++++
A web/vendor/plugins/dynamic_form/RE… | 13 +++++++++++++
A web/vendor/plugins/dynamic_form/Ra… | 10 ++++++++++
A web/vendor/plugins/dynamic_form/dy… | 12 ++++++++++++
A web/vendor/plugins/dynamic_form/in… | 1 +
A web/vendor/plugins/dynamic_form/li… | 300 +++++++++++++++++++++++++++++++
A web/vendor/plugins/dynamic_form/li… | 8 ++++++++
A web/vendor/plugins/dynamic_form/li… | 5 +++++
A web/vendor/plugins/dynamic_form/te… | 43 ++++++++++++++++++++++++++++++
A web/vendor/plugins/dynamic_form/te… | 371 +++++++++++++++++++++++++++++++
A web/vendor/plugins/dynamic_form/te… | 9 +++++++++
A web/vendor/plugins/ezgraphix/Fusio… | 49 +++++++++++++++++++++++++++++++
A web/vendor/plugins/ezgraphix/READM… | 157 +++++++++++++++++++++++++++++++
A web/vendor/plugins/ezgraphix/init.… | 3 +++
A web/vendor/plugins/ezgraphix/lib/e… | 199 +++++++++++++++++++++++++++++++
A web/vendor/plugins/ezgraphix/lib/e… | 107 +++++++++++++++++++++++++++++++
A web/vendor/plugins/ezgraphix/lib/t… | 20 ++++++++++++++++++++
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 0
A web/vendor/plugins/ezgraphix/publi… | 362 +++++++++++++++++++++++++++++++
A web/vendor/plugins/ezgraphix/spec/… | 85 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/C… | 139 ++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/L… | 18 ++++++++++++++++++
A web/vendor/plugins/will_paginate/R… | 107 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/R… | 53 ++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/e… | 0
A web/vendor/plugins/will_paginate/e… | 69 ++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/e… | 92 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/e… | 91 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/e… | 91 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/i… | 1 +
A web/vendor/plugins/will_paginate/l… | 90 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/l… | 16 ++++++++++++++++
A web/vendor/plugins/will_paginate/l… | 144 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/l… | 43 ++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/l… | 264 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/l… | 170 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/l… | 37 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/l… | 9 +++++++++
A web/vendor/plugins/will_paginate/l… | 410 ++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 21 +++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 143 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 8 ++++++++
A web/vendor/plugins/will_paginate/t… | 22 ++++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 496 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 3 +++
A web/vendor/plugins/will_paginate/t… | 14 ++++++++++++++
A web/vendor/plugins/will_paginate/t… | 14 ++++++++++++++
A web/vendor/plugins/will_paginate/t… | 17 +++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 6 ++++++
A web/vendor/plugins/will_paginate/t… | 29 +++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 7 +++++++
A web/vendor/plugins/will_paginate/t… | 38 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 12 ++++++++++++
A web/vendor/plugins/will_paginate/t… | 30 ++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 2 ++
A web/vendor/plugins/will_paginate/t… | 35 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 37 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 43 ++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 76 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 11 +++++++++++
A web/vendor/plugins/will_paginate/t… | 179 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 59 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/t… | 373 +++++++++++++++++++++++++++++++
A web/vendor/plugins/will_paginate/w… | 22 ++++++++++++++++++++++
246 files changed, 18930 insertions(+), 50 deletions(-)
---
(DIR) diff --git a/Makefile b/Makefile
@@ -3,7 +3,7 @@ all: test
test: install
bin/verify_install.rb
-install: iaxrecord dtmf2num ruby-kissfft db
+install: bundler iaxrecord dtmf2num ruby-kissfft db
cp -a src/iaxrecord/iaxrecord bin/
cp -a src/dtmf2num/dtmf2num bin/
@@ -29,6 +29,16 @@ db_null:
web/db/production.sqlite3: ruby-kissfft-install
(cd web; RAILS_ENV=production rake db:migrate )
+bundler:
+ @echo "Checking for RubyGems and the Bundler gem..."
+ @ruby -rrubygems -e 'require "bundler"; puts "OK"'
+
+ @echo "Validating that 'bundle' is in the path..."
+ which bundle
+
+ @echo "Installing missing gems as needed.."
+ (cd web; bundle install)
+
clean:
( cd src/ruby-kissfft/; ruby extconf.rb )
make -C src/ruby-kissfft/ clean
(DIR) diff --git a/bin/verify_install.rb b/bin/verify_install.rb
@@ -22,6 +22,25 @@ puts("* *")
puts("**********************************************************************")
puts(" ")
+
+begin
+ require 'rubygems'
+ puts "[*] RubyGems have been installed"
+rescue ::LoadError
+ puts "[*] ERROR: The RubyGems package has not been installed:"
+ puts " $ sudo apt-get install rubygems"
+ exit
+end
+
+begin
+ require 'bundler'
+ puts "[*] The Bundler gem has been installed"
+rescue ::LoadError
+ puts "[*] ERROR: The Bundler gem has not been installed:"
+ puts " $ sudo gem install bundler"
+ exit
+end
+
begin
require 'kissfft'
puts "[*] The KissFFT module appears to be available"
(DIR) diff --git a/bin/warvox.rb b/bin/warvox.rb
@@ -1,6 +1,9 @@
#!/usr/bin/env ruby
###################
+require 'getoptlong'
+
+
#
# Load the library path
#
@@ -10,10 +13,9 @@ while File.symlink?(base)
end
voxroot = File.join(File.dirname(base), '..', 'web')
-Dir.chdir(voxroot)
-require 'getoptlong'
+voxserv = File.join(File.expand_path(voxroot), 'script', 'rails')
-voxserv = File.join('script', 'server')
+Dir.chdir(voxroot)
def usage
$stderr.puts "#{$0} [--address IP] [--port PORT] --background"
@@ -47,20 +49,15 @@ args.each do |opt,arg|
end
end
-
-# Clear ARGV
-while(ARGV.length > 0)
- ARGV.shift
-end
-
-# Rebuild ARGV
-[
+args = [
+ 'server',
'-p', opts['ServerPort'].to_s,
'-b', opts['ServerHost'],
- '-e', 'production',
- (opts['Background'] ? '-d' : '')
-].each do |arg|
- ARGV.push arg
+ '-e', 'development',
+]
+
+if opts['Background']
+ args.push("-d")
end
$browser_url = "http://#{opts['ServerHost']}:#{opts['ServerPort']}/"
@@ -69,4 +66,7 @@ $stderr.puts ""
$stderr.puts "[*] Starting WarVOX on #{$browser_url}"
$stderr.puts ""
+while(ARGV.length > 0); ARGV.shift; end
+args.each {|arg| ARGV.push(arg) }
+
load(voxserv)
(DIR) diff --git a/docs/BUGS b/docs/BUGS
@@ -4,5 +4,7 @@ KNOWN BUGS
need to be removed and restarted.
* Using the sound card (playing music, videos, etc) while warvox is running
- can cause the dialing process to handle. Systems without sound cards are
- not affected.
+ can cause the dialing process to hang.
+
+ * Systems without sound cards may not be able to record any audio. Reports
+ have varied depending on the Linux distribution and version.
(DIR) diff --git a/docs/ChangeLog b/docs/ChangeLog
@@ -1,3 +1,9 @@
+2010-11-03 H D Moore <hdm[at]metasploit.com>
+ * bumped the version number to 1.1.0
+ * upgraded the web interface to Rails 3
+ * added support for Ruby 1.9.1
+ * updated README and LICENSE
+
2009-05-25 H D Moore <hdm[at]metasploit.com>
* switched MP3 quality to 32kbps from 8kpbs for better listening
* added bin/warvox.agi as an asterisk plugin to allow job re-dialing
@@ -7,7 +13,6 @@
* added dtmf2num support to decode dtmf tones
2009-05-10 H D Moore <hdm[at]metasploit.com>
-
* release of version 1.0.1
* switched to the BSD license for WarVOX
* swapped the sox -w flag to -2 (deprecated)
@@ -18,5 +23,4 @@
* added getopt parser to iaxrecord.c
2009-03-05 H D Moore <hdm[at]metasploit.com>
-
* initial public release of version 1.0.0
(DIR) diff --git a/docs/LICENSE b/docs/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2009, Metasploit LLC
+Copyright (c) 2009-2010, Rapid7 LLC
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
@@ -11,7 +11,7 @@ are permitted provided that the following conditions are met:
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- * Neither the name of Metasploit LLC nor the names of its contributors
+ * Neither the name of Rapid7 LLC nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
@@ -30,7 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
WarVOX is provided under the BSD license above.
-The copyright on this package is held by Metasploit LLC.
+The copyright on this package is held by Rapid7 LLC.
This copyright and license does not apply to the following components:
- The ruby on rails vendor libraries under web/vendor/ (Ruby license)
@@ -39,9 +39,6 @@ This copyright and license does not apply to the following components:
The latest version of this software is available from http://warvox.org/
-Bug tracking and development information can be found at:
- http://trac.metasploit.com/ (component: warvox)
-
Questions and suggestions can be sent to:
warvox[at]metasploit.com
(DIR) diff --git a/docs/README b/docs/README
@@ -5,8 +5,12 @@ WarVOX Quick Start Guide
Ubuntu 8.10 or BackTrack 4 (beta+)
$ sudo apt-get install libiaxclient-dev sox lame ruby rake \
rubygems libsqlite3-ruby libopenssl-ruby gnuplot ruby-dev
+
+ $ sudo apt-get install rubygems
+
+ $ sudo gem install bundler
- For better performance, install the 'mongrel' gem
+ For better performance, install the 'mongrel' gem (on ruby 1.8.7)
$ gem install mongrel
2. Build the WarVOX tools and modules
(DIR) diff --git a/etc/sigs/01.default.rb b/etc/sigs/01.default.rb
@@ -44,7 +44,7 @@ data[:ecnt] = ecnt
#
if( (fcnt[2100] > 1.0 or fcnt[2230] > 1.0) and fcnt[2250] > 0.5)
@line_type = 'modem'
- break
+ raise Completed
end
#
@@ -52,7 +52,7 @@ end
#
if(fcnt[2100] > 1.0 and (maxf > 2245.0 and maxf < 2255.0))
@line_type = 'modem'
- break
+ raise Completed
end
#
@@ -60,7 +60,7 @@ end
#
if(fcnt[2100] > 1.0 and (maxf > 2995.0 and maxf < 3005.0))
@line_type = 'modem'
- break
+ raise Completed
end
#
@@ -75,7 +75,7 @@ fax_sum = 0
].map{|x| fax_sum += [x,1.0].min }
if(fax_sum >= 2.0)
@line_type = 'fax'
- break
+ raise Completed
end
#
@@ -83,7 +83,7 @@ end
#
if(fcnt[440] > 1.0 and fcnt[350] > 1.0)
@line_type = 'dialtone'
- break
+ raise Completed
end
#
(DIR) diff --git a/etc/sigs/99.default.rb b/etc/sigs/99.default.rb
@@ -19,7 +19,7 @@ scnt = data[:scnt]
# is often a different frequency entirely.
if(fcnt[1000] >= 1.0)
@line_type = 'voicemail'
- break
+ raise Completed
end
# Look for voicemail by detecting a peak frequency of
@@ -27,7 +27,7 @@ end
# the fallback script.
if(maxf > 995 and maxf < 1005)
@line_type = 'voicemail'
- break
+ raise Completed
end
#
@@ -35,12 +35,12 @@ end
#
if(freq.map{|f| f.length}.inject(:+) == 0)
@line_type = 'silence'
- break
+ raise Completed
end
if(ecnt == scnt)
@line_type = 'silence'
- break
+ raise Completed
end
#
(DIR) diff --git a/lib/warvox.rb b/lib/warvox.rb
@@ -11,7 +11,7 @@ require 'warvox/db'
# Global configuration
module WarVOX
- VERSION = '1.0.2'
+ VERSION = '1.1.0'
Base = File.expand_path(File.join(File.dirname(__FILE__), '..'))
Conf = File.expand_path(File.join(Base, 'etc', 'warvox.conf'))
JobManager = WarVOX::JobQueue.new
(DIR) diff --git a/lib/warvox/jobs.rb b/lib/warvox/jobs.rb
@@ -23,8 +23,13 @@ class JobQueue
end
def schedule(klass, job_id)
+ begin
return false if scheduled?(klass, job_id)
@queue.push(klass.new(job_id))
+ rescue ::Exception => e
+ $stderr.puts "ERROR!!!!!: #{e} #{e.backtrace}"
+ false
+ end
end
def manage_queue
(DIR) diff --git a/lib/warvox/jobs/analysis.rb b/lib/warvox/jobs/analysis.rb
@@ -15,6 +15,10 @@ class Analysis < Base
end
class SignalProcessor
+
+ class Completed < RuntimeError
+ end
+
attr_accessor :line_type
attr_accessor :signatures
attr_accessor :data
@@ -25,9 +29,9 @@ class Analysis < Base
end
def proc(str)
- while(true);
+ begin
eval(str)
- break
+ rescue Completed
end
end
end
(DIR) diff --git a/lib/warvox/phone.rb b/lib/warvox/phone.rb
@@ -2,7 +2,11 @@ module WarVOX
class Phone
# Convert 123456XXXX to an array of expanded numbers
- def self.crack_mask(masks)
+ def self.crack_mask(mask)
+ self.crack_masks([mask])
+ end
+
+ def self.crack_masks(masks)
res = {}
masks.each do |mask|
mask = mask.strip
(DIR) diff --git a/src/ruby-kissfft/main.c b/src/ruby-kissfft/main.c
@@ -1,13 +1,13 @@
/*
ruby-kissfft: a simple ruby module embedding the Kiss FFT library
- Copyright (C) 2009 H D Moore <hdm[at]metasploit.com>
+ Copyright (C) 2009-2010 Rapid7 LLC - H D Moore <hdm[at]metasploit.com>
Derived from "psdpng.c" from the KissFFT tools directory
Copyright (C) 2003-2006 Mark Borgerding
*/
#include "ruby.h"
-#include "rubysig.h"
+
#include <stdlib.h>
#include <math.h>
@@ -39,11 +39,8 @@ rbkiss_s_fftr(VALUE class, VALUE r_nfft, VALUE r_rate, VALUE r_buckets, VALUE r_
kiss_fft_cpx *fbuf;
float *mag2buf;
int i;
- int n;
- int sidx;
int avgctr=0;
int nrows=0;
- int stereo=0;
int nfft;
int rate;
@@ -78,11 +75,11 @@ rbkiss_s_fftr(VALUE class, VALUE r_nfft, VALUE r_rate, VALUE r_buckets, VALUE r_
return Qnil;
}
- if(RARRAY(r_data)->len == 0) {
+ if(RARRAY_LEN(r_data) == 0) {
return Qnil;
}
- if(TYPE(RARRAY(r_data)->ptr[0]) != T_FIXNUM ) {
+ if(TYPE(RARRAY_PTR(r_data)[0]) != T_FIXNUM ) {
return Qnil;
}
@@ -95,7 +92,7 @@ rbkiss_s_fftr(VALUE class, VALUE r_nfft, VALUE r_rate, VALUE r_buckets, VALUE r_
memset(mag2buf,0,sizeof(mag2buf)*nfreqs);
- inp_len = RARRAY(r_data)->len;
+ inp_len = RARRAY_LEN(r_data);
inp_idx = 0;
while(inp_idx < inp_len) {
@@ -105,10 +102,10 @@ rbkiss_s_fftr(VALUE class, VALUE r_nfft, VALUE r_rate, VALUE r_buckets, VALUE r_
if(inp_idx + i >= inp_len) {
tbuf[i] = 0;
} else {
- if(TYPE(RARRAY(r_data)->ptr[ inp_idx + i ]) != T_FIXNUM) {
+ if(TYPE(RARRAY_PTR(r_data)[ inp_idx + i ]) != T_FIXNUM) {
tbuf[i] = 0;
} else {
- tbuf[i] = NUM2INT( RARRAY(r_data)->ptr[ inp_idx + i ] );
+ tbuf[i] = NUM2INT( RARRAY_PTR(r_data)[ inp_idx + i ] );
}
}
}
(DIR) diff --git a/web/Gemfile b/web/Gemfile
@@ -0,0 +1,30 @@
+source 'http://rubygems.org'
+
+gem 'rails', '3.0.1'
+
+# Bundle edge Rails instead:
+# gem 'rails', :git => 'git://github.com/rails/rails.git'
+
+gem 'sqlite3-ruby', :require => 'sqlite3'
+
+# Use unicorn as the web server
+# gem 'unicorn'
+
+# Deploy with Capistrano
+# gem 'capistrano'
+
+# To use debugger
+# gem 'ruby-debug'
+
+# Bundle the extra gems:
+# gem 'bj'
+# gem 'nokogiri'
+# gem 'sqlite3-ruby', :require => 'sqlite3'
+# gem 'aws-s3', :require => 'aws/s3'
+
+# Bundle gems for the local environment. Make sure to
+# put test-only gems in this group so their generators
+# and rake tasks are available in development mode:
+# group :development, :test do
+# gem 'webrat'
+# end
(DIR) diff --git a/web/Gemfile.lock b/web/Gemfile.lock
@@ -0,0 +1,74 @@
+GEM
+ remote: http://rubygems.org/
+ specs:
+ abstract (1.0.0)
+ actionmailer (3.0.1)
+ actionpack (= 3.0.1)
+ mail (~> 2.2.5)
+ actionpack (3.0.1)
+ activemodel (= 3.0.1)
+ activesupport (= 3.0.1)
+ builder (~> 2.1.2)
+ erubis (~> 2.6.6)
+ i18n (~> 0.4.1)
+ rack (~> 1.2.1)
+ rack-mount (~> 0.6.12)
+ rack-test (~> 0.5.4)
+ tzinfo (~> 0.3.23)
+ activemodel (3.0.1)
+ activesupport (= 3.0.1)
+ builder (~> 2.1.2)
+ i18n (~> 0.4.1)
+ activerecord (3.0.1)
+ activemodel (= 3.0.1)
+ activesupport (= 3.0.1)
+ arel (~> 1.0.0)
+ tzinfo (~> 0.3.23)
+ activeresource (3.0.1)
+ activemodel (= 3.0.1)
+ activesupport (= 3.0.1)
+ activesupport (3.0.1)
+ arel (1.0.1)
+ activesupport (~> 3.0.0)
+ builder (2.1.2)
+ erubis (2.6.6)
+ abstract (>= 1.0.0)
+ i18n (0.4.2)
+ mail (2.2.9)
+ activesupport (>= 2.3.6)
+ i18n (~> 0.4.1)
+ mime-types (~> 1.16)
+ treetop (~> 1.4.8)
+ mime-types (1.16)
+ polyglot (0.3.1)
+ rack (1.2.1)
+ rack-mount (0.6.13)
+ rack (>= 1.0.0)
+ rack-test (0.5.6)
+ rack (>= 1.0)
+ rails (3.0.1)
+ actionmailer (= 3.0.1)
+ actionpack (= 3.0.1)
+ activerecord (= 3.0.1)
+ activeresource (= 3.0.1)
+ activesupport (= 3.0.1)
+ bundler (~> 1.0.0)
+ railties (= 3.0.1)
+ railties (3.0.1)
+ actionpack (= 3.0.1)
+ activesupport (= 3.0.1)
+ rake (>= 0.8.4)
+ thor (~> 0.14.0)
+ rake (0.8.7)
+ sqlite3-ruby (1.3.2)
+ thor (0.14.3)
+ treetop (1.4.8)
+ polyglot (>= 0.3.1)
+ tzinfo (0.3.23)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ rails (= 3.0.1)
+ sqlite3-ruby
(DIR) diff --git a/web/README b/web/README
@@ -0,0 +1,256 @@
+== Welcome to Rails
+
+Rails is a web-application framework that includes everything needed to create
+database-backed web applications according to the Model-View-Control pattern.
+
+This pattern splits the view (also called the presentation) into "dumb"
+templates that are primarily responsible for inserting pre-built data in between
+HTML tags. The model contains the "smart" domain objects (such as Account,
+Product, Person, Post) that holds all the business logic and knows how to
+persist themselves to a database. The controller handles the incoming requests
+(such as Save New Account, Update Product, Show Post) by manipulating the model
+and directing data to the view.
+
+In Rails, the model is handled by what's called an object-relational mapping
+layer entitled Active Record. This layer allows you to present the data from
+database rows as objects and embellish these data objects with business logic
+methods. You can read more about Active Record in
+link:files/vendor/rails/activerecord/README.html.
+
+The controller and view are handled by the Action Pack, which handles both
+layers by its two parts: Action View and Action Controller. These two layers
+are bundled in a single package due to their heavy interdependence. This is
+unlike the relationship between the Active Record and Action Pack that is much
+more separate. Each of these packages can be used independently outside of
+Rails. You can read more about Action Pack in
+link:files/vendor/rails/actionpack/README.html.
+
+
+== Getting Started
+
+1. At the command prompt, create a new Rails application:
+ <tt>rails new myapp</tt> (where <tt>myapp</tt> is the application name)
+
+2. Change directory to <tt>myapp</tt> and start the web server:
+ <tt>cd myapp; rails server</tt> (run with --help for options)
+
+3. Go to http://localhost:3000/ and you'll see:
+ "Welcome aboard: You're riding Ruby on Rails!"
+
+4. Follow the guidelines to start developing your application. You can find
+the following resources handy:
+
+* The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html
+* Ruby on Rails Tutorial Book: http://www.railstutorial.org/
+
+
+== Debugging Rails
+
+Sometimes your application goes wrong. Fortunately there are a lot of tools that
+will help you debug it and get it back on the rails.
+
+First area to check is the application log files. Have "tail -f" commands
+running on the server.log and development.log. Rails will automatically display
+debugging and runtime information to these files. Debugging info will also be
+shown in the browser on requests from 127.0.0.1.
+
+You can also log your own messages directly into the log file from your code
+using the Ruby logger class from inside your controllers. Example:
+
+ class WeblogController < ActionController::Base
+ def destroy
+ @weblog = Weblog.find(params[:id])
+ @weblog.destroy
+ logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!")
+ end
+ end
+
+The result will be a message in your log file along the lines of:
+
+ Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1!
+
+More information on how to use the logger is at http://www.ruby-doc.org/core/
+
+Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are
+several books available online as well:
+
+* Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe)
+* Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide)
+
+These two books will bring you up to speed on the Ruby language and also on
+programming in general.
+
+
+== Debugger
+
+Debugger support is available through the debugger command when you start your
+Mongrel or WEBrick server with --debugger. This means that you can break out of
+execution at any point in the code, investigate and change the model, and then,
+resume execution! You need to install ruby-debug to run the server in debugging
+mode. With gems, use <tt>sudo gem install ruby-debug</tt>. Example:
+
+ class WeblogController < ActionController::Base
+ def index
+ @posts = Post.find(:all)
+ debugger
+ end
+ end
+
+So the controller will accept the action, run the first line, then present you
+with a IRB prompt in the server window. Here you can do things like:
+
+ >> @posts.inspect
+ => "[#<Post:0x14a6be8
+ @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}>,
+ #<Post:0x14a6620
+ @attributes={"title"=>"Rails", "body"=>"Only ten..", "id"=>"2"}>]"
+ >> @posts.first.title = "hello from a debugger"
+ => "hello from a debugger"
+
+...and even better, you can examine how your runtime objects actually work:
+
+ >> f = @posts.first
+ => #<Post:0x13630c4 @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}>
+ >> f.
+ Display all 152 possibilities? (y or n)
+
+Finally, when you're ready to resume execution, you can enter "cont".
+
+
+== Console
+
+The console is a Ruby shell, which allows you to interact with your
+application's domain model. Here you'll have all parts of the application
+configured, just like it is when the application is running. You can inspect
+domain models, change values, and save to the database. Starting the script
+without arguments will launch it in the development environment.
+
+To start the console, run <tt>rails console</tt> from the application
+directory.
+
+Options:
+
+* Passing the <tt>-s, --sandbox</tt> argument will rollback any modifications
+ made to the database.
+* Passing an environment name as an argument will load the corresponding
+ environment. Example: <tt>rails console production</tt>.
+
+To reload your controllers and models after launching the console run
+<tt>reload!</tt>
+
+More information about irb can be found at:
+link:http://www.rubycentral.com/pickaxe/irb.html
+
+
+== dbconsole
+
+You can go to the command line of your database directly through <tt>rails
+dbconsole</tt>. You would be connected to the database with the credentials
+defined in database.yml. Starting the script without arguments will connect you
+to the development database. Passing an argument will connect you to a different
+database, like <tt>rails dbconsole production</tt>. Currently works for MySQL,
+PostgreSQL and SQLite 3.
+
+== Description of Contents
+
+The default directory structure of a generated Ruby on Rails application:
+
+ |-- app
+ | |-- controllers
+ | |-- helpers
+ | |-- models
+ | `-- views
+ | `-- layouts
+ |-- config
+ | |-- environments
+ | |-- initializers
+ | `-- locales
+ |-- db
+ |-- doc
+ |-- lib
+ | `-- tasks
+ |-- log
+ |-- public
+ | |-- images
+ | |-- javascripts
+ | `-- stylesheets
+ |-- script
+ | `-- performance
+ |-- test
+ | |-- fixtures
+ | |-- functional
+ | |-- integration
+ | |-- performance
+ | `-- unit
+ |-- tmp
+ | |-- cache
+ | |-- pids
+ | |-- sessions
+ | `-- sockets
+ `-- vendor
+ `-- plugins
+
+app
+ Holds all the code that's specific to this particular application.
+
+app/controllers
+ Holds controllers that should be named like weblogs_controller.rb for
+ automated URL mapping. All controllers should descend from
+ ApplicationController which itself descends from ActionController::Base.
+
+app/models
+ Holds models that should be named like post.rb. Models descend from
+ ActiveRecord::Base by default.
+
+app/views
+ Holds the template files for the view that should be named like
+ weblogs/index.html.erb for the WeblogsController#index action. All views use
+ eRuby syntax by default.
+
+app/views/layouts
+ Holds the template files for layouts to be used with views. This models the
+ common header/footer method of wrapping views. In your views, define a layout
+ using the <tt>layout :default</tt> and create a file named default.html.erb.
+ Inside default.html.erb, call <% yield %> to render the view using this
+ layout.
+
+app/helpers
+ Holds view helpers that should be named like weblogs_helper.rb. These are
+ generated for you automatically when using generators for controllers.
+ Helpers can be used to wrap functionality for your views into methods.
+
+config
+ Configuration files for the Rails environment, the routing map, the database,
+ and other dependencies.
+
+db
+ Contains the database schema in schema.rb. db/migrate contains all the
+ sequence of Migrations for your schema.
+
+doc
+ This directory is where your application documentation will be stored when
+ generated using <tt>rake doc:app</tt>
+
+lib
+ Application specific libraries. Basically, any kind of custom code that
+ doesn't belong under controllers, models, or helpers. This directory is in
+ the load path.
+
+public
+ The directory available for the web server. Contains subdirectories for
+ images, stylesheets, and javascripts. Also contains the dispatchers and the
+ default HTML files. This should be set as the DOCUMENT_ROOT of your web
+ server.
+
+script
+ Helper scripts for automation and generation.
+
+test
+ Unit and functional tests along with fixtures. When using the rails generate
+ command, template test files will be generated for you and placed in this
+ directory.
+
+vendor
+ External libraries that the application depends on. Also includes the plugins
+ subdirectory. If the app has frozen rails, those gems also go here, under
+ vendor/rails/. This directory is in the load path.
(DIR) diff --git a/web/Rakefile b/web/Rakefile
@@ -0,0 +1,7 @@
+# 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.expand_path('../config/application', __FILE__)
+require 'rake'
+
+Web::Application.load_tasks
(DIR) diff --git a/web/app/controllers/analyze_controller.rb b/web/app/controllers/analyze_controller.rb
@@ -0,0 +1,148 @@
+class AnalyzeController < ApplicationController
+ layout 'warvox'
+
+ def index
+ @jobs = DialJob.paginate_all_by_processed(
+ true,
+ :page => params[:page],
+ :order => 'id DESC',
+ :per_page => 30
+ )
+ end
+
+ def view
+ @job_id = params[:id]
+ @dial_job = DialJob.find(@job_id)
+ @shown = params[:show]
+
+ @g1 = Ezgraphix::Graphic.new(:c_type => 'col3d', :div_name => 'calls_pie1')
+ @g1.render_options(:caption => 'Detected Lines by Type', :y_name => 'Lines', :w => 700, :h => 300)
+
+ ltypes = DialResult.find( :all, :select => 'DISTINCT line_type', :conditions => ["dial_job_id = ?", @job_id] ).map{|r| r.line_type}
+ res_types = {}
+
+ ltypes.each do |k|
+ next if not k
+ res_types[k.capitalize.to_sym] = DialResult.count(
+ :conditions => ['dial_job_id = ? and line_type = ?', @job_id, k]
+ )
+ end
+
+ @g1.data = res_types
+
+ if(@shown and @shown != 'all')
+ @results = DialResult.paginate_all_by_dial_job_id(
+ @job_id,
+ :page => params[:page],
+ :order => 'number ASC',
+ :per_page => 10,
+ :conditions => [ 'completed = ? and processed = ? and busy = ? and line_type = ?', true, true, false, @shown ]
+ )
+ else
+ @results = DialResult.paginate_all_by_dial_job_id(
+ @job_id,
+ :page => params[:page],
+ :order => 'number ASC',
+ :per_page => 10,
+ :conditions => [ 'completed = ? and processed = ? and busy = ?', true, true, false ]
+ )
+ end
+
+ @filters = []
+ @filters << { :scope => "all", :label => "All" }
+ res_types.keys.each do |t|
+ @filters << { :scope => t.to_s.downcase, :label => t.to_s }
+ end
+
+ end
+
+ def show
+ @job_id = params[:id]
+ @dial_job = DialJob.find(@job_id)
+ @shown = params[:show]
+
+ @g1 = Ezgraphix::Graphic.new(:c_type => 'col3d', :div_name => 'calls_pie1')
+ @g1.render_options(:caption => 'Detected Lines by Type', :y_name => 'Lines', :w => 700, :h => 300)
+
+ ltypes = DialResult.find( :all, :select => 'DISTINCT line_type', :conditions => ["dial_job_id = ?", @job_id] ).map{|r| r.line_type}
+ res_types = {}
+
+ ltypes.each do |k|
+ next if not k
+ res_types[k.capitalize.to_sym] = DialResult.count(
+ :conditions => ['dial_job_id = ? and line_type = ?', @job_id, k]
+ )
+ end
+
+ @g1.data = res_types
+
+ if(@shown and @shown != 'all')
+ @results = DialResult.paginate_all_by_dial_job_id(
+ @job_id,
+ :page => params[:page],
+ :order => 'number ASC',
+ :per_page => 20,
+ :conditions => [ 'completed = ? and processed = ? and busy = ? and line_type = ?', true, true, false, @shown ]
+ )
+ else
+ @results = DialResult.paginate_all_by_dial_job_id(
+ @job_id,
+ :page => params[:page],
+ :order => 'number ASC',
+ :per_page => 20,
+ :conditions => [ 'completed = ? and processed = ? and busy = ?', true, true, false ]
+ )
+ end
+
+ @filters = []
+ @filters << { :scope => "all", :label => "All" }
+ res_types.keys.each do |t|
+ @filters << { :scope => t.to_s.downcase, :label => t.to_s }
+ end
+
+ end
+
+
+ # GET /dial_results/1/resource?id=XXX&type=YYY
+ def resource
+ ctype = 'text/html'
+ cpath = nil
+
+ res = DialResult.find(params[:result_id])
+ if(res and res.processed and res.rawfile)
+ case params[:type]
+ when 'big_sig'
+ ctype = 'image/png'
+ cpath = res.rawfile.gsub(/\..*/, '') + '_big.png'
+ when 'big_sig_dots'
+ ctype = 'image/png'
+ cpath = res.rawfile.gsub(/\..*/, '') + '_big_dots.png'
+ when 'small_sig'
+ ctype = 'image/png'
+ cpath = res.rawfile.gsub(/\..*/, '') + '.png'
+ when 'mp3'
+ ctype = 'audio/mpeg'
+ cpath = res.rawfile.gsub(/\..*/, '') + '.mp3'
+ when 'sig'
+ ctype = 'text/plain'
+ cpath = res.rawfile.gsub(/\..*/, '') + '.sig'
+ when 'raw'
+ ctype = 'octet/binary-stream'
+ cpath = res.rawfile
+ when 'big_freq'
+ ctype = 'image/png'
+ cpath = res.rawfile.gsub(/\..*/, '') + '_freq_big.png'
+ when 'small_freq'
+ ctype = 'image/png'
+ cpath = res.rawfile.gsub(/\..*/, '') + '_freq.png'
+ end
+ end
+
+ cdata = "File not found"
+ if(cpath and File.readable?(cpath))
+ cdata = File.read(cpath, File.size(cpath))
+ end
+
+ send_data(cdata, :type => ctype, :disposition => 'inline')
+ end
+end
(DIR) diff --git a/web/app/controllers/application_controller.rb b/web/app/controllers/application_controller.rb
@@ -0,0 +1,54 @@
+class ApplicationController < ActionController::Base
+ helper :all
+ protect_from_forgery
+ before_filter :get_auth
+
+private
+
+ def get_creds
+ username = nil
+ password = nil
+
+ headers = %W{X-HTTP_AUTHORIZATION REDIRECT_X_HTTP_AUTHORIZATION HTTP_AUTHORIZATION}
+
+ headers.each do |head|
+ blob = request.env[head]
+ next if not blob
+
+ meth,blob = blob.split(/\s+/)
+ next if not blob
+ next if meth.downcase != 'basic'
+
+ username,password = blob.unpack('m*')[0].split(':', 2)
+ break if (username and username.length > 0)
+ end
+
+ [username, password]
+ end
+
+ def check_auth
+ return true if session.data[:user]
+ user,pass = get_creds
+ return false if not (user and pass)
+
+ if(WarVOX::Config.authenticate(user,pass))
+ session.data[:user] = user
+ return true
+ end
+
+ return false
+ end
+
+ def get_auth
+ return true
+
+ if(not check_auth())
+ response.headers["Status"] = "Unauthorized"
+ response.headers["WWW-Authenticate"] = 'Basic realm="WarVOX Console"'
+ render :text => "Authentication Failure", :status => 401
+ return
+ end
+ true
+ end
+
+end
(DIR) diff --git a/web/app/controllers/dial_jobs_controller.rb b/web/app/controllers/dial_jobs_controller.rb
@@ -0,0 +1,123 @@
+class DialJobsController < ApplicationController
+ layout 'warvox'
+
+ # GET /dial_jobs
+ # GET /dial_jobs.xml
+ def index
+ @submitted_jobs = DialJob.find_all_by_status('submitted')
+ @active_jobs = DialJob.find_all_by_status('active')
+ @new_job = DialJob.new
+ respond_to do |format|
+ format.html # index.html.erb
+ format.xml { render :xml => @active_jobs + @submitted_jobs }
+ end
+ end
+
+ # GET /dial_jobs/new
+ # GET /dial_jobs/new.xml
+ def new
+ @dial_job = DialJob.new
+ respond_to do |format|
+ format.html # new.html.erb
+ format.xml { render :xml => @dial_job }
+ end
+ end
+
+=begin
+ # GET /dial_jobs/1/edit
+ def edit
+ @dial_job = DialJob.find(params[:id])
+ end
+=end
+
+
+ # GET /dial_jobs/1/run
+ def run
+ @dial_job = DialJob.find(params[:id])
+
+ if(@dial_job.status != 'submitted')
+ flash[:notice] = 'Job is already running or completed'
+ return
+ end
+
+ WarVOX::JobManager.schedule(::WarVOX::Jobs::Dialer, @dial_job.id)
+ redirect_to :action => 'index'
+ end
+
+ def stop
+ @dial_job = DialJob.find(params[:id])
+
+ if(@dial_job.status != 'submitted')
+ flash[:notice] = 'Job is already running or completed'
+ return
+ end
+ end
+
+
+ # POST /dial_jobs
+ # POST /dial_jobs.xml
+ def create
+
+ @dial_job = DialJob.new(params[:dial_job])
+
+ if(Provider.find_all_by_enabled(true).length == 0)
+ @dial_job.errors.add("No providers have been configured or enabled, this job cannot be run")
+ respond_to do |format|
+ format.html { render :action => "new" }
+ format.xml { render :xml => @dial_job.errors, :status => :unprocessable_entity }
+ end
+ return
+ end
+
+ @dial_job.status = 'submitted'
+ @dial_job.progress = 0
+ @dial_job.started_at = nil
+ @dial_job.completed_at = nil
+ @dial_job.range.gsub!(/[^0-9X:,\n]/, '')
+ @dial_job.cid_mask.gsub!(/[^0-9X]/, '') if @dial_job.cid_mask != "SELF"
+
+ if(@dial_job.range_file.to_s != "")
+ @dial_job.range = @dial_job.range_file.read.gsub!(/[^0-9X:,\n]/, '')
+ end
+
+ respond_to do |format|
+ if @dial_job.save
+ flash[:notice] = 'Job was successfully created.'
+
+ # Launch it
+ WarVOX::JobManager.schedule(::WarVOX::Jobs::Dialer, @dial_job.id)
+
+ format.html { redirect_to(@dial_job) }
+ format.xml { render :xml => @dial_job, :status => :created, :location => @dial_job }
+ else
+ format.html { render :action => "new" }
+ format.xml { render :xml => @dial_job.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ # DELETE /dial_jobs/1
+ # DELETE /dial_jobs/1.xml
+ def destroy
+ @dial_job = DialJob.find(params[:id])
+ @dial_job.destroy
+
+ respond_to do |format|
+ format.html { redirect_to(dial_jobs_url) }
+ format.xml { head :ok }
+ end
+ end
+
+ # GET /dial_jobs/1
+ # GET /dial_jobs/1.xml
+ def show
+ @dial_job = DialJob.find(params[:id])
+
+ respond_to do |format|
+ format.html # show.html.erb
+ format.xml { render :xml => @dial_job }
+ end
+ end
+
+
+end
(DIR) diff --git a/web/app/controllers/dial_results_controller.rb b/web/app/controllers/dial_results_controller.rb
@@ -0,0 +1,204 @@
+class DialResultsController < ApplicationController
+ layout 'warvox'
+
+ # GET /dial_results
+ # GET /dial_results.xml
+ def index
+ @completed_jobs = DialJob.paginate_all_by_status(
+ 'completed',
+ :page => params[:page],
+ :order => 'id DESC',
+ :per_page => 30
+
+ )
+
+ respond_to do |format|
+ format.html # index.html.erb
+ format.xml { render :xml => @dial_results }
+ end
+ end
+
+ # GET /dial_results/1/reanalyze
+ def reanalyze
+ DialResult.update_all(['processed = ?', false], ['dial_job_id = ?', params[:id]])
+ j = DialJob.find(params[:id])
+ j.processed = false
+ j.save
+
+ redirect_to :action => 'analyze'
+ end
+
+ # GET /dial_results/1/process
+ # GET /dial_results/1/process.xml
+ def analyze
+ @job_id = params[:id]
+ @job = DialJob.find(@job_id)
+
+ if(@job.processed)
+ redirect_to :controller => 'analyze', :action => 'view', :id => @job_id
+ return
+ end
+
+ @dial_data_total = DialResult.count(
+ :conditions => [ 'dial_job_id = ? and completed = ?', @job_id, true ]
+ )
+
+ @dial_data_done = DialResult.count(
+ :conditions => [ 'dial_job_id = ? and processed = ?', @job_id, true ]
+ )
+
+ @g1 = Ezgraphix::Graphic.new(:c_type => 'col3d', :div_name => 'calls_pie1')
+ @g1.render_options(:caption => 'Detected Lines by Type', :y_name => 'Lines', :w => 700, :h => 300)
+
+ ltypes = DialResult.find( :all, :select => 'DISTINCT line_type', :conditions => ["dial_job_id = ?", @job_id] ).map{|r| r.line_type}
+ res_types = {}
+
+ ltypes.each do |k|
+ next if not k
+ res_types[k.capitalize.to_sym] = DialResult.count(
+ :conditions => ['dial_job_id = ? and line_type = ?', @job_id, k]
+ )
+ end
+
+ @g1.data = res_types
+
+ @dial_data_todo = DialResult.paginate_all_by_dial_job_id(
+ @job_id,
+ :page => params[:page],
+ :order => 'number ASC',
+ :per_page => 50,
+ :conditions => [ 'completed = ? and processed = ? and busy = ?', true, false, false ]
+ )
+
+ if(@dial_data_todo.length > 0)
+ WarVOX::JobManager.schedule(::WarVOX::Jobs::Analysis, @job_id)
+ end
+ end
+
+ # GET /dial_results/1/view
+ # GET /dial_results/1/view.xml
+ def view
+ @dial_results = DialResult.paginate_all_by_dial_job_id(
+ params[:id],
+ :page => params[:page],
+ :order => 'number ASC',
+ :per_page => 30
+ )
+
+ if(@dial_results)
+ @g1 = Ezgraphix::Graphic.new(:c_type => 'col3d', :div_name => 'calls_pie1')
+ @g1.render_options(:caption => 'Call Results', :w => 700, :h => 300)
+
+ @g1.data = {
+ :Timeout => DialResult.count(:conditions =>['dial_job_id = ? and completed = ?', params[:id], false]),
+ :Busy => DialResult.count(:conditions =>['dial_job_id = ? and busy = ?', params[:id], true]),
+ :Answered => DialResult.count(:conditions =>['dial_job_id = ? and completed = ?', params[:id], true]),
+ }
+ end
+
+ respond_to do |format|
+ format.html # index.html.erb
+ format.xml { render :xml => @dial_results }
+ end
+ end
+
+ # GET /dial_results/1
+ # GET /dial_results/1.xml
+ def show
+ @dial_result = DialResult.find(params[:id])
+
+ respond_to do |format|
+ format.html # show.html.erb
+ format.xml { render :xml => @dial_result }
+ end
+ end
+
+ # GET /dial_results/new
+ # GET /dial_results/new.xml
+ def new
+ @dial_result = DialResult.new
+
+ respond_to do |format|
+ format.html # new.html.erb
+ format.xml { render :xml => @dial_result }
+ end
+ end
+
+ # GET /dial_results/1/edit
+ def edit
+ @dial_result = DialResult.find(params[:id])
+ end
+
+ # POST /dial_results
+ # POST /dial_results.xml
+ def create
+ @dial_result = DialResult.new(params[:dial_result])
+
+ respond_to do |format|
+ if @dial_result.save
+ flash[:notice] = 'DialResult was successfully created.'
+ format.html { redirect_to(@dial_result) }
+ format.xml { render :xml => @dial_result, :status => :created, :location => @dial_result }
+ else
+ format.html { render :action => "new" }
+ format.xml { render :xml => @dial_result.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ # PUT /dial_results/1
+ # PUT /dial_results/1.xml
+ def update
+ @dial_result = DialResult.find(params[:id])
+
+ respond_to do |format|
+ if @dial_result.update_attributes(params[:dial_result])
+ flash[:notice] = 'DialResult was successfully updated.'
+ format.html { redirect_to(@dial_result) }
+ format.xml { head :ok }
+ else
+ format.html { render :action => "edit" }
+ format.xml { render :xml => @dial_result.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ # DELETE /dial_results/1
+ # DELETE /dial_results/1.xml
+ def purge
+
+ @job = DialJob.find(params[:id])
+ @job.dial_results.each do |r|
+ r.destroy
+ end
+ @job.destroy
+
+ dir = nil
+ jid = @job.id
+ dfd = Dir.new(WarVOX::Config.data_path)
+ dfd.entries.each do |ent|
+ j,m = ent.split('-', 2)
+ if (m and j == jid)
+ dir = File.join(WarVOX::Config.data_path, ent)
+ end
+ end
+
+ FileUtils.rm_rf(dir) if dir
+
+ respond_to do |format|
+ format.html { redirect_to :action => 'index' }
+ format.xml { head :ok }
+ end
+ end
+
+ # DELETE /dial_results/1
+ # DELETE /dial_results/1.xml
+ def delete
+ @res = DialResult.find(params[:id])
+ @res.destroy
+ respond_to do |format|
+ format.html { redirect_to :action => 'index' }
+ format.xml { head :ok }
+ end
+ end
+end
(DIR) diff --git a/web/app/controllers/home_controller.rb b/web/app/controllers/home_controller.rb
@@ -0,0 +1,16 @@
+class HomeController < ApplicationController
+ layout 'warvox'
+
+ def index
+
+ end
+
+ def about
+ begin
+ @has_kissfft = "MISSING"
+ require 'kissfft'
+ @has_kissfft = $LOADED_FEATURES.grep(/kissfft/)[0]
+ rescue ::LoadError
+ end
+ end
+end
(DIR) diff --git a/web/app/controllers/providers_controller.rb b/web/app/controllers/providers_controller.rb
@@ -0,0 +1,91 @@
+class ProvidersController < ApplicationController
+ layout 'warvox'
+
+ # GET /providers
+ # GET /providers.xml
+ def index
+ @providers = Provider.find(:all)
+ @new_provider = Provider.new
+ @new_provider.enabled = true
+
+ respond_to do |format|
+ format.html # index.html.erb
+ format.xml { render :xml => @providers }
+ end
+ end
+
+ # GET /providers/1
+ # GET /providers/1.xml
+ def show
+ @provider = Provider.find(params[:id])
+
+ respond_to do |format|
+ format.html # show.html.erb
+ format.xml { render :xml => @provider }
+ end
+ end
+
+ # GET /providers/new
+ # GET /providers/new.xml
+ def new
+ @provider = Provider.new
+ @provider.enabled = true
+
+ respond_to do |format|
+ format.html # new.html.erb
+ format.xml { render :xml => @provider }
+ end
+ end
+
+ # GET /providers/1/edit
+ def edit
+ @provider = Provider.find(params[:id])
+ end
+
+ # POST /providers
+ # POST /providers.xml
+ def create
+ @provider = Provider.new(params[:provider])
+ @provider.enabled = true
+
+ respond_to do |format|
+ if @provider.save
+ flash[:notice] = 'Provider was successfully created.'
+ format.html { redirect_to(@provider) }
+ format.xml { render :xml => @provider, :status => :created, :location => @provider }
+ else
+ format.html { render :action => "new" }
+ format.xml { render :xml => @provider.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ # PUT /providers/1
+ # PUT /providers/1.xml
+ def update
+ @provider = Provider.find(params[:id])
+
+ respond_to do |format|
+ if @provider.update_attributes(params[:provider])
+ flash[:notice] = 'Provider was successfully updated.'
+ format.html { redirect_to(@provider) }
+ format.xml { head :ok }
+ else
+ format.html { render :action => "edit" }
+ format.xml { render :xml => @provider.errors, :status => :unprocessable_entity }
+ end
+ end
+ end
+
+ # DELETE /providers/1
+ # DELETE /providers/1.xml
+ def destroy
+ @provider = Provider.find(params[:id])
+ @provider.destroy
+
+ respond_to do |format|
+ format.html { redirect_to(providers_url) }
+ format.xml { head :ok }
+ end
+ end
+end
(DIR) diff --git a/web/app/helpers/analyze_helper.rb b/web/app/helpers/analyze_helper.rb
@@ -0,0 +1,2 @@
+module AnalyzeHelper
+end
(DIR) diff --git a/web/app/helpers/application_helper.rb b/web/app/helpers/application_helper.rb
@@ -0,0 +1,70 @@
+# Methods added to this helper will be available to all templates in the application.
+module ApplicationHelper
+
+ def get_sections
+
+ count = 0
+ html = ""
+ asection = nil
+
+ sections =
+ [
+ { :name => 'Home', :link => '/', :controller => 'home', :subsections => [
+
+ ] },
+ { :name => 'Jobs' , :link => '/dial_jobs/', :controller => 'dial_jobs', :subsections => [
+ #
+ ] },
+ { :name => 'Results', :link => '/dial_results/', :controller => 'dial_results', :subsections => [
+ #
+ ] },
+ { :name => 'Analysis', :link => '/analyze/', :controller => 'analyze', :subsections => [
+ #
+ ] },
+ { :name => 'Providers', :link => '/providers/', :controller => 'providers', :subsections => [
+ #
+ ] },
+ { :name => 'About', :link => '/home/about/', :controller => 'home/about', :subsections => [
+ #
+ ] }
+ ]
+
+ html << "<div id='sections_container'>\n"
+ html << "<ul id='sections_ul'>\n"
+ sections.each do |section|
+ lactive = ''
+ if (params[:controller] == section[:controller])
+ lactive = "id='sections_active'"
+ asection = section
+ end
+ html << "<li><a #{lactive} href='#{section[:link]}'>#{section[:name]}</a></li>";
+ end
+ html << "\n</ul></div>\n"
+
+ count = 0
+ html << "<div id='subsections_container'>\n"
+ html << "<ul id='subsections_ul'>\n"
+ (asection ? asection[:subsections] : [] ).each do |section|
+ html << "<li><a href='#{section[:link]}'>#{section[:name]}</a></li>";
+ end
+ html << "\n</ul></div>\n"
+ raw(html)
+ end
+
+ def select_tag_for_filter(nvpairs, params)
+ _url = ( url_for :overwrite_params => { }).split('?')[0]
+ _html = %{<label for="show">Filter: </label>}
+ _html << %{<select name="show" id="show"}
+ _html << %{onchange="window.location='#{_url}' + '?show=' + this.value">}
+ nvpairs.each do |pair|
+ _html << %{<option value="#{pair[:scope]}"}
+ if params[:show] == pair[:scope] || ((params[:show].nil? || params[:show].empty?) && pair[:scope] == "all")
+ _html << %{ selected="selected"}
+ end
+ _html << %{>#{pair[:label]}}
+ _html << %{</option>}
+ end
+ _html << %{</select>}
+ raw(_html)
+ end
+end
(DIR) diff --git a/web/app/helpers/dial_jobs_helper.rb b/web/app/helpers/dial_jobs_helper.rb
@@ -0,0 +1,2 @@
+module DialJobsHelper
+end
(DIR) diff --git a/web/app/helpers/dial_results_helper.rb b/web/app/helpers/dial_results_helper.rb
@@ -0,0 +1,2 @@
+module DialResultsHelper
+end
(DIR) diff --git a/web/app/helpers/home_helper.rb b/web/app/helpers/home_helper.rb
@@ -0,0 +1,2 @@
+module HomeHelper
+end
(DIR) diff --git a/web/app/helpers/providers_helper.rb b/web/app/helpers/providers_helper.rb
@@ -0,0 +1,2 @@
+module ProvidersHelper
+end
(DIR) diff --git a/web/app/models/dial_job.rb b/web/app/models/dial_job.rb
@@ -0,0 +1,30 @@
+class DialJob < ActiveRecord::Base
+ attr_accessor :range_file
+
+ has_many :dial_results
+
+ validates_presence_of :range, :lines, :seconds
+ validates_numericality_of :lines, :less_than => 256, :greater_than => 0
+ validates_numericality_of :seconds, :less_than => 301, :greater_than => 0
+
+
+ validate :validate_range
+
+ def validate_range
+ if(range.gsub(/[^0-9X:,\n]/, '').empty?)
+ errors.add(:range, "must be at least 1 character long and made up of 0-9 and X as the mask.")
+ end
+
+ if(range.scan(/X/).length > 5)
+ errors.add(:range, "must contain no more than 5 mask digits.")
+ end
+
+ if(cid_mask != "SELF" and cid_mask.gsub(/[^0-9X]/, '').empty?)
+ errors.add(:range, "The Caller ID must be at least 1 character long and made up of 0-9 and X as the mask.")
+ end
+
+ if(cid_mask != "SELF" and cid_mask.scan(/X/).length > 5)
+ errors.add(:range, "The Caller ID must contain no more than 5 mask digits.")
+ end
+ end
+end
(DIR) diff --git a/web/app/models/dial_result.rb b/web/app/models/dial_result.rb
@@ -0,0 +1,4 @@
+class DialResult < ActiveRecord::Base
+ belongs_to :provider
+ belongs_to :dial_job
+end
(DIR) diff --git a/web/app/models/provider.rb b/web/app/models/provider.rb
@@ -0,0 +1,7 @@
+class Provider < ActiveRecord::Base
+ has_many :dial_results
+
+ validates_presence_of :name, :host, :port, :user, :pass, :lines
+ validates_numericality_of :port, :less_than => 65536, :greater_than => 0
+ validates_numericality_of :lines, :less_than => 255, :greater_than => 0
+end
(DIR) diff --git a/web/app/views/analyze/index.html.erb b/web/app/views/analyze/index.html.erb
@@ -0,0 +1,38 @@
+<% if @jobs.length > 0 %>
+<h1 class='title'>Analyzed Jobs</h1>
+
+<%= will_paginate @jobs %>
+<table class='table_scaffold' width='100%'>
+ <tr>
+ <th>ID</th>
+ <th>Range</th>
+ <th>CallerID</th>
+ <th>Connected</th>
+ <th>Date</th>
+ </tr>
+
+<% @jobs.sort{|a,b| b.id <=> a.id}.each do |dial_job| %>
+ <tr>
+ <td><%=h dial_job.id %></td>
+ <td><%=h dial_job.range %></td>
+ <td><%=h dial_job.cid_mask %></td>
+ <td><%=h (
+ DialResult.count(:conditions => ['dial_job_id = ? and processed = ?', dial_job.id, true]).to_s +
+ "/" +
+ DialResult.count(:conditions => ['dial_job_id = ?', dial_job.id]).to_s
+ )%></td>
+ <td><%=h dial_job.started_at.localtime.strftime("%Y-%m-%d %H:%M:%S") %></td>
+ <!-- <td><%= link_to 'Overview', show_analyze_path(dial_job) %></td> -->
+ <td><%= link_to 'Browse', view_analyze_path(dial_job) %></td>
+ <td><%= link_to 'ReAnalyze', reanalyze_dial_result_path(dial_job), :confirm => 'Process this job again?' %></td>
+ </tr>
+<% end %>
+</table>
+
+<%= will_paginate @jobs %>
+
+<% else %>
+
+<h1 class='title'>No Analyzed Jobs</h1>
+
+<% end %>
(DIR) diff --git a/web/app/views/analyze/show.html.erb b/web/app/views/analyze/show.html.erb
@@ -0,0 +1,9 @@
+<h1 class='title'>Overview of Job ID <%= @job_id %></h1>
+
+<table width='100%' align='center' border=0 cellspacing=0 cellpadding=6>
+<tr>
+ <td align='center'><%= render_ezgraphix @g1 %></td>
+</tr>
+</table>
+
+
(DIR) diff --git a/web/app/views/analyze/view.html.erb b/web/app/views/analyze/view.html.erb
@@ -0,0 +1,62 @@
+<h1 class='title'>Analysis of Job ID <%= @job_id %></h1>
+
+<%= select_tag_for_filter(@filters, params) %>
+
+<table width='100%' align='center' border=0 cellspacing=0 cellpadding=6>
+<tr>
+ <td align='center'><%= raw(render_ezgraphix @g1) %></td>
+</tr>
+</table>
+
+<%= will_paginate @results %>
+
+<table class='table_scaffold' width='100%'>
+ <tr>
+ <th>Number</th>
+ <th>Signal</th>
+ </tr>
+
+<% @results.each do |dial_result| %>
+ <tr>
+ <td align='center'>
+
+ <object
+ type="application/x-shockwave-flash"
+ data="/images/musicplayer.swf?song_url=<%=resource_analyze_path(@job_id, dial_result.id, "mp3")%>"
+ width="20"
+ height="17"
+ style="margin-bottom: -5px;"
+ >
+ <param name="movie" value="/musicplayer.swf?song_url=<%=resource_analyze_path(@job_id, dial_result.id, "mp3")%>"></param>
+ <param name="wmode" value="transparent"></param>
+ </object>
+ <b><%= dial_result.number %></b>
+ <hr width='100%' size='1'/>
+ CallerID: <%= dial_result.cid%><br/>
+ Provider: <%=h dial_result.provider.name %><br/>
+ Audio: <%=h dial_result.seconds %> Seconds<br/>
+ Ringer: <%=h dial_result.ringtime %> Seconds<br/>
+ <% if(dial_result.dtmf and dial_result.dtmf.length > 0) %>
+ DTMF: <%=h dial_result.dtmf %><br/>
+ <% end %>
+ <% if(dial_result.mf and dial_result.mf.length > 0) %>
+ MF: <%=h dial_result.mf %><br/>
+ <% end %>
+ </td>
+ <td align='center'>
+ <b><%=h dial_result.line_type.upcase %></b><br/>
+ <a href="<%=resource_analyze_path(@job_id, dial_result.id, "big_sig_dots")%>" rel="lightbox"><img src="<%=resource_analyze_path(@job_id, dial_result.id, "small_sig")%>" /></a>
+ <a href="<%=resource_analyze_path(@job_id, dial_result.id, "big_freq")%>" rel="lightbox"><img src="<%=resource_analyze_path(@job_id, dial_result.id, "small_freq")%>" /></a><br/>
+ <% (dial_result.signatures||"").split("\n").each do |s|
+ sid,mat,name = s.split(':', 3)
+ str = [mat.to_i * 6.4, 255].min
+ col = ("%.2x" % (255 - str)) * 3
+ %>
+ <div style="color: #<%= col%>;"><%=h name%> (<%=h sid %>@<%=h mat %>)</div>
+ <% end %>
+ </td>
+ </tr>
+<% end %>
+</table>
+
+<%= will_paginate @results %>
(DIR) diff --git a/web/app/views/dial_jobs/edit.html.erb b/web/app/views/dial_jobs/edit.html.erb
@@ -0,0 +1,24 @@
+<h1 class='title'>Modify Job</h1>
+
+<% form_for(@dial_job) do |f| %>
+ <%= f.error_messages %>
+
+ <p>
+ <%= f.label :range %><br />
+ <%= f.text_area :range, :size => "35x5" %>
+ </p>
+ <p>
+ <%= f.label :seconds %><br />
+ <%= f.text_field :seconds %>
+ </p>
+ <p>
+ <%= f.label :lines %><br />
+ <%= f.text_field :lines %>
+ </p>
+ <p>
+ <%= f.submit "Update" %>
+ </p>
+<% end %>
+
+<%= link_to 'Show', @dial_job %> |
+<%= link_to 'Back', dial_jobs_path %>
(DIR) diff --git a/web/app/views/dial_jobs/index.html.erb b/web/app/views/dial_jobs/index.html.erb
@@ -0,0 +1,107 @@
+<% if(@submitted_jobs.length > 0) %>
+
+<h1 class='title'>Submitted Jobs</h1>
+
+<table class='table_scaffold' width='100%'>
+ <tr>
+ <th>ID</th>
+ <th>Range</th>
+ <th>CallerID</th>
+ <th>Seconds</th>
+ <th>Lines</th>
+ <th>Submitted Time</th>
+ </tr>
+
+<% @submitted_jobs.each do |dial_job| %>
+ <tr>
+ <td><%=h dial_job.id %></td>
+ <td><%=h dial_job.range %></td>
+ <td><%=h dial_job.cid_mask %></td>
+ <td><%=h dial_job.seconds %></td>
+ <td><%=h dial_job.lines %></td>
+ <td><%=h dial_job.created_at.localtime.strftime("%Y-%m-%d %H:%M:%S %Z") %></td>
+ <td><%= link_to 'Execute', run_dial_job_path(dial_job), :confirm => 'Launch this job?'%></td>
+ <td><%= link_to 'Modify', edit_dial_job_path(dial_job)%></td>
+ <td><%= link_to 'Delete', dial_job, :confirm => 'Are you sure?', :method => :delete %></td>
+ </tr>
+<% end %>
+</table>
+<br />
+<% end %>
+
+<% if(@active_jobs.length > 0) %>
+
+<h1 class='title'>Active Jobs</h1>
+
+<table class='table_scaffold' width='100%'>
+ <tr>
+ <th>ID</th>
+ <th>Range</th>
+ <th>CallerID</th>
+ <th>Seconds</th>
+ <th>Lines</th>
+ <th>Status</th>
+ <th>Progress</th>
+ <th>Start Time</th>
+ </tr>
+
+<% @active_jobs.each do |dial_job| %>
+ <tr class='active_job_row'>
+ <td><%=h dial_job.id %></td>
+ <td><%=h dial_job.range %></td>
+ <td><%=h dial_job.cid_mask %></td>
+ <td><%=h dial_job.seconds %></td>
+ <td><%=h dial_job.lines %></td>
+ <td><%=h dial_job.status %></td>
+ <td><%=h dial_job.progress %>%</td>
+ <td><%=h dial_job.started_at.localtime.strftime("%Y-%m-%d %H:%M:%S %Z") %></td>
+ <td colspan='3'><%= link_to 'Stop', dial_job, :confirm => 'Are you sure?', :method => :delete %></td>
+ </tr>
+<% end %>
+</table>
+<br />
+
+<script language="javascript">
+ setTimeout("location.reload(true);", 20000);
+</script>
+
+<% end %>
+
+<% if (@active_jobs.length + @submitted_jobs.length == 0) %>
+<h1 class='title'>No Active or Submitted Jobs</h1>
+<br/>
+
+<% end %>
+<h1 class='title'>Submit a New Job</h1>
+
+<%= form_for(@new_job, :html => { :multipart => true }) do |f| %>
+ <%= f.error_messages %>
+ <p>
+ <%= f.label :range, 'Specify target telephone range(s) (1-123-456-7890 or 1-123-456-XXXX or 1-123-300-1000:1-123-400-2000)' %><br />
+ <%= f.text_area :range, :size => "35x5" %>
+ </p>
+
+ <p>
+ <%= f.label :range_file, 'Or upload a file containing the target ranges' %><br />
+ <%= f.file_field :range_file %>
+ </p>
+
+ <p>
+ <%= f.label :seconds, 'Seconds of audio to capture' %><br />
+ <%= f.text_field :seconds, :value => 53 %>
+ </p>
+
+ <p>
+ <%= f.label :lines, 'Maximum number of outgoing lines' %><br />
+ <%= f.text_field :lines, :value => 10 %>
+ </p>
+
+ <p>
+ <%= f.label :lines, 'The source Caller ID range (1-555-555-5555 or 1-555-555-55XX)' %><br />
+ <%= f.text_field :cid_mask, :value => '1-123-456-XXXX' %>
+ </p>
+
+ <p>
+ <%= f.submit "Create" %>
+ </p>
+<% end %>
(DIR) diff --git a/web/app/views/dial_jobs/new.html.erb b/web/app/views/dial_jobs/new.html.erb
@@ -0,0 +1,35 @@
+<h1 class='title'>Submit a New Job</h1>
+
+<% form_for(@dial_job, :html => { :multipart => true }) do |f| %>
+ <%= f.error_messages %>
+ <p>
+ <%= f.label :range, 'Specify target telephone range(s) (1-123-456-7890 or 1-123-456-XXXX or 1-123-300-1000:1-123-400-2000)' %><br />
+ <%= f.text_area :range, :size => "35x5" %>
+ </p>
+
+ <p>
+ <%= f.label :range_file, 'Or upload a file containing the target ranges' %><br />
+ <%= f.file_field :range_file %>
+ </p>
+
+ <p>
+ <%= f.label :seconds, 'Seconds of audio to capture' %><br />
+ <%= f.text_field :seconds, :value => 53 %>
+ </p>
+
+ <p>
+ <%= f.label :lines, 'Maximum number of outgoing lines' %><br />
+ <%= f.text_field :lines, :value => 10 %>
+ </p>
+
+ <p>
+ <%= f.label :lines, 'The source Caller ID range (1-555-555-55XX or SELF)' %><br />
+ <%= f.text_field :cid_mask, :value => '1-123-456-XXXX' %>
+ </p>
+
+ <p>
+ <%= f.submit "Create" %>
+ </p>
+<% end %>
+
+<%= link_to 'Back', dial_jobs_path %>
(DIR) diff --git a/web/app/views/dial_jobs/run.html.erb b/web/app/views/dial_jobs/run.html.erb
@@ -0,0 +1,5 @@
+<h1 class='title'>Run Job</h1>
+
+Running this job...<br/>
+
+<%= link_to 'Back', dial_jobs_path %>
(DIR) diff --git a/web/app/views/dial_jobs/show.html.erb b/web/app/views/dial_jobs/show.html.erb
@@ -0,0 +1,39 @@
+<h1 class='title'>Show Job</h1>
+<p>
+ <b>Range:</b>
+ <%=h @dial_job.range %>
+</p>
+
+<p>
+ <b>Seconds:</b>
+ <%=h @dial_job.seconds %>
+</p>
+
+<p>
+ <b>Lines:</b>
+ <%=h @dial_job.lines %>
+</p>
+
+<p>
+ <b>Status:</b>
+ <%=h @dial_job.status %>
+</p>
+
+<p>
+ <b>Progress:</b>
+ <%=h @dial_job.progress %>
+</p>
+
+<p>
+ <b>Started at:</b>
+ <%=h @dial_job.started_at %>
+</p>
+
+<p>
+ <b>Completed at:</b>
+ <%=h @dial_job.completed_at %>
+</p>
+
+
+<%= link_to 'Edit', edit_dial_job_path(@dial_job) %> |
+<%= link_to 'Back', dial_jobs_path %>
(DIR) diff --git a/web/app/views/dial_results/analyze.html.erb b/web/app/views/dial_results/analyze.html.erb
@@ -0,0 +1,25 @@
+<% if @dial_data_todo.length > 0 %>
+
+<h1 class='title'>
+ Analyzing Audio for <%= @dial_data_total-@dial_data_done %> of <%= @dial_data_total %> Calls...
+</h1>
+
+<table width='100%' align='center' border=0 cellspacing=0 cellpadding=6>
+<tr>
+<% if @dial_data_done > 0 %>
+ <td align='center'><%= render_ezgraphix @g1 %></td>
+<% end %>
+</tr>
+</table>
+
+<script language="javascript">
+ setTimeout("location.reload(true);", 10000);
+</script>
+
+<% else %>
+
+<h1 class='title'>No Completed Calls Found</h1>
+
+<% end %>
+
+<br />
(DIR) diff --git a/web/app/views/dial_results/edit.html.erb b/web/app/views/dial_results/edit.html.erb
@@ -0,0 +1,48 @@
+<h1>Editing dial_result</h1>
+
+<% form_for(@dial_result) do |f| %>
+ <%= f.error_messages %>
+
+ <p>
+ <%= f.label :number %><br />
+ <%= f.text_field :number %>
+ </p>
+ <p>
+ <%= f.label :cid %><br />
+ <%= f.text_field :cid %>
+ </p>
+ <p>
+ <%= f.label :dial_job_id %><br />
+ <%= f.text_field :dial_job_id %>
+ </p>
+ <p>
+ <%= f.label :provider %><br />
+ <%= f.text_field :provider %>
+ </p>
+ <p>
+ <%= f.label :completed %><br />
+ <%= f.check_box :completed %>
+ </p>
+ <p>
+ <%= f.label :busy %><br />
+ <%= f.check_box :busy %>
+ </p>
+ <p>
+ <%= f.label :seconds %><br />
+ <%= f.text_field :seconds %>
+ </p>
+ <p>
+ <%= f.label :ringtime %><br />
+ <%= f.text_field :ringtime %>
+ </p>
+ <p>
+ <%= f.label :rawfile %><br />
+ <%= f.text_field :rawfile %>
+ </p>
+ <p>
+ <%= f.submit "Update" %>
+ </p>
+<% end %>
+
+<%= link_to 'Show', @dial_result %> |
+<%= link_to 'Back', dial_results_path %>
(DIR) diff --git a/web/app/views/dial_results/index.html.erb b/web/app/views/dial_results/index.html.erb
@@ -0,0 +1,41 @@
+<% if @completed_jobs.length > 0 %>
+<h1 class='title'>Completed Jobs</h1>
+
+<table class='table_scaffold' width='100%'>
+ <tr>
+ <th>ID</th>
+ <th>Range</th>
+ <th>CID</th>
+ <th>Answered</th>
+ <th>Time</th>
+ </tr>
+
+<% @completed_jobs.sort{|a,b| b.id <=> a.id}.each do |dial_job| %>
+ <tr>
+ <td><%=h dial_job.id %></td>
+ <td><%=h dial_job.range %></td>
+ <td><%=h dial_job.cid_mask %></td>
+ <td><%=h (
+ DialResult.count(:conditions => ['dial_job_id = ? and completed = ?', dial_job.id, true]).to_s +
+ "/" +
+ DialResult.count(:conditions => ['dial_job_id = ?', dial_job.id]).to_s
+ )%></td>
+ <td><%=h dial_job.started_at.localtime.strftime("%Y-%m-%d %H:%M:%S") %></td>
+ <td><%= link_to 'View', view_dial_result_path(dial_job) %></td>
+ <% if(dial_job.processed) %>
+ <td><%= link_to 'View Analysis', analyze_dial_result_path(dial_job) %></td>
+ <% else %>
+ <td><%= link_to 'Analyze Calls', analyze_dial_result_path(dial_job) %></td>
+ <% end %>
+ <td><%= link_to 'Purge', purge_dial_result_path(dial_job), :confirm => "Delete all data for this job?" %></td>
+ </tr>
+<% end %>
+</table>
+
+<%= will_paginate @completed_jobs %>
+
+<% else %>
+
+<h1 class='title'>No Completed Jobs</h1>
+
+<% end %>
(DIR) diff --git a/web/app/views/dial_results/new.html.erb b/web/app/views/dial_results/new.html.erb
@@ -0,0 +1,43 @@
+<h1>New dial_result</h1>
+
+<% form_for(@dial_result) do |f| %>
+ <%= f.error_messages %>
+
+ <p>
+ <%= f.label :number %><br />
+ <%= f.text_field :number %>
+ </p>
+ <p>
+ <%= f.label :dial_job_id %><br />
+ <%= f.text_field :dial_job_id %>
+ </p>
+ <p>
+ <%= f.label :provider %><br />
+ <%= f.text_field :provider %>
+ </p>
+ <p>
+ <%= f.label :completed %><br />
+ <%= f.check_box :completed %>
+ </p>
+ <p>
+ <%= f.label :busy %><br />
+ <%= f.check_box :busy %>
+ </p>
+ <p>
+ <%= f.label :seconds %><br />
+ <%= f.text_field :seconds %>
+ </p>
+ <p>
+ <%= f.label :ringtime %><br />
+ <%= f.text_field :ringtime %>
+ </p>
+ <p>
+ <%= f.label :rawfile %><br />
+ <%= f.text_field :rawfile %>
+ </p>
+ <p>
+ <%= f.submit "Create" %>
+ </p>
+<% end %>
+
+<%= link_to 'Back', dial_results_path %>
(DIR) diff --git a/web/app/views/dial_results/show.html.erb b/web/app/views/dial_results/show.html.erb
@@ -0,0 +1,48 @@
+<p>
+ <b>Number:</b>
+ <%=h @dial_result.number %>
+</p>
+
+<p>
+ <b>CallerID:</b>
+ <%=h @dial_result.cid %>
+</p>
+
+<p>
+ <b>Dial job:</b>
+ <%=h @dial_result.dial_job_id %>
+</p>
+
+<p>
+ <b>Provider:</b>
+ <%=h @dial_result.provider %>
+</p>
+
+<p>
+ <b>Completed:</b>
+ <%=h @dial_result.completed %>
+</p>
+
+<p>
+ <b>Busy:</b>
+ <%=h @dial_result.busy %>
+</p>
+
+<p>
+ <b>Seconds:</b>
+ <%=h @dial_result.seconds %>
+</p>
+
+<p>
+ <b>Ringtime:</b>
+ <%=h @dial_result.ringtime %>
+</p>
+
+<p>
+ <b>Rawfile:</b>
+ <%=h @dial_result.rawfile %>
+</p>
+
+
+<%= link_to 'Edit', edit_dial_result_path(@dial_result) %> |
+<%= link_to 'Back', dial_results_path %>
(DIR) diff --git a/web/app/views/dial_results/view.html.erb b/web/app/views/dial_results/view.html.erb
@@ -0,0 +1,44 @@
+<% if @dial_results %>
+
+
+<h1 class='title'>Dial Results for Job <%=@dial_results[0].dial_job_id%></h1>
+
+<%= will_paginate @dial_results %>
+<table width='100%' align='center' border=0 cellspacing=0 cellpadding=6>
+<tr>
+ <td align='center'><%= raw(render_ezgraphix @g1) %></td>
+</tr>
+</table>
+
+<table class='table_scaffold' width='100%'>
+ <tr>
+ <th>Number</th>
+ <th>CallerID</th>
+ <th>Provider</th>
+ <th>Completed</th>
+ <th>Busy</th>
+ <th>Seconds</th>
+ <th>Ring Time</th>
+ </tr>
+
+
+<% for dial_result in @dial_results.sort{|a,b| a.number <=> b.number } %>
+ <tr>
+ <td><%=h dial_result.number %></td>
+ <td><%=h dial_result.cid %></td>
+ <td><%=h dial_result.provider.name %></td>
+ <td><%=h dial_result.completed %></td>
+ <td><%=h dial_result.busy %></td>
+ <td><%=h dial_result.seconds %></td>
+ <td><%=h dial_result.ringtime.to_i %></td>
+ </tr>
+<% end %>
+</table>
+<%= will_paginate @dial_results %>
+
+<% else %>
+
+<h1 class='title'>No Dial Results</h1>
+
+<% end %>
+<br />
(DIR) diff --git a/web/app/views/home/about.html.erb b/web/app/views/home/about.html.erb
@@ -0,0 +1,142 @@
+<table width='100%' align='center' border='0' cellpadding='9' cellspacing='0'>
+<tr><td valign='top'>
+
+<h1 class='title'>About WarVOX</h1>
+
+<b>WarVOX</b> is a product of <a href="http://www.rapid7.com/">Rapid7 LLC</a> and is provided as
+free software under the BSD license. WarVOX is intended for legal security assessment, asset inventory,
+and research purposes only. The latest version of WarVOX can be found at
+<a href="http://warvox.org/">http://warvox.org/</a>. The WarVOX development
+team can be reached by email at warvox[at]metasploit.com.
+
+</td><td valign='top' align='center'>
+
+
+<h1 class='title'>Statistics</h1>
+
+<table id="warvox_stats" cellspacing="0" width=200>
+
+<tr>
+ <td valign="top" align="right" class="header_item">
+ WarVOX Version:
+ </td>
+ <td><%= WarVOX::VERSION %></td>
+</tr>
+
+<tr>
+ <td valign="top" align="right" class="header_item">
+ Providers:
+ </td>
+ <td><%= Provider.count %></td>
+</tr>
+
+
+<tr>
+ <td valign="top" align="right" class="header_item">
+ Active Jobs:
+ </td>
+ <td><%= DialJob.count(:conditions => ['status = ?', 'active']) %></td>
+</tr>
+
+<tr>
+ <td valign="top" align="right" class="header_item">
+ Total Jobs:
+ </td>
+ <td><%= DialJob.count %></td>
+</tr>
+
+<tr>
+ <td valign="top" align="right" class="header_item">
+ Results:
+ </td>
+ <td><%= DialResult.count %></td>
+</tr>
+</table>
+
+
+</td></tr>
+<tr><td valign='top' colspan='2'>
+
+<h1 class='title'>Configuration</h1>
+
+
+<table id="warvox_conf" cellspacing="0" width='100%'>
+
+<tr>
+ <td valign="top" align="right" class="header_item" width='200'>
+ Base Directory:
+ </td>
+ <td><%= WarVOX::Base %></td>
+</tr>
+
+
+<tr>
+ <td valign="top" align="right" class="header_item" width='200'>
+ Configuration File:
+ </td>
+ <td><%= WarVOX::Conf %></td>
+</tr>
+
+
+<tr>
+ <td valign="top" align="right" class="header_item" width='200'>
+ Data Storage:
+ </td>
+ <td><%= WarVOX::Config.data_path %></td>
+</tr>
+
+<tr>
+ <td valign="top" align="right" class="header_item" width='200'>
+ Admin User:
+ </td>
+ <td><%= WarVOX::Config.authentication_creds[0] %></td>
+</tr>
+
+<tr>
+ <td valign="top" align="right" class="header_item" width='200'>
+ GNUPlot
+ </td>
+ <td><%= WarVOX::Config.tool_path('gnuplot') || "MISSING" %></td>
+</tr>
+
+<tr>
+ <td valign="top" align="right" class="header_item" width='200'>
+ SOX
+ </td>
+ <td><%= WarVOX::Config.tool_path('sox') || "MISSING" %></td>
+</tr>
+
+<tr>
+ <td valign="top" align="right" class="header_item" width='200'>
+ LAME
+ </td>
+ <td><%= WarVOX::Config.tool_path('lame') || "MISSING" %></td>
+</tr>
+
+<tr>
+ <td valign="top" align="right" class="header_item" width='200'>
+ IAXRECORD
+ </td>
+ <td><%= WarVOX::Config.tool_path('iaxrecord') || "MISSING" %></td>
+</tr>
+
+<tr>
+ <td valign="top" align="right" class="header_item" width='200'>
+ KissFFT
+ </td>
+ <td><%= @has_kissfft %></td>
+</tr>
+</table>
+
+<br/><br/>
+
+<h1 class='title'>Dial Exclusions (Blacklist)</h1>
+<table id="warvox_blacklist" cellspacing="0" width='100%'>
+<tr>
+ <td valign="top" align="left" class="header_item" width='200'>
+ <pre><%=h File.read(WarVOX::Config.blacklist_path) %></pre>
+ </td>
+</tr>
+</table>
+
+</td></tr></table>
(DIR) diff --git a/web/app/views/home/index.html.erb b/web/app/views/home/index.html.erb
@@ -0,0 +1,45 @@
+<h1 class='title'>Introduction</h1>
+
+<p>
+WarVOX is a suite of tools for exploring, classifying, and auditing telephone systems. Unlike normal wardialing tools,
+WarVOX works with the actual audio from each call and does not use a modem directly. This model allows WarVOX to find
+and classify a wide range of interesting lines, including modems, faxes, voice mail boxes, PBXs, loops, dial tones, IVRs,
+and forwarders. WarVOX provides the unique ability to classify all telephone lines in a given range, not just those
+connected to modems, allowing for a comprehensive audit of a telephone system.
+</p>
+
+<h1 class='title'>Getting Started</h1>
+<p>
+In order to make phone calls, WarVOX needs to be configured with one or more service providers. For a list of compatible VoIP ISPs, please see the <a href="http://warvox.org/#Providers">Service Providers</a> section of the WarVOX web site. To add a new provider to WarVOX, access the web interface, click the <a href="/providers">Providers</a> link, and fill in the <b>New Provider</b> form. This form allows you to specify a nickname for the provider and indicate how many concurrent outbound calls can be made using this account. WarVOX can make use of multiple service providers and multiple outbuond calls per provider when processing jobs.
+</p>
+
+<p>Once one or more service providers have been configured, click the <a href="/dial_jobs/">Jobs</a> link. This will present a form that asks for the phone number range to dial, the number of seconds of audio to capture, and the maximum number of outgoing lines to use for this job.
+</p>
+
+<p>The phone number range is specified by entering the phone number (including country code), with numbers replaced by X's where an entire range should be dialed. For example, the value 1-512-555-XXXX will make 10,000 calls, one to each number within the 512-555 exchange. In contrast, the value 1-512-555-555X will only make 10 calls, covering 5550 to 5559. Only 5 digits of the phone number range can be masked.
+</p>
+
+<p>
+The seconds field indicates the number of seconds to spend on each call, including the ring time. The larger the value, the more audio that can be captured, but you run the risk of leaving silent voice mail messages if this value is too large. Keep in mind that many per-minute service providers include ring time in the billing and round up to the nearest minute. This rounding is why the default value is set to 53 seconds. Most service providers do not charge for calls which were not answered.
+</p>
+
+<p>
+The outgoing line count is limited by the number of providers available and the number of lines available at each provider. If you are dialing a range with a limited number of inbound lines, the outgoing line count should be set to a small value, otherwise leave this value at the maximum number of available lines to complete jobs as quickly as possible. Each concurrent outbound call requires approximately 80kbits/s of downstream bandwidth.
+</p>
+
+<p>The Caller ID is specified by entering the phone number (including country code), with numbers replaced by X's where parts of the number should be chosen randomly. This field also accepts the special value of "SELF", which will cause all calls to be made with the Caller ID set to the destination number (useful for testing poorly implemented voice mail security).
+</p>
+
+<p>
+Once the job parameters have been specified, click the <b>Create</b> button to start dialing. If you return the main <a href="/dial_jobs/">Jobs</a> screen you can track the progress of the job via the percentage value in the active jobs table. The amount of time a given job takes depends on the number of phone numbers within the range, the number of seconds of audio to record, and the number of concurrent outbound lines. A 1,000 number range with 53 seconds of audio and 10 outbound lines would take approximately 100 minutes to complete.
+</p>
+
+<p>
+After the job completes, access the <a href="/dial_results/">Results</a> link to view a listing of finished jobs. Click the <b>View</b> link to look at the raw call breakdown. To analyze the call data, click on the Results link, and then click the <b>Analyze Calls</b> link. Depending on the speed of your server, this can take up to 60 seconds per connected call to process all of the data. Once the data has been processed, the results will now be listed under the <a href="/analyze/">Analysis</a> link. Viewing the results will provide the signal graph, spectrum graph, and an audio player to hear the actual call.
+</p>
+
+
+<h1 class='title'>Troubleshooting</h1>
+<p>
+If for some reason WarVOX is not working correctly, or if you have any questions, please contact the WarVOX development team at warvox[at]metasploit.com. To determine whether software dependencies have been satisfied, you can use the <b>bin/verify_install.rb</b> Ruby script included with the WarVOX distribution. The <a href="/home/about/">About</a> page also includes detailed information that may help resolve installation issues. If your calls fail to produce any results, double check the Provider configuration. To obtain the latest version of this software, please check the <a href="http://warvox.org/">WarVOX Web Site</a>.
+</p>
(DIR) diff --git a/web/app/views/layouts/application.html.erb b/web/app/views/layouts/application.html.erb
(DIR) diff --git a/web/app/views/layouts/warvox.html.erb b/web/app/views/layouts/warvox.html.erb
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title><%= @title || "WarVOX" %></title>
+ <%= stylesheet_link_tag :all %>
+ <%= javascript_include_tag :defaults %>
+ <%= csrf_meta_tag %>
+ <%= stylesheet_link_tag 'global', 'lightbox' %>
+ <!--[if IE 7]><%= stylesheet_link_tag 'ie7' %><![endif]-->
+ <%= javascript_include_tag 'custom', 'prototype', 'effects', 'FusionCharts', 'lightbox' %>
+</head>
+<body>
+
+<div id="header">
+ <%= render :partial => 'shared/header' %>
+</div>
+
+<div id="content">
+ <div class="box_full">
+ <img src="/images/round_top.png" id="round_top" alt=""/>
+ <div id="main">
+<%= yield %>
+ <br/><br/>
+
+ <div id="footer">
+ <%= render :partial => 'shared/footer' %>
+ </div>
+
+ </div>
+ <img src="/images/round_bot.png" id="round_bot" alt=""/>
+ </div>
+</div>
+
+<br/><br/>
+
+</body>
+</html>
(DIR) diff --git a/web/app/views/providers/edit.html.erb b/web/app/views/providers/edit.html.erb
@@ -0,0 +1,38 @@
+<h1 class='title'>Edit Provider</h1>
+
+<% form_for(@provider) do |f| %>
+ <%= f.error_messages %>
+ <p>
+ <%= f.label :enabled %><br />
+ <%= f.check_box :enabled %>
+ </p>
+ <p>
+ <%= f.label :name %><br />
+ <%= f.text_field :name %>
+ </p>
+ <p>
+ <%= f.label :host %><br />
+ <%= f.text_field :host %>
+ </p>
+ <p>
+ <%= f.label :port %><br />
+ <%= f.text_field :port %>
+ </p>
+ <p>
+ <%= f.label :user %><br />
+ <%= f.text_field :user %>
+ </p>
+ <p>
+ <%= f.label :pass %><br />
+ <%= f.text_field :pass %>
+ </p>
+ <p>
+ <%= f.label :lines %><br />
+ <%= f.text_field :lines %>
+ </p>
+ <p>
+ <%= f.submit "Update" %>
+ </p>
+<% end %>
+
+<%= link_to 'Back', providers_path %>
(DIR) diff --git a/web/app/views/providers/index.html.erb b/web/app/views/providers/index.html.erb
@@ -0,0 +1,72 @@
+<% if @providers.length > 0 %>
+<h1 class='title'>Providers</h1>
+<table class='table_scaffold' width='100%'>
+ <tr>
+ <th>Enabled</th>
+ <th>Name</th>
+ <th>Host</th>
+ <th>Port</th>
+ <th>User</th>
+ <th>Pass</th>
+ <th>Lines</th>
+ </tr>
+
+<% for provider in @providers %>
+ <tr>
+ <td><%=h provider.enabled %></td>
+ <td><%=h provider.name %></td>
+ <td><%=h provider.host %></td>
+ <td><%=h provider.port %></td>
+ <td><%=h provider.user %></td>
+ <td>********</td>
+ <td><%=h provider.lines %></td>
+ <td><%= link_to 'Modify', edit_provider_path(provider) %></td>
+ <td><%= link_to 'Delete', provider, :confirm => 'Are you sure?', :method => :delete %></td>
+ </tr>
+<% end %>
+</table>
+
+<br />
+
+<%= link_to 'New Provider', new_provider_path %>
+
+<% else %>
+
+<h1 class='title'>No Configured Providers</h1>
+<br/>
+
+<h1 class='title'>New Provider</h1>
+
+<% form_for(@new_provider) do |f| %>
+ <%= f.error_messages %>
+ <p>
+ <%= f.label :name, 'The nickname for this provider' %><br />
+ <%= f.text_field :name %>
+ </p>
+ <p>
+ <%= f.label :host, 'The IAX2 server name' %><br />
+ <%= f.text_field :host %>
+ </p>
+ <p>
+ <%= f.label :port, 'The IAX2 port (normally 4569)' %><br />
+ <%= f.text_field :port, :value => 4569 %>
+ </p>
+ <p>
+ <%= f.label :user, 'The username to access the provider' %><br />
+ <%= f.text_field :user %>
+ </p>
+ <p>
+ <%= f.label :pass, 'The password to access the provider' %><br />
+ <%= f.text_field :pass %>
+ </p>
+ <p>
+ <%= f.label :lines, 'The number of available outbound lines' %><br />
+ <%= f.text_field :lines, :value => 1 %>
+ </p>
+ <p>
+ <%= f.submit "Create" %>
+ </p>
+<% end %>
+
+<% end %>
+
(DIR) diff --git a/web/app/views/providers/new.html.erb b/web/app/views/providers/new.html.erb
@@ -0,0 +1,34 @@
+<h1 class='title'>New Provider</h1>
+
+<% form_for(@provider) do |f| %>
+ <%= f.error_messages %>
+ <p>
+ <%= f.label :name, 'The nickname for this provider' %><br />
+ <%= f.text_field :name %>
+ </p>
+ <p>
+ <%= f.label :host, 'The IAX2 server name' %><br />
+ <%= f.text_field :host %>
+ </p>
+ <p>
+ <%= f.label :port, 'The IAX2 port (normally 4569)' %><br />
+ <%= f.text_field :port, :value => 4569 %>
+ </p>
+ <p>
+ <%= f.label :user, 'The username to access the provider' %><br />
+ <%= f.text_field :user %>
+ </p>
+ <p>
+ <%= f.label :pass, 'The password to access the provider' %><br />
+ <%= f.text_field :pass %>
+ </p>
+ <p>
+ <%= f.label :lines, 'The number of available outbound lines' %><br />
+ <%= f.text_field :lines, :value => 1 %>
+ </p>
+ <p>
+ <%= f.submit "Create" %>
+ </p>
+<% end %>
+
+<%= link_to 'Back', providers_path %>
(DIR) diff --git a/web/app/views/providers/show.html.erb b/web/app/views/providers/show.html.erb
@@ -0,0 +1,38 @@
+<h1 class='title'>View Provider</h1>
+<p>
+ <b>Enabled:</b>
+ <%=h @provider.enabled %>
+</p>
+<p>
+ <b>Name:</b>
+ <%=h @provider.name %>
+</p>
+
+<p>
+ <b>Host:</b>
+ <%=h @provider.host %>
+</p>
+
+<p>
+ <b>Port:</b>
+ <%=h @provider.port %>
+</p>
+
+<p>
+ <b>User:</b>
+ <%=h @provider.user %>
+</p>
+
+<p>
+ <b>Pass:</b>
+ ********
+</p>
+
+<p>
+ <b>Lines:</b>
+ <%=h @provider.lines %>
+</p>
+
+
+<%= link_to 'Edit', edit_provider_path(@provider) %> |
+<%= link_to 'Back', providers_path %>
(DIR) diff --git a/web/app/views/shared/_footer.html.erb b/web/app/views/shared/_footer.html.erb
@@ -0,0 +1,5 @@
+<div id="footer">
+ <div id='copyright'>
+ Copyright ©2009-2010 Rapid7 LLC<br/>
+ </div>
+</div>
(DIR) diff --git a/web/app/views/shared/_header.html.erb b/web/app/views/shared/_header.html.erb
@@ -0,0 +1,17 @@
+<div id="header">
+
+ <span width='50%'>
+ <a href="/"><img id="logo" src="/images/logo.png" alt="WarVOX v<%= WarVOX::VERSION %>" border="0"/></a>
+ </span>
+
+ <span width='50%'>
+ <div id="navwrap">
+ <div id="nav">
+ <%= get_sections() %>
+ </div>
+ </div>
+ </span>
+
+ <br/>
+ <hr width='100%' size=5 color='black'/>
+</div>
(DIR) diff --git a/web/config.ru b/web/config.ru
@@ -0,0 +1,4 @@
+# This file is used by Rack-based servers to start the application.
+
+require ::File.expand_path('../config/environment', __FILE__)
+run Web::Application
(DIR) diff --git a/web/config/application.rb b/web/config/application.rb
@@ -0,0 +1,49 @@
+require File.expand_path('../boot', __FILE__)
+
+# Bootstrap the WarVOX code base
+$:.unshift(File.join(File.dirname(__FILE__), '..', '..', 'lib'))
+require 'warvox'
+
+require 'rails/all'
+
+# If you have a Gemfile, require the gems listed there, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(:default, Rails.env) if defined?(Bundler)
+
+module Web
+ class Application < Rails::Application
+ # Settings in config/environments/* take precedence over those specified here.
+ # Application configuration should go into files in config/initializers
+ # -- all .rb files in that directory are automatically loaded.
+
+ # Custom directories with classes and modules you want to be autoloadable.
+ # config.autoload_paths += %W(#{config.root}/extras)
+
+ # Only load the plugins named here, in the order given (default is alphabetical).
+ # :all can be used as a placeholder for all plugins not explicitly named.
+ # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
+
+ # Activate observers that should always be running.
+ # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
+
+ # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
+ # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
+ # config.time_zone = 'Central Time (US & Canada)'
+
+ # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
+ # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
+ # config.i18n.default_locale = :de
+
+ # JavaScript files you want as :defaults (application.js is always included).
+ # config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
+
+ # Configure the default encoding used in templates for Ruby 1.9.
+ config.encoding = "utf-8"
+
+ # Configure sensitive parameters which will be filtered from the log file.
+ config.filter_parameters += [:password, :pass]
+
+ config.session_store :cookie_store, :key => "_warvox"
+ config.secret_token = WarVOX::Config.load_session_key
+ end
+end
(DIR) diff --git a/web/config/boot.rb b/web/config/boot.rb
@@ -0,0 +1,13 @@
+require 'rubygems'
+
+# Set up gems listed in the Gemfile.
+gemfile = File.expand_path('../../Gemfile', __FILE__)
+begin
+ ENV['BUNDLE_GEMFILE'] = gemfile
+ require 'bundler'
+ Bundler.setup
+rescue Bundler::GemNotFound => e
+ STDERR.puts e.message
+ STDERR.puts "Try running `bundle install`."
+ exit!
+end if File.exist?(gemfile)
(DIR) diff --git a/web/config/database.yml b/web/config/database.yml
@@ -0,0 +1,22 @@
+# SQLite version 3.x
+# gem install sqlite3-ruby (not necessary on OS X Leopard)
+development:
+ adapter: sqlite3
+ database: db/development.sqlite3
+ pool: 5
+ timeout: 5000
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ adapter: sqlite3
+ database: db/test.sqlite3
+ pool: 5
+ timeout: 5000
+
+production:
+ adapter: sqlite3
+ database: db/production.sqlite3
+ pool: 5
+ timeout: 5000
(DIR) diff --git a/web/config/environment.rb b/web/config/environment.rb
@@ -0,0 +1,5 @@
+# Load the rails application
+require File.expand_path('../application', __FILE__)
+
+# Initialize the rails application
+Web::Application.initialize!
(DIR) diff --git a/web/config/environments/development.rb b/web/config/environments/development.rb
@@ -0,0 +1,26 @@
+Web::Application.configure do
+ # 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
+
+ # Show full error reports and disable caching
+ config.consider_all_requests_local = true
+ config.action_view.debug_rjs = true
+ config.action_controller.perform_caching = false
+
+ # Don't care if the mailer can't send
+ config.action_mailer.raise_delivery_errors = false
+
+ # Print deprecation notices to the Rails logger
+ config.active_support.deprecation = :log
+
+ # Only use best-standards-support built into browsers
+ config.action_dispatch.best_standards_support = :builtin
+end
+
(DIR) diff --git a/web/config/environments/production.rb b/web/config/environments/production.rb
@@ -0,0 +1,49 @@
+Web::Application.configure do
+ # Settings specified here will take precedence over those in config/environment.rb
+
+ # The production environment is meant for finished, "live" apps.
+ # Code is not reloaded between requests
+ config.cache_classes = true
+
+ # Full error reports are disabled and caching is turned on
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Specifies the header that your server uses for sending files
+ config.action_dispatch.x_sendfile_header = "X-Sendfile"
+
+ # For nginx:
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
+
+ # If you have no front-end server that supports something like X-Sendfile,
+ # just comment this out and Rails will serve the files
+
+ # See everything in the log (default is :info)
+ # config.log_level = :debug
+
+ # Use a different logger for distributed setups
+ # config.logger = SyslogLogger.new
+
+ # Use a different cache store in production
+ # config.cache_store = :mem_cache_store
+
+ # Disable Rails's static asset server
+ # In production, Apache or nginx will already do this
+ config.serve_static_assets = false
+
+ # Enable serving of images, stylesheets, and javascripts from an asset server
+ # config.action_controller.asset_host = "http://assets.example.com"
+
+ # Disable delivery errors, bad email addresses will be ignored
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable threaded mode
+ # config.threadsafe!
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation can not be found)
+ config.i18n.fallbacks = true
+
+ # Send deprecation notices to registered listeners
+ config.active_support.deprecation = :notify
+end
(DIR) diff --git a/web/config/environments/test.rb b/web/config/environments/test.rb
@@ -0,0 +1,35 @@
+Web::Application.configure do
+ # 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 "scratch space" 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.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+
+ # Raise exceptions instead of rendering exception templates
+ config.action_dispatch.show_exceptions = false
+
+ # Disable request forgery protection in test environment
+ config.action_controller.allow_forgery_protection = false
+
+ # Tell Action Mailer 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
+
+ # Use SQL instead of Active Record's schema dumper when creating the test database.
+ # This is necessary if your schema can't be completely dumped by the schema dumper,
+ # like if you have constraints or database-specific column types
+ # config.active_record.schema_format = :sql
+
+ # Print deprecation notices to the stderr
+ config.active_support.deprecation = :stderr
+end
(DIR) diff --git a/web/config/initializers/backtrace_silencers.rb b/web/config/initializers/backtrace_silencers.rb
@@ -0,0 +1,7 @@
+# Be sure to restart your server when you modify this file.
+
+# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
+# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+
+# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
+# Rails.backtrace_cleaner.remove_silencers!
(DIR) diff --git a/web/config/initializers/inflections.rb b/web/config/initializers/inflections.rb
@@ -0,0 +1,10 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format
+# (all these examples are active by default):
+# ActiveSupport::Inflector.inflections do |inflect|
+# inflect.plural /^(ox)$/i, '\1en'
+# inflect.singular /^(ox)en/i, '\1'
+# inflect.irregular 'person', 'people'
+# inflect.uncountable %w( fish sheep )
+# end
(DIR) diff --git a/web/config/initializers/mime_types.rb b/web/config/initializers/mime_types.rb
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file.
+
+# Add new mime types for use in respond_to blocks:
+# Mime::Type.register "text/richtext", :rtf
+# Mime::Type.register_alias "text/html", :iphone
(DIR) diff --git a/web/config/initializers/secret_token.rb b/web/config/initializers/secret_token.rb
@@ -0,0 +1,7 @@
+# Be sure to restart your server when you modify this file.
+
+# Your secret key for verifying the integrity of signed cookies.
+# If you change this key, all old signed cookies will become invalid!
+# Make sure the secret is at least 30 characters and all random,
+# no regular words or you'll be exposed to dictionary attacks.
+Web::Application.config.secret_token = '1bd648becb978b46fd65fe255a047ea09d9a068749f6b1bf8340e3a109c91cda4176732d9dbadae8bba444436828cde14073151a993497fc5f314c3079dbea26'
(DIR) diff --git a/web/config/initializers/session_store.rb b/web/config/initializers/session_store.rb
@@ -0,0 +1,8 @@
+# Be sure to restart your server when you modify this file.
+
+Web::Application.config.session_store :cookie_store, :key => '_web_session'
+
+# Use the database for sessions instead of the cookie-based default,
+# which shouldn't be used to store highly confidential information
+# (create the session table with "rake db:sessions:create")
+# Web::Application.config.session_store :active_record_store
(DIR) diff --git a/web/config/initializers/warvox.rb b/web/config/initializers/warvox.rb
(DIR) diff --git a/web/config/locales/en.yml b/web/config/locales/en.yml
@@ -0,0 +1,5 @@
+# Sample localization file for English. Add more files in this directory for other locales.
+# See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
+
+en:
+ hello: "Hello world"
(DIR) diff --git a/web/config/routes.rb b/web/config/routes.rb
@@ -0,0 +1,23 @@
+Web::Application.routes.draw do
+
+ resources :dial_jobs
+ resources :dial_results
+ resources :providers
+
+ match '/dial_jobs/:id/run' => 'dial_jobs#run', :as => :run_dial_job
+ match '/dial_results/:id/view' => 'dial_results#view', :as => :view_dial_result
+ match '/dial_results/:id/analyze' => 'dial_results#analyze', :as => :analyze_dial_result
+ match '/dial_results/:id/reanalyze' => 'dial_results#reanalyze', :as => :reanalyze_dial_result
+ match '/dial_results/:id/purge' => 'dial_results#purge', :as => :purge_dial_result
+
+ match '/analyze/:id/resource/:result_id/:type' => 'analyze#resource', :as => :resource_analyze
+ match '/analyze/:id/view' => 'analyze#view', :as => :view_analyze
+ match '/analyze/:id/show' => 'analyze#show', :as => :show_analyze
+ match '/analyze' => 'analyze#index'
+
+ match '/about' => 'home#about'
+ match '/home/about' => 'home#about'
+
+ root :to => "home#index"
+
+end
(DIR) diff --git a/web/db/migrate/20090228195925_create_providers.rb b/web/db/migrate/20090228195925_create_providers.rb
@@ -0,0 +1,18 @@
+class CreateProviders < ActiveRecord::Migration
+ def self.up
+ create_table :providers do |t|
+ t.string :name
+ t.string :host
+ t.integer :port
+ t.string :user
+ t.string :pass
+ t.integer :lines
+
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :providers
+ end
+end
(DIR) diff --git a/web/db/migrate/20090228200035_create_dial_jobs.rb b/web/db/migrate/20090228200035_create_dial_jobs.rb
@@ -0,0 +1,20 @@
+class CreateDialJobs < ActiveRecord::Migration
+ def self.up
+ create_table :dial_jobs do |t|
+ t.string :range
+ t.integer :seconds
+ t.integer :lines
+ t.string :status
+ t.integer :progress
+ t.datetime :started_at
+ t.datetime :completed_at
+ t.boolean :processed
+
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :dial_jobs
+ end
+end
(DIR) diff --git a/web/db/migrate/20090228200141_create_dial_results.rb b/web/db/migrate/20090228200141_create_dial_results.rb
@@ -0,0 +1,21 @@
+class CreateDialResults < ActiveRecord::Migration
+ def self.up
+ create_table :dial_results do |t|
+ t.integer :number
+ t.integer :dial_job_id
+ t.integer :provider_id
+ t.boolean :completed
+ t.boolean :busy
+ t.integer :seconds
+ t.integer :ringtime
+ t.string :rawfile
+ t.boolean :processed
+
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :dial_results
+ end
+end
(DIR) diff --git a/web/db/migrate/20090301084459_add_processed_at_to_dial_result.rb b/web/db/migrate/20090301084459_add_processed_at_to_dial_result.rb
@@ -0,0 +1,9 @@
+class AddProcessedAtToDialResult < ActiveRecord::Migration
+ def self.up
+ add_column :dial_results, :processed_at, :datetime
+ end
+
+ def self.down
+ remove_column :dial_results, :processed_at
+ end
+end
(DIR) diff --git a/web/db/migrate/20090303204859_add_cid_mask_to_dial_jobs.rb b/web/db/migrate/20090303204859_add_cid_mask_to_dial_jobs.rb
@@ -0,0 +1,9 @@
+class AddCidMaskToDialJobs < ActiveRecord::Migration
+ def self.up
+ add_column :dial_jobs, :cid_mask, :string
+ end
+
+ def self.down
+ remove_column :dial_jobs, :cid_mask
+ end
+end
(DIR) diff --git a/web/db/migrate/20090303204917_add_cid_to_dial_results.rb b/web/db/migrate/20090303204917_add_cid_to_dial_results.rb
@@ -0,0 +1,9 @@
+class AddCidToDialResults < ActiveRecord::Migration
+ def self.up
+ add_column :dial_results, :cid, :string
+ end
+
+ def self.down
+ remove_column :dial_results, :cid
+ end
+end
(DIR) diff --git a/web/db/migrate/20090303225838_add_enabled_to_providers.rb b/web/db/migrate/20090303225838_add_enabled_to_providers.rb
@@ -0,0 +1,9 @@
+class AddEnabledToProviders < ActiveRecord::Migration
+ def self.up
+ add_column :providers, :enabled, :boolean
+ end
+
+ def self.down
+ remove_column :providers, :enabled
+ end
+end
(DIR) diff --git a/web/db/migrate/20090304013815_add_peak_freq_to_dial_results.rb b/web/db/migrate/20090304013815_add_peak_freq_to_dial_results.rb
@@ -0,0 +1,9 @@
+class AddPeakFreqToDialResults < ActiveRecord::Migration
+ def self.up
+ add_column :dial_results, :peak_freq, :number
+ end
+
+ def self.down
+ remove_column :dial_results, :peak_freq
+ end
+end
(DIR) diff --git a/web/db/migrate/20090304013839_add_peak_freq_data_to_dial_results.rb b/web/db/migrate/20090304013839_add_peak_freq_data_to_dial_results.rb
@@ -0,0 +1,9 @@
+class AddPeakFreqDataToDialResults < ActiveRecord::Migration
+ def self.up
+ add_column :dial_results, :peak_freq_data, :string
+ end
+
+ def self.down
+ remove_column :dial_results, :peak_freq_data
+ end
+end
(DIR) diff --git a/web/db/migrate/20090304013909_add_sig_data_to_dial_results.rb b/web/db/migrate/20090304013909_add_sig_data_to_dial_results.rb
@@ -0,0 +1,9 @@
+class AddSigDataToDialResults < ActiveRecord::Migration
+ def self.up
+ add_column :dial_results, :sig_data, :string
+ end
+
+ def self.down
+ remove_column :dial_results, :sig_data
+ end
+end
(DIR) diff --git a/web/db/migrate/20090304014018_add_line_type_to_dial_results.rb b/web/db/migrate/20090304014018_add_line_type_to_dial_results.rb
@@ -0,0 +1,9 @@
+class AddLineTypeToDialResults < ActiveRecord::Migration
+ def self.up
+ add_column :dial_results, :line_type, :string
+ end
+
+ def self.down
+ remove_column :dial_results, :line_type
+ end
+end
(DIR) diff --git a/web/db/migrate/20090304014033_add_notes_to_dial_results.rb b/web/db/migrate/20090304014033_add_notes_to_dial_results.rb
@@ -0,0 +1,9 @@
+class AddNotesToDialResults < ActiveRecord::Migration
+ def self.up
+ add_column :dial_results, :notes, :string
+ end
+
+ def self.down
+ remove_column :dial_results, :notes
+ end
+end
(DIR) diff --git a/web/db/migrate/20090522202032_add_signatures_to_dial_results.rb b/web/db/migrate/20090522202032_add_signatures_to_dial_results.rb
@@ -0,0 +1,9 @@
+class AddSignaturesToDialResults < ActiveRecord::Migration
+ def self.up
+ add_column :dial_results, :signatures, :string
+ end
+
+ def self.down
+ remove_column :dial_results, :signatures
+ end
+end
(DIR) diff --git a/web/db/migrate/20090526031826_add_mf_and_dtmf_to_dial_results.rb b/web/db/migrate/20090526031826_add_mf_and_dtmf_to_dial_results.rb
@@ -0,0 +1,11 @@
+class AddMfAndDtmfToDialResults < ActiveRecord::Migration
+ def self.up
+ add_column :dial_results, :dtmf, :string
+ add_column :dial_results, :mf, :string
+ end
+
+ def self.down
+ remove_column :dial_results, :dtmf
+ remove_column :dial_results, :mf
+ end
+end
(DIR) diff --git a/web/db/schema.rb b/web/db/schema.rb
@@ -0,0 +1,65 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# Note that this schema.rb definition is the authoritative source for your
+# database schema. If you need to create the application database on another
+# system, you should be using db:schema:load, not running all the migrations
+# from scratch. The latter is a flawed and unsustainable approach (the more migrations
+# you'll amass, the slower it'll run and the greater likelihood for issues).
+#
+# It's strongly recommended to check this file into your version control system.
+
+ActiveRecord::Schema.define(:version => 20090526031826) do
+
+ create_table "dial_jobs", :force => true do |t|
+ t.string "range"
+ t.integer "seconds"
+ t.integer "lines"
+ t.string "status"
+ t.integer "progress"
+ t.datetime "started_at"
+ t.datetime "completed_at"
+ t.boolean "processed"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "cid_mask"
+ end
+
+ create_table "dial_results", :force => true do |t|
+ t.integer "number"
+ t.integer "dial_job_id"
+ t.integer "provider_id"
+ t.boolean "completed"
+ t.boolean "busy"
+ t.integer "seconds"
+ t.integer "ringtime"
+ t.string "rawfile"
+ t.boolean "processed"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.datetime "processed_at"
+ t.string "cid"
+ t.decimal "peak_freq"
+ t.string "peak_freq_data"
+ t.string "sig_data"
+ t.string "line_type"
+ t.string "notes"
+ t.string "signatures"
+ t.string "dtmf"
+ t.string "mf"
+ end
+
+ create_table "providers", :force => true do |t|
+ t.string "name"
+ t.string "host"
+ t.integer "port"
+ t.string "user"
+ t.string "pass"
+ t.integer "lines"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "enabled"
+ end
+
+end
(DIR) diff --git a/web/db/seeds.rb b/web/db/seeds.rb
@@ -0,0 +1,7 @@
+# This file should contain all the record creation needed to seed the database with its default values.
+# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
+#
+# Examples:
+#
+# cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }])
+# Mayor.create(:name => 'Daley', :city => cities.first)
(DIR) diff --git a/web/public/404.html b/web/public/404.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>The page you were looking for doesn't exist (404)</title>
+ <style type="text/css">
+ 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; }
+ </style>
+</head>
+
+<body>
+ <!-- This file lives in public/404.html -->
+ <div class="dialog">
+ <h1>The page you were looking for doesn't exist.</h1>
+ <p>You may have mistyped the address or the page may have moved.</p>
+ </div>
+</body>
+</html>
(DIR) diff --git a/web/public/422.html b/web/public/422.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>The change you wanted was rejected (422)</title>
+ <style type="text/css">
+ 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; }
+ </style>
+</head>
+
+<body>
+ <!-- This file lives in public/422.html -->
+ <div class="dialog">
+ <h1>The change you wanted was rejected.</h1>
+ <p>Maybe you tried to change something you didn't have access to.</p>
+ </div>
+</body>
+</html>
(DIR) diff --git a/web/public/500.html b/web/public/500.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>We're sorry, but something went wrong (500)</title>
+ <style type="text/css">
+ 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; }
+ </style>
+</head>
+
+<body>
+ <!-- This file lives in public/500.html -->
+ <div class="dialog">
+ <h1>We're sorry, but something went wrong.</h1>
+ <p>We've been notified about this issue and we'll take a look at it shortly.</p>
+ </div>
+</body>
+</html>
(DIR) diff --git a/web/public/FusionCharts/FCF_Area2D.swf b/web/public/FusionCharts/FCF_Area2D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_Bar2D.swf b/web/public/FusionCharts/FCF_Bar2D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_Candlestick.swf b/web/public/FusionCharts/FCF_Candlestick.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_Column2D.swf b/web/public/FusionCharts/FCF_Column2D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_Column3D.swf b/web/public/FusionCharts/FCF_Column3D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_Doughnut2D.swf b/web/public/FusionCharts/FCF_Doughnut2D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_Funnel.swf b/web/public/FusionCharts/FCF_Funnel.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_Gantt.swf b/web/public/FusionCharts/FCF_Gantt.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_Line.swf b/web/public/FusionCharts/FCF_Line.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_MSArea2D.swf b/web/public/FusionCharts/FCF_MSArea2D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_MSBar2D.swf b/web/public/FusionCharts/FCF_MSBar2D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_MSColumn2D.swf b/web/public/FusionCharts/FCF_MSColumn2D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_MSColumn2DLineDY.swf b/web/public/FusionCharts/FCF_MSColumn2DLineDY.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_MSColumn3D.swf b/web/public/FusionCharts/FCF_MSColumn3D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_MSColumn3DLineDY.swf b/web/public/FusionCharts/FCF_MSColumn3DLineDY.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_MSLine.swf b/web/public/FusionCharts/FCF_MSLine.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_Pie2D.swf b/web/public/FusionCharts/FCF_Pie2D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_Pie3D.swf b/web/public/FusionCharts/FCF_Pie3D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_StackedArea2D.swf b/web/public/FusionCharts/FCF_StackedArea2D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_StackedBar2D.swf b/web/public/FusionCharts/FCF_StackedBar2D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_StackedColumn2D.swf b/web/public/FusionCharts/FCF_StackedColumn2D.swf
Binary files differ.
(DIR) diff --git a/web/public/FusionCharts/FCF_StackedColumn3D.swf b/web/public/FusionCharts/FCF_StackedColumn3D.swf
Binary files differ.
(DIR) diff --git a/web/public/favicon.ico b/web/public/favicon.ico
(DIR) diff --git a/web/public/images/balloon.png b/web/public/images/balloon.png
Binary files differ.
(DIR) diff --git a/web/public/images/bluefade.jpg b/web/public/images/bluefade.jpg
Binary files differ.
(DIR) diff --git a/web/public/images/close.gif b/web/public/images/close.gif
Binary files differ.
(DIR) diff --git a/web/public/images/left-round.png b/web/public/images/left-round.png
Binary files differ.
(DIR) diff --git a/web/public/images/loading.gif b/web/public/images/loading.gif
Binary files differ.
(DIR) diff --git a/web/public/images/logo.png b/web/public/images/logo.png
Binary files differ.
(DIR) diff --git a/web/public/images/logo_raw.xcf b/web/public/images/logo_raw.xcf
Binary files differ.
(DIR) diff --git a/web/public/images/musicplayer.swf b/web/public/images/musicplayer.swf
Binary files differ.
(DIR) diff --git a/web/public/images/rails.png b/web/public/images/rails.png
Binary files differ.
(DIR) diff --git a/web/public/images/right-round.png b/web/public/images/right-round.png
Binary files differ.
(DIR) diff --git a/web/public/images/round_bot.png b/web/public/images/round_bot.png
Binary files differ.
(DIR) diff --git a/web/public/images/round_top.png b/web/public/images/round_top.png
Binary files differ.
(DIR) diff --git a/web/public/javascripts/FusionCharts.js b/web/public/javascripts/FusionCharts.js
@@ -0,0 +1,361 @@
+/**
+ * FusionCharts: Flash Player detection and Chart embedding.
+ * Version 1.2.3F ( 22 November 2008) - Specialized for FusionChartsFREE
+ * Checking Flash Version >=6 and added updateChartXML() for FREE Charts.
+ * Version: 1.2.3 (1st September, 2008) - Added Fix for % and & characters, scaled dimensions, fixes in to properly handling of double quotes and single quotes in setDataXML() function.
+ * Version: 1.2.2 (10th July, 2008) - Added Fix for % scaled dimensions, fixes in setDataXML() and setDataURL() functions
+ * Version: 1.2.1 (21st December, 2007) - Added setting up Transparent/opaque mode: setTransparent() function
+ * Version: 1.2 (1st November, 2007) - Added FORM fixes for IE
+ * Version: 1.1 (29th June, 2007) - Added Player detection, New conditional fixes for IE
+ *
+ * Morphed from SWFObject (http://blog.deconcept.com/swfobject/) under MIT License:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ */
+if(typeof infosoftglobal == "undefined") var infosoftglobal = new Object();
+if(typeof infosoftglobal.FusionChartsUtil == "undefined") infosoftglobal.FusionChartsUtil = new Object();
+infosoftglobal.FusionCharts = function(swf, id, w, h, debugMode, registerWithJS, c, scaleMode, lang, detectFlashVersion, autoInstallRedirect){
+ if (!document.getElementById) { return; }
+
+ //Flag to see whether data has been set initially
+ this.initialDataSet = false;
+
+ //Create container objects
+ this.params = new Object();
+ this.variables = new Object();
+ this.attributes = new Array();
+
+ //Set attributes for the SWF
+ if(swf) { this.setAttribute('swf', swf); }
+ if(id) { this.setAttribute('id', id); }
+
+ w=w.toString().replace(/\%$/,"%25");
+ if(w) { this.setAttribute('width', w); }
+ h=h.toString().replace(/\%$/,"%25");
+ if(h) { this.setAttribute('height', h); }
+
+
+ //Set background color
+ if(c) { this.addParam('bgcolor', c); }
+
+ //Set Quality
+ this.addParam('quality', 'high');
+
+ //Add scripting access parameter
+ this.addParam('allowScriptAccess', 'always');
+
+ //Pass width and height to be appended as chartWidth and chartHeight
+ this.addVariable('chartWidth', w);
+ this.addVariable('chartHeight', h);
+
+ //Whether in debug mode
+ debugMode = debugMode ? debugMode : 0;
+ this.addVariable('debugMode', debugMode);
+ //Pass DOM ID to Chart
+ this.addVariable('DOMId', id);
+ //Whether to registed with JavaScript
+ registerWithJS = registerWithJS ? registerWithJS : 0;
+ this.addVariable('registerWithJS', registerWithJS);
+
+ //Scale Mode of chart
+ scaleMode = scaleMode ? scaleMode : 'noScale';
+ this.addVariable('scaleMode', scaleMode);
+
+ //Application Message Language
+ lang = lang ? lang : 'EN';
+ this.addVariable('lang', lang);
+
+ //Whether to auto detect and re-direct to Flash Player installation
+ this.detectFlashVersion = detectFlashVersion?detectFlashVersion:1;
+ this.autoInstallRedirect = autoInstallRedirect?autoInstallRedirect:1;
+
+ //Ger Flash Player version
+ this.installedVer = infosoftglobal.FusionChartsUtil.getPlayerVersion();
+
+ if (!window.opera && document.all && this.installedVer.major > 7) {
+ // Only add the onunload cleanup if the Flash Player version supports External Interface and we are in IE
+ infosoftglobal.FusionCharts.doPrepUnload = true;
+ }
+}
+
+infosoftglobal.FusionCharts.prototype = {
+ setAttribute: function(name, value){
+ this.attributes[name] = value;
+ },
+ getAttribute: function(name){
+ return this.attributes[name];
+ },
+ addParam: function(name, value){
+ this.params[name] = value;
+ },
+ getParams: function(){
+ return this.params;
+ },
+ addVariable: function(name, value){
+ this.variables[name] = value;
+ },
+ getVariable: function(name){
+ return this.variables[name];
+ },
+ getVariables: function(){
+ return this.variables;
+ },
+ getVariablePairs: function(){
+ var variablePairs = new Array();
+ var key;
+ var variables = this.getVariables();
+ for(key in variables){
+ variablePairs.push(key +"="+ variables[key]);
+ }
+ return variablePairs;
+ },
+ getSWFHTML: function() {
+ var swfNode = "";
+ if (navigator.plugins && navigator.mimeTypes && navigator.mimeTypes.length) {
+ // netscape plugin architecture
+ swfNode = '<embed type="application/x-shockwave-flash" src="'+ this.getAttribute('swf') +'" width="'+ this.getAttribute('width') +'" height="'+ this.getAttribute('height') +'" ';
+ swfNode += ' id="'+ this.getAttribute('id') +'" name="'+ this.getAttribute('id') +'" ';
+ var params = this.getParams();
+ for(var key in params){ swfNode += [key] +'="'+ params[key] +'" '; }
+ var pairs = this.getVariablePairs().join("&");
+ if (pairs.length > 0){ swfNode += 'flashvars="'+ pairs +'"'; }
+ swfNode += '/>';
+ } else { // PC IE
+ swfNode = '<object id="'+ this.getAttribute('id') +'" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="'+ this.getAttribute('width') +'" height="'+ this.getAttribute('height') +'">';
+ swfNode += '<param name="movie" value="'+ this.getAttribute('swf') +'" />';
+ var params = this.getParams();
+ for(var key in params) {
+ swfNode += '<param name="'+ key +'" value="'+ params[key] +'" />';
+ }
+ var pairs = this.getVariablePairs().join("&");
+ if(pairs.length > 0) {swfNode += '<param name="flashvars" value="'+ pairs +'" />';}
+ swfNode += "</object>";
+ }
+ return swfNode;
+ },
+ setDataURL: function(strDataURL){
+ //This method sets the data URL for the chart.
+ //If being set initially
+ if (this.initialDataSet==false){
+ this.addVariable('dataURL',strDataURL);
+ //Update flag
+ this.initialDataSet = true;
+ }else{
+ //Else, we update the chart data using External Interface
+ //Get reference to chart object
+ var chartObj = infosoftglobal.FusionChartsUtil.getChartObject(this.getAttribute('id'));
+
+ if (!chartObj.setDataURL)
+ {
+ __flash__addCallback(chartObj, "setDataURL");
+ }
+
+ chartObj.setDataURL(strDataURL);
+ }
+ },
+ //This function :
+ //fixes the double quoted attributes to single quotes
+ //Encodes all quotes inside attribute values
+ //Encodes % to %25 and & to %26;
+ encodeDataXML: function(strDataXML){
+
+ var regExpReservedCharacters=["\\$","\\+"];
+ var arrDQAtt=strDataXML.match(/=\s*\".*?\"/g);
+ if (arrDQAtt){
+ for(var i=0;i<arrDQAtt.length;i++){
+ var repStr=arrDQAtt[i].replace(/^=\s*\"|\"$/g,"");
+ repStr=repStr.replace(/\'/g,"%26apos;");
+ var strTo=strDataXML.indexOf(arrDQAtt[i]);
+ var repStrr="='"+repStr+"'";
+ var strStart=strDataXML.substring(0,strTo);
+ var strEnd=strDataXML.substring(strTo+arrDQAtt[i].length);
+ var strDataXML=strStart+repStrr+strEnd;
+ }
+ }
+
+ strDataXML=strDataXML.replace(/\"/g,"%26quot;");
+ strDataXML=strDataXML.replace(/%(?![\da-f]{2}|[\da-f]{4})/ig,"%25");
+ strDataXML=strDataXML.replace(/\&/g,"%26");
+
+ return strDataXML;
+
+ },
+ setDataXML: function(strDataXML){
+ //If being set initially
+ if (this.initialDataSet==false){
+ //This method sets the data XML for the chart INITIALLY.
+ this.addVariable('dataXML',this.encodeDataXML(strDataXML));
+ //Update flag
+ this.initialDataSet = true;
+ }else{
+ //Else, we update the chart data using External Interface
+ //Get reference to chart object
+ var chartObj = infosoftglobal.FusionChartsUtil.getChartObject(this.getAttribute('id'));
+ chartObj.setDataXML(strDataXML);
+ }
+ },
+ setTransparent: function(isTransparent){
+ //Sets chart to transparent mode when isTransparent is true (default)
+ //When no parameter is passed, we assume transparent to be true.
+ if(typeof isTransparent=="undefined") {
+ isTransparent=true;
+ }
+ //Set the property
+ if(isTransparent)
+ this.addParam('WMode', 'transparent');
+ else
+ this.addParam('WMode', 'Opaque');
+ },
+
+ render: function(elementId){
+ //First check for installed version of Flash Player - we need a minimum of 6
+ if((this.detectFlashVersion==1) && (this.installedVer.major < 6)){
+ if (this.autoInstallRedirect==1){
+ //If we can auto redirect to install the player?
+ var installationConfirm = window.confirm("You need Adobe Flash Player 6 (or above) to view the charts. It is a free and lightweight installation from Adobe.com. Please click on Ok to install the same.");
+ if (installationConfirm){
+ window.location = "http://www.adobe.com/shockwave/download/download.cgi?P1_Prod_Version=ShockwaveFlash";
+ }else{
+ return false;
+ }
+ }else{
+ //Else, do not take an action. It means the developer has specified a message in the DIV (and probably a link).
+ //So, expect the developers to provide a course of way to their end users.
+ //window.alert("You need Adobe Flash Player 8 (or above) to view the charts. It is a free and lightweight installation from Adobe.com. ");
+ return false;
+ }
+ }else{
+ //Render the chart
+ var n = (typeof elementId == 'string') ? document.getElementById(elementId) : elementId;
+ n.innerHTML = this.getSWFHTML();
+
+ //Added <FORM> compatibility
+ //Check if it's added in Mozilla embed array or if already exits
+ if(!document.embeds[this.getAttribute('id')] && !window[this.getAttribute('id')])
+ window[this.getAttribute('id')]=document.getElementById(this.getAttribute('id'));
+ //or else document.forms[formName/formIndex][chartId]
+ return true;
+ }
+ }
+}
+
+/* ---- detection functions ---- */
+infosoftglobal.FusionChartsUtil.getPlayerVersion = function(){
+ var PlayerVersion = new infosoftglobal.PlayerVersion([0,0,0]);
+ if(navigator.plugins && navigator.mimeTypes.length){
+ var x = navigator.plugins["Shockwave Flash"];
+ if(x && x.description) {
+ PlayerVersion = new infosoftglobal.PlayerVersion(x.description.replace(/([a-zA-Z]|\s)+/, "").replace(/(\s+r|\s+b[0-9]+)/, ".").split("."));
+ }
+ }else if (navigator.userAgent && navigator.userAgent.indexOf("Windows CE") >= 0){
+ //If Windows CE
+ var axo = 1;
+ var counter = 3;
+ while(axo) {
+ try {
+ counter++;
+ axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash."+ counter);
+ PlayerVersion = new infosoftglobal.PlayerVersion([counter,0,0]);
+ } catch (e) {
+ axo = null;
+ }
+ }
+ } else {
+ // Win IE (non mobile)
+ // Do minor version lookup in IE, but avoid Flash Player 6 crashing issues
+ try{
+ var axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");
+ }catch(e){
+ try {
+ var axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");
+ PlayerVersion = new infosoftglobal.PlayerVersion([6,0,21]);
+ axo.AllowScriptAccess = "always"; // error if player version < 6.0.47 (thanks to Michael Williams @ Adobe for this code)
+ } catch(e) {
+ if (PlayerVersion.major == 6) {
+ return PlayerVersion;
+ }
+ }
+ try {
+ axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash");
+ } catch(e) {}
+ }
+ if (axo != null) {
+ PlayerVersion = new infosoftglobal.PlayerVersion(axo.GetVariable("$version").split(" ")[1].split(","));
+ }
+ }
+ return PlayerVersion;
+}
+infosoftglobal.PlayerVersion = function(arrVersion){
+ this.major = arrVersion[0] != null ? parseInt(arrVersion[0]) : 0;
+ this.minor = arrVersion[1] != null ? parseInt(arrVersion[1]) : 0;
+ this.rev = arrVersion[2] != null ? parseInt(arrVersion[2]) : 0;
+}
+// ------------ Fix for Out of Memory Bug in IE in FP9 ---------------//
+/* Fix for video streaming bug */
+infosoftglobal.FusionChartsUtil.cleanupSWFs = function() {
+ var objects = document.getElementsByTagName("OBJECT");
+ for (var i = objects.length - 1; i >= 0; i--) {
+ objects[i].style.display = 'none';
+ for (var x in objects[i]) {
+ if (typeof objects[i][x] == 'function') {
+ objects[i][x] = function(){};
+ }
+ }
+ }
+}
+// Fixes bug in fp9
+if (infosoftglobal.FusionCharts.doPrepUnload) {
+ if (!infosoftglobal.unloadSet) {
+ infosoftglobal.FusionChartsUtil.prepUnload = function() {
+ __flash_unloadHandler = function(){};
+ __flash_savedUnloadHandler = function(){};
+ window.attachEvent("onunload", infosoftglobal.FusionChartsUtil.cleanupSWFs);
+ }
+ window.attachEvent("onbeforeunload", infosoftglobal.FusionChartsUtil.prepUnload);
+ infosoftglobal.unloadSet = true;
+ }
+}
+/* Add document.getElementById if needed (mobile IE < 5) */
+if (!document.getElementById && document.all) { document.getElementById = function(id) { return document.all[id]; }}
+/* Add Array.push if needed (ie5) */
+if (Array.prototype.push == null) { Array.prototype.push = function(item) { this[this.length] = item; return this.length; }}
+
+/* Function to return Flash Object from ID */
+infosoftglobal.FusionChartsUtil.getChartObject = function(id)
+{
+ var chartRef=null;
+ if (navigator.appName.indexOf("Microsoft Internet")==-1) {
+ if (document.embeds && document.embeds[id])
+ chartRef = document.embeds[id];
+ else
+ chartRef = window.document[id];
+ }
+ else {
+ chartRef = window[id];
+ }
+ if (!chartRef)
+ chartRef = document.getElementById(id);
+
+ return chartRef;
+}
+/*
+ Function to update chart's data at client side (FOR FusionCharts vFREE and 2.x
+*/
+infosoftglobal.FusionChartsUtil.updateChartXML = function(chartId, strXML){
+ //Get reference to chart object
+ var chartObj = infosoftglobal.FusionChartsUtil.getChartObject(chartId);
+ //Set dataURL to null
+ chartObj.SetVariable("_root.dataURL","");
+ //Set the flag
+ chartObj.SetVariable("_root.isNewData","1");
+ //Set the actual data
+ chartObj.SetVariable("_root.newData",strXML);
+ //Go to the required frame
+ chartObj.TGotoLabel("/", "JavaScriptHandler");
+}
+
+
+/* Aliases for easy usage */
+var getChartFromId = infosoftglobal.FusionChartsUtil.getChartObject;
+var updateChartXML = infosoftglobal.FusionChartsUtil.updateChartXML;
+var FusionCharts = infosoftglobal.FusionCharts;
+\ No newline at end of file
(DIR) diff --git a/web/public/javascripts/application.js b/web/public/javascripts/application.js
@@ -0,0 +1,2 @@
+// Place your application-specific JavaScript functions and classes here
+// This file is automatically included by javascript_include_tag :defaults
(DIR) diff --git a/web/public/javascripts/controls.js b/web/public/javascripts/controls.js
@@ -0,0 +1,965 @@
+// script.aculo.us controls.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009
+
+// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+// (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+// (c) 2005-2009 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("controls.js requires including script.aculo.us' effects.js library");
+
+var Autocompleter = { };
+Autocompleter.Base = Class.create({
+ baseInitialize: function(element, update, options) {
+ element = $(element);
+ this.element = element;
+ this.update = $(update);
+ this.hasFocus = false;
+ this.changed = false;
+ this.active = false;
+ this.index = 0;
+ this.entryCount = 0;
+ this.oldElementValue = this.element.value;
+
+ 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);
+ // Force carriage returns as token delimiters anyway
+ if (!this.options.tokens.include('\n'))
+ this.options.tokens.push('\n');
+
+ this.observer = null;
+
+ this.element.setAttribute('autocomplete','off');
+
+ Element.hide(this.update);
+
+ Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
+ Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
+ },
+
+ show: function() {
+ if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+ if(!this.iefix &&
+ (Prototype.Browser.IE) &&
+ (Element.getStyle(this.update, 'position')=='absolute')) {
+ new Insertion.After(this.update,
+ '<iframe id="' + this.update.id + '_iefix" '+
+ 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+ 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+ this.iefix = $(this.update.id+'_iefix');
+ }
+ if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+ },
+
+ fixIEOverlapping: function() {
+ Position.clone(this.update, this.iefix, {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();
+ Event.stop(event);
+ return;
+ case Event.KEY_DOWN:
+ this.markNext();
+ this.render();
+ Event.stop(event);
+ return;
+ }
+ else
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
+ (Prototype.Browser.WebKit > 0 && 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 > 0) {
+ for (var i = 0; i < this.entryCount; i++)
+ this.index==i ?
+ Element.addClassName(this.getEntry(i),"selected") :
+ Element.removeClassName(this.getEntry(i),"selected");
+ if(this.hasFocus) {
+ this.show();
+ this.active = true;
+ }
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ },
+
+ markPrevious: function() {
+ if(this.index > 0) this.index--;
+ else this.index = this.entryCount-1;
+ this.getEntry(this.index).scrollIntoView(true);
+ },
+
+ markNext: function() {
+ if(this.index < 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 = $(selectedElement).select('.' + this.options.select) || [];
+ if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
+ } else
+ value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+
+ var bounds = this.getTokenBounds();
+ if (bounds[0] != -1) {
+ var newValue = this.element.value.substr(0, bounds[0]);
+ var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
+ if (whitespace)
+ newValue += whitespace[0];
+ this.element.value = newValue + value + this.element.value.substr(bounds[1]);
+ } else {
+ this.element.value = value;
+ }
+ this.oldElementValue = this.element.value;
+ this.element.focus();
+
+ if (this.options.afterUpdateElement)
+ this.options.afterUpdateElement(this.element, selectedElement);
+ },
+
+ updateChoices: function(choices) {
+ if(!this.changed && this.hasFocus) {
+ this.update.innerHTML = choices;
+ Element.cleanWhitespace(this.update);
+ Element.cleanWhitespace(this.update.down());
+
+ if(this.update.firstChild && this.update.down().childNodes) {
+ this.entryCount =
+ this.update.down().childNodes.length;
+ for (var i = 0; i < this.entryCount; i++) {
+ var entry = this.getEntry(i);
+ entry.autocompleteIndex = i;
+ this.addObservers(entry);
+ }
+ } else {
+ this.entryCount = 0;
+ }
+
+ this.stopIndicator();
+ this.index = 0;
+
+ if(this.entryCount==1 && this.options.autoSelect) {
+ this.selectEntry();
+ this.hide();
+ } else {
+ this.render();
+ }
+ }
+ },
+
+ addObservers: function(element) {
+ Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+ Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+ },
+
+ onObserverEvent: function() {
+ this.changed = false;
+ this.tokenBounds = null;
+ if(this.getToken().length>=this.options.minChars) {
+ this.getUpdatedChoices();
+ } else {
+ this.active = false;
+ this.hide();
+ }
+ this.oldElementValue = this.element.value;
+ },
+
+ getToken: function() {
+ var bounds = this.getTokenBounds();
+ return this.element.value.substring(bounds[0], bounds[1]).strip();
+ },
+
+ getTokenBounds: function() {
+ if (null != this.tokenBounds) return this.tokenBounds;
+ var value = this.element.value;
+ if (value.strip().empty()) return [-1, 0];
+ var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
+ var offset = (diff == this.oldElementValue.length ? 1 : 0);
+ var prevTokenPos = -1, nextTokenPos = value.length;
+ var tp;
+ for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
+ tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
+ if (tp > prevTokenPos) prevTokenPos = tp;
+ tp = value.indexOf(this.options.tokens[index], diff + offset);
+ if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
+ }
+ return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
+ }
+});
+
+Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
+ var boundary = Math.min(newS.length, oldS.length);
+ for (var index = 0; index < boundary; ++index)
+ if (newS[index] != oldS[index])
+ return index;
+ return boundary;
+};
+
+Ajax.Autocompleter = Class.create(Autocompleter.Base, {
+ 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() {
+ this.startIndicator();
+
+ var entry = encodeURIComponent(this.options.paramName) + '=' +
+ encodeURIComponent(this.getToken());
+
+ this.options.parameters = this.options.callback ?
+ this.options.callback(this.element, entry) : entry;
+
+ if(this.options.defaultParams)
+ this.options.parameters += '&' + this.options.defaultParams;
+
+ new Ajax.Request(this.url, this.options);
+ },
+
+ onComplete: function(request) {
+ this.updateChoices(request.responseText);
+ }
+});
+
+// The local array autocompleter. Used when you'd prefer to
+// inject an array of autocompletion options into the page, rather
+// than sending out Ajax queries, which can be quite slow sometimes.
+//
+// The constructor takes four parameters. The first two are, as usual,
+// the id of the monitored textbox, and id of the autocompletion menu.
+// The third is the array you want to autocomplete from, and the fourth
+// is the options block.
+//
+// Extra local autocompletion options:
+// - choices - How many autocompletion choices to offer
+//
+// - partialSearch - If false, the autocompleter will match entered
+// text only at the beginning of strings in the
+// autocomplete array. Defaults to true, which will
+// match text at the beginning of any *word* in the
+// strings in the autocomplete array. If you want to
+// search anywhere in the string, additionally set
+// the option fullSearch to true (default: off).
+//
+// - fullSsearch - Search anywhere in autocomplete array strings.
+//
+// - partialChars - How many characters to enter before triggering
+// a partial match (unlike minChars, which defines
+// how many characters are required to do any match
+// at all). Defaults to 2.
+//
+// - ignoreCase - Whether to ignore case when autocompleting.
+// Defaults to true.
+//
+// It's possible to pass in a custom function as the 'selector'
+// option, if you prefer to write your own autocompletion logic.
+// In that case, the other options above will not apply unless
+// you support them.
+
+Autocompleter.Local = Class.create(Autocompleter.Base, {
+ initialize: function(element, update, array, options) {
+ this.baseInitialize(element, update, options);
+ this.options.array = array;
+ },
+
+ getUpdatedChoices: function() {
+ this.updateChoices(this.options.selector(this));
+ },
+
+ setOptions: function(options) {
+ this.options = Object.extend({
+ choices: 10,
+ partialSearch: true,
+ partialChars: 2,
+ ignoreCase: true,
+ fullSearch: false,
+ selector: function(instance) {
+ var ret = []; // Beginning matches
+ var partial = []; // Inside matches
+ var entry = instance.getToken();
+ var count = 0;
+
+ for (var i = 0; i < instance.options.array.length &&
+ ret.length < instance.options.choices ; i++) {
+
+ var elem = instance.options.array[i];
+ var foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase()) :
+ elem.indexOf(entry);
+
+ while (foundPos != -1) {
+ if (foundPos == 0 && elem.length != entry.length) {
+ ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
+ elem.substr(entry.length) + "</li>");
+ break;
+ } else if (entry.length >= instance.options.partialChars &&
+ instance.options.partialSearch && foundPos != -1) {
+ if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
+ partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
+ elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
+ foundPos + entry.length) + "</li>");
+ break;
+ }
+ }
+
+ foundPos = instance.options.ignoreCase ?
+ elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
+ elem.indexOf(entry, foundPos + 1);
+
+ }
+ }
+ if (partial.length)
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
+ return "<ul>" + ret.join('') + "</ul>";
+ }
+ }, options || { });
+ }
+});
+
+// AJAX in-place editor and collection editor
+// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
+
+// 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({
+ initialize: function(element, url, options) {
+ this.url = url;
+ this.element = element = $(element);
+ this.prepareOptions();
+ this._controls = { };
+ arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
+ Object.extend(this.options, options || { });
+ if (!this.options.formId && this.element.id) {
+ this.options.formId = this.element.id + '-inplaceeditor';
+ if ($(this.options.formId))
+ this.options.formId = '';
+ }
+ if (this.options.externalControl)
+ this.options.externalControl = $(this.options.externalControl);
+ if (!this.options.externalControl)
+ this.options.externalControlOnly = false;
+ this._originalBackground = this.element.getStyle('background-color') || 'transparent';
+ this.element.title = this.options.clickToEditText;
+ this._boundCancelHandler = this.handleFormCancellation.bind(this);
+ this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
+ this._boundFailureHandler = this.handleAJAXFailure.bind(this);
+ this._boundSubmitHandler = this.handleFormSubmission.bind(this);
+ this._boundWrapperHandler = this.wrapUp.bind(this);
+ this.registerListeners();
+ },
+ checkForEscapeOrReturn: function(e) {
+ if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
+ if (Event.KEY_ESC == e.keyCode)
+ this.handleFormCancellation(e);
+ else if (Event.KEY_RETURN == e.keyCode)
+ this.handleFormSubmission(e);
+ },
+ createControl: function(mode, handler, extraClasses) {
+ var control = this.options[mode + 'Control'];
+ var text = this.options[mode + 'Text'];
+ if ('button' == control) {
+ var btn = document.createElement('input');
+ btn.type = 'submit';
+ btn.value = text;
+ btn.className = 'editor_' + mode + '_button';
+ if ('cancel' == mode)
+ btn.onclick = this._boundCancelHandler;
+ this._form.appendChild(btn);
+ this._controls[mode] = btn;
+ } else if ('link' == control) {
+ var link = document.createElement('a');
+ link.href = '#';
+ link.appendChild(document.createTextNode(text));
+ link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
+ link.className = 'editor_' + mode + '_link';
+ if (extraClasses)
+ link.className += ' ' + extraClasses;
+ this._form.appendChild(link);
+ this._controls[mode] = link;
+ }
+ },
+ createEditField: function() {
+ var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
+ var fld;
+ if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
+ fld = document.createElement('input');
+ fld.type = 'text';
+ var size = this.options.size || this.options.cols || 0;
+ if (0 < size) fld.size = size;
+ } else {
+ fld = document.createElement('textarea');
+ fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
+ fld.cols = this.options.cols || 40;
+ }
+ fld.name = this.options.paramName;
+ fld.value = text; // No HTML breaks conversion anymore
+ fld.className = 'editor_field';
+ if (this.options.submitOnBlur)
+ fld.onblur = this._boundSubmitHandler;
+ this._controls.editor = fld;
+ if (this.options.loadTextURL)
+ this.loadExternalText();
+ this._form.appendChild(this._controls.editor);
+ },
+ createForm: function() {
+ var ipe = this;
+ function addText(mode, condition) {
+ var text = ipe.options['text' + mode + 'Controls'];
+ if (!text || condition === false) return;
+ ipe._form.appendChild(document.createTextNode(text));
+ };
+ this._form = $(document.createElement('form'));
+ this._form.id = this.options.formId;
+ this._form.addClassName(this.options.formClassName);
+ this._form.onsubmit = this._boundSubmitHandler;
+ this.createEditField();
+ if ('textarea' == this._controls.editor.tagName.toLowerCase())
+ this._form.appendChild(document.createElement('br'));
+ if (this.options.onFormCustomization)
+ this.options.onFormCustomization(this, this._form);
+ addText('Before', this.options.okControl || this.options.cancelControl);
+ this.createControl('ok', this._boundSubmitHandler);
+ addText('Between', this.options.okControl && this.options.cancelControl);
+ this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
+ addText('After', this.options.okControl || this.options.cancelControl);
+ },
+ destroy: function() {
+ if (this._oldInnerHTML)
+ this.element.innerHTML = this._oldInnerHTML;
+ this.leaveEditMode();
+ this.unregisterListeners();
+ },
+ enterEditMode: function(e) {
+ if (this._saving || this._editing) return;
+ this._editing = true;
+ this.triggerCallback('onEnterEditMode');
+ if (this.options.externalControl)
+ this.options.externalControl.hide();
+ this.element.hide();
+ this.createForm();
+ this.element.parentNode.insertBefore(this._form, this.element);
+ if (!this.options.loadTextURL)
+ this.postProcessEditField();
+ if (e) Event.stop(e);
+ },
+ enterHover: function(e) {
+ if (this.options.hoverClassName)
+ this.element.addClassName(this.options.hoverClassName);
+ if (this._saving) return;
+ this.triggerCallback('onEnterHover');
+ },
+ getText: function() {
+ return this.element.innerHTML.unescapeHTML();
+ },
+ handleAJAXFailure: function(transport) {
+ this.triggerCallback('onFailure', transport);
+ if (this._oldInnerHTML) {
+ this.element.innerHTML = this._oldInnerHTML;
+ this._oldInnerHTML = null;
+ }
+ },
+ handleFormCancellation: function(e) {
+ this.wrapUp();
+ if (e) Event.stop(e);
+ },
+ handleFormSubmission: function(e) {
+ var form = this._form;
+ var value = $F(this._controls.editor);
+ this.prepareSubmission();
+ var params = this.options.callback(form, value) || '';
+ if (Object.isString(params))
+ params = params.toQueryParams();
+ params.editorId = this.element.id;
+ if (this.options.htmlResponse) {
+ var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: params,
+ onComplete: this._boundWrapperHandler,
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Updater({ success: this.element }, this.url, options);
+ } else {
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: params,
+ onComplete: this._boundWrapperHandler,
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Request(this.url, options);
+ }
+ if (e) Event.stop(e);
+ },
+ leaveEditMode: function() {
+ this.element.removeClassName(this.options.savingClassName);
+ this.removeForm();
+ this.leaveHover();
+ this.element.style.backgroundColor = this._originalBackground;
+ this.element.show();
+ if (this.options.externalControl)
+ this.options.externalControl.show();
+ this._saving = false;
+ this._editing = false;
+ this._oldInnerHTML = null;
+ this.triggerCallback('onLeaveEditMode');
+ },
+ leaveHover: function(e) {
+ if (this.options.hoverClassName)
+ this.element.removeClassName(this.options.hoverClassName);
+ if (this._saving) return;
+ this.triggerCallback('onLeaveHover');
+ },
+ loadExternalText: function() {
+ this._form.addClassName(this.options.loadingClassName);
+ this._controls.editor.disabled = true;
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ this._form.removeClassName(this.options.loadingClassName);
+ var text = transport.responseText;
+ if (this.options.stripLoadedTextTags)
+ text = text.stripTags();
+ this._controls.editor.value = text;
+ this._controls.editor.disabled = false;
+ this.postProcessEditField();
+ }.bind(this),
+ onFailure: this._boundFailureHandler
+ });
+ new Ajax.Request(this.options.loadTextURL, options);
+ },
+ postProcessEditField: function() {
+ var fpc = this.options.fieldPostCreation;
+ if (fpc)
+ $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
+ },
+ prepareOptions: function() {
+ this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
+ Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
+ [this._extraDefaultOptions].flatten().compact().each(function(defs) {
+ Object.extend(this.options, defs);
+ }.bind(this));
+ },
+ prepareSubmission: function() {
+ this._saving = true;
+ this.removeForm();
+ this.leaveHover();
+ this.showSaving();
+ },
+ registerListeners: function() {
+ this._listeners = { };
+ var listener;
+ $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
+ listener = this[pair.value].bind(this);
+ this._listeners[pair.key] = listener;
+ if (!this.options.externalControlOnly)
+ this.element.observe(pair.key, listener);
+ if (this.options.externalControl)
+ this.options.externalControl.observe(pair.key, listener);
+ }.bind(this));
+ },
+ removeForm: function() {
+ if (!this._form) return;
+ this._form.remove();
+ this._form = null;
+ this._controls = { };
+ },
+ showSaving: function() {
+ this._oldInnerHTML = this.element.innerHTML;
+ this.element.innerHTML = this.options.savingText;
+ this.element.addClassName(this.options.savingClassName);
+ this.element.style.backgroundColor = this._originalBackground;
+ this.element.show();
+ },
+ triggerCallback: function(cbName, arg) {
+ if ('function' == typeof this.options[cbName]) {
+ this.options[cbName](this, arg);
+ }
+ },
+ unregisterListeners: function() {
+ $H(this._listeners).each(function(pair) {
+ if (!this.options.externalControlOnly)
+ this.element.stopObserving(pair.key, pair.value);
+ if (this.options.externalControl)
+ this.options.externalControl.stopObserving(pair.key, pair.value);
+ }.bind(this));
+ },
+ wrapUp: function(transport) {
+ this.leaveEditMode();
+ // Can't use triggerCallback due to backward compatibility: requires
+ // binding + direct element
+ this._boundComplete(transport, this.element);
+ }
+});
+
+Object.extend(Ajax.InPlaceEditor.prototype, {
+ dispose: Ajax.InPlaceEditor.prototype.destroy
+});
+
+Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
+ initialize: function($super, element, url, options) {
+ this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
+ $super(element, url, options);
+ },
+
+ createEditField: function() {
+ var list = document.createElement('select');
+ list.name = this.options.paramName;
+ list.size = 1;
+ this._controls.editor = list;
+ this._collection = this.options.collection || [];
+ if (this.options.loadCollectionURL)
+ this.loadCollection();
+ else
+ this.checkForExternalText();
+ this._form.appendChild(this._controls.editor);
+ },
+
+ loadCollection: function() {
+ this._form.addClassName(this.options.loadingClassName);
+ this.showLoadingText(this.options.loadingCollectionText);
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ var js = transport.responseText.strip();
+ if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
+ throw('Server returned an invalid collection representation.');
+ this._collection = eval(js);
+ this.checkForExternalText();
+ }.bind(this),
+ onFailure: this.onFailure
+ });
+ new Ajax.Request(this.options.loadCollectionURL, options);
+ },
+
+ showLoadingText: function(text) {
+ this._controls.editor.disabled = true;
+ var tempOption = this._controls.editor.firstChild;
+ if (!tempOption) {
+ tempOption = document.createElement('option');
+ tempOption.value = '';
+ this._controls.editor.appendChild(tempOption);
+ tempOption.selected = true;
+ }
+ tempOption.update((text || '').stripScripts().stripTags());
+ },
+
+ checkForExternalText: function() {
+ this._text = this.getText();
+ if (this.options.loadTextURL)
+ this.loadExternalText();
+ else
+ this.buildOptionList();
+ },
+
+ loadExternalText: function() {
+ this.showLoadingText(this.options.loadingText);
+ var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+ Object.extend(options, {
+ parameters: 'editorId=' + encodeURIComponent(this.element.id),
+ onComplete: Prototype.emptyFunction,
+ onSuccess: function(transport) {
+ this._text = transport.responseText.strip();
+ this.buildOptionList();
+ }.bind(this),
+ onFailure: this.onFailure
+ });
+ new Ajax.Request(this.options.loadTextURL, options);
+ },
+
+ buildOptionList: function() {
+ this._form.removeClassName(this.options.loadingClassName);
+ this._collection = this._collection.map(function(entry) {
+ return 2 === entry.length ? entry : [entry, entry].flatten();
+ });
+ var marker = ('value' in this.options) ? this.options.value : this._text;
+ var textFound = this._collection.any(function(entry) {
+ return entry[0] == marker;
+ }.bind(this));
+ this._controls.editor.update('');
+ var option;
+ this._collection.each(function(entry, index) {
+ option = document.createElement('option');
+ option.value = entry[0];
+ option.selected = textFound ? entry[0] == marker : 0 == index;
+ option.appendChild(document.createTextNode(entry[1]));
+ this._controls.editor.appendChild(option);
+ }.bind(this));
+ this._controls.editor.disabled = false;
+ Field.scrollFreeActivate(this._controls.editor);
+ }
+});
+
+//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
+//**** This only exists for a while, in order to let ****
+//**** users adapt to the new API. Read up on the new ****
+//**** API and convert your code to it ASAP! ****
+
+Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
+ if (!options) return;
+ function fallback(name, expr) {
+ if (name in options || expr === undefined) return;
+ options[name] = expr;
+ };
+ fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
+ options.cancelLink == options.cancelButton == false ? false : undefined)));
+ fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
+ options.okLink == options.okButton == false ? false : undefined)));
+ fallback('highlightColor', options.highlightcolor);
+ fallback('highlightEndColor', options.highlightendcolor);
+};
+
+Object.extend(Ajax.InPlaceEditor, {
+ DefaultOptions: {
+ ajaxOptions: { },
+ autoRows: 3, // Use when multi-line w/ rows == 1
+ cancelControl: 'link', // 'link'|'button'|false
+ cancelText: 'cancel',
+ clickToEditText: 'Click to edit',
+ externalControl: null, // id|elt
+ externalControlOnly: false,
+ fieldPostCreation: 'activate', // 'activate'|'focus'|false
+ formClassName: 'inplaceeditor-form',
+ formId: null, // id|elt
+ highlightColor: '#ffff99',
+ highlightEndColor: '#ffffff',
+ hoverClassName: '',
+ htmlResponse: true,
+ loadingClassName: 'inplaceeditor-loading',
+ loadingText: 'Loading...',
+ okControl: 'button', // 'link'|'button'|false
+ okText: 'ok',
+ paramName: 'value',
+ rows: 1, // If 1 and multi-line, uses autoRows
+ savingClassName: 'inplaceeditor-saving',
+ savingText: 'Saving...',
+ size: 0,
+ stripLoadedTextTags: false,
+ submitOnBlur: false,
+ textAfterControls: '',
+ textBeforeControls: '',
+ textBetweenControls: ''
+ },
+ DefaultCallbacks: {
+ callback: function(form) {
+ return Form.serialize(form);
+ },
+ onComplete: function(transport, element) {
+ // For backward compatibility, this one is bound to the IPE, and passes
+ // the element directly. It was too often customized, so we don't break it.
+ new Effect.Highlight(element, {
+ startcolor: this.options.highlightColor, keepBackgroundImage: true });
+ },
+ onEnterEditMode: null,
+ onEnterHover: function(ipe) {
+ ipe.element.style.backgroundColor = ipe.options.highlightColor;
+ if (ipe._effect)
+ ipe._effect.cancel();
+ },
+ onFailure: function(transport, ipe) {
+ alert('Error communication with the server: ' + transport.responseText.stripTags());
+ },
+ onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
+ onLeaveEditMode: null,
+ onLeaveHover: function(ipe) {
+ ipe._effect = new Effect.Highlight(ipe.element, {
+ startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
+ restorecolor: ipe._originalBackground, keepBackgroundImage: true
+ });
+ }
+ },
+ Listeners: {
+ click: 'enterEditMode',
+ keydown: 'checkForEscapeOrReturn',
+ mouseover: 'enterHover',
+ mouseout: 'leaveHover'
+ }
+});
+
+Ajax.InPlaceCollectionEditor.DefaultOptions = {
+ loadingCollectionText: 'Loading options...'
+};
+
+// Delayed observer, like Form.Element.Observer,
+// but waits for delay after last key input
+// Ideal for live-search fields
+
+Form.Element.DelayedObserver = Class.create({
+ 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));
+ }
+});
+\ No newline at end of file
(DIR) diff --git a/web/public/javascripts/custom.js b/web/public/javascripts/custom.js
(DIR) diff --git a/web/public/javascripts/dragdrop.js b/web/public/javascripts/dragdrop.js
@@ -0,0 +1,974 @@
+// script.aculo.us dragdrop.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009
+
+// Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// 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(Object.isUndefined(Effect))
+ throw("dragdrop.js requires including script.aculo.us' effects.js library");
+
+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(Object.isArray(containment)) {
+ 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 < 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) &&
+ ((!drop._containers) ||
+ this.isContained(element, drop)) &&
+ ((!drop.accept) ||
+ (Element.classNames(element).detect(
+ function(v) { return drop.accept.include(v) } ) )) &&
+ 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 drop, affected = [];
+
+ this.drops.each( function(drop) {
+ if(Droppables.isAffected(point, element, drop))
+ affected.push(drop);
+ });
+
+ if(affected.length>0)
+ drop = Droppables.findDeepestChild(affected);
+
+ if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
+ if (drop) {
+ Position.within(drop.element, point[0], point[1]);
+ if(drop.onHover)
+ drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
+
+ if (drop != this.last_active) 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);
+ return true;
+ }
+ },
+
+ 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, "mouseup", this.eventMouseUp);
+ Event.observe(document, "mousemove", this.eventMouseMove);
+ Event.observe(document, "keypress", 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, "mouseup", this.eventMouseUp);
+ Event.stopObserving(document, "mousemove", this.eventMouseMove);
+ Event.stopObserving(document, "keypress", 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 && (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'] > 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({
+ 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 = Object.isNumber(element._opacity) ? 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,
+ quiet: 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] || Object.isUndefined(arguments[1].endeffect))
+ 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 && Object.isString(options.handle))
+ 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 && !options.scroll.scrollTo && !options.scroll.outerHTML) {
+ options.scroll = $(options.scroll);
+ this._isScrollChild = Element.childOf(this.element, options.scroll);
+ }
+
+ Element.makePositioned(this.element); // fix IE
+
+ this.options = options;
+ this.dragging = false;
+
+ this.eventMouseDown = this.initDrag.bindAsEventListener(this);
+ Event.observe(this.handle, "mousedown", this.eventMouseDown);
+
+ Draggables.register(this);
+ },
+
+ destroy: function() {
+ Event.stopObserving(this.handle, "mousedown", 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(!Object.isUndefined(Draggable._dragging[this.element]) &&
+ Draggable._dragging[this.element]) return;
+ if(Event.isLeftClick(event)) {
+ // abort on form elements, fixes a Firefox issue
+ var src = Event.element(event);
+ if((tag_name = src.tagName.toUpperCase()) && (
+ tag_name=='INPUT' ||
+ tag_name=='SELECT' ||
+ tag_name=='OPTION' ||
+ tag_name=='BUTTON' ||
+ tag_name=='TEXTAREA')) return;
+
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ var pos = this.element.cumulativeOffset();
+ 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.delta)
+ this.delta = this.currentDelta();
+
+ 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);
+ this._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
+ if (!this._originallyAbsolute)
+ 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);
+
+ if(!this.options.quiet){
+ 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] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
+ if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
+ if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
+ if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
+ this.startScrolling(speed);
+ }
+
+ // fix AppleWebKit rendering
+ if(Prototype.Browser.WebKit) window.scrollBy(0,0);
+
+ Event.stop(event);
+ },
+
+ finishDrag: function(event, success) {
+ this.dragging = false;
+
+ if(this.options.quiet){
+ Position.prepare();
+ var pointer = [Event.pointerX(event), Event.pointerY(event)];
+ Droppables.show(pointer, this.element);
+ }
+
+ if(this.options.ghosting) {
+ if (!this._originallyAbsolute)
+ Position.relativize(this.element);
+ delete this._originallyAbsolute;
+ Element.remove(this._clone);
+ this._clone = null;
+ }
+
+ var dropped = false;
+ if(success) {
+ dropped = Droppables.fire(event, this.element);
+ if (!dropped) dropped = false;
+ }
+ if(dropped && this.options.onDropped) this.options.onDropped(this.element);
+ Draggables.notify('onEnd', this, event);
+
+ var revert = this.options.revert;
+ if(revert && Object.isFunction(revert)) revert = revert(this.element);
+
+ var d = this.currentDelta();
+ if(revert && this.options.reverteffect) {
+ if (dropped == 0 || revert != 'failure')
+ 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 = this.element.cumulativeOffset();
+ 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 && (this.options.scroll != window && 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(Object.isFunction(this.options.snap)) {
+ p = this.options.snap(p[0],p[1],this);
+ } else {
+ if(Object.isArray(this.options.snap)) {
+ p = p.map( function(v, i) {
+ return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this));
+ } else {
+ p = p.map( function(v) {
+ return (v/this.options.snap).round()*this.options.snap }.bind(this));
+ }
+ }}
+
+ var style = this.element.style;
+ if((!this.options.constraint) || (this.options.constraint=='horizontal'))
+ style.left = p[0] + "px";
+ if((!this.options.constraint) || (this.options.constraint=='vertical'))
+ style.top = p[1] + "px";
+
+ if(style.visibility=="hidden") style.visibility = ""; // 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] < 0)
+ Draggables._lastScrollPointer[0] = 0;
+ if (Draggables._lastScrollPointer[1] < 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 && 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 && documentElement.clientWidth) {
+ W = documentElement.clientWidth;
+ H = documentElement.clientHeight;
+ } else {
+ W = body.offsetWidth;
+ H = body.offsetHeight;
+ }
+ }
+ return { top: T, left: L, width: W, height: H };
+ }
+});
+
+Draggable._dragging = { };
+
+/*--------------------------------------------------------------------------*/
+
+var SortableObserver = Class.create({
+ 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.toUpperCase() != "BODY") {
+ if(element.id && 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){
+ element = $(element);
+ var s = Sortable.sortables[element.id];
+
+ 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,
+ quiet: false,
+ scroll: false,
+ scrollSensitivity: 20,
+ scrollSpeed: 15,
+ format: this.SERIALIZE_RULE,
+
+ // these take arrays of elements or ids and can be
+ // used for better initialization performance
+ elements: false,
+ handles: false,
+
+ 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,
+ quiet: options.quiet,
+ 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);
+ }
+
+ (options.elements || this.findElements(element, options) || []).each( function(e,i) {
+ var handle = options.handles ? $(options.handles[i]) :
+ (options.handle ? $(e).select('.' + 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.identify()] = 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 > .33 && overlap < .66 && Sortable.options(dropon).tree) {
+ return;
+ } else if(overlap>0.5) {
+ Sortable.mark(dropon, 'before');
+ if(dropon.previousSibling != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, dropon);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ } else {
+ Sortable.mark(dropon, 'after');
+ var nextElement = dropon.nextSibling || null;
+ if(nextElement != element) {
+ var oldParentNode = element.parentNode;
+ element.style.visibility = "hidden"; // fix gecko rendering
+ dropon.parentNode.insertBefore(element, nextElement);
+ if(dropon.parentNode!=oldParentNode)
+ Sortable.options(oldParentNode).onChange(element);
+ Sortable.options(dropon.parentNode).onChange(element);
+ }
+ }
+ },
+
+ onEmptyHover: function(element, dropon, 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 < children.length; index += 1) {
+ if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
+ offset -= Element.offsetSize (children[index], droponOptions.overlap);
+ } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
+ child = index + 1 < 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 && !sortable.ghosting) return;
+
+ if(!Sortable._marker) {
+ Sortable._marker =
+ ($('dropmarker') || Element.extend(document.createElement('DIV'))).
+ hide().addClassName('dropmarker').setStyle({position:'absolute'});
+ document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
+ }
+ var offsets = dropon.cumulativeOffset();
+ 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 < 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] && 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) + "[id]=" +
+ encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
+ }).flatten().join('&');
+ } else {
+ return Sortable.sequence(element, arguments[1]).map( function(item) {
+ return name + "[]=" + encodeURIComponent(item);
+ }).join('&');
+ }
+ }
+};
+
+// 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 && e.tagName.toUpperCase()==tagName &&
+ (!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>0 ? elements.flatten() : []);
+};
+
+Element.offsetSize = function (element, type) {
+ return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
+};
+\ No newline at end of file
(DIR) diff --git a/web/public/javascripts/effects.js b/web/public/javascripts/effects.js
@@ -0,0 +1,1123 @@
+// script.aculo.us effects.js v1.8.3, Thu Oct 08 11:23:33 +0200 2009
+
+// Copyright (c) 2005-2009 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<3);
+ } else {
+ if (this.slice(0,1) == '#') {
+ if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
+ if (this.length==7) color = this.toLowerCase();
+ }
+ }
+ return (color.length==7 ? color : (arguments[0] || this));
+};
+
+/*--------------------------------------------------------------------------*/
+
+Element.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() && !Element.hasClassName(node,className)) ?
+ Element.collectTextNodesIgnoreClass(node, className) : ''));
+ }).flatten().join('');
+};
+
+Element.setContentZoom = function(element, percent) {
+ element = $(element);
+ element.setStyle({fontSize: (percent/100) + 'em'});
+ if (Prototype.Browser.WebKit) window.scrollBy(0,0);
+ 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) { }
+};
+
+/*--------------------------------------------------------------------------*/
+
+var Effect = {
+ _elementDoesNotExistError: {
+ name: 'ElementDoesNotExistError',
+ message: 'The specified DOM element does not exist, but is required for this effect to operate'
+ },
+ Transitions: {
+ linear: Prototype.K,
+ sinoidal: function(pos) {
+ return (-Math.cos(pos*Math.PI)/2) + .5;
+ },
+ reverse: function(pos) {
+ return 1-pos;
+ },
+ flicker: function(pos) {
+ var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4;
+ return pos > 1 ? 1 : pos;
+ },
+ wobble: function(pos) {
+ return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5;
+ },
+ pulse: function(pos, pulses) {
+ return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5;
+ },
+ spring: function(pos) {
+ return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
+ },
+ none: function(pos) {
+ return 0;
+ },
+ full: function(pos) {
+ return 1;
+ }
+ },
+ DefaultOptions: {
+ duration: 1.0, // seconds
+ fps: 100, // 100= assume 66fps max.
+ sync: false, // true for combining
+ from: 0.0,
+ to: 1.0,
+ delay: 0.0,
+ queue: 'parallel'
+ },
+ tagifyText: function(element) {
+ var tagifyStyle = 'position:relative';
+ if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';
+
+ element = $(element);
+ $A(element.childNodes).each( function(child) {
+ if (child.nodeType==3) {
+ child.nodeValue.toArray().each( function(character) {
+ element.insertBefore(
+ new Element('span', {style: tagifyStyle}).update(
+ character == ' ' ? String.fromCharCode(160) : character),
+ child);
+ });
+ Element.remove(child);
+ }
+ });
+ },
+ multiple: function(element, effect) {
+ var elements;
+ if (((typeof element == 'object') ||
+ Object.isFunction(element)) &&
+ (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, options) {
+ element = $(element);
+ effect = (effect || 'appear').toLowerCase();
+
+ return Effect[ Effect.PAIRS[ effect ][ element.visible() ? 1 : 0 ] ](element, Object.extend({
+ queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
+ }, options || {}));
+ }
+};
+
+Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;
+
+/* ------------- core effects ------------- */
+
+Effect.ScopedQueue = Class.create(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 = Object.isString(effect.options.queue) ?
+ 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 < effect.options.queue.limit))
+ this.effects.push(effect);
+
+ if (!this.interval)
+ this.interval = setInterval(this.loop.bind(this), 15);
+ },
+ 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();
+ for(var i=0, len=this.effects.length;i<len;i++)
+ this.effects[i] && this.effects[i].loop(timePos);
+ }
+});
+
+Effect.Queues = {
+ instances: $H(),
+ get: function(queueName) {
+ if (!Object.isString(queueName)) return queueName;
+
+ return this.instances.get(queueName) ||
+ this.instances.set(queueName, new Effect.ScopedQueue());
+ }
+};
+Effect.Queue = Effect.Queues.get('global');
+
+Effect.Base = Class.create({
+ position: null,
+ start: function(options) {
+ if (options && options.transition === false) options.transition = Effect.Transitions.linear;
+ 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.fromToDelta = this.options.to-this.options.from;
+ this.totalTime = this.finishOn-this.startOn;
+ this.totalFrames = this.options.fps*this.options.duration;
+
+ this.render = (function() {
+ function dispatch(effect, eventName) {
+ if (effect.options[eventName + 'Internal'])
+ effect.options[eventName + 'Internal'](effect);
+ if (effect.options[eventName])
+ effect.options[eventName](effect);
+ }
+
+ return function(pos) {
+ if (this.state === "idle") {
+ this.state = "running";
+ dispatch(this, 'beforeSetup');
+ if (this.setup) this.setup();
+ dispatch(this, 'afterSetup');
+ }
+ if (this.state === "running") {
+ pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from;
+ this.position = pos;
+ dispatch(this, 'beforeUpdate');
+ if (this.update) this.update(pos);
+ dispatch(this, 'afterUpdate');
+ }
+ };
+ })();
+
+ this.event('beforeStart');
+ if (!this.options.sync)
+ Effect.Queues.get(Object.isString(this.options.queue) ?
+ 'global' : this.options.queue.scope).add(this);
+ },
+ loop: function(timePos) {
+ if (timePos >= this.startOn) {
+ if (timePos >= this.finishOn) {
+ this.render(1.0);
+ this.cancel();
+ this.event('beforeFinish');
+ if (this.finish) this.finish();
+ this.event('afterFinish');
+ return;
+ }
+ var pos = (timePos - this.startOn) / this.totalTime,
+ frame = (pos * this.totalFrames).round();
+ if (frame > this.currentFrame) {
+ this.render(pos);
+ this.currentFrame = frame;
+ }
+ }
+ },
+ cancel: function() {
+ if (!this.options.sync)
+ Effect.Queues.get(Object.isString(this.options.queue) ?
+ '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() {
+ var data = $H();
+ for(property in this)
+ if (!Object.isFunction(this[property])) data.set(property, this[property]);
+ return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
+ }
+});
+
+Effect.Parallel = Class.create(Effect.Base, {
+ 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.Tween = Class.create(Effect.Base, {
+ initialize: function(object, from, to) {
+ object = Object.isString(object) ? $(object) : object;
+ var args = $A(arguments), method = args.last(),
+ options = args.length == 5 ? args[3] : null;
+ this.method = Object.isFunction(method) ? method.bind(object) :
+ Object.isFunction(object[method]) ? object[method].bind(object) :
+ function(value) { object[method] = value };
+ this.start(Object.extend({ from: from, to: to }, options || { }));
+ },
+ update: function(position) {
+ this.method(position);
+ }
+});
+
+Effect.Event = Class.create(Effect.Base, {
+ initialize: function() {
+ this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
+ },
+ update: Prototype.emptyFunction
+});
+
+Effect.Opacity = Class.create(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ // make this work on IE on elements without 'layout'
+ if (Prototype.Browser.IE && (!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(Effect.Base, {
+ 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() {
+ this.element.makePositioned();
+ this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
+ this.originalTop = parseFloat(this.element.getStyle('top') || '0');
+ if (this.options.mode == 'absolute') {
+ this.options.x = this.options.x - this.originalLeft;
+ this.options.y = this.options.y - this.originalTop;
+ }
+ },
+ update: function(position) {
+ this.element.setStyle({
+ left: (this.options.x * position + this.originalLeft).round() + 'px',
+ top: (this.options.y * position + this.originalTop).round() + '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(Effect.Base, {
+ 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)>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 && 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 = width.round() + 'px';
+ if (this.options.scaleY) d.height = height.round() + '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(Effect.Base, {
+ 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 = { };
+ if (!this.options.keepBackgroundImage) {
+ 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+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
+ },
+ finish: function() {
+ this.element.setStyle(Object.extend(this.oldStyle, {
+ backgroundColor: this.options.restorecolor
+ }));
+ }
+});
+
+Effect.ScrollTo = function(element) {
+ var options = arguments[1] || { },
+ scrollOffsets = document.viewport.getScrollOffsets(),
+ elementOffsets = $(element).cumulativeOffset();
+
+ if (options.offset) elementOffsets[1] += options.offset;
+
+ return new Effect.Tween(null,
+ scrollOffsets.top,
+ elementOffsets[1],
+ options,
+ function(p){ scrollTo(scrollOffsets.left, p.round()); }
+ );
+};
+
+/* ------------- 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 options = Object.extend({
+ distance: 20,
+ duration: 0.5
+ }, arguments[1] || {});
+ var distance = parseFloat(options.distance);
+ var split = parseFloat(options.duration) / 10.0;
+ var oldStyle = {
+ top: element.getStyle('top'),
+ left: element.getStyle('left') };
+ return new Effect.Move(element,
+ { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
+ new Effect.Move(effect.element,
+ { x: -distance, y: 0, duration: split, 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');
+ var elementDimensions = element.getDimensions();
+ return new Effect.Scale(element, window.opera ? 0 : 1,
+ Object.extend({ scaleContent: false,
+ scaleX: false,
+ scaleMode: 'box',
+ scaleFrom: 100,
+ 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().show();
+ },
+ afterUpdateInternal: function(effect) {
+ effect.element.down().setStyle({bottom:
+ (effect.dims[0] - effect.element.clientHeight) + 'px' });
+ },
+ afterFinishInternal: function(effect) {
+ effect.element.hide().undoClipping().undoPositioned();
+ effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
+ }
+ }, 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] || { },
+ oldOpacity = element.getInlineOpacity(),
+ transition = options.transition || Effect.Transitions.linear,
+ reverser = function(pos){
+ return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5);
+ };
+
+ 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(Effect.Base, {
+ initialize: function(element) {
+ this.element = $(element);
+ if (!this.element) throw(Effect._elementDoesNotExistError);
+ var options = Object.extend({
+ style: { }
+ }, arguments[1] || { });
+
+ if (!Object.isString(options.style)) this.style = $H(options.style);
+ else {
+ if (options.style.include(':'))
+ this.style = options.style.parseStyle();
+ else {
+ this.element.addClassName(options.style);
+ this.style = $H(this.element.getStyles());
+ this.element.removeClassName(options.style);
+ var css = this.element.getStyles();
+ this.style = this.style.reject(function(style) {
+ return style.value == css[style.key];
+ });
+ options.afterFinishInternal = function(effect) {
+ effect.element.addClassName(effect.options.style);
+ effect.transforms.each(function(transform) {
+ effect.element.style[transform.style] = '';
+ });
+ };
+ }
+ }
+ 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.style.map(function(pair){
+ var property = pair[0], value = pair[1], unit = null;
+
+ if (value.parseColor('#zzzzzz') != '#zzzzzz') {
+ value = value.parseColor();
+ unit = 'color';
+ } else if (property == 'opacity') {
+ value = parseFloat(value);
+ if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
+ this.element.setStyle({zoom: 1});
+ } else if (Element.CSS_LENGTH.test(value)) {
+ var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
+ value = parseFloat(components[1]);
+ unit = (components.length == 3) ? components[2] : null;
+ }
+
+ var originalValue = this.element.getStyle(property);
+ return {
+ style: property.camelize(),
+ originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
+ targetValue: unit=='color' ? parseColor(value) : value,
+ unit: unit
+ };
+ }.bind(this)).reject(function(transform){
+ return (
+ (transform.originalValue == transform.targetValue) ||
+ (
+ transform.unit != 'color' &&
+ (isNaN(transform.originalValue) || isNaN(transform.targetValue))
+ )
+ );
+ });
+ },
+ update: function(position) {
+ var style = { }, transform, i = this.transforms.length;
+ while(i--)
+ style[(transform = this.transforms[i]).style] =
+ transform.unit=='color' ? '#'+
+ (Math.round(transform.originalValue[0]+
+ (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
+ (Math.round(transform.originalValue[1]+
+ (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
+ (Math.round(transform.originalValue[2]+
+ (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
+ (transform.originalValue +
+ (transform.targetValue - transform.originalValue) * position).toFixed(3) +
+ (transform.unit === null ? '' : transform.unit);
+ this.element.setStyle(style, true);
+ }
+});
+
+Effect.Transform = Class.create({
+ initialize: function(tracks){
+ this.tracks = [];
+ this.options = arguments[1] || { };
+ this.addTracks(tracks);
+ },
+ addTracks: function(tracks){
+ tracks.each(function(track){
+ track = $H(track);
+ var data = track.values().first();
+ this.tracks.push($H({
+ ids: 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 ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
+ var elements = [$(ids) || $$(ids)].flatten();
+ return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) });
+ }).flatten(),
+ this.options
+ );
+ }
+});
+
+Element.CSS_PROPERTIES = $w(
+ 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
+ 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
+ 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
+ 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
+ 'fontSize fontWeight height left letterSpacing lineHeight ' +
+ 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
+ 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
+ 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
+ 'right textIndent top width wordSpacing zIndex');
+
+Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
+
+String.__parseStyleElement = document.createElement('div');
+String.prototype.parseStyle = function(){
+ var style, styleRules = $H();
+ if (Prototype.Browser.WebKit)
+ style = new Element('div',{style:this}).style;
+ else {
+ String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
+ style = String.__parseStyleElement.childNodes[0].style;
+ }
+
+ Element.CSS_PROPERTIES.each(function(property){
+ if (style[property]) styleRules.set(property, style[property]);
+ });
+
+ if (Prototype.Browser.IE && this.include('opacity'))
+ styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);
+
+ return styleRules;
+};
+
+if (document.defaultView && document.defaultView.getComputedStyle) {
+ Element.getStyles = function(element) {
+ var css = document.defaultView.getComputedStyle($(element), null);
+ return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
+ styles[property] = css[property];
+ return styles;
+ });
+ };
+} else {
+ Element.getStyles = function(element) {
+ element = $(element);
+ var css = element.currentStyle, styles;
+ styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) {
+ results[property] = css[property];
+ return results;
+ });
+ if (!styles.opacity) styles.opacity = element.getOpacity();
+ return styles;
+ };
+}
+
+Effect.Methods = {
+ morph: function(element, style) {
+ element = $(element);
+ new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
+ return element;
+ },
+ visualEffect: function(element, effect, options) {
+ element = $(element);
+ var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
+ new Effect[klass](element, options);
+ return element;
+ },
+ highlight: function(element, options) {
+ element = $(element);
+ new Effect.Highlight(element, options);
+ return element;
+ }
+};
+
+$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
+ 'pulsate shake puff squish switchOff dropOut').each(
+ function(effect) {
+ Effect.Methods[effect] = function(element, options){
+ element = $(element);
+ Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
+ return element;
+ };
+ }
+);
+
+$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
+ function(f) { Effect.Methods[f] = Element[f]; }
+);
+
+Element.addMethods(Effect.Methods);
+\ No newline at end of file
(DIR) diff --git a/web/public/javascripts/lightbox.js b/web/public/javascripts/lightbox.js
@@ -0,0 +1,426 @@
+/*
+ Lightbox JS: Fullsize Image Overlays
+ by Lokesh Dhakar - http://www.huddletogether.com
+
+ For more information on this script, visit:
+ http://huddletogether.com/projects/lightbox/
+
+ Script featured on Dynamic Drive code library Jan 24th, 06':
+ http://www.dynamicdrive.com
+
+ Licensed under the Creative Commons Attribution 2.5 License - http://creativecommons.org/licenses/by/2.5/
+ (basically, do anything you want, just leave my name and link)
+
+ Table of Contents
+ -----------------
+ Configuration
+
+ Functions
+ - getPageScroll()
+ - getPageSize()
+ - pause()
+ - getKey()
+ - listenKey()
+ - showLightbox()
+ - hideLightbox()
+ - initLightbox()
+ - addLoadEvent()
+
+ Function Calls
+ - addLoadEvent(initLightbox)
+
+*/
+
+
+
+//
+// Configuration
+//
+
+// If you would like to use a custom loading image or close button reference them in the next two lines.
+var loadingImage = '/images/loading.gif';
+var closeButton = '/images/close.gif';
+
+
+
+
+
+//
+// getPageScroll()
+// Returns array with x,y page scroll values.
+// Core code from - quirksmode.org
+//
+function getPageScroll(){
+
+ var yScroll;
+
+ if (self.pageYOffset) {
+ yScroll = self.pageYOffset;
+ } else if (document.documentElement && document.documentElement.scrollTop){ // Explorer 6 Strict
+ yScroll = document.documentElement.scrollTop;
+ } else if (document.body) {// all other Explorers
+ yScroll = document.body.scrollTop;
+ }
+
+ arrayPageScroll = new Array('',yScroll)
+ return arrayPageScroll;
+}
+
+
+
+//
+// getPageSize()
+// Returns array with page width, height and window width, height
+// Core code from - quirksmode.org
+// Edit for Firefox by pHaez
+//
+function getPageSize(){
+
+ var xScroll, yScroll;
+
+ if (window.innerHeight && window.scrollMaxY) {
+ xScroll = document.body.scrollWidth;
+ yScroll = window.innerHeight + window.scrollMaxY;
+ } else if (document.body.scrollHeight > document.body.offsetHeight){ // all but Explorer Mac
+ xScroll = document.body.scrollWidth;
+ yScroll = document.body.scrollHeight;
+ } else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari
+ xScroll = document.body.offsetWidth;
+ yScroll = document.body.offsetHeight;
+ }
+
+ var windowWidth, windowHeight;
+ if (self.innerHeight) { // all except Explorer
+ windowWidth = self.innerWidth;
+ windowHeight = self.innerHeight;
+ } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode
+ windowWidth = document.documentElement.clientWidth;
+ windowHeight = document.documentElement.clientHeight;
+ } else if (document.body) { // other Explorers
+ windowWidth = document.body.clientWidth;
+ windowHeight = document.body.clientHeight;
+ }
+
+ // for small pages with total height less then height of the viewport
+ if(yScroll < windowHeight){
+ pageHeight = windowHeight;
+ } else {
+ pageHeight = yScroll;
+ }
+
+ // for small pages with total width less then width of the viewport
+ if(xScroll < windowWidth){
+ pageWidth = windowWidth;
+ } else {
+ pageWidth = xScroll;
+ }
+
+
+ arrayPageSize = new Array(pageWidth,pageHeight,windowWidth,windowHeight)
+ return arrayPageSize;
+}
+
+
+//
+// pause(numberMillis)
+// Pauses code execution for specified time. Uses busy code, not good.
+// Code from http://www.faqts.com/knowledge_base/view.phtml/aid/1602
+//
+function pause(numberMillis) {
+ var now = new Date();
+ var exitTime = now.getTime() + numberMillis;
+ while (true) {
+ now = new Date();
+ if (now.getTime() > exitTime)
+ return;
+ }
+}
+
+//
+// getKey(key)
+// Gets keycode. If 'x' is pressed then it hides the lightbox.
+//
+
+function getKey(e){
+ if (e == null) { // ie
+ keycode = event.keyCode;
+ } else { // mozilla
+ keycode = e.which;
+ }
+ key = String.fromCharCode(keycode).toLowerCase();
+
+ if(key == 'x'){ hideLightbox(); }
+}
+
+
+//
+// listenKey()
+//
+function listenKey () { document.onkeypress = getKey; }
+
+
+//
+// showLightbox()
+// Preloads images. Pleaces new image in lightbox then centers and displays.
+//
+function showLightbox(objLink)
+{
+ // prep objects
+ var objOverlay = document.getElementById('overlay');
+ var objLightbox = document.getElementById('lightbox');
+ var objCaption = document.getElementById('lightboxCaption');
+ var objImage = document.getElementById('lightboxImage');
+ var objLoadingImage = document.getElementById('loadingImage');
+ var objLightboxDetails = document.getElementById('lightboxDetails');
+
+
+ var arrayPageSize = getPageSize();
+ var arrayPageScroll = getPageScroll();
+
+ // center loadingImage if it exists
+ if (objLoadingImage) {
+ objLoadingImage.style.top = (arrayPageScroll[1] + ((arrayPageSize[3] - 35 - objLoadingImage.height) / 2) + 'px');
+ objLoadingImage.style.left = (((arrayPageSize[0] - 20 - objLoadingImage.width) / 2) + 'px');
+ objLoadingImage.style.display = 'block';
+ }
+
+ // set height of Overlay to take up whole page and show
+ objOverlay.style.height = (arrayPageSize[1] + 'px');
+ objOverlay.style.display = 'block';
+
+ // preload image
+ imgPreload = new Image();
+
+ imgPreload.onload=function(){
+ objImage.src = objLink.href;
+
+ // center lightbox and make sure that the top and left values are not negative
+ // and the image placed outside the viewport
+ var lightboxTop = arrayPageScroll[1] + ((arrayPageSize[3] - 35 - imgPreload.height) / 2);
+ var lightboxLeft = ((arrayPageSize[0] - 20 - imgPreload.width) / 2);
+
+ objLightbox.style.top = (lightboxTop < 0) ? "0px" : lightboxTop + "px";
+ objLightbox.style.left = (lightboxLeft < 0) ? "0px" : lightboxLeft + "px";
+
+
+ objLightboxDetails.style.width = imgPreload.width + 'px';
+
+ if(objLink.getAttribute('title')){
+ objCaption.style.display = 'block';
+ //objCaption.style.width = imgPreload.width + 'px';
+ objCaption.innerHTML = objLink.getAttribute('title');
+ } else {
+ objCaption.style.display = 'none';
+ }
+
+ // A small pause between the image loading and displaying is required with IE,
+ // this prevents the previous image displaying for a short burst causing flicker.
+ if (navigator.appVersion.indexOf("MSIE")!=-1){
+ pause(250);
+ }
+
+ if (objLoadingImage) { objLoadingImage.style.display = 'none'; }
+ objLightbox.style.display = 'block';
+
+ // After image is loaded, update the overlay height as the new image might have
+ // increased the overall page height.
+ arrayPageSize = getPageSize();
+ objOverlay.style.height = (arrayPageSize[1] + 'px');
+
+ // Check for 'x' keypress
+ listenKey();
+
+ return false;
+ }
+
+ imgPreload.src = objLink.href;
+
+}
+
+
+
+
+
+//
+// hideLightbox()
+//
+function hideLightbox()
+{
+ // get objects
+ objOverlay = document.getElementById('overlay');
+ objLightbox = document.getElementById('lightbox');
+
+ // hide lightbox and overlay
+ objOverlay.style.display = 'none';
+ objLightbox.style.display = 'none';
+
+ // disable keypress listener
+ document.onkeypress = '';
+}
+
+
+
+
+//
+// initLightbox()
+// Function runs on window load, going through link tags looking for rel="lightbox".
+// These links receive onclick events that enable the lightbox display for their targets.
+// The function also inserts html markup at the top of the page which will be used as a
+// container for the overlay pattern and the inline image.
+//
+function initLightbox()
+{
+
+ if (!document.getElementsByTagName){ return; }
+ var anchors = document.getElementsByTagName("a");
+
+ // loop through all anchor tags
+ for (var i=0; i<anchors.length; i++){
+ var anchor = anchors[i];
+
+ if (anchor.getAttribute("href") && (anchor.getAttribute("rel") == "lightbox")){
+ anchor.onclick = function () {showLightbox(this); return false;}
+ }
+ }
+
+ // the rest of this code inserts html at the top of the page that looks like this:
+ //
+ // <div id="overlay">
+ // <a href="#" onclick="hideLightbox(); return false;"><img id="loadingImage" /></a>
+ // </div>
+ // <div id="lightbox">
+ // <a href="#" onclick="hideLightbox(); return false;" title="Click anywhere to close image">
+ // <img id="closeButton" />
+ // <img id="lightboxImage" />
+ // </a>
+ // <div id="lightboxDetails">
+ // <div id="lightboxCaption"></div>
+ // <div id="keyboardMsg"></div>
+ // </div>
+ // </div>
+
+ var objBody = document.getElementsByTagName("body").item(0);
+
+ // create overlay div and hardcode some functional styles (aesthetic styles are in CSS file)
+ var objOverlay = document.createElement("div");
+ objOverlay.setAttribute('id','overlay');
+ objOverlay.onclick = function () {hideLightbox(); return false;}
+ objOverlay.style.display = 'none';
+ objOverlay.style.position = 'absolute';
+ objOverlay.style.top = '0';
+ objOverlay.style.left = '0';
+ objOverlay.style.zIndex = '90';
+ objOverlay.style.width = '100%';
+ objBody.insertBefore(objOverlay, objBody.firstChild);
+
+ var arrayPageSize = getPageSize();
+ var arrayPageScroll = getPageScroll();
+
+ // preload and create loader image
+ var imgPreloader = new Image();
+
+ // if loader image found, create link to hide lightbox and create loadingimage
+ imgPreloader.onload=function(){
+
+ var objLoadingImageLink = document.createElement("a");
+ objLoadingImageLink.setAttribute('href','#');
+ objLoadingImageLink.onclick = function () {hideLightbox(); return false;}
+ objOverlay.appendChild(objLoadingImageLink);
+
+ var objLoadingImage = document.createElement("img");
+ objLoadingImage.src = loadingImage;
+ objLoadingImage.setAttribute('id','loadingImage');
+ objLoadingImage.style.position = 'absolute';
+ objLoadingImage.style.zIndex = '150';
+ objLoadingImageLink.appendChild(objLoadingImage);
+
+ imgPreloader.onload=function(){}; // clear onLoad, as IE will flip out w/animated gifs
+
+ return false;
+ }
+
+ imgPreloader.src = loadingImage;
+
+ // create lightbox div, same note about styles as above
+ var objLightbox = document.createElement("div");
+ objLightbox.setAttribute('id','lightbox');
+ objLightbox.style.display = 'none';
+ objLightbox.style.position = 'absolute';
+ objLightbox.style.zIndex = '100';
+ objBody.insertBefore(objLightbox, objOverlay.nextSibling);
+
+ // create link
+ var objLink = document.createElement("a");
+ objLink.setAttribute('href','#');
+ objLink.setAttribute('title','Click to close');
+ objLink.onclick = function () {hideLightbox(); return false;}
+ objLightbox.appendChild(objLink);
+
+ // preload and create close button image
+ var imgPreloadCloseButton = new Image();
+
+ // if close button image found,
+ imgPreloadCloseButton.onload=function(){
+
+ var objCloseButton = document.createElement("img");
+ objCloseButton.src = closeButton;
+ objCloseButton.setAttribute('id','closeButton');
+ objCloseButton.style.position = 'absolute';
+ objCloseButton.style.zIndex = '200';
+ objLink.appendChild(objCloseButton);
+
+ return false;
+ }
+
+ imgPreloadCloseButton.src = closeButton;
+
+ // create image
+ var objImage = document.createElement("img");
+ objImage.setAttribute('id','lightboxImage');
+ objLink.appendChild(objImage);
+
+ // create details div, a container for the caption and keyboard message
+ var objLightboxDetails = document.createElement("div");
+ objLightboxDetails.setAttribute('id','lightboxDetails');
+ objLightbox.appendChild(objLightboxDetails);
+
+ // create caption
+ var objCaption = document.createElement("div");
+ objCaption.setAttribute('id','lightboxCaption');
+ objCaption.style.display = 'none';
+ objLightboxDetails.appendChild(objCaption);
+
+ // create keyboard message
+ var objKeyboardMsg = document.createElement("div");
+ objKeyboardMsg.setAttribute('id','keyboardMsg');
+ objKeyboardMsg.innerHTML = 'press <kbd>x</kbd> to close';
+ objLightboxDetails.appendChild(objKeyboardMsg);
+
+
+}
+
+
+
+
+//
+// addLoadEvent()
+// Adds event to window.onload without overwriting currently assigned onload functions.
+// Function found at Simon Willison's weblog - http://simon.incutio.com/
+//
+function addLoadEvent(func)
+{
+ var oldonload = window.onload;
+ if (typeof window.onload != 'function'){
+ window.onload = func;
+ } else {
+ window.onload = function(){
+ oldonload();
+ func();
+ }
+ }
+
+}
+
+
+
+addLoadEvent(initLightbox); // run initLightbox onLoad
(DIR) diff --git a/web/public/javascripts/prototype.js b/web/public/javascripts/prototype.js
@@ -0,0 +1,6001 @@
+/* Prototype JavaScript framework, version 1.7_rc2
+ * (c) 2005-2010 Sam Stephenson
+ *
+ * Prototype is freely distributable under the terms of an MIT-style license.
+ * For details, see the Prototype web site: http://www.prototypejs.org/
+ *
+ *--------------------------------------------------------------------------*/
+
+var Prototype = {
+
+ Version: '1.7_rc2',
+
+ Browser: (function(){
+ var ua = navigator.userAgent;
+ var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]';
+ return {
+ IE: !!window.attachEvent && !isOpera,
+ Opera: isOpera,
+ WebKit: ua.indexOf('AppleWebKit/') > -1,
+ Gecko: ua.indexOf('Gecko') > -1 && ua.indexOf('KHTML') === -1,
+ MobileSafari: /Apple.*Mobile/.test(ua)
+ }
+ })(),
+
+ BrowserFeatures: {
+ XPath: !!document.evaluate,
+
+ SelectorsAPI: !!document.querySelector,
+
+ ElementExtensions: (function() {
+ var constructor = window.Element || window.HTMLElement;
+ return !!(constructor && constructor.prototype);
+ })(),
+ SpecificElementExtensions: (function() {
+ if (typeof window.HTMLDivElement !== 'undefined')
+ return true;
+
+ var div = document.createElement('div'),
+ form = document.createElement('form'),
+ isSupported = false;
+
+ if (div['__proto__'] && (div['__proto__'] !== form['__proto__'])) {
+ isSupported = true;
+ }
+
+ div = form = null;
+
+ return isSupported;
+ })()
+ },
+
+ ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
+ JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,
+
+ emptyFunction: function() { },
+
+ K: function(x) { return x }
+};
+
+if (Prototype.Browser.MobileSafari)
+ Prototype.BrowserFeatures.SpecificElementExtensions = false;
+
+
+var Abstract = { };
+
+
+var Try = {
+ these: function() {
+ var returnValue;
+
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ var lambda = arguments[i];
+ try {
+ returnValue = lambda();
+ break;
+ } catch (e) { }
+ }
+
+ return returnValue;
+ }
+};
+
+/* Based on Alex Arnell's inheritance implementation. */
+
+var Class = (function() {
+
+ var IS_DONTENUM_BUGGY = (function(){
+ for (var p in { toString: 1 }) {
+ if (p === 'toString') return false;
+ }
+ return true;
+ })();
+
+ function subclass() {};
+ function create() {
+ var parent = null, properties = $A(arguments);
+ if (Object.isFunction(properties[0]))
+ parent = properties.shift();
+
+ function klass() {
+ this.initialize.apply(this, arguments);
+ }
+
+ Object.extend(klass, Class.Methods);
+ klass.superclass = parent;
+ klass.subclasses = [];
+
+ if (parent) {
+ subclass.prototype = parent.prototype;
+ klass.prototype = new subclass;
+ parent.subclasses.push(klass);
+ }
+
+ for (var i = 0, length = properties.length; i < length; i++)
+ klass.addMethods(properties[i]);
+
+ if (!klass.prototype.initialize)
+ klass.prototype.initialize = Prototype.emptyFunction;
+
+ klass.prototype.constructor = klass;
+ return klass;
+ }
+
+ function addMethods(source) {
+ var ancestor = this.superclass && this.superclass.prototype,
+ properties = Object.keys(source);
+
+ if (IS_DONTENUM_BUGGY) {
+ if (source.toString != Object.prototype.toString)
+ properties.push("toString");
+ if (source.valueOf != Object.prototype.valueOf)
+ properties.push("valueOf");
+ }
+
+ for (var i = 0, length = properties.length; i < length; i++) {
+ var property = properties[i], value = source[property];
+ if (ancestor && Object.isFunction(value) &&
+ value.argumentNames()[0] == "$super") {
+ var method = value;
+ value = (function(m) {
+ return function() { return ancestor[m].apply(this, arguments); };
+ })(property).wrap(method);
+
+ value.valueOf = method.valueOf.bind(method);
+ value.toString = method.toString.bind(method);
+ }
+ this.prototype[property] = value;
+ }
+
+ return this;
+ }
+
+ return {
+ create: create,
+ Methods: {
+ addMethods: addMethods
+ }
+ };
+})();
+(function() {
+
+ var _toString = Object.prototype.toString,
+ NULL_TYPE = 'Null',
+ UNDEFINED_TYPE = 'Undefined',
+ BOOLEAN_TYPE = 'Boolean',
+ NUMBER_TYPE = 'Number',
+ STRING_TYPE = 'String',
+ OBJECT_TYPE = 'Object',
+ BOOLEAN_CLASS = '[object Boolean]',
+ NUMBER_CLASS = '[object Number]',
+ STRING_CLASS = '[object String]',
+ ARRAY_CLASS = '[object Array]',
+ NATIVE_JSON_STRINGIFY_SUPPORT = window.JSON &&
+ typeof JSON.stringify === 'function' &&
+ JSON.stringify(0) === '0' &&
+ typeof JSON.stringify(Prototype.K) === 'undefined';
+
+ function Type(o) {
+ switch(o) {
+ case null: return NULL_TYPE;
+ case (void 0): return UNDEFINED_TYPE;
+ }
+ var type = typeof o;
+ switch(type) {
+ case 'boolean': return BOOLEAN_TYPE;
+ case 'number': return NUMBER_TYPE;
+ case 'string': return STRING_TYPE;
+ }
+ return OBJECT_TYPE;
+ }
+
+ function extend(destination, source) {
+ for (var property in source)
+ destination[property] = source[property];
+ return destination;
+ }
+
+ function inspect(object) {
+ try {
+ if (isUndefined(object)) return 'undefined';
+ if (object === null) return 'null';
+ return object.inspect ? object.inspect() : String(object);
+ } catch (e) {
+ if (e instanceof RangeError) return '...';
+ throw e;
+ }
+ }
+
+ function toJSON(value) {
+ return Str('', { '': value }, []);
+ }
+
+ function Str(key, holder, stack) {
+ var value = holder[key],
+ type = typeof value;
+
+ if (Type(value) === OBJECT_TYPE && typeof value.toJSON === 'function') {
+ value = value.toJSON(key);
+ }
+
+ var _class = _toString.call(value);
+
+ switch (_class) {
+ case NUMBER_CLASS:
+ case BOOLEAN_CLASS:
+ case STRING_CLASS:
+ value = value.valueOf();
+ }
+
+ switch (value) {
+ case null: return 'null';
+ case true: return 'true';
+ case false: return 'false';
+ }
+
+ type = typeof value;
+ switch (type) {
+ case 'string':
+ return value.inspect(true);
+ case 'number':
+ return isFinite(value) ? String(value) : 'null';
+ case 'object':
+
+ for (var i = 0, length = stack.length; i < length; i++) {
+ if (stack[i] === value) { throw new TypeError(); }
+ }
+ stack.push(value);
+
+ var partial = [];
+ if (_class === ARRAY_CLASS) {
+ for (var i = 0, length = value.length; i < length; i++) {
+ var str = Str(i, value, stack);
+ partial.push(typeof str === 'undefined' ? 'null' : str);
+ }
+ partial = '[' + partial.join(',') + ']';
+ } else {
+ var keys = Object.keys(value);
+ for (var i = 0, length = keys.length; i < length; i++) {
+ var key = keys[i], str = Str(key, value, stack);
+ if (typeof str !== "undefined") {
+ partial.push(key.inspect(true)+ ':' + str);
+ }
+ }
+ partial = '{' + partial.join(',') + '}';
+ }
+ stack.pop();
+ return partial;
+ }
+ }
+
+ function stringify(object) {
+ return JSON.stringify(object);
+ }
+
+ function toQueryString(object) {
+ return $H(object).toQueryString();
+ }
+
+ function toHTML(object) {
+ return object && object.toHTML ? object.toHTML() : String.interpret(object);
+ }
+
+ function keys(object) {
+ if (Type(object) !== OBJECT_TYPE) { throw new TypeError(); }
+ var results = [];
+ for (var property in object) {
+ if (object.hasOwnProperty(property)) {
+ results.push(property);
+ }
+ }
+ return results;
+ }
+
+ function values(object) {
+ var results = [];
+ for (var property in object)
+ results.push(object[property]);
+ return results;
+ }
+
+ function clone(object) {
+ return extend({ }, object);
+ }
+
+ function isElement(object) {
+ return !!(object && object.nodeType == 1);
+ }
+
+ function isArray(object) {
+ return _toString.call(object) === ARRAY_CLASS;
+ }
+
+ var hasNativeIsArray = (typeof Array.isArray == 'function')
+ && Array.isArray([]) && !Array.isArray({});
+
+ if (hasNativeIsArray) {
+ isArray = Array.isArray;
+ }
+
+ function isHash(object) {
+ return object instanceof Hash;
+ }
+
+ function isFunction(object) {
+ return typeof object === "function";
+ }
+
+ function isString(object) {
+ return _toString.call(object) === STRING_CLASS;
+ }
+
+ function isNumber(object) {
+ return _toString.call(object) === NUMBER_CLASS;
+ }
+
+ function isUndefined(object) {
+ return typeof object === "undefined";
+ }
+
+ extend(Object, {
+ extend: extend,
+ inspect: inspect,
+ toJSON: NATIVE_JSON_STRINGIFY_SUPPORT ? stringify : toJSON,
+ toQueryString: toQueryString,
+ toHTML: toHTML,
+ keys: Object.keys || keys,
+ values: values,
+ clone: clone,
+ isElement: isElement,
+ isArray: isArray,
+ isHash: isHash,
+ isFunction: isFunction,
+ isString: isString,
+ isNumber: isNumber,
+ isUndefined: isUndefined
+ });
+})();
+Object.extend(Function.prototype, (function() {
+ var slice = Array.prototype.slice;
+
+ function update(array, args) {
+ var arrayLength = array.length, length = args.length;
+ while (length--) array[arrayLength + length] = args[length];
+ return array;
+ }
+
+ function merge(array, args) {
+ array = slice.call(array, 0);
+ return update(array, args);
+ }
+
+ function argumentNames() {
+ var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1]
+ .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '')
+ .replace(/\s+/g, '').split(',');
+ return names.length == 1 && !names[0] ? [] : names;
+ }
+
+ function bind(context) {
+ if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
+ var __method = this, args = slice.call(arguments, 1);
+ return function() {
+ var a = merge(args, arguments);
+ return __method.apply(context, a);
+ }
+ }
+
+ function bindAsEventListener(context) {
+ var __method = this, args = slice.call(arguments, 1);
+ return function(event) {
+ var a = update([event || window.event], args);
+ return __method.apply(context, a);
+ }
+ }
+
+ function curry() {
+ if (!arguments.length) return this;
+ var __method = this, args = slice.call(arguments, 0);
+ return function() {
+ var a = merge(args, arguments);
+ return __method.apply(this, a);
+ }
+ }
+
+ function delay(timeout) {
+ var __method = this, args = slice.call(arguments, 1);
+ timeout = timeout * 1000;
+ return window.setTimeout(function() {
+ return __method.apply(__method, args);
+ }, timeout);
+ }
+
+ function defer() {
+ var args = update([0.01], arguments);
+ return this.delay.apply(this, args);
+ }
+
+ function wrap(wrapper) {
+ var __method = this;
+ return function() {
+ var a = update([__method.bind(this)], arguments);
+ return wrapper.apply(this, a);
+ }
+ }
+
+ function methodize() {
+ if (this._methodized) return this._methodized;
+ var __method = this;
+ return this._methodized = function() {
+ var a = update([this], arguments);
+ return __method.apply(null, a);
+ };
+ }
+
+ return {
+ argumentNames: argumentNames,
+ bind: bind,
+ bindAsEventListener: bindAsEventListener,
+ curry: curry,
+ delay: delay,
+ defer: defer,
+ wrap: wrap,
+ methodize: methodize
+ }
+})());
+
+
+
+(function(proto) {
+
+
+ function toISOString() {
+ return this.getUTCFullYear() + '-' +
+ (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
+ this.getUTCDate().toPaddedString(2) + 'T' +
+ this.getUTCHours().toPaddedString(2) + ':' +
+ this.getUTCMinutes().toPaddedString(2) + ':' +
+ this.getUTCSeconds().toPaddedString(2) + 'Z';
+ }
+
+
+ function toJSON() {
+ return this.toISOString();
+ }
+
+ if (!proto.toISOString) proto.toISOString = toISOString;
+ if (!proto.toJSON) proto.toJSON = toJSON;
+
+})(Date.prototype);
+
+
+RegExp.prototype.match = RegExp.prototype.test;
+
+RegExp.escape = function(str) {
+ return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
+};
+var PeriodicalExecuter = Class.create({
+ 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);
+ },
+
+ execute: function() {
+ this.callback(this);
+ },
+
+ stop: function() {
+ if (!this.timer) return;
+ clearInterval(this.timer);
+ this.timer = null;
+ },
+
+ onTimerEvent: function() {
+ if (!this.currentlyExecuting) {
+ try {
+ this.currentlyExecuting = true;
+ this.execute();
+ this.currentlyExecuting = false;
+ } catch(e) {
+ this.currentlyExecuting = false;
+ throw e;
+ }
+ }
+ }
+});
+Object.extend(String, {
+ interpret: function(value) {
+ return value == null ? '' : String(value);
+ },
+ specialChar: {
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '\\': '\\\\'
+ }
+});
+
+Object.extend(String.prototype, (function() {
+ var NATIVE_JSON_PARSE_SUPPORT = window.JSON &&
+ typeof JSON.parse === 'function' &&
+ JSON.parse('{"test": true}').test;
+
+ function prepareReplacement(replacement) {
+ if (Object.isFunction(replacement)) return replacement;
+ var template = new Template(replacement);
+ return function(match) { return template.evaluate(match) };
+ }
+
+ function gsub(pattern, replacement) {
+ var result = '', source = this, match;
+ replacement = prepareReplacement(replacement);
+
+ if (Object.isString(pattern))
+ pattern = RegExp.escape(pattern);
+
+ if (!(pattern.length || pattern.source)) {
+ replacement = replacement('');
+ return replacement + source.split('').join(replacement) + replacement;
+ }
+
+ while (source.length > 0) {
+ if (match = source.match(pattern)) {
+ result += source.slice(0, match.index);
+ result += String.interpret(replacement(match));
+ source = source.slice(match.index + match[0].length);
+ } else {
+ result += source, source = '';
+ }
+ }
+ return result;
+ }
+
+ function sub(pattern, replacement, count) {
+ replacement = prepareReplacement(replacement);
+ count = Object.isUndefined(count) ? 1 : count;
+
+ return this.gsub(pattern, function(match) {
+ if (--count < 0) return match[0];
+ return replacement(match);
+ });
+ }
+
+ function scan(pattern, iterator) {
+ this.gsub(pattern, iterator);
+ return String(this);
+ }
+
+ function truncate(length, truncation) {
+ length = length || 30;
+ truncation = Object.isUndefined(truncation) ? '...' : truncation;
+ return this.length > length ?
+ this.slice(0, length - truncation.length) + truncation : String(this);
+ }
+
+ function strip() {
+ return this.replace(/^\s+/, '').replace(/\s+$/, '');
+ }
+
+ function stripTags() {
+ return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi, '');
+ }
+
+ function stripScripts() {
+ return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
+ }
+
+ function extractScripts() {
+ var matchAll = new RegExp(Prototype.ScriptFragment, 'img'),
+ matchOne = new RegExp(Prototype.ScriptFragment, 'im');
+ return (this.match(matchAll) || []).map(function(scriptTag) {
+ return (scriptTag.match(matchOne) || ['', ''])[1];
+ });
+ }
+
+ function evalScripts() {
+ return this.extractScripts().map(function(script) { return eval(script) });
+ }
+
+ function escapeHTML() {
+ return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
+ }
+
+ function unescapeHTML() {
+ return this.stripTags().replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&');
+ }
+
+
+ function toQueryParams(separator) {
+ var match = this.strip().match(/([^?#]*)(#.*)?$/);
+ if (!match) return { };
+
+ return match[1].split(separator || '&').inject({ }, function(hash, pair) {
+ if ((pair = pair.split('='))[0]) {
+ var key = decodeURIComponent(pair.shift()),
+ value = pair.length > 1 ? pair.join('=') : pair[0];
+
+ if (value != undefined) value = decodeURIComponent(value);
+
+ if (key in hash) {
+ if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
+ hash[key].push(value);
+ }
+ else hash[key] = value;
+ }
+ return hash;
+ });
+ }
+
+ function toArray() {
+ return this.split('');
+ }
+
+ function succ() {
+ return this.slice(0, this.length - 1) +
+ String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
+ }
+
+ function times(count) {
+ return count < 1 ? '' : new Array(count + 1).join(this);
+ }
+
+ function camelize() {
+ return this.replace(/-+(.)?/g, function(match, chr) {
+ return chr ? chr.toUpperCase() : '';
+ });
+ }
+
+ function capitalize() {
+ return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
+ }
+
+ function underscore() {
+ return this.replace(/::/g, '/')
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
+ .replace(/([a-z\d])([A-Z])/g, '$1_$2')
+ .replace(/-/g, '_')
+ .toLowerCase();
+ }
+
+ function dasherize() {
+ return this.replace(/_/g, '-');
+ }
+
+ function inspect(useDoubleQuotes) {
+ var escapedString = this.replace(/[\x00-\x1f\\]/g, function(character) {
+ if (character in String.specialChar) {
+ return String.specialChar[character];
+ }
+ return '\\u00' + character.charCodeAt().toPaddedString(2, 16);
+ });
+ if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
+ return "'" + escapedString.replace(/'/g, '\\\'') + "'";
+ }
+
+ function unfilterJSON(filter) {
+ return this.replace(filter || Prototype.JSONFilter, '$1');
+ }
+
+ function isJSON() {
+ var str = this;
+ if (str.blank()) return false;
+ str = str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@');
+ str = str.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']');
+ str = str.replace(/(?:^|:|,)(?:\s*\[)+/g, '');
+ return (/^[\],:{}\s]*$/).test(str);
+ }
+
+ function evalJSON(sanitize) {
+ var json = this.unfilterJSON(),
+ cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
+ if (cx.test(json)) {
+ json = json.replace(cx, function (a) {
+ return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ });
+ }
+ try {
+ if (!sanitize || json.isJSON()) return eval('(' + json + ')');
+ } catch (e) { }
+ throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
+ }
+
+ function parseJSON() {
+ var json = this.unfilterJSON();
+ return JSON.parse(json);
+ }
+
+ function include(pattern) {
+ return this.indexOf(pattern) > -1;
+ }
+
+ function startsWith(pattern) {
+ return this.lastIndexOf(pattern, 0) === 0;
+ }
+
+ function endsWith(pattern) {
+ var d = this.length - pattern.length;
+ return d >= 0 && this.indexOf(pattern, d) === d;
+ }
+
+ function empty() {
+ return this == '';
+ }
+
+ function blank() {
+ return /^\s*$/.test(this);
+ }
+
+ function interpolate(object, pattern) {
+ return new Template(this, pattern).evaluate(object);
+ }
+
+ return {
+ gsub: gsub,
+ sub: sub,
+ scan: scan,
+ truncate: truncate,
+ strip: String.prototype.trim || strip,
+ stripTags: stripTags,
+ stripScripts: stripScripts,
+ extractScripts: extractScripts,
+ evalScripts: evalScripts,
+ escapeHTML: escapeHTML,
+ unescapeHTML: unescapeHTML,
+ toQueryParams: toQueryParams,
+ parseQuery: toQueryParams,
+ toArray: toArray,
+ succ: succ,
+ times: times,
+ camelize: camelize,
+ capitalize: capitalize,
+ underscore: underscore,
+ dasherize: dasherize,
+ inspect: inspect,
+ unfilterJSON: unfilterJSON,
+ isJSON: isJSON,
+ evalJSON: NATIVE_JSON_PARSE_SUPPORT ? parseJSON : evalJSON,
+ include: include,
+ startsWith: startsWith,
+ endsWith: endsWith,
+ empty: empty,
+ blank: blank,
+ interpolate: interpolate
+ };
+})());
+
+var Template = Class.create({
+ initialize: function(template, pattern) {
+ this.template = template.toString();
+ this.pattern = pattern || Template.Pattern;
+ },
+
+ evaluate: function(object) {
+ if (object && Object.isFunction(object.toTemplateReplacements))
+ object = object.toTemplateReplacements();
+
+ return this.template.gsub(this.pattern, function(match) {
+ if (object == null) return (match[1] + '');
+
+ var before = match[1] || '';
+ if (before == '\\') return match[2];
+
+ var ctx = object, expr = match[3],
+ pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
+
+ match = pattern.exec(expr);
+ if (match == null) return before;
+
+ while (match != null) {
+ var comp = match[1].startsWith('[') ? match[2].replace(/\\\\]/g, ']') : match[1];
+ ctx = ctx[comp];
+ if (null == ctx || '' == match[3]) break;
+ expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
+ match = pattern.exec(expr);
+ }
+
+ return before + String.interpret(ctx);
+ });
+ }
+});
+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;
+
+var $break = { };
+
+var Enumerable = (function() {
+ function each(iterator, context) {
+ var index = 0;
+ try {
+ this._each(function(value) {
+ iterator.call(context, value, index++);
+ });
+ } catch (e) {
+ if (e != $break) throw e;
+ }
+ return this;
+ }
+
+ function eachSlice(number, iterator, context) {
+ var index = -number, slices = [], array = this.toArray();
+ if (number < 1) return array;
+ while ((index += number) < array.length)
+ slices.push(array.slice(index, index+number));
+ return slices.collect(iterator, context);
+ }
+
+ function all(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var result = true;
+ this.each(function(value, index) {
+ result = result && !!iterator.call(context, value, index);
+ if (!result) throw $break;
+ });
+ return result;
+ }
+
+ function any(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var result = false;
+ this.each(function(value, index) {
+ if (result = !!iterator.call(context, value, index))
+ throw $break;
+ });
+ return result;
+ }
+
+ function collect(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var results = [];
+ this.each(function(value, index) {
+ results.push(iterator.call(context, value, index));
+ });
+ return results;
+ }
+
+ function detect(iterator, context) {
+ var result;
+ this.each(function(value, index) {
+ if (iterator.call(context, value, index)) {
+ result = value;
+ throw $break;
+ }
+ });
+ return result;
+ }
+
+ function findAll(iterator, context) {
+ var results = [];
+ this.each(function(value, index) {
+ if (iterator.call(context, value, index))
+ results.push(value);
+ });
+ return results;
+ }
+
+ function grep(filter, iterator, context) {
+ iterator = iterator || Prototype.K;
+ var results = [];
+
+ if (Object.isString(filter))
+ filter = new RegExp(RegExp.escape(filter));
+
+ this.each(function(value, index) {
+ if (filter.match(value))
+ results.push(iterator.call(context, value, index));
+ });
+ return results;
+ }
+
+ function include(object) {
+ if (Object.isFunction(this.indexOf))
+ if (this.indexOf(object) != -1) return true;
+
+ var found = false;
+ this.each(function(value) {
+ if (value == object) {
+ found = true;
+ throw $break;
+ }
+ });
+ return found;
+ }
+
+ function inGroupsOf(number, fillWith) {
+ fillWith = Object.isUndefined(fillWith) ? null : fillWith;
+ return this.eachSlice(number, function(slice) {
+ while(slice.length < number) slice.push(fillWith);
+ return slice;
+ });
+ }
+
+ function inject(memo, iterator, context) {
+ this.each(function(value, index) {
+ memo = iterator.call(context, memo, value, index);
+ });
+ return memo;
+ }
+
+ function invoke(method) {
+ var args = $A(arguments).slice(1);
+ return this.map(function(value) {
+ return value[method].apply(value, args);
+ });
+ }
+
+ function max(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var result;
+ this.each(function(value, index) {
+ value = iterator.call(context, value, index);
+ if (result == null || value >= result)
+ result = value;
+ });
+ return result;
+ }
+
+ function min(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var result;
+ this.each(function(value, index) {
+ value = iterator.call(context, value, index);
+ if (result == null || value < result)
+ result = value;
+ });
+ return result;
+ }
+
+ function partition(iterator, context) {
+ iterator = iterator || Prototype.K;
+ var trues = [], falses = [];
+ this.each(function(value, index) {
+ (iterator.call(context, value, index) ?
+ trues : falses).push(value);
+ });
+ return [trues, falses];
+ }
+
+ function pluck(property) {
+ var results = [];
+ this.each(function(value) {
+ results.push(value[property]);
+ });
+ return results;
+ }
+
+ function reject(iterator, context) {
+ var results = [];
+ this.each(function(value, index) {
+ if (!iterator.call(context, value, index))
+ results.push(value);
+ });
+ return results;
+ }
+
+ function sortBy(iterator, context) {
+ return this.map(function(value, index) {
+ return {
+ value: value,
+ criteria: iterator.call(context, value, index)
+ };
+ }).sort(function(left, right) {
+ var a = left.criteria, b = right.criteria;
+ return a < b ? -1 : a > b ? 1 : 0;
+ }).pluck('value');
+ }
+
+ function toArray() {
+ return this.map();
+ }
+
+ function zip() {
+ var iterator = Prototype.K, args = $A(arguments);
+ if (Object.isFunction(args.last()))
+ iterator = args.pop();
+
+ var collections = [this].concat(args).map($A);
+ return this.map(function(value, index) {
+ return iterator(collections.pluck(index));
+ });
+ }
+
+ function size() {
+ return this.toArray().length;
+ }
+
+ function inspect() {
+ return '#<Enumerable:' + this.toArray().inspect() + '>';
+ }
+
+
+
+
+
+
+
+
+
+ return {
+ each: each,
+ eachSlice: eachSlice,
+ all: all,
+ every: all,
+ any: any,
+ some: any,
+ collect: collect,
+ map: collect,
+ detect: detect,
+ findAll: findAll,
+ select: findAll,
+ filter: findAll,
+ grep: grep,
+ include: include,
+ member: include,
+ inGroupsOf: inGroupsOf,
+ inject: inject,
+ invoke: invoke,
+ max: max,
+ min: min,
+ partition: partition,
+ pluck: pluck,
+ reject: reject,
+ sortBy: sortBy,
+ toArray: toArray,
+ entries: toArray,
+ zip: zip,
+ size: size,
+ inspect: inspect,
+ find: detect
+ };
+})();
+
+function $A(iterable) {
+ if (!iterable) return [];
+ if ('toArray' in Object(iterable)) return iterable.toArray();
+ var length = iterable.length || 0, results = new Array(length);
+ while (length--) results[length] = iterable[length];
+ return results;
+}
+
+
+function $w(string) {
+ if (!Object.isString(string)) return [];
+ string = string.strip();
+ return string ? string.split(/\s+/) : [];
+}
+
+Array.from = $A;
+
+
+(function() {
+ var arrayProto = Array.prototype,
+ slice = arrayProto.slice,
+ _each = arrayProto.forEach; // use native browser JS 1.6 implementation if available
+
+ function each(iterator) {
+ for (var i = 0, length = this.length; i < length; i++)
+ iterator(this[i]);
+ }
+ if (!_each) _each = each;
+
+ function clear() {
+ this.length = 0;
+ return this;
+ }
+
+ function first() {
+ return this[0];
+ }
+
+ function last() {
+ return this[this.length - 1];
+ }
+
+ function compact() {
+ return this.select(function(value) {
+ return value != null;
+ });
+ }
+
+ function flatten() {
+ return this.inject([], function(array, value) {
+ if (Object.isArray(value))
+ return array.concat(value.flatten());
+ array.push(value);
+ return array;
+ });
+ }
+
+ function without() {
+ var values = slice.call(arguments, 0);
+ return this.select(function(value) {
+ return !values.include(value);
+ });
+ }
+
+ function reverse(inline) {
+ return (inline === false ? this.toArray() : this)._reverse();
+ }
+
+ function uniq(sorted) {
+ return this.inject([], function(array, value, index) {
+ if (0 == index || (sorted ? array.last() != value : !array.include(value)))
+ array.push(value);
+ return array;
+ });
+ }
+
+ function intersect(array) {
+ return this.uniq().findAll(function(item) {
+ return array.detect(function(value) { return item === value });
+ });
+ }
+
+
+ function clone() {
+ return slice.call(this, 0);
+ }
+
+ function size() {
+ return this.length;
+ }
+
+ function inspect() {
+ return '[' + this.map(Object.inspect).join(', ') + ']';
+ }
+
+ function indexOf(item, i) {
+ i || (i = 0);
+ var length = this.length;
+ if (i < 0) i = length + i;
+ for (; i < length; i++)
+ if (this[i] === item) return i;
+ return -1;
+ }
+
+ function lastIndexOf(item, i) {
+ i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
+ var n = this.slice(0, i).reverse().indexOf(item);
+ return (n < 0) ? n : i - n - 1;
+ }
+
+ function concat() {
+ var array = slice.call(this, 0), item;
+ for (var i = 0, length = arguments.length; i < length; i++) {
+ item = arguments[i];
+ if (Object.isArray(item) && !('callee' in item)) {
+ for (var j = 0, arrayLength = item.length; j < arrayLength; j++)
+ array.push(item[j]);
+ } else {
+ array.push(item);
+ }
+ }
+ return array;
+ }
+
+ Object.extend(arrayProto, Enumerable);
+
+ if (!arrayProto._reverse)
+ arrayProto._reverse = arrayProto.reverse;
+
+ Object.extend(arrayProto, {
+ _each: _each,
+ clear: clear,
+ first: first,
+ last: last,
+ compact: compact,
+ flatten: flatten,
+ without: without,
+ reverse: reverse,
+ uniq: uniq,
+ intersect: intersect,
+ clone: clone,
+ toArray: clone,
+ size: size,
+ inspect: inspect
+ });
+
+ var CONCAT_ARGUMENTS_BUGGY = (function() {
+ return [].concat(arguments)[0][0] !== 1;
+ })(1,2)
+
+ if (CONCAT_ARGUMENTS_BUGGY) arrayProto.concat = concat;
+
+ if (!arrayProto.indexOf) arrayProto.indexOf = indexOf;
+ if (!arrayProto.lastIndexOf) arrayProto.lastIndexOf = lastIndexOf;
+})();
+function $H(object) {
+ return new Hash(object);
+};
+
+var Hash = Class.create(Enumerable, (function() {
+ function initialize(object) {
+ this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
+ }
+
+
+ function _each(iterator) {
+ for (var key in this._object) {
+ var value = this._object[key], pair = [key, value];
+ pair.key = key;
+ pair.value = value;
+ iterator(pair);
+ }
+ }
+
+ function set(key, value) {
+ return this._object[key] = value;
+ }
+
+ function get(key) {
+ if (this._object[key] !== Object.prototype[key])
+ return this._object[key];
+ }
+
+ function unset(key) {
+ var value = this._object[key];
+ delete this._object[key];
+ return value;
+ }
+
+ function toObject() {
+ return Object.clone(this._object);
+ }
+
+
+
+ function keys() {
+ return this.pluck('key');
+ }
+
+ function values() {
+ return this.pluck('value');
+ }
+
+ function index(value) {
+ var match = this.detect(function(pair) {
+ return pair.value === value;
+ });
+ return match && match.key;
+ }
+
+ function merge(object) {
+ return this.clone().update(object);
+ }
+
+ function update(object) {
+ return new Hash(object).inject(this, function(result, pair) {
+ result.set(pair.key, pair.value);
+ return result;
+ });
+ }
+
+ function toQueryPair(key, value) {
+ if (Object.isUndefined(value)) return key;
+ return key + '=' + encodeURIComponent(String.interpret(value));
+ }
+
+ function toQueryString() {
+ return this.inject([], function(results, pair) {
+ var key = encodeURIComponent(pair.key), values = pair.value;
+
+ if (values && typeof values == 'object') {
+ if (Object.isArray(values))
+ return results.concat(values.map(toQueryPair.curry(key)));
+ } else results.push(toQueryPair(key, values));
+ return results;
+ }).join('&');
+ }
+
+ function inspect() {
+ return '#<Hash:{' + this.map(function(pair) {
+ return pair.map(Object.inspect).join(': ');
+ }).join(', ') + '}>';
+ }
+
+ function clone() {
+ return new Hash(this);
+ }
+
+ return {
+ initialize: initialize,
+ _each: _each,
+ set: set,
+ get: get,
+ unset: unset,
+ toObject: toObject,
+ toTemplateReplacements: toObject,
+ keys: keys,
+ values: values,
+ index: index,
+ merge: merge,
+ update: update,
+ toQueryString: toQueryString,
+ inspect: inspect,
+ toJSON: toObject,
+ clone: clone
+ };
+})());
+
+Hash.from = $H;
+Object.extend(Number.prototype, (function() {
+ function toColorPart() {
+ return this.toPaddedString(2, 16);
+ }
+
+ function succ() {
+ return this + 1;
+ }
+
+ function times(iterator, context) {
+ $R(0, this, true).each(iterator, context);
+ return this;
+ }
+
+ function toPaddedString(length, radix) {
+ var string = this.toString(radix || 10);
+ return '0'.times(length - string.length) + string;
+ }
+
+ function abs() {
+ return Math.abs(this);
+ }
+
+ function round() {
+ return Math.round(this);
+ }
+
+ function ceil() {
+ return Math.ceil(this);
+ }
+
+ function floor() {
+ return Math.floor(this);
+ }
+
+ return {
+ toColorPart: toColorPart,
+ succ: succ,
+ times: times,
+ toPaddedString: toPaddedString,
+ abs: abs,
+ round: round,
+ ceil: ceil,
+ floor: floor
+ };
+})());
+
+function $R(start, end, exclusive) {
+ return new ObjectRange(start, end, exclusive);
+}
+
+var ObjectRange = Class.create(Enumerable, (function() {
+ function initialize(start, end, exclusive) {
+ this.start = start;
+ this.end = end;
+ this.exclusive = exclusive;
+ }
+
+ function _each(iterator) {
+ var value = this.start;
+ while (this.include(value)) {
+ iterator(value);
+ value = value.succ();
+ }
+ }
+
+ function include(value) {
+ if (value < this.start)
+ return false;
+ if (this.exclusive)
+ return value < this.end;
+ return value <= this.end;
+ }
+
+ return {
+ initialize: initialize,
+ _each: _each,
+ include: include
+ };
+})());
+
+
+
+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 (Object.isFunction(responder[callback])) {
+ 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 = Class.create({
+ initialize: function(options) {
+ this.options = {
+ method: 'post',
+ asynchronous: true,
+ contentType: 'application/x-www-form-urlencoded',
+ encoding: 'UTF-8',
+ parameters: '',
+ evalJSON: true,
+ evalJS: true
+ };
+ Object.extend(this.options, options || { });
+
+ this.options.method = this.options.method.toLowerCase();
+
+ if (Object.isString(this.options.parameters))
+ this.options.parameters = this.options.parameters.toQueryParams();
+ else if (Object.isHash(this.options.parameters))
+ this.options.parameters = this.options.parameters.toObject();
+ }
+});
+Ajax.Request = Class.create(Ajax.Base, {
+ _complete: false,
+
+ initialize: function($super, url, options) {
+ $super(options);
+ this.transport = Ajax.getTransport();
+ this.request(url);
+ },
+
+ request: function(url) {
+ this.url = url;
+ this.method = this.options.method;
+ var params = Object.clone(this.options.parameters);
+
+ if (!['get', 'post'].include(this.method)) {
+ params['_method'] = this.method;
+ this.method = 'post';
+ }
+
+ this.parameters = params;
+
+ if (params = Object.toQueryString(params)) {
+ if (this.method == 'get')
+ this.url += (this.url.include('?') ? '&' : '?') + params;
+ else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
+ params += '&_=';
+ }
+
+ try {
+ var response = new Ajax.Response(this);
+ if (this.options.onCreate) this.options.onCreate(response);
+ Ajax.Responders.dispatch('onCreate', this, response);
+
+ this.transport.open(this.method.toUpperCase(), this.url,
+ this.options.asynchronous);
+
+ if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);
+
+ this.transport.onreadystatechange = this.onStateChange.bind(this);
+ this.setRequestHeaders();
+
+ this.body = this.method == 'post' ? (this.options.postBody || params) : null;
+ this.transport.send(this.body);
+
+ /* Force Firefox to handle ready state 4 for synchronous requests */
+ if (!this.options.asynchronous && this.transport.overrideMimeType)
+ this.onStateChange();
+
+ }
+ catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ onStateChange: function() {
+ var readyState = this.transport.readyState;
+ if (readyState > 1 && !((readyState == 4) && this._complete))
+ this.respondToReadyState(this.transport.readyState);
+ },
+
+ setRequestHeaders: function() {
+ var headers = {
+ 'X-Requested-With': 'XMLHttpRequest',
+ 'X-Prototype-Version': Prototype.Version,
+ 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
+ };
+
+ if (this.method == 'post') {
+ headers['Content-type'] = this.options.contentType +
+ (this.options.encoding ? '; charset=' + this.options.encoding : '');
+
+ /* Force "Connection: close" for older Mozilla browsers to work
+ * around a bug where XMLHttpRequest sends an incorrect
+ * Content-length header. See Mozilla Bugzilla #246651.
+ */
+ if (this.transport.overrideMimeType &&
+ (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
+ headers['Connection'] = 'close';
+ }
+
+ if (typeof this.options.requestHeaders == 'object') {
+ var extras = this.options.requestHeaders;
+
+ if (Object.isFunction(extras.push))
+ for (var i = 0, length = extras.length; i < length; i += 2)
+ headers[extras[i]] = extras[i+1];
+ else
+ $H(extras).each(function(pair) { headers[pair.key] = pair.value });
+ }
+
+ for (var name in headers)
+ this.transport.setRequestHeader(name, headers[name]);
+ },
+
+ success: function() {
+ var status = this.getStatus();
+ return !status || (status >= 200 && status < 300);
+ },
+
+ getStatus: function() {
+ try {
+ return this.transport.status || 0;
+ } catch (e) { return 0 }
+ },
+
+ respondToReadyState: function(readyState) {
+ var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);
+
+ if (state == 'Complete') {
+ try {
+ this._complete = true;
+ (this.options['on' + response.status]
+ || this.options['on' + (this.success() ? 'Success' : 'Failure')]
+ || Prototype.emptyFunction)(response, response.headerJSON);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ var contentType = response.getHeader('Content-type');
+ if (this.options.evalJS == 'force'
+ || (this.options.evalJS && this.isSameOrigin() && contentType
+ && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
+ this.evalResponse();
+ }
+
+ try {
+ (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
+ Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
+ } catch (e) {
+ this.dispatchException(e);
+ }
+
+ if (state == 'Complete') {
+ this.transport.onreadystatechange = Prototype.emptyFunction;
+ }
+ },
+
+ isSameOrigin: function() {
+ var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
+ return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
+ protocol: location.protocol,
+ domain: document.domain,
+ port: location.port ? ':' + location.port : ''
+ }));
+ },
+
+ getHeader: function(name) {
+ try {
+ return this.transport.getResponseHeader(name) || null;
+ } catch (e) { return null; }
+ },
+
+ evalResponse: function() {
+ try {
+ return eval((this.transport.responseText || '').unfilterJSON());
+ } catch (e) {
+ this.dispatchException(e);
+ }
+ },
+
+ dispatchException: function(exception) {
+ (this.options.onException || Prototype.emptyFunction)(this, exception);
+ Ajax.Responders.dispatch('onException', this, exception);
+ }
+});
+
+Ajax.Request.Events =
+ ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
+
+
+
+
+
+
+
+
+Ajax.Response = Class.create({
+ initialize: function(request){
+ this.request = request;
+ var transport = this.transport = request.transport,
+ readyState = this.readyState = transport.readyState;
+
+ if ((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
+ this.status = this.getStatus();
+ this.statusText = this.getStatusText();
+ this.responseText = String.interpret(transport.responseText);
+ this.headerJSON = this._getHeaderJSON();
+ }
+
+ if (readyState == 4) {
+ var xml = transport.responseXML;
+ this.responseXML = Object.isUndefined(xml) ? null : xml;
+ this.responseJSON = this._getResponseJSON();
+ }
+ },
+
+ status: 0,
+
+ statusText: '',
+
+ getStatus: Ajax.Request.prototype.getStatus,
+
+ getStatusText: function() {
+ try {
+ return this.transport.statusText || '';
+ } catch (e) { return '' }
+ },
+
+ getHeader: Ajax.Request.prototype.getHeader,
+
+ getAllHeaders: function() {
+ try {
+ return this.getAllResponseHeaders();
+ } catch (e) { return null }
+ },
+
+ getResponseHeader: function(name) {
+ return this.transport.getResponseHeader(name);
+ },
+
+ getAllResponseHeaders: function() {
+ return this.transport.getAllResponseHeaders();
+ },
+
+ _getHeaderJSON: function() {
+ var json = this.getHeader('X-JSON');
+ if (!json) return null;
+ json = decodeURIComponent(escape(json));
+ try {
+ return json.evalJSON(this.request.options.sanitizeJSON ||
+ !this.request.isSameOrigin());
+ } catch (e) {
+ this.request.dispatchException(e);
+ }
+ },
+
+ _getResponseJSON: function() {
+ var options = this.request.options;
+ if (!options.evalJSON || (options.evalJSON != 'force' &&
+ !(this.getHeader('Content-type') || '').include('application/json')) ||
+ this.responseText.blank())
+ return null;
+ try {
+ return this.responseText.evalJSON(options.sanitizeJSON ||
+ !this.request.isSameOrigin());
+ } catch (e) {
+ this.request.dispatchException(e);
+ }
+ }
+});
+
+Ajax.Updater = Class.create(Ajax.Request, {
+ initialize: function($super, container, url, options) {
+ this.container = {
+ success: (container.success || container),
+ failure: (container.failure || (container.success ? null : container))
+ };
+
+ options = Object.clone(options);
+ var onComplete = options.onComplete;
+ options.onComplete = (function(response, json) {
+ this.updateContent(response.responseText);
+ if (Object.isFunction(onComplete)) onComplete(response, json);
+ }).bind(this);
+
+ $super(url, options);
+ },
+
+ updateContent: function(responseText) {
+ var receiver = this.container[this.success() ? 'success' : 'failure'],
+ options = this.options;
+
+ if (!options.evalScripts) responseText = responseText.stripScripts();
+
+ if (receiver = $(receiver)) {
+ if (options.insertion) {
+ if (Object.isString(options.insertion)) {
+ var insertion = { }; insertion[options.insertion] = responseText;
+ receiver.insert(insertion);
+ }
+ else options.insertion(receiver, responseText);
+ }
+ else receiver.update(responseText);
+ }
+ }
+});
+
+Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
+ initialize: function($super, container, url, options) {
+ $super(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(response) {
+ if (this.options.decay) {
+ this.decay = (response.responseText == this.lastText ?
+ this.decay * this.options.decay : 1);
+
+ this.lastText = response.responseText;
+ }
+ this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
+ },
+
+ onTimerEvent: function() {
+ this.updater = new Ajax.Updater(this.container, this.url, this.options);
+ }
+});
+
+
+function $(element) {
+ if (arguments.length > 1) {
+ for (var i = 0, elements = [], length = arguments.length; i < length; i++)
+ elements.push($(arguments[i]));
+ return elements;
+ }
+ if (Object.isString(element))
+ element = document.getElementById(element);
+ return Element.extend(element);
+}
+
+if (Prototype.BrowserFeatures.XPath) {
+ document._getElementsByXPath = function(expression, parentElement) {
+ var results = [];
+ var query = document.evaluate(expression, $(parentElement) || document,
+ null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
+ for (var i = 0, length = query.snapshotLength; i < length; i++)
+ results.push(Element.extend(query.snapshotItem(i)));
+ return results;
+ };
+}
+
+/*--------------------------------------------------------------------------*/
+
+if (!Node) var Node = { };
+
+if (!Node.ELEMENT_NODE) {
+ Object.extend(Node, {
+ ELEMENT_NODE: 1,
+ ATTRIBUTE_NODE: 2,
+ TEXT_NODE: 3,
+ CDATA_SECTION_NODE: 4,
+ ENTITY_REFERENCE_NODE: 5,
+ ENTITY_NODE: 6,
+ PROCESSING_INSTRUCTION_NODE: 7,
+ COMMENT_NODE: 8,
+ DOCUMENT_NODE: 9,
+ DOCUMENT_TYPE_NODE: 10,
+ DOCUMENT_FRAGMENT_NODE: 11,
+ NOTATION_NODE: 12
+ });
+}
+
+
+
+(function(global) {
+
+ var HAS_EXTENDED_CREATE_ELEMENT_SYNTAX = (function(){
+ try {
+ var el = document.createElement('<input name="x">');
+ return el.tagName.toLowerCase() === 'input' && el.name === 'x';
+ }
+ catch(err) {
+ return false;
+ }
+ })();
+
+ var element = global.Element;
+
+ global.Element = function(tagName, attributes) {
+ attributes = attributes || { };
+ tagName = tagName.toLowerCase();
+ var cache = Element.cache;
+ if (HAS_EXTENDED_CREATE_ELEMENT_SYNTAX && attributes.name) {
+ tagName = '<' + tagName + ' name="' + attributes.name + '">';
+ delete attributes.name;
+ return Element.writeAttribute(document.createElement(tagName), attributes);
+ }
+ if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
+ return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
+ };
+
+ Object.extend(global.Element, element || { });
+ if (element) global.Element.prototype = element.prototype;
+
+})(this);
+
+Element.idCounter = 1;
+Element.cache = { };
+
+function purgeElement(element) {
+ var uid = element._prototypeUID;
+ if (uid) {
+ Element.stopObserving(element);
+ element._prototypeUID = void 0;
+ delete Element.Storage[uid];
+ }
+}
+
+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 = $(element);
+ element.style.display = 'none';
+ return element;
+ },
+
+ show: function(element) {
+ element = $(element);
+ element.style.display = '';
+ return element;
+ },
+
+ remove: function(element) {
+ element = $(element);
+ element.parentNode.removeChild(element);
+ return element;
+ },
+
+ update: (function(){
+
+ var SELECT_ELEMENT_INNERHTML_BUGGY = (function(){
+ var el = document.createElement("select"),
+ isBuggy = true;
+ el.innerHTML = "<option value=\"test\">test</option>";
+ if (el.options && el.options[0]) {
+ isBuggy = el.options[0].nodeName.toUpperCase() !== "OPTION";
+ }
+ el = null;
+ return isBuggy;
+ })();
+
+ var TABLE_ELEMENT_INNERHTML_BUGGY = (function(){
+ try {
+ var el = document.createElement("table");
+ if (el && el.tBodies) {
+ el.innerHTML = "<tbody><tr><td>test</td></tr></tbody>";
+ var isBuggy = typeof el.tBodies[0] == "undefined";
+ el = null;
+ return isBuggy;
+ }
+ } catch (e) {
+ return true;
+ }
+ })();
+
+ var SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING = (function () {
+ var s = document.createElement("script"),
+ isBuggy = false;
+ try {
+ s.appendChild(document.createTextNode(""));
+ isBuggy = !s.firstChild ||
+ s.firstChild && s.firstChild.nodeType !== 3;
+ } catch (e) {
+ isBuggy = true;
+ }
+ s = null;
+ return isBuggy;
+ })();
+
+ function update(element, content) {
+ element = $(element);
+
+ var descendants = element.getElementsByTagName('*'),
+ i = descendants.length;
+ while (i--) purgeElement(descendants[i]);
+
+ if (content && content.toElement)
+ content = content.toElement();
+
+ if (Object.isElement(content))
+ return element.update().insert(content);
+
+ content = Object.toHTML(content);
+
+ var tagName = element.tagName.toUpperCase();
+
+ if (tagName === 'SCRIPT' && SCRIPT_ELEMENT_REJECTS_TEXTNODE_APPENDING) {
+ element.text = content;
+ return element;
+ }
+
+ if (SELECT_ELEMENT_INNERHTML_BUGGY || TABLE_ELEMENT_INNERHTML_BUGGY) {
+ if (tagName in Element._insertionTranslations.tags) {
+ while (element.firstChild) {
+ element.removeChild(element.firstChild);
+ }
+ Element._getContentFromAnonymousElement(tagName, content.stripScripts())
+ .each(function(node) {
+ element.appendChild(node)
+ });
+ }
+ else {
+ element.innerHTML = content.stripScripts();
+ }
+ }
+ else {
+ element.innerHTML = content.stripScripts();
+ }
+
+ content.evalScripts.bind(content).defer();
+ return element;
+ }
+
+ return update;
+ })(),
+
+ replace: function(element, content) {
+ element = $(element);
+ if (content && content.toElement) content = content.toElement();
+ else if (!Object.isElement(content)) {
+ content = Object.toHTML(content);
+ var range = element.ownerDocument.createRange();
+ range.selectNode(element);
+ content.evalScripts.bind(content).defer();
+ content = range.createContextualFragment(content.stripScripts());
+ }
+ element.parentNode.replaceChild(content, element);
+ return element;
+ },
+
+ insert: function(element, insertions) {
+ element = $(element);
+
+ if (Object.isString(insertions) || Object.isNumber(insertions) ||
+ Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
+ insertions = {bottom:insertions};
+
+ var content, insert, tagName, childNodes;
+
+ for (var position in insertions) {
+ content = insertions[position];
+ position = position.toLowerCase();
+ insert = Element._insertionTranslations[position];
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) {
+ insert(element, content);
+ continue;
+ }
+
+ content = Object.toHTML(content);
+
+ tagName = ((position == 'before' || position == 'after')
+ ? element.parentNode : element).tagName.toUpperCase();
+
+ childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+
+ if (position == 'top' || position == 'after') childNodes.reverse();
+ childNodes.each(insert.curry(element));
+
+ content.evalScripts.bind(content).defer();
+ }
+
+ return element;
+ },
+
+ wrap: function(element, wrapper, attributes) {
+ element = $(element);
+ if (Object.isElement(wrapper))
+ $(wrapper).writeAttribute(attributes || { });
+ else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
+ else wrapper = new Element('div', wrapper);
+ if (element.parentNode)
+ element.parentNode.replaceChild(wrapper, element);
+ wrapper.appendChild(element);
+ return wrapper;
+ },
+
+ inspect: function(element) {
+ element = $(element);
+ var result = '<' + element.tagName.toLowerCase();
+ $H({'id': 'id', 'className': 'class'}).each(function(pair) {
+ var property = pair.first(),
+ attribute = pair.last(),
+ value = (element[property] || '').toString();
+ if (value) result += ' ' + attribute + '=' + value.inspect(true);
+ });
+ return result + '>';
+ },
+
+ recursivelyCollect: function(element, property, maximumLength) {
+ element = $(element);
+ maximumLength = maximumLength || -1;
+ var elements = [];
+
+ while (element = element[property]) {
+ if (element.nodeType == 1)
+ elements.push(Element.extend(element));
+ if (elements.length == maximumLength)
+ break;
+ }
+
+ return elements;
+ },
+
+ ancestors: function(element) {
+ return Element.recursivelyCollect(element, 'parentNode');
+ },
+
+ descendants: function(element) {
+ return Element.select(element, "*");
+ },
+
+ firstDescendant: function(element) {
+ element = $(element).firstChild;
+ while (element && element.nodeType != 1) element = element.nextSibling;
+ return $(element);
+ },
+
+ immediateDescendants: function(element) {
+ var results = [], child = $(element).firstChild;
+ while (child) {
+ if (child.nodeType === 1) {
+ results.push(Element.extend(child));
+ }
+ child = child.nextSibling;
+ }
+ return results;
+ },
+
+ previousSiblings: function(element, maximumLength) {
+ return Element.recursivelyCollect(element, 'previousSibling');
+ },
+
+ nextSiblings: function(element) {
+ return Element.recursivelyCollect(element, 'nextSibling');
+ },
+
+ siblings: function(element) {
+ element = $(element);
+ return Element.previousSiblings(element).reverse()
+ .concat(Element.nextSiblings(element));
+ },
+
+ match: function(element, selector) {
+ element = $(element);
+ if (Object.isString(selector))
+ return Prototype.Selector.match(element, selector);
+ return selector.match(element);
+ },
+
+ up: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return $(element.parentNode);
+ var ancestors = Element.ancestors(element);
+ return Object.isNumber(expression) ? ancestors[expression] :
+ Prototype.Selector.find(ancestors, expression, index);
+ },
+
+ down: function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return Element.firstDescendant(element);
+ return Object.isNumber(expression) ? Element.descendants(element)[expression] :
+ Element.select(element, expression)[index || 0];
+ },
+
+ previous: function(element, expression, index) {
+ element = $(element);
+ if (Object.isNumber(expression)) index = expression, expression = false;
+ if (!Object.isNumber(index)) index = 0;
+
+ if (expression) {
+ return Prototype.Selector.find(element.previousSiblings(), expression, index);
+ } else {
+ return element.recursivelyCollect("previousSibling", index + 1)[index];
+ }
+ },
+
+ next: function(element, expression, index) {
+ element = $(element);
+ if (Object.isNumber(expression)) index = expression, expression = false;
+ if (!Object.isNumber(index)) index = 0;
+
+ if (expression) {
+ return Prototype.Selector.find(element.nextSiblings(), expression, index);
+ } else {
+ var maximumLength = Object.isNumber(index) ? index + 1 : 1;
+ return element.recursivelyCollect("nextSibling", index + 1)[index];
+ }
+ },
+
+
+ select: function(element) {
+ element = $(element);
+ var expressions = Array.prototype.slice.call(arguments, 1).join(', ');
+ return Prototype.Selector.select(expressions, element);
+ },
+
+ adjacent: function(element) {
+ element = $(element);
+ var expressions = Array.prototype.slice.call(arguments, 1).join(', ');
+ return Prototype.Selector.select(expressions, element.parentNode).without(element);
+ },
+
+ identify: function(element) {
+ element = $(element);
+ var id = Element.readAttribute(element, 'id');
+ if (id) return id;
+ do { id = 'anonymous_element_' + Element.idCounter++ } while ($(id));
+ Element.writeAttribute(element, 'id', id);
+ return id;
+ },
+
+ readAttribute: function(element, name) {
+ element = $(element);
+ if (Prototype.Browser.IE) {
+ var t = Element._attributeTranslations.read;
+ if (t.values[name]) return t.values[name](element, name);
+ if (t.names[name]) name = t.names[name];
+ if (name.include(':')) {
+ return (!element.attributes || !element.attributes[name]) ? null :
+ element.attributes[name].value;
+ }
+ }
+ return element.getAttribute(name);
+ },
+
+ writeAttribute: function(element, name, value) {
+ element = $(element);
+ var attributes = { }, t = Element._attributeTranslations.write;
+
+ if (typeof name == 'object') attributes = name;
+ else attributes[name] = Object.isUndefined(value) ? true : value;
+
+ for (var attr in attributes) {
+ name = t.names[attr] || attr;
+ value = attributes[attr];
+ if (t.values[attr]) name = t.values[attr](element, value);
+ if (value === false || value === null)
+ element.removeAttribute(name);
+ else if (value === true)
+ element.setAttribute(name, name);
+ else element.setAttribute(name, value);
+ }
+ return element;
+ },
+
+ getHeight: function(element) {
+ return Element.getDimensions(element).height;
+ },
+
+ getWidth: function(element) {
+ return Element.getDimensions(element).width;
+ },
+
+ classNames: function(element) {
+ return new Element.ClassNames(element);
+ },
+
+ hasClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ var elementClassName = element.className;
+ return (elementClassName.length > 0 && (elementClassName == className ||
+ new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
+ },
+
+ addClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ if (!Element.hasClassName(element, className))
+ element.className += (element.className ? ' ' : '') + className;
+ return element;
+ },
+
+ removeClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ element.className = element.className.replace(
+ new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
+ return element;
+ },
+
+ toggleClassName: function(element, className) {
+ if (!(element = $(element))) return;
+ return Element[Element.hasClassName(element, className) ?
+ 'removeClassName' : 'addClassName'](element, className);
+ },
+
+ cleanWhitespace: function(element) {
+ element = $(element);
+ var node = element.firstChild;
+ while (node) {
+ var nextNode = node.nextSibling;
+ if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
+ element.removeChild(node);
+ node = nextNode;
+ }
+ return element;
+ },
+
+ empty: function(element) {
+ return $(element).innerHTML.blank();
+ },
+
+ descendantOf: function(element, ancestor) {
+ element = $(element), ancestor = $(ancestor);
+
+ if (element.compareDocumentPosition)
+ return (element.compareDocumentPosition(ancestor) & 8) === 8;
+
+ if (ancestor.contains)
+ return ancestor.contains(element) && ancestor !== element;
+
+ while (element = element.parentNode)
+ if (element == ancestor) return true;
+
+ return false;
+ },
+
+ scrollTo: function(element) {
+ element = $(element);
+ var pos = Element.cumulativeOffset(element);
+ window.scrollTo(pos[0], pos[1]);
+ return element;
+ },
+
+ getStyle: function(element, style) {
+ element = $(element);
+ style = style == 'float' ? 'cssFloat' : style.camelize();
+ var value = element.style[style];
+ if (!value || value == 'auto') {
+ var css = document.defaultView.getComputedStyle(element, null);
+ value = css ? css[style] : null;
+ }
+ if (style == 'opacity') return value ? parseFloat(value) : 1.0;
+ return value == 'auto' ? null : value;
+ },
+
+ getOpacity: function(element) {
+ return $(element).getStyle('opacity');
+ },
+
+ setStyle: function(element, styles) {
+ element = $(element);
+ var elementStyle = element.style, match;
+ if (Object.isString(styles)) {
+ element.style.cssText += ';' + styles;
+ return styles.include('opacity') ?
+ element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
+ }
+ for (var property in styles)
+ if (property == 'opacity') element.setOpacity(styles[property]);
+ else
+ elementStyle[(property == 'float' || property == 'cssFloat') ?
+ (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
+ property] = styles[property];
+
+ return element;
+ },
+
+ setOpacity: function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1 || value === '') ? '' :
+ (value < 0.00001) ? 0 : value;
+ return element;
+ },
+
+ makePositioned: function(element) {
+ element = $(element);
+ var pos = Element.getStyle(element, 'position');
+ if (pos == 'static' || !pos) {
+ element._madePositioned = true;
+ element.style.position = 'relative';
+ if (Prototype.Browser.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.getStyle(element, 'overflow') || 'auto';
+ if (element._overflow !== '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;
+ },
+
+ cumulativeOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ if (element.parentNode) {
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ } while (element);
+ }
+ return Element._returnOffset(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.toUpperCase() == 'BODY') break;
+ var p = Element.getStyle(element, 'position');
+ if (p !== 'static') break;
+ }
+ } while (element);
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ absolutize: function(element) {
+ element = $(element);
+ if (Element.getStyle(element, 'position') == 'absolute') return element;
+
+ var offsets = Element.positionedOffset(element),
+ top = offsets[1],
+ left = offsets[0],
+ width = element.clientWidth,
+ 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';
+ return element;
+ },
+
+ relativize: function(element) {
+ element = $(element);
+ if (Element.getStyle(element, 'position') == 'relative') return element;
+
+ element.style.position = 'relative';
+ var top = parseFloat(element.style.top || 0) - (element._originalTop || 0),
+ 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;
+ return element;
+ },
+
+ cumulativeScrollOffset: function(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.scrollTop || 0;
+ valueL += element.scrollLeft || 0;
+ element = element.parentNode;
+ } while (element);
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ getOffsetParent: function(element) {
+ if (element.offsetParent) return $(element.offsetParent);
+ if (element == document.body) return $(element);
+
+ while ((element = element.parentNode) && element != document.body)
+ if (Element.getStyle(element, 'position') != 'static')
+ return $(element);
+
+ return $(document.body);
+ },
+
+ viewportOffset: function(forElement) {
+ var valueT = 0,
+ valueL = 0,
+ element = forElement;
+
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+
+ if (element.offsetParent == document.body &&
+ Element.getStyle(element, 'position') == 'absolute') break;
+
+ } while (element = element.offsetParent);
+
+ element = forElement;
+ do {
+ if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) {
+ valueT -= element.scrollTop || 0;
+ valueL -= element.scrollLeft || 0;
+ }
+ } while (element = element.parentNode);
+
+ return Element._returnOffset(valueL, valueT);
+ },
+
+ clonePosition: function(element, source) {
+ var options = Object.extend({
+ setLeft: true,
+ setTop: true,
+ setWidth: true,
+ setHeight: true,
+ offsetTop: 0,
+ offsetLeft: 0
+ }, arguments[2] || { });
+
+ source = $(source);
+ var p = Element.viewportOffset(source), delta = [0, 0], parent = null;
+
+ element = $(element);
+
+ if (Element.getStyle(element, 'position') == 'absolute') {
+ parent = Element.getOffsetParent(element);
+ delta = Element.viewportOffset(parent);
+ }
+
+ if (parent == document.body) {
+ delta[0] -= document.body.offsetLeft;
+ delta[1] -= document.body.offsetTop;
+ }
+
+ if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px';
+ if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px';
+ if (options.setWidth) element.style.width = source.offsetWidth + 'px';
+ if (options.setHeight) element.style.height = source.offsetHeight + 'px';
+ return element;
+ }
+};
+
+Object.extend(Element.Methods, {
+ getElementsBySelector: Element.Methods.select,
+
+ childElements: Element.Methods.immediateDescendants
+});
+
+Element._attributeTranslations = {
+ write: {
+ names: {
+ className: 'class',
+ htmlFor: 'for'
+ },
+ values: { }
+ }
+};
+
+if (Prototype.Browser.Opera) {
+ Element.Methods.getStyle = Element.Methods.getStyle.wrap(
+ function(proceed, element, style) {
+ switch (style) {
+ case 'left': case 'top': case 'right': case 'bottom':
+ if (proceed(element, 'position') === 'static') return null;
+ case 'height': case 'width':
+ if (!Element.visible(element)) return null;
+
+ var dim = parseInt(proceed(element, style), 10);
+
+ if (dim !== element['offset' + style.capitalize()])
+ return dim + 'px';
+
+ var properties;
+ if (style === 'height') {
+ properties = ['border-top-width', 'padding-top',
+ 'padding-bottom', 'border-bottom-width'];
+ }
+ else {
+ properties = ['border-left-width', 'padding-left',
+ 'padding-right', 'border-right-width'];
+ }
+ return properties.inject(dim, function(memo, property) {
+ var val = proceed(element, property);
+ return val === null ? memo : memo - parseInt(val, 10);
+ }) + 'px';
+ default: return proceed(element, style);
+ }
+ }
+ );
+
+ Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
+ function(proceed, element, attribute) {
+ if (attribute === 'title') return element.title;
+ return proceed(element, attribute);
+ }
+ );
+}
+
+else if (Prototype.Browser.IE) {
+ Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
+ function(proceed, element) {
+ element = $(element);
+ if (!element.parentNode) return $(document.body);
+ var position = element.getStyle('position');
+ if (position !== 'static') return proceed(element);
+ element.setStyle({ position: 'relative' });
+ var value = proceed(element);
+ element.setStyle({ position: position });
+ return value;
+ }
+ );
+
+ $w('positionedOffset viewportOffset').each(function(method) {
+ Element.Methods[method] = Element.Methods[method].wrap(
+ function(proceed, element) {
+ element = $(element);
+ if (!element.parentNode) return Element._returnOffset(0, 0);
+ var position = element.getStyle('position');
+ if (position !== 'static') return proceed(element);
+ var offsetParent = element.getOffsetParent();
+ if (offsetParent && offsetParent.getStyle('position') === 'fixed')
+ offsetParent.setStyle({ zoom: 1 });
+ element.setStyle({ position: 'relative' });
+ var value = proceed(element);
+ element.setStyle({ position: position });
+ return value;
+ }
+ );
+ });
+
+ Element.Methods.getStyle = function(element, style) {
+ element = $(element);
+ style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
+ var value = element.style[style];
+ if (!value && element.currentStyle) value = element.currentStyle[style];
+
+ if (style == 'opacity') {
+ if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
+ if (value[1]) return parseFloat(value[1]) / 100;
+ return 1.0;
+ }
+
+ if (value == 'auto') {
+ if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
+ return element['offset' + style.capitalize()] + 'px';
+ return null;
+ }
+ return value;
+ };
+
+ Element.Methods.setOpacity = function(element, value) {
+ function stripAlpha(filter){
+ return filter.replace(/alpha\([^\)]*\)/gi,'');
+ }
+ element = $(element);
+ var currentStyle = element.currentStyle;
+ if ((currentStyle && !currentStyle.hasLayout) ||
+ (!currentStyle && element.style.zoom == 'normal'))
+ element.style.zoom = 1;
+
+ var filter = element.getStyle('filter'), style = element.style;
+ if (value == 1 || value === '') {
+ (filter = stripAlpha(filter)) ?
+ style.filter = filter : style.removeAttribute('filter');
+ return element;
+ } else if (value < 0.00001) value = 0;
+ style.filter = stripAlpha(filter) +
+ 'alpha(opacity=' + (value * 100) + ')';
+ return element;
+ };
+
+ Element._attributeTranslations = (function(){
+
+ var classProp = 'className',
+ forProp = 'for',
+ el = document.createElement('div');
+
+ el.setAttribute(classProp, 'x');
+
+ if (el.className !== 'x') {
+ el.setAttribute('class', 'x');
+ if (el.className === 'x') {
+ classProp = 'class';
+ }
+ }
+ el = null;
+
+ el = document.createElement('label');
+ el.setAttribute(forProp, 'x');
+ if (el.htmlFor !== 'x') {
+ el.setAttribute('htmlFor', 'x');
+ if (el.htmlFor === 'x') {
+ forProp = 'htmlFor';
+ }
+ }
+ el = null;
+
+ return {
+ read: {
+ names: {
+ 'class': classProp,
+ 'className': classProp,
+ 'for': forProp,
+ 'htmlFor': forProp
+ },
+ values: {
+ _getAttr: function(element, attribute) {
+ return element.getAttribute(attribute);
+ },
+ _getAttr2: function(element, attribute) {
+ return element.getAttribute(attribute, 2);
+ },
+ _getAttrNode: function(element, attribute) {
+ var node = element.getAttributeNode(attribute);
+ return node ? node.value : "";
+ },
+ _getEv: (function(){
+
+ var el = document.createElement('div'), f;
+ el.onclick = Prototype.emptyFunction;
+ var value = el.getAttribute('onclick');
+
+ if (String(value).indexOf('{') > -1) {
+ f = function(element, attribute) {
+ attribute = element.getAttribute(attribute);
+ if (!attribute) return null;
+ attribute = attribute.toString();
+ attribute = attribute.split('{')[1];
+ attribute = attribute.split('}')[0];
+ return attribute.strip();
+ };
+ }
+ else if (value === '') {
+ f = function(element, attribute) {
+ attribute = element.getAttribute(attribute);
+ if (!attribute) return null;
+ return attribute.strip();
+ };
+ }
+ el = null;
+ return f;
+ })(),
+ _flag: function(element, attribute) {
+ return $(element).hasAttribute(attribute) ? attribute : null;
+ },
+ style: function(element) {
+ return element.style.cssText.toLowerCase();
+ },
+ title: function(element) {
+ return element.title;
+ }
+ }
+ }
+ }
+ })();
+
+ Element._attributeTranslations.write = {
+ names: Object.extend({
+ cellpadding: 'cellPadding',
+ cellspacing: 'cellSpacing'
+ }, Element._attributeTranslations.read.names),
+ values: {
+ checked: function(element, value) {
+ element.checked = !!value;
+ },
+
+ style: function(element, value) {
+ element.style.cssText = value ? value : '';
+ }
+ }
+ };
+
+ Element._attributeTranslations.has = {};
+
+ $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
+ 'encType maxLength readOnly longDesc frameBorder').each(function(attr) {
+ Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
+ Element._attributeTranslations.has[attr.toLowerCase()] = attr;
+ });
+
+ (function(v) {
+ Object.extend(v, {
+ href: v._getAttr2,
+ src: v._getAttr2,
+ type: v._getAttr,
+ action: v._getAttrNode,
+ disabled: v._flag,
+ checked: v._flag,
+ readonly: v._flag,
+ multiple: v._flag,
+ onload: v._getEv,
+ onunload: v._getEv,
+ onclick: v._getEv,
+ ondblclick: v._getEv,
+ onmousedown: v._getEv,
+ onmouseup: v._getEv,
+ onmouseover: v._getEv,
+ onmousemove: v._getEv,
+ onmouseout: v._getEv,
+ onfocus: v._getEv,
+ onblur: v._getEv,
+ onkeypress: v._getEv,
+ onkeydown: v._getEv,
+ onkeyup: v._getEv,
+ onsubmit: v._getEv,
+ onreset: v._getEv,
+ onselect: v._getEv,
+ onchange: v._getEv
+ });
+ })(Element._attributeTranslations.read.values);
+
+ if (Prototype.BrowserFeatures.ElementExtensions) {
+ (function() {
+ function _descendants(element) {
+ var nodes = element.getElementsByTagName('*'), results = [];
+ for (var i = 0, node; node = nodes[i]; i++)
+ if (node.tagName !== "!") // Filter out comment nodes.
+ results.push(node);
+ return results;
+ }
+
+ Element.Methods.down = function(element, expression, index) {
+ element = $(element);
+ if (arguments.length == 1) return element.firstDescendant();
+ return Object.isNumber(expression) ? _descendants(element)[expression] :
+ Element.select(element, expression)[index || 0];
+ }
+ })();
+ }
+
+}
+
+else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1) ? 0.999999 :
+ (value === '') ? '' : (value < 0.00001) ? 0 : value;
+ return element;
+ };
+}
+
+else if (Prototype.Browser.WebKit) {
+ Element.Methods.setOpacity = function(element, value) {
+ element = $(element);
+ element.style.opacity = (value == 1 || value === '') ? '' :
+ (value < 0.00001) ? 0 : value;
+
+ if (value == 1)
+ if (element.tagName.toUpperCase() == 'IMG' && element.width) {
+ element.width++; element.width--;
+ } else try {
+ var n = document.createTextNode(' ');
+ element.appendChild(n);
+ element.removeChild(n);
+ } catch (e) { }
+
+ return element;
+ };
+
+ Element.Methods.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 Element._returnOffset(valueL, valueT);
+ };
+}
+
+if ('outerHTML' in document.documentElement) {
+ Element.Methods.replace = function(element, content) {
+ element = $(element);
+
+ if (content && content.toElement) content = content.toElement();
+ if (Object.isElement(content)) {
+ element.parentNode.replaceChild(content, element);
+ return element;
+ }
+
+ content = Object.toHTML(content);
+ var parent = element.parentNode, tagName = parent.tagName.toUpperCase();
+
+ if (Element._insertionTranslations.tags[tagName]) {
+ var nextSibling = element.next(),
+ fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
+ parent.removeChild(element);
+ if (nextSibling)
+ fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
+ else
+ fragments.each(function(node) { parent.appendChild(node) });
+ }
+ else element.outerHTML = content.stripScripts();
+
+ content.evalScripts.bind(content).defer();
+ return element;
+ };
+}
+
+Element._returnOffset = function(l, t) {
+ var result = [l, t];
+ result.left = l;
+ result.top = t;
+ return result;
+};
+
+Element._getContentFromAnonymousElement = function(tagName, html) {
+ var div = new Element('div'),
+ t = Element._insertionTranslations.tags[tagName];
+ if (t) {
+ div.innerHTML = t[0] + html + t[1];
+ for (var i = t[2]; i--; ) {
+ div = div.firstChild;
+ }
+ }
+ else {
+ div.innerHTML = html;
+ }
+ return $A(div.childNodes);
+};
+
+Element._insertionTranslations = {
+ before: function(element, node) {
+ element.parentNode.insertBefore(node, element);
+ },
+ top: function(element, node) {
+ element.insertBefore(node, element.firstChild);
+ },
+ bottom: function(element, node) {
+ element.appendChild(node);
+ },
+ after: function(element, node) {
+ element.parentNode.insertBefore(node, element.nextSibling);
+ },
+ tags: {
+ TABLE: ['<table>', '</table>', 1],
+ TBODY: ['<table><tbody>', '</tbody></table>', 2],
+ TR: ['<table><tbody><tr>', '</tr></tbody></table>', 3],
+ TD: ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
+ SELECT: ['<select>', '</select>', 1]
+ }
+};
+
+(function() {
+ var tags = Element._insertionTranslations.tags;
+ Object.extend(tags, {
+ THEAD: tags.TBODY,
+ TFOOT: tags.TBODY,
+ TH: tags.TD
+ });
+})();
+
+Element.Methods.Simulated = {
+ hasAttribute: function(element, attribute) {
+ attribute = Element._attributeTranslations.has[attribute] || attribute;
+ var node = $(element).getAttributeNode(attribute);
+ return !!(node && node.specified);
+ }
+};
+
+Element.Methods.ByTag = { };
+
+Object.extend(Element, Element.Methods);
+
+(function(div) {
+
+ if (!Prototype.BrowserFeatures.ElementExtensions && div['__proto__']) {
+ window.HTMLElement = { };
+ window.HTMLElement.prototype = div['__proto__'];
+ Prototype.BrowserFeatures.ElementExtensions = true;
+ }
+
+ div = null;
+
+})(document.createElement('div'));
+
+Element.extend = (function() {
+
+ function checkDeficiency(tagName) {
+ if (typeof window.Element != 'undefined') {
+ var proto = window.Element.prototype;
+ if (proto) {
+ var id = '_' + (Math.random()+'').slice(2),
+ el = document.createElement(tagName);
+ proto[id] = 'x';
+ var isBuggy = (el[id] !== 'x');
+ delete proto[id];
+ el = null;
+ return isBuggy;
+ }
+ }
+ return false;
+ }
+
+ function extendElementWith(element, methods) {
+ for (var property in methods) {
+ var value = methods[property];
+ if (Object.isFunction(value) && !(property in element))
+ element[property] = value.methodize();
+ }
+ }
+
+ var HTMLOBJECTELEMENT_PROTOTYPE_BUGGY = checkDeficiency('object');
+
+ if (Prototype.BrowserFeatures.SpecificElementExtensions) {
+ if (HTMLOBJECTELEMENT_PROTOTYPE_BUGGY) {
+ return function(element) {
+ if (element && typeof element._extendedByPrototype == 'undefined') {
+ var t = element.tagName;
+ if (t && (/^(?:object|applet|embed)$/i.test(t))) {
+ extendElementWith(element, Element.Methods);
+ extendElementWith(element, Element.Methods.Simulated);
+ extendElementWith(element, Element.Methods.ByTag[t.toUpperCase()]);
+ }
+ }
+ return element;
+ }
+ }
+ return Prototype.K;
+ }
+
+ var Methods = { }, ByTag = Element.Methods.ByTag;
+
+ var extend = Object.extend(function(element) {
+ if (!element || typeof element._extendedByPrototype != 'undefined' ||
+ element.nodeType != 1 || element == window) return element;
+
+ var methods = Object.clone(Methods),
+ tagName = element.tagName.toUpperCase();
+
+ if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);
+
+ extendElementWith(element, methods);
+
+ element._extendedByPrototype = Prototype.emptyFunction;
+ return element;
+
+ }, {
+ refresh: function() {
+ if (!Prototype.BrowserFeatures.ElementExtensions) {
+ Object.extend(Methods, Element.Methods);
+ Object.extend(Methods, Element.Methods.Simulated);
+ }
+ }
+ });
+
+ extend.refresh();
+ return extend;
+})();
+
+if (document.documentElement.hasAttribute) {
+ Element.hasAttribute = function(element, attribute) {
+ return element.hasAttribute(attribute);
+ };
+}
+else {
+ Element.hasAttribute = Element.Methods.Simulated.hasAttribute;
+}
+
+Element.addMethods = function(methods) {
+ var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;
+
+ if (!methods) {
+ Object.extend(Form, Form.Methods);
+ Object.extend(Form.Element, Form.Element.Methods);
+ Object.extend(Element.Methods.ByTag, {
+ "FORM": Object.clone(Form.Methods),
+ "INPUT": Object.clone(Form.Element.Methods),
+ "SELECT": Object.clone(Form.Element.Methods),
+ "TEXTAREA": Object.clone(Form.Element.Methods)
+ });
+ }
+
+ if (arguments.length == 2) {
+ var tagName = methods;
+ methods = arguments[1];
+ }
+
+ if (!tagName) Object.extend(Element.Methods, methods || { });
+ else {
+ if (Object.isArray(tagName)) tagName.each(extend);
+ else extend(tagName);
+ }
+
+ function extend(tagName) {
+ tagName = tagName.toUpperCase();
+ if (!Element.Methods.ByTag[tagName])
+ Element.Methods.ByTag[tagName] = { };
+ Object.extend(Element.Methods.ByTag[tagName], methods);
+ }
+
+ function copy(methods, destination, onlyIfAbsent) {
+ onlyIfAbsent = onlyIfAbsent || false;
+ for (var property in methods) {
+ var value = methods[property];
+ if (!Object.isFunction(value)) continue;
+ if (!onlyIfAbsent || !(property in destination))
+ destination[property] = value.methodize();
+ }
+ }
+
+ function findDOMClass(tagName) {
+ var klass;
+ var trans = {
+ "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
+ "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
+ "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
+ "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
+ "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
+ "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
+ "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
+ "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
+ "FrameSet", "IFRAME": "IFrame"
+ };
+ if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName + 'Element';
+ if (window[klass]) return window[klass];
+ klass = 'HTML' + tagName.capitalize() + 'Element';
+ if (window[klass]) return window[klass];
+
+ var element = document.createElement(tagName),
+ proto = element['__proto__'] || element.constructor.prototype;
+
+ element = null;
+ return proto;
+ }
+
+ var elementPrototype = window.HTMLElement ? HTMLElement.prototype :
+ Element.prototype;
+
+ if (F.ElementExtensions) {
+ copy(Element.Methods, elementPrototype);
+ copy(Element.Methods.Simulated, elementPrototype, true);
+ }
+
+ if (F.SpecificElementExtensions) {
+ for (var tag in Element.Methods.ByTag) {
+ var klass = findDOMClass(tag);
+ if (Object.isUndefined(klass)) continue;
+ copy(T[tag], klass.prototype);
+ }
+ }
+
+ Object.extend(Element, Element.Methods);
+ delete Element.ByTag;
+
+ if (Element.extend.refresh) Element.extend.refresh();
+ Element.cache = { };
+};
+
+
+document.viewport = {
+
+ getDimensions: function() {
+ return { width: this.getWidth(), height: this.getHeight() };
+ },
+
+ getScrollOffsets: function() {
+ return Element._returnOffset(
+ window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
+ window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
+ }
+};
+
+(function(viewport) {
+ var B = Prototype.Browser, doc = document, element, property = {};
+
+ function getRootElement() {
+ if (B.WebKit && !doc.evaluate)
+ return document;
+
+ if (B.Opera && window.parseFloat(window.opera.version()) < 9.5)
+ return document.body;
+
+ return document.documentElement;
+ }
+
+ function define(D) {
+ if (!element) element = getRootElement();
+
+ property[D] = 'client' + D;
+
+ viewport['get' + D] = function() { return element[property[D]] };
+ return viewport['get' + D]();
+ }
+
+ viewport.getWidth = define.curry('Width');
+
+ viewport.getHeight = define.curry('Height');
+})(document.viewport);
+
+
+Element.Storage = {
+ UID: 1
+};
+
+Element.addMethods({
+ getStorage: function(element) {
+ if (!(element = $(element))) return;
+
+ var uid;
+ if (element === window) {
+ uid = 0;
+ } else {
+ if (typeof element._prototypeUID === "undefined")
+ element._prototypeUID = Element.Storage.UID++;
+ uid = element._prototypeUID;
+ }
+
+ if (!Element.Storage[uid])
+ Element.Storage[uid] = $H();
+
+ return Element.Storage[uid];
+ },
+
+ store: function(element, key, value) {
+ if (!(element = $(element))) return;
+
+ if (arguments.length === 2) {
+ Element.getStorage(element).update(key);
+ } else {
+ Element.getStorage(element).set(key, value);
+ }
+
+ return element;
+ },
+
+ retrieve: function(element, key, defaultValue) {
+ if (!(element = $(element))) return;
+ var hash = Element.getStorage(element), value = hash.get(key);
+
+ if (Object.isUndefined(value)) {
+ hash.set(key, defaultValue);
+ value = defaultValue;
+ }
+
+ return value;
+ },
+
+ clone: function(element, deep) {
+ if (!(element = $(element))) return;
+ var clone = element.cloneNode(deep);
+ clone._prototypeUID = void 0;
+ if (deep) {
+ var descendants = Element.select(clone, '*'),
+ i = descendants.length;
+ while (i--) {
+ descendants[i]._prototypeUID = void 0;
+ }
+ }
+ return Element.extend(clone);
+ },
+
+ purge: function(element) {
+ if (!(element = $(element))) return;
+ purgeElement(element);
+
+ var descendants = element.getElementsByTagName('*'),
+ i = descendants.length;
+
+ while (i--) purgeElement(descendants[i]);
+
+ return null;
+ }
+});
+
+(function() {
+
+ function toDecimal(pctString) {
+ var match = pctString.match(/^(\d+)%?$/i);
+ if (!match) return null;
+ return (Number(match[1]) / 100);
+ }
+
+ function getPixelValue(value, property) {
+ if (Object.isElement(value)) {
+ element = value;
+ value = element.getStyle(property);
+ }
+ if (value === null) {
+ return null;
+ }
+
+ if ((/^(?:-)?\d+(\.\d+)?(px)?$/i).test(value)) {
+ return window.parseFloat(value);
+ }
+
+ if (/\d/.test(value) && element.runtimeStyle) {
+ var style = element.style.left, rStyle = element.runtimeStyle.left;
+ element.runtimeStyle.left = element.currentStyle.left;
+ element.style.left = value || 0;
+ value = element.style.pixelLeft;
+ element.style.left = style;
+ element.runtimeStyle.left = rStyle;
+
+ return value;
+ }
+
+ if (value.include('%')) {
+ var decimal = toDecimal(value);
+ var whole;
+ if (property.include('left') || property.include('right') ||
+ property.include('width')) {
+ whole = $(element.parentNode).measure('width');
+ } else if (property.include('top') || property.include('bottom') ||
+ property.include('height')) {
+ whole = $(element.parentNode).measure('height');
+ }
+
+ return whole * decimal;
+ }
+
+ return 0;
+ }
+
+ function toCSSPixels(number) {
+ if (Object.isString(number) && number.endsWith('px')) {
+ return number;
+ }
+ return number + 'px';
+ }
+
+ function isDisplayed(element) {
+ var originalElement = element;
+ while (element && element.parentNode) {
+ var display = element.getStyle('display');
+ if (display === 'none') {
+ return false;
+ }
+ element = $(element.parentNode);
+ }
+ return true;
+ }
+
+ var hasLayout = Prototype.K;
+ if ('currentStyle' in document.documentElement) {
+ hasLayout = function(element) {
+ if (!element.currentStyle.hasLayout) {
+ element.style.zoom = 1;
+ }
+ return element;
+ };
+ }
+
+ function cssNameFor(key) {
+ if (key.include('border')) key = key + '-width';
+ return key.camelize();
+ }
+
+ Element.Layout = Class.create(Hash, {
+ initialize: function($super, element, preCompute) {
+ $super();
+ this.element = $(element);
+
+ Element.Layout.PROPERTIES.each( function(property) {
+ this._set(property, null);
+ }, this);
+
+ if (preCompute) {
+ this._preComputing = true;
+ this._begin();
+ Element.Layout.PROPERTIES.each( this._compute, this );
+ this._end();
+ this._preComputing = false;
+ }
+ },
+
+ _set: function(property, value) {
+ return Hash.prototype.set.call(this, property, value);
+ },
+
+ set: function(property, value) {
+ throw "Properties of Element.Layout are read-only.";
+ },
+
+ get: function($super, property) {
+ var value = $super(property);
+ return value === null ? this._compute(property) : value;
+ },
+
+ _begin: function() {
+ if (this._prepared) return;
+
+ var element = this.element;
+ if (isDisplayed(element)) {
+ this._prepared = true;
+ return;
+ }
+
+ var originalStyles = {
+ position: element.style.position || '',
+ width: element.style.width || '',
+ visibility: element.style.visibility || '',
+ display: element.style.display || ''
+ };
+
+ element.store('prototype_original_styles', originalStyles);
+
+ var position = element.getStyle('position'),
+ width = element.getStyle('width');
+
+ element.setStyle({
+ position: 'absolute',
+ visibility: 'hidden',
+ display: 'block'
+ });
+
+ var positionedWidth = element.getStyle('width');
+
+ var newWidth;
+ if (width && (positionedWidth === width)) {
+ newWidth = getPixelValue(width);
+ } else if (width && (position === 'absolute' || position === 'fixed')) {
+ newWidth = getPixelValue(width);
+ } else {
+ var parent = element.parentNode, pLayout = $(parent).getLayout();
+
+ newWidth = pLayout.get('width') -
+ this.get('margin-left') -
+ this.get('border-left') -
+ this.get('padding-left') -
+ this.get('padding-right') -
+ this.get('border-right') -
+ this.get('margin-right');
+ }
+
+ element.setStyle({ width: newWidth + 'px' });
+
+ this._prepared = true;
+ },
+
+ _end: function() {
+ var element = this.element;
+ var originalStyles = element.retrieve('prototype_original_styles');
+ element.store('prototype_original_styles', null);
+ element.setStyle(originalStyles);
+ this._prepared = false;
+ },
+
+ _compute: function(property) {
+ var COMPUTATIONS = Element.Layout.COMPUTATIONS;
+ if (!(property in COMPUTATIONS)) {
+ throw "Property not found.";
+ }
+ return this._set(property, COMPUTATIONS[property].call(this, this.element));
+ },
+
+ toObject: function() {
+ var args = $A(arguments);
+ var keys = (args.length === 0) ? Element.Layout.PROPERTIES :
+ args.join(' ').split(' ');
+ var obj = {};
+ keys.each( function(key) {
+ if (!Element.Layout.PROPERTIES.include(key)) return;
+ var value = this.get(key);
+ if (value != null) obj[key] = value;
+ }, this);
+ return obj;
+ },
+
+ toHash: function() {
+ var obj = this.toObject.apply(this, arguments);
+ return new Hash(obj);
+ },
+
+ toCSS: function() {
+ var args = $A(arguments);
+ var keys = (args.length === 0) ? Element.Layout.PROPERTIES :
+ args.join(' ').split(' ');
+ var css = {};
+
+ keys.each( function(key) {
+ if (!Element.Layout.PROPERTIES.include(key)) return;
+ if (Element.Layout.COMPOSITE_PROPERTIES.include(key)) return;
+
+ var value = this.get(key);
+ if (value != null) css[cssNameFor(key)] = value + 'px';
+ }, this);
+ return css;
+ },
+
+ inspect: function() {
+ return "#<Element.Layout>";
+ }
+ });
+
+ Object.extend(Element.Layout, {
+ PROPERTIES: $w('height width top left right bottom border-left border-right border-top border-bottom padding-left padding-right padding-top padding-bottom margin-top margin-bottom margin-left margin-right padding-box-width padding-box-height border-box-width border-box-height margin-box-width margin-box-height'),
+
+ COMPOSITE_PROPERTIES: $w('padding-box-width padding-box-height margin-box-width margin-box-height border-box-width border-box-height'),
+
+ COMPUTATIONS: {
+ 'height': function(element) {
+ if (!this._preComputing) this._begin();
+
+ var bHeight = this.get('border-box-height');
+ if (bHeight <= 0) return 0;
+
+ var bTop = this.get('border-top'),
+ bBottom = this.get('border-bottom');
+
+ var pTop = this.get('padding-top'),
+ pBottom = this.get('padding-bottom');
+
+ if (!this._preComputing) this._end();
+
+ return bHeight - bTop - bBottom - pTop - pBottom;
+ },
+
+ 'width': function(element) {
+ if (!this._preComputing) this._begin();
+
+ var bWidth = this.get('border-box-width');
+ if (bWidth <= 0) return 0;
+
+ var bLeft = this.get('border-left'),
+ bRight = this.get('border-right');
+
+ var pLeft = this.get('padding-left'),
+ pRight = this.get('padding-right');
+
+ if (!this._preComputing) this._end();
+
+ return bWidth - bLeft - bRight - pLeft - pRight;
+ },
+
+ 'padding-box-height': function(element) {
+ var height = this.get('height'),
+ pTop = this.get('padding-top'),
+ pBottom = this.get('padding-bottom');
+
+ return height + pTop + pBottom;
+ },
+
+ 'padding-box-width': function(element) {
+ var width = this.get('width'),
+ pLeft = this.get('padding-left'),
+ pRight = this.get('padding-right');
+
+ return width + pLeft + pRight;
+ },
+
+ 'border-box-height': function(element) {
+ return element.offsetHeight;
+ },
+
+ 'border-box-width': function(element) {
+ return element.offsetWidth;
+ },
+
+ 'margin-box-height': function(element) {
+ var bHeight = this.get('border-box-height'),
+ mTop = this.get('margin-top'),
+ mBottom = this.get('margin-bottom');
+
+ if (bHeight <= 0) return 0;
+
+ return bHeight + mTop + mBottom;
+ },
+
+ 'margin-box-width': function(element) {
+ var bWidth = this.get('border-box-width'),
+ mLeft = this.get('margin-left'),
+ mRight = this.get('margin-right');
+
+ if (bWidth <= 0) return 0;
+
+ return bWidth + mLeft + mRight;
+ },
+
+ 'top': function(element) {
+ var offset = element.positionedOffset();
+ return offset.top;
+ },
+
+ 'bottom': function(element) {
+ var offset = element.positionedOffset(),
+ parent = element.getOffsetParent(),
+ pHeight = parent.measure('height');
+
+ var mHeight = this.get('border-box-height');
+
+ return pHeight - mHeight - offset.top;
+ },
+
+ 'left': function(element) {
+ var offset = element.positionedOffset();
+ return offset.left;
+ },
+
+ 'right': function(element) {
+ var offset = element.positionedOffset(),
+ parent = element.getOffsetParent(),
+ pWidth = parent.measure('width');
+
+ var mWidth = this.get('border-box-width');
+
+ return pWidth - mWidth - offset.left;
+ },
+
+ 'padding-top': function(element) {
+ return getPixelValue(element, 'paddingTop');
+ },
+
+ 'padding-bottom': function(element) {
+ return getPixelValue(element, 'paddingBottom');
+ },
+
+ 'padding-left': function(element) {
+ return getPixelValue(element, 'paddingLeft');
+ },
+
+ 'padding-right': function(element) {
+ return getPixelValue(element, 'paddingRight');
+ },
+
+ 'border-top': function(element) {
+ return Object.isNumber(element.clientTop) ? element.clientTop :
+ getPixelValue(element, 'borderTopWidth');
+ },
+
+ 'border-bottom': function(element) {
+ return Object.isNumber(element.clientBottom) ? element.clientBottom :
+ getPixelValue(element, 'borderBottomWidth');
+ },
+
+ 'border-left': function(element) {
+ return Object.isNumber(element.clientLeft) ? element.clientLeft :
+ getPixelValue(element, 'borderLeftWidth');
+ },
+
+ 'border-right': function(element) {
+ return Object.isNumber(element.clientRight) ? element.clientRight :
+ getPixelValue(element, 'borderRightWidth');
+ },
+
+ 'margin-top': function(element) {
+ return getPixelValue(element, 'marginTop');
+ },
+
+ 'margin-bottom': function(element) {
+ return getPixelValue(element, 'marginBottom');
+ },
+
+ 'margin-left': function(element) {
+ return getPixelValue(element, 'marginLeft');
+ },
+
+ 'margin-right': function(element) {
+ return getPixelValue(element, 'marginRight');
+ }
+ }
+ });
+
+ if ('getBoundingClientRect' in document.documentElement) {
+ Object.extend(Element.Layout.COMPUTATIONS, {
+ 'right': function(element) {
+ var parent = hasLayout(element.getOffsetParent());
+ var rect = element.getBoundingClientRect(),
+ pRect = parent.getBoundingClientRect();
+
+ return (pRect.right - rect.right).round();
+ },
+
+ 'bottom': function(element) {
+ var parent = hasLayout(element.getOffsetParent());
+ var rect = element.getBoundingClientRect(),
+ pRect = parent.getBoundingClientRect();
+
+ return (pRect.bottom - rect.bottom).round();
+ }
+ });
+ }
+
+ Element.Offset = Class.create({
+ initialize: function(left, top) {
+ this.left = left.round();
+ this.top = top.round();
+
+ this[0] = this.left;
+ this[1] = this.top;
+ },
+
+ relativeTo: function(offset) {
+ return new Element.Offset(
+ this.left - offset.left,
+ this.top - offset.top
+ );
+ },
+
+ inspect: function() {
+ return "#<Element.Offset left: #{left} top: #{top}>".interpolate(this);
+ },
+
+ toString: function() {
+ return "[#{left}, #{top}]".interpolate(this);
+ },
+
+ toArray: function() {
+ return [this.left, this.top];
+ }
+ });
+
+ function getLayout(element, preCompute) {
+ return new Element.Layout(element, preCompute);
+ }
+
+ function measure(element, property) {
+ return $(element).getLayout().get(property);
+ }
+
+ function getDimensions(element) {
+ var layout = $(element).getLayout();
+ return {
+ width: layout.get('width'),
+ height: layout.get('height')
+ };
+ }
+
+ function getOffsetParent(element) {
+ if (isDetached(element)) return $(document.body);
+
+ var isInline = (Element.getStyle(element, 'display') === 'inline');
+ if (!isInline && element.offsetParent) return $(element.offsetParent);
+ if (element === document.body) return $(element);
+
+ while ((element = element.parentNode) && element !== document.body) {
+ if (Element.getStyle(element, 'position') !== 'static') {
+ return (element.nodeName === 'HTML') ? $(document.body) : $(element);
+ }
+ }
+
+ return $(document.body);
+ }
+
+
+ function cumulativeOffset(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ } while (element);
+ return new Element.Offset(valueL, valueT);
+ }
+
+ function positionedOffset(element) {
+ var layout = element.getLayout();
+
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ element = element.offsetParent;
+ if (element) {
+ if (isBody(element)) break;
+ var p = Element.getStyle(element, 'position');
+ if (p !== 'static') break;
+ }
+ } while (element);
+
+ valueL -= layout.get('margin-top');
+ valueT -= layout.get('margin-left');
+
+ return new Element.Offset(valueL, valueT);
+ }
+
+ function cumulativeScrollOffset(element) {
+ var valueT = 0, valueL = 0;
+ do {
+ valueT += element.scrollTop || 0;
+ valueL += element.scrollLeft || 0;
+ element = element.parentNode;
+ } while (element);
+ return new Element.Offset(valueL, valueT);
+ }
+
+ function viewportOffset(forElement) {
+ var valueT = 0, valueL = 0, docBody = document.body;
+
+ var element = forElement;
+ do {
+ valueT += element.offsetTop || 0;
+ valueL += element.offsetLeft || 0;
+ if (element.offsetParent == docBody &&
+ Element.getStyle(element, 'position') == 'absolute') break;
+ } while (element = element.offsetParent);
+
+ element = forElement;
+ do {
+ if (element != docBody) {
+ valueT -= element.scrollTop || 0;
+ valueL -= element.scrollLeft || 0;
+ }
+ } while (element = element.parentNode);
+ return new Element.Offset(valueL, valueT);
+ }
+
+ function absolutize(element) {
+ element = $(element);
+
+ if (Element.getStyle(element, 'position') === 'absolute') {
+ return element;
+ }
+
+ var offsetParent = getOffsetParent(element);
+ var eOffset = element.viewportOffset(),
+ pOffset = offsetParent.viewportOffset();
+
+ var offset = eOffset.relativeTo(pOffset);
+ var layout = element.getLayout();
+
+ element.store('prototype_absolutize_original_styles', {
+ left: element.getStyle('left'),
+ top: element.getStyle('top'),
+ width: element.getStyle('width'),
+ height: element.getStyle('height')
+ });
+
+ element.setStyle({
+ position: 'absolute',
+ top: offset.top + 'px',
+ left: offset.left + 'px',
+ width: layout.get('width') + 'px',
+ height: layout.get('height') + 'px'
+ });
+
+ return element;
+ }
+
+ function relativize(element) {
+ element = $(element);
+ if (Element.getStyle(element, 'position') === 'relative') {
+ return element;
+ }
+
+ var originalStyles =
+ element.retrieve('prototype_absolutize_original_styles');
+
+ if (originalStyles) element.setStyle(originalStyles);
+ return element;
+ }
+
+ Element.addMethods({
+ getLayout: getLayout,
+ measure: measure,
+ getDimensions: getDimensions,
+ getOffsetParent: getOffsetParent,
+ cumulativeOffset: cumulativeOffset,
+ positionedOffset: positionedOffset,
+ cumulativeScrollOffset: cumulativeScrollOffset,
+ viewportOffset: viewportOffset,
+ absolutize: absolutize,
+ relativize: relativize
+ });
+
+ function isBody(element) {
+ return element.nodeName.toUpperCase() === 'BODY';
+ }
+
+ function isDetached(element) {
+ return element !== document.body &&
+ !Element.descendantOf(element, document.body);
+ }
+
+ if ('getBoundingClientRect' in document.documentElement) {
+ Element.addMethods({
+ viewportOffset: function(element) {
+ element = $(element);
+ if (isDetached(element)) return new Element.Offset(0, 0);
+
+ var rect = element.getBoundingClientRect(),
+ docEl = document.documentElement;
+ return new Element.Offset(rect.left - docEl.clientLeft,
+ rect.top - docEl.clientTop);
+ },
+
+ positionedOffset: function(element) {
+ element = $(element);
+ var parent = element.getOffsetParent();
+ if (isDetached(element)) return new Element.Offset(0, 0);
+
+ if (element.offsetParent &&
+ element.offsetParent.nodeName.toUpperCase() === 'HTML') {
+ return positionedOffset(element);
+ }
+
+ var eOffset = element.viewportOffset(),
+ pOffset = isBody(parent) ? viewportOffset(parent) :
+ parent.viewportOffset();
+ var retOffset = eOffset.relativeTo(pOffset);
+
+ var layout = element.getLayout();
+ var top = retOffset.top - layout.get('margin-top');
+ var left = retOffset.left - layout.get('margin-left');
+
+ return new Element.Offset(left, top);
+ }
+ });
+ }
+})();
+window.$$ = function() {
+ var expression = $A(arguments).join(', ');
+ return Prototype.Selector.select(expression, document);
+};
+
+Prototype.Selector = (function() {
+
+ function select() {
+ throw new Error('Method "Prototype.Selector.select" must be defined.');
+ }
+
+ function match() {
+ throw new Error('Method "Prototype.Selector.match" must be defined.');
+ }
+
+ function find(elements, expression, index) {
+ index = index || 0;
+ var match = Prototype.Selector.match, length = elements.length, matchIndex = 0, i;
+
+ for (i = 0; i < length; i++) {
+ if (match(elements[i], expression) && index == matchIndex++) {
+ return Element.extend(elements[i]);
+ }
+ }
+ }
+
+ function extendElements(elements) {
+ for (var i = 0, length = elements.length; i < length; i++) {
+ Element.extend(elements[i]);
+ }
+ return elements;
+ }
+
+
+ var K = Prototype.K;
+
+ return {
+ select: select,
+ match: match,
+ find: find,
+ extendElements: (Element.extend === K) ? K : extendElements,
+ extendElement: Element.extend
+ };
+})();
+Prototype._original_property = window.Sizzle;
+/*!
+ * Sizzle CSS Selector Engine - v1.0
+ * Copyright 2009, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ * More information: http://sizzlejs.com/
+ */
+(function(){
+
+var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
+ done = 0,
+ toString = Object.prototype.toString,
+ hasDuplicate = false,
+ baseHasDuplicate = true;
+
+[0, 0].sort(function(){
+ baseHasDuplicate = false;
+ return 0;
+});
+
+var Sizzle = function(selector, context, results, seed) {
+ results = results || [];
+ var origContext = context = context || document;
+
+ if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
+ return [];
+ }
+
+ if ( !selector || typeof selector !== "string" ) {
+ return results;
+ }
+
+ var parts = [], m, set, checkSet, check, mode, extra, prune = true, contextXML = isXML(context),
+ soFar = selector;
+
+ while ( (chunker.exec(""), m = chunker.exec(soFar)) !== null ) {
+ soFar = m[3];
+
+ parts.push( m[1] );
+
+ if ( m[2] ) {
+ extra = m[3];
+ break;
+ }
+ }
+
+ if ( parts.length > 1 && origPOS.exec( selector ) ) {
+ if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
+ set = posProcess( parts[0] + parts[1], context );
+ } else {
+ set = Expr.relative[ parts[0] ] ?
+ [ context ] :
+ Sizzle( parts.shift(), context );
+
+ while ( parts.length ) {
+ selector = parts.shift();
+
+ if ( Expr.relative[ selector ] )
+ selector += parts.shift();
+
+ set = posProcess( selector, set );
+ }
+ }
+ } else {
+ if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
+ Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {
+ var ret = Sizzle.find( parts.shift(), context, contextXML );
+ context = ret.expr ? Sizzle.filter( ret.expr, ret.set )[0] : ret.set[0];
+ }
+
+ if ( context ) {
+ var ret = seed ?
+ { expr: parts.pop(), set: makeArray(seed) } :
+ Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
+ set = ret.expr ? Sizzle.filter( ret.expr, ret.set ) : ret.set;
+
+ if ( parts.length > 0 ) {
+ checkSet = makeArray(set);
+ } else {
+ prune = false;
+ }
+
+ while ( parts.length ) {
+ var cur = parts.pop(), pop = cur;
+
+ if ( !Expr.relative[ cur ] ) {
+ cur = "";
+ } else {
+ pop = parts.pop();
+ }
+
+ if ( pop == null ) {
+ pop = context;
+ }
+
+ Expr.relative[ cur ]( checkSet, pop, contextXML );
+ }
+ } else {
+ checkSet = parts = [];
+ }
+ }
+
+ if ( !checkSet ) {
+ checkSet = set;
+ }
+
+ if ( !checkSet ) {
+ throw "Syntax error, unrecognized expression: " + (cur || selector);
+ }
+
+ if ( toString.call(checkSet) === "[object Array]" ) {
+ if ( !prune ) {
+ results.push.apply( results, checkSet );
+ } else if ( context && context.nodeType === 1 ) {
+ for ( var i = 0; checkSet[i] != null; i++ ) {
+ if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && contains(context, checkSet[i])) ) {
+ results.push( set[i] );
+ }
+ }
+ } else {
+ for ( var i = 0; checkSet[i] != null; i++ ) {
+ if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
+ results.push( set[i] );
+ }
+ }
+ }
+ } else {
+ makeArray( checkSet, results );
+ }
+
+ if ( extra ) {
+ Sizzle( extra, origContext, results, seed );
+ Sizzle.uniqueSort( results );
+ }
+
+ return results;
+};
+
+Sizzle.uniqueSort = function(results){
+ if ( sortOrder ) {
+ hasDuplicate = baseHasDuplicate;
+ results.sort(sortOrder);
+
+ if ( hasDuplicate ) {
+ for ( var i = 1; i < results.length; i++ ) {
+ if ( results[i] === results[i-1] ) {
+ results.splice(i--, 1);
+ }
+ }
+ }
+ }
+
+ return results;
+};
+
+Sizzle.matches = function(expr, set){
+ return Sizzle(expr, null, null, set);
+};
+
+Sizzle.find = function(expr, context, isXML){
+ var set, match;
+
+ if ( !expr ) {
+ return [];
+ }
+
+ for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
+ var type = Expr.order[i], match;
+
+ if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
+ var left = match[1];
+ match.splice(1,1);
+
+ if ( left.substr( left.length - 1 ) !== "\\" ) {
+ match[1] = (match[1] || "").replace(/\\/g, "");
+ set = Expr.find[ type ]( match, context, isXML );
+ if ( set != null ) {
+ expr = expr.replace( Expr.match[ type ], "" );
+ break;
+ }
+ }
+ }
+ }
+
+ if ( !set ) {
+ set = context.getElementsByTagName("*");
+ }
+
+ return {set: set, expr: expr};
+};
+
+Sizzle.filter = function(expr, set, inplace, not){
+ var old = expr, result = [], curLoop = set, match, anyFound,
+ isXMLFilter = set && set[0] && isXML(set[0]);
+
+ while ( expr && set.length ) {
+ for ( var type in Expr.filter ) {
+ if ( (match = Expr.match[ type ].exec( expr )) != null ) {
+ var filter = Expr.filter[ type ], found, item;
+ anyFound = false;
+
+ if ( curLoop == result ) {
+ result = [];
+ }
+
+ if ( Expr.preFilter[ type ] ) {
+ match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
+
+ if ( !match ) {
+ anyFound = found = true;
+ } else if ( match === true ) {
+ continue;
+ }
+ }
+
+ if ( match ) {
+ for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
+ if ( item ) {
+ found = filter( item, match, i, curLoop );
+ var pass = not ^ !!found;
+
+ if ( inplace && found != null ) {
+ if ( pass ) {
+ anyFound = true;
+ } else {
+ curLoop[i] = false;
+ }
+ } else if ( pass ) {
+ result.push( item );
+ anyFound = true;
+ }
+ }
+ }
+ }
+
+ if ( found !== undefined ) {
+ if ( !inplace ) {
+ curLoop = result;
+ }
+
+ expr = expr.replace( Expr.match[ type ], "" );
+
+ if ( !anyFound ) {
+ return [];
+ }
+
+ break;
+ }
+ }
+ }
+
+ if ( expr == old ) {
+ if ( anyFound == null ) {
+ throw "Syntax error, unrecognized expression: " + expr;
+ } else {
+ break;
+ }
+ }
+
+ old = expr;
+ }
+
+ return curLoop;
+};
+
+var Expr = Sizzle.selectors = {
+ order: [ "ID", "NAME", "TAG" ],
+ match: {
+ ID: /#((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
+ CLASS: /\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
+ NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,
+ ATTR: /\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
+ TAG: /^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,
+ CHILD: /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,
+ POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,
+ PSEUDO: /:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/
+ },
+ leftMatch: {},
+ attrMap: {
+ "class": "className",
+ "for": "htmlFor"
+ },
+ attrHandle: {
+ href: function(elem){
+ return elem.getAttribute("href");
+ }
+ },
+ relative: {
+ "+": function(checkSet, part, isXML){
+ var isPartStr = typeof part === "string",
+ isTag = isPartStr && !/\W/.test(part),
+ isPartStrNotTag = isPartStr && !isTag;
+
+ if ( isTag && !isXML ) {
+ part = part.toUpperCase();
+ }
+
+ for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
+ if ( (elem = checkSet[i]) ) {
+ while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
+
+ checkSet[i] = isPartStrNotTag || elem && elem.nodeName === part ?
+ elem || false :
+ elem === part;
+ }
+ }
+
+ if ( isPartStrNotTag ) {
+ Sizzle.filter( part, checkSet, true );
+ }
+ },
+ ">": function(checkSet, part, isXML){
+ var isPartStr = typeof part === "string";
+
+ if ( isPartStr && !/\W/.test(part) ) {
+ part = isXML ? part : part.toUpperCase();
+
+ for ( var i = 0, l = checkSet.length; i < l; i++ ) {
+ var elem = checkSet[i];
+ if ( elem ) {
+ var parent = elem.parentNode;
+ checkSet[i] = parent.nodeName === part ? parent : false;
+ }
+ }
+ } else {
+ for ( var i = 0, l = checkSet.length; i < l; i++ ) {
+ var elem = checkSet[i];
+ if ( elem ) {
+ checkSet[i] = isPartStr ?
+ elem.parentNode :
+ elem.parentNode === part;
+ }
+ }
+
+ if ( isPartStr ) {
+ Sizzle.filter( part, checkSet, true );
+ }
+ }
+ },
+ "": function(checkSet, part, isXML){
+ var doneName = done++, checkFn = dirCheck;
+
+ if ( !/\W/.test(part) ) {
+ var nodeCheck = part = isXML ? part : part.toUpperCase();
+ checkFn = dirNodeCheck;
+ }
+
+ checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML);
+ },
+ "~": function(checkSet, part, isXML){
+ var doneName = done++, checkFn = dirCheck;
+
+ if ( typeof part === "string" && !/\W/.test(part) ) {
+ var nodeCheck = part = isXML ? part : part.toUpperCase();
+ checkFn = dirNodeCheck;
+ }
+
+ checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML);
+ }
+ },
+ find: {
+ ID: function(match, context, isXML){
+ if ( typeof context.getElementById !== "undefined" && !isXML ) {
+ var m = context.getElementById(match[1]);
+ return m ? [m] : [];
+ }
+ },
+ NAME: function(match, context, isXML){
+ if ( typeof context.getElementsByName !== "undefined" ) {
+ var ret = [], results = context.getElementsByName(match[1]);
+
+ for ( var i = 0, l = results.length; i < l; i++ ) {
+ if ( results[i].getAttribute("name") === match[1] ) {
+ ret.push( results[i] );
+ }
+ }
+
+ return ret.length === 0 ? null : ret;
+ }
+ },
+ TAG: function(match, context){
+ return context.getElementsByTagName(match[1]);
+ }
+ },
+ preFilter: {
+ CLASS: function(match, curLoop, inplace, result, not, isXML){
+ match = " " + match[1].replace(/\\/g, "") + " ";
+
+ if ( isXML ) {
+ return match;
+ }
+
+ for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
+ if ( elem ) {
+ if ( not ^ (elem.className && (" " + elem.className + " ").indexOf(match) >= 0) ) {
+ if ( !inplace )
+ result.push( elem );
+ } else if ( inplace ) {
+ curLoop[i] = false;
+ }
+ }
+ }
+
+ return false;
+ },
+ ID: function(match){
+ return match[1].replace(/\\/g, "");
+ },
+ TAG: function(match, curLoop){
+ for ( var i = 0; curLoop[i] === false; i++ ){}
+ return curLoop[i] && isXML(curLoop[i]) ? match[1] : match[1].toUpperCase();
+ },
+ CHILD: function(match){
+ if ( match[1] == "nth" ) {
+ var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(
+ match[2] == "even" && "2n" || match[2] == "odd" && "2n+1" ||
+ !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
+
+ match[2] = (test[1] + (test[2] || 1)) - 0;
+ match[3] = test[3] - 0;
+ }
+
+ match[0] = done++;
+
+ return match;
+ },
+ ATTR: function(match, curLoop, inplace, result, not, isXML){
+ var name = match[1].replace(/\\/g, "");
+
+ if ( !isXML && Expr.attrMap[name] ) {
+ match[1] = Expr.attrMap[name];
+ }
+
+ if ( match[2] === "~=" ) {
+ match[4] = " " + match[4] + " ";
+ }
+
+ return match;
+ },
+ PSEUDO: function(match, curLoop, inplace, result, not){
+ if ( match[1] === "not" ) {
+ if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
+ match[3] = Sizzle(match[3], null, null, curLoop);
+ } else {
+ var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
+ if ( !inplace ) {
+ result.push.apply( result, ret );
+ }
+ return false;
+ }
+ } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {
+ return true;
+ }
+
+ return match;
+ },
+ POS: function(match){
+ match.unshift( true );
+ return match;
+ }
+ },
+ filters: {
+ enabled: function(elem){
+ return elem.disabled === false && elem.type !== "hidden";
+ },
+ disabled: function(elem){
+ return elem.disabled === true;
+ },
+ checked: function(elem){
+ return elem.checked === true;
+ },
+ selected: function(elem){
+ elem.parentNode.selectedIndex;
+ return elem.selected === true;
+ },
+ parent: function(elem){
+ return !!elem.firstChild;
+ },
+ empty: function(elem){
+ return !elem.firstChild;
+ },
+ has: function(elem, i, match){
+ return !!Sizzle( match[3], elem ).length;
+ },
+ header: function(elem){
+ return /h\d/i.test( elem.nodeName );
+ },
+ text: function(elem){
+ return "text" === elem.type;
+ },
+ radio: function(elem){
+ return "radio" === elem.type;
+ },
+ checkbox: function(elem){
+ return "checkbox" === elem.type;
+ },
+ file: function(elem){
+ return "file" === elem.type;
+ },
+ password: function(elem){
+ return "password" === elem.type;
+ },
+ submit: function(elem){
+ return "submit" === elem.type;
+ },
+ image: function(elem){
+ return "image" === elem.type;
+ },
+ reset: function(elem){
+ return "reset" === elem.type;
+ },
+ button: function(elem){
+ return "button" === elem.type || elem.nodeName.toUpperCase() === "BUTTON";
+ },
+ input: function(elem){
+ return /input|select|textarea|button/i.test(elem.nodeName);
+ }
+ },
+ setFilters: {
+ first: function(elem, i){
+ return i === 0;
+ },
+ last: function(elem, i, match, array){
+ return i === array.length - 1;
+ },
+ even: function(elem, i){
+ return i % 2 === 0;
+ },
+ odd: function(elem, i){
+ return i % 2 === 1;
+ },
+ lt: function(elem, i, match){
+ return i < match[3] - 0;
+ },
+ gt: function(elem, i, match){
+ return i > match[3] - 0;
+ },
+ nth: function(elem, i, match){
+ return match[3] - 0 == i;
+ },
+ eq: function(elem, i, match){
+ return match[3] - 0 == i;
+ }
+ },
+ filter: {
+ PSEUDO: function(elem, match, i, array){
+ var name = match[1], filter = Expr.filters[ name ];
+
+ if ( filter ) {
+ return filter( elem, i, match, array );
+ } else if ( name === "contains" ) {
+ return (elem.textContent || elem.innerText || "").indexOf(match[3]) >= 0;
+ } else if ( name === "not" ) {
+ var not = match[3];
+
+ for ( var i = 0, l = not.length; i < l; i++ ) {
+ if ( not[i] === elem ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ },
+ CHILD: function(elem, match){
+ var type = match[1], node = elem;
+ switch (type) {
+ case 'only':
+ case 'first':
+ while ( (node = node.previousSibling) ) {
+ if ( node.nodeType === 1 ) return false;
+ }
+ if ( type == 'first') return true;
+ node = elem;
+ case 'last':
+ while ( (node = node.nextSibling) ) {
+ if ( node.nodeType === 1 ) return false;
+ }
+ return true;
+ case 'nth':
+ var first = match[2], last = match[3];
+
+ if ( first == 1 && last == 0 ) {
+ return true;
+ }
+
+ var doneName = match[0],
+ parent = elem.parentNode;
+
+ if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) {
+ var count = 0;
+ for ( node = parent.firstChild; node; node = node.nextSibling ) {
+ if ( node.nodeType === 1 ) {
+ node.nodeIndex = ++count;
+ }
+ }
+ parent.sizcache = doneName;
+ }
+
+ var diff = elem.nodeIndex - last;
+ if ( first == 0 ) {
+ return diff == 0;
+ } else {
+ return ( diff % first == 0 && diff / first >= 0 );
+ }
+ }
+ },
+ ID: function(elem, match){
+ return elem.nodeType === 1 && elem.getAttribute("id") === match;
+ },
+ TAG: function(elem, match){
+ return (match === "*" && elem.nodeType === 1) || elem.nodeName === match;
+ },
+ CLASS: function(elem, match){
+ return (" " + (elem.className || elem.getAttribute("class")) + " ")
+ .indexOf( match ) > -1;
+ },
+ ATTR: function(elem, match){
+ var name = match[1],
+ result = Expr.attrHandle[ name ] ?
+ Expr.attrHandle[ name ]( elem ) :
+ elem[ name ] != null ?
+ elem[ name ] :
+ elem.getAttribute( name ),
+ value = result + "",
+ type = match[2],
+ check = match[4];
+
+ return result == null ?
+ type === "!=" :
+ type === "=" ?
+ value === check :
+ type === "*=" ?
+ value.indexOf(check) >= 0 :
+ type === "~=" ?
+ (" " + value + " ").indexOf(check) >= 0 :
+ !check ?
+ value && result !== false :
+ type === "!=" ?
+ value != check :
+ type === "^=" ?
+ value.indexOf(check) === 0 :
+ type === "$=" ?
+ value.substr(value.length - check.length) === check :
+ type === "|=" ?
+ value === check || value.substr(0, check.length + 1) === check + "-" :
+ false;
+ },
+ POS: function(elem, match, i, array){
+ var name = match[2], filter = Expr.setFilters[ name ];
+
+ if ( filter ) {
+ return filter( elem, i, match, array );
+ }
+ }
+ }
+};
+
+var origPOS = Expr.match.POS;
+
+for ( var type in Expr.match ) {
+ Expr.match[ type ] = new RegExp( Expr.match[ type ].source + /(?![^\[]*\])(?![^\(]*\))/.source );
+ Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source );
+}
+
+var makeArray = function(array, results) {
+ array = Array.prototype.slice.call( array, 0 );
+
+ if ( results ) {
+ results.push.apply( results, array );
+ return results;
+ }
+
+ return array;
+};
+
+try {
+ Array.prototype.slice.call( document.documentElement.childNodes, 0 );
+
+} catch(e){
+ makeArray = function(array, results) {
+ var ret = results || [];
+
+ if ( toString.call(array) === "[object Array]" ) {
+ Array.prototype.push.apply( ret, array );
+ } else {
+ if ( typeof array.length === "number" ) {
+ for ( var i = 0, l = array.length; i < l; i++ ) {
+ ret.push( array[i] );
+ }
+ } else {
+ for ( var i = 0; array[i]; i++ ) {
+ ret.push( array[i] );
+ }
+ }
+ }
+
+ return ret;
+ };
+}
+
+var sortOrder;
+
+if ( document.documentElement.compareDocumentPosition ) {
+ sortOrder = function( a, b ) {
+ if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
+ if ( a == b ) {
+ hasDuplicate = true;
+ }
+ return 0;
+ }
+
+ var ret = a.compareDocumentPosition(b) & 4 ? -1 : a === b ? 0 : 1;
+ if ( ret === 0 ) {
+ hasDuplicate = true;
+ }
+ return ret;
+ };
+} else if ( "sourceIndex" in document.documentElement ) {
+ sortOrder = function( a, b ) {
+ if ( !a.sourceIndex || !b.sourceIndex ) {
+ if ( a == b ) {
+ hasDuplicate = true;
+ }
+ return 0;
+ }
+
+ var ret = a.sourceIndex - b.sourceIndex;
+ if ( ret === 0 ) {
+ hasDuplicate = true;
+ }
+ return ret;
+ };
+} else if ( document.createRange ) {
+ sortOrder = function( a, b ) {
+ if ( !a.ownerDocument || !b.ownerDocument ) {
+ if ( a == b ) {
+ hasDuplicate = true;
+ }
+ return 0;
+ }
+
+ var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange();
+ aRange.setStart(a, 0);
+ aRange.setEnd(a, 0);
+ bRange.setStart(b, 0);
+ bRange.setEnd(b, 0);
+ var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange);
+ if ( ret === 0 ) {
+ hasDuplicate = true;
+ }
+ return ret;
+ };
+}
+
+(function(){
+ var form = document.createElement("div"),
+ id = "script" + (new Date).getTime();
+ form.innerHTML = "<a name='" + id + "'/>";
+
+ var root = document.documentElement;
+ root.insertBefore( form, root.firstChild );
+
+ if ( !!document.getElementById( id ) ) {
+ Expr.find.ID = function(match, context, isXML){
+ if ( typeof context.getElementById !== "undefined" && !isXML ) {
+ var m = context.getElementById(match[1]);
+ return m ? m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? [m] : undefined : [];
+ }
+ };
+
+ Expr.filter.ID = function(elem, match){
+ var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
+ return elem.nodeType === 1 && node && node.nodeValue === match;
+ };
+ }
+
+ root.removeChild( form );
+ root = form = null; // release memory in IE
+})();
+
+(function(){
+
+ var div = document.createElement("div");
+ div.appendChild( document.createComment("") );
+
+ if ( div.getElementsByTagName("*").length > 0 ) {
+ Expr.find.TAG = function(match, context){
+ var results = context.getElementsByTagName(match[1]);
+
+ if ( match[1] === "*" ) {
+ var tmp = [];
+
+ for ( var i = 0; results[i]; i++ ) {
+ if ( results[i].nodeType === 1 ) {
+ tmp.push( results[i] );
+ }
+ }
+
+ results = tmp;
+ }
+
+ return results;
+ };
+ }
+
+ div.innerHTML = "<a href='#'></a>";
+ if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
+ div.firstChild.getAttribute("href") !== "#" ) {
+ Expr.attrHandle.href = function(elem){
+ return elem.getAttribute("href", 2);
+ };
+ }
+
+ div = null; // release memory in IE
+})();
+
+if ( document.querySelectorAll ) (function(){
+ var oldSizzle = Sizzle, div = document.createElement("div");
+ div.innerHTML = "<p class='TEST'></p>";
+
+ if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
+ return;
+ }
+
+ Sizzle = function(query, context, extra, seed){
+ context = context || document;
+
+ if ( !seed && context.nodeType === 9 && !isXML(context) ) {
+ try {
+ return makeArray( context.querySelectorAll(query), extra );
+ } catch(e){}
+ }
+
+ return oldSizzle(query, context, extra, seed);
+ };
+
+ for ( var prop in oldSizzle ) {
+ Sizzle[ prop ] = oldSizzle[ prop ];
+ }
+
+ div = null; // release memory in IE
+})();
+
+if ( document.getElementsByClassName && document.documentElement.getElementsByClassName ) (function(){
+ var div = document.createElement("div");
+ div.innerHTML = "<div class='test e'></div><div class='test'></div>";
+
+ if ( div.getElementsByClassName("e").length === 0 )
+ return;
+
+ div.lastChild.className = "e";
+
+ if ( div.getElementsByClassName("e").length === 1 )
+ return;
+
+ Expr.order.splice(1, 0, "CLASS");
+ Expr.find.CLASS = function(match, context, isXML) {
+ if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
+ return context.getElementsByClassName(match[1]);
+ }
+ };
+
+ div = null; // release memory in IE
+})();
+
+function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
+ var sibDir = dir == "previousSibling" && !isXML;
+ for ( var i = 0, l = checkSet.length; i < l; i++ ) {
+ var elem = checkSet[i];
+ if ( elem ) {
+ if ( sibDir && elem.nodeType === 1 ){
+ elem.sizcache = doneName;
+ elem.sizset = i;
+ }
+ elem = elem[dir];
+ var match = false;
+
+ while ( elem ) {
+ if ( elem.sizcache === doneName ) {
+ match = checkSet[elem.sizset];
+ break;
+ }
+
+ if ( elem.nodeType === 1 && !isXML ){
+ elem.sizcache = doneName;
+ elem.sizset = i;
+ }
+
+ if ( elem.nodeName === cur ) {
+ match = elem;
+ break;
+ }
+
+ elem = elem[dir];
+ }
+
+ checkSet[i] = match;
+ }
+ }
+}
+
+function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
+ var sibDir = dir == "previousSibling" && !isXML;
+ for ( var i = 0, l = checkSet.length; i < l; i++ ) {
+ var elem = checkSet[i];
+ if ( elem ) {
+ if ( sibDir && elem.nodeType === 1 ) {
+ elem.sizcache = doneName;
+ elem.sizset = i;
+ }
+ elem = elem[dir];
+ var match = false;
+
+ while ( elem ) {
+ if ( elem.sizcache === doneName ) {
+ match = checkSet[elem.sizset];
+ break;
+ }
+
+ if ( elem.nodeType === 1 ) {
+ if ( !isXML ) {
+ elem.sizcache = doneName;
+ elem.sizset = i;
+ }
+ if ( typeof cur !== "string" ) {
+ if ( elem === cur ) {
+ match = true;
+ break;
+ }
+
+ } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
+ match = elem;
+ break;
+ }
+ }
+
+ elem = elem[dir];
+ }
+
+ checkSet[i] = match;
+ }
+ }
+}
+
+var contains = document.compareDocumentPosition ? function(a, b){
+ return a.compareDocumentPosition(b) & 16;
+} : function(a, b){
+ return a !== b && (a.contains ? a.contains(b) : true);
+};
+
+var isXML = function(elem){
+ return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" ||
+ !!elem.ownerDocument && elem.ownerDocument.documentElement.nodeName !== "HTML";
+};
+
+var posProcess = function(selector, context){
+ var tmpSet = [], later = "", match,
+ root = context.nodeType ? [context] : context;
+
+ while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
+ later += match[0];
+ selector = selector.replace( Expr.match.PSEUDO, "" );
+ }
+
+ selector = Expr.relative[selector] ? selector + "*" : selector;
+
+ for ( var i = 0, l = root.length; i < l; i++ ) {
+ Sizzle( selector, root[i], tmpSet );
+ }
+
+ return Sizzle.filter( later, tmpSet );
+};
+
+
+window.Sizzle = Sizzle;
+
+})();
+
+;(function(engine) {
+ var extendElements = Prototype.Selector.extendElements;
+
+ function select(selector, scope) {
+ return extendElements(engine(selector, scope || document));
+ }
+
+ function match(element, selector) {
+ return engine.matches(selector, [element]).length == 1;
+ }
+
+ Prototype.Selector.engine = engine;
+ Prototype.Selector.select = select;
+ Prototype.Selector.match = match;
+})(Sizzle);
+
+window.Sizzle = Prototype._original_property;
+delete Prototype._original_property;
+
+var Form = {
+ reset: function(form) {
+ form = $(form);
+ form.reset();
+ return form;
+ },
+
+ serializeElements: function(elements, options) {
+ if (typeof options != 'object') options = { hash: !!options };
+ else if (Object.isUndefined(options.hash)) options.hash = true;
+ var key, value, submitted = false, submit = options.submit;
+
+ var data = elements.inject({ }, function(result, element) {
+ if (!element.disabled && element.name) {
+ key = element.name; value = $(element).getValue();
+ if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted &&
+ submit !== false && (!submit || key == submit) && (submitted = true)))) {
+ if (key in result) {
+ if (!Object.isArray(result[key])) result[key] = [result[key]];
+ result[key].push(value);
+ }
+ else result[key] = value;
+ }
+ }
+ return result;
+ });
+
+ return options.hash ? data : Object.toQueryString(data);
+ }
+};
+
+Form.Methods = {
+ serialize: function(form, options) {
+ return Form.serializeElements(Form.getElements(form), options);
+ },
+
+ getElements: function(form) {
+ var elements = $(form).getElementsByTagName('*'),
+ element,
+ arr = [ ],
+ serializers = Form.Element.Serializers;
+ for (var i = 0; element = elements[i]; i++) {
+ arr.push(element);
+ }
+ return arr.inject([], function(elements, child) {
+ if (serializers[child.tagName.toLowerCase()])
+ elements.push(Element.extend(child));
+ return elements;
+ })
+ },
+
+ getInputs: function(form, typeName, name) {
+ form = $(form);
+ var inputs = form.getElementsByTagName('input');
+
+ if (!typeName && !name) return $A(inputs).map(Element.extend);
+
+ for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
+ var input = inputs[i];
+ if ((typeName && input.type != typeName) || (name && input.name != name))
+ continue;
+ matchingInputs.push(Element.extend(input));
+ }
+
+ return matchingInputs;
+ },
+
+ disable: function(form) {
+ form = $(form);
+ Form.getElements(form).invoke('disable');
+ return form;
+ },
+
+ enable: function(form) {
+ form = $(form);
+ Form.getElements(form).invoke('enable');
+ return form;
+ },
+
+ findFirstElement: function(form) {
+ var elements = $(form).getElements().findAll(function(element) {
+ return 'hidden' != element.type && !element.disabled;
+ });
+ var firstByIndex = elements.findAll(function(element) {
+ return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
+ }).sortBy(function(element) { return element.tabIndex }).first();
+
+ return firstByIndex ? firstByIndex : elements.find(function(element) {
+ return /^(?:input|select|textarea)$/i.test(element.tagName);
+ });
+ },
+
+ focusFirstElement: function(form) {
+ form = $(form);
+ form.findFirstElement().activate();
+ return form;
+ },
+
+ request: function(form, options) {
+ form = $(form), options = Object.clone(options || { });
+
+ var params = options.parameters, action = form.readAttribute('action') || '';
+ if (action.blank()) action = window.location.href;
+ options.parameters = form.serialize(true);
+
+ if (params) {
+ if (Object.isString(params)) params = params.toQueryParams();
+ Object.extend(options.parameters, params);
+ }
+
+ if (form.hasAttribute('method') && !options.method)
+ options.method = form.method;
+
+ return new Ajax.Request(action, options);
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+
+Form.Element = {
+ focus: function(element) {
+ $(element).focus();
+ return element;
+ },
+
+ select: function(element) {
+ $(element).select();
+ return element;
+ }
+};
+
+Form.Element.Methods = {
+
+ serialize: function(element) {
+ element = $(element);
+ if (!element.disabled && element.name) {
+ var value = element.getValue();
+ if (value != undefined) {
+ var pair = { };
+ pair[element.name] = value;
+ return Object.toQueryString(pair);
+ }
+ }
+ return '';
+ },
+
+ getValue: function(element) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ return Form.Element.Serializers[method](element);
+ },
+
+ setValue: function(element, value) {
+ element = $(element);
+ var method = element.tagName.toLowerCase();
+ Form.Element.Serializers[method](element, value);
+ return element;
+ },
+
+ clear: function(element) {
+ $(element).value = '';
+ return element;
+ },
+
+ present: function(element) {
+ return $(element).value != '';
+ },
+
+ activate: function(element) {
+ element = $(element);
+ try {
+ element.focus();
+ if (element.select && (element.tagName.toLowerCase() != 'input' ||
+ !(/^(?:button|reset|submit)$/i.test(element.type))))
+ element.select();
+ } catch (e) { }
+ return element;
+ },
+
+ disable: function(element) {
+ element = $(element);
+ element.disabled = true;
+ return element;
+ },
+
+ enable: function(element) {
+ element = $(element);
+ element.disabled = false;
+ return element;
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+var Field = Form.Element;
+
+var $F = Form.Element.Methods.getValue;
+
+/*--------------------------------------------------------------------------*/
+
+Form.Element.Serializers = {
+ input: function(element, value) {
+ switch (element.type.toLowerCase()) {
+ case 'checkbox':
+ case 'radio':
+ return Form.Element.Serializers.inputSelector(element, value);
+ default:
+ return Form.Element.Serializers.textarea(element, value);
+ }
+ },
+
+ inputSelector: function(element, value) {
+ if (Object.isUndefined(value)) return element.checked ? element.value : null;
+ else element.checked = !!value;
+ },
+
+ textarea: function(element, value) {
+ if (Object.isUndefined(value)) return element.value;
+ else element.value = value;
+ },
+
+ select: function(element, value) {
+ if (Object.isUndefined(value))
+ return this[element.type == 'select-one' ?
+ 'selectOne' : 'selectMany'](element);
+ else {
+ var opt, currentValue, single = !Object.isArray(value);
+ for (var i = 0, length = element.length; i < length; i++) {
+ opt = element.options[i];
+ currentValue = this.optionValue(opt);
+ if (single) {
+ if (currentValue == value) {
+ opt.selected = true;
+ return;
+ }
+ }
+ else opt.selected = value.include(currentValue);
+ }
+ }
+ },
+
+ selectOne: function(element) {
+ var index = element.selectedIndex;
+ return index >= 0 ? this.optionValue(element.options[index]) : null;
+ },
+
+ selectMany: function(element) {
+ var values, length = element.length;
+ if (!length) return null;
+
+ for (var i = 0, values = []; i < length; i++) {
+ var opt = element.options[i];
+ if (opt.selected) values.push(this.optionValue(opt));
+ }
+ return values;
+ },
+
+ optionValue: function(opt) {
+ return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+
+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
+ initialize: function($super, element, frequency, callback) {
+ $super(callback, frequency);
+ this.element = $(element);
+ this.lastValue = this.getValue();
+ },
+
+ execute: function() {
+ var value = this.getValue();
+ if (Object.isString(this.lastValue) && Object.isString(value) ?
+ this.lastValue != value : String(this.lastValue) != String(value)) {
+ this.callback(this.element, value);
+ this.lastValue = value;
+ }
+ }
+});
+
+Form.Element.Observer = Class.create(Abstract.TimedObserver, {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.Observer = Class.create(Abstract.TimedObserver, {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+
+/*--------------------------------------------------------------------------*/
+
+Abstract.EventObserver = Class.create({
+ 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, 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(Abstract.EventObserver, {
+ getValue: function() {
+ return Form.Element.getValue(this.element);
+ }
+});
+
+Form.EventObserver = Class.create(Abstract.EventObserver, {
+ getValue: function() {
+ return Form.serialize(this.element);
+ }
+});
+(function() {
+
+ var 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,
+ KEY_INSERT: 45,
+
+ cache: {}
+ };
+
+ var docEl = document.documentElement;
+ var MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED = 'onmouseenter' in docEl
+ && 'onmouseleave' in docEl;
+
+ var _isButton;
+ if (Prototype.Browser.IE) {
+ var buttonMap = { 0: 1, 1: 4, 2: 2 };
+ _isButton = function(event, code) {
+ return event.button === buttonMap[code];
+ };
+ } else if (Prototype.Browser.WebKit) {
+ _isButton = function(event, code) {
+ switch (code) {
+ case 0: return event.which == 1 && !event.metaKey;
+ case 1: return event.which == 1 && event.metaKey;
+ default: return false;
+ }
+ };
+ } else {
+ _isButton = function(event, code) {
+ return event.which ? (event.which === code + 1) : (event.button === code);
+ };
+ }
+
+ function isLeftClick(event) { return _isButton(event, 0) }
+
+ function isMiddleClick(event) { return _isButton(event, 1) }
+
+ function isRightClick(event) { return _isButton(event, 2) }
+
+ function element(event) {
+ event = Event.extend(event);
+
+ var node = event.target, type = event.type,
+ currentTarget = event.currentTarget;
+
+ if (currentTarget && currentTarget.tagName) {
+ if (type === 'load' || type === 'error' ||
+ (type === 'click' && currentTarget.tagName.toLowerCase() === 'input'
+ && currentTarget.type === 'radio'))
+ node = currentTarget;
+ }
+
+ if (node.nodeType == Node.TEXT_NODE)
+ node = node.parentNode;
+
+ return Element.extend(node);
+ }
+
+ function findElement(event, expression) {
+ var element = Event.element(event);
+ if (!expression) return element;
+ while (element) {
+ if (Object.isElement(element) && Prototype.Selector.match(element, expression)) {
+ return Element.extend(element);
+ }
+ element = element.parentNode;
+ }
+ }
+
+ function pointer(event) {
+ return { x: pointerX(event), y: pointerY(event) };
+ }
+
+ function pointerX(event) {
+ var docElement = document.documentElement,
+ body = document.body || { scrollLeft: 0 };
+
+ return event.pageX || (event.clientX +
+ (docElement.scrollLeft || body.scrollLeft) -
+ (docElement.clientLeft || 0));
+ }
+
+ function pointerY(event) {
+ var docElement = document.documentElement,
+ body = document.body || { scrollTop: 0 };
+
+ return event.pageY || (event.clientY +
+ (docElement.scrollTop || body.scrollTop) -
+ (docElement.clientTop || 0));
+ }
+
+
+ function stop(event) {
+ Event.extend(event);
+ event.preventDefault();
+ event.stopPropagation();
+
+ event.stopped = true;
+ }
+
+ Event.Methods = {
+ isLeftClick: isLeftClick,
+ isMiddleClick: isMiddleClick,
+ isRightClick: isRightClick,
+
+ element: element,
+ findElement: findElement,
+
+ pointer: pointer,
+ pointerX: pointerX,
+ pointerY: pointerY,
+
+ stop: stop
+ };
+
+
+ var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
+ m[name] = Event.Methods[name].methodize();
+ return m;
+ });
+
+ if (Prototype.Browser.IE) {
+ function _relatedTarget(event) {
+ var element;
+ switch (event.type) {
+ case 'mouseover': element = event.fromElement; break;
+ case 'mouseout': element = event.toElement; break;
+ default: return null;
+ }
+ return Element.extend(element);
+ }
+
+ Object.extend(methods, {
+ stopPropagation: function() { this.cancelBubble = true },
+ preventDefault: function() { this.returnValue = false },
+ inspect: function() { return '[object Event]' }
+ });
+
+ Event.extend = function(event, element) {
+ if (!event) return false;
+ if (event._extendedByPrototype) return event;
+
+ event._extendedByPrototype = Prototype.emptyFunction;
+ var pointer = Event.pointer(event);
+
+ Object.extend(event, {
+ target: event.srcElement || element,
+ relatedTarget: _relatedTarget(event),
+ pageX: pointer.x,
+ pageY: pointer.y
+ });
+
+ return Object.extend(event, methods);
+ };
+ } else {
+ Event.prototype = window.Event.prototype || document.createEvent('HTMLEvents').__proto__;
+ Object.extend(Event.prototype, methods);
+ Event.extend = Prototype.K;
+ }
+
+ function _createResponder(element, eventName, handler) {
+ var registry = Element.retrieve(element, 'prototype_event_registry');
+
+ if (Object.isUndefined(registry)) {
+ CACHE.push(element);
+ registry = Element.retrieve(element, 'prototype_event_registry', $H());
+ }
+
+ var respondersForEvent = registry.get(eventName);
+ if (Object.isUndefined(respondersForEvent)) {
+ respondersForEvent = [];
+ registry.set(eventName, respondersForEvent);
+ }
+
+ if (respondersForEvent.pluck('handler').include(handler)) return false;
+
+ var responder;
+ if (eventName.include(":")) {
+ responder = function(event) {
+ if (Object.isUndefined(event.eventName))
+ return false;
+
+ if (event.eventName !== eventName)
+ return false;
+
+ Event.extend(event, element);
+ handler.call(element, event);
+ };
+ } else {
+ if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED &&
+ (eventName === "mouseenter" || eventName === "mouseleave")) {
+ if (eventName === "mouseenter" || eventName === "mouseleave") {
+ responder = function(event) {
+ Event.extend(event, element);
+
+ var parent = event.relatedTarget;
+ while (parent && parent !== element) {
+ try { parent = parent.parentNode; }
+ catch(e) { parent = element; }
+ }
+
+ if (parent === element) return;
+
+ handler.call(element, event);
+ };
+ }
+ } else {
+ responder = function(event) {
+ Event.extend(event, element);
+ handler.call(element, event);
+ };
+ }
+ }
+
+ responder.handler = handler;
+ respondersForEvent.push(responder);
+ return responder;
+ }
+
+ function _destroyCache() {
+ for (var i = 0, length = CACHE.length; i < length; i++) {
+ Event.stopObserving(CACHE[i]);
+ CACHE[i] = null;
+ }
+ }
+
+ var CACHE = [];
+
+ if (Prototype.Browser.IE)
+ window.attachEvent('onunload', _destroyCache);
+
+ if (Prototype.Browser.WebKit)
+ window.addEventListener('unload', Prototype.emptyFunction, false);
+
+
+ var _getDOMEventName = Prototype.K,
+ translations = { mouseenter: "mouseover", mouseleave: "mouseout" };
+
+ if (!MOUSEENTER_MOUSELEAVE_EVENTS_SUPPORTED) {
+ _getDOMEventName = function(eventName) {
+ return (translations[eventName] || eventName);
+ };
+ }
+
+ function observe(element, eventName, handler) {
+ element = $(element);
+
+ var responder = _createResponder(element, eventName, handler);
+
+ if (!responder) return element;
+
+ if (eventName.include(':')) {
+ if (element.addEventListener)
+ element.addEventListener("dataavailable", responder, false);
+ else {
+ element.attachEvent("ondataavailable", responder);
+ element.attachEvent("onfilterchange", responder);
+ }
+ } else {
+ var actualEventName = _getDOMEventName(eventName);
+
+ if (element.addEventListener)
+ element.addEventListener(actualEventName, responder, false);
+ else
+ element.attachEvent("on" + actualEventName, responder);
+ }
+
+ return element;
+ }
+
+ function stopObserving(element, eventName, handler) {
+ element = $(element);
+
+ var registry = Element.retrieve(element, 'prototype_event_registry');
+ if (!registry) return element;
+
+ if (!eventName) {
+ registry.each( function(pair) {
+ var eventName = pair.key;
+ stopObserving(element, eventName);
+ });
+ return element;
+ }
+
+ var responders = registry.get(eventName);
+ if (!responders) return element;
+
+ if (!handler) {
+ responders.each(function(r) {
+ stopObserving(element, eventName, r.handler);
+ });
+ return element;
+ }
+
+ var responder = responders.find( function(r) { return r.handler === handler; });
+ if (!responder) return element;
+
+ if (eventName.include(':')) {
+ if (element.removeEventListener)
+ element.removeEventListener("dataavailable", responder, false);
+ else {
+ element.detachEvent("ondataavailable", responder);
+ element.detachEvent("onfilterchange", responder);
+ }
+ } else {
+ var actualEventName = _getDOMEventName(eventName);
+ if (element.removeEventListener)
+ element.removeEventListener(actualEventName, responder, false);
+ else
+ element.detachEvent('on' + actualEventName, responder);
+ }
+
+ registry.set(eventName, responders.without(responder));
+
+ return element;
+ }
+
+ function fire(element, eventName, memo, bubble) {
+ element = $(element);
+
+ if (Object.isUndefined(bubble))
+ bubble = true;
+
+ if (element == document && document.createEvent && !element.dispatchEvent)
+ element = document.documentElement;
+
+ var event;
+ if (document.createEvent) {
+ event = document.createEvent('HTMLEvents');
+ event.initEvent('dataavailable', true, true);
+ } else {
+ event = document.createEventObject();
+ event.eventType = bubble ? 'ondataavailable' : 'onfilterchange';
+ }
+
+ event.eventName = eventName;
+ event.memo = memo || { };
+
+ if (document.createEvent)
+ element.dispatchEvent(event);
+ else
+ element.fireEvent(event.eventType, event);
+
+ return Event.extend(event);
+ }
+
+ Event.Handler = Class.create({
+ initialize: function(element, eventName, selector, callback) {
+ this.element = $(element);
+ this.eventName = eventName;
+ this.selector = selector;
+ this.callback = callback;
+ this.handler = this.handleEvent.bind(this);
+ },
+
+ start: function() {
+ Event.observe(this.element, this.eventName, this.handler);
+ return this;
+ },
+
+ stop: function() {
+ Event.stopObserving(this.element, this.eventName, this.handler);
+ return this;
+ },
+
+ handleEvent: function(event) {
+ var element = event.findElement(this.selector);
+ if (element) this.callback.call(this.element, event, element);
+ }
+ });
+
+ function on(element, eventName, selector, callback) {
+ element = $(element);
+ if (Object.isFunction(selector) && Object.isUndefined(callback)) {
+ callback = selector, selector = null;
+ }
+
+ return new Event.Handler(element, eventName, selector, callback).start();
+ }
+
+ Object.extend(Event, Event.Methods);
+
+ Object.extend(Event, {
+ fire: fire,
+ observe: observe,
+ stopObserving: stopObserving,
+ on: on
+ });
+
+ Element.addMethods({
+ fire: fire,
+
+ observe: observe,
+
+ stopObserving: stopObserving,
+
+ on: on
+ });
+
+ Object.extend(document, {
+ fire: fire.methodize(),
+
+ observe: observe.methodize(),
+
+ stopObserving: stopObserving.methodize(),
+
+ on: on.methodize(),
+
+ loaded: false
+ });
+
+ if (window.Event) Object.extend(window.Event, Event);
+ else window.Event = Event;
+})();
+
+(function() {
+ /* Support for the DOMContentLoaded event is based on work by Dan Webb,
+ Matthias Miller, Dean Edwards, John Resig, and Diego Perini. */
+
+ var timer;
+
+ function fireContentLoadedEvent() {
+ if (document.loaded) return;
+ if (timer) window.clearTimeout(timer);
+ document.loaded = true;
+ document.fire('dom:loaded');
+ }
+
+ function checkReadyState() {
+ if (document.readyState === 'complete') {
+ document.stopObserving('readystatechange', checkReadyState);
+ fireContentLoadedEvent();
+ }
+ }
+
+ function pollDoScroll() {
+ try { document.documentElement.doScroll('left'); }
+ catch(e) {
+ timer = pollDoScroll.defer();
+ return;
+ }
+ fireContentLoadedEvent();
+ }
+
+ if (document.addEventListener) {
+ document.addEventListener('DOMContentLoaded', fireContentLoadedEvent, false);
+ } else {
+ document.observe('readystatechange', checkReadyState);
+ if (window == top)
+ timer = pollDoScroll.defer();
+ }
+
+ Event.observe(window, 'load', fireContentLoadedEvent);
+})();
+
+Element.addMethods();
+
+/*------------------------------- DEPRECATED -------------------------------*/
+
+Hash.toQueryString = Object.toQueryString;
+
+var Toggle = { display: Element.toggle };
+
+Element.Methods.childOf = Element.Methods.descendantOf;
+
+var Insertion = {
+ Before: function(element, content) {
+ return Element.insert(element, {before:content});
+ },
+
+ Top: function(element, content) {
+ return Element.insert(element, {top:content});
+ },
+
+ Bottom: function(element, content) {
+ return Element.insert(element, {bottom:content});
+ },
+
+ After: function(element, content) {
+ return Element.insert(element, {after:content});
+ }
+};
+
+var $continue = new Error('"throw $continue" is deprecated, use "return" instead');
+
+var Position = {
+ includeScrollOffsets: false,
+
+ prepare: function() {
+ this.deltaX = window.pageXOffset
+ || document.documentElement.scrollLeft
+ || document.body.scrollLeft
+ || 0;
+ this.deltaY = window.pageYOffset
+ || document.documentElement.scrollTop
+ || document.body.scrollTop
+ || 0;
+ },
+
+ within: function(element, x, y) {
+ if (this.includeScrollOffsets)
+ return this.withinIncludingScrolloffsets(element, x, y);
+ this.xcomp = x;
+ this.ycomp = y;
+ this.offset = Element.cumulativeOffset(element);
+
+ return (y >= this.offset[1] &&
+ y < this.offset[1] + element.offsetHeight &&
+ x >= this.offset[0] &&
+ x < this.offset[0] + element.offsetWidth);
+ },
+
+ withinIncludingScrolloffsets: function(element, x, y) {
+ var offsetcache = Element.cumulativeScrollOffset(element);
+
+ this.xcomp = x + offsetcache[0] - this.deltaX;
+ this.ycomp = y + offsetcache[1] - this.deltaY;
+ this.offset = Element.cumulativeOffset(element);
+
+ return (this.ycomp >= this.offset[1] &&
+ this.ycomp < this.offset[1] + element.offsetHeight &&
+ this.xcomp >= this.offset[0] &&
+ this.xcomp < this.offset[0] + element.offsetWidth);
+ },
+
+ 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;
+ },
+
+
+ cumulativeOffset: Element.Methods.cumulativeOffset,
+
+ positionedOffset: Element.Methods.positionedOffset,
+
+ absolutize: function(element) {
+ Position.prepare();
+ return Element.absolutize(element);
+ },
+
+ relativize: function(element) {
+ Position.prepare();
+ return Element.relativize(element);
+ },
+
+ realOffset: Element.Methods.cumulativeScrollOffset,
+
+ offsetParent: Element.Methods.getOffsetParent,
+
+ page: Element.Methods.viewportOffset,
+
+ clone: function(source, target, options) {
+ options = options || { };
+ return Element.clonePosition(target, source, options);
+ }
+};
+
+/*--------------------------------------------------------------------------*/
+
+if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
+ function iter(name) {
+ return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
+ }
+
+ instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
+ function(element, className) {
+ className = className.toString().strip();
+ var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
+ return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
+ } : function(element, className) {
+ className = className.toString().strip();
+ var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
+ if (!classNames && !className) return elements;
+
+ var nodes = $(element).getElementsByTagName('*');
+ className = ' ' + className + ' ';
+
+ for (var i = 0, child, cn; child = nodes[i]; i++) {
+ if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
+ (classNames && classNames.all(function(name) {
+ return !name.toString().blank() && cn.include(' ' + name + ' ');
+ }))))
+ elements.push(Element.extend(child));
+ }
+ return elements;
+ };
+
+ return function(className, parentElement) {
+ return $(parentElement || document.body).getElementsByClassName(className);
+ };
+}(Element.Methods);
+
+/*--------------------------------------------------------------------------*/
+
+Element.ClassNames = Class.create();
+Element.ClassNames.prototype = {
+ initialize: function(element) {
+ this.element = $(element);
+ },
+
+ _each: function(iterator) {
+ this.element.className.split(/\s+/).select(function(name) {
+ return name.length > 0;
+ })._each(iterator);
+ },
+
+ set: function(className) {
+ this.element.className = className;
+ },
+
+ add: function(classNameToAdd) {
+ if (this.include(classNameToAdd)) return;
+ this.set($A(this).concat(classNameToAdd).join(' '));
+ },
+
+ remove: function(classNameToRemove) {
+ if (!this.include(classNameToRemove)) return;
+ this.set($A(this).without(classNameToRemove).join(' '));
+ },
+
+ toString: function() {
+ return $A(this).join(' ');
+ }
+};
+
+Object.extend(Element.ClassNames.prototype, Enumerable);
+
+/*--------------------------------------------------------------------------*/
+
+(function() {
+ window.Selector = Class.create({
+ initialize: function(expression) {
+ this.expression = expression.strip();
+ },
+
+ findElements: function(rootElement) {
+ return Prototype.Selector.select(this.expression, rootElement);
+ },
+
+ match: function(element) {
+ return Prototype.Selector.match(element, this.expression);
+ },
+
+ toString: function() {
+ return this.expression;
+ },
+
+ inspect: function() {
+ return "#<Selector: " + this.expression + ">";
+ }
+ });
+
+ Object.extend(Selector, {
+ matchElements: function(elements, expression) {
+ var match = Prototype.Selector.match,
+ results = [];
+
+ for (var i = 0, length = elements.length; i < length; i++) {
+ var element = elements[i];
+ if (match(element, expression)) {
+ results.push(Element.extend(element));
+ }
+ }
+ return results;
+ },
+
+ findElement: function(elements, expression, index) {
+ index = index || 0;
+ var matchIndex = 0, element;
+ for (var i = 0, length = elements.length; i < length; i++) {
+ element = elements[i];
+ if (Prototype.Selector.match(element, expression) && index === matchIndex++) {
+ return Element.extend(element);
+ }
+ }
+ },
+
+ findChildElements: function(element, expressions) {
+ var selector = expressions.toArray().join(', ');
+ return Prototype.Selector.select(selector, element || document);
+ }
+ });
+})();
(DIR) diff --git a/web/public/javascripts/rails.js b/web/public/javascripts/rails.js
@@ -0,0 +1,175 @@
+(function() {
+ // Technique from Juriy Zaytsev
+ // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
+ function isEventSupported(eventName) {
+ var el = document.createElement('div');
+ eventName = 'on' + eventName;
+ var isSupported = (eventName in el);
+ if (!isSupported) {
+ el.setAttribute(eventName, 'return;');
+ isSupported = typeof el[eventName] == 'function';
+ }
+ el = null;
+ return isSupported;
+ }
+
+ function isForm(element) {
+ return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM'
+ }
+
+ function isInput(element) {
+ if (Object.isElement(element)) {
+ var name = element.nodeName.toUpperCase()
+ return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA'
+ }
+ else return false
+ }
+
+ var submitBubbles = isEventSupported('submit'),
+ changeBubbles = isEventSupported('change')
+
+ if (!submitBubbles || !changeBubbles) {
+ // augment the Event.Handler class to observe custom events when needed
+ Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap(
+ function(init, element, eventName, selector, callback) {
+ init(element, eventName, selector, callback)
+ // is the handler being attached to an element that doesn't support this event?
+ if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) ||
+ (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) {
+ // "submit" => "emulated:submit"
+ this.eventName = 'emulated:' + this.eventName
+ }
+ }
+ )
+ }
+
+ if (!submitBubbles) {
+ // discover forms on the page by observing focus events which always bubble
+ document.on('focusin', 'form', function(focusEvent, form) {
+ // special handler for the real "submit" event (one-time operation)
+ if (!form.retrieve('emulated:submit')) {
+ form.on('submit', function(submitEvent) {
+ var emulated = form.fire('emulated:submit', submitEvent, true)
+ // if custom event received preventDefault, cancel the real one too
+ if (emulated.returnValue === false) submitEvent.preventDefault()
+ })
+ form.store('emulated:submit', true)
+ }
+ })
+ }
+
+ if (!changeBubbles) {
+ // discover form inputs on the page
+ document.on('focusin', 'input, select, texarea', function(focusEvent, input) {
+ // special handler for real "change" events
+ if (!input.retrieve('emulated:change')) {
+ input.on('change', function(changeEvent) {
+ input.fire('emulated:change', changeEvent, true)
+ })
+ input.store('emulated:change', true)
+ }
+ })
+ }
+
+ function handleRemote(element) {
+ var method, url, params;
+
+ var event = element.fire("ajax:before");
+ if (event.stopped) return false;
+
+ if (element.tagName.toLowerCase() === 'form') {
+ method = element.readAttribute('method') || 'post';
+ url = element.readAttribute('action');
+ params = element.serialize();
+ } else {
+ method = element.readAttribute('data-method') || 'get';
+ url = element.readAttribute('href');
+ params = {};
+ }
+
+ new Ajax.Request(url, {
+ method: method,
+ parameters: params,
+ evalScripts: true,
+
+ onComplete: function(request) { element.fire("ajax:complete", request); },
+ onSuccess: function(request) { element.fire("ajax:success", request); },
+ onFailure: function(request) { element.fire("ajax:failure", request); }
+ });
+
+ element.fire("ajax:after");
+ }
+
+ function handleMethod(element) {
+ var method = element.readAttribute('data-method'),
+ url = element.readAttribute('href'),
+ csrf_param = $$('meta[name=csrf-param]')[0],
+ csrf_token = $$('meta[name=csrf-token]')[0];
+
+ var form = new Element('form', { method: "POST", action: url, style: "display: none;" });
+ element.parentNode.insert(form);
+
+ if (method !== 'post') {
+ var field = new Element('input', { type: 'hidden', name: '_method', value: method });
+ form.insert(field);
+ }
+
+ if (csrf_param) {
+ var param = csrf_param.readAttribute('content'),
+ token = csrf_token.readAttribute('content'),
+ field = new Element('input', { type: 'hidden', name: param, value: token });
+ form.insert(field);
+ }
+
+ form.submit();
+ }
+
+
+ document.on("click", "*[data-confirm]", function(event, element) {
+ var message = element.readAttribute('data-confirm');
+ if (!confirm(message)) event.stop();
+ });
+
+ document.on("click", "a[data-remote]", function(event, element) {
+ if (event.stopped) return;
+ handleRemote(element);
+ event.stop();
+ });
+
+ document.on("click", "a[data-method]", function(event, element) {
+ if (event.stopped) return;
+ handleMethod(element);
+ event.stop();
+ });
+
+ document.on("submit", function(event) {
+ var element = event.findElement(),
+ message = element.readAttribute('data-confirm');
+ if (message && !confirm(message)) {
+ event.stop();
+ return false;
+ }
+
+ var inputs = element.select("input[type=submit][data-disable-with]");
+ inputs.each(function(input) {
+ input.disabled = true;
+ input.writeAttribute('data-original-value', input.value);
+ input.value = input.readAttribute('data-disable-with');
+ });
+
+ var element = event.findElement("form[data-remote]");
+ if (element) {
+ handleRemote(element);
+ event.stop();
+ }
+ });
+
+ document.on("ajax:after", "form", function(event, element) {
+ var inputs = element.select("input[type=submit][disabled=true][data-disable-with]");
+ inputs.each(function(input) {
+ input.value = input.readAttribute('data-original-value');
+ input.removeAttribute('data-original-value');
+ input.disabled = false;
+ });
+ });
+})();
(DIR) diff --git a/web/public/robots.txt b/web/public/robots.txt
@@ -0,0 +1,5 @@
+# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
+#
+# To ban all spiders from the entire site uncomment the next two lines:
+# User-Agent: *
+# Disallow: /
(DIR) diff --git a/web/public/stylesheets/global.css b/web/public/stylesheets/global.css
@@ -0,0 +1,556 @@
+/* global element overrides */
+
+body {
+ height: 100%;
+ background: #fff;
+ margin: 0;
+ padding: 0;
+ font-family: Tahoma, sans-serif;
+ font-size: 12px;
+ color: #555;
+}
+
+img {
+ border: none;
+}
+
+table {
+ border: none;
+ padding: 0;
+ margin: 0;
+}
+
+a:link, a:visited {
+ color: #003366;
+ text-decoration: none;
+}
+
+a:hover, a:active {
+ color: #cc0033;
+ text-decoration: underline;
+}
+
+h1 {
+ color: #333;
+ margin-top: 0;
+ padding-top: 0;
+ font-weight: normal;
+ font-size: 36px;
+}
+
+h2 {
+ font-size: 1.4em;
+ margin-bottom: 4px;
+}
+
+h3 {
+ font-size: 1.05em;
+ font-weight: normal;
+ font-style: italic;
+}
+
+/* unique elements */
+
+#content {
+ padding: 0;
+ margin: 0 auto;
+ width: 800px;
+ line-height: 1.5;
+}
+
+#main {
+ border: 0;
+ padding: 0;
+ background-color: white;
+ padding-top: 6px;
+ padding-left: 18px;
+ padding-right: 20px;
+ margin-top: -3px;
+ margin-bottom: -3px;
+}
+
+#main img {
+ max-width: 530px;
+}
+
+#footer {
+
+ margin: 0;
+ text-align: center;
+}
+
+#quote {
+ font-size: 12px;
+ padding: 10px;
+ margin: 0 auto;
+ width: 70%;
+ background: #dddddd;
+ margin-bottom: 10px;
+}
+
+#copyright {
+ padding: 10px;
+ text-align: center;
+ font-size: 10px;
+ color: #cccccc;
+}
+
+#header {
+ width: 800px;
+ margin: 0 auto 10px;
+ color: white;
+
+}
+
+#logo {
+ float: left;
+ margin-top: 0px;
+}
+
+
+
+#warvox_stats {
+ margin-top: 5px;
+ border: 1px solid black;
+ font-size: 12px;
+ color: #555;
+}
+
+#warvox_stats td {
+ margin-left: 20px;
+ padding: 0 10px 0px 6px;
+}
+
+
+#warvox_conf {
+ margin-top: 5px;
+ border: 1px solid black;
+ font-size: 12px;
+ color: #555;
+}
+
+#warvox_conf td {
+ margin-left: 20px;
+ padding: 0 10px 0px 6px;
+}
+
+#warvox_blacklist {
+ margin-top: 5px;
+ border: 1px solid black;
+ font-size: 12px;
+ color: #555;
+}
+
+#warvox_blacklist td {
+ margin-left: 20px;
+ padding: 0 10px 0px 6px;
+}
+
+#home_logo {
+ border: 0;
+}
+
+#home_links {
+
+}
+
+#home_intro, #home_intro td {
+ border: 0;
+}
+
+#home_table {
+ border: 0;
+ margin-top: 0px;
+}
+
+#home_table td {
+ border: 0;
+}
+
+#home_text {
+ padding-left: 20px;
+}
+
+#navwrap {
+ margin-top: 10px;
+ padding-top: 10px;
+ text-align: center;
+}
+
+#nav {
+ margin: 0 0 0 0 auto;
+ padding: 10px;
+}
+
+#sections_container {
+ font-size: 14px;
+}
+
+#sections_ul {
+ margin: 0;
+ padding: 0;
+ white-space: nowrap;
+}
+
+#sections_ul li {
+ display: inline;
+ list-style-type: none;
+}
+
+#sections_container a {
+ padding: 5px 10px 5px 10px;
+ line-height: 28px;
+}
+
+#sections_container a:link, #sections_container a:visited {
+ color: #fff;
+ background: black repeat-x top;
+ text-decoration: none;
+ font-variant: small-caps;
+}
+
+#sections_container a:hover {
+ color: #fff;
+ background: red repeat-x top;
+ text-decoration: none;
+ font-variant: small-caps;
+}
+
+#sections_active {
+ font-weight: bold;
+}
+
+#subsections_active {
+ font-weight: bold;
+}
+
+#subsections_container {
+ font-size: 12px;
+ padding-top: 5px;
+}
+
+#subsections_ul {
+ margin: 0;
+ padding: 0;
+ white-space: nowrap;
+}
+
+#subsections_ul li {
+ display: inline;
+ list-style-type: none;
+}
+
+#subsections_container a {
+ padding: 5px 10px 5px 10px;
+ line-height: 20px;
+}
+
+#subsections_container a:link, #subsections_container a:visited {
+ color: #fff;
+ background: #333333 repeat-x top;
+ text-decoration: none;
+ font-variant: small-caps;
+}
+
+#subsections_container a:hover {
+ color: #fff;
+ background: #440000 repeat-x top;
+ text-decoration: none;
+ font-variant: small-caps;
+}
+
+
+#calls_pie1 {
+ text-align: center;
+}
+#calls_pie2 {
+ text-align: center;
+}
+#calls_pie3 {
+ text-align: center;
+}
+
+/* non-unique elements */
+
+
+.title {
+ font-size: 20px;
+ font-weight: bold;
+ color: black;
+ font-variant: small-caps;
+}
+
+.active_job_row {
+ background: yellow;
+}
+
+.table_scaffold {
+ margin-top: 5px;
+ border: 1px solid black;
+ font-size: 12px;
+ color: #555;
+}
+
+.table_scaffold th {
+ font-size: 14px;
+ text-decoration: underline;
+}
+
+.table_scaffold td {
+ margin-left: 20px;
+ padding: 0 10px 0px 6px;
+ border: 1px solid #cccccc;
+}
+
+.header_item {
+ font-weight: bold;
+}
+
+.date-header {
+ background: url('/images/bluefade.jpg') #222222 repeat-x top;
+ color: white;
+ padding: 2px;
+ margin: 2px;
+ font-weight: bold;
+ padding-left: 8px;
+ font-variant: small-caps;
+ border: 0;
+}
+
+.date-header-center {
+ background: url('/images/bluefade.jpg') #8c0000 repeat-x top;
+ color: white;
+ padding: 2px;
+ margin: 2px;
+ font-weight: bold;
+ padding-left: 8px;
+ font-variant: small-caps;
+ text-align: center;
+ border: 0;
+}
+
+
+.balloon {
+ background: url('/images/balloon.png') repeat-x top;
+ width: 277px;
+ height: 98px;
+ color: white;
+ margin: 40px auto;
+}
+
+.balloon_links {
+ float: left;
+ margin-top: 10px;
+ margin-left: 6px;
+
+}
+
+.balloon_links p {
+ font-size: 14px;
+ font-weight: bold;
+ margin: 0;
+}
+
+.balloon_links a, .balloon_links a:link, .balloon_links a:visited {
+ font-size: 12px;
+ color: white;
+ text-decoration: none;
+ margin-left: 12px;
+}
+
+.balloon_links a:hover, .balloon_links a:active {
+ color: yellow;
+ text-decoration: underline;
+}
+
+.balloon_icon {
+ float: left;
+ padding-top: 25px;
+ padding-left: 14px;
+}
+
+.box_full {
+ position: relative;
+ clear: both;
+ width: 790px;
+}
+
+#round_top {
+ padding: 0;
+ border: 0;
+ margin: 0;
+ height: 9px;
+ margin-bottom: -2px;
+ visibility: hidden;
+}
+
+#round_bot {
+ padding: 0;
+ border: 0;
+ margin-top: -2px;
+ height: 15px;
+ visibility: hidden;
+}
+
+.box_full p {
+ line-height: 1.5em;
+}
+
+
+.intro {
+ font-size: 14px;
+}
+
+.center {
+ text-align: center;
+}
+
+.code {
+ font-family: fixed, courier new;
+ color: black;
+ background: #dddddd;
+ padding: 0.25em 0.25em 0.25em 0.25em;
+}
+
+.fatp {
+ margin: 2.0em 0 0.5em;
+}
+
+.announce {
+ padding: 0px 20px 0px 20px;
+ line-height: 1.5;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.vulnTitle {
+ font-weight: bold;
+ font-size: 10pt;
+ padding-bottom: 2em;
+}
+
+.vulnHeader {
+ font-weight: bold;
+}
+
+.vulnText {
+ padding-bottom: 1em;
+}
+
+.vulnInfoTable {
+ background: #dddddd;
+
+}
+
+.vulnInfoHeader {
+ font-weight: bold;
+}
+
+.vulnInfoData {
+
+}
+
+.talk_title {
+ font-family: verdana, sans-serif, arial, helvetica;
+ font-size: 9pt;
+ padding: 0.1em 0.5em 0.15em .5em;
+ font-weight: bold;
+}
+
+.materials {
+ background: #dddddd;
+ padding: 10px 10px 10px 10px;
+ border: 1px solid black;
+}
+
+
+
+.level1
+{
+ list-style: none;
+ text-align: left;
+ padding: 0em 0em 1em 0em;
+ font-size: 16px;
+ font-weight: bold;
+}
+.level1 li
+{
+ padding: 1em .25em 0.25em 0.25em;
+}
+
+.level2
+{
+ list-style: circle;
+ text-align: left;
+ padding: 0em 0.25em 0.25em 4em;
+}
+.level2 li
+{
+ padding: 0.25em .25em 0.25em 0.25em;
+ font-size: 14px;
+ font-weight: bold;
+}
+
+.level3
+{
+ list-style: square;
+ text-align: left;
+ padding: 0em 0.25em 0.25em 4em;
+}
+.level3 li
+{
+ padding: 0.25em .25em 0.25em 0.25em;
+}
+
+
+
+.boxTable
+{
+ border-left-style: solid;
+ border-top-style: solid;
+ border-right-style: solid;
+ border-bottom-style: solid;
+ border-color: #dddddd;
+ font-family: verdana, sans-serif;
+ font-size: 8pt;
+ background-color: #dddddd;
+}
+.boxInnerTable
+{
+ font-family: verdana, sans-serif;
+ font-size: 8pt;
+}
+.boxTdHeader
+{
+ border-width: 0 0 0 0;
+ border-color: #dddddd;
+ border-left-style: solid;
+ border-top-style: solid;
+ border-right-style: solid;
+ border-bottom-style: solid;
+ background-color: #dddddd;
+ color: black;
+ height: 20px;
+ font-weight: bold
+}
+.boxTd
+{
+ border-width: 0 0 0 0;
+ border-left-style: solid;
+ border-top-style: solid;
+ border-right-style: solid;
+ border-bottom-style: solid;
+}
+.boxTd1
+{
+ background-color: #eeeeee;
+}
+.boxTd2
+{
+ background-color: #eeeeee;
+}
+.boxTh
+{
+ background-color: #dddddd;
+}
+
(DIR) diff --git a/web/public/stylesheets/ie7.css b/web/public/stylesheets/ie7.css
@@ -0,0 +1,3 @@
+#logo {
+ margin-top: 7px;
+}
(DIR) diff --git a/web/public/stylesheets/lightbox.css b/web/public/stylesheets/lightbox.css
@@ -0,0 +1,26 @@
+#lightbox{
+ background-color:#eee;
+ padding: 10px;
+ border-bottom: 2px solid #666;
+ border-right: 2px solid #666;
+ }
+#lightboxDetails{
+ font-size: 0.8em;
+ padding-top: 0.4em;
+ }
+#lightboxCaption{ float: left; }
+#keyboardMsg{ float: right; }
+#closeButton{ top: 5px; right: 5px; }
+
+#lightbox img{ border: none; clear: both;}
+#overlay img{ border: none; }
+
+#overlay{ background-image: url(overlay.png); }
+
+* html #overlay{
+ background-color: #333;
+ back\ground-color: transparent;
+ background-image: url(blank.gif);
+ filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src="overlay.png", sizingMethod="scale");
+ }
+
+\ No newline at end of file
(DIR) diff --git a/web/public/stylesheets/overlay.png b/web/public/stylesheets/overlay.png
Binary files differ.
(DIR) diff --git a/web/public/stylesheets/scaffold.css b/web/public/stylesheets/scaffold.css
@@ -0,0 +1,54 @@
+body { background-color: #fff; color: #333; }
+
+body, p, ol, ul, td {
+ font-family: verdana, arial, helvetica, sans-serif;
+ font-size: 13px;
+ line-height: 18px;
+}
+
+pre {
+ background-color: #eee;
+ padding: 10px;
+ font-size: 11px;
+}
+
+a { color: #000; }
+a:visited { color: #666; }
+a:hover { color: #fff; background-color:#000; }
+
+.fieldWithErrors {
+ padding: 2px;
+ background-color: red;
+ display: table;
+}
+
+#errorExplanation {
+ width: 400px;
+ border: 2px solid red;
+ padding: 7px;
+ padding-bottom: 12px;
+ margin-bottom: 20px;
+ background-color: #f0f0f0;
+}
+
+#errorExplanation h2 {
+ text-align: left;
+ font-weight: bold;
+ padding: 5px 5px 5px 15px;
+ font-size: 12px;
+ margin: -7px;
+ background-color: #c00;
+ color: #fff;
+}
+
+#errorExplanation p {
+ color: #333;
+ margin-bottom: 0;
+ padding: 5px;
+}
+
+#errorExplanation ul li {
+ font-size: 12px;
+ list-style: square;
+}
+
(DIR) diff --git a/web/script/rails b/web/script/rails
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
+
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require File.expand_path('../../config/boot', __FILE__)
+require 'rails/commands'
(DIR) diff --git a/web/test/fixtures/dial_jobs.yml b/web/test/fixtures/dial_jobs.yml
@@ -0,0 +1,11 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+
+# This model initially had no columns defined. If you add columns to the
+# model remove the '{}' from the fixture names and add the columns immediately
+# below each fixture, per the syntax in the comments below
+#
+one: {}
+# column: value
+#
+two: {}
+# column: value
(DIR) diff --git a/web/test/fixtures/dial_results.yml b/web/test/fixtures/dial_results.yml
@@ -0,0 +1,11 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+
+# This model initially had no columns defined. If you add columns to the
+# model remove the '{}' from the fixture names and add the columns immediately
+# below each fixture, per the syntax in the comments below
+#
+one: {}
+# column: value
+#
+two: {}
+# column: value
(DIR) diff --git a/web/test/fixtures/providers.yml b/web/test/fixtures/providers.yml
@@ -0,0 +1,11 @@
+# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
+
+# This model initially had no columns defined. If you add columns to the
+# model remove the '{}' from the fixture names and add the columns immediately
+# below each fixture, per the syntax in the comments below
+#
+one: {}
+# column: value
+#
+two: {}
+# column: value
(DIR) diff --git a/web/test/functional/analyze_controller_test.rb b/web/test/functional/analyze_controller_test.rb
@@ -0,0 +1,8 @@
+require 'test_helper'
+
+class AnalyzeControllerTest < ActionController::TestCase
+ # Replace this with your real tests.
+ test "the truth" do
+ assert true
+ end
+end
(DIR) diff --git a/web/test/functional/dial_jobs_controller_test.rb b/web/test/functional/dial_jobs_controller_test.rb
@@ -0,0 +1,8 @@
+require 'test_helper'
+
+class DialJobsControllerTest < ActionController::TestCase
+ # Replace this with your real tests.
+ test "the truth" do
+ assert true
+ end
+end
(DIR) diff --git a/web/test/functional/dial_results_controller_test.rb b/web/test/functional/dial_results_controller_test.rb
@@ -0,0 +1,8 @@
+require 'test_helper'
+
+class DialResultsControllerTest < ActionController::TestCase
+ # Replace this with your real tests.
+ test "the truth" do
+ assert true
+ end
+end
(DIR) diff --git a/web/test/functional/home_controller_test.rb b/web/test/functional/home_controller_test.rb
@@ -0,0 +1,8 @@
+require 'test_helper'
+
+class HomeControllerTest < ActionController::TestCase
+ # Replace this with your real tests.
+ test "the truth" do
+ assert true
+ end
+end
(DIR) diff --git a/web/test/functional/providers_controller_test.rb b/web/test/functional/providers_controller_test.rb
@@ -0,0 +1,8 @@
+require 'test_helper'
+
+class ProvidersControllerTest < ActionController::TestCase
+ # Replace this with your real tests.
+ test "the truth" do
+ assert true
+ end
+end
(DIR) diff --git a/web/test/performance/browsing_test.rb b/web/test/performance/browsing_test.rb
@@ -0,0 +1,9 @@
+require 'test_helper'
+require 'rails/performance_test_help'
+
+# Profiling results for each test method are written to tmp/performance.
+class BrowsingTest < ActionDispatch::PerformanceTest
+ def test_homepage
+ get '/'
+ end
+end
(DIR) diff --git a/web/test/test_helper.rb b/web/test/test_helper.rb
@@ -0,0 +1,13 @@
+ENV["RAILS_ENV"] = "test"
+require File.expand_path('../../config/environment', __FILE__)
+require 'rails/test_help'
+
+class ActiveSupport::TestCase
+ # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
+ #
+ # Note: You'll currently still have to declare fixtures explicitly in integration tests
+ # -- they do not yet inherit this setting
+ fixtures :all
+
+ # Add more helper methods to be used by all tests here...
+end
(DIR) diff --git a/web/test/unit/dial_job_test.rb b/web/test/unit/dial_job_test.rb
@@ -0,0 +1,8 @@
+require 'test_helper'
+
+class DialJobTest < ActiveSupport::TestCase
+ # Replace this with your real tests.
+ test "the truth" do
+ assert true
+ end
+end
(DIR) diff --git a/web/test/unit/dial_result_test.rb b/web/test/unit/dial_result_test.rb
@@ -0,0 +1,8 @@
+require 'test_helper'
+
+class DialResultTest < ActiveSupport::TestCase
+ # Replace this with your real tests.
+ test "the truth" do
+ assert true
+ end
+end
(DIR) diff --git a/web/test/unit/helpers/analyze_helper_test.rb b/web/test/unit/helpers/analyze_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class AnalyzeHelperTest < ActionView::TestCase
+end
(DIR) diff --git a/web/test/unit/helpers/dial_jobs_helper_test.rb b/web/test/unit/helpers/dial_jobs_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class DialJobsHelperTest < ActionView::TestCase
+end
(DIR) diff --git a/web/test/unit/helpers/dial_results_helper_test.rb b/web/test/unit/helpers/dial_results_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class DialResultsHelperTest < ActionView::TestCase
+end
(DIR) diff --git a/web/test/unit/helpers/home_helper_test.rb b/web/test/unit/helpers/home_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class HomeHelperTest < ActionView::TestCase
+end
(DIR) diff --git a/web/test/unit/helpers/providers_helper_test.rb b/web/test/unit/helpers/providers_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class ProvidersHelperTest < ActionView::TestCase
+end
(DIR) diff --git a/web/test/unit/provider_test.rb b/web/test/unit/provider_test.rb
@@ -0,0 +1,8 @@
+require 'test_helper'
+
+class ProviderTest < ActiveSupport::TestCase
+ # Replace this with your real tests.
+ test "the truth" do
+ assert true
+ end
+end
(DIR) diff --git a/web/vendor/plugins/dynamic_form/MIT-LICENSE b/web/vendor/plugins/dynamic_form/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2010 David Heinemeier Hansson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
(DIR) diff --git a/web/vendor/plugins/dynamic_form/README b/web/vendor/plugins/dynamic_form/README
@@ -0,0 +1,13 @@
+DynamicForm
+===========
+
+DynamicForm holds a few helpers method to help you deal with your models, they are:
+
+* input(record, method, options = {})
+* form(record, options = {})
+* error_message_on(object, method, options={})
+* error_messages_for(record, options={})
+
+It also adds f.error_messages and f.error_messages_on to your form builders.
+
+Copyright (c) 2010 David Heinemeier Hansson, released under the MIT license
(DIR) diff --git a/web/vendor/plugins/dynamic_form/Rakefile b/web/vendor/plugins/dynamic_form/Rakefile
@@ -0,0 +1,10 @@
+require 'rake/testtask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test the active_model_helper plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'test'
+ t.pattern = 'test/**/*_test.rb'
+end
(DIR) diff --git a/web/vendor/plugins/dynamic_form/dynamic_form.gemspec b/web/vendor/plugins/dynamic_form/dynamic_form.gemspec
@@ -0,0 +1,12 @@
+Gem::Specification.new do |s|
+ s.name = 'dynamic_form'
+ s.version = '1.0.0'
+ s.author = 'David Heinemeier Hansson'
+ s.email = 'david@loudthinking.com'
+ s.summary = 'Deprecated dynamic form helpers: input, form, error_messages_for, error_messages_on'
+
+ s.add_dependency('rails', '>= 3.0.0')
+
+ s.files = Dir['lib/**/*']
+ s.require_path = 'lib'
+end
(DIR) diff --git a/web/vendor/plugins/dynamic_form/init.rb b/web/vendor/plugins/dynamic_form/init.rb
@@ -0,0 +1 @@
+require 'dynamic_form'
(DIR) diff --git a/web/vendor/plugins/dynamic_form/lib/action_view/helpers/dynamic_form.rb b/web/vendor/plugins/dynamic_form/lib/action_view/helpers/dynamic_form.rb
@@ -0,0 +1,300 @@
+require 'action_view/helpers'
+require 'active_support/i18n'
+require 'active_support/core_ext/enumerable'
+require 'active_support/core_ext/object/blank'
+
+module ActionView
+ module Helpers
+ # The Active Record Helper makes it easier to create forms for records kept in instance variables. The most far-reaching is the +form+
+ # method that creates a complete form for all the basic content types of the record (not associations or aggregations, though). This
+ # is a great way of making the record quickly available for editing, but likely to prove lackluster for a complicated real-world form.
+ # In that case, it's better to use the +input+ method and the specialized +form+ methods in link:classes/ActionView/Helpers/FormHelper.html
+ module DynamicForm
+ # Returns a default input tag for the type of object returned by the method. For example, if <tt>@post</tt>
+ # has an attribute +title+ mapped to a +VARCHAR+ column that holds "Hello World":
+ #
+ # input("post", "title")
+ # # => <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
+ def input(record_name, method, options = {})
+ InstanceTag.new(record_name, method, self).to_tag(options)
+ end
+
+ # Returns an entire form with all needed input tags for a specified Active Record object. For example, if <tt>@post</tt>
+ # has attributes named +title+ of type +VARCHAR+ and +body+ of type +TEXT+ then
+ #
+ # form("post")
+ #
+ # would yield a form like the following (modulus formatting):
+ #
+ # <form action='/posts/create' method='post'>
+ # <p>
+ # <label for="post_title">Title</label><br />
+ # <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
+ # </p>
+ # <p>
+ # <label for="post_body">Body</label><br />
+ # <textarea cols="40" id="post_body" name="post[body]" rows="20"></textarea>
+ # </p>
+ # <input name="commit" type="submit" value="Create" />
+ # </form>
+ #
+ # It's possible to specialize the form builder by using a different action name and by supplying another
+ # block renderer. For example, if <tt>@entry</tt> has an attribute +message+ of type +VARCHAR+ then
+ #
+ # form("entry",
+ # :action => "sign",
+ # :input_block => Proc.new { |record, column|
+ # "#{column.human_name}: #{input(record, column.name)}<br />"
+ # })
+ #
+ # would yield a form like the following (modulus formatting):
+ #
+ # <form action="/entries/sign" method="post">
+ # Message:
+ # <input id="entry_message" name="entry[message]" size="30" type="text" /><br />
+ # <input name="commit" type="submit" value="Sign" />
+ # </form>
+ #
+ # It's also possible to add additional content to the form by giving it a block, such as:
+ #
+ # form("entry", :action => "sign") do |form|
+ # form << content_tag("b", "Department")
+ # form << collection_select("department", "id", @departments, "id", "name")
+ # end
+ #
+ # The following options are available:
+ #
+ # * <tt>:action</tt> - The action used when submitting the form (default: +create+ if a new record, otherwise +update+).
+ # * <tt>:input_block</tt> - Specialize the output using a different block, see above.
+ # * <tt>:method</tt> - The method used when submitting the form (default: +post+).
+ # * <tt>:multipart</tt> - Whether to change the enctype of the form to "multipart/form-data", used when uploading a file (default: +false+).
+ # * <tt>:submit_value</tt> - The text of the submit button (default: "Create" if a new record, otherwise "Update").
+ def form(record_name, options = {})
+ record = instance_variable_get("@#{record_name}")
+ record = convert_to_model(record)
+
+ options = options.symbolize_keys
+ options[:action] ||= record.persisted? ? "update" : "create"
+ action = url_for(:action => options[:action], :id => record)
+
+ submit_value = options[:submit_value] || options[:action].gsub(/[^\w]/, '').capitalize
+
+ contents = form_tag({:action => action}, :method =>(options[:method] || 'post'), :enctype => options[:multipart] ? 'multipart/form-data': nil)
+ contents.safe_concat hidden_field(record_name, :id) if record.persisted?
+ contents.safe_concat all_input_tags(record, record_name, options)
+ yield contents if block_given?
+ contents.safe_concat submit_tag(submit_value)
+ contents.safe_concat('</form>')
+ end
+
+ # Returns a string containing the error message attached to the +method+ on the +object+ if one exists.
+ # This error message is wrapped in a <tt>DIV</tt> tag by default or with <tt>:html_tag</tt> if specified,
+ # which can be extended to include a <tt>:prepend_text</tt> and/or <tt>:append_text</tt> (to properly explain
+ # the error), and a <tt>:css_class</tt> to style it accordingly. +object+ should either be the name of an
+ # instance variable or the actual object. The method can be passed in either as a string or a symbol.
+ # As an example, let's say you have a model <tt>@post</tt> that has an error message on the +title+ attribute:
+ #
+ # <%= error_message_on "post", "title" %>
+ # # => <div class="formError">can't be empty</div>
+ #
+ # <%= error_message_on @post, :title %>
+ # # => <div class="formError">can't be empty</div>
+ #
+ # <%= error_message_on "post", "title",
+ # :prepend_text => "Title simply ",
+ # :append_text => " (or it won't work).",
+ # :html_tag => "span",
+ # :css_class => "inputError" %>
+ # # => <span class="inputError">Title simply can't be empty (or it won't work).</span>
+ def error_message_on(object, method, *args)
+ options = args.extract_options!
+ unless args.empty?
+ ActiveSupport::Deprecation.warn('error_message_on takes an option hash instead of separate ' +
+ 'prepend_text, append_text, html_tag, and css_class arguments', caller)
+
+ options[:prepend_text] = args[0] || ''
+ options[:append_text] = args[1] || ''
+ options[:html_tag] = args[2] || 'div'
+ options[:css_class] = args[3] || 'formError'
+ end
+ options.reverse_merge!(:prepend_text => '', :append_text => '', :html_tag => 'div', :css_class => 'formError')
+
+ object = convert_to_model(object)
+
+ if (obj = (object.respond_to?(:errors) ? object : instance_variable_get("@#{object}"))) &&
+ (errors = obj.errors[method]).presence
+ content_tag(options[:html_tag],
+ (options[:prepend_text].html_safe << errors.first).safe_concat(options[:append_text]),
+ :class => options[:css_class]
+ )
+ else
+ ''
+ end
+ end
+
+ # Returns a string with a <tt>DIV</tt> containing all of the error messages for the objects located as instance variables by the names
+ # given. If more than one object is specified, the errors for the objects are displayed in the order that the object names are
+ # provided.
+ #
+ # This <tt>DIV</tt> can be tailored by the following options:
+ #
+ # * <tt>:header_tag</tt> - Used for the header of the error div (default: "h2").
+ # * <tt>:id</tt> - The id of the error div (default: "errorExplanation").
+ # * <tt>:class</tt> - The class of the error div (default: "errorExplanation").
+ # * <tt>:object</tt> - The object (or array of objects) for which to display errors,
+ # if you need to escape the instance variable convention.
+ # * <tt>:object_name</tt> - The object name to use in the header, or any text that you prefer.
+ # If <tt>:object_name</tt> is not set, the name of the first object will be used.
+ # * <tt>:header_message</tt> - The message in the header of the error div. Pass +nil+
+ # or an empty string to avoid the header message altogether. (Default: "X errors
+ # prohibited this object from being saved").
+ # * <tt>:message</tt> - The explanation message after the header message and before
+ # the error list. Pass +nil+ or an empty string to avoid the explanation message
+ # altogether. (Default: "There were problems with the following fields:").
+ #
+ # To specify the display for one object, you simply provide its name as a parameter.
+ # For example, for the <tt>@user</tt> model:
+ #
+ # error_messages_for 'user'
+ #
+ # You can also supply an object:
+ #
+ # error_messages_for @user
+ #
+ # This will use the last part of the model name in the presentation. For instance, if
+ # this is a MyKlass::User object, this will use "user" as the name in the String. This
+ # is taken from MyKlass::User.model_name.human, which can be overridden.
+ #
+ # To specify more than one object, you simply list them; optionally, you can add an extra <tt>:object_name</tt> parameter, which
+ # will be the name used in the header message:
+ #
+ # error_messages_for 'user_common', 'user', :object_name => 'user'
+ #
+ # You can also use a number of objects, which will have the same naming semantics
+ # as a single object.
+ #
+ # error_messages_for @user, @post
+ #
+ # If the objects cannot be located as instance variables, you can add an extra <tt>:object</tt> parameter which gives the actual
+ # object (or array of objects to use):
+ #
+ # error_messages_for 'user', :object => @question.user
+ #
+ # NOTE: This is a pre-packaged presentation of the errors with embedded strings and a certain HTML structure. If what
+ # you need is significantly different from the default presentation, it makes plenty of sense to access the <tt>object.errors</tt>
+ # instance yourself and set it up. View the source of this method to see how easy it is.
+ def error_messages_for(*params)
+ options = params.extract_options!.symbolize_keys
+
+ objects = Array.wrap(options.delete(:object) || params).map do |object|
+ object = instance_variable_get("@#{object}") unless object.respond_to?(:to_model)
+ object = convert_to_model(object)
+
+ if object.class.respond_to?(:model_name)
+ options[:object_name] ||= object.class.model_name.human.downcase
+ end
+
+ object
+ end
+
+ objects.compact!
+ count = objects.inject(0) {|sum, object| sum + object.errors.count }
+
+ unless count.zero?
+ html = {}
+ [:id, :class].each do |key|
+ if options.include?(key)
+ value = options[key]
+ html[key] = value unless value.blank?
+ else
+ html[key] = 'errorExplanation'
+ end
+ end
+ options[:object_name] ||= params.first
+
+ I18n.with_options :locale => options[:locale], :scope => [:errors, :template] do |locale|
+ header_message = if options.include?(:header_message)
+ options[:header_message]
+ else
+ locale.t :header, :count => count, :model => options[:object_name].to_s.gsub('_', ' ')
+ end
+
+ message = options.include?(:message) ? options[:message] : locale.t(:body)
+
+ error_messages = objects.sum do |object|
+ object.errors.full_messages.map do |msg|
+ content_tag(:li, msg)
+ end
+ end.join.html_safe
+
+ contents = ''
+ contents << content_tag(options[:header_tag] || :h2, header_message) unless header_message.blank?
+ contents << content_tag(:p, message) unless message.blank?
+ contents << content_tag(:ul, error_messages)
+
+ content_tag(:div, contents.html_safe, html)
+ end
+ else
+ ''
+ end
+ end
+
+ private
+
+ def all_input_tags(record, record_name, options)
+ input_block = options[:input_block] || default_input_block
+ record.class.content_columns.collect{ |column| input_block.call(record_name, column) }.join("\n")
+ end
+
+ def default_input_block
+ Proc.new { |record, column| %(<p><label for="#{record}_#{column.name}">#{column.human_name}</label><br />#{input(record, column.name)}</p>) }
+ end
+
+ module InstanceTagMethods
+ def to_tag(options = {})
+ case column_type
+ when :string
+ field_type = @method_name.include?("password") ? "password" : "text"
+ to_input_field_tag(field_type, options)
+ when :text
+ to_text_area_tag(options)
+ when :integer, :float, :decimal
+ to_input_field_tag("text", options)
+ when :date
+ to_date_select_tag(options)
+ when :datetime, :timestamp
+ to_datetime_select_tag(options)
+ when :time
+ to_time_select_tag(options)
+ when :boolean
+ to_boolean_select_tag(options)
+ end
+ end
+
+ def column_type
+ object.send(:column_for_attribute, @method_name).type
+ end
+ end
+
+ module FormBuilderMethods
+ def error_message_on(method, *args)
+ @template.error_message_on(@object || @object_name, method, *args)
+ end
+
+ def error_messages(options = {})
+ @template.error_messages_for(@object_name, objectify_options(options))
+ end
+ end
+ end
+
+ class InstanceTag
+ include DynamicForm::InstanceTagMethods
+ end
+
+ class FormBuilder
+ include DynamicForm::FormBuilderMethods
+ end
+ end
+end
+
+I18n.load_path << File.expand_path("../../locale/en.yml", __FILE__)
(DIR) diff --git a/web/vendor/plugins/dynamic_form/lib/action_view/locale/en.yml b/web/vendor/plugins/dynamic_form/lib/action_view/locale/en.yml
@@ -0,0 +1,8 @@
+en:
+ errors:
+ template:
+ header:
+ one: "1 error prohibited this %{model} from being saved"
+ other: "%{count} errors prohibited this %{model} from being saved"
+ # The variable :count is also available
+ body: "There were problems with the following fields:"
(DIR) diff --git a/web/vendor/plugins/dynamic_form/lib/dynamic_form.rb b/web/vendor/plugins/dynamic_form/lib/dynamic_form.rb
@@ -0,0 +1,5 @@
+require 'action_view/helpers/dynamic_form'
+
+class ActionView::Base
+ include DynamicForm
+end
(DIR) diff --git a/web/vendor/plugins/dynamic_form/test/dynamic_form_i18n_test.rb b/web/vendor/plugins/dynamic_form/test/dynamic_form_i18n_test.rb
@@ -0,0 +1,42 @@
+require 'test_helper'
+
+class DynamicFormI18nTest < Test::Unit::TestCase
+ include ActionView::Context
+ include ActionView::Helpers::DynamicForm
+
+ attr_reader :request
+
+ def setup
+ @object = stub :errors => stub(:count => 1, :full_messages => ['full_messages'])
+ @object.stubs :to_model => @object
+ @object.stubs :class => stub(:model_name => stub(:human => ""))
+
+ @object_name = 'book_seller'
+ @object_name_without_underscore = 'book seller'
+
+ stubs(:content_tag).returns 'content_tag'
+
+ I18n.stubs(:t).with(:'header', :locale => 'en', :scope => [:errors, :template], :count => 1, :model => '').returns "1 error prohibited this from being saved"
+ I18n.stubs(:t).with(:'body', :locale => 'en', :scope => [:errors, :template]).returns 'There were problems with the following fields:'
+ end
+
+ def test_error_messages_for_given_a_header_option_it_does_not_translate_header_message
+ I18n.expects(:t).with(:'header', :locale => 'en', :scope => [:errors, :template], :count => 1, :model => '').never
+ error_messages_for(:object => @object, :header_message => 'header message', :locale => 'en')
+ end
+
+ def test_error_messages_for_given_no_header_option_it_translates_header_message
+ I18n.expects(:t).with(:'header', :locale => 'en', :scope => [:errors, :template], :count => 1, :model => '').returns 'header message'
+ error_messages_for(:object => @object, :locale => 'en')
+ end
+
+ def test_error_messages_for_given_a_message_option_it_does_not_translate_message
+ I18n.expects(:t).with(:'body', :locale => 'en', :scope => [:errors, :template]).never
+ error_messages_for(:object => @object, :message => 'message', :locale => 'en')
+ end
+
+ def test_error_messages_for_given_no_message_option_it_translates_message
+ I18n.expects(:t).with(:'body', :locale => 'en', :scope => [:errors, :template]).returns 'There were problems with the following fields:'
+ error_messages_for(:object => @object, :locale => 'en')
+ end
+end
+\ No newline at end of file
(DIR) diff --git a/web/vendor/plugins/dynamic_form/test/dynamic_form_test.rb b/web/vendor/plugins/dynamic_form/test/dynamic_form_test.rb
@@ -0,0 +1,370 @@
+require 'test_helper'
+require 'action_view/template/handlers/erb'
+
+class DynamicFormTest < ActionView::TestCase
+ tests ActionView::Helpers::DynamicForm
+
+ def form_for(*)
+ @output_buffer = super
+ end
+
+ silence_warnings do
+ class Post < Struct.new(:title, :author_name, :body, :secret, :written_on)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+ end
+
+ class User < Struct.new(:email)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+ end
+
+ class Column < Struct.new(:type, :name, :human_name)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+ end
+ end
+
+ class DirtyPost
+ class Errors
+ def empty?
+ false
+ end
+
+ def count
+ 1
+ end
+
+ def full_messages
+ ["Author name can't be <em>empty</em>"]
+ end
+
+ def [](field)
+ ["can't be <em>empty</em>"]
+ end
+ end
+
+ def errors
+ Errors.new
+ end
+ end
+
+ def setup_post
+ @post = Post.new
+ def @post.errors
+ Class.new {
+ def [](field)
+ case field.to_s
+ when "author_name"
+ ["can't be empty"]
+ when "body"
+ ['foo']
+ else
+ []
+ end
+ end
+ def empty?() false end
+ def count() 1 end
+ def full_messages() [ "Author name can't be empty" ] end
+ }.new
+ end
+
+ def @post.persisted?() false end
+ def @post.to_param() nil end
+
+ def @post.column_for_attribute(attr_name)
+ Post.content_columns.select { |column| column.name == attr_name }.first
+ end
+
+ silence_warnings do
+ def Post.content_columns() [ Column.new(:string, "title", "Title"), Column.new(:text, "body", "Body") ] end
+ end
+
+ @post.title = "Hello World"
+ @post.author_name = ""
+ @post.body = "Back to the hill and over it again!"
+ @post.secret = 1
+ @post.written_on = Date.new(2004, 6, 15)
+ end
+
+ def setup_user
+ @user = User.new
+ def @user.errors
+ Class.new {
+ def [](field) field == "email" ? ['nonempty'] : [] end
+ def empty?() false end
+ def count() 1 end
+ def full_messages() [ "User email can't be empty" ] end
+ }.new
+ end
+
+ def @user.new_record?() true end
+ def @user.to_param() nil end
+
+ def @user.column_for_attribute(attr_name)
+ User.content_columns.select { |column| column.name == attr_name }.first
+ end
+
+ silence_warnings do
+ def User.content_columns() [ Column.new(:string, "email", "Email") ] end
+ end
+
+ @user.email = ""
+ end
+
+ def protect_against_forgery?
+ @protect_against_forgery ? true : false
+ end
+ attr_accessor :request_forgery_protection_token, :form_authenticity_token
+
+ def setup
+ super
+ setup_post
+ setup_user
+
+ @response = ActionController::TestResponse.new
+ end
+
+ def url_for(options)
+ options = options.symbolize_keys
+ [options[:action], options[:id].to_param].compact.join('/')
+ end
+
+ def test_generic_input_tag
+ assert_dom_equal(
+ %(<input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />), input("post", "title")
+ )
+ end
+
+ def test_text_area_with_errors
+ assert_dom_equal(
+ %(<div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div>),
+ text_area("post", "body")
+ )
+ end
+
+ def test_text_field_with_errors
+ assert_dom_equal(
+ %(<div class="fieldWithErrors"><input id="post_author_name" name="post[author_name]" size="30" type="text" value="" /></div>),
+ text_field("post", "author_name")
+ )
+ end
+
+ def test_field_error_proc
+ old_proc = ActionView::Base.field_error_proc
+ ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
+ %(<div class=\"fieldWithErrors\">#{html_tag} <span class="error">#{[instance.error_message].join(', ')}</span></div>).html_safe
+ end
+
+ assert_dom_equal(
+ %(<div class="fieldWithErrors"><input id="post_author_name" name="post[author_name]" size="30" type="text" value="" /> <span class="error">can't be empty</span></div>),
+ text_field("post", "author_name")
+ )
+ ensure
+ ActionView::Base.field_error_proc = old_proc if old_proc
+ end
+
+ def test_form_with_string
+ assert_dom_equal(
+ %(<form action="create" method="post"><p><label for="post_title">Title</label><br /><input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /></p>\n<p><label for="post_body">Body</label><br /><div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div></p><input name="commit" type="submit" value="Create" /></form>),
+ form("post")
+ )
+
+ silence_warnings do
+ class << @post
+ def persisted?() true end
+ def to_param() id end
+ def id() 1 end
+ end
+ end
+
+ assert_dom_equal(
+ %(<form action="update/1" method="post"><input id="post_id" name="post[id]" type="hidden" value="1" /><p><label for="post_title">Title</label><br /><input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /></p>\n<p><label for="post_body">Body</label><br /><div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div></p><input name="commit" type="submit" value="Update" /></form>),
+ form("post")
+ )
+ end
+
+ def test_form_with_protect_against_forgery
+ @protect_against_forgery = true
+ @request_forgery_protection_token = 'authenticity_token'
+ @form_authenticity_token = '123'
+ assert_dom_equal(
+ %(<form action="create" method="post"><div style='margin:0;padding:0;display:inline'><input type='hidden' name='authenticity_token' value='123' /></div><p><label for="post_title">Title</label><br /><input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /></p>\n<p><label for="post_body">Body</label><br /><div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div></p><input name="commit" type="submit" value="Create" /></form>),
+ form("post")
+ )
+ end
+
+ def test_form_with_method_option
+ assert_dom_equal(
+ %(<form action="create" method="get"><p><label for="post_title">Title</label><br /><input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /></p>\n<p><label for="post_body">Body</label><br /><div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div></p><input name="commit" type="submit" value="Create" /></form>),
+ form("post", :method=>'get')
+ )
+ end
+
+ def test_form_with_action_option
+ output_buffer << form("post", :action => "sign")
+ assert_select "form[action=sign]" do |form|
+ assert_select "input[type=submit][value=Sign]"
+ end
+ end
+
+ def test_form_with_date
+ silence_warnings do
+ def Post.content_columns() [ Column.new(:date, "written_on", "Written on") ] end
+ end
+
+ assert_dom_equal(
+ %(<form action="create" method="post"><p><label for="post_written_on">Written on</label><br /><select id="post_written_on_1i" name="post[written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n<select id="post_written_on_2i" name="post[written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n<select id="post_written_on_3i" name="post[written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n</p><input name="commit" type="submit" value="Create" /></form>),
+ form("post")
+ )
+ end
+
+ def test_form_with_datetime
+ silence_warnings do
+ def Post.content_columns() [ Column.new(:datetime, "written_on", "Written on") ] end
+ end
+ @post.written_on = Time.gm(2004, 6, 15, 16, 30)
+
+ assert_dom_equal(
+ %(<form action="create" method="post"><p><label for="post_written_on">Written on</label><br /><select id="post_written_on_1i" name="post[written_on(1i)]">\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n</select>\n<select id="post_written_on_2i" name="post[written_on(2i)]">\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n</select>\n<select id="post_written_on_3i" name="post[written_on(3i)]">\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n</select>\n — <select id="post_written_on_4i" name="post[written_on(4i)]">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n</select>\n : <select id="post_written_on_5i" name="post[written_on(5i)]">\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30" selected="selected">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n</select>\n</p><input name="commit" type="submit" value="Create" /></form>),
+ form("post")
+ )
+ end
+
+ def test_error_for_block
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>), error_messages_for("post")
+ assert_equal %(<div class="errorDeathByClass" id="errorDeathById"><h1>1 error prohibited this post from being saved</h1><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>), error_messages_for("post", :class => "errorDeathByClass", :id => "errorDeathById", :header_tag => "h1")
+ assert_equal %(<div id="errorDeathById"><h1>1 error prohibited this post from being saved</h1><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>), error_messages_for("post", :class => nil, :id => "errorDeathById", :header_tag => "h1")
+ assert_equal %(<div class="errorDeathByClass"><h1>1 error prohibited this post from being saved</h1><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>), error_messages_for("post", :class => "errorDeathByClass", :id => nil, :header_tag => "h1")
+ end
+
+ def test_error_messages_for_escapes_html
+ @dirty_post = DirtyPost.new
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this dirty post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be <em>empty</em></li></ul></div>), error_messages_for("dirty_post")
+ end
+
+ def test_error_messages_for_handles_nil
+ assert_equal "", error_messages_for("notthere")
+ end
+
+ def test_error_message_on_escapes_html
+ @dirty_post = DirtyPost.new
+ assert_dom_equal "<div class=\"formError\">can't be <em>empty</em></div>", error_message_on(:dirty_post, :author_name)
+ end
+
+ def test_error_message_on_handles_nil
+ assert_equal "", error_message_on("notthere", "notthere")
+ end
+
+ def test_error_message_on
+ assert_dom_equal "<div class=\"formError\">can't be empty</div>", error_message_on(:post, :author_name)
+ end
+
+ def test_error_message_on_no_instance_variable
+ other_post = @post
+ assert_dom_equal "<div class=\"formError\">can't be empty</div>", error_message_on(other_post, :author_name)
+ end
+
+ def test_error_message_on_with_options_hash
+ assert_dom_equal "<div class=\"differentError\">beforecan't be emptyafter</div>", error_message_on(:post, :author_name, :css_class => 'differentError', :prepend_text => 'before', :append_text => 'after')
+ end
+
+ def test_error_message_on_with_tag_option_in_options_hash
+ assert_dom_equal "<span class=\"differentError\">beforecan't be emptyafter</span>", error_message_on(:post, :author_name, :html_tag => "span", :css_class => 'differentError', :prepend_text => 'before', :append_text => 'after')
+ end
+
+ def test_error_message_on_handles_empty_errors
+ assert_equal "", error_message_on(@post, :tag)
+ end
+
+ def test_error_messages_for_many_objects
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li><li>User email can't be empty</li></ul></div>), error_messages_for("post", "user")
+
+ # reverse the order, error order changes and so does the title
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this user from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for("user", "post")
+
+ # add the default to put post back in the title
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for("user", "post", :object_name => "post")
+
+ # symbols work as well
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for(:user, :post, :object_name => :post)
+
+ # any default works too
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this monkey from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for(:user, :post, :object_name => "monkey")
+
+ # should space object name
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this chunky bacon from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for(:user, :post, :object_name => "chunky_bacon")
+
+ # hide header and explanation messages with nil or empty string
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for(:user, :post, :header_message => nil, :message => "")
+
+ # override header and explanation messages
+ header_message = "Yikes! Some errors"
+ message = "Please fix the following fields and resubmit:"
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>#{header_message}</h2><p>#{message}</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for(:user, :post, :header_message => header_message, :message => message)
+ end
+
+ def test_error_messages_for_non_instance_variable
+ actual_user = @user
+ actual_post = @post
+ @user = nil
+ @post = nil
+
+ #explicitly set object
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>), error_messages_for("post", :object => actual_post)
+
+ #multiple objects
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this user from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>), error_messages_for("user", "post", :object => [actual_user, actual_post])
+
+ #nil object
+ assert_equal '', error_messages_for('user', :object => nil)
+ end
+
+ def test_error_messages_for_model_objects
+ error = error_messages_for(@post)
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>),
+ error
+
+ error = error_messages_for(@user, @post)
+ assert_dom_equal %(<div class="errorExplanation" id="errorExplanation"><h2>2 errors prohibited this user from being saved</h2><p>There were problems with the following fields:</p><ul><li>User email can't be empty</li><li>Author name can't be empty</li></ul></div>),
+ error
+ end
+
+ def test_form_with_string_multipart
+ assert_dom_equal(
+ %(<form action="create" enctype="multipart/form-data" method="post"><p><label for="post_title">Title</label><br /><input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /></p>\n<p><label for="post_body">Body</label><br /><div class="fieldWithErrors"><textarea cols="40" id="post_body" name="post[body]" rows="20">Back to the hill and over it again!</textarea></div></p><input name="commit" type="submit" value="Create" /></form>),
+ form("post", :multipart => true)
+ )
+ end
+
+ def test_default_form_builder_with_dynamic_form_helpers
+ form_for(@post, :as => :post, :url => {}) do |f|
+ concat f.error_message_on('author_name')
+ concat f.error_messages
+ end
+
+ expected = %(<form class="post_new" method="post" action="" id="post_new">) +
+ %(<div class="formError">can't be empty</div>) +
+ %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>) +
+ %(</form>)
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_default_form_builder_no_instance_variable
+ post = @post
+ @post = nil
+
+ form_for(post, :as => :post, :url => {}) do |f|
+ concat f.error_message_on('author_name')
+ concat f.error_messages
+ end
+
+ expected = %(<form class="post_new" method="post" action="" id="post_new">) +
+ %(<div class="formError">can't be empty</div>) +
+ %(<div class="errorExplanation" id="errorExplanation"><h2>1 error prohibited this post from being saved</h2><p>There were problems with the following fields:</p><ul><li>Author name can't be empty</li></ul></div>) +
+ %(</form>)
+
+ assert_dom_equal expected, output_buffer
+ end
+end
+\ No newline at end of file
(DIR) diff --git a/web/vendor/plugins/dynamic_form/test/test_helper.rb b/web/vendor/plugins/dynamic_form/test/test_helper.rb
@@ -0,0 +1,9 @@
+require 'rubygems'
+require 'test/unit'
+require 'active_support'
+require 'active_support/core_ext'
+require 'action_view'
+require 'action_controller'
+require 'action_controller/test_case'
+require 'active_model'
+require 'action_view/helpers/dynamic_form'
(DIR) diff --git a/web/vendor/plugins/ezgraphix/FusionChartsFreeLICENSE.textile b/web/vendor/plugins/ezgraphix/FusionChartsFreeLICENSE.textile
@@ -0,0 +1,48 @@
+h2. FusionCharts Free License Agreement
+
+InfoSoft Global grants you a nonexclusive right to use FusionCharts Free, subject to the following terms and conditions:
+
+FusionCharts Free can be used for free if you are a individual/research/commercial user.
+FusionCharts Free can be distributed for free with your free or commercial softwares, irrespective of whether they're open source or closed source.
+You must not sell FusionCharts Free as a component in itself. However, your commercial product can embed FusionCharts Free.
+You must not represent in any way that you're the author of FusionCharts Free.
+
+h3. RE-DISTRIBUTION AS A PART OF A PRODUCT/SOFTWARE/APPLICATION
+
+FusionCharts Free is completely free to OEM and distributed with your open or closed source applications. To do so, you just need to:
+Send us an email at support@fusioncharts.com with the following information:
+Your company name & address
+Name and description of your product with which you'll be re-distributing "FusionCharts Free"
+Mention "FusionCharts Free" with relevant link back to www.fusioncharts.com/free in your product or its documentation.
+We would also appreciate a link back from your product website to our website www.fusioncharts.com/free.
+
+h3. AS-IS DISTRIBUTION
+
+InfoSoft Global allows and encourages 3rd parties to distribute and make as many Exact copies as you wish of FusionCharts Free and distribute it by Internet, BBS's, Shareware distribution libraries, CD-ROMs, etc provided that the following terms are satisfied:
+Exact copies of the complete software package, and that it is unmodified and individual package and not as a part of any other application/product/software.
+The Software may not be made available on any site, CD-ROM, or with any package which makes available or contains viruses, virus source code, virus construction programs, or virus creation material
+Nothing may be charged for the software other than a nominal fee for the distribution costs
+You must disclose that this product is copyrighted by InfoSoft Global
+You must not represent in any way that you are selling the software itself
+
+InfoSoft Global would be thankful to be notified of your distribution efforts: support@fusioncharts.com
+
+h3. RESTRICTIONS
+
+You agree not to modify, adapt, translate, reverse engineer, decompile, disassemble or otherwise attempt to discover the source code of the Software.
+
+h3. OWNERSHIP & COPYRIGHT
+
+The Software is owned by InfoSoft Global and is protected by international copyright law and treaty provisions. Your license confers no title or ownership in the Software and should not be construed as a sale of any right in the Software. You further acknowledge that title and full ownership rights to the Software will remain the exclusive property of InfoSoft Global and you will not acquire any rights to the Software except as expressly set forth in this license. You agree that any copies of the Software will contain the same proprietary notices which appear on and in the Software.
+
+h3. WARRANTY
+
+InfoSoft Global expressly disclaims any warranty for the software. THE SOFTWARE AND ANY RELATED DOCUMENTATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OR MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NONINFRINGEMENT. THE ENTIRE RISK ARISING OUT OF USE OR PERFORMANCE OF THE SOFTWARE REMAINS WITH YOU.
+
+h3. INDEMNIFICATION
+
+You hereby imdemnify and hold harmless InfoSoft Global and its affiliates against any loss, liability, damages, costs or expenses suffered or incurred by the Indemnified Parties at any time as a result of any claim, action or proceeding arising out of or relating to your use, operation or damages, costs or expenses which may be suffered or incurred at any time by you as a result of your reliance upon or use of the Software. InfoSoft Global SHALL NOT BE LIABLE FOR DAMAGES OF ANY KIND, INCLUDING GENERAL, DIRECT, SPECIAL, INCIDENTAL AND CONSEQUENTIAL DAMAGES, RESULTING FROM OR ARISING OUT OF THIS AGREEMENT OR YOUR USE OF THE SOFTWARE.
+
+h3. TERM AND TERMINATION.
+
+This Agreement shall commence on the Effective Date of installation of software and shall remain in full force and effect until terminated, or through the mutual written consent of the parties (the “Term”).
+\ No newline at end of file
(DIR) diff --git a/web/vendor/plugins/ezgraphix/README.textile b/web/vendor/plugins/ezgraphix/README.textile
@@ -0,0 +1,157 @@
+h2. EZGraphix
+
+h3. Documentation
+
+"API Documentation (generated with RDoc)":http://ezgraphix.rubyforge.org
+
+h3. Demo
+
+"Online Demo (hosted by Heroku)":http://ezgraphixdemo.heroku.com/
+
+h3. Installation Notes
+
+<b>Install the plugin:</b>
+
+<pre>script/plugin install git://github.com/jpemberthy/ezgraphix.git</pre>
+
+<b>Run the tasks:</b> (it will move the necessary files to use the plugin)
+
+<pre>rake ezgraphix:setup # run from your project's root </pre>
+
+After the installation <b>include the FusionCharts.js</b> in each layout that will render ezgraphix:
+
+<pre><%= javascript_include_tag "FusionCharts" %></pre>
+
+and that's it!.
+
+h3. Usage
+
+<b> In your Controller: </b>
+
+For single series charts:
+
+<pre>
+def index
+ @g = Ezgraphix::Graphic.new
+ @g.data = {:ruby => 1, :perl => 2, :smalltalk => 3}
+end
+</pre>
+
+or you can specify the data directly in the constructor:
+
+<pre>
+def index
+ @g = Ezgraphix::Graphic.new(:data => {:ruby => 1, :perl => 2, :smalltalk => 3})
+end
+</pre>
+
+
+For multiseries charts:
+
+In this case you have multiple values for each serie. Also you need to specify categories (labels). In the example below there are 2 series "Registrations" and "Payments" and values are divided into 7 months (labels) with a value for each serie/month.
+
+<pre>
+def index
+ @g = Ezgraphix::Graphic.new(:c_type => 'mscol3d')
+ @g.data = { "Registrations" => [4,2,12,7,0,3,6], "Payments" => [7,3,5,2,1,0,9] }
+ @g.labels = ["March", "April", "May", "June", "July", "August", "September"]
+end
+</pre>
+
+You can also pass the data collection to the constructor:
+
+<pre>
+ @g = Ezgraphix::Graphic.new(:data => {:ruby => 1, :perl => 2, :smalltalk => 3})
+</pre>
+
+There are 2 ways to <b> pass render options: </b>
+
+<b>1.</b> When the Graphic object is created, you can define it also as:
+
+<pre>@g = Ezgraphix::Graphic.new(:w => 200, :h => 300, :c_type => 'bar3d', :div_name => 'my_chart_tag_name')</pre>
+
+The defaults are: <pre>{:c_type => 'col3d', :w => 300, :h => 300, :div_name => 'ez_graphic'}</pre>
+At the moment, you can render the following graphics:
+
+<b> Single Series </b>
+
+<pre>
+Column 2D => :c_type => 'col2d'
+Column 3D => :c_type => 'col3d'
+Bar 2D => :c_type => 'bar2d'
+Pie 2D => :c_type => 'pie2d'
+Pie 3D => :c_type => 'pie3d'
+Line => :c_type => 'line'
+Doughnut 2D => :c_type => 'doug2d'
+</pre>
+
+<b> Multi Series </b>
+
+<pre>
+MultiSeriesLine => :ctype => 'msline'
+MultiSeriesColumn3D => :ctype => 'mscol3d'
+MultiSeriesColumn2D => :ctype => 'mscol2d'
+MultiSeriesArea2D => :ctype => 'msarea2d'
+MultiSeriesBar2D => :ctype => 'msbar2d'
+</pre>
+
+<b>2.</b> Anytime you want, by accessing the render_options attribute.
+
+<pre>@g.render_options(:caption => 'cool languages', :w => 400) #Merges new options with the last configuration.</pre>
+
+<b> In your view. </b>
+
+Add the following line wherever you want to render the graphic.
+
+<pre><%= render_ezgraphix @g %></pre>
+
+or just
+
+<pre><%= @g %></pre>
+
+<b> Tests </b>
+
+<pre> rake spec #run from the plugin's </pre>
+
+<b> Note.</b>
+
+Full set of render_options are specified in the rdoc, and <b>more chart's support will be included soon!.</b>
+
+<b> Pending </b>
+
+Full and detailed specs.
+
+h3. EZGraphix
+
+A Ruby gem to generate flash based graphics for rails applications using a free and customizable chart's set.
+
+Copyright (c) 2008 Juan Esteban Pemberthy, released under the MIT License.
+
+EzGraphix uses FusionCharts Free, It's license is specified in the FusionChartsFreeLICENSE file.
+
+Fork and feel free to contribute!
+
+h3. LICENSE:
+
+(The MIT License)
+
+Copyright (c) 2008 Juan Esteban Pemberthy
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/init.rb b/web/vendor/plugins/ezgraphix/init.rb
@@ -0,0 +1,2 @@
+require 'ezgraphix'
+ActionView::Base.send :include, EzgraphixHelper
+\ No newline at end of file
(DIR) diff --git a/web/vendor/plugins/ezgraphix/lib/ezgraphix.rb b/web/vendor/plugins/ezgraphix/lib/ezgraphix.rb
@@ -0,0 +1,199 @@
+# == ezgraphix.rb
+# This file contains the Ezgraphix module, and the Ezgraphix::Graphic class.
+#
+# == Summary
+# A rails plugin to generate flash based graphics
+# for rails applications using a free and customizable chart's set.
+#
+# == Installation
+# Instructions are listed in the respository's README[http://github.com/jpemberthy/ezgraphix/tree/master/README.textile]
+#
+# == Online demo
+# Online demo[http://ezgraphixdemo.heroku.com/] Hosted by Heroku!
+#
+# == Contact
+#
+# Author:: Juan E Pemberthy
+# Mail:: jpemberthy@gmail.com
+# Copyright:: Copyright (c) 2008
+# License:: Distributes under MIT License.
+
+unless defined? Ezgraphix
+ module Ezgraphix
+ require File.dirname(__FILE__) + '/ezgraphix/ezgraphix_helper'
+ require 'builder'
+
+ # This class contains the neccesary methods and attributes to render a Graphic,
+ # most of time you will be playing with the render_options and _data_ attributes to
+ # define the graphic's properties, also you can re-define those properties easily by accessing them
+ # at any time.
+ #
+ # == Example
+ # Define the Graphic in your controller.
+ # @g = Ezgraphix::Graphic.new # render_options can also be passed from here,
+ # # @g = Ezgraphix::Graphic.new(:div_name => 'my_graph', :w => 400)
+ #
+ # @g.defaults
+ # => {:c_type=>'col3d', :div_name=>'ez_graphic', :w=>300, :h=>300}
+ #
+ # @g.render_options #equals to defaults if not options were passed to the initializer.
+ # => {:c_type=>'col3d', :div_name=>'ez_graphic', :w=>300, :h=>300}
+ #
+ # It's always a good idea to change the div_name if your planning to render more
+ # than one Graphic in the same page, this makes the graphic unique.
+ # @g.render_options(:div_name => 'my_graph')
+ # => {:c_type=>'col3d', :div_name=>'my_graph', :w=>300, :h=>300}
+ #
+ # In order to render, you have to feed the graphic with data you want to show, Ezgraphix uses
+ # a Hash to represent that data where the keys represents names, for example:
+ # @g.data = {:ruby => 1, :perl => 2, :smalltalk => 3}
+ # => {:smalltalk => 3, :ruby => 1, :perl => 2}
+ #
+ # With this information, the graphic will be a column 3D, with a size of 300x300 pixels, indentified with the
+ # "my_graph" name, with 3 columns containing the names: 'ruby', 'perl', and 'smalltalk' for the values 1,2,3 respectively.
+ #
+ # To render the graphic, from a view call the render_ezgraphix method defined in the Ezgraphix::Helper module.
+ # <%= render_ezgraphix @g %>
+ #
+ class Graphic
+ include EzgraphixHelper
+
+ #Hash containing the names and values to render.
+ attr_accessor :data
+
+ # Array containing the categories to render multi series charts
+ attr_accessor :labels
+
+ # Hash containing all the render options. basic options are:
+ # * <tt> :c_type</tt> -- Chart type to render.
+ # * <tt> :div_name</tt> -- Name for the graphic, should be unique.
+ # * <tt> :w </tt> -- Width in pixels.
+ # * <tt> :h </tt> -- Height in pixels.
+ # Full list of options are listed below render_options
+ attr_accessor :render_options
+
+ COLORS = ['AFD8f6', '8E468E', '588526', 'B3A000', 'B2FF66',
+ 'F984A1', 'A66EDD', 'B20000', '3300CC', '000033',
+ '66FF33', '000000', 'FFFF00', '669966', 'FF3300',
+ 'F19CBB', '9966CC', '00FFFF', '4B5320', '007FFF',
+ '0000FF', '66FF00', 'CD7F32', '964B00', 'CC5500']
+
+ #Creates a new Graphic with the given _options_, if no _options_ are specified,
+ #the new Graphic will be initalized with the Graphic#defaults options.
+ def initialize(options={})
+ @render_options = defaults.merge!(options)
+ @data = options[:data] || Hash.new
+ end
+
+ #Returns defaults render options.
+ def defaults
+ {:c_type => 'col3d', :w => 300, :h => 300, :div_name => 'ez_graphic'}
+ end
+
+ # Receives a Hash containing a set of render options that will be merged with the current configuration.
+ #
+ # ==== Options
+ # Basics:
+ # * <tt>:c_type</tt></tt> -- Chart type to render, default: "col3d" for Column 3D, supported chars:
+ # :c_type => "col3d"
+ # :c_type => "bar3d" #Bar3D
+ # :c_type => "bar2d" #Bar2D
+ # :c_type => "pie2d" #Pie2D
+ # :c_type => "pie3D" #Pie3D
+ # :c_type => "line" #Line
+ # :c_type => "doug2d" #Doughnut2D
+ # * <tt>:div_name</tt></tt> -- Name for the graphic, would be unique, default: "ez_graphic"
+ # * <tt>:w</tt></tt> -- Width in pixels, default: 300
+ # * <tt>:h</tt></tt> -- Height in pixels, default: 300
+ # * <tt> :caption</tt> -- Graphic's caption, default: ""
+ # * <tt> :subcaption</tt> -- Graphic's subcaption, default: ""
+ # * <tt> :y_name</tt> -- Y axis name, default: ""
+ # * <tt> :x_name</tt> -- X axis name, default: ""
+ # Numbers:
+ # * <tt> :prefix</tt> -- Prefix to values defined in the _data_ attribute, default: nil, some prefix could be
+ # :prefix => "$" or :prefix => "€"
+ # * <tt> :precision</tt> -- Number of decimal places to which all numbers on the chart would be rounded to, default: 2
+ # * <tt> :f_number</tt> -- Format number. if set to 0, numbers will not use separator, if set to 1 numbers will use separator
+ # * <tt> :d_separator</tt> -- Decimal Separator, default: "."
+ # * <tt> :t_separator</tt> -- Thousand Separator, default: ","
+ # Design:
+ # * <tt> :background</tt> -- Background Color
+ # * <tt> :names</tt> -- Hide/Show(0/1) labels names, default: 1
+ # * <tt> :values</tt> -- Hide/Show(0/1) Values, default: 1
+ # * <tt> :limits</tt> -- Hide/Show(0/1) Limits.
+ #
+ def render_options(options={})
+ @render_options.merge!(options)
+ end
+
+ #Returns the Graphic's type.
+ def c_type
+ self.render_options[:c_type]
+ end
+
+ #Returns the Graphic's width.
+ def w
+ self.render_options[:w]
+ end
+
+ #Returns the Graphic's height.
+ def h
+ self.render_options[:h]
+ end
+
+ #Returns the div's tag name would be unique if you want to render multiples graphics in the same page.
+ def div_name
+ self.render_options[:div_name]
+ end
+
+
+ #Returns a random color from the Graphic#COLORS collection.
+ def rand_color
+ @available_colors = COLORS.clone if @available_colors.to_a.empty?
+ @available_colors.delete_at rand(@available_colors.size)
+ end
+
+ #Builds the xml to feed the chart.
+ def to_xml
+ options = parse_options(self.render_options)
+ g_xml = Builder::XmlMarkup.new
+ #For single series charts
+ if ["area2d"].include? self.c_type
+ # These graphics should be one color only
+ escaped_xml = g_xml.graph(options) do
+ self.data.each{ |k,v|
+ g_xml.set :value => v, :name => k
+ }
+ end
+ elsif !['msline', 'mscol2d', 'msbar2d', 'mscol3d'].include?(self.c_type)
+ escaped_xml = g_xml.graph(options) do
+ self.data.each{ |k,v|
+ g_xml.set :value => v, :name => k, :color => self.rand_color
+ }
+ end
+ else
+ #For multiseries charts
+ escaped_xml = g_xml.graph(options) do
+ g_xml.categories do
+ for label in self.labels
+ g_xml.category :name => label
+ end
+ end
+ for d in self.data
+ g_xml.dataset(:color => self.rand_color, :seriesName => d.first ) do
+ d[1].each do |v|
+ g_xml.set :value => v
+ end
+ end
+ end
+ end
+ end
+ escaped_xml.gsub("\"", "'")
+ end
+
+ def to_s
+ render_ezgraphix self
+ end
+ end
+ end
+end
(DIR) diff --git a/web/vendor/plugins/ezgraphix/lib/ezgraphix/ezgraphix_helper.rb b/web/vendor/plugins/ezgraphix/lib/ezgraphix/ezgraphix_helper.rb
@@ -0,0 +1,107 @@
+module EzgraphixHelper
+ #method used in ActionView::Base to render graphics.
+ def render_ezgraphix(g)
+ result = ""
+ html = Builder::XmlMarkup.new(:target => result)
+ html.div("test", :id => g.div_name)
+ html = Builder::XmlMarkup.new(:target => result)
+ html.script(:type => 'text/javascript') do
+ html << "var ezChart = new FusionCharts('#{f_type(g.c_type)}','#{g.div_name}','#{g.w}','#{g.h}','0','0');\n"
+ html << "ezChart.setDataXML(\"#{g.to_xml}\");\n" unless g.data.is_a?(String)
+ html << "ezChart.setDataURL(\"#{g.data}\");\n" if g.data.is_a?(String)
+ html << "ezChart.render(\"#{g.div_name}\");\n"
+ end
+ result
+ end
+
+ def f_type(c_type)
+ case c_type
+ when 'area2d'
+ '/FusionCharts/FCF_Area2D.swf'
+ when 'col3d'
+ '/FusionCharts/FCF_Column3D.swf'
+ when 'bar2d'
+ '/FusionCharts/FCF_Bar2D.swf'
+ when 'barline3d'
+ '/FusionCharts/FCF_MSColumn3DLineDY.swf'
+ when 'col2d'
+ '/FusionCharts/FCF_Column2D.swf'
+ when 'pie2d'
+ '/FusionCharts/FCF_Pie2D.swf'
+ when 'pie3d'
+ '/FusionCharts/FCF_Pie3D.swf'
+ when 'line'
+ '/FusionCharts/FCF_Line.swf'
+ when 'doug2d'
+ '/FusionCharts/FCF_Doughnut2D.swf'
+ when 'msline'
+ '/FusionCharts/FCF_MSLine.swf'
+ when 'mscol3d'
+ '/FusionCharts/FCF_MSColumn3D.swf'
+ when 'mscol2d'
+ '/FusionCharts/FCF_MSColumn2D.swf'
+ when 'msarea2d'
+ '/FusionCharts/FCF_MSArea2D.swf'
+ when 'msbar2d'
+ '/FusionCharts/FCF_MSBar2D.swf'
+ end
+ end
+
+ def parse_options(options)
+ original_names = Hash.new
+
+ options.each{|k,v|
+ case k
+ when :animation
+ original_names['animation'] = v
+ when :y_name
+ original_names['yAxisName'] = v
+ when :caption
+ original_names['caption'] = v
+ when :subcaption
+ original_names['subCaption'] = v
+ when :prefix
+ original_names['numberPrefix'] = v
+ when :precision
+ original_names['decimalPrecision'] = v
+ when :div_line_precision
+ original_names['divlinedecimalPrecision'] = v
+ when :limits_precision
+ original_names['limitsdecimalPrecision'] = v
+ when :f_number
+ original_names['formatNumber'] = v
+ when :f_number_scale
+ original_names['formatNumberScale'] = v
+ when :rotate
+ original_names['rotateNames'] = v
+ when :background
+ original_names['bgColor'] = v
+ when :line
+ original_names['lineColor'] = v
+ when :names
+ original_names['showNames'] = v
+ when :values
+ original_names['showValues'] = v
+ when :limits
+ original_names['showLimits'] = v
+ when :y_lines
+ original_names['numdivlines'] = v
+ when :p_y
+ original_names['parentYAxis'] = v
+ when :d_separator
+ original_names['decimalSeparator'] = v
+ when :t_separator
+ original_name['thousandSeparator'] = v
+ when :left_label_name
+ original_names['PYAxisName'] = v
+ when :right_label_name
+ original_names['SYAxisName'] = v
+ when :x_name
+ original_names['xAxisName'] = v
+ when :show_column_shadow
+ original_names['showColumnShadow'] = v
+ end
+ }
+ original_names
+ end
+end
(DIR) diff --git a/web/vendor/plugins/ezgraphix/lib/tasks/ezgraphix_tasks.rake b/web/vendor/plugins/ezgraphix/lib/tasks/ezgraphix_tasks.rake
@@ -0,0 +1,19 @@
+ namespace :ezgraphix do
+ task :dir_setup do
+ Dir.mkdir("#{RAILS_ROOT}/public/FusionCharts", 0700)
+ puts "Created FusionCharts directory in public/"
+ end
+
+ task :cp_charts do
+ FileUtils.cp_r("#{RAILS_ROOT}/vendor/plugins/ezgraphix/public/FusionCharts/", "#{RAILS_ROOT}/public/")
+ puts "Charts copied."
+ end
+
+ task :cp_javascript do
+ FileUtils.cp_r("#{RAILS_ROOT}/vendor/plugins/ezgraphix/public/javascripts/FusionCharts.js", "#{RAILS_ROOT}/public/javascripts/")
+ puts "FusionCharts.js copied"
+ end
+
+ desc "Creates and copies all necessary files in order to use ezgraphix!"
+ task :setup => [:dir_setup, :cp_charts, :cp_javascript]
+ end
+\ No newline at end of file
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Area2D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Area2D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Bar2D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Bar2D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Candlestick.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Candlestick.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Column2D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Column2D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Column3D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Column3D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Doughnut2D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Doughnut2D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Funnel.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Funnel.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Gantt.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Gantt.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Line.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Line.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSArea2D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSArea2D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSBar2D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSBar2D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSColumn2D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSColumn2D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSColumn2DLineDY.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSColumn2DLineDY.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSColumn3D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSColumn3D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSColumn3DLineDY.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSColumn3DLineDY.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSLine.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_MSLine.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Pie2D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Pie2D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Pie3D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_Pie3D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_StackedArea2D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_StackedArea2D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_StackedBar2D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_StackedBar2D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_StackedColumn2D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_StackedColumn2D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_StackedColumn3D.swf b/web/vendor/plugins/ezgraphix/public/FusionCharts/FCF_StackedColumn3D.swf
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/ezgraphix/public/javascripts/FusionCharts.js b/web/vendor/plugins/ezgraphix/public/javascripts/FusionCharts.js
@@ -0,0 +1,361 @@
+/**
+ * FusionCharts: Flash Player detection and Chart embedding.
+ * Version 1.2.3F ( 22 November 2008) - Specialized for FusionChartsFREE
+ * Checking Flash Version >=6 and added updateChartXML() for FREE Charts.
+ * Version: 1.2.3 (1st September, 2008) - Added Fix for % and & characters, scaled dimensions, fixes in to properly handling of double quotes and single quotes in setDataXML() function.
+ * Version: 1.2.2 (10th July, 2008) - Added Fix for % scaled dimensions, fixes in setDataXML() and setDataURL() functions
+ * Version: 1.2.1 (21st December, 2007) - Added setting up Transparent/opaque mode: setTransparent() function
+ * Version: 1.2 (1st November, 2007) - Added FORM fixes for IE
+ * Version: 1.1 (29th June, 2007) - Added Player detection, New conditional fixes for IE
+ *
+ * Morphed from SWFObject (http://blog.deconcept.com/swfobject/) under MIT License:
+ * http://www.opensource.org/licenses/mit-license.php
+ *
+ */
+if(typeof infosoftglobal == "undefined") var infosoftglobal = new Object();
+if(typeof infosoftglobal.FusionChartsUtil == "undefined") infosoftglobal.FusionChartsUtil = new Object();
+infosoftglobal.FusionCharts = function(swf, id, w, h, debugMode, registerWithJS, c, scaleMode, lang, detectFlashVersion, autoInstallRedirect){
+ if (!document.getElementById) { return; }
+
+ //Flag to see whether data has been set initially
+ this.initialDataSet = false;
+
+ //Create container objects
+ this.params = new Object();
+ this.variables = new Object();
+ this.attributes = new Array();
+
+ //Set attributes for the SWF
+ if(swf) { this.setAttribute('swf', swf); }
+ if(id) { this.setAttribute('id', id); }
+
+ w=w.toString().replace(/\%$/,"%25");
+ if(w) { this.setAttribute('width', w); }
+ h=h.toString().replace(/\%$/,"%25");
+ if(h) { this.setAttribute('height', h); }
+
+
+ //Set background color
+ if(c) { this.addParam('bgcolor', c); }
+
+ //Set Quality
+ this.addParam('quality', 'high');
+
+ //Add scripting access parameter
+ this.addParam('allowScriptAccess', 'always');
+
+ //Pass width and height to be appended as chartWidth and chartHeight
+ this.addVariable('chartWidth', w);
+ this.addVariable('chartHeight', h);
+
+ //Whether in debug mode
+ debugMode = debugMode ? debugMode : 0;
+ this.addVariable('debugMode', debugMode);
+ //Pass DOM ID to Chart
+ this.addVariable('DOMId', id);
+ //Whether to registed with JavaScript
+ registerWithJS = registerWithJS ? registerWithJS : 0;
+ this.addVariable('registerWithJS', registerWithJS);
+
+ //Scale Mode of chart
+ scaleMode = scaleMode ? scaleMode : 'noScale';
+ this.addVariable('scaleMode', scaleMode);
+
+ //Application Message Language
+ lang = lang ? lang : 'EN';
+ this.addVariable('lang', lang);
+
+ //Whether to auto detect and re-direct to Flash Player installation
+ this.detectFlashVersion = detectFlashVersion?detectFlashVersion:1;
+ this.autoInstallRedirect = autoInstallRedirect?autoInstallRedirect:1;
+
+ //Ger Flash Player version
+ this.installedVer = infosoftglobal.FusionChartsUtil.getPlayerVersion();
+
+ if (!window.opera && document.all && this.installedVer.major > 7) {
+ // Only add the onunload cleanup if the Flash Player version supports External Interface and we are in IE
+ infosoftglobal.FusionCharts.doPrepUnload = true;
+ }
+}
+
+infosoftglobal.FusionCharts.prototype = {
+ setAttribute: function(name, value){
+ this.attributes[name] = value;
+ },
+ getAttribute: function(name){
+ return this.attributes[name];
+ },
+ addParam: function(name, value){
+ this.params[name] = value;
+ },
+ getParams: function(){
+ return this.params;
+ },
+ addVariable: function(name, value){
+ this.variables[name] = value;
+ },
+ getVariable: function(name){
+ return this.variables[name];
+ },
+ getVariables: function(){
+ return this.variables;
+ },
+ getVariablePairs: function(){
+ var variablePairs = new Array();
+ var key;
+ var variables = this.getVariables();
+ for(key in variables){
+ variablePairs.push(key +"="+ variables[key]);
+ }
+ return variablePairs;
+ },
+ getSWFHTML: function() {
+ var swfNode = "";
+ if (navigator.plugins && navigator.mimeTypes && navigator.mimeTypes.length) {
+ // netscape plugin architecture
+ swfNode = '<embed type="application/x-shockwave-flash" src="'+ this.getAttribute('swf') +'" width="'+ this.getAttribute('width') +'" height="'+ this.getAttribute('height') +'" ';
+ swfNode += ' id="'+ this.getAttribute('id') +'" name="'+ this.getAttribute('id') +'" ';
+ var params = this.getParams();
+ for(var key in params){ swfNode += [key] +'="'+ params[key] +'" '; }
+ var pairs = this.getVariablePairs().join("&");
+ if (pairs.length > 0){ swfNode += 'flashvars="'+ pairs +'"'; }
+ swfNode += '/>';
+ } else { // PC IE
+ swfNode = '<object id="'+ this.getAttribute('id') +'" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="'+ this.getAttribute('width') +'" height="'+ this.getAttribute('height') +'">';
+ swfNode += '<param name="movie" value="'+ this.getAttribute('swf') +'" />';
+ var params = this.getParams();
+ for(var key in params) {
+ swfNode += '<param name="'+ key +'" value="'+ params[key] +'" />';
+ }
+ var pairs = this.getVariablePairs().join("&");
+ if(pairs.length > 0) {swfNode += '<param name="flashvars" value="'+ pairs +'" />';}
+ swfNode += "</object>";
+ }
+ return swfNode;
+ },
+ setDataURL: function(strDataURL){
+ //This method sets the data URL for the chart.
+ //If being set initially
+ if (this.initialDataSet==false){
+ this.addVariable('dataURL',strDataURL);
+ //Update flag
+ this.initialDataSet = true;
+ }else{
+ //Else, we update the chart data using External Interface
+ //Get reference to chart object
+ var chartObj = infosoftglobal.FusionChartsUtil.getChartObject(this.getAttribute('id'));
+
+ if (!chartObj.setDataURL)
+ {
+ __flash__addCallback(chartObj, "setDataURL");
+ }
+
+ chartObj.setDataURL(strDataURL);
+ }
+ },
+ //This function :
+ //fixes the double quoted attributes to single quotes
+ //Encodes all quotes inside attribute values
+ //Encodes % to %25 and & to %26;
+ encodeDataXML: function(strDataXML){
+
+ var regExpReservedCharacters=["\\$","\\+"];
+ var arrDQAtt=strDataXML.match(/=\s*\".*?\"/g);
+ if (arrDQAtt){
+ for(var i=0;i<arrDQAtt.length;i++){
+ var repStr=arrDQAtt[i].replace(/^=\s*\"|\"$/g,"");
+ repStr=repStr.replace(/\'/g,"%26apos;");
+ var strTo=strDataXML.indexOf(arrDQAtt[i]);
+ var repStrr="='"+repStr+"'";
+ var strStart=strDataXML.substring(0,strTo);
+ var strEnd=strDataXML.substring(strTo+arrDQAtt[i].length);
+ var strDataXML=strStart+repStrr+strEnd;
+ }
+ }
+
+ strDataXML=strDataXML.replace(/\"/g,"%26quot;");
+ strDataXML=strDataXML.replace(/%(?![\da-f]{2}|[\da-f]{4})/ig,"%25");
+ strDataXML=strDataXML.replace(/\&/g,"%26");
+
+ return strDataXML;
+
+ },
+ setDataXML: function(strDataXML){
+ //If being set initially
+ if (this.initialDataSet==false){
+ //This method sets the data XML for the chart INITIALLY.
+ this.addVariable('dataXML',this.encodeDataXML(strDataXML));
+ //Update flag
+ this.initialDataSet = true;
+ }else{
+ //Else, we update the chart data using External Interface
+ //Get reference to chart object
+ var chartObj = infosoftglobal.FusionChartsUtil.getChartObject(this.getAttribute('id'));
+ chartObj.setDataXML(strDataXML);
+ }
+ },
+ setTransparent: function(isTransparent){
+ //Sets chart to transparent mode when isTransparent is true (default)
+ //When no parameter is passed, we assume transparent to be true.
+ if(typeof isTransparent=="undefined") {
+ isTransparent=true;
+ }
+ //Set the property
+ if(isTransparent)
+ this.addParam('WMode', 'transparent');
+ else
+ this.addParam('WMode', 'Opaque');
+ },
+
+ render: function(elementId){
+ //First check for installed version of Flash Player - we need a minimum of 6
+ if((this.detectFlashVersion==1) && (this.installedVer.major < 6)){
+ if (this.autoInstallRedirect==1){
+ //If we can auto redirect to install the player?
+ var installationConfirm = window.confirm("You need Adobe Flash Player 6 (or above) to view the charts. It is a free and lightweight installation from Adobe.com. Please click on Ok to install the same.");
+ if (installationConfirm){
+ window.location = "http://www.adobe.com/shockwave/download/download.cgi?P1_Prod_Version=ShockwaveFlash";
+ }else{
+ return false;
+ }
+ }else{
+ //Else, do not take an action. It means the developer has specified a message in the DIV (and probably a link).
+ //So, expect the developers to provide a course of way to their end users.
+ //window.alert("You need Adobe Flash Player 8 (or above) to view the charts. It is a free and lightweight installation from Adobe.com. ");
+ return false;
+ }
+ }else{
+ //Render the chart
+ var n = (typeof elementId == 'string') ? document.getElementById(elementId) : elementId;
+ n.innerHTML = this.getSWFHTML();
+
+ //Added <FORM> compatibility
+ //Check if it's added in Mozilla embed array or if already exits
+ if(!document.embeds[this.getAttribute('id')] && !window[this.getAttribute('id')])
+ window[this.getAttribute('id')]=document.getElementById(this.getAttribute('id'));
+ //or else document.forms[formName/formIndex][chartId]
+ return true;
+ }
+ }
+}
+
+/* ---- detection functions ---- */
+infosoftglobal.FusionChartsUtil.getPlayerVersion = function(){
+ var PlayerVersion = new infosoftglobal.PlayerVersion([0,0,0]);
+ if(navigator.plugins && navigator.mimeTypes.length){
+ var x = navigator.plugins["Shockwave Flash"];
+ if(x && x.description) {
+ PlayerVersion = new infosoftglobal.PlayerVersion(x.description.replace(/([a-zA-Z]|\s)+/, "").replace(/(\s+r|\s+b[0-9]+)/, ".").split("."));
+ }
+ }else if (navigator.userAgent && navigator.userAgent.indexOf("Windows CE") >= 0){
+ //If Windows CE
+ var axo = 1;
+ var counter = 3;
+ while(axo) {
+ try {
+ counter++;
+ axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash."+ counter);
+ PlayerVersion = new infosoftglobal.PlayerVersion([counter,0,0]);
+ } catch (e) {
+ axo = null;
+ }
+ }
+ } else {
+ // Win IE (non mobile)
+ // Do minor version lookup in IE, but avoid Flash Player 6 crashing issues
+ try{
+ var axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");
+ }catch(e){
+ try {
+ var axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");
+ PlayerVersion = new infosoftglobal.PlayerVersion([6,0,21]);
+ axo.AllowScriptAccess = "always"; // error if player version < 6.0.47 (thanks to Michael Williams @ Adobe for this code)
+ } catch(e) {
+ if (PlayerVersion.major == 6) {
+ return PlayerVersion;
+ }
+ }
+ try {
+ axo = new ActiveXObject("ShockwaveFlash.ShockwaveFlash");
+ } catch(e) {}
+ }
+ if (axo != null) {
+ PlayerVersion = new infosoftglobal.PlayerVersion(axo.GetVariable("$version").split(" ")[1].split(","));
+ }
+ }
+ return PlayerVersion;
+}
+infosoftglobal.PlayerVersion = function(arrVersion){
+ this.major = arrVersion[0] != null ? parseInt(arrVersion[0]) : 0;
+ this.minor = arrVersion[1] != null ? parseInt(arrVersion[1]) : 0;
+ this.rev = arrVersion[2] != null ? parseInt(arrVersion[2]) : 0;
+}
+// ------------ Fix for Out of Memory Bug in IE in FP9 ---------------//
+/* Fix for video streaming bug */
+infosoftglobal.FusionChartsUtil.cleanupSWFs = function() {
+ var objects = document.getElementsByTagName("OBJECT");
+ for (var i = objects.length - 1; i >= 0; i--) {
+ objects[i].style.display = 'none';
+ for (var x in objects[i]) {
+ if (typeof objects[i][x] == 'function') {
+ objects[i][x] = function(){};
+ }
+ }
+ }
+}
+// Fixes bug in fp9
+if (infosoftglobal.FusionCharts.doPrepUnload) {
+ if (!infosoftglobal.unloadSet) {
+ infosoftglobal.FusionChartsUtil.prepUnload = function() {
+ __flash_unloadHandler = function(){};
+ __flash_savedUnloadHandler = function(){};
+ window.attachEvent("onunload", infosoftglobal.FusionChartsUtil.cleanupSWFs);
+ }
+ window.attachEvent("onbeforeunload", infosoftglobal.FusionChartsUtil.prepUnload);
+ infosoftglobal.unloadSet = true;
+ }
+}
+/* Add document.getElementById if needed (mobile IE < 5) */
+if (!document.getElementById && document.all) { document.getElementById = function(id) { return document.all[id]; }}
+/* Add Array.push if needed (ie5) */
+if (Array.prototype.push == null) { Array.prototype.push = function(item) { this[this.length] = item; return this.length; }}
+
+/* Function to return Flash Object from ID */
+infosoftglobal.FusionChartsUtil.getChartObject = function(id)
+{
+ var chartRef=null;
+ if (navigator.appName.indexOf("Microsoft Internet")==-1) {
+ if (document.embeds && document.embeds[id])
+ chartRef = document.embeds[id];
+ else
+ chartRef = window.document[id];
+ }
+ else {
+ chartRef = window[id];
+ }
+ if (!chartRef)
+ chartRef = document.getElementById(id);
+
+ return chartRef;
+}
+/*
+ Function to update chart's data at client side (FOR FusionCharts vFREE and 2.x
+*/
+infosoftglobal.FusionChartsUtil.updateChartXML = function(chartId, strXML){
+ //Get reference to chart object
+ var chartObj = infosoftglobal.FusionChartsUtil.getChartObject(chartId);
+ //Set dataURL to null
+ chartObj.SetVariable("_root.dataURL","");
+ //Set the flag
+ chartObj.SetVariable("_root.isNewData","1");
+ //Set the actual data
+ chartObj.SetVariable("_root.newData",strXML);
+ //Go to the required frame
+ chartObj.TGotoLabel("/", "JavaScriptHandler");
+}
+
+
+/* Aliases for easy usage */
+var getChartFromId = infosoftglobal.FusionChartsUtil.getChartObject;
+var updateChartXML = infosoftglobal.FusionChartsUtil.updateChartXML;
+var FusionCharts = infosoftglobal.FusionCharts;
+\ No newline at end of file
(DIR) diff --git a/web/vendor/plugins/ezgraphix/spec/ezgraphix_spec.rb b/web/vendor/plugins/ezgraphix/spec/ezgraphix_spec.rb
@@ -0,0 +1,85 @@
+#using rspec 1.1.11
+require 'rubygems'
+require 'spec'
+require File.dirname(__FILE__) + '/../lib/ezgraphix'
+require File.dirname(__FILE__) + '/../lib/ezgraphix/ezgraphix_helper'
+
+include EzgraphixHelper
+include Ezgraphix
+
+describe Graphic do
+
+ before do
+ @g = Graphic.new
+ end
+
+ it do
+ @g.should be_an_instance_of(Graphic)
+ end
+
+ it do
+ @g.should have(4).defaults
+ end
+
+ it "should have right defaults" do
+ @g.defaults.values_at(:c_type, :w, :h, :div_name).should == ['col3d', 300, 300, 'ez_graphic']
+ end
+
+ before do
+ @g = Graphic.new(:c_type => 'bar2d', :w => 200, :caption => 'ezgraphix spec')
+ end
+
+ it "should merge defaults and options" do
+ @g.render_options.values_at(:c_type, :w, :h, :div_name, :caption).should == ['bar2d', 200, 300, 'ez_graphic', 'ezgraphix spec']
+ end
+
+ it "should have chart type, width, height and div_name" do
+ @g.c_type.should == 'bar2d'
+ @g.w.should == 200
+ @g.h.should == 300
+ @g.div_name.should == 'ez_graphic'
+ end
+
+ it "should have colors" do
+ Graphic::COLORS.should_not be_empty
+ end
+
+ it "should have valid colors" do
+ @g.rand_color.should be_instance_of(String)
+ @g.rand_color.length.should == 6
+ end
+
+ specify "colors should not repeat" do
+ Graphic::COLORS.inject([]){|used, color| used << @g.rand_color}.uniq.count.should == Graphic::COLORS.count
+ end
+
+ before do
+ @g.data = {:ruby => 1, :perl => 2, :smalltalk => 3}
+ end
+
+ it "should have valid data" do
+ @g.data.values_at(:ruby, :perl, :smalltalk).should == [1,2,3]
+ end
+
+ before do
+ @g.render_options(:y_name => 'score')
+ end
+
+ it "should update render options" do
+ @g.render_options.values_at(:c_type, :w, :h, :div_name, :caption, :y_name).should == ['bar2d', 200, 300, 'ez_graphic', 'ezgraphix spec', 'score']
+ end
+
+ it "should parse render options" do
+ parsed = parse_options(@g.render_options)
+ parsed.values_at('caption', 'yAxisName').should == ['ezgraphix spec', 'score']
+ end
+
+ it "should have original filename/location" do
+ f_type(@g.c_type).should == '/FusionCharts/FCF_Bar2D.swf'
+ end
+
+ it "should have style" do
+ get_style(@g).should == 'render_simple'
+ end
+
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/CHANGELOG.rdoc b/web/vendor/plugins/will_paginate/CHANGELOG.rdoc
@@ -0,0 +1,139 @@
+= 2.3.12, released 2009-12-01
+
+* make view helpers "HTML safe" for Rails 2.3.5 with rails_xss plugin
+
+= 2.3.11, released 2009-06-02
+
+* fix `enable_actionpack`
+
+= 2.3.10, released 2009-05-21
+
+* count_by_sql: don't use table alias with any adapters starting with "oracle"
+* Add back "AS count_table" alias to `paginate_by_sql` counter SQL
+
+= 2.3.9, released 2009-05-29
+
+* remove "AS count_table" alias from `paginate_by_sql` counter SQL
+* Rails 2.3.2 compat: monkeypatch Rails issue #2189 (count breaks has_many :through)
+* fix generation of page URLs that contain the "@" character
+* check for method existance in a ruby 1.8- and 1.9-compatible way
+* load will_paginate view helpers even if ActiveRecord is not loaded
+
+== 2.3.8, released 2009-03-09
+
+* Rails 2.3 compat: query parameter parsing with Rack
+
+== 2.3.7, released 2009-02-09
+
+* Removed all unnecessary &block variables since they cause serious memory damage and lots of subsequent gc runs.
+
+== 2.3.6, released 2008-10-26
+
+* Rails 2.2 fix: stop using `extract_attribute_names_from_match` inernal AR method, it no longer exists
+
+== 2.3.5, released 2008-10-07
+
+* update the backported named_scope implementation for Rails versions older than 2.1
+* break out of scope of paginated_each() yielded block when used on named scopes
+* fix paginate(:from)
+
+== 2.3.4, released 2008-09-16
+
+* Removed gem dependency to Active Support (causes trouble with vendored rails).
+* Rails 2.1: fix a failing test and a deprecation warning.
+* Cope with scoped :select when counting.
+
+== 2.3.3, released 2008-08-29
+
+* Ensure that paginate_by_sql doesn't change the original SQL query.
+* RDoc love (now live at http://gitrdoc.com/mislav/will_paginate/tree/master)
+* Rename :prev_label to :previous_label for consistency. old name still functions but is deprecated
+* ActiveRecord 2.1: Remove :include option from count_all query when it's possible.
+
+== 2.3.2, released 2008-05-16
+
+* Fixed LinkRenderer#stringified_merge by removing "return" from iterator block
+* Ensure that 'href' values in pagination links are escaped URLs
+
+== 2.3.1, released 2008-05-04
+
+* Fixed page numbers not showing with custom routes and implicit first page
+* Try to use Hanna for documentation (falls back to default RDoc template if not)
+
+== 2.3.0, released 2008-04-29
+
+* Changed LinkRenderer to receive collection, options and reference to view template NOT in
+ constructor, but with the #prepare method. This is a step towards supporting passing of
+ LinkRenderer (or subclass) instances that may be preconfigured in some way
+* LinkRenderer now has #page_link and #page_span methods for easier customization of output in
+ subclasses
+* Changed page_entries_info() method to adjust its output according to humanized class name of
+ collection items. Override this with :entry_name parameter (singular).
+
+ page_entries_info(@posts)
+ #-> "Displaying all 12 posts"
+ page_entries_info(@posts, :entry_name => 'item')
+ #-> "Displaying all 12 items"
+
+== 2.2.3, released 2008-04-26
+
+* will_paginate gem is no longer published on RubyForge, but on
+ gems.github.com:
+
+ gem sources -a http://gems.github.com/ (you only need to do this once)
+ gem install mislav-will_paginate
+
+* extract reusable pagination testing stuff into WillPaginate::View
+* rethink the page URL construction mechanizm to be more bulletproof when
+ combined with custom routing for page parameter
+* test that anchor parameter can be used in pagination links
+
+== 2.2.2, released 2008-04-21
+
+* Add support for page parameter in custom routes like "/foo/page/2"
+* Change output of "page_entries_info" on single-page collection and erraneous
+ output with empty collection as reported by Tim Chater
+
+== 2.2.1, released 2008-04-08
+
+* take less risky path when monkeypatching named_scope; fix that it no longer
+ requires ActiveRecord::VERSION
+* use strings in "respond_to?" calls to work around a bug in acts_as_ferret
+ stable (ugh)
+* add rake release task
+
+
+== 2.2.0, released 2008-04-07
+
+=== API changes
+* Rename WillPaginate::Collection#page_count to "total_pages" for consistency.
+ If you implemented this interface, change your implementation accordingly.
+* Remove old, deprecated style of calling Array#paginate as "paginate(page,
+ per_page)". If you want to specify :page, :per_page or :total_entries, use a
+ parameter hash.
+* Rename LinkRenderer#url_options to "url_for" and drastically optimize it
+
+=== View changes
+* Added "prev_page" and "next_page" CSS classes on previous/next page buttons
+* Add examples of pagination links styling in "examples/index.html"
+* Change gap in pagination links from "..." to
+ "<span class="gap">…</span>".
+* Add "paginated_section", a block helper that renders pagination both above and
+ below content in the block
+* Add rel="prev|next|start" to page links
+
+=== Other
+
+* Add ability to opt-in for Rails 2.1 feature "named_scope" by calling
+ WillPaginate.enable_named_scope (tested in Rails 1.2.6 and 2.0.2)
+* Support complex page parameters like "developers[page]"
+* Move Array#paginate definition to will_paginate/array.rb. You can now easily
+ use pagination on arrays outside of Rails:
+
+ gem 'will_paginate'
+ require 'will_paginate/array'
+
+* Add "paginated_each" method for iterating through every record by loading only
+ one page of records at the time
+* Rails 2: Rescue from WillPaginate::InvalidPage error with 404 Not Found by
+ default
(DIR) diff --git a/web/vendor/plugins/will_paginate/LICENSE b/web/vendor/plugins/will_paginate/LICENSE
@@ -0,0 +1,18 @@
+Copyright (c) 2007 PJ Hyett and Mislav Marohnić
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
(DIR) diff --git a/web/vendor/plugins/will_paginate/README.rdoc b/web/vendor/plugins/will_paginate/README.rdoc
@@ -0,0 +1,107 @@
+= WillPaginate
+
+Pagination is just limiting the number of records displayed. Why should you let
+it get in your way while developing, then? This plugin makes magic happen. Did
+you ever want to be able to do just this on a model:
+
+ Post.paginate :page => 1, :order => 'created_at DESC'
+
+... and then render the page links with a single view helper? Well, now you
+can.
+
+Some resources to get you started:
+
+* {Installation instructions}[http://github.com/mislav/will_paginate/wikis/installation]
+ on {the wiki}[http://github.com/mislav/will_paginate/wikis]
+* Your mind reels with questions? Join our
+ {Google group}[http://groups.google.com/group/will_paginate].
+* {How to report bugs}[http://github.com/mislav/will_paginate/wikis/report-bugs]
+
+
+== Example usage
+
+Use a paginate finder in the controller:
+
+ @posts = Post.paginate_by_board_id @board.id, :page => params[:page], :order => 'updated_at DESC'
+
+Yeah, +paginate+ works just like +find+ -- it just doesn't fetch all the
+records. Don't forget to tell it which page you want, or it will complain!
+Read more on WillPaginate::Finder::ClassMethods.
+
+Render the posts in your view like you would normally do. When you need to render
+pagination, just stick this in:
+
+ <%= will_paginate @posts %>
+
+You're done. (You can find the option list at WillPaginate::ViewHelpers.)
+
+How does it know how much items to fetch per page? It asks your model by calling
+its <tt>per_page</tt> class method. You can define it like this:
+
+ class Post < ActiveRecord::Base
+ cattr_reader :per_page
+ @@per_page = 50
+ end
+
+... or like this:
+
+ class Post < ActiveRecord::Base
+ def self.per_page
+ 50
+ end
+ end
+
+... or don't worry about it at all. WillPaginate defines it to be <b>30</b> by default.
+But you can always specify the count explicitly when calling +paginate+:
+
+ @posts = Post.paginate :page => params[:page], :per_page => 50
+
+The +paginate+ finder wraps the original finder and returns your resultset that now has
+some new properties. You can use the collection as you would with any ActiveRecord
+resultset. WillPaginate view helpers also need that object to be able to render pagination:
+
+ <ol>
+ <% for post in @posts -%>
+ <li>Render `post` in some nice way.</li>
+ <% end -%>
+ </ol>
+
+ <p>Now let's render us some pagination!</p>
+ <%= will_paginate @posts %>
+
+More detailed documentation:
+
+* WillPaginate::Finder::ClassMethods for pagination on your models;
+* WillPaginate::ViewHelpers for your views.
+
+
+== Authors and credits
+
+Authors:: Mislav Marohnić, PJ Hyett
+Original announcement:: http://errtheblog.com/post/929
+Original PHP source:: http://www.strangerstudios.com/sandbox/pagination/diggstyle.php
+
+All these people helped making will_paginate what it is now with their code
+contributions or just simply awesome ideas:
+
+Chris Wanstrath, Dr. Nic Williams, K. Adam Christensen, Mike Garey, Bence
+Golda, Matt Aimonetti, Charles Brian Quinn, Desi McAdam, James Coglan, Matijs
+van Zuijlen, Maria, Brendan Ribera, Todd Willey, Bryan Helmkamp, Jan Berkel,
+Lourens Naudé, Rick Olson, Russell Norris, Piotr Usewicz, Chris Eppstein,
+Denis Barushev, Ben Pickles.
+
+
+== Usable pagination in the UI
+
+There are some CSS styles to get you started in the "examples/" directory. They
+are {showcased online here}[http://mislav.uniqpath.com/will_paginate/].
+
+More reading about pagination as design pattern:
+
+* {Pagination 101}[http://kurafire.net/log/archive/2007/06/22/pagination-101]
+* {Pagination gallery}[http://www.smashingmagazine.com/2007/11/16/pagination-gallery-examples-and-good-practices/]
+* {Pagination on Yahoo Design Pattern Library}[http://developer.yahoo.com/ypatterns/parent.php?pattern=pagination]
+
+Want to discuss, request features, ask questions? Join the
+{Google group}[http://groups.google.com/group/will_paginate].
+
(DIR) diff --git a/web/vendor/plugins/will_paginate/Rakefile b/web/vendor/plugins/will_paginate/Rakefile
@@ -0,0 +1,53 @@
+require 'rubygems'
+begin
+ hanna_dir = '/Users/mislav/Projects/Hanna/lib'
+ $:.unshift hanna_dir if File.exists? hanna_dir
+ require 'hanna/rdoctask'
+rescue LoadError
+ require 'rake'
+ require 'rake/rdoctask'
+end
+load 'test/tasks.rake'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Generate RDoc documentation for the will_paginate plugin.'
+Rake::RDocTask.new(:rdoc) do |rdoc|
+ rdoc.rdoc_files.include('README.rdoc', 'LICENSE', 'CHANGELOG.rdoc').
+ include('lib/**/*.rb').
+ exclude('lib/will_paginate/named_scope*').
+ exclude('lib/will_paginate/array.rb').
+ exclude('lib/will_paginate/version.rb')
+
+ rdoc.main = "README.rdoc" # page to start on
+ rdoc.title = "will_paginate documentation"
+
+ rdoc.rdoc_dir = 'doc' # rdoc output folder
+ rdoc.options << '--inline-source' << '--charset=UTF-8'
+ rdoc.options << '--webcvs=http://github.com/mislav/will_paginate/tree/master/'
+end
+
+desc %{Update ".manifest" with the latest list of project filenames. Respect\
+.gitignore by excluding everything that git ignores. Update `files` and\
+`test_files` arrays in "*.gemspec" file if it's present.}
+task :manifest do
+ list = `git ls-files --full-name --exclude=*.gemspec --exclude=.*`.chomp.split("\n")
+
+ if spec_file = Dir['*.gemspec'].first
+ spec = File.read spec_file
+ spec.gsub! /^(\s* s.(test_)?files \s* = \s* )( \[ [^\]]* \] | %w\( [^)]* \) )/mx do
+ assignment = $1
+ bunch = $2 ? list.grep(/^test\//) : list
+ '%s%%w(%s)' % [assignment, bunch.join(' ')]
+ end
+
+ File.open(spec_file, 'w') { |f| f << spec }
+ end
+ File.open('.manifest', 'w') { |f| f << list.join("\n") }
+end
+
+task :examples do
+ %x(haml examples/index.haml examples/index.html)
+ %x(sass examples/pagination.sass examples/pagination.css)
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/examples/apple-circle.gif b/web/vendor/plugins/will_paginate/examples/apple-circle.gif
Binary files differ.
(DIR) diff --git a/web/vendor/plugins/will_paginate/examples/index.haml b/web/vendor/plugins/will_paginate/examples/index.haml
@@ -0,0 +1,69 @@
+!!!
+%html
+%head
+ %title Samples of pagination styling for will_paginate
+ %link{ :rel => 'stylesheet', :type => 'text/css', :href => 'pagination.css' }
+ %style{ :type => 'text/css' }
+ :sass
+ html
+ :margin 0
+ :padding 0
+ :background #999
+ :font normal 76% "Lucida Grande", Verdana, Helvetica, sans-serif
+ body
+ :margin 2em
+ :padding 2em
+ :border 2px solid gray
+ :background white
+ :color #222
+ h1
+ :font-size 2em
+ :font-weight normal
+ :margin 0 0 1em 0
+ h2
+ :font-size 1.4em
+ :margin 1em 0 .5em 0
+ pre
+ :font-size 13px
+ :font-family Monaco, "DejaVu Sans Mono", "Bitstream Vera Mono", "Courier New", monospace
+
+- pagination = '<span class="disabled prev_page">« Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">…</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next »</a>'
+- pagination_no_page_links = '<span class="disabled prev_page">« Previous</span> <a href="./?page=2" rel="next" class="next_page">Next »</a>'
+
+%body
+ %h1 Samples of pagination styling for will_paginate
+ %p
+ Find these styles in <b>"examples/pagination.css"</b> of <i>will_paginate</i> library.
+ There is a Sass version of it for all you sassy people.
+ %p
+ Read about good rules for pagination:
+ %a{ :href => 'http://kurafire.net/log/archive/2007/06/22/pagination-101' } Pagination 101
+ %p
+ %em Warning:
+ page links below don't lead anywhere (so don't click on them).
+
+ %h2 Unstyled pagination <span style="font-weight:normal">(<i>ewww!</i>)</span>
+ %div= pagination
+
+ %h2 Digg.com
+ .digg_pagination= pagination
+
+ %h2 Digg-style, no page links
+ .digg_pagination= pagination_no_page_links
+ %p Code that renders this:
+ %pre= '<code>%s</code>' % %[<%= will_paginate @posts, :page_links => false %>].gsub('<', '<').gsub('>', '>')
+
+ %h2 Digg-style, extra content
+ .digg_pagination
+ .page_info Displaying entries <b>1 - 6</b> of <b>180</b> in total
+ = pagination
+ %p Code that renders this:
+ %pre= '<code>%s</code>' % %[<div class="digg_pagination">\n <div clas="page_info">\n <%= page_entries_info @posts %>\n </div>\n <%= will_paginate @posts, :container => false %>\n</div>].gsub('<', '<').gsub('>', '>')
+
+ %h2 Apple.com store
+ .apple_pagination= pagination
+
+ %h2 Flickr.com
+ .flickr_pagination
+ = pagination
+ .page_info (118 photos)
(DIR) diff --git a/web/vendor/plugins/will_paginate/examples/index.html b/web/vendor/plugins/will_paginate/examples/index.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+</html>
+<head>
+ <title>Samples of pagination styling for will_paginate</title>
+ <link href='pagination.css' rel='stylesheet' type='text/css' />
+ <style type='text/css'>
+ html {
+ margin: 0;
+ padding: 0;
+ background: #999;
+ font: normal 76% "Lucida Grande", Verdana, Helvetica, sans-serif; }
+
+ body {
+ margin: 2em;
+ padding: 2em;
+ border: 2px solid gray;
+ background: white;
+ color: #222; }
+
+ h1 {
+ font-size: 2em;
+ font-weight: normal;
+ margin: 0 0 1em 0; }
+
+ h2 {
+ font-size: 1.4em;
+ margin: 1em 0 .5em 0; }
+
+ pre {
+ font-size: 13px;
+ font-family: Monaco, "DejaVu Sans Mono", "Bitstream Vera Mono", "Courier New", monospace; }
+ </style>
+</head>
+<body>
+ <h1>Samples of pagination styling for will_paginate</h1>
+ <p>
+ Find these styles in <b>"examples/pagination.css"</b> of <i>will_paginate</i> library.
+ There is a Sass version of it for all you sassy people.
+ </p>
+ <p>
+ Read about good rules for pagination:
+ <a href='http://kurafire.net/log/archive/2007/06/22/pagination-101'>Pagination 101</a>
+ </p>
+ <p>
+ <em>Warning:</em>
+ page links below don't lead anywhere (so don't click on them).
+ </p>
+ <h2>
+ Unstyled pagination <span style="font-weight:normal">(<i>ewww!</i>)</span>
+ </h2>
+ <div>
+ <span class="disabled prev_page">« Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">…</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next »</a>
+ </div>
+ <h2>Digg.com</h2>
+ <div class='digg_pagination'>
+ <span class="disabled prev_page">« Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">…</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next »</a>
+ </div>
+ <h2>Digg-style, no page links</h2>
+ <div class='digg_pagination'>
+ <span class="disabled prev_page">« Previous</span> <a href="./?page=2" rel="next" class="next_page">Next »</a>
+ </div>
+ <p>Code that renders this:</p>
+ <pre>
+ <code><%= will_paginate @posts, :page_links => false %></code>
+ </pre>
+ <h2>Digg-style, extra content</h2>
+ <div class='digg_pagination'>
+ <div class='page_info'>
+ Displaying entries <b>1 - 6</b> of <b>180</b> in total
+ </div>
+ <span class="disabled prev_page">« Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">…</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next »</a>
+ </div>
+ <p>Code that renders this:</p>
+ <pre>
+ <code><div class="digg_pagination">
+ <div clas="page_info">
+ <%= page_entries_info @posts %>
+ </div>
+ <%= will_paginate @posts, :container => false %>
+ </div></code>
+ </pre>
+ <h2>Apple.com store</h2>
+ <div class='apple_pagination'>
+ <span class="disabled prev_page">« Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">…</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next »</a>
+ </div>
+ <h2>Flickr.com</h2>
+ <div class='flickr_pagination'>
+ <span class="disabled prev_page">« Previous</span> <span class="current">1</span> <a href="./?page=2" rel="next">2</a> <a href="./?page=3">3</a> <a href="./?page=4">4</a> <a href="./?page=5">5</a> <a href="./?page=6">6</a> <a href="./?page=7">7</a> <a href="./?page=8">8</a> <a href="./?page=9">9</a> <span class="gap">…</span> <a href="./?page=29">29</a> <a href="./?page=30">30</a> <a href="./?page=2" rel="next" class="next_page">Next »</a>
+ <div class='page_info'>(118 photos)</div>
+ </div>
+</body>
(DIR) diff --git a/web/vendor/plugins/will_paginate/examples/pagination.css b/web/vendor/plugins/will_paginate/examples/pagination.css
@@ -0,0 +1,91 @@
+.digg_pagination {
+ background: white;
+ /* self-clearing method: */ }
+ .digg_pagination a, .digg_pagination span, .digg_pagination em {
+ padding: .2em .5em;
+ display: block;
+ float: left;
+ margin-right: 1px; }
+ .digg_pagination span.disabled {
+ color: #999;
+ border: 1px solid #DDD; }
+ .digg_pagination em {
+ font-weight: bold;
+ background: #2E6AB1;
+ color: white;
+ border: 1px solid #2E6AB1; }
+ .digg_pagination a {
+ text-decoration: none;
+ color: #105CB6;
+ border: 1px solid #9AAFE5; }
+ .digg_pagination a:hover, .digg_pagination a:focus {
+ color: #003;
+ border-color: #003; }
+ .digg_pagination .page_info {
+ background: #2E6AB1;
+ color: white;
+ padding: .4em .6em;
+ width: 22em;
+ margin-bottom: .3em;
+ text-align: center; }
+ .digg_pagination .page_info b {
+ color: #003;
+ background: #6aa6ed;
+ padding: .1em .25em; }
+ .digg_pagination:after {
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden; }
+ * html .digg_pagination {
+ height: 1%; }
+ *:first-child+html .digg_pagination {
+ overflow: hidden; }
+
+.apple_pagination {
+ background: #F1F1F1;
+ border: 1px solid #E5E5E5;
+ text-align: center;
+ padding: 1em; }
+ .apple_pagination a, .apple_pagination span, .digg_pagination em {
+ padding: .2em .3em; }
+ .apple_pagination span.disabled {
+ color: #AAA; }
+ .apple_pagination em {
+ font-weight: bold;
+ background: transparent url(apple-circle.gif) no-repeat 50% 50%; }
+ .apple_pagination a {
+ text-decoration: none;
+ color: black; }
+ .apple_pagination a:hover, .apple_pagination a:focus {
+ text-decoration: underline; }
+
+.flickr_pagination {
+ text-align: center;
+ padding: .3em; }
+ .flickr_pagination a, .flickr_pagination span, .digg_pagination em {
+ padding: .2em .5em; }
+ .flickr_pagination span.disabled {
+ color: #AAA; }
+ .flickr_pagination em {
+ font-weight: bold;
+ color: #FF0084; }
+ .flickr_pagination a {
+ border: 1px solid #DDDDDD;
+ color: #0063DC;
+ text-decoration: none; }
+ .flickr_pagination a:hover, .flickr_pagination a:focus {
+ border-color: #003366;
+ background: #0063DC;
+ color: white; }
+ .flickr_pagination .page_info {
+ color: #aaa;
+ padding-top: .8em; }
+ .flickr_pagination .prev_page, .flickr_pagination .next_page {
+ border-width: 2px; }
+ .flickr_pagination .prev_page {
+ margin-right: 1em; }
+ .flickr_pagination .next_page {
+ margin-left: 1em; }
+
(DIR) diff --git a/web/vendor/plugins/will_paginate/examples/pagination.sass b/web/vendor/plugins/will_paginate/examples/pagination.sass
@@ -0,0 +1,91 @@
+.digg_pagination
+ :background white
+ a, span, em
+ :padding .2em .5em
+ :display block
+ :float left
+ :margin-right 1px
+ span.disabled
+ :color #999
+ :border 1px solid #DDD
+ em
+ :font-weight bold
+ :background #2E6AB1
+ :color white
+ :border 1px solid #2E6AB1
+ a
+ :text-decoration none
+ :color #105CB6
+ :border 1px solid #9AAFE5
+ &:hover, &:focus
+ :color #003
+ :border-color #003
+ .page_info
+ :background #2E6AB1
+ :color white
+ :padding .4em .6em
+ :width 22em
+ :margin-bottom .3em
+ :text-align center
+ b
+ :color #003
+ :background = #2E6AB1 + 60
+ :padding .1em .25em
+
+ /* self-clearing method:
+ &:after
+ :content "."
+ :display block
+ :height 0
+ :clear both
+ :visibility hidden
+ * html &
+ :height 1%
+ *:first-child+html &
+ :overflow hidden
+
+.apple_pagination
+ :background #F1F1F1
+ :border 1px solid #E5E5E5
+ :text-align center
+ :padding 1em
+ a, span, em
+ :padding .2em .3em
+ span.disabled
+ :color #AAA
+ em
+ :font-weight bold
+ :background transparent url(apple-circle.gif) no-repeat 50% 50%
+ a
+ :text-decoration none
+ :color black
+ &:hover, &:focus
+ :text-decoration underline
+
+.flickr_pagination
+ :text-align center
+ :padding .3em
+ a, span, em
+ :padding .2em .5em
+ span.disabled
+ :color #AAA
+ em
+ :font-weight bold
+ :color #FF0084
+ a
+ :border 1px solid #DDDDDD
+ :color #0063DC
+ :text-decoration none
+ &:hover, &:focus
+ :border-color #003366
+ :background #0063DC
+ :color white
+ .page_info
+ :color #aaa
+ :padding-top .8em
+ .prev_page, .next_page
+ :border-width 2px
+ .prev_page
+ :margin-right 1em
+ .next_page
+ :margin-left 1em
(DIR) diff --git a/web/vendor/plugins/will_paginate/init.rb b/web/vendor/plugins/will_paginate/init.rb
@@ -0,0 +1 @@
+require 'will_paginate'
(DIR) diff --git a/web/vendor/plugins/will_paginate/lib/will_paginate.rb b/web/vendor/plugins/will_paginate/lib/will_paginate.rb
@@ -0,0 +1,90 @@
+require 'active_support'
+require 'will_paginate/core_ext'
+
+# = You *will* paginate!
+#
+# First read about WillPaginate::Finder::ClassMethods, then see
+# WillPaginate::ViewHelpers. The magical array you're handling in-between is
+# WillPaginate::Collection.
+#
+# Happy paginating!
+module WillPaginate
+ class << self
+ # shortcut for <tt>enable_actionpack</tt> and <tt>enable_activerecord</tt> combined
+ def enable
+ enable_actionpack
+ enable_activerecord
+ end
+
+ # hooks WillPaginate::ViewHelpers into ActionView::Base
+ def enable_actionpack
+ return if ActionView::Base.instance_methods.include_method? :will_paginate
+ require 'will_paginate/view_helpers'
+ ActionView::Base.send :include, ViewHelpers
+
+ if defined?(ActionController::Base) and ActionController::Base.respond_to? :rescue_responses
+ ActionController::Base.rescue_responses['WillPaginate::InvalidPage'] = :not_found
+ end
+ end
+
+ # hooks WillPaginate::Finder into ActiveRecord::Base and classes that deal
+ # with associations
+ def enable_activerecord
+ return if ActiveRecord::Base.respond_to? :paginate
+ require 'will_paginate/finder'
+ ActiveRecord::Base.send :include, Finder
+
+ # support pagination on associations
+ a = ActiveRecord::Associations
+ [ a::AssociationCollection ].tap { |classes|
+ # detect http://dev.rubyonrails.org/changeset/9230
+ unless a::HasManyThroughAssociation.superclass == a::HasManyAssociation
+ classes << a::HasManyThroughAssociation
+ end
+ }.each do |klass|
+ klass.send :include, Finder::ClassMethods
+ klass.class_eval { alias_method_chain :method_missing, :paginate }
+ end
+
+ # monkeypatch Rails ticket #2189: "count breaks has_many :through"
+ ActiveRecord::Base.class_eval do
+ protected
+ def self.construct_count_options_from_args(*args)
+ result = super
+ result[0] = '*' if result[0].is_a?(String) and result[0] =~ /\.\*$/
+ result
+ end
+ end
+ end
+
+ # Enable named_scope, a feature of Rails 2.1, even if you have older Rails
+ # (tested on Rails 2.0.2 and 1.2.6).
+ #
+ # You can pass +false+ for +patch+ parameter to skip monkeypatching
+ # *associations*. Use this if you feel that <tt>named_scope</tt> broke
+ # has_many, has_many :through or has_and_belongs_to_many associations in
+ # your app. By passing +false+, you can still use <tt>named_scope</tt> in
+ # your models, but not through associations.
+ def enable_named_scope(patch = true)
+ return if defined? ActiveRecord::NamedScope
+ require 'will_paginate/named_scope'
+ require 'will_paginate/named_scope_patch' if patch
+
+ ActiveRecord::Base.send :include, WillPaginate::NamedScope
+ end
+ end
+
+ module Deprecation # :nodoc:
+ extend ActiveSupport::Deprecation
+
+ def self.warn(message, callstack = caller)
+ message = 'WillPaginate: ' + message.strip.gsub(/\s+/, ' ')
+ ActiveSupport::Deprecation.warn(message, callstack)
+ end
+ end
+end
+
+if defined? Rails
+ WillPaginate.enable_activerecord if defined? ActiveRecord
+ WillPaginate.enable_actionpack if defined? ActionController
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/lib/will_paginate/array.rb b/web/vendor/plugins/will_paginate/lib/will_paginate/array.rb
@@ -0,0 +1,16 @@
+require 'will_paginate/collection'
+
+# http://www.desimcadam.com/archives/8
+Array.class_eval do
+ def paginate(options = {})
+ raise ArgumentError, "parameter hash expected (got #{options.inspect})" unless Hash === options
+
+ WillPaginate::Collection.create(
+ options[:page] || 1,
+ options[:per_page] || 30,
+ options[:total_entries] || self.length
+ ) { |pager|
+ pager.replace self[pager.offset, pager.per_page].to_a
+ }
+ end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/lib/will_paginate/collection.rb b/web/vendor/plugins/will_paginate/lib/will_paginate/collection.rb
@@ -0,0 +1,144 @@
+module WillPaginate
+ # = Invalid page number error
+ # This is an ArgumentError raised in case a page was requested that is either
+ # zero or negative number. You should decide how do deal with such errors in
+ # the controller.
+ #
+ # If you're using Rails 2, then this error will automatically get handled like
+ # 404 Not Found. The hook is in "will_paginate.rb":
+ #
+ # ActionController::Base.rescue_responses['WillPaginate::InvalidPage'] = :not_found
+ #
+ # If you don't like this, use your preffered method of rescuing exceptions in
+ # public from your controllers to handle this differently. The +rescue_from+
+ # method is a nice addition to Rails 2.
+ #
+ # This error is *not* raised when a page further than the last page is
+ # requested. Use <tt>WillPaginate::Collection#out_of_bounds?</tt> method to
+ # check for those cases and manually deal with them as you see fit.
+ class InvalidPage < ArgumentError
+ def initialize(page, page_num)
+ super "#{page.inspect} given as value, which translates to '#{page_num}' as page number"
+ end
+ end
+
+ # = The key to pagination
+ # Arrays returned from paginating finds are, in fact, instances of this little
+ # class. You may think of WillPaginate::Collection as an ordinary array with
+ # some extra properties. Those properties are used by view helpers to generate
+ # correct page links.
+ #
+ # WillPaginate::Collection also assists in rolling out your own pagination
+ # solutions: see +create+.
+ #
+ # If you are writing a library that provides a collection which you would like
+ # to conform to this API, you don't have to copy these methods over; simply
+ # make your plugin/gem dependant on this library and do:
+ #
+ # require 'will_paginate/collection'
+ # # WillPaginate::Collection is now available for use
+ class Collection < Array
+ attr_reader :current_page, :per_page, :total_entries, :total_pages
+
+ # Arguments to the constructor are the current page number, per-page limit
+ # and the total number of entries. The last argument is optional because it
+ # is best to do lazy counting; in other words, count *conditionally* after
+ # populating the collection using the +replace+ method.
+ def initialize(page, per_page, total = nil)
+ @current_page = page.to_i
+ raise InvalidPage.new(page, @current_page) if @current_page < 1
+ @per_page = per_page.to_i
+ raise ArgumentError, "`per_page` setting cannot be less than 1 (#{@per_page} given)" if @per_page < 1
+
+ self.total_entries = total if total
+ end
+
+ # Just like +new+, but yields the object after instantiation and returns it
+ # afterwards. This is very useful for manual pagination:
+ #
+ # @entries = WillPaginate::Collection.create(1, 10) do |pager|
+ # result = Post.find(:all, :limit => pager.per_page, :offset => pager.offset)
+ # # inject the result array into the paginated collection:
+ # pager.replace(result)
+ #
+ # unless pager.total_entries
+ # # the pager didn't manage to guess the total count, do it manually
+ # pager.total_entries = Post.count
+ # end
+ # end
+ #
+ # The possibilities with this are endless. For another example, here is how
+ # WillPaginate used to define pagination for Array instances:
+ #
+ # Array.class_eval do
+ # def paginate(page = 1, per_page = 15)
+ # WillPaginate::Collection.create(page, per_page, size) do |pager|
+ # pager.replace self[pager.offset, pager.per_page].to_a
+ # end
+ # end
+ # end
+ #
+ # The Array#paginate API has since then changed, but this still serves as a
+ # fine example of WillPaginate::Collection usage.
+ def self.create(page, per_page, total = nil)
+ pager = new(page, per_page, total)
+ yield pager
+ pager
+ end
+
+ # Helper method that is true when someone tries to fetch a page with a
+ # larger number than the last page. Can be used in combination with flashes
+ # and redirecting.
+ def out_of_bounds?
+ current_page > total_pages
+ end
+
+ # Current offset of the paginated collection. If we're on the first page,
+ # it is always 0. If we're on the 2nd page and there are 30 entries per page,
+ # the offset is 30. This property is useful if you want to render ordinals
+ # side by side with records in the view: simply start with offset + 1.
+ def offset
+ (current_page - 1) * per_page
+ end
+
+ # current_page - 1 or nil if there is no previous page
+ def previous_page
+ current_page > 1 ? (current_page - 1) : nil
+ end
+
+ # current_page + 1 or nil if there is no next page
+ def next_page
+ current_page < total_pages ? (current_page + 1) : nil
+ end
+
+ # sets the <tt>total_entries</tt> property and calculates <tt>total_pages</tt>
+ def total_entries=(number)
+ @total_entries = number.to_i
+ @total_pages = (@total_entries / per_page.to_f).ceil
+ end
+
+ # This is a magic wrapper for the original Array#replace method. It serves
+ # for populating the paginated collection after initialization.
+ #
+ # Why magic? Because it tries to guess the total number of entries judging
+ # by the size of given array. If it is shorter than +per_page+ limit, then we
+ # know we're on the last page. This trick is very useful for avoiding
+ # unnecessary hits to the database to do the counting after we fetched the
+ # data for the current page.
+ #
+ # However, after using +replace+ you should always test the value of
+ # +total_entries+ and set it to a proper value if it's +nil+. See the example
+ # in +create+.
+ def replace(array)
+ result = super
+
+ # The collection is shorter then page limit? Rejoice, because
+ # then we know that we are on the last page!
+ if total_entries.nil? and length < per_page and (current_page == 1 or length > 0)
+ self.total_entries = offset + length
+ end
+
+ result
+ end
+ end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb b/web/vendor/plugins/will_paginate/lib/will_paginate/core_ext.rb
@@ -0,0 +1,43 @@
+require 'set'
+require 'will_paginate/array'
+
+# helper to check for method existance in ruby 1.8- and 1.9-compatible way
+# because `methods`, `instance_methods` and others return strings in 1.8 and symbols in 1.9
+#
+# ['foo', 'bar'].include_method?(:foo) # => true
+class Array
+ def include_method?(name)
+ name = name.to_sym
+ !!(find { |item| item.to_sym == name })
+ end
+end
+
+unless Hash.instance_methods.include_method? :except
+ Hash.class_eval do
+ # Returns a new hash without the given keys.
+ def except(*keys)
+ rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
+ reject { |key,| rejected.include?(key) }
+ end
+
+ # Replaces the hash without only the given keys.
+ def except!(*keys)
+ replace(except(*keys))
+ end
+ end
+end
+
+unless Hash.instance_methods.include_method? :slice
+ Hash.class_eval do
+ # Returns a new hash with only the given keys.
+ def slice(*keys)
+ allowed = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
+ reject { |key,| !allowed.include?(key) }
+ end
+
+ # Replaces the hash with only the given keys.
+ def slice!(*keys)
+ replace(slice(*keys))
+ end
+ end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/lib/will_paginate/finder.rb b/web/vendor/plugins/will_paginate/lib/will_paginate/finder.rb
@@ -0,0 +1,264 @@
+require 'will_paginate/core_ext'
+
+module WillPaginate
+ # A mixin for ActiveRecord::Base. Provides +per_page+ class method
+ # and hooks things up to provide paginating finders.
+ #
+ # Find out more in WillPaginate::Finder::ClassMethods
+ #
+ module Finder
+ def self.included(base)
+ base.extend ClassMethods
+ class << base
+ alias_method_chain :method_missing, :paginate
+ # alias_method_chain :find_every, :paginate
+ define_method(:per_page) { 30 } unless respond_to?(:per_page)
+ end
+ end
+
+ # = Paginating finders for ActiveRecord models
+ #
+ # WillPaginate adds +paginate+, +per_page+ and other methods to
+ # ActiveRecord::Base class methods and associations. It also hooks into
+ # +method_missing+ to intercept pagination calls to dynamic finders such as
+ # +paginate_by_user_id+ and translate them to ordinary finders
+ # (+find_all_by_user_id+ in this case).
+ #
+ # In short, paginating finders are equivalent to ActiveRecord finders; the
+ # only difference is that we start with "paginate" instead of "find" and
+ # that <tt>:page</tt> is required parameter:
+ #
+ # @posts = Post.paginate :all, :page => params[:page], :order => 'created_at DESC'
+ #
+ # In paginating finders, "all" is implicit. There is no sense in paginating
+ # a single record, right? So, you can drop the <tt>:all</tt> argument:
+ #
+ # Post.paginate(...) => Post.find :all
+ # Post.paginate_all_by_something => Post.find_all_by_something
+ # Post.paginate_by_something => Post.find_all_by_something
+ #
+ # == The importance of the <tt>:order</tt> parameter
+ #
+ # In ActiveRecord finders, <tt>:order</tt> parameter specifies columns for
+ # the <tt>ORDER BY</tt> clause in SQL. It is important to have it, since
+ # pagination only makes sense with ordered sets. Without the <tt>ORDER
+ # BY</tt> clause, databases aren't required to do consistent ordering when
+ # performing <tt>SELECT</tt> queries; this is especially true for
+ # PostgreSQL.
+ #
+ # Therefore, make sure you are doing ordering on a column that makes the
+ # most sense in the current context. Make that obvious to the user, also.
+ # For perfomance reasons you will also want to add an index to that column.
+ module ClassMethods
+ # This is the main paginating finder.
+ #
+ # == Special parameters for paginating finders
+ # * <tt>:page</tt> -- REQUIRED, but defaults to 1 if false or nil
+ # * <tt>:per_page</tt> -- defaults to <tt>CurrentModel.per_page</tt> (which is 30 if not overridden)
+ # * <tt>:total_entries</tt> -- use only if you manually count total entries
+ # * <tt>:count</tt> -- additional options that are passed on to +count+
+ # * <tt>:finder</tt> -- name of the ActiveRecord finder used (default: "find")
+ #
+ # All other options (+conditions+, +order+, ...) are forwarded to +find+
+ # and +count+ calls.
+ def paginate(*args)
+ options = args.pop
+ page, per_page, total_entries = wp_parse_options(options)
+ finder = (options[:finder] || 'find').to_s
+
+ if finder == 'find'
+ # an array of IDs may have been given:
+ total_entries ||= (Array === args.first and args.first.size)
+ # :all is implicit
+ args.unshift(:all) if args.empty?
+ end
+
+ WillPaginate::Collection.create(page, per_page, total_entries) do |pager|
+ count_options = options.except :page, :per_page, :total_entries, :finder
+ find_options = count_options.except(:count).update(:offset => pager.offset, :limit => pager.per_page)
+
+ args << find_options
+ # @options_from_last_find = nil
+ pager.replace(send(finder, *args) { |*a| yield(*a) if block_given? })
+
+ # magic counting for user convenience:
+ pager.total_entries = wp_count(count_options, args, finder) unless pager.total_entries
+ end
+ end
+
+ # Iterates through all records by loading one page at a time. This is useful
+ # for migrations or any other use case where you don't want to load all the
+ # records in memory at once.
+ #
+ # It uses +paginate+ internally; therefore it accepts all of its options.
+ # You can specify a starting page with <tt>:page</tt> (default is 1). Default
+ # <tt>:order</tt> is <tt>"id"</tt>, override if necessary.
+ #
+ # See {Faking Cursors in ActiveRecord}[http://weblog.jamisbuck.org/2007/4/6/faking-cursors-in-activerecord]
+ # where Jamis Buck describes this and a more efficient way for MySQL.
+ def paginated_each(options = {})
+ options = { :order => 'id', :page => 1 }.merge options
+ options[:page] = options[:page].to_i
+ options[:total_entries] = 0 # skip the individual count queries
+ total = 0
+
+ begin
+ collection = paginate(options)
+ with_exclusive_scope(:find => {}) do
+ # using exclusive scope so that the block is yielded in scope-free context
+ total += collection.each { |item| yield item }.size
+ end
+ options[:page] += 1
+ end until collection.size < collection.per_page
+
+ total
+ end
+
+ # Wraps +find_by_sql+ by simply adding LIMIT and OFFSET to your SQL string
+ # based on the params otherwise used by paginating finds: +page+ and
+ # +per_page+.
+ #
+ # Example:
+ #
+ # @developers = Developer.paginate_by_sql ['select * from developers where salary > ?', 80000],
+ # :page => params[:page], :per_page => 3
+ #
+ # A query for counting rows will automatically be generated if you don't
+ # supply <tt>:total_entries</tt>. If you experience problems with this
+ # generated SQL, you might want to perform the count manually in your
+ # application.
+ #
+ def paginate_by_sql(sql, options)
+ WillPaginate::Collection.create(*wp_parse_options(options)) do |pager|
+ query = sanitize_sql(sql.dup)
+ original_query = query.dup
+ # add limit, offset
+ add_limit! query, :offset => pager.offset, :limit => pager.per_page
+ # perfom the find
+ pager.replace find_by_sql(query)
+
+ unless pager.total_entries
+ count_query = original_query.sub /\bORDER\s+BY\s+[\w`,\s]+$/mi, ''
+ count_query = "SELECT COUNT(*) FROM (#{count_query})"
+
+ unless self.connection.adapter_name =~ /^(oracle|oci$)/i
+ count_query << ' AS count_table'
+ end
+ # perform the count query
+ pager.total_entries = count_by_sql(count_query)
+ end
+ end
+ end
+
+ def respond_to?(method, include_priv = false) #:nodoc:
+ case method.to_sym
+ when :paginate, :paginate_by_sql
+ true
+ else
+ super || super(method.to_s.sub(/^paginate/, 'find'), include_priv)
+ end
+ end
+
+ protected
+
+ def method_missing_with_paginate(method, *args) #:nodoc:
+ # did somebody tried to paginate? if not, let them be
+ unless method.to_s.index('paginate') == 0
+ if block_given?
+ return method_missing_without_paginate(method, *args) { |*a| yield(*a) }
+ else
+ return method_missing_without_paginate(method, *args)
+ end
+ end
+
+ # paginate finders are really just find_* with limit and offset
+ finder = method.to_s.sub('paginate', 'find')
+ finder.sub!('find', 'find_all') if finder.index('find_by_') == 0
+
+ options = args.pop
+ raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
+ options = options.dup
+ options[:finder] = finder
+ args << options
+
+ paginate(*args) { |*a| yield(*a) if block_given? }
+ end
+
+ # Does the not-so-trivial job of finding out the total number of entries
+ # in the database. It relies on the ActiveRecord +count+ method.
+ def wp_count(options, args, finder)
+ excludees = [:count, :order, :limit, :offset, :readonly]
+ excludees << :from unless ActiveRecord::Calculations::CALCULATIONS_OPTIONS.include?(:from)
+
+ # we may be in a model or an association proxy
+ klass = (@owner and @reflection) ? @reflection.klass : self
+
+ # Use :select from scope if it isn't already present.
+ options[:select] = scope(:find, :select) unless options[:select]
+
+ if options[:select] and options[:select] =~ /^\s*DISTINCT\b/i
+ # Remove quoting and check for table_name.*-like statement.
+ if options[:select].gsub(/[`"]/, '') =~ /\w+\.\*/
+ options[:select] = "DISTINCT #{klass.table_name}.#{klass.primary_key}"
+ end
+ else
+ excludees << :select # only exclude the select param if it doesn't begin with DISTINCT
+ end
+
+ # count expects (almost) the same options as find
+ count_options = options.except *excludees
+
+ # merge the hash found in :count
+ # this allows you to specify :select, :order, or anything else just for the count query
+ count_options.update options[:count] if options[:count]
+
+ # forget about includes if they are irrelevant (Rails 2.1)
+ if count_options[:include] and
+ klass.private_methods.include_method?(:references_eager_loaded_tables?) and
+ !klass.send(:references_eager_loaded_tables?, count_options)
+ count_options.delete :include
+ end
+
+ # we may have to scope ...
+ counter = Proc.new { count(count_options) }
+
+ count = if finder.index('find_') == 0 and klass.respond_to?(scoper = finder.sub('find', 'with'))
+ # scope_out adds a 'with_finder' method which acts like with_scope, if it's present
+ # then execute the count with the scoping provided by the with_finder
+ send(scoper, &counter)
+ elsif finder =~ /^find_(all_by|by)_([_a-zA-Z]\w*)$/
+ # extract conditions from calls like "paginate_by_foo_and_bar"
+ attribute_names = $2.split('_and_')
+ conditions = construct_attributes_from_arguments(attribute_names, args)
+ with_scope(:find => { :conditions => conditions }, &counter)
+ else
+ counter.call
+ end
+
+ (!count.is_a?(Integer) && count.respond_to?(:length)) ? count.length : count
+ end
+
+ def wp_parse_options(options) #:nodoc:
+ raise ArgumentError, 'parameter hash expected' unless options.respond_to? :symbolize_keys
+ options = options.symbolize_keys
+ raise ArgumentError, ':page parameter required' unless options.key? :page
+
+ if options[:count] and options[:total_entries]
+ raise ArgumentError, ':count and :total_entries are mutually exclusive'
+ end
+
+ page = options[:page] || 1
+ per_page = options[:per_page] || self.per_page
+ total = options[:total_entries]
+ [page, per_page, total]
+ end
+
+ private
+
+ # def find_every_with_paginate(options)
+ # @options_from_last_find = options
+ # find_every_without_paginate(options)
+ # end
+ end
+ end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/lib/will_paginate/named_scope.rb b/web/vendor/plugins/will_paginate/lib/will_paginate/named_scope.rb
@@ -0,0 +1,170 @@
+module WillPaginate
+ # This is a feature backported from Rails 2.1 because of its usefullness not only with will_paginate,
+ # but in other aspects when managing complex conditions that you want to be reusable.
+ module NamedScope
+ # All subclasses of ActiveRecord::Base have two named_scopes:
+ # * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
+ # * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
+ #
+ # These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
+ # intermediate values (scopes) around as first-class objects is convenient.
+ def self.included(base)
+ base.class_eval do
+ extend ClassMethods
+ named_scope :scoped, lambda { |scope| scope }
+ end
+ end
+
+ module ClassMethods
+ def scopes
+ read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
+ end
+
+ # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
+ # such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
+ #
+ # class Shirt < ActiveRecord::Base
+ # named_scope :red, :conditions => {:color => 'red'}
+ # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
+ # end
+ #
+ # The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
+ # in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
+ #
+ # Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
+ # constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
+ # <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
+ # as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
+ # <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
+ #
+ # These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
+ # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
+ # for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
+ #
+ # All scopes are available as class methods on the ActiveRecord::Base descendent upon which the scopes were defined. But they are also available to
+ # <tt>has_many</tt> associations. If,
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :shirts
+ # end
+ #
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
+ # only shirts.
+ #
+ # Named scopes can also be procedural.
+ #
+ # class Shirt < ActiveRecord::Base
+ # named_scope :colored, lambda { |color|
+ # { :conditions => { :color => color } }
+ # }
+ # end
+ #
+ # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
+ #
+ # Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
+ #
+ # class Shirt < ActiveRecord::Base
+ # named_scope :red, :conditions => {:color => 'red'} do
+ # def dom_id
+ # 'red_shirts'
+ # end
+ # end
+ # end
+ #
+ #
+ # For testing complex named scopes, you can examine the scoping options using the
+ # <tt>proxy_options</tt> method on the proxy itself.
+ #
+ # class Shirt < ActiveRecord::Base
+ # named_scope :colored, lambda { |color|
+ # { :conditions => { :color => color } }
+ # }
+ # end
+ #
+ # expected_options = { :conditions => { :colored => 'red' } }
+ # assert_equal expected_options, Shirt.colored('red').proxy_options
+ def named_scope(name, options = {})
+ name = name.to_sym
+ scopes[name] = lambda do |parent_scope, *args|
+ Scope.new(parent_scope, case options
+ when Hash
+ options
+ when Proc
+ options.call(*args)
+ end) { |*a| yield(*a) if block_given? }
+ end
+ (class << self; self end).instance_eval do
+ define_method name do |*args|
+ scopes[name].call(self, *args)
+ end
+ end
+ end
+ end
+
+ class Scope
+ attr_reader :proxy_scope, :proxy_options
+
+ [].methods.each do |m|
+ unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/
+ delegate m, :to => :proxy_found
+ end
+ end
+
+ delegate :scopes, :with_scope, :to => :proxy_scope
+
+ def initialize(proxy_scope, options)
+ [options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
+ extend Module.new { |*args| yield(*args) } if block_given?
+ @proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
+ end
+
+ def reload
+ load_found; self
+ end
+
+ def first(*args)
+ if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
+ proxy_found.first(*args)
+ else
+ find(:first, *args)
+ end
+ end
+
+ def last(*args)
+ if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
+ proxy_found.last(*args)
+ else
+ find(:last, *args)
+ end
+ end
+
+ def empty?
+ @found ? @found.empty? : count.zero?
+ end
+
+ def respond_to?(method, include_private = false)
+ super || @proxy_scope.respond_to?(method, include_private)
+ end
+
+ protected
+ def proxy_found
+ @found || load_found
+ end
+
+ private
+ def method_missing(method, *args)
+ if scopes.include?(method)
+ scopes[method].call(self, *args)
+ else
+ with_scope :find => proxy_options do
+ proxy_scope.send(method, *args) { |*a| yield(*a) if block_given? }
+ end
+ end
+ end
+
+ def load_found
+ @found = find(:all)
+ end
+ end
+ end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/lib/will_paginate/named_scope_patch.rb b/web/vendor/plugins/will_paginate/lib/will_paginate/named_scope_patch.rb
@@ -0,0 +1,37 @@
+ActiveRecord::Associations::AssociationProxy.class_eval do
+ protected
+ def with_scope(*args)
+ @reflection.klass.send(:with_scope, *args) { |*a| yield(*a) if block_given? }
+ end
+end
+
+[ ActiveRecord::Associations::AssociationCollection,
+ ActiveRecord::Associations::HasManyThroughAssociation ].each do |klass|
+ klass.class_eval do
+ protected
+ alias :method_missing_without_scopes :method_missing_without_paginate
+ def method_missing_without_paginate(method, *args)
+ if @reflection.klass.scopes.include?(method)
+ @reflection.klass.scopes[method].call(self, *args) { |*a| yield(*a) if block_given? }
+ else
+ method_missing_without_scopes(method, *args) { |*a| yield(*a) if block_given? }
+ end
+ end
+ end
+end
+
+# Rails 1.2.6
+ActiveRecord::Associations::HasAndBelongsToManyAssociation.class_eval do
+ protected
+ def method_missing(method, *args)
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
+ super
+ elsif @reflection.klass.scopes.include?(method)
+ @reflection.klass.scopes[method].call(self, *args)
+ else
+ @reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do
+ @reflection.klass.send(method, *args) { |*a| yield(*a) if block_given? }
+ end
+ end
+ end
+end if ActiveRecord::Base.respond_to? :find_first
(DIR) diff --git a/web/vendor/plugins/will_paginate/lib/will_paginate/version.rb b/web/vendor/plugins/will_paginate/lib/will_paginate/version.rb
@@ -0,0 +1,9 @@
+module WillPaginate
+ module VERSION
+ MAJOR = 2
+ MINOR = 3
+ TINY = 15
+
+ STRING = [MAJOR, MINOR, TINY].join('.')
+ end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/lib/will_paginate/view_helpers.rb b/web/vendor/plugins/will_paginate/lib/will_paginate/view_helpers.rb
@@ -0,0 +1,410 @@
+require 'will_paginate/core_ext'
+
+module WillPaginate
+ # = Will Paginate view helpers
+ #
+ # The main view helper, #will_paginate, renders
+ # pagination links for the given collection. The helper itself is lightweight
+ # and serves only as a wrapper around LinkRenderer instantiation; the
+ # renderer then does all the hard work of generating the HTML.
+ #
+ # == Global options for helpers
+ #
+ # Options for pagination helpers are optional and get their default values from the
+ # <tt>WillPaginate::ViewHelpers.pagination_options</tt> hash. You can write to this hash to
+ # override default options on the global level:
+ #
+ # WillPaginate::ViewHelpers.pagination_options[:previous_label] = 'Previous page'
+ #
+ # By putting this into "config/initializers/will_paginate.rb" (or simply environment.rb in
+ # older versions of Rails) you can easily translate link texts to previous
+ # and next pages, as well as override some other defaults to your liking.
+ module ViewHelpers
+ # default options that can be overridden on the global level
+ @@pagination_options = {
+ :class => 'pagination',
+ :previous_label => '« Previous',
+ :next_label => 'Next »',
+ :inner_window => 4, # links around the current page
+ :outer_window => 1, # links around beginning and end
+ :separator => ' ', # single space is friendly to spiders and non-graphic browsers
+ :param_name => :page,
+ :params => nil,
+ :renderer => 'WillPaginate::LinkRenderer',
+ :page_links => true,
+ :container => true
+ }
+ mattr_reader :pagination_options
+
+ # Renders Digg/Flickr-style pagination for a WillPaginate::Collection
+ # object. Nil is returned if there is only one page in total; no point in
+ # rendering the pagination in that case...
+ #
+ # ==== Options
+ # Display options:
+ # * <tt>:previous_label</tt> -- default: "« Previous" (this parameter is called <tt>:prev_label</tt> in versions <b>2.3.2</b> and older!)
+ # * <tt>:next_label</tt> -- default: "Next »"
+ # * <tt>:page_links</tt> -- when false, only previous/next links are rendered (default: true)
+ # * <tt>:inner_window</tt> -- how many links are shown around the current page (default: 4)
+ # * <tt>:outer_window</tt> -- how many links are around the first and the last page (default: 1)
+ # * <tt>:separator</tt> -- string separator for page HTML elements (default: single space)
+ #
+ # HTML options:
+ # * <tt>:class</tt> -- CSS class name for the generated DIV (default: "pagination")
+ # * <tt>:container</tt> -- toggles rendering of the DIV container for pagination links, set to
+ # false only when you are rendering your own pagination markup (default: true)
+ # * <tt>:id</tt> -- HTML ID for the container (default: nil). Pass +true+ to have the ID
+ # automatically generated from the class name of objects in collection: for example, paginating
+ # ArticleComment models would yield an ID of "article_comments_pagination".
+ #
+ # Advanced options:
+ # * <tt>:param_name</tt> -- parameter name for page number in URLs (default: <tt>:page</tt>)
+ # * <tt>:params</tt> -- additional parameters when generating pagination links
+ # (eg. <tt>:controller => "foo", :action => nil</tt>)
+ # * <tt>:renderer</tt> -- class name, class or instance of a link renderer (default:
+ # <tt>WillPaginate::LinkRenderer</tt>)
+ #
+ # All options not recognized by will_paginate will become HTML attributes on the container
+ # element for pagination links (the DIV). For example:
+ #
+ # <%= will_paginate @posts, :style => 'font-size: small' %>
+ #
+ # ... will result in:
+ #
+ # <div class="pagination" style="font-size: small"> ... </div>
+ #
+ # ==== Using the helper without arguments
+ # If the helper is called without passing in the collection object, it will
+ # try to read from the instance variable inferred by the controller name.
+ # For example, calling +will_paginate+ while the current controller is
+ # PostsController will result in trying to read from the <tt>@posts</tt>
+ # variable. Example:
+ #
+ # <%= will_paginate :id => true %>
+ #
+ # ... will result in <tt>@post</tt> collection getting paginated:
+ #
+ # <div class="pagination" id="posts_pagination"> ... </div>
+ #
+ def will_paginate(collection = nil, options = {})
+ options, collection = collection, nil if collection.is_a? Hash
+ unless collection or !controller
+ collection_name = "@#{controller.controller_name}"
+ collection = instance_variable_get(collection_name)
+ raise ArgumentError, "The #{collection_name} variable appears to be empty. Did you " +
+ "forget to pass the collection object for will_paginate?" unless collection
+ end
+ # early exit if there is nothing to render
+ return nil unless WillPaginate::ViewHelpers.total_pages_for_collection(collection) > 1
+
+ options = options.symbolize_keys.reverse_merge WillPaginate::ViewHelpers.pagination_options
+ if options[:prev_label]
+ WillPaginate::Deprecation::warn(":prev_label view parameter is now :previous_label; the old name has been deprecated", caller)
+ options[:previous_label] = options.delete(:prev_label)
+ end
+
+ # get the renderer instance
+ renderer = case options[:renderer]
+ when String
+ options[:renderer].to_s.constantize.new
+ when Class
+ options[:renderer].new
+ else
+ options[:renderer]
+ end
+ # render HTML for pagination
+ renderer.prepare collection, options, self
+ renderer.to_html
+ end
+
+ # Wrapper for rendering pagination links at both top and bottom of a block
+ # of content.
+ #
+ # <% paginated_section @posts do %>
+ # <ol id="posts">
+ # <% for post in @posts %>
+ # <li> ... </li>
+ # <% end %>
+ # </ol>
+ # <% end %>
+ #
+ # will result in:
+ #
+ # <div class="pagination"> ... </div>
+ # <ol id="posts">
+ # ...
+ # </ol>
+ # <div class="pagination"> ... </div>
+ #
+ # Arguments are passed to a <tt>will_paginate</tt> call, so the same options
+ # apply. Don't use the <tt>:id</tt> option; otherwise you'll finish with two
+ # blocks of pagination links sharing the same ID (which is invalid HTML).
+ def paginated_section(*args, &block)
+ pagination = will_paginate(*args).to_s
+
+ unless ActionView::Base.respond_to? :erb_variable
+ concat pagination
+ yield
+ concat pagination
+ else
+ content = pagination + capture(&block) + pagination
+ concat(content, block.binding)
+ end
+ end
+
+ # Renders a helpful message with numbers of displayed vs. total entries.
+ # You can use this as a blueprint for your own, similar helpers.
+ #
+ # <%= page_entries_info @posts %>
+ # #-> Displaying posts 6 - 10 of 26 in total
+ #
+ # By default, the message will use the humanized class name of objects
+ # in collection: for instance, "project types" for ProjectType models.
+ # Override this with the <tt>:entry_name</tt> parameter:
+ #
+ # <%= page_entries_info @posts, :entry_name => 'item' %>
+ # #-> Displaying items 6 - 10 of 26 in total
+ def page_entries_info(collection, options = {})
+ entry_name = options[:entry_name] ||
+ (collection.empty?? 'entry' : collection.first.class.name.underscore.sub('_', ' '))
+
+ if collection.total_pages < 2
+ case collection.size
+ when 0; "No #{entry_name.pluralize} found"
+ when 1; "Displaying <b>1</b> #{entry_name}"
+ else; "Displaying <b>all #{collection.size}</b> #{entry_name.pluralize}"
+ end
+ else
+ %{Displaying #{entry_name.pluralize} <b>%d - %d</b> of <b>%d</b> in total} % [
+ collection.offset + 1,
+ collection.offset + collection.length,
+ collection.total_entries
+ ]
+ end
+ end
+
+ if respond_to? :safe_helper
+ safe_helper :will_paginate, :paginated_section, :page_entries_info
+ end
+
+ def self.total_pages_for_collection(collection) #:nodoc:
+ if collection.respond_to?('page_count') and !collection.respond_to?('total_pages')
+ WillPaginate::Deprecation.warn %{
+ You are using a paginated collection of class #{collection.class.name}
+ which conforms to the old API of WillPaginate::Collection by using
+ `page_count`, while the current method name is `total_pages`. Please
+ upgrade yours or 3rd-party code that provides the paginated collection}, caller
+ class << collection
+ def total_pages; page_count; end
+ end
+ end
+ collection.total_pages
+ end
+ end
+
+ # This class does the heavy lifting of actually building the pagination
+ # links. It is used by the <tt>will_paginate</tt> helper internally.
+ class LinkRenderer
+
+ # The gap in page links is represented by:
+ #
+ # <span class="gap">…</span>
+ attr_accessor :gap_marker
+
+ def initialize
+ @gap_marker = '<span class="gap">…</span>'
+ end
+
+ # * +collection+ is a WillPaginate::Collection instance or any other object
+ # that conforms to that API
+ # * +options+ are forwarded from +will_paginate+ view helper
+ # * +template+ is the reference to the template being rendered
+ def prepare(collection, options, template)
+ @collection = collection
+ @options = options
+ @template = template
+
+ # reset values in case we're re-using this instance
+ @total_pages = @param_name = @url_string = nil
+ end
+
+ # Process it! This method returns the complete HTML string which contains
+ # pagination links. Feel free to subclass LinkRenderer and change this
+ # method as you see fit.
+ def to_html
+ links = @options[:page_links] ? windowed_links : []
+ # previous/next buttons
+ links.unshift page_link_or_span(@collection.previous_page, 'disabled prev_page', @options[:previous_label])
+ links.push page_link_or_span(@collection.next_page, 'disabled next_page', @options[:next_label])
+
+ html = links.join(@options[:separator])
+ html = html.html_safe if html.respond_to? :html_safe
+ @options[:container] ? @template.content_tag(:div, html, html_attributes) : html
+ end
+
+ # Returns the subset of +options+ this instance was initialized with that
+ # represent HTML attributes for the container element of pagination links.
+ def html_attributes
+ return @html_attributes if @html_attributes
+ @html_attributes = @options.except *(WillPaginate::ViewHelpers.pagination_options.keys - [:class])
+ # pagination of Post models will have the ID of "posts_pagination"
+ if @options[:container] and @options[:id] === true
+ @html_attributes[:id] = @collection.first.class.name.underscore.pluralize + '_pagination'
+ end
+ @html_attributes
+ end
+
+ protected
+
+ # Collects link items for visible page numbers.
+ def windowed_links
+ prev = nil
+
+ visible_page_numbers.inject [] do |links, n|
+ # detect gaps:
+ links << gap_marker if prev and n > prev + 1
+ links << page_link_or_span(n, 'current')
+ prev = n
+ links
+ end
+ end
+
+ # Calculates visible page numbers using the <tt>:inner_window</tt> and
+ # <tt>:outer_window</tt> options.
+ def visible_page_numbers
+ inner_window, outer_window = @options[:inner_window].to_i, @options[:outer_window].to_i
+ window_from = current_page - inner_window
+ window_to = current_page + inner_window
+
+ # adjust lower or upper limit if other is out of bounds
+ if window_to > total_pages
+ window_from -= window_to - total_pages
+ window_to = total_pages
+ end
+ if window_from < 1
+ window_to += 1 - window_from
+ window_from = 1
+ window_to = total_pages if window_to > total_pages
+ end
+
+ visible = (1..total_pages).to_a
+ left_gap = (2 + outer_window)...window_from
+ right_gap = (window_to + 1)...(total_pages - outer_window)
+ visible -= left_gap.to_a if left_gap.last - left_gap.first > 1
+ visible -= right_gap.to_a if right_gap.last - right_gap.first > 1
+
+ visible
+ end
+
+ def page_link_or_span(page, span_class, text = nil)
+ text ||= page.to_s
+ text = text.html_safe if text.respond_to? :html_safe
+
+ if page and page != current_page
+ classnames = span_class && span_class.index(' ') && span_class.split(' ', 2).last
+ page_link page, text, :rel => rel_value(page), :class => classnames
+ else
+ page_span page, text, :class => span_class
+ end
+ end
+
+ def page_link(page, text, attributes = {})
+ @template.link_to text, url_for(page), attributes
+ end
+
+ def page_span(page, text, attributes = {})
+ @template.content_tag :span, text, attributes
+ end
+
+ # Returns URL params for +page_link_or_span+, taking the current GET params
+ # and <tt>:params</tt> option into account.
+ def url_for(page)
+ page_one = page == 1
+ unless @url_string and !page_one
+ @url_params = {}
+ # page links should preserve GET parameters
+ stringified_merge @url_params, @template.params if @template.request.get?
+ stringified_merge @url_params, @options[:params] if @options[:params]
+
+ if complex = param_name.index(/[^\w-]/)
+ page_param = parse_query_parameters("#{param_name}=#{page}")
+
+ stringified_merge @url_params, page_param
+ else
+ @url_params[param_name] = page_one ? 1 : 2
+ end
+
+ url = @template.url_for(@url_params)
+ return url if page_one
+
+ if complex
+ @url_string = url.sub(%r!((?:\?|&)#{CGI.escape param_name}=)#{page}!, "\\1\0")
+ return url
+ else
+ @url_string = url
+ @url_params[param_name] = 3
+ @template.url_for(@url_params).split(//).each_with_index do |char, i|
+ if char == '3' and url[i, 1] == '2'
+ @url_string[i] = "\0"
+ break
+ end
+ end
+ end
+ end
+ # finally!
+ @url_string.sub "\0", page.to_s
+ end
+
+ private
+
+ def rel_value(page)
+ case page
+ when @collection.previous_page; 'prev' + (page == 1 ? ' start' : '')
+ when @collection.next_page; 'next'
+ when 1; 'start'
+ end
+ end
+
+ def current_page
+ @collection.current_page
+ end
+
+ def total_pages
+ @total_pages ||= WillPaginate::ViewHelpers.total_pages_for_collection(@collection)
+ end
+
+ def param_name
+ @param_name ||= @options[:param_name].to_s
+ end
+
+ # Recursively merge into target hash by using stringified keys from the other one
+ def stringified_merge(target, other)
+ other.each do |key, value|
+ key = key.to_s # this line is what it's all about!
+ existing = target[key]
+
+ if value.is_a?(Hash) and (existing.is_a?(Hash) or existing.nil?)
+ stringified_merge(existing || (target[key] = {}), value)
+ else
+ target[key] = value
+ end
+ end
+ end
+
+ def parse_query_parameters(params)
+ if defined? Rack::Utils
+ # For Rails > 2.3
+ Rack::Utils.parse_nested_query(params)
+ elsif defined?(ActionController::AbstractRequest)
+ ActionController::AbstractRequest.parse_query_parameters(params)
+ elsif defined?(ActionController::UrlEncodedPairParser)
+ # For Rails > 2.2
+ ActionController::UrlEncodedPairParser.parse_query_parameters(params)
+ elsif defined?(CGIMethods)
+ CGIMethods.parse_query_parameters(params)
+ else
+ raise "unsupported ActionPack version"
+ end
+ end
+ end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/boot.rb b/web/vendor/plugins/will_paginate/test/boot.rb
@@ -0,0 +1,21 @@
+plugin_root = File.join(File.dirname(__FILE__), '..')
+version = ENV['RAILS_VERSION']
+version = nil if version and version == ""
+
+# first look for a symlink to a copy of the framework
+if !version and framework_root = ["#{plugin_root}/rails", "#{plugin_root}/../../rails"].find { |p| File.directory? p }
+ puts "found framework root: #{framework_root}"
+ # this allows for a plugin to be tested outside of an app and without Rails gems
+ $:.unshift "#{framework_root}/activesupport/lib", "#{framework_root}/activerecord/lib", "#{framework_root}/actionpack/lib"
+else
+ # simply use installed gems if available
+ puts "using Rails#{version ? ' ' + version : nil} gems"
+ require 'rubygems'
+
+ if version
+ gem 'rails', version
+ else
+ gem 'actionpack', '< 3.0.0.a'
+ gem 'activerecord', '< 3.0.0.a'
+ end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/collection_test.rb b/web/vendor/plugins/will_paginate/test/collection_test.rb
@@ -0,0 +1,143 @@
+require 'helper'
+require 'will_paginate/array'
+
+class ArrayPaginationTest < Test::Unit::TestCase
+
+ def setup ; end
+
+ def test_simple
+ collection = ('a'..'e').to_a
+
+ [{ :page => 1, :per_page => 3, :expected => %w( a b c ) },
+ { :page => 2, :per_page => 3, :expected => %w( d e ) },
+ { :page => 1, :per_page => 5, :expected => %w( a b c d e ) },
+ { :page => 3, :per_page => 5, :expected => [] },
+ ].
+ each do |conditions|
+ expected = conditions.delete :expected
+ assert_equal expected, collection.paginate(conditions)
+ end
+ end
+
+ def test_defaults
+ result = (1..50).to_a.paginate
+ assert_equal 1, result.current_page
+ assert_equal 30, result.size
+ end
+
+ def test_deprecated_api
+ assert_raise(ArgumentError) { [].paginate(2) }
+ assert_raise(ArgumentError) { [].paginate(2, 10) }
+ end
+
+ def test_total_entries_has_precedence
+ result = %w(a b c).paginate :total_entries => 5
+ assert_equal 5, result.total_entries
+ end
+
+ def test_argument_error_with_params_and_another_argument
+ assert_raise ArgumentError do
+ [].paginate({}, 5)
+ end
+ end
+
+ def test_paginated_collection
+ entries = %w(a b c)
+ collection = create(2, 3, 10) do |pager|
+ assert_equal entries, pager.replace(entries)
+ end
+
+ assert_equal entries, collection
+ assert_respond_to_all collection, %w(total_pages each offset size current_page per_page total_entries)
+ assert_kind_of Array, collection
+ assert_instance_of Array, collection.entries
+ assert_equal 3, collection.offset
+ assert_equal 4, collection.total_pages
+ assert !collection.out_of_bounds?
+ end
+
+ def test_previous_next_pages
+ collection = create(1, 1, 3)
+ assert_nil collection.previous_page
+ assert_equal 2, collection.next_page
+
+ collection = create(2, 1, 3)
+ assert_equal 1, collection.previous_page
+ assert_equal 3, collection.next_page
+
+ collection = create(3, 1, 3)
+ assert_equal 2, collection.previous_page
+ assert_nil collection.next_page
+ end
+
+ def test_out_of_bounds
+ entries = create(2, 3, 2){}
+ assert entries.out_of_bounds?
+
+ entries = create(1, 3, 2){}
+ assert !entries.out_of_bounds?
+ end
+
+ def test_guessing_total_count
+ entries = create do |pager|
+ # collection is shorter than limit
+ pager.replace array
+ end
+ assert_equal 8, entries.total_entries
+
+ entries = create(2, 5, 10) do |pager|
+ # collection is shorter than limit, but we have an explicit count
+ pager.replace array
+ end
+ assert_equal 10, entries.total_entries
+
+ entries = create do |pager|
+ # collection is the same as limit; we can't guess
+ pager.replace array(5)
+ end
+ assert_equal nil, entries.total_entries
+
+ entries = create do |pager|
+ # collection is empty; we can't guess
+ pager.replace array(0)
+ end
+ assert_equal nil, entries.total_entries
+
+ entries = create(1) do |pager|
+ # collection is empty and we're on page 1,
+ # so the whole thing must be empty, too
+ pager.replace array(0)
+ end
+ assert_equal 0, entries.total_entries
+ end
+
+ def test_invalid_page
+ bad_inputs = [0, -1, nil, '', 'Schnitzel']
+
+ bad_inputs.each do |bad|
+ assert_raise(WillPaginate::InvalidPage) { create bad }
+ end
+ end
+
+ def test_invalid_per_page_setting
+ assert_raise(ArgumentError) { create(1, -1) }
+ end
+
+ def test_page_count_was_removed
+ assert_raise(NoMethodError) { create.page_count }
+ # It's `total_pages` now.
+ end
+
+ private
+ def create(page = 2, limit = 5, total = nil, &block)
+ if block_given?
+ WillPaginate::Collection.create(page, limit, total, &block)
+ else
+ WillPaginate::Collection.new(page, limit, total)
+ end
+ end
+
+ def array(size = 3)
+ Array.new(size)
+ end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/console b/web/vendor/plugins/will_paginate/test/console
@@ -0,0 +1,8 @@
+#!/usr/bin/env ruby
+irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
+libs = []
+
+libs << 'irb/completion'
+libs << File.join('lib', 'load_fixtures')
+
+exec "#{irb} -Ilib:test#{libs.map{ |l| " -r #{l}" }.join} --simple-prompt"
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/database.yml b/web/vendor/plugins/will_paginate/test/database.yml
@@ -0,0 +1,22 @@
+sqlite3:
+ database: ":memory:"
+ adapter: sqlite3
+ timeout: 500
+
+sqlite2:
+ database: ":memory:"
+ adapter: sqlite2
+
+mysql:
+ adapter: mysql
+ username: root
+ password:
+ encoding: utf8
+ database: will_paginate_unittest
+
+postgres:
+ adapter: postgresql
+ username: mislav
+ password:
+ database: will_paginate_unittest
+ min_messages: warning
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/finder_test.rb b/web/vendor/plugins/will_paginate/test/finder_test.rb
@@ -0,0 +1,496 @@
+require 'helper'
+require 'lib/activerecord_test_case'
+
+require 'will_paginate'
+WillPaginate.enable_activerecord
+WillPaginate.enable_named_scope
+
+class FinderTest < ActiveRecordTestCase
+ fixtures :topics, :replies, :users, :projects, :developers_projects
+
+ def test_new_methods_presence
+ assert_respond_to_all Topic, %w(per_page paginate paginate_by_sql paginate_by_definition_in_class)
+ end
+
+ def test_simple_paginate
+ assert_queries(1) do
+ entries = Topic.paginate :page => nil
+ assert_equal 1, entries.current_page
+ assert_equal 1, entries.total_pages
+ assert_equal 4, entries.size
+ end
+
+ assert_queries(2) do
+ entries = Topic.paginate :page => 2
+ assert_equal 1, entries.total_pages
+ assert entries.empty?
+ end
+ end
+
+ def test_parameter_api
+ # :page parameter in options is required!
+ assert_raise(ArgumentError){ Topic.paginate }
+ assert_raise(ArgumentError){ Topic.paginate({}) }
+
+ # explicit :all should not break anything
+ assert_equal Topic.paginate(:page => nil), Topic.paginate(:all, :page => 1)
+
+ # :count could be nil and we should still not cry
+ assert_nothing_raised { Topic.paginate :page => 1, :count => nil }
+ end
+
+ def test_counting_when_integer_has_length_method
+ Integer.module_eval { def length; to_s.length; end }
+ begin
+ assert_equal 2, 11.length
+ entries = Developer.paginate :page => 1, :per_page => 5
+ assert_equal 11, entries.total_entries
+ assert_equal 5, entries.size
+ assert_equal 3, entries.total_pages
+ ensure
+ begin
+ Integer.module_eval { remove_method :length }
+ rescue
+ end
+ end
+ end
+
+ def test_paginate_with_per_page
+ entries = Topic.paginate :page => 1, :per_page => 1
+ assert_equal 1, entries.size
+ assert_equal 4, entries.total_pages
+
+ # Developer class has explicit per_page at 10
+ entries = Developer.paginate :page => 1
+ assert_equal 10, entries.size
+ assert_equal 2, entries.total_pages
+
+ entries = Developer.paginate :page => 1, :per_page => 5
+ assert_equal 11, entries.total_entries
+ assert_equal 5, entries.size
+ assert_equal 3, entries.total_pages
+ end
+
+ def test_paginate_with_order
+ entries = Topic.paginate :page => 1, :order => 'created_at desc'
+ expected = [topics(:futurama), topics(:harvey_birdman), topics(:rails), topics(:ar)].reverse
+ assert_equal expected, entries.to_a
+ assert_equal 1, entries.total_pages
+ end
+
+ def test_paginate_with_conditions
+ entries = Topic.paginate :page => 1, :conditions => ["created_at > ?", 30.minutes.ago]
+ expected = [topics(:rails), topics(:ar)]
+ assert_equal expected, entries.to_a
+ assert_equal 1, entries.total_pages
+ end
+
+ def test_paginate_with_include_and_conditions
+ entries = Topic.paginate \
+ :page => 1,
+ :include => :replies,
+ :conditions => "replies.content LIKE 'Bird%' ",
+ :per_page => 10
+
+ expected = Topic.find :all,
+ :include => 'replies',
+ :conditions => "replies.content LIKE 'Bird%' ",
+ :limit => 10
+
+ assert_equal expected, entries.to_a
+ assert_equal 1, entries.total_entries
+ end
+
+ def test_paginate_with_include_and_order
+ entries = nil
+ assert_queries(2) do
+ entries = Topic.paginate \
+ :page => 1,
+ :include => :replies,
+ :order => 'replies.created_at asc, topics.created_at asc',
+ :per_page => 10
+ end
+
+ expected = Topic.find :all,
+ :include => 'replies',
+ :order => 'replies.created_at asc, topics.created_at asc',
+ :limit => 10
+
+ assert_equal expected, entries.to_a
+ assert_equal 4, entries.total_entries
+ end
+
+ def test_paginate_associations_with_include
+ entries, project = nil, projects(:active_record)
+
+ assert_nothing_raised "THIS IS A BUG in Rails 1.2.3 that was fixed in [7326]. " +
+ "Please upgrade to a newer version of Rails." do
+ entries = project.topics.paginate \
+ :page => 1,
+ :include => :replies,
+ :conditions => "replies.content LIKE 'Nice%' ",
+ :per_page => 10
+ end
+
+ expected = Topic.find :all,
+ :include => 'replies',
+ :conditions => "project_id = #{project.id} AND replies.content LIKE 'Nice%' ",
+ :limit => 10
+
+ assert_equal expected, entries.to_a
+ end
+
+ def test_paginate_associations
+ dhh = users :david
+ expected_name_ordered = [projects(:action_controller), projects(:active_record)]
+ expected_id_ordered = [projects(:active_record), projects(:action_controller)]
+
+ assert_queries(2) do
+ # with association-specified order
+ entries = dhh.projects.paginate(:page => 1)
+ assert_equal expected_name_ordered, entries
+ assert_equal 2, entries.total_entries
+ end
+
+ # with explicit order
+ entries = dhh.projects.paginate(:page => 1, :order => 'projects.id')
+ assert_equal expected_id_ordered, entries
+ assert_equal 2, entries.total_entries
+
+ assert_nothing_raised { dhh.projects.find(:all, :order => 'projects.id', :limit => 4) }
+ entries = dhh.projects.paginate(:page => 1, :order => 'projects.id', :per_page => 4)
+ assert_equal expected_id_ordered, entries
+
+ # has_many with implicit order
+ topic = Topic.find(1)
+ expected = [replies(:spam), replies(:witty_retort)]
+ assert_equal expected.map(&:id).sort, topic.replies.paginate(:page => 1).map(&:id).sort
+ assert_equal expected.reverse, topic.replies.paginate(:page => 1, :order => 'replies.id ASC')
+ end
+
+ def test_paginate_association_extension
+ project = Project.find(:first)
+
+ assert_queries(2) do
+ entries = project.replies.paginate_recent :page => 1
+ assert_equal [replies(:brave)], entries
+ end
+ end
+
+ def test_paginate_with_joins
+ entries = nil
+
+ assert_queries(1) do
+ entries = Developer.paginate :page => 1,
+ :joins => 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id',
+ :conditions => 'project_id = 1'
+ assert_equal 2, entries.size
+ developer_names = entries.map &:name
+ assert developer_names.include?('David')
+ assert developer_names.include?('Jamis')
+ end
+
+ assert_queries(1) do
+ expected = entries.to_a
+ entries = Developer.paginate :page => 1,
+ :joins => 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id',
+ :conditions => 'project_id = 1', :count => { :select => "users.id" }
+ assert_equal expected, entries.to_a
+ assert_equal 2, entries.total_entries
+ end
+ end
+
+ def test_paginate_with_group
+ entries = nil
+ assert_queries(1) do
+ entries = Developer.paginate :page => 1, :per_page => 10,
+ :group => 'salary', :select => 'salary', :order => 'salary'
+ end
+
+ expected = [ users(:david), users(:jamis), users(:dev_10), users(:poor_jamis) ].map(&:salary).sort
+ assert_equal expected, entries.map(&:salary)
+ end
+
+ def test_paginate_with_dynamic_finder
+ expected = [replies(:witty_retort), replies(:spam)]
+ assert_equal expected, Reply.paginate_by_topic_id(1, :page => 1, :order => :created_at)
+
+ entries = Developer.paginate :conditions => { :salary => 100000 }, :page => 1, :per_page => 5
+ assert_equal 8, entries.total_entries
+ assert_equal entries, Developer.paginate_by_salary(100000, :page => 1, :per_page => 5)
+
+ # dynamic finder + conditions
+ entries = Developer.paginate_by_salary(100000, :page => 1,
+ :conditions => ['id > ?', 6])
+ assert_equal 4, entries.total_entries
+ assert_equal (7..10).to_a, entries.map(&:id)
+
+ assert_raises NoMethodError do
+ Developer.paginate_by_inexistent_attribute 100000, :page => 1
+ end
+ end
+
+ def test_scoped_paginate
+ entries = Developer.with_poor_ones { Developer.paginate :page => 1 }
+
+ assert_equal 2, entries.size
+ assert_equal 2, entries.total_entries
+ end
+
+ ## named_scope ##
+
+ def test_paginate_in_named_scope
+ entries = Developer.poor.paginate :page => 1, :per_page => 1
+
+ assert_equal 1, entries.size
+ assert_equal 2, entries.total_entries
+ end
+
+ def test_paginate_in_named_scope_on_habtm_association
+ project = projects(:active_record)
+ assert_queries(2) do
+ entries = project.developers.poor.paginate :page => 1, :per_page => 1
+
+ assert_equal 1, entries.size, 'one developer should be found'
+ assert_equal 1, entries.total_entries, 'only one developer should be found'
+ end
+ end
+
+ def test_paginate_in_named_scope_on_hmt_association
+ project = projects(:active_record)
+ expected = [replies(:brave)]
+
+ assert_queries(2) do
+ entries = project.replies.recent.paginate :page => 1, :per_page => 1
+ assert_equal expected, entries
+ assert_equal 1, entries.total_entries, 'only one reply should be found'
+ end
+ end
+
+ def test_paginate_in_named_scope_on_has_many_association
+ project = projects(:active_record)
+ expected = [topics(:ar)]
+
+ assert_queries(2) do
+ entries = project.topics.mentions_activerecord.paginate :page => 1, :per_page => 1
+ assert_equal expected, entries
+ assert_equal 1, entries.total_entries, 'only one topic should be found'
+ end
+ end
+
+ def test_named_scope_with_include
+ project = projects(:active_record)
+ entries = project.topics.with_replies_starting_with('AR ').paginate(:page => 1, :per_page => 1)
+ assert_equal 1, entries.size
+ end
+
+ ## misc ##
+
+ def test_count_and_total_entries_options_are_mutually_exclusive
+ e = assert_raise ArgumentError do
+ Developer.paginate :page => 1, :count => {}, :total_entries => 1
+ end
+ assert_match /exclusive/, e.to_s
+ end
+
+ def test_readonly
+ assert_nothing_raised { Developer.paginate :readonly => true, :page => 1 }
+ end
+
+ # this functionality is temporarily removed
+ def xtest_pagination_defines_method
+ pager = "paginate_by_created_at"
+ assert !User.methods.include_method?(pager), "User methods should not include `#{pager}` method"
+ # paginate!
+ assert 0, User.send(pager, nil, :page => 1).total_entries
+ # the paging finder should now be defined
+ assert User.methods.include_method?(pager), "`#{pager}` method should be defined on User"
+ end
+
+ # Is this Rails 2.0? Find out by testing find_all which was removed in [6998]
+ unless ActiveRecord::Base.respond_to? :find_all
+ def test_paginate_array_of_ids
+ # AR finders also accept arrays of IDs
+ # (this was broken in Rails before [6912])
+ assert_queries(1) do
+ entries = Developer.paginate((1..8).to_a, :per_page => 3, :page => 2, :order => 'id')
+ assert_equal (4..6).to_a, entries.map(&:id)
+ assert_equal 8, entries.total_entries
+ end
+ end
+ end
+
+ uses_mocha 'internals' do
+ def test_implicit_all_with_dynamic_finders
+ Topic.expects(:find_all_by_foo).returns([])
+ Topic.expects(:count).returns(0)
+ Topic.paginate_by_foo :page => 2
+ end
+
+ def test_guessing_the_total_count
+ Topic.expects(:find).returns(Array.new(2))
+ Topic.expects(:count).never
+
+ entries = Topic.paginate :page => 2, :per_page => 4
+ assert_equal 6, entries.total_entries
+ end
+
+ def test_guessing_that_there_are_no_records
+ Topic.expects(:find).returns([])
+ Topic.expects(:count).never
+
+ entries = Topic.paginate :page => 1, :per_page => 4
+ assert_equal 0, entries.total_entries
+ end
+
+ def test_extra_parameters_stay_untouched
+ Topic.expects(:find).with(:all, {:foo => 'bar', :limit => 4, :offset => 0 }).returns(Array.new(5))
+ Topic.expects(:count).with({:foo => 'bar'}).returns(1)
+
+ Topic.paginate :foo => 'bar', :page => 1, :per_page => 4
+ end
+
+ def test_count_skips_select
+ Developer.stubs(:find).returns([])
+ Developer.expects(:count).with({}).returns(0)
+ Developer.paginate :select => 'salary', :page => 2
+ end
+
+ def test_count_select_when_distinct
+ Developer.stubs(:find).returns([])
+ Developer.expects(:count).with(:select => 'DISTINCT salary').returns(0)
+ Developer.paginate :select => 'DISTINCT salary', :page => 2
+ end
+
+ def test_count_with_scoped_select_when_distinct
+ Developer.stubs(:find).returns([])
+ Developer.expects(:count).with(:select => 'DISTINCT users.id').returns(0)
+ Developer.distinct.paginate :page => 2
+ end
+
+ def test_should_use_scoped_finders_if_present
+ # scope-out compatibility
+ Topic.expects(:find_best).returns(Array.new(5))
+ Topic.expects(:with_best).returns(1)
+
+ Topic.paginate_best :page => 1, :per_page => 4
+ end
+
+ def test_paginate_by_sql
+ sql = "SELECT * FROM users WHERE type = 'Developer' ORDER BY id"
+ entries = Developer.paginate_by_sql(sql, :page => 2, :per_page => 3)
+ assert_equal 11, entries.total_entries
+ assert_equal [users(:dev_4), users(:dev_5), users(:dev_6)], entries
+ end
+
+ def test_paginate_by_sql_respects_total_entries_setting
+ sql = "SELECT * FROM users"
+ entries = Developer.paginate_by_sql(sql, :page => 1, :total_entries => 999)
+ assert_equal 999, entries.total_entries
+ end
+
+ def test_paginate_by_sql_strips_order_by_when_counting
+ Developer.expects(:find_by_sql).returns([])
+ Developer.expects(:count_by_sql).with("SELECT COUNT(*) FROM (sql\n ) AS count_table").returns(0)
+
+ Developer.paginate_by_sql "sql\n ORDER\nby foo, bar, `baz` ASC", :page => 2
+ end
+
+ # TODO: counts are still wrong
+ def test_ability_to_use_with_custom_finders
+ # acts_as_taggable defines find_tagged_with(tag, options)
+ Topic.expects(:find_tagged_with).with('will_paginate', :offset => 5, :limit => 5).returns([])
+ Topic.expects(:count).with({}).returns(0)
+
+ Topic.paginate_tagged_with 'will_paginate', :page => 2, :per_page => 5
+ end
+
+ def test_array_argument_doesnt_eliminate_count
+ ids = (1..8).to_a
+ Developer.expects(:find_all_by_id).returns([])
+ Developer.expects(:count).returns(0)
+
+ Developer.paginate_by_id(ids, :per_page => 3, :page => 2, :order => 'id')
+ end
+
+ def test_paginating_finder_doesnt_mangle_options
+ Developer.expects(:find).returns([])
+ options = { :page => 1, :per_page => 2, :foo => 'bar' }
+ options_before = options.dup
+
+ Developer.paginate(options)
+ assert_equal options_before, options
+ end
+
+ def test_paginate_by_sql_doesnt_change_original_query
+ query = 'SQL QUERY'
+ original_query = query.dup
+ Developer.expects(:find_by_sql).returns([])
+
+ Developer.paginate_by_sql query, :page => 1
+ assert_equal original_query, query
+ end
+
+ def test_paginated_each
+ collection = stub('collection', :size => 5, :empty? => false, :per_page => 5)
+ collection.expects(:each).times(2).returns(collection)
+ last_collection = stub('collection', :size => 4, :empty? => false, :per_page => 5)
+ last_collection.expects(:each).returns(last_collection)
+
+ params = { :order => 'id', :total_entries => 0 }
+
+ Developer.expects(:paginate).with(params.merge(:page => 2)).returns(collection)
+ Developer.expects(:paginate).with(params.merge(:page => 3)).returns(collection)
+ Developer.expects(:paginate).with(params.merge(:page => 4)).returns(last_collection)
+
+ assert_equal 14, Developer.paginated_each(:page => '2') { }
+ end
+
+ def test_paginated_each_with_named_scope
+ assert_equal 2, Developer.poor.paginated_each(:per_page => 1) {
+ assert_equal 11, Developer.count
+ }
+ end
+
+ # detect ActiveRecord 2.1
+ if ActiveRecord::Base.private_methods.include_method?(:references_eager_loaded_tables?)
+ def test_removes_irrelevant_includes_in_count
+ Developer.expects(:find).returns([1])
+ Developer.expects(:count).with({}).returns(0)
+
+ Developer.paginate :page => 1, :per_page => 1, :include => :projects
+ end
+
+ def test_doesnt_remove_referenced_includes_in_count
+ Developer.expects(:find).returns([1])
+ Developer.expects(:count).with({ :include => :projects, :conditions => 'projects.id > 2' }).returns(0)
+
+ Developer.paginate :page => 1, :per_page => 1,
+ :include => :projects, :conditions => 'projects.id > 2'
+ end
+ end
+
+ def test_paginate_from
+ result = Developer.paginate(:from => 'users', :page => 1, :per_page => 1)
+ assert_equal 1, result.size
+ end
+
+ def test_hmt_with_include
+ # ticket #220
+ reply = projects(:active_record).replies.find(:first, :order => 'replies.id')
+ assert_equal replies(:decisive), reply
+
+ # ticket #223
+ Project.find(1, :include => :replies)
+
+ # I cannot reproduce any of the failures from those reports :(
+ end
+
+ def test_hmt_with_uniq
+ project = Project.find(1)
+ result = project.unique_replies.paginate :page => 1, :per_page => 1,
+ :order => 'replies.id'
+ assert_equal replies(:decisive), result.first
+ end
+ end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/fixtures/admin.rb b/web/vendor/plugins/will_paginate/test/fixtures/admin.rb
@@ -0,0 +1,3 @@
+class Admin < User
+ has_many :companies, :finder_sql => 'SELECT * FROM companies'
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/fixtures/developer.rb b/web/vendor/plugins/will_paginate/test/fixtures/developer.rb
@@ -0,0 +1,14 @@
+class Developer < User
+ has_and_belongs_to_many :projects, :include => :topics, :order => 'projects.name'
+
+ def self.with_poor_ones(&block)
+ with_scope :find => { :conditions => ['salary <= ?', 80000], :order => 'salary' } do
+ yield
+ end
+ end
+
+ named_scope :distinct, :select => 'DISTINCT `users`.*'
+ named_scope :poor, :conditions => ['salary <= ?', 80000], :order => 'salary'
+
+ def self.per_page() 10 end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/fixtures/developers_projects.yml b/web/vendor/plugins/will_paginate/test/fixtures/developers_projects.yml
@@ -0,0 +1,13 @@
+david_action_controller:
+ developer_id: 1
+ project_id: 2
+ joined_on: 2004-10-10
+
+david_active_record:
+ developer_id: 1
+ project_id: 1
+ joined_on: 2004-10-10
+
+jamis_active_record:
+ developer_id: 2
+ project_id: 1
+\ No newline at end of file
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/fixtures/project.rb b/web/vendor/plugins/will_paginate/test/fixtures/project.rb
@@ -0,0 +1,17 @@
+class Project < ActiveRecord::Base
+ has_and_belongs_to_many :developers, :uniq => true
+
+ has_many :topics
+ # :finder_sql => 'SELECT * FROM topics WHERE (topics.project_id = #{id})',
+ # :counter_sql => 'SELECT COUNT(*) FROM topics WHERE (topics.project_id = #{id})'
+
+ has_many :replies, :through => :topics do
+ def find_recent(params = {})
+ with_scope :find => { :conditions => ['replies.created_at > ?', 15.minutes.ago] } do
+ find :all, params
+ end
+ end
+ end
+
+ has_many :unique_replies, :through => :topics, :source => :replies, :uniq => true
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/fixtures/projects.yml b/web/vendor/plugins/will_paginate/test/fixtures/projects.yml
@@ -0,0 +1,6 @@
+active_record:
+ id: 1
+ name: Active Record
+action_controller:
+ id: 2
+ name: Active Controller
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/fixtures/replies.yml b/web/vendor/plugins/will_paginate/test/fixtures/replies.yml
@@ -0,0 +1,29 @@
+witty_retort:
+ id: 1
+ topic_id: 1
+ content: Birdman is better!
+ created_at: <%= 6.hours.ago.to_s(:db) %>
+
+another:
+ id: 2
+ topic_id: 2
+ content: Nuh uh!
+ created_at: <%= 1.hour.ago.to_s(:db) %>
+
+spam:
+ id: 3
+ topic_id: 1
+ content: Nice site!
+ created_at: <%= 1.hour.ago.to_s(:db) %>
+
+decisive:
+ id: 4
+ topic_id: 4
+ content: "I'm getting to the bottom of this"
+ created_at: <%= 30.minutes.ago.to_s(:db) %>
+
+brave:
+ id: 5
+ topic_id: 4
+ content: "AR doesn't scare me a bit"
+ created_at: <%= 10.minutes.ago.to_s(:db) %>
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/fixtures/reply.rb b/web/vendor/plugins/will_paginate/test/fixtures/reply.rb
@@ -0,0 +1,7 @@
+class Reply < ActiveRecord::Base
+ belongs_to :topic, :include => [:replies]
+
+ named_scope :recent, :conditions => ['replies.created_at > ?', 15.minutes.ago]
+
+ validates_presence_of :content
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/fixtures/schema.rb b/web/vendor/plugins/will_paginate/test/fixtures/schema.rb
@@ -0,0 +1,38 @@
+ActiveRecord::Schema.define do
+
+ create_table "users", :force => true do |t|
+ t.column "name", :text
+ t.column "salary", :integer, :default => 70000
+ t.column "created_at", :datetime
+ t.column "updated_at", :datetime
+ t.column "type", :text
+ end
+
+ create_table "projects", :force => true do |t|
+ t.column "name", :text
+ end
+
+ create_table "developers_projects", :id => false, :force => true do |t|
+ t.column "developer_id", :integer, :null => false
+ t.column "project_id", :integer, :null => false
+ t.column "joined_on", :date
+ t.column "access_level", :integer, :default => 1
+ end
+
+ create_table "topics", :force => true do |t|
+ t.column "project_id", :integer
+ t.column "title", :string
+ t.column "subtitle", :string
+ t.column "content", :text
+ t.column "created_at", :datetime
+ t.column "updated_at", :datetime
+ end
+
+ create_table "replies", :force => true do |t|
+ t.column "content", :text
+ t.column "created_at", :datetime
+ t.column "updated_at", :datetime
+ t.column "topic_id", :integer
+ end
+
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/fixtures/topic.rb b/web/vendor/plugins/will_paginate/test/fixtures/topic.rb
@@ -0,0 +1,12 @@
+class Topic < ActiveRecord::Base
+ has_many :replies, :dependent => :destroy, :order => 'replies.created_at DESC'
+ belongs_to :project
+
+ named_scope :mentions_activerecord, :conditions => ['topics.title LIKE ?', '%ActiveRecord%']
+
+ named_scope :with_replies_starting_with, lambda { |text|
+ { :conditions => "replies.content LIKE '#{text}%' ", :include => :replies }
+ }
+
+ def self.paginate_by_definition_in_class; end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/fixtures/topics.yml b/web/vendor/plugins/will_paginate/test/fixtures/topics.yml
@@ -0,0 +1,30 @@
+futurama:
+ id: 1
+ title: Isnt futurama awesome?
+ subtitle: It really is, isnt it.
+ content: I like futurama
+ created_at: <%= 1.day.ago.to_s(:db) %>
+ updated_at:
+
+harvey_birdman:
+ id: 2
+ title: Harvey Birdman is the king of all men
+ subtitle: yup
+ content: He really is
+ created_at: <%= 2.hours.ago.to_s(:db) %>
+ updated_at:
+
+rails:
+ id: 3
+ project_id: 1
+ title: Rails is nice
+ subtitle: It makes me happy
+ content: except when I have to hack internals to fix pagination. even then really.
+ created_at: <%= 20.minutes.ago.to_s(:db) %>
+
+ar:
+ id: 4
+ project_id: 1
+ title: ActiveRecord sometimes freaks me out
+ content: "I mean, what's the deal with eager loading?"
+ created_at: <%= 15.minutes.ago.to_s(:db) %>
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/fixtures/user.rb b/web/vendor/plugins/will_paginate/test/fixtures/user.rb
@@ -0,0 +1,2 @@
+class User < ActiveRecord::Base
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/fixtures/users.yml b/web/vendor/plugins/will_paginate/test/fixtures/users.yml
@@ -0,0 +1,35 @@
+david:
+ id: 1
+ name: David
+ salary: 80000
+ type: Developer
+
+jamis:
+ id: 2
+ name: Jamis
+ salary: 150000
+ type: Developer
+
+<% for digit in 3..10 %>
+dev_<%= digit %>:
+ id: <%= digit %>
+ name: fixture_<%= digit %>
+ salary: 100000
+ type: Developer
+<% end %>
+
+poor_jamis:
+ id: 11
+ name: Jamis
+ salary: 9000
+ type: Developer
+
+admin:
+ id: 12
+ name: admin
+ type: Admin
+
+goofy:
+ id: 13
+ name: Goofy
+ type: Admin
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/helper.rb b/web/vendor/plugins/will_paginate/test/helper.rb
@@ -0,0 +1,37 @@
+require 'test/unit'
+require 'rubygems'
+
+# gem install redgreen for colored test output
+begin require 'redgreen'; rescue LoadError; end
+
+require 'boot' unless defined?(ActiveRecord)
+
+class Test::Unit::TestCase
+ protected
+ def assert_respond_to_all object, methods
+ methods.each do |method|
+ [method.to_s, method.to_sym].each { |m| assert_respond_to object, m }
+ end
+ end
+
+ def collect_deprecations
+ old_behavior = WillPaginate::Deprecation.behavior
+ deprecations = []
+ WillPaginate::Deprecation.behavior = Proc.new do |message, callstack|
+ deprecations << message
+ end
+ result = yield
+ [result, deprecations]
+ ensure
+ WillPaginate::Deprecation.behavior = old_behavior
+ end
+end
+
+# Wrap tests that use Mocha and skip if unavailable.
+def uses_mocha(test_name)
+ require 'mocha'
+rescue LoadError
+ $stderr.puts "Skipping #{test_name} tests. `gem install mocha` and try again."
+else
+ yield
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/lib/activerecord_test_case.rb b/web/vendor/plugins/will_paginate/test/lib/activerecord_test_case.rb
@@ -0,0 +1,43 @@
+require 'lib/activerecord_test_connector'
+
+class ActiveRecordTestCase < Test::Unit::TestCase
+ if defined?(ActiveSupport::Testing::SetupAndTeardown)
+ include ActiveSupport::Testing::SetupAndTeardown
+ end
+
+ if defined?(ActiveRecord::TestFixtures)
+ include ActiveRecord::TestFixtures
+ end
+ # Set our fixture path
+ if ActiveRecordTestConnector.able_to_connect
+ self.fixture_path = File.join(File.dirname(__FILE__), '..', 'fixtures')
+ self.use_transactional_fixtures = true
+ end
+
+ def self.fixtures(*args)
+ super if ActiveRecordTestConnector.connected
+ end
+
+ def run(*args)
+ super if ActiveRecordTestConnector.connected
+ end
+
+ # Default so Test::Unit::TestCase doesn't complain
+ def test_truth
+ end
+
+ protected
+
+ def assert_queries(num = 1)
+ $query_count = 0
+ yield
+ ensure
+ assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed."
+ end
+
+ def assert_no_queries(&block)
+ assert_queries(0, &block)
+ end
+end
+
+ActiveRecordTestConnector.setup
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/lib/activerecord_test_connector.rb b/web/vendor/plugins/will_paginate/test/lib/activerecord_test_connector.rb
@@ -0,0 +1,76 @@
+require 'active_record'
+require 'active_record/version'
+require 'active_record/fixtures'
+
+class ActiveRecordTestConnector
+ cattr_accessor :able_to_connect
+ cattr_accessor :connected
+
+ FIXTURES_PATH = File.join(File.dirname(__FILE__), '..', 'fixtures')
+
+ # Set our defaults
+ self.connected = false
+ self.able_to_connect = true
+
+ def self.setup
+ unless self.connected || !self.able_to_connect
+ setup_connection
+ load_schema
+ add_load_path FIXTURES_PATH
+ self.connected = true
+ end
+ rescue Exception => e # errors from ActiveRecord setup
+ $stderr.puts "\nSkipping ActiveRecord tests: #{e}\n\n"
+ self.able_to_connect = false
+ end
+
+ private
+
+ def self.add_load_path(path)
+ dep = defined?(ActiveSupport::Dependencies) ? ActiveSupport::Dependencies : ::Dependencies
+ autoload_paths = dep.respond_to?(:autoload_paths) ? dep.autoload_paths : dep.load_paths
+ autoload_paths.unshift path
+ end
+
+ def self.setup_connection
+ db = ENV['DB'].blank?? 'sqlite3' : ENV['DB']
+
+ configurations = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'database.yml'))
+ raise "no configuration for '#{db}'" unless configurations.key? db
+ configuration = configurations[db]
+
+ ActiveRecord::Base.logger = Logger.new(STDOUT) if $0 == 'irb'
+ puts "using #{configuration['adapter']} adapter" unless ENV['DB'].blank?
+
+ gem 'sqlite3-ruby' if 'sqlite3' == db
+
+ ActiveRecord::Base.establish_connection(configuration)
+ ActiveRecord::Base.configurations = { db => configuration }
+ prepare ActiveRecord::Base.connection
+
+ unless Object.const_defined?(:QUOTED_TYPE)
+ Object.send :const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')
+ end
+ end
+
+ def self.load_schema
+ ActiveRecord::Base.silence do
+ ActiveRecord::Migration.verbose = false
+ load File.join(FIXTURES_PATH, 'schema.rb')
+ end
+ end
+
+ def self.prepare(conn)
+ class << conn
+ IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SHOW FIELDS /]
+
+ def execute_with_counting(sql, name = nil, &block)
+ $query_count ||= 0
+ $query_count += 1 unless IGNORED_SQL.any? { |r| sql =~ r }
+ execute_without_counting(sql, name, &block)
+ end
+
+ alias_method_chain :execute, :counting
+ end
+ end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/lib/load_fixtures.rb b/web/vendor/plugins/will_paginate/test/lib/load_fixtures.rb
@@ -0,0 +1,11 @@
+require 'boot'
+require 'lib/activerecord_test_connector'
+
+# setup the connection
+ActiveRecordTestConnector.setup
+
+# load all fixtures
+Fixtures.create_fixtures(ActiveRecordTestConnector::FIXTURES_PATH, ActiveRecord::Base.connection.tables)
+
+require 'will_paginate'
+WillPaginate.enable_activerecord
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/lib/view_test_process.rb b/web/vendor/plugins/will_paginate/test/lib/view_test_process.rb
@@ -0,0 +1,179 @@
+require 'will_paginate/core_ext'
+require 'action_controller'
+require 'action_controller/test_process'
+
+require 'will_paginate'
+WillPaginate.enable_actionpack
+
+ActionController::Routing::Routes.draw do |map|
+ map.connect 'dummy/page/:page', :controller => 'dummy'
+ map.connect 'dummy/dots/page.:page', :controller => 'dummy', :action => 'dots'
+ map.connect 'ibocorp/:page', :controller => 'ibocorp',
+ :requirements => { :page => /\d+/ },
+ :defaults => { :page => 1 }
+
+ map.connect ':controller/:action/:id'
+end
+
+ActionController::Base.perform_caching = false
+
+class WillPaginate::ViewTestCase < Test::Unit::TestCase
+ if defined?(ActionController::TestCase::Assertions)
+ include ActionController::TestCase::Assertions
+ end
+ if defined?(ActiveSupport::Testing::Deprecation)
+ include ActiveSupport::Testing::Deprecation
+ end
+
+ def setup
+ super
+ @controller = DummyController.new
+ @request = @controller.request
+ @html_result = nil
+ @template = '<%= will_paginate collection, options %>'
+
+ @view = ActionView::Base.new
+ @view.assigns['controller'] = @controller
+ @view.assigns['_request'] = @request
+ @view.assigns['_params'] = @request.params
+ end
+
+ def test_no_complain; end
+
+ protected
+
+ def paginate(collection = {}, options = {}, &block)
+ if collection.instance_of? Hash
+ page_options = { :page => 1, :total_entries => 11, :per_page => 4 }.merge(collection)
+ collection = [1].paginate(page_options)
+ end
+
+ locals = { :collection => collection, :options => options }
+
+ unless @view.respond_to? :render_template
+ # Rails 2.2
+ @html_result = ActionView::InlineTemplate.new(@template).render(@view, locals)
+ else
+ if defined? ActionView::InlineTemplate
+ # Rails 2.1
+ args = [ ActionView::InlineTemplate.new(@view, @template, locals) ]
+ else
+ # older Rails versions
+ args = [nil, @template, nil, locals]
+ end
+
+ @html_result = @view.render_template(*args)
+ end
+
+ @html_document = HTML::Document.new(@html_result, true, false)
+
+ if block_given?
+ classname = options[:class] || WillPaginate::ViewHelpers.pagination_options[:class]
+ assert_select("div.#{classname}", 1, 'no main DIV', &block)
+ end
+ end
+
+ def response_from_page_or_rjs
+ @html_document.root
+ end
+
+ def validate_page_numbers expected, links, param_name = :page
+ param_pattern = /\W#{CGI.escape(param_name.to_s)}=([^&]*)/
+
+ assert_equal(expected, links.map { |e|
+ e['href'] =~ param_pattern
+ $1 ? $1.to_i : $1
+ })
+ end
+
+ def assert_links_match pattern, links = nil, numbers = nil
+ links ||= assert_select 'div.pagination a[href]' do |elements|
+ elements
+ end
+
+ pages = [] if numbers
+
+ links.each do |el|
+ assert_match pattern, el['href']
+ if numbers
+ el['href'] =~ pattern
+ pages << ($1.nil?? nil : $1.to_i)
+ end
+ end
+
+ assert_equal numbers, pages, "page numbers don't match" if numbers
+ end
+
+ def assert_no_links_match pattern
+ assert_select 'div.pagination a[href]' do |elements|
+ elements.each do |el|
+ assert_no_match pattern, el['href']
+ end
+ end
+ end
+end
+
+class DummyRequest
+ attr_accessor :symbolized_path_parameters
+
+ def initialize
+ @get = true
+ @params = {}
+ @symbolized_path_parameters = { :controller => 'foo', :action => 'bar' }
+ end
+
+ def get?
+ @get
+ end
+
+ def post
+ @get = false
+ end
+
+ def relative_url_root
+ ''
+ end
+
+ def params(more = nil)
+ @params.update(more) if more
+ @params
+ end
+end
+
+class DummyController
+ attr_reader :request
+ attr_accessor :controller_name
+
+ def initialize
+ @request = DummyRequest.new
+ @url = ActionController::UrlRewriter.new(@request, @request.params)
+ end
+
+ def params
+ @request.params
+ end
+
+ def url_for(params)
+ @url.rewrite(params)
+ end
+end
+
+module HTML
+ Node.class_eval do
+ def inner_text
+ children.map(&:inner_text).join('')
+ end
+ end
+
+ Text.class_eval do
+ def inner_text
+ self.to_s
+ end
+ end
+
+ Tag.class_eval do
+ def inner_text
+ childless?? '' : super
+ end
+ end
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/tasks.rake b/web/vendor/plugins/will_paginate/test/tasks.rake
@@ -0,0 +1,59 @@
+require 'rake/testtask'
+
+desc 'Test the will_paginate plugin.'
+Rake::TestTask.new(:test) do |t|
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+ t.libs << 'test'
+end
+
+# I want to specify environment variables at call time
+class EnvTestTask < Rake::TestTask
+ attr_accessor :env
+
+ def ruby(*args)
+ env.each { |key, value| ENV[key] = value } if env
+ super
+ env.keys.each { |key| ENV.delete key } if env
+ end
+end
+
+for configuration in %w( sqlite3 mysql postgres )
+ EnvTestTask.new("test_#{configuration}") do |t|
+ t.pattern = 'test/finder_test.rb'
+ t.verbose = true
+ t.env = { 'DB' => configuration }
+ t.libs << 'test'
+ end
+end
+
+task :test_databases => %w(test_mysql test_sqlite3 test_postgres)
+
+desc %{Test everything on SQLite3, MySQL and PostgreSQL}
+task :test_full => %w(test test_mysql test_postgres)
+
+desc %{Test everything with Rails 2.1.x, 2.0.x & 1.2.x gems}
+task :test_all do
+ all = Rake::Task['test_full']
+ versions = %w(2.3.2 2.2.2 2.1.0 2.0.4 1.2.6)
+ versions.each do |version|
+ ENV['RAILS_VERSION'] = "~> #{version}"
+ all.invoke
+ reset_invoked unless version == versions.last
+ end
+end
+
+def reset_invoked
+ %w( test_full test test_mysql test_postgres ).each do |name|
+ Rake::Task[name].instance_variable_set '@already_invoked', false
+ end
+end
+
+task :rcov do
+ excludes = %w( lib/will_paginate/named_scope*
+ lib/will_paginate/core_ext.rb
+ lib/will_paginate.rb
+ rails* )
+
+ system %[rcov -Itest:lib test/*.rb -x #{excludes.join(',')}]
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/test/view_test.rb b/web/vendor/plugins/will_paginate/test/view_test.rb
@@ -0,0 +1,373 @@
+require 'helper'
+require 'lib/view_test_process'
+
+class AdditionalLinkAttributesRenderer < WillPaginate::LinkRenderer
+ def initialize(link_attributes = nil)
+ super()
+ @additional_link_attributes = link_attributes || { :default => 'true' }
+ end
+
+ def page_link(page, text, attributes = {})
+ @template.link_to text, url_for(page), attributes.merge(@additional_link_attributes)
+ end
+end
+
+class ViewTest < WillPaginate::ViewTestCase
+
+ ## basic pagination ##
+
+ def test_will_paginate
+ paginate do |pagination|
+ assert_select 'a[href]', 3 do |elements|
+ validate_page_numbers [2,3,2], elements
+ assert_select elements.last, ':last-child', "Next »"
+ end
+ assert_select 'span', 2
+ assert_select 'span.disabled:first-child', '« Previous'
+ assert_select 'span.current', '1'
+ assert_equal '« Previous 1 2 3 Next »', pagination.first.inner_text
+ end
+ end
+
+ def test_no_pagination_when_page_count_is_one
+ paginate :per_page => 30
+ assert_equal '', @html_result
+ end
+
+ def test_will_paginate_with_options
+ paginate({ :page => 2 },
+ :class => 'will_paginate', :previous_label => 'Prev', :next_label => 'Next') do
+ assert_select 'a[href]', 4 do |elements|
+ validate_page_numbers [1,1,3,3], elements
+ # test rel attribute values:
+ assert_select elements[1], 'a', '1' do |link|
+ assert_equal 'prev start', link.first['rel']
+ end
+ assert_select elements.first, 'a', "Prev" do |link|
+ assert_equal 'prev start', link.first['rel']
+ end
+ assert_select elements.last, 'a', "Next" do |link|
+ assert_equal 'next', link.first['rel']
+ end
+ end
+ assert_select 'span.current', '2'
+ end
+ end
+
+ def test_will_paginate_using_renderer_class
+ paginate({}, :renderer => AdditionalLinkAttributesRenderer) do
+ assert_select 'a[default=true]', 3
+ end
+ end
+
+ def test_will_paginate_using_renderer_instance
+ renderer = WillPaginate::LinkRenderer.new
+ renderer.gap_marker = '<span class="my-gap">~~</span>'
+
+ paginate({ :per_page => 2 }, :inner_window => 0, :outer_window => 0, :renderer => renderer) do
+ assert_select 'span.my-gap', '~~'
+ end
+
+ renderer = AdditionalLinkAttributesRenderer.new(:title => 'rendered')
+ paginate({}, :renderer => renderer) do
+ assert_select 'a[title=rendered]', 3
+ end
+ end
+
+ def test_prev_next_links_have_classnames
+ paginate do |pagination|
+ assert_select 'span.disabled.prev_page:first-child'
+ assert_select 'a.next_page[href]:last-child'
+ end
+ end
+
+ def test_prev_label_deprecated
+ assert_deprecated ':previous_label' do
+ paginate({ :page => 2 }, :prev_label => 'Deprecated') do
+ assert_select 'a[href]:first-child', 'Deprecated'
+ end
+ end
+ end
+
+ def test_full_output
+ paginate
+ expected = <<-HTML
+ <div class="pagination"><span class="disabled prev_page">« Previous</span>
+ <span class="current">1</span>
+ <a href="/foo/bar?page=2" rel="next">2</a>
+ <a href="/foo/bar?page=3">3</a>
+ <a href="/foo/bar?page=2" class="next_page" rel="next">Next »</a></div>
+ HTML
+ expected.strip!.gsub!(/\s{2,}/, ' ')
+
+ assert_dom_equal expected, @html_result
+ end
+
+ def test_escaping_of_urls
+ paginate({:page => 1, :per_page => 1, :total_entries => 2},
+ :page_links => false, :params => { :tag => '<br>' })
+
+ assert_select 'a[href]', 1 do |links|
+ query = links.first['href'].split('?', 2)[1]
+ assert_equal %w(page=2 tag=%3Cbr%3E), query.split('&').sort
+ end
+ end
+
+ ## advanced options for pagination ##
+
+ def test_will_paginate_without_container
+ paginate({}, :container => false)
+ assert_select 'div.pagination', 0, 'main DIV present when it shouldn\'t'
+ assert_select 'a[href]', 3
+ end
+
+ def test_will_paginate_without_page_links
+ paginate({ :page => 2 }, :page_links => false) do
+ assert_select 'a[href]', 2 do |elements|
+ validate_page_numbers [1,3], elements
+ end
+ end
+ end
+
+ def test_will_paginate_windows
+ paginate({ :page => 6, :per_page => 1 }, :inner_window => 1) do |pagination|
+ assert_select 'a[href]', 8 do |elements|
+ validate_page_numbers [5,1,2,5,7,10,11,7], elements
+ assert_select elements.first, 'a', '« Previous'
+ assert_select elements.last, 'a', 'Next »'
+ end
+ assert_select 'span.current', '6'
+ assert_equal '« Previous 1 2 … 5 6 7 … 10 11 Next »', pagination.first.inner_text
+ end
+ end
+
+ def test_will_paginate_eliminates_small_gaps
+ paginate({ :page => 6, :per_page => 1 }, :inner_window => 2) do
+ assert_select 'a[href]', 12 do |elements|
+ validate_page_numbers [5,1,2,3,4,5,7,8,9,10,11,7], elements
+ end
+ end
+ end
+
+ def test_container_id
+ paginate do |div|
+ assert_nil div.first['id']
+ end
+
+ # magic ID
+ paginate({}, :id => true) do |div|
+ assert_equal 'fixnums_pagination', div.first['id']
+ end
+
+ # explicit ID
+ paginate({}, :id => 'custom_id') do |div|
+ assert_equal 'custom_id', div.first['id']
+ end
+ end
+
+ ## other helpers ##
+
+ def test_paginated_section
+ @template = <<-ERB
+ <% paginated_section collection, options do %>
+ <%= content_tag :div, '', :id => "developers" %>
+ <% end %>
+ ERB
+
+ paginate
+ assert_select 'div.pagination', 2
+ assert_select 'div.pagination + div#developers', 1
+ end
+
+ def test_page_entries_info
+ @template = '<%= page_entries_info collection %>'
+ array = ('a'..'z').to_a
+
+ paginate array.paginate(:page => 2, :per_page => 5)
+ assert_equal %{Displaying strings <b>6 - 10</b> of <b>26</b> in total},
+ @html_result
+
+ paginate array.paginate(:page => 7, :per_page => 4)
+ assert_equal %{Displaying strings <b>25 - 26</b> of <b>26</b> in total},
+ @html_result
+ end
+
+ uses_mocha 'class name' do
+ def test_page_entries_info_with_longer_class_name
+ @template = '<%= page_entries_info collection %>'
+ collection = ('a'..'z').to_a.paginate
+ collection.first.stubs(:class).returns(mock('class', :name => 'ProjectType'))
+
+ paginate collection
+ assert @html_result.index('project types'), "expected <#{@html_result.inspect}> to mention 'project types'"
+ end
+ end
+
+ def test_page_entries_info_with_single_page_collection
+ @template = '<%= page_entries_info collection %>'
+
+ paginate(('a'..'d').to_a.paginate(:page => 1, :per_page => 5))
+ assert_equal %{Displaying <b>all 4</b> strings}, @html_result
+
+ paginate(['a'].paginate(:page => 1, :per_page => 5))
+ assert_equal %{Displaying <b>1</b> string}, @html_result
+
+ paginate([].paginate(:page => 1, :per_page => 5))
+ assert_equal %{No entries found}, @html_result
+ end
+
+ def test_page_entries_info_with_custom_entry_name
+ @template = '<%= page_entries_info collection, :entry_name => "author" %>'
+
+ entries = (1..20).to_a
+
+ paginate(entries.paginate(:page => 1, :per_page => 5))
+ assert_equal %{Displaying authors <b>1 - 5</b> of <b>20</b> in total}, @html_result
+
+ paginate(entries.paginate(:page => 1, :per_page => 20))
+ assert_equal %{Displaying <b>all 20</b> authors}, @html_result
+
+ paginate(['a'].paginate(:page => 1, :per_page => 5))
+ assert_equal %{Displaying <b>1</b> author}, @html_result
+
+ paginate([].paginate(:page => 1, :per_page => 5))
+ assert_equal %{No authors found}, @html_result
+ end
+
+ ## parameter handling in page links ##
+
+ def test_will_paginate_preserves_parameters_on_get
+ @request.params :foo => { :bar => 'baz' }
+ paginate
+ assert_links_match /foo%5Bbar%5D=baz/
+ end
+
+ def test_will_paginate_doesnt_preserve_parameters_on_post
+ @request.post
+ @request.params :foo => 'bar'
+ paginate
+ assert_no_links_match /foo=bar/
+ end
+
+ def test_adding_additional_parameters
+ paginate({}, :params => { :foo => 'bar' })
+ assert_links_match /foo=bar/
+ end
+
+ def test_adding_anchor_parameter
+ paginate({}, :params => { :anchor => 'anchor' })
+ assert_links_match /#anchor$/
+ end
+
+ def test_removing_arbitrary_parameters
+ @request.params :foo => 'bar'
+ paginate({}, :params => { :foo => nil })
+ assert_no_links_match /foo=bar/
+ end
+
+ def test_adding_additional_route_parameters
+ paginate({}, :params => { :controller => 'baz', :action => 'list' })
+ assert_links_match %r{\Wbaz/list\W}
+ end
+
+ def test_will_paginate_with_custom_page_param
+ paginate({ :page => 2 }, :param_name => :developers_page) do
+ assert_select 'a[href]', 4 do |elements|
+ validate_page_numbers [1,1,3,3], elements, :developers_page
+ end
+ end
+ end
+
+ def test_will_paginate_with_atmark_url
+ @request.symbolized_path_parameters[:action] = "@tag"
+ renderer = WillPaginate::LinkRenderer.new
+
+ paginate({ :page => 1 }, :renderer=>renderer)
+ assert_links_match %r[/foo/@tag\?page=\d]
+ end
+
+ def test_complex_custom_page_param
+ @request.params :developers => { :page => 2 }
+
+ paginate({ :page => 2 }, :param_name => 'developers[page]') do
+ assert_select 'a[href]', 4 do |links|
+ assert_links_match /\?developers%5Bpage%5D=\d+$/, links
+ validate_page_numbers [1,1,3,3], links, 'developers[page]'
+ end
+ end
+ end
+
+ def test_custom_routing_page_param
+ @request.symbolized_path_parameters.update :controller => 'dummy', :action => nil
+ paginate :per_page => 2 do
+ assert_select 'a[href]', 6 do |links|
+ assert_links_match %r{/page/(\d+)$}, links, [2, 3, 4, 5, 6, 2]
+ end
+ end
+ end
+
+ def test_custom_routing_page_param_with_dot_separator
+ @request.symbolized_path_parameters.update :controller => 'dummy', :action => 'dots'
+ paginate :per_page => 2 do
+ assert_select 'a[href]', 6 do |links|
+ assert_links_match %r{/page\.(\d+)$}, links, [2, 3, 4, 5, 6, 2]
+ end
+ end
+ end
+
+ def test_custom_routing_with_first_page_hidden
+ @request.symbolized_path_parameters.update :controller => 'ibocorp', :action => nil
+ paginate :page => 2, :per_page => 2 do
+ assert_select 'a[href]', 7 do |links|
+ assert_links_match %r{/ibocorp(?:/(\d+))?$}, links, [nil, nil, 3, 4, 5, 6, 3]
+ end
+ end
+ end
+
+ ## internal hardcore stuff ##
+
+ class LegacyCollection < WillPaginate::Collection
+ alias :page_count :total_pages
+ undef :total_pages
+ end
+
+ def test_deprecation_notices_with_page_count
+ collection = LegacyCollection.new(1, 1, 2)
+
+ assert_deprecated collection.class.name do
+ paginate collection
+ end
+ end
+
+ uses_mocha 'view internals' do
+ def test_collection_name_can_be_guessed
+ collection = mock
+ collection.expects(:total_pages).returns(1)
+
+ @template = '<%= will_paginate options %>'
+ @controller.controller_name = 'developers'
+ @view.assigns['developers'] = collection
+
+ paginate(nil)
+ end
+ end
+
+ def test_inferred_collection_name_raises_error_when_nil
+ @template = '<%= will_paginate options %>'
+ @controller.controller_name = 'developers'
+
+ e = assert_raise ArgumentError do
+ paginate(nil)
+ end
+ assert e.message.include?('@developers')
+ end
+
+ if ActionController::Base.respond_to? :rescue_responses
+ # only on Rails 2
+ def test_rescue_response_hook_presence
+ assert_equal :not_found,
+ ActionController::Base.rescue_responses['WillPaginate::InvalidPage']
+ end
+ end
+
+end
(DIR) diff --git a/web/vendor/plugins/will_paginate/will_paginate.gemspec b/web/vendor/plugins/will_paginate/will_paginate.gemspec
@@ -0,0 +1,22 @@
+# encoding: utf-8
+require File.expand_path('../lib/will_paginate/version', __FILE__)
+
+Gem::Specification.new do |gem|
+ gem.name = 'will_paginate'
+ gem.version = WillPaginate::VERSION::STRING
+ gem.date = Time.now.strftime('%Y-%m-%d')
+
+ gem.summary = "Pagination for Rails"
+ gem.description = "The will_paginate library provides a simple, yet powerful and extensible API for ActiveRecord pagination and rendering of pagination links in ActionView templates."
+
+ gem.authors = ['Mislav Marohnić', 'PJ Hyett']
+ gem.email = 'mislav.marohnic@gmail.com'
+ gem.homepage = 'http://github.com/mislav/will_paginate/wikis'
+
+ gem.rubyforge_project = nil
+ gem.has_rdoc = true
+ gem.rdoc_options = ['--main', 'README.rdoc', '--charset=UTF-8']
+ gem.extra_rdoc_files = ['README.rdoc', 'LICENSE', 'CHANGELOG.rdoc']
+
+ gem.files = Dir['Rakefile', '{bin,lib,rails,test,spec}/**/*', 'README*', 'LICENSE*'] & `git ls-files -z`.split("\0")
+end