diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index 2d0f95c6..cc629c6f 100755 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -6,8 +6,9 @@ class StatsController < ApplicationController def index @page_title = 'TRACKS::Statistics' - - @unique_tags = @tags.count(:all, {:group=>"tag_id"}) + + @tags_count = get_total_number_of_tags_of_user + @unique_tags_count = get_unique_tags_of_user.count @hidden_contexts = @contexts.hidden @first_action = @actions.find(:first, :order => "created_at ASC") @@ -394,7 +395,7 @@ class StatsController < ApplicationController def context_running_actions_data # get incomplete action count per visible context - # + # # Went from GROUP BY c.id to c.name for compatibility with postgresql. Since # the name is forced to be unique, this should work. @all_actions_per_context = @contexts.find_by_sql( @@ -643,11 +644,31 @@ class StatsController < ApplicationController private + def get_unique_tags_of_user + tag_ids = @actions.find_by_sql([ + "SELECT DISTINCT tags.id as id "+ + "FROM tags, taggings, todos "+ + "WHERE todos.user_id=? "+ + "AND tags.id = taggings.tag_id " + + "AND taggings.taggable_id = todos.id ", current_user.id]) + tags_ids_s = tag_ids.map(&:id).sort.join(",") + return Tag.find(:all, :conditions => "id in (" + tags_ids_s + ")") + end + + def get_total_number_of_tags_of_user + # same query as get_unique_tags_of_user except for the DISTINCT + return @actions.find_by_sql([ + "SELECT tags.id as id "+ + "FROM tags, taggings, todos "+ + "WHERE todos.user_id=? "+ + "AND tags.id = taggings.tag_id " + + "AND taggings.taggable_id = todos.id ", current_user.id]).count + end + def init @actions = @user.todos @projects = @user.projects @contexts = @user.contexts - @tags = @user.tags # default chart dimensions @chart_width=460 @@ -724,7 +745,7 @@ class StatsController < ApplicationController def get_stats_contexts # get action count per context for TOP 5 - # + # # Went from GROUP BY c.id to c.id, c.name for compatibility with postgresql. # Since the name is forced to be unique, this should work. @actions_per_context = @contexts.find_by_sql( @@ -737,7 +758,7 @@ class StatsController < ApplicationController ) # get incomplete action count per visible context for TOP 5 - # + # # Went from GROUP BY c.id to c.id, c.name for compatibility with postgresql. # Since the name is forced to be unique, this should work. @running_actions_per_context = @contexts.find_by_sql( @@ -752,7 +773,7 @@ class StatsController < ApplicationController def get_stats_projects # get the first 10 projects and their action count (all actions) - # + # # Went from GROUP BY p.id to p.name for compatibility with postgresql. Since # the name is forced to be unique, this should work. @projects_and_actions = @projects.find_by_sql( @@ -812,9 +833,10 @@ class StatsController < ApplicationController # Get the tag cloud for all tags for actions query = "SELECT tags.id, name, count(*) AS count" - query << " FROM taggings, tags" + query << " FROM taggings, tags, todos" query << " WHERE tags.id = tag_id" - query << " AND taggings.user_id="+@user.id.to_s+" " + query << " AND taggings.taggable_id = todos.id" + query << " AND todos.user_id="+current_user.id.to_s+" " query << " AND taggings.taggable_type='Todo' " query << " GROUP BY tags.id, tags.name" query << " ORDER BY count DESC, name" @@ -833,7 +855,7 @@ class StatsController < ApplicationController query = "SELECT tags.id, tags.name AS name, count(*) AS count" query << " FROM taggings, tags, todos" query << " WHERE tags.id = tag_id" - query << " AND taggings.user_id=? " + query << " AND todos.user_id=? " query << " AND taggings.taggable_type='Todo' " query << " AND taggings.taggable_id=todos.id " query << " AND (todos.created_at > ? OR " @@ -842,7 +864,7 @@ class StatsController < ApplicationController query << " ORDER BY count DESC, name" query << " LIMIT 100" @tags_for_cloud_90days = Tag.find_by_sql( - [query, @user.id, @cut_off_3months, @cut_off_3months] + [query, current_user.id, @cut_off_3months, @cut_off_3months] ).sort_by { |tag| tag.name.downcase } max_90days, @tags_min_90days = 0, 0 diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index cf36bfaa..2e6a1b5d 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -1,935 +1,932 @@ -class TodosController < ApplicationController - - helper :todos - - 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 = current_user.contexts.active - - respond_to do |format| - format.html &render_todos_html - format.m &render_todos_mobile - format.xml { render :xml => @todos.to_xml( :except => :user_id ) } - format.rss &render_rss_feed - format.atom &render_atom_feed - format.text &render_text_feed - format.ics &render_ical_feed - end - end - - def new - @projects = current_user.projects.active - @contexts = current_user.contexts.find(:all) - respond_to do |format| - format.m { - @new_mobile = true - @return_path=cookies[:mobile_url] - @mobile_from_context = current_user.contexts.find_by_id(params[:from_context]) if params[:from_context] - @mobile_from_project = current_user.projects.find_by_id(params[:from_project]) if params[:from_project] - if params[:from_project] && !params[:from_context] - # we have a project but not a context -> use the default context - @mobile_from_context = @mobile_from_project.default_context - end - render :action => "new" - } - end - end - - def create - @source_view = params['_source_view'] || 'todo' - p = TodoCreateParamsHelper.new(params, prefs) - p.parse_dates() unless mobile? - - @todo = current_user.todos.build(p.attributes) - - if p.project_specified_by_name? - project = current_user.projects.find_or_create_by_name(p.project_name) - @new_project_created = project.new_record_before_save? - @todo.project_id = project.id - end - - if p.context_specified_by_name? - context = current_user.contexts.find_or_create_by_name(p.context_name) - @new_context_created = context.new_record_before_save? - @not_done_todos = [@todo] if @new_context_created - @todo.context_id = context.id - end - - @saved = @todo.save - unless (@saved == false) || p.tag_list.blank? - @todo.tag_with(p.tag_list) - @todo.tags.reload - end - - respond_to do |format| - format.html { redirect_to :action => "index" } - format.m do - @return_path=cookies[:mobile_url] - # todo: use function for this fixed path - @return_path='/m' if @return_path.nil? - if @saved - redirect_to @return_path - else - @projects = current_user.projects.find(:all) - @contexts = current_user.contexts.find(:all) - render :action => "new" - end - end - format.js do - determine_down_count if @saved - @contexts = current_user.contexts.find(:all) if @new_context_created - @projects = current_user.projects.find(:all) if @new_project_created - @initial_context_name = params['default_context_name'] - @initial_project_name = params['default_project_name'] - render :action => 'create' - end - format.xml do - if @saved - head :created, :location => todo_url(@todo) - else - render :xml => @todo.errors.to_xml, :status => 422 - end - end - end - end - - def edit - @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 - end - - def show - respond_to do |format| - format.m do - @projects = current_user.projects.active - @contexts = current_user.contexts.find(:all) - @edit_mobile = true - @return_path=cookies[:mobile_url] - render :action => 'show' - end - format.xml { render :xml => @todo.to_xml( :root => 'todo', :except => :user_id ) } - end - 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 - @new_recurring_todo = check_for_next_todo(@todo) if @saved - - respond_to do |format| - format.js do - if @saved - 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 - format.xml { render :xml => @todo.to_xml( :except => :user_id ) } - format.html do - if @saved - # TODO: I think this will work, but can't figure out how to test it - notify :notice, "The action '#{@todo.description}' was marked as #{@todo.completed? ? 'complete' : 'incomplete' }" - redirect_to :action => "index" - else - notify :notice, "The action '#{@todo.description}' was NOT marked as #{@todo.completed? ? 'complete' : 'incomplete' } due to an error on the server.", "index" - redirect_to :action => "index" - end - end - end - end - - def toggle_star - @todo.toggle_star! - @saved = @todo.save! - respond_to do |format| - format.js - format.xml { render :xml => @todo.to_xml( :except => :user_id ) } - end - end - - def update - @source_view = params['_source_view'] || 'todo' - init_data_for_sidebar unless mobile? - if params[:tag_list] - @todo.tag_with(params[:tag_list]) - @todo.tags(true) #force a reload for proper rendering - end - @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 - else - project = current_user.projects.find_by_name(params['project_name'].strip) - unless project - project = current_user.projects.build - project.name = params['project_name'].strip - project.save - @new_project_created = true - end - end - params["todo"]["project_id"] = project.id - end - - if params['todo']['context_id'].blank? && !params['context_name'].blank? - context = current_user.contexts.find_by_name(params['context_name'].strip) - unless context - context = current_user.contexts.build - context.name = params['context_name'].strip - context.save - @new_context_created = true - @not_done_todos = [@todo] - end - params["todo"]["context_id"] = context.id - end - - if params["todo"].has_key?("due") - params["todo"]["due"] = parse_date_per_user_prefs(params["todo"]["due"]) - else - params["todo"]["due"] = "" - end - - if params['todo']['show_from'] - params['todo']['show_from'] = parse_date_per_user_prefs(params['todo']['show_from']) - end - - if params['done'] == '1' && !@todo.completed? - @todo.complete! - end - # strange. if checkbox is not checked, there is no 'done' in params. - # Therefore I've used the negation - if !(params['done'] == '1') && @todo.completed? - @todo.activate! - end - - @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? - - 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 - respond_to do |format| - format.js - format.xml { render :xml => @todo.to_xml( :except => :user_id ) } - format.m do - if @saved - if cookies[:mobile_url] - cookies[:mobile_url] = {:value => nil, :secure => TRACKS_COOKIES_SECURE} - redirect_to cookies[:mobile_url] - else - redirect_to formatted_todos_path(:m) - end - else - render :action => "edit", :format => :m - end - end - end - end - - 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 - @new_recurring_todo = check_for_next_todo(@todo) if @saved - - respond_to do |format| - - format.html do - if @saved - notify :notice, "Successfully deleted next action", 2.0 - redirect_to :action => 'index' - else - notify :error, "Failed to delete the action", 2.0 - redirect_to :action => 'index' - end - end - - format.js do - if @saved - 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 - end - - format.xml { render :text => '200 OK. Action deleted.', :status => 200 } - - end - end - - def completed - @page_title = "TRACKS::Completed tasks" - @done = current_user.completed_todos - @done_today = @done.completed_within Time.zone.now - 1.day - @done_this_week = @done.completed_within Time.zone.now - 1.week - @done_this_month = @done.completed_within Time.zone.now - 4.week - @count = @done_today.size + @done_this_week.size + @done_this_month.size - end - - def completed_archive - @page_title = "TRACKS::Archived completed tasks" - @done = current_user.completed_todos - @count = @done.size - @done_archive = @done.completed_more_than Time.zone.now - 28.days - end - - def list_deferred - @source_view = 'deferred' - @page_title = "TRACKS::Tickler" - - @projects = current_user.projects.find(:all, :include => [ :todos, :default_context ]) - @contexts_to_show = @contexts = current_user.contexts.find(:all, :include => [ :todos ]) - - current_user.deferred_todos.find_and_activate_ready - @not_done_todos = current_user.deferred_todos - @count = @not_done_todos.size - @down_count = @count - @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json unless mobile? - - respond_to do |format| - format.html - format.m { render :action => 'mobile_list_deferred' } - end - end - - # Check for any due tickler items, activate them Called by - # periodically_call_remote - def check_deferred - @due_tickles = current_user.deferred_todos.find_and_activate_ready - respond_to do |format| - format.html { redirect_to home_path } - format.js - end - end - - def filter_to_context - context = current_user.contexts.find(params['context']['id']) - redirect_to formatted_context_todos_path(context, :m) - end - - def filter_to_project - project = current_user.projects.find(params['project']['id']) - redirect_to formatted_project_todos_path(project, :m) - end - - # /todos/tag/[tag_name] shows all the actions tagged with tag_name - 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'], - :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'], - :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') - - @projects = current_user.projects - @contexts = current_user.contexts - @contexts_to_show = @contexts.reject {|x| x.hide? } - - # Set count badge to number of items with this tag - @not_done_todos.empty? ? @count = 0 : @count = @not_done_todos.size - @down_count = @count - - 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]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE} - render :action => "mobile_tag" - } - end - end - - 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']) - end - - def init - @source_view = params['_source_view'] || 'todo' - init_data_for_sidebar unless mobile? - init_todos - end - - def with_feed_query_scope(&block) - unless TodosController.is_feed_request(request) - Todo.send(:with_scope, :find => {:conditions => ['todos.state = ?', 'active']}) do - yield - return - end - end - condition_builder = FindConditionBuilder.new - - if params.key?('done') - condition_builder.add 'todos.state = ?', 'completed' - else - condition_builder.add 'todos.state = ?', 'active' - end - - @title = "Tracks - Next Actions" - @description = "Filter: " - - if params.key?('due') - due_within = params['due'].to_i - due_within_when = Time.zone.now + due_within.days - condition_builder.add('todos.due <= ?', due_within_when) - due_within_date_s = due_within_when.strftime("%Y-%m-%d") - @title << " due today" if (due_within == 0) - @title << " due within a week" if (due_within == 6) - @description << " with a due date #{due_within_date_s} or earlier" - end - - if params.key?('done') - done_in_last = params['done'].to_i - condition_builder.add('todos.completed_at >= ?', Time.zone.now - done_in_last.days) - @title << " actions completed" - @description << " in the last #{done_in_last.to_s} days" - end - - if params.key?('tag') - tag = Tag.find_by_name(params['tag']) - if tag.nil? - tag = Tag.new(:name => params['tag']) - end - condition_builder.add('taggings.tag_id = ?', tag.id) - end - - Todo.send :with_scope, :find => {:conditions => condition_builder.to_conditions} do - yield - end - - end - - def with_parent_resource_scope(&block) - @feed_title = "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 - else - yield - end - end - - def with_limit_scope(&block) - if params.key?('limit') - Todo.send :with_scope, :find => { :limit => params['limit'] } do - yield - end - if TodosController.is_feed_request(request) && @description - if params.key?('limit') - @description << "Lists the last #{params['limit']} incomplete next actions" - else - @description << "Lists incomplete next actions" - end - end - else - yield - end - end - - def init_todos - with_feed_query_scope do - with_parent_resource_scope do # @context or @project may get defined here - with_limit_scope do - - if mobile? - init_todos_for_mobile_view - else - - # Note: these next two finds were previously using - # current_users.todos.find but that broke with_scope for :limit - - # Exclude hidden projects from count on home page - @todos = Todo.find(:all, :conditions => ['todos.user_id = ?', current_user.id], :include => [ :project, :context, :tags ]) - - # Exclude hidden projects from the home page - @not_done_todos = Todo.find(:all, - :conditions => ['todos.user_id = ? AND contexts.hide = ? AND (projects.state = ? OR todos.project_id IS NULL)', - current_user.id, false, 'active'], - :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", - :include => [ :project, :context, :tags ]) - end - - end - end - end - end - - def init_todos_for_mobile_view - # Note: these next two finds were previously using current_users.todos.find - # but that broke with_scope for :limit - - # Exclude hidden projects from the home page - @not_done_todos = Todo.find(:all, - :conditions => ['todos.user_id = ? AND todos.state = ? AND contexts.hide = ? AND (projects.state = ? OR todos.project_id IS NULL)', - current_user.id, 'active', false, 'active'], - :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", - :include => [ :project, :context, :tags ]) - end - - def determine_down_count - source_view do |from| - from.todo do - @down_count = Todo.count( - :all, - :conditions => ['todos.user_id = ? and todos.state = ? and contexts.hide = ? AND (projects.state = ? OR todos.project_id IS NULL)', current_user.id, 'active', false, 'active'], - :include => [ :project, :context ]) - # #@down_count = Todo.count_by_sql(['SELECT COUNT(*) FROM todos, - # contexts WHERE todos.context_id = contexts.id and todos.user_id = ? - # and todos.state = ? and contexts.hide = ?', current_user.id, 'active', - # false]) - end - from.context do - @down_count = current_user.contexts.find(@todo.context_id).not_done_todo_count - end - from.project do - unless @todo.project_id == nil - @down_count = current_user.projects.find(@todo.project_id).not_done_todo_count(:include_project_hidden_todos => true) - @deferred_count = current_user.projects.find(@todo.project_id).deferred_todo_count - end - end - from.deferred do - @down_count = current_user.todos.count_in_state(:deferred) - end - from.tag do - @tag_name = params['_tag_name'] - @tag = Tag.find_by_name(@tag_name) - if @tag.nil? - @tag = Tag.new(:name => @tag_name) - end - tag_collection = @tag.todos - @not_done_todos = tag_collection.find(:all, :conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'active']) - @not_done_todos.empty? ? @down_count = 0 : @down_count = @not_done_todos.size - end - end - end - - def determine_remaining_in_context_count(context_id = @todo.context_id) - source_view do |from| - from.deferred { @remaining_in_context = current_user.contexts.find(context_id).deferred_todo_count } - from.tag { - tag = Tag.find_by_name(params['_tag_name']) - if tag.nil? - tag = Tag.new(:name => params['tag']) - end - @remaining_in_context = current_user.contexts.find(context_id).not_done_todo_count({:tag => tag.id}) - } - end - @remaining_in_context = current_user.contexts.find(context_id).not_done_todo_count if @remaining_in_context.nil? - end - - def determine_completed_count - source_view do |from| - from.todo do - @completed_count = Todo.count_by_sql(['SELECT COUNT(*) FROM todos, contexts WHERE todos.context_id = contexts.id and todos.user_id = ? and todos.state = ? and contexts.hide = ?', current_user.id, 'completed', false]) - end - from.context do - @completed_count = current_user.contexts.find(@todo.context_id).done_todo_count - end - from.project do - unless @todo.project_id == nil - @completed_count = current_user.projects.find(@todo.project_id).done_todo_count - end - end - end - end - - def render_todos_html - lambda do - @page_title = "TRACKS::List tasks" - - # If you've set no_completed to zero, the completed items box isn't shown - # on the home page - max_completed = current_user.prefs.show_number_completed - @done = current_user.completed_todos.find(:all, :limit => max_completed, :include => [ :context, :project, :tags ]) unless max_completed == 0 - - # Set count badge to number of not-done, not hidden context items - @count = 0 - @todos.each do |x| - if x.active? - if x.project.nil? - @count += 1 if !x.context.hide? - else - @count += 1 if x.project.active? && !x.context.hide? - end - end - end - - @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json - - render - end - end - - def render_todos_mobile - lambda do - @page_title = "All actions" - @home = true - cookies[:mobile_url]= { :value => request.request_uri, :secure => TRACKS_COOKIES_SECURE} - determine_down_count - - render :action => 'index' - end - end - - def render_rss_feed - lambda do - render_rss_feed_for @todos, :feed => todo_feed_options, - :item => { - :title => :description, - :link => lambda { |t| @project_feed.nil? ? context_url(t.context) : project_url(t.project) }, - :guid => lambda { |t| todo_url(t) }, - :description => todo_feed_content - } - end - end - - def todo_feed_options - options = Todo.feed_options(current_user) - options[:title] = @feed_title - return options - end - - def todo_feed_content - lambda do |i| - item_notes = sanitize(markdown( i.notes )) if i.notes? - due = "
Due: #{format_date(i.due)}
\n" if i.due? - done = "
Completed: #{format_date(i.completed_at)}
\n" if i.completed? - context_link = "#{ i.context.name }" - if i.project_id? - project_link = "#{ i.project.name }" - else - project_link = "none" - end - "#{done||''}#{due||''}#{item_notes||''}\n
Project: #{project_link}
\n
Context: #{context_link}
" - end - end - - def render_atom_feed - lambda do - render_atom_feed_for @todos, :feed => todo_feed_options, - :item => { - :title => :description, - :link => lambda { |t| context_url(t.context) }, - :description => todo_feed_content, - :author => lambda { |p| nil } - } - end - end - - def render_text_feed - lambda do - render :action => 'index', :layout => false, :content_type => Mime::TEXT - end - end - - def render_ical_feed - lambda do - render :action => 'index', :layout => false, :content_type => Mime::ICS - end - end - - def self.is_feed_request(req) - ['rss','atom','txt','ics'].include?(req.parameters[:format]) - end - - def check_for_next_todo(todo) - # check if this todo has a related recurring_todo. If so, create next todo - new_recurring_todo = nil - 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 - 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 - - def initialize - @queries = Array.new - @params = Array.new - end - - def add(query, param) - @queries << query - @params << param - end - - def to_conditions - [@queries.join(' AND ')] + @params - end - end - - class TodoCreateParamsHelper - - def initialize(params, prefs) - @params = params['request'] || params - @prefs = prefs - @attributes = params['request'] && params['request']['todo'] || params['todo'] - end - - def attributes - @attributes - end - - def show_from - @attributes['show_from'] - end - - def due - @attributes['due'] - end - - def project_name - @params['project_name'].strip unless @params['project_name'].nil? - end - - def context_name - @params['context_name'].strip unless @params['context_name'].nil? - end - - def tag_list - @params['tag_list'] - end - - def parse_dates() - @attributes['show_from'] = @prefs.parse_date(show_from) - @attributes['due'] = @prefs.parse_date(due) - @attributes['due'] ||= '' - end - - def project_specified_by_name? - return false unless @attributes['project_id'].blank? - return false if project_name.blank? - return false if project_name == 'None' - true - end - - def context_specified_by_name? - return false unless @attributes['context_id'].blank? - return false if context_name.blank? - true - end - - end -end +class TodosController < ApplicationController + + helper :todos + + 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 = current_user.contexts.active + + respond_to do |format| + format.html &render_todos_html + format.m &render_todos_mobile + format.xml { render :xml => @todos.to_xml( :except => :user_id ) } + format.rss &render_rss_feed + format.atom &render_atom_feed + format.text &render_text_feed + format.ics &render_ical_feed + end + end + + def new + @projects = current_user.projects.active + @contexts = current_user.contexts.find(:all) + respond_to do |format| + format.m { + @new_mobile = true + @return_path=cookies[:mobile_url] + @mobile_from_context = current_user.contexts.find_by_id(params[:from_context]) if params[:from_context] + @mobile_from_project = current_user.projects.find_by_id(params[:from_project]) if params[:from_project] + if params[:from_project] && !params[:from_context] + # we have a project but not a context -> use the default context + @mobile_from_context = @mobile_from_project.default_context + end + render :action => "new" + } + end + end + + def create + @source_view = params['_source_view'] || 'todo' + p = TodoCreateParamsHelper.new(params, prefs) + p.parse_dates() unless mobile? + + @todo = current_user.todos.build(p.attributes) + + if p.project_specified_by_name? + project = current_user.projects.find_or_create_by_name(p.project_name) + @new_project_created = project.new_record_before_save? + @todo.project_id = project.id + end + + if p.context_specified_by_name? + context = current_user.contexts.find_or_create_by_name(p.context_name) + @new_context_created = context.new_record_before_save? + @not_done_todos = [@todo] if @new_context_created + @todo.context_id = context.id + end + + @saved = @todo.save + unless (@saved == false) || p.tag_list.blank? + @todo.tag_with(p.tag_list) + @todo.tags.reload + end + + respond_to do |format| + format.html { redirect_to :action => "index" } + format.m do + @return_path=cookies[:mobile_url] + # todo: use function for this fixed path + @return_path='/m' if @return_path.nil? + if @saved + redirect_to @return_path + else + @projects = current_user.projects.find(:all) + @contexts = current_user.contexts.find(:all) + render :action => "new" + end + end + format.js do + determine_down_count if @saved + @contexts = current_user.contexts.find(:all) if @new_context_created + @projects = current_user.projects.find(:all) if @new_project_created + @initial_context_name = params['default_context_name'] + @initial_project_name = params['default_project_name'] + render :action => 'create' + end + format.xml do + if @saved + head :created, :location => todo_url(@todo) + else + render :xml => @todo.errors.to_xml, :status => 422 + end + end + end + end + + def edit + @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 + end + + def show + respond_to do |format| + format.m do + @projects = current_user.projects.active + @contexts = current_user.contexts.find(:all) + @edit_mobile = true + @return_path=cookies[:mobile_url] + render :action => 'show' + end + format.xml { render :xml => @todo.to_xml( :root => 'todo', :except => :user_id ) } + end + 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 + @new_recurring_todo = check_for_next_todo(@todo) if @saved + + respond_to do |format| + format.js do + if @saved + 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 + format.xml { render :xml => @todo.to_xml( :except => :user_id ) } + format.html do + if @saved + # TODO: I think this will work, but can't figure out how to test it + notify :notice, "The action '#{@todo.description}' was marked as #{@todo.completed? ? 'complete' : 'incomplete' }" + redirect_to :action => "index" + else + notify :notice, "The action '#{@todo.description}' was NOT marked as #{@todo.completed? ? 'complete' : 'incomplete' } due to an error on the server.", "index" + redirect_to :action => "index" + end + end + end + end + + def toggle_star + @todo.toggle_star! + @saved = @todo.save! + respond_to do |format| + format.js + format.xml { render :xml => @todo.to_xml( :except => :user_id ) } + end + end + + def update + @source_view = params['_source_view'] || 'todo' + init_data_for_sidebar unless mobile? + @todo.tag_with(params[:tag_list]) if params[:tag_list] + @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 + else + project = current_user.projects.find_by_name(params['project_name'].strip) + unless project + project = current_user.projects.build + project.name = params['project_name'].strip + project.save + @new_project_created = true + end + end + params["todo"]["project_id"] = project.id + end + + if params['todo']['context_id'].blank? && !params['context_name'].blank? + context = current_user.contexts.find_by_name(params['context_name'].strip) + unless context + context = current_user.contexts.build + context.name = params['context_name'].strip + context.save + @new_context_created = true + @not_done_todos = [@todo] + end + params["todo"]["context_id"] = context.id + end + + if params["todo"].has_key?("due") + params["todo"]["due"] = parse_date_per_user_prefs(params["todo"]["due"]) + else + params["todo"]["due"] = "" + end + + if params['todo']['show_from'] + params['todo']['show_from'] = parse_date_per_user_prefs(params['todo']['show_from']) + end + + if params['done'] == '1' && !@todo.completed? + @todo.complete! + end + # strange. if checkbox is not checked, there is no 'done' in params. + # Therefore I've used the negation + if !(params['done'] == '1') && @todo.completed? + @todo.activate! + end + + @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? + + 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 + respond_to do |format| + format.js + format.xml { render :xml => @todo.to_xml( :except => :user_id ) } + format.m do + if @saved + if cookies[:mobile_url] + cookies[:mobile_url] = {:value => nil, :secure => TRACKS_COOKIES_SECURE} + redirect_to cookies[:mobile_url] + else + redirect_to formatted_todos_path(:m) + end + else + render :action => "edit", :format => :m + end + end + end + end + + 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 + @new_recurring_todo = check_for_next_todo(@todo) if @saved + + respond_to do |format| + + format.html do + if @saved + notify :notice, "Successfully deleted next action", 2.0 + redirect_to :action => 'index' + else + notify :error, "Failed to delete the action", 2.0 + redirect_to :action => 'index' + end + end + + format.js do + if @saved + 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 + end + + format.xml { render :text => '200 OK. Action deleted.', :status => 200 } + + end + end + + def completed + @page_title = "TRACKS::Completed tasks" + @done = current_user.completed_todos + @done_today = @done.completed_within Time.zone.now - 1.day + @done_this_week = @done.completed_within Time.zone.now - 1.week + @done_this_month = @done.completed_within Time.zone.now - 4.week + @count = @done_today.size + @done_this_week.size + @done_this_month.size + end + + def completed_archive + @page_title = "TRACKS::Archived completed tasks" + @done = current_user.completed_todos + @count = @done.size + @done_archive = @done.completed_more_than Time.zone.now - 28.days + end + + def list_deferred + @source_view = 'deferred' + @page_title = "TRACKS::Tickler" + + @projects = current_user.projects.find(:all, :include => [ :todos, :default_context ]) + @contexts_to_show = @contexts = current_user.contexts.find(:all, :include => [ :todos ]) + + current_user.deferred_todos.find_and_activate_ready + @not_done_todos = current_user.deferred_todos + @count = @not_done_todos.size + @down_count = @count + @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json unless mobile? + + respond_to do |format| + format.html + format.m { render :action => 'mobile_list_deferred' } + end + end + + # Check for any due tickler items, activate them Called by + # periodically_call_remote + def check_deferred + @due_tickles = current_user.deferred_todos.find_and_activate_ready + respond_to do |format| + format.html { redirect_to home_path } + format.js + end + end + + def filter_to_context + context = current_user.contexts.find(params['context']['id']) + redirect_to formatted_context_todos_path(context, :m) + end + + def filter_to_project + project = current_user.projects.find(params['project']['id']) + redirect_to formatted_project_todos_path(project, :m) + end + + # /todos/tag/[tag_name] shows all the actions tagged with tag_name + 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 => ['todos.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'], + :order => 'todos.completed_at DESC, todos.created_at DESC') + @deferred = tag_collection.find(:all, + :conditions => ['todos.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 => ['todos.user_id = ? and state = ?', current_user.id, 'completed'], + :order => 'todos.completed_at DESC') + + @projects = current_user.projects + @contexts = current_user.contexts + @contexts_to_show = @contexts.reject {|x| x.hide? } + + # Set count badge to number of items with this tag + @not_done_todos.empty? ? @count = 0 : @count = @not_done_todos.size + @down_count = @count + + 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]= {:value => request.request_uri, :secure => TRACKS_COOKIES_SECURE} + render :action => "mobile_tag" + } + end + end + + 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']) + end + + def init + @source_view = params['_source_view'] || 'todo' + init_data_for_sidebar unless mobile? + init_todos + end + + def with_feed_query_scope(&block) + unless TodosController.is_feed_request(request) + Todo.send(:with_scope, :find => {:conditions => ['todos.state = ?', 'active']}) do + yield + return + end + end + condition_builder = FindConditionBuilder.new + + if params.key?('done') + condition_builder.add 'todos.state = ?', 'completed' + else + condition_builder.add 'todos.state = ?', 'active' + end + + @title = "Tracks - Next Actions" + @description = "Filter: " + + if params.key?('due') + due_within = params['due'].to_i + due_within_when = Time.zone.now + due_within.days + condition_builder.add('todos.due <= ?', due_within_when) + due_within_date_s = due_within_when.strftime("%Y-%m-%d") + @title << " due today" if (due_within == 0) + @title << " due within a week" if (due_within == 6) + @description << " with a due date #{due_within_date_s} or earlier" + end + + if params.key?('done') + done_in_last = params['done'].to_i + condition_builder.add('todos.completed_at >= ?', Time.zone.now - done_in_last.days) + @title << " actions completed" + @description << " in the last #{done_in_last.to_s} days" + end + + if params.key?('tag') + tag = Tag.find_by_name(params['tag']) + if tag.nil? + tag = Tag.new(:name => params['tag']) + end + condition_builder.add('taggings.tag_id = ?', tag.id) + end + + Todo.send :with_scope, :find => {:conditions => condition_builder.to_conditions} do + yield + end + + end + + def with_parent_resource_scope(&block) + @feed_title = "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 + else + yield + end + end + + def with_limit_scope(&block) + if params.key?('limit') + Todo.send :with_scope, :find => { :limit => params['limit'] } do + yield + end + if TodosController.is_feed_request(request) && @description + if params.key?('limit') + @description << "Lists the last #{params['limit']} incomplete next actions" + else + @description << "Lists incomplete next actions" + end + end + else + yield + end + end + + def init_todos + with_feed_query_scope do + with_parent_resource_scope do # @context or @project may get defined here + with_limit_scope do + + if mobile? + init_todos_for_mobile_view + else + + # Note: these next two finds were previously using + # current_users.todos.find but that broke with_scope for :limit + + # Exclude hidden projects from count on home page + @todos = Todo.find(:all, :conditions => ['todos.user_id = ?', current_user.id], :include => [ :project, :context, :tags ]) + + # Exclude hidden projects from the home page + @not_done_todos = Todo.find(:all, + :conditions => ['todos.user_id = ? AND contexts.hide = ? AND (projects.state = ? OR todos.project_id IS NULL)', + current_user.id, false, 'active'], + :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", + :include => [ :project, :context, :tags ]) + end + + end + end + end + end + + def init_todos_for_mobile_view + # Note: these next two finds were previously using current_users.todos.find + # but that broke with_scope for :limit + + # Exclude hidden projects from the home page + @not_done_todos = Todo.find(:all, + :conditions => ['todos.user_id = ? AND todos.state = ? AND contexts.hide = ? AND (projects.state = ? OR todos.project_id IS NULL)', + current_user.id, 'active', false, 'active'], + :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", + :include => [ :project, :context, :tags ]) + end + + def determine_down_count + source_view do |from| + from.todo do + @down_count = Todo.count( + :all, + :conditions => ['todos.user_id = ? and todos.state = ? and contexts.hide = ? AND (projects.state = ? OR todos.project_id IS NULL)', current_user.id, 'active', false, 'active'], + :include => [ :project, :context ]) + # #@down_count = Todo.count_by_sql(['SELECT COUNT(*) FROM todos, + # contexts WHERE todos.context_id = contexts.id and todos.user_id = ? + # and todos.state = ? and contexts.hide = ?', current_user.id, 'active', + # false]) + end + from.context do + @down_count = current_user.contexts.find(@todo.context_id).not_done_todo_count + end + from.project do + unless @todo.project_id == nil + @down_count = current_user.projects.find(@todo.project_id).not_done_todo_count(:include_project_hidden_todos => true) + @deferred_count = current_user.projects.find(@todo.project_id).deferred_todo_count + end + end + from.deferred do + @down_count = current_user.todos.count_in_state(:deferred) + end + from.tag do + @tag_name = params['_tag_name'] + @tag = Tag.find_by_name(@tag_name) + if @tag.nil? + @tag = Tag.new(:name => @tag_name) + end + tag_collection = @tag.todos + @not_done_todos = tag_collection.find(:all, :conditions => ['taggings.user_id = ? and state = ?', current_user.id, 'active']) + @not_done_todos.empty? ? @down_count = 0 : @down_count = @not_done_todos.size + end + end + end + + def determine_remaining_in_context_count(context_id = @todo.context_id) + source_view do |from| + from.deferred { @remaining_in_context = current_user.contexts.find(context_id).deferred_todo_count } + from.tag { + tag = Tag.find_by_name(params['_tag_name']) + if tag.nil? + tag = Tag.new(:name => params['tag']) + end + @remaining_in_context = current_user.contexts.find(context_id).not_done_todo_count({:tag => tag.id}) + } + end + @remaining_in_context = current_user.contexts.find(context_id).not_done_todo_count if @remaining_in_context.nil? + end + + def determine_completed_count + source_view do |from| + from.todo do + @completed_count = Todo.count_by_sql(['SELECT COUNT(*) FROM todos, contexts WHERE todos.context_id = contexts.id and todos.user_id = ? and todos.state = ? and contexts.hide = ?', current_user.id, 'completed', false]) + end + from.context do + @completed_count = current_user.contexts.find(@todo.context_id).done_todo_count + end + from.project do + unless @todo.project_id == nil + @completed_count = current_user.projects.find(@todo.project_id).done_todo_count + end + end + end + end + + def render_todos_html + lambda do + @page_title = "TRACKS::List tasks" + + # If you've set no_completed to zero, the completed items box isn't shown + # on the home page + max_completed = current_user.prefs.show_number_completed + @done = current_user.completed_todos.find(:all, :limit => max_completed, :include => [ :context, :project, :tags ]) unless max_completed == 0 + + # Set count badge to number of not-done, not hidden context items + @count = 0 + @todos.each do |x| + if x.active? + if x.project.nil? + @count += 1 if !x.context.hide? + else + @count += 1 if x.project.active? && !x.context.hide? + end + end + end + + @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json + + render + end + end + + def render_todos_mobile + lambda do + @page_title = "All actions" + @home = true + cookies[:mobile_url]= { :value => request.request_uri, :secure => TRACKS_COOKIES_SECURE} + determine_down_count + + render :action => 'index' + end + end + + def render_rss_feed + lambda do + render_rss_feed_for @todos, :feed => todo_feed_options, + :item => { + :title => :description, + :link => lambda { |t| @project_feed.nil? ? context_url(t.context) : project_url(t.project) }, + :guid => lambda { |t| todo_url(t) }, + :description => todo_feed_content + } + end + end + + def todo_feed_options + options = Todo.feed_options(current_user) + options[:title] = @feed_title + return options + end + + def todo_feed_content + lambda do |i| + item_notes = sanitize(markdown( i.notes )) if i.notes? + due = "
Due: #{format_date(i.due)}
\n" if i.due? + done = "
Completed: #{format_date(i.completed_at)}
\n" if i.completed? + context_link = "#{ i.context.name }" + if i.project_id? + project_link = "#{ i.project.name }" + else + project_link = "none" + end + "#{done||''}#{due||''}#{item_notes||''}\n
Project: #{project_link}
\n
Context: #{context_link}
" + end + end + + def render_atom_feed + lambda do + render_atom_feed_for @todos, :feed => todo_feed_options, + :item => { + :title => :description, + :link => lambda { |t| context_url(t.context) }, + :description => todo_feed_content, + :author => lambda { |p| nil } + } + end + end + + def render_text_feed + lambda do + render :action => 'index', :layout => false, :content_type => Mime::TEXT + end + end + + def render_ical_feed + lambda do + render :action => 'index', :layout => false, :content_type => Mime::ICS + end + end + + def self.is_feed_request(req) + ['rss','atom','txt','ics'].include?(req.parameters[:format]) + end + + def check_for_next_todo(todo) + # check if this todo has a related recurring_todo. If so, create next todo + new_recurring_todo = nil + 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 + 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 + + def initialize + @queries = Array.new + @params = Array.new + end + + def add(query, param) + @queries << query + @params << param + end + + def to_conditions + [@queries.join(' AND ')] + @params + end + end + + class TodoCreateParamsHelper + + def initialize(params, prefs) + @params = params['request'] || params + @prefs = prefs + @attributes = params['request'] && params['request']['todo'] || params['todo'] + end + + def attributes + @attributes + end + + def show_from + @attributes['show_from'] + end + + def due + @attributes['due'] + end + + def project_name + @params['project_name'].strip unless @params['project_name'].nil? + end + + def context_name + @params['context_name'].strip unless @params['context_name'].nil? + end + + def tag_list + @params['tag_list'] + end + + def parse_dates() + @attributes['show_from'] = @prefs.parse_date(show_from) + @attributes['due'] = @prefs.parse_date(due) + @attributes['due'] ||= '' + end + + def project_specified_by_name? + return false unless @attributes['project_id'].blank? + return false if project_name.blank? + return false if project_name == 'None' + true + end + + def context_specified_by_name? + return false unless @attributes['context_id'].blank? + return false if context_name.blank? + true + end + + end +end diff --git a/app/models/tagging.rb b/app/models/tagging.rb index de6c3743..4d16acd2 100644 --- a/app/models/tagging.rb +++ b/app/models/tagging.rb @@ -5,7 +5,6 @@ class Tagging < ActiveRecord::Base belongs_to :tag belongs_to :taggable, :polymorphic => true - # belongs_to :user # If you also need to use acts_as_list, you will have to manage the tagging positions manually by creating decorated join records when you associate Tags with taggables. # acts_as_list :scope => :taggable diff --git a/app/models/user.rb b/app/models/user.rb index 6da8bf2c..f2ecedb8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -104,8 +104,6 @@ class User < ActiveRecord::Base end has_many :notes, :order => "created_at DESC", :dependent => :delete_all has_one :preference, :dependent => :destroy - has_many :taggings - has_many :tags, :through => :taggings, :select => "DISTINCT tags.*" attr_protected :is_admin diff --git a/app/views/stats/_totals.rhtml b/app/views/stats/_totals.rhtml index ebb3b986..fe83f388 100755 --- a/app/views/stats/_totals.rhtml +++ b/app/views/stats/_totals.rhtml @@ -16,7 +16,7 @@ deferred actions.

a total of <%= @actions.count %> actions. <%= @actions.count(:conditions => "NOT completed_at IS NULL") %> of these are completed. -

You have <%= @tags.count-%> tags placed on actions. Of those tags, -<%= @unique_tags.size -%> are unique. +

You have <%= @tags_count-%> tags placed on actions. Of those tags, +<%= @unique_tags_count -%> are unique. <% end -%> diff --git a/db/migrate/045_remove_user_from_taggings.rb.rb b/db/migrate/045_remove_user_from_taggings.rb.rb new file mode 100644 index 00000000..dec44876 --- /dev/null +++ b/db/migrate/045_remove_user_from_taggings.rb.rb @@ -0,0 +1,9 @@ +class RemoveUserFromTaggings < ActiveRecord::Migration + def self.up + remove_column :taggings, :user_id + end + + def self.down + add_column :taggings, :user_id, :integer + end +end \ No newline at end of file diff --git a/test/fixtures/taggings.yml b/test/fixtures/taggings.yml index 2d1b1de7..880ba6b4 100644 --- a/test/fixtures/taggings.yml +++ b/test/fixtures/taggings.yml @@ -4,14 +4,12 @@ foo_bar1: tag_id: 1 taggable_id: 1 # Call Bill Gates taggable_type: Todo - user_id: 1 foo_bar2: id: 2 tag_id: 2 taggable_id: 1 # Call Bill Gates taggable_type: Todo - user_id: 1 # Todo 2 should be tagged with foo foo1: @@ -19,11 +17,9 @@ foo1: tag_id: 1 taggable_id: 2 # Call dinosaur exterminator taggable_type: Todo - user_id: 1 foo2: id: 4 tag_id: 1 taggable_id: 3 # Buy milk - completed - taggable_type: Todo - user_id: 1 + taggable_type: Todo \ No newline at end of file diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index c66906aa..db82051e 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -232,7 +232,7 @@ class ProjectsControllerTest < TodoContainerControllerTestBase login_as :admin_user u = users(:admin_user) post :alphabetize, :state => "active", :format => 'js' - assert_equal 1, projects(:timemachine).position + assert_equal 1, projects(:timemachine).position assert_equal 2, projects(:gardenclean).position assert_equal 3, projects(:moremoney).position end diff --git a/test/functional/stats_controller_test.rb b/test/functional/stats_controller_test.rb index f3f2c64a..6ca0b8fd 100755 --- a/test/functional/stats_controller_test.rb +++ b/test/functional/stats_controller_test.rb @@ -57,8 +57,8 @@ class StatsControllerTest < Test::Unit::TestCase assert_equal 3, assigns['projects'].count(:conditions => "state = 'active'") assert_equal 10, assigns['contexts'].count assert_equal 16, assigns['actions'].count - assert_equal 4, assigns['tags'].count - assert_equal 2, assigns['unique_tags'].size + assert_equal 4, assigns['tags_count'] + assert_equal 2, assigns['unique_tags_count'] assert_equal 2.week.ago.utc.beginning_of_day, assigns['first_action'].created_at end diff --git a/test/functional/todos_controller_test.rb b/test/functional/todos_controller_test.rb index 624a702d..6b93857e 100644 --- a/test/functional/todos_controller_test.rb +++ b/test/functional/todos_controller_test.rb @@ -1,488 +1,488 @@ -require File.dirname(__FILE__) + '/../test_helper' -require 'todos_controller' - -# Re-raise errors caught by the controller. -class TodosController; def rescue_action(e) raise e end; end - -class TodosControllerTest < Test::Rails::TestCase - fixtures :users, :preferences, :projects, :contexts, :todos, :tags, :taggings, :recurring_todos - - def setup - @controller = TodosController.new - @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new - end - - def test_get_index_when_not_logged_in - get :index - assert_redirected_to :controller => 'login', :action => 'login' - end - - def test_not_done_counts - login_as(:admin_user) - get :index - assert_equal 2, assigns['project_not_done_counts'][projects(:timemachine).id] - assert_equal 3, assigns['context_not_done_counts'][contexts(:call).id] - assert_equal 1, assigns['context_not_done_counts'][contexts(:lab).id] - end - - def test_tag_is_retrieved_properly - login_as(:admin_user) - get :index - t = assigns['not_done_todos'].find{|t| t.id == 2} - assert_equal 1, t.tags.count - assert_equal 'foo', t.tags[0].name - assert !t.starred? - end - - def test_not_done_counts_after_hiding_project - p = Project.find(1) - p.hide! - p.save! - login_as(:admin_user) - get :index - assert_equal nil, assigns['project_not_done_counts'][projects(:timemachine).id] - assert_equal 2, assigns['context_not_done_counts'][contexts(:call).id] - assert_equal nil, assigns['context_not_done_counts'][contexts(:lab).id] - end - - def test_not_done_counts_after_hiding_and_unhiding_project - p = Project.find(1) - p.hide! - p.save! - p.activate! - p.save! - login_as(:admin_user) - get :index - assert_equal 2, assigns['project_not_done_counts'][projects(:timemachine).id] - assert_equal 3, assigns['context_not_done_counts'][contexts(:call).id] - assert_equal 1, assigns['context_not_done_counts'][contexts(:lab).id] - end - - def test_deferred_count_for_project_source_view - login_as(:admin_user) - xhr :post, :toggle_check, :id => 5, :_source_view => 'project' - assert_equal 1, assigns['deferred_count'] - xhr :post, :toggle_check, :id => 15, :_source_view => 'project' - assert_equal 0, assigns['deferred_count'] - end - - def test_destroy_todo - 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' - end - - def test_create_todo - assert_difference Todo, :count do - login_as(:admin_user) - put :create, :_source_view => 'todo', "context_name"=>"library", "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" - end - end - - def test_create_todo_via_xml - login_as(:admin_user) - assert_difference Todo, :count do - put :create, :format => "xml", "request" => { "context_name"=>"library", "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 201 - end - end - - def test_fail_to_create_todo_via_xml - login_as(:admin_user) - # #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" - end - end - - def test_create_deferred_todo - original_todo_count = Todo.count - login_as(:admin_user) - put :create, :_source_view => 'todo', "context_name"=>"library", "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/2026", 'show_from' => '30/10/2026'}, "tag_list"=>"foo bar" - assert_equal original_todo_count + 1, Todo.count - end - - def test_update_todo_project - t = Todo.find(1) - login_as(:admin_user) - xhr :post, :update, :id => 1, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"Build a working time machine", "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" - t = Todo.find(1) - assert_equal 1, t.project_id - end - - def test_update_todo_project_to_none - t = Todo.find(1) - login_as(:admin_user) - xhr :post, :update, :id => 1, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"None", "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" - t = Todo.find(1) - assert_nil t.project_id - end - - def test_update_todo_to_deferred_is_reflected_in_badge_count - login_as(:admin_user) - get :index - 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 10, assigns['down_count'] - end - - def test_update_todo - t = Todo.find(1) - login_as(:admin_user) - xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo, bar" - t = Todo.find(1) - assert_equal "Call Warren Buffet to find out how much he makes per day", t.description - assert_equal "bar, foo", t.tag_list - expected = Date.new(2006,11,30) - actual = t.due.to_date - assert_equal expected, actual, "Expected #{expected.to_s(:db)}, was #{actual.to_s(:db)}" - end - - def test_update_todos_with_blank_project_name - t = Todo.find(1) - login_as(:admin_user) - xhr :post, :update, :id => 1, :_source_view => 'todo', :project_name => '', "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo, bar" - t.reload - assert t.project.nil? - end - - def test_update_todo_tags_to_none - t = Todo.find(1) - login_as(:admin_user) - xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"" - t = Todo.find(1) - assert_equal true, t.tag_list.empty? - end - - def test_update_todo_tags_with_whitespace_and_dots - t = Todo.find(1) - login_as(:admin_user) - taglist = " one , two,three ,four, 8.1.2, version1.5" - xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>taglist - t = Todo.find(1) - assert_equal "8.1.2, four, one, three, two, version1.5", t.tag_list - end - - def test_find_tagged_with - login_as(:admin_user) - @user = User.find(@request.session['user_id']) - tag = Tag.find_by_name('foo').todos - @tagged = tag.find(:all, :conditions => ['taggings.user_id = ?', @user.id]).size - get :tag, :name => 'foo' - assert_response :success - assert_equal 3, @tagged - end - - def test_rss_feed - login_as(:admin_user) - get :index, { :format => "rss" } - assert_equal 'application/rss+xml', @response.content_type - # puts @response.body - - assert_xml_select 'rss[version="2.0"]' do - assert_select 'channel' do - assert_select '>title', 'Actions' - assert_select '>description', "Actions for #{users(:admin_user).display_name}" - assert_select 'language', 'en-us' - assert_select 'ttl', '40' - 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', todos(:book).updated_at.to_s(:rfc822) - end - end - end - end - - def test_rss_feed_with_limit - login_as(:admin_user) - get :index, { :format => "rss", :limit => '5' } - - assert_xml_select 'rss[version="2.0"]' do - assert_select 'channel' do - assert_select '>title', 'Actions' - assert_select '>description', "Actions for #{users(:admin_user).display_name}" - assert_select 'item', 5 do - assert_select 'title', /.+/ - assert_select 'description', /.*/ - end - end - end - end - - def test_rss_feed_not_accessible_to_anonymous_user_without_token - login_as nil - get :index, { :format => "rss" } - assert_response 401 - end - - def test_rss_feed_not_accessible_to_anonymous_user_with_invalid_token - login_as nil - get :index, { :format => "rss", :token => 'foo' } - assert_response 401 - end - - def test_rss_feed_accessible_to_anonymous_user_with_valid_token - login_as nil - get :index, { :format => "rss", :token => users(:admin_user).token } - assert_response :ok - end - - def test_atom_feed_content - login_as :admin_user - get :index, { :format => "atom" } - assert_equal 'application/atom+xml', @response.content_type - # #puts @response.body - - assert_xml_select 'feed[xmlns="http://www.w3.org/2005/Atom"]' do - assert_xml_select '>title', 'Actions' - assert_xml_select '>subtitle', "Actions for #{users(:admin_user).display_name}" - assert_xml_select 'entry', 11 do - assert_xml_select 'title', /.+/ - assert_xml_select 'content[type="html"]', /.*/ - assert_xml_select 'published', /(#{Regexp.escape(todos(:book).updated_at.xmlschema)}|#{Regexp.escape(projects(:moremoney).updated_at.xmlschema)})/ - end - end - end - - def test_atom_feed_not_accessible_to_anonymous_user_without_token - login_as nil - get :index, { :format => "atom" } - assert_response 401 - end - - def test_atom_feed_not_accessible_to_anonymous_user_with_invalid_token - login_as nil - get :index, { :format => "atom", :token => 'foo' } - assert_response 401 - end - - def test_atom_feed_accessible_to_anonymous_user_with_valid_token - login_as nil - get :index, { :format => "atom", :token => users(:admin_user).token } - assert_response :ok - end - - def test_text_feed_content - login_as(:admin_user) - get :index, { :format => "txt" } - assert_equal 'text/plain', @response.content_type - assert !(/ /.match(@response.body)) - # #puts @response.body - end - - def test_text_feed_not_accessible_to_anonymous_user_without_token - login_as nil - get :index, { :format => "txt" } - assert_response 401 - end - - def test_text_feed_not_accessible_to_anonymous_user_with_invalid_token - login_as nil - get :index, { :format => "txt", :token => 'foo' } - assert_response 401 - end - - def test_text_feed_accessible_to_anonymous_user_with_valid_token - login_as nil - get :index, { :format => "txt", :token => users(:admin_user).token } - assert_response :ok - end - - def test_ical_feed_content - login_as :admin_user - get :index, { :format => "ics" } - assert_equal 'text/calendar', @response.content_type - assert !(/ /.match(@response.body)) - # #puts @response.body - end - - def test_mobile_index_uses_text_html_content_type - login_as(:admin_user) - get :index, { :format => "m" } - assert_equal 'text/html', @response.content_type - end - - def test_mobile_index_assigns_down_count - login_as(:admin_user) - get :index, { :format => "m" } - 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"}} - t = Todo.find_by_description("test_mobile_create_action") - assert_not_nil t - assert_equal 2, t.context_id - assert_equal 1, t.project_id - assert t.active? - assert_equal 'test notes', t.notes - assert_nil t.show_from - 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"}} - 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"} - assert_template 'todos/new' - end - - def test_index_html_assigns_default_project_name_map - login_as(:admin_user) - get :index, {"format"=>"html"} - 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, today.day)+1.month, 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 +require File.dirname(__FILE__) + '/../test_helper' +require 'todos_controller' + +# Re-raise errors caught by the controller. +class TodosController; def rescue_action(e) raise e end; end + +class TodosControllerTest < Test::Rails::TestCase + fixtures :users, :preferences, :projects, :contexts, :todos, :tags, :taggings, :recurring_todos + + def setup + @controller = TodosController.new + @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new + end + + def test_get_index_when_not_logged_in + get :index + assert_redirected_to :controller => 'login', :action => 'login' + end + + def test_not_done_counts + login_as(:admin_user) + get :index + assert_equal 2, assigns['project_not_done_counts'][projects(:timemachine).id] + assert_equal 3, assigns['context_not_done_counts'][contexts(:call).id] + assert_equal 1, assigns['context_not_done_counts'][contexts(:lab).id] + end + + def test_tag_is_retrieved_properly + login_as(:admin_user) + get :index + t = assigns['not_done_todos'].find{|t| t.id == 2} + assert_equal 1, t.tags.count + assert_equal 'foo', t.tags[0].name + assert !t.starred? + end + + def test_not_done_counts_after_hiding_project + p = Project.find(1) + p.hide! + p.save! + login_as(:admin_user) + get :index + assert_equal nil, assigns['project_not_done_counts'][projects(:timemachine).id] + assert_equal 2, assigns['context_not_done_counts'][contexts(:call).id] + assert_equal nil, assigns['context_not_done_counts'][contexts(:lab).id] + end + + def test_not_done_counts_after_hiding_and_unhiding_project + p = Project.find(1) + p.hide! + p.save! + p.activate! + p.save! + login_as(:admin_user) + get :index + assert_equal 2, assigns['project_not_done_counts'][projects(:timemachine).id] + assert_equal 3, assigns['context_not_done_counts'][contexts(:call).id] + assert_equal 1, assigns['context_not_done_counts'][contexts(:lab).id] + end + + def test_deferred_count_for_project_source_view + login_as(:admin_user) + xhr :post, :toggle_check, :id => 5, :_source_view => 'project' + assert_equal 1, assigns['deferred_count'] + xhr :post, :toggle_check, :id => 15, :_source_view => 'project' + assert_equal 0, assigns['deferred_count'] + end + + def test_destroy_todo + 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' + end + + def test_create_todo + assert_difference Todo, :count do + login_as(:admin_user) + put :create, :_source_view => 'todo', "context_name"=>"library", "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" + end + end + + def test_create_todo_via_xml + login_as(:admin_user) + assert_difference Todo, :count do + put :create, :format => "xml", "request" => { "context_name"=>"library", "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 201 + end + end + + def test_fail_to_create_todo_via_xml + login_as(:admin_user) + # #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" + end + end + + def test_create_deferred_todo + original_todo_count = Todo.count + login_as(:admin_user) + put :create, :_source_view => 'todo', "context_name"=>"library", "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/2026", 'show_from' => '30/10/2026'}, "tag_list"=>"foo bar" + assert_equal original_todo_count + 1, Todo.count + end + + def test_update_todo_project + t = Todo.find(1) + login_as(:admin_user) + xhr :post, :update, :id => 1, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"Build a working time machine", "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" + t = Todo.find(1) + assert_equal 1, t.project_id + end + + def test_update_todo_project_to_none + t = Todo.find(1) + login_as(:admin_user) + xhr :post, :update, :id => 1, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"None", "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" + t = Todo.find(1) + assert_nil t.project_id + end + + def test_update_todo_to_deferred_is_reflected_in_badge_count + login_as(:admin_user) + get :index + 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 10, assigns['down_count'] + end + + def test_update_todo + t = Todo.find(1) + login_as(:admin_user) + xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo, bar" + t = Todo.find(1) + assert_equal "Call Warren Buffet to find out how much he makes per day", t.description + assert_equal "bar, foo", t.tag_list + expected = Date.new(2006,11,30) + actual = t.due.to_date + assert_equal expected, actual, "Expected #{expected.to_s(:db)}, was #{actual.to_s(:db)}" + end + + def test_update_todos_with_blank_project_name + t = Todo.find(1) + login_as(:admin_user) + xhr :post, :update, :id => 1, :_source_view => 'todo', :project_name => '', "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo, bar" + t.reload + assert t.project.nil? + end + + def test_update_todo_tags_to_none + t = Todo.find(1) + login_as(:admin_user) + xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"" + t = Todo.find(1) + assert_equal true, t.tag_list.empty? + end + + def test_update_todo_tags_with_whitespace_and_dots + t = Todo.find(1) + login_as(:admin_user) + taglist = " one , two,three ,four, 8.1.2, version1.5" + xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>taglist + t = Todo.find(1) + assert_equal "8.1.2, four, one, three, two, version1.5", t.tag_list + end + + def test_find_tagged_with + login_as(:admin_user) + @user = User.find(@request.session['user_id']) + tag = Tag.find_by_name('foo').todos + @tagged = tag.count + get :tag, :name => 'foo' + assert_response :success + assert_equal 3, @tagged + end + + def test_rss_feed + login_as(:admin_user) + get :index, { :format => "rss" } + assert_equal 'application/rss+xml', @response.content_type + # puts @response.body + + assert_xml_select 'rss[version="2.0"]' do + assert_select 'channel' do + assert_select '>title', 'Actions' + assert_select '>description', "Actions for #{users(:admin_user).display_name}" + assert_select 'language', 'en-us' + assert_select 'ttl', '40' + 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', todos(:book).updated_at.to_s(:rfc822) + end + end + end + end + + def test_rss_feed_with_limit + login_as(:admin_user) + get :index, { :format => "rss", :limit => '5' } + + assert_xml_select 'rss[version="2.0"]' do + assert_select 'channel' do + assert_select '>title', 'Actions' + assert_select '>description', "Actions for #{users(:admin_user).display_name}" + assert_select 'item', 5 do + assert_select 'title', /.+/ + assert_select 'description', /.*/ + end + end + end + end + + def test_rss_feed_not_accessible_to_anonymous_user_without_token + login_as nil + get :index, { :format => "rss" } + assert_response 401 + end + + def test_rss_feed_not_accessible_to_anonymous_user_with_invalid_token + login_as nil + get :index, { :format => "rss", :token => 'foo' } + assert_response 401 + end + + def test_rss_feed_accessible_to_anonymous_user_with_valid_token + login_as nil + get :index, { :format => "rss", :token => users(:admin_user).token } + assert_response :ok + end + + def test_atom_feed_content + login_as :admin_user + get :index, { :format => "atom" } + assert_equal 'application/atom+xml', @response.content_type + # #puts @response.body + + assert_xml_select 'feed[xmlns="http://www.w3.org/2005/Atom"]' do + assert_xml_select '>title', 'Actions' + assert_xml_select '>subtitle', "Actions for #{users(:admin_user).display_name}" + assert_xml_select 'entry', 11 do + assert_xml_select 'title', /.+/ + assert_xml_select 'content[type="html"]', /.*/ + assert_xml_select 'published', /(#{Regexp.escape(todos(:book).updated_at.xmlschema)}|#{Regexp.escape(projects(:moremoney).updated_at.xmlschema)})/ + end + end + end + + def test_atom_feed_not_accessible_to_anonymous_user_without_token + login_as nil + get :index, { :format => "atom" } + assert_response 401 + end + + def test_atom_feed_not_accessible_to_anonymous_user_with_invalid_token + login_as nil + get :index, { :format => "atom", :token => 'foo' } + assert_response 401 + end + + def test_atom_feed_accessible_to_anonymous_user_with_valid_token + login_as nil + get :index, { :format => "atom", :token => users(:admin_user).token } + assert_response :ok + end + + def test_text_feed_content + login_as(:admin_user) + get :index, { :format => "txt" } + assert_equal 'text/plain', @response.content_type + assert !(/ /.match(@response.body)) + # #puts @response.body + end + + def test_text_feed_not_accessible_to_anonymous_user_without_token + login_as nil + get :index, { :format => "txt" } + assert_response 401 + end + + def test_text_feed_not_accessible_to_anonymous_user_with_invalid_token + login_as nil + get :index, { :format => "txt", :token => 'foo' } + assert_response 401 + end + + def test_text_feed_accessible_to_anonymous_user_with_valid_token + login_as nil + get :index, { :format => "txt", :token => users(:admin_user).token } + assert_response :ok + end + + def test_ical_feed_content + login_as :admin_user + get :index, { :format => "ics" } + assert_equal 'text/calendar', @response.content_type + assert !(/ /.match(@response.body)) + # #puts @response.body + end + + def test_mobile_index_uses_text_html_content_type + login_as(:admin_user) + get :index, { :format => "m" } + assert_equal 'text/html', @response.content_type + end + + def test_mobile_index_assigns_down_count + login_as(:admin_user) + get :index, { :format => "m" } + 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"}} + t = Todo.find_by_description("test_mobile_create_action") + assert_not_nil t + assert_equal 2, t.context_id + assert_equal 1, t.project_id + assert t.active? + assert_equal 'test notes', t.notes + assert_nil t.show_from + 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"}} + 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"} + assert_template 'todos/new' + end + + def test_index_html_assigns_default_project_name_map + login_as(:admin_user) + get :index, {"format"=>"html"} + 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, today.day)+1.month, 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/unit/recurring_todo_test.rb b/test/unit/recurring_todo_test.rb index e7f5ef25..b223b2a1 100644 --- a/test/unit/recurring_todo_test.rb +++ b/test/unit/recurring_todo_test.rb @@ -1,283 +1,283 @@ -require File.dirname(__FILE__) + '/../test_helper' - -class RecurringTodoTest < Test::Rails::TestCase - fixtures :todos, :users, :contexts, :preferences, :tags, :taggings, :recurring_todos - - def setup - @every_day = RecurringTodo.find(1).reload - @every_workday = RecurringTodo.find(2).reload - @weekly_every_day = RecurringTodo.find(3).reload - @monthly_every_last_friday = RecurringTodo.find(4).reload - @yearly = RecurringTodo.find(5).reload - - @today = Time.now.utc - @tomorrow = @today + 1.day - @in_three_days = Time.now.utc + 3.days - @in_four_days = @in_three_days + 1.day # need a day after start_from - - @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 - assert_equal "every day", @every_day.recurrence_pattern - assert_equal "on work days", @every_workday.recurrence_pattern - assert_equal "every last Friday of every 2 months", @monthly_every_last_friday.recurrence_pattern - assert_equal "every year on June 8", @yearly.recurrence_pattern - end - - 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 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 - - # do something every 14 days - @every_day.every_other1=14 - due_date = @every_day.get_due_date(@today) - assert_equal @today+14.days, due_date - end - - def test_daily_work_days - assert_equal @monday, @every_workday.get_due_date(@friday) - assert_equal @monday, @every_workday.get_due_date(@saturday) - assert_equal @monday, @every_workday.get_due_date(@sunday) - assert_equal @tuesday, @every_workday.get_due_date(@monday) - end - - def test_show_from_date - # assume that target due_date works fine, i.e. don't do the same tests over - - @every_day.target='show_from_date' - # when recurrence is targeted on show_from, due date shoult remain nil - assert_equal nil, @every_day.get_due_date(nil) - assert_equal nil, @every_day.get_due_date(@today-3.days) - - # check show from get the next day - assert_equal @today, @every_day.get_show_from_date(@today-1.days) - assert_equal @today+1.day, @every_day.get_show_from_date(@today) - - @every_day.target='due_date' - # when target on due_date, show_from is relative to due date unless delta=0 - assert_equal nil, @every_day.get_show_from_date(@today-1.days) - - @every_day.show_from_delta=10 - assert_equal @today, @every_day.get_show_from_date(@today+9.days) #today+1+9-10 - - # TODO: show_from has no use case for daily pattern. Need to test on - # weekly/monthly/yearly - end - - def test_end_date_on_recurring_todo - assert_equal true, @every_day.has_next_todo(@in_three_days) - assert_equal true, @every_day.has_next_todo(@in_four_days) - @every_day.end_date = @in_four_days - assert_equal false, @every_day.has_next_todo(@in_four_days) - end - - def test_weekly_every_day_setters - @weekly_every_day.every_day = ' ' - - @weekly_every_day.weekly_return_sunday=('s') - assert_equal 's ', @weekly_every_day.every_day - @weekly_every_day.weekly_return_monday=('m') - assert_equal 'sm ', @weekly_every_day.every_day - @weekly_every_day.weekly_return_tuesday=('t') - assert_equal 'smt ', @weekly_every_day.every_day - @weekly_every_day.weekly_return_wednesday=('w') - assert_equal 'smtw ', @weekly_every_day.every_day - @weekly_every_day.weekly_return_thursday=('t') - assert_equal 'smtwt ', @weekly_every_day.every_day - @weekly_every_day.weekly_return_friday=('f') - assert_equal 'smtwtf ', @weekly_every_day.every_day - @weekly_every_day.weekly_return_saturday=('s') - assert_equal 'smtwtfs', @weekly_every_day.every_day - - # test remove - @weekly_every_day.weekly_return_wednesday=(' ') - assert_equal 'smt tfs', @weekly_every_day.every_day - end - - def test_weekly_pattern - assert_equal true, @weekly_every_day.has_next_todo(nil) - - due_date = @weekly_every_day.get_due_date(@sunday) - assert_equal @monday, due_date - - # saturday is last day in week, so the next date should be sunday + n_weeks - due_date = @weekly_every_day.get_due_date(@saturday) - assert_equal @sunday + 2.weeks, due_date - - # remove tuesday and wednesday - @weekly_every_day.weekly_return_tuesday=(' ') - @weekly_every_day.weekly_return_wednesday=(' ') - assert_equal 'sm tfs', @weekly_every_day.every_day - due_date = @weekly_every_day.get_due_date(@monday) - assert_equal @thursday, due_date - - @weekly_every_day.every_other1 = 1 - @weekly_every_day.every_day = ' tw ' - due_date = @weekly_every_day.get_due_date(@tuesday) - assert_equal @wednesday, due_date - due_date = @weekly_every_day.get_due_date(@wednesday) - assert_equal @tuesday+1.week, due_date - end - - def test_monthly_pattern - due_date = @monthly_every_last_friday.get_due_date(@sunday) - assert_equal Time.zone.local(2008,6,27), due_date - - 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.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.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 - @monthly.every_other1 = 8 # every 8th day of the month - @monthly.every_other2 = 2 # every 2 months - - due_date = @monthly.get_due_date(@saturday) # june 7th - assert_equal @sunday, due_date # june 8th - - due_date = @monthly.get_due_date(@sunday) # june 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.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.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.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.zone.local(2008,6,11), due_date # june 11th - # same month, after second wednesday - 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 - 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.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 - t.toggle_completion! - assert_equal :completed, t.current_state - t.toggle_completion! - assert_equal :active, t.current_state - end - - def test_starred - @yearly.tag_with("1, 2, starred") - @yearly.tags.reload - - assert_equal true, @yearly.starred? - assert_equal false, @weekly_every_day.starred? - - @yearly.toggle_star! - assert_equal false, @yearly.starred? - @yearly.toggle_star! - assert_equal true, @yearly.starred? - end - - def test_occurence_count - @every_day.number_of_occurences = 2 - assert_equal true, @every_day.has_next_todo(@in_three_days) - @every_day.inc_occurences - assert_equal true, @every_day.has_next_todo(@in_three_days) - @every_day.inc_occurences - assert_equal false, @every_day.has_next_todo(@in_three_days) - - # after completion, when you reactivate the recurring todo, the occurences - # count should be reset - assert_equal 2, @every_day.occurences_count - @every_day.toggle_completion! - @every_day.toggle_completion! - assert_equal true, @every_day.has_next_todo(@in_three_days) - assert_equal 0, @every_day.occurences_count - end - -end +require File.dirname(__FILE__) + '/../test_helper' + +class RecurringTodoTest < Test::Rails::TestCase + fixtures :todos, :users, :contexts, :preferences, :tags, :taggings, :recurring_todos + + def setup + @every_day = RecurringTodo.find(1).reload + @every_workday = RecurringTodo.find(2).reload + @weekly_every_day = RecurringTodo.find(3).reload + @monthly_every_last_friday = RecurringTodo.find(4).reload + @yearly = RecurringTodo.find(5).reload + + @today = Time.now.utc + @tomorrow = @today + 1.day + @in_three_days = Time.now.utc + 3.days + @in_four_days = @in_three_days + 1.day # need a day after start_from + + @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 + assert_equal "every day", @every_day.recurrence_pattern + assert_equal "on work days", @every_workday.recurrence_pattern + assert_equal "every last Friday of every 2 months", @monthly_every_last_friday.recurrence_pattern + assert_equal "every year on June 8", @yearly.recurrence_pattern + end + + 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 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 + + # do something every 14 days + @every_day.every_other1=14 + due_date = @every_day.get_due_date(@today) + assert_equal @today+14.days, due_date + end + + def test_daily_work_days + assert_equal @monday, @every_workday.get_due_date(@friday) + assert_equal @monday, @every_workday.get_due_date(@saturday) + assert_equal @monday, @every_workday.get_due_date(@sunday) + assert_equal @tuesday, @every_workday.get_due_date(@monday) + end + + def test_show_from_date + # assume that target due_date works fine, i.e. don't do the same tests over + + @every_day.target='show_from_date' + # when recurrence is targeted on show_from, due date shoult remain nil + assert_equal nil, @every_day.get_due_date(nil) + assert_equal nil, @every_day.get_due_date(@today-3.days) + + # check show from get the next day + assert_equal @today, @every_day.get_show_from_date(@today-1.days) + assert_equal @today+1.day, @every_day.get_show_from_date(@today) + + @every_day.target='due_date' + # when target on due_date, show_from is relative to due date unless delta=0 + assert_equal nil, @every_day.get_show_from_date(@today-1.days) + + @every_day.show_from_delta=10 + assert_equal @today, @every_day.get_show_from_date(@today+9.days) #today+1+9-10 + + # TODO: show_from has no use case for daily pattern. Need to test on + # weekly/monthly/yearly + end + + def test_end_date_on_recurring_todo + assert_equal true, @every_day.has_next_todo(@in_three_days) + assert_equal true, @every_day.has_next_todo(@in_four_days) + @every_day.end_date = @in_four_days + assert_equal false, @every_day.has_next_todo(@in_four_days) + end + + def test_weekly_every_day_setters + @weekly_every_day.every_day = ' ' + + @weekly_every_day.weekly_return_sunday=('s') + assert_equal 's ', @weekly_every_day.every_day + @weekly_every_day.weekly_return_monday=('m') + assert_equal 'sm ', @weekly_every_day.every_day + @weekly_every_day.weekly_return_tuesday=('t') + assert_equal 'smt ', @weekly_every_day.every_day + @weekly_every_day.weekly_return_wednesday=('w') + assert_equal 'smtw ', @weekly_every_day.every_day + @weekly_every_day.weekly_return_thursday=('t') + assert_equal 'smtwt ', @weekly_every_day.every_day + @weekly_every_day.weekly_return_friday=('f') + assert_equal 'smtwtf ', @weekly_every_day.every_day + @weekly_every_day.weekly_return_saturday=('s') + assert_equal 'smtwtfs', @weekly_every_day.every_day + + # test remove + @weekly_every_day.weekly_return_wednesday=(' ') + assert_equal 'smt tfs', @weekly_every_day.every_day + end + + def test_weekly_pattern + assert_equal true, @weekly_every_day.has_next_todo(nil) + + due_date = @weekly_every_day.get_due_date(@sunday) + assert_equal @monday, due_date + + # saturday is last day in week, so the next date should be sunday + n_weeks + due_date = @weekly_every_day.get_due_date(@saturday) + assert_equal @sunday + 2.weeks, due_date + + # remove tuesday and wednesday + @weekly_every_day.weekly_return_tuesday=(' ') + @weekly_every_day.weekly_return_wednesday=(' ') + assert_equal 'sm tfs', @weekly_every_day.every_day + due_date = @weekly_every_day.get_due_date(@monday) + assert_equal @thursday, due_date + + @weekly_every_day.every_other1 = 1 + @weekly_every_day.every_day = ' tw ' + due_date = @weekly_every_day.get_due_date(@tuesday) + assert_equal @wednesday, due_date + due_date = @weekly_every_day.get_due_date(@wednesday) + assert_equal @tuesday+1.week, due_date + end + + def test_monthly_pattern + due_date = @monthly_every_last_friday.get_due_date(@sunday) + assert_equal Time.zone.local(2008,6,27), due_date + + 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.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.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 + @monthly.every_other1 = 8 # every 8th day of the month + @monthly.every_other2 = 2 # every 2 months + + due_date = @monthly.get_due_date(@saturday) # june 7th + assert_equal @sunday, due_date # june 8th + + due_date = @monthly.get_due_date(@sunday) # june 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.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.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.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.zone.local(2008,6,11), due_date # june 11th + # same month, after second wednesday + 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 + 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.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 + t.toggle_completion! + assert_equal :completed, t.current_state + t.toggle_completion! + assert_equal :active, t.current_state + end + + def test_starred + @yearly.tag_with("1, 2, starred") + @yearly.tags.reload + + assert_equal true, @yearly.starred? + assert_equal false, @weekly_every_day.starred? + + @yearly.toggle_star! + assert_equal false, @yearly.starred? + @yearly.toggle_star! + assert_equal true, @yearly.starred? + end + + def test_occurence_count + @every_day.number_of_occurences = 2 + assert_equal true, @every_day.has_next_todo(@in_three_days) + @every_day.inc_occurences + assert_equal true, @every_day.has_next_todo(@in_three_days) + @every_day.inc_occurences + assert_equal false, @every_day.has_next_todo(@in_three_days) + + # after completion, when you reactivate the recurring todo, the occurences + # count should be reset + assert_equal 2, @every_day.occurences_count + @every_day.toggle_completion! + @every_day.toggle_completion! + assert_equal true, @every_day.has_next_todo(@in_three_days) + assert_equal 0, @every_day.occurences_count + end + +end diff --git a/test/unit/todo_test.rb b/test/unit/todo_test.rb index 3e0fdf4d..6b82340b 100644 --- a/test/unit/todo_test.rb +++ b/test/unit/todo_test.rb @@ -1,181 +1,181 @@ -require File.dirname(__FILE__) + '/../test_helper' -require 'date' - -class TodoTest < Test::Rails::TestCase - fixtures :todos, :users, :contexts, :preferences, :tags, :taggings - - def setup - @not_completed1 = Todo.find(1).reload - @not_completed2 = Todo.find(2).reload - @completed = Todo.find(8).reload - end - - # Test loading a todo item - def test_load - assert_kind_of Todo, @not_completed1 - assert_equal 1, @not_completed1.id - assert_equal 1, @not_completed1.context_id - assert_equal 2, @not_completed1.project_id - assert_equal "Call Bill Gates to find out how much he makes per day", @not_completed1.description - assert_nil @not_completed1.notes - assert @not_completed1.completed? == false - assert_equal 1.week.ago.beginning_of_day.strftime("%Y-%m-%d %H:%M"), @not_completed1.created_at.strftime("%Y-%m-%d %H:%M") - assert_equal 2.week.from_now.beginning_of_day.strftime("%Y-%m-%d"), @not_completed1.due.strftime("%Y-%m-%d") - assert_nil @not_completed1.completed_at - assert_equal 1, @not_completed1.user_id - end - - def test_completed - assert_kind_of Todo, @completed - assert @completed.completed? - assert_not_nil @completed.completed_at - end - - def test_completed_at_cleared_after_toggle_to_active - assert_kind_of Todo, @completed - assert @completed.completed? - @completed.toggle_completion! - assert @completed.active? - assert_nil @completed.completed_at - end - - - # Validation tests - # - def test_validate_presence_of_description - assert_equal "Call dinosaur exterminator", @not_completed2.description - @not_completed2.description = "" - assert !@not_completed2.save - assert_equal 1, @not_completed2.errors.count - assert_equal "can't be blank", @not_completed2.errors.on(:description) - end - - def test_validate_length_of_description - assert_equal "Call dinosaur exterminator", @not_completed2.description - @not_completed2.description = generate_random_string(101) - assert !@not_completed2.save - assert_equal 1, @not_completed2.errors.count - assert_equal "is too long (maximum is 100 characters)", @not_completed2.errors.on(:description) - end - - def test_validate_length_of_notes - assert_equal "Ask him if I need to hire a skip for the corpses.", @not_completed2.notes - @not_completed2.notes = generate_random_string(60001) - assert !@not_completed2.save - assert_equal 1, @not_completed2.errors.count - assert_equal "is too long (maximum is 60000 characters)", @not_completed2.errors.on(:notes) - end - - def test_validate_show_from_must_be_a_date_in_the_future - t = @not_completed2 - 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 !t.save - assert_equal 1, t.errors.count - assert_equal "must be a date in the future", t.errors.on(:show_from) - end - - def test_defer_an_existing_todo - @not_completed2 - assert_equal :active, @not_completed2.current_state - @not_completed2.show_from = next_week - assert @not_completed2.save, "should have saved successfully" + @not_completed2.errors.to_xml - assert_equal :deferred, @not_completed2.current_state - end - - def test_create_a_new_deferred_todo - user = users(:other_user) - todo = user.todos.build - todo.show_from = next_week - todo.context_id = 1 - todo.description = 'foo' - assert todo.save, "should have saved successfully" + todo.errors.to_xml - assert_equal :deferred, todo.current_state - end - - def test_create_a_new_deferred_todo_by_passing_attributes - user = users(:other_user) - todo = user.todos.build(:show_from => next_week, :context_id => 1, :description => 'foo') - assert todo.save, "should have saved successfully" + todo.errors.to_xml - assert_equal :deferred, todo.current_state - end - - def test_feed_options - opts = Todo.feed_options(users(:admin_user)) - assert_equal 'Tracks Actions', opts[:title], 'Unexpected value for :title key of feed_options' - assert_equal 'Actions for Admin Schmadmin', opts[:description], 'Unexpected value for :description key of feed_options' - end - - def test_toggle_completion - t = @not_completed1 - assert_equal :active, t.current_state - t.toggle_completion! - assert_equal :completed, t.current_state - t.toggle_completion! - assert_equal :active, t.current_state - end - - def test_activate_also_saves - t = @not_completed1 - t.show_from = 1.week.from_now - t.save! - assert t.deferred? - t.reload - t.activate! - assert t.active? - t.reload - assert t.active? - end - - def test_project_returns_null_object_when_nil - t = @not_completed1 - assert !t.project.is_a?(NullProject) - t.project = nil - assert t.project.is_a?(NullProject) - end - - def test_initial_state_defaults_to_active - t = Todo.new - t.description = 'foo' - t.context_id = 1 - t.save! - t.reload - assert_equal :active, t.current_state - end - - def test_initial_state_is_deferred_when_show_from_in_future - t = Todo.new - t.user = users(:admin_user) - t.description = 'foo' - t.context_id = 1 - t.show_from = 1.week.from_now.to_date - t.save! - t.reload - assert_equal :deferred, t.current_state - end - - def test_todo_is_not_starred - assert !@not_completed1.starred? - end - - def test_todo_2_is_not_starred - assert !Todo.find(2).starred? - end - - def test_todo_is_starred_after_starred_tag_is_added - @not_completed1._add_tags('starred') - assert @not_completed1.starred? - end - - def test_todo_is_starred_after_toggle_starred - @not_completed1.toggle_star! - assert @not_completed1.starred? - end - - def test_todo_is_not_starred_after_toggle_starred_twice - @not_completed1.toggle_star! - @not_completed1.toggle_star! - assert !@not_completed1.starred? - end - -end +require File.dirname(__FILE__) + '/../test_helper' +require 'date' + +class TodoTest < Test::Rails::TestCase + fixtures :todos, :users, :contexts, :preferences, :tags, :taggings + + def setup + @not_completed1 = Todo.find(1).reload + @not_completed2 = Todo.find(2).reload + @completed = Todo.find(8).reload + end + + # Test loading a todo item + def test_load + assert_kind_of Todo, @not_completed1 + assert_equal 1, @not_completed1.id + assert_equal 1, @not_completed1.context_id + assert_equal 2, @not_completed1.project_id + assert_equal "Call Bill Gates to find out how much he makes per day", @not_completed1.description + assert_nil @not_completed1.notes + assert @not_completed1.completed? == false + assert_equal 1.week.ago.beginning_of_day.strftime("%Y-%m-%d %H:%M"), @not_completed1.created_at.strftime("%Y-%m-%d %H:%M") + assert_equal 2.week.from_now.beginning_of_day.strftime("%Y-%m-%d"), @not_completed1.due.strftime("%Y-%m-%d") + assert_nil @not_completed1.completed_at + assert_equal 1, @not_completed1.user_id + end + + def test_completed + assert_kind_of Todo, @completed + assert @completed.completed? + assert_not_nil @completed.completed_at + end + + def test_completed_at_cleared_after_toggle_to_active + assert_kind_of Todo, @completed + assert @completed.completed? + @completed.toggle_completion! + assert @completed.active? + assert_nil @completed.completed_at + end + + + # Validation tests + # + def test_validate_presence_of_description + assert_equal "Call dinosaur exterminator", @not_completed2.description + @not_completed2.description = "" + assert !@not_completed2.save + assert_equal 1, @not_completed2.errors.count + assert_equal "can't be blank", @not_completed2.errors.on(:description) + end + + def test_validate_length_of_description + assert_equal "Call dinosaur exterminator", @not_completed2.description + @not_completed2.description = generate_random_string(101) + assert !@not_completed2.save + assert_equal 1, @not_completed2.errors.count + assert_equal "is too long (maximum is 100 characters)", @not_completed2.errors.on(:description) + end + + def test_validate_length_of_notes + assert_equal "Ask him if I need to hire a skip for the corpses.", @not_completed2.notes + @not_completed2.notes = generate_random_string(60001) + assert !@not_completed2.save + assert_equal 1, @not_completed2.errors.count + assert_equal "is too long (maximum is 60000 characters)", @not_completed2.errors.on(:notes) + end + + def test_validate_show_from_must_be_a_date_in_the_future + t = @not_completed2 + 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 !t.save + assert_equal 1, t.errors.count + assert_equal "must be a date in the future", t.errors.on(:show_from) + end + + def test_defer_an_existing_todo + @not_completed2 + assert_equal :active, @not_completed2.current_state + @not_completed2.show_from = next_week + assert @not_completed2.save, "should have saved successfully" + @not_completed2.errors.to_xml + assert_equal :deferred, @not_completed2.current_state + end + + def test_create_a_new_deferred_todo + user = users(:other_user) + todo = user.todos.build + todo.show_from = next_week + todo.context_id = 1 + todo.description = 'foo' + assert todo.save, "should have saved successfully" + todo.errors.to_xml + assert_equal :deferred, todo.current_state + end + + def test_create_a_new_deferred_todo_by_passing_attributes + user = users(:other_user) + todo = user.todos.build(:show_from => next_week, :context_id => 1, :description => 'foo') + assert todo.save, "should have saved successfully" + todo.errors.to_xml + assert_equal :deferred, todo.current_state + end + + def test_feed_options + opts = Todo.feed_options(users(:admin_user)) + assert_equal 'Tracks Actions', opts[:title], 'Unexpected value for :title key of feed_options' + assert_equal 'Actions for Admin Schmadmin', opts[:description], 'Unexpected value for :description key of feed_options' + end + + def test_toggle_completion + t = @not_completed1 + assert_equal :active, t.current_state + t.toggle_completion! + assert_equal :completed, t.current_state + t.toggle_completion! + assert_equal :active, t.current_state + end + + def test_activate_also_saves + t = @not_completed1 + t.show_from = 1.week.from_now + t.save! + assert t.deferred? + t.reload + t.activate! + assert t.active? + t.reload + assert t.active? + end + + def test_project_returns_null_object_when_nil + t = @not_completed1 + assert !t.project.is_a?(NullProject) + t.project = nil + assert t.project.is_a?(NullProject) + end + + def test_initial_state_defaults_to_active + t = Todo.new + t.description = 'foo' + t.context_id = 1 + t.save! + t.reload + assert_equal :active, t.current_state + end + + def test_initial_state_is_deferred_when_show_from_in_future + t = Todo.new + t.user = users(:admin_user) + t.description = 'foo' + t.context_id = 1 + t.show_from = 1.week.from_now.to_date + t.save! + t.reload + assert_equal :deferred, t.current_state + end + + def test_todo_is_not_starred + assert !@not_completed1.starred? + end + + def test_todo_2_is_not_starred + assert !Todo.find(2).starred? + end + + def test_todo_is_starred_after_starred_tag_is_added + @not_completed1._add_tags('starred') + assert @not_completed1.starred? + end + + def test_todo_is_starred_after_toggle_starred + @not_completed1.toggle_star! + assert @not_completed1.starred? + end + + def test_todo_is_not_starred_after_toggle_starred_twice + @not_completed1.toggle_star! + @not_completed1.toggle_star! + assert !@not_completed1.starred? + end + +end