app/models/recurring_todos/abstract_recurrence_pattern.rb
module RecurringTodos
class AbstractRecurrencePattern
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 number_of_occurrences
get :number_of_occurrences
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 AbstractRecurrencePattern.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.add(:base, msg) if object.blank?
end
def validate_not_nil(object, msg)
errors.add(: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_occurrences, "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.add(: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.add(: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).at_midnight
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).at_midnight
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 AbstractRecurrencePattern.get_next_date directly. Override in subclass"
end
def continues_recurring?(previous)
return @recurring_todo.occurrences_count < @recurring_todo.number_of_occurrences unless @recurring_todo.number_of_occurrences.nil?
return true if end_date.nil? || ends_on == 'no_end_date'
case target
when 'due_date'
get_due_date(previous) <= end_date
when 'show_from_date'
get_show_from_date(previous) <= 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 = start_from || NullTime.new
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
now = Time.zone.now
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).cover?(weekday)
raise "x should be 1-4 for first-fourth or 5 for last. You supplied #{x}" unless (0..5).cover?(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)
last_day = Time.zone.local(year, month, Time.days_in_month(month))
while last_day.wday != weekday
last_day -= 1.day
end
last_day
end
def find_xth_day_of_month(x, weekday, month, year)
start = Time.zone.local(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
start
end
end
end