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 ****

なんだかなぁー、、