add recurring todos to tracks

This commit is contained in:
Reinier Balt 2008-07-19 20:27:45 +02:00
parent c46f0a8e04
commit 8bc41e2cb0
41 changed files with 2576 additions and 632 deletions

View file

@ -1,5 +1,6 @@
# The filters added to this controller will be run for all controllers in the application.
# Likewise will all the methods added be available for all controllers.
# The filters added to this controller will be run for all controllers in the
# application. Likewise will all the methods added be available for all
# controllers.
require_dependency "login_system"
require_dependency "tracks/source_view"
@ -47,11 +48,12 @@ class ApplicationController < ActionController::Base
# http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions
unless session == nil
return if @controller_name == 'feed' or session['noexpiry'] == "on"
# If the method is called by the feed controller (which we don't have under session control)
# or if we checked the box to keep logged in on login
# don't set the session expiry time.
# If the method is called by the feed controller (which we don't have
# under session control) or if we checked the box to keep logged in on
# login don't set the session expiry time.
if session
# Get expiry time (allow ten seconds window for the case where we have none)
# Get expiry time (allow ten seconds window for the case where we have
# none)
expiry_time = session['expiry_time'] || Time.now + 10
if expiry_time < Time.now
# Too late, matey... bang goes your session!
@ -80,8 +82,8 @@ class ApplicationController < ActionController::Base
# end
# end
# Returns a count of next actions in the given context or project
# The result is count and a string descriptor, correctly pluralised if there are no
# Returns a count of next actions in the given context or project The result
# is count and a string descriptor, correctly pluralised if there are no
# actions or multiple actions
#
def count_undone_todos_phrase(todos_parent, string="actions")
@ -105,8 +107,8 @@ class ApplicationController < ActionController::Base
count || 0
end
# Convert a date object to the format specified in the user's preferences
# in config/settings.yml
# Convert a date object to the format specified in the user's preferences in
# config/settings.yml
#
def format_date(date)
if date
@ -118,9 +120,9 @@ class ApplicationController < ActionController::Base
formatted_date
end
# Uses RedCloth to transform text using either Textile or Markdown
# Need to require redcloth above
# RedCloth 3.0 or greater is needed to use Markdown, otherwise it only handles Textile
# Uses RedCloth to transform text using either Textile or Markdown Need to
# require redcloth above RedCloth 3.0 or greater is needed to use Markdown,
# otherwise it only handles Textile
#
def markdown(text)
RedCloth.new(text).to_html
@ -130,21 +132,19 @@ class ApplicationController < ActionController::Base
Hash[*projects.reject{ |p| p.default_context.nil? }.map{ |p| [p.name, p.default_context.name] }.flatten].to_json
end
# Here's the concept behind this "mobile content negotiation" hack:
# In addition to the main, AJAXy Web UI, Tracks has a lightweight
# low-feature 'mobile' version designed to be suitablef or use
# from a phone or PDA. It makes some sense that tne pages of that
# mobile version are simply alternate representations of the same
# Todo resources. The implementation goal was to treat mobile
# as another format and be able to use respond_to to render both
# versions. Unfortunately, I ran into a lot of trouble simply
# registering a new mime type 'text/html' with format :m because
# :html already is linked to that mime type and the new
# registration was forcing all html requests to be rendered in
# the mobile view. The before_filter and after_filter hackery
# below accomplishs that implementation goal by using a 'fake'
# mime type during the processing and then setting it to
# 'text/html' in an 'after_filter' -LKM 2007-04-01
# Here's the concept behind this "mobile content negotiation" hack: In
# addition to the main, AJAXy Web UI, Tracks has a lightweight low-feature
# 'mobile' version designed to be suitablef or use from a phone or PDA. It
# makes some sense that tne pages of that mobile version are simply alternate
# representations of the same Todo resources. The implementation goal was to
# treat mobile as another format and be able to use respond_to to render both
# versions. Unfortunately, I ran into a lot of trouble simply registering a
# new mime type 'text/html' with format :m because :html already is linked to
# that mime type and the new registration was forcing all html requests to be
# rendered in the mobile view. The before_filter and after_filter hackery
# below accomplishs that implementation goal by using a 'fake' mime type
# during the processing and then setting it to 'text/html' in an
# 'after_filter' -LKM 2007-04-01
def mobile?
return params[:format] == 'm' || response.content_type == MOBILE_CONTENT_TYPE
end
@ -220,9 +220,9 @@ class ApplicationController < ActionController::Base
end
end
# Set the contents of the flash message from a controller
# Usage: notify :warning, "This is the message"
# Sets the flash of type 'warning' to "This is the message"
# Set the contents of the flash message from a controller Usage: notify
# :warning, "This is the message" Sets the flash of type 'warning' to "This is
# the message"
def notify(type, message)
flash[type] = message
logger.error("ERROR: #{message}") if type == :error
@ -232,4 +232,28 @@ class ApplicationController < ActionController::Base
Time.zone = current_user.prefs.time_zone if logged_in?
end
def create_todo_from_recurring_todo(rt, date=nil)
# create todo and initialize with data from recurring_todo rt
todo = current_user.todos.build( { :description => rt.description, :notes => rt.notes, :project_id => rt.project_id, :context_id => rt.context_id})
# set dates
todo.due = rt.get_due_date(date)
todo.show_from = rt.get_show_from_date(date)
todo.recurring_todo_id = rt.id
saved = todo.save
if saved
todo.tag_with(rt.tag_list, current_user)
todo.tags.reload
end
# increate number of occurences created from recurring todo
rt.inc_occurences
# mark recurring todo complete if there are no next actions left
checkdate = todo.due.nil? ? todo.show_from : todo.due
rt.toggle_completion! unless rt.has_next_todo(checkdate)
return saved ? todo : nil
end
end

View file

@ -0,0 +1,237 @@
class RecurringTodosController < ApplicationController
helper :todos, :recurring_todos
append_before_filter :init, :only => [:index, :new, :edit]
append_before_filter :get_recurring_todo_from_param, :only => [:destroy, :toggle_check, :toggle_star, :edit, :update]
def index
@recurring_todos = current_user.recurring_todos.find(:all, :conditions => ["state = ?", "active"])
@completed_recurring_todos = current_user.recurring_todos.find(:all, :conditions => ["state = ?", "completed"])
@no_recurring_todos = @recurring_todos.size == 0
@no_completed_recurring_todos = @completed_recurring_todos.size == 0
@count = @recurring_todos.size
end
def new
end
def show
end
def edit
respond_to do |format|
format.js
end
end
def update
@recurring_todo.tag_with(params[:tag_list], current_user) if params[:tag_list]
@original_item_context_id = @recurring_todo.context_id
@original_item_project_id = @recurring_todo.project_id
# we needed to rename the recurring_period selector in the edit form
# because the form for a new recurring todo and the edit form are on the
# same page.
params['recurring_todo']['recurring_period']=params['recurring_edit_todo']['recurring_period']
# update project
if params['recurring_todo']['project_id'].blank? && !params['project_name'].nil?
if params['project_name'] == 'None'
project = Project.null_object
else
project = current_user.projects.find_by_name(params['project_name'].strip)
unless project
project = current_user.projects.build
project.name = params['project_name'].strip
project.save
@new_project_created = true
end
end
params["recurring_todo"]["project_id"] = project.id
end
# update context
if params['recurring_todo']['context_id'].blank? && !params['context_name'].blank?
context = current_user.contexts.find_by_name(params['context_name'].strip)
unless context
context = current_user.contexts.build
context.name = params['context_name'].strip
context.save
@new_context_created = true
end
params["recurring_todo"]["context_id"] = context.id
end
params["recurring_todo"]["weekly_return_monday"]=' ' if params["recurring_todo"]["weekly_return_monday"].nil?
params["recurring_todo"]["weekly_return_tuesday"]=' ' if params["recurring_todo"]["weekly_return_tuesday"].nil?
params["recurring_todo"]["weekly_return_wednesday"]=' ' if params["recurring_todo"]["weekly_return_wednesday"].nil?
params["recurring_todo"]["weekly_return_thursday"]=' ' if params["recurring_todo"]["weekly_return_thursday"].nil?
params["recurring_todo"]["weekly_return_friday"]=' ' if params["recurring_todo"]["weekly_return_friday"].nil?
params["recurring_todo"]["weekly_return_saturday"]=' ' if params["recurring_todo"]["weekly_return_saturday"].nil?
params["recurring_todo"]["weekly_return_sunday"]=' ' if params["recurring_todo"]["weekly_return_sunday"].nil?
@saved = @recurring_todo.update_attributes params["recurring_todo"]
respond_to do |format|
format.js
end
end
def create
p = RecurringTodoCreateParamsHelper.new(params)
@recurring_todo = current_user.recurring_todos.build(p.attributes)
if p.project_specified_by_name?
project = current_user.projects.find_or_create_by_name(p.project_name)
@new_project_created = project.new_record_before_save?
@recurring_todo.project_id = project.id
end
if p.context_specified_by_name?
context = current_user.contexts.find_or_create_by_name(p.context_name)
@new_context_created = context.new_record_before_save?
@recurring_todo.context_id = context.id
end
@recurring_saved = @recurring_todo.save
unless (@recurring_saved == false) || p.tag_list.blank?
@recurring_todo.tag_with(p.tag_list, current_user)
@recurring_todo.tags.reload
end
if @recurring_saved
@message = "The recurring todo was saved"
@todo_saved = create_todo_from_recurring_todo(@recurring_todo).nil? == false
if @todo_saved
@message += " / created a new todo"
else
@message += " / did not create todo"
end
@count = current_user.recurring_todos.count(:all, :conditions => ["state = ?", "active"])
else
@message = "Error saving recurring todo"
end
respond_to do |format|
format.js
end
end
def destroy
# remove all references to this recurring todo
@todos = current_user.todos.find(:all, {:conditions => ["recurring_todo_id = ?", params[:id]]})
@number_of_todos = @todos.size
@todos.each do |t|
t.recurring_todo_id = nil
t.save
end
# delete the recurring todo
@saved = @recurring_todo.destroy
@remaining = current_user.recurring_todos.count(:all)
respond_to do |format|
format.html do
if @saved
notify :notice, "Successfully deleted recurring action", 2.0
redirect_to :action => 'index'
else
notify :error, "Failed to delete the recurring action", 2.0
redirect_to :action => 'index'
end
end
format.js do
render
end
end
end
def toggle_check
@saved = @recurring_todo.toggle_completion!
@count = current_user.recurring_todos.count(:all, :conditions => ["state = ?", "active"])
@remaining = @count
if @recurring_todo.active?
@remaining = current_user.recurring_todos.count(:all, :conditions => ["state = ?", 'completed'])
# from completed back to active -> check if there is an active todo
@active_todos = current_user.todos.count(:all, {:conditions => ["state = ? AND recurring_todo_id = ?", 'active',params[:id]]})
# create todo if there is no active todo belonging to the activated
# recurring_todo
@new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo) if @active_todos == 0
end
respond_to do |format|
format.js
end
end
def toggle_star
@recurring_todo.toggle_star!
@saved = @recurring_todo.save!
respond_to do |format|
format.js
end
end
class RecurringTodoCreateParamsHelper
def initialize(params)
@params = params['request'] || params
@attributes = params['request'] && params['request']['recurring_todo'] || params['recurring_todo']
end
def attributes
@attributes
end
def project_name
@params['project_name'].strip unless @params['project_name'].nil?
end
def context_name
@params['context_name'].strip unless @params['context_name'].nil?
end
def tag_list
@params['tag_list']
end
def project_specified_by_name?
return false unless @attributes['project_id'].blank?
return false if project_name.blank?
return false if project_name == 'None'
true
end
def context_specified_by_name?
return false unless @attributes['context_id'].blank?
return false if context_name.blank?
true
end
end
private
def init
@days_of_week = [ ['Sunday',0], ['Monday',1], ['Tuesday', 2], ['Wednesday',3], ['Thursday',4], ['Friday',5], ['Saturday',6]]
@months_of_year = [
['January',1], ['Februari',2], ['March', 3], ['April',4], ['May',5], ['June',6],
['July',7], ['August',8], ['September',9], ['October', 10], ['November', 11], ['December',12]]
@xth_day = [['first',1],['second',2],['third',3],['fourth',4],['last',5]]
@projects = current_user.projects.find(:all, :include => [:default_context])
@contexts = current_user.contexts.find(:all)
@default_project_context_name_map = build_default_project_context_name_map(@projects).to_json
end
def get_recurring_todo_from_param
@recurring_todo = current_user.recurring_todos.find(params[:id])
end
end

