You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

issue_import.rb 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. # frozen_string_literal: true
  2. # Redmine - project management software
  3. # Copyright (C) 2006-2020 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. }
  35. def self.menu_item
  36. :issues
  37. end
  38. def self.authorized?(user)
  39. user.allowed_to?(:import_issues, nil, :global => true)
  40. end
  41. # Returns the objects that were imported
  42. def saved_objects
  43. object_ids = saved_items.pluck(:obj_id)
  44. objects = Issue.where(:id => object_ids).order(:id).preload(:tracker, :priority, :status)
  45. end
  46. # Returns a scope of projects that user is allowed to
  47. # import issue to
  48. def allowed_target_projects
  49. Project.allowed_to(user, :import_issues)
  50. end
  51. def project
  52. project_id = mapping['project_id'].to_i
  53. allowed_target_projects.find_by_id(project_id) || allowed_target_projects.first
  54. end
  55. # Returns a scope of trackers that user is allowed to
  56. # import issue to
  57. def allowed_target_trackers
  58. Issue.allowed_target_trackers(project, user)
  59. end
  60. def tracker
  61. if mapping['tracker'].to_s =~ /\Avalue:(\d+)\z/
  62. tracker_id = $1.to_i
  63. allowed_target_trackers.find_by_id(tracker_id)
  64. end
  65. end
  66. # Returns true if missing categories should be created during the import
  67. def create_categories?
  68. user.allowed_to?(:manage_categories, project) &&
  69. mapping['create_categories'] == '1'
  70. end
  71. # Returns true if missing versions should be created during the import
  72. def create_versions?
  73. user.allowed_to?(:manage_versions, project) &&
  74. mapping['create_versions'] == '1'
  75. end
  76. def mappable_custom_fields
  77. if tracker
  78. issue = Issue.new
  79. issue.project = project
  80. issue.tracker = tracker
  81. issue.editable_custom_field_values(user).map(&:custom_field)
  82. elsif project
  83. project.all_issue_custom_fields
  84. else
  85. []
  86. end
  87. end
  88. private
  89. def build_object(row, item)
  90. issue = Issue.new
  91. issue.author = user
  92. issue.notify = !!ActiveRecord::Type::Boolean.new.cast(settings['notifications'])
  93. tracker_id = nil
  94. if tracker
  95. tracker_id = tracker.id
  96. elsif tracker_name = row_value(row, 'tracker')
  97. tracker_id = allowed_target_trackers.named(tracker_name).first.try(:id)
  98. end
  99. attributes = {
  100. 'project_id' => mapping['project_id'],
  101. 'tracker_id' => tracker_id,
  102. 'subject' => row_value(row, 'subject'),
  103. 'description' => row_value(row, 'description')
  104. }
  105. if status_name = row_value(row, 'status')
  106. if status_id = IssueStatus.named(status_name).first.try(:id)
  107. attributes['status_id'] = status_id
  108. end
  109. end
  110. issue.send :safe_attributes=, attributes, user
  111. attributes = {}
  112. if priority_name = row_value(row, 'priority')
  113. if priority_id = IssuePriority.active.named(priority_name).first.try(:id)
  114. attributes['priority_id'] = priority_id
  115. end
  116. end
  117. if issue.project && category_name = row_value(row, 'category')
  118. if category = issue.project.issue_categories.named(category_name).first
  119. attributes['category_id'] = category.id
  120. elsif create_categories?
  121. category = issue.project.issue_categories.build
  122. category.name = category_name
  123. if category.save
  124. attributes['category_id'] = category.id
  125. end
  126. end
  127. end
  128. if assignee_name = row_value(row, 'assigned_to')
  129. if assignee = Principal.detect_by_keyword(issue.assignable_users, assignee_name)
  130. attributes['assigned_to_id'] = assignee.id
  131. end
  132. end
  133. if issue.project && version_name = row_value(row, 'fixed_version')
  134. version =
  135. issue.project.versions.named(version_name).first ||
  136. issue.project.shared_versions.named(version_name).first
  137. if version
  138. attributes['fixed_version_id'] = version.id
  139. elsif create_versions?
  140. version = issue.project.versions.build
  141. version.name = version_name
  142. if version.save
  143. attributes['fixed_version_id'] = version.id
  144. end
  145. end
  146. end
  147. if is_private = row_value(row, 'is_private')
  148. if yes?(is_private)
  149. attributes['is_private'] = '1'
  150. end
  151. end
  152. if parent_issue_id = row_value(row, 'parent_issue_id')
  153. if parent_issue_id.start_with? '#'
  154. # refers to existing issue
  155. attributes['parent_issue_id'] = parent_issue_id[1..-1]
  156. elsif use_unique_id?
  157. # refers to other row with unique id
  158. issue_id = items.where(:unique_id => parent_issue_id).first.try(:obj_id)
  159. if issue_id
  160. attributes['parent_issue_id'] = issue_id
  161. else
  162. add_callback(parent_issue_id, 'set_as_parent', item.position)
  163. end
  164. elsif /\A\d+\z/.match?(parent_issue_id)
  165. # refers to other row by position
  166. parent_issue_id = parent_issue_id.to_i
  167. if parent_issue_id > item.position
  168. add_callback(parent_issue_id, 'set_as_parent', item.position)
  169. elsif issue_id = items.where(:position => parent_issue_id).first.try(:obj_id)
  170. attributes['parent_issue_id'] = issue_id
  171. end
  172. else
  173. # Something is odd. Assign parent_issue_id to trigger validation error
  174. attributes['parent_issue_id'] = parent_issue_id
  175. end
  176. end
  177. if start_date = row_date(row, 'start_date')
  178. attributes['start_date'] = start_date
  179. end
  180. if due_date = row_date(row, 'due_date')
  181. attributes['due_date'] = due_date
  182. end
  183. if estimated_hours = row_value(row, 'estimated_hours')
  184. attributes['estimated_hours'] = estimated_hours
  185. end
  186. if done_ratio = row_value(row, 'done_ratio')
  187. attributes['done_ratio'] = done_ratio
  188. end
  189. attributes['custom_field_values'] = issue.custom_field_values.inject({}) do |h, v|
  190. value =
  191. case v.custom_field.field_format
  192. when 'date'
  193. row_date(row, "cf_#{v.custom_field.id}")
  194. else
  195. row_value(row, "cf_#{v.custom_field.id}")
  196. end
  197. if value
  198. h[v.custom_field.id.to_s] = v.custom_field.value_from_keyword(value, issue)
  199. end
  200. h
  201. end
  202. issue.send :safe_attributes=, attributes, user
  203. if issue.tracker_id != tracker_id
  204. issue.tracker_id = nil
  205. end
  206. issue
  207. end
  208. def extend_object(row, item, issue)
  209. build_relations(row, item, issue)
  210. end
  211. def build_relations(row, item, issue)
  212. IssueRelation::TYPES.each_key do |type|
  213. has_delay = [IssueRelation::TYPE_PRECEDES, IssueRelation::TYPE_FOLLOWS].include?(type)
  214. if decls = relation_values(row, "relation_#{type}")
  215. decls.each do |decl|
  216. unless decl[:matches]
  217. # Invalid relation syntax - doesn't match regexp
  218. next
  219. end
  220. if decl[:delay] && !has_delay
  221. # Invalid relation syntax - delay for relation that doesn't support delays
  222. next
  223. end
  224. relation = IssueRelation.new(
  225. "relation_type" => type,
  226. "issue_from_id" => issue.id
  227. )
  228. if decl[:other_id]
  229. relation.issue_to_id = decl[:other_id]
  230. elsif decl[:other_pos]
  231. if use_unique_id?
  232. issue_id = items.where(:unique_id => decl[:other_pos]).first.try(:obj_id)
  233. if issue_id
  234. relation.issue_to_id = issue_id
  235. else
  236. add_callback(decl[:other_pos], 'set_relation', item.position, type, decl[:delay])
  237. next
  238. end
  239. elsif decl[:other_pos] > item.position
  240. add_callback(decl[:other_pos], 'set_relation', item.position, type, decl[:delay])
  241. next
  242. elsif issue_id = items.where(:position => decl[:other_pos]).first.try(:obj_id)
  243. relation.issue_to_id = issue_id
  244. end
  245. end
  246. relation.delay = decl[:delay] if decl[:delay]
  247. relation.save!
  248. end
  249. end
  250. end
  251. issue
  252. end
  253. def relation_values(row, name)
  254. content = row_value(row, name)
  255. return if content.blank?
  256. content.split(",").map do |declaration|
  257. declaration = declaration.strip
  258. # Valid expression:
  259. #
  260. # 123 => row 123 within the CSV
  261. # #123 => issue with ID 123
  262. #
  263. # For precedes and follows
  264. #
  265. # 123 7d => row 123 within CSV with 7 day delay
  266. # #123 7d => issue with ID 123 with 7 day delay
  267. # 123 -3d => negative delay allowed
  268. #
  269. #
  270. # Invalid expression:
  271. #
  272. # No. 123 => Invalid leading letters
  273. # # 123 => Invalid space between # and issue number
  274. # 123 8h => No other time units allowed (just days)
  275. #
  276. # Please note: If unique_id mapping is present, the whole line - but the
  277. # trailing delay expression - is considered unique_id.
  278. #
  279. # See examples at Rubular http://rubular.com/r/mgXM5Rp6zK
  280. #
  281. match = declaration.match(/\A(?<unique_id>(?<is_id>#)?(?<id>\d+)|.+?)(?:\s+(?<delay>-?\d+)d)?\z/)
  282. result = {
  283. :matches => false,
  284. :declaration => declaration
  285. }
  286. if match
  287. result[:matches] = true
  288. result[:delay] = match[:delay]
  289. if match[:is_id] && match[:id]
  290. result[:other_id] = match[:id]
  291. elsif use_unique_id? && match[:unique_id]
  292. result[:other_pos] = match[:unique_id]
  293. elsif match[:id]
  294. result[:other_pos] = match[:id].to_i
  295. else
  296. result[:matches] = false
  297. end
  298. end
  299. result
  300. end
  301. end
  302. # Callback that sets issue as the parent of a previously imported issue
  303. def set_as_parent_callback(issue, child_position)
  304. child_id = items.where(:position => child_position).first.try(:obj_id)
  305. return unless child_id
  306. child = Issue.find_by_id(child_id)
  307. return unless child
  308. child.parent_issue_id = issue.id
  309. child.save!
  310. issue.reload
  311. end
  312. def set_relation_callback(to_issue, from_position, type, delay)
  313. return if to_issue.new_record?
  314. from_id = items.where(:position => from_position).first.try(:obj_id)
  315. return unless from_id
  316. IssueRelation.create!(
  317. 'relation_type' => type,
  318. 'issue_from_id' => from_id,
  319. 'issue_to_id' => to_issue.id,
  320. 'delay' => delay
  321. )
  322. to_issue.reload
  323. end
  324. end