-
Notifications
You must be signed in to change notification settings - Fork 2.6k
/
Copy pathupdate_progress_job.rb
291 lines (264 loc) · 10.2 KB
/
update_progress_job.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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
#-- 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.
#++
# rubocop:disable Rails/SquishedSQLHeredocs
class WorkPackages::UpdateProgressJob < ApplicationJob
queue_with_priority :default
def perform(current_mode:, previous_mode:)
with_temporary_progress_table do
case current_mode
when "field"
unset_all_percent_complete_values if previous_mode == "disabled"
fix_remaining_work_set_with_100p_complete
fix_remaining_work_exceeding_work
fix_only_work_being_set
fix_only_remaining_work_being_set
derive_unset_remaining_work_from_work_and_p_complete
derive_unset_work_from_remaining_work_and_p_complete
derive_p_complete_from_work_and_remaining_work
when "status"
set_p_complete_from_status
fix_remaining_work_set_with_100p_complete
derive_unset_work_from_remaining_work_and_p_complete
derive_remaining_work_from_work_and_p_complete
else
raise "Unknown progress calculation mode: #{current_mode}, aborting."
end
update_totals
unset_total_p_complete
copy_progress_values_to_work_packages_and_update_journals
end
end
private
def with_temporary_progress_table
WorkPackage.transaction do
create_temporary_progress_table
yield
ensure
drop_temporary_progress_table
end
end
def create_temporary_progress_table
execute(<<~SQL)
CREATE UNLOGGED TABLE temp_wp_progress_values
AS SELECT
id,
status_id,
estimated_hours,
remaining_hours,
done_ratio,
NULL::double precision AS total_work,
NULL::double precision AS total_remaining_work,
NULL::integer AS total_p_complete
FROM work_packages
SQL
end
def drop_temporary_progress_table
execute(<<~SQL)
DROP TABLE temp_wp_progress_values
SQL
end
def unset_all_percent_complete_values
execute(<<~SQL.squish)
UPDATE temp_wp_progress_values
SET done_ratio = NULL
WHERE done_ratio IS NOT NULL
SQL
end
def fix_remaining_work_set_with_100p_complete
execute(<<~SQL.squish)
UPDATE temp_wp_progress_values
SET estimated_hours = remaining_hours,
remaining_hours = 0
WHERE estimated_hours IS NULL
AND remaining_hours IS NOT NULL
AND done_ratio = 100
SQL
end
def fix_remaining_work_exceeding_work
execute(<<~SQL.squish)
UPDATE temp_wp_progress_values
SET remaining_hours = CASE
WHEN done_ratio IS NULL THEN estimated_hours
ELSE ROUND((estimated_hours - (estimated_hours * done_ratio / 100.0))::numeric, 2)
END
WHERE remaining_hours > estimated_hours
SQL
end
def fix_only_work_being_set
execute(<<~SQL.squish)
UPDATE temp_wp_progress_values
SET remaining_hours = estimated_hours
WHERE estimated_hours IS NOT NULL
AND remaining_hours IS NULL
AND done_ratio IS NULL
SQL
end
def fix_only_remaining_work_being_set
execute(<<~SQL.squish)
UPDATE temp_wp_progress_values
SET estimated_hours = remaining_hours
WHERE estimated_hours IS NULL
AND remaining_hours IS NOT NULL
AND done_ratio IS NULL
SQL
end
def derive_unset_remaining_work_from_work_and_p_complete
execute(<<~SQL.squish)
UPDATE temp_wp_progress_values
SET remaining_hours = ROUND((estimated_hours - (estimated_hours * done_ratio / 100.0))::numeric, 2)
WHERE estimated_hours IS NOT NULL
AND remaining_hours IS NULL
AND done_ratio IS NOT NULL
SQL
end
def derive_remaining_work_from_work_and_p_complete
execute(<<~SQL.squish)
UPDATE temp_wp_progress_values
SET remaining_hours = ROUND((estimated_hours - (estimated_hours * done_ratio / 100.0))::numeric, 2)
WHERE estimated_hours IS NOT NULL
AND done_ratio IS NOT NULL
SQL
end
def derive_unset_work_from_remaining_work_and_p_complete
execute(<<~SQL.squish)
UPDATE temp_wp_progress_values
SET estimated_hours = ROUND((remaining_hours * 100 / (100 - done_ratio))::numeric, 2)
WHERE estimated_hours IS NULL
AND remaining_hours IS NOT NULL
AND done_ratio IS NOT NULL
SQL
end
def derive_p_complete_from_work_and_remaining_work
execute(<<~SQL.squish)
UPDATE temp_wp_progress_values
SET done_ratio = CASE
WHEN estimated_hours = 0 THEN NULL
ELSE (estimated_hours - remaining_hours) * 100 / estimated_hours
END
WHERE estimated_hours >= 0
AND remaining_hours >= 0
SQL
end
def set_p_complete_from_status
execute(<<~SQL.squish)
UPDATE temp_wp_progress_values
SET done_ratio = statuses.default_done_ratio
FROM statuses
WHERE temp_wp_progress_values.status_id = statuses.id
SQL
end
def update_totals
execute(<<~SQL)
UPDATE temp_wp_progress_values
SET total_work = totals.total_work,
total_remaining_work = totals.total_remaining_work,
total_p_complete = CASE
WHEN totals.total_work = 0 THEN NULL
ELSE (1 - (totals.total_remaining_work / totals.total_work)) * 100
END
FROM (
SELECT wp_tree.ancestor_id AS id,
SUM(estimated_hours) AS total_work,
SUM(remaining_hours) AS total_remaining_work
FROM work_package_hierarchies wp_tree
LEFT JOIN temp_wp_progress_values wp_progress ON wp_tree.descendant_id = wp_progress.id
GROUP BY wp_tree.ancestor_id
) totals
WHERE temp_wp_progress_values.id = totals.id
SQL
end
# The value for derived_done_ratio had been calculated wrong in the past. So prior to executing the job
# values in the work_packages and work_package_journals table sometimes contained wrong data.
# The whole job/migration is now treating the derived_done_ratio as a value newly introduced even if it, under
# the hood has existed before. But it was not shown in the activites before so the user would not have seen it.
#
# Because of this, all values, in the work_packages and work_package_journals table, for derived_done_ratio are
# reset to null.
#
# This results in two cases:
# * The value before has been something (most of the time 0) and is now null. This will hopefully be the
# majority of the cases as it would save a lot of journal creation, the slowest part of the job.
# For that case, the derived_done_ratio will be treated as not having changed by the job since with the rewrite
# the value looks to have been null before and is now null again.
# * The value before has been something and is now something. It could have been the same value as before. But
# since the job resets the value to null, it will in every case be treated as having changed (set for the first time)
def unset_total_p_complete
execute(<<~SQL)
UPDATE work_packages
SET derived_done_ratio = NULL
SQL
execute(<<~SQL)
UPDATE work_package_journals
SET derived_done_ratio = NULL
SQL
end
def copy_progress_values_to_work_packages_and_update_journals
updated_work_package_ids = copy_progress_values_to_work_packages
create_journals_for_updated_work_packages(updated_work_package_ids)
end
def copy_progress_values_to_work_packages
results = execute(<<~SQL)
UPDATE work_packages
SET estimated_hours = temp_wp_progress_values.estimated_hours,
remaining_hours = temp_wp_progress_values.remaining_hours,
done_ratio = temp_wp_progress_values.done_ratio,
derived_estimated_hours = temp_wp_progress_values.total_work,
derived_remaining_hours = temp_wp_progress_values.total_remaining_work,
derived_done_ratio = temp_wp_progress_values.total_p_complete,
lock_version = lock_version + 1,
updated_at = NOW()
FROM temp_wp_progress_values
WHERE work_packages.id = temp_wp_progress_values.id
AND (
work_packages.estimated_hours IS DISTINCT FROM temp_wp_progress_values.estimated_hours
OR work_packages.remaining_hours IS DISTINCT FROM temp_wp_progress_values.remaining_hours
OR work_packages.done_ratio IS DISTINCT FROM temp_wp_progress_values.done_ratio
OR work_packages.derived_estimated_hours IS DISTINCT FROM temp_wp_progress_values.total_work
OR work_packages.derived_remaining_hours IS DISTINCT FROM temp_wp_progress_values.total_remaining_work
OR work_packages.derived_done_ratio IS DISTINCT FROM temp_wp_progress_values.total_p_complete
)
RETURNING work_packages.id
SQL
results.column_values(0)
end
def create_journals_for_updated_work_packages(updated_work_package_ids)
WorkPackage.where(id: updated_work_package_ids).find_each do |work_package|
Journals::CreateService
.new(work_package, system_user)
.call(cause: Journal::CausedBySystemUpdate.new(feature: "progress_calculation_changed"))
end
end
# Executes an sql statement, shorter.
def execute(sql)
ActiveRecord::Base.connection.execute(sql)
end
def system_user
@system_user ||= User.system
end
end
# rubocop:enable Rails/SquishedSQLHeredocs