From 91641500a7caab7fa216517c9db661ed192b778d Mon Sep 17 00:00:00 2001 From: nic Date: Mon, 8 Aug 2005 01:54:05 +0000 Subject: [PATCH] Merged tracks-mu-import branch changes r113:130 into the trunk git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@131 a4c988fc-2ded-0310-b66e-134b36920a42 --- tracks/CHANGELOG | 189 ++- tracks/Rakefile | 93 +- tracks/app/controllers/application.rb | 32 +- tracks/app/controllers/context_controller.rb | 190 ++- tracks/app/controllers/feed_controller.rb | 50 +- tracks/app/controllers/login_controller.rb | 62 +- tracks/app/controllers/note_controller.rb | 46 +- tracks/app/controllers/project_controller.rb | 198 ++- tracks/app/controllers/todo_controller.rb | 153 +-- tracks/app/helpers/application_helper.rb | 27 +- tracks/app/helpers/feed_helper.rb | 30 +- tracks/app/helpers/todo_helper.rb | 64 +- tracks/app/models/context.rb | 70 +- tracks/app/models/note.rb | 10 +- tracks/app/models/project.rb | 70 +- tracks/app/models/todo.rb | 18 +- tracks/app/models/user.rb | 23 +- tracks/app/views/context/_show_items.rhtml | 14 +- tracks/app/views/context/show.rhtml | 41 +- tracks/app/views/feed/na_text.rhtml | 2 +- tracks/app/views/layouts/login.rhtml | 22 + tracks/app/views/layouts/standard.rhtml | 3 +- tracks/app/views/login/nosignup.rhtml | 5 + tracks/app/views/login/signup.rhtml | 54 +- tracks/app/views/note/_notes.rhtml | 7 +- tracks/app/views/note/_notes_summary.rhtml | 5 +- tracks/app/views/project/_show_items.rhtml | 14 +- tracks/app/views/project/show.rhtml | 78 +- .../app/views/shared/add_new_item_form.rhtml | 46 + tracks/app/views/shared/sidebar.rhtml | 31 + tracks/app/views/todo/_action_edit_form.rhtml | 9 +- tracks/app/views/todo/_done.rhtml | 28 +- tracks/app/views/todo/_item.rhtml | 59 + tracks/app/views/todo/_show_items.rhtml | 98 -- tracks/app/views/todo/completed.rhtml | 7 +- tracks/app/views/todo/completed_archive.rhtml | 2 +- tracks/app/views/todo/list.rhtml | 103 +- tracks/config/environment.rb | 2 +- tracks/config/environments/development.rb | 18 +- tracks/config/environments/production.rb | 8 +- tracks/config/environments/test.rb | 15 +- tracks/config/routes.rb | 10 +- tracks/config/settings.yml.tmpl | 1 + tracks/db/migrate/1_create_tracks_db.rb | 41 + tracks/db/migrate/2_add_user_id.rb | 16 + tracks/db/migrate/3_created_at.rb | 9 + tracks/db/migrate/4_notes.rb | 15 + tracks/doc/CHANGELOG | 9 +- tracks/public/404.html | 2 + tracks/public/500.html | 2 + tracks/public/dispatch.cgi | 2 +- tracks/public/dispatch.fcgi | 27 +- tracks/public/dispatch.rb | 2 +- tracks/public/javascripts/controls.js | 446 +++++++ tracks/public/javascripts/dragdrop.js | 537 ++++++++ tracks/public/javascripts/effects.js | 612 +++++++++ tracks/public/javascripts/prototype.js | 1143 ++++++++++------- tracks/script/benchmarker | 2 +- tracks/script/breakpointer | 2 +- tracks/script/console | 13 +- tracks/script/destroy | 4 +- tracks/script/generate | 4 +- tracks/script/profiler | 39 +- tracks/script/runner | 27 +- tracks/script/server | 8 +- tracks/test/fixtures/contexts.yml | 45 + tracks/test/fixtures/notes.yml | 2 + tracks/test/fixtures/projects.yml | 15 + tracks/test/fixtures/todos.yml | 168 +++ tracks/test/fixtures/users.yml | 13 + .../test/functional/login_controller_test.rb | 57 +- tracks/test/test_helper.rb | 43 +- tracks/test/unit/notes_test.rb | 4 +- tracks/test/unit/todo_test.rb | 55 +- tracks/test/unit/user_test.rb | 58 +- 75 files changed, 4054 insertions(+), 1375 deletions(-) create mode 100644 tracks/app/views/layouts/login.rhtml create mode 100644 tracks/app/views/login/nosignup.rhtml create mode 100644 tracks/app/views/shared/add_new_item_form.rhtml create mode 100644 tracks/app/views/shared/sidebar.rhtml create mode 100644 tracks/app/views/todo/_item.rhtml delete mode 100644 tracks/app/views/todo/_show_items.rhtml create mode 100644 tracks/db/migrate/1_create_tracks_db.rb create mode 100644 tracks/db/migrate/2_add_user_id.rb create mode 100644 tracks/db/migrate/3_created_at.rb create mode 100644 tracks/db/migrate/4_notes.rb create mode 100644 tracks/public/javascripts/controls.js create mode 100644 tracks/public/javascripts/dragdrop.js create mode 100644 tracks/public/javascripts/effects.js diff --git a/tracks/CHANGELOG b/tracks/CHANGELOG index de4b41bc..cc0cabe9 100644 --- a/tracks/CHANGELOG +++ b/tracks/CHANGELOG @@ -1,3 +1,172 @@ +*0.13.1* (11 July, 2005) + +* Fixed that each request with the WEBrick adapter would open a new database connection #1685 [Sam Stephenson] + +* Added support for SQL Server in the database rake tasks #1652 [ken.barker@gmail.com] Note: osql and scptxfr may need to be installed on your development environment. This involves getting the .exes and a .rll (scptxfr) from a production SQL Server (not developer level SQL Server). Add their location to your Environment PATH and you are all set. + +* Added a VERSION parameter to the migrate task that allows you to do "rake migrate VERSION=34" to migrate to the 34th version traveling up or down depending on the current version + +* Extend Ruby version check to include RUBY_RELEASE_DATE >= '2005-12-25', the final Ruby 1.8.2 release #1674 [court3nay@gmail.com] + +* Improved documentation for environment config files #1625 [court3nay@gmail.com] + + +*0.13.0* (6 July, 2005) + +* Changed the default logging level in config/environment.rb to INFO for production (so SQL statements won't be logged) + +* Added migration generator: ./script/generate migration add_system_settings + +* Added "migrate" as rake task to execute all the pending migrations from db/migrate + +* Fixed that model generator would make fixtures plural, even if ActiveRecord::Base.pluralize_table_names was false #1185 [Marcel Molina] + +* Added a DOCTYPE of HTML transitional to the HTML files generated by Rails #1124 [Michael Koziarski] + +* SIGTERM also gracefully exits dispatch.fcgi. Ignore SIGUSR1 on Windows. + +* Add the option to manually manage garbage collection in the FastCGI dispatcher. Set the number of requests between GC runs in your public/dispatch.fcgi [skaes@web.de] + +* Allow dynamic application reloading for dispatch.fcgi processes by sending a SIGHUP. If the process is currently handling a request, the request will be allowed to complete first. This allows production fcgi's to be reloaded without having to restart them. + +* RailsFCGIHandler (dispatch.fcgi) no longer tries to explicitly flush $stdout (CgiProcess#out always calls flush) + +* Fixed rakefile actions against PostgreSQL when the password is all numeric #1462 [michael@schubert.cx] + +* ActionMailer::Base subclasses are reloaded with the other rails components #1262 + +* Made the WEBrick adapter not use a mutex around action performance if ActionController::Base.allow_concurrency is true (default is false) + +* Fixed that mailer generator generated fixtures/plural while units expected fixtures/singular #1457 [Scott Barron] + +* Added a 'whiny nil' that's aim to ensure that when users pass nil to methods where that isn't appropriate, instead of NoMethodError? and the name of some method used by the framework users will see a message explaining what type of object was expected. Only active in test and development environments by default #1209 [Michael Koziarski] + +* Fixed the test_helper.rb to be safe for requiring controllers from multiple spots, like app/controllers/article_controller.rb and app/controllers/admin/article_controller.rb, without reloading the environment twice #1390 [Nicholas Seckar] + +* Fixed Webrick to escape + characters in URL's the same way that lighttpd and apache do #1397 [Nicholas Seckar] + +* Added -e/--environment option to script/runner #1408 [fbeausoleil@ftml.net] + +* Modernize the scaffold generator to use the simplified render and test methods and to change style from @params["id"] to params[:id]. #1367 + +* Added graceful exit from pressing CTRL-C during the run of the rails command #1150 [Caleb Tennis] + +* Allow graceful exits for dispatch.fcgi processes by sending a SIGUSR1. If the process is currently handling a request, the request will be allowed to complete and then will terminate itself. If a request is not being handled, the process is terminated immediately (via #exit). This basically works like restart graceful on Apache. [Jamis Buck] + +* Made dispatch.fcgi more robust by catching fluke errors and retrying unless its a permanent condition. [Jamis Buck] + +* Added console --profile for profiling an IRB session #1154 [Jeremy Kemper] + +* Changed console_sandbox into console --sandbox #1154 [Jeremy Kemper] + + +*0.12.1* (20th April, 2005) + +* Upgraded to Active Record 1.10.1, Action Pack 1.8.1, Action Mailer 0.9.1, Action Web Service 0.7.1 + + +*0.12.0* (19th April, 2005) + +* Fixed that purge_test_database would use database settings from the development environment when recreating the test database #1122 [rails@cogentdude.com] + +* Added script/benchmarker to easily benchmark one or more statement a number of times from within the environment. Examples: + + # runs the one statement 10 times + script/benchmarker 10 'Person.expensive_method(10)' + + # pits the two statements against each other with 50 runs each + script/benchmarker 50 'Person.expensive_method(10)' 'Person.cheap_method(10)' + +* Added script/profiler to easily profile a single statement from within the environment. Examples: + + script/profiler 'Person.expensive_method(10)' + script/profiler 'Person.expensive_method(10)' 10 # runs the statement 10 times + +* Added Rake target clear_logs that'll truncate all the *.log files in log/ to zero #1079 [Lucas Carlson] + +* Added lazy typing for generate, such that ./script/generate cn == ./script/generate controller and the likes #1051 [k@v2studio.com] + +* Fixed that ownership is brought over in pg_dump during tests for PostgreSQL #1060 [pburleson@gmail.com] + +* Upgraded to Active Record 1.10.0, Action Pack 1.8.0, Action Mailer 0.9.0, Action Web Service 0.7.0, Active Support 1.0.4 + + +*0.11.1* (27th March, 2005) + +* Fixed the dispatch.fcgi use of a logger + +* Upgraded to Active Record 1.9.1, Action Pack 1.7.0, Action Mailer 0.8.1, Action Web Service 0.6.2, Active Support 1.0.3 + + +*0.11.0* (22th March, 2005) + +* Removed SCRIPT_NAME from the WEBrick environment to prevent conflicts with PATH_INFO #896 [Nicholas Seckar] + +* Removed ?$1 from the dispatch.f/cgi redirect line to get rid of 'complete/path/from/request.html' => nil being in the @params now that the ENV["REQUEST_URI"] is used to determine the path #895 [dblack/Nicholas Seckar] + +* Added additional error handling to the FastCGI dispatcher to catch even errors taking down the entire process + +* Improved the generated scaffold code a lot to take advantage of recent Rails developments #882 [Tobias Luetke] + +* Combined the script/environment.rb used for gems and regular files version. If vendor/rails/* has all the frameworks, then files version is used, otherwise gems #878 [Nicholas Seckar] + +* Changed .htaccess to allow dispatch.* to be called from a sub-directory as part of the push with Action Pack to make Rails work on non-vhost setups #826 [Nicholas Seckar/Tobias Luetke] + +* Added script/runner which can be used to run code inside the environment by eval'ing the first parameter. Examples: + + ./script/runner 'ReminderService.deliver' + ./script/runner 'Mailer.receive(STDIN.read)' + + This makes it easier to do CRON and postfix scripts without actually making a script just to trigger 1 line of code. + +* Fixed webrick_server cookie handling to allow multiple cookes to be set at once #800, #813 [dave@cherryville.org] + +* Fixed the Rakefile's interaction with postgresql to: + + 1. Use PGPASSWORD and PGHOST in the environment to fix prompting for + passwords when connecting to a remote db and local socket connections. + 2. Add a '-x' flag to pg_dump which stops it dumping privileges #807 [rasputnik] + 3. Quote the user name and use template0 when dumping so the functions doesn't get dumped too #855 [pburleson] + 4. Use the port if available #875 [madrobby] + +* Upgraded to Active Record 1.9.0, Action Pack 1.6.0, Action Mailer 0.8.0, Action Web Service 0.6.1, Active Support 1.0.2 + + +*0.10.1* (7th March, 2005) + +* Fixed rake stats to ignore editor backup files like model.rb~ #791 [skanthak] + +* Added exception shallowing if the DRb server can't be started (not worth making a fuss about to distract new users) #779 [Tobias Luetke] + +* Added an empty favicon.ico file to the public directory of new applications (so the logs are not spammed by its absence) + +* Fixed that scaffold generator new template should use local variable instead of instance variable #778 [Dan Peterson] + +* Allow unit tests to run on a remote server for PostgreSQL #781 [adamm@galacticasoftware.com] + +* Added web_service generator (run ./script/generate web_service for help) #776 [Leon Bredt] + +* Added app/apis and components to code statistics report #729 [Scott Barron] + +* Fixed WEBrick server to use ABSOLUTE_RAILS_ROOT instead of working_directory #687 [Nicholas Seckar] + +* Fixed rails_generator to be usable without RubyGems #686 [Cristi BALAN] + +* Fixed -h/--help for generate and destroy generators #331 + +* Added begin/rescue around the FCGI dispatcher so no uncaught exceptions can bubble up to kill the process (logs to log/fastcgi.crash.log) + +* Fixed that association#count would produce invalid sql when called sequentialy #659 [kanis@comcard.de] + +* Fixed test/mocks/testing to the correct test/mocks/test #740 + +* Added early failure if the Ruby version isn't 1.8.2 or above #735 + +* Removed the obsolete -i/--index option from the WEBrick servlet #743 + +* Upgraded to Active Record 1.8.0, Action Pack 1.5.1, Action Mailer 0.7.1, Action Web Service 0.6.0, Active Support 1.0.1 + + *0.10.0* (24th February, 2005) * Changed default IP binding for WEBrick from 127.0.0.1 to 0.0.0.0 so that the server is accessible both locally and remotely #696 [Marcel] @@ -25,17 +194,17 @@ Views : components/list/items/show.rhtml -* Added --sandbox option to script/console that'll roll back all changes made to the database when you quit #672 [bitsweat] +* Added --sandbox option to script/console that'll roll back all changes made to the database when you quit #672 [Jeremy Kemper] -* Added 'recent' as a rake target that'll run tests for files that changed in the last 10 minutes #612 [bitsweat] +* Added 'recent' as a rake target that'll run tests for files that changed in the last 10 minutes #612 [Jeremy Kemper] -* Changed script/console to default to development environment and drop --no-inspect #650 [bitsweat] +* Changed script/console to default to development environment and drop --no-inspect #650 [Jeremy Kemper] -* Added that the 'fixture :posts' syntax can be used for has_and_belongs_to_many fixtures where a model doesn't exist #572 [bitsweat] +* Added that the 'fixture :posts' syntax can be used for has_and_belongs_to_many fixtures where a model doesn't exist #572 [Jeremy Kemper] * Added that running test_units and test_functional now performs the clone_structure_to_test as well #566 [rasputnik] -* Added new generator framework that informs about its doings on generation and enables updating and destruction of generated artifacts. See the new script/destroy and script/update for more details #487 [bitsweat] +* Added new generator framework that informs about its doings on generation and enables updating and destruction of generated artifacts. See the new script/destroy and script/update for more details #487 [Jeremy Kemper] * Added Action Web Service as a new add-on framework for Action Pack [Leon Bredt] @@ -239,7 +408,7 @@ Nothing changes inside the files themselves. -* Fixed a few references in the tests generated by new_mailer [bitsweat] +* Fixed a few references in the tests generated by new_mailer [Jeremy Kemper] * Added support for mocks in testing with test/mocks @@ -248,7 +417,7 @@ *0.8.5* (9) -* Made dev-util available to all tests, so you can insert breakpoints in any test case to get an IRB prompt at that point [bitsweat]: +* Made dev-util available to all tests, so you can insert breakpoints in any test case to get an IRB prompt at that point [Jeremy Kemper]: def test_complex_stuff @david.projects << @new_project @@ -257,11 +426,11 @@ You need to install dev-utils yourself for this to work ("gem install dev-util"). -* Added shared generator behavior so future upgrades should be possible without manually copying over files [bitsweat] +* Added shared generator behavior so future upgrades should be possible without manually copying over files [Jeremy Kemper] -* Added the new helper style to both controller and helper templates [bitsweat] +* Added the new helper style to both controller and helper templates [Jeremy Kemper] -* Added new_crud generator for creating a model and controller at the same time with explicit scaffolding [bitsweat] +* Added new_crud generator for creating a model and controller at the same time with explicit scaffolding [Jeremy Kemper] * Added configuration of Test::Unit::TestCase.fixture_path to test_helper to concide with the new AR fixtures style diff --git a/tracks/Rakefile b/tracks/Rakefile index 8ae36567..a22de1bd 100644 --- a/tracks/Rakefile +++ b/tracks/Rakefile @@ -16,7 +16,7 @@ task :environment do end end -desc "Generate API documentatio, show coding stats" +desc "Generate API documentation, show coding stats" task :doc => [ :appdoc, :stats ] @@ -65,39 +65,53 @@ Rake::RDocTask.new("appdoc") { |rdoc| rdoc.title = "Tracks Documentation" rdoc.options << '--line-numbers --inline-source' rdoc.rdoc_files.include('doc/README_FOR_APP') - rdoc.rdoc_files.include('doc/CHANGELOG') rdoc.rdoc_files.include('app/**/*.rb') } desc "Generate documentation for the Rails framework" Rake::RDocTask.new("apidoc") { |rdoc| rdoc.rdoc_dir = 'doc/api' + rdoc.template = "#{ENV['template']}.rb" if ENV['template'] rdoc.title = "Rails Framework Documentation" rdoc.options << '--line-numbers --inline-source' rdoc.rdoc_files.include('README') rdoc.rdoc_files.include('CHANGELOG') - rdoc.rdoc_files.include('vendor/railties/lib/breakpoint.rb') - rdoc.rdoc_files.include('vendor/railties/CHANGELOG') - rdoc.rdoc_files.include('vendor/railties/MIT-LICENSE') - rdoc.rdoc_files.include('vendor/activerecord/README') - rdoc.rdoc_files.include('vendor/activerecord/CHANGELOG') - rdoc.rdoc_files.include('vendor/activerecord/lib/active_record/**/*.rb') - rdoc.rdoc_files.exclude('vendor/activerecord/lib/active_record/vendor/*') - rdoc.rdoc_files.include('vendor/actionpack/README') - rdoc.rdoc_files.include('vendor/actionpack/CHANGELOG') - rdoc.rdoc_files.include('vendor/actionpack/lib/action_controller/**/*.rb') - rdoc.rdoc_files.include('vendor/actionpack/lib/action_view/**/*.rb') - rdoc.rdoc_files.include('vendor/actionmailer/README') - rdoc.rdoc_files.include('vendor/actionmailer/CHANGELOG') - rdoc.rdoc_files.include('vendor/actionmailer/lib/action_mailer/base.rb') + rdoc.rdoc_files.include('vendor/rails/railties/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/railties/MIT-LICENSE') + rdoc.rdoc_files.include('vendor/rails/activerecord/README') + rdoc.rdoc_files.include('vendor/rails/activerecord/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/activerecord/lib/active_record/**/*.rb') + rdoc.rdoc_files.exclude('vendor/rails/activerecord/lib/active_record/vendor/*') + rdoc.rdoc_files.include('vendor/rails/actionpack/README') + rdoc.rdoc_files.include('vendor/rails/actionpack/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/actionpack/lib/action_controller/**/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionpack/lib/action_view/**/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionmailer/README') + rdoc.rdoc_files.include('vendor/rails/actionmailer/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/actionmailer/lib/action_mailer/base.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/README') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/api/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/client/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/container/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/dispatcher/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/protocol/*.rb') + rdoc.rdoc_files.include('vendor/rails/actionwebservice/lib/action_web_service/support/*.rb') + rdoc.rdoc_files.include('vendor/rails/activesupport/README') + rdoc.rdoc_files.include('vendor/rails/activesupport/CHANGELOG') + rdoc.rdoc_files.include('vendor/rails/activesupport/lib/active_support/**/*.rb') } desc "Report code statistics (KLOCs, etc) from the application" -task :stats do +task :stats => [ :environment ] do require 'code_statistics' CodeStatistics.new( ["Helpers", "app/helpers"], ["Controllers", "app/controllers"], + ["APIs", "app/apis"], + ["Components", "components"], ["Functionals", "test/functional"], ["Models", "app/models"], ["Units", "test/unit"] @@ -114,10 +128,15 @@ task :clone_structure_to_test => [ :db_structure_dump, :purge_test_database ] do IO.readlines("db/#{RAILS_ENV}_structure.sql").join.split("\n\n").each do |table| ActiveRecord::Base.connection.execute(table) end - when "postgresql" - `psql -U #{abcs["test"]["username"]} -f db/#{RAILS_ENV}_structure.sql #{abcs["test"]["database"]}` + when "postgresql" + ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"] + ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"] + ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"] + `psql -U "#{abcs["test"]["username"]}" -f db/#{RAILS_ENV}_structure.sql #{abcs["test"]["database"]}` when "sqlite", "sqlite3" `#{abcs[RAILS_ENV]["adapter"]} #{abcs["test"]["dbfile"]} < db/#{RAILS_ENV}_structure.sql` + when "sqlserver" + `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql` else raise "Unknown database adapter '#{abcs["test"]["adapter"]}'" end @@ -130,10 +149,16 @@ task :db_structure_dump => :environment do when "mysql" ActiveRecord::Base.establish_connection(abcs[RAILS_ENV]) File.open("db/#{RAILS_ENV}_structure.sql", "w+") { |f| f << ActiveRecord::Base.connection.structure_dump } - when "postgresql" - `pg_dump -U #{abcs[RAILS_ENV]["username"]} -s -f db/#{RAILS_ENV}_structure.sql #{abcs[RAILS_ENV]["database"]}` + when "postgresql" + ENV['PGHOST'] = abcs[RAILS_ENV]["host"] if abcs[RAILS_ENV]["host"] + ENV['PGPORT'] = abcs[RAILS_ENV]["port"].to_s if abcs[RAILS_ENV]["port"] + ENV['PGPASSWORD'] = abcs[RAILS_ENV]["password"].to_s if abcs[RAILS_ENV]["password"] + `pg_dump -U "#{abcs[RAILS_ENV]["username"]}" -s -x -O -f db/#{RAILS_ENV}_structure.sql #{abcs[RAILS_ENV]["database"]}` when "sqlite", "sqlite3" `#{abcs[RAILS_ENV]["adapter"]} #{abcs[RAILS_ENV]["dbfile"]} .schema > db/#{RAILS_ENV}_structure.sql` + when "sqlserver" + `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /f db\\#{RAILS_ENV}_structure.sql /q /A /r` + `scptxfr /s #{abcs[RAILS_ENV]["host"]} /d #{abcs[RAILS_ENV]["database"]} /I /F db\ /q /A /r` else raise "Unknown database adapter '#{abcs["test"]["adapter"]}'" end @@ -144,14 +169,34 @@ task :purge_test_database => :environment do abcs = ActiveRecord::Base.configurations case abcs["test"]["adapter"] when "mysql" - ActiveRecord::Base.establish_connection(abcs[RAILS_ENV]) + ActiveRecord::Base.establish_connection(:test) ActiveRecord::Base.connection.recreate_database(abcs["test"]["database"]) when "postgresql" - `dropdb -U #{abcs["test"]["username"]} #{abcs["test"]["database"]}` - `createdb -U #{abcs["test"]["username"]} #{abcs["test"]["database"]}` + ENV['PGHOST'] = abcs["test"]["host"] if abcs["test"]["host"] + ENV['PGPORT'] = abcs["test"]["port"].to_s if abcs["test"]["port"] + ENV['PGPASSWORD'] = abcs["test"]["password"].to_s if abcs["test"]["password"] + `dropdb -U "#{abcs["test"]["username"]}" #{abcs["test"]["database"]}` + `createdb -T template0 -U "#{abcs["test"]["username"]}" #{abcs["test"]["database"]}` when "sqlite","sqlite3" File.delete(abcs["test"]["dbfile"]) if File.exist?(abcs["test"]["dbfile"]) + when "sqlserver" + dropfkscript = "#{abcs["test"]["host"]}.#{abcs["test"]["database"]}.DP1".gsub(/\\/,'-') + `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{dropfkscript}` + `osql -E -S #{abcs["test"]["host"]} -d #{abcs["test"]["database"]} -i db\\#{RAILS_ENV}_structure.sql` else raise "Unknown database adapter '#{abcs["test"]["adapter"]}'" end end + +desc "Clears all *.log files in log/" +task :clear_logs => :environment do + FileList["log/*.log"].each do |log_file| + f = File.open(log_file, "w") + f.close + end +end + +desc "Migrate the database according to the migrate scripts in db/migrate (only supported on PG/MySQL). A specific version can be targetted with VERSION=x" +task :migrate => :environment do + ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + '/db/migrate/', ENV["VERSION"] ? ENV["VERSION"].to_i : nil) +end diff --git a/tracks/app/controllers/application.rb b/tracks/app/controllers/application.rb index 183e5c7e..9e462b05 100644 --- a/tracks/app/controllers/application.rb +++ b/tracks/app/controllers/application.rb @@ -6,17 +6,19 @@ require_dependency "login_system" require 'date' class ApplicationController < ActionController::Base - + helper :application include LoginSystem - + + before_filter :set_session_expiration + # Contstants from settings.yml # DATE_FORMAT = app_configurations["formats"]["date"] WEEK_STARTS_ON = app_configurations["formats"]["week_starts"] NO_OF_ACTIONS = app_configurations["formats"]["hp_completed"] STALENESS_STARTS = app_configurations["formats"]["staleness_starts"] - + # Count the number of uncompleted actions, excluding those in hidden contexts # def count_shown_items(hidden) @@ -33,11 +35,29 @@ class ApplicationController < ActionController::Base def errors_for( obj ) error_messages_for( obj ) unless instance_eval("@#{obj}").nil? end - + # Reverses the urlize() method by substituting underscores for spaces # def deurlize(name) name.to_s.gsub(/_/, " ") end - -end \ No newline at end of file + + def set_session_expiration + # http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions + return if @controller_name == 'feed' + # If no session we don't care + if @session + # Get expiry time (allow ten seconds window for the case where we have none) + expiry_time = @session['expiry_time'] || Time.now + 10 + if expiry_time < Time.now + # Too late, matey... bang goes your session! + reset_session + else + # Okay, you get another hour + @session['expiry_time'] = Time.now + (60*60) + end + end + end + + +end diff --git a/tracks/app/controllers/context_controller.rb b/tracks/app/controllers/context_controller.rb index 382b2c3a..7e1abd2d 100644 --- a/tracks/app/controllers/context_controller.rb +++ b/tracks/app/controllers/context_controller.rb @@ -1,9 +1,9 @@ class ContextController < ApplicationController - + helper :context model :project model :todo - + before_filter :login_required layout "standard" @@ -16,160 +16,124 @@ class ContextController < ApplicationController # Set page title, and collect existing contexts in @contexts # def list + self.init @page_title = "TRACKS::List Contexts" - @contexts = Context.find(:all, :conditions => nil, :order => "position ASC", :limit => nil ) end # Filter the projects to show just the one passed in the URL # e.g. /project/show/ shows just . # def show - @context = Context.find_by_name(deurlize(@params["name"])) - @places = Context.find(:all, :order => "position ASC") - @projects = Project.find( :all, :conditions => "done=0", :order => "position ASC" ) + self.init + self.init_todos + self.check_user_set_context @page_title = "TRACKS::Context: #{@context.name}" - @not_done = Todo.find(:all, :conditions => "done=0 AND context_id=#{@context.id}", - :order => "due IS NULL, due ASC, created ASC") - @done = Todo.find(:all, :conditions => "done=1 AND context_id=#{@context.id}", - :order => "completed DESC") - @count = Todo.count( "context_id=#{@context.id} AND done=0" ) - end - + end + # Creates a new context via Ajax helpers # def new_context - @context = Context.new(@params['context']) - if @context.save - render_partial( 'context_listing', @context ) + context = @session['user'].contexts.build + context.attributes = @params['context'] + context.name = deurlize(context.name) + + if context.save + render :partial => 'context_listing', :locals => { :context_listing => context } else flash["warning"] = "Couldn't add new context" - render_text "#{flash["warning"]}" + render :text => "#{flash["warning"]}" end end - + # Edit the details of the context - # + # def update - context = Context.find(params[:id]) - context.attributes = @params["context"] - if context.save - render_partial 'context_listing', context + check_user_set_context + @context.attributes = @params["context"] + @context.name = deurlize(@context.name) + if @context.save + render_partial 'context_listing', @context else flash["warning"] = "Couldn't update new context" - render_text "" + render :text => "" end end - - # Edit the details of the action in this context - # - def update_action - @places = Context.find(:all, :order => "position ASC") - @projects = Project.find( :all, :conditions => "done=0", :order => "position ASC" ) - action = Todo.find(params[:id]) - action.attributes = @params["item"] - if action.due? - action.due = Date.strptime(@params["item"]["due"], DATE_FORMAT) - else - action.due = "" - end - - if action.save - render_partial 'show_items', action - else - flash["warning"] = "Couldn't update the action" - render_text "" - end - end - - # Called by a form button - # Parameters from form fields are passed to create new action - # - def add_item - @projects = Project.find( :all, :conditions => "done=0", :order => "position ASC" ) - @places = Context.find( :all, :order => "position ASC" ) - item = Todo.new - item.attributes = @params["new_item"] - - if item.due? - item.due = Date.strptime(@params["new_item"]["due"], DATE_FORMAT) - else - item.due = "" - end - - if item.save - render_partial 'show_items', item - else - flash["warning"] = "Couldn't add next action \"#{item.description}\"" - render_text "" - end - end - # Fairly self-explanatory; deletes the context # If the context contains actions, you'll get a warning dialogue. # If you choose to go ahead, any actions in the context will also be deleted. - def destroy - this_context = Context.find(params[:id]) - if this_context.destroy + def destroy + check_user_set_context + if @context.destroy render_text "" else - flash["warning"] = "Couldn't delete context \"#{context.name}\"" + flash["warning"] = "Couldn't delete context \"#{@context.name}\"" redirect_to( :controller => "context", :action => "list" ) end end - # Delete a next action in a context - # - def destroy_action - item = Todo.find(params[:id]) - if item.destroy - render_text "" - else - flash["warning"] = "Couldn't delete next action \"#{item.description}\"" - redirect_to :action => "list" - end - end - - # Toggles the 'done' status of the action - # - def toggle_check - @places = Context.find(:all, :order => "position ASC") - @projects = Project.find( :all, :conditions => "done=0", :order => "position ASC" ) - - item = Todo.find(params[:id]) - - item.toggle!('done') - render_partial 'show_items', item - end - # Methods for changing the sort order of the contexts in the list # def move_up - line = Context.find(params[:id]) - line.move_higher - line.save + check_user_set_context + @context.move_higher + @context.save redirect_to(:controller => "context", :action => "list") end - + def move_down - line = Context.find(params[:id]) - line.move_lower - line.save + check_user_set_context + @context.move_lower + @context.save redirect_to(:controller => "context", :action => "list") end - + def move_top - line = Context.find(params[:id]) - line.move_to_top - line.save + check_user_set_context + @context.move_to_top + @context.save redirect_to(:controller => "context", :action => "list") end - + def move_bottom - line = Context.find(params[:id]) - line.move_to_bottom - line.save + check_user_set_context + @context.move_to_bottom + @context.save redirect_to(:controller => "context", :action => "list" ) end - + + protected + + def check_user_set_context + @user = @session['user'] + if @params["name"] + @context = Context.find_by_name_and_user_id(deurlize(@params["name"]), @user.id) + elsif @params['id'] + @context = Context.find_by_id_and_user_id(@params["id"], @user.id) + else + redirect_to(:controller => "context", :action => "list" ) + end + if @user == @context.user + return @context + else + @context = nil # Should be nil anyway. + flash["warning"] = "Item and session user mis-match: #{@context.user_id} and #{@session['user'].id}!" + render_text "" + end + end + + + def init + @user = @session['user'] + @projects = @user.projects.collect { |x| x.done? ? nil:x }.compact + @contexts = @user.contexts + end + + def init_todos + check_user_set_context + @done = @context.find_done_todos + @not_done = @context.find_not_done_todos + @count = @not_done.size + end + end diff --git a/tracks/app/controllers/feed_controller.rb b/tracks/app/controllers/feed_controller.rb index 6502e2e3..aa2d0c87 100644 --- a/tracks/app/controllers/feed_controller.rb +++ b/tracks/app/controllers/feed_controller.rb @@ -1,31 +1,24 @@ # Produces an feeds of the next actions, both RSS and plain text # class FeedController < ApplicationController - + helper :feed model :todo, :context, :project - + + before_filter :check_token_against_user_word + def index end - + # Builds an RSS feed for the latest 15 items # This is fairly basic: it lists the action description as the title # and the item context as the description # def na_feed - # Check whether the token in the URL matches the word in the User's table - # Render the RSS feed if it is, or show an error message if not - @token = @params['token'] - @user_name = @params['name'] - @current_user = User.find_by_login(@user_name) - if (@token == @current_user.word && @user_name == @current_user.login) - @not_done = Todo.find_all( "done=0", "created DESC" ) - @headers["Content-Type"] = "text/xml; charset=utf-8" - else - render_text "Sorry, you don't have permission to view this page." - end + @not_done = @user.todos.collect { |x| x.done? ? nil:x }.compact.sort! {|x,y| y.created_at <=> x.created_at } + @headers["Content-Type"] = "text/xml; charset=utf-8" end - + # Builds a plain text page listing all the next actions, # sorted by context. Showing notes doesn't make much sense here # so they are omitted. You can use this with GeekTool to get your next actions @@ -33,19 +26,20 @@ class FeedController < ApplicationController # curl [url from "TXT" link on todo/list] # def na_text - # Check whether the token in the URL matches the word in the User's table - # Render the text file if it is, or show an error message if not - @token = @params['token'] - @user_name = @params['name'] - @current_user = User.find_by_login(@user_name) - if (@token == @current_user.word && @user_name == @current_user.login) - @places = Context.find_all - @projects = Project.find_all - @not_done = Todo.find_all( "done=0", "context_id ASC" ) - @headers["Content-Type"] = "text/plain; charset=utf-8" - else - render_text "Sorry, you don't have permission to view this page." + @contexts = @user.contexts + @not_done = @user.todos.collect { |x| x.done? ? nil:x }.compact.sort! {|x,y| x.context_id <=> y.context_id } + @headers["Content-Type"] = "text/plain; charset=utf-8" + end + +protected + + # Check whether the token in the URL matches the word in the User's table + def check_token_against_user_word + @user = User.find_by_login( @params['name'] ) + unless ( @params['token'] == @user.word) + render :text => "Sorry, you don't have permission to view this page." + return false end end - + end diff --git a/tracks/app/controllers/login_controller.rb b/tracks/app/controllers/login_controller.rb index ef9bfbfb..e99a2971 100644 --- a/tracks/app/controllers/login_controller.rb +++ b/tracks/app/controllers/login_controller.rb @@ -3,10 +3,10 @@ class LoginController < ApplicationController layout 'scaffold' def login + @page_title = "Login" case @request.method when :post if @session['user'] = User.authenticate(@params['user_login'], @params['user_password']) - flash['notice'] = "Login successful" redirect_back_or_default :controller => "todo", :action => "list" else @@ -15,35 +15,55 @@ class LoginController < ApplicationController end end end - + def signup - case @request.method - when :post - @user = User.new(@params['user']) - - if @user.save - @session['user'] = User.authenticate(@user.login, @params['user']['password']) - flash['notice'] = "Signup successful" - redirect_back_or_default :controller => "todo", :action => "list" - end - when :get - @user = User.new - end - end - + unless (User.find_all.empty? || ( @session['user'] && @session['user']['is_admin'] ) ) + @page_title = "No signups" + render :action => "nosignup" + return + end + @signupname = User.find_all.empty? ? "as the admin":"a new" + @page_title = "Sign up #{@signupname} user" + + if @session['new_user'] + @user = @session['new_user'] + @session['new_user'] = nil + else + @user = User.new + end + end + + def create + user = User.new(@params['user']) + unless user.valid? + @session['new_user'] = user + redirect_to :controller => 'login', :action => 'signup' + return + end + + user.is_admin = 1 if User.find_all.empty? + if user.save + @session['user'] = User.authenticate(user.login, @params['user']['password']) + flash['notice'] = "Signup successful" + redirect_back_or_default :controller => "todo", :action => "list" + end + end + def delete - if @params['id'] + if @params['id'] and ( @params['id'] = @session['user'].id or @session['user'].is_admin ) @user = User.find(@params['id']) + # TODO: Maybe it would be better to mark deleted. That way user deletes can be reversed. @user.destroy end redirect_back_or_default :controller => "todo", :action => "list" - end - + end + def logout @session['user'] = nil + reset_session end - + def welcome end - + end diff --git a/tracks/app/controllers/note_controller.rb b/tracks/app/controllers/note_controller.rb index 0656ebd9..24e9cf04 100644 --- a/tracks/app/controllers/note_controller.rb +++ b/tracks/app/controllers/note_controller.rb @@ -1,22 +1,26 @@ class NoteController < ApplicationController - + + model :user + before_filter :login_required + layout "standard" - + def index - @all_notes = Note.list_all + @user = @session['user'] + @all_notes = @user.notes @page_title = "TRACKS::All notes" end - + def show - @note = Note.find(@params[:id]) + @note = check_user_return_note @page_title = "TRACKS::Note " + @note.id.to_s end - + # Add a new note to this project # - def add_note - - note = Note.new + def add + @user = @session['user'] + note = @user.notes.build note.attributes = @params["new_note"] if note.save @@ -25,9 +29,9 @@ class NoteController < ApplicationController render_text "" end end - - def destroy_note - note = Note.find_by_id(@params[:id]) + + def delete + note = check_user_return_note if note.destroy render_text "" else @@ -35,9 +39,9 @@ class NoteController < ApplicationController render_text "" end end - - def update_note - note = Note.find_by_id(@params[:id]) + + def update + note = check_user_return_note note.attributes = @params["note"] if note.save render_partial 'notes', note @@ -46,5 +50,15 @@ class NoteController < ApplicationController render_text "" end end - + + protected + + def check_user_return_note + note = Note.find_by_id( @params['id'] ) + if @session['user'] == note.user + return note + else + render_text "" + end + end end diff --git a/tracks/app/controllers/project_controller.rb b/tracks/app/controllers/project_controller.rb index 21eccdd5..c04d2c97 100644 --- a/tracks/app/controllers/project_controller.rb +++ b/tracks/app/controllers/project_controller.rb @@ -1,13 +1,13 @@ class ProjectController < ApplicationController - + helper :project model :context model :todo - + before_filter :login_required layout "standard" - - def index + + def index list render_action "list" end @@ -16,161 +16,121 @@ class ProjectController < ApplicationController # Set page title, and collect existing projects in @projects # def list + init @page_title = "TRACKS::List Projects" - @projects = Project.find( :all, :order => "position ASC" ) end - + # Filter the projects to show just the one passed in the URL # e.g. /project/show/ shows just . # def show - @project = Project.find_by_name(deurlize(@params["name"])) - @places = Context.find(:all, :order => "position ASC") - @projects = Project.find( :all, :conditions => "done=0", :order => "position ASC" ) + init + init_todos + @notes = @project.notes @page_title = "TRACKS::Project: #{@project.name}" - @not_done = Todo.find(:all, :conditions => "done=0 AND project_id=#{@project.id}", - :order => "due IS NULL, due ASC, created ASC") - @done = Todo.find(:all, :conditions => "done=1 AND project_id=#{@project.id}", - :order => "completed DESC") - @count = @not_done.length end - + def new_project - @project = Project.new(@params['project']) - if @project.save - render_partial( 'project_listing', @project ) - else - flash["warning"] = "Couldn't update new project" - render_text "" - end - end - - # Edit the details of the project - # - def update - project = Project.find(params[:id]) - project.attributes = @params["project"] + project = @session['user'].projects.build + project.attributes = @params['project'] + project.name = deurlize(project.name) + if project.save - render_partial 'project_listing', project + render :partial => 'project_listing', :locals => { :project_listing => project } else flash["warning"] = "Couldn't update new project" + render :text => "" + end + end + + # Edit the details of the project + # + def update + check_user_set_project + @project.attributes = @params["project"] + @project.name = deurlize(@project.name) + if @project.save + render_partial 'project_listing', @project + else + flash["warning"] = "Couldn't update project" render_text "" end end - - # Edit the details of the action in this project - # - def update_action - @places = Context.find(:all, :order => "position ASC") - @projects = Project.find( :all, :conditions => "done=0", :order => "position ASC" ) - action = Todo.find(params[:id]) - action.attributes = @params["item"] - - if action.due? - action.due = Date.strptime(@params["item"]["due"], DATE_FORMAT) - else - action.due = "" - end - - if action.save - render_partial 'show_items', action - else - flash["warning"] = "Couldn't update the action" - render_text "" - end - end - - # Called by a form button - # Parameters from form fields are passed to create new action - # - def add_item - @projects = Project.find( :all, :conditions => "done=0", :order => "position ASC" ) - @places = Context.find( :all, :order => "position ASC" ) - item = Todo.new - item.attributes = @params["new_item"] - - if item.due? - item.due = Date.strptime(@params["new_item"]["due"], DATE_FORMAT) - else - item.due = "" - end - - if item.save - render_partial 'show_items', item - else - flash["warning"] = "Couldn't add next action \"#{item.description}\"" - render_text "" - end - end - # Delete a project # def destroy - this_project = Project.find( @params['id'] ) - if this_project.destroy + check_user_set_project + if @project.destroy render_text "" else - flash["warning"] = "Couldn't delete project \"#{project.name}\"" + flash["warning"] = "Couldn't delete project \"#{@project.name}\"" redirect_to( :controller => "project", :action => "list" ) end end - - - # Delete a next action in a project - # - def destroy_action - item = Todo.find(@params['id']) - if item.destroy - #flash["confirmation"] = "Next action \"#{item.description}\" was successfully deleted" - render_text "" - else - flash["warning"] = "Couldn't delete next action \"#{item.description}\"" - redirect_to :action => "list" - end - end - # Toggles the 'done' status of the action - # - def toggle_check - @places = Context.find(:all, :order => "position ASC") - @projects = Project.find( :all, :conditions => "done=0", :order => "position ASC" ) - item = Todo.find(@params['id']) - - item.toggle!('done') - render_partial 'show_items', item - end - - # Methods for changing the sort order of the projects in the list # def move_up - line = Project.find(params[:id]) - line.move_higher - line.save + check_user_set_project + @project.move_higher + @project.save redirect_to(:controller => "project", :action => "list") end def move_down - line = Project.find(params[:id]) - line.move_lower - line.save + check_user_set_project + @project.move_lower + @project.save redirect_to(:controller => "project", :action => "list") end def move_top - line = Project.find(params[:id]) - line.move_to_top - line.save + check_user_set_project + @project.move_to_top + @project.save redirect_to(:controller => "project", :action => "list") end def move_bottom - line = Project.find(params[:id]) - line.move_to_bottom - line.save + check_user_set_project + @project.move_to_bottom + @project.save redirect_to(:controller => "project", :action => "list" ) end - - + + protected + + def check_user_set_project + @user = @session['user'] + if @params["name"] + @project = Project.find_by_name_and_user_id(deurlize(@params["name"]), @user.id) + elsif @params['id'] + @project = Project.find_by_id_and_user_id(@params["id"], @user.id) + else + redirect_to(:controller => "project", :action => "list" ) + end + if @user == @project.user + return @project + else + @project = nil # Should be nil anyway + flash["warning"] = "Project and session user mis-match: #{@project.user_id} and #{@session['user'].id}!" + render_text "" + end + end + + + def init + @user = @session['user'] + @projects = @user.projects + @contexts = @user.contexts + end + + def init_todos + check_user_set_project + @done = @project.find_done_todos + @not_done = @project.find_not_done_todos + @count = @not_done.size + end + end diff --git a/tracks/app/controllers/todo_controller.rb b/tracks/app/controllers/todo_controller.rb index 54ab166b..69700d92 100644 --- a/tracks/app/controllers/todo_controller.rb +++ b/tracks/app/controllers/todo_controller.rb @@ -1,11 +1,12 @@ class TodoController < ApplicationController - + helper :todo - model :context, :project - + model :context, :project, :user + before_filter :login_required layout "standard" - + + def index list render_action "list" @@ -16,106 +17,93 @@ class TodoController < ApplicationController # Number of completed actions to show is determined by a setting in settings.yml # def list + self.init @page_title = "TRACKS::List tasks" - @projects = Project.find( :all, :conditions => "done=0", :order => "position ASC" ) - @places = Context.find( :all, :order => "position ASC" ) - @shown_places = Context.find( :all, :conditions => "hide=0", :order => "position ASC" ) - @hidden_places = Context.find( :all, :conditions => "hide=1", :order => "position ASC" ) - @done = Todo.find( :all, :conditions => "done=1", :order => "completed DESC", - :limit => NO_OF_ACTIONS ) - - # Set count badge to number of not-done, not hidden context items - @count = count_shown_items( @hidden_places ) - end + @done = @done[0..(NO_OF_ACTIONS-1)] + # Set count badge to number of not-done, not hidden context items + @count = @todos.collect { |x| ( !x.done? and !x.context.hidden? ) ? x:nil }.compact.size + end # List the completed tasks, sorted by completion date # # Use days declaration? 1.day.ago? def completed + self.init @page_title = "TRACKS::Completed tasks" - today_date = Date::today() - 1 - today_query = today_date.strftime("'%Y-%m-%d'") + " <= completed" - week_begin = Date::today() - 2 - week_end = Date::today() - 7 + day = (60 * 60 * 24) + today = Time.now - week_query = week_begin.strftime("'%Y-%m-%d'") + " >= completed - AND " + week_end.strftime("'%Y-%m-%d'") + " <= completed" + today_date = today - 1 * day + week_begin = today - 2 * day + week_end = today - 7* day + month_begin = today - 8 * day + month_end = today - 31 * day - month_begin = Date::today() - 8 - month_end = Date::today() - 31 + @done_today = @done.collect { |x| today_date <= x.completed ? x:nil }.compact + @done_this_week = @done.collect { |x| week_begin >= x.completed && week_end <= x.completed ? x:nil }.compact + @done_this_month = @done.collect { |x| month_begin >= x.completed && month_end <= x.completed ? x:nil }.compact - month_query = month_begin.strftime("'%Y-%m-%d'") + " >= completed - AND " + month_end.strftime("'%Y-%m-%d'") + " <= completed" - - @done_today = Todo.find_by_sql( "SELECT * FROM todos WHERE done = 1 AND #{today_query} - ORDER BY completed DESC;" ) - @done_this_week = Todo.find_by_sql( "SELECT * FROM todos WHERE done = 1 AND #{week_query} - ORDER BY completed DESC;" ) - @done_this_month = Todo.find_by_sql( "SELECT * FROM todos WHERE done = 1 AND #{month_query} - ORDER BY completed DESC;" ) end - + # Archived completed items, older than 31 days # def completed_archive + self.init @page_title = "TRACKS::Archived completed tasks" - archive_date = Date::today() - 32 - archive_query = archive_date.strftime("'%Y-%m-%d'") + " >= completed" - @done_archive = Todo.find_by_sql( "SELECT * FROM todos WHERE done = 1 AND #{archive_query} - ORDER BY completed DESC;" ) + archive_date = Time.now - 32 * (60 * 60 * 24) + @done_archive = @done.collect { |x| archive_date >= x.completed ? x:nil }.compact end - + # Called by a form button # Parameters from form fields are passed to create new action # in the selected context. def add_item - @projects = Project.find( :all, :conditions => "done=0", :order => "position ASC" ) - @places = Context.find( :all, :order => "position ASC" ) - - item = Todo.new + self.init + item = @user.todos.build item.attributes = @params["new_item"] - + if item.due? item.due = Date.strptime(@params["new_item"]["due"], DATE_FORMAT) else item.due = "" end - if item.save - render_partial 'show_items', item - else - flash["warning"] = "Couldn't add next action \"#{item.description}\"" - render_text "" - end - end - - # Edit the details of an action - # - def update_action - @places = Context.find(:all, :order => "position ASC") - @projects = Project.find( :all, :conditions => "done=0", :order => "position ASC" ) - action = Todo.find(params[:id]) - action.attributes = @params["item"] - if action.due? - action.due = Date.strptime(@params["item"]["due"], DATE_FORMAT) + if item.save + render :partial => 'item', :object => item else - action.due = "" + flash["warning"] = "Couldn't add next action \"#{item.description}\"" + render_text "" end - - if action.save - render_partial 'show_items', action + end + + # Edit the details of an action + # + def update_action + self.init + + item = check_user_return_item + item.attributes = @params["item"] + + if item.due? + item.due = Date.strptime(@params["item"]["due"], DATE_FORMAT) + else + item.due = "" + end + + if item.save + render :partial => 'item', :object => item else flash["warning"] = "Couldn't update the action" render_text "" end end - + # Delete a next action in a context # def destroy_action - item = Todo.find(@params['id']) + item = check_user_return_item if item.destroy render_text "" else @@ -123,17 +111,34 @@ class TodoController < ApplicationController render_text "" end end - + # Toggles the 'done' status of the action # - def toggle_check - @projects = Project.find( :all, :conditions => "done=0", :order => "position ASC" ) - @places = Context.find(:all, :order => "position ASC") - - item = Todo.find(@params['id']) + def toggle_check + self.init + item = check_user_return_item item.toggle!('done') - render_partial 'show_items', item - end - + render :partial => 'item', :object => item + end + + protected + + def check_user_return_item + item = Todo.find( @params['id'] ) + if @session['user'] == item.user + return item + else + flash["warning"] = "Item and session user mis-match: #{item.user.name} and #{@session['user'].name}!" + render_text "" + end + end + + def init + @user = @session['user'] + @projects = @user.projects + @contexts = @user.contexts + @todos = @user.todos + @done = @todos.collect { |x| x.done? ? x:nil }.compact.sort! {|x,y| y.completed <=> x.completed } + end end diff --git a/tracks/app/helpers/application_helper.rb b/tracks/app/helpers/application_helper.rb index e3e82acc..928bba29 100644 --- a/tracks/app/helpers/application_helper.rb +++ b/tracks/app/helpers/application_helper.rb @@ -1,6 +1,6 @@ # The methods added to this helper will be available to all templates in the application. module ApplicationHelper - + # Convert a date object to the format specified # in config/settings.yml # @@ -19,8 +19,7 @@ module ApplicationHelper def markdown(text) RedCloth.new(text).to_html end - - + # Wraps object in HTML tags, tag # def tag_object(object, tag) @@ -32,8 +31,8 @@ module ApplicationHelper def urlize(name) name.to_s.gsub(/ /, "_") end - - + + # Check due date in comparison to today's date # Flag up date appropriately with a 'traffic light' colour code # @@ -41,7 +40,7 @@ module ApplicationHelper if due == nil return "" end - + @now = Date.today @days = due-@now case @days @@ -56,7 +55,7 @@ module ApplicationHelper "" + format_date(due) + " " end end - + # Uses the 'staleness_starts' value from settings.yml (in days) to colour # the background of the action appropriately according to the age # of the creation date: @@ -64,16 +63,22 @@ module ApplicationHelper # * l2: created more than 2 x staleness_starts, but < 3 x staleness_starts # * l3: created more than 3 x staleness_starts # - def staleness(created) - if created < (ApplicationController::STALENESS_STARTS*3).days.ago + def staleness(item) + if item.created_at < (ApplicationController::STALENESS_STARTS*3).days.ago return "
" - elsif created < (ApplicationController::STALENESS_STARTS*2).days.ago + elsif item.created_at < (ApplicationController::STALENESS_STARTS*2).days.ago return "
" - elsif created < (ApplicationController::STALENESS_STARTS).days.ago + elsif item.created_at < (ApplicationController::STALENESS_STARTS).days.ago return "
" else return "
" end end + def calendar_setup( input_field ) + str = "Calendar.setup({ ifFormat:\"#{ApplicationController::DATE_FORMAT}\"" + str << ",firstDay:#{ApplicationController::WEEK_STARTS_ON},showOthers:true,range:[2004, 2010]" + str << ",step:1,inputField:\"" + input_field + "\",cache:true,align:\"TR\" })" + javascript_tag str + end end diff --git a/tracks/app/helpers/feed_helper.rb b/tracks/app/helpers/feed_helper.rb index 0d7f227a..a46bad0c 100644 --- a/tracks/app/helpers/feed_helper.rb +++ b/tracks/app/helpers/feed_helper.rb @@ -1,31 +1,31 @@ module FeedHelper - + # Build a nicely formatted text string for display # Context forms the heading, then the items are # indented underneath. If there is a due date # and the item is in a project, these are also displayed # - def build_text_page(list,contexts,projects) + def build_text_page(list,contexts) result_string = "" - for @place in @places - result_string << "\n" + @place.name.upcase + ":\n" + for context in contexts + result_string << "\n" + context.name.upcase + ":\n" - list.each do |@item| - if @item.context_id == @place.id - - if @item.due - result_string << " [" + format_date(@item.due) + "] " - result_string << @item.description + " " + list.each do |item| + if item.context_id == context.id + + if item.due + result_string << " [" + format_date(item.due) + "] " + result_string << item.description + " " else - result_string << " " + @item.description + " " + result_string << " " + item.description + " " end - - if @item.project_id - result_string << "(" + @item.project['name'] + ")" + + if item.project_id + result_string << "(" + item.project.name + ")" end result_string << "\n" end - + end end return result_string diff --git a/tracks/app/helpers/todo_helper.rb b/tracks/app/helpers/todo_helper.rb index 04595b69..afd1b762 100644 --- a/tracks/app/helpers/todo_helper.rb +++ b/tracks/app/helpers/todo_helper.rb @@ -1,9 +1,69 @@ module TodoHelper - + # Counts the number of uncompleted items in the selected context # def count_items(context) count = Todo.find_all("done=0 AND context_id=#{context.id}").length end - + + def form_remote_tag_todo_notdone( item ) + form_remote_tag( :url => url_for( :controller => "todo", :action => "toggle_check", :id => item.id ), + :html => { :id=> "checkbox-notdone-#{item.id}", :class => "inline-form" }, + :update => "completed", + :position => "top", + :loading => "Form.disable('checkbox-notdone-#{item.id}');", + :complete => "new Effect2.Fade('item-#{item.id}-container', true);" + ) + end + + def form_remote_tag_todo_done( item ) + form_remote_tag( :url => url_for( :controller => "todo", :action => "toggle_check", :id => item.id ), + :html => { :id=> "checkbox-done-#{item.id}", :class => "inline-form" }, + :update => "new_actions", + :position => "bottom", + :loading => "Form.disable('checkbox-done-#{item.id}');", + :complete => "Element.toggle('new_actions');new Effect2.Fade('done-item-#{item.id}-container', true);" + ) + end + + def form_remote_tag_edit_todo( item ) + form_remote_tag( :url => { :controller => 'todo', :action => 'update_action', :id => item.id }, + :html => { :id => "form-action-#{item.id}", :class => "inline-form" }, + :update => "item-#{item.id}-container", + :complete => "new Effect.Appear('item-#{item.id}-container');" + ) + end + + def link_to_remote_todo_notdone( item ) + str = "Element.toggle('item-#{item.id}','action-#{item.id}-edit-form');" + str << " new Effect.Appear('action-#{item.id}-edit-form');" + str << " Form.focusFirstElement('form-action-#{item.id}')" + link_to_remote( image_tag("blank", :title =>"Delete action", :class=>"delete_item"), + :update => "item-#{item.id}-container", + :loading => "new Effect2.Fade('item-#{item.id}-container', true)", + :url => { :controller => "todo", :action => "destroy_action", :id => item.id }, + :confirm => "Are you sure that you want to delete the action, \'#{item.description}\'?") + " " + + link_to_function(image_tag( "blank", :title => "Edit action", :class => "edit_item"), + str ) + " " + end + + def link_to_remote_todo_done( item ) + link_to_remote( image_tag("blank", :title =>"Delete action", :class=>"delete_item"), + :update => "done-item-#{item.id}-container", + :loading => "new Effect2.Fade('done-item-#{item.id}-container', true)", + :url => { :controller => "todo", :action => "destroy_action", :id => item.id }, + :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + + image_tag("blank") + " " + end + + def toggle_show_notes( item ) + str = "" + str << image_tag( "blank", :width=>"16", :height=>"16", :border=>"0" ) + "" + m_notes = markdown( item.notes ) + str << "
" + str << m_notes + "
" + str + end end diff --git a/tracks/app/models/context.rb b/tracks/app/models/context.rb index 3fab268e..30caafbb 100644 --- a/tracks/app/models/context.rb +++ b/tracks/app/models/context.rb @@ -1,31 +1,47 @@ class Context < ActiveRecord::Base - acts_as_list - has_many :todo, :dependent => true - - # Context name must not be empty - # and must be less than 255 bytes - validates_presence_of :name, :message => "context must have a name" - validates_length_of :name, :maximum => 255, :message => "context name must be less than %d" - validates_uniqueness_of :name, :message => "already exists" - - def self.list_of(hidden=0) - find(:all, :conditions => [ "hide = ?" , hidden ], :order => "position ASC") + has_many :todos, :order => "completed DESC" + belongs_to :user + acts_as_list :scope => :user + + attr_protected :user + + # Context name must not be empty + # and must be less than 255 bytes + validates_presence_of :name, :message => "context must have a name" + validates_length_of :name, :maximum => 255, :message => "context name must be less than %d" + validates_uniqueness_of :name, :message => "already exists", :scope => "user_id" + + def self.list_of(hidden=0) + find(:all, :conditions => [ "hide = ?" , hidden ], :order => "position ASC") + end + + def find_not_done_todos + todos = Todo.find :all, :conditions => "context_id = #{id} AND done = 0", + :order => "due IS NULL, due ASC, created_at ASC" + end + + def find_done_todos + todos = Todo.find :all, :conditions => "context_id = #{id} AND done = 1", + :order => "due IS NULL, due ASC, created_at ASC" + end + + # Returns a count of next actions in the given context + # The result is count and a string descriptor, correctly pluralised if there are no + # actions or multiple actions + # + def count_undone_todos(string="actions") + count = self.find_not_done_todos.size + if count == 1 + word = string.singularize + else + word = string.pluralize end - - # Returns a count of next actions in the given context - # The result is count and a string descriptor, correctly pluralised if there are no - # actions or multiple actions - # - def count_undone_todos(string="actions") - count = Todo.count( "context_id=#{self.id} AND done=0" ) - - if count == 1 - word = string.singularize - else - word = string.pluralize - end - return count.to_s + " " + word - end - + return count.to_s + " " + word + end + + def hidden? + self.hide == 1 + end + end diff --git a/tracks/app/models/note.rb b/tracks/app/models/note.rb index 115ae51b..93d920d8 100644 --- a/tracks/app/models/note.rb +++ b/tracks/app/models/note.rb @@ -1,13 +1,15 @@ class Note < ActiveRecord::Base - + belongs_to :user belongs_to :project - + + attr_protected :user + def self.list_all find(:all, :order => "created_at DESC") end - + def self.list_of(project_id) find(:all, :conditions => [ "project_id = ?" , project_id ]) end - + end diff --git a/tracks/app/models/project.rb b/tracks/app/models/project.rb index a3b4d55f..b018d660 100644 --- a/tracks/app/models/project.rb +++ b/tracks/app/models/project.rb @@ -1,32 +1,44 @@ class Project < ActiveRecord::Base - - has_many :todo, :dependent => true - has_many :note - acts_as_list - - # Project name must not be empty - # and must be less than 255 bytes - validates_presence_of :name, :message => "project must have a name" - validates_length_of :name, :maximum => 255, :message => "project name must be less than %d" - validates_uniqueness_of :name, :message => "already exists" - - def self.list_of(isdone=0) - find(:all, :conditions => [ "done = ?" , isdone ], :order => "position ASC") + + has_many :todos, :dependent => true + has_many :notes, :order => "created_at DESC" + belongs_to :user + acts_as_list :scope => :user + + attr_protected :user + + # Project name must not be empty + # and must be less than 255 bytes + validates_presence_of :name, :message => "project must have a name" + validates_length_of :name, :maximum => 255, :message => "project name must be less than %d" + validates_uniqueness_of :name, :message => "already exists", :scope =>"user_id" + + def self.list_of(isdone=0) + find(:all, :conditions => [ "done = ?" , isdone ], :order => "position ASC") + end + + def find_not_done_todos + todos = Todo.find :all, :conditions => "project_id = #{id} AND done = 0", + :order => "due IS NULL, due ASC, created_at ASC" + end + + def find_done_todos + todos = Todo.find :all, :conditions => "project_id = #{id} AND done = 1", + :order => "due IS NULL, due ASC, created_at ASC" + end + + # Returns a count of next actions in the given project + # The result is count and a string descriptor, correctly pluralised if there are no + # actions or multiple actions + # + def count_undone_todos(string="actions") + count = find_not_done_todos.size + if count == 1 + word = string.singularize + else + word = string.pluralize end - - # Returns a count of next actions in the given project - # The result is count and a string descriptor, correctly pluralised if there are no - # actions or multiple actions - # - def count_undone_todos(string="actions") - count = Todo.count( "project_id=#{self.id} AND done=0" ) - - if count == 1 - word = string.singularize - else - word = string.pluralize - end - return count.to_s + " " + word - end - + return count.to_s + " " + word + end + end diff --git a/tracks/app/models/todo.rb b/tracks/app/models/todo.rb index f19f679a..ae86b38b 100644 --- a/tracks/app/models/todo.rb +++ b/tracks/app/models/todo.rb @@ -1,26 +1,30 @@ class Todo < ActiveRecord::Base - + belongs_to :context, :order => 'name' belongs_to :project - + belongs_to :user + + attr_protected :user + # Description field can't be empty, and must be < 100 bytes # Notes must be < 60,000 bytes (65,000 actually, but I'm being cautious) validates_presence_of :description validates_length_of :description, :maximum => 100 validates_length_of :notes, :maximum => 60000 - + # Add a creation date (Ruby object format) to item before it's saved # if there is no existing creation date (this prevents creation date # being reset to completion date when item is completed) # def before_save - if self.created == nil - self.created = Time.now() - end - if self.done == 1 self.completed = Time.now() end end + def self.not_done( id=id ) + self.find(:all, :conditions =>[ "done = 0 AND context_id = ?", id], \ + :order =>"due IS NULL, due ASC, created_at ASC") + end + end diff --git a/tracks/app/models/user.rb b/tracks/app/models/user.rb index d7317c04..6862c901 100644 --- a/tracks/app/models/user.rb +++ b/tracks/app/models/user.rb @@ -2,32 +2,37 @@ require 'digest/sha1' # this model expects a certain database layout and its based on the name/login pattern. class User < ActiveRecord::Base + has_many :contexts, :order => "position ASC" + has_many :projects, :order => "position ASC" + has_many :todos, :order => "completed DESC" + has_many :notes, :order => "created_at DESC" + + attr_protected :is_admin def self.authenticate(login, pass) find_first(["login = ? AND password = ?", login, sha1(pass)]) - end + end def change_password(pass) update_attribute "password", self.class.sha1(pass) end - - protected + +protected def self.sha1(pass) - Digest::SHA1.hexdigest("change-me--#{pass}--") + Digest::SHA1.hexdigest("#{app_configurations["admin"]["loginhash"]}--#{pass}--") end - + before_create :crypt_password - + def crypt_password write_attribute("password", self.class.sha1(password)) if password == @password_confirmation - write_attribute("word", self.class.sha1(word)) + write_attribute("word", self.class.sha1(login + word)) end validates_length_of :password, :within => 5..40 validates_length_of :login, :within => 3..80 validates_presence_of :password, :login, :word validates_uniqueness_of :login, :on => :create - validates_uniqueness_of :word, :on => :create - validates_confirmation_of :password, :on => :create + validates_confirmation_of :password, :on => :create end diff --git a/tracks/app/views/context/_show_items.rhtml b/tracks/app/views/context/_show_items.rhtml index 4318b36d..80f8b335 100644 --- a/tracks/app/views/context/_show_items.rhtml +++ b/tracks/app/views/context/_show_items.rhtml @@ -1,7 +1,7 @@ <% item = show_items %> <% if !item.done? %>
-<%= form_remote_tag( :url => url_for( :controller => "context", :action => "toggle_check", :id => item.id ), +<%= form_remote_tag( :url => url_for( :controller => "todo", :action => "toggle_check", :id => item.id ), :html => { :id=> "checkbox-notdone-#{item.id}", :class => "inline-form" }, :update => "completed", :position => "top", @@ -15,7 +15,7 @@ link_to_remote( image_tag("blank", :title =>"Delete this action", :class=>"delete_item"), :update => "item-#{item.id}-container", :loading => "new Effect2.Fade('item-#{item.id}-container', true)", - :url => { :controller => "context", :action => "destroy_action", :id => item.id }, :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + " " + + :url => { :controller => "todo", :action => "destroy_action", :id => item.id }, :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + " " + link_to_function(image_tag( "blank", :title => "Edit item", :class=>"edit_item"), "Element.toggle('item-#{item.id}','action-#{item.id}-edit-form'); new Effect.Appear('action-#{item.id}-edit-form'); Form.focusFirstElement('form-action-#{item.id}');" ) + " " %> @@ -28,7 +28,7 @@ <% if item.due %>
<% else %> - <%= staleness(item.created) %> + <%= staleness( item ) %> <% end %> <%= due_date( item.due ) %> <%= item.description %> @@ -45,7 +45,7 @@ <%= end_form_tag %> <% else %>
-<%= form_remote_tag( :url => url_for( :controller => "context", :action => "toggle_check", :id => item.id ), +<%= form_remote_tag( :url => url_for( :controller => "todo", :action => "toggle_check", :id => item.id ), :html => { :id=> "checkbox-done-#{item.id}", :class => "inline-form" }, :update => "next_actions", :position => "bottom", @@ -70,7 +70,7 @@ link_to_remote( image_tag("blank", :title =>"Delete this action", :class=>"delete_item"), :update => "done-item-#{item.id}-container", :loading => "new Effect2.Fade('done-item-#{item.id}-container', true)", - :url => { :controller => "context", :action => "destroy_action", :id => item.id }, :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + " " + :url => { :controller => "todo", :action => "destroy_action", :id => item.id }, :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + " " %> <%= image_tag("blank") %>
@@ -95,4 +95,4 @@ <%= end_form_tag %>
<% end %> -<% item = nil %> \ No newline at end of file +<% item = nil %> diff --git a/tracks/app/views/context/show.rhtml b/tracks/app/views/context/show.rhtml index a609b619..7c2f01bf 100644 --- a/tracks/app/views/context/show.rhtml +++ b/tracks/app/views/context/show.rhtml @@ -36,7 +36,7 @@ - -

Active Projects:

-
    - <% for project in Project.list_of -%> -
  • <%= link_to( project.name, { :controller => "project", :action => "show", - :name => urlize(project.name) } ) + " (" + project.count_undone_todos("actions") + ")" %>
  • - <% end -%> -
- -

Completed Projects:

-
    - <% for project in Project.list_of( isdone=1 ) -%> -
  • <%= link_to( project.name, { :controller => "project", :action => "show", - :name => urlize(project.name) } ) + " (" + project.count_undone_todos("actions") + ")" %>
  • - <% end -%> -
- -

Active Contexts:

-
    - <% for context in Context.list_of -%> -
  • <%= link_to( context.name, { :controller => "context", :action => "show", - :name => urlize(context.name) } ) + " (" + context.count_undone_todos("actions") + ")" %>
  • - <% end -%> -
- -

Hidden Contexts:

-
    - <% for context in Context.list_of( hidden=1 ) -%> -
  • <%= link_to( context.name, { :controller => "context", :action => "show", - :name => urlize(context.name) } ) + " (" + context.count_undone_todos("actions") + ")" %>
  • - <% end -%> -
+<%= render "shared/sidebar" %>
<% if @flash["confirmation"] %>
<%= @flash["confirmation"] %>
<% end %> diff --git a/tracks/app/views/feed/na_text.rhtml b/tracks/app/views/feed/na_text.rhtml index c9c00007..42f4f50e 100644 --- a/tracks/app/views/feed/na_text.rhtml +++ b/tracks/app/views/feed/na_text.rhtml @@ -1 +1 @@ -<%= build_text_page( @not_done, @places, @projects ) %> +<%= build_text_page( @not_done, @contexts ) %> diff --git a/tracks/app/views/layouts/login.rhtml b/tracks/app/views/layouts/login.rhtml new file mode 100644 index 00000000..92dfce14 --- /dev/null +++ b/tracks/app/views/layouts/login.rhtml @@ -0,0 +1,22 @@ + + + + + <%= stylesheet_link_tag "scaffold" %> +<%= @page_title -%> + + + + + +<%= @content_for_layout %> + + + + diff --git a/tracks/app/views/layouts/standard.rhtml b/tracks/app/views/layouts/standard.rhtml index e0b115b8..7129e359 100644 --- a/tracks/app/views/layouts/standard.rhtml +++ b/tracks/app/views/layouts/standard.rhtml @@ -11,7 +11,8 @@ <%= javascript_include_tag 'calendar', 'calendar-en', 'calendar-setup' %> - + <%= auto_discovery_link_tag(:rss,{:controller => "feed", :action => "na_feed", :name => "#{@session['user']['login']}", :token => "#{@session['user']['word']}"}, {:title => "RSS feed of next actions"}) %> + <%= @page_title %> diff --git a/tracks/app/views/login/nosignup.rhtml b/tracks/app/views/login/nosignup.rhtml new file mode 100644 index 00000000..e0476cad --- /dev/null +++ b/tracks/app/views/login/nosignup.rhtml @@ -0,0 +1,5 @@ +
+

No Signups

+

You don't have permission to sign up for a new account.

+

Please contact the site administrator <%= mail_to "#{app_configurations["admin"]["email"]}", "by email", :encode => "hex" %> to get permission.

+
diff --git a/tracks/app/views/login/signup.rhtml b/tracks/app/views/login/signup.rhtml index bdfac4f2..aa35d32c 100644 --- a/tracks/app/views/login/signup.rhtml +++ b/tracks/app/views/login/signup.rhtml @@ -1,37 +1,25 @@ -<%= start_form_tag :action=> "signup" %> -
- <% if User.find_all.empty? %> - <%= hidden_field "user", "is_admin", "value" => 1 %> -

Sign up as the admin user

- <%= render_errors @user %>
-
- <%= text_field "user", "login", :size => 20 %>
-
- <%= password_field "user", "password", :size => 20 %>
-
- <%= password_field "user", "password_confirmation", :size => 20 %>
-
- <%= password_field "user", "word", :size => 20 %>
- - <% elsif (@session['user'] && @session['user']['is_admin'] == 1) %> - <%= hidden_field "user", "is_admin", "value" => 0 %> -

Sign up a new user

- <%= render_errors @user %>
-
- <%= text_field "user", "login", :size => 20 %>
-
- <%= password_field "user", "password", :size => 20 %>
-
- <%= password_field "user", "password_confirmation", :size => 20 %>
-
- <%= password_field "user", "word", :size => 20 %>
- - <% else %> -

Signup

-

You don't have permission to sign up for a new account.

-

Please contact the site administrator <%= mail_to "#{app_configurations["admin"]["email"]}", "by email", :encode => "hex" %> to get permission.

- <% end %> +<%= start_form_tag :action=> "create" %> + + <%= error_messages_for 'user' %>
+ +

<%= @page_title -%>

+ +
+ <%= text_field "user", "login", :size => 20 %>
+ +
+ <%= password_field "user", "password", :size => 20 %>
+ +
+ <%= password_field "user", "password_confirmation", :size => 20 %>
+ +
+ <%= password_field "user", "word", :size => 20 %>
+ + <%= end_form_tag %> +
+ diff --git a/tracks/app/views/note/_notes.rhtml b/tracks/app/views/note/_notes.rhtml index 4fb0b5a0..7f447998 100644 --- a/tracks/app/views/note/_notes.rhtml +++ b/tracks/app/views/note/_notes.rhtml @@ -10,7 +10,8 @@ :update => "note-#{note.id}", :loading => "new Effect2.Fade('note-#{note.id}-container', true)", :complete => "Element.remove('note-#{note.id}-wrapper');", - :url => { :action => "destroy_note", :id => note.id }, :confirm => "Are you sure that you want to delete the note \'#{note.id.to_s}\'?" ) + " "%><%= link_to_function(image_tag( "blank", :title => "Edit item", :class=>"edit_item"), + :url => { :controller => "note", :action => "delete", :id => note.id }, + :confirm => "Are you sure that you want to delete the note \'#{note.id.to_s}\'?" ) + " "%><%= link_to_function(image_tag( "blank", :title => "Edit item", :class=>"edit_item"), "Element.toggle('note-#{note.id}','note-#{note.id}-edit-form'); new Effect.Appear('note-#{note.id}-edit-form'); Form.focusFirstElement('form-note-#{note.id}');" ) + " | " %> <%= link_to("In: " + note.project.name, {:controller => "project", :action => "show", :name => urlize(note.project.name)}, :class=>"footer_link" ) %> |  Created: <%= format_date(note.created_at) %> @@ -21,7 +22,7 @@
-<% note = nil -%> \ No newline at end of file +<% note = nil -%> diff --git a/tracks/app/views/note/_notes_summary.rhtml b/tracks/app/views/note/_notes_summary.rhtml index 50ad67a2..b1565a51 100644 --- a/tracks/app/views/note/_notes_summary.rhtml +++ b/tracks/app/views/note/_notes_summary.rhtml @@ -1,6 +1,7 @@ <% note = notes_summary -%>
-<%= link_to(image_tag("blank"), {:controller => "note", :action => "show", :id => note.id}, :title => "Show note", :class => "show_notes" ) %>  +<%= link_to( image_tag("blank"), { :controller => "note", :action => "show", + :id => note.id}, :title => "Show note", :class => "show_notes" ) %>  <%= textilize(truncate(note.body, 50, "...")) %>
-<% note = nil -%> \ No newline at end of file +<% note = nil -%> diff --git a/tracks/app/views/project/_show_items.rhtml b/tracks/app/views/project/_show_items.rhtml index 8be82621..2b82331b 100644 --- a/tracks/app/views/project/_show_items.rhtml +++ b/tracks/app/views/project/_show_items.rhtml @@ -1,7 +1,7 @@ <% item = show_items %> <% if !item.done? %>
-<%= form_remote_tag( :url => url_for( :controller => "project", :action => "toggle_check", :id => item.id ), +<%= form_remote_tag( :url => url_for( :controller => "todo", :action => "toggle_check", :id => item.id ), :html => { :id=> "checkbox-notdone-#{item.id}", :class => "inline-form" }, :update => "completed", :position => "top", @@ -15,7 +15,7 @@ link_to_remote( image_tag("blank", :title =>"Delete action", :class=>"delete_item"), :update => "item-#{item.id}-container", :loading => "new Effect2.Fade('item-#{item.id}-container', true)", - :url => { :controller => "project", :action => "destroy_action", :id => item.id }, :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + " " + + :url => { :controller => "todo", :action => "destroy_action", :id => item.id }, :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + " " + link_to_function(image_tag( "blank", :title => "Edit action", :class=>"edit_item"), "Element.toggle('item-#{item.id}','action-#{item.id}-edit-form'); new Effect.Appear('action-#{item.id}-edit-form'); Form.focusFirstElement('form-action-#{item.id}');" ) + " " %>
@@ -28,7 +28,7 @@ <% if item.due %>
<% else %> - <%= staleness(item.created) %> + <%= staleness( item ) %> <% end %> <%= due_date( item.due ) %> @@ -46,7 +46,7 @@ <%= end_form_tag %> <% else %>
-<%= form_remote_tag( :url => url_for( :controller => "project", :action => "toggle_check", :id => item.id ), +<%= form_remote_tag( :url => url_for( :controller => "todo", :action => "toggle_check", :id => item.id ), :html => { :id=> "checkbox-done-#{item.id}", :class => "inline-form" }, :update => "next_actions", :position => "bottom", @@ -71,7 +71,7 @@ link_to_remote( image_tag("blank", :title =>"Delete action", :class=>"delete_item"), :update => "done-item-#{item.id}-container", :loading => "new Effect2.Fade('done-item-#{item.id}-container', true)", - :url => { :controller => "project", :action => "destroy_action", :id => item.id }, :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + " " + :url => { :controller => "todo", :action => "destroy_action", :id => item.id }, :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + " " %>
@@ -95,4 +95,4 @@ <%= end_form_tag %>
<% end %> -<% item = nil %> \ No newline at end of file +<% item = nil %> diff --git a/tracks/app/views/project/show.rhtml b/tracks/app/views/project/show.rhtml index 8ec7b711..3ce893cb 100644 --- a/tracks/app/views/project/show.rhtml +++ b/tracks/app/views/project/show.rhtml @@ -12,48 +12,40 @@ <% elsif @not_done.empty? -%>

There are no next actions yet in this project

<% else -%> - <% for item in @not_done -%> - <%= render_partial "show_items", item %> - <% end -%> + <%= render :partial => "show_items", :collection => @not_done %> <% end -%>
- +

Completed actions in this project

- +
<% if @done.empty? %>

There are no completed next actions yet in this project

<% else %> - <% for done_item in @done %> - <%= render_partial "show_items", done_item %> - <% end %> + <%= render :partial => "show_items", :collection => @done %> <% end %>
-<% @notes = Note.list_of(@project.id) -%>
-<% if @notes.empty? -%> -

There are no notes yet for this project.

-<% end -%> - -
-

Notes

- <% for note in @notes -%> - <%= render_partial "note/notes_summary", note %> +
+

Notes

+ <% if @notes.empty? -%> +

There are no notes yet for this project.

+ <% else -%> + <%= render :partial => "note/notes_summary", :collection => @notes %> <% end -%> -
+
-<%= link_to_function( "Add a note", - "Element.toggle('new-note'); Form.focusFirstElement('form-new-note');") %> - +<%= link_to_function( "Add a note", "Element.toggle('new-note'); Form.focusFirstElement('form-new-note');") %> + <% if @flash["confirmation"] %>
<%= @flash["confirmation"] %>
<% end %> diff --git a/tracks/app/views/shared/add_new_item_form.rhtml b/tracks/app/views/shared/add_new_item_form.rhtml new file mode 100644 index 00000000..34d32b9f --- /dev/null +++ b/tracks/app/views/shared/add_new_item_form.rhtml @@ -0,0 +1,46 @@ +<% + case controller.controller_name + when "context" + add_string = "Add a next action in this context »" + when "project" + add_string = "Add a next action in this project »" + else + add_string = "Add a next action »" + end +%> + +<%= link_to_function( + add_string, + "Element.toggle('todo_new_action');Form.focusFirstElement('todo-form-new-action');Element.toggle('new_actions');", + {:title => "Add the next action [Alt+n]", :accesskey => "n"}) %> + + diff --git a/tracks/app/views/shared/sidebar.rhtml b/tracks/app/views/shared/sidebar.rhtml new file mode 100644 index 00000000..465d37c1 --- /dev/null +++ b/tracks/app/views/shared/sidebar.rhtml @@ -0,0 +1,31 @@ +

Active Projects:

+
    + <% for project in @projects.collect { |x| x.done? ? nil:x }.compact -%> +
  • <%= link_to( project.name, { :controller => "project", :action => "show", + :name => urlize(project.name) } ) + " (" + project.count_undone_todos("actions") + ")" %>
  • + <% end -%> +
+ +

Completed Projects:

+
    + <% for project in @projects.collect { |x| x.done? ? x:nil }.compact -%> +
  • <%= link_to( project.name, { :controller => "project", :action => "show", + :name => urlize(project.name) } ) + " (" + project.count_undone_todos("actions") + ")" %>
  • + <% end -%> +
+ +

Active Contexts:

+
    + <% for context in @contexts.collect { |x| x.hidden? ? nil:x }.compact -%> +
  • <%= link_to( context.name, { :controller => "context", :action => "show", + :name => urlize(context.name) } ) + " (" + context.count_undone_todos("actions") + ")" %>
  • + <% end -%> +
+ +

Hidden Contexts:

+
    + <% for context in @contexts.collect { |x| x.hidden? ? x:nil }.compact -%> +
  • <%= link_to( context.name, { :controller => "context", :action => "show", + :name => urlize(context.name) } ) + " (" + context.count_undone_todos("actions") + ")" %>
  • + <% end -%> +
diff --git a/tracks/app/views/todo/_action_edit_form.rhtml b/tracks/app/views/todo/_action_edit_form.rhtml index e8b34be3..35a037ec 100644 --- a/tracks/app/views/todo/_action_edit_form.rhtml +++ b/tracks/app/views/todo/_action_edit_form.rhtml @@ -16,7 +16,7 @@ + + + <% if item.due %> +
+ <% else %> + <%= staleness( item ) %> + <% end %> + <%= due_date( item.due ) %> + <%= item.description %> + <% if item.project_id %> + <%= link_to( "[P]", { :controller => "project", :action => "show", :name => urlize(item.project.name) }, :title => "View project: #{item.project.name}" ) %> + <% end %> + <% if item.notes? %> + <%= toggle_show_notes( item ) %> + <% end %> +
+ +<%= end_form_tag %> + + + + +<% else %> +
+ <%= form_remote_tag_todo_done( item ) %> +
+
<%= link_to_remote_todo_done( item ) %>
+ +
+ +
+ +
+ <%= format_date( item.completed ) %> + <%= item.description %> + <% if item.project_id %> + <%= link_to( "[P]", { :controller => "project", :action => "show", :name => urlize(item.project.name) }, :title => "View project: #{item.project.name}" ) %> + <% end %> + <% if item.notes? %> + <%= toggle_show_notes( item ) %> + <% end %> +
+
+<%= end_form_tag %> +
+<% end %> diff --git a/tracks/app/views/todo/_show_items.rhtml b/tracks/app/views/todo/_show_items.rhtml deleted file mode 100644 index 1dd359af..00000000 --- a/tracks/app/views/todo/_show_items.rhtml +++ /dev/null @@ -1,98 +0,0 @@ -<% item = show_items %> -<% if !item.done? %> -
-<%= form_remote_tag( :url => url_for( :controller => "todo", :action => "toggle_check", :id => item.id ), - :html => { :id=> "checkbox-notdone-#{item.id}", :class => "inline-form" }, - :update => "completed", - :position => "top", - :loading => "Form.disable('checkbox-notdone-#{item.id}');", - :complete => "new Effect2.Fade('item-#{item.id}-container', true);" - ) %> - -
-
- <%= - link_to_remote( image_tag("blank", :title =>"Delete action", :class=>"delete_item"), - :update => "item-#{item.id}-container", - :loading => "new Effect2.Fade('item-#{item.id}-container', true)", - :url => { :controller => "todo", :action => "destroy_action", :id => item.id }, - :confirm => "Are you sure that you want to delete the action, \'#{item.description}\'?") + " " + - link_to_function(image_tag( "blank", :title => "Edit action", :class => "edit_item"), - "Element.toggle('item-#{item.id}','action-#{item.id}-edit-form'); new Effect.Appear('action-#{item.id}-edit-form'); Form.focusFirstElement('form-action-#{item.id}')" ) + " " - %> -
-
- -
- - <% if item.due %> -
- <% else %> - <%= staleness(item.created) %> - <% end %> - <%= due_date( item.due ) %> - <%= item.description %> - <% if item.project_id %> - <%= link_to( "[P]", { :controller => "project", :action => "show", :name => urlize(item.project.name) }, :title => "View project: #{item.project.name}" ) %> - <% end %> - <% if item.notes? %> - <%= "" + image_tag( "blank", :width=>"16", :height=>"16", :border=>"0" ) + "" %> - <% m_notes = markdown( item.notes ) %> - <%= "
" + m_notes + "
" %> - <% end %> -
-
-<%= end_form_tag %> - - - -
-<% else %> -
-<%= form_remote_tag( :url => url_for( :controller => "todo", :action => "toggle_check", :id => item.id ), - :html => { :id=> "checkbox-done-#{item.id}", :class => "inline-form" }, - :update => "new_actions", - :position => "bottom", - :loading => "Form.disable('checkbox-done-#{item.id}');", - :complete => "Element.toggle('new_actions');new Effect2.Fade('done-item-#{item.id}-container', true);" - ) %> - -
-
- <%= - link_to_remote( image_tag("blank", :title =>"Delete action", :class=>"delete_item"), - :update => "done-item-#{item.id}-container", - :loading => "new Effect2.Fade('done-item-#{item.id}-container', true)", - :url => { :controller => "todo", :action => "destroy_action", :id => item.id }, :confirm => "Are you sure that you want to delete the action \'#{item.description}\'?" ) + " " - %> - <%= image_tag("blank") %> -
- -
- -
- -
- <%= format_date( item.completed ) %> - <%= item.description %> - <% if item.project_id %> - <%= link_to( "[P]", { :controller => "project", :action => "show", :name => urlize(item.project.name) }, :title => "View project: #{item.project.name}" ) %> - <% end %> - <% if item.notes? %> - <%= "" + image_tag( "blank", :width=>"16", :height=>"16", :border=>"0" ) + "" %> - <% m_notes = markdown( item.notes ) %> - <%= "
" + m_notes + "
" %> - <% end %> -
-
-<%= end_form_tag %> -
-<% end %> -<% item = nil %> \ No newline at end of file diff --git a/tracks/app/views/todo/completed.rhtml b/tracks/app/views/todo/completed.rhtml index 9d8ba71c..908ed09a 100644 --- a/tracks/app/views/todo/completed.rhtml +++ b/tracks/app/views/todo/completed.rhtml @@ -1,24 +1,23 @@
-

You have completed <%= @done_today.length %> actions so far today.

Completed today

- <%= render_collection_of_partials "done", @done_today %> + <%= render :partial => "done", :collection => @done_today %>

Completed in last 7 days

- <%= render_collection_of_partials "done", @done_this_week %> + <%= render :partial => "done", :collection => @done_this_week %>
Completed in the last 31 days - <%= render_collection_of_partials "done", @done_this_month %> + <%= render :partial => "done", :collection => @done_this_month %>
diff --git a/tracks/app/views/todo/completed_archive.rhtml b/tracks/app/views/todo/completed_archive.rhtml index 15643012..a95d4369 100644 --- a/tracks/app/views/todo/completed_archive.rhtml +++ b/tracks/app/views/todo/completed_archive.rhtml @@ -4,7 +4,7 @@

Completed more than 31 days ago

- <%= render_collection_of_partials "done", @done_archive %> + <%= render :partial => "done", :collection => @done_archive %>
diff --git a/tracks/app/views/todo/list.rhtml b/tracks/app/views/todo/list.rhtml index b1835413..95a7b8bd 100644 --- a/tracks/app/views/todo/list.rhtml +++ b/tracks/app/views/todo/list.rhtml @@ -1,29 +1,23 @@
- +
- - <% for @shown_place in @shown_places -%> - <% @not_done = Todo.find_all("done=0 AND context_id=#{@shown_place.id}", "due IS NULL, due ASC, created ASC") -%> - <% if !@not_done.empty? -%> + <% + for @context in @contexts + next if @context.hidden? + @not_done = @context.find_not_done_todos + next if @not_done.empty? + -%>
-

<%= image_tag("collapse.png", :name=>"toggle_context_#{@shown_place.id}", :border=>"0") %> - <%= link_to( "#{@shown_place.name}", :controller => "context", :action => "show", :name => urlize(@shown_place.name) ) %>

+

<%= image_tag("collapse.png", :name=>"toggle_context_#{@context.id}", :border=>"0") %> + <%= link_to( "#{@context.name}", :controller => "context", :action => "show", :name => urlize(@context.name) ) %>

-
- <% if @not_done.empty? -%> -

There are no next actions yet in this context

- <% else -%> - <% for item in @not_done -%> - <%= render_partial "show_items", item %> - <% end -%> - <% end -%> +
+ <%= render :partial => "item", :collection => @not_done %>
- <% end -%> <% end -%>
@@ -31,85 +25,22 @@
<% if @done.empty? -%> -

There are no completed next actions yet in this context

+

There are no completed next actions

<% else -%> - <% for done_item in @done -%> - <%= render_partial "show_items", done_item %> - <% end -%> + <%= render :partial => "item", :collection => @done %> <% end -%>
+<%= render "shared/add_new_item_form" %> +<%= render "shared/sidebar" %> +
+ <% if @flash["confirmation"] -%>
<%= @flash["confirmation"] %>
<% end -%> <% if @flash["warning"] -%>
<%= @flash["warning"] %>
<% end -%> - -<%= link_to_function( "Add a next action »", "Element.toggle('todo_new_action');Element.toggle('new_actions');Form.focusFirstElement('todo-form-new-action');", {:title => "Add the next action [Alt+n]", :accesskey => "n"}) %> - - - -

Active Projects:

-
    - <% for project in Project.list_of -%> -
  • <%= link_to( project.name, { :controller => "project", :action => "show", - :name => urlize(project.name) } ) + " (" + project.count_undone_todos("actions") + ")" %>
  • - <% end -%> -
- -

Completed Projects:

-
    - <% for project in Project.list_of( isdone=1 ) -%> -
  • <%= link_to( project.name, { :controller => "project", :action => "show", - :name => urlize(project.name) } ) + " (" + project.count_undone_todos("actions") + ")" %>
  • - <% end -%> -
- -

Active Contexts:

-
    - <% for context in Context.list_of -%> -
  • <%= link_to( context.name, { :controller => "context", :action => "show", - :name => urlize(context.name) } ) + " (" + context.count_undone_todos("actions") + ")" %>
  • - <% end -%> -
- -

Hidden Contexts:

-
    - <% for context in Context.list_of( hidden=1 ) -%> -
  • <%= link_to( context.name, { :controller => "context", :action => "show", - :name => urlize(context.name) } ) + " (" + context.count_undone_todos("actions") + ")" %>
  • - <% end -%> -
-
diff --git a/tracks/config/environment.rb b/tracks/config/environment.rb index 6968003a..0064f751 100644 --- a/tracks/config/environment.rb +++ b/tracks/config/environment.rb @@ -65,4 +65,4 @@ Controllers = Dependencies::LoadingModule.root( # Include your app's configuration here: def app_configurations YAML::load(File.open("#{RAILS_ROOT}/config/settings.yml")) -end \ No newline at end of file +end diff --git a/tracks/config/environments/development.rb b/tracks/config/environments/development.rb index fb8baacd..8b8bcb79 100644 --- a/tracks/config/environments/development.rb +++ b/tracks/config/environments/development.rb @@ -1,7 +1,19 @@ +# 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. + +# Log error messages when you accidentally call methods on nil. +require 'active_support/whiny_nil' + +# Reload code; show full error reports; disable caching. Dependencies.mechanism = :load ActionController::Base.consider_all_requests_local = true ActionController::Base.perform_caching = false + +# The breakpoint server port that script/breakpointer connects to. BREAKPOINT_SERVER_PORT = 42531 -# Use Memory Store if you are using FCGI, otherwise use file store -ActionController::Base.fragment_cache_store = ActionController::Caching::Fragments::MemoryStore.new -#ActionController::Base.fragment_cache_store = ActionController::Caching::Fragments::FileStore.new("/path/to/cache/directory") \ No newline at end of file + +# Unique cookies +ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:session_key] = "TrackDev" +ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:tmpdir] = "#{RAILS_ROOT}/tmp" + diff --git a/tracks/config/environments/production.rb b/tracks/config/environments/production.rb index e97ed79f..4fc13b05 100644 --- a/tracks/config/environments/production.rb +++ b/tracks/config/environments/production.rb @@ -1,6 +1,8 @@ +# The production environment is meant for finished, "live" apps. +# Code is not reloaded between requests, full error reports are disabled, +# and caching is turned on. + +# Don't reload code; don't show full error reports; enable caching. Dependencies.mechanism = :require ActionController::Base.consider_all_requests_local = false ActionController::Base.perform_caching = true -# Use Memory Store if you are using FCGI, otherwise use file store -ActionController::Base.fragment_cache_store = ActionController::Caching::Fragments::MemoryStore.new -#ActionController::Base.fragment_cache_store = ActionController::Caching::Fragments::FileStore.new("/path/to/cache/directory") diff --git a/tracks/config/environments/test.rb b/tracks/config/environments/test.rb index 8aa4c65a..fea6fdff 100644 --- a/tracks/config/environments/test.rb +++ b/tracks/config/environments/test.rb @@ -1,4 +1,17 @@ +# 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! + +# Log error messages when you accidentally call methods on nil. +require 'active_support/whiny_nil' + +# Don't reload code; show full error reports; disable caching. Dependencies.mechanism = :require ActionController::Base.consider_all_requests_local = true ActionController::Base.perform_caching = false -ActionMailer::Base.delivery_method = :test \ No newline at end of file + +# Tell ActionMailer not to deliver emails to the real world. +# The :test delivery method accumulates sent emails in the +# ActionMailer::Base.deliveries array. +ActionMailer::Base.delivery_method = :test diff --git a/tracks/config/routes.rb b/tracks/config/routes.rb index 8a52cb7d..19e2c13d 100644 --- a/tracks/config/routes.rb +++ b/tracks/config/routes.rb @@ -36,19 +36,19 @@ ActionController::Routing::Routes.draw do |map| map.connect 'projects', :controller => 'project', :action => 'list' map.connect 'project/:name', :controller => 'project', :action => 'show' map.connect 'project/:id', :controller => 'project', :action => 'show' - + # Notes Routes - map.connect 'note/add_note', :controller => 'note', :action => 'add_note' - map.connect 'note/update_note/:id', :controller => 'note', :action => 'update_note', :id => 'id' + map.connect 'note/add', :controller => 'note', :action => 'add' + map.connect 'note/update/:id', :controller => 'note', :action => 'update', :id => 'id' map.connect 'note/:id', :controller => 'note', :action => 'show', :id => 'id' map.connect 'notes', :controller => 'note', :action => 'index' - + # Feed Routes map.connect 'feed/:action/:name/:user', :controller => 'feed' map.connect 'add_item', :controller => 'todo', :action => 'add_item' - + # Install the default route as the lowest priority. map.connect ':controller/:action' map.connect ':controller/:action/:id' diff --git a/tracks/config/settings.yml.tmpl b/tracks/config/settings.yml.tmpl index 48cbf839..664b2209 100644 --- a/tracks/config/settings.yml.tmpl +++ b/tracks/config/settings.yml.tmpl @@ -11,3 +11,4 @@ formats: staleness_starts: 7 admin: email: butshesagirl@rousette.org.uk + loginhash: change-me diff --git a/tracks/db/migrate/1_create_tracks_db.rb b/tracks/db/migrate/1_create_tracks_db.rb new file mode 100644 index 00000000..2ceaf1fd --- /dev/null +++ b/tracks/db/migrate/1_create_tracks_db.rb @@ -0,0 +1,41 @@ +# Verision 1.0.3 database +class CreateTracksDb < ActiveRecord::Migration + def self.up + create_table :contexts do |t| + t.column :name, :string, :null => false + t.column :position, :integer, :null => false + t.column :hide, :boolean, :default => 0 + end + + create_table :projects do |t| + t.column :name, :string, :null => false + t.column :position, :integer, :null => false + t.column :done, :boolean, :default => 0 + end + + create_table :todos do |t| + t.column :context_id, :integer, :null => false + t.column :project_id, :integer + t.column :description, :string, :null => false + t.column :notes, :text + t.column :done, :boolean, :default => 0, :null => false + t.column :created, :datetime, :default => '0000-00-00 00:00:00' + t.column :due, :date + t.column :completed, :datetime + end + + create_table :users do |t| + t.column :login, :string, :limit => 80, :null => false + t.column :password, :string, :limit => 40, :null => false + t.column :word, :string + t.column :is_admin, :boolean, :default => 0, :null => false + end + end + + def self.down + drop_table :contexts + drop_table :projects + drop_table :todos + drop_table :users + end +end diff --git a/tracks/db/migrate/2_add_user_id.rb b/tracks/db/migrate/2_add_user_id.rb new file mode 100644 index 00000000..e16b808f --- /dev/null +++ b/tracks/db/migrate/2_add_user_id.rb @@ -0,0 +1,16 @@ +class AddUserId < ActiveRecord::Migration + def self.up + add_column :contexts, :user_id, :integer, :null => false + add_column :projects, :user_id, :integer, :null => false + add_column :todos, :user_id, :integer, :null => false + execute "UPDATE `contexts` SET `user_id` = 1;" + execute "UPDATE `projects` SET `user_id` = 1;" + execute "UPDATE `todos` SET `user_id` = 1;" + end + + def self.down + remove_column :contexts, :user_id + remove_column :projects, :user_id + remove_column :todos, :user_id + end +end diff --git a/tracks/db/migrate/3_created_at.rb b/tracks/db/migrate/3_created_at.rb new file mode 100644 index 00000000..fb12b97f --- /dev/null +++ b/tracks/db/migrate/3_created_at.rb @@ -0,0 +1,9 @@ +class CreatedAt < ActiveRecord::Migration + def self.up + rename_column :todos, :created, :created_at + end + + def self.down + rename_column :todos, :created_at, :created + end +end diff --git a/tracks/db/migrate/4_notes.rb b/tracks/db/migrate/4_notes.rb new file mode 100644 index 00000000..cd5f86f2 --- /dev/null +++ b/tracks/db/migrate/4_notes.rb @@ -0,0 +1,15 @@ +class Notes < ActiveRecord::Migration + def self.up + create_table :notes do |t| + t.column :user_id, :integer, :null => false + t.column :project_id, :integer, :null => false + t.column :body, :text + t.column :created_at, :datetime, :default => '0000-00-00 00:00:00' + t.column :updated_at, :datetime, :default => '0000-00-00 00:00:00' + end + end + + def self.down + drop_table :notes + end +end diff --git a/tracks/doc/CHANGELOG b/tracks/doc/CHANGELOG index 7c38b49e..03488937 100644 --- a/tracks/doc/CHANGELOG +++ b/tracks/doc/CHANGELOG @@ -2,7 +2,7 @@ * Homepage: http://www.rousette.org.uk/projects/ * Author: bsag (http://www.rousette.org.uk/) -* Contributors: Nicholas Lee, Lolindrath, Jim Ray, Arnaud Limbourg +* Contributors: Nicholas Lee, Lolindrath, Jim Ray, Arnaud Limbourg, Timothy Martens * Version: 1.03 * Copyright: (cc) 2004-2005 rousette.org.uk * License: GNU GPL @@ -18,6 +18,11 @@ Wiki (deprecated - please use Trac): http://www.rousette.org.uk/projects/wiki/ 1. Tidied up the interface a bit, fixing mistakes in the wording. 2. The number of actions reported is now correctly pluralized depending on the number of actions (e.g. 1 action, 2 actions). 3. Added a Note model and notes database table. Notes have their own interface (/notes lists them all, and /note/1 shows note id 1), but they are displayed on the /project/show/[name] page. For now, notes belong to a particular project, and are added from the /project/show/[name] pages. You can delete and edit notes either from the /notes page or from the /note/[id] page. Notes use Textile/Markdown formatting. +4. Can now host multiple users on a single tracks install. User creation only possible by admin user. +5. Major improves to the render-flow, code reuse and MVC rationalisation. +6. Rails based db creation and migration via rake. +7. Added loginhash to settings.yml. +8. Modify signup to prevent is_admin being set by malicious user. Begin work on standardising layout for login controller. == Version 1.03 @@ -80,4 +85,4 @@ A small increment to fix a few bugs and typographical errors in the README: 8. You can now add a new item to a particular project when you are viewing it at /project/show/[id]. This makes it easy to add a block of next actions to a project. Edit and delete buttons work on this page, but redirect you to /todo/list. I need to fix this. 9. Added a login system by using the login_generator: http://wiki.rubyonrails.com/rails/show/LoginGenerator. The first time you access the system, you need to visit [tracks_url]/login/signup to enter a username and password. Then when you visit, you will be greeted with a login page. This stores a temporary cookie for your session. The whole app is protected by the login. 10. You can now add a new item to a particular context when you are viewing it at /context/show/[id]. This makes it easy to add a block of next actions to a project. Edit and delete buttons work on this page, but redirect you to /todo/list. I need to fix this. -11. Feeds! There's an RSS feed available at [tracks_url]/feed/na_feed which shows the last 15 next actions, and a text feed at [tracks_url]/feed/na_text which gives you a plain text list of all your next actions, sorted by context. You can use the latter to display your next actions on the desktop with GeekTool. Simply set up a shell command containing the following: curl http://[tracks_url]/feed/na_text. Obviously, you need to replace [tracks_url] with the actual URL to the Tracks application. \ No newline at end of file +11. Feeds! There's an RSS feed available at [tracks_url]/feed/na_feed which shows the last 15 next actions, and a text feed at [tracks_url]/feed/na_text which gives you a plain text list of all your next actions, sorted by context. You can use the latter to display your next actions on the desktop with GeekTool. Simply set up a shell command containing the following: curl http://[tracks_url]/feed/na_text. Obviously, you need to replace [tracks_url] with the actual URL to the Tracks application. diff --git a/tracks/public/404.html b/tracks/public/404.html index edbc89bf..0e184561 100644 --- a/tracks/public/404.html +++ b/tracks/public/404.html @@ -1,3 +1,5 @@ +

File not found

diff --git a/tracks/public/500.html b/tracks/public/500.html index ee0c919c..a1001a00 100644 --- a/tracks/public/500.html +++ b/tracks/public/500.html @@ -1,3 +1,5 @@ +

Application error (Apache)

diff --git a/tracks/public/dispatch.cgi b/tracks/public/dispatch.cgi index 9730473f..3848806d 100755 --- a/tracks/public/dispatch.cgi +++ b/tracks/public/dispatch.cgi @@ -1,4 +1,4 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby1.8 require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) diff --git a/tracks/public/dispatch.fcgi b/tracks/public/dispatch.fcgi index f0be2449..3169ba26 100755 --- a/tracks/public/dispatch.fcgi +++ b/tracks/public/dispatch.fcgi @@ -1,7 +1,24 @@ -#!/usr/bin/ruby - +#!/usr/bin/ruby1.8 +# +# You may specify the path to the FastCGI crash log (a log of unhandled +# exceptions which forced the FastCGI instance to exit, great for debugging) +# and the number of requests to process before running garbage collection. +# +# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log +# and the GC period is nil (turned off). A reasonable number of requests +# could range from 10-100 depending on the memory footprint of your app. +# +# Example: +# # Default log path, normal GC behavior. +# RailsFCGIHandler.process! +# +# # Default log path, 50 requests between GC. +# RailsFCGIHandler.process! nil, 50 +# +# # Custom log path, normal GC behavior. +# RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log' +# require File.dirname(__FILE__) + "/../config/environment" -require 'dispatcher' -require 'fcgi' +require 'fcgi_handler' -FCGI.each_cgi { |cgi| Dispatcher.dispatch(cgi) } \ No newline at end of file +RailsFCGIHandler.process! diff --git a/tracks/public/dispatch.rb b/tracks/public/dispatch.rb index 9730473f..3848806d 100755 --- a/tracks/public/dispatch.rb +++ b/tracks/public/dispatch.rb @@ -1,4 +1,4 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby1.8 require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT) diff --git a/tracks/public/javascripts/controls.js b/tracks/public/javascripts/controls.js new file mode 100644 index 00000000..cece0a91 --- /dev/null +++ b/tracks/public/javascripts/controls.js @@ -0,0 +1,446 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// +// 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. + +Element.collectTextNodesIgnoreClass = function(element, ignoreclass) { + var children = $(element).childNodes; + var text = ""; + var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i"); + + for (var i = 0; i < children.length; i++) { + if(children[i].nodeType==3) { + text+=children[i].nodeValue; + } else { + if((!children[i].className.match(classtest)) && children[i].hasChildNodes()) + text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass); + } + } + + return text; +} + +// 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.getEntry(), 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: new Array (',', '\n') } which +// enables autocompletion on multiple tokens. This is most +// useful when one of the tokens is \n (a newline), as it +// allows smart autocompletion after linebreaks. + +var Autocompleter = {} +Autocompleter.Base = function() {}; +Autocompleter.Base.prototype = { + base_initialize: function(element, update, options) { + this.element = $(element); + this.update = $(update); + this.has_focus = false; + this.changed = false; + this.active = false; + this.index = 0; + this.entry_count = 0; + + if (this.setOptions) + this.setOptions(options); + else + this.options = {} + + this.options.tokens = this.options.tokens || new Array(); + this.options.frequency = this.options.frequency || 0.4; + this.options.min_chars = this.options.min_chars || 1; + this.options.onShow = this.options.onShow || + function(element, update){ + if(!update.style.position || update.style.position=='absolute') { + update.style.position = 'absolute'; + var offsets = Position.cumulativeOffset(element); + update.style.left = offsets[0] + 'px'; + update.style.top = (offsets[1] + element.offsetHeight) + 'px'; + update.style.width = element.offsetWidth + 'px'; + } + new Effect.Appear(update,{duration:0.15}); + }; + this.options.onHide = this.options.onHide || + function(element, update){ new Effect.Fade(update,{duration:0.15}) }; + + if(this.options.indicator) + this.indicator = $(this.options.indicator); + + if (typeof(this.options.tokens) == 'string') + this.options.tokens = new Array(this.options.tokens); + + this.observer = null; + + Element.hide(this.update); + + Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); + Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); + }, + + show: function() { + if(this.update.style.display=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && this.update.style.position=='absolute') { + new Insertion.After(this.update, + ''); + this.iefix = $(this.update.id+'_iefix'); + } + if(this.iefix) { + Position.clone(this.update, this.iefix); + this.iefix.style.zIndex = 1; + this.update.style.zIndex = 2; + Element.show(this.iefix); + } + }, + + hide: function() { + if(this.update.style.display=='') this.options.onHide(this.element, this.update); + if(this.iefix) Element.hide(this.iefix); + }, + + startIndicator: function() { + if(this.indicator) Element.show(this.indicator); + }, + + stopIndicator: function() { + if(this.indicator) Element.hide(this.indicator); + }, + + onKeyPress: function(event) { + if(this.active) + switch(event.keyCode) { + case Event.KEY_TAB: + case Event.KEY_RETURN: + this.select_entry(); + Event.stop(event); + case Event.KEY_ESC: + this.hide(); + this.active = false; + return; + case Event.KEY_LEFT: + case Event.KEY_RIGHT: + return; + case Event.KEY_UP: + this.mark_previous(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + case Event.KEY_DOWN: + this.mark_next(); + this.render(); + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + return; + } + else + if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) + return; + + this.changed = true; + this.has_focus = true; + + if(this.observer) clearTimeout(this.observer); + this.observer = + setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); + }, + + onHover: function(event) { + var element = Event.findElement(event, 'LI'); + if(this.index != element.autocompleteIndex) + { + this.index = element.autocompleteIndex; + this.render(); + } + Event.stop(event); + }, + + onClick: function(event) { + var element = Event.findElement(event, 'LI'); + this.index = element.autocompleteIndex; + this.select_entry(); + Event.stop(event); + }, + + onBlur: function(event) { + // needed to make click events working + setTimeout(this.hide.bind(this), 250); + this.has_focus = false; + this.active = false; + }, + + render: function() { + if(this.entry_count > 0) { + for (var i = 0; i < this.entry_count; i++) + this.index==i ? + Element.addClassName(this.get_entry(i),"selected") : + Element.removeClassName(this.get_entry(i),"selected"); + + if(this.has_focus) { + if(this.get_current_entry().scrollIntoView) + this.get_current_entry().scrollIntoView(false); + + this.show(); + this.active = true; + } + } else this.hide(); + }, + + mark_previous: function() { + if(this.index > 0) this.index-- + else this.index = this.entry_count-1; + }, + + mark_next: function() { + if(this.index < this.entry_count-1) this.index++ + else this.index = 0; + }, + + get_entry: function(index) { + return this.update.firstChild.childNodes[index]; + }, + + get_current_entry: function() { + return this.get_entry(this.index); + }, + + select_entry: function() { + this.active = false; + value = Element.collectTextNodesIgnoreClass(this.get_current_entry(), 'informal').unescapeHTML(); + this.updateElement(value); + this.element.focus(); + }, + + updateElement: function(value) { + var last_token_pos = this.findLastToken(); + if (last_token_pos != -1) { + var new_value = this.element.value.substr(0, last_token_pos + 1); + var whitespace = this.element.value.substr(last_token_pos + 1).match(/^\s+/); + if (whitespace) + new_value += whitespace[0]; + this.element.value = new_value + value; + } else { + this.element.value = value; + } + }, + + updateChoices: function(choices) { + if(!this.changed && this.has_focus) { + this.update.innerHTML = choices; + Element.cleanWhitespace(this.update); + Element.cleanWhitespace(this.update.firstChild); + + if(this.update.firstChild && this.update.firstChild.childNodes) { + this.entry_count = + this.update.firstChild.childNodes.length; + for (var i = 0; i < this.entry_count; i++) { + entry = this.get_entry(i); + entry.autocompleteIndex = i; + this.addObservers(entry); + } + } else { + this.entry_count = 0; + } + + this.stopIndicator(); + + this.index = 0; + this.render(); + } + }, + + addObservers: function(element) { + Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); + Event.observe(element, "click", this.onClick.bindAsEventListener(this)); + }, + + onObserverEvent: function() { + this.changed = false; + if(this.getEntry().length>=this.options.min_chars) { + this.startIndicator(); + this.getUpdatedChoices(); + } else { + this.active = false; + this.hide(); + } + }, + + getEntry: function() { + var token_pos = this.findLastToken(); + if (token_pos != -1) + var ret = this.element.value.substr(token_pos + 1).replace(/^\s+/,'').replace(/\s+$/,''); + else + var ret = this.element.value; + + return /\n/.test(ret) ? '' : ret; + }, + + findLastToken: function() { + var last_token_pos = -1; + + for (var i=0; i last_token_pos) + last_token_pos = this_token_pos; + } + return last_token_pos; + } +} + +Ajax.Autocompleter = Class.create(); +Ajax.Autocompleter.prototype = Object.extend(new Autocompleter.Base(), +Object.extend(new Ajax.Base(), { + initialize: function(element, update, url, options) { + this.base_initialize(element, update, options); + this.options.asynchronous = true; + this.options.onComplete = this.onComplete.bind(this) + this.options.method = 'post'; + this.options.defaultParams = this.options.parameters || null; + this.url = url; + }, + + getUpdatedChoices: function() { + entry = encodeURIComponent(this.element.name) + '=' + + encodeURIComponent(this.getEntry()); + + 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 +// +// - partial_search - 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 full_search to true (default: off). +// +// - full_search - Search anywhere in autocomplete array strings. +// +// - partial_chars - How many characters to enter before triggering +// a partial match (unlike min_chars, which defines +// how many characters are required to do any match +// at all). Defaults to 2. +// +// - ignore_case - Whether to ignore case when autocompleting. +// Defaults to true. +// +// It's possible to pass in a custom function as the 'selector' +// option, if you prefer to write your own autocompletion logic. +// In that case, the other options above will not apply unless +// you support them. + +Autocompleter.Local = Class.create(); +Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { + initialize: function(element, update, array, options) { + this.base_initialize(element, update, options); + this.options.array = array; + }, + + getUpdatedChoices: function() { + this.updateChoices(this.options.selector(this)); + }, + + setOptions: function(options) { + this.options = Object.extend({ + choices: 10, + partial_search: true, + partial_chars: 2, + ignore_case: true, + full_search: false, + selector: function(instance) { + var ret = new Array(); // Beginning matches + var partial = new Array(); // Inside matches + var entry = instance.getEntry(); + 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 found_pos = instance.options.ignore_case ? + elem.toLowerCase().indexOf(entry.toLowerCase()) : + elem.indexOf(entry); + + while (found_pos != -1) { + if (found_pos == 0 && elem.length != entry.length) { + ret.push("
  • " + elem.substr(0, entry.length) + "" + + elem.substr(entry.length) + "
  • "); + break; + } else if (entry.length >= instance.options.partial_chars && + instance.options.partial_search && found_pos != -1) { + if (instance.options.full_search || /\s/.test(elem.substr(found_pos-1,1))) { + partial.push("
  • " + elem.substr(0, found_pos) + "" + + elem.substr(found_pos, entry.length) + "" + elem.substr( + found_pos + entry.length) + "
  • "); + break; + } + } + + found_pos = instance.options.ignore_case ? + elem.toLowerCase().indexOf(entry.toLowerCase(), found_pos + 1) : + elem.indexOf(entry, found_pos + 1); + + } + } + if (partial.length) + ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) + return "
      " + ret.join('') + "
    "; + } + }, options || {}); + } +}); diff --git a/tracks/public/javascripts/dragdrop.js b/tracks/public/javascripts/dragdrop.js new file mode 100644 index 00000000..c0fd1d1e --- /dev/null +++ b/tracks/public/javascripts/dragdrop.js @@ -0,0 +1,537 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// +// Element.Class part Copyright (c) 2005 by Rick Olson +// +// 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. + +Element.Class = { + // Element.toggleClass(element, className) toggles the class being on/off + // Element.toggleClass(element, className1, className2) toggles between both classes, + // defaulting to className1 if neither exist + toggle: function(element, className) { + if(Element.Class.has(element, className)) { + Element.Class.remove(element, className); + if(arguments.length == 3) Element.Class.add(element, arguments[2]); + } else { + Element.Class.add(element, className); + if(arguments.length == 3) Element.Class.remove(element, arguments[2]); + } + }, + + // gets space-delimited classnames of an element as an array + get: function(element) { + element = $(element); + return element.className.split(' '); + }, + + // functions adapted from original functions by Gavin Kistner + remove: function(element) { + element = $(element); + var regEx; + for(var i = 1; i < arguments.length; i++) { + regEx = new RegExp("^" + arguments[i] + "\\b\\s*|\\s*\\b" + arguments[i] + "\\b", 'g'); + element.className = element.className.replace(regEx, '') + } + }, + + add: function(element) { + element = $(element); + for(var i = 1; i < arguments.length; i++) { + Element.Class.remove(element, arguments[i]); + element.className += (element.className.length > 0 ? ' ' : '') + arguments[i]; + } + }, + + // returns true if all given classes exist in said element + has: function(element) { + element = $(element); + if(!element || !element.className) return false; + var regEx; + for(var i = 1; i < arguments.length; i++) { + regEx = new RegExp("\\b" + arguments[i] + "\\b"); + if(!regEx.test(element.className)) return false; + } + return true; + }, + + // expects arrays of strings and/or strings as optional paramters + // Element.Class.has_any(element, ['classA','classB','classC'], 'classD') + has_any: function(element) { + element = $(element); + if(!element || !element.className) return false; + var regEx; + for(var i = 1; i < arguments.length; i++) { + if((typeof arguments[i] == 'object') && + (arguments[i].constructor == Array)) { + for(var j = 0; j < arguments[i].length; j++) { + regEx = new RegExp("\\b" + arguments[i][j] + "\\b"); + if(regEx.test(element.className)) return true; + } + } else { + regEx = new RegExp("\\b" + arguments[i] + "\\b"); + if(regEx.test(element.className)) return true; + } + } + return false; + }, + + childrenWith: function(element, className) { + var children = $(element).getElementsByTagName('*'); + var elements = new Array(); + + for (var i = 0; i < children.length; i++) { + if (Element.Class.has(children[i], className)) { + elements.push(children[i]); + break; + } + } + + return elements; + } +} + +/*--------------------------------------------------------------------------*/ + +var Droppables = { + drops: false, + + remove: function(element) { + for(var i = 0; i < this.drops.length; i++) + if(this.drops[i].element == element) + this.drops.splice(i,1); + }, + + add: function(element) { + var element = $(element); + var options = Object.extend({ + greedy: true, + hoverclass: null + }, arguments[1] || {}); + + // cache containers + if(options.containment) { + options._containers = new Array(); + var containment = options.containment; + if((typeof containment == 'object') && + (containment.constructor == Array)) { + for(var i=0; i0) window.scrollBy(0,0); + + Event.stop(event); + } + } +} + +/*--------------------------------------------------------------------------*/ + +SortableObserver = Class.create(); +SortableObserver.prototype = { + initialize: function(element, observer) { + this.element = $(element); + this.observer = observer; + this.lastValue = Sortable.serialize(this.element); + }, + onStart: function() { + this.lastValue = Sortable.serialize(this.element); + }, + onEnd: function() { + if(this.lastValue != Sortable.serialize(this.element)) + this.observer(this.element) + } +} + +Sortable = { + sortables: new Array(), + options: function(element){ + var element = $(element); + for(var i=0;i0.5) { + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode && oldParentNode.sortable) + oldParentNode.sortable.onChange(element); + if(dropon.parentNode.sortable) + dropon.parentNode.sortable.onChange(element); + } + } else { + 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 && oldParentNode.sortable) + oldParentNode.sortable.onChange(element); + if(dropon.parentNode.sortable) + dropon.parentNode.sortable.onChange(element); + } + } + } + } + + // fix for gecko engine + Element.cleanWhitespace(element); + + options.draggables = []; + options.droppables = []; + + // make it so + var elements = element.childNodes; + for (var i = 0; i < elements.length; i++) + if(elements[i].tagName && elements[i].tagName==options.tag.toUpperCase() && + (!options.only || (Element.Class.has(elements[i], options.only)))) { + + // handles are per-draggable + var handle = options.handle ? + Element.Class.childrenWith(elements[i], options.handle)[0] : elements[i]; + + options.draggables.push(new Draggable(elements[i], Object.extend(options_for_draggable, { handle: handle }))); + + Droppables.add(elements[i], options_for_droppable); + options.droppables.push(elements[i]); + + } + + // keep reference + this.sortables.push(options); + + // for onupdate + Draggables.addObserver(new SortableObserver(element, options.onUpdate)); + + }, + serialize: function(element) { + var element = $(element); + var sortableOptions = this.options(element); + var options = Object.extend({ + tag: sortableOptions.tag, + only: sortableOptions.only, + name: element.id + }, arguments[1] || {}); + + var items = $(element).childNodes; + var queryComponents = new Array(); + + for(var i=0; i= this.finishOn) { + this.render(this.options.to); + if(this.finish) this.finish(); + if(this.options.afterFinish) this.options.afterFinish(this); + return; + } + var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); + var frame = Math.round(pos * this.options.fps * this.options.duration); + if(frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } + this.timeout = setTimeout(this.loop.bind(this), 10); + }, + render: function(pos) { + if(this.options.transition) pos = this.options.transition(pos); + pos *= (this.options.to-this.options.from); + pos += this.options.from; + if(this.options.beforeUpdate) this.options.beforeUpdate(this); + if(this.update) this.update(pos); + if(this.options.afterUpdate) this.options.afterUpdate(this); + }, + cancel: function() { + if(this.timeout) clearTimeout(this.timeout); + } +} + +Effect.Parallel = Class.create(); +Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { + initialize: function(effects) { + this.effects = effects || []; + this.start(arguments[1]); + }, + update: function(position) { + for (var i = 0; i < this.effects.length; i++) + this.effects[i].render(position); + }, + finish: function(position) { + for (var i = 0; i < this.effects.length; i++) + if(this.effects[i].finish) this.effects[i].finish(position); + } +}); + +// Internet Explorer caveat: works only on elements the have +// a 'layout', meaning having a given width or height. +// There is no way to safely set this automatically. +Effect.Opacity = Class.create(); +Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + options = Object.extend({ + from: 0.0, + to: 1.0 + }, arguments[1] || {}); + this.start(options); + }, + update: function(position) { + this.setOpacity(position); + }, + setOpacity: function(opacity) { + opacity = (opacity == 1) ? 0.99999 : opacity; + this.element.style.opacity = opacity; + this.element.style.filter = "alpha(opacity:"+opacity*100+")"; + } +}); + +Effect.MoveBy = Class.create(); +Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), { + initialize: function(element, toTop, toLeft) { + this.element = $(element); + this.originalTop = parseFloat(this.element.style.top || '0'); + this.originalLeft = parseFloat(this.element.style.left || '0'); + this.toTop = toTop; + this.toLeft = toLeft; + Element.makePositioned(this.element); + this.start(arguments[3]); + }, + update: function(position) { + topd = this.toTop * position + this.originalTop; + leftd = this.toLeft * position + this.originalLeft; + this.setPosition(topd, leftd); + }, + setPosition: function(topd, leftd) { + this.element.style.top = topd + "px"; + this.element.style.left = leftd + "px"; + } +}); + +Effect.Scale = Class.create(); +Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { + initialize: function(element, percent) { + this.element = $(element) + options = Object.extend({ + scaleX: true, + scaleY: true, + scaleContent: true, + scaleFromCenter: false, + scaleMode: 'box', // 'box' or 'contents' or {} with provided values + scaleFrom: 100.0 + }, arguments[2] || {}); + this.originalTop = this.element.offsetTop; + this.originalLeft = this.element.offsetLeft; + if(this.element.style.fontSize=="") this.sizeEm = 1.0; + if(this.element.style.fontSize && this.element.style.fontSize.indexOf("em")>0) + this.sizeEm = parseFloat(this.element.style.fontSize); + this.factor = (percent/100.0) - (options.scaleFrom/100.0); + if(options.scaleMode=='box') { + this.originalHeight = this.element.clientHeight; + this.originalWidth = this.element.clientWidth; + } else + if(options.scaleMode=='contents') { + this.originalHeight = this.element.scrollHeight; + this.originalWidth = this.element.scrollWidth; + } else { + this.originalHeight = options.scaleMode.originalHeight; + this.originalWidth = options.scaleMode.originalWidth; + } + this.start(options); + }, + + update: function(position) { + currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if(this.options.scaleContent && this.sizeEm) + this.element.style.fontSize = this.sizeEm*currentScale + "em"; + this.setDimensions( + this.originalWidth * currentScale, + this.originalHeight * currentScale); + }, + + setDimensions: function(width, height) { + if(this.options.scaleX) this.element.style.width = width + 'px'; + if(this.options.scaleY) this.element.style.height = height + 'px'; + if(this.options.scaleFromCenter) { + topd = (height - this.originalHeight)/2; + leftd = (width - this.originalWidth)/2; + if(this.element.style.position=='absolute') { + if(this.options.scaleY) this.element.style.top = this.originalTop-topd + "px"; + if(this.options.scaleX) this.element.style.left = this.originalLeft-leftd + "px"; + } else { + if(this.options.scaleY) this.element.style.top = -topd + "px"; + if(this.options.scaleX) this.element.style.left = -leftd + "px"; + } + } + } +}); + +Effect.Highlight = Class.create(); +Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + + // try to parse current background color as default for endcolor + // browser stores this as: "rgb(255, 255, 255)", convert to "#ffffff" format + var endcolor = "#ffffff"; + var current = this.element.style.backgroundColor; + if(current && current.slice(0,4) == "rgb(") { + endcolor = "#"; + var cols = current.slice(4,current.length-1).split(','); + var i=0; do { endcolor += parseInt(cols[i]).toColorPart() } while (++i<3); } + + var options = Object.extend({ + startcolor: "#ffff99", + endcolor: endcolor, + restorecolor: current + }, arguments[1] || {}); + + // init color calculations + this.colors_base = [ + parseInt(options.startcolor.slice(1,3),16), + parseInt(options.startcolor.slice(3,5),16), + parseInt(options.startcolor.slice(5),16) ]; + this.colors_delta = [ + parseInt(options.endcolor.slice(1,3),16)-this.colors_base[0], + parseInt(options.endcolor.slice(3,5),16)-this.colors_base[1], + parseInt(options.endcolor.slice(5),16)-this.colors_base[2] ]; + + this.start(options); + }, + update: function(position) { + var colors = [ + Math.round(this.colors_base[0]+(this.colors_delta[0]*position)), + Math.round(this.colors_base[1]+(this.colors_delta[1]*position)), + Math.round(this.colors_base[2]+(this.colors_delta[2]*position)) ]; + this.element.style.backgroundColor = "#" + + colors[0].toColorPart() + colors[1].toColorPart() + colors[2].toColorPart(); + }, + finish: function() { + this.element.style.backgroundColor = this.options.restorecolor; + } +}); + +Effect.ScrollTo = Class.create(); +Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { + initialize: function(element) { + this.element = $(element); + Position.prepare(); + var offsets = Position.cumulativeOffset(this.element); + var max = window.innerHeight ? + window.height - window.innerHeight : + document.body.scrollHeight - + (document.documentElement.clientHeight ? + document.documentElement.clientHeight : document.body.clientHeight); + this.scrollStart = Position.deltaY; + this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; + this.start(arguments[1] || {}); + }, + update: function(position) { + Position.prepare(); + window.scrollTo(Position.deltaX, + this.scrollStart + (position*this.delta)); + } +}); + +/* ------------- prepackaged effects ------------- */ + +Effect.Fade = function(element) { + options = Object.extend({ + from: 1.0, + to: 0.0, + afterFinish: function(effect) + { Element.hide(effect.element); + effect.setOpacity(1); } + }, arguments[1] || {}); + new Effect.Opacity(element,options); +} + +Effect.Appear = function(element) { + options = Object.extend({ + from: 0.0, + to: 1.0, + beforeStart: function(effect) + { effect.setOpacity(0); + Element.show(effect.element); }, + afterUpdate: function(effect) + { Element.show(effect.element); } + }, arguments[1] || {}); + new Effect.Opacity(element,options); +} + +Effect.Puff = function(element) { + new Effect.Parallel( + [ new Effect.Scale(element, 200, { sync: true, scaleFromCenter: true }), + new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0 } ) ], + { duration: 1.0, + afterUpdate: function(effect) + { effect.effects[0].element.style.position = 'absolute'; }, + afterFinish: function(effect) + { Element.hide(effect.effects[0].element); } + } + ); +} + +Effect.BlindUp = function(element) { + Element.makeClipping(element); + new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + afterFinish: function(effect) + { + Element.hide(effect.element); + Element.undoClipping(effect.element); + } + }, arguments[1] || {}) + ); +} + +Effect.BlindDown = function(element) { + $(element).style.height = '0px'; + Element.makeClipping(element); + Element.show(element); + new Effect.Scale(element, 100, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'contents', + scaleFrom: 0, + afterFinish: function(effect) { + Element.undoClipping(effect.element); + } + }, arguments[1] || {}) + ); +} + +Effect.SwitchOff = function(element) { + new Effect.Appear(element, + { duration: 0.4, + transition: Effect.Transitions.flicker, + afterFinish: function(effect) + { effect.element.style.overflow = 'hidden'; + new Effect.Scale(effect.element, 1, + { duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, + afterUpdate: function(effect) { + if(effect.element.style.position=="") + effect.element.style.position = 'relative'; }, + afterFinish: function(effect) { Element.hide(effect.element); } + } ) + } + } ); +} + +Effect.DropOut = function(element) { + new Effect.Parallel( + [ new Effect.MoveBy(element, 100, 0, { sync: true }), + new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0 } ) ], + { duration: 0.5, + afterFinish: function(effect) + { Element.hide(effect.effects[0].element); } + }); +} + +Effect.Shake = function(element) { + new Effect.MoveBy(element, 0, 20, + { duration: 0.05, afterFinish: function(effect) { + new Effect.MoveBy(effect.element, 0, -40, + { duration: 0.1, afterFinish: function(effect) { + new Effect.MoveBy(effect.element, 0, 40, + { duration: 0.1, afterFinish: function(effect) { + new Effect.MoveBy(effect.element, 0, -40, + { duration: 0.1, afterFinish: function(effect) { + new Effect.MoveBy(effect.element, 0, 40, + { duration: 0.1, afterFinish: function(effect) { + new Effect.MoveBy(effect.element, 0, -20, + { duration: 0.05, afterFinish: function(effect) { + }}) }}) }}) }}) }}) }}); +} + +Effect.SlideDown = function(element) { + element = $(element); + element.style.height = '0px'; + Element.makeClipping(element); + Element.cleanWhitespace(element); + Element.makePositioned(element.firstChild); + Element.show(element); + new Effect.Scale(element, 100, + Object.extend({ scaleContent: false, + scaleX: false, + scaleMode: 'contents', + scaleFrom: 0, + afterUpdate: function(effect) + { effect.element.firstChild.style.bottom = + (effect.originalHeight - effect.element.clientHeight) + 'px'; }, + afterFinish: function(effect) + { Element.undoClipping(effect.element); } + }, arguments[1] || {}) + ); +} + +Effect.SlideUp = function(element) { + element = $(element); + Element.makeClipping(element); + Element.cleanWhitespace(element); + Element.makePositioned(element.firstChild); + Element.show(element); + new Effect.Scale(element, 0, + Object.extend({ scaleContent: false, + scaleX: false, + afterUpdate: function(effect) + { effect.element.firstChild.style.bottom = + (effect.originalHeight - effect.element.clientHeight) + 'px'; }, + afterFinish: function(effect) + { + Element.hide(effect.element); + Element.undoClipping(effect.element); + } + }, arguments[1] || {}) + ); +} + +Effect.Squish = function(element) { + new Effect.Scale(element, 0, + { afterFinish: function(effect) { Element.hide(effect.element); } }); +} + +Effect.Grow = function(element) { + element = $(element); + var options = arguments[1] || {}; + + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + element.style.overflow = 'hidden'; + Element.show(element); + + var direction = options.direction || 'center'; + var moveTransition = options.moveTransition || Effect.Transitions.sinoidal; + var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal; + var opacityTransition = options.opacityTransition || Effect.Transitions.full; + + var initialMoveX, initialMoveY; + var moveX, moveY; + + switch (direction) { + case 'top-left': + initialMoveX = initialMoveY = moveX = moveY = 0; + break; + case 'top-right': + initialMoveX = originalWidth; + initialMoveY = moveY = 0; + moveX = -originalWidth; + break; + case 'bottom-left': + initialMoveX = moveX = 0; + initialMoveY = originalHeight; + moveY = -originalHeight; + break; + case 'bottom-right': + initialMoveX = originalWidth; + initialMoveY = originalHeight; + moveX = -originalWidth; + moveY = -originalHeight; + break; + case 'center': + initialMoveX = originalWidth / 2; + initialMoveY = originalHeight / 2; + moveX = -originalWidth / 2; + moveY = -originalHeight / 2; + break; + } + + new Effect.MoveBy(element, initialMoveY, initialMoveX, { + duration: 0.01, + beforeUpdate: function(effect) { $(element).style.height = '0px'; }, + afterFinish: function(effect) { + new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }), + new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition }), + new Effect.Scale(element, 100, { + scaleMode: { originalHeight: originalHeight, originalWidth: originalWidth }, + sync: true, scaleFrom: 0, scaleTo: 100, transition: scaleTransition })], + options); } + }); +} + +Effect.Shrink = function(element) { + element = $(element); + var options = arguments[1] || {}; + + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + element.style.overflow = 'hidden'; + Element.show(element); + + var direction = options.direction || 'center'; + var moveTransition = options.moveTransition || Effect.Transitions.sinoidal; + var scaleTransition = options.scaleTransition || Effect.Transitions.sinoidal; + var opacityTransition = options.opacityTransition || Effect.Transitions.none; + + var moveX, moveY; + + switch (direction) { + case 'top-left': + moveX = moveY = 0; + break; + case 'top-right': + moveX = originalWidth; + moveY = 0; + break; + case 'bottom-left': + moveX = 0; + moveY = originalHeight; + break; + case 'bottom-right': + moveX = originalWidth; + moveY = originalHeight; + break; + case 'center': + moveX = originalWidth / 2; + moveY = originalHeight / 2; + break; + } + + new Effect.Parallel( + [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: opacityTransition }), + new Effect.Scale(element, 0, { sync: true, transition: moveTransition }), + new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: scaleTransition }) ], + options); +} + +Effect.Pulsate = function(element) { + var options = arguments[1] || {}; + var transition = options.transition || Effect.Transitions.sinoidal; + var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) }; + reverser.bind(transition); + new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 3.0, + afterFinish: function(effect) { Element.show(effect.element); } + }, options), {transition: reverser})); +} + +Effect.Fold = function(element) { + $(element).style.overflow = 'hidden'; + new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleTo: 100, + scaleX: false, + afterFinish: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleTo: 0, + scaleY: false, + afterFinish: function(effect) { Element.hide(effect.element) } }); + }}, arguments[1] || {})); +} + +// old: new Effect.ContentZoom(element, percent) +// new: Element.setContentZoom(element, percent) + +Element.setContentZoom = function(element, percent) { + var element = $(element); + element.style.fontSize = (percent/100) + "em"; + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); +} diff --git a/tracks/public/javascripts/prototype.js b/tracks/public/javascripts/prototype.js index 6e6dc6fc..37635ccf 100644 --- a/tracks/public/javascripts/prototype.js +++ b/tracks/public/javascripts/prototype.js @@ -1,4 +1,4 @@ -/* Prototype: an object-oriented Javascript library, version 1.2.1 +/* Prototype JavaScript framework, version 1.3.1 * (c) 2005 Sam Stephenson * * THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff @@ -11,7 +11,8 @@ /*--------------------------------------------------------------------------*/ var Prototype = { - Version: '1.2.1' + Version: '1.3.1', + emptyFunction: function() {} } var Class = { @@ -24,24 +25,28 @@ var Class = { var Abstract = new Object(); -Object.prototype.extend = function(object) { - for (property in object) { - this[property] = object[property]; +Object.extend = function(destination, source) { + for (property in source) { + destination[property] = source[property]; } - return this; + return destination; +} + +Object.prototype.extend = function(object) { + return Object.extend.apply(this, [this, object]); } Function.prototype.bind = function(object) { - var method = this; + var __method = this; return function() { - method.apply(object, arguments); + __method.apply(object, arguments); } } Function.prototype.bindAsEventListener = function(object) { - var method = this; + var __method = this; return function(event) { - method.call(object, event || window.event); + __method.call(object, event || window.event); } } @@ -54,7 +59,7 @@ Number.prototype.toColorPart = function() { var Try = { these: function() { var returnValue; - + for (var i = 0; i < arguments.length; i++) { var lambda = arguments[i]; try { @@ -62,7 +67,7 @@ var Try = { break; } catch (e) {} } - + return returnValue; } } @@ -75,14 +80,14 @@ PeriodicalExecuter.prototype = { this.callback = callback; this.frequency = frequency; this.currentlyExecuting = false; - + this.registerCallback(); }, - + registerCallback: function() { - setTimeout(this.onTimerEvent.bind(this), this.frequency * 1000); + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); }, - + onTimerEvent: function() { if (!this.currentlyExecuting) { try { @@ -92,8 +97,6 @@ PeriodicalExecuter.prototype = { this.currentlyExecuting = false; } } - - this.registerCallback(); } } @@ -101,7 +104,7 @@ PeriodicalExecuter.prototype = { function $() { var elements = new Array(); - + for (var i = 0; i < arguments.length; i++) { var element = arguments[i]; if (typeof element == 'string') @@ -109,21 +112,19 @@ function $() { if (arguments.length == 1) return element; - + elements.push(element); } - + return elements; } -/*--------------------------------------------------------------------------*/ - if (!Array.prototype.push) { Array.prototype.push = function() { - var startLength = this.length; - for (var i = 0; i < arguments.length; i++) + var startLength = this.length; + for (var i = 0; i < arguments.length; i++) this[startLength + i] = arguments[i]; - return this.length; + return this.length; } } @@ -135,10 +136,10 @@ if (!Function.prototype.apply) { if (!parameters) parameters = new Array(); for (var i = 0; i < parameters.length; i++) - parameterStrings[i] = 'x[' + i + ']'; + parameterStrings[i] = 'parameters[' + i + ']'; object.__apply__ = this; - var result = eval('obj.__apply__(' + + var result = eval('object.__apply__(' + parameterStrings[i].join(', ') + ')'); object.__apply__ = null; @@ -146,7 +147,24 @@ if (!Function.prototype.apply) { } } -/*--------------------------------------------------------------------------*/ +String.prototype.extend({ + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + escapeHTML: function() { + var div = document.createElement('div'); + var text = document.createTextNode(this); + div.appendChild(text); + return div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0].nodeValue; + } +}); var Ajax = { getTransport: function() { @@ -155,9 +173,7 @@ var Ajax = { function() {return new ActiveXObject('Microsoft.XMLHTTP')}, function() {return new XMLHttpRequest()} ) || false; - }, - - emptyFunction: function() {} + } } Ajax.Base = function() {}; @@ -168,6 +184,16 @@ Ajax.Base.prototype = { asynchronous: true, parameters: '' }.extend(options || {}); + }, + + responseIsSuccess: function() { + return this.transport.status == undefined + || this.transport.status == 0 + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + responseIsFailure: function() { + return !this.responseIsSuccess(); } } @@ -179,260 +205,178 @@ Ajax.Request.prototype = (new Ajax.Base()).extend({ initialize: function(url, options) { this.transport = Ajax.getTransport(); this.setOptions(options); - + this.request(url); + }, + + request: function(url) { + var parameters = this.options.parameters || ''; + if (parameters.length > 0) parameters += '&_='; + try { if (this.options.method == 'get') - url += '?' + this.options.parameters + '&_='; - + url += '?' + parameters; + this.transport.open(this.options.method, url, this.options.asynchronous); - + if (this.options.asynchronous) { this.transport.onreadystatechange = this.onStateChange.bind(this); setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); } - - this.transport.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - this.transport.setRequestHeader('X-Prototype-Version', Prototype.Version); - if (this.options.method == 'post') { - this.transport.setRequestHeader('Connection', 'close'); - this.transport.setRequestHeader('Content-type', - 'application/x-www-form-urlencoded'); - } - - this.transport.send(this.options.method == 'post' ? - this.options.parameters + '&_=' : null); - + this.setRequestHeaders(); + + var body = this.options.postBody ? this.options.postBody : parameters; + this.transport.send(this.options.method == 'post' ? body : null); + } catch (e) { - } + } }, - + + setRequestHeaders: function() { + var requestHeaders = + ['X-Requested-With', 'XMLHttpRequest', + 'X-Prototype-Version', Prototype.Version]; + + if (this.options.method == 'post') { + requestHeaders.push('Content-type', + 'application/x-www-form-urlencoded'); + + /* Force "Connection: close" for Mozilla browsers to work around + * a bug where XMLHttpReqeuest sends an incorrect Content-length + * header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType) + requestHeaders.push('Connection', 'close'); + } + + if (this.options.requestHeaders) + requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + + for (var i = 0; i < requestHeaders.length; i += 2) + this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); + }, + onStateChange: function() { var readyState = this.transport.readyState; if (readyState != 1) this.respondToReadyState(this.transport.readyState); }, - + respondToReadyState: function(readyState) { var event = Ajax.Request.Events[readyState]; - (this.options['on' + event] || Ajax.emptyFunction)(this.transport); + + if (event == 'Complete') + (this.options['on' + this.transport.status] + || this.options['on' + this.responseIsSuccess() ? 'Success' : 'Failure'] + || Prototype.emptyFunction)(this.transport); + + (this.options['on' + event] || Prototype.emptyFunction)(this.transport); + + /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ + if (event == 'Complete') + this.transport.onreadystatechange = Prototype.emptyFunction; } }); Ajax.Updater = Class.create(); -Ajax.Updater.prototype = (new Ajax.Base()).extend({ +Ajax.Updater.ScriptFragment = '(?:)((\n|.)*?)(?:<\/script>)'; + +Ajax.Updater.prototype.extend(Ajax.Request.prototype).extend({ initialize: function(container, url, options) { - this.container = $(container); + this.containers = { + success: container.success ? $(container.success) : $(container), + failure: container.failure ? $(container.failure) : + (container.success ? null : $(container)) + } + + this.transport = Ajax.getTransport(); this.setOptions(options); - - if (this.options.asynchronous) { - this.onComplete = this.options.onComplete; - this.options.onComplete = this.updateContent.bind(this); - } - - this.request = new Ajax.Request(url, this.options); - - if (!this.options.asynchronous) + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function() { this.updateContent(); + onComplete(this.transport); + }).bind(this); + + this.request(url); }, - + updateContent: function() { - if (this.options.insertion) { - new this.options.insertion(this.container, - this.request.transport.responseText); - } else { - this.container.innerHTML = this.request.transport.responseText; - } + var receiver = this.responseIsSuccess() ? + this.containers.success : this.containers.failure; - if (this.onComplete) { - setTimeout((function() {this.onComplete(this.request)}).bind(this), 10); - } - } -}); + var match = new RegExp(Ajax.Updater.ScriptFragment, 'img'); + var response = this.transport.responseText.replace(match, ''); + var scripts = this.transport.responseText.match(match); -/*--------------------------------------------------------------------------*/ - -var Field = { - clear: function() { - for (var i = 0; i < arguments.length; i++) - $(arguments[i]).value = ''; - }, - - focus: function(element) { - $(element).focus(); - }, - - present: function() { - for (var i = 0; i < arguments.length; i++) - if ($(arguments[i]).value == '') return false; - return true; - }, - - select: function(element) { - $(element).select(); - }, - - activate: function(element) { - $(element).focus(); - $(element).select(); - } -} - -/*--------------------------------------------------------------------------*/ - -var Form = { - serialize: function(form) { - var elements = Form.getElements($(form)); - var queryComponents = new Array(); - - for (var i = 0; i < elements.length; i++) { - var queryComponent = Form.Element.serialize(elements[i]); - if (queryComponent) - queryComponents.push(queryComponent); - } - - return queryComponents.join('&'); - }, - - getElements: function(form) { - form = $(form); - var elements = new Array(); - - for (tagName in Form.Element.Serializers) { - var tagElements = form.getElementsByTagName(tagName); - for (var j = 0; j < tagElements.length; j++) - elements.push(tagElements[j]); - } - return elements; - }, - - disable: function(form) { - var elements = Form.getElements(form); - for (var i = 0; i < elements.length; i++) { - var element = elements[i]; - element.blur(); - element.disable = 'true'; - } - }, - - focusFirstElement: function(form) { - form = $(form); - var elements = Form.getElements(form); - for (var i = 0; i < elements.length; i++) { - var element = elements[i]; - if (element.type != 'hidden' && !element.disabled) { - Field.activate(element); - break; + if (receiver) { + if (this.options.insertion) { + new this.options.insertion(receiver, response); + } else { + receiver.innerHTML = response; } } - }, - reset: function(form) { - $(form).reset(); - } -} - -Form.Element = { - serialize: function(element) { - element = $(element); - var method = element.tagName.toLowerCase(); - var parameter = Form.Element.Serializers[method](element); - - if (parameter) - return encodeURIComponent(parameter[0]) + '=' + - encodeURIComponent(parameter[1]); - }, - - getValue: function(element) { - element = $(element); - var method = element.tagName.toLowerCase(); - var parameter = Form.Element.Serializers[method](element); - - if (parameter) - return parameter[1]; - } -} - -Form.Element.Serializers = { - input: function(element) { - switch (element.type.toLowerCase()) { - case 'hidden': - case 'password': - case 'text': - return Form.Element.Serializers.textarea(element); - case 'checkbox': - case 'radio': - return Form.Element.Serializers.inputSelector(element); + if (this.responseIsSuccess()) { + if (this.onComplete) + setTimeout((function() {this.onComplete( + this.transport)}).bind(this), 10); } - return false; - }, - inputSelector: function(element) { - if (element.checked) - return [element.name, element.value]; - }, - - textarea: function(element) { - return [element.name, element.value]; - }, - - select: function(element) { - var index = element.selectedIndex; - var value = element.options[index].value || element.options[index].text; - return [element.name, (index >= 0) ? value : '']; + if (this.options.evalScripts && scripts) { + match = new RegExp(Ajax.Updater.ScriptFragment, 'im'); + setTimeout((function() { + for (var i = 0; i < scripts.length; i++) + eval(scripts[i].match(match)[1]); + }).bind(this), 10); + } } -} +}); -/*--------------------------------------------------------------------------*/ +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = (new Ajax.Base()).extend({ + initialize: function(container, url, options) { + this.setOptions(options); + this.onComplete = this.options.onComplete; -var $F = Form.Element.getValue; + this.frequency = (this.options.frequency || 2); + this.decay = 1; -/*--------------------------------------------------------------------------*/ + this.updater = {}; + this.container = container; + this.url = url; -Abstract.TimedObserver = function() {} -Abstract.TimedObserver.prototype = { - initialize: function(element, frequency, callback) { - this.frequency = frequency; - this.element = $(element); - this.callback = callback; - - this.lastValue = this.getValue(); - this.registerCallback(); + this.start(); }, - - registerCallback: function() { - setTimeout(this.onTimerEvent.bind(this), this.frequency * 1000); + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); }, - + + stop: function() { + this.updater.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Ajax.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + onTimerEvent: function() { - var value = this.getValue(); - if (this.lastValue != value) { - this.callback(this.element, value); - this.lastValue = value; - } - - this.registerCallback(); - } -} - -Form.Element.Observer = Class.create(); -Form.Element.Observer.prototype = (new Abstract.TimedObserver()).extend({ - getValue: function() { - return Form.Element.getValue(this.element); + this.updater = new Ajax.Updater(this.container, this.url, this.options); } }); -Form.Observer = Class.create(); -Form.Observer.prototype = (new Abstract.TimedObserver()).extend({ - getValue: function() { - return Form.serialize(this.element); - } -}); - - -/*--------------------------------------------------------------------------*/ - document.getElementsByClassName = function(className) { var children = document.getElementsByTagName('*') || document.all; var elements = new Array(); @@ -453,7 +397,11 @@ document.getElementsByClassName = function(className) { /*--------------------------------------------------------------------------*/ -var Element = { +if (!window.Element) { + var Element = new Object(); +} + +Object.extend(Element, { toggle: function() { for (var i = 0; i < arguments.length; i++) { var element = $(arguments[i]); @@ -484,8 +432,52 @@ var Element = { getHeight: function(element) { element = $(element); return element.offsetHeight; + }, + + hasClassName: function(element, className) { + element = $(element); + if (!element) + return; + var a = element.className.split(' '); + for (var i = 0; i < a.length; i++) { + if (a[i] == className) + return true; + } + return false; + }, + + addClassName: function(element, className) { + element = $(element); + Element.removeClassName(element, className); + element.className += ' ' + className; + }, + + removeClassName: function(element, className) { + element = $(element); + if (!element) + return; + var newClassName = ''; + var a = element.className.split(' '); + for (var i = 0; i < a.length; i++) { + if (a[i] != className) { + if (i > 0) + newClassName += ' '; + newClassName += a[i]; + } + } + element.className = newClassName; + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + var element = $(element); + for (var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + Element.remove(node); + } } -} +}); var Toggle = new Object(); Toggle.display = Element.toggle; @@ -561,205 +553,486 @@ Insertion.After.prototype = (new Abstract.Insertion('afterEnd')).extend({ } }); +var Field = { + clear: function() { + for (var i = 0; i < arguments.length; i++) + $(arguments[i]).value = ''; + }, + + focus: function(element) { + $(element).focus(); + }, + + present: function() { + for (var i = 0; i < arguments.length; i++) + if ($(arguments[i]).value == '') return false; + return true; + }, + + select: function(element) { + $(element).select(); + }, + + activate: function(element) { + $(element).focus(); + $(element).select(); + } +} + /*--------------------------------------------------------------------------*/ -var Effect = new Object(); - -Effect.Highlight = Class.create(); -Effect.Highlight.prototype = { - initialize: function(element) { - this.element = $(element); - this.start = 153; - this.finish = 255; - this.current = this.start; - this.fade(); - }, - - fade: function() { - if (this.isFinished()) return; - if (this.timer) clearTimeout(this.timer); - this.highlight(this.element, this.current); - this.current += 17; - this.timer = setTimeout(this.fade.bind(this), 250); - }, - - isFinished: function() { - return this.current > this.finish; - }, - - highlight: function(element, current) { - element.style.backgroundColor = "#ffff" + current.toColorPart(); - } -} - - -Effect.Fade = Class.create(); -Effect.Fade.prototype = { - initialize: function(element) { - this.element = $(element); - this.start = 100; - this.finish = 0; - this.current = this.start; - this.fade(); - }, - - fade: function() { - if (this.isFinished()) { this.element.style.display = 'none'; return; } - if (this.timer) clearTimeout(this.timer); - this.setOpacity(this.element, this.current); - this.current -= 10; - this.timer = setTimeout(this.fade.bind(this), 50); - }, - - isFinished: function() { - return this.current <= this.finish; - }, - - setOpacity: function(element, opacity) { - opacity = (opacity == 100) ? 99.999 : opacity; - element.style.filter = "alpha(opacity:"+opacity+")"; - element.style.opacity = opacity/100 /*//*/; - } -} - -Effect.Scale = Class.create(); -Effect.Scale.prototype = { - initialize: function(element, percent) { - this.element = $(element); - this.startScale = 1.0; - this.startHeight = this.element.offsetHeight; - this.startWidth = this.element.offsetWidth; - this.currentHeight = this.startHeight; - this.currentWidth = this.startWidth; - this.finishScale = (percent/100) /*//*/; - if (this.element.style.fontSize=="") this.sizeEm = 1.0; - if (this.element.style.fontSize.indexOf("em")>0) - this.sizeEm = parseFloat(this.element.style.fontSize); - if(this.element.effect_scale) { - clearTimeout(this.element.effect_scale.timer); - this.startScale = this.element.effect_scale.currentScale; - this.startHeight = this.element.effect_scale.startHeight; - this.startWidth = this.element.effect_scale.startWidth; - if(this.element.effect_scale.sizeEm) - this.sizeEm = this.element.effect_scale.sizeEm; +var Form = { + serialize: function(form) { + var elements = Form.getElements($(form)); + var queryComponents = new Array(); + + for (var i = 0; i < elements.length; i++) { + var queryComponent = Form.Element.serialize(elements[i]); + if (queryComponent) + queryComponents.push(queryComponent); } - this.element.effect_scale = this; - this.currentScale = this.startScale; - this.factor = this.finishScale - this.startScale; - this.options = arguments[2] || {}; - this.scale(); + + return queryComponents.join('&'); }, - scale: function() { - if (this.isFinished()) { - this.setDimensions(this.element, this.startWidth*this.finishScale, this.startHeight*this.finishScale); - if(this.sizeEm) this.element.style.fontSize = this.sizeEm*this.finishScale + "em"; - if(this.options.complete) this.options.complete(this); - return; + getElements: function(form) { + var form = $(form); + var elements = new Array(); + + for (tagName in Form.Element.Serializers) { + var tagElements = form.getElementsByTagName(tagName); + for (var j = 0; j < tagElements.length; j++) + elements.push(tagElements[j]); } - if (this.timer) clearTimeout(this.timer); - if (this.options.step) this.options.step(this); - this.setDimensions(this.element, this.currentWidth, this.currentHeight); - if(this.sizeEm) this.element.style.fontSize = this.sizeEm*this.currentScale + "em"; - this.currentScale += (this.factor/10) /*//*/; - this.currentWidth = this.startWidth * this.currentScale; - this.currentHeight = this.startHeight * this.currentScale; - this.timer = setTimeout(this.scale.bind(this), 50); + return elements; }, - isFinished: function() { - return (this.factor < 0) ? - this.currentScale <= this.finishScale : this.currentScale >= this.finishScale; - }, - - setDimensions: function(element, width, height) { - element.style.width = width + 'px'; - element.style.height = height + 'px'; - } -} - -Effect.Squish = Class.create(); -Effect.Squish.prototype = { - initialize: function(element) { - this.element = $(element); - new Effect.Scale(this.element, 1, { complete: this.hide.bind(this) } ); - }, - hide: function() { - this.element.style.display = 'none'; - } -} - -Effect.Puff = Class.create(); -Effect.Puff.prototype = { - initialize: function(element) { - this.element = $(element); - this.opacity = 100; - this.startTop = this.element.top || this.element.offsetTop; - this.startLeft = this.element.left || this.element.offsetLeft; - new Effect.Scale(this.element, 200, { step: this.fade.bind(this), complete: this.hide.bind(this) } ); - }, - fade: function(effect) { - topd = (((effect.currentScale)*effect.startHeight) - effect.startHeight)/2; - leftd = (((effect.currentScale)*effect.startWidth) - effect.startWidth)/2; - this.element.style.position='absolute'; - this.element.style.top = this.startTop-topd + "px"; - this.element.style.left = this.startLeft-leftd + "px"; - this.opacity -= 10; - this.setOpacity(this.element, this.opacity); - if(navigator.appVersion.indexOf('AppleWebKit')>0) this.element.innerHTML += ''; //force redraw on safari - }, - hide: function() { - this.element.style.display = 'none'; - }, - setOpacity: function(element, opacity) { - opacity = (opacity == 100) ? 99.999 : opacity; - element.style.filter = "alpha(opacity:"+opacity+")"; - element.style.opacity = opacity/100 /*//*/; - } -} - -Effect.Appear = Class.create(); -Effect.Appear.prototype = { - initialize: function(element) { - this.element = $(element); - this.start = 0; - this.finish = 100; - this.current = this.start; - this.fade(); - }, - - fade: function() { - if (this.isFinished()) return; - if (this.timer) clearTimeout(this.timer); - this.setOpacity(this.element, this.current); - this.current += 10; - this.timer = setTimeout(this.fade.bind(this), 50); - }, - - isFinished: function() { - return this.current > this.finish; - }, - - setOpacity: function(element, opacity) { - opacity = (opacity == 100) ? 99.999 : opacity; - element.style.filter = "alpha(opacity:"+opacity+")"; - element.style.opacity = opacity/100 /*//*/; - element.style.display = ''; - } -} - -Effect.ContentZoom = Class.create(); -Effect.ContentZoom.prototype = { - initialize: function(element, percent) { - this.element = $(element); - if (this.element.style.fontSize=="") this.sizeEm = 1.0; - if (this.element.style.fontSize.indexOf("em")>0) - this.sizeEm = parseFloat(this.element.style.fontSize); - if(this.element.effect_contentzoom) { - this.sizeEm = this.element.effect_contentzoom.sizeEm; + getInputs: function(form, typeName, name) { + var form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) + return inputs; + + var matchingInputs = new Array(); + for (var i = 0; i < inputs.length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || + (name && input.name != name)) + continue; + matchingInputs.push(input); } - this.element.effect_contentzoom = this; - this.element.style.fontSize = this.sizeEm*(percent/100) + "em" /*//*/; - if(navigator.appVersion.indexOf('AppleWebKit')>0) { this.element.scrollTop -= 1; }; + + return matchingInputs; + }, + + disable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.blur(); + element.disabled = 'true'; + } + }, + + enable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.disabled = ''; + } + }, + + focusFirstElement: function(form) { + var form = $(form); + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + if (element.type != 'hidden' && !element.disabled) { + Field.activate(element); + break; + } + } + }, + + reset: function(form) { + $(form).reset(); } -} \ No newline at end of file +} + +Form.Element = { + serialize: function(element) { + var element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return encodeURIComponent(parameter[0]) + '=' + + encodeURIComponent(parameter[1]); + }, + + getValue: function(element) { + var element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return parameter[1]; + } +} + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'submit': + case 'hidden': + case 'password': + case 'text': + return Form.Element.Serializers.textarea(element); + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + } + return false; + }, + + inputSelector: function(element) { + if (element.checked) + return [element.name, element.value]; + }, + + textarea: function(element) { + return [element.name, element.value]; + }, + + select: function(element) { + var value = ''; + if (element.type == 'select-one') { + var index = element.selectedIndex; + if (index >= 0) + value = element.options[index].value || element.options[index].text; + } else { + value = new Array(); + for (var i = 0; i < element.length; i++) { + var opt = element.options[i]; + if (opt.selected) + value.push(opt.value || opt.text); + } + } + return [element.name, value]; + } +} + +/*--------------------------------------------------------------------------*/ + +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = (new Abstract.TimedObserver()).extend({ + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = (new Abstract.TimedObserver()).extend({ + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + var elements = Form.getElements(this.element); + for (var i = 0; i < elements.length; i++) + this.registerCallback(elements[i]); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + element.target = this; + element.prev_onclick = element.onclick || Prototype.emptyFunction; + element.onclick = function() { + this.prev_onclick(); + this.target.onElementEvent(); + } + break; + case 'password': + case 'text': + case 'textarea': + case 'select-one': + case 'select-multiple': + element.target = this; + element.prev_onchange = element.onchange || Prototype.emptyFunction; + element.onchange = function() { + this.prev_onchange(); + this.target.onElementEvent(); + } + break; + } + } + } +} + +Form.Element.EventObserver = Class.create(); +Form.Element.EventObserver.prototype = (new Abstract.EventObserver()).extend({ + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = (new Abstract.EventObserver()).extend({ + getValue: function() { + return Form.serialize(this.element); + } +}); + + +if (!window.Event) { + var Event = new Object(); +} + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + + element: function(event) { + return event.target || event.srcElement; + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0; i < Event.observers.length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + ((navigator.appVersion.indexOf('AppleWebKit') > 0) + || element.attachEvent)) + name = 'keydown'; + + this._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + ((navigator.appVersion.indexOf('AppleWebKit') > 0) + || element.detachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + element.detachEvent('on' + name, observer); + } + } +}); + +/* prevent memory leaks in IE */ +Event.observe(window, 'unload', Event.unloadCache, false); + +var Position = { + + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [valueL, valueT]; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = this.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + clone: function(source, target) { + source = $(source); + target = $(target); + target.style.position = 'absolute'; + var offsets = this.cumulativeOffset(source); + target.style.top = offsets[1] + 'px'; + target.style.left = offsets[0] + 'px'; + target.style.width = source.offsetWidth + 'px'; + target.style.height = source.offsetHeight + 'px'; + } +} diff --git a/tracks/script/benchmarker b/tracks/script/benchmarker index 75703f56..2e323b66 100755 --- a/tracks/script/benchmarker +++ b/tracks/script/benchmarker @@ -1,4 +1,4 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby1.8 if ARGV.empty? puts "Usage: benchmarker times 'Person.expensive_way' 'Person.another_expensive_way' ..." diff --git a/tracks/script/breakpointer b/tracks/script/breakpointer index 6964c9f1..dc3e8110 100755 --- a/tracks/script/breakpointer +++ b/tracks/script/breakpointer @@ -1,4 +1,4 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby1.8 require 'rubygems' require_gem 'rails' require 'breakpoint_client' diff --git a/tracks/script/console b/tracks/script/console index 22a487f4..a7b2cccf 100755 --- a/tracks/script/console +++ b/tracks/script/console @@ -1,16 +1,17 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby1.8 irb = RUBY_PLATFORM =~ /mswin32/ ? 'irb.bat' : 'irb' require 'optparse' -options = {} +options = { :sandbox => false, :irb => irb } OptionParser.new do |opt| opt.on('-s', '--sandbox', 'Rollback database modifications on exit.') { |options[:sandbox]| } + opt.on("--irb=[#{irb}]", 'Invoke a different irb.') { |options[:irb]| } opt.parse!(ARGV) end -libs = " -r #{File.dirname(__FILE__)}/../config/environment" -libs << " -r #{File.dirname(__FILE__)}/console_sandbox" if options[:sandbox] -libs << " -r irb/completion" +libs = " -r irb/completion" +libs << " -r #{File.dirname(__FILE__)}/../config/environment" +libs << " -r console_sandbox" if options[:sandbox] ENV['RAILS_ENV'] = ARGV.first || 'development' if options[:sandbox] @@ -19,4 +20,4 @@ if options[:sandbox] else puts "Loading #{ENV['RAILS_ENV']} environment." end -exec "#{irb} #{libs}" +exec "#{options[:irb]} #{libs} --prompt-mode simple" diff --git a/tracks/script/destroy b/tracks/script/destroy index 34070bbb..624049da 100755 --- a/tracks/script/destroy +++ b/tracks/script/destroy @@ -1,5 +1,7 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby1.8 require File.dirname(__FILE__) + '/../config/environment' require 'rails_generator' require 'rails_generator/scripts/destroy' + +ARGV.shift if ['--help', '-h'].include?(ARGV[0]) Rails::Generator::Scripts::Destroy.new.run(ARGV) diff --git a/tracks/script/generate b/tracks/script/generate index ea78b2f0..a104fc94 100755 --- a/tracks/script/generate +++ b/tracks/script/generate @@ -1,5 +1,7 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby1.8 require File.dirname(__FILE__) + '/../config/environment' require 'rails_generator' require 'rails_generator/scripts/generate' + +ARGV.shift if ['--help', '-h'].include?(ARGV[0]) Rails::Generator::Scripts::Generate.new.run(ARGV) diff --git a/tracks/script/profiler b/tracks/script/profiler index 432a3c80..92cf8247 100755 --- a/tracks/script/profiler +++ b/tracks/script/profiler @@ -1,17 +1,34 @@ -#!/usr/bin/ruby - +#!/usr/bin/ruby1.8 if ARGV.empty? - puts "Usage: profiler 'Person.expensive_method(10)' [times]" - exit + $stderr.puts "Usage: profiler 'Person.expensive_method(10)' [times]" + exit(1) end +# Keep the expensive require out of the profile. +$stderr.puts 'Loading Rails...' require File.dirname(__FILE__) + '/../config/environment' -require "profiler" -# Don't include compilation in the profile -eval(ARGV.first) +# Define a method to profile. +if ARGV[1] and ARGV[1].to_i > 1 + eval "def profile_me() #{ARGV[1]}.times { #{ARGV[0]} } end" +else + eval "def profile_me() #{ARGV[0]} end" +end -Profiler__::start_profile -(ARGV[1] || 1).to_i.times { eval(ARGV.first) } -Profiler__::stop_profile -Profiler__::print_profile($stdout) \ No newline at end of file +# Use the ruby-prof extension if available. Fall back to stdlib profiler. +begin + require 'prof' + $stderr.puts 'Using the ruby-prof extension.' + Prof.clock_mode = Prof::GETTIMEOFDAY + Prof.start + profile_me + results = Prof.stop + require 'rubyprof_ext' + Prof.print_profile(results, $stderr) +rescue LoadError + $stderr.puts 'Using the standard Ruby profiler.' + Profiler__.start_profile + profile_me + Profiler__.stop_profile + Profiler__.print_profile($stderr) +end diff --git a/tracks/script/runner b/tracks/script/runner index 5effaa17..a9f705c1 100755 --- a/tracks/script/runner +++ b/tracks/script/runner @@ -1,4 +1,29 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby1.8 +require 'optparse' + +options = { :environment => "development" } + +ARGV.options do |opts| + script_name = File.basename($0) + opts.banner = "Usage: runner 'puts Person.find(1).name' [options]" + + opts.separator "" + + opts.on("-e", "--environment=name", String, + "Specifies the environment for the runner to operate under (test/development/production).", + "Default: development") { |options[:environment]| } + + opts.separator "" + + opts.on("-h", "--help", + "Show this help message.") { puts opts; exit } + + opts.parse! +end + +ENV["RAILS_ENV"] = options[:environment] + +#!/usr/local/bin/ruby require File.dirname(__FILE__) + '/../config/environment' eval(ARGV.first) \ No newline at end of file diff --git a/tracks/script/server b/tracks/script/server index a53a4d28..51eeab46 100755 --- a/tracks/script/server +++ b/tracks/script/server @@ -1,4 +1,4 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby1.8 require 'webrick' require 'optparse' @@ -22,10 +22,7 @@ ARGV.options do |opts| "Default: 3000") { |OPTIONS[:port]| } opts.on("-b", "--binding=ip", String, "Binds Rails to the specified ip.", - "Default: 127.0.0.1") { |OPTIONS[:ip]| } - opts.on("-i", "--index=controller", String, - "Specifies an index controller that requests for root will go to (instead of congratulations screen)." - ) { |OPTIONS[:index_controller]| } + "Default: 0.0.0.0") { |OPTIONS[:ip]| } opts.on("-e", "--environment=name", String, "Specifies the environment to run this server under (test/development/production).", "Default: development") { |OPTIONS[:environment]| } @@ -48,4 +45,5 @@ require 'webrick_server' OPTIONS['working_directory'] = File.expand_path(RAILS_ROOT) puts "=> Rails application started on http://#{OPTIONS[:ip]}:#{OPTIONS[:port]}" +puts "=> Ctrl-C to shutdown server; call with --help for options" if OPTIONS[:server_type] == WEBrick::SimpleServer DispatchServlet.dispatch(OPTIONS) diff --git a/tracks/test/fixtures/contexts.yml b/tracks/test/fixtures/contexts.yml index a56c1646..a876f3e7 100644 --- a/tracks/test/fixtures/contexts.yml +++ b/tracks/test/fixtures/contexts.yml @@ -1 +1,46 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html + +agenda: + id: 1 + name: agenda + user_id: 1 + +call: + id: 2 + name: call + user_id: 1 + +email: + id: 3 + name: email + user_id: 1 + +errand: + id: 4 + name: errand + user_id: 1 + +lab: + id: 5 + name: lab + user_id: 1 + +library: + id: 6 + name: library + user_id: 1 + +freetime: + id: 7 + name: freetime + user_id: 1 + +office: + id: 8 + name: office + user_id: 1 + +waitingfor: + id: 9 + name: waiting for + user_id: 1 diff --git a/tracks/test/fixtures/notes.yml b/tracks/test/fixtures/notes.yml index cce14437..ceafa8e7 100644 --- a/tracks/test/fixtures/notes.yml +++ b/tracks/test/fixtures/notes.yml @@ -1,5 +1,7 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html first_notes: id: 1 + user_id: 1 another_notes: id: 2 + user_id: 1 diff --git a/tracks/test/fixtures/projects.yml b/tracks/test/fixtures/projects.yml index a56c1646..4f6dc14a 100644 --- a/tracks/test/fixtures/projects.yml +++ b/tracks/test/fixtures/projects.yml @@ -1 +1,16 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html + +timemachine: + id: 1 + name: Build a working time machine + user_id: 1 + +moremoney: + id: 2 + name: Make more money than Billy Gates + user_id: 1 + +gardenclean: + id: 3 + name: Evict dinosaurs from the garden + user_id: 1 diff --git a/tracks/test/fixtures/todos.yml b/tracks/test/fixtures/todos.yml index a56c1646..9d24564b 100644 --- a/tracks/test/fixtures/todos.yml +++ b/tracks/test/fixtures/todos.yml @@ -1 +1,169 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html + +1: + id: 1 + context_id: 1 + project_id: 2 + description: Call Bill Gates to find out how much he makes per day + notes: ~ + done: 0 + created_at: 2004-11-28 16:01:00 + due: 2004-10-30 + completed: ~ + user_id: 1 + +2: + id: 2 + context_id: 2 + project_id: 3 + description: Call dinosaur exterminator + notes: Ask him if I need to hire a skip for the corpses. + done: 0 + created_at: 2004-11-28 16:06:08 + due: 2004-11-30 + completed: ~ + user_id: 1 + +3: + id: 3 + context_id: 4 + project_id: ~ + description: Buy milk + notes: ~ + done: 1 + created_at: 2004-11-28 16:06:31 + due: ~ + completed: 2004-11-28 + user_id: 1 + +4: + id: 4 + context_id: 4 + project_id: ~ + description: Buy bread + notes: ~ + done: 1 + created_at: 2004-11-28 16:06:58 + due: ~ + completed: 2004-11-28 + user_id: 1 + +5: + id: 5 + context_id: 5 + project_id: 1 + description: Construct time dilation device + notes: ~ + done: 0 + created_at: 2004-11-28 16:07:33 + due: ~ + completed: ~ + user_id: 1 + +6: + id: 6 + context_id: 2 + project_id: 1 + description: Phone Grandfather to ask about the paradox + notes: Added some _notes_. + done: 0 + created_at: 2004-11-28 16:08:33 + due: 2004-12-30 + completed: ~ + user_id: 1 + +7: + id: 7 + context_id: 6 + project_id: 3 + description: Get a book out of the library + notes: 'Dinosaurs''R' + done: 0 + created_at: 2004-12-22 14:07:06 + due: ~ + completed: ~ + user_id: 1 + +8: + id: 8 + context_id: 4 + project_id: ~ + description: Upgrade to Rails 0.9.1 + notes: ~ + done: 1 + created_at: 2004-12-20 17:02:52 + due: 2004-12-21 + completed: 2004-12-20 + user_id: 1 + +9: + id: 9 + context_id: 1 + project_id: ~ + description: This should be due today + notes: ~ + done: 0 + created_at: 2004-12-31 17:23:06 + due: 2004-12-31 + completed: ~ + user_id: 1 + +10: + id: 10 + context_id: 1 + project_id: ~ + description: foo + notes: ~ + done: 1 + created_at: 2004-12-31 18:38:34 + due: 2005-01-05 + completed: 2005-01-02 12:27:10 + user_id: 1 + +11: + id: 11 + context_id: 1 + project_id: 2 + description: Buy shares + notes: ~ + done: 0 + created_at: 2005-01-01 12:40:26 + due: 2005-02-01 + completed: ~ + user_id: 1 + +12: + id: 12 + context_id: 1 + project_id: 3 + description: Buy stegosaurus bait + notes: ~ + done: 1 + created_at: 2005-01-01 12:53:12 + due: 2005-01-02 + completed: 2005-01-01 15:44:19 + user_id: 1 + +13: + id: 13 + context_id: 1 + project_id: 3 + description: New action in context + notes: Some notes + done: 1 + created_at: 2005-01-02 14:52:49 + due: 2005-03-01 + completed: 2005-01-02 15:44:19 + user_id: 1 + +14: + id: 14 + context_id: 2 + project_id: 2 + description: Call stock broker + notes: 'tel: 12345' + done: 0 + created_at: 2005-01-03 11:38:25 + due: ~ + completed: ~ + user_id: 1 diff --git a/tracks/test/fixtures/users.yml b/tracks/test/fixtures/users.yml index a56c1646..cb01964b 100644 --- a/tracks/test/fixtures/users.yml +++ b/tracks/test/fixtures/users.yml @@ -1 +1,14 @@ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +admin_user: + id: 1 + login: admin + password: <%= Digest::SHA1.hexdigest("change-me--abracadabra--") %> + word: <%= Digest::SHA1.hexdigest("change-me--badger--") %> + is_admin: 1 + +other_user: + id: 2 + login: jane + password: <%= Digest::SHA1.hexdigest("change-me--sesame--") %> + word: <%= Digest::SHA1.hexdigest("change-me--mouse--") %> + is_admin: 0 \ No newline at end of file diff --git a/tracks/test/functional/login_controller_test.rb b/tracks/test/functional/login_controller_test.rb index 4a1efef6..407e1265 100644 --- a/tracks/test/functional/login_controller_test.rb +++ b/tracks/test/functional/login_controller_test.rb @@ -1,17 +1,66 @@ require File.dirname(__FILE__) + '/../test_helper' require 'login_controller' +require_dependency "login_system" # Re-raise errors caught by the controller. class LoginController; def rescue_action(e) raise e end; end class LoginControllerTest < Test::Unit::TestCase + fixtures :users + def setup @controller = LoginController.new - @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new end - # Replace this with your real tests. - def test_truth - assert true + def test_login_with_invalid_user + post :login, {:user_login => 'cracker', :user_password => 'secret'} + assert_response :success + assert_template 'login/login' + assert_nil(session['user']) end + + def test_login_with_valid_admin_user + user = login('admin','abracadabra') + assert_equal "Login successful", flash['notice'] + assert_redirected_to :controller => 'todo', :action => 'list' + assert_equal 'admin', user.login + assert_equal 1, user.is_admin + assert_equal "#{Digest::SHA1.hexdigest("change-me--badger--")}", user.word + end + + def test_login_with_valid_standard_user + user = login('jane','sesame') + assert_equal "Login successful", flash['notice'] + assert_redirected_to :controller => 'todo', :action => 'list' + assert_equal 'jane', user.login + assert_equal 0, user.is_admin + assert_equal "#{Digest::SHA1.hexdigest("change-me--mouse--")}", user.word + end + + def test_logout + user = login('admin','abracadabra') + get :logout + assert_nil(session['user']) + assert_template 'logout' + end + + # TODO: Not sure how to test whether the user is blocked if the admin user is + # not logged in. I tried setting the session[:user] cookie to nil first, + # but that generated an error. + # + def test_create + post :create, :user => {:login => 'newbie', + :password => 'newbiepass', + :password_confirmation => 'newbiepass', + :word => 'turkey'} + assert_equal "Signup successful", flash['notice'] + assert_redirected_to :controller => 'todo', :action => 'list' + assert_not_nil(session['user']) + user = User.find(session['user'].id) + assert_equal 'newbie', user.login + assert_equal 0, user.is_admin + end + end diff --git a/tracks/test/test_helper.rb b/tracks/test/test_helper.rb index e560caa7..cea2c8d1 100644 --- a/tracks/test/test_helper.rb +++ b/tracks/test/test_helper.rb @@ -1,14 +1,47 @@ ENV["RAILS_ENV"] = "test" -require File.dirname(__FILE__) + "/../config/environment" + +# Expand the path to environment so that Ruby does not load it multiple times +# File.expand_path can be removed if Ruby 1.9 is in use. +require File.expand_path(File.dirname(__FILE__) + "/../config/environment") require 'application' require 'test/unit' require 'active_record/fixtures' require 'action_controller/test_process' +require 'action_web_service/test_invoke' require 'breakpoint' -def create_fixtures(*table_names) - Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures", table_names) -end +Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" -Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/" \ No newline at end of file +class Test::Unit::TestCase + # Turn these on to use transactional fixtures with table_name(:fixture_name) instantiation of fixtures + # self.use_transactional_fixtures = true + # self.use_instantiated_fixtures = false + + def create_fixtures(*table_names) + Fixtures.create_fixtures(File.dirname(__FILE__) + "/fixtures", table_names) + end + + # Logs in a user and returns the user object found in the session object + # + def login(login,password) + post :login, {:user_login => login, :user_password => password} + assert_not_nil(session['user']) + return User.find(session['user'].id) + end + + # Generates a random string of ascii characters (a-z, "1 0") + # of a given length for testing assignment to fields + # for validation purposes + # + def generate_random_string(length) + string = "" + characters = %w(a b c d e f g h i j k l m n o p q r s t u v w z y z 1\ 0) + length.times do + pick = characters[rand(26)] + string << pick + end + return string + end + +end \ No newline at end of file diff --git a/tracks/test/unit/notes_test.rb b/tracks/test/unit/notes_test.rb index cc9bc619..de4354f9 100644 --- a/tracks/test/unit/notes_test.rb +++ b/tracks/test/unit/notes_test.rb @@ -4,11 +4,11 @@ class NotesTest < Test::Unit::TestCase fixtures :notes def setup - @notes = Notes.find(1) + @notes = Note.find(1) end # Replace this with your real tests. def test_truth - assert_kind_of Notes, @notes + assert_kind_of Note, @notes end end diff --git a/tracks/test/unit/todo_test.rb b/tracks/test/unit/todo_test.rb index 8c8599f5..30bfb76f 100644 --- a/tracks/test/unit/todo_test.rb +++ b/tracks/test/unit/todo_test.rb @@ -1,10 +1,59 @@ require File.dirname(__FILE__) + '/../test_helper' +require 'date' class TodoTest < Test::Unit::TestCase fixtures :todos - # Replace this with your real tests. - def test_truth - assert true + def setup + @not_completed1 = Todo.find(1) + @not_completed2 = Todo.find(2) + @completed = Todo.find(8) + end + + # Test loading a todo item + def test_load + assert_kind_of Todo, @not_completed1 + assert_equal 1, @not_completed1.id + assert_equal 1, @not_completed1.context_id + assert_equal 2, @not_completed1.project_id + assert_equal "Call Bill Gates to find out how much he makes per day", @not_completed1.description + assert_nil @not_completed1.notes + assert_equal 0, @not_completed1.done + assert_equal "2004-11-28 16:01:00", @not_completed1.created_at.strftime("%Y-%m-%d %H:%M:%S") + assert_equal "2004-10-30", @not_completed1.due.strftime("%Y-%m-%d") + assert_nil @not_completed1.completed + assert_equal 1, @not_completed1.user_id + end + + def test_completed + assert_kind_of Todo, @completed + assert_equal 1, @completed.done + assert_not_nil @completed.completed + end + + # Validation tests + # + def test_validate_presence_of_description + assert_equal "Call dinosaur exterminator", @not_completed2.description + @not_completed2.description = "" + assert !@not_completed2.save + assert_equal 1, @not_completed2.errors.count + assert_equal "can't be blank", @not_completed2.errors.on(:description) + end + + def test_validate_length_of_description + assert_equal "Call dinosaur exterminator", @not_completed2.description + @not_completed2.description = generate_random_string(101) + assert !@not_completed2.save + assert_equal 1, @not_completed2.errors.count + assert_equal "is too long (max is 100 characters)", @not_completed2.errors.on(:description) + end + + def test_validate_length_of_notes + assert_equal "Ask him if I need to hire a skip for the corpses.", @not_completed2.notes + @not_completed2.notes = generate_random_string(60001) + assert !@not_completed2.save + assert_equal 1, @not_completed2.errors.count + assert_equal "is too long (max is 60000 characters)", @not_completed2.errors.on(:notes) end end diff --git a/tracks/test/unit/user_test.rb b/tracks/test/unit/user_test.rb index 5468f7a2..37d241ac 100644 --- a/tracks/test/unit/user_test.rb +++ b/tracks/test/unit/user_test.rb @@ -3,8 +3,60 @@ require File.dirname(__FILE__) + '/../test_helper' class UserTest < Test::Unit::TestCase fixtures :users - # Replace this with your real tests. - def test_truth - assert true + def setup + @admin_user = User.find(1) + @other_user = User.find(2) + end + + # Test an admin user model + def test_admin + assert_kind_of User, @admin_user + assert_equal 1, @admin_user.id + assert_equal "admin", @admin_user.login + assert_equal "#{Digest::SHA1.hexdigest("change-me--abracadabra--")}", @admin_user.password + assert_equal "#{Digest::SHA1.hexdigest("change-me--badger--")}", @admin_user.word + assert_equal 1, @admin_user.is_admin + end + + # Test a non-admin user model + def test_non_admin + assert_kind_of User, @other_user + assert_equal 2, @other_user.id + assert_equal "jane", @other_user.login + assert_equal "#{Digest::SHA1.hexdigest("change-me--sesame--")}", @other_user.password + assert_equal "#{Digest::SHA1.hexdigest("change-me--mouse--")}", @other_user.word + assert_equal 0, @other_user.is_admin + end + + def test_validate_short_password + assert_equal "#{Digest::SHA1.hexdigest("change-me--sesame--")}", @other_user.password + @other_user.password = "four" + assert !@other_user.save + assert_equal 1, @other_user.errors.count + assert_equal "is too short (min is 5 characters)", @other_user.errors.on(:password) + end + + # Test a password longer than 40 characters + def test_validate_long_password + assert_equal "#{Digest::SHA1.hexdigest("change-me--sesame--")}", @other_user.password + @other_user.password = generate_random_string(41) + assert !@other_user.save + assert_equal 1, @other_user.errors.count + assert_equal "is too long (max is 40 characters)", @other_user.errors.on(:password) + end + + def test_validate_correct_length_password + assert_equal "#{Digest::SHA1.hexdigest("change-me--sesame--")}", @other_user.password + @other_user.password = generate_random_string(6) + assert @other_user.save + end + + # Test an invalid user with no password + def test_validate_missing_password + assert_equal 2, @other_user.id + @other_user.password = "" + assert !@other_user.save + assert_equal 2, @other_user.errors.count + assert_equal ["can't be blank", "is too short (min is 5 characters)"], @other_user.errors.on(:password) end end