mirror of
https://github.com/TracksApp/tracks.git
synced 2025-12-25 11:38:49 +01:00
Merge branch 'master' into new-gui
Conflicts: Gemfile.lock
This commit is contained in:
commit
fdd4c267b3
39 changed files with 2824 additions and 1022 deletions
1
Gemfile
1
Gemfile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
53
app/controllers/recurring_todos/form_helper.rb
Normal file
53
app/controllers/recurring_todos/form_helper.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def age_in_days
|
||||
@age_in_days ||= ((Time.now.utc - created_at).to_i / 1.day) + 1
|
||||
@age_in_days ||= (Time.current.to_date - created_at.to_date).to_i + 1
|
||||
end
|
||||
|
||||
def self.import(filename, params, user)
|
||||
|
|
|
|||
|
|
@ -29,86 +29,23 @@ class RecurringTodo < ActiveRecord::Base
|
|||
validates_length_of :description, :maximum => 100
|
||||
validates_length_of :notes, :maximum => 60000, :allow_nil => true
|
||||
|
||||
validate :period_specific_validations
|
||||
validate :starts_and_ends_on_validations
|
||||
validate :set_recurrence_on_validations
|
||||
validate :period_validation
|
||||
validate :pattern_specific_validations
|
||||
|
||||
def period_specific_validations
|
||||
if %W[daily weekly monthly yearly].include?(recurring_period)
|
||||
self.send("validate_#{recurring_period}")
|
||||
def pattern_specific_validations
|
||||
if pattern
|
||||
pattern.validate
|
||||
else
|
||||
errors.add(:recurring_period, "is an unknown recurrence pattern: '#{recurring_period}'")
|
||||
errors[:recurring_todo] << "Invalid recurrence period '#{recurring_period}'"
|
||||
end
|
||||
end
|
||||
|
||||
def validate_daily
|
||||
if (!only_work_days) && daily_every_x_days.blank?
|
||||
errors[:base] << "Every other nth day may not be empty for recurrence setting"
|
||||
end
|
||||
def valid_period?
|
||||
%W[daily weekly monthly yearly].include?(recurring_period)
|
||||
end
|
||||
|
||||
def validate_weekly
|
||||
if weekly_every_x_week.blank?
|
||||
errors[:base] << "Every other nth week may not be empty for recurrence setting"
|
||||
end
|
||||
something_set = false
|
||||
%w{sunday monday tuesday wednesday thursday friday saturday}.each { |day| something_set ||= self.send("on_#{day}") }
|
||||
errors[:base] << "You must specify at least one day on which the todo recurs" unless something_set
|
||||
end
|
||||
|
||||
def validate_monthly
|
||||
case recurrence_selector
|
||||
when 0 # 'monthly_every_x_day'
|
||||
errors[:base] << "The day of the month may not be empty for recurrence setting" if monthly_every_x_day.blank?
|
||||
errors[:base] << "Every other nth month may not be empty for recurrence setting" if monthly_every_x_month.blank?
|
||||
when 1 # 'monthly_every_xth_day'
|
||||
errors[:base] <<"Every other nth month may not be empty for recurrence setting" if monthly_every_x_month2.blank?
|
||||
errors[:base] <<"The nth day of the month may not be empty for recurrence setting" if monthly_every_xth_day.blank?
|
||||
errors[:base] <<"The day of the month may not be empty for recurrence setting" if monthly_day_of_week.blank?
|
||||
else
|
||||
raise Exception.new, "unexpected value of recurrence selector '#{recurrence_selector}'"
|
||||
end
|
||||
end
|
||||
|
||||
def validate_yearly
|
||||
case recurrence_selector
|
||||
when 0 # 'yearly_every_x_day'
|
||||
errors[:base] << "The month of the year may not be empty for recurrence setting" if yearly_month_of_year.blank?
|
||||
errors[:base] << "The day of the month may not be empty for recurrence setting" if yearly_every_x_day.blank?
|
||||
when 1 # 'yearly_every_xth_day'
|
||||
errors[:base] << "The month of the year may not be empty for recurrence setting" if yearly_month_of_year2.blank?
|
||||
errors[:base] << "The nth day of the month may not be empty for recurrence setting" if yearly_every_xth_day.blank?
|
||||
errors[:base] << "The day of the week may not be empty for recurrence setting" if yearly_day_of_week.blank?
|
||||
else
|
||||
raise Exception.new, "unexpected value of recurrence selector '#{recurrence_selector}'"
|
||||
end
|
||||
end
|
||||
|
||||
def starts_and_ends_on_validations
|
||||
errors[:base] << "The start date needs to be filled in" if start_from.blank?
|
||||
case ends_on
|
||||
when 'ends_on_number_of_times'
|
||||
errors[:base] << "The number of recurrences needs to be filled in for 'Ends on'" if number_of_occurences.blank?
|
||||
when "ends_on_end_date"
|
||||
errors[:base] << "The end date needs to be filled in for 'Ends on'" if end_date.blank?
|
||||
else
|
||||
errors[:base] << "The end of the recurrence is not selected" unless ends_on == "no_end_date"
|
||||
end
|
||||
end
|
||||
|
||||
def set_recurrence_on_validations
|
||||
# show always or x days before due date. x not null
|
||||
case target
|
||||
when 'show_from_date'
|
||||
# no validations
|
||||
when 'due_date'
|
||||
errors[:base] << "Please select when to show the action" if show_always.nil?
|
||||
unless show_always
|
||||
errors[:base] << "Please fill in the number of days to show the todo before the due date" if show_from_delta.blank?
|
||||
end
|
||||
else
|
||||
raise Exception.new, "unexpected value of recurrence target selector '#{recurrence_target}'"
|
||||
end
|
||||
def period_validation
|
||||
errors.add(:recurring_period, "is an unknown recurrence pattern: '#{recurring_period}'") unless valid_period?
|
||||
end
|
||||
|
||||
# the following recurrence patterns can be stored:
|
||||
|
|
@ -133,262 +70,20 @@ class RecurringTodo < ActiveRecord::Base
|
|||
# choosing between both options is done on recurrence_selector where 0 is
|
||||
# for first type and 1 for second type
|
||||
|
||||
# DAILY
|
||||
|
||||
def daily_selector=(selector)
|
||||
case selector
|
||||
when 'daily_every_x_day'
|
||||
only_work_days = false
|
||||
when 'daily_every_work_day'
|
||||
only_work_days = true
|
||||
else
|
||||
raise Exception.new, "unknown daily recurrence pattern: '#{selector}'"
|
||||
end
|
||||
end
|
||||
|
||||
def daily_every_x_days=(x)
|
||||
self.every_other1 = x if recurring_period=='daily'
|
||||
end
|
||||
|
||||
def daily_every_x_days
|
||||
every_other1
|
||||
end
|
||||
|
||||
# WEEKLY
|
||||
|
||||
def weekly_every_x_week=(x)
|
||||
self.every_other1 = x if recurring_period=='weekly'
|
||||
end
|
||||
|
||||
def weekly_every_x_week
|
||||
self.every_other1
|
||||
end
|
||||
|
||||
def switch_week_day(day, position)
|
||||
self.every_day = ' ' if self.every_day.nil?
|
||||
self.every_day = every_day[0, position] + day + every_day[position+1, every_day.length]
|
||||
end
|
||||
|
||||
{ monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 0 }.each do |day, number|
|
||||
define_method("weekly_return_#{day}=") do |selector|
|
||||
switch_week_day(selector, number) if recurring_period=='weekly'
|
||||
end
|
||||
|
||||
define_method("on_#{day}") do
|
||||
on_xday number
|
||||
end
|
||||
end
|
||||
|
||||
def on_xday(n)
|
||||
every_day && every_day[n, 1] != ' '
|
||||
end
|
||||
|
||||
# MONTHLY
|
||||
|
||||
def monthly_selector=(selector)
|
||||
self.recurrence_selector = ( (selector=='monthly_every_x_day') ? 0 : 1) if recurring_period=='monthly'
|
||||
end
|
||||
|
||||
def monthly_every_x_day=(x)
|
||||
self.every_other1 = x if recurring_period=='monthly'
|
||||
end
|
||||
|
||||
def monthly_every_x_day
|
||||
self.every_other1
|
||||
end
|
||||
|
||||
def is_monthly_every_x_day
|
||||
recurring_period == 'monthly' && self.recurrence_selector == 0
|
||||
end
|
||||
|
||||
def is_monthly_every_xth_day
|
||||
recurring_period == 'monthly' && self.recurrence_selector == 1
|
||||
end
|
||||
|
||||
def monthly_every_x_month=(x)
|
||||
self.every_other2 = x if recurring_period=='monthly' && recurrence_selector == 0
|
||||
end
|
||||
|
||||
def monthly_every_x_month
|
||||
# in case monthly pattern is every day x, return every_other2 otherwise
|
||||
# return a default value
|
||||
self.recurrence_selector == 0 ? self.every_other2 : 1
|
||||
end
|
||||
|
||||
def monthly_every_x_month2=(x)
|
||||
self.every_other2 = x if recurring_period=='monthly' && recurrence_selector == 1
|
||||
end
|
||||
|
||||
def monthly_every_x_month2
|
||||
# in case monthly pattern is every xth day, return every_other2 otherwise
|
||||
# return a default value
|
||||
self.recurrence_selector == 1 ? self.every_other2 : 1
|
||||
end
|
||||
|
||||
def monthly_every_xth_day=(x)
|
||||
self.every_other3 = x if recurring_period=='monthly'
|
||||
end
|
||||
|
||||
def monthly_every_xth_day(default=nil)
|
||||
self.every_other3 || default
|
||||
end
|
||||
|
||||
def monthly_day_of_week=(dow)
|
||||
self.every_count = dow if recurring_period=='monthly'
|
||||
end
|
||||
|
||||
def monthly_day_of_week
|
||||
self.every_count
|
||||
end
|
||||
|
||||
# YEARLY
|
||||
|
||||
def yearly_selector=(selector)
|
||||
self.recurrence_selector = ( (selector=='yearly_every_x_day') ? 0 : 1) if recurring_period=='yearly'
|
||||
end
|
||||
|
||||
def yearly_month_of_year=(moy)
|
||||
self.every_other2 = moy if self.recurring_period=='yearly' && self.recurrence_selector == 0
|
||||
end
|
||||
|
||||
def yearly_month_of_year
|
||||
# if recurrence pattern is every x day in a month, return month otherwise
|
||||
# return a default value
|
||||
self.recurrence_selector == 0 ? self.every_other2 : Time.zone.now.month
|
||||
end
|
||||
|
||||
def yearly_month_of_year2=(moy)
|
||||
self.every_other2 = moy if self.recurring_period=='yearly' && self.recurrence_selector == 1
|
||||
end
|
||||
|
||||
def yearly_month_of_year2
|
||||
# if recurrence pattern is every xth day in a month, return month otherwise
|
||||
# return a default value
|
||||
self.recurrence_selector == 1 ? self.every_other2 : Time.zone.now.month
|
||||
end
|
||||
|
||||
def yearly_every_x_day=(x)
|
||||
self.every_other1 = x if recurring_period=='yearly'
|
||||
end
|
||||
|
||||
def yearly_every_x_day
|
||||
self.every_other1
|
||||
end
|
||||
|
||||
def yearly_every_xth_day=(x)
|
||||
self.every_other3 = x if recurring_period=='yearly'
|
||||
end
|
||||
|
||||
def yearly_every_xth_day
|
||||
self.every_other3
|
||||
end
|
||||
|
||||
def yearly_day_of_week=(dow)
|
||||
self.every_count=dow if recurring_period=='yearly'
|
||||
end
|
||||
|
||||
def yearly_day_of_week
|
||||
self.every_count
|
||||
end
|
||||
|
||||
# target
|
||||
|
||||
def recurring_target=(t)
|
||||
self.target = t
|
||||
end
|
||||
|
||||
def recurring_target_as_text
|
||||
case self.target
|
||||
when 'due_date'
|
||||
I18n.t("todos.recurrence.pattern.due")
|
||||
when 'show_from_date'
|
||||
I18n.t("todos.recurrence.pattern.show")
|
||||
else
|
||||
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
|
||||
end
|
||||
end
|
||||
|
||||
def recurring_show_days_before=(days)
|
||||
self.show_from_delta=days
|
||||
end
|
||||
|
||||
def recurring_show_always=(value)
|
||||
self.show_always=value
|
||||
end
|
||||
|
||||
def daily_recurrence_pattern
|
||||
if only_work_days
|
||||
I18n.t("todos.recurrence.pattern.on_work_days")
|
||||
elsif every_other1 > 1
|
||||
I18n.t("todos.recurrence.pattern.every_n", :n => every_other1) + " " + I18n.t("common.days_midsentence.other")
|
||||
else
|
||||
I18n.t("todos.recurrence.pattern.every_day")
|
||||
end
|
||||
end
|
||||
|
||||
def weekly_recurrence_pattern
|
||||
if every_other1 > 1
|
||||
I18n.t("todos.recurrence.pattern.every_n", :n => every_other1) + " " + I18n.t("common.weeks")
|
||||
else
|
||||
I18n.t('todos.recurrence.pattern.weekly')
|
||||
end
|
||||
end
|
||||
|
||||
def monthly_recurrence_pattern
|
||||
return "invalid repeat pattern" if every_other2.nil?
|
||||
if self.recurrence_selector == 0
|
||||
on_day = " #{I18n.t('todos.recurrence.pattern.on_day_n', :n => self.every_other1)}"
|
||||
if self.every_other2>1
|
||||
I18n.t("todos.recurrence.pattern.every_n", :n => self.every_other2) + " " + I18n.t('common.months') + on_day
|
||||
else
|
||||
I18n.t("todos.recurrence.pattern.every_month") + on_day
|
||||
end
|
||||
else
|
||||
n_months = if self.every_other2 > 1
|
||||
"#{self.every_other2} #{I18n.t('common.months')}"
|
||||
else
|
||||
I18n.t('common.month')
|
||||
end
|
||||
I18n.t('todos.recurrence.pattern.every_xth_day_of_every_n_months',
|
||||
:x => self.xth, :day => self.day_of_week, :n_months => n_months)
|
||||
end
|
||||
end
|
||||
|
||||
def yearly_recurrence_pattern
|
||||
if self.recurrence_selector == 0
|
||||
I18n.t("todos.recurrence.pattern.every_year_on",
|
||||
:date => I18n.l(DateTime.new(Time.zone.now.year, self.every_other2, self.every_other1), :format => :month_day))
|
||||
else
|
||||
I18n.t("todos.recurrence.pattern.every_year_on",
|
||||
:date => I18n.t("todos.recurrence.pattern.the_xth_day_of_month", :x => self.xth, :day => self.day_of_week, :month => self.month_of_year))
|
||||
def pattern
|
||||
if valid_period?
|
||||
@pattern = eval("RecurringTodos::#{recurring_period.capitalize}RepeatPattern.new(user)")
|
||||
@pattern.build_from_recurring_todo(self)
|
||||
end
|
||||
@pattern
|
||||
end
|
||||
|
||||
def recurrence_pattern
|
||||
return "invalid repeat pattern" if every_other1.nil?
|
||||
case recurring_period
|
||||
when 'daily' then daily_recurrence_pattern
|
||||
when 'weekly' then weekly_recurrence_pattern
|
||||
when 'monthly' then monthly_recurrence_pattern
|
||||
when 'yearly' then yearly_recurrence_pattern
|
||||
else
|
||||
'unknown recurrence pattern: period unknown'
|
||||
end
|
||||
pattern.recurrence_pattern
|
||||
end
|
||||
|
||||
def xth
|
||||
xth_day = [
|
||||
I18n.t('todos.recurrence.pattern.first'),I18n.t('todos.recurrence.pattern.second'),I18n.t('todos.recurrence.pattern.third'),
|
||||
I18n.t('todos.recurrence.pattern.fourth'),I18n.t('todos.recurrence.pattern.last')]
|
||||
self.every_other3.nil? ? '??' : xth_day[self.every_other3-1]
|
||||
end
|
||||
|
||||
def day_of_week
|
||||
self.every_count.nil? ? '??' : I18n.t('todos.recurrence.pattern.day_names')[self.every_count]
|
||||
end
|
||||
|
||||
def month_of_year
|
||||
self.every_other2.nil? ? '??' : I18n.t('todos.recurrence.pattern.month_names')[self.every_other2]
|
||||
def recurring_target_as_text
|
||||
pattern.recurring_target_as_text
|
||||
end
|
||||
|
||||
def starred?
|
||||
|
|
@ -396,214 +91,11 @@ class RecurringTodo < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def get_due_date(previous)
|
||||
case self.target
|
||||
when 'due_date'
|
||||
get_next_date(previous)
|
||||
when 'show_from_date'
|
||||
# so leave due date empty
|
||||
nil
|
||||
else
|
||||
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
|
||||
end
|
||||
pattern.get_due_date(previous)
|
||||
end
|
||||
|
||||
def get_show_from_date(previous)
|
||||
case self.target
|
||||
when 'due_date'
|
||||
# so set show from date relative to due date unless show_always is true or show_from_delta is nil
|
||||
(self.show_always? || self.show_from_delta.nil?) ? nil : get_due_date(previous) - self.show_from_delta.days
|
||||
when 'show_from_date'
|
||||
# Leave due date empty
|
||||
get_next_date(previous)
|
||||
else
|
||||
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
|
||||
end
|
||||
end
|
||||
|
||||
def get_next_date(previous)
|
||||
case self.recurring_period
|
||||
when 'daily' then get_daily_date(previous)
|
||||
when 'weekly' then get_weekly_date(previous)
|
||||
when 'monthly' then get_monthly_date(previous)
|
||||
when 'yearly' then get_yearly_date(previous)
|
||||
else
|
||||
raise Exception.new, "unknown recurrence pattern: '#{self.recurring_period}'"
|
||||
end
|
||||
end
|
||||
|
||||
def get_daily_date(previous)
|
||||
# previous is the due date of the previous todo or it is the completed_at
|
||||
# date when the completed_at date is after due_date (i.e. you did not make
|
||||
# the due date in time)
|
||||
#
|
||||
# assumes self.recurring_period == 'daily'
|
||||
|
||||
start = determine_start(previous, 1.day)
|
||||
|
||||
if self.only_work_days
|
||||
return start + 2.day if start.wday() == 6 # saturday
|
||||
return start + 1.day if start.wday() == 0 # sunday
|
||||
return start
|
||||
else # every nth day; n = every_other1
|
||||
# if there was no previous todo, do not add n: the first todo starts on
|
||||
# today or on start_from
|
||||
return previous == nil ? start : start+every_other1.day-1.day
|
||||
end
|
||||
end
|
||||
|
||||
def get_weekly_date(previous)
|
||||
# determine start
|
||||
if previous == nil
|
||||
start = self.start_from.nil? ? Time.zone.now : self.start_from
|
||||
else
|
||||
start = previous + 1.day
|
||||
if start.wday() == 0
|
||||
# we went to a new week , go to the nth next week and find first match
|
||||
# that week. Note that we already went into the next week, so -1
|
||||
start += (self.every_other1-1).week
|
||||
end
|
||||
unless self.start_from.nil?
|
||||
# check if the start_from date is later than previous. If so, use
|
||||
# start_from as start to search for next date
|
||||
start = self.start_from if self.start_from > previous
|
||||
end
|
||||
end
|
||||
|
||||
day = find_first_day_in_this_week(start)
|
||||
return day unless day == -1
|
||||
|
||||
# we did not find anything this week, so check the nth next, starting from
|
||||
# sunday
|
||||
start = start + self.every_other1.week - (start.wday()).days
|
||||
|
||||
start = find_first_day_in_this_week(start)
|
||||
return start unless start == -1
|
||||
|
||||
raise Exception.new, "unable to find next weekly date (#{self.every_day})"
|
||||
end
|
||||
|
||||
def get_monthly_date(previous)
|
||||
start = determine_start(previous)
|
||||
day = self.every_other1
|
||||
n = self.every_other2
|
||||
|
||||
case self.recurrence_selector
|
||||
when 0 # specific day of the month
|
||||
if (previous && start.mday >= day) || (previous.nil? && start.mday > day)
|
||||
# there is no next day n in this month, search in next month
|
||||
#
|
||||
# start += n.months
|
||||
#
|
||||
# The above seems to not work. Fiddle with timezone. Looks like we hit a
|
||||
# bug in rails here where 2008-12-01 +0100 plus 1.month becomes
|
||||
# 2008-12-31 +0100. For now, just calculate in UTC and convert back to
|
||||
# local timezone.
|
||||
#
|
||||
# TODO: recheck if future rails versions have this problem too
|
||||
start = Time.utc(start.year, start.month, start.day)+n.months
|
||||
start = Time.zone.local(start.year, start.month, start.day)
|
||||
|
||||
# go back to day
|
||||
end
|
||||
Time.zone.local(start.year, start.month, day)
|
||||
|
||||
when 1 # relative weekday of a month
|
||||
the_next = get_xth_day_of_month(self.every_other3, self.every_count, start.month, start.year)
|
||||
if the_next.nil? || the_next <= start
|
||||
# the nth day is already passed in this month, go to next month and try
|
||||
# again
|
||||
|
||||
# fiddle with timezone. Looks like we hit a bug in rails here where
|
||||
# 2008-12-01 +0100 plus 1.month becomes 2008-12-31 +0100. For now, just
|
||||
# calculate in UTC and convert back to local timezone.
|
||||
# TODO: recheck if future rails versions have this problem too
|
||||
the_next = Time.utc(the_next.year, the_next.month, the_next.day)+n.months
|
||||
the_next = Time.zone.local(the_next.year, the_next.month, the_next.day)
|
||||
|
||||
# TODO: if there is still no match, start will be set to nil. if we ever
|
||||
# support 5th day of the month, we need to handle this case
|
||||
the_next = get_xth_day_of_month(self.every_other3, self.every_count, the_next.month, the_next.year)
|
||||
end
|
||||
the_next
|
||||
else
|
||||
raise Exception.new, "unknown monthly recurrence selection (#{self.recurrence_selector})"
|
||||
end
|
||||
end
|
||||
|
||||
def get_xth_day_of_month(x, weekday, month, year)
|
||||
if x == 5
|
||||
# last -> count backwards. use UTC to avoid strange timezone oddities
|
||||
# where last_day -= 1.day seems to shift tz+0100 to tz+0000
|
||||
last_day = Time.utc(year, month, Time.days_in_month(month))
|
||||
while last_day.wday != weekday
|
||||
last_day -= 1.day
|
||||
end
|
||||
# convert back to local timezone
|
||||
Time.zone.local(last_day.year, last_day.month, last_day.day)
|
||||
else
|
||||
# 1-4th -> count upwards last -> count backwards. use UTC to avoid strange
|
||||
# timezone oddities where last_day -= 1.day seems to shift tz+0100 to
|
||||
# tz+0000
|
||||
start = Time.utc(year,month,1)
|
||||
n = x
|
||||
while n > 0
|
||||
while start.wday() != weekday
|
||||
start+= 1.day
|
||||
end
|
||||
n -= 1
|
||||
start += 1.day unless n==0
|
||||
end
|
||||
# convert back to local timezone
|
||||
Time.zone.local(start.year, start.month, start.day)
|
||||
end
|
||||
end
|
||||
|
||||
def get_yearly_date(previous)
|
||||
start = determine_start(previous)
|
||||
day = self.every_other1
|
||||
month = self.every_other2
|
||||
|
||||
case self.recurrence_selector
|
||||
when 0 # specific day of a specific month
|
||||
if start.month > month || (start.month == month && start.day >= day)
|
||||
# if there is no next month n and day m in this year, search in next
|
||||
# year
|
||||
start = Time.zone.local(start.year+1, month, 1)
|
||||
else
|
||||
# if there is a next month n, stay in this year
|
||||
start = Time.zone.local(start.year, month, 1)
|
||||
end
|
||||
Time.zone.local(start.year, month, day)
|
||||
|
||||
when 1 # relative weekday of a specific month
|
||||
# if there is no next month n in this year, search in next year
|
||||
the_next = start.month > month ? Time.zone.local(start.year+1, month, 1) : start
|
||||
|
||||
# get the xth day of the month
|
||||
the_next = get_xth_day_of_month(self.every_other3, self.every_count, month, the_next.year)
|
||||
|
||||
# if the_next is before previous, we went back into the past, so try next
|
||||
# year
|
||||
the_next = get_xth_day_of_month(self.every_other3, self.every_count, month, start.year+1) if the_next <= start
|
||||
|
||||
the_next
|
||||
else
|
||||
raise Exception.new, "unknown monthly recurrence selection (#{self.recurrence_selector})"
|
||||
end
|
||||
end
|
||||
|
||||
def continues_recurring?(previous)
|
||||
return self.occurences_count < self.number_of_occurences unless self.number_of_occurences.nil?
|
||||
return true if self.end_date.nil? || self.ends_on == 'no_end_date'
|
||||
|
||||
case self.target
|
||||
when 'due_date'
|
||||
get_due_date(previous) <= self.end_date
|
||||
when 'show_from_date'
|
||||
get_show_from_date(previous) <= self.end_date
|
||||
else
|
||||
raise Exception.new, "unexpected value of recurrence target '#{self.target}'"
|
||||
end
|
||||
pattern.get_show_from_date(previous)
|
||||
end
|
||||
|
||||
def done?(end_date)
|
||||
|
|
@ -627,7 +119,7 @@ class RecurringTodo < ActiveRecord::Base
|
|||
def remove_from_project!
|
||||
self.project = nil
|
||||
self.save
|
||||
end
|
||||
end
|
||||
|
||||
def clear_todos_association
|
||||
unless todos.nil?
|
||||
|
|
@ -643,29 +135,8 @@ class RecurringTodo < ActiveRecord::Base
|
|||
self.save
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Determine start date to calculate next date for recurring todo
|
||||
# offset needs to be 1.day for daily patterns
|
||||
def determine_start(previous, offset=0.day)
|
||||
start = self.start_from || NullTime.new
|
||||
now = Time.zone.now
|
||||
if previous
|
||||
# check if the start_from date is later than previous. If so, use
|
||||
# start_from as start to search for next date
|
||||
start > previous ? start : previous + offset
|
||||
else
|
||||
# skip to present
|
||||
start > now ? start : now
|
||||
end
|
||||
def continues_recurring?(previous)
|
||||
pattern.continues_recurring?(previous)
|
||||
end
|
||||
|
||||
def find_first_day_in_this_week(start)
|
||||
# check if there are any days left this week for the next todo
|
||||
start.wday().upto 6 do |i|
|
||||
return start + (i-start.wday()).days unless self.every_day[i,1] == ' '
|
||||
end
|
||||
-1
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
152
app/models/recurring_todos/abstract_recurring_todos_builder.rb
Normal file
152
app/models/recurring_todos/abstract_recurring_todos_builder.rb
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
module RecurringTodos
|
||||
|
||||
class AbstractRecurringTodosBuilder
|
||||
|
||||
attr_reader :mapped_attributes, :pattern
|
||||
|
||||
def initialize(user, attributes, pattern_class)
|
||||
@user = user
|
||||
@saved = false
|
||||
|
||||
@attributes = attributes
|
||||
@selector = get_selector(selector_key)
|
||||
@filterred_attributes = filter_attributes(@attributes)
|
||||
@mapped_attributes = map_attributes(@filterred_attributes)
|
||||
|
||||
@pattern = pattern_class.new(user)
|
||||
@pattern.attributes = @mapped_attributes
|
||||
|
||||
end
|
||||
|
||||
# build does not add tags. For tags, the recurring todos needs to be saved
|
||||
def build
|
||||
@recurring_todo = @pattern.build_recurring_todo(@mapped_attributes)
|
||||
end
|
||||
|
||||
def update(recurring_todo)
|
||||
@recurring_todo = @pattern.update_recurring_todo(recurring_todo, @mapped_attributes)
|
||||
save_recurring_todo
|
||||
end
|
||||
|
||||
def save
|
||||
build
|
||||
save_recurring_todo
|
||||
end
|
||||
|
||||
def save_project
|
||||
save_collection(:project, :project_id)
|
||||
end
|
||||
|
||||
def save_context
|
||||
save_collection(:context, :context_id)
|
||||
end
|
||||
|
||||
def saved_recurring_todo
|
||||
raise(Exception.new, @recurring_todo.valid? ? "Recurring todo was not saved yet" : "Recurring todos was not saved because of validation errors") unless @saved
|
||||
|
||||
@recurring_todo
|
||||
end
|
||||
|
||||
def attributes
|
||||
@pattern.attributes
|
||||
end
|
||||
|
||||
def errors
|
||||
@recurring_todo.try(:errors)
|
||||
end
|
||||
|
||||
def attributes_to_filter
|
||||
raise Exception.new, "attributes_to_filter should be overridden"
|
||||
end
|
||||
|
||||
def filter_attributes(attributes)
|
||||
# get pattern independend attributes
|
||||
filterred_attributes = filter_generic_attributes(attributes)
|
||||
# append pattern specific attributes
|
||||
attributes_to_filter.each{|key| filterred_attributes[key]= attributes[key] if attributes.key?(key)}
|
||||
|
||||
filterred_attributes
|
||||
end
|
||||
|
||||
def filter_generic_attributes(attributes)
|
||||
return Tracks::AttributeHandler.new(@user, {
|
||||
recurring_period: attributes[:recurring_period],
|
||||
description: attributes[:description],
|
||||
notes: attributes[:notes],
|
||||
tag_list: tag_list_or_empty_string(attributes),
|
||||
start_from: attributes[:start_from],
|
||||
end_date: attributes[:end_date],
|
||||
ends_on: attributes[:ends_on],
|
||||
target: attributes[:target],
|
||||
project: attributes[:project],
|
||||
context: attributes[:context],
|
||||
project_id: attributes[:project_id],
|
||||
context_id: attributes[:context_id],
|
||||
target: attributes[:recurring_target],
|
||||
show_from_delta: attributes[:recurring_show_days_before],
|
||||
show_always: attributes[:recurring_show_always]
|
||||
})
|
||||
end
|
||||
|
||||
def map_attributes
|
||||
# should be overwritten by subclasses to map attributes to activerecord model attributes
|
||||
@filterred_attributes
|
||||
end
|
||||
|
||||
# helper method to be used in mapped_attributes in subclasses
|
||||
# changes name of key from source_key to key
|
||||
def map(mapping, key, source_key)
|
||||
mapping[key] = mapping[source_key]
|
||||
mapping.except(source_key)
|
||||
end
|
||||
|
||||
# should return period specific selector like yearly_selector or daily_selector
|
||||
def selector_key
|
||||
raise Exception.new, "selector_key should be overridden in subclass of AbstractRecurringTodosBuilder"
|
||||
end
|
||||
|
||||
def get_selector(key)
|
||||
return nil if key.nil?
|
||||
|
||||
raise Exception.new, "recurrence selector pattern (#{key}) not given" unless @attributes.selector_key_present?(key)
|
||||
selector = @attributes[key]
|
||||
|
||||
raise Exception.new, "unknown recurrence selector pattern: '#{selector}'" unless valid_selector?(selector)
|
||||
|
||||
@attributes = @attributes.except(key)
|
||||
return selector
|
||||
end
|
||||
|
||||
def valid_selector?(selector)
|
||||
raise Exception.new, "valid_selector? should be overridden in subclass of AbstractRecurringTodosBuilder"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def save_recurring_todo
|
||||
@saved = @recurring_todo.save
|
||||
save_tags if @saved
|
||||
return @saved
|
||||
end
|
||||
|
||||
def save_tags
|
||||
@recurring_todo.tag_with(@filterred_attributes[:tag_list]) if @filterred_attributes[:tag_list].present?
|
||||
@recurring_todo.reload
|
||||
end
|
||||
|
||||
def save_collection(collection, collection_id)
|
||||
# save object (project or context) and add its id to @mapped_attributes and remove the object from the attributes
|
||||
object = @mapped_attributes[collection]
|
||||
object.save
|
||||
@mapped_attributes[collection_id] = object.id
|
||||
@mapped_attributes.except(collection)
|
||||
end
|
||||
|
||||
def tag_list_or_empty_string(attributes)
|
||||
# avoid nil
|
||||
attributes[:tag_list].blank? ? "" : attributes[:tag_list].strip
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
226
app/models/recurring_todos/abstract_repeat_pattern.rb
Normal file
226
app/models/recurring_todos/abstract_repeat_pattern.rb
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
module RecurringTodos
|
||||
|
||||
class AbstractRepeatPattern
|
||||
|
||||
attr_accessor :attributes
|
||||
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def start_from
|
||||
get :start_from
|
||||
end
|
||||
|
||||
def end_date
|
||||
get :end_date
|
||||
end
|
||||
|
||||
def ends_on
|
||||
get :ends_on
|
||||
end
|
||||
|
||||
def target
|
||||
get :target
|
||||
end
|
||||
|
||||
def show_always?
|
||||
get :show_always
|
||||
end
|
||||
|
||||
def show_from_delta
|
||||
get :show_from_delta
|
||||
end
|
||||
|
||||
def recurring_target_as_text
|
||||
target == 'due_date' ? I18n.t("todos.recurrence.pattern.due") : I18n.t("todos.recurrence.pattern.show")
|
||||
end
|
||||
|
||||
def recurrence_pattern
|
||||
raise "Should not call AbstractRepeatPattern.recurrence_pattern directly. Overwrite in subclass"
|
||||
end
|
||||
|
||||
def xth(x)
|
||||
xth_day = [
|
||||
I18n.t('todos.recurrence.pattern.first'),I18n.t('todos.recurrence.pattern.second'),I18n.t('todos.recurrence.pattern.third'),
|
||||
I18n.t('todos.recurrence.pattern.fourth'),I18n.t('todos.recurrence.pattern.last')]
|
||||
x.nil? ? '??' : xth_day[x-1]
|
||||
end
|
||||
|
||||
def day_of_week_as_text(day)
|
||||
day.nil? ? '??' : I18n.t('todos.recurrence.pattern.day_names')[day]
|
||||
end
|
||||
|
||||
def month_of_year_as_text(month)
|
||||
month.nil? ? '??' : I18n.t('todos.recurrence.pattern.month_names')[month]
|
||||
end
|
||||
|
||||
def build_recurring_todo(attribute_handler)
|
||||
@recurring_todo = @user.recurring_todos.build(attribute_handler.safe_attributes)
|
||||
end
|
||||
|
||||
def update_recurring_todo(recurring_todo, attribute_handler)
|
||||
recurring_todo.assign_attributes(attribute_handler.safe_attributes)
|
||||
recurring_todo
|
||||
end
|
||||
|
||||
def build_from_recurring_todo(recurring_todo)
|
||||
@recurring_todo = recurring_todo
|
||||
@attributes = Tracks::AttributeHandler.new(@user, recurring_todo.attributes)
|
||||
end
|
||||
|
||||
def valid?
|
||||
@recurring_todo.valid?
|
||||
end
|
||||
|
||||
def validate_not_blank(object, msg)
|
||||
errors[:base] << msg if object.blank?
|
||||
end
|
||||
|
||||
def validate_not_nil(object, msg)
|
||||
errors[:base] << msg if object.nil?
|
||||
end
|
||||
|
||||
def validate
|
||||
starts_and_ends_on_validations
|
||||
set_recurrence_on_validations
|
||||
end
|
||||
|
||||
def starts_and_ends_on_validations
|
||||
validate_not_blank(start_from, "The start date needs to be filled in")
|
||||
case ends_on
|
||||
when 'ends_on_number_of_times'
|
||||
validate_not_blank(number_of_occurences, "The number of recurrences needs to be filled in for 'Ends on'")
|
||||
when "ends_on_end_date"
|
||||
validate_not_blank(end_date, "The end date needs to be filled in for 'Ends on'")
|
||||
else
|
||||
errors[:base] << "The end of the recurrence is not selected" unless ends_on == "no_end_date"
|
||||
end
|
||||
end
|
||||
|
||||
def set_recurrence_on_validations
|
||||
# show always or x days before due date. x not null
|
||||
case target
|
||||
when 'show_from_date'
|
||||
# no validations
|
||||
when 'due_date'
|
||||
validate_not_nil(show_always?, "Please select when to show the action")
|
||||
validate_not_blank(show_from_delta, "Please fill in the number of days to show the todo before the due date") unless show_always?
|
||||
else
|
||||
errors[:base] << "Unexpected value of recurrence target selector '#{target}'"
|
||||
end
|
||||
end
|
||||
|
||||
def errors
|
||||
@recurring_todo.errors
|
||||
end
|
||||
|
||||
def get(attribute)
|
||||
@attributes[attribute]
|
||||
end
|
||||
|
||||
# gets the next due date. returns nil if recurrence_target is not 'due_date'
|
||||
def get_due_date(previous)
|
||||
case target
|
||||
when 'due_date'
|
||||
get_next_date(previous)
|
||||
when 'show_from_date'
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def get_show_from_date(previous)
|
||||
case target
|
||||
when 'due_date'
|
||||
# so set show from date relative to due date unless show_always is true or show_from_delta is nil
|
||||
return nil unless put_in_tickler?
|
||||
get_due_date(previous) - show_from_delta.days
|
||||
when 'show_from_date'
|
||||
# Leave due date empty
|
||||
get_next_date(previous)
|
||||
end
|
||||
end
|
||||
|
||||
# checks if the next todos should be put in the tickler for recurrence_target == 'due_date'
|
||||
def put_in_tickler?
|
||||
!( show_always? || show_from_delta.nil?)
|
||||
end
|
||||
|
||||
def get_next_date(previous)
|
||||
raise "Should not call AbstractRepeatPattern.get_next_date directly. Overwrite in subclass"
|
||||
end
|
||||
|
||||
def continues_recurring?(previous)
|
||||
return @recurring_todo.occurences_count < @recurring_todo.number_of_occurences unless @recurring_todo.number_of_occurences.nil?
|
||||
return true if self.end_date.nil? || self.ends_on == 'no_end_date'
|
||||
|
||||
case self.target
|
||||
when 'due_date'
|
||||
get_due_date(previous) <= self.end_date
|
||||
when 'show_from_date'
|
||||
get_show_from_date(previous) <= self.end_date
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Determine start date to calculate next date for recurring todo which
|
||||
# takes start_from and previous into account.
|
||||
# offset needs to be 1.day for daily patterns or the start will be the
|
||||
# same day as the previous
|
||||
def determine_start(previous, offset=0.day)
|
||||
start = self.start_from || NullTime.new
|
||||
now = Time.zone.now
|
||||
if previous
|
||||
# check if the start_from date is later than previous. If so, use
|
||||
# start_from as start to search for next date
|
||||
start > previous ? start : previous + offset
|
||||
else
|
||||
# skip to present
|
||||
start > now ? start : now
|
||||
end
|
||||
end
|
||||
|
||||
# Example: get 3rd (x) wednesday (weekday) of december (month) 2014 (year)
|
||||
# 5th means last, so it will return the 4th if there is no 5th
|
||||
def get_xth_day_of_month(x, weekday, month, year)
|
||||
raise "Weekday should be between 0 and 6 with 0=sunday. You supplied #{weekday}" unless (0..6).include?(weekday)
|
||||
raise "x should be 1-4 for first-fourth or 5 for last. You supplied #{x}" unless (0..5).include?(x)
|
||||
|
||||
if x == 5
|
||||
return find_last_day_x_of_month(weekday, month, year)
|
||||
else
|
||||
return find_xth_day_of_month(x, weekday, month, year)
|
||||
end
|
||||
end
|
||||
|
||||
def find_last_day_x_of_month(weekday, month, year)
|
||||
# count backwards. use UTC to avoid strange timezone oddities
|
||||
# where last_day -= 1.day seems to shift tz+0100 to tz+0000
|
||||
last_day = Time.utc(year, month, Time.days_in_month(month))
|
||||
while last_day.wday != weekday
|
||||
last_day -= 1.day
|
||||
end
|
||||
# convert back to local timezone
|
||||
Time.zone.local(last_day.year, last_day.month, last_day.day)
|
||||
end
|
||||
|
||||
def find_xth_day_of_month(x, weekday, month, year)
|
||||
# 1-4th -> count upwards last -> count backwards. use UTC to avoid strange
|
||||
# timezone oddities where last_day -= 1.day seems to shift tz+0100 to
|
||||
# tz+0000
|
||||
start = Time.utc(year,month,1)
|
||||
n = x
|
||||
while n > 0
|
||||
while start.wday() != weekday
|
||||
start+= 1.day
|
||||
end
|
||||
n -= 1
|
||||
start+= 1.day unless n==0
|
||||
end
|
||||
# convert back to local timezone
|
||||
Time.zone.local(start.year, start.month, start.day)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
35
app/models/recurring_todos/daily_recurring_todos_builder.rb
Normal file
35
app/models/recurring_todos/daily_recurring_todos_builder.rb
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
module RecurringTodos
|
||||
|
||||
class DailyRecurringTodosBuilder < AbstractRecurringTodosBuilder
|
||||
attr_reader :recurring_todo, :pattern
|
||||
|
||||
def initialize(user, attributes)
|
||||
super(user, attributes, DailyRepeatPattern)
|
||||
end
|
||||
|
||||
def attributes_to_filter
|
||||
%w{daily_selector daily_every_x_days}
|
||||
end
|
||||
|
||||
def map_attributes(mapping)
|
||||
mapping.set(:only_work_days, only_work_days?(@selector))
|
||||
mapping.set(:every_other1, mapping.get(:daily_every_x_days))
|
||||
mapping.except(:daily_every_x_days)
|
||||
end
|
||||
|
||||
def only_work_days?(daily_selector)
|
||||
{ 'daily_every_x_day' => false,
|
||||
'daily_every_work_day' => true}[daily_selector]
|
||||
end
|
||||
|
||||
def selector_key
|
||||
:daily_selector
|
||||
end
|
||||
|
||||
def valid_selector?(selector)
|
||||
%w{daily_every_x_day daily_every_work_day}.include?(selector)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
52
app/models/recurring_todos/daily_repeat_pattern.rb
Normal file
52
app/models/recurring_todos/daily_repeat_pattern.rb
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
module RecurringTodos
|
||||
|
||||
class DailyRepeatPattern < AbstractRepeatPattern
|
||||
|
||||
def initialize(user)
|
||||
super user
|
||||
end
|
||||
|
||||
def every_x_days
|
||||
get :every_other1
|
||||
end
|
||||
|
||||
def only_work_days?
|
||||
get :only_work_days
|
||||
end
|
||||
|
||||
def recurrence_pattern
|
||||
if only_work_days?
|
||||
I18n.t("todos.recurrence.pattern.on_work_days")
|
||||
elsif every_x_days > 1
|
||||
I18n.t("todos.recurrence.pattern.every_n_days", :n => every_x_days)
|
||||
else
|
||||
I18n.t("todos.recurrence.pattern.every_day")
|
||||
end
|
||||
end
|
||||
|
||||
def validate
|
||||
super
|
||||
errors[:base] << "Every other nth day may not be empty for this daily recurrence setting" if (!only_work_days?) && every_x_days.blank?
|
||||
end
|
||||
|
||||
def get_next_date(previous)
|
||||
# previous is the due date of the previous todo or it is the completed_at
|
||||
# date when the completed_at date is after due_date (i.e. you did not make
|
||||
# the due date in time)
|
||||
|
||||
start = determine_start(previous, 1.day)
|
||||
|
||||
if only_work_days?
|
||||
# jump over weekend if necessary
|
||||
return start + 2.day if start.wday() == 6 # saturday
|
||||
return start + 1.day if start.wday() == 0 # sunday
|
||||
return start
|
||||
else
|
||||
# if there was no previous todo, do not add n: the first todo starts on
|
||||
# today or on start_from
|
||||
return previous == nil ? start : start+every_x_days.day-1.day
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
module RecurringTodos
|
||||
|
||||
class MonthlyRecurringTodosBuilder < AbstractRecurringTodosBuilder
|
||||
|
||||
def initialize(user, attributes)
|
||||
super(user, attributes, MonthlyRepeatPattern)
|
||||
end
|
||||
|
||||
def attributes_to_filter
|
||||
%w{
|
||||
monthly_selector monthly_every_x_day monthly_every_x_month
|
||||
monthly_every_x_month2 monthly_every_xth_day monthly_day_of_week
|
||||
}
|
||||
end
|
||||
|
||||
def map_attributes(mapping)
|
||||
mapping = map(mapping, :every_other1, 'monthly_every_x_day')
|
||||
mapping = map(mapping, :every_other3, 'monthly_every_xth_day')
|
||||
mapping = map(mapping, :every_count, 'monthly_day_of_week')
|
||||
|
||||
mapping.set(:recurrence_selector, get_recurrence_selector)
|
||||
|
||||
mapping.set(:every_other2, mapping.get(get_every_other2))
|
||||
mapping.except('monthly_every_x_month').except('monthly_every_x_month2')
|
||||
end
|
||||
|
||||
def get_recurrence_selector
|
||||
@selector=='monthly_every_x_day' ? 0 : 1
|
||||
end
|
||||
|
||||
def get_every_other2
|
||||
get_recurrence_selector == 0 ? 'monthly_every_x_month' : 'monthly_every_x_month2'
|
||||
end
|
||||
|
||||
def selector_key
|
||||
:monthly_selector
|
||||
end
|
||||
|
||||
def valid_selector?(selector)
|
||||
%w{monthly_every_x_day monthly_every_xth_day}.include?(selector)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
142
app/models/recurring_todos/monthly_repeat_pattern.rb
Normal file
142
app/models/recurring_todos/monthly_repeat_pattern.rb
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
module RecurringTodos
|
||||
|
||||
class MonthlyRepeatPattern < AbstractRepeatPattern
|
||||
|
||||
def initialize(user)
|
||||
super user
|
||||
end
|
||||
|
||||
def recurrence_selector
|
||||
get :recurrence_selector
|
||||
end
|
||||
|
||||
def every_x_day?
|
||||
get(:recurrence_selector) == 0
|
||||
end
|
||||
|
||||
def every_x_day
|
||||
get(:every_other1)
|
||||
end
|
||||
|
||||
def every_xth_day?
|
||||
get(:recurrence_selector) == 1
|
||||
end
|
||||
|
||||
def every_x_month
|
||||
# in case monthly pattern is every day x, return every_other2 otherwise
|
||||
# return a default value
|
||||
get(:recurrence_selector) == 0 ? get(:every_other2) : 1
|
||||
end
|
||||
|
||||
def every_x_month2
|
||||
# in case monthly pattern is every xth day, return every_other2 otherwise
|
||||
# return a default value
|
||||
get(:recurrence_selector) == 1 ? get(:every_other2) : 1
|
||||
end
|
||||
|
||||
def every_xth_day(default=nil)
|
||||
get(:every_other3) || default
|
||||
end
|
||||
|
||||
def day_of_week
|
||||
get :every_count
|
||||
end
|
||||
|
||||
def recurrence_pattern
|
||||
if recurrence_selector == 0
|
||||
recurrence_pattern_for_specific_day
|
||||
else
|
||||
recurrence_pattern_for_relative_day_in_month
|
||||
end
|
||||
end
|
||||
|
||||
def validate
|
||||
super
|
||||
|
||||
case recurrence_selector
|
||||
when 0 # 'monthly_every_x_day'
|
||||
validate_not_blank(every_x_month, "Every other nth month may not be empty for recurrence setting")
|
||||
when 1 # 'monthly_every_xth_day'
|
||||
validate_not_blank(every_x_month2, "Every other nth month may not be empty for recurrence setting")
|
||||
validate_not_blank(day_of_week, "The day of the month may not be empty for recurrence setting")
|
||||
else
|
||||
raise Exception.new, "unexpected value of recurrence selector '#{recurrence_selector}'"
|
||||
end
|
||||
end
|
||||
|
||||
def get_next_date(previous)
|
||||
start = determine_start(previous)
|
||||
n = get(:every_other2)
|
||||
|
||||
case recurrence_selector
|
||||
when 0 # specific day of the month
|
||||
return find_specific_day_of_month(previous, start, n)
|
||||
when 1 # relative weekday of a month
|
||||
return find_relative_day_of_month(start, n)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_specific_day_of_month(previous, start, n)
|
||||
if (previous && start.mday >= every_x_day) || (previous.nil? && start.mday > every_x_day)
|
||||
# there is no next day n in this month, search in next month
|
||||
#
|
||||
# start += n.months
|
||||
#
|
||||
# The above seems to not work. Fiddle with timezone. Looks like we hit a
|
||||
# bug in rails here where 2008-12-01 +0100 plus 1.month becomes
|
||||
# 2008-12-31 +0100. For now, just calculate in UTC and convert back to
|
||||
# local timezone.
|
||||
#
|
||||
# TODO: recheck if future rails versions have this problem too
|
||||
start = Time.utc(start.year, start.month, start.day)+n.months
|
||||
end
|
||||
Time.zone.local(start.year, start.month, every_x_day)
|
||||
end
|
||||
|
||||
def find_relative_day_of_month(start, n)
|
||||
the_next = get_xth_day_of_month(every_xth_day, day_of_week, start.month, start.year)
|
||||
if the_next.nil? || the_next <= start
|
||||
# the nth day is already passed in this month, go to next month and try
|
||||
# again
|
||||
|
||||
# fiddle with timezone. Looks like we hit a bug in rails here where
|
||||
# 2008-12-01 +0100 plus 1.month becomes 2008-12-31 +0100. For now, just
|
||||
# calculate in UTC and convert back to local timezone.
|
||||
# TODO: recheck if future rails versions have this problem too
|
||||
the_next = Time.utc(the_next.year, the_next.month, the_next.day)+n.months
|
||||
the_next = Time.zone.local(the_next.year, the_next.month, the_next.day)
|
||||
|
||||
# TODO: if there is still no match, start will be set to nil. if we ever
|
||||
# support 5th day of the month, we need to handle this case
|
||||
the_next = get_xth_day_of_month(every_xth_day, day_of_week, the_next.month, the_next.year)
|
||||
end
|
||||
the_next
|
||||
end
|
||||
|
||||
def recurrence_pattern_for_specific_day
|
||||
on_day = " #{I18n.t('todos.recurrence.pattern.on_day_n', :n => every_x_day)}"
|
||||
if every_xth_day(0) > 1
|
||||
I18n.t("todos.recurrence.pattern.every_n_months", :n => every_xth_day) + on_day
|
||||
else
|
||||
I18n.t("todos.recurrence.pattern.every_month") + on_day
|
||||
end
|
||||
end
|
||||
|
||||
def recurrence_pattern_for_relative_day_in_month
|
||||
n_months = if every_x_month2 > 1
|
||||
"#{every_x_month2} #{I18n.t('common.months')}"
|
||||
else
|
||||
I18n.t('common.month')
|
||||
end
|
||||
I18n.t('todos.recurrence.pattern.every_xth_day_of_every_n_months',
|
||||
x: xth(every_xth_day),
|
||||
day: day_of_week_as_text(day_of_week),
|
||||
n_months: n_months)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
78
app/models/recurring_todos/recurring_todos_builder.rb
Normal file
78
app/models/recurring_todos/recurring_todos_builder.rb
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
module RecurringTodos
|
||||
|
||||
class RecurringTodosBuilder
|
||||
|
||||
attr_reader :builder, :project, :context, :tag_list, :user
|
||||
|
||||
def initialize (user, attributes)
|
||||
@user = user
|
||||
@attributes = Tracks::AttributeHandler.new(@user, attributes)
|
||||
|
||||
parse_dates
|
||||
parse_project
|
||||
parse_context
|
||||
|
||||
@builder = create_builder(@attributes[:recurring_period])
|
||||
end
|
||||
|
||||
def create_builder(selector)
|
||||
raise "Unknown recurrence selector in :recurring_period (#{selector})" unless valid_selector? selector
|
||||
eval("RecurringTodos::#{selector.capitalize}RecurringTodosBuilder.new(@user, @attributes)")
|
||||
end
|
||||
|
||||
def build
|
||||
@builder.build
|
||||
end
|
||||
|
||||
def update(recurring_todo)
|
||||
@builder.update(recurring_todo)
|
||||
end
|
||||
|
||||
def save
|
||||
@builder.save_project if @new_project_created
|
||||
@builder.save_context if @new_context_created
|
||||
|
||||
return @builder.save
|
||||
end
|
||||
|
||||
def saved_recurring_todo
|
||||
@builder.saved_recurring_todo
|
||||
end
|
||||
|
||||
def recurring_todo
|
||||
@builder.recurring_todo
|
||||
end
|
||||
|
||||
def attributes
|
||||
@builder.attributes
|
||||
end
|
||||
|
||||
def pattern
|
||||
@builder.pattern
|
||||
end
|
||||
|
||||
def errors
|
||||
@builder.errors
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_selector?(selector)
|
||||
%w{daily weekly monthly yearly}.include?(selector)
|
||||
end
|
||||
|
||||
def parse_dates
|
||||
%w{end_date start_from}.each {|date| @attributes.parse_date date }
|
||||
end
|
||||
|
||||
def parse_project
|
||||
@project, @new_project_created = @attributes.parse_collection(:project, @user.projects)
|
||||
end
|
||||
|
||||
def parse_context
|
||||
@context, @new_context_created = @attributes.parse_collection(:context, @user.contexts)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
42
app/models/recurring_todos/weekly_recurring_todos_builder.rb
Normal file
42
app/models/recurring_todos/weekly_recurring_todos_builder.rb
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
module RecurringTodos
|
||||
|
||||
class WeeklyRecurringTodosBuilder < AbstractRecurringTodosBuilder
|
||||
|
||||
def initialize(user, attributes)
|
||||
super(user, attributes, WeeklyRepeatPattern)
|
||||
end
|
||||
|
||||
def attributes_to_filter
|
||||
%w{weekly_selector weekly_every_x_week} + %w{monday tuesday wednesday thursday friday saturday sunday}.map{|day| "weekly_return_#{day}" }
|
||||
end
|
||||
|
||||
def map_attributes(mapping)
|
||||
mapping = map(mapping, :every_other1, 'weekly_every_x_week')
|
||||
|
||||
{ monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 0 }
|
||||
.each do |day, index|
|
||||
mapping = map_day(mapping, :every_day, "weekly_return_#{day}", index)
|
||||
end
|
||||
|
||||
mapping
|
||||
end
|
||||
|
||||
def map_day(mapping, key, source_key, index)
|
||||
mapping.set_if_nil(key, ' ') # avoid nil
|
||||
mapping.set_if_nil(source_key, ' ') # avoid nil
|
||||
|
||||
mapping.set(key, mapping.get(key)[0, index] + mapping.get(source_key) + mapping.get(key)[index+1, mapping.get(key).length])
|
||||
mapping.except(source_key)
|
||||
end
|
||||
|
||||
def selector_key
|
||||
nil
|
||||
end
|
||||
|
||||
def valid_selector?(key)
|
||||
true
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
86
app/models/recurring_todos/weekly_repeat_pattern.rb
Normal file
86
app/models/recurring_todos/weekly_repeat_pattern.rb
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
module RecurringTodos
|
||||
|
||||
class WeeklyRepeatPattern < AbstractRepeatPattern
|
||||
|
||||
def initialize(user)
|
||||
super user
|
||||
end
|
||||
|
||||
def every_x_week
|
||||
get :every_other1
|
||||
end
|
||||
|
||||
{ monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 0 }.each do |day, number|
|
||||
define_method("on_#{day}") do
|
||||
on_xday number
|
||||
end
|
||||
end
|
||||
|
||||
def on_xday(n)
|
||||
get(:every_day) && get(:every_day)[n, 1] != ' '
|
||||
end
|
||||
|
||||
def recurrence_pattern
|
||||
if every_x_week > 1
|
||||
I18n.t("todos.recurrence.pattern.every_n", :n => every_x_week) + " " + I18n.t("common.weeks")
|
||||
else
|
||||
I18n.t('todos.recurrence.pattern.weekly')
|
||||
end
|
||||
end
|
||||
|
||||
def validate
|
||||
super
|
||||
validate_not_blank(every_x_week, "Every other nth week may not be empty for weekly recurrence setting")
|
||||
something_set = %w{sunday monday tuesday wednesday thursday friday saturday}.inject(false) { |set, day| set || self.send("on_#{day}") }
|
||||
errors[:base] << "You must specify at least one day on which the todo recurs" unless something_set
|
||||
end
|
||||
|
||||
def get_next_date(previous)
|
||||
start = determine_start_date(previous)
|
||||
|
||||
day = find_first_day_in_this_week(start)
|
||||
return day unless day == -1
|
||||
|
||||
# we did not find anything this week, so check the nth next, starting from
|
||||
# sunday
|
||||
start = start + self.every_x_week.week - (start.wday()).days
|
||||
|
||||
start = find_first_day_in_this_week(start)
|
||||
return start unless start == -1
|
||||
|
||||
raise Exception.new, "unable to find next weekly date (#{self.every_day})"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def determine_start_date(previous)
|
||||
if previous.nil?
|
||||
return self.start_from || Time.zone.now
|
||||
else
|
||||
start = previous + 1.day
|
||||
if start.wday() == 0
|
||||
# we went to a new week, go to the nth next week and find first match
|
||||
# that week. Note that we already went into the next week, so -1
|
||||
start += (every_x_week-1).week
|
||||
end
|
||||
unless self.start_from.nil?
|
||||
# check if the start_from date is later than previous. If so, use
|
||||
# start_from as start to search for next date
|
||||
start = self.start_from if self.start_from > previous
|
||||
end
|
||||
return start
|
||||
end
|
||||
end
|
||||
|
||||
def find_first_day_in_this_week(start)
|
||||
# check if there are any days left this week for the next todo
|
||||
start.wday().upto 6 do |i|
|
||||
return start + (i-start.wday()).days if on_xday(i)
|
||||
end
|
||||
-1
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
44
app/models/recurring_todos/yearly_recurring_todos_builder.rb
Normal file
44
app/models/recurring_todos/yearly_recurring_todos_builder.rb
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
module RecurringTodos
|
||||
|
||||
class YearlyRecurringTodosBuilder < AbstractRecurringTodosBuilder
|
||||
|
||||
def initialize(user, attributes)
|
||||
super(user, attributes, YearlyRepeatPattern)
|
||||
end
|
||||
|
||||
def attributes_to_filter
|
||||
%w{ yearly_selector yearly_month_of_year yearly_month_of_year2
|
||||
yearly_every_x_day yearly_every_xth_day yearly_day_of_week
|
||||
}
|
||||
end
|
||||
|
||||
def map_attributes(mapping)
|
||||
mapping.set(:recurrence_selector, get_recurrence_selector)
|
||||
mapping.set(:every_other2, mapping.get(get_every_other2))
|
||||
|
||||
mapping = map(mapping, :every_other1, 'yearly_every_x_day')
|
||||
mapping = map(mapping, :every_other3, 'yearly_every_xth_day')
|
||||
mapping = map(mapping, :every_count, 'yearly_day_of_week')
|
||||
|
||||
mapping.except(:yearly_month_of_year).except(:yearly_month_of_year2)
|
||||
end
|
||||
|
||||
def selector_key
|
||||
:yearly_selector
|
||||
end
|
||||
|
||||
def valid_selector?(selector)
|
||||
%w{yearly_every_x_day yearly_every_xth_day}.include?(selector.to_s)
|
||||
end
|
||||
|
||||
def get_recurrence_selector
|
||||
@selector=='yearly_every_x_day' ? 0 : 1
|
||||
end
|
||||
|
||||
def get_every_other2
|
||||
{ 0 => :yearly_month_of_year, 1 => :yearly_month_of_year2 }[get_recurrence_selector]
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
109
app/models/recurring_todos/yearly_repeat_pattern.rb
Normal file
109
app/models/recurring_todos/yearly_repeat_pattern.rb
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
module RecurringTodos
|
||||
|
||||
class YearlyRepeatPattern < AbstractRepeatPattern
|
||||
|
||||
def initialize(user)
|
||||
super user
|
||||
end
|
||||
|
||||
def recurrence_selector
|
||||
get :recurrence_selector
|
||||
end
|
||||
|
||||
def month_of_year
|
||||
get :every_other2
|
||||
end
|
||||
|
||||
def every_x_day
|
||||
get :every_other1
|
||||
end
|
||||
|
||||
def every_xth_day
|
||||
get :every_other3
|
||||
end
|
||||
|
||||
def day_of_week
|
||||
get :every_count
|
||||
end
|
||||
|
||||
def month_of_year2
|
||||
# if recurrence pattern is every xth day in a month, return month otherwise
|
||||
# return a default value
|
||||
get(:recurrence_selector) == 1 ? get(:every_other2) : Time.zone.now.month
|
||||
end
|
||||
|
||||
def recurrence_pattern
|
||||
if self.recurrence_selector == 0
|
||||
I18n.t("todos.recurrence.pattern.every_year_on", :date => date_as_month_day)
|
||||
else
|
||||
I18n.t("todos.recurrence.pattern.every_year_on",
|
||||
:date => I18n.t("todos.recurrence.pattern.the_xth_day_of_month",
|
||||
:x => xth(every_xth_day),
|
||||
:day => day_of_week_as_text(day_of_week),
|
||||
:month => month_of_year_as_text(month_of_year)
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
def validate
|
||||
super
|
||||
case recurrence_selector
|
||||
when 0 # 'yearly_every_x_day'
|
||||
validate_not_blank(month_of_year, "The month of the year may not be empty for recurrence setting")
|
||||
validate_not_blank(every_x_day, "The day of the month may not be empty for recurrence setting")
|
||||
when 1 # 'yearly_every_xth_day'
|
||||
validate_not_blank(month_of_year2, "The month of the year may not be empty for recurrence setting")
|
||||
validate_not_blank(every_xth_day, "The nth day of the month may not be empty for recurrence setting")
|
||||
validate_not_blank(day_of_week, "The day of the week may not be empty for recurrence setting")
|
||||
else
|
||||
raise "unexpected value of recurrence selector '#{recurrence_selector}'"
|
||||
end
|
||||
end
|
||||
|
||||
def get_next_date(previous)
|
||||
start = determine_start(previous)
|
||||
month = get(:every_other2)
|
||||
|
||||
case recurrence_selector
|
||||
when 0 # specific day of a specific month
|
||||
return get_specific_day_of_month(start, month)
|
||||
when 1 # relative weekday of a specific month
|
||||
return get_relative_weekday_of_month(start, month)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def date_as_month_day
|
||||
I18n.l(DateTime.new(Time.zone.now.year, month_of_year, every_x_day), :format => :month_day)
|
||||
end
|
||||
|
||||
def get_specific_day_of_month(start, month)
|
||||
if start.month > month || (start.month == month && start.day >= every_x_day)
|
||||
# if there is no next month n and day m in this year, search in next
|
||||
# year
|
||||
start = Time.zone.local(start.year+1, month, 1)
|
||||
else
|
||||
# if there is a next month n, stay in this year
|
||||
start = Time.zone.local(start.year, month, 1)
|
||||
end
|
||||
Time.zone.local(start.year, month, every_x_day)
|
||||
end
|
||||
|
||||
def get_relative_weekday_of_month(start, month)
|
||||
# if there is no next month n in this year, search in next year
|
||||
the_next = start.month > month ? Time.zone.local(start.year+1, month, 1) : start
|
||||
|
||||
# get the xth day of the month
|
||||
the_next = get_xth_day_of_month(self.every_xth_day, day_of_week, month, the_next.year)
|
||||
|
||||
# if the_next is before previous, we went back into the past, so try next
|
||||
# year
|
||||
the_next = get_xth_day_of_month(self.every_xth_day, day_of_week, month, start.year+1) if the_next <= start
|
||||
|
||||
the_next
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -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(/"/,""") %>" />
|
||||
<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(/"/,""") %>" />
|
||||
<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 %>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
140
lib/tracks/attribute_handler.rb
Normal file
140
lib/tracks/attribute_handler.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
2
test/fixtures/recurring_todos.yml
vendored
2
test/fixtures/recurring_todos.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
97
test/models/attribute_handler_test.rb
Normal file
97
test/models/attribute_handler_test.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
151
test/models/recurring_todos/abstract_repeat_pattern_test.rb
Normal file
151
test/models/recurring_todos/abstract_repeat_pattern_test.rb
Normal 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
|
||||
|
|
@ -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
|
||||
81
test/models/recurring_todos/daily_repeat_pattern_test.rb
Normal file
81
test/models/recurring_todos/daily_repeat_pattern_test.rb
Normal 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
|
||||
24
test/models/recurring_todos/form_helper_test.rb
Normal file
24
test/models/recurring_todos/form_helper_test.rb
Normal 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
|
||||
|
|
@ -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
|
||||
148
test/models/recurring_todos/monthly_repeat_pattern_test.rb
Normal file
148
test/models/recurring_todos/monthly_repeat_pattern_test.rb
Normal 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
|
||||
155
test/models/recurring_todos/recurring_todos_builder_test.rb
Normal file
155
test/models/recurring_todos/recurring_todos_builder_test.rb
Normal 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
|
||||
|
|
@ -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
|
||||
82
test/models/recurring_todos/weekly_repeat_pattern_test.rb
Normal file
82
test/models/recurring_todos/weekly_repeat_pattern_test.rb
Normal 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
|
||||
|
|
@ -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
|
||||
115
test/models/recurring_todos/yearly_repeat_pattern_test.rb
Normal file
115
test/models/recurring_todos/yearly_repeat_pattern_test.rb
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue