From fcaea3ce20495977240d2d75771636924a2b0c49 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Thu, 2 Oct 2008 17:28:49 +0200 Subject: [PATCH 1/3] first pass at adding calendar view --- app/controllers/todos_controller.rb | 34 +++++++++++++++++++++++++++-- app/views/todos/calendar.html.erb | 34 +++++++++++++++++++++++++++++ config/routes.rb | 2 ++ 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 app/views/todos/calendar.html.erb diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index 4e946f5c..e2bac2ce 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -4,7 +4,7 @@ class TodosController < ApplicationController skip_before_filter :login_required, :only => [:index] prepend_before_filter :login_or_feed_token_required, :only => [:index] - append_before_filter :init, :except => [ :destroy, :completed, :completed_archive, :check_deferred, :toggle_check, :toggle_star, :edit, :update, :create ] + append_before_filter :init, :except => [ :destroy, :completed, :completed_archive, :check_deferred, :toggle_check, :toggle_star, :edit, :update, :create, :calendar ] append_before_filter :get_todo_from_params, :only => [ :edit, :toggle_check, :toggle_star, :show, :update, :destroy ] session :off, :only => :index, :if => Proc.new { |req| is_feed_request(req) } @@ -404,7 +404,37 @@ class TodosController < ApplicationController format.js {render :action => 'update'} end end - + + def calendar + @source_view = params['_source_view'] || 'calendar' + @page_title = "TRACKS::Calendar" + + due_today_date = Time.zone.now + due_this_week_date = Time.zone.now.end_of_week + due_next_week_date = due_this_week_date + 7.days + due_this_month_date = Time.zone.now.end_of_month + + @due_today = current_user.todos.find(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due <= ?', 'active', 'deferred', due_today_date], + :order => "due") + @due_this_week = current_user.todos.find(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_today_date, due_this_week_date], + :order => "due") + @due_next_week = current_user.todos.find(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_this_week_date, due_next_week_date], + :order => "due") + @due_this_month = current_user.todos.find(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_next_week_date, due_this_month_date], + :order => "due") + @due_after_this_month = current_user.todos.find(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ?', 'active', 'deferred', due_this_month_date], + :order => "due") + + respond_to do |format| + format.html + format.ics { render :text => 'Not implemented yet', :status => 200 } + end + end private diff --git a/app/views/todos/calendar.html.erb b/app/views/todos/calendar.html.erb new file mode 100644 index 00000000..f887ffac --- /dev/null +++ b/app/views/todos/calendar.html.erb @@ -0,0 +1,34 @@ +
+ +
+

Due today

+ <%= render :partial => "todos/todo", :collection => @due_today %> +
+ +
+

Due this week

+ <%= render :partial => "todos/todo", :collection => @due_this_week %> +
+ +
+

Due next week

+ <%= render :partial => "todos/todo", :collection => @due_next_week %> +
+ +
+

Due rest of this month

+ <%= render :partial => "todos/todo", :collection => @due_this_month %> +
+ +
+

Due next month and later

+ <%= render :partial => "todos/todo", :collection => @due_after_this_month %> +
+ +
+
+ + +

+

iCal - Get this calendar in iCal-format to add to your own calendar

