Merge pull request #11205 from opf/implementation/42923-handle-updating-schedule-as-non-working-days-are-changed

[#42923] Add job to reschedule on weekend days changes
pull/11261/head
Oliver Günther 2 years ago committed by GitHub
commit 2e6ae87747
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 50
      app/services/work_packages/set_schedule_service.rb
  2. 78
      app/workers/work_packages/apply_working_days_change_job.rb
  3. 2
      modules/calendar/spec/features/calendar_dates_spec.rb
  4. 2
      modules/team_planner/spec/features/team_planner_dates_spec.rb
  5. 6
      spec/contracts/work_packages/base_contract_spec.rb
  6. 22
      spec/factories/week_day_factory.rb
  7. 15
      spec/factories/work_package_factory.rb
  8. 2
      spec/lib/api/v3/days/day_collection_representer_spec.rb
  9. 2
      spec/models/day_spec.rb
  10. 1
      spec/models/queries/work_packages/filter/duration_filter_spec.rb
  11. 2
      spec/requests/api/v3/days/day_spec.rb
  12. 2
      spec/requests/api/v3/days/week_spec.rb
  13. 6
      spec/services/work_packages/set_attributes_service_spec.rb
  14. 9
      spec/services/work_packages/set_schedule_service_spec.rb
  15. 73
      spec/services/work_packages/set_schedule_service_working_days_spec.rb
  16. 2
      spec/services/work_packages/shared/shared_examples_days.rb
  17. 5
      spec/support/schedule_helpers/chart.rb
  18. 4
      spec/support/schedule_helpers/chart_builder.rb
  19. 12
      spec/support/schedule_helpers/chart_representer.rb
  20. 20
      spec/support_spec/schedule_helpers/chart_builder_spec.rb
  21. 2
      spec/support_spec/schedule_helpers/chart_representer_spec.rb
  22. 2
      spec/support_spec/schedule_helpers/chart_spec.rb
  23. 298
      spec/workers/work_packages/apply_working_days_change_job_spec.rb

@ -141,15 +141,16 @@ class WorkPackages::SetScheduleService
# moving it. Then it is moved to the earliest date possible. This
# limitation is propagated transitively to all following work packages.
def reschedule_by_predecessors(scheduled, dependency)
delta = follows_delta(dependency)
min_start_date = dependency.soonest_start_date
return unless min_start_date
if delta.zero? && min_start_date
reschedule_to_date(scheduled, min_start_date)
elsif !scheduled.start_date && min_start_date
delta = predecessor_delta(dependency)
if !scheduled.start_date
schedule_on_missing_dates(scheduled, min_start_date)
elsif !delta.zero?
reschedule_by_delta(scheduled, delta, min_start_date, dependency)
elsif delta >= 0
reschedule_to_date(scheduled, min_start_date)
elsif delta < 0
reschedule_by_delta(scheduled, delta, min_start_date)
end
end
@ -174,36 +175,33 @@ class WorkPackages::SetScheduleService
scheduled.due_date && scheduled.due_date < min_start_date ? min_start_date : scheduled.due_date)
end
def reschedule_by_delta(scheduled, moved_delta, min_start_date, dependency)
days = WorkPackages::Shared::Days.for(dependency.work_package)
def reschedule_by_delta(scheduled, moved_delta, min_start_date)
days = WorkPackages::Shared::Days.for(scheduled)
# TODO: can it be moved to dependency?
min_start_delta = days.delta(previous: scheduled.start_date || min_start_date, current: min_start_date)
required_delta = [min_start_delta, [moved_delta, 0].min].max
scheduled_days = WorkPackages::Shared::Days.for(scheduled)
new_start_date = scheduled_days.add_days(scheduled.start_date, required_delta)
new_due_date = scheduled_days.add_days(scheduled.due_date, required_delta) if scheduled.due_date
new_start_date = days.add_days(scheduled.start_date, required_delta)
new_due_date = days.due_date(new_start_date, scheduled.duration) if scheduled.due_date && scheduled.duration
scheduled.start_date = new_start_date
scheduled.due_date = new_due_date
end
def follows_delta(dependency)
if dependency.moving_predecessors.any?
date_rescheduling_delta(dependency.moving_predecessors.first, dependency.work_package)
else
0
end
def predecessor_delta(dependency)
predecessor = dependency.moving_predecessors.first
return 0 unless predecessor
days = WorkPackages::Shared::Days.for(dependency.work_package)
rescheduling_delta(dependency.moving_predecessors.first, days)
end
def date_rescheduling_delta(predecessor, follower)
days = WorkPackages::Shared::Days.for(follower)
if predecessor.due_date.present?
previous_due_date = predecessor.due_date_before_last_save || predecessor.due_date_was || predecessor.due_date
days.delta(previous: previous_due_date, current: predecessor.due_date)
elsif predecessor.start_date.present?
previous_start_date = predecessor.start_date_before_last_save || predecessor.start_date_was || predecessor.start_date
days.delta(previous: previous_start_date, current: predecessor.start_date)
def rescheduling_delta(work_package, days)
if work_package.due_date.present?
previous_due_date = work_package.due_date_before_last_save || work_package.due_date_was || work_package.due_date
days.delta(previous: previous_due_date, current: work_package.due_date)
elsif work_package.start_date.present?
previous_start_date = work_package.start_date_before_last_save || work_package.start_date_was || work_package.start_date
days.delta(previous: previous_start_date, current: work_package.start_date)
else
0
end

@ -0,0 +1,78 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
class WorkPackages::ApplyWorkingDaysChangeJob < ApplicationJob
queue_with_priority :above_normal
def perform(user_id:)
user = User.find(user_id)
each_applicable_work_package do |work_package|
WorkPackages::UpdateService
.new(user:, model: work_package, contract_class: EmptyContract)
.call(duration: work_package.duration)
end
end
private
def each_applicable_work_package
WorkPackage
.where(ignore_non_working_days: false)
.where.not(start_date: nil, due_date: nil)
.order(WorkPackage.arel_table[:start_date].asc.nulls_first,
WorkPackage.arel_table[:due_date].asc)
.pluck(:id)
.each do |id|
work_package = WorkPackage.find(id)
next unless dates_and_duration_mismatch?(work_package)
yield work_package
end
end
def dates_and_duration_mismatch?(work_package)
# precondition: ignore_non_working_days is false
non_working?(work_package.start_date) \
|| non_working?(work_package.due_date) \
|| wrong_duration?(work_package)
end
def non_working?(date)
date && !days.working?(date)
end
def wrong_duration?(work_package)
computed_duration = days.duration(work_package.start_date, work_package.due_date)
computed_duration && work_package.duration != computed_duration
end
def days
@days ||= WorkPackages::Shared::WorkingDays.new
end
end

@ -77,7 +77,7 @@ describe 'Calendar non working days', type: :feature, js: true do
context 'with all days marked as weekend' do
let!(:week_days) do
days = create(:week_days)
days = create(:week_with_saturday_and_sunday_as_weekend)
WeekDay.update_all(working: false)

@ -73,7 +73,7 @@ describe 'Team planner working days', type: :feature, js: true do
context 'with all days marked as weekend' do
let!(:week_days) do
days = create(:week_days)
days = create(:week_with_saturday_and_sunday_as_weekend)
WeekDay.update_all(working: false)

@ -600,7 +600,7 @@ describe WorkPackages::BaseContract do
context 'when setting duration and dates while covering non-working days' do
before do
create(:week_days) # sat and sun are weekends
create(:week_with_saturday_and_sunday_as_weekend)
work_package.ignore_non_working_days = false
work_package.duration = 6
work_package.start_date = "2022-08-22"
@ -623,7 +623,7 @@ describe WorkPackages::BaseContract do
context 'when setting duration and dates while covering non-working days and duration is too small' do
before do
create(:week_days) # sat and sun are weekends
create(:week_with_saturday_and_sunday_as_weekend)
work_package.ignore_non_working_days = false
work_package.duration = 1
work_package.start_date = "2022-08-22"
@ -646,7 +646,7 @@ describe WorkPackages::BaseContract do
context 'when setting duration and dates while covering non-working days and duration is too big' do
before do
create(:week_days) # sat and sun are weekends
create(:week_with_saturday_and_sunday_as_weekend)
work_package.ignore_non_working_days = false
work_package.duration = 99
work_package.start_date = "2022-08-22"

@ -43,14 +43,30 @@ FactoryBot.define do
end
# Factory to create all 7 week days at once, Saturday and Sunday being weekend days
factory :week_days, class: 'Array' do
factory :week_with_saturday_and_sunday_as_weekend, aliases: [:week_days], parent: :week do
working_days { %w[monday tuesday wednesday thursday friday] }
end
# Factory to create all 7 week days at once
#
# use +working: ['monday', 'tuesday', ...]+ to define which days of the week
# will be working days. By default, all days are working days.
factory :week, class: 'Array' do
transient do
working_days { %w[monday tuesday wednesday thursday friday saturday sunday] }
end
# Skip the create callback to be able to use non-AR models. Otherwise FactoryBot will
# try to call #save! on any created object.
skip_create
initialize_with do
days = 1.upto(7).map { |day| create(:week_day, day:) }
new(days)
%w[monday tuesday wednesday thursday friday saturday sunday]
.map.with_index do |day_name, i|
day = i + 1
working = working_days.include?(day_name)
create(:week_day, day:, working:)
end
end
end
end

@ -30,6 +30,7 @@ FactoryBot.define do
factory :work_package do
transient do
custom_values { nil }
days { WorkPackages::Shared::Days.for(self) }
end
priority
@ -40,7 +41,19 @@ FactoryBot.define do
author factory: :user
created_at { Time.zone.now }
updated_at { Time.zone.now }
duration { WorkPackages::Shared::Days.for(self).duration(start_date&.to_date, due_date&.to_date) }
start_date do
# derive start date if due date and duration were provided
next unless %i[due_date duration].all? { |field| __override_names__.include?(field) }
due_date && duration && days.start_date(due_date.to_date, duration)
end
due_date do
# derive due date if start date and duration were provided
next unless %i[start_date duration].all? { |field| __override_names__.include?(field) }
start_date && duration && days.due_date(start_date.to_date, duration)
end
duration { days.duration(start_date&.to_date, due_date&.to_date) }
trait :is_milestone do
type factory: :type_milestone

