diff --git a/Gemfile b/Gemfile index f840f1c2..f28652b7 100644 --- a/Gemfile +++ b/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 diff --git a/Gemfile.lock b/Gemfile.lock index 0098c16c..40c7041f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/controllers/recurring_todos/form_helper.rb b/app/controllers/recurring_todos/form_helper.rb new file mode 100644 index 00000000..a4f078b3 --- /dev/null +++ b/app/controllers/recurring_todos/form_helper.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/recurring_todos_controller.rb b/app/controllers/recurring_todos_controller.rb index fe8f6132..a15f6922 100644 --- a/app/controllers/recurring_todos_controller.rb +++ b/app/controllers/recurring_todos_controller.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index f60718ed..834428b0 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -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) diff --git a/app/models/recurring_todo.rb b/app/models/recurring_todo.rb index 536779ca..3473f81a 100644 --- a/app/models/recurring_todo.rb +++ b/app/models/recurring_todo.rb @@ -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 \ No newline at end of file diff --git a/app/models/recurring_todos/abstract_recurring_todos_builder.rb b/app/models/recurring_todos/abstract_recurring_todos_builder.rb new file mode 100644 index 00000000..67b55aef --- /dev/null +++ b/app/models/recurring_todos/abstract_recurring_todos_builder.rb @@ -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 \ No newline at end of file diff --git a/app/models/recurring_todos/abstract_repeat_pattern.rb b/app/models/recurring_todos/abstract_repeat_pattern.rb new file mode 100644 index 00000000..45332d99 --- /dev/null +++ b/app/models/recurring_todos/abstract_repeat_pattern.rb @@ -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 \ No newline at end of file diff --git a/app/models/recurring_todos/daily_recurring_todos_builder.rb b/app/models/recurring_todos/daily_recurring_todos_builder.rb new file mode 100644 index 00000000..fb8a3289 --- /dev/null +++ b/app/models/recurring_todos/daily_recurring_todos_builder.rb @@ -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 \ No newline at end of file diff --git a/app/models/recurring_todos/daily_repeat_pattern.rb b/app/models/recurring_todos/daily_repeat_pattern.rb new file mode 100644 index 00000000..17dd00f0 --- /dev/null +++ b/app/models/recurring_todos/daily_repeat_pattern.rb @@ -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 \ No newline at end of file diff --git a/app/models/recurring_todos/monthly_recurring_todos_builder.rb b/app/models/recurring_todos/monthly_recurring_todos_builder.rb new file mode 100644 index 00000000..a70cd257 --- /dev/null +++ b/app/models/recurring_todos/monthly_recurring_todos_builder.rb @@ -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 \ No newline at end of file diff --git a/app/models/recurring_todos/monthly_repeat_pattern.rb b/app/models/recurring_todos/monthly_repeat_pattern.rb new file mode 100644 index 00000000..04d1c8f6 --- /dev/null +++ b/app/models/recurring_todos/monthly_repeat_pattern.rb @@ -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 \ No newline at end of file diff --git a/app/models/recurring_todos/recurring_todos_builder.rb b/app/models/recurring_todos/recurring_todos_builder.rb new file mode 100644 index 00000000..56bbcaf5 --- /dev/null +++ b/app/models/recurring_todos/recurring_todos_builder.rb @@ -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 \ No newline at end of file diff --git a/app/models/recurring_todos/weekly_recurring_todos_builder.rb b/app/models/recurring_todos/weekly_recurring_todos_builder.rb new file mode 100644 index 00000000..8a5254ca --- /dev/null +++ b/app/models/recurring_todos/weekly_recurring_todos_builder.rb @@ -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 \ No newline at end of file diff --git a/app/models/recurring_todos/weekly_repeat_pattern.rb b/app/models/recurring_todos/weekly_repeat_pattern.rb new file mode 100644 index 00000000..4e36ad43 --- /dev/null +++ b/app/models/recurring_todos/weekly_repeat_pattern.rb @@ -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 \ No newline at end of file diff --git a/app/models/recurring_todos/yearly_recurring_todos_builder.rb b/app/models/recurring_todos/yearly_recurring_todos_builder.rb new file mode 100644 index 00000000..091509d6 --- /dev/null +++ b/app/models/recurring_todos/yearly_recurring_todos_builder.rb @@ -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 \ No newline at end of file diff --git a/app/models/recurring_todos/yearly_repeat_pattern.rb b/app/models/recurring_todos/yearly_repeat_pattern.rb new file mode 100644 index 00000000..041909c3 --- /dev/null +++ b/app/models/recurring_todos/yearly_repeat_pattern.rb @@ -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 \ No newline at end of file diff --git a/app/views/recurring_todos/_edit_form.html.erb b/app/views/recurring_todos/_edit_form.html.erb index dfb94095..21c4ab24 100644 --- a/app/views/recurring_todos/_edit_form.html.erb +++ b/app/views/recurring_todos/_edit_form.html.erb @@ -5,91 +5,91 @@
<%= - 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") -%> <%= - 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}) -%> - " /> + " /> - + - <%= 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 -%>

