diff --git a/tracks/app/controllers/application.rb b/tracks/app/controllers/application.rb index 9fb16a6f..6b42a528 100644 --- a/tracks/app/controllers/application.rb +++ b/tracks/app/controllers/application.rb @@ -13,7 +13,7 @@ class ApplicationController < ActionController::Base helper :application include LoginSystem - + layout 'standard' before_filter :set_session_expiration @@ -21,6 +21,9 @@ class ApplicationController < ActionController::Base after_filter :set_charset + include ActionView::Helpers::TextHelper + 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" @@ -78,13 +81,35 @@ class ApplicationController < ActionController::Base end def count_undone_todos(todos_parent) - if (todos_parent.is_a?(Project) && todos_parent.hidden?) + 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 if count == nil - count + count || 0 + end + + # Convert a date object to the format specified + # in config/settings.yml + # + def format_date(date) + if date + date_format = @user.prefs.date_format + formatted_date = date.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 protected diff --git a/tracks/app/controllers/feed_controller.rb b/tracks/app/controllers/feed_controller.rb deleted file mode 100644 index badce178..00000000 --- a/tracks/app/controllers/feed_controller.rb +++ /dev/null @@ -1,133 +0,0 @@ -# Produces an feeds of the next actions, both RSS and plain text -# -class FeedController < ApplicationController - - helper :feed - layout nil - - session :disabled => true # Prevents session control from interfering with feed - - skip_before_filter :login_required - before_filter :check_token_against_user_word - before_filter :prepare_for_feed, :only => [:rss, :text, :ical] - before_filter :identify_contexts, :only => [:text, :ical] - - # Build an RSS feed - def rss - headers["Content-Type"] = "text/xml; charset=utf-8" - end - - # Builds a plain text page listing incomplete next actions, - # grouped by context (contexts are sorted by position, as on the home page). - # Showing notes doesn't make much sense here so they are omitted. - # Hidden contexts are also hidden in the text view - # You can use this with GeekTool to get your next actions - # on the desktop: - # curl [url from "TXT" link on todo/list] - # - def text - headers["Content-Type"] = "text/plain; charset=utf-8" - end - - # Builds an iCal compatible export of incomplete todos - # so that each action forms a VTODO in your iCal calendar. - # Due dates are supported, and notes are included. - # - def ical - headers["Content-Type"] = "text/calendar" - end - -protected - - # Check whether the token in the URL matches the word in the User's table - def check_token_against_user_word - @user = User.find_by_login( params['login'] ) - unless ( params['token'] == @user.word) - render :text => "Sorry, you don't have permission to view this page." - return false - end - end - - def identify_contexts - if params.key?('context') - @contexts = [ @user.contexts.find_by_params(params) ] - else - @contexts = @user.contexts.find_all_by_hide(false, "position ASC") - end - end - - def prepare_for_feed - condition_builder = FindConditionBuilder.new - options = Hash.new - - if params.key?('done') - condition_builder.add 'todos.state = ?', 'completed' - else - condition_builder.add 'todos.state = ?', 'active' - end - - if params.key?('limit') - options[:limit] = limit = params['limit'] - @description = limit ? "Lists the last #{limit} incomplete next actions" : "Lists incomplete next actions" - end - @title = "Tracks - Next Actions" - @description = "Filter: " - - if params.key?('due') - due_within = params['due'].to_i - due_within_when = @user.time + 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 >= ?', @user.time - done_in_last.days) - @title << " actions completed" - @description << " in the last #{done_in_last.to_s} days" - end - - if params.key?('context') - context = @user.contexts.find_by_params(params) - condition_builder.add('todos.context_id = ?', context.id) - @title << " in #{context.name}" - @description << " in context '#{context.name}'" - end - - if params.key?('project') - project = @user.projects.find_by_params(params) - condition_builder.add('todos.project_id = ?', project.id) - @title << " for #{project.name}" - @description << " for project '#{project.name}'" - end - - options[:conditions] = condition_builder.to_conditions - options[:order] = "todos.due IS NULL, todos.due ASC, todos.created_at ASC" - options[:include] = :project - - @todos = @user.todos.find(:all, options ) - - 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 - - -end diff --git a/tracks/app/controllers/projects_controller.rb b/tracks/app/controllers/projects_controller.rb index df042b5e..5ed1149c 100644 --- a/tracks/app/controllers/projects_controller.rb +++ b/tracks/app/controllers/projects_controller.rb @@ -138,14 +138,15 @@ class ProjectsController < ApplicationController def render_rss_feed lambda do render_rss_feed_for @projects, :feed => Project.feed_options(@user), - :item => { :description => lambda { |p| p.summary(count_undone_todos_phrase(p)) } } + :item => { :title => :name, :description => lambda { |p| summary(p) } } end end def render_atom_feed lambda do render_atom_feed_for @projects, :feed => Project.feed_options(@user), - :item => { :description => lambda { |p| p.summary(count_undone_todos_phrase(p)) }, + :item => { :description => lambda { |p| summary(p) }, + :title => :name, :author => lambda { |p| nil } } end end @@ -181,4 +182,13 @@ class ProjectsController < ApplicationController init_data_for_sidebar end + def summary(project) + project_description = '' + project_description += sanitize(markdown( project.description )) unless project.description.blank? + project_description += "
#{count_undone_todos_phrase(p)}. " + project_description += "Project is #{project.state}." + project_description += "
" + project_description + end + end diff --git a/tracks/app/controllers/todos_controller.rb b/tracks/app/controllers/todos_controller.rb index e900b3bb..20e508de 100644 --- a/tracks/app/controllers/todos_controller.rb +++ b/tracks/app/controllers/todos_controller.rb @@ -3,33 +3,28 @@ class TodosController < ApplicationController helper :todos append_before_filter :init, :except => [ :destroy, :completed, :completed_archive, :check_deferred ] + skip_before_filter :login_required, :only => [:index] + prepend_before_filter :login_or_feed_token_required, :only => [:index] + session :off, :only => :index, :if => Proc.new { |req| is_feed_request(req) } + layout 'standard' - # Main method for listing tasks - # Set page title, and fill variables with contexts and done and not-done tasks - # Number of completed actions to show is determined by a setting in settings.yml def index @projects = @user.projects.find(:all, :include => [ :todos ]) @contexts = @user.contexts.find(:all, :include => [ :todos ]) - - @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 = @user.prefs.show_number_completed - @done = @user.completed_todos.find(:all, :limit => max_completed, :include => [ :context, :project, :tags ]) unless max_completed == 0 - + @contexts_to_show = @contexts.reject {|x| x.hide? } - # Set count badge to number of not-done, not hidden context items - @count = @todos.reject { |x| !x.active? || x.context.hide? }.size - - respond_to do |wants| - wants.html - wants.xml { render :action => 'list.rxml', :layout => false } + respond_to do |format| + format.html &render_todos_html + format.xml { render :action => 'list.rxml', :layout => false } + format.rss &render_rss_feed + format.atom &render_atom_feed + format.text &render_text_feed + format.ics &render_ical_feed end end - + def create @todo = @user.todos.build p = params['request'] || params @@ -100,7 +95,7 @@ class TodosController < ApplicationController # def toggle_check @todo = check_user_return_todo - @todo.toggle_completion() + @todo.toggle_completion! @saved = @todo.save respond_to do |format| format.js do @@ -279,13 +274,89 @@ class TodosController < ApplicationController init_data_for_sidebar init_todos end - - def init_todos - # Exclude hidden projects from count on home page - @todos = @user.todos.find(:all, :conditions => ['todos.state = ? or todos.state = ?', 'active', 'complete'], :include => [ :project, :context, :tags ]) - # Exclude hidden projects from the home page - @not_done_todos = @user.todos.find(:all, :conditions => ['todos.state = ?', 'active'], :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", :include => [ :project, :context, :tags ]) + def with_feed_query_scope(&block) + unless TodosController.is_feed_request(request) + yield + return + end + + condition_builder = FindConditionBuilder.new + options = Hash.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 = @user.time + 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 >= ?', @user.time - done_in_last.days) + @title << " actions completed" + @description << " in the last #{done_in_last.to_s} days" + end + + Todo.with_scope :find => {:conditions => condition_builder.to_conditions} do + yield + end + + end + + def with_parent_resource_scope(&block) + if (params[:context_id]) + context = @user.contexts.find_by_params(params) + Todo.with_scope :find => {:conditions => ['todos.context_id = ?', context.id]} do + yield + end + elsif (params[:project_id]) + project = @user.projects.find_by_params(params) + Todo.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.with_scope :find => {:limit => params['limit']} do + yield + end + #@description = limit ? "Lists the last #{limit} incomplete next actions" : "Lists incomplete next actions" + else + yield + end + end + + def init_todos + with_feed_query_scope do + with_parent_resource_scope do + with_limit_scope do + + # Exclude hidden projects from count on home page + @todos = @user.todos.find(:all, :conditions => ['todos.state = ? or todos.state = ?', 'active', 'complete'], :include => [ :project, :context, :tags ]) + + # Exclude hidden projects from the home page + @not_done_todos = @user.todos.find(:all, :conditions => ['todos.state = ?', 'active'], :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", :include => [ :project, :context, :tags ]) + + end + end + end end def determine_down_count @@ -323,5 +394,92 @@ class TodosController < ApplicationController 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 = @user.prefs.show_number_completed + @done = @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 = @todos.reject { |x| !x.active? || x.context.hide? }.size + + render + end + end + + def render_rss_feed + lambda do + render_rss_feed_for @todos, :feed => Todo.feed_options(@user), + :item => { + :title => :description, + :link => lambda { |t| context_url(t.context) }, + :description => todo_feed_content + } + end + end + + def todo_feed_content + lambda do |i| + item_notes = sanitize(markdown( i.notes )) if i.notes? + due = "#{undone_todo_count}. " - s += "Context is #{hidden? ? 'Hidden' : 'Active'}. " + s += "Context is #{hidden? ? 'Hidden' : 'Active'}." s += "
" s end diff --git a/tracks/app/models/project.rb b/tracks/app/models/project.rb index 84022914..46519b37 100644 --- a/tracks/app/models/project.rb +++ b/tracks/app/models/project.rb @@ -40,50 +40,34 @@ class Project < ActiveRecord::Base def self.feed_options(user) { :title => 'Tracks Projects', - :description => "Lists all the projects for #{user.display_name}." + :description => "Lists all the projects for #{user.display_name}" } end def to_param url_friendly_name end - - def description_present? - attribute_present?("description") - end - - def linkurl_present? - attribute_present?("linkurl") - end - - def title - name - end - - def summary(undone_todo_count) - project_description = '' - project_description += sanitize(markdown( description )) if description_present? - project_description += "#{undone_todo_count}. " - project_description += "Project is #{state}. " - project_description += "#{linkurl}" if linkurl_present? - project_description += "
" - project_description - end def hide_todos todos.each do |t| - t.hide! unless t.completed? || t.deferred? - t.save + unless t.completed? || t.deferred? + t.hide! + t.save + end end end def unhide_todos todos.each do |t| - t.unhide! if t.project_hidden? - t.save + if t.project_hidden? + t.unhide! + t.save + end end end - + + # would prefer to call this method state=(), but that causes an endless loop + # as a result of acts_as_state_machine calling state=() to update the attribute def transition_to(candidate_state) case candidate_state.to_sym when current_state diff --git a/tracks/app/models/todo.rb b/tracks/app/models/todo.rb index eb87de24..b0080439 100644 --- a/tracks/app/models/todo.rb +++ b/tracks/app/models/todo.rb @@ -1,5 +1,4 @@ class Todo < ActiveRecord::Base - require 'validations' belongs_to :context, :order => 'name' belongs_to :project @@ -45,12 +44,12 @@ class Todo < ActiveRecord::Base validates_presence_of :context def validate - if deferred? && !show_from.blank? && show_from < user.date - errors.add("Show From", "must be a date in the future.") + if !show_from.blank? && show_from < user.date + errors.add("show_from", "must be a date in the future") end end - def toggle_completion + def toggle_completion! if completed? activate! else @@ -78,11 +77,18 @@ class Todo < ActiveRecord::Base alias_method :original_set_initial_state, :set_initial_state def set_initial_state - if show_from && (show_from > Time.now.utc.to_date) + if show_from && (show_from > user.date) write_attribute self.class.state_column, 'deferred' else original_set_initial_state end end + def self.feed_options(user) + { + :title => 'Tracks Actions', + :description => "Actions for #{user.display_name}" + } + end + end \ No newline at end of file diff --git a/tracks/app/models/user.rb b/tracks/app/models/user.rb index b944351b..43dac6f9 100644 --- a/tracks/app/models/user.rb +++ b/tracks/app/models/user.rb @@ -13,6 +13,8 @@ class User < ActiveRecord::Base find_by_url_friendly_name(params['id']) elsif params['context'] find_by_url_friendly_name(params['context']) + elsif params['context_id'] + find_by_url_friendly_name(params['context_id']) end end end @@ -28,6 +30,8 @@ class User < ActiveRecord::Base find_by_url_friendly_name(params['id']) elsif params['project'] find_by_url_friendly_name(params['project']) + elsif params['project_id'] + find_by_url_friendly_name(params['project_id']) end end def update_positions(project_ids) @@ -61,7 +65,7 @@ class User < ActiveRecord::Base :conditions => [ 'state = ?', 'deferred' ], :order => 'show_from ASC, todos.created_at DESC' do def find_and_activate_ready - find(:all, :conditions => ['show_from <= ?', Time.now.utc.to_date ]).collect { |t| t.activate_and_save! } + find(:all, :conditions => ['show_from <= ?', Time.now.utc.to_date.to_time ]).collect { |t| t.activate_and_save! } end end has_many :completed_todos, @@ -90,9 +94,14 @@ class User < ActiveRecord::Base validates_confirmation_of :password validates_length_of :login, :within => 3..80 validates_uniqueness_of :login, :on => :create - validates_inclusion_of :auth_type, :in => Tracks::Config.auth_schemes, :message=>"not a valid authentication type" validates_presence_of :open_id_url, :if => Proc.new{|user| user.auth_type == 'open_id'} + def validate + unless Tracks::Config.auth_schemes.include?(auth_type) + errors.add("auth_type", "not a valid authentication type") + end + end + alias_method :prefs, :preference def self.authenticate(login, pass) diff --git a/tracks/app/views/contexts/_text_context.rhtml b/tracks/app/views/contexts/_text_context.rhtml new file mode 100644 index 00000000..bfc5babb --- /dev/null +++ b/tracks/app/views/contexts/_text_context.rhtml @@ -0,0 +1,8 @@ +<% +context = text_context +todos_in_context = todos.select { |t| t.context_id == context.id } +if todos_in_context.length > 0 + %> +<%= context.name.upcase %>: +<% end -%> +<%= render :partial => "todos/text_todo", :collection => todos_in_context -%> \ No newline at end of file diff --git a/tracks/app/views/feed/rss.rxml b/tracks/app/views/feed/rss.rxml deleted file mode 100644 index 39ce9069..00000000 --- a/tracks/app/views/feed/rss.rxml +++ /dev/null @@ -1,25 +0,0 @@ -xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do - xml.channel do - xml.title(@title) - xml.link("http://#{request.host}:#{request.port}/todos/list") - xml.description(@description) - @todos.each do |i| - xml.item do - xml.title(i.description) - xml.link(context_url(i.context)) - item_notes = sanitize(markdown( i.notes )) if i.notes? - due = "#{undone_todo_count}. Context is Active.
", @agenda.summary(undone_todo_count) + @agenda.hide = true + assert_equal "#{undone_todo_count}. Context is Hidden.
", @agenda.summary(undone_todo_count) + end + end diff --git a/tracks/test/unit/project_test.rb b/tracks/test/unit/project_test.rb index 9f2eb098..a529c0e4 100644 --- a/tracks/test/unit/project_test.rb +++ b/tracks/test/unit/project_test.rb @@ -142,9 +142,55 @@ class ProjectTest < Test::Unit::TestCase def test_to_param_returns_url_friendly_name assert_equal 'Build_a_working_time_machine', @timemachine.to_param end - - def test_title_reader_returns_name - assert_equal @timemachine.name, @timemachine.title + + def test_null_object + p = Project.null_object + assert !p.hidden? + assert p.nil? + assert_nil p.id end - + + def test_feed_options + opts = Project.feed_options(users(:admin_user)) + assert_equal 'Tracks Projects', opts[:title], 'Unexpected value for :title key of feed_options' + assert_equal 'Lists all the projects for Admin Schmadmin', opts[:description], 'Unexpected value for :description key of feed_options' + end + + def test_transition_to_another_state + assert_equal :active, @timemachine.current_state + @timemachine.transition_to(:hidden) + assert_equal :hidden, @timemachine.current_state + @timemachine.transition_to(:completed) + assert_equal :completed, @timemachine.current_state + @timemachine.transition_to(:active) + assert_equal :active, @timemachine.current_state + end + + def test_transition_to_same_state + assert_equal :active, @timemachine.current_state + @timemachine.transition_to(:active) + assert_equal :active, @timemachine.current_state + end + + def test_deferred_todo_count + assert_equal 1, @timemachine.deferred_todo_count + assert_equal 0, @moremoney.deferred_todo_count + @moremoney.todos[0].show_from = next_week + assert_equal 1, @moremoney.deferred_todo_count + end + + def test_done_todo_count + assert_equal 0, @timemachine.done_todo_count + assert_equal 0, @moremoney.done_todo_count + @moremoney.todos[0].complete! + assert_equal 1, @moremoney.done_todo_count + end + + def test_not_done_todo_count + assert_equal 2, @timemachine.not_done_todo_count + assert_equal 3, @moremoney.not_done_todo_count + @moremoney.todos[0].complete! + assert_equal 2, @moremoney.not_done_todo_count + end + end diff --git a/tracks/test/unit/todo_test.rb b/tracks/test/unit/todo_test.rb index 481e287a..4ef5f891 100644 --- a/tracks/test/unit/todo_test.rb +++ b/tracks/test/unit/todo_test.rb @@ -2,7 +2,7 @@ require File.dirname(__FILE__) + '/../test_helper' require 'date' class TodoTest < Test::Unit::TestCase - fixtures :todos, :users, :contexts + fixtures :todos, :users, :contexts, :preferences def setup @not_completed1 = Todo.find(1).reload @@ -19,8 +19,8 @@ class TodoTest < Test::Unit::TestCase 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.utc.strftime("%Y-%m-%d %H:%M"), @not_completed1.created_at.strftime("%Y-%m-%d %H:%M") - assert_equal 2.week.from_now.utc.strftime("%Y-%m-%d"), @not_completed1.due.strftime("%Y-%m-%d") + assert_equal 1.week.ago.utc.to_date.to_time.strftime("%Y-%m-%d %H:%M"), @not_completed1.created_at.strftime("%Y-%m-%d %H:%M") + assert_equal 2.week.from_now.utc.to_date.to_time.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 @@ -56,6 +56,16 @@ class TodoTest < Test::Unit::TestCase 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.to_date # we have to set this via the indexer because show_from=() updates the state + # and actual show_from value appropriately based on the date + assert_equal 1.week.ago.to_date, t.show_from + assert !t.save + assert_equal 1, t.errors.count + assert_equal "must be a date in the future", t.errors.on(:show_from) + end def test_defer_an_existing_todo @not_completed2 @@ -74,4 +84,58 @@ class TodoTest < Test::Unit::TestCase assert item.save, "should have saved successfully" + item.errors.to_xml assert_equal :deferred, item.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_and_save + t = @not_completed1 + t.show_from = 1.week.from_now.to_date + t.save! + assert t.deferred? + t.reload + t.activate_and_save! + 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.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 + end diff --git a/tracks/test/unit/user_test.rb b/tracks/test/unit/user_test.rb index ab643ec1..7e68efcb 100644 --- a/tracks/test/unit/user_test.rb +++ b/tracks/test/unit/user_test.rb @@ -1,11 +1,28 @@ require File.dirname(__FILE__) + '/../test_helper' +module Tracks + class Config + def self.auth_schemes + ['database', 'ldap'] + end + end +end + +class SimpleLdapAuthenticator + cattr_accessor :fake_success + + def self.valid?(login, pass) + fake_success + end +end + class UserTest < Test::Unit::TestCase - fixtures :users, :preferences, :projects, :todos + fixtures :users, :preferences, :projects, :contexts, :todos def setup assert_equal "test", ENV['RAILS_ENV'] assert_equal "change-me", Tracks::Config.salt + assert_equal ['database', 'ldap'], Tracks::Config.auth_schemes @admin_user = User.find(1) @other_user = User.find(2) end @@ -28,15 +45,15 @@ class UserTest < Test::Unit::TestCase assert_equal "jane", @other_user.login assert_equal "#{Digest::SHA1.hexdigest("#{Tracks::Config.salt}--sesame--")}", @other_user.password assert_not_nil @other_user.word - assert @other_user.is_admin == false || @other_user.is_admin == 0 + assert @other_user.is_admin == false || @other_user.is_admin == 0 end # ============================================ # Validations # ============================================ - + # Test a password shorter than 5 characters - # + # def test_validate_short_password assert_equal "#{Digest::SHA1.hexdigest("#{Tracks::Config.salt}--sesame--")}", @other_user.password @other_user.password = "four" @@ -46,25 +63,25 @@ class UserTest < Test::Unit::TestCase end # Test a password longer than 40 characters - # + # def test_validate_long_password assert_equal "#{Digest::SHA1.hexdigest("#{Tracks::Config.salt}--sesame--")}", @other_user.password @other_user.password = generate_random_string(41) assert !@other_user.save assert_equal 1, @other_user.errors.count assert_equal "is too long (maximum is 40 characters)", @other_user.errors.on(:password) - end - + end + # Test that correct length password is valid - # + # def test_validate_correct_length_password assert_equal "#{Digest::SHA1.hexdigest("#{Tracks::Config.salt}--sesame--")}", @other_user.password @other_user.password = generate_random_string(6) assert @other_user.save end - + # Test a missing password - # + # def test_validate_missing_password assert_equal 2, @other_user.id @other_user.password = "" @@ -72,9 +89,9 @@ class UserTest < Test::Unit::TestCase assert_equal 2, @other_user.errors.count assert_equal ["is too short (minimum is 5 characters)", "can't be blank"], @other_user.errors.on(:password) end - + # Test a login shorter than 3 characters - # + # def test_validate_short_login assert_equal "jane", @other_user.login @other_user.login = "ba" @@ -82,27 +99,27 @@ class UserTest < Test::Unit::TestCase assert_equal 1, @other_user.errors.count assert_equal "is too short (minimum is 3 characters)", @other_user.errors.on(:login) end - + # Test a login longer than 80 characters - # + # def test_validate_long_login assert_equal "jane", @other_user.login @other_user.login = generate_random_string(81) assert !@other_user.save assert_equal 1, @other_user.errors.count assert_equal "is too long (maximum is 80 characters)", @other_user.errors.on(:login) - end - + end + # Test that correct length login is valid - # + # def test_validate_correct_length_login assert_equal "jane", @other_user.login @other_user.login = generate_random_string(6) assert @other_user.save end - + # Test a missing login - # + # def test_validate_missing_login assert_equal 2, @other_user.id @other_user.login = "" @@ -110,39 +127,39 @@ class UserTest < Test::Unit::TestCase assert_equal 2, @other_user.errors.count assert_equal ["is too short (minimum is 3 characters)", "can't be blank"], @other_user.errors.on(:login) end - + def test_display_name_with_first_and_last_name_set @other_user.first_name = "Jane" @other_user.last_name = "Doe" assert_equal "Jane Doe", @other_user.display_name end - + def test_display_name_with_first_name_set @other_user.first_name = "Jane" @other_user.last_name = nil assert_equal "Jane", @other_user.display_name end - + def test_display_name_with_last_name_set @other_user.first_name = nil @other_user.last_name = "Doe" assert_equal "Doe", @other_user.display_name end - + def test_display_name_with_neither_first_nor_last_name_set @other_user.first_name = nil @other_user.last_name = nil assert_equal @other_user.login, @other_user.display_name end - + def test_prefs_is_short_for_preference assert_equal @admin_user.preference, @admin_user.prefs end - + def test_to_param_returns_login assert_equal @admin_user.login, @admin_user.to_param end - + def test_change_password assert_not_nil User.authenticate(@admin_user.login, "abracadabra") @admin_user.change_password("foobar", "foobar") @@ -152,28 +169,117 @@ class UserTest < Test::Unit::TestCase end def test_projects_next_project - moremoney = projects(:moremoney) - next_project = @admin_user.projects.next_from(moremoney) - assert_equal projects(:gardenclean), next_project + moremoney = projects(:moremoney) + next_project = @admin_user.projects.next_from(moremoney) + assert_equal projects(:gardenclean), next_project end def test_projects_previous_project - moremoney = projects(:moremoney) - previous_project = @admin_user.projects.previous_from(moremoney) - assert_equal projects(:timemachine), previous_project + moremoney = projects(:moremoney) + previous_project = @admin_user.projects.previous_from(moremoney) + assert_equal projects(:timemachine), previous_project end def test_projects_next_project_nil - gardenclean = projects(:gardenclean) - next_project = @admin_user.projects.next_from(gardenclean) - assert_nil next_project + gardenclean = projects(:gardenclean) + next_project = @admin_user.projects.next_from(gardenclean) + assert_nil next_project end def test_projects_previous_project_nil - timemachine = projects(:timemachine) - previous_project = @admin_user.projects.previous_from(timemachine) - assert_nil previous_project + timemachine = projects(:timemachine) + previous_project = @admin_user.projects.previous_from(timemachine) + assert_nil previous_project + end + + def test_no_users_yet + assert !User.no_users_yet? + User.delete_all + assert User.no_users_yet? + end + + def test_crypt_word_updates_word + old_word = @admin_user.word + @admin_user.crypt_word + assert_not_equal old_word, @admin_user.word + end + + def test_find_admin + assert_equal @admin_user, User.find_admin + end + + def test_validates_auth_type + @other_user.auth_type = 'dnacheck' + assert !@other_user.save + assert_equal 1, @other_user.errors.count + assert_equal "not a valid authentication type", @other_user.errors.on(:auth_type) + end + + def test_authenticate_can_use_ldap + u = @other_user + u.auth_type = 'ldap' + u.save! + SimpleLdapAuthenticator.fake_success = false + assert_nil User.authenticate(u.login, 'foobar') + SimpleLdapAuthenticator.fake_success = true + assert_equal @other_user, User.authenticate(u.login, 'foobar') + end + + def test_find_context_by_params + u = @admin_user + c = u.contexts.find_by_params('url_friendly_name' => 'agenda') + assert_equal contexts(:agenda), c + c = u.contexts.find_by_params('id' => 'agenda') + assert_equal contexts(:agenda), c + c = u.contexts.find_by_params('id' => '1') + assert_equal contexts(:agenda), c + c = u.contexts.find_by_params('context' => 'agenda') + assert_equal contexts(:agenda), c + c = u.contexts.find_by_params('context_id' => 'agenda') + assert_equal contexts(:agenda), c + end + + def test_find_project_by_params + u = @admin_user + p = u.projects.find_by_params('url_friendly_name' => 'Build_a_working_time_machine') + assert_equal projects(:timemachine), p + p = u.projects.find_by_params('id' => 'Build_a_working_time_machine') + assert_equal projects(:timemachine), p + p = u.projects.find_by_params('id' => '1') + assert_equal projects(:timemachine), p + p = u.projects.find_by_params('project' => 'Build_a_working_time_machine') + assert_equal projects(:timemachine), p + p = u.projects.find_by_params('project_id' => 'Build_a_working_time_machine') + assert_equal projects(:timemachine), p + end + + def test_update_project_positions + assert_equal 1, Project.find(1).position + assert_equal 2, Project.find(2).position + assert_equal 3, Project.find(3).position + + @admin_user.projects.update_positions([2,1,3]) + + assert_equal 2, Project.find(1).position + assert_equal 1, Project.find(2).position + assert_equal 3, Project.find(3).position + end + + def test_find_and_activate_deferred_todos_that_are_ready + assert_equal 1, @admin_user.deferred_todos.count + @admin_user.deferred_todos.find_and_activate_ready + @admin_user.deferred_todos.reload + assert_equal 0, @admin_user.deferred_todos.count + end + + def test_completed_todos_completed_within + todos = @admin_user.completed_todos.completed_within(@admin_user.time - 1.day) + assert_equal 3, todos.length + end + + def test_completed_todos_complete_more_than + todos = @admin_user.completed_todos.completed_more_than(@admin_user.time - 1.day) + assert_equal 1, todos.length end - end