+
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index e0a91a43..222f7619 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,6 +46,8 @@ ActionController::Routing::Routes.draw do |map| todos.tag 'todos/tag/:name.m', :action => "tag", :format => 'm' todos.tag 'todos/tag/:name', :action => "tag", :name => /.*/ + todos.calendar 'calendar', :action => "calendar" + todos.mobile 'mobile', :action => "index", :format => 'm' todos.mobile_abbrev 'm', :action => "index", :format => 'm' todos.mobile_abbrev_new 'm/new', :action => "new", :format => 'm' From 541d6f5b69f943ebfb63c96b0573abeaf48678f9 Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Fri, 3 Oct 2008 22:44:58 +0200 Subject: [PATCH 2/3] add calendar to menu and implement ical tested ical with outlook 2007 --- app/controllers/todos_controller.rb | 31 +++++++++++++++++++--- app/views/layouts/standard.html.erb | 1 + app/views/todos/calendar.html.erb | 39 ++++++++++++++++++++-------- app/views/todos/calendar.ics.erb | 25 ++++++++++++++++++ app/views/todos/update.js.rjs | 6 +++++ config/routes.rb | 1 + public/images/x-office-calendar.png | Bin 0 -> 515 bytes 7 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 app/views/todos/calendar.ics.erb create mode 100644 public/images/x-office-calendar.png diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index e2bac2ce..beceb67a 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -2,8 +2,8 @@ class TodosController < ApplicationController helper :todos - skip_before_filter :login_required, :only => [:index] - prepend_before_filter :login_or_feed_token_required, :only => [:index] + skip_before_filter :login_required, :only => [:index, :calendar] + 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 ] append_before_filter :get_todo_from_params, :only => [ :edit, :toggle_check, :toggle_star, :show, :update, :destroy ] @@ -172,6 +172,7 @@ class TodosController < ApplicationController @original_item_context_id = @todo.context_id @original_item_project_id = @todo.project_id @original_item_was_deferred = @todo.deferred? + @original_item_due = @todo.due if params['todo']['project_id'].blank? && !params['project_name'].nil? if params['project_name'] == 'None' project = Project.null_object @@ -222,6 +223,25 @@ class TodosController < ApplicationController @context_changed = @original_item_context_id != @todo.context_id @todo_was_activated_from_deferred_state = @original_item_was_deferred && @todo.active? + @due_date_changed = @original_item_due != @todo.due + if @due_date_changed + due_today_date = Time.zone.now + due_this_week_date = Time.zone.now.end_of_week + due_next_week_date = due_this_week_date + 7.days + due_this_month_date = Time.zone.now.end_of_month + if @todo.due <= due_today_date + @new_due_id = "due_today" + elsif @todo.due <= due_this_week_date + @new_due_id = "due_this_week" + elsif @todo.due <= due_next_week_date + @new_due_id = "due_next_week" + elsif @todo.due <= due_this_month_date + @new_due_id = "due_this_month_week" + else + @new_due_id = "due_after_this_month" + end + end + if @context_changed determine_remaining_in_context_count(@original_item_context_id) else @@ -432,7 +452,12 @@ class TodosController < ApplicationController respond_to do |format| format.html - format.ics { render :text => 'Not implemented yet', :status => 200 } + format.ics { + @due_all = current_user.todos.find(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND NOT todos.due IS NULL', 'active', 'deferred'], + :order => "due") + render :action => 'calendar', :layout => false, :content_type => Mime::ICS + } end end diff --git a/app/views/layouts/standard.html.erb b/app/views/layouts/standard.html.erb index 21c9cdc4..42ea20d2 100644 --- a/app/views/layouts/standard.html.erb +++ b/app/views/layouts/standard.html.erb @@ -54,6 +54,7 @@ window.onload=function(){ <% if current_user.is_admin? -%>
  • <%= navigation_link("Admin", users_path, {:accesskey => "a", :title => "Add or delete users"} ) %>
  • <% end -%> +
  • <%= navigation_link(image_tag("x-office-calendar.png", :size => "16X16", :border => 0), calendar_path, :title => "Calendar of due actions" ) %>
  • <%= navigation_link(image_tag("recurring_menu16x16.png", :size => "16X16", :border => 0), {:controller => "recurring_todos", :action => "index"}, :title => "Manage recurring actions" ) %>
  • <%= navigation_link(image_tag("feed-icon.png", :size => "16X16", :border => 0), {:controller => "feedlist", :action => "index"}, :title => "See a list of available feeds" ) %>
  • <%= navigation_link(image_tag("menustar.gif", :size => "16X16", :border => 0), tag_path("starred"), :title => "See your starred actions" ) %>
  • diff --git a/app/views/todos/calendar.html.erb b/app/views/todos/calendar.html.erb index f887ffac..29860508 100644 --- a/app/views/todos/calendar.html.erb +++ b/app/views/todos/calendar.html.erb @@ -2,33 +2,50 @@

    Due today

    - <%= render :partial => "todos/todo", :collection => @due_today %> +
    + <%= render :partial => "todos/todo", :collection => @due_today %> +
    -

    Due this week

    - <%= render :partial => "todos/todo", :collection => @due_this_week %> +

    Due in rest of this week

    +
    + <%= render :partial => "todos/todo", :collection => @due_this_week %> +

    Due next week

    - <%= render :partial => "todos/todo", :collection => @due_next_week %> +
    + <%= render :partial => "todos/todo", :collection => @due_next_week %> +
    -

    Due rest of this month

    - <%= render :partial => "todos/todo", :collection => @due_this_month %> +

    Due in rest of this month

    +
    + <%= render :partial => "todos/todo", :collection => @due_this_month %> +

    Due next month and later

    - <%= render :partial => "todos/todo", :collection => @due_after_this_month %> +
    + <%= render :partial => "todos/todo", :collection => @due_after_this_month %> +
    -
    - +
    +

    -

    iCal - Get this calendar in iCal-format to add to your own calendar

    -
    \ No newline at end of file +

    <%= link_to('iCal', {:format => 'ics', :token => current_user.token}, :title => "iCal feed" ) %> + - Get this calendar in iCal format

    +
    + +<% +apply_behavior 'input.hide_tickler:click', :prevent_default => true do |page| + page << "alert('hiding action in tickler from calendar is not yet implemented');" +end +%> \ No newline at end of file diff --git a/app/views/todos/calendar.ics.erb b/app/views/todos/calendar.ics.erb new file mode 100644 index 00000000..af9d7682 --- /dev/null +++ b/app/views/todos/calendar.ics.erb @@ -0,0 +1,25 @@ +BEGIN:VCALENDAR +PRODID:-//TRACKS//<%= TRACKS_VERSION %>//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Tracks +<% for todo in @due_all -%> +BEGIN:VEVENT +DTSTART;VALUE=DATE:<%= todo.due.strftime("%Y%m%d") %> +DTEND;VALUE=DATE:<%= (todo.due+1.day).strftime("%Y%m%d") %> +DTSTAMP:<%= todo.due.strftime("%Y%m%dT%H%M%SZ") %> +UID:<%= todo_url(todo) %> +CLASS:PUBLIC +CATEGORIES:Tracks +CREATED:<%= todo.created_at.strftime("%Y%m%dT%H%M%SZ") %> +DESCRIPTION:<%= format_ical_notes(todo.notes) %> +LAST-MODIFIED:<%= todo.due.strftime("%Y%m%dT%H%M%SZ") %> +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:<%= todo.description %> +TRANSP:TRANSPARENT +END:VEVENT +<% end -%> +END:VCALENDAR \ No newline at end of file diff --git a/app/views/todos/update.js.rjs b/app/views/todos/update.js.rjs index dae71f41..a5c8e147 100644 --- a/app/views/todos/update.js.rjs +++ b/app/views/todos/update.js.rjs @@ -101,6 +101,12 @@ if @saved elsif source_view_is :stats page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type } page.visual_effect :highlight, dom_id(@todo), :duration => 3 + elsif source_view_is :calendar + if @due_date_changed + page[@todo].remove + page.insert_html :bottom, @new_due_id, :partial => 'todos/todo' + page.visual_effect :highlight, dom_id(@todo), :duration => 3 + end else logger.error "unexpected source_view '#{params[:_source_view]}'" end diff --git a/config/routes.rb b/config/routes.rb index 222f7619..50e29ff7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,6 +46,7 @@ ActionController::Routing::Routes.draw do |map| todos.tag 'todos/tag/:name.m', :action => "tag", :format => 'm' todos.tag 'todos/tag/:name', :action => "tag", :name => /.*/ + todos.calendar 'calendar.ics', :action => "calendar", :format => 'ics' todos.calendar 'calendar', :action => "calendar" todos.mobile 'mobile', :action => "index", :format => 'm' diff --git a/public/images/x-office-calendar.png b/public/images/x-office-calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..f8607e3303a23051686f1bbc1a34a10845a946d6 GIT binary patch literal 515 zcmV+e0{s1nP)WFU8GbZ8({Xk{QrNlj4iWF>9@00C!7L_t(I%bk)xOT$nU z#ZRP@U0O*0EIw!{{Spca{z32~C^$r0mkwfS1?i#$L+V)aO{8KKB6Mn91d+}i8d6^3 z*5yI2gA@{ywABM|xaVA!s(6kr0A3 zT_sId_x!zU{=#)#IF18{JK&td{dVy_9OCKu5%$!^)nx+!dVWX%Ktv!Scp!L0AdgTe z7BCnLkz7y0p4vD$J_Uf@yeHuY+^M&)tV!hZm5Vbl#*oR#h+V<5CSYtM5>PA_FdmO_ zcyJW(0}0M63bwa)!8s2BWLd^r{}riJDn5Zd;JRrjt3>X0y8vL$>mOYzng13n?oI#z002ovPDHLk FV1i6{%~JpX literal 0 HcmV?d00001 From 5d30512e9786ff75bf1dc114d30a4499bd411d0d Mon Sep 17 00:00:00 2001 From: Reinier Balt Date: Sat, 4 Oct 2008 15:56:22 +0200 Subject: [PATCH 3/3] add message when there are no actions in a container and handle state changes --- app/controllers/todos_controller.rb | 81 +++++++++++++++++++++++------ app/views/todos/calendar.html.erb | 17 ++++++ app/views/todos/destroy.js.rjs | 1 + app/views/todos/toggle_check.js.rjs | 3 +- app/views/todos/update.js.rjs | 11 ++++ 5 files changed, 97 insertions(+), 16 deletions(-) diff --git a/app/controllers/todos_controller.rb b/app/controllers/todos_controller.rb index beceb67a..091fa85a 100644 --- a/app/controllers/todos_controller.rb +++ b/app/controllers/todos_controller.rb @@ -128,8 +128,10 @@ class TodosController < ApplicationController # Toggles the 'done' status of the action # def toggle_check + @source_view = params['_source_view'] || 'todo' + @original_item_due = @todo.due @saved = @todo.toggle_completion! - + # check if this todo has a related recurring_todo. If so, create next todo check_for_next_todo if @saved @@ -139,6 +141,8 @@ class TodosController < ApplicationController determine_remaining_in_context_count(@todo.context_id) determine_down_count determine_completed_count if @todo.completed? + @original_item_due_id = get_due_id_for_calendar(@original_item_due) + @old_due_empty = is_old_due_empty(@original_item_due_id) end render end @@ -173,6 +177,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) + if params['todo']['project_id'].blank? && !params['project_name'].nil? if params['project_name'] == 'None' project = Project.null_object @@ -225,21 +231,13 @@ class TodosController < ApplicationController @due_date_changed = @original_item_due != @todo.due if @due_date_changed - due_today_date = Time.zone.now - due_this_week_date = Time.zone.now.end_of_week - due_next_week_date = due_this_week_date + 7.days - due_this_month_date = Time.zone.now.end_of_month - if @todo.due <= due_today_date - @new_due_id = "due_today" - elsif @todo.due <= due_this_week_date - @new_due_id = "due_this_week" - elsif @todo.due <= due_next_week_date - @new_due_id = "due_next_week" - elsif @todo.due <= due_this_month_date - @new_due_id = "due_this_month_week" + @old_due_empty = is_old_due_empty(@original_item_due_id) + if @todo.due.nil? + # do not act further on date change when date is changed to nil + @due_date_changed = false else - @new_due_id = "due_after_this_month" - end + @new_due_id = get_due_id_for_calendar(@todo.due) + end end if @context_changed @@ -271,6 +269,7 @@ class TodosController < ApplicationController def destroy @todo = get_todo_from_params + @original_item_due = @todo.due @context_id = @todo.context_id @project_id = @todo.project_id @@ -297,6 +296,8 @@ class TodosController < ApplicationController if source_view_is_one_of(:todo, :deferred) determine_remaining_in_context_count(@context_id) end + @original_item_due_id = get_due_id_for_calendar(@original_item_due) + @old_due_empty = is_old_due_empty(@original_item_due_id) end render end @@ -435,18 +436,23 @@ class TodosController < ApplicationController due_this_month_date = Time.zone.now.end_of_month @due_today = current_user.todos.find(:all, + :include => [:taggings, :tags], :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due <= ?', 'active', 'deferred', due_today_date], :order => "due") @due_this_week = current_user.todos.find(:all, + :include => [:taggings, :tags], :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_today_date, due_this_week_date], :order => "due") @due_next_week = current_user.todos.find(:all, + :include => [:taggings, :tags], :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_this_week_date, due_next_week_date], :order => "due") @due_this_month = current_user.todos.find(:all, + :include => [:taggings, :tags], :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_next_week_date, due_this_month_date], :order => "due") @due_after_this_month = current_user.todos.find(:all, + :include => [:taggings, :tags], :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ?', 'active', 'deferred', due_this_month_date], :order => "due") @@ -773,6 +779,51 @@ class TodosController < ApplicationController end end end + + def get_due_id_for_calendar(due) + due_today_date = Time.zone.now + due_this_week_date = Time.zone.now.end_of_week + due_next_week_date = due_this_week_date + 7.days + due_this_month_date = Time.zone.now.end_of_month + if due <= due_today_date + new_due_id = "due_today" + elsif due <= due_this_week_date + new_due_id = "due_this_week" + elsif due <= due_next_week_date + new_due_id = "due_next_week" + elsif due <= due_this_month_date + new_due_id = "due_this_month" + else + new_due_id = "due_after_this_month" + end + return new_due_id + end + + def is_old_due_empty(id) + due_today_date = Time.zone.now + due_this_week_date = Time.zone.now.end_of_week + due_next_week_date = due_this_week_date + 7.days + due_this_month_date = Time.zone.now.end_of_month + case id + when "due_today" + return 0 == current_user.todos.count(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due <= ?', 'active', 'deferred', due_today_date]) + when "due_this_week" + return 0 == current_user.todos.count(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_today_date, due_this_week_date]) + when "due_next_week" + return 0 == current_user.todos.count(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_this_week_date, due_next_week_date]) + when "due_this_month" + return 0 == current_user.todos.count(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ? AND todos.due <= ?', 'active', 'deferred', due_next_week_date, due_this_month_date]) + when "due_after_this_month" + return 0 == current_user.todos.count(:all, + :conditions => ['(todos.state = ? OR todos.state = ?) AND todos.due > ?', 'active', 'deferred', due_this_month_date]) + else + raise Exception.new, "unknown due id for calendar: '#{id}'" + end + end class FindConditionBuilder diff --git a/app/views/todos/calendar.html.erb b/app/views/todos/calendar.html.erb index 29860508..056c0cff 100644 --- a/app/views/todos/calendar.html.erb +++ b/app/views/todos/calendar.html.erb @@ -2,6 +2,9 @@

    Due today

    +
    > + No actions due today +
    <%= render :partial => "todos/todo", :collection => @due_today %>
    @@ -9,6 +12,9 @@

    Due in rest of this week

    +
    > + No actions due in rest of this week +
    <%= render :partial => "todos/todo", :collection => @due_this_week %>
    @@ -16,6 +22,9 @@

    Due next week

    +
    > + No actions due in next week +
    <%= render :partial => "todos/todo", :collection => @due_next_week %>
    @@ -23,6 +32,9 @@

    Due in rest of this month

    +
    > + No actions due in rest of this month +
    <%= render :partial => "todos/todo", :collection => @due_this_month %>
    @@ -30,6 +42,9 @@

    Due next month and later

    +
    > + No actions due after this month +
    <%= render :partial => "todos/todo", :collection => @due_after_this_month %>
    @@ -37,9 +52,11 @@
    +

    <%= link_to('iCal', {:format => 'ics', :token => current_user.token}, :title => "iCal feed" ) %> - Get this calendar in iCal format

    diff --git a/app/views/todos/destroy.js.rjs b/app/views/todos/destroy.js.rjs index 7f37f87d..ac1ba3d4 100644 --- a/app/views/todos/destroy.js.rjs +++ b/app/views/todos/destroy.js.rjs @@ -1,5 +1,6 @@ if @saved page[@todo].remove + page.show "empty_"+@original_item_due_id if @old_due_empty page['badge_count'].replace_html @down_count # remove context if empty diff --git a/app/views/todos/toggle_check.js.rjs b/app/views/todos/toggle_check.js.rjs index 1462162a..d862862c 100644 --- a/app/views/todos/toggle_check.js.rjs +++ b/app/views/todos/toggle_check.js.rjs @@ -1,5 +1,6 @@ if @saved page[@todo].remove + page.show "empty_"+@original_item_due_id if @old_due_empty if @todo.completed? # completed todos move from their context to the completed container @@ -26,7 +27,7 @@ if @saved page.notify :notice, "There is no next action after the recurring action you just finished. The recurrence is completed", 6.0 if @new_recurring_todo.nil? end end - + else # todo is activated from completed container page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id(@todo) diff --git a/app/views/todos/update.js.rjs b/app/views/todos/update.js.rjs index a5c8e147..2f263b54 100644 --- a/app/views/todos/update.js.rjs +++ b/app/views/todos/update.js.rjs @@ -104,8 +104,19 @@ if @saved elsif source_view_is :calendar if @due_date_changed page[@todo].remove + page.show "empty_"+@original_item_due_id if @old_due_empty + page.hide "empty_"+@new_due_id page.insert_html :bottom, @new_due_id, :partial => 'todos/todo' page.visual_effect :highlight, dom_id(@todo), :duration => 3 + else + if @todo.due.nil? + # due date removed + page[@todo].remove + page.show "empty_"+@original_item_due_id if @old_due_empty + else + page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type } + page.visual_effect :highlight, dom_id(@todo), :duration => 3 + end end else logger.error "unexpected source_view '#{params[:_source_view]}'"