Skip to content

Commit 622c27e

Browse files
authored
Merge pull request opf#15782 from opf/bug/55507-not-possible-to-add-multiple-projects-at-the-same-time
[#55507] Make it possible to activate custom fields on multiple projects at the same time https://community.openproject.org/work_packages/55507
2 parents 76ea564 + e7a4343 commit 622c27e

File tree

4 files changed

+77
-27
lines changed

4 files changed

+77
-27
lines changed

app/controllers/admin/settings/project_custom_fields_controller.rb

+6-6
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class ProjectCustomFieldsController < ::Admin::SettingsController
4343
before_action :prepare_custom_option_position, only: %i(update create)
4444
before_action :find_custom_option, only: :delete_option
4545
before_action :project_custom_field_mappings_query, only: %i[project_mappings unlink]
46-
before_action :find_link_project_custom_field_mapping, only: :link
46+
before_action :find_custom_field_projects_to_link, only: :link
4747
before_action :find_unlink_project_custom_field_mapping, only: :unlink
4848
# rubocop:enable Rails/LexicallyScopedActionFilter
4949

@@ -75,15 +75,15 @@ def project_mappings
7575

7676
def link
7777
create_service = ProjectCustomFieldProjectMappings::BulkCreateService
78-
.new(user: current_user, project: @project, project_custom_field: @custom_field,
78+
.new(user: current_user, projects: @projects, project_custom_field: @custom_field,
7979
include_sub_projects: include_sub_projects?)
8080
.call
8181

8282
create_service.on_success { render_project_list }
8383

8484
create_service.on_failure do
8585
update_flash_message_via_turbo_stream(
86-
message: join_flash_messages(create_service.errors.full_messages),
86+
message: join_flash_messages(create_service.errors),
8787
full: true, dismiss_scheme: :hide, scheme: :danger
8888
)
8989
end
@@ -195,8 +195,8 @@ def find_unlink_project_custom_field_mapping
195195
respond_with_turbo_streams
196196
end
197197

198-
def find_link_project_custom_field_mapping
199-
@project = Project.find(permitted_params.project_custom_field_project_mapping[:project_id])
198+
def find_custom_field_projects_to_link
199+
@projects = Project.find(params.to_unsafe_h[:project_custom_field_project_mapping][:project_ids])
200200
rescue ActiveRecord::RecordNotFound
201201
update_flash_message_via_turbo_stream(
202202
message: t(:notice_file_not_found), full: true, dismiss_scheme: :hide, scheme: :danger
@@ -220,7 +220,7 @@ def drop_success_streams(call)
220220
end
221221

222222
def include_sub_projects?
223-
ActiveRecord::Type::Boolean.new.cast(permitted_params.project_custom_field_project_mapping[:include_sub_projects])
223+
ActiveRecord::Type::Boolean.new.cast(params.to_unsafe_h[:project_custom_field_project_mapping][:include_sub_projects])
224224
end
225225
end
226226
end

app/forms/projects/custom_fields/custom_field_mapping_form.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ class CustomFieldMappingForm < ApplicationForm
3737
autocomplete_options: {
3838
openDirectly: false,
3939
focusDirectly: false,
40+
multiple: true,
4041
dropdownPosition: "bottom",
4142
disabledProjects: projects_with_custom_field_mapping,
42-
inputName: "project_custom_field_project_mapping[project_id]"
43+
inputName: "project_custom_field_project_mapping[project_ids]"
4344
}
4445
)
4546

app/services/project_custom_field_project_mappings/bulk_create_service.rb

+17-13
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@
3030

3131
module ProjectCustomFieldProjectMappings
3232
class BulkCreateService < ::BaseServices::BaseCallable
33-
def initialize(user:, project:, project_custom_field:, include_sub_projects: false)
33+
def initialize(user:, projects:, project_custom_field:, include_sub_projects: false)
3434
super()
3535
@user = user
36-
@project = project
36+
@projects = projects
3737
@project_custom_field = project_custom_field
3838
@include_sub_projects = include_sub_projects
3939
end
@@ -49,10 +49,12 @@ def perform
4949
private
5050

5151
def validate_permissions
52-
if @user.allowed_in_project?(:select_project_custom_fields, projects)
52+
return ServiceResult.failure(errors: I18n.t(:label_not_found)) if incoming_projects.empty?
53+
54+
if @user.allowed_in_project?(:select_project_custom_fields, incoming_projects)
5355
ServiceResult.success
5456
else
55-
ServiceResult.failure(errors: { base: :error_unauthorized })
57+
ServiceResult.failure(errors: I18n.t("activerecord.errors.messages.error_unauthorized"))
5658
end
5759
end
5860

@@ -72,23 +74,25 @@ def validate_contract(service_call, project_ids)
7274
end
7375

7476
def perform_bulk_create(service_call)
75-
ProjectCustomFieldProjectMapping.import(service_call.result, validate: false)
77+
ProjectCustomFieldProjectMapping.insert_all(
78+
service_call.result.map { |model| model.attributes.slice("project_id", "custom_field_id") }
79+
)
7680

7781
service_call
78-
rescue StandardError => e
79-
service_call.success = false
80-
service_call.errors = e.message
8182
end
8283

8384
def incoming_mapping_ids
84-
project_ids = projects.pluck(:id)
85+
project_ids = incoming_projects.pluck(:id)
8586
project_ids - existing_project_mappings(project_ids)
8687
end
8788

88-
def projects
89-
[@project].tap do |projects_array|
90-
projects_array.concat(@project.active_subprojects.to_a) if @include_sub_projects
91-
end
89+
def incoming_projects
90+
@projects.each_with_object(Set.new) do |project, projects_set|
91+
next unless project.active?
92+
93+
projects_set << project
94+
projects_set.merge(project.active_subprojects.to_a) if @include_sub_projects
95+
end.to_a
9296
end
9397

9498
def existing_project_mappings(project_ids)

spec/services/project_custom_field_project_mappings/bulk_create_service_spec.rb

+52-7
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
context "with a single project" do
3838
let(:project) { create(:project) }
39-
let(:instance) { described_class.new(user:, project:, project_custom_field:) }
39+
let(:instance) { described_class.new(user:, projects: [project], project_custom_field:) }
4040

4141
it "creates the mappings" do
4242
expect { instance.call }.to change(ProjectCustomFieldProjectMapping, :count).by(1)
@@ -49,19 +49,36 @@
4949
end
5050

5151
context "with subprojects" do
52+
let(:projects) { create_list(:project, 2) }
53+
let!(:subproject) { create(:project, parent: projects.first) }
54+
let!(:subproject2) { create(:project, parent: subproject) }
55+
56+
it "creates the mappings for the project and sub-projects" do
57+
create_service = described_class.new(user:, projects: projects.map(&:reload), project_custom_field:,
58+
include_sub_projects: true)
59+
60+
expect { create_service.call }.to change(ProjectCustomFieldProjectMapping, :count).by(4)
61+
62+
aggregate_failures "creates the mapping for the correct project and custom field" do
63+
expect(ProjectCustomFieldProjectMapping.where(project_custom_field:).pluck(:project_id))
64+
.to contain_exactly(*projects.map(&:id), subproject.id, subproject2.id)
65+
end
66+
end
67+
end
68+
69+
context "with multiple projects including subprojects" do
5270
let(:project) { create(:project) }
5371
let!(:subproject) { create(:project, parent: project) }
54-
let!(:subproject2) { create(:project, parent: subproject) }
5572

5673
it "creates the mappings for the project and sub-projects" do
57-
create_service = described_class.new(user:, project: project.reload, project_custom_field:,
74+
create_service = described_class.new(user:, projects: [project.reload, subproject], project_custom_field:,
5875
include_sub_projects: true)
5976

60-
expect { create_service.call }.to change(ProjectCustomFieldProjectMapping, :count).by(3)
77+
expect { create_service.call }.to change(ProjectCustomFieldProjectMapping, :count).by(2)
6178

6279
aggregate_failures "creates the mapping for the correct project and custom field" do
6380
expect(ProjectCustomFieldProjectMapping.where(project_custom_field:).pluck(:project_id))
64-
.to contain_exactly(project.id, subproject.id, subproject2.id)
81+
.to contain_exactly(project.id, subproject.id)
6582
end
6683
end
6784
end
@@ -80,7 +97,7 @@
8097
end
8198

8299
let(:project) { create(:project) }
83-
let(:instance) { described_class.new(user:, project:, project_custom_field:) }
100+
let(:instance) { described_class.new(user:, projects: [project], project_custom_field:) }
84101

85102
it "creates the mappings" do
86103
expect { instance.call }.to change(ProjectCustomFieldProjectMapping, :count).by(1)
@@ -103,11 +120,39 @@
103120
})
104121
end
105122
let(:project) { create(:project) }
106-
let(:instance) { described_class.new(user:, project:, project_custom_field:) }
123+
let(:instance) { described_class.new(user:, projects: [project], project_custom_field:) }
107124

108125
it "does not create the mappings" do
109126
expect { instance.call }.not_to change(ProjectCustomFieldProjectMapping, :count)
110127
expect(instance.call).to be_failure
111128
end
112129
end
130+
131+
context "with empty projects" do
132+
let(:user) { create(:admin) }
133+
let(:instance) { described_class.new(user:, projects: [], project_custom_field:) }
134+
135+
it "does not create the mappings" do
136+
service_result = instance.call
137+
expect(service_result).to be_failure
138+
expect(service_result.errors).to eq("not found")
139+
end
140+
end
141+
142+
context "with archived projects" do
143+
let(:user) { create(:admin) }
144+
let(:archived_project) { create(:project, active: false) }
145+
let(:active_project) { create(:project) }
146+
147+
let(:instance) { described_class.new(user:, projects: [archived_project, active_project], project_custom_field:) }
148+
149+
it "only creates mappins for the active project" do
150+
expect { instance.call }.to change(ProjectCustomFieldProjectMapping, :count).by(1)
151+
152+
aggregate_failures "creates the mapping for the correct project and custom field" do
153+
expect(ProjectCustomFieldProjectMapping.where(project_custom_field:).pluck(:project_id))
154+
.to contain_exactly(active_project.id)
155+
end
156+
end
157+
end
113158
end

0 commit comments

Comments
 (0)