@ -29,7 +29,7 @@
require 'spec_helper'
describe ::API::V3::Days::DayCollectionRepresenter do
let!(:week_days) { create(:week_days) }
let!(:week_days) { create(:week_with_saturday_and_sunday_as_weekend) }
let(:days) do
[
build(:day, date: Date.new(2022, 12, 27)),

@ -52,7 +52,7 @@ describe Day, type: :model do
let(:non_working_dates) { [date_range.begin, date_range.begin + 1.day] }
before do
create(:week_days)
create(:week_with_saturday_and_sunday_as_weekend)
non_working_dates.each { |date| create(:non_working_day, date:) }
end

@ -48,6 +48,7 @@ describe Queries::WorkPackages::Filter::DurationFilter, type: :model do
it_behaves_like 'non ar filter'
describe '#where' do
# TODO: 0 duration should not happen in 12.x. Should we remove it?
let!(:work_package_zero_duration) { create(:work_package, duration: 0) }
let!(:work_package_no_duration) { create(:work_package, duration: nil) }
let!(:work_package_with_duration) { create(:work_package, duration: 1) }

@ -38,7 +38,7 @@ describe ::API::V3::Days::DaysAPI,
current_user { user }
before do
create(:week_days)
create(:week_with_saturday_and_sunday_as_weekend)
get api_v3_paths.path_for :days, filters:
end

@ -38,7 +38,7 @@ describe ::API::V3::Days::WeekAPI,
current_user { user }
before do
create(:week_days)
create(:week_with_saturday_and_sunday_as_weekend)
get api_v3_paths.days_week
end

@ -1039,7 +1039,7 @@ describe WorkPackages::SetAttributesService,
end
context 'with non-working days' do
shared_let(:week_days) { create(:week_days) }
shared_let(:week_days) { create(:week_with_saturday_and_sunday_as_weekend) }
let(:monday) { Time.zone.today.beginning_of_week }
let(:tuesday) { monday + 1.day }
let(:wednesday) { monday + 2.days }
@ -1153,7 +1153,7 @@ describe WorkPackages::SetAttributesService,
let(:call_attributes) { { ignore_non_working_days: false } }
it_behaves_like 'service call' do
it "updates the start date to be on next working day, and due date to accomodate duration" do
it "updates the start date to be on next working day, and due date to accommodate duration" do
expect { subject }
.to change { work_package.slice(:start_date, :due_date, :duration) }
.from(start_date: monday - 1.day, due_date: friday, duration: 6)
@ -1648,7 +1648,7 @@ describe WorkPackages::SetAttributesService,
let(:soonest_start) { saturday }
before do
create(:week_days)
create(:week_with_saturday_and_sunday_as_weekend)
work_package.ignore_non_working_days = false
end

@ -138,16 +138,19 @@ describe WorkPackages::SetScheduleService do
expect(result.start_date)
.to eql(start_date),
"Expected work package ##{wp.id} '#{wp.subject}' to have start date #{start_date}, got #{result.start_date}"
"Expected work package ##{wp.id} '#{wp.subject}' " \
"to have start date #{start_date.inspect}, got #{result.start_date.inspect}"
expect(result.due_date)
.to eql(due_date),
"Expected work package ##{wp.id} '#{wp.subject}' to have due date #{due_date}, got #{result.due_date}"
"Expected work package ##{wp.id} '#{wp.subject}' " \
"to have due date #{due_date.inspect}, got #{result.due_date.inspect}"
duration = WorkPackages::Shared::AllDays.new.duration(start_date, due_date)
expect(result.duration)
.to eql(duration),
"Expected work package ##{wp.id} '#{wp.subject}' to have duration #{duration}, got #{result.duration}"
"Expected work package ##{wp.id} '#{wp.subject}' " \
"to have duration #{duration.inspect}, got #{result.duration.inspect}"
end
end

@ -31,7 +31,7 @@ require 'spec_helper'
describe WorkPackages::SetScheduleService, 'working days' do
create_shared_association_defaults_for_work_package_factory
shared_let(:week_days) { create(:week_days) }
shared_let(:week_days) { create(:week_with_saturday_and_sunday_as_weekend) }
let(:instance) do
described_class.new(user:, work_package:)
@ -1022,6 +1022,77 @@ describe WorkPackages::SetScheduleService, 'working days' do
end
end
def set_non_working_week_days(*days)
days.each do |day|
wday = %w[xxx monday tuesday wednesday thursday friday saturday sunday].index(day.downcase)
WeekDay.find_by!(day: wday).update(working: false)
end
end
context 'when moving forward due to days and predecessor due date now being non-working days' do
let_schedule(<<~CHART)
days | MTWTFSS |
work_package | XX |
follower1 | X | follows work_package
follower2 | XX | follows follower1
CHART
before do
# Tuesday, Thursday, and Friday are now non-working days. So work_package
# was starting on Monday and now is being shifted to Tuesday by the
# SetAttributesService.
#
# Below instructions reproduce the conditions in which such scheduling
# must happen.
set_non_working_week_days('tuesday', 'thursday', 'friday')
change_schedule([work_package], <<~CHART)
days | MTWTFSS |
work_package | X.X |
CHART
end
it 'reschedules all the followers keeping the delay and compacting the extra spaces' do
expect(subject.all_results).to match_schedule(<<~CHART)
days | MTWTFSSm w m |
work_package | X.X |
follower1 | X |
follower2 | X....X |
CHART
end
end
context 'when moving forward due to days and predecessor start date now being non-working days' do
let_schedule(<<~CHART)
days | MTWTFSS |
work_package | XX |
follower1 | X | follows work_package
follower2 | XX | follows follower1
CHART
before do
# Monday, Thursday, and Friday are now non-working days. So work_package
# was starting on Monday and now is being shifted to Tuesday by the
# SetAttributesService.
#
# Below instructions reproduce the conditions in which such scheduling
# must happen.
set_non_working_week_days('monday', 'thursday', 'friday')
change_schedule([work_package], <<~CHART)
days | MTWTFSS |
work_package | XX |
CHART
end
it 'reschedules all the followers without crossing each other' do
expect(subject.all_results).to match_schedule(<<~CHART)
days | MTWTFSS tw tw |
work_package | XX |
follower1 | X |
follower2 | X.....X |
CHART
end
end
context 'when moving backwards' do
let_schedule(<<~CHART)
days | MTWTFSSm sm sm |

@ -29,7 +29,7 @@
Date::DATE_FORMATS[:wday_date] = '%a %-d %b %Y' # Fri 5 Aug 2022
RSpec.shared_context 'with weekend days Saturday and Sunday' do
shared_let(:week_days) { create(:week_days) }
shared_let(:week_days) { create(:week_with_saturday_and_sunday_as_weekend) }
end
RSpec.shared_context 'with non working days Christmas 2022 and new year 2023' do

@ -144,6 +144,11 @@ module ScheduleHelpers
attributes[:duration] = duration
end
def set_ignore_non_working_days(name, ignore_non_working_days)
attributes = work_package_attributes(name.to_sym)
attributes[:ignore_non_working_days] = ignore_non_working_days
end
def add_follows_relation(predecessor:, follower:, delay:)
predecessors_by_follower(follower) << predecessor
delays_between[[predecessor, follower]] = delay

@ -114,6 +114,10 @@ module ScheduleHelpers
)
when /^duration (\d+)/
chart.set_duration(name, $1.to_i)
when /^working days work week$/
chart.set_ignore_non_working_days(name, false)
when /^working days include weekends$/
chart.set_ignore_non_working_days(name, true)
else
raise "unable to parse property #{property.inspect} for line #{name.inspect}"
end

@ -30,12 +30,12 @@ module ScheduleHelpers
class ChartRepresenter
LINE = "%<id>s | %<days>s |".freeze
def self.normalized_to_s(reference_chart, other_chart)
order = reference_chart.work_package_names
id_column_size = [reference_chart, other_chart].map(&:id_column_size).max
first_day = [reference_chart, other_chart].map(&:first_day).min
last_day = [reference_chart, other_chart].map(&:last_day).max
[reference_chart, other_chart]
def self.normalized_to_s(expected_chart, actual_chart)
order = expected_chart.work_package_names
id_column_size = [expected_chart, actual_chart].map(&:id_column_size).max
first_day = [expected_chart, actual_chart].map(&:first_day).min
last_day = [expected_chart, actual_chart].map(&:last_day).max
[expected_chart, actual_chart]
.map { |chart| chart.with(order:, id_column_size:, first_day:, last_day:) }
.map(&:to_s)
end

@ -156,6 +156,26 @@ describe ScheduleHelpers::ChartBuilder do
expect(chart.work_package_attributes(:main)).to include(duration: 3)
end
end
describe 'working days work week' do
it 'sets ignore_non_working_days to false for the work package' do
chart = builder.parse(<<~CHART)
days | MTWTFSS |
main | | working days work week
CHART
expect(chart.work_package_attributes(:main)).to include(ignore_non_working_days: false)
end
end
describe 'working days include weekends' do
it 'sets ignore_non_working_days to true for the work package' do
chart = builder.parse(<<~CHART)
days | MTWTFSS |
main | | working days include weekends
CHART
expect(chart.work_package_attributes(:main)).to include(ignore_non_working_days: true)
end
end
end
describe 'error handling' do

