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

![Screenshot 2015-08-03 16.24.10.png](https://qiita-image-store.s3.amazonaws.com/0/82804/0e9d65ee-b8d7-61c0-1ab4-e77041553573.png)

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

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

#この方法の長所

- ビューテンプレートに実装すると、（何もしなくても）[ビューヘルパーメソッド](http://api.rubyonrails.org/classes/ActionView/Helpers.html)にアクセス可能。
- モデルに実装しないので、モデルをコンパクトにできる。

例：[railscasts/379-template-handlers](https://github.com/railscasts/379-template-handlers/blob/master/store-after/app/views/products/index.csv.rb)

```rb
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`

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

require 'csv'
require 'rails/all'

# ...
```

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

- [ActionController::Streaming::send_file_headers!](http://apidock.com/rails/ActionController/Streaming/send_file_headers!)を参考にした。
- 細かい記述方法を気にしなくてよくなる。
- 必要なデータの渡し忘れを防げる。

```rb:/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
```

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

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

  def index
    @users = User.all
  end

  # ...
end
```

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

```rb:/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")`へのリンクを好きなところに設置

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

###実際に出力されたデータ
```rb
"<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](https://qiita-image-store.s3.amazonaws.com/0/82804/42a149db-0ae6-ac5d-1405-000dc22ffb82.png)

#テスト

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

[render_views](https://www.relishapp.com/rspec/rspec-rails/docs/controller-specs/render-views)を用いて強制的にrspecにビューを生成させることが可能ということを知り、下記のテストを作成。
（@hidakatsuyaさん、情報ありがとうございました。）

```rb:/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](https://qiita-image-store.s3.amazonaws.com/0/82804/c8f40354-2440-1445-de69-1ac81bf01abb.png)

シンプルなテストの例

- [blue_csv/test/blue_csv_test.rb](https://github.com/rtsinani/blue_csv/blob/master/test/blue_csv_test.rb)

#資料

Ruby CSV library

- [Ruby CSV](http://ruby-doc.org/stdlib-2.0.0/libdoc/csv/rdoc/CSV.html)
- [A Guide to the Ruby CSV Library, Part I](http://www.sitepoint.com/guide-ruby-csv-library-part/)

CSV export

- [Railscasts #362 Exporting Csv And Excel](http://railscasts.com/episodes/362-exporting-csv-and-excel)
- [Railscasts #362 Exporting Csv And Excel - YouTube](https://www.youtube.com/watch?v=SelheZSdZj8)
- [GoRails #45 Exporting Records To CSV](https://gorails.com/episodes/export-to-csv?autoplay=1)


Template Handlers

- [Railscasts PRO #379 Template Handlers (pro)](http://railscasts.com/episodes/379-template-handlers)
- [Railscasts PRO #379 Template Handlers (pro) - YouTube] (https://www.youtube.com/watch?v=lEnw14mjhLk)

Rails

- [rails/actionview/lib/action_view/template/handlers.rb](https://github.com/rails/rails/blob/master/actionview/lib/action_view/template/handlers.rb)
- [rails/ActionController/Streaming/DEFAULT_SEND_FILE_OPTIONS](https://www.omniref.com/github/rails/docrails/1.1.0.RC1/symbols/ActionController::Streaming::DEFAULT_SEND_FILE_OPTIONS#line=9)
- [rails/ActionController/Streaming/send_file]( http://apidock.com/rails/ActionController/Streaming/send_file)
- [rails/ActionController/Streaming/send_file_headers!](http://apidock.com/rails/ActionController/Streaming/send_file_headers!)

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

- [Rename .rb template handler to .ruby to avoid conflicts with mustache…](https://github.com/rails/rails/commit/de1060f4e02925c12004f2)

RSpec

- [render_views](https://www.relishapp.com/rspec/rspec-rails/docs/controller-specs/render-views)