ActionMailer でテンプレートを指定する2009/08/11

ActionMailer のRDocでは何故か解説が無いようですが

Model側で

class FooMailer < ActionMailer::Base
  def bar
    from 'foobar@example.com'
    recipients 'foobar@example.com'
    
    template 'hoge'
    body :mes1 => 'hogehoge', :mes2 => 'foobar'
  end
end

FooMailer.create_bar # foo_mailer/bar.erb ではなく foo_mailer/hoge.erbが使用される

と、templateメソッドを利用すると、当該のメールがcreate!される際のテンプレートが明示的に指定できます。 引数は、APP_ROOT/app/views/foo_mailer からの相対パスです。 拡張子は、例のルールで補完されます。

テンプレートをファイルではなく String で与えたい場合に適切なメソッドは無いのですが、 ActionMailer::Base#create! は実行された際、@body が 既にStringのインスタンスならレンダリング処理はスキップして @body の内容をそのままメール本文とするようです。

class FooMailer < ActionMailer::Base
  def piyo
    from 'foobar@example.com'
    recipients 'foobar@example.com'
    
    body 'piyo piyo'
  end
end

FooMailer.create_piyo.body #=> 'piyo piyo'

あとは、自前でerbのコンパイルをするか、 ActionController::Base#render に、:inlineを与えたときのようなメソッドを実装すればいけるはず。 (ActionMailer::Base#render はファイル名しか取れない)

とりあえず、今日はここまで

win32ole で Execlファイルのフォーマット変換2009/08/11

Spreadsheetでどうにもパースできないxlsファイルがあったので、 久し振りにwin32ole経由でExcelを制御しようとしたら、結構忘れていたので自分用メモ

require 'win32ole'

module Excel
end

excel = WIN32OLE.new('Excel.Application')
excel.displayAlerts = false
WIN32OLE.const_load(excel, Excel)

Dir['*.xls'].each do |fn|
  ifn = File.expand_path(fn).gsub('/', '\\')
  ofn = ifn.sub(/xls$/, 'tsv')
  wb = excel.Workbooks.Open(ifn)
  wb.Worksheets(1).Select
  wb.SaveAs(ofn, Excel::XlText)
  wb.Close
end

Workbooks.Openにはフルパスを与える必要があるのと、パスの区切り記号を'\'にするのを忘れないこと。

フォーマット定数はこちら

Ruby のwin32oleで扱うときは、先頭のxを大文字にすること。

TSV(Excelでの表記は"テキスト形式")がリストに無いような気がするけど Excel::XlText(-4158) を指定すると、TSVになる。

Excel::XlUnicodeText(42) は UTF-16のTSVなので扱いに困る。

CSVが良ければ Excel::XlCSV(6) を指定する。

Closeを忘れると60ぐらいで落ちる。

pure ruby の各種フォーマット対応Spreadsheetパーサ(xlsx対応!)2009/08/12

roo - rubyforge プロジェクトページ

OOo, Google Document, Excel(2007の新フォーマット対応)と、幅広いフォーマットへの対応を謳う、スプレッドシートのパーサ

ドキュメントの更新が追い付いていないようですが、最新版は1.3.9

  • ファイルの新規作成には未対応
  • ファイルの更新は、Google Spreadsheetのみ対応

(セルに値を設定するメソッドまでは全てのフォーマットに対して実装されているようですが)

xlsxなファイルを扱う必要があって、自前でなんとでもなりそうだったけど、既存のライブラリを見付けたのでメモ。

ちなみにxlsも対応していることになっていますが、Spreadsheetに丸投げなので、 xlsだけの対応で良い場合は、Spreadsheetを直接利用するべきだと思います。

(Spreadsheet方が高機能で、実装の筋も良い)

また、rooではGoogle Spreadsheet以外ではファイルの更新に対応していませんが、Spreadsheet.open(filename) とか無造作にやっているので、 Spreadsheetのデフォルト値'rb+'が採用されてファイルにロックがかかります。

--- /lib/ruby/gems/1.8/gems/roo-1.3.9/lib/roo/excel.rb.old
+++ /lib/ruby/gems/1.8/gems/roo-1.3.9/lib/roo/excel.rb
@@ -126,7 +126,7 @@
       unless File.file?(@filename)
         raise IOError, "file #{@filename} does not exist"
       end
-      @workbook = Spreadsheet.open(filename)
+      @workbook = Spreadsheet.open(filename, 'rb')
       @default_sheet = self.sheets.first
     ensure
       #if ENV["roo_local"] != "thomas-p"

ぐらいしておくと良いかも。

インストール

rubyforgeにgemがあります。

$ sudo gem install roo

ファイルを開く

require 'rubygems'
require 'roo'

spreadsheet = Openoffice.new("myspreadsheet.ods")      # Openoffice Spreadsheet
spreadsheet = Excel.new("myspreadsheet.xls")           # Excel(xls)
spreadsheet = Google.new("myspreadsheetkey_at_google") # Google Spreadsheet
spreadsheet = Excelx.new("myspreadsheet.xlsx")         # Excel(xlsx)

の個別のクラスを直接利用する他

spreadsheet = Roo::Spreadsheet.open("myspreadsheet.xlsx")

でファイルの拡張子から前述のクラスを自動で判別してもらうことができます。

値の参照

Spreadsheetライブラリとは異なり、Worksheet(やrow, cell)を抽象化したクラスはありません。 GenericSpreadsheetクラスを継承した、Openoffice, Google, Excel, Excelxといったクラスに #cell(row, col, sheet=nil)のアクセサがあります。

spreadsheet.cell(2, 1)
spreadsheet.cell(2, 'A')
spreadsheet.cell(2, 'a')
spreadsheet.cell('A', 2)

これらは同じセルの値を参照します。 数値は1 orignです。(表計算ソフトの画面との整合性を優先したのでしょうが、ちょっと好きになれない)

ワークシートの選択は3つめの引数に、シートの名称を与えます。

spreadsheet.cell('A', 2, 'Sheet 1')

省略した場合は #default_sheet に設定されたシート名が使用されます。 シート名の一覧は #sheets で得られます。

spreadsheet.sheets #=> ["Sheet 1", "Sheet 2", "Sheet 3"]
spreadsheet.default_sheet = spreadsheet.sheets.first #=> "Sheet 1"

問題点

いくつか問題点が存在すると思っています。

名前空間の汚染

Openoffice, Google, Excel, Excelxといったクラス名が、グローバルに定義されています。 module Roo 配下になっているのは、前述のRoo::Spreadsheet.openメソッドと Roo::VERSION 定数だけです。

そもそも、OOoのSpreadsheetアプリのデータファイルのパーサ に Openofficeのクラス名を与えるセンスは許せない気がします。

アクセサがださい

ワークブック(ワークシート)に対するアクセサには、セル値を参照する #cell, セルの値の型を参照する #celltype, セルの内容が式である場合にその式を取り出す #formula の他、限定的なサポートのようですが、書式を参照する #font などもありますが、そのいずれも Enumerableではありません。

Enumerableなメソッドが一つもありません

lib/roo/roo_rails_helper.rb に、spreadsheet(これもグローバルに定義された関数形式のメソッドだな……)という、 ワークシートの内容をtableタグでhtml出力してくれるメソッドが定義されていますが、こいつは

@rspreadsheet.first_row.upto(@rspreadsheet.last_row) do |y|
  .. do something ..
  @rspreadsheet.first_column(sheet).upto(@rspreadsheet.last_column(sheet)) do |x|
    .. do something ..
  end
end

と、ワークシートの使用範囲の下限.upto(上限)でループを回しているので、超格好悪いです。

なにより面倒臭いので GenericSpreadsheet にメソッドを追加します。

class GenericSpreadsheet
  def to_a(sheet = nil)
    sheet ||= @default_sheet
    raise RangeError unless sheets.include?(sheet)
    
    [*header_line.succ..last_row(sheet)].map |row|
      [*1..last_column(sheet)].map do |col|
        @cell[sheet][ [row, col] ]
      end
    end
  end
end

本当は GenericSpreadsheet#each にしたかったのですが、@cellの構成が変態的で結局Arrayを生成するしかなさそうだったので諦めました。

HashのKeyがArrayって危険な気がするのですが……。

また、@cell[sheet][ [row, col] ] の内部表現は各パーサに任されているようなので、値の取得にはpublicメソッドの cell(row, col, sheet) を利用するべきかも知れません。 とりあえず、Excelx(xlsx)では、日付/日時のパース(@cellの中身はStringだけど、#cellの戻り値はDateないしDateTime)ぐらいしか 処理がされていない、にも関わらず判定コストが高そうなメソッドになっているので、上記のようにしています。

ナイト・ミュージアム22009/08/15

……一人で見に行く映画じゃなかった。

安心して見れるし、安心して笑える。悪くはない、悪くはないはずなんだが、この敗北感はどうだ(笑

サマーウォーズ@新宿バルト92009/08/16

前回は気がつかなかった細かいいろいろまで、目が届いて楽しかった。

バルトの頭文字がWだと思わなかったので、通り過ぎそうになったのは秘密。

劇場自体は良かったのですが、レイトショーや会員カード系の割引サービスが一切無いようなので、 久しぶりに1800円払いました。 なんか屈辱(ぇ?

#サマーウォーズの前売券なんて、チケット屋でも手に入らない予感が……。

あと、やはり待ち時間がそれなりにあったので、新宿を練り歩いてDQ9のすれ違い通信をしていたら、 ものすごい釣果でした。

まさゆきの地図とか川崎ロッカーとかありがとうございました。