1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
|
# frozen_string_literal: true
# Redmine - project management software
# Copyright (C) 2006- 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',
'unique_id' => 'field_unique_id',
'relation_duplicates' => 'label_duplicates',
'relation_duplicated' => 'label_duplicated_by',
'relation_blocks' => 'label_blocks',
'relation_blocked' => 'label_blocked_by',
'relation_relates' => 'label_relates_to',
'relation_precedes' => 'label_precedes',
'relation_follows' => 'label_follows',
'relation_copied_to' => 'label_copied_to',
'relation_copied_from' => 'label_copied_from'
}
def self.menu_item
:issues
end
def self.authorized?(user)
user.allowed_to?(:import_issues, nil, :global => true) && user.allowed_to?(:add_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]
begin
relation.save!
rescue
nil
end
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
|