Skip to content

Commit 8b8f8f0

Browse files
committed
Make "Project copy" copy all work packages as-is
When doing a project copy, copy as-is and accept that the state can be faulty. Before, it was disabling parts of the validations while keeping some other parts, which made the experience confusing: the SetAttributesService would be ok, but then saving the work package would fail, and later the work package would be saved again without validation in the `UpdateAncestorsService`. Actually it was working previously only due to side-effects of `UpdateAncestorsService` which does save and also due to changes introduced in 0700299 which refactored the part that detect `save` returned `false`, leading to believe that `save` always returned `true`. Now the validation are done solely by the `SetAttributesService`, and when saving the work package validations are ignored.
1 parent b41671c commit 8b8f8f0

File tree

4 files changed

+66
-32
lines changed

4 files changed

+66
-32
lines changed

app/contracts/work_packages/copy_project_contract.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ class CopyProjectContract < CopyContract
4242
delegate :to_s,
4343
to: :model
4444

45+
def valid?(_context = nil)
46+
# For project copying, we want to preserve the exact state
47+
# even if copied work packages would normally be invalid
48+
true
49+
end
50+
4551
def validate_model? = false
4652

4753
private

app/services/work_packages/create_service.rb

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,14 @@ def perform
5555
def create(attributes, work_package)
5656
result = set_attributes(attributes, work_package)
5757

58-
result.success =
59-
if result.success
60-
work_package.attachments = work_package.attachments_replacements if work_package.attachments_replacements
61-
work_package.save
58+
if result.success?
59+
# Set attributes service passed, meaning the contract is fullfilled.
60+
# Avoid running validations again as we might be in a project copy scenario.
61+
work_package.attachments = work_package.attachments_replacements if work_package.attachments_replacements
62+
work_package.save(validate: false)
6263

63-
set_templated_subject(work_package)
64-
end
64+
update_subject_if_automatically_generated(work_package)
6565

66-
if result.success?
6766
# update ancestors before rescheduling, as the parent might switch to automatic mode
6867
multi_update_ancestors(result.all_results).each do |ancestor_result|
6968
result.merge!(ancestor_result)
@@ -77,11 +76,13 @@ def create(attributes, work_package)
7776
result
7877
end
7978

80-
def set_templated_subject(work_package)
81-
return true unless work_package.type&.replacement_pattern_defined_for?(:subject)
82-
83-
work_package.subject = work_package.type.enabled_patterns[:subject].resolve(work_package)
84-
work_package.save
79+
def update_subject_if_automatically_generated(work_package)
80+
if work_package.type&.replacement_pattern_defined_for?(:subject)
81+
Journal::NotificationConfiguration.with(false) do
82+
work_package.subject = work_package.type.enabled_patterns[:subject].resolve(work_package)
83+
work_package.save(validate: false)
84+
end
85+
end
8586
end
8687

8788
def set_attributes(attributes, work_package)

app/services/work_packages/update_ancestors_service.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def changes?(work_package)
8484
def save_updated_work_packages(updated_work_packages)
8585
updated_initiators, updated_ancestors = updated_work_packages.partition { initiator?(it) }
8686

87+
# TODO: Can we do better and not save if not changed?
8788
# Send notifications for initiator updates
8889
success = updated_initiators.all? { |wp| wp.save(validate: false) }
8990
# Do not send notifications for parent updates

spec/workers/copy_project_job_spec.rb

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,25 @@
3838

3939
before { allow(mail_double).to receive(:deliver_later) }
4040

