From 1f62d567e9f4f94b7dd0a327cd2f4a0dd41dc22b Mon Sep 17 00:00:00 2001 From: Leo Hourvitz Date: Sat, 25 Sep 2010 02:31:40 +0900 Subject: [PATCH 01/36] Fixed instance variable dependency and made match view match heading automatically when possible --- app/controllers/importer_controller.rb | 35 ++++++-------------------- app/views/importer/match.html.erb | 3 ++- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/app/controllers/importer_controller.rb b/app/controllers/importer_controller.rb index f8ecb6d..1f8220c 100644 --- a/app/controllers/importer_controller.rb +++ b/app/controllers/importer_controller.rb @@ -22,21 +22,8 @@ def match # save import file @original_filename = file.original_filename - tmpfile = Tempfile.new("redmine_importer") - if tmpfile - tmpfile.write(file.read) - tmpfile.close - tmpfilename = File.basename(tmpfile.path) - if !$tmpfiles - $tmpfiles = Hash.new - end - $tmpfiles[tmpfilename] = tmpfile - else - flash[:error] = "Cannot save import file." - return - end - - session[:importer_tmpfile] = tmpfilename + # Abuse of session system, but... + session[:importer_csvdata] = file.read session[:importer_splitter] = splitter session[:importer_wrapper] = wrapper session[:importer_encoding] = encoding @@ -46,8 +33,8 @@ def match i = 0 @samples = [] - FasterCSV.foreach(tmpfile.path, {:headers=>true, - :encoding=>encoding, :quote_char=>wrapper, :col_sep=>splitter}) do |row| + FasterCSV.new(session[:importer_csvdata], {:headers=>true, + :encoding=>encoding, :quote_char=>wrapper, :col_sep=>splitter}).each do |row| @samples[i] = row i += 1 @@ -73,18 +60,12 @@ def match end def result - tmpfilename = session[:importer_tmpfile] + csvdata = session[:importer_csvdata] splitter = session[:importer_splitter] wrapper = session[:importer_wrapper] encoding = session[:importer_encoding] - - if tmpfilename - tmpfile = $tmpfiles[tmpfilename] - if tmpfile == nil - flash[:error] = "Missing imported file" - return - end - end + # Give the poor session system some relief + session[:importer_csvdata] = "" default_tracker = params[:default_tracker] update_issue = params[:update_issue] @@ -110,7 +91,7 @@ def result # attrs_map is fields_map's invert attrs_map = fields_map.invert - FasterCSV.foreach(tmpfile.path, {:headers=>true, :encoding=>encoding, :quote_char=>wrapper, :col_sep=>splitter}) do |row| + FasterCSV.new(csvdata, {:headers=>true, :encoding=>encoding, :quote_char=>wrapper, :col_sep=>splitter}).each do |row| project = Project.find_by_name(row[attrs_map["project"]]) tracker = Tracker.find_by_name(row[attrs_map["tracker"]]) diff --git a/app/views/importer/match.html.erb b/app/views/importer/match.html.erb index e9de6e6..fb0f586 100644 --- a/app/views/importer/match.html.erb +++ b/app/views/importer/match.html.erb @@ -15,7 +15,8 @@
<%= l(:label_match_select) %> <% @headers.each do |column| %> + <%= select_tag "fields_map[#{column}]", "" + + options_for_select( @attrs.collect {|a| [a[0],a[1].to_s] }, column.to_s.downcase ) %> <% end %>
From eabe3fa0ca4896e076a280bd125a0861a7d55733 Mon Sep 17 00:00:00 2001 From: Leo Hourvitz Date: Sat, 25 Sep 2010 02:35:49 +0900 Subject: [PATCH 02/36] Updated README --- README.rdoc | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.rdoc b/README.rdoc index ad22fb5..f0b5733 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,9 +1,10 @@ = Redmine Issue Importer -This plugin is broken and should not be used. If the use of it is -required, the only option is to run it in a single process, such as -WEBrick. Running this in a multi-process/thread environment will almost -guaranteed run in to errors due to a global variable that was used by -the initial author. - +This plugin seems to be functional now, +including in multiprocess environments. +I got rid of the use of instance variables to pass +data from the match stge to the result stage. +At this point, it's using the session support +to pass the data, so extremely large import files +could use a bit of extra memory on the server. From 960f169fff717f418c62df867c3f4a33cd756a88 Mon Sep 17 00:00:00 2001 From: Leo Hourvitz Date: Sat, 25 Sep 2010 03:14:32 +0900 Subject: [PATCH 03/36] Made Custom Fields also match, and made the match work for either key or value --- app/views/importer/match.html.erb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/views/importer/match.html.erb b/app/views/importer/match.html.erb index fb0f586..630c043 100644 --- a/app/views/importer/match.html.erb +++ b/app/views/importer/match.html.erb @@ -14,9 +14,12 @@ <%= hidden_field_tag 'project_id', @project.id %>
<%= l(:label_match_select) %> <% @headers.each do |column| %> + <% matched = '' + @attrs.each do |k,v| if v.to_s.casecmp(column.to_s) == 0 or k.to_s.casecmp(column.to_s) == 0 then matched = v end end + %> + options_for_select( @attrs, matched ) %> <% end %>
From 94f1bd5e4e77bfec6ad54956e717f419d6dd8264 Mon Sep 17 00:00:00 2001 From: Leo Hourvitz Date: Sat, 25 Sep 2010 15:03:11 +0900 Subject: [PATCH 04/36] Oops, forgot to update version number. --- init.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init.rb b/init.rb index 6e20dd7..0188919 100644 --- a/init.rb +++ b/init.rb @@ -4,7 +4,7 @@ name 'Issue Importer' author 'Martin Liu' description 'Issue import plugin for Redmine.' - version '0.3.1' + version '0.4' project_module :importer do permission :import, :importer => :index From ae86592f4f1abaa8e68254747f830e96c8103ea6 Mon Sep 17 00:00:00 2001 From: Leo Hourvitz Date: Fri, 15 Oct 2010 14:20:58 +0900 Subject: [PATCH 05/36] Switched to database for intermediate store --- app/controllers/importer_controller.rb | 74 ++++++++++++------- app/models/import_in_progress.rb | 5 ++ app/views/importer/match.html.erb | 1 + config/locales/en.yml | 6 +- config/locales/ja.yml | 34 +++++++++ db/migrate/001_create_import_in_progresses.rb | 16 ++++ init.rb | 2 +- test/fixtures/import_in_progresses.yml | 11 +++ test/unit/import_in_progress_test.rb | 10 +++ 9 files changed, 127 insertions(+), 32 deletions(-) create mode 100644 app/models/import_in_progress.rb create mode 100644 config/locales/ja.yml create mode 100644 db/migrate/001_create_import_in_progresses.rb create mode 100644 test/fixtures/import_in_progresses.yml create mode 100644 test/unit/import_in_progress_test.rb diff --git a/app/controllers/importer_controller.rb b/app/controllers/importer_controller.rb index 1f8220c..32dc32d 100644 --- a/app/controllers/importer_controller.rb +++ b/app/controllers/importer_controller.rb @@ -14,27 +14,30 @@ def index end def match - # params - file = params[:file] - splitter = params[:splitter] - wrapper = params[:wrapper] - encoding = params[:encoding] + # Delete existing iip to ensure there can't be two iips for a user + print "params are ", params + ImportInProgress.delete_all(["user_id = ?",User.current.id]) + # save import-in-progress data + iip = ImportInProgress.find_or_create_by_user_id(User.current.id) + iip.quote_char = params[:wrapper] + iip.col_sep = params[:splitter] + iip.encoding = params[:encoding] + iip.created = Time.new + iip.csv_data = params[:file].read + iip.save - # save import file - @original_filename = file.original_filename - # Abuse of session system, but... - session[:importer_csvdata] = file.read - session[:importer_splitter] = splitter - session[:importer_wrapper] = wrapper - session[:importer_encoding] = encoding + # Put the timestamp in the params to detect + # users with two imports in progress + @import_timestamp = iip.created.strftime("%Y-%m-%d %H:%M:%S") + @original_filename = params[:file].original_filename # display sample sample_count = 5 i = 0 @samples = [] - FasterCSV.new(session[:importer_csvdata], {:headers=>true, - :encoding=>encoding, :quote_char=>wrapper, :col_sep=>splitter}).each do |row| + FasterCSV.new(iip.csv_data, {:headers=>true, + :encoding=>iip.encoding, :quote_char=>iip.quote_char, :col_sep=>iip.col_sep}).each do |row| @samples[i] = row i += 1 @@ -60,12 +63,27 @@ def match end def result - csvdata = session[:importer_csvdata] - splitter = session[:importer_splitter] - wrapper = session[:importer_wrapper] - encoding = session[:importer_encoding] - # Give the poor session system some relief - session[:importer_csvdata] = "" + @handle_count = 0 + @update_count = 0 + @skip_count = 0 + @failed_count = 0 + @failed_issues = Hash.new + @affect_projects_issues = Hash.new + + # Retrieve saved import data + iip = ImportInProgress.find_by_user_id(User.current.id) + if iip == nil + flash[:error] = "No import is currently in progress" + return + end + print "iip.created is ", iip.created + print "params[:import_timestamp] is ", params[:import_timestamp] + if iip.created.strftime("%Y-%m-%d %H:%M:%S") != params[:import_timestamp] + flash[:error] = "You seem to have started another import " \ + "since starting this one. " \ + "This import cannot be completed" + return + end default_tracker = params[:default_tracker] update_issue = params[:update_issue] @@ -81,17 +99,11 @@ def result return end - @handle_count = 0 - @update_count = 0 - @skip_count = 0 - @failed_count = 0 - @failed_issues = Hash.new - @affect_projects_issues = Hash.new - # attrs_map is fields_map's invert attrs_map = fields_map.invert - FasterCSV.new(csvdata, {:headers=>true, :encoding=>encoding, :quote_char=>wrapper, :col_sep=>splitter}).each do |row| + FasterCSV.new(iip.csv_data, {:headers=>true, :encoding=>iip.encoding, + :quote_char=>iip.quote_char, :col_sep=>iip.col_sep}).each do |row| project = Project.find_by_name(row[attrs_map["project"]]) tracker = Tracker.find_by_name(row[attrs_map["tracker"]]) @@ -216,6 +228,12 @@ def result @failed_issues = @failed_issues.sort @headers = @failed_issues[0][1].headers end + + # Clean up after ourselves + iip.delete + + # Garbage prevention: clean up iips older than 3 days + ImportInProgress.delete_all(["created < ?",Time.new - 3*24*60*60]) end private diff --git a/app/models/import_in_progress.rb b/app/models/import_in_progress.rb new file mode 100644 index 0000000..33f35ed --- /dev/null +++ b/app/models/import_in_progress.rb @@ -0,0 +1,5 @@ +class ImportInProgress < ActiveRecord::Base + unloadable + belongs_to :user + belongs_to :project +end diff --git a/app/views/importer/match.html.erb b/app/views/importer/match.html.erb index 630c043..a7b30c8 100644 --- a/app/views/importer/match.html.erb +++ b/app/views/importer/match.html.erb @@ -12,6 +12,7 @@ --> <% form_tag({:action => 'result'}, {:multipart => true}) do %> <%= hidden_field_tag 'project_id', @project.id %> + <%= hidden_field_tag 'import_timestamp', @import_timestamp %>
<%= l(:label_match_select) %> <% @headers.each do |column| %> <% matched = '' diff --git a/config/locales/en.yml b/config/locales/en.yml index fc4128d..00dec1f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,11 +1,11 @@ en: label_import: "Import" label_issue_importer: "Issue Importer" - label_upload_notice: "Select a CSV file to import. Import file must have a header row." + label_upload_notice: "Select a CSV file to import. Import file must have a header row. Maximum size 4MB." label_upload_format: "File format settings" label_upload_encoding: "Encoding:" - label_upload_splitter: "Field seperate char:" - label_upload_wrapper: "Field wrap char:" + label_upload_splitter: "Field separator character:" + label_upload_wrapper: "Field quoting character:" label_load_rules: "Load saved rules" label_toplines: "Refer to top lines of {{value}}:" diff --git a/config/locales/ja.yml b/config/locales/ja.yml new file mode 100644 index 0000000..b47f3d9 --- /dev/null +++ b/config/locales/ja.yml @@ -0,0 +1,34 @@ +ja: + label_import: "インポート" + label_issue_importer: "チケットのインポート" + label_upload_notice: "CSVファイルを選択する。ヘーダー行が要る。4MBのサイズの限定がある。" + label_upload_format: "ファイルフォーマットのオプション" + label_upload_encoding: "エンコーディング:" + label_upload_splitter: "フィールド区切り文字:" + label_upload_wrapper: "引用文字:" + + label_load_rules: "保存されたルールを読み込む" + label_toplines: "{{value}}の上の行を参考するÔºö" + label_match_columns: "マッチされたコラム:" + label_match_select: "マッチするフィールドを選択:" + label_import_rule: "インポートのルール" + label_default_tracker: "トラッカーのディフォルト:" + label_update_issue: "存在があるチケットを更新" + label_journal_field: "メモとして入るフィールドを選択:" + label_unique_field: "チケットをマッチするフィールドを選択:" + label_update_other_project: "他のプロジェクトのチケットでも更新" + label_ignore_non_exist: "チケットの存在がなくても無視" + label_rule_name: "ルール舞を入れる:" + + label_import_result: "インポートの結果" + label_result_notice: "{{handle_count}}行整理された。{{success_count}}行がうまく行った。" + label_result_projects: "関われるプロジェクト:" + label_result_issues: "{{count}}行" + label_result_failed: "{{count}}行が失敗:" + + option_ignore: "無視" + + button_upload: "ファイルをアップ" + button_submit: "確認" + button_save_rules_and_submit: "マッチルールを保存して確認" + diff --git a/db/migrate/001_create_import_in_progresses.rb b/db/migrate/001_create_import_in_progresses.rb new file mode 100644 index 0000000..81fbacc --- /dev/null +++ b/db/migrate/001_create_import_in_progresses.rb @@ -0,0 +1,16 @@ +class CreateImportInProgresses < ActiveRecord::Migration + def self.up + create_table :import_in_progresses do |t| + t.column :user_id, :integer, :null => false + t.string :quote_char, :limit => 8 + t.string :col_sep, :limit => 8 + t.string :encoding, :limit => 64 + t.column :created, :datetime + t.column :csv_data, :binary, :limit => 4096*1024 + end + end + + def self.down + drop_table :import_in_progresses + end +end diff --git a/init.rb b/init.rb index 0188919..4228f86 100644 --- a/init.rb +++ b/init.rb @@ -4,7 +4,7 @@ name 'Issue Importer' author 'Martin Liu' description 'Issue import plugin for Redmine.' - version '0.4' + version '0.7' project_module :importer do permission :import, :importer => :index diff --git a/test/fixtures/import_in_progresses.yml b/test/fixtures/import_in_progresses.yml new file mode 100644 index 0000000..0d53737 --- /dev/null +++ b/test/fixtures/import_in_progresses.yml @@ -0,0 +1,11 @@ +# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html +one: + id: 1 + csv_data: + user: + created: 2010-10-09 20:40:03 +two: + id: 2 + csv_data: + user: + created: 2010-10-09 20:40:03 diff --git a/test/unit/import_in_progress_test.rb b/test/unit/import_in_progress_test.rb new file mode 100644 index 0000000..613ef6a --- /dev/null +++ b/test/unit/import_in_progress_test.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../test_helper' + +class ImportInProgressTest < ActiveSupport::TestCase + fixtures :import_in_progresses + + # Replace this with your real tests. + def test_truth + assert true + end +end From 003d917fd4e9044190e2283ca4368be32899d7fa Mon Sep 17 00:00:00 2001 From: Leo Hourvitz Date: Fri, 15 Oct 2010 14:22:57 +0900 Subject: [PATCH 06/36] Updated README --- README.rdoc | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.rdoc b/README.rdoc index f0b5733..6fe8388 100644 --- a/README.rdoc +++ b/README.rdoc @@ -2,9 +2,13 @@ This plugin seems to be functional now, including in multiprocess environments. -I got rid of the use of instance variables to pass -data from the match stge to the result stage. -At this point, it's using the session support -to pass the data, so extremely large import files -could use a bit of extra memory on the server. +The database is used for intermediate storage. +To install: +- Download the plugin to your vendors/plugins directory +- Run 'rake db:migrate_plugins' +- Restart your redmine as appropriate +- Go to the Admin/Projects/../Modules +- Enable "Importer" + +en, zh, and ja localizations included. From fe7485dc444e561d2e57237a56896c59f0c943f6 Mon Sep 17 00:00:00 2001 From: Fahad Sadah Date: Sun, 3 Oct 2010 00:20:08 +0800 Subject: [PATCH 07/36] Added zh: at the top, to fix Rails errors --- config/locales/zh.yml | 66 +++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/config/locales/zh.yml b/config/locales/zh.yml index dcd6ef3..28c5c8e 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1,33 +1,33 @@ - -label_import: "导入" -label_issue_importer: "问题列表导入工具" -label_upload_notice: "请选择需要上传的问题列表CSV文件。文件必须具有标题行。" -label_upload_format: "文件格式设置" -label_upload_encoding: "编码:" -label_upload_splitter: "字段分隔符:" -label_upload_wrapper: "字段包裹符:" - -label_load_rules: "载入已保存的匹配规则" -label_toplines: "参考文件 %s 的头几行:" -label_match_columns: "字段配对" -label_match_select: "选择对应的字段" -label_import_rule: "导入规则" -label_default_tracker: "默认跟踪:" -label_update_issue: "更新已存在的问题" -label_journal_field: "选择用于日志的字段:" -label_unique_field: "选择用于标识问题的唯一字段:" -label_update_other_project: "允许更新其他项目的问题" -label_ignore_non_exist: "忽略不存在的问题" -label_rule_name: "输入规则名称" - -label_import_result: "导入结果" -label_result_notice: "处理了%d个问题。%d个问题被成功导入。" -label_result_projects: "受影响的项目:" -label_result_issues: "%d个问题" -label_result_failed: "%d条失败的行:" - -option_ignore: "忽略" - -button_upload: "上传文件" -button_submit: "提交" -button_save_rules_and_submit: "存储匹配规则后提交" +zh: + label_import: "导入" + label_issue_importer: "问题列表导入工具" + label_upload_notice: "请选择需要上传的问题列表CSV文件。文件必须具有标题行。" + label_upload_format: "文件格式设置" + label_upload_encoding: "编码:" + label_upload_splitter: "字段分隔符:" + label_upload_wrapper: "字段包裹符:" + + label_load_rules: "载入已保存的匹配规则" + label_toplines: "参考文件 %s 的头几行:" + label_match_columns: "字段配对" + label_match_select: "选择对应的字段" + label_import_rule: "导入规则" + label_default_tracker: "默认跟踪:" + label_update_issue: "更新已存在的问题" + label_journal_field: "选择用于日志的字段:" + label_unique_field: "选择用于标识问题的唯一字段:" + label_update_other_project: "允许更新其他项目的问题" + label_ignore_non_exist: "忽略不存在的问题" + label_rule_name: "输入规则名称" + + label_import_result: "导入结果" + label_result_notice: "处理了%d个问题。%d个问题被成功导入。" + label_result_projects: "受影响的项目:" + label_result_issues: "%d个问题" + label_result_failed: "%d条失败的行:" + + option_ignore: "忽略" + + button_upload: "上传文件" + button_submit: "提交" + button_save_rules_and_submit: "存储匹配规则后提交" From b2106a4fa3c3ac8a34938c663050e43aa4825cd8 Mon Sep 17 00:00:00 2001 From: Leo Hourvitz Date: Wed, 30 Mar 2011 20:30:03 +0900 Subject: [PATCH 08/36] Fixed two problems with users: - If no "Author" field is provided, the author will now be blank (in some cases, it would previously be the user "Anonymous") - If the Assigned To field contains an invalid value (a non-existant user id), it will now be blank (instead of Anonymous) --- app/controllers/importer_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/importer_controller.rb b/app/controllers/importer_controller.rb index 32dc32d..bc990b0 100644 --- a/app/controllers/importer_controller.rb +++ b/app/controllers/importer_controller.rb @@ -108,10 +108,10 @@ def result project = Project.find_by_name(row[attrs_map["project"]]) tracker = Tracker.find_by_name(row[attrs_map["tracker"]]) status = IssueStatus.find_by_name(row[attrs_map["status"]]) - author = User.find_by_login(row[attrs_map["author"]]) + author = attrs_map["author"] ? User.find_by_login(row[attrs_map["author"]]) : User.current priority = Enumeration.find_by_name(row[attrs_map["priority"]]) category = IssueCategory.find_by_name(row[attrs_map["category"]]) - assigned_to = User.find_by_login(row[attrs_map["assigned_to"]]) + assigned_to = row[attrs_map["assigned_to"]] != nil ? User.find_by_login(row[attrs_map["assigned_to"]]) : nil fixed_version = Version.find_by_name(row[attrs_map["fixed_version"]]) # new issue or find exists one issue = Issue.new From bbecb108c6db3f4820db2983a85f5ab71698e656 Mon Sep 17 00:00:00 2001 From: Leo Hourvitz Date: Wed, 30 Mar 2011 21:46:31 +0900 Subject: [PATCH 09/36] Added a new capability to import parent tasks. If you provide a column that you map to "Parent Task", you can import parent tasks. There are several restrictions: - If you want to import parent tasks, you must set the "Select unique field for identify issue" value in the upload. - Obviously, that field must be unqiue. - Finally, and most importantly, parent tasks must come before child tasks in the list. Although this is kind of a pain, it makes this a one-pass rather than two-pass feature. This has not yet been tested with using the ID as the unique column. Also, right now the "Select unique field for identify issue" option is disabled by default; the second half of this update will update the UI as well. --- app/controllers/importer_controller.rb | 47 ++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/app/controllers/importer_controller.rb b/app/controllers/importer_controller.rb index bc990b0..5724013 100644 --- a/app/controllers/importer_controller.rb +++ b/app/controllers/importer_controller.rb @@ -8,7 +8,8 @@ class ImporterController < ApplicationController ISSUE_ATTRS = [:id, :subject, :assigned_to, :fixed_version, :author, :description, :category, :priority, :tracker, :status, - :start_date, :due_date, :done_ratio, :estimated_hours] + :start_date, :due_date, :done_ratio, :estimated_hours, + :parent_issue] def index end @@ -93,15 +94,16 @@ def result ignore_non_exist = params[:ignore_non_exist] fields_map = params[:fields_map] unique_attr = fields_map[unique_field] + + # attrs_map is fields_map's invert + attrs_map = fields_map.invert + # check params - if update_issue && unique_attr == nil - flash[:error] = "Unique field hasn't match an issue's field" + if (update_issue || attrs_map["parent_issue"] != nil) && unique_attr == nil + flash[:error] = "Unique field doesn't match an issue's field" return end - - # attrs_map is fields_map's invert - attrs_map = fields_map.invert FasterCSV.new(iip.csv_data, {:headers=>true, :encoding=>iip.encoding, :quote_char=>iip.quote_char, :col_sep=>iip.col_sep}).each do |row| @@ -203,6 +205,39 @@ def result issue.done_ratio = row[attrs_map["done_ratio"]] || issue.done_ratio issue.estimated_hours = row[attrs_map["estimated_hours"]] || issue.estimated_hours + # parent issue + if row[attrs_map["parent_issue"]] != nil + if unique_attr == "id" + parent_issues = [Issue.find_by_id(row[unique_field])] + else + query = Query.new(:name => "_importer", :project => @project) + query.add_filter("status_id", "*", [1]) + query.add_filter(unique_attr, "=", [row[attrs_map["parent_issue"]]]) + logger.info("Querying for parent issue with #{unique_attr} = '#{row[unique_field]}") + + parent_issues = Issue.find :all, :conditions => query.statement, :limit => 2, :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ] + end + + if parent_issues.size > 1 + flash[:warning] = "Unique field #{unique_field} has duplicate record" + @failed_count += 1 + @failed_issues[@handle_count + 1] = row + break + else + if parent_issues.size > 0 + # found issue + issue.parent_issue_id = parent_issues.first.id + + else + # ignore none exist issues + if ignore_non_exist + @skip_count += 1 + next + end + end + end + end + # custom fields issue.custom_field_values = issue.available_custom_fields.inject({}) do |h, c| if value = row[attrs_map[c.name]] From 211c568de22416eeebffd9e532112f1e736f317c Mon Sep 17 00:00:00 2001 From: Leo Hourvitz Date: Sat, 2 Apr 2011 20:12:54 +0900 Subject: [PATCH 10/36] Added an upload sample sheet and a screenshot of the needed UI. --- test/samples/ParentTaskBySubject.csv | 3 +++ test/samples/ParentUploadSample.png | Bin 0 -> 43236 bytes 2 files changed, 3 insertions(+) create mode 100644 test/samples/ParentTaskBySubject.csv create mode 100644 test/samples/ParentUploadSample.png diff --git a/test/samples/ParentTaskBySubject.csv b/test/samples/ParentTaskBySubject.csv new file mode 100644 index 0000000..bd16063 --- /dev/null +++ b/test/samples/ParentTaskBySubject.csv @@ -0,0 +1,3 @@ +"Subject","Description","Parent task" +"Task #1","This is a parent task.", +"Task #1-1","This is a child task.","Task #1" diff --git a/test/samples/ParentUploadSample.png b/test/samples/ParentUploadSample.png new file mode 100644 index 0000000000000000000000000000000000000000..40072a574a1a8be73983fab3c08b1c1ff4167bb7 GIT binary patch literal 43236 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 Date: Sun, 3 Apr 2011 00:29:04 +0900 Subject: [PATCH 11/36] Fixed importer to work with custom fields as the unique column. Added lots more import samples. Added reference to the documentation in progress. Fixed the interface to correspond to the new features. Did some preparatory work towards supporting importing issue relationships. --- README.rdoc | 2 + app/controllers/importer_controller.rb | 155 ++++++++++++----------- app/views/importer/match.html.erb | 7 +- config/locales/en.yml | 12 +- test/samples/AllStandardFields.csv | 2 + test/samples/CustomField.csv | 3 + test/samples/CustomFieldUpdate.csv | 3 + test/samples/ParentTaskByCustomField.csv | 4 + 8 files changed, 103 insertions(+), 85 deletions(-) create mode 100644 test/samples/AllStandardFields.csv create mode 100644 test/samples/CustomField.csv create mode 100644 test/samples/CustomFieldUpdate.csv create mode 100644 test/samples/ParentTaskByCustomField.csv diff --git a/README.rdoc b/README.rdoc index 6fe8388..1309cdf 100644 --- a/README.rdoc +++ b/README.rdoc @@ -12,3 +12,5 @@ To install: en, zh, and ja localizations included. +User documentation at https://github.com/leovitch/redmine_importer/wiki. + diff --git a/app/controllers/importer_controller.rb b/app/controllers/importer_controller.rb index 5724013..4524682 100644 --- a/app/controllers/importer_controller.rb +++ b/app/controllers/importer_controller.rb @@ -1,6 +1,12 @@ require 'fastercsv' require 'tempfile' +class MultipleIssuesForUniqueValue < Exception +end + +class NoIssueForUniqueValue < Exception +end + class ImporterController < ApplicationController unloadable @@ -62,6 +68,35 @@ def match end @attrs.sort! end + + def issue_for_unique_attr(unique_attr, attr_value) + if unique_attr == "id" + issues = [Issue.find_by_id(attr_value)] + else + query = Query.new(:name => "_importer", :project => @project) + query.add_filter("status_id", "*", [1]) + query.add_filter(unique_attr, "=", [attr_value]) + logger.info("Querying for issue with #{unique_attr} = '#{attr_value}'") + + issues = Issue.find :all, :conditions => query.statement, :limit => 2, :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ] + logger.info("condition for parent query is #{query.statement}") + end + + if issues.size > 1 + issues.each do |pi| + logger.info("Found parent #{pi}") + end + flash[:warning] = "Unique field #{unique_attr} with value '#{attr_value}' has duplicate record" + @failed_count += 1 + @failed_issues[@handle_count + 1] = row + raise MultipleIssuesForUniqueValue, "Unique field #{unique_attr} with value '#{attr_value}' has duplicate record" + else + if issues.size == 0 + raise NoIssueForUniqueValue, "No issue with #{unique_attr} of '#{attr_value}' found" + end + issues.first + end + end def result @handle_count = 0 @@ -94,6 +129,7 @@ def result ignore_non_exist = params[:ignore_non_exist] fields_map = params[:fields_map] unique_attr = fields_map[unique_field] + unique_attr_checked = false # Used to optimize some work that has to happen inside the loop # attrs_map is fields_map's invert attrs_map = fields_map.invert @@ -122,64 +158,52 @@ def result issue.tracker_id = tracker != nil ? tracker.id : default_tracker issue.author_id = author != nil ? author.id : User.current.id - if update_issue - # custom field + # reprocess unique_attr if it's a custom field -- only on the first issue + if !unique_attr_checked if !ISSUE_ATTRS.include?(unique_attr.to_sym) issue.available_custom_fields.each do |cf| if cf.name == unique_attr unique_attr = "cf_#{cf.id}" break end - end + end end - - if unique_attr == "id" - issues = [Issue.find_by_id(row[unique_field])] - else - query = Query.new(:name => "_importer", :project => @project) - query.add_filter("status_id", "*", [1]) - query.add_filter(unique_attr, "=", [row[unique_field]]) + unique_attr_checked = true + end - issues = Issue.find :all, :conditions => query.statement, :limit => 2, :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ] - end - - if issues.size > 1 - flash[:warning] = "Unique field #{unique_field} has duplicate record" - @failed_count += 1 - @failed_issues[@handle_count + 1] = row - break - else - if issues.size > 0 - # found issue - issue = issues.first - - # ignore other project's issue or not - if issue.project_id != @project.id && !update_other_project - @skip_count += 1 - next - end - - # ignore closed issue except reopen - if issue.status.is_closed? - if status == nil || status.is_closed? - @skip_count += 1 - next - end - end - - # init journal - note = row[journal_field] || '' - journal = issue.init_journal(author || User.current, - note || '') - - @update_count += 1 - else - # ignore none exist issues - if ignore_non_exist + if update_issue + begin + issue = issue_for_unique_attr(unique_attr,row[unique_field]) + + # ignore other project's issue or not + if issue.project_id != @project.id && !update_other_project + @skip_count += 1 + next + end + + # ignore closed issue except reopen + if issue.status.is_closed? + if status == nil || status.is_closed? @skip_count += 1 next end end + + # init journal + note = row[journal_field] || '' + journal = issue.init_journal(author || User.current, + note || '') + + @update_count += 1 + + rescue NoIssueForUniqueValue + if ignore_non_exist + @skip_count += 1 + next + end + + rescue MultipleIssuesForUniqueValue + break end end @@ -205,37 +229,18 @@ def result issue.done_ratio = row[attrs_map["done_ratio"]] || issue.done_ratio issue.estimated_hours = row[attrs_map["estimated_hours"]] || issue.estimated_hours - # parent issue - if row[attrs_map["parent_issue"]] != nil - if unique_attr == "id" - parent_issues = [Issue.find_by_id(row[unique_field])] - else - query = Query.new(:name => "_importer", :project => @project) - query.add_filter("status_id", "*", [1]) - query.add_filter(unique_attr, "=", [row[attrs_map["parent_issue"]]]) - logger.info("Querying for parent issue with #{unique_attr} = '#{row[unique_field]}") - - parent_issues = Issue.find :all, :conditions => query.statement, :limit => 2, :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ] + # issue relations & parent issues + begin + if row[attrs_map["parent_issue"]] != nil + issue.parent_issue_id = issue_for_unique_attr(unique_attr,row[attrs_map["parent_issue"]]).id end - - if parent_issues.size > 1 - flash[:warning] = "Unique field #{unique_field} has duplicate record" - @failed_count += 1 - @failed_issues[@handle_count + 1] = row - break - else - if parent_issues.size > 0 - # found issue - issue.parent_issue_id = parent_issues.first.id - - else - # ignore none exist issues - if ignore_non_exist - @skip_count += 1 - next - end - end + rescue NoIssueForUniqueValue + if ignore_non_exist + @skip_count += 1 + next end + rescue MultipleIssuesForUniqueValue + break end # custom fields diff --git a/app/views/importer/match.html.erb b/app/views/importer/match.html.erb index a7b30c8..75496af 100644 --- a/app/views/importer/match.html.erb +++ b/app/views/importer/match.html.erb @@ -28,10 +28,12 @@
+
+
<%= observe_field("update_issue", :function => < -     
-     
diff --git a/config/locales/en.yml b/config/locales/en.yml index 00dec1f..693a7f0 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -10,14 +10,14 @@ en: label_load_rules: "Load saved rules" label_toplines: "Refer to top lines of {{value}}:" label_match_columns: "Matching Columns" - label_match_select: "Select match field" + label_match_select: "Select field to set from each column:" label_import_rule: "Import rules" label_default_tracker: "Default tracker:" - label_update_issue: "Update exists issue" - label_journal_field: "Select field as journal:" - label_unique_field: "Select unique field for identify issue:" - label_update_other_project: "Allow update issues of other projects" - label_ignore_non_exist: "Ignore none exist issues" + label_update_issue: "Update existing issues" + label_journal_field: "Select column to use as note:" + label_unique_field: "Select unique-valued column for updating or importing relations:" + label_update_other_project: "Allow updating issues from other projects" + label_ignore_non_exist: "Ignore non-existant issues" label_rule_name: "Input rule name" label_import_result: "Import Result" diff --git a/test/samples/AllStandardFields.csv b/test/samples/AllStandardFields.csv new file mode 100644 index 0000000..586a22a --- /dev/null +++ b/test/samples/AllStandardFields.csv @@ -0,0 +1,2 @@ +"Subject","Description","Assignee","Target version","Author","Category","Priority","Tracker","Status","Start date","Due date","% done","Estimated time" +"A full task","A lengthily described set of activities.","admin","The Target Version","admin","Default","High","Bug","In Progress",2011-05-01,2011-08-28,25,200 diff --git a/test/samples/CustomField.csv b/test/samples/CustomField.csv new file mode 100644 index 0000000..af87a37 --- /dev/null +++ b/test/samples/CustomField.csv @@ -0,0 +1,3 @@ +"Subject","Description","External id" +"Important Task","A truly critical deed.","5643-4" +"Less Important Task","Something that would be useful.","5644-6" diff --git a/test/samples/CustomFieldUpdate.csv b/test/samples/CustomFieldUpdate.csv new file mode 100644 index 0000000..52c4f71 --- /dev/null +++ b/test/samples/CustomFieldUpdate.csv @@ -0,0 +1,3 @@ +"Subject","Description","External id" +"Important Task","A truly critical deed.","5643-4" +"Less Important Task","Altering this task to make it even more useful.","5644-6" diff --git a/test/samples/ParentTaskByCustomField.csv b/test/samples/ParentTaskByCustomField.csv new file mode 100644 index 0000000..b893128 --- /dev/null +++ b/test/samples/ParentTaskByCustomField.csv @@ -0,0 +1,4 @@ +"Subject","Description","External id","Parent task" +"The overall task","A truly critical deed.","7643-4", +"The first sub-task","Something that would be useful.","7644-6","7643-4" +"The second sub-task","A really important component","7644-7","7643-4" From 1ffceca72c9f9720cfb16710d7260ce22e14eecb Mon Sep 17 00:00:00 2001 From: Leo Hourvitz Date: Sun, 3 Apr 2011 13:42:07 +0900 Subject: [PATCH 12/36] Added limited ability to import issue relationships (only one relationship of a particular type can be imported in one import run). Optimized the general performance of relationship import by eliminating queries in the simple case. This also makes the uniqueness constraint much weaker for the simple use case where the target issues are higher in the same CSV file. Improved both the English and Japanese strings for the new functions. --- app/controllers/importer_controller.rb | 63 +++++++++++++++++----- app/views/importer/match.html.erb | 3 +- config/locales/en.yml | 2 +- config/locales/ja.yml | 10 ++-- test/samples/IssueRelationsCustomField.csv | 10 ++++ 5 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 test/samples/IssueRelationsCustomField.csv diff --git a/app/controllers/importer_controller.rb b/app/controllers/importer_controller.rb index 4524682..5b47257 100644 --- a/app/controllers/importer_controller.rb +++ b/app/controllers/importer_controller.rb @@ -22,7 +22,6 @@ def index def match # Delete existing iip to ensure there can't be two iips for a user - print "params are ", params ImportInProgress.delete_all(["user_id = ?",User.current.id]) # save import-in-progress data iip = ImportInProgress.find_or_create_by_user_id(User.current.id) @@ -66,26 +65,29 @@ def match @project.all_issue_custom_fields.each do |cfield| @attrs.push([cfield.name, cfield.name]) end + IssueRelation::TYPES.each_pair do |rtype, rinfo| + @attrs.push([l_or_humanize(rinfo[:name]),rtype]) + end @attrs.sort! end + # Returns the issue object associated with the given value of the given attribute. + # Raises NoIssueForUniqueValue if not found or MultipleIssuesForUniqueValue def issue_for_unique_attr(unique_attr, attr_value) + if @issue_by_unique_attr.has_key?(attr_value) + return @issue_by_unique_attr[attr_value] + end if unique_attr == "id" issues = [Issue.find_by_id(attr_value)] else query = Query.new(:name => "_importer", :project => @project) query.add_filter("status_id", "*", [1]) query.add_filter(unique_attr, "=", [attr_value]) - logger.info("Querying for issue with #{unique_attr} = '#{attr_value}'") issues = Issue.find :all, :conditions => query.statement, :limit => 2, :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ] - logger.info("condition for parent query is #{query.statement}") end if issues.size > 1 - issues.each do |pi| - logger.info("Found parent #{pi}") - end flash[:warning] = "Unique field #{unique_attr} with value '#{attr_value}' has duplicate record" @failed_count += 1 @failed_issues[@handle_count + 1] = row @@ -105,6 +107,9 @@ def result @failed_count = 0 @failed_issues = Hash.new @affect_projects_issues = Hash.new + # This is a cache of previously inserted issues indexed by the value + # the user provided in the unique column + @issue_by_unique_attr = Hash.new # Retrieve saved import data iip = ImportInProgress.find_by_user_id(User.current.id) @@ -112,8 +117,6 @@ def result flash[:error] = "No import is currently in progress" return end - print "iip.created is ", iip.created - print "params[:import_timestamp] is ", params[:import_timestamp] if iip.created.strftime("%Y-%m-%d %H:%M:%S") != params[:import_timestamp] flash[:error] = "You seem to have started another import " \ "since starting this one. " \ @@ -123,7 +126,7 @@ def result default_tracker = params[:default_tracker] update_issue = params[:update_issue] - unique_field = params[:unique_field] + unique_field = params[:unique_field].empty? ? nil : params[:unique_field] journal_field = params[:journal_field] update_other_project = params[:update_other_project] ignore_non_exist = params[:ignore_non_exist] @@ -135,8 +138,15 @@ def result attrs_map = fields_map.invert # check params - if (update_issue || attrs_map["parent_issue"] != nil) && unique_attr == nil - flash[:error] = "Unique field doesn't match an issue's field" + unique_required = update_issue || attrs_map["parent_issue"] != nil + IssueRelation::TYPES.each_key do |rtype| + if attrs_map[rtype] + unique_required = true + break + end + end + if unique_required && unique_attr == nil + flash[:error] = "Unique field must be specified" return end @@ -158,9 +168,9 @@ def result issue.tracker_id = tracker != nil ? tracker.id : default_tracker issue.author_id = author != nil ? author.id : User.current.id - # reprocess unique_attr if it's a custom field -- only on the first issue + # trnaslate unique_attr if it's a custom field -- only on the first issue if !unique_attr_checked - if !ISSUE_ATTRS.include?(unique_attr.to_sym) + if unique_field && !ISSUE_ATTRS.include?(unique_attr.to_sym) issue.available_custom_fields.each do |cf| if cf.name == unique_attr unique_attr = "cf_#{cf.id}" @@ -229,7 +239,7 @@ def result issue.done_ratio = row[attrs_map["done_ratio"]] || issue.done_ratio issue.estimated_hours = row[attrs_map["estimated_hours"]] || issue.estimated_hours - # issue relations & parent issues + # parent issues begin if row[attrs_map["parent_issue"]] != nil issue.parent_issue_id = issue_for_unique_attr(unique_attr,row[attrs_map["parent_issue"]]).id @@ -255,6 +265,31 @@ def result # 记录错误 @failed_count += 1 @failed_issues[@handle_count + 1] = row + else + if unique_field + @issue_by_unique_attr[row[unique_field]] = issue + end + # Issue relations + begin + IssueRelation::TYPES.each_pair do |rtype, rinfo| + if !row[attrs_map[rtype]] + next + end + other_issue = issue_for_unique_attr(unique_attr,row[attrs_map[rtype]]) + relations = issue.relations.select { |r| (r.other_issue(issue).id == other_issue.id) && (r.relation_type_for(issue) == rtype) } + if relations.length == 0 + relation = IssueRelation.new( :issue_from => issue, :issue_to => other_issue, :relation_type => rtype ) + relation.save + end + end + rescue NoIssueForUniqueValue + if ignore_non_exist + @skip_count += 1 + next + end + rescue MultipleIssuesForUniqueValue + break + end end if journal diff --git a/app/views/importer/match.html.erb b/app/views/importer/match.html.erb index 75496af..d3ec318 100644 --- a/app/views/importer/match.html.erb +++ b/app/views/importer/match.html.erb @@ -29,7 +29,8 @@ <%= select_tag "default_tracker", options_from_collection_for_select(@project.trackers, 'id', 'name') %>

+ <%= select_tag "unique_field", "" + + options_for_select(@headers) %>

diff --git a/config/locales/en.yml b/config/locales/en.yml index 693a7f0..2223a89 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -15,7 +15,7 @@ en: label_default_tracker: "Default tracker:" label_update_issue: "Update existing issues" label_journal_field: "Select column to use as note:" - label_unique_field: "Select unique-valued column for updating or importing relations:" + label_unique_field: "Select unique-valued column (required for updating existing issues or importing relations):" label_update_other_project: "Allow updating issues from other projects" label_ignore_non_exist: "Ignore non-existant issues" label_rule_name: "Input rule name" diff --git a/config/locales/ja.yml b/config/locales/ja.yml index b47f3d9..c234a16 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -8,14 +8,14 @@ ja: label_upload_wrapper: "引用文字:" label_load_rules: "保存されたルールを読み込む" - label_toplines: "{{value}}の上の行を参考するÔºö" - label_match_columns: "マッチされたコラム:" - label_match_select: "マッチするフィールドを選択:" + label_toplines: "{{value}}の上の行を参考:" + label_match_columns: "コラムの対象のフィールド" + label_match_select: "コラム毎の対象のフィールドを選択:" label_import_rule: "インポートのルール" label_default_tracker: "トラッカーのディフォルト:" - label_update_issue: "存在があるチケットを更新" + label_update_issue: "存在があるチケットを更新:" label_journal_field: "メモとして入るフィールドを選択:" - label_unique_field: "チケットをマッチするフィールドを選択:" + label_unique_field: "一意な値がある欄を選択(存在がある更新または連携をインポートする際に必須):" label_update_other_project: "他のプロジェクトのチケットでも更新" label_ignore_non_exist: "チケットの存在がなくても無視" label_rule_name: "ルール舞を入れる:" diff --git a/test/samples/IssueRelationsCustomField.csv b/test/samples/IssueRelationsCustomField.csv new file mode 100644 index 0000000..02eda53 --- /dev/null +++ b/test/samples/IssueRelationsCustomField.csv @@ -0,0 +1,10 @@ +"Subject","Description","External id","Parent task","follows" +"Run the company","What we actually have to do","T55",, +"Major new project","The top-level new task","T56","T55", +"Market Research","Find out what people what to buy","T57","T56", +"Research and Development","Design that thing","T58","T56","T57" +"Manufacturing Engineering","Figure out how to make that thing","T59","T56","T58" +"Manufacturing","Make that thing","T60","T56","T59" +"Marketing","Convince people they really want that thing","T61","T56","T58" +"Sales","Sell people that thing","T62","T56","T60" +"Investor Relations","Keep the investors happy in the meantime","T63","T55", From 82e7a11698f099871720a1817b35ce64e861772c Mon Sep 17 00:00:00 2001 From: Leo Hourvitz Date: Sun, 3 Apr 2011 14:14:46 +0900 Subject: [PATCH 13/36] Removed png file -- these belong in the wiki. --- test/samples/ParentUploadSample.png | Bin 43236 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test/samples/ParentUploadSample.png diff --git a/test/samples/ParentUploadSample.png b/test/samples/ParentUploadSample.png deleted file mode 100644 index 40072a574a1a8be73983fab3c08b1c1ff4167bb7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43236 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 Date: Tue, 5 Apr 2011 02:14:38 +0900 Subject: [PATCH 14/36] Added capability to import watchers. --- .gitignore | 1 + app/controllers/importer_controller.rb | 18 +++++++++++++++++- test/samples/AllStandardFields.csv | 4 ++-- 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..181d655 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.~lock* diff --git a/app/controllers/importer_controller.rb b/app/controllers/importer_controller.rb index 5b47257..9a4ea55 100644 --- a/app/controllers/importer_controller.rb +++ b/app/controllers/importer_controller.rb @@ -15,7 +15,7 @@ class ImporterController < ApplicationController ISSUE_ATTRS = [:id, :subject, :assigned_to, :fixed_version, :author, :description, :category, :priority, :tracker, :status, :start_date, :due_date, :done_ratio, :estimated_hours, - :parent_issue] + :parent_issue, :watchers ] def index end @@ -161,6 +161,7 @@ def result category = IssueCategory.find_by_name(row[attrs_map["category"]]) assigned_to = row[attrs_map["assigned_to"]] != nil ? User.find_by_login(row[attrs_map["assigned_to"]]) : nil fixed_version = Version.find_by_name(row[attrs_map["fixed_version"]]) + watchers = row[attrs_map["watchers"]] # new issue or find exists one issue = Issue.new journal = nil @@ -260,6 +261,20 @@ def result end h end + + # watchers + if watchers + addable_watcher_users = issue.addable_watcher_users + watchers.split(',').each do |watcher| + watcher_user = User.find_by_login(watcher) + if (!watcher_user) || (issue.watcher_users.include?(watcher_user)) + next + end + if addable_watcher_users.include?(watcher_user) + issue.add_watcher(watcher_user) + end + end + end if (!issue.save) # 记录错误 @@ -269,6 +284,7 @@ def result if unique_field @issue_by_unique_attr[row[unique_field]] = issue end + # Issue relations begin IssueRelation::TYPES.each_pair do |rtype, rinfo| diff --git a/test/samples/AllStandardFields.csv b/test/samples/AllStandardFields.csv index 586a22a..f77a2f1 100644 --- a/test/samples/AllStandardFields.csv +++ b/test/samples/AllStandardFields.csv @@ -1,2 +1,2 @@ -"Subject","Description","Assignee","Target version","Author","Category","Priority","Tracker","Status","Start date","Due date","% done","Estimated time" -"A full task","A lengthily described set of activities.","admin","The Target Version","admin","Default","High","Bug","In Progress",2011-05-01,2011-08-28,25,200 +"Subject","Description","Assignee","Target version","Author","Category","Priority","Tracker","Status","Start date","Due date","% done","Estimated time","Watchers" +"A full task","A lengthily described set of activities.","admin","The Target Version","admin","Default","High","Bug","In Progress",05/01/11,08/28/11,25,200,"test1,test2" From 7fa9ad20c3c88e98441a4e54adf10ec20fc0892f Mon Sep 17 00:00:00 2001 From: Leo Hourvitz Date: Thu, 7 Apr 2011 01:14:19 +0900 Subject: [PATCH 15/36] Added ability to auto-add categories and target versions. Added UI and labels for controlling these options. i --- app/controllers/importer_controller.rb | 19 +++++++++++++++++-- app/views/importer/match.html.erb | 4 +++- config/locales/en.yml | 2 ++ config/locales/ja.yml | 2 ++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/app/controllers/importer_controller.rb b/app/controllers/importer_controller.rb index 9a4ea55..0c494dc 100644 --- a/app/controllers/importer_controller.rb +++ b/app/controllers/importer_controller.rb @@ -131,6 +131,8 @@ def result update_other_project = params[:update_other_project] ignore_non_exist = params[:ignore_non_exist] fields_map = params[:fields_map] + add_categories = params[:add_categories] + add_versions = params[:add_versions] unique_attr = fields_map[unique_field] unique_attr_checked = false # Used to optimize some work that has to happen inside the loop @@ -154,13 +156,26 @@ def result :quote_char=>iip.quote_char, :col_sep=>iip.col_sep}).each do |row| project = Project.find_by_name(row[attrs_map["project"]]) + if !project + project = @project + end tracker = Tracker.find_by_name(row[attrs_map["tracker"]]) status = IssueStatus.find_by_name(row[attrs_map["status"]]) author = attrs_map["author"] ? User.find_by_login(row[attrs_map["author"]]) : User.current priority = Enumeration.find_by_name(row[attrs_map["priority"]]) - category = IssueCategory.find_by_name(row[attrs_map["category"]]) + category_name = row[attrs_map["category"]] + category = IssueCategory.find_by_name(category_name) + if (!category) && category_name && category_name.length > 0 && add_categories + category = project.issue_categories.build(:name => category_name) + category.save + end assigned_to = row[attrs_map["assigned_to"]] != nil ? User.find_by_login(row[attrs_map["assigned_to"]]) : nil - fixed_version = Version.find_by_name(row[attrs_map["fixed_version"]]) + fixed_version_name = row[attrs_map["fixed_version"]] + fixed_version = Version.find_by_name(fixed_version_name) + if (!fixed_version) && fixed_version_name && fixed_version_name.length > 0 && add_versions + fixed_version = project.versions.build(:name=>fixed_version_name) + fixed_version.save + end watchers = row[attrs_map["watchers"]] # new issue or find exists one issue = Issue.new diff --git a/app/views/importer/match.html.erb b/app/views/importer/match.html.erb index d3ec318..cb32a0f 100644 --- a/app/views/importer/match.html.erb +++ b/app/views/importer/match.html.erb @@ -31,7 +31,9 @@
- + +
+

<%= observe_field("update_issue", :function => < Date: Tue, 12 Apr 2011 23:06:36 +0900 Subject: [PATCH 16/36] Added support for sending emails normally. Can be turned off with the new checkbox, but it defaults to the normal emails. --- app/controllers/importer_controller.rb | 13 +++++++++++++ app/views/importer/match.html.erb | 1 + config/locales/en.yml | 1 + config/locales/ja.yml | 1 + 4 files changed, 16 insertions(+) diff --git a/app/controllers/importer_controller.rb b/app/controllers/importer_controller.rb index 0c494dc..8cd1331 100644 --- a/app/controllers/importer_controller.rb +++ b/app/controllers/importer_controller.rb @@ -131,6 +131,7 @@ def result update_other_project = params[:update_other_project] ignore_non_exist = params[:ignore_non_exist] fields_map = params[:fields_map] + send_emails = params[:send_emails] add_categories = params[:add_categories] add_versions = params[:add_versions] unique_attr = fields_map[unique_field] @@ -299,6 +300,18 @@ def result if unique_field @issue_by_unique_attr[row[unique_field]] = issue end + + if send_emails + if update_issue + if Setting.notified_events.include?('issue_updated') + Mailer.deliver_issue_edit(issue.current_journal) + end + else + if Setting.notified_events.include?('issue_added') + Mailer.deliver_issue_add(issue) + end + end + end # Issue relations begin diff --git a/app/views/importer/match.html.erb b/app/views/importer/match.html.erb index cb32a0f..b1e4ccc 100644 --- a/app/views/importer/match.html.erb +++ b/app/views/importer/match.html.erb @@ -32,6 +32,7 @@ <%= select_tag "unique_field", "" + options_for_select(@headers) %>
+