2011年9月24日土曜日

Rails3を初歩から学ぶ #23 関連テーブルのコントローラ

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

今回はUserテーブルとMovieテーブルを結ぶFanテーブルに対応するコントローラを作成していきます。仕様としては、映画の一覧から選択すると好きな映画として登録、削除を選ぶと好きな映画の対象から外すというものとします。
インターフェースは映画情報の一覧画面(Moviesコントローラのindexアクション)を使うことにして、Fansコントローラとしてはcreate(Fanテーブルへの追加)とdestroy(Fanテーブルからの削除)のみになります。
> rails generate controller Fans
ではテストを作成していきましょう。

# -*- coding: utf-8 -*-
require 'spec_helper'
describe FansController do
describe "アクセスコントロールの検証" do
it "未認証ユーザーによるcreate要求はサインイン画面へ" do
post :create
response.should redirect_to(new_session_path)
end
it "未認証ユーザーによるdestroy要求はサインイン画面へ" do
delete :destroy, :id => 1
response.should redirect_to(new_session_path)
end
end #アクセスコントロールの検証
describe "POST 'create'" do
before(:each) do
@user = Factory(:user)
@movie = Factory(:movie)
controller.sign_in(@user)
end
it "お気に入り登録できること" do
lambda do
post :create, :fan => { :movie_id => @movie }
response.should be_redirect
end.should change(Fan, :count).by(1)
end
end #POST 'create'
describe "DELETE 'destroy'" do
before(:each) do
@user = Factory(:user)
@movie = Factory(:movie)
controller.sign_in(@user)
@user.favorite_movie!(@movie)
@fan = @user.fans.find_by_movie_id(@movie)
end
it "お気に入り解除できること" do
lambda do
delete :destroy, :id => @fan
response.should be_redirect
end.should change(Fan, :count).by(-1)
end
end #DELETE 'destroy'
end
対応するコードを書いていきます。
まずはルートを定義しましょう。
resources :fans, :only => [:create, :destroy]
createとdestroyアクションしか不要なので「:only」で限定しています。
そしてapp/controller/fans_controller.rbを編集します。

class FansController < ApplicationController
before_filter :authenticate
def create
@movie = Movie.find(params[:fan][:movie_id])
current_user.favorite_movie!(@movie)
redirect_to movies_path
end
def destroy
@movie = Fan.find(params[:id])
current_user.unfavorite_movie!(@movie)
redirect_to movies_path
end
end
これでテストは全てパスできます。
最後にviewをつくって完成です。
映画情報の個別ページ(showアクション)にお気に入り登録/解除するボタンを設置しましょう。


<h1>タイトル:<%= @movie.name %><h1>

<% if current_user.favorite?(@movie) %>
<%= form_for current_user.fans.find_by_movie_id(@movie), :html => { :method => :delete } do |f| %>
<div class="actions"><%= f.submit "お気に入り解除" %></div>
<% end %>

<% else %>

<%= form_for current_user.fans.build(:movie_id => @movie.id) do |f| %>
<div><%= f.hidden_field :movie_id %></div>
<div class="actions"><%= f.submit "お気に入り" %></div>
<% end %>

<% end %>


「favorite?」メソッドの結果に応じてお気に入りに登録するか解除するか、表示するボタンを切り替えています。
「form_for」に指定するリソースはお気に入り登録時はbuildメソッドで生成した(DB保存前の)新しいリソース、解除時は渡された映画情報IDから検索して取得したリソースを指定しています。
登録時はPOSTで映画情報IDを渡したいのでhiddenフィールドで指定しています。

今回までで一通りRails3のおおまかな説明はおしまい!

2011年9月22日木曜日

Rails3を初歩から学ぶ #22 関連の実装

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

前回作成したテストをパスできるよう実装を進めます。
まずはUserモデルを編集します。
validatesをしている辺りに以下の行を追加します。

has_many :fans, :foreign_key => "user_id",
:dependent => :destroy
has_many :movies, :through => :fans
has_manyは1対多関係における1側(多を持つ側)を表します。
Userモデル1つにつき複数のfanモデル、movieモデルを持つことを意味します。
has_manyのパラメータは複数形になります(fans, movies)。
これでuser.fansとするとfanオブジェクトの配列にアクセスできます。
foreign_keyは外部キーを指定します。
「:dependent => :destroy」はUserモデルを削除したときに連動して該当するuser_idのFanテーブルの項目を削除するように指定します。

2行目のhas_many文は「user.movies」とすることでそのユーザーが好きな映画の配列にアクセス可能とするための文です。userとmovieの間は直接ではなくFanテーブルを間に挟んでいます。user_idとmovie_idをフィールドに持つFanテーブルを通じてUserテーブルとMovieテーブルの多対多関係を表しています。
こうすることでuser.moviesとするだけで、そのユーザーの好きな映画情報にアクセスできます。

では次にapp/model/fan.rbを編集します。

class Fan < ActiveRecord::Base
belongs_to :user
belongs_to :movie
validates :user_id, :presence => true
validates :movie_id, :presence => true
end
1対多の多側(相手が1つ)なのでbelongs_toで相手側モデルを指定します。
belongs_toのパラメータは単数になります(user, movie)。
そしてMovieモデルを編集します。
以下の2行を追加します。

has_many :fans
has_many :users, :through => :fans
Userモデルで定義した内容と逆方向です。
これでmovie.usersとすることでその映画を好きだといっているユーザー配列にアクセスできます。
次にuserモデルに好きな映画を登録する機能を追加しましょう。
まずはspec/model/user_spec.rbにテストコードを追加します。
beforeブロックの赤字部分と、以降の検査コードを追加します。
describe "fans" do
before(:each) do
@user = Factory(:user)
@movie = Factory(:movie)
end
〜(省略)〜
it "favorite_movie!メソッドが機能すること" do
@user.should respond_to(:favorite_movie!)
end
it "unfavorite_movie!メソッドが機能すること" do
@user.should respond_to(:unfavorite_movie!)
end
it "favorite?メソッドが機能すること" do
@user.should respond_to(:favorite?)
end
it "追加した映画が登録されていること" do
@user.favorite_movie!(@movie)
@user.movies.should include(@movie)
end
it "削除した映画が登録されていないこと" do
@user.favorite_movie!(@movie)
@user.unfavorite_movie!(@movie)
@user.movies.should_not include(@movie)
end
ここでは好きな映画を登録する「favorite_movie」、解除する「unfavorite_movie」、さらにその映画が好きかどうかを返す「favorite?」メソッドが存在するかどうかといったことなどをチェックしています。
では対応するコードをapp/model/user.rbに定義しましょう。
def favorite_movie!(movie)
fans.create!(:movie_id => movie.id)
end
def unfavorite_movie!(movie)
fans.find_by_movie_id(movie).destroy
end
def pushed?(member)
members.include?(member)
end
「favorite_movie!」ではFanモデルのインスタンスを新たに生成しています。
「unfavorit_movie!」では渡されたmovieのidからfanインスタンスを検索して、該当するfanモデルを削除します。


2011年9月17日土曜日

Rails3を初歩から学ぶ #21 関連モデルの作成

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

今回からはユーザーがお気に入りとして登録する対象となる映画情報を作成していきます。
まずはMovieモデルを用意しましょう。
rails generate model Movie name:string
rake db:migrate
rake db:test:prepare
映画の名前だけをフィールドに持ったテーブルです。
このMovieモデルにおけるvalidatesの実装やテストコードについては省略します。
またコントーラ、ビュー、ルート定義もUserモデルと同じ(パスワードの暗号化が無い分、こちらの方がシンプル)なので説明は省略します。
Userモデルを参考にしながら作ってみてください。

そしてこの段階でUserとMovieというテーブルが出来ました。
ここで例えばuser.moviesとすると、そのuserインスタンスが好きだと言っている映画のタイトルが配列で取れる、なんてことを実現したいとします。
また同時にmovie.usersとすると、そのmovieのことを好きだと言っているユーザーの配列が取れる、ということも実現したいのです。

このようなとき、「userが好きだといっているmovie」を表すテーブルを用意します。
スキーマとしては以下のようになります。
ユーザーIDinteger
映画IDinteger
これを「Fanモデル」としましょう。このFanモデルをUserモデルと、さらにFanモデルとMovieモデルを紐づけます。そうするとFanモデルを経由してUserモデルとMovieモデルが対応付けられます。
まあとにかく、やってみましょう。
最初にFanテーブルをつくります。

>rails generate model Fan user_id:integer movie_id:integer
ユーザーIDと映画IDにはインデックスを設定したいので、生成されたマイグレーションファイルを編集します。db/migrate/XXXXX_create_fans.rb を編集します。
class CreateFans < ActiveRecord::Migration
def self.up
create_table :fans do |t|
t.integer :user_id
t.integer :movie_id
t.timestamps
end
add_index :fans, :user_id
add_index :fans, :movie_id

end
def self.down
drop_table :fans
end
end
赤字の部分を追加します。
「rake db:migrate」でマイグレーションして、「rake db:test:prepare」で検査環境に反映させます。そしてこのfan.rb用の試験コードを作成しましょう。
# -*- coding: utf-8 -*-
require 'spec_helper'
describe Fan do
before(:each) do
@user = Factory(:user)
@movie = Factory(:movie)
@fan = @user.fans.build(:movie_id => @movie.id)
end
it "登録できること" do
@fan.save!
end
describe "Userオブジェクトからのアクセス" do
before(:each) do
@fan.save
end
it "fanインスタンスからuserインスタンスを取得できること" do
@fan.user.should == @user
end
it "fanインスタンスからmovieインスタンスを取得できること" do
@fan.movie.should == @movie
end
end #Userオブジェクトからのアクセス
describe "バリデーションの検証" do
it "user_idが必要" do
@fan.user_id = nil
@fan.should_not be_valid
end
it "movie_idが必要" do
@fan.movie_id = nil
@fan.should_not be_valid
end
end #バリデーションの検証
end


映画情報に関するファクトリメソッドもUserモデルと同じように存在するもとしています。
buildメソッドは保存前のモデルオブジェクトを生成します。
buildメソッドで受け取ったインスタンスの「save!」メソッドを呼び出すことで検証しています。今まで使って来た「should」が使われていませんが、save!メソッドは失敗すると例外が発生するので、このままで問題ありません。
次にuser.moviesといった呼び出し方が出来るかどうかの検査項目をspec/model/user_spec.rbに追加します。
describe "fans" do
before(:each) do
@user = Factory(:user)
end
it "fansメソッドが機能すること" do
@user.should respond_to(:fans)
end
it "moviesメソッドが機能すること" do
@user.should respond_to(:movies)
end
end #fans
user.fansやuser.moviesといった呼び出し方が出来ることを検証しています。
同様にmoviesの検証も追加します。
describe "fans" do
before(:each) do
@movie = Factory(:movie)
end
it "fansメソッドが機能すること" do
@movie.should respond_to(:fans)
end
it "usersメソッドが機能すること" do
@movie.should respond_to(:users)
end
end #recommends
先ほどのUserモデル向けの検査と向きが逆方向になっただけで、ほぼ同じ内容です。
つまりmovie.fansやmovie.usersといった呼び出しの検証です。
この検査をパスするための実装は次回からとりかかります。

2011年9月15日木曜日

Rails3を初歩から学ぶ #20 ユーザー削除

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

一連のユーザーリソースに対するアクションも残すところあと1つ。
今回はユーザー削除に対応するdeleteアクションを実装します。
仕様としてはユーザー情報ページに「退会する」というリンクを設けて、クリックしたら退会するものとします。
ではテストをつくりましょう。

describe "DELETE 'destroy'" do
describe "削除失敗するケースの検証" do
before(:each) do
@user = Factory(:user)
end
it "サインインしていなければサインインページに飛ばす" do
delete :destroy, :id => @user
response.should redirect_to(new_session_path)
end
it "削除対象が異なるユーザーならルートに飛ばす" do
wrong_user = Factory(:user, :name => "Sabrou")
controller.sign_in(wrong_user)

delete :destroy, :id => @user
response.should redirect_to(root_path)
end
end #削除失敗するケースの検証
describe "削除成功の検証" do
before(:each) do
@user = Factory(:user)
controller.sign_in(@user)
end
it "ユーザー数が減っていること" do
lambda do
delete :destroy, :id => @user
end.should change(User, :count).by(-1)
end
it "削除したら新規登録ページに移動する" do
delete :destroy, :id => @user
response.should redirect_to(new_user_path)
end
end #削除成功の検証
end #DELETE 'destroy'
ちょっと長いですが、今まで数回に分けていたものを一度に書いているだけで特に目新しいものはないです。
次にコントローラを実装します。

def destroy
User.find(params[:id]).destroy
flash[:success] = "退会しました"
redirect_to new_user_path
end
この段階でテストはパスします。
ユーザー情報ページに退会用のリンクをつくりましょう。
app/views/layouts/_header.html.erbに以下の一行を追加します。
<li><%= link_to "退会する", current_user, :method => :delete, :confirm => "退会します。よろしいですか?", :title => "#{current_user.name}" %></li>
link_toメソッドのパラメータとして「:confirm」を指定して確認メッセージを表示するようにしています。
これでUserリソースに対する操作は一通り実装しました。
次回からは映画情報のモデルを(ようやく)つくります。

2011年9月13日火曜日

Rails3を初歩から学ぶ #19 情報の更新

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

前回editアクションを実装しましたが、そこで表示されるページで更新ボタンを押したときに呼ばれるのがupdateアクションです。今回はこちらを実装していきます。

describe "PUT 'update'" do
before(:each) do
@user = Factory(:user)
controller.sign_in(@user)
end
describe "更新に失敗するケースの検証" do
before(:each) do
@attr = { :name => "", :password => "", :password_confirmation => ""}
end
it "設定変更ページを再表示すること" do
put :update, :id => @user, :user=> @attr
response.should reder_template('edit')
end
it "設定変更ページのタイトルであること" do
put :update, :id => @user, :user => @attr
response.should have_selector("title", :content => "設定変更")
end
end #更新に失敗するケースの検証
describe"更新に成功するケース" do
before(:each) do
@attr = { :name => "Saburou", :password => "barbaz",
:password_confirmation => "barbaz"}
end
it "ユーザー情報が更新されていること" do
put :update, :id => @user, :user => @attr
@user.reload
@user.name.should == @attr[:name]
end
it "ユーザー個人ページへ遷移すること" do
put :update, :id => @user, :user => @attr
response.should redirect_to(user_path(@user))
end
end #更新に成功するケースの検証
end #PUT 'update'
目新しい部分としてはユーザー情報が更新されたことを確認するさいに「@user.reload」としてDBから読み直しています。読み直した値をつかって設定した内容と等しいことを確認しています。
ではアクションを実装します。
def update
@user = User.find(params[:id])
if @user.update_attributes(params[:user])
flash[:success] = "ユーザー情報を更新しました"
redirect_to @user
else
@title = "設定変更"
render 'edit'
end
end
edit.html.erbの入力フィールドは「user[name]」といった名前がつけられいて、コントローラに渡ってくると「params[:user]」として渡されます。入力された名前にアクセスするには「params[:user][:name]」とします。
ここでは「params[:user]」として入力されたフィールドを全てupdate_attributesに渡しています。
update_attributesでは渡されたパラメータをまとめてDBを更新します。
ここで悪意あるユーザーが不正なパラメータ(例えば admin=trueなど)を指定してもUserモデルでattr_accessibleを指定しているパラメータ以外は更新されません(第4回にやりましたね)。

さて、これでテストはパスします。
が、例えば適当なユーザーでサインインして「http://localhost:3000/users/1/edit」などとサインインユーザー以外のユーザーIDの編集ページを直接指定すると編集画面に遷移できてしまいます。
このままでは自分以外のユーザーに勝手に情報を変更されてしまいます。
自分の編集ページ以外にはアクセスできないようにしましょう。
テストコードは以下の通りです。

describe "自分以外のユーザー情報を更新できないこと" do
before(:each) do
@user = Factory(:user)
@wrong_user = Factory(:user, :name => "Jirou")
controller.sign_in(@wrong_user)
end
it "自分以外のユーザー編集ページにアクセスしたらルートに飛ばす" do
get :edit, :id => @user
response.should redirect_to(root_path)
end
it "自分以外のユーザー情報更新が要求されたらルートに飛ばす" do
put :update, :id => @user, :user => {}
response.should redirect_to(root_path)
end
end #自分以外のユーザー情報を更新できないこと
まず今現在のユーザーとアクセス先のユーザーが一致するかどうかをチェックするヘルパmソッドを定義します。app/helpers/sessions_helper.rbに下記のメソッドを追加します。

def current_user?(user)
user == current_user
end
current_userはcookieからユーザーを取得するメソッドです。渡されたユーザーとcookieを元にDBから取得したユーザーを比較した結果を返します。
次にこのヘルパの呼び出し処理を追加します。app/controllers/users_controller.rbに以下のプライベートメソッドを追加します。

def correct_user
@user = User.find(params[:id])
redirect_to(root_path) unless current_user?(@user)
end
そして以下のフィルタを定義してedit、updateアクションの実行前にユーザーが一致するかをチェックするようにします。
before_filter :correct_user, :only => [:edit, :update]
これでユーザー情報のアップデートはできあがり。

2011年9月11日日曜日

Rails3を初歩から学ぶ#18 エラーメッセージの表示

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

前回先にリンクだけつくってしまったのでユーザー情報の編集ページをつくりましょう。
最終的には映画情報(忘れているかもしれませんが、これはお気に入りの映画を登録するアプリケーションなのでした!)を編集できるようにしますが、とりあえず現時点ではユーザー名とパスワードのみ変更可能とします。
まずはeditアクション用のテストケースです。

describe "GET 'edit'" do
describe "未認証ユーザーの検証" do
it "未認証ならサインインページに遷移すること" do
user = Factory(:user)
get :edit, :id => user
response.should redirect_to(new_session_path)
end
end #未認証ユーザーの検証
describe "認証済みユーザーの検証" do
before(:each) do
@user = Factory(:user)
controller.sign_in(@user)
end
it "アクセスできること" do
get :edit, :id => @user
response.should be_success
end
it "タイトルの検証" do
get :edit, :id => @user
response.should have_selector("title", :content => "設定変更")
end
it "パスワード確認フィールドが存在すること" do
get :edit, :id => @user
response.should have_selector("input[name='user[password_confirmation]'][type='password']")
end #代表してここだけ調べる

end #認証済みユーザーの検証
end #GET edit
フィールド一個しか見てないとか若干の手抜き感がありますが、特に目新しいことは無いのでこのまま進めます。
次にコントローラの実装です。
before_filter :authenticate, :except => [:new, :create]
def edit
@title = "設定変更"
@user = User.find(params[:id])
end

フィルタなんですがこれまでは「:only」で対象となるアクションを指定していましたが、認証が不要なページの方が少ないので「:except」で認証が不要なページのみを指定するようにしています。
次はapp/views/users/edit.html.erbを以下のように編集します。

<h1>設定変更</h1>
<%= form_for(@user) do |f| %>
<%= render 'fields', :f => f %>
<div class="actions">
<%= f.submit "更新" %>
</div>
<% end %>
新規登録時と同じフォームを表示するので部分テンプレート化しました。
new.html.erbも部分テンプレートを呼び出すように変更します。
<h1>ユーザー新規登録</h1>
<%= form_for(@user) do |f| %>
<%= render 'fields', :f => f %>
<div class="actions">
<%= f.submit "新規登録" %>
</div>
<% end %>
さっぱりしました。では部分テンプレートをつくりましょう。
app/views/users/_fields.html.erbです。

<%= render 'shared/error_messages', :object => f.object %>
<div class="field">
<%= f.label :name %><br />
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :password %><br />
<%= f.password_field :password %>
</div>
<div class="field">
<%= f.label :password_confirmation, "Confirmation" %><br />
<%= f.password_field :password_confirmation %>
</div>
2行目以降は元々new.html.erbにあった内容そのものです。
これまでは入力内容がUserモデルで定義した妥当性検証でNGになった場合に何も表示していませんでしたが、なぜエラーになったのかメッセージを表示するようにしました。
エラーの格納方法はRailsで共通なので使い回しできるようapp/views/shared/以下に部分テンプレートを用意します。名前は「_error_messages.html.erb」です。

<% if object.errors.any? %>
<div id="error_explanation">
<h2>登録できませんでした</h2>
<p>以下の内容をご確認ください</p>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>


ここのobjectというのは呼び出し元をたどっていくと@userになります。
Userモデルの検証結果(validatesの実行結果)を@userインスタンス変数がもっていますので、この内容を表示しています。
エラーメッセージは英語です。
日本語化する方法もありますが、ここでは実施しません。
こちらのページに手順が紹介されています。

rails3 エラーメッセージ日本語化のメモ

2011年9月8日木曜日

rubyをバージョンアップしたらrakeが動かない

Rubyを1.9にしたらrakeが動かなくなっていました。。。

なにをやっても下記のようなメッセージが出る。


~/gems/rake-0.9.2/lib/rake/task_arguments.rb:73: warning: already initialized constant EMPTY_TASK_ARGS

~/gems/rake-0.9.2/lib/rake/invocation_chain.rb:49: warning: already initialized constant EMPTY

~/gems/rake-0.9.2/lib/rake/file_utils.rb:10: warning: already initialized constant RUBY

~/gems/rake-0.9.2/lib/rake/file_utils.rb:84: warning: already initialized constant LN_SUPPORTED

~/gems/rake-0.9.2/lib/rake/dsl_definition.rb:143: warning: already initialized constant Commands

~/gems/rake-0.9.2/lib/rake/file_list.rb:44: warning: already initialized constant ARRAY_METHODS

~/gems/rake-0.9.2/lib/rake/file_list.rb:47: warning: already initialized constant MUST_DEFINE

~/gems/rake-0.9.2/lib/rake/file_list.rb:51: warning: already initialized constant MUST_NOT_DEFINE

~/gems/rake-0.9.2/lib/rake/file_list.rb:55: warning: already initialized constant SPECIAL_RETURN

~/gems/rake-0.9.2/lib/rake/file_list.rb:61: warning: already initialized constant DELEGATING_METHODS

~/gems/rake-0.9.2/lib/rake/file_list.rb:364: warning: already initialized constant DEFAULT_IGNORE_PATTERNS

~/gems/rake-0.9.2/lib/rake/file_list.rb:370: warning: already initialized constant DEFAULT_IGNORE_PROCS

~/gems/rake-0.9.2/lib/rake.rb:64: warning: already initialized constant FileList

~/gems/rake-0.9.2/lib/rake.rb:65: warning: already initialized constant RakeFileUtils


いろいろと解決策はあるようですが、一番簡単なのは以下のようにbundle exec経由でrakeを使うこと。

bundle exec rake ****

なんだかなぁー、、


Rails3を初歩から学ぶ #17 インテグレーションテスト

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

未認証状態で「http://localhost:3000/users/」でアクセスするとフィルタで弾かれてサインイン画面へ遷移します。
そしてサインインするとユーザーごとの個人ページ(/users/:id)に遷移します。
これはちょっと不親切ですね。
最初にindexを表示しようとしていたのだから、そこへ行ってあげるのが親切でしょう。
じゃあそうしましょうか、というとき、これはどうやって検証すればよいのでしょうか。
ここで検証したいのは「未認証状態でindex -> サインインページへ遷移 -> サインインしてindexへ」という一連の流れを検証したいのですが今までのRSpecファイルでは対応できそうもありません。

そこで登場するのがインテグレーションテストです。
まずはrailsコマンドでインテグレーションテストを生成します。
rails generate integration_test user_session
generateコマンドにintegration_testを指定するとインテグレーションテストを生成します。ここではuser_sessionという名前で生成しています。
実際に生成されるのは spec/requests/user_session_spec.rb です。早速編集しましょう。

# -*- coding: utf-8 -*-
require 'spec_helper'
describe "サインイン後の遷移先" do
it "サインイン前に要求したページに遷移する" do
user = Factory(:user)
visit users_path
#この段階でサインインページにリダイレクトされているハズ
fill_in :name, :with => user.name
fill_in :password, :with => user.password
click_button
#サインイン前に要求していたindexに遷移すること
response.should render_template('users/index')
end
end
だいたい英文のようになっているので何しているか分かると思います。
それでは実装に入りましょう。まずはアクセス拒否時にアクセス先を記録する処理と、サインイン後にリダイレクト先を決定するヘルパメソッドを追加します。
app/helpers/session_helper.rb に以下のメソッドを追加します。

def deny_access
store_location
redirect_to new_session_path, :notice => "サインインしてください"
end
def redirect_back_or(default)
redirect_to(session[:return_to] || default)
clear_return_to
end
※以下2メソッドはprivateで定義する
def store_location
session[:return_to] = request.fullpath
end
def clear_return_to
session[:return_to] = nil
end

deny_accessメソッドは既に存在していますが赤文字部分を追加します。
このstore_locationメソッドでセッションに要求先URLを保存します。
そしてredirect_back_orメソッドでセッションをチェックして直前にアクセス拒否されていたら拒否されたページにリダイレクトしています。
リダイレクトしたらセッションからアクセス先情報を削除しています。
あとはサインイン成功時のリダイレクト処理をredirect_back_orに置き換えればOKです。
app/controller/sessions_controller.rbのcreateメソッドを以下の赤文字部分のように修正します。

def create
user = User.authenticate(params[:session][:name],
params[:session][:password])
if user.nil?
flash.now[:error] = "サインインできませんでした"
@title = "サインイン"
render 'new'
else
sign_in user
redirect_back_or user
end
end
これまでredirect_toとしていた箇所を先ほど追加したredirect_back_orに置き換えています。これでインテグレーションテストもパスします。
実際に動かして試してみましょう。

2011年9月7日水曜日

ActionMailerをGmail経由で使いたい

RailsのActionMailerでgmailを使いたいのだ。
やり方はこちら。


若干、記事が古いのが気になる。
当たって砕けましょう。

まずRails3だとプラグインをインストールするコマンドが違います。
rails plugin install git://github.com/adamwiggins/gmail_smtp.git
あとは書いてある通りなのだけど、Ruby1.8.7以降を使っている場合は上記記事のコメント欄に書いてある修正が必要になります。
「vendor/plugins/gmail_smtp/lib/smtp_tls.rb」に対してコメント欄の通りの修正を加えます。
check_auth_args user, secret, authtype if user or secret という箇所が最初の方にあるので、、、

if RUBY_VERSION > "1.8.6"
 check_auth_args user, secret
else
 check_auth_args user, secret, authtype if user or secret
end

とします。
これをHerokuにアップデートすればgmail経由で送信できるようになります。
config/environment.rbは手を入れずにOK.


fakerがあるとHerokuでpushできない

なぜかfakerを使っているとHerokuでpushした際に
no such file to load -- faker
などと言われて失敗します。
対処法はこちら

Gemfileでfakerを指定している箇所を「:require => false」としたうえで、sample_data.rakeの「require 'faker'」を削除してやります。
まあfakerは使えなくなりますが、とりあえずpushできるようにはなります。

2011年9月6日火曜日

Rails3を初歩から学ぶ#16 サインアウト

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

前回まででサインインに関する処理が一通り完成したので、今度はサインアウトを実装しましょう。
spec/controller/sessions_controller_spec.rb に以下の検証コードを追加します。

describe "DELETE 'destroy'" do
it "未認証状態になり入り口に戻る" do
user = Factory(:user)
controller.sign_in(user)

delete :destroy
controller.should_not be_signed_in
response.should redirect_to(root_path)
end
end #DELETE 'destroy'
もう目新しいことはありません。
次にsign_inに対応するsign_outをヘルパメソッドとして定義しましょう。
app/helpers/sessions_helper.rbに追加します。

def sign_out
cookies.delete(:remember_token)
end

これだけです。次にdeleteアクションを実装しましょう。
sessions_controller.rbを編集します。
ちなみにusers_controller.rbじゃないです。意味的にこちらのdeleteはユーザーを削除することになるので、実装はいつかそのうちに。

def destroy
sign_out
redirect_to root_path
end
destroyアクションは以前sessions_controller.rbを生成した際に既に空で定義されているハズです。なのでそこに先ほど定義したサインアウトメソッドとリダイレクトの呼び出しを追加するのみです。
アプリケーションの入り口を示す意味で「root_path」を指定していますが、これはまだ未定義なのでconfig/route.rbに定義します。

