Source

redmine_kagemai_import / lib / tasks / import_kagemai_reports.rake

=begin
Imports issues from Kagemai BTS

Usage:

  $ rake redmine:import_kagemai_reports RAILS_ENV=production \
          KAGEMAI_ROOT=/path/to/kagemai KAGEMAI_URL=http://example.org/kagemai/guest.cgi \
          project=kagemai_proj_id

Copyright (c) 2009, Yuya Nishihara <yuya@tcha.org>

This software may be used and distributed according to the terms of the
GNU General Public License version 2, or (at your option) any later version,
incorporated herein by reference.
=end

require 'iconv'

namespace :redmine do
  
desc 'Import issues from Kagemai BTS'
task :import_kagemai_reports => :environment do
  module KagemaiImport
    raise 'KAGEMAI_ROOT not specified' unless ENV['KAGEMAI_ROOT']
    KAGEMAI_ROOT = File.expand_path(ENV['KAGEMAI_ROOT'])

    KAGEMAI_URL = ENV['KAGEMAI_URL']  # optional

    TRACKER_BUG = Tracker.find_by_position(1)
    TRACKER_FEATURE = Tracker.find_by_position(2)
    TRACKER_SUPPORT = Tracker.find_by_position(3)

    SUBJECT_PREFIX = 'K%03d: '

    # Kagemai to Redmine
    PROJECT_MAP = {}
    STATUS_MAP = {'受付済' => '担当', '割当済み' => '担当', '修正済' => '解決', '確認待ち' => '解決',
                  '保留' => '新規', '完了' => '終了'}
    DONE_RATIO_MAP = {'修正済' => 100, '確認待ち' => 100, '完了' => 100}  # default: 0
    PRIORITY_MAP = {'緊急' => '急いで', '高' => '高め', '中' => '通常', '低' => '低め'}
    USER_EMAIL_MAP = {}

    # assigned user name to email
    USER_NAME_EMAIL_MAP = {'未定' => nil}

    def self.initialize()
      # we cannot add LOAD_PATH before loading rails due to unexpected
      # `uninitialized constant XMLScan::Visitor' error.
      # it seems rails magically tries loading xsd/xmlparser/xmlscanner, but it
      # contains broken dependency to XMLScan::Visitor.
      $: << "#{KAGEMAI_ROOT}/lib"

      require 'kagemai/config'

      Kagemai::Config.initialize(KAGEMAI_ROOT, "#{KAGEMAI_ROOT}/kagemai.conf")

      # due to `premature end of regular expression' error
      switch_kcode('EUC-JP') { require 'kagemai/fold' }

      require 'kagemai/bts'
      require 'kagemai/project'
      require 'kagemai/message_bundle'

      Kagemai::MessageBundle.open(Kagemai::Config[:resource_dir],
                                  Kagemai::Config[:language],
                                  Kagemai::Config[:message_bundle_name])

      @bts = Kagemai::BTS.new(Kagemai::Config[:project_dir])

      @iconv = Iconv.new($KCODE, Kagemai::Config[:charset])
    end

    def self.reformat_text(kagemai_proj_id, report, text)
      # map <BTS:n> link to #m
      proj = map_project(kagemai_proj_id, report)
      text.gsub(/<BTS:(\d+)>/) do |s|
        e = Issue.find(:last, :conditions => ['project_id = ? AND subject LIKE ?',
                                               proj.id, "#{SUBJECT_PREFIX % $1.to_i}%"], :order => 'id')
        e ? "##{e.id}" : s
      end
    end

    def self.reformat_description(kagemai_proj_id, report)
      text = reformat_text(kagemai_proj_id, report, encode(report.first['body']))
      return text unless KAGEMAI_URL

      link_url = "#{KAGEMAI_URL}?action=view_report&id=#{report.id}&project=#{kagemai_proj_id}"
      "#{text}\n\n#{link_url}"
    end

    def self.reformat_subject(kagemai_proj_id, report)
      "#{SUBJECT_PREFIX % report.id}#{encode(report.attr('title'))}"
    end

    def self.map_project(kagemai_proj_id, report)
      Project.find_by_identifier(PROJECT_MAP[kagemai_proj_id] || kagemai_proj_id) \
          or raise "unknown project: #{kagemai_proj_id}"
    end

    def self.map_status(kagemai_proj_id, report)
      status = encode(report['status'])
      IssueStatus.find_by_name(STATUS_MAP[status] || status) or raise "unknown status: #{status}"
    end

    def self.map_priority(kagemai_proj_id, report)
      return Enumeration.default('IPRI') unless report.first.has_element? 'priority'
      prio = encode(report['priority'])
      Enumeration.find(:first, :conditions => ['opt = ? AND name = ?', 'IPRI', PRIORITY_MAP[prio] || prio]) \
          or raise "unknown priority: #{prio}"
    end

    def self.map_author(kagemai_proj_id, report)  # report or message
      name = encode(report['email'])
      User.find_by_mail(USER_EMAIL_MAP[name] || name) or raise "unknown user: #{name}"
    end

    def self.map_assigned_to(kagemai_proj_id, report)
      name = encode(report['assigned'])
      email = USER_NAME_EMAIL_MAP.fetch(name, name)
      return nil unless email  # nil means not assigned
      User.find_by_mail(email) or raise "unknown user: #{name} (email: #{email})"
    end

    def self.map_done_ratio(kagemai_proj_id, report)
      status = encode(report['status'])
      DONE_RATIO_MAP[status] || 0
    end

    def self.map_tracker(kagemai_proj_id, report)
      TRACKER_BUG
    end

    def self.import_project(kagemai_proj_id)
      raise 'kagemai project not specified' unless kagemai_proj_id

      puts "Importing reports: #{kagemai_proj_id}"

      # XMLScan cannot parse reports correctly with $KCODE = 'UTF-8'
      switch_kcode(Kagemai::Config[:charset]) do
        kagemai_proj = @bts.open_project(kagemai_proj_id)
        kagemai_proj.each do |rep|
          issue = Issue.new(:subject => reformat_subject(kagemai_proj_id, rep),
                            :description => reformat_description(kagemai_proj_id, rep),
                            :created_on => rep.create_time)
          issue.project = map_project(kagemai_proj_id, rep)
          issue.author = map_author(kagemai_proj_id, rep)
          issue.tracker = map_tracker(kagemai_proj_id, rep)
          issue.status = map_status(kagemai_proj_id, rep)
          issue.priority = map_priority(kagemai_proj_id, rep)
          issue.assigned_to = map_assigned_to(kagemai_proj_id, rep)
          issue.done_ratio = map_done_ratio(kagemai_proj_id, rep)
          # TODO: attachment

          Time.fake(rep.modify_time) { issue.save }
          print "[#{rep.id}->#{issue.id}]"

          rep.each do |msg|
            next if msg == rep.first  # first message is treated as issue
            note = Journal.new(:notes => reformat_text(kagemai_proj_id, msg, encode(msg['body'])))
            note.journalized = issue
            note.user = map_author(kagemai_proj_id, msg)
            # TODO: attachment
            Time.fake(msg.create_time) { note.save }
            print '.'
          end
        end
        puts
        puts 'done'
      end
    end

    # borrowed idea from migrate_from_track.rake
    class ::Time
      class << self
        alias :real_now :now
        def now
          @fake_time or real_now
        end
        def fake(time)
          @fake_time = time
          begin
            res = yield
          ensure
            @fake_time = nil
          end
          res
        end
      end
    end

    def self.encode(text)
      @iconv.iconv text
    end

    # do something in another $KCODE
    def self.switch_kcode(encoding)
      orig_kcode = $KCODE
      $KCODE = encoding
      begin
        ret = yield
      ensure
        $KCODE = orig_kcode
      end
      ret
    end
  end

  KagemaiImport.initialize()
  KagemaiImport.import_project(ENV['project'])

end
end