Ruby on Rails: ファイルアップロード機能を作る (ajax_scaffold_generator 編)
前回の記事で作ったファイルアップローダーは、ファイル本体をバイナリデータとして DB に保存したが、DB ではなくファイルとしてサーバに置くものを作ってみる。
それだけではつまらないので、今回は ajax_scaffold_generator を使って見栄えのいいものを作ってみることにした。
■ バージョン情報
$ ruby script/about ./script/../config/boot.rb:29:Warning: require_gem is obsolete. Use gem instead. About your application's environment Ruby version 1.8.5 (i686-linux) RubyGems version 0.9.2 Rails version 1.2.2 Active Record version 1.15.2 Action Pack version 1.13.2 Action Web Service version 1.2.2 Action Mailer version 1.3.2 Active Support version 1.4.1 Application root /home/y-ohtaki/rails/ajax_uploader Environment development Database adapter mysql Database schema version 1
初期設定
■ 準備
ajax_scaffold のインストール。
$ gem install ajax_scaffold_generator
ファイルを保存するためのフォルダを作っておく。
$ mkdir files
■ rails してから scaffold 作成まで
$ rails ajax_uploader
データベース設定。とりあえずデータベースにはファイル名のみ保存。
$ ruby script/generate migration create_attachments
migrate は次のような感じ。
$ cat db/migrate/001_create_attachments.rb
class CreateAttachments < ActiveRecord::Migration
def self.up
create_table :attachments do |t|
t.column(:filename, :string)
end
end
def self.down
drop_table :attachments
end
end
migrate する。
$ rake db:migrate
scaffold 作成。
$ rails script/generate ajax_scaffold attachment
ビューの修正
結構めんどくさい。
■ ファイルをダウンロードするためのリンクを作る
ajax_scaffold を使うと edit という項目が自動的につくけど、アップロードしたファイルを edit するようなことは想定しないので、ここをダウンロードリンクとして使うことにする。
_attachment.rhtml の link_to_remote を
<%= link_to "Download", edit_options %>
に変更。
本当はメソッドも download とか view_file とかにするべきなんだけど、めんどくさいのでメソッド名は edit のまま(ダメ仕様)
■ 新規登録 form 修正
scaffold で生成された新規登録 form は text_field になっているが、実際にはファイルをアップロードするので、_form.rhtml の
<%= text_field 'attachment', 'filename' , {:class=>"text-input"} %>
の部分を
<%= file_field 'attachment', 'filename' , {:class=>"text-input"} %>
に変更。
コントローラでは、ここで選択されたファイルからファイル名を抜き取ってデータベースに入れるようにする(後述)。
■ 新規登録 form で multipart を扱えるようにする
ここがめんどくさい。ajax_scaffold で生成される form_remote_tag では :multipart を使えない。
理由は、javascript ではローカルファイルを扱えないから。
【参考】 Can you upload files with AJAX?
しょうがないので responds_to_parent を使うことにする。
【参考】 Ajaxっぽく画面遷移なしでファイルアップロードしたい! (yamazのRails日記)
$ ruby script/plugin install http://sean.treadway.info/svn/plugins/responds_to_parent/
responds_to_parent の使い方
- ビューに iframe を用意して、form_start_tag の :target にその iframe を指定しておく。
- form_start_tag で呼ばれるアクションに対応したメソッド内で responds_to_parent メソッドを使う。
- responds_to_parent メソッドの引数にブロックを渡す。ブロック内では render :update や rjs を使って javascript を render するようにする。
responds_to_parent の動作
- 渡された“render の内容 (response)” = “実行したい javascript” が、親要素を基準として実行されるよう処理をする。
- 要は location をいじってるだけだったりする。
- 渡された javascript を実行した後に、location を再設定するための javascript を setTimeout で呼んでる。
- なので、setTimeout のコールバックが呼ばれる前にresponds_to_parent を処理する iframe を消したりするとスクリプトエラーになる。
■ responds_to_parent 用の iframe を追加する
ajax_scaffold では、新規追加や変更時の form は submit されると消去されるようになっているので、form 内に iframe を入れとくと前述の setTimeout の関係ではまります。注意。(←はまった)。
今回は component.rhtml 先頭部分に iframe を作ることにする。
<% if @show_wrapper %> <iframe id='iframe_for_responds_to_parent' name='iframe_for_responds_to_parent' style="display:none;"></iframe>
■ ファイルアップロードフォーム修正
_new_edit.rhtml を変更。form_remote_tag を start_form_tag にする。
修正前
<%= form_remote_tag :url => @options.merge(:controller => '/attachments'),
:loading => "Element.show('#{loading_indicator_id(@options)}'); Form.disable('#{element_form_id(@options)}');",
:html => { :href => url_for(@options.merge(:controller => '/attachments')),
:id => element_form_id(@options) } %>
修正後
<%= start_form_tag(@options.merge(:controller => '/attachments'),
{:id => element_form_id(@options),
:multipart => true,
:target => "iframe_for_responds_to_parent",
:onsubmit => "submit();Element.show('#{loading_indicator_id(@options)}'); Form.disable('#{element_form_id(@options)}'); return false;"}) %>
form_remote_tag の :loading は Ajax で要求をした後に実行されるようになっているので、これを start_form_tag の :onsubmit にそのまま持ってくるとまずいことになることがある。(onsubmit は POST (or GET) の前に実行される)
ここでは、Form.disable が POST の前に動いて、form の内容を全部無効にしてしまうので、POST する時に要求データが空になってしまう。 (なぜか 404 になった)
これを避けるために、:onsubmit 内で最初に submit(); して POST しておき、return false; で :onsubmit 終了時の POST を抑制する。
コントローラの修正 (attachments_controller.rb)
ファイルパスを定数で定義。以下を追加。
FILE_PATH = "#{RAILS_ROOT}/files"
■ edit メソッドを修正する
edit メソッドでファイルダウンロードさせるようにしたいので、render せずに send_file する。
return render(:action => 'edit.rjs') if request.xhr?
を削除。
@options = { :scaffold_id => params[:scaffold_id], :action => "update", :id => params[:id] }
render :partial => 'new_edit', :layout => true
も削除。
代わりに
file = "#{FILE_PATH}/#{params[:id]}"
send_file(file, :filename => @attachment.filename,
:stream => false,
:disposition => 'inline')
を追加。
■ create メソッドを修正する
begin の後に set_file_name を追加。
set_file_name メソッドでは、まずファイル本体をインスタンス変数に格納してから、params の :filename をファイル名に書き換える。
@successful = @attachment.save の後に save_file を追加。
save_file メソッドは、名前の通りファイルを保存するメソッド。
んで、
return render(:action => 'create.rjs') if request.xhr?
を
return responds_to_parent do render(:action => 'create.rjs') end
に変更。
ajax_scaffold は javascript が使えない環境でもうまく表示されるようになってるんだけど、ここの render の部分でそれを見事にスポイルしてる。しかし、今回はとりあえず無視。
set_file_name メソッドと save_file メソッドを追加。
private
def set_file_name
@file = params[:attachment][:filename]
params[:attachment][:filename] = @file.original_filename
end
def save_file
if @file
File.open("#{FILE_PATH}/#{@attachment.id}", "w") do |f|
f.write(@file.read)
end
end
end
■ destroy メソッドを修正する
@successful = Attachment.find(params[:id]).destroy
を
@attachment = Attachment.find(params[:id])
if @attachment.attribute_present?('filename')
File.delete("#{FILE_PATH}/#{params[:id]}")
end
@successful = @attachment.destroy
に変更。
これで一応完成。
ajax_scaffold はカスタマイズするには結構な労力がかかることがわかった。(特にビュー)
以上
Comment
vostorg, pefrect!
www
Hello, please, help.
What should i do about this?
Thenks, bro. I am vaiting for answer!!!
Hi Man!. Just one more question. Realy, please, help me.
Is 5 days long enough 2 get alcohol out of your urine for a urine test?
Thenk you. I am vaiting for answer!!!