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が多重に定義されていたらどうなるかは細かく試していないです(ちょろっと試した感じでは止まっていたと思いますけども……).
should xxxのxxxはMatcher
なるほどー. matches?メソッドを備えたMatcherなるモノを渡せばいいんだなー.
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を使うときにハマったところ
前々から興味のあったOpenIDをRails上で試そうとしたところ, ちょいちょい動かなかったりしたのでそのメモメモ.
とりあえず導入. OpenIDを使うためのRubyGemsとRailsのプラグインを入れる.
$ 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)に与えないのでこっちの方がいいかも?
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);
でもなんで静的メソッドじゃないんだろ?