View file

@ -121,6 +121,10 @@ class TodosController < ApplicationController
#
def toggle_check
@saved = @todo.toggle_completion!
# check if this todo has a related recurring_todo. If so, create next todo
check_for_next_todo if @saved
respond_to do |format|
format.js do
if @saved
@ -235,6 +239,10 @@ class TodosController < ApplicationController
@todo = get_todo_from_params
@context_id = @todo.context_id
@project_id = @todo.project_id
# check if this todo has a related recurring_todo. If so, create next todo
check_for_next_todo
@saved = @todo.destroy
respond_to do |format|
@ -644,6 +652,19 @@ class TodosController < ApplicationController
['rss','atom','txt','ics'].include?(req.parameters[:format])
end
def check_for_next_todo
# check if this todo has a related recurring_todo. If so, create next todo
@new_recurring_todo = nil
@recurring_todo = nil
if @todo.from_recurring_todo?
@recurring_todo = current_user.recurring_todos.find(@todo.recurring_todo_id)
if @recurring_todo.active? && @recurring_todo.has_next_todo(@todo.due)
date = @todo.due >= Date.today() ? @todo.due : Date.today()-1.day
@new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo, date)
end
end
end
class FindConditionBuilder
def initialize

View file

@ -0,0 +1,73 @@
module RecurringTodosHelper
def recurrence_time_span(rt)
case rt.ends_on
when "no_end_date"
return ""
when "ends_on_number_of_times"
return "for "+rt.number_of_occurences.to_s + " times"
when "ends_on_end_date"
starts = rt.start_from.nil? ? "" : "from " + format_date(rt.start_from)
ends = rt.end_date.nil? ? "" : " until " + format_date(rt.end_date)
return starts+ends
else
raise Exception.new, "unknown recurrence time span selection (#{self.ends_on})"
end
end
def recurrence_target(rt)
case rt.target
when 'due_date'
return "due"
when 'show_from_date'
return "show"
else
return "ERROR"
end
end
def recurring_todo_tag_list
tags_except_starred = @recurring_todo.tags.reject{|t| t.name == Todo::STARRED_TAG_NAME}
tag_list = tags_except_starred.collect{|t| "<span class=\"tag #{t.name.gsub(' ','-')}\">" +
# link_to(t.name, :controller => "todos", :action => "tag", :id =>
# t.name) + TODO: tag view for recurring_todos (yet?)
t.name +
"</span>"}.join('')
"<span class='tags'>#{tag_list}</span>"
end
def recurring_todo_remote_delete_icon
str = link_to( image_tag_for_delete,
recurring_todo_path(@recurring_todo), :id => "delete_icon_"+@recurring_todo.id.to_s,
:class => "icon delete_icon", :title => "delete the recurring action '#{@recurring_todo.description}'")
set_behavior_for_delete_icon
str
end
def recurring_todo_remote_star_icon
str = link_to( image_tag_for_star(@recurring_todo),
toggle_star_recurring_todo_path(@recurring_todo),
:class => "icon star_item", :title => "star the action '#{@recurring_todo.description}'")
set_behavior_for_star_icon
str
end
def recurring_todo_remote_edit_icon
if !@recurring_todo.completed?
str = link_to( image_tag_for_edit(@recurring_todo),
edit_recurring_todo_path(@recurring_todo),
:class => "icon edit_icon")
set_behavior_for_edit_icon
else
str = '<a class="icon">' + image_tag("blank.png") + "</a> "
end
str
end
def recurring_todo_remote_toggle_checkbox
str = check_box_tag('item_id', toggle_check_recurring_todo_path(@recurring_todo), @recurring_todo.completed?, :class => 'item-checkbox')
set_behavior_for_toggle_checkbox
str
end
end

View file

@ -66,7 +66,7 @@ module TodosHelper
def remote_edit_icon
if !@todo.completed?
str = link_to( image_tag_for_edit,
str = link_to( image_tag_for_edit(@todo),
edit_todo_path(@todo),
:class => "icon edit_icon")
set_behavior_for_edit_icon
@ -205,12 +205,12 @@ module TodosHelper
javascript_tag str
end
def item_container_id
def item_container_id (todo)
if source_view_is :project
return "p#{@todo.project_id}" if @todo.active?
return "tickler" if @todo.deferred?
return "p#{todo.project_id}" if todo.active?
return "tickler" if todo.deferred?
end
return "c#{@todo.context_id}"
return "c#{todo.context_id}"
end
def should_show_new_item
@ -272,8 +272,8 @@ module TodosHelper
image_tag("blank.png", :title =>"Delete action", :class=>"delete_item")
end
def image_tag_for_edit
image_tag("blank.png", :title =>"Edit action", :class=>"edit_item", :id=> dom_id(@todo, 'edit_icon'))
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)

View file

@ -0,0 +1,517 @@
class RecurringTodo < ActiveRecord::Base
belongs_to :context
belongs_to :project
belongs_to :user
attr_protected :user
acts_as_state_machine :initial => :active, :column => 'state'
state :active, :enter => Proc.new { |t|
t[:show_from], t.completed_at = nil, nil
t.occurences_count = 0
}
state :completed, :enter => Proc.new { |t| t.completed_at = Time.now.utc }, :exit => Proc.new { |t| t.completed_at = nil }
validates_presence_of :description
validates_length_of :description, :maximum => 100
validates_length_of :notes, :maximum => 60000, :allow_nil => true
validates_presence_of :context
event :complete do
transitions :to => :completed, :from => [:active]
end
event :activate do
transitions :to => :active, :from => [:completed]
end
# the following recurrence patterns can be stored:
#
# daily todos - recurrence_period = 'daily'
# every nth day - nth stored in every_other1
# every work day - only_work_days = true
# tracks will choose between both options using only_work_days
# weekly todos - recurrence_period = 'weekly'
# every nth week on a specific day -
# nth stored in every_other1 and the specific day is stored in every_day
# monthly todos - recurrence_period = 'monthly'
# every day x of nth month - x stored in every_other1 and nth is stored in every_other2
# the xth y-day of every nth month (the forth tuesday of every 2 months) -
# x stored in every_other3, y stored in every_count, nth stored in every_other2
# choosing between both options is done on recurrence_selector where 0 is
# for first type and 1 for second type
# yearly todos - recurrence_period = 'yearly'
# every day x of month y - x is stored in every_other1, y is stored in every_other2
# the x-th day y of month z (the forth tuesday of september) -
# x is stored in every_other3, y is stored in every_count, z is stored in every_other2
# choosing between both options is done on recurrence_selector where 0 is
# for first type and 1 for second type
# DAILY
def daily_selector=(selector)
case selector
when 'daily_every_x_day'
self.only_work_days = false
when 'daily_every_work_day'
self.only_work_days = true
end
end
def daily_every_x_days=(x)
if recurring_period=='daily'
self.every_other1 = x
end
end
# WEEKLY
def weekly_every_x_week=(x)
if recurring_period=='weekly'
self.every_other1 = x
end
end
def switch_week_day (day, position)
if self.every_day.nil?
self.every_day=' '
end
self.every_day = self.every_day[0,position] + day + self.every_day[position+1,self.every_day.length]
end
def weekly_return_monday=(selector)
if recurring_period=='weekly'
switch_week_day(selector,1)
end
end
def weekly_return_tuesday=(selector)
switch_week_day(selector,2) if recurring_period=='weekly'
end
def weekly_return_wednesday=(selector)
switch_week_day(selector,3) if recurring_period=='weekly'
end
def weekly_return_thursday=(selector)
switch_week_day(selector,4) if recurring_period=='weekly'
end
def weekly_return_friday=(selector)
switch_week_day(selector,5) if recurring_period=='weekly'
end
def weekly_return_saturday=(selector)
switch_week_day(selector,6) if recurring_period=='weekly'
end
def weekly_return_sunday=(selector)
switch_week_day(selector,0) if recurring_period=='weekly'
end
def on_xday(n)
unless self.every_day.nil?
return self.every_day[n,1] == ' ' ? false : true
else
return false
end
end
def on_monday
return on_xday(1)
end
def on_tuesday
return on_xday(2)
end
def on_wednesday
return on_xday(3)
end
def on_thursday
return on_xday(4)
end
def on_friday
return on_xday(5)
end
def on_saturday
return on_xday(6)
end
def on_sunday
return on_xday(0)
end
# MONTHLY
def monthly_selector=(selector)
if recurring_period=='monthly'
self.recurrence_selector= (selector=='monthly_every_x_day')? 0 : 1
end
# todo
end
def monthly_every_x_day=(x)
if recurring_period=='monthly'
self.every_other1 = x
end
end
def monthly_every_x_month=(x)
if recurring_period=='monthly'
self.every_other2 = x
end
end
def monthly_every_xth_day=(x)
if recurring_period=='monthly'
self.every_other3 = x
end
end
def monthly_day_of_week=(dow)
if recurring_period=='monthly'
self.every_count = dow
end
end
# YEARLY
def yearly_selector=(selector)
if recurring_period=='yearly'
self.recurrence_selector = (selector=='yearly_every_x_day') ? 0 : 1
end
end
def yearly_month_of_year=(moy)
if recurring_period=='yearly'
self.every_other2 = moy
end
end
def yearly_every_x_day=(x)
if recurring_period=='yearly'
self.every_other1 = x
end
end
def yearly_every_xth_day=(x)
if recurring_period=='yearly'
self.every_other3 = x
end
end
def yearly_day_of_week=(dow)
if recurring_period=='yearly'
self.every_count=dow
end
end
# target
def recurring_target=(t)
self.target = t
end
def recurring_show_days_before=(days)
self.show_from_delta=days
end
def recurrence_pattern
case recurring_period
when 'daily'
if only_work_days
return "on work days"
else
if every_other1 > 1
return "every #{every_other1} days"
else
return "every day"
end
end
when 'weekly'
if every_other1 > 1
return "every #{every_other1} weeks"
else
return 'weekly'
end
when 'monthly'
if self.recurrence_selector == 0
return "every month on day #{self.every_other1}"
else
return "every #{self.xth} #{self.day_of_week} of every #{self.every_other2} month#{self.every_other2>1?'s':''}"
end
when 'yearly'
if self.recurrence_selector == 0
return "every year on #{self.month_of_year} #{self.every_other1}"
else
return "every year on the #{self.xth} #{self.day_of_week} of #{self.month_of_year}"
end
else
return 'unknown recurrence pattern: period unknown'
end
end
def xth
xth_day = ['first','second','third','fourth','last']
return self.every_other3.nil? ? '??' : xth_day[self.every_other3-1]
end
def day_of_week
days_of_week = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']
return (self.every_count.nil? ? '??' : days_of_week[self.every_count])
end
def month_of_year
months_of_year = ['January','Februari','March','April','May','June','July','August','September','October','November','December']
return self.every_other2.nil? ? '??' : months_of_year[self.every_other2-1]
end
def starred?
tags.any? {|tag| tag.name == Todo::STARRED_TAG_NAME}
end
def get_due_date(previous)
case self.target
when 'due_date'
return get_next_date(previous)
when 'show_from'
# so leave due date empty
return nil
end
end
def get_show_from_date(previous)
case self.target
when 'due_date'
# so set show from date relative to due date unless show_from_delta is
# zero / nil
return (self.show_from_delta == 0 || self.show_from_delta.nil?) ? nil : get_due_date(previous) - self.show_from_delta.days
when 'show_from_date'
# Leave due date empty
return get_next_date(previous)
else
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
end
end
def get_next_date(previous)
case self.recurring_period
when 'daily'
return get_daily_date(previous)
when 'weekly'
return get_weekly_date(previous)
when 'monthly'
return get_monthly_date(previous)
when 'yearly'
return get_yearly_date(previous)
else
raise Exception.new, "unknown recurrence pattern: '#{self.recurring_period}'"
end
end
def get_daily_date(previous)
# previous is the due date of the previous todo or it is the completed_at
# date when the completed_at date is after due_date (i.e. you did not make
# the due date in time)
#
# assumes self.recurring_period == 'daily'
if previous.nil?
start = self.start_from.nil? ? Time.now.utc : self.start_from
else
# use the next day
start = previous + 1.day
end
if self.only_work_days
if start.wday() >= 1 && start.wday() <= 5 # 1=monday; 5=friday
return start
else
if start.wday() == 0 # sunday
return start + 1.day
else # saturday
return start + 2.day
end
end
else # every nth day; n = every_other1
# if there was no previous todo, do not add n: the first todo starts on
# today or on start_from
return previous == nil ? start : start+every_other1.day-1.day
end
end
def get_weekly_date(previous)
if previous == nil
start = self.start_from.nil? ? Time.now.utc : self.start_from
else
start = previous + 1.day
if start.wday() == 0
# we went to a new week , go to the nth next week and find first match
# that week
start += self.every_other1.week
end
end
# check if there are any days left this week for the next todo
start.wday().upto 6 do |i|
return start + (i-start.wday()).days unless self.every_day[i,1] == ' '
end
# we did not find anything this week, so check the nth next, starting from
# sunday
start = start + self.every_other1.week - (start.wday()).days
# check if there are any days left this week for the next todo
start.wday().upto 6 do |i|
return start + (i-start.wday()).days unless self.every_day[i,1] == ' '
end
raise Exception.new, "unable to find next weekly date (#{self.every_day})"
end
def get_monthly_date(previous)
if previous.nil?
start = self.start_from.nil? ? Time.now.utc : self.start_from
else
start = previous
end
day = self.every_other1
n = self.every_other2
case self.recurrence_selector
when 0 # specific day of the month
if start.mday >= day
# there is no next day n in this month, search in next month
start += n.months
# go back to day
end
return Time.utc(start.year, start.month, day)
when 1 # relative weekday of a month
the_next = get_xth_day_of_month(self.every_other3, self.every_count, start.month, start.year)
if the_next.nil? || the_next < start
# the nth day is already passed in this month, go to next month and try
# again
the_next = the_next+n.months
# TODO: if there is still no match, start will be set to nil. if we ever
# support 5th day of the month, we need to handle this case
the_next = get_xth_day_of_month(self.every_other3, self.every_count, the_next.month, the_next.year)
end
return the_next
else
raise Exception.new, "unknown monthly recurrence selection (#{self.recurrence_selector})"
end
return nil
end
def get_xth_day_of_month(x, weekday, month, year)
if x == 5
# last -> count backwards
last_day = Time.utc(year, month, Time.days_in_month(month))
while last_day.wday != weekday
last_day -= 1.day
end
return last_day
else
# 1-4th -> count upwards
start = Time.utc(year,month,1)
n = x
while n > 0
while start.wday() != weekday
start+= 1.day
end
n -= 1
start += 1.day unless n==0
end
return start
end
end
def get_yearly_date(previous)
if previous.nil?
start = self.start_from.nil? ? Time.now.utc : self.start_from
else
start = previous
end
day = self.every_other1
month = self.every_other2
case self.recurrence_selector
when 0 # specific day of a specific month
# if there is no next month n in this year, search in next year
if start.month >= month
start = Time.utc(start.year+1, month, 1) if start.day >= day
start = Time.utc(start.year, month, 1) if start.day <= day
end
return Time.utc(start.year, month, day)
when 1 # relative weekday of a specific month
# if there is no next month n in this year, search in next year
the_next = start.month > month ? Time.utc(start.year+1, month, 1) : start
# get the xth day of the month
the_next = get_xth_day_of_month(self.every_other3, self.every_count, month, the_next.year)
# if the_next is before previous, we went back into the past, so try next
# year
the_next = get_xth_day_of_month(self.every_other3, self.every_count, month, start.year+1) if the_next <= start
return the_next
else
raise Exception.new, "unknown monthly recurrence selection (#{self.recurrence_selector})"
end
return nil
end
def has_next_todo(previous)
unless self.number_of_occurences.nil?
return self.occurences_count < self.number_of_occurences
else
if self.end_date.nil?
return true
else
case self.target
when 'due_date'
return get_due_date(previous) <= self.end_date
when 'show_from_date'
return get_show_from_date(previous) <= self.end_date
else
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
end
end
end
end
def toggle_completion!
saved = false
if completed?
saved = activate!
else
saved = complete!
end
return saved
end
def toggle_star!
if starred?
delete_tags Todo::STARRED_TAG_NAME
tags.reload
else
add_tag Todo::STARRED_TAG_NAME
tags.reload
end
starred?
end
def inc_occurences
self.occurences_count += 1
self.save
end
end

