Help us understand the problem. What is going on with this article?

Rails - .rubyテンプレートでCSVダウンロード

More than 5 years have passed since last update.

Screenshot 2015-08-03 16.24.10.png

経緯

  • .ruby template handlerを使用するとCSV関連のロジックを綺麗にモデルから分離できると知り調査。
  • 今後のためにメモ。

やりたいこと

  • UsersテーブルのデータをCSVとしてエクスポートしたい。
  • ビューテンプレートに実装したい。

この方法の長所

  • ビューテンプレートに実装すると、(何もしなくても)ビューヘルパーメソッドにアクセス可能。
  • モデルに実装しないので、モデルをコンパクトにできる。

例:railscasts/379-template-handlers

response.headers["Content-Disposition"] = 'attachment; filename="products.csv"'

CSV.generate do |csv|
  csv << ["Name", "Price", "URL"]
  @products.each do |product|
    csv << [
      product.name,
      number_to_currency(product.price),
      product_url(product)
    ]
  end
end

環境

  • Ruby 2.2.1
  • Rails 4.2.3

手順

/config/application.rbでCSVモジュールをrequire

/config/application.rb
require File.expand_path('../boot', __FILE__)

require 'csv'
require 'rails/all'

# ...

ヘッダーを設定するためのメソッド

/app/helpers/csv_helper.rb
module CsvHelper

  def set_file_headers(options)

    [:filename, :disposition].each do |arg|
      raise ArgumentError, ":#{arg} option required" if options[arg].nil?
    end

    disposition = options[:disposition]
    disposition += %(; filename="#{options[:filename]}") if options[:filename]

    headers.merge!(
      'Content-Disposition'       => disposition,
      'Content-Transfer-Encoding' => 'binary'
    )
  end
end

エクスポートしたいデータをクエリ

/app/controllers/users_controller.rb
class UsersController < ApplicationController
  include CsvHelper
  # ...

  def index
    @users = User.all
  end

  # ...
end

*.csv.rubyファイルを新規作成し、CSV処理をビューテンプレートとして実装

/app/views/users/index.csv.ruby
# ==> 1. Set response headers
# http://api.rubyonrails.org/classes/ActionDispatch/Request.html#method-i-headers
set_file_headers filename:    "users-#{Date.today}.csv",
                 disposition: "attachment"

# ==> 2. Set options if you want (e.g. :col_sep, :headers, etc)
# http://ruby-doc.org/stdlib-2.0.0/libdoc/csv/rdoc/CSV.html#DEFAULT_OPTIONS
options = { headers: true }

# ==> 3. Generate csv that is to be downloaded

attributes = %w(id username sign_in_count created_at confirmed_at updated_at)

CSV.generate(options) do |csv|
  # Column names in a first row
  csv << attributes

  # Write each record as an array of strings
  @users.unscoped.each do |user|
    csv << attributes.map{ |attr| user.send(attr) }
  end
end

users_path(format: "csv")へのリンクを好きなところに設置

/app/views/users/index.html.haml
= link_to "CSVダウンロード", users_path(format: "csv"), class: "btn btn-warning"

実際に出力されたデータ

"<pre class=\"debug_dump\"><kbd style=\"color:brown\">&quot;id,username,sign_in_count,created_at,confirmed_at,updated_at\\n1,Masa Nishiguchi,2,2015-07-30 20:30:25 UTC,2015-07-30 20:30:24 UTC,2015-08-02 22:07:55 UTC\\n2,Elton Gottlieb,0,2015-07-30 20:30:25 UTC,2015-07-30 20:30:25 UTC,2015-07-30 20:30:25 UTC\\n3, (...中略...) ,Elroy Howe,1,2015-07-30 20:30:25 UTC,2015-07-30 20:30:25 UTC,2015-07-31 13:40:07 UTC\\n&quot;</kbd></pre>"

Screenshot 2015-08-03 16.32.49.png

テスト

RSpecのコントローラスペックは、デフォルトの状態ではresponse.bodyが空になっておりresponse.bodyによるCSV内容確認は不可能。

render_viewsを用いて強制的にrspecにビューを生成させることが可能ということを知り、下記のテストを作成。
@hidakatsuyaさん、情報ありがとうございました。)

/spec/controllers/users_controller_spec.rb
RSpec.describe UsersController, type: :controller do
  # ...

  describe "admin user" do
    before { log_in_as FactoryGirl.create(:admin), no_capybara: :true }

    describe 'GET #index' do

      before(:all) { 10.times { FactoryGirl.create(:user) } }

      it "renders the index page" do
        get :index
        expect(response).to render_template :index
      end

      describe "CSV format" do
        render_views  #<== 強制的にrspecにビューを生成させる

        before { get :index, format: "csv" }

        let(:user) { User.first}

        it { expect(response).to render_template :index }
        it { expect(response.headers["Content-Type"]).to include "text/csv" }

        attributes = %w(id username sign_in_count created_at confirmed_at updated_at)

        attributes.each do |field|
          it "has column name - #{field}" do
            expect(response.body).to include field
          end
        end

        attributes.each do |field|
          it "has correct value for #{field}" do
            expect(response.body).to include user[field].to_s
          end
        end

        it "has correct number of rows" do
          num_of_rows = 1 + User.all.count
          expect(response.body.split(/\n/).size).to eq num_of_rows
        end
      end
    end

    # ...
  end
end

Screenshot 2015-08-04 10.09.25.png

シンプルなテストの例

資料

Ruby CSV library

CSV export

Template Handlers

Rails

.rb template handlerが.rubyに変更された経緯

RSpec

mnishiguchi
Software Engineer in Washington DC. Ruby and Rails at work; Elixir and Nerves at home.
http://mnishiguchi.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away