@ -39,7 +39,7 @@ describe ScheduleHelpers::ChartRepresenter do
let(:sunday) { Date.new(2022, 6, 26) }
describe '#normalized_to_s' do
let!(:week_days) { create(:week_days) }
let!(:week_days) { create(:week_with_saturday_and_sunday_as_weekend) }
context 'when both charts have different work packages items and/or order' do
def to_first_columns(charts)

@ -157,7 +157,7 @@ describe ScheduleHelpers::Chart do
end
describe '#to_s' do
let!(:week_days) { create(:week_days) }
let!(:week_days) { create(:week_with_saturday_and_sunday_as_weekend) }
context 'with a chart built from ascii representation' do
let(:chart) do

@ -0,0 +1,298 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2022 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++
require 'rails_helper'
RSpec.describe WorkPackages::ApplyWorkingDaysChangeJob do
subject(:job) { described_class }
shared_let(:user) { create(:user) }
let!(:week) { create(:week_with_saturday_and_sunday_as_weekend) }
def set_non_working_week_days(*days)
set_week_days(*days, working: false)
end
def set_working_week_days(*days)
set_week_days(*days, working: true)
end
def set_week_days(*days, working:)
days.each do |day|
wday = %w[xxx monday tuesday wednesday thursday friday saturday sunday].index(day.downcase)
WeekDay.find_by!(day: wday).update(working:)
end
end
context 'when a work package includes a date that is now a non-working day' do
let_schedule(<<~CHART)
days | MTWTFSS |
work_package | XXXX |
work_package_on_start | XX |
work_package_on_due | XXX |
wp_start_only | [ |
wp_due_only | ] |
CHART
before do
set_non_working_week_days('wednesday')
end
it 'moves the finish date to the corresponding number of now-excluded days to maintain duration [#31992]' do
job.perform_now(user_id: user.id)
expect(WorkPackage.all).to match_schedule(<<~CHART)
days | MTWTFSS |
work_package | XX.XX |
work_package_on_start | XX |
work_package_on_due | XX.X |
wp_start_only | [ |
wp_due_only | ] |
CHART
end
end
context 'when a work package was scheduled to start on a date that is now a non-working day' do
let_schedule(<<~CHART)
days | MTWTFSS |
work_package | XX |
CHART
before do
set_non_working_week_days('wednesday')
end
it 'moves the start date to the earliest working day in the future, ' \
'and the finish date changes by consequence [#31992]' do
job.perform_now(user_id: user.id)
expect(WorkPackage.all).to match_schedule(<<~CHART)
days | MTWTFSS |
work_package | XX |
CHART
end
end
context 'when a work package includes a date that is no more a non-working day' do
let_schedule(<<~CHART)
days | fssMTWTFSS |
work_package | X..XX |
CHART
before do
set_working_week_days('saturday')
end
it 'moves the finish date backwards to the corresponding number of now-included days to maintain duration [#31992]' do
job.perform_now(user_id: user.id)
expect(WorkPackage.all).to match_schedule(<<~CHART)
days | fssMTWTFSS |
work_package | XX.X |
CHART
end
end
context 'when a follower has a predecessor with dates covering a day that is now a non-working day' do
let_schedule(<<~CHART)
days | MTWTFSS |
predecessor | XX | working days work week
follower | XXX | working days include weekends, follows predecessor
CHART
before do
set_non_working_week_days('wednesday')
end
it 'moves the follower start date by consequence of the predecessor dates shift [#31992]' do
job.perform_now(user_id: user.id)
expect(WorkPackage.all).to match_schedule(<<~CHART)
days | MTWTFSS |
predecessor | X.X | working days work week
follower | XXX | working days include weekends
CHART
end
end
context 'when a follower has a predecessor with dates covering a day that is now a working day' do
let!(:week) { create(:week, working_days: ['monday', 'tuesday', 'thursday', 'friday']) }
let_schedule(<<~CHART)
days | MTWTFSS |
predecessor | X.X | working days work week
follower | XXX | working days include weekends, follows predecessor
CHART
before do
set_working_week_days('wednesday')
end
it 'moves the follower start date backwards by consequence of the predecessor dates shift' do
job.perform_now(user_id: user.id)
expect(WorkPackage.all).to match_schedule(<<~CHART)
days | MTWTFSS |
predecessor | XX | working days work week
follower | XXX | working days include weekends
CHART
end
end
xcontext 'when a follower has a predecessor with a non-working day between them that is now a working day' do
let!(:week) { create(:week, working_days: ['monday', 'tuesday', 'thursday', 'friday']) }
let_schedule(<<~CHART)
days | MTWTFSS |
predecessor | XX |
follower | XX | follows predecessor
CHART
before do
set_working_week_days('wednesday')
end
it 'moves the follower start date one day back to keep the same gap between them' do
job.perform_now(user_id: user.id)
expect(WorkPackage.all).to match_schedule(<<~CHART)
days | MTWTFSS |
predecessor | XX |
follower | XX |
CHART
end
end
context 'when a work package has working days include weekends, and includes a date that is now a non-working day' do
let_schedule(<<~CHART)
days | MTWTFSS |
work_package | XXXX | working days include weekends
CHART
before do
set_non_working_week_days('wednesday')
end
it 'does not move any dates' do
job.perform_now(user_id: user.id)
expect(WorkPackage.all).to match_schedule(<<~CHART)
days | MTWTFSS |
work_package | XXXX | working days include weekends
CHART
end
end
context 'when a work package only has a duration' do
let_schedule(<<~CHART)
days | MTWTFSS |
work_package | | duration 3 days
CHART
before do
set_non_working_week_days('wednesday')
end
it 'does not change anything' do
job.perform_now(user_id: user.id)
expect(work_package.duration).to eq(3)
end
end
context 'when having multiple work packages following each other, and having days becoming non working days' do
let_schedule(<<~CHART)
days | MTWTFSS |
wp1 | X..XX | follows wp2
wp2 | X | follows wp3
wp3 | XXX |
CHART
before do
set_non_working_week_days('tuesday', 'wednesday', 'friday')
end
it 'updates them only once' do
expect { job.perform_now(user_id: user.id) }
.to change { WorkPackage.pluck(:lock_version) }
.from([0, 0, 0])
.to([1, 1, 1])
expect(WorkPackage.all).to match_schedule(<<~CHART)
days | MTWTFSSmtwtfssmtwtfss |
wp1 | X..X...X |
wp2 | X |
wp3 | X..X...X |
CHART
end
end
xcontext 'when having multiple work packages following each other, and having days becoming working days' do
let!(:week) { create(:week, working_days: ['monday', 'thursday']) }
let_schedule(<<~CHART)
days | MTWTFSSmtwtfssmtwtfss |
wp1 | X..X...X | follows wp2
wp2 | X | follows wp3
wp3 | X..X...X |
CHART
before do
set_working_week_days('tuesday', 'wednesday', 'friday')
end
it 'updates them only once' do
job.perform_now(user_id: user.id)
expect(WorkPackage.all).to match_schedule(<<~CHART)
days | MTWTFSSmt |
wp1 | X..XX |
wp2 | X |
wp3 | XXX |
CHART
expect(WorkPackage.pluck(:lock_version)).to all(be_less_or_equal_than(1))
end
end
context 'when having multiple work packages following each other and first one only has a due date' do
let_schedule(<<~CHART)
days | MTWTFSS |
wp1 | X..XX | follows wp2
wp2 | XX | follows wp3
wp3 | ] |
CHART
before do
set_non_working_week_days('tuesday', 'wednesday', 'friday')
end
it 'updates them only once' do
expect { job.perform_now(user_id: user.id) }
.to change { WorkPackage.pluck(:lock_version) }
.from([0, 0, 0])
.to([1, 1, 1])
expect(WorkPackage.all).to match_schedule(<<~CHART)
days | MTWTFSSm t ssm t ssm |
wp1 | X..X...X |
wp2 | X..X |
wp3 | ] |
CHART
end
end
end
Loading…
Cancel
Save