Rails で XLSX ダウンロード

昨日のリベンジ。

Gemfile に rubyML を追加し、bundle 実行。

サンプルとして、Userモデルの一覧をダウンロードさせてみる。
クライアントは Win、IE とする。

User モデルの作成

適当に。

% rails g scaffold User name email birthday:date

MIME Type の追加

参考
2007 Office system ファイル形式の MIME タイプをサーバーで登録する

config/initializers/mime_types.rb

Mime::Type.register "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", :xlsx

ファイルのアップロードでは form input の accpet 指定でも使うし、
この MIME Type はいちいち書いていられないので rails_config などに書いて利用するのが良いと思う。

railsconfig/rails_config · GitHub

xlsx 出力ストリーム を返すメソッドの作成

app/models/user.rb

class User < ActiveRecord::Base
  def self.to_xlsx(users)
    workbook = RubyXL::Workbook.new
    worksheet = workbook[0]

    %w(氏名 EMAIL 生年月日).each_with_index do |val, idx|
      worksheet.add_cell(0, idx, val)
    end

    users.each.with_index(1) do |user, row_idx|
      %w(name email birthday).each_with_index do |key, col_idx|
        worksheet.add_cell(row_idx, col_idx, user.__send__(key))
      end
    end

    workbook.stream.read
  end
end

単純なマスタ系だけなら、to_xlsx はモジュールにしてmix-in。
rails_config や I18n と組み合わせて 出力項目や列名表示などを呼び出すようにしておくと、
設定ファイルに追加するだけで to_xlsx 呼び出せて便利。
多少複雑なら、draper などと組み合わせれば良いかな。

drapergem/draper · GitHub

コントローラの設定

app/controllers/users_controller.rb

class UsersController < ApplicationController
  # 省略
  def index
    @users = User.all

    respond_to do |format|
      format.html
      format.xlsx do
        send_data User.to_xlsx(@users),
          filename: "ユーザ一覧.xlsx".encode(Encoding::Windows_31J)
      end
    end
  end
  # 省略
end

ここで、Win 機 IE だと日本語ファイル名は文字化ける。FireFoxChrome だと OK。
なので、ファイル名は文字コード変換する。
が、Win 機以外だと文字化けるのでどうするのが良いんだろう。
日本語ファイル名を使わないのが一番か。

ダウンロードリンク

app/views/users/index.html.haml

= link_to 'All User Download', users_path(format: :xlsx)

Excelファイルのフォント

デフォルトだとフォントは Verdana になっている。
MS の奴隷なので "MS Pゴシック" にしたい。
ざっと見たところ、行やセルでのフォント指定はあるように見えたが、全体を変える方法が無いように見える。
なので、適当に元を変えてやった。
また、Macなどのフォントがない状態だとどうなるかは試してない。

./config/initializers/rubyxl_font.rb

module RubyXL
  class Font < OOXMLObject
    def self.default(size = 10)
      self.new(:name => RubyXL::StringValue.new(val: 'MS Pゴシック'),
               :sz => RubyXL::FloatValue.new(val: size) )
    end
  end
end