From ba994897af6a438b87169905c68d801631a8dadd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 25 Nov 2025 10:38:04 +0100 Subject: [PATCH 1/4] Upload artifact in storage --- .../create_artifact_work_package_service.rb | 51 ++++++++++++- config/locales/en.yml | 1 + ...eate_artifact_work_package_service_spec.rb | 71 +++++++++++++++++++ 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/app/services/projects/create_artifact_work_package_service.rb b/app/services/projects/create_artifact_work_package_service.rb index a156d95444e..a5c2db65fb0 100644 --- a/app/services/projects/create_artifact_work_package_service.rb +++ b/app/services/projects/create_artifact_work_package_service.rb @@ -58,6 +58,44 @@ module Projects service_call end + def after_perform(service_call) + return service_call if store_attachment_locally? + + if project_storage.nil? + service_call.errors.add(:base, I18n.t("projects.wizard.create_artifact_storage_error")) + return service_call + end + + upload_artifact_to_storage(service_call) + end + + def upload_artifact_to_storage(service_call) + export = create_pdf_export! + + storage_call = Storages::UploadFileService + .call( + container: service_call.result, + project_storage:, + file_path: project.project_creation_wizard_artifact_name, + file_data: StringIO.new(export.content), + filename: export.title + ) + + storage_call.on_failure do + service_call.errors.add(:base, I18n.t("projects.wizard.create_artifact_storage_error")) + end + + service_call + end + + def project_storage + return @project_storage if defined?(@project_storage) + + @project_storage = project + .project_storages + .find_by(id: project.project_creation_wizard_artifact_export_storage) + end + def create_artifact_work_package create_params = { project:, @@ -65,10 +103,10 @@ module Projects status_id: project.project_creation_wizard_status_when_submitted_id, subject:, assigned_to_id:, - attachments: [pdf_attachment], journal_notes: } + create_params[:attachments] = [pdf_attachment] if store_attachment_locally? WorkPackages::CreateService.new(user:).call(create_params) end @@ -86,6 +124,10 @@ module Projects scope: "settings.project_initiation_request.name.options") end + def store_attachment_locally? + project.project_creation_wizard_artifact_export_type == "attachment" + end + def assigned_to_id project.custom_value_for(assignee_custom_field).value end @@ -101,9 +143,12 @@ module Projects .find_by(id: project.project_creation_wizard_assignee_custom_field_id) end - def pdf_attachment - export = Project::PDFExport::ProjectInitiation.new(project).export! + def create_pdf_export! + Project::PDFExport::ProjectInitiation.new(project).export! + end + def pdf_attachment + export = create_pdf_export! file = OpenProject::Files.create_uploaded_file( name: export.title, content_type: export.mime_type, diff --git a/config/locales/en.yml b/config/locales/en.yml index adfdb1aa834..e0a653cc29a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -655,6 +655,7 @@ en: success: "Project attributes saved and artifact work package created successfully." progress_label: "%{current} of %{total}" create_artifact_work_package_error: "Failed to create artifact work package" + create_artifact_storage_error: "Failed to store artifact in file storage" lists: create: success: "The modified list has been saved as a new list" diff --git a/spec/services/projects/create_artifact_work_package_service_spec.rb b/spec/services/projects/create_artifact_work_package_service_spec.rb index 91be042a125..22cbb885c6c 100644 --- a/spec/services/projects/create_artifact_work_package_service_spec.rb +++ b/spec/services/projects/create_artifact_work_package_service_spec.rb @@ -47,6 +47,7 @@ RSpec.describe Projects::CreateArtifactWorkPackageService do shared_let(:project) do create( :project, + name: "Important Project", types: [type], project_custom_fields: [user_custom_field], # project initiation request settings @@ -156,5 +157,75 @@ RSpec.describe Projects::CreateArtifactWorkPackageService do expect(artifact_work_package.last_journal.notes).to include(project.project_creation_wizard_work_package_comment) expect(artifact_work_package.last_journal.notes).to include(/]+>@#{assignee_user.name}<\/mention>/) end + + context "when artifact storage is internal" do + it "attaches directly to the work package" do + project.update(project_creation_wizard_artifact_export_type: "attachment") + result = instance.call + project = result.result + + artifact_work_package = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id) + expect(artifact_work_package.attachments.count).to eq(1) + attachment = artifact_work_package.attachments.first + date = Time.zone.today.iso8601 + expect(attachment.content_type).to eq "application/pdf" + expect(attachment.filename).to match /Important_Project_project_mandate_#{date}_\d+-\d+.pdf/ + end + end + + context "when artifact storage is project storage" do + let(:storage) { create(:nextcloud_storage_with_local_connection) } + let(:project_storage) { create(:project_storage, project:, storage:, project_folder_id: "/project_folder") } + + let(:service_result) { ServiceResult.success(result: nil) } + + before do + project.update( + project_creation_wizard_artifact_export_type: "file_link", + project_creation_wizard_artifact_export_storage: project_storage.id + ) + + allow(Storages::UploadFileService) + .to receive(:call) + .and_return(service_result) + end + + it "calls the nextcloud storage service" do + result = instance.call + project = result.result + + expect(result).to be_success + artifact_work_package = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id) + expect(artifact_work_package.attachments.count).to eq(0) + + date = Time.zone.today.iso8601 + expect(Storages::UploadFileService) + .to have_received(:call) + .with(container: project, + project_storage:, + file_path: "project_mandate", + filename: /Important_Project_project_mandate_#{date}_\d+-\d+.pdf/, + file_data: instance_of(StringIO)) + end + + context "when service call fails" do + let(:service_result) { ServiceResult.failure(result: nil) } + + it "rolls back the work package" do + result = instance.call + project = result.result + + expect(Storages::UploadFileService) + .to have_received(:call) + + # The outer service is successful, but an error is added + expect(result).to be_success + expect(result.errors[:base]).to include "Failed to store artifact in file storage" + + artifact_work_package = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id) + expect(artifact_work_package.attachments.count).to eq(0) + end + end + end end end From 3f92daa79c71244c9bad8ee5e57782bb8ba64543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 25 Nov 2025 16:08:51 +0100 Subject: [PATCH 2/4] Remove duplicate spec --- .../create_artifact_work_package_service_spec.rb | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/spec/services/projects/create_artifact_work_package_service_spec.rb b/spec/services/projects/create_artifact_work_package_service_spec.rb index 22cbb885c6c..9f577069794 100644 --- a/spec/services/projects/create_artifact_work_package_service_spec.rb +++ b/spec/services/projects/create_artifact_work_package_service_spec.rb @@ -104,18 +104,6 @@ RSpec.describe Projects::CreateArtifactWorkPackageService do expect(artifact_work_package.assigned_to).to eq(assignee_user) end - it "attaches the project initiation request pdf file to the artifact work package" do - result = instance.call - project = result.result - - artifact_work_package = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id) - expect(artifact_work_package.attachments.count).to eq(1) - - attachment = artifact_work_package.attachments.first - expect(attachment.file.content_type).to eq("application/pdf") - expect(attachment.author).to eq(current_user) - end - it "sets the subject to the artifact name configured in the project initiation request settings" do result = instance.call project = result.result From 944b95303d4e4d4215ee484788efa0550ccfed7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 25 Nov 2025 16:10:22 +0100 Subject: [PATCH 3/4] Use Date.current --- .../projects/create_artifact_work_package_service_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/services/projects/create_artifact_work_package_service_spec.rb b/spec/services/projects/create_artifact_work_package_service_spec.rb index 9f577069794..c3af38aaaf2 100644 --- a/spec/services/projects/create_artifact_work_package_service_spec.rb +++ b/spec/services/projects/create_artifact_work_package_service_spec.rb @@ -155,9 +155,9 @@ RSpec.describe Projects::CreateArtifactWorkPackageService do artifact_work_package = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id) expect(artifact_work_package.attachments.count).to eq(1) attachment = artifact_work_package.attachments.first - date = Time.zone.today.iso8601 + date = Date.current.iso8601 expect(attachment.content_type).to eq "application/pdf" - expect(attachment.filename).to match /Important_Project_project_mandate_#{date}_\d+-\d+.pdf/ + expect(attachment.filename).to match /Important_Project_Project_mandate_#{date}_\d+-\d+.pdf/ end end @@ -186,13 +186,13 @@ RSpec.describe Projects::CreateArtifactWorkPackageService do artifact_work_package = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id) expect(artifact_work_package.attachments.count).to eq(0) - date = Time.zone.today.iso8601 + date = Date.current.iso8601 expect(Storages::UploadFileService) .to have_received(:call) .with(container: project, project_storage:, file_path: "project_mandate", - filename: /Important_Project_project_mandate_#{date}_\d+-\d+.pdf/, + filename: /Important_Project_Project_mandate_#{date}_\d+-\d+.pdf/, file_data: instance_of(StringIO)) end From 7681aaf64c99ace75fe888053f74b82cc2f8ddfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 25 Nov 2025 16:25:25 +0100 Subject: [PATCH 4/4] Merge without success --- .../projects/create_artifact_work_package_service.rb | 2 +- .../create_artifact_work_package_service_spec.rb | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/services/projects/create_artifact_work_package_service.rb b/app/services/projects/create_artifact_work_package_service.rb index a5c2db65fb0..eeeabacdf74 100644 --- a/app/services/projects/create_artifact_work_package_service.rb +++ b/app/services/projects/create_artifact_work_package_service.rb @@ -82,7 +82,7 @@ module Projects ) storage_call.on_failure do - service_call.errors.add(:base, I18n.t("projects.wizard.create_artifact_storage_error")) + service_call.merge!(storage_call, without_success: true) end service_call diff --git a/spec/services/projects/create_artifact_work_package_service_spec.rb b/spec/services/projects/create_artifact_work_package_service_spec.rb index c3af38aaaf2..617837f82b2 100644 --- a/spec/services/projects/create_artifact_work_package_service_spec.rb +++ b/spec/services/projects/create_artifact_work_package_service_spec.rb @@ -197,9 +197,13 @@ RSpec.describe Projects::CreateArtifactWorkPackageService do end context "when service call fails" do - let(:service_result) { ServiceResult.failure(result: nil) } + let(:service_result) do + ServiceResult.failure(result: nil).tap do |result| + result.errors.add(:base, "Something happened!") + end + end - it "rolls back the work package" do + it "keeps the work package, but shows an error" do result = instance.call project = result.result @@ -208,7 +212,7 @@ RSpec.describe Projects::CreateArtifactWorkPackageService do # The outer service is successful, but an error is added expect(result).to be_success - expect(result.errors[:base]).to include "Failed to store artifact in file storage" + expect(result.errors[:base]).to include "Something happened!" artifact_work_package = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id) expect(artifact_work_package.attachments.count).to eq(0)