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

@ -52,7 +52,6 @@ gem 'turbolinks'
group :development do
gem "yard"
gem "tolk", git: 'https://github.com/tolk/tolk'
gem 'protected_attributes' # needed for tolk. remove when tolk updates
gem "bullet"
gem "rack-mini-profiler"
end

View file

@ -133,8 +133,6 @@ GEM
nokogiri (1.6.1)
mini_portile (~> 0.5.0)
polyglot (0.3.4)
protected_attributes (1.0.7)
activemodel (>= 4.0.1, < 5.0)
rack (1.5.2)
rack-mini-profiler (0.9.1)
rack (>= 1.1.3)
@ -245,7 +243,6 @@ DEPENDENCIES
mocha
mousetrap-rails
mysql2
protected_attributes
rack-mini-profiler
rails (~> 4.0.0)
rails_autolink

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 %>

View file

@ -630,13 +630,15 @@ en:
- December
third: third
every_n: every %{n}
on_day_n: on day %{n}
every_n_days: every %{n} days
every_n_months: every %{n} months
every_day: every day
on_day_n: on day %{n}
second: second
every_xth_day_of_every_n_months: every %{x} %{day} of every %{n_months}
from: from
weekly: weekly
last: last
every_day: every day
the_xth_day_of_month: the %{x} %{day} of %{month}
times: for %{number} times
on_work_days: on work days

View file

@ -20,5 +20,15 @@ namespace :tracks do
user.errors.each_full { |msg| puts "- #{msg}\n" }
end
end
desc 'Check all passwords for deprecated hashes'
task :check_passwords => :environment do
puts "The following users have deprecated password hashes:"
User.all.each do |user|
if user.uses_deprecated_password?
puts " #{user.login}"
end
end
end
end

View file

@ -1,5 +1,6 @@
class TodoFromRecurringTodo
attr_reader :user, :recurring_todo, :todo
def initialize(user, recurring_todo)
@user = user
@recurring_todo = recurring_todo
@ -32,9 +33,9 @@ class TodoFromRecurringTodo
def attributes
{
:description => recurring_todo.description,
:notes => recurring_todo.notes,
:project_id => recurring_todo.project_id,
:context_id => recurring_todo.context_id
:notes => recurring_todo.notes,
:project_id => recurring_todo.project_id,
:context_id => recurring_todo.context_id
}
end

View file

@ -0,0 +1,140 @@
module Tracks
class AttributeHandler
attr_reader :attributes
def initialize(user, attributes)
@user = user
@orig_attributes = attributes
@attributes = normalize(attributes)
end
def get(attribute)
@attributes[attribute.to_sym]
end
def [](attribute)
get attribute
end
def set(key, value)
@attributes[key.to_sym] = value
end
def set_if_nil(key, value)
@attributes[key.to_sym] ||= value
end
def []=(attribute, value)
set attribute, value
end
def except(key)
AttributeHandler.new(@user, @attributes.except(key.to_sym))
end
def key?(key)
@attributes.key?(key.to_sym)
end
def selector_key_present?(key)
key?(key)
end
def parse_date(date)
set(date, @user.prefs.parse_date(get(date)))
end
def parse_collection(object_type, relation)
object = nil
new_object_created = false
if specified_by_name?(object_type)
object, new_object_created = find_or_create_by_name(relation, object_name(object_type))
# put id of object in @attributes, i.e. set :project_id to project.id
@attributes[object_type.to_s + "_id"] = object.id unless new_object_created
else
# find context or project by its id
object = attribute_with_id_of(object_type).present? ? relation.find(attribute_with_id_of(object_type)) : nil
end
@attributes[object_type] = object
return object, new_object_created
end
def object_name(object_type)
send("#{object_type}_name")
end
def attribute_with_id_of(object_type)
map = { project: 'project_id', context: 'context_id' }
get map[object_type]
end
def find_or_create_by_name(relation, name)
new_object_created = false
object = relation.where(:name => name).first
unless object
object = relation.build(:name => name)
new_object_created = true
end
return object, new_object_created
end
def specified_by_name?(object_type)
self.send("#{object_type}_specified_by_name?")
end
def project_specified_by_name?
return false if get(: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 get(:context_id).present?
return false if context_name.blank?
true
end
def project_name
get(:project_name).try(:strip)
end
def context_name
get(:context_name).try(:strip)
end
def normalize(attributes)
# make sure the hash keys are all symbols
Hash[attributes.map{|k,v| [k.to_sym,v]}]
end
def safe_attributes
ActionController::Parameters.new(attributes).permit(
:context, :project,
# model attributes
:context_id, :project_id, :description, :notes, :state, :start_from,
: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, :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,
:monthly_every_x_day, :monthly_every_x_month2, :monthly_every_x_month,
:monthly_every_xth_day, :recurring_show_days_before,
:recurring_show_always, :weekly_every_x_week, :weekly_return_monday,
:yearly_day_of_week, :yearly_every_x_day, :yearly_every_xth_day,
:yearly_month_of_year2, :yearly_month_of_year,
# derived attributes
:weekly_return_monday, :weekly_return_tuesday, :weekly_return_wednesday,
:weekly_return_thursday, :weekly_return_friday, :weekly_return_saturday, :weekly_return_sunday
)
end
end
end

View file

@ -9,13 +9,21 @@ class RecurringTodosControllerTest < ActionController::TestCase
def test_destroy_recurring_todo
login_as(:admin_user)
rc = RecurringTodo.find(1)
todo = rc.todos.first
xhr :post, :destroy, :id => 1, :_source_view => 'todo'
begin
rc = RecurringTodo.find(1)
rescue
rc = nil
end
assert_nil rc
assert_nil rc, "rc should be deleted"
assert_nil todo.reload.recurring_todo_id, "todo should be unlinked from deleted recurring_todo"
end
def test_new_recurring_todo
@ -62,6 +70,52 @@ class RecurringTodosControllerTest < ActionController::TestCase
assert_equal orig_todo_count+1, Todo.count
end
def test_new_recurring_todo_handles_attribs_outside_rec_todo
login_as(:admin_user)
# check new rec todo is not there
assert_nil RecurringTodo.where(:description => "new recurring pattern").first
put :create,
"context_name"=>"library",
"project_name"=>"Build a working time machine",
"recurring_todo" =>
{
"daily_every_x_days"=>"1",
"daily_selector"=>"daily_every_x_day",
"description"=>"new recurring pattern",
"end_date" => "31/08/2010",
"ends_on" => "ends_on_end_date",
"monthly_day_of_week" => "1",
"monthly_every_x_day" => "18",
"monthly_every_x_month2" => "1",
"monthly_every_x_month" => "1",
"monthly_every_xth_day"=>"1",
"monthly_selector"=>"monthly_every_x_day",
"notes"=>"with some notes",
"number_of_occurences" => "",
"recurring_period"=>"yearly",
"recurring_show_days_before"=>"10",
"recurring_target"=>"due_date",
"recurring_show_always" => "1",
"start_from"=>"18/08/2008",
"weekly_every_x_week"=>"1",
"weekly_return_monday"=>"m",
"yearly_day_of_week"=>"1",
"yearly_every_x_day"=>"8",
"yearly_every_xth_day"=>"1",
"yearly_month_of_year2"=>"8",
"yearly_month_of_year"=>"6",
"yearly_selector"=>"yearly_every_x_day"
},
"tag_list"=>"one, two, three, four", :format => :js
new_rec_todo = RecurringTodo.where(:description => "new recurring pattern").first
assert_not_nil new_rec_todo
end
def test_recurring_todo_toggle_check
# the test fixtures did add recurring_todos but not the corresponding todos,
# so we check complete and uncheck to force creation of a todo from the
@ -313,4 +367,29 @@ class RecurringTodosControllerTest < ActionController::TestCase
assert_equal "completed", rt.state, "repeat pattern should be completed"
end
def test_update_recurring_todo
login_as(:admin_user)
rt = recurring_todos(:call_bill_gates_every_day)
put :update,
"recurring_todo" => {
"description" => "changed",
"daily_selector" => "daily_every_x_day",
"daily_every_x_days" => "2",
"ends_on" => "no_end_date",
"recurring_target" => "show_from_date"
},
"recurring_edit_todo" => {
"recurring_period" => rt.recurring_period,
},
"recurring_todo_edit_start_from" => "2/1/2013",
"end_date" => nil,
"ends_on" => "no_end_date",
"id" => "#{rt.id}",
"context_name" => "library",
format: :js
assert_equal "changed", rt.reload.description
end
end

View file

@ -92,7 +92,7 @@ call_bill_gates_every_week:
end_date: ~
number_of_occurences: ~
target: due_date
show_from_delta: ~
show_from_delta: 5
recurring_period: weekly
recurrence_selector: ~
show_always: false

View file

@ -0,0 +1,97 @@
require_relative '../test_helper'
class AttributeHandlerTest < ActiveSupport::TestCase
fixtures :users
def test_setting_attributes
h = Tracks::AttributeHandler.new(nil, {})
h.set('test', '123')
h['other']='one'
assert_equal '123', h.attributes[:test], ":test should be added"
assert_nil h.attributes['test'], "string should be converted to symbol"
assert_equal 'one', h[:other], ":other should be added as symbol using []="
assert_nil h.attributes[:new]
h.set_if_nil(:new, 'value')
assert_equal 'value', h.attributes[:new], "value should be set for new key"
h.set_if_nil(:new, 'other')
assert_equal 'value', h.attributes[:new], "value should not be set for existing key"
h.attributes[:empty] = nil
h.set_if_nil(:empty, "test")
assert_equal "test", h.attributes[:empty], "nil value should be overwritten"
end
def test_getting_attributes
h = Tracks::AttributeHandler.new(nil, { :get => "me"} )
assert h.key?(:get), "attributehandler should have key :get"
assert h.key?('get'), "attributehandler should have key :get"
assert_equal "me", h.attributes[:get], "attributehandler should have key :get"
assert_equal "me", h.get('get'), "key should be converted to symbol"
assert_equal "me", h[:get], "AttributeHandler should act like hash"
end
def test_removing_attributes
h = Tracks::AttributeHandler.new(nil, { :i_am => "here"} )
assert h.key?(:i_am)
h.except(:i_am)
assert h.key?(:i_am), "AttributeHandler should be immutable"
h2 = h.except("i_am")
assert !h2.key?(:i_am), "key as symbol should be removed"
end
def test_project_specified_by_name
h = Tracks::AttributeHandler.new(nil, { } )
assert !h.project_specified_by_name?, "project is not specified by id or by name"
h[:project_id]=4
assert !h.project_specified_by_name?, "project is specified by id, not by name"
h = h.except(:project_id)
h[:project_name] = "A project"
assert h.project_specified_by_name?, "project is specified by name"
h[:project_name] = "None"
assert !h.project_specified_by_name?, "None is special token to specify nil-project"
end
def test_context_specified_by_name
h = Tracks::AttributeHandler.new(nil, { } )
assert !h.context_specified_by_name?, "context is not specified by id or by name"
h["context_id"] = 4
assert !h.context_specified_by_name?, "context is specified by id, not by name"
h = h.except(:context_id)
h[:context_name] = "A context"
assert h.context_specified_by_name?, "context is specified by name"
end
def test_parse_collection
admin = users(:admin_user)
project = admin.projects.first
h = Tracks::AttributeHandler.new(admin, { "project_id" => project.id } )
parsed_project, new_project_created = h.parse_collection(:project, admin.projects)
assert !new_project_created, "should find existing project"
assert_equal project.id, parsed_project.id, "it should find the project"
h = Tracks::AttributeHandler.new(admin, { "project_name" => project.name } )
parsed_project, new_project_created = h.parse_collection(:project, admin.projects)
assert !new_project_created, "should find existing project"
assert_equal project.id, parsed_project.id, "it should find the project"
h = Tracks::AttributeHandler.new(admin, { "project_name" => "new project" } )
parsed_project, new_project_created = h.parse_collection(:project, admin.projects)
assert new_project_created, "should detect that no project exist with that name"
assert_equal "new project", parsed_project.name, "it should return a new project"
assert !parsed_project.persisted?, "new project should not be persisted (yet)"
end
end

View file

@ -25,36 +25,6 @@ class RecurringTodoTest < ActiveSupport::TestCase
@thursday = Time.zone.local(2008,6,12)
end
def test_pattern_text
assert_equal "every day", @every_day.recurrence_pattern
assert_equal "on work days", @every_workday.recurrence_pattern
assert_equal "every last friday of every 2 months", @monthly_every_last_friday.recurrence_pattern
assert_equal "every year on June 08", @yearly.recurrence_pattern
end
def test_daily_every_day
# every_day should return todays date if there was no previous date
due_date = @every_day.get_due_date(nil)
# use only day-month-year compare, because milisec / secs could be different
assert_equal_dmy @today, due_date
# when the last todo was completed today, the next todo is due tomorrow
due_date =@every_day.get_due_date(@today)
assert_equal @tomorrow, due_date
# do something every 14 days
@every_day.every_other1=14
due_date = @every_day.get_due_date(@today)
assert_equal @today+14.days, due_date
end
def test_daily_work_days
assert_equal @monday, @every_workday.get_due_date(@friday)
assert_equal @monday, @every_workday.get_due_date(@saturday)
assert_equal @monday, @every_workday.get_due_date(@sunday)
assert_equal @tuesday, @every_workday.get_due_date(@monday)
end
def test_show_from_date
# assume that target due_date works fine, i.e. don't do the same tests over
@ -88,128 +58,6 @@ class RecurringTodoTest < ActiveSupport::TestCase
# weekly/monthly/yearly
end
def test_end_date_on_recurring_todo
assert_equal true, @every_day.continues_recurring?(@in_three_days)
assert_equal true, @every_day.continues_recurring?(@in_four_days)
@every_day.end_date = @in_four_days
@every_day.ends_on = 'ends_on_end_date'
assert_equal false, @every_day.continues_recurring?(@in_four_days)
end
def test_weekly_every_day_setters
@weekly_every_day.every_day = ' '
@weekly_every_day.weekly_return_sunday=('s')
assert_equal 's ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_monday=('m')
assert_equal 'sm ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_tuesday=('t')
assert_equal 'smt ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_wednesday=('w')
assert_equal 'smtw ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_thursday=('t')
assert_equal 'smtwt ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_friday=('f')
assert_equal 'smtwtf ', @weekly_every_day.every_day
@weekly_every_day.weekly_return_saturday=('s')
assert_equal 'smtwtfs', @weekly_every_day.every_day
# test remove
@weekly_every_day.weekly_return_wednesday=(' ')
assert_equal 'smt tfs', @weekly_every_day.every_day
end
def test_weekly_pattern
assert_equal true, @weekly_every_day.continues_recurring?(nil)
due_date = @weekly_every_day.get_due_date(@sunday)
assert_equal @monday, due_date
# saturday is last day in week, so the next date should be sunday + n-1 weeks
# n-1 because sunday is already in the next week
@weekly_every_day.every_other1 = 3
due_date = @weekly_every_day.get_due_date(@saturday)
assert_equal @sunday + 2.weeks, due_date
# remove tuesday and wednesday
@weekly_every_day.weekly_return_tuesday=(' ')
@weekly_every_day.weekly_return_wednesday=(' ')
assert_equal 'sm tfs', @weekly_every_day.every_day
due_date = @weekly_every_day.get_due_date(@monday)
assert_equal @thursday, due_date
@weekly_every_day.every_other1 = 1
@weekly_every_day.every_day = ' tw '
due_date = @weekly_every_day.get_due_date(@tuesday)
assert_equal @wednesday, due_date
due_date = @weekly_every_day.get_due_date(@wednesday)
assert_equal @tuesday+1.week, due_date
@weekly_every_day.every_day = ' s'
due_date = @weekly_every_day.get_due_date(@sunday)
assert_equal @saturday+1.week, due_date
end
def test_monthly_pattern
due_date = @monthly_every_last_friday.get_due_date(@sunday)
assert_equal Time.zone.local(2008,6,27), due_date
friday_is_last_day_of_month = Time.zone.local(2008,10,31)
due_date = @monthly_every_last_friday.get_due_date(friday_is_last_day_of_month-1.day )
assert_equal friday_is_last_day_of_month , due_date
@monthly_every_third_friday = @monthly_every_last_friday
@monthly_every_third_friday.every_other3=3 #third
due_date = @monthly_every_last_friday.get_due_date(@sunday) # june 8th 2008
assert_equal Time.zone.local(2008, 6, 20), due_date
# set date past third friday of this month
due_date = @monthly_every_last_friday.get_due_date(Time.zone.local(2008,6,21)) # june 21th 2008
assert_equal Time.zone.local(2008, 8, 15), due_date # every 2 months, so aug
@monthly = @monthly_every_last_friday
@monthly.recurrence_selector=0
@monthly.every_other1 = 8 # every 8th day of the month
@monthly.every_other2 = 2 # every 2 months
due_date = @monthly.get_due_date(@saturday) # june 7th
assert_equal @sunday, due_date # june 8th
due_date = @monthly.get_due_date(@sunday) # june 8th
assert_equal Time.zone.local(2008,8,8), due_date # aug 8th
end
def test_yearly_pattern
# beginning of same year
due_date = @yearly.get_due_date(Time.zone.local(2008,2,10)) # feb 10th
assert_equal @sunday, due_date # june 8th
# same month, previous date
due_date = @yearly.get_due_date(@saturday) # june 7th
show_from_date = @yearly.get_show_from_date(@saturday) # june 7th
assert_equal @sunday, due_date # june 8th
assert_equal @sunday-5.days, show_from_date
# same month, day after
due_date = @yearly.get_due_date(@monday) # june 9th
assert_equal Time.zone.local(2009,6,8), due_date # june 8th next year
# very overdue
due_date = @yearly.get_due_date(@monday+5.months-2.days) # november 7
assert_equal Time.zone.local(2009,6,8), due_date # june 8th next year
@yearly.recurrence_selector = 1
@yearly.every_other3 = 2 # second
@yearly.every_count = 3 # wednesday
# beginning of same year
due_date = @yearly.get_due_date(Time.zone.local(2008,2,10)) # feb 10th
assert_equal Time.zone.local(2008,6,11), due_date # june 11th
# same month, before second wednesday
due_date = @yearly.get_due_date(@saturday) # june 7th
assert_equal Time.zone.local(2008,6,11), due_date # june 11th
# same month, after second wednesday
due_date = @yearly.get_due_date(Time.zone.local(2008,6,12)) # june 7th
assert_equal Time.zone.local(2009,6,10), due_date # june 10th
end
def test_next_todo_without_previous_todo
# test handling of nil as previous
#
@ -277,7 +125,7 @@ class RecurringTodoTest < ActiveSupport::TestCase
def test_toggle_completion
assert @yearly.active?
assert @yearly.toggle_completion!
assert @yearly.toggle_completion!, "toggle of completion should succeed"
assert @yearly.completed?
# entering completed state should set completed_at
@ -320,97 +168,4 @@ class RecurringTodoTest < ActiveSupport::TestCase
assert_equal true, @every_day.continues_recurring?(@in_three_days)
assert_equal 0, @every_day.occurences_count
end
def test_invalid_recurring_period_will_not_save
@every_day.recurring_period = 'invalid'
assert !@every_day.valid?
@every_month.recurrence_selector = 99
assert_raise(Exception){ @every_month.valid? }
@yearly.recurrence_selector = 99
assert_raise(Exception){ @yearly.valid? }
end
def test_every_n_the_day_must_be_filled
@every_day.every_other1 = nil
assert !@every_day.valid?
end
def test_every_n_week_must_be_filled
@every_week.every_other1 = nil
assert !@every_week.valid?
end
def test_every_n_month_must_be_filled
@every_month.every_other1 = nil
@every_month.every_other2 = nil
assert !@every_month.valid?
@every_month.recurrence_selector = 0
assert !@every_month.valid?
end
def test_set_every_n_days_from_form_input
todo = RecurringTodo.new({
:description => "Task every 2 days",
:context => Context.first,
:recurring_target => "show_from_date",
:start_from => "01/01/01",
:ends_on => "no_end_date",
:recurring_period => "daily",
:daily_selector => "daily_every_x_day",
:daily_every_x_days => 2,
})
assert todo.valid?, todo.errors.full_messages
assert_equal 2, todo.every_other1
end
def test_set_every_n_weeks_from_form_input
todo = RecurringTodo.new({
:description => "Task every 3 weeks",
:context => Context.first,
:recurring_target => "show_from_date",
:start_from => "01/01/01",
:ends_on => "no_end_date",
:recurring_period => "weekly",
:weekly_every_x_week => 3,
:weekly_return_monday => "m",
})
assert todo.valid?, todo.errors.full_messages
assert_equal 3, todo.every_other1
assert todo.on_monday
end
def test_set_every_n_months_from_form_input
todo = RecurringTodo.new({
:description => "Task every 4 months",
:context => Context.first,
:recurring_target => "show_from_date",
:start_from => "01/01/01",
:ends_on => "no_end_date",
:recurring_period => "monthly",
:monthly_selector => "monthly_every_x_day",
:monthly_every_x_day => 1,
:monthly_every_x_month => 4,
})
assert todo.valid?, todo.errors.full_messages
assert_equal 4, todo.every_other2
end
def test_set_yearly_from_form_input
todo = RecurringTodo.new({
:description => "Task every year in May",
:context => Context.first,
:recurring_target => "show_from_date",
:start_from => "01/01/01",
:ends_on => "no_end_date",
:recurring_period => "yearly",
:yearly_selector => "yearly_every_x_day",
:yearly_every_x_day => 15,
:yearly_month_of_year => 5,
})
assert todo.valid?, todo.errors.full_messages
assert_equal 5, todo.every_other2
end
end
end

View file

@ -0,0 +1,165 @@
require_relative '../../test_helper'
module RecurringTodos
class AbstractRecurringTodosBuilderTest < ActiveSupport::TestCase
fixtures :users
class TestRepeatPattern < AbstractRepeatPattern
def selector_key
'test'
end
def valid_selector?(selector)
true
end
end
def setup
@admin = users(:admin_user)
end
def test_filter_attributes_should_throw_exception
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => "daily",
'description' => "test",
'tag_list' => "tag, this, that",
'context_name' => "my new context",
'daily_selector' => 'daily_every_work_day',
'target' => 'due_date',
'show_always' => true,
'start_from' => '01/01/01',
'ends_on' => 'no_end_date'
})
assert_raise(Exception, "should have exception since we are using abstract builder") do
builder = AbstractRecurringTodosBuilder.new(@admin, attributes, DailyRepeatPattern)
end
end
def test_tags_should_be_filled_or_empty_string
# given attributes with filled tag_list
attributes = {
'recurring_period' => 'daily',
'daily_selector' => 'daily_every_work_day',
'tag_list' => "tag, this, that"
}
builder = RecurringTodosBuilder.new(@admin, attributes)
assert_equal "tag, this, that", builder.attributes[:tag_list]
# given attributes without tag_list
attributes = {
'recurring_period' => 'daily',
'daily_selector' => 'daily_every_work_day',
}
builder = RecurringTodosBuilder.new(@admin, attributes)
assert_equal "", builder.attributes[:tag_list]
# given attributes with nil tag_list
attributes = {
'recurring_period' => 'daily',
'daily_selector' => 'daily_every_work_day',
'tag_list' => nil
}
builder = RecurringTodosBuilder.new(@admin, attributes)
assert_equal "", builder.attributes[:tag_list]
# given attributes with empty tag_list ==> should be stripped
attributes = {
'recurring_period' => 'daily',
'daily_selector' => 'daily_every_work_day',
'tag_list' => " "
}
builder = RecurringTodosBuilder.new(@admin, attributes)
assert_equal "", builder.attributes[:tag_list]
end
def test_tags_should_be_saved_on_create_and_update
attributes = {
'recurring_period' => "daily",
'description' => "test",
'tag_list' => "tag, this, that",
'context_name' => "my new context",
'daily_selector' => 'daily_every_work_day',
'recurring_target' => 'show_from_date',
'show_always' => true,
'start_from' => '01/01/01',
'ends_on' => 'no_end_date'
}
builder = RecurringTodosBuilder.new(@admin, attributes)
assert builder.save, "it should be saved"
assert_equal "tag, that, this", builder.saved_recurring_todo.tag_list, "tags should be saved"
attributes['tag_list'] = '' # clear tag_list
builder = RecurringTodosBuilder.new(@admin, attributes)
assert !builder.tag_list.present?, "tag list should not be present"
assert builder.save, "it should be saved"
assert_equal "", builder.saved_recurring_todo.tag_list, "tag list should be empty"
# tags should be updated
rt = builder.saved_recurring_todo
attributes['tag_list'] = "bar, foo"
updater = RecurringTodosBuilder.new(@admin, attributes)
updater.update(rt)
rt.reload
assert_equal "bar, foo", rt.tag_list
end
def test_saved_should_raise_exception_on_validation_errors
attributes = {
'recurring_period' => "daily",
'description' => "test",
'tag_list' => "tag, this, that",
'context_name' => "my new context",
'daily_selector' => 'daily_every_work_day',
'recurring_target' => 'due_date',
'show_always' => true,
'start_from' => '01/01/01',
'ends_on' => 'no_end_date_error' # invalid end_on value
}
# creating builder should not raise exception
builder = RecurringTodosBuilder.new(@admin, attributes)
builder.build
assert !builder.recurring_todo.valid?, "model should have validation errors"
assert !builder.save, "should not be able to save because of validation errors"
assert_raise(Exception, "should have exception since there is no saved recurring todo"){ builder.saved_recurring_todo }
end
def test_map_removes_mapped_key
attributes = Tracks::AttributeHandler.new(@admin, { :source => "value"})
a_builder = WeeklyRecurringTodosBuilder.new(@admin, attributes)
attributes = a_builder.map(attributes, :target, :source)
assert_equal "value", attributes[:target]
assert_nil attributes[:source]
assert !attributes.key?(:source)
end
def test_get_selector_removes_selector_from_hash
attributes = Tracks::AttributeHandler.new(@admin, { :selector => "weekly" })
a_builder = WeeklyRecurringTodosBuilder.new(@admin, attributes)
assert "weekly", a_builder.get_selector(:selector)
assert !a_builder.attributes.key?(:selector)
end
def test_get_selector_raises_exception_when_missing_selector
attributes = Tracks::AttributeHandler.new(@admin, { })
a_builder = WeeklyRecurringTodosBuilder.new(@admin, attributes)
assert_raise(Exception, "should raise exception when recurrence selector is missing"){ a_builder.get_selector(:selector) }
end
end
end

