diff --git a/app/controllers/application.rb b/app/controllers/application.rb index 0d54b75f..8a329f56 100644 --- a/app/controllers/application.rb +++ b/app/controllers/application.rb @@ -1,268 +1,268 @@ -# The filters added to this controller will be run for all controllers in the -# application. Likewise will all the methods added be available for all -# controllers. - -require_dependency "login_system" -require_dependency "tracks/source_view" -require "redcloth" - -require 'date' -require 'time' - -# Commented the following line because of #744. It prevented rake db:migrate to -# run because this tag went looking for the taggings table that did not exist -# when you feshly create a new database Old comment: We need this in development -# mode, or you get 'method missing' errors -# -# Tag - -class CannotAccessContext < RuntimeError; end - -class ApplicationController < ActionController::Base - - protect_from_forgery :secret => SALT - - helper :application - include LoginSystem - helper_method :current_user, :prefs - - layout proc{ |controller| controller.mobile? ? "mobile" : "standard" } - - before_filter :set_session_expiration - before_filter :set_time_zone - prepend_before_filter :login_required - prepend_before_filter :enable_mobile_content_negotiation - after_filter :set_charset - - - - include ActionView::Helpers::TextHelper - include ActionView::Helpers::SanitizeHelper - extend ActionView::Helpers::SanitizeHelper::ClassMethods - helper_method :format_date, :markdown - - # By default, sets the charset to UTF-8 if it isn't already set - def set_charset - headers["Content-Type"] ||= "text/html; charset=UTF-8" - end - - def set_session_expiration - # http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions - unless session == nil - return if @controller_name == 'feed' or session['noexpiry'] == "on" - # If the method is called by the feed controller (which we don't have - # under session control) or if we checked the box to keep logged in on - # login don't set the session expiry time. - if session - # Get expiry time (allow ten seconds window for the case where we have - # none) - expiry_time = session['expiry_time'] || Time.now + 10 - if expiry_time < Time.now - # Too late, matey... bang goes your session! - reset_session - else - # Okay, you get another hour - session['expiry_time'] = Time.now + (60*60) - end - end - end - end - - def render_failure message, status = 404 - render :text => message, :status => status - end - - # def rescue_action(exception) - # log_error(exception) if logger - # respond_to do |format| - # format.html do - # notify :warning, "An error occurred on the server." - # render :action => "index" - # end - # format.js { render :action => 'error' } - # format.xml { render :text => 'An error occurred on the server.' + $! } - # end - # end - - # Returns a count of next actions in the given context or project The result - # is count and a string descriptor, correctly pluralised if there are no - # actions or multiple actions - # - def count_undone_todos_phrase(todos_parent, string="actions") - count = count_undone_todos(todos_parent) - if count == 1 - word = string.singularize - else - word = string.pluralize - end - return count.to_s + " " + word - end - - def count_undone_todos(todos_parent) - if todos_parent.nil? - count = 0 - elsif (todos_parent.is_a?(Project) && todos_parent.hidden?) - count = eval "@project_project_hidden_todo_counts[#{todos_parent.id}]" - else - count = eval "@#{todos_parent.class.to_s.downcase}_not_done_counts[#{todos_parent.id}]" - end - count || 0 - end - - # Convert a date object to the format specified in the user's preferences in - # config/settings.yml - # - def format_date(date) - if date - date_format = prefs.date_format - formatted_date = date.in_time_zone(prefs.time_zone).strftime("#{date_format}") - else - formatted_date = '' - end - formatted_date - end - - # Uses RedCloth to transform text using either Textile or Markdown Need to - # require redcloth above RedCloth 3.0 or greater is needed to use Markdown, - # otherwise it only handles Textile - # - def markdown(text) - RedCloth.new(text).to_html - end - - def build_default_project_context_name_map(projects) - Hash[*projects.reject{ |p| p.default_context.nil? }.map{ |p| [p.name, p.default_context.name] }.flatten].to_json - end - - # Here's the concept behind this "mobile content negotiation" hack: In - # addition to the main, AJAXy Web UI, Tracks has a lightweight low-feature - # 'mobile' version designed to be suitablef or use from a phone or PDA. It - # makes some sense that tne pages of that mobile version are simply alternate - # representations of the same Todo resources. The implementation goal was to - # treat mobile as another format and be able to use respond_to to render both - # versions. Unfortunately, I ran into a lot of trouble simply registering a - # new mime type 'text/html' with format :m because :html already is linked to - # that mime type and the new registration was forcing all html requests to be - # rendered in the mobile view. The before_filter and after_filter hackery - # below accomplishs that implementation goal by using a 'fake' mime type - # during the processing and then setting it to 'text/html' in an - # 'after_filter' -LKM 2007-04-01 - def mobile? - return params[:format] == 'm' - end - - def enable_mobile_content_negotiation - if mobile? - request.format = :m - end - end - - def create_todo_from_recurring_todo(rt, date=nil) - # create todo and initialize with data from recurring_todo rt - todo = current_user.todos.build( { :description => rt.description, :notes => rt.notes, :project_id => rt.project_id, :context_id => rt.context_id}) - - # set dates - todo.recurring_todo_id = rt.id - todo.due = rt.get_due_date(date) - - show_from_date = rt.get_show_from_date(date) - if show_from_date.nil? - todo.show_from=nil - else - # make sure that show_from is not in the past - todo.show_from = show_from_date < Time.zone.now ? nil : show_from_date - end - - saved = todo.save - if saved - todo.tag_with(rt.tag_list, current_user) - todo.tags.reload - end - - # increate number of occurences created from recurring todo - rt.inc_occurences - - # mark recurring todo complete if there are no next actions left - checkdate = todo.due.nil? ? todo.show_from : todo.due - rt.toggle_completion! unless rt.has_next_todo(checkdate) - - return saved ? todo : nil - end - - protected - - def admin_login_required - unless User.find_by_id_and_is_admin(session['user_id'], true) - render :text => "401 Unauthorized: Only admin users are allowed access to this function.", :status => 401 - return false - end - end - - def redirect_back_or_home - respond_to do |format| - format.html { redirect_back_or_default home_url } - format.m { redirect_back_or_default mobile_url } - end - end - - def boolean_param(param_name) - return false if param_name.blank? - s = params[param_name] - return false if s.blank? || s == false || s =~ /^false$/i - return true if s == true || s =~ /^true$/i - raise ArgumentError.new("invalid value for Boolean: \"#{s}\"") - end - - def self.openid_enabled? - Tracks::Config.openid_enabled? - end - - def openid_enabled? - self.class.openid_enabled? - end - - private - - def parse_date_per_user_prefs( s ) - prefs.parse_date(s) - end - - def init_data_for_sidebar - @completed_projects = current_user.projects.completed - @hidden_projects = current_user.projects.hidden - @active_projects = current_user.projects.active - - @active_contexts = current_user.contexts.active - @hidden_contexts = current_user.contexts.hidden - - init_not_done_counts - if prefs.show_hidden_projects_in_sidebar - init_project_hidden_todo_counts(['project']) - end - end - - def init_not_done_counts(parents = ['project','context']) - parents.each do |parent| - eval("@#{parent}_not_done_counts = @#{parent}_not_done_counts || current_user.todos.active.count(:group => :#{parent}_id)") - end - end - - def init_project_hidden_todo_counts(parents = ['project','context']) - parents.each do |parent| - eval("@#{parent}_project_hidden_todo_counts = @#{parent}_project_hidden_todo_counts || current_user.todos.count(:conditions => ['state = ? or state = ?', 'project_hidden', 'active'], :group => :#{parent}_id)") - end - end - - # Set the contents of the flash message from a controller Usage: notify - # :warning, "This is the message" Sets the flash of type 'warning' to "This is - # the message" - def notify(type, message) - flash[type] = message - logger.error("ERROR: #{message}") if type == :error - end - - def set_time_zone - Time.zone = current_user.prefs.time_zone if logged_in? - end - -end +# The filters added to this controller will be run for all controllers in the +# application. Likewise will all the methods added be available for all +# controllers. + +require_dependency "login_system" +require_dependency "tracks/source_view" +require "redcloth" + +require 'date' +require 'time' + +# Commented the following line because of #744. It prevented rake db:migrate to +# run because this tag went looking for the taggings table that did not exist +# when you feshly create a new database Old comment: We need this in development +# mode, or you get 'method missing' errors +# +# Tag + +class CannotAccessContext < RuntimeError; end + +class ApplicationController < ActionController::Base + + protect_from_forgery :secret => SALT + + helper :application + include LoginSystem + helper_method :current_user, :prefs + + layout proc{ |controller| controller.mobile? ? "mobile" : "standard" } + + before_filter :set_session_expiration + before_filter :set_time_zone + prepend_before_filter :login_required + prepend_before_filter :enable_mobile_content_negotiation + after_filter :set_charset + + + + include ActionView::Helpers::TextHelper + include ActionView::Helpers::SanitizeHelper + extend ActionView::Helpers::SanitizeHelper::ClassMethods + helper_method :format_date, :markdown + + # By default, sets the charset to UTF-8 if it isn't already set + def set_charset + headers["Content-Type"] ||= "text/html; charset=UTF-8" + end + + def set_session_expiration + # http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions + unless session == nil + return if @controller_name == 'feed' or session['noexpiry'] == "on" + # If the method is called by the feed controller (which we don't have + # under session control) or if we checked the box to keep logged in on + # login don't set the session expiry time. + if session + # Get expiry time (allow ten seconds window for the case where we have + # none) + expiry_time = session['expiry_time'] || Time.now + 10 + if expiry_time < Time.now + # Too late, matey... bang goes your session! + reset_session + else + # Okay, you get another hour + session['expiry_time'] = Time.now + (60*60) + end + end + end + end + + def render_failure message, status = 404 + render :text => message, :status => status + end + + # def rescue_action(exception) + # log_error(exception) if logger + # respond_to do |format| + # format.html do + # notify :warning, "An error occurred on the server." + # render :action => "index" + # end + # format.js { render :action => 'error' } + # format.xml { render :text => 'An error occurred on the server.' + $! } + # end + # end + + # Returns a count of next actions in the given context or project The result + # is count and a string descriptor, correctly pluralised if there are no + # actions or multiple actions + # + def count_undone_todos_phrase(todos_parent, string="actions") + count = count_undone_todos(todos_parent) + if count == 1 + word = string.singularize + else + word = string.pluralize + end + return count.to_s + " " + word + end + + def count_undone_todos(todos_parent) + if todos_parent.nil? + count = 0 + elsif (todos_parent.is_a?(Project) && todos_parent.hidden?) + count = eval "@project_project_hidden_todo_counts[#{todos_parent.id}]" + else + count = eval "@#{todos_parent.class.to_s.downcase}_not_done_counts[#{todos_parent.id}]" + end + count || 0 + end + + # Convert a date object to the format specified in the user's preferences in + # config/settings.yml + # + def format_date(date) + if date + date_format = prefs.date_format + formatted_date = date.in_time_zone(prefs.time_zone).strftime("#{date_format}") + else + formatted_date = '' + end + formatted_date + end + + # Uses RedCloth to transform text using either Textile or Markdown Need to + # require redcloth above RedCloth 3.0 or greater is needed to use Markdown, + # otherwise it only handles Textile + # + def markdown(text) + RedCloth.new(text).to_html + end + + def build_default_project_context_name_map(projects) + Hash[*projects.reject{ |p| p.default_context.nil? }.map{ |p| [p.name, p.default_context.name] }.flatten].to_json + end + + # Here's the concept behind this "mobile content negotiation" hack: In + # addition to the main, AJAXy Web UI, Tracks has a lightweight low-feature + # 'mobile' version designed to be suitablef or use from a phone or PDA. It + # makes some sense that tne pages of that mobile version are simply alternate + # representations of the same Todo resources. The implementation goal was to + # treat mobile as another format and be able to use respond_to to render both + # versions. Unfortunately, I ran into a lot of trouble simply registering a + # new mime type 'text/html' with format :m because :html already is linked to + # that mime type and the new registration was forcing all html requests to be + # rendered in the mobile view. The before_filter and after_filter hackery + # below accomplishs that implementation goal by using a 'fake' mime type + # during the processing and then setting it to 'text/html' in an + # 'after_filter' -LKM 2007-04-01 + def mobile? + return params[:format] == 'm' + end + + def enable_mobile_content_negotiation + if mobile? + request.format = :m + end + end + + def create_todo_from_recurring_todo(rt, date=nil) + # create todo and initialize with data from recurring_todo rt + todo = current_user.todos.build( { :description => rt.description, :notes => rt.notes, :project_id => rt.project_id, :context_id => rt.context_id}) + + # set dates + todo.recurring_todo_id = rt.id + todo.due = rt.get_due_date(date) + + show_from_date = rt.get_show_from_date(date) + if show_from_date.nil? + todo.show_from=nil + else + # make sure that show_from is not in the past + todo.show_from = show_from_date < Time.zone.now ? nil : show_from_date + end + + saved = todo.save + if saved + todo.tag_with(rt.tag_list) + todo.tags.reload + end + + # increate number of occurences created from recurring todo + rt.inc_occurences + + # mark recurring todo complete if there are no next actions left + checkdate = todo.due.nil? ? todo.show_from : todo.due + rt.toggle_completion! unless rt.has_next_todo(checkdate) + + return saved ? todo : nil + end + + protected + + def admin_login_required + unless User.find_by_id_and_is_admin(session['user_id'], true) + render :text => "401 Unauthorized: Only admin users are allowed access to this function.", :status => 401 + return false + end + end + + def redirect_back_or_home + respond_to do |format| + format.html { redirect_back_or_default home_url } + format.m { redirect_back_or_default mobile_url } + end + end + + def boolean_param(param_name) + return false if param_name.blank? + s = params[param_name] + return false if s.blank? || s == false || s =~ /^false$/i + return true if s == true || s =~ /^true$/i + raise ArgumentError.new("invalid value for Boolean: \"#{s}\"") + end + + def self.openid_enabled? + Tracks::Config.openid_enabled? + end + + def openid_enabled? + self.class.openid_enabled? + end + + private + + def parse_date_per_user_prefs( s ) + prefs.parse_date(s) + end + + def init_data_for_sidebar + @completed_projects = current_user.projects.completed + @hidden_projects = current_user.projects.hidden + @active_projects = current_user.projects.active + + @active_contexts = current_user.contexts.active + @hidden_contexts = current_user.contexts.hidden + + init_not_done_counts + if prefs.show_hidden_projects_in_sidebar + init_project_hidden_todo_counts(['project']) + end + end + + def init_not_done_counts(parents = ['project','context']) + parents.each do |parent| + eval("@#{parent}_not_done_counts = @#{parent}_not_done_counts || current_user.todos.active.count(:group => :#{parent}_id)") + end + end + + def init_project_hidden_todo_counts(parents = ['project','context']) + parents.each do |parent| + eval("@#{parent}_project_hidden_todo_counts = @#{parent}_project_hidden_todo_counts || current_user.todos.count(:conditions => ['state = ? or state = ?', 'project_hidden', 'active'], :group => :#{parent}_id)") + end + end + + # Set the contents of the flash message from a controller Usage: notify + # :warning, "This is the message" Sets the flash of type 'warning' to "This is + # the message" + def notify(type, message) + flash[type] = message + logger.error("ERROR: #{message}") if type == :error + end + + def set_time_zone + Time.zone = current_user.prefs.time_zone if logged_in? + end + +end diff --git a/app/controllers/recurring_todos_controller.rb b/app/controllers/recurring_todos_controller.rb index 668e6acb..aaed6a33 100644 --- a/app/controllers/recurring_todos_controller.rb +++ b/app/controllers/recurring_todos_controller.rb @@ -1,268 +1,268 @@ -class RecurringTodosController < ApplicationController - - helper :todos, :recurring_todos - - append_before_filter :init, :only => [:index, :new, :edit] - append_before_filter :get_recurring_todo_from_param, :only => [:destroy, :toggle_check, :toggle_star, :edit, :update] - - def index - find_and_inactivate - - @recurring_todos = current_user.recurring_todos.active - @completed_recurring_todos = current_user.recurring_todos.completed - @no_recurring_todos = @recurring_todos.size == 0 - @no_completed_recurring_todos = @completed_recurring_todos.size == 0 - @count = @recurring_todos.size - - @page_title = "TRACKS::Recurring Actions" - end - - def new - end - - def show - end - - def edit - respond_to do |format| - format.js - end - end - - def update - @recurring_todo.tag_with(params[:tag_list], current_user) if params[:tag_list] - @original_item_context_id = @recurring_todo.context_id - @original_item_project_id = @recurring_todo.project_id - - # we needed to rename the recurring_period selector in the edit form because - # the form for a new recurring todo and the edit form are on the same page. - # Same goes for start_from and end_date - params['recurring_todo']['recurring_period']=params['recurring_edit_todo']['recurring_period'] - params['recurring_todo']['end_date']=parse_date_per_user_prefs(params['recurring_todo_edit_end_date']) - params['recurring_todo']['start_from']=parse_date_per_user_prefs(params['recurring_todo_edit_start_from']) - - # update project - if params['recurring_todo']['project_id'].blank? && !params['project_name'].nil? - 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["recurring_todo"]["project_id"] = project.id - end - - # update context - if params['recurring_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 - end - params["recurring_todo"]["context_id"] = context.id - end - - params["recurring_todo"]["weekly_return_monday"]=' ' if params["recurring_todo"]["weekly_return_monday"].nil? - params["recurring_todo"]["weekly_return_tuesday"]=' ' if params["recurring_todo"]["weekly_return_tuesday"].nil? - params["recurring_todo"]["weekly_return_wednesday"]=' ' if params["recurring_todo"]["weekly_return_wednesday"].nil? - params["recurring_todo"]["weekly_return_thursday"]=' ' if params["recurring_todo"]["weekly_return_thursday"].nil? - params["recurring_todo"]["weekly_return_friday"]=' ' if params["recurring_todo"]["weekly_return_friday"].nil? - params["recurring_todo"]["weekly_return_saturday"]=' ' if params["recurring_todo"]["weekly_return_saturday"].nil? - params["recurring_todo"]["weekly_return_sunday"]=' ' if params["recurring_todo"]["weekly_return_sunday"].nil? - - @saved = @recurring_todo.update_attributes params["recurring_todo"] - - respond_to do |format| - format.js - end - end - - def create - p = RecurringTodoCreateParamsHelper.new(params) - p.attributes['end_date']=parse_date_per_user_prefs(p.attributes['end_date']) - p.attributes['start_from']=parse_date_per_user_prefs(p.attributes['start_from']) - - @recurring_todo = current_user.recurring_todos.build(p.selector_attributes) - @recurring_todo.update_attributes(p.attributes) - - 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? - @recurring_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? - @recurring_todo.context_id = context.id - end - - @recurring_saved = @recurring_todo.save - unless (@recurring_saved == false) || p.tag_list.blank? - @recurring_todo.tag_with(p.tag_list, current_user) - @recurring_todo.tags.reload - end - - if @recurring_saved - @message = "The recurring todo was saved" - @todo_saved = create_todo_from_recurring_todo(@recurring_todo).nil? == false - if @todo_saved - @message += " / created a new todo" - else - @message += " / did not create todo" - end - @count = current_user.recurring_todos.active.count - else - @message = "Error saving recurring todo" - end - - respond_to do |format| - format.js - end - end - - def destroy - - # remove all references to this recurring todo - @todos = @recurring_todo.todos - @number_of_todos = @todos.size - @todos.each do |t| - t.recurring_todo_id = nil - t.save - end - - # delete the recurring todo - @saved = @recurring_todo.destroy - @remaining = current_user.recurring_todos.count - - respond_to do |format| - - format.html do - if @saved - notify :notice, "Successfully deleted recurring action", 2.0 - redirect_to :action => 'index' - else - notify :error, "Failed to delete the recurring action", 2.0 - redirect_to :action => 'index' - end - end - - format.js do - render - end - end - end - - def toggle_check - @saved = @recurring_todo.toggle_completion! - - @count = current_user.recurring_todos.active.count - @remaining = @count - - if @recurring_todo.active? - @remaining = current_user.recurring_todos.completed.count - - # from completed back to active -> check if there is an active todo - # current_user.todos.count(:all, {:conditions => ["state = ? AND recurring_todo_id = ?", 'active',params[:id]]}) - @active_todos = @recurring_todo.todos.active.count - # create todo if there is no active todo belonging to the activated - # recurring_todo - @new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo) if @active_todos == 0 - end - - respond_to do |format| - format.js - end - end - - def toggle_star - @recurring_todo.toggle_star! - @saved = @recurring_todo.save! - respond_to do |format| - format.js - end - end - - class RecurringTodoCreateParamsHelper - - def initialize(params) - @params = params['request'] || params - @attributes = params['request'] && params['request']['recurring_todo'] || params['recurring_todo'] - - # make sure all selectors (recurring_period, recurrence_selector, - # daily_selector, monthly_selector and yearly_selector) are first in hash - # so that they are processed first by the model - @selector_attributes = { - 'recurring_period' => @attributes['recurring_period'], - 'daily_selector' => @attributes['daily_selector'], - 'monthly_selector' => @attributes['monthly_selector'], - 'yearly_selector' => @attributes['yearly_selector'] - } - end - - def attributes - @attributes - end - - def selector_attributes - return @selector_attributes - 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 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 - - private - - def init - @days_of_week = [ ['Sunday',0], ['Monday',1], ['Tuesday', 2], ['Wednesday',3], ['Thursday',4], ['Friday',5], ['Saturday',6]] - @months_of_year = [ - ['January',1], ['Februari',2], ['March', 3], ['April',4], ['May',5], ['June',6], - ['July',7], ['August',8], ['September',9], ['October', 10], ['November', 11], ['December',12]] - @xth_day = [['first',1],['second',2],['third',3],['fourth',4],['last',5]] - @projects = current_user.projects.find(:all, :include => [:default_context]) - @contexts = current_user.contexts.find(:all) - @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json - end - - def get_recurring_todo_from_param - @recurring_todo = current_user.recurring_todos.find(params[:id]) - end - - def find_and_inactivate - # find active recurring todos without active todos and inactivate them - recurring_todos = current_user.recurring_todos.active - recurring_todos.each { |rt| rt.toggle_completion! if rt.todos.not_completed.count == 0} - end - -end +class RecurringTodosController < ApplicationController + + helper :todos, :recurring_todos + + append_before_filter :init, :only => [:index, :new, :edit] + append_before_filter :get_recurring_todo_from_param, :only => [:destroy, :toggle_check, :toggle_star, :edit, :update] + + def index + find_and_inactivate + + @recurring_todos = current_user.recurring_todos.active + @completed_recurring_todos = current_user.recurring_todos.completed + @no_recurring_todos = @recurring_todos.size == 0 + @no_completed_recurring_todos = @completed_recurring_todos.size == 0 + @count = @recurring_todos.size + + @page_title = "TRACKS::Recurring Actions" + end + + def new + end + + def show + end + + def edit + respond_to do |format| + format.js + end + end + + def update + @recurring_todo.tag_with(params[:tag_list]) if params[:tag_list] + @original_item_context_id = @recurring_todo.context_id + @original_item_project_id = @recurring_todo.project_id + + # we needed to rename the recurring_period selector in the edit form because + # the form for a new recurring todo and the edit form are on the same page. + # Same goes for start_from and end_date + params['recurring_todo']['recurring_period']=params['recurring_edit_todo']['recurring_period'] + params['recurring_todo']['end_date']=parse_date_per_user_prefs(params['recurring_todo_edit_end_date']) + params['recurring_todo']['start_from']=parse_date_per_user_prefs(params['recurring_todo_edit_start_from']) + + # update project + if params['recurring_todo']['project_id'].blank? && !params['project_name'].nil? + 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["recurring_todo"]["project_id"] = project.id + end + + # update context + if params['recurring_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 + end + params["recurring_todo"]["context_id"] = context.id + end + + params["recurring_todo"]["weekly_return_monday"]=' ' if params["recurring_todo"]["weekly_return_monday"].nil? + params["recurring_todo"]["weekly_return_tuesday"]=' ' if params["recurring_todo"]["weekly_return_tuesday"].nil? + params["recurring_todo"]["weekly_return_wednesday"]=' ' if params["recurring_todo"]["weekly_return_wednesday"].nil? + params["recurring_todo"]["weekly_return_thursday"]=' ' if params["recurring_todo"]["weekly_return_thursday"].nil? + params["recurring_todo"]["weekly_return_friday"]=' ' if params["recurring_todo"]["weekly_return_friday"].nil? + params["recurring_todo"]["weekly_return_saturday"]=' ' if params["recurring_todo"]["weekly_return_saturday"].nil? + params["recurring_todo"]["weekly_return_sunday"]=' ' if params["recurring_todo"]["weekly_return_sunday"].nil? + + @saved = @recurring_todo.update_attributes params["recurring_todo"] + + respond_to do |format| + format.js + end + end + + def create + p = RecurringTodoCreateParamsHelper.new(params) + p.attributes['end_date']=parse_date_per_user_prefs(p.attributes['end_date']) + p.attributes['start_from']=parse_date_per_user_prefs(p.attributes['start_from']) + + @recurring_todo = current_user.recurring_todos.build(p.selector_attributes) + @recurring_todo.update_attributes(p.attributes) + + 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? + @recurring_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? + @recurring_todo.context_id = context.id + end + + @recurring_saved = @recurring_todo.save + unless (@recurring_saved == false) || p.tag_list.blank? + @recurring_todo.tag_with(p.tag_list) + @recurring_todo.tags.reload + end + + if @recurring_saved + @message = "The recurring todo was saved" + @todo_saved = create_todo_from_recurring_todo(@recurring_todo).nil? == false + if @todo_saved + @message += " / created a new todo" + else + @message += " / did not create todo" + end + @count = current_user.recurring_todos.active.count + else + @message = "Error saving recurring todo" + end + + respond_to do |format| + format.js + end + end + + def destroy + + # remove all references to this recurring todo + @todos = @recurring_todo.todos + @number_of_todos = @todos.size + @todos.each do |t| + t.recurring_todo_id = nil + t.save + end + + # delete the recurring todo + @saved = @recurring_todo.destroy + @remaining = current_user.recurring_todos.count + + respond_to do |format| + + format.html do + if @saved + notify :notice, "Successfully deleted recurring action", 2.0 + redirect_to :action => 'index' + else + notify :error, "Failed to delete the recurring action", 2.0 + redirect_to :action => 'index' + end + end + + format.js do + render + end + end + end + + def toggle_check + @saved = @recurring_todo.toggle_completion! + + @count = current_user.recurring_todos.active.count + @remaining = @count + + if @recurring_todo.active? + @remaining = current_user.recurring_todos.completed.count + + # from completed back to active -> check if there is an active todo + # current_user.todos.count(:all, {:conditions => ["state = ? AND recurring_todo_id = ?", 'active',params[:id]]}) + @active_todos = @recurring_todo.todos.active.count + # create todo if there is no active todo belonging to the activated + # recurring_todo + @new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo) if @active_todos == 0 + end + + respond_to do |format| + format.js + end + end + + def toggle_star + @recurring_todo.toggle_star! + @saved = @recurring_todo.save! + respond_to do |format| + format.js + end + end + + class RecurringTodoCreateParamsHelper + + def initialize(params) + @params = params['request'] || params + @attributes = params['request'] && params['request']['recurring_todo'] || params['recurring_todo'] + + # make sure all selectors (recurring_period, recurrence_selector, + # daily_selector, monthly_selector and yearly_selector) are first in hash + # so that they are processed first by the model + @selector_attributes = { + 'recurring_period' => @attributes['recurring_period'], + 'daily_selector' => @attributes['daily_selector'], + 'monthly_selector' => @attributes['monthly_selector'], + 'yearly_selector' => @attributes['yearly_selector'] + } + end + + def attributes + @attributes + end + + def selector_attributes + return @selector_attributes + 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 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 + + private + + def init + @days_of_week = [ ['Sunday',0], ['Monday',1], ['Tuesday', 2], ['Wednesday',3], ['Thursday',4], ['Friday',5], ['Saturday',6]] + @months_of_year = [ + ['January',1], ['Februari',2], ['March', 3], ['April',4], ['May',5], ['June',6], + ['July',7], ['August',8], ['September',9], ['October', 10], ['November', 11], ['December',12]] + @xth_day = [['first',1],['second',2],['third',3],['fourth',4],['last',5]] + @projects = current_user.projects.find(:all, :include => [:default_context]) + @contexts = current_user.contexts.find(:all) + @default_project_context_name_map = build_default_project_context_name_map(@projects).to_json + end + + def get_recurring_todo_from_param + @recurring_todo = current_user.recurring_todos.find(params[:id]) + end + + def find_and_inactivate + # find active recurring todos without active todos and inactivate them + recurring_todos = current_user.recurring_todos.active + recurring_todos.each { |rt| rt.toggle_completion! if rt.todos.not_completed.count == 0} + end + +end diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 8ccf710a..ff5b767f 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -1,932 +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, current_user) - @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], current_user) 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 => ['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 => ['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 diff --git a/app/models/recurring_todo.rb b/app/models/recurring_todo.rb index 142a1b3c..93726a86 100644 --- a/app/models/recurring_todo.rb +++ b/app/models/recurring_todo.rb @@ -623,10 +623,10 @@ class RecurringTodo < ActiveRecord::Base def toggle_star! if starred? - delete_tags Todo::STARRED_TAG_NAME + _remove_tags Todo::STARRED_TAG_NAME tags.reload else - add_tag Todo::STARRED_TAG_NAME + _add_tags(Todo::STARRED_TAG_NAME) tags.reload end starred? diff --git a/app/models/todo.rb b/app/models/todo.rb index fb5539ac..97a1b984 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -118,10 +118,10 @@ class Todo < ActiveRecord::Base def toggle_star! if starred? - delete_tags STARRED_TAG_NAME + _remove_tags STARRED_TAG_NAME tags.reload else - add_tag STARRED_TAG_NAME + _add_tags(STARRED_TAG_NAME) tags.reload end starred? diff --git a/lib/tagging_extensions.rb b/lib/tagging_extensions.rb index 5ef0e061..748b226f 100644 --- a/lib/tagging_extensions.rb +++ b/lib/tagging_extensions.rb @@ -7,11 +7,11 @@ class ActiveRecord::Base #:nodoc: # Add tags to self. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags. # # We need to avoid name conflicts with the built-in ActiveRecord association methods, thus the underscores. - def _add_tags incoming, user + def _add_tags incoming taggable?(true) tag_cast_to_string(incoming).each do |tag_name| begin - tag = Tag.find_or_create_by_name(tag_name).on(self,user) + tag = Tag.find_or_create_by_name(tag_name) raise Tag::Error, "tag could not be saved: #{tag_name}" if tag.new_record? tag.taggables << self rescue ActiveRecord::StatementInvalid => e @@ -21,7 +21,7 @@ class ActiveRecord::Base #:nodoc: end # Removes tags from self. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags. - def _remove_tags outgoing, user + def _remove_tags outgoing taggable?(true) outgoing = tag_cast_to_string(outgoing) @@ -36,7 +36,7 @@ class ActiveRecord::Base #:nodoc: end # Replace the existing tags on self. Accepts a string of tagnames, an array of tagnames, an array of ids, or an array of Tags. - def tag_with list, user + def tag_with list #:stopdoc: taggable?(true) list = tag_cast_to_string(list) @@ -44,8 +44,8 @@ class ActiveRecord::Base #:nodoc: # Transactions may not be ideal for you here; be aware. Tag.transaction do current = tags.map(&:name) - _add_tags(list - current, user) - _remove_tags(current - list, user) + _add_tags(list - current) + _remove_tags(current - list) end self @@ -78,6 +78,7 @@ class ActiveRecord::Base #:nodoc: when String obj = obj.split(Tag::DELIMITER).map do |tag_name| tag_name.strip.squeeze(" ") + puts "tn=#{tag_name.strip.squeeze(" ")}" end else raise "Invalid object of class #{obj.class} as tagging method parameter" diff --git a/test/unit/recurring_todo_test.rb b/test/unit/recurring_todo_test.rb index 80106554..e7f5ef25 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", User.find(@yearly.user_id)) - @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 85c69b39..3e0fdf4d 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_tag('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