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/contexts_controller.rb b/app/controllers/contexts_controller.rb index 348151aa..2412ba95 100644 --- a/app/controllers/contexts_controller.rb +++ b/app/controllers/contexts_controller.rb @@ -23,6 +23,7 @@ class ContextsController < ApplicationController end def show + @contexts = current_user.contexts(true) if (@context.nil?) respond_to do |format| format.html { render :text => 'Context not found', :status => 404 } 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/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 0b38d202..1bf4eb13 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -1,287 +1,287 @@ -module TodosHelper - - # #require 'users_controller' Counts the number of incomplete items in the - # specified context - # - def count_items(context) - count = Todo.find_all("done=0 AND context_id=#{context.id}").length - end - - def form_remote_tag_edit_todo( &block ) - form_tag( - todo_path(@todo), { - :method => :put, - :id => dom_id(@todo, 'form'), - :class => dom_id(@todo, 'form') + " inline-form edit_todo_form" }, - &block ) - apply_behavior 'form.edit_todo_form', make_remote_form( - :method => :put, - :before => "this.down('button.positive').startWaiting()", - :loaded => "this.down('button.positive').stopWaiting()", - :condition => "!(this.down('button.positive').isWaiting())"), - :prevent_default => true - end - - def set_behavior_for_delete_icon - parameters = "_source_view=#{@source_view}" - parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag' - apply_behavior '.item-container a.delete_icon:click', :prevent_default => true do |page| - page.confirming "'Are you sure that you want to ' + this.title + '?'" do - page << "itemContainer = this.up('.item-container'); itemContainer.startWaiting();" - page << remote_to_href(:method => 'delete', :with => "'#{parameters}'", :complete => "itemContainer.stopWaiting();") - end - end - end - - def remote_delete_icon - str = link_to( image_tag_for_delete, - todo_path(@todo), :id => "delete_icon_"+@todo.id.to_s, - :class => "icon delete_icon", :title => "delete the action '#{@todo.description}'") - set_behavior_for_delete_icon - str - end - - def set_behavior_for_star_icon - apply_behavior '.item-container a.star_item:click', - remote_to_href(:method => 'put', :with => "{ _source_view : '#{@source_view}' }"), - :prevent_default => true - end - - def remote_star_icon - str = link_to( image_tag_for_star(@todo), - toggle_star_todo_path(@todo), - :class => "icon star_item", :title => "star the action '#{@todo.description}'") - set_behavior_for_star_icon - str - end - - def set_behavior_for_edit_icon - parameters = "_source_view=#{@source_view}" - parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag' - apply_behavior '.item-container a.edit_icon:click', :prevent_default => true do |page| - page << "Effect.Pulsate(this);" - page << remote_to_href(:method => 'get', :with => "'#{parameters}'") - end - end - - def remote_edit_icon - if !@todo.completed? - str = link_to( image_tag_for_edit(@todo), - edit_todo_path(@todo), - :class => "icon edit_icon") - set_behavior_for_edit_icon - else - str = '' + image_tag("blank.png") + " " - end - str - end - - def set_behavior_for_toggle_checkbox - parameters = "_source_view=#{@source_view}" - parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag' - apply_behavior '.item-container input.item-checkbox:click', - remote_function(:url => javascript_variable('this.value'), :method => 'put', - :with => "'#{parameters}'") - end - - def remote_toggle_checkbox - str = check_box_tag('item_id', toggle_check_todo_path(@todo), @todo.completed?, :class => 'item-checkbox') - set_behavior_for_toggle_checkbox - str - end - - def date_span - if @todo.completed? - "#{format_date( @todo.completed_at )}" - elsif @todo.deferred? - show_date( @todo.show_from ) - else - due_date( @todo.due ) - end - end - - def tag_list_text - @todo.tags.collect{|t| t.name}.join(', ') - end - - def tag_list - tags_except_starred = @todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME} - tag_list = tags_except_starred.collect{|t| "" + link_to(t.name, :controller => "todos", :action => "tag", :id => t.name) + ""}.join('') - "#{tag_list}" - end - - def tag_list_mobile - tags_except_starred = @todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME} - # removed the link. TODO: add link to mobile view of tagged actions - tag_list = tags_except_starred.collect{|t| - "" + - link_to(t.name, {:action => "tag", :controller => "todos", :id => t.name+".m"}) + - ""}.join('') - if tag_list.empty? then "" else "#{tag_list}" end - end - - def deferred_due_date - if @todo.deferred? && @todo.due - "(action due on #{format_date(@todo.due)})" - end - end - - def project_and_context_links(parent_container_type, opts = {}) - str = '' - if @todo.completed? - str += @todo.context.name unless opts[:suppress_context] - should_suppress_project = opts[:suppress_project] || @todo.project.nil? - str += ", " unless str.blank? || should_suppress_project - str += @todo.project.name unless should_suppress_project - str = "(#{str})" unless str.blank? - else - if (['project', 'tag', 'stats', 'search'].include?(parent_container_type)) - str << item_link_to_context( @todo ) - end - if (['context', 'tickler', 'tag', 'stats', 'search'].include?(parent_container_type)) && @todo.project_id - str << item_link_to_project( @todo ) - end - end - return str - end - - # Uses the 'staleness_starts' value from settings.yml (in days) to colour the - # background of the action appropriately according to the age of the creation - # date: - # * l1: created more than 1 x staleness_starts, but < 2 x staleness_starts - # * l2: created more than 2 x staleness_starts, but < 3 x staleness_starts - # * l3: created more than 3 x staleness_starts - # - def staleness_class(item) - if item.due || item.completed? - return "" - elsif item.created_at < user_time - (prefs.staleness_starts * 3).days - return " stale_l3" - elsif item.created_at < user_time - (prefs.staleness_starts * 2).days - return " stale_l2" - elsif item.created_at < user_time - (prefs.staleness_starts).days - return " stale_l1" - else - return "" - end - end - - # Check show_from date in comparison to today's date Flag up date - # appropriately with a 'traffic light' colour code - # - def show_date(d) - if d == nil - return "" - end - - days = days_from_today(d) - - case days - # overdue or due very soon! sound the alarm! - when -1000..-1 - "Scheduled to show " + (days * -1).to_s + " days ago " - when 0 - "Show Today " - when 1 - "Show Tomorrow " - # due 2-7 days away - when 2..7 - if prefs.due_style == Preference.due_styles[:due_on] - "Show on " + d.strftime("%A") + " " - else - "Show in " + days.to_s + " days " - end - # more than a week away - relax - else - "Show in " + days.to_s + " days " - end - end - - def calendar_setup( input_field ) - str = "Calendar.setup({ ifFormat:\"#{prefs.date_format}\"" - str << ",firstDay:#{prefs.week_starts},showOthers:true,range:[2004, 2010]" - str << ",step:1,inputField:\"" + input_field + "\",cache:true,align:\"TR\" })\n" - javascript_tag str - end - - def item_container_id (todo) - if source_view_is :project - return "p#{todo.project_id}" if todo.active? - return "tickler" if todo.deferred? - end - return "c#{todo.context_id}" - end - - def should_show_new_item - - if @todo.project.nil? == false - # do not show new actions that were added to hidden or completed projects - # on home page and context page - return false if source_view_is(:todo) && (@todo.project.hidden? || @todo.project.completed?) - return false if source_view_is(:context) && (@todo.project.hidden? || @todo.project.completed?) - end - - return true if source_view_is(:deferred) && @todo.deferred? - return true if source_view_is(:project) && @todo.project.hidden? && @todo.project_hidden? - return true if source_view_is(:project) && @todo.deferred? - return true if !source_view_is(:deferred) && @todo.active? - return false - end - - def parent_container_type - return 'tickler' if source_view_is :deferred - return 'project' if source_view_is :project - return 'stats' if source_view_is :stats - return 'context' - end - - def empty_container_msg_div_id - return "tickler-empty-nd" if source_view_is(:project) && @todo.deferred? - return "p#{@todo.project_id}empty-nd" if source_view_is :project - return "c#{@todo.context_id}empty-nd" - end - - def project_names_for_autocomplete - array_or_string_for_javascript( ['None'] + current_user.projects.active.collect{|p| escape_javascript(p.name) } ) - end - - def context_names_for_autocomplete - # #return array_or_string_for_javascript(['Create a new context']) if - # @contexts.empty? - array_or_string_for_javascript( current_user.contexts.collect{|c| escape_javascript(c.name) } ) - end - - def format_ical_notes(notes) - split_notes = notes.split(/\n/) - joined_notes = split_notes.join("\\n") - end - - def formatted_pagination(total) - s = will_paginate(@todos) - (s.gsub /(<\/[^<]+>)/, '\1 ').chomp(' ') - end - - def date_field_tag(name, id, value = nil, options = {}) - text_field_tag name, value, {"size" => 12, "id" => id, "class" => "Date", "onfocus" => "Calendar.setup", "autocomplete" => "off"}.update(options.stringify_keys) - end - - private - - def image_tag_for_delete - image_tag("blank.png", :title =>"Delete action", :class=>"delete_item") - end - - def image_tag_for_edit(todo) - image_tag("blank.png", :title =>"Edit action", :class=>"edit_item", :id=> dom_id(todo, 'edit_icon')) - end - - def image_tag_for_star(todo) - class_str = todo.starred? ? "starred_todo" : "unstarred_todo" - image_tag("blank.png", :title =>"Star action", :class => class_str) - end - - def defer_link(days) - link_to_remote image_tag("defer_#{days}.png", :alt => "Defer #{pluralize(days, 'day')}"), :url => {:controller => 'todos', :action => 'defer', :id => @todo.id, :days => days, :_source_view => (@source_view.underscore.gsub(/\s+/,'_') rescue "")} - end - -end +module TodosHelper + + # #require 'users_controller' Counts the number of incomplete items in the + # specified context + # + def count_items(context) + count = Todo.find_all("done=0 AND context_id=#{context.id}").length + end + + def form_remote_tag_edit_todo( &block ) + form_tag( + todo_path(@todo), { + :method => :put, + :id => dom_id(@todo, 'form'), + :class => dom_id(@todo, 'form') + " inline-form edit_todo_form" }, + &block ) + apply_behavior 'form.edit_todo_form', make_remote_form( + :method => :put, + :before => "this.down('button.positive').startWaiting()", + :loaded => "this.down('button.positive').stopWaiting()", + :condition => "!(this.down('button.positive').isWaiting())"), + :prevent_default => true + end + + def set_behavior_for_delete_icon + parameters = "_source_view=#{@source_view}" + parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag' + apply_behavior '.item-container a.delete_icon:click', :prevent_default => true do |page| + page.confirming "'Are you sure that you want to ' + this.title + '?'" do + page << "itemContainer = this.up('.item-container'); itemContainer.startWaiting();" + page << remote_to_href(:method => 'delete', :with => "'#{parameters}'", :complete => "itemContainer.stopWaiting();") + end + end + end + + def remote_delete_icon + str = link_to( image_tag_for_delete, + todo_path(@todo), :id => "delete_icon_"+@todo.id.to_s, + :class => "icon delete_icon", :title => "delete the action '#{@todo.description}'") + set_behavior_for_delete_icon + str + end + + def set_behavior_for_star_icon + apply_behavior '.item-container a.star_item:click', + remote_to_href(:method => 'put', :with => "{ _source_view : '#{@source_view}' }"), + :prevent_default => true + end + + def remote_star_icon + str = link_to( image_tag_for_star(@todo), + toggle_star_todo_path(@todo), + :class => "icon star_item", :title => "star the action '#{@todo.description}'") + set_behavior_for_star_icon + str + end + + def set_behavior_for_edit_icon + parameters = "_source_view=#{@source_view}" + parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag' + apply_behavior '.item-container a.edit_icon:click', :prevent_default => true do |page| + page << "Effect.Pulsate(this);" + page << remote_to_href(:method => 'get', :with => "'#{parameters}'") + end + end + + def remote_edit_icon + if !@todo.completed? + str = link_to( image_tag_for_edit(@todo), + edit_todo_path(@todo), + :class => "icon edit_icon") + set_behavior_for_edit_icon + else + str = '' + image_tag("blank.png") + " " + end + str + end + + def set_behavior_for_toggle_checkbox + parameters = "_source_view=#{@source_view}" + parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag' + apply_behavior '.item-container input.item-checkbox:click', + remote_function(:url => javascript_variable('this.value'), :method => 'put', + :with => "'#{parameters}'") + end + + def remote_toggle_checkbox + str = check_box_tag('item_id', toggle_check_todo_path(@todo), @todo.completed?, :class => 'item-checkbox') + set_behavior_for_toggle_checkbox + str + end + + def date_span + if @todo.completed? + "#{format_date( @todo.completed_at )}" + elsif @todo.deferred? + show_date( @todo.show_from ) + else + due_date( @todo.due ) + end + end + + def tag_list_text + @todo.tags.collect{|t| t.name}.join(', ') + end + + def tag_list + tags_except_starred = @todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME} + tag_list = tags_except_starred.collect{|t| "" + link_to(t.name, :controller => "todos", :action => "tag", :id => t.name) + ""}.join('') + "#{tag_list}" + end + + def tag_list_mobile + tags_except_starred = @todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME} + # removed the link. TODO: add link to mobile view of tagged actions + tag_list = tags_except_starred.collect{|t| + "" + + link_to(t.name, {:action => "tag", :controller => "todos", :id => t.name+".m"}) + + ""}.join('') + if tag_list.empty? then "" else "#{tag_list}" end + end + + def deferred_due_date + if @todo.deferred? && @todo.due + "(action due on #{format_date(@todo.due)})" + end + end + + def project_and_context_links(parent_container_type, opts = {}) + str = '' + if @todo.completed? + str += @todo.context.name unless opts[:suppress_context] + should_suppress_project = opts[:suppress_project] || @todo.project.nil? + str += ", " unless str.blank? || should_suppress_project + str += @todo.project.name unless should_suppress_project + str = "(#{str})" unless str.blank? + else + if (['project', 'tag', 'stats', 'search'].include?(parent_container_type)) + str << item_link_to_context( @todo ) + end + if (['context', 'tickler', 'tag', 'stats', 'search'].include?(parent_container_type)) && @todo.project_id + str << item_link_to_project( @todo ) + end + end + return str + end + + # Uses the 'staleness_starts' value from settings.yml (in days) to colour the + # background of the action appropriately according to the age of the creation + # date: + # * l1: created more than 1 x staleness_starts, but < 2 x staleness_starts + # * l2: created more than 2 x staleness_starts, but < 3 x staleness_starts + # * l3: created more than 3 x staleness_starts + # + def staleness_class(item) + if item.due || item.completed? + return "" + elsif item.created_at < user_time - (prefs.staleness_starts * 3).days + return " stale_l3" + elsif item.created_at < user_time - (prefs.staleness_starts * 2).days + return " stale_l2" + elsif item.created_at < user_time - (prefs.staleness_starts).days + return " stale_l1" + else + return "" + end + end + + # Check show_from date in comparison to today's date Flag up date + # appropriately with a 'traffic light' colour code + # + def show_date(d) + if d == nil + return "" + end + + days = days_from_today(d) + + case days + # overdue or due very soon! sound the alarm! + when -1000..-1 + "Scheduled to show " + (days * -1).to_s + " days ago " + when 0 + "Show Today " + when 1 + "Show Tomorrow " + # due 2-7 days away + when 2..7 + if prefs.due_style == Preference.due_styles[:due_on] + "Show on " + d.strftime("%A") + " " + else + "Show in " + days.to_s + " days " + end + # more than a week away - relax + else + "Show in " + days.to_s + " days " + end + end + + def calendar_setup( input_field ) + str = "Calendar.setup({ ifFormat:\"#{prefs.date_format}\"" + str << ",firstDay:#{prefs.week_starts},showOthers:true,range:[2004, 2010]" + str << ",step:1,inputField:\"" + input_field + "\",cache:true,align:\"TR\" })\n" + javascript_tag str + end + + def item_container_id (todo) + if source_view_is :project + return "p#{todo.project_id}" if todo.active? + return "tickler" if todo.deferred? + end + return "c#{todo.context_id}" + end + + def should_show_new_item + + if @todo.project.nil? == false + # do not show new actions that were added to hidden or completed projects + # on home page and context page + return false if source_view_is(:todo) && (@todo.project.hidden? || @todo.project.completed?) + return false if source_view_is(:context) && (@todo.project.hidden? || @todo.project.completed?) + end + + return true if source_view_is(:deferred) && @todo.deferred? + return true if source_view_is(:project) && @todo.project.hidden? && @todo.project_hidden? + return true if source_view_is(:project) && @todo.deferred? + return true if !source_view_is(:deferred) && @todo.active? + return false + end + + def parent_container_type + return 'tickler' if source_view_is :deferred + return 'project' if source_view_is :project + return 'stats' if source_view_is :stats + return 'context' + end + + def empty_container_msg_div_id + return "tickler-empty-nd" if source_view_is(:project) && @todo.deferred? + return "p#{@todo.project_id}empty-nd" if source_view_is :project + return "c#{@todo.context_id}empty-nd" + end + + def project_names_for_autocomplete + array_or_string_for_javascript( ['None'] + current_user.projects.active.collect{|p| escape_javascript(p.name) } ) + end + + def context_names_for_autocomplete + # #return array_or_string_for_javascript(['Create a new context']) if + # @contexts.empty? + array_or_string_for_javascript( current_user.contexts.collect{|c| escape_javascript(c.name) } ) + end + + def format_ical_notes(notes) + split_notes = notes.split(/\n/) + joined_notes = split_notes.join("\\n") + end + + def formatted_pagination(total) + s = will_paginate(@todos) + (s.gsub(/(<\/[^<]+>)/, '\1 ')).chomp(' ') + end + + def date_field_tag(name, id, value = nil, options = {}) + text_field_tag name, value, {"size" => 12, "id" => id, "class" => "Date", "onfocus" => "Calendar.setup", "autocomplete" => "off"}.update(options.stringify_keys) + end + + private + + def image_tag_for_delete + image_tag("blank.png", :title =>"Delete action", :class=>"delete_item") + end + + def image_tag_for_edit(todo) + image_tag("blank.png", :title =>"Edit action", :class=>"edit_item", :id=> dom_id(todo, 'edit_icon')) + end + + def image_tag_for_star(todo) + class_str = todo.starred? ? "starred_todo" : "unstarred_todo" + image_tag("blank.png", :title =>"Star action", :class => class_str) + end + + def defer_link(days) + link_to_remote image_tag("defer_#{days}.png", :alt => "Defer #{pluralize(days, 'day')}"), :url => {:controller => 'todos', :action => 'defer', :id => @todo.id, :days => days, :_source_view => (@source_view.underscore.gsub(/\s+/,'_') rescue "")} + end + +end diff --git a/app/models/recurring_todo.rb b/app/models/recurring_todo.rb index 7d24b7eb..f8f22526 100644 --- a/app/models/recurring_todo.rb +++ b/app/models/recurring_todo.rb @@ -362,14 +362,14 @@ class RecurringTodo < ActiveRecord::Base end def starred? - tags.any? {|tag| tag.name == Todo::STARRED_TAG_NAME} + tags.any? {|tag| tag.name == Todo::STARRED_TAG_NAME } end def get_due_date(previous) case self.target when 'due_date' return get_next_date(previous) - when 'show_from' + when 'show_from_date' # so leave due date empty return nil else @@ -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/tag.rb b/app/models/tag.rb index 81684c13..28f72662 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -1,11 +1,48 @@ -class Tag < ActiveRecord::Base - has_many_polymorphs :taggables, - :from => [:todos, :recurring_todos], - :through => :taggings, - :dependent => :destroy - def on(taggable, user) - tagging = taggings.create :taggable => taggable, :user => user +# The Tag model. This model is automatically generated and added to your app if +# you run the tagging generator included with has_many_polymorphs. + +class Tag < ActiveRecord::Base + + DELIMITER = "," # Controls how to split and join tagnames from strings. You may need to change the validates_format_of parameters if you change this. + JOIN_DELIMITER = ", " + + # If database speed becomes an issue, you could remove these validations and + # rescue the ActiveRecord database constraint errors instead. + validates_presence_of :name + validates_uniqueness_of :name, :case_sensitive => false + + # Change this validation if you need more complex tag names. + # validates_format_of :name, :with => /^[a-zA-Z0-9\_\-]+$/, :message => "can not contain special characters" + + # Set up the polymorphic relationship. + has_many_polymorphs :taggables, + :from => [:todos, :recurring_todos], + :through => :taggings, + :dependent => :destroy, + :skip_duplicates => false, + :parent_extend => proc { + # Defined on the taggable models, not on Tag itself. Return the tagnames + # associated with this record as a string. + def to_s + self.map(&:name).sort.join(Tag::JOIN_DELIMITER) + end + } + + # Callback to strip extra spaces from the tagname before saving it. If you + # allow tags to be renamed later, you might want to use the + # before_save callback instead. + def before_create + self.name = name.downcase.strip.squeeze(" ") end -end \ No newline at end of file + def on(taggable, user) + taggings.create :taggable => taggable, :user => user + end + + # Tag::Error class. Raised by ActiveRecord::Base::TaggingExtensions if + # something goes wrong. + class Error < StandardError + end + +end diff --git a/app/models/tagging.rb b/app/models/tagging.rb index 16c1d5da..de6c3743 100644 --- a/app/models/tagging.rb +++ b/app/models/tagging.rb @@ -1,11 +1,17 @@ -class Tagging < ActiveRecord::Base + +# The Tagging join model. This model is automatically generated and added to your app if you run the tagging generator included with has_many_polymorphs. + +class Tagging < ActiveRecord::Base + belongs_to :tag belongs_to :taggable, :polymorphic => true - belongs_to :user - - # def before_destroy - # # disallow orphaned tags - # # TODO: this doesn't seem to be working - # tag.destroy if tag.taggings.count < 2 - # end + # belongs_to :user + + # If you also need to use acts_as_list, you will have to manage the tagging positions manually by creating decorated join records when you associate Tags with taggables. + # acts_as_list :scope => :taggable + + # This callback makes sure that an orphaned Tag is deleted if it no longer tags anything. + def after_destroy + tag.destroy_without_callbacks if tag and tag.taggings.count == 0 + end end diff --git a/app/models/todo.rb b/app/models/todo.rb index 5aed02be..97a1b984 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -1,176 +1,176 @@ -class Todo < ActiveRecord::Base - - belongs_to :context - belongs_to :project - belongs_to :user - belongs_to :recurring_todo - - named_scope :active, :conditions => { :state => 'active' } - named_scope :not_completed, :conditions => ['NOT state = ? ', 'completed'] - named_scope :are_due, :conditions => ['NOT todos.due IS NULL'] - - STARRED_TAG_NAME = "starred" - - acts_as_state_machine :initial => :active, :column => 'state' - - # when entering active state, also remove completed_at date. Looks like :exit - # of state completed is not run, see #679 - state :active, :enter => Proc.new { |t| t[:show_from], t.completed_at = nil, nil } - state :project_hidden - state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil } - state :deferred - - event :defer do - transitions :to => :deferred, :from => [:active] - end - - event :complete do - transitions :to => :completed, :from => [:active, :project_hidden, :deferred] - end - - event :activate do - transitions :to => :active, :from => [:project_hidden, :completed, :deferred] - end - - event :hide do - transitions :to => :project_hidden, :from => [:active, :deferred] - end - - event :unhide do - transitions :to => :deferred, :from => [:project_hidden], :guard => Proc.new{|t| !t.show_from.blank? } - transitions :to => :active, :from => [:project_hidden] - end - - attr_protected :user - - # Description field can't be empty, and must be < 100 bytes Notes must be < - # 60,000 bytes (65,000 actually, but I'm being cautious) - validates_presence_of :description - validates_length_of :description, :maximum => 100 - validates_length_of :notes, :maximum => 60000, :allow_nil => true - validates_presence_of :show_from, :if => :deferred? - validates_presence_of :context - - def validate - if !show_from.blank? && show_from < user.date - errors.add("show_from", "must be a date in the future") - end - end - - def toggle_completion! - saved = false - if completed? - saved = activate! - else - saved = complete! - end - return saved - end - - def show_from - self[:show_from] - end - - def show_from=(date) - # parse Date objects into the proper timezone - date = user.at_midnight(date) if (date.is_a? Date) - activate! if deferred? && date.blank? - defer! if active? && !date.blank? && date > user.date - self[:show_from] = date - end - - alias_method :original_project, :project - - def project - original_project.nil? ? Project.null_object : original_project - end - - alias_method :original_set_initial_state, :set_initial_state - - def set_initial_state - if show_from && (show_from > user.date) - write_attribute self.class.state_column, 'deferred' - else - original_set_initial_state - end - end - - alias_method :original_run_initial_state_actions, :run_initial_state_actions - - def run_initial_state_actions - # only run the initial state actions if the standard initial state hasn't - # been changed - if self.class.initial_state.to_sym == current_state - original_run_initial_state_actions - end - end - - def self.feed_options(user) - { - :title => 'Tracks Actions', - :description => "Actions for #{user.display_name}" - } - end - - def starred? - tags.any? {|tag| tag.name == STARRED_TAG_NAME} - end - - def toggle_star! - if starred? - delete_tags STARRED_TAG_NAME - tags.reload - else - add_tag STARRED_TAG_NAME - tags.reload - end - starred? - end - - def from_recurring_todo? - return self.recurring_todo_id != nil - end - - # Rich Todo API - - def self.from_rich_message(user, default_context_id, description, notes) - fields = description.match /([^>@]*)@?([^>]*)>?(.*)/ - description = fields[1].strip - context = fields[2].strip - project = fields[3].strip - - context = nil if context == "" - project = nil if project == "" - - context_id = default_context_id - unless(context.nil?) - found_context = user.active_contexts.find_by_namepart(context) - found_context = user.contexts.find_by_namepart(context) if found_context.nil? - context_id = found_context.id unless found_context.nil? - end - - unless user.contexts.exists? context_id - raise(CannotAccessContext, "Cannot access a context that does not belong to this user.") - end - - project_id = nil - unless(project.blank?) - if(project[0..3].downcase == "new:") - found_project = user.projects.build - found_project.name = project[4..255+4].strip - found_project.save! - else - found_project = user.active_projects.find_by_namepart(project) - found_project = user.projects.find_by_namepart(project) if found_project.nil? - end - project_id = found_project.id unless found_project.nil? - end - - todo = user.todos.build - todo.description = description - todo.notes = notes - todo.context_id = context_id - todo.project_id = project_id unless project_id.nil? - return todo - end -end +class Todo < ActiveRecord::Base + + belongs_to :context + belongs_to :project + belongs_to :user + belongs_to :recurring_todo + + named_scope :active, :conditions => { :state => 'active' } + named_scope :not_completed, :conditions => ['NOT state = ? ', 'completed'] + named_scope :are_due, :conditions => ['NOT todos.due IS NULL'] + + STARRED_TAG_NAME = "starred" + + acts_as_state_machine :initial => :active, :column => 'state' + + # when entering active state, also remove completed_at date. Looks like :exit + # of state completed is not run, see #679 + state :active, :enter => Proc.new { |t| t[:show_from], t.completed_at = nil, nil } + state :project_hidden + state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil } + state :deferred + + event :defer do + transitions :to => :deferred, :from => [:active] + end + + event :complete do + transitions :to => :completed, :from => [:active, :project_hidden, :deferred] + end + + event :activate do + transitions :to => :active, :from => [:project_hidden, :completed, :deferred] + end + + event :hide do + transitions :to => :project_hidden, :from => [:active, :deferred] + end + + event :unhide do + transitions :to => :deferred, :from => [:project_hidden], :guard => Proc.new{|t| !t.show_from.blank? } + transitions :to => :active, :from => [:project_hidden] + end + + attr_protected :user + + # Description field can't be empty, and must be < 100 bytes Notes must be < + # 60,000 bytes (65,000 actually, but I'm being cautious) + validates_presence_of :description + validates_length_of :description, :maximum => 100 + validates_length_of :notes, :maximum => 60000, :allow_nil => true + validates_presence_of :show_from, :if => :deferred? + validates_presence_of :context + + def validate + if !show_from.blank? && show_from < user.date + errors.add("show_from", "must be a date in the future") + end + end + + def toggle_completion! + saved = false + if completed? + saved = activate! + else + saved = complete! + end + return saved + end + + def show_from + self[:show_from] + end + + def show_from=(date) + # parse Date objects into the proper timezone + date = user.at_midnight(date) if (date.is_a? Date) + activate! if deferred? && date.blank? + defer! if active? && !date.blank? && date > user.date + self[:show_from] = date + end + + alias_method :original_project, :project + + def project + original_project.nil? ? Project.null_object : original_project + end + + alias_method :original_set_initial_state, :set_initial_state + + def set_initial_state + if show_from && (show_from > user.date) + write_attribute self.class.state_column, 'deferred' + else + original_set_initial_state + end + end + + alias_method :original_run_initial_state_actions, :run_initial_state_actions + + def run_initial_state_actions + # only run the initial state actions if the standard initial state hasn't + # been changed + if self.class.initial_state.to_sym == current_state + original_run_initial_state_actions + end + end + + def self.feed_options(user) + { + :title => 'Tracks Actions', + :description => "Actions for #{user.display_name}" + } + end + + def starred? + tags.any? {|tag| tag.name == STARRED_TAG_NAME} + end + + def toggle_star! + if starred? + _remove_tags STARRED_TAG_NAME + tags.reload + else + _add_tags(STARRED_TAG_NAME) + tags.reload + end + starred? + end + + def from_recurring_todo? + return self.recurring_todo_id != nil + end + + # Rich Todo API + + def self.from_rich_message(user, default_context_id, description, notes) + fields = description.match(/([^>@]*)@?([^>]*)>?(.*)/) + description = fields[1].strip + context = fields[2].strip + project = fields[3].strip + + context = nil if context == "" + project = nil if project == "" + + context_id = default_context_id + unless(context.nil?) + found_context = user.active_contexts.find_by_namepart(context) + found_context = user.contexts.find_by_namepart(context) if found_context.nil? + context_id = found_context.id unless found_context.nil? + end + + unless user.contexts.exists? context_id + raise(CannotAccessContext, "Cannot access a context that does not belong to this user.") + end + + project_id = nil + unless(project.blank?) + if(project[0..3].downcase == "new:") + found_project = user.projects.build + found_project.name = project[4..255+4].strip + found_project.save! + else + found_project = user.active_projects.find_by_namepart(project) + found_project = user.projects.find_by_namepart(project) if found_project.nil? + end + project_id = found_project.id unless found_project.nil? + end + + todo = user.todos.build + todo.description = description + todo.notes = notes + todo.context_id = context_id + todo.project_id = project_id unless project_id.nil? + return todo + end +end diff --git a/app/views/shared/_add_new_item_form.rhtml b/app/views/shared/_add_new_item_form.rhtml index 3c3b6f8a..3e3f749f 100644 --- a/app/views/shared/_add_new_item_form.rhtml +++ b/app/views/shared/_add_new_item_form.rhtml @@ -24,7 +24,21 @@ :html=> { :id=>'todo-form-new-action', :name=>'todo', :class => 'inline-form' }, :before => "$('todo_new_action_submit').startWaiting()", :complete => "$('todo_new_action_submit').stopWaiting()", - :condition => "!$('todo_new_action_submit').isWaiting()") do -%> + :condition => "!$('todo_new_action_submit').isWaiting() && askIfNewContextProvided()") do -%> + +
<%= error_messages_for("item", :object_name => 'action') %>
diff --git a/app/views/todos/_mobile_todo.rhtml b/app/views/todos/_mobile_todo.rhtml index 88147afa..e2561b69 100644 --- a/app/views/todos/_mobile_todo.rhtml +++ b/app/views/todos/_mobile_todo.rhtml @@ -14,12 +14,12 @@ end -%> <% end -%> <%= date_span -%> <%= link_to mobile_todo.description, formatted_todo_path(mobile_todo, :m) -%> <% if parent_container_type == 'context' or parent_container_type == 'tag' -%> -<%= " (" + +<%= " (" + link_to(mobile_todo.project.name, formatted_project_path(mobile_todo.project, :m)) + ")" unless mobile_todo.project.nil? -%> <% end if parent_container_type == 'project' or parent_container_type == 'tag' -%> -<%= " (" + +<%= " (" + link_to(mobile_todo.context.name, formatted_context_path(mobile_todo.context, :m)) + ")" -%> <% end -%> diff --git a/config/environment.rb.tmpl b/config/environment.rb.tmpl index b9fbcf86..fa48939e 100644 --- a/config/environment.rb.tmpl +++ b/config/environment.rb.tmpl @@ -1,109 +1,111 @@ -# Be sure to restart your webserver when you modify this file. -# Uncomment below to force Rails into production mode - -# (Use only when you can't set environment variables through your web/app server) -# ENV['RAILS_ENV'] = 'production' - -# Bootstrap the Rails environment, frameworks, and default configuration -require File.join(File.dirname(__FILE__), 'boot') - -# This is the 'salt' to add to the password before it is encrypted -# You need to change this to something unique for yourself -SALT = "change-me" - -class Rails::Configuration - attr_accessor :action_web_service -end - -# Leave this alone or set it to one or more of ['database', 'ldap', 'open_id']. -# If you choose ldap, see the additional configuration options further down. -AUTHENTICATION_SCHEMES = ['database'] - -Rails::Initializer.run do |config| - # Skip frameworks you're not going to use - # config.frameworks -= [ :action_web_service, :action_mailer ] - config.frameworks += [ :action_web_service] - config.action_web_service = Rails::OrderedOptions.new - config.load_paths += %W( #{RAILS_ROOT}/app/apis ) - - config.action_controller.use_accept_header = true - - # Add additional load paths for your own custom dirs - # config.load_paths += %W( #{RAILS_ROOT}/app/services ) - - # Force all environments to use the same logger level - # (by default production uses :info, the others :debug) - # config.log_level = :debug - - # Use the database for sessions instead of the file system - # (create the session table with 'rake create_sessions_table') - config.action_controller.session_store = :active_record_store - - config.action_controller.session = { - :session_key => '_tracks_session_id', - :secret => SALT * (30.0 / SALT.length).ceil #must be at least 30 characters - } - - # Enable page/fragment caching by setting a file-based store - # (remember to create the caching directory and make it readable to the application) - # config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache" - - # Activate observers that should always be running - # config.active_record.observers = :cacher, :garbage_collector - - # Make Active Record use UTC-base instead of local time - config.active_record.default_timezone = :utc - - # You''ll probably want to change this to the time zone of the computer where Tracks is running - # run rake time:zones:local have Rails suggest time zone names on your system - config.time_zone = 'UTC' - - # Use Active Record's schema dumper instead of SQL when creating the test database - # (enables use of different database adapters for development and test environments) - config.active_record.schema_format = :ruby - - # See Rails::Configuration for more options -end - -# Add new inflection rules using the following format -# (all these examples are active by default): -# Inflector.inflections do |inflect| -# inflect.plural /^(ox)$/i, '\1en' -# inflect.singular /^(ox)en/i, '\1' -# inflect.irregular 'person', 'people' -# inflect.uncountable %w( fish sheep ) -# end - -# Include your application configuration below - - -require 'name_part_finder' -require 'tracks/todo_list' -require 'tracks/config' -require 'activerecord_base_tag_extensions' # Needed for tagging-specific extensions -require 'digest/sha1' #Needed to support 'rake db:fixtures:load' on some ruby installs: http://dev.rousette.org.uk/ticket/557 -require 'prototype_helper_extensions' - -if (AUTHENTICATION_SCHEMES.include? 'ldap') - require 'net/ldap' #requires ruby-net-ldap gem be installed - require 'simple_ldap_authenticator' - SimpleLdapAuthenticator.ldap_library = 'net/ldap' - SimpleLdapAuthenticator.servers = %w'localhost' - SimpleLdapAuthenticator.use_ssl = false - SimpleLdapAuthenticator.login_format = 'cn=%s,dc=example,dc=com' -end -if (AUTHENTICATION_SCHEMES.include? 'open_id') - #requires ruby-openid gem to be installed -end - -# setting this to true will make the cookies only available over HTTPS -TRACKS_COOKIES_SECURE = false - -tracks_version='1.7RC' - -# comment out next two lines if you do not want (or can not) the date of the -# last git commit in the footer -# info=`git log --pretty=format:"%ai" -1` -# tracks_version=tracks_version + ' ('+info+')' - -TRACKS_VERSION=tracks_version +# Be sure to restart your webserver when you modify this file. +# Uncomment below to force Rails into production mode + +# (Use only when you can't set environment variables through your web/app server) +# ENV['RAILS_ENV'] = 'production' + +# Bootstrap the Rails environment, frameworks, and default configuration +require File.join(File.dirname(__FILE__), 'boot') + +# This is the 'salt' to add to the password before it is encrypted +# You need to change this to something unique for yourself +SALT = "change-me" + +class Rails::Configuration + attr_accessor :action_web_service +end + +# Leave this alone or set it to one or more of ['database', 'ldap', 'open_id']. +# If you choose ldap, see the additional configuration options further down. +AUTHENTICATION_SCHEMES = ['database'] + +Rails::Initializer.run do |config| + # Skip frameworks you're not going to use + # config.frameworks -= [ :action_web_service, :action_mailer ] + config.frameworks += [ :action_web_service] + config.action_web_service = Rails::OrderedOptions.new + config.load_paths += %W( #{RAILS_ROOT}/app/apis ) + + config.gem "highline" + + config.action_controller.use_accept_header = true + + # Add additional load paths for your own custom dirs + # config.load_paths += %W( #{RAILS_ROOT}/app/services ) + + # Force all environments to use the same logger level + # (by default production uses :info, the others :debug) + # config.log_level = :debug + + # Use the database for sessions instead of the file system + # (create the session table with 'rake create_sessions_table') + config.action_controller.session_store = :active_record_store + + config.action_controller.session = { + :session_key => '_tracks_session_id', + :secret => SALT * (30.0 / SALT.length).ceil #must be at least 30 characters + } + + # Enable page/fragment caching by setting a file-based store + # (remember to create the caching directory and make it readable to the application) + # config.action_controller.fragment_cache_store = :file_store, "#{RAILS_ROOT}/cache" + + # Activate observers that should always be running + # config.active_record.observers = :cacher, :garbage_collector + + # Make Active Record use UTC-base instead of local time + config.active_record.default_timezone = :utc + + # You''ll probably want to change this to the time zone of the computer where Tracks is running + # run rake time:zones:local have Rails suggest time zone names on your system + config.time_zone = 'UTC' + + # Use Active Record's schema dumper instead of SQL when creating the test database + # (enables use of different database adapters for development and test environments) + config.active_record.schema_format = :ruby + + # See Rails::Configuration for more options +end + +# Add new inflection rules using the following format +# (all these examples are active by default): +# Inflector.inflections do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# Include your application configuration below + + +require 'name_part_finder' +require 'tracks/todo_list' +require 'tracks/config' +require 'tagging_extensions' # Needed for tagging-specific extensions +require 'digest/sha1' #Needed to support 'rake db:fixtures:load' on some ruby installs: http://dev.rousette.org.uk/ticket/557 +require 'prototype_helper_extensions' + +if (AUTHENTICATION_SCHEMES.include? 'ldap') + require 'net/ldap' #requires ruby-net-ldap gem be installed + require 'simple_ldap_authenticator' + SimpleLdapAuthenticator.ldap_library = 'net/ldap' + SimpleLdapAuthenticator.servers = %w'localhost' + SimpleLdapAuthenticator.use_ssl = false + SimpleLdapAuthenticator.login_format = 'cn=%s,dc=example,dc=com' +end +if (AUTHENTICATION_SCHEMES.include? 'open_id') + #requires ruby-openid gem to be installed +end + +# setting this to true will make the cookies only available over HTTPS +TRACKS_COOKIES_SECURE = false + +tracks_version='1.7RC' + +# comment out next two lines if you do not want (or can not) the date of the +# last git commit in the footer +# info=`git log --pretty=format:"%ai" -1` +# tracks_version=tracks_version + ' ('+info+')' + +TRACKS_VERSION=tracks_version diff --git a/lib/activerecord_base_tag_extensions.rb b/lib/activerecord_base_tag_extensions.rb deleted file mode 100644 index 5361f73e..00000000 --- a/lib/activerecord_base_tag_extensions.rb +++ /dev/null @@ -1,30 +0,0 @@ -class ActiveRecord::Base - # These methods will work for any model instances - - # Tag with deletes all the current tags before adding the new ones - # This makes the edit form more intiuitive: - # Whatever is in the tags text field is what gets set as the tags for that action - # If you submit an empty tags text field, all the tags are removed. - def tag_with(tags, user) - Tag.transaction do - Tagging.delete_all("taggable_id = #{self.id} and taggable_type = '#{self.class}' and user_id = #{user.id}") - tags.downcase.split(",").each do |tag| - Tag.find_or_create_by_name(tag.strip).on(self, user) - end - end - end - - def tag_list - tags.map(&:name).join(', ') - end - - def delete_tags tag_string - split = tag_string.downcase.split(",") - tags.delete tags.select{|t| split.include? t.name.strip} - end - - def add_tag tag_name - Tag.find_or_create_by_name(tag_name.strip).on(self,user) - end - -end \ No newline at end of file diff --git a/lib/tagging_extensions.rb b/lib/tagging_extensions.rb new file mode 100644 index 00000000..ab47bf1f --- /dev/null +++ b/lib/tagging_extensions.rb @@ -0,0 +1,148 @@ + +class ActiveRecord::Base #:nodoc: + + # These extensions make models taggable. This file is automatically generated and required by your app if you run the tagging generator included with has_many_polymorphs. + module TaggingExtensions + + # 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 + taggable?(true) + tag_cast_to_string(incoming).each do |tag_name| + begin + 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 + raise unless e.to_s =~ /duplicate/i + end + end + 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 + taggable?(true) + outgoing = tag_cast_to_string(outgoing) + + tags.delete(*(tags.select do |tag| + outgoing.include? tag.name + end)) + end + + # Returns the tags on self as a string. + def tag_list + # Redefined later to avoid an RDoc parse error. + 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 + #:stopdoc: + taggable?(true) + list = tag_cast_to_string(list) + + # Transactions may not be ideal for you here; be aware. + Tag.transaction do + current = tags.map(&:name) + _add_tags(list - current) + _remove_tags(current - list) + end + + self + #:startdoc: + end + + # Returns the tags on self as a string. + def tag_list #:nodoc: + #:stopdoc: + taggable?(true) + tags.reload + tags.to_s + #:startdoc: + end + + private + + def tag_cast_to_string obj #:nodoc: + case obj + when Array + obj.map! do |item| + case item + # removed next line: its prevents adding numbers as tags + # when /^\d+$/, Fixnum then Tag.find(item).name # This will be slow if you use ids a lot. + when Tag then item.name + when String then item + else + raise "Invalid type" + end + end + when String + obj = obj.split(Tag::DELIMITER).map do |tag_name| + tag_name.strip.squeeze(" ") + end + else + raise "Invalid object of class #{obj.class} as tagging method parameter" + end.flatten.compact.map(&:downcase).uniq + end + + # Check if a model is in the :taggables target list. The alternative to this check is to explicitly include a TaggingMethods module (which you would create) in each target model. + def taggable?(should_raise = false) #:nodoc: + unless flag = respond_to?(:tags) + raise "#{self.class} is not a taggable model" if should_raise + end + flag + end + + end + + module TaggingFinders + # + # Find all the objects tagged with the supplied list of tags + # + # Usage : Model.tagged_with("ruby") + # Model.tagged_with("hello", "world") + # Model.tagged_with("hello", "world", :limit => 10) + # + def tagged_with(*tag_list) + options = tag_list.last.is_a?(Hash) ? tag_list.pop : {} + tag_list = parse_tags(tag_list) + + scope = scope(:find) + options[:select] ||= "#{table_name}.*" + options[:from] ||= "#{table_name}, tags, taggings" + + sql = "SELECT #{(scope && scope[:select]) || options[:select]} " + sql << "FROM #{(scope && scope[:from]) || options[:from]} " + + add_joins!(sql, options, scope) + + sql << "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " + sql << "AND taggings.taggable_type = '#{ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s}' " + sql << "AND taggings.tag_id = tags.id " + + tag_list_condition = tag_list.map {|t| "'#{t}'"}.join(", ") + + sql << "AND (tags.name IN (#{sanitize_sql(tag_list_condition)})) " + sql << "AND #{sanitize_sql(options[:conditions])} " if options[:conditions] + sql << "GROUP BY #{table_name}.id " + sql << "HAVING COUNT(taggings.tag_id) = #{tag_list.size}" + + add_order!(sql, options[:order], scope) + add_limit!(sql, options, scope) + add_lock!(sql, options, scope) + + find_by_sql(sql) + end + + def parse_tags(tags) + return [] if tags.blank? + tags = Array(tags).first + tags = tags.respond_to?(:flatten) ? tags.flatten : tags.split(Tag::DELIMITER) + tags.map { |tag| tag.strip.squeeze(" ") }.flatten.compact.map(&:downcase).uniq + end + + end + + include TaggingExtensions + extend TaggingFinders +end diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index fbaa1dc5..7b2ad866 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -1,259 +1,259 @@ -require File.dirname(__FILE__) + '/../test_helper' -require File.dirname(__FILE__) + '/todo_container_controller_test_base' -require 'projects_controller' - -# Re-raise errors caught by the controller. -class ProjectsController; def rescue_action(e) raise e end; end - -class ProjectsControllerTest < TodoContainerControllerTestBase - fixtures :users, :todos, :preferences, :projects, :contexts - - def setup - perform_setup(Project, ProjectsController) - end - - def test_projects_list - login_as :admin_user - get :index - end - - def test_show_exposes_deferred_todos - p = projects(:timemachine) - login_as :admin_user - get :show, :id => p.to_param - assert_not_nil assigns['deferred'] - assert_equal 1, assigns['deferred'].size - - t = p.not_done_todos[0] - t.show_from = 1.days.from_now.utc - t.save! - - get :show, :id => p.to_param - assert_equal 2, assigns['deferred'].size - end - - def test_show_exposes_next_project_in_same_state - login_as :admin_user - get :show, :id => projects(:timemachine).to_param - assert_equal(projects(:moremoney), assigns['next_project']) - end - - def test_show_exposes_previous_project_in_same_state - login_as :admin_user - get :show, :id => projects(:moremoney).to_param - assert_equal(projects(:timemachine), assigns['previous_project']) - end - - def test_create_project_via_ajax_increments_number_of_projects - assert_ajax_create_increments_count 'My New Project' - end - - def test_create_project_with_ajax_success_rjs - ajax_create 'My New Project' - assert_rjs :insert_html, :bottom, "list-active-projects" - assert_rjs :sortable, 'list-active-projects', { :tag => 'div', :handle => 'handle', :complete => visual_effect(:highlight, 'list-active-projects'), :url => order_projects_path } - # not yet sure how to write the following properly... - assert_rjs :call, "Form.reset", "project-form" - assert_rjs :call, "Form.focusFirstElement", "project-form" - end - - def test_create_project_and_go_to_project_page - num_projects = Project.count - xhr :post, :create, { :project => {:name => 'Immediate Project Planning Required'}, :go_to_project => 1} - assert_js_redirected_to %r{/?projects/\d+} - assert_equal num_projects + 1, Project.count - end - - def test_create_with_comma_in_name_does_not_increment_number_of_projects - assert_ajax_create_does_not_increment_count 'foo,bar' - end - - def test_create_with_comma_in_name_fails_with_rjs - ajax_create 'foo,bar' - assert_rjs :show, 'status' - # Not working with Rails 2.0 upgrade - # assert_rjs :update, 'status', "

