Merge branch 'deps'

This commit is contained in:
Eric Allen 2009-11-26 02:04:54 -05:00
commit 34aeb83891
31 changed files with 571 additions and 37 deletions

View file

@ -47,10 +47,11 @@ class ProjectsController < ApplicationController
@not_done = @project.not_done_todos_including_hidden
@deferred = @project.deferred_todos
@pending = @project.pending_todos
@done = @project.todos.find_in_state(:all, :completed, :order => "todos.completed_at DESC", :limit => current_user.prefs.show_number_completed, :include => [:context])
@count = @not_done.size
@down_count = @count + @deferred.size
@down_count = @count + @deferred.size + @pending.size
@next_project = current_user.projects.next_from(@project)
@previous_project = current_user.projects.previous_from(@project)
@default_tags = @project.default_tags

View file

@ -6,9 +6,9 @@ class TodosController < ApplicationController
prepend_before_filter :login_or_feed_token_required, :only => [:index, :calendar]
append_before_filter :init, :except => [ :destroy, :completed,
:completed_archive, :check_deferred, :toggle_check, :toggle_star,
:edit, :update, :create, :calendar, :auto_complete_for_tag]
append_before_filter :get_todo_from_params, :only => [ :edit, :toggle_check, :toggle_star, :show, :update, :destroy ]
protect_from_forgery :except => [:auto_complete_for_tag]
:edit, :update, :create, :calendar, :auto_complete_for_tag, :auto_complete_for_predecessor, :remove_predecessor, :add_predecessor]
append_before_filter :get_todo_from_params, :only => [ :edit, :toggle_check, :toggle_star, :show, :update, :destroy, :remove_predecessor]
protect_from_forgery :except => [:auto_complete_for_tag, :auto_complete_for_predecessor]
session :off, :only => :index, :if => Proc.new { |req| is_feed_request(req) }
@ -54,6 +54,7 @@ class TodosController < ApplicationController
p = TodoCreateParamsHelper.new(params, prefs)
p.parse_dates() unless mobile?
tag_list = p.tag_list
predecessor_list = p.predecessor_list
@todo = current_user.todos.build(p.attributes)
@ -70,13 +71,21 @@ class TodosController < ApplicationController
@todo.context_id = context.id
end
@todo.add_predecessor_list(predecessor_list)
@todo.update_state_from_project
@saved = @todo.save
unless (@saved == false) || tag_list.blank?
@todo.tag_with(tag_list)
@todo.tags.reload
end
unless (@aved == false)
unless @todo.uncompleted_predecessors.empty? || @todo.state == 'project_hidden'
@todo.state = 'pending'
end
@todo.save
end
respond_to do |format|
format.html { redirect_to :action => "index" }
format.m do
@ -130,6 +139,30 @@ class TodosController < ApplicationController
format.xml { render :xml => @todo.to_xml( :root => 'todo', :except => :user_id ) }
end
end
def add_predecessor
@source_view = params['_source_view'] || 'todo'
@predecessor = Todo.find(params['predecessor'])
@todo = Todo.find(params['successor'])
@original_state = @todo.state
# Add predecessor
@todo.add_predecessor(@predecessor)
@todo.state = 'pending'
@saved = @todo.save
respond_to do |format|
format.js
end
end
def remove_predecessor
@source_view = params['_source_view'] || 'todo'
@predecessor = Todo.find(params['predecessor'])
@successor = @todo
@removed = @successor.remove_predecessor(@predecessor)
respond_to do |format|
format.js
end
end
# Toggles the 'done' status of the action
#
@ -142,6 +175,18 @@ class TodosController < ApplicationController
# check if this todo has a related recurring_todo. If so, create next todo
@new_recurring_todo = check_for_next_todo(@todo) if @saved
if @todo.completed?
@pending_to_activate = @todo.pending_to_activate
@pending_to_activate.each do |t|
t.activate!
end
else
@active_to_block = @todo.active_to_block
@active_to_block.each do |t|
t.block!
end
end
respond_to do |format|
format.js do
if @saved
@ -190,7 +235,8 @@ class TodosController < ApplicationController
@original_item_project_id = @todo.project_id
@original_item_was_deferred = @todo.deferred?
@original_item_due = @todo.due
@original_item_due_id = get_due_id_for_calendar(@todo.due)
@original_item_due_id = get_due_id_for_calendar(@todo.due)
@original_item_predecessor_list = @todo.predecessors.map{|t| t.specification}.join(', ')
if params['todo']['project_id'].blank? && !params['project_name'].nil?
if params['project_name'] == 'None'
@ -231,15 +277,38 @@ class TodosController < ApplicationController
if params['done'] == '1' && !@todo.completed?
@todo.complete!
@todo.pending_to_activate.each do |t|
t.activate!
end
end
# strange. if checkbox is not checked, there is no 'done' in params.
# Therefore I've used the negation
if !(params['done'] == '1') && @todo.completed?
@todo.activate!
@todo.active_to_block.each do |t|
t.block!
end
end
@todo.attributes = params["todo"]
@todo.add_predecessor_list(params[:predecessor_list])
@saved = @todo.save
if @saved && params[:predecessor_list]
if @original_item_predecessor_list != params[:predecessor_list]
# Possible state change with new dependencies
if @todo.uncompleted_predecessors.empty?
if @todo.state == 'pending'
@todo.activate! # Activate pending if no uncompleted predecessors
end
else
if @todo.state == 'active'
@todo.block! # Block active if we got uncompleted predecessors
end
end
end
@todo.save!
end
@context_changed = @original_item_context_id != @todo.context_id
@todo_was_activated_from_deferred_state = @original_item_was_deferred && @todo.active?
@ -297,16 +366,30 @@ class TodosController < ApplicationController
@context_id = @todo.context_id
@project_id = @todo.project_id
# activate successors if they only depend on this todo
activated_successor_count = 0
@todo.pending_successors.each do |successor|
successor.uncompleted_predecessors.delete(@todo)
if successor.uncompleted_predecessors.empty?
successor.activate!
activated_successor_count += 1
end
end
@saved = @todo.destroy
# check if this todo has a related recurring_todo. If so, create next todo
@new_recurring_todo = check_for_next_todo(@todo) if @saved
respond_to do |format|
format.html do
if @saved
notify :notice, "Successfully deleted next action", 2.0
message = "Successfully deleted next action"
if activated_successor_count > 0
message += " activated #{pluralize(activated_successor_count, 'pending action')}"
end
notify :notice, message, 2.0
redirect_to :action => 'index'
else
notify :error, "Failed to delete the action", 2.0
@ -356,7 +439,7 @@ class TodosController < ApplicationController
@contexts_to_show = @contexts = current_user.contexts.find(:all, :include => [ :todos ])
current_user.deferred_todos.find_and_activate_ready
@not_done_todos = current_user.deferred_todos
@not_done_todos = current_user.deferred_todos + current_user.pending_todos
@count = @not_done_todos.size
@down_count = @count
@ -409,6 +492,9 @@ class TodosController < ApplicationController
@deferred = tag_collection.find(:all,
:conditions => ['todos.user_id = ? and state = ?', current_user.id, 'deferred'],
:order => 'show_from ASC, todos.created_at DESC')
@pending = tag_collection.find(:all,
:conditions => ['todos.user_id = ? and state = ?', current_user.id, 'pending'],
:order => 'show_from ASC, todos.created_at DESC')
# If you've set no_completed to zero, the completed items box isn't shown on
# the tag page
@ -513,6 +599,46 @@ class TodosController < ApplicationController
render :inline => "<%= auto_complete_result(@items, :name) %>"
end
def auto_complete_for_predecessor
unless params['id'].nil?
get_todo_from_params
# Begin matching todos in current project
@items = current_user.todos.find(:all,
:select => 'description, project_id, context_id, created_at',
:conditions => [ '(todos.state = ? OR todos.state = ?) AND ' +
'NOT (id = ?) AND lower(description) LIKE ? AND project_id = ?',
'active', 'pending',
@todo.id,
'%' + params[:predecessor_list].downcase + '%',
@todo.project_id ],
:order => 'description ASC',
:limit => 10
)
if @items.empty? # Match todos in other projects
@items = current_user.todos.find(:all,
:select => 'description, project_id, context_id, created_at',
:conditions => [ '(todos.state = ? OR todos.state = ?) AND ' +
'NOT (id = ?) AND lower(description) LIKE ?',
'active', 'pending',
params[:id], '%' + params[:predecessor_list].downcase + '%' ],
:order => 'description ASC',
:limit => 10
)
end
else
# New todo - TODO: Filter on project
@items = current_user.todos.find(:all,
:select => 'description, project_id, context_id, created_at',
:conditions => [ '(todos.state = ? OR todos.state = ?) AND lower(description) LIKE ?',
'active', 'pending',
'%' + params[:predecessor_list].downcase + '%' ],
:order => 'description ASC',
:limit => 10
)
end
render :inline => "<%= auto_complete_result2(@items) %>"
end
private
def get_todo_from_params
@ -670,6 +796,7 @@ class TodosController < ApplicationController
unless @todo.project_id == nil
@down_count = current_user.projects.find(@todo.project_id).not_done_todos_including_hidden.count
@deferred_count = current_user.projects.find(@todo.project_id).deferred_todos.count
@pending_count = current_user.projects.find(@todo.project_id).pending_todos.count
end
end
from.deferred do
@ -956,6 +1083,10 @@ class TodosController < ApplicationController
def tag_list
@params['tag_list']
end
def predecessor_list
@params['predecessor_list']
end
def parse_dates()
@attributes['show_from'] = @prefs.parse_date(show_from)

View file

@ -64,7 +64,7 @@ module TodosHelper
:complete => todo_stop_waiting_js(todo))
end
end
def todo_start_waiting_js(todo)
return "$('#ul#{dom_id(todo)}').css('visibility', 'hidden'); $('##{dom_id(todo)}').block({message: null})"
end
@ -82,12 +82,15 @@ module TodosHelper
def remote_toggle_checkbox
check_box_tag('item_id', toggle_check_todo_path(@todo), @todo.completed?, :class => 'item-checkbox')
check_box_tag('item_id', toggle_check_todo_path(@todo), @todo.completed?, :class => 'item-checkbox',
:title => @todo.pending? ? 'Blocked by ' + @todo.uncompleted_predecessors.map(&:description).join(', ') : "", :readonly => @todo.pending?)
end
def date_span
if @todo.completed?
"<span class=\"grey\">#{format_date( @todo.completed_at )}</span>"
elsif @todo.pending?
"<a title='Depends on: #{@todo.uncompleted_predecessors.map(&:description).join(', ')}'><span class=\"orange\">Pending</span></a> "
elsif @todo.deferred?
show_date( @todo.show_from )
else
@ -95,6 +98,22 @@ module TodosHelper
end
end
def successors_span
unless @todo.pending_successors.empty?
pending_count = @todo.pending_successors.length
title = "Has #{pluralize(pending_count, 'pending action')}: #{@todo.pending_successors.map(&:description).join(', ')}"
image_tag( 'successor_off.png', :width=>'10', :height=>'16', :border=>'0', :title => title )
end
end
def grip_span
unless @todo.completed?
image_tag('grip.png', :width => '7', :height => '16', :border => '0',
:title => 'Drag onto another action to make it depend on that action',
:class => 'grip')
end
end
def tag_list_text
@todo.tags.collect{|t| t.name}.join(', ')
end
@ -115,6 +134,10 @@ module TodosHelper
if tag_list.empty? then "" else "<span class=\"tags\">#{tag_list}</span>" end
end
def predecessor_list_text
@todo.predecessors.map{|t| t.specification}.join(', ')
end
def deferred_due_date
if @todo.deferred? && @todo.due
"(action due on #{format_date(@todo.due)})"
@ -201,9 +224,10 @@ module TodosHelper
end
def item_container_id (todo)
if source_view_is :project
return "p#{todo.project_id}items" if todo.active?
return "tickler" if todo.deferred?
if todo.deferred? or todo.pending?
return "tickleritems"
elsif source_view_is :project
return "p#{todo.project_id}items"
end
return "c#{todo.context_id}items"
end
@ -221,6 +245,8 @@ module TodosHelper
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 true if source_view_is(:project) && @todo.pending?
return true if source_view_is(:tag) && @todo.pending?
return false
end
@ -281,4 +307,14 @@ module TodosHelper
class_str = todo.starred? ? "starred_todo" : "unstarred_todo"
image_tag("blank.png", :title =>"Star action", :class => class_str)
end
def auto_complete_result2(entries, phrase = nil)
return unless entries
items = entries.map do |entry|
item = entry.specification()
content_tag("li", phrase ? highlight(h(item), phrase) : h(item))
end
content_tag("ul", items.uniq)
end
end

7
app/models/dependency.rb Normal file
View file

@ -0,0 +1,7 @@
class Dependency < ActiveRecord::Base
belongs_to :predecessor, :foreign_key => 'predecessor_id', :class_name => 'Todo'
belongs_to :successor, :foreign_key => 'successor_id', :class_name => 'Todo'
end

View file

@ -20,6 +20,11 @@ class Project < ActiveRecord::Base
:class_name => 'Todo',
:conditions => ["todos.state = ? ", "deferred"],
:order => "show_from"
has_many :pending_todos,
:include => [:context,:tags,:project],
:class_name => 'Todo',
:conditions => ["todos.state = ? ", "pending"],
:order => "show_from"
has_many :notes, :dependent => :delete_all, :order => "created_at DESC"

View file

@ -4,12 +4,26 @@ class Todo < ActiveRecord::Base
belongs_to :project
belongs_to :user
belongs_to :recurring_todo
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
has_many :predecessors, :through => :successor_dependencies
has_many :successors, :through => :predecessor_dependencies
has_many :uncompleted_predecessors, :through => :successor_dependencies,
:source => :predecessor, :conditions => ['NOT (state = ?)', 'completed']
has_many :pending_successors, :through => :predecessor_dependencies,
:source => :successor, :conditions => ['state = ?', 'pending']
after_save :save_predecessors
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"
RE_TODO = /[^"]+/
RE_CONTEXT = /[^"]+/
RE_PROJECT = /[^"]+/
acts_as_state_machine :initial => :active, :column => 'state'
@ -19,6 +33,7 @@ class Todo < ActiveRecord::Base
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
state :pending
event :defer do
transitions :to => :deferred, :from => [:active]
@ -30,6 +45,8 @@ class Todo < ActiveRecord::Base
event :activate do
transitions :to => :active, :from => [:project_hidden, :completed, :deferred]
transitions :to => :active, :from => [:pending], :guard => :no_uncompleted_predecessors_or_deferral?
transitions :to => :deferred, :from => [:pending], :guard => :no_uncompleted_predecessors?
end
event :hide do
@ -40,6 +57,10 @@ class Todo < ActiveRecord::Base
transitions :to => :deferred, :from => [:project_hidden], :guard => Proc.new{|t| !t.show_from.blank? }
transitions :to => :active, :from => [:project_hidden]
end
event :block do
transitions :to => :pending, :from => [:active, :deferred]
end
attr_protected :user
@ -51,15 +72,120 @@ class Todo < ActiveRecord::Base
validates_presence_of :show_from, :if => :deferred?
validates_presence_of :context
def initialize(*args)
super(*args)
@predecessor_array = nil # Used for deferred save of predecessors
end
def no_uncompleted_predecessors_or_deferral?
return (show_from.blank? or Time.zone.now > show_from and uncompleted_predecessors.empty?)
end
def no_uncompleted_predecessors?
return uncompleted_predecessors.empty?
end
# Returns a string with description <context, project>
def specification
project_name = project.is_a?(NullProject) ? "(none)" : project.name
return "\"#{description}\" <\"#{context.title}\"; \"#{project_name}\">"
end
def todo_from_specification(specification)
# Split specification into parts: description <context, project>
re_parts = /"(#{RE_TODO})"\s<"(#{RE_CONTEXT})";\s"(#{RE_PROJECT})">/
parts = specification.scan(re_parts)
return nil unless parts.length == 1
return nil unless parts[0].length == 3
todo_description = parts[0][0]
context_name = parts[0][1]
todos = Todo.all(
:joins => :context,
:conditions => {
:description => todo_description,
:contexts => {:name => context_name}
}
)
return nil if todos.empty?
# todos now contains all todos with matching description and context
# TODO: Is this possible to do with a single query?
todos.each do |todo|
project_name = todo.project.is_a?(NullProject) ? "(none)" : todo.project.name
return todo if project_name == parts[0][2]
end
return nil
end
def validate
if !show_from.blank? && show_from < user.date
errors.add("show_from", "must be a date in the future")
end
errors.add(:description, "may not contain \" characters") if /\"/.match(description)
unless @predecessor_array.nil? # Only validate predecessors if they changed
@predecessor_array.each do |specification|
t = todo_from_specification(specification)
if t.nil?
errors.add("Depends on:", "Could not find action '#{h(specification)}'")
else
errors.add("Depends on:", "Adding '#{h(specification)}' would create a circular dependency") if is_successor?(t)
end
end
end
end
def save_predecessors
unless @predecessor_array.nil? # Only save predecessors if they changed
current_array = predecessors.map{|p| p.specification}
remove_array = current_array - @predecessor_array
add_array = @predecessor_array - current_array
# This is probably a bit naive code...
remove_array.each do |specification|
t = todo_from_specification(specification)
self.predecessors.delete(t) unless t.nil?
end
# ... as is this?
add_array.each do |specification|
t = todo_from_specification(specification)
unless t.nil?
self.predecessors << t unless self.predecessors.include?(t)
else
logger.error "Could not find #{specification}" # Unexpected since validation passed
end
end
end
end
def remove_predecessor(predecessor)
# remove predecessor and activate myself
predecessors.delete(predecessor)
self.activate!
end
# Returns true if t is equal to self or a successor of self
def is_successor?(t)
if self == t
return true
elsif self.successors.empty?
return false
else
self.successors.each do |item|
if item.is_successor?(t)
return true
end
end
end
return false
end
def update_state_from_project
if state == 'project_hidden' and !project.hidden?
self.state = 'active'
if self.uncompleted_predecessors.empty?
self.state = 'active'
else
self.state = 'pending'
end
elsif state == 'active' and project.hidden?
self.state = 'project_hidden'
end
@ -92,7 +218,7 @@ class Todo < ActiveRecord::Base
def project
original_project.nil? ? Project.null_object : original_project
end
alias_method :original_set_initial_state, :set_initial_state
def set_initial_state
@ -138,6 +264,28 @@ class Todo < ActiveRecord::Base
def from_recurring_todo?
return self.recurring_todo_id != nil
end
def add_predecessor_list(predecessor_list)
return unless predecessor_list.kind_of? String
# Split into list
re_specification = /"#{RE_TODO}"\s<"#{RE_CONTEXT}";\s"#{RE_PROJECT}">/
@predecessor_array = predecessor_list.scan(re_specification)
end
def add_predecessor(t)
@predecessor_array = predecessors.map{|p| p.specification}
@predecessor_array << t.specification
end
# Return todos that should be activated if the current todo is completed
def pending_to_activate
return successors.find_all {|t| t.uncompleted_predecessors.empty?}
end
# Return todos that should be blocked if the current todo is undone
def active_to_block
return successors.find_all {|t| t.active? or t.deferred?}
end
# Rich Todo API

View file

@ -98,6 +98,10 @@ class User < ActiveRecord::Base
find(:all, :conditions => ['show_from <= ?', Time.zone.now ]).collect { |t| t.activate! }
end
end
has_many :pending_todos,
:class_name => 'Todo',
:conditions => [ 'state = ?', 'pending' ],
:order => 'show_from ASC, todos.created_at DESC'
has_many :completed_todos,
:class_name => 'Todo',
:conditions => ['todos.state = ? AND NOT(todos.completed_at IS NULL)', 'completed'],

View file

@ -4,7 +4,7 @@
</div>
<%= render :partial => "projects/project", :locals => { :project => @project, :collapsible => false } %>
<%= render :partial => "todos/deferred", :locals => { :deferred => @deferred, :collapsible => false, :append_descriptor => "in this project", :parent_container_type => 'project' } %>
<%= render :partial => "todos/deferred", :locals => { :deferred => @deferred, :collapsible => false, :append_descriptor => "in this project", :parent_container_type => 'project', :pending => @pending } %>
<% unless @max_completed==0 -%>
<%= render :partial => "todos/completed", :locals => { :done => @done, :collapsible => false, :suppress_project => true, :append_descriptor => "in this project" } %>
<% end -%>

View file

@ -51,6 +51,14 @@
<%= text_field("todo", "show_from", "size" => 12, "class" => "Date", "tabindex" => 7, "autocomplete" => "off") %>
</div>
<label for="predecessor_list">Depends on</label>
<%= text_field_tag "predecessor_list", nil, :size => 30, :tabindex => 8 %>
<%= content_tag("div", "", :id => "predecessor_list_auto_complete", :class => "auto_complete") %>
<%= auto_complete_field 'predecessor_list', {
:url => {:controller => 'todos', :action => 'auto_complete_for_predecessor', :id => nil},
:tokens => [',']
} %>
<%= source_view_tag( @source_view ) %>
<%= hidden_field_tag :_tag_name, @tag_name.underscore.gsub(/\s+/,'_') if source_view_is :tag %>

View file

@ -3,15 +3,16 @@
<% if collapsible %>
<a href="#" class="container_toggle" id="toggle_deferred"><%= image_tag("collapse.png") %></a>
<% end %>
Deferred actions <%= append_descriptor ? append_descriptor : '' %>
Deferred/pending actions <%= append_descriptor ? append_descriptor : '' %>
</h2>
<div id="tickleritems" class="items toggle_target">
<div id="tickler-empty-nd" style="display:<%= deferred.empty? ? 'block' : 'none'%>;">
<div class="message"><p>Currently there are no deferred actions</p></div>
<div id="tickler-empty-nd" style="display:<%= deferred.empty? && pending.empty? ? 'block' : 'none'%>;">
<div class="message"><p>Currently there are no deferred or pending actions</p></div>
</div>
<%= render :partial => "todos/todo", :collection => deferred, :locals => { :parent_container_type => parent_container_type } %>
<%= render :partial => "todos/todo", :collection => pending, :locals => { :parent_container_type => parent_container_type } %>
</div><!-- [end:items] -->
</div><!-- [end:tickler] -->
</div><!-- [end:tickler] -->

View file

@ -46,13 +46,21 @@
</a>
</div>
<label class="predecessor_list_label" for="<%= dom_id(@todo, 'predecessor_list') %>">Depends on (separate with commas)</label>
<%= text_field_tag 'predecessor_list', predecessor_list_text, :id => dom_id(@todo, 'predecessor_list'), :size => 30, :tabindex => 15 %>
<%= content_tag("div", "", :id => dom_id(@todo, 'predecessor_list')+"_auto_complete", :class => "auto_complete") %>
<%= auto_complete_field dom_id(@todo, 'predecessor_list'), {
:url => {:controller => 'todos', :action => 'auto_complete_for_predecessor', :id => @todo.id},
:tokens => [',']
} %>
<% if controller.controller_name == "project" || @todo.deferred? -%>
<input type="hidden" name="on_project_page" value="true" />
<% end -%>
<div class="submit_box">
<div class="widgets">
<button type="submit" class="positive" id="<%= dom_id(@todo, 'submit') %>" tabindex="15">
<button type="submit" class="positive" id="<%= dom_id(@todo, 'submit') %>" tabindex="16">
<%=image_tag("accept.png", :alt => "") %>
Update
</button>

View file

@ -0,0 +1,27 @@
<%
suppress_context ||= false
suppress_project ||= false
suppress_dependencies ||= false
parameters = "_source_view=#{@source_view}"
parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
@z_index_counter = @z_index_counter - 1 # for IE z-index bug
%>
<div id="<%= dom_id(successor, 'successor') %>" class="item-container">
<div id="<%= dom_id(successor, 'successor_line') %>">
<div class="description<%= staleness_class( successor ) %>" style="margin-left: 20px">
<span class="todo.descr"><%= h sanitize(successor.description) %></span>
<%= link_to_remote(
image_tag("blank.png", :title => "Remove dependency (does not delete the action)", :align => "absmiddle", :class => "delete_item"),
{:url => {:controller => 'todos', :action => 'remove_predecessor', :id => successor.id},
:method => 'delete',
:with => "'#{parameters}&predecessor=#{predecessor.id}'",
:before => todo_start_waiting_js(dom_id(successor, 'successor')),
:complete => todo_stop_waiting_js},
{:style => "background: transparent;"}) %>
<%= render(:partial => "todos/toggle_successors", :locals => { :item => successor, :suppress_button => true }) unless successor.pending_successors.empty? %>
</div>
</div>
</div>

View file

@ -26,13 +26,16 @@ parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
</li>
</ul>
<div class="description<%= staleness_class( todo ) %>">
<%= grip_span %>
<%= date_span -%>
<span class="todo.descr"><%= h sanitize(todo.description) %></span>
<% #= successors_span %>
<%= image_tag_for_recurring_todo(todo) if @todo.from_recurring_todo? %>
<%= tag_list %>
<%= deferred_due_date %>
<%= project_and_context_links( parent_container_type, :suppress_context => suppress_context, :suppress_project => suppress_project ) %>
<%= render(:partial => "todos/toggle_notes", :locals => { :item => todo }) if todo.notes? %>
<%= render(:partial => "todos/toggle_successors", :locals => { :item => todo }) unless todo.pending_successors.empty? %>
</div>
</div>
<div id="<%= dom_id(todo, 'edit') %>" class="edit-form" style="display:none">
@ -42,3 +45,14 @@ parameters += "&_tag_name=#{@tag_name}" if @source_view == 'tag'
<% end -%>
</div>
</div>
<%=
draggable_element(dom_id(todo), :revert => "'true'", :handle => "'grip'", :onDrop => "''")
%>
<%=
drop_receiving_element(dom_id(todo),
:url => {:controller => "todos", :action => "add_predecessor"},
:with => "'#{parameters}&successor=' + encodeURIComponent(element.id.split('_').last()) + '&predecessor=' + encodeURIComponent(#{todo.id})",
:hoverclass => 'hover'
)
%>

View file

@ -0,0 +1,15 @@
<%
suppress_button ||= false
%>
<%= link_to(image_tag( 'blank.png', :width=>'16', :height=>'16', :border=>'0' ), "#", {:class => 'show_successors', :title => 'Show successors'}) unless suppress_button %>
<div class="todo_successors" id="<%= dom_id(item, 'successors') %>" style=<%= suppress_button ? "display:display" : "display:none" %> >
<%= render :partial => "todos/successor",
:collection => item.pending_successors,
:locals => { :todo => item,
:parent_container_type => parent_container_type,
:suppress_dependencies => true,
:predecessor => item }
%>
</div>

View file

@ -0,0 +1,28 @@
if @saved
# show update message
status_message = "Added #{@predecessor.description} as dependency."
unless @original_state == 'pending'
status_message += " #{@todo.description} set to pending"
end
# remove successor from page
page[@todo].remove
# regenerate predecessor to add arrow
page[@predecessor].replace_html :partial => 'todos/todo', :locals => { :todo => @predecessor, :parent_container_type => parent_container_type }
# show in tickler box in project view
if source_view_is_one_of :project, :tag
page['tickler-empty-nd'].hide
page.replace "tickler", :partial => 'todos/deferred', :locals => { :deferred => @todo.project.deferred_todos,
:collapsible => false,
:append_descriptor => "in this project",
:parent_container_type => 'project',
:pending => @todo.project.pending_todos }
end
page << "TodoBehavior.enableToggleNotes();"
page << "TodoBehavior.enableToggleSuccessors();"
page.notify :notice, status_message, 5.0
else
page.replace_html "status", content_tag("div", content_tag("h2", "Unable to add dependency"), "id" => "errorExplanation", "class" => "errorExplanation")
end

View file

@ -2,6 +2,7 @@ if @saved
page.hide 'status'
status_message = 'Added new next action'
status_message += ' to tickler' if @todo.deferred?
status_message += ' in pending state' if @todo.pending?
status_message = 'Added new project / ' + status_message if @new_project_created
status_message = 'Added new context / ' + status_message if @new_context_created
page.notify :notice, status_message, 5.0
@ -21,9 +22,20 @@ if @saved
page.visual_effect :highlight, dom_id(@todo), :duration => 3
page[empty_container_msg_div_id].hide unless empty_container_msg_div_id.nil?
end
# make sure the behavior of the new/updated todo is enabled
page['tickler-empty-nd'].hide if source_view_is :deferred
if (source_view_is :project and @todo.pending?) or (source_view_is :deferred)
page['tickler-empty-nd'].hide # For some reason this does not work: page['tickler-empty-nd'].hide if (@todo.pending? or (source_view_is :deferred))
end
end
# Update predecessors (if they exist and are visible)
@todo.uncompleted_predecessors.each do |p|
page << "if ($(\'#{item_container_id(p)}\')) {"
page[p].replace_html :partial => 'todos/todo',
:locals => { :todo => p, :parent_container_type => parent_container_type }
page << "}"
end
# make sure the behavior of the new/updated todo is enabled
page << "TodoBehavior.enableToggleNotes()"
page << "TodoBehavior.enableToggleSuccessors()"
else
page.show 'status'
page.replace_html 'status', "#{error_messages_for('todo', :object_name => 'action')}"

View file

@ -0,0 +1,27 @@
if @removed
status_message = "Removed #{@successor.description} as dependency from #{@predecessor.description}."
page.notify :notice, status_message, 5.0
# replace old predecessor with one without the successor
page.replace dom_id(@predecessor), :partial => 'todos/todo', :locals => {
:todo => @predecessor, :parent_container_type => parent_container_type }
# update display if pending->active
if @successor.active?
page[@successor].remove unless source_view_is_one_of(:todo, :context)
page.insert_html :bottom, item_container_id(@successor), :partial => 'todos/todo', :locals => {
:todo => @successor, :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@successor, 'line'), {'startcolor' => "'#99ff99'"}
end
# update display if pending->deferred
if @successor.deferred?
page.replace dom_id(@successor), :partial => 'todos/todo', :locals => {
:todo => @successor, :parent_container_type => parent_container_type }
end
page << "TodoBehavior.enableToggleNotes()"
page << "TodoBehavior.enableToggleSuccessors()"
else
page.notify :error, "There was an error removing the dependency", 8.0
end

View file

@ -8,7 +8,13 @@
:locals => { :collapsible => true } %>
<% unless @deferred.nil? -%>
<%= render :partial => "todos/deferred", :locals => { :deferred => @deferred, :collapsible => true, :append_descriptor => "tagged with &lsquo;#{@tag_name}&rsquo;", :parent_container_type => 'tag' } %>
<%= render :partial => "todos/deferred", :locals => {
:deferred => @deferred,
:pending => @pending,
:collapsible => true,
:append_descriptor => "tagged with &lsquo;#{@tag_name}&rsquo;",
:parent_container_type => 'tag'
} %>
<% end -%>
<% unless @hidden_todos.nil? -%>

View file

@ -8,9 +8,16 @@ if @saved
page.insert_html :top, "completed_containeritems", :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => "completed" }
page.visual_effect :highlight, dom_id(@todo, 'line'), {'startcolor' => "'#99ff99'"}
page[empty_container_msg_div_id].show if @down_count == 0 && !empty_container_msg_div_id.nil?
page.show 'tickler-empty-nd' if source_view_is(:project) && @deferred_count == 0
page.show 'tickler-empty-nd' if source_view_is(:project) && @deferred_count == 0 && @pending_count == 0
page.hide 'empty-d' # If we've checked something as done, completed items can't be empty
end
# Activate pending todos that are successors of the completed
@pending_to_activate.each do |t|
logger.debug "#300: Removing #{t.description} from pending block and adding it to active"
page[t].remove if source_view_is(:project) or source_view_is(:tag)
page.insert_html :bottom, item_container_id(t), :partial => 'todos/todo', :locals => { :todo => t, :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(t, 'line'), {'startcolor' => "'#99ff99'"}
end
# remove container if empty
if @remaining_in_context == 0 && source_view_is(:todo)
@ -44,6 +51,15 @@ if @saved
page.show "empty-d" if @completed_count == 0
page.show "c"+@todo.context.id.to_s
page[empty_container_msg_div_id].hide unless empty_container_msg_div_id.nil? # If we've checked something as undone, incomplete items can't be empty
# If active todos are successors of the reactivated todo they will be blocked
@active_to_block.each do |t|
logger.debug "#300: Block #{t.description} that are a successor of #{@todo.description}"
page[t].remove # Remove it from active
if source_view_is(:project) or source_view_is(:tag) # Insert it in deferred/pending block if existing
logger.debug "Insert #{t.description} in #{item_container_id(t)} block"
page.insert_html :bottom, item_container_id(t), :partial => 'todos/todo', :locals => { :todo => t, :parent_container_type => parent_container_type }
end
end
end
page.hide "status"

View file

@ -7,7 +7,7 @@ if @saved
page.notify :notice, status_message, 5.0
if source_view_is_one_of(:todo, :context, :tag)
if @context_changed || @todo.deferred?
if @context_changed || @todo.deferred? || @todo.pending?
page[@todo].remove
if (@remaining_in_context == 0)
@ -87,7 +87,14 @@ if @saved
page["p#{@todo.project_id}empty-nd"].hide
page.replace_html "badge_count", @down_count
else
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :todo => @todo, :parent_container_type => parent_container_type }
page.replace_html "p#{@todo.project_id}items", :partial => 'todos/todo', :collection => @todo.project.not_done_todos,
:locals => { :parent_container_type => parent_container_type }
page.replace "tickler", :partial => 'todos/deferred', :locals => { :deferred => @todo.project.deferred_todos,
:collapsible => false,
:append_descriptor => "in this project",
:parent_container_type => 'project',
:pending => @todo.project.pending_todos }
page['tickler-empty-nd'].show if (@deferred_count == 0 and @pending_count == 0)
page.visual_effect :highlight, dom_id(@todo), :duration => 3
end
elsif source_view_is :deferred
@ -135,6 +142,13 @@ if @saved
else
logger.error "unexpected source_view '#{params[:_source_view]}'"
end
# Update predecessors (if they exist and are visible)
@todo.uncompleted_predecessors.each do |p|
page << "if ($(\'#{item_container_id(p)}\')) {"
page[p].replace_html :partial => 'todos/todo',
:locals => { :todo => p, :parent_container_type => parent_container_type }
page << "}"
end
else
page.show 'error_status'
page.replace_html 'error_status', "#{error_messages_for('todo')}"

View file

@ -0,0 +1,13 @@
class AddTodoDependencies < ActiveRecord::Migration
def self.up
create_table :dependencies do |t|
t.integer :successor_id, :null => false
t.integer :predecessor_id, :null => false
t.string :relationship_type
end
end
def self.down
drop_table :dependencies
end
end

Binary file not shown.

BIN
db/tracks-17-test.db Normal file

Binary file not shown.

BIN
public/images/grip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -247,6 +247,10 @@ $(document).ready(function() {
$(this).next().toggle("fast"); return false;
});
$(".show_successors").live('click', function () {
$(this).next().toggle("fast"); return false;
});
/* fade flashes and alerts in automatically */
$(".alert").fadeOut(8000);

View file

@ -99,6 +99,9 @@ a.to_bottom:hover {background: transparent url(/images/bottom_on.png) no-repeat;
a.show_notes, a.link_to_notes {background-image: url(/images/notes_off.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;}
a.show_notes:hover, a.link_to_notes:hover {background-image: url(/images/notes_on.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;}
a.show_successors, a.link_to_successors {background-image: url(/images/successor_off.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;}
a.show_successors:hover, a.link_to_successors:hover {background-image: url(/images/successor_on.png); background-repeat: no-repeat; padding: 1px; background-color: transparent;}
/* Structural divs */
#content {
@ -378,11 +381,7 @@ input.item-checkbox {
.description, .stale_l1, .stale_l2, .stale_l3 {
margin-left: 60px;
position:relative;
}
.stale_l1, .stale_l2, .stale_l3 {
padding-left: 3px;
position:relative;
}
.stale_l1 {
@ -936,6 +935,15 @@ div.message {
color: #666;
}
.grip {
cursor: move;
}
.hover {
background: #EAEAEA;
font-weight: bold;
}
/* Error message styles */
.fieldWithErrors {
padding: 2px;
@ -1262,6 +1270,7 @@ div.auto_complete ul {
list-style-type:none;
}
div.auto_complete ul li {
font-size: 1.0em;
margin:0;
padding:3px;
list-style-type: none;

View file

@ -1,5 +1,5 @@
type "todo_description", "choose era"
type "todo_show_from", "1/1/2030"
click "css=#todo-form-new-action .submit_box button"
wait_for_element_present "xpath=//div[@id='tickler'] //div[@class='item-container']"
wait_for_element_present "xpath=//div[@id='tickler'] //div[@class='item-container'] //a[@title='01/01/2030']"
wait_for_element_present "css=div#tickler div.item-container"
wait_for_element_present "css=div#tickler div.item-container a[title=01/01/2030]"

View file

@ -5,7 +5,7 @@ open "/projects/1"
# add new todo
type "todo_description", "a brand new todo"
click "css=#todo-form-new-action .submit_box button"
wait_for_element_present "xpath=//div[@id='p1items'] //div[@class='item-container']"
wait_for_element_present "css=div#p1items div.item-container"
# wait for flash to mention that todo was added and verify existence of new todo
wait_for_visible "flash"

View file

@ -10,4 +10,4 @@ assert_context_count_incremented do
assert_confirmation "New context \"errands\" will be also created. Are you sure?"
end
wait_for_not_visible "tickler-empty-nd"
wait_for_element_present "xpath=//div[@class='item-container'] //a[@title='01/01/2030']"
wait_for_element_present "css=div.item-container a[title=01/01/2030]"