View file

@ -0,0 +1,151 @@
require_relative '../../test_helper'
module RecurringTodos
class AbstractRepeatPatternTest < ActiveSupport::TestCase
fixtures :users
def setup
super
@admin = users(:admin_user)
end
def test_pattern_builds_from_existing_recurring_todo
rt = @admin.recurring_todos.first
pattern = rt.pattern
assert pattern.is_a?(DailyRepeatPattern), "recurring todo should have daily pattern"
end
def test_validation_on_due_date
attributes = {
'weekly_every_x_week' => 1,
'weekly_return_monday' => 'm', # weekly specific
}
pattern = create_recurring_todo(attributes)
assert !pattern.valid?, "should fail because show_always and show_from_delta are not there"
attributes['recurring_show_always'] = false
pattern = create_recurring_todo(attributes)
assert !pattern.valid?, "should fail because show_from_delta is not there"
attributes[:recurring_show_days_before] = 5
pattern = create_recurring_todo(attributes)
assert pattern.valid?, "should be valid:" + pattern.errors.full_messages.to_s
end
def test_validation_on_start_date
attributes = {
'weekly_every_x_week' => 1,
'weekly_return_monday' => 'm', # weekly specific
'recurring_show_always' => false,
'recurring_show_days_before' => 5,
'start_from' => nil
}
pattern = create_recurring_todo(attributes)
assert !pattern.valid?, "should be not valid because start_from is empty"
attributes['start_from'] = Time.zone.now - 1.week
pattern = create_recurring_todo(attributes)
assert pattern.valid?, "should be valid: " + pattern.errors.full_messages.to_s
end
def test_validation_on_end_date
attributes = {
'weekly_return_monday' => 'm', # weekly specific
'ends_on' => 'invalid_value',
'weekly_every_x_week' => 1,
'recurring_show_always' => false,
'recurring_show_days_before' => 5,
}
pattern = create_recurring_todo(attributes)
assert !pattern.valid?
attributes['ends_on']='ends_on_end_date'
attributes['end_date']=nil
pattern = create_recurring_todo(attributes)
assert !pattern.valid?, "should not be valid, because end_date is not supplied"
attributes['end_date']= Time.zone.now + 1.week
pattern = create_recurring_todo(attributes)
assert pattern.valid?, "should be valid"
end
def test_end_date_on_recurring_todo
rt = recurring_todos(:call_bill_gates_every_day)
assert_equal true, rt.continues_recurring?(@in_three_days)
assert_equal true, rt.continues_recurring?(@in_four_days)
rt.end_date = @in_four_days
rt.ends_on = 'ends_on_end_date'
assert_equal false, rt.continues_recurring?(@in_four_days)
end
def test_continues_recurring
rt = recurring_todos(:call_bill_gates_every_day)
assert rt.continues_recurring?(Time.zone.now), "should not end"
rt.end_date = Time.zone.now - 1.day
rt.ends_on = 'ends_on_end_date'
assert !rt.continues_recurring?(Time.zone.now), "should end because end_date is in the past"
rt.reload # reset
rt.number_of_occurences = 2
rt.occurences_count = 1
assert rt.continues_recurring?(Time.zone.now), "should continue since there still may come occurences"
rt.occurences_count = 2
assert !rt.continues_recurring?(Time.zone.now), "should end since all occurences are there"
end
def test_determine_start
Timecop.travel(2013,1,1) do
rt = create_recurring_todo
assert_equal "2013-01-01 00:00:00", rt.send(:determine_start, nil).to_s(:db), "no previous date, use today"
assert_equal "2013-01-01 00:00:00", rt.send(:determine_start, nil, 1.day).to_s(:db), "no previous date, use today without offset"
assert_equal "2013-01-02 00:00:00", rt.send(:determine_start, Time.zone.now, 1.day).to_s(:db), "use previous date and offset"
end
end
def test_xth_day_of_month
rt = create_recurring_todo
# march 2014 has 5 saturdays, the last will return the 5th
assert_equal "2014-03-01 00:00:00", rt.send(:get_xth_day_of_month, 1, 6, 3, 2014).to_s(:db)
assert_equal "2014-03-22 00:00:00", rt.send(:get_xth_day_of_month, 4, 6, 3, 2014).to_s(:db)
assert_equal "2014-03-29 00:00:00", rt.send(:get_xth_day_of_month, 5, 6, 3, 2014).to_s(:db)
# march 2014 has 4 fridays, the last will return the 4th
assert_equal "2014-03-07 00:00:00", rt.send(:get_xth_day_of_month, 1, 5, 3, 2014).to_s(:db)
assert_equal "2014-03-28 00:00:00", rt.send(:get_xth_day_of_month, 4, 5, 3, 2014).to_s(:db)
assert_equal "2014-03-28 00:00:00", rt.send(:get_xth_day_of_month, 5, 5, 3, 2014).to_s(:db)
assert_raise(RuntimeError, "should check on valid weekdays"){ rt.send(:get_xth_day_of_month, 5, 9, 3, 2014) }
assert_raise(RuntimeError, "should check on valid count x"){ rt.send(:get_xth_day_of_month, 6, 5, 3, 2014) }
end
private
def create_pattern(attributes)
builder = RecurringTodosBuilder.new(@admin, attributes)
builder.build
builder.pattern
end
def create_recurring_todo(attributes={})
create_pattern(attributes.reverse_merge({
'recurring_period' => 'weekly',
'recurring_target' => 'due_date',
'description' => 'a repeating todo', # generic
'ends_on' => 'ends_on_end_date',
'end_date' => Time.zone.now + 1.week,
'context_id' => @admin.contexts.first.id,
'start_from' => Time.zone.now - 1.week,
}))
end
end
end

View file

@ -0,0 +1,79 @@
require_relative '../../test_helper'
module RecurringTodos
class DailyRecurringTodosBuilderTest < ActiveSupport::TestCase
fixtures :users
def setup
@admin = users(:admin_user)
end
def test_pattern_is_daily
object = RecurringTodosBuilder.new(@admin, { 'recurring_period' => 'daily', 'daily_selector' => 'daily_every_x_day' })
assert object.builder.is_a? DailyRecurringTodosBuilder
end
def test_filter_non_daily_attributes
attributes = {
'recurring_period' => 'daily',
'description' => 'a repeating todo', # generic
'daily_selector' => 'daily_every_x_day', # daily specific
'bla_bla' => 'go away' # irrelevant for daily
}
result = RecurringTodosBuilder.new(@admin, attributes).attributes
assert_nil result.get('bla_bla'), "bla_bla should be filtered"
assert_nil result.get(:bla_bla), "bla_bla should be filtered"
assert_equal false, result.get(:only_work_days), "daily attributes should be preserved"
assert_equal "a repeating todo", result.get(:description), "description should be preserved"
end
def test_valid_selector
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => 'daily'
})
# should not raise
%w{daily_every_x_day daily_every_work_day}.each do |selector|
attributes.set('daily_selector', selector)
DailyRecurringTodosBuilder.new(@admin, attributes)
end
# should raise
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => 'daily',
'daily_selector' => 'wrong value'
})
# should raise
assert_raise(Exception, "should have exception since daily_selector has wrong value"){ DailyRecurringTodosBuilder.new(@admin, attributes) }
end
def test_mapping_of_attributes
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => 'daily',
'description' => 'a repeating todo', # generic
'daily_selector' => 'daily_every_x_day', # daily specific --> mapped to only_work_days=false
'daily_every_x_days' => '5' # mapped to every_other1
})
pattern = DailyRecurringTodosBuilder.new(@admin, attributes)
assert_equal '5', pattern.mapped_attributes.get(:every_other1), "every_other1 should be set to daily_every_x_days"
assert_equal false, pattern.mapped_attributes.get(:only_work_days), "only_work_days should be set to false for daily_every_x_day"
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => 'daily',
'description' => 'a repeating todo', # generic
'daily_selector' => 'daily_every_work_day', # daily specific --> mapped to only_work_days=true
})
pattern = DailyRecurringTodosBuilder.new(@admin, attributes)
assert_equal true, pattern.mapped_attributes.get(:only_work_days)
end
end
end

View file

@ -0,0 +1,81 @@
require_relative '../../test_helper'
module RecurringTodos
class DailyRepeatPatternTest < ActiveSupport::TestCase
fixtures :users
def setup
super
@admin = users(:admin_user)
@every_day = recurring_todos(:call_bill_gates_every_day)
@every_workday = recurring_todos(:call_bill_gates_every_workday)
end
def test_daily_attributes
rt = @admin.recurring_todos.first
assert_equal rt.every_other1, rt.pattern.every_x_days
assert_equal rt.only_work_days, rt.pattern.only_work_days?
end
def test_validate
rt = @admin.recurring_todos.first
assert rt.valid?, "rt should be valid at start"
rt.every_other1 = nil
rt.only_work_days = false
assert !rt.valid?, "every_x_days should not be empty then only_work_days==false"
rt.only_work_days = true
assert rt.valid?, "every_x_days may have any value for only_work_days==true"
rt.only_work_days = false
rt.every_other1 = 2
assert rt.valid?, "should be valid again"
end
def test_pattern_text
@every_day = recurring_todos(:call_bill_gates_every_day)
@every_workday = recurring_todos(:call_bill_gates_every_workday)
assert_equal "every day", @every_day.recurrence_pattern
assert_equal "on work days", @every_workday.recurrence_pattern
@every_day.every_other1 = 2
assert_equal "every 2 days", @every_day.recurrence_pattern
end
def test_daily_every_day
# every_day should return todays date if there was no previous date
due_date = @every_day.get_due_date(nil)
# use only day-month-year compare, because milisec / secs could be different
assert_equal_dmy @today, due_date
# when the last todo was completed today, the next todo is due tomorrow
due_date =@every_day.get_due_date(@today)
assert_equal @tomorrow, due_date
# do something every 14 days
@every_day.every_other1=14
due_date = @every_day.get_due_date(@today)
assert_equal @today+14.days, due_date
end
def test_only_work_days_skips_weekend
assert_equal @tuesday, @every_workday.get_due_date(@monday), "should select next day if it is not in weekend"
assert_equal @monday, @every_workday.get_due_date(@friday), "should select monday if it is in weekend"
assert_equal @monday, @every_workday.get_due_date(@saturday), "should select monday if it is in weekend"
assert_equal @monday, @every_workday.get_due_date(@sunday), "should select monday if it is in weekend"
end
def test_every_x_days
assert_equal @tuesday, @every_day.get_due_date(@monday), "should select next day in middle week"
assert_equal @saturday, @every_day.get_due_date(@friday), "should select next day at end of week"
assert_equal @sunday, @every_day.get_due_date(@saturday), "should select next day in weekend"
end
end
end

View file

@ -0,0 +1,24 @@
require_relative '../../test_helper'
module RecurringTodos
class AttributeHandlerTest < ActiveSupport::TestCase
fixtures :users
def test_method_missing
rt = users(:admin_user).recurring_todos.first
rt.every_other1 = 42
rt.every_day = 'smtwtfs'
rt.save
h = FormHelper.new(rt)
assert_equal 42, h.daily_every_x_days, "should be passed to DailyRepeatPattern"
assert_equal 42, h.weekly_every_x_week, "should be passed to WeeklyRepeatPattern"
assert_equal 42, h.monthly_every_x_day, "should be passed to MonthlyRepeatPattern"
assert_equal 42, h.yearly_every_x_day, "should be passed to YearlyRepeatPattern"
assert h.on_monday, "should be passed to WeeklyRepeatPattern"
end
end
end

View file

@ -0,0 +1,95 @@
require_relative '../../test_helper'
module RecurringTodos
class MonthlyRecurringTodosBuilderTest < ActiveSupport::TestCase
fixtures :users
def setup
@admin = users(:admin_user)
end
def test_pattern_is_monthly
object = RecurringTodosBuilder.new(@admin, { 'recurring_period' => 'monthly', 'monthly_selector' => 'monthly_every_x_day' })
assert object.builder.is_a?(MonthlyRecurringTodosBuilder), "Builder should be of type MonthlyRecurringTodosBuilder"
end
def test_filter_non_daily_attributes
attributes = {
'recurring_period' => 'monthly',
'description' => 'a repeating todo', # generic
'monthly_selector' => 'monthly_every_x_day', # monthly specific
'monthly_every_x_day' => 5, # should be preserved as :every_other1
'bla_bla' => 'go away' # irrelevant for daily
}
result = RecurringTodosBuilder.new(@admin, attributes).attributes
assert_nil result.get('bla_bla'), "bla_bla should be filtered"
assert_nil result.get(:bla_bla), "bla_bla should be filtered"
assert_equal 5, result.get(:every_other1), "should be preserved"
end
def test_valid_selector
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => 'monthly'
})
# should not raise
%w{monthly_every_x_day monthly_every_xth_day}.each do |selector|
attributes.set('monthly_selector', selector)
MonthlyRecurringTodosBuilder.new(@admin, attributes)
end
# should raise
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => 'monthly',
'monthly_selector' => 'wrong value'
})
# should raise
assert_raise(Exception, "should have exception since monthly_selector has wrong value"){ MonthlyRecurringTodosBuilder.new(@admin, attributes) }
end
def test_mapping_of_attributes
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => 'monthly',
'description' => 'a repeating todo', # generic
'monthly_selector' => 'monthly_every_x_day', # monthly specific
'monthly_every_x_day' => '5', # mapped to :every_other1
'monthly_every_xth_day' => '7', # mapped to :every_other3
'monthly_day_of_week' => 3, # mapped to :every_count
'monthly_every_x_month' => '10', # mapped to :every_other2
'monthly_every_x_month2' => '20' # not mapped
})
builder = MonthlyRecurringTodosBuilder.new(@admin, attributes)
assert_equal 0, builder.mapped_attributes.get(:recurrence_selector), "selector should be 0 for monthly_every_x_day"
assert_equal '5', builder.mapped_attributes.get(:every_other1), "every_other1 should be set to monthly_every_x_days"
assert_equal '10', builder.mapped_attributes.get(:every_other2), "every_other2 should be set to monthly_every_x_month when selector is monthly_every_x_day (=0)"
assert_equal '7', builder.mapped_attributes.get(:every_other3), "every_other3 should be set to monthly_every_xth_day"
assert_equal 3, builder.mapped_attributes.get(:every_count), "every_count should be set to monthly_day_of_week"
builder.build
assert builder.pattern.every_x_day?, "every_x_day? should say true for selector monthly_every_x_day"
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => 'monthly',
'description' => 'a repeating todo', # generic
'monthly_selector' => 'monthly_every_xth_day', # monthly specific
'monthly_every_x_day' => '5', # mapped to :every_other1
'monthly_every_x_month' => '10', # not mapped
'monthly_every_x_month2' => '20' # mapped to :every_other2
})
builder = MonthlyRecurringTodosBuilder.new(@admin, attributes)
assert_equal 1, builder.mapped_attributes.get(:recurrence_selector), "selector should be 1 for monthly_every_xth_day"
assert_equal '20', builder.mapped_attributes.get(:every_other2), "every_other2 should be set to monthly_every_x_month2 when selector is monthly_every_xth_day (=0)"
builder.build
assert builder.pattern.every_xth_day?, "every_xth_day? should say true for selector monthly_every_xth_day"
end
end
end

View file

@ -0,0 +1,148 @@
require_relative '../../test_helper'
module RecurringTodos
class MonthlyRepeatPatternTest < ActiveSupport::TestCase
fixtures :users
def setup
super
@admin = users(:admin_user)
end
def test_attribute_mapping
builder = RecurringTodosBuilder.new(@admin, {
'recurring_period' => 'monthly',
'description' => 'a repeating todo', # generic
'recurring_period' => 'monthly',
'recurring_target' => 'show_from_date',
'ends_on' => 'ends_on_end_date',
'end_date' => Time.zone.now + 1.week,
'start_from' => Time.zone.now,
'context_name' => @admin.contexts.first.name,
'monthly_selector' => 'monthly_every_x_day',
'monthly_every_xth_day' => 1,
'monthly_day_of_week' => 2,
'monthly_every_x_month' => 3
})
assert builder.save, "should save: #{builder.errors.full_messages}"
rt = builder.saved_recurring_todo
assert builder.pattern.is_a?(MonthlyRepeatPattern), "should be monthly pattern, but is #{builder.pattern.class}"
assert builder.pattern.every_x_day?, "should be true for monthly_every_x_day"
assert 1, rt.recurrence_selector
assert_equal 1, builder.pattern.every_xth_day, "pattern should map every_other2 to every_xth_day from monthly_every_xth_day"
assert_equal 1, rt.every_other3
assert_equal 2, builder.pattern.day_of_week, "pattern should map every_count to day_of_week from monthly_day_of_week"
assert_equal 2, rt.every_count
end
def test_every_x_month
builder = RecurringTodosBuilder.new(@admin, {
'recurring_period' => 'monthly',
'description' => 'a repeating todo', # generic
'recurring_period' => 'monthly',
'recurring_target' => 'show_from_date',
'ends_on' => 'ends_on_end_date',
'end_date' => Time.zone.now + 1.week,
'start_from' => Time.zone.now,
'context_name' => @admin.contexts.first.name,
'monthly_selector' => 'monthly_every_x_day',
'monthly_every_x_month' => 3,
'monthly_every_x_month2' => 2
})
assert builder.save, "should save: #{builder.errors.full_messages}"
rt = builder.saved_recurring_todo
assert_equal 3, builder.pattern.every_x_month
assert_equal 3, rt.every_other2
builder = RecurringTodosBuilder.new(@admin, {
'recurring_period' => 'monthly',
'description' => 'a repeating todo', # generic
'recurring_period' => 'monthly',
'recurring_target' => 'show_from_date',
'ends_on' => 'ends_on_end_date',
'end_date' => Time.zone.now + 1.week,
'start_from' => Time.zone.now,
'context_name' => @admin.contexts.first.name,
'monthly_selector' => 'monthly_every_xth_day',
'monthly_every_x_month' => 3,
'monthly_every_x_month2' => 2,
'monthly_day_of_week' => 7
})
assert builder.save, "should save: #{builder.errors.full_messages}"
rt = builder.saved_recurring_todo
assert_equal 2, builder.pattern.every_x_month2
assert_equal 2, rt.every_other2
end
def test_validations
rt = @admin.recurring_todos.where(recurring_period: 'monthly').first
assert rt.valid?, "should be valid at start: #{rt.errors.full_messages}"
rt.recurrence_selector = 0 # 'monthly_every_x_day'
rt.every_other2 = nil
assert !rt.valid?, "should not be valid since every_x_month is empty"
rt.recurrence_selector = 1 # 'monthly_every_xth_day'
rt.every_other2 = nil
assert !rt.valid?, "should not be valid since every_xth_month is empty"
rt.every_count = nil
assert !rt.valid?, "should not be valid since day_of_week is empty"
end
def test_pattern_text
rt = recurring_todos(:check_with_bill_every_last_friday_of_month)
assert_equal "every last friday of every 2 months", rt.recurrence_pattern
rt.every_other2 = 1
assert_equal "every last friday of every month", rt.recurrence_pattern
rt.recurrence_selector = 0
assert_equal "every 5 months on day 1", rt.recurrence_pattern
rt.every_other3 = 1
assert_equal "every month on day 1", rt.recurrence_pattern
end
def test_monthly_pattern
@monthly_every_last_friday = recurring_todos(:check_with_bill_every_last_friday_of_month)
due_date = @monthly_every_last_friday.get_due_date(@sunday)
assert_equal Time.zone.local(2008,6,27), due_date
friday_is_last_day_of_month = Time.zone.local(2008,10,31)
due_date = @monthly_every_last_friday.get_due_date(friday_is_last_day_of_month-1.day )
assert_equal friday_is_last_day_of_month , due_date
@monthly_every_third_friday = @monthly_every_last_friday
@monthly_every_third_friday.every_other3=3 #third
due_date = @monthly_every_last_friday.get_due_date(@sunday) # june 8th 2008
assert_equal Time.zone.local(2008, 6, 20), due_date
# set date past third friday of this month
due_date = @monthly_every_last_friday.get_due_date(Time.zone.local(2008,6,21)) # june 21th 2008
assert_equal Time.zone.local(2008, 8, 15), due_date # every 2 months, so aug
@monthly = @monthly_every_last_friday
@monthly.recurrence_selector=0
@monthly.every_other1 = 8 # every 8th day of the month
@monthly.every_other2 = 2 # every 2 months
due_date = @monthly.get_due_date(@saturday) # june 7th
assert_equal @sunday, due_date # june 8th
due_date = @monthly.get_due_date(@sunday) # june 8th
assert_equal Time.zone.local(2008,8,8), due_date # aug 8th
end
end
end

View file

@ -0,0 +1,155 @@
require_relative '../../test_helper'
module RecurringTodos
class RecurringTodosBuilderTest < ActiveSupport::TestCase
fixtures :users
def setup
@admin = users(:admin_user)
end
def test_create_builder_needs_selector
assert_raise(RuntimeError){ builder = RecurringTodosBuilder.new(@admin, {}) }
end
def test_create_builder_needs_valid_selector
assert_raise(RuntimeError){ builder = RecurringTodosBuilder.new(@admin, { 'recurring_period' => 'wrong_value'}) }
end
def test_create_builder_uses_selector
builder = RecurringTodosBuilder.new(@admin, {'recurring_period' => "daily", 'daily_selector' => 'daily_every_work_day'}).builder
assert builder.is_a?(DailyRecurringTodosBuilder)
builder = RecurringTodosBuilder.new(@admin, {'recurring_period' => "weekly"}).builder
assert builder.is_a?(WeeklyRecurringTodosBuilder)
builder = RecurringTodosBuilder.new(@admin, {'recurring_period' => "monthly", 'monthly_selector' => 'monthly_every_x_day'}).builder
assert builder.is_a?(MonthlyRecurringTodosBuilder)
builder = RecurringTodosBuilder.new(@admin, {'recurring_period' => "yearly", 'yearly_selector' => 'yearly_every_x_day'}).builder
assert builder.is_a?(YearlyRecurringTodosBuilder)
end
def test_dates_are_parsed
builder = RecurringTodosBuilder.new(@admin, {
'recurring_period' => "daily",
'daily_selector' => 'daily_every_work_day',
'start_from' => "01/01/01",
'end_date' => '05/05/05'
})
assert builder.attributes[:start_from].is_a?(ActiveSupport::TimeWithZone), "Dates should be parsed to ActiveSupport::TimeWithZone class"
assert builder.attributes[:end_date ].is_a?(ActiveSupport::TimeWithZone), "Dates should be parsed to ActiveSupport::TimeWithZone class"
end
def test_exisisting_project_is_used
# test by project_name
builder = RecurringTodosBuilder.new(@admin, {
'recurring_period' => "daily",
'project_name' => @admin.projects.first.name,
'daily_selector' => 'daily_every_work_day'})
assert_equal @admin.projects.first, builder.project
# test by project_id
builder = RecurringTodosBuilder.new(@admin, {
'recurring_period' => "daily",
'daily_selector' => 'daily_every_work_day',
'project_id' => @admin.projects.first.id})
assert_equal @admin.projects.first, builder.project
end
def test_not_exisisting_project_is_created
builder = RecurringTodosBuilder.new(@admin, {
'recurring_period' => "daily",
'project_name' => "my new project",
'daily_selector' => 'daily_every_work_day',
'recurring_target' => 'due_date'})
assert_equal "my new project", builder.project.name, "project should exist"
assert !builder.project.persisted?, "new project should not be persisted before save"
builder.save
assert builder.project.persisted?, "new project should be persisted after save"
end
def test_exisisting_context_is_used
builder = RecurringTodosBuilder.new(@admin, {
'recurring_period' => "daily",
'context_name' => @admin.contexts.first.name,
'daily_selector' => 'daily_every_work_day'})
assert_equal @admin.contexts.first, builder.context
builder = RecurringTodosBuilder.new(@admin, {
'recurring_period' => "daily",
'daily_selector' => 'daily_every_work_day',
'context_id' => @admin.contexts.first.id})
assert_equal @admin.contexts.first, builder.context
end
def test_not_exisisting_context_is_created
builder = RecurringTodosBuilder.new(@admin, {
'recurring_period' => "daily",
'context_name' => "my new context",
'daily_selector' => 'daily_every_work_day',
'recurring_target' => 'due_date'})
assert_equal "my new context", builder.context.name, "context should exist"
assert !builder.context.persisted?, "new context should not be persisted before save"
builder.save
assert builder.context.persisted?, "new context should be persisted after save"
end
def test_project_is_optional
attributes = {
'recurring_period' => "daily",
'description' => "test",
'context_name' => "my new context",
'daily_selector' => 'daily_every_work_day',
'recurring_target' => 'show_from_date',
'show_always' => true,
'start_from' => '01/01/01',
'ends_on' => 'no_end_date'}
builder = RecurringTodosBuilder.new(@admin, attributes)
assert_nil builder.project, "project should not exist"
builder.save
assert_nil builder.saved_recurring_todo.project
end
def test_builder_can_update_description
attributes = {
'recurring_period' => "daily",
'description' => "test",
'context_name' => "my new context",
'daily_selector' => 'daily_every_work_day',
'recurring_target' => 'show_from_date',
'show_always' => true,
'start_from' => '01/01/01',
'ends_on' => 'no_end_date'}
builder = RecurringTodosBuilder.new(@admin, attributes)
builder.save
rt = builder.saved_recurring_todo
assert_equal "test", rt.description
attributes['description'] = 'updated'
updater = RecurringTodosBuilder.new(@admin, attributes)
updater.update(rt)
rt.reload
assert_equal rt.id, builder.saved_recurring_todo.id
assert_equal "updated", rt.description
end
end
end

View file

@ -0,0 +1,87 @@
require_relative '../../test_helper'
module RecurringTodos
class WeeklyRecurringTodosBuilderTest < ActiveSupport::TestCase
fixtures :users
def setup
@admin = users(:admin_user)
end
def test_pattern_is_weekly
object = RecurringTodosBuilder.new(@admin, { 'recurring_period' => 'weekly' })
assert object.builder.is_a? WeeklyRecurringTodosBuilder
end
def test_filter_non_daily_attributes
attributes = {
'recurring_period' => 'weekly',
'description' => 'a repeating todo', # generic
'weekly_return_monday' => 'm', # weekly specific
'bla_bla' => 'go away' # irrelevant
}
result = RecurringTodosBuilder.new(@admin, attributes).attributes
assert_nil result.get('bla_bla'), "bla_bla should be filtered"
assert_nil result.get(:bla_bla), "bla_bla should be filtered"
assert_equal ' m ', result.get(:every_day), "weekly attributes should be preserved"
assert_equal "a repeating todo", result.get(:description), "description should be preserved"
end
def test_attributes_to_filter
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => 'weekly',
'description' => 'a repeating todo', # generic
'weekly_return_monday' => 'm', # weekly specific
})
w = WeeklyRecurringTodosBuilder.new(@admin, attributes)
assert_equal 9, w.attributes_to_filter.size
assert w.attributes_to_filter.include?('weekly_selector'), "attributes_to_filter should return static attribute weekly_selector"
assert w.attributes_to_filter.include?('weekly_return_monday'), "attributes_to_filter should return generated weekly_return_xyz"
end
def test_mapping_of_attributes
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => 'weekly',
'description' => 'a repeating todo', # generic
'weekly_every_x_week' => '5', # mapped to every_other1
'weekly_return_monday' => 'm'
})
pattern = WeeklyRecurringTodosBuilder.new(@admin, attributes)
assert_equal '5', pattern.mapped_attributes.get(:every_other1), "every_other1 should be set to weekly_every_x_week"
assert_equal ' m ', pattern.mapped_attributes.get(:every_day), "weekly_return_<weekday> should be mapped to :every_day in format 'smtwtfs'"
end
def test_map_day
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => 'weekly',
'description' => 'a repeating todo', # generic
'weekly_every_x_week' => '5' # mapped to every_other1
})
pattern = WeeklyRecurringTodosBuilder.new(@admin, attributes)
assert_equal ' ', pattern.mapped_attributes.get(:every_day), "all days should be empty in :every_day"
# add all days
{ sunday: 's', monday: 'm', tuesday: 't', wednesday: 'w', thursday: 't', friday: 'f', saturday: 's' }.each do |day, short|
attributes.set("weekly_return_#{day}", short)
end
pattern = WeeklyRecurringTodosBuilder.new(@admin, attributes)
assert_equal 'smtwtfs', pattern.mapped_attributes.get(:every_day), "all days should be filled in :every_day"
# remove wednesday
attributes = attributes.except('weekly_return_wednesday')
pattern = WeeklyRecurringTodosBuilder.new(@admin, attributes)
assert_equal 'smt tfs', pattern.mapped_attributes.get(:every_day), "only wednesday should be empty in :every_day"
end
end
end

View file

@ -0,0 +1,82 @@
require_relative '../../test_helper'
module RecurringTodos
class WeeklyRepeatPatternTest < ActiveSupport::TestCase
fixtures :users
def setup
super
@admin = users(:admin_user)
end
def test_every_x_week
rt = @admin.recurring_todos.where(recurring_period: 'weekly').first
assert_equal rt.every_other1, rt.pattern.every_x_week
end
def test_on_xday
rt = @admin.recurring_todos.where(recurring_period: 'weekly').first
assert rt.valid?, "should be valid at start: id= #{rt.id} --> #{rt.errors.full_messages}"
rt.every_day = 'smtwtfs'
%w{monday tuesday wednesday thursday friday saturday sunday}.each do |day|
assert rt.pattern.send("on_#{day}"), "on_#{day} should return true"
end
rt.every_day = 'smt tfs' # no wednesday
assert !rt.pattern.on_wednesday, "wednesday should be false"
end
def test_validations
rt = @admin.recurring_todos.where(recurring_period: 'weekly').first
assert rt.valid?, "should be valid at start: #{rt.errors.full_messages}"
rt.every_other1 = nil
assert !rt.valid?, "missing evert_x_week should not be valid"
rt.every_other1 = 1
rt.every_day = ' '
assert !rt.valid?, "missing selected days in every_day"
end
def test_pattern_text
rt = @admin.recurring_todos.where(recurring_period: 'weekly').first
assert_equal "every 2 weeks", rt.recurrence_pattern
rt.every_other1 = 1
assert_equal "weekly", rt.recurrence_pattern
end
def test_weekly_pattern
rt = recurring_todos(:call_bill_gates_every_week)
due_date = rt.get_due_date(@sunday)
assert_equal @monday, due_date
# saturday is last day in week, so the next date should be sunday + n-1 weeks
# n-1 because sunday is already in the next week
rt.every_other1 = 3
due_date = rt.get_due_date(@saturday)
assert_equal @sunday + 2.weeks, due_date
# remove tuesday and wednesday
rt.every_day = 'sm tfs'
due_date = rt.get_due_date(@monday)
assert_equal @thursday, due_date
rt.every_other1 = 1
rt.every_day = ' tw '
due_date = rt.get_due_date(@tuesday)
assert_equal @wednesday, due_date
due_date = rt.get_due_date(@wednesday)
assert_equal @tuesday+1.week, due_date
rt.every_day = ' s'
due_date = rt.get_due_date(@sunday)
assert_equal @saturday+1.week, due_date
end
end
end

View file

@ -0,0 +1,88 @@
require_relative '../../test_helper'
module RecurringTodos
class YearlyRecurringTodosBuilderTest < ActiveSupport::TestCase
fixtures :users
def setup
@admin = users(:admin_user)
end
def test_pattern_is_yearly
object = RecurringTodosBuilder.new(@admin, { 'recurring_period' => 'yearly', 'yearly_selector' => 'yearly_every_x_day' })
assert object.builder.is_a? YearlyRecurringTodosBuilder
end
def test_filter_non_daily_attributes
attributes = {
'recurring_period' => 'yearly',
'description' => 'a repeating todo', # generic
'yearly_selector' => 'yearly_every_x_day', # daily specific
'yearly_month_of_year' => '1', # mapped to evert_other2 because yearly_selector is yearly_every_x_day
'bla_bla' => 'go away' # irrelevant for daily
}
result = RecurringTodosBuilder.new(@admin, attributes).attributes
assert_nil result.get('bla_bla'), "bla_bla should be filtered"
assert_nil result.get(:bla_bla), "bla_bla should be filtered"
assert_equal '1', result.get(:every_other2), "yearly attributes should be preserved"
assert_equal "a repeating todo", result.get(:description), "description should be preserved"
end
def test_valid_selector
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => 'yearly'
})
# should not raise
%w{yearly_every_x_day yearly_every_xth_day}.each do |selector|
attributes.set(:yearly_selector, selector)
YearlyRecurringTodosBuilder.new(@admin, attributes)
end
# should raise
attributes = Tracks::AttributeHandler.new(@admin, {
'recurring_period' => 'yearly',
'yearly_selector' => 'wrong value'
})
# should raise
assert_raise(Exception, "should have exception since yearly_selector has wrong value"){ YearlyRecurringTodosBuilder.new(@admin, attributes) }
end
def test_mapping_of_attributes
attributes = {
'recurring_period' => 'yearly',
'description' => 'a repeating todo', # generic
'yearly_selector' => 'yearly_every_x_day', # yearly specific
'yearly_every_x_day' => '5', # mapped to every_other1
'yearly_every_xth_day' => '7', # mapped to every_other3
'yearly_day_of_week' => '3', # mapped to every_count
'yearly_month_of_year' => '1', # mapped to evert_other2 because yearly_selector is yearly_every_x_day
'yearly_month_of_year2' => '2' # ignored because yearly_selector is yearly_every_x_day
}
pattern = YearlyRecurringTodosBuilder.new(@admin, Tracks::AttributeHandler.new(@admin, attributes))
assert_equal '5', pattern.mapped_attributes.get(:every_other1), "every_other1 should be set to yearly_every_x_day"
assert_equal '1', pattern.mapped_attributes.get(:every_other2), "every_other2 should be set to yearly_month_of_year because selector is yearly_every_x_day"
assert_equal '7', pattern.mapped_attributes.get(:every_other3), "every_other3 should be set to yearly_every_xth_day"
assert_equal '3', pattern.mapped_attributes.get(:every_count), "every_count should be set to yearly_day_of_week"
attributes = {
'recurring_period' => 'yearly',
'description' => 'a repeating todo', # generic
'yearly_selector' => 'yearly_every_xth_day', # daily specific --> mapped to only_work_days=false
'yearly_month_of_year' => '1', # ignored because yearly_selector is yearly_every_xth_day
'yearly_month_of_year2' => '2' # mapped to evert_other2 because yearly_selector is yearly_every_xth_day
}
pattern = YearlyRecurringTodosBuilder.new(@admin, Tracks::AttributeHandler.new(@admin, attributes))
assert_equal '2', pattern.mapped_attributes.get(:every_other2), "every_other2 should be set to yearly_month_of_year2 because selector is yearly_every_xth_day"
end
end
end

View file

@ -0,0 +1,115 @@
require_relative '../../test_helper'
module RecurringTodos
class YearlyRepeatPatternTest < ActiveSupport::TestCase
fixtures :users
def setup
super
@admin = users(:admin_user)
end
def test_attribute_mapping
builder = RecurringTodosBuilder.new(@admin, {
'recurring_period' => 'yearly',
'description' => 'a repeating todo', # generic
'recurring_target' => 'show_from_date',
'ends_on' => 'ends_on_end_date',
'end_date' => Time.zone.now + 1.week,
'start_from' => Time.zone.now,
'context_name' => @admin.contexts.first.name,
'yearly_selector' => 'yearly_every_x_day',
'yearly_every_x_day' => 5,
'yearly_every_xth_day' => 6,
'yearly_month_of_year' => 7,
'yearly_month_of_year2' => 8,
'yearly_day_of_week' => 9
})
assert builder.save, "should save: #{builder.errors.full_messages}"
rt = builder.saved_recurring_todo
assert builder.pattern.is_a?(YearlyRepeatPattern), "should be monthly pattern, but is #{builder.pattern.class}"
assert_equal rt.recurrence_selector, builder.pattern.recurrence_selector
assert_equal rt.every_other2, builder.pattern.month_of_year
assert_equal rt.every_other1, builder.pattern.every_x_day
assert_equal rt.every_other3, builder.pattern.every_xth_day
assert_equal rt.every_count, builder.pattern.day_of_week
assert_equal Time.zone.now.month, builder.pattern.month_of_year2, "uses default for moy2, which is current month"
rt.recurrence_selector = 1 # 'yearly_every_xth_day'
assert_equal rt.every_other2, rt.pattern.month_of_year2, "uses every_other2 for moy2 when yearly_every_xth_day"
end
def test_validations
rt = @admin.recurring_todos.where(recurring_period: 'yearly').first
assert rt.valid?, "should be valid at start: #{rt.errors.full_messages}"
rt.recurrence_selector = 0 # 'yearly_every_x_day'
rt.every_other1 = nil
assert !rt.valid?, "should not be valid since every_x_day is empty"
rt.every_other1 = 1
rt.every_other2 = nil
assert !rt.valid?, "should not be valid since month_of_year is empty"
rt.recurrence_selector = 1 # 'yearly_every_xth_day'
rt.every_other2 = nil
assert !rt.valid?, "should not be valid since month_of_year2 is empty"
rt.every_other2 = 1
rt.every_other3 = nil
assert !rt.valid?, "should not be valid since every_xth_day is empty"
rt.every_other3 = 1
rt.every_count = nil
assert !rt.valid?, "should not be valid since day_of_week is empty"
end
def test_pattern_text
rt = recurring_todos(:birthday_reinier)
assert_equal "every year on June 08", rt.recurrence_pattern
rt.recurrence_selector = 1
rt.every_count = 3
rt.every_other3 = 3
assert_equal "every year on the third wednesday of June", rt.recurrence_pattern
end
def test_yearly_pattern
@yearly = recurring_todos(:birthday_reinier)
# beginning of same year
due_date = @yearly.get_due_date(Time.zone.local(2008,2,10)) # feb 10th
assert_equal @sunday, due_date # june 8th
# same month, previous date
due_date = @yearly.get_due_date(@saturday) # june 7th
show_from_date = @yearly.get_show_from_date(@saturday) # june 7th
assert_equal @sunday, due_date # june 8th
assert_equal @sunday-5.days, show_from_date
# same month, day after
due_date = @yearly.get_due_date(@monday) # june 9th
assert_equal Time.zone.local(2009,6,8), due_date # june 8th next year
# very overdue
due_date = @yearly.get_due_date(@monday+5.months-2.days) # november 7
assert_equal Time.zone.local(2009,6,8), due_date # june 8th next year
@yearly.recurrence_selector = 1
@yearly.every_other3 = 2 # second
@yearly.every_count = 3 # wednesday
# beginning of same year
due_date = @yearly.get_due_date(Time.zone.local(2008,2,10)) # feb 10th
assert_equal Time.zone.local(2008,6,11), due_date # june 11th
# same month, before second wednesday
due_date = @yearly.get_due_date(@saturday) # june 7th
assert_equal Time.zone.local(2008,6,11), due_date # june 11th
# same month, after second wednesday
due_date = @yearly.get_due_date(Time.zone.local(2008,6,12)) # june 7th
assert_equal Time.zone.local(2009,6,10), due_date # june 10th
end
end
end

View file

@ -13,7 +13,22 @@ class ActiveSupport::TestCase
# Note: You'll currently still have to declare fixtures explicitly in integration tests
# -- they do not yet inherit this setting
fixtures :all
def setup
@today = Time.now.utc
@tomorrow = @today + 1.day
@in_three_days = @today + 3.days
@in_four_days = @in_three_days + 1.day # need a day after start_from
@friday = Time.zone.local(2008,6,6)
@saturday = Time.zone.local(2008,6,7)
@sunday = Time.zone.local(2008,6,8) # june 8, 2008 was a sunday
@monday = Time.zone.local(2008,6,9)
@tuesday = Time.zone.local(2008,6,10)
@wednesday = Time.zone.local(2008,6,11)
@thursday = Time.zone.local(2008,6,12)
end
# Add more helper methods to be used by all tests here...
def assert_value_changed(object, method = nil)
initial_value = object.send(method)