Railsで添付ファイルを扱う attachment-fu プラグイン (その1)
社内のシステムを作っていて何かと要望として多いのが、添付ファイルを保存できないかということだ。スキャンしたデータなどをとりあえずサーバーにアップロードしておいて、好きな時にダウンロードできるようにしたいという。うちのような税理士業界では、少しはIT化(死語のような気もするが・・・)が進んだとは言え、デジタル化されていない紙の資料がまだまだ山のように存在するのだ。
Rails で添付ファイルを保存したり、ファイルの内容の検証や加工を行うためのプラグインとして代表的なものに、attachment-fu というものがあった。
手元にあった本「Railsレシピブック 183の技」にも載っていたので、これを参考にやってみた。
環境
今回は例題として、作家(Novelists)のリストがあって、それにその作家の著書の添付ファイルが付くという簡単なアプリにしてみよう。
作家の情報を表現するクラスを「Novelist」とし、それに対応するテーブルを「novelists」とする。
では早速 attachment-fu プラグインをインストールしてみる。
jruby -S script/plugin install http://svn.techno-weenie.net/projects/plugins/attachment_fu/
続いて、添付ファイルのメタデータ(ファイル名、サイズ、Content-Type)を表現するためのクラスとそれに対応するDBのテーブルを作成する。
ここでは「Attachment」という名前にする。(名前は何でもOK)
jruby -S script/generate model Attachment
attachment-fuプラグインでは、この添付ファイルを表現するモデルに以下の4つの属性が必要となる。
1. size (integer・添付ファイルのサイズ)
2. content_type (string。添付ファイルのContent_Type)
3. filename (string・ファイル名)
4. db_file_id (integer・実際の添付ファイルを保存する先のテーブルとの紐付けのためのID)
Attachmentクラスは、作家を表すNovelistクラスを参照し、多対1の関係になっているものとすると、上記4つの属性にもう一つ、このNovelistクラスを参照するための属性として、「novelist_id」も必要となってくるのでこれを追加しよう。
db/migrate/yyyymmddxxxxxx_create_attachments.rb ファイルを編集
class CreateAttachments < ActiveRecord::Migration
def self.up
create_table :attachments do |t|
t.integer :size
t.string :content_type
t.string :filename
t.integer :db_file_id
t.integer :novelist_id
t.timestamps
end
enddef self.down
drop_table :attachments
end
end
次に、作家を表すモデル「Novelist」を生成する。
jruby -S script/generate model Novelist name:string
次に、実際に添付ファイルを保存するテーブルを作成する。先ほど作成した「Attachment」はあくまでもメタデータを保存するものなので、実体はいまから作成する「db_files」というテーブルに保存される。attachment-fu の仕様上、「db_files」という名前は、他の名前に変更すると動かないようだ。
jruby -S script/generate migration create_db_files
db/migrate/yyyymmddxxxxxx_create_db_files.rb ファイルを編集
class CreateDbFiles < ActiveRecord::Migration
def self.up
create_table :db_files do |t|
t.binary :data , :null=>false , :default=>''
t.timestamps
end enddef self.down
drop_table :db_files
end
end
rakeコマンドで、DBとテーブルを生成
rake db:create
rake db:migrate
モデルに添付ファイルを扱う宣言「has_attachment」と、Novelistクラスとの関係を追記する。
app/models/attachment.rb を編集
class Attachment < ActiveRecord::Base
has_attachment
belongs_to :novelistend
さらに、NovelistモデルがAttachmentモデルから参照されているので、
app/models/novelist.rb を編集
class Novelist < ActiveRecord::Base
has_many :attachments
end
ここまででとりあえず添付ファイルを扱う前準備ができたことになる。
ここからは実際にViewとControllerに手を入れていこうと思うが、このサンプルアプリの簡単な仕様を決めておくことにしよう。
1. 作家の一覧画面では作家の名前と添付ファイルが一覧表示され、添付ファイルをクリックしたらダウンロードできる
2. 作家の新規作成画面では、作家の名前と添付ファイルのアップロードが可能
添付ファイルの編集や削除、複数添付ファイルがある場合については次回以降の記事で書こうと思うので、今回はかなりシンプルに、登録と表示というごく簡単な機能だけを紹介する。
Scaffoldを作成しておこう。
jruby -S script/generate scaffold novelist
まずは登録画面(view)から
app/views/novelists/new.html.erb の編集(赤色部が追記したところ)
<h1>New novelist</h1><% form_for(@novelist, :html=>{:multipart=>true}) do |f| %>
<%= f.error_messages %><%= label(:novelist, :name, "作家名") -%>
<br/>
<%= text_field :novelist, :name -%>
<br/>
<br/>
<%= label(:attachment, :uploaded_data, "添付ファイル") -%>
<br/>
<%= file_field :attachment, :uploaded_data %>
<p>
<%= f.submit "Create" %>
</p>
<% end %><%= link_to 'Back', novelists_path %>
※form_forタグのところに、:html=>{:multipart=>true} を追記すると、生成されるhtmlのformタグのところに、「enctype="multipart/form-data"」が入る。
添付ファイルを送る場合は、これが必須となっている。もしこれがないと、添付ファイル名だけ送信されて実際のファイルは送信されない。
実際にはこんなhtmlが生成される
<h1>New novelist</h1> <form id="new_novelist" class="new_novelist" method="post" enctype="multipart/form-data" action="/novelists"> <div style="margin: 0pt; padding: 0pt;"> <input type="hidden" value="d3b456561cd8736cca8c3c5881a4fa860f4b015d" name="authenticity_token"/> </div> <label for="novelist_name">作家名</label> <br/> <input id="novelist_name" type="text" size="30" name="novelist[name]"/> <br/> <br/> <label for="attachment_uploaded_data">添付ファイル</label> <br/> <input id="attachment_uploaded_data" type="file" size="30" name="attachment[uploaded_data]"/> <p> <input id="novelist_submit" type="submit" value="Create" name="commit"/> </p> </form> <a href="/novelists">Back</a>
今度は、いま作った登録画面のフォームデータを受け取るコントローラーの番だ。
app/controllers/novelist_controllers.rb の編集
編集前
# POST /novelists # POST /novelists.xml def create @novelist = Novelist.new(params[:novelist]) respond_to do |format| if @novelist.save flash[:notice] = 'Novelist was successfully created.' format.html { redirect_to(@novelist) } format.xml { render :xml => @novelist, :status => :created, :location => @novelist } else format.html { render :action => "new" } format.xml { render :xml => @novelist.errors, :status => :unprocessable_entity } end end end
編集後
# POST /novelists # POST /novelists.xml def create @novelist = Novelist.new(params[:novelist]) if params[:attachment][:uploaded_data] != "" @attachment = Attachment.new(params[:attachment]) @attachment.novelist = @novelist end respond_to do |format| begin Novelist.transaction do @novelist.save! if params[:attachment][:uploaded_data] != "" @attachment.save! end end flash[:notice] = 'Novelist was successfully created.' format.html { redirect_to(@novelist) } format.xml { render :xml => @novelist, :status => :created, :location => @novelist } rescue format.html { render :action => "new" } format.xml { render :xml => @novelist.errors, :status => :unprocessable_entity } end end end
登録機能ができたので、次は一覧表示画面だ。
作家名と添付ファイル名が出る一覧画面にする。
app/views/index.html.erb を編集(赤色部が追記したところ)
<h1>Listing novelists</h1><table border="1" cellspacing=0>
<tr>
<td>作家名</td>
<td>添付ファイル</td> </tr><% for novelist in @novelists %>
<tr>
<td><%=h novelist.name %></td>
<td>
<% novelist.attachments.each do |a| %>
<%= link_to a.filename, :controller => "novelists",:action=>"download_attachment",:id=>novelist.id %>
<% end %>
</td> <td><%= link_to 'Show', novelist %></td>
<td><%= link_to 'Edit', edit_novelist_path(novelist) %></td>
<td><%= link_to 'Destroy', novelist, :confirm => 'Are you sure?', :method => :delete %></td>
</tr>
<% end %>
</table><br />
<%= link_to 'New novelist', new_novelist_path %>
よし。とりあえず一覧画面、新規登録画面はできたので、アクセスしてみよう。
まだ何も登録されていない一覧画面が表示された
一覧画面の「New novelist」をクリックすると
新規登録画面が表示された
新規登録画面で、作家名を入力し、添付ファイル欄に書籍の画像を選んで「Create」ボタンを押す
無事登録完了。「Back」ボタンを押して一覧画面を見てみよう
作家名と添付ファイルが表示されている
さぁ、今度は一覧画面に表示されている添付ファイル名をクリックするとダウンロードされる機能をつける必要がある。
先ほど、app/views/index.html.erb の中で、クリックされたら AttachmentsControllers の download_attachment メソッドが呼ばれるように書いたので、コントローラーにメソッドを追加する。
app/controllers/novelist_controllers.rb に以下のコードを追記
def download_attachment
@attachment = Novelist.find(params[:id]).attachments.find(:first)
send_data(@attachment.current_data, :type=>@attachment.content_type)
end
もう一度一覧画面に戻って、添付ファイル名をクリックしてみる
プログラムで開いてみる。
添付ファイルが表示された!
今のところ日本語名のファイルだと db_files に書き込まれないという状態だが、半角英数字のファイル名であれば問題ない。
今日はここまでにして、次回は添付ファイルの削除や複数添付ファイルのアップロードなどをしていこう。
Railsのバージョンを固定 rake rails:freeze:gems
Rails 1.2.3 時代に作っていたものをレンタルサーバー上で運用していたところ、レンタルサーバーのRailsが自動でバージョンアップしたため動かなくなっていた。
それでRailsのバージョンを固定する rakeコマンドで固定しようとしてみた。
rake rails:freeze:gems VERSION=1.2.3
しかし以下のエラーが出てうまくいかない。
Freezing to the gems for Rails 1.2.3
rake aborted!
uninitialized constant Gem::GemRunner(See full trace by running task with --trace)
調べたところ、require オプションを付ければ良さそうだったので、もう一度トライ。
rake --require=rubygems/gem_runner rails:freeze:gems VERSION=1.2.3
が、また rake aborted! になってしまう・・・・
どうやら、RAILS_ROOT/vendor/rails という空ディレクトリが出来ていてそれが悪さをしているということなので、それを削除し、再度実行してみたところ成功した。
グリーンカレー
今回は全くSaaS開発には関係のない話です。
今日Railsの開発案件の打ち合わせも兼ねて、私の尊敬する先生ご家族とスタッフの方もまじえてお食事(一部飲み?)に行きました。場所は「屋台屋いんごち」さん。
地元の海の幸をはじめとした料理の数々はどれも美味しく、お酒メニューにもこだわりのウイスキーが並ぶなどかなり充実していました。
さてそんな中、今日は特別に事前に先生からお願いしてもらって、通常メニューには載っていない隠れメニューをいただくことができました!
それは「グリーンカレー」!
マスターはタイが好きでよく行くらしく、本場のタイ料理を勉強されていて、メニューにはたまにしか載らないけど、タイ料理にはすごく精通されているそうです。
ちなみに私は大のカレー好きで、地元をはじめ世界各地のカレーを食べて廻るのを(今や?)ライフワークとしています ^ ^;
私のカレー好きをご存じである先生の粋なはからいで今回運良く「いんごち」さんのタイカレーをいただくことができました。
「グリーンカレー」ですが、色は白っぽい感じ。ココナッツミルクの色ですね。美味しそう・・・(よだれ)
通常「グリーンカレー」というとその名の通り少し緑色をしたものが多いですが、それは青トウガラシの色なのです。いんごちさんのグリーンカレーは緑色はほとんどなく全体的に白いスープでした。青トオガラシは男らしくそのままの姿で1本まるごとのってます(^ ^)
さあ、これを大胆にライスの上にかけると、コリアンダー(パクチー)の香りがしてきてもう気分は I'm in Bangkok now.
早速一番にいただきました。美味しいー!!
香辛料(ハーブかな?)のバランスも絶妙で癖が少なく、口に入れてしばらく噛んで喉を通るときあたりにじわりと広がるトオガラシの辛みが何とも言えません。。。大切りのナスに染みこんだスープが口の中でジュワっと出てきてたまりませんな。。
たまにパクチーが苦手でタイ料理は合わないいう人もいますが、そんな人でもこのタイカレーは大丈夫な気がします。
大人が5人いましたが、気づいたらほとんど私が食べていました。。
美味しいお酒に美味しいカレーを食べていい感じになってたら、肝心のRails案件の話をするのを思いだし、あわてて簡単な設計書をもとに打ち合わせをしました(汗)。なるべく使い勝手のいいシンプルなシステムになるようがんばります。
今日はとても楽しい時間がすごせました。先生ご馳走さまでした!
※通常私は「カリー」と言っているのですが、本ブログ上では「カレー」と表記してます。
Rails テキストボックスの入力補完(サジェスト)機能 text_field_with_auto_complete
Railsで、テキストボックスに入力された値をもとにDBを検索し、入力補完をするような機能をつけてみる。
Google Suggest のようなオートコンプリート機能のイメージだ。
環境
検索してみるといくつかの方法があったが、一番メジャーっぽい text_field_with_auto_complete というのを使ってみることにした。
参考にしたサイトはこちら
http://wiki.rubyonrails.org/rails/pages/How+to+use+text_field_with_auto_complete
http://d.hatena.ne.jp/zariganitosh/20071007
http://blog.livedoor.jp/maru_tak/archives/50606971.html
http://www.thinkit.co.jp/cert/article/0605/2/5/2.htm
今回は、テキストフィールドに入力された値をもとに、会社マスタ(companies)を検索し、会社名(name)と部分一致するリストを出して入力補完を行えるという機能を作ってみよう。
まずは適当なRailsプロジェクト(仮に autocomplete というプロジェクト名にする)を作って、Company クラスのScaffoldを作る。
jruby -S script/generate scaffold Company
auto_complete のプラグインをインストール。
jruby -S script/plugin install http://svn.rubyonrails.org/rails/plugins/auto_complete
CreateCompanies のマイグレーションファイルに会社名(name)のカラムを追記。
class CreateCompanies < ActiveRecord::Migration def self.up create_table :companies do |t| <span style="color:#FF0000;">t.string :name</span> t.timestamps end end def self.down drop_table :companies end end
config/database.yml にhostを追記
(原因はよくわからないが、hostの情報を書いておかないとDB操作ができなかった)
development: adapter: mysql encoding: utf8 database: autocomplete_development username: root password: socket: /var/run/mysqld/mysqld.sock host: 127.0.0.1 test: adapter: mysql encoding: utf8 database: autocomplete_test username: root password: socket: /var/run/mysqld/mysqld.sock host: 127.0.0.1 production: adapter: mysql encoding: utf8 database: autocomplete_production username: root password: socket: /var/run/mysqld/mysqld.sock host: 127.0.0.1
ちなみに database.yml は、共通の記述箇所は以下のようにまとめることができる(内容は、上記database.ymlと同じ。参考までに。)
common: &common adapter: mysql encoding: utf8 username: root password: socket: /var/run/mysqld/mysqld.sock host: 127.0.0.1 development: database: autocomplete_development <<: *common test: database: autocomplete_test <<: *common production: database: autocomplete_production <<: *common
DBをCreateして、マイグレーションファイルを実行
DBとcompaniesテーブルができたので、companiesテーブルにダミーデータを入れておこう。
以下のような簡単なCSVファイルを作って、companies.csv という名前で test/fixtures 配下に保存する。
この時、もともと companies.yml というファイルが存在しているので、org_companies.yml というような適当な名前にリネームしておく。
"id","name","created_at","updated_at"
1,"まるまる建設",,
2,"居酒屋ぺけぺけ",,
3,"いたくない歯科医院",,
4,"きれい美容室",,
5,"ろっくんろーる電器",,
6,"123病院",,
7,"1256総合病院",,
8,"1237777整形外科",,
9,"しかく商事",,
このCSVファイルをrakeコマンドを使ってDBにLoadする。
jruby -S rake db:fixtures:load FIXTURES=companies
必要なJavascriptライブラリをincludeする。
1.prototype.js
2.effects.js
3.controls.js
(app/views/layouts/companies.html.erb のhead部分に以下3ファイルを追加)
<%= javascript_include_tag "prototype" %> <%= javascript_include_tag "effects" %> <%= javascript_include_tag "controls" %>
今回はサンプルなので、index.html.erb にサジェスト機能のついたテキストボックスを配置してみる。
下記1行をerbファイルに追加する。
<%= text_field_with_auto_complete :company, :name %>
companies_controller.rb にメソッドを追加
auto_complete_for :company, :name def auto_complete_for_company_name find_options = { :conditions => [ "name LIKE ?", '%' + params[:company][:name] + '%' ], :limit => 10 } @items = Company.find(:all, find_options) render :partial => 'auto_complete_for_company_name' end
app/views/companies 配下に、_auto_complete_for_company_name.html.erb を追加
<ul> <% @items.each do |item| -%> <li id=<%= item.id %>> <%= item.name -%> </li> <% end -%> </ul>
http://localhost:3000/companies にアクセスしてみる。
動かない・・・。。
なぜだ。調べてみると、Rails 2 以降では、CSRF対策処理にひっかかるので、それをスキップさせる必要があるようだ。
(ちょっとセキュリティ的には問題があると思うが、まぁ今はとりあえずこれで回避しておこう。)
companies_controller.rb に下記1行を追加
skip_before_filter :verify_authenticity_token
すると、、
ただ、なぜか文字列の両端に半角スペースが入ってしまっている。
いろいろやってみると、以下のようにView上のliタグのところを1行で書いてやれば解決した。
<ul> <% @items.each do |item| -%> <li id=<%= item.id %>><%= item.name -%></li> <% end -%> </ul>
あとは、選択された会社のidをコントローラー側に渡してやりたいので、index.html.erb を以下のように変更した。
変更前
<%= text_field_with_auto_complete :company, :name %>
変更後
<%= text_field_with_auto_complete :company, :name,{}, :after_update_element=>"function(element, selectedItem){$('company_id').value = selectedItem.id;}" -%> <%= hidden_field :company, :id %>
これでコントローラー側で params[:company][:id] で会社のidを取得することができる。