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
|
|
@ -1,5 +1,6 @@
|
|||
# The filters added to this controller will be run for all controllers in the application.
|
||||
# Likewise will all the methods added be available for all controllers.
|
||||
# The filters added to this controller will be run for all controllers in the
|
||||
# application. Likewise will all the methods added be available for all
|
||||
# controllers.
|
||||
|
||||
require_dependency "login_system"
|
||||
require_dependency "tracks/source_view"
|
||||
|
|
@ -47,11 +48,12 @@ class ApplicationController < ActionController::Base
|
|||
# http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions
|
||||
unless session == nil
|
||||
return if @controller_name == 'feed' or session['noexpiry'] == "on"
|
||||
# If the method is called by the feed controller (which we don't have under session control)
|
||||
# or if we checked the box to keep logged in on login
|
||||
# don't set the session expiry time.
|
||||
# If the method is called by the feed controller (which we don't have
|
||||
# under session control) or if we checked the box to keep logged in on
|
||||
# login don't set the session expiry time.
|
||||
if session
|
||||
# Get expiry time (allow ten seconds window for the case where we have none)
|
||||
# Get expiry time (allow ten seconds window for the case where we have
|
||||
# none)
|
||||
expiry_time = session['expiry_time'] || Time.now + 10
|
||||
if expiry_time < Time.now
|
||||
# Too late, matey... bang goes your session!
|
||||
|
|
@ -80,8 +82,8 @@ class ApplicationController < ActionController::Base
|
|||
# end
|
||||
# end
|
||||
|
||||
# Returns a count of next actions in the given context or project
|
||||
# The result is count and a string descriptor, correctly pluralised if there are no
|
||||
# Returns a count of next actions in the given context or project The result
|
||||
# is count and a string descriptor, correctly pluralised if there are no
|
||||
# actions or multiple actions
|
||||
#
|
||||
def count_undone_todos_phrase(todos_parent, string="actions")
|
||||
|
|
@ -105,8 +107,8 @@ class ApplicationController < ActionController::Base
|
|||
count || 0
|
||||
end
|
||||
|
||||
# Convert a date object to the format specified in the user's preferences
|
||||
# in config/settings.yml
|
||||
# Convert a date object to the format specified in the user's preferences in
|
||||
# config/settings.yml
|
||||
#
|
||||
def format_date(date)
|
||||
if date
|
||||
|
|
@ -118,9 +120,9 @@ class ApplicationController < ActionController::Base
|
|||
formatted_date
|
||||
end
|
||||
|
||||
# Uses RedCloth to transform text using either Textile or Markdown
|
||||
# Need to require redcloth above
|
||||
# RedCloth 3.0 or greater is needed to use Markdown, otherwise it only handles Textile
|
||||
# Uses RedCloth to transform text using either Textile or Markdown Need to
|
||||
# require redcloth above RedCloth 3.0 or greater is needed to use Markdown,
|
||||
# otherwise it only handles Textile
|
||||
#
|
||||
def markdown(text)
|
||||
RedCloth.new(text).to_html
|
||||
|
|
@ -130,21 +132,19 @@ class ApplicationController < ActionController::Base
|
|||
Hash[*projects.reject{ |p| p.default_context.nil? }.map{ |p| [p.name, p.default_context.name] }.flatten].to_json
|
||||
end
|
||||
|
||||
# Here's the concept behind this "mobile content negotiation" hack:
|
||||
# In addition to the main, AJAXy Web UI, Tracks has a lightweight
|
||||
# low-feature 'mobile' version designed to be suitablef or use
|
||||
# from a phone or PDA. It makes some sense that tne pages of that
|
||||
# mobile version are simply alternate representations of the same
|
||||
# Todo resources. The implementation goal was to treat mobile
|
||||
# as another format and be able to use respond_to to render both
|
||||
# versions. Unfortunately, I ran into a lot of trouble simply
|
||||
# registering a new mime type 'text/html' with format :m because
|
||||
# :html already is linked to that mime type and the new
|
||||
# registration was forcing all html requests to be rendered in
|
||||
# the mobile view. The before_filter and after_filter hackery
|
||||
# below accomplishs that implementation goal by using a 'fake'
|
||||
# mime type during the processing and then setting it to
|
||||
# 'text/html' in an 'after_filter' -LKM 2007-04-01
|
||||
# Here's the concept behind this "mobile content negotiation" hack: In
|
||||
# addition to the main, AJAXy Web UI, Tracks has a lightweight low-feature
|
||||
# 'mobile' version designed to be suitablef or use from a phone or PDA. It
|
||||
# makes some sense that tne pages of that mobile version are simply alternate
|
||||
# representations of the same Todo resources. The implementation goal was to
|
||||
# treat mobile as another format and be able to use respond_to to render both
|
||||
# versions. Unfortunately, I ran into a lot of trouble simply registering a
|
||||
# new mime type 'text/html' with format :m because :html already is linked to
|
||||
# that mime type and the new registration was forcing all html requests to be
|
||||
# rendered in the mobile view. The before_filter and after_filter hackery
|
||||
# below accomplishs that implementation goal by using a 'fake' mime type
|
||||
# during the processing and then setting it to 'text/html' in an
|
||||
# 'after_filter' -LKM 2007-04-01
|
||||
def mobile?
|
||||
return params[:format] == 'm' || response.content_type == MOBILE_CONTENT_TYPE
|
||||
end
|
||||
|
|
@ -220,9 +220,9 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
# Set the contents of the flash message from a controller
|
||||
# Usage: notify :warning, "This is the message"
|
||||
# Sets the flash of type 'warning' to "This is the message"
|
||||
# Set the contents of the flash message from a controller Usage: notify
|
||||
# :warning, "This is the message" Sets the flash of type 'warning' to "This is
|
||||
# the message"
|
||||
def notify(type, message)
|
||||
flash[type] = message
|
||||
logger.error("ERROR: #{message}") if type == :error
|
||||
|
|
@ -232,4 +232,28 @@ class ApplicationController < ActionController::Base
|
|||
Time.zone = current_user.prefs.time_zone if logged_in?
|
||||
end
|
||||
|
||||
def create_todo_from_recurring_todo(rt, date=nil)
|
||||
# create todo and initialize with data from recurring_todo rt
|
||||
todo = current_user.todos.build( { :description => rt.description, :notes => rt.notes, :project_id => rt.project_id, :context_id => rt.context_id})
|
||||
|
||||
# set dates
|
||||
todo.due = rt.get_due_date(date)
|
||||
todo.show_from = rt.get_show_from_date(date)
|
||||
todo.recurring_todo_id = rt.id
|
||||
saved = todo.save
|
||||
if saved
|
||||
todo.tag_with(rt.tag_list, current_user)
|
||||
todo.tags.reload
|
||||
end
|
||||
|
||||
# increate number of occurences created from recurring todo
|
||||
rt.inc_occurences
|
||||
|
||||
# mark recurring todo complete if there are no next actions left
|
||||
checkdate = todo.due.nil? ? todo.show_from : todo.due
|
||||
rt.toggle_completion! unless rt.has_next_todo(checkdate)
|
||||
|
||||
return saved ? todo : nil
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
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
|
||||
@saved = @todo.toggle_completion!
|
||||
|
||||
# check if this todo has a related recurring_todo. If so, create next todo
|
||||
check_for_next_todo if @saved
|
||||
|
||||
respond_to do |format|
|
||||
format.js do
|
||||
if @saved
|
||||
|
|
@ -235,6 +239,10 @@ class TodosController < ApplicationController
|
|||
@todo = get_todo_from_params
|
||||
@context_id = @todo.context_id
|
||||
@project_id = @todo.project_id
|
||||
|
||||
# check if this todo has a related recurring_todo. If so, create next todo
|
||||
check_for_next_todo
|
||||
|
||||
@saved = @todo.destroy
|
||||
|
||||
respond_to do |format|
|
||||
|
|
@ -644,6 +652,19 @@ class TodosController < ApplicationController
|
|||
['rss','atom','txt','ics'].include?(req.parameters[:format])
|
||||
end
|
||||
|
||||
def check_for_next_todo
|
||||
# check if this todo has a related recurring_todo. If so, create next todo
|
||||
@new_recurring_todo = nil
|
||||
@recurring_todo = nil
|
||||
if @todo.from_recurring_todo?
|
||||
@recurring_todo = current_user.recurring_todos.find(@todo.recurring_todo_id)
|
||||
if @recurring_todo.active? && @recurring_todo.has_next_todo(@todo.due)
|
||||
date = @todo.due >= Date.today() ? @todo.due : Date.today()-1.day
|
||||
@new_recurring_todo = create_todo_from_recurring_todo(@recurring_todo, date)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class FindConditionBuilder
|
||||
|
||||
def initialize
|
||||
|
|
|
|||
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
|
||||
|
|
@ -66,7 +66,7 @@ module TodosHelper
|
|||
|
||||
def remote_edit_icon
|
||||
if !@todo.completed?
|
||||
str = link_to( image_tag_for_edit,
|
||||
str = link_to( image_tag_for_edit(@todo),
|
||||
edit_todo_path(@todo),
|
||||
:class => "icon edit_icon")
|
||||
set_behavior_for_edit_icon
|
||||
|
|
@ -205,12 +205,12 @@ module TodosHelper
|
|||
javascript_tag str
|
||||
end
|
||||
|
||||
def item_container_id
|
||||
def item_container_id (todo)
|
||||
if source_view_is :project
|
||||
return "p#{@todo.project_id}" if @todo.active?
|
||||
return "tickler" if @todo.deferred?
|
||||
return "p#{todo.project_id}" if todo.active?
|
||||
return "tickler" if todo.deferred?
|
||||
end
|
||||
return "c#{@todo.context_id}"
|
||||
return "c#{todo.context_id}"
|
||||
end
|
||||
|
||||
def should_show_new_item
|
||||
|
|
@ -272,8 +272,8 @@ module TodosHelper
|
|||
image_tag("blank.png", :title =>"Delete action", :class=>"delete_item")
|
||||
end
|
||||
|
||||
def image_tag_for_edit
|
||||
image_tag("blank.png", :title =>"Edit action", :class=>"edit_item", :id=> dom_id(@todo, 'edit_icon'))
|
||||
def image_tag_for_edit(todo)
|
||||
image_tag("blank.png", :title =>"Edit action", :class=>"edit_item", :id=> dom_id(todo, 'edit_icon'))
|
||||
end
|
||||
|
||||
def image_tag_for_star(todo)
|
||||
|
|
|
|||
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
|
||||
has_many_polymorphs :taggables,
|
||||
:from => [:todos],
|
||||
:from => [:todos, :recurring_todos],
|
||||
:through => :taggings,
|
||||
:dependent => :destroy
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ class Todo < ActiveRecord::Base
|
|||
belongs_to :context
|
||||
belongs_to :project
|
||||
belongs_to :user
|
||||
belongs_to :recurring_todo
|
||||
|
||||
STARRED_TAG_NAME = "starred"
|
||||
|
||||
|
|
@ -120,4 +121,8 @@ class Todo < ActiveRecord::Base
|
|||
starred?
|
||||
end
|
||||
|
||||
def from_recurring_todo?
|
||||
return self.recurring_todo_id != nil
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -63,6 +63,9 @@ class User < ActiveRecord::Base
|
|||
has_many :todos,
|
||||
:order => 'todos.completed_at DESC, todos.created_at DESC',
|
||||
:dependent => :delete_all
|
||||
has_many :recurring_todos,
|
||||
:order => 'recurring_todos.completed_at DESC, recurring_todos.created_at DESC',
|
||||
:dependent => :delete_all
|
||||
has_many :deferred_todos,
|
||||
:class_name => 'Todo',
|
||||
:conditions => [ 'state = ?', 'deferred' ],
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ window.onload=function(){
|
|||
Nifty("div#todo_new_action_container","normal");
|
||||
Nifty("div#project_new_project_container","normal");
|
||||
Nifty("div#context_new_container","normal");
|
||||
Nifty("div#recurring_new_container","normal");
|
||||
if ($('flash').visible()) { new Effect.Fade("flash",{duration:5.0}); }
|
||||
}
|
||||
</script>
|
||||
|
|
@ -56,6 +57,7 @@ window.onload=function(){
|
|||
<% if current_user.is_admin? -%>
|
||||
<li><%= navigation_link("Admin", users_path, {:accesskey => "a", :title => "Add or delete users"} ) %></li>
|
||||
<% end -%>
|
||||
<li><%= navigation_link(image_tag("recurring_menu16x16.png", :size => "16X16", :border => 0), {:controller => "recurring_todos", :action => "index"}, :title => "Manage recurring actions" ) %></li>
|
||||
<li><%= navigation_link(image_tag("feed-icon.png", :size => "16X16", :border => 0), {:controller => "feedlist", :action => "index"}, :title => "See a list of available feeds" ) %></li>
|
||||
<li><%= navigation_link(image_tag("menustar.gif", :size => "16X16", :border => 0), tag_path("starred"), :title => "See your starred actions" ) %></li>
|
||||
<li><%= navigation_link(image_tag("stats.gif", :size => "16X16", :border => 0), {:controller => "stats", :action => "index"}, :title => "See your statistics" ) %></li>
|
||||
|
|
|
|||
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 ) %>">
|
||||
<%= date_span -%>
|
||||
<span class="todo.descr"><%= h sanitize(todo.description) %></span>
|
||||
<%= link_to(image_tag("recurring16x16.png"), {:controller => "recurring_todos", :action => "index"}, :class => "recurring_icon") if @todo.from_recurring_todo? %>
|
||||
<%= tag_list %>
|
||||
<%= deferred_due_date %>
|
||||
<%= project_and_context_links( parent_container_type, :suppress_context => suppress_context, :suppress_project => suppress_project ) %>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ if @saved
|
|||
page.insert_html :top, 'display_box', :partial => 'contexts/context', :locals => { :context => @todo.context, :collapsible => true }
|
||||
else
|
||||
page.call "todoItems.ensureVisibleWithEffectAppear", "c#{@todo.context_id}" if source_view_is_one_of(:todo, :deferred)
|
||||
page.insert_html :bottom, item_container_id + 'items', :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type, :source_view => @source_view }
|
||||
page.insert_html :bottom, item_container_id(@todo) + 'items', :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type, :source_view => @source_view }
|
||||
page.visual_effect :highlight, dom_id(@todo), :duration => 3
|
||||
page[empty_container_msg_div_id].hide unless empty_container_msg_div_id.nil?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,9 +1,23 @@
|
|||
if @saved
|
||||
page[@todo].remove
|
||||
page['badge_count'].replace_html @down_count
|
||||
page.visual_effect :fade, item_container_id, :duration => 0.4 if source_view_is_one_of(:todo, :deferred) && @remaining_in_context == 0
|
||||
|
||||
# remove context if empty
|
||||
page.visual_effect :fade, item_container_id(@todo), :duration => 0.4 if source_view_is_one_of(:todo, :deferred) && @remaining_in_context == 0
|
||||
|
||||
# show message if there are no actions
|
||||
page[empty_container_msg_div_id].show if !empty_container_msg_div_id.nil? && @down_count == 0
|
||||
page['tickler-empty-nd'].show if source_view_is(:deferred) && @down_count == 0
|
||||
|
||||
# show new todo if the completed todo was recurring
|
||||
unless @new_recurring_todo.nil?
|
||||
page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id(@new_recurring_todo)
|
||||
page.insert_html :bottom, item_container_id(@new_recurring_todo), :partial => 'todos/todo', :locals => { :todo => @new_recurring_todo, :parent_container_type => parent_container_type }
|
||||
page.visual_effect :highlight, dom_id(@new_recurring_todo, 'line'), {'startcolor' => "'#99ff99'"}
|
||||
page.notify :notice, "Action was deleted. Because this action is recurring, a new action was added", 6.0
|
||||
else
|
||||
page.notify :notice, "There is no next action after the recurring action you just deleted. The recurrence is completed", 6.0 unless @recurring_todo.nil?
|
||||
end
|
||||
else
|
||||
page.notify :error, "There was an error deleting the item #{@todo.description}", 8.0
|
||||
end
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
if @saved
|
||||
page[@todo].remove
|
||||
if @todo.completed?
|
||||
# Don't try to insert contents into a non-existent container!
|
||||
# completed todos move from their context to the completed container
|
||||
|
||||
unless @prefs.hide_completed_actions?
|
||||
page.insert_html :top, "completed", :partial => 'todos/todo', :locals => { :parent_container_type => "completed" }
|
||||
page.visual_effect :highlight, dom_id(@todo, 'line'), {'startcolor' => "'#99ff99'"}
|
||||
|
|
@ -9,16 +10,30 @@ if @saved
|
|||
page.show 'tickler-empty-nd' if source_view_is(:project) && @deferred_count == 0
|
||||
page.hide 'empty-d' # If we've checked something as done, completed items can't be empty
|
||||
end
|
||||
|
||||
# remove container if empty
|
||||
if @remaining_in_context == 0 && source_view_is(:todo)
|
||||
page.visual_effect :fade, item_container_id, :duration => 0.4
|
||||
page.visual_effect :fade, item_container_id(@todo), :duration => 0.4
|
||||
end
|
||||
|
||||
# show new todo if the completed todo was recurring
|
||||
unless @new_recurring_todo.nil?
|
||||
page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id(@new_recurring_todo)
|
||||
page.insert_html :bottom, item_container_id(@new_recurring_todo), :partial => 'todos/todo', :locals => { :todo => @new_recurring_todo, :parent_container_type => parent_container_type }
|
||||
page.visual_effect :highlight, dom_id(@new_recurring_todo, 'line'), {'startcolor' => "'#99ff99'"}
|
||||
else
|
||||
page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id
|
||||
page.insert_html :bottom, item_container_id, :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
|
||||
page.notify :notice, "There is no next action after the recurring action you just finished. The recurrence is completed", 6.0 unless @recurring_todo.nil?
|
||||
end
|
||||
|
||||
else
|
||||
# todo is activated from completed container
|
||||
page.call "todoItems.ensureVisibleWithEffectAppear", item_container_id(@todo)
|
||||
page.insert_html :bottom, item_container_id(@todo), :partial => 'todos/todo', :locals => { :parent_container_type => parent_container_type }
|
||||
page.visual_effect :highlight, dom_id(@todo, 'line'), {'startcolor' => "'#99ff99'"}
|
||||
page.show "empty-d" if @completed_count == 0
|
||||
page[empty_container_msg_div_id].hide unless empty_container_msg_div_id.nil? # If we've checked something as undone, incomplete items can't be empty
|
||||
end
|
||||
|
||||
page.hide "status"
|
||||
page.replace_html "badge_count", @down_count
|
||||
if @todo.completed? && !@todo.project_id.nil? && @prefs.show_project_on_todo_done && !source_view_is(:project)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ if @saved
|
|||
status_message = 'Added new context / ' + status_message if @new_context_created
|
||||
page.notify :notice, status_message, 5.0
|
||||
|
||||
#update auto completer arrays for context and project
|
||||
# #update auto completer arrays for context and project
|
||||
page << "contextAutoCompleter.options.array = #{context_names_for_autocomplete}; contextAutoCompleter.changed = true" if @new_context_created
|
||||
page << "projectAutoCompleter.options.array = #{project_names_for_autocomplete}; projectAutoCompleter.changed = true" if @new_project_created
|
||||
if source_view_is_one_of(:todo, :context)
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ ActionController::Routing::Routes.draw do |map|
|
|||
map.preferences 'preferences', :controller => 'preferences', :action => 'index'
|
||||
map.integrations 'integrations', :controller => 'integrations', :action => 'index'
|
||||
|
||||
map.resources :recurring_todos,
|
||||
:member => {:toggle_check => :put, :toggle_star => :put}
|
||||
map.recurring_todos 'recurring_todos', :controller => 'recurring_todos', :action => 'index'
|
||||
|
||||
# Install the default route as the lowest priority.
|
||||
map.connect ':controller/:action/:id'
|
||||
|
||||
|
|
|
|||
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 |
|
|
@ -32,8 +32,59 @@ var TracksForm = {
|
|||
Form.focusFirstElement(formId);
|
||||
}
|
||||
toggleDiv.toggleClassName('hide_form');
|
||||
},
|
||||
get_period: function() {
|
||||
if ($('recurring_todo_recurring_period_daily').checked) {
|
||||
return 'daily';
|
||||
}
|
||||
else if ($('recurring_todo_recurring_period_weekly').checked) {
|
||||
return 'weekly';
|
||||
}
|
||||
else if ($('recurring_todo_recurring_period_monthly').checked) {
|
||||
return 'monthly';
|
||||
}
|
||||
else if ($('recurring_todo_recurring_period_yearly').checked) {
|
||||
return 'yearly';
|
||||
}
|
||||
else {
|
||||
return 'no period'
|
||||
}
|
||||
},
|
||||
get_edit_period: function() {
|
||||
if ($('recurring_edit_todo_recurring_period_daily').checked) {
|
||||
return 'daily';
|
||||
}
|
||||
else if ($('recurring_edit_todo_recurring_period_weekly').checked) {
|
||||
return 'weekly';
|
||||
}
|
||||
else if ($('recurring_edit_todo_recurring_period_monthly').checked) {
|
||||
return 'monthly';
|
||||
}
|
||||
else if ($('recurring_edit_todo_recurring_period_yearly').checked) {
|
||||
return 'yearly';
|
||||
}
|
||||
else {
|
||||
return 'no period'
|
||||
}
|
||||
},
|
||||
hide_all_recurring: function () {
|
||||
$('recurring_daily').hide();
|
||||
$('recurring_weekly').hide();
|
||||
$('recurring_monthly').hide();
|
||||
$('recurring_yearly').hide();
|
||||
},
|
||||
hide_all_edit_recurring: function () {
|
||||
$('recurring_edit_daily').hide();
|
||||
$('recurring_edit_weekly').hide();
|
||||
$('recurring_edit_monthly').hide();
|
||||
$('recurring_edit_yearly').hide();
|
||||
},
|
||||
toggle_overlay: function () {
|
||||
el = document.getElementById("overlay");
|
||||
el.style.visibility = (el.style.visibility == "visible") ? "hidden" : "visible";
|
||||
}
|
||||
}
|
||||
|
||||
// uncomment the next four lines for easier debugging with FireBug
|
||||
// Ajax.Responders.register({
|
||||
// onException: function(source, exception) {
|
||||
|
|
|
|||
|
|
@ -131,6 +131,82 @@ a.show_notes:hover, a.link_to_notes:hover {background-image: url(../images/notes
|
|||
margin: 0px 15px 90px 15px;
|
||||
}
|
||||
|
||||
#recurring_timespan, #recurring_target {
|
||||
border: none;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#recurring_target {
|
||||
border-top-style: dotted;
|
||||
padding: 15px 0px 0px 0px;
|
||||
}
|
||||
|
||||
#recurring_daily, #recurring_weekly, #recurring_monthly, #recurring_yearly,
|
||||
#recurring_edit_daily, #recurring_edit_weekly, #recurring_edit_monthly, #recurring_edit_yearly {
|
||||
border: none;
|
||||
border-top-style: dotted;
|
||||
clear: both;
|
||||
padding: 15px 0px 15px 0px;
|
||||
}
|
||||
|
||||
#recurring_period_id, #recurring_edit_period_id {
|
||||
border: none;
|
||||
float: left;
|
||||
margin: 0px 50px 0px 0px;
|
||||
padding: 0px 25px 15px 50px;
|
||||
border-right-style: none;
|
||||
}
|
||||
|
||||
#recurring_todo {
|
||||
width: 270px;
|
||||
}
|
||||
|
||||
#recurring_todo_form_container {
|
||||
border-right-style: dotted;
|
||||
padding: 0px 50px 15px 0px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#recurring_todo input, #recurring_todo textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#overlay {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 102;
|
||||
text-align: center;
|
||||
background-image:url("../images/trans70.png");
|
||||
}
|
||||
|
||||
#overlay #new-recurring-todo, #overlay #edit-recurring-todo {
|
||||
width:750px;
|
||||
background-color: #fff;
|
||||
border:1px solid #000;
|
||||
padding: 15px;
|
||||
margin: 70px auto;
|
||||
}
|
||||
|
||||
.recurring_container {
|
||||
padding: 0px 5px 0px 5px;
|
||||
border: 1px solid #999;
|
||||
margin: 0px 0px 0px 0px;
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.recurring_submit_box {
|
||||
height: 25px;
|
||||
padding: 5px 0;
|
||||
text-align: center;
|
||||
clear: both;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Navigation links at the top */
|
||||
|
||||
#navcontainer {
|
||||
|
|
@ -183,7 +259,7 @@ a.show_notes:hover, a.link_to_notes:hover {background-image: url(../images/notes
|
|||
opacity: .75;
|
||||
color: #eee;
|
||||
width: 100%;
|
||||
z-index:1100;
|
||||
z-index:101;
|
||||
}
|
||||
body.stats #topbar {
|
||||
filter: alpha(opacity=100);
|
||||
|
|
@ -277,6 +353,11 @@ div.item-container {
|
|||
clear: both;
|
||||
}
|
||||
|
||||
a.recurring_icon {
|
||||
vertical-align: middle;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
a.icon {
|
||||
float: left;
|
||||
vertical-align: middle;
|
||||
|
|
@ -673,7 +754,7 @@ form {
|
|||
.inline-form table td.label {
|
||||
width: 13ex;
|
||||
}
|
||||
#todo_new_action_container, #project_new_project_container, #context_new_container {
|
||||
#todo_new_action_container, #project_new_project_container, #context_new_container, #recurring_new_container {
|
||||
background: #ddd;
|
||||
width: 270px;
|
||||
padding: 5px 10px;
|
||||
|
|
@ -684,6 +765,10 @@ form {
|
|||
color: #eee;
|
||||
}
|
||||
|
||||
#recurring_new_container img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#project_new_project_filler {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
|
|
|||
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