2018-11-03 15:57:14 -05:00
class Todo < ApplicationRecord
2013-07-23 01:28:43 -04:00
MAX_DESCRIPTION_LENGTH = 300
2020-10-10 02:27:42 +03:00
MAX_NOTES_LENGTH = 60_000
2013-07-21 13:37:35 -04:00
2011-05-01 12:48:32 +02:00
after_save :save_predecessors
2012-04-08 22:10:43 +02:00
# associations
2012-08-27 21:00:51 +02:00
belongs_to :context , :touch = > true
belongs_to :project , :touch = > true
2009-01-21 13:36:26 +01:00
belongs_to :user
belongs_to :recurring_todo
2011-08-18 17:15:00 +02:00
2012-04-08 22:10:43 +02:00
# Tag association
include IsTaggable
2013-04-29 15:24:32 -05:00
2012-04-08 22:10:43 +02:00
# Dependencies associations
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
2020-10-10 02:27:42 +03:00
has_many :uncompleted_predecessors , - > { where ( 'NOT (todos.state = ?)' , 'completed' ) } , :through = > :successor_dependencies ,
2013-05-11 23:13:32 +02:00
:source = > :predecessor
2020-10-10 02:27:42 +03:00
has_many :pending_successors , - > { where ( 'todos.state = ?' , 'pending' ) } , :through = > :predecessor_dependencies ,
2013-05-11 23:13:32 +02:00
:source = > :successor
2015-08-05 13:01:02 +02:00
has_many :attachments , dependent : :destroy
2013-04-29 15:24:32 -05:00
2011-05-01 12:48:32 +02:00
# scopes for states of this todo
2016-01-24 00:52:23 +01:00
scope :active , - > { active_or_hidden . not_hidden }
scope :active_or_hidden , - > { where state : 'active' }
scope :context_hidden , - > { joins ( 'INNER JOIN contexts c ON c.id = todos.context_id' ) . where ( 'c.state = ?' , 'hidden' ) }
scope :project_hidden , - > { joins ( 'LEFT OUTER JOIN projects p ON p.id = todos.project_id' ) . where ( 'p.state = ?' , 'hidden' ) }
scope :completed , - > { where 'todos.state = ?' , 'completed' }
scope :deferred , - > { where 'todos.state = ?' , 'deferred' }
2020-10-10 02:27:42 +03:00
scope :blocked , - > { where 'todos.state = ?' , 'pending' }
scope :pending , - > { where 'todos.state = ?' , 'pending' }
2016-01-24 00:52:23 +01:00
scope :deferred_or_blocked , - > { where '(todos.state = ?) OR (todos.state = ?)' , 'deferred' , 'pending' }
2013-05-11 23:13:32 +02:00
scope :hidden , - > {
2020-10-10 02:27:42 +03:00
joins ( 'INNER JOIN contexts c_hidden ON c_hidden.id = todos.context_id' )
. joins ( 'LEFT OUTER JOIN projects p_hidden ON p_hidden.id = todos.project_id' )
. where ( '(c_hidden.state = ? OR p_hidden.state = ?)' , 'hidden' , 'hidden' )
. where ( 'NOT todos.state = ?' , 'completed' ) }
2016-01-24 00:52:23 +01:00
scope :not_hidden , - > { not_context_hidden . not_project_hidden }
scope :not_deferred_or_blocked , - > { where '(NOT todos.state=?) AND (NOT todos.state = ?)' , 'deferred' , 'pending' }
scope :not_project_hidden , - > { joins ( 'LEFT OUTER JOIN projects p ON p.id = todos.project_id' ) . where ( 'p.id IS NULL OR NOT(p.state = ?)' , 'hidden' ) }
scope :not_context_hidden , - > { joins ( 'INNER JOIN contexts c ON c.id = todos.context_id' ) . where ( 'NOT(c.state = ?)' , 'hidden' ) }
scope :not_completed , - > { where 'NOT (todos.state = ?)' , 'completed' }
2009-01-21 13:36:26 +01:00
2011-05-01 12:48:32 +02:00
# other scopes
2013-05-11 23:13:32 +02:00
scope :are_due , - > { where 'NOT (todos.due IS NULL)' }
scope :due_today , - > { where ( " todos.due <= ? " , Time . zone . now ) }
2014-12-19 19:09:37 +00:00
scope :with_tag , lambda { | tag_id | joins ( " INNER JOIN taggings ON todos.id = taggings.taggable_id " ) . where ( " taggings.tag_id = ? AND taggings.taggable_type='Todo' " , tag_id ) }
2020-10-10 02:27:42 +03:00
scope :with_tags , lambda { | tag_ids | where ( " EXISTS(SELECT * FROM taggings t WHERE t.tag_id IN (?) AND t.taggable_id=todos.id AND t.taggable_type='Todo') " , tag_ids ) }
2013-05-11 23:13:32 +02:00
scope :completed_after , lambda { | date | where ( " todos.completed_at > ? " , date ) }
scope :completed_before , lambda { | date | where ( " todos.completed_at < ? " , date ) }
scope :created_after , lambda { | date | where ( " todos.created_at > ? " , date ) }
scope :created_before , lambda { | date | where ( " todos.created_at < ? " , date ) }
2020-10-10 02:27:42 +03:00
scope :created_or_completed_after , lambda { | date | where ( " todos.created_at > ? OR todos.completed_at > ? " , date , date ) }
2013-04-29 16:35:50 -05:00
def self . due_after ( date )
where ( 'todos.due > ?' , date )
end
def self . due_between ( start_date , end_date )
where ( 'todos.due > ? AND todos.due <= ?' , start_date , end_date )
end
2020-10-10 13:58:13 +03:00
STARRED_TAG_NAME = " starred " . freeze
DEFAULT_INCLUDES = [ :project , :context , :tags , :taggings , :pending_successors , :uncompleted_predecessors , :recurring_todo ] . freeze
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
2020-10-27 21:39:19 +02:00
aasm_initial_state = Proc . new { ( show_from && user && ( show_from > user . date ) ) ? :deferred : :active }
2011-08-18 17:15:00 +02:00
2013-04-29 15:24:32 -05:00
aasm :column = > :state do
2014-08-14 21:05:05 -05:00
state :active
2020-10-10 02:27:42 +03:00
state :completed , :before_enter = > Proc . new { self . completed_at = Time . zone . now } , :before_exit = > Proc . new { self . completed_at = nil }
2020-08-25 17:33:21 +03:00
state :deferred , :before_exit = > Proc . new { self [ :show_from ] = nil }
2013-04-29 11:53:32 +02:00
state :pending
2011-08-18 17:15:00 +02:00
2013-04-29 11:53:32 +02:00
event :defer do
transitions :to = > :deferred , :from = > [ :active ]
end
2011-08-18 17:15:00 +02:00
2013-04-29 11:53:32 +02:00
event :complete do
2016-01-24 00:52:23 +01:00
transitions :to = > :completed , :from = > [ :active , :deferred , :pending ]
2013-04-29 11:53:32 +02:00
end
2011-08-18 17:15:00 +02:00
2013-04-29 11:53:32 +02:00
event :activate do
2016-01-24 00:52:23 +01:00
transitions :to = > :active , :from = > [ :deferred ]
2013-04-29 11:53:32 +02:00
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?
transitions :to = > :deferred , :from = > [ :pending ] , :guard = > :no_uncompleted_predecessors?
end
2011-08-18 17:15:00 +02:00
2013-04-29 11:53:32 +02:00
event :block do
2016-01-24 00:52:23 +01:00
transitions :to = > :pending , :from = > [ :active , :deferred ]
2013-04-29 11:53:32 +02:00
end
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
# 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)
2020-10-27 21:39:19 +02:00
validates :description , presence : true , length : { maximum : MAX_DESCRIPTION_LENGTH }
validates :notes , length : { maximum : MAX_NOTES_LENGTH , allow_nil : true }
validates :show_from , presence : true , if : :deferred?
validates :context , presence : true
2012-04-17 16:47:37 +02:00
validate :check_show_from_in_future
2011-08-18 17:15:00 +02:00
2012-04-17 16:47:37 +02:00
def check_show_from_in_future
2012-08-26 17:33:51 +02:00
if show_from_changed? # only check on change of show_from
2013-09-13 15:19:25 +03:00
if show_from . present? && ( show_from < user . date )
2012-08-26 17:33:51 +02:00
errors . add ( " show_from " , I18n . t ( 'models.todo.error_date_must_be_future' ) )
2012-04-17 16:47:37 +02:00
end
end
end
2013-04-29 15:24:32 -05: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?
2020-10-10 02:27:42 +03:00
no_deferral = show_from . blank? || 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?
2013-05-11 23:13:32 +02:00
return ! uncompleted_predecessors . empty?
2011-08-18 17:15:00 +02:00
end
2013-06-04 10:07:02 +02:00
def guard_for_transition_from_deferred_to_pending
no_uncompleted_predecessors? && not_part_of_hidden_container?
end
def not_part_of_hidden_container?
2020-10-27 21:39:19 +02:00
! ( ( project && project . hidden? ) || context . hidden? )
2013-06-04 10:07:02 +02:00
end
2009-08-20 09:54:22 +02:00
# Returns a string with description <context, project>
def specification
2020-10-27 21:39:19 +02:00
project_name = project . is_a? ( NullProject ) ? " (none) " : project . name
return " \' #{ description } \' < \' #{ context . title } \' ; \' #{ project_name } \' > "
2009-08-20 09:54:22 +02:00
end
2011-08-18 17:15:00 +02:00
2009-06-30 23:17:33 +02:00
def save_predecessors
2020-10-10 13:58:13 +03:00
unless @predecessor_array . nil? # Only save predecessors if they changed
2020-10-27 21:39:19 +02:00
current_array = 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
2020-10-27 21:39:19 +02:00
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?
2020-10-27 21:39:19 +02:00
predecessors << todo unless 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
2013-02-17 17:37:15 +01:00
def touch_predecessors
2020-10-27 21:39:19 +02:00
touch
2015-08-19 14:42:42 +02:00
predecessors . each ( & :touch_predecessors )
2013-02-17 17:37:15 +01:00
end
2011-02-03 16:59:59 +01:00
def removed_predecessors
return @removed_predecessors
2009-06-30 23:17:33 +02:00
end
2011-08-18 17:15:00 +02:00
2012-04-08 14:52:44 +02:00
# remove predecessor and activate myself if it was the last predecessor
2009-11-10 22:15:16 -05:00
def remove_predecessor ( predecessor )
2020-10-27 21:39:19 +02:00
predecessors . delete ( predecessor )
if predecessors . empty?
reload # reload predecessors
activate!
2012-04-08 14:52:44 +02:00
else
save!
end
2009-11-10 22:15:16 -05:00
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
2020-10-27 21:39:19 +02:00
elsif successors . empty?
2009-06-30 23:17:33 +02:00
return false
else
2020-10-27 21:39:19 +02:00
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-01-01 18:55:53 +01:00
def hidden?
2020-10-27 21:39:19 +02:00
project . hidden? || context . hidden?
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 )
2013-05-03 19:28:26 +02:00
if deferred? && date . blank?
activate
else
# parse Date objects into the proper timezone
2020-10-27 21:39:19 +02:00
date = date . in_time_zone . beginning_of_day if date . is_a? Date
2011-06-12 04:29:35 +02:00
2013-05-03 19:28:26 +02:00
# show_from needs to be set before state_change because of "bug" in aasm.
# If show_from is not set, the todo will not validate and thus aasm will not save
# (see http://stackoverflow.com/questions/682920/persisting-the-state-column-on-transition-using-rubyist-aasm-acts-as-state-machi)
self [ :show_from ] = date
2011-06-10 14:28:42 +02:00
2013-09-13 15:19:25 +03:00
defer if active? && date . present? && show_from > user . date
2013-05-03 19:28:26 +02:00
end
2009-01-21 13:36:26 +01:00
end
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!
2020-10-10 13:58:13 +03: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?
2020-10-27 21:39:19 +02:00
return recurring_todo_id != nil
2009-01-21 13:36:26 +01:00
end
2009-08-20 09:54:22 +02:00
2009-11-04 22:39:19 -05:00
def add_predecessor_list ( predecessor_list )
2020-10-27 21:39:19 +02:00
return unless predecessor_list . is_a? String
2011-02-12 10:38:15 +01:00
2020-10-10 02:27:42 +03:00
@predecessor_array = predecessor_list . split ( " , " ) . inject ( [ ] ) do | list , todo_id |
2020-10-27 21:39:19 +02:00
predecessor = user . todos . find ( todo_id . to_i ) if todo_id . present?
2020-10-10 02:27:42 +03:00
list << predecessor unless predecessor . nil?
2011-11-16 19:36:09 +01:00
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
2020-10-10 02:27:42 +03:00
pending_todos = successors . select { | t | t . uncompleted_predecessors . empty? && ! t . completed? }
pending_todos . each { | t | t . activate! }
2011-06-21 11:03:23 +02:00
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
2020-10-10 02:27:42 +03:00
active_successors = successors . select { | t | t . active? || t . deferred? }
active_successors . each { | t | t . block! }
2011-06-21 11:03:23 +02:00
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 )
2012-04-08 16:01:29 +02:00
deps = params [ :predecessor ]
return if deps . 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
2012-04-08 16:01:29 +02:00
deps = [ deps ] unless deps . class == Array
2011-11-20 14:48:49 +01:00
2020-10-27 21:39:19 +02:00
deps . each { | dep | add_predecessor ( user . todos . find ( dep . to_i ) ) if dep . present? }
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
2020-10-10 13:58:13 +03:00
self . original_context = ( value )
2011-10-10 22:25:51 +02:00
else
2013-02-27 11:50:49 +01:00
c = Context . where ( :name = > value [ :name ] ) . first
2011-11-21 15:24:29 +01:00
c = Context . create ( value ) if c . nil?
2020-10-10 02:27:42 +03:00
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
2020-10-10 02:27:42 +03:00
self . original_project = ( value )
2011-11-21 15:24:29 +01:00
elsif ! ( value . nil? || value . is_a? ( NullProject ) )
2013-02-27 11:50:49 +01:00
p = Project . where ( :name = > value [ :name ] ) . first
2011-11-21 15:24:29 +01:00
p = Project . create ( value ) if p . nil?
2020-10-10 02:27:42 +03:00
self . original_project = ( p )
2011-11-16 19:36:09 +01:00
else
2020-10-10 02:27:42 +03:00
self . original_project = value
2011-10-10 22:25:51 +02:00
end
end
2011-11-16 16:37:04 +01:00
2013-05-03 21:54:03 +02:00
def has_project?
return ! ( project_id . nil? || project . is_a? ( NullProject ) )
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 )
2012-03-19 20:04:56 +01:00
unless params [ :tag ] . nil?
tag_list = params [ :tag ] . inject ( [ ] ) { | list , value | list << value [ :name ] }
tag_with tag_list . join ( " , " )
end
2011-10-10 22:25:51 +02:00
end
2011-08-18 17:15:00 +02:00
2013-09-16 11:37:16 +02:00
def self . import ( filename , params , user )
default_context = user . contexts . order ( 'id' ) . first
2022-02-09 11:48:09 +02:00
return false if default_context . nil?
2014-08-14 21:05:05 -05:00
2013-07-21 13:37:35 -04:00
count = 0
2013-09-16 11:37:16 +02:00
CSV . foreach ( filename , headers : true ) do | row |
2013-07-21 13:37:35 -04:00
unless find_by_description_and_user_id row [ params [ :description ] . to_i ] , user . id
2014-08-14 21:05:05 -05:00
todo = new
2013-07-21 13:37:35 -04:00
todo . user = user
2013-07-23 01:52:47 -04:00
todo . description = row [ params [ :description ] . to_i ] . truncate MAX_DESCRIPTION_LENGTH
2013-07-21 13:37:35 -04:00
todo . context = Context . find_by_name_and_user_id ( row [ params [ :context ] . to_i ] , user . id ) || default_context
todo . project = Project . find_by_name_and_user_id ( row [ params [ :project ] . to_i ] , user . id ) if row [ params [ :project ] . to_i ] . present?
todo . state = row [ params [ :completed_at ] . to_i ] . present? ? 'completed' : 'active'
todo . notes = row [ params [ :notes ] . to_i ] . truncate MAX_NOTES_LENGTH if row [ params [ :notes ] . to_i ] . present?
todo . created_at = row [ params [ :created_at ] . to_i ] if row [ params [ :created_at ] . to_i ] . present?
2014-08-14 21:05:05 -05:00
todo . due = row [ params [ :due ] . to_i ]
2013-07-21 13:37:35 -04:00
todo . completed_at = row [ params [ :completed_at ] . to_i ] if row [ params [ :completed_at ] . to_i ] . present?
todo . save!
count += 1
end
end
count
2013-07-23 01:28:43 -04:00
end
2013-07-21 13:37:35 -04:00
2015-08-09 13:47:17 +02:00
def destroy
# activate successors if they only depend on this action
2020-10-27 21:39:19 +02:00
pending_successors . each do | successor |
2015-08-09 13:47:17 +02:00
successor . uncompleted_predecessors . delete ( self )
if successor . uncompleted_predecessors . empty?
successor . activate!
end
end
super
end
2009-01-21 13:36:26 +01:00
end