View file

@ -1,6 +1,6 @@
class Tag < ActiveRecord::Base
has_many_polymorphs :taggables,
:from => [:todos],
:from => [:todos, :recurring_todos],
:through => :taggings,
:dependent => :destroy

View file

@ -3,6 +3,7 @@ class Todo < ActiveRecord::Base
belongs_to :context
belongs_to :project
belongs_to :user
belongs_to :recurring_todo
STARRED_TAG_NAME = "starred"
@ -120,4 +121,8 @@ class Todo < ActiveRecord::Base
starred?
end
def from_recurring_todo?
return self.recurring_todo_id != nil
end
end

View file

@ -63,6 +63,9 @@ class User < ActiveRecord::Base
has_many :todos,
:order => 'todos.completed_at DESC, todos.created_at DESC',
:dependent => :delete_all
has_many :recurring_todos,
:order => 'recurring_todos.completed_at DESC, recurring_todos.created_at DESC',
:dependent => :delete_all
has_many :deferred_todos,
:class_name => 'Todo',
:conditions => [ 'state = ?', 'deferred' ],

View file

@ -18,6 +18,7 @@ window.onload=function(){
Nifty("div#todo_new_action_container","normal");
Nifty("div#project_new_project_container","normal");
Nifty("div#context_new_container","normal");
Nifty("div#recurring_new_container","normal");
if ($('flash').visible()) { new Effect.Fade("flash",{duration:5.0}); }
}
</script>
@ -56,6 +57,7 @@ window.onload=function(){
<% if current_user.is_admin? -%>
<li><%= navigation_link("Admin", users_path, {:accesskey => "a", :title => "Add or delete users"} ) %></li>
<% end -%>
<li><%= navigation_link(image_tag("recurring_menu16x16.png", :size => "16X16", :border => 0), {:controller => "recurring_todos", :action => "index"}, :title => "Manage recurring actions" ) %></li>
<li><%= navigation_link(image_tag("feed-icon.png", :size => "16X16", :border => 0), {:controller => "feedlist", :action => "index"}, :title => "See a list of available feeds" ) %></li>
<li><%= navigation_link(image_tag("menustar.gif", :size => "16X16", :border => 0), tag_path("starred"), :title => "See your starred actions" ) %></li>
<li><%= navigation_link(image_tag("stats.gif", :size => "16X16", :border => 0), {:controller => "stats", :action => "index"}, :title => "See your statistics" ) %></li>

View file

@ -0,0 +1,147 @@
<label>Edit Recurring Todo</label><br/>
<div class="recurring_container">
<% form_remote_tag(
:url => recurring_todo_path(@recurring_todo), :method => :put,
:html=> { :id=>'recurring-todo-form-edit-action', :name=>'recurring_todo', :class => 'inline-form' },
:before => "$('recurring_todo_edit_action_submit').startWaiting()",
:complete => "$('recurring_todo_edit_action_submit').stopWaiting();",
:condition => "!$('recurring_todo_edit_action_submit').isWaiting()") do
-%>
<div id="status"><%= error_messages_for("item", :object_name => 'action') %></div>
<div id="recurring_todo_form_container">
<div id="recurring_todo">
<label for="recurring_todo_description">Description</label><%=
text_field_tag( "recurring_todo[description]", @recurring_todo.description, "size" => 30, "tabindex" => 1) -%>
<label for="recurring_todo_notes">Notes</label><%=
text_area_tag( "recurring_todo[notes]", @recurring_todo.notes, {:cols => 29, :rows => 6, :tabindex => 2}) -%>
<label for="recurring_todo_project_name">Project</label>
<input id="recurring_todo_project_name" name="project_name" autocomplete="off" tabindex="3" size="30" type="text" value="<%= @recurring_todo.project.nil? ? 'None' : @recurring_todo.project.name.gsub(/"/,"&quot;") %>" />
<div class="page_name_auto_complete" id="project_list" style="display:none"></div>
<script type="text/javascript">
projectAutoCompleter = new Autocompleter.Local('recurring_todo_project_name', 'project_list', <%= project_names_for_autocomplete %>, {choices:100,autoSelect:false});
function selectDefaultContext() {
todoContextNameElement = $('recurring_todo_context_name');
defaultContextName = todoContextNameElement.projectDefaultContextsMap[this.value];
if (defaultContextName && !todoContextNameElement.editedByTracksUser) {
todoContextNameElement.value = defaultContextName;
}
}
Event.observe($('recurring_todo_project_name'), "focus", projectAutoCompleter.activate.bind(projectAutoCompleter));
Event.observe($('recurring_todo_project_name'), "click", projectAutoCompleter.activate.bind(projectAutoCompleter));
Event.observe($('recurring_todo_project_name'), "blur", selectDefaultContext.bind($('recurring_todo_project_name')));
</script>
<label for="recurring_todo_context_name">Context</label>
<input id="recurring_todo_context_name" name="context_name" autocomplete="off" tabindex="4" size="30" type="text" value="<%= @recurring_todo.context.name %>" />
<div class="page_name_auto_complete" id="context_list" style="display:none"></div>
<script type="text/javascript">
var contextAutoCompleter;
function initializeNamesForAutoComplete(contextNamesForAutoComplete) {
if (contextNamesForAutoComplete.length == 0 || contextNamesForAutoComplete[0].length == 0) {
return;
}
contextAutoCompleter = new Autocompleter.Local('recurring_todo_context_name', 'context_list', contextNamesForAutoComplete, {choices:100,autoSelect:false});
Event.observe($('recurring_todo_context_name'), "focus", function(){ $('recurring_todo_context_name').editedByTracksUser = true; });
Event.observe($('recurring_todo_context_name'), "focus", contextAutoCompleter.activate.bind(contextAutoCompleter));
Event.observe($('recurring_todo_context_name'), "click", contextAutoCompleter.activate.bind(contextAutoCompleter));
}
function updateContextNamesForAutoComplete(contextNamesForAutoComplete) {
if (contextAutoCompleter) { // i.e. if we're already initialized
contextAutoCompleter.options.array = contextNamesForAutoComplete
} else {
initializeNamesForAutoComplete(contextNamesForAutoComplete)
}
}
initializeNamesForAutoComplete(<%= context_names_for_autocomplete %>);
$('recurring_todo_context_name').projectDefaultContextsMap = eval('(' + <%= @default_project_context_name_map %> + ')');
</script>
<label for="tag_list">Tags (separate with commas)</label><%=
text_field_tag "tag_list", @recurring_todo.tag_list, :size => 30, :tabindex => 5 -%>
</div>
</div>
<div id="recurring_edit_period_id">
<div id="recurring_edit_period">
<label>Recurrence period</label><br/>
<%= radio_button_tag('recurring_edit_todo[recurring_period]', 'daily', @recurring_todo.recurring_period == 'daily')%> Daily<br/>
<%= radio_button_tag('recurring_edit_todo[recurring_period]', 'weekly', @recurring_todo.recurring_period == 'weekly')%> Weekly<br/>
<%= radio_button_tag('recurring_edit_todo[recurring_period]', 'monthly', @recurring_todo.recurring_period == 'monthly')%> Monthly<br/>
<%= radio_button_tag('recurring_edit_todo[recurring_period]', 'yearly', @recurring_todo.recurring_period == 'yearly')%> Yearly<br/>
<% #behaviour is set in index because behaviours in partials are not generated -%>
</div>
<div id="recurring_timespan">
<br/>
<label for="recurring_todo[start_from]">Starts on </label><%=
text_field_tag("recurring_todo[start_from]", format_date(@recurring_todo.start_from), "size" => 12, "class" => "Date", "onfocus" => "Calendar.setup", "tabindex" => 6, "autocomplete" => "off") %><br/>
<br/>
<label for="recurring_todo[ends_on]">Ends on:</label><br/>
<%= radio_button_tag('recurring_todo[ends_on]', 'no_end_date', @recurring_todo.ends_on == 'no_end_date')%> No end date<br/>
<%= radio_button_tag('recurring_todo[ends_on]', 'ends_on_number_of_times', @recurring_todo.ends_on == 'ends_on_number_of_times')%> Ends after <%= text_field_tag("recurring_todo[number_of_occurences]", @recurring_todo.number_of_occurences, "size" => 3, "tabindex" => 7) %> times<br/>
<%= radio_button_tag('recurring_todo[ends_on]', 'ends_on_end_date', @recurring_todo.ends_on == 'ends_on_end_date')%> Ends on <%=
text_field_tag('recurring_todo[end_date]', format_date(@recurring_todo.end_date), "size" => 12, "class" => "Date", "onfocus" => "Calendar.setup", "tabindex" => 8, "autocomplete" => "off") %><br/>
</div></div>
<div id="recurring_edit_daily" style="display:<%= @recurring_todo.recurring_period == 'daily' ? 'block' : 'none' %> ">
<label>Recurrence</label><br/>
<%= radio_button_tag('recurring_todo[daily_selector]', 'daily_every_x_day', !@recurring_todo.only_work_days)%> Every <%=
text_field_tag( 'recurring_todo[daily_every_x_days]', @recurring_todo.every_other1, {"size" => 3, "tabindex" => 9}) %> day(s)<br/>
<%= radio_button_tag('recurring_todo[daily_selector]', 'daily_every_work_day', @recurring_todo.only_work_days)%> Every work day<br/>
</div>
<div id="recurring_edit_weekly" style="display:<%= @recurring_todo.recurring_period == 'weekly' ? 'block' : 'none' %>">
<label>Recurrence</label><br/>
Returns every <%= text_field_tag('recurring_todo[weekly_every_x_week]', @recurring_todo.every_other1, {"size" => 3, "tabindex" => 9}) %> week on<br/>
<%= check_box_tag('recurring_todo[weekly_return_monday]', 'm', @recurring_todo.on_monday ) %> Monday
<%= check_box_tag('recurring_todo[weekly_return_tuesday]', 't', @recurring_todo.on_tuesday) %> Tuesday
<%= check_box_tag('recurring_todo[weekly_return_wednesday]', 'w', @recurring_todo.on_wednesday) %> Wednesday
<%= check_box_tag('recurring_todo[weekly_return_thursday]', 't', @recurring_todo.on_thursday) %> Thursday<br/>
<%= check_box_tag('recurring_todo[weekly_return_friday]', 'f', @recurring_todo.on_friday) %> Friday
<%= check_box_tag('recurring_todo[weekly_return_saturday]', 's', @recurring_todo.on_saturday) %> Saturday
<%= check_box_tag('recurring_todo[weekly_return_sunday]', 's', @recurring_todo.on_sunday) %> Sunday<br/>
</div>
<div id="recurring_edit_monthly" style="display:<%= @recurring_todo.recurring_period == 'monthly' ? 'block' : 'none' %>">
<label>Recurrence</label><br/>
<%= radio_button_tag('recurring_todo[monthly_selector]', 'monthly_every_x_day', @recurring_todo.recurrence_selector == 0)%> Day <%=
text_field_tag('recurring_todo[monthly_every_x_day]', Time.now.mday, {"size" => 3, "tabindex" => 9}) %> on every <%=
text_field_tag('recurring_todo[monthly_every_x_month]', 1, {"size" => 3, "tabindex" => 10}) %> month<br/>
<%= radio_button_tag('recurring_todo[monthly_selector]', 'monthly_every_xth_day', @recurring_todo.recurrence_selector == 1)%> The <%=
select_tag('recurring_todo[monthly_every_xth_day]', options_for_select(@xth_day), {}) %> <%=
select_tag('recurring_todo[monthly_day_of_week]' , options_for_select(@days_of_week, Time.now.wday), {}) %> of every <%=
text_field_tag('recurring_todo[monthly_every_x_month]', 1, {"size" => 3, "tabindex" => 11}) %> month<br/>
</div>
<div id="recurring_edit_yearly" style="display:<%= @recurring_todo.recurring_period == 'yearly' ? 'block' : 'none' %>">
<label>Recurrence</label><br/>
<%= radio_button_tag('recurring_todo[yearly_selector]', 'yearly_every_x_day', @recurring_todo.recurrence_selector == 0)%> Every <%=
select_tag('recurring_todo[yearly_month_of_year]', options_for_select(@months_of_year, @recurring_todo.every_other2), {}) %> <%=
text_field_tag('recurring_todo[yearly_every_x_day]', @recurring_todo.every_other1, "size" => 3, "tabindex" => 9) %><br/>
<%= radio_button_tag('recurring_todo[yearly_selector]', 'yearly_every_xth_day', @recurring_todo.recurrence_selector == 1)%> The <%=
select_tag('recurring_todo[yearly_every_xth_day]', options_for_select(@xth_day, @recurring_todo.every_other3), {}) %> <%=
select_tag('recurring_todo[yearly_day_of_week]', options_for_select(@days_of_week, @recurring_todo.every_count), {}) %> of <%=
select_tag('recurring_todo[yearly_month_of_year]', options_for_select(@months_of_year, @recurring_todo.every_other2), {}) %><br/>
</div>
<div id="recurring_target">
<label>Set recurrence on</label><br/>
<%= radio_button_tag('recurring_todo[recurring_target]', 'due_date', @recurring_todo.target == 'due_date')%> the date that the todo is due.
Show the todo <%=
text_field_tag( 'recurring_todo[recurring_show_days_before]', @recurring_todo.show_from_delta, {"size" => 3, "tabindex" => 12}) %>
days before the todo is due (0=show always)<br/>
<%= radio_button_tag('recurring_todo[recurring_target]', 'show_from_date', @recurring_todo.target == 'show_from_date')%> the date todo comes from tickler (no due date set)<br/>
<br/>
</div>
<div class="recurring_submit_box">
<div class="widgets">
<button type="submit" class="positive" id="recurring_todo_edit_action_submit" tabindex="15">
<%=image_tag("accept.png", :alt => "") %>
Update
</button>
<button type="button" class="positive" id="recurring_todo_edit_action_cancel" tabindex="15" onclick="TracksForm.toggle_overlay();">
<%=image_tag("cancel.png", :alt => "") %>
Cancel
</button>
</div>
</div>
<% end %>
<%= calendar_setup( "recurring_todo_start_from" ) %>
<%= calendar_setup( "recurring_todo_end_date" ) %>
</div>

View file

@ -0,0 +1,12 @@
<% @recurring_todo = recurring_todo -%>
<div id="<%= dom_id(@recurring_todo)%>" class="recurring_todo item-container">
<%= recurring_todo_remote_delete_icon %> <%= recurring_todo_remote_edit_icon -%>
<%= recurring_todo_remote_star_icon %> <%= recurring_todo_remote_toggle_checkbox -%>
<div class="description">
<span class="todo.descr"><%= sanitize(recurring_todo.description) %></span> <%= recurring_todo_tag_list %>
<span class='recurrence_pattern'>
[<%=recurrence_target(recurring_todo)%> <%= recurring_todo.recurrence_pattern %> <%= recurrence_time_span(recurring_todo) %>]
</span>
</div>
</div>

View file

@ -0,0 +1,145 @@
<div class="recurring_container">
<% form_remote_tag(
:url => recurring_todos_path, :method => :post,
:html=> { :id=>'recurring-todo-form-new-action', :name=>'recurring_todo', :class => 'inline-form' },
:before => "$('recurring_todo_new_action_submit').startWaiting()",
:complete => "$('recurring_todo_new_action_submit').stopWaiting();",
:condition => "!$('recurring_todo_new_action_submit').isWaiting()") do
-%>
<div id="status"><%= error_messages_for("item", :object_name => 'action') %></div>
<div id="recurring_todo_form_container">
<div id="recurring_todo">
<label for="recurring_todo_description">Description</label><%=
text_field_tag( "recurring_todo[description]", "", "size" => 30, "tabindex" => 1) -%>
<label for="recurring_todo_notes">Notes</label><%=
text_area_tag( "recurring_todo[notes]", nil, {:cols => 29, :rows => 6, :tabindex => 2}) -%>
<label for="recurring_todo_project_name">Project</label>
<input id="recurring_todo_project_name" name="project_name" autocomplete="off" tabindex="3" size="30" type="text" value="" />
<div class="page_name_auto_complete" id="project_list" style="display:none"></div>
<script type="text/javascript">
projectAutoCompleter = new Autocompleter.Local('recurring_todo_project_name', 'project_list', <%= project_names_for_autocomplete %>, {choices:100,autoSelect:false});
function selectDefaultContext() {
todoContextNameElement = $('recurring_todo_context_name');
defaultContextName = todoContextNameElement.projectDefaultContextsMap[this.value];
if (defaultContextName && !todoContextNameElement.editedByTracksUser) {
todoContextNameElement.value = defaultContextName;
}
}
Event.observe($('recurring_todo_project_name'), "focus", projectAutoCompleter.activate.bind(projectAutoCompleter));
Event.observe($('recurring_todo_project_name'), "click", projectAutoCompleter.activate.bind(projectAutoCompleter));
Event.observe($('recurring_todo_project_name'), "blur", selectDefaultContext.bind($('recurring_todo_project_name')));
</script>
<label for="recurring_todo_context_name">Context</label>
<input id="recurring_todo_context_name" name="context_name" autocomplete="off" tabindex="4" size="30" type="text" value="" />
<div class="page_name_auto_complete" id="context_list" style="display:none"></div>
<script type="text/javascript">
var contextAutoCompleter;
function initializeNamesForAutoComplete(contextNamesForAutoComplete) {
if (contextNamesForAutoComplete.length == 0 || contextNamesForAutoComplete[0].length == 0) {
return;
}
contextAutoCompleter = new Autocompleter.Local('recurring_todo_context_name', 'context_list', contextNamesForAutoComplete, {choices:100,autoSelect:false});
Event.observe($('recurring_todo_context_name'), "focus", function(){ $('recurring_todo_context_name').editedByTracksUser = true; });
Event.observe($('recurring_todo_context_name'), "focus", contextAutoCompleter.activate.bind(contextAutoCompleter));
Event.observe($('recurring_todo_context_name'), "click", contextAutoCompleter.activate.bind(contextAutoCompleter));
}
function updateContextNamesForAutoComplete(contextNamesForAutoComplete) {
if (contextAutoCompleter) { // i.e. if we're already initialized
contextAutoCompleter.options.array = contextNamesForAutoComplete
} else {
initializeNamesForAutoComplete(contextNamesForAutoComplete)
}
}
initializeNamesForAutoComplete(<%= context_names_for_autocomplete %>);
$('recurring_todo_context_name').projectDefaultContextsMap = eval('(' + <%= @default_project_context_name_map %> + ')');
</script>
<label for="tag_list">Tags (separate with commas)</label><%=
text_field_tag "tag_list", nil, :size => 30, :tabindex => 5 -%>
</div>
</div>
<div id="recurring_period_id">
<label>Recurrence period</label><br/>
<%= radio_button_tag('recurring_todo[recurring_period]', 'daily', true)%> Daily<br/>
<%= radio_button_tag('recurring_todo[recurring_period]', 'weekly')%> Weekly<br/>
<%= radio_button_tag('recurring_todo[recurring_period]', 'monthly')%> Monthly<br/>
<%= radio_button_tag('recurring_todo[recurring_period]', 'yearly')%> Yearly<br/>
<% apply_behaviour "#recurring_period_id:click",
"TracksForm.hide_all_recurring(); $('recurring_'+TracksForm.get_period()).show();" %>
<div id="recurring_timespan">
<br/>
<label for="recurring_todo[start_from]">Starts on </label><%=
text_field(:recurring_todo, :start_from, "size" => 12, "class" => "Date", "onfocus" => "Calendar.setup", "tabindex" => 6, "autocomplete" => "off") %><br/>
<br/>
<label for="recurring_todo[ends_on]">Ends on:</label><br/>
<%= radio_button_tag('recurring_todo[ends_on]', 'no_end_date', true)%> No end date<br/>
<%= radio_button_tag('recurring_todo[ends_on]', 'ends_on_number_of_times')%> Ends after <%= text_field( :recurring_todo, :number_of_occurences, "size" => 3, "tabindex" => 7) %> times<br/>
<%= radio_button_tag('recurring_todo[ends_on]', 'ends_on_end_date')%> Ends on <%= text_field(:recurring_todo, :end_date, "size" => 12, "class" => "Date", "onfocus" => "Calendar.setup", "tabindex" => 8, "autocomplete" => "off") %><br/>
</div></div>
<div id="recurring_daily" style="display:block">
<label>Recurrence</label><br/>
<%= radio_button_tag('recurring_todo[daily_selector]', 'daily_every_x_day', true)%> Every <%=
text_field_tag( 'recurring_todo[daily_every_x_days]', "1", {"size" => 3, "tabindex" => 9}) %> day(s)<br/>
<%= radio_button_tag('recurring_todo[daily_selector]', 'daily_every_work_day')%> Every work day<br/>
</div>
<div id="recurring_weekly" style="display:none">
<label>Recurrence</label><br/>
Returns every <%= text_field_tag('recurring_todo[weekly_every_x_week]', 1, {"size" => 3, "tabindex" => 9}) %> week on<br/>
<% week_day = Time.new.wday -%>
<%= check_box_tag('recurring_todo[weekly_return_monday]', 'm', week_day == 1 ? true : false) %> Monday
<%= check_box_tag('recurring_todo[weekly_return_tuesday]', 't', week_day == 2 ? true : false) %> Tuesday
<%= check_box_tag('recurring_todo[weekly_return_wednesday]', 'w', week_day == 3 ? true : false) %> Wednesday
<%= check_box_tag('recurring_todo[weekly_return_thursday]', 't', week_day == 4 ? true : false) %> Thursday<br/>
<%= check_box_tag('recurring_todo[weekly_return_friday]', 'f', week_day == 5 ? true : false) %> Friday
<%= check_box_tag('recurring_todo[weekly_return_saturday]', 's', week_day == 6 ? true : false) %> Saturday
<%= check_box_tag('recurring_todo[weekly_return_sunday]', 's', week_day == 0 ? true : false) %> Sunday<br/>
</div>
<div id="recurring_monthly" style="display:none">
<label>Recurrence</label><br/>
<%= radio_button_tag('recurring_todo[monthly_selector]', 'monthly_every_x_day', true)%> Day <%=
text_field_tag('recurring_todo[monthly_every_x_day]', Time.now.mday, {"size" => 3, "tabindex" => 9}) %> on every <%=
text_field_tag('recurring_todo[monthly_every_x_month]', 1, {"size" => 3, "tabindex" => 10}) %> month<br/>
<%= radio_button_tag('recurring_todo[monthly_selector]', 'monthly_every_xth_day')%> The <%=
select_tag('recurring_todo[monthly_every_xth_day]', options_for_select(@xth_day), {}) %> <%=
select_tag('recurring_todo[monthly_day_of_week]' , options_for_select(@days_of_week, Time.now.wday), {}) %> of every <%=
text_field_tag('recurring_todo[monthly_every_x_month]', 1, {"size" => 3, "tabindex" => 11}) %> month<br/>
</div>
<div id="recurring_yearly" style="display:none">
<label>Recurrence</label><br/>
<%= radio_button_tag('recurring_todo[yearly_selector]', 'yearly_every_x_day', true)%> Every <%=
select_tag('recurring_todo[yearly_month_of_year]', options_for_select(@months_of_year, Time.now.month), {}) %> <%=
text_field_tag('recurring_todo[yearly_every_x_day]', Time.now.day, "size" => 3, "tabindex" => 9) %><br/>
<%= radio_button_tag('recurring_todo[yearly_selector]', 'yearly_every_xth_day')%> The <%=
select_tag('recurring_todo[yearly_every_xth_day]', options_for_select(@xth_day), {}) %> <%=
select_tag('recurring_todo[yearly_day_of_week]', options_for_select(@days_of_week, Time.now.wday), {}) %> of <%=
select_tag('recurring_todo[yearly_month_of_year]', options_for_select(@months_of_year, Time.now.month), {}) %><br/>
</div>
<div id="recurring_target">
<label>Set recurrence on</label><br/>
<%= radio_button_tag('recurring_todo[recurring_target]', 'due_date', true)%> the date that the todo is due.
Show the todo <%=
text_field_tag( 'recurring_todo[recurring_show_days_before]', "0", {"size" => 3, "tabindex" => 12}) %>
days before the todo is due (0=show always)<br/>
<%= radio_button_tag('recurring_todo[recurring_target]', 'show_from_date', false)%> the date todo comes from tickler (no due date set)<br/>
<br/>
</div>
<div class="recurring_submit_box">
<div class="widgets">
<button type="submit" class="positive" id="recurring_todo_new_action_submit" tabindex="15">
<%=image_tag("accept.png", :alt => "") %>
Create
</button>
<button type="button" class="positive" id="recurring_todo_new_action_cancel" tabindex="15" onclick="Form.reset('recurring-todo-form-new-action');Form.focusFirstElement('recurring-todo-form-new-action');TracksForm.hide_all_recurring(); $('recurring_daily').show();TracksForm.toggle_overlay();">
<%=image_tag("cancel.png", :alt => "") %>
Cancel
</button>
</div>
</div>
<% end %>
<%= calendar_setup( "recurring_todo_start_from" ) %>
<%= calendar_setup( "recurring_todo_end_date" ) %>
</div>

