Merge pull request #11854 from opf/implementation/45473-update-the-workpackagesapplyworkingdayschangejob-to-take-nonworkingdays-into-account

Implementation/45473 update the WorkpackagesApplyWorkingdaysChangeJob to take NonWorkingDays into account
pull/11980/head
Dombi Attila 2 years ago committed by GitHub
commit efe41a9139
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      app/models/work_package.rb
  2. 53
      app/models/work_packages/scopes/covering_dates_and_days_of_week.rb
  3. 29
      app/services/settings/update_service.rb
  4. 14
      app/services/settings/working_days_update_service.rb
  5. 43
      app/workers/work_packages/apply_working_days_change_job.rb
  6. 14
      config/constants/settings/definition.rb
  7. 5
      config/constants/settings/definitions.rb
  8. 3
      config/locales/en.yml
  9. 28
      spec/constants/settings/definition_spec.rb
  10. 2
      spec/features/admin/working_days_spec.rb
  11. 192
      spec/models/work_packages/scopes/covering_dates_and_days_of_week_spec.rb
  12. 129
      spec/models/work_packages/scopes/covering_days_of_week_spec.rb
  13. 17
      spec/services/settings/update_service_spec.rb
  14. 45
      spec/services/settings/working_days_update_service_spec.rb
  15. 7
      spec/services/work_packages/set_schedule_service_working_days_spec.rb
  16. 48
      spec/support/non_working_days.rb
  17. 2047
      spec/workers/work_packages/apply_working_days_change_job_spec.rb

@ -120,7 +120,7 @@ class WorkPackage < ApplicationRecord
where(author_id: author.id) where(author_id: author.id)
} }
scopes :covering_days_of_week, scopes :covering_dates_and_days_of_week,
:for_scheduling, :for_scheduling,
:include_derived_dates, :include_derived_dates,
:include_spent_time, :include_spent_time,

@ -26,27 +26,29 @@
# See COPYRIGHT and LICENSE files for more details. # See COPYRIGHT and LICENSE files for more details.
#++ #++
module WorkPackages::Scopes::CoveringDaysOfWeek module WorkPackages::Scopes::CoveringDatesAndDaysOfWeek
extend ActiveSupport::Concern extend ActiveSupport::Concern
using CoreExtensions::SquishSql using CoreExtensions::SquishSql
class_methods do class_methods do
# Fetches all work packages that cover specific days of the week. # Fetches all work packages that cover specific days of the week, and/or specific dates.
# #
# The period considered is from the work package start date to the due date. # The period considered is from the work package start date to the due date.
# #
# @param dates Date[] An array of the Date objects.
# @param days_of_week number[] An array of the ISO days of the week to # @param days_of_week number[] An array of the ISO days of the week to
# consider. 1 is Monday, 7 is Sunday. # consider. 1 is Monday, 7 is Sunday.
def covering_days_of_week(days_of_week) def covering_dates_and_days_of_week(days_of_week: [], dates: [])
days_of_week = Array(days_of_week) days_of_week = Array(days_of_week)
return none if days_of_week.empty? dates = Array(dates)
return none if days_of_week.empty? && dates.empty?
where("id IN (#{query(days_of_week)})") where("id IN (#{query(days_of_week, dates)})")
end end
private private
def query(days_of_week) def query(days_of_week, dates)
sql = <<~SQL.squish sql = <<~SQL.squish
-- select work packages dates with their followers dates -- select work packages dates with their followers dates
WITH work_packages_with_dates AS ( WITH work_packages_with_dates AS (
@ -59,7 +61,6 @@ module WorkPackages::Scopes::CoveringDaysOfWeek
work_packages.start_date IS NOT NULL work_packages.start_date IS NOT NULL
OR work_packages.due_date IS NOT NULL OR work_packages.due_date IS NOT NULL
) )
ORDER BY work_packages.id
), ),
-- coalesce non-existing dates of work package to get period start/end -- coalesce non-existing dates of work package to get period start/end
work_packages_periods AS ( work_packages_periods AS (
@ -68,29 +69,29 @@ module WorkPackages::Scopes::CoveringDaysOfWeek
GREATEST(work_package_start_date, work_package_due_date) AS end_date GREATEST(work_package_start_date, work_package_due_date) AS end_date
FROM work_packages_with_dates FROM work_packages_with_dates
), ),
-- expand period into days of the week. Limit to 7 days (more would be useless). -- All days between the start date of a work package and its due date
work_packages_days_of_week AS ( covered_dates AS (
SELECT id, SELECT
extract( id,
isodow generate_series(work_packages_periods.start_date,
from generate_series( work_packages_periods.end_date,
work_packages_periods.start_date, '1 day') AS date
LEAST( FROM work_packages_periods
work_packages_periods.start_date + 6, ),
work_packages_periods.end_date -- All days between the start date of a work package and its due date including the day of the week for each date
), covered_dates_and_wday AS (
'1 day' SELECT
) id,
) AS dow date,
FROM work_packages_periods EXTRACT(isodow FROM date) dow
FROM covered_dates
) )
-- select id of work packages covering the given days -- select id of work packages covering the given days
SELECT DISTINCT id SELECT id FROM covered_dates_and_wday
FROM work_packages_days_of_week WHERE dow IN (:days_of_week) OR date IN (:dates)
WHERE dow IN (:days_of_week)
SQL SQL
sanitize_sql([sql, { days_of_week: }]) sanitize_sql([sql, { days_of_week:, dates: }])
end end
end end
end end

@ -32,13 +32,6 @@ class Settings::UpdateService < BaseServices::BaseContracted
contract_class: Settings::UpdateContract contract_class: Settings::UpdateContract
end end
def after_validate(params, call)
params.keys.each(&method(:remember_previous_value))
call
end
# We will have a problem with error handling on the form.
# How can we still display the user changed values in case the form is not successfully saved?
def persist(call) def persist(call)
params.each do |name, value| params.each do |name, value|
set_setting_value(name, value) set_setting_value(name, value)
@ -46,34 +39,12 @@ class Settings::UpdateService < BaseServices::BaseContracted
call call
end end
def after_perform(call)
super.tap do
params.each_key do |name|
run_on_change_callback(name)
end
end
end
private private
def remember_previous_value(name)
previous_values[name] = Setting[name]
end
def set_setting_value(name, value) def set_setting_value(name, value)
Setting[name] = derive_value(value) Setting[name] = derive_value(value)
end end
def previous_values
@previous_values ||= {}
end
def run_on_change_callback(name)
if (definition = Settings::Definition[name]) && definition.on_change
definition.on_change.call(previous_values[name])
end
end
def derive_value(value) def derive_value(value)
case value case value
when Array, Hash when Array, Hash

@ -30,6 +30,8 @@ class Settings::WorkingDaysUpdateService < Settings::UpdateService
def call(params) def call(params)
params = params.to_h.deep_symbolize_keys params = params.to_h.deep_symbolize_keys
self.non_working_days_params = params.delete(:non_working_days) || [] self.non_working_days_params = params.delete(:non_working_days) || []
self.previous_working_days = Setting[:working_days]
self.previous_non_working_days = NonWorkingDay.pluck(:date)
super super
end end
@ -54,9 +56,19 @@ class Settings::WorkingDaysUpdateService < Settings::UpdateService
results results
end end
def after_perform(call)
super.tap do
WorkPackages::ApplyWorkingDaysChangeJob.perform_later(
user_id: User.current.id,
previous_working_days:,
previous_non_working_days:
)
end
end
private private
attr_accessor :non_working_days_params attr_accessor :non_working_days_params, :previous_working_days, :previous_non_working_days
def persist_non_working_days def persist_non_working_days
# We don't support update for now # We don't support update for now

@ -30,18 +30,19 @@ class WorkPackages::ApplyWorkingDaysChangeJob < ApplicationJob
queue_with_priority :above_normal queue_with_priority :above_normal
include ::ScheduledJob include ::ScheduledJob
def perform(user_id:, previous_working_days:) def perform(user_id:, previous_working_days:, previous_non_working_days:)
user = User.find(user_id) user = User.find(user_id)
User.execute_as user do User.execute_as user do
updated_work_package_ids = collect_id_for_each(applicable_work_package(previous_working_days)) do |work_package| updated_work_package_ids = collect_id_for_each(applicable_work_package(previous_working_days,
previous_non_working_days)) do |work_package|
apply_change_to_work_package(user, work_package) apply_change_to_work_package(user, work_package)
end end
updated_work_package_ids += collect_id_for_each(applicable_predecessor(updated_work_package_ids)) do |predecessor| updated_work_package_ids += collect_id_for_each(applicable_predecessor(updated_work_package_ids)) do |predecessor|
apply_change_to_predecessor(user, predecessor) apply_change_to_predecessor(user, predecessor)
end end
set_journal_notice(updated_work_package_ids, previous_working_days) set_journal_notice(updated_work_package_ids, previous_working_days, previous_non_working_days)
end end
end end
@ -68,11 +69,11 @@ class WorkPackages::ApplyWorkingDaysChangeJob < ApplicationJob
end end
end end
def applicable_work_package(previous_working_days) def applicable_work_package(previous_working_days, previous_non_working_days)
changed_days = changed_days(previous_working_days) days_of_week = changed_days(previous_working_days).keys
dates = changed_non_working_dates(previous_non_working_days).keys
WorkPackage WorkPackage
.covering_days_of_week(changed_days) .covering_dates_and_days_of_week(days_of_week:, dates:)
.order(WorkPackage.arel_table[:start_date].asc.nulls_first, .order(WorkPackage.arel_table[:start_date].asc.nulls_first,
WorkPackage.arel_table[:due_date].asc) WorkPackage.arel_table[:due_date].asc)
end end
@ -83,7 +84,16 @@ class WorkPackages::ApplyWorkingDaysChangeJob < ApplicationJob
# `^` is a Set method returning a new set containing elements exclusive to # `^` is a Set method returning a new set containing elements exclusive to
# each other # each other
(previous ^ current).to_a (previous ^ current).index_with { |day| current.include?(day) }
end
def changed_non_working_dates(previous_non_working_days)
previous = Set.new(previous_non_working_days)
current = Set.new(NonWorkingDay.pluck(:date))
# `^` is a Set method returning a new set containing elements exclusive to
# each other
(previous ^ current).index_with { |day| current.exclude?(day) }
end end
def applicable_predecessor(excluded) def applicable_predecessor(excluded)
@ -92,9 +102,10 @@ class WorkPackages::ApplyWorkingDaysChangeJob < ApplicationJob
.where.not(id: excluded) .where.not(id: excluded)
end end
def set_journal_notice(updated_work_package_ids, previous_working_days) def set_journal_notice(updated_work_package_ids, previous_working_days, previous_non_working_days)
day_changes = changed_days(previous_working_days).index_with { |day| Setting.working_days.include?(day) } day_changes = changed_days(previous_working_days)
journal_note = journal_notice_text(day_changes) date_changes = changed_non_working_dates(previous_non_working_days)
journal_note = journal_notice_text(day_changes, date_changes)
WorkPackage WorkPackage
.where(id: updated_work_package_ids.uniq) .where(id: updated_work_package_ids.uniq)
@ -105,10 +116,12 @@ class WorkPackages::ApplyWorkingDaysChangeJob < ApplicationJob
end end
end end
def journal_notice_text(day_changes) def journal_notice_text(day_changes, date_changes)
I18n.with_locale(Setting.default_language) do I18n.with_locale(Setting.default_language) do
day_changes_messages = day_changes.collect { |day, working| working_day_change_message(day, working) }
date_changes_messages = date_changes.collect { |date, working| working_date_change_message(date, working) }
I18n.t(:'working_days.journal_note.changed', I18n.t(:'working_days.journal_note.changed',
changes: day_changes.collect { |day, working| working_day_change_message(day, working) }.join(', ')) changes: (day_changes_messages + date_changes_messages).join(', '))
end end
end end
@ -117,6 +130,10 @@ class WorkPackages::ApplyWorkingDaysChangeJob < ApplicationJob
day: WeekDay.find_by!(day:).name) day: WeekDay.find_by!(day:).name)
end end
def working_date_change_message(date, working)
I18n.t(:"working_days.journal_note.dates.#{working ? :working : :non_working}", date:)
end
def collect_id_for_each(scope) def collect_id_for_each(scope)
scope.pluck(:id).map do |id| scope.pluck(:id).map do |id|
yield(WorkPackage.find(id)).pluck(:id) yield(WorkPackage.find(id)).pluck(:id)

@ -33,8 +33,7 @@ module Settings
attr_accessor :name, attr_accessor :name,
:format, :format,
:env_alias, :env_alias
:on_change
attr_writer :value, attr_writer :value,
:allowed :allowed
@ -44,8 +43,7 @@ module Settings
format: nil, format: nil,
writable: true, writable: true,
allowed: nil, allowed: nil,
env_alias: nil, env_alias: nil)
on_change: nil)
self.name = name.to_s self.name = name.to_s
@default = default.is_a?(Hash) ? default.deep_stringify_keys : default @default = default.is_a?(Hash) ? default.deep_stringify_keys : default
@default.freeze @default.freeze
@ -54,7 +52,6 @@ module Settings
self.writable = writable self.writable = writable
self.allowed = allowed self.allowed = allowed
self.env_alias = env_alias self.env_alias = env_alias
self.on_change = on_change
end end
def default def default
@ -136,14 +133,12 @@ module Settings
# @param [nil] env_alias Alternative for the default env name to also look up. E.g. with the alias set to # @param [nil] env_alias Alternative for the default env name to also look up. E.g. with the alias set to
# `OPENPROJECT_2FA` for a definition with the name `two_factor_authentication`, the value is fetched # `OPENPROJECT_2FA` for a definition with the name `two_factor_authentication`, the value is fetched
# from the ENV OPENPROJECT_2FA as well. # from the ENV OPENPROJECT_2FA as well.
# @param [nil] on_change A callback lambda to be triggered whenever the setting is stored to the database.
def add(name, def add(name,
default:, default:,
format: nil, format: nil,
writable: true, writable: true,
allowed: nil, allowed: nil,
env_alias: nil, env_alias: nil)
on_change: nil)
return if @by_name.present? && @by_name[name.to_s].present? return if @by_name.present? && @by_name[name.to_s].present?
@by_name = nil @by_name = nil
@ -153,8 +148,7 @@ module Settings
default:, default:,
writable:, writable:,
allowed:, allowed:,
env_alias:, env_alias:)
on_change:)
override_value(definition) override_value(definition)

@ -938,10 +938,7 @@ Settings::Definition.define do
add :working_days, add :working_days,
format: :array, format: :array,
allowed: Array(1..7), allowed: Array(1..7),
default: Array(1..5), # Sat, Sun being non-working days default: Array(1..5) # Sat, Sun being non-working days
on_change: ->(previous_working_days) do
WorkPackages::ApplyWorkingDaysChangeJob.perform_later(user_id: User.current.id, previous_working_days:)
end
add :youtube_channel, add :youtube_channel,
default: 'https://www.youtube.com/c/OpenProjectCommunity', default: 'https://www.youtube.com/c/OpenProjectCommunity',

@ -3160,6 +3160,9 @@ en:
days: days:
working: "%{day} is now working" working: "%{day} is now working"
non_working: "%{day} is now non-working" non_working: "%{day} is now non-working"
dates:
working: "%{date} is now working"
non_working: "%{date} is now non-working"
nothing_to_preview: "Nothing to preview" nothing_to_preview: "Nothing to preview"
api_v3: api_v3:

@ -968,32 +968,4 @@ describe Settings::Definition do
end end
end end
end end
describe '#on_change', :settings_reset do
context 'for a definition with a callback' do
let(:callback) { -> { 'foobar ' } }
it 'includes the callback' do
described_class.add 'bogus',
default: 1,
format: :integer,
on_change: callback
expect(described_class['bogus'].on_change)
.to eq callback
end
end
context 'for a definition without a callback' do
it 'includes the callback' do
described_class.add 'bogus',
default: 1,
format: :integer,
on_change: nil
expect(described_class['bogus'].on_change)
.to be_nil
end
end
end
end end

@ -161,7 +161,7 @@ describe 'Working Days', js: true do
# delayed jobs are handled. # delayed jobs are handled.
ActiveJob::QueueAdapters::DelayedJobAdapter ActiveJob::QueueAdapters::DelayedJobAdapter
.new .new
.enqueue(WorkPackages::ApplyWorkingDaysChangeJob.new(user_id: 5, previous_working_days: [])) .enqueue(WorkPackages::ApplyWorkingDaysChangeJob.new(user_id: 5))
uncheck 'Tuesday' uncheck 'Tuesday'
click_on 'Save' click_on 'Save'

@ -0,0 +1,192 @@
#-- 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::Scopes::CoveringDatesAndDaysOfWeek do
create_shared_association_defaults_for_work_package_factory
shared_let(:next_monday) { Date.current.next_occurring(:monday) }
shared_examples_for 'covering days' do
# Construct the keyword arguments for the `#covering_dates_and_days_of_week` method.
# It builds the expected day values based on the argument type provided (`:dates`, `:days_of_week`)
# and based on the day values provided.
# By providing the `expected_days`, the specific tetscase can decide which days are expected.
let(:day_args) do
->(*expected_days) do
days.keys.reduce(Hash.new { |hsh, key| hsh[key] = [] }) do |day_args, dt|
Array(expected_days).each do |day|
day_args[dt] << days[dt][day] if days[dt][day]
end
day_args
end
end
end
it 'returns work packages having start date or due date being in the given days of week' do
schedule =
create_schedule(<<~CHART)
days | MTWTFSS |
covered1 | XX |
covered2 | XX |
covered3 | X |
covered4 | [ |
covered5 | ] |
not_covered1 | X |
not_covered2 | X |
not_covered3 | XX |
not_covered4 | |
CHART
expect(WorkPackage.covering_dates_and_days_of_week(**day_args[:tuesday]))
.to contain_exactly(
schedule.work_package("covered1"),
schedule.work_package("covered2"),
schedule.work_package("covered3"),
schedule.work_package("covered4"),
schedule.work_package("covered5")
)
end
it 'returns work packages having days between start date and due date being in the given days of week' do
schedule =
create_schedule(<<~CHART)
days | MTWTFSS |
covered1 | XXXX |
covered2 | XXX |
not_covered1 | XX |
not_covered2 | X |
CHART
expect(WorkPackage.covering_dates_and_days_of_week(**day_args[:tuesday, :wednesday]))
.to contain_exactly(
schedule.work_package("covered1"),
schedule.work_package("covered2")
)
end
context 'if work package ignores non working days' do
it 'does not returns it' do
create_schedule(<<~CHART)
days | MTWTFSS |
not_covered | XXXXXXX | working days include weekends
CHART
expect(WorkPackage.covering_dates_and_days_of_week(**day_args[:wednesday]))
.to eq([])
end
end
it 'does not return work packages having follows relation covering the given days of week' do
create_schedule(<<~CHART)
days | MTWTFSS |
not_covered1 | X |
follower1 | X | follows not_covered1
not_covered2 | X |
follower2 | X | follows not_covered2
CHART
expect(WorkPackage.covering_dates_and_days_of_week(**day_args[:tuesday, :thursday]))
.to eq([])
end
it 'does not return work packages having follows relation with delay covering the given days of week' do
create_schedule(<<~CHART)
days | MTWTFSS |
not_covered1 | X |
follower1 | X | follows not_covered1 with delay 3
not_covered2 | X |
follower2 | X | follows not_covered2 with delay 1
CHART
expect(WorkPackage.covering_dates_and_days_of_week(**day_args[:tuesday, :thursday]))
.to eq([])
end
it 'accepts a single day of week or an array of days' do
schedule =
create_schedule(<<~CHART)
days | MTWTFSS |
covered | X |
not_covered | X |
CHART
single_value = day_args[:tuesday].transform_values { |v| Array(v).first }
expect(WorkPackage.covering_dates_and_days_of_week(**single_value))
.to eq([schedule.work_package("covered")])
expect(WorkPackage.covering_dates_and_days_of_week(**day_args[:tuesday]))
.to eq([schedule.work_package("covered")])
expect(WorkPackage.covering_dates_and_days_of_week(**day_args[:tuesday, :wednesday]))
.to eq([schedule.work_package("covered")])
end
end
context 'with the days of week' do
let(:days) do
{
days_of_week: {
tuesday: 2,
wednesday: 3,
thursday: 4
}
}
end
it_behaves_like 'covering days'
end
context 'with specific dates' do
let(:days) do
{
dates: {
tuesday: next_monday.next_occurring(:tuesday),
wednesday: next_monday.next_occurring(:wednesday),
thursday: next_monday.next_occurring(:thursday)
}
}
end
it_behaves_like 'covering days'
end
context 'with days of week and specific dates mixed' do
let(:days) do
{
days_of_week: { wednesday: 3 },
dates: {
tuesday: next_monday.next_occurring(:tuesday),
thursday: next_monday.next_occurring(:thursday)
}
}
end
it_behaves_like 'covering days'
end
end

@ -1,129 +0,0 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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::Scopes::CoveringDaysOfWeek do
create_shared_association_defaults_for_work_package_factory
it 'returns work packages having start date or due date being in the given days of week' do
schedule =
create_schedule(<<~CHART)
days | MTWTFSS |
covered1 | XX |
covered2 | XX |
covered3 | X |
covered4 | [ |
covered5 | ] |
not_covered1 | X |
not_covered2 | X |
not_covered3 | XX |
not_covered4 | |
CHART
expect(WorkPackage.covering_days_of_week(2))
.to contain_exactly(
schedule.work_package("covered1"),
schedule.work_package("covered2"),
schedule.work_package("covered3"),
schedule.work_package("covered4"),
schedule.work_package("covered5")
)
end
it 'returns work packages having days between start date and due date being in the given days of week' do
schedule =
create_schedule(<<~CHART)
days | MTWTFSS |
covered1 | XXXX |
covered2 | XXX |
not_covered1 | XX |
not_covered2 | X |
CHART
expect(WorkPackage.covering_days_of_week([2, 3]))
.to contain_exactly(
schedule.work_package("covered1"),
schedule.work_package("covered2")
)
end
context 'if work package ignores non working days' do
it 'does not returns it' do
create_schedule(<<~CHART)
days | MTWTFSS |
not_covered | XXXXXXX | working days include weekends
CHART
expect(WorkPackage.covering_days_of_week(3))
.to eq([])
end
end
it 'does not return work packages having follows relation covering the given days of week' do
create_schedule(<<~CHART)
days | MTWTFSS |
not_covered1 | X |
follower1 | X | follows not_covered1
not_covered2 | X |
follower2 | X | follows not_covered2
CHART
expect(WorkPackage.covering_days_of_week([2, 4]))
.to eq([])
end
it 'does not return work packages having follows relation with delay covering the given days of week' do
create_schedule(<<~CHART)
days | MTWTFSS |
not_covered1 | X |
follower1 | X | follows not_covered1 with delay 3
not_covered2 | X |
follower2 | X | follows not_covered2 with delay 1
CHART
expect(WorkPackage.covering_days_of_week([2, 4]))
.to eq([])
end
it 'accepts a single day of week or an array of days' do
schedule =
create_schedule(<<~CHART)
days | MTWTFSS |
covered | X |
not_covered | X |
CHART
expect(WorkPackage.covering_days_of_week(2))
.to eq([schedule.work_package("covered")])
expect(WorkPackage.covering_days_of_week([2]))
.to eq([schedule.work_package("covered")])
expect(WorkPackage.covering_days_of_week([2, 3]))
.to eq([schedule.work_package("covered")])
end
end

