LoginSignup
19

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

Last updated at Posted at 2015-08-03

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19