View file

@ -0,0 +1,20 @@
page.show 'status'
page.replace_html 'status', "#{error_messages_for('recurring_todo')}"
page.notify :notice, @message, 5.0
if @recurring_saved
# reset form
page << "TracksForm.hide_all_recurring(); $('recurring_daily').show();"
page << "Form.reset('recurring-todo-form-new-action');"
page << "Form.focusFirstElement('recurring-todo-form-new-action');"
# hide overlayed edit form
page << "TracksForm.toggle_overlay();"
# insert new recurring todo
page.hide 'recurring-todos-empty-nd'
page.insert_html :bottom,
'recurring_todos_container',
:partial => 'recurring_todos/recurring_todo'
page.visual_effect :highlight, dom_id(@recurring_todo), :duration => 3
# update badge count
page['badge_count'].replace_html @count
end

View file

@ -0,0 +1,12 @@
if @saved
if @remaining == 0
page.show 'recurring-todos-empty-nd'
end
page.notify :notice, "The recurring action was deleted succesfully. " +
"The recurrence pattern is removed from " +
pluralize(@number_of_todos, "todo"), 5.0
page[@recurring_todo].remove
page.visual_effect :fade, dom_id(@recurring_todo), :duration => 0.4
else
page.notify :error, "There was an error deleting the recurring todo #{@recurring_todo.description}", 8.0
end

View file

@ -0,0 +1,4 @@
page << "TracksForm.toggle_overlay();"
page['new-recurring-todo'].hide
page['edit-recurring-todo'].replace_html :partial => 'recurring_todos/edit_form'
page['edit-recurring-todo'].show

View file

@ -0,0 +1,42 @@
<div id="display_box">
<div class="container recurring_todos">
<h2>Recurring todos</h2>
<div id="recurring_todos_container">
<div id="recurring-todos-empty-nd" style="<%= @no_recurring_todos ? 'display:block' : 'display:none'%>">
<div class="message"><p>Currently there are no recurring todos</p></div>
</div>
<%= render :partial => "recurring_todo", :collection => @recurring_todos %>
</div>
</div>
<div class="container recurring_todos_done">
<h2>Recurring todos that are completed</h2>
<div id="completed_recurring_todos_container">
<div id="completed-empty-nd" style="<%= @no_completed_recurring_todos ? 'display:block' : 'display:none'%>">
<div class="message"><p>Currently there are no completed recurring todos</p></div>
</div>
<%= render :partial => "recurring_todo", :collection => @completed_recurring_todos %>
</div>
</div>
</div>
<div id="input_box">
<div id="recurring_new_container">
<a href='#' onclick="$('new-recurring-todo').show();$('edit-recurring-todo').hide();TracksForm.toggle_overlay()"><%= image_tag("add.png", {:alt => "[ADD]"})-%>Add a new recurring action</a>
</div>
</div>
<div id="overlay">
<div id="new-recurring-todo" class="new-form">
<label>Add new recurring action</label><br/>
<%= render :partial => "recurring_todo_form" %>
</div>
<div id="edit-recurring-todo" class="edit-form" style="display:none">
<div class='placeholder'>This should not be visible</div>
</div>
</div><%
# need to add behaviour for edit form here. Behaviour defined in partials are
# not generated for
apply_behaviour "#recurring_edit_period:click",
"TracksForm.hide_all_edit_recurring(); $('recurring_edit_'+TracksForm.get_edit_period()).show();"
-%>

View file

@ -0,0 +1 @@
<%= render :partial => "recurring_todo_form" %>

View file

@ -0,0 +1,2 @@
<h1>RecurringTodo#show</h1>
<p>Find me in app/views/recurring_todo/show.html.erb</p>

View file

@ -0,0 +1,30 @@
if @saved
page[@recurring_todo].remove
page['badge_count'].replace_html @count
if @recurring_todo.completed?
# show completed recurring todo
page.insert_html :top, "completed_recurring_todos_container", :partial => 'recurring_todos/recurring_todo'
page.visual_effect :highlight, dom_id(@recurring_todo), :duration => 3
# set empty messages
page.show 'recurring-todos-empty-nd' if @remaining == 0
page.hide 'completed-empty-nd'
else
# recurring_todo is activated
# show completed recurring todo
page.insert_html :top, "recurring_todos_container", :partial => 'recurring_todos/recurring_todo'
page.visual_effect :highlight, dom_id(@recurring_todo), :duration => 3
# inform user if a new todo has been created because of the activation
page.notify :notice, "A new todo was added which belongs to this recurring todo", 3.0 unless @new_recurring_todo.nil?
# set empty messages
page.show 'completed-empty-nd' if @remaining == 0
page.hide 'recurring-todos-empty-nd'
end
else
page.notify :error, "There was an error completing / activating the recurring todo #{@recurring_todo.description}", 8.0
end

