diff --git a/tracks/app/apis/todo_api.rb b/tracks/app/apis/todo_api.rb index 584640ca..06f359a0 100644 --- a/tracks/app/apis/todo_api.rb +++ b/tracks/app/apis/todo_api.rb @@ -2,6 +2,10 @@ class TodoApi < ActionWebService::API::Base api_method :new_todo, :expects => [{:username => :string}, {:token => :string}, {:context_id => :int}, {:description => :string}], :returns => [:int] + + api_method :new_rich_todo, + :expects => [{:username => :string}, {:token => :string}, {:default_context_id => :int}, {:description => :string}], + :returns => [:int] api_method :list_contexts, :expects => [{:username => :string}, {:token => :string}], diff --git a/tracks/app/controllers/backend_controller.rb b/tracks/app/controllers/backend_controller.rb index 6507bfba..200c3129 100644 --- a/tracks/app/controllers/backend_controller.rb +++ b/tracks/app/controllers/backend_controller.rb @@ -6,15 +6,41 @@ class BackendController < ApplicationController def new_todo(username, token, context_id, description) check_token_against_user_word(username, token) check_context_belongs_to_user(context_id) - - item = @user.todos.build - item.description = description - item.context_id = context_id - item.save - raise item.errors.full_messages.to_s if item.new_record? + item = create_todo(description, context_id) item.id end + def new_rich_todo(username, token, default_context_id, description) + check_token_against_user_word(username,token) + description,context = split_by_char('@',description) + description,project = split_by_char('>',description) + if(!context.nil? && project.nil?) + context,project = split_by_char('>',context) + end + + context_id = default_context_id + unless(context.nil?) + found_context = @user.contexts.find_by_namepart(context) + context_id = found_context.id unless found_context.nil? + end + check_context_belongs_to_user(context_id) + + project_id = nil + unless(project.blank?) + if(project[0..3].downcase == "new:") + found_project = @user.projects.build + found_project.name = project[4..255+4].strip + found_project.save! + else + found_project = @user.projects.find_by_namepart(project) + end + project_id = found_project.id unless found_project.nil? + end + + todo = create_todo(description, context_id, project_id) + todo.id + end + def list_contexts(username, token) check_token_against_user_word(username, token) @@ -42,7 +68,26 @@ class BackendController < ApplicationController raise(CannotAccessContext, "Cannot access a context that does not belong to this user.") end end - + + def create_todo(description, context_id, project_id = nil) + item = @user.todos.build + item.description = description + item.context_id = context_id + item.project_id = project_id unless project_id.nil? + item.save + raise item.errors.full_messages.to_s if item.new_record? + item + end + + def split_by_char(separator,string) + parts = string.split(separator) + return safe_strip(parts[0]), safe_strip(parts[1]) + end + + def safe_strip(s) + s.strip! unless s.nil? + s + end end class InvalidToken < RuntimeError; end diff --git a/tracks/app/models/context.rb b/tracks/app/models/context.rb index 808f1d01..cb3f4daa 100644 --- a/tracks/app/models/context.rb +++ b/tracks/app/models/context.rb @@ -2,7 +2,10 @@ class Context < ActiveRecord::Base has_many :todos, :dependent => true, :order => "completed DESC" belongs_to :user + acts_as_list :scope => :user + acts_as_namepart_finder + acts_as_todo_container :find_todos_include => :project attr_protected :user @@ -13,33 +16,8 @@ class Context < ActiveRecord::Base validates_uniqueness_of :name, :message => "already exists", :scope => "user_id" validates_format_of :name, :with => /^[^\/]*$/i, :message => "cannot contain the slash ('/') character" - def not_done_todos - @not_done_todos = self.find_not_done_todos if @not_done_todos == nil - @not_done_todos - end - - def done_todos - @done_todos = self.find_done_todos if @done_todos == nil - @done_todos - end - - def find_not_done_todos - todos = Todo.find(:all, - :conditions => ['todos.context_id = ? and todos.type = ? and todos.done = ?', id, "Immediate", false], - :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", - :include => [ :project, :context ]) - - end - - def find_done_todos - todos = Todo.find :all, :conditions => ["todos.context_id = ? AND todos.type = ? AND todos.done = ?", id, "Immediate", true], - :order => "completed DESC", - :include => [:context, :project], - :limit => @user.preference.show_number_completed - end - def hidden? self.hide == true end - + end diff --git a/tracks/app/models/project.rb b/tracks/app/models/project.rb index 41f05e52..54c0469f 100644 --- a/tracks/app/models/project.rb +++ b/tracks/app/models/project.rb @@ -2,7 +2,7 @@ class Project < ActiveRecord::Base has_many :todos, :dependent => true has_many :notes, :dependent => true, :order => "created_at DESC" belongs_to :user - + # Project name must not be empty # and must be less than 255 bytes validates_presence_of :name, :message => "project must have a name" @@ -12,6 +12,8 @@ class Project < ActiveRecord::Base acts_as_list :scope => :user acts_as_state_machine :initial => :active, :column => 'state' + acts_as_namepart_finder + acts_as_todo_container :find_todos_include => :context state :active state :hidden @@ -38,31 +40,5 @@ class Project < ActiveRecord::Base def linkurl_present? attribute_present?("linkurl") end - - def not_done_todos - @not_done_todos = self.find_not_done_todos if @not_done_todos == nil - @not_done_todos - end - - def done_todos - @done_todos = self.find_done_todos if @done_todos == nil - @done_todos - end - - def find_not_done_todos - todos = Todo.find(:all, - :conditions => ['todos.project_id = ? and todos.type = ? and todos.done = ?', id, "Immediate", false], - :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", - :include => [ :project, :context ]) - - end - - def find_done_todos - todos = Todo.find :all, :conditions => ["todos.project_id = ? AND todos.type = ? AND todos.done = ?", id, "Immediate", true], - :order => "completed DESC", - :include => [:context, :project], - :limit => @user.preference.show_number_completed - end - - + end diff --git a/tracks/config/environment.rb.tmpl b/tracks/config/environment.rb.tmpl index 0270043e..846f3a8e 100644 --- a/tracks/config/environment.rb.tmpl +++ b/tracks/config/environment.rb.tmpl @@ -57,3 +57,12 @@ SALT = "change-me" # You should be able to find a list of time zones in /usr/share/zoneinfo # e.g. if you are in the Eastern time zone of the US, set the value below. # ENV['TZ'] = 'US/Eastern' + +require 'acts_as_namepart_finder' +require 'acts_as_todo_container' + +ActiveRecord::Base.class_eval do + include Tracks::Acts::NamepartFinder + include Tracks::Acts::TodoContainer +end + diff --git a/tracks/lib/acts_as_namepart_finder.rb b/tracks/lib/acts_as_namepart_finder.rb new file mode 100644 index 00000000..b3ea045e --- /dev/null +++ b/tracks/lib/acts_as_namepart_finder.rb @@ -0,0 +1,30 @@ +module Tracks + module Acts #:nodoc: + module NamepartFinder #:nodoc: + + # This act provides the capabilities for finding a name that equals or starts with a given string + + def self.included(base) #:nodoc: + base.extend ActMacro + end + + module ActMacro + def acts_as_namepart_finder + self.extend(ClassMethods) + end + end + + module ClassMethods + + def find_by_namepart(namepart) + entity = find(:first, :conditions => ['name = ?', namepart]) + if (entity.nil?) + entity = find :first, :conditions => ["name LIKE ?", namepart + '%'] + end + entity + end + end + + end + end +end diff --git a/tracks/lib/acts_as_todo_container.rb b/tracks/lib/acts_as_todo_container.rb new file mode 100644 index 00000000..d6288ca9 --- /dev/null +++ b/tracks/lib/acts_as_todo_container.rb @@ -0,0 +1,56 @@ +module Tracks + module Acts #:nodoc: + module TodoContainer #:nodoc: + + # This act provides the capabilities for finding todos that belong to the entity + + def self.included(base) #:nodoc: + base.extend ActMacro + end + + module ActMacro + def acts_as_todo_container(opts = {}) + + opts[:find_todos_include] = [] unless opts.key?(:find_todos_include) + opts[:find_todos_include] = [opts[:find_todos_include]] unless opts[:find_todos_include].is_a?(Array) + write_inheritable_attribute :find_todos_include, [base_class.name.singularize.downcase] + opts[:find_todos_include] + + class_inheritable_reader :find_todos_include + + class_eval "include Tracks::Acts::TodoContainer::InstanceMethods" + end + end + + module InstanceMethods + + def not_done_todos + @not_done_todos = self.find_not_done_todos if @not_done_todos == nil + @not_done_todos + end + + def done_todos + @done_todos = self.find_done_todos if @done_todos == nil + @done_todos + end + + def find_not_done_todos + todos = Todo.find(:all, + :conditions => ["todos.#{self.class.base_class.name.singularize.downcase}_id = ? and todos.type = ? and todos.done = ?", id, "Immediate", false], + :order => "todos.due IS NULL, todos.due ASC, todos.created_at ASC", + :include => find_todos_include) + + end + + def find_done_todos + todos = Todo.find :all, :conditions => ["todos.#{self.class.base_class.name.singularize.downcase}_id = ? AND todos.type = ? AND todos.done = ?", id, "Immediate", true], + :order => "completed DESC", + :include => find_todos_include, + :limit => @user.preference.show_number_completed + end + + end + + + end + end +end diff --git a/tracks/test/functional/backend_controller_test.rb b/tracks/test/functional/backend_controller_test.rb index e88d0b7d..8ca029b7 100644 --- a/tracks/test/functional/backend_controller_test.rb +++ b/tracks/test/functional/backend_controller_test.rb @@ -20,6 +20,47 @@ class BackendControllerTest < Test::Unit::TestCase def test_new_todo_fails_with_context_that_does_not_belong_to_user assert_raise(CannotAccessContext, "Cannot access a context that does not belong to this user.") { @controller.new_todo(users('other_user').login, users('other_user').word, contexts('agenda').id, 'test') } end + + def test_new_rich_todo_fails_with_incorrect_token + assert_raises_invalid_token { @controller.new_rich_todo('admin', 'notthecorrecttoken', contexts('agenda').id, 'test') } + end + + #"Call mfox @call > Build a working time machine" should create the "Call mfox" todo in the 'call' context and the 'Build a working time machine' project. + def test_new_rich_todo_creates_todo_with_exact_match + assert_new_rich_todo_creates_mfox_todo("Call mfox @call > Build a working time machine") + end + + #"Call mfox @cal > Build" should create the "Call mfox" todo in the 'call' context and the 'Build a working time machine' project. + def test_new_rich_todo_creates_todo_with_starts_with_match + assert_new_rich_todo_creates_mfox_todo("Call mfox @cal > Build") + end + + #"Call mfox @call > new:Run for president" should create the 'Run for president' project, create the "Call mfox" todo in the 'call' context and the new project. + def test_new_rich_todo_creates_todo_with_new_project + max_todo_id = Todo.maximum('id') + max_project_id = Project.maximum('id') + @controller.new_rich_todo(users(:admin_user).login, users(:admin_user).word, contexts(:agenda).id, 'Call mfox @call > new:Run for president') + todo = Todo.find(:first, :conditions => ["id > ?", max_todo_id]) + new_project = Project.find(:first, :conditions => ["id > ?", max_project_id]) + assert_equal(users(:admin_user).id, todo.user_id) + assert_equal(contexts(:call).id, todo.context_id) + assert_equal(new_project.id, todo.project_id) + assert_equal("Call mfox", todo.description) + end + + def assert_new_rich_todo_creates_mfox_todo(description_input) + max_id = Todo.maximum('id') + @controller.new_rich_todo(users(:admin_user).login, users(:admin_user).word, contexts(:agenda).id, 'Call mfox @cal > Build') + todo = Todo.find(:first, :conditions => ["id > ?", max_id]) + assert_equal(users(:admin_user).id, todo.user_id) + assert_equal(contexts(:call).id, todo.context_id) + assert_equal(projects(:timemachine).id, todo.project_id) + assert_equal("Call mfox", todo.description) + end + + def test_new_rich_todo_fails_with_context_that_does_not_belong_to_user + assert_raise(CannotAccessContext, "Cannot access a context that does not belong to this user.") { @controller.new_rich_todo(users('other_user').login, users('other_user').word, contexts('agenda').id, 'test') } + end def test_list_projects_fails_with_incorrect_token assert_raises_invalid_token { @controller.list_projects('admin', 'notthecorrecttoken') }