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
This commit is contained in:
lukemelia 2006-10-27 07:19:24 +00:00
parent 4c2742f6f3
commit 4e0b459524
8 changed files with 200 additions and 61 deletions

View file

@ -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}],

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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') }