- <%= radio_button_tag('recurring_edit_todo[recurring_period]', 'daily', @recurring_todo.recurring_period == 'daily')%> <%= t('todos.recurrence.daily') %>
- <%= radio_button_tag('recurring_edit_todo[recurring_period]', 'weekly', @recurring_todo.recurring_period == 'weekly')%> <%= t('todos.recurrence.weekly') %>
- <%= radio_button_tag('recurring_edit_todo[recurring_period]', 'monthly', @recurring_todo.recurring_period == 'monthly')%> <%= t('todos.recurrence.monthly') %>
- <%= radio_button_tag('recurring_edit_todo[recurring_period]', 'yearly', @recurring_todo.recurring_period == 'yearly')%> <%= t('todos.recurrence.yearly') %>
+ <%= radio_button_tag('recurring_edit_todo[recurring_period]', 'daily', @form_helper.recurring_period == 'daily')%> <%= t('todos.recurrence.daily') %>
+ <%= radio_button_tag('recurring_edit_todo[recurring_period]', 'weekly', @form_helper.recurring_period == 'weekly')%> <%= t('todos.recurrence.weekly') %>
+ <%= radio_button_tag('recurring_edit_todo[recurring_period]', 'monthly', @form_helper.recurring_period == 'monthly')%> <%= t('todos.recurrence.monthly') %>
+ <%= radio_button_tag('recurring_edit_todo[recurring_period]', 'yearly', @form_helper.recurring_period == 'yearly')%> <%= t('todos.recurrence.yearly') %>

<%= - text_field_tag("recurring_todo_edit_start_from", format_date(@recurring_todo.start_from), "size" => 12, "class" => "Date", "autocomplete" => "off") %>
+ text_field_tag("recurring_todo_edit_start_from", format_date(@form_helper.start_from), "size" => 12, "class" => "Date", "autocomplete" => "off") %>


- <%= radio_button_tag('recurring_todo[ends_on]', 'no_end_date', @recurring_todo.ends_on == 'no_end_date')%> <%= t('todos.recurrence.no_end_date') %>
- <%= 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') %>
+ <%= 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)) %>
- <%= 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")) %>
+ <%= 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")) %>
-
+

- <%= 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})) %>
- <%= radio_button_tag('recurring_todo[daily_selector]', 'daily_every_work_day', @recurring_todo.only_work_days)%> <%= t('todos.recurrence.every_work_day') %>
+ <%= 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})) %>
+ <%= radio_button_tag('recurring_todo[daily_selector]', 'daily_every_work_day', @form_helper.only_work_days)%> <%= t('todos.recurrence.every_work_day') %>
-
+

- <%= 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})) %>
- <%= 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] %>
- <%= 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] %>
+ <%= 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})) %>
+ <%= 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] %>
+ <%= 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] %>
-
+

- <%= 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})) %>
- <%= 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})) %>
+ <%= 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})) %>
+ :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})) %>
-
+

- <%= 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)) %>
- <%= 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)) %>
+ <%= 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))) %>
+ :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))) %>

- <%= 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})) %>
- <%= radio_button_tag('recurring_todo[recurring_target]', 'show_from_date', @recurring_todo.target == 'show_from_date')%> <%= t('todos.recurrence.recurrence_on.from_tickler') %>
+ <%= 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})) %>
+ <%= radio_button_tag('recurring_todo[recurring_target]', 'show_from_date', @form_helper.target == 'show_from_date')%> <%= t('todos.recurrence.recurrence_on.from_tickler') %>

<% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index a8737ab1..f8607500 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/lib/tasks/reset_password.rake b/lib/tasks/tracks.rake similarity index 74% rename from lib/tasks/reset_password.rake rename to lib/tasks/tracks.rake index 3dbd8a3c..2a5ef952 100644 --- a/lib/tasks/reset_password.rake +++ b/lib/tasks/tracks.rake @@ -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 diff --git a/lib/todo_from_recurring_todo.rb b/lib/todo_from_recurring_todo.rb index a5bff067..531f0db0 100644 --- a/lib/todo_from_recurring_todo.rb +++ b/lib/todo_from_recurring_todo.rb @@ -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 diff --git a/lib/tracks/attribute_handler.rb b/lib/tracks/attribute_handler.rb new file mode 100644 index 00000000..add42787 --- /dev/null +++ b/lib/tracks/attribute_handler.rb @@ -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 \ No newline at end of file diff --git a/test/controllers/recurring_todos_controller_test.rb b/test/controllers/recurring_todos_controller_test.rb index 30c456a7..b8ce950e 100644 --- a/test/controllers/recurring_todos_controller_test.rb +++ b/test/controllers/recurring_todos_controller_test.rb @@ -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 diff --git a/test/fixtures/recurring_todos.yml b/test/fixtures/recurring_todos.yml index ebbb188f..b1c4cf70 100644 --- a/test/fixtures/recurring_todos.yml +++ b/test/fixtures/recurring_todos.yml @@ -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 diff --git a/test/models/attribute_handler_test.rb b/test/models/attribute_handler_test.rb new file mode 100644 index 00000000..54d6bddd --- /dev/null +++ b/test/models/attribute_handler_test.rb @@ -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 \ No newline at end of file diff --git a/test/models/recurring_todo_test.rb b/test/models/recurring_todo_test.rb index a1bd2d1e..7ac14610 100644 --- a/test/models/recurring_todo_test.rb +++ b/test/models/recurring_todo_test.rb @@ -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 \ No newline at end of file diff --git a/test/models/recurring_todos/abstract_recurring_todos_builder_test.rb b/test/models/recurring_todos/abstract_recurring_todos_builder_test.rb new file mode 100644 index 00000000..5c051eb5 --- /dev/null +++ b/test/models/recurring_todos/abstract_recurring_todos_builder_test.rb @@ -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 \ No newline at end of file diff --git a/test/models/recurring_todos/abstract_repeat_pattern_test.rb b/test/models/recurring_todos/abstract_repeat_pattern_test.rb new file mode 100644 index 00000000..d0bcf98b --- /dev/null +++ b/test/models/recurring_todos/abstract_repeat_pattern_test.rb @@ -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 \ No newline at end of file diff --git a/test/models/recurring_todos/daily_recurring_todos_builder_test.rb b/test/models/recurring_todos/daily_recurring_todos_builder_test.rb new file mode 100644 index 00000000..b6463c9b --- /dev/null +++ b/test/models/recurring_todos/daily_recurring_todos_builder_test.rb @@ -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 \ No newline at end of file diff --git a/test/models/recurring_todos/daily_repeat_pattern_test.rb b/test/models/recurring_todos/daily_repeat_pattern_test.rb new file mode 100644 index 00000000..edfd4977 --- /dev/null +++ b/test/models/recurring_todos/daily_repeat_pattern_test.rb @@ -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 \ No newline at end of file diff --git a/test/models/recurring_todos/form_helper_test.rb b/test/models/recurring_todos/form_helper_test.rb new file mode 100644 index 00000000..80ae0c0c --- /dev/null +++ b/test/models/recurring_todos/form_helper_test.rb @@ -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 \ No newline at end of file diff --git a/test/models/recurring_todos/monthly_recurring_todos_builder_test.rb b/test/models/recurring_todos/monthly_recurring_todos_builder_test.rb new file mode 100644 index 00000000..f02299ca --- /dev/null +++ b/test/models/recurring_todos/monthly_recurring_todos_builder_test.rb @@ -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 \ No newline at end of file diff --git a/test/models/recurring_todos/monthly_repeat_pattern_test.rb b/test/models/recurring_todos/monthly_repeat_pattern_test.rb new file mode 100644 index 00000000..5603108f --- /dev/null +++ b/test/models/recurring_todos/monthly_repeat_pattern_test.rb @@ -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 \ No newline at end of file diff --git a/test/models/recurring_todos/recurring_todos_builder_test.rb b/test/models/recurring_todos/recurring_todos_builder_test.rb new file mode 100644 index 00000000..a5771f86 --- /dev/null +++ b/test/models/recurring_todos/recurring_todos_builder_test.rb @@ -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 \ No newline at end of file diff --git a/test/models/recurring_todos/weekly_recurring_todos_builder_test.rb b/test/models/recurring_todos/weekly_recurring_todos_builder_test.rb new file mode 100644 index 00000000..963e9b37 --- /dev/null +++ b/test/models/recurring_todos/weekly_recurring_todos_builder_test.rb @@ -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_ 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 \ No newline at end of file diff --git a/test/models/recurring_todos/weekly_repeat_pattern_test.rb b/test/models/recurring_todos/weekly_repeat_pattern_test.rb new file mode 100644 index 00000000..87f87397 --- /dev/null +++ b/test/models/recurring_todos/weekly_repeat_pattern_test.rb @@ -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 \ No newline at end of file diff --git a/test/models/recurring_todos/yearly_recurring_todos_builder_test.rb b/test/models/recurring_todos/yearly_recurring_todos_builder_test.rb new file mode 100644 index 00000000..51db4860 --- /dev/null +++ b/test/models/recurring_todos/yearly_recurring_todos_builder_test.rb @@ -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 \ No newline at end of file diff --git a/test/models/recurring_todos/yearly_repeat_pattern_test.rb b/test/models/recurring_todos/yearly_repeat_pattern_test.rb new file mode 100644 index 00000000..0514e655 --- /dev/null +++ b/test/models/recurring_todos/yearly_repeat_pattern_test.rb @@ -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 \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb index 22b639f1..19c74096 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -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)