mirror of
https://github.com/TracksApp/tracks.git
synced 2026-01-07 17:58:50 +01:00
Merge branch 'master' into new-gui
Conflicts: Gemfile.lock
This commit is contained in:
commit
fdd4c267b3
39 changed files with 2824 additions and 1022 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
152
app/models/recurring_todos/abstract_recurring_todos_builder.rb
Normal file
152
app/models/recurring_todos/abstract_recurring_todos_builder.rb
Normal 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
|
||||
226
app/models/recurring_todos/abstract_repeat_pattern.rb
Normal file
226
app/models/recurring_todos/abstract_repeat_pattern.rb
Normal 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
|
||||
35
app/models/recurring_todos/daily_recurring_todos_builder.rb
Normal file
35
app/models/recurring_todos/daily_recurring_todos_builder.rb
Normal 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
|
||||
52
app/models/recurring_todos/daily_repeat_pattern.rb
Normal file
52
app/models/recurring_todos/daily_repeat_pattern.rb
Normal 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
|
||||
|
|
@ -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
|
||||
142
app/models/recurring_todos/monthly_repeat_pattern.rb
Normal file
142
app/models/recurring_todos/monthly_repeat_pattern.rb
Normal 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
|
||||
78
app/models/recurring_todos/recurring_todos_builder.rb
Normal file
78
app/models/recurring_todos/recurring_todos_builder.rb
Normal 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
|
||||
42
app/models/recurring_todos/weekly_recurring_todos_builder.rb
Normal file
42
app/models/recurring_todos/weekly_recurring_todos_builder.rb
Normal 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
|
||||
86
app/models/recurring_todos/weekly_repeat_pattern.rb
Normal file
86
app/models/recurring_todos/weekly_repeat_pattern.rb
Normal 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
|
||||
44
app/models/recurring_todos/yearly_recurring_todos_builder.rb
Normal file
44
app/models/recurring_todos/yearly_recurring_todos_builder.rb
Normal 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
|
||||
109
app/models/recurring_todos/yearly_repeat_pattern.rb
Normal file
109
app/models/recurring_todos/yearly_repeat_pattern.rb
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue