diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 3d26cdd0..b24dfd54 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -9,12 +9,14 @@ 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 CannotAccessContext < RuntimeError; end class ApplicationController < ActionController::Base @@ -113,7 +115,7 @@ class ApplicationController < ActionController::Base def format_date(date) if date date_format = prefs.date_format - formatted_date = date.strftime("#{date_format}") + formatted_date = date.in_time_zone(prefs.time_zone).strftime("#{date_format}") else formatted_date = '' end @@ -160,7 +162,39 @@ 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) + + show_from_date = rt.get_show_from_date(date) + if show_from_date.nil? + todo.show_from=nil + else + # make sure that show_from is not in the past + todo.show_from = show_from_date < Time.zone.now ? nil : show_from_date + end + + saved = todo.save + if saved + todo.tag_with(rt.tag_list, 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 +226,7 @@ class ApplicationController < ActionController::Base def openid_enabled? self.class.openid_enabled? end - + private def parse_date_per_user_prefs( s ) @@ -231,29 +265,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/app/controllers/backend_controller.rb b/app/controllers/backend_controller.rb index cc4c605e..206f2e81 100644 --- a/app/controllers/backend_controller.rb +++ b/app/controllers/backend_controller.rb @@ -14,36 +14,10 @@ class BackendController < ApplicationController def new_rich_todo(username, token, default_context_id, description, notes) check_token(username,token) - description,context = split_by_char('@',description) - description,project = split_by_char('>',description) - if(!context.nil? && project.nil?) - context,project = split_by_char('>',context) - end -# logger.info("context='#{context}' project='#{project}") - - context_id = default_context_id - unless(context.nil?) - found_context = @user.active_contexts.find_by_namepart(context) - found_context = @user.contexts.find_by_namepart(context) if found_context.nil? - context_id = found_context.id unless found_context.nil? - end - check_context_belongs_to_user(context_id) - - project_id = nil - unless(project.blank?) - if(project[0..3].downcase == "new:") - found_project = @user.projects.build - found_project.name = project[4..255+4].strip - found_project.save! - else - found_project = @user.active_projects.find_by_namepart(project) - found_project = @user.projects.find_by_namepart(project) if found_project.nil? - end - project_id = found_project.id unless found_project.nil? - end - - todo = create_todo(description, context_id, project_id, notes) - todo.id + item = Todo.from_rich_message(@user, default_context_id, description, notes) + item.save + raise item.errors.full_messages.to_s if item.new_record? + item.id end def list_contexts(username, token) @@ -84,25 +58,6 @@ class BackendController < ApplicationController raise item.errors.full_messages.to_s if item.new_record? item end - - def split_by_char(separator,string) - parts = string.split(separator) - - # if the separator is used more than once, concat the last parts this is - # needed to get 'description @ @home > project' working for contexts - # starting with @ - if parts.length > 2 - 2.upto(parts.length-1) { |i| parts[1] += (separator +parts[i]) } - end - - return safe_strip(parts[0]), safe_strip(parts[1]) - end - - def safe_strip(s) - s.strip! unless s.nil? - s - end end class InvalidToken < RuntimeError; end -class CannotAccessContext < RuntimeError; end diff --git a/app/controllers/contexts_controller.rb b/app/controllers/contexts_controller.rb index 4a9984b8..d09883a4 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 @@ -130,10 +134,10 @@ class ContextsController < ApplicationController def render_contexts_mobile lambda do @page_title = "TRACKS::List Contexts" - @active_contexts = @contexts.find(:all, { :conditions => ["hide = ?", false]}) - @hidden_contexts = @contexts.find(:all, { :conditions => ["hide = ?", true]}) + @active_contexts = @contexts.active + @hidden_contexts = @contexts.hidden @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 +147,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/data_controller.rb b/app/controllers/data_controller.rb index 47a85fd6..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 @@ -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 @@ -34,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) @@ -58,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) @@ -81,6 +83,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 @@ -102,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 @@ -151,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 } @@ -180,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 } @@ -196,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/controllers/feedlist_controller.rb b/app/controllers/feedlist_controller.rb index 68c7118c..4c2924e9 100644 --- a/app/controllers/feedlist_controller.rb +++ b/app/controllers/feedlist_controller.rb @@ -12,12 +12,12 @@ class FeedlistController < ApplicationController @contexts = current_user.contexts end - @active_projects = @projects.select{ |p| p.active? } - @hidden_projects = @projects.select{ |p| p.hidden? } - @completed_projects = @projects.select{ |p| p.completed? } + @active_projects = current_user.projects.active + @hidden_projects = current_user.projects.hidden + @completed_projects = current_user.projects.completed - @active_contexts = @contexts.select{ |c| !c.hidden? } - @hidden_contexts = @contexts.select{ |c| c.hidden? } + @active_contexts = current_user.contexts.active + @hidden_contexts = current_user.contexts.hidden respond_to do |format| format.html { render :layout => 'standard' } 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..9443168f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -31,7 +31,7 @@ class ProjectsController < ApplicationController end def projects_and_actions - @projects = @projects.select { |p| p.active? } + @projects = @projects.active respond_to do |format| format.text { render :action => 'index_text_projects_and_actions', :layout => false, :content_type => Mime::TEXT @@ -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 @@ -83,7 +83,7 @@ class ProjectsController < ApplicationController @go_to_project = params['go_to_project'] @saved = @project.save @project_not_done_counts = { @project.id => 0 } - @active_projects_count = current_user.projects.count(:conditions => "state = 'active'") + @active_projects_count = current_user.projects.active.count @contexts = current_user.contexts respond_to do |format| format.js { @down_count = current_user.projects.size } @@ -124,9 +124,9 @@ class ProjectsController < ApplicationController @project_not_done_counts[@project.id] = @project.reload().not_done_todo_count(:include_project_hidden_todos => true) end @contexts = current_user.contexts - @active_projects_count = current_user.projects.count(:conditions => "state = 'active'") - @hidden_projects_count = current_user.projects.count(:conditions => "state = 'hidden'") - @completed_projects_count = current_user.projects.count(:conditions => "state = 'completed'") + @active_projects_count = current_user.projects.active.count + @hidden_projects_count = current_user.projects.hidden.count + @completed_projects_count = current_user.projects.completed.count render :template => 'projects/update.js.rjs' return elsif boolean_param('update_status') @@ -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 @@ -157,9 +161,9 @@ class ProjectsController < ApplicationController def destroy @project.destroy - @active_projects_count = current_user.projects.count(:conditions => "state = 'active'") - @hidden_projects_count = current_user.projects.count(:conditions => "state = 'hidden'") - @completed_projects_count = current_user.projects.count(:conditions => "state = 'completed'") + @active_projects_count = current_user.projects.active.count + @hidden_projects_count = current_user.projects.hidden.count + @completed_projects_count = current_user.projects.completed.count respond_to do |format| format.js { @down_count = current_user.projects.size } format.xml { render :text => "Deleted project #{@project.name}" } @@ -182,15 +186,22 @@ class ProjectsController < ApplicationController init_not_done_counts(['project']) end + def actionize + @state = params['state'] + @projects = current_user.projects.actionize(current_user.id, :state => @state) if @state + @contexts = current_user.contexts + init_not_done_counts(['project']) + end + protected def render_projects_html lambda do @page_title = "TRACKS::List Projects" @count = current_user.projects.size - @active_projects = @projects.select{ |p| p.active? } - @hidden_projects = @projects.select{ |p| p.hidden? } - @completed_projects = @projects.select{ |p| p.completed? } + @active_projects = @projects.active + @hidden_projects = @projects.hidden + @completed_projects = @projects.completed @no_projects = @projects.empty? @projects.cache_note_counts @new_project = current_user.projects.build @@ -200,11 +211,11 @@ class ProjectsController < ApplicationController def render_projects_mobile lambda do - @active_projects = @projects.select{ |p| p.active? } - @hidden_projects = @projects.select{ |p| p.hidden? } - @completed_projects = @projects.select{ |p| p.completed? } + @active_projects = @projects.active + @hidden_projects = @projects.hidden + @completed_projects = @projects.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 +228,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/recurring_todos_controller.rb b/app/controllers/recurring_todos_controller.rb index 763ed06b..668e6acb 100644 --- a/app/controllers/recurring_todos_controller.rb +++ b/app/controllers/recurring_todos_controller.rb @@ -6,8 +6,10 @@ class RecurringTodosController < ApplicationController append_before_filter :get_recurring_todo_from_param, :only => [:destroy, :toggle_check, :toggle_star, :edit, :update] def index - @recurring_todos = current_user.recurring_todos.find(:all, :conditions => ["state = ?", "active"]) - @completed_recurring_todos = current_user.recurring_todos.find(:all, :conditions => ["state = ?", "completed"]) + find_and_inactivate + + @recurring_todos = current_user.recurring_todos.active + @completed_recurring_todos = current_user.recurring_todos.completed @no_recurring_todos = @recurring_todos.size == 0 @no_completed_recurring_todos = @completed_recurring_todos.size == 0 @count = @recurring_todos.size @@ -36,8 +38,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 +86,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) @@ -113,7 +118,7 @@ class RecurringTodosController < ApplicationController else @message += " / did not create todo" end - @count = current_user.recurring_todos.count(:all, :conditions => ["state = ?", "active"]) + @count = current_user.recurring_todos.active.count else @message = "Error saving recurring todo" end @@ -126,7 +131,7 @@ class RecurringTodosController < ApplicationController def destroy # remove all references to this recurring todo - @todos = current_user.todos.find(:all, {:conditions => ["recurring_todo_id = ?", params[:id]]}) + @todos = @recurring_todo.todos @number_of_todos = @todos.size @todos.each do |t| t.recurring_todo_id = nil @@ -135,7 +140,7 @@ class RecurringTodosController < ApplicationController # delete the recurring todo @saved = @recurring_todo.destroy - @remaining = current_user.recurring_todos.count(:all) + @remaining = current_user.recurring_todos.count respond_to do |format| @@ -158,14 +163,15 @@ class RecurringTodosController < ApplicationController def toggle_check @saved = @recurring_todo.toggle_completion! - @count = current_user.recurring_todos.count(:all, :conditions => ["state = ?", "active"]) + @count = current_user.recurring_todos.active.count @remaining = @count if @recurring_todo.active? - @remaining = current_user.recurring_todos.count(:all, :conditions => ["state = ?", 'completed']) + @remaining = current_user.recurring_todos.completed.count # from completed back to active -> check if there is an active todo - @active_todos = current_user.todos.count(:all, {:conditions => ["state = ? AND recurring_todo_id = ?", 'active',params[:id]]}) + # current_user.todos.count(:all, {:conditions => ["state = ? AND recurring_todo_id = ?", 'active',params[:id]]}) + @active_todos = @recurring_todo.todos.active.count # create todo if there is no active todo belonging to the activated # recurring_todo @new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo) if @active_todos == 0 @@ -252,5 +258,11 @@ class RecurringTodosController < ApplicationController def get_recurring_todo_from_param @recurring_todo = current_user.recurring_todos.find(params[:id]) end + + def find_and_inactivate + # find active recurring todos without active todos and inactivate them + recurring_todos = current_user.recurring_todos.active + recurring_todos.each { |rt| rt.toggle_completion! if rt.todos.not_completed.count == 0} + end end diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index c74811d4..2d0f95c6 100755 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -8,7 +8,7 @@ class StatsController < ApplicationController @page_title = 'TRACKS::Statistics' @unique_tags = @tags.count(:all, {:group=>"tag_id"}) - @hidden_contexts = @contexts.find(:all, {:conditions => ["hide = ? ", true]}) + @hidden_contexts = @contexts.hidden @first_action = @actions.find(:first, :order => "created_at ASC") get_stats_actions diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 49d9d0ca..6038f3f3 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -2,18 +2,19 @@ class TodosController < ApplicationController helper :todos - 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 ] + skip_before_filter :login_required, :only => [:index, :calendar] + prepend_before_filter :login_or_feed_token_required, :only => [:index, :calendar] + 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) } 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) - @contexts_to_show = @contexts.reject {|x| x.hide? } + @contexts_to_show = current_user.contexts.active respond_to do |format| format.html &render_todos_html @@ -27,7 +28,7 @@ class TodosController < ApplicationController end def new - @projects = current_user.projects.select { |p| p.active? } + @projects = current_user.projects.active @contexts = current_user.contexts.find(:all) respond_to do |format| format.m { @@ -89,6 +90,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 @@ -105,6 +107,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 @@ -113,7 +116,7 @@ class TodosController < ApplicationController def show respond_to do |format| format.m do - @projects = current_user.projects.select { |p| p.active? } + @projects = current_user.projects.active @contexts = current_user.contexts.find(:all) @edit_mobile = true @return_path=cookies[:mobile_url] @@ -124,12 +127,14 @@ class TodosController < ApplicationController end # 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 + @new_recurring_todo = check_for_next_todo(@todo) if @saved respond_to do |format| format.js do @@ -137,6 +142,10 @@ class TodosController < ApplicationController determine_remaining_in_context_count(@todo.context_id) determine_down_count determine_completed_count if @todo.completed? + 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 @@ -170,6 +179,9 @@ class TodosController < ApplicationController @original_item_context_id = @todo.context_id @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 @@ -211,7 +223,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 @@ -219,7 +231,26 @@ 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 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 + + 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 @@ -229,7 +260,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) @@ -243,13 +274,14 @@ 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 + + @saved = @todo.destroy # check if this todo has a related recurring_todo. If so, create next todo - check_for_next_todo - - @saved = @todo.destroy + @new_recurring_todo = check_for_next_todo(@todo) if @saved respond_to do |format| @@ -268,6 +300,9 @@ 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 end render @@ -337,46 +372,109 @@ class TodosController < ApplicationController def tag @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? @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.due IS NULL, todos.due ASC, todos.created_at ASC') @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 } format.m { - cookies[:mobile_url]=request.request_uri + cookies[:mobile_url]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE} render :action => "mobile_tag" } 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 || @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'} + 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.not_completed.find(:all, + :include => [:taggings, :tags], + :conditions => ['todos.due <= ?', due_today_date], + :order => "due") + @due_this_week = current_user.todos.not_completed.find(:all, + :include => [:taggings, :tags], + :conditions => ['todos.due > ? AND todos.due <= ?', due_today_date, due_this_week_date], + :order => "due") + @due_next_week = current_user.todos.not_completed.find(:all, + :include => [:taggings, :tags], + :conditions => ['todos.due > ? AND todos.due <= ?', due_this_week_date, due_next_week_date], + :order => "due") + @due_this_month = current_user.todos.not_completed.find(:all, + :include => [:taggings, :tags], + :conditions => ['todos.due > ? AND todos.due <= ?', due_next_week_date, due_this_month_date], + :order => "due") + @due_after_this_month = current_user.todos.not_completed.find(:all, + :include => [:taggings, :tags], + :conditions => ['todos.due > ?', due_this_month_date], + :order => "due") + + @count = current_user.todos.not_completed.are_due.count + + respond_to do |format| + format.html + format.ics { + @due_all = current_user.todos.not_completed.are_due.find(:all, :order => "due") + render :action => 'calendar', :layout => false, :content_type => Mime::ICS + } + end + end + + private def get_todo_from_params @todo = current_user.todos.find(params['id']) @@ -438,13 +536,17 @@ class TodosController < ApplicationController end def with_parent_resource_scope(&block) + @feed_title = "Actions " if (params[:context_id]) @context = current_user.contexts.find_by_params(params) + @feed_title = @feed_title + "in context '#{@context.name}'" Todo.send :with_scope, :find => {:conditions => ['todos.context_id = ?', @context.id]} do yield end elsif (params[:project_id]) @project = current_user.projects.find_by_params(params) + @feed_title = @feed_title + "in project '#{@project.name}'" + @project_feed = true Todo.send :with_scope, :find => {:conditions => ['todos.project_id = ?', @project.id]} do yield end @@ -608,7 +710,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' @@ -620,7 +722,7 @@ class TodosController < ApplicationController render_rss_feed_for @todos, :feed => todo_feed_options, :item => { :title => :description, - :link => lambda { |t| context_url(t.context) }, + :link => lambda { |t| @project_feed.nil? ? context_url(t.context) : project_url(t.project) }, :guid => lambda { |t| todo_url(t) }, :description => todo_feed_content } @@ -628,7 +730,9 @@ class TodosController < ApplicationController end def todo_feed_options - Todo.feed_options(current_user) + options = Todo.feed_options(current_user) + options[:title] = @feed_title + return options end def todo_feed_content @@ -674,18 +778,85 @@ 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) - date_to_check = @todo.due.nil? ? @todo.show_from : @todo.due - 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) + new_recurring_todo = nil + recurring_todo = nil + if todo.from_recurring_todo? + recurring_todo = todo.recurring_todo + + # check if there are active todos belonging to this recurring todo. only + # add new one if all active todos are completed + if recurring_todo.todos.active.count == 0 + + # 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 from now + date_to_check = Time.zone.now if date_to_check.nil? + + 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 + # 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. We pick yesterday so + # that new todos due for today will be created instead of new todos + # for tomorrow. + 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 - end + end + return new_recurring_todo + 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 + 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.not_completed.count(:all, + :conditions => ['todos.due <= ?', due_today_date]) + when "due_this_week" + return 0 == current_user.todos.not_completed.count(:all, + :conditions => ['todos.due > ? AND todos.due <= ?', due_today_date, due_this_week_date]) + when "due_next_week" + return 0 == current_user.todos.not_completed.count(:all, + :conditions => ['todos.due > ? AND todos.due <= ?', due_this_week_date, due_next_week_date]) + when "due_this_month" + return 0 == current_user.todos.not_completed.count(:all, + :conditions => ['todos.due > ? AND todos.due <= ?', due_next_week_date, due_this_month_date]) + when "due_after_this_month" + return 0 == current_user.todos.not_completed.count(:all, + :conditions => ['todos.due > ?', due_this_month_date]) + else + raise Exception.new, "unknown due id for calendar: '#{id}'" + end end class FindConditionBuilder diff --git a/app/helpers/recurring_todos_helper.rb b/app/helpers/recurring_todos_helper.rb index b346e7ea..76fcc95e 100644 --- a/app/helpers/recurring_todos_helper.rb +++ b/app/helpers/recurring_todos_helper.rb @@ -3,7 +3,7 @@ module RecurringTodosHelper def recurrence_time_span(rt) case rt.ends_on when "no_end_date" - return "" + return rt.start_from.nil? ? "" : "from " + format_date(rt.start_from) when "ends_on_number_of_times" return "for "+rt.number_of_occurences.to_s + " times" when "ends_on_end_date" @@ -11,7 +11,7 @@ module RecurringTodosHelper ends = rt.end_date.nil? ? "" : " until " + format_date(rt.end_date) return starts+ends else - raise Exception.new, "unknown recurrence time span selection (#{self.ends_on})" + raise Exception.new, "unknown recurrence time span selection (#{self.ends_on})" end end @@ -28,10 +28,10 @@ module RecurringTodosHelper def recurring_todo_tag_list tags_except_starred = @recurring_todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME} - tag_list = tags_except_starred.collect{|t| "" + + tag_list = tags_except_starred.collect{|t| "" + # link_to(t.name, :controller => "todos", :action => "tag", :id => # t.name) + TODO: tag view for recurring_todos (yet?) - t.name + + t.name + ""}.join('') "#{tag_list}" end @@ -44,7 +44,7 @@ module RecurringTodosHelper str end - def recurring_todo_remote_star_icon + def recurring_todo_remote_star_icon str = link_to( image_tag_for_star(@recurring_todo), toggle_star_recurring_todo_path(@recurring_todo), :class => "icon star_item", :title => "star the action '#{@recurring_todo.description}'") diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index fa1b9d8b..46c30663 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 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 diff --git a/app/models/context.rb b/app/models/context.rb index 39cce497..41d27a53 100644 --- a/app/models/context.rb +++ b/app/models/context.rb @@ -2,7 +2,10 @@ class Context < ActiveRecord::Base has_many :todos, :dependent => :delete_all, :include => :project, :order => "todos.completed_at DESC" belongs_to :user - + + named_scope :active, :conditions => { :hide => false } + named_scope :hidden, :conditions => { :hide => true } + acts_as_list :scope => :user extend NamePartFinder include Tracks::TodoList diff --git a/app/models/message_gateway.rb b/app/models/message_gateway.rb new file mode 100644 index 00000000..8fcb78d3 --- /dev/null +++ b/app/models/message_gateway.rb @@ -0,0 +1,34 @@ +class MessageGateway < ActionMailer::Base + include ActionView::Helpers::SanitizeHelper + def receive(email) + user = User.find(:first, :include => [:preference], :conditions => ["preferences.sms_email = ?", email.from[0].strip]) + 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 + + description = nil + notes = nil + + if email.content_type == "multipart/related" + description = sanitize email.subject + body_part = email.parts.find{|m| m.content_type == "text/plain"} + notes = sanitize body_part.body.strip + else + if email.subject.empty? + description = sanitize email.body.strip + notes = nil + else + description = sanitize email.subject.strip + notes = sanitize email.body.strip + end + end + + # stupid T-Mobile often sends the same message multiple times + return if user.todos.find(:first, :conditions => {:description => description}) + + todo = Todo.from_rich_message(user, context.id, description, notes) + todo.save! + end +end diff --git a/app/models/preference.rb b/app/models/preference.rb index 6b76fd97..d4530f3f 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} @@ -21,7 +22,7 @@ class Preference < ActiveRecord::Base def parse_date(s) return nil if s.blank? - Date.strptime(s, date_format) + user.at_midnight(Date.strptime(s, date_format)) end end \ No newline at end of file diff --git a/app/models/project.rb b/app/models/project.rb index de0ad2c6..4013e985 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -3,6 +3,10 @@ class Project < ActiveRecord::Base has_many :notes, :dependent => :delete_all, :order => "created_at DESC" belongs_to :default_context, :class_name => "Context", :foreign_key => "default_context_id" belongs_to :user + + named_scope :active, :conditions => { :state => 'active' } + named_scope :hidden, :conditions => { :state => 'hidden' } + named_scope :completed, :conditions => { :state => 'completed'} validates_presence_of :name, :message => "project must have a name" validates_length_of :name, :maximum => 255, :message => "project name must be less than 256 characters" @@ -16,7 +20,7 @@ class Project < ActiveRecord::Base state :active state :hidden, :enter => :hide_todos, :exit => :unhide_todos - state :completed, :enter => Proc.new { |p| p.completed_at = Time.now.utc }, :exit => Proc.new { |p| p.completed_at = nil } + state :completed, :enter => Proc.new { |p| p.completed_at = Time.zone.now }, :exit => Proc.new { |p| p.completed_at = nil } event :activate do transitions :to => :active, :from => [:hidden, :completed] diff --git a/app/models/recurring_todo.rb b/app/models/recurring_todo.rb index ab9fa3f1..be4e2414 100644 --- a/app/models/recurring_todo.rb +++ b/app/models/recurring_todo.rb @@ -4,6 +4,8 @@ class RecurringTodo < ActiveRecord::Base belongs_to :project belongs_to :user + has_many :todos + attr_protected :user acts_as_state_machine :initial => :active, :column => 'state' @@ -12,7 +14,7 @@ class RecurringTodo < ActiveRecord::Base t[:show_from], t.completed_at = nil, nil t.occurences_count = 0 } - state :completed, :enter => Proc.new { |t| t.completed_at = Time.now.utc }, :exit => Proc.new { |t| t.completed_at = nil } + state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil } validates_presence_of :description validates_length_of :description, :maximum => 100 @@ -20,6 +22,9 @@ class RecurringTodo < ActiveRecord::Base validates_presence_of :context + named_scope :active, :conditions => { :state => 'active'} + named_scope :completed, :conditions => { :state => 'completed'} + event :complete do transitions :to => :completed, :from => [:active] end @@ -29,7 +34,7 @@ class RecurringTodo < ActiveRecord::Base end # the following recurrence patterns can be stored: - # + # # daily todos - recurrence_period = 'daily' # every nth day - nth stored in every_other1 # every work day - only_work_days = true @@ -243,7 +248,7 @@ class RecurringTodo < ActiveRecord::Base if self.recurrence_selector == 0 return self.every_other2 else - return Time.now.month + return Time.zone.now.month end end @@ -257,7 +262,7 @@ class RecurringTodo < ActiveRecord::Base if self.recurrence_selector == 1 return self.every_other2 else - return Time.now.month + return Time.zone.now.month end end @@ -392,13 +397,21 @@ class RecurringTodo < ActiveRecord::Base # previous is the due date of the previous todo or it is the completed_at # date when the completed_at date is after due_date (i.e. you did not make # 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 + start = self.start_from.nil? ? Time.zone.now : 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,8 +432,9 @@ 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 + start = self.start_from.nil? ? Time.zone.now : self.start_from else start = previous + 1.day if start.wday() == 0 @@ -428,7 +442,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,29 +467,44 @@ 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 - + case self.recurrence_selector when 0 # specific day of the month - if start.mday >= day + if start.mday >= day # there is no next day n in this month, search in next month - start += n.months + # + # start += n.months + # + # The above seems to not work. Fiddle with timezone. Looks like we hit a + # bug in rails here where 2008-12-01 +0100 plus 1.month becomes + # 2008-12-31 +0100. For now, just calculate in UTC and convert back to + # local timezone. + # + # TODO: recheck if future rails versions have this problem too + start = Time.utc(start.year, start.month, start.day)+n.months + start = Time.zone.local(start.year, start.month, start.day) + # go back to day end - return Time.utc(start.year, start.month, day) + return Time.zone.local(start.year, start.month, day) when 1 # relative weekday of a month the_next = get_xth_day_of_month(self.every_other3, self.every_count, start.month, start.year) if the_next.nil? || the_next <= start # the nth day is already passed in this month, go to next month and try # again - the_next = the_next+n.months + + # fiddle with timezone. Looks like we hit a bug in rails here where + # 2008-12-01 +0100 plus 1.month becomes 2008-12-31 +0100. For now, just + # calculate in UTC and convert back to local timezone. + # TODO: recheck if future rails versions have this problem too + the_next = Time.utc(the_next.year, the_next.month, the_next.day)+n.months + the_next = Time.zone.local(the_next.year, the_next.month, the_next.day) + # TODO: if there is still no match, start will be set to nil. if we ever # support 5th day of the month, we need to handle this case the_next = get_xth_day_of_month(self.every_other3, self.every_count, the_next.month, the_next.year) @@ -483,14 +518,18 @@ class RecurringTodo < ActiveRecord::Base def get_xth_day_of_month(x, weekday, month, year) if x == 5 - # last -> count backwards + # 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 + # 1-4th -> count upwards last -> count backwards. use UTC to avoid strange + # timezone oddities where last_day -= 1.day seems to shift tz+0100 to + # tz+0000 start = Time.utc(year,month,1) n = x while n > 0 @@ -500,32 +539,31 @@ class RecurringTodo < ActiveRecord::Base n -= 1 start += 1.day unless n==0 end - return start + # convert back to local timezone + return Time.zone.local(start.year, start.month, start.day) end end def get_yearly_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 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.utc(start.year+1, month, 1) if start.day >= day - start = Time.utc(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.utc(start.year, month, day) + return Time.zone.local(start.year, month, day) when 1 # relative weekday of a specific month # if there is no next month n in this year, search in next year - the_next = start.month > month ? Time.utc(start.year+1, month, 1) : start + the_next = start.month > month ? Time.zone.local(start.year+1, month, 1) : start # get the xth day of the month the_next = get_xth_day_of_month(self.every_other3, self.every_count, month, the_next.year) @@ -592,4 +630,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.zone.now : 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/app/models/todo.rb b/app/models/todo.rb index 7a6f3892..5aed02be 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -4,7 +4,11 @@ class Todo < ActiveRecord::Base belongs_to :project belongs_to :user belongs_to :recurring_todo - + + named_scope :active, :conditions => { :state => 'active' } + named_scope :not_completed, :conditions => ['NOT state = ? ', 'completed'] + named_scope :are_due, :conditions => ['NOT todos.due IS NULL'] + STARRED_TAG_NAME = "starred" acts_as_state_machine :initial => :active, :column => 'state' @@ -13,7 +17,7 @@ class Todo < ActiveRecord::Base # of state completed is not run, see #679 state :active, :enter => Proc.new { |t| t[:show_from], t.completed_at = nil, nil } state :project_hidden - state :completed, :enter => Proc.new { |t| t.completed_at = Time.now.utc }, :exit => Proc.new { |t| t.completed_at = nil } + state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil } state :deferred event :defer do @@ -68,6 +72,8 @@ class Todo < ActiveRecord::Base end def show_from=(date) + # parse Date objects into the proper timezone + date = user.at_midnight(date) if (date.is_a? Date) activate! if deferred? && date.blank? defer! if active? && !date.blank? && date > user.date self[:show_from] = date @@ -125,4 +131,46 @@ class Todo < ActiveRecord::Base return self.recurring_todo_id != nil end -end \ No newline at end of file + # Rich Todo API + + def self.from_rich_message(user, default_context_id, description, notes) + fields = description.match /([^>@]*)@?([^>]*)>?(.*)/ + description = fields[1].strip + context = fields[2].strip + project = fields[3].strip + + context = nil if context == "" + project = nil if project == "" + + context_id = default_context_id + unless(context.nil?) + found_context = user.active_contexts.find_by_namepart(context) + found_context = user.contexts.find_by_namepart(context) if found_context.nil? + context_id = found_context.id unless found_context.nil? + end + + unless user.contexts.exists? context_id + raise(CannotAccessContext, "Cannot access a context that does not belong to this user.") + end + + project_id = nil + unless(project.blank?) + if(project[0..3].downcase == "new:") + found_project = user.projects.build + found_project.name = project[4..255+4].strip + found_project.save! + else + found_project = user.active_projects.find_by_namepart(project) + found_project = user.projects.find_by_namepart(project) if found_project.nil? + end + project_id = found_project.id unless found_project.nil? + end + + todo = user.todos.build + todo.description = description + todo.notes = notes + todo.context_id = context_id + todo.project_id = project_id unless project_id.nil? + return todo + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 8afb8e41..8a85b64b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -51,6 +51,21 @@ class User < ActiveRecord::Base self.update_positions(projects.map{ |p| p.id }) return projects end + def actionize(user_id, scope_conditions = {}) + @state = scope_conditions[:state] + query_state = "" + query_state = "AND project.state = '" + @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 = ? AND NOT todo.state='completed' " + + query_state + + " GROUP BY project.id ORDER by p_count DESC",user_id]) + self.update_positions(projects.map{ |p| p.id }) + projects = find(:all, :conditions => scope_conditions) + return projects + end end has_many :active_projects, :class_name => 'Project', @@ -59,7 +74,7 @@ class User < ActiveRecord::Base has_many :active_contexts, :class_name => 'Context', :order => 'position ASC', - :conditions => [ 'hide = ?', 'true' ] + :conditions => [ 'hide = ?', false ] has_many :todos, :order => 'todos.completed_at DESC, todos.created_at DESC', :dependent => :delete_all @@ -71,7 +86,7 @@ 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! } + find(:all, :conditions => ['show_from <= ?', Time.zone.now ]).collect { |t| t.activate! } end end has_many :completed_todos, @@ -169,7 +184,11 @@ class User < ActiveRecord::Base end def date - time.to_date + time.midnight + end + + def at_midnight(date) + return TimeZone[prefs.time_zone].local(date.year, date.month, date.day, 0, 0, 0) end def generate_token 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/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/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/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 %> +

