mirror of
https://github.com/TracksApp/tracks.git
synced 2025-12-16 15:20:13 +01:00
add recurring todos to tracks
This commit is contained in:
parent
c46f0a8e04
commit
8bc41e2cb0
41 changed files with 2576 additions and 632 deletions
2
README
2
README
|
|
@ -20,4 +20,4 @@ For those upgrading, change notes are available in /doc/CHANGELOG. If you are th
|
||||||
|
|
||||||
While fully usable for everyday use, Tracks is still a work in progress. Make sure that you take sensible precautions and back up all your data frequently, taking particular care when you are upgrading.
|
While fully usable for everyday use, Tracks is still a work in progress. Make sure that you take sensible precautions and back up all your data frequently, taking particular care when you are upgrading.
|
||||||
|
|
||||||
Enjoy being productive!
|
Enjoy being productive!
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
# The filters added to this controller will be run for all controllers in the application.
|
# The filters added to this controller will be run for all controllers in the
|
||||||
# Likewise will all the methods added be available for all controllers.
|
# application. Likewise will all the methods added be available for all
|
||||||
|
# controllers.
|
||||||
|
|
||||||
require_dependency "login_system"
|
require_dependency "login_system"
|
||||||
require_dependency "tracks/source_view"
|
require_dependency "tracks/source_view"
|
||||||
|
|
@ -47,11 +48,12 @@ class ApplicationController < ActionController::Base
|
||||||
# http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions
|
# http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions
|
||||||
unless session == nil
|
unless session == nil
|
||||||
return if @controller_name == 'feed' or session['noexpiry'] == "on"
|
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)
|
# If the method is called by the feed controller (which we don't have
|
||||||
# or if we checked the box to keep logged in on login
|
# under session control) or if we checked the box to keep logged in on
|
||||||
# don't set the session expiry time.
|
# login don't set the session expiry time.
|
||||||
if session
|
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
|
expiry_time = session['expiry_time'] || Time.now + 10
|
||||||
if expiry_time < Time.now
|
if expiry_time < Time.now
|
||||||
# Too late, matey... bang goes your session!
|
# Too late, matey... bang goes your session!
|
||||||
|
|
@ -80,10 +82,10 @@ class ApplicationController < ActionController::Base
|
||||||
# end
|
# end
|
||||||
# end
|
# end
|
||||||
|
|
||||||
# Returns a count of next actions in the given context or project
|
# Returns a count of next actions in the given context or project The result
|
||||||
# The result is count and a string descriptor, correctly pluralised if there are no
|
# is count and a string descriptor, correctly pluralised if there are no
|
||||||
# actions or multiple actions
|
# actions or multiple actions
|
||||||
#
|
#
|
||||||
def count_undone_todos_phrase(todos_parent, string="actions")
|
def count_undone_todos_phrase(todos_parent, string="actions")
|
||||||
count = count_undone_todos(todos_parent)
|
count = count_undone_todos(todos_parent)
|
||||||
if count == 1
|
if count == 1
|
||||||
|
|
@ -105,9 +107,9 @@ class ApplicationController < ActionController::Base
|
||||||
count || 0
|
count || 0
|
||||||
end
|
end
|
||||||
|
|
||||||
# Convert a date object to the format specified in the user's preferences
|
# Convert a date object to the format specified in the user's preferences in
|
||||||
# in config/settings.yml
|
# config/settings.yml
|
||||||
#
|
#
|
||||||
def format_date(date)
|
def format_date(date)
|
||||||
if date
|
if date
|
||||||
date_format = prefs.date_format
|
date_format = prefs.date_format
|
||||||
|
|
@ -118,10 +120,10 @@ class ApplicationController < ActionController::Base
|
||||||
formatted_date
|
formatted_date
|
||||||
end
|
end
|
||||||
|
|
||||||
# Uses RedCloth to transform text using either Textile or Markdown
|
# Uses RedCloth to transform text using either Textile or Markdown Need to
|
||||||
# Need to require redcloth above
|
# require redcloth above RedCloth 3.0 or greater is needed to use Markdown,
|
||||||
# RedCloth 3.0 or greater is needed to use Markdown, otherwise it only handles Textile
|
# otherwise it only handles Textile
|
||||||
#
|
#
|
||||||
def markdown(text)
|
def markdown(text)
|
||||||
RedCloth.new(text).to_html
|
RedCloth.new(text).to_html
|
||||||
end
|
end
|
||||||
|
|
@ -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
|
Hash[*projects.reject{ |p| p.default_context.nil? }.map{ |p| [p.name, p.default_context.name] }.flatten].to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
# Here's the concept behind this "mobile content negotiation" hack:
|
# Here's the concept behind this "mobile content negotiation" hack: In
|
||||||
# In addition to the main, AJAXy Web UI, Tracks has a lightweight
|
# addition to the main, AJAXy Web UI, Tracks has a lightweight low-feature
|
||||||
# low-feature 'mobile' version designed to be suitablef or use
|
# 'mobile' version designed to be suitablef or use from a phone or PDA. It
|
||||||
# from a phone or PDA. It makes some sense that tne pages of that
|
# makes some sense that tne pages of that mobile version are simply alternate
|
||||||
# mobile version are simply alternate representations of the same
|
# representations of the same Todo resources. The implementation goal was to
|
||||||
# Todo resources. The implementation goal was to treat mobile
|
# treat mobile as another format and be able to use respond_to to render both
|
||||||
# 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
|
||||||
# versions. Unfortunately, I ran into a lot of trouble simply
|
# new mime type 'text/html' with format :m because :html already is linked to
|
||||||
# registering a new mime type 'text/html' with format :m because
|
# that mime type and the new registration was forcing all html requests to be
|
||||||
# :html already is linked to that mime type and the new
|
# rendered in the mobile view. The before_filter and after_filter hackery
|
||||||
# registration was forcing all html requests to be rendered in
|
# below accomplishs that implementation goal by using a 'fake' mime type
|
||||||
# the mobile view. The before_filter and after_filter hackery
|
# during the processing and then setting it to 'text/html' in an
|
||||||
# below accomplishs that implementation goal by using a 'fake'
|
# 'after_filter' -LKM 2007-04-01
|
||||||
# mime type during the processing and then setting it to
|
|
||||||
# 'text/html' in an 'after_filter' -LKM 2007-04-01
|
|
||||||
def mobile?
|
def mobile?
|
||||||
return params[:format] == 'm' || response.content_type == MOBILE_CONTENT_TYPE
|
return params[:format] == 'm' || response.content_type == MOBILE_CONTENT_TYPE
|
||||||
end
|
end
|
||||||
|
|
@ -220,9 +220,9 @@ class ApplicationController < ActionController::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set the contents of the flash message from a controller
|
# Set the contents of the flash message from a controller Usage: notify
|
||||||
# Usage: notify :warning, "This is the message"
|
# :warning, "This is the message" Sets the flash of type 'warning' to "This is
|
||||||
# Sets the flash of type 'warning' to "This is the message"
|
# the message"
|
||||||
def notify(type, message)
|
def notify(type, message)
|
||||||
flash[type] = message
|
flash[type] = message
|
||||||
logger.error("ERROR: #{message}") if type == :error
|
logger.error("ERROR: #{message}") if type == :error
|
||||||
|
|
@ -231,5 +231,29 @@ class ApplicationController < ActionController::Base
|
||||||
def set_time_zone
|
def set_time_zone
|
||||||
Time.zone = current_user.prefs.time_zone if logged_in?
|
Time.zone = current_user.prefs.time_zone if logged_in?
|
||||||
end
|
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
|
end
|
||||||
|
|
|
||||||
237
app/controllers/recurring_todos_controller.rb
Normal file
237
app/controllers/recurring_todos_controller.rb
Normal 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
|
||||||
|
|
@ -121,6 +121,10 @@ class TodosController < ApplicationController
|
||||||
#
|
#
|
||||||
def toggle_check
|
def toggle_check
|
||||||
@saved = @todo.toggle_completion!
|
@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|
|
respond_to do |format|
|
||||||
format.js do
|
format.js do
|
||||||
if @saved
|
if @saved
|
||||||
|
|
@ -235,6 +239,10 @@ class TodosController < ApplicationController
|
||||||
@todo = get_todo_from_params
|
@todo = get_todo_from_params
|
||||||
@context_id = @todo.context_id
|
@context_id = @todo.context_id
|
||||||
@project_id = @todo.project_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
|
@saved = @todo.destroy
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
|
@ -643,6 +651,19 @@ class TodosController < ApplicationController
|
||||||
def self.is_feed_request(req)
|
def self.is_feed_request(req)
|
||||||
['rss','atom','txt','ics'].include?(req.parameters[:format])
|
['rss','atom','txt','ics'].include?(req.parameters[:format])
|
||||||
end
|
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
|
class FindConditionBuilder
|
||||||
|
|
||||||
|
|
@ -711,6 +732,6 @@ class TodosController < ApplicationController
|
||||||
return false if context_name.blank?
|
return false if context_name.blank?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
73
app/helpers/recurring_todos_helper.rb
Normal file
73
app/helpers/recurring_todos_helper.rb
Normal 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
|
||||||
|
|
@ -47,7 +47,7 @@ module TodosHelper
|
||||||
:prevent_default => true
|
:prevent_default => true
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote_star_icon
|
def remote_star_icon
|
||||||
str = link_to( image_tag_for_star(@todo),
|
str = link_to( image_tag_for_star(@todo),
|
||||||
toggle_star_todo_path(@todo),
|
toggle_star_todo_path(@todo),
|
||||||
:class => "icon star_item", :title => "star the action '#{@todo.description}'")
|
:class => "icon star_item", :title => "star the action '#{@todo.description}'")
|
||||||
|
|
@ -66,7 +66,7 @@ module TodosHelper
|
||||||
|
|
||||||
def remote_edit_icon
|
def remote_edit_icon
|
||||||
if !@todo.completed?
|
if !@todo.completed?
|
||||||
str = link_to( image_tag_for_edit,
|
str = link_to( image_tag_for_edit(@todo),
|
||||||
edit_todo_path(@todo),
|
edit_todo_path(@todo),
|
||||||
:class => "icon edit_icon")
|
:class => "icon edit_icon")
|
||||||
set_behavior_for_edit_icon
|
set_behavior_for_edit_icon
|
||||||
|
|
@ -205,12 +205,12 @@ module TodosHelper
|
||||||
javascript_tag str
|
javascript_tag str
|
||||||
end
|
end
|
||||||
|
|
||||||
def item_container_id
|
def item_container_id (todo)
|
||||||
if source_view_is :project
|
if source_view_is :project
|
||||||
return "p#{@todo.project_id}" if @todo.active?
|
return "p#{todo.project_id}" if todo.active?
|
||||||
return "tickler" if @todo.deferred?
|
return "tickler" if todo.deferred?
|
||||||
end
|
end
|
||||||
return "c#{@todo.context_id}"
|
return "c#{todo.context_id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def should_show_new_item
|
def should_show_new_item
|
||||||
|
|
@ -272,8 +272,8 @@ module TodosHelper
|
||||||
image_tag("blank.png", :title =>"Delete action", :class=>"delete_item")
|
image_tag("blank.png", :title =>"Delete action", :class=>"delete_item")
|
||||||
end
|
end
|
||||||
|
|
||||||
def image_tag_for_edit
|
def image_tag_for_edit(todo)
|
||||||
image_tag("blank.png", :title =>"Edit action", :class=>"edit_item", :id=> dom_id(@todo, 'edit_icon'))
|
image_tag("blank.png", :title =>"Edit action", :class=>"edit_item", :id=> dom_id(todo, 'edit_icon'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def image_tag_for_star(todo)
|
def image_tag_for_star(todo)
|
||||||
|
|
@ -281,4 +281,4 @@ module TodosHelper
|
||||||
image_tag("blank.png", :title =>"Star action", :class => class_str)
|
image_tag("blank.png", :title =>"Star action", :class => class_str)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
517
app/models/recurring_todo.rb
Normal file
517
app/models/recurring_todo.rb
Normal 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
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
class Tag < ActiveRecord::Base
|
class Tag < ActiveRecord::Base
|
||||||
has_many_polymorphs :taggables,
|
has_many_polymorphs :taggables,
|
||||||
:from => [:todos],
|
:from => [:todos, :recurring_todos],
|
||||||
:through => :taggings,
|
:through => :taggings,
|
||||||
:dependent => :destroy
|
:dependent => :destroy
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ class Todo < ActiveRecord::Base
|
||||||
belongs_to :context
|
belongs_to :context
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
belongs_to :recurring_todo
|
||||||
|
|
||||||
STARRED_TAG_NAME = "starred"
|
STARRED_TAG_NAME = "starred"
|
||||||
|
|
||||||
|
|
@ -120,4 +121,8 @@ class Todo < ActiveRecord::Base
|
||||||
starred?
|
starred?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def from_recurring_todo?
|
||||||
|
return self.recurring_todo_id != nil
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
@ -63,6 +63,9 @@ class User < ActiveRecord::Base
|
||||||
has_many :todos,
|
has_many :todos,
|
||||||
:order => 'todos.completed_at DESC, todos.created_at DESC',
|
:order => 'todos.completed_at DESC, todos.created_at DESC',
|
||||||
:dependent => :delete_all
|
: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,
|
has_many :deferred_todos,
|
||||||
:class_name => 'Todo',
|
:class_name => 'Todo',
|
||||||
:conditions => [ 'state = ?', 'deferred' ],
|
:conditions => [ 'state = ?', 'deferred' ],
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ window.onload=function(){
|
||||||
Nifty("div#todo_new_action_container","normal");
|
Nifty("div#todo_new_action_container","normal");
|
||||||
Nifty("div#project_new_project_container","normal");
|
Nifty("div#project_new_project_container","normal");
|
||||||
Nifty("div#context_new_container","normal");
|
Nifty("div#context_new_container","normal");
|
||||||
|
Nifty("div#recurring_new_container","normal");
|
||||||
if ($('flash').visible()) { new Effect.Fade("flash",{duration:5.0}); }
|
if ($('flash').visible()) { new Effect.Fade("flash",{duration:5.0}); }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -56,6 +57,7 @@ window.onload=function(){
|
||||||
<% if current_user.is_admin? -%>
|
<% if current_user.is_admin? -%>
|
||||||
<li><%= navigation_link("Admin", users_path, {:accesskey => "a", :title => "Add or delete users"} ) %></li>
|
<li><%= navigation_link("Admin", users_path, {:accesskey => "a", :title => "Add or delete users"} ) %></li>
|
||||||
<% end -%>
|
<% 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("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("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>
|
<li><%= navigation_link(image_tag("stats.gif", :size => "16X16", :border => 0), {:controller => "stats", :action => "index"}, :title => "See your statistics" ) %></li>
|
||||||
|
|
|
||||||
147
app/views/recurring_todos/_edit_form.html.erb
Normal file
147
app/views/recurring_todos/_edit_form.html.erb
Normal 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(/"/,""") %>" />
|
||||||
|
<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>
|
||||||
12
app/views/recurring_todos/_recurring_todo.html.erb
Normal file
12
app/views/recurring_todos/_recurring_todo.html.erb
Normal 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>
|
||||||
145
app/views/recurring_todos/_recurring_todo_form.erb
Normal file
145
app/views/recurring_todos/_recurring_todo_form.erb
Normal 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>
|
||||||
20
app/views/recurring_todos/create.js.rjs
Normal file
20
app/views/recurring_todos/create.js.rjs
Normal 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
|
||||||
12
app/views/recurring_todos/destroy.js.rjs
Normal file
12
app/views/recurring_todos/destroy.js.rjs
Normal 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
|
||||||
4
app/views/recurring_todos/edit.js.rjs
Normal file
4
app/views/recurring_todos/edit.js.rjs
Normal 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
|
||||||
42
app/views/recurring_todos/index.html.erb
Normal file
42
app/views/recurring_todos/index.html.erb
Normal 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();"
|
||||||
|
-%>
|
||||||
1
app/views/recurring_todos/new.html.erb
Normal file
1
app/views/recurring_todos/new.html.erb
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<%= render :partial => "recurring_todo_form" %>
|
||||||
2
app/views/recurring_todos/show.html.erb
Normal file
2
app/views/recurring_todos/show.html.erb
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<h1>RecurringTodo#show</h1>
|
||||||
|
<p>Find me in app/views/recurring_todo/show.html.erb</p>
|
||||||
30
app/views/recurring_todos/toggle_check.js.rjs
Normal file
30
app/views/recurring_todos/toggle_check.js.rjs
Normal 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
|
||||||
3
app/views/recurring_todos/toggle_star.js.rjs
Normal file
3
app/views/recurring_todos/toggle_star.js.rjs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
if @saved
|
||||||
|
page[@recurring_todo].down('a.star_item').down('img').toggleClassName('starred_todo').toggleClassName('unstarred_todo')
|
||||||
|
end
|
||||||
22
app/views/recurring_todos/update.js.rjs
Normal file
22
app/views/recurring_todos/update.js.rjs
Normal 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
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
<div class="description<%= staleness_class( todo ) %>">
|
<div class="description<%= staleness_class( todo ) %>">
|
||||||
<%= date_span -%>
|
<%= date_span -%>
|
||||||
<span class="todo.descr"><%= h sanitize(todo.description) %></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 %>
|
<%= tag_list %>
|
||||||
<%= deferred_due_date %>
|
<%= deferred_due_date %>
|
||||||
<%= project_and_context_links( parent_container_type, :suppress_context => suppress_context, :suppress_project => suppress_project ) %>
|
<%= project_and_context_links( parent_container_type, :suppress_context => suppress_context, :suppress_project => suppress_project ) %>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ if @saved
|
||||||
page.insert_html :top, 'display_box', :partial => 'contexts/context', :locals => { :context => @todo.context, :collapsible => true }
|
page.insert_html :top, 'display_box', :partial => 'contexts/context', :locals => { :context => @todo.context, :collapsible => true }
|
||||||
else
|
else
|
||||||
page.call "todoItems.ensureVisibleWithEffectAppear", "c#{@todo.context_id}" if source_view_is_one_of(:todo, :deferred)
|
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.visual_effect :highlight, dom_id(@todo), :duration => 3
|
||||||
page[empty_container_msg_div_id].hide unless empty_container_msg_div_id.nil?
|
page[empty_container_msg_div_id].hide unless empty_container_msg_div_id.nil?
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,23 @@
|
||||||
if @saved
|
if @saved
|
||||||
page[@todo].remove
|
page[@todo].remove
|
||||||
page['badge_count'].replace_html @down_count
|
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[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
|
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
|
else
|
||||||
page.notify :error, "There was an error deleting the item #{@todo.description}", 8.0
|
page.notify :error, "There was an error deleting the item #{@todo.description}", 8.0
|
||||||
end
|
end
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
if @saved
|
if @saved
|
||||||
page[@todo].remove
|
page[@todo].remove
|
||||||
if @todo.completed?
|
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?
|
unless @prefs.hide_completed_actions?
|
||||||
page.insert_html :top, "completed", :partial => 'todos/todo', :locals => { :parent_container_type => "completed" }
|
page.insert_html :top, "completed", :partial => 'todos/todo', :locals => { :parent_container_type => "completed" }
|
||||||
page.visual_effect :highlight, dom_id(@todo, 'line'), {'startcolor' => "'#99ff99'"}
|
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.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
|
page.hide 'empty-d' # If we've checked something as done, completed items can't be empty
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# remove container if empty
|
||||||
if @remaining_in_context == 0 && source_view_is(:todo)
|
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
|
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.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
|
else
|
||||||
page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id
|
# todo is activated from completed container
|
||||||
page.insert_html :bottom, item_container_id, :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
|
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.visual_effect :highlight, dom_id(@todo, 'line'), {'startcolor' => "'#99ff99'"}
|
||||||
page.show "empty-d" if @completed_count == 0
|
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
|
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
|
end
|
||||||
|
|
||||||
page.hide "status"
|
page.hide "status"
|
||||||
page.replace_html "badge_count", @down_count
|
page.replace_html "badge_count", @down_count
|
||||||
if @todo.completed? && !@todo.project_id.nil? && @prefs.show_project_on_todo_done && !source_view_is(:project)
|
if @todo.completed? && !@todo.project_id.nil? && @prefs.show_project_on_todo_done && !source_view_is(:project)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ if @saved
|
||||||
status_message = 'Added new context / ' + status_message if @new_context_created
|
status_message = 'Added new context / ' + status_message if @new_context_created
|
||||||
page.notify :notice, status_message, 5.0
|
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 << "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
|
page << "projectAutoCompleter.options.array = #{project_names_for_autocomplete}; projectAutoCompleter.changed = true" if @new_project_created
|
||||||
if source_view_is_one_of(:todo, :context)
|
if source_view_is_one_of(:todo, :context)
|
||||||
|
|
@ -88,9 +88,9 @@ if @saved
|
||||||
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
|
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
|
||||||
page.visual_effect :highlight, dom_id(@todo), :duration => 3
|
page.visual_effect :highlight, dom_id(@todo), :duration => 3
|
||||||
end
|
end
|
||||||
elsif source_view_is :stats
|
elsif source_view_is :stats
|
||||||
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
|
page.replace dom_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
|
||||||
page.visual_effect :highlight, dom_id(@todo), :duration => 3
|
page.visual_effect :highlight, dom_id(@todo), :duration => 3
|
||||||
else
|
else
|
||||||
logger.error "unexpected source_view '#{params[:_source_view]}'"
|
logger.error "unexpected source_view '#{params[:_source_view]}'"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,10 @@ ActionController::Routing::Routes.draw do |map|
|
||||||
map.preferences 'preferences', :controller => 'preferences', :action => 'index'
|
map.preferences 'preferences', :controller => 'preferences', :action => 'index'
|
||||||
map.integrations 'integrations', :controller => 'integrations', :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.
|
# Install the default route as the lowest priority.
|
||||||
map.connect ':controller/:action/:id'
|
map.connect ':controller/:action/:id'
|
||||||
|
|
||||||
|
|
|
||||||
52
db/migrate/039_create_recurring_todos.rb
Normal file
52
db/migrate/039_create_recurring_todos.rb
Normal 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
BIN
public/images/add.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 596 B |
BIN
public/images/recurring16x16.png
Normal file
BIN
public/images/recurring16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 410 B |
BIN
public/images/recurring24x24.png
Normal file
BIN
public/images/recurring24x24.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 598 B |
BIN
public/images/recurring_menu16x16.png
Normal file
BIN
public/images/recurring_menu16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 475 B |
BIN
public/images/recurring_menu24x24.png
Normal file
BIN
public/images/recurring_menu24x24.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 814 B |
BIN
public/images/trans70.png
Normal file
BIN
public/images/trans70.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 328 B |
|
|
@ -1,39 +1,90 @@
|
||||||
var Login = {
|
var Login = {
|
||||||
showOpenid: function() {
|
showOpenid: function() {
|
||||||
if ($('database_auth_form')) $('database_auth_form').hide();
|
if ($('database_auth_form')) $('database_auth_form').hide();
|
||||||
if ($('openid_auth_form')) $('openid_auth_form').show();
|
if ($('openid_auth_form')) $('openid_auth_form').show();
|
||||||
if ($('alternate_auth_openid')) $('alternate_auth_openid').hide();
|
if ($('alternate_auth_openid')) $('alternate_auth_openid').hide();
|
||||||
if ($('alternate_auth_database')) $('alternate_auth_database').show();
|
if ($('alternate_auth_database')) $('alternate_auth_database').show();
|
||||||
if ($('openid_url')) $('openid_url').focus();
|
if ($('openid_url')) $('openid_url').focus();
|
||||||
if ($('openid_url')) $('openid_url').select();
|
if ($('openid_url')) $('openid_url').select();
|
||||||
new CookieManager().setCookie('preferred_auth', 'openid');
|
new CookieManager().setCookie('preferred_auth', 'openid');
|
||||||
},
|
},
|
||||||
|
|
||||||
showDatabase: function(container) {
|
showDatabase: function(container) {
|
||||||
if ($('openid_auth_form')) $('openid_auth_form').hide();
|
if ($('openid_auth_form')) $('openid_auth_form').hide();
|
||||||
if ($('database_auth_form')) $('database_auth_form').show();
|
if ($('database_auth_form')) $('database_auth_form').show();
|
||||||
if ($('alternate_auth_database')) $('alternate_auth_database').hide();
|
if ($('alternate_auth_database')) $('alternate_auth_database').hide();
|
||||||
if ($('alternate_auth_openid')) $('alternate_auth_openid').show();
|
if ($('alternate_auth_openid')) $('alternate_auth_openid').show();
|
||||||
if ($('user_login')) $('user_login').focus();
|
if ($('user_login')) $('user_login').focus();
|
||||||
if ($('user_login')) $('user_login').select();
|
if ($('user_login')) $('user_login').select();
|
||||||
new CookieManager().setCookie('preferred_auth', 'database');
|
new CookieManager().setCookie('preferred_auth', 'database');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var TracksForm = {
|
var TracksForm = {
|
||||||
toggle: function(toggleDivId, formContainerId, formId, hideLinkText, hideLinkTitle, showLinkText, showLinkTitle) {
|
toggle: function(toggleDivId, formContainerId, formId, hideLinkText, hideLinkTitle, showLinkText, showLinkTitle) {
|
||||||
$(formContainerId).toggle();
|
$(formContainerId).toggle();
|
||||||
toggleDiv = $(toggleDivId);
|
toggleDiv = $(toggleDivId);
|
||||||
toggleLink = toggleDiv.down('a');
|
toggleLink = toggleDiv.down('a');
|
||||||
if (toggleDiv.hasClassName('hide_form')) {
|
if (toggleDiv.hasClassName('hide_form')) {
|
||||||
toggleLink.update(showLinkText).setAttribute('title', showLinkTitle);
|
toggleLink.update(showLinkText).setAttribute('title', showLinkTitle);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
toggleLink.update(hideLinkText).setAttribute('title', hideLinkTitle);
|
||||||
|
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";
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
toggleLink.update(hideLinkText).setAttribute('title', hideLinkTitle);
|
|
||||||
Form.focusFirstElement(formId);
|
|
||||||
}
|
|
||||||
toggleDiv.toggleClassName('hide_form');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// uncomment the next four lines for easier debugging with FireBug
|
// uncomment the next four lines for easier debugging with FireBug
|
||||||
// Ajax.Responders.register({
|
// Ajax.Responders.register({
|
||||||
// onException: function(source, exception) {
|
// onException: function(source, exception) {
|
||||||
|
|
@ -43,10 +94,10 @@ var TracksForm = {
|
||||||
|
|
||||||
/* fade flashes automatically */
|
/* fade flashes automatically */
|
||||||
Event.observe(window, 'load', function() {
|
Event.observe(window, 'load', function() {
|
||||||
$A(document.getElementsByClassName('alert')).each(function(o) {
|
$A(document.getElementsByClassName('alert')).each(function(o) {
|
||||||
o.opacity = 100.0
|
o.opacity = 100.0
|
||||||
Effect.Fade(o, {duration: 8.0})
|
Effect.Fade(o, {duration: 8.0})
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -55,12 +106,12 @@ Event.observe(window, 'load', function() {
|
||||||
*/
|
*/
|
||||||
CookieManager = Class.create();
|
CookieManager = Class.create();
|
||||||
CookieManager.prototype =
|
CookieManager.prototype =
|
||||||
{
|
{
|
||||||
BROWSER_IS_IE:
|
BROWSER_IS_IE:
|
||||||
(document.all
|
(document.all
|
||||||
&& window.ActiveXObject
|
&& window.ActiveXObject
|
||||||
&& navigator.userAgent.toLowerCase().indexOf("msie") > -1
|
&& navigator.userAgent.toLowerCase().indexOf("msie") > -1
|
||||||
&& navigator.userAgent.toLowerCase().indexOf("opera") == -1),
|
&& navigator.userAgent.toLowerCase().indexOf("opera") == -1),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* I hate navigator string based browser detection too, but when Opera alone
|
* I hate navigator string based browser detection too, but when Opera alone
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
129
test/fixtures/recurring_todos.yml
vendored
Normal file
129
test/fixtures/recurring_todos.yml
vendored
Normal 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: ~
|
||||||
22
test/functional/recurring_todos_controller_test.rb
Normal file
22
test/functional/recurring_todos_controller_test.rb
Normal 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
|
||||||
249
test/unit/recurring_todo_test.rb
Normal file
249
test/unit/recurring_todo_test.rb
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue