2009-01-21 13:36:26 +01:00
class Todo < ActiveRecord :: Base
2011-05-01 12:48:32 +02:00
after_save :save_predecessors
# relations
2009-01-21 13:36:26 +01:00
belongs_to :context
belongs_to :project
belongs_to :user
belongs_to :recurring_todo
2011-08-18 17:15:00 +02:00
2009-06-08 23:43:40 +02:00
has_many :predecessor_dependencies , :foreign_key = > 'predecessor_id' , :class_name = > 'Dependency' , :dependent = > :destroy
has_many :successor_dependencies , :foreign_key = > 'successor_id' , :class_name = > 'Dependency' , :dependent = > :destroy
2009-06-09 13:45:39 +02:00
has_many :predecessors , :through = > :successor_dependencies
has_many :successors , :through = > :predecessor_dependencies
2009-05-20 13:00:14 +02:00
has_many :uncompleted_predecessors , :through = > :successor_dependencies ,
2011-02-04 20:11:42 +01:00
:source = > :predecessor , :conditions = > [ 'NOT (todos.state = ?)' , 'completed' ]
2009-06-08 23:43:40 +02:00
has_many :pending_successors , :through = > :predecessor_dependencies ,
2011-02-04 20:11:42 +01:00
:source = > :successor , :conditions = > [ 'todos.state = ?' , 'pending' ]
2009-01-21 13:36:26 +01:00
2011-05-01 12:48:32 +02:00
# scopes for states of this todo
2009-01-21 13:36:26 +01:00
named_scope :active , :conditions = > { :state = > 'active' }
2011-01-06 23:01:17 +01:00
named_scope :active_or_hidden , :conditions = > [ " todos.state = ? OR todos.state = ? " , 'active' , 'project_hidden' ]
2011-05-01 12:48:32 +02:00
named_scope :not_completed , :conditions = > [ 'NOT (todos.state = ?)' , 'completed' ]
named_scope :completed , :conditions = > [ " NOT (todos.completed_at IS NULL) " ]
named_scope :deferred , :conditions = > [ " todos.completed_at IS NULL AND NOT (todos.show_from IS NULL) " ]
2010-05-01 17:19:28 +02:00
named_scope :blocked , :conditions = > [ 'todos.state = ?' , 'pending' ]
2011-05-01 12:48:32 +02:00
named_scope :pending , :conditions = > [ 'todos.state = ?' , 'pending' ]
2011-05-03 11:43:02 +02:00
named_scope :deferred_or_blocked , :conditions = > [ " (todos.completed_at IS NULL AND NOT(todos.show_from IS NULL)) OR (todos.state = ?) " , " pending " ]
2012-01-03 22:08:50 +01:00
named_scope :not_deferred_or_blocked , :conditions = > [ " (todos.completed_at IS NULL) AND (todos.show_from IS NULL) AND (NOT todos.state = ?) " , " pending " ]
2011-08-18 17:15:00 +02:00
named_scope :hidden ,
2011-10-28 19:33:51 +02:00
:joins = > " INNER JOIN contexts c_hidden ON c_hidden.id = todos.context_id " ,
:conditions = > [ " todos.state = ? OR (c_hidden.hide = ? AND (todos.state = ? OR todos.state = ? OR todos.state = ?)) " ,
2011-01-06 10:51:58 +01:00
'project_hidden' , true , 'active' , 'deferred' , 'pending' ]
2011-01-01 18:55:53 +01:00
named_scope :not_hidden ,
2011-10-28 19:33:51 +02:00
:joins = > " INNER JOIN contexts c_hidden ON c_hidden.id = todos.context_id " ,
:conditions = > [ 'NOT(todos.state = ? OR (c_hidden.hide = ? AND (todos.state = ? OR todos.state = ? OR todos.state = ?)))' ,
2011-01-06 10:51:58 +01:00
'project_hidden' , true , 'active' , 'deferred' , 'pending' ]
2009-01-21 13:36:26 +01:00
2011-05-01 12:48:32 +02:00
# other scopes
named_scope :are_due , :conditions = > [ 'NOT (todos.due IS NULL)' ]
2011-10-31 14:39:52 +01:00
named_scope :with_tag , lambda { | tag_id | { :joins = > :taggings , :conditions = > [ " taggings.tag_id = ? " , tag_id ] } }
2011-10-28 19:33:51 +02:00
named_scope :with_tags , lambda { | tag_ids | { :conditions = > [ " EXISTS(SELECT * from taggings t WHERE t.tag_id IN (?) AND t.taggable_id=todos.id AND t.taggable_type='Todo') " , tag_ids ] } }
2011-05-01 12:48:32 +02:00
named_scope :of_user , lambda { | user_id | { :conditions = > [ " todos.user_id = ? " , user_id ] } }
2011-11-19 02:41:06 +01:00
named_scope :completed_after , lambda { | date | { :conditions = > [ " todos.completed_at > ? " , date ] } }
named_scope :completed_before , lambda { | date | { :conditions = > [ " todos.completed_at < ? " , date ] } }
named_scope :created_after , lambda { | date | { :conditions = > [ " todos.created_at > ? " , date ] } }
named_scope :created_before , lambda { | date | { :conditions = > [ " todos.created_at < ? " , date ] } }
2011-05-01 12:48:32 +02:00
2009-01-21 13:36:26 +01:00
STARRED_TAG_NAME = " starred "
2011-06-17 14:58:32 +02:00
DEFAULT_INCLUDES = [ :project , :context , :tags , :taggings , :pending_successors , :uncompleted_predecessors , :recurring_todo ]
2010-08-12 14:39:58 +02:00
2011-09-30 12:06:43 +02:00
# state machine
2011-05-16 15:42:47 +08:00
include AASM
aasm_column :state
2011-06-12 04:29:35 +02:00
aasm_initial_state Proc . new { | t | ( t . show_from && t . user && ( t . show_from > t . user . date ) ) ? :deferred : :active }
2011-08-18 17:15:00 +02:00
2011-06-10 14:28:42 +02:00
aasm_state :active
2011-06-12 00:11:29 +02:00
aasm_state :project_hidden
2011-08-18 17:15:00 +02:00
aasm_state :completed , :enter = > Proc . new { | t | t . completed_at = Time . zone . now } , :exit = > Proc . new { | t | t . completed_at = nil }
2011-06-10 14:28:42 +02:00
aasm_state :deferred , :exit = > Proc . new { | t | t [ :show_from ] = nil }
2011-08-18 17:15:00 +02:00
aasm_state :pending
2009-01-21 13:36:26 +01:00
2011-05-16 15:42:47 +08:00
aasm_event :defer do
2009-01-21 13:36:26 +01:00
transitions :to = > :deferred , :from = > [ :active ]
end
2011-08-18 17:15:00 +02:00
2011-05-16 15:42:47 +08:00
aasm_event :complete do
2011-08-18 17:15:00 +02:00
transitions :to = > :completed , :from = > [ :active , :project_hidden , :deferred , :pending ]
2009-01-21 13:36:26 +01:00
end
2011-08-18 17:15:00 +02:00
2011-05-16 15:42:47 +08:00
aasm_event :activate do
2011-08-18 17:15:00 +02:00
transitions :to = > :active , :from = > [ :project_hidden , :deferred ]
transitions :to = > :active , :from = > [ :completed ] , :guard = > :no_uncompleted_predecessors?
transitions :to = > :active , :from = > [ :pending ] , :guard = > :no_uncompleted_predecessors_or_deferral?
transitions :to = > :pending , :from = > [ :completed ] , :guard = > :uncompleted_predecessors?
2009-08-16 22:00:20 +02:00
transitions :to = > :deferred , :from = > [ :pending ] , :guard = > :no_uncompleted_predecessors?
2009-01-21 13:36:26 +01:00
end
2011-08-18 17:15:00 +02:00
2011-05-16 15:42:47 +08:00
aasm_event :hide do
2011-09-14 14:22:30 +02:00
transitions :to = > :project_hidden , :from = > [ :active , :deferred , :pending ]
2009-01-21 13:36:26 +01:00
end
2011-08-18 17:15:00 +02:00
2011-05-16 15:42:47 +08:00
aasm_event :unhide do
2009-01-21 13:36:26 +01:00
transitions :to = > :deferred , :from = > [ :project_hidden ] , :guard = > Proc . new { | t | ! t . show_from . blank? }
2011-09-14 14:22:30 +02:00
transitions :to = > :pending , :from = > [ :project_hidden ] , :guard = > :uncompleted_predecessors?
2009-01-21 13:36:26 +01:00
transitions :to = > :active , :from = > [ :project_hidden ]
end
2011-08-18 17:15:00 +02:00
2011-05-16 15:42:47 +08:00
aasm_event :block do
2009-08-16 22:00:20 +02:00
transitions :to = > :pending , :from = > [ :active , :deferred ]
2009-05-20 02:05:49 +02:00
end
2011-08-18 17:15:00 +02:00
2009-01-21 13:36:26 +01:00
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
2011-08-18 17:15:00 +02:00
validates_length_of :notes , :maximum = > 60000 , :allow_nil = > true
2009-01-21 13:36:26 +01:00
validates_presence_of :show_from , :if = > :deferred?
validates_presence_of :context
2011-08-18 17:15:00 +02:00
2009-06-30 23:17:33 +02:00
def initialize ( * args )
super ( * args )
2009-07-19 21:46:45 +02:00
@predecessor_array = nil # Used for deferred save of predecessors
2011-02-03 16:59:59 +01:00
@removed_predecessors = nil
2009-06-30 23:17:33 +02:00
end
2011-08-18 17:15:00 +02:00
2009-08-16 22:00:20 +02:00
def no_uncompleted_predecessors_or_deferral?
2011-08-18 17:15:00 +02:00
no_deferral = show_from . blank? or Time . zone . now > show_from
2011-11-16 19:36:09 +01:00
return ( no_deferral && no_uncompleted_predecessors? )
2009-08-16 22:00:20 +02:00
end
2011-08-18 17:15:00 +02:00
2009-08-16 22:00:20 +02:00
def no_uncompleted_predecessors?
2011-11-16 19:36:09 +01:00
return ! uncompleted_predecessors?
2009-08-19 22:15:38 +02:00
end
2011-08-18 17:15:00 +02:00
def uncompleted_predecessors?
return ! uncompleted_predecessors . all ( true ) . empty?
end
2009-08-20 09:54:22 +02:00
# Returns a string with description <context, project>
def specification
2011-02-12 10:38:15 +01:00
project_name = self . project . is_a? ( NullProject ) ? " (none) " : self . project . name
return " \' #{ self . description } \' < \' #{ self . context . title } \' ; \' #{ project_name } \' > "
2009-08-20 09:54:22 +02:00
end
2011-08-18 17:15:00 +02:00
2009-01-21 13:36:26 +01:00
def validate
if ! show_from . blank? && show_from < user . date
2010-11-09 10:47:09 +01:00
errors . add ( " show_from " , I18n . t ( 'models.todo.error_date_must_be_future' ) )
2009-01-21 13:36:26 +01:00
end
2009-07-19 21:46:45 +02:00
unless @predecessor_array . nil? # Only validate predecessors if they changed
2011-02-12 10:38:15 +01:00
@predecessor_array . each do | todo |
errors . add ( " Depends on: " , " Adding ' #{ h ( todo . specification ) } ' would create a circular dependency " ) if is_successor? ( todo )
2009-06-30 23:17:33 +02:00
end
end
end
2011-08-18 17:15:00 +02:00
2009-06-30 23:17:33 +02:00
def save_predecessors
2009-07-19 21:46:45 +02:00
unless @predecessor_array . nil? # Only save predecessors if they changed
2011-02-12 10:38:15 +01:00
current_array = self . predecessors
2009-07-19 21:46:45 +02:00
remove_array = current_array - @predecessor_array
add_array = @predecessor_array - current_array
2011-02-03 16:59:59 +01:00
@removed_predecessors = [ ]
2011-02-12 10:38:15 +01:00
remove_array . each do | todo |
unless todo . nil?
@removed_predecessors << todo
self . predecessors . delete ( todo )
2011-02-03 16:59:59 +01:00
end
2009-07-19 21:46:45 +02:00
end
2011-02-12 10:38:15 +01:00
add_array . each do | todo |
unless todo . nil?
self . predecessors << todo unless self . predecessors . include? ( todo )
2009-07-19 21:46:45 +02:00
else
2011-02-12 10:38:15 +01:00
logger . error " Could not find #{ todo . description } " # Unexpected since validation passed
2009-07-19 21:46:45 +02:00
end
2009-06-30 23:17:33 +02:00
end
2011-08-18 17:15:00 +02:00
end
2011-02-03 16:59:59 +01:00
end
def removed_predecessors
return @removed_predecessors
2009-06-30 23:17:33 +02:00
end
2011-08-18 17:15:00 +02:00
2009-11-10 22:15:16 -05:00
def remove_predecessor ( predecessor )
# remove predecessor and activate myself
2011-02-12 10:38:15 +01:00
self . predecessors . delete ( predecessor )
2009-11-10 22:15:16 -05:00
self . activate!
end
2011-08-18 17:15:00 +02:00
2009-06-30 23:17:33 +02:00
# Returns true if t is equal to self or a successor of self
2011-02-12 10:38:15 +01:00
def is_successor? ( todo )
if self == todo
2009-06-30 23:17:33 +02:00
return true
elsif self . successors . empty?
return false
else
self . successors . each do | item |
2011-02-12 10:38:15 +01:00
if item . is_successor? ( todo )
2009-06-30 23:17:33 +02:00
return true
end
end
end
return false
2009-01-21 13:36:26 +01:00
end
2011-08-18 17:15:00 +02:00
2011-06-17 13:33:54 +02:00
def has_pending_successors
2011-06-17 14:58:32 +02:00
return ! pending_successors . empty?
2011-06-17 13:33:54 +02:00
end
2011-08-18 17:15:00 +02:00
2011-11-16 19:36:09 +01:00
def has_tag? ( tag_name )
return self . tags . any? { | tag | tag . name == tag_name }
2011-01-01 18:55:53 +01:00
end
def hidden?
return self . state == 'project_hidden' || ( self . context . hidden? && ( self . state == 'active' || self . state == 'deferred' ) )
end
2009-01-21 13:36:26 +01:00
def update_state_from_project
2011-01-06 23:01:17 +01:00
if self . state == 'project_hidden' and ! self . project . hidden?
2009-11-04 22:45:38 -05:00
if self . uncompleted_predecessors . empty?
self . state = 'active'
2009-11-25 16:13:52 -05:00
else
self . state = 'pending'
2009-11-04 22:45:38 -05:00
end
2011-01-01 18:55:53 +01:00
elsif self . state == 'active' and self . project . hidden?
2009-01-21 13:36:26 +01:00
self . state = 'project_hidden'
end
2011-01-06 23:01:17 +01:00
self . save!
2009-01-21 13:36:26 +01:00
end
2011-08-18 17:15:00 +02:00
2009-01-21 13:36:26 +01:00
def toggle_completion!
2011-06-21 11:03:23 +02:00
return completed? ? activate! : complete!
2009-01-21 13:36:26 +01:00
end
2011-08-18 17:15:00 +02:00
2009-01-21 13:36:26 +01:00
def show_from
self [ :show_from ]
end
2011-08-18 17:15:00 +02:00
2009-01-21 13:36:26 +01:00
def show_from = ( date )
# parse Date objects into the proper timezone
date = user . at_midnight ( date ) if ( date . is_a? Date )
2011-06-12 04:29:35 +02:00
2011-08-18 17:15:00 +02:00
# show_from needs to be set before state_change because of "bug" in aasm.
2011-06-12 04:29:35 +02:00
# If show_from is not set, the todo will not validate and thus aasm will not save
2011-06-10 14:28:42 +02:00
# (see http://stackoverflow.com/questions/682920/persisting-the-state-column-on-transition-using-rubyist-aasm-acts-as-state-machi)
2011-08-18 17:15:00 +02:00
self [ :show_from ] = date
2011-06-10 14:28:42 +02:00
activate! if deferred? && date . blank?
defer! if active? && ! date . blank? && date > user . date
2009-01-21 13:36:26 +01:00
end
def self . feed_options ( user )
{
:title = > 'Tracks Actions' ,
:description = > " Actions for #{ user . display_name } "
}
end
2011-08-18 17:15:00 +02:00
2009-01-21 13:36:26 +01:00
def starred?
2011-11-16 19:36:09 +01:00
return has_tag? ( STARRED_TAG_NAME )
2009-01-21 13:36:26 +01:00
end
2011-08-18 17:15:00 +02:00
2009-01-21 13:36:26 +01:00
def toggle_star!
2011-08-04 23:14:29 +02:00
self . starred = ! starred?
2011-07-30 18:52:11 +02:00
end
def starred = ( starred )
if starred
2011-08-04 23:14:29 +02:00
_add_tags STARRED_TAG_NAME unless starred?
2011-07-30 18:52:11 +02:00
else
_remove_tags STARRED_TAG_NAME
end
starred
2009-01-21 13:36:26 +01:00
end
def from_recurring_todo?
return self . recurring_todo_id != nil
end
2009-08-20 09:54:22 +02:00
2009-11-04 22:39:19 -05:00
def add_predecessor_list ( predecessor_list )
2009-08-21 14:59:03 +02:00
return unless predecessor_list . kind_of? String
2011-02-12 10:38:15 +01:00
2011-11-16 19:36:09 +01:00
@predecessor_array = predecessor_list . split ( " , " ) . inject ( [ ] ) do | list , todo_id |
2011-02-12 10:38:15 +01:00
predecessor = self . user . todos . find_by_id ( todo_id . to_i ) unless todo_id . blank?
2011-11-16 19:36:09 +01:00
list << predecessor unless predecessor . nil?
list
2011-02-12 10:38:15 +01:00
end
2010-08-12 14:39:58 +02:00
return @predecessor_array
2009-11-04 22:39:19 -05:00
end
2011-08-18 17:15:00 +02:00
2009-06-30 23:51:41 +02:00
def add_predecessor ( t )
2011-10-10 22:25:51 +02:00
return if t . nil?
2011-11-16 16:37:04 +01:00
2011-02-12 10:38:15 +01:00
@predecessor_array = predecessors
2011-02-12 23:18:00 +01:00
@predecessor_array << t
2009-06-30 23:51:41 +02:00
end
2011-08-18 17:15:00 +02:00
2011-06-21 11:03:23 +02:00
# activate todos that should be activated if the current todo is completed
def activate_pending_todos
pending_todos = successors . find_all { | t | t . uncompleted_predecessors . empty? }
pending_todos . each { | t | t . activate! }
return pending_todos
2009-11-10 22:10:52 -05:00
end
2011-08-18 17:15:00 +02:00
2009-11-10 22:10:52 -05:00
# Return todos that should be blocked if the current todo is undone
2011-06-21 11:03:23 +02:00
def block_successors
active_successors = successors . find_all { | t | t . active? or t . deferred? }
active_successors . each { | t | t . block! }
return active_successors
2009-11-10 22:10:52 -05:00
end
2010-04-07 10:06:46 -04:00
def raw_notes = ( value )
self [ :notes ] = value
end
2011-11-16 16:37:04 +01:00
2011-10-10 22:25:51 +02:00
# XML API fixups
def predecessor_dependencies = ( params )
value = params [ :predecessor ]
2011-11-20 14:48:49 +01:00
return if value . nil?
2011-11-16 16:37:04 +01:00
2011-11-20 14:48:49 +01:00
# for multiple dependencies, value will be an array of id's, but for a single dependency,
# value will be a string. In that case convert to array
value = [ value ] unless value . class == Array
value . each { | ele | add_predecessor ( self . user . todos . find_by_id ( ele . to_i ) ) unless ele . blank? }
2011-10-10 22:25:51 +02:00
end
2011-11-16 16:37:04 +01:00
2011-10-10 22:25:51 +02:00
alias_method :original_context = , :context =
def context = ( value )
if value . is_a? Context
self . original_context = ( value )
else
2011-11-21 15:24:29 +01:00
c = Context . find_by_name ( value [ :name ] )
c = Context . create ( value ) if c . nil?
self . original_context = ( c )
2011-10-10 22:25:51 +02:00
end
end
2011-11-16 16:37:04 +01:00
2011-11-16 19:36:09 +01:00
alias_method :original_project , :project
def project
original_project . nil? ? Project . null_object : original_project
end
2011-10-10 22:25:51 +02:00
alias_method :original_project = , :project =
def project = ( value )
if value . is_a? Project
self . original_project = ( value )
2011-11-21 15:24:29 +01:00
elsif ! ( value . nil? || value . is_a? ( NullProject ) )
p = Project . find_by_name ( value [ :name ] )
p = Project . create ( value ) if p . nil?
self . original_project = ( p )
2011-11-16 19:36:09 +01:00
else
self . original_project = value
2011-10-10 22:25:51 +02:00
end
end
2011-11-16 16:37:04 +01:00
# used by the REST API. <tags> will also work, this is renamed to add_tags in TodosController::TodoCreateParamsHelper::initialize
def add_tags = ( params )
tag_with params [ :tag ] . inject ( [ ] ) { | list , value | list << value [ :name ] } unless params [ :tag ] . nil?
2011-10-10 22:25:51 +02:00
end
2011-08-18 17:15:00 +02:00
2011-11-16 16:37:04 +01:00
# Rich Todo API
2009-01-21 13:36:26 +01:00
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
2011-08-18 17:15:00 +02:00
2009-01-21 13:36:26 +01:00
context = nil if context == " "
project = nil if project == " "
context_id = default_context_id
unless ( context . nil? )
2011-05-01 12:48:32 +02:00
found_context = user . contexts . active . find_by_namepart ( context )
2009-01-21 13:36:26 +01:00
found_context = user . contexts . find_by_namepart ( context ) if found_context . nil?
context_id = found_context . id unless found_context . nil?
end
2011-08-18 17:15:00 +02:00
2009-01-21 13:36:26 +01:00
unless user . contexts . exists? context_id
raise ( CannotAccessContext , " Cannot access a context that does not belong to this user. " )
end
2011-08-18 17:15:00 +02:00
2009-01-21 13:36:26 +01:00
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
2011-05-01 12:48:32 +02:00
found_project = user . projects . active . find_by_namepart ( project )
2009-01-21 13:36:26 +01:00
found_project = user . projects . find_by_namepart ( project ) if found_project . nil?
end
project_id = found_project . id unless found_project . nil?
end
2011-08-18 17:15:00 +02:00
2009-01-21 13:36:26 +01:00
todo = user . todos . build
todo . description = description
2010-04-07 10:06:46 -04:00
todo . raw_notes = notes
2009-01-21 13:36:26 +01:00
todo . context_id = context_id
todo . project_id = project_id unless project_id . nil?
return todo
end
2011-08-18 17:15:00 +02:00
2009-01-21 13:36:26 +01:00
end