-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
set_attributes_service.rb
428 lines (340 loc) · 12.3 KB
/
set_attributes_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
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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
#-- 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::SetAttributesService < BaseServices::SetAttributes
include Attachments::SetReplacements
private
def set_attributes(attributes)
file_links_ids = attributes.delete(:file_links_ids)
model.file_links = Storages::FileLink.where(id: file_links_ids) if file_links_ids
set_attachments_attributes(attributes)
set_static_attributes(attributes)
model.change_by_system do
set_calculated_attributes(attributes)
end
set_custom_attributes(attributes)
end
def set_static_attributes(attributes)
assignable_attributes = attributes.select do |key, _|
!CustomField.custom_field_attribute?(key) && work_package.respond_to?(key)
end
work_package.attributes = assignable_attributes
end
def set_calculated_attributes(attributes)
if work_package.new_record?
set_default_attributes(attributes)
unify_milestone_dates
else
update_dates
end
shift_dates_to_soonest_working_days
update_duration
update_derivable
update_project_dependent_attributes
reassign_invalid_status_if_type_changed
set_templated_description
set_cause_for_readonly_attributes
end
def derivable_attribute
derivable_attribute_by_others_presence || derivable_attribute_by_others_absence
end
# Returns a field derivable by the presence of the two others, or +nil+ if
# none was found.
#
# Matching is done in the order :duration, :due_date, :start_date. The first
# one to match is returned.
#
# If +ignore_non_working_days+ has been changed, try deriving +due_date+ and
# +start_date+ before +duration+.
def derivable_attribute_by_others_presence
fields =
if work_package.ignore_non_working_days_changed?
%i[due_date start_date duration]
else
%i[duration due_date start_date]
end
fields.find { |field| derivable_by_others_presence?(field) }
end
# Returns true if given +field+ is derivable from the presence of the two
# others.
#
# A field is derivable if it has not been set explicitly while the other two
# fields are set.
def derivable_by_others_presence?(field)
others = %i[start_date due_date duration].without(field)
attribute_not_set_in_params?(field) && all_present?(*others)
end
# Returns a field derivable by the absence of one of the two others, or +nil+
# if none was found.
#
# Matching is done in the order :duration, :due_date, :start_date. The first
# one to match is returned.
def derivable_attribute_by_others_absence
%i[duration due_date start_date].find { |field| derivable_by_others_absence?(field) }
end
# Returns true if given +field+ is derivable from the absence of one of the
# two others.
#
# A field is derivable if it has not been set explicitly while the other two
# fields have one set and one nil.
#
# Note: if both other fields are nil, then the field is not derivable
def derivable_by_others_absence?(field)
others = %i[start_date due_date duration].without(field)
attribute_not_set_in_params?(field) && only_one_present?(*others)
end
def attribute_not_set_in_params?(field)
!params.has_key?(field)
end
def all_present?(*fields)
work_package.values_at(*fields).all?(&:present?)
end
def only_one_present?(*fields)
work_package.values_at(*fields).one?(&:present?)
end
# rubocop:disable Metrics/AbcSize
def update_derivable
case derivable_attribute
when :duration
work_package.duration =
if work_package.milestone?
1
else
days.duration(work_package.start_date, work_package.due_date)
end
when :due_date
work_package.due_date = days.due_date(work_package.start_date, work_package.duration)
when :start_date
work_package.start_date = days.start_date(work_package.due_date, work_package.duration)
end
end
# rubocop:enable Metrics/AbcSize
def set_default_attributes(attributes)
set_default_priority
set_default_author
set_default_status
set_default_start_date(attributes)
set_default_due_date(attributes)
end
def non_or_default_description?
work_package.description.blank? || false
end
def set_default_author
work_package.author ||= user
end
def set_default_status
work_package.status ||= Status.default
end
def set_default_priority
work_package.priority ||= IssuePriority.active.default
end
def set_default_start_date(attributes)
return if attributes.has_key?(:start_date)
work_package.start_date ||= if parent_start_earlier_than_due?
work_package.parent.start_date
elsif Setting.work_package_startdate_is_adddate?
Time.zone.today
end
end
def set_default_due_date(attributes)
return if attributes.has_key?(:due_date)
work_package.due_date ||= if parent_due_later_than_start?
work_package.parent.due_date
end
end
def set_templated_description
# We only set this if the work package is new
return unless work_package.new_record?
# And the type was changed
return unless work_package.type_id_changed?
# And the new type has a default text
default_description = work_package.type&.description
return if default_description.blank?
# And the current description matches ANY current default text
return unless work_package.description.blank? || default_description?
work_package.description = default_description
end
def default_description?
Type
.pluck(:description)
.compact
.map(&method(:normalize_whitespace))
.include?(normalize_whitespace(work_package.description))
end
def normalize_whitespace(string)
string.gsub(/\s/, ' ').squeeze(' ')
end
def set_custom_attributes(attributes)
assignable_attributes = attributes.select do |key, _|
CustomField.custom_field_attribute?(key) && work_package.respond_to?(key)
end
work_package.attributes = assignable_attributes
initialize_unset_custom_values
end
def custom_field_context_changed?
work_package.type_id_changed? || work_package.project_id_changed?
end
def work_package_now_milestone?
work_package.type_id_changed? && work_package.milestone?
end
def update_project_dependent_attributes
return unless work_package.project_id_changed? && work_package.project_id
model.change_by_system do
set_version_to_nil
reassign_category
set_parent_to_nil
reassign_type unless work_package.type_id_changed?
end
end
def update_dates
unify_milestone_dates
min_start = new_start_date
return unless min_start
work_package.due_date = new_due_date(min_start)
work_package.start_date = min_start
end
def unify_milestone_dates
return unless work_package_now_milestone?
unified_date = work_package.due_date || work_package.start_date
work_package.start_date = work_package.due_date = unified_date
end
def shift_dates_to_soonest_working_days
return if work_package.ignore_non_working_days?
work_package.start_date = days.soonest_working_day(work_package.start_date)
work_package.due_date = days.soonest_working_day(work_package.due_date)
end
def update_duration
work_package.duration = 1 if work_package.milestone?
end
def set_version_to_nil
if work_package.version &&
work_package.project &&
work_package.project.shared_versions.exclude?(work_package.version)
work_package.version = nil
end
end
def set_parent_to_nil
if !Setting.cross_project_work_package_relations? &&
!work_package.parent_changed?
work_package.parent = nil
end
end
def reassign_category
# work_package is moved to another project
# reassign to the category with same name if any
if work_package.category.present?
category = work_package.project.categories.find_by(name: work_package.category.name)
work_package.category = category
end
end
def reassign_type
available_types = work_package.project.types.order(:position)
return if available_types.include?(work_package.type) && work_package.type
work_package.type = available_types.first
update_duration
unify_milestone_dates
reassign_status assignable_statuses
end
def reassign_status(available_statuses)
return if available_statuses.include?(work_package.status) || work_package.status.is_a?(Status::InexistentStatus)
new_status = available_statuses.detect(&:is_default) || available_statuses.first || Status.default
work_package.status = new_status if new_status.present?
end
def reassign_invalid_status_if_type_changed
# Checks that the issue can not be moved to a type with the status unchanged
# and the target type does not have this status
if work_package.type_id_changed?
reassign_status work_package.type.statuses(include_default: true)
end
end
# Take over any default custom values
# for new custom fields
def initialize_unset_custom_values
work_package.set_default_values! if custom_field_context_changed?
end
def new_start_date
current_start_date = work_package.start_date || work_package.due_date
return unless current_start_date && work_package.schedule_automatically?
min_start = new_start_date_from_parent || new_start_date_from_self
min_start = days.soonest_working_day(min_start)
if min_start && (min_start > current_start_date || work_package.schedule_manually_changed?)
min_start
end
end
def new_start_date_from_parent
return unless work_package.parent_id_changed? &&
work_package.parent
work_package.parent.soonest_start
end
def new_start_date_from_self
return unless work_package.schedule_manually_changed?
[min_child_date, work_package.soonest_start].compact.max
end
def new_due_date(min_start)
duration = children_duration || work_package.duration
days.due_date(min_start, duration)
end
def work_package
model
end
def assignable_statuses
instantiate_contract(work_package, user).assignable_statuses(include_default: true)
end
def min_child_date
children_dates.min
end
def children_duration
max = max_child_date
return unless max
days.duration(min_child_date, max_child_date)
end
def days
WorkPackages::Shared::Days.for(work_package)
end
def max_child_date
children_dates.max
end
def children_dates
@children_dates ||= work_package.children.pluck(:start_date, :due_date).flatten.compact
end
def parent_start_earlier_than_due?
start = work_package.parent&.start_date
due = work_package.due_date || work_package.parent&.due_date
(start && !due) || ((due && start) && (start < due))
end
def parent_due_later_than_start?
due = work_package.parent&.due_date
start = work_package.start_date || work_package.parent&.start_date
(due && !start) || ((due && start) && (due > start))
end
def set_cause_for_readonly_attributes
return unless work_package.changes.keys.intersect?(%w(created_at updated_at author))
work_package.journal_cause = {
"type" => "default_attribute_written"
}
end
end