From 4dc8e455a81e71f2ecf16763b9ab3fa65957efce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 4 Dec 2025 19:52:40 +0100 Subject: [PATCH 1/3] Use project_attributes_only contract flag for UpdateService We're only updating project attributes, so we shouldn't need edit_projects permissions --- app/controllers/projects/creation_wizard_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/creation_wizard_controller.rb b/app/controllers/projects/creation_wizard_controller.rb index d14d3db418d..5f5afc1a172 100644 --- a/app/controllers/projects/creation_wizard_controller.rb +++ b/app/controllers/projects/creation_wizard_controller.rb @@ -50,7 +50,7 @@ class Projects::CreationWizardController < ApplicationController def update # rubocop:disable Metrics/AbcSize service_call = Projects::UpdateService - .new(user: current_user, model: @project) + .new(user: current_user, model: @project, contract_options: { project_attributes_only: true }) .call(permitted_params.project) if service_call.success? From e2027b2684ea1beade3a14b9e0e400459db423c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 4 Dec 2025 19:52:55 +0100 Subject: [PATCH 2/3] Use User.execute_as_admin to submit PIR --- .../projects/creation_wizard_controller.rb | 44 ++-- .../project_creation_wizard_spec.rb | 0 .../wizard_from_template_flow_spec.rb | 213 ++++++++++++++++++ 3 files changed, 240 insertions(+), 17 deletions(-) rename spec/features/projects/{ => creation_wizard}/project_creation_wizard_spec.rb (100%) create mode 100644 spec/features/projects/creation_wizard/wizard_from_template_flow_spec.rb diff --git a/app/controllers/projects/creation_wizard_controller.rb b/app/controllers/projects/creation_wizard_controller.rb index 5f5afc1a172..3d2b9109c73 100644 --- a/app/controllers/projects/creation_wizard_controller.rb +++ b/app/controllers/projects/creation_wizard_controller.rb @@ -48,39 +48,49 @@ class Projects::CreationWizardController < ApplicationController respond_with_turbo_streams end - def update # rubocop:disable Metrics/AbcSize + def update service_call = Projects::UpdateService .new(user: current_user, model: @project, contract_options: { project_attributes_only: true }) .call(permitted_params.project) if service_call.success? if last_page? - creation_call = Projects::CreateArtifactWorkPackageService.new(user: current_user, model: @project).call - - # even when successful, there can be errors related to the artifact - # upload to Nextcloud that needs to be shown to the user - flash[:error] = creation_call.errors.full_messages # rubocop:disable Rails/ActionControllerFlashBeforeRender - if creation_call.success? - redirect_to project_work_packages_path(@project, @project.project_creation_wizard_artifact_work_package_id), - notice: I18n.t("projects.wizard.success") - else - render :show, - locals: { menu_name: :none }, - status: :unprocessable_entity - end + create_work_package_artifact else redirect_to project_creation_wizard_path(@project, section: params[:next_section]) end else @project = service_call.result - render :show, - locals: { menu_name: :none }, - status: :unprocessable_entity + render_wizard_error_step end end private + def render_wizard_error_step + render :show, + locals: { menu_name: :none }, + status: :unprocessable_entity + end + + def create_work_package_artifact # rubocop:disable Metrics/AbcSize + creation_call = User.execute_as_admin(current_user) do + Projects::CreateArtifactWorkPackageService + .new(user: current_user, model: @project) + .call + end + + # even when successful, there can be errors related to the artifact + # upload to Nextcloud that needs to be shown to the user + flash[:error] = creation_call.errors.full_messages # rubocop:disable Rails/ActionControllerFlashBeforeRender + if creation_call.success? + redirect_to project_work_packages_path(@project, @project.project_creation_wizard_artifact_work_package_id), + notice: I18n.t("projects.wizard.success") + else + render_wizard_error_step + end + end + def last_page? params[:finish] end diff --git a/spec/features/projects/project_creation_wizard_spec.rb b/spec/features/projects/creation_wizard/project_creation_wizard_spec.rb similarity index 100% rename from spec/features/projects/project_creation_wizard_spec.rb rename to spec/features/projects/creation_wizard/project_creation_wizard_spec.rb diff --git a/spec/features/projects/creation_wizard/wizard_from_template_flow_spec.rb b/spec/features/projects/creation_wizard/wizard_from_template_flow_spec.rb new file mode 100644 index 00000000000..c08feb5a6b1 --- /dev/null +++ b/spec/features/projects/creation_wizard/wizard_from_template_flow_spec.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +require "spec_helper" + +RSpec.describe "Project creation wizard from a template", + :js, + :with_cuprite, + with_good_job_batches: [CopyProjectJob, SendCopyProjectStatusEmailJob] do + include Components::Autocompleter::NgSelectAutocompleteHelpers + + # Role for template access - only copy_projects permission + shared_let(:template_role) do + create(:project_role, permissions: %i[copy_projects]) + end + + # Role assigned to users in newly created projects - only wizard permissions, NO add_work_packages + shared_let(:new_project_role) do + create(:project_role, + permissions: %i[view_work_packages view_project_attributes edit_project_attributes]) + end + + # Role for the assignee user - assigned via the user custom field role assignment + # This role gives work_package_assigned permission needed for artifact WP creation + shared_let(:assignee_role) do + create(:project_role, permissions: %i[work_package_assigned view_work_packages]) + end + + shared_let(:status_new) { create(:status, name: "New") } + shared_let(:status_in_progress) { create(:status, name: "In Progress") } + shared_let(:type) { create(:type, name: "Project initiation") } + shared_let(:default_priority) { create(:default_priority) } + + shared_let(:workflow) do + create(:workflow, + type:, + role: assignee_role, + old_status: status_new, + new_status: status_in_progress) + end + + shared_let(:section) do + create(:project_custom_field_section, name: "Project Information") + end + + shared_let(:string_custom_field) do + create(:string_project_custom_field, + name: "Project Code", + project_custom_field_section: section) + end + + shared_let(:user_custom_field) do + create(:user_project_custom_field, + name: "Project Validator", + project_custom_field_section: section, + role_id: assignee_role.id) + end + + shared_let(:user_assignee) do + create(:user, firstname: "Assignee", lastname: "User") + end + + shared_let(:template) do + create(:template_project, + name: "Wizard Template", + types: [type], + project_creation_wizard_enabled: true, + project_creation_wizard_work_package_type_id: type.id, + project_creation_wizard_status_when_submitted_id: status_new.id, + project_creation_wizard_assignee_custom_field_id: user_custom_field.id) + end + + # Enable custom fields for the template with wizard enabled + shared_let(:string_cf_mapping) do + create(:project_custom_field_project_mapping, + project: template, + project_custom_field: string_custom_field, + creation_wizard: true) + end + + shared_let(:user_cf_mapping) do + create(:project_custom_field_project_mapping, + project: template, + project_custom_field: user_custom_field, + creation_wizard: true) + end + + # User with only copy_projects on template, and add_project globally + # Will get new_project_role in the created project (which lacks add_work_packages) + current_user do + create(:user, + firstname: "Regular", + lastname: "User", + member_with_roles: { template => template_role }, + global_permissions: %i[add_project view_all_principals]) + end + + before do + # Configure the role that new project creators get + allow(Setting) + .to receive(:new_project_user_role_id) + .and_return(new_project_role.id.to_s) + end + + it "creates a project from template and completes the wizard, " \ + "creating an artifact work package despite lacking add_work_packages permission" do + # Verify user does NOT have add_work_packages permission through their role + expect(new_project_role.permissions).not_to include(:add_work_packages) + + # Verify the user custom field has role assignment configured + expect(user_custom_field.role_id).to eq(assignee_role.id) + + # Start project creation from template + visit new_project_path(template_id: template.id) + + # Step 2: Project details + expect(page).to have_heading "New project" + fill_in "Name", with: "My New Project" + + click_on "Complete" + + # Background job dialog appears + expect(page).to have_dialog "Background job status" + + within_dialog "Background job status" do + expect(page).to have_heading "Applying template" + end + + # Run background jobs + GoodJob.perform_inline + + # Should redirect to the creation wizard (because project_creation_wizard_enabled is true) + expect(page).to have_current_path(/\/projects\/my-new-project\/creation_wizard/, wait: 20) + + # Verify we're on the wizard page + expect(page).to have_css("h3", text: "Project Information") + + # Find the created project + project = Project.find_by(identifier: "my-new-project") + expect(project).to be_present + + # Verify user is a member with the new_project_role (no add_work_packages) + user_member = project.members.find_by(user_id: current_user.id) + expect(user_member).to be_present + expect(user_member.roles).to include(new_project_role) + expect(current_user).not_to be_allowed_in_project(:add_work_packages, project) + + # Fill in the wizard fields + fill_in "Project Code", with: "NEW-001" + + # Select the assignee user - this should also assign them the assignee_role + # via the CustomFieldsRole mechanism since user_custom_field has role_id set + select_autocomplete page.find("[data-custom-field-id='#{user_custom_field.id}']"), + results_selector: "body", + query: user_assignee.name + + # Complete the wizard - this should create the artifact work package + # via User.execute_as_admin despite lacking add_work_packages permission + click_button "Complete" + + expect(page).to have_text("Project attributes saved and artifact work package created successfully.") + + # Verify we're redirected to the artifact work package + project.reload + expect(project.project_creation_wizard_artifact_work_package_id).to be_present + expect(page).to have_current_path( + "/projects/#{project.identifier}/work_packages/#{project.project_creation_wizard_artifact_work_package_id}/activity" + ) + + # Verify the work package was created correctly + artifact_wp = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id) + expect(artifact_wp).to be_present + expect(artifact_wp.type).to eq(type) + expect(artifact_wp.status).to eq(status_new) + expect(artifact_wp.assigned_to).to eq(user_assignee) + + # Verify custom field values were saved + expect(project.typed_custom_value_for(string_custom_field)).to eq("NEW-001") + expect(project.typed_custom_value_for(user_custom_field)).to eq(user_assignee) + + # Verify the assignee was added as a member via the role assignment from the custom field + assignee_member = project.members.find_by(user_id: user_assignee.id) + expect(assignee_member).to be_present + expect(assignee_member.roles).to include(assignee_role) + end +end From b4ae77d75690401b69c91ad7509f7da9f3a7986f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Thu, 4 Dec 2025 19:57:33 +0100 Subject: [PATCH 3/3] Use flash.now when rendering directly --- app/controllers/projects/creation_wizard_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/creation_wizard_controller.rb b/app/controllers/projects/creation_wizard_controller.rb index 3d2b9109c73..d6078940d41 100644 --- a/app/controllers/projects/creation_wizard_controller.rb +++ b/app/controllers/projects/creation_wizard_controller.rb @@ -82,11 +82,12 @@ class Projects::CreationWizardController < ApplicationController # even when successful, there can be errors related to the artifact # upload to Nextcloud that needs to be shown to the user - flash[:error] = creation_call.errors.full_messages # rubocop:disable Rails/ActionControllerFlashBeforeRender if creation_call.success? + flash[:error] = creation_call.errors.full_messages if creation_call.errors.any? redirect_to project_work_packages_path(@project, @project.project_creation_wizard_artifact_work_package_id), notice: I18n.t("projects.wizard.success") else + flash.now[:error] = creation_call.errors.full_messages render_wizard_error_step end end