Merge branch 'master' into new-gui

Conflicts:
	Gemfile.lock
This commit is contained in:
Dan Rice 2014-04-02 13:26:17 -04:00
commit fdd4c267b3
39 changed files with 2824 additions and 1022 deletions

View file

@ -0,0 +1,53 @@
module RecurringTodos
class FormHelper
def initialize(recurring_todo)
@recurring_todo = recurring_todo
@method_map = {
# delegate daily_xxx to daily_pattern.xxx
"daily" => {prefix: "", method: daily_pattern},
"weekly" => {prefix: "", method: weekly_pattern},
"monthly" => {prefix: "", method: monthly_pattern},
"yearly" => {prefix: "", method: yearly_pattern},
# delegate on_xxx to weekly_pattern.on_xxx
"on" => {prefix: "on_", method: weekly_pattern}
}
end
def create_pattern(pattern_class)
pattern = pattern_class.new(@recurring_todo.user)
pattern.build_from_recurring_todo(@recurring_todo)
pattern
end
def daily_pattern
@daily_pattern ||= create_pattern(DailyRepeatPattern)
end
def weekly_pattern
@weekly_pattern ||= create_pattern(WeeklyRepeatPattern)
end
def monthly_pattern
@monthly_pattern ||= create_pattern(MonthlyRepeatPattern)
end
def yearly_pattern
@yearly_pattern ||= create_pattern(YearlyRepeatPattern)
end
def method_missing(method, *args)
# delegate daily_xxx to daily_pattern, weekly_xxx to weekly_pattern, etc.
if method.to_s =~ /^([^_]+)_(.+)$/
return @method_map[$1][:method].send(@method_map[$1][:prefix]+$2, *args) unless @method_map[$1].nil?
end
# no match, let @recurring_todo handle it, or fail
@recurring_todo.send(method, *args)
end
end
end

View file

@ -37,68 +37,18 @@ class RecurringTodosController < ApplicationController
end
def edit
@form_helper = RecurringTodos::FormHelper.new(@recurring_todo)
respond_to do |format|
format.js
end
end
def update
# TODO: write tests for updating
@recurring_todo.tag_with(params[:edit_recurring_todo_tag_list]) if params[:edit_recurring_todo_tag_list]
@original_item_context_id = @recurring_todo.context_id
@original_item_project_id = @recurring_todo.project_id
updater = RecurringTodos::RecurringTodosBuilder.new(current_user, update_recurring_todo_params)
@saved = updater.update(@recurring_todo)
# 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.
# Same goes for start_from and end_date
params['recurring_todo']['recurring_period']=params['recurring_edit_todo']['recurring_period']
params['recurring_todo']['end_date']=parse_date_per_user_prefs(params['recurring_todo_edit_end_date'])
params['recurring_todo']['start_from']=parse_date_per_user_prefs(params['recurring_todo_edit_start_from'])
# 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.where(: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'].present?
context = current_user.contexts.where(:name => params['context_name'].strip).first
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
# make sure that we set weekly_return_xxx to empty (space) when they are
# not checked (and thus not present in params["recurring_todo"])
%w{monday tuesday wednesday thursday friday saturday sunday}.each do |day|
params["recurring_todo"]["weekly_return_"+day]=' ' if params["recurring_todo"]["weekly_return_"+day].nil?
end
selector_attributes = {
'recurring_period' => recurring_todo_params['recurring_period'],
'daily_selector' => recurring_todo_params['daily_selector'],
'monthly_selector' => recurring_todo_params['monthly_selector'],
'yearly_selector' => recurring_todo_params['yearly_selector']
}
@recurring_todo.assign_attributes(:recurring_period => recurring_todo_params[:recurring_period])
@recurring_todo.assign_attributes(selector_attributes)
@saved = @recurring_todo.update_attributes recurring_todo_params
@recurring_todo.reload
respond_to do |format|
format.js
@ -106,42 +56,17 @@ class RecurringTodosController < ApplicationController
end
def create
p = RecurringTodoCreateParamsHelper.new(params, recurring_todo_params)
p.attributes['end_date']=parse_date_per_user_prefs(p.attributes['end_date'])
p.attributes['start_from']=parse_date_per_user_prefs(p.attributes['start_from'])
# make sure we set :recurring_period first, since other setters depend on it being set
# TODO: move logic into model
@recurring_todo = current_user.recurring_todos.build(:recurring_period => params[:recurring_period])
@recurring_todo.assign_attributes(p.selector_attributes)
@recurring_todo.update_attributes(p.attributes)
if p.project_specified_by_name?
project = current_user.projects.where(:name => p.project_name).first_or_create
@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.where(:name => p.context_name).first_or_create
@new_context_created = context.new_record_before_save?
@recurring_todo.context_id = context.id
end
@saved = @recurring_todo.save
if @saved && p.tag_list.present?
@recurring_todo.tag_with(p.tag_list)
@recurring_todo.tags.reload
end
builder = RecurringTodos::RecurringTodosBuilder.new(current_user, all_recurring_todo_params)
@saved = builder.save
if @saved
@status_message = t('todos.recurring_action_saved')
@todo_saved = TodoFromRecurringTodo.new(current_user, @recurring_todo).create.nil? == false
if @todo_saved
@status_message += " / " + t('todos.new_related_todo_created_short')
else
@status_message += " / " + t('todos.new_related_todo_not_created_short')
end
@recurring_todo = builder.saved_recurring_todo
todo_saved = TodoFromRecurringTodo.new(current_user, @recurring_todo).create.nil? == false
@status_message =
t('todos.recurring_action_saved') + " / " +
t("todos.new_related_todo_#{todo_saved ? "" : "not_"}created_short")
@down_count = current_user.recurring_todos.active.count
@new_recurring_todo = RecurringTodo.new
else
@ -154,13 +79,10 @@ class RecurringTodosController < ApplicationController
end
def destroy
@number_of_todos = @recurring_todo.todos.count
# remove all references to this recurring todo
@todos = @recurring_todo.todos
@number_of_todos = @todos.size
@todos.each do |t|
t.recurring_todo_id = nil
t.save
end
@recurring_todo.clear_todos_association
# delete the recurring todo
@saved = @recurring_todo.destroy
@ -174,11 +96,10 @@ class RecurringTodosController < ApplicationController
format.html do
if @saved
notify :notice, t('todos.recurring_deleted_success')
redirect_to :action => 'index'
else
notify :error, t('todos.error_deleting_recurring', :description => @recurring_todo.description)
redirect_to :action => 'index'
notify :error, t('todos.error_deleting_recurring', :description => @recurring_todo.description)
end
redirect_to :action => 'index'
end
format.js do
@ -217,58 +138,6 @@ class RecurringTodosController < ApplicationController
end
end
class RecurringTodoCreateParamsHelper
def initialize(params, recurring_todo_params)
@params = params['request'] || params
@attributes = recurring_todo_params
# make sure all selectors (recurring_period, recurrence_selector,
# daily_selector, monthly_selector and yearly_selector) are first in hash
# so that they are processed first by the model
@selector_attributes = {
'recurring_period' => @attributes['recurring_period'],
'daily_selector' => @attributes['daily_selector'],
'monthly_selector' => @attributes['monthly_selector'],
'yearly_selector' => @attributes['yearly_selector']
}
end
def attributes
@attributes
end
def selector_attributes
return @selector_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 if @attributes['project_id'].present?
return false if project_name.blank?
return false if project_name == 'None'
true
end
def context_specified_by_name?
return false if @attributes['context_id'].present?
return false if context_name.blank?
true
end
end
private
def recurring_todo_params
@ -278,7 +147,7 @@ class RecurringTodosController < ApplicationController
:ends_on, :end_date, :number_of_occurences, :occurences_count, :target,
:show_from_delta, :recurring_period, :recurrence_selector, :every_other1,
:every_other2, :every_other3, :every_day, :only_work_days, :every_count,
:weekday, :show_always,
:weekday, :show_always, :context_name, :project_name, :tag_list,
# form attributes
:recurring_period, :daily_selector, :monthly_selector, :yearly_selector,
:recurring_target, :daily_every_x_days, :monthly_day_of_week,
@ -293,17 +162,50 @@ class RecurringTodosController < ApplicationController
)
end
def all_recurring_todo_params
# move context_name, project_name and tag_list into :recurring_todo hash for easier processing
{
context_name: :context_name,
project_name: :project_name,
tag_list: :tag_list
}.each do |target,source|
move_into_recurring_todo_param(params, target, source)
end
recurring_todo_params
end
def update_recurring_todo_params
# 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.
# Same goes for start_from and end_date
params['recurring_todo']['recurring_period'] = params['recurring_edit_todo']['recurring_period']
{
context_name: :context_name,
project_name: :project_name,
tag_list: :edit_recurring_todo_tag_list,
end_date: :recurring_todo_edit_end_date,
start_from: :recurring_todo_edit_start_from
}.each do |target,source|
move_into_recurring_todo_param(params, target, source)
end
# make sure that we set weekly_return_xxx to empty (space) when they are
# not checked (and thus not present in params["recurring_todo"])
%w{monday tuesday wednesday thursday friday saturday sunday}.each do |day|
params["recurring_todo"]["weekly_return_#{day}"]=' ' if params["recurring_todo"]["weekly_return_#{day}"].nil?
end
recurring_todo_params
end
def move_into_recurring_todo_param(params, target, source)
params[:recurring_todo][target] = params[source] unless params[source].blank?
end
def init
@days_of_week = []
0.upto 6 do |i|
@days_of_week << [t('date.day_names')[i], i]
end
@months_of_year = []
1.upto 12 do |i|
@months_of_year << [t('date.month_names')[i], i]
end
@days_of_week = (0..6).map{|i| [t('date.day_names')[i], i] }
@months_of_year = (1..12).map{|i| [t('date.month_names')[i], i] }
@xth_day = [[t('common.first'),1],[t('common.second'),2],[t('common.third'),3],[t('common.fourth'),4],[t('common.last'),5]]
@projects = current_user.projects.includes(:default_context)
@contexts = current_user.contexts

View file

@ -136,7 +136,7 @@ class Project < ActiveRecord::Base
end
def age_in_days
@age_in_days ||= ((Time.now.utc - created_at).to_i / 1.day) + 1
@age_in_days ||= (Time.current.to_date - created_at.to_date).to_i + 1
end
def self.import(filename, params, user)

View file

@ -29,86 +29,23 @@ class RecurringTodo < ActiveRecord::Base
validates_length_of :description, :maximum => 100
validates_length_of :notes, :maximum => 60000, :allow_nil => true
validate :period_specific_validations
validate :starts_and_ends_on_validations
validate :set_recurrence_on_validations
validate :period_validation
validate :pattern_specific_validations
def period_specific_validations
if %W[daily weekly monthly yearly].include?(recurring_period)
self.send("validate_#{recurring_period}")
def pattern_specific_validations
if pattern
pattern.validate
else
errors.add(:recurring_period, "is an unknown recurrence pattern: '#{recurring_period}'")
errors[:recurring_todo] << "Invalid recurrence period '#{recurring_period}'"
end
end
def validate_daily
if (!only_work_days) && daily_every_x_days.blank?
errors[:base] << "Every other nth day may not be empty for recurrence setting"
end
def valid_period?
%W[daily weekly monthly yearly].include?(recurring_period)
end
def validate_weekly
if weekly_every_x_week.blank?
errors[:base] << "Every other nth week may not be empty for recurrence setting"
end
something_set = false
%w{sunday monday tuesday wednesday thursday friday saturday}.each { |day| something_set ||= self.send("on_#{day}") }
errors[:base] << "You must specify at least one day on which the todo recurs" unless something_set
end
def validate_monthly
case recurrence_selector
when 0 # 'monthly_every_x_day'
errors[:base] << "The day of the month may not be empty for recurrence setting" if monthly_every_x_day.blank?
errors[:base] << "Every other nth month may not be empty for recurrence setting" if monthly_every_x_month.blank?
when 1 # 'monthly_every_xth_day'
errors[:base] <<"Every other nth month may not be empty for recurrence setting" if monthly_every_x_month2.blank?
errors[:base] <<"The nth day of the month may not be empty for recurrence setting" if monthly_every_xth_day.blank?
errors[:base] <<"The day of the month may not be empty for recurrence setting" if monthly_day_of_week.blank?
else
raise Exception.new, "unexpected value of recurrence selector '#{recurrence_selector}'"
end
end
def validate_yearly
case recurrence_selector
when 0 # 'yearly_every_x_day'
errors[:base] << "The month of the year may not be empty for recurrence setting" if yearly_month_of_year.blank?
errors[:base] << "The day of the month may not be empty for recurrence setting" if yearly_every_x_day.blank?
when 1 # 'yearly_every_xth_day'
errors[:base] << "The month of the year may not be empty for recurrence setting" if yearly_month_of_year2.blank?
errors[:base] << "The nth day of the month may not be empty for recurrence setting" if yearly_every_xth_day.blank?
errors[:base] << "The day of the week may not be empty for recurrence setting" if yearly_day_of_week.blank?
else
raise Exception.new, "unexpected value of recurrence selector '#{recurrence_selector}'"
end
end
def starts_and_ends_on_validations
errors[:base] << "The start date needs to be filled in" if start_from.blank?
case ends_on
when 'ends_on_number_of_times'
errors[:base] << "The number of recurrences needs to be filled in for 'Ends on'" if number_of_occurences.blank?
when "ends_on_end_date"
errors[:base] << "The end date needs to be filled in for 'Ends on'" if end_date.blank?
else
errors[:base] << "The end of the recurrence is not selected" unless ends_on == "no_end_date"
end
end
def set_recurrence_on_validations
# show always or x days before due date. x not null
case target
when 'show_from_date'
# no validations
when 'due_date'
errors[:base] << "Please select when to show the action" if show_always.nil?
unless show_always
errors[:base] << "Please fill in the number of days to show the todo before the due date" if show_from_delta.blank?
end
else
raise Exception.new, "unexpected value of recurrence target selector '#{recurrence_target}'"
end
def period_validation
errors.add(:recurring_period, "is an unknown recurrence pattern: '#{recurring_period}'") unless valid_period?
end
# the following recurrence patterns can be stored:
@ -133,262 +70,20 @@ class RecurringTodo < ActiveRecord::Base
# 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'
only_work_days = false
when 'daily_every_work_day'
only_work_days = true
else
raise Exception.new, "unknown daily recurrence pattern: '#{selector}'"
end
end
def daily_every_x_days=(x)
self.every_other1 = x if recurring_period=='daily'
end
def daily_every_x_days
every_other1
end
# WEEKLY
def weekly_every_x_week=(x)
self.every_other1 = x if recurring_period=='weekly'
end
def weekly_every_x_week
self.every_other1
end
def switch_week_day(day, position)
self.every_day = ' ' if self.every_day.nil?
self.every_day = every_day[0, position] + day + every_day[position+1, every_day.length]
end
{ monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 0 }.each do |day, number|
define_method("weekly_return_#{day}=") do |selector|
switch_week_day(selector, number) if recurring_period=='weekly'
end
define_method("on_#{day}") do
on_xday number
end
end
def on_xday(n)
every_day && every_day[n, 1] != ' '
end
# MONTHLY
def monthly_selector=(selector)
self.recurrence_selector = ( (selector=='monthly_every_x_day') ? 0 : 1) if recurring_period=='monthly'
end
def monthly_every_x_day=(x)
self.every_other1 = x if recurring_period=='monthly'
end
def monthly_every_x_day
self.every_other1
end
def is_monthly_every_x_day
recurring_period == 'monthly' && self.recurrence_selector == 0
end
def is_monthly_every_xth_day
recurring_period == 'monthly' && self.recurrence_selector == 1
end
def monthly_every_x_month=(x)
self.every_other2 = x if recurring_period=='monthly' && recurrence_selector == 0
end
def monthly_every_x_month
# in case monthly pattern is every day x, return every_other2 otherwise
# return a default value
self.recurrence_selector == 0 ? self.every_other2 : 1
end
def monthly_every_x_month2=(x)
self.every_other2 = x if recurring_period=='monthly' && recurrence_selector == 1
end
def monthly_every_x_month2
# in case monthly pattern is every xth day, return every_other2 otherwise
# return a default value
self.recurrence_selector == 1 ? self.every_other2 : 1
end
def monthly_every_xth_day=(x)
self.every_other3 = x if recurring_period=='monthly'
end
def monthly_every_xth_day(default=nil)
self.every_other3 || default
end
def monthly_day_of_week=(dow)
self.every_count = dow if recurring_period=='monthly'
end
def monthly_day_of_week
self.every_count
end
# YEARLY
def yearly_selector=(selector)
self.recurrence_selector = ( (selector=='yearly_every_x_day') ? 0 : 1) if recurring_period=='yearly'
end
def yearly_month_of_year=(moy)
self.every_other2 = moy if self.recurring_period=='yearly' && self.recurrence_selector == 0
end
def yearly_month_of_year
# if recurrence pattern is every x day in a month, return month otherwise
# return a default value
self.recurrence_selector == 0 ? self.every_other2 : Time.zone.now.month
end
def yearly_month_of_year2=(moy)
self.every_other2 = moy if self.recurring_period=='yearly' && self.recurrence_selector == 1
end
def yearly_month_of_year2
# if recurrence pattern is every xth day in a month, return month otherwise
# return a default value
self.recurrence_selector == 1 ? self.every_other2 : Time.zone.now.month
end
def yearly_every_x_day=(x)
self.every_other1 = x if recurring_period=='yearly'
end
def yearly_every_x_day
self.every_other1
end
def yearly_every_xth_day=(x)
self.every_other3 = x if recurring_period=='yearly'
end
def yearly_every_xth_day
self.every_other3
end
def yearly_day_of_week=(dow)
self.every_count=dow if recurring_period=='yearly'
end
def yearly_day_of_week
self.every_count
end
# target
def recurring_target=(t)
self.target = t
end
def recurring_target_as_text
case self.target
when 'due_date'
I18n.t("todos.recurrence.pattern.due")
when 'show_from_date'
I18n.t("todos.recurrence.pattern.show")
else
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
end
end
def recurring_show_days_before=(days)
self.show_from_delta=days
end
def recurring_show_always=(value)
self.show_always=value
end
def daily_recurrence_pattern
if only_work_days
I18n.t("todos.recurrence.pattern.on_work_days")
elsif every_other1 > 1
I18n.t("todos.recurrence.pattern.every_n", :n => every_other1) + " " + I18n.t("common.days_midsentence.other")
else
I18n.t("todos.recurrence.pattern.every_day")
end
end
def weekly_recurrence_pattern
if every_other1 > 1
I18n.t("todos.recurrence.pattern.every_n", :n => every_other1) + " " + I18n.t("common.weeks")
else
I18n.t('todos.recurrence.pattern.weekly')
end
end
def monthly_recurrence_pattern
return "invalid repeat pattern" if every_other2.nil?
if self.recurrence_selector == 0
on_day = " #{I18n.t('todos.recurrence.pattern.on_day_n', :n => self.every_other1)}"
if self.every_other2>1
I18n.t("todos.recurrence.pattern.every_n", :n => self.every_other2) + " " + I18n.t('common.months') + on_day
else
I18n.t("todos.recurrence.pattern.every_month") + on_day
end
else
n_months = if self.every_other2 > 1
"#{self.every_other2} #{I18n.t('common.months')}"
else
I18n.t('common.month')
end
I18n.t('todos.recurrence.pattern.every_xth_day_of_every_n_months',
:x => self.xth, :day => self.day_of_week, :n_months => n_months)
end
end
def yearly_recurrence_pattern
if self.recurrence_selector == 0
I18n.t("todos.recurrence.pattern.every_year_on",
:date => I18n.l(DateTime.new(Time.zone.now.year, self.every_other2, self.every_other1), :format => :month_day))
else
I18n.t("todos.recurrence.pattern.every_year_on",
:date => I18n.t("todos.recurrence.pattern.the_xth_day_of_month", :x => self.xth, :day => self.day_of_week, :month => self.month_of_year))
def pattern
if valid_period?
@pattern = eval("RecurringTodos::#{recurring_period.capitalize}RepeatPattern.new(user)")
@pattern.build_from_recurring_todo(self)
end
@pattern
end
def recurrence_pattern
return "invalid repeat pattern" if every_other1.nil?
case recurring_period
when 'daily' then daily_recurrence_pattern
when 'weekly' then weekly_recurrence_pattern
when 'monthly' then monthly_recurrence_pattern
when 'yearly' then yearly_recurrence_pattern
else
'unknown recurrence pattern: period unknown'
end
pattern.recurrence_pattern
end
def xth
xth_day = [
I18n.t('todos.recurrence.pattern.first'),I18n.t('todos.recurrence.pattern.second'),I18n.t('todos.recurrence.pattern.third'),
I18n.t('todos.recurrence.pattern.fourth'),I18n.t('todos.recurrence.pattern.last')]
self.every_other3.nil? ? '??' : xth_day[self.every_other3-1]
end
def day_of_week
self.every_count.nil? ? '??' : I18n.t('todos.recurrence.pattern.day_names')[self.every_count]
end
def month_of_year
self.every_other2.nil? ? '??' : I18n.t('todos.recurrence.pattern.month_names')[self.every_other2]
def recurring_target_as_text
pattern.recurring_target_as_text
end
def starred?
@ -396,214 +91,11 @@ class RecurringTodo < ActiveRecord::Base
end
def get_due_date(previous)
case self.target
when 'due_date'
get_next_date(previous)
when 'show_from_date'
# so leave due date empty
nil
else
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
end
pattern.get_due_date(previous)
end
def get_show_from_date(previous)
case self.target
when 'due_date'
# so set show from date relative to due date unless show_always is true or show_from_delta is nil
(self.show_always? || self.show_from_delta.nil?) ? nil : get_due_date(previous) - self.show_from_delta.days
when 'show_from_date'
# Leave due date empty
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' then get_daily_date(previous)
when 'weekly' then get_weekly_date(previous)
when 'monthly' then get_monthly_date(previous)
when 'yearly' then 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'
start = determine_start(previous, 1.day)
if self.only_work_days
return start + 2.day if start.wday() == 6 # saturday
return start + 1.day if start.wday() == 0 # sunday
return start
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)
# determine start
if previous == nil
start = self.start_from.nil? ? Time.zone.now : 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. Note that we already went into the next week, so -1
start += (self.every_other1-1).week
end
unless self.start_from.nil?
# check if the start_from date is later than previous. If so, use
# start_from as start to search for next date
start = self.start_from if self.start_from > previous
end
end
day = find_first_day_in_this_week(start)
return day unless day == -1
# we did not find anything this week, so check the nth next, starting from
# sunday
start = start + self.every_other1.week - (start.wday()).days
start = find_first_day_in_this_week(start)
return start unless start == -1
raise Exception.new, "unable to find next weekly date (#{self.every_day})"
end
def get_monthly_date(previous)
start = determine_start(previous)
day = self.every_other1
n = self.every_other2
case self.recurrence_selector
when 0 # specific day of the month
if (previous && start.mday >= day) || (previous.nil? && start.mday > day)
# there is no next day n in this month, search in next month
#
# start += n.months
#
# The above seems to not work. Fiddle with timezone. Looks like we hit a
# bug in rails here where 2008-12-01 +0100 plus 1.month becomes
# 2008-12-31 +0100. For now, just calculate in UTC and convert back to
# local timezone.
#
# TODO: recheck if future rails versions have this problem too
start = Time.utc(start.year, start.month, start.day)+n.months
start = Time.zone.local(start.year, start.month, start.day)
# go back to day
end
Time.zone.local(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
# fiddle with timezone. Looks like we hit a bug in rails here where
# 2008-12-01 +0100 plus 1.month becomes 2008-12-31 +0100. For now, just
# calculate in UTC and convert back to local timezone.
# TODO: recheck if future rails versions have this problem too
the_next = Time.utc(the_next.year, the_next.month, the_next.day)+n.months
the_next = Time.zone.local(the_next.year, the_next.month, the_next.day)
# 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
the_next
else
raise Exception.new, "unknown monthly recurrence selection (#{self.recurrence_selector})"
end
end
def get_xth_day_of_month(x, weekday, month, year)
if x == 5
# last -> count backwards. use UTC to avoid strange timezone oddities
# where last_day -= 1.day seems to shift tz+0100 to tz+0000
last_day = Time.utc(year, month, Time.days_in_month(month))
while last_day.wday != weekday
last_day -= 1.day
end
# convert back to local timezone
Time.zone.local(last_day.year, last_day.month, last_day.day)
else
# 1-4th -> count upwards last -> count backwards. use UTC to avoid strange
# timezone oddities where last_day -= 1.day seems to shift tz+0100 to
# tz+0000
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
# convert back to local timezone
Time.zone.local(start.year, start.month, start.day)
end
end
def get_yearly_date(previous)
start = determine_start(previous)
day = self.every_other1
month = self.every_other2
case self.recurrence_selector
when 0 # specific day of a specific month
if start.month > month || (start.month == month && start.day >= day)
# if there is no next month n and day m in this year, search in next
# year
start = Time.zone.local(start.year+1, month, 1)
else
# if there is a next month n, stay in this year
start = Time.zone.local(start.year, month, 1)
end
Time.zone.local(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.zone.local(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
the_next
else
raise Exception.new, "unknown monthly recurrence selection (#{self.recurrence_selector})"
end
end
def continues_recurring?(previous)
return self.occurences_count < self.number_of_occurences unless self.number_of_occurences.nil?
return true if self.end_date.nil? || self.ends_on == 'no_end_date'
case self.target
when 'due_date'
get_due_date(previous) <= self.end_date
when 'show_from_date'
get_show_from_date(previous) <= self.end_date
else
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
end
pattern.get_show_from_date(previous)
end
def done?(end_date)
@ -627,7 +119,7 @@ class RecurringTodo < ActiveRecord::Base
def remove_from_project!
self.project = nil
self.save
end
end
def clear_todos_association
unless todos.nil?
@ -643,29 +135,8 @@ class RecurringTodo < ActiveRecord::Base
self.save
end
protected
# Determine start date to calculate next date for recurring todo
# offset needs to be 1.day for daily patterns
def determine_start(previous, offset=0.day)
start = self.start_from || NullTime.new
now = Time.zone.now
if previous
# check if the start_from date is later than previous. If so, use
# start_from as start to search for next date
start > previous ? start : previous + offset
else
# skip to present
start > now ? start : now
end
def continues_recurring?(previous)
pattern.continues_recurring?(previous)
end
def find_first_day_in_this_week(start)
# 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
-1
end
end
end

View file

@ -0,0 +1,152 @@
module RecurringTodos
class AbstractRecurringTodosBuilder
attr_reader :mapped_attributes, :pattern
def initialize(user, attributes, pattern_class)
@user = user
@saved = false
@attributes = attributes
@selector = get_selector(selector_key)
@filterred_attributes = filter_attributes(@attributes)
@mapped_attributes = map_attributes(@filterred_attributes)
@pattern = pattern_class.new(user)
@pattern.attributes = @mapped_attributes
end
# build does not add tags. For tags, the recurring todos needs to be saved
def build
@recurring_todo = @pattern.build_recurring_todo(@mapped_attributes)
end
def update(recurring_todo)
@recurring_todo = @pattern.update_recurring_todo(recurring_todo, @mapped_attributes)
save_recurring_todo
end
def save
build
save_recurring_todo
end
def save_project
save_collection(:project, :project_id)
end
def save_context
save_collection(:context, :context_id)
end
def saved_recurring_todo
raise(Exception.new, @recurring_todo.valid? ? "Recurring todo was not saved yet" : "Recurring todos was not saved because of validation errors") unless @saved
@recurring_todo
end
def attributes
@pattern.attributes
end
def errors
@recurring_todo.try(:errors)
end
def attributes_to_filter
raise Exception.new, "attributes_to_filter should be overridden"
end
def filter_attributes(attributes)
# get pattern independend attributes
filterred_attributes = filter_generic_attributes(attributes)
# append pattern specific attributes
attributes_to_filter.each{|key| filterred_attributes[key]= attributes[key] if attributes.key?(key)}
filterred_attributes
end
def filter_generic_attributes(attributes)
return Tracks::AttributeHandler.new(@user, {
recurring_period: attributes[:recurring_period],
description: attributes[:description],
notes: attributes[:notes],
tag_list: tag_list_or_empty_string(attributes),
start_from: attributes[:start_from],
end_date: attributes[:end_date],
ends_on: attributes[:ends_on],
target: attributes[:target],
project: attributes[:project],
context: attributes[:context],
project_id: attributes[:project_id],
context_id: attributes[:context_id],
target: attributes[:recurring_target],
show_from_delta: attributes[:recurring_show_days_before],
show_always: attributes[:recurring_show_always]
})
end
def map_attributes
# should be overwritten by subclasses to map attributes to activerecord model attributes
@filterred_attributes
end
# helper method to be used in mapped_attributes in subclasses
# changes name of key from source_key to key
def map(mapping, key, source_key)
mapping[key] = mapping[source_key]
mapping.except(source_key)
end
# should return period specific selector like yearly_selector or daily_selector
def selector_key
raise Exception.new, "selector_key should be overridden in subclass of AbstractRecurringTodosBuilder"
end
def get_selector(key)
return nil if key.nil?
raise Exception.new, "recurrence selector pattern (#{key}) not given" unless @attributes.selector_key_present?(key)
selector = @attributes[key]
raise Exception.new, "unknown recurrence selector pattern: '#{selector}'" unless valid_selector?(selector)
@attributes = @attributes.except(key)
return selector
end
def valid_selector?(selector)
raise Exception.new, "valid_selector? should be overridden in subclass of AbstractRecurringTodosBuilder"
end
private
def save_recurring_todo
@saved = @recurring_todo.save
save_tags if @saved
return @saved
end
def save_tags
@recurring_todo.tag_with(@filterred_attributes[:tag_list]) if @filterred_attributes[:tag_list].present?
@recurring_todo.reload
end
def save_collection(collection, collection_id)
# save object (project or context) and add its id to @mapped_attributes and remove the object from the attributes
object = @mapped_attributes[collection]
object.save
@mapped_attributes[collection_id] = object.id
@mapped_attributes.except(collection)
end
def tag_list_or_empty_string(attributes)
# avoid nil
attributes[:tag_list].blank? ? "" : attributes[:tag_list].strip
end
end
end

View file

@ -0,0 +1,226 @@
module RecurringTodos
class AbstractRepeatPattern
attr_accessor :attributes
def initialize(user)
@user = user
end
def start_from
get :start_from
end
def end_date
get :end_date
end
def ends_on
get :ends_on
end
def target
get :target
end
def show_always?
get :show_always
end
def show_from_delta
get :show_from_delta
end
def recurring_target_as_text
target == 'due_date' ? I18n.t("todos.recurrence.pattern.due") : I18n.t("todos.recurrence.pattern.show")
end
def recurrence_pattern
raise "Should not call AbstractRepeatPattern.recurrence_pattern directly. Overwrite in subclass"
end
def xth(x)
xth_day = [
I18n.t('todos.recurrence.pattern.first'),I18n.t('todos.recurrence.pattern.second'),I18n.t('todos.recurrence.pattern.third'),
I18n.t('todos.recurrence.pattern.fourth'),I18n.t('todos.recurrence.pattern.last')]
x.nil? ? '??' : xth_day[x-1]
end
def day_of_week_as_text(day)
day.nil? ? '??' : I18n.t('todos.recurrence.pattern.day_names')[day]
end
def month_of_year_as_text(month)
month.nil? ? '??' : I18n.t('todos.recurrence.pattern.month_names')[month]
end
def build_recurring_todo(attribute_handler)
@recurring_todo = @user.recurring_todos.build(attribute_handler.safe_attributes)
end
def update_recurring_todo(recurring_todo, attribute_handler)
recurring_todo.assign_attributes(attribute_handler.safe_attributes)
recurring_todo
end
def build_from_recurring_todo(recurring_todo)
@recurring_todo = recurring_todo
@attributes = Tracks::AttributeHandler.new(@user, recurring_todo.attributes)
end
def valid?
@recurring_todo.valid?
end
def validate_not_blank(object, msg)
errors[:base] << msg if object.blank?
end
def validate_not_nil(object, msg)
errors[:base] << msg if object.nil?
end
def validate
starts_and_ends_on_validations
set_recurrence_on_validations
end
def starts_and_ends_on_validations
validate_not_blank(start_from, "The start date needs to be filled in")
case ends_on
when 'ends_on_number_of_times'
validate_not_blank(number_of_occurences, "The number of recurrences needs to be filled in for 'Ends on'")
when "ends_on_end_date"
validate_not_blank(end_date, "The end date needs to be filled in for 'Ends on'")
else
errors[:base] << "The end of the recurrence is not selected" unless ends_on == "no_end_date"
end
end
def set_recurrence_on_validations
# show always or x days before due date. x not null
case target
when 'show_from_date'
# no validations
when 'due_date'
validate_not_nil(show_always?, "Please select when to show the action")
validate_not_blank(show_from_delta, "Please fill in the number of days to show the todo before the due date") unless show_always?
else
errors[:base] << "Unexpected value of recurrence target selector '#{target}'"
end
end
def errors
@recurring_todo.errors
end
def get(attribute)
@attributes[attribute]
end
# gets the next due date. returns nil if recurrence_target is not 'due_date'
def get_due_date(previous)
case target
when 'due_date'
get_next_date(previous)
when 'show_from_date'
nil
end
end
def get_show_from_date(previous)
case target
when 'due_date'
# so set show from date relative to due date unless show_always is true or show_from_delta is nil
return nil unless put_in_tickler?
get_due_date(previous) - show_from_delta.days
when 'show_from_date'
# Leave due date empty
get_next_date(previous)
end
end
# checks if the next todos should be put in the tickler for recurrence_target == 'due_date'
def put_in_tickler?
!( show_always? || show_from_delta.nil?)
end
def get_next_date(previous)
raise "Should not call AbstractRepeatPattern.get_next_date directly. Overwrite in subclass"
end
def continues_recurring?(previous)
return @recurring_todo.occurences_count < @recurring_todo.number_of_occurences unless @recurring_todo.number_of_occurences.nil?
return true if self.end_date.nil? || self.ends_on == 'no_end_date'
case self.target
when 'due_date'
get_due_date(previous) <= self.end_date
when 'show_from_date'
get_show_from_date(previous) <= self.end_date
end
end
private
# Determine start date to calculate next date for recurring todo which
# takes start_from and previous into account.
# offset needs to be 1.day for daily patterns or the start will be the
# same day as the previous
def determine_start(previous, offset=0.day)
start = self.start_from || NullTime.new
now = Time.zone.now
if previous
# check if the start_from date is later than previous. If so, use
# start_from as start to search for next date
start > previous ? start : previous + offset
else
# skip to present
start > now ? start : now
end
end
# Example: get 3rd (x) wednesday (weekday) of december (month) 2014 (year)
# 5th means last, so it will return the 4th if there is no 5th
def get_xth_day_of_month(x, weekday, month, year)
raise "Weekday should be between 0 and 6 with 0=sunday. You supplied #{weekday}" unless (0..6).include?(weekday)
raise "x should be 1-4 for first-fourth or 5 for last. You supplied #{x}" unless (0..5).include?(x)
if x == 5
return find_last_day_x_of_month(weekday, month, year)
else
return find_xth_day_of_month(x, weekday, month, year)
end
end
def find_last_day_x_of_month(weekday, month, year)
# count backwards. use UTC to avoid strange timezone oddities
# where last_day -= 1.day seems to shift tz+0100 to tz+0000
last_day = Time.utc(year, month, Time.days_in_month(month))
while last_day.wday != weekday
last_day -= 1.day
end
# convert back to local timezone
Time.zone.local(last_day.year, last_day.month, last_day.day)
end
def find_xth_day_of_month(x, weekday, month, year)
# 1-4th -> count upwards last -> count backwards. use UTC to avoid strange
# timezone oddities where last_day -= 1.day seems to shift tz+0100 to
# tz+0000
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
# convert back to local timezone
Time.zone.local(start.year, start.month, start.day)
end
end
end

View file

@ -0,0 +1,35 @@
module RecurringTodos
class DailyRecurringTodosBuilder < AbstractRecurringTodosBuilder
attr_reader :recurring_todo, :pattern
def initialize(user, attributes)
super(user, attributes, DailyRepeatPattern)
end
def attributes_to_filter
%w{daily_selector daily_every_x_days}
end
def map_attributes(mapping)
mapping.set(:only_work_days, only_work_days?(@selector))
mapping.set(:every_other1, mapping.get(:daily_every_x_days))
mapping.except(:daily_every_x_days)
end
def only_work_days?(daily_selector)
{ 'daily_every_x_day' => false,
'daily_every_work_day' => true}[daily_selector]
end
def selector_key
:daily_selector
end
def valid_selector?(selector)
%w{daily_every_x_day daily_every_work_day}.include?(selector)
end
end
end

View file

@ -0,0 +1,52 @@
module RecurringTodos
class DailyRepeatPattern < AbstractRepeatPattern
def initialize(user)
super user
end
def every_x_days
get :every_other1
end
def only_work_days?
get :only_work_days
end
def recurrence_pattern
if only_work_days?
I18n.t("todos.recurrence.pattern.on_work_days")
elsif every_x_days > 1
I18n.t("todos.recurrence.pattern.every_n_days", :n => every_x_days)
else
I18n.t("todos.recurrence.pattern.every_day")
end
end
def validate
super
errors[:base] << "Every other nth day may not be empty for this daily recurrence setting" if (!only_work_days?) && every_x_days.blank?
end
def get_next_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)
start = determine_start(previous, 1.day)
if only_work_days?
# jump over weekend if necessary
return start + 2.day if start.wday() == 6 # saturday
return start + 1.day if start.wday() == 0 # sunday
return start
else
# 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_x_days.day-1.day
end
end
end
end

View file

@ -0,0 +1,45 @@
module RecurringTodos
class MonthlyRecurringTodosBuilder < AbstractRecurringTodosBuilder
def initialize(user, attributes)
super(user, attributes, MonthlyRepeatPattern)
end
def attributes_to_filter
%w{
monthly_selector monthly_every_x_day monthly_every_x_month
monthly_every_x_month2 monthly_every_xth_day monthly_day_of_week
}
end
def map_attributes(mapping)
mapping = map(mapping, :every_other1, 'monthly_every_x_day')
mapping = map(mapping, :every_other3, 'monthly_every_xth_day')
mapping = map(mapping, :every_count, 'monthly_day_of_week')
mapping.set(:recurrence_selector, get_recurrence_selector)
mapping.set(:every_other2, mapping.get(get_every_other2))
mapping.except('monthly_every_x_month').except('monthly_every_x_month2')
end
def get_recurrence_selector
@selector=='monthly_every_x_day' ? 0 : 1
end
def get_every_other2
get_recurrence_selector == 0 ? 'monthly_every_x_month' : 'monthly_every_x_month2'
end
def selector_key
:monthly_selector
end
def valid_selector?(selector)
%w{monthly_every_x_day monthly_every_xth_day}.include?(selector)
end
end
end

View file

@ -0,0 +1,142 @@
module RecurringTodos
class MonthlyRepeatPattern < AbstractRepeatPattern
def initialize(user)
super user
end
def recurrence_selector
get :recurrence_selector
end
def every_x_day?
get(:recurrence_selector) == 0
end
def every_x_day
get(:every_other1)
end
def every_xth_day?
get(:recurrence_selector) == 1
end
def every_x_month
# in case monthly pattern is every day x, return every_other2 otherwise
# return a default value
get(:recurrence_selector) == 0 ? get(:every_other2) : 1
end
def every_x_month2
# in case monthly pattern is every xth day, return every_other2 otherwise
# return a default value
get(:recurrence_selector) == 1 ? get(:every_other2) : 1
end
def every_xth_day(default=nil)
get(:every_other3) || default
end
def day_of_week
get :every_count
end
def recurrence_pattern
if recurrence_selector == 0
recurrence_pattern_for_specific_day
else
recurrence_pattern_for_relative_day_in_month
end
end
def validate
super
case recurrence_selector
when 0 # 'monthly_every_x_day'
validate_not_blank(every_x_month, "Every other nth month may not be empty for recurrence setting")
when 1 # 'monthly_every_xth_day'
validate_not_blank(every_x_month2, "Every other nth month may not be empty for recurrence setting")
validate_not_blank(day_of_week, "The day of the month may not be empty for recurrence setting")
else
raise Exception.new, "unexpected value of recurrence selector '#{recurrence_selector}'"
end
end
def get_next_date(previous)
start = determine_start(previous)
n = get(:every_other2)
case recurrence_selector
when 0 # specific day of the month
return find_specific_day_of_month(previous, start, n)
when 1 # relative weekday of a month
return find_relative_day_of_month(start, n)
end
nil
end
private
def find_specific_day_of_month(previous, start, n)
if (previous && start.mday >= every_x_day) || (previous.nil? && start.mday > every_x_day)
# there is no next day n in this month, search in next month
#
# start += n.months
#
# The above seems to not work. Fiddle with timezone. Looks like we hit a
# bug in rails here where 2008-12-01 +0100 plus 1.month becomes
# 2008-12-31 +0100. For now, just calculate in UTC and convert back to
# local timezone.
#
# TODO: recheck if future rails versions have this problem too
start = Time.utc(start.year, start.month, start.day)+n.months
end
Time.zone.local(start.year, start.month, every_x_day)
end
def find_relative_day_of_month(start, n)
the_next = get_xth_day_of_month(every_xth_day, day_of_week, 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
# fiddle with timezone. Looks like we hit a bug in rails here where
# 2008-12-01 +0100 plus 1.month becomes 2008-12-31 +0100. For now, just
# calculate in UTC and convert back to local timezone.
# TODO: recheck if future rails versions have this problem too
the_next = Time.utc(the_next.year, the_next.month, the_next.day)+n.months
the_next = Time.zone.local(the_next.year, the_next.month, the_next.day)
# 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(every_xth_day, day_of_week, the_next.month, the_next.year)
end
the_next
end
def recurrence_pattern_for_specific_day
on_day = " #{I18n.t('todos.recurrence.pattern.on_day_n', :n => every_x_day)}"
if every_xth_day(0) > 1
I18n.t("todos.recurrence.pattern.every_n_months", :n => every_xth_day) + on_day
else
I18n.t("todos.recurrence.pattern.every_month") + on_day
end
end
def recurrence_pattern_for_relative_day_in_month
n_months = if every_x_month2 > 1
"#{every_x_month2} #{I18n.t('common.months')}"
else
I18n.t('common.month')
end
I18n.t('todos.recurrence.pattern.every_xth_day_of_every_n_months',
x: xth(every_xth_day),
day: day_of_week_as_text(day_of_week),
n_months: n_months)
end
end
end

View file

@ -0,0 +1,78 @@
module RecurringTodos
class RecurringTodosBuilder
attr_reader :builder, :project, :context, :tag_list, :user
def initialize (user, attributes)
@user = user
@attributes = Tracks::AttributeHandler.new(@user, attributes)
parse_dates
parse_project
parse_context
@builder = create_builder(@attributes[:recurring_period])
end
def create_builder(selector)
raise "Unknown recurrence selector in :recurring_period (#{selector})" unless valid_selector? selector
eval("RecurringTodos::#{selector.capitalize}RecurringTodosBuilder.new(@user, @attributes)")
end
def build
@builder.build
end
def update(recurring_todo)
@builder.update(recurring_todo)
end
def save
@builder.save_project if @new_project_created
@builder.save_context if @new_context_created
return @builder.save
end
def saved_recurring_todo
@builder.saved_recurring_todo
end
def recurring_todo
@builder.recurring_todo
end
def attributes
@builder.attributes
end
def pattern
@builder.pattern
end
def errors
@builder.errors
end
private
def valid_selector?(selector)
%w{daily weekly monthly yearly}.include?(selector)
end
def parse_dates
%w{end_date start_from}.each {|date| @attributes.parse_date date }
end
def parse_project
@project, @new_project_created = @attributes.parse_collection(:project, @user.projects)
end
def parse_context
@context, @new_context_created = @attributes.parse_collection(:context, @user.contexts)
end
end
end

View file

@ -0,0 +1,42 @@
module RecurringTodos
class WeeklyRecurringTodosBuilder < AbstractRecurringTodosBuilder
def initialize(user, attributes)
super(user, attributes, WeeklyRepeatPattern)
end
def attributes_to_filter
%w{weekly_selector weekly_every_x_week} + %w{monday tuesday wednesday thursday friday saturday sunday}.map{|day| "weekly_return_#{day}" }
end
def map_attributes(mapping)
mapping = map(mapping, :every_other1, 'weekly_every_x_week')
{ monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 0 }
.each do |day, index|
mapping = map_day(mapping, :every_day, "weekly_return_#{day}", index)
end
mapping
end
def map_day(mapping, key, source_key, index)
mapping.set_if_nil(key, ' ') # avoid nil
mapping.set_if_nil(source_key, ' ') # avoid nil
mapping.set(key, mapping.get(key)[0, index] + mapping.get(source_key) + mapping.get(key)[index+1, mapping.get(key).length])
mapping.except(source_key)
end
def selector_key
nil
end
def valid_selector?(key)
true
end
end
end

View file

@ -0,0 +1,86 @@
module RecurringTodos
class WeeklyRepeatPattern < AbstractRepeatPattern
def initialize(user)
super user
end
def every_x_week
get :every_other1
end
{ monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 0 }.each do |day, number|
define_method("on_#{day}") do
on_xday number
end
end
def on_xday(n)
get(:every_day) && get(:every_day)[n, 1] != ' '
end
def recurrence_pattern
if every_x_week > 1
I18n.t("todos.recurrence.pattern.every_n", :n => every_x_week) + " " + I18n.t("common.weeks")
else
I18n.t('todos.recurrence.pattern.weekly')
end
end
def validate
super
validate_not_blank(every_x_week, "Every other nth week may not be empty for weekly recurrence setting")
something_set = %w{sunday monday tuesday wednesday thursday friday saturday}.inject(false) { |set, day| set || self.send("on_#{day}") }
errors[:base] << "You must specify at least one day on which the todo recurs" unless something_set
end
def get_next_date(previous)
start = determine_start_date(previous)
day = find_first_day_in_this_week(start)
return day unless day == -1
# we did not find anything this week, so check the nth next, starting from
# sunday
start = start + self.every_x_week.week - (start.wday()).days
start = find_first_day_in_this_week(start)
return start unless start == -1
raise Exception.new, "unable to find next weekly date (#{self.every_day})"
end
private
def determine_start_date(previous)
if previous.nil?
return self.start_from || Time.zone.now
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. Note that we already went into the next week, so -1
start += (every_x_week-1).week
end
unless self.start_from.nil?
# check if the start_from date is later than previous. If so, use
# start_from as start to search for next date
start = self.start_from if self.start_from > previous
end
return start
end
end
def find_first_day_in_this_week(start)
# 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 if on_xday(i)
end
-1
end
end
end

View file

@ -0,0 +1,44 @@
module RecurringTodos
class YearlyRecurringTodosBuilder < AbstractRecurringTodosBuilder
def initialize(user, attributes)
super(user, attributes, YearlyRepeatPattern)
end
def attributes_to_filter
%w{ yearly_selector yearly_month_of_year yearly_month_of_year2
yearly_every_x_day yearly_every_xth_day yearly_day_of_week
}
end
def map_attributes(mapping)
mapping.set(:recurrence_selector, get_recurrence_selector)
mapping.set(:every_other2, mapping.get(get_every_other2))
mapping = map(mapping, :every_other1, 'yearly_every_x_day')
mapping = map(mapping, :every_other3, 'yearly_every_xth_day')
mapping = map(mapping, :every_count, 'yearly_day_of_week')
mapping.except(:yearly_month_of_year).except(:yearly_month_of_year2)
end
def selector_key
:yearly_selector
end
def valid_selector?(selector)
%w{yearly_every_x_day yearly_every_xth_day}.include?(selector.to_s)
end
def get_recurrence_selector
@selector=='yearly_every_x_day' ? 0 : 1
end
def get_every_other2
{ 0 => :yearly_month_of_year, 1 => :yearly_month_of_year2 }[get_recurrence_selector]
end
end
end

View file

@ -0,0 +1,109 @@
module RecurringTodos
class YearlyRepeatPattern < AbstractRepeatPattern
def initialize(user)
super user
end
def recurrence_selector
get :recurrence_selector
end
def month_of_year
get :every_other2
end
def every_x_day
get :every_other1
end
def every_xth_day
get :every_other3
end
def day_of_week
get :every_count
end
def month_of_year2
# if recurrence pattern is every xth day in a month, return month otherwise
# return a default value
get(:recurrence_selector) == 1 ? get(:every_other2) : Time.zone.now.month
end
def recurrence_pattern
if self.recurrence_selector == 0
I18n.t("todos.recurrence.pattern.every_year_on", :date => date_as_month_day)
else
I18n.t("todos.recurrence.pattern.every_year_on",
:date => I18n.t("todos.recurrence.pattern.the_xth_day_of_month",
:x => xth(every_xth_day),
:day => day_of_week_as_text(day_of_week),
:month => month_of_year_as_text(month_of_year)
))
end
end
def validate
super
case recurrence_selector
when 0 # 'yearly_every_x_day'
validate_not_blank(month_of_year, "The month of the year may not be empty for recurrence setting")
validate_not_blank(every_x_day, "The day of the month may not be empty for recurrence setting")
when 1 # 'yearly_every_xth_day'
validate_not_blank(month_of_year2, "The month of the year may not be empty for recurrence setting")
validate_not_blank(every_xth_day, "The nth day of the month may not be empty for recurrence setting")
validate_not_blank(day_of_week, "The day of the week may not be empty for recurrence setting")
else
raise "unexpected value of recurrence selector '#{recurrence_selector}'"
end
end
def get_next_date(previous)
start = determine_start(previous)
month = get(:every_other2)
case recurrence_selector
when 0 # specific day of a specific month
return get_specific_day_of_month(start, month)
when 1 # relative weekday of a specific month
return get_relative_weekday_of_month(start, month)
end
nil
end
private
def date_as_month_day
I18n.l(DateTime.new(Time.zone.now.year, month_of_year, every_x_day), :format => :month_day)
end
def get_specific_day_of_month(start, month)
if start.month > month || (start.month == month && start.day >= every_x_day)
# if there is no next month n and day m in this year, search in next
# year
start = Time.zone.local(start.year+1, month, 1)
else
# if there is a next month n, stay in this year
start = Time.zone.local(start.year, month, 1)
end
Time.zone.local(start.year, month, every_x_day)
end
def get_relative_weekday_of_month(start, month)
# if there is no next month n in this year, search in next year
the_next = start.month > month ? Time.zone.local(start.year+1, month, 1) : start
# get the xth day of the month
the_next = get_xth_day_of_month(self.every_xth_day, day_of_week, 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_xth_day, day_of_week, month, start.year+1) if the_next <= start
the_next
end
end
end

View file

@ -5,91 +5,91 @@
<div id="recurring_todo_form_container">
<div id="recurring_todo">
<label for="recurring_todo_description"><%= Todo.human_attribute_name('description') %></label><%=
text_field_tag( "recurring_todo[description]", @recurring_todo.description, "size" => 30, "maxlength" => 100, :id => "edit_recurring_todo_description") -%>
text_field_tag( "recurring_todo[description]", @form_helper.description, "size" => 30, "maxlength" => 100, :id => "edit_recurring_todo_description") -%>
<label for="recurring_todo_notes"><%= Todo.human_attribute_name('notes') %></label><%=
text_area_tag( "recurring_todo[notes]", @recurring_todo.notes, {:cols => 29, :rows => 6}) -%>
text_area_tag( "recurring_todo[notes]", @form_helper.notes, {:cols => 29, :rows => 6}) -%>
<label for="edit_recurring_todo_project_name"><%= Todo.human_attribute_name('project') %></label>
<input id="edit_recurring_todo_project_name" name="project_name" autocomplete="off" size="30" type="text" value="<%= @recurring_todo.project.nil? ? 'None' : @recurring_todo.project.name.gsub(/"/,"&quot;") %>" />
<input id="edit_recurring_todo_project_name" name="project_name" autocomplete="off" size="30" type="text" value="<%= @form_helper.project.nil? ? 'None' : @form_helper.project.name.gsub(/"/,"&quot;") %>" />
<div class="page_name_auto_complete" id="edit_project_list" style="display:none"></div>
<label for="edit_recurring_todo_context_name"><%= Todo.human_attribute_name('context') %></label>
<input id="edit_recurring_todo_context_name" name="context_name" autocomplete="off" size="30" type="text" value="<%= @recurring_todo.context.name %>" />
<input id="edit_recurring_todo_context_name" name="context_name" autocomplete="off" size="30" type="text" value="<%= @form_helper.context.name %>" />
<div class="page_name_auto_complete" id="edit_context_list" style="display:none"></div>
<label for="edit_recurring_todo_tag_list"><%= "#{Todo.human_attribute_name('tags')} #{t('shared.separate_tags_with_commas')}"%></label>
<%= text_field_tag "edit_recurring_todo_tag_list", @recurring_todo.tag_list, :size => 30 -%>
<%= text_field_tag "edit_recurring_todo_tag_list", @form_helper.tag_list, :size => 30 -%>
</div>
</div>
<div id="recurring_edit_period_id">
<div id="recurring_edit_period">
<label><%= t('todos.recurrence_period') %></label><br/>
<%= radio_button_tag('recurring_edit_todo[recurring_period]', 'daily', @recurring_todo.recurring_period == 'daily')%> <%= t('todos.recurrence.daily') %><br/>
<%= radio_button_tag('recurring_edit_todo[recurring_period]', 'weekly', @recurring_todo.recurring_period == 'weekly')%> <%= t('todos.recurrence.weekly') %><br/>
<%= radio_button_tag('recurring_edit_todo[recurring_period]', 'monthly', @recurring_todo.recurring_period == 'monthly')%> <%= t('todos.recurrence.monthly') %><br/>
<%= radio_button_tag('recurring_edit_todo[recurring_period]', 'yearly', @recurring_todo.recurring_period == 'yearly')%> <%= t('todos.recurrence.yearly') %><br/>
<%= radio_button_tag('recurring_edit_todo[recurring_period]', 'daily', @form_helper.recurring_period == 'daily')%> <%= t('todos.recurrence.daily') %><br/>
<%= radio_button_tag('recurring_edit_todo[recurring_period]', 'weekly', @form_helper.recurring_period == 'weekly')%> <%= t('todos.recurrence.weekly') %><br/>
<%= radio_button_tag('recurring_edit_todo[recurring_period]', 'monthly', @form_helper.recurring_period == 'monthly')%> <%= t('todos.recurrence.monthly') %><br/>
<%= radio_button_tag('recurring_edit_todo[recurring_period]', 'yearly', @form_helper.recurring_period == 'yearly')%> <%= t('todos.recurrence.yearly') %><br/>
</div>
<div id="recurring_timespan">
<br/>
<label for="recurring_todo[start_from]"><%= t('todos.recurrence.starts_on') %>: </label><%=
text_field_tag("recurring_todo_edit_start_from", format_date(@recurring_todo.start_from), "size" => 12, "class" => "Date", "autocomplete" => "off") %><br/>
text_field_tag("recurring_todo_edit_start_from", format_date(@form_helper.start_from), "size" => 12, "class" => "Date", "autocomplete" => "off") %><br/>
<br/>
<label for="recurring_todo[ends_on]"><%= t('todos.recurrence.ends_on') %>:</label><br/>
<%= radio_button_tag('recurring_todo[ends_on]', 'no_end_date', @recurring_todo.ends_on == 'no_end_date')%> <%= t('todos.recurrence.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')%>
<%= radio_button_tag('recurring_todo[ends_on]', 'no_end_date', @form_helper.ends_on == 'no_end_date')%> <%= t('todos.recurrence.no_end_date') %><br/>
<%= radio_button_tag('recurring_todo[ends_on]', 'ends_on_number_of_times', @form_helper.ends_on == 'ends_on_number_of_times')%>
<%= raw t('todos.recurrence.ends_on_number_times', :number => text_field( :recurring_todo, :number_of_occurences, "size" => 3)) %><br/>
<%= radio_button_tag('recurring_todo[ends_on]', 'ends_on_end_date', @recurring_todo.ends_on == 'ends_on_end_date')%>
<%= raw t('todos.recurrence.ends_on_date', :date => text_field_tag('recurring_todo_edit_end_date', format_date(@recurring_todo.end_date), "size" => 12, "class" => "Date", "autocomplete" => "off")) %><br/>
<%= radio_button_tag('recurring_todo[ends_on]', 'ends_on_end_date', @form_helper.ends_on == 'ends_on_end_date')%>
<%= raw t('todos.recurrence.ends_on_date', :date => text_field_tag('recurring_todo_edit_end_date', format_date(@form_helper.end_date), "size" => 12, "class" => "Date", "autocomplete" => "off")) %><br/>
</div></div>
<div id="recurring_edit_daily" style="display:<%= @recurring_todo.recurring_period == 'daily' ? 'block' : 'none' %> ">
<div id="recurring_edit_daily" style="display:<%= @form_helper.recurring_period == 'daily' ? 'block' : 'none' %> ">
<label><%= t('todos.recurrence.daily_options') %></label><br/>
<%= radio_button_tag('recurring_todo[daily_selector]', 'daily_every_x_day', !@recurring_todo.only_work_days)%>
<%= raw t('todos.recurrence.daily_every_number_day', :number=> text_field_tag( 'recurring_todo[daily_every_x_days]', @recurring_todo.daily_every_x_days, {"size" => 3})) %><br/>
<%= radio_button_tag('recurring_todo[daily_selector]', 'daily_every_work_day', @recurring_todo.only_work_days)%> <%= t('todos.recurrence.every_work_day') %><br/>
<%= radio_button_tag('recurring_todo[daily_selector]', 'daily_every_x_day', !@form_helper.only_work_days)%>
<%= raw t('todos.recurrence.daily_every_number_day', :number=> text_field_tag( 'recurring_todo[daily_every_x_days]', @form_helper.daily_every_x_days, {"size" => 3})) %><br/>
<%= radio_button_tag('recurring_todo[daily_selector]', 'daily_every_work_day', @form_helper.only_work_days)%> <%= t('todos.recurrence.every_work_day') %><br/>
</div>
<div id="recurring_edit_weekly" style="display:<%= @recurring_todo.recurring_period == 'weekly' ? 'block' : 'none' %>">
<div id="recurring_edit_weekly" style="display:<%= @form_helper.recurring_period == 'weekly' ? 'block' : 'none' %>">
<label><%= t('todos.recurrence.weekly_options') %></label><br/>
<%= raw t('todos.recurrence.weekly_every_number_week', :number => text_field_tag('recurring_todo[weekly_every_x_week]', @recurring_todo.weekly_every_x_week, {"size" => 3})) %><br/>
<%= check_box_tag('recurring_todo[weekly_return_monday]', 'm', @recurring_todo.on_monday ) %> <%= t('date.day_names')[1] %>
<%= check_box_tag('recurring_todo[weekly_return_tuesday]', 't', @recurring_todo.on_tuesday) %> <%= t('date.day_names')[2] %>
<%= check_box_tag('recurring_todo[weekly_return_wednesday]', 'w', @recurring_todo.on_wednesday) %> <%= t('date.day_names')[3] %>
<%= check_box_tag('recurring_todo[weekly_return_thursday]', 't', @recurring_todo.on_thursday) %> <%= t('date.day_names')[4] %><br/>
<%= check_box_tag('recurring_todo[weekly_return_friday]', 'f', @recurring_todo.on_friday) %> <%= t('date.day_names')[5] %>
<%= check_box_tag('recurring_todo[weekly_return_saturday]', 's', @recurring_todo.on_saturday) %> <%= t('date.day_names')[6] %>
<%= check_box_tag('recurring_todo[weekly_return_sunday]', 's', @recurring_todo.on_sunday) %> <%= t('date.day_names')[0] %><br/>
<%= raw t('todos.recurrence.weekly_every_number_week', :number => text_field_tag('recurring_todo[weekly_every_x_week]', @form_helper.weekly_every_x_week, {"size" => 3})) %><br/>
<%= check_box_tag('recurring_todo[weekly_return_monday]', 'm', @form_helper.on_monday ) %> <%= t('date.day_names')[1] %>
<%= check_box_tag('recurring_todo[weekly_return_tuesday]', 't', @form_helper.on_tuesday) %> <%= t('date.day_names')[2] %>
<%= check_box_tag('recurring_todo[weekly_return_wednesday]', 'w', @form_helper.on_wednesday) %> <%= t('date.day_names')[3] %>
<%= check_box_tag('recurring_todo[weekly_return_thursday]', 't', @form_helper.on_thursday) %> <%= t('date.day_names')[4] %><br/>
<%= check_box_tag('recurring_todo[weekly_return_friday]', 'f', @form_helper.on_friday) %> <%= t('date.day_names')[5] %>
<%= check_box_tag('recurring_todo[weekly_return_saturday]', 's', @form_helper.on_saturday) %> <%= t('date.day_names')[6] %>
<%= check_box_tag('recurring_todo[weekly_return_sunday]', 's', @form_helper.on_sunday) %> <%= t('date.day_names')[0] %><br/>
</div>
<div id="recurring_edit_monthly" style="display:<%= @recurring_todo.recurring_period == 'monthly' ? 'block' : 'none' %>">
<div id="recurring_edit_monthly" style="display:<%= @form_helper.recurring_period == 'monthly' ? 'block' : 'none' %>">
<label><%= t('todos.recurrence.monthly_options') %></label><br/>
<%= radio_button_tag('recurring_todo[monthly_selector]', 'monthly_every_x_day', @recurring_todo.is_monthly_every_x_day || @recurring_todo.recurring_period == 'weekly')%>
<%= radio_button_tag('recurring_todo[monthly_selector]', 'monthly_every_x_day', @form_helper.monthly_every_x_day? || @form_helper.recurring_period == 'weekly')%>
<%= raw t('todos.recurrence.day_x_on_every_x_month',
:day => text_field_tag('recurring_todo[monthly_every_x_day]', @recurring_todo.monthly_every_x_day, {"size" => 3}),
:month => text_field_tag('recurring_todo[monthly_every_x_month]', @recurring_todo.monthly_every_x_month, {"size" => 3})) %><br/>
<%= radio_button_tag('recurring_todo[monthly_selector]', 'monthly_every_xth_day', @recurring_todo.is_monthly_every_xth_day)%>
:day => text_field_tag('recurring_todo[monthly_every_x_day]', @form_helper.monthly_every_x_day, {"size" => 3}),
:month => text_field_tag('recurring_todo[monthly_every_x_month]', @form_helper.monthly_every_x_month, {"size" => 3})) %><br/>
<%= radio_button_tag('recurring_todo[monthly_selector]', 'monthly_every_xth_day', @form_helper.monthly_every_xth_day?)%>
<%= raw t('todos.recurrence.monthly_every_xth_day',
:day => select_tag('recurring_todo[monthly_every_xth_day]', options_for_select(@xth_day, @xth_day[@recurring_todo.monthly_every_xth_day(1)-1][1])),
:day_of_week => select_tag('recurring_todo[monthly_day_of_week]' , options_for_select(@days_of_week, @recurring_todo.monthly_day_of_week)),
:month => text_field_tag('recurring_todo[monthly_every_x_month2]', @recurring_todo.monthly_every_x_month2, {"size" => 3})) %><br/>
:day => select_tag('recurring_todo[monthly_every_xth_day]', options_for_select(@xth_day, @xth_day[@form_helper.monthly_every_xth_day(1)-1][1])),
:day_of_week => select_tag('recurring_todo[monthly_day_of_week]' , options_for_select(@days_of_week, @form_helper.monthly_day_of_week)),
:month => text_field_tag('recurring_todo[monthly_every_x_month2]', @form_helper.monthly_every_x_month2, {"size" => 3})) %><br/>
</div>
<div id="recurring_edit_yearly" style="display:<%= @recurring_todo.recurring_period == 'yearly' ? 'block' : 'none' %>">
<div id="recurring_edit_yearly" style="display:<%= @form_helper.recurring_period == 'yearly' ? 'block' : 'none' %>">
<label><%= t('todos.recurrence.yearly_options') %></label><br/>
<%= radio_button_tag('recurring_todo[yearly_selector]', 'yearly_every_x_day', @recurring_todo.recurrence_selector == 0)%>
<%= radio_button_tag('recurring_todo[yearly_selector]', 'yearly_every_x_day', @form_helper.recurrence_selector == 0)%>
<%= raw t('todos.recurrence.yearly_every_x_day',
:month => select_tag('recurring_todo[yearly_month_of_year]', options_for_select(@months_of_year, @recurring_todo.yearly_month_of_year)),
:day => text_field_tag('recurring_todo[yearly_every_x_day]', @recurring_todo.yearly_every_x_day, "size" => 3)) %><br/>
<%= radio_button_tag('recurring_todo[yearly_selector]', 'yearly_every_xth_day', @recurring_todo.recurrence_selector == 1)%>
:month => select_tag('recurring_todo[yearly_month_of_year]', options_for_select(@months_of_year, @form_helper.yearly_month_of_year)),
:day => text_field_tag('recurring_todo[yearly_every_x_day]', @form_helper.yearly_every_x_day, "size" => 3)) %><br/>
<%= radio_button_tag('recurring_todo[yearly_selector]', 'yearly_every_xth_day', @form_helper.recurrence_selector == 1)%>
<%= raw t('todos.recurrence.yearly_every_xth_day',
:day => select_tag('recurring_todo[yearly_every_xth_day]', options_for_select(@xth_day, @recurring_todo.yearly_every_xth_day)),
:day_of_week => select_tag('recurring_todo[yearly_day_of_week]', options_for_select(@days_of_week, @recurring_todo.yearly_day_of_week)),
:month => select_tag('recurring_todo[yearly_month_of_year2]', options_for_select(@months_of_year, @recurring_todo.yearly_month_of_year2))) %><br/>
:day => select_tag('recurring_todo[yearly_every_xth_day]', options_for_select(@xth_day, @form_helper.yearly_every_xth_day)),
:day_of_week => select_tag('recurring_todo[yearly_day_of_week]', options_for_select(@days_of_week, @form_helper.yearly_day_of_week)),
:month => select_tag('recurring_todo[yearly_month_of_year2]', options_for_select(@months_of_year, @form_helper.yearly_month_of_year2))) %><br/>
</div>
<div id="recurring_target">
<label><%= t('todos.recurrence.recurrence_on.options') %></label><br/>
<%= radio_button_tag('recurring_todo[recurring_target]', 'due_date', @recurring_todo.target == 'due_date')%> <%= t('todos.recurrence.recurrence_on.due_date') %>. <%= t('todos.recurrence.recurrence_on.show_options') %>:
<%= radio_button_tag('recurring_todo[recurring_show_always]', '1', @recurring_todo.show_always?)%> <%= t('todos.recurrence.recurrence_on.show_always') %>
<%= radio_button_tag('recurring_todo[recurring_show_always]', '0', !@recurring_todo.show_always?)%>
<%= raw t('todos.recurrence.recurrence_on.show_days_before', :days => text_field_tag( 'recurring_todo[recurring_show_days_before]', @recurring_todo.show_from_delta, {"size" => 3})) %><br/>
<%= radio_button_tag('recurring_todo[recurring_target]', 'show_from_date', @recurring_todo.target == 'show_from_date')%> <%= t('todos.recurrence.recurrence_on.from_tickler') %><br/>
<%= radio_button_tag('recurring_todo[recurring_target]', 'due_date', @form_helper.target == 'due_date')%> <%= t('todos.recurrence.recurrence_on.due_date') %>. <%= t('todos.recurrence.recurrence_on.show_options') %>:
<%= radio_button_tag('recurring_todo[recurring_show_always]', '1', @form_helper.show_always?)%> <%= t('todos.recurrence.recurrence_on.show_always') %>
<%= radio_button_tag('recurring_todo[recurring_show_always]', '0', !@form_helper.show_always?)%>
<%= raw t('todos.recurrence.recurrence_on.show_days_before', :days => text_field_tag( 'recurring_todo[recurring_show_days_before]', @form_helper.show_from_delta, {"size" => 3})) %><br/>
<%= radio_button_tag('recurring_todo[recurring_target]', 'show_from_date', @form_helper.target == 'show_from_date')%> <%= t('todos.recurrence.recurrence_on.from_tickler') %><br/>
<br/>
</div>
<% end %>