Merge pull request #21139 from opf/feature/68862-create-work-package-to-submit-project-initiation-request

[68862] Create work package to submit project initiation request
This commit is contained in:
Christophe Bliard
2025-11-25 15:53:06 +01:00
committed by GitHub
13 changed files with 713 additions and 33 deletions
@@ -0,0 +1,122 @@
# 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.
#++
module Projects
class CreateArtifactWorkPackageContract < ::BaseContract
validate :validate_project_initiation_request_enabled
validate :allowed_to_create_work_package
validate :validate_work_package_type
validate :validate_work_package_status
validate :validate_assignee_custom_field
def project = model
protected
def validate_project_initiation_request_enabled
if !project.project_creation_wizard_enabled?
add_error :base, :project_initiation_request_disabled
end
end
def allowed_to_create_work_package
return if user.allowed_in_project?(:add_work_packages, project)
add_error :base, :error_unauthorized
end
def validate_work_package_type
if project.project_creation_wizard_work_package_type_id.blank?
add_error :project_creation_wizard_work_package_type_id, :blank
elsif !project.project_creation_wizard_work_package_type_id.in?(project.type_ids)
add_error :project_creation_wizard_work_package_type_id, :inclusion
end
end
def validate_work_package_status
if project.project_creation_wizard_status_when_submitted_id.blank?
add_error :project_creation_wizard_status_when_submitted_id, :blank
elsif invalid_status_for_type?
add_error :project_creation_wizard_status_when_submitted_id, :inclusion
end
end
def validate_assignee_custom_field
if project_assignee_custom_field_not_configured?
add_error :project_creation_wizard_assignee_custom_field_id, :blank
elsif not_allowed_to_read_assignee_custom_field_value?
add_error assignee_custom_field.attribute_name, :unauthorized
elsif missing_assignee_custom_field_value?
add_error assignee_custom_field.attribute_name, :blank
elsif assignee_not_allowed_be_assigned_to_work_package?
add_error assignee_custom_field.attribute_name, :cannot_be_assigned_to_artifact_work_package
end
end
def project_assignee_custom_field_not_configured?
project.project_creation_wizard_assignee_custom_field_id.blank?
end
def not_allowed_to_read_assignee_custom_field_value?
# insufficient permissions to see the custom field value (current user is
# not a member of the project or other reason)
project.custom_value_for(assignee_custom_field).blank?
end
def missing_assignee_custom_field_value?
project.custom_value_for(assignee_custom_field).value.blank?
end
def assignee_not_allowed_be_assigned_to_work_package?
assignee = project.typed_custom_value_for(assignee_custom_field)
!assignee.allowed_in_project?(:work_package_assigned, project)
end
def assignee_custom_field
return @assignee_custom_field if defined?(@assignee_custom_field)
@assignee_custom_field = project.available_custom_fields
.find_by(id: project.project_creation_wizard_assignee_custom_field_id)
end
def invalid_status_for_type?
type = Type.find_by(id: project.project_creation_wizard_work_package_type_id)
return false if type.blank? # no extra error if there is already an error about type being blank
type.statuses.pluck(:id).exclude?(project.project_creation_wizard_status_when_submitted_id)
end
def add_error(attribute, error)
return if errors.added?(:base, :project_initiation_request_disabled)
errors.add attribute, error
end
end
end
@@ -54,14 +54,23 @@ class Projects::CreationWizardController < ApplicationController
.call(permitted_params.project)
if service_call.success?
if params[:finish]
redirect_to project_path(@project), notice: I18n.t("projects.wizard.success")
if last_page?
creation_call = Projects::CreateArtifactWorkPackageService.new(user: current_user, model: @project).call
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
flash[:error] = creation_call.errors.full_messages
render :show,
locals: { menu_name: :none },
status: :unprocessable_entity
end
else
redirect_to project_creation_wizard_path(@project, section: params[:next_section])
end
else
@project = service_call.result
@errors = service_call.errors
render :show,
locals: { menu_name: :none },
status: :unprocessable_entity
@@ -70,6 +79,10 @@ class Projects::CreationWizardController < ApplicationController
private
def last_page?
params[:finish]
end
def load_sections_and_fields
enabled_in_wizard_ids = @project
.project_custom_field_project_mappings
+3 -1
View File
@@ -30,6 +30,7 @@
module Projects::CreationWizard
ARTIFACT_NAME_OPTIONS = %w[project_creation_wizard project_initiation_request project_mandate].freeze
DEFAULT_ARTIFACT_NAME_OPTION = "project_creation_wizard"
extend ActiveSupport::Concern
@@ -42,13 +43,14 @@ module Projects::CreationWizard
store_attribute :settings, :project_creation_wizard_assignee_custom_field_id, :integer
store_attribute :settings, :project_creation_wizard_notification_text, :string
store_attribute :settings, :project_creation_wizard_work_package_comment, :string
store_attribute :settings, :project_creation_wizard_artifact_work_package_id, :integer
store_attribute :settings, :project_creation_wizard_artifact_export_type, :string, default: "attachment"
store_attribute :settings, :project_creation_wizard_artifact_export_storage, :string
# The store_attribute default cannot be used here, because the default is not returned
# when the JSON defintion is present but it's nil.
def project_creation_wizard_artifact_name
super.presence || "project_creation_wizard"
super.presence || DEFAULT_ARTIFACT_NAME_OPTION
end
end
end
@@ -0,0 +1,134 @@
# 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.
#++
module Projects
class CreateArtifactWorkPackageService < ::BaseServices::BaseContracted
include Contracted
prepend Projects::Concerns::UpdateDemoData
def initialize(user:, model:, contract_class: Projects::CreateArtifactWorkPackageContract)
super(user:, contract_class:)
self.model = model
end
def project = model
private
def persist(service_call)
creation_call = create_artifact_work_package
creation_call.on_success do
artifact_work_package = creation_call.result
project.project_creation_wizard_artifact_work_package_id = artifact_work_package.id
project.save
end
creation_call.on_failure do
service_call.errors.add(:base, I18n.t("projects.wizard.create_artifact_work_package_error"))
service_call.merge!(creation_call)
end
service_call
end
def create_artifact_work_package
create_params = {
project:,
type_id: project.project_creation_wizard_work_package_type_id,
status_id: project.project_creation_wizard_status_when_submitted_id,
subject:,
assigned_to_id:,
attachments: [pdf_attachment],
journal_notes:
}
WorkPackages::CreateService.new(user:).call(create_params)
end
def journal_notes
<<~COMMENT
#{mention_tag(assignee_user)}
#{project.project_creation_wizard_work_package_comment}
COMMENT
end
def subject
I18n.t(project.project_creation_wizard_artifact_name,
default: ::Projects::CreationWizard::DEFAULT_ARTIFACT_NAME_OPTION.to_sym,
scope: "settings.project_initiation_request.name.options")
end
def assigned_to_id
project.custom_value_for(assignee_custom_field).value
end
def assignee_user
User.find(assigned_to_id)
end
def assignee_custom_field
return @assignee_custom_field if defined?(@assignee_custom_field)
@assignee_custom_field = project.available_custom_fields
.find_by(id: project.project_creation_wizard_assignee_custom_field_id)
end
def pdf_attachment
export = Project::PDFExport::ProjectInitiation.new(project).export!
file = OpenProject::Files.create_uploaded_file(
name: export.title,
content_type: export.mime_type,
content: export.content,
binary: true
)
Attachment.new(
container: nil,
author: user,
file:
)
end
def mention_tag(user)
ApplicationController.helpers.content_tag(
"mention",
"@#{user.name}",
class: "mention",
data: {
id: user.id,
type: "user",
text: "@#{user.name}"
}
)
end
end
end
+10 -1
View File
@@ -73,7 +73,8 @@ class CopyProjectJob < ApplicationJob
end
def successful_status_update(target_project, errors)
payload = redirect_payload(url_helpers.project_url(target_project)).merge(hal_links(target_project))
payload = redirect_payload(success_redirect_url(target_project))
.merge(hal_links(target_project))
if errors.any?
payload[:errors] = errors
@@ -84,6 +85,14 @@ class CopyProjectJob < ApplicationJob
payload:
end
def success_redirect_url(target_project)
if target_project.project_creation_wizard_enabled
url_helpers.project_creation_wizard_path(target_project)
else
url_helpers.project_url(target_project)
end
end
def failure_status_update(errors)
message = I18n.t("copy_project.failed", source_project_name: source_project.name)
+4 -1
View File
@@ -652,8 +652,9 @@ en:
sections: "Sections"
title: "Project initiation request"
no_help_text: "This attribute has no help text defined."
success: "Project attributes have been saved successfully."
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"
lists:
create:
success: "The modified list has been saved as a new list"
@@ -1688,9 +1689,11 @@ en:
project:
archived_ancestor: "The project has an archived ancestor."
foreign_wps_reference_version: "Work packages in non descendant projects reference versions of the project or its descendants."
cannot_be_assigned_to_artifact_work_package: "The chosen user is not allowed to be assigned to work packages."
attributes:
base:
archive_permission_missing_on_subprojects: "You do not have the permissions required to archive all sub-projects. Please contact an administrator."
project_initiation_request_disabled: "Project initiation request is disabled. It must be enabled to create the artifact work package."
types:
in_use_by_work_packages: "still in use by work packages: %{types}"
enabled_modules:
@@ -30,7 +30,7 @@
# But we have references to symbols_for still in the code base.
module OpenProject::ActiveModelErrorsPatch
def symbols_for(attribute)
details[attribute].pluck(:error)
details[attribute.to_sym].pluck(:error)
end
end
@@ -61,5 +61,13 @@ module OpenProject::JobStatus
# That way, the result of a background job is available even after the original job is gone.
EventListener.register!
end
config.after_initialize do
if Rails.application.config.reloading_enabled?
Rails.autoloaders.main.on_unload(EventListener.name) do |klass, _abspath|
klass.unsubscribe_all_to_prevent_duplicates_on_reload!
end
end
end
end
end
@@ -30,32 +30,25 @@ module OpenProject
module JobStatus
class EventListener
class << self
def register!
def register! # rubocop:disable Metrics/AbcSize
# Listen to enqueues
ActiveSupport::Notifications.subscribe(/enqueue(_at)?\.active_job/) do |_name, _started, _call, _id, payload|
job = payload[:job]
next unless job
subscribe(/enqueue(_at)?\.active_job/) do |job, _payload|
Rails.logger.debug { "Enqueuing background job #{job.inspect}" }
for_statused_jobs(job) { create_job_status(job) }
end
# Start of process
ActiveSupport::Notifications.subscribe("perform_start.active_job") do |_name, _started, _call, _id, payload|
job = payload[:job]
next unless job
subscribe("perform_start.active_job") do |job, _payload|
Rails.logger.debug { "Background job #{job.inspect} is being started" }
for_statused_jobs(job) { on_start(job) }
end
# Complete, or failure
ActiveSupport::Notifications.subscribe("perform.active_job") do |_name, _started, _call, _id, payload|
job = payload[:job]
subscribe("perform.active_job") do |job, payload|
exception_object = payload[:exception_object]
Rails.logger.debug do
successful = exception_object ? "with error #{exception_object}" : "successful"
successful = exception_object ? "with error #{exception_object}" : "successfully"
"Background job #{job.inspect} was performed #{successful}."
end
@@ -63,8 +56,7 @@ module OpenProject
end
# Retry stopped -> failure
ActiveSupport::Notifications.subscribe("retry_stopped.active_job") do |_name, _started, _call, _id, payload|
job = payload[:job]
subscribe("retry_stopped.active_job") do |job, payload|
error = payload[:error]
Rails.logger.debug { "Background job #{job.inspect} no longer retrying due to: #{error}" }
@@ -72,8 +64,7 @@ module OpenProject
end
# Retry enqueued
ActiveSupport::Notifications.subscribe("enqueue_retry.active_job") do |_name, _started, _call, _id, payload|
job = payload[:job]
subscribe("enqueue_retry.active_job") do |job, payload|
error = payload[:error]
Rails.logger.debug { "Background job #{job.inspect} is being retried after error: #{error}" }
@@ -81,8 +72,7 @@ module OpenProject
end
# Discarded job
ActiveSupport::Notifications.subscribe("discard.active_job") do |_name, _started, _call, _id, payload|
job = payload[:job]
subscribe("discard.active_job") do |job, payload|
error = payload[:error]
Rails.logger.debug { "Background job #{job.inspect} is being discarded after error: #{error}" }
@@ -90,8 +80,27 @@ module OpenProject
end
end
# Unsubscribe all existing subscribers to prevent duplicate
# subscriptions on reload in development env.
def unsubscribe_all_to_prevent_duplicates_on_reload!
return unless @subscribers
@subscribers.each { |subscriber| ActiveSupport::Notifications.unsubscribe(subscriber) }
@subscribers.clear
end
private
def subscribe(pattern, &)
@subscribers ||= []
@subscribers << ActiveSupport::Notifications.subscribe(pattern) do |_name, _started, _call, _id, payload|
job = payload[:job]
next unless job
yield job, payload
end
end
##
# Yiels the block if the job
# handles statuses
@@ -0,0 +1,177 @@
# 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"
require "contracts/shared/model_contract_shared_context"
RSpec.describe Projects::CreateArtifactWorkPackageContract, :check_errors_i18n do
include_context "ModelContract shared context"
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(:user_custom_field) { create(:user_project_custom_field, name: "Project Manager") }
shared_let(:user_assignee) { create(:user, firstname: "user_assignee") }
shared_let(:current_user) { create(:user, lastname: "current_user") }
shared_let(:role_for_user) { create(:project_role, permissions: %i[add_work_packages view_project_attributes]) }
shared_let(:role_for_assignee) { create(:project_role, permissions: %i[work_package_assigned]) }
shared_let(:project) do
create(
:project,
types: [type],
project_custom_fields: [user_custom_field],
# project initiation request settings
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,
user_custom_field.attribute_name => user_assignee.id
).tap do |p|
p.members << create(:member, principal: user_assignee, project: p, roles: [role_for_assignee])
p.members << create(:member, principal: current_user, project: p, roles: [role_for_user])
end
end
shared_let(:workflow_type_new_to_in_progress) do
create(:workflow, type:, role: role_for_assignee, old_status: status_new, new_status: status_in_progress)
end
let(:contract) { described_class.new(project, current_user) }
before do
login_as current_user
end
context "with all project initiation request information filled correctly" do
it_behaves_like "contract is valid"
end
context "with project initiation request disabled" do
before do
project.update(project_creation_wizard_enabled: false)
end
it_behaves_like "contract is invalid", base: :project_initiation_request_disabled
context "without any project initiation request settings set" do
before do
project.update(project_creation_wizard_work_package_type_id: nil,
project_creation_wizard_status_when_submitted_id: nil,
project_creation_wizard_assignee_custom_field_id: nil)
end
# no other errors than 'project_initiation_request_disabled' are shown
it_behaves_like "contract is invalid", base: :project_initiation_request_disabled,
project_creation_wizard_work_package_type_id: [],
project_creation_wizard_status_when_submitted_id: [],
project_creation_wizard_assignee_custom_field_id: []
end
end
context "with missing :add_work_packages permission" do
before do
role_for_user.role_permissions.where(permission: "add_work_packages").delete_all
end
it_behaves_like "contract is invalid", base: :error_unauthorized
end
context "with unset work package type" do
before do
project.update(project_creation_wizard_work_package_type_id: nil)
end
it_behaves_like "contract is invalid", project_creation_wizard_work_package_type_id: :blank
end
context "with unallowed work package type for the project" do
let(:other_type) { create(:type, name: "Other type") }
before do
project.update(project_creation_wizard_work_package_type_id: other_type.id)
end
it_behaves_like "contract is invalid", project_creation_wizard_work_package_type_id: :inclusion
end
context "with unset work package status" do
before do
project.update(project_creation_wizard_status_when_submitted_id: nil)
end
it_behaves_like "contract is invalid", project_creation_wizard_status_when_submitted_id: :blank
end
context "with unallowed work package status for the type" do
let(:other_status) { create(:status, name: "Other status") }
before do
project.update(project_creation_wizard_status_when_submitted_id: other_status.id)
end
it_behaves_like "contract is invalid", project_creation_wizard_status_when_submitted_id: :inclusion
context "with unset work_package_type" do
before do
project.update(project_creation_wizard_work_package_type_id: nil)
end
it_behaves_like "contract is invalid", project_creation_wizard_work_package_type_id: :blank
end
end
context "with 'Assignee when submitted' not set" do
before do
project.update(project_creation_wizard_assignee_custom_field_id: nil)
end
it_behaves_like "contract is invalid", project_creation_wizard_assignee_custom_field_id: :blank
end
context "with project attribute pointed by 'Assignee when submitted' not set" do
before do
project.send(user_custom_field.attribute_setter, nil)
project.save
end
it "has invalid contract with :blank error for the assignee custom field" do
expect_contract_invalid(user_custom_field.attribute_name => :blank)
end
end
context "with assignee not having the :work_package_assigned permission (cannot be assigned to a wp)" do
before do
role_for_assignee.role_permissions.where(permission: "work_package_assigned").delete_all
end
it "has invalid contract with :cannot_be_assigned_to_artifact_work_package error for the assignee custom field" do
expect_contract_invalid(user_custom_field.attribute_name => :cannot_be_assigned_to_artifact_work_package)
end
end
end
@@ -40,16 +40,20 @@ RSpec.shared_context "ModelContract shared context" do # rubocop:disable RSpec/C
expected_errors = errors.transform_values do |error_symbols|
case error_symbols
when [], nil
[]
when Array
an_array_matching(error_symbols)
when nil
[]
else
[error_symbols]
end
end
contract_errors = errors.keys.index_with { |key| contract.errors.symbols_for(key) }
expect(contract_errors).to match(expected_errors)
if RSpec.current_example.metadata[:check_errors_i18n]
# ensure no I18n::MissingTranslationData is raised because of missing attributes and/or errors translations
expect { contract.errors.full_messages }.not_to raise_error
end
end
shared_examples "contract is valid" do
@@ -63,6 +63,12 @@ RSpec.describe "Project creation wizard",
possible_values: %w[Internal External Research])
end
shared_let(:user_custom_field) do
create(:user_project_custom_field,
name: "Project Validator",
project_custom_field_section: section2)
end
shared_let(:int_custom_field) do
create(:integer_project_custom_field,
name: "Team Size",
@@ -87,7 +93,28 @@ RSpec.describe "Project creation wizard",
help_text: "Select the type that best describes your project.")
end
let(:project) { create(:project, name: "Test Project") }
shared_let(:user_assignee) do
create(:user, firstname: "user_assignee")
end
shared_let(:project) do
status_new = create(:status, name: "New")
status_in_progress = create(:status, name: "In Progress")
type = create(:type, name: "Project initiation")
role = create(:project_role, permissions: %i[view_project_attributes add_work_packages work_package_assigned])
create(:workflow, type:, role:, old_status: status_new, new_status: status_in_progress)
create(:default_priority)
create(:project,
name: "Test Project",
types: [type],
project_custom_fields: [user_custom_field],
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).tap do |prj|
prj.members << create(:member, principal: user_assignee, project: prj, roles: [role])
end
end
let(:wizard_path) { "/projects/#{project.identifier}/creation_wizard" }
let(:text_field_editor) do
Components::WysiwygEditor.new "[data-test-selector='augmented-text-area-#{text_custom_field.id}']"
@@ -217,17 +244,23 @@ RSpec.describe "Project creation wizard",
select_autocomplete page.find("[data-custom-field-id='#{list_custom_field.id}']"),
results_selector: "body",
query: "Internal"
select_autocomplete page.find("[data-custom-field-id='#{user_custom_field.id}']"),
results_selector: "body",
query: user_assignee.name
fill_in "Team Size", with: "5"
click_button "Complete"
expect(page).to have_current_path("/projects/#{project.identifier}")
expect(page).to have_text("Project attributes have been saved successfully.")
expect(page).to have_text("Project attributes saved and artifact work package created successfully.")
project.reload
expect(page).to have_current_path("/projects/#{project.identifier}/" \
"work_packages/#{project.project_creation_wizard_artifact_work_package_id}/" \
"activity")
expect(project.typed_custom_value_for(text_custom_field)).to eq("This is a test project for validation")
expect(project.typed_custom_value_for(string_custom_field)).to eq("TEST-001")
expect(project.typed_custom_value_for(list_custom_field)).to eq("Internal")
expect(project.typed_custom_value_for(int_custom_field)).to eq(5)
expect(project.typed_custom_value_for(user_custom_field)).to eq(user_assignee)
end
it "shows completion checkmarks for sections with filled fields" do
@@ -328,25 +361,31 @@ RSpec.describe "Project creation wizard",
select_autocomplete page.find("[data-custom-field-id='#{list_custom_field.id}']"),
results_selector: "body",
query: "Internal"
select_autocomplete page.find("[data-custom-field-id='#{user_custom_field.id}']"),
results_selector: "body",
query: user_assignee.name
fill_in "Team Size", with: "3"
click_button "Complete"
expect(page).to have_current_path("/projects/#{project.identifier}")
expect(page).to have_text("Project attributes have been saved successfully.")
expect(page).to have_text("Project attributes saved and artifact work package created successfully.")
project.reload
expect(page).to have_current_path("/projects/#{project.identifier}/" \
"work_packages/#{project.project_creation_wizard_artifact_work_package_id}/" \
"activity")
expect(project.typed_custom_value_for(text_custom_field)).to eq("Test description")
expect(project.typed_custom_value_for(string_custom_field)).to eq("TEST-ENABLED")
expect(project.typed_custom_value_for(list_custom_field)).to eq("Internal")
expect(project.typed_custom_value_for(int_custom_field)).to eq(3)
expect(project.typed_custom_value_for(user_custom_field)).to eq(user_assignee)
end
end
context "when all fields in a section are disabled in the creation wizard" do
before do
ProjectCustomFieldProjectMapping
.where(project:, custom_field_id: [list_custom_field.id, int_custom_field.id])
.where(project:, custom_field_id: [list_custom_field.id, user_custom_field.id, int_custom_field.id])
.update_all(creation_wizard: false)
end
@@ -0,0 +1,160 @@
# 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 Projects::CreateArtifactWorkPackageService do
shared_let(:status_new) { create(:status, name: "New") }
shared_let(:type) { create(:type, name: "Project initiation") }
shared_let(:user_custom_field) { create(:user_project_custom_field, name: "Project Manager") }
shared_let(:assignee_user) { create(:user, firstname: "assignee_user") }
shared_let(:current_user) { create(:user, lastname: "current_user") }
shared_let(:role) do
create(:project_role, permissions: %i[
add_work_packages
view_project_attributes
work_package_assigned
])
end
shared_let(:default_priority) { create(:default_priority) }
shared_let(:project) do
create(
:project,
types: [type],
project_custom_fields: [user_custom_field],
# project initiation request settings
project_creation_wizard_artifact_name: "project_mandate",
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,
project_creation_wizard_work_package_comment: "PIR submitted for **Project Name**.",
user_custom_field.attribute_name => assignee_user.id
).tap do |p|
p.members << create(:member, principal: assignee_user, project: p, roles: [role])
p.members << create(:member, principal: current_user, project: p, roles: [role])
end
end
let(:mocked_contract) { instance_double(Projects::CreateArtifactWorkPackageContract, "mocked_contract") }
let(:instance) do
described_class.new(user: current_user, model: project).tap do |instance|
allow(instance).to receive(:instantiate_contract).and_return(mocked_contract)
end
end
before do
login_as current_user
end
context "when contract is valid" do
before do
allow(mocked_contract).to receive(:validate).and_return(true)
end
it "creates an artifact work package (for after submitting a project initiation request)" do
result = instance.call
expect(result.errors.full_messages).to be_empty
project = result.result
expect(project.project_creation_wizard_artifact_work_package_id).to be_present
end
it "uses the type and status defined in the project initiation request settings" do
result = instance.call
project = result.result
artifact_work_package = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id)
expect(artifact_work_package.type.id).to eq(project.project_creation_wizard_work_package_type_id)
expect(artifact_work_package.status.id).to eq(project.project_creation_wizard_status_when_submitted_id)
end
it "assigns the artifact work package to the user pointed by the 'Assignee when submitted' custom field" do
result = instance.call
project = result.result
artifact_work_package = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id)
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
artifact_work_package = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id)
expected_name = I18n.t("settings.project_initiation_request.name.options.#{project.project_creation_wizard_artifact_name}")
expect(artifact_work_package.subject).to eq(expected_name)
end
it "if the artifact name is misconfigured (unexisting name key), " \
"sets the subject to the 'project_creation_wizard' artifact name" do
project.update(project_creation_wizard_artifact_name: "misconfigured")
result = instance.call
project = result.result
artifact_work_package = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id)
expected_name = I18n.t("settings.project_initiation_request.name.options.project_creation_wizard")
expect(artifact_work_package.subject).to eq(expected_name)
end
it "if the artifact name is nil, sets the subject to the 'project_creation_wizard' artifact name" do
project.update(project_creation_wizard_artifact_name: nil)
result = instance.call
project = result.result
artifact_work_package = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id)
expected_name = I18n.t("settings.project_initiation_request.name.options.project_creation_wizard")
expect(artifact_work_package.subject).to eq(expected_name)
end
it "adds a comment to the artifact work package " \
"using the project_creation_wizard_work_package_comment setting " \
"and mentioning the assignee" do
result = instance.call
project = result.result
artifact_work_package = WorkPackage.find(project.project_creation_wizard_artifact_work_package_id)
expect(artifact_work_package.last_journal.notes).not_to be_empty
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(/<mention[^>]+>@#{assignee_user.name}<\/mention>/)
end
end
end