import and revert custom fields

This commit is contained in:
as-op
2026-03-30 13:03:52 +02:00
parent d91149336f
commit 92bbfbaced
3 changed files with 207 additions and 14 deletions
@@ -0,0 +1,125 @@
# 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 Import
class JiraCustomFieldBuilder
JIRA_TO_OP_FIELD_FORMAT = {
"string" => "string",
"text" => "text",
"number" => "float",
"date" => "date",
"datetime" => "date", # TODO loss of precision
"option" => "list",
"any" => "string"
}.freeze
attr_reader :jira_project, :jira_field, :values
def initialize(jira_field, jira_project, values)
@jira_field = jira_field
@jira_project = jira_project
@values = values
@import_name = jira_field.payload["name"]
end
def find_existing_custom_field
existing_cf = custom_field_by_name(@import_name) if %w[hierarchy list].exclude?(format)
return existing_cf if existing_cf&.field_format == format
@import_name = unique_custom_field_name
nil
end
def custom_field_settings
[@import_name, format]
end
def custom_field_parameters
params = {}
if format == "list"
params[:multi_value] = multi
options = collect_list_options(values)
params[:possible_values] = options unless options.empty?
end
params
end
def convert_values(custom_field)
@values
end
def custom_field_post_processing(custom_field)
populate_hierarchy_items(custom_field, values) if format == "hierarchy"
end
private
def custom_field_by_name(name)
WorkPackageCustomField.where("LOWER(name) = LOWER(?)", name).first
end
def unique_custom_field_name
unique_name = @import_name
suffix = 2
while custom_field_by_name(unique_name)
unique_name = "#{@import_name} (#{suffix})"
suffix += 1
end
unique_name
end
def format
@format ||= jira_to_op_field_format(jira_field)
end
def jira_to_op_field_format(jira_field)
schema = jira_field.payload["schema"] || {}
type = schema["type"]
items = schema["items"]
custom = schema["custom"].to_s
if type == "array"
items == "option" ? "list" : "string"
elsif type == "option" && custom.include?("cascadingselect")
EnterpriseToken.allows_to?(:custom_field_hierarchies) ? "hierarchy" : "string"
else
JIRA_TO_OP_FIELD_FORMAT.fetch(type, "string")
end
end
def collect_list_options(values)
[] # TODO: collect_list_options
end
def populate_hierarchy_items(custom_field, values)
# TODO: populate_hierarchy_items
end
end
end
+71 -14
View File
@@ -44,9 +44,10 @@ module Import
@project_role = setup_project_role
Import::JiraProject.where(jira_id: @jira_id, jira_project_id: @jira_import.project_ids).find_each do |jira_project|
project = import_project(jira_project)
custom_field_list = import_custom_fields(jira_project)
project = import_project(jira_project, custom_field_list)
Import::JiraIssue.where(jira_id: @jira_id, jira_project_id: jira_project.id).find_each do |jira_issue|
import_issue(jira_issue, project)
import_issue(jira_issue, project, custom_field_list)
end
end
end
@@ -71,7 +72,7 @@ module Import
Role.find_by!(name: "JiraMember")
end
def import_project(jira_project)
def import_project(jira_project, _custom_field_list)
identifier = jira_project.payload.fetch("key").downcase
service_call = Projects::CreateService
.new(user: @user, contract_class: EmptyContract)
@@ -96,18 +97,18 @@ module Import
taken_identifier = error.options[:value]
project = Project.find_by!(identifier: taken_identifier)
raise "You are trying to import a project with already used " \
"identifier: #{taken_identifier}. Existing project: #{project}."
"identifier: #{taken_identifier}. Existing project: #{project}."
end
raise service_call.message
end
def import_issue(jira_issue, project)
def import_issue(jira_issue, project, custom_field_list)
type = import_type(jira_issue, project)
status = import_status(jira_issue)
update_workflows(type)
priority = import_priority(jira_issue)
import_work_package(jira_issue, project, type, status, priority)
import_work_package(jira_issue, project, type, status, priority, custom_field_list)
end
def import_type(jira_issue, project)
@@ -173,7 +174,7 @@ module Import
raise call.message if call.failure?
end
def import_work_package(jira_issue, project, type, status, priority)
def import_work_package(jira_issue, project, type, status, priority, custom_field_list)
# required because otherwise project.types does not include type and then wp creation fails.
project.reload
author_key = jira_issue.payload.dig("fields", "creator", "key")
@@ -182,17 +183,27 @@ module Import
assigned_to = find_user(assignee_key)
[author, assigned_to].uniq.compact.each { |member| import_member(project, member) }
description = Import::JiraWikiMarkupConverter.new(jira_issue.payload["fields"]["description"] || "").convert
custom_field_attrs = custom_field_list.each_with_object({}) do |field, attrs|
value = field[:values].select { |value| value[:issue_id] == jira_issue.id }
next if value&.nil?
custom_field = field[:custom_field]
attrs[custom_field.attribute_getter] = value.first[:value]
end
service_call = WorkPackages::CreateService
.new(user: author || User.system, contract_class: EmptyContract)
.call(
project:,
subject: jira_issue.payload["fields"]["summary"],
description: convert_rich_text(jira_issue.payload["fields"]["description"]),
description:,
type:,
priority:,
status:,
assigned_to:
assigned_to:,
**custom_field_attrs
)
raise service_call.message unless service_call.success?
@@ -262,6 +273,57 @@ module Import
raise service_call.message if service_call.errors.find { |error| error.type == :taken }.blank?
end
def import_custom_fields(jira_project)
usage = {}
Import::JiraIssue.where(jira_id: @jira_id, jira_project_id: jira_project.id).find_each do |issue|
issue.payload["fields"].each do |key, value|
next unless key.start_with?("customfield_") && value.present?
usage[key] ||= { values: [] }
usage[key][:values] << { value:, issue_id: issue.id }
end
end
custom_field_list = []
usage.each do |jira_field_id, value|
jira_field = Import::JiraField.find_by(jira_id: @jira_id, jira_field_id:)
custom_field, values = import_custom_field(jira_field, jira_project, value[:values])
custom_field_list << { custom_field:, values: }
end
custom_field_list
end
def import_custom_field(jira_field, jira_project, values)
jira_custom_field_builder = Import::JiraCustomFieldBuilder.new(jira_field, jira_project, values)
existing_cf = jira_custom_field_builder.find_existing_custom_field
if existing_cf
unless Import::JiraOpenProjectReference.exists?(op_entity_id: existing_cf.id,
op_entity_class: existing_cf.class.to_s,
jira_id: @jira_id)
create_reference!(op_leg: existing_cf, jira_leg: jira_field, jira_import:, uses_existing: true)
end
return [existing_cf, jira_custom_field_builder.convert_values(existing_cf)]
end
name, field_format = jira_custom_field_builder.custom_field_settings
params = {
type: "WorkPackageCustomField",
name:,
field_format:,
is_required: false,
is_for_all: false,
**jira_custom_field_builder.custom_field_parameters
}
service_call = CustomFields::CreateService.new(user: @user).call(**params)
if service_call.success?
custom_field = service_call.result
create_reference!(op_leg: custom_field, jira_leg: jira_field, jira_import: @jira_import, uses_existing: false)
jira_custom_field_builder.custom_field_post_processing(custom_field)
[custom_field, jira_custom_field_builder.convert_values(custom_field)]
else
raise "Failed to create custom field '#{jira_field['name']}': #{service_call.message}"
end
end
def find_user(jira_user_key)
return if jira_user_key.blank?
@@ -276,11 +338,6 @@ module Import
end
end
def convert_rich_text(description)
return "" if description.blank?
Import::JiraWikiMarkupConverter.new(description).convert
end
# rubocop:enable Metrics/AbcSize
end
end
@@ -37,6 +37,7 @@ module Import
delete_users
delete_groups
delete_project_roles
delete_custom_fields
delete_references
delete_jira_objects].freeze
@@ -132,6 +133,16 @@ module Import
end
end
def delete_custom_fields
Import::JiraOpenProjectReference
.where(jira_import_id: @jira_import.id, uses_existing: false)
.where(op_entity_class: "WorkPackageCustomField")
.find_each do |ref|
op_leg = ref.op_leg
op_leg.destroy!
end
end
def delete_references
Import::JiraOpenProjectReference.where(jira_import_id: @jira_import.id).delete_all
end