From 2a6b8f1cd93dc835a9bca3e1f756c3de9b6dc3a3 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Mon, 18 Aug 2008 14:11:27 +0200 Subject: [PATCH 001/102] fixes #753 by adding a global option to enable secure cookies you need to recreate your environment.rb from the tmpl for this change --- app/controllers/contexts_controller.rb | 4 ++-- app/controllers/login_controller.rb | 10 +++++----- app/controllers/projects_controller.rb | 4 ++-- app/controllers/todos_controller.rb | 6 +++--- config/environment.rb.tmpl | 2 ++ lib/login_system.rb | 2 +- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/controllers/contexts_controller.rb b/app/controllers/contexts_controller.rb index 4a9984b8..228434d5 100644 --- a/app/controllers/contexts_controller.rb +++ b/app/controllers/contexts_controller.rb @@ -133,7 +133,7 @@ class ContextsController < ApplicationController @active_contexts = @contexts.find(:all, { :conditions => ["hide = ?", false]}) @hidden_contexts = @contexts.find(:all, { :conditions => ["hide = ?", true]}) @down_count = @active_contexts.size + @hidden_contexts.size - cookies[:mobile_url]=request.request_uri + cookies[:mobile_url]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE} render :action => 'index_mobile' end end @@ -143,7 +143,7 @@ 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]=request.request_uri + cookies[:mobile_url]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE} @mobile_from_context = @context.id render :action => 'mobile_show_context' end diff --git a/app/controllers/login_controller.rb b/app/controllers/login_controller.rb index d6d41f4b..a9b7e8be 100644 --- a/app/controllers/login_controller.rb +++ b/app/controllers/login_controller.rb @@ -20,10 +20,10 @@ class LoginController < ApplicationController 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 } + cookies[:tracks_login] = { :value => @user.login, :expires => Time.now + 1.year, :secure => TRACKS_COOKIES_SECURE } unless should_expire_sessions? @user.remember_me - cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at } + cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at, :secure => TRACKS_COOKIES_SECURE } end redirect_back_or_home return @@ -94,12 +94,12 @@ class LoginController < ApplicationController session['noexpiry'] = session['user_noexpiry'] msg = (should_expire_sessions?) ? "will expire after 1 hour of inactivity." : "will not expire." notify :notice, "You have successfully verified #{openid_url} as your identity. Login successful: session #{msg}" - cookies[:tracks_login] = { :value => @user.login, :expires => Time.now + 1.year } + cookies[:tracks_login] = { :value => @user.login, :expires => Time.now + 1.year, :secure => TRACKS_COOKIES_SECURE } unless should_expire_sessions? @user.remember_me - cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at } + cookies[:auth_token] = { :value => @user.remember_token , :expires => @user.remember_token_expires_at, :secure => TRACKS_COOKIES_SECURE } end - cookies[:openid_url] = { :value => openid_url, :expires => Time.now + 1.year } + cookies[:openid_url] = { :value => openid_url, :expires => Time.now + 1.year, :secure => TRACKS_COOKIES_SECURE } redirect_back_or_home else notify :warning, "You have successfully verified #{openid_url} as your identity, but you do not have a Tracks account. Please ask your administrator to sign you up." diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3889b8bd..da268803 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -204,7 +204,7 @@ class ProjectsController < ApplicationController @hidden_projects = @projects.select{ |p| p.hidden? } @completed_projects = @projects.select{ |p| p.completed? } @down_count = @active_projects.size + @hidden_projects.size + @completed_projects.size - cookies[:mobile_url]=request.request_uri + cookies[:mobile_url]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE} render :action => 'index_mobile' end end @@ -217,7 +217,7 @@ class ProjectsController < ApplicationController @project_default_context = "The default context for this project is "+ @project.default_context.name end - cookies[:mobile_url]=request.request_uri + cookies[:mobile_url]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE} @mobile_from_project = @project.id render :action => 'project_mobile' end diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 49d9d0ca..bb72723b 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -229,7 +229,7 @@ class TodosController < ApplicationController format.m do if @saved if cookies[:mobile_url] - cookies[:mobile_url] = nil + cookies[:mobile_url] = {:value => nil, :secure => TRACKS_COOKIES_SECURE} redirect_to cookies[:mobile_url] else redirect_to formatted_todos_path(:m) @@ -370,7 +370,7 @@ class TodosController < ApplicationController @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json } format.m { - cookies[:mobile_url]=request.request_uri + cookies[:mobile_url]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE} render :action => "mobile_tag" } end @@ -608,7 +608,7 @@ class TodosController < ApplicationController lambda do @page_title = "All actions" @home = true - cookies[:mobile_url]=request.request_uri + cookies[:mobile_url]= { :value => request.request_uri, :secure => TRACKS_COOKIES_SECURE} determine_down_count render :action => 'index' diff --git a/config/environment.rb.tmpl b/config/environment.rb.tmpl index 903df142..d8123647 100644 --- a/config/environment.rb.tmpl +++ b/config/environment.rb.tmpl @@ -93,6 +93,8 @@ if (AUTHENTICATION_SCHEMES.include? 'open_id') #requires ruby-openid gem to be installed end +# setting this to true will make the cookies only available over HTTPS +TRACKS_COOKIES_SECURE = false MOBILE_CONTENT_TYPE = 'tracks/mobile' Mime::Type.register(MOBILE_CONTENT_TYPE, :m) diff --git a/lib/login_system.rb b/lib/login_system.rb index 388ef63a..92128422 100644 --- a/lib/login_system.rb +++ b/lib/login_system.rb @@ -48,7 +48,7 @@ module LoginSystem session['user_id'] = user.id set_current_user(user) current_user.remember_me - cookies[:auth_token] = { :value => current_user.remember_token , :expires => current_user.remember_token_expires_at } + cookies[:auth_token] = { :value => current_user.remember_token , :expires => current_user.remember_token_expires_at, :secure => TRACKS_COOKIES_SECURE } flash[:notice] = "Logged in successfully. Welcome back!" end end From b2e82ea6b69c395fc5a192ffc23d4582af0d0b35 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Mon, 18 Aug 2008 16:02:13 +0200 Subject: [PATCH 002/102] fix for #749 This one needs a nice test --- app/controllers/contexts_controller.rb | 6 +++++- app/controllers/projects_controller.rb | 4 ++++ app/controllers/todos_controller.rb | 1 + app/views/contexts/_context.rhtml | 4 +++- app/views/contexts/update_context_name.js.rjs | 10 ++++++++++ app/views/projects/_project.rhtml | 4 +++- app/views/projects/update_project_name.js.rjs | 10 ++++++++++ app/views/shared/_add_new_item_form.rhtml | 1 + app/views/todos/create.js.rjs | 1 + 9 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 app/views/contexts/update_context_name.js.rjs create mode 100644 app/views/projects/update_project_name.js.rjs diff --git a/app/controllers/contexts_controller.rb b/app/controllers/contexts_controller.rb index 228434d5..34c11d26 100644 --- a/app/controllers/contexts_controller.rb +++ b/app/controllers/contexts_controller.rb @@ -83,10 +83,14 @@ class ContextsController < ApplicationController end @context.attributes = params["context"] if @context.save - if params['wants_render'] + if boolean_param('wants_render') respond_to do |format| format.js end + elsif boolean_param('update_context_name') + @contexts = current_user.projects + render :template => 'contexts/update_context_name.js.rjs' + return else render :text => success_text || 'Success' end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index da268803..4cd565d2 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -136,6 +136,10 @@ class ProjectsController < ApplicationController @initial_context_name = @project.default_context.name render :template => 'projects/update_default_context.js.rjs' return + elsif boolean_param('update_project_name') + @projects = current_user.projects + render :template => 'projects/update_project_name.js.rjs' + return else render :text => success_text || 'Success' return diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index bb72723b..3dddc2bd 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -89,6 +89,7 @@ class TodosController < ApplicationController @contexts = current_user.contexts.find(:all) if @new_context_created @projects = current_user.projects.find(:all) if @new_project_created @initial_context_name = params['default_context_name'] + @initial_project_name = params['default_project_name'] render :action => 'create' end format.xml do diff --git a/app/views/contexts/_context.rhtml b/app/views/contexts/_context.rhtml index 7fb2e942..bd90099f 100644 --- a/app/views/contexts/_context.rhtml +++ b/app/views/contexts/_context.rhtml @@ -25,7 +25,9 @@ <% end -%> <% if source_view_is :context %> <%= context.name %> - <%= in_place_editor 'context_name_in_place_editor', { :url => { :controller => 'contexts', :action => 'update', :id => context.id, :field => 'name', :wants_render => false, :escape => false} , :options=>"{method:'put'}" } %> + <%= in_place_editor 'context_name_in_place_editor', { + :url => { :controller => 'contexts', :action => 'update', :id => context.id, :field => 'name', :update_context_name => true, :escape => false}, + :options=>"{method:'put'}", :script => true } %> <% else %> <%= link_to_context( context ) %> <% end %> diff --git a/app/views/contexts/update_context_name.js.rjs b/app/views/contexts/update_context_name.js.rjs new file mode 100644 index 00000000..acee3308 --- /dev/null +++ b/app/views/contexts/update_context_name.js.rjs @@ -0,0 +1,10 @@ +page['context_name_in_place_editor'].replace_html @context.name + +page['default_context_name_id'].value = @context.name +page['todo_context_name'].value = @context.name + +# renew context auto complete array +page << "contextAutoCompleter.options.array = #{context_names_for_autocomplete}; contextAutoCompleter.changed = true" + +status_message = "Name of context was changed" +page.notify :notice, status_message, 5.0 diff --git a/app/views/projects/_project.rhtml b/app/views/projects/_project.rhtml index 3e1d6418..2006c184 100644 --- a/app/views/projects/_project.rhtml +++ b/app/views/projects/_project.rhtml @@ -6,7 +6,9 @@ <%= image_tag("collapse.png") %> <% end %> <%= project.name %> - <%= in_place_editor 'project_name_in_place_editor', { :url => { :controller => 'projects', :action => 'update', :id => project.id, :field => 'name', :wants_render => false, :escape => false} , :options=>"{method:'put'}" } %> + <%= in_place_editor 'project_name_in_place_editor', { + :url => { :controller => 'projects', :action => 'update', :id => project.id, :field => 'name', :update_project_name => true, :escape => false} , + :options=>"{method:'put'}", :script => true} %> <% unless project.description.blank? -%>
<%= sanitize(project.description) %>
diff --git a/app/views/projects/update_project_name.js.rjs b/app/views/projects/update_project_name.js.rjs new file mode 100644 index 00000000..e643738b --- /dev/null +++ b/app/views/projects/update_project_name.js.rjs @@ -0,0 +1,10 @@ +page['project_name_in_place_editor'].replace_html @project.name + +page['default_project_name_id'].value = @project.name +page['todo_project_name'].value = @project.name + +# renew project auto complete array +page << "projectAutoCompleter.options.array = #{project_names_for_autocomplete}; projectAutoCompleter.changed = true" + +status_message = "Name of project was changed" +page.notify :notice, status_message, 5.0 diff --git a/app/views/shared/_add_new_item_form.rhtml b/app/views/shared/_add_new_item_form.rhtml index 49345eb9..ee43217e 100644 --- a/app/views/shared/_add_new_item_form.rhtml +++ b/app/views/shared/_add_new_item_form.rhtml @@ -34,6 +34,7 @@ <%= text_area( "todo", "notes", "cols" => 29, "rows" => 6, "tabindex" => 2) %> + diff --git a/app/views/todos/create.js.rjs b/app/views/todos/create.js.rjs index eb796a77..404efb75 100644 --- a/app/views/todos/create.js.rjs +++ b/app/views/todos/create.js.rjs @@ -8,6 +8,7 @@ if @saved page['badge_count'].replace_html @down_count page.send :record, "Form.reset('todo-form-new-action');Form.focusFirstElement('todo-form-new-action')" page['todo_context_name'].value = @initial_context_name + page['todo_project_name'].value = @initial_project_name page << "updateContextNamesForAutoComplete(#{context_names_for_autocomplete})" if @new_context_created page << "projectAutoCompleter.options.array = #{project_names_for_autocomplete}" if @new_project_created if should_show_new_item() From 779dbe849f52e0989b6e770c01e284a78d4f7213 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Mon, 18 Aug 2008 16:32:49 +0200 Subject: [PATCH 003/102] add date of latest git commit to footer for better version reporting this will only work if you recreate your environment.rb from the tmpl --- config/environment.rb.tmpl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/environment.rb.tmpl b/config/environment.rb.tmpl index d8123647..56e80221 100644 --- a/config/environment.rb.tmpl +++ b/config/environment.rb.tmpl @@ -100,3 +100,5 @@ MOBILE_CONTENT_TYPE = 'tracks/mobile' Mime::Type.register(MOBILE_CONTENT_TYPE, :m) TRACKS_VERSION='1.7-devel' +info=`git log --pretty=format:"%ai" -1` +TRACKS_VERSION=TRACKS_VERSION+' ('+info+')' \ No newline at end of file From 8d6267f92fb2fd77c45b04e9bc3d799135c0f87f Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Mon, 18 Aug 2008 17:14:34 +0200 Subject: [PATCH 004/102] fix #754 by parsing dates using the format set in the preferences of the user Thanks Claus for reporting this --- app/controllers/recurring_todos_controller.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/controllers/recurring_todos_controller.rb b/app/controllers/recurring_todos_controller.rb index 763ed06b..60fb5a00 100644 --- a/app/controllers/recurring_todos_controller.rb +++ b/app/controllers/recurring_todos_controller.rb @@ -36,8 +36,8 @@ class RecurringTodosController < ApplicationController # the form for a new recurring todo and the edit form are on the same page. # Same goes for start_from and end_date params['recurring_todo']['recurring_period']=params['recurring_edit_todo']['recurring_period'] - params['recurring_todo']['end_date']=params['recurring_todo_edit_end_date'] - params['recurring_todo']['start_from']=params['recurring_todo_edit_start_from'] + params['recurring_todo']['end_date']=parse_date_per_user_prefs(params['recurring_todo_edit_end_date']) + params['recurring_todo']['start_from']=parse_date_per_user_prefs(params['recurring_todo_edit_start_from']) # update project if params['recurring_todo']['project_id'].blank? && !params['project_name'].nil? @@ -84,6 +84,9 @@ class RecurringTodosController < ApplicationController def create p = RecurringTodoCreateParamsHelper.new(params) + p.attributes['end_date']=parse_date_per_user_prefs(p.attributes['end_date']) + p.attributes['start_from']=parse_date_per_user_prefs(p.attributes['start_from']) + @recurring_todo = current_user.recurring_todos.build(p.selector_attributes) @recurring_todo.update_attributes(p.attributes) From 223cf93597cd7e0876015b1665bfe7e53d48e796 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Mon, 18 Aug 2008 20:26:28 +0200 Subject: [PATCH 005/102] fix #755. Restores accesskey for toggling new todo form Thanks marqpdx for finding this fix! On my windows vista laptop with ff3 alt-shift-p will still start Windows Media Player, but the other accesskeys now seem to work for me --- app/views/layouts/standard.html.erb | 2 +- app/views/shared/_add_new_item_form.rhtml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/standard.html.erb b/app/views/layouts/standard.html.erb index de66ec95..59f34550 100644 --- a/app/views/layouts/standard.html.erb +++ b/app/views/layouts/standard.html.erb @@ -49,7 +49,7 @@ window.onload=function(){
  • <%= navigation_link("Home", home_path, {:accesskey => "t", :title => "Home"} ) %>
  • <%= navigation_link( "Contexts", contexts_path, {:accesskey=>"c", :title=>"Contexts"} ) %>
  • <%= navigation_link( "Projects", projects_path, {:accesskey=>"p", :title=>"Projects"} ) %>
  • -
  • <%= navigation_link( "Tickler", tickler_path, :title => "Tickler" ) %>
  • +
  • <%= navigation_link( "Tickler", tickler_path, {:accesskey =>"k", :title => "Tickler"} ) %>
  • <%= navigation_link( "Done", done_path, {:accesskey=>"d", :title=>"Completed"} ) %>
  • <%= navigation_link( "Notes", notes_path, {:accesskey => "o", :title => "Show all notes"} ) %>
  • <%= navigation_link( "Preferences", preferences_path, {:accesskey => "u", :title => "Show my preferences"} ) %>
  • diff --git a/app/views/shared/_add_new_item_form.rhtml b/app/views/shared/_add_new_item_form.rhtml index ee43217e..65316245 100644 --- a/app/views/shared/_add_new_item_form.rhtml +++ b/app/views/shared/_add_new_item_form.rhtml @@ -8,7 +8,7 @@
    - « Hide form + « Hide form <% apply_behavior '#toggle_action_new a:click', :prevent_default => true do |page| page << "TracksForm.toggle('toggle_action_new', 'todo_new_action', 'todo-form-new-action', '« Hide form', 'Hide next action form', From 065f543a83657a555523e59cdfc1cbd44de0bd2b Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 19 Aug 2008 14:33:09 +0200 Subject: [PATCH 006/102] fix corner case where recurring todo with due in future and show_from in past did not create corresponding todo you cannot add todos with show_from in the past --- app/controllers/application.rb | 63 ++--- lib/authenticated_test_helper.rb | 224 +++++++++--------- .../recurring_todos_controller_test.rb | 2 +- test/unit/recurring_todo_test.rb | 8 +- 4 files changed, 151 insertions(+), 146 deletions(-) diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 3d26cdd0..5fe6f26a 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -9,12 +9,12 @@ require "redcloth" require 'date' require 'time' -# Commented the following line because of #744. It prevented rake db:migrate to +# Commented the following line because of #744. It prevented rake db:migrate to # run because this tag went looking for the taggings table that did not exist -# when you feshly create a new database -# Old comment: We need this in development mode, or you get 'method missing' errors +# when you feshly create a new database Old comment: We need this in development +# mode, or you get 'method missing' errors # -# Tag +# Tag class ApplicationController < ActionController::Base @@ -160,7 +160,34 @@ class ApplicationController < ActionController::Base response.content_type = 'text/html' 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}) + + # set dates + todo.recurring_todo_id = rt.id + todo.due = rt.get_due_date(date) + # make sure that show_from is not in the past + show_from_date = rt.get_show_from_date(date) + todo.show_from = show_from_date < Time.now.utc ? nil : show_from_date + + saved = todo.save + if saved + todo.tag_with(rt.tag_list, current_user) + 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 + protected def admin_login_required @@ -192,7 +219,7 @@ class ApplicationController < ActionController::Base def openid_enabled? self.class.openid_enabled? end - + private def parse_date_per_user_prefs( s ) @@ -231,29 +258,5 @@ class ApplicationController < ActionController::Base def set_time_zone Time.zone = current_user.prefs.time_zone if logged_in? 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}) - - # set dates - todo.due = rt.get_due_date(date) - todo.show_from = rt.get_show_from_date(date) - todo.recurring_todo_id = rt.id - saved = todo.save - if saved - todo.tag_with(rt.tag_list, current_user) - 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 end diff --git a/lib/authenticated_test_helper.rb b/lib/authenticated_test_helper.rb index 2f743aa0..9137cf37 100644 --- a/lib/authenticated_test_helper.rb +++ b/lib/authenticated_test_helper.rb @@ -1,113 +1,113 @@ -module AuthenticatedTestHelper - # Sets the current user in the session from the user fixtures. - def login_as(user) - @request.session['user_id'] = user ? users(user).id : nil - end - - def content_type(type) - @request.env['Content-Type'] = type - end - - def accept(accept) - @request.env["HTTP_ACCEPT"] = accept - end - - def authorize_as(user) - if user - @request.env["HTTP_AUTHORIZATION"] = "Basic #{Base64.encode64("#{users(user).login}:test")}" - accept 'application/xml' - content_type 'application/xml' - else - @request.env["HTTP_AUTHORIZATION"] = nil - accept nil - content_type nil - end - end - - # http://project.ioni.st/post/217#post-217 - # - # def test_new_publication - # assert_difference(Publication, :count) do - # post :create, :publication => {...} - # # ... - # end - # end - # - def assert_difference(object, method = nil, difference = 1) - initial_value = object.send(method) - yield - assert_equal initial_value + difference, object.send(method), "#{object}##{method}" - end - - def assert_no_difference(object, method, &block) - assert_difference object, method, 0, &block - end - - # Assert the block redirects to the login - # - # assert_requires_login(:bob) { |c| c.get :edit, :id => 1 } - # - def assert_requires_login(login = nil) - yield HttpLoginProxy.new(self, login) - end - - def assert_http_authentication_required(login = nil) - yield XmlLoginProxy.new(self, login) - end - - def reset!(*instance_vars) - instance_vars = [:controller, :request, :response] unless instance_vars.any? - instance_vars.collect! { |v| "@#{v}".to_sym } - instance_vars.each do |var| - instance_variable_set(var, instance_variable_get(var).class.new) - end - end -end - -class BaseLoginProxy - attr_reader :controller - attr_reader :options - def initialize(controller, login) - @controller = controller - @login = login - end - - private - def authenticated - raise NotImplementedError - end - - def check - raise NotImplementedError - end - - def method_missing(method, *args) - @controller.reset! - authenticate - @controller.send(method, *args) - check - end -end - -class HttpLoginProxy < BaseLoginProxy - protected - def authenticate - @controller.login_as @login if @login - end - - def check - @controller.assert_redirected_to :controller => 'account', :action => 'login' - end -end - -class XmlLoginProxy < BaseLoginProxy - protected - def authenticate - @controller.accept 'application/xml' - @controller.authorize_as @login if @login - end - - def check - @controller.assert_response 401 - end +module AuthenticatedTestHelper + # Sets the current user in the session from the user fixtures. + def login_as(user) + @request.session['user_id'] = user ? users(user).id : nil + end + + def content_type(type) + @request.env['Content-Type'] = type + end + + def accept(accept) + @request.env["HTTP_ACCEPT"] = accept + end + + def authorize_as(user) + if user + @request.env["HTTP_AUTHORIZATION"] = "Basic #{Base64.encode64("#{users(user).login}:test")}" + accept 'application/xml' + content_type 'application/xml' + else + @request.env["HTTP_AUTHORIZATION"] = nil + accept nil + content_type nil + end + end + + # http://project.ioni.st/post/217#post-217 + # + # def test_new_publication + # assert_difference(Publication, :count) do + # post :create, :publication => {...} + # # ... + # end + # end + # + def assert_difference(object, method = nil, difference = 1) + initial_value = object.send(method) + yield + assert_equal initial_value + difference, object.send(method), "#{object}##{method}" + end + + def assert_no_difference(object, method, &block) + assert_difference object, method, 0, &block + end + + # Assert the block redirects to the login + # + # assert_requires_login(:bob) { |c| c.get :edit, :id => 1 } + # + def assert_requires_login(login = nil) + yield HttpLoginProxy.new(self, login) + end + + def assert_http_authentication_required(login = nil) + yield XmlLoginProxy.new(self, login) + end + + def reset!(*instance_vars) + instance_vars = [:controller, :request, :response] unless instance_vars.any? + instance_vars.collect! { |v| "@#{v}".to_sym } + instance_vars.each do |var| + instance_variable_set(var, instance_variable_get(var).class.new) + end + end +end + +class BaseLoginProxy + attr_reader :controller + attr_reader :options + def initialize(controller, login) + @controller = controller + @login = login + end + + private + def authenticated + raise NotImplementedError + end + + def check + raise NotImplementedError + end + + def method_missing(method, *args) + @controller.reset! + authenticate + @controller.send(method, *args) + check + end +end + +class HttpLoginProxy < BaseLoginProxy + protected + def authenticate + @controller.login_as @login if @login + end + + def check + @controller.assert_redirected_to :controller => 'account', :action => 'login' + end +end + +class XmlLoginProxy < BaseLoginProxy + protected + def authenticate + @controller.accept 'application/xml' + @controller.authorize_as @login if @login + end + + def check + @controller.assert_response 401 + end end \ No newline at end of file diff --git a/test/functional/recurring_todos_controller_test.rb b/test/functional/recurring_todos_controller_test.rb index 1d0d174e..6ba70d75 100644 --- a/test/functional/recurring_todos_controller_test.rb +++ b/test/functional/recurring_todos_controller_test.rb @@ -18,5 +18,5 @@ class RecurringTodosControllerTest < ActionController::TestCase xhr :post, :destroy, :id => 1, :_source_view => 'todo' assert_rjs :page, "recurring_todo_1", :remove end - + end diff --git a/test/unit/recurring_todo_test.rb b/test/unit/recurring_todo_test.rb index 068bb35c..f3fccbbd 100644 --- a/test/unit/recurring_todo_test.rb +++ b/test/unit/recurring_todo_test.rb @@ -174,12 +174,14 @@ class RecurringTodoTest < Test::Rails::TestCase def test_yearly_pattern # beginning of same year due_date = @yearly.get_due_date(Time.utc(2008,2,10)) # feb 10th - assert_equal @sunday, due_date # june 8th + assert_equal @sunday, due_date # june 8th + # same month, previous date due_date = @yearly.get_due_date(@saturday) # june 7th show_from_date = @yearly.get_show_from_date(@saturday) # june 7th assert_equal @sunday, due_date # june 8th assert_equal @sunday-5.days, show_from_date + # same month, day after due_date = @yearly.get_due_date(@monday) # june 9th assert_equal Time.utc(2009,6,8), due_date # june 8th next year @@ -197,7 +199,7 @@ class RecurringTodoTest < Test::Rails::TestCase due_date = @yearly.get_due_date(Time.utc(2008,6,12)) # june 7th assert_equal Time.utc(2009,6,10), due_date # june 10th - # test handling of nil + # test handling of nil due_date1 = @yearly.get_due_date(nil) due_date2 = @yearly.get_due_date(Time.now.utc + 1.day) assert_equal due_date1, due_date2 @@ -207,7 +209,7 @@ class RecurringTodoTest < Test::Rails::TestCase due_date = @yearly.get_due_date(nil) assert_equal due_date.year, this_year+2 end - + def test_toggle_completion t = @yearly assert_equal :active, t.current_state From b076ae46f8146f67ab84fe0bbe3bd1c7dd153a94 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 19 Aug 2008 14:47:51 +0200 Subject: [PATCH 007/102] handle nil correctly in last patch --- app/controllers/application.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 5fe6f26a..5a9a6fa9 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -170,7 +170,11 @@ class ApplicationController < ActionController::Base todo.due = rt.get_due_date(date) # make sure that show_from is not in the past show_from_date = rt.get_show_from_date(date) - todo.show_from = show_from_date < Time.now.utc ? nil : show_from_date + if show_from_date.nil? + todo.show_from=nil + else + todo.show_from = show_from_date < Time.now.utc ? nil : show_from_date + end saved = todo.save if saved From 666f524b16c847cf8b3d241636e57087097306dc Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 19 Aug 2008 14:58:45 +0200 Subject: [PATCH 008/102] add functional tests for recurring todos includes a test for rec todos with due date in future and show_from in past --- lib/authenticated_test_helper.rb | 2 +- .../recurring_todos_controller_test.rb | 101 ++++++++++++++++++ test/functional/todos_controller_test.rb | 59 ++++++---- 3 files changed, 142 insertions(+), 20 deletions(-) diff --git a/lib/authenticated_test_helper.rb b/lib/authenticated_test_helper.rb index 9137cf37..7a52e62b 100644 --- a/lib/authenticated_test_helper.rb +++ b/lib/authenticated_test_helper.rb @@ -3,7 +3,7 @@ module AuthenticatedTestHelper def login_as(user) @request.session['user_id'] = user ? users(user).id : nil end - + def content_type(type) @request.env['Content-Type'] = type end diff --git a/test/functional/recurring_todos_controller_test.rb b/test/functional/recurring_todos_controller_test.rb index 6ba70d75..a1e40b77 100644 --- a/test/functional/recurring_todos_controller_test.rb +++ b/test/functional/recurring_todos_controller_test.rb @@ -17,6 +17,107 @@ class RecurringTodosControllerTest < ActionController::TestCase login_as(:admin_user) xhr :post, :destroy, :id => 1, :_source_view => 'todo' assert_rjs :page, "recurring_todo_1", :remove + begin + rc = RecurringTodo.find(1) + rescue + rc = nil + end + assert_nil rc + end + + def test_new_recurring_todo + login_as(:admin_user) + orig_rt_count = RecurringTodo.count + orig_todo_count = Todo.count + put :create, + "context_name"=>"library", + "project_name"=>"Build a working time machine", + "recurring_todo" => + { + "daily_every_x_days"=>"1", + "daily_selector"=>"daily_every_x_day", + "description"=>"new recurring pattern", + "end_date" => "31/08/2010", + "ends_on" => "ends_on_end_date", + "monthly_day_of_week" => "1", + "monthly_every_x_day" => "18", + "monthly_every_x_month2" => "1", + "monthly_every_x_month" => "1", + "monthly_every_xth_day"=>"1", + "monthly_selector"=>"monthly_every_x_day", + "notes"=>"with some notes", + "number_of_occurences" => "", + "recurring_period"=>"yearly", + "recurring_show_days_before"=>"10", + "recurring_target"=>"due_date", + "start_from"=>"18/08/2008", + "weekly_every_x_week"=>"1", + "weekly_return_monday"=>"m", + "yearly_day_of_week"=>"1", + "yearly_every_x_day"=>"8", + "yearly_every_xth_day"=>"1", + "yearly_month_of_year2"=>"8", + "yearly_month_of_year"=>"6", + "yearly_selector"=>"yearly_every_x_day" + }, + "tag_list"=>"one, two, three, four" + + # check new recurring todo added + assert_equal orig_rt_count+1, RecurringTodo.count + # check new todo added + assert_equal orig_todo_count+1, Todo.count + end + + def test_recurring_todo_toggle_check + # the test fixtures did add recurring_todos but not the corresponding todos, + # so we check complete and uncheck to force creation of a todo from the + # pattern + login_as(:admin_user) + + # mark as complete + xhr :post, :toggle_check, :id=>1, :_source_view=>"" + recurring_todo_1 = RecurringTodo.find(1) + assert recurring_todo_1.completed? + + todo_count = Todo.count + + # mark as active + xhr :post, :toggle_check, :id=>1, :_source_view=>"" + recurring_todo_1.reload + assert recurring_todo_1.active? + + # by making active, a new todo should be created from the pattern + assert_equal todo_count+1, Todo.count + + # find the new todo and check its description + new_todo = Todo.find_by_recurring_todo_id 1 + assert_equal "Call Bill Gates every day", new_todo.description end + def test_creating_recurring_todo_with_show_from_in_past + login_as(:admin_user) + + @yearly = RecurringTodo.find(5) # yearly on june 8th + + # change due date in four days from now and show from 10 days before, i.e. 6 + # days ago + target_date = Time.now.utc + 4.days + @yearly.every_other1 = target_date.day + @yearly.every_other2 = target_date.month + @yearly.show_from_delta = 10 + assert @yearly.save + + # toggle twice to force generation of new todo + xhr :post, :toggle_check, :id=>5, :_source_view=>"" + xhr :post, :toggle_check, :id=>5, :_source_view=>"" + + new_todo = Todo.find_by_recurring_todo_id 5 + + # due date should be the target_date + assert_equal Time.utc(target_date.year, target_date.month, target_date.day), new_todo.due + + # show_from should be nil since now+4.days-10.days is in the past + assert_equal nil, new_todo.show_from + end + end diff --git a/test/functional/todos_controller_test.rb b/test/functional/todos_controller_test.rb index 3c3127e4..b99b537c 100644 --- a/test/functional/todos_controller_test.rb +++ b/test/functional/todos_controller_test.rb @@ -70,7 +70,7 @@ class TodosControllerTest < Test::Rails::TestCase login_as(:admin_user) xhr :post, :destroy, :id => 1, :_source_view => 'todo' assert_rjs :page, "todo_1", :remove - #assert_rjs :replace_html, "badge-count", '9' + # #assert_rjs :replace_html, "badge-count", '9' end def test_create_todo @@ -90,11 +90,11 @@ class TodosControllerTest < Test::Rails::TestCase def test_fail_to_create_todo_via_xml login_as(:admin_user) - #try to create with no context, which is not valid + # #try to create with no context, which is not valid put :create, :format => "xml", "request" => { "project_name"=>"Build a working time machine", "todo"=>{"notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" } assert_response 422 assert_xml_select "errors" do - assert_xml_select "error", "Context can't be blank" + assert_xml_select "error", "Context can't be blank" end end @@ -180,7 +180,7 @@ class TodosControllerTest < Test::Rails::TestCase login_as(:admin_user) get :index, { :format => "rss" } assert_equal 'application/rss+xml', @response.content_type - #puts @response.body + # #puts @response.body assert_xml_select 'rss[version="2.0"]' do assert_select 'channel' do @@ -237,7 +237,7 @@ class TodosControllerTest < Test::Rails::TestCase login_as :admin_user get :index, { :format => "atom" } assert_equal 'application/atom+xml', @response.content_type - #puts @response.body + # #puts @response.body assert_xml_select 'feed[xmlns="http://www.w3.org/2005/Atom"]' do assert_xml_select '>title', 'Tracks Actions' @@ -273,7 +273,7 @@ class TodosControllerTest < Test::Rails::TestCase get :index, { :format => "txt" } assert_equal 'text/plain', @response.content_type assert !(/ /.match(@response.body)) - #puts @response.body + # #puts @response.body end def test_text_feed_not_accessible_to_anonymous_user_without_token @@ -299,7 +299,7 @@ class TodosControllerTest < Test::Rails::TestCase get :index, { :format => "ics" } assert_equal 'text/calendar', @response.content_type assert !(/ /.match(@response.body)) - #puts @response.body + # #puts @response.body end def test_mobile_index_uses_text_html_content_type @@ -317,10 +317,10 @@ class TodosControllerTest < Test::Rails::TestCase def test_mobile_create_action_creates_a_new_todo login_as(:admin_user) post :create, {"format"=>"m", "todo"=>{"context_id"=>"2", - "due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2", - "show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"", - "project_id"=>"1", - "notes"=>"test notes", "description"=>"test_mobile_create_action", "state"=>"0"}} + "due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2", + "show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"", + "project_id"=>"1", + "notes"=>"test notes", "description"=>"test_mobile_create_action", "state"=>"0"}} t = Todo.find_by_description("test_mobile_create_action") assert_not_nil t assert_equal 2, t.context_id @@ -334,20 +334,20 @@ class TodosControllerTest < Test::Rails::TestCase def test_mobile_create_action_redirects_to_mobile_home_page_when_successful login_as(:admin_user) post :create, {"format"=>"m", "todo"=>{"context_id"=>"2", - "due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2", - "show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"", - "project_id"=>"1", - "notes"=>"test notes", "description"=>"test_mobile_create_action", "state"=>"0"}} + "due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2", + "show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"", + "project_id"=>"1", + "notes"=>"test notes", "description"=>"test_mobile_create_action", "state"=>"0"}} assert_redirected_to '/m' end def test_mobile_create_action_renders_new_template_when_save_fails login_as(:admin_user) post :create, {"format"=>"m", "todo"=>{"context_id"=>"2", - "due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2", - "show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"", - "project_id"=>"1", - "notes"=>"test notes", "state"=>"0"}, "tag_list"=>"test, test2"} + "due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2", + "show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"", + "project_id"=>"1", + "notes"=>"test notes", "state"=>"0"}, "tag_list"=>"test, test2"} assert_template 'todos/new' end @@ -357,4 +357,25 @@ class TodosControllerTest < Test::Rails::TestCase assert_equal '"{\\"Build a working time machine\\": \\"lab\\"}"', assigns(:default_project_context_name_map) end + def test_toggle_check_on_recurring_todo + login_as(:admin_user) + + # link todo_1 and recurring_todo_1 + recurring_todo_1 = RecurringTodo.find(1) + todo_1 = Todo.find(1) + todo_1.recurring_todo_id = recurring_todo_1.id + + # update todo_1 + assert todo_1.save + + # mark todo_1 as complete by toggle_check + xhr :post, :toggle_check, :id => 1, :_source_view => 'todo' + todo_1.reload + assert todo_1.completed? + + # check there is a new todo linked to the recurring pattern + next_todo = Todo.find_by_recurring_todo_id(recurring_todo_1.id) + assert_equal "Call Bill Gates every day", next_todo.description + end + end From f4378ffde1b2364bbcbf999191a76f580fa02904 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 19 Aug 2008 21:25:25 +0200 Subject: [PATCH 009/102] fix case where future start_from was ignored for yearly recurrence patterns --- app/models/recurring_todo.rb | 8 +++++++- test/unit/recurring_todo_test.rb | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/models/recurring_todo.rb b/app/models/recurring_todo.rb index ab9fa3f1..ba40527e 100644 --- a/app/models/recurring_todo.rb +++ b/app/models/recurring_todo.rb @@ -508,7 +508,13 @@ class RecurringTodo < ActiveRecord::Base if previous.nil? start = self.start_from.nil? ? Time.now.utc : self.start_from else - start = previous + if self.start_from.nil? + start = previous + else + # check if the start_from date is later than previous. If so, use + # start_from as start to search for next date + start = self.start_from > previous ? self.start_from : previous + end end day = self.every_other1 diff --git a/test/unit/recurring_todo_test.rb b/test/unit/recurring_todo_test.rb index f3fccbbd..4ef86ddd 100644 --- a/test/unit/recurring_todo_test.rb +++ b/test/unit/recurring_todo_test.rb @@ -203,6 +203,13 @@ class RecurringTodoTest < Test::Rails::TestCase due_date1 = @yearly.get_due_date(nil) due_date2 = @yearly.get_due_date(Time.now.utc + 1.day) assert_equal due_date1, due_date2 + end + + def test_start_from_in_future + # start from after june 8th 2008 + @yearly.start_from = Time.utc(2008,6,12) + assert_equal Time.utc(2009,6,8), @yearly.get_due_date(nil) # jun 8th next year + assert_equal Time.utc(2009,6,8), @yearly.get_due_date(Time.utc(2008,6,1)) # also next year this_year = Time.now.utc.year @yearly.start_from = Time.utc(this_year+1,6,12) From 88ea02d29a44c51031e7e45eb4b4d3729646b5d5 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 19 Aug 2008 22:04:53 +0200 Subject: [PATCH 010/102] fix case with future start_from for other recurrence periods too this patch belongs to previous --- app/models/recurring_todo.rb | 49 +++++++++++++++++++++----------- test/unit/recurring_todo_test.rb | 46 +++++++++++++++++++----------- 2 files changed, 62 insertions(+), 33 deletions(-) diff --git a/app/models/recurring_todo.rb b/app/models/recurring_todo.rb index ba40527e..e39a15e3 100644 --- a/app/models/recurring_todo.rb +++ b/app/models/recurring_todo.rb @@ -394,11 +394,19 @@ class RecurringTodo < ActiveRecord::Base # the due date in time) # # assumes self.recurring_period == 'daily' + + # determine start if previous.nil? start = self.start_from.nil? ? Time.now.utc : self.start_from else # use the next day start = previous + 1.day + + unless self.start_from.nil? + # check if the start_from date is later than previous. If so, use + # start_from as start to search for next date + start = self.start_from if self.start_from > previous + end end if self.only_work_days @@ -419,6 +427,7 @@ class RecurringTodo < ActiveRecord::Base end def get_weekly_date(previous) + # determine start if previous == nil start = self.start_from.nil? ? Time.now.utc : self.start_from else @@ -428,7 +437,13 @@ class RecurringTodo < ActiveRecord::Base # that week start += self.every_other1.week end + unless self.start_from.nil? + # check if the start_from date is later than previous. If so, use + # start_from as start to search for next date + start = self.start_from if self.start_from > previous + end end + # check if there are any days left this week for the next todo start.wday().upto 6 do |i| return start + (i-start.wday()).days unless self.every_day[i,1] == ' ' @@ -447,11 +462,8 @@ class RecurringTodo < ActiveRecord::Base end def get_monthly_date(previous) - if previous.nil? - start = self.start_from.nil? ? Time.now.utc : self.start_from - else - start = previous - end + + start = determine_start(previous) day = self.every_other1 n = self.every_other2 @@ -505,18 +517,8 @@ class RecurringTodo < ActiveRecord::Base end def get_yearly_date(previous) - if previous.nil? - start = self.start_from.nil? ? Time.now.utc : self.start_from - else - if self.start_from.nil? - start = previous - else - # check if the start_from date is later than previous. If so, use - # start_from as start to search for next date - start = self.start_from > previous ? self.start_from : previous - end - end + start = determine_start(previous) day = self.every_other1 month = self.every_other2 @@ -598,4 +600,19 @@ class RecurringTodo < ActiveRecord::Base errors.add("", "At least one day must be selected in the weekly pattern") if self.every_day == ' ' end + def determine_start(previous) + if previous.nil? + start = self.start_from.nil? ? Time.now.utc : self.start_from + else + start = previous + + unless self.start_from.nil? + # check if the start_from date is later than previous. If so, use + # start_from as start to search for next date + start = self.start_from if self.start_from > previous + end + end + return start + end + end diff --git a/test/unit/recurring_todo_test.rb b/test/unit/recurring_todo_test.rb index 4ef86ddd..b3982d4e 100644 --- a/test/unit/recurring_todo_test.rb +++ b/test/unit/recurring_todo_test.rb @@ -34,23 +34,13 @@ class RecurringTodoTest < Test::Rails::TestCase def test_daily_every_day # every_day should return todays date if there was no previous date due_date = @every_day.get_due_date(nil) - # use to_s in compare, because milisec could be different - assert_equal @today.to_s, due_date.to_s + # use strftime in compare, because milisec / secs could be different + assert_equal @today.strftime("%d-%m-%y"), due_date.strftime("%d-%m-%y") # when the last todo was completed today, the next todo is due tomorrow due_date =@every_day.get_due_date(@today) assert_equal @tomorrow, due_date - - # every_day should return start_day if it is in the future - @every_day.start_from = @in_three_days - due_date = @every_day.get_due_date(nil) - assert_equal @in_three_days, due_date - - # if we give a date in the future for the previous todo, the next to do - # should be based on that future date. - due_date = @every_day.get_due_date(@in_four_days) - assert_equal @in_four_days+1.day, due_date - + # do something every 14 days @every_day.every_other1=14 due_date = @every_day.get_due_date(@today) @@ -168,7 +158,6 @@ class RecurringTodoTest < Test::Rails::TestCase due_date = @monthly.get_due_date(@sunday) # june 8th assert_equal Time.utc(2008,8,8), due_date # aug 8th - end def test_yearly_pattern @@ -206,10 +195,33 @@ class RecurringTodoTest < Test::Rails::TestCase end def test_start_from_in_future + # every_day should return start_day if it is in the future + @every_day.start_from = @in_three_days + due_date = @every_day.get_due_date(nil) + assert_equal @in_three_days, due_date + due_date = @every_day.get_due_date(@tomorrow) + assert_equal @in_three_days, due_date + + # if we give a date in the future for the previous todo, the next to do + # should be based on that future date. + due_date = @every_day.get_due_date(@in_four_days) + assert_equal @in_four_days+1.day, due_date + + @weekly_every_day.start_from = Time.utc(2020,1,1) + assert_equal Time.utc(2020,1,1), @weekly_every_day.get_due_date(nil) + assert_equal Time.utc(2020,1,1), @weekly_every_day.get_due_date(Time.utc(2019,10,1)) + assert_equal Time.utc(2020,1,10), @weekly_every_day.get_due_date(Time.utc(2020,1,9)) + + @monthly_every_last_friday.start_from = Time.utc(2020,1,1) + assert_equal Time.utc(2020,1,31), @monthly_every_last_friday.get_due_date(nil) # last friday of jan + assert_equal Time.utc(2020,1,31), @monthly_every_last_friday.get_due_date(Time.utc(2019,12,1)) # last friday of jan + assert_equal Time.utc(2020,2,28), @monthly_every_last_friday.get_due_date(Time.utc(2020,2,1)) # last friday of feb + # start from after june 8th 2008 - @yearly.start_from = Time.utc(2008,6,12) - assert_equal Time.utc(2009,6,8), @yearly.get_due_date(nil) # jun 8th next year - assert_equal Time.utc(2009,6,8), @yearly.get_due_date(Time.utc(2008,6,1)) # also next year + @yearly.start_from = Time.utc(2020,6,12) + assert_equal Time.utc(2021,6,8), @yearly.get_due_date(nil) # jun 8th next year + assert_equal Time.utc(2021,6,8), @yearly.get_due_date(Time.utc(2019,6,1)) # also next year + assert_equal Time.utc(2021,6,8), @yearly.get_due_date(Time.utc(2020,6,15)) # also next year this_year = Time.now.utc.year @yearly.start_from = Time.utc(this_year+1,6,12) From 6e8ea419f97151337dbf986143537f11b0d701d5 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 19 Aug 2008 22:24:00 +0200 Subject: [PATCH 011/102] add accesskey to submit button in mobile view in mobile view when you edit an action, pressing # will submit changes --- app/views/todos/show.m.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/todos/show.m.erb b/app/views/todos/show.m.erb index c09ac503..004b94f6 100644 --- a/app/views/todos/show.m.erb +++ b/app/views/todos/show.m.erb @@ -1,5 +1,5 @@ <% form_tag formatted_todo_path(@todo, :m), :method => :put do %> <%= render :partial => 'edit_mobile', :locals => { :parent_container_type => "show_mobile" } %> -

    +

    <% end -%> <%= link_to "Cancel", @return_path %> From c113a0f800a803abca73700848e97d157125f45e Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Mon, 25 Aug 2008 10:01:15 +0200 Subject: [PATCH 012/102] update sqlite sample databases to latest migration also clean the sessions and vacuum the databases --- db/tracks-15-blank.db | Bin 38912 -> 48128 bytes db/tracks-15-example.db | Bin 44032 -> 50176 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/db/tracks-15-blank.db b/db/tracks-15-blank.db index b35978021bbd58cb1614046726aae5dcbafb5f61..aff96bb2c4a72643a8384ebf4096084840c5bf63 100644 GIT binary patch delta 2834 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 XE4aWcOC3Oep$R`JXv)78y3GFqxFNUu diff --git a/db/tracks-15-example.db b/db/tracks-15-example.db index b9b4473bc7a345e7844a1f59ba3042d268c4a7ae..97f586c7d86f5463fed14ee564bc0456b4e8d3de 100644 GIT binary patch delta 3000 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 Date: Mon, 25 Aug 2008 10:20:05 +0200 Subject: [PATCH 013/102] fix small error where Time was compared to Date --- app/controllers/application.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 5a9a6fa9..55c89a60 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -173,7 +173,7 @@ class ApplicationController < ActionController::Base if show_from_date.nil? todo.show_from=nil else - todo.show_from = show_from_date < Time.now.utc ? nil : show_from_date + todo.show_from = show_from_date.to_time < Time.now.utc ? nil : show_from_date end saved = todo.save From c2624840e7f1a81d22043686379de4cba27452ba Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Mon, 25 Aug 2008 15:38:50 +0200 Subject: [PATCH 014/102] update Tracks RESTapi documentation page when using xml as content type, you should send xml --- app/views/integrations/rest_api.html.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/integrations/rest_api.html.erb b/app/views/integrations/rest_api.html.erb index f401328f..4ab9a2b7 100644 --- a/app/views/integrations/rest_api.html.erb +++ b/app/views/integrations/rest_api.html.erb @@ -73,7 +73,7 @@
     
     $ curl -u username:p4ssw0rd -H "Content-Type: text/xml" \
    -    -d "project[name]=Build a treehouse for the kids" \
    +    -d "<project><name>Build a treehouse for the kids</name></project>" \
         <%= home_url %>projects.xml -i
     >> HTTP/1.1 201 Created
     Location: <%= home_url %>projects/65.xml
    @@ -86,7 +86,7 @@ Location: <%= home_url %>projects/65.xml
     
     
     $ curl -u username:p4ssw0rd -H "Content-Type: text/xml" \
    -    -d "todo[description]=Model treehouse in SketchUp&todo[context_id]=2&todo[project_id]=65" \
    +    -d "<todo><description>Model treehouse in SketchUp</description><context_id>2</context_id><project_id>65<project_id>" \
         <%= home_url %>todos.xml -i
     >> HTTP/1.1 201 Created
     Location: <%= home_url %>todos/452.xml
    @@ -99,7 +99,7 @@ Location: <%= home_url %>todos/452.xml
     
     
       $ curl -u username:p4ssw0rd -H "Content-Type: text/xml" -X PUT \
    -      -d "todo[notes]=use maple texture" \
    +      -d "<todo><notes>use maple texture</notes></todos>" \
           <%= home_url %>todos/452.xml -i
       >> HTTP/1.1 200 OK
       ...
    
    From fceb5a75034c7be0b5f3655f189846ff4b27d0f9 Mon Sep 17 00:00:00 2001
    From: Reinier Balt 
    Date: Mon, 25 Aug 2008 15:43:05 +0200
    Subject: [PATCH 015/102] also add # as access key to mobile form for new todo
    
    forgot this when I added access key to mobile edit form
    ---
     app/views/todos/new.m.erb | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/app/views/todos/new.m.erb b/app/views/todos/new.m.erb
    index 17144323..2dfe0fd7 100644
    --- a/app/views/todos/new.m.erb
    +++ b/app/views/todos/new.m.erb
    @@ -1,5 +1,5 @@
     <% form_tag formatted_todos_path(:m), :method => :post do %>
       <%= render :partial => 'edit_mobile' %>
    -  

    +

    <% end -%> <%= link_to "Back", @return_path %> \ No newline at end of file From 73765c184dbf33a92ad88f33d71aa74e95820bb3 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Mon, 25 Aug 2008 16:03:24 +0200 Subject: [PATCH 016/102] fix for #628. Up/down key arrows should work better in autocomplete required a change in the control.js from rails / scipt.aculos.us so we need to check for regression on upgrade found fix here: http://dev.rubyonrails.org/ticket/8148 --- public/javascripts/controls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/javascripts/controls.js b/public/javascripts/controls.js index 5aaf0bb2..28808419 100644 --- a/public/javascripts/controls.js +++ b/public/javascripts/controls.js @@ -211,7 +211,7 @@ Autocompleter.Base = Class.create({ markPrevious: function() { if(this.index > 0) this.index-- else this.index = this.entryCount-1; - this.getEntry(this.index).scrollIntoView(true); + this.getEntry(this.index).scrollIntoView(false); }, markNext: function() { From 955b751791a3523f5f19a41603e0d182a11f9554 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Mon, 25 Aug 2008 16:21:39 +0200 Subject: [PATCH 017/102] update template to remove warning about already initialized constant as reported in #758 Thanks Claus --- config/environment.rb.tmpl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/config/environment.rb.tmpl b/config/environment.rb.tmpl index 56e80221..8ab4ade4 100644 --- a/config/environment.rb.tmpl +++ b/config/environment.rb.tmpl @@ -99,6 +99,11 @@ TRACKS_COOKIES_SECURE = false MOBILE_CONTENT_TYPE = 'tracks/mobile' Mime::Type.register(MOBILE_CONTENT_TYPE, :m) -TRACKS_VERSION='1.7-devel' -info=`git log --pretty=format:"%ai" -1` -TRACKS_VERSION=TRACKS_VERSION+' ('+info+')' \ No newline at end of file +tracks_version='1.7-devel' + +# comment out next two lines if you do not want (or can not) the date of the +# last git commit in the footer +info=`git log --pretty=format:"%ai" -1`+ +tracks_version=tracks_version + ' ('+info+')' + +TRACKS_VERSION=tracks_version \ No newline at end of file From 0e70d27093a25b3509f248410dddba61ec459053 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 25 Aug 2008 16:43:43 +0200 Subject: [PATCH 018/102] fix page to handle empty database without errors. Fixes #757 Thanks lburton for the report --- app/views/feedlist/index.html.erb | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/views/feedlist/index.html.erb b/app/views/feedlist/index.html.erb index 8beace82..7acc405c 100644 --- a/app/views/feedlist/index.html.erb +++ b/app/views/feedlist/index.html.erb @@ -64,10 +64,13 @@ Active projects with their actions
  • Feeds for incomplete actions in a specific context:

    + <% if @active_contexts.empty? && @hidden_contexts.empty? -%> +
    • There need to be at least one context before you can request a feed
    + <% else -%>
    • Step 1 - Choose the context you want a feed of: <%= observe_field "feed-contexts", :update => "feeds-for-context", @@ -80,17 +83,21 @@
    • Step 2 - Select the feed for this context
      - <%= render :partial => 'feed_for_context', :locals => { :context => @active_contexts.first } %> + <%= render :partial => 'feed_for_context', :locals => { :context => @active_contexts.empty? ? @hidden_contexts.first : @active_contexts.first } %>
    + <% end -%>
  • Feeds for incomplete actions in a specific project:

    + <% if @active_projects.empty? && @hidden_projects.empty? -%> +
    • There need to be at least one project before you can request a feed
    + <% else -%>
    • Step 1 - Choose the project you want a feed of: @@ -104,11 +111,12 @@
    • Step 2 - Select the feed for this project
      - <%= render :partial => 'feed_for_project', :locals => { :project => @active_projects.first } %> + <%= render :partial => 'feed_for_project', :locals => { :project => @active_projects.empty? ? @hidden_projects.first : @active_projects.first } %>
    + <% end -%>
  • From 4ac09ed3ae391672f4077c0991b0bb8fc6424756 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Mon, 25 Aug 2008 17:20:27 +0200 Subject: [PATCH 019/102] hopefully fix time that a todo comes from tickler assumes the datetime in db is in utc --- app/models/user.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 8afb8e41..b49db26c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -71,7 +71,8 @@ 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 <= ?', proxy_owner.time ]).collect { |t| t.activate! } + # assumes that active record uses :utc to store datetime in db + find(:all, :conditions => ['show_from <= ?', Time.now.utc ]).collect { |t| t.activate! } end end has_many :completed_todos, From 4d11e156f4228dcd405c7c87ffd52b07b3cc3be2 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 26 Aug 2008 16:07:35 +0200 Subject: [PATCH 020/102] fix moving todos from one context to another on tag view also fix ordening on the collections on the tag view page --- app/controllers/todos_controller.rb | 33 +++++++++++++++++------------ app/views/todos/_edit_form.rhtml | 5 +++-- app/views/todos/update.js.rjs | 3 ++- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 3dddc2bd..0ce52b51 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -106,6 +106,7 @@ class TodosController < ApplicationController @projects = current_user.projects.find(:all) @contexts = current_user.contexts.find(:all) @source_view = params['_source_view'] || 'todo' + @tag_name = params['_tag_name'] respond_to do |format| format.js end @@ -212,7 +213,7 @@ class TodosController < ApplicationController @todo.complete! end # strange. if checkbox is not checked, there is no 'done' in params. - # Therfore I've used the negation + # Therefore I've used the negation if !(params['done'] == '1') && @todo.completed? @todo.activate! end @@ -336,6 +337,7 @@ class TodosController < ApplicationController # /todos/tag/[tag_name] shows all the actions tagged with tag_name def tag + @page_title = "TRACKS::Tagged with \'#{@tag_name}\'" @source_view = params['_source_view'] || 'tag' @tag_name = params[:name] @@ -344,28 +346,33 @@ class TodosController < ApplicationController @tag = Tag.find_by_name(@tag_name) @tag = Tag.new(:name => @tag_name) if @tag.nil? - tag_collection = @tag.todos - @not_done_todos = tag_collection.find(:all, :conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'active']) + + @not_done_todos = tag_collection.find(:all, + :conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'active'], + :order => 'todos.completed_at DESC, todos.created_at DESC') @hidden_todos = current_user.todos.find(:all, :include => [:taggings, :tags, :context], - :conditions => ['tags.name = ? AND (todos.state = ? OR (contexts.hide = ? AND todos.state = ?))', @tag_name, 'project_hidden', true, 'active']) + :conditions => ['tags.name = ? AND (todos.state = ? OR (contexts.hide = ? AND todos.state = ?))', @tag_name, 'project_hidden', true, 'active'], + :order => 'todos.completed_at DESC, todos.created_at DESC') + @deferred = tag_collection.find(:all, + :conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'deferred'], + :order => 'show_from ASC, todos.created_at DESC') + # If you've set no_completed to zero, the completed items box isn't shown on + # the tag page + max_completed = current_user.prefs.show_number_completed + @done = tag_collection.find(:all, + :limit => max_completed, + :conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'completed'], + :order => 'todos.completed_at DESC') @contexts = current_user.contexts.find(:all) @contexts_to_show = @contexts.reject {|x| x.hide? } - @deferred = tag_collection.find(:all, :conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'deferred']) - - @page_title = "TRACKS::Tagged with \'#{@tag_name}\'" - # 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 = tag_collection.find(:all, :limit => max_completed, :conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'completed']) # Set count badge to number of items with this tag @not_done_todos.empty? ? @count = 0 : @count = @not_done_todos.size @down_count = @count - # @default_project_context_name_map = - # build_default_project_context_name_map(@projects).to_json + respond_to do |format| format.html { @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json diff --git a/app/views/todos/_edit_form.rhtml b/app/views/todos/_edit_form.rhtml index 1807504f..b504f695 100644 --- a/app/views/todos/_edit_form.rhtml +++ b/app/views/todos/_edit_form.rhtml @@ -1,7 +1,8 @@
    <%= error_messages_for("todo", :object_name => 'action') %>
    -<%= hidden_field( "todo", "id" ) %> -<%= source_view_tag( @source_view ) %> +<%= hidden_field( "todo", "id" ) -%> +<%= source_view_tag( @source_view ) -%> +<%= "" unless @tag_name.nil? -%> <%= text_field( "todo", "description", "size" => 30, "tabindex" => 8) %> diff --git a/app/views/todos/update.js.rjs b/app/views/todos/update.js.rjs index 6cfc48d2..582460ff 100644 --- a/app/views/todos/update.js.rjs +++ b/app/views/todos/update.js.rjs @@ -6,9 +6,10 @@ if @saved status_message = 'Added new context / ' + status_message if @new_context_created page.notify :notice, status_message, 5.0 - # #update auto completer arrays for context and project + # update auto completer arrays for context and project page << "contextAutoCompleter.options.array = #{context_names_for_autocomplete}; contextAutoCompleter.changed = true" if @new_context_created page << "projectAutoCompleter.options.array = #{project_names_for_autocomplete}; projectAutoCompleter.changed = true" if @new_project_created + if source_view_is_one_of(:todo, :context, :tag) if @context_changed || @todo.deferred? page[@todo].remove From 11f9114ad599818ddfb233ed9da6908e3633166e Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 26 Aug 2008 17:31:34 +0200 Subject: [PATCH 021/102] polish the clear date buttons in edit form * placed them besides the text field * replaces red x with grey x. the red x draws to much attention. also adds tool tip to x --- app/views/todos/_edit_form.rhtml | 8 ++++---- .../{tracks_1216850288.js => tracks_1219672692.js} | 2 +- public/stylesheets/standard.css | 13 ++++++++----- ...{tracks_1217189789.css => tracks_1219763287.css} | 3 ++- 4 files changed, 15 insertions(+), 11 deletions(-) rename public/javascripts/{tracks_1216850288.js => tracks_1219672692.js} (99%) rename public/stylesheets/{tracks_1217189789.css => tracks_1219763287.css} (99%) diff --git a/app/views/todos/_edit_form.rhtml b/app/views/todos/_edit_form.rhtml index b504f695..fc7b5c8d 100644 --- a/app/views/todos/_edit_form.rhtml +++ b/app/views/todos/_edit_form.rhtml @@ -49,16 +49,16 @@ Event.observe($('<%= dom_id(@todo, 'context_name') %>'), "click", <%= dom_id(@to
    <%= date_field_tag("todo[due]", dom_id(@todo, 'due'), format_date(@todo.due), "tabindex" => 13) %> - - <%= image_tag("cancel.png", :alt => "") %> + + <%= image_tag("delete_off.png", :alt => "Clear due date") %>
    <%= date_field_tag("todo[show_from]", dom_id(@todo, 'show_from'), format_date(@todo.show_from), "tabindex" => 14) %> - - <%= image_tag("cancel.png", :alt => "") %> + + <%= image_tag("delete_off.png", :alt => "Clear show from date") %>
    diff --git a/public/javascripts/tracks_1216850288.js b/public/javascripts/tracks_1219672692.js similarity index 99% rename from public/javascripts/tracks_1216850288.js rename to public/javascripts/tracks_1219672692.js index 33f474d8..2c6f9eb4 100644 --- a/public/javascripts/tracks_1216850288.js +++ b/public/javascripts/tracks_1219672692.js @@ -407,7 +407,7 @@ if(event.keyCode==Event.KEY_TAB||event.keyCode==Event.KEY_RETURN||(Prototype.Bro {this.index=element.autocompleteIndex;this.render();} Event.stop(event);},onClick:function(event){var element=Event.findElement(event,'LI');this.index=element.autocompleteIndex;this.selectEntry();this.hide();},onBlur:function(event){setTimeout(this.hide.bind(this),250);this.hasFocus=false;this.active=false;},render:function(){if(this.entryCount>0){for(var i=0;i0)this.index-- -else this.index=this.entryCount-1;this.getEntry(this.index).scrollIntoView(true);},markNext:function(){if(this.index0)value=Element.collectTextNodes(nodes[0],this.options.select);}else value=Element.collectTextNodesIgnoreClass(selectedElement,'informal');var bounds=this.getTokenBounds();if(bounds[0]!=-1){var newValue=this.element.value.substr(0,bounds[0]);var whitespace=this.element.value.substr(bounds[0]).match(/^\s+/);if(whitespace) diff --git a/public/stylesheets/standard.css b/public/stylesheets/standard.css index 617f9ff9..0a5b9516 100644 --- a/public/stylesheets/standard.css +++ b/public/stylesheets/standard.css @@ -808,6 +808,14 @@ input#go_to_project, input#context_hide { width:100%; } +.edit_todo_form .Date { + width:89%; +} + +.edit_todo_form a.date_clear:hover { + background: #CCCCCC; +} + .edit_todo_form .tag_list_label { clear:both; } @@ -1192,9 +1200,4 @@ body.integrations textarea { padding:3px; width:80%; background-color:#ddd; -} - -.date_clear -{ - float: right; } \ No newline at end of file diff --git a/public/stylesheets/tracks_1217189789.css b/public/stylesheets/tracks_1219763287.css similarity index 99% rename from public/stylesheets/tracks_1217189789.css rename to public/stylesheets/tracks_1219763287.css index 0ea5a631..c2d06198 100644 --- a/public/stylesheets/tracks_1217189789.css +++ b/public/stylesheets/tracks_1219763287.css @@ -149,6 +149,8 @@ input#go_to_project, input#context_hide {width: 5%} #todo-form-new-action .submit_box, #project_form .submit_box, #context_form .submit_box {height: 25px; padding: 5px 0; text-align: center; clear: right} .edit_todo_form .submit_box {height: 25px; padding: 5px 0; text-align: center; clear: right} .edit_todo_form input, .edit_todo_form textarea {width:100%} +.edit_todo_form .Date {width:89%} +.edit_todo_form a.date_clear:hover {background: #CCCCCC} .edit_todo_form .tag_list_label {clear:both} .edit_todo_form .due_input, .edit_todo_form .show_from_input, .edit_todo_form .project_input, .edit_todo_form .context_input {width:48%} .edit_todo_form .show_from_input, .edit_todo_form .context_input {float: right} @@ -218,7 +220,6 @@ body.integrations h2 {margin-top:40px; padding-top:20px; margin-bottom:10px; bor body.integrations p, body.integrations li {font-size:1.0em} body.integrations li {list-style-type: disc; list-style-position: inside; margin-left:30px} body.integrations textarea {margin:10px; padding:3px; width:80%; background-color:#ddd} -.date_clear {float: right} div.calendar {position: relative} .calendar, .calendar table {border: 1px solid #556; font-size: 11px; color: #000; cursor: default; background: #eef; z-index: 110; font-family: tahoma,verdana,sans-serif} .calendar .button {text-align: center; padding: 2px} From 9d09a4d47a90dd475751117f084d4ab5ccae1327 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Wed, 27 Aug 2008 20:54:56 +0200 Subject: [PATCH 022/102] fix #763 where all tags were shown twice thanks Marcin for the report --- app/controllers/projects_controller.rb | 2 +- app/views/projects/_project.rhtml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 4cd565d2..bf84e44d 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -43,7 +43,7 @@ class ProjectsController < ApplicationController init_data_for_sidebar unless mobile? @projects = current_user.projects @page_title = "TRACKS::Project: #{@project.name}" - @project.todos.send :with_scope, :find => { :include => [:context, :tags] } do + @project.todos.send :with_scope, :find => { :include => [:context] } do @not_done = @project.not_done_todos(:include_project_hidden_todos => true) @deferred = @project.deferred_todos.sort_by { |todo| todo.show_from } @done = @project.done_todos diff --git a/app/views/projects/_project.rhtml b/app/views/projects/_project.rhtml index 2006c184..de460309 100644 --- a/app/views/projects/_project.rhtml +++ b/app/views/projects/_project.rhtml @@ -1,4 +1,4 @@ -<% @not_done = project.not_done_todos -%> +<% #@not_done = project.not_done_todos -%>

    From abdac8855d5d17eaa63f877cca53fc6df83f5e35 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Thu, 28 Aug 2008 10:25:58 +0200 Subject: [PATCH 023/102] restore page title in tag view --- app/controllers/todos_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 0ce52b51..54a83116 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -337,9 +337,9 @@ class TodosController < ApplicationController # /todos/tag/[tag_name] shows all the actions tagged with tag_name def tag - @page_title = "TRACKS::Tagged with \'#{@tag_name}\'" @source_view = params['_source_view'] || 'tag' @tag_name = params[:name] + @page_title = "TRACKS::Tagged with \'#{@tag_name}\'" # mobile tags are routed with :name ending on .m. So we need to chomp it @tag_name = @tag_name.chomp('.m') if mobile? From 18815769bb78ebf80b6e1996b2c68abadd5d8e4f Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Fri, 29 Aug 2008 10:05:14 +0200 Subject: [PATCH 024/102] fix corner case for repeating todos without due date --- app/controllers/todos_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 54a83116..ac0c251a 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -689,6 +689,7 @@ class TodosController < ApplicationController if @todo.from_recurring_todo? @recurring_todo = current_user.recurring_todos.find(@todo.recurring_todo_id) date_to_check = @todo.due.nil? ? @todo.show_from : @todo.due + date_to_check = Date.today()-1.day if date_to_check.nil? if @recurring_todo.active? && @recurring_todo.has_next_todo(date_to_check) date = date_to_check >= Date.today() ? date_to_check : Date.today()-1.day @new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo, date) From ce3bed564a71efa27323ab020d45db551f6f6621 Mon Sep 17 00:00:00 2001 From: Hans de Graaff Date: Sat, 30 Aug 2008 11:12:48 +0200 Subject: [PATCH 025/102] Remove spurious + at end of line. --- config/environment.rb.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/environment.rb.tmpl b/config/environment.rb.tmpl index 8ab4ade4..77deeb30 100644 --- a/config/environment.rb.tmpl +++ b/config/environment.rb.tmpl @@ -103,7 +103,7 @@ tracks_version='1.7-devel' # comment out next two lines if you do not want (or can not) the date of the # last git commit in the footer -info=`git log --pretty=format:"%ai" -1`+ +info=`git log --pretty=format:"%ai" -1` tracks_version=tracks_version + ' ('+info+')' -TRACKS_VERSION=tracks_version \ No newline at end of file +TRACKS_VERSION=tracks_version From 88159d3f6ad4b0d0aa4d6b69c94b75d645cd2ecc Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Fri, 5 Sep 2008 17:41:30 +0200 Subject: [PATCH 026/102] add recurring todos to export to yml and xml --- app/controllers/data_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/data_controller.rb b/app/controllers/data_controller.rb index 47a85fd6..884c1496 100644 --- a/app/controllers/data_controller.rb +++ b/app/controllers/data_controller.rb @@ -24,6 +24,7 @@ class DataController < ApplicationController all_tables['tags'] = current_user.tags.find(:all) all_tables['taggings'] = current_user.taggings.find(:all) all_tables['notes'] = current_user.notes.find(:all) + all_tables['recurring_todos'] = current_user.recurring_todos.find(:all) result = all_tables.to_yaml result.gsub!(/\n/, "\r\n") # TODO: general functionality for line endings @@ -81,6 +82,7 @@ class DataController < ApplicationController result << current_user.tags.find(:all).to_xml(:skip_instruct => true) result << current_user.taggings.find(:all).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) send_data(result, :filename => "tracks_backup.xml", :type => 'text/xml') end From 3a56d2bdd2ad86718f263bc5d1263757d844bbaa Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Fri, 5 Sep 2008 17:53:18 +0200 Subject: [PATCH 027/102] clean up export controller a bit. also some cleanups in the view --- app/controllers/data_controller.rb | 69 ++++++++++++----------- app/views/data/index.html.erb | 89 ++++++++++++++++-------------- app/views/data/yaml_form.html.erb | 34 +++++++----- 3 files changed, 101 insertions(+), 91 deletions(-) diff --git a/app/controllers/data_controller.rb b/app/controllers/data_controller.rb index 884c1496..ba997607 100644 --- a/app/controllers/data_controller.rb +++ b/app/controllers/data_controller.rb @@ -3,7 +3,7 @@ class DataController < ApplicationController require 'csv' def index - @page_title = "TRACKS::Export" + @page_title = "TRACKS::Export" end def import @@ -35,21 +35,21 @@ class DataController < ApplicationController content_type = 'text/csv' CSV::Writer.generate(result = "") do |csv| csv << ["id", "Context", "Project", "Description", "Notes", "Tags", - "Created at", "Due", "Completed at", "User ID", "Show from", - "state"] + "Created at", "Due", "Completed at", "User ID", "Show from", + "state"] current_user.todos.find(:all, :include => [:context, :project]).each do |todo| - # Format dates in ISO format for easy sorting in spreadsheet - # Print context and project names for easy viewing + # Format dates in ISO format for easy sorting in spreadsheet Print + # context and project names for easy viewing csv << [todo.id, todo.context.name, - todo.project_id = todo.project_id.nil? ? "" : todo.project.name, - todo.description, - todo.notes, todo.tags.collect{|t| t.name}.join(', '), - todo.created_at.to_formatted_s(:db), - todo.due = todo.due? ? todo.due.to_formatted_s(:db) : "", - todo.completed_at = todo.completed_at? ? todo.completed_at.to_formatted_s(:db) : "", - todo.user_id, - todo.show_from = todo.show_from? ? todo.show_from.to_formatted_s(:db) : "", - todo.state] + todo.project_id = todo.project_id.nil? ? "" : todo.project.name, + todo.description, + todo.notes, todo.tags.collect{|t| t.name}.join(', '), + todo.created_at.to_formatted_s(:db), + todo.due = todo.due? ? todo.due.to_formatted_s(:db) : "", + todo.completed_at = todo.completed_at? ? todo.completed_at.to_formatted_s(:db) : "", + todo.user_id, + todo.show_from = todo.show_from? ? todo.show_from.to_formatted_s(:db) : "", + todo.state] end end send_data(result, :filename => "todos.csv", :type => content_type) @@ -59,16 +59,17 @@ class DataController < ApplicationController content_type = 'text/csv' CSV::Writer.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 + "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| - # Format dates in ISO format for easy sorting in spreadsheet - # Print context and project names for easy viewing + # Format dates in ISO format for easy sorting in spreadsheet Print + # context and project names for easy viewing 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.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)] end end send_data(result, :filename => "notes.csv", :type => content_type) @@ -104,7 +105,6 @@ class DataController < ApplicationController @inarray = YAML::load(params['import']['yaml']) # arrays to handle id translations - # contexts translate_context = Hash.new translate_context[nil] = nil @@ -153,18 +153,18 @@ class DataController < ApplicationController # state + dates case item.ivars['attributes']['state'] - when 'active' : newitem.activate! - when 'project_hidden' : newitem.hide! - when 'completed' - newitem.complete! - newitem.completed_at = adjust_time(item.ivars['attributes']['completed_at']) - when 'deferred' : newitem.defer! + when 'active' then newitem.activate! + when 'project_hidden' then newitem.hide! + when 'completed' + newitem.complete! + newitem.completed_at = adjust_time(item.ivars['attributes']['completed_at']) + when 'deferred' then newitem.defer! end newitem.created_at = adjust_time(item.ivars['attributes']['created_at']) newitem.save(false) } - #tags + # tags translate_tag = Hash.new translate_tag[nil] = nil current_user.tags.each { |item| item.destroy } @@ -182,8 +182,8 @@ class DataController < ApplicationController newitem.user_id = current_user.id newitem.tag_id = translate_tag[newitem.tag_id] case newitem.taggable_type - when 'Todo' : newitem.taggable_id = translate_todo[newitem.taggable_id] - else newitem.taggable_id = 0 + when 'Todo' then newitem.taggable_id = translate_todo[newitem.taggable_id] + else newitem.taggable_id = 0 end newitem.save } @@ -198,7 +198,6 @@ class DataController < ApplicationController newitem.created_at = adjust_time(item.ivars['attributes']['created_at']) newitem.save } - end - -end + +end \ No newline at end of file diff --git a/app/views/data/index.html.erb b/app/views/data/index.html.erb index 8f7aec2c..5ed8adaf 100644 --- a/app/views/data/index.html.erb +++ b/app/views/data/index.html.erb @@ -1,44 +1,49 @@ -
    -
    -

    Exporting data

    -

    You can choose between the following formats:

    -
      -
    • YAML: Best for exporting data.
      Please note that importing YAML files is currently supported only in experimentally. Do not rely on it for backing up critical data.
    • -
    • CSV: Best for importing into spreadsheet or data analysis software
    • -
    • XML: Best for importing or repurposing the data
    • -
    -
    - -

    - - - - - - - - - - - - - - - - - - - - - -
    DescriptionDownload link
    YAML file containing all your actions, contexts, projects, tags and notes<%= link_to "YAML file", :controller => 'data', :action => 'yaml_export' %>
    CSV file containing all of your actions, with named contexts and projects<%= link_to "CSV file (actions, contexts and projects)", :controller => 'data', :action => 'csv_actions' %>
    CSV file containing all your notes<%= link_to "CSV file (notes only)", :controller => 'data', :action => 'csv_notes' %>
    XML file containing all your actions, contexts, projects, tags and notes<%= link_to "XML file (actions only)", :controller => 'data', :action => 'xml_export' %>
    -

    - -
    -
    -

    Importing data

    -

    Curently there is a experimental support for importing YAML files. Beware: all your current data will be destroyed before importing the YAML file, so if you have access to the database, we strongly reccoment backing up the database right now in case that anything goes wrong.

    -

    <%= link_to "Start import", :controller => 'data', :action => 'yaml_form' %>.

    +
    +
    +
    +

    Importing data

    +

    Curently there is a experimental support for importing YAML files. + Beware: all your current data will be destroyed before importing the YAML + file, so if you have access to the database, we strongly recomment backing up + the database right now in case that anything goes wrong. +

    +

    <%= link_to "Start import", :controller => 'data', :action => 'yaml_form' %>.

    +

    Exporting data

    +

    You can choose between the following formats:

    +
      +
    • YAML: Best for exporting data.
      Please note that importing YAML files is currently supported only in experimentally. Do not rely on it for backing up critical data.
    • +
    • CSV: Best for importing into spreadsheet or data analysis software
    • +
    • XML: Best for importing or repurposing the data
    • +
    +
    +

    + + + + + + + + + + + + + + + + + + + + + +
    DescriptionDownload link
    YAML file containing all your actions, contexts, projects, tags and notes<%= link_to "YAML file", :controller => 'data', :action => 'yaml_export' %>
    CSV file containing all of your actions, with named contexts and projects<%= link_to "CSV file (actions, contexts and projects)", :controller => 'data', :action => 'csv_actions' %>
    CSV file containing all your notes<%= link_to "CSV file (notes only)", :controller => 'data', :action => 'csv_notes' %>
    XML file containing all your actions, contexts, projects, tags and notes<%= link_to "XML file (actions only)", :controller => 'data', :action => 'xml_export' %>
    +
    -
    + diff --git a/app/views/data/yaml_form.html.erb b/app/views/data/yaml_form.html.erb index d3be8c1e..edc13098 100644 --- a/app/views/data/yaml_form.html.erb +++ b/app/views/data/yaml_form.html.erb @@ -1,17 +1,23 @@
    -
    -
    -

    Beware: all your current data will be destroyed before importing the YAML file, so if you have access to the database, we strongly reccoment 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| %> - <%= f.text_area :yaml %>
    - - <% end %> -

    -
    +
    +
    +

    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| %> + <%= f.text_area :yaml %>
    + + <% end %> +

    +
    -
    -
    + From 1d64ca0f34f30fdfde21f16e46624ac9f429de49 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Fri, 5 Sep 2008 18:01:22 +0200 Subject: [PATCH 028/102] move nifty js calls to respective pages to reduce js execution a bit (on every page) --- app/views/contexts/index.html.erb | 7 ++++++- app/views/layouts/standard.html.erb | 3 --- app/views/projects/index.html.erb | 8 +++++++- app/views/recurring_todos/index.html.erb | 6 ++++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/views/contexts/index.html.erb b/app/views/contexts/index.html.erb index 846d01ca..ef915ac1 100644 --- a/app/views/contexts/index.html.erb +++ b/app/views/contexts/index.html.erb @@ -53,4 +53,9 @@
    <% sortable_element 'list-contexts', get_listing_sortable_options --%> \ No newline at end of file +-%> + diff --git a/app/views/layouts/standard.html.erb b/app/views/layouts/standard.html.erb index 59f34550..21c9cdc4 100644 --- a/app/views/layouts/standard.html.erb +++ b/app/views/layouts/standard.html.erb @@ -16,9 +16,6 @@ diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb index b2108140..e8c7ec54 100644 --- a/app/views/projects/index.html.erb +++ b/app/views/projects/index.html.erb @@ -61,4 +61,10 @@

    - \ No newline at end of file + + + diff --git a/app/views/recurring_todos/index.html.erb b/app/views/recurring_todos/index.html.erb index 936773d8..40b586c5 100644 --- a/app/views/recurring_todos/index.html.erb +++ b/app/views/recurring_todos/index.html.erb @@ -40,3 +40,9 @@ apply_behaviour "#recurring_edit_period:click", "TracksForm.hide_all_edit_recurring(); $('recurring_edit_'+TracksForm.get_edit_period()).show();" -%> + + From 8783beb280483d32442705a0b3a8486ba726b178 Mon Sep 17 00:00:00 2001 From: epall Date: Wed, 4 Jun 2008 18:16:06 -0700 Subject: [PATCH 029/102] Added SMS and MMS gateway that can handle a variety of messages formats along with a set of testes for it. NOTE: this is the first patch submitted on ticket --- app/models/preference.rb | 1 + app/models/sms_gateway.rb | 29 ++ app/views/preferences/edit.html.erb | 4 + app/views/preferences/index.html.erb | 2 + db/migrate/039_add_sms_to_preference.rb | 11 + test/fixtures/contexts.yml | 9 + test/fixtures/preferences.yml | 20 ++ test/fixtures/sample_mms.txt | 374 +++++++++++++++++++++++ test/fixtures/sample_sms.txt | 12 + test/fixtures/users.yml | 10 + test/functional/users_controller_test.rb | 2 +- test/integration/users_xml_api_test.rb | 2 +- test/unit/sms_gateway_test.rb | 51 ++++ 13 files changed, 525 insertions(+), 2 deletions(-) create mode 100644 app/models/sms_gateway.rb create mode 100644 db/migrate/039_add_sms_to_preference.rb create mode 100644 test/fixtures/sample_mms.txt create mode 100644 test/fixtures/sample_sms.txt create mode 100644 test/unit/sms_gateway_test.rb diff --git a/app/models/preference.rb b/app/models/preference.rb index 6b76fd97..b61b1566 100644 --- a/app/models/preference.rb +++ b/app/models/preference.rb @@ -1,5 +1,6 @@ class Preference < ActiveRecord::Base belongs_to :user + belongs_to :sms_context, :class_name => 'Context' def self.due_styles { :due_in_n_days => 0, :due_on => 1} diff --git a/app/models/sms_gateway.rb b/app/models/sms_gateway.rb new file mode 100644 index 00000000..0034a4c8 --- /dev/null +++ b/app/models/sms_gateway.rb @@ -0,0 +1,29 @@ +class SMSGateway < ActionMailer::Base + CONTEXT_NAME = 'Inbox' + def receive(email) + user = User.find(:first, :include => [:preference], :conditions => ["preferences.sms_email = ?", email.from[0].strip]) + context = user.prefs.sms_context + + description = nil + notes = nil + + if email.content_type == "multipart/related" + description = email.subject + body_part = email.parts.find{|m| m.content_type == "text/plain"} + notes = body_part.body.strip + else + if email.subject.empty? + description = email.body.strip + notes = nil + else + description = email.subject.strip + notes = email.body.strip + end + end + + unless user.todos.find(:first, :conditions => {:description => description}) + # stupid T-Mobile often sends the same message multiple times + todo = user.todos.create(:context => context, :description => description, :notes => notes) + end + end +end diff --git a/app/views/preferences/edit.html.erb b/app/views/preferences/edit.html.erb index 86bbac6e..28171a7e 100644 --- a/app/views/preferences/edit.html.erb +++ b/app/views/preferences/edit.html.erb @@ -19,6 +19,8 @@
  • refresh: automatic refresh interval for each of the pages (in minutes)
  • verbose action descriptor: when true, show project/context name in action listing; when false show [P]/[C] with tool tips
  • mobile todos per page: the maximum number of actions to show on a single page in the mobile view
  • +
  • SMS email: the email address of your cell phone for sending text messages to your Tracks account
  • +
  • SMS context: the context to which tasks sent in via SMS should be added
  • @@ -67,6 +69,8 @@ <%= row_with_text_field('refresh') %> <%= row_with_select_field("verbose_action_descriptors") %> <%= row_with_text_field("mobile_todos_per_page") %> + <%= row_with_text_field("sms_email") %> + <%= table_row("sms_context", false) { select('prefs', 'sms_context_id', current_user.contexts.map{|c| [c.name, c.id]}) } %> <%= submit_tag "Update" %> <%= link_to "Cancel", :action => 'index' %> diff --git a/app/views/preferences/index.html.erb b/app/views/preferences/index.html.erb index 985834dd..d86dccf1 100644 --- a/app/views/preferences/index.html.erb +++ b/app/views/preferences/index.html.erb @@ -28,6 +28,8 @@
  • Refresh interval (in minutes): <%= prefs.refresh %>
  • Verbose action descriptors: <%= prefs.verbose_action_descriptors %>
  • Actions per page (Mobile View): <%= prefs.mobile_todos_per_page %>
  • +
  • SMS email address: <%= prefs.sms_email %>
  • +
  • SMS context: <%= prefs.sms_context.nil? ? "None" : prefs.sms_context.name %>
  • <%= link_to "Edit preferences »", { :controller => 'preferences', :action => 'edit'}, :class => 'edit_link' %> diff --git a/db/migrate/039_add_sms_to_preference.rb b/db/migrate/039_add_sms_to_preference.rb new file mode 100644 index 00000000..61f7e05a --- /dev/null +++ b/db/migrate/039_add_sms_to_preference.rb @@ -0,0 +1,11 @@ +class AddSmsToPreference < ActiveRecord::Migration + def self.up + add_column :preferences, :sms_email, :string + add_column :preferences, :sms_context_id, :integer + end + + def self.down + remove_column :preferences, :sms_context_id + remove_column :preferences, :sms_email + end +end diff --git a/test/fixtures/contexts.yml b/test/fixtures/contexts.yml index 0991767f..c9d3d5e0 100644 --- a/test/fixtures/contexts.yml +++ b/test/fixtures/contexts.yml @@ -113,3 +113,12 @@ someday_maybe: user_id: 1 created_at: <%= today %> updated_at: <%= today %> + +inbox: + id: 13 + name: Inbox + position: 1 + hide: false + user_id: 4 + created_at: <%= today %> + updated_at: <%= today %> diff --git a/test/fixtures/preferences.yml b/test/fixtures/preferences.yml index 380d1dc7..cad91d12 100644 --- a/test/fixtures/preferences.yml +++ b/test/fixtures/preferences.yml @@ -34,3 +34,23 @@ other_user_prefs: time_zone: "London" verbose_action_descriptors: false show_project_on_todo_done: true + +sms_user_prefs: + id: 3 + user_id: 4 + staleness_starts: 7 + date_format: "%d/%m/%Y" + title_date_format: "%A, %d %B %Y" + show_number_completed: 5 + show_completed_projects_in_sidebar: true + show_hidden_contexts_in_sidebar: true + show_hidden_projects_in_sidebar: true + admin_email: butshesagirl@rousette.org.uk + week_starts: 1 + due_style: 0 + refresh: 0 + time_zone: "London" + verbose_action_descriptors: false + show_project_on_todo_done: true + sms_email: 5555555555@tmomail.net + sms_context_id: 13 \ No newline at end of file diff --git a/test/fixtures/sample_mms.txt b/test/fixtures/sample_mms.txt new file mode 100644 index 00000000..eb5e7443 --- /dev/null +++ b/test/fixtures/sample_mms.txt @@ -0,0 +1,374 @@ +Return-Path: <15555555555/TYPE=PLMN@tmomail.net> +Date: Fri, 6 Jun 2008 21:38:26 -0400 +From: 5555555555@tmomail.net +To: gtd@tracks.com +Message-ID: <3645873.13759311212802713215.JavaMail.mms@rlyatl28> +Subject: This is the subject +MIME-Version: 1.0 +Content-Type: multipart/related; type="text/html"; + boundary="----=_Part_1240237_22156211.1212802713213" +X-UA: treo_600 +Importance: Normal +X-Mms-Sender-Visibility: Show +X-MMS-Message-Type: MM4_forward.REQ +X-Priority: 3 +X-Proofpoint-Spam-Reason: safe + +------=_Part_1240237_22156211.1212802713213 +Content-Type: text/html +Content-Transfer-Encoding: quoted-printable +Content-ID: <0000> +Content-Location:mms.smil +Content-Disposition: inline + + + + T-Mobile=20 + + + + + + + + + + + + + + + + + + + + + + + + +
    + +
    This is the message body
    =20 + +

    <= +/td> +
    + + + +------=_Part_1240237_22156211.1212802713213 +Content-Type: text/plain; charset=utf-8; name=text.txt +Content-Transfer-Encoding: 7bit +Content-ID: <133> +Content-Location: text.txt +Content-Disposition: inline + +This is the message body +------=_Part_1240237_22156211.1212802713213 +Content-Type: image/gif; name=dottedline350.gif +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=dottedline350.gif +Content-ID: + +R0lGODlhXgEBAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/ +/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBm +AABmMwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/ +MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNm +ZjNmmTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/ +mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZm +zGZm/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb/ +/5kAAJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZ +AJmZM5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwA +M8wAZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZ +ZsyZmcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8A +mf8AzP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+Z +zP+Z///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///yH5BAEAABAALAAAAABeAQEA +AAg1AFP9+yeQ4MCCCA8qNMgwYcOFDiNCnPiwokSLFC9qzMgRo8eNHzuCHCmyZMiTJFGaTMlSYUAA +Ow== +------=_Part_1240237_22156211.1212802713213 +Content-Type: image/gif; name=dottedline600.gif +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=dottedline600.gif +Content-ID: + +R0lGODlhWAIBAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/AP//AAAA//8A/wD/ +/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMwAAZgAAmQAAzAAA/wAzAAAzMwAzZgAzmQAzzAAz/wBm +AABmMwBmZgBmmQBmzABm/wCZAACZMwCZZgCZmQCZzACZ/wDMAADMMwDMZgDMmQDMzADM/wD/AAD/ +MwD/ZgD/mQD/zAD//zMAADMAMzMAZjMAmTMAzDMA/zMzADMzMzMzZjMzmTMzzDMz/zNmADNmMzNm +ZjNmmTNmzDNm/zOZADOZMzOZZjOZmTOZzDOZ/zPMADPMMzPMZjPMmTPMzDPM/zP/ADP/MzP/ZjP/ +mTP/zDP//2YAAGYAM2YAZmYAmWYAzGYA/2YzAGYzM2YzZmYzmWYzzGYz/2ZmAGZmM2ZmZmZmmWZm +zGZm/2aZAGaZM2aZZmaZmWaZzGaZ/2bMAGbMM2bMZmbMmWbMzGbM/2b/AGb/M2b/Zmb/mWb/zGb/ +/5kAAJkAM5kAZpkAmZkAzJkA/5kzAJkzM5kzZpkzmZkzzJkz/5lmAJlmM5lmZplmmZlmzJlm/5mZ +AJmZM5mZZpmZmZmZzJmZ/5nMAJnMM5nMZpnMmZnMzJnM/5n/AJn/M5n/Zpn/mZn/zJn//8wAAMwA +M8wAZswAmcwAzMwA/8wzAMwzM8wzZswzmcwzzMwz/8xmAMxmM8xmZsxmmcxmzMxm/8yZAMyZM8yZ +ZsyZmcyZzMyZ/8zMAMzMM8zMZszMmczMzMzM/8z/AMz/M8z/Zsz/mcz/zMz///8AAP8AM/8AZv8A +mf8AzP8A//8zAP8zM/8zZv8zmf8zzP8z//9mAP9mM/9mZv9mmf9mzP9m//+ZAP+ZM/+ZZv+Zmf+Z +zP+Z///MAP/MM//MZv/Mmf/MzP/M////AP//M///Zv//mf//zP///yH5BAEAABAALAAAAABYAgEA +AAhFAFP9+yeQ4MCCCA8qNMgwYcOFDiNCnPiwokSLFC9qzMgRo8eNHzuCHCmyZMiTJFGaTMlypUuV +MFvGfCmzJs2bM3Pa9BgQADs= +------=_Part_1240237_22156211.1212802713213 +Content-Type: image/gif; name=tmobilelogo.gif +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=tmobilelogo.gif +Content-ID: + +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 +d3Pf9lg/d3Vf94INCAA7 +------=_Part_1240237_22156211.1212802713213 +Content-Type: image/gif; name=tmobilespace.gif +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename=tmobilespace.gif +Content-ID: + +R0lGODlhAQABAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwMDcwKbK8AAAMzMAADMAMwAzMxYW +FhwcHCIiIikpKVVVVU1NTUJCQjk5Of98gP9QUNYAk8zs/+/Wxufn1q2pkDP/AGYAAJkAAMwAAAAz +ADMzAGYzAJkzAMwzAP8zAABmADNmAGZmAJlmAMxmAP9mAACZADOZAGaZAJmZAMyZAP+ZAADMADPM +AGbMAJnMAMzMAP/MAGb/AJn/AMz/AAD/MzMA/2YAM5kAM8wAM/8AMwAz/zMzM2YzM5kzM8wzM/8z +MwBmMzNmM2ZmM5lmM8xmM/9mMwCZMzOZM2aZM5mZM8yZM/+ZMwDMMzPMM2bMM5nMM8zMM//MMzP/ +M2b/M5n/M8z/M///MwAAZjMAZmYAZpkAZswAZv8AZgAzZjMzZmYzZpkzZswzZv8zZgBmZjNmZmZm +ZplmZsxmZgCZZjOZZmaZZpmZZsyZZv+ZZgDMZjPMZpnMZszMZv/MZgD/ZjP/Zpn/Zsz/Zv8AzMwA +/wCZmZkzmZkAmcwAmQAAmTMzmWYAmcwzmf8AmQBmmTNmmWYzmZlmmcxmmf8zmTOZmWaZmZmZmcyZ +mf+ZmQDMmTPMmWbMZpnMmczMmf/MmQD/mTP/mWbMmZn/mcz/mf//mQAAzDMAmWYAzJkAzMwAzAAz +mTMzzGYzzJkzzMwzzP8zzABmzDNmzGZmmZlmzMxmzP9mmQCZzDOZzGaZzJmZzMyZzP+ZzADMzDPM +zGbMzJnMzMzMzP/MzAD/zDP/zGb/mZn/zMz/zP//zDMAzGYA/5kA/wAzzDMz/2Yz/5kz/8wz//8z +/wBm/zNm/2ZmzJlm/8xm//9mzACZ/zOZ/2aZ/5mZ/8yZ//+Z/wDM/zPM/2bM/5nM/8zM///M/zP/ +/2b/zJn//8z///9mZmb/Zv//ZmZm//9m/2b//6UAIV9fX3d3d4aGhpaWlsvLy7KystfX193d3ePj +4+rq6vHx8fj4+P/78KCgpICAgP8AAAD/AP//AAAA//8A/wD//////ywAAAAAAQABAAAIBAD/BQQA +Ow== +------=_Part_1240237_22156211.1212802713213-- diff --git a/test/fixtures/sample_sms.txt b/test/fixtures/sample_sms.txt new file mode 100644 index 00000000..002cc204 --- /dev/null +++ b/test/fixtures/sample_sms.txt @@ -0,0 +1,12 @@ +Return-Path: <5555555555@tmomail.net> +Date: Tue, 3 Jun 2008 23:11:26 -0400 +From: 5555555555@tmomail.net +To: gtd@tracks.com +Message-ID: <6100602.65827251212549086388.JavaMail.imb@mgwatl02.cns.mms.com> +Subject: +MIME-Version: 1.0 +Content-Type: text/plain;charset=utf-8 +Content-Transfer-Encoding: 7bit +Importance: Normal + +This is a todo 4112093 \ No newline at end of file diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index ff277ba2..ef6f82c5 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -28,3 +28,13 @@ ldap_user: first_name: John last_name: Deere auth_type: ldap + +sms_user: + id: 4 + login: sms_user + crypted_password: <%= Digest::SHA1.hexdigest("#{Tracks::Config.salt}--sesame--") %> + token: <%= Digest::SHA1.hexdigest("sms_userSun Feb 19 14:42:45 GMT 20060.408173979260027") %> + is_admin: false + first_name: SMS + last_name: Tester + auth_type: database \ No newline at end of file diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb index 47677eb8..daad85d9 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -31,7 +31,7 @@ class UsersControllerTest < Test::Rails::TestCase get :index assert_response :success assert_equal "TRACKS::Manage Users", assigns['page_title'] - assert_equal 3, assigns['total_users'] + assert_equal 4, assigns['total_users'] assert_equal "/users", session['return-to'] end diff --git a/test/integration/users_xml_api_test.rb b/test/integration/users_xml_api_test.rb index 57d14d9f..ed26d194 100644 --- a/test/integration/users_xml_api_test.rb +++ b/test/integration/users_xml_api_test.rb @@ -78,7 +78,7 @@ class UsersXmlApiTest < ActionController::IntegrationTest get '/users.xml', {}, basic_auth_headers() assert_response :success assert_tag :tag => "users", - :children => { :count => 3, :only => { :tag => "user" } } + :children => { :count => 4, :only => { :tag => "user" } } assert_no_tag :tag => "password" end diff --git a/test/unit/sms_gateway_test.rb b/test/unit/sms_gateway_test.rb new file mode 100644 index 00000000..24660dbb --- /dev/null +++ b/test/unit/sms_gateway_test.rb @@ -0,0 +1,51 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class SMSGatewayTest < Test::Rails::TestCase + fixtures :users, :contexts + + def setup + @user = users(:sms_user) + @inbox = contexts(:inbox) + end + + def load_message(filename) + SMSGateway.receive(File.read(File.join(RAILS_ROOT, 'test', 'fixtures', filename))) + end + + def test_sms_with_no_subject + todo_count = Todo.count + + load_message('sample_sms.txt') + # assert some stuff about it being created + assert_equal(todo_count+1, Todo.count) + + message_todo = Todo.find(:first, :conditions => {:description => "This is a todo 4112093"}) + assert_not_nil(message_todo) + + assert_equal(@inbox, message_todo.context) + assert_equal(@user, message_todo.user) + end + + def test_double_sms + todo_count = Todo.count + load_message('sample_sms.txt') + load_message('sample_sms.txt') + assert_equal(todo_count+1, Todo.count) + end + + def test_mms_with_subject + todo_count = Todo.count + + load_message('sample_mms.txt') + + # assert some stuff about it being created + assert_equal(todo_count+1, Todo.count) + + message_todo = Todo.find(:first, :conditions => {:description => "This is the subject"}) + assert_not_nil(message_todo) + + assert_equal(@inbox, message_todo.context) + assert_equal(@user, message_todo.user) + assert_equal("This is the message body", message_todo.notes) + end +end From 920507441dfa3d997564653f9051804b32746c27 Mon Sep 17 00:00:00 2001 From: epall Date: Sat, 7 Jun 2008 17:15:12 -0700 Subject: [PATCH 030/102] Didn't handle case of no user found gracefully --- app/models/sms_gateway.rb | 2 ++ test/unit/sms_gateway_test.rb | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/app/models/sms_gateway.rb b/app/models/sms_gateway.rb index 0034a4c8..6c81e456 100644 --- a/app/models/sms_gateway.rb +++ b/app/models/sms_gateway.rb @@ -2,6 +2,8 @@ class SMSGateway < ActionMailer::Base CONTEXT_NAME = 'Inbox' def receive(email) user = User.find(:first, :include => [:preference], :conditions => ["preferences.sms_email = ?", email.from[0].strip]) + logger.info "Receiving SMS task from #{email.from[0].strip} For user #{user.nil? nil : user.login}" + return if user.nil? context = user.prefs.sms_context description = nil diff --git a/test/unit/sms_gateway_test.rb b/test/unit/sms_gateway_test.rb index 24660dbb..1c3879c8 100644 --- a/test/unit/sms_gateway_test.rb +++ b/test/unit/sms_gateway_test.rb @@ -48,4 +48,12 @@ class SMSGatewayTest < Test::Rails::TestCase assert_equal(@user, message_todo.user) assert_equal("This is the message body", message_todo.notes) end + + def test_no_user + todo_count = Todo.count + badmessage = File.read(File.join(RAILS_ROOT, 'test', 'fixtures', 'sample_sms.txt')) + badmessage.gsub!("5555555555", "notauser") + SMSGateway.receive(badmessage) + assert_equal(todo_count, Todo.count) + end end From dc0c5bffa4ea88e35af911e0876ec9994760ba4b Mon Sep 17 00:00:00 2001 From: epall Date: Fri, 13 Jun 2008 10:27:58 -0700 Subject: [PATCH 031/102] MMS prepends a 1 that needs to get stripped --- app/models/sms_gateway.rb | 4 +++- test/fixtures/sample_mms.txt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/sms_gateway.rb b/app/models/sms_gateway.rb index 6c81e456..563fa8a4 100644 --- a/app/models/sms_gateway.rb +++ b/app/models/sms_gateway.rb @@ -2,7 +2,9 @@ class SMSGateway < ActionMailer::Base CONTEXT_NAME = 'Inbox' def receive(email) user = User.find(:first, :include => [:preference], :conditions => ["preferences.sms_email = ?", email.from[0].strip]) - logger.info "Receiving SMS task from #{email.from[0].strip} For user #{user.nil? nil : user.login}" + if user.nil? + user = User.find(:first, :include => [:preference], :conditions => ["preferences.sms_email = ?", email.from[0].strip[1,100]]) + end return if user.nil? context = user.prefs.sms_context diff --git a/test/fixtures/sample_mms.txt b/test/fixtures/sample_mms.txt index eb5e7443..a532e3ab 100644 --- a/test/fixtures/sample_mms.txt +++ b/test/fixtures/sample_mms.txt @@ -1,6 +1,6 @@ Return-Path: <15555555555/TYPE=PLMN@tmomail.net> Date: Fri, 6 Jun 2008 21:38:26 -0400 -From: 5555555555@tmomail.net +From: 15555555555@tmomail.net To: gtd@tracks.com Message-ID: <3645873.13759311212802713215.JavaMail.mms@rlyatl28> Subject: This is the subject From 6210d3033d74c8e507058b2434dfed21eee90d8e Mon Sep 17 00:00:00 2001 From: epall Date: Sat, 14 Jun 2008 22:12:13 -0700 Subject: [PATCH 032/102] Modified sample_sms.txt to be subbed into in a sane way. Added ability to set context, due date, and show_from date from within an emailed message. --- app/models/sms_gateway.rb | 33 ++++++++++++++- test/fixtures/contexts.yml | 9 ++++ test/fixtures/sample_sms.txt | 2 +- test/unit/sms_gateway_test.rb | 78 ++++++++++++++++++++++++++++++++++- 4 files changed, 118 insertions(+), 4 deletions(-) diff --git a/app/models/sms_gateway.rb b/app/models/sms_gateway.rb index 563fa8a4..d7b3cefa 100644 --- a/app/models/sms_gateway.rb +++ b/app/models/sms_gateway.rb @@ -25,9 +25,38 @@ class SMSGateway < ActionMailer::Base end end - unless user.todos.find(:first, :conditions => {:description => description}) # stupid T-Mobile often sends the same message multiple times - todo = user.todos.create(:context => context, :description => description, :notes => notes) + return if user.todos.find(:first, :conditions => {:description => description}) + + # parse context + context_data = description.match(/^([^ ]*): (.*)/) + if context_data + context_name = context_data[1] + custom_context = user.contexts.find(:first, :conditions => {:name => context_name}) + if custom_context + context = custom_context + description = context_data[2] + end end + + # parse due date + due_regex = / ?due:([0-9\/-]{3,})/ + due_date = description.match(due_regex)[1] rescue nil + if due_date + #strip from description + description.sub!(due_regex, '').strip! + end + + # parse due date + show_regex = / ?show:([0-9\/-]{3,})/ + show_date = description.match(show_regex)[1] rescue nil + if show_date + #strip from description + description.sub!(show_regex, '').strip! + end + + # p "creating todo with description '#{description}', show from #{show_date}, context #{context.name}" + todo = user.todos.create(:context => context, :description => description, :notes => notes, :due => due_date, :show_from => show_date) + # p todo.validate end end diff --git a/test/fixtures/contexts.yml b/test/fixtures/contexts.yml index c9d3d5e0..49ce9d2f 100644 --- a/test/fixtures/contexts.yml +++ b/test/fixtures/contexts.yml @@ -122,3 +122,12 @@ inbox: user_id: 4 created_at: <%= today %> updated_at: <%= today %> + +anothercontext: + id: 14 + name: anothercontext + position: 2 + hide: false + user_id: 4 + created_at: <%= today %> + updated_at: <%= today %> diff --git a/test/fixtures/sample_sms.txt b/test/fixtures/sample_sms.txt index 002cc204..86b449a3 100644 --- a/test/fixtures/sample_sms.txt +++ b/test/fixtures/sample_sms.txt @@ -9,4 +9,4 @@ Content-Type: text/plain;charset=utf-8 Content-Transfer-Encoding: 7bit Importance: Normal -This is a todo 4112093 \ No newline at end of file +message_content \ No newline at end of file diff --git a/test/unit/sms_gateway_test.rb b/test/unit/sms_gateway_test.rb index 1c3879c8..6852566e 100644 --- a/test/unit/sms_gateway_test.rb +++ b/test/unit/sms_gateway_test.rb @@ -19,7 +19,7 @@ class SMSGatewayTest < Test::Rails::TestCase # assert some stuff about it being created assert_equal(todo_count+1, Todo.count) - message_todo = Todo.find(:first, :conditions => {:description => "This is a todo 4112093"}) + message_todo = Todo.find(:first, :conditions => {:description => "message_content"}) assert_not_nil(message_todo) assert_equal(@inbox, message_todo.context) @@ -56,4 +56,80 @@ class SMSGatewayTest < Test::Rails::TestCase SMSGateway.receive(badmessage) assert_equal(todo_count, Todo.count) end + + def test_direct_to_context + message = File.read(File.join(RAILS_ROOT, 'test', 'fixtures', 'sample_sms.txt')) + + valid_context_msg = message.gsub('message_content', 'anothercontext: this is a task') + invalid_context_msg = message.gsub('message_content', 'notacontext: this is a task') + + SMSGateway.receive(valid_context_msg) + valid_context_todo = Todo.find(:first, :conditions => {:description => "this is a task"}) + assert_not_nil(valid_context_todo) + assert_equal(contexts(:anothercontext), valid_context_todo.context) + + SMSGateway.receive(invalid_context_msg) + invalid_context_todo = Todo.find(:first, :conditions => {:description => 'notacontext: this is a task'}) + assert_not_nil(invalid_context_todo) + assert_equal(@inbox, invalid_context_todo.context) + end + + def test_due_date + message = File.read(File.join(RAILS_ROOT, 'test', 'fixtures', 'sample_sms.txt')) + + valid_due_msg1 = message.gsub('message_content', 'do something tomorrow due:6/15/2008') + valid_due_msg2 = message.gsub('message_content', 'do something tomorrow due:6/28/2008 and remember it!') + valid_due_msg3 = message.gsub('message_content', 'due:1/28/2008 funky!') + invalid_due_msg1 = message.gsub('message_content', 'do something tomorrow due:xxxx and remember it!') + + SMSGateway.receive(valid_due_msg1) + valid_due_todo1 = Todo.find(:first, :conditions => {:description => "do something tomorrow"}) + assert_not_nil(valid_due_todo1) + assert_equal(Date.civil(2008, 6, 15), valid_due_todo1.due) + + SMSGateway.receive(valid_due_msg2) + valid_due_todo2 = Todo.find(:first, :conditions => {:description => "do something tomorrow and remember it!"}) + assert_not_nil(valid_due_todo2) + assert_equal(Date.civil(2008, 6, 28), valid_due_todo2.due) + + SMSGateway.receive(valid_due_msg3) + valid_due_todo3 = Todo.find(:first, :conditions => {:description => "funky!"}) + assert_not_nil(valid_due_todo3) + assert_equal(Date.civil(2008, 1, 28), valid_due_todo3.due) + + SMSGateway.receive(invalid_due_msg1) + invalid_due_todo1 = Todo.find(:first, :conditions => {:description => "do something tomorrow due:xxxx and remember it!"}) + assert_not_nil(invalid_due_todo1) + assert_nil(invalid_due_todo1.due) + end + + def test_show_date + message = File.read(File.join(RAILS_ROOT, 'test', 'fixtures', 'sample_sms.txt')) + + valid_show_msg1 = message.gsub('message_content', "do something tomorrow show:#{Date.tomorrow.to_s}") + valid_show_msg2 = message.gsub('message_content', "do something next week show:#{Date.today.next_week.to_s} and remember it!") + valid_show_msg3 = message.gsub('message_content', "show:#{Date.tomorrow.to_s} alternative format") + invalid_show_msg1 = message.gsub('message_content', 'do something tomorrow show:xxxx and remember it!') + + SMSGateway.receive(valid_show_msg1) + valid_show_todo1 = Todo.find(:first, :conditions => {:description => "do something tomorrow"}) + assert_not_nil(valid_show_todo1) + assert_equal(Date.tomorrow, valid_show_todo1.show_from) + + SMSGateway.receive(valid_show_msg2) + valid_show_todo2 = Todo.find(:first, :conditions => {:description => "do something next week and remember it!"}) + assert_not_nil(valid_show_todo2) + assert_equal(Date.tomorrow.next_week, valid_show_todo2.show_from) + + SMSGateway.receive(valid_show_msg3) + valid_show_todo3 = Todo.find(:first, :conditions => {:description => "alternative format"}) + # p @user.todos.last + assert_not_nil(valid_show_todo3) + assert_equal(Date.tomorrow, valid_show_todo3.show_from) + + SMSGateway.receive(invalid_show_msg1) + invalid_show_todo1 = Todo.find(:first, :conditions => {:description => "do something tomorrow show:xxxx and remember it!"}) + assert_not_nil(invalid_show_todo1) + assert_nil(invalid_show_todo1.show_from) + end end From 98136d8ef277ab57f62b1472abaa9ffe199ff813 Mon Sep 17 00:00:00 2001 From: epall Date: Sun, 15 Jun 2008 08:56:07 -0700 Subject: [PATCH 033/102] Inconsistency in test. Why didn't I catch that? --- test/unit/sms_gateway_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/sms_gateway_test.rb b/test/unit/sms_gateway_test.rb index 6852566e..da6c121d 100644 --- a/test/unit/sms_gateway_test.rb +++ b/test/unit/sms_gateway_test.rb @@ -119,7 +119,7 @@ class SMSGatewayTest < Test::Rails::TestCase SMSGateway.receive(valid_show_msg2) valid_show_todo2 = Todo.find(:first, :conditions => {:description => "do something next week and remember it!"}) assert_not_nil(valid_show_todo2) - assert_equal(Date.tomorrow.next_week, valid_show_todo2.show_from) + assert_equal(Date.today.next_week, valid_show_todo2.show_from) SMSGateway.receive(valid_show_msg3) valid_show_todo3 = Todo.find(:first, :conditions => {:description => "alternative format"}) From 539fda21dcefcf90a69f5cbbe1ed1dafb3a807f8 Mon Sep 17 00:00:00 2001 From: epall Date: Mon, 16 Jun 2008 22:37:44 -0700 Subject: [PATCH 034/102] Preliminary defer buttons --- app/controllers/todos_controller.rb | 16 +++++++++++++++- app/helpers/todos_helper.rb | 4 ++++ app/views/todos/_todo.html.erb | 1 + public/stylesheets/standard.css | 6 +++++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index ac0c251a..d4f95738 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -384,7 +384,21 @@ class TodosController < ApplicationController end end - private + def defer + @source_view = params['_source_view'] || 'todo' + numdays = params['days'].to_i + @todo = Todo.find(params[:id]) + @todo.show_from = (@todo.show_from || Time.now.to_date) + numdays.days + @saved = @todo.save + + respond_to do |format| + format.html { redirect_to :back } + format.js {render :action => 'update'} + end + end + + + private def get_todo_from_params @todo = current_user.todos.find(params['id']) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index ec70762e..662c3d25 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -280,4 +280,8 @@ module TodosHelper image_tag("blank.png", :title =>"Star action", :class => class_str) end + def defer_link(days) + link_to_remote "+#{days}", :url => {:controller => 'todos', :action => 'defer', :id => @todo.id, :days => days, :_source_view => @source_view.underscore.gsub(/\s+/,'_')} + end + end diff --git a/app/views/todos/_todo.html.erb b/app/views/todos/_todo.html.erb index c03e64e6..8a4f70c2 100644 --- a/app/views/todos/_todo.html.erb +++ b/app/views/todos/_todo.html.erb @@ -18,6 +18,7 @@ <%= deferred_due_date %> <%= project_and_context_links( parent_container_type, :suppress_context => suppress_context, :suppress_project => suppress_project ) %> <%= render(:partial => "todos/toggle_notes", :locals => { :item => todo }) if todo.notes? %> + <%= defer_link(1) %> <%= defer_link(7) %>
    diff --git a/config/routes.rb b/config/routes.rb index a4eb29e3..4c1e69a6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,6 +28,10 @@ ActionController::Routing::Routes.draw do |map| projects.resources :todos, :name_prefix => "project_" end + map.resources :projects, :collection => {:order => :post, :actionize => :post} do |projects| + projects.resources :todos, :name_prefix => "project_" + end + map.resources :todos, :member => {:toggle_check => :put, :toggle_star => :put}, :collection => {:check_deferred => :post, :filter_to_context => :post, :filter_to_project => :post} From 0f4a80d839d0092ead30c1591db41c93b659811b Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Thu, 2 Oct 2008 09:49:51 +0200 Subject: [PATCH 056/102] fix recurring todos where new todos were not placed in tickler there was a Date.now in the code that was not migrated to the new timezone handling --- app/controllers/todos_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 3f36f194..e2034522 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -703,9 +703,9 @@ class TodosController < ApplicationController if @todo.from_recurring_todo? @recurring_todo = current_user.recurring_todos.find(@todo.recurring_todo_id) date_to_check = @todo.due.nil? ? @todo.show_from : @todo.due - date_to_check = Date.today()-1.day if date_to_check.nil? + date_to_check = Time.zone.now-1.day if date_to_check.nil? if @recurring_todo.active? && @recurring_todo.has_next_todo(date_to_check) - date = date_to_check >= Date.today() ? date_to_check : Date.today()-1.day + date = date_to_check >= Time.zone.now ? date_to_check : Time.zone.now-1.day @new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo, date) end end From eae4d0f6f164af2ff1629f06fee1e9e1eecb6b8c Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Thu, 2 Oct 2008 10:17:40 +0200 Subject: [PATCH 057/102] add test to check if a new recurring todo ends up in the tickler --- test/functional/todos_controller_test.rb | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/test/functional/todos_controller_test.rb b/test/functional/todos_controller_test.rb index 2adf747b..cd4fba01 100644 --- a/test/functional/todos_controller_test.rb +++ b/test/functional/todos_controller_test.rb @@ -5,7 +5,7 @@ require 'todos_controller' class TodosController; def rescue_action(e) raise e end; end class TodosControllerTest < Test::Rails::TestCase - fixtures :users, :preferences, :projects, :contexts, :todos, :tags, :taggings + fixtures :users, :preferences, :projects, :contexts, :todos, :tags, :taggings, :recurring_todos def setup @controller = TodosController.new @@ -376,6 +376,24 @@ class TodosControllerTest < Test::Rails::TestCase # check there is a new todo linked to the recurring pattern next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'}) assert_equal "Call Bill Gates every day", next_todo.description + + # change recurrence pattern to weekly and set show_from 2 days befor due date + # this forces the next todo to be put in the tickler + recurring_todo_1.show_from_delta = 2 + recurring_todo_1.recurring_period = 'weekly' + recurring_todo_1.every_day = 'smtwtfs' + recurring_todo_1.save + + # mark next_todo as complete by toggle_check + xhr :post, :toggle_check, :id => next_todo.id, :_source_view => 'todo' + next_todo.reload + assert next_todo.completed? + + # check there is a new todo linked to the recurring pattern in the tickler + next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'deferred'}) + assert_equal "Call Bill Gates every day", next_todo.description + # check that the todo is in the tickler + assert !next_todo.show_from.nil? end - + end From 0a39e1ea7e37f9fb0a1fb12d5dd7b13b60497d65 Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Thu, 2 Oct 2008 20:25:58 -0400 Subject: [PATCH 058/102] Fix routes to re-enable Selenium tests --- config/routes.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index a4eb29e3..c1b0180f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,6 +55,10 @@ ActionController::Routing::Routes.draw do |map| map.feeds 'feeds', :controller => 'feedlist', :action => 'index' map.feeds 'feeds.m', :controller => 'feedlist', :action => 'index', :format => 'm' + if Rails.env == 'test' + map.connect '/selenium_helper/login', :controller => 'selenium_helper', :action => 'login' + end + map.preferences 'preferences', :controller => 'preferences', :action => 'index' map.integrations 'integrations', :controller => 'integrations', :action => 'index' From 397029dfc15908000033927d25994c04c04d8b49 Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Thu, 2 Oct 2008 21:15:05 -0400 Subject: [PATCH 059/102] Add Selenium test for bug #776 and bug #775 and resolve both. Also fixed an unreported issue where deferring a todo by editing its show_from wouldn't hide an empty context. --- app/controllers/todos_controller.rb | 12 ++++++++++-- app/views/todos/update.js.rjs | 16 ++++++++++++---- test/selenium/home/defer_todo_empty_context.rsel | 10 ++++++++++ .../defer_todo_with_button_empty_context.rsel | 8 ++++++++ 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 test/selenium/home/defer_todo_empty_context.rsel create mode 100644 test/selenium/home/defer_todo_with_button_empty_context.rsel diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index e2034522..4e946f5c 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -221,7 +221,13 @@ class TodosController < ApplicationController @saved = @todo.update_attributes params["todo"] @context_changed = @original_item_context_id != @todo.context_id @todo_was_activated_from_deferred_state = @original_item_was_deferred && @todo.active? - determine_remaining_in_context_count(@original_item_context_id) if @context_changed + + if @context_changed + determine_remaining_in_context_count(@original_item_context_id) + else + determine_remaining_in_context_count(@todo.context_id) + end + @project_changed = @original_item_project_id != @todo.project_id if (@project_changed && !@original_item_project_id.nil?) then @remaining_undone_in_project = current_user.projects.find(@original_item_project_id).not_done_todo_count; end determine_down_count @@ -390,7 +396,9 @@ class TodosController < ApplicationController @todo = Todo.find(params[:id]) @todo.show_from = (@todo.show_from || @todo.user.date) + numdays.days @saved = @todo.save - + + determine_down_count + determine_remaining_in_context_count(@todo.context_id) respond_to do |format| format.html { redirect_to :back } format.js {render :action => 'update'} diff --git a/app/views/todos/update.js.rjs b/app/views/todos/update.js.rjs index 582460ff..dae71f41 100644 --- a/app/views/todos/update.js.rjs +++ b/app/views/todos/update.js.rjs @@ -16,10 +16,18 @@ if @saved if (@remaining_in_context == 0) # remove context container from page if empty - source_view do |from| - from.todo { page.visual_effect :fade, "c#{@original_item_context_id}", :duration => 0.4 } - from.tag { page.visual_effect :fade, "c#{@original_item_context_id}", :duration => 0.4 } - from.context { page.show "c#{@original_item_context_id}empty-nd" } + if @context_changed + source_view do |from| + from.todo { page.visual_effect :fade, "c#{@original_item_context_id}", :duration => 0.4 } + from.tag { page.visual_effect :fade, "c#{@original_item_context_id}", :duration => 0.4 } + from.context { page.show "c#{@original_item_context_id}empty-nd" } + end + else + source_view do |from| + from.todo { page.visual_effect :fade, item_container_id(@todo), :duration => 0.4 } + from.tag { page.visual_effect :fade, item_container_id(@todo), :duration => 0.4 } + from.context { page.show "c#{@original_item_context_id}empty-nd" } + end end end diff --git a/test/selenium/home/defer_todo_empty_context.rsel b/test/selenium/home/defer_todo_empty_context.rsel new file mode 100644 index 00000000..afb56fd7 --- /dev/null +++ b/test/selenium/home/defer_todo_empty_context.rsel @@ -0,0 +1,10 @@ +setup :fixtures => :all +login :as => 'admin' +open "/" +click "edit_icon_todo_5" +wait_for_element_present "show_from_todo_5" +type "show_from_todo_5", "1/1/2030" +click "css=#submit_todo_5" +wait_for_element_not_present "todo_5" +assert_text 'badge_count', '9' +wait_for_not_visible "c5" diff --git a/test/selenium/home/defer_todo_with_button_empty_context.rsel b/test/selenium/home/defer_todo_with_button_empty_context.rsel new file mode 100644 index 00000000..3997c0fb --- /dev/null +++ b/test/selenium/home/defer_todo_with_button_empty_context.rsel @@ -0,0 +1,8 @@ +setup :fixtures => :all +login :as => 'admin' +open "/" +wait_for_element_present '//div[@id="line_todo_5"]//img[@alt="Defer_1"]/..' +click '//div[@id="line_todo_5"]//img[@alt="Defer_1"]/..' +wait_for_element_not_present "todo_5" +assert_text 'badge_count', '9' +wait_for_not_visible "c5" From c6b159b97ea0800f8b4ad00ce765a623360909a5 Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Thu, 2 Oct 2008 21:43:38 -0400 Subject: [PATCH 060/102] Updated Selenium test suite so that *all tests pass*. Found and fixed one bug in routes that a Selenium test caught. --- config/routes.rb | 2 +- .../home/create_new_todo_in_context_and_hide_context.rsel | 2 +- test/selenium/home/mark_todo_complete_1.rsel | 2 +- test/selenium/home/mark_todo_complete_2.rsel | 2 +- test/selenium/home/mark_todo_incomplete.rsel | 2 +- test/selenium/mobile/create_new_action.rsel | 2 +- test/selenium/mobile/navigation.rsel | 4 ++-- test/selenium/project_detail/mark_deferred_todo_complete.rsel | 2 +- test/selenium/tags/badge_count.rsel | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index c1b0180f..e0a91a43 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -43,7 +43,7 @@ ActionController::Routing::Routes.draw do |map| # so /todos/tag/version1.5.xml will result in :name => 'version1.5.xml' # UPDATE: added support for mobile view. All tags ending on .m will be # routed to mobile view of tags. - todos.tag 'todos/tag/:name', :action => "tag", :format => 'm', :name => /.*\.m/ + todos.tag 'todos/tag/:name.m', :action => "tag", :format => 'm' todos.tag 'todos/tag/:name', :action => "tag", :name => /.*/ todos.mobile 'mobile', :action => "index", :format => 'm' diff --git a/test/selenium/home/create_new_todo_in_context_and_hide_context.rsel b/test/selenium/home/create_new_todo_in_context_and_hide_context.rsel index 64ff3ed9..4b8adbe7 100644 --- a/test/selenium/home/create_new_todo_in_context_and_hide_context.rsel +++ b/test/selenium/home/create_new_todo_in_context_and_hide_context.rsel @@ -9,7 +9,7 @@ assert_element_present "todo_9" # add new action to existing context type "todo_description", "a new action" -type "todo_context_name", "Agenda" +type "todo_context_name", "agenda" click "css=#todo-form-new-action .submit_box button" wait_for_visible "flash" diff --git a/test/selenium/home/mark_todo_complete_1.rsel b/test/selenium/home/mark_todo_complete_1.rsel index 35194bde..06ce62b5 100644 --- a/test/selenium/home/mark_todo_complete_1.rsel +++ b/test/selenium/home/mark_todo_complete_1.rsel @@ -2,4 +2,4 @@ setup :fixtures => :all login :as => 'admin' open '/' click "xpath=//div[@id='c1'] //div[@id='todo_9'] //input[@class='item-checkbox']" -wait_for_element_present "xpath=//div[@id='completed'] //div[@id='todo_9']" +wait_for_element_present "xpath=//div[@id='completed_container'] //div[@id='todo_9']" diff --git a/test/selenium/home/mark_todo_complete_2.rsel b/test/selenium/home/mark_todo_complete_2.rsel index dcb66769..b6cf4de9 100644 --- a/test/selenium/home/mark_todo_complete_2.rsel +++ b/test/selenium/home/mark_todo_complete_2.rsel @@ -2,5 +2,5 @@ setup :fixtures => :all login :as => 'admin' open '/' click "xpath=//div[@id='c5'] //div[@id='todo_5'] //input[@class='item-checkbox']" -wait_for_element_present "xpath=//div[@id='completed'] //div[@id='todo_5']" +wait_for_element_present "xpath=//div[@id='completed_container'] //div[@id='todo_5']" wait_for_not_visible 'c5' diff --git a/test/selenium/home/mark_todo_incomplete.rsel b/test/selenium/home/mark_todo_incomplete.rsel index d0a159ac..86c31088 100644 --- a/test/selenium/home/mark_todo_incomplete.rsel +++ b/test/selenium/home/mark_todo_incomplete.rsel @@ -1,6 +1,6 @@ setup :fixtures => :all login :as => 'admin' open '/' -click "xpath=//div[@id='completed'] //div[@id='todo_3'] //input[@class='item-checkbox']" +click "xpath=//div[@id='completed_container'] //div[@id='todo_3'] //input[@class='item-checkbox']" wait_for_element_present "xpath=//div[@id='c4'] //div[@id='todo_3']" assert_not_visible "c4empty-nd" diff --git a/test/selenium/mobile/create_new_action.rsel b/test/selenium/mobile/create_new_action.rsel index d475a799..a8501da1 100644 --- a/test/selenium/mobile/create_new_action.rsel +++ b/test/selenium/mobile/create_new_action.rsel @@ -4,7 +4,7 @@ login :as => 'admin' open '/m' wait_for_text 'css=h1 span.count', '10' -click_and_wait "link=Add new action" +click_and_wait "link=0-Add new action" type "todo_notes", "test notes" type "todo_description", "test name" diff --git a/test/selenium/mobile/navigation.rsel b/test/selenium/mobile/navigation.rsel index bf459466..44bde92c 100644 --- a/test/selenium/mobile/navigation.rsel +++ b/test/selenium/mobile/navigation.rsel @@ -7,7 +7,7 @@ wait_for_title "All actions" wait_for_text 'css=h1 span.count', '10' # open context page -click_and_wait "link=Contexts" +click_and_wait "link=2-Contexts" # verify_title "All actions in context agenda" # choose agenda context click_and_wait "link=agenda" @@ -18,7 +18,7 @@ click_and_wait "link=foo" verify_title "TRACKS::Tagged with 'foo'" wait_for_text 'css=h1 span.count', '2' -click_and_wait "link=Projects" +click_and_wait "link=3-Projects" wait_for_text 'css=h1 span.count', '3' click_and_wait "link=Build a working time machine" wait_for_text 'css=h1 span.count', '3' diff --git a/test/selenium/project_detail/mark_deferred_todo_complete.rsel b/test/selenium/project_detail/mark_deferred_todo_complete.rsel index 893192f8..0b07d445 100644 --- a/test/selenium/project_detail/mark_deferred_todo_complete.rsel +++ b/test/selenium/project_detail/mark_deferred_todo_complete.rsel @@ -3,5 +3,5 @@ login :as => 'admin' open "/projects/1" include_partial 'project_detail/add_deferred_todo' click "xpath=//div[@id='tickler'] //div[@id='todo_15'] //input[@class='item-checkbox']" -wait_for_element_present "xpath=//div[@id='completed'] //div[@id='todo_15']" +wait_for_element_present "xpath=//div[@id='completed_container'] //div[@id='todo_15']" assert_not_visible "tickler-empty-nd" \ No newline at end of file diff --git a/test/selenium/tags/badge_count.rsel b/test/selenium/tags/badge_count.rsel index 56cd9f84..4359059d 100644 --- a/test/selenium/tags/badge_count.rsel +++ b/test/selenium/tags/badge_count.rsel @@ -11,6 +11,6 @@ assert_text 'badge_count', '1' # mark one complete click "xpath=//div[@id='c1'] //div[@id='todo_1'] //input[@class='item-checkbox']" -wait_for_element_present "xpath=//div[@id='completed'] //div[@id='todo_1']" +wait_for_element_present "xpath=//div[@id='completed_container'] //div[@id='todo_1']" assert_text 'badge_count', '0' From 01c7fd129656b543c4b4115961bdcbe3f5ca2176 Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Fri, 3 Oct 2008 00:35:03 -0400 Subject: [PATCH 061/102] Tweak preference name a bit and add documentation to Integrations controller --- app/views/integrations/index.html.erb | 12 ++++++++++++ app/views/preferences/edit.html.erb | 2 +- app/views/preferences/index.html.erb | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/views/integrations/index.html.erb b/app/views/integrations/index.html.erb index d200b12c..be2a5a0c 100644 --- a/app/views/integrations/index.html.erb +++ b/app/views/integrations/index.html.erb @@ -12,6 +12,18 @@

    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.

    + +

    Integrated email/SMS receiver

    +

    +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: +

      +
    • Go to <%= link_to "Preferences", preferences_url %> and set your "From email" and "default email context" for todos sent in via email (which could come from an SMS message)
    • +
    • In sendmail/qmail/postfix/whatever, set up an email address alias to pipe messages to
      /PATH/TO/RUBY/ruby /PATH/TO/TRACKS/script/runner -e production 'MessageGateway.receive(STDIN.read)'
    • +
    • Send an email to your newly configured address!
    • +
    +You can also use the Rich Todo API to send in tasks like "do laundry @ Home" or "Call Bill > project X". The subject of the message will fill description, context, and project, while the body will populate the tasks's note. +

    +

    Add an Action with Applescript

    This is a simple script that pops up a dialog box asking for a description, and then sends that to Tracks with a hard-coded context.

    diff --git a/app/views/preferences/edit.html.erb b/app/views/preferences/edit.html.erb index 796cd8ce..4b1c3b5f 100644 --- a/app/views/preferences/edit.html.erb +++ b/app/views/preferences/edit.html.erb @@ -20,7 +20,7 @@
  • verbose action descriptor: when true, show project/context name in action listing; when false show [P]/[C] with tool tips
  • mobile todos per page: the maximum number of actions to show on a single page in the mobile view
  • From email: the email address you use for sending todos as email or SMS messages to your Tracks account
  • -
  • Message context: the context to which tasks sent in via email or SMS should be added
  • +
  • Default email context: the context to which tasks sent in via email or SMS should be added
  • diff --git a/app/views/preferences/index.html.erb b/app/views/preferences/index.html.erb index 81b0edf0..05c90a86 100644 --- a/app/views/preferences/index.html.erb +++ b/app/views/preferences/index.html.erb @@ -29,7 +29,7 @@
  • Verbose action descriptors: <%= prefs.verbose_action_descriptors %>
  • Actions per page (Mobile View): <%= prefs.mobile_todos_per_page %>
  • From email: <%= prefs.sms_email %>
  • -
  • Message context: <%= prefs.sms_context.nil? ? "None" : prefs.sms_context.name %>
  • +
  • Default email context: <%= prefs.sms_context.nil? ? "None" : prefs.sms_context.name %>
  • <%= link_to "Edit preferences »", { :controller => 'preferences', :action => 'edit'}, :class => 'edit_link' %> From fcaea3ce20495977240d2d75771636924a2b0c49 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Thu, 2 Oct 2008 17:28:49 +0200 Subject: [PATCH 062/102] first pass at adding calendar view --- app/controllers/todos_controller.rb | 34 +++++++++++++++++++++++++++-- app/views/todos/calendar.html.erb | 34 +++++++++++++++++++++++++++++ config/routes.rb | 2 ++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 app/views/todos/calendar.html.erb diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 4e946f5c..e2bac2ce 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -4,7 +4,7 @@ class TodosController < ApplicationController skip_before_filter :login_required, :only => [:index] prepend_before_filter :login_or_feed_token_required, :only => [:index] - append_before_filter :init, :except => [ :destroy, :completed, :completed_archive, :check_deferred, :toggle_check, :toggle_star, :edit, :update, :create ] + append_before_filter :init, :except => [ :destroy, :completed, :completed_archive, :check_deferred, :toggle_check, :toggle_star, :edit, :update, :create, :calendar ] append_before_filter :get_todo_from_params, :only => [ :edit, :toggle_check, :toggle_star, :show, :update, :destroy ] session :off, :only => :index, :if => Proc.new { |req| is_feed_request(req) } @@ -404,7 +404,37 @@ class TodosController < ApplicationController format.js {render :action => 'update'} end end - + + def calendar + @source_view = params['_source_view'] || 'calendar' + @page_title = "TRACKS::Calendar" + + due_today_date = Time.zone.now + due_this_week_date = Time.zone.now.end_of_week + due_next_week_date = due_this_week_date + 7.days + due_this_month_date = Time.zone.now.end_of_month + + @due_today = current_user.todos.find(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due <= ?', 'active', 'deferred', due_today_date], + :order => "due") + @due_this_week = current_user.todos.find(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_today_date, due_this_week_date], + :order => "due") + @due_next_week = current_user.todos.find(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_this_week_date, due_next_week_date], + :order => "due") + @due_this_month = current_user.todos.find(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_next_week_date, due_this_month_date], + :order => "due") + @due_after_this_month = current_user.todos.find(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ?', 'active', 'deferred', due_this_month_date], + :order => "due") + + respond_to do |format| + format.html + format.ics { render :text => 'Not implemented yet', :status => 200 } + end + end private diff --git a/app/views/todos/calendar.html.erb b/app/views/todos/calendar.html.erb new file mode 100644 index 00000000..f887ffac --- /dev/null +++ b/app/views/todos/calendar.html.erb @@ -0,0 +1,34 @@ +
    + +
    +

    Due today

    + <%= render :partial => "todos/todo", :collection => @due_today %> +
    + +
    +

    Due this week

    + <%= render :partial => "todos/todo", :collection => @due_this_week %> +
    + +
    +

    Due next week

    + <%= render :partial => "todos/todo", :collection => @due_next_week %> +
    + +
    +

    Due rest of this month

    + <%= render :partial => "todos/todo", :collection => @due_this_month %> +
    + +
    +

    Due next month and later

    + <%= render :partial => "todos/todo", :collection => @due_after_this_month %> +
    + +
    +
    + + +

    +

    iCal - Get this calendar in iCal-format to add to your own calendar

    +
    \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index e0a91a43..222f7619 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,6 +46,8 @@ ActionController::Routing::Routes.draw do |map| todos.tag 'todos/tag/:name.m', :action => "tag", :format => 'm' todos.tag 'todos/tag/:name', :action => "tag", :name => /.*/ + todos.calendar 'calendar', :action => "calendar" + todos.mobile 'mobile', :action => "index", :format => 'm' todos.mobile_abbrev 'm', :action => "index", :format => 'm' todos.mobile_abbrev_new 'm/new', :action => "new", :format => 'm' From 05f21ebba25cc3232876add7dc4be33c62ce93f0 Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Fri, 3 Oct 2008 10:19:41 -0400 Subject: [PATCH 063/102] Spec and fix bug #776 by using auto_link instead of my dumb regex --- app/views/notes/_notes.rhtml | 2 +- app/views/todos/_toggle_notes.rhtml | 2 +- spec/views/notes/_notes.rhtml_spec.rb | 36 ++++++++++++++++++++ spec/views/todos/_toggle_notes.rhtml_spec.rb | 33 ++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 spec/views/notes/_notes.rhtml_spec.rb create mode 100644 spec/views/todos/_toggle_notes.rhtml_spec.rb diff --git a/app/views/notes/_notes.rhtml b/app/views/notes/_notes.rhtml index 9391d108..de9068b8 100644 --- a/app/views/notes/_notes.rhtml +++ b/app/views/notes/_notes.rhtml @@ -2,7 +2,7 @@

    <%= link_to("Note #{note.id}", note_path(note), :title => "Show note #{note.id}" ) %>

    - <%= sanitize(textilize(note.body.gsub(/((https?:\/\/[^ \n\t]*))/, '"\1":\2'))) %> + <%= sanitize(markdown(auto_link(note.body))) %> -
    - +
    +

    -

    iCal - Get this calendar in iCal-format to add to your own calendar

    -
    \ No newline at end of file +

    <%= link_to('iCal', {:format => 'ics', :token => current_user.token}, :title => "iCal feed" ) %> + - Get this calendar in iCal format

    +
    + +<% +apply_behavior 'input.hide_tickler:click', :prevent_default => true do |page| + page << "alert('hiding action in tickler from calendar is not yet implemented');" +end +%> \ No newline at end of file diff --git a/app/views/todos/calendar.ics.erb b/app/views/todos/calendar.ics.erb new file mode 100644 index 00000000..af9d7682 --- /dev/null +++ b/app/views/todos/calendar.ics.erb @@ -0,0 +1,25 @@ +BEGIN:VCALENDAR +PRODID:-//TRACKS//<%= TRACKS_VERSION %>//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Tracks +<% for todo in @due_all -%> +BEGIN:VEVENT +DTSTART;VALUE=DATE:<%= todo.due.strftime("%Y%m%d") %> +DTEND;VALUE=DATE:<%= (todo.due+1.day).strftime("%Y%m%d") %> +DTSTAMP:<%= todo.due.strftime("%Y%m%dT%H%M%SZ") %> +UID:<%= todo_url(todo) %> +CLASS:PUBLIC +CATEGORIES:Tracks +CREATED:<%= todo.created_at.strftime("%Y%m%dT%H%M%SZ") %> +DESCRIPTION:<%= format_ical_notes(todo.notes) %> +LAST-MODIFIED:<%= todo.due.strftime("%Y%m%dT%H%M%SZ") %> +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:<%= todo.description %> +TRANSP:TRANSPARENT +END:VEVENT +<% end -%> +END:VCALENDAR \ No newline at end of file diff --git a/app/views/todos/update.js.rjs b/app/views/todos/update.js.rjs index dae71f41..a5c8e147 100644 --- a/app/views/todos/update.js.rjs +++ b/app/views/todos/update.js.rjs @@ -101,6 +101,12 @@ if @saved elsif source_view_is :stats page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type } page.visual_effect :highlight, dom_id(@todo), :duration => 3 + elsif source_view_is :calendar + if @due_date_changed + page[@todo].remove + page.insert_html :bottom, @new_due_id, :partial => 'todos/todo' + page.visual_effect :highlight, dom_id(@todo), :duration => 3 + end else logger.error "unexpected source_view '#{params[:_source_view]}'" end diff --git a/config/routes.rb b/config/routes.rb index 222f7619..50e29ff7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,6 +46,7 @@ ActionController::Routing::Routes.draw do |map| todos.tag 'todos/tag/:name.m', :action => "tag", :format => 'm' todos.tag 'todos/tag/:name', :action => "tag", :name => /.*/ + todos.calendar 'calendar.ics', :action => "calendar", :format => 'ics' todos.calendar 'calendar', :action => "calendar" todos.mobile 'mobile', :action => "index", :format => 'm' diff --git a/public/images/x-office-calendar.png b/public/images/x-office-calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..f8607e3303a23051686f1bbc1a34a10845a946d6 GIT binary patch literal 515 zcmV+e0{s1nP)WFU8GbZ8({Xk{QrNlj4iWF>9@00C!7L_t(I%bk)xOT$nU z#ZRP@U0O*0EIw!{{Spca{z32~C^$r0mkwfS1?i#$L+V)aO{8KKB6Mn91d+}i8d6^3 z*5yI2gA@{ywABM|xaVA!s(6kr0A3 zT_sId_x!zU{=#)#IF18{JK&td{dVy_9OCKu5%$!^)nx+!dVWX%Ktv!Scp!L0AdgTe z7BCnLkz7y0p4vD$J_Uf@yeHuY+^M&)tV!hZm5Vbl#*oR#h+V<5CSYtM5>PA_FdmO_ zcyJW(0}0M63bwa)!8s2BWLd^r{}riJDn5Zd;JRrjt3>X0y8vL$>mOYzng13n?oI#z002ovPDHLk FV1i6{%~JpX literal 0 HcmV?d00001 From 5d30512e9786ff75bf1dc114d30a4499bd411d0d Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Sat, 4 Oct 2008 15:56:22 +0200 Subject: [PATCH 065/102] add message when there are no actions in a container and handle state changes --- app/controllers/todos_controller.rb | 81 +++++++++++++++++++++++------ app/views/todos/calendar.html.erb | 17 ++++++ app/views/todos/destroy.js.rjs | 1 + app/views/todos/toggle_check.js.rjs | 3 +- app/views/todos/update.js.rjs | 11 ++++ 5 files changed, 97 insertions(+), 16 deletions(-) diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index beceb67a..091fa85a 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -128,8 +128,10 @@ class TodosController < ApplicationController # Toggles the 'done' status of the action # def toggle_check + @source_view = params['_source_view'] || 'todo' + @original_item_due = @todo.due @saved = @todo.toggle_completion! - + # check if this todo has a related recurring_todo. If so, create next todo check_for_next_todo if @saved @@ -139,6 +141,8 @@ class TodosController < ApplicationController determine_remaining_in_context_count(@todo.context_id) determine_down_count determine_completed_count if @todo.completed? + @original_item_due_id = get_due_id_for_calendar(@original_item_due) + @old_due_empty = is_old_due_empty(@original_item_due_id) end render end @@ -173,6 +177,8 @@ class TodosController < ApplicationController @original_item_project_id = @todo.project_id @original_item_was_deferred = @todo.deferred? @original_item_due = @todo.due + @original_item_due_id = get_due_id_for_calendar(@todo.due) + if params['todo']['project_id'].blank? && !params['project_name'].nil? if params['project_name'] == 'None' project = Project.null_object @@ -225,21 +231,13 @@ class TodosController < ApplicationController @due_date_changed = @original_item_due != @todo.due if @due_date_changed - due_today_date = Time.zone.now - due_this_week_date = Time.zone.now.end_of_week - due_next_week_date = due_this_week_date + 7.days - due_this_month_date = Time.zone.now.end_of_month - if @todo.due <= due_today_date - @new_due_id = "due_today" - elsif @todo.due <= due_this_week_date - @new_due_id = "due_this_week" - elsif @todo.due <= due_next_week_date - @new_due_id = "due_next_week" - elsif @todo.due <= due_this_month_date - @new_due_id = "due_this_month_week" + @old_due_empty = is_old_due_empty(@original_item_due_id) + if @todo.due.nil? + # do not act further on date change when date is changed to nil + @due_date_changed = false else - @new_due_id = "due_after_this_month" - end + @new_due_id = get_due_id_for_calendar(@todo.due) + end end if @context_changed @@ -271,6 +269,7 @@ class TodosController < ApplicationController def destroy @todo = get_todo_from_params + @original_item_due = @todo.due @context_id = @todo.context_id @project_id = @todo.project_id @@ -297,6 +296,8 @@ class TodosController < ApplicationController if source_view_is_one_of(:todo, :deferred) determine_remaining_in_context_count(@context_id) end + @original_item_due_id = get_due_id_for_calendar(@original_item_due) + @old_due_empty = is_old_due_empty(@original_item_due_id) end render end @@ -435,18 +436,23 @@ class TodosController < ApplicationController due_this_month_date = Time.zone.now.end_of_month @due_today = current_user.todos.find(:all, + :include => [:taggings, :tags], :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due <= ?', 'active', 'deferred', due_today_date], :order => "due") @due_this_week = current_user.todos.find(:all, + :include => [:taggings, :tags], :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_today_date, due_this_week_date], :order => "due") @due_next_week = current_user.todos.find(:all, + :include => [:taggings, :tags], :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_this_week_date, due_next_week_date], :order => "due") @due_this_month = current_user.todos.find(:all, + :include => [:taggings, :tags], :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_next_week_date, due_this_month_date], :order => "due") @due_after_this_month = current_user.todos.find(:all, + :include => [:taggings, :tags], :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ?', 'active', 'deferred', due_this_month_date], :order => "due") @@ -773,6 +779,51 @@ class TodosController < ApplicationController end end end + + def get_due_id_for_calendar(due) + due_today_date = Time.zone.now + due_this_week_date = Time.zone.now.end_of_week + due_next_week_date = due_this_week_date + 7.days + due_this_month_date = Time.zone.now.end_of_month + if due <= due_today_date + new_due_id = "due_today" + elsif due <= due_this_week_date + new_due_id = "due_this_week" + elsif due <= due_next_week_date + new_due_id = "due_next_week" + elsif due <= due_this_month_date + new_due_id = "due_this_month" + else + new_due_id = "due_after_this_month" + end + return new_due_id + end + + def is_old_due_empty(id) + due_today_date = Time.zone.now + due_this_week_date = Time.zone.now.end_of_week + due_next_week_date = due_this_week_date + 7.days + due_this_month_date = Time.zone.now.end_of_month + case id + when "due_today" + return 0 == current_user.todos.count(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due <= ?', 'active', 'deferred', due_today_date]) + when "due_this_week" + return 0 == current_user.todos.count(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_today_date, due_this_week_date]) + when "due_next_week" + return 0 == current_user.todos.count(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_this_week_date, due_next_week_date]) + when "due_this_month" + return 0 == current_user.todos.count(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_next_week_date, due_this_month_date]) + when "due_after_this_month" + return 0 == current_user.todos.count(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ?', 'active', 'deferred', due_this_month_date]) + else + raise Exception.new, "unknown due id for calendar: '#{id}'" + end + end class FindConditionBuilder diff --git a/app/views/todos/calendar.html.erb b/app/views/todos/calendar.html.erb index 29860508..056c0cff 100644 --- a/app/views/todos/calendar.html.erb +++ b/app/views/todos/calendar.html.erb @@ -2,6 +2,9 @@

    Due today

    +
    > + No actions due today +
    <%= render :partial => "todos/todo", :collection => @due_today %>
    @@ -9,6 +12,9 @@

    Due in rest of this week

    +
    > + No actions due in rest of this week +
    <%= render :partial => "todos/todo", :collection => @due_this_week %>
    @@ -16,6 +22,9 @@

    Due next week

    +
    > + No actions due in next week +
    <%= render :partial => "todos/todo", :collection => @due_next_week %>
    @@ -23,6 +32,9 @@

    Due in rest of this month

    +
    > + No actions due in rest of this month +
    <%= render :partial => "todos/todo", :collection => @due_this_month %>
    @@ -30,6 +42,9 @@

    Due next month and later

    +
    > + No actions due after this month +
    <%= render :partial => "todos/todo", :collection => @due_after_this_month %>
    @@ -37,9 +52,11 @@
    +

    <%= link_to('iCal', {:format => 'ics', :token => current_user.token}, :title => "iCal feed" ) %> - Get this calendar in iCal format

    diff --git a/app/views/todos/destroy.js.rjs b/app/views/todos/destroy.js.rjs index 7f37f87d..ac1ba3d4 100644 --- a/app/views/todos/destroy.js.rjs +++ b/app/views/todos/destroy.js.rjs @@ -1,5 +1,6 @@ if @saved page[@todo].remove + page.show "empty_"+@original_item_due_id if @old_due_empty page['badge_count'].replace_html @down_count # remove context if empty diff --git a/app/views/todos/toggle_check.js.rjs b/app/views/todos/toggle_check.js.rjs index 1462162a..d862862c 100644 --- a/app/views/todos/toggle_check.js.rjs +++ b/app/views/todos/toggle_check.js.rjs @@ -1,5 +1,6 @@ if @saved page[@todo].remove + page.show "empty_"+@original_item_due_id if @old_due_empty if @todo.completed? # completed todos move from their context to the completed container @@ -26,7 +27,7 @@ if @saved page.notify :notice, "There is no next action after the recurring action you just finished. The recurrence is completed", 6.0 if @new_recurring_todo.nil? end end - + else # todo is activated from completed container page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id(@todo) diff --git a/app/views/todos/update.js.rjs b/app/views/todos/update.js.rjs index a5c8e147..2f263b54 100644 --- a/app/views/todos/update.js.rjs +++ b/app/views/todos/update.js.rjs @@ -104,8 +104,19 @@ if @saved elsif source_view_is :calendar if @due_date_changed page[@todo].remove + page.show "empty_"+@original_item_due_id if @old_due_empty + page.hide "empty_"+@new_due_id page.insert_html :bottom, @new_due_id, :partial => 'todos/todo' page.visual_effect :highlight, dom_id(@todo), :duration => 3 + else + if @todo.due.nil? + # due date removed + page[@todo].remove + page.show "empty_"+@original_item_due_id if @old_due_empty + else + page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type } + page.visual_effect :highlight, dom_id(@todo), :duration => 3 + end end else logger.error "unexpected source_view '#{params[:_source_view]}'" From 89043af7ff86530fb54492a680f9f9b4b5352322 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Sat, 4 Oct 2008 20:41:12 +0200 Subject: [PATCH 066/102] fix editing of todos (also delete and check complete) bug was introduced in calendar code --- app/controllers/todos_controller.rb | 32 +++++++++++++++++------------ 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 091fa85a..ff4a108d 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -141,8 +141,10 @@ class TodosController < ApplicationController determine_remaining_in_context_count(@todo.context_id) determine_down_count determine_completed_count if @todo.completed? - @original_item_due_id = get_due_id_for_calendar(@original_item_due) - @old_due_empty = is_old_due_empty(@original_item_due_id) + if source_view_is :calendar + @original_item_due_id = get_due_id_for_calendar(@original_item_due) + @old_due_empty = is_old_due_empty(@original_item_due_id) + end end render end @@ -177,7 +179,7 @@ class TodosController < ApplicationController @original_item_project_id = @todo.project_id @original_item_was_deferred = @todo.deferred? @original_item_due = @todo.due - @original_item_due_id = get_due_id_for_calendar(@todo.due) + @original_item_due_id = get_due_id_for_calendar(@todo.due) if params['todo']['project_id'].blank? && !params['project_name'].nil? if params['project_name'] == 'None' @@ -229,14 +231,16 @@ class TodosController < ApplicationController @context_changed = @original_item_context_id != @todo.context_id @todo_was_activated_from_deferred_state = @original_item_was_deferred && @todo.active? - @due_date_changed = @original_item_due != @todo.due - if @due_date_changed - @old_due_empty = is_old_due_empty(@original_item_due_id) - if @todo.due.nil? - # do not act further on date change when date is changed to nil - @due_date_changed = false - else - @new_due_id = get_due_id_for_calendar(@todo.due) + if source_view_is :calendar + @due_date_changed = @original_item_due != @todo.due + if @due_date_changed + @old_due_empty = is_old_due_empty(@original_item_due_id) + if @todo.due.nil? + # do not act further on date change when date is changed to nil + @due_date_changed = false + else + @new_due_id = get_due_id_for_calendar(@todo.due) + end end end @@ -295,9 +299,10 @@ class TodosController < ApplicationController determine_down_count if source_view_is_one_of(:todo, :deferred) determine_remaining_in_context_count(@context_id) + elsif source_view_is :calendar + @original_item_due_id = get_due_id_for_calendar(@original_item_due) + @old_due_empty = is_old_due_empty(@original_item_due_id) end - @original_item_due_id = get_due_id_for_calendar(@original_item_due) - @old_due_empty = is_old_due_empty(@original_item_due_id) end render end @@ -781,6 +786,7 @@ class TodosController < ApplicationController end def get_due_id_for_calendar(due) + return "" if due.nil? due_today_date = Time.zone.now due_this_week_date = Time.zone.now.end_of_week due_next_week_date = due_this_week_date + 7.days From 7b90c00d849993c4083910d23ac15ee3fa01d7b6 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Mon, 6 Oct 2008 11:11:56 +0200 Subject: [PATCH 067/102] fix corner case where checking repeating todos complete that are due todo today will create a new todo that is also due today with the intriduction of datetime for due and show_from, the time part needed to be discarded in a compare also adding some comments to be able to understand the code better --- app/controllers/todos_controller.rb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index ff4a108d..ada43168 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -776,10 +776,24 @@ class TodosController < ApplicationController @recurring_todo = nil if @todo.from_recurring_todo? @recurring_todo = current_user.recurring_todos.find(@todo.recurring_todo_id) + + # check for next todo either from the due date or the show_from date date_to_check = @todo.due.nil? ? @todo.show_from : @todo.due + + # if both due and show_from are nil, check for a next todo with yesterday + # as reference point. We pick yesterday so that new todos for today will + # be created instead of new todos for tomorrow. date_to_check = Time.zone.now-1.day if date_to_check.nil? + if @recurring_todo.active? && @recurring_todo.has_next_todo(date_to_check) - date = date_to_check >= Time.zone.now ? date_to_check : Time.zone.now-1.day + + # shift the reference date to yesterday if date_to_check is furher in + # the past. This is to make sure we do not get older todos for overdue + # todos. I.e. checking a daily todo that is overdue with 5 days will + # create a new todo which is overdue by 4 days if we don't shift the + # date. Discard the time part in the compare + date = date_to_check.at_midnight >= Time.zone.now.at_midnight ? date_to_check : Time.zone.now-1.day + @new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo, date) end end From f5c50d367e7da119b540020c1bf371a3016232d7 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Mon, 6 Oct 2008 17:13:38 +0200 Subject: [PATCH 068/102] move overdue actions to today in the ics calendar and mark them as overdue as suggested by Hans de Graaff on the mailing list --- app/views/todos/calendar.ics.erb | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/app/views/todos/calendar.ics.erb b/app/views/todos/calendar.ics.erb index af9d7682..27189470 100644 --- a/app/views/todos/calendar.ics.erb +++ b/app/views/todos/calendar.ics.erb @@ -4,22 +4,28 @@ VERSION:2.0 CALSCALE:GREGORIAN METHOD:PUBLISH X-WR-CALNAME:Tracks -<% for todo in @due_all -%> -BEGIN:VEVENT -DTSTART;VALUE=DATE:<%= todo.due.strftime("%Y%m%d") %> -DTEND;VALUE=DATE:<%= (todo.due+1.day).strftime("%Y%m%d") %> -DTSTAMP:<%= todo.due.strftime("%Y%m%dT%H%M%SZ") %> +<% for todo in @due_all + due_date = todo.due + overdue_text = "" + if due_date < Time.zone.now + due_date = Time.zone.now + overdue_text = "Overdue: " + end +%>BEGIN:VEVENT +DTSTART;VALUE=DATE:<%= due_date.strftime("%Y%m%d") %> +DTEND;VALUE=DATE:<%= (due_date+1.day).strftime("%Y%m%d") %> +DTSTAMP:<%= due_date.strftime("%Y%m%dT%H%M%SZ") %> UID:<%= todo_url(todo) %> CLASS:PUBLIC CATEGORIES:Tracks CREATED:<%= todo.created_at.strftime("%Y%m%dT%H%M%SZ") %> DESCRIPTION:<%= format_ical_notes(todo.notes) %> -LAST-MODIFIED:<%= todo.due.strftime("%Y%m%dT%H%M%SZ") %> +LAST-MODIFIED:<%= due_date.strftime("%Y%m%dT%H%M%SZ") %> LOCATION: SEQUENCE:0 STATUS:CONFIRMED -SUMMARY:<%= todo.description %> +SUMMARY:<%= overdue_text + todo.description %> TRANSP:TRANSPARENT END:VEVENT -<% end -%> -END:VCALENDAR \ No newline at end of file +<% end +%>END:VCALENDAR \ No newline at end of file From ddfc0e5e90bbf9f0701e5a1f5ea1c735dcb8b393 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 7 Oct 2008 09:11:29 +0200 Subject: [PATCH 069/102] fix for actions that are due today were marked overdue in the ical calendar feed --- app/views/todos/calendar.ics.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/todos/calendar.ics.erb b/app/views/todos/calendar.ics.erb index 27189470..46a02f60 100644 --- a/app/views/todos/calendar.ics.erb +++ b/app/views/todos/calendar.ics.erb @@ -7,7 +7,7 @@ X-WR-CALNAME:Tracks <% for todo in @due_all due_date = todo.due overdue_text = "" - if due_date < Time.zone.now + if due_date.at_midnight < Time.zone.now.at_midnight due_date = Time.zone.now overdue_text = "Overdue: " end @@ -28,4 +28,4 @@ SUMMARY:<%= overdue_text + todo.description %> TRANSP:TRANSPARENT END:VEVENT <% end -%>END:VCALENDAR \ No newline at end of file +%>END:VCALENDAR From b00b9282fd4336ebe96fb8e941dbe8d7e98b8f1b Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 7 Oct 2008 09:36:51 +0200 Subject: [PATCH 070/102] update CHANGELOG --- doc/CHANGELOG | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/doc/CHANGELOG b/doc/CHANGELOG index 1dd8e775..05f59965 100644 --- a/doc/CHANGELOG +++ b/doc/CHANGELOG @@ -2,7 +2,7 @@ * Homepage: http://www.rousette.org.uk/projects/ * Author: bsag (http://www.rousette.org.uk/) -* Contributors: Nicholas Lee, Lolindrath, Jim Ray, Arnaud Limbourg, Timothy Martens, Luke Melia, John Leonard, Jim Strupp, Eric Lesh, Damien Cirotteau, Janet Riley, Reinier Balt, Jacqui Maher, James Kebinger, Jeffrey Gipson +* Contributors: Nicholas Lee, Lolindrath, Jim Ray, Arnaud Limbourg, Timothy Martens, Luke Melia, John Leonard, Jim Strupp, Eric Lesh, Damien Cirotteau, Janet Riley, Reinier Balt, Jacqui Maher, James Kebinger, Jeffrey Gipson, Eric Allen * Version: 1.6 * Copyright: (cc) 2004-2008 rousette.org.uk * License: GNU GPL @@ -13,6 +13,20 @@ Trac (for bug reports and feature requests): http://dev.rousette.org.uk/report/6 Wiki (deprecated - please use Trac): http://www.rousette.org.uk/projects/wiki/ +== Version 1.7dev + +New features: +1. Recurring todos +2. Cleanup of feed page and add feed for starred actions +3. Initial importer of yaml files (still very EXPERIMENTAL) +4. New interface to import an email / sms messages into Tracks (needs an email server on the same server as Tracks) +5. New buttons to quickly defer an action 1 or 7 days +6. Calendar view to review due actions, includes iCal feed to use in your calendar app (tested with Google Calendar, Evolution, Outlook 2007) + +Under the hood: +1. Move selenium tests to RSpec +2. Bugfixes + == Version 1.6 1. upgrade to rails 2.0.2 2. new mobile interface (with some iPhone compatibility fixes) From 1118a582d7bd18c471821126c49bf8b6e000c76d Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 7 Oct 2008 20:20:47 +0200 Subject: [PATCH 071/102] add check for actions that should move from tickler to home page to the index page for todos --- app/controllers/todos_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index ada43168..2bf99d63 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -10,6 +10,7 @@ class TodosController < ApplicationController session :off, :only => :index, :if => Proc.new { |req| is_feed_request(req) } def index + current_user.deferred_todos.find_and_activate_ready @projects = current_user.projects.find(:all, :include => [:default_context]) @contexts = current_user.contexts.find(:all) From 24c2b57b4af66176b218f24394421cf4bf8b73f7 Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Sat, 4 Oct 2008 13:41:37 -0400 Subject: [PATCH 072/102] Make migration 42 more resilient: don't freak out when user has no prefs object --- db/migrate/042_change_dates_to_datetimes.rb | 31 +++++++++++++++------ 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/db/migrate/042_change_dates_to_datetimes.rb b/db/migrate/042_change_dates_to_datetimes.rb index 99e75179..57b62ed5 100644 --- a/db/migrate/042_change_dates_to_datetimes.rb +++ b/db/migrate/042_change_dates_to_datetimes.rb @@ -6,18 +6,33 @@ class ChangeDatesToDatetimes < ActiveRecord::Migration change_column :recurring_todos, :end_date, :datetime User.all(:include => [:todos, :recurring_todos]).each do |user| - user.todos.each do |todo| - todo.update_attribute(:show_from, user.at_midnight(todo.show_from)) unless todo.show_from.nil? - todo.update_attribute(:due, user.at_midnight(todo.due)) unless todo.due.nil? - end + if user.prefs + user.todos.each do |todo| + todo.update_attribute(:show_from, user.at_midnight(todo.show_from)) unless todo.show_from.nil? + todo.update_attribute(:due, user.at_midnight(todo.due)) unless todo.due.nil? + end - user.recurring_todos.each do |todo| - todo.update_attribute(:start_from, user.at_midnight(todo.start_from)) unless todo.start_from.nil? - todo.update_attribute(:end_date, user.at_midnight(todo.end_date)) unless todo.end_date.nil? - end + user.recurring_todos.each do |todo| + todo.update_attribute(:start_from, user.at_midnight(todo.start_from)) unless todo.start_from.nil? + todo.update_attribute(:end_date, user.at_midnight(todo.end_date)) unless todo.end_date.nil? + end + else # weird...no preferences for this user + user.todos.each do |todo| + todo.update_attribute(:show_from, at_midnight(todo.show_from)) unless todo.show_from.nil? + todo.update_attribute(:due, at_midnight(todo.due)) unless todo.due.nil? + end + + user.recurring_todos.each do |todo| + todo.update_attribute(:start_from, at_midnight(todo.start_from)) unless todo.start_from.nil? + todo.update_attribute(:end_date, at_midnight(todo.end_date)) unless todo.end_date.nil? + end end end + def at_midnight(date) + return Time.zone.local(date.year, date.month, date.day, 0, 0, 0) + end + def self.down change_column :todos, :show_from, :date change_column :todos, :due, :date From 51b0a2bd9bc3dd310e39ea3e7265850dc83b9570 Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Sat, 4 Oct 2008 13:44:38 -0400 Subject: [PATCH 073/102] Informative alt text for defer buttons --- app/helpers/todos_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index b989b432..aee59c90 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -281,7 +281,7 @@ module TodosHelper end def defer_link(days) - link_to_remote image_tag("defer_#{days}.png"), :url => {:controller => 'todos', :action => 'defer', :id => @todo.id, :days => days, :_source_view => (@source_view.underscore.gsub(/\s+/,'_') rescue "")} + link_to_remote image_tag("defer_#{days}.png", :alt => "Defer #{pluralize(days, 'day')}"), :url => {:controller => 'todos', :action => 'defer', :id => @todo.id, :days => days, :_source_view => (@source_view.underscore.gsub(/\s+/,'_') rescue "")} end end From b36ed968137183bb24b62c23236be9119da17547 Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Wed, 8 Oct 2008 23:47:59 -0400 Subject: [PATCH 074/102] Wow I broke the migration. Oops. --- db/migrate/042_change_dates_to_datetimes.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/migrate/042_change_dates_to_datetimes.rb b/db/migrate/042_change_dates_to_datetimes.rb index 57b62ed5..1ea940b0 100644 --- a/db/migrate/042_change_dates_to_datetimes.rb +++ b/db/migrate/042_change_dates_to_datetimes.rb @@ -26,6 +26,7 @@ class ChangeDatesToDatetimes < ActiveRecord::Migration todo.update_attribute(:start_from, at_midnight(todo.start_from)) unless todo.start_from.nil? todo.update_attribute(:end_date, at_midnight(todo.end_date)) unless todo.end_date.nil? end + end end end From a9a02896a021b652904990db3a60156193031e64 Mon Sep 17 00:00:00 2001 From: Eric Allen Date: Thu, 9 Oct 2008 10:14:26 -0400 Subject: [PATCH 075/102] Another shot at fixing migration 42. Actually tested to work. --- db/migrate/042_change_dates_to_datetimes.rb | 42 ++++++++++----------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/db/migrate/042_change_dates_to_datetimes.rb b/db/migrate/042_change_dates_to_datetimes.rb index 1ea940b0..d534c642 100644 --- a/db/migrate/042_change_dates_to_datetimes.rb +++ b/db/migrate/042_change_dates_to_datetimes.rb @@ -6,32 +6,28 @@ class ChangeDatesToDatetimes < ActiveRecord::Migration change_column :recurring_todos, :end_date, :datetime User.all(:include => [:todos, :recurring_todos]).each do |user| - if user.prefs - user.todos.each do |todo| - todo.update_attribute(:show_from, user.at_midnight(todo.show_from)) unless todo.show_from.nil? - todo.update_attribute(:due, user.at_midnight(todo.due)) unless todo.due.nil? - end - - user.recurring_todos.each do |todo| - todo.update_attribute(:start_from, user.at_midnight(todo.start_from)) unless todo.start_from.nil? - todo.update_attribute(:end_date, user.at_midnight(todo.end_date)) unless todo.end_date.nil? - end - else # weird...no preferences for this user - user.todos.each do |todo| - todo.update_attribute(:show_from, at_midnight(todo.show_from)) unless todo.show_from.nil? - todo.update_attribute(:due, at_midnight(todo.due)) unless todo.due.nil? - end - - user.recurring_todos.each do |todo| - todo.update_attribute(:start_from, at_midnight(todo.start_from)) unless todo.start_from.nil? - todo.update_attribute(:end_date, at_midnight(todo.end_date)) unless todo.end_date.nil? + if !user.prefs ## ugly hack for strange edge-case of not having preferences object + user.instance_eval do + def at_midnight(date) + return Time.zone.local(date.year, date.month, date.day, 0, 0, 0) + end + def time + Time.zone.now + end end end - end - end + user.todos.each do |todo| + todo[:show_from] = user.at_midnight(todo.show_from) unless todo.show_from.nil? + todo[:due] = user.at_midnight(todo.due) unless todo.due.nil? + todo.save_with_validation(false) + end - def at_midnight(date) - return Time.zone.local(date.year, date.month, date.day, 0, 0, 0) + user.recurring_todos.each do |todo| + todo[:start_from] = user.at_midnight(todo.start_from) unless todo.start_from.nil? + todo[:end_date] = user.at_midnight(todo.end_date) unless todo.end_date.nil? + todo.save_with_validation(false) + end + end end def self.down From c64bbb9e60defa4a96b33f1d8dc230870805efe8 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 14 Oct 2008 10:57:12 +0200 Subject: [PATCH 076/102] fix corner case for yearly repeating todos also adds test for this case --- app/models/recurring_todo.rb | 11 ++++++----- test/unit/recurring_todo_test.rb | 3 +++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/models/recurring_todo.rb b/app/models/recurring_todo.rb index 86b24dae..0b103b03 100644 --- a/app/models/recurring_todo.rb +++ b/app/models/recurring_todo.rb @@ -517,17 +517,18 @@ class RecurringTodo < ActiveRecord::Base end def get_yearly_date(previous) - start = determine_start(previous) day = self.every_other1 month = self.every_other2 case self.recurrence_selector when 0 # specific day of a specific month - # if there is no next month n in this year, search in next year - if start.month >= month - start = Time.zone.local(start.year+1, month, 1) if start.day >= day - start = Time.zone.local(start.year, month, 1) if start.day <= day + if start.month > month || (start.month == month && start.day > day) + # if there is no next month n and day m in this year, search in next year + start = Time.zone.local(start.year+1, month, 1) + else + # if there is a next month n, stay in this year + start = Time.zone.local(start.year, month, 1) end return Time.zone.local(start.year, month, day) diff --git a/test/unit/recurring_todo_test.rb b/test/unit/recurring_todo_test.rb index bd256eba..82ba1cb5 100644 --- a/test/unit/recurring_todo_test.rb +++ b/test/unit/recurring_todo_test.rb @@ -174,6 +174,9 @@ class RecurringTodoTest < Test::Rails::TestCase # same month, day after due_date = @yearly.get_due_date(@monday) # june 9th assert_equal Time.zone.local(2009,6,8), due_date # june 8th next year + # very overdue + due_date = @yearly.get_due_date(@monday+5.months-2.days) # november 7 + assert_equal Time.zone.local(2009,6,8), due_date # june 8th next year @yearly.recurrence_selector = 1 @yearly.every_other3 = 2 # second From ce671f23f481093566dec6fa4ea430f6a47943ce Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 14 Oct 2008 22:49:17 +0200 Subject: [PATCH 077/102] fix bug introduced by last commit and add test for it also refactor check_for_next_todo a bit to depend less on globals --- app/controllers/todos_controller.rb | 23 +++--- app/models/recurring_todo.rb | 2 +- test/fixtures/todos.yml | 14 ++++ .../recurring_todos_controller_test.rb | 7 +- test/functional/stats_controller_test.rb | 2 +- test/functional/todos_controller_test.rb | 77 +++++++++++++++---- test/unit/context_test.rb | 6 +- test/unit/project_test.rb | 4 +- 8 files changed, 99 insertions(+), 36 deletions(-) diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 2bf99d63..b4cd1935 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -134,7 +134,7 @@ class TodosController < ApplicationController @saved = @todo.toggle_completion! # check if this todo has a related recurring_todo. If so, create next todo - check_for_next_todo if @saved + @new_recurring_todo = check_for_next_todo(@todo) if @saved respond_to do |format| format.js do @@ -279,7 +279,7 @@ class TodosController < ApplicationController @project_id = @todo.project_id # check if this todo has a related recurring_todo. If so, create next todo - check_for_next_todo + @new_recurring_todo = check_for_next_todo(@todo) @saved = @todo.destroy @@ -771,22 +771,22 @@ class TodosController < ApplicationController ['rss','atom','txt','ics'].include?(req.parameters[:format]) end - def check_for_next_todo + def check_for_next_todo(todo) # check if this todo has a related recurring_todo. If so, create next todo - @new_recurring_todo = nil - @recurring_todo = nil - if @todo.from_recurring_todo? - @recurring_todo = current_user.recurring_todos.find(@todo.recurring_todo_id) + new_recurring_todo = nil + recurring_todo = nil + if todo.from_recurring_todo? + recurring_todo = current_user.recurring_todos.find(todo.recurring_todo_id) # check for next todo either from the due date or the show_from date - date_to_check = @todo.due.nil? ? @todo.show_from : @todo.due + date_to_check = todo.due.nil? ? todo.show_from : todo.due # if both due and show_from are nil, check for a next todo with yesterday # as reference point. We pick yesterday so that new todos for today will # be created instead of new todos for tomorrow. date_to_check = Time.zone.now-1.day if date_to_check.nil? - if @recurring_todo.active? && @recurring_todo.has_next_todo(date_to_check) + if recurring_todo.active? && recurring_todo.has_next_todo(date_to_check) # shift the reference date to yesterday if date_to_check is furher in # the past. This is to make sure we do not get older todos for overdue @@ -795,9 +795,10 @@ class TodosController < ApplicationController # date. Discard the time part in the compare date = date_to_check.at_midnight >= Time.zone.now.at_midnight ? date_to_check : Time.zone.now-1.day - @new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo, date) + new_recurring_todo = create_todo_from_recurring_todo(recurring_todo, date) end - end + end + return new_recurring_todo end def get_due_id_for_calendar(due) diff --git a/app/models/recurring_todo.rb b/app/models/recurring_todo.rb index 0b103b03..fbe2f07a 100644 --- a/app/models/recurring_todo.rb +++ b/app/models/recurring_todo.rb @@ -523,7 +523,7 @@ class RecurringTodo < ActiveRecord::Base case self.recurrence_selector when 0 # specific day of a specific month - if start.month > month || (start.month == month && start.day > day) + if start.month > month || (start.month == month && start.day >= day) # if there is no next month n and day m in this year, search in next year start = Time.zone.local(start.year+1, month, 1) else diff --git a/test/fixtures/todos.yml b/test/fixtures/todos.yml index 52db71ea..f4941c0c 100644 --- a/test/fixtures/todos.yml +++ b/test/fixtures/todos.yml @@ -230,3 +230,17 @@ end completed_at: ~ show_from: <%= next_week %> user_id: 2 + +18: + id: 18 + user_id: 1 + context_id: 1 + project_id: 2 + description: Call Bill Gates every day + notes: ~ + state: active + created_at: <%= last_week %> + due: <%= last_week %> + completed_at: ~ + show_from: ~ + recurring_todo_id: 1 \ No newline at end of file diff --git a/test/functional/recurring_todos_controller_test.rb b/test/functional/recurring_todos_controller_test.rb index e9485c5b..150bce59 100644 --- a/test/functional/recurring_todos_controller_test.rb +++ b/test/functional/recurring_todos_controller_test.rb @@ -79,6 +79,11 @@ class RecurringTodosControllerTest < ActionController::TestCase recurring_todo_1 = RecurringTodo.find(1) assert recurring_todo_1.completed? + # remove remaining todo + todo = Todo.find_by_recurring_todo_id(1) + todo.recurring_todo_id = 2 + todo.save + todo_count = Todo.count # mark as active @@ -119,5 +124,5 @@ class RecurringTodosControllerTest < ActionController::TestCase # show_from should be nil since now+4.days-10.days is in the past assert_equal nil, new_todo.show_from end - + end diff --git a/test/functional/stats_controller_test.rb b/test/functional/stats_controller_test.rb index a3cf9645..f3f2c64a 100755 --- a/test/functional/stats_controller_test.rb +++ b/test/functional/stats_controller_test.rb @@ -56,7 +56,7 @@ class StatsControllerTest < Test::Unit::TestCase assert_equal 3, assigns['projects'].count assert_equal 3, assigns['projects'].count(:conditions => "state = 'active'") assert_equal 10, assigns['contexts'].count - assert_equal 15, assigns['actions'].count + assert_equal 16, assigns['actions'].count assert_equal 4, assigns['tags'].count assert_equal 2, assigns['unique_tags'].size assert_equal 2.week.ago.utc.beginning_of_day, assigns['first_action'].created_at diff --git a/test/functional/todos_controller_test.rb b/test/functional/todos_controller_test.rb index cd4fba01..0b5d8b1a 100644 --- a/test/functional/todos_controller_test.rb +++ b/test/functional/todos_controller_test.rb @@ -124,9 +124,9 @@ class TodosControllerTest < Test::Rails::TestCase def test_update_todo_to_deferred_is_reflected_in_badge_count login_as(:admin_user) get :index - assert_equal 10, assigns['count'] + assert_equal 11, assigns['count'] xhr :post, :update, :id => 1, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"Make more money than Billy Gates", "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006", "show_from"=>"30/11/2030"}, "tag_list"=>"foo bar" - assert_equal 9, assigns['down_count'] + assert_equal 10, assigns['down_count'] end def test_update_todo @@ -188,7 +188,7 @@ class TodosControllerTest < Test::Rails::TestCase assert_select '>description', "Actions for #{users(:admin_user).display_name}" assert_select 'language', 'en-us' assert_select 'ttl', '40' - assert_select 'item', 10 do + assert_select 'item', 11 do assert_select 'title', /.+/ assert_select 'description', /.*/ assert_select 'link', %r{http://test.host/contexts/.+} @@ -242,7 +242,7 @@ class TodosControllerTest < Test::Rails::TestCase assert_xml_select 'feed[xmlns="http://www.w3.org/2005/Atom"]' do assert_xml_select '>title', 'Tracks Actions' assert_xml_select '>subtitle', "Actions for #{users(:admin_user).display_name}" - assert_xml_select 'entry', 10 do + assert_xml_select 'entry', 11 do assert_xml_select 'title', /.+/ assert_xml_select 'content[type="html"]', /.*/ assert_xml_select 'published', /(#{Regexp.escape(projects(:timemachine).updated_at.xmlschema)}|#{Regexp.escape(projects(:moremoney).updated_at.xmlschema)})/ @@ -311,7 +311,7 @@ class TodosControllerTest < Test::Rails::TestCase def test_mobile_index_assigns_down_count login_as(:admin_user) get :index, { :format => "m" } - assert_equal 10, assigns['down_count'] + assert_equal 11, assigns['down_count'] end def test_mobile_create_action_creates_a_new_todo @@ -362,38 +362,81 @@ class TodosControllerTest < Test::Rails::TestCase # link todo_1 and recurring_todo_1 recurring_todo_1 = RecurringTodo.find(1) - todo_1 = Todo.find(1) - todo_1.recurring_todo_id = recurring_todo_1.id - - # update todo_1 - assert todo_1.save + todo_1 = Todo.find_by_recurring_todo_id(1) # mark todo_1 as complete by toggle_check - xhr :post, :toggle_check, :id => 1, :_source_view => 'todo' + xhr :post, :toggle_check, :id => todo_1.id, :_source_view => 'todo' todo_1.reload assert todo_1.completed? + # check that there is only one active todo belonging to recurring_todo + count = Todo.count(:all, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'}) + assert_equal 1, count + # check there is a new todo linked to the recurring pattern next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'}) assert_equal "Call Bill Gates every day", next_todo.description + # check that the new todo is not the same as todo_1 + assert_not_equal todo_1.id, next_todo.id - # change recurrence pattern to weekly and set show_from 2 days befor due date - # this forces the next todo to be put in the tickler + # change recurrence pattern to monthly and set show_from 2 days before due + # date this forces the next todo to be put in the tickler recurring_todo_1.show_from_delta = 2 - recurring_todo_1.recurring_period = 'weekly' - recurring_todo_1.every_day = 'smtwtfs' + recurring_todo_1.recurring_period = 'monthly' + recurring_todo_1.recurrence_selector = 0 + recurring_todo_1.every_other1 = 1 + recurring_todo_1.every_other2 = 2 + recurring_todo_1.every_other3 = 5 recurring_todo_1.save - + # mark next_todo as complete by toggle_check xhr :post, :toggle_check, :id => next_todo.id, :_source_view => 'todo' next_todo.reload assert next_todo.completed? + # check that there are three todos belonging to recurring_todo: two + # completed and one deferred + count = Todo.count(:all, :conditions => {:recurring_todo_id => recurring_todo_1.id}) + assert_equal 3, count + # check there is a new todo linked to the recurring pattern in the tickler next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'deferred'}) + assert !next_todo.nil? assert_equal "Call Bill Gates every day", next_todo.description # check that the todo is in the tickler assert !next_todo.show_from.nil? end - + + def test_check_for_next_todo + login_as :admin_user + + recurring_todo_1 = RecurringTodo.find(5) + @todo = Todo.find_by_recurring_todo_id(1) + assert @todo.from_recurring_todo? + # rewire @todo to yearly recurring todo + @todo.recurring_todo_id = 5 + + # make todo due tomorrow and change recurring date also to tomorrow + @todo.due = Time.zone.now + 1.day + @todo.save + recurring_todo_1.every_other1 = @todo.due.day + recurring_todo_1.every_other2 = @todo.due.month + recurring_todo_1.save + + # mark todo complete + xhr :post, :toggle_check, :id => @todo.id, :_source_view => 'todo' + @todo.reload + assert @todo.completed? + + # check that there is no active todo + next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'}) + assert next_todo.nil? + + # check for new deferred todo + next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'deferred'}) + assert !next_todo.nil? + # check that the due date of the new todo is later than tomorrow + assert next_todo.due > @todo.due + end + end diff --git a/test/unit/context_test.rb b/test/unit/context_test.rb index 4952d8c6..121a7c29 100644 --- a/test/unit/context_test.rb +++ b/test/unit/context_test.rb @@ -53,7 +53,7 @@ class ContextTest < Test::Rails::TestCase end def test_delete_context_deletes_todos_within_it - assert_equal 6, @agenda.todos.count + assert_equal 7, @agenda.todos.count agenda_todo_ids = @agenda.todos.collect{|t| t.id } @agenda.destroy agenda_todo_ids.each do |todo_id| @@ -62,11 +62,11 @@ class ContextTest < Test::Rails::TestCase end def test_not_done_todos - assert_equal 5, @agenda.not_done_todos.size + assert_equal 6, @agenda.not_done_todos.size t = @agenda.not_done_todos[0] t.complete! t.save! - assert_equal 4, Context.find(@agenda.id).not_done_todos.size + assert_equal 5, Context.find(@agenda.id).not_done_todos.size end def test_done_todos diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb index 966c8c6d..58f4f794 100644 --- a/test/unit/project_test.rb +++ b/test/unit/project_test.rb @@ -172,9 +172,9 @@ class ProjectTest < Test::Rails::TestCase def test_not_done_todo_count assert_equal 2, @timemachine.not_done_todo_count - assert_equal 3, @moremoney.not_done_todo_count + assert_equal 4, @moremoney.not_done_todo_count @moremoney.todos[0].complete! - assert_equal 2, @moremoney.not_done_todo_count + assert_equal 3, @moremoney.not_done_todo_count end def test_default_context_name From c5cff97f3f280cb9b1d01566ab47d44f5f01d0d7 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Tue, 21 Oct 2008 09:11:21 +0200 Subject: [PATCH 078/102] fixes #785 --- app/views/integrations/_applescript2.rhtml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/integrations/_applescript2.rhtml b/app/views/integrations/_applescript2.rhtml index d63ee561..e883f998 100644 --- a/app/views/integrations/_applescript2.rhtml +++ b/app/views/integrations/_applescript2.rhtml @@ -8,9 +8,9 @@ the newly created action. *) (* Edit appropriately for your setup *) -property myUsername to "<%= current_user.login %>" -property myToken to "<%= current_user.token %>" -property myContextID to <%= context.id %> (* <%= context.name %> *) +property myUsername : "<%= current_user.login %>" +property myToken : "<%= current_user.token %>" +property myContextID : <%= context.id %> (* <%= context.name %> *) -- this string is used when the message subject is empty property emptySubject : "No Subject Specified" From a3f23c60e49c2c4bb50938209fe88c5b78918f1f Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Wed, 22 Oct 2008 10:16:31 +0200 Subject: [PATCH 079/102] fixes #786 where timezone handling of RoR screwed the algorithm to find the last sunday of march in 2009 Fix is a bit of a hack, see #786 for futher explanation. --- app/controllers/application.rb | 3 +- app/models/recurring_todo.rb | 11 ++-- .../recurring_todos_controller_test.rb | 57 ++++++++++++++++++- test/unit/recurring_todo_test.rb | 9 +++ 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/app/controllers/application.rb b/app/controllers/application.rb index b08dc0be..31a370e0 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -170,11 +170,12 @@ class ApplicationController < ActionController::Base # set dates todo.recurring_todo_id = rt.id todo.due = rt.get_due_date(date) - # make sure that show_from is not in the past + 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 diff --git a/app/models/recurring_todo.rb b/app/models/recurring_todo.rb index fbe2f07a..fdd230e7 100644 --- a/app/models/recurring_todo.rb +++ b/app/models/recurring_todo.rb @@ -495,12 +495,14 @@ class RecurringTodo < ActiveRecord::Base def get_xth_day_of_month(x, weekday, month, year) if x == 5 - # last -> count backwards - last_day = Time.zone.local(year, month, Time.days_in_month(month)) + # last -> count backwards. use UTC to avoid strange timezone oddities + # where last_day -= 1.day seems to shift tz+0100 to tz+0000 + last_day = Time.utc(year, month, Time.days_in_month(month)) while last_day.wday != weekday last_day -= 1.day end - return last_day + # convert back to local timezone + return Time.zone.local(last_day.year, last_day.month, last_day.day) else # 1-4th -> count upwards start = Time.zone.local(year,month,1) @@ -524,7 +526,8 @@ class RecurringTodo < ActiveRecord::Base case self.recurrence_selector when 0 # specific day of a specific month if start.month > month || (start.month == month && start.day >= day) - # if there is no next month n and day m in this year, search in next year + # if there is no next month n and day m in this year, search in next + # year start = Time.zone.local(start.year+1, month, 1) else # if there is a next month n, stay in this year diff --git a/test/functional/recurring_todos_controller_test.rb b/test/functional/recurring_todos_controller_test.rb index 150bce59..2bd67e67 100644 --- a/test/functional/recurring_todos_controller_test.rb +++ b/test/functional/recurring_todos_controller_test.rb @@ -79,7 +79,7 @@ class RecurringTodosControllerTest < ActionController::TestCase recurring_todo_1 = RecurringTodo.find(1) assert recurring_todo_1.completed? - # remove remaining todo + # remove remaining todo todo = Todo.find_by_recurring_todo_id(1) todo.recurring_todo_id = 2 todo.save @@ -124,5 +124,60 @@ class RecurringTodosControllerTest < ActionController::TestCase # show_from should be nil since now+4.days-10.days is in the past assert_equal nil, new_todo.show_from end + + def test_last_sunday_of_march + # this test is a duplicate of the unit test. Only this test covers the + # codepath in the controllers + + login_as(:admin_user) + + orig_rt_count = RecurringTodo.count + orig_todo_count = Todo.count + + put :create, + "context_name"=>"library", + "project_name"=>"Build a working time machine", + "recurring_todo" => + { + "daily_every_x_days"=>"1", + "daily_selector"=>"daily_every_x_day", + "description"=>"new recurring pattern", + "end_date" => "", + "ends_on" => "no_end_date", + "monthly_day_of_week" => "1", + "monthly_every_x_day" => "22", + "monthly_every_x_month2" => "1", + "monthly_every_x_month" => "1", + "monthly_every_xth_day"=>"1", + "monthly_selector"=>"monthly_every_x_day", + "notes"=>"with some notes", + "number_of_occurences" => "", + "recurring_period"=>"yearly", + "recurring_show_days_before"=>"0", + "recurring_target"=>"due_date", + "start_from"=>"", + "weekly_every_x_week"=>"1", + "weekly_return_monday"=>"w", + "yearly_day_of_week"=>"0", + "yearly_every_x_day"=>"22", + "yearly_every_xth_day"=>"5", + "yearly_month_of_year2"=>"3", + "yearly_month_of_year"=>"10", + "yearly_selector"=>"yearly_every_xth_day" + }, + "tag_list"=>"one, two, three, four" + + # check new recurring todo added + assert_equal orig_rt_count+1, RecurringTodo.count + # check new todo added + assert_equal orig_todo_count+1, Todo.count + + # find the newly created todo + new_todo = Todo.find_by_description("new recurring pattern") + assert !new_todo.nil? + + # the date should be 29 march 2009 + assert_equal Time.zone.local(2009,3,29), new_todo.due + end end diff --git a/test/unit/recurring_todo_test.rb b/test/unit/recurring_todo_test.rb index 82ba1cb5..80106554 100644 --- a/test/unit/recurring_todo_test.rb +++ b/test/unit/recurring_todo_test.rb @@ -197,6 +197,15 @@ class RecurringTodoTest < Test::Rails::TestCase assert_equal due_date1, due_date2 end + def test_last_sunday_of_march + @yearly.recurrence_selector = 1 + @yearly.every_other2 = 3 # march + @yearly.every_other3 = 5 # last + @yearly.every_count = 0 # sunday + due_date = @yearly.get_due_date(Time.zone.local(2008,10,1)) # oct 1st + assert_equal Time.zone.local(2009,3,29), due_date # march 29th + end + def test_start_from_in_future # every_day should return start_day if it is in the future @every_day.start_from = @in_three_days From e82be2e52214c90762f5d702f9361694afdc1262 Mon Sep 17 00:00:00 2001 From: waltercruz Date: Fri, 24 Oct 2008 10:25:04 -0200 Subject: [PATCH 080/102] Adding test to the sort by number of todos --- test/functional/projects_controller_test.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index f97a892a..1b3fe5db 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -218,6 +218,15 @@ class ProjectsControllerTest < TodoContainerControllerTestBase get :index, { :format => "txt", :token => users(:admin_user).token } assert_response :ok end + + def test_actionize_sorts_active_projects_by_number_of_tasks + login_as :admin_user + u = users(:admin_user) + post :actionize, :state => "active", :format => 'js' + assert_equal 1, projects(:gardenclean).position + assert_equal 2, projects(:moremoney).position + assert_equal 3, projects(:timemachine).position + end def test_alphabetize_sorts_active_projects_alphabetically login_as :admin_user From 385f34d07110ca0876fa22b6650b5449bfd695ae Mon Sep 17 00:00:00 2001 From: waltercruz Date: Sat, 25 Oct 2008 17:43:52 -0200 Subject: [PATCH 081/102] Taking care of css --- app/views/projects/_project_state_group.rhtml | 8 ++++---- public/stylesheets/standard.css | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/views/projects/_project_state_group.rhtml b/app/views/projects/_project_state_group.rhtml index c4a0f0b1..e5393399 100644 --- a/app/views/projects/_project_state_group.rhtml +++ b/app/views/projects/_project_state_group.rhtml @@ -1,5 +1,6 @@
    >

    <%= project_state_group.length %><%= state.titlecase %> Projects

    +
    <%= render :partial => 'project_listing', :collection => project_state_group %> diff --git a/public/stylesheets/standard.css b/public/stylesheets/standard.css index 3b31fcd7..ea8f0dc5 100644 --- a/public/stylesheets/standard.css +++ b/public/stylesheets/standard.css @@ -692,11 +692,14 @@ div#list-active-projects, div#list-hidden-projects, div#list-completed-projects, margin:20px 0px 8px 13px } -div.alpha_sort { +div.menu_sort { margin-top:-20px; float:right; } +div.alpha_sort, div.tasks_sort,span.sort_separator { + float:left; +} .container td { border: none; From 0b57b23b2d1aec73003773152f38725d23fdb743 Mon Sep 17 00:00:00 2001 From: waltercruz Date: Tue, 28 Oct 2008 16:40:28 -0200 Subject: [PATCH 082/102] html fixes and sort by number of todos logic --- app/controllers/projects_controller.rb | 11 ++++++++++- app/models/user.rb | 5 ++--- app/views/projects/_project_state_group.rhtml | 6 +++--- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index ed2128de..cab0bd10 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -188,7 +188,16 @@ class ProjectsController < ApplicationController def actionize @state = params['state'] - @projects = current_user.projects.actionize(:state => @state) if @state + query_state = '' + query_state = 'AND project.state = "' + params['state'] +'" 'if @state + projects = Project.find_by_sql([ + "SELECT project.id, count(todo.id) as p_count " + + "FROM projects as project " + + "LEFT OUTER JOIN todos as todo ON todo.project_id = project.id "+ + "WHERE project.user_id = ? " + + query_state + + " GROUP BY project.id ORDER by p_count DESC",current_user.id]) + @projects = current_user.projects.actionize(projects,:state => @state) if @state @contexts = current_user.contexts init_not_done_counts(['project']) end diff --git a/app/models/user.rb b/app/models/user.rb index 4b1f41e7..59e3cdbc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -51,10 +51,9 @@ class User < ActiveRecord::Base self.update_positions(projects.map{ |p| p.id }) return projects end - def actionize(scope_conditions = {}) + def actionize(proj,scope_conditions = {}) + self.update_positions(proj.map{ |p| p.id }) projects = find(:all, :conditions => scope_conditions) - projects.sort!{ |x,y| y.todos.count(:state == 'active') <=> x.todos.count(:state == 'active') } - self.update_positions(projects.map{ |p| p.id }) return projects end end diff --git a/app/views/projects/_project_state_group.rhtml b/app/views/projects/_project_state_group.rhtml index e5393399..32693f0f 100644 --- a/app/views/projects/_project_state_group.rhtml +++ b/app/views/projects/_project_state_group.rhtml @@ -1,8 +1,8 @@
    >

    <%= project_state_group.length %><%= state.titlecase %> Projects

    -