From 4f37547b395f7ce3e4952a6bf410f341b3c2cf55 Mon Sep 17 00:00:00 2001 From: Kabiru Mwenja Date: Tue, 17 Sep 2024 10:09:44 +0300 Subject: [PATCH] chore[Op#57902]: Introduce base bulk create service DRY up common logic in: - `CustomFields::CustomFieldProjects::BulkCreateService` - `ProjectCustomFieldProjectMappings::BulkCreateService` - `::Storages::ProjectStorages::BulkCreateService` --- .../project_mappings/base_create_service.rb | 153 ++++++++++++++++++ .../bulk_create_service.rb | 92 +---------- .../bulk_create_service.rb | 14 +- .../project_storages/bulk_create_service.rb | 71 ++------ 4 files changed, 172 insertions(+), 158 deletions(-) create mode 100644 app/services/bulk_services/project_mappings/base_create_service.rb diff --git a/app/services/bulk_services/project_mappings/base_create_service.rb b/app/services/bulk_services/project_mappings/base_create_service.rb new file mode 100644 index 000000000000..bf5518cd583b --- /dev/null +++ b/app/services/bulk_services/project_mappings/base_create_service.rb @@ -0,0 +1,153 @@ +# 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 BulkServices + module ProjectMappings + class BaseCreateService < ::BaseServices::BaseCallable + def initialize(user:, projects:, model:, include_sub_projects: false) + super() + @user = user + @projects = projects + @model = model + @include_sub_projects = include_sub_projects + end + + def perform(params = {}) + service_call = validate_permissions + service_call = validate_contract(service_call, incoming_mapping_ids, params) if service_call.success? + service_call = perform_bulk_create(service_call) if service_call.success? + service_call = after_perform(service_call, params) 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?(permission, incoming_projects) + ServiceResult.success + else + ServiceResult.failure(errors: I18n.t("activerecord.errors.messages.error_unauthorized")) + end + end + + def validate_contract(service_call, project_ids, _params) + set_attributes_results = project_ids.map do |id| + set_attributes(project_id: id, model_foreign_key_id => @model.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) + mapping_model_class.insert_all( + service_call.result.map { |model| model.attributes.slice("project_id", model_foreign_key_id.to_s) }, + unique_by: %i[project_id].push(model_foreign_key_id.to_sym) + ) + + service_call + end + + def after_perform(service_call, _params) + service_call # Subclasses can override this method to add additional logic + 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) + mapping_model_class.where( + model_foreign_key_id => @model.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) + mapping_model_class.new(params) + end + + # @return [Symbol] the permission required to create the mapping + def permission + raise NotImplementedError + end + + # @return [Symbol] the column name of the mapping + def model_foreign_key_id + raise NotImplementedError + end + + # @return [Class] the model class of the mapping + def mapping_model_class + raise NotImplementedError + end + + def attributes_service_class + "#{namespace}::SetAttributesService".constantize + end + + def default_contract_class + "#{namespace}::UpdateContract".constantize + end + + def namespace + self.class.name.deconstantize.pluralize + end + end + end +end diff --git a/app/services/custom_fields/custom_field_projects/bulk_create_service.rb b/app/services/custom_fields/custom_field_projects/bulk_create_service.rb index 3b452006ce68..cfa9673cc6ae 100644 --- a/app/services/custom_fields/custom_field_projects/bulk_create_service.rb +++ b/app/services/custom_fields/custom_field_projects/bulk_create_service.rb @@ -30,96 +30,14 @@ module CustomFields module CustomFieldProjects - class BulkCreateService < ::BaseServices::BaseCallable + class BulkCreateService < ::BulkServices::ProjectMappings::BaseCreateService 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 + super(user:, projects:, model: custom_field, 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(permission: :select_custom_fields) - return ServiceResult.failure(errors: I18n.t(:label_not_found)) if incoming_projects.empty? - - if @user.allowed_in_project?(permission, 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 = CustomFields::CustomFieldProjects::SetAttributesService - def default_contract_class = CustomFields::CustomFieldProjects::UpdateContract - def custom_field_project_mapping_class = CustomFieldsProject + def permission = :select_custom_fields + def model_foreign_key_id = :custom_field_id + def mapping_model_class = CustomFieldsProject end end end diff --git a/app/services/project_custom_field_project_mappings/bulk_create_service.rb b/app/services/project_custom_field_project_mappings/bulk_create_service.rb index 432112326ac1..3bc34b44a142 100644 --- a/app/services/project_custom_field_project_mappings/bulk_create_service.rb +++ b/app/services/project_custom_field_project_mappings/bulk_create_service.rb @@ -29,19 +29,15 @@ #++ module ProjectCustomFieldProjectMappings - class BulkCreateService < ::CustomFields::CustomFieldProjects::BulkCreateService + class BulkCreateService < ::BulkServices::ProjectMappings::BaseCreateService def initialize(user:, projects:, project_custom_field:, include_sub_projects: false) - super(user:, projects:, custom_field: project_custom_field, include_sub_projects:) + super(user:, projects:, model: project_custom_field, include_sub_projects:) end private - def validate_permissions(permission: :select_project_custom_fields) - super - end - - def attributes_service_class = ProjectCustomFieldProjectMappings::SetAttributesService - def default_contract_class = ProjectCustomFieldProjectMappings::UpdateContract - def custom_field_project_mapping_class = ProjectCustomFieldProjectMapping + def permission = :select_project_custom_fields + def model_foreign_key_id = :custom_field_id + def mapping_model_class = ProjectCustomFieldProjectMapping end end diff --git a/modules/storages/app/services/storages/project_storages/bulk_create_service.rb b/modules/storages/app/services/storages/project_storages/bulk_create_service.rb index 23ab331e4d7e..b6c9205c5278 100644 --- a/modules/storages/app/services/storages/project_storages/bulk_create_service.rb +++ b/modules/storages/app/services/storages/project_storages/bulk_create_service.rb @@ -29,19 +29,12 @@ #++ module Storages::ProjectStorages - class BulkCreateService < ::BaseServices::BaseCallable + class BulkCreateService < ::BulkServices::ProjectMappings::BaseCreateService def initialize(user:, projects:, storage:, include_sub_projects: false) - super() - @user = user - @projects = projects - @storage = storage - @include_sub_projects = include_sub_projects + super(user:, projects:, model: storage, include_sub_projects:) end - def perform(params = {}) - service_call = validate_permissions - service_call = validate_contract(service_call, incoming_activations_ids, params) if service_call.success? - service_call = perform_bulk_create(service_call) if service_call.success? + def after_perform(service_call, params) service_call = create_last_project_folders(service_call, params) if service_call.success? broadcast_project_storages_created(params) if service_call.success? @@ -50,21 +43,16 @@ def perform(params = {}) private - def validate_permissions - return ServiceResult.failure(errors: I18n.t(:label_not_found)) if incoming_projects.empty? - - if @user.allowed_in_project?(:manage_files_in_project, incoming_projects) - ServiceResult.success - else - ServiceResult.failure(errors: I18n.t("activerecord.errors.messages.error_unauthorized")) - end - end + def permission = :manage_files_in_project + def model_foreign_key_id = :storage_id + def mapping_model_class = ::Storages::ProjectStorage + def default_contract_class = ::Storages::ProjectStorages::CreateContract def validate_contract(service_call, project_ids, params) project_folder_params = params.slice(:project_folder_mode, :project_folder_id) set_attributes_results = project_ids.map do |id| - set_attributes(project_id: id, storage_id: @storage.id, **project_folder_params) + set_attributes(project_id: id, storage_id: @model.id, **project_folder_params) end if (failures = set_attributes_results.select(&:failure?)).any? @@ -103,49 +91,8 @@ def broadcast_project_storages_created(params) event: :created, project_folder_mode: params[:project_folder_mode], project_folder_mode_previously_was: nil, - storage: @storage + storage: @model ) end - - def incoming_activations_ids - project_ids = incoming_projects.pluck(:id) - project_ids - existing_project_storages(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_storages(project_ids) - ::Storages::ProjectStorage - .where(storage_id: @storage.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) - ::Storages::ProjectStorage.new(params) - end - - def attributes_service_class - ::Storages::ProjectStorages::SetAttributesService - end - - def default_contract_class - ::Storages::ProjectStorages::CreateContract - end end end