From 4e0b459524e9c22fd842d0b7c6b2efc3b1c42950 Mon Sep 17 00:00:00 2001 From: lukemelia Date: Fri, 27 Oct 2006 07:19:24 +0000 Subject: [PATCH] Applied Tomas Rich's patch (with some refactoring) to introduce a new api method that allows task creation using the same syntax as the "Send to kGTD" quicksilver plugin: Put an @ in front of context name and > in front of project name so "Call jim @calls > Fix house" would create the "Call jim" todo in the calls context and the Fix house project. If there aren't any exact matches, it selects the first context and project that starts with the given strings, so "Call jim @cal >Fix h" would also work. If no project and no context are give, it works exactly like the NewTodo. It also supports the ability to create a new project on the fly by prefacing the project with "new:", for example "Call jim @calls > new:Clean the porch" The new api method the new api method has the name NewRichTodo, which neither Tomas nor I like very much. Perhaps you have an idea for a better name? Closes #384. Also, I removed duplication between context and project models by introducing acts_as_namepart_finder and acts_as_todo_container. git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@333 a4c988fc-2ded-0310-b66e-134b36920a42 --- tracks/app/apis/todo_api.rb | 4 ++ tracks/app/controllers/backend_controller.rb | 59 ++++++++++++++++--- tracks/app/models/context.rb | 30 ++-------- tracks/app/models/project.rb | 32 ++-------- tracks/config/environment.rb.tmpl | 9 +++ tracks/lib/acts_as_namepart_finder.rb | 30 ++++++++++ tracks/lib/acts_as_todo_container.rb | 56 ++++++++++++++++++ .../functional/backend_controller_test.rb | 41 +++++++++++++ 8 files changed, 200 insertions(+), 61 deletions(-) create mode 100644 tracks/lib/acts_as_namepart_finder.rb create mode 100644 tracks/lib/acts_as_todo_container.rb 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') }