Rails3の form_for で Ajax をやってみた
一番シンプルっぽいパターンをメモ。とりあえず Ajax通信できた、というレベル。
通常の Ajax通信ではコールバックを html か js ファイルに書くと思っていたが、Rails3ではコントローラ側で js を生成してレスポンスを返すイメージらしい。
index.html.erb
- @tunes.each のループの中で form_for を使用しており、第一引数に tuneオブジェクトを設定している
- Ajax なので :remote => true とする
- フォームは簡単にテキストフィールドのみ。初期値を設定しつつ、リターンで変更後の値を Ajax通信として送信する
- レスポンスを "result" のタグに差し込む
<div id="result"></div> <table class="tunes"> <% @tunes.each do |tune| %> <tr> <td><%= link_to tune.name, tune %></td> <td><%= tune.tuning %></td> <td><%= tune.album %></td> <td> <%= form_for tune , :url => {:action => "update_progress", :id => tune.id }, :remote => true do |form| %> <%= form.text_field :progress %> <% end %> </td> </tr> <% end %> </table>
コントローラ
- コントローラ側に Ajax 通信を受け取る action の "update_progress" を追加
- Tune のデータベースを更新し、レスポンスで返す javascriptで使用する @name @updated_progress をメンバにしておく
- render を呼ぶことで対応する js.erb の "update_progress.js.erb" がレスポンスとして返る
def update_progress @tune = Tune.find(params[:id]) @name = @tune.name @updated_progress = params[:tune][:progress].to_i @tune.update_attribute(:progress, @updated_progress) render end
update_progress.js.erb
- レスポンスとして html上の "result" タグに更新された曲名と進捗率を表示する javascript を表示
$("result").update("updated_progress [<%= @name %>] : <%= @updated_progress %>%");
出来てしまえば簡単だけど、この方式を理解するのにすごく時間がかかった‥。きっとコントローラで js を生成しなくても、従来のように html または js 側で:success コールバックを登録する書き方もあるのかもしれない。jQuery なら見つけられた(参考2)が、prototype版も調べてみよう。
「はじめる! Rails3」で勉強中
達人出版会の「はじめる! Rails3」という電子書籍を購入してみました。読みながら例題アプリを作成していくという構成になっていて、Railsの仕組みを1から教えてくれるのでとてもわかりやすい。また、各章の終わりに演習問題がついていて、メインで作成していく例題アプリの他に、それまで習ったことを復習するような形で別線のアプリを作って行くというのが面白い。
http://tatsu-zine.com/books/rails3
この手法は結果的には似たようなアプリを2つ作ることになるんだけど、一方は本書に従ってサンプルソースをひたすら写して流れを掴んでいくのに対し、演習問題では課題だけ与えられて基本的には自分で考えてコードを組んで行く。最初はなるべく本書で習ったことは見ないようにして記憶を頼りにあれこれ書いてみることで、理解できていない部分がより明確になるので自分の理解度を確かめながら一歩ずつ進んで行けるのがいい。うまい作りだなぁ、と思った。
でも、せっかくだから自分なりにやりがいが持てるように一工夫入れてみている。それは演習問題の代わりに自分が興味ある内容に読み替えてアプリを作るというもの。本書だとメインが「タスクリスト」で演習問題が「書籍一覧」になっているが、僕は趣味のギターにちなんで「押尾コータロー曲&チューニング一覧」を後者で作ることにした。そうすることで完成した後にも自分用に使えそうだし、改造していってオリジナルのアプリにしていく楽しみもある。進捗度とか youtube連携とかもやってみたい。
現在ようやく 8章の終盤に差し掛かりデータの追加までいけた。続きが楽しみ。
作りかけのアプリを Heroku に上げてみた。
http://sharp-sunset-796.heroku.com/
git に登録して heroku にデプロイするまでのコマンド
$ git init $ git add . $ git commit $ heroku create $ git push heroku master $ heroku rake db:migrate VERSION=0 #カラムを変更した場合など。始めからやり直したい $ heroku rake db:migrate $ heroku rake db:seed $ heroku open
これまでの疑問備忘録
- 8.2
form.date_select によって年月日を指定できるセレクトボックスを表示しているが、「年」だけを表示することは可能なのだろうか?
- 8.3
<%= link_to "新規作成", [ :new , :task ] %>
ここの :task の意味がわからない‥。前にも出て来ている link_to の第2引数の意味自体があやしい。 :tasks task :task の違いがよくわかっていないんだろう。
2011/03/13 追記
herokuデプロイでハマる
部分テンプレートを適用した後に heroku にデプロイしたら new と edit の操作時にエラーになってしまうという現象でハマった。ローカルではきちんと動くのに heroku 上では上手く行かない。herokuサーバ上でのログを見てみようとしたが、「heroku logs」というコマンド打ってみても「This feature is not supported by your client version.」というメッセージが出るだけ。新しい環境ではこういう場合どうしていいのかわからないのが辛いところ。
結局解決したのだけど、単に部分テンプレートとして分割した _form.html.erb を git に登録しわすれていたというのが原因だった。ローカルで動くのに herokuでエラーになる場合はまず git への登録漏れを疑おう。
カラム追加でハマる
一度作成したモデルへカラムを追加する方法でどうやればよいのか悩んだ。既に存在する 「db/migrate/20110309094831_create_tunes.rb」に新たなカラムを書き込んで、「 rake db:migrate 」をしても追加されない。新たなマイグレーションファイルを追加する方法もどこかに書いてあったけど、そこまで大げさじゃないしなぁ。
結局以下の方法で、いちどDBを初期状態に戻してから再度 migrate することで変更を反映させることができた。
$ rake db:migrate VERSION=0 $ rake db:migrate
当然データも無くなってしまったので db/seeds.rb に追加分カラムのデータも書き込んで「rake db:seed」を実行する必要があった。DBの内容を維持したままカラムを追加する方法も調べておく必要があるなぁ。herokuへデプロイ後も同様の操作が必要なので注意。
札幌C++勉強会#1に行って来た
C++の勉強会があるということで思い切って行ってみた。内容は東京で行われている Boost勉強会の様子をみんなでustで鑑賞するというもの。内容はかなりレベルが高かった‥。各発表の合間に勉強会に来ていた方々といろいろお話できてよかった。買ってからスマートポインタと bindの章くらいしか読んでいなかった boost本の未知のページを読むきっかけにもなってとてもよかった。
ちょっと後半体調が悪くなったけれど、夜の部の懇親会にも参加させてもらい、非常に有意義な時間を過ごさせていただきました。Boostの話はもちろん、カルネージハートの話で盛り上がったりととても楽しかった。みなさん本当に勉強熱心だなぁ。
Mac に boost を入れる
パソコンを持って行ったので無線LANに繋がせてもらい、勉強会中はあれこれ調べながらかつtwitterのTLを追いかけながら発表を聞けたのはよかった。途中、どうしても Boostのサンプルを動かしたくなって MacPort でインストールしてみた。
$ sudo port install boost
- .bashrc に以下を追加
export PATH=/opt/local/bin:/opt/local/sbin/:$PATH export MANPATH=/opt/local/man:$MANPATH export LIBRARY_PATH=/opt/local/lib:$LIBRARY_PATH export LD_LIBRARY_PATH=/opt/local/lib:$LD_LIBRARY_PATH export C_INCLUDE_PATH=/opt/local/include:$C_INCLUDE_PATH export CPLUS_INCLUDE_PATH=/opt/local/include:$CPLUS_INCLUDE_PATH export DYLD_FALLBACK_LIBRARY_PATH=/opt/local/lib export BOOST_ROOT=/opt/local/include/boost:$BOOST_ROOT
発表の中で出て来ていた tuple や optional のサンプルを Boost本を読みながら動かしてみた。やっぱり理解するには動かすのが一番だと思った。
tuple と optional のサンプル
- tuple のサンプル
#include <iostream> #include <string> #include "boost/tuple/tuple.hpp" using namespace std; boost::tuple<int, double, std::string> getTriple(void){ return boost::make_tuple( 100, 2.45, "関数から" ); } void printTriple(boost::tuple<int, double, std::string> &triple){ cout << triple.get<0>() << endl; cout << triple.get<1>() << endl; cout << triple.get<2>() << endl; } int main() { boost::tuple<int, double, std::string> triple(40,3.14,"すごい"); boost::tuple<int, double, std::string> triple2 = getTriple(); printTriple( triple ); printTriple( triple2 ); return 0; }
戻りを複数個返したい時とかに凄く使えそう。これはいい。
- optional のサンプル
#include <iostream> #include <string> #include "boost/optional.hpp" using namespace std; using namespace boost; // √x を越えない最大の整数を返す関数 // ただし、xが負の数の時は、「不正です」という値を返す optional<int> sqrt( int x ) { if( x < 0 ) return optional<int>(); int i; for(i=0; i*i<=x; ++i) {} return optional<int>(i-1); } int main() { for(int j=-5; j<=5; ++j) { optional<int> x = sqrt(j); if( x ) // 正しいint値を保持しているかどうかチェック cout << *x << endl; else cout << "invalid" << endl; } return 0; }
intの戻り値を欲しい関数を作るときに、エラー時の値を別途返す必要がなくなる。戻り値にエラーの意味を含めるとかいう無理矢理な仕様を作らなく済むのがいい。戻り値を if で判定して通ったら正しい値を取得できているというロジックを組むことができる。
同じようなことを shared_ptr を使うことでも可能だけど、optional の場合は動的メモリを取る訳ではないから処理が軽いという。
鑑賞会メモ
東京で発表している方々はC++界隈では結構すごい人達らしい。知らない用語が飛び交っていて、boost本とにらめっこしながら追いついて行くのがやっとだった。半分くらいはよくわからなかったけど、印象に残ったことをメモ。
- Fusionの話
- tuple が出て来てそれを調べている間に終わってしまった‥
- オーディオプログラミング(OpenAL)
- サウンド事情の抽象化する。ドップラー効果、音量変更、ピッチ変更、ストリーミング再生などの機能がある。クロスプラットフォームでいろんな環境で動くらしい。
- C++をDisる
- C++のいけてないところをひたすら言っていた。これだけ言えるって相当使い込んでいるんだろうなぁ。
- テンプレート関数。forall じゃない。Tのメンバを呼ぶ構文が書けるから結局入力クラスの型が制限されてしまう。v.foo()。それを制限する方法がない。
- テンプレートを駆使すると分割コンパイルできなくてコンパイル時間がとても長くなる。cppを一つにまとめるとかいうテクニックもあり。
- テンプレートでusing namespace できない。ヘッダではしちゃいけないらしい。
- テンプレートはドキュメント化しずらい。型に意味を持たすのが困難。名前で表現する手もあるが、継承関係とかを指定できない。
- ラムダ式がきもい
- Boost.Proto
- Expression Template
- 結局よくわからず。後で調べる。
- 例外を改善する
- lexical_cast で文字列から数字へキャストするのにいちいち例外投げるのしんどい。
- boost::optionalを使う。 if でチェックして * で参照する。実行時メモリ確保しなくて済む。生ポインタを返すこともできるが、受け取り側で解放しなくてはならない。そのため、shard_ptr を使うことも考えられるが、オーバースペック。
- shared_ptrについて
- カスタムデリーターを有効につかおう。所有者いなくなったら呼ばれるデリーターを自前で用意する。
- ハンドルのクローズで使う。例外時に自前でクローズするの無理な場合にカスタムデリーターにやらせる。
- 保持はするが責任を持たないレガシー構造体のメンバの所有権をレガシーデリーターで持つ。
- Nullデリーター。誰かが管理しているリソースや、ポインタのふりをしているだけのデータの場合は Nullデリーターで「何もしない」。
- void* を含む構造体。バイナリオブジェクト(Blob)の構造体にshared_ptr
を追加して create時に関連付けを行う。
- TR18015:2006を読む
- Cキャストは使うな
- ネームスペース使え
- 仮想関数にしても遅くなるとは限らない
- ゲーム開発
- EASTL。ゲームに特化したSTL
- スマートリンカ。template で使ってないメソッドはリンクしないので、コードの肥大化を防ぐ
その他参加メンバーから聞いた情報など
- twitterタグ
- #boostjp
- C++0xとは、仕様策定中の次バージョン。いろいろ便利な機能が乗っているらしい。
- lua
- スクリプト言語
- ゲームとかで使われていて結構人気があるらしい。おもわず MacPortでインストールしてしまった。
- http://wiki.livedoor.jp/holynightlovers/
- boost サイト
- shared_ptr & weak_ptr のわかりやすいスライド
- プログラミングの魔導書
- http://longgate.co.jp/products.html
- 今はPDF販売のみらしい。面白そうなことがたくさん書かれていた。
それにしても、東京の Boost勉強会の参加者は 100名を超えていたらしく、懇親会も50名以上参加でキャンセル待ちが出ていたという‥。どんだけ Boost盛り上がっているんだ‥‥、東京恐るべし。
Railsでテストを書いてみる(Rspec:Controller編)
コントローラもテストしてみる。
- ページにアクセスしてサクセスが返ることと、期待するテンプレートを表示することを確認するシンプルなケース
- 画面にアクセスするのは get :アクション名
- 成功が返るのは response.should be_success
- テンプレートの表示判定は response.should render_template("XXXX")
describe '登録画面にアクセスしたら' do before do get :add_index end it 'サクセスであること' do response.should be_success end it '登録画面を表示すること' do response.should render_template("tunes/add") end end
- フォームからデータを送信したケースのテスト
- post でアクションに対してパラメータを送ったことにする
- データが登録されたことを直接DBを見て判定している。
- リダイレクトの判定と、リダイレクト先を判定している
describe '登録画面で曲を登録したら' do before do post :add_tune,{ :name => "Believe", :album => "Eternal Chain", :tuning => "CGDGBD"} end it '曲データが登録されること' do tune = Tune.find(:all, :conditions=>[ "name=?", 'Believe']) tune.length.should == 1 tune[0].name.should == "Believe" tune[0].album.should == "Eternal Chain" tune[0].progress.should == 0 end it '曲一覧画面を表示すること' do response.should be_redirect response.should redirect_to(:action => 'list') end end
直接DBの値を見ているけれど、DB登録したかどうかの判定は一枚モデルに登録用のラッパーをかませてそれを mock で判定したほうが良いのだろうか。まだよくわからない。
- DBからデータを読み出して表示するページのテスト
- mock を使って DB読み出ししたかを判定している。
- mock の後に get しないとダメっぽい。before でまとめて get できないか。
describe '曲一覧画面を表示した場合' do before do # get :list end it 'サクセスであること' do get :list response.should be_success end it 'DBから全曲を読み出すこと' do Tune.should_receive(:find).with(:all) get :list end end
mock が非常に使いやすい。上の例だと list アクション内で Tune.find(:all)しているかどうかを判定している
- テスト対象のコードはこんな感じになった。意外とスカスカ
class TunesController < ApplicationController def add_index end def add_tune new_tune = Tune.new new_tune.name = params[:name] new_tune.album = params[:album] new_tune.tuning = params[:tuning] new_tune.progress = 0 new_tune.save redirect_to :action => "list" end def list @tunes = Tune.find(:all) end end
add_tune のアクション内で直接DB登録しているけれど、Slim3だったらサービスクラスに当たる登録用のラッパーをかませた方がいいような気がする。
- それと、rake で spec の結果をいい感じの表示にする方法がわからない。 いろいろ調べると rake spec:doc かと思うんだけど、エラーになってしまうんだよなぁ‥。
$ rake spec:doc (in /Users/hoge/rails_work/practice) rake aborted! Don't know how to build task 'spec:doc' (See full trace by running task with --trace) $ rake (in /Users/hoge/rails_work/practice) /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -S bundle exec rspec ./spec/controllers/tunes_controller_spec.rb ./spec/helpers/tunes_helper_spec.rb ./spec/models/tune_spec.rb ......... Finished in 0.16196 seconds 9 examples, 0 failures
Railsでテストを書いてみる(Rspec編)
rspec 環境設定
Rails3.0.3に rspecをインストールするのに少しハマったのでメモ
- gem のアップデート
$ sudo gem update --system $ gem -v 1.5.2
- activesupport のアップグレード
$ sudo gem update activesupport $ gem list | grep active activemodel (3.0.4, 3.0.3) activerecord (3.0.3, 2.3.5, 2.2.2, 1.15.6) activeresource (3.0.3, 2.3.5, 2.2.2) activesupport (3.0.4, 3.0.3, 2.3.5, 2.2.2, 1.4.4)
$ sudo gem install rspec $ sudo gem install rspec-rails $ gem list | grep rspec rspec (2.5.0) rspec-core (2.5.1) rspec-expectations (2.5.0) rspec-mocks (2.5.0) rspec-rails (2.5.0)
- hoe のインストール
- 何に使うかよくわかってないけど必要との記事をどこかで見かけた。
$ sudo gem install hoe $ gem list | grep hoe hoe (2.9.1)
- bundler のアップデート
- bundle install を実行したらエラーになったのでアップデートする
$ sudo gem update bundler $ gem list | grep bundle bundler (1.0.10, 1.0.7)
- rspec を generate したらエラーが。Gemfile を変更しないといけないらしい
$ rails g rspec:install
Could not find generator rspec:install.
source 'http://rubygems.org' gem 'rails', '3.0.3' gem 'mysql2' group :development do gem 'rspec-rails', '2.5.0' end group :test do gem 'rspec', '2.5.0' end
$ rails g rspec:install create .rspec create spec create spec/spec_helper.rb
行けた!
- モデルに対する spec を生成
$ rails g rspec:model tune create spec/models/tune_spec.rb
- spec の実行
$ rake spec (in /Users/hoge/rails_work/practice) /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -S bundle exec rspec ./spec/models/tune_spec.rb * Pending: Tune add some examples to (or delete) /Users/hoge/rails_work/practice/spec/models/tune_spec.rb # Not Yet Implemented # ./spec/models/tune_spec.rb:4 Finished in 0.00024 seconds 1 example, 0 failures, 1 pending
とりあえず空テストの実行まで完了!
テストコードの記述
- fixture を入れるには spec/fixtures に yml データを作成する
$ vi spec/fixtures/tunes.yml
one: name: splash album: Dramatic tuning: DADGAD progress: 100 # column: value # two: name: 翼 album: Be Happy tuning: DADGAD progress: 100
- spec ファイルには fixtures :tunes を書くとテストデータが挿入される
$ vi spec/models/tune_spec.rb
require 'spec_helper' describe Tune do fixtures :tunes describe "DBにあらかじめデータが登録されている場合" do it "DB内のデータを全て読み出せる" do tune = Tune.find(:all) tune.length.should == 2 end end describe "新たに曲データを追加した場合" do before do tune = Tune.new tune.name = "sun dance" tune.album = "Dramatic" tune.tuning = "EAEF#BE" tune.progress = 100 tune.save end it "データ数が増加している" do tune = Tune.find(:all) tune.length.should == 3 end it "追加したデータを読み出せる" do tune = Tune.find(:all, :conditions=>[ "name=?", 'sun dance']) tune[0].name.should == "sun dance" end end end
- model のテストは何を書いたらいいのかわからない。上だと、ActiveRecordのテストをしているような感覚になる。
Railsでテストを書いてみる(UnitTest編)
前準備の環境構築
改めておさらいしつつ。Rails3.0.3
- mysql でアプリを生成
$ rails new practice -d mysql
- DB生成
$ cd practice
$ rake db:create:all
- DB確認
$ mysql5 -u root -p mysql> show databases; +----------------------+ | Database | +----------------------+ | information_schema | | mysql | | practice_development | | practice_production | | practice_test | | sandbox | | test | +----------------------+ 7 rows in set (0.00 sec)
-
- practice_XXXX が3つ出来てればOK
- モデルを生成
$ rails generate model tune invoke active_record create db/migrate/20110216080756_create_tunes.rb create app/models/tune.rb invoke test_unit create test/unit/tune_test.rb create test/fixtures/tunes.yml
- モデルにカラムを追加
$ vi db/migrate/20110216080756_create_tunes.rb
class CreateTunes < ActiveRecord::Migration def self.up create_table :tunes do |t| t.string :name #add t.string :album #add t.string :tuning #add t.integer :progress #add t.timestamps end end def self.down drop_table :tunes end end
- テスト実行
$ rake test (in /Users/hoge/rails_work/practice) Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader Started . Finished in 0.034337 seconds. 1 tests, 1 assertions, 0 failures, 0 errors
とりあえずモデルのテスト環境は揃った。
テストデータの挿入
- yml ファイルにテストデータを設定できる。
- 試しに2つ適当に入れてみる
$ vi test/fixtures/tunes.yml
one: name: splash album: Dramatic tuning: DADGAD progress: 100 two: name: 翼 album: Be Happy tuning: DADGAD progress: 10
テストコードの記述
- さっき設定したテストデータの数と、テストデータの内容を見てみる
$ vi test/unit/tune_test.rb
require 'test_helper' class TuneTest < ActiveSupport::TestCase # テストデータの数を判定 test "test_datas_len_is_2" do tune = Tune.find(:all) assert_equal 2 ,tune.length end # 固有のテストデータの存在を判定 test "test_datas_splash_tuning_is_DADGAD" do tune = Tune.find(:all,:conditions=>["name=?","splash"])[0] assert_equal "DADGAD" ,tune.tuning end end
cssでグラデーションをつける
今まで li要素などにグラデーションをつけるには小さいグラデーション画像を作って background-image に設定していたけど、cssでもグラデーションを付けられることがわかったのでやってみる。
cssの方が画像読み込みの時間が省略できるし、画像読み込み時のぱらぱらした遅延描画がなくなるのがいい。最初からグラデーションされた画面を表示できる。
- 変更前。ちっちゃい画像をリピートで背景に設定していた
background-image: url("/img/todo/doing_gra.jpg"); background-position: center bottom; background-repeat: repeat;
- 変更後。様々なブラウザへの対応を書かないといけないけれど、画像はいらなくなる。
/* For WebKit (Safari, Google Chrome etc) */ background: -webkit-gradient(linear, left top, left bottom, from(#e0f0f6), to(#90c0c6)); /* For Mozilla/Gecko (Firefox etc) */ background: -moz-linear-gradient(top, #e0f0f6, #90c0c6); /* For Internet Explorer 5.5 - 7 */ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#e0f0f6, endColorstr=#90c0c6); /* For Internet Explorer 8 */ -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#FFe0f0f6, endColorstr=#FF90c0c6)";
最大のメリットは、li要素が縦に長くなった場合に画像の場合は縦方向へのリピート表示でグラデーションが段々になってみっともなくなるけれど、cssではいくら伸ばしてもグラデーションを維持してくれる。こういう些細な表示って使う側のモチベーションに多少影響するから結構重要だと思った。
ただ、IE6 での表示がうまくいかない。なぜだろう‥。
参考
- CSSでグラデーションを実現する方法、主要ブラウザ全対応