-
Notifications
You must be signed in to change notification settings - Fork 2.6k
/
Copy pathupdate_ancestors_service.rb
190 lines (158 loc) · 6.71 KB
/
update_ancestors_service.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 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::UpdateAncestorsService
attr_accessor :user,
:initiator_work_package
def initialize(user:, work_package:)
self.user = user
self.initiator_work_package = work_package
end
def call(attributes)
updated_work_packages = update_current_and_former_ancestors(attributes)
set_journal_note(ancestors(updated_work_packages))
success = save_updated_work_packages(updated_work_packages)
result = ServiceResult.new(success:, result: initiator_work_package)
ancestors(updated_work_packages).each do |wp|
result.add_dependent!(ServiceResult.new(success: !wp.changed?, result: wp))
end
result
end
private
def initiator?(work_package)
work_package == initiator_work_package
end
def ancestors(work_packages)
work_packages.reject { initiator?(_1) }
end
def update_current_and_former_ancestors(attributes)
include_former_ancestors = attributes.intersect?(%i[parent_id parent])
WorkPackages::UpdateAncestors::Loader
.new(initiator_work_package, include_former_ancestors)
.select do |ancestor, loader|
derive_attributes(ancestor, loader, attributes)
ancestor.changed?
end
end
def save_updated_work_packages(updated_work_packages)
updated_initiators, updated_ancestors = updated_work_packages.partition { initiator?(_1) }
# Send notifications for initiator updates
success = updated_initiators.all? { |wp| wp.save(validate: false) }
# Do not send notifications for parent updates
success &&= Journal::NotificationConfiguration.with(false) do
updated_ancestors.all? { |wp| wp.save(validate: false) }
end
success
end
def derive_attributes(work_package, loader, attributes)
return unless modified_attributes_justify_derivation?(attributes)
{
# Derived estimated hours and Derived remaining hours need to be
# calculated before the Derived done ratio below since the
# aggregation depends on both derived fields.
# Changes in any of these, also warrant a recalculation of
# the Derived done ratio.
#
# Changes to estimated hours also warrant a recalculation of
# derived done ratios in the work package's ancestry as the
# derived estimated hours would affect the derived done ratio
# or the derived remaining hours, depending on the % Complete mode
# currently active.
#
%i[estimated_hours remaining_hours] => :derive_total_estimated_and_remaining_hours,
%i[estimated_hours remaining_hours done_ratio status status_id] => :derive_done_ratio,
%i[ignore_non_working_days] => :derive_ignore_non_working_days
}.each do |derivative_attributes, method|
if attributes.intersect?(derivative_attributes + %i[parent parent_id])
send(method, work_package, loader)
end
end
end
def set_journal_note(work_packages)
work_packages.each do |wp|
wp.journal_notes = I18n.t("work_package.updated_automatically_by_child_changes", child: "##{initiator_work_package.id}")
end
end
def derive_done_ratio(ancestor, loader)
ancestor.derived_done_ratio = compute_derived_done_ratio(ancestor, loader)
end
def compute_derived_done_ratio(work_package, _loader)
return if work_package.derived_estimated_hours.nil? || work_package.derived_remaining_hours.nil?
if work_package.derived_estimated_hours.zero?
nil
else
work_done = (work_package.derived_estimated_hours - work_package.derived_remaining_hours)
progress = (work_done.to_f / work_package.derived_estimated_hours) * 100
progress.round
end
end
# Sets the ignore_non_working_days to true if any descendant has its value set to true.
# If there is no value returned from the descendants, that means that the work package in
# question no longer has a descendant. But since we are in the service going up the ancestor chain,
# such a work package is the former parent. The property of such a work package is reset to `false`.
def derive_ignore_non_working_days(ancestor, loader)
return if initiator?(ancestor)
return if ancestor.schedule_manually
descendant_value = ignore_non_working_days_of_descendants(ancestor, loader)
if descendant_value.nil?
descendant_value = ancestor.ignore_non_working_days
end
ancestor.ignore_non_working_days = descendant_value
end
def derive_total_estimated_and_remaining_hours(work_package, loader)
descendants = loader.descendants_of(work_package)
work_package.derived_estimated_hours = total(all_estimated_hours([work_package] + descendants))
work_package.derived_remaining_hours = total(all_remaining_hours([work_package] + descendants))
end
def total(hours)
hours.empty? ? nil : hours.sum.to_f
end
def all_estimated_hours(work_packages)
work_packages.filter_map(&:estimated_hours)
end
def all_remaining_hours(work_packages)
work_packages.filter_map(&:remaining_hours)
end
def modified_attributes_justify_derivation?(attributes)
attributes.intersect?(%i[
done_ratio
estimated_hours
ignore_non_working_days
parent parent_id
remaining_hours
status status_id
])
end
def ignore_non_working_days_of_descendants(ancestor, loader)
children = loader
.children_of(ancestor)
.reject(&:schedule_manually)
if children.any?
children.any?(&:ignore_non_working_days)
end
end
end