diff --git a/.gitignore.rails2 b/.gitignore.rails2 new file mode 100644 index 00000000..8e2cc0f5 --- /dev/null +++ b/.gitignore.rails2 @@ -0,0 +1,25 @@ +*~ +*.tmproj +.dotest +/.emacs-project +/.redcar +config/database.yml +config/site.yml +config/deploy.rb +db/*.sqlite3 +db/data.yml +db/schema.rb +log +nbproject +public/javascripts/cache +public/stylesheets/cache +tmp +vendor/plugins/query_trace/ +rerun.txt +public/javascripts/jquery-cached.js +public/javascripts/tracks-cached.js +public/stylesheets/tracks-cached.css +.idea +.rvmrc +.yardoc +tags diff --git a/Gemfile b/Gemfile index 92d8ee9f..47989c8d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,34 +1,55 @@ -source :gemcutter -source "http://gems.github.com/" +source 'https://rubygems.org' -gem "rake", "~>0.8.7" -gem "rails", "~>2.3.12" -gem "highline", "~>1.5.0" -gem "RedCloth", "4.2.8" -gem "sanitize", "~>1.2.1" -gem "rack", "1.1.0" -gem "will_paginate", "~> 2.3.15" -gem "has_many_polymorphs", "~> 2.13" -gem "acts_as_list", "~>0.1.4" -gem "aasm", "~>2.2.0" -gem "rubyjedi-actionwebservice", :require => "actionwebservice" -gem "rubycas-client", "~>2.2.1" -gem "ruby-openid", :require => "openid" +gem 'rails', '3.2.3' + +# Bundle edge Rails instead: +# gem 'rails', :git => 'git://github.com/rails/rails.git' + +# you may comment out the database driver you will not be using. +# This will prevent a native build of the driver. Building native drivers is not always possible on all hosters gem "sqlite3" -gem "mysql" -gem 'bcrypt-ruby', '~> 2.1.4' -gem 'htmlentities', '~> 4.3.0' -gem "mail" +gem "mysql2" -if RUBY_VERSION.to_f >= 1.9 - gem "soap4r-ruby1.9" -else - gem "soap4r", "~>1.5.8" +# gem "highline", "~>1.5.0" +gem "RedCloth" +gem "formatize" +gem "sanitize" +gem "will_paginate" +gem "acts_as_list" +gem "aasm" +gem "htmlentities" +gem "swf_fu" +gem "rails_autolink" + +# Gems used only for assets and not required +# in production environments by default. +group :assets do + gem 'sass-rails', '~> 3.2.3' + gem 'coffee-rails', '~> 3.2.1' + + # See https://github.com/sstephenson/execjs#readme for more supported runtimes + # gem 'therubyracer', :platform => :ruby + + gem 'uglifier', '>= 1.0.3' end +gem 'jquery-rails' + +# To use ActiveModel has_secure_password +gem 'bcrypt-ruby', '~> 3.0.0' + +# To use Jbuilder templates for JSON +# gem 'jbuilder' + +# Use unicorn as the app server +# gem 'unicorn' + +# Deploy with Capistrano +# gem 'capistrano' + group :development do if RUBY_VERSION.to_f >= 1.9 - gem "ruby-debug19" + # gem "ruby-debug19", :require => 'ruby-debug' gem "mongrel", "1.2.0.pre2" else gem "ruby-debug" @@ -38,16 +59,24 @@ group :development do end group :test do - gem "test-unit", "1.2.3" - gem "flexmock" - gem "ZenTest", ">=4.0.0" - gem "hpricot" - gem "hoe" - gem "rspec-rails", "~>1.3.3" - gem "thoughtbot-factory_girl" - gem 'memory_test_fix', '~>0.1.3' - gem "selenium-client" - gem "webrat", ">=0.7.0" - gem "database_cleaner", ">=0.5.0" - gem "cucumber-rails", "~>0.3.0" +# gem "test-unit", "1.2.3" +# gem "flexmock" +# gem "ZenTest", ">=4.0.0" +# gem "hpricot" +# gem "hoe" +# gem "rspec-rails", "~>1.3.3" +# gem 'memory_test_fix', '~>0.1.3' + gem "factory_girl_rails" + gem "capybara" + gem "selenium-webdriver" # Note that > 2.14 has problems: https://code.google.com/p/selenium/issues/detail?id=3075 + gem "database_cleaner" + gem "cucumber-rails" + gem "aruba" + + # uncomment to use the webkit option. This depends on Qt to be installed + #gem "capybara-webkit" + + # uncomment to be able to make screenshots from scenarios + #gem "capybara-screenshot" + #gem "launchy" end diff --git a/Gemfile.lock b/Gemfile.lock index 27b98074..6f52bf1c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,150 +1,210 @@ GEM - remote: http://rubygems.org/ - remote: http://gems.github.com/ + remote: https://rubygems.org/ specs: - RedCloth (4.2.8) - ZenTest (4.6.1) - aasm (2.2.0) - actionmailer (2.3.14) - actionpack (= 2.3.14) - actionpack (2.3.14) - activesupport (= 2.3.14) - rack (~> 1.1.0) - activerecord (2.3.14) - activesupport (= 2.3.14) - activeresource (2.3.14) - activesupport (= 2.3.14) - activesupport (2.3.14) - acts_as_list (0.1.4) - bcrypt-ruby (2.1.4) - builder (3.0.0) - cgi_multipart_eof_fix (2.5.0) - columnize (0.3.4) - cucumber (1.0.2) - builder (>= 2.1.2) - diff-lcs (>= 1.1.2) - gherkin (~> 2.4.5) - json (>= 1.4.6) - term-ansicolor (>= 1.0.5) - cucumber-rails (0.3.2) - cucumber (>= 0.8.0) - daemons (1.1.4) - database_cleaner (0.6.7) - diff-lcs (1.1.2) - fastthread (1.0.7) - flexmock (0.9.0) - gem_plugin (0.2.3) - gherkin (2.4.11) - json (>= 1.4.6) - has_many_polymorphs (2.13) + RedCloth (4.2.9) + aasm (3.0.6) activerecord - highline (1.5.2) - hoe (2.12.0) - rake (~> 0.8) - hpricot (0.8.4) - htmlentities (4.3.0) - httpclient (2.2.1) + actionmailer (3.2.3) + actionpack (= 3.2.3) + mail (~> 2.4.4) + actionpack (3.2.3) + activemodel (= 3.2.3) + activesupport (= 3.2.3) + builder (~> 3.0.0) + erubis (~> 2.7.0) + journey (~> 1.0.1) + rack (~> 1.4.0) + rack-cache (~> 1.2) + rack-test (~> 0.6.1) + sprockets (~> 2.1.2) + activemodel (3.2.3) + activesupport (= 3.2.3) + builder (~> 3.0.0) + activerecord (3.2.3) + activemodel (= 3.2.3) + activesupport (= 3.2.3) + arel (~> 3.0.2) + tzinfo (~> 0.3.29) + activeresource (3.2.3) + activemodel (= 3.2.3) + activesupport (= 3.2.3) + activesupport (3.2.3) + i18n (~> 0.6) + multi_json (~> 1.0) + acts_as_list (0.1.6) + addressable (2.2.8) + arel (3.0.2) + aruba (0.4.11) + childprocess (>= 0.2.3) + cucumber (>= 1.1.1) + ffi (>= 1.0.11) + rspec (>= 2.7.0) + bcrypt-ruby (3.0.1) + bluecloth (2.2.0) + builder (3.0.0) + capybara (1.1.2) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + selenium-webdriver (~> 2.0) + xpath (~> 0.1.4) + childprocess (0.3.2) + ffi (~> 1.0.6) + coffee-rails (3.2.2) + coffee-script (>= 2.2.0) + railties (~> 3.2.0) + coffee-script (2.2.0) + coffee-script-source + execjs + coffee-script-source (1.3.3) + cucumber (1.2.0) + builder (>= 2.1.2) + diff-lcs (>= 1.1.3) + gherkin (~> 2.10.0) + json (>= 1.4.6) + cucumber-rails (1.3.0) + capybara (>= 1.1.2) + cucumber (>= 1.1.8) + nokogiri (>= 1.5.0) + daemons (1.0.10) + database_cleaner (0.7.2) + diff-lcs (1.1.3) + erubis (2.7.0) + execjs (1.3.2) + multi_json (~> 1.0) + factory_girl (3.3.0) + activesupport (>= 3.0.0) + factory_girl_rails (3.3.0) + factory_girl (~> 3.3.0) + railties (>= 3.0.0) + ffi (1.0.11) + formatize (1.1.0) + RedCloth (~> 4.2) + actionpack (~> 3.0) + bluecloth (~> 2.2) + gem_plugin (0.2.3) + gherkin (2.10.0) + json (>= 1.4.6) + hike (1.2.1) + htmlentities (4.3.1) i18n (0.6.0) - json (1.5.3) - linecache (0.46) - rbx-require-relative (> 0.0.4) - mail (2.3.0) + journey (1.0.3) + jquery-rails (2.0.2) + railties (>= 3.2.0, < 5.0) + thor (~> 0.14) + json (1.7.3) + libwebsocket (0.1.3) + addressable + mail (2.4.4) i18n (>= 0.4.0) mime-types (~> 1.16) treetop (~> 1.4.8) - memory_test_fix (0.1.3) - mime-types (1.16) - mongrel (1.1.5) - cgi_multipart_eof_fix (>= 2.4) - daemons (>= 1.0.3) - fastthread (>= 1.0.1) - gem_plugin (>= 0.2.3) - mysql (2.8.1) - nokogiri (1.4.7) - polyglot (0.3.2) - rack (1.1.0) + mime-types (1.18) + mongrel (1.2.0.pre2) + daemons (~> 1.0.10) + gem_plugin (~> 0.2.3) + multi_json (1.3.5) + mysql2 (0.3.11) + nokogiri (1.5.2) + polyglot (0.3.3) + rack (1.4.1) + rack-cache (1.2) + rack (>= 0.4) + rack-ssl (1.3.2) + rack rack-test (0.6.1) rack (>= 1.0) - rails (2.3.14) - actionmailer (= 2.3.14) - actionpack (= 2.3.14) - activerecord (= 2.3.14) - activeresource (= 2.3.14) - activesupport (= 2.3.14) - rake (>= 0.8.3) - rake (0.8.7) - rbx-require-relative (0.0.5) - rspec (1.3.2) - rspec-rails (1.3.4) - rack (>= 1.0.0) - rspec (~> 1.3.1) - ruby-debug (0.10.4) - columnize (>= 0.1) - ruby-debug-base (~> 0.10.4.0) - ruby-debug-base (0.10.4) - linecache (>= 0.3) - ruby-openid (2.1.8) - rubycas-client (2.2.1) - activesupport - rubyjedi-actionwebservice (2.3.5.20100714122544) - actionpack (~> 2.3.0) - activerecord (~> 2.3.0) - activesupport (~> 2.3.0) - sanitize (1.2.1) - nokogiri (~> 1.4.1) - selenium-client (1.2.18) - soap4r (1.5.8) - httpclient (>= 2.1.1) - sqlite3 (1.3.4) - term-ansicolor (1.0.6) - test-unit (1.2.3) - hoe (>= 1.5.1) - thoughtbot-factory_girl (1.2.2) + rails (3.2.3) + actionmailer (= 3.2.3) + actionpack (= 3.2.3) + activerecord (= 3.2.3) + activeresource (= 3.2.3) + activesupport (= 3.2.3) + bundler (~> 1.0) + railties (= 3.2.3) + rails_autolink (1.0.7) + rails (~> 3.1) + railties (3.2.3) + actionpack (= 3.2.3) + activesupport (= 3.2.3) + rack-ssl (~> 1.3.2) + rake (>= 0.8.7) + rdoc (~> 3.4) + thor (~> 0.14.6) + rake (0.9.2.2) + rdoc (3.12) + json (~> 1.4) + rspec (2.10.0) + rspec-core (~> 2.10.0) + rspec-expectations (~> 2.10.0) + rspec-mocks (~> 2.10.0) + rspec-core (2.10.0) + rspec-expectations (2.10.0) + diff-lcs (~> 1.1.3) + rspec-mocks (2.10.1) + rubyzip (0.9.8) + sanitize (2.0.3) + nokogiri (>= 1.4.4, < 1.6) + sass (3.1.18) + sass-rails (3.2.5) + railties (~> 3.2.0) + sass (>= 3.1.10) + tilt (~> 1.3) + selenium-webdriver (2.21.2) + childprocess (>= 0.2.5) + ffi (~> 1.0) + libwebsocket (~> 0.1.3) + multi_json (~> 1.0) + rubyzip + sprockets (2.1.3) + hike (~> 1.2) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + sqlite3 (1.3.6) + swf_fu (2.0.2) + coffee-script + rails (>= 3.1) + thor (0.14.6) + tilt (1.3.3) treetop (1.4.10) polyglot polyglot (>= 0.3.1) - webrat (0.7.3) - nokogiri (>= 1.2.0) - rack (>= 1.0) - rack-test (>= 0.5.3) - will_paginate (2.3.16) - yard (0.7.3) + tzinfo (0.3.33) + uglifier (1.2.4) + execjs (>= 0.3.0) + multi_json (>= 1.0.2) + will_paginate (3.0.3) + xpath (0.1.4) + nokogiri (~> 1.3) + yard (0.8.1) PLATFORMS ruby DEPENDENCIES - RedCloth (= 4.2.8) - ZenTest (>= 4.0.0) - aasm (~> 2.2.0) - acts_as_list (~> 0.1.4) - bcrypt-ruby (~> 2.1.4) - cucumber-rails (~> 0.3.0) - database_cleaner (>= 0.5.0) - flexmock - has_many_polymorphs (~> 2.13) - highline (~> 1.5.0) - hoe - hpricot - htmlentities (~> 4.3.0) - mail - memory_test_fix (~> 0.1.3) - mongrel - mysql - rack (= 1.1.0) - rails (~> 2.3.12) - rake (~> 0.8.7) - rspec-rails (~> 1.3.3) - ruby-debug - ruby-openid - rubycas-client (~> 2.2.1) - rubyjedi-actionwebservice - sanitize (~> 1.2.1) - selenium-client - soap4r (~> 1.5.8) + RedCloth + aasm + acts_as_list + aruba + bcrypt-ruby (~> 3.0.0) + capybara + coffee-rails (~> 3.2.1) + cucumber-rails + database_cleaner + factory_girl_rails + formatize + htmlentities + jquery-rails + mongrel (= 1.2.0.pre2) + mysql2 + rails (= 3.2.3) + rails_autolink + sanitize + sass-rails (~> 3.2.3) + selenium-webdriver sqlite3 - test-unit (= 1.2.3) - thoughtbot-factory_girl - webrat (>= 0.7.0) - will_paginate (~> 2.3.15) + swf_fu + uglifier (>= 1.0.3) + will_paginate yard diff --git a/README b/README index 3b9a58bc..848cefa0 100644 --- a/README +++ b/README @@ -1,28 +1,21 @@ # Tracks: a GTD(TM) web application, built with Ruby on Rails -**IMPORTANT: Tracks is moving to a GitHub Organization to make it easier to continue administering the project. Development will soon cease at bsag/tracks and move to TracksApp/tracks. If you are currently pulling from bsag/tracks, please pull from TracksApp instead.** - -`git clone git://github.com/TracksApp/tracks.git` - -**The new home for Tracks is https://github.com/TracksApp/tracks** - * Project homepage: http://getontracks.org/ -* Manual: http://TracksApp.github.com/tracks/ +* Manual: http://getontracks.org/manual/ * Source at GitHub: http://github.com/TracksApp/tracks/ * Assembla space (for bug reports and feature requests): http://www.assembla.com/spaces/tracks-tickets/tickets * Wiki (community contributed information): https://github.com/TracksApp/tracks/wiki -* Forum: http://getontracks.org/forums/ +* Forum (read-only): http://getontracks.org/forums/ * Mailing list: http://lists.rousette.org.uk/mailman/listinfo/tracks-discuss * Original developer: bsag (http://www.rousette.org.uk/) * Contributors: https://github.com/TracksApp/tracks/wiki/Contributors -* Version: 2.1devel -* Copyright: (cc) 2004-2011 rousette.org.uk. +* Version: 2.2devel +* Copyright: (cc) 2004-2012 rousette.org.uk. * License: GNU GPL -All the documentation for Tracks can be found within the /doc directory and at -http://bsag.github.com/tracks/ +More documentation for Tracks can be found within the /doc directory -The latter includes full instructions for both new installations and upgrades +The manual includes full instructions for both new installations and upgrades from older installations of Tracks. The instructions might appear long and intimidatingly complex, but that is @@ -37,12 +30,11 @@ you cannot find a solution to your problem. The wiki has a lot of user contributed installation HOWTOs for various webhosts, specific OS's and more. If you are thinking about contributing towards the development of Tracks, -please read /doc/README_DEVELOPERS for general information, or -/doc/tracks_api_wrapper.rb for information on Tracks' API. Also you can find -some information on development on the wiki. +please read /doc/README_DEVELOPERS for general information. Also you can find +some information on development, testing and contributing on the wiki. While fully usable for everyday use, Tracks is still a work in progress. Make sure that you take sensible precautions and back up all your data frequently, taking particular care when you are upgrading. -Enjoy being productive! +Enjoy being productive! \ No newline at end of file diff --git a/Rakefile b/Rakefile index 2704572a..af3b6bdc 100644 --- a/Rakefile +++ b/Rakefile @@ -1,16 +1,7 @@ +#!/usr/bin/env rake # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require(File.join(File.dirname(__FILE__), 'config', 'boot')) +require File.expand_path('../config/application', __FILE__) -require 'rake' -require 'rake/testtask' -require 'rake/rdoctask' - -require 'tasks/rails' - -begin - require 'test/rails/rake_tasks' -rescue LoadError => e - #It's ok if you don't have ZenTest installed if you're not a developer -end +Tracksapp::Application.load_tasks diff --git a/public/images/accept.png b/app/assets/images/accept.png similarity index 100% rename from public/images/accept.png rename to app/assets/images/accept.png diff --git a/public/images/add.png b/app/assets/images/add.png similarity index 100% rename from public/images/add.png rename to app/assets/images/add.png diff --git a/public/apple-touch-icon.png b/app/assets/images/apple-touch-icon.png similarity index 100% rename from public/apple-touch-icon.png rename to app/assets/images/apple-touch-icon.png diff --git a/public/images/arrows-ffffff.png b/app/assets/images/arrows-ffffff.png similarity index 100% rename from public/images/arrows-ffffff.png rename to app/assets/images/arrows-ffffff.png diff --git a/public/images/bigBlackWaiting.gif b/app/assets/images/bigBlackWaiting.gif similarity index 100% rename from public/images/bigBlackWaiting.gif rename to app/assets/images/bigBlackWaiting.gif diff --git a/public/images/bigWaiting.gif b/app/assets/images/bigWaiting.gif similarity index 100% rename from public/images/bigWaiting.gif rename to app/assets/images/bigWaiting.gif diff --git a/public/images/blackWaiting.gif b/app/assets/images/blackWaiting.gif similarity index 100% rename from public/images/blackWaiting.gif rename to app/assets/images/blackWaiting.gif diff --git a/public/images/blank.png b/app/assets/images/blank.png similarity index 100% rename from public/images/blank.png rename to app/assets/images/blank.png diff --git a/public/images/bottom_off.png b/app/assets/images/bottom_off.png similarity index 100% rename from public/images/bottom_off.png rename to app/assets/images/bottom_off.png diff --git a/public/images/bottom_on.png b/app/assets/images/bottom_on.png similarity index 100% rename from public/images/bottom_on.png rename to app/assets/images/bottom_on.png diff --git a/public/images/cancel.png b/app/assets/images/cancel.png similarity index 100% rename from public/images/cancel.png rename to app/assets/images/cancel.png diff --git a/public/images/close.gif b/app/assets/images/close.gif similarity index 100% rename from public/images/close.gif rename to app/assets/images/close.gif diff --git a/public/images/collapse.png b/app/assets/images/collapse.png similarity index 100% rename from public/images/collapse.png rename to app/assets/images/collapse.png diff --git a/public/images/construction.gif b/app/assets/images/construction.gif similarity index 100% rename from public/images/construction.gif rename to app/assets/images/construction.gif diff --git a/public/images/container-gradient.png b/app/assets/images/container-gradient.png similarity index 100% rename from public/images/container-gradient.png rename to app/assets/images/container-gradient.png diff --git a/public/images/defer_1.png b/app/assets/images/defer_1.png similarity index 100% rename from public/images/defer_1.png rename to app/assets/images/defer_1.png diff --git a/public/images/defer_1_off.png b/app/assets/images/defer_1_off.png similarity index 100% rename from public/images/defer_1_off.png rename to app/assets/images/defer_1_off.png diff --git a/app/assets/images/defer_2.png b/app/assets/images/defer_2.png new file mode 100644 index 00000000..c27b735b Binary files /dev/null and b/app/assets/images/defer_2.png differ diff --git a/app/assets/images/defer_2_off.png b/app/assets/images/defer_2_off.png new file mode 100644 index 00000000..d9ab3384 Binary files /dev/null and b/app/assets/images/defer_2_off.png differ diff --git a/app/assets/images/defer_3.png b/app/assets/images/defer_3.png new file mode 100644 index 00000000..7c8c68ee Binary files /dev/null and b/app/assets/images/defer_3.png differ diff --git a/app/assets/images/defer_3_off.png b/app/assets/images/defer_3_off.png new file mode 100644 index 00000000..3fee2133 Binary files /dev/null and b/app/assets/images/defer_3_off.png differ diff --git a/public/images/defer_7.png b/app/assets/images/defer_7.png similarity index 100% rename from public/images/defer_7.png rename to app/assets/images/defer_7.png diff --git a/public/images/defer_7_off.png b/app/assets/images/defer_7_off.png similarity index 100% rename from public/images/defer_7_off.png rename to app/assets/images/defer_7_off.png diff --git a/public/images/delete_off.png b/app/assets/images/delete_off.png similarity index 100% rename from public/images/delete_off.png rename to app/assets/images/delete_off.png diff --git a/public/images/delete_on.png b/app/assets/images/delete_on.png similarity index 100% rename from public/images/delete_on.png rename to app/assets/images/delete_on.png diff --git a/public/images/done.png b/app/assets/images/done.png similarity index 100% rename from public/images/done.png rename to app/assets/images/done.png diff --git a/public/images/down_off.png b/app/assets/images/down_off.png similarity index 100% rename from public/images/down_off.png rename to app/assets/images/down_off.png diff --git a/public/images/down_on.png b/app/assets/images/down_on.png similarity index 100% rename from public/images/down_on.png rename to app/assets/images/down_on.png diff --git a/public/images/downarrow.png b/app/assets/images/downarrow.png similarity index 100% rename from public/images/downarrow.png rename to app/assets/images/downarrow.png diff --git a/public/images/edit_off.png b/app/assets/images/edit_off.png similarity index 100% rename from public/images/edit_off.png rename to app/assets/images/edit_off.png diff --git a/public/images/edit_on.png b/app/assets/images/edit_on.png similarity index 100% rename from public/images/edit_on.png rename to app/assets/images/edit_on.png diff --git a/public/images/expand.png b/app/assets/images/expand.png similarity index 100% rename from public/images/expand.png rename to app/assets/images/expand.png diff --git a/public/favicon.ico b/app/assets/images/favicon.ico similarity index 100% rename from public/favicon.ico rename to app/assets/images/favicon.ico diff --git a/public/images/feed-icon.png b/app/assets/images/feed-icon.png similarity index 100% rename from public/images/feed-icon.png rename to app/assets/images/feed-icon.png diff --git a/public/images/grip.png b/app/assets/images/grip.png similarity index 100% rename from public/images/grip.png rename to app/assets/images/grip.png diff --git a/public/images/icon_delete.png b/app/assets/images/icon_delete.png similarity index 100% rename from public/images/icon_delete.png rename to app/assets/images/icon_delete.png diff --git a/public/images/menuarrow.gif b/app/assets/images/menuarrow.gif similarity index 100% rename from public/images/menuarrow.gif rename to app/assets/images/menuarrow.gif diff --git a/public/images/menustar.gif b/app/assets/images/menustar.gif similarity index 100% rename from public/images/menustar.gif rename to app/assets/images/menustar.gif diff --git a/public/images/menustar_small.gif b/app/assets/images/menustar_small.gif similarity index 100% rename from public/images/menustar_small.gif rename to app/assets/images/menustar_small.gif diff --git a/public/images/mobile_notes.png b/app/assets/images/mobile_notes.png similarity index 100% rename from public/images/mobile_notes.png rename to app/assets/images/mobile_notes.png diff --git a/public/images/new-action-gradient.png b/app/assets/images/new-action-gradient.png similarity index 100% rename from public/images/new-action-gradient.png rename to app/assets/images/new-action-gradient.png diff --git a/public/images/notes_off.png b/app/assets/images/notes_off.png similarity index 100% rename from public/images/notes_off.png rename to app/assets/images/notes_off.png diff --git a/public/images/notes_on.png b/app/assets/images/notes_on.png similarity index 100% rename from public/images/notes_on.png rename to app/assets/images/notes_on.png diff --git a/public/images/open-id-login-bg.gif b/app/assets/images/open-id-login-bg.gif similarity index 100% rename from public/images/open-id-login-bg.gif rename to app/assets/images/open-id-login-bg.gif diff --git a/app/assets/images/rails.png b/app/assets/images/rails.png new file mode 100644 index 00000000..d5edc04e Binary files /dev/null and b/app/assets/images/rails.png differ diff --git a/public/images/recurring16x16.png b/app/assets/images/recurring16x16.png similarity index 100% rename from public/images/recurring16x16.png rename to app/assets/images/recurring16x16.png diff --git a/public/images/recurring24x24.png b/app/assets/images/recurring24x24.png similarity index 100% rename from public/images/recurring24x24.png rename to app/assets/images/recurring24x24.png diff --git a/public/images/recurring_menu16x16.png b/app/assets/images/recurring_menu16x16.png similarity index 100% rename from public/images/recurring_menu16x16.png rename to app/assets/images/recurring_menu16x16.png diff --git a/public/images/recurring_menu24x24.png b/app/assets/images/recurring_menu24x24.png similarity index 100% rename from public/images/recurring_menu24x24.png rename to app/assets/images/recurring_menu24x24.png diff --git a/public/images/reviewed.png b/app/assets/images/reviewed.png similarity index 100% rename from public/images/reviewed.png rename to app/assets/images/reviewed.png diff --git a/public/images/shadow.png b/app/assets/images/shadow.png similarity index 100% rename from public/images/shadow.png rename to app/assets/images/shadow.png diff --git a/public/images/spinner.gif b/app/assets/images/spinner.gif similarity index 100% rename from public/images/spinner.gif rename to app/assets/images/spinner.gif diff --git a/public/images/staricons.png b/app/assets/images/staricons.png similarity index 100% rename from public/images/staricons.png rename to app/assets/images/staricons.png diff --git a/public/images/stats.gif b/app/assets/images/stats.gif similarity index 100% rename from public/images/stats.gif rename to app/assets/images/stats.gif diff --git a/public/images/successor_off.png b/app/assets/images/successor_off.png similarity index 100% rename from public/images/successor_off.png rename to app/assets/images/successor_off.png diff --git a/public/images/successor_on.png b/app/assets/images/successor_on.png similarity index 100% rename from public/images/successor_on.png rename to app/assets/images/successor_on.png diff --git a/public/images/system-search.png b/app/assets/images/system-search.png similarity index 100% rename from public/images/system-search.png rename to app/assets/images/system-search.png diff --git a/public/images/to_project_off.png b/app/assets/images/to_project_off.png similarity index 100% rename from public/images/to_project_off.png rename to app/assets/images/to_project_off.png diff --git a/public/images/top_off.png b/app/assets/images/top_off.png similarity index 100% rename from public/images/top_off.png rename to app/assets/images/top_off.png diff --git a/public/images/top_on.png b/app/assets/images/top_on.png similarity index 100% rename from public/images/top_on.png rename to app/assets/images/top_on.png diff --git a/public/images/trans70.png b/app/assets/images/trans70.png similarity index 100% rename from public/images/trans70.png rename to app/assets/images/trans70.png diff --git a/public/images/ui-anim_basic_16x16.gif b/app/assets/images/ui-anim_basic_16x16.gif similarity index 100% rename from public/images/ui-anim_basic_16x16.gif rename to app/assets/images/ui-anim_basic_16x16.gif diff --git a/public/images/up_off.png b/app/assets/images/up_off.png similarity index 100% rename from public/images/up_off.png rename to app/assets/images/up_off.png diff --git a/public/images/up_on.png b/app/assets/images/up_on.png similarity index 100% rename from public/images/up_on.png rename to app/assets/images/up_on.png diff --git a/public/images/waiting.gif b/app/assets/images/waiting.gif similarity index 100% rename from public/images/waiting.gif rename to app/assets/images/waiting.gif diff --git a/public/images/x-office-calendar.png b/app/assets/images/x-office-calendar.png similarity index 100% rename from public/images/x-office-calendar.png rename to app/assets/images/x-office-calendar.png diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 00000000..be027979 --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,30 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// +// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD +// GO AFTER THE REQUIRES BELOW. +// +//= require jquery +//= require jquery_ujs + +// Stuff in app/assets +//= require tracks.js + +// Stuff in vendor/assets +//= require jquery-ui-1.8.17.custom.min +//= require jquery.blockUI +//= require jquery.cookie +//= require jquery.form +//= require jquery.jeditable.mini +//= require jquery.simulate.drag-sortable +//= require jquery.truncator +//= require niftycube +//= require superfish +//= require supersubs +//= require swfobject \ No newline at end of file diff --git a/public/javascripts/application.js b/app/assets/javascripts/tracks.js similarity index 97% rename from public/javascripts/application.js rename to app/assets/javascripts/tracks.js index 5c2302f8..47a4a4be 100644 --- a/public/javascripts/application.js +++ b/app/assets/javascripts/tracks.js @@ -434,7 +434,7 @@ var TodoItems = { return confirm(i18n['contexts.new_context_pre'] + givenContextName + i18n['contexts.new_context_post']); }, generate_predecessor: function(todo_id, todo_spec) { - var img = ""; + var img = ""; var anchor = "" + img + ""; var li = "
  • "+ anchor +" "+ todo_spec + "
  • "; return li; @@ -534,12 +534,12 @@ var TodoItems = { }); $('.item-show').droppable({ drop: TodoItems.drop_todo, - tolerance: 'pointer', + tolerance: 'intersect', /* warning: selenium fails on drag_and_drop when this is 'pointer' */ hoverClass: 'hover' }); $('.context_target').droppable({ drop: TodoItems.drop_todo_on_context, - tolerance: 'pointer', + tolerance: 'intersect', /* warning: selenium fails on drag_and_drop when this is 'pointer' */ hoverClass: 'hover' }); }, @@ -610,7 +610,7 @@ var TodoItems = { return false; }); - /* delete button to delete a project from the list */ + /* delete button to delete a dependency from the list */ $('.item-container a.delete_dependency_button').live('click', function(evt){ var predecessor_id=$(this).attr("x_predecessors_id"); var ajax_options = default_ajax_options_for_scripts('DELETE', this.href, $(this).parents('.item-container')); @@ -726,9 +726,10 @@ var ProjectListPage = { var highlight = function(){ $('h2#project_name').effect('highlight', {}, 500); }; - $.post(relative_to_root('projects/update/'+project_id), { + $.post(relative_to_root('projects/'+project_id), { 'project[name]': value, - 'update_project_name': 'true' + 'update_project_name': 'true', + '_method': 'put' }, highlight, 'script'); return(value); }, @@ -770,8 +771,9 @@ var ProjectListPage = { /* set behavior for edit project settings link in both projects list page and project page */ $("a.project_edit_settings").live('click', function (evt) { - get_with_ajax_and_block_element(this.href, $(this).parent().parent()); - return false; + $(this).parent().parent().addClass('project-edit-current'); /* mark project in list */ + get_with_ajax_and_block_element(this.href, $(this).parent().parent()); + return false; }); /* submit project form after edit */ @@ -785,6 +787,7 @@ var ProjectListPage = { $('form.edit-project-form a.negative').live('click', function(){ $('div#project_name').editable('enable'); $(this).parents('.edit-form').fadeOut(200, function () { + $(this).parents('.project-edit-current').removeClass('project-edit-current'); $(this).parents('.list').find('.project').fadeIn(500); $(this).parents('.container').find('.item-show').fadeIn(500); }) @@ -806,7 +809,7 @@ var ProjectListPage = { /* make the three lists of project sortable */ $(['active', 'hidden', 'completed']).each(function() { $("#list-"+this+"-projects").sortable({ - handle: '.handle', + handle: '.grip', update: update_order }); }); @@ -841,8 +844,9 @@ var ContextListPage = { var highlight = function(){ $('div.context span#context_name').effect('highlight', {}, 500); }; - $.post(relative_to_root('contexts/update/'+context_id), { - 'context[name]': value + $.post(relative_to_root('contexts/'+context_id), { + 'context[name]': value, + '_method': 'put' }, highlight); return value; }, @@ -891,7 +895,7 @@ var ContextListPage = { /* make the two state lists of context sortable */ $(['active', 'hidden']).each(function() { $("#list-contexts-"+this).sortable({ - handle: '.handle', + handle: '.grip', update: update_order }) }); @@ -1014,8 +1018,14 @@ var RecurringTodosPage = { width: 750, modal: true, buttons: { - "Create": function() { submit_with_ajax_and_block_element('form.#recurring-todo-form-new-action', $(this).parents(".ui-dialog")); }, - Cancel: function() { $( this ).dialog( "close" ); } + create: { + text: i18n['common.create'], + click: function() { submit_with_ajax_and_block_element('form.#recurring-todo-form-new-action', $(this).parents(".ui-dialog")); }, + }, + cancel: { + text: i18n['common.cancel'], + click: function() { $( this ).dialog( "close" ); } + } }, show: "fade", hide: "fade", @@ -1041,11 +1051,14 @@ var RecurringTodosPage = { modal: true, buttons: { "Update": { - text: "Update", + text: i18n['common.update'], id: 'recurring_todo_edit_update_button', click: function() { submit_with_ajax_and_block_element('form#recurring-todo-form-edit-action', $(this).parents(".ui-dialog")); } }, - Cancel: function() { $( this ).dialog( "close" ); } + cancel: { + text: i18n['common.cancel'], + click: function() { $( this ).dialog( "close" ); } + } }, show: "fade", hide: "fade", @@ -1147,7 +1160,7 @@ $.fn.clearDeps = function() { /**************************************/ function generic_get_script_for_list(element, getter, param){ - $(element).load(relative_to_root(getter+'?'+param)); + $(element).load(relative_to_root(getter+'.js?'+param)); } function default_ajax_options_for_submit(ajax_type, element_to_block) { diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 00000000..c1705b19 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,14 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the top of the + * compiled file, but it's generally better to create a new file per style scope. + * + *= require_self + *= require tracks + *= require_tree ../../../vendor/assets/stylesheets +*/ diff --git a/public/stylesheets/mobile.css b/app/assets/stylesheets/mobile.css similarity index 67% rename from public/stylesheets/mobile.css rename to app/assets/stylesheets/mobile.css index f4795a55..cf05eea6 100644 --- a/public/stylesheets/mobile.css +++ b/app/assets/stylesheets/mobile.css @@ -1,22 +1,20 @@ body { font-family: Arial,Helvetica,sans-serif; - font-size: smaller; } #content { - margin-top: 50px; + margin-top: 4em; } div.footer { - font-size: XX-small; + font-size: small; color: #999999; text-align: center; - } +} a, a:link, a:active, a:visited { color: #CC3334; - padding-left: 1px; - padding-right: 1px; + padding: 0.25em; text-decoration: none; } @@ -25,27 +23,27 @@ a:hover { color: #FFFFFF; } -div.footer a { +div.footer a { text-decoration: underline; color: #999999; } h1 { color: #fff; - font-size: small; padding-top: 0.2em; padding-bottom: 0.2em; padding-left:8px; margin-top:0; margin-bottom:0; + font-size:medium; } h2 { background-color: #aaaaaa; - font-size : small; margin: .3em 0; padding: .3em 0 .1em .3em; border-top: 1px solid #777777; + font-size:medium; } h2 a, h2 a:link, h2 a:active, h2 a:visited { @@ -60,27 +58,27 @@ h2 a:hover { } h4.alert { - border: 1px solid #666666; + border: 1px solid #666666; text-align: center; } h4.warning { - border: 1px solid #ED2E38; + border: 1px solid #ED2E38; background-color: #F6979C; color: #000; } h4.error { - color:#fff; + color:#fff; background:#c00; } h4.notice { - border: 1px solid #007E00; + border: 1px solid #007E00; background-color: #c2ffc2; color: #007E00; } span.tag { - font-size: x-small; + font-size: small; background-color: #CCE7FF; color: #000; padding: 1px; @@ -88,12 +86,12 @@ span.tag { } span.r { - font-size: XX-small; + font-size: small; color: #777777; } span.prj, span.ctx{ - font-size: X-small; + font-size: small; } #ctx, #pjr { @@ -104,7 +102,7 @@ span.prj, span.ctx{ padding: 0.1em 0; } -/* Draw attention to some text +/* Draw attention to some text Same format as traffic lights */ .red { color: #fff; @@ -162,7 +160,7 @@ ul.c { } ul.c li { - padding: 0; + padding: 0.25em 0; margin: 0; } @@ -178,9 +176,7 @@ span.r { background-color: #000000; clear: both; color: #EEEEEE; - height: 45px; left: 0; - margin-bottom: 5px; position: fixed; top: 0; width: 100%; @@ -190,19 +186,16 @@ span.r { .nav { color: #fff; background: #000; - padding-top: 0.2em; - padding-bottom: 0.2em; -} - -#topbar .nav { - padding-left:8px; - margin-bottom:0.3em; + padding:0; + overflow:auto; + list-style:none; + margin:0; } .nav a, .nav a:link, .nav a:active, .nav a:visited { + background: #666; color: #fff; - padding-top: 1.0em; - padding-bottom: 0.5em; + padding: 0.5em; } .nav a:focus, .nav a:hover, .nav a:active { @@ -214,6 +207,17 @@ span.r { color: #CCCCCC; } +.nav li.link { + width:20%; + float:left; + outline:black solid 1px; +} +.nav .link a { + font-size:small; + text-align:center; + display:block; +} + #database_auth_form table td { width:7em; } @@ -221,10 +225,54 @@ span.r { table.c { margin-left: 5px; } + .mobile-done { display:inline; } -input#todo_description, input#tag_list, textarea#todo_notes, select#todo_project_id, select#todo_context_id { +input#todo_description, input#todo_tag_list, textarea#todo_notes, select#todo_project_id, select#todo_context_id { width: 95%; } + +.next-prev-project { + overflow:auto; + padding:0; + margin:0; + list-style:none; +} + +.prev, +.next { + float:left; + width:50%; +} + +.next { + float:right; +} + +.prev a, +.next a { + display:block; + height:1em; + overflow:hidden; +} + +.prev a { + background: url(assets/previous.png) left center no-repeat; + padding-left: 20px; +} + +.prev a:hover { + background: #cc3334 url(assets/previous.png) left center no-repeat; +} + +.next a { + text-align:right; + background: url(assets/next.png) right center no-repeat; + padding-right: 20px; +} + +.next a:hover { + background: #cc3334 url(assets/next.png) right center no-repeat; +} diff --git a/public/stylesheets/print.css b/app/assets/stylesheets/print.css similarity index 100% rename from public/stylesheets/print.css rename to app/assets/stylesheets/print.css diff --git a/public/stylesheets/scaffold.css b/app/assets/stylesheets/scaffold.css similarity index 92% rename from public/stylesheets/scaffold.css rename to app/assets/stylesheets/scaffold.css index 060a372a..1abda09f 100644 --- a/public/stylesheets/scaffold.css +++ b/app/assets/stylesheets/scaffold.css @@ -71,23 +71,23 @@ h4.alert { } h4.warning { - border: 1px solid #ED2E38; + border: 1px solid #ED2E38; background-color: #F6979C; color: #000000; } h4.error { - color:#fff; + color:#fff; background:#c00; } h4.notice { - border: 1px solid #007E00; + border: 1px solid #007E00; background-color: #c2ffc2; color: #007E00; } /*#notice { padding: 2px; - border: 1px solid #007E00; + border: 1px solid #007E00; background-color: #c2ffc2; color: #007E00; margin-bottom: 15px; @@ -147,8 +147,8 @@ input.login_text { width:200px; } input.open_id { - background: url(../images/open-id-login-bg.gif) no-repeat; - background-color: #fff; + background: url(/assets/open-id-login-bg.gif) no-repeat; + background-color: #fff; background-position: 0 50%; color: #000; padding-left: 18px; diff --git a/public/stylesheets/standard.css b/app/assets/stylesheets/tracks.css similarity index 89% rename from public/stylesheets/standard.css rename to app/assets/stylesheets/tracks.css index 337c1370..e2c4a38b 100644 --- a/public/stylesheets/standard.css +++ b/app/assets/stylesheets/tracks.css @@ -6,6 +6,10 @@ float:right; } +div.depends_on { + clear:both; +} + div.depends_on label { float: left; } @@ -102,35 +106,35 @@ h3 { /* Rules for the icon links */ -img.edit_item {background-image: url(../images/edit_off.png); background-repeat: no-repeat; border: none;} -a:hover img.edit_item {background-image: url(../images/edit_on.png); background-color: transparent; background-repeat: no-repeat; border: none;} +img.edit_item {background-image: url(/assets/edit_off.png); background-repeat: no-repeat; border: none;} +a:hover img.edit_item {background-image: url(/assets/edit_on.png); background-color: transparent; background-repeat: no-repeat; border: none;} -img.delete_item {background-image: url(../images/delete_off.png); background-repeat: no-repeat; border: none;} -a:hover img.delete_item {background-image: url(../images/delete_on.png);background-color: transparent;background-repeat: no-repeat; border: none;} +img.delete_item {background-image: url(/assets/delete_off.png); background-repeat: no-repeat; border: none;} +a:hover img.delete_item {background-image: url(/assets/delete_on.png);background-color: transparent;background-repeat: no-repeat; border: none;} a.undecorated_link {background-color:transparent;color:transparent;} -img.todo_star {background-image: url(../images/staricons.png); background-repeat: no-repeat; border:none; background-position: -32px 0px;} +img.todo_star {background-image: url(/assets/staricons.png); background-repeat: no-repeat; border:none; background-position: -32px 0px;} img.todo_star.starred{ background-position: 0px 0px; } a:hover img.todo_star { background-position: -48px 0px;} a:hover img.todo_star.starred { background-position: -16px 0px; } -a.to_top {background: transparent url(../images/top_off.png) no-repeat;} -a.to_top:hover {background: transparent url(../images/top_on.png) no-repeat;} +a.to_top {background: transparent url(/assets/top_off.png) no-repeat;} +a.to_top:hover {background: transparent url(/assets/top_on.png) no-repeat;} -a.up {background: transparent url(../images/up_off.png) no-repeat;} -a.up:hover {background: transparent url(../images/up_on.png) no-repeat;} +a.up {background: transparent url(/assets/up_off.png) no-repeat;} +a.up:hover {background: transparent url(/assets/up_on.png) no-repeat;} -a.down {background: transparent url(../images/down_off.png) no-repeat;} -a.down:hover {background: transparent url(../images/down_on.png) no-repeat;} +a.down {background: transparent url(/assets/down_off.png) no-repeat;} +a.down:hover {background: transparent url(/assets/down_on.png) no-repeat;} -a.to_bottom {background: transparent url(../images/bottom_off.png) no-repeat;} -a.to_bottom:hover {background: transparent url(../images/bottom_on.png) no-repeat;} +a.to_bottom {background: transparent url(/assets/bottom_off.png) no-repeat;} +a.to_bottom:hover {background: transparent url(/assets/bottom_on.png) no-repeat;} -a.show_notes, a.link_to_notes {background-image: url(../images/notes_off.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} -a.show_notes:hover, a.link_to_notes:hover {background-image: url(../images/notes_on.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} +a.show_notes, a.link_to_notes {background-image: url(/assets/notes_off.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} +a.show_notes:hover, a.link_to_notes:hover {background-image: url(/assets/notes_on.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} -a.show_successors, a.link_to_successors {background-image: url(../images/successor_off.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} -a.show_successors:hover, a.link_to_successors:hover {background-image: url(../images/successor_on.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} +a.show_successors, a.link_to_successors {background-image: url(/assets/successor_off.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} +a.show_successors:hover, a.link_to_successors:hover {background-image: url(/assets/successor_on.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;} /* Structural divs */ @@ -251,7 +255,7 @@ a.show_successors:hover, a.link_to_successors:hover {background-image: url(../im #develop-notify-bar { line-height:0.5; - background-image: url(/images/construction.gif); + background-image: url(/assets/construction.gif); background-repeat: repeat-x; } @@ -361,7 +365,7 @@ div#input_box { } .selected_predecessors { - clear:right; + clear:both; } form.new_todo_form ul.predecessor_list, @@ -380,8 +384,8 @@ ul.predecessor_list li { } /* deleting dependency from new form of a todo */ -img.icon_delete_dep {width: 10px; background-image: url(../images/icon_delete.png); background-repeat: no-repeat; background-position: -9px 0; border: none; color:black;} -a:hover img.icon_delete_dep {width: 10px; background-image: url(../images/icon_delete.png); background-repeat: no-repeat; background-position: 0 0; border: none; color:black; background-color: black;} +img.icon_delete_dep {width: 10px; background-image: url(/assets/icon_delete.png); background-repeat: no-repeat; background-position: -9px 0; border: none; color:black;} +a:hover img.icon_delete_dep {width: 10px; background-image: url(/assets/icon_delete.png); background-repeat: no-repeat; background-position: 0 0; border: none; color:black; background-color: black;} a.icon_delete_dep:hover {width: 10px; background-color: black;} /* deleting dependency from edit form of a todo */ @@ -426,13 +430,13 @@ input.item-checkbox { vertical-align: middle; } -.rec_description, .description { +.rec_description, .description, .project_description, .context_description { margin-left: 80px; position:relative; } -.rec_description { - margin-left: 80px; +.project_description, .context_description { + margin-left: 50px; } .stale_l1 { @@ -546,6 +550,10 @@ div.note_wrapper p { display: inline; } +div.note_note { + margin-left:24px; +} + div.note_footer { border-top: 1px solid #999; padding-top: 3px; @@ -857,6 +865,7 @@ form { padding: 10px; margin: 0px; } + .inline-form { border: none; padding: 3px; @@ -1152,7 +1161,7 @@ ul#prefs {list-style-type: disc; margin-left: 15px;} } input.open_id { - background: url(../images/open-id-login-bg.gif) no-repeat; + background: url(/assets/open-id-login-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; color: #000; @@ -1304,7 +1313,7 @@ button.positive, .widgets a.positive{ } .blockUI.blockOverlay { - background-image:url('../images/waiting.gif'); + background-image:url('/assets/waiting.gif'); background-repeat:no-repeat; background-position:center center; background-color:white; @@ -1312,21 +1321,21 @@ button.positive, .widgets a.positive{ } .bigWaiting { - background-image:url('../images/bigWaiting.gif'); + background-image:url('/assets/bigWaiting.gif'); background-repeat:no-repeat; background-position:center 20%; background-color:white; } .blackWaiting { - background-image:url('../images/blackWaiting.gif'); + background-image:url('/assets/blackWaiting.gif'); background-repeat:no-repeat; background-position:center center; background-color:black; } .bigBlackWaiting { - background-image:url('../images/bigBlackWaiting.gif'); + background-image:url('/assets/bigBlackWaiting.gif'); background-repeat:no-repeat; background-position:center center; background-color:black; @@ -1415,9 +1424,9 @@ div.auto_complete ul strong.highlight { } .ui-datepicker { - z-index: 1000; + z-index: 1000 !important; } .ui-autocomplete-loading { - background: white url('/images/ui-anim_basic_16x16.gif') right center no-repeat; + background: white url('/assets/ui-anim_basic_16x16.gif') right center no-repeat; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f2c55bb3..152f68d2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -9,12 +9,11 @@ class ApplicationController < ActionController::Base protect_from_forgery - helper :application include LoginSystem helper_method :current_user, :prefs, :format_date, :markdown layout proc{ |controller| controller.mobile? ? "mobile" : "standard" } - exempt_from_layout /\.js\.erb$/ + # exempt_from_layout /\.js\.erb$/ before_filter :check_for_deprecated_password_hash before_filter :set_session_expiration @@ -73,32 +72,19 @@ class ApplicationController < ActionController::Base render :text => message, :status => status end - # def rescue_action(exception) - # log_error(exception) if logger - # respond_to do |format| - # format.html do - # notify :warning, "An error occurred on the server." - # render :action => "index" - # end - # format.js { render :action => 'error' } - # format.xml { render :text => 'An error occurred on the server.' + $! } - # end - # end - # Returns a count of next actions in the given context or project The result # is count and a string descriptor, correctly pluralised if there are no # actions or multiple actions # - def count_undone_todos_phrase(todos_parent, string="actions") + def count_undone_todos_phrase(todos_parent) count = count_undone_todos(todos_parent) deferred_count = count_deferred_todos(todos_parent) if count == 0 && deferred_count > 0 - word = deferred_count == 1 ? string.singularize : string.pluralize - word = "deferred " + word - deferred_count.to_s + " " + word + word = "#{I18n.t('common.deferred')} #{I18n.t('common.actions_midsentence', :count => deferred_count)}" + return "#{deferred_count.to_s} #{word}".html_safe else - word = count == 1 ? string.singularize : string.pluralize - count.to_s + " " + word + word = I18n.t('common.actions_midsentence', :count => count) + return "#{count} #{word}".html_safe end end @@ -143,14 +129,6 @@ class ApplicationController < ActionController::Base return json_elems end - # Uses RedCloth to transform text using either Textile or Markdown Need to - # require redcloth above RedCloth 3.0 or greater is needed to use Markdown, - # otherwise it only handles Textile - # - def markdown(text) - RedCloth.new(text).to_html - end - # Here's the concept behind this "mobile content negotiation" hack: In # addition to the main, AJAXy Web UI, Tracks has a lightweight low-feature # 'mobile' version designed to be suitablef or use from a phone or PDA. It @@ -211,6 +189,10 @@ class ApplicationController < ActionController::Base super # handle xml http auth via our own login code end end + + def sanitize(arg) + ActionController::Base.helpers.sanitize(arg) + end protected @@ -223,7 +205,7 @@ class ApplicationController < ActionController::Base def redirect_back_or_home respond_to do |format| - format.html { redirect_back_or_default home_url } + format.html { redirect_back_or_default root_url } format.m { redirect_back_or_default mobile_url } end end @@ -260,24 +242,26 @@ class ApplicationController < ActionController::Base self.class.prefered_auth? end + # all completed todos [today@00:00, today@now] def get_done_today(completed_todos, includes = {:include => Todo::DEFAULT_INCLUDES}) start_of_this_day = Time.zone.now.beginning_of_day completed_todos.completed_after(start_of_this_day).all(includes) end + # all completed todos [begin_of_week, start_of_today] def get_done_this_week(completed_todos, includes = {:include => Todo::DEFAULT_INCLUDES}) start_of_this_week = Time.zone.now.beginning_of_week start_of_this_day = Time.zone.now.beginning_of_day - completed_todos.completed_after(start_of_this_week).completed_before(start_of_this_day).all(includes) + completed_todos.completed_before(start_of_this_day).completed_after(start_of_this_week).all(includes) end + # all completed todos [begin_of_month, begin_of_week] def get_done_this_month(completed_todos, includes = {:include => Todo::DEFAULT_INCLUDES}) start_of_this_month = Time.zone.now.beginning_of_month start_of_this_week = Time.zone.now.beginning_of_week - completed_todos.completed_after(start_of_this_month).completed_before(start_of_this_week).all(includes) + completed_todos.completed_before(start_of_this_week).completed_after(start_of_this_month).all(includes) end - private def parse_date_per_user_prefs( s ) diff --git a/app/controllers/application_controller.rb.rails2 b/app/controllers/application_controller.rb.rails2 new file mode 100644 index 00000000..761be1d9 --- /dev/null +++ b/app/controllers/application_controller.rb.rails2 @@ -0,0 +1,324 @@ +# The filters added to this controller will be run for all controllers in the +# application. Likewise will all the methods added be available for all +# controllers. + +require_dependency "login_system" +require_dependency "tracks/source_view" + +class ApplicationController < ActionController::Base + + protect_from_forgery + + helper :application + include LoginSystem + helper_method :current_user, :prefs, :format_date + + layout proc{ |controller| controller.mobile? ? "mobile" : "standard" } + exempt_from_layout /\.js\.erb$/ + + before_filter :check_for_deprecated_password_hash + before_filter :set_session_expiration + before_filter :set_time_zone + before_filter :set_zindex_counter + before_filter :set_locale + prepend_before_filter :login_required + prepend_before_filter :enable_mobile_content_negotiation + after_filter :set_charset + + # By default, sets the charset to UTF-8 if it isn't already set + def set_charset + headers["Content-Type"] ||= "text/html; charset=UTF-8" + end + + def set_locale + locale = params[:locale] # specifying a locale in the request takes precedence + locale = locale || prefs.locale unless current_user.nil? # otherwise, the locale of the currently logged in user takes over + locale = locale || request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first if request.env['HTTP_ACCEPT_LANGUAGE'] + I18n.locale = locale.nil? ? I18n.default_locale : (I18n::available_locales.include?(locale.to_sym) ? locale : I18n.default_locale) + logger.debug("Selected '#{I18n.locale}' as locale") + end + + def set_session_expiration + # http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions + unless session == nil + return if self.controller_name == 'feed' or session['noexpiry'] == "on" + # If the method is called by the feed controller (which we don't have + # under session control) or if we checked the box to keep logged in on + # login don't set the session expiry time. + 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 + + # Redirects to change_password_user_path if the current user uses a + # deprecated password hashing algorithm. + def check_for_deprecated_password_hash + if current_user and current_user.uses_deprecated_password? + notify :warning, t('users.you_have_to_reset_your_password') + redirect_to change_password_user_path current_user + end + end + + def render_failure message, status = 404 + render :text => message, :status => status + end + + # def rescue_action(exception) + # log_error(exception) if logger + # respond_to do |format| + # format.html do + # notify :warning, "An error occurred on the server." + # render :action => "index" + # end + # format.js { render :action => 'error' } + # format.xml { render :text => 'An error occurred on the server.' + $! } + # end + # end + + # Returns a count of next actions in the given context or project The result + # is count and a string descriptor, correctly pluralised if there are no + # actions or multiple actions + # + def count_undone_todos_phrase(todos_parent) + count = count_undone_todos(todos_parent) + deferred_count = count_deferred_todos(todos_parent) + if count == 0 && deferred_count > 0 + word = I18n.t('common.actions_midsentence', :count => deferred_count) + word = I18n.t('common.deferred') + " " + word + return deferred_count.to_s + " " + word + else + word = I18n.t('common.actions_midsentence', :count => count) + return count.to_s + " " + word + end + end + + def count_undone_todos(todos_parent) + if todos_parent.nil? + count = 0 + elsif (todos_parent.is_a?(Project) && todos_parent.hidden?) + count = eval "@project_project_hidden_todo_counts[#{todos_parent.id}]" + else + count = eval "@#{todos_parent.class.to_s.downcase}_not_done_counts[#{todos_parent.id}]" + end + count || 0 + end + + def count_deferred_todos(todos_parent) + if todos_parent.nil? + count = 0 + else + count = todos_parent.todos.deferred.count + end + end + + # Convert a date object to the format specified in the user's preferences in + # config/settings.yml + # + def format_date(date) + return date ? date.in_time_zone(prefs.time_zone).strftime("#{prefs.date_format}") : '' + end + + def for_autocomplete(coll, substr) + if substr # protect agains empty request + filtered = coll.find_all{|item| item.name.downcase.include? substr.downcase} + json_elems = Array[*filtered.map{ |e| {:id => e.id.to_s, :value => e.name} }].to_json + return json_elems + else + return "" + end + end + + def format_dependencies_as_json_for_auto_complete(entries) + json_elems = Array[*entries.map{ |e| {:value => e.id.to_s, :label => e.specification} }].to_json + return json_elems + end + + # Here's the concept behind this "mobile content negotiation" hack: In + # addition to the main, AJAXy Web UI, Tracks has a lightweight low-feature + # 'mobile' version designed to be suitablef or use from a phone or PDA. It + # makes some sense that tne pages of that mobile version are simply alternate + # representations of the same Todo resources. The implementation goal was to + # treat mobile as another format and be able to use respond_to to render both + # versions. Unfortunately, I ran into a lot of trouble simply registering a + # new mime type 'text/html' with format :m because :html already is linked to + # that mime type and the new registration was forcing all html requests to be + # rendered in the mobile view. The before_filter and after_filter hackery + # below accomplishs that implementation goal by using a 'fake' mime type + # during the processing and then setting it to 'text/html' in an + # 'after_filter' -LKM 2007-04-01 + def mobile? + return params[:format] == 'm' + end + + def enable_mobile_content_negotiation + if mobile? + request.format = :m + end + end + + def create_todo_from_recurring_todo(rt, date=nil) + # create todo and initialize with data from recurring_todo rt + todo = current_user.todos.build( { :description => rt.description, :notes => rt.notes, :project_id => rt.project_id, :context_id => rt.context_id}) + todo.recurring_todo_id = rt.id + + # set dates + todo.due = rt.get_due_date(date) + + show_from_date = rt.get_show_from_date(date) + if show_from_date.nil? + todo.show_from=nil + else + # make sure that show_from is not in the past + todo.show_from = show_from_date < Time.zone.now ? nil : show_from_date + end + + saved = todo.save + if saved + todo.tag_with(rt.tag_list) + todo.tags.reload + end + + # increate number of occurences created from recurring todo + rt.inc_occurences + + # mark recurring todo complete if there are no next actions left + checkdate = todo.due.nil? ? todo.show_from : todo.due + rt.toggle_completion! unless rt.has_next_todo(checkdate) + + return saved ? todo : nil + end + + def handle_unverified_request + unless request.format=="application/xml" + super # handle xml http auth via our own login code + end + end + + protected + + def admin_login_required + unless User.find_by_id_and_is_admin(session['user_id'], true) + render :text => t('errors.user_unauthorized'), :status => 401 + return false + end + end + + def redirect_back_or_home + respond_to do |format| + format.html { redirect_back_or_default home_url } + format.m { redirect_back_or_default mobile_url } + end + end + + def boolean_param(param_name) + return false if param_name.blank? + s = params[param_name] + return false if s.blank? || s == false || s =~ /^false$/i + return true if s == true || s =~ /^true$/i + raise ArgumentError.new("invalid value for Boolean: \"#{s}\"") + end + + def self.openid_enabled? + Tracks::Config.openid_enabled? + end + + def openid_enabled? + self.class.openid_enabled? + end + + def self.cas_enabled? + Tracks::Config.cas_enabled? + end + + def cas_enabled? + self.class.cas_enabled? + end + + def self.prefered_auth? + Tracks::Config.prefered_auth? + end + + def prefered_auth? + self.class.prefered_auth? + end + + # all completed todos [today@00:00, today@now] + def get_done_today(completed_todos, includes = {:include => Todo::DEFAULT_INCLUDES}) + start_of_this_day = Time.zone.now.beginning_of_day + completed_todos.completed_after(start_of_this_day).all(includes) + end + + # all completed todos [begin_of_week, start_of_today] + def get_done_this_week(completed_todos, includes = {:include => Todo::DEFAULT_INCLUDES}) + start_of_this_week = Time.zone.now.beginning_of_week + start_of_this_day = Time.zone.now.beginning_of_day + completed_todos.completed_before(start_of_this_day).completed_after(start_of_this_week).all(includes) + end + + # all completed todos [begin_of_month, begin_of_week] + def get_done_this_month(completed_todos, includes = {:include => Todo::DEFAULT_INCLUDES}) + start_of_this_month = Time.zone.now.beginning_of_month + start_of_this_week = Time.zone.now.beginning_of_week + completed_todos.completed_before(start_of_this_week).completed_after(start_of_this_month).all(includes) + end + + private + + def parse_date_per_user_prefs( s ) + prefs.parse_date(s) + end + + def init_data_for_sidebar + @completed_projects = current_user.projects.completed + @hidden_projects = current_user.projects.hidden + @active_projects = current_user.projects.active + + @active_contexts = current_user.contexts.active + @hidden_contexts = current_user.contexts.hidden + + init_not_done_counts + if prefs.show_hidden_projects_in_sidebar + init_project_hidden_todo_counts(['project']) + end + end + + def init_not_done_counts(parents = ['project','context']) + parents.each do |parent| + eval("@#{parent}_not_done_counts = @#{parent}_not_done_counts || current_user.todos.active.count(:group => :#{parent}_id)") + end + end + + def init_project_hidden_todo_counts(parents = ['project','context']) + parents.each do |parent| + eval("@#{parent}_project_hidden_todo_counts = @#{parent}_project_hidden_todo_counts || current_user.todos.count(:conditions => ['state = ? or state = ?', 'project_hidden', 'active'], :group => :#{parent}_id)") + end + end + + # Set the contents of the flash message from a controller Usage: notify + # :warning, "This is the message" Sets the flash of type 'warning' to "This is + # the message" + def notify(type, message) + flash[type] = message + logger.error("ERROR: #{message}") if type == :error + end + + def set_time_zone + Time.zone = current_user.prefs.time_zone if logged_in? + end + + def set_zindex_counter + # this counter can be used to handle the IE z-index bug + @z_index_counter = 500 + end + +end diff --git a/app/controllers/contexts_controller.rb b/app/controllers/contexts_controller.rb index 7d8a14ea..2aabfc84 100644 --- a/app/controllers/contexts_controller.rb +++ b/app/controllers/contexts_controller.rb @@ -9,32 +9,36 @@ class ContextsController < ApplicationController prepend_before_filter :login_or_feed_token_required, :only => [:index] def index - # #true is passed here to force an immediate load so that size and empty? - # checks later don't result in separate SQL queries - @active_contexts = current_user.contexts.active(true) - @hidden_contexts = current_user.contexts.hidden(true) + @active_contexts = current_user.contexts.active + @hidden_contexts = current_user.contexts.hidden @new_context = current_user.contexts.build + init_not_done_counts(['context']) # save all contexts here as @new_context will add an empty one to current_user.contexts @all_contexts = @active_contexts + @hidden_contexts @count = @all_contexts.size - init_not_done_counts(['context']) - respond_to do |format| format.html &render_contexts_html format.m &render_contexts_mobile format.xml { render :xml => @all_contexts.to_xml( :except => :user_id ) } - format.rss &render_contexts_rss_feed - format.atom &render_contexts_atom_feed + format.rss do + @feed_title = 'Tracks Contexts' + @feed_description = "Lists all the contexts for #{current_user.display_name}" + end + format.atom do + @feed_title = 'Tracks Contexts' + @feed_description = "Lists all the contexts for #{current_user.display_name}" + end format.text do - @all_contexts = current_user.contexts.all + # somehow passing Mime::TEXT using content_type to render does not work + headers['Content-Type']=Mime::TEXT.to_s render :action => 'index', :layout => false, :content_type => Mime::TEXT end - format.autocomplete { render :text => for_autocomplete(@active_contexts + @hidden_contexts, params[:term])} + format.autocomplete &render_autocomplete end end - + def show @contexts = current_user.contexts(true) if @context.nil? @@ -51,24 +55,13 @@ class ContextsController < ApplicationController end end end - - # Example XML usage: curl -H 'Accept: application/xml' -H 'Content-Type: - # application/xml' - # -u username:password - # -d 'new context_name' - # http://our.tracks.host/contexts - # + def create if params[:format] == 'application/xml' && params['exception'] - render_failure "Expected post format is valid xml like so: context name.", 400 + render_failure "Expected post format is valid xml like so: context name.", 400 return end - @context = current_user.contexts.build - params_are_invalid = true - if (params['context'] || (params['request'] && params['request']['context'])) - @context.attributes = params['context'] || params['request']['context'] - params_are_invalid = false - end + @context = current_user.contexts.build(params['context']) @saved = @context.save @context_not_done_counts = { @context.id => 0 } respond_to do |format| @@ -76,10 +69,8 @@ class ContextsController < ApplicationController @down_count = current_user.contexts.size end format.xml do - if @context.new_record? && params_are_invalid - render_failure "Expected post format is valid xml like so: context name.", 400 - elsif @context.new_record? - render_failure @context.errors.to_xml, 409 + if @context.new_record? + render_failure @context.errors.to_xml.html_safe, 409 else head :created, :location => context_url(@context) end @@ -218,8 +209,8 @@ class ContextsController < ApplicationController @active_contexts = current_user.contexts.active @hidden_contexts = current_user.contexts.hidden @down_count = @active_contexts.size + @hidden_contexts.size - cookies[:mobile_url]= {:value => request.request_uri, :secure => SITE_CONFIG['secure_cookies']} - render :action => 'index_mobile' + cookies[:mobile_url]= {:value => request.fullpath, :secure => SITE_CONFIG['secure_cookies']} + render end end @@ -228,24 +219,18 @@ class ContextsController < ApplicationController @page_title = "TRACKS::List actions in "+@context.name @not_done = @not_done_todos.select {|t| t.context_id == @context.id } @down_count = @not_done.size - cookies[:mobile_url]= {:value => request.request_uri, :secure => SITE_CONFIG['secure_cookies']} + cookies[:mobile_url]= {:value => request.fullpath, :secure => SITE_CONFIG['secure_cookies']} @mobile_from_context = @context.id - render :action => 'mobile_show_context' + render end end - - def render_contexts_rss_feed + + def render_autocomplete lambda do - render_rss_feed_for current_user.contexts.all, :feed => feed_options, - :item => { :description => lambda { |c| @template.summary(c, count_undone_todos_phrase(c)) } } - end - end - - def render_contexts_atom_feed - lambda do - render_atom_feed_for current_user.contexts.all, :feed => feed_options, - :item => { :description => lambda { |c| @template.summary(c, count_undone_todos_phrase(c)) }, - :author => lambda { |c| nil } } + # first get active contexts with todos then those without + filled_contexts = @active_contexts.reject { |ctx| ctx.todos.count == 0 } + @hidden_contexts.reject { |ctx| ctx.todos.count == 0 } + empty_contexts = @active_contexts.find_all { |ctx| ctx.todos.count == 0 } + @hidden_contexts.find_all { |ctx| ctx.todos.count == 0 } + render :text => for_autocomplete(filled_contexts + empty_contexts, params[:term]) end end @@ -254,7 +239,7 @@ class ContextsController < ApplicationController end def set_context_from_params - @context = current_user.contexts.find_by_params(params) + @context = current_user.contexts.find(params[:id]) rescue @context = nil end @@ -267,11 +252,8 @@ class ContextsController < ApplicationController def init_todos set_context_from_params unless @context.nil? - @context.todos.send :with_scope, :find => { :include => Todo::DEFAULT_INCLUDES } do - @done = @context.done_todos - end - @max_completed = current_user.prefs.show_number_completed + @done = @context.todos.completed.all(:limit => @max_completed) # @not_done_todos = @context.not_done_todos TODO: Temporarily doing this # search manually until I can work out a way to do the same thing using @@ -281,9 +263,12 @@ class ContextsController < ApplicationController :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", :include => Todo::DEFAULT_INCLUDES) + @deferred = @context.todos.deferred(:include => Todo::DEFAULT_INCLUDES) + @pending = @context.todos.pending(:include => Todo::DEFAULT_INCLUDES) + @projects = current_user.projects - @count = @not_done_todos.size + @count = @not_done_todos.count + @deferred.count + @pending.count end end diff --git a/app/controllers/data_controller.rb b/app/controllers/data_controller.rb index 3e87a489..a38726a3 100644 --- a/app/controllers/data_controller.rb +++ b/app/controllers/data_controller.rb @@ -18,9 +18,9 @@ class DataController < ApplicationController def yaml_export all_tables = {} - all_tables['todos'] = current_user.todos.find(:all, :include => [:tags]) - all_tables['contexts'] = current_user.contexts.find(:all) - all_tables['projects'] = current_user.projects.find(:all) + all_tables['todos'] = current_user.todos.includes(:tags).all + all_tables['contexts'] = current_user.contexts.all + all_tables['projects'] = current_user.projects.all todo_tag_ids = Tag.find_by_sql([ "SELECT DISTINCT tags.id "+ @@ -34,13 +34,13 @@ class DataController < ApplicationController "WHERE recurring_todos.user_id=? "+ "AND tags.id = taggings.tag_id " + "AND taggings.taggable_id = recurring_todos.id ", current_user.id]) - tags = Tag.find(:all, :conditions => ["id IN (?) OR id IN (?)", todo_tag_ids, rec_todo_tag_ids]) - taggings = Tagging.find(:all, :conditions => ["tag_id IN (?) OR tag_id IN(?)", todo_tag_ids, rec_todo_tag_ids]) + tags = Tag.where("id IN (?) OR id IN (?)", todo_tag_ids, rec_todo_tag_ids) + taggings = Tagging.where("tag_id IN (?) OR tag_id IN(?)", todo_tag_ids, rec_todo_tag_ids) - all_tables['tags'] = tags - all_tables['taggings'] = taggings - all_tables['notes'] = current_user.notes.find(:all) - all_tables['recurring_todos'] = current_user.recurring_todos.find(:all) + all_tables['tags'] = tags.all + all_tables['taggings'] = taggings.all + all_tables['notes'] = current_user.notes.all + all_tables['recurring_todos'] = current_user.recurring_todos.all result = all_tables.to_yaml result.gsub!(/\n/, "\r\n") # TODO: general functionality for line endings @@ -53,7 +53,7 @@ class DataController < ApplicationController csv << ["id", "Context", "Project", "Description", "Notes", "Tags", "Created at", "Due", "Completed at", "User ID", "Show from", "state"] - current_user.todos.find(:all, :include => [:context, :project]).each do |todo| + current_user.todos.include(:context, :project).all.each do |todo| csv << [todo.id, todo.context.name, todo.project_id.nil? ? "" : todo.project.name, todo.description, @@ -72,19 +72,19 @@ class DataController < ApplicationController def csv_notes content_type = 'text/csv' - CSV::Writer.generate(result = "") do |csv| + CSV.generate(result = "") do |csv| csv << ["id", "User ID", "Project", "Note", "Created at", "Updated at"] # had to remove project include because it's association order is leaking # through and causing an ambiguous column ref even with_exclusive_scope # didn't seem to help -JamesKebinger - current_user.notes.find(:all,:order=>"notes.created_at").each do |note| + current_user.notes.reorder("notes.created_at").each do |note| # Format dates in ISO format for easy sorting in spreadsheet Print # context and project names for easy viewing - csv << [note.id, note.user_id, + csv << [note.id, note.user_id, note.project_id = note.project_id.nil? ? "" : note.project.name, note.body, note.created_at.to_formatted_s(:db), - note.updated_at.to_formatted_s(:db)] + note.updated_at.to_formatted_s(:db)] end end send_data(result, :filename => "notes.csv", :type => content_type) @@ -103,17 +103,17 @@ class DataController < ApplicationController "WHERE recurring_todos.user_id=? "+ "AND tags.id = taggings.tag_id " + "AND taggings.taggable_id = recurring_todos.id ", current_user.id]) - tags = Tag.find(:all, :conditions => ["id IN (?) OR id IN (?)", todo_tag_ids, rec_todo_tag_ids]) - taggings = Tagging.find(:all, :conditions => ["tag_id IN (?) OR tag_id IN(?)", todo_tag_ids, rec_todo_tag_ids]) + tags = Tag.where("id IN (?) OR id IN (?)", todo_tag_ids, rec_todo_tag_ids) + taggings = Tagging.where("tag_id IN (?) OR tag_id IN(?)", todo_tag_ids, rec_todo_tag_ids) result = "" - result << current_user.todos.find(:all).to_xml(:skip_instruct => true) - result << current_user.contexts.find(:all).to_xml(:skip_instruct => true) - result << current_user.projects.find(:all).to_xml(:skip_instruct => true) + result << current_user.todos.to_xml(:skip_instruct => true) + result << current_user.contexts.to_xml(:skip_instruct => true) + result << current_user.projects.to_xml(:skip_instruct => true) result << tags.to_xml(:skip_instruct => true) result << taggings.to_xml(:skip_instruct => true) - result << current_user.notes.find(:all).to_xml(:skip_instruct => true) - result << current_user.recurring_todos.find(:all).to_xml(:skip_instruct => true) + result << current_user.notes.to_xml(:skip_instruct => true) + result << current_user.recurring_todos.to_xml(:skip_instruct => true) result << "" send_data(result, :filename => "tracks_data.xml", :type => 'text/xml') end @@ -126,7 +126,7 @@ class DataController < ApplicationController def adjust_time(timestring) if (timestring=='') or ( timestring == nil) return nil - else + else return Time.parse(timestring + 'UTC') end end @@ -186,7 +186,7 @@ class DataController < ApplicationController case item.ivars['attributes']['state'] when 'active' then newitem.activate! when 'project_hidden' then newitem.hide! - when 'completed' + when 'completed' newitem.complete! newitem.completed_at = adjust_time(item.ivars['attributes']['completed_at']) when 'deferred' then newitem.defer! diff --git a/app/controllers/feedlist_controller.rb b/app/controllers/feedlist_controller.rb index 16d1ddc7..f0e0e991 100644 --- a/app/controllers/feedlist_controller.rb +++ b/app/controllers/feedlist_controller.rb @@ -6,7 +6,7 @@ class FeedlistController < ApplicationController @page_title = 'TRACKS::Feeds' unless mobile? - init_data_for_sidebar + init_data_for_sidebar else @projects = current_user.projects @contexts = current_user.contexts @@ -21,7 +21,7 @@ class FeedlistController < ApplicationController respond_to do |format| format.html { render :layout => 'standard' } - format.m { render :action => 'mobile_index' } + format.m end end diff --git a/app/controllers/integrations_controller.rb b/app/controllers/integrations_controller.rb index 70c93b17..002b3d34 100644 --- a/app/controllers/integrations_controller.rb +++ b/app/controllers/integrations_controller.rb @@ -27,7 +27,8 @@ class IntegrationsController < ApplicationController end def search_plugin - @icon_data = [File.open(RAILS_ROOT + '/public/images/done.png').read]. + # TODO: ASSET PATH!! + @icon_data = [File.open(Rails.root + '/app/assets/images/done.png').read]. pack('m').gsub(/\n/, '') render :layout => false @@ -52,7 +53,7 @@ class IntegrationsController < ApplicationController message = Mail.new(params[:message]) # find user - user = User.find(:first, :include => [:preference], :conditions => ["preferences.sms_email = ?", message.from]) + user = User.where("preferences.sms_email = ?", message.from).includes(:preference).first if user.nil? render :text => "No user found", :status => 404 return false diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index fed1dc3a..fb421026 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -1,7 +1,6 @@ class LoginController < ApplicationController layout 'login' - filter_parameter_logging :user_password skip_before_filter :set_session_expiration skip_before_filter :login_required before_filter :login_optional @@ -9,62 +8,38 @@ class LoginController < ApplicationController protect_from_forgery :except => [:check_expiry, :login] - if ( SITE_CONFIG['authentication_schemes'].include? 'cas') - # This will allow the user to view the index page without authentication - # but will process CAS authentication data if the user already - # has an SSO session open. - if defined? CASClient - # Only require sub-library if gem is installed and loaded - require 'casclient/frameworks/rails/filter' - before_filter CASClient::Frameworks::Rails::GatewayFilter, :only => :login_cas - - # This requires the user to be authenticated for viewing all other pages. - before_filter CASClient::Frameworks::Rails::Filter, :only => [:login_cas ] - end - end - def login - if cas_enabled? - @username = session[:cas_user] - @login_url = CASClient::Frameworks::Rails::Filter.login_url(self) + @page_title = "TRACKS::Login" + cookies[:preferred_auth] = prefered_auth? unless cookies[:preferred_auth] + case request.method + when 'POST' + if @user = User.authenticate(params['user_login'], params['user_password']) + session['user_id'] = @user.id + # If checkbox on login page checked, we don't expire the session after 1 hour + # of inactivity and we remember this user for future browser sessions + session['noexpiry'] = params['user_noexpiry'] + msg = (should_expire_sessions?) ? "will expire after 1 hour of inactivity." : "will not expire." + notify :notice, "Login successful: session #{msg}" + cookies[:tracks_login] = { :value => @user.login, :expires => Time.now + 1.year, :secure => SITE_CONFIG['secure_cookies'] } + unless should_expire_sessions? + @user.remember_me + cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at, :secure => SITE_CONFIG['secure_cookies'] } + end + redirect_back_or_home + return + else + @login = params['user_login'] + notify :warning, t('login.unsuccessful') + end + when 'GET' + if User.no_users_yet? + redirect_to signup_path + return + end end - if openid_enabled? && using_open_id? - login_openid - elsif cas_enabled? && session[:cas_user] - login_cas - else - @page_title = "TRACKS::Login" - cookies[:preferred_auth] = prefered_auth? unless cookies[:preferred_auth] - case request.method - when :post - if @user = User.authenticate(params['user_login'], params['user_password']) - session['user_id'] = @user.id - # If checkbox on login page checked, we don't expire the session after 1 hour - # of inactivity and we remember this user for future browser sessions - session['noexpiry'] = params['user_noexpiry'] - msg = (should_expire_sessions?) ? "will expire after 1 hour of inactivity." : "will not expire." - notify :notice, "Login successful: session #{msg}" - cookies[:tracks_login] = { :value => @user.login, :expires => Time.now + 1.year, :secure => SITE_CONFIG['secure_cookies'] } - unless should_expire_sessions? - @user.remember_me - cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at, :secure => SITE_CONFIG['secure_cookies'] } - end - redirect_back_or_home - return - else - @login = params['user_login'] - notify :warning, t('login.unsuccessful') - end - when :get - if User.no_users_yet? - redirect_to signup_path - return - end - end - respond_to do |format| - format.html - format.m { render :action => 'login_mobile.html.erb', :layout => 'mobile' } - end + respond_to do |format| + format.html + format.m { render :action => 'login', :layout => 'mobile' } end end @@ -111,32 +86,6 @@ class LoginController < ApplicationController format.js end end - - def login_cas - # If checkbox on login page checked, we don't expire the session after 1 hour - # of inactivity and we remember this user for future browser sessions - - session['noexpiry'] ||= params['user_noexpiry'] - if session[:cas_user] - if @user = User.find_by_login(session[:cas_user]) - session['user_id'] = @user.id - msg = (should_expire_sessions?) ? t('login.session_will_expire', :hours => 1) : t('login.session_will_not_expire') - notify :notice, (t('login.successful_with_session_info') + msg) - cookies[:tracks_login] = { :value => @user.login, :expires => Time.now + 1.year, :secure => SITE_CONFIG['secure_cookies'] } - unless should_expire_sessions? - @user.remember_me - cookies[:auth_token] = { :value => @user.remember_token, :expires => @user.remember_token_expires_at, :secure => SITE_CONFIG['secure_cookies'] } - end - else - notify :warning, t('login.cas_username_not_found', :username => session[:cas_user]) - redirect_to signup_url ; return - end - else - notify :warning, result.message - end - redirect_back_or_home - - end private @@ -144,32 +93,4 @@ class LoginController < ApplicationController session['noexpiry'] != "on" end - protected - - def login_openid - # If checkbox on login page checked, we don't expire the session after 1 hour - # of inactivity and we remember this user for future browser sessions - session['noexpiry'] ||= params['user_noexpiry'] - authenticate_with_open_id do |result, identity_url| - if result.successful? - if @user = User.find_by_open_id_url(identity_url) - session['user_id'] = @user.id - msg = (should_expire_sessions?) ? t('login.session_will_expire', :hours => 1) : t('login.session_will_not_expire') - notify :notice, (t('login.successful_with_session_info') + msg) - cookies[:tracks_login] = { :value => @user.login, :expires => Time.now + 1.year, :secure => SITE_CONFIG['secure_cookies'] } - unless should_expire_sessions? - @user.remember_me - cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at, :secure => SITE_CONFIG['secure_cookies'] } - end - redirect_back_or_home - else - notify :warning, t('login.openid_identity_url_not_found', :identity_url => identity_url) - end - else - notify :warning, result.message - end - end - end - - end diff --git a/app/controllers/notes_controller.rb b/app/controllers/notes_controller.rb index 4081611a..fc63407a 100644 --- a/app/controllers/notes_controller.rb +++ b/app/controllers/notes_controller.rb @@ -18,7 +18,7 @@ class NotesController < ApplicationController @page_title = "TRACKS::Note " + @note.id.to_s respond_to do |format| format.html - format.m { render :action => 'note_mobile' } + format.m end end @@ -29,7 +29,7 @@ class NotesController < ApplicationController @saved = @note.save respond_to do |format| - format.js + format.js format.xml do if @saved head :created, :location => note_url(@note), :text => "new note with id #{@note.id}" diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 07f42df1..5605fc5c 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -9,11 +9,10 @@ class ProjectsController < ApplicationController def index @source_view = params['_source_view'] || 'project_list' - @new_project = current_user.projects.build if params[:projects_and_actions] projects_and_actions else - @contexts = current_user.contexts.all + @contexts = current_user.contexts init_not_done_counts(['project']) init_project_hidden_todo_counts(['project']) if params[:only_active_with_no_next_actions] @@ -21,19 +20,47 @@ class ProjectsController < ApplicationController else @projects = current_user.projects.all end + @new_project = current_user.projects.build + @active_projects = current_user.projects.active + @hidden_projects = current_user.projects.hidden respond_to do |format| - format.html &render_projects_html - format.m &render_projects_mobile - format.xml { render :xml => @projects.to_xml( :except => :user_id ) } - format.rss &render_rss_feed - format.atom &render_atom_feed - format.text &render_text_feed - format.autocomplete { render :text => for_autocomplete(current_user.projects.uncompleted, params[:term]) } + format.html do + @page_title = t('projects.list_projects') + @count = current_user.projects.count + @completed_projects = current_user.projects.completed.limit(10) + @completed_count = current_user.projects.completed.count + @no_projects = current_user.projects.empty? + current_user.projects.cache_note_counts + @new_project = current_user.projects.build + end + format.m do + @completed_projects = current_user.projects.completed + @down_count = @active_projects.size + @hidden_projects.size + @completed_projects.size + cookies[:mobile_url]= {:value => request.fullpath, :secure => SITE_CONFIG['secure_cookies']} + end + format.xml { render :xml => @projects.all.to_xml( :except => :user_id ) } + format.rss do + @feed_title = I18n.t('models.project.feed_title') + @feed_description = I18n.t('models.project.feed_description', :username => current_user.display_name) + end + format.atom do + @feed_title = I18n.t('models.project.feed_title') + @feed_description = I18n.t('models.project.feed_description', :username => current_user.display_name) + end + format.text do + # somehow passing Mime::TEXT using content_type to render does not work + headers['Content-Type']=Mime::TEXT.to_s + end + format.autocomplete do + projects = current_user.projects.active + current_user.projects.hidden + render :text => for_autocomplete(projects, params[:term]) + end end end end def review + @source_view = params['_source_view'] || 'review' @page_title = t('projects.list_reviews') @projects = current_user.projects.all @contexts = current_user.contexts.all @@ -73,13 +100,25 @@ class ProjectsController < ApplicationController def set_reviewed @project.last_reviewed = Time.zone.now @project.save - redirect_to :action => 'show' + + case @source_view + when "project" + redirect_to :action => 'show' + when "project_list" + redirect_to :action => 'index' + when "review" + redirect_to :action => 'review' + else + redirect_to :action => 'index' + end end def projects_and_actions @projects = current_user.projects.active respond_to do |format| format.text { + # somehow passing Mime::TEXT using content_type to render does not work + headers['Content-Type']=Mime::TEXT.to_s render :action => 'index_text_projects_and_actions', :layout => false, :content_type => Mime::TEXT } end @@ -90,11 +129,15 @@ class ProjectsController < ApplicationController init_data_for_sidebar unless mobile? @page_title = t('projects.page_title', :project => @project.name) - @not_done = @project.todos.active_or_hidden(:include => Todo::DEFAULT_INCLUDES) - @deferred = @project.todos.deferred(:include => Todo::DEFAULT_INCLUDES) - @pending = @project.todos.pending(:include => Todo::DEFAULT_INCLUDES) - @done = @project.todos.find_in_state(:all, :completed, - :order => "todos.completed_at DESC", :limit => current_user.prefs.show_number_completed, :include => Todo::DEFAULT_INCLUDES) + @not_done = @project.todos.active_or_hidden.includes(Todo::DEFAULT_INCLUDES) + @deferred = @project.todos.deferred.includes(Todo::DEFAULT_INCLUDES) + @pending = @project.todos.pending.includes(Todo::DEFAULT_INCLUDES) + + @done = {} + @done = @project.todos.completed. + reorder("todos.completed_at DESC"). + limit(current_user.prefs.show_number_completed). + includes(Todo::DEFAULT_INCLUDES) unless current_user.prefs.show_number_completed == 0 @count = @not_done.size @down_count = @count + @deferred.size + @pending.size @@ -106,39 +149,34 @@ class ProjectsController < ApplicationController @contexts = current_user.contexts respond_to do |format| format.html - format.m &render_project_mobile - format.xml { + format.m do + if @project.default_context.nil? + @project_default_context = t('projects.no_default_context') + else + @project_default_context = t('projects.default_context', :context => @project.default_context.name) + end + cookies[:mobile_url]= {:value => request.fullpath, :secure => SITE_CONFIG['secure_cookies']} + @mobile_from_project = @project.id + end + format.xml do render :xml => @project.to_xml(:except => :user_id) { |xml| xml.not_done { @not_done.each { |child| child.to_xml(:builder => xml, :skip_instruct => true) } } xml.deferred { @deferred.each { |child| child.to_xml(:builder => xml, :skip_instruct => true) } } xml.pending { @pending.each { |child| child.to_xml(:builder => xml, :skip_instruct => true) } } xml.done { @done.each { |child| child.to_xml(:builder => xml, :skip_instruct => true) } } } - } + end end end - # Example XML usage: curl -H 'Accept: application/xml' -H 'Content-Type: - # application/xml' - # -u username:password - # -d 'new project_name' - # http://our.tracks.host/projects - # def create if params[:format] == 'application/xml' && params['exception'] - render_failure "Expected post format is valid xml like so: project name." + render_failure "Expected post format is valid xml like so: project name.", 400 return end - - @project = current_user.projects.build - params_are_invalid = true - if (params['project'] || (params['request'] && params['request']['project'])) - @project.attributes = params['project'] || params['request']['project'] - params_are_invalid = false - end + @project = current_user.projects.build(params['project']) @go_to_project = params['go_to_project'] @saved = @project.save - @project_not_done_counts = { @project.id => 0 } @active_projects_count = current_user.projects.active.count @contexts = current_user.contexts @@ -146,10 +184,8 @@ class ProjectsController < ApplicationController respond_to do |format| format.js { @down_count = current_user.projects.size } format.xml do - if @project.new_record? && params_are_invalid - render_failure "Expected post format is valid xml like so: project name." - elsif @project.new_record? - render_failure @project.errors.full_messages.join(', ') + if @project.new_record? + render_failure @project.errors.to_xml.html_safe, 409 else head :created, :location => project_url(@project), :text => @project.id end @@ -179,38 +215,30 @@ class ProjectsController < ApplicationController if @saved @project.transition_to(@new_state) if @state_changed if boolean_param('wants_render') - if (@project.hidden?) - @project_project_hidden_todo_counts = Hash.new - @project_project_hidden_todo_counts[@project.id] = @project.reload().todos.active_or_hidden.count - else - @project_not_done_counts = Hash.new - @project_not_done_counts[@project.id] = @project.reload().todos.active_or_hidden.count - end @contexts = current_user.contexts update_state_counts init_data_for_sidebar - template = 'projects/update.js.erb' - - # TODO: are these params ever set? or is this dead code? + template = 'projects/update' + # TODO: are these params ever set? or is this dead code? elsif boolean_param('update_status') - template = 'projects/update_status.js.rjs' + template = 'projects/update_status' elsif boolean_param('update_default_context') @initial_context_name = @project.default_context.name - template = 'projects/update_default_context.js.rjs' + template = 'projects/update_default_context' elsif boolean_param('update_default_tags') - template = 'projects/update_default_tags.js.rjs' + template = 'projects/update_default_tags' elsif boolean_param('update_project_name') @projects = current_user.projects - template = 'projects/update_project_name.js.rjs' + template = 'projects/update_project_name' else render :text => success_text || 'Success' return end else init_data_for_sidebar - template = 'projects/update.js.erb' + template = 'projects/update' end respond_to do |format| @@ -304,73 +332,7 @@ class ProjectsController < ApplicationController @show_hidden_projects = @hidden_projects_count > 0 @show_completed_projects = @completed_projects_count > 0 end - - def render_projects_html - lambda do - @page_title = t('projects.list_projects') - @count = current_user.projects.count - @active_projects = current_user.projects.active - @hidden_projects = current_user.projects.hidden - @completed_projects = current_user.projects.completed.find(:all, :limit => 10) - @completed_count = current_user.projects.completed.count - @no_projects = current_user.projects.empty? - current_user.projects.cache_note_counts - @new_project = current_user.projects.build - render - end - end - - def render_projects_mobile - lambda do - @active_projects = current_user.projects.active - @hidden_projects = current_user.projects.hidden - @completed_projects = current_user.projects.completed - @down_count = @active_projects.size + @hidden_projects.size + @completed_projects.size - cookies[:mobile_url]= {:value => request.request_uri, :secure => SITE_CONFIG['secure_cookies']} - render :action => 'index_mobile' - end - end - - def render_project_mobile - lambda do - if @project.default_context.nil? - @project_default_context = t('projects.no_default_context') - else - @project_default_context = t('projects.default_context', :context => @project.default_context.name) - end - cookies[:mobile_url]= {:value => request.request_uri, :secure => SITE_CONFIG['secure_cookies']} - @mobile_from_project = @project.id - render :action => 'project_mobile' - end - end - - def render_rss_feed - lambda do - render_rss_feed_for @projects, :feed => feed_options, - :title => :name, - :item => { :description => lambda { |p| @template.summary(p) } } - end - end - - def render_atom_feed - lambda do - render_atom_feed_for @projects, :feed => feed_options, - :item => { :description => lambda { |p| @template.summary(p) }, - :title => :name, - :author => lambda { |p| nil } } - end - end - - def feed_options - Project.feed_options(current_user) - end - - def render_text_feed - lambda do - render :action => 'index', :layout => false, :content_type => Mime::TEXT - end - end - + def set_project_from_params @project = current_user.projects.find_by_params(params) end diff --git a/app/controllers/recurring_todos_controller.rb b/app/controllers/recurring_todos_controller.rb index 69517597..0ec4edd7 100644 --- a/app/controllers/recurring_todos_controller.rb +++ b/app/controllers/recurring_todos_controller.rb @@ -9,12 +9,12 @@ class RecurringTodosController < ApplicationController @page_title = t('todos.recurring_actions_title') @source_view = params['_source_view'] || 'recurring_todo' find_and_inactivate - @recurring_todos = current_user.recurring_todos.active.find(:all, :include => [:tags, :taggings]) - @completed_recurring_todos = current_user.recurring_todos.completed.find(:all, :limit => 10, :include => [:tags, :taggings]) + @recurring_todos = current_user.recurring_todos.active.includes(:tags, :taggings) + @completed_recurring_todos = current_user.recurring_todos.completed.limit(10).includes(:tags, :taggings) - @no_recurring_todos = @recurring_todos.size == 0 - @no_completed_recurring_todos = @completed_recurring_todos.size == 0 - @count = @recurring_todos.size + @no_recurring_todos = @recurring_todos.count == 0 + @no_completed_recurring_todos = @completed_recurring_todos.count == 0 + @count = @recurring_todos.count @new_recurring_todo = RecurringTodo.new end @@ -161,10 +161,10 @@ class RecurringTodosController < ApplicationController format.html do if @saved - notify :notice, t('todos.recurring_deleted_success'), 2.0 + notify :notice, t('todos.recurring_deleted_success') redirect_to :action => 'index' else - notify :error, t('todos.error_deleting_recurring', :description => @recurring_todo.description), 2.0 + notify :error, t('todos.error_deleting_recurring', :description => @recurring_todo.description) redirect_to :action => 'index' end end @@ -271,8 +271,8 @@ class RecurringTodosController < ApplicationController end @xth_day = [[t('common.first'),1],[t('common.second'),2],[t('common.third'),3],[t('common.fourth'),4],[t('common.last'),5]] - @projects = current_user.projects.find(:all, :include => [:default_context]) - @contexts = current_user.contexts.find(:all) + @projects = current_user.projects.includes(:default_context) + @contexts = current_user.contexts end def get_recurring_todo_from_param @@ -282,10 +282,11 @@ class RecurringTodosController < ApplicationController def find_and_inactivate # find active recurring todos without active todos and inactivate them - current_user.recurring_todos.active.all( - :select => "recurring_todos.id, recurring_todos.state", - :joins => "LEFT JOIN todos fai_todos ON (recurring_todos.id = fai_todos.recurring_todo_id) AND (NOT fai_todos.state='completed')", - :conditions => "fai_todos.id IS NULL").each { |rt| current_user.recurring_todos.find(rt.id).toggle_completion! } + current_user.recurring_todos.active. + select("recurring_todos.id, recurring_todos.state"). + joins("LEFT JOIN todos fai_todos ON (recurring_todos.id = fai_todos.recurring_todo_id) AND (NOT fai_todos.state='completed')"). + where("fai_todos.id IS NULL"). + each { |rt| current_user.recurring_todos.find(rt.id).toggle_completion! } end end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 44fa0d3f..817c9f1e 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,25 +1,30 @@ class SearchController < ApplicationController - helper :todos, :application, :notes, :projects - + helper :todos, :application, :notes, :projects, :contexts + def results @source_view = params['_source_view'] || 'search' @page_title = "TRACKS::Search Results for #{params[:search]}" - terms = '%' + params[:search] + '%' + terms = "%#{params[:search]}%" - @found_not_complete_todos = current_user.todos.find(:all, - :conditions => ["(todos.description LIKE ? OR todos.notes LIKE ?) AND todos.completed_at IS NULL", terms, terms], - :include => Todo::DEFAULT_INCLUDES, - :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC") - @found_complete_todos = current_user.todos.find(:all, - :conditions => ["(todos.description LIKE ? OR todos.notes LIKE ?) AND NOT (todos.completed_at IS NULL)", terms, terms], - :include => Todo::DEFAULT_INCLUDES, - :order => "todos.completed_at DESC") + @found_not_complete_todos = current_user.todos. + where("(todos.description LIKE ? OR todos.notes LIKE ?) AND todos.completed_at IS NULL", terms, terms). + includes(Todo::DEFAULT_INCLUDES). + reorder("todos.due IS NULL, todos.due ASC, todos.created_at ASC"). + all + + @found_complete_todos = current_user.todos. + where("(todos.description LIKE ? OR todos.notes LIKE ?) AND NOT (todos.completed_at IS NULL)", terms, terms). + includes(Todo::DEFAULT_INCLUDES). + reorder("todos.completed_at DESC"). + all + @found_todos = @found_not_complete_todos + @found_complete_todos - @found_projects = current_user.projects.find(:all, :conditions => ["name LIKE ? OR description LIKE ?", terms, terms]) - @found_notes = current_user.notes.find(:all, :conditions => ["body LIKE ?", terms]) - @found_contexts = current_user.contexts.find(:all, :conditions => ["name LIKE ?", terms]) + @found_projects = current_user.projects.where("name LIKE ? OR description LIKE ?", terms, terms).all + @found_notes = current_user.notes.where("body LIKE ?", terms).all + @found_contexts = current_user.contexts.where("name LIKE ?", terms).all + # TODO: limit search to tags on todos @found_tags = Tagging.find_by_sql([ "SELECT DISTINCT tags.name as name "+ diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index fac58ec2..95fa8b6f 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -1,396 +1,224 @@ class StatsController < ApplicationController helper :todos, :projects, :recurring_todos + append_before_filter :init - append_before_filter :init, :exclude => [] - def index - @page_title = 'TRACKS::Statistics' + @page_title = t('stats.index_title') + @first_action = current_user.todos.reorder("created_at ASC").first @tags_count = get_total_number_of_tags_of_user @unique_tags_count = get_unique_tags_of_user.size - @hidden_contexts = @contexts.hidden - @first_action = @actions.find(:first, :order => "created_at ASC") - + @hidden_contexts = current_user.contexts.hidden + get_stats_actions get_stats_contexts get_stats_projects - get_stats_tags - + get_stats_tags + render :layout => 'standard' - end - - def actions_done_last12months_data - @actions = @user.todos - - # get actions created and completed in the past 12+3 months. +3 for running - # average - @actions_done_last12months = @actions.find(:all, { - :select => "completed_at", - :conditions => ["completed_at > ? AND completed_at IS NOT NULL", @cut_off_year_plus3] - }) - @actions_created_last12months = @actions.find(:all, { - :select => "created_at", - :conditions => ["created_at > ?", @cut_off_year_plus3] - }) - - # convert to hash to be able to fill in non-existing days in - # @actions_done_last12months and count the total actions done in the past - # 12 months to be able to calculate percentage - - # use 0 to initialise action count to zero - @actions_done_last12months_hash = Hash.new(0) - @actions_done_last12months.each do |r| - months = (@today.year - r.completed_at.year)*12 + (@today.month - r.completed_at.month) - - @actions_done_last12months_hash[months] += 1 - end - - # convert to hash to be able to fill in non-existing days in - # @actions_created_last12months and count the total actions done in the - # past 12 months to be able to calculate percentage - - # use 0 to initialise action count to zero - @actions_created_last12months_hash = Hash.new(0) - @actions_created_last12months.each do |r| - months = (@today.year - r.created_at.year)*12 + (@today.month - r.created_at.month) - - @actions_created_last12months_hash[months] += 1 - end - - @sum_actions_done_last12months=0 - @sum_actions_created_last12months=0 - - # find max for graph in both hashes - @max=0 - 0.upto 13 do |i| - @sum_actions_done_last12months += @actions_done_last12months_hash[i] - @max = @actions_done_last12months_hash[i] if @actions_done_last12months_hash[i] > @max - end - 0.upto 13 do |i| - @sum_actions_created_last12months += @actions_created_last12months_hash[i] - @max = @actions_created_last12months_hash[i] if @actions_created_last12months_hash[i] > @max - end - - # find running avg for month i by calculating avg of month i and the two - # after them. Ignore current month because you do not have full data for - # it - @actions_done_avg_last12months_hash = Hash.new("null") - 1.upto(12) { |i| - @actions_done_avg_last12months_hash[i] = (@actions_done_last12months_hash[i] + - @actions_done_last12months_hash[i+1] + - @actions_done_last12months_hash[i+2])/3.0 - } - - # find running avg for month i by calculating avg of month i and the two - # after them. Ignore current month because you do not have full data for - # it - @actions_created_avg_last12months_hash = Hash.new("null") - 1.upto(12) { |i| - @actions_created_avg_last12months_hash[i] = (@actions_created_last12months_hash[i] + - @actions_created_last12months_hash[i+1] + - @actions_created_last12months_hash[i+2])/3.0 - } - - # interpolate avg for this month. Assume 31 days in this month - days_passed_this_month = Time.new.day/1.0 - @interpolated_actions_created_this_month = ( - @actions_created_last12months_hash[0]/days_passed_this_month*31.0+ - @actions_created_last12months_hash[1]+ - @actions_created_last12months_hash[2]) / 3.0 - - @interpolated_actions_done_this_month = ( - @actions_done_last12months_hash[0]/days_passed_this_month*31.0 + - @actions_done_last12months_hash[1]+ - @actions_done_last12months_hash[2]) / 3.0 - - render :layout => false end + def actions_done_last12months_data + # get actions created and completed in the past 12+3 months. +3 for running + # average + @actions_done_last12months = current_user.todos.completed_after(@cut_off_year).select("completed_at" ) + @actions_created_last12months = current_user.todos.created_after(@cut_off_year).select("created_at") + @actions_done_last12monthsPlus3 = current_user.todos.completed_after(@cut_off_year_plus3).select("completed_at" ) + @actions_created_last12monthsPlus3 = current_user.todos.created_after(@cut_off_year_plus3).select("created_at") + + # convert to array and fill in non-existing months + @actions_done_last12months_array = convert_to_months_from_today_array(@actions_done_last12months, 13, :completed_at) + @actions_created_last12months_array = convert_to_months_from_today_array(@actions_created_last12months, 13, :created_at) + @actions_done_last12monthsPlus3_array = convert_to_months_from_today_array(@actions_done_last12monthsPlus3, 16, :completed_at) + @actions_created_last12monthsPlus3_array = convert_to_months_from_today_array(@actions_created_last12monthsPlus3, 16, :created_at) + + # find max for graph in both arrays + @max = [@actions_done_last12months_array.max, @actions_created_last12months_array.max].max + + # find running avg + @actions_done_avg_last12months_array, @actions_created_avg_last12months_array = + find_running_avg_array(@actions_done_last12monthsPlus3_array, @actions_created_last12monthsPlus3_array, 13) + + # interpolate avg for current month. + percent_of_month = Time.zone.now.day.to_f / Time.zone.now.end_of_month.day.to_f + @interpolated_actions_created_this_month = interpolate_avg(@actions_created_last12months_array, percent_of_month) + @interpolated_actions_done_this_month = interpolate_avg(@actions_done_last12months_array, percent_of_month) + + render :layout => false + end + def actions_done_last_years + @page_title = t('stats.index_title') @chart_width = 900 @chart_height = 400 end - + def actions_done_lastyears_data - @actions = @user.todos - - # get actions created and completed in the past 12+3 months. +3 for running - # average - @actions_done_last_months = @actions.find(:all, { - :select => "completed_at", - :conditions => ["completed_at IS NOT NULL"] - }) - @actions_created_last_months = @actions.find(:all, { - :select => "created_at", - }) - - @month_count = 0 - - # convert to hash to be able to fill in non-existing days in - # @actions_done_last12months and count the total actions done in the past - # 12 months to be able to calculate percentage - - # use 0 to initialise action count to zero - @actions_done_last_months_hash = Hash.new(0) - @actions_done_last_months.each do |r| - months = (@today.year - r.completed_at.year)*12 + (@today.month - r.completed_at.month) - @month_count = months if months > @month_count - @actions_done_last_months_hash[months] += 1 - end - - # convert to hash to be able to fill in non-existing days in - # @actions_created_last12months and count the total actions done in the - # past 12 months to be able to calculate percentage + @actions_done_last_months = current_user.todos.completed.select("completed_at").reorder("completed_at DESC") + @actions_created_last_months = current_user.todos.select("created_at").reorder("created_at DESC" ) - # use 0 to initialise action count to zero - @actions_created_last_months_hash = Hash.new(0) - @actions_created_last_months.each do |r| - months = (@today.year - r.created_at.year)*12 + (@today.month - r.created_at.month) - @month_count = months if months > @month_count - @actions_created_last_months_hash[months] += 1 - end + # query is sorted, so use last todo to calculate number of months + @month_count = [difference_in_months(@today, @actions_created_last_months.last.created_at), + difference_in_months(@today, @actions_done_last_months.last.completed_at)].max - @sum_actions_done_last_months=0 - @sum_actions_created_last_months=0 + # convert to array and fill in non-existing months + @actions_done_last_months_array = convert_to_months_from_today_array(@actions_done_last_months, @month_count+1, :completed_at) + @actions_created_last_months_array = convert_to_months_from_today_array(@actions_created_last_months, @month_count+1, :created_at) # find max for graph in both hashes - @max=0 - 0.upto @month_count do |i| - @sum_actions_done_last_months += @actions_done_last_months_hash[i] - @max = @actions_done_last_months_hash[i] if @actions_done_last_months_hash[i] > @max - end - 0.upto @month_count do |i| - @sum_actions_created_last_months += @actions_created_last_months_hash[i] - @max = @actions_created_last_months_hash[i] if @actions_created_last_months_hash[i] > @max - end - - # find running avg for month i by calculating avg of month i and the two - # after them. Ignore current month because you do not have full data for - # it - @actions_done_avg_last_months_hash = Hash.new("null") - 1.upto(@month_count) { |i| - @actions_done_avg_last_months_hash[i] = (@actions_done_last_months_hash[i] + - @actions_done_last_months_hash[i+1] + - @actions_done_last_months_hash[i+2])/3.0 - } - # correct last two months - @actions_done_avg_last_months_hash[@month_count] = @actions_done_avg_last_months_hash[@month_count] * 3 - @actions_done_avg_last_months_hash[@month_count-1] = @actions_done_avg_last_months_hash[@month_count-1] * 3 / 2 if @month_count > 1 + @max = [@actions_done_last_months_array.max, @actions_created_last_months_array.max].max + + # find running avg + @actions_done_avg_last_months_array, @actions_created_avg_last_months_array = + find_running_avg_array(@actions_done_last_months_array, @actions_created_last_months_array, @month_count+1) + + # correct last two months since the data of last+1 and last+2 are not available for avg + correct_last_two_months(@actions_done_avg_last_months_array, @month_count) + correct_last_two_months(@actions_created_avg_last_months_array, @month_count) + + # interpolate avg for this month. + percent_of_month = Time.zone.now.day.to_f / Time.zone.now.end_of_month.day.to_f + @interpolated_actions_created_this_month = interpolate_avg(@actions_created_last_months_array, percent_of_month) + @interpolated_actions_done_this_month = interpolate_avg(@actions_done_last_months_array, percent_of_month) - # find running avg for month i by calculating avg of month i and the two - # after them. Ignore current month because you do not have full data for - # it - @actions_created_avg_last_months_hash = Hash.new("null") - 1.upto(@month_count) { |i| - @actions_created_avg_last_months_hash[i] = (@actions_created_last_months_hash[i] + - @actions_created_last_months_hash[i+1] + - @actions_created_last_months_hash[i+2])/3.0 - } - # correct last two months - @actions_created_avg_last_months_hash[@month_count] = @actions_created_avg_last_months_hash[@month_count] * 3 - @actions_created_avg_last_months_hash[@month_count-1] = @actions_created_avg_last_months_hash[@month_count-1] * 3 / 2 if @month_count > 1 - - # interpolate avg for this month. Assume 31 days in this month - days_passed_this_month = Time.new.day/1.0 - @interpolated_actions_created_this_month = ( - @actions_created_last_months_hash[0]/days_passed_this_month*31.0+ - @actions_created_last_months_hash[1]+ - @actions_created_last_months_hash[2]) / 3.0 - - @interpolated_actions_done_this_month = ( - @actions_done_last_months_hash[0]/days_passed_this_month*31.0 + - @actions_done_last_months_hash[1]+ - @actions_done_last_months_hash[2]) / 3.0 - render :layout => false end - def actions_done_last30days_data # get actions created and completed in the past 30 days. - @actions_done_last30days = @actions.find(:all, { - :select => "completed_at", - :conditions => ["completed_at > ? AND completed_at IS NOT NULL", @cut_off_month] - }) - @actions_created_last30days = @actions.find(:all, { - :select => "created_at", - :conditions => ["created_at > ?", @cut_off_month] - }) - - # convert to hash to be able to fill in non-existing days in - # @actions_done_last30days and count the total actions done in the past 30 - # days to be able to calculate percentage - @sum_actions_done_last30days=0 - - # use 0 to initialise action count to zero - @actions_done_last30days_hash = Hash.new(0) - @actions_done_last30days.each do |r| - # only use date part of completed_at - action_date = Time.utc(r.completed_at.year, r.completed_at.month, r.completed_at.day, 0,0) - days = ((@today - action_date) / @seconds_per_day).to_i + @actions_done_last30days = current_user.todos.completed_after(@cut_off_month).select("completed_at") + @actions_created_last30days = current_user.todos.created_after(@cut_off_month).select("created_at") - @actions_done_last30days_hash[days] += 1 - @sum_actions_done_last30days+=1 - end - - # convert to hash to be able to fill in non-existing days in - # @actions_done_last30days and count the total actions done in the past 30 - # days to be able to calculate percentage - @sum_actions_created_last30days=0 - - # use 0 to initialise action count to zero - @actions_created_last30days_hash = Hash.new(0) - @actions_created_last30days.each do |r| - # only use date part of created_at - action_date = Time.utc(r.created_at.year, r.created_at.month, r.created_at.day, 0,0) - days = ((@today - action_date) / @seconds_per_day).to_i - - @actions_created_last30days_hash[days] += 1 - @sum_actions_created_last30days += 1 - end + # convert to array. 30+1 to have 30 complete days and one current day [0] + @actions_done_last30days_array = convert_to_days_from_today_array(@actions_done_last30days, 31, :completed_at) + @actions_created_last30days_array = convert_to_days_from_today_array(@actions_created_last30days, 31, :created_at) # find max for graph in both hashes - @max=0 - 0.upto(30) { |i| @max = @actions_done_last30days_hash[i] if @actions_done_last30days_hash[i] > @max } - 0.upto(30) { |i| @max = @actions_created_last30days_hash[i] if @actions_created_last30days_hash[i] > @max } - + @max = [@actions_done_last30days_array.max, @actions_created_last30days_array.max].max + render :layout => false end def actions_completion_time_data - @actions_completion_time = @actions.find(:all, { - :select => "completed_at, created_at", - :conditions => "completed_at IS NOT NULL" - }) - - # convert to hash to be able to fill in non-existing days in - # @actions_completion_time also convert days to weeks (/7) - - @max_days, @max_actions, @sum_actions=0,0,0 - @actions_completion_time_hash = Hash.new(0) - @actions_completion_time.each do |r| - days = (r.completed_at - r.created_at) / @seconds_per_day - weeks = (days/7).to_i - @actions_completion_time_hash[weeks] += 1 + @actions_completion_time = current_user.todos.completed.select("completed_at, created_at").reorder("completed_at DESC" ) - @max_days=days if days > @max_days - @max_actions = @actions_completion_time_hash[weeks] if @actions_completion_time_hash[weeks] > @max_actions - @sum_actions += 1 - end - + # convert to array and fill in non-existing weeks with 0 + @max_weeks = difference_in_weeks(@today, @actions_completion_time.last.completed_at) + @actions_completed_per_week_array = convert_to_weeks_running_array(@actions_completion_time, @max_weeks+1) + # stop the chart after 10 weeks - @cut_off = 10 - + @count = [10, @max_weeks].min + + # convert to new array to hold max @cut_off elems + 1 for sum of actions after @cut_off + @actions_completion_time_array = cut_off_array_with_sum(@actions_completed_per_week_array, @count) + @max_actions = @actions_completion_time_array.max + + # get percentage done cummulative + @cumm_percent_done = convert_to_cummulative_array(@actions_completion_time_array, @actions_completion_time.count) + render :layout => false end def actions_running_time_data - @actions_running_time = @actions.find(:all, { - :select => "created_at", - :conditions => "completed_at IS NULL" - }) + @actions_running_time = current_user.todos.not_completed.select("created_at").reorder("created_at DESC") - # convert to hash to be able to fill in non-existing days in - # @actions_running_time also convert days to weeks (/7) - - @max_days, @max_actions, @sum_actions=0,0,0 - @actions_running_time_hash = Hash.new(0) - @actions_running_time.each do |r| - days = (@today - r.created_at) / @seconds_per_day - weeks = (days/7).to_i + # convert to array and fill in non-existing weeks with 0 + @max_weeks = difference_in_weeks(@today, @actions_running_time.last.created_at) + @actions_running_per_week_array = convert_to_weeks_from_today_array(@actions_running_time, @max_weeks+1, :created_at) - @actions_running_time_hash[weeks] += 1 - - @max_days=days if days > @max_days - @max_actions = @actions_running_time_hash[weeks] if @actions_running_time_hash[weeks] > @max_actions - @sum_actions += 1 - end - # cut off chart at 52 weeks = one year - @cut_off=52 - + @count = [52, @max_weeks].min + + # convert to new array to hold max @cut_off elems + 1 for sum of actions after @cut_off + @actions_running_time_array = cut_off_array_with_sum(@actions_running_per_week_array, @count) + @max_actions = @actions_running_time_array.max + + # get percentage done cummulative + @cumm_percent_done = convert_to_cummulative_array(@actions_running_time_array, @actions_running_time.count ) + render :layout => false end def actions_visible_running_time_data # running means - # - not completed (completed_at must be null) visible means + # - not completed (completed_at must be null) + # visible means # - actions not part of a hidden project # - actions not part of a hidden context # - actions not deferred (show_from must be null) # - actions not pending/blocked - - @actions_running_time = @actions.find_by_sql([ - "SELECT t.created_at "+ - "FROM todos t LEFT OUTER JOIN projects p ON t.project_id = p.id LEFT OUTER JOIN contexts c ON t.context_id = c.id "+ - "WHERE t.user_id=? "+ - "AND t.completed_at IS NULL " + - "AND t.show_from IS NULL " + - "AND NOT (p.state='hidden' OR p.state='pending' OR c.hide=?) " + - "ORDER BY t.created_at ASC", @user.id, true] - ) - - # convert to hash to be able to fill in non-existing days in - # @actions_running_time also convert days to weeks (/7) - - @max_days, @max_actions, @sum_actions=0,0,0 - @actions_running_time_hash = Hash.new(0) - @actions_running_time.each do |r| - days = (@today - r.created_at) / @seconds_per_day - weeks = (days/7).to_i - # RAILS_DEFAULT_LOGGER.error("\n" + total.to_s + " - " + days + "\n") - @actions_running_time_hash[weeks] += 1 - - @max_days=days if days > @max_days - @max_actions = @actions_running_time_hash[weeks] if @actions_running_time_hash[weeks] > @max_actions - @sum_actions += 1 - end - + + @actions_running_time = current_user.todos.not_completed.not_hidden.not_deferred_or_blocked. + select("todos.created_at"). + reorder("todos.created_at DESC") + + @max_weeks = difference_in_weeks(@today, @actions_running_time.last.created_at) + @actions_running_per_week_array = convert_to_weeks_from_today_array(@actions_running_time, @max_weeks+1, :created_at) + # cut off chart at 52 weeks = one year - @cut_off=52 - + @count = [52, @max_weeks].min + + # convert to new array to hold max @cut_off elems + 1 for sum of actions after @cut_off + @actions_running_time_array = cut_off_array_with_sum(@actions_running_per_week_array, @count) + @max_actions = @actions_running_time_array.max + + # get percentage done cummulative + @cumm_percent_done = convert_to_cummulative_array(@actions_running_time_array, @actions_running_time.count ) + + render :layout => false + end + + def actions_open_per_week_data + @actions_started = current_user.todos.created_after(@today-53.weeks). + select("todos.created_at, todos.completed_at"). + reorder("todos.created_at DESC") + + @max_weeks = difference_in_weeks(@today, @actions_started.last.created_at) + + # cut off chart at 52 weeks = one year + @count = [52, @max_weeks].min + + @actions_open_per_week_array = convert_to_weeks_running_from_today_array(@actions_started, @max_weeks+1) + @actions_open_per_week_array = cut_off_array(@actions_open_per_week_array, @count) + @max_actions = @actions_open_per_week_array.max + render :layout => false end - def context_total_actions_data # get total action count per context Went from GROUP BY c.id to c.name for # compatibility with postgresql. Since the name is forced to be unique, this # should work. - @all_actions_per_context = @contexts.find_by_sql( + @all_actions_per_context = current_user.contexts.find_by_sql( "SELECT c.name AS name, c.id as id, count(*) AS total "+ "FROM contexts c, todos t "+ "WHERE t.context_id=c.id "+ - "AND t.user_id="+@user.id.to_s+" "+ + "AND c.user_id = #{current_user.id} " + "GROUP BY c.name, c.id "+ "ORDER BY total DESC" ) - + @sum = @all_actions_per_context.inject(0){|sum, apc| sum += apc['total'].to_i } + pie_cutoff=10 - size = @all_actions_per_context.size() - size = pie_cutoff if size > pie_cutoff - @actions_per_context = Array.new(size) - 0.upto size-1 do |i| - @actions_per_context[i] = @all_actions_per_context[i] - end + size = [@all_actions_per_context.size, pie_cutoff].min + + # explicitely copy contents of hash to avoid ending up with two arrays pointing to same hashes + @actions_per_context = Array.new(size){|i| { + 'name' => @all_actions_per_context[i][:name], + 'total' => @all_actions_per_context[i][:total].to_i, + 'id' => @all_actions_per_context[i][:id] + } } if size==pie_cutoff @actions_per_context[size-1]['name']=t('stats.other_actions_label') - @actions_per_context[size-1]['total']=0 + @actions_per_context[size-1]['total']=@actions_per_context[size-1]['total'] @actions_per_context[size-1]['id']=-1 - (size-1).upto @all_actions_per_context.size()-1 do |i| - @actions_per_context[size-1]['total']+=@all_actions_per_context[i]['total'].to_i - end + size.upto(@all_actions_per_context.size-1){ |i| @actions_per_context[size-1]['total']+=(@all_actions_per_context[i]['total'].to_i) } end - @sum=0 - 0.upto @all_actions_per_context.size()-1 do |i| - @sum += @all_actions_per_context[i]['total'].to_i - end - - @truncate_chars = 15 - + @truncate_chars = 15 + render :layout => false end @@ -399,188 +227,124 @@ class StatsController < ApplicationController # # Went from GROUP BY c.id to c.name for compatibility with postgresql. Since # the name is forced to be unique, this should work. - @all_actions_per_context = @contexts.find_by_sql( + @all_actions_per_context = current_user.contexts.find_by_sql( "SELECT c.name AS name, c.id as id, count(*) AS total "+ "FROM contexts c, todos t "+ "WHERE t.context_id=c.id AND t.completed_at IS NULL AND NOT c.hide "+ - "AND t.user_id="+@user.id.to_s+" "+ + "AND c.user_id = #{current_user.id} " + "GROUP BY c.name, c.id "+ "ORDER BY total DESC" ) - + @sum = @all_actions_per_context.inject(0){|sum, apc| sum += apc['total'].to_i } + pie_cutoff=10 - size = @all_actions_per_context.size() - size = pie_cutoff if size > pie_cutoff - @actions_per_context = Array.new(size) - 0.upto size-1 do |i| - @actions_per_context[i] = @all_actions_per_context[i] - end + size = [@all_actions_per_context.size, pie_cutoff].min + + # explicitely copy contents of hash to avoid ending up with two arrays pointing to same hashes + @actions_per_context = Array.new(size){|i| { + 'name' => @all_actions_per_context[i][:name], + 'total' => @all_actions_per_context[i][:total].to_i, + 'id' => @all_actions_per_context[i][:id] + } } if size==pie_cutoff @actions_per_context[size-1]['name']=t('stats.other_actions_label') - @actions_per_context[size-1]['total']=0 + @actions_per_context[size-1]['total']=@actions_per_context[size-1]['total'] @actions_per_context[size-1]['id']=-1 - (size-1).upto @all_actions_per_context.size()-1 do |i| - @actions_per_context[size-1]['total']+=@all_actions_per_context[i]['total'].to_i - end + (size).upto(@all_actions_per_context.size()-1){|i| @actions_per_context[size-1]['total']+=@all_actions_per_context[i]['total'].to_i } end - - @sum=0 - 0.upto @all_actions_per_context.size()-1 do |i| - @sum += @all_actions_per_context[i]['total'].to_i - end - + @truncate_chars = 15 - + render :layout => false end def actions_day_of_week_all_data - @actions = @user.todos - - @actions_creation_day = @actions.find(:all, { - :select => "created_at" - }) - - @actions_completion_day = @actions.find(:all, { - :select => "completed_at", - :conditions => "completed_at IS NOT NULL" - }) + @actions_creation_day = current_user.todos.select("created_at") + @actions_completion_day = current_user.todos.completed.select("completed_at") - # convert to hash to be able to fill in non-existing days + # convert to array and fill in non-existing days @actions_creation_day_array = Array.new(7) { |i| 0} - @actions_creation_day.each do |t| - # dayofweek: sunday=0..saterday=6 - dayofweek = t.created_at.wday - @actions_creation_day_array[dayofweek] += 1 - end - # find max - @max=0 - 0.upto(6) { |i| @max = @actions_creation_day_array[i] if @actions_creation_day_array[i] > @max} - + @actions_creation_day.each { |t| @actions_creation_day_array[ t.created_at.wday ] += 1 } + @max = @actions_creation_day_array.max - # convert to hash to be able to fill in non-existing days + # convert to array and fill in non-existing days @actions_completion_day_array = Array.new(7) { |i| 0} - @actions_completion_day.each do |t| - # dayofweek: sunday=0..saterday=6 - dayofweek = t.completed_at.wday - @actions_completion_day_array[dayofweek] += 1 - end - 0.upto(6) { |i| @max = @actions_completion_day_array[i] if @actions_completion_day_array[i] > @max} - + @actions_completion_day.each { |t| @actions_completion_day_array[ t.completed_at.wday ] += 1 } + @max = @actions_completion_day_array.max + render :layout => false end def actions_day_of_week_30days_data - @actions_creation_day = @actions.find(:all, { - :select => "created_at", - :conditions => ["created_at > ?", @cut_off_month] - }) - - @actions_completion_day = @actions.find(:all, { - :select => "completed_at", - :conditions => ["completed_at IS NOT NULL AND completed_at > ?", @cut_off_month] - }) + @actions_creation_day = current_user.todos.created_after(@cut_off_month).select("created_at") + @actions_completion_day = current_user.todos.completed_after(@cut_off_month).select("completed_at") # convert to hash to be able to fill in non-existing days @max=0 @actions_creation_day_array = Array.new(7) { |i| 0} - @actions_creation_day.each do |r| - # dayofweek: sunday=1..saterday=8 - dayofweek = r.created_at.wday - @actions_creation_day_array[dayofweek] += 1 - end - 0.upto(6) { |i| @max = @actions_creation_day_array[i] if @actions_creation_day_array[i] > @max} + @actions_creation_day.each { |r| @actions_creation_day_array[ r.created_at.wday ] += 1 } # convert to hash to be able to fill in non-existing days @actions_completion_day_array = Array.new(7) { |i| 0} - @actions_completion_day.each do |r| - # dayofweek: sunday=1..saterday=7 - dayofweek = r.completed_at.wday - @actions_completion_day_array[dayofweek] += 1 - end - 0.upto(6) { |i| @max = @actions_completion_day_array[i] if @actions_completion_day_array[i] > @max} - + @actions_completion_day.each { |r| @actions_completion_day_array[r.completed_at.wday] += 1 } + + @max = [@actions_creation_day_array.max, @actions_completion_day_array.max].max + render :layout => false end def actions_time_of_day_all_data - @actions_creation_hour = @actions.find(:all, { - :select => "created_at" - }) - @actions_completion_hour = @actions.find(:all, { - :select => "completed_at", - :conditions => "completed_at IS NOT NULL" - }) + @actions_creation_hour = current_user.todos.select("created_at") + @actions_completion_hour = current_user.todos.completed.select("completed_at") # convert to hash to be able to fill in non-existing days - @max=0 @actions_creation_hour_array = Array.new(24) { |i| 0} - @actions_creation_hour.each do |r| - hour = r.created_at.hour - @actions_creation_hour_array[hour] += 1 - end - 0.upto(23) { |i| @max = @actions_creation_hour_array[i] if @actions_creation_hour_array[i] > @max} + @actions_creation_hour.each{|r| @actions_creation_hour_array[r.created_at.hour] += 1 } # convert to hash to be able to fill in non-existing days @actions_completion_hour_array = Array.new(24) { |i| 0} - @actions_completion_hour.each do |r| - hour = r.completed_at.hour - @actions_completion_hour_array[hour] += 1 - end - 0.upto(23) { |i| @max = @actions_completion_hour_array[i] if @actions_completion_hour_array[i] > @max} - + @actions_completion_hour.each{|r| @actions_completion_hour_array[r.completed_at.hour] += 1 } + + @max = [@actions_creation_hour_array.max, @actions_completion_hour_array.max].max + render :layout => false end def actions_time_of_day_30days_data - @actions_creation_hour = @actions.find(:all, { - :select => "created_at", - :conditions => ["created_at > ?", @cut_off_month] - }) - - @actions_completion_hour = @actions.find(:all, { - :select => "completed_at", - :conditions => ["completed_at IS NOT NULL AND completed_at > ?", @cut_off_month] - }) + @actions_creation_hour = current_user.todos.created_after(@cut_off_month).select("created_at") + @actions_completion_hour = current_user.todos.completed_after(@cut_off_month).select("completed_at") # convert to hash to be able to fill in non-existing days - @max=0 @actions_creation_hour_array = Array.new(24) { |i| 0} - @actions_creation_hour.each do |r| - hour = r.created_at.hour - @actions_creation_hour_array[hour] += 1 - end - 0.upto(23) { |i| @max = @actions_creation_hour_array[i] if @actions_creation_hour_array[i] > @max} + @actions_creation_hour.each{|r| @actions_creation_hour_array[r.created_at.hour] += 1 } # convert to hash to be able to fill in non-existing days @actions_completion_hour_array = Array.new(24) { |i| 0} - @actions_completion_hour.each do |r| - hour = r.completed_at.hour - @actions_completion_hour_array[hour] += 1 - end - 0.upto(23) { |i| @max = @actions_completion_hour_array[i] if @actions_completion_hour_array[i] > @max} - + @actions_completion_hour.each{|r| @actions_completion_hour_array[r.completed_at.hour] += 1 } + + @max = [@actions_creation_hour_array.max, @max = @actions_completion_hour_array.max].max + render :layout => false end - + def show_selected_actions_from_chart @page_title = t('stats.action_selection_title') @count = 99 @source_view = 'stats' - + case params['id'] when 'avrt', 'avrt_end' # actions_visible_running_time - + # HACK: because open flash chart uses & to denote the end of a parameter, # we cannot use URLs with multiple parameters (that would use &). So we # revert to using two id's for the same selection. avtr_end means that the # last bar of the chart is selected. avtr is used for all other bars - + week_from = params['index'].to_i week_to = week_from+1 - + @chart_name = "actions_visible_running_time_data" @page_title = t('stats.actions_selected_from_week') @further = false @@ -592,28 +356,20 @@ class StatsController < ApplicationController end # get all running actions that are visible - @actions_running_time = @actions.find_by_sql([ - "SELECT t.id, t.created_at "+ - "FROM todos t LEFT OUTER JOIN projects p ON t.project_id = p.id LEFT OUTER JOIN contexts c ON t.context_id = c.id "+ - "WHERE t.user_id=? "+ - "AND t.completed_at IS NULL " + - "AND t.show_from IS NULL " + - "AND NOT (p.state='hidden' OR c.hide=?) " + - "ORDER BY t.created_at ASC", @user.id, true] - ) + @actions_running_time = current_user.todos.not_completed.not_hidden.not_deferred_or_blocked. + select("todos.id, todos.created_at"). + reorder("todos.created_at DESC") + + selected_todo_ids = get_ids_from(@actions_running_time, week_from, week_to, params['id']== 'avrt_end') + @selected_actions = selected_todo_ids.size == 0 ? [] : current_user.todos.where("id in (" + selected_todo_ids.join(",") + ")") + @count = @selected_actions.size - @selected_todo_ids, @count = get_ids_from(@actions_running_time, week_from, week_to, params['id']== 'avrt_end') - @actions = @user.todos - @selected_actions = @actions.find(:all, { - :conditions => "id in (" + @selected_todo_ids + ")" - }) - render :action => "show_selection_from_chart" - + when 'art', 'art_end' week_from = params['index'].to_i week_to = week_from+1 - + @chart_name = "actions_running_time_data" @page_title = "Actions selected from week " @further = false @@ -624,17 +380,12 @@ class StatsController < ApplicationController @page_title += week_from.to_s + " - " + week_to.to_s + "" end - @actions = @user.todos # get all running actions - @actions_running_time = @actions.find(:all, { - :select => "id, created_at", - :conditions => "completed_at IS NULL" - }) + @actions_running_time = current_user.todos.not_completed.select("id, created_at") - @selected_todo_ids, @count = get_ids_from(@actions_running_time, week_from, week_to, params['id']=='art_end') - @selected_actions = @actions.find(:all, { - :conditions => "id in (" + @selected_todo_ids + ")" - }) + selected_todo_ids = get_ids_from(@actions_running_time, week_from, week_to, params['id']=='art_end') + @selected_actions = selected_todo_ids.size == 0 ? [] : current_user.todos.where("id in (#{selected_todo_ids.join(",")})") + @count = @selected_actions.size render :action => "show_selection_from_chart" else @@ -643,118 +394,95 @@ class StatsController < ApplicationController end end - def done + def done @source_view = 'done' - + init_not_done_counts - - @done_recently = current_user.todos.completed.all(:limit => 10, :order => 'completed_at DESC', :include => Todo::DEFAULT_INCLUDES) - @last_completed_projects = current_user.projects.completed.all(:limit => 10, :order => 'completed_at DESC', :include => [:todos, :notes]) + + @done_recently = current_user.todos.completed.limit(10).reorder('completed_at DESC').includes(Todo::DEFAULT_INCLUDES) + @last_completed_projects = current_user.projects.completed.limit(10).reorder('completed_at DESC').includes(:todos, :notes) @last_completed_contexts = [] - @last_completed_recurring_todos = current_user.recurring_todos.completed.all(:limit => 10, :order => 'completed_at DESC', :include => [:tags, :taggings]) + @last_completed_recurring_todos = current_user.recurring_todos.completed.limit(10).reorder('completed_at DESC').includes(:tags, :taggings) #TODO: @last_completed_contexts = current_user.contexts.completed.all(:limit => 10, :order => 'completed_at DESC') end private def get_unique_tags_of_user - tag_ids = @actions.find_by_sql([ + tag_ids = current_user.todos.find_by_sql([ "SELECT DISTINCT tags.id as id "+ "FROM tags, taggings, todos "+ - "WHERE todos.user_id=? "+ - "AND tags.id = taggings.tag_id " + - "AND taggings.taggable_id = todos.id ", current_user.id]) + "WHERE tags.id = taggings.tag_id " + + "AND taggings.taggable_id = todos.id "+ + "AND todos.user_id = #{current_user.id}"]) tags_ids_s = tag_ids.map(&:id).sort.join(",") return {} if tags_ids_s.blank? # return empty hash for .size to work - return Tag.find(:all, :conditions => "id in (" + tags_ids_s + ")") + return Tag.where("id in (#{tags_ids_s})") end def get_total_number_of_tags_of_user # same query as get_unique_tags_of_user except for the DISTINCT - return @actions.find_by_sql([ + return current_user.todos.find_by_sql([ "SELECT tags.id as id "+ "FROM tags, taggings, todos "+ - "WHERE todos.user_id=? "+ - "AND tags.id = taggings.tag_id " + - "AND taggings.taggable_id = todos.id ", current_user.id]).size + "WHERE tags.id = taggings.tag_id " + + "AND taggings.taggable_id = todos.id " + + "AND todos.user_id = #{current_user.id}"]).size end def init - @actions = @user.todos - @projects = @user.projects - @contexts = @user.contexts + @me = self # for meta programming # default chart dimensions @chart_width=460 @chart_height=250 @pie_width=@chart_width @pie_height=325 - + # get the current date wih time set to 0:0 - now = Time.new - @today = Time.utc(now.year, now.month, now.day, 0,0) + @today = Time.zone.now.utc.beginning_of_day # define the number of seconds in a day @seconds_per_day = 60*60*24 # define cut_off date and discard the time for a month, 3 months and a year - cut_off_time = 13.months.ago() - @cut_off_year = Time.utc(cut_off_time.year, cut_off_time.month, cut_off_time.day,0,0) - - cut_off_time = 16.months.ago() - @cut_off_year_plus3 = Time.utc(cut_off_time.year, cut_off_time.month, cut_off_time.day,0,0) - - cut_off_time = 31.days.ago - @cut_off_month = Time.utc(cut_off_time.year, cut_off_time.month, cut_off_time.day,0,0) - - cut_off_time = 91.days.ago - @cut_off_3months = Time.utc(cut_off_time.year, cut_off_time.month, cut_off_time.day,0,0) - + @cut_off_year = 12.months.ago.beginning_of_day + @cut_off_year_plus3 = 15.months.ago.beginning_of_day + @cut_off_month = 1.month.ago.beginning_of_day + @cut_off_3months = 3.months.ago.beginning_of_day end - + def get_stats_actions # time to complete - @completed_actions = @actions.find(:all, { - :select => "completed_at, created_at", - :conditions => "completed_at IS NOT NULL", - }) + @completed_actions = current_user.todos.completed.select("completed_at, created_at") + + actions_sum, actions_max = 0,0 + actions_min = @completed_actions.first ? @completed_actions.first.completed_at - @completed_actions.first.created_at : 0 - actions_sum, actions_max, actions_min = 0,0,-1 @completed_actions.each do |r| actions_sum += (r.completed_at - r.created_at) - actions_max = (r.completed_at - r.created_at) if (r.completed_at - r.created_at) > actions_max - - actions_min = (r.completed_at - r.created_at) if actions_min == -1 - actions_min = (r.completed_at - r.created_at) if (r.completed_at - r.created_at) < actions_min + actions_max = [(r.completed_at - r.created_at), actions_max].max + actions_min = [(r.completed_at - r.created_at), actions_min].min end - + sum_actions = @completed_actions.size - sum_actions = 1 if sum_actions==0 - + sum_actions = 1 if sum_actions==0 # to prevent dividing by zero + @actions_avg_ttc = (actions_sum/sum_actions)/@seconds_per_day @actions_max_ttc = actions_max/@seconds_per_day @actions_min_ttc = actions_min/@seconds_per_day - - min_ttc_sec = Time.utc(2000,1,1,0,0)+actions_min + + min_ttc_sec = Time.utc(2000,1,1,0,0)+actions_min # convert to a datetime @actions_min_ttc_sec = (min_ttc_sec).strftime("%H:%M:%S") @actions_min_ttc_sec = (actions_min / @seconds_per_day).round.to_s + " days " + @actions_min_ttc_sec if actions_min > @seconds_per_day - # get count of actions created and actions done in the past 30 days. - @sum_actions_done_last30days = @actions.count(:all, { - :conditions => ["completed_at > ? AND completed_at IS NOT NULL", @cut_off_month] - }) - @sum_actions_created_last30days = @actions.count(:all, { - :conditions => ["created_at > ?", @cut_off_month] - }) - + @sum_actions_done_last30days = current_user.todos.completed.completed_after(@cut_off_month).count + @sum_actions_created_last30days = current_user.todos.created_after(@cut_off_month).count + # get count of actions done in the past 12 months. - @sum_actions_done_last12months = @actions.count(:all, { - :conditions => ["completed_at > ? AND completed_at IS NOT NULL", @cut_off_year] - }) - @sum_actions_created_last12months = @actions.count(:all, { - :conditions => ["created_at > ?", @cut_off_year] - }) + @sum_actions_done_last12months = current_user.todos.completed.completed_after(@cut_off_year).count + @sum_actions_created_last12months = current_user.todos.created_after(@cut_off_year).count end def get_stats_contexts @@ -762,11 +490,11 @@ class StatsController < ApplicationController # # Went from GROUP BY c.id to c.id, c.name for compatibility with postgresql. # Since the name is forced to be unique, this should work. - @actions_per_context = @contexts.find_by_sql( + @actions_per_context = current_user.contexts.find_by_sql( "SELECT c.id AS id, c.name AS name, count(*) AS total "+ "FROM contexts c, todos t "+ "WHERE t.context_id=c.id "+ - "AND t.user_id="+@user.id.to_s+" "+ + "AND t.user_id=#{current_user.id} " + "GROUP BY c.id, c.name ORDER BY total DESC " + "LIMIT 5" ) @@ -775,14 +503,14 @@ class StatsController < ApplicationController # # Went from GROUP BY c.id to c.id, c.name for compatibility with postgresql. # Since the name is forced to be unique, this should work. - @running_actions_per_context = @contexts.find_by_sql( + @running_actions_per_context = current_user.contexts.find_by_sql( "SELECT c.id AS id, c.name AS name, count(*) AS total "+ "FROM contexts c, todos t "+ "WHERE t.context_id=c.id AND t.completed_at IS NULL AND NOT c.hide "+ - "AND t.user_id="+@user.id.to_s+" "+ + "AND t.user_id=#{current_user.id} " + "GROUP BY c.id, c.name ORDER BY total DESC " + "LIMIT 5" - ) + ) end def get_stats_projects @@ -790,61 +518,61 @@ class StatsController < ApplicationController # # Went from GROUP BY p.id to p.name for compatibility with postgresql. Since # the name is forced to be unique, this should work. - @projects_and_actions = @projects.find_by_sql( + @projects_and_actions = current_user.projects.find_by_sql( "SELECT p.id, p.name, count(*) AS count "+ "FROM projects p, todos t "+ "WHERE p.id = t.project_id "+ - "AND p.user_id="+@user.id.to_s+" "+ + "AND t.user_id=#{current_user.id} " + "GROUP BY p.id, p.name "+ "ORDER BY count DESC " + "LIMIT 10" ) - + # get the first 10 projects with their actions count of actions that have # been created or completed the past 30 days - + # using GROUP BY p.name (was: p.id) for compatibility with Postgresql. Since # you cannot create two contexts with the same name, this will work. - @projects_and_actions_last30days = @projects.find_by_sql([ + @projects_and_actions_last30days = current_user.projects.find_by_sql([ "SELECT p.id, p.name, count(*) AS count "+ "FROM todos t, projects p "+ "WHERE t.project_id = p.id AND "+ " (t.created_at > ? OR t.completed_at > ?) "+ - "AND p.user_id=? "+ + "AND t.user_id=#{current_user.id} " + "GROUP BY p.id, p.name "+ "ORDER BY count DESC " + - "LIMIT 10", @cut_off_month, @cut_off_month, @user.id] + "LIMIT 10", @cut_off_month, @cut_off_month] ) - + # get the first 10 projects and their running time (creation date versus # now()) - @projects_and_runtime_sql = @projects.find_by_sql( + @projects_and_runtime_sql = current_user.projects.find_by_sql( "SELECT id, name, created_at "+ "FROM projects "+ "WHERE state='active' "+ - "AND user_id="+@user.id.to_s+" "+ + "AND user_id=#{current_user.id} "+ "ORDER BY created_at ASC "+ "LIMIT 10" ) i=0 - @projects_and_runtime = Array.new(10, [-1, "n/a", "n/a"]) + @projects_and_runtime = Array.new(10, [-1, t('common.not_available_abbr'), t('common.not_available_abbr')]) @projects_and_runtime_sql.each do |r| - days = (@today - r.created_at) / @seconds_per_day - # add one so that a project that you just create returns 1 day + days = difference_in_days(@today, r.created_at) + # add one so that a project that you just created returns 1 day @projects_and_runtime[i]=[r.id, r.name, days.to_i+1] i += 1 end - + end - + def get_stats_tags # tag cloud code inspired by this article # http://www.juixe.com/techknow/index.php/2006/07/15/acts-as-taggable-tag-cloud/ levels=10 # TODO: parameterize limit - + # Get the tag cloud for all tags for actions query = "SELECT tags.id, name, count(*) AS count" query << " FROM taggings, tags, todos" @@ -856,11 +584,11 @@ class StatsController < ApplicationController query << " ORDER BY count DESC, name" query << " LIMIT 100" @tags_for_cloud = Tag.find_by_sql(query).sort_by { |tag| tag.name.downcase } - + max, @tags_min = 0, 0 @tags_for_cloud.each { |t| - max = t.count.to_i if t.count.to_i > max - @tags_min = t.count.to_i if t.count.to_i < @tags_min + max = [t.count.to_i, max].max + @tags_min = [t.count.to_i, @tags_min].min } @tags_divisor = ((max - @tags_min) / levels) + 1 @@ -883,38 +611,122 @@ class StatsController < ApplicationController max_90days, @tags_min_90days = 0, 0 @tags_for_cloud_90days.each { |t| - max_90days = t.count.to_i if t.count.to_i > max_90days - @tags_min_90days = t.count.to_i if t.count.to_i < @tags_min_90days + max_90days = [t.count.to_i, max_90days].max + @tags_min_90days = [t.count.to_i, @tags_min_90days].min } @tags_divisor_90days = ((max_90days - @tags_min_90days) / levels) + 1 - end - - def get_ids_from (actions_running_time, week_from, week_to, at_end) - count=0 - selected_todo_ids = "" - - actions_running_time.each do |r| - days = (@today - r.created_at) / @seconds_per_day - weeks = (days/7).to_i + + def get_ids_from (actions, week_from, week_to, at_end) + selected_todo_ids = [] + + actions.each do |r| + weeks = difference_in_weeks(@today, r.created_at) if at_end - if weeks >= week_from - selected_todo_ids += r.id.to_s+"," - count+=1 - end + selected_todo_ids << r.id.to_s if weeks >= week_from else - if weeks.between?(week_from, week_to-1) - selected_todo_ids += r.id.to_s+"," - count+=1 - end + selected_todo_ids << r.id.to_s if weeks.between?(week_from, week_to-1) end end - - # strip trailing comma - selected_todo_ids = selected_todo_ids[0..selected_todo_ids.length-2] - - return selected_todo_ids, count + + return selected_todo_ids + end + + # uses the supplied block to determine array of indexes in hash + # the block should return an array of indexes each is added to the hash and summed + def convert_to_array(records, upper_bound) + # use 0 to initialise action count to zero + a = Array.new(upper_bound){|i| 0 } + records.each { |r| (yield r).each { |i| a[i] += 1 } } + return a end -end \ No newline at end of file + def convert_to_months_from_today_array(records, array_size, date_method_on_todo) + return convert_to_array(records, array_size){ |r| [difference_in_months(@today, r.send(date_method_on_todo))]} + end + + def convert_to_days_from_today_array(records, array_size, date_method_on_todo) + return convert_to_array(records, array_size){ |r| [difference_in_days(@today, r.send(date_method_on_todo))]} + end + + def convert_to_weeks_from_today_array(records, array_size, date_method_on_todo) + return convert_to_array(records, array_size) { |r| [difference_in_weeks(@today, r.send(date_method_on_todo))]} + end + + def convert_to_weeks_running_array(records, array_size) + return convert_to_array(records, array_size) { |r| [difference_in_weeks(r.completed_at, r.created_at)]} + end + + def convert_to_weeks_running_from_today_array(records, array_size) + return convert_to_array(records, array_size) { |r| week_indexes_of(r) } + end + + def week_indexes_of(record) + a = [] + start_week = difference_in_weeks(@today, record.created_at) + end_week = record.completed_at ? difference_in_weeks(@today, record.completed_at) : 0 + end_week.upto(start_week) { |i| a << i }; + return a + end + + # returns a new array containing all elems of array up to cut_off and + # adds the sum of the rest of array to the last elem + def cut_off_array_with_sum(array, cut_off) + # +1 to hold sum of rest + a = Array.new(cut_off+1){|i| array[i]||0} + # add rest of array to last elem + a[cut_off] += array.inject(:+) - a.inject(:+) + return a + end + + def cut_off_array(array, cut_off) + return Array.new(cut_off){|i| array[i]||0} + end + + def convert_to_cummulative_array(array, max) + # calculate fractions + a = Array.new(array.size){|i| array[i]*100.0/max} + # make cummulative + 1.upto(array.size-1){ |i| a[i] += a[i-1] } + return a + end + + # assumes date1 > date2 + # this results in the number of months before the month of date1, not taking days into account, so diff of 31-dec and 1-jan is 1 month! + def difference_in_months(date1, date2) + return (date1.utc.year - date2.utc.year)*12 + (date1.utc.month - date2.utc.month) + end + + # assumes date1 > date2 + def difference_in_days(date1, date2) + return ((date1.utc.at_midnight-date2.utc.at_midnight)/@seconds_per_day).to_i + end + + # assumes date1 > date2 + def difference_in_weeks(date1, date2) + return difference_in_days(date1, date2) / 7 + end + + def three_month_avg(set, i) + return ( (set[i]||0) + (set[i+1]||0) + (set[i+2]||0) ) / 3.0 + end + + def interpolate_avg(set, percent) + return (set[0]*(1/percent) + set[1] + set[2]) / 3.0 + end + + def correct_last_two_months(month_data, count) + month_data[count] = month_data[count] * 3 + month_data[count-1] = month_data[count-1] * 3 / 2 if count > 1 + end + + def find_running_avg_array(done_array, created_array, upper_bound) + avg_done = Array.new(upper_bound){ |i| three_month_avg(done_array,i) } + avg_created = Array.new(upper_bound){ |i| three_month_avg(created_array,i) } + avg_done[0] = avg_created[0] = "null" + + return avg_done, avg_created + end + +end diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index ffce91b9..8660eae8 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -1,42 +1,113 @@ class TodosController < ApplicationController - helper :todos - skip_before_filter :login_required, :only => [:index, :calendar, :tag] prepend_before_filter :login_or_feed_token_required, :only => [:index, :calendar, :tag] append_before_filter :find_and_activate_ready, :only => [:index, :list_deferred] - # TODO: replace :except with :only - append_before_filter :init, :except => [ :tag, :tags, :destroy, :done, - :check_deferred, :toggle_check, :toggle_star, :edit, :update, :defer, :create, - :calendar, :auto_complete_for_predecessor, :remove_predecessor, :add_predecessor] + # # TODO: replace :except with :only + # append_before_filter :init, :except => [ :tag, :tags, :destroy, :done, + # :check_deferred, :toggle_check, :toggle_star, :edit, :update, :defer, :create, + # :calendar, :auto_complete_for_predecessor, :remove_predecessor, :add_predecessor] protect_from_forgery :except => :check_deferred - # these are needed for todo_feed_content. TODO: remove this view stuff from controller - include ActionView::Helpers::SanitizeHelper - extend ActionView::Helpers::SanitizeHelper::ClassMethods - + def with_parent_resource_scope(&block) + @feed_title = t('common.actions') + if (params[:context_id]) + @context = current_user.contexts.find_by_params(params) + @feed_title = @feed_title + t('todos.feed_title_in_context', :context => @context.name) + Todo.send :where, ['todos.context_id = ?', @context.id] do + yield + end + elsif (params[:project_id]) + @project = current_user.projects.find_by_params(params) + @feed_title = @feed_title + t('todos.feed_title_in_project', :project => @project.name) + @project_feed = true + Todo.send :where, ['todos.project_id = ?', @project.id] do + yield + end + else + yield + end + end + def index - @projects = current_user.projects.find(:all, :include => [:default_context]) - @contexts = current_user.contexts.find(:all) - + @source_view = params['_source_view'] || 'todo' + init_data_for_sidebar unless mobile? + + @todos = current_user.todos.includes(Todo::DEFAULT_INCLUDES) + + # TODO: refactor text feed for done todos to todos/done.text, not /todos.text?done=true + if params[:done] + @not_done_todos = current_user.todos.completed.completed_after(Time.zone.now - params[:done].to_i.days) + else + @not_done_todos = current_user.todos.active.not_hidden + end + + @not_done_todos = @not_done_todos. + reorder("todos.due IS NULL, todos.due ASC, todos.created_at ASC"). + includes(Todo::DEFAULT_INCLUDES) + @not_done_todos = @not_done_todos.limit(sanitize(params[:limit])) if params[:limit] + + if params[:due] + due_within_when = Time.zone.now + params['due'].to_i.days + @not_done_todos = @not_done_todos.where('todos.due <= ?', due_within_when) + end + + if params[:tag] + tag = Tag.find_by_name(params['tag']) + @not_done_todos = @not_done_todos.where('taggings.tag_id = ?', tag.id) + end + + if params[:context_id] + context = current_user.contexts.find(params[:context_id]) + @not_done_todos = @not_done_todos.where('context_id' => context.id) + end + + if params[:project_id] + project = current_user.projects.find(params[:project_id]) + @not_done_todos = @not_done_todos.where('project_id' => project) + end + + @projects = current_user.projects.includes(:default_context) + @contexts = current_user.contexts @contexts_to_show = current_user.contexts.active + + # If you've set no_completed to zero, the completed items box isn't shown + # on the home page + max_completed = current_user.prefs.show_number_completed + @done = current_user.todos.completed.limit(max_completed).includes(Todo::DEFAULT_INCLUDES) unless max_completed == 0 respond_to do |format| - format.html &render_todos_html - format.m &render_todos_mobile + format.html do + @page_title = t('todos.task_list_title') + # Set count badge to number of not-done, not hidden context items + @count = current_user.todos.active.not_hidden.count(:all) + end + format.m do + @page_title = t('todos.mobile_todos_page_title') + @home = true + + cookies[:mobile_url]= { :value => request.fullpath, :secure => SITE_CONFIG['secure_cookies']} + determine_down_count + + render :action => 'index' + end + format.text do + # somehow passing Mime::TEXT using content_type to render does not work + headers['Content-Type']=Mime::TEXT.to_s + render :content_type => Mime::TEXT + end format.xml { render :xml => @todos.to_xml( *to_xml_params ) } - format.rss &render_rss_feed - format.atom &render_atom_feed - format.text &render_text_feed - format.ics &render_ical_feed + format.rss { @feed_title, @feed_description = 'Tracks Actions', "Actions for #{current_user.display_name}" } + format.atom { @feed_title, @feed_description = 'Tracks Actions', "Actions for #{current_user.display_name}" } + format.ics end end def new @projects = current_user.projects.active - @contexts = current_user.contexts.find(:all) + @contexts = current_user.contexts respond_to do |format| format.m { @new_mobile = true @@ -47,7 +118,6 @@ class TodosController < ApplicationController # we have a project but not a context -> use the default context @mobile_from_context = @mobile_from_project.default_context end - render :action => "new" } end end @@ -76,7 +146,7 @@ class TodosController < ApplicationController @todo.project_id = project.id elsif !(p.project_id.nil? || p.project_id.blank?) project = current_user.projects.find_by_id(p.project_id) - @todo.errors.add(:project, "unknown") if project.nil? + @todo.errors[:project] << "unknown" if project.nil? end if p.context_specified_by_name? @@ -86,37 +156,31 @@ class TodosController < ApplicationController @todo.context_id = context.id elsif !(p.context_id.nil? || p.context_id.blank?) context = current_user.contexts.find_by_id(p.context_id) - @todo.errors.add(:context, "unknown") if context.nil? + @todo.errors[:context] << "unknown" if context.nil? end if @todo.errors.empty? @todo.starred= (params[:new_todo_starred]||"").include? "true" if params[:new_todo_starred] - @todo.add_predecessor_list(predecessor_list) - - # Fix for #977 because AASM overrides @state on creation - specified_state = @todo.state @saved = @todo.save - @todo.update_state_from_project if @saved else @saved = false end - unless (@saved == false) || tag_list.blank? + unless ( !@saved ) || tag_list.blank? @todo.tag_with(tag_list) @todo.tags.reload end if @saved - unless @todo.uncompleted_predecessors.empty? || @todo.state == 'project_hidden' - @todo.state = 'pending' - end - @todo.save + @todo.block! unless @todo.uncompleted_predecessors.empty? || @todo.state == 'project_hidden' + @saved = @todo.save end @todo.reload if @saved @todo_was_created_deferred = @todo.deferred? + @todo_was_created_blocked = @todo.pending? respond_to do |format| format.html { redirect_to :action => "index" } @@ -125,16 +189,16 @@ class TodosController < ApplicationController if @saved redirect_to @return_path else - @projects = current_user.projects.find(:all) - @contexts = current_user.contexts.find(:all) + @projects = current_user.projects + @contexts = current_user.contexts render :action => "new" end end format.js do if @saved determine_down_count - @contexts = current_user.contexts.find(:all) if @new_context_created - @projects = current_user.projects.find(:all) if @new_project_created + @contexts = current_user.contexts if @new_context_created + @projects = current_user.projects if @new_project_created @initial_context_name = params['default_context_name'] @initial_project_name = params['default_project_name'] @initial_tags = params['initial_tag_list'] @@ -151,7 +215,7 @@ class TodosController < ApplicationController if @saved head :created, :location => todo_url(@todo) else - render :xml => @todo.errors.to_xml, :status => 422 + render_failure @todo.errors.to_xml.html_safe, 409 end end end @@ -176,46 +240,64 @@ class TodosController < ApplicationController @sequential = !params[:todos_sequential].blank? && params[:todos_sequential]=='true' - @todos = [] + @todos_init = [] @predecessor = nil + validates = true + errors = [] + + # first build all todos and check if they would validate on save params[:todo][:multiple_todos].split("\n").map do |line| unless line.blank? @todo = current_user.todos.build( :description => line) @todo.project_id = @project_id @todo.context_id = @context_id - @saved = @todo.save + validates = false if @todo.invalid? + + @todos_init << @todo + end + end + + # if all todos validate, then save them and add predecessors and tags + @todos = [] + if validates + @todos_init.each do |todo| + @saved = todo.save + validates = validates && @saved if @predecessor && @saved && @sequential - @todo.add_predecessor(@predecessor) - @todo.block! + todo.add_predecessor(@predecessor) + todo.block! end unless (@saved == false) || tag_list.blank? - @todo.tag_with(tag_list) - @todo.tags.reload + todo.tag_with(tag_list) + todo.tags.reload end - @todos << @todo - @not_done_todos << @todo if @new_context_created - @predecessor = @todo + @todos << todo + @not_done_todos << todo if @new_context_created + @predecessor = todo end + else + @todos = @todos_init + @saved = false end - + respond_to do |format| format.html { redirect_to :action => "index" } format.js do determine_down_count if @saved - @contexts = current_user.contexts.find(:all) if @new_context_created - @projects = current_user.projects.find(:all) if @new_project_created + @contexts = current_user.contexts if @new_context_created + @projects = current_user.projects if @new_project_created @initial_context_name = params['default_context_name'] @initial_project_name = params['default_project_name'] @initial_tags = params['initial_tag_list'] - if @todos.size > 0 + if @saved && @todos.size > 0 @default_tags = @todos[0].project.default_tags unless @todos[0].project.nil? else - @multiple_error = t('todos.next_action_needed') - @saved = false; + @multiple_error = @todos.size > 0 ? "" : t('todos.next_action_needed') + @saved = false @default_tags = current_user.projects.find_by_name(@initial_project_name).default_tags unless @initial_project_name.blank? end @@ -236,23 +318,22 @@ class TodosController < ApplicationController end def edit - @todo = current_user.todos.find(params['id'], :include => Todo::DEFAULT_INCLUDES) + @todo = current_user.todos.find(params['id']) @source_view = params['_source_view'] || 'todo' @tag_name = params['_tag_name'] respond_to do |format| format.js format.m { @projects = current_user.projects.active - @contexts = current_user.contexts.find(:all) + @contexts = current_user.contexts @edit_mobile = true @return_path=cookies[:mobile_url] ? cookies[:mobile_url] : mobile_path - render :template => "/todos/edit_mobile.html.erb" } end end def show - @todo = current_user.todos.find(params['id']) + @todo = current_user.todos.find_by_id(params['id']) respond_to do |format| format.m { render :action => 'show' } format.xml { render :xml => @todo.to_xml( *to_xml_params ) } @@ -261,13 +342,13 @@ class TodosController < ApplicationController def add_predecessor @source_view = params['_source_view'] || 'todo' - @predecessor = current_user.todos.find(params['predecessor']) + @predecessor = current_user.todos.find_by_id(params['predecessor']) @predecessors = @predecessor.predecessors - @todo = current_user.todos.find(params['successor'], :include => Todo::DEFAULT_INCLUDES) + @todo = current_user.todos.includes(Todo::DEFAULT_INCLUDES).find_by_id(params['successor']) @original_state = @todo.state unless @predecessor.completed? @todo.add_predecessor(@predecessor) - @todo.block! + @todo.block! unless @todo.pending? @saved = @todo.save @status_message = t('todos.added_dependency', :dependency => @predecessor.description) @@ -282,8 +363,8 @@ class TodosController < ApplicationController def remove_predecessor @source_view = params['_source_view'] || 'todo' - @todo = current_user.todos.find(params['id'], :include => Todo::DEFAULT_INCLUDES) - @predecessor = current_user.todos.find(params['predecessor']) + @todo = current_user.todos.find_by_id(params['id']).includes(Todo::DEFAULT_INCLUDES) + @predecessor = current_user.todos.find_by_id(params['predecessor']) @predecessors = @predecessor.predecessors @successor = @todo @removed = @successor.remove_predecessor(@predecessor) @@ -348,64 +429,25 @@ class TodosController < ApplicationController end end format.m { - if cookies[:mobile_url] - old_path = cookies[:mobile_url] - cookies[:mobile_url] = {:value => nil, :secure => SITE_CONFIG['secure_cookies']} - notify(:notice, t("todos.action_marked_complete", :description => @todo.description, :completed => @todo.completed? ? 'complete' : 'incomplete')) - redirect_to old_path + if @saved + if cookies[:mobile_url] + old_path = cookies[:mobile_url] + cookies[:mobile_url] = {:value => nil, :secure => SITE_CONFIG['secure_cookies']} + notify(:notice, t("todos.action_marked_complete", :description => @todo.description, :completed => @todo.completed? ? 'complete' : 'incomplete')) + redirect_to old_path + else + notify(:notice, t("todos.action_marked_complete", :description => @todo.description, :completed => @todo.completed? ? 'complete' : 'incomplete')) + redirect_to todos_path(:format => 'm') + end else - notify(:notice, t("todos.action_marked_complete", :description => @todo.description, :completed => @todo.completed? ? 'complete' : 'incomplete')) - redirect_to todos_path(:format => 'm') + render :action => "edit", :format => :m end } end end - def mobile_done - # copied from toggle_check, left out other formats as they shouldn't come here - # ultimately would like to just use toggle_check - @todo = current_user.todos.find(params['id']) - @source_view = params['_source_view'] || 'todo' - @original_item_due = @todo.due - @original_item_was_deferred = @todo.deferred? - @original_item_was_pending = @todo.pending? - @original_item_was_hidden = @todo.hidden? - @original_item_context_id = @todo.context_id - @original_item_project_id = @todo.project_id - @todo_was_completed_from_deferred_or_blocked_state = @original_item_was_deferred || @original_item_was_pending - @saved = @todo.toggle_completion! - - @todo_was_blocked_from_completed_state = @todo.pending? # since we toggled_completion the previous state was completed - - # check if this todo has a related recurring_todo. If so, create next todo - @new_recurring_todo = check_for_next_todo(@todo) if @saved - - @predecessors = @todo.uncompleted_predecessors - if @saved - if @todo.completed? - @pending_to_activate = @todo.activate_pending_todos - else - @active_to_block = @todo.block_successors - end - end - - if @saved - if cookies[:mobile_url] - old_path = cookies[:mobile_url] - cookies[:mobile_url] = {:value => nil, :secure => SITE_CONFIG['secure_cookies']} - notify(:notice, t("todos.action_marked_complete", :description => @todo.description, :completed => @todo.completed? ? 'complete' : 'incomplete')) - redirect_to old_path - else - notify(:notice, t("todos.action_marked_complete", :description => @todo.description, :completed => @todo.completed? ? 'complete' : 'incomplete')) - redirect_to todos_path(:format => 'm') - end - else - render :action => "edit", :format => :m - end - end - def toggle_star - @todo = current_user.todos.find(params['id']) + @todo = current_user.todos.find_by_id(params['id']) @todo.toggle_star! @saved = true # cannot determine error respond_to do |format| @@ -428,9 +470,9 @@ class TodosController < ApplicationController def change_context # TODO: is this method used? - @todo = Todo.find(params[:todo][:id]) + @todo = Todo.find_by_id(params[:todo][:id]) @original_item_context_id = @todo.context_id - @context = Context.find(params[:todo][:context_id]) + @context = Context.find_by_id(params[:todo][:context_id]) @todo.context = @context @saved = @todo.save @@ -446,7 +488,7 @@ class TodosController < ApplicationController end def update - @todo = current_user.todos.find(params['id']) + @todo = current_user.todos.find_by_id(params['id']) @source_view = params['_source_view'] || 'todo' init_data_for_sidebar unless mobile? @@ -500,7 +542,7 @@ class TodosController < ApplicationController def destroy @source_view = params['_source_view'] || 'todo' - @todo = current_user.todos.find(params['id']) + @todo = current_user.todos.find_by_id(params['id']) @original_item_due = @todo.due @context_id = @todo.context_id @project_id = @todo.project_id @@ -549,7 +591,7 @@ class TodosController < ApplicationController format.js do if @saved determine_down_count - if source_view_is_one_of(:todo, :deferred, :project) + if source_view_is_one_of(:todo, :deferred, :project, :context) determine_remaining_in_context_count(@context_id) elsif source_view_is :calendar @original_item_due_id = get_due_id_for_calendar(@original_item_due) @@ -585,7 +627,7 @@ class TodosController < ApplicationController @source_view = 'done' @page_title = t('todos.completed_tasks_title') - @done = current_user.todos.completed.paginate :page => params[:page], :per_page => 20, :order => 'completed_at DESC', :include => Todo::DEFAULT_INCLUDES + @done = current_user.todos.completed.includes(Todo::DEFAULT_INCLUDES).reorder('completed_at DESC').paginate :page => params[:page], :per_page => 20 @count = @done.size end @@ -593,22 +635,30 @@ class TodosController < ApplicationController @source_view = 'deferred' @page_title = t('todos.deferred_tasks_title') - @contexts_to_show = @contexts = current_user.contexts.find(:all) + @contexts_to_show = @contexts = current_user.contexts includes = params[:format]=='xml' ? [:context, :project] : Todo::DEFAULT_INCLUDES - @not_done_todos = current_user.todos.deferred(:include => includes) + current_user.todos.pending(:include => includes) + @not_done_todos = current_user.todos.deferred.includes(includes) + current_user.todos.pending.includes(includes) @down_count = @count = @not_done_todos.size respond_to do |format| - format.html - format.m { render :action => 'mobile_list_deferred' } + format.html do + init_not_done_counts + init_project_hidden_todo_counts + @active_projects = current_user.projects.active + @active_contexts = current_user.contexts.active + @hidden_projects = current_user.projects.hidden + @hidden_contexts = current_user.contexts.hidden + @completed_projects = current_user.projects.completed + end + format.m format.xml { render :xml => @not_done_todos.to_xml( *to_xml_params ) } end end - # Check for any due tickler items, activate them Called by - # periodically_call_remote + # Check for any due tickler items, activate them + # Called by periodically_call_remote def check_deferred @due_tickles = current_user.deferred_todos.find_and_activate_ready respond_to do |format| @@ -618,12 +668,12 @@ class TodosController < ApplicationController end def filter_to_context - context = current_user.contexts.find(params['context']['id']) + context = current_user.contexts.find_by_id(params['context']['id']) redirect_to context_todos_path(context, :format => 'm') end def filter_to_project - project = current_user.projects.find(params['project']['id']) + project = current_user.projects.find_by_id(params['project']['id']) redirect_to project_todos_path(project, :format => 'm') end @@ -642,21 +692,29 @@ class TodosController < ApplicationController todos_with_tag_ids = find_todos_with_tag_expr(@tag_expr) - @not_done_todos = todos_with_tag_ids.active.not_hidden.find(:all, - :order => 'todos.due IS NULL, todos.due ASC, todos.created_at ASC', :include => Todo::DEFAULT_INCLUDES) - @hidden_todos = todos_with_tag_ids.hidden.find(:all, - :include => Todo::DEFAULT_INCLUDES, - :order => 'todos.completed_at DESC, todos.created_at DESC') - @deferred = todos_with_tag_ids.deferred.find(:all, - :order => 'todos.show_from ASC, todos.created_at DESC', :include => Todo::DEFAULT_INCLUDES) - @pending = todos_with_tag_ids.blocked.find(:all, - :order => 'todos.show_from ASC, todos.created_at DESC', :include => Todo::DEFAULT_INCLUDES) + @not_done_todos = todos_with_tag_ids. + active.not_hidden. + reorder('todos.due IS NULL, todos.due ASC, todos.created_at ASC'). + includes(Todo::DEFAULT_INCLUDES) + @hidden_todos = todos_with_tag_ids. + hidden. + reorder('todos.completed_at DESC, todos.created_at DESC'). + includes(Todo::DEFAULT_INCLUDES) + @deferred = todos_with_tag_ids. + deferred. + reorder('todos.show_from ASC, todos.created_at DESC'). + includes(Todo::DEFAULT_INCLUDES) + @pending = todos_with_tag_ids. + blocked. + reorder('todos.show_from ASC, todos.created_at DESC'). + includes(Todo::DEFAULT_INCLUDES) # If you've set no_completed to zero, the completed items box isn't shown on # the tag page - @done = todos_with_tag_ids.completed.find(:all, - :limit => current_user.prefs.show_number_completed, - :order => 'todos.completed_at DESC', :include => Todo::DEFAULT_INCLUDES) + @done = todos_with_tag_ids.completed. + limit(current_user.prefs.show_number_completed). + reorder('todos.completed_at DESC'). + includes(Todo::DEFAULT_INCLUDES) @projects = current_user.projects @contexts = current_user.contexts @@ -665,7 +723,7 @@ class TodosController < ApplicationController # Set defaults for new_action @initial_tags = @tag_name unless @not_done_todos.empty? - @context = current_user.contexts.find_by_id(@not_done_todos[0].context_id) + @context = current_user.contexts.find(@not_done_todos.first.context_id) end # Set count badge to number of items with this tag @@ -675,8 +733,7 @@ class TodosController < ApplicationController respond_to do |format| format.html format.m { - cookies[:mobile_url]= {:value => request.request_uri, :secure => SITE_CONFIG['secure_cookies']} - render :action => "mobile_tag" + cookies[:mobile_url]= {:value => request.fullpath, :secure => SITE_CONFIG['secure_cookies']} } format.text { render :action => 'index', :layout => false, :content_type => Mime::TEXT @@ -708,15 +765,19 @@ class TodosController < ApplicationController @tag = Tag.find_by_name(@tag_name) @tag = Tag.new(:name => @tag_name) if @tag.nil? - @done = current_user.todos.completed.with_tag(@tag.id).paginate :page => params[:page], :per_page => 20, :order => 'completed_at DESC', :include => Todo::DEFAULT_INCLUDES + @done = current_user.todos.completed.with_tag(@tag.id).reorder('completed_at DESC').includes(Todo::DEFAULT_INCLUDES).paginate :page => params[:page], :per_page => 20 @count = @done.size render :template => 'todos/all_done' end def tags - @tags = Tag.find(:all, :conditions =>['name like ?', '%'+params[:term]+'%']) + # TODO: limit to current_user + tags_beginning = Tag.where('name like ?', params[:term]+'%') + tags_all = Tag.where('name like ?', '%'+params[:term]+'%') + tags_all= tags_all - tags_beginning + respond_to do |format| - format.autocomplete { render :text => for_autocomplete(@tags, params[:term]) } + format.autocomplete { render :text => for_autocomplete(tags_beginning+tags_all, params[:term]) } end end @@ -724,7 +785,7 @@ class TodosController < ApplicationController @source_view = params['_source_view'] || 'todo' numdays = params['days'].to_i - @todo = current_user.todos.find(params[:id]) + @todo = current_user.todos.find_by_id(params[:id]) @original_item_context_id = @todo.context_id @todo_deferred_state_changed = true @new_context_created = false @@ -741,7 +802,7 @@ class TodosController < ApplicationController determine_remaining_in_context_count(@todo.context_id) source_view do |page| page.project { - @remaining_undone_in_project = current_user.projects.find(@todo.project_id).todos.not_completed.count + @remaining_undone_in_project = current_user.projects.find_by_id(@todo.project_id).todos.not_completed.count @original_item_project_id = @todo.project_id } page.tag { @@ -769,45 +830,45 @@ class TodosController < ApplicationController @source_view = params['_source_view'] || 'calendar' @page_title = t('todos.calendar_page_title') - @projects = current_user.projects.find(:all) + @projects = current_user.projects due_today_date = Time.zone.now - due_this_week_date = Time.zone.now.end_of_week + due_this_week_date = due_today_date.end_of_week due_next_week_date = due_this_week_date + 7.days - due_this_month_date = Time.zone.now.end_of_month + due_this_month_date = due_today_date.end_of_month included_tables = Todo::DEFAULT_INCLUDES - @due_today = current_user.todos.not_completed.find(:all, - :include => included_tables, - :conditions => ['todos.due <= ?', due_today_date], - :order => "due") - @due_this_week = current_user.todos.not_completed.find(:all, - :include => included_tables, - :conditions => ['todos.due > ? AND todos.due <= ?', due_today_date, due_this_week_date], - :order => "due") - @due_next_week = current_user.todos.not_completed.find(:all, - :include => included_tables, - :conditions => ['todos.due > ? AND todos.due <= ?', due_this_week_date, due_next_week_date], - :order => "due") - @due_this_month = current_user.todos.not_completed.find(:all, - :include => included_tables, - :conditions => ['todos.due > ? AND todos.due <= ?', due_next_week_date, due_this_month_date], - :order => "due") - @due_after_this_month = current_user.todos.not_completed.find(:all, - :include => included_tables, - :conditions => ['todos.due > ?', due_this_month_date], - :order => "due") + @due_today = current_user.todos.not_completed. + where('todos.due <= ?', due_today_date). + includes(included_tables). + reorder("due") + @due_this_week = current_user.todos.not_completed. + where('todos.due > ? AND todos.due <= ?', due_today_date, due_this_week_date). + includes(included_tables). + reorder("due") + @due_next_week = current_user.todos.not_completed. + where('todos.due > ? AND todos.due <= ?', due_this_week_date, due_next_week_date). + includes(included_tables). + reorder("due") + @due_this_month = current_user.todos.not_completed. + where('todos.due > ? AND todos.due <= ?', due_next_week_date, due_this_month_date). + includes(included_tables). + reorder("due") + @due_after_this_month = current_user.todos.not_completed. + where('todos.due > ?', due_this_month_date). + includes(included_tables). + reorder("due") @count = current_user.todos.not_completed.are_due.count respond_to do |format| format.html format.ics { - @due_all = current_user.todos.not_completed.are_due.find(:all, :order => "due") + @due_all = current_user.todos.not_completed.are_due.reorder("due") render :action => 'calendar', :layout => false, :content_type => Mime::ICS } format.xml { - @due_all = current_user.todos.not_completed.are_due.find(:all, :order => "due") + @due_all = current_user.todos.not_completed.are_due.reorder("due") render :xml => @due_all.to_xml( *to_xml_params ) } end @@ -825,61 +886,59 @@ class TodosController < ApplicationController def auto_complete_for_predecessor unless params['id'].nil? get_todo_from_params - # Begin matching todos in current project - @items = current_user.todos.find(:all, - :include => [:context, :project], - :conditions => [ '(todos.state = ? OR todos.state = ? OR todos.state = ?) AND ' + - 'NOT (id = ?) AND lower(description) LIKE ? AND project_id = ?', - 'active', 'pending', 'deferred', - @todo.id, - '%' + params[:predecessor_list].downcase + '%', - @todo.project_id ], - :order => 'description ASC', - :limit => 10 - ) - if @items.empty? # Match todos in other projects - @items = current_user.todos.find(:all, - :include => [:context, :project], - :conditions => [ '(todos.state = ? OR todos.state = ? OR todos.state = ?) AND ' + - 'NOT (id = ?) AND lower(description) LIKE ?', - 'active', 'pending', 'deferred', - params[:id], '%' + params[:term].downcase + '%' ], - :order => 'description ASC', - :limit => 10 - ) - end + # Begin matching todos in current project, excluding @todo itself + @items = @todo.project.todos.not_completed. + where('(LOWER(todos.description) LIKE ?) AND NOT(todos.id=?)', "%#{params[:term].downcase}%", @todo.id). + includes(:context, :project). + reorder('description ASC'). + limit(10) unless @todo.project.nil? + # Then look in the current context, excluding @todo itself + @items = @todo.context.todos.not_completed + where('(LOWER(todos.description) LIKE ?) AND NOT(todos.id=?)', "%#{params[:term].downcase}%", @todo.id). + includes(:context, :project). + reorder('description ASC'). + limit(10) unless !@items.empty? || @todo.context.nil? + # Match todos in other projects, excluding @todo itself + @items = current_user.todos.not_completed. + where('(LOWER(todos.description) LIKE ?) AND NOT(todos.id=?)', "%#{params[:term].downcase}%", @todo.id). + includes(:context, :project). + reorder('description ASC'). + limit(10) unless !@items.empty? else - # New todo - TODO: Filter on project - @items = current_user.todos.find(:all, - :include => [:context, :project], - :conditions => [ '(todos.state = ? OR todos.state = ? OR todos.state = ?) AND lower(description) LIKE ?', - 'active', 'pending', 'deferred', - '%' + params[:term].downcase + '%' ], - :order => 'description ASC', - :limit => 10 - ) + # New todo - TODO: Filter on current project in project view + @items = current_user.todos.not_completed. + where('(LOWER(todos.description) LIKE ?)', "%#{params[:term].downcase}%"). + includes(:context, :project). + reorder('description ASC'). + limit(10) end render :inline => format_dependencies_as_json_for_auto_complete(@items) end def convert_to_project - @todo = current_user.todos.find(params[:id]) + @todo = current_user.todos.find_by_id(params[:id]) @project = current_user.projects.new(:name => @todo.description, :description => @todo.notes, :default_context => @todo.context) - @todo.destroy - @project.save! - redirect_to project_url(@project) + + unless @project.invalid? + @todo.destroy + @project.save! + redirect_to project_url(@project) + else + flash[:error] = "Could not create project from todo: #{@project.errors.full_messages[0]}" + redirect_to request.env["HTTP_REFERER"] || root_url + end end def show_notes - @todo = current_user.todos.find(params['id']) + @todo = current_user.todos.find_by_id(params['id']) @return_path=cookies[:mobile_url] ? cookies[:mobile_url] : mobile_path respond_to do |format| format.html { redirect_to home_path, "Viewing note of todo is not implemented" } format.m { - render:action => "mobile_show_notes" + render :action => "show_notes" } end end @@ -897,143 +956,13 @@ class TodosController < ApplicationController def get_todo_from_params # TODO: this was a :append_before but was removed to tune performance per # method. Reconsider re-enabling it - @todo = current_user.todos.find(params['id']) + @todo = current_user.todos.find_by_id(params['id']) end def find_and_activate_ready current_user.deferred_todos.find_and_activate_ready end - def init - @source_view = params['_source_view'] || 'todo' - init_data_for_sidebar unless mobile? - init_todos - end - - def with_feed_query_scope(&block) - unless TodosController.is_feed_request(request) - Todo.send(:with_scope, :find => {:conditions => ['todos.state = ?', 'active']}) do - yield - return - end - end - condition_builder = FindConditionBuilder.new - - if params.key?('done') - condition_builder.add 'todos.state = ?', 'completed' - else - condition_builder.add 'todos.state = ?', 'active' - end - - @title = t('todos.next_actions_title') - @description = t('todos.next_actions_description') - - if params.key?('due') - due_within = params['due'].to_i - due_within_when = Time.zone.now + due_within.days - condition_builder.add('todos.due <= ?', due_within_when) - due_within_date_s = due_within_when.strftime("%Y-%m-%d") - @title << t('todos.next_actions_title_additions.due_today') if (due_within == 0) - @title << t('todos.next_actions_title_additions.due_within_a_week') if (due_within == 6) - @description << t('todos.next_actions_description_additions.due_date', :due_date => due_within_date_s) - end - - if params.key?('done') - done_in_last = params['done'].to_i - condition_builder.add('todos.completed_at >= ?', Time.zone.now - done_in_last.days) - @title << t('todos.next_actions_title_additions.completed') - @description << t('todos.next_actions_description_additions.completed', :count => done_in_last.to_s) - end - - if params.key?('tag') - tag = Tag.find_by_name(params['tag']) - if tag.nil? - tag = Tag.new(:name => params['tag']) - end - condition_builder.add('taggings.tag_id = ?', tag.id) - end - - Todo.send :with_scope, :find => {:conditions => condition_builder.to_conditions} do - yield - end - - end - - def with_parent_resource_scope(&block) - @feed_title = t('common.actions') - if (params[:context_id]) - @context = current_user.contexts.find_by_params(params) - @feed_title = @feed_title + t('todos.feed_title_in_context', :context => @context.name) - Todo.send :with_scope, :find => {:conditions => ['todos.context_id = ?', @context.id]} do - yield - end - elsif (params[:project_id]) - @project = current_user.projects.find_by_params(params) - @feed_title = @feed_title + t('todos.feed_title_in_project', :project => @project.name) - @project_feed = true - Todo.send :with_scope, :find => {:conditions => ['todos.project_id = ?', @project.id]} do - yield - end - else - yield - end - end - - def with_limit_scope(&block) - if params.key?('limit') - Todo.send :with_scope, :find => { :limit => params['limit'] } do - yield - end - if TodosController.is_feed_request(request) && @description - if params.key?('limit') - @description << t('todos.list_incomplete_next_actions_with_limit', :count => params['limit']) - else - @description << t('todos.list_incomplete_next_actions') - end - end - else - yield - end - end - - def init_todos - with_feed_query_scope do - with_parent_resource_scope do # @context or @project may get defined here - with_limit_scope do - - if mobile? - init_todos_for_mobile_view - else - - # Note: these next two finds were previously using - # current_users.todos.find but that broke with_scope for :limit - - # Exclude hidden projects from count on home page - @todos = current_user.todos.find(:all, :include => Todo::DEFAULT_INCLUDES) - - # Exclude hidden projects from the home page - @not_done_todos = current_user.todos.find(:all, - :conditions => ['contexts.hide = ? AND (projects.state = ? OR todos.project_id IS NULL)', false, 'active'], - :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", - :include => Todo::DEFAULT_INCLUDES) - end - - end - end - end - end - - def init_todos_for_mobile_view - # Note: these next two finds were previously using current_users.todos.find - # but that broke with_scope for :limit - - # Exclude hidden projects from the home page - @not_done_todos = current_user.todos.find(:all, - :conditions => ['todos.state = ? AND contexts.hide = ? AND (projects.state = ? OR todos.project_id IS NULL)', 'active', false, 'active'], - :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", - :include => [ :project, :context, :tags ]) - end - def tag_title(tag_expr) and_list = tag_expr.inject([]) { |s,tag_list| s << tag_list.join(',') } return and_list.join(' AND ') @@ -1093,23 +1022,23 @@ class TodosController < ApplicationController end from.context do context_id = @original_item_context_id || @todo.context_id - todos = current_user.contexts.find(context_id).todos.not_completed + todos = current_user.contexts.find_by_id(context_id).todos.not_completed if @todo.context.hide? # include hidden todos - @down_count = todos.count(:all) + @down_count = todos.count else # exclude hidden_todos - @down_count = todos.not_hidden.count(:all) + @down_count = todos.not_hidden.count end end from.project do unless @todo.project_id == nil - @down_count = current_user.projects.find(@todo.project_id).todos.active_or_hidden.count + @down_count = current_user.projects.find_by_id(@todo.project_id).todos.active_or_hidden.count end end from.deferred do - @down_count = current_user.todos.deferred_or_blocked.count(:all) + @down_count = current_user.todos.deferred_or_blocked.count end from.tag do @tag_name = params['_tag_name'] @@ -1126,8 +1055,8 @@ class TodosController < ApplicationController source_view do |from| from.deferred { # force reload to todos to get correct count and not a cached one - @remaining_in_context = current_user.contexts.find(context_id).todos.deferred_or_blocked.count - @target_context_count = current_user.contexts.find(@todo.context_id).todos.deferred_or_blocked.count + @remaining_in_context = current_user.contexts.find_by_id(context_id).todos.deferred_or_blocked.count + @target_context_count = current_user.contexts.find_by_id(@todo.context_id).todos.deferred_or_blocked.count } from.tag { tag = Tag.find_by_name(params['_tag_name']) @@ -1135,39 +1064,44 @@ class TodosController < ApplicationController tag = Tag.new(:name => params['tag']) end @remaining_deferred_or_pending_count = current_user.todos.with_tag(tag.id).deferred_or_blocked.count - @remaining_in_context = current_user.contexts.find(context_id).todos.active.not_hidden.with_tag(tag.id).count - @target_context_count = current_user.contexts.find(@todo.context_id).todos.active.not_hidden.with_tag(tag.id).count + @remaining_in_context = current_user.contexts.find_by_id(context_id).todos.active.not_hidden.with_tag(tag.id).count + @target_context_count = current_user.contexts.find_by_id(@todo.context_id).todos.active.not_hidden.with_tag(tag.id).count @remaining_hidden_count = current_user.todos.hidden.with_tag(tag.id).count } from.project { project_id = @project_changed ? @original_item_project_id : @todo.project_id - @remaining_deferred_or_pending_count = current_user.projects.find(project_id).todos.deferred_or_blocked.count + @remaining_deferred_or_pending_count = current_user.projects.find_by_id(project_id).todos.deferred_or_blocked.count if @todo_was_completed_from_deferred_or_blocked_state @remaining_in_context = @remaining_deferred_or_pending_count else - @remaining_in_context = current_user.projects.find(project_id).todos.active_or_hidden.count + @remaining_in_context = current_user.projects.find_by_id(project_id).todos.active_or_hidden.count end - @target_context_count = current_user.projects.find(project_id).todos.active.count + @target_context_count = current_user.projects.find_by_id(project_id).todos.active.count } from.calendar { @target_context_count = @new_due_id.blank? ? 0 : count_old_due_empty(@new_due_id) } from.context { - context = current_user.contexts.find(context_id) + context = current_user.contexts.find_by_id(context_id) + @remaining_deferred_or_pending_count = context.todos.deferred_or_blocked.count remaining_actions_in_context = context.todos(true).active remaining_actions_in_context = remaining_actions_in_context.not_hidden if !context.hide? @remaining_in_context = remaining_actions_in_context.count - actions_in_target = current_user.contexts.find(@todo.context_id).todos(true).active - actions_in_target = actions_in_target.not_hidden if !context.hide? + if @todo_was_deferred_or_blocked + actions_in_target = current_user.contexts.find_by_id(@todo.context_id).todos(true).active + actions_in_target = actions_in_target.not_hidden if !context.hide? + else + actions_in_target = @todo.context.todos.deferred_or_blocked + end @target_context_count = actions_in_target.count } end - @remaining_in_context = current_user.contexts.find(context_id).todos(true).active.not_hidden.count if !@remaining_in_context - @target_context_count = current_user.contexts.find(@todo.context_id).todos(true).active.not_hidden.count if !@target_context_count + @remaining_in_context = current_user.contexts.find_by_id(context_id).todos(true).active.not_hidden.count if !@remaining_in_context + @target_context_count = current_user.contexts.find_by_id(@todo.context_id).todos(true).active.not_hidden.count if !@target_context_count end def determine_completed_count @@ -1176,13 +1110,13 @@ class TodosController < ApplicationController @completed_count = current_user.todos.not_hidden.completed.count end from.context do - todos = current_user.contexts.find(@todo.context_id).todos.completed + todos = current_user.contexts.find_by_id(@todo.context_id).todos.completed todos = todos.not_hidden if !@todo.context.hidden? @completed_count = todos.count end from.project do unless @todo.project_id == nil - todos = current_user.projects.find(@todo.project_id).todos.completed + todos = current_user.projects.find_by_id(@todo.project_id).todos.completed todos = todos.not_hidden if !@todo.project.hidden? @completed_count = todos.count end @@ -1199,99 +1133,6 @@ class TodosController < ApplicationController @remaining_deferred_or_pending_count = tag.nil? ? 0 : current_user.todos.deferred.with_tag(tag.id).count end - def render_todos_html - lambda do - @page_title = t('todos.task_list_title') - - # If you've set no_completed to zero, the completed items box isn't shown - # on the home page - max_completed = current_user.prefs.show_number_completed - @done = current_user.todos.completed.find(:all, :limit => max_completed, :include => Todo::DEFAULT_INCLUDES) unless max_completed == 0 - - # Set count badge to number of not-done, not hidden context items - @count = current_user.todos.active.not_hidden.count(:all) - - render - end - end - - def render_todos_mobile - lambda do - @page_title = t('todos.mobile_todos_page_title') - @home = true - - max_completed = current_user.prefs.show_number_completed - @done = current_user.todos.completed.find(:all, :limit => max_completed, :include => Todo::DEFAULT_INCLUDES) unless max_completed == 0 - - cookies[:mobile_url]= { :value => request.request_uri, :secure => SITE_CONFIG['secure_cookies']} - determine_down_count - - render :action => 'index' - end - end - - def render_rss_feed - lambda do - render_rss_feed_for @todos, :feed => todo_feed_options, - :item => { - :title => :description, - :link => lambda { |t| @project_feed.nil? ? context_url(t.context) : project_url(t.project) }, - :guid => lambda { |t| todo_url(t) }, - :description => todo_feed_content - } - end - end - - def todo_feed_options - options = Todo.feed_options(current_user) - options[:title] = @feed_title - return options - end - - def todo_feed_content - # TODO: move view stuff into view, also the includes at the top - lambda do |i| - item_notes = sanitize(markdown( i.notes )) if i.notes? - due = "
    #{t('todos.feeds.due', :date => format_date(i.due))}
    \n" if i.due? - done = "
    #{t('todos.feeds.completed', :date => format_date(i.completed_at))}
    \n" if i.completed? - context_link = "#{ i.context.name }" - if i.project_id? - project_link = "#{ i.project.name }" - else - project_link = "#{t('common.none')}" - end - "#{done||''}#{due||''}#{item_notes||''}\n
    #{t('common.project')}: #{project_link}
    \n
    #{t('common.context')}: #{context_link}
    " - end - end - - def render_atom_feed - lambda do - render_atom_feed_for @todos, :feed => todo_feed_options, - :item => { - :title => :description, - :link => lambda { |t| context_url(t.context) }, - :description => todo_feed_content, - :author => lambda { |p| nil } - } - end - end - - def render_text_feed - lambda do - render :action => 'index', :layout => false, :content_type => Mime::TEXT - end - end - - def render_ical_feed - lambda do - render :action => 'index', :layout => false, :content_type => Mime::ICS - end - end - - def self.is_feed_request(req) - ['rss','atom','txt','ics'].include?(req.parameters[:format]) - end - def check_for_next_todo(todo) # check if this todo has a related recurring_todo. If so, create next todo new_recurring_todo = nil @@ -1358,20 +1199,15 @@ class TodosController < ApplicationController due_this_month_date = Time.zone.now.end_of_month case id when "due_today" - return current_user.todos.not_completed.count(:all, - :conditions => ['todos.due <= ?', due_today_date]) + return current_user.todos.not_completed.where('todos.due <= ?', due_today_date).count when "due_this_week" - return current_user.todos.not_completed.count(:all, - :conditions => ['todos.due > ? AND todos.due <= ?', due_today_date, due_this_week_date]) + return current_user.todos.not_completed.where('todos.due > ? AND todos.due <= ?', due_today_date, due_this_week_date).count when "due_next_week" - return current_user.todos.not_completed.count(:all, - :conditions => ['todos.due > ? AND todos.due <= ?', due_this_week_date, due_next_week_date]) + return current_user.todos.not_completed.where('todos.due > ? AND todos.due <= ?', due_this_week_date, due_next_week_date).count when "due_this_month" - return current_user.todos.not_completed.count(:all, - :conditions => ['todos.due > ? AND todos.due <= ?', due_next_week_date, due_this_month_date]) + return current_user.todos.not_completed.where('todos.due > ? AND todos.due <= ?', due_next_week_date, due_this_month_date).count when "due_after_this_month" - return current_user.todos.not_completed.count(:all, - :conditions => ['todos.due > ?', due_this_month_date]) + return current_user.todos.not_completed.where('todos.due > ?', due_this_month_date).count else raise Exception.new, "unknown due id for calendar: '#{id}'" end @@ -1386,6 +1222,7 @@ class TodosController < ApplicationController @original_item_due = @todo.due @original_item_due_id = get_due_id_for_calendar(@todo.due) @original_item_predecessor_list = @todo.predecessors.map{|t| t.specification}.join(', ') + @todo_was_deferred_or_blocked = @todo.deferred? || @todo.pending? end def update_project @@ -1410,7 +1247,7 @@ class TodosController < ApplicationController def update_todo_state_if_project_changed if ( @project_changed ) then @todo.update_state_from_project - @remaining_undone_in_project = current_user.projects.find(@original_item_project_id).todos.active.count if source_view_is :project + @remaining_undone_in_project = current_user.projects.find_by_id(@original_item_project_id).todos.active.count if source_view_is :project end end @@ -1432,8 +1269,8 @@ class TodosController < ApplicationController end def update_tags - if params[:tag_list] - @todo.tag_with(params[:tag_list]) + if params[:todo_tag_list] + @todo.tag_with(params[:todo_tag_list]) @todo.tags(true) #force a reload for proper rendering end end @@ -1443,7 +1280,7 @@ class TodosController < ApplicationController begin params["todo"]["due"] = parse_date_per_user_prefs(params["todo"]["due"]) rescue - @todo.errors.add_to_base("Invalid due date") + @todo.errors[:base] << "Invalid due date" end else params["todo"]["due"] = "" @@ -1452,7 +1289,7 @@ class TodosController < ApplicationController begin params['todo']['show_from'] = parse_date_per_user_prefs(params['todo']['show_from']) rescue - @todo.errors.add_to_base("Invalid show from date") + @todo.errors[:base] << "Invalid show from date" end end end @@ -1539,24 +1376,7 @@ class TodosController < ApplicationController return !( all_list_uniq_ids.length == all_list_count ) end - - class FindConditionBuilder - - def initialize - @queries = Array.new - @params = Array.new - end - - def add(query, param) - @queries << query - @params << param - end - - def to_conditions - [@queries.join(' AND ')] + @params - end - end - + class TodoCreateParamsHelper def initialize(params, prefs) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 7b0eb8b7..c0029a9f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,4 +1,5 @@ class UsersController < ApplicationController + before_filter :admin_login_required, :only => [ :index, :show, :destroy ] skip_before_filter :login_required, :only => [ :new, :create ] skip_before_filter :check_for_deprecated_password_hash, @@ -17,7 +18,7 @@ class UsersController < ApplicationController store_location end format.xml do - @users = User.find(:all, :order => 'login') + @users = User.order('login').all render :xml => @users.to_xml(:except => [ :password ]) end end @@ -25,7 +26,7 @@ class UsersController < ApplicationController # GET /users/id GET /users/id.xml def show - @user = User.find_by_id(params[:id]) + @user = User.find(params[:id]) render :xml => @user.to_xml(:except => [ :password ]) end @@ -64,7 +65,7 @@ class UsersController < ApplicationController # POST /users POST /users.xml def create if params['exception'] - render_failure "Expected post format is valid xml like so: usernameabc123." + render_failure "Expected post format is valid xml like so: usernameabc123." return end respond_to do |format| @@ -82,7 +83,7 @@ class UsersController < ApplicationController user.auth_type == 'ldap' && !SimpleLdapAuthenticator.valid?(user.login, params['user']['password']) notify :warning, "Incorrect password" - redirect_to :action => 'new' + redirect_to signup_path return end @@ -111,23 +112,21 @@ class UsersController < ApplicationController return end format.xml do - unless User.find_by_id_and_is_admin(session['user_id'], true) + unless current_user && current_user.is_admin render :text => "401 Unauthorized: Only admin users are allowed access to this function.", :status => 401 return end unless check_create_user_params - render_failure "Expected post format is valid xml like so: usernameabc123." + render_failure "Expected post format is valid xml like so: usernameabc123.", 400 return end - user = User.new(params[:request]) - if Tracks::Config.auth_schemes.include?('cas') && session[:cas_user] - user.auth_type = "cas" #if they area cas user - end - user.password_confirmation = params[:request][:password] - if user.save + user = User.new(params[:user]) + user.password_confirmation = params[:user][:password] + saved = user.save + unless user.new_record? render :text => t('users.user_created'), :status => 200 else - render_failure user.errors.to_xml + render_failure user.errors.to_xml, 409 end return end @@ -136,16 +135,16 @@ class UsersController < ApplicationController # DELETE /users/id DELETE /users/id.xml def destroy - @deleted_user = User.find_by_id(params[:id]) + @deleted_user = User.find(params[:id]) @saved = @deleted_user.destroy - @total_users = User.find(:all).size + @total_users = User.all.size respond_to do |format| format.html do if @saved - notify :notice, t('users.successfully_deleted_user', :username => @deleted_user.login), 2.0 + notify :notice, t('users.successfully_deleted_user', :username => @deleted_user.login) else - notify :error, t('users.failed_to_delete_user', :username => @deleted_user.login), 2.0 + notify :error, t('users.failed_to_delete_user', :username => @deleted_user.login) end redirect_to users_url end @@ -154,19 +153,18 @@ class UsersController < ApplicationController end end - def change_password @page_title = t('users.change_password_title') end def update_password # is used for focing password change after sha->bcrypt upgrade - @user.change_password(params[:user][:password], params[:user][:password_confirmation]) + current_user.change_password(params[:user][:password], params[:user][:password_confirmation]) notify :notice, t('users.password_updated') redirect_to preferences_path rescue Exception => error notify :error, error.message - redirect_to :action => 'change_password' + redirect_to change_password_user_path(current_user) end def change_auth_type @@ -174,40 +172,19 @@ class UsersController < ApplicationController end def update_auth_type - if (params[:open_id_complete] || (params[:user][:auth_type] == 'open_id')) && openid_enabled? - authenticate_with_open_id do |result, identity_url| - if result.successful? - # Success means that the transaction completed without error. If info - # is nil, it means that the user cancelled the verification. - @user.auth_type = 'open_id' - @user.open_id_url = identity_url - if @user.save - notify :notice, t('users.openid_url_verified', :url => identity_url) - else - debugger - notify :warning, t('users.openid_ok_pref_failed', :url => identity_url) - end - redirect_to preferences_path - else - notify :warning, result.message - redirect_to :action => 'change_auth_type' - end - end - return - end - @user.auth_type = params[:user][:auth_type] - if @user.save + current_user.auth_type = params[:user][:auth_type] + if current_user.save notify :notice, t('users.auth_type_updated') redirect_to preferences_path else - notify :warning, t('users.auth_type_update_error', :error_messages => @user.errors.full_messages.join(', ')) - redirect_to :action => 'change_auth_type' + notify :warning, t('users.auth_type_update_error', :error_messages => current_user.errors.full_messages.join(', ')) + redirect_to change_auth_type_user_path(current_user) end end def refresh_token - @user.generate_token - @user.save! + current_user.generate_token + current_user.save! notify :notice, t('users.new_token_generated') redirect_to preferences_path end @@ -225,11 +202,11 @@ class UsersController < ApplicationController end def check_create_user_params - return false unless params.has_key?(:request) - return false unless params[:request].has_key?(:login) - return false if params[:request][:login].empty? - return false unless params[:request].has_key?(:password) - return false if params[:request][:password].empty? + return false unless params.has_key?(:user) + return false unless params[:user].has_key?(:login) + return false if params[:user][:login].empty? + return false unless params[:user].has_key?(:password) + return false if params[:user][:password].empty? return true end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index fbf0a0cf..05afee5b 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -6,21 +6,23 @@ module ApplicationHelper # current page. If that matches the url, the link is marked id = "current" # def navigation_link(name, options = {}, html_options = nil, *parameters_for_method_reference) - if html_options - html_options = html_options.stringify_keys - convert_options_to_javascript!(html_options) - tag_options = tag_options(html_options) - else - tag_options = nil - end - url = options.is_a?(String) ? options : self.url_for(options, *parameters_for_method_reference) - id_tag = (request.request_uri == url) ? " id=\"current\"" : "" - - "#{name || url}" + link_to name, options, html_options + # TODO: check if this needs to be converted + # if html_options + # html_options = html_options.stringify_keys + # convert_options_to_javascript!(html_options) + # tag_options = tag_options(html_options) + # else + # tag_options = nil + # end + # url = options.is_a?(String) ? options : self.url_for(options, *parameters_for_method_reference) + # id_tag = (request.request_uri == url) ? " id=\"current\"" : "" + # + # "#{name || url}" end def days_from_today(date) - date.in_time_zone.to_date - current_user.time.to_date + Integer (date.in_time_zone.to_date - current_user.time.to_date) end # Check due date in comparison to today's date Flag up date appropriately with @@ -53,7 +55,7 @@ module ApplicationHelper else # overdue or due very soon! sound the alarm! if days == -1 - t('todos.next_actions_due_date.overdue_by', :days => days * -1) + t('todos.next_actions_due_date.overdue_by', :days => days * -1) elsif days < -1 t('todos.next_actions_due_date.overdue_by_plural', :days => days * -1) else @@ -98,18 +100,18 @@ module ApplicationHelper # is count and a string descriptor, correctly pluralised if there are no # actions or multiple actions # - def count_undone_todos_phrase(todos_parent, string="actions") - @controller.count_undone_todos_phrase(todos_parent, string) + def count_undone_todos_phrase(todos_parent) + controller.count_undone_todos_phrase(todos_parent).html_safe end - def count_undone_todos_phrase_text(todos_parent, string="actions") - count_undone_todos_phrase(todos_parent, string).gsub(" "," ") + def count_undone_todos_phrase_text(todos_parent) + count_undone_todos_phrase(todos_parent).gsub(" "," ").html_safe end - def count_undone_todos_and_notes_phrase(project, string="actions") - s = count_undone_todos_phrase(project, string) - s += ", #{pluralize(project.note_count, 'note')}" unless project.note_count == 0 - s + def count_undone_todos_and_notes_phrase(project) + s = count_undone_todos_phrase(project) + s += ", #{t('common.note', :count => project.note_count)}" unless project.note_count == 0 + s.html_safe end def link_to_context(context, descriptor = sanitize(context.name)) @@ -120,18 +122,6 @@ module ApplicationHelper link_to( descriptor, project, :title => "View project: #{project.name}" ) end - def link_to_edit_project (project, descriptor = sanitize(project.name)) - link_to(descriptor, - url_for({:controller => 'projects', :action => 'edit', :id => project.id}), - {:id => "link_edit_#{dom_id(project)}", :class => "project_edit_settings"}) - end - - def link_to_edit_context (context, descriptor = sanitize(context.name)) - link_to(descriptor, - url_for({:controller => 'contexts', :action => 'edit', :id => context.id}), - {:id => "link_edit_#{dom_id(context)}", :class => "context_edit_settings"}) - end - def link_to_edit_note (note, descriptor = sanitize(note.id.to_s)) link_to(descriptor, url_for({:controller => 'notes', :action => 'edit', :id => note.id}), @@ -155,7 +145,7 @@ module ApplicationHelper end def render_flash - render :partial => 'shared/flash', :object => flash + render :partial => 'shared/flash', :object => flash end def recurrence_time_span(rt) @@ -201,50 +191,18 @@ module ApplicationHelper end end - AUTO_LINK_MESSAGE_RE = %r{message://<[^>]+>} unless const_defined?(:AUTO_LINK_MESSAGE_RE) - - # Converts message:// links to href. This URL scheme is used on Mac OS X - # to link to a mail message in Mail.app. - def auto_link_message(text) - text.gsub(AUTO_LINK_MESSAGE_RE) do - href = $& - left, right = $`, $' - # detect already linked URLs and URLs in the middle of a tag - if left =~ /<[^>]+$/ && right =~ /^[^>]*>/ - # do not change string; URL is alreay linked - href - else - content = content_tag(:a, h(href), :href => h(href)) - end - end - end - - def format_note(note) - note = auto_link_message(note) - note = markdown(note) - note = auto_link(note, :link => :urls) - - # add onenote and message protocols - Sanitize::Config::RELAXED[:protocols]['a']['href'] << 'onenote' - Sanitize::Config::RELAXED[:protocols]['a']['href'] << 'message' - - note = Sanitize.clean(note, Sanitize::Config::RELAXED) - return note - end - def sidebar_html_for_titled_list (list, title) - return content_tag(:h3, title+" (#{list.length})") + - content_tag(:ul, sidebar_html_for_list(list)) + return content_tag(:h3, title+" (#{list.size})") + content_tag(:ul, sidebar_html_for_list(list)) end def sidebar_html_for_list(list) if list.empty? - return content_tag(:li, t('sidebar.list_empty')) + return content_tag(:li, t('sidebar.list_empty')).html_safe else return list.inject("") do |html, item| link = (item.class == "Project") ? link_to_project( item ) : link_to_context(item) - html << content_tag(:li, link + " (" + count_undone_todos_phrase(item,"actions")+")") - end + html << content_tag(:li, link + " (" + count_undone_todos_phrase(item)+")") + end.html_safe end end @@ -261,6 +219,7 @@ module ApplicationHelper contexts.show_form contexts.show_form_title contexts.new_context_pre contexts.new_context_post common.cancel common.ok + common.update common.create common.ajaxError todos.unresolved_dependency }.each do |s| js << "i18n['#{s}'] = '#{ t(s).gsub(/'/, "\\\\'") }';\n" @@ -277,7 +236,7 @@ module ApplicationHelper end def determine_done_path - case @controller.controller_name + case controller.controller_name when "contexts" done_todos_context_path(@context) when "projects" @@ -287,14 +246,14 @@ module ApplicationHelper done_tag_path(@tag_name) else done_todos_path - end + end else done_todos_path end end def determine_all_done_path - case @controller.controller_name + case controller.controller_name when "contexts" all_done_todos_context_path(@context) when "projects" @@ -309,5 +268,13 @@ module ApplicationHelper all_done_todos_path end end + + def get_list_of_error_messages_for(model) + error_messages = "" + if model.errors.any? + list_of_messages = model.errors.full_messages.inject("") { |all, msg| all += content_tag(:li, msg) } + error_messages = content_tag(:ul, list_of_messages) + end + end end diff --git a/app/helpers/contexts_helper.rb b/app/helpers/contexts_helper.rb index 1b512689..423d18ad 100644 --- a/app/helpers/contexts_helper.rb +++ b/app/helpers/contexts_helper.rb @@ -1,30 +1,36 @@ module ContextsHelper + + def get_listing_sortable_options + { + :tag => 'div', + :handle => 'handle', + :complete => visual_effect(:highlight, 'list-contexts'), + :url => order_contexts_path + } + end + + def link_to_delete_context(context, descriptor = sanitize(context.name)) + link_to(descriptor, + context_path(context, :format => 'js'), + { + :id => "delete_context_#{context.id}", + :class => "delete_context_button icon", + :x_confirm_message => t('contexts.delete_context_confirmation', :name => context.name), + :title => t('contexts.delete_context_title') + }) + end -def get_listing_sortable_options - { - :tag => 'div', - :handle => 'handle', - :complete => visual_effect(:highlight, 'list-contexts'), - :url => order_contexts_path - } -end - -def link_to_delete_context(context, descriptor = sanitize(context.name)) - link_to( - descriptor, - context_path(context, :format => 'js'), - { - :id => "delete_context_#{context.id}", - :class => "delete_context_button", - :x_confirm_message => t('contexts.delete_context_confirmation', :name => context.name), - :title => t('contexts.delete_context_title') - } - ) -end - -def summary(context, undone_todo_count) - content_tag(:p, "#{undone_todo_count}. Context is #{context.hidden? ? 'Hidden' : 'Active'}.") -end + def link_to_edit_context (context, descriptor = sanitize(context.name)) + link_to(descriptor, + url_for({:controller => 'contexts', :action => 'edit', :id => context.id}), + { + :id => "link_edit_#{dom_id(context)}", + :class => "context_edit_settings icon" + }) + end + def context_summary(context, undone_todo_count) + content_tag(:p, "#{undone_todo_count}. Context is #{context.hidden? ? 'Hidden' : 'Active'}.".html_safe) + end end diff --git a/app/helpers/feedlist_helper.rb b/app/helpers/feedlist_helper.rb index 6b3a557b..d6b086f4 100644 --- a/app/helpers/feedlist_helper.rb +++ b/app/helpers/feedlist_helper.rb @@ -8,12 +8,12 @@ module FeedlistHelper def text_formatted_link(options = {}) linkoptions = merge_hashes( {:format => 'txt'}, user_token_hash, options) - link_to('TXT', linkoptions, :title => "Plain text feed" ) + link_to(content_tag(:span, 'TXT', {:class => 'feed', :title => "Plain text feed"}), linkoptions) end def ical_formatted_link(options = {}) linkoptions = merge_hashes( {:format => 'ics'}, user_token_hash, options) - link_to('iCal', linkoptions, :title => "iCal feed" ) + link_to(content_tag(:span, 'iCal', {:class=>"feed", :title => "iCal feed"}), linkoptions) end def feed_links(feeds, link_options, title) @@ -23,7 +23,7 @@ module FeedlistHelper html << text_formatted_link(link_options)+space if feeds.include?(:txt) html << ical_formatted_link(link_options)+space if feeds.include?(:ical) html << title - return html + return html.html_safe end def all_feed_links_for_project(project) diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 4dd9330e..fb1f189e 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -1,9 +1,10 @@ module PreferencesHelper def pref(model, pref_name, &block) - s = "
    " + s = content_tag(:label, Preference.human_attribute_name(pref_name), :for => model+pref_name) + s << "
    ".html_safe s << yield - s << "

    " + s << "

    ".html_safe s end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 50ea79e9..44b7609c 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -18,31 +18,30 @@ module ProjectsHelper end def project_next_prev - html = '' - unless @previous_project.nil? + html = "" + if @previous_project project_name = truncate(@previous_project.name, :length => 40, :omission => "...") - html << link_to_project(@previous_project, "« #{project_name}") + html << link_to_project(@previous_project, "« #{project_name}".html_safe) end - html << ' | ' if @previous_project && @next_project - unless @next_project.nil? + html << " | " if @previous_project && @next_project + if @next_project project_name = truncate(@next_project.name, :length => 40, :omission => "...") - html << link_to_project(@next_project, "#{project_name} »") + html << link_to_project(@next_project, "#{project_name} »".html_safe) end - html + html.html_safe end def project_next_prev_mobile - html = '' + prev_project,next_project= "", "" unless @previous_project.nil? project_name = truncate(@previous_project.name, :length => 40, :omission => "...") - html << link_to_project_mobile(@previous_project, "5", "« 5-#{project_name}") + prev_project = content_tag(:li, link_to_project_mobile(@previous_project, "5", project_name), :class=>"prev") end - html << ' | ' if @previous_project && @next_project unless @next_project.nil? project_name = truncate(@next_project.name, :length => 40, :omission => "...") - html << link_to_project_mobile(@next_project, "6", "6-#{project_name} »") + next_project = content_tag(:li, link_to_project_mobile(@next_project, "6", project_name), :class=>"next") end - html + return content_tag(:ul, "#{prev_project}#{next_project}".html_safe, :class=>"next-prev-project").html_safe end def link_to_delete_project(project, descriptor = sanitize(project.name)) @@ -51,20 +50,29 @@ module ProjectsHelper project_path(project, :format => 'js'), { :id => "delete_project_#{project.id}", - :class => "delete_project_button", + :class => "delete_project_button icon", :x_confirm_message => t('projects.delete_project_confirmation', :name => project.name), :title => t('projects.delete_project_title') } ) end - def summary(project) + def link_to_edit_project (project, descriptor = sanitize(project.name)) + link_to(descriptor, + url_for({:controller => 'projects', :action => 'edit', :id => project.id}), + { + :id => "link_edit_#{dom_id(project)}", + :class => "project_edit_settings icon" + }) + end + + def project_summary(project) project_description = '' - project_description += sanitize(markdown( project.description )) unless project.description.blank? + project_description += Tracks::Utils.render_text( project.description ) unless project.description.blank? project_description += content_tag(:p, - "#{count_undone_todos_phrase(p)}. " + t('projects.project_state', :state => project.state) + "#{count_undone_todos_phrase(p)}. #{t('projects.project_state', :state => project.state)}".html_safe ) - project_description + raw project_description end def needsreview_class(item) diff --git a/app/helpers/recurring_todos_helper.rb b/app/helpers/recurring_todos_helper.rb index 7cea9601..ce78a1a8 100644 --- a/app/helpers/recurring_todos_helper.rb +++ b/app/helpers/recurring_todos_helper.rb @@ -2,11 +2,10 @@ module RecurringTodosHelper def recurring_todo_tag_list tags_except_starred = @recurring_todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME} - tag_list = tags_except_starred.collect{|t| "" + - link_to(t.name, :controller => "todos", :action => "tag", :id => - t.name) + #TODO: tag view for recurring_todos (yet?) - ""}.join('') - "#{tag_list}" + tag_list = tags_except_starred. + collect{|t| content_tag(:span,link_to(t.name, tag_path(t.name)), :class => "tag #{t.name.gsub(' ','-')}")}. + join('') + return content_tag :span, tag_list.html_safe, :class => "tags" end def recurring_todo_remote_delete_icon @@ -27,7 +26,7 @@ module RecurringTodosHelper edit_recurring_todo_path(@recurring_todo), :class => "icon edit_icon", :id => "link_edit_recurring_todo_#{@recurring_todo.id}") else - str = '' + image_tag("blank.png") + " " + str = content_tag(:a, image_tag("blank.png"), :class => "icon") end str end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index d6f3ed53..bc1e24a2 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -63,8 +63,14 @@ module TodosHelper end def collapsed_notes_image(todo) - link = link_to(image_tag( 'blank.png', :width=>'16', :height=>'16', :border=>'0' ), "#", {:class => 'show_notes', :title => 'Show notes'}) - notes = content_tag(:div, {:class => "todo_notes", :id => dom_id(todo, 'notes'), :style => "display:none"}) { format_note(todo.notes) } + link = link_to( + image_tag( 'blank.png', :width=>'16', :height=>'16', :border=>'0' ), + "#", + {:class => 'show_notes', :title => 'Show notes'}) + notes = content_tag(:div, { + :class => "todo_notes", + :id => dom_id(todo, 'notes'), + :style => "display:none"}) { raw todo.rendered_notes } return link+notes end @@ -80,7 +86,7 @@ module TodosHelper def image_tag_for_recurring_todo(todo) return link_to( image_tag("recurring16x16.png"), - {:controller => "recurring_todos", :action => "index"}, + recurring_todos_path, :class => "recurring_icon", :title => recurrence_pattern_as_text(todo.recurring_todo)) end @@ -90,9 +96,9 @@ module TodosHelper end def remote_mobile_checkbox(todo=@todo) - form_tag mobile_done_todo_path(@todo, :format => 'm'), :method => :put, :class => "mobile-done", :name => "mobile_complete_#{@todo.id}" do + form_tag toggle_check_todo_path(@todo, :format => 'm'), :method => :put, :class => "mobile-done", :name => "mobile_complete_#{@todo.id}" do check_box_tag('_source_view', 'todo', @todo && @todo.completed?, "onClick" => "document.mobile_complete_#{@todo.id}.submit()") - end + end end def date_span(todo=@todo) @@ -110,7 +116,7 @@ module TodosHelper def successors_span(todo=@todo) unless todo.pending_successors.empty? - pending_count = todo.pending_successors.length + pending_count = todo.pending_successors.count title = "#{t('todos.has_x_pending', :count => pending_count)}: #{todo.pending_successors.map(&:description).join(', ')}" image_tag( 'successor_off.png', :width=>'10', :height=>'16', :border=>'0', :title => title ) end @@ -129,25 +135,19 @@ module TodosHelper end def tag_span (tag, mobile=false) - content_tag(:span, :class => "tag #{tag.name.gsub(' ','-')}") { link_to(tag.name, (mobile ? mobile_tag_path(tag.name) : tag_path(tag.name))) } + content_tag(:span, :class => "tag #{tag.name.gsub(' ','-')}") { link_to(tag.name, tag_path(tag.name, :format => mobile ? :m : :html)) } end def tag_list(todo=@todo, mobile=false) - content_tag(:span, :class => 'tags') { todo.tags.all_except_starred.collect{|tag| tag_span(tag, mobile)}.join('') } + content_tag(:span, :class => 'tags') { todo.tags.all_except_starred.collect{|tag| tag_span(tag, mobile)}.join('').html_safe } end def tag_list_mobile(todo=@todo) - unless todo.tags.all_except_starred.empty? - return tag_list(todo, true) - else - return "" - end + todo.tags.all_except_starred.empty? ? "" : tag_list(todo, true) end def deferred_due_date(todo=@todo) - if todo.deferred? && todo.due - t('todos.action_due_on', :date => format_date(todo.due)) - end + t('todos.action_due_on', :date => format_date(todo.due)) if todo.deferred? && todo.due end def project_and_context_links(todo, parent_container_type, opts = {}) @@ -166,7 +166,7 @@ module TodosHelper str << item_link_to_project( todo ) end end - return str + return str.html_safe end # Uses the 'staleness_starts' value from settings.yml (in days) to colour the @@ -272,12 +272,12 @@ module TodosHelper end def default_contexts_for_autocomplete - projects = current_user.uncompleted.projects.find(:all, :include => [:context], :conditions => ['default_context_id is not null']) + projects = current_user.projects.uncompleted.includes(:default_context).where('NOT(default_context_id IS NULL)') Hash[*projects.map{ |p| [escape_javascript(p.name), escape_javascript(p.default_context.name)] }.flatten].to_json end def default_tags_for_autocomplete - projects = current_user.projects.uncompleted.find(:all, :conditions => ["default_tags != ''"]) + projects = current_user.projects.uncompleted.where("NOT(default_tags = '')") Hash[*projects.map{ |p| [escape_javascript(p.name), p.default_tags] }.flatten].to_json end @@ -311,7 +311,7 @@ module TodosHelper def update_needs_to_remove_todo_from_container source_view do |page| - page.context { return @context_changed || @todo.deferred? || @todo.pending? || @todo_should_be_hidden } + page.context { return @context_changed || @todo_deferred_state_changed || @todo_pending_state_changed || @todo_should_be_hidden } page.project { return @todo_deferred_state_changed || @todo_pending_state_changed || @project_changed} page.deferred { return @context_changed || !(@todo.deferred? || @todo.pending?) } page.calendar { return @due_date_changed || !@todo.due } @@ -339,7 +339,7 @@ module TodosHelper def append_updated_todo source_view do |page| - page.context { return false } + page.context { return @todo_deferred_state_changed || @todo_pending_state_changed } page.project { return @todo_deferred_state_changed || @todo_pending_state_changed } page.deferred { return @context_changed && (@todo.deferred? || @todo.pending?) } page.calendar { return @due_date_changed && @todo.due } @@ -370,7 +370,8 @@ module TodosHelper @todo_was_blocked_from_active_state || @todo_was_destroyed_from_deferred_state || @todo_was_created_deferred || - @todo_was_blocked_from_completed_state + @todo_was_blocked_from_completed_state || + @todo_was_created_blocked return "p#{todo.project_id}empty-nd" } page.tag { @@ -379,13 +380,31 @@ module TodosHelper @todo_was_blocked_from_active_state || @todo_was_destroyed_from_deferred_state || @todo_was_created_deferred || - @todo_was_blocked_from_completed_state + @todo_was_blocked_from_completed_state || + @todo_was_created_blocked return "hidden-empty-nd" if @todo.hidden? return "c#{todo.context_id}empty-nd" } page.calendar { + return "tickler-empty-nd" if + @todo_was_deferred_from_active_state || + @todo_was_blocked_from_active_state || + @todo_was_destroyed_from_deferred_state || + @todo_was_created_deferred || + @todo_was_blocked_from_completed_state || + @todo_was_created_blocked return "empty_#{@new_due_id}" } + page.context { + return "tickler-empty-nd" if + @todo_was_deferred_from_active_state || + @todo_was_blocked_from_active_state || + @todo_was_destroyed_from_deferred_state || + @todo_was_created_deferred || + @todo_was_blocked_from_completed_state || + @todo_was_created_blocked + return "c#{todo.context_id}empty-nd" + } end return "c#{todo.context_id}empty-nd" @@ -417,11 +436,12 @@ module TodosHelper } page.context { container_id = "c#{@original_item_context_id}empty-nd" if @remaining_in_context == 0 + container_id = "tickler-empty-nd" if todo_was_removed_from_deferred_or_blocked_container && @remaining_deferred_or_pending_count == 0 container_id = "empty-d" if @completed_count && @completed_count == 0 && !@todo.completed? } page.todo { container_id = "c#{@original_item_context_id}empty-nd" if @remaining_in_context == 0 } end - return container_id.blank? ? "" : "$(\"##{container_id}\").slideDown(100);" + return container_id.blank? ? "" : "$(\"##{container_id}\").slideDown(100);".html_safe end def render_animation(animation) @@ -447,6 +467,17 @@ module TodosHelper return $tracks_tab_index end + def feed_content_for_todo(todo) + item_notes = todo.notes ? todo.rendered_notes : '' + due = todo.due ? content_tag(:div, t('todos.feeds.due', :date => format_date(todo.due))) : '' + done = todo.completed? ? content_tag(:div, t('todos.feeds.completed', :date => format_date(todo.completed_at))) : '' + context_link = link_to(context_url(todo.context), todo.context.name) + project_link = todo.project.is_a?(NullProject) ? content_tag(:em, t('common.none')) : link_to(project_url(todo.project), todo.project.name) + return "#{done} #{due} #{item_notes}\n" + + content_tag(:div, "#{t('common.project')}: #{project_link}") + "\n" + + content_tag(:div, "#{t('common.context')}: #{context_link}") + end + private def image_tag_for_star(todo) diff --git a/app/mailers/.gitkeep b/app/mailers/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/models/.gitkeep b/app/models/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/models/context.rb b/app/models/context.rb index 2a797397..d5e5be00 100644 --- a/app/models/context.rb +++ b/app/models/context.rb @@ -5,27 +5,16 @@ class Context < ActiveRecord::Base has_many :recurring_todos, :dependent => :delete_all belongs_to :user - named_scope :active, :conditions => { :hide => false } - named_scope :hidden, :conditions => { :hide => true } + scope :active, :conditions => { :hide => false } + scope :hidden, :conditions => { :hide => true } acts_as_list :scope => :user, :top_of_list => 0 - extend NamePartFinder - include Tracks::TodoList attr_protected :user validates_presence_of :name, :message => "context must have a name" validates_length_of :name, :maximum => 255, :message => "context name must be less than 256 characters" validates_uniqueness_of :name, :message => "already exists", :scope => "user_id" - validates_does_not_contain :name, :string => ',', :message => "cannot contain the comma (',') character" - - def self.feed_options(user) - # TODO: move to view or helper - { - :title => 'Tracks Contexts', - :description => "Lists all the contexts for #{user.display_name}" - } - end def self.null_object NullContext.new diff --git a/app/models/message_gateway.rb b/app/models/message_gateway.rb index af67a782..e3a4adc8 100644 --- a/app/models/message_gateway.rb +++ b/app/models/message_gateway.rb @@ -3,41 +3,59 @@ class MessageGateway < ActionMailer::Base extend ActionView::Helpers::SanitizeHelper::ClassMethods def receive(email) - address = '' - if SITE_CONFIG['email_dispatch'] == 'to' - address = email.to[0] - else - address = email.from[0] - end - - user = User.find(:first, :include => [:preference], :conditions => ["preferences.sms_email = ?", address.strip]) - if user.nil? - user = User.find(:first, :include => [:preference], :conditions => ["preferences.sms_email = ?", address.strip[1,100]]) - end + user = get_user_from_email_address(email) return if user.nil? + context = user.prefs.sms_context - description = nil notes = nil - - if email.content_type == "multipart/related" - description = sanitize email.subject - body_part = email.parts.find{|m| m.content_type == "text/plain"} - notes = sanitize body_part.body.strip + + if email.multipart? + description = get_text_or_nil(email.subject) + notes = get_first_text_plain_part(email) else - if email.subject.empty? - description = sanitize email.body.strip + if email.subject.blank? + description = get_decoded_text_or_nil(email.body) notes = nil else - description = sanitize email.subject.strip - notes = sanitize email.body.strip + description = get_text_or_nil(email.subject) + notes = get_decoded_text_or_nil(email.body) end end # stupid T-Mobile often sends the same message multiple times - return if user.todos.find(:first, :conditions => {:description => description}) + return if user.todos.where(:description => description).first todo = Todo.from_rich_message(user, context.id, description, notes) todo.save! end + + private + + def get_address(email) + return SITE_CONFIG['email_dispatch'] == 'to' ? email.to[0] : email.from[0] + end + + def get_user_from_email_address(email) + address = get_address(email) + user = User.where("preferences.sms_email" => address.strip).includes(:preference).first + if user.nil? + user = User.where("preferences.sms_email" => address.strip[1.100]).includes(:preference).first + end + return user + end + + def get_text_or_nil(text) + return text ? sanitize(text.strip) : nil + end + + def get_decoded_text_or_nil(text) + return text ? sanitize(text.decoded.strip) : nil + end + + def get_first_text_plain_part(email) + parts = email.parts.reject{|part| !part.content_type.start_with?("text/plain") } + return parts ? sanitize(parts[0].decoded.strip) : "" + end + end diff --git a/app/models/preference.rb b/app/models/preference.rb index 3223b8ae..d730acc9 100644 --- a/app/models/preference.rb +++ b/app/models/preference.rb @@ -2,6 +2,11 @@ class Preference < ActiveRecord::Base belongs_to :user belongs_to :sms_context, :class_name => 'Context' + attr_accessible :date_format, :week_starts, :show_number_completed, :show_completed_projects_in_sidebar, + :show_hidden_contexts_in_sidebar, :staleness_starts, :due_style, :admin_email, :locale, + :title_date_format, :time_zone, :show_hidden_projects_in_sidebar, :show_project_on_todo_done, :review_period, + :refresh, :verbose_action_descriptors, :mobile_todos_per_page, :sms_email, :sms_context_id + def self.due_styles { :due_in_n_days => 0, :due_on => 1} end diff --git a/app/models/project.rb b/app/models/project.rb index adba4559..d877dbcc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -6,10 +6,10 @@ class Project < ActiveRecord::Base belongs_to :default_context, :class_name => "Context", :foreign_key => "default_context_id" belongs_to :user - named_scope :active, :conditions => { :state => 'active' } - named_scope :hidden, :conditions => { :state => 'hidden' } - named_scope :completed, :conditions => { :state => 'completed'} - named_scope :uncompleted, :conditions => ["NOT(state = ?)", 'completed'] + scope :active, :conditions => { :state => 'active' } + scope :hidden, :conditions => { :state => 'hidden' } + scope :completed, :conditions => { :state => 'completed'} + scope :uncompleted, :conditions => ["NOT(state = ?)", 'completed'] validates_presence_of :name validates_length_of :name, :maximum => 255 @@ -21,8 +21,8 @@ class Project < ActiveRecord::Base aasm_column :state aasm_initial_state :active - extend NamePartFinder - #include Tracks::TodoList + # extend NamePartFinder + # include Tracks::TodoList aasm_state :active aasm_state :hidden, :enter => :hide_todos, :exit => :unhide_todos @@ -47,13 +47,6 @@ class Project < ActiveRecord::Base NullProject.new end - def self.feed_options(user) - { - :title => I18n.t('models.project.feed_title'), - :description => I18n.t('models.project.feed_description', :username => user.display_name) - } - end - def hide_todos todos.each do |t| unless t.completed? || t.deferred? diff --git a/app/models/recurring_todo.rb b/app/models/recurring_todo.rb index 634b242b..82d8e2d0 100644 --- a/app/models/recurring_todo.rb +++ b/app/models/recurring_todo.rb @@ -6,8 +6,10 @@ class RecurringTodo < ActiveRecord::Base has_many :todos - named_scope :active, :conditions => { :state => 'active'} - named_scope :completed, :conditions => { :state => 'completed'} + include IsTaggable + + scope :active, :conditions => { :state => 'active'} + scope :completed, :conditions => { :state => 'completed'} attr_protected :user @@ -49,30 +51,30 @@ class RecurringTodo < ActiveRecord::Base def validate_daily if (!only_work_days) && (daily_every_x_days.nil? || daily_every_x_days.blank?) - errors.add_to_base("Every other nth day may not be empty for recurrence setting") + errors[:base] << "Every other nth day may not be empty for recurrence setting" end end def validate_weekly if weekly_every_x_week.nil? || weekly_every_x_week.blank? - errors.add_to_base("Every other nth week may not be empty for recurrence setting") + errors[:base] << "Every other nth week may not be empty for recurrence setting" end something_set = false %w{sunday monday tuesday wednesday thursday friday saturday}.each do |day| something_set ||= self.send("on_#{day}") end - errors.add_to_base("You must specify at least one day on which the todo recurs") if !something_set + errors[:base] << "You must specify at least one day on which the todo recurs" if !something_set end def validate_monthly case recurrence_selector when 0 # 'monthly_every_x_day' - errors.add_to_base("The day of the month may not be empty for recurrence setting") if monthly_every_x_day.nil? || monthly_every_x_day.blank? - errors.add_to_base("Every other nth month may not be empty for recurrence setting") if monthly_every_x_month.nil? || monthly_every_x_month.blank? + errors[:base] << "The day of the month may not be empty for recurrence setting" if monthly_every_x_day.nil? || monthly_every_x_day.blank? + errors[:base] << "Every other nth month may not be empty for recurrence setting" if monthly_every_x_month.nil? || monthly_every_x_month.blank? when 1 # 'monthly_every_xth_day' - errors.add_to_base("Every other nth month may not be empty for recurrence setting") if monthly_every_x_month2.nil? || monthly_every_x_month2.blank? - errors.add_to_base("The nth day of the month may not be empty for recurrence setting") if monthly_every_xth_day.nil? || monthly_every_xth_day.blank? - errors.add_to_base("The day of the month may not be empty for recurrence setting") if monthly_day_of_week.nil? || monthly_day_of_week.blank? + errors[:base] <<"Every other nth month may not be empty for recurrence setting" if monthly_every_x_month2.nil? || monthly_every_x_month2.blank? + errors[:base] <<"The nth day of the month may not be empty for recurrence setting" if monthly_every_xth_day.nil? || monthly_every_xth_day.blank? + errors[:base] <<"The day of the month may not be empty for recurrence setting" if monthly_day_of_week.nil? || monthly_day_of_week.blank? else raise Exception.new, "unexpected value of recurrence selector '#{self.recurrence_selector}'" end @@ -81,26 +83,26 @@ class RecurringTodo < ActiveRecord::Base def validate_yearly case recurrence_selector when 0 # 'yearly_every_x_day' - errors.add_to_base("The month of the year may not be empty for recurrence setting") if yearly_month_of_year.nil? || yearly_month_of_year.blank? - errors.add_to_base("The day of the month may not be empty for recurrence setting") if yearly_every_x_day.nil? || yearly_every_x_day.blank? + errors[:base] << "The month of the year may not be empty for recurrence setting" if yearly_month_of_year.nil? || yearly_month_of_year.blank? + errors[:base] << "The day of the month may not be empty for recurrence setting" if yearly_every_x_day.nil? || yearly_every_x_day.blank? when 1 # 'yearly_every_xth_day' - errors.add_to_base("The month of the year may not be empty for recurrence setting") if yearly_month_of_year2.nil? || yearly_month_of_year2.blank? - errors.add_to_base("The nth day of the month may not be empty for recurrence setting") if yearly_every_xth_day.nil? || yearly_every_xth_day.blank? - errors.add_to_base("The day of the week may not be empty for recurrence setting") if yearly_day_of_week.nil? || yearly_day_of_week.blank? + errors[:base] << "The month of the year may not be empty for recurrence setting" if yearly_month_of_year2.nil? || yearly_month_of_year2.blank? + errors[:base] << "The nth day of the month may not be empty for recurrence setting" if yearly_every_xth_day.nil? || yearly_every_xth_day.blank? + errors[:base] << "The day of the week may not be empty for recurrence setting" if yearly_day_of_week.nil? || yearly_day_of_week.blank? else raise Exception.new, "unexpected value of recurrence selector '#{self.recurrence_selector}'" end end def starts_and_ends_on_validations - errors.add_to_base("The start date needs to be filled in") if start_from.nil? || start_from.blank? + errors[:base] << "The start date needs to be filled in" if start_from.nil? || start_from.blank? case self.ends_on when 'ends_on_number_of_times' - errors.add_to_base("The number of recurrences needs to be filled in for 'Ends on'") if number_of_occurences.nil? || number_of_occurences.blank? + errors[:base] << "The number of recurrences needs to be filled in for 'Ends on'" if number_of_occurences.nil? || number_of_occurences.blank? when "ends_on_end_date" - errors.add_to_base("The end date needs to be filled in for 'Ends on'") if end_date.nil? || end_date.blank? + errors[:base] << "The end date needs to be filled in for 'Ends on'" if end_date.nil? || end_date.blank? else - errors.add_to_base("The end of the recurrence is not selected") unless ends_on == "no_end_date" + errors[:base] << "The end of the recurrence is not selected" unless ends_on == "no_end_date" end end @@ -110,9 +112,9 @@ class RecurringTodo < ActiveRecord::Base when 'show_from_date' # no validations when 'due_date' - errors.add_to_base("Please select when to show the action") if show_always.nil? + errors[:base] << "Please select when to show the action" if show_always.nil? unless show_always - errors.add_to_base("Please fill in the number of days to show the todo before the due date") if show_from_delta.nil? || show_from_delta.blank? + errors[:base] << "Please fill in the number of days to show the todo before the due date" if show_from_delta.nil? || show_from_delta.blank? end else raise Exception.new, "unexpected value of recurrence target selector '#{self.recurrence_target}'" @@ -431,7 +433,7 @@ class RecurringTodo < ActiveRecord::Base end else if self.every_other2>1 - n_months = "#{self.every_other2} " + I18n.t('common.months') + n_months = "#{self.every_other2} #{I18n.t('common.months')}" else n_months = I18n.t('common.month') end diff --git a/app/models/tag.rb b/app/models/tag.rb index 0d6d24e5..92bdaa20 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -1,9 +1,8 @@ - -# The Tag model. This model is automatically generated and added to your app if -# you run the tagging generator included with has_many_polymorphs. - class Tag < ActiveRecord::Base - + + has_many :taggings + has_many :taggable, :through => :taggings + DELIMITER = "," # Controls how to split and join tagnames from strings. You may need to change the validates_format_of parameters if you change this. JOIN_DELIMITER = ", " @@ -12,26 +11,10 @@ class Tag < ActiveRecord::Base validates_presence_of :name validates_uniqueness_of :name, :case_sensitive => false - # Change this validation if you need more complex tag names. - # validates_format_of :name, :with => /^[a-zA-Z0-9\_\-]+$/, :message => "can not contain special characters" - - # Set up the polymorphic relationship. - has_many_polymorphs :taggables, - :from => [:todos, :recurring_todos], - :through => :taggings, - :dependent => :destroy, - :skip_duplicates => false, - :parent_extend => proc { - # Defined on the taggable models, not on Tag itself. Return the tagnames - # associated with this record as a string. - def to_s - self.map(&:name).sort.join(Tag::JOIN_DELIMITER) - end - def all_except_starred - self.reject{|tag| tag.name == Todo::STARRED_TAG_NAME} - end - } - + attr_accessible :name + + before_create :before_create + # Callback to strip extra spaces from the tagname before saving it. If you # allow tags to be renamed later, you might want to use the # before_save callback instead. @@ -43,9 +26,4 @@ class Tag < ActiveRecord::Base taggings.create :taggable => taggable, :user => user end - # Tag::Error class. Raised by ActiveRecord::Base::TaggingExtensions if - # something goes wrong. - class Error < StandardError - end - end diff --git a/app/models/tagging.rb b/app/models/tagging.rb index 4d16acd2..95e14516 100644 --- a/app/models/tagging.rb +++ b/app/models/tagging.rb @@ -1,16 +1,19 @@ -# The Tagging join model. This model is automatically generated and added to your app if you run the tagging generator included with has_many_polymorphs. +# The Tagging join model. -class Tagging < ActiveRecord::Base +class Tagging < ActiveRecord::Base + + attr_accessible :taggable_id, :tag belongs_to :tag belongs_to :taggable, :polymorphic => true - # If you also need to use acts_as_list, you will have to manage the tagging positions manually by creating decorated join records when you associate Tags with taggables. - # acts_as_list :scope => :taggable - + after_destroy :after_destroy + + private + # This callback makes sure that an orphaned Tag is deleted if it no longer tags anything. def after_destroy - tag.destroy_without_callbacks if tag and tag.taggings.count == 0 - end + tag.destroy if tag and tag.taggings.count == 0 + end end diff --git a/app/models/todo.rb b/app/models/todo.rb index 9122afae..9402491a 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -1,13 +1,18 @@ class Todo < ActiveRecord::Base + before_save :render_note after_save :save_predecessors - # relations + # associations belongs_to :context belongs_to :project belongs_to :user belongs_to :recurring_todo + # Tag association + include IsTaggable + + # Dependencies associations has_many :predecessor_dependencies, :foreign_key => 'predecessor_id', :class_name => 'Dependency', :dependent => :destroy has_many :successor_dependencies, :foreign_key => 'successor_id', :class_name => 'Dependency', :dependent => :destroy has_many :predecessors, :through => :successor_dependencies @@ -16,33 +21,35 @@ class Todo < ActiveRecord::Base :source => :predecessor, :conditions => ['NOT (todos.state = ?)', 'completed'] has_many :pending_successors, :through => :predecessor_dependencies, :source => :successor, :conditions => ['todos.state = ?', 'pending'] - + # scopes for states of this todo - named_scope :active, :conditions => { :state => 'active' } - named_scope :active_or_hidden, :conditions => ["todos.state = ? OR todos.state = ?", 'active', 'project_hidden'] - named_scope :not_completed, :conditions => ['NOT (todos.state = ?)', 'completed'] - named_scope :completed, :conditions => ["NOT (todos.completed_at IS NULL)"] - named_scope :deferred, :conditions => ["todos.completed_at IS NULL AND NOT (todos.show_from IS NULL)"] - named_scope :blocked, :conditions => ['todos.state = ?', 'pending'] - named_scope :pending, :conditions => ['todos.state = ?', 'pending'] - named_scope :deferred_or_blocked, :conditions => ["(todos.completed_at IS NULL AND NOT(todos.show_from IS NULL)) OR (todos.state = ?)", "pending"] - named_scope :not_deferred_or_blocked, :conditions => ["todos.completed_at IS NULL AND todos.show_from IS NULL AND NOT(todos.state = ?)", "pending"] - named_scope :hidden, + scope :active, :conditions => { :state => 'active' } + scope :active_or_hidden, :conditions => ["todos.state = ? OR todos.state = ?", 'active', 'project_hidden'] + scope :not_completed, :conditions => ['NOT (todos.state = ?)', 'completed'] + scope :completed, :conditions => ["NOT (todos.completed_at IS NULL)"] + scope :deferred, :conditions => ["todos.completed_at IS NULL AND NOT (todos.show_from IS NULL)"] + scope :blocked, :conditions => ['todos.state = ?', 'pending'] + scope :pending, :conditions => ['todos.state = ?', 'pending'] + scope :deferred_or_blocked, :conditions => ["(todos.completed_at IS NULL AND NOT(todos.show_from IS NULL)) OR (todos.state = ?)", "pending"] + scope :not_deferred_or_blocked, :conditions => ["(todos.completed_at IS NULL) AND (todos.show_from IS NULL) AND (NOT todos.state = ?)", "pending"] + scope :hidden, :joins => "INNER JOIN contexts c_hidden ON c_hidden.id = todos.context_id", :conditions => ["todos.state = ? OR (c_hidden.hide = ? AND (todos.state = ? OR todos.state = ? OR todos.state = ?))", 'project_hidden', true, 'active', 'deferred', 'pending'] - named_scope :not_hidden, + scope :not_hidden, :joins => "INNER JOIN contexts c_hidden ON c_hidden.id = todos.context_id", :conditions => ['NOT(todos.state = ? OR (c_hidden.hide = ? AND (todos.state = ? OR todos.state = ? OR todos.state = ?)))', 'project_hidden', true, 'active', 'deferred', 'pending'] # other scopes - named_scope :are_due, :conditions => ['NOT (todos.due IS NULL)'] - named_scope :with_tag, lambda { |tag_id| {:joins => :taggings, :conditions => ["taggings.tag_id = ? ", tag_id] } } - named_scope :with_tags, lambda { |tag_ids| {:conditions => ["EXISTS(SELECT * from taggings t WHERE t.tag_id IN (?) AND t.taggable_id=todos.id AND t.taggable_type='Todo')", tag_ids] } } - named_scope :of_user, lambda { |user_id| {:conditions => ["todos.user_id = ? ", user_id] } } - named_scope :completed_after, lambda { |date| {:conditions => ["todos.completed_at > ? ", date] } } - named_scope :completed_before, lambda { |date| {:conditions => ["todos.completed_at < ? ", date] } } + scope :are_due, :conditions => ['NOT (todos.due IS NULL)'] + scope :with_tag, lambda { |tag_id| joins("INNER JOIN taggings ON todos.id = taggings.taggable_id").where("taggings.tag_id = ? ", tag_id) } + scope :with_tags, lambda { |tag_ids| where("EXISTS(SELECT * from taggings t WHERE t.tag_id IN (?) AND t.taggable_id=todos.id AND t.taggable_type='Todo')", tag_ids) } + # scope :of_user, lambda { |user_id| {:conditions => ["todos.user_id = ? ", user_id] } } + scope :completed_after, lambda { |date| where("todos.completed_at > ?", date) } + scope :completed_before, lambda { |date| where("todos.completed_at < ?", date) } + scope :created_after, lambda { |date| where("todos.created_at > ?", date) } + scope :created_before, lambda { |date| where("todos.created_at < ?", date) } STARRED_TAG_NAME = "starred" DEFAULT_INCLUDES = [ :project, :context, :tags, :taggings, :pending_successors, :uncompleted_predecessors, :recurring_todo ] @@ -97,7 +104,23 @@ class Todo < ActiveRecord::Base validates_length_of :notes, :maximum => 60000, :allow_nil => true validates_presence_of :show_from, :if => :deferred? validates_presence_of :context + validate :check_show_from_in_future + validate :check_circular_dependencies + def check_show_from_in_future + if !show_from.blank? && show_from < user.date + errors.add("show_from", I18n.t('models.todo.error_date_must_be_future')) + end + end + + def check_circular_dependencies + unless @predecessor_array.nil? # Only validate predecessors if they changed + @predecessor_array.each do |todo| + errors.add("Depends on:", "Adding '#{todo.specification}' would create a circular dependency") if is_successor?(todo) + end + end + end + def initialize(*args) super(*args) @predecessor_array = nil # Used for deferred save of predecessors @@ -114,7 +137,7 @@ class Todo < ActiveRecord::Base end def uncompleted_predecessors? - return !uncompleted_predecessors.all(true).empty? + return !uncompleted_predecessors.all.empty? end # Returns a string with description @@ -123,18 +146,6 @@ class Todo < ActiveRecord::Base return "\'#{self.description}\' <\'#{self.context.title}\'; \'#{project_name}\'>" end - def validate - if !show_from.blank? && show_from < user.date - errors.add("show_from", I18n.t('models.todo.error_date_must_be_future')) - end - errors.add(:description, "may not contain \" characters") if /\"/.match(self.description) - unless @predecessor_array.nil? # Only validate predecessors if they changed - @predecessor_array.each do |todo| - errors.add("Depends on:", "Adding '#{h(todo.specification)}' would create a circular dependency") if is_successor?(todo) - end - end - end - def save_predecessors unless @predecessor_array.nil? # Only save predecessors if they changed current_array = self.predecessors @@ -163,10 +174,14 @@ class Todo < ActiveRecord::Base return @removed_predecessors end + # remove predecessor and activate myself if it was the last predecessor def remove_predecessor(predecessor) - # remove predecessor and activate myself self.predecessors.delete(predecessor) - self.activate! + if self.predecessors.empty? + self.activate! + else + save! + end end # Returns true if t is equal to self or a successor of self @@ -231,13 +246,6 @@ class Todo < ActiveRecord::Base defer! if active? && !date.blank? && date > user.date end - def self.feed_options(user) - { - :title => 'Tracks Actions', - :description => "Actions for #{user.display_name}" - } - end - def starred? return has_tag?(STARRED_TAG_NAME) end @@ -280,14 +288,14 @@ class Todo < ActiveRecord::Base # activate todos that should be activated if the current todo is completed def activate_pending_todos - pending_todos = successors.find_all {|t| t.uncompleted_predecessors.empty?} + pending_todos = successors.select {|t| t.uncompleted_predecessors.empty?} pending_todos.each {|t| t.activate! } return pending_todos end # Return todos that should be blocked if the current todo is undone def block_successors - active_successors = successors.find_all {|t| t.active? or t.deferred?} + active_successors = successors.select {|t| t.active? or t.deferred?} active_successors.each {|t| t.block!} return active_successors end @@ -298,14 +306,14 @@ class Todo < ActiveRecord::Base # XML API fixups def predecessor_dependencies=(params) - value = params[:predecessor] - return if value.nil? + deps = params[:predecessor] + return if deps.nil? # for multiple dependencies, value will be an array of id's, but for a single dependency, # value will be a string. In that case convert to array - value = [value] unless value.class == Array + deps = [deps] unless deps.class == Array - value.each { |ele| add_predecessor(self.user.todos.find_by_id(ele.to_i)) unless ele.blank? } + deps.each { |dep| self.add_predecessor(self.user.todos.find_by_id(dep.to_i)) unless dep.blank? } end alias_method :original_context=, :context= @@ -339,7 +347,10 @@ class Todo < ActiveRecord::Base # used by the REST API. will also work, this is renamed to add_tags in TodosController::TodoCreateParamsHelper::initialize def add_tags=(params) - tag_with params[:tag].inject([]) { |list, value| list << value[:name] } unless params[:tag].nil? + unless params[:tag].nil? + tag_list = params[:tag].inject([]) { |list, value| list << value[:name] } + tag_with tag_list.join(", ") + end end # Rich Todo API @@ -354,9 +365,9 @@ class Todo < ActiveRecord::Base context_id = default_context_id unless(context.nil?) - found_context = user.contexts.active.find_by_namepart(context) - found_context = user.contexts.find_by_namepart(context) if found_context.nil? - context_id = found_context.id unless found_context.nil? + found_context = user.contexts.active.where("name like ?", "%#{context}%").first + found_context = user.contexts.where("name like ?", "%#{context}%").first if !found_context + context_id = found_context.id if found_context end unless user.contexts.exists? context_id @@ -384,4 +395,12 @@ class Todo < ActiveRecord::Base return todo end + def render_note + unless self.notes.nil? + self.rendered_notes = Tracks::Utils.render_text(self.notes) + else + self.rendered_notes = nil + end + end + end diff --git a/app/models/user.rb b/app/models/user.rb index 26ebe9c7..0dd35660 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,12 +5,17 @@ class User < ActiveRecord::Base # Virtual attribute for the unencrypted password attr_accessor :password attr_protected :is_admin # don't allow mass-assignment for this + + attr_accessible :login, :first_name, :last_name, :password_confirmation, :password, :auth_type, :open_id_url + #for will_paginate plugin + cattr_accessor :per_page + @@per_page = 5 has_many :contexts, :order => 'position ASC', :dependent => :delete_all do def find_by_params(params) - find(params['id'] || params['context_id']) || nil + find_by_id(params['id'] || params['context_id']) || nil end def update_positions(context_ids) context_ids.each_with_index {|id, position| @@ -24,7 +29,7 @@ class User < ActiveRecord::Base :order => 'projects.position ASC', :dependent => :delete_all do def find_by_params(params) - find(params['id'] || params['project_id']) + find_by_id(params['id'] || params['project_id']) end def update_positions(project_ids) project_ids.each_with_index {|id, position| @@ -34,7 +39,7 @@ class User < ActiveRecord::Base } end def projects_in_state_by_position(state) - self.sort{ |a,b| a.position <=> b.position }.select{ |p| p.state == state } + self.sort{ |a,b| a.position <=> b.position }.select{ |p| p.state == state } end def next_from(project) self.offset_from(project, 1) @@ -49,29 +54,29 @@ class User < ActiveRecord::Base projects.at( position + offset) end def cache_note_counts - project_note_counts = Note.count(:group => 'project_id') + project_note_counts = Note.group(:project_id).count self.each do |project| project.cached_note_count = project_note_counts[project.id] || 0 end end def alphabetize(scope_conditions = {}) - projects = find(:all, :conditions => scope_conditions) + projects = where(scope_conditions) projects.sort!{ |x,y| x.name.downcase <=> y.name.downcase } self.update_positions(projects.map{ |p| p.id }) return projects end def actionize(scope_conditions = {}) - todos_in_project = find(:all, :conditions => scope_conditions, :include => [:todos]) + todos_in_project = where(scope_conditions).includes(:todos) todos_in_project.sort!{ |x, y| -(x.todos.active.count <=> y.todos.active.count) } todos_in_project.reject{ |p| p.todos.active.count > 0 } sorted_project_ids = todos_in_project.map {|p| p.id} - all_project_ids = find(:all).map {|p| p.id} + all_project_ids = all.map {|p| p.id} other_project_ids = all_project_ids - sorted_project_ids update_positions(sorted_project_ids + other_project_ids) - return find(:all, :conditions => scope_conditions) + return where(scope_conditions) end end has_many :todos, @@ -85,14 +90,12 @@ class User < ActiveRecord::Base :conditions => [ 'state = ?', 'deferred' ], :order => 'show_from ASC, todos.created_at DESC' do def find_and_activate_ready - find(:all, :conditions => ['show_from <= ?', Time.zone.now ]).collect { |t| t.activate! } + where('show_from <= ?', Time.zone.now).collect { |t| t.activate! } end end has_many :notes, :order => "created_at DESC", :dependent => :delete_all has_one :preference, :dependent => :destroy - attr_protected :is_admin - validates_presence_of :login validates_presence_of :password, :if => :password_required? validates_length_of :password, :within => 5..40, :if => :password_required? @@ -100,17 +103,12 @@ class User < ActiveRecord::Base validates_confirmation_of :password validates_length_of :login, :within => 3..80 validates_uniqueness_of :login, :on => :create - validates_presence_of :open_id_url, :if => :using_openid? + validate :validate_auth_type before_create :crypt_password, :generate_token before_update :crypt_password - before_save :normalize_open_id_url - #for will_paginate plugin - cattr_accessor :per_page - @@per_page = 5 - - def validate + def validate_auth_type unless Tracks::Config.auth_schemes.include?(auth_type) errors.add("auth_type", "not a valid authentication type (#{auth_type})") end @@ -120,7 +118,7 @@ class User < ActiveRecord::Base def self.authenticate(login, pass) return nil if login.blank? - candidate = find(:first, :conditions => ["login = ?", login]) + candidate = where("login = ?", login).first return nil if candidate.nil? if Tracks::Config.auth_schemes.include?('database') @@ -137,25 +135,15 @@ class User < ActiveRecord::Base return candidate if candidate.auth_type.eql?("cas") end - if Tracks::Config.auth_schemes.include?('open_id') - # hope the user enters the correct data - return candidate if candidate.auth_type.eql?("open_id") - end - return nil end - def self.find_by_open_id_url(raw_identity_url) - normalized_open_id_url = OpenIdAuthentication.normalize_identifier(raw_identity_url) - find(:first, :conditions => ['open_id_url = ?', normalized_open_id_url]) - end - def self.no_users_yet? count == 0 end def self.find_admin - find(:first, :conditions => [ "is_admin = ?", true ]) + where(:is_admin => true).first end def to_param @@ -192,7 +180,7 @@ class User < ActiveRecord::Base end def generate_token - self.token = sha1 "#{Time.now.to_i}#{rand}" + self.token = Digest::SHA1.hexdigest "#{Time.now.to_i}#{rand}" end def remember_token? @@ -202,14 +190,14 @@ class User < ActiveRecord::Base # These create and unset the fields required for remembering users between browser closes def remember_me self.remember_token_expires_at = 2.weeks.from_now.utc - self.remember_token ||= sha1("#{login}--#{remember_token_expires_at}") - save(false) + self.remember_token ||= Digest::SHA1.hexdigest("#{login}--#{remember_token_expires_at}") + save end def forget_me self.remember_token_expires_at = nil self.remember_token = nil - save(false) + save end # Returns true if the user has a password hashed using SHA-1. @@ -230,37 +218,22 @@ class User < ActiveRecord::Base end def sha1(s) - Digest::SHA1.hexdigest salted s + Digest::SHA1.hexdigest(salted(s)) end - def hash(s) - BCrypt::Password.create s + def create_hash(s) + BCrypt::Password.create(s) end protected def crypt_password return if password.blank? - write_attribute("crypted_password", hash(password)) if password == password_confirmation + write_attribute("crypted_password", self.create_hash(password)) if password == password_confirmation end def password_required? auth_type == 'database' && crypted_password.blank? || !password.blank? end - def using_openid? - auth_type == 'open_id' - end - - def normalize_open_id_url - return if open_id_url.nil? - - # fixup empty url value - if open_id_url.empty? - self.open_id_url = nil - return - end - - self.open_id_url = OpenIdAuthentication.normalize_identifier(open_id_url) - end end diff --git a/app/views/contexts/_context.rhtml b/app/views/contexts/_context.html.erb similarity index 80% rename from app/views/contexts/_context.rhtml rename to app/views/contexts/_context.html.erb index a6eb63ab..fdd23a1f 100644 --- a/app/views/contexts/_context.rhtml +++ b/app/views/contexts/_context.html.erb @@ -1,5 +1,5 @@ -<% @not_done = @not_done_todos.select {|t| t.context_id == context.id } %> -
    > +<% @not_done = @not_done_todos.select {|t| t.context_id == context.id } -%> +
    ">

    <% if collapsible -%> <%= image_tag("collapse.png") %> @@ -16,5 +16,5 @@

    <%= t 'contexts.no_actions' %>

    <%= render :partial => "todos/todo", :collection => @not_done, :locals => { :parent_container_type => "context" } %> -
    - + + diff --git a/app/views/contexts/_context.m.erb b/app/views/contexts/_context.m.erb new file mode 100644 index 00000000..6e9174af --- /dev/null +++ b/app/views/contexts/_context.m.erb @@ -0,0 +1,14 @@ +<% +# select actions from this context +@not_done = @not_done_todos.select {|t| t.context_id == context.id } + +if not @not_done.empty? + # only show a context when there are actions in it + -%> +

    <%= link_to context.name, context_path(context, :format => 'm') %>

    + +<% end -%> diff --git a/app/views/contexts/_context.text.erb b/app/views/contexts/_context.text.erb new file mode 100644 index 00000000..65767520 --- /dev/null +++ b/app/views/contexts/_context.text.erb @@ -0,0 +1,6 @@ +<% + todos_in_context = not_done_todos.select { |t| t.context_id == context.id } + if todos_in_context.length > 0 +-%> <%= context.name.upcase %>: +<%= render :partial => "todos/todo", :collection => todos_in_context -%> +<% end -%> diff --git a/app/views/contexts/_context_form.rhtml b/app/views/contexts/_context_form.html.erb similarity index 74% rename from app/views/contexts/_context_form.rhtml rename to app/views/contexts/_context_form.html.erb index 92bd39aa..228be0c8 100644 --- a/app/views/contexts/_context_form.rhtml +++ b/app/views/contexts/_context_form.html.erb @@ -1,20 +1,18 @@ -<% context = context_form -@context = context +<% context = context_form -%> -<% form_for(context, :html => { +<%= form_for(context, :html => { :id => dom_id(context, 'edit_form'), :class => "inline-form edit-project-form", :method => :put }) do -%> -
    <%= error_messages_for("project") %>
    +
    <%= get_list_of_error_messages_for(context) %>
    - -
    +
    <%= text_field('context', 'name', :class => 'context-name', :tabindex => next_tab_index) %>
    - + <%= check_box('context', 'hide', {:class => 'context-hide', :tabindex => next_tab_index}) %> diff --git a/app/views/contexts/_context_listing.html.erb b/app/views/contexts/_context_listing.html.erb new file mode 100644 index 00000000..06bd864f --- /dev/null +++ b/app/views/contexts/_context_listing.html.erb @@ -0,0 +1,18 @@ +<% +context = context_listing +suppress_drag_handle ||= false +suppress_edit_button ||= false +suppress_delete_button ||= false +%> +
    " class="list"> +
    + <%= suppress_delete_button ? "" : link_to_delete_context(context, image_tag( "blank.png", :title => t('contexts.delete_context'), :class=>"delete_item")) -%> + <%= suppress_edit_button ? "" : link_to_edit_context(context, image_tag( "blank.png", :title => t('contexts.edit_context'), :class=>"edit_item")) -%> + <%= suppress_drag_handle ? "" : image_tag('grip.png', :width => '7', :height => '16', :border => '0', :title => t('common.drag_handle'), :class => 'grip')-%> +
    + <%= link_to_context( context ) %> <%= raw " (#{count_undone_todos_phrase(context)})" %> +
    +
    + +
    diff --git a/app/views/contexts/_context_listing.m.erb b/app/views/contexts/_context_listing.m.erb new file mode 100644 index 00000000..c13cba2d --- /dev/null +++ b/app/views/contexts/_context_listing.m.erb @@ -0,0 +1,2 @@ +<% context = context_listing -%> +
    <%= link_to context.name, context_path(context, :format => 'm') %><%= " (#{count_undone_todos_phrase(context)})".html_safe %>
    diff --git a/app/views/contexts/_context_listing.rhtml b/app/views/contexts/_context_listing.rhtml deleted file mode 100644 index b98a0f51..00000000 --- a/app/views/contexts/_context_listing.rhtml +++ /dev/null @@ -1,30 +0,0 @@ -<% context = context_listing -suppress_drag_handle ||= false -suppress_edit_button ||= false -%> -
    " class="list"> -
    - <% unless suppress_drag_handle -%> -
    - <%= t('common.drag_handle') %> -
    - <% end -%> -
    - <%= link_to_context( context ) %> <%= " (" + count_undone_todos_phrase(context,"actions") + ")" %> -
    - -
    - <% if context.hide? %> - <%= t('states.hidden') %> - <% else %> - <%= t('states.visible') %> - <% end %> - - <%= link_to_delete_context(context, image_tag( "blank.png", :title => t('contexts.delete_context'), :class=>"delete_item")) %> - <%= suppress_edit_button ? "" : link_to_edit_context(context, image_tag( "blank.png", :title => t('contexts.edit_context'), :class=>"edit_item")) %> - -
    -
    - -
    diff --git a/app/views/contexts/_context_state_group.rhtml b/app/views/contexts/_context_state_group.html.erb similarity index 99% rename from app/views/contexts/_context_state_group.rhtml rename to app/views/contexts/_context_state_group.html.erb index d2971eb3..3437a5f8 100644 --- a/app/views/contexts/_context_state_group.rhtml +++ b/app/views/contexts/_context_state_group.html.erb @@ -3,9 +3,11 @@ <%= context_state_group.length %> <%= t("states."+ state +"_plural")%> <%= t('common.contexts') %> +

    <%= t('contexts.no_contexts_' + state) %>

    +
    <%= render :partial => 'context_listing', :collection => context_state_group %>
    diff --git a/app/views/contexts/_mobile_context.rhtml b/app/views/contexts/_mobile_context.rhtml deleted file mode 100644 index 46d45db9..00000000 --- a/app/views/contexts/_mobile_context.rhtml +++ /dev/null @@ -1,14 +0,0 @@ -<% -# select actions from this context -@not_done = @not_done_todos.select {|t| t.context_id == mobile_context.id } - -if not @not_done.empty? - # only show a context when there are actions in it - -%> -

    <%= link_to mobile_context.name, context_path(mobile_context, :format => 'm') %>

    - -<% end -%> diff --git a/app/views/contexts/_mobile_context_listing.rhtml b/app/views/contexts/_mobile_context_listing.rhtml deleted file mode 100644 index c2360ce7..00000000 --- a/app/views/contexts/_mobile_context_listing.rhtml +++ /dev/null @@ -1,2 +0,0 @@ -<% context = mobile_context_listing -%> -
    <%= link_to context.name, context_path(context, :format => 'm') %><%= " (" + count_undone_todos_phrase(context,"actions") + ")" %>
    diff --git a/app/views/contexts/_new_context_form.rhtml b/app/views/contexts/_new_context_form.html.erb similarity index 64% rename from app/views/contexts/_new_context_form.rhtml rename to app/views/contexts/_new_context_form.html.erb index 26d65348..92fd8c20 100644 --- a/app/views/contexts/_new_context_form.rhtml +++ b/app/views/contexts/_new_context_form.html.erb @@ -6,9 +6,17 @@
    - <% form_for(@new_context, :html => {:id => 'context-form',:name=>'context',:class => "inline-form", :method => :post }) do -%> + <%= form_for(@new_context, :html => {:id => 'context-form',:name=>'context',:class => "inline-form", :method => :post }) do -%> -
    <%= error_messages_for('context') %>
    +
    + <% if @new_context.errors.any? -%> +
      + <% @new_context.errors.full_messages.each do |msg| %> +
    • <%= msg %>
    • + <% end %> +
    + <% end -%> +

    <%= text_field( "context", "name", :tabindex => next_tab_index ) %>
    @@ -19,7 +27,7 @@
    diff --git a/app/views/contexts/_text_context.rhtml b/app/views/contexts/_text_context.rhtml deleted file mode 100644 index 84e57a25..00000000 --- a/app/views/contexts/_text_context.rhtml +++ /dev/null @@ -1,7 +0,0 @@ -<% -context = text_context -todos_in_context = not_done_todos.select { |t| t.context_id == context.id } -if todos_in_context.length > 0 --%> <%= context.name.upcase %>: -<%= render :partial => "todos/text_todo", :collection => todos_in_context -%> -<% end -%> diff --git a/app/views/contexts/create.js.erb b/app/views/contexts/create.js.erb index 4b3bcdde..fead9ed7 100644 --- a/app/views/contexts/create.js.erb +++ b/app/views/contexts/create.js.erb @@ -26,5 +26,5 @@ function html_for_context_listing() { } function html_for_error_messages() { - return "<%= escape_javascript(error_messages_for('context')) %>"; + return "<%= escape_javascript(get_list_of_error_messages_for(@context)) %>"; } \ No newline at end of file diff --git a/app/views/contexts/index.atom.builder b/app/views/contexts/index.atom.builder new file mode 100644 index 00000000..5a6cabc9 --- /dev/null +++ b/app/views/contexts/index.atom.builder @@ -0,0 +1,12 @@ +atom_feed do |feed| + feed.title(@feed_title) + feed.subtitle(@feed_description) + feed.updated(@all_contexts.last.updated_at) + + @all_contexts.each do |context| + feed.entry(context) do |entry| + entry.title(h(context.name)) + entry.content(context_summary(context, count_undone_todos_phrase(context)), :type => :html) + end + end +end \ No newline at end of file diff --git a/app/views/contexts/index.m.erb b/app/views/contexts/index.m.erb new file mode 100644 index 00000000..d32cc414 --- /dev/null +++ b/app/views/contexts/index.m.erb @@ -0,0 +1,2 @@ +

    <%= t('contexts.visible_contexts') %>

    <%= render :partial => 'context_listing', :collection => @active_contexts %> +

    <%= t('contexts.hidden_contexts') %>

    <%= render :partial => 'context_listing', :collection => @hidden_contexts %> \ No newline at end of file diff --git a/app/views/contexts/index.rss.builder b/app/views/contexts/index.rss.builder new file mode 100644 index 00000000..5459d1f1 --- /dev/null +++ b/app/views/contexts/index.rss.builder @@ -0,0 +1,20 @@ +xml.instruct! :xml, :version => "1.0" +xml.rss :version => "2.0" do + xml.channel do + xml.title @feed_title + xml.description @feed_description + xml.link contexts_url + xml.language 'en-us' + xml.ttl 40 + + @all_contexts.each do |context| + xml.item do + xml.title h(context.title) + xml.description context_summary(context, count_undone_todos_phrase(context)) + xml.pubDate context.created_at.to_s(:rfc822) + xml.link context_url(context) + xml.guid context_url(context) + end + end + end +end diff --git a/app/views/contexts/index_mobile.rhtml b/app/views/contexts/index_mobile.rhtml deleted file mode 100644 index 1d78e9c8..00000000 --- a/app/views/contexts/index_mobile.rhtml +++ /dev/null @@ -1,2 +0,0 @@ -

    <%= t('contexts.visible_contexts') %>

    <%= render :partial => 'mobile_context_listing', :collection => @active_contexts %> -

    <%= t('contexts.hidden_contexts') %>

    <%= render :partial => 'mobile_context_listing', :collection => @hidden_contexts %> \ No newline at end of file diff --git a/app/views/contexts/show.html.erb b/app/views/contexts/show.html.erb index 065fe94a..f3b3f804 100644 --- a/app/views/contexts/show.html.erb +++ b/app/views/contexts/show.html.erb @@ -1,11 +1,12 @@
    -<%= render :partial => "contexts/context", :object => @context, :locals => { :collapsible => false } %> +<%= render :partial => @context, :locals => { :collapsible => false } %> +<%= render :partial => "todos/deferred", :object => @deferred, :locals => { :collapsible => false, :append_descriptor => t('contexts.todos_append'), :parent_container_type => 'context', :pending => @pending } %> <% unless @max_completed==0 -%> <%= render :partial => "todos/completed", :object => @done, :locals => { :suppress_context => true, :collapsible => false, :append_descriptor => t('contexts.last_completed_in_context', :number=>prefs.show_number_completed) } %> <% end -%> -
    +
    <%= render :partial => "shared/add_new_item_form" %> - <%= render :file => "sidebar/sidebar.html.erb" %> -
    + <%= render :file => "sidebar/sidebar" %> + diff --git a/app/views/contexts/mobile_show_context.rhtml b/app/views/contexts/show.m.erb similarity index 55% rename from app/views/contexts/mobile_show_context.rhtml rename to app/views/contexts/show.m.erb index 3bf1fc3b..7c50f7c5 100644 --- a/app/views/contexts/mobile_show_context.rhtml +++ b/app/views/contexts/show.m.erb @@ -1,14 +1,12 @@ +

    <%=@context.name%>

    <% # select actions from this context @not_done = @not_done_todos.select {|t| t.context_id == @context.id } -if not @not_done.empty? +if not @not_done.empty? # only show a context when there are actions in it - %> -

    <%=@context.name%>

    +-%> <% end -%> diff --git a/app/views/contexts/update.js.erb b/app/views/contexts/update.js.erb index 576cf6f3..b88b538e 100644 --- a/app/views/contexts/update.js.erb +++ b/app/views/contexts/update.js.erb @@ -34,7 +34,7 @@ function replace_context_form_with_updated_context() { } function html_for_error_messages() { - return "<%= escape_javascript(error_messages_for('context')) %>"; + return "<%= escape_javascript(get_list_of_error_messages_for(@context)) %>"; } function html_for_context_listing() { diff --git a/app/views/data/yaml_form.de.html.erb b/app/views/data/yaml_form.de.html.erb index 7f6baeb7..a1827241 100644 --- a/app/views/data/yaml_form.de.html.erb +++ b/app/views/data/yaml_form.de.html.erb @@ -8,7 +8,7 @@

    Fügen Sie den Inhalt der kopierten YAML Datei in das untenstehende Formular ein:

    - <% form_for :import, @import, :url => {:controller => 'data', :action => 'yaml_import'} do |f| %> + <%= form_for :import, @import, :url => {:controller => 'data', :action => 'yaml_import'} do |f| %> <%= f.text_area :yaml %>
    <% end %> diff --git a/app/views/data/yaml_form.en.html.erb b/app/views/data/yaml_form.en.html.erb index 1b89a9db..c7f04165 100644 --- a/app/views/data/yaml_form.en.html.erb +++ b/app/views/data/yaml_form.en.html.erb @@ -1,14 +1,14 @@

    -

    Beware: all your current data will be destroyed before importing - the YAML file, so if you have access to the database, we strongly recommend +

    Beware: all your current data will be destroyed before importing + the YAML file, so if you have access to the database, we strongly recommend backing up the database right now in case that anything goes wrong.

    Paste the contents of the YAML file you exported into the text box below:

    - <% form_for :import, @import, :url => {:controller => 'data', :action => 'yaml_import'} do |f| %> + <%= form_for :import, @import, :url => {:controller => 'data', :action => 'yaml_import'} do |f| %> <%= f.text_area :yaml %>
    <% end %> diff --git a/app/views/feedlist/_feed_for_context.html.erb b/app/views/feedlist/_feed_for_context.html.erb index 33e8b6d9..0ffa13e1 100644 --- a/app/views/feedlist/_feed_for_context.html.erb +++ b/app/views/feedlist/_feed_for_context.html.erb @@ -1,25 +1,21 @@ -<% context = @active_contexts.empty? ? @hidden_contexts.first : @active_contexts.first --%> - +<% context = @active_contexts.empty? ? @hidden_contexts.first : @active_contexts.first -%>

    <%= t('feedlist.context_centric_actions') %>:

    +
      <% if @active_contexts.empty? && @hidden_contexts.empty? -%> -
      • <%= t('feedlist.context_needed') %>
      +
    • <%= t('feedlist.context_needed') %>
    • <% else -%> -
        -
      • <%= t('common.numbered_step', :number => 1) %> - <%= t('feedlist.choose_context') %>: - -
      • -
      • <%= t('common.numbered_step', :number => 2) %> - <%= t('feedlist.select_feed_for_context') %> -
        -
        -
      • <%= all_feed_links_for_context(context) %>
      • -
    +
  • <%= t('common.numbered_step', :number => 1) %> - <%= t('feedlist.choose_context') %>: + +
  • +
  • <%= t('common.numbered_step', :number => 2) %> - <%= t('feedlist.select_feed_for_context') %> +
    +
    + <%= all_feed_links_for_context(context) %>
    -
  • - +
    + <% end -%> - - + \ No newline at end of file diff --git a/app/views/feedlist/_feed_for_project.html.erb b/app/views/feedlist/_feed_for_project.html.erb index 53d80462..0372e571 100644 --- a/app/views/feedlist/_feed_for_project.html.erb +++ b/app/views/feedlist/_feed_for_project.html.erb @@ -14,7 +14,7 @@
  • <%= t('common.numbered_step', :number => 2) %> - <%= t('feedlist.select_feed_for_project') %>
    -
  • <%= all_feed_links_for_project(project) %>
  • + <%= all_feed_links_for_project(project) %> diff --git a/app/views/feedlist/_legend.rhtml b/app/views/feedlist/_legend.html.erb similarity index 100% rename from app/views/feedlist/_legend.rhtml rename to app/views/feedlist/_legend.html.erb diff --git a/app/views/feedlist/index.html.erb b/app/views/feedlist/index.html.erb index 2812f5d6..544282fe 100644 --- a/app/views/feedlist/index.html.erb +++ b/app/views/feedlist/index.html.erb @@ -19,5 +19,5 @@
    - <%= render :file => "sidebar/sidebar.html.erb" %> + <%= render :file => "sidebar/sidebar" %>
    diff --git a/app/views/feedlist/mobile_index.rhtml b/app/views/feedlist/index.m.erb similarity index 100% rename from app/views/feedlist/mobile_index.rhtml rename to app/views/feedlist/index.m.erb diff --git a/app/views/integrations/_applescript1.rhtml b/app/views/integrations/_applescript1.html.erb similarity index 90% rename from app/views/integrations/_applescript1.rhtml rename to app/views/integrations/_applescript1.html.erb index a0a2e14f..1e6497d5 100644 --- a/app/views/integrations/_applescript1.rhtml +++ b/app/views/integrations/_applescript1.html.erb @@ -6,13 +6,13 @@ set myUsername to "<%= current_user.login %>" set myToken to "<%= current_user.token %>" set myContextID to <%= context.id %> (* <%= context.name %> *) --- Display dialog to enter your description +-- Display dialog to enter your description display dialog "<%= t('integrations.applescript_next_action_prompt') %>" default answer "" set myDesc to text returned of the result -- Now send all that info to Tracks -- Edit the URL of your Tracks installation if necessary" -tell application "<%= home_url %>backend/api" +tell application "<%= root_url %>backend/api" set returnValue to call xmlrpc {method name:"NewTodo", parameters:{myUsername, myToken, myContextID, myDesc}} end tell diff --git a/app/views/integrations/_applescript2.rhtml b/app/views/integrations/_applescript2.html.erb similarity index 88% rename from app/views/integrations/_applescript2.rhtml rename to app/views/integrations/_applescript2.html.erb index ba18d42d..7eb014bb 100644 --- a/app/views/integrations/_applescript2.rhtml +++ b/app/views/integrations/_applescript2.html.erb @@ -1,10 +1,10 @@ -(* +(* Script to grab the sender and subject of the selected Mail message(s), and create new next action(s) with description "Email [sender] about [subject]" If you have Growl, it pops a notification up with the id of -the newly created action. +the newly created action. *) (* Edit appropriately for your setup *) @@ -50,25 +50,25 @@ on importMessage(theMessage) -- Now send all that info to Tracks -- Edit the URL of your Tracks installation if necessary" - tell application "<%= home_url %>backend/api" + tell application "<%= root_url %>backend/api" set returnValue to call xmlrpc {method name:"NewTodo", parameters:{myUsername, myToken, myContextID, myDesc, myNote}} end tell - (* Growl support - comment out or delete this section if + (* Growl support - comment out or delete this section if you don't have Growl *) tell application "GrowlHelperApp" set the allNotificationsList to ¬ {"Tracks Notification"} - -- Make a list of the notifications - -- that will be enabled by default. - -- Those not enabled by default can be enabled later + -- Make a list of the notifications + -- that will be enabled by default. + -- Those not enabled by default can be enabled later -- in the 'Applications' tab of the growl prefpane. set the enabledNotificationsList to ¬ {"Tracks Notification"} -- Register our script with growl. - -- You can optionally (as here) set a default icon + -- You can optionally (as here) set a default icon -- for this script's notifications. register as application ¬ "Tracks Applescript" all notifications allNotificationsList ¬ diff --git a/app/views/integrations/_quicksilver_applescript.rhtml b/app/views/integrations/_quicksilver_applescript.html.erb similarity index 91% rename from app/views/integrations/_quicksilver_applescript.rhtml rename to app/views/integrations/_quicksilver_applescript.html.erb index 35db68db..59a0e285 100644 --- a/app/views/integrations/_quicksilver_applescript.rhtml +++ b/app/views/integrations/_quicksilver_applescript.html.erb @@ -5,7 +5,7 @@ using terms from application "Quicksilver" set myToken to "<%= current_user.token %>" set myContextID to <%= context.id %> (* <%= context.name %> *) - tell application "<%= home_url %>backend/api" + tell application "<%= root_url %>backend/api" set returnValue to call xmlrpc {method name:"NewTodo", parameters:{myUsername, myToken, myContextID, ThisClipping}} end tell tell application "Quicksilver" diff --git a/app/views/integrations/google_gadget.erb b/app/views/integrations/google_gadget.erb index f9e93562..ff113f54 100644 --- a/app/views/integrations/google_gadget.erb +++ b/app/views/integrations/google_gadget.erb @@ -1,5 +1,5 @@ - + - + \ No newline at end of file diff --git a/app/views/integrations/index.de.html.erb b/app/views/integrations/index.de.html.erb index 52f812e9..3f701411 100644 --- a/app/views/integrations/index.de.html.erb +++ b/app/views/integrations/index.de.html.erb @@ -90,7 +90,7 @@

    If you enter the following entry to your crontab, you will receive email every day around 5 AM with a list of the upcoming actions which are due within the next 7 days.

    - +

    You can of course use other text <%= link_to 'feeds provided by Tracks', feeds_path %> -- why not email a list of next actions in a particular project to a group of colleagues who are working on the project?

    @@ -100,7 +100,7 @@ If Tracks is running on the same server as your mail server, you can use the integrated mail handler built into tracks. Steps to set it up:


    -

    Do you have one of your own to add? +

    Do you have one of your own to add? Tell us about it in our Tips and Tricks forum and we may include it on this page in a future versions of Tracks.

    @@ -90,7 +90,7 @@

    If you enter the following entry to your crontab, you will receive email every day around 5 AM with a list of the upcoming actions which are due within the next 7 days.

    - +

    You can of course use other text <%= link_to 'feeds provided by Tracks', feeds_path %> -- why not email a list of next actions in a particular project to a group of colleagues who are working on the project?

    @@ -100,7 +100,7 @@ If Tracks is running on the same server as your mail server, you can use the integrated mail handler built into tracks. Steps to set it up: