disregard manually scheduled nodes when scheduling work package graphs

Instead of fetching all work packages from a subset of the whole graph
constructed by hierarchy and follows relationships upon scheduling, only
work packages eligible for automatic rescheduling are returned. In order
to avoid quering the database multiple times, the whole subgraph is
returned at once using a recursive CTE.
pull/8437/head
ulferts 4 years ago committed by Oliver Günther
parent a2a559e553
commit 7065c162fd
No known key found for this signature in database
GPG Key ID: A3A8BDAD7C0C552C
  1. 23
      app/models/work_package.rb
  2. 345
      app/models/work_packages/scopes/for_scheduling.rb
  3. 12
      app/services/work_packages/schedule_dependency.rb
  4. 2
      app/services/work_packages/set_schedule_service.rb
  5. 111
      spec/features/work_packages/scheduling/scheduling_mode_spec.rb
  6. 67
      spec/features/work_packages/scheduling/toggle_scheduling_mode_spec.rb
  7. 423
      spec/models/work_packages/scopes/for_scheduling_spec.rb
  8. 153
      spec/services/work_packages/set_schedule_service_spec.rb

@ -39,6 +39,7 @@ class WorkPackage < ApplicationRecord
include WorkPackage::TypedDagDefaults
include WorkPackage::CustomActioned
include WorkPackage::Hooks
include ::Scopes::Scoped
include OpenProject::Journal::AttachmentHelper
@ -113,6 +114,8 @@ class WorkPackage < ApplicationRecord
where(author_id: author.id)
}
scope_classes WorkPackages::Scopes::ForScheduling
acts_as_watchable
before_create :default_assign
@ -578,26 +581,6 @@ class WorkPackage < ApplicationRecord
"#{table_name}.id IN (#{relation_subquery.to_sql}) OR #{table_name}.id = #{work_package.id}"
end
def self.hierarchy_tree_following(work_packages)
following = Relation
.where(to: work_packages)
.hierarchy_or_follows
following_from_hierarchy = Relation
.hierarchy
.where(from_id: following.select(:from_id))
.select("to_id common_id")
following_from_self = following.select("from_id common_id")
# Using a union here for performance.
# Using or would yield the same results and be less complicated
# but it will require two orders of magnitude more time.
sub_query = [following_from_hierarchy, following_from_self].map(&:to_sql).join(" UNION ")
where("id IN (SELECT common_id FROM (#{sub_query}) following_relations)")
end
# Overrides Redmine::Acts::Customizable::ClassMethods#available_custom_fields
def self.available_custom_fields(work_package)
WorkPackage::AvailableCustomFields.for(work_package.project, work_package.type)

