mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user