View file

@ -0,0 +1,3 @@
if @saved
page[@recurring_todo].down('a.star_item').down('img').toggleClassName('starred_todo').toggleClassName('unstarred_todo')
end

View file

@ -0,0 +1,22 @@
if @saved
# hide overlayed edit form
page << "TracksForm.toggle_overlay();"
# show update message
status_message = 'Recurring action saved'
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
# update auto completer arrays for context and project
page << "contextAutoCompleter.options.array = #{context_names_for_autocomplete}; contextAutoCompleter.changed = true" if @new_context_created
page << "projectAutoCompleter.options.array = #{project_names_for_autocomplete}; projectAutoCompleter.changed = true" if @new_project_created
# replace old recurring todo with updated todo
page.replace dom_id(@recurring_todo), :partial => 'recurring_todos/recurring_todo'
page.visual_effect :highlight, dom_id(@recurring_todo), :duration => 3
else
page.show 'error_status'
page.replace_html 'error_status', "#{error_messages_for('todo')}"
end

View file

@ -13,6 +13,7 @@
<div class="description<%= staleness_class( todo ) %>">
<%= date_span -%>
<span class="todo.descr"><%= h sanitize(todo.description) %></span>
<%= link_to(image_tag("recurring16x16.png"), {:controller => "recurring_todos", :action => "index"}, :class => "recurring_icon") 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 ) %>

View file

@ -15,7 +15,7 @@ if @saved
page.insert_html :top, 'display_box', :partial => 'contexts/context', :locals => { :context => @todo.context, :collapsible => true }
else
page.call "todoItems.ensureVisibleWithEffectAppear", "c#{@todo.context_id}" if source_view_is_one_of(:todo, :deferred)
page.insert_html :bottom, item_container_id + 'items', :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type, :source_view => @source_view }
page.insert_html :bottom, item_container_id(@todo) + 'items', :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type, :source_view => @source_view }
page.visual_effect :highlight, dom_id(@todo), :duration => 3
page[empty_container_msg_div_id].hide unless empty_container_msg_div_id.nil?
end

View file

@ -1,9 +1,23 @@
if @saved
page[@todo].remove
page['badge_count'].replace_html @down_count
page.visual_effect :fade, item_container_id, :duration => 0.4 if source_view_is_one_of(:todo, :deferred) && @remaining_in_context == 0
# remove context if empty
page.visual_effect :fade, item_container_id(@todo), :duration => 0.4 if source_view_is_one_of(:todo, :deferred) && @remaining_in_context == 0
# show message if there are no actions
page[empty_container_msg_div_id].show if !empty_container_msg_div_id.nil? && @down_count == 0
page['tickler-empty-nd'].show if source_view_is(:deferred) && @down_count == 0
# show new todo if the completed todo was recurring
unless @new_recurring_todo.nil?
page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id(@new_recurring_todo)
page.insert_html :bottom, item_container_id(@new_recurring_todo), :partial => 'todos/todo', :locals => { :todo => @new_recurring_todo, :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@new_recurring_todo, 'line'), {'startcolor' => "'#99ff99'"}
page.notify :notice, "Action was deleted. Because this action is recurring, a new action was added", 6.0
else
page.notify :notice, "There is no next action after the recurring action you just deleted. The recurrence is completed", 6.0 unless @recurring_todo.nil?
end
else
page.notify :error, "There was an error deleting the item #{@todo.description}", 8.0
end

View file

@ -1,7 +1,8 @@
if @saved
page[@todo].remove
if @todo.completed?
# Don't try to insert contents into a non-existent container!
# completed todos move from their context to the completed container
unless @prefs.hide_completed_actions?
page.insert_html :top, "completed", :partial => 'todos/todo', :locals => { :parent_container_type => "completed" }
page.visual_effect :highlight, dom_id(@todo, 'line'), {'startcolor' => "'#99ff99'"}
@ -9,16 +10,30 @@ if @saved
page.show 'tickler-empty-nd' if source_view_is(:project) && @deferred_count == 0
page.hide 'empty-d' # If we've checked something as done, completed items can't be empty
end
# remove container if empty
if @remaining_in_context == 0 && source_view_is(:todo)
page.visual_effect :fade, item_container_id, :duration => 0.4
page.visual_effect :fade, item_container_id(@todo), :duration => 0.4
end
# show new todo if the completed todo was recurring
unless @new_recurring_todo.nil?
page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id(@new_recurring_todo)
page.insert_html :bottom, item_container_id(@new_recurring_todo), :partial => 'todos/todo', :locals => { :todo => @new_recurring_todo, :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@new_recurring_todo, 'line'), {'startcolor' => "'#99ff99'"}
else
page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id
page.insert_html :bottom, item_container_id, :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.notify :notice, "There is no next action after the recurring action you just finished. The recurrence is completed", 6.0 unless @recurring_todo.nil?
end
else
# todo is activated from completed container
page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id(@todo)
page.insert_html :bottom, item_container_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
page.visual_effect :highlight, dom_id(@todo, 'line'), {'startcolor' => "'#99ff99'"}
page.show "empty-d" if @completed_count == 0
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
end
page.hide "status"
page.replace_html "badge_count", @down_count
if @todo.completed? && !@todo.project_id.nil? && @prefs.show_project_on_todo_done && !source_view_is(:project)

View file

@ -6,7 +6,7 @@ if @saved
status_message = 'Added new context / ' + status_message if @new_context_created
page.notify :notice, status_message, 5.0
#update auto completer arrays for context and project
# #update auto completer arrays for context and project
page << "contextAutoCompleter.options.array = #{context_names_for_autocomplete}; contextAutoCompleter.changed = true" if @new_context_created
page << "projectAutoCompleter.options.array = #{project_names_for_autocomplete}; projectAutoCompleter.changed = true" if @new_project_created
if source_view_is_one_of(:todo, :context)

View file

@ -58,6 +58,10 @@ ActionController::Routing::Routes.draw do |map|
map.preferences 'preferences', :controller => 'preferences', :action => 'index'
map.integrations 'integrations', :controller => 'integrations', :action => 'index'
map.resources :recurring_todos,
:member => {:toggle_check => :put, :toggle_star => :put}
map.recurring_todos 'recurring_todos', :controller => 'recurring_todos', :action => 'index'
# Install the default route as the lowest priority.
map.connect ':controller/:action/:id'

View file

@ -0,0 +1,52 @@
class CreateRecurringTodos < ActiveRecord::Migration
def self.up
create_table :recurring_todos do |t|
# todo data
t.column :user_id, :integer, :default => 1
t.column :context_id, :integer, :null => false
t.column :project_id, :integer
t.column :description, :string, :null => false
t.column :notes, :text
t.column :state, :string, :limit => 20, :default => "active", :null => false
# running time
t.column :start_from, :date
t.column :ends_on, :string # no_end_date, ends_on_number_of_times, ends_on_end_date
t.column :end_date, :date # end_date should be null when
# number_of_occurrences is not null
t.column :number_of_occurences, :integer
t.column :occurences_count, :integer, :default => 0 # current count
# target
t.column :target, :string # 'due_date' or 'show_from'
t.column :show_from_delta, :integer # number of days before due date
# recurring parameters
t.column :recurring_period, :string # daily, monthly, yearly
t.column :recurrence_selector, :integer # which recurrence is selected
t.column :every_other1, :integer # every 1 day, every 2nd week,
# every day 12 of the month
t.column :every_other2, :integer # for month: every 12th of
# every 2 (other) month and
# year: every 12th of 3 (march)
t.column :every_other3, :integer # for months and years
t.column :every_day, :string # for weekly: 'smtwtfs' for
# every week on all days or
# ' m w f ' for every week on
# every other day
t.column :only_work_days, :boolean, :default => false # for daily
t.column :every_count, :integer # monthly and yearly to describe
# the second monday of a month
t.column :weekday, :integer # monthly and yearly to describe
# day of week for every second
# saturday of the month
t.column :completed_at, :datetime
t.timestamps
end
add_column :todos, :recurring_todo_id, :integer
end
def self.down
remove_column :todos, :recurring_todo_id
drop_table :recurring_todos
end
end

BIN
public/images/add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

BIN
public/images/trans70.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

View file