resources :users
resources :sessions, :only => [:new, :create, :destroy]
match '/sessions/delete', :to => 'sessions#destroy' root :to => 'sessions#new'
赤字の箇所が追加分です。
ここではルートとしてサインインページを指定しています。
この状態で「http://localhost:3000/」とするとサインインページに行って欲しいんですが、public/index.htmlというファイルが存在すると、これが強制的に表示されます。
なのでrmコマンドで削除しておきましょう。
後はどこかにサインアウト用のリンクを追加すれば完成です。
これを期に各ページ共通のヘッダ部分を定義しましょう。
部分テンプレートとして定義します。
app/views/layouts/_header.html.erb を作成して以下のように編集します。
<header>
<nav class="round">
<ul>
<% if signed_in? %>
<li><%= link_to "ユーザー一覧", users_path %></li>
<li><%= link_to "プロフィール", current_user %></li>
<li><%= link_to "設定変更", edit_user_path(current_user) %></li>
<li><%= link_to "サインアウト", sessions_delete_path, :method => :delete %></li>
<% else %>
<li><%= link_to "サインイン", new_session_path %></li>
<% end %>
</ul>
</nav>
</header>
まとめてリンクを設定しました。設定変更のedit_user_pathだけは現時点で該当するアクションを実装していませんが、ついでに定義してしまいました。
こちらは次回作成しましょう。
それでは app/views/layouts/application.html.erb に今作成した部分テンプレートの呼び出しを追加します。

<div class="container">
<%= render 'layouts/header' %>
<section class="round">
<% flash.each do |key, value| %>
<%= content_tag(:div, value, :class => "flash #{key}") %>
<% end %>
</section>
<%= yield %>
</div>
renderメソッドでlayouts/headerを指定しています。これで部分テンプレート「_header.html.erb」が呼ばれます。その他、見た目用に少し手を入れています。CSS未設定なので見た目もへったくれも無いですが。


ここまで出来たらテストを実行してNGが0件であることを確認して、ブラウザで動作を見てみましょう。

ではテストを実行して全てパスすることを確認したら実際に動かして確認してみましょう。

2011年9月5日月曜日

BambooとCedar

Herokuを使ってみようと思ってドキュメントを読んでいたのだけど、Cedarって何よ。
Bambooって何ぞ。
ちんぷんかんぷんじゃないか。
というわけで調べてみました。

CedarというのはBambooの後継でRails3以降を使うならCedarを使わないといけない。
で、Bambooって何よというところで参考は以下。


Heroku上でRailsを動かすプラットフォームという位置づけ。
つまり、、、それは何ですか???
WEBrickみたいのもの?
オフィシャルを当たりましょう。英語だけど。


ここでもHerokuのランタイムだよ、と書いてある。
Java VMか、Dalvikか、という感じと解釈すればいいのかな。



2011年9月4日日曜日

Rails3を初歩から学ぶ #15 部分テンプレート

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

前回までで、ページごとの認証チェックとサインイン処理が完成しました。
今度は登録ユーザーの一覧が見えるindexアクションを実装しましょう。
例によってテストから作成するのですが、ユーザー一覧を検証したいのでユーザー数はある程度欲しいところではあります。
以前導入したFactoryGirlの機能を使えばそれも簡単です。
spec/factories.rb に以下の定義を追加します。

Factory.sequence :name do |n|
"Tarou#{n}"
end
このようにFactory.sequenceメソッドを使って定義すると「Factory.next(:name)」とすることで続々と「Tarou1」「Tarou2」とユーザー名を生成することが出来ます。
では試験コードです。
spec/controller/users_controller_spec.rbに以下のindexアクション用の検証コードを追加します。

describe "GET 'index'" do
describe "未認証ユーザーの検証"do
it "サインインページに飛ばす" do
get :index
response.should redirect_to(new_session_path)
flash[:notice].should =~ /サインインしてください/
end
end #未認証ユーザーの検証
describe"認証済みユーザーの検証" do
before(:each) do
@user = Factory(:user)
controller.sign_in(@user)
second = Factory(:user, :name => "Jirou")
third = Factory(:user, :name => "Saburou")
@users = [@user, second,third]
30.times do
@users << Factory(:user, :name => Factory.next(:name))
end
end
it "アクセスできること" do
get :index
response.should be_success
end
it "タイトル検証" do
get :index
response.should have_selector("title", :content => "ユーザー一覧")
end
it "ユーザー名のリストを表示していること" do
get :index
@users[0..2].each do |user|
response.should have_selector("li", :content => user.name)
end
end
end #認証済みユーザーの検証
end #GET index


特に目新しいものはないんですが、認証済みユーザーの検証をするbeforeブロックで先ほど定義したFactory.next(:user)を使ってユーザー名を自動生成しています。
ではアクションを定義しましょう。

before_filter :authenticate, :only => [:show, :index]
def index
@title = "ユーザー一覧"
@users = User.all
end
前回定義したbefore_filterにindexアクションを追加します。
複数指定するときは配列になります。
indexアクションではUser.allで全ユーザーを取得しています。
次はindexに対応したviewを作成します。app/views/users/index.html.erbファイルを作成した以下のように編集します。

<h1>ユーザー一覧</h1>
<ul class="users">
<%= render @users %>
</ul>
複数存在するユーザーごとに同じタグを適用することになるので、ここでは部分テンプレートを使用しています。renderメソッドに渡した値に応じた部分テンプレートに該当部分が置き換えられます。
通常は「render 'hoge'」などと指定して、対応する「_hoge.html.erb」に置き換えられます。ここではindexアクションで格納したUser配列をまとめて渡しています。こうすると自動的に1つ1つの要素に分解して部分テンプレートを適用してくれます。
このとき対応するテンプレートは「_user.html.erb」となります。部分テンプレートの先頭には「_」がつきます。
ではその_user.html.erbを以下のように作成しましょう。

<li>
<%= link_to user.name, user %>
</li>
link_toメソッドは1つめのパラメータを値に、2つ目のパラメータをリンク先にaタグを生成します。上記の例ではリンク先としてuserを渡していますが、これも例によって「/users/:id」に変換されます。

2011年9月2日金曜日

Rails3を初歩から学ぶ #14 アクセス制限してみよう

この一連のエントリは
Ruby on Rails 3 Tutorial: Learn Rails by Example (Addison-Wesley Professional Ruby Series)
で参考に学んだことを凝縮してお送りしています。

前回まででサインイン処理を実現しました。
が、認証せずに「http://localhost:3000/users/1」とするとアクセスできてしまいます。
認証済みユーザー以外はアクセスして欲しくないページには認証を事前に確認するようにしましょう。
spec/controller/users_controller_spec.rbを編集します。

describe "GET 'show'" do
describe "サインインしていなければアクセス禁止" do
before(:each) do
@user = Factory(:user)
end
it "サインインページにリダイレクトされること" do
get :show, :id => @user
response.should redirect_to(signin_path)
end
end #サインインしていなければアクセス禁止

describe "サインインしていればアクセス可能" do
before(:each) do
@user = Factory(:user)
controller.sign_in(@user)
end
it "should be successful" do
get :show, :id => @user
response.should be_success
end
it "正しいユーザーを表示していること" do
get :show, :id => @user
assigns(:user).should == @user
end
it "タイトルの検証" do
get :show, :id => @user
response.should have_selector("title", :content => @user.name)
end
end #サインインしていればアクセス可能
end #GET 'show'
赤字の部分を追加します。
既存の検証コードはbeforeブロックでサインインするようにしています。
そして今回新たにサインインしていないときはサインイン画面に遷移することを検証しています。では実装しましょう。
まずは認証済みかどうかをチェックして、認証していない場合はサインインページに飛ばす処理ですが、これは色々と使い回しそうなのでヘルパメソッドとして定義しておきます。
app/helpers/sessions_helper.rb に以下の2つのメソッド(public)を追加します。

def authenticate
deny_access unless signed_in?
end
def deny_access
redirect_to new_session_path, :notice => "サインインしてください"
end
signed_in?メソッドは前回実装したものでcookieの情報から認証状態をチェックします。
これでnilが返った場合はdeny_accessメソッドによりサインインページに飛ばしています。
ではこのヘルパを呼び出しましょう。
app/controller/users_controller.rbに以下の行を追加します。
before_filter :authenticate, :only => :show
これでshowアクション実行前に先ほど定義したauthenticateが呼ばれるようになります。
これでテストもパスするようになるので実際にサーバからサインインせずに
「http://localhost:3000/users/1」などとアクセスしてみてください。
サインインページに飛ばされます。

で、この状態で新規登録ページからユーザーを登録してみると、サインページに飛んでしまいます。実はまだユーザー登録時にサインインを処理していないので未認証状態なんですね。それはまあ良しとして問題はテストを実施してもNG0件なってしまうことです。
users_controller_spec.rbにてサインイン時に以下のような検査コードがあります。
response.should redirect_to(user_path(assigns(:user)))
これはサーバからusers/:idへのリダイレクトメッセージが返ることを検証しているもので、確かにこの検証からすればNGにはなりません。今、問題になっているのはリダイレクトしたらその先のbefore_filterで弾かれているからです。
というわけで新規ユーザーを登録したらサインイン済みになっているかどうかを検証するテストを追加しておきましょう。
spec/controller/users_controller_spec.rb の「POST create」の「登録に成功するケースの検証」のブロックに以下の検証コードを追加します。

it "新規登録成功時はサインイン済みとすること" do
post :create, :user => @attr
controller.should be_signed_in
end
RSpecではbool値を返すメソッドに対して「be_***」という検証用メソッドを自動で追加します。ここではsigined_inメソッドがtrueを返すことを期待しています。
このテストコードにより無事、NGが検出されるようになりました。
では対応しましょう。
といってもapp/controller/users_controller.rb のcreateアクションに下記赤字の1行を追加するのみです。


def create
@user = User.new(params[:user])
if @user.save
sign_in @user
flash[:success] = "登録に成功しました"
redirect_to @user
else
@title = "新規登録"
@user.password = ""
@user.password_confirmation =""
render 'new'
end
end
これで新規登録と同時にサインインした状態になるようにできました。