mirror of
https://github.com/redmine/redmine.git
synced 2025-10-26 07:46:17 +01:00
Patch by Go MAEDA. git-svn-id: http://svn.redmine.org/redmine/trunk@20168 e93f8b46-1217-0410-a6f0-8f06a7374b81
372 lines
11 KiB
Ruby
372 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Redmine - project management software
|
|
# Copyright (C) 2006-2020 Jean-Philippe Lang
|
|
#
|
|
# 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.
|
|
|
|
class IssueImport < Import
|
|
AUTO_MAPPABLE_FIELDS = {
|
|
'tracker' => 'field_tracker',
|
|
'subject' => 'field_subject',
|
|
'description' => 'field_description',
|
|
'status' => 'field_status',
|
|
'priority' => 'field_priority',
|
|
'category' => 'field_category',
|
|
'assigned_to' => 'field_assigned_to',
|
|
'fixed_version' => 'field_fixed_version',
|
|
'is_private' => 'field_is_private',
|
|
'parent_issue_id' => 'field_parent_issue',
|
|
'start_date' => 'field_start_date',
|
|
'due_date' => 'field_due_date',
|
|
'estimated_hours' => 'field_estimated_hours',
|
|
'done_ratio' => 'field_done_ratio'
|
|
}
|
|
|
|
def self.menu_item
|
|
:issues
|
|
end
|
|
|
|
def self.authorized?(user)
|
|
user.allowed_to?(:import_issues, nil, :global => true)
|
|
end
|
|
|
|
# Returns the objects that were imported
|
|
def saved_objects
|
|
object_ids = saved_items.pluck(:obj_id)
|
|
objects = Issue.where(:id => object_ids).order(:id).preload(:tracker, :priority, :status)
|
|
end
|
|
|
|
# Returns a scope of projects that user is allowed to
|
|
# import issue to
|
|
def allowed_target_projects
|
|
Project.allowed_to(user, :import_issues)
|
|
end
|
|
|
|
def project
|
|
project_id = mapping['project_id'].to_i
|
|
allowed_target_projects.find_by_id(project_id) || allowed_target_projects.first
|
|
end
|
|
|
|
# Returns a scope of trackers that user is allowed to
|
|
# import issue to
|
|
def allowed_target_trackers
|
|
Issue.allowed_target_trackers(project, user)
|
|
end
|
|
|
|
def tracker
|
|
if mapping['tracker'].to_s =~ /\Avalue:(\d+)\z/
|
|
tracker_id = $1.to_i
|
|
allowed_target_trackers.find_by_id(tracker_id)
|
|
end
|
|
end
|
|
|
|
# Returns true if missing categories should be created during the import
|
|
def create_categories?
|
|
user.allowed_to?(:manage_categories, project) &&
|
|
mapping['create_categories'] == '1'
|
|
end
|
|
|
|
# Returns true if missing versions should be created during the import
|
|
def create_versions?
|
|
user.allowed_to?(:manage_versions, project) &&
|
|
mapping['create_versions'] == '1'
|
|
end
|
|
|
|
def mappable_custom_fields
|
|
if tracker
|
|
issue = Issue.new
|
|
issue.project = project
|
|
issue.tracker = tracker
|
|
issue.editable_custom_field_values(user).map(&:custom_field)
|
|
elsif project
|
|
project.all_issue_custom_fields
|
|
else
|
|
[]
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def build_object(row, item)
|
|
issue = Issue.new
|
|
issue.author = user
|
|
issue.notify = !!ActiveRecord::Type::Boolean.new.cast(settings['notifications'])
|
|
|
|
tracker_id = nil
|
|
if tracker
|
|
tracker_id = tracker.id
|
|
elsif tracker_name = row_value(row, 'tracker')
|
|
tracker_id = allowed_target_trackers.named(tracker_name).first.try(:id)
|
|
end
|
|
|
|
attributes = {
|
|
'project_id' => mapping['project_id'],
|
|
'tracker_id' => tracker_id,
|
|
'subject' => row_value(row, 'subject'),
|
|
'description' => row_value(row, 'description')
|
|
}
|
|
if status_name = row_value(row, 'status')
|
|
if status_id = IssueStatus.named(status_name).first.try(:id)
|
|
attributes['status_id'] = status_id
|
|
end
|
|
end
|
|
issue.send :safe_attributes=, attributes, user
|
|
|
|
attributes = {}
|
|
if priority_name = row_value(row, 'priority')
|
|
if priority_id = IssuePriority.active.named(priority_name).first.try(:id)
|
|
attributes['priority_id'] = priority_id
|
|
end
|
|
end
|
|
if issue.project && category_name = row_value(row, 'category')
|
|
if category = issue.project.issue_categories.named(category_name).first
|
|
attributes['category_id'] = category.id
|
|
elsif create_categories?
|
|
category = issue.project.issue_categories.build
|
|
category.name = category_name
|
|
if category.save
|
|
attributes['category_id'] = category.id
|
|
end
|
|
end
|
|
end
|
|
if assignee_name = row_value(row, 'assigned_to')
|
|
if assignee = Principal.detect_by_keyword(issue.assignable_users, assignee_name)
|
|
attributes['assigned_to_id'] = assignee.id
|
|
end
|
|
end
|
|
if issue.project && version_name = row_value(row, 'fixed_version')
|
|
version =
|
|
issue.project.versions.named(version_name).first ||
|
|
issue.project.shared_versions.named(version_name).first
|
|
if version
|
|
attributes['fixed_version_id'] = version.id
|
|
elsif create_versions?
|
|
version = issue.project.versions.build
|
|
version.name = version_name
|
|
if version.save
|
|
attributes['fixed_version_id'] = version.id
|
|
end
|
|
end
|
|
end
|
|
if is_private = row_value(row, 'is_private')
|
|
if yes?(is_private)
|
|
attributes['is_private'] = '1'
|
|
end
|
|
end
|
|
if parent_issue_id = row_value(row, 'parent_issue_id')
|
|
if parent_issue_id.start_with? '#'
|
|
# refers to existing issue
|
|
attributes['parent_issue_id'] = parent_issue_id[1..-1]
|
|
elsif use_unique_id?
|
|
# refers to other row with unique id
|
|
issue_id = items.where(:unique_id => parent_issue_id).first.try(:obj_id)
|
|
|
|
if issue_id
|
|
attributes['parent_issue_id'] = issue_id
|
|
else
|
|
add_callback(parent_issue_id, 'set_as_parent', item.position)
|
|
end
|
|
elsif /\A\d+\z/.match?(parent_issue_id)
|
|
# refers to other row by position
|
|
parent_issue_id = parent_issue_id.to_i
|
|
|
|
if parent_issue_id > item.position
|
|
add_callback(parent_issue_id, 'set_as_parent', item.position)
|
|
elsif issue_id = items.where(:position => parent_issue_id).first.try(:obj_id)
|
|
attributes['parent_issue_id'] = issue_id
|
|
end
|
|
|
|
else
|
|
# Something is odd. Assign parent_issue_id to trigger validation error
|
|
attributes['parent_issue_id'] = parent_issue_id
|
|
end
|
|
end
|
|
if start_date = row_date(row, 'start_date')
|
|
attributes['start_date'] = start_date
|
|
end
|
|
if due_date = row_date(row, 'due_date')
|
|
attributes['due_date'] = due_date
|
|
end
|
|
if estimated_hours = row_value(row, 'estimated_hours')
|
|
attributes['estimated_hours'] = estimated_hours
|
|
end
|
|
if done_ratio = row_value(row, 'done_ratio')
|
|
attributes['done_ratio'] = done_ratio
|
|
end
|
|
|
|
attributes['custom_field_values'] = issue.custom_field_values.inject({}) do |h, v|
|
|
value =
|
|
case v.custom_field.field_format
|
|
when 'date'
|
|
row_date(row, "cf_#{v.custom_field.id}")
|
|
else
|
|
row_value(row, "cf_#{v.custom_field.id}")
|
|
end
|
|
if value
|
|
h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(value, issue)
|
|
end
|
|
h
|
|
end
|
|
|
|
issue.send :safe_attributes=, attributes, user
|
|
|
|
if issue.tracker_id != tracker_id
|
|
issue.tracker_id = nil
|
|
end
|
|
|
|
issue
|
|
end
|
|
|
|
def extend_object(row, item, issue)
|
|
build_relations(row, item, issue)
|
|
end
|
|
|
|
def build_relations(row, item, issue)
|
|
IssueRelation::TYPES.each_key do |type|
|
|
has_delay = [IssueRelation::TYPE_PRECEDES, IssueRelation::TYPE_FOLLOWS].include?(type)
|
|
|
|
if decls = relation_values(row, "relation_#{type}")
|
|
decls.each do |decl|
|
|
unless decl[:matches]
|
|
# Invalid relation syntax - doesn't match regexp
|
|
next
|
|
end
|
|
|
|
if decl[:delay] && !has_delay
|
|
# Invalid relation syntax - delay for relation that doesn't support delays
|
|
next
|
|
end
|
|
|
|
relation = IssueRelation.new(
|
|
"relation_type" => type,
|
|
"issue_from_id" => issue.id
|
|
)
|
|
|
|
if decl[:other_id]
|
|
relation.issue_to_id = decl[:other_id]
|
|
elsif decl[:other_pos]
|
|
if use_unique_id?
|
|
issue_id = items.where(:unique_id => decl[:other_pos]).first.try(:obj_id)
|
|
if issue_id
|
|
relation.issue_to_id = issue_id
|
|
else
|
|
add_callback(decl[:other_pos], 'set_relation', item.position, type, decl[:delay])
|
|
next
|
|
end
|
|
elsif decl[:other_pos] > item.position
|
|
add_callback(decl[:other_pos], 'set_relation', item.position, type, decl[:delay])
|
|
next
|
|
elsif issue_id = items.where(:position => decl[:other_pos]).first.try(:obj_id)
|
|
relation.issue_to_id = issue_id
|
|
end
|
|
end
|
|
|
|
relation.delay = decl[:delay] if decl[:delay]
|
|
|
|
relation.save!
|
|
end
|
|
end
|
|
end
|
|
|
|
issue
|
|
end
|
|
|
|
def relation_values(row, name)
|
|
content = row_value(row, name)
|
|
|
|
return if content.blank?
|
|
|
|
content.split(",").map do |declaration|
|
|
declaration = declaration.strip
|
|
|
|
# Valid expression:
|
|
#
|
|
# 123 => row 123 within the CSV
|
|
# #123 => issue with ID 123
|
|
#
|
|
# For precedes and follows
|
|
#
|
|
# 123 7d => row 123 within CSV with 7 day delay
|
|
# #123 7d => issue with ID 123 with 7 day delay
|
|
# 123 -3d => negative delay allowed
|
|
#
|
|
#
|
|
# Invalid expression:
|
|
#
|
|
# No. 123 => Invalid leading letters
|
|
# # 123 => Invalid space between # and issue number
|
|
# 123 8h => No other time units allowed (just days)
|
|
#
|
|
# Please note: If unique_id mapping is present, the whole line - but the
|
|
# trailing delay expression - is considered unique_id.
|
|
#
|
|
# See examples at Rubular http://rubular.com/r/mgXM5Rp6zK
|
|
#
|
|
match = declaration.match(/\A(?<unique_id>(?<is_id>#)?(?<id>\d+)|.+?)(?:\s+(?<delay>-?\d+)d)?\z/)
|
|
|
|
result = {
|
|
:matches => false,
|
|
:declaration => declaration
|
|
}
|
|
|
|
if match
|
|
result[:matches] = true
|
|
result[:delay] = match[:delay]
|
|
|
|
if match[:is_id] && match[:id]
|
|
result[:other_id] = match[:id]
|
|
elsif use_unique_id? && match[:unique_id]
|
|
result[:other_pos] = match[:unique_id]
|
|
elsif match[:id]
|
|
result[:other_pos] = match[:id].to_i
|
|
else
|
|
result[:matches] = false
|
|
end
|
|
end
|
|
|
|
result
|
|
end
|
|
end
|
|
|
|
# Callback that sets issue as the parent of a previously imported issue
|
|
def set_as_parent_callback(issue, child_position)
|
|
child_id = items.where(:position => child_position).first.try(:obj_id)
|
|
return unless child_id
|
|
|
|
child = Issue.find_by_id(child_id)
|
|
return unless child
|
|
|
|
child.parent_issue_id = issue.id
|
|
child.save!
|
|
issue.reload
|
|
end
|
|
|
|
def set_relation_callback(to_issue, from_position, type, delay)
|
|
return if to_issue.new_record?
|
|
|
|
from_id = items.where(:position => from_position).first.try(:obj_id)
|
|
return unless from_id
|
|
|
|
IssueRelation.create!(
|
|
'relation_type' => type,
|
|
'issue_from_id' => from_id,
|
|
'issue_to_id' => to_issue.id,
|
|
'delay' => delay
|
|
)
|
|
to_issue.reload
|
|
end
|
|
end
|