diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 0b38d202..1bf4eb13 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -1,287 +1,287 @@
-module TodosHelper
-
- # #require 'users_controller' Counts the number of incomplete items in the
- # specified context
- #
- def count_items(context)
- count = Todo.find_all("done=0 AND context_id=#{context.id}").length
- end
-
- def form_remote_tag_edit_todo( &block )
- form_tag(
- todo_path(@todo), {
- :method => :put,
- :id => dom_id(@todo, 'form'),
- :class => dom_id(@todo, 'form') + " inline-form edit_todo_form" },
- &block )
- apply_behavior 'form.edit_todo_form', make_remote_form(
- :method => :put,
- :before => "this.down('button.positive').startWaiting()",
- :loaded => "this.down('button.positive').stopWaiting()",
- :condition => "!(this.down('button.positive').isWaiting())"),
- :prevent_default => true
- end
-
- def set_behavior_for_delete_icon
- parameters = "_source_view=#{@source_view}"
- parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
- apply_behavior '.item-container a.delete_icon:click', :prevent_default => true do |page|
- page.confirming "'Are you sure that you want to ' + this.title + '?'" do
- page << "itemContainer = this.up('.item-container'); itemContainer.startWaiting();"
- page << remote_to_href(:method => 'delete', :with => "'#{parameters}'", :complete => "itemContainer.stopWaiting();")
- end
- end
- end
-
- def remote_delete_icon
- str = link_to( image_tag_for_delete,
- todo_path(@todo), :id => "delete_icon_"+@todo.id.to_s,
- :class => "icon delete_icon", :title => "delete the action '#{@todo.description}'")
- set_behavior_for_delete_icon
- str
- end
-
- def set_behavior_for_star_icon
- apply_behavior '.item-container a.star_item:click',
- remote_to_href(:method => 'put', :with => "{ _source_view : '#{@source_view}' }"),
- :prevent_default => true
- end
-
- def remote_star_icon
- str = link_to( image_tag_for_star(@todo),
- toggle_star_todo_path(@todo),
- :class => "icon star_item", :title => "star the action '#{@todo.description}'")
- set_behavior_for_star_icon
- str
- end
-
- def set_behavior_for_edit_icon
- parameters = "_source_view=#{@source_view}"
- parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
- apply_behavior '.item-container a.edit_icon:click', :prevent_default => true do |page|
- page << "Effect.Pulsate(this);"
- page << remote_to_href(:method => 'get', :with => "'#{parameters}'")
- end
- end
-
- def remote_edit_icon
- if !@todo.completed?
- str = link_to( image_tag_for_edit(@todo),
- edit_todo_path(@todo),
- :class => "icon edit_icon")
- set_behavior_for_edit_icon
- else
- str = '' + image_tag("blank.png") + " "
- end
- str
- end
-
- def set_behavior_for_toggle_checkbox
- parameters = "_source_view=#{@source_view}"
- parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
- apply_behavior '.item-container input.item-checkbox:click',
- remote_function(:url => javascript_variable('this.value'), :method => 'put',
- :with => "'#{parameters}'")
- end
-
- def remote_toggle_checkbox
- str = check_box_tag('item_id', toggle_check_todo_path(@todo), @todo.completed?, :class => 'item-checkbox')
- set_behavior_for_toggle_checkbox
- str
- end
-
- def date_span
- if @todo.completed?
- "#{format_date( @todo.completed_at )}"
- elsif @todo.deferred?
- show_date( @todo.show_from )
- else
- due_date( @todo.due )
- end
- end
-
- def tag_list_text
- @todo.tags.collect{|t| t.name}.join(', ')
- end
-
- def tag_list
- tags_except_starred = @todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME}
- tag_list = tags_except_starred.collect{|t| "" + link_to(t.name, :controller => "todos", :action => "tag", :id => t.name) + ""}.join('')
- "#{tag_list}"
- end
-
- def tag_list_mobile
- tags_except_starred = @todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME}
- # removed the link. TODO: add link to mobile view of tagged actions
- tag_list = tags_except_starred.collect{|t|
- "" +
- link_to(t.name, {:action => "tag", :controller => "todos", :id => t.name+".m"}) +
- ""}.join('')
- if tag_list.empty? then "" else "#{tag_list}" end
- end
-
- def deferred_due_date
- if @todo.deferred? && @todo.due
- "(action due on #{format_date(@todo.due)})"
- end
- end
-
- def project_and_context_links(parent_container_type, opts = {})
- str = ''
- if @todo.completed?
- str += @todo.context.name unless opts[:suppress_context]
- should_suppress_project = opts[:suppress_project] || @todo.project.nil?
- str += ", " unless str.blank? || should_suppress_project
- str += @todo.project.name unless should_suppress_project
- str = "(#{str})" unless str.blank?
- else
- if (['project', 'tag', 'stats', 'search'].include?(parent_container_type))
- str << item_link_to_context( @todo )
- end
- if (['context', 'tickler', 'tag', 'stats', 'search'].include?(parent_container_type)) && @todo.project_id
- str << item_link_to_project( @todo )
- end
- end
- return str
- end
-
- # Uses the 'staleness_starts' value from settings.yml (in days) to colour the
- # background of the action appropriately according to the age of the creation
- # date:
- # * l1: created more than 1 x staleness_starts, but < 2 x staleness_starts
- # * l2: created more than 2 x staleness_starts, but < 3 x staleness_starts
- # * l3: created more than 3 x staleness_starts
- #
- def staleness_class(item)
- if item.due || item.completed?
- return ""
- elsif item.created_at < user_time - (prefs.staleness_starts * 3).days
- return " stale_l3"
- elsif item.created_at < user_time - (prefs.staleness_starts * 2).days
- return " stale_l2"
- elsif item.created_at < user_time - (prefs.staleness_starts).days
- return " stale_l1"
- else
- return ""
- end
- end
-
- # Check show_from date in comparison to today's date Flag up date
- # appropriately with a 'traffic light' colour code
- #
- def show_date(d)
- if d == nil
- return ""
- end
-
- days = days_from_today(d)
-
- case days
- # overdue or due very soon! sound the alarm!
- when -1000..-1
- "Scheduled to show " + (days * -1).to_s + " days ago "
- when 0
- "Show Today "
- when 1
- "Show Tomorrow "
- # due 2-7 days away
- when 2..7
- if prefs.due_style == Preference.due_styles[:due_on]
- "Show on " + d.strftime("%A") + " "
- else
- "Show in " + days.to_s + " days "
- end
- # more than a week away - relax
- else
- "Show in " + days.to_s + " days "
- end
- end
-
- def calendar_setup( input_field )
- str = "Calendar.setup({ ifFormat:\"#{prefs.date_format}\""
- str << ",firstDay:#{prefs.week_starts},showOthers:true,range:[2004, 2010]"
- str << ",step:1,inputField:\"" + input_field + "\",cache:true,align:\"TR\" })\n"
- javascript_tag str
- end
-
- def item_container_id (todo)
- if source_view_is :project
- return "p#{todo.project_id}" if todo.active?
- return "tickler" if todo.deferred?
- end
- return "c#{todo.context_id}"
- end
-
- def should_show_new_item
-
- if @todo.project.nil? == false
- # do not show new actions that were added to hidden or completed projects
- # on home page and context page
- return false if source_view_is(:todo) && (@todo.project.hidden? || @todo.project.completed?)
- return false if source_view_is(:context) && (@todo.project.hidden? || @todo.project.completed?)
- end
-
- return true if source_view_is(:deferred) && @todo.deferred?
- return true if source_view_is(:project) && @todo.project.hidden? && @todo.project_hidden?
- return true if source_view_is(:project) && @todo.deferred?
- return true if !source_view_is(:deferred) && @todo.active?
- return false
- end
-
- def parent_container_type
- return 'tickler' if source_view_is :deferred
- return 'project' if source_view_is :project
- return 'stats' if source_view_is :stats
- return 'context'
- end
-
- def empty_container_msg_div_id
- return "tickler-empty-nd" if source_view_is(:project) && @todo.deferred?
- return "p#{@todo.project_id}empty-nd" if source_view_is :project
- return "c#{@todo.context_id}empty-nd"
- end
-
- def project_names_for_autocomplete
- array_or_string_for_javascript( ['None'] + current_user.projects.active.collect{|p| escape_javascript(p.name) } )
- end
-
- def context_names_for_autocomplete
- # #return array_or_string_for_javascript(['Create a new context']) if
- # @contexts.empty?
- array_or_string_for_javascript( current_user.contexts.collect{|c| escape_javascript(c.name) } )
- end
-
- def format_ical_notes(notes)
- split_notes = notes.split(/\n/)
- joined_notes = split_notes.join("\\n")
- end
-
- def formatted_pagination(total)
- s = will_paginate(@todos)
- (s.gsub /(<\/[^<]+>)/, '\1 ').chomp(' ')
- end
-
- def date_field_tag(name, id, value = nil, options = {})
- text_field_tag name, value, {"size" => 12, "id" => id, "class" => "Date", "onfocus" => "Calendar.setup", "autocomplete" => "off"}.update(options.stringify_keys)
- end
-
- private
-
- def image_tag_for_delete
- image_tag("blank.png", :title =>"Delete action", :class=>"delete_item")
- end
-
- def image_tag_for_edit(todo)
- image_tag("blank.png", :title =>"Edit action", :class=>"edit_item", :id=> dom_id(todo, 'edit_icon'))
- end
-
- def image_tag_for_star(todo)
- class_str = todo.starred? ? "starred_todo" : "unstarred_todo"
- image_tag("blank.png", :title =>"Star action", :class => class_str)
- end
-
- def defer_link(days)
- link_to_remote image_tag("defer_#{days}.png", :alt => "Defer #{pluralize(days, 'day')}"), :url => {:controller => 'todos', :action => 'defer', :id => @todo.id, :days => days, :_source_view => (@source_view.underscore.gsub(/\s+/,'_') rescue "")}
- end
-
-end
+module TodosHelper
+
+ # #require 'users_controller' Counts the number of incomplete items in the
+ # specified context
+ #
+ def count_items(context)
+ count = Todo.find_all("done=0 AND context_id=#{context.id}").length
+ end
+
+ def form_remote_tag_edit_todo( &block )
+ form_tag(
+ todo_path(@todo), {
+ :method => :put,
+ :id => dom_id(@todo, 'form'),
+ :class => dom_id(@todo, 'form') + " inline-form edit_todo_form" },
+ &block )
+ apply_behavior 'form.edit_todo_form', make_remote_form(
+ :method => :put,
+ :before => "this.down('button.positive').startWaiting()",
+ :loaded => "this.down('button.positive').stopWaiting()",
+ :condition => "!(this.down('button.positive').isWaiting())"),
+ :prevent_default => true
+ end
+
+ def set_behavior_for_delete_icon
+ parameters = "_source_view=#{@source_view}"
+ parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
+ apply_behavior '.item-container a.delete_icon:click', :prevent_default => true do |page|
+ page.confirming "'Are you sure that you want to ' + this.title + '?'" do
+ page << "itemContainer = this.up('.item-container'); itemContainer.startWaiting();"
+ page << remote_to_href(:method => 'delete', :with => "'#{parameters}'", :complete => "itemContainer.stopWaiting();")
+ end
+ end
+ end
+
+ def remote_delete_icon
+ str = link_to( image_tag_for_delete,
+ todo_path(@todo), :id => "delete_icon_"+@todo.id.to_s,
+ :class => "icon delete_icon", :title => "delete the action '#{@todo.description}'")
+ set_behavior_for_delete_icon
+ str
+ end
+
+ def set_behavior_for_star_icon
+ apply_behavior '.item-container a.star_item:click',
+ remote_to_href(:method => 'put', :with => "{ _source_view : '#{@source_view}' }"),
+ :prevent_default => true
+ end
+
+ def remote_star_icon
+ str = link_to( image_tag_for_star(@todo),
+ toggle_star_todo_path(@todo),
+ :class => "icon star_item", :title => "star the action '#{@todo.description}'")
+ set_behavior_for_star_icon
+ str
+ end
+
+ def set_behavior_for_edit_icon
+ parameters = "_source_view=#{@source_view}"
+ parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
+ apply_behavior '.item-container a.edit_icon:click', :prevent_default => true do |page|
+ page << "Effect.Pulsate(this);"
+ page << remote_to_href(:method => 'get', :with => "'#{parameters}'")
+ end
+ end
+
+ def remote_edit_icon
+ if !@todo.completed?
+ str = link_to( image_tag_for_edit(@todo),
+ edit_todo_path(@todo),
+ :class => "icon edit_icon")
+ set_behavior_for_edit_icon
+ else
+ str = '' + image_tag("blank.png") + " "
+ end
+ str
+ end
+
+ def set_behavior_for_toggle_checkbox
+ parameters = "_source_view=#{@source_view}"
+ parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
+ apply_behavior '.item-container input.item-checkbox:click',
+ remote_function(:url => javascript_variable('this.value'), :method => 'put',
+ :with => "'#{parameters}'")
+ end
+
+ def remote_toggle_checkbox
+ str = check_box_tag('item_id', toggle_check_todo_path(@todo), @todo.completed?, :class => 'item-checkbox')
+ set_behavior_for_toggle_checkbox
+ str
+ end
+
+ def date_span
+ if @todo.completed?
+ "#{format_date( @todo.completed_at )}"
+ elsif @todo.deferred?
+ show_date( @todo.show_from )
+ else
+ due_date( @todo.due )
+ end
+ end
+
+ def tag_list_text
+ @todo.tags.collect{|t| t.name}.join(', ')
+ end
+
+ def tag_list
+ tags_except_starred = @todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME}
+ tag_list = tags_except_starred.collect{|t| "" + link_to(t.name, :controller => "todos", :action => "tag", :id => t.name) + ""}.join('')
+ "#{tag_list}"
+ end
+
+ def tag_list_mobile
+ tags_except_starred = @todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME}
+ # removed the link. TODO: add link to mobile view of tagged actions
+ tag_list = tags_except_starred.collect{|t|
+ "" +
+ link_to(t.name, {:action => "tag", :controller => "todos", :id => t.name+".m"}) +
+ ""}.join('')
+ if tag_list.empty? then "" else "#{tag_list}" end
+ end
+
+ def deferred_due_date
+ if @todo.deferred? && @todo.due
+ "(action due on #{format_date(@todo.due)})"
+ end
+ end
+
+ def project_and_context_links(parent_container_type, opts = {})
+ str = ''
+ if @todo.completed?
+ str += @todo.context.name unless opts[:suppress_context]
+ should_suppress_project = opts[:suppress_project] || @todo.project.nil?
+ str += ", " unless str.blank? || should_suppress_project
+ str += @todo.project.name unless should_suppress_project
+ str = "(#{str})" unless str.blank?
+ else
+ if (['project', 'tag', 'stats', 'search'].include?(parent_container_type))
+ str << item_link_to_context( @todo )
+ end
+ if (['context', 'tickler', 'tag', 'stats', 'search'].include?(parent_container_type)) && @todo.project_id
+ str << item_link_to_project( @todo )
+ end
+ end
+ return str
+ end
+
+ # Uses the 'staleness_starts' value from settings.yml (in days) to colour the
+ # background of the action appropriately according to the age of the creation
+ # date:
+ # * l1: created more than 1 x staleness_starts, but < 2 x staleness_starts
+ # * l2: created more than 2 x staleness_starts, but < 3 x staleness_starts
+ # * l3: created more than 3 x staleness_starts
+ #
+ def staleness_class(item)
+ if item.due || item.completed?
+ return ""
+ elsif item.created_at < user_time - (prefs.staleness_starts * 3).days
+ return " stale_l3"
+ elsif item.created_at < user_time - (prefs.staleness_starts * 2).days
+ return " stale_l2"
+ elsif item.created_at < user_time - (prefs.staleness_starts).days
+ return " stale_l1"
+ else
+ return ""
+ end
+ end
+
+ # Check show_from date in comparison to today's date Flag up date
+ # appropriately with a 'traffic light' colour code
+ #
+ def show_date(d)
+ if d == nil
+ return ""
+ end
+
+ days = days_from_today(d)
+
+ case days
+ # overdue or due very soon! sound the alarm!
+ when -1000..-1
+ "Scheduled to show " + (days * -1).to_s + " days ago "
+ when 0
+ "Show Today "
+ when 1
+ "Show Tomorrow "
+ # due 2-7 days away
+ when 2..7
+ if prefs.due_style == Preference.due_styles[:due_on]
+ "Show on " + d.strftime("%A") + " "
+ else
+ "Show in " + days.to_s + " days "
+ end
+ # more than a week away - relax
+ else
+ "Show in " + days.to_s + " days "
+ end
+ end
+
+ def calendar_setup( input_field )
+ str = "Calendar.setup({ ifFormat:\"#{prefs.date_format}\""
+ str << ",firstDay:#{prefs.week_starts},showOthers:true,range:[2004, 2010]"
+ str << ",step:1,inputField:\"" + input_field + "\",cache:true,align:\"TR\" })\n"
+ javascript_tag str
+ end
+
+ def item_container_id (todo)
+ if source_view_is :project
+ return "p#{todo.project_id}" if todo.active?
+ return "tickler" if todo.deferred?
+ end
+ return "c#{todo.context_id}"
+ end
+
+ def should_show_new_item
+
+ if @todo.project.nil? == false
+ # do not show new actions that were added to hidden or completed projects
+ # on home page and context page
+ return false if source_view_is(:todo) && (@todo.project.hidden? || @todo.project.completed?)
+ return false if source_view_is(:context) && (@todo.project.hidden? || @todo.project.completed?)
+ end
+
+ return true if source_view_is(:deferred) && @todo.deferred?
+ return true if source_view_is(:project) && @todo.project.hidden? && @todo.project_hidden?
+ return true if source_view_is(:project) && @todo.deferred?
+ return true if !source_view_is(:deferred) && @todo.active?
+ return false
+ end
+
+ def parent_container_type
+ return 'tickler' if source_view_is :deferred
+ return 'project' if source_view_is :project
+ return 'stats' if source_view_is :stats
+ return 'context'
+ end
+
+ def empty_container_msg_div_id
+ return "tickler-empty-nd" if source_view_is(:project) && @todo.deferred?
+ return "p#{@todo.project_id}empty-nd" if source_view_is :project
+ return "c#{@todo.context_id}empty-nd"
+ end
+
+ def project_names_for_autocomplete
+ array_or_string_for_javascript( ['None'] + current_user.projects.active.collect{|p| escape_javascript(p.name) } )
+ end
+
+ def context_names_for_autocomplete
+ # #return array_or_string_for_javascript(['Create a new context']) if
+ # @contexts.empty?
+ array_or_string_for_javascript( current_user.contexts.collect{|c| escape_javascript(c.name) } )
+ end
+
+ def format_ical_notes(notes)
+ split_notes = notes.split(/\n/)
+ joined_notes = split_notes.join("\\n")
+ end
+
+ def formatted_pagination(total)
+ s = will_paginate(@todos)
+ (s.gsub(/(<\/[^<]+>)/, '\1 ')).chomp(' ')
+ end
+
+ def date_field_tag(name, id, value = nil, options = {})
+ text_field_tag name, value, {"size" => 12, "id" => id, "class" => "Date", "onfocus" => "Calendar.setup", "autocomplete" => "off"}.update(options.stringify_keys)
+ end
+
+ private
+
+ def image_tag_for_delete
+ image_tag("blank.png", :title =>"Delete action", :class=>"delete_item")
+ end
+
+ def image_tag_for_edit(todo)
+ image_tag("blank.png", :title =>"Edit action", :class=>"edit_item", :id=> dom_id(todo, 'edit_icon'))
+ end
+
+ def image_tag_for_star(todo)
+ class_str = todo.starred? ? "starred_todo" : "unstarred_todo"
+ image_tag("blank.png", :title =>"Star action", :class => class_str)
+ end
+
+ def defer_link(days)
+ link_to_remote image_tag("defer_#{days}.png", :alt => "Defer #{pluralize(days, 'day')}"), :url => {:controller => 'todos', :action => 'defer', :id => @todo.id, :days => days, :_source_view => (@source_view.underscore.gsub(/\s+/,'_') rescue "")}
+ end
+
+end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 5aed02be..fb5539ac 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -1,176 +1,176 @@
-class Todo < ActiveRecord::Base
-
- belongs_to :context
- belongs_to :project
- belongs_to :user
- belongs_to :recurring_todo
-
- named_scope :active, :conditions => { :state => 'active' }
- named_scope :not_completed, :conditions => ['NOT state = ? ', 'completed']
- named_scope :are_due, :conditions => ['NOT todos.due IS NULL']
-
- STARRED_TAG_NAME = "starred"
-
- acts_as_state_machine :initial => :active, :column => 'state'
-
- # when entering active state, also remove completed_at date. Looks like :exit
- # of state completed is not run, see #679
- state :active, :enter => Proc.new { |t| t[:show_from], t.completed_at = nil, nil }
- state :project_hidden
- state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil }
- state :deferred
-
- event :defer do
- transitions :to => :deferred, :from => [:active]
- end
-
- event :complete do
- transitions :to => :completed, :from => [:active, :project_hidden, :deferred]
- end
-
- event :activate do
- transitions :to => :active, :from => [:project_hidden, :completed, :deferred]
- end
-
- event :hide do
- transitions :to => :project_hidden, :from => [:active, :deferred]
- end
-
- event :unhide do
- transitions :to => :deferred, :from => [:project_hidden], :guard => Proc.new{|t| !t.show_from.blank? }
- transitions :to => :active, :from => [:project_hidden]
- end
-
- 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
- validates_length_of :notes, :maximum => 60000, :allow_nil => true
- validates_presence_of :show_from, :if => :deferred?
- validates_presence_of :context
-
- def validate
- if !show_from.blank? && show_from < user.date
- errors.add("show_from", "must be a date in the future")
- end
- end
-
- def toggle_completion!
- saved = false
- if completed?
- saved = activate!
- else
- saved = complete!
- end
- return saved
- end
-
- def show_from
- self[:show_from]
- end
-
- def show_from=(date)
- # parse Date objects into the proper timezone
- date = user.at_midnight(date) if (date.is_a? Date)
- activate! if deferred? && date.blank?
- defer! if active? && !date.blank? && date > user.date
- self[:show_from] = date
- end
-
- alias_method :original_project, :project
-
- def project
- original_project.nil? ? Project.null_object : original_project
- end
-
- alias_method :original_set_initial_state, :set_initial_state
-
- def set_initial_state
- if show_from && (show_from > user.date)
- write_attribute self.class.state_column, 'deferred'
- else
- original_set_initial_state
- end
- end
-
- alias_method :original_run_initial_state_actions, :run_initial_state_actions
-
- def run_initial_state_actions
- # only run the initial state actions if the standard initial state hasn't
- # been changed
- if self.class.initial_state.to_sym == current_state
- original_run_initial_state_actions
- end
- end
-
- def self.feed_options(user)
- {
- :title => 'Tracks Actions',
- :description => "Actions for #{user.display_name}"
- }
- end
-
- def starred?
- tags.any? {|tag| tag.name == STARRED_TAG_NAME}
- end
-
- def toggle_star!
- if starred?
- delete_tags STARRED_TAG_NAME
- tags.reload
- else
- add_tag STARRED_TAG_NAME
- tags.reload
- end
- starred?
- end
-
- def from_recurring_todo?
- return self.recurring_todo_id != nil
- end
-
- # Rich Todo API
-
- 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
-
- context = nil if context == ""
- project = nil if project == ""
-
- context_id = default_context_id
- unless(context.nil?)
- found_context = user.active_contexts.find_by_namepart(context)
- found_context = user.contexts.find_by_namepart(context) if found_context.nil?
- context_id = found_context.id unless found_context.nil?
- end
-
- unless user.contexts.exists? context_id
- raise(CannotAccessContext, "Cannot access a context that does not belong to this user.")
- end
-
- 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.active_projects.find_by_namepart(project)
- found_project = user.projects.find_by_namepart(project) if found_project.nil?
- end
- project_id = found_project.id unless found_project.nil?
- end
-
- todo = user.todos.build
- todo.description = description
- todo.notes = notes
- todo.context_id = context_id
- todo.project_id = project_id unless project_id.nil?
- return todo
- end
-end
+class Todo < ActiveRecord::Base
+
+ belongs_to :context
+ belongs_to :project
+ belongs_to :user
+ belongs_to :recurring_todo
+
+ named_scope :active, :conditions => { :state => 'active' }
+ named_scope :not_completed, :conditions => ['NOT state = ? ', 'completed']
+ named_scope :are_due, :conditions => ['NOT todos.due IS NULL']
+
+ STARRED_TAG_NAME = "starred"
+
+ acts_as_state_machine :initial => :active, :column => 'state'
+
+ # when entering active state, also remove completed_at date. Looks like :exit
+ # of state completed is not run, see #679
+ state :active, :enter => Proc.new { |t| t[:show_from], t.completed_at = nil, nil }
+ state :project_hidden
+ state :completed, :enter => Proc.new { |t| t.completed_at = Time.zone.now }, :exit => Proc.new { |t| t.completed_at = nil }
+ state :deferred
+
+ event :defer do
+ transitions :to => :deferred, :from => [:active]
+ end
+
+ event :complete do
+ transitions :to => :completed, :from => [:active, :project_hidden, :deferred]
+ end
+
+ event :activate do
+ transitions :to => :active, :from => [:project_hidden, :completed, :deferred]
+ end
+
+ event :hide do
+ transitions :to => :project_hidden, :from => [:active, :deferred]
+ end
+
+ event :unhide do
+ transitions :to => :deferred, :from => [:project_hidden], :guard => Proc.new{|t| !t.show_from.blank? }
+ transitions :to => :active, :from => [:project_hidden]
+ end
+
+ 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
+ validates_length_of :notes, :maximum => 60000, :allow_nil => true
+ validates_presence_of :show_from, :if => :deferred?
+ validates_presence_of :context
+
+ def validate
+ if !show_from.blank? && show_from < user.date
+ errors.add("show_from", "must be a date in the future")
+ end
+ end
+
+ def toggle_completion!
+ saved = false
+ if completed?
+ saved = activate!
+ else
+ saved = complete!
+ end
+ return saved
+ end
+
+ def show_from
+ self[:show_from]
+ end
+
+ def show_from=(date)
+ # parse Date objects into the proper timezone
+ date = user.at_midnight(date) if (date.is_a? Date)
+ activate! if deferred? && date.blank?
+ defer! if active? && !date.blank? && date > user.date
+ self[:show_from] = date
+ end
+
+ alias_method :original_project, :project
+
+ def project
+ original_project.nil? ? Project.null_object : original_project
+ end
+
+ alias_method :original_set_initial_state, :set_initial_state
+
+ def set_initial_state
+ if show_from && (show_from > user.date)
+ write_attribute self.class.state_column, 'deferred'
+ else
+ original_set_initial_state
+ end
+ end
+
+ alias_method :original_run_initial_state_actions, :run_initial_state_actions
+
+ def run_initial_state_actions
+ # only run the initial state actions if the standard initial state hasn't
+ # been changed
+ if self.class.initial_state.to_sym == current_state
+ original_run_initial_state_actions
+ end
+ end
+
+ def self.feed_options(user)
+ {
+ :title => 'Tracks Actions',
+ :description => "Actions for #{user.display_name}"
+ }
+ end
+
+ def starred?
+ tags.any? {|tag| tag.name == STARRED_TAG_NAME}
+ end
+
+ def toggle_star!
+ if starred?
+ delete_tags STARRED_TAG_NAME
+ tags.reload
+ else
+ add_tag STARRED_TAG_NAME
+ tags.reload
+ end
+ starred?
+ end
+
+ def from_recurring_todo?
+ return self.recurring_todo_id != nil
+ end
+
+ # Rich Todo API
+
+ 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
+
+ context = nil if context == ""
+ project = nil if project == ""
+
+ context_id = default_context_id
+ unless(context.nil?)
+ found_context = user.active_contexts.find_by_namepart(context)
+ found_context = user.contexts.find_by_namepart(context) if found_context.nil?
+ context_id = found_context.id unless found_context.nil?
+ end
+
+ unless user.contexts.exists? context_id
+ raise(CannotAccessContext, "Cannot access a context that does not belong to this user.")
+ end
+
+ 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.active_projects.find_by_namepart(project)
+ found_project = user.projects.find_by_namepart(project) if found_project.nil?
+ end
+ project_id = found_project.id unless found_project.nil?
+ end
+
+ todo = user.todos.build
+ todo.description = description
+ todo.notes = notes
+ todo.context_id = context_id
+ todo.project_id = project_id unless project_id.nil?
+ return todo
+ end
+end