Skip to content

Commit

Permalink
feat[Op#57579]: define custom field projects bulk create service
Browse files Browse the repository at this point in the history
  • Loading branch information
akabiru committed Sep 4, 2024
1 parent 5e92bd0 commit 579ccd6
Show file tree
Hide file tree
Showing 3 changed files with 335 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# 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 CustomFields
module CustomFieldProjects
class BulkCreateService < ::BaseServices::BaseCallable
def initialize(user:, projects:, custom_field:, include_sub_projects: false)
super()
@user = user
@projects = projects
@custom_field = custom_field
@include_sub_projects = include_sub_projects
end

def perform
service_call = validate_permissions
service_call = validate_contract(service_call, incoming_mapping_ids) if service_call.success?
service_call = perform_bulk_create(service_call) if service_call.success?

service_call
end

private

def validate_permissions
return ServiceResult.failure(errors: I18n.t(:label_not_found)) if incoming_projects.empty?

if @user.allowed_in_project?(:select_custom_fields, incoming_projects)
ServiceResult.success
else
ServiceResult.failure(errors: I18n.t("activerecord.errors.messages.error_unauthorized"))
end
end

def validate_contract(service_call, project_ids)
set_attributes_results = project_ids.map do |id|
set_attributes(project_id: id, custom_field_id: @custom_field.id)
end

if (failures = set_attributes_results.select(&:failure?)).any?
service_call.success = false
service_call.errors = failures.map(&:errors)
else
service_call.result = set_attributes_results.map(&:result)
end

service_call
end

def perform_bulk_create(service_call)
custom_field_project_mapping_class.insert_all(
service_call.result.map { |model| model.attributes.slice("project_id", "custom_field_id") },
unique_by: %i[project_id custom_field_id]
)

service_call
end

def incoming_mapping_ids
project_ids = incoming_projects.pluck(:id)
project_ids - existing_project_mappings(project_ids)
end

def incoming_projects
@projects.each_with_object(Set.new) do |project, projects_set|
next unless project.active?

projects_set << project
projects_set.merge(project.active_subprojects.to_a) if @include_sub_projects
end.to_a
end

def existing_project_mappings(project_ids)
custom_field_project_mapping_class.where(
custom_field_id: @custom_field.id,
project_id: project_ids
).pluck(:project_id)
end

def set_attributes(params)
attributes_service_class
.new(user: @user,
model: instance(params),
contract_class: default_contract_class,
contract_options: {})
.call(params)
end

def instance(params)
custom_field_project_mapping_class.new(params)
end

def attributes_service_class
custom_field_project_mapping_module::SetAttributesService
end

def default_contract_class
custom_field_project_mapping_module::UpdateContract
end

def custom_field_project_mapping_module = CustomFields::CustomFieldProjects
def custom_field_project_mapping_class = CustomFieldsProject
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#-- 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 CustomFields
module CustomFieldProjects
class SetAttributesService < ::BaseServices::SetAttributes
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#-- 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 CustomFields::CustomFieldProjects::BulkCreateService do
shared_let(:custom_field) { create(:wp_custom_field) }

context "with admin permissions" do
let(:user) { create(:admin) }

context "with a single project" do
let(:project) { create(:project) }
let(:instance) { described_class.new(user:, projects: [project], custom_field:) }

it "creates the mappings" do
expect { instance.call }.to change(CustomFieldsProject, :count).by(1)

aggregate_failures "creates the mapping for the correct project and custom field" do
expect(CustomFieldsProject.where(custom_field:).pluck(:project_id)).to contain_exactly(project.id)
end
end
end

context "with subprojects" do
let(:projects) { create_list(:project, 2) }
let!(:subproject) { create(:project, parent: projects.first) }
let!(:subproject2) { create(:project, parent: subproject) }

it "creates the mappings for the project and sub-projects" do
create_service = described_class.new(user:, projects: projects.map(&:reload), custom_field:,
include_sub_projects: true)

expect { create_service.call }.to change(CustomFieldsProject, :count).by(4)

aggregate_failures "creates the mapping for the correct project and custom field" do
expect(CustomFieldsProject.where(custom_field:).pluck(:project_id))
.to contain_exactly(*projects.map(&:id), subproject.id, subproject2.id)
end
end
end

context "with multiple projects including subprojects" do
let(:project) { create(:project) }
let!(:subproject) { create(:project, parent: project) }

it "creates the mappings for the project and sub-projects" do
create_service = described_class.new(user:, projects: [project.reload, subproject], custom_field:,
include_sub_projects: true)

expect { create_service.call }.to change(CustomFieldsProject, :count).by(2)

aggregate_failures "creates the mapping for the correct project and custom field" do
expect(CustomFieldsProject.where(custom_field:).pluck(:project_id))
.to contain_exactly(project.id, subproject.id)
end
end
end

context "with duplicates" do
let(:project) { create(:project) }
let(:instance) { described_class.new(user:, projects: [project, project], custom_field:) }

it "creates the mappings only once" do
expect { instance.call }.to change(CustomFieldsProject, :count).by(1)

aggregate_failures "creates the mapping for the correct project and custom field" do
expect(CustomFieldsProject.where(custom_field:).pluck(:project_id)).to contain_exactly(project.id)
end
end
end
end

context "with non-admin but sufficient permissions" do
let(:user) do
create(:user,
member_with_permissions: {
project => %w[
view_work_packages
edit_project
select_custom_fields
]
})
end

let(:project) { create(:project) }
let(:instance) { described_class.new(user:, projects: [project], custom_field:) }

it "creates the mappings" do
expect { instance.call }.to change(CustomFieldsProject, :count).by(1)

aggregate_failures "creates the mapping for the correct project and custom field" do
expect(CustomFieldsProject.where(custom_field:).pluck(:project_id)).to contain_exactly(project.id)
end
end
end

context "without sufficient permissions" do
let(:user) do
create(:user,
member_with_permissions: {
project => %w[
view_work_packages
edit_project
]
})
end
let(:project) { create(:project) }
let(:instance) { described_class.new(user:, projects: [project], custom_field:) }

it "does not create the mappings" do
expect { instance.call }.not_to change(CustomFieldsProject, :count)
expect(instance.call).to be_failure
end
end

context "with empty projects" do
let(:user) { create(:admin) }
let(:instance) { described_class.new(user:, projects: [], custom_field:) }

it "does not create the mappings" do
service_result = instance.call
expect(service_result).to be_failure
expect(service_result.errors).to eq("not found")
end
end

context "with archived projects" do
let(:user) { create(:admin) }
let(:archived_project) { create(:project, active: false) }
let(:active_project) { create(:project) }

let(:instance) { described_class.new(user:, projects: [archived_project, active_project], custom_field:) }

it "only creates mappins for the active project" do
expect { instance.call }.to change(CustomFieldsProject, :count).by(1)

aggregate_failures "creates the mapping for the correct project and custom field" do
expect(CustomFieldsProject.where(custom_field:).pluck(:project_id))
.to contain_exactly(active_project.id)
end
end
end
end

0 comments on commit 579ccd6

Please sign in to comment.