@ -32,8 +32,59 @@ var TracksForm = {
Form.focusFirstElement(formId);
}
toggleDiv.toggleClassName('hide_form');
},
get_period: function() {
if ($('recurring_todo_recurring_period_daily').checked) {
return 'daily';
}
else if ($('recurring_todo_recurring_period_weekly').checked) {
return 'weekly';
}
else if ($('recurring_todo_recurring_period_monthly').checked) {
return 'monthly';
}
else if ($('recurring_todo_recurring_period_yearly').checked) {
return 'yearly';
}
else {
return 'no period'
}
},
get_edit_period: function() {
if ($('recurring_edit_todo_recurring_period_daily').checked) {
return 'daily';
}
else if ($('recurring_edit_todo_recurring_period_weekly').checked) {
return 'weekly';
}
else if ($('recurring_edit_todo_recurring_period_monthly').checked) {
return 'monthly';
}
else if ($('recurring_edit_todo_recurring_period_yearly').checked) {
return 'yearly';
}
else {
return 'no period'
}
},
hide_all_recurring: function () {
$('recurring_daily').hide();
$('recurring_weekly').hide();
$('recurring_monthly').hide();
$('recurring_yearly').hide();
},
hide_all_edit_recurring: function () {
$('recurring_edit_daily').hide();
$('recurring_edit_weekly').hide();
$('recurring_edit_monthly').hide();
$('recurring_edit_yearly').hide();
},
toggle_overlay: function () {
el = document.getElementById("overlay");
el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible";
}
}
// uncomment the next four lines for easier debugging with FireBug
// Ajax.Responders.register({
// onException: function(source, exception) {
@ -55,7 +106,7 @@ Event.observe(window, 'load', function() {
*/
CookieManager = Class.create();
CookieManager.prototype =
{
{
BROWSER_IS_IE:
(document.all
&& window.ActiveXObject

View file

@ -39,25 +39,25 @@ body {
padding: 0px 10px;
margin: 0px;
background: #eee;
}
}
p {
padding: 2px;
font-size: 92%;
line-height: 140%;
}
}
a, a:link, a:active, a:visited {
color: #cc3334;
text-decoration: none;
padding-left: 1px;
padding-right: 1px;
}
}
a:hover {
color: #fff;
background-color: #cc3334;
}
}
h1 {
font-size: 304%;
@ -106,30 +106,106 @@ a.show_notes:hover, a.link_to_notes:hover {background-image: url(../images/notes
#content {
margin-top: 90px;
}
}
#display_box {
float: left;
width: 55%;
margin: 0px 10px 50px 15px;
}
}
#single_box {
width: 60%;
margin: 80px auto;
}
}
#full_width_display {
float: left;
width: 95%;
margin: 0px 15px 90px 15px;
}
}
#display_box_projects {
float: left;
width: 95%;
margin: 0px 15px 90px 15px;
}
}
#recurring_timespan, #recurring_target {
border: none;
clear: both;
}
#recurring_target {
border-top-style: dotted;
padding: 15px 0px 0px 0px;
}
#recurring_daily, #recurring_weekly, #recurring_monthly, #recurring_yearly,
#recurring_edit_daily, #recurring_edit_weekly, #recurring_edit_monthly, #recurring_edit_yearly {
border: none;
border-top-style: dotted;
clear: both;
padding: 15px 0px 15px 0px;
}
#recurring_period_id, #recurring_edit_period_id {
border: none;
float: left;
margin: 0px 50px 0px 0px;
padding: 0px 25px 15px 50px;
border-right-style: none;
}
#recurring_todo {
width: 270px;
}
#recurring_todo_form_container {
border-right-style: dotted;
padding: 0px 50px 15px 0px;
float: left;
}
#recurring_todo input, #recurring_todo textarea {
width: 100%;
}
#overlay {
visibility: hidden;
position: absolute;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
z-index: 102;
text-align: center;
background-image:url("../images/trans70.png");
}
#overlay #new-recurring-todo, #overlay #edit-recurring-todo {
width:750px;
background-color: #fff;
border:1px solid #000;
padding: 15px;
margin: 70px auto;
}
.recurring_container {
padding: 0px 5px 0px 5px;
border: 1px solid #999;
margin: 0px 0px 0px 0px;
background: #fff;
text-align: left;
}
.recurring_submit_box {
height: 25px;
padding: 5px 0;
text-align: center;
clear: both;
border: none;
}
/* Navigation links at the top */
@ -137,20 +213,20 @@ a.show_notes:hover, a.link_to_notes:hover {background-image: url(../images/notes
position: fixed;
top: 48px;
left: 0px;
}
}
#navlist {
margin: 0;
padding: 0 0 20px 5px;
/* border-bottom: 1px solid #000;*/
}
/* border-bottom: 1px solid #000;*/
}
#navlist ul, #navlist li {
margin: 0;
padding: 0;
display: inline;
list-style-type: none;
}
}
#navlist a:link, #navlist a:visited {
float: left;
@ -159,14 +235,14 @@ a.show_notes:hover, a.link_to_notes:hover {background-image: url(../images/notes
margin: 0 10px 4px 10px;
text-decoration: none;
color: #eee;
}
}
#navlist a:link#current, #navlist a:visited#current, #navlist a:hover {
border-bottom: 4px solid #CCC;
padding-bottom: 2px;
background: transparent;
color: #CCC;
}
}
#navlist a:hover { color: #CCC; }
@ -183,8 +259,8 @@ a.show_notes:hover, a.link_to_notes:hover {background-image: url(../images/notes
opacity: .75;
color: #eee;
width: 100%;
z-index:1100;
}
z-index:101;
}
body.stats #topbar {
filter: alpha(opacity=100);
-moz-opacity: 1;
@ -197,10 +273,10 @@ body.stats #topbar {
margin-top: 15px;
margin-bottom: 5px;
white-space: nowrap; /* added 2006-05-17 for safari display, timfm */
}
}
#date h1 {
font-size: 152%;
}
}
#minilinks {
text-align: right;
@ -208,14 +284,14 @@ body.stats #topbar {
right: 15px;
top: 10px;
font-size: 0.9em;
}
}
.container {
padding: 0px 5px 0px 5px;
border: 1px solid #999;
margin: 0px 0px 15px 0px;
background: #fff;
}
}
.completed {
background: #eee;
@ -231,34 +307,34 @@ body.stats #topbar {
color: #666;
position:left; /* changed from relative to left in order to show 'add note' */
/* text-shadow: rgba(0,0,0,.4) 0px 2px 5px; */
}
}
.container_toggle img {
height:20px;
width:20px;
border:0px;
}
}
h2 a, h2 a:link, h2 a:active, h2 a:visited {
color: #666;
text-decoration: none;
}
}
h2 a:hover {
color: #cc3334;
background-color: transparent;
text-decoration: none;
}
}
div#input_box {
margin: 0px 15px 0px 58%;
padding: 0px 15px 0px 0px;
}
}
#input_box h2 {
color: #999;
}
}
#input_box ul {list-style-type: circle; font-size: 0.9em;}
@ -269,45 +345,50 @@ div#input_box {
.box {
float: left;
width: 20px;
}
}
div.item-container {
padding: 2px 0px;
line-height:20px;
clear: both;
}
}
a.recurring_icon {
vertical-align: middle;
background-color: transparent;
}
a.icon {
float: left;
vertical-align: middle;
background-color: transparent;
}
}
input.item-checkbox {
float: left;
margin-left: 10px;
vertical-align: middle;
}
}
.description {
margin-left: 85px;
position:relative
}
}
.stale_l1, .stale_l2, .stale_l3 {
margin-left: 82px;
padding-left: 3px;
}
}
.stale_l1 {
background: #ffC;
}
}
.tools {
margin-left: 25px;
width: 40px;
border-top: 1px solid #999;
}
}
#footer {
clear: both;
@ -316,7 +397,7 @@ input.item-checkbox {
color: #999;
margin: 20px 20px 5px 20px;
padding: 0px;
}
}
/* The notes which may be attached to an item */
.todo_notes {
@ -325,13 +406,13 @@ input.item-checkbox {
border: 1px solid #F5ED59;
background: #FAF6AE;
color: #666666;
}
}
.todo_notes p, .todo_notes li {
padding: 1px;
margin: 0px;
font-size: 12px;
}
}
.todo_notes ul, .note_wrapper ul {
list-style-type: disc;
@ -348,11 +429,11 @@ input.item-checkbox {
div.note_wrapper {
margin: 3px;
padding: 2px;
}
}
div.note_wrapper p {
display: inline;
}
}
div.note_footer {
border-top: 1px solid #999;
@ -360,22 +441,22 @@ div.note_footer {
font-style: italic;
font-size: 0.9em;
color: #666;
}
}
div.note_footer a, div.note_footer a:hover {
border-top: none;
padding-top: 0px;
vertical-align: middle;
background-color: transparent;
}
}
div.add_note_link {
margin-top:12px;
float: right;
}
}
div#project_status > div {
padding: 10px;
}
}
#project_status span {
margin-right:5px;
background-color:white;
@ -456,41 +537,41 @@ h4.notice {
}
/* Draw attention to some text
Same format as traffic lights */
Same format as traffic lights */
.red {
color: #fff;
background: #f00;
padding: 1px;
font-size: 85%;
}
}
.amber {
color: #fff;
background: #ff6600;
padding: 1px;
font-size: 85%;
}
}
.orange {
color: #fff;
background: #FFA500;
padding: 1px;
font-size: 85%;
}
}
.green {
color: #fff;
background: #33cc00;
padding: 1px;
font-size: 85%;
}
}
.grey {
color: #fff;
background: #999;
padding: 2px;
font-size: 85%;
}
}
.info {
color: #fff;
@ -498,7 +579,7 @@ h4.notice {
border: 1px solid #999;
padding: 5px;
text-align: center;
}
}
.highlight {
background: #ffC;
@ -506,19 +587,19 @@ h4.notice {
}
/* Backgrounds marking out 'staleness' of a task based on age of creation date
The colour of the background gets progressively yellower with age */
The colour of the background gets progressively yellower with age */
.stale_l1 {
background: #ffC;
}
}
.stale_l2 {
background: #ff6;
}
}
.stale_l3 {
background: #ff0;
}
}
/* Shows the number of undone next action */
.badge {
@ -528,11 +609,11 @@ h4.notice {
font-size: 12pt;
margin: 10px 10px 0px 0px;
height:26px;
}
}
ul {
list-style-type: none;
}
}
#sidebar h3 {
margin-top:15px;
@ -542,15 +623,15 @@ ul {
#sidebar ul {
margin-left: auto;
list-style-position: inside;
}
}
li {
font-size: 1.1em;
padding: 3px 0px;
}
}
#sidebar li {
padding: auto;
}
}
#sidebar .integrations-link {
margin-top:10px;
padding-top:10px;
@ -562,7 +643,7 @@ li {
padding: 4px 4px 4px 8px;
margin: 2px 2px;
border: 1px solid #ccc;
}
}
.edit-form {
background: #ccc;
@ -570,12 +651,12 @@ li {
border-top: 1px solid #999;
border-bottom: 1px solid #999;
position:relative;
}
}
/* Right align labels in forms */
.label {
text-align: right;
}
}
input {
vertical-align: middle;
@ -587,25 +668,25 @@ img.position, a:hover img.position {
text-align: left;
vertical-align: middle;
background-color: transparent;
}
}
.data {
text-align: left;
margin-left: 20px;
float: left;
}
}
div.buttons, div.buttons a, div.buttons a:hover {
text-align: right;
margin-right: 0px;
vertical-align: middle;
background-color: transparent;
}
}
div#list-active-projects, div#list-hidden-projects, div#list-completed-projects, div#list-contexts, div#projects-empty-nd {
clear:right;
border: 1px solid #999;
}
}
.project-state-group h2 {
margin:20px 0px 8px 13px;
}
@ -623,11 +704,11 @@ div.alpha_sort {
.container td {
border: none;
padding-bottom: 5px;
}
}
.container form {
border: none;
}
}
div.project_description {
background: #eee;
@ -642,7 +723,7 @@ div.project_description {
/* Uncomment line below if you want the description to have
shadowed text */
/* text-shadow: rgba(0,0,0,.4) 0px 2px 5px; */
}
}
#project-next-prev {
text-align:right;
}
@ -652,28 +733,28 @@ form {
border: 1px solid #CCC;
padding: 10px;
margin: 0px;
}
}
.inline-form {
border: none;
padding: 3px;
}
}
.inline-form table {
padding-right: 3px;
}
}
/* expand form contents to fill the whole form */
.inline-form table,
.inline-form textarea#item_notes,
.inline-form input#item_description {
width: 100%;
}
}
/* shrink the label/left column as small as necessary */
.inline-form table td.label {
width: 13ex;
}
#todo_new_action_container, #project_new_project_container, #context_new_container {
}
#todo_new_action_container, #project_new_project_container, #context_new_container, #recurring_new_container {
background: #ddd;
width: 270px;
padding: 5px 10px;
@ -684,6 +765,10 @@ form {
color: #eee;
}
#recurring_new_container img {
vertical-align: middle;
}
#project_new_project_filler {
padding-top: 50px;
}
@ -757,11 +842,11 @@ form.button-to {
label {
font-weight: bold;
padding: 0px 0px;
}
}
input, select, textarea {
margin: 0px 0px 5px 0px;
}
}
.feed {
font-family: verdana, sans-serif;
@ -774,13 +859,13 @@ input, select, textarea {
border-color: #FC9 #630 #330 #F96;
padding:0px 3px 0px 3px;
margin:0px;
}
}
/* Classes for Drag and Drop */
.position {
float: left;
margin-top:2px;
}
}
.handle {
color: #fff;
background: #000;
@ -963,7 +1048,7 @@ table.export_table {
.export_table td {border: 1px; padding: 5px 0px 5px 5px;}
/* Submit button styling from ParticleTree
http://particletree.com/features/rediscovering-the-button-element/ */
http://particletree.com/features/rediscovering-the-button-element/ */
.widgets a, .widgets button{
display:block;

129
test/fixtures/recurring_todos.yml vendored Normal file
View file

@ -0,0 +1,129 @@
1:
id: 1
user_id: 1
context_id: 1
project_id: 2
description: Call Bill Gates every day
notes: ~
state: active
start_from: ~
ends_on: no_end_date
end_date: ~
number_of_occurences: ~
target: due_date
show_from_delta: ~
recurring_period: daily
recurrence_selector: ~
every_other1: 1
every_other2: ~
every_other3: ~
every_day: ~
only_work_days: false
every_count: ~
weekday: ~
created_at: <%= last_week %>
completed_at: ~
2:
id: 2
user_id: 1
context_id: 1
project_id: 2
description: Call Bill Gates every workday
notes: ~
state: active
start_from: ~
ends_on: no_end_date
end_date: ~
number_of_occurences: ~
target: due_date
show_from_delta: ~
recurring_period: daily
recurrence_selector: ~
every_other1: ~
every_other2: ~
every_other3: ~
every_day: ~
only_work_days: true
every_count: ~
weekday: ~
created_at: <%= last_week %>
completed_at: ~
3:
id: 3
user_id: 1
context_id: 1
project_id: 2
description: Call Bill Gates every week
notes: ~
state: active
start_from: ~
ends_on: no_end_date
end_date: ~
number_of_occurences: ~
target: due_date
show_from_delta: ~
recurring_period: weekly
recurrence_selector: ~
every_other1: 2
every_other2: ~
every_other3: ~
every_day: smtwtfs
only_work_days: false
every_count: ~
weekday: ~
created_at: <%= last_week %>
completed_at: ~
4:
id: 4
user_id: 1
context_id: 1
project_id: 2
description: Check with Bill every last friday of the month
notes: ~
state: active
start_from: ~
ends_on: no_end_date
end_date: ~
number_of_occurences: ~
target: due_date
show_from_delta: 5
recurring_period: monthly
recurrence_selector: 1
every_other1: 1
every_other2: 2
every_other3: 5
every_day: ~
only_work_days: false
every_count: 5
weekday: ~
created_at: <%= last_week %>
completed_at: ~
5:
id: 5
user_id: 1
context_id: 1
project_id: 2
description: Congratulate Reinier on his birthday
notes: ~
state: active
start_from: ~
ends_on: no_end_date
end_date: ~
number_of_occurences: ~
target: due_date
show_from_delta: 5
recurring_period: yearly
recurrence_selector: 0
every_other1: 8
every_other2: 6
every_other3: ~
every_day: ~
only_work_days: false
every_count: ~
weekday: ~
created_at: <%= last_week %>
completed_at: ~

View file

@ -0,0 +1,22 @@
require File.dirname(__FILE__) + '/../test_helper'
class RecurringTodosControllerTest < ActionController::TestCase
fixtures :users, :preferences, :projects, :contexts, :todos, :tags, :taggings, :recurring_todos
def setup
@controller = RecurringTodosController.new
@request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new
end
def test_get_index_when_not_logged_in
get :index
assert_redirected_to :controller => 'login', :action => 'login'
end
def test_destroy_recurring_todo
login_as(:admin_user)
xhr :post, :destroy, :id => 1, :_source_view => 'todo'
assert_rjs :page, "recurring_todo_1", :remove
end
end

View file

@ -0,0 +1,249 @@
require File.dirname(__FILE__) + '/../test_helper'
class RecurringTodoTest < Test::Rails::TestCase
fixtures :todos, :users, :contexts, :preferences, :tags, :taggings, :recurring_todos
def setup
@every_day = RecurringTodo.find(1).reload
@every_workday = RecurringTodo.find(2).reload
@weekly_every_day = RecurringTodo.find(3).reload
@monthly_every_last_friday = RecurringTodo.find(4).reload
@yearly = RecurringTodo.find(5).reload
@today = Time.now.utc
@tomorrow = @today + 1.day
@in_three_days = Time.now.utc + 3.days
@in_four_days = @in_three_days + 1.day # need a day after start_from
@friday = Time.utc(2008,6,6)
@saturday = Time.utc(2008,6,7)
@sunday = Time.utc(2008,6,8) # june 8, 2008 was a sunday
@monday = Time.utc(2008,6,9)
@tuesday = Time.utc(2008,6,10)
@wednesday = Time.utc(2008,6,11)
@thursday = Time.utc(2008,6,12)
end
def test_pattern_text
assert_equal "every day", @every_day.recurrence_pattern
assert_equal "on work days", @every_workday.recurrence_pattern
assert_equal "every last Friday of every 2 months", @monthly_every_last_friday.recurrence_pattern
assert_equal "every year on June 8", @yearly.recurrence_pattern
end
def test_daily_every_day
# every_day should return todays date if there was no previous date
due_date = @every_day.get_due_date(nil)
# use to_s in compare, because milisec could be different
assert_equal @today.to_s, due_date.to_s
# when the last todo was completed today, the next todo is due tomorrow
due_date =@every_day.get_due_date(@today)
assert_equal @tomorrow, due_date
# every_day should return start_day if it is in the future
@every_day.start_from = @in_three_days
due_date = @every_day.get_due_date(nil)
assert_equal @in_three_days, due_date
# if we give a date in the future for the previous todo, the next to do
# should be based on that future date.
due_date = @every_day.get_due_date(@in_four_days)
assert_equal @in_four_days+1.day, due_date
# do something every 14 days
@every_day.every_other1=14
due_date = @every_day.get_due_date(@today)
assert_equal @today+14.days, due_date
end
def test_daily_work_days
assert_equal @monday, @every_workday.get_due_date(@friday)
assert_equal @monday, @every_workday.get_due_date(@saturday)
assert_equal @monday, @every_workday.get_due_date(@sunday)
assert_equal @tuesday, @every_workday.get_due_date(@monday)
end
def test_show_from_date
# assume that target due_date works fine, i.e. don't do the same tests over
@every_day.target='show_from_date'
# when recurrence is targeted on show_from, due date shoult remain nil
assert_equal nil, @every_day.get_due_date(nil)
assert_equal nil, @every_day.get_due_date(@today-3.days)
# check show from get the next day
assert_equal @today, @every_day.get_show_from_date(@today-1.days)
@every_day.target='due_date'
# when target on due_date, show_from is relative to due date unless delta=0
assert_equal nil, @every_day.get_show_from_date(@today-1.days)
@every_day.show_from_delta=10
assert_equal @today, @every_day.get_show_from_date(@today+9.days) #today+1+9-10
# TODO: show_from has no use case for daily pattern. Need to test on
# weekly/monthly/yearly
end
def test_end_date_on_recurring_todo
assert_equal true, @every_day.has_next_todo(@in_three_days)
assert_equal true, @every_day.has_next_todo(@in_four_days)
@every_day.end_date = @in_four_days
assert_equal false, @every_day.has_next_todo(@in_four_days)
end
def test_weekly_every_day_setters
@weekly_every_day.every_day = ' '
@weekly_every_day.weekly_return_sunday=('s')
assert_equal 's ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_monday=('m')
assert_equal 'sm ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_tuesday=('t')
assert_equal 'smt ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_wednesday=('w')
assert_equal 'smtw ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_thursday=('t')
assert_equal 'smtwt ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_friday=('f')
assert_equal 'smtwtf ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_saturday=('s')
assert_equal 'smtwtfs', @weekly_every_day.every_day
# test remove
@weekly_every_day.weekly_return_wednesday=(' ')
assert_equal 'smt tfs', @weekly_every_day.every_day
end
def test_weekly_pattern
assert_equal true, @weekly_every_day.has_next_todo(nil)
due_date = @weekly_every_day.get_due_date(@sunday)
assert_equal @monday, due_date
# saturday is last day in week, so the next date should be sunday + n_weeks
due_date = @weekly_every_day.get_due_date(@saturday)
assert_equal @sunday + 2.weeks, due_date
# remove tuesday and wednesday
@weekly_every_day.weekly_return_tuesday=(' ')
@weekly_every_day.weekly_return_wednesday=(' ')
assert_equal 'sm tfs', @weekly_every_day.every_day
due_date = @weekly_every_day.get_due_date(@monday)
assert_equal @thursday, due_date
@weekly_every_day.every_other1 = 1
@weekly_every_day.every_day = ' tw '
due_date = @weekly_every_day.get_due_date(@tuesday)
assert_equal @wednesday, due_date
due_date = @weekly_every_day.get_due_date(@wednesday)
assert_equal @tuesday+1.week, due_date
end
def test_monthly_pattern
due_date = @monthly_every_last_friday.get_due_date(@sunday)
assert_equal Time.utc(2008,6,27), due_date
friday_is_last_day_of_month = Time.utc(2008,10,31)
due_date = @monthly_every_last_friday.get_due_date(friday_is_last_day_of_month )
assert_equal friday_is_last_day_of_month , due_date
@monthly_every_third_friday = @monthly_every_last_friday
@monthly_every_third_friday.every_other3=3 #third
due_date = @monthly_every_last_friday.get_due_date(@sunday) # june 8th 2008
assert_equal Time.utc(2008, 6, 20), due_date
# set date past third friday of this month
due_date = @monthly_every_last_friday.get_due_date(Time.utc(2008,6,21)) # june 21th 2008
assert_equal Time.utc(2008, 8, 15), due_date # every 2 months, so aug
@monthly = @monthly_every_last_friday
@monthly.recurrence_selector=0
@monthly.every_other1 = 8 # every 8th day of the month
@monthly.every_other2 = 2 # every 2 months
due_date = @monthly.get_due_date(@saturday) # june 7th
assert_equal @sunday, due_date # june 8th
due_date = @monthly.get_due_date(@sunday) # june 8th
assert_equal Time.utc(2008,8,8), due_date # aug 8th
end
def test_yearly_pattern
# beginning of same year
due_date = @yearly.get_due_date(Time.utc(2008,2,10)) # feb 10th
assert_equal @sunday, due_date # june 8th
# same month, previous date
due_date = @yearly.get_due_date(@saturday) # june 7th
show_from_date = @yearly.get_show_from_date(@saturday) # june 7th
assert_equal @sunday, due_date # june 8th
assert_equal @sunday-5.days, show_from_date
# same month, day after
due_date = @yearly.get_due_date(@monday) # june 9th
assert_equal Time.utc(2009,6,8), due_date # june 8th next year
@yearly.recurrence_selector = 1
@yearly.every_other3 = 2 # second
@yearly.every_count = 3 # wednesday
# beginning of same year
due_date = @yearly.get_due_date(Time.utc(2008,2,10)) # feb 10th
assert_equal Time.utc(2008,6,11), due_date # june 11th
# same month, before second wednesday
due_date = @yearly.get_due_date(@saturday) # june 7th
assert_equal Time.utc(2008,6,11), due_date # june 11th
# same month, after second wednesday
due_date = @yearly.get_due_date(Time.utc(2008,6,12)) # june 7th
assert_equal Time.utc(2009,6,10), due_date # june 10th
# test handling of nil
due_date1 = @yearly.get_due_date(nil)
due_date2 = @yearly.get_due_date(Time.now.utc + 1.day)
assert_equal due_date1, due_date2
this_year = Time.now.utc.year
@yearly.start_from = Time.utc(this_year+1,6,12)
due_date = @yearly.get_due_date(nil)
assert_equal due_date.year, this_year+2
end
def test_toggle_completion
t = @yearly
assert_equal :active, t.current_state
t.toggle_completion!
assert_equal :completed, t.current_state
t.toggle_completion!
assert_equal :active, t.current_state
end
def test_starred
@yearly.tag_with("1, 2, starred", User.find(@yearly.user_id))
@yearly.tags.reload
assert_equal true, @yearly.starred?
assert_equal false, @weekly_every_day.starred?
@yearly.toggle_star!
assert_equal false, @yearly.starred?
@yearly.toggle_star!
assert_equal true, @yearly.starred?
end
def test_occurence_count
@every_day.number_of_occurences = 2
assert_equal true, @every_day.has_next_todo(@in_three_days)
@every_day.inc_occurences
assert_equal true, @every_day.has_next_todo(@in_three_days)
@every_day.inc_occurences
assert_equal false, @every_day.has_next_todo(@in_three_days)
# after completion, when you reactivate the recurring todo, the occurences
# count should be reset
assert_equal 2, @every_day.occurences_count
@every_day.toggle_completion!
@every_day.toggle_completion!
assert_equal true, @every_day.has_next_todo(@in_three_days)
assert_equal 0, @every_day.occurences_count
end
end