Redmineでチケットの一括インポート

ここのページにあるやつをカスタマイズした。
RedmineでチケットをCSVから一括登録/更新するプラグイン - エンジニアブログ - スカイアーク
といっても85%くらいは作り直したけど。オリジナルと比べて改善させたのは以下の点。

  • カスタムフィールドにも(できる限り)柔軟に対応できる
  • 列の順番を動的に指定できる
  • 登録された結果がきちんと見れる
  • 管理者権限を持ったユーザーじゃないと使えない

なんとか公開できないものかとオンラインストレージを調べてみたけど「これ」ってものが見つからない。(はてながこういうサービスすれば便利なんだけど)
ということで色々調べてたらそもそも、

これらの著作物の二次著作物を再配布することはできません。

http://www.skyarc.co.jp/engineerblog/entry/2635.html

とあるので、まずいのかも。どうしたものか。

追記:
時間ができたらExcelからでも読めるようにして、プラグインの構築からやって独自のものとして公開するかもしれない。オリジナルからかなり変えているので問題はない…と思う。

追記2:2009年5月29日
ざっくりと書いてみます。そのままでは動かないかもしれませんが、参考になれば幸甚です。

管理者権限を持ったユーザーじゃないと使えないようにする

これはinit.rbをいじるだけです。メニューにリンクを追加する記述に:if => Proc.new {User.current.admin?}を加えます。

menu :application_menu, :importer, { :controller => 'importer', :action => 'index' }, :caption => 'importer', :if => Proc.new {User.current.admin?}

列の順番を動的に指定できるようにしてカスタムフィールドにもできる限り柔軟に対応する

ちょっと泥くさいですが、以下を参考にImporterController#executeを変更します。

def to_utf8(src)
  return src unless src
  Iconv.conv('UTF-8', l(:general_csv_encoding), src)
end

def execute
  # Current user
  @user = User.current

  # uploaded file 
  @file = params[:file]

  # Create issues
  @status_msg = ''
  @datas = CSV::IOReader.parse(@file)
  
  @row = @datas.shift
  columns = []
  @row.each do |r|
    header = to_utf8(r)
    case header
    when l(:field_status)
      columns << :status
    when l(:field_project)
      columns << :project
    when l(:field_tracker)
      columns << :tracker
    when l(:field_priority)
      columns << :priority
    when l(:field_subject)
      columns << :subject
    when l(:field_assigned_to)
      columns << :assigned_to
    when l(:field_category)
      columns << :category
    when l(:field_fixed_version)
      columns << :fixed_version
    when l(:field_author)
      columns << :author
    when l(:field_start_date)
      columns << :start_date
    when l(:field_due_date)
      columns << :due_date
    when l(:field_done_ratio)
      columns << :done_ratio
    when l(:field_estimated_hours)
      columns << :estimated_hours
    when l(:field_description)
      columns << :description
    else
      if IssueCustomField.find_by_name(header)
        columns << header
      else
        #error when not found in custom_fields
        @status_msg << "Unknown column : #{header}, #{l(:general_csv_encoding)}, #{Kconv.guess(r)}"
        return
      end
    end
  end
  
  @issues = Array.new
  @line_count = 0
  @datas.each_with_index do |data, index|
    issue = Issue.new
    @line_count += 1

    if data[data.size - 1] == nil || data[data.size - 1].empty?
        @status_msg << "Line #{index} : error. description required."
        next
    end
    
    status = project = tracker = priority = subject = assigned_to = nil
    category = fixed_version = author = start_date = due_date = done_ratio = nil
    estimated_hours = description = nil
    custom_fields = {}
    
    data.each_with_index do |d,i|
      case columns[i]
      when :status
        status = IssueStatus.find_by_name(to_utf8(d))
      when :project
        project = Project.find_by_name(to_utf8(d))
      when :tracker
        tracker = Tracker.find_by_name(to_utf8(d))
      when :priority
        priority = Enumeration.find_by_name(to_utf8(d))
      when :subject
        subject = to_utf8(d)
      when :assigned_to
        assigned_to = User.find_by_login(to_utf8(d))
      when :category
        category = IssueCategory.find_by_name(to_utf8(d))
      when :fixed_version
        fixed_version = Version.find_by_name(to_utf8(d))
      when :author
        author = User.find_by_login(to_utf8(d))
      when :start_date
        start_date = d
      when :due_date
        due_date = d
      when :done_ratio
        done_ratio = d
      when :estimated_hours
        estimated_hours = d
      when :description
        description = to_utf8(d)
      else
        f = IssueCustomField.find_by_name(columns[i])
        case f.field_format
        when 'user_list'
          custom_fields[f.id] = User.find_by_login(to_utf8(d)).login
        when 'string'
          custom_fields[f.id] = to_utf8(d)
        else
          custom_fields[f.id] = d
        end
      end
    end
    
    unless issue or project or tracker or status
      @status_msg << "error"
      next
    end
    
    issue.status_id = status.id
    issue.project_id = project.id
    issue.tracker_id = tracker.id
    issue.priority_id = priority ? priority.id : ''
    issue.subject = subject
    issue.assigned_to_id = assigned_to ? assigned_to.id : ''
    issue.category_id = category ? category.id : ''
    issue.fixed_version_id = fixed_version ? fixed_version.id : ''
    issue.author_id = author ? author.id : nil
    issue.start_date = start_date
    issue.due_date = due_date
    issue.done_ratio = done_ratio
    issue.estimated_hours = estimated_hours
    issue.description = description
    issue.custom_field_values = custom_fields
    if issue.save
      @issues << issue
    else
      @status_msg << "Line #{index} : error"
    end
  end