@ -0,0 +1,345 @@
#-- encoding: UTF-8
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details.
#++
#
module WorkPackages::Scopes
class ForScheduling
class << self
# Fetches all work packages that need to be evaluated for eventual rescheduling after a related (i.e. follows/precedes
# and hierarchy) work package is modified or created.
#
# The SQL relies on CTEs which, after constructing the set of all potential work_packages then filter down the
# work packages to the actually affected work packages. The set of potentially affected work packages can be diminished by
# manually schedule work packages.
#
# The first CTE works recursively to fetch all work packages related to the provided work package and the path of
# intermediate work packages. The work packages can either be connected via a follows relationship, a hierarchy relationship
# or a combination of both.
# E.g. in a graph of
# A <- follows - B <- hierarchy (C is parent of B) - C <- follows D
#
# D would also be subject to reschedule.
#
# At least for hierarchical relationships, we need to follow the relationship in both directions.
# E.g. in a graph of
# A <- follows - B - hierarchy (B is parent of C) -> C <- follows D
#
# D would also be subject to reschedule.
#
# That possible switch in direction means that we cannot simply get all possibly affected work packages by one
# SQL query which the DAG implementation would have allowed us to do otherwise.
# Additionally, we need to get the whole paths (with all intermediate work packages included) which would be possible
# with DAG but as we need to rely on a recursive approach already we do not need to complicate the SQL statement any
# further. Fetching the whole path (at least in one direction) relying on DAG would be faster though
# so we might revisit this if any performance shortcomings are identified.
# The first CTE returns all work packages with their path so reusing the example above, the result would be
# id | path
# A | {A}
# B | {A,B}
# C | {A,B,C}
# D | {A,B,C,D}
# If the graph where to contain multiple paths to one node work package, because of multiple follows relationship
# to the same hierarchical tree, the work package would be returned twice with different paths.
#
# The paths are followed until either:
# * no more follows and/or hierarchy relations can be followed
# * a manually scheduled work package is encountered.
#
# So if, in the example above, B would be manually scheduled, the first CTE would only return
# id | path
# A | {A}
# B | {A,B}
#
# The interim result, provided by the first CTE, is thus the set of all work packages, that are in a direct or transitive
# follows and/or hierarchy relationship up until the point where the relationships end or a manually scheduled work package
# is encountered.
#
# That set needs to be filtered down because of additional constraints on scheduling:
# * Manually scheduled work packages prevent automatic scheduling up the hierarchy chain. So even with an existing follows
# relationship work packages might not be scheduled automatically if their children or descendants are automatically
# scheduled. This is only true for a work package if *all* the children are manually scheduled either directly or because
# their respective children are all scheduled manually. In case of the hierarchy
# A and B <- hierarchy (C is parent of both A and B) C <- D
# if A and B are both scheduled manually, C is also scheduled manually and so is D. But if only A is scheduled manually,
# B, C and D are scheduled automatically.
# * the first constraint might cause gaps in the previously established paths. If a work package follows an automatically
# scheduled work package, and that preceding work package has children that are manually scheduled, the preciding
# work package will no longer be automatically scheduled and the same is then true for the following work package.
#
# To visualize the above:
# A <- follows - B <- follows C
# |
# hierarchy
# v
# D (manually)
# The first, path fetching CTE will return B, C and D. The constraint above will then remove B and D and the second
# constraint will remove C.
#
# The work packages that are identified to be in a direct or transitive relationship with the provided work packages and
# that neither have only manually scheduled children/descendants or would only be reachable via work packages for which
# the before mentioned constraint is true are returned. The provided work package is always excluded.
#
# @param work_packages WorkPackage[] A set of work packages for which the set of related work packages that might
# be subject to reschedule is fetched.
def fetch(work_packages)
return WorkPackage.none if work_packages.empty?
sql = <<~SQL
WITH
RECURSIVE
#{paths_sql(work_packages)},
#{paths_without_manual_hierarchy_sql},
#{paths_without_gaps_sql}
SELECT id FROM eligible_paths_without_gaps
SQL
WorkPackage
.where("id IN (#{sql})")
.where.not(id: work_packages)
end
private
# This recursive CTE fetches all work packages that are in a direct or transitive follows and/or hierarchy
# relationship with the provided work package.
#
# Hierarchy relationships are followed up as well as down (from and to) but follows relations are only followed
# from the predecessor to the successor (from_id to to_id).
#
# We will need the exact path (meaning all intermediate work packages) for the later filtering so for each
# recursive step the statement only adds the all the work packages directly connected to the current step and
# does not make use of the abilities of DAG. Using the transitive relationships provided by DAG should be possible
# but the constraints caused by PostgreSQL's implementation of recursive CTEs (no outer join of, no duplicate
# reference to and no subqueries with the recursive query) makes writing it extremly hard.
#
# While using DAG should theoretically be faster, as less iterative steps are required, the difference should
# not be noticeable.
#
# The CTE starts from the provided work package and for that returns:
# * the id of the work package
# * the path to that work package which is again the id but this time as a PostgreSQL array
# * again, a path, same as above but referred to as the path_root (explained below)
# * the information, that the starting work package is not manually scheduled.
# Whether the starting work package is manually scheduled or in fact automatically scheduled does make no
# difference but we need those four columns later on.
#
# For each recursive step, we return all work packages that are directly related to our current set of work
# packages by a hierarchy (up or down) or follows relationship (only successors). For each such work package
# the statement returns:
# * id of the work package that is currently at the end of a path.
# * the path to the added work package. This is the path of the work package the statement extended the path
# from (joined with) with the added work package appended.
# * the path_root which is the path up to the first work package that is within the current work package
# hierarchy. Whenever a new hierarchy is reached (indicated by joining a follow relationship), a new root
# path is created. If the hierarchy is kept, the root_path is taken from the recursive step before.
# The root_path is later on used to identify all work packages within the result set that are within
# the same hierarchy and that might need to be removed because of manual scheduling bubbling up the
# hierarchy tree. Therefore, follow relationships constructed between members of the same hierarchy are
# no problem as well.
# * the flag indicating whether the added work package is automatically or manually scheduled.
#
# Paths whose ending work package is marked to be manually scheduled are not joined with any more.
#
# The recursion ends when no more work packages can be added to the set either because:
# * There is no more work package with a relationship to the current set
# * The current paths all end in manually scheduled work packages
# Both conditions can also stop the recursion together.
def paths_sql(work_packages)
values = work_packages.map { |wp| "(#{wp.id},ARRAY[#{wp.id}], ARRAY[#{wp.id}], false)" }.join(', ')
<<~SQL
clean_paths (id, path, root_path, manually) AS (
SELECT * FROM (VALUES#{values}) AS t(id, path, root_path, manually)
UNION ALL
SELECT
CASE
WHEN relations.to_id = clean_paths.id
THEN relations.from_id
ELSE relations.to_id
END id,
CASE
WHEN relations.to_id = clean_paths.id
THEN array_append(path, relations.from_id)
ELSE array_append(path, relations.to_id)
END path,
CASE
WHEN relations.to_id = clean_paths.id AND relations.follows = 1
THEN array_append(path, relations.from_id)
ELSE clean_paths.root_path
END root_path,
work_packages.schedule_manually manually
FROM
clean_paths
JOIN
relations
ON NOT clean_paths.manually
AND (#{relations_condition_sql})
AND
((relations.to_id = clean_paths.id AND NOT relations.from_id = any(clean_paths.path))
OR (relations.from_id = clean_paths.id AND NOT relations.to_id = any(clean_paths.path) AND relations.follows = 0))
LEFT JOIN work_packages
ON (CASE
WHEN relations.to_id = clean_paths.id
THEN relations.from_id
ELSE relations.to_id
END) = work_packages.id
)
SQL
end
# Filters a set of paths (as returned by the recursive path constructing CTE above) to only contain
# work packages (and their paths) that are truly automatically scheduled.
# Even though a work package is flagged to be automatically scheduled, a work package can in fact be manually scheduled
# nonetheless if:
# * all of its paths towards their leafs have at least one manually scheduled work package in them.
#
# As the recursive CTE above terminates a paths once a manually scheduled work package is identified,
# those manually scheduled work packages are leafs for the sake of the set inserted into this query but might
# very well have children outside of the set.
#
# Identifying all leafs (for the sake of the set) is complicated by the possibility of having multiple
# follow relationships spanning into the same hierarchy tree. E.g. in a graph of
#
# C
# |
# hierarchy
# |
# v
# A <- follows - B
# ^ |
# | hierarchy
# | |
# | v
# | D (manually)
# | |
# | hierarchy
# | |
# | v
# -- follows - E
#
# D is excluded directly. But B and C also need to be considered manually scheduled as their descendant D is
# scheduled manually. But E (which is the actual leaf of that hierarchy) is reached via a different follows
# relationship.
#
# Please not that when D has an automatically scheduled sibling F:
#
# C
# |
# hierarchy
# |
# v
# A <- follows - B - hierarchy -
# ^ | |
# | hierarchy |
# | | |
# | v v
# | D (manually) F
# | |
# | hierarchy
# | |
# | v
# -- follows - E
#
# Neither B nor C are considered manually scheduled any more.
#
# The query works by joining the paths with itself and with the relations first to identify all paths (calculated by
# the CTE before) that lead to descendants of a work package. Here, the root_path is considered to avoid mixing
# individual follows relationships jumps.
# Next, the paths are joined again to identify those, that have no longer paths.
# The result are all paths that lead to descendants of a work packages identified in the path that have no longer paths
# which, within the set, are the leafs. Of those, only the paths are returned that do not lead to a manually scheduled
# work package.
# This step also removes all work packages that are scheduled manually directly.
def paths_without_manual_hierarchy_sql
<<~SQL
paths_without_manual_hierarchy AS (
SELECT
paths.id,
paths.path
FROM
clean_paths paths
LEFT JOIN
relations
ON
relations.from_id = paths.id AND "relations"."follows" = 0 AND (#{relations_condition_sql(transitive: true)})
LEFT JOIN
clean_paths to_paths
ON
relations.to_id = to_paths.id AND to_paths.root_path = paths.root_path
LEFT JOIN
clean_paths longer_paths
ON
longer_paths.path[1:array_length(longer_paths.path, 1) - 1] = to_paths.path
AND to_paths.root_path = longer_paths.root_path
AND longer_paths.path <> paths.path
WHERE longer_paths.id IS NULL
AND NOT (paths.manually OR COALESCE(to_paths.manually, false))
)
SQL
end
# Returns all paths that do not include intermediary hops (work packages) that are not within the set of paths
# themselves.
# This serves as a second filter after work packages scheduled manually by transition are removed from the set.
# E.g in a graph of
# A <- follows - B <- follows C
# |
# hierarchy
# v
# D (manually)
#
# The recursive CTE will return A, B, C and D, with D flagged as manually scheduled. The first filter will then remove
# D and B from the set. Now, there is no longer a connection between A and C. So the query below removes C from the
# result as well.
def paths_without_gaps_sql
<<~SQL
eligible_paths_without_gaps AS (
SELECT
*
FROM
paths_without_manual_hierarchy
WHERE
path <@ (SELECT array_agg(id) FROM paths_without_manual_hierarchy)
)
SQL
end
def relations_condition_sql(transitive: false)
<<~SQL
"relations"."relates" = 0 AND "relations"."duplicates" = 0 AND "relations"."blocks" = 0 AND "relations"."includes" = 0 AND "relations"."requires" = 0
AND (relations.hierarchy + relations.relates + relations.duplicates + relations.follows + relations.blocks + relations.includes + relations.requires #{transitive ? '>' : ''}= 1)
SQL
end
end
end
end

@ -79,16 +79,12 @@ class WorkPackages::ScheduleDependency
self.known_work_packages_by_id = known_work_packages.group_by(&:id)
self.known_work_packages_by_parent_id = known_work_packages.group_by(&:parent_id)
new_dependencies = add_dependencies(following)
if new_dependencies.any?
load_all_following(new_dependencies.keys)
end
add_dependencies(following)
end
def load_following(work_packages)
WorkPackage
.hierarchy_tree_following(work_packages)
.for_scheduling(work_packages)
.includes(parent_relation: :from,
follows_relations: :to)
end
@ -121,11 +117,9 @@ class WorkPackages::ScheduleDependency
moved = find_moved(added)
newly_added = moved.except(*dependencies.keys)
moved.except(*dependencies.keys)
dependencies.merge!(moved)
newly_added
end
class Dependency

@ -74,6 +74,8 @@ class WorkPackages::SetScheduleService
# are first all loaded, and then sorted by their need to be scheduled before one another:
# - predecessors are scheduled before their successors
# - children/descendants are scheduled before their parents/ancestors
# Manually scheduled work packages are not encountered at this point as they are filtered out when fetching the
# work packages eligible for rescheduling.
def schedule_following
altered = []

@ -0,0 +1,111 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'features/page_objects/notification'
require 'features/work_packages/details/inplace_editor/shared_examples'
require 'features/work_packages/shared_contexts'
require 'support/edit_fields/edit_field'
require 'features/work_packages/work_packages_page'
describe 'scheduling mode',
js: true do
let(:project) { FactoryBot.create :project_with_types, public: true }
# Constructing a work package graph that looks like this:
#
# wp_parent wp_suc_parent
# | |
# hierarchy hierarchy
# | |
# v v
# wp_pre <- follows <- wp <- follows - wp_suc
# | |
# hierarchy hierarchy
# | |
# v v
# wp_child wp_suc_child
#
let!(:wp) { FactoryBot.create :work_package, project: project, start_date: '2016-01-01', due_date: '2016-01-05' }
let!(:wp_parent) do
FactoryBot.create(:work_package, project: project, start_date: '2016-01-01', due_date: '2016-01-05').tap do |parent|
FactoryBot.create(:hierarchy_relation, from: parent, to: wp)
end
end
let!(:wp_child) do
FactoryBot.create(:work_package, project: project, start_date: '2016-01-01', due_date: '2016-01-05').tap do |child|
FactoryBot.create(:hierarchy_relation, from: wp, to: child)
end
end
let!(:wp_pre) do
FactoryBot.create(:work_package, project: project, start_date: '2016-01-06', due_date: '2016-01-10').tap do |pre|
FactoryBot.create(:follows_relation, from: wp, to: pre)
end
end
let!(:wp_suc) do
FactoryBot.create(:work_package, project: project, start_date: '2016-01-06', due_date: '2016-01-10').tap do |suc|
FactoryBot.create(:follows_relation, from: suc, to: wp)
end
end
let!(:wp_suc_parent) do
FactoryBot.create(:work_package, project: project, start_date: '2016-01-06', due_date: '2016-01-10').tap do |parent|
FactoryBot.create(:hierarchy_relation, from: parent, to: wp_suc)
end
end
let!(:wp_suc_child) do
FactoryBot.create(:work_package, project: project, start_date: '2016-01-06', due_date: '2016-01-10').tap do |child|
FactoryBot.create(:hierarchy_relation, from: wp_suc, to: child)
end
end
let(:user) { FactoryBot.create :admin }
let(:work_packages_page) { Pages::SplitWorkPackage.new(wp, project) }
let(:start_date) { work_packages_page.edit_field(:startDate) }
before do
login_as(user)
work_packages_page.visit!
work_packages_page.ensure_page_loaded
end
it 'can toggle the scheduling mode through the date modal' do
expect(wp.schedule_manually).to eq false
start_date.activate!
start_date.expect_active!
start_date.expect_scheduling_mode manually: false
start_date.toggle_scheduling_mode
start_date.expect_scheduling_mode manually: true
start_date.save!
work_packages_page.expect_and_dismiss_notification message: 'Successful update.'
work_package.reload
expect(wp.schedule_manually).to eq true
end
end

@ -1,67 +0,0 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
require 'features/page_objects/notification'
require 'features/work_packages/details/inplace_editor/shared_examples'
require 'features/work_packages/shared_contexts'
require 'support/edit_fields/edit_field'
require 'features/work_packages/work_packages_page'
describe 'toggle scheduling mode',
js: true do
let(:project) { FactoryBot.create :project_with_types, public: true }
let(:work_package) { FactoryBot.create :work_package, project: project, start_date: '2016-01-01' }
let(:user) { FactoryBot.create :admin }
let(:work_packages_page) { Pages::FullWorkPackage.new(work_package, project) }
let(:start_date) { work_packages_page.edit_field(:startDate) }
before do
login_as(user)
work_packages_page.visit!
work_packages_page.ensure_page_loaded
end
it 'can toggle the scheduling mode through the date modal' do
expect(work_package.schedule_manually).to eq false
start_date.activate!
start_date.expect_active!
start_date.expect_scheduling_mode manually: false
start_date.toggle_scheduling_mode
start_date.expect_scheduling_mode manually: true
start_date.save!
work_packages_page.expect_and_dismiss_notification message: 'Successful update.'
work_package.reload
expect(work_package.schedule_manually).to eq true
end
end

@ -0,0 +1,423 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2020 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-2017 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 docs/COPYRIGHT.rdoc for more details.
#++
require 'spec_helper'
describe WorkPackages::Scopes::ForScheduling, 'allowed scope' do
let(:project) { FactoryBot.create(:project) }
let(:origin) { FactoryBot.create(:work_package, project: project) }
let(:predecessor) do
FactoryBot.create(:work_package, project: project).tap do |pre|
FactoryBot.create(:follows_relation, from: origin, to: pre)
end
end
let(:parent) do
FactoryBot.create(:work_package, project: project).tap do |par|
FactoryBot.create(:hierarchy_relation, from: par, to: origin)
end
end
let(:grandparent) do
FactoryBot.create(:work_package, project: project).tap do |grand|
FactoryBot.create(:hierarchy_relation, from: grand, to: parent)
end
end
let(:successor) do
FactoryBot.create(:work_package, project: project).tap do |suc|
FactoryBot.create(:follows_relation, from: suc, to: origin)
end
end
let(:successor2) do
FactoryBot.create(:work_package, project: project).tap do |suc|
FactoryBot.create(:follows_relation, from: suc, to: origin)
end
end
let(:successor_parent) do
FactoryBot.create(:work_package, project: project).tap do |par|
FactoryBot.create(:hierarchy_relation, from: par, to: successor)
end
end
let(:successor_child) do
FactoryBot.create(:work_package, project: project).tap do |chi|
FactoryBot.create(:hierarchy_relation, from: successor, to: chi)
end
end
let(:successor_child2) do
FactoryBot.create(:work_package, project: project).tap do |chi|
FactoryBot.create(:hierarchy_relation, from: successor, to: chi)
end
end
let(:successor_successor) do
FactoryBot.create(:work_package, project: project).tap do |suc|
FactoryBot.create(:follows_relation, from: suc, to: successor)
end
end
let(:parent_successor) do
FactoryBot.create(:work_package, project: project).tap do |suc|
FactoryBot.create(:follows_relation, from: suc, to: parent)
end
end
let(:parent_successor_parent) do
FactoryBot.create(:work_package, project: project).tap do |par|
FactoryBot.create(:hierarchy_relation, from: par, to: parent_successor)
end
end
let(:parent_successor_child) do
FactoryBot.create(:work_package, project: project).tap do |chi|
FactoryBot.create(:hierarchy_relation, from: parent_successor, to: chi)
end
end
let(:blocker) do
FactoryBot.create(:work_package, project: project).tap do |blo|
FactoryBot.create(:relation, relation_type: 'blocks', from: blo, to: origin)
end
end
let(:includer) do
FactoryBot.create(:work_package, project: project).tap do |inc|
FactoryBot.create(:relation, relation_type: 'includes', from: inc, to: origin)
end
end
let(:existing_work_packages) { [] }
describe '.fetch' do
it 'is a AR scope' do
expect(described_class.fetch([origin]))
.to be_a ActiveRecord::Relation
end
context 'for an empty array' do
it 'is empty' do
expect(described_class.fetch([]))
.to be_empty
end
end
context 'for a work package with a predecessor' do
let!(:existing_work_packages) { [predecessor] }
it 'is empty' do
expect(described_class.fetch([origin]))
.to be_empty
end
end
context 'for a work package with a parent' do
let!(:existing_work_packages) { [parent] }
it 'consists of the parent' do
expect(described_class.fetch([origin]))
.to match_array([parent])
end
end
context 'for a work package with a successor' do
let!(:existing_work_packages) { [successor] }
it 'consists of the successor' do
expect(described_class.fetch([origin]))
.to match_array([successor])
end
end
context 'for a work package with a blocking work package' do
let!(:existing_work_packages) { [blocker] }
it 'is empty' do
expect(described_class.fetch([origin]))
.to be_empty
end
end
context 'for a work package with an including work package' do
let!(:existing_work_packages) { [includer] }
it 'is empty' do
expect(described_class.fetch([origin]))
.to be_empty
end
end
context 'for a work package with a successor which has parent and child' do
let!(:existing_work_packages) { [successor, successor_child, successor_parent] }
context 'with all scheduled automatically' do
it 'consists of the successor, its child and parent' do
expect(described_class.fetch([origin]))
.to match_array([successor, successor_child, successor_parent])
end
end
context 'with successor scheduled manually' do
before do
successor.update_column(:schedule_manually, true)
end
it 'is empty' do
expect(described_class.fetch([origin]))
.to be_empty
end
end
context 'with the successor\'s parent scheduled manually' do
before do
successor_parent.update_column(:schedule_manually, true)
end
it 'consists of the successor and its child' do
expect(described_class.fetch([origin]))
.to match_array([successor, successor_child])
end
end
context 'with successor\'s child scheduled manually' do
before do
successor_child.update_column(:schedule_manually, true)
end
it 'is empty' do
expect(described_class.fetch([origin]))
.to be_empty
end
end
end
context 'for a work package with a successor which has parent and child and a successor of its own which is also a child of parent' do
let!(:existing_work_packages) { [successor, successor_child, successor_parent, successor_successor] }
before do
FactoryBot.create(:hierarchy_relation, from: successor_parent, to: successor_successor)
end
context 'with all scheduled automatically' do
it 'consists of the successor, its child and parent and the successor successor' do
expect(described_class.fetch([origin]))
.to match_array([successor, successor_child, successor_parent, successor_successor])
end
end
context 'with successor parent scheduled manually' do
before do
successor_parent.update_column(:schedule_manually, true)
end
it 'consists of the successor, its child and successor successor' do
expect(described_class.fetch([origin]))
.to match_array([successor, successor_child, successor_successor])
end
end
context 'with successor\'s child scheduled manually' do
before do
successor_child.update_column(:schedule_manually, true)
end
it 'is empty' do
expect(described_class.fetch([origin]))
.to be_empty
end
end
end
context 'for a work package with a successor which has parent and the parent has a follows relationship itself' do
let!(:existing_work_packages) { [successor, successor_parent] }
before do
FactoryBot.create(:follows_relation, from: successor_parent, to: origin)
end
context 'with all scheduled automatically' do
it 'consists of the successor, its child and parent' do
expect(described_class.fetch([origin]))
.to match_array([successor, successor_parent])
end
end
context 'with successor scheduled manually' do
before do
successor.update_column(:schedule_manually, true)
end
it 'is empty (hierarchy over relationships)' do
expect(described_class.fetch([origin]))
.to be_empty
end
end
context 'with the successor\'s parent scheduled manually' do
before do
successor_parent.update_column(:schedule_manually, true)
end
it 'consists of the successor' do
expect(described_class.fetch([origin]))
.to match_array([successor])
end
end
context 'both scheduled manually' do
before do
successor.update_column(:schedule_manually, true)
successor_parent.update_column(:schedule_manually, true)
end
it 'is empty' do
expect(described_class.fetch([origin]))
.to be_empty
end
end
end
context 'for a work package with a successor with two children and the successor having a successor' do
let!(:existing_work_packages) { [successor, successor_child, successor_child2, successor_successor] }
context 'with all scheduled automatically' do
it 'consists of the successor, its child and parent' do
expect(described_class.fetch([origin]))
.to match_array([successor, successor_child, successor_child2, successor_successor])
end
end
context 'with one of the successor`s children scheduled manually' do
before do
successor_child2.update_column(:schedule_manually, true)
end
it 'is empty' do
expect(described_class.fetch([origin]))
.to match_array([successor_child, successor, successor_successor])
end
end
context 'with both of the successor`s children scheduled manually' do
before do
successor_child.update_column(:schedule_manually, true)
successor_child2.update_column(:schedule_manually, true)
end
it 'is empty' do
expect(described_class.fetch([origin]))
.to be_empty
end
end
end
context 'for a work package with a parent and grandparent' do
let!(:existing_work_packages) { [parent, grandparent] }
it 'consists of the parent, grandparent' do
expect(described_class.fetch([origin]))
.to match_array([parent, grandparent])
end
end
context 'for a work package with a parent which has a successor' do
let!(:existing_work_packages) { [parent, parent_successor] }
it 'consists of the parent, parent successor' do
expect(described_class.fetch([origin]))
.to match_array([parent, parent_successor])
end
end
context 'for a work package with a parent which has a successor which has parent and child' do
let!(:existing_work_packages) { [parent, parent_successor, parent_successor_child, parent_successor_parent] }
context 'with all scheduled automatically' do
it 'consists of the parent, self and the whole parent successor hierarchy' do
expect(described_class.fetch([origin]))
.to match_array([parent, parent_successor, parent_successor_parent, parent_successor_child])
end
end
context 'with the parent successor scheduled manually' do
before do
parent_successor.update_column(:schedule_manually, true)
end
it 'consists of the parent' do
expect(described_class.fetch([origin]))
.to match_array([parent])
end
end
context 'with the parent scheduled manually' do
before do
parent.update_column(:schedule_manually, true)
end
it 'is empty' do
expect(described_class.fetch([origin]))
.to be_empty
end
end
context 'with the parent successor\'s child scheduled manually' do
before do
parent_successor_child.update_column(:schedule_manually, true)
end
it 'contains the parent and self' do
expect(described_class.fetch([origin]))
.to match_array([parent])
end
end
end
context 'for a work package with a successor that has a successor' do
let!(:existing_work_packages) { [successor, successor_successor] }
context 'with all scheduled automatically' do
it 'consists of the both successors' do
expect(described_class.fetch([origin]))
.to match_array([successor, successor_successor])
end
end
context 'with the successor scheduled manually' do
before do
successor.update_column(:schedule_manually, true)
end
it 'is empty' do
expect(described_class.fetch([origin]))
.to be_empty
end
end
context 'with the successor\'s successor scheduled manually' do
before do
successor_successor.update_column(:schedule_manually, true)
end
it 'contains the successor' do
expect(described_class.fetch([origin]))
.to match_array([successor])
end
end
end
end
end

@ -41,9 +41,7 @@ describe WorkPackages::SetScheduleService do
let(:instance) do
described_class.new(user: user, work_package: work_package)
end
let(:following) do
{ [work_package] => [] }
end
let!(:following) { [] }
let(:user) { FactoryBot.build_stubbed(:user) }
let(:type) { FactoryBot.build_stubbed(:type) }
@ -62,7 +60,7 @@ describe WorkPackages::SetScheduleService do
allow(work_package)
.to receive(:follows_relations)
.and_return relations
.and_return relations
work_package
end
@ -78,7 +76,7 @@ describe WorkPackages::SetScheduleService do
allow(child)
.to receive(:parent_relation)
.and_return relation
.and_return relation
child
end
@ -146,16 +144,14 @@ describe WorkPackages::SetScheduleService do
subject { instance.call(attributes) }
before do
following.each do |wp, results|
allow(WorkPackage)
.to receive(:hierarchy_tree_following)
.with(wp)
.and_return(results)
allow(results)
.to receive(:includes)
.and_return(results)
end
allow(WorkPackage)
.to receive(:for_scheduling)
.with([work_package])
.and_return(following)
allow(following)
.to receive(:includes)
.and_return(following)
end
let(:attributes) { [:start_date] }
@ -200,15 +196,8 @@ describe WorkPackages::SetScheduleService do
end
context 'with a single successor' do
let(:following) do
{
[work_package] => [following_work_package1],
[following_work_package1] => []
}
end
before do
following_work_package1
let!(:following) do
[following_work_package1]
end
context 'moving forward' do
@ -500,11 +489,8 @@ describe WorkPackages::SetScheduleService do
FactoryBot.build_stubbed(:stubbed_work_package)
end
let(:work_package_start_date) { Date.today - 5.days }
let(:following) do
{
[work_package] => [parent_work_package],
[parent_work_package] => []
}
let!(:following) do
[parent_work_package]
end
before do
@ -523,18 +509,9 @@ describe WorkPackages::SetScheduleService do
end
context 'with a single successor having a parent' do
let(:following) do
{
[work_package] => [following_work_package1,
parent_following_work_package1],
[following_work_package1,
parent_following_work_package1] => []
}
end
before do
following_work_package1
parent_following_work_package1
let!(:following) do
[following_work_package1,
parent_following_work_package1]
end
context 'moving forward' do
@ -554,14 +531,10 @@ describe WorkPackages::SetScheduleService do
let(:parent_follower1_start_date) { follower1_start_date }
let(:parent_follower1_due_date) { follower1_due_date + 4.days }
let(:following) do
{
[work_package] => [following_work_package1,
parent_following_work_package1,
follower_sibling_work_package],
[following_work_package1,
parent_following_work_package1] => []
}
let!(:following) do
[following_work_package1,
parent_following_work_package1,
follower_sibling_work_package]
end
before do
@ -667,14 +640,10 @@ describe WorkPackages::SetScheduleService do
let(:parent_follower1_start_date) { follower1_start_date }
let(:parent_follower1_due_date) { follower1_due_date + 4.days }
let(:following) do
{
[work_package] => [following_work_package1,
parent_following_work_package1,
follower_sibling_work_package],
[following_work_package1,
parent_following_work_package1] => []
}
let!(:following) do
[following_work_package1,
parent_following_work_package1,
follower_sibling_work_package]
end
before do
@ -700,18 +669,9 @@ describe WorkPackages::SetScheduleService do
let(:child_work_package) { stub_follower_child(following_work_package1, child_start_date, child_due_date) }
let(:following) do
{
[work_package] => [following_work_package1,
child_work_package],
[following_work_package1,
child_work_package] => []
}
end
before do
following_work_package1
child_work_package
let!(:following) do
[following_work_package1,
child_work_package]
end
context 'moving forward' do
@ -739,15 +699,10 @@ describe WorkPackages::SetScheduleService do
let(:child1_work_package) { stub_follower_child(following_work_package1, child1_start_date, child1_due_date) }
let(:child2_work_package) { stub_follower_child(following_work_package1, child2_start_date, child2_due_date) }
let(:following) do
{
[work_package] => [following_work_package1,
child1_work_package,
child2_work_package],
[following_work_package1,
child1_work_package,
child2_work_package] => []
}
let!(:following) do
[following_work_package1,
child1_work_package,
child2_work_package]
end
before do
@ -837,21 +792,10 @@ describe WorkPackages::SetScheduleService do
let(:follower3_start_date) { Date.today + 9.day }
let(:follower3_due_date) { Date.today + 10.day }
let(:following) do
{
[work_package] => [following_work_package1,
following_work_package2,
following_work_package3],
[following_work_package1,
following_work_package2,
following_work_package3] => []
}
end
before do
following_work_package1
following_work_package2
following_work_package3
let!(:following) do
[following_work_package1,
following_work_package2,
following_work_package3]
end
context 'moving forward' do
@ -926,24 +870,11 @@ describe WorkPackages::SetScheduleService do
following_work_package2 => follower4_delay_2,
following_work_package3 => follower4_delay_3)
end
let(:following) do
{
[work_package] => [following_work_package1,
following_work_package2,
following_work_package3,
following_work_package4],
[following_work_package1,
following_work_package2,
following_work_package3,
following_work_package4] => []
}
end
before do
following_work_package1
following_work_package2
following_work_package3
following_work_package4
let!(:following) do
[following_work_package1,
following_work_package2,
following_work_package3,
following_work_package4]
end
context 'moving forward' do

Loading…
Cancel
Save