before_filterでfalse返しているのにアクションが実行されるコトがある

以前ちょろーっと読んだ本には「before_filterに指定したブロックやメソッドが評価された結果, falseを返した場合, フィルターのチェインはそこで終わるよ!」って書いてあった(と思う).

それを信じてやってみたら, なんとチェインが終わらない(そのままアクションが実行される)!

class HogeController
  before_filter :fuga

  private

  def fuga
    false
  end
end

終わらないのはボクの記憶違いなのか, それともRailsのバージョンによるものなのか…… そしてふと思う. チェインが終わった時点でレスポンス(render, redirect_to)が決まっていないときはどうなるんだろう, って. あー, なんとなくわかったような……

そしてrenderしてみる.

class HogeController
  before_filter :fuga

  private

  def fuga
    render
    false
  end
end

お, 止まった! いまさらながらにソースを見てみる.

# actionpack-2.3.2/lib/action_controller/filters.rb
# 略
def call_filters(chain, index, nesting)
  index = run_before_filters(chain, index, nesting)
  aborted = @before_filter_chain_aborted
  perform_action_without_filters unless performed? || aborted
  return index if nesting != 0 || aborted
  run_after_filters(chain, index)
end
# 略

(あんまり詳しく見ていないのでおそらくだけど)perform_action_without_filtersがアクションのみの実行だと思う. そしてそれを実行する条件にperformed?(レスポンスが決定しているかどうか)が含まれているからかなぁ. てかperformed?がtrueの時点でアクションを実行しないのなら, falseを返す必要ないんじゃない?

というコトで下記のコードでもアクションは実行されないようです.

class HogeController
  before_filter :fuga

  private

  def fuga
    render
  end
end

でもbefore_filterが多重に定義されていたらどうなるかは細かく試していないです(ちょろっと試した感じでは止まっていたと思いますけども……).

RSpec + Railsで遊んでいたら

OpenIDと同じく前々からやってみたかったBDD. つーコトでRailsのテストをRSpecで試しに書いていたんですが, どうやらレスポンスのステータスコードの判定がRSpecっぽくできないっぽい.

describe HogeController, 'のfugaアクションは' do
  it '直接アクセスしても拒否られる' do
    get :fuga
    response.should be_status 403 # もしくは……
    response.should be_forbidden  # こんな感じ. コレがボクの思うRSpecっぽさ
  end
end

んー, というかRSpecどうなってんの? って思って試しに:

def be_forbidden
  403
end

response.should be_forbidden

とやってみたら:

undefined method `matches?' for 403:Fixnum

って言われた. ならmatches?メソッドがあれば怒られないんですかね?

def be_forbidden
  o = {} # とりあえず空のHashで
  def o.matches?(v) # 与えられるのはshould(should_not)のレシーバーだと予想
    v.status == '403 Forbidden'
  end
  o
end

response.should be_forbidden

通ったー! つかRSpecの仕組みを見ずにやったから, 通った方にびっくり.

適当にやってみたから上のはよくないだろうけど, こんな感じに拡張すればいいんかな?

RailsでOpenIDを使うときにハマったところ

前々から興味のあったOpenIDRails上で試そうとしたところ, ちょいちょい動かなかったりしたのでそのメモメモ.

とりあえず導入. OpenIDを使うためのRubyGemsRailsプラグインを入れる.

$ sudo gem install ruby-openid
$ script/plugin install open_id_authentication

そしてプラグインが使うテーブルを追加する.

$ rake open_id_authentication:db:create # これでマイグレーションファイルが作られるので,
$ rake db:migrate                       # 改めてmigrate

そして適当にコントローラーとビューを書く. ちなみに技評さんの記事を参考にしました.

<!-- apps/views/sessions/new.html.erb -->
<% form_for :session, :url => {:action => 'create'} do |f| -%>
  <%= f.label :openid_url, 'OpenID URL' %>
  <%= f.text_field :openid_url %>
  <%= f.submit 'Login' %>
<% end -%>
# apps/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new
  end

  def create
    openid_url = params[:session][:openid_url]
    authenticate_with_open_id openid_url do |result, identity_url, sreg|
      if result.unsuccessful?
        # ログインに失敗
        return
      end

      if User.find_by_identity_url identity_url
        # ログインに成功し, すでにアカウントがある
      else
        # ログインには成功したけど, アカウントを所有していない
      end
    end
  end

  def destroy
    # 省略
  end
end

あとルーティングも設定.

# config/routes.rb
ActionController::Routing::Routes.draw do |map|
  map.resource :session, :only => [:new, :create, :destroy]
end

"undefined method 'relative_url_root' ~"の問題

プラグインのOpenIdAuthentication#requested_urlで使われているActionController::Request#relative_url_rootがRailsのバージョンによっては存在しない(たぶんActionController::Baseに移された)ので上記の例外が投げられる.

なのでrelative_url_rootの呼び出しの箇所をActionController::Base.relative_url_rootに置き換えればいいかと思ったけど, なぜかnilを返すので, いっそ取り除いてみる.

def requested_url
  "#{request.protocol + request.host_with_port + request.path}"
end

ひとまずおk, かな?

認証後に"Only post and delete requests are allowed."とか言われる問題

とりあえずOpenIdAuthenticationモジュールのコードの一部を載せる.

# vendor/plugins/open_id_authentication/lib/open_id_authentication.rb
  protected

    # 略

    def authenticate_with_open_id(identity_url = params[:openid_url], options = {}, &block) #:doc:
      if params[:open_id_complete].nil?
        begin_open_id_authentication(normalize_url(identity_url), options, &block)
      else
        complete_open_id_authentication(&block)
      end
    end

  private
    def begin_open_id_authentication(identity_url, options = {})
      return_to = options.delete(:return_to)
      open_id_request = open_id_consumer.begin(identity_url)
      add_simple_registration_fields(open_id_request, options)
      redirect_to(open_id_redirect_url(open_id_request, return_to))
    rescue OpenID::OpenIDError, Timeout::Error => e
      logger.error("[OPENID] #{e}")
      yield Result[:missing], identity_url, nil
    end

    def complete_open_id_authentication
      params_with_path = params.reject { |key, value| request.path_parameters[key] }
      params_with_path.delete(:format)
      open_id_response = timeout_protection_from_identity_server { open_id_consumer.complete(params_with_path, requested_url) }
      identity_url     = normalize_url(open_id_response.endpoint.claimed_id) if open_id_response.endpoint.claimed_id

      case open_id_response.status
      when OpenID::Consumer::SUCCESS
        yield Result[:successful], identity_url, OpenID::SReg::Response.from_success_response(open_id_response)
      when OpenID::Consumer::CANCEL
        yield Result[:canceled], identity_url, nil
      when OpenID::Consumer::FAILURE
        yield Result[:failed], identity_url, nil
      when OpenID::Consumer::SETUP_NEEDED
        yield Result[:setup_needed], open_id_response.setup_url, nil
      end
    end

createアクションで使っているauthenticate_with_open_idはパラメーターにopen_id_completeがあるかどうかで:

  • フォームからOpenID URLが送信された場合(begin_open_id_authenticationの実行)
  • OPによって認証され, 戻ってきた場合(complete_open_id_authenticationの実行)

を振り分けている. なのでcreateアクションは上記の2つの場合で実行されることになる.

ただし1回目はフォームからのPOSTメソッドでのアクセスだが, 2回目はOPからのリダイレクトによるGETメソッドでのアクセスなので, resourcesを使っている場合, アウトー*1. リダイレクトされるURL(return_to)はこの場合/session?open_id_complete=1になるので, showアクションがあれば問題ないけど, 同じコード書くのは……

んでちょっといろいろ見ていたら(というより他サイトで試していたら)iKnow!改めsmart.fmが良さげな感じに対処してた. smart.fmのreturn_toは以下の通り.

https://smart.fm/openid?_method=post&open_id_complete=1

なるほどー! 以下のようにOpenIdAuthentication#open_id_redirect_urlを書き換えれば_method=postを追加できるのでやってみる.

def open_id_redirect_url(open_id_request, return_to = nil)
  open_id_request.return_to_args['_method'] = 'post'
  open_id_request.return_to_args['open_id_complete'] = '1'
  open_id_request.redirect_url realm, return_to || requested_url
end

でもダメー. なんでだろ?

ひとまずボクなりに解決はしてみた. authenticate_with_open_idを用いてひとつのアクションで対応するのではなく, ふたつのアクションで対応してみた.

# apps/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new
  end

  def create
    # authenticate_with_open_idで行われるnormalize_urlが行われなくなるので,
    # この時点で使っておく
    openid_url = normalize_url params[:session][:openid_url]
    begin_open_id_authentication openid_url do |result, identity_url, sreg|
      # ログインに失敗
      # (begin_open_id_authenticationメソッドに与えたブロックは認証に失敗したとき
      #  にしか呼ばれないので, ブロック実行=認証失敗)
    end
  end

  def complete
    complete_open_id_authentication do |result, identity_url, sreg|
      if User.find_by_identity_url identity_url
        # ログインに成功し, すでにアカウントがある
      else
        # ログインには成功したけど, アカウントを所有していない
      end
    end
  rescue => ex
    render :text => ex.message
  end

  def destroy
    # 略
  end
end
# config/routes.rb
ActionController::Routing::Routes.draw do |map|
  map.resource :session, :only => [:new, :create, :destroy], :member => {:complete => :get}
end
# config/initializers/fix_open_id_authentication.rb
require 'open_id_authentication'

module OpenIdAuthentication
  private
    def open_id_redirect_url(open_id_request, return_to = nil)
      # return_toをコントローラ名/completeにするので, open_id_complete=1で判断する必要なし
      # よって取り除いた(たぶんその判断にしか使っていないと思う)
      open_id_request.redirect_url realm, return_to || url_for(:action => 'complete')
    end

    def requested_url
      "#{request.protocol + request.host_with_port + request.path}"
    end
end

コレで一応OK. 用途の違うブロックをひとつのメソッド(authenticate_with_open_id)に与えないのでこっちの方がいいかも?

*1:たとえ通しても, createアクションに書かれたコードではparams[:session]がnilとなってnil[]を行ってしまうからどのみちマズい

Tabsを用いてタブを切り替えたときに, 入力フィールドにフォーカスさせる

こういうケースが多い気がする*1のでメモ.

<ul id="tabs">
  <li><a href="#foo">Foo</a></li>
  <li><a href="#bar">Bar</a></li>
</ul>
<div id="foo">
  <input type="text" />
</div>
<div id="bar">
  <input type="text" />
</div>
$('#tabs').tabs({
  select: function(event, ui) {
    $(ui.panel).find(':input:first').focus();
  }
});

tabsselectのイベント発生時にフォーカスさせるようにしてあげればいいんだけど, コレではうまくいかない…… どうやらこの段階でフォーカスを当てる要素が非表示(親の非表示が受け継がれている)で, 非表示だとフォーカスが当てられないらしい. ui.panelからui-tabs-hideというクラスを取り除いてあげればいいけど, パネルの高さがけっこうあると, 画面がちらつくコトがあってよくない. というコトで実行を関数の後にしてやる.

$('#tabs').tabs({
  select: function(event, ui) {
    setTimeout(function() { $(ui.panel).find(':input:first').focus() }, 0);
  }
});

まぁ妥当な解決かなぁ.

*1:特にコールバックを用るような仕組みのとき

jQuery UIのSortableとTabsを併用したときに発生するおかしなのをなんとかする

さっきjQueryを使っていたら少しおかしな症状が出ました. どうやらjQuery UIのSortableとTabsを併用したときに出るっぽい. たとえば下記のコードでタブレイアウトにして, なおかつそのタブを並べ替え可能にする.

<ul id="tabs"></ul>
$('#tabs').tabs().sortable();

上記のではタブがいっさいないままなので, 新たなタブを追加するボタンもつけておきます. しかも任意の位置に追加できるリッチさ! 追加する位置は省略すれば最後に追加されるようにしておきました.

追加する位置: <input type="text" id="index" />
<a href="#add_tab" id="add_tab">タブを追加</a>
var num = 0;

$('#add_tab').click(function() {
  var url = '#new_tab_' + num;
  var label = 'Number: ' + num;
  var index = $('#index').val();

  if (index == '') {
    $('#tabs').tabs('add', url, label);
  }
  else {
    $('#tabs').tabs('add', url, label, parseInt(index));
  }

  num++;
});

そして2つほどタブを追加(この時点で0, 1の順)して, 入れ替えー(そして1, 0の順). そしてタブをNumber: 1のタブの後に追加(テキストボックスに1と入力)すると…… 後に挿入しようと思ったのに先に挿入されてしまうはず……(2, 1, 0の順)!

どうやらインデックスが絡んでくる話にはすべてこのような不都合が出るらしく, タブの削除などでも同じ症状が出ます. どうやらTabsに対してなにかするときには現在のliなどの並びを見ずに, jQueryオブジェクトが保持している内部的なデータを元に対して処理するようです. なので並び替えが行われても内部的なデータは並び変わっていないのでこのような症状が出るみたいです. 仕方のない話だとは思いますが.

でも"仕方ない"では済ませたくない(済ませられない)コトもあると思うので, なんとかしてみます. というコトで内部データも並べ替えましょー.

$('#tabs').tabs().sortable({
  update: function(event, ui) {
    var data = $('#tabs').data('tabs');
    var $tabs = [];
    var $panels = [];
    var $lis = [];

    ui.item.parent().find('a').each(function(i, v) {
      var id = v.href.split('#')[1];

      data.$panels.each(function(i, v) {
        if (v.id == id) {
          $tabs.push(data.$tabs[i]);
          $panels.push(data.$panels[i]);
          $lis.push(data.$lis[i]);
          return false;
        }
      });
    });

    data.$tabs = $($tabs);
    data.$panels = $($panels);
    data.$lis = $($lis);
  }
});

書き換えるべき内部データは$tabs, $panels, $lisの3つ(多分)なので, それらが並び替えの対象です. 単純に現在の並びを元にして並び替えて, 最後の書き換えのときにはjQueryオブジェクトにして書き換えています($tabsなどはjQueryオブジェクトなので).

コレでなんとかなった! でもなんかなー…… もっといい方法ないかなー?

file_columnを使ってアップロードされたファイルを削除すると, marshal_dumpがどうとか言われる

Railsでファイルをアップロードする際によく使われているfile_columnプラグイン. 便利なんですが, とある条件でいやーなエラーが発生するみたいです.

その条件は"file_columnを使ったモデルをセッションに格納"し, その状態で"アップロードしたファイルを削除"すると, です. 発生するエラー(例外)は以下のとおり(TypeError).

no marshal_dump is defined for class Proc

どうやらセッションの内容をシリアライズする際, どこかにProcクラスのインスタンスが紛れ込んでいるらしい. そのせいで"Marshal.dumpでシリアライズできないよ"と言われています. 問題が発生している箇所がフレームワークなので, ブラウザで見ると500(サーバーエラー)となります.

原因はfile_columnプラグインのFileColumn::BaseUploadedFile#after_save.

# script/plugin installでインストールした状態で
# /vendor/plugins/file_column/lib/file_column.rb 98行目辺り
def after_save
  @on_save.each { |blk| blk.call } if @on_save
  self
end

@on_saveがくせ者なので, 下記のようにして@on_saveを取り除いてやればOKでした.

def after_save
  if @on_save
    @on_save.each { |blk| blk.call }
    remove_instance_variable :@on_save
  end

  self
end

にしても, file_columnプラグインがガンというコトに気づくのに時間喰った. あとあまりこの不具合を目にしないというコトは, セッションにモデルのインスタンスを格納するコト自体一般的でない?

form_forでフォームを書く

久しぶりにRailsを触るぜ!

いやー, とある時期にRailsは触っていたけども, 最近めっきり触っていないので, Railsの進化にとまどっている次第です. なんせ触っていたのはバージョン1.2.3のだしね. 今はもう2.2.2…… とまどうのは仕方ないけど, ちょいと使ってみたかったので, リハビリも兼ねて追いかけるコトに. そんでココにメモっとく.

ひとまず本は使わず(というか買わず), ソースとWebでやっていこうと思う. まずは早速つまずいたフォームの書き方.

form_forヘルパー

Rails1.*のときはstart_form_tag, end_form_tag(form_tag)を使っていたけど, Rails2.0からform_forというヘルパーを使うようになったらしい. それに合わせて, text_fieldなどの記述方法も変更. 以下のようになる.

<% form_for :human, :url => {:action => 'create'} do |f| -%>
<p>
  <label for="human_name">Name</label>:
  <%= f.text_field 'name' %>
</p>
<p>
  <label for="human_age">Age</label>:
  <%= f.text_field 'age' %>
</p>
<%= f.submit 'Save' %>
<% end -%>

form_forによって(X)HTMLの階層とRubyのブロックによるインデントがある程度一致してわかりやすい感じ. 他にもfields_forなどがある.

あと以前は上記の場合必ず@human変数にモデルのインスタンスを入れておく必要があったけど, form_forでは第2引数にモデルのインスタンスを与えればいい(その場合オプションは第3引数)ので, 必ずしも@humanという名前の変数である必要はないし, もっと言えば上記のように省略できる = newのときにモデルのインスタンスを生成する必要がない.

ちなみにform_forに渡しているブロックのパラメータであるfにはFormBuilderというクラスのインスタンスが渡される模様. というコトで次はFormBuilder.

FormBuilder

コレは素敵. 惚れた. コレを使うとf.text_fieldなどの挙動を変えらるとか.

仕組みは単純で, まず下記のようにFormBuilderのサブクラスを作る.

# app/helpers/extended_form_builder.rb
class ExtendedFormBuilder < ActionView::Helpers::FormBuilder
  def text_field(field, *args)
    (args[0] && args[0][:label] ? "#{label(field, args[0].delete(:label))}: " : '') + super
  end
end

使うときは下記のようにして指定すればOK.

<% form_for :human, :url => {:action => 'create'}, :builder => ExtendedFormBuilder do |f| -%>
<%= f.text_field 'name', :label => 'Name' %>
<% end -%>

こんな風に出ます(text_fieldのところだけ抜粋).

<label for="human_name">Name</label>: <input type="text" size="30" name="human[namae]" id="human_name"/>

ってな具合にフィールド周りのタグをFormBuilderの方に固めてしまうコトができるのかー.

実際には下記のように使ってみました(google:非同期アップロードなんてそう頻繁に使うのか, と言われればアレですが).

module ApplicationHelper
  def async_upload_form_for(record_or_name_or_array, *args, &proc)
    args << {} unless args[-1] && args[-1].class == Hash

    html_options = {
      :target    => AsyncUploadFormBuilder.target(record_or_name_or_array),
      :multipart => true
    }

    options = args.pop
    options[:html] = (options[:html] || {}).update html_options
    options[:builder] = AsyncUploadFormBuilder
    form_for record_or_name_or_array, *(args << options), &proc
  end
end
class AsyncUploadFormBuilder < ActionView::Helpers::FormBuilder
  def self.target(form_name)
    "#{form_name}_async_upload"
  end

  def file_field(field, *args)
    target = self.class.target(@object_name)

    options = {
      :id    => target,
      :name  => target,
      :style => 'width: 0; height: 0; position: absolute; left: -9999px;'
    }

    @template.content_tag(:iframe, '', options) + super
  end
end

enctype属性やtarget属性を勝手に補完して, file_fieldを呼び出したときにiframeタグも出力する, ってな感じです. 後はviewに下記のような感じで書くだけ.

<% async_upload_form_for :human, :url => {:action => 'upload'} do |f| -%>
<%= f.file_field 'file' %>
<%= f.submit 'Upload' %>
<% end -%>

すっきり!

関数が定義されているか判断する

特定のモジュールに関数が定義されているかはerlang:function_exported/3という関数で判断できる(正確にはエクスポートされているかどうか). だけどコレ, モジュールがロード済みじゃないと効果ないんだよねー. 今まで"それなら使えないじゃん!"とか思っていたけど, "それならモジュールをロードすればいいじゃん!"ってコトを今しがた思いついた.

exists_function(Module, Function, Arity) ->
    case module_loaded(Module) of
        true ->
            erlang:function_exported(Module, Function, Arity);
        false ->
            case code:load_file(Module) of
                {module, _} -> erlang:function_exported(Module, Function, Arity);
                _           -> false
            end
    end.

関数が定義(エクスポート)されていないのか, そもそもモジュール自体が存在しないのかわからなくなるけど, とりあえずコレでOK. あとそのモジュールがロードされっぱなしになるけど.

そういやerlangモジュールの一部はモジュール名なしでも関数を呼べる(モジュール名:関数名と書かなくてもよい)けど, それらの関数とそうでない関数の差はなんだろう? よく呼ぶ/呼ばないとか? うーん.

あとそもそもなぜ関数が定義されているか知りたい理由を忘れてしまった……!

PNGEncoderが標準で使えるっぽい

ってけっこう前から? それとも最初から?

以前はBitmapDataをPNGエンコードするためにas3corelib - Google Codeからas3corelibを落として, com.adobe.images.PNGEncoderをインポートして使っていた.

でもmx.graphics.codec.PNGEncoderってのがあって, それでPNGエンコードできちゃう. そーなのかー.

var bytes:ByteArray = new PNGEncoder().encode(bitmapData);

でもなんで静的メソッドじゃないんだろ?