41-
describe "copy project succeeds with errors" do
42-
let(:source_project) { create(:project, types: [type]) }
43-
44-
let!(:work_package) { create(:work_package, :skip_validations, done_ratio: 101, project: source_project, type:) }
41+
describe "with a source project having invalid work packages" do
42+
shared_let(:type) { create(:type_bug) }
43+
shared_let(:source_project) { create(:project, types: [type]) }
44+
45+
shared_let(:work_package_with_model_errors) do
46+
# done ratio must be between 0 and 100, this is a model validation
47+
create(:work_package, :skip_validations,
48+
project: source_project, type:,
49+
subject: "wp with invalid done_ratio",
50+
done_ratio: 101)
51+
end
52+
shared_let(:work_package_with_contract_errors) do
53+
# remaining work must be less than or equal to work, this is a WorkPackages::BaseContract validation
54+
create(:work_package, :skip_validations,
55+
project: source_project, type:,
56+
subject: "wp with invalid work and remaining work",
57+
remaining_hours: 10, estimated_hours: 2)
58+
end
4559

46-
let(:type) { create(:type_bug) }
4760
let(:job_args) do
4861
{
4962
target_project_params: params,
@@ -52,13 +65,8 @@
5265
end
5366

5467
let(:params) { { name: "Copy", identifier: "copy", type_ids: [type.id] } }
55-
let(:expected_error_message) do
56-
"#{WorkPackage.model_name.human} '#{work_package.type.name} ##{work_package.id}: " \
57-
"#{work_package.subject}': % Complete " \
58-
"#{I18n.t('activerecord.errors.models.work_package.attributes.done_ratio.inclusion')}"
59-
end
6068

61-
it "copies the project", :aggregate_failures do
69+
it "copies the project and invalid work packages without reporting any errors", :aggregate_failures do
6270
copy_job = nil
6371
batch = GoodJob::Batch.enqueue(user: admin, source_project:) do
6472
copy_job = described_class.perform_later(**job_args)
@@ -67,9 +75,12 @@
6775
batch.reload
6876

6977
copied_project = Project.find_by(identifier: params[:identifier])
70-
7178
expect(copied_project).to eq(batch.properties[:target_project])
72-
expect(batch.properties[:errors].first).to eq(expected_error_message)
79+
80+
# all work packages were copied, even if they are invalid
81+
expect(copied_project.work_packages.count).to eq(source_project.work_packages.count)
82+
# no errors reported
83+
expect(batch.properties[:errors]).to be_empty
7384

7485
# expect to create a status
7586
expect(copy_job.job_status).to be_present
@@ -80,15 +91,30 @@
8091
expect(copy_job.job_status[:payload]["_links"]["project"]).to eq(expected_link)
8192
end
8293

83-
it "ensures that error messages are correctly localized" do
84-
batch = GoodJob::Batch.enqueue(user: user_de, source_project:) do
85-
described_class.perform_later(**job_args)
94+
context "when the source project has automatically generated subjects" do
95+
before do
96+
type.update(patterns: { subject: { blueprint: "wp {{id}} in project {{project_id}}", enabled: true } })
8697
end
87-
GoodJob.perform_inline
88-
batch.reload
8998

90-
msg = /Arbeitspaket 'Bug #\d+: WorkPackage No. \d+': % abgeschlossen muss zwischen 0 und 100 liegen\./
91-
expect(batch.properties[:errors].first).to match(msg)
99+
it "copies the project without reporting any errors and generates subjects different from source project" do
100+
batch = GoodJob::Batch.enqueue(user: admin, source_project:) do
101+
described_class.perform_later(**job_args)
102+
end
103+
GoodJob.perform_inline
104+
batch.reload
105+
106+
copied_project = Project.find_by(identifier: params[:identifier])
107+
108+
# all work packages were copied, even if they are invalid
109+
expect(copied_project.work_packages.count).to eq(source_project.work_packages.count)
110+
# no errors reported
111+
expect(batch.properties[:errors]).to be_empty
112+
113+
# generated subjects are different from source project
114+
copied_project.work_packages.each do |work_package|
115+
expect(work_package.subject).to eq("wp #{work_package.id} in project #{copied_project.id}")
116+
end
117+
end
92118
end
93119
end
94120

0 commit comments

Comments
 (0)