@ -39,8 +39,7 @@ describe Settings::UpdateService do
end end
let(:contract_success) { true } let(:contract_success) { true }
let(:setting_definition) do let(:setting_definition) do
instance_double(Settings::Definition, instance_double(Settings::Definition)
on_change: definition_on_change)
end end
let(:definition_on_change) do let(:definition_on_change) do
instance_double(Proc, instance_double(Proc,
@ -80,24 +79,10 @@ describe Settings::UpdateService do
include_examples 'successful call' include_examples 'successful call'
it 'calls the on_change handler' do
subject
expect(definition_on_change)
.to have_received(:call).with(previous_setting_value)
end
context 'when the contract is not successfully validated' do context 'when the contract is not successfully validated' do
let(:contract_success) { false } let(:contract_success) { false }
include_examples 'unsuccessful call' include_examples 'unsuccessful call'
it 'does not call the on_change handler' do
subject
expect(definition_on_change)
.not_to have_received(:call)
end
end end
end end
end end

@ -74,15 +74,39 @@ describe Settings::WorkingDaysUpdateService do
describe '#call' do describe '#call' do
subject { instance.call(params) } subject { instance.call(params) }
shared_examples 'successful working days settings call' do
include_examples 'successful call'
it 'calls the WorkPackages::ApplyWorkingDaysChangeJob' do
previous_working_days = Setting[:working_days]
previous_non_working_days = NonWorkingDay.pluck(:date)
allow(WorkPackages::ApplyWorkingDaysChangeJob).to receive(:perform_later)
subject
expect(WorkPackages::ApplyWorkingDaysChangeJob)
.to have_received(:perform_later)
.with(user_id: user.id, previous_working_days:, previous_non_working_days:)
end
end
shared_examples 'unsuccessful working days settings call' do shared_examples 'unsuccessful working days settings call' do
include_examples 'unsuccessful call' include_examples 'unsuccessful call'
it 'does not persists the non working days' do it 'does not persists the non working days' do
expect { subject }.not_to change(NonWorkingDay, :count) expect { subject }.not_to change(NonWorkingDay, :count)
end end
it 'does not calls the WorkPackages::ApplyWorkingDaysChangeJob' do
allow(WorkPackages::ApplyWorkingDaysChangeJob).to receive(:perform_later)
subject
expect(WorkPackages::ApplyWorkingDaysChangeJob).not_to have_received(:perform_later)
end
end end
include_examples 'successful call' include_examples 'successful working days settings call'
context 'when non working days are present' do context 'when non working days are present' do
let!(:existing_nwd) { create(:non_working_day, name: 'Existing NWD') } let!(:existing_nwd) { create(:non_working_day, name: 'Existing NWD') }
@ -96,7 +120,7 @@ describe Settings::WorkingDaysUpdateService do
] ]
end end
include_examples 'successful call' include_examples 'successful working days settings call'
it 'persists (create/delete) the non working days' do it 'persists (create/delete) the non working days' do
expect { subject }.to change(NonWorkingDay, :count).by(1) expect { subject }.to change(NonWorkingDay, :count).by(1)
@ -132,12 +156,23 @@ describe Settings::WorkingDaysUpdateService do
context 'when deleting and re-creating the duplicate non-working day' do context 'when deleting and re-creating the duplicate non-working day' do
let(:non_working_days_params) do let(:non_working_days_params) do
[ [
existing_nwd.slice(:id, :name, :date).merge('_destroy' => true), nwd_to_delete.slice(:id, :name, :date).merge('_destroy' => true),
existing_nwd.slice(:name, :date) nwd_to_delete.slice(:name, :date)
] ]
end end
include_examples 'successful call' include_examples 'successful working days settings call'
it 'persists (create/delete) the non working days' do
expect { subject }.not_to change(NonWorkingDay, :count)
expect { nwd_to_delete.reload }.to raise_error(ActiveRecord::RecordNotFound)
# The nwd_to_delete is being re-created after the deletion.
expect(NonWorkingDay.all).to contain_exactly(
have_attributes(existing_nwd.slice(:name, :date)),
have_attributes(nwd_to_delete.slice(:name, :date))
)
end
end end
end end
end end

@ -972,13 +972,6 @@ describe WorkPackages::SetScheduleService, 'working days' do
end end
end end
def set_non_working_week_days(*days)
non_working_days = days.map do |day|
%w[xxx monday tuesday wednesday thursday friday saturday sunday].index(day.downcase)
end
Setting.working_days -= non_working_days
end
context 'when moving forward due to days and predecessor due date now being non-working days' do context 'when moving forward due to days and predecessor due date now being non-working days' do
let_schedule(<<~CHART) let_schedule(<<~CHART)
days | MTWTFSS | days | MTWTFSS |

@ -0,0 +1,48 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 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.
#++
def week_with_saturday_and_sunday_as_non_working_day(monday: Date.current.monday, weeks_size: 4)
weeks_size.times.map do |week_count|
[
create(:non_working_day, date: monday.next_occurring(:saturday) + week_count.weeks),
create(:non_working_day, date: monday.next_occurring(:sunday) + week_count.weeks)
]
end.flatten.pluck(:date)
end
def week_without_non_working_days(monday: Date.current.monday, weeks_size: 4)
NonWorkingDay.where(date: monday...monday + weeks_size.weeks).destroy_all
end
def set_non_working_days(*dates)
dates.map { |date| create(:non_working_day, date:) }
end
def set_working_days(*dates)
NonWorkingDay.where(date: dates).destroy_all
end
Loading…
Cancel
Save