1 error prohibited this record from being saved

There were problems with the following fields:

    Name cannot contain the comma (',') character
" - end - - def test_todo_state_is_project_hidden_after_hiding_project - p = projects(:timemachine) - todos = p.todos.find_in_state(:all, :active) - login_as(:admin_user) - xhr :post, :update, :id => 1, "project"=>{"name"=>p.name, "description"=>p.description, "state"=>"hidden"} - todos.each do |t| - assert_equal :project_hidden, t.reload().current_state - end - assert p.reload().hidden? - end - - def test_not_done_counts_after_hiding_and_unhiding_project - p = projects(:timemachine) - todos = p.todos.find_in_state(:all, :active) - login_as(:admin_user) - xhr :post, :update, :id => 1, "project"=>{"name"=>p.name, "description"=>p.description, "state"=>"hidden"} - xhr :post, :update, :id => 1, "project"=>{"name"=>p.name, "description"=>p.description, "state"=>"active"} - todos.each do |t| - assert_equal :active, t.reload().current_state - end - assert p.reload().active? - end - - def test_rss_feed_content - login_as(:admin_user) - get :index, { :format => "rss" } - assert_equal 'application/rss+xml', @response.content_type - #puts @response.body - - assert_xml_select 'rss[version="2.0"]' do - assert_select 'channel' do - assert_select '>title', 'Tracks Projects' - assert_select '>description', "Lists all the projects for #{users(:admin_user).display_name}" - assert_select 'language', 'en-us' - assert_select 'ttl', '40' - end - assert_select 'item', 3 do - assert_select 'title', /.+/ - assert_select 'description' do - assert_select_encoded do - assert_select 'p', /^\d+ actions\. Project is (active|hidden|completed)\.$/ - end - end - %w(guid link).each do |node| - assert_select node, /http:\/\/test.host\/projects\/.+/ - end - assert_select 'pubDate', projects(:timemachine).updated_at.to_s(:rfc822) - end - end - end - - def test_rss_feed_not_accessible_to_anonymous_user_without_token - login_as nil - get :index, { :format => "rss" } - assert_response 401 - end - - def test_rss_feed_not_accessible_to_anonymous_user_with_invalid_token - login_as nil - get :index, { :format => "rss", :token => 'foo' } - assert_response 401 - end - - def test_rss_feed_accessible_to_anonymous_user_with_valid_token - login_as nil - get :index, { :format => "rss", :token => users(:admin_user).token } - assert_response :ok - end - - def test_atom_feed_content - login_as :admin_user - get :index, { :format => "atom" } - assert_equal 'application/atom+xml', @response.content_type - #puts @response.body - - assert_xml_select 'feed[xmlns="http://www.w3.org/2005/Atom"]' do - assert_select '>title', 'Tracks Projects' - assert_select '>subtitle', "Lists all the projects for #{users(:admin_user).display_name}" - assert_select 'entry', 3 do - assert_select 'title', /.+/ - assert_select 'content[type="html"]' do - assert_select_encoded do - assert_select 'p', /\d+ actions. Project is (active|hidden|completed)./ - end - end - assert_select 'published', /(#{Regexp.escape(projects(:timemachine).updated_at.xmlschema)}|#{Regexp.escape(projects(:moremoney).updated_at.xmlschema)})/ - end - end - end - - def test_atom_feed_not_accessible_to_anonymous_user_without_token - login_as nil - get :index, { :format => "atom" } - assert_response 401 - end - - def test_atom_feed_not_accessible_to_anonymous_user_with_invalid_token - login_as nil - get :index, { :format => "atom", :token => 'foo' } - assert_response 401 - end - - def test_atom_feed_accessible_to_anonymous_user_with_valid_token - login_as nil - get :index, { :format => "atom", :token => users(:admin_user).token } - assert_response :ok - end - - def test_text_feed_content - login_as :admin_user - get :index, { :format => "txt" } - assert_equal 'text/plain', @response.content_type - assert !(/ /.match(@response.body)) - #puts @response.body - end - - def test_text_feed_content_for_projects_with_no_actions - login_as :admin_user - p = projects(:timemachine) - p.todos.each { |t| t.destroy } - - get :index, { :format => "txt", :only_active_with_no_next_actions => true } - assert (/^\s*BUILD A WORKING TIME MACHINE\s+0 actions. Project is active.\s*$/.match(@response.body)) - assert !(/[1-9] actions/.match(@response.body)) - end - - def test_text_feed_not_accessible_to_anonymous_user_without_token - login_as nil - get :index, { :format => "txt" } - assert_response 401 - end - - def test_text_feed_not_accessible_to_anonymous_user_with_invalid_token - login_as nil - get :index, { :format => "txt", :token => 'foo' } - assert_response 401 - end - - def test_text_feed_accessible_to_anonymous_user_with_valid_token - login_as nil - get :index, { :format => "txt", :token => users(:admin_user).token } - assert_response :ok - end - - def test_actionize_sorts_active_projects_by_number_of_tasks - login_as :admin_user - u = users(:admin_user) - post :actionize, :state => "active", :format => 'js' - assert_equal 2, projects(:gardenclean).position - assert_equal 1, projects(:moremoney).position - assert_equal 3, projects(:timemachine).position - end - - def test_alphabetize_sorts_active_projects_alphabetically - login_as :admin_user - u = users(:admin_user) - post :alphabetize, :state => "active", :format => 'js' - assert_equal 1, projects(:timemachine).position - assert_equal 2, projects(:gardenclean).position - assert_equal 3, projects(:moremoney).position - end - - def test_alphabetize_assigns_state - login_as :admin_user - post :alphabetize, :state => "active", :format => 'js' - assert_equal "active", assigns['state'] - end - - def test_alphabetize_assigns_projects - login_as :admin_user - post :alphabetize, :state => "active", :format => 'js' - exposed_projects = assigns['projects'] - assert_equal 3, exposed_projects.length - assert_equal projects(:timemachine), exposed_projects[0] - assert_equal projects(:gardenclean), exposed_projects[1] - assert_equal projects(:moremoney), exposed_projects[2] - end - - def protect_against_forgery? - false - end -end +require File.dirname(__FILE__) + '/../test_helper' +require File.dirname(__FILE__) + '/todo_container_controller_test_base' +require 'projects_controller' + +# Re-raise errors caught by the controller. +class ProjectsController; def rescue_action(e) raise e end; end + +class ProjectsControllerTest < TodoContainerControllerTestBase + fixtures :users, :todos, :preferences, :projects, :contexts + + def setup + perform_setup(Project, ProjectsController) + end + + def test_projects_list + login_as :admin_user + get :index + end + + def test_show_exposes_deferred_todos + p = projects(:timemachine) + login_as :admin_user + get :show, :id => p.to_param + assert_not_nil assigns['deferred'] + assert_equal 1, assigns['deferred'].size + + t = p.not_done_todos[0] + t.show_from = 1.days.from_now.utc + t.save! + + get :show, :id => p.to_param + assert_equal 2, assigns['deferred'].size + end + + def test_show_exposes_next_project_in_same_state + login_as :admin_user + get :show, :id => projects(:timemachine).to_param + assert_equal(projects(:moremoney), assigns['next_project']) + end + + def test_show_exposes_previous_project_in_same_state + login_as :admin_user + get :show, :id => projects(:moremoney).to_param + assert_equal(projects(:timemachine), assigns['previous_project']) + end + + def test_create_project_via_ajax_increments_number_of_projects + assert_ajax_create_increments_count 'My New Project' + end + + def test_create_project_with_ajax_success_rjs + ajax_create 'My New Project' + assert_rjs :insert_html, :bottom, "list-active-projects" + assert_rjs :sortable, 'list-active-projects', { :tag => 'div', :handle => 'handle', :complete => visual_effect(:highlight, 'list-active-projects'), :url => order_projects_path } + # not yet sure how to write the following properly... + assert_rjs :call, "Form.reset", "project-form" + assert_rjs :call, "Form.focusFirstElement", "project-form" + end + + def test_create_project_and_go_to_project_page + num_projects = Project.count + xhr :post, :create, { :project => {:name => 'Immediate Project Planning Required'}, :go_to_project => 1} + assert_js_redirected_to %r{/?projects/\d+} + assert_equal num_projects + 1, Project.count + end + + def test_create_with_comma_in_name_does_not_increment_number_of_projects + assert_ajax_create_does_not_increment_count 'foo,bar' + end + + def test_create_with_comma_in_name_fails_with_rjs + ajax_create 'foo,bar' + assert_rjs :show, 'status' + # Not working with Rails 2.0 upgrade + # assert_rjs :update, 'status', "

1 error prohibited this record from being saved

There were problems with the following fields:

    Name cannot contain the comma (',') character
" + end + + def test_todo_state_is_project_hidden_after_hiding_project + p = projects(:timemachine) + todos = p.todos.find_in_state(:all, :active) + login_as(:admin_user) + xhr :post, :update, :id => 1, "project"=>{"name"=>p.name, "description"=>p.description, "state"=>"hidden"} + todos.each do |t| + assert_equal :project_hidden, t.reload().current_state + end + assert p.reload().hidden? + end + + def test_not_done_counts_after_hiding_and_unhiding_project + p = projects(:timemachine) + todos = p.todos.find_in_state(:all, :active) + login_as(:admin_user) + xhr :post, :update, :id => 1, "project"=>{"name"=>p.name, "description"=>p.description, "state"=>"hidden"} + xhr :post, :update, :id => 1, "project"=>{"name"=>p.name, "description"=>p.description, "state"=>"active"} + todos.each do |t| + assert_equal :active, t.reload().current_state + end + assert p.reload().active? + end + + def test_rss_feed_content + login_as(:admin_user) + get :index, { :format => "rss" } + assert_equal 'application/rss+xml', @response.content_type + #puts @response.body + + assert_xml_select 'rss[version="2.0"]' do + assert_select 'channel' do + assert_select '>title', 'Tracks Projects' + assert_select '>description', "Lists all the projects for #{users(:admin_user).display_name}" + assert_select 'language', 'en-us' + assert_select 'ttl', '40' + end + assert_select 'item', 3 do + assert_select 'title', /.+/ + assert_select 'description' do + assert_select_encoded do + assert_select 'p', /^\d+ actions\. Project is (active|hidden|completed)\.$/ + end + end + %w(guid link).each do |node| + assert_select node, /http:\/\/test.host\/projects\/.+/ + end + assert_select 'pubDate', projects(:timemachine).updated_at.to_s(:rfc822) + end + end + end + + def test_rss_feed_not_accessible_to_anonymous_user_without_token + login_as nil + get :index, { :format => "rss" } + assert_response 401 + end + + def test_rss_feed_not_accessible_to_anonymous_user_with_invalid_token + login_as nil + get :index, { :format => "rss", :token => 'foo' } + assert_response 401 + end + + def test_rss_feed_accessible_to_anonymous_user_with_valid_token + login_as nil + get :index, { :format => "rss", :token => users(:admin_user).token } + assert_response :ok + end + + def test_atom_feed_content + login_as :admin_user + get :index, { :format => "atom" } + assert_equal 'application/atom+xml', @response.content_type + #puts @response.body + + assert_xml_select 'feed[xmlns="http://www.w3.org/2005/Atom"]' do + assert_select '>title', 'Tracks Projects' + assert_select '>subtitle', "Lists all the projects for #{users(:admin_user).display_name}" + assert_select 'entry', 3 do + assert_select 'title', /.+/ + assert_select 'content[type="html"]' do + assert_select_encoded do + assert_select 'p', /\d+ actions. Project is (active|hidden|completed)./ + end + end + assert_select 'published', /(#{Regexp.escape(projects(:timemachine).updated_at.xmlschema)}|#{Regexp.escape(projects(:moremoney).updated_at.xmlschema)})/ + end + end + end + + def test_atom_feed_not_accessible_to_anonymous_user_without_token + login_as nil + get :index, { :format => "atom" } + assert_response 401 + end + + def test_atom_feed_not_accessible_to_anonymous_user_with_invalid_token + login_as nil + get :index, { :format => "atom", :token => 'foo' } + assert_response 401 + end + + def test_atom_feed_accessible_to_anonymous_user_with_valid_token + login_as nil + get :index, { :format => "atom", :token => users(:admin_user).token } + assert_response :ok + end + + def test_text_feed_content + login_as :admin_user + get :index, { :format => "txt" } + assert_equal 'text/plain', @response.content_type + assert !(/ /.match(@response.body)) + #puts @response.body + end + + def test_text_feed_content_for_projects_with_no_actions + login_as :admin_user + p = projects(:timemachine) + p.todos.each { |t| t.destroy } + + get :index, { :format => "txt", :only_active_with_no_next_actions => true } + assert (/^\s*BUILD A WORKING TIME MACHINE\s+0 actions. Project is active.\s*$/.match(@response.body)) + assert !(/[1-9] actions/.match(@response.body)) + end + + def test_text_feed_not_accessible_to_anonymous_user_without_token + login_as nil + get :index, { :format => "txt" } + assert_response 401 + end + + def test_text_feed_not_accessible_to_anonymous_user_with_invalid_token + login_as nil + get :index, { :format => "txt", :token => 'foo' } + assert_response 401 + end + + def test_text_feed_accessible_to_anonymous_user_with_valid_token + login_as nil + get :index, { :format => "txt", :token => users(:admin_user).token } + assert_response :ok + end + + def test_actionize_sorts_active_projects_by_number_of_tasks + login_as :admin_user + u = users(:admin_user) + post :actionize, :state => "active", :format => 'js' + assert_equal 1, projects(:gardenclean).position + assert_equal 2, projects(:moremoney).position + assert_equal 3, projects(:timemachine).position + end + + def test_alphabetize_sorts_active_projects_alphabetically + login_as :admin_user + u = users(:admin_user) + post :alphabetize, :state => "active", :format => 'js' + assert_equal 1, projects(:timemachine).position + assert_equal 2, projects(:gardenclean).position + assert_equal 3, projects(:moremoney).position + end + + def test_alphabetize_assigns_state + login_as :admin_user + post :alphabetize, :state => "active", :format => 'js' + assert_equal "active", assigns['state'] + end + + def test_alphabetize_assigns_projects + login_as :admin_user + post :alphabetize, :state => "active", :format => 'js' + exposed_projects = assigns['projects'] + assert_equal 3, exposed_projects.length + assert_equal projects(:timemachine), exposed_projects[0] + assert_equal projects(:gardenclean), exposed_projects[1] + assert_equal projects(:moremoney), exposed_projects[2] + end + + def protect_against_forgery? + false + end +end diff --git a/test/functional/todos_controller_test.rb b/test/functional/todos_controller_test.rb index 832aa764..624a702d 100644 --- a/test/functional/todos_controller_test.rb +++ b/test/functional/todos_controller_test.rb @@ -1,488 +1,488 @@ -require File.dirname(__FILE__) + '/../test_helper' -require 'todos_controller' - -# Re-raise errors caught by the controller. -class TodosController; def rescue_action(e) raise e end; end - -class TodosControllerTest < Test::Rails::TestCase - fixtures :users, :preferences, :projects, :contexts, :todos, :tags, :taggings, :recurring_todos - - def setup - @controller = TodosController.new - @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new - end - - def test_get_index_when_not_logged_in - get :index - assert_redirected_to :controller => 'login', :action => 'login' - end - - def test_not_done_counts - login_as(:admin_user) - get :index - assert_equal 2, assigns['project_not_done_counts'][projects(:timemachine).id] - assert_equal 3, assigns['context_not_done_counts'][contexts(:call).id] - assert_equal 1, assigns['context_not_done_counts'][contexts(:lab).id] - end - - def test_tag_is_retrieved_properly - login_as(:admin_user) - get :index - t = assigns['not_done_todos'].find{|t| t.id == 2} - assert_equal 1, t.tags.count - assert_equal 'foo', t.tags[0].name - assert !t.starred? - end - - def test_not_done_counts_after_hiding_project - p = Project.find(1) - p.hide! - p.save! - login_as(:admin_user) - get :index - assert_equal nil, assigns['project_not_done_counts'][projects(:timemachine).id] - assert_equal 2, assigns['context_not_done_counts'][contexts(:call).id] - assert_equal nil, assigns['context_not_done_counts'][contexts(:lab).id] - end - - def test_not_done_counts_after_hiding_and_unhiding_project - p = Project.find(1) - p.hide! - p.save! - p.activate! - p.save! - login_as(:admin_user) - get :index - assert_equal 2, assigns['project_not_done_counts'][projects(:timemachine).id] - assert_equal 3, assigns['context_not_done_counts'][contexts(:call).id] - assert_equal 1, assigns['context_not_done_counts'][contexts(:lab).id] - end - - def test_deferred_count_for_project_source_view - login_as(:admin_user) - xhr :post, :toggle_check, :id => 5, :_source_view => 'project' - assert_equal 1, assigns['deferred_count'] - xhr :post, :toggle_check, :id => 15, :_source_view => 'project' - assert_equal 0, assigns['deferred_count'] - end - - def test_destroy_todo - login_as(:admin_user) - xhr :post, :destroy, :id => 1, :_source_view => 'todo' - assert_rjs :page, "todo_1", :remove - # #assert_rjs :replace_html, "badge-count", '9' - end - - def test_create_todo - assert_difference Todo, :count do - login_as(:admin_user) - put :create, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"Build a working time machine", "todo"=>{"notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" - end - end - - def test_create_todo_via_xml - login_as(:admin_user) - assert_difference Todo, :count do - put :create, :format => "xml", "request" => { "context_name"=>"library", "project_name"=>"Build a working time machine", "todo"=>{"notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" } - assert_response 201 - end - end - - def test_fail_to_create_todo_via_xml - login_as(:admin_user) - # #try to create with no context, which is not valid - put :create, :format => "xml", "request" => { "project_name"=>"Build a working time machine", "todo"=>{"notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" } - assert_response 422 - assert_xml_select "errors" do - assert_xml_select "error", "Context can't be blank" - end - end - - def test_create_deferred_todo - original_todo_count = Todo.count - login_as(:admin_user) - put :create, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"Build a working time machine", "todo"=>{"notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2026", 'show_from' => '30/10/2026'}, "tag_list"=>"foo bar" - assert_equal original_todo_count + 1, Todo.count - end - - def test_update_todo_project - t = Todo.find(1) - login_as(:admin_user) - xhr :post, :update, :id => 1, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"Build a working time machine", "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" - t = Todo.find(1) - assert_equal 1, t.project_id - end - - def test_update_todo_project_to_none - t = Todo.find(1) - login_as(:admin_user) - xhr :post, :update, :id => 1, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"None", "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" - t = Todo.find(1) - assert_nil t.project_id - end - - def test_update_todo_to_deferred_is_reflected_in_badge_count - login_as(:admin_user) - get :index - assert_equal 11, assigns['count'] - xhr :post, :update, :id => 1, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"Make more money than Billy Gates", "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006", "show_from"=>"30/11/2030"}, "tag_list"=>"foo bar" - assert_equal 10, assigns['down_count'] - end - - def test_update_todo - t = Todo.find(1) - login_as(:admin_user) - xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo, bar" - t = Todo.find(1) - assert_equal "Call Warren Buffet to find out how much he makes per day", t.description - assert_equal "foo, bar", t.tag_list - expected = Date.new(2006,11,30) - actual = t.due.to_date - assert_equal expected, actual, "Expected #{expected.to_s(:db)}, was #{actual.to_s(:db)}" - end - - def test_update_todos_with_blank_project_name - t = Todo.find(1) - login_as(:admin_user) - xhr :post, :update, :id => 1, :_source_view => 'todo', :project_name => '', "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo, bar" - t.reload - assert t.project.nil? - end - - def test_update_todo_tags_to_none - t = Todo.find(1) - login_as(:admin_user) - xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"" - t = Todo.find(1) - assert_equal true, t.tag_list.empty? - end - - def test_update_todo_tags_with_whitespace_and_dots - t = Todo.find(1) - login_as(:admin_user) - taglist = " one , two,three ,four, 8.1.2, version1.5" - xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>taglist - t = Todo.find(1) - assert_equal "one, two, three, four, 8.1.2, version1.5", t.tag_list - end - - def test_find_tagged_with - login_as(:admin_user) - @user = User.find(@request.session['user_id']) - tag = Tag.find_by_name('foo').todos - @tagged = tag.find(:all, :conditions => ['taggings.user_id = ?', @user.id]).size - get :tag, :name => 'foo' - assert_response :success - assert_equal 3, @tagged - end - - def test_rss_feed - login_as(:admin_user) - get :index, { :format => "rss" } - assert_equal 'application/rss+xml', @response.content_type - # puts @response.body - - assert_xml_select 'rss[version="2.0"]' do - assert_select 'channel' do - assert_select '>title', 'Actions' - assert_select '>description', "Actions for #{users(:admin_user).display_name}" - assert_select 'language', 'en-us' - assert_select 'ttl', '40' - assert_select 'item', 11 do - assert_select 'title', /.+/ - assert_select 'description', /.*/ - assert_select 'link', %r{http://test.host/contexts/.+} - assert_select 'guid', %r{http://test.host/todos/.+} - assert_select 'pubDate', todos(:book).updated_at.to_s(:rfc822) - end - end - end - end - - def test_rss_feed_with_limit - login_as(:admin_user) - get :index, { :format => "rss", :limit => '5' } - - assert_xml_select 'rss[version="2.0"]' do - assert_select 'channel' do - assert_select '>title', 'Actions' - assert_select '>description', "Actions for #{users(:admin_user).display_name}" - assert_select 'item', 5 do - assert_select 'title', /.+/ - assert_select 'description', /.*/ - end - end - end - end - - def test_rss_feed_not_accessible_to_anonymous_user_without_token - login_as nil - get :index, { :format => "rss" } - assert_response 401 - end - - def test_rss_feed_not_accessible_to_anonymous_user_with_invalid_token - login_as nil - get :index, { :format => "rss", :token => 'foo' } - assert_response 401 - end - - def test_rss_feed_accessible_to_anonymous_user_with_valid_token - login_as nil - get :index, { :format => "rss", :token => users(:admin_user).token } - assert_response :ok - end - - def test_atom_feed_content - login_as :admin_user - get :index, { :format => "atom" } - assert_equal 'application/atom+xml', @response.content_type - # #puts @response.body - - assert_xml_select 'feed[xmlns="http://www.w3.org/2005/Atom"]' do - assert_xml_select '>title', 'Actions' - assert_xml_select '>subtitle', "Actions for #{users(:admin_user).display_name}" - assert_xml_select 'entry', 11 do - assert_xml_select 'title', /.+/ - assert_xml_select 'content[type="html"]', /.*/ - assert_xml_select 'published', /(#{Regexp.escape(todos(:book).updated_at.xmlschema)}|#{Regexp.escape(projects(:moremoney).updated_at.xmlschema)})/ - end - end - end - - def test_atom_feed_not_accessible_to_anonymous_user_without_token - login_as nil - get :index, { :format => "atom" } - assert_response 401 - end - - def test_atom_feed_not_accessible_to_anonymous_user_with_invalid_token - login_as nil - get :index, { :format => "atom", :token => 'foo' } - assert_response 401 - end - - def test_atom_feed_accessible_to_anonymous_user_with_valid_token - login_as nil - get :index, { :format => "atom", :token => users(:admin_user).token } - assert_response :ok - end - - def test_text_feed_content - login_as(:admin_user) - get :index, { :format => "txt" } - assert_equal 'text/plain', @response.content_type - assert !(/ /.match(@response.body)) - # #puts @response.body - end - - def test_text_feed_not_accessible_to_anonymous_user_without_token - login_as nil - get :index, { :format => "txt" } - assert_response 401 - end - - def test_text_feed_not_accessible_to_anonymous_user_with_invalid_token - login_as nil - get :index, { :format => "txt", :token => 'foo' } - assert_response 401 - end - - def test_text_feed_accessible_to_anonymous_user_with_valid_token - login_as nil - get :index, { :format => "txt", :token => users(:admin_user).token } - assert_response :ok - end - - def test_ical_feed_content - login_as :admin_user - get :index, { :format => "ics" } - assert_equal 'text/calendar', @response.content_type - assert !(/ /.match(@response.body)) - # #puts @response.body - end - - def test_mobile_index_uses_text_html_content_type - login_as(:admin_user) - get :index, { :format => "m" } - assert_equal 'text/html', @response.content_type - end - - def test_mobile_index_assigns_down_count - login_as(:admin_user) - get :index, { :format => "m" } - assert_equal 11, assigns['down_count'] - end - - def test_mobile_create_action_creates_a_new_todo - login_as(:admin_user) - post :create, {"format"=>"m", "todo"=>{"context_id"=>"2", - "due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2", - "show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"", - "project_id"=>"1", - "notes"=>"test notes", "description"=>"test_mobile_create_action", "state"=>"0"}} - t = Todo.find_by_description("test_mobile_create_action") - assert_not_nil t - assert_equal 2, t.context_id - assert_equal 1, t.project_id - assert t.active? - assert_equal 'test notes', t.notes - assert_nil t.show_from - assert_equal Date.new(2007,1,2), t.due.to_date - end - - def test_mobile_create_action_redirects_to_mobile_home_page_when_successful - login_as(:admin_user) - post :create, {"format"=>"m", "todo"=>{"context_id"=>"2", - "due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2", - "show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"", - "project_id"=>"1", - "notes"=>"test notes", "description"=>"test_mobile_create_action", "state"=>"0"}} - assert_redirected_to '/m' - end - - def test_mobile_create_action_renders_new_template_when_save_fails - login_as(:admin_user) - post :create, {"format"=>"m", "todo"=>{"context_id"=>"2", - "due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2", - "show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"", - "project_id"=>"1", - "notes"=>"test notes", "state"=>"0"}, "tag_list"=>"test, test2"} - assert_template 'todos/new' - end - - def test_index_html_assigns_default_project_name_map - login_as(:admin_user) - get :index, {"format"=>"html"} - assert_equal '"{\\"Build a working time machine\\": \\"lab\\"}"', assigns(:default_project_context_name_map) - end - - def test_toggle_check_on_recurring_todo - login_as(:admin_user) - - # link todo_1 and recurring_todo_1 - recurring_todo_1 = RecurringTodo.find(1) - todo_1 = Todo.find_by_recurring_todo_id(1) - - # mark todo_1 as complete by toggle_check - xhr :post, :toggle_check, :id => todo_1.id, :_source_view => 'todo' - todo_1.reload - assert todo_1.completed? - - # check that there is only one active todo belonging to recurring_todo - count = Todo.count(:all, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'}) - assert_equal 1, count - - # check there is a new todo linked to the recurring pattern - next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'}) - assert_equal "Call Bill Gates every day", next_todo.description - # check that the new todo is not the same as todo_1 - assert_not_equal todo_1.id, next_todo.id - - # change recurrence pattern to monthly and set show_from 2 days before due - # date this forces the next todo to be put in the tickler - recurring_todo_1.show_from_delta = 2 - recurring_todo_1.recurring_period = 'monthly' - recurring_todo_1.recurrence_selector = 0 - recurring_todo_1.every_other1 = 1 - recurring_todo_1.every_other2 = 2 - recurring_todo_1.every_other3 = 5 - recurring_todo_1.save - - # mark next_todo as complete by toggle_check - xhr :post, :toggle_check, :id => next_todo.id, :_source_view => 'todo' - next_todo.reload - assert next_todo.completed? - - # check that there are three todos belonging to recurring_todo: two - # completed and one deferred - count = Todo.count(:all, :conditions => {:recurring_todo_id => recurring_todo_1.id}) - assert_equal 3, count - - # check there is a new todo linked to the recurring pattern in the tickler - next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'deferred'}) - assert !next_todo.nil? - assert_equal "Call Bill Gates every day", next_todo.description - # check that the todo is in the tickler - assert !next_todo.show_from.nil? - end - - def test_toggle_check_on_rec_todo_show_from_today - login_as(:admin_user) - - # link todo_1 and recurring_todo_1 - recurring_todo_1 = RecurringTodo.find(1) - todo_1 = Todo.find_by_recurring_todo_id(1) - today = Time.now.utc.at_midnight - - # change recurrence pattern to monthly and set show_from to today - recurring_todo_1.target = 'show_from_date' - recurring_todo_1.recurring_period = 'monthly' - recurring_todo_1.recurrence_selector = 0 - recurring_todo_1.every_other1 = today.day - recurring_todo_1.every_other2 = 1 - recurring_todo_1.save - - # mark todo_1 as complete by toggle_check, this gets rid of todo_1 that was - # not correctly created from the adjusted recurring pattern we defined - # above. - xhr :post, :toggle_check, :id => todo_1.id, :_source_view => 'todo' - todo_1.reload - assert todo_1.completed? - - # locate the new todo. This todo is created from the adjusted recurring - # pattern defined in this test - new_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'}) - assert !new_todo.nil? - - # mark new_todo as complete by toggle_check - xhr :post, :toggle_check, :id => new_todo.id, :_source_view => 'todo' - new_todo.reload - assert todo_1.completed? - - # locate the new todo in tickler - new_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'deferred'}) - assert !new_todo.nil? - - assert_equal "Call Bill Gates every day", new_todo.description - # check that the new todo is not the same as todo_1 - assert_not_equal todo_1.id, new_todo.id - - # check that the new_todo is in the tickler to show next month - assert !new_todo.show_from.nil? - assert_equal Time.utc(today.year, today.month, today.day)+1.month, new_todo.show_from - end - - def test_check_for_next_todo - login_as :admin_user - - recurring_todo_1 = RecurringTodo.find(5) - @todo = Todo.find_by_recurring_todo_id(1) - assert @todo.from_recurring_todo? - # rewire @todo to yearly recurring todo - @todo.recurring_todo_id = 5 - - # make todo due tomorrow and change recurring date also to tomorrow - @todo.due = Time.zone.now + 1.day - @todo.save - recurring_todo_1.every_other1 = @todo.due.day - recurring_todo_1.every_other2 = @todo.due.month - recurring_todo_1.save - - # mark todo complete - xhr :post, :toggle_check, :id => @todo.id, :_source_view => 'todo' - @todo.reload - assert @todo.completed? - - # check that there is no active todo - next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'}) - assert next_todo.nil? - - # check for new deferred todo - next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'deferred'}) - assert !next_todo.nil? - # check that the due date of the new todo is later than tomorrow - assert next_todo.due > @todo.due - end - -end +require File.dirname(__FILE__) + '/../test_helper' +require 'todos_controller' + +# Re-raise errors caught by the controller. +class TodosController; def rescue_action(e) raise e end; end + +class TodosControllerTest < Test::Rails::TestCase + fixtures :users, :preferences, :projects, :contexts, :todos, :tags, :taggings, :recurring_todos + + def setup + @controller = TodosController.new + @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new + end + + def test_get_index_when_not_logged_in + get :index + assert_redirected_to :controller => 'login', :action => 'login' + end + + def test_not_done_counts + login_as(:admin_user) + get :index + assert_equal 2, assigns['project_not_done_counts'][projects(:timemachine).id] + assert_equal 3, assigns['context_not_done_counts'][contexts(:call).id] + assert_equal 1, assigns['context_not_done_counts'][contexts(:lab).id] + end + + def test_tag_is_retrieved_properly + login_as(:admin_user) + get :index + t = assigns['not_done_todos'].find{|t| t.id == 2} + assert_equal 1, t.tags.count + assert_equal 'foo', t.tags[0].name + assert !t.starred? + end + + def test_not_done_counts_after_hiding_project + p = Project.find(1) + p.hide! + p.save! + login_as(:admin_user) + get :index + assert_equal nil, assigns['project_not_done_counts'][projects(:timemachine).id] + assert_equal 2, assigns['context_not_done_counts'][contexts(:call).id] + assert_equal nil, assigns['context_not_done_counts'][contexts(:lab).id] + end + + def test_not_done_counts_after_hiding_and_unhiding_project + p = Project.find(1) + p.hide! + p.save! + p.activate! + p.save! + login_as(:admin_user) + get :index + assert_equal 2, assigns['project_not_done_counts'][projects(:timemachine).id] + assert_equal 3, assigns['context_not_done_counts'][contexts(:call).id] + assert_equal 1, assigns['context_not_done_counts'][contexts(:lab).id] + end + + def test_deferred_count_for_project_source_view + login_as(:admin_user) + xhr :post, :toggle_check, :id => 5, :_source_view => 'project' + assert_equal 1, assigns['deferred_count'] + xhr :post, :toggle_check, :id => 15, :_source_view => 'project' + assert_equal 0, assigns['deferred_count'] + end + + def test_destroy_todo + login_as(:admin_user) + xhr :post, :destroy, :id => 1, :_source_view => 'todo' + assert_rjs :page, "todo_1", :remove + # #assert_rjs :replace_html, "badge-count", '9' + end + + def test_create_todo + assert_difference Todo, :count do + login_as(:admin_user) + put :create, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"Build a working time machine", "todo"=>{"notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" + end + end + + def test_create_todo_via_xml + login_as(:admin_user) + assert_difference Todo, :count do + put :create, :format => "xml", "request" => { "context_name"=>"library", "project_name"=>"Build a working time machine", "todo"=>{"notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" } + assert_response 201 + end + end + + def test_fail_to_create_todo_via_xml + login_as(:admin_user) + # #try to create with no context, which is not valid + put :create, :format => "xml", "request" => { "project_name"=>"Build a working time machine", "todo"=>{"notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" } + assert_response 422 + assert_xml_select "errors" do + assert_xml_select "error", "Context can't be blank" + end + end + + def test_create_deferred_todo + original_todo_count = Todo.count + login_as(:admin_user) + put :create, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"Build a working time machine", "todo"=>{"notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2026", 'show_from' => '30/10/2026'}, "tag_list"=>"foo bar" + assert_equal original_todo_count + 1, Todo.count + end + + def test_update_todo_project + t = Todo.find(1) + login_as(:admin_user) + xhr :post, :update, :id => 1, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"Build a working time machine", "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" + t = Todo.find(1) + assert_equal 1, t.project_id + end + + def test_update_todo_project_to_none + t = Todo.find(1) + login_as(:admin_user) + xhr :post, :update, :id => 1, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"None", "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo bar" + t = Todo.find(1) + assert_nil t.project_id + end + + def test_update_todo_to_deferred_is_reflected_in_badge_count + login_as(:admin_user) + get :index + assert_equal 11, assigns['count'] + xhr :post, :update, :id => 1, :_source_view => 'todo', "context_name"=>"library", "project_name"=>"Make more money than Billy Gates", "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006", "show_from"=>"30/11/2030"}, "tag_list"=>"foo bar" + assert_equal 10, assigns['down_count'] + end + + def test_update_todo + t = Todo.find(1) + login_as(:admin_user) + xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo, bar" + t = Todo.find(1) + assert_equal "Call Warren Buffet to find out how much he makes per day", t.description + assert_equal "bar, foo", t.tag_list + expected = Date.new(2006,11,30) + actual = t.due.to_date + assert_equal expected, actual, "Expected #{expected.to_s(:db)}, was #{actual.to_s(:db)}" + end + + def test_update_todos_with_blank_project_name + t = Todo.find(1) + login_as(:admin_user) + xhr :post, :update, :id => 1, :_source_view => 'todo', :project_name => '', "todo"=>{"id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"foo, bar" + t.reload + assert t.project.nil? + end + + def test_update_todo_tags_to_none + t = Todo.find(1) + login_as(:admin_user) + xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>"" + t = Todo.find(1) + assert_equal true, t.tag_list.empty? + end + + def test_update_todo_tags_with_whitespace_and_dots + t = Todo.find(1) + login_as(:admin_user) + taglist = " one , two,three ,four, 8.1.2, version1.5" + xhr :post, :update, :id => 1, :_source_view => 'todo', "todo"=>{"context_id"=>"1", "project_id"=>"2", "id"=>"1", "notes"=>"", "description"=>"Call Warren Buffet to find out how much he makes per day", "due"=>"30/11/2006"}, "tag_list"=>taglist + t = Todo.find(1) + assert_equal "8.1.2, four, one, three, two, version1.5", t.tag_list + end + + def test_find_tagged_with + login_as(:admin_user) + @user = User.find(@request.session['user_id']) + tag = Tag.find_by_name('foo').todos + @tagged = tag.find(:all, :conditions => ['taggings.user_id = ?', @user.id]).size + get :tag, :name => 'foo' + assert_response :success + assert_equal 3, @tagged + end + + def test_rss_feed + login_as(:admin_user) + get :index, { :format => "rss" } + assert_equal 'application/rss+xml', @response.content_type + # puts @response.body + + assert_xml_select 'rss[version="2.0"]' do + assert_select 'channel' do + assert_select '>title', 'Actions' + assert_select '>description', "Actions for #{users(:admin_user).display_name}" + assert_select 'language', 'en-us' + assert_select 'ttl', '40' + assert_select 'item', 11 do + assert_select 'title', /.+/ + assert_select 'description', /.*/ + assert_select 'link', %r{http://test.host/contexts/.+} + assert_select 'guid', %r{http://test.host/todos/.+} + assert_select 'pubDate', todos(:book).updated_at.to_s(:rfc822) + end + end + end + end + + def test_rss_feed_with_limit + login_as(:admin_user) + get :index, { :format => "rss", :limit => '5' } + + assert_xml_select 'rss[version="2.0"]' do + assert_select 'channel' do + assert_select '>title', 'Actions' + assert_select '>description', "Actions for #{users(:admin_user).display_name}" + assert_select 'item', 5 do + assert_select 'title', /.+/ + assert_select 'description', /.*/ + end + end + end + end + + def test_rss_feed_not_accessible_to_anonymous_user_without_token + login_as nil + get :index, { :format => "rss" } + assert_response 401 + end + + def test_rss_feed_not_accessible_to_anonymous_user_with_invalid_token + login_as nil + get :index, { :format => "rss", :token => 'foo' } + assert_response 401 + end + + def test_rss_feed_accessible_to_anonymous_user_with_valid_token + login_as nil + get :index, { :format => "rss", :token => users(:admin_user).token } + assert_response :ok + end + + def test_atom_feed_content + login_as :admin_user + get :index, { :format => "atom" } + assert_equal 'application/atom+xml', @response.content_type + # #puts @response.body + + assert_xml_select 'feed[xmlns="http://www.w3.org/2005/Atom"]' do + assert_xml_select '>title', 'Actions' + assert_xml_select '>subtitle', "Actions for #{users(:admin_user).display_name}" + assert_xml_select 'entry', 11 do + assert_xml_select 'title', /.+/ + assert_xml_select 'content[type="html"]', /.*/ + assert_xml_select 'published', /(#{Regexp.escape(todos(:book).updated_at.xmlschema)}|#{Regexp.escape(projects(:moremoney).updated_at.xmlschema)})/ + end + end + end + + def test_atom_feed_not_accessible_to_anonymous_user_without_token + login_as nil + get :index, { :format => "atom" } + assert_response 401 + end + + def test_atom_feed_not_accessible_to_anonymous_user_with_invalid_token + login_as nil + get :index, { :format => "atom", :token => 'foo' } + assert_response 401 + end + + def test_atom_feed_accessible_to_anonymous_user_with_valid_token + login_as nil + get :index, { :format => "atom", :token => users(:admin_user).token } + assert_response :ok + end + + def test_text_feed_content + login_as(:admin_user) + get :index, { :format => "txt" } + assert_equal 'text/plain', @response.content_type + assert !(/ /.match(@response.body)) + # #puts @response.body + end + + def test_text_feed_not_accessible_to_anonymous_user_without_token + login_as nil + get :index, { :format => "txt" } + assert_response 401 + end + + def test_text_feed_not_accessible_to_anonymous_user_with_invalid_token + login_as nil + get :index, { :format => "txt", :token => 'foo' } + assert_response 401 + end + + def test_text_feed_accessible_to_anonymous_user_with_valid_token + login_as nil + get :index, { :format => "txt", :token => users(:admin_user).token } + assert_response :ok + end + + def test_ical_feed_content + login_as :admin_user + get :index, { :format => "ics" } + assert_equal 'text/calendar', @response.content_type + assert !(/ /.match(@response.body)) + # #puts @response.body + end + + def test_mobile_index_uses_text_html_content_type + login_as(:admin_user) + get :index, { :format => "m" } + assert_equal 'text/html', @response.content_type + end + + def test_mobile_index_assigns_down_count + login_as(:admin_user) + get :index, { :format => "m" } + assert_equal 11, assigns['down_count'] + end + + def test_mobile_create_action_creates_a_new_todo + login_as(:admin_user) + post :create, {"format"=>"m", "todo"=>{"context_id"=>"2", + "due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2", + "show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"", + "project_id"=>"1", + "notes"=>"test notes", "description"=>"test_mobile_create_action", "state"=>"0"}} + t = Todo.find_by_description("test_mobile_create_action") + assert_not_nil t + assert_equal 2, t.context_id + assert_equal 1, t.project_id + assert t.active? + assert_equal 'test notes', t.notes + assert_nil t.show_from + assert_equal Date.new(2007,1,2), t.due.to_date + end + + def test_mobile_create_action_redirects_to_mobile_home_page_when_successful + login_as(:admin_user) + post :create, {"format"=>"m", "todo"=>{"context_id"=>"2", + "due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2", + "show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"", + "project_id"=>"1", + "notes"=>"test notes", "description"=>"test_mobile_create_action", "state"=>"0"}} + assert_redirected_to '/m' + end + + def test_mobile_create_action_renders_new_template_when_save_fails + login_as(:admin_user) + post :create, {"format"=>"m", "todo"=>{"context_id"=>"2", + "due(1i)"=>"2007", "due(2i)"=>"1", "due(3i)"=>"2", + "show_from(1i)"=>"", "show_from(2i)"=>"", "show_from(3i)"=>"", + "project_id"=>"1", + "notes"=>"test notes", "state"=>"0"}, "tag_list"=>"test, test2"} + assert_template 'todos/new' + end + + def test_index_html_assigns_default_project_name_map + login_as(:admin_user) + get :index, {"format"=>"html"} + assert_equal '"{\\"Build a working time machine\\": \\"lab\\"}"', assigns(:default_project_context_name_map) + end + + def test_toggle_check_on_recurring_todo + login_as(:admin_user) + + # link todo_1 and recurring_todo_1 + recurring_todo_1 = RecurringTodo.find(1) + todo_1 = Todo.find_by_recurring_todo_id(1) + + # mark todo_1 as complete by toggle_check + xhr :post, :toggle_check, :id => todo_1.id, :_source_view => 'todo' + todo_1.reload + assert todo_1.completed? + + # check that there is only one active todo belonging to recurring_todo + count = Todo.count(:all, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'}) + assert_equal 1, count + + # check there is a new todo linked to the recurring pattern + next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'}) + assert_equal "Call Bill Gates every day", next_todo.description + # check that the new todo is not the same as todo_1 + assert_not_equal todo_1.id, next_todo.id + + # change recurrence pattern to monthly and set show_from 2 days before due + # date this forces the next todo to be put in the tickler + recurring_todo_1.show_from_delta = 2 + recurring_todo_1.recurring_period = 'monthly' + recurring_todo_1.recurrence_selector = 0 + recurring_todo_1.every_other1 = 1 + recurring_todo_1.every_other2 = 2 + recurring_todo_1.every_other3 = 5 + recurring_todo_1.save + + # mark next_todo as complete by toggle_check + xhr :post, :toggle_check, :id => next_todo.id, :_source_view => 'todo' + next_todo.reload + assert next_todo.completed? + + # check that there are three todos belonging to recurring_todo: two + # completed and one deferred + count = Todo.count(:all, :conditions => {:recurring_todo_id => recurring_todo_1.id}) + assert_equal 3, count + + # check there is a new todo linked to the recurring pattern in the tickler + next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'deferred'}) + assert !next_todo.nil? + assert_equal "Call Bill Gates every day", next_todo.description + # check that the todo is in the tickler + assert !next_todo.show_from.nil? + end + + def test_toggle_check_on_rec_todo_show_from_today + login_as(:admin_user) + + # link todo_1 and recurring_todo_1 + recurring_todo_1 = RecurringTodo.find(1) + todo_1 = Todo.find_by_recurring_todo_id(1) + today = Time.now.utc.at_midnight + + # change recurrence pattern to monthly and set show_from to today + recurring_todo_1.target = 'show_from_date' + recurring_todo_1.recurring_period = 'monthly' + recurring_todo_1.recurrence_selector = 0 + recurring_todo_1.every_other1 = today.day + recurring_todo_1.every_other2 = 1 + recurring_todo_1.save + + # mark todo_1 as complete by toggle_check, this gets rid of todo_1 that was + # not correctly created from the adjusted recurring pattern we defined + # above. + xhr :post, :toggle_check, :id => todo_1.id, :_source_view => 'todo' + todo_1.reload + assert todo_1.completed? + + # locate the new todo. This todo is created from the adjusted recurring + # pattern defined in this test + new_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'}) + assert !new_todo.nil? + + # mark new_todo as complete by toggle_check + xhr :post, :toggle_check, :id => new_todo.id, :_source_view => 'todo' + new_todo.reload + assert todo_1.completed? + + # locate the new todo in tickler + new_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'deferred'}) + assert !new_todo.nil? + + assert_equal "Call Bill Gates every day", new_todo.description + # check that the new todo is not the same as todo_1 + assert_not_equal todo_1.id, new_todo.id + + # check that the new_todo is in the tickler to show next month + assert !new_todo.show_from.nil? + assert_equal Time.utc(today.year, today.month, today.day)+1.month, new_todo.show_from + end + + def test_check_for_next_todo + login_as :admin_user + + recurring_todo_1 = RecurringTodo.find(5) + @todo = Todo.find_by_recurring_todo_id(1) + assert @todo.from_recurring_todo? + # rewire @todo to yearly recurring todo + @todo.recurring_todo_id = 5 + + # make todo due tomorrow and change recurring date also to tomorrow + @todo.due = Time.zone.now + 1.day + @todo.save + recurring_todo_1.every_other1 = @todo.due.day + recurring_todo_1.every_other2 = @todo.due.month + recurring_todo_1.save + + # mark todo complete + xhr :post, :toggle_check, :id => @todo.id, :_source_view => 'todo' + @todo.reload + assert @todo.completed? + + # check that there is no active todo + next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'active'}) + assert next_todo.nil? + + # check for new deferred todo + next_todo = Todo.find(:first, :conditions => {:recurring_todo_id => recurring_todo_1.id, :state => 'deferred'}) + assert !next_todo.nil? + # check that the due date of the new todo is later than tomorrow + assert next_todo.due > @todo.due + end + +end diff --git a/test/selenium/mobile/create_new_action.rsel b/test/selenium/mobile/create_new_action.rsel index 8756d3ea..93ec8505 100644 --- a/test/selenium/mobile/create_new_action.rsel +++ b/test/selenium/mobile/create_new_action.rsel @@ -1,18 +1,18 @@ -setup :fixtures => :all -login :as => 'admin' - -open '/m' -wait_for_text 'css=h1 span.count', '11' - -click_and_wait "link=0-Add new action" - -type "todo_notes", "test notes" -type "todo_description", "test name" -select "todo_context_id", "label=call" -select "todo_project_id", "label=Make more money than Billy Gates" -select "todo_due_3i", "label=1" -select "todo_due_2i", "label=January" -select "todo_due_1i", "label=2009" -click_and_wait "//input[@value='Create']" - -wait_for_text 'css=h1 span.count', '12' +setup :fixtures => :all +login :as => 'admin' + +open '/m' +wait_for_text 'css=h1 span.count', '11' + +click_and_wait "link=0-New action" + +type "todo_notes", "test notes" +type "todo_description", "test name" +select "todo_context_id", "label=call" +select "todo_project_id", "label=Make more money than Billy Gates" +select "todo_due_3i", "label=1" +select "todo_due_2i", "label=January" +select "todo_due_1i", "label=2009" +click_and_wait "//input[@value='Create']" + +wait_for_text 'css=h1 span.count', '12' 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/tag_test.rb b/test/unit/tag_test.rb index 031d8bb7..c0aac834 100644 --- a/test/unit/tag_test.rb +++ b/test/unit/tag_test.rb @@ -4,7 +4,26 @@ class TagTest < Test::Rails::TestCase fixtures :tags # Replace this with your real tests. - def test_truth - assert true + def test_find_or_create_with_single_word + tag = Tag.find_or_create_by_name("test") + assert !tag.new_record? + end + + def test_find_or_create_with_space + tag = Tag.find_or_create_by_name("test test") + assert !tag.new_record? + end + + def test_find_or_create_with_dot + tag = Tag.find_or_create_by_name("a.b.c") + assert !tag.new_record? + end + + def test_find_or_create_with_number_as_string + tag = Tag.find_or_create_by_name("12343") + assert !tag.new_record? + + tag = Tag.find_or_create_by_name("8.1.2") + assert !tag.new_record? 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 diff --git a/vendor/gems/highline-1.5.0/.specification b/vendor/gems/highline-1.5.0/.specification new file mode 100644 index 00000000..2ebbb33c --- /dev/null +++ b/vendor/gems/highline-1.5.0/.specification @@ -0,0 +1,87 @@ +--- !ruby/object:Gem::Specification +name: highline +version: !ruby/object:Gem::Version + version: 1.5.0 +platform: ruby +authors: +- James Edward Gray II +autorequire: +bindir: bin +cert_chain: [] + +date: 2008-11-05 00:00:00 +01:00 +default_executable: +dependencies: [] + +description: A high-level IO library that provides validation, type conversion, and more for command-line interfaces. HighLine also includes a complete menu system that can crank out anything from simple list selection to complete shells with just minutes of work. +email: james@grayproductions.net +executables: [] + +extensions: [] + +extra_rdoc_files: +- README +- INSTALL +- TODO +- CHANGELOG +- LICENSE +files: +- examples/ansi_colors.rb +- examples/asking_for_arrays.rb +- examples/basic_usage.rb +- examples/color_scheme.rb +- examples/menus.rb +- examples/overwrite.rb +- examples/page_and_wrap.rb +- examples/password.rb +- examples/trapping_eof.rb +- examples/using_readline.rb +- lib/highline/color_scheme.rb +- lib/highline/import.rb +- lib/highline/menu.rb +- lib/highline/question.rb +- lib/highline/system_extensions.rb +- lib/highline.rb +- test/tc_color_scheme.rb +- test/tc_highline.rb +- test/tc_import.rb +- test/tc_menu.rb +- test/ts_all.rb +- Rakefile +- setup.rb +- README +- INSTALL +- TODO +- CHANGELOG +- LICENSE +has_rdoc: true +homepage: http://highline.rubyforge.org +post_install_message: +rdoc_options: +- --title +- HighLine Documentation +- --main +- README +require_paths: +- lib +required_ruby_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +required_rubygems_version: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: "0" + version: +requirements: [] + +rubyforge_project: highline +rubygems_version: 1.3.1 +signing_key: +specification_version: 2 +summary: HighLine is a high-level command-line IO library. +test_files: +- test/ts_all.rb diff --git a/vendor/gems/ruby-openid-2.1.2/.specification b/vendor/gems/ruby-openid-2.1.2/.specification index e6c9612d..6a1a03db 100644 --- a/vendor/gems/ruby-openid-2.1.2/.specification +++ b/vendor/gems/ruby-openid-2.1.2/.specification @@ -8,7 +8,7 @@ authors: autorequire: openid bindir: bin cert_chain: -date: 2008-06-27 00:00:00 -04:00 +date: 2008-06-27 05:00:00 +01:00 default_executable: dependencies: [] @@ -265,6 +265,7 @@ rdoc_options: - --main - README require_paths: +- bin - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: @@ -281,7 +282,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement requirements: [] rubyforge_project: -rubygems_version: 1.2.0 +rubygems_version: 1.0.1 signing_key: specification_version: 1 summary: A library for consuming and serving OpenID identities. diff --git a/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tagging_extensions.rb b/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tagging_extensions.rb index 280aa3ce..9795abd5 100644 --- a/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tagging_extensions.rb +++ b/vendor/plugins/has_many_polymorphs/generators/tagging/templates/tagging_extensions.rb @@ -120,7 +120,7 @@ class ActiveRecord::Base #:nodoc: sql = "SELECT #{(scope && scope[:select]) || options[:select]} " sql << "FROM #{(scope && scope[:from]) || options[:from]} " - add_joins!(sql, options, scope) + add_joins!(sql, options[:joins], scope) sql << "WHERE #{table_name}.#{primary_key} = taggings.taggable_id " sql << "AND taggings.taggable_type = '#{ActiveRecord::Base.send(:class_name_of_active_record_descendant, self).to_s}' " diff --git a/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/class_methods.rb b/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/class_methods.rb index 86c7be90..394985cf 100644 --- a/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/class_methods.rb +++ b/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/class_methods.rb @@ -360,10 +360,14 @@ Be aware, however, that NULL != 'Spot' returns false due to SQ begin table = plural._as_class.table_name rescue NameError => e - raise PolymorphicError, "Could not find a valid class for #{plural.inspect}. If it's namespaced, be sure to specify it as :\"module/#{plural}\" instead." + raise PolymorphicError, "Could not find a valid class for #{plural.inspect} (tried #{plural.to_s._classify}). If it's namespaced, be sure to specify it as :\"module/#{plural}\" instead." end - plural._as_class.columns.map(&:name).each_with_index do |field, f_index| - aliases["#{table}.#{field}"] = "t#{t_index}_r#{f_index}" + begin + plural._as_class.columns.map(&:name).each_with_index do |field, f_index| + aliases["#{table}.#{field}"] = "t#{t_index}_r#{f_index}" + end + rescue ActiveRecord::StatementInvalid => e + _logger_warn "Looks like your table doesn't exist for #{plural.to_s._classify}.\nError #{e}\nSkipping..." end end end diff --git a/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/reflection.rb b/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/reflection.rb index 67c69d5a..0091397d 100644 --- a/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/reflection.rb +++ b/vendor/plugins/has_many_polymorphs/lib/has_many_polymorphs/reflection.rb @@ -33,7 +33,7 @@ Inherits from ActiveRecord::Reflection::AssociationReflection. =end - class PolymorphicReflection < AssociationReflection + class PolymorphicReflection < ThroughReflection # Stub out the validity check. Has_many_polymorphs checks validity on macro creation, not on reflection. def check_validity! # nothing diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/public/dispatch.cgi b/vendor/plugins/has_many_polymorphs/test/integration/app/public/dispatch.cgi old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/public/dispatch.fcgi b/vendor/plugins/has_many_polymorphs/test/integration/app/public/dispatch.fcgi old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/public/dispatch.rb b/vendor/plugins/has_many_polymorphs/test/integration/app/public/dispatch.rb old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/script/about b/vendor/plugins/has_many_polymorphs/test/integration/app/script/about old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/script/breakpointer b/vendor/plugins/has_many_polymorphs/test/integration/app/script/breakpointer old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/script/console b/vendor/plugins/has_many_polymorphs/test/integration/app/script/console old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/script/destroy b/vendor/plugins/has_many_polymorphs/test/integration/app/script/destroy old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/script/generate b/vendor/plugins/has_many_polymorphs/test/integration/app/script/generate old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/script/performance/benchmarker b/vendor/plugins/has_many_polymorphs/test/integration/app/script/performance/benchmarker old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/script/performance/profiler b/vendor/plugins/has_many_polymorphs/test/integration/app/script/performance/profiler old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/script/plugin b/vendor/plugins/has_many_polymorphs/test/integration/app/script/plugin old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/script/process/inspector b/vendor/plugins/has_many_polymorphs/test/integration/app/script/process/inspector old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/script/process/reaper b/vendor/plugins/has_many_polymorphs/test/integration/app/script/process/reaper old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/script/process/spawner b/vendor/plugins/has_many_polymorphs/test/integration/app/script/process/spawner old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/script/runner b/vendor/plugins/has_many_polymorphs/test/integration/app/script/runner old mode 100755 new mode 100644 diff --git a/vendor/plugins/has_many_polymorphs/test/integration/app/script/server b/vendor/plugins/has_many_polymorphs/test/integration/app/script/server old mode 100755 new mode 100644