end

登録された結果をきちんと見れるようにする

execute.html.erbに次の行を追加します。ImporterController#execute内の処理で@issuesに作成したチケットが入ってることが必要です。

<table>
  <thead><tr>
    <th>#</th>
    <th><%= l(:field_status) %></th>
    <th><%= l(:field_project) %></th>
    <th><%= l(:field_tracker) %></th>
    <th><%= l(:field_priority) %></th>
    <th><%= l(:field_subject) %></th>
    <th><%= l(:field_assigned_to) %></th>
    <th><%= l(:field_category) %></th>
    <th><%= l(:field_fixed_version) %></th>
    <th><%= l(:field_author) %></th>
    <th><%= l(:field_start_date) %></th>
    <th><%= l(:field_due_date) %></th>
    <th><%= l(:field_done_ratio) %></th>
    <th><%= l(:field_estimated_hours) %></th>
    <th><%= l(:field_description) %></th>
    <% @issues && @issues.first && @issues.first.available_custom_fields.each do |f| %>
      <th><%= f.name %></th>
    <% end %>
  </tr></thead>
  <tbody>
    <% @issues && @issues.each do |issue| %>
      <tr>
      <td><%= issue.id %></td>
      <td><%= issue.status.name %></td>
      <td><%= issue.project.name %></td>
      <td><%= issue.tracker.name %></td>
      <td><%= issue.priority.name %></td>
      <td><%= issue.subject %></td>
      <td><%= issue.assigned_to && issue.assigned_to.name %></td>
      <td><%= issue.category && issue.category.name %></td>
      <td><%= issue.fixed_version && issue.fixed_version.name %></td>
      <td><%= issue.author.name %></td>
      <td><%= issue.start_date %></td>
      <td><%= issue.due_date %></td>
      <td><%= issue.done_ratio %></td>
      <td><%= issue.estimated_hours %></td>
      <td><%= issue.description[0..17] %>...</td>
      <% issue.available_custom_fields.each do |f| %>
        <td><%= issue.custom_value_for(f) %></td>
      <% end %>
      </tr>
    <% end %>
  </tbody>
</table>