[Rails]ActiveAdminでCSVダウンロード機能をカスタマイズする


[まとめ] 現在開催中のKindleセール情報はこちら

ActiveAdminにはcsvダウンロード機能がデフォルトついているのですが、デフォルトのままだと一括でcsvデータを取得することができません。(管理画面表示1ページあたりの表示件数分になるので30件しか取得できませんでした)

また、csvの項目もデフォルトだとDBのカラム名のままになるので、管理画面上で表示項目をカスタマイズしている場合はcsvもカスタマイズする必要があります。

カスタマイズ方法を調べてみたところ方法は以下の2通りがありました。

  1. 既存のcsvダウンロード機能をカスタマイズする方法
  2. csvダウンロード用のアクションを新たに作成する方法

両方試してみたのですが、csvダウンロード後にExcelで開きたい場合は2.の方法がおすすめです。

1.の場合はcsvファイルは文字コードがUTF-8で出力されるため、csvダウンロード後に文字コードをShift-JISに変換する必要があります。

1. 既存のcsvダウンロード機能をカスタマイズ

ActiveAdmin.register Post do

  # csvダウンロード時のみ1ページの取得件数を10000件に変更
  before_filter only: :index do
    @per_page = 10_000 if request.format == 'text/csv'
  end

  # csvの内容をカスタマイズ
  csv :force_quotes => true do
    column :id
    column :title
    column("Author") { |post| post.author.full_name }
  end

end

2. csvダウンロード用のアクションを新たに作成

ActiveAdmin.register Post do

  # csvダウンロードアクションを作成
  collection_action :download_report, :method => :get do
    posts = Post.order('created_at DESC')
    csv = CSV.generate do |csv|
      csv << ['id', 'title', 'Author']
      posts.each do |post|
        csv << [
          post.id,
          post.title,
          post.author.full_name
        ]
      end
    end
    # Shift-JISでcsvを出力
    send_data csv.encode('Shift_JIS', :invalid => :replace, :undef => :replace), type: 'text/csv; charset=shift_jis; header=present', disposition: "attachment; filename=report.csv"
  end

  # csvダウンロードリンクを追加
  action_item(:csv_report, only: :index) do
    link_to('csv report', params.merge(:action => :download_report))
  end

  # 既存のcsvダウンロードリンクを非表示にしておく
  index :download_links => false do
    # indexに表示する項目を記述
  end

end

変換できない文字は「?」に置換するようにしています。

2015/2/9追記

データ数が多い場合はeachではなく、find_eachやfind_in_batchesを用いて分割して処理するようにしましょう。

ただし、orderは使えなくなるのでその点は注意が必要です。(idの昇順になります)

ActiveAdmin.register Post do

  # csvダウンロードアクションを作成
  collection_action :download_report, :method => :get do
    csv = CSV.generate( encoding: 'SJIS' ) do |csv|
      csv << ['id', 'title', 'Author']
      Post.find_each(batch_size: 500) do |post|  # 500件ずつ分割で取得
        csv << [
          post.id,
          post.title,
          post.author.full_name
        ]
      end
    end
    # Shift-JISでcsvを出力
    send_data csv.encode('Shift_JIS', :invalid => :replace, :undef => :replace), type: 'text/csv; charset=shift_jis; header=present', disposition: "attachment; filename=report.csv"
  end

  # csvダウンロードリンクを追加
  action_item(:csv_report, only: :index) do
    link_to('csv report', params.merge(:action => :download_report))
  end

  # 既存のcsvダウンロードリンクを非表示にしておく
  index :download_links => false do
    # indexに表示する項目を記述
  end

end

2015/6/25追記

データ件数が10万件を超えたあたりで、上記の対応ではunicornのタイムアウトが発生してしまったため、collection_actionの部分をストリーミングでcsvをダウンロードするように変更しました。

  • csvのエンコード変更メソッドを持つCsvUtilを追加
# app/admin/concerns/csv_util.rb
module CsvUtil
  def self.encode_sjis(data)
    data.encode('Shift_JIS', :invalid => :replace, :undef => :replace)
  end
end
  • collection_actionの修正
ActiveAdmin.register Post do
  collection_action :download_report, :method => :get do
    self.response.headers["Content-Type"] ||= 'text/csv; charset=Shift_JIS'
    self.response.headers["Content-Disposition"] = "attachment;filename=report.csv"
    self.response.headers["Content-Transfer-Encoding"] = "binary"
    self.response.headers["Last-Modified"] = Time.now.ctime.to_s

    self.response_body = Enumerator.new do |yielder|
      yielder << CsvUtil.encode_sjis(['id', 'title', 'Author'].to_csv)
      posts.find_each(batch_size: 500) do |post|  # 500件ずつ分割で取得
        yielder << CsvUtil.encode_sjis([
          post.id,
          post.title,
          post.author.full_name
        ].to_csv)
      end
    end
  end

  action_item(:csv_report, only: :index) do
    link_to('csv report', params.merge(:action => :download_report))
  end
end
  • unicorn.rbのtimeout時間を修正
# config/unicorn.rb

timeout 120

データが巨大な場合は120秒でもタイムアウトするので、適宜調整してください。

これで先にレスポンスを返して、残りのデータをストリーミングでダウンロードするようになりタイムアウト問題を解消できました。

参考

[まとめ] 現在開催中のKindleセール情報はこちら