選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

issue_import.rb 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. # frozen_string_literal: true
  2. # Redmine - project management software
  3. # Copyright (C) 2006-2021 Jean-Philippe Lang
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; either version 2
  8. # of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. class IssueImport < Import
  19. AUTO_MAPPABLE_FIELDS = {
  20. 'tracker' => 'field_tracker',
  21. 'subject' => 'field_subject',
  22. 'description' => 'field_description',
  23. 'status' => 'field_status',
  24. 'priority' => 'field_priority',
  25. 'category' => 'field_category',
  26. 'assigned_to' => 'field_assigned_to',
  27. 'fixed_version' => 'field_fixed_version',
  28. 'is_private' => 'field_is_private',
  29. 'parent_issue_id' => 'field_parent_issue',
  30. 'start_date' => 'field_start_date',
  31. 'due_date' => 'field_due_date',
  32. 'estimated_hours' => 'field_estimated_hours',
  33. 'done_ratio' => 'field_done_ratio',
  34. 'unique_id' => 'field_unique_id',
  35. 'relation_duplicates' => 'label_duplicates',
  36. 'relation_duplicated' => 'label_duplicated_by',
  37. 'relation_blocks' => 'label_blocks',
  38. 'relation_blocked' => 'label_blocked_by',
  39. 'relation_relates' => 'label_relates_to',
  40. 'relation_precedes' => 'label_precedes',
  41. 'relation_follows' => 'label_follows',
  42. 'relation_copied_to' => 'label_copied_to',
  43. 'relation_copied_from' => 'label_copied_from'
  44. }
  45. def self.menu_item
  46. :issues
  47. end
  48. def self.authorized?(user)
  49. user.allowed_to?(:import_issues, nil, :global => true)
  50. end
  51. # Returns the objects that were imported
  52. def saved_objects
  53. object_ids = saved_items.pluck(:obj_id)
  54. objects = Issue.where(:id => object_ids).order(:id).preload(:tracker, :priority, :status)
  55. end
  56. # Returns a scope of projects that user is allowed to
  57. # import issue to
  58. def allowed_target_projects
  59. Project.allowed_to(user, :import_issues)
  60. end
  61. def project
  62. project_id = mapping['project_id'].to_i
  63. allowed_target_projects.find_by_id(project_id) || allowed_target_projects.first
  64. end
  65. # Returns a scope of trackers that user is allowed to
  66. # import issue to
  67. def allowed_target_trackers
  68. Issue.allowed_target_trackers(project, user)
  69. end
  70. def tracker
  71. if mapping['tracker'].to_s =~ /\Avalue:(\d+)\z/
  72. tracker_id = $1.to_i
  73. allowed_target_trackers.find_by_id(tracker_id)
  74. end
  75. end
  76. # Returns true if missing categories should be created during the import
  77. def create_categories?
  78. user.allowed_to?(:manage_categories, project) &&
  79. mapping['create_categories'] == '1'
  80. end
  81. # Returns true if missing versions should be created during the import
  82. def create_versions?
  83. user.allowed_to?(:manage_versions, project) &&
  84. mapping['create_versions'] == '1'
  85. end
  86. def mappable_custom_fields
  87. if tracker
  88. issue = Issue.new
  89. issue.project = project
  90. issue.tracker = tracker
  91. issue.editable_custom_field_values(user).map(&:custom_field)
  92. elsif project
  93. project.all_issue_custom_fields
  94. else
  95. []
  96. end
  97. end
  98. private
  99. def build_object(row, item)
  100. issue = Issue.new
  101. issue.author = user
  102. issue.notify = !!ActiveRecord::Type::Boolean.new.cast(settings['notifications'])
  103. tracker_id = nil
  104. if tracker
  105. tracker_id = tracker.id
  106. elsif tracker_name = row_value(row, 'tracker')
  107. tracker_id = allowed_target_trackers.named(tracker_name).first.try(:id)
  108. end
  109. attributes = {
  110. 'project_id' => mapping['project_id'],
  111. 'tracker_id' => tracker_id,
  112. 'subject' => row_value(row, 'subject'),
  113. 'description' => row_value(row, 'description')
  114. }
  115. if status_name = row_value(row, 'status')
  116. if status_id = IssueStatus.named(status_name).first.try(:id)
  117. attributes['status_id'] = status_id
  118. end
  119. end
  120. issue.send :safe_attributes=, attributes, user
  121. attributes = {}
  122. if priority_name = row_value(row, 'priority')
  123. if priority_id = IssuePriority.active.named(priority_name).first.try(:id)
  124. attributes['priority_id'] = priority_id
  125. end
  126. end
  127. if issue.project && category_name = row_value(row, 'category')
  128. if category = issue.project.issue_categories.named(category_name).first
  129. attributes['category_id'] = category.id
  130. elsif create_categories?
  131. category = issue.project.issue_categories.build
  132. category.name = category_name
  133. if category.save
  134. attributes['category_id'] = category.id
  135. end
  136. end
  137. end
  138. if assignee_name = row_value(row, 'assigned_to')
  139. if assignee = Principal.detect_by_keyword(issue.assignable_users, assignee_name)
  140. attributes['assigned_to_id'] = assignee.id
  141. end
  142. end
  143. if issue.project && version_name = row_value(row, 'fixed_version')
  144. version =
  145. issue.project.versions.named(version_name).first ||
  146. issue.project.shared_versions.named(version_name).first
  147. if version
  148. attributes['fixed_version_id'] = version.id
  149. elsif create_versions?
  150. version = issue.project.versions.build
  151. version.name = version_name
  152. if version.save
  153. attributes['fixed_version_id'] = version.id
  154. end
  155. end
  156. end
  157. if is_private = row_value(row, 'is_private')
  158. if yes?(is_private)
  159. attributes['is_private'] = '1'
  160. end
  161. end
  162. if parent_issue_id = row_value(row, 'parent_issue_id')
  163. if parent_issue_id.start_with? '#'
  164. # refers to existing issue
  165. attributes['parent_issue_id'] = parent_issue_id[1..-1]
  166. elsif use_unique_id?
  167. # refers to other row with unique id
  168. issue_id = items.where(:unique_id => parent_issue_id).first.try(:obj_id)
  169. if issue_id
  170. attributes['parent_issue_id'] = issue_id
  171. else
  172. add_callback(parent_issue_id, 'set_as_parent', item.position)
  173. end
  174. elsif /\A\d+\z/.match?(parent_issue_id)
  175. # refers to other row by position
  176. parent_issue_id = parent_issue_id.to_i
  177. if parent_issue_id > item.position
  178. add_callback(parent_issue_id, 'set_as_parent', item.position)
  179. elsif issue_id = items.where(:position => parent_issue_id).first.try(:obj_id)
  180. attributes['parent_issue_id'] = issue_id
  181. end
  182. else
  183. # Something is odd. Assign parent_issue_id to trigger validation error
  184. attributes['parent_issue_id'] = parent_issue_id
  185. end
  186. end
  187. if start_date = row_date(row, 'start_date')
  188. attributes['start_date'] = start_date
  189. end
  190. if due_date = row_date(row, 'due_date')
  191. attributes['due_date'] = due_date
  192. end
  193. if estimated_hours = row_value(row, 'estimated_hours')
  194. attributes['estimated_hours'] = estimated_hours
  195. end
  196. if done_ratio = row_value(row, 'done_ratio')
  197. attributes['done_ratio'] = done_ratio
  198. end
  199. attributes['custom_field_values'] = issue.custom_field_values.inject({}) do |h, v|
  200. value =
  201. case v.custom_field.field_format
  202. when 'date'
  203. row_date(row, "cf_#{v.custom_field.id}")
  204. else
  205. row_value(row, "cf_#{v.custom_field.id}")
  206. end
  207. if value
  208. h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(value, issue)
  209. end
  210. h
  211. end
  212. issue.send :safe_attributes=, attributes, user
  213. if issue.tracker_id != tracker_id
  214. issue.tracker_id = nil
  215. end
  216. issue
  217. end
  218. def extend_object(row, item, issue)
  219. build_relations(row, item, issue)
  220. end
  221. def build_relations(row, item, issue)
  222. IssueRelation::TYPES.each_key do |type|
  223. has_delay = [IssueRelation::TYPE_PRECEDES, IssueRelation::TYPE_FOLLOWS].include?(type)
  224. if decls = relation_values(row, "relation_#{type}")
  225. decls.each do |decl|
  226. unless decl[:matches]
  227. # Invalid relation syntax - doesn't match regexp
  228. next
  229. end
  230. if decl[:delay] && !has_delay
  231. # Invalid relation syntax - delay for relation that doesn't support delays
  232. next
  233. end
  234. relation = IssueRelation.new(
  235. "relation_type" => type,
  236. "issue_from_id" => issue.id
  237. )
  238. if decl[:other_id]
  239. relation.issue_to_id = decl[:other_id]
  240. elsif decl[:other_pos]
  241. if use_unique_id?
  242. issue_id = items.where(:unique_id => decl[:other_pos]).first.try(:obj_id)
  243. if issue_id
  244. relation.issue_to_id = issue_id
  245. else
  246. add_callback(decl[:other_pos], 'set_relation', item.position, type, decl[:delay])
  247. next
  248. end
  249. elsif decl[:other_pos] > item.position
  250. add_callback(decl[:other_pos], 'set_relation', item.position, type, decl[:delay])
  251. next
  252. elsif issue_id = items.where(:position => decl[:other_pos]).first.try(:obj_id)
  253. relation.issue_to_id = issue_id
  254. end
  255. end
  256. relation.delay = decl[:delay] if decl[:delay]
  257. relation.save!
  258. end
  259. end
  260. end
  261. issue
  262. end
  263. def relation_values(row, name)
  264. content = row_value(row, name)
  265. return if content.blank?
  266. content.split(",").map do |declaration|
  267. declaration = declaration.strip
  268. # Valid expression:
  269. #
  270. # 123 => row 123 within the CSV
  271. # #123 => issue with ID 123
  272. #
  273. # For precedes and follows
  274. #
  275. # 123 7d => row 123 within CSV with 7 day delay
  276. # #123 7d => issue with ID 123 with 7 day delay
  277. # 123 -3d => negative delay allowed
  278. #
  279. #
  280. # Invalid expression:
  281. #
  282. # No. 123 => Invalid leading letters
  283. # # 123 => Invalid space between # and issue number
  284. # 123 8h => No other time units allowed (just days)
  285. #
  286. # Please note: If unique_id mapping is present, the whole line - but the
  287. # trailing delay expression - is considered unique_id.
  288. #
  289. # See examples at Rubular http://rubular.com/r/mgXM5Rp6zK
  290. #
  291. match = declaration.match(/\A(?<unique_id>(?<is_id>#)?(?<id>\d+)|.+?)(?:\s+(?<delay>-?\d+)d)?\z/)
  292. result = {
  293. :matches => false,
  294. :declaration => declaration
  295. }
  296. if match
  297. result[:matches] = true
  298. result[:delay] = match[:delay]
  299. if match[:is_id] && match[:id]
  300. result[:other_id] = match[:id]
  301. elsif use_unique_id? && match[:unique_id]
  302. result[:other_pos] = match[:unique_id]
  303. elsif match[:id]
  304. result[:other_pos] = match[:id].to_i
  305. else
  306. result[:matches] = false
  307. end
  308. end
  309. result
  310. end
  311. end
  312. # Callback that sets issue as the parent of a previously imported issue
  313. def set_as_parent_callback(issue, child_position)
  314. child_id = items.where(:position => child_position).first.try(:obj_id)
  315. return unless child_id
  316. child = Issue.find_by_id(child_id)
  317. return unless child
  318. child.parent_issue_id = issue.id
  319. child.save!
  320. issue.reload
  321. end
  322. def set_relation_callback(to_issue, from_position, type, delay)
  323. return if to_issue.new_record?
  324. from_id = items.where(:position => from_position).first.try(:obj_id)
  325. return unless from_id
  326. IssueRelation.create!(
  327. 'relation_type' => type,
  328. 'issue_from_id' => from_id,
  329. 'issue_to_id' => to_issue.id,
  330. 'delay' => delay
  331. )
  332. to_issue.reload
  333. end
  334. end