Added the beginnings of a tickler to Tracks. It's fairly rudimentary at the moment, but it's designed to set the foundations for more kinds of deferred tasks.The current system works, but isn't very DRY: it will need refactoring for speed.

It has these features:

* The todos table and model has been altered (run rake migrate to update) to create two sub-classes of the todo model: Immediate and Deferred. Fairly obviously, Immediate actions are those shown immediately, and Deferred are those shown when certain conditions are fulfilled. At the moment, this is when the 'show_from' date arrives.
* Deferred actions are created on a separate page: /todo/tickler. You can view the show_from date here and delete or edit the actions. Deferred actions don't show on the home page (their handling on project and context pages is still to be fixed).
* A periodically called method (every 10 minutes) checks whether any of the deferred actions is due to be show, and if so, a warning message is shown on the home page to tell you how many deferred actions are to be shown. You need to refresh the page to see them (again, this is to be fixed).
* When deferred actions become due, their type is changed from "Deferred" to "Immediate". The handling of their staleness is still to be fixed.

There's a way to go before it's really smooth, but it's a start.

At least partially fixes #270 and #78, but will be improved with time too.



git-svn-id: http://www.rousette.org.uk/svn/tracks-repos/trunk@232 a4c988fc-2ded-0310-b66e-134b36920a42
This commit is contained in:
bsag 2006-05-02 17:11:46 +00:00
parent 4de6537af8
commit 03ff56d703
24 changed files with 399 additions and 16 deletions

View file

@ -56,7 +56,7 @@ class TodoController < ApplicationController
else
@item.due = ""
end
@saved = @item.save
@on_page = "home"
@ -82,6 +82,44 @@ class TodoController < ApplicationController
render :action => 'list'
end
end
# Adding deferred actions from form on todo/tickler
#
def add_deferred_item
self.init
@tickle = Deferred.create(@params["todo"])
if @tickle.due?
@tickle.due = Date.strptime(@params["todo"]["due"], @user.preferences["date_format"])
else
@tickle.due = ""
end
@saved = @tickle.save
@on_page = "home"
if @saved
@up_count = @todos.collect { |x| ( !x.done? and !x.context.hide? ) ? x:nil }.compact.size.to_s
end
return if request.xhr?
# fallback for standard requests
if @saved
flash["notice"] = 'Added new next action.'
redirect_to :action => 'tickler'
else
flash["warning"] = 'The next action was not added. Please try again.'
redirect_to :action => 'tickler'
end
rescue
if request.xhr? # be sure to include an error.rjs
render :action => 'error'
else
flash["warning"] = 'An error occurred on the server.'
render :action => 'tickler'
end
end
def edit_action
self.init
@ -90,6 +128,13 @@ class TodoController < ApplicationController
render :partial => 'action_edit_form', :object => item
end
def edit_deferred_action
self.init
item = check_user_return_item
render :partial => 'action_edit_deferred_form', :object => item
end
# Toggles the 'done' status of the action
#
def toggle_check
@ -133,6 +178,21 @@ class TodoController < ApplicationController
@saved = @item.save
end
def deferred_update_action
#self.init
@tickle = check_user_return_item
@original_item_context_id = @tickle.context_id
@tickle.attributes = @params["item"]
if @tickle.due?
@tickle.due = Date.strptime(@params["item"]["due"], @user.preferences["date_format"])
else
@tickle.due = ""
end
@saved = @tickle.save
end
# Delete a next action
#
def destroy_action
@ -191,7 +251,31 @@ class TodoController < ApplicationController
self.init
@page_title = "TRACKS::Feeds"
end
def tickler
self.init
@page_title = "TRACKS::Tickler"
@tickles = @user.todos.find(:all, :conditions => ['type = ?', "Deferred"], :order => "show_from ASC")
@count = @tickles.size
end
# Called by periodically_call_remote
# Check for any due tickler items, change them to type Immediate and show
# on the page
#
def check_tickler
self.init
now = Date.today()
@due_tickles = @user.todos.find(:all, :conditions => ['type = ? AND (show_from < ? OR show_from = ?)', "Deferred", now, now ], :order => "show_from ASC")
unless @due_tickles.empty?
# Change the due tickles to type "Immediate"
@due_tickles.each do |t|
t[:type] = "Immediate"
t.show_from = nil
t.save
end
end
end
protected
@ -208,8 +292,9 @@ class TodoController < ApplicationController
def init
@projects = @user.projects
@contexts = @user.contexts
@todos = @user.todos
@done = @todos.find(:all, :conditions => ["done = ?", true])
@todos = Todo.find(:all, :conditions => ['user_id = ? and type = ?', @user.id, "Immediate"])
@done = Todo.find(:all, :conditions => ['user_id = ? and done = ?', @user.id, true])
# @todos = @todos.collect { |x| (x.class == Immediate) ? x : nil }.compact
end
end

