|
|
|
@ -26,97 +26,63 @@ |
|
|
|
|
# See COPYRIGHT and LICENSE files for more details. |
|
|
|
|
#++ |
|
|
|
|
|
|
|
|
|
# TODO: use default update base class |
|
|
|
|
class WorkPackages::UpdateService < ::BaseServices::BaseCallable |
|
|
|
|
class WorkPackages::UpdateService < ::BaseServices::Update |
|
|
|
|
include ::WorkPackages::Shared::UpdateAncestors |
|
|
|
|
include ::Shared::ServiceContext |
|
|
|
|
include Attachments::ReplaceAttachments |
|
|
|
|
|
|
|
|
|
attr_accessor :user, |
|
|
|
|
:model, |
|
|
|
|
:contract_class |
|
|
|
|
private |
|
|
|
|
|
|
|
|
|
def initialize(user:, model:, contract_class: WorkPackages::UpdateContract) |
|
|
|
|
self.user = user |
|
|
|
|
self.model = model |
|
|
|
|
self.contract_class = contract_class |
|
|
|
|
end |
|
|
|
|
def after_perform(service_call) |
|
|
|
|
update_related_work_packages(service_call) |
|
|
|
|
cleanup(service_call.result) |
|
|
|
|
|
|
|
|
|
def perform(send_notifications: true, **attributes) |
|
|
|
|
in_context(model, send_notifications) do |
|
|
|
|
update(attributes) |
|
|
|
|
end |
|
|
|
|
service_call |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
private |
|
|
|
|
|
|
|
|
|
def update(attributes) |
|
|
|
|
result = set_attributes(attributes) |
|
|
|
|
|
|
|
|
|
if result.success? |
|
|
|
|
work_package.attachments = work_package.attachments_replacements if work_package.attachments_replacements |
|
|
|
|
result.merge!(update_dependent) |
|
|
|
|
def update_related_work_packages(service_call) |
|
|
|
|
update_ancestors([service_call.result]).each do |ancestor_service_call| |
|
|
|
|
service_call.merge!(ancestor_service_call) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
if save_if_valid(result) |
|
|
|
|
update_ancestors([work_package]).each do |ancestor_result| |
|
|
|
|
result.merge!(ancestor_result) |
|
|
|
|
end |
|
|
|
|
update_related(service_call.result).each do |related_service_call| |
|
|
|
|
service_call.merge!(related_service_call) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def save_if_valid(result) |
|
|
|
|
if result.success? |
|
|
|
|
result.success = consolidated_results(result) |
|
|
|
|
.all? { |wp| wp.save(validate: false) } |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
result.success? |
|
|
|
|
def update_related(work_package) |
|
|
|
|
consolidated_calls(update_descendants(work_package) + reschedule_related(work_package)) |
|
|
|
|
.reject { |dependent_call| dependent_call.result.id == work_package.id } |
|
|
|
|
.each { |dependent_call| dependent_call.result.save(validate: false) } |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def update_dependent |
|
|
|
|
result = ServiceResult.new(success: true, result: work_package) |
|
|
|
|
|
|
|
|
|
result.merge!(update_descendants) |
|
|
|
|
|
|
|
|
|
cleanup if result.success? |
|
|
|
|
|
|
|
|
|
result.merge!(reschedule_related) |
|
|
|
|
def update_descendants(work_package) |
|
|
|
|
if work_package.saved_change_to_project_id? |
|
|
|
|
attributes = { project: work_package.project } |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
work_package.descendants.map do |descendant| |
|
|
|
|
set_descendant_attributes(attributes, descendant) |
|
|
|
|
end |
|
|
|
|
else |
|
|
|
|
[] |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def set_attributes(attributes, wp = work_package) |
|
|
|
|
def set_descendant_attributes(attributes, descendant) |
|
|
|
|
WorkPackages::SetAttributesService |
|
|
|
|
.new(user:, |
|
|
|
|
model: wp, |
|
|
|
|
contract_class:) |
|
|
|
|
model: descendant, |
|
|
|
|
contract_class: EmptyContract) |
|
|
|
|
.call(attributes) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def update_descendants |
|
|
|
|
result = ServiceResult.new(success: true, result: work_package) |
|
|
|
|
|
|
|
|
|
if work_package.project_id_changed? |
|
|
|
|
attributes = { project: work_package.project } |
|
|
|
|
|
|
|
|
|
work_package.descendants.each do |descendant| |
|
|
|
|
result.add_dependent!(set_attributes(attributes, descendant)) |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def cleanup |
|
|
|
|
if work_package.project_id_changed? |
|
|
|
|
def cleanup(work_package) |
|
|
|
|
if work_package.saved_change_to_project_id? |
|
|
|
|
moved_work_packages = [work_package] + work_package.descendants |
|
|
|
|
delete_relations(moved_work_packages) |
|
|
|
|
move_time_entries(moved_work_packages, work_package.project_id) |
|
|
|
|
end |
|
|
|
|
if work_package.type_id_changed? |
|
|
|
|
reset_custom_values |
|
|
|
|
if work_package.saved_change_to_type_id? |
|
|
|
|
reset_custom_values(work_package) |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
@ -134,60 +100,29 @@ class WorkPackages::UpdateService < ::BaseServices::BaseCallable |
|
|
|
|
.update_all(project_id:) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def reset_custom_values |
|
|
|
|
def reset_custom_values(work_package) |
|
|
|
|
work_package.reset_custom_values! |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def reschedule_related |
|
|
|
|
result = ServiceResult.new(success: true, result: work_package) |
|
|
|
|
|
|
|
|
|
with_temporarily_persisted_parent_changes do |
|
|
|
|
if work_package.parent_id_changed? && work_package.parent_id_was |
|
|
|
|
result.merge!(reschedule_former_siblings) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
result.merge!(reschedule(work_package)) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
result |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def with_temporarily_persisted_parent_changes |
|
|
|
|
# Explicitly using requires_new: true since we are already within a transaction. |
|
|
|
|
# Because of that, raising ActiveRecord::Rollback would have no effect: |
|
|
|
|
# https://www.bigbinary.com/learn-rubyonrails-book/activerecord-transactions-in-depth#nested-transactions |
|
|
|
|
WorkPackage.transaction(requires_new: true) do |
|
|
|
|
if work_package.parent_id_changed? |
|
|
|
|
# HACK: we need to persist the parent relation before rescheduling the parent |
|
|
|
|
# and the former parent since we rely on the database for scheduling. |
|
|
|
|
# The following will update the parent_id of the work package without that being noticed by the |
|
|
|
|
# work package instance (work_package) that is already instantiated. That way, the change can be rolled |
|
|
|
|
# back without any side effects to the instance (e.g. dirty tracking). |
|
|
|
|
WorkPackage.where(id: work_package.id).update_all(parent_id: work_package.parent_id) |
|
|
|
|
work_package.rebuild! # using the ClosureTree#rebuild! method to update the transitive hierarchy information |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
yield |
|
|
|
|
def reschedule_related(work_package) |
|
|
|
|
rescheduled = if work_package.saved_change_to_parent_id? && work_package.parent_id_before_last_save |
|
|
|
|
reschedule_former_siblings(work_package).dependent_results |
|
|
|
|
else |
|
|
|
|
[] |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# Always rolling back the changes we made in here |
|
|
|
|
raise ActiveRecord::Rollback |
|
|
|
|
end |
|
|
|
|
rescheduled + reschedule(work_package, [work_package]).dependent_results |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# Rescheduling the former siblings will lead to the whole former tree being rescheduled. |
|
|
|
|
def reschedule_former_siblings |
|
|
|
|
reschedule(WorkPackage.where(parent_id: work_package.parent_id_was)) |
|
|
|
|
def reschedule_former_siblings(work_package) |
|
|
|
|
reschedule(work_package, WorkPackage.where(parent_id: work_package.parent_id_before_last_save)) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def reschedule(work_packages) |
|
|
|
|
def reschedule(work_package, work_packages) |
|
|
|
|
WorkPackages::SetScheduleService |
|
|
|
|
.new(user:, |
|
|
|
|
work_package: work_packages) |
|
|
|
|
.call(changed_attributes) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def changed_attributes |
|
|
|
|
work_package.changed.map(&:to_sym) |
|
|
|
|
.call(work_package.saved_changes.keys.map(&:to_sym)) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
# When multiple services change a work package, we still only want one update to the database due to: |
|
|
|
@ -195,19 +130,15 @@ class WorkPackages::UpdateService < ::BaseServices::BaseCallable |
|
|
|
|
# * having only one journal entry |
|
|
|
|
# * stale object errors |
|
|
|
|
# we thus consolidate the results so that one instance contains the changes made by all the services. |
|
|
|
|
def consolidated_results(result) |
|
|
|
|
result.all_results.group_by(&:id).inject([]) do |a, (_, instances)| |
|
|
|
|
master = instances.pop |
|
|
|
|
|
|
|
|
|
instances.each do |instance| |
|
|
|
|
master.attributes = instance.changes.transform_values(&:last) |
|
|
|
|
def consolidated_calls(service_calls) |
|
|
|
|
service_calls |
|
|
|
|
.group_by { |sc| sc.result.id } |
|
|
|
|
.map do |(_, same_work_package_calls)| |
|
|
|
|
same_work_package_calls.pop.tap do |master| |
|
|
|
|
same_work_package_calls.each do |sc| |
|
|
|
|
master.result.attributes = sc.result.changes.transform_values(&:last) |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
a + [master] |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def work_package |
|
|
|
|
model |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|