+
-
-
+ 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 -%>
  • 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" 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: +

    +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/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
       ...
    diff --git a/app/views/layouts/standard.html.erb b/app/views/layouts/standard.html.erb
    index de66ec95..42ea20d2 100644
    --- a/app/views/layouts/standard.html.erb
    +++ b/app/views/layouts/standard.html.erb
    @@ -16,9 +16,6 @@
     
    @@ -49,7 +46,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"} ) %>
  • @@ -57,6 +54,7 @@ window.onload=function(){ <% if current_user.is_admin? -%>
  • <%= navigation_link("Admin", users_path, {:accesskey => "a", :title => "Add or delete users"} ) %>
  • <% end -%> +
  • <%= navigation_link(image_tag("x-office-calendar.png", :size => "16X16", :border => 0), calendar_path, :title => "Calendar of due actions" ) %>
  • <%= navigation_link(image_tag("recurring_menu16x16.png", :size => "16X16", :border => 0), {:controller => "recurring_todos", :action => "index"}, :title => "Manage recurring actions" ) %>
  • <%= navigation_link(image_tag("feed-icon.png", :size => "16X16", :border => 0), {:controller => "feedlist", :action => "index"}, :title => "See a list of available feeds" ) %>
  • <%= navigation_link(image_tag("menustar.gif", :size => "16X16", :border => 0), tag_path("starred"), :title => "See your starred actions" ) %>
  • 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))) %> @@ -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..05c90a86 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 %>
  • +
  • From email: <%= prefs.sms_email %>
  • +
  • Default email context: <%= prefs.sms_context.nil? ? "None" : prefs.sms_context.name %>
  • <%= link_to "Edit preferences »", { :controller => 'preferences', :action => 'edit'}, :class => 'edit_link' %> diff --git a/app/views/projects/_project.rhtml b/app/views/projects/_project.rhtml index 3e1d6418..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 -%>

    @@ -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/_project_state_group.rhtml b/app/views/projects/_project_state_group.rhtml index 6fe6ef21..32693f0f 100644 --- a/app/views/projects/_project_state_group.rhtml +++ b/app/views/projects/_project_state_group.rhtml @@ -1,7 +1,8 @@
    >

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

    +
    <%= render :partial => 'project_listing', :collection => project_state_group %>
    <%= sortable_element "list-#{state}-projects", get_listing_sortable_options("list-#{state}-projects") %> -
    \ No newline at end of file +
    diff --git a/app/views/projects/actionize.js.rjs b/app/views/projects/actionize.js.rjs new file mode 100644 index 00000000..ccb10451 --- /dev/null +++ b/app/views/projects/actionize.js.rjs @@ -0,0 +1,6 @@ +list_id = "list-#{@state}-projects" +page.replace_html list_id, + :partial => 'project_listing', + :collection => @projects +page.sortable list_id, get_listing_sortable_options(list_id) + \ No newline at end of file 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/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/recurring_todos/_recurring_todo.html.erb b/app/views/recurring_todos/_recurring_todo.html.erb index 3962b27b..ccc93cbb 100644 --- a/app/views/recurring_todos/_recurring_todo.html.erb +++ b/app/views/recurring_todos/_recurring_todo.html.erb @@ -6,7 +6,16 @@
    <%= sanitize(recurring_todo.description) %> <%= recurring_todo_tag_list %> - [<%=recurrence_target(recurring_todo)%> <%= recurring_todo.recurrence_pattern %> <%= recurrence_time_span(recurring_todo) %>] + <% + rt = recurrence_target(recurring_todo) + rp = recurring_todo.recurrence_pattern + # only add space if recurrence_pattern has content + rp = " " + rp if !rp.nil? + rts = recurrence_time_span(recurring_todo) + # only add space if recurrence_time_span has content + rts = " " + rts if !(rts == "") + %> + [<%=rt%><%=rp%><%=rts%>]
    diff --git a/app/views/recurring_todos/_recurring_todo_form.erb b/app/views/recurring_todos/_recurring_todo_form.erb index 4a767661..a9709e45 100644 --- a/app/views/recurring_todos/_recurring_todo_form.erb +++ b/app/views/recurring_todos/_recurring_todo_form.erb @@ -101,22 +101,22 @@

    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();" -%> + + diff --git a/app/views/shared/_add_new_item_form.rhtml b/app/views/shared/_add_new_item_form.rhtml index 49345eb9..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', @@ -34,6 +34,7 @@ <%= text_area( "todo", "notes", "cols" => 29, "rows" => 6, "tabindex" => 2) %> + diff --git a/app/views/todos/_edit_form.rhtml b/app/views/todos/_edit_form.rhtml index 1807504f..fc7b5c8d 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) %> @@ -48,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/app/views/todos/_edit_mobile.rhtml b/app/views/todos/_edit_mobile.rhtml index f1caa191..0837f026 100644 --- a/app/views/todos/_edit_mobile.rhtml +++ b/app/views/todos/_edit_mobile.rhtml @@ -33,8 +33,8 @@ end %>

    <%= text_field_tag "tag_list", @tag_list_text, :size => 30, :tabindex => 6 %>

    -<%= date_select("todo", "due", :order => [:day, :month, :year], - :start_year => this_year, :include_blank => true) %> +<%= date_select("todo", "due", {:order => [:day, :month, :year], + :start_year => this_year, :include_blank => true}, :tabindex => 7) %>

    -<%= date_select("todo", "show_from", :order => [:day, :month, :year], - :start_year => this_year, :include_blank => true) %> +<%= date_select("todo", "show_from", {:order => [:day, :month, :year], + :start_year => this_year, :include_blank => true}, :tabindex => 8) %> diff --git a/app/views/todos/_todo.html.erb b/app/views/todos/_todo.html.erb index c03e64e6..288325a0 100644 --- a/app/views/todos/_todo.html.erb +++ b/app/views/todos/_todo.html.erb @@ -11,6 +11,7 @@ <%= remote_star_icon %> <%= remote_toggle_checkbox unless source_view_is :deferred %>
    + <% unless @todo.completed? %><%= defer_link(1) %> <%= defer_link(7) %><% end %> <%= date_span -%> <%= h sanitize(todo.description) %> <%= link_to(image_tag("recurring16x16.png"), {:controller => "recurring_todos", :action => "index"}, :class => "recurring_icon") if @todo.from_recurring_todo? %> diff --git a/app/views/todos/_toggle_notes.rhtml b/app/views/todos/_toggle_notes.rhtml index 704894b3..52f279a1 100644 --- a/app/views/todos/_toggle_notes.rhtml +++ b/app/views/todos/_toggle_notes.rhtml @@ -3,5 +3,5 @@ element.next('.todo_notes').toggle end -%> \ No newline at end of file diff --git a/app/views/todos/calendar.html.erb b/app/views/todos/calendar.html.erb new file mode 100644 index 00000000..0d7e1dcb --- /dev/null +++ b/app/views/todos/calendar.html.erb @@ -0,0 +1,68 @@ +
    + +
    +

    Due today

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

    Due in rest of this week

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

    Due next week

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

    Due in rest of <%= Time.zone.now.strftime("%B") %>

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

    Due in <%= (Time.zone.now+1.month).strftime("%B") %> and later

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

    <%= 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..8378a096 --- /dev/null +++ b/app/views/todos/calendar.ics.erb @@ -0,0 +1,31 @@ +BEGIN:VCALENDAR +PRODID:-//TRACKS//<%= TRACKS_VERSION %>//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Tracks +<% for todo in @due_all + due_date = todo.due + overdue_text = "" + if due_date.at_midnight < Time.zone.now.at_midnight + 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.updated_at.strftime("%Y%m%dT%H%M%SZ") %> +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:<%= overdue_text + todo.description %> +TRANSP:TRANSPARENT +END:VEVENT +<% end +%>END:VCALENDAR 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() 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/new.m.erb b/app/views/todos/new.m.erb index 17144323..361e223d 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 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 %> diff --git a/app/views/todos/toggle_check.js.rjs b/app/views/todos/toggle_check.js.rjs index 1462162a..ee9c26e1 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 @@ -23,10 +24,12 @@ if @saved page.insert_html :bottom, item_container_id(@new_recurring_todo), :partial => 'todos/todo', :locals => { :todo => @new_recurring_todo, :parent_container_type => parent_container_type } page.visual_effect :highlight, dom_id(@new_recurring_todo, 'line'), {'startcolor' => "'#99ff99'"} else - 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? + if @todo.recurring_todo.todos.active.count == 0 + 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 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 6cfc48d2..2f263b54 100644 --- a/app/views/todos/update.js.rjs +++ b/app/views/todos/update.js.rjs @@ -6,19 +6,28 @@ 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 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 @@ -92,6 +101,23 @@ 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.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]}'" end diff --git a/config/environment.rb.tmpl b/config/environment.rb.tmpl index 903df142..77deeb30 100644 --- a/config/environment.rb.tmpl +++ b/config/environment.rb.tmpl @@ -93,8 +93,17 @@ 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) -TRACKS_VERSION='1.7-devel' +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 diff --git a/config/routes.rb b/config/routes.rb index a4eb29e3..1b8bf1c3 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} @@ -43,9 +47,12 @@ 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.calendar 'calendar.ics', :action => "calendar", :format => 'ics' + 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' @@ -55,6 +62,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' diff --git a/db/migrate/041_add_sms_to_preference.rb b/db/migrate/041_add_sms_to_preference.rb new file mode 100644 index 00000000..61f7e05a --- /dev/null +++ b/db/migrate/041_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/db/migrate/042_change_dates_to_datetimes.rb b/db/migrate/042_change_dates_to_datetimes.rb new file mode 100644 index 00000000..d534c642 --- /dev/null +++ b/db/migrate/042_change_dates_to_datetimes.rb @@ -0,0 +1,39 @@ +class ChangeDatesToDatetimes < ActiveRecord::Migration + def self.up + change_column :todos, :show_from, :datetime + change_column :todos, :due, :datetime + change_column :recurring_todos, :start_from, :datetime + change_column :recurring_todos, :end_date, :datetime + + User.all(:include => [:todos, :recurring_todos]).each do |user| + 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 + 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 + + 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 + change_column :todos, :show_from, :date + change_column :todos, :due, :date + change_column :recurring_todos, :start_from, :date + change_column :recurring_todos, :end_date, :date + end +end diff --git a/db/migrate/043_add_updated_at_to_todos.rb b/db/migrate/043_add_updated_at_to_todos.rb new file mode 100644 index 00000000..ec14720a --- /dev/null +++ b/db/migrate/043_add_updated_at_to_todos.rb @@ -0,0 +1,10 @@ +class AddUpdatedAtToTodos < ActiveRecord::Migration + def self.up + add_column :todos, :updated_at, :timestamp + execute 'update todos set updated_at = created_at where completed_at IS NULL' + execute 'update todos set updated_at = completed_at where NOT completed_at IS NULL' + end + def self.down + remove_column :todos, :updated_at + end +end diff --git a/db/tracks-15-blank.db b/db/tracks-15-blank.db index b3597802..aff96bb2 100644 Binary files a/db/tracks-15-blank.db and b/db/tracks-15-blank.db differ diff --git a/db/tracks-15-example.db b/db/tracks-15-example.db index b9b4473b..97f586c7 100644 Binary files a/db/tracks-15-example.db and b/db/tracks-15-example.db differ diff --git a/doc/CHANGELOG b/doc/CHANGELOG index 1dd8e775..7cb7dcfc 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,21 @@ 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) +7. You can now sort projects on number of active todos + +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) diff --git a/lib/authenticated_test_helper.rb b/lib/authenticated_test_helper.rb index 2f743aa0..7a52e62b 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/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 diff --git a/public/images/defer_1.png b/public/images/defer_1.png new file mode 100644 index 00000000..d40a9c01 Binary files /dev/null and b/public/images/defer_1.png differ diff --git a/public/images/defer_7.png b/public/images/defer_7.png new file mode 100644 index 00000000..7e80ff15 Binary files /dev/null and b/public/images/defer_7.png differ diff --git a/public/images/x-office-calendar.png b/public/images/x-office-calendar.png new file mode 100644 index 00000000..f8607e33 Binary files /dev/null and b/public/images/x-office-calendar.png differ 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() { diff --git a/public/javascripts/tracks_1216850288.js b/public/javascripts/tracks_1222451275.js similarity index 99% rename from public/javascripts/tracks_1216850288.js rename to public/javascripts/tracks_1222451275.js index 33f474d8..2c6f9eb4 100644 --- a/public/javascripts/tracks_1216850288.js +++ b/public/javascripts/tracks_1222451275.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..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; @@ -808,6 +811,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; } @@ -1193,8 +1204,9 @@ body.integrations textarea { width:80%; background-color:#ddd; } - -.date_clear -{ - float: right; -} \ No newline at end of file +.defer-container { + float:right; +} +.defer-container a:hover { + background-color: inherit; +} diff --git a/public/stylesheets/tracks_1217189789.css b/public/stylesheets/tracks_1225223947.css similarity index 98% rename from public/stylesheets/tracks_1217189789.css rename to public/stylesheets/tracks_1225223947.css index 0ea5a631..8afe55d2 100644 --- a/public/stylesheets/tracks_1217189789.css +++ b/public/stylesheets/tracks_1225223947.css @@ -129,7 +129,8 @@ div.buttons, div.buttons a, div.buttons a:hover {text-align: right; margin-right div#list-active-projects, div#list-hidden-projects, div#list-completed-projects, div#list-contexts, div#projects-empty-nd {clear:right; border: 1px solid #999} .project-state-group h2 {margin:20px 0px 8px 13px} .search-result-group h2 {margin:20px 0px 8px 13px } -div.alpha_sort {margin-top:-20px; float:right} +div.menu_sort {margin-top:-20px; float:right} +div.alpha_sort, div.tasks_sort,span.sort_separator {float:left} .container td {border: none; padding-bottom: 5px} .container form {border: none} div.project_description {background: #eee; padding: 5px; margin-top: 0px; margin-left: -5px; margin-right: -5px; color: #666; font-style: italic; font-size: 12px; font-weight: normal} @@ -149,6 +150,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 +221,8 @@ 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} +.defer-container {float:right} +.defer-container a:hover {background-color: inherit} 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} diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index 410860a1..5cc542f0 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -10,7 +10,7 @@ describe Todo do def create_todo(attributes={}) todo = Todo.new(valid_attributes(attributes)) - todo.stub!(:user).and_return(mock_model(User, :date => Time.now)) + todo.stub!(:user).and_return(mock_model(User, :date => Time.zone.now)) todo.save! todo end @@ -32,7 +32,7 @@ describe Todo do it 'ensures that show_from is a date in the future' do todo = Todo.new(valid_attributes) - todo.stub!(:user).and_return(mock_model(User, :date => Time.now)) + todo.stub!(:user).and_return(mock_model(User, :date => Time.zone.now)) todo.show_from = 3.days.ago todo.should have(1).error_on(:show_from) end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7f915fbd..7b4eb222 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -37,7 +37,7 @@ describe User do it 'has many active contexts' do User.should have_many(:active_contexts). with_order('position ASC'). - with_conditions('hide = ?', 'true'). + with_conditions('hide = ?', false). with_class_name('Context') end @@ -178,4 +178,20 @@ describe User do @user.remember_token_expires_at.should be_between(before, after) end end + + it "should not activate todos that are showing when UTC is tomorrow" do + context = Context.create(:name => 'a context') + user = User.create(:login => 'user7', :password => 'foobar', :password_confirmation => 'foobar') + user.save! + user.create_preference + user.preference.update_attribute('time_zone', 'Pacific Time (US & Canada)') +# Time.zone = 'Pacific Time (US & Canada)' + Time.stub!(:now).and_return(Time.new.end_of_day - 20.minutes) + todo = user.todos.build(:description => 'test task', :context => context, :show_from => user.date + 1.days) + todo.save! + + user.deferred_todos.find_and_activate_ready + user = User.find(user.id) + user.deferred_todos.should include(todo) + end end diff --git a/spec/views/notes/_notes.rhtml_spec.rb b/spec/views/notes/_notes.rhtml_spec.rb new file mode 100644 index 00000000..3f07ead3 --- /dev/null +++ b/spec/views/notes/_notes.rhtml_spec.rb @@ -0,0 +1,36 @@ +require File.dirname(__FILE__) + '/../../spec_helper' + +describe "/notes/_notes.rhtml" do + before :each do + @project = mock_model(Project, :name => "a project") + @note = mock_model(Note, :body => "this is a note", :project => @project, + :created_at => Time.now, :updated_at? => false) + @controller.template.stub!(:apply_behavior) + @controller.template.stub!(:format_date) + @controller.template.stub!(:render) + @controller.template.stub!(:form_remote_tag) + end + + it "should render" do + render :partial => "/notes/notes", :locals => {:notes => @note} + response.should have_tag("div.note_footer") + end + + it "should auto-link URLs" do + @note.stub!(:body).and_return("http://www.google.com/") + render :partial => "/notes/notes", :locals => {:notes => @note} + response.should have_tag("a[href=\"http://www.google.com/\"]") + end + + it "should auto-link embedded URLs" do + @note.stub!(:body).and_return("this is cool: http://www.google.com/") + render :partial => "/notes/notes", :locals => {:notes => @note} + response.should have_tag("a[href=\"http://www.google.com/\"]") + end + + it "should parse Textile links correctly" do + @note.stub!(:body).and_return("\"link\":http://www.google.com/") + render :partial => "/notes/notes", :locals => {:notes => @note} + response.should have_tag("a[href=\"http://www.google.com/\"]") + end +end diff --git a/spec/views/todos/_toggle_notes.rhtml_spec.rb b/spec/views/todos/_toggle_notes.rhtml_spec.rb new file mode 100644 index 00000000..d00d38a0 --- /dev/null +++ b/spec/views/todos/_toggle_notes.rhtml_spec.rb @@ -0,0 +1,33 @@ +require File.dirname(__FILE__) + '/../../spec_helper' + +describe "/todos/_toggle_notes.rhtml" do + # include ControllerHelper + + before :each do + @item = mock_model(Todo, :notes => "this is a note") + @controller.template.stub!(:apply_behavior) + end + + it "should render" do + render :partial => "/todos/toggle_notes", :locals => {:item => @item} + response.should have_tag("div.todo_notes") + end + + it "should auto-link URLs" do + @item.stub!(:notes).and_return("http://www.google.com/") + render :partial => "/todos/toggle_notes", :locals => {:item => @item} + response.should have_tag("a[href=\"http://www.google.com/\"]") + end + + it "should auto-link embedded URLs" do + @item.stub!(:notes).and_return("this is cool: http://www.google.com/") + render :partial => "/todos/toggle_notes", :locals => {:item => @item} + response.should have_tag("a[href=\"http://www.google.com/\"]") + end + + it "should parse Textile URLs correctly" do + @item.stub!(:notes).and_return("\"link\":http://www.google.com/") + render :partial => "/todos/toggle_notes", :locals => {:item => @item} + response.should have_tag("a[href=\"http://www.google.com/\"]") + end +end diff --git a/test/fixtures/contexts.yml b/test/fixtures/contexts.yml index 0991767f..49ce9d2f 100644 --- a/test/fixtures/contexts.yml +++ b/test/fixtures/contexts.yml @@ -113,3 +113,21 @@ 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 %> + +anothercontext: + id: 14 + name: anothercontext + position: 2 + 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..a532e3ab --- /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: 15555555555@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..86b449a3 --- /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 + +message_content \ No newline at end of file diff --git a/test/fixtures/todos.yml b/test/fixtures/todos.yml index 52db71ea..4dcf984e 100644 --- a/test/fixtures/todos.yml +++ b/test/fixtures/todos.yml @@ -95,7 +95,7 @@ end completed_at: ~ user_id: 1 -7: +book: id: 7 context_id: 6 project_id: 3 @@ -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/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/projects_controller_test.rb b/test/functional/projects_controller_test.rb index 4644b9f3..33e6ca95 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -25,7 +25,7 @@ class ProjectsControllerTest < TodoContainerControllerTestBase assert_equal 1, assigns['deferred'].size t = p.not_done_todos[0] - t.show_from = 1.days.from_now.utc.to_date + t.show_from = 1.days.from_now.utc t.save! get :show, :id => p.to_param @@ -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 diff --git a/test/functional/recurring_todos_controller_test.rb b/test/functional/recurring_todos_controller_test.rb index 1d0d174e..2bd67e67 100644 --- a/test/functional/recurring_todos_controller_test.rb +++ b/test/functional/recurring_todos_controller_test.rb @@ -17,6 +17,167 @@ 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? + + # 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 + 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 users(:admin_user).at_midnight(Date.new(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 + + 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/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 3c3127e4..4888a7c4 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 @@ -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 @@ -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 @@ -137,7 +137,7 @@ class TodosControllerTest < Test::Rails::TestCase assert_equal "Call Warren Buffet to find out how much he makes per day", t.description assert_equal "foo, bar", t.tag_list expected = Date.new(2006,11,30) - actual = t.due + actual = t.due.to_date assert_equal expected, actual, "Expected #{expected.to_s(:db)}, was #{actual.to_s(:db)}" 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 @@ -188,12 +188,12 @@ 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/.+} assert_select 'guid', %r{http://test.host/todos/.+} - assert_select 'pubDate', projects(:timemachine).updated_at.to_s(:rfc822) + assert_select 'pubDate', todos(:book).updated_at.to_s(:rfc822) end end end @@ -237,15 +237,15 @@ 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' 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)})/ + assert_xml_select 'published', /(#{Regexp.escape(todos(:book).updated_at.xmlschema)}|#{Regexp.escape(projects(:moremoney).updated_at.xmlschema)})/ end end end @@ -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 @@ -311,16 +311,16 @@ 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 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 @@ -328,26 +328,26 @@ class TodosControllerTest < Test::Rails::TestCase assert t.active? assert_equal 'test notes', t.notes assert_nil t.show_from - assert_equal Date.new(2007,1,2).to_s, t.due.to_s + assert_equal Date.new(2007,1,2), t.due.to_date end 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,132 @@ 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_by_recurring_todo_id(1) + + # mark todo_1 as complete by toggle_check + 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 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 = '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_toggle_check_on_rec_todo_show_from_today + login_as(:admin_user) + + # link todo_1 and recurring_todo_1 + recurring_todo_1 = RecurringTodo.find(1) + todo_1 = Todo.find_by_recurring_todo_id(1) + today = Time.now.utc.at_midnight + + # change recurrence pattern to monthly and set show_from to today + recurring_todo_1.target = 'show_from_date' + recurring_todo_1.recurring_period = 'monthly' + recurring_todo_1.recurrence_selector = 0 + recurring_todo_1.every_other1 = today.day + recurring_todo_1.every_other2 = 1 + recurring_todo_1.save + + # mark todo_1 as complete by toggle_check, this gets rid of todo_1 that was + # not correctly created from the adjusted recurring pattern we defined + # above. + xhr :post, :toggle_check, :id => todo_1.id, :_source_view => 'todo' + todo_1.reload + assert todo_1.completed? + + # locate the new todo. This todo is created from the adjusted recurring + # pattern defined in this test + new_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'}) + assert !new_todo.nil? + + # mark new_todo as complete by toggle_check + xhr :post, :toggle_check, :id => new_todo.id, :_source_view => 'todo' + new_todo.reload + assert todo_1.completed? + + # locate the new todo in tickler + new_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'deferred'}) + assert !new_todo.nil? + + assert_equal "Call Bill Gates every day", new_todo.description + # check that the new todo is not the same as todo_1 + assert_not_equal todo_1.id, new_todo.id + + # check that the new_todo is in the tickler to show next month + assert !new_todo.show_from.nil? + assert_equal Time.utc(today.year, today.month+1, today.day), new_todo.show_from + 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/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/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/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" 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' diff --git a/test/test_helper.rb b/test/test_helper.rb index 85392073..c34d707f 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -71,7 +71,7 @@ class Test::Rails::TestCase < Test::Unit::TestCase end def next_week - 1.week.from_now.utc.to_date + 1.week.from_now.utc end # Courtesy of http://habtm.com/articles/2006/02/20/assert-yourself-man-redirecting-with-rjs 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/message_gateway_test.rb b/test/unit/message_gateway_test.rb new file mode 100644 index 00000000..32f77159 --- /dev/null +++ b/test/unit/message_gateway_test.rb @@ -0,0 +1,76 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class MessageGatewayTest < Test::Rails::TestCase + fixtures :users, :contexts + + def setup + @user = users(:sms_user) + @inbox = contexts(:inbox) + end + + def load_message(filename) + MessageGateway.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 => "message_content"}) + 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 + + def test_no_user + todo_count = Todo.count + badmessage = File.read(File.join(RAILS_ROOT, 'test', 'fixtures', 'sample_sms.txt')) + badmessage.gsub!("5555555555", "notauser") + MessageGateway.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', 'this is a task @ anothercontext') + invalid_context_msg = message.gsub('message_content', 'this is also a task @ notacontext') + + MessageGateway.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) + + MessageGateway.receive(invalid_context_msg) + invalid_context_todo = Todo.find(:first, :conditions => {:description => 'this is also a task'}) + assert_not_nil(invalid_context_todo) + assert_equal(@inbox, invalid_context_todo.context) + end +end diff --git a/test/unit/preference_test.rb b/test/unit/preference_test.rb index 30daace5..18595ef7 100644 --- a/test/unit/preference_test.rb +++ b/test/unit/preference_test.rb @@ -20,7 +20,7 @@ class PreferenceTest < Test::Rails::TestCase end def test_parse_date - assert_equal Date.new(2007, 5, 20).to_s, @admin_user.preference.parse_date('20/5/2007').to_s + assert_equal @admin_user.at_midnight(Date.new(2007, 5, 20)).to_s, @admin_user.preference.parse_date('20/5/2007').to_s end def test_parse_date_returns_nil_if_string_is_empty diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb index e8954937..58f4f794 100644 --- a/test/unit/project_test.rb +++ b/test/unit/project_test.rb @@ -118,7 +118,7 @@ class ProjectTest < Test::Rails::TestCase def test_deferred_todos assert_equal 1, @timemachine.deferred_todos.size t = @timemachine.not_done_todos[0] - t.show_from = 1.days.from_now.utc.to_date + t.show_from = 1.days.from_now.utc t.save! assert_equal 2, Project.find(@timemachine.id).deferred_todos.size end @@ -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 diff --git a/test/unit/recurring_todo_test.rb b/test/unit/recurring_todo_test.rb index 068bb35c..80106554 100644 --- a/test/unit/recurring_todo_test.rb +++ b/test/unit/recurring_todo_test.rb @@ -15,13 +15,13 @@ class RecurringTodoTest < Test::Rails::TestCase @in_three_days = Time.now.utc + 3.days @in_four_days = @in_three_days + 1.day # need a day after start_from - @friday = Time.utc(2008,6,6) - @saturday = Time.utc(2008,6,7) - @sunday = Time.utc(2008,6,8) # june 8, 2008 was a sunday - @monday = Time.utc(2008,6,9) - @tuesday = Time.utc(2008,6,10) - @wednesday = Time.utc(2008,6,11) - @thursday = Time.utc(2008,6,12) + @friday = Time.zone.local(2008,6,6) + @saturday = Time.zone.local(2008,6,7) + @sunday = Time.zone.local(2008,6,8) # june 8, 2008 was a sunday + @monday = Time.zone.local(2008,6,9) + @tuesday = Time.zone.local(2008,6,10) + @wednesday = Time.zone.local(2008,6,11) + @thursday = Time.zone.local(2008,6,12) end def test_pattern_text @@ -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) @@ -144,19 +134,19 @@ class RecurringTodoTest < Test::Rails::TestCase def test_monthly_pattern due_date = @monthly_every_last_friday.get_due_date(@sunday) - assert_equal Time.utc(2008,6,27), due_date + assert_equal Time.zone.local(2008,6,27), due_date - friday_is_last_day_of_month = Time.utc(2008,10,31) + friday_is_last_day_of_month = Time.zone.local(2008,10,31) due_date = @monthly_every_last_friday.get_due_date(friday_is_last_day_of_month-1.day ) assert_equal friday_is_last_day_of_month , due_date @monthly_every_third_friday = @monthly_every_last_friday @monthly_every_third_friday.every_other3=3 #third due_date = @monthly_every_last_friday.get_due_date(@sunday) # june 8th 2008 - assert_equal Time.utc(2008, 6, 20), due_date + assert_equal Time.zone.local(2008, 6, 20), due_date # set date past third friday of this month - due_date = @monthly_every_last_friday.get_due_date(Time.utc(2008,6,21)) # june 21th 2008 - assert_equal Time.utc(2008, 8, 15), due_date # every 2 months, so aug + due_date = @monthly_every_last_friday.get_due_date(Time.zone.local(2008,6,21)) # june 21th 2008 + assert_equal Time.zone.local(2008, 8, 15), due_date # every 2 months, so aug @monthly = @monthly_every_last_friday @monthly.recurrence_selector=0 @@ -167,47 +157,90 @@ class RecurringTodoTest < Test::Rails::TestCase assert_equal @sunday, due_date # june 8th due_date = @monthly.get_due_date(@sunday) # june 8th - assert_equal Time.utc(2008,8,8), due_date # aug 8th - + assert_equal Time.zone.local(2008,8,8), due_date # aug 8th end 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 + due_date = @yearly.get_due_date(Time.zone.local(2008,2,10)) # feb 10th + 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 + 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 @yearly.every_count = 3 # wednesday # beginning of same year - due_date = @yearly.get_due_date(Time.utc(2008,2,10)) # feb 10th - assert_equal Time.utc(2008,6,11), due_date # june 11th + due_date = @yearly.get_due_date(Time.zone.local(2008,2,10)) # feb 10th + assert_equal Time.zone.local(2008,6,11), due_date # june 11th # same month, before second wednesday due_date = @yearly.get_due_date(@saturday) # june 7th - assert_equal Time.utc(2008,6,11), due_date # june 11th + assert_equal Time.zone.local(2008,6,11), due_date # june 11th # same month, after second wednesday - due_date = @yearly.get_due_date(Time.utc(2008,6,12)) # june 7th - assert_equal Time.utc(2009,6,10), due_date # june 10th + due_date = @yearly.get_due_date(Time.zone.local(2008,6,12)) # june 7th + assert_equal Time.zone.local(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 + 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 + 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.zone.local(2020,1,1) + assert_equal Time.zone.local(2020,1,1), @weekly_every_day.get_due_date(nil) + assert_equal Time.zone.local(2020,1,1), @weekly_every_day.get_due_date(Time.zone.local(2019,10,1)) + assert_equal Time.zone.local(2020,1,10), @weekly_every_day.get_due_date(Time.zone.local(2020,1,9)) + + @monthly_every_last_friday.start_from = Time.zone.local(2020,1,1) + assert_equal Time.zone.local(2020,1,31), @monthly_every_last_friday.get_due_date(nil) # last friday of jan + assert_equal Time.zone.local(2020,1,31), @monthly_every_last_friday.get_due_date(Time.zone.local(2019,12,1)) # last friday of jan + assert_equal Time.zone.local(2020,2,28), @monthly_every_last_friday.get_due_date(Time.zone.local(2020,2,1)) # last friday of feb + + # start from after june 8th 2008 + @yearly.start_from = Time.zone.local(2020,6,12) + assert_equal Time.zone.local(2021,6,8), @yearly.get_due_date(nil) # jun 8th next year + assert_equal Time.zone.local(2021,6,8), @yearly.get_due_date(Time.zone.local(2019,6,1)) # also next year + assert_equal Time.zone.local(2021,6,8), @yearly.get_due_date(Time.zone.local(2020,6,15)) # also next year this_year = Time.now.utc.year - @yearly.start_from = Time.utc(this_year+1,6,12) + @yearly.start_from = Time.zone.local(this_year+1,6,12) 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 diff --git a/test/unit/todo_create_params_helper_test.rb b/test/unit/todo_create_params_helper_test.rb index 4b38b60d..ade5cbfd 100644 --- a/test/unit/todo_create_params_helper_test.rb +++ b/test/unit/todo_create_params_helper_test.rb @@ -18,7 +18,7 @@ class TodoCreateParamsHelperTest < Test::Rails::TestCase end def test_show_from_accessor - expected_date = Time.now.to_date + expected_date = Time.now params = { 'todo' => { 'show_from' => expected_date}} prefs = flexmock() params_helper = TodosController::TodoCreateParamsHelper.new(params, prefs) @@ -26,7 +26,7 @@ class TodoCreateParamsHelperTest < Test::Rails::TestCase end def test_due_accessor - expected_date = Time.now.to_date + expected_date = Time.now params = { 'todo' => { 'due' => expected_date}} prefs = flexmock() params_helper = TodosController::TodoCreateParamsHelper.new(params, prefs) diff --git a/test/unit/todo_test.rb b/test/unit/todo_test.rb index 9227f40b..85c69b39 100644 --- a/test/unit/todo_test.rb +++ b/test/unit/todo_test.rb @@ -68,9 +68,8 @@ class TodoTest < Test::Rails::TestCase def test_validate_show_from_must_be_a_date_in_the_future t = @not_completed2 - t[:show_from] = 1.week.ago.to_date # we have to set this via the indexer because show_from=() updates the state + t[:show_from] = 1.week.ago # we have to set this via the indexer because show_from=() updates the state # and actual show_from value appropriately based on the date - assert_equal 1.week.ago.to_date, t.show_from assert !t.save assert_equal 1, t.errors.count assert_equal "must be a date in the future", t.errors.on(:show_from) @@ -118,7 +117,7 @@ class TodoTest < Test::Rails::TestCase def test_activate_also_saves t = @not_completed1 - t.show_from = 1.week.from_now.to_date + t.show_from = 1.week.from_now t.save! assert t.deferred? t.reload diff --git a/test/unit/user_test.rb b/test/unit/user_test.rb index 3245099c..efadc277 100644 --- a/test/unit/user_test.rb +++ b/test/unit/user_test.rb @@ -235,7 +235,7 @@ class UserTest < Test::Rails::TestCase def test_find_and_activate_deferred_todos_that_are_ready assert_equal 1, @admin_user.deferred_todos.count - @admin_user.deferred_todos[0].show_from = @admin_user.time.to_date + @admin_user.deferred_todos[0].show_from = Time.now.utc - 5.seconds @admin_user.deferred_todos[0].save @admin_user.deferred_todos.reload @admin_user.deferred_todos.find_and_activate_ready diff --git a/test/views/todos_helper_test.rb b/test/views/todos_helper_test.rb index 843d859e..3ef4a81c 100644 --- a/test/views/todos_helper_test.rb +++ b/test/views/todos_helper_test.rb @@ -20,28 +20,28 @@ class TodosHelperTest < Test::Rails::HelperTestCase end def test_show_date_in_past - date = 3.days.ago.to_date + date = 3.days.ago html = show_date(date) formatted_date = format_date(date) assert_equal %Q{Scheduled to show 3 days ago }, html end def test_show_date_today - date = Time.zone.now.to_date + date = Time.zone.now html = show_date(date) formatted_date = format_date(date) assert_equal %Q{Show Today }, html end def test_show_date_tomorrow - date = 1.day.from_now.to_date + date = 1.day.from_now html = show_date(date) formatted_date = format_date(date) assert_equal %Q{Show Tomorrow }, html end def test_show_date_future - date = 10.days.from_now.to_date + date = 10.days.from_now html = show_date(date) formatted_date = format_date(date) assert_equal %Q{Show in 10 days }, html @@ -49,20 +49,22 @@ class TodosHelperTest < Test::Rails::HelperTestCase def test_remote_star_icon_unstarred @todo = flexmock(:id => 1, :to_param => 1, :description => 'Get gas', :starred? => false) - assert_remote_star_icon_helper_matches %r{Blank} + # added dot (.) to regexp because somehouw the extra dot is added in the tests while its not in the rendered html + assert_remote_star_icon_helper_matches %r{Blank} assert_behavior_registered end def test_remote_star_icon_starred @todo = flexmock(:id => 1, :to_param => 1, :description => 'Get gas', :starred? => true) - assert_remote_star_icon_helper_matches %r{Blank} + # added dot (.) to regexp because somehouw the extra dot is added in the tests while its not in the rendered html + assert_remote_star_icon_helper_matches %r{Blank} assert_behavior_registered end def assert_remote_star_icon_helper_matches(regex) @controller.send :initialise_js_behaviours #simulate before filter output = remote_star_icon - #puts output + # puts output assert output =~ regex @controller.send :store_js_behaviours #simulate after filter end @@ -74,7 +76,7 @@ class TodosHelperTest < Test::Rails::HelperTestCase rule = behaviors[:rules][0] assert_equal ".item-container a.star_item:click", rule[0] assert_equal "new Ajax.Request(this.href, {asynchronous:true, evalScripts:true, method:'put', parameters:{ _source_view : '' }})\n; return false;", - rule[1] + rule[1] end def protect_against_forgery?