View file

@ -7,13 +7,20 @@ module TodoHelper
count = Todo.find_all("done=0 AND context_id=#{context.id}").length
end
def form_remote_tag_edit_todo( item )
form_remote_tag( :url => { :controller => 'todo', :action => 'update_action', :id => item.id },
def form_remote_tag_edit_todo( item, type )
(type == "deferred") ? act = 'deferred_update_action' : act = 'update_action'
form_remote_tag( :url => { :controller => 'todo', :action => act, :id => item.id },
:html => { :id => "form-action-#{item.id}", :class => "inline-form" }
)
end
def link_to_remote_todo( item, handled_by)
def link_to_remote_todo( item, handled_by, type)
if type == "deferred"
act = "edit_deferred_action"
else
act = "edit_action"
end
str = link_to_remote( image_tag("blank", :title =>"Delete action", :class=>"delete_item"),
{:url => { :controller => handled_by, :action => "destroy_action", :id => item.id },
:confirm => "Are you sure that you want to delete the action, \'#{item.description}\'?"},
@ -23,7 +30,7 @@ module TodoHelper
{
:update => "form-action-#{item.id}",
:loading => visual_effect(:pulsate, "action-#{item.id}-edit-icon"),
:url => { :controller => "todo", :action => "edit_action", :id => item.id },
:url => { :controller => "todo", :action => act, :id => item.id },
:success => "Element.toggle('item-#{item.id}','action-#{item.id}-edit-form'); new Effect.Appear('action-#{item.id}-edit-form', { duration: .2 }); Form.focusFirstElement('form-action-#{item.id}')"
},
{
@ -88,6 +95,38 @@ module TodoHelper
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(due)
if due == nil
return ""
end
@now = Date.today
@days = due-@now
case @days
# overdue or due very soon! sound the alarm!
when -1000..-1
"<a title='" + format_date(due) + "'><span class=\"red\">Shown on " + (@days * -1).to_s + " days</span></a> "
when 0
"<a title='" + format_date(due) + "'><span class=\"amber\">Show Today</span></a> "
when 1
"<a title='" + format_date(due) + "'><span class=\"amber\">Show Tomorrow</span></a> "
# due 2-7 days away
when 2..7
if @user.preferences["due_style"] == "1"
"<a title='" + format_date(due) + "'><span class=\"orange\">Show on " + due.strftime("%A") + "</span></a> "
else
"<a title='" + format_date(due) + "'><span class=\"orange\">Show in " + @days.to_s + " days</span></a> "
end
# more than a week away - relax
else
"<a title='" + format_date(due) + "'><span class=\"green\">Show in " + @days.to_s + " days</span></a> "
end
end
def toggle_show_notes( item )
str = "<a href=\"javascript:Element.toggle('"
str << item.id.to_s

View file

@ -17,13 +17,13 @@ class Context < ActiveRecord::Base
end
def find_not_done_todos
todos = Todo.find :all, :conditions => ["todos.context_id = #{id} AND todos.done = ?", false],
todos = Todo.find :all, :conditions => ["todos.context_id = #{id} AND todos.done = ? AND type = ?", false, "Immediate"],
:include => [:context, :project],
:order => "due IS NULL, due ASC, created_at ASC"
end
def find_done_todos
todos = Todo.find :all, :conditions => ["todos.context_id = #{id} AND todos.done = ?", true],
todos = Todo.find :all, :conditions => ["todos.context_id = #{id} AND todos.done = ? AND type = ?", true, "Immediate"],
:include => [:context, :project],
:order => "due IS NULL, due ASC, created_at ASC",
:limit => @user.preferences["no_completed"].to_i

View file

@ -0,0 +1,2 @@
class Deferred < Todo
end

View file

@ -0,0 +1,2 @@
class Immediate < Todo
end

View file

@ -1 +1 @@
page.replace_html "status", "A server error has occurred"
page.replace_html "status", content_tag("div", "A server error has occurred", "class" => "warning")

View file

@ -31,6 +31,7 @@
<li><%= navigation_link("Home", {:controller => "todo", :action => "list"}, {:accesskey => "t", :title => "Home"} ) %></li>
<li><%= navigation_link( "Contexts", {:controller => "context", :action => "list"}, {:accesskey=>"c", :title=>"Contexts"} ) %></li>
<li><%= navigation_link( "Projects", {:controller => "project", :action => "list"}, {:accesskey=>"p", :title=>"Projects"} ) %></li>
<li><%= navigation_link( "Tickler", {:controller => "todo", :action => "tickler"}, :title => "Ticker" ) %></li>
<li><%= navigation_link( "Done", {:controller => "todo", :action => "completed"}, {:accesskey=>"d", :title=>"Completed"} ) %></li>
<li><%= navigation_link( "Notes", {:controller => "note", :action => "index"}, {:accesskey => "o", :title => "Show all notes"} ) %></li>
<li><%= navigation_link( "Preferences", {:controller => "user", :action => "preferences"}, {:accesskey => "u", :title => "Show my preferences"} ) %></li>
@ -47,7 +48,9 @@
<% unless @controller_name == 'feed' or session['noexpiry'] == "on" -%>
<%= periodically_call_remote( :url => {:controller => "login", :action => "check_expiry"},
:frequency => (5*60)) %>
<% end -%>
<% end -%>
<%= periodically_call_remote( :url => {:controller => "todo", :action => "check_tickler"},
:frequency => (10*60)) %>
<%= @content_for_layout %>
<div id="footer">

View file

@ -7,6 +7,10 @@
else
add_string = "Add a next action &#187;"
end
if request.request_uri == "/todo/tickler"
add_string = "Add a deferred action &#187;"
end
%>
<% hide_link ||= false %>
@ -21,6 +25,12 @@
<div id="status">
</div>
<!--[form:todo]-->
<% if request.request_uri == "/todo/tickler" -%>
<%= form_remote_tag(
:url => { :controller => controller.controller_name, :action => "add_deferred_item" },
:html=> { :id=>'todo-form-new-action', :name=>'todo', :class => 'inline-form' }) %>
<% end -%>
<%= form_remote_tag(
:url => { :controller => controller.controller_name, :action => "add_item" },
:html=> { :id=>'todo-form-new-action', :name=>'todo', :class => 'inline-form' }) %>
@ -43,7 +53,7 @@
{ :include_blank => true }, {"tabindex" => 4}) %><br />
<% end -%>
<label for="item_due">Due</label><br />
<label for="todo_due">Due</label><br />
<%= text_field("todo", "due", "size" => 10, "class" => "Date", "onFocus" => "Calendar.setup", "tabindex" => 5, "autocomplete" => "off") %>
<% if controller.controller_name == "project" -%>
@ -51,6 +61,12 @@
<% elsif controller.controller_name == "context" -%>
<%= hidden_field( "todo", "context_id", "value" => "#{@context.id}") %>
<% end -%>
<% if request.request_uri == "/todo/tickler" -%>
<br /> <label for="todo_show_from">Show from</label><br />
<%= date_select( "todo", "show_from", :start_year => Date.today.year, :order => [:year, :month, :day] ) %>
<% end -%>
<br /><br />
<input type="submit" value="Add item" tabindex="6">
<%= end_form_tag %><!--[eoform:todo]-->

View file

@ -0,0 +1,62 @@
<%
@item = action_edit_deferred_form
%>
<%= error_messages_for("item") %>
<%= hidden_field( "item", "id" ) %>
<table>
<tr>
<td class="label"><label for="item_description">Next action</label></td>
<td><%= text_field( "item", "description", "tabindex" => 1) %></td>
</tr>
<tr>
<td class="label"><label for="item_notes">Notes</label></td>
<td><%= text_area( "item", "notes", "cols" => 20, "rows" => 5, "tabindex" => 2) %></td>
</tr>
<tr>
<td class="label"><label for="item_context_id">Context</label></td>
<td><select name="item[context_id]" id="item_context_id" tabindex="3">
<% for @place in @contexts %>
<% if @item %>
<% if @place.id == @item.context_id %>
<option value="<%= @place.id %>" selected="selected"><%= @place.name %></option>
<% else %>
<option value="<%= @place.id %>"><%= @place.name %></option>
<% end %>
<% else %>
<option value="<%= @place.id %>"><%= @place.name %></option>
<% end %>
<% end %>
</select></td>
</tr>
<tr>
<td class="label"><label for="item_project_id">Project</label></td>
<td>
<select name="item[project_id]" id="item_project_id" tabindex="4">
<% if !@item.project_id? %>
<option selected="selected"></option>
<%= options_from_collection_for_select(@projects, "id", "name") %>
<% else %>
<option></option>
<%= options_from_collection_for_select(@projects, "id", "name", @item.project_id) %>
<% end %>
</select>
</td>
</tr>
<tr>
<td class="label"><label for="item_due">Due</td>
<td><input name="item[due]" id="due_<%= @item.id %>" type="text" value="<%= format_date(@item.due) %>" tabindex="5" size="10" onFocus="Calendar.setup" /></td>
</tr>
<tr>
<td class="label"><label for="item_show_from">Show from</label></td>
<td><%= date_select( "item", "show_from", :start_year => Date.today.year,
:order => [:year, :month, :day]) %></td>
</tr>
<tr>
<td><input type="hidden" name="on_project_page" value="true" /></td>
<td><input type="submit" value="Update" tabindex="6" />
<a href="javascript:void(0);" onclick="Element.toggle('item-<%= @item.id %>','action-<%= @item.id %>-edit-form');Form.reset('form-action-<%= @item.id %>');">Cancel</a></td>
</tr>
</table>
<%= calendar_setup( "due_#{@item.id}" ) %>
<% @item = nil %>

View file

@ -0,0 +1,46 @@
<%
add_string = "Add a deferred action &#187;"
%>
<% hide_link ||= false %>
<% unless hide_link -%>
<%= link_to_function(
add_string,
"Element.toggle('todo_new_action');Form.focusFirstElement('todo-form-new-action');",
{:title => "Add the next action", :accesskey => "n"}) %>
<% end -%>
<div id="todo_new_action" class="context_new" style="display:<%= hide_link ? 'block' : 'none'%>">
<div id="status">
</div>
<!--[form:todo]-->
<%= form_remote_tag(
:url => { :controller => "todo", :action => "add_deferred_item" },
:html=> { :id=>'todo-form-new-action', :name=>'todo', :class => 'inline-form' }) %>
<label for="todo_description">Description</label><br />
<%= text_field( "todo", "description", "size" => 25, "tabindex" => 1) %><br />
<label for="todo_notes">Notes</label><br />
<%= text_area( "todo", "notes", "cols" => 25, "rows" => 10, "tabindex" => 2) %><br />
<label for="todo_context_id">Context</label><br />
<%= collection_select( "todo", "context_id", @contexts, "id", "name",
{}, {"tabindex" => 3}) %><br />
<label for="todo_project_id">Project</label><br />
<%= collection_select( "todo", "project_id", @projects, "id", "name",
{ :include_blank => true }, {"tabindex" => 4}) %><br />
<label for="todo_due">Due</label><br />
<%= text_field("todo", "due", "size" => 10, "class" => "Date", "onFocus" => "Calendar.setup", "tabindex" => 5, "autocomplete" => "off") %><br />
<label for="todo_show_from">Show from</label><br />
<%= date_select( "todo", "show_from", :start_year => Date.today.year, :order => [:year, :month, :day] ) %>
<br /><br />
<input type="submit" value="Add item" tabindex="6">
<%= end_form_tag %><!--[eoform:todo]-->
<%= calendar_setup( "todo_due" ) %>
</div><!-- [end:todo-new-action] -->

View file

@ -1,7 +1,7 @@
<div id="item-<%= item.id %>-container" class="item-container">
<div id="item-<%= item.id %>">
<%= link_to_remote_todo( item, controller.controller_name ) %>
<%= link_to_remote_todo( item, controller.controller_name, "immediate" ) %>
<input type="checkbox" class="item-checkbox"
onclick="new Ajax.Request('<%= url_for :controller => controller.controller_name, :action => 'toggle_check', :id => item.id %>', {asynchronous:true, evalScripts:true});"
name="item_id" value="<%= item.id %>"<% if item.done? %> checked="checked" <% end %> />
@ -28,7 +28,7 @@
</div>
</div><!-- [end:item-item.id] -->
<div id="action-<%= item.id %>-edit-form" class="edit-form" style="display:none;">
<%= form_remote_tag_edit_todo( item ) -%>
<%= form_remote_tag_edit_todo( item, "immediate" ) -%>
<% #note: edit form will load here remotely -%>
<%= end_form_tag -%>
</div><!-- [end:action-item.id-edit-form] -->

View file

@ -0,0 +1,26 @@
<div id="item-<%= tickle.id %>-container" class="item-container">
<div id="item-<%= tickle.id %>">
<%= link_to_remote_todo( tickle, controller.controller_name, "deferred" ) %>
<div class="description">
<%= show_date( tickle.show_from ) -%>
<%= sanitize(tickle.description) %>
<% if tickle.due -%>
(action due on <%= tickle.due.to_s %>)
<% end -%>
<%= link_to( "[C]", { :controller => "context", :action => "show", :name => urlize(tickle.context.name) }, :title => "View context: #{tickle.context.name}" ) %>
<% if tickle.project_id -%>
<%= link_to( "[P]", { :controller => "project", :action => "show", :name => urlize(tickle.project.name) }, :title => "View project: #{tickle.project.name}" ) %>
<% end -%>
<% if tickle.notes? -%>
<%= toggle_show_notes( tickle ) %>
<% end -%>
</div>
</div><!-- [end:item-tickle.id] -->
<div id="action-<%= tickle.id %>-edit-form" class="edit-form" style="display:none;">
<%= form_remote_tag_edit_todo( tickle, "deferred" ) -%>
<% #note: edit form will load here remotely -%>
<%= end_form_tag -%>
</div><!-- [end:action-tickle.id-edit-form] -->
</div><!-- [end:item-tickle.id-container] -->

View file

@ -0,0 +1,12 @@
<div id="tickler" class="container project">
<h2>Deferred actions</h2>
<div class="items toggle_target">
<div id="empty-nd" style="display:<%= @tickles.empty? ? 'block' : 'none'%>;">
<div class="message"><p>Currently there are no deferred actions</p></div>
</div>
<%= render :partial => "tickle", :collection => @tickles %>
</div><!-- [end:items] -->
</div><!-- [end:tickler] -->

View file

@ -0,0 +1,13 @@
if @saved
page.hide "status"
page.replace_html "status", content_tag("div", "Added new next action", "class" => "confirmation")
page.visual_effect :appear, 'status', :duration => 0.5
page.replace_html "badge_count", @up_count
page.send :record, "Form.reset('todo-form-new-action');Form.focusFirstElement('todo-form-new-action')"
page.insert_html :bottom, "tickler", :partial => 'todo/tickle'
page.visual_effect :highlight, "item-#{@tickle.id}-container", :duration => 3
else
page.hide "status"
page.replace_html "status", content_tag("div", content_tag("h2", "#{pluralize(@tickle.errors.count, "error")} prohibited this record from being saved") + content_tag("p", "There were problems with the following fields:") + content_tag("ul", @tickle.errors.each_full { |msg| content_tag("li", msg) }), "id" => "ErrorExplanation", "class" => "ErrorExplanation")
page.visual_effect :appear, 'status', :duration => 0.5
end

View file

@ -0,0 +1,3 @@
unless @due_tickles.empty?
page.replace_html "info", content_tag("div", "#{@due_tickles.length.to_s} tickler items are now due - refresh the page to see them.", "class" => "warning")
end

View file

@ -0,0 +1,14 @@
if @saved
if @tickle.context_id == @original_item_context_id
page.replace_html "item-#{@tickle.id}-container", :partial => 'todo/tickle'
page.visual_effect :highlight, "item-#{@tickle.id}-container", :duration => 3
else
page["item-#{@tickle.id}-container"].remove
page.insert_html :bottom, "c#{@tickle.context_id}items", :partial => 'todo/tickle'
page.visual_effect :highlight, "item-#{@tickle.id}-container", :duration => 3
end
else
page.hide "info"
page.replace_html "info", content_tag("div", content_tag("div", "#{pluralize(@tickle.errors.count, "error")} prohibited this record from being saved", "id" => "warning", "class" => "warning") + content_tag("p", "There were problems with the following fields:") + content_tag("ul", @tickle.errors.each_full { |msg| content_tag("li", msg) }))
page.visual_effect :appear, 'info', :duration => 0.5
end

View file

@ -0,0 +1,16 @@
<div id="display_box">
<% for name in ["notice", "warning", "message"] %>
<% if flash[name] %>
<%= "<div id=\"#{name}\">#{flash[name]}</div>" %>
<% end %>
<% end %>
<%= render :partial => "tickler_items" %>
</div><!-- End of display_box -->
<div id="input_box">
<%= render :partial => "shared/add_new_item_form", :locals => {:hide_link => false, :msg => ""} %>
<%= render "shared/sidebar" %>
</div><!-- End of input box -->

View file

@ -0,0 +1,12 @@
class AddSubclassAttrToTodos < ActiveRecord::Migration
def self.up
add_column :todos, :type, :string, :null => false, :default => "Immediate"
add_column :todos, :show_from, :date
Todo.find(:all).each { |todo| todo.type = "Immediate" }
end
def self.down
remove_column :todos, :type
remove_column :todos, :show_from
end
end

View file

@ -2,7 +2,7 @@
# migrations feature of ActiveRecord to incrementally modify your database, and
# then regenerate this schema definition.
ActiveRecord::Schema.define(:version => 7) do
ActiveRecord::Schema.define(:version => 8) do
create_table "contexts", :force => true do |t|
t.column "name", :string, :default => "", :null => false
@ -45,6 +45,8 @@ ActiveRecord::Schema.define(:version => 7) do
t.column "completed", :datetime
t.column "project_id", :integer
t.column "user_id", :integer, :default => 1
t.column "type", :string, :default => "Immediate", :null => false
t.column "show_from", :date
end
create_table "users", :force => true do |t|

View file

@ -289,7 +289,7 @@ a.footer_link:hover {color: #fff; background-color: #cc3334 !important;}
text-align: center;
}
#message {
#message, .message {
padding: 2px;
border: 1px solid #CCC;
background-color: #D2D3D6;

5
tracks/test/fixtures/deferreds.yml vendored Normal file
View file

@ -0,0 +1,5 @@
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
first:
id: 1
another:
id: 2

5
tracks/test/fixtures/immediates.yml vendored Normal file
View file

@ -0,0 +1,5 @@
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
first:
id: 1
another:
id: 2

View file

@ -0,0 +1,10 @@
require File.dirname(__FILE__) + '/../test_helper'
class DeferredTest < Test::Unit::TestCase
fixtures :deferreds
# Replace this with your real tests.
def test_truth
assert true
end
end

View file

@ -0,0 +1,10 @@
require File.dirname(__FILE__) + '/../test_helper'
class ImmediateTest < Test::Unit::TestCase
fixtures :immediates
# Replace this with your real tests.
def test_truth
assert true
end
end