From 40ff8e280e26fcc7bdeb8b365f119fd236d1d787 Mon Sep 17 00:00:00 2001
From: Gregor Schmidt
Date: Fri, 16 Feb 2018 10:54:54 +0100
Subject: [PATCH] Support issue relations when importing issues
---
app/models/import.rb | 8 +-
app/models/issue_import.rb | 113 +++++++++++++++++++++
app/views/imports/_fields_mapping.html.erb | 16 ++-
app/views/imports/_relations_mapping.html.erb | 55 ++++++++++
app/views/imports/mapping.html.erb | 7 ++
config/locales/de.yml | 1 +
config/locales/en.yml | 1 +
test/fixtures/files/import_subtasks.csv | 10 +-
.../files/import_subtasks_with_relations.csv | 5 +
test/unit/issue_import_test.rb | 108 ++++++++++++++++++++
10 files changed, 308 insertions(+), 16 deletions(-)
create mode 100644 app/views/imports/_relations_mapping.html.erb
create mode 100644 test/fixtures/files/import_subtasks_with_relations.csv
diff --git a/app/models/import.rb b/app/models/import.rb
index d2c53baac..00ebf4bb5 100644
--- a/app/models/import.rb
+++ b/app/models/import.rb
@@ -186,6 +186,7 @@ class Import < ActiveRecord::Base
item.save!
imported += 1
+ extend_object(row, item, object) if object.persisted?
do_callbacks(item.position, object)
end
current = position
@@ -243,7 +244,12 @@ class Import < ActiveRecord::Base
# Builds a record for the given row and returns it
# To be implemented by subclasses
- def build_object(row)
+ def build_object(row, item)
+ end
+
+ # Extends object with properties, that may only be handled after it's been
+ # persisted.
+ def extend_object(row, item, object)
end
# Generates a filename used to store the import file
diff --git a/app/models/issue_import.rb b/app/models/issue_import.rb
index ad04c0be5..2c5d97011 100644
--- a/app/models/issue_import.rb
+++ b/app/models/issue_import.rb
@@ -188,6 +188,104 @@ class IssueImport < Import
issue
end
+ def extend_object(row, item, issue)
+ build_relations(row, item, issue)
+ end
+
+ def build_relations(row, item, issue)
+ IssueRelation::TYPES.keys.each do |type|
+ has_delay = type == IssueRelation::TYPE_PRECEDES || type == IssueRelation::TYPE_FOLLOWS
+
+ 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 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)
+ #
+ # See examples at Rubular http://rubular.com/r/mgXM5Rp6zK
+ #
+ match = declaration.match(/\A(?#)?(?\d+)(?:\s+(?-?\d+)d)?\z/)
+
+ result = {
+ :matches => false,
+ :declaration => declaration
+ }
+
+ if match
+ result[:matches] = true
+ result[:delay] = match[:delay]
+
+ if match[:is_id] and match[:id]
+ result[:other_id] = match[: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)
@@ -200,4 +298,19 @@ class IssueImport < Import
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
diff --git a/app/views/imports/_fields_mapping.html.erb b/app/views/imports/_fields_mapping.html.erb
index f59350116..cc35deef0 100644
--- a/app/views/imports/_fields_mapping.html.erb
+++ b/app/views/imports/_fields_mapping.html.erb
@@ -52,12 +52,6 @@
<% end %>
-<% @custom_fields.each do |field| %>
-
-
- <%= mapping_select_tag @import, "cf_#{field.id}" %>
-
-<% end %>
@@ -65,10 +59,6 @@
<%= mapping_select_tag @import, 'is_private' %>
-
-
- <%= mapping_select_tag @import, 'parent_issue_id' %>
-
<%= mapping_select_tag @import, 'start_date' %>
@@ -85,6 +75,12 @@
<%= mapping_select_tag @import, 'done_ratio' %>
+<% @custom_fields.each do |field| %>
+
+
+ <%= mapping_select_tag @import, "cf_#{field.id}" %>
+
+<% end %>
diff --git a/app/views/imports/_relations_mapping.html.erb b/app/views/imports/_relations_mapping.html.erb
new file mode 100644
index 000000000..3549bca18
--- /dev/null
+++ b/app/views/imports/_relations_mapping.html.erb
@@ -0,0 +1,55 @@
+
+
+
+
+ <%= mapping_select_tag @import, 'parent_issue_id' %>
+
+
+
+
+ <%= mapping_select_tag @import, 'relation_duplicates' %>
+
+
+
+
+ <%= mapping_select_tag @import, 'relation_duplicated' %>
+
+
+
+
+ <%= mapping_select_tag @import, 'relation_blocks' %>
+
+
+
+
+ <%= mapping_select_tag @import, 'relation_blocked' %>
+
+
+
+
+
+
+ <%= mapping_select_tag @import, 'relation_relates' %>
+
+
+
+
+ <%= mapping_select_tag @import, 'relation_precedes' %>
+
+
+
+
+ <%= mapping_select_tag @import, 'relation_follows' %>
+
+
+
+
+ <%= mapping_select_tag @import, 'relation_copied_to' %>
+
+
+
+
+ <%= mapping_select_tag @import, 'relation_copied_from' %>
+
+
+
diff --git a/app/views/imports/mapping.html.erb b/app/views/imports/mapping.html.erb
index 2e225d6c2..8a465891a 100644
--- a/app/views/imports/mapping.html.erb
+++ b/app/views/imports/mapping.html.erb
@@ -8,6 +8,13 @@
+
+