2011年8月31日水曜日

Rails3を初歩から学ぶ #13 認証情報を保持する

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

前回は認証処理を実装しましたがセッションをまたぐことが出来ていないので大変に不便です。そこでCookieにセッション情報を保存するようにしてやりましょう。
Cookieで保持する情報はユーザー入力を介さないフィールドであるidとsaltを使います。
そしてCookieで保持しているidとsaltから該当するUserオブジェクトを取得するメソッドを用意します。
app/model/user.rb に以下のpublicメソッドを追加します。

def self.authenticate_with_salt(id, cookie_salt)
user = find_by_id(id)
(user && user.salt == cookie_salt) ? user :nil
end
渡されたidをキーにUserオブジェクトを取得し、渡されたsaltと比較します。
ではcookieに書き込む側と読み込む側を実装します。
app/helpers/sessions_helper.rbに記述します。

module SessionsHelper
def sign_in(user)
cookies.permanent.signed[:remember_token] = [user.id,user.salt]
end
def current_user
User.authenticate_with_salt(*remember_token)
end
def signed_in?
!current_user.nil?
end
private

def remember_token
cookies.signed[:remember_token] || [nil, nil]
end
end


sign_inメソッドでは渡されたuserオブジェクトのidとsaltをcookieに保存しています。
現在アクセスしているユーザーを参照するcurrent_userメソッドではまずremember_tokenメソッドでcookieで保存している情報(idとsalt)を取得します。そして取得したidとsaltを先ほど実装したauthenticate_with_saltメソッドで認証します。パラメータに「*」がついていますが、これは配列を分解して渡すことを意味します(ここでは[id, salt]という配列を「id」「salt」という2つの変数として渡す)。
そしてauthenticate_with_saltの認証結果に応じた結果(成功=userオブジェクト、失敗=nil)を返します。

ではサインインのタイミングでcookieに値を保存するようコントローラに追記しましょう。
app/controller/sessions_controllerに下記の赤字部分を追加します。

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_to user
end
end
認証成功時にヘルパメソッドを通じてcookieに値を保存します。
しかしまだこのままではコントローラからヘルパメソッドを呼ぶことができません。
アプリケーション共通で使えるように app/controller/application_controller.rb に下記の赤字部分を追記します。


class ApplicationController < ActionController::Base
protect_from_forgery
include SessionsHelper
end
これでどこからでもSessionsHelperに定義したヘルパメソッドが呼べるようになりました。ではテストを実行してみましょう。
遂に0件になったハズです。

2011年8月29日月曜日

Rails3を初歩から学ぶ #12 セッション管理(実装編)

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

今回からセッション管理を実装していきます。
Userモデルにはnameとencrypted_passwordというフィールドを用意していますが、今までDBにはnameしか保存していません。
新規登録時にはpasswordを入力してもらっていますが、これは暗号化前の値であってDBに保存していません。なので、まずは新規登録時にpasswordを暗号化してencrypted_passwordに保存するようにしましょう。
app/model/user.rbを編集します。
require 'digest'
SHAハッシュを使いたいのでdigestをrequireします。
そしてクラス宣言後のvalidatesなどが並んでいる箇所に
before_save :encrypt_password
とフィルタを追加します。
before_saveはsaveする前に呼び出されるフィルタです。ここではencrypt_passwordというプライベートメソッドを呼び出しています。ここで暗号化してから保存するようになります。ではencrypt_password(とその周辺)を実装します。

private
def encrypt_password
self.salt = make_salt if new_record?
self.encrypted_password= encrypt(password)
end
def encrypt(string)
secure_hash("#{salt}--#{string}")
end
def make_salt
secure_hash("#{Time.now.utc}--#{password}")
end
def secure_hash(string)
Digest::SHA2.hexdigest(string)
end
まずencrypt_passwordではnew_record?メソッドを使ってDBに保存済みであるかどうかを確認します。新規登録時はUserモデルのインスタンスを生成してsaveメソッドを呼ぶ前にここに到達するのでnew_record?はtrueを返します。
new_record?がtrueを返したときはmake_saltメソッドでsaltを生成します。
以前説明したようにパスワードのハッシュを算出する際にはsaltと呼ばれるユーザー毎に異なる値を生成して付加しています。
ここではパスワードと現在時刻を組み合わせたものにSHAハッシュをかけてsaltを生成しています。
saltが用意できたらencryptメソッドでユーザーから渡されたパスワードを暗号化します。
encryptでは先ほど生成したsaltとユーザーから渡されたパスワードを組み合わせてSHAハッシュをかけています。

ここまででもう一度今の段階でrspecを実施しておきましょう。エラーの数が増えていなければ今回の修正が特に影響していないことが分かります(サインイン処理がまだ未実装なのでエラーは出ています)。
ではサインイン処理をごそっと実装しましょう。
まずはapp/mode/user.rbに以下のメソッドを追加します(publicなので先ほど編集したprivateよりも上の位置に追加するかpublicと明示して追加します)。

def has_password?(submitted_password)
encrypted_password == encrypt(submitted_password)
end
def self.authenticate(name, submitted_password)
user = find_by_name(name)
return nil if user.nil?
return user if user.has_password?(submitted_password)
end
has_password?メソッドでは渡されたパスワード(ユーザーが入力したもの)を暗号化して、それがインスタンス変数(DBに保存されている)パスワードと一致することを確認しています。
これは後続のクラスメソッド「authenticate」から呼ばれます。authenticateメソッドではユーザーが入力したユーザー名とパスワードを受け取りユーザー名をキーにUserインスタンスを取得します。そして取得したUserインスタンスのパスワードとユーザー入力パスワードが一致するかをhas_password?メソッドを通じて検証しています。検証失敗時はnilを返します。

次にコントローラのcreateアクションを実装します。
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
redirect_to user
end
end
サインインページからのパラメータがparams[:session]に格納されているので、ユーザー名とパスワードを取り出して先ほど実装したUser.authenticateに渡して認証します。
失敗するとnilが返ってくるのでflash.nowにエラーの旨を格納してサインインページを再描画しています。
認証成功時はユーザーごとのページ(users/:id)にリダイレクトします。

この段階でテストを実行するとNGが1件のみとなっているハズです。
このNGはcurrent_userが未定義であることに起因しています。
current_userメソッドの役割は今現在のUserインスタンスを取得して、サインイン状態をチェックするための窓口となるものです。
しかし今回の実装ではサインインページからcreateアクション実行時の一瞬だけ認証を実行してどこにも認証済みであるかどうかの情報を保持していません。
というわけでCookieに認証情報を書き込んでやる必要があるのですが、これは次回に。

2011年8月27日土曜日

Rails3を初歩から学ぶ #11 セッション管理(RSpec編)

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

前回サインインページをつくったので対応するサインイン処理をつくります。
createアクションがそれに該当します。
まずはテストからですが、ここで検証したいのは以下の点です。

「サインインに失敗するケース」
・サインイン画面を再表示すること
・タイトルの検証
・サインインに失敗した旨のメッセージを表示すること

「サインインに成功するケース」
・正しいユーザー名/パウワードでサインインできること
・ユーザー個別のページに遷移すること

ここで問題となるのが「サインインできたかどうか」をどうやって判定するかです。
そのために必要なのは「操作中のユーザーのインスタンス」を参照して、そのインスタンスが「サインイン済みであること」のチェックが出来ることです。
それぞれを可能にするヘルパメソッド「currenjt_user」「signed_in?」が定義されることを想定してテストコードを書いてみましょう。

describe "POST 'create'" do
describe "サインインに失敗するケース" do
before(:each) do
@attr = { :name => "Yam Cha", :password => "invalid" }
end
it "サインイン画面を再表示すること" do
post :create, :session => @attr
response.should render_template('new')
end
it "再表示した画面のタイトル検証" do
post :create, :session => @attr
response.should have_selector("title", :content => "サインイン")
end
it "サインイン失敗した旨を表示する" do
post :create, :session => @attr
flash.now[:error].should =~ /サインインできませんでした/
end
end #サインインに失敗するケース
describe "サインインに成功するケース" do
before(:each) do
@user = Factory(:user)
@attr = { :name => @user.name, :password => @user.password }
end
it "正しいユーザー名/パスワードでサインインできること" do
post :create, :session => @attr
controller.current_user.should == @user
controller.should be_signed_in
end
it "サインインに成功したらユーザー情報ページへ遷移する" do
post :create, :session => @attr
response.should redirect_to(user_path(@user))
end
end #サインインに成功するケース
end # POST 'create'
あまり目新しいものはありませんが1つだけ。
サインイン失敗時のメッセージ表示確認として「flash.now[:error]」を参照しています。
以前登場したときは「flash[:success]」のように直接アクセスしていたのに今回は「now」を経由しています。
なんでしょうか。

これは以前説明した「redirect_to」と「render」の違い、つまりアクションを経由するか否かに関係します。
新規登録時の処理では以下のようになります。

1. Usersコントローラのcreateアクションでflashに値をセットしてredirect_to
2. Usesコントローラのshowアクションを実行
3. Usersビューのshow.html.erbからHTMLを生成(ここでflash表示&クリア)

一方、今回のサインイン失敗時の流れは以下の通りです。

1. Sessionsコントローラのcreateアクションでflashに値をセットしてrender
2. Sessionsビューのnew.html.erbからHTMLを生成(ここでflash表示)

つまり間にアクションを挟みません。flashはアクションを一度またいで値を保持する機能なので、後者のケースでは2終了段階でflashはクリアされていません。
次に何らかのアクションを実行すると再度同じflashが表示されてしまいます。
このような時に「flash.now」をnowを経由することで一度表示した段階でクリアされるよう仕向けることが出来ます。

当然ですが、今の段階でRSpecを実行するとエラーになります。特にcurrent_userやsigned_in?ではNoMethodErrorになりますが、これらは次回からつくっていきましょう。

2011年8月26日金曜日

Rails3を初歩から学ぶ #10 サインイン画面をつくる

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

ユーザーを新規登録する処理まで完成しましたので、今回から登録済みユーザーを認証する部分をつくってきましょう。セッション管理用に新たなコントローラを用意します。既存のUserモデルを参照するのでモデルは新たにつくる必要はありません。
rails generate controller Sessions new create destroy
まとめてnew、create、destroyアクションも生成しておきます。
それぞれサインイン画面の表示、セッションの生成(サインイン)と破棄(サインアウト)に対応しています。
RSpec用のテストファイルも同時に生成されていますが、spec/views/sessions/の下は必要ないので削除してしまいましょう。
それではルートを定義しておきましょう。
new、create、destroyそれぞれに対するgetが定義されているので削除しておいて、代わりに以下のように定義します。
resources :sessions, :only => [:new, :create, :destroy]
「resources」に渡すとindex,create,new,edit,etc....と一式自動で定義されますが、今回のsessionsコントローラに対してはサインイン画面を表示するnewとセッションを生成するcreateとセッションを破棄するdestroyのみが必要なので「:only」オプションで指定します。
それでは生成されたコントローラに対してテストコードを書いていきます。

# -*- coding: utf-8 -*-
require 'spec_helper'
describe SessionsController do
render_views
describe "GET 'new'" do
it "should be successful" do
get 'new'
response.should be_success
end
it "タイトルの検証" do
get :new
response.should have_selector("title", :content => "サインイン")
end
it "ユーザー名入力フィールドがあること" do
get :new
response.should have_selector("input[name='session[name]'][type='text']")
end
it "パスワード入力フィールドがあること" do
get :new
response.should have_selector("input[name='session[password]']
[type='password']")
end
end #GET 'new'
end
Usersコントローラのnewアクション向けのテストコードとほぼ同じです。
フィールドの名前がsessionとなることが異なります。
newアクション用のコントローラは@titleに「サインイン」を格納するのみ。
対応するviewコードは「app/views/session/new.html.erb」です。
以下のように編集します。

<h1>サインイン</h1>
<%= form_for(:session, :url => sessions_path) do |f| %>
<div class="field">
<%= f.label :name %>&;t;br />
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :password %>%lt;br />
<%= f.password_field :password %>
</div>
<div class="actions">
<%= f.submit "サインイン" %>
</div>
<% end %>
<p><%= link_to "新規登録はこちら", new_user_path %></p>
これも基本的にはusers/newと同じなのですが「form_for」に渡しているパラメータが異なります。users/newではコントーラで生成したUserモデルのインスタンス変数を指定していましたが、サインイン時には該当するインスタンスが無いので「:session」とSessionsコントローラへ振り分けるよう指定します。
ただしこのままだとviewと同じ「sessions/new」に対してPOSTするHTMLが出力されていまうので、「:url => sessions_path」として「sessions/」に対してPOSTするよう指定しています。

2011年8月24日水曜日

Rails3を初歩から学ぶ #9 createアクションをつくろう

前回createアクションの検査までつくったのでコントローラを実装しましょう。

def create
@user = User.new(params[:user])
if @user.save
flash[:success] = "登録に成功しました"
redirect_to @user
else
@title = "新規登録"
@user.password = ""
@user.password_confirmation =""
render 'new'
end
end
User.newでインスタンスを生成した段階ではDBには保存していません。
保存するにはインスタンスからsaveメソッドを呼びます。戻り値がtrueなら成功、falseなら失敗なので戻り値に応じて画面を遷移します。
redirect_toとrenderの違いですが、renderは単に指定されたテンプレートで描画するのみです。この例でいえば登録失敗時に「new.html.erb」を再表示します。
一方のredirect_toはリダイレクト先のURLをクライアントに返して、クライアントからそのURLにアクセスさせます。つまりもう一度アクションが呼ばれることになります。

ここではリダイレクト先としてインスタンス変数を渡していますが、Userモデルのインスタンス変数を渡すと、ActiveControllerは「user_path/@user.id」として解釈してくれます。例えば登録した@userのidが1であればリダイレクト先として「user_path/1」がクライアントに渡り、クライアントはそのアドレスに向けてGETします。
すると今度はshowアクションが呼ばれるわけです。
というわけで、createアクション向けには対応するviewのテンプレートを用意する必要はありません(今回の例では、です)。

が、このままではせっかく設定したflashが表示されないので表示させてやりましょう。flashの表示はshowアクションに限ったことではないのでアプリケーション共通で使用する「view/layout/application.html.erb」に追加します。

<body>
<% flash.each do |key, value| %>
<%= content_tag(:div, value, :class => "flash #{key}") %>

<% end %>
<%= yield %>
</body>
ここでは単純にflashに設定された全てのキーと値を表示するようにしています。
「content_tag」ヘルパは何となく何するか分かると思いますが、指定されたタグを指定された値で指定された属性を付加して生成するものです。
この結果
<div class="flash success">登録に成功しました<div>
といったタグが生成されます。
ここまで出来たらRSpecを実行して検査をパスすることを確認してください。
また「rails server」としてサーバを起動して、「http://localhost:3000/users/new」にアクセスして必要な情報を入力して登録してみましょう。

次回からは認証プロセスをつくっていってみましょう。

2011年8月23日火曜日

Rails3を初歩から学ぶ #8 create(テスト編)

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


前回showアクションを実装しました。今回は後回しにしていたcreateアクションを実装しましょう。前々回説明したようにnewアクションで表示されるページでボタンをクリックするとcreateアクションに処理が渡ります。ここで渡されたパラメータを元にDBに新規ユーザーを登録します。登録に成功したら登録したユーザーのページ(/users/:id)に遷移します。
これをやりたかったので前回showアクション(/users/:idで呼ばれる)を先に実装しました。

というわけでテストから。

describe "POST 'create'" do
describe "登録させないケースの検証" do
before(:each) do
@attr = {:name => "", :password => "", :password_confirmation => "" }
end
it "ユーザーを登録しないこと" do
lambda do
post :create, :user => @attr
end.should_not change(User, :count)
end
it "新規登録ページに戻ること(タイトル検証)" do
post :create, :user => @attr
response.should have_selector("title", :content => "新規登録")
end

it "新規登録ページに戻ること(テンプレート検証)" do
post :create, :user => @attr
response.should render_template('new')
end
end #登録させないケースの検証
describe "登録に成功するケースの検証" do
before(:each) do
@attr = { :name => "Cli lin", :password => "foobar",
:password_confirmation => "foobar" }
end
it "DBに新規ユーザーを登録していること" do
lambda do
post :create, :user => @attr
end.should change(User, :count).by(1)
end
it "ユーザー情報ページに遷移していること" do
post :create, :user => @attr
response.should redirect_to(user_path(assigns(:user)))
end
it "登録に成功した旨をユーザーに表示していること" do
post :create, :user => @attr
flash[:success].should =~ /登録に成功しました/
end
end #登録に成功するケースの検証
end # POST 'create'
ちょっと長いです。登録に成功するケースと失敗するケースそれぞれで別階層にテストを記述しています(describe文)。
検証時に「lambda」を使用している箇所があります。これは要求を実行した結果、データベースがどうなったかを検証するために使用します。これまで要求に対するResponseを検証していましたが、データベースの結果については応答では判定できないためこのようにlambdaでくくってやって実行結果に対して検証するようにしています。
「change(User, :count)」はUserモデルのレコード数(:count)の変化有無を確認するメソッドです。1つ増加したことを検証する場合は「change(User, :count).by(1)」とします。削除確認のときは「-1」にしてやります。

登録失敗時は再度新規登録画面を表示するため「Erender_template('new')」としてnewテンプレート(= new.html.erb)に遷移することを確認しています。
また登録成功時には「redirect_to(user_path(assgins(:user)))」として新規に登録したユーザーのidから生成したURL、つまり「/users/1」などといったパスにリダイレクトすることを検証しています。
ここで出てくる「user_path」とは何でしょう。
もう一度「rake routes」としてルート定義を見てみましょう。


users GET /users(.:format) {:action=>"index", :controller=>"users"}
POST /users(.:format) {:action=>"create", :controller=>"users"}
new_user GET /users/new(.:format) {:action=>"new", :controller=>"users"}
edit_user GET /users/:id/edit(.:format) {:action=>"edit", :controller=>"users"}
user GET /users/:id(.:format) {:action=>"show", :controller=>"users"}
PUT /users/:id(.:format) {:action=>"update", :controller=>"users"}
DELETE /users/:id(.:format) {:action=>"destroy", :controller=>"users"}

注目するのは左端、各GETメソッドに対して「users」や「new_user」と定義されています。これらはコード中に「users_path」とすると「/users」に、「new_user_path」とすれば「/users/new」に変換されることを示しています。
ここで使用している「user_path」は「/users/:id」に該当しますが、「:id」が変数に当たるので「user_path」のパラメータとして渡してやります。つまり「user_path(1) 」とすると「/users/1」に変換されます。テストコードでは「assigns(:user)」としてコントローラのインスタンス変数@userのidを渡しているわけです。
整理するとここではユーザーを新規に登録し、「/users/登録ユーザーID」にリダイレクトすることを検証していることになります。

最後にflashについて。flashはアクションをまたいで一時的に情報を保持する機能です。
「登録成功しました」「パスワード間違ってます」なんて情報を遷移先のページに表示させるために使います。自分で一時変数を用意すると値を消し忘れて、特定の遷移ルートをたどった時のみ変なメッセージが出てくるんですけど!なんて問題になりかねませんが、Railsにおけるflashは自動で削除してくれるので心配ありません。
ここでは登録成功時に「flash[:success]」に成功した旨の情報を格納していることを検証しています。

コントローラの実装は次回。

2011年8月15日月曜日

Rails3を初歩から学ぶ #7 FactoryGirlを導入しよう

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



前回の予告通り、今回はUsersコントローラにshowアクションを定義します。
では例によってテストを書きましょう。
でも、待てよ、と。
showアクションは登録ユーザーの情報を表示するアクションですが検査時にその登録ユーザーはどうやって用意するんだ!?という問題があります。
もちろんUserオブジェクトを用意してDBに登録してやれば出来なくはないけれど、それってshowアクションの検査としてはあまり純粋じゃないし、不要なトラブルの元になりそうです。
こういうときにはテスト用のデータを用意するわけで、YAML形式でデータを用意するFixtureというものがRailsでは用意されています。
が、これはちょっと柔軟性に欠ける仕組みなのでRubyで試験データを用意できてしまうFactoryGirlなるものを導入してしまいましょう。
「Gemfile」に下記の赤字部分を追加します。

group :test do
  gem 'rspec'
  gem 'webrat'
  gem 'factory_girl_rails', '1.0'
end
バージョンを指定しないと最新版がインストールされますが、ここでは「1.0」を明示的に指定しています。最新版でも問題ないかもしれませんが、トラブったら1.0を指定してみましょう。
編集したら「bundle install」でインストールします。

インストールしたら「spec/factories.rb」というファイルを用意して以下のように編集します。

Factory.define :user do |user|
  user.name      "Son Gokuu"
  user.password  "foobar"
  user.password_confirmation  "foobar"
end
これでRSpecファイルから「Factory(:user)」とすれば名前が「Son Gokuu」でパスワードが「foobar」というユーザーがDBに登録済み、という状態になります。
早速つかってみましょう。

  describe "GET 'show'" do
    before(:each) do
      @user = Factory(: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 #GET 'show'
もうちょっとHTMLの中身を検査してもいいかもしれませんが、そこはお任せ。
とりあえず例題のshowアクションの検査としては上記のような感じになります。
beforeブロックは(eachを指定しているので)各検証コード(itブロック)を実行する前に毎回実行されます。ここで先ほど用意したFactoryGirlの機能を使ってテスト用のUserオブジェクトを取得しています。
RSpecでは1つの検証を終えるたびに環境をキレイにするので毎回DBへの登録が必要です。
また「assigns」メソッドはコントローラのインスタンス変数にアクセスするものです。
コントローラの@userがテストコードで生成した@userと一致することを確認しているわけです。
ではコントローラのshowアクションを用意しましょう。

  def show
    @user = User.find(params[:id])
    @title = @user.name
  end
ルート定義にあった「/users/:id」の「:id」に当たる部分を「params[:id]」で取得しています。Userテーブルに対して「Where id = X」で検索した結果を@userに格納している形になります。
さて次はviewを用意するんですが、「rails generate」でコントローラを生成するときにアクションまで指定していれば該当するviewファイルまで自動生成してくれます。が、今回のshowアクションは手動で追加しているのでviewファイルも手動で用意します。
「app/views/users/show.html.erb」を用意して以下のように編集します。
<h1><%= @user.name %></h1>
まあ、なんとも手抜きですが。。
ここまで出来たらテストを実行しましょう。全てパスするハズです。
ブラウザから動作確認する場合はDBが空なので(FactoryGirlはあくまでテスト環境のみで有効です)エラーになってしまいます。
「rails console」としてコンソールから無理矢理データをつくってしまいましょう。
※「rails console --sandbox」としないように。サンドボックス環境は終了時にロールバックされてしまいます
$rails console
>user = User.new(:name => "Son Gohan", :password => "foobar",
:password_confirmation => "foobar")
>user.save!
さあ、「rails server」でサーバーを起動したらブラウザから「http://localhost:3000/users/1」にアクセスしてみましょう。
デカデカと「Son Gohan」と出ていれば成功です。

2011年8月13日土曜日

Rails3を初歩から学ぶ #6 コントローラをつくろう

前回作成したコントローラ向けのテストをパスできるようにnewアクションを実装します。
app/controller/users_controller.rb を開いて以下のように編集します。

class UsersController < ApplicationController
  def new
    @user = User.new
    @title = "新規登録"
  end
end
@のついたインスタンス変数をViewで参照します。
ここでは空で生成したUserオブジェクトと 、タイトルを変数に格納しています。
次に対応したviewを編集するのですが、Railsでは「app/views/コントローラ名/アクション名.html.erb」が該当するviewとなります。
今回の例では「app/views/users/new.html.erb」がnewアクションに対応したviewとなります。
<%= form_for(@user) do |f| %>
    <%= f.label :name, "ユーザー名" %>
    <%= f.text_field :name %>
    <%= f.label :password, "パスワード" %>
    <%= f.password_field :password %>
    <%= f.label :password_confirmation, "パスワード(確認)" %>
    <%=f.password_field :password_confirmation %>
    <%=f.submit "新規登録" %>
<% end %>
form_forに渡している「@user」がコントローラで生成した@userです。
form_forからendまでのブロック内はそのuserオブジェクトのフィールドに対応づけられます。「 f.text_field :name」なんていう指定だけで、うまいこと設定されるのはこのようにRailsが対応付けしてくれているからなんですね。

ここでは一つ一つのdivにクラス指定していますが、CSS用です。
今回はデザイン面は放置するので使いませんが。
ここまででほぼテストは通りますが、まだタイトルがNGです。
タイトルを定義しましょう。
ここではWebアプリ全体で「FavMovie | XXXX」というタイトルを表示するものとします。「XXXX」の部分には「新規登録」「ユーザー情報編集」などアクションに応じたタイトルを設定します。

アクション共通で使用されるテンプレートは app/views/layout/application.html.erb です。この中に「<%= yield %>」となっている箇所がありますが、ここにアクションごとの個別のテンプレートが挿入されます。先ほどの例でいえば「new.html.erb」の内容がここに挿入されます。
titleは上のheader要素内にありますが、今回は動的にタイトルを変更したいので、ここでそのようなコードを入れると可読性が損なわれます。そこでヘルパメソッドとして定義しましょう。
app/helpers/application_helper.rb を開きます。

module ApplicationHelper
  def title
    base_title = "FavMovie"
    if @title.nil?
      base_title
    else
      #{base_title} | #{@title}
    end
  end
end

ここで見ている@titleがコントローラで設定されるものです。セットされていない場合は「FavMovie」とだけ表示します。
そして application.html.erb で呼び出してみましょう。
<title><%= title %></title>
これでテストは全てパスするようになります。
コンソールから「rails server」としてサーバを立ち上げて「http://localhost:3000/users/new」にアクセスしてみましょう。
3つのテキストボックスと1つのサブミットボタンがあることが確認できます。
ここでソースを確認してみるとform要素は以下のようになっています。
<form accept-charset="UTF-8" action="/users" class="new_user" id="new_user" method="post">
ここから「users」コントローラに対して「POST」で要求されることが読み取れます。
前回実行した「rake routes」の結果を再確認してみましょう。
POST   /users(.:format)          {:action=>"create", :controller=>"users"}
「/users」に対して「POST」要求がされた場合、usersコントローラのcreateアクションに処理が渡ることが示されています。
というわけで、お次はcreateアクションの実装、といきたいところですが、その前にshowアクションを実装することにします。ルート定義はこちら。


user GET    /users/:id(.:format)      {:action=>"show", :controller=>"users"}


「/users/1」などというパスでGETが要求されるとshowアクションが呼ばれます。
「:id」に当たる部分はUserテーブルのid(主キー)に該当します。
つまりリソース(ここではUserモデル)の1レコードに対する情報を表示するページとなります。
今回のUsersモデルの例でいえば各ユーザーの個人ページに当たるでしょう。
次回はこちらを作成してみることにします。



2011年8月11日木曜日

Rails3を初歩から学ぶ#5 ルートを定義しよう

ものすごくちゃんとしたRailsの日本語解説を見つけてしまった。
Ruby on Rails 3.0日記
一連のエントリの存在意義が、、、(あと20回くらい続く予定、、、)
まあ、自分自身の復習も兼ねているので続けますね。
=======================

前回モデルを実装しましたが、RailsではRESTfulなインターフェースに沿ってCRUD(create, read, update, delete)を実現する思想になっています。
HTTPメソッドの「GET」「POST」「PUT」「DELETE」を通じてリソースを操作します。
そこで前回作成したUserモデル用のインターフェースを定義しましょう。
config/route.rbを開きます。

Favmovie::Application.routes.draw do
  resources :users
end
赤字の行を追加します。
route.rbはルートを定義するファイルです。
ここの定義に従ってアクセスしてきた要求をどのコントローラのどのアクションに振り分けるかを決定します。
「resources」メソッドは指定したリソースに対するCRUD操作を自動で定義してくれる機能を提供しています。
ファイルを保存したら「rake routes」として定義されたルートを確認してみましょう。
たった1行でこれだけのルートが定義されます。
例えばHTTPのGETで「http://〜/users」にアクセスすると「usersコントローラ」の「index」アクションが呼ばれることが上図から分かります。
これらコントローラに対して対応するアクションを実装していくのが次の仕事です。
まずはコントローラの生成から始めましょう。
rails generate controller Users new
コントローラは対応するモデルの複数形が名前になります。ここでは「User」モデルに対応したコントローラであるため「Users」となります。
次のパラメータがコントローラに定義するアクションです。とりあえず今の段階では「new」アクションだけを用意します。
ちなみに先ほどの図から「new」アクションが呼ばれるのは「http://〜/users/new」に対してGETが要求されたときであることが分かります。
ちょっと試してみましょう。
rails server
としてサーバを起動します。
ブラウザを立ち上げてアドレス欄に「http://localhost:3000/users/new」と入力してアクセスすると「Users #new」とデカデカと表示されたページが表示されます。
これで正しくusersコントローラのnewアクションに処理が渡ったことが分かります。
ちなみにアクションを定義していないindexアクションにアクセスしてみましょう。
アドレス欄から「new」の部分を消してやればindexアクションです。
そうすると「Unknown action」と出て「The action 'index' could not be found for UsersController」というメッセージが表示されます。
これはUsersControllerにindexアクションは定義されていませんよ、ということを示しています。

ではこれからUsersControllerにnewアクションを実装していきます。
まずはテストコードから作成しますが、先ほどコントローラを生成したときにいくつか不要なテストファイルが出来ているので削除しておきます。
rm spec/helpers/users_helper_spec.rb
rm spec/views/users/new.html.erb_spec.rb
ヘルパはそれを利用するコントローラの検査で、ビューはインテグレーションテストで検証するので削除しておきます。この辺りは個人の好みで。
さて、コントローラのテストをつくりましょう。
spec/controller/users_controller_spec.rb を開きます。

# -*- coding: utf-8 -*-
require 'spec_helper'
describe UsersController do
  render_views
  describe "GET 'new'" do
    it "should be successful" do
      get 'new'
      response.should be_success
    end
    it "タイトルの検証" do
      get :new
      response.should have_selector("title", :content => "新規登録")
    end
    it "ユーザー名入力欄があること" do
      get :new
      response.should have_selector("input[name='user[name]'][type='text']")
    end
    it "パスワード入力欄があること" do
      get :new
      response.should have_selector("input[name='user[password]']
[type='password']")
    end
    it "パスワード確認欄があること" do
      get :new
      response.should    have_selector("input[name='user[password_confirmation]']
[type='password]']")
    end
  end #GET 'new'
end
newアクションはユーザーの新規登録のページを表示するアクションですので、ちゃんと新規登録ページを表示できているかの検証になります。
どこまで評価するかは色んな方針があると思いますが、見た目に関わる部分なのであまり細かく検証してしまうとデザインを変えるためびにNGになってメンドーなのでバランスを考えましょう。
ここではタイトルと最低限の入力項目が存在することのみ検証しています。
2行目にある「render_views」を忘れないでください。
これがないとviewを描画しないので必ずエラーになります。どう考えても検査をパスするハズなのになぁ、と悩んだら確認してみましょう。

次回はこの検証コードがパスできるようにコントローラを実装します。

2011年8月9日火曜日

新たにMac book Air を買ったとき、開発環境はどうなるか?

買ってしまいました、Mac book Air。
その素晴らしさは、まあ置いておいて、新たにマシンを購入したときにiPhoneアプリ開発のために用意した各種証明書やプロビジョニングをどうしたらいいのか、という点が心配な方は多いんじゃないでしょうか。
キーチェインからエクスポートして、再度iTunesConnectからダウンロードして、うんぬんかんぬん、そういった面倒な手続きがあって、、、、

なんて考えていたとしたら大間違い。

1)旧マシンを外付けHDDでタイムマシーンを使ってバックアップ
2)新マシン起動時にそのHDDを指定する
3)旧マシンと同じ環境が出来上がり

いきなり開発できます。
AppStoreへの公開も同じ。
Lion素晴らしい。

外付けHDDを使ってなくてもHDDの代わりに旧マシンを指定してやれば同じことが出来ます。


2011年8月8日月曜日

Rails3を初歩から学ぶ#4 モデル向けのテストをつくる

前々回Userモデルを作成したので、このモデルにバリデーションを追加しましょう。
Userモデルで必要な必要な検証を下表に示します。
idinteger-
namestring必須、一意,50文字以内
★passwordstring必須,6文字以上40文字以内
encrypted_passwordstring
saltstring-
adminboolean-
created_atdatetime-
updated_atdatetime-
★印をつけた「password」だけは例外で実際のデータベースには格納しません。passwordを元に暗号化した値を「encrypted_password」としてDBに格納します。
ただしUserオブジェクト生成時では必須なので必須指定としています。
この辺りの事情は前回の解説をみてください。

というわけでまずはこの「name」と「password」に対する検証がちゃんと動作しているかを確認するテストコードを作成します。
モデル生成時に「spec/model/user_spec.rb」というファイルが出来ているのでこれを日以下のように編集します。

# -*- coding: utf-8 -*-
require 'spec_helper'
describe User do
  before(:each) do
    @attr = {
      :name => "hogetarou",
      :password => "foobar",
      :password_confirmation => "foobar",
    }
  end
  it "名前がないと検証NGであること" do
    user = User.new(@attr.merge(:name => ""))
    user.should_not be_valid
  end
  it "名前が50文字を超えると検証NGであること" do
    long_name = "a"* 51
    user = User.new(@attr.merge(:name => long_name))
    user.should_not be_valid
  end
  it "パスワードがないと検証NGであること" do
    user = User.new(@attr.merge(:password => "", :password_confirmation => ""))
    user.should_not be_valid
  end
  it "パスワードと確認用が一致しないと検証NGであること" do
    user = User.new(@attr.merge(:password_confirmation => "invalid"))
    user.should_not be_valid
  end
  it "パスワードが6文字未満だと検証NGであること" do
    short = "a" * 5
    user = User.new(@attr.merge(:password => short, :password_confirmation => short))
    user.should_not be_valid
  end
  it "パスワードが41文字以上だと検証NGであること" do
    long = "a" * 41
    user = User.new(@attr.merge(:password => long, :password_confirmation => long))
    user.should_not be_valid
  end
  it "全て満たしたとき検証OKであること" do
    user = User.new(@attr)
    user.should be_valid
  end
  it "ユーザー名が重複したらダメ" do
    User.create!(@attr)
    user = User.new(@attr)
    user.should_not be_valid
  end
end
最初の「before」で正しいデータを用意しておいて、各検証コードで「merge」メソッドを使って検証対象のフィールドをいじりながら検証しています。
rspec spec/user_spec.rb
と実行すると全てNGで返ってきます。
ちなみに「:password_confirmationなんて無いぞ!」というエラーばかりだと思います。
今はこれでOK。
ではモデルの方を作ってみましょう。
「app/model/user.rb」を以下のように編集します。

class User < ActiveRecord::Base
  attr_accessor  :password
  attr_accessible :name, :password, :password_confirmation
  validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 50 }
  validates :password, :presence => true, :confirmation => true, :length => { :within => 6..40}
end
まず1行目の「attr_accessor」ですが、これはRailsではなくRubyの仕様で、渡したパラメータのgetterとsetterを生成してくれるメソッドです。
モデル生成時に指定したフィールドはActiveModelが自動で生成してくれますが、passwordフィールドは生成していません(何度も言いますがDBに格納するのは暗号化した「encrypted_password」です)。なのでここでpasswordというフィールドを定義しているわけです。

次のattr_accessibleは指定した以外のフィールドを保護するメソッドです。
例えば前々回作成したモデルには管理者を示す「admin」フィールドが存在しますが、ユーザーインターフェース上に「admin」を示す入力項目が無かったとしても、悪意あるユーザーがPOSTメソッドで「admin=true」としたパラメータを送りつけて来た場合に、無条件に「update_attributes」メソッドなどでUserオブジェクトを更新してしまうと、そのユーザーに管理者権限を与えてしまうことになります。
attr_accessibleではコンストラクタや「update_attributes」メソッドで更新可能なフィールドを指定し、指定されなかったフィールドについては更新を許可しません。
ちょっと確認してみましょう。
「rails console --sandbox」としてコンソールを起動してみます。
rails console --sandbox

>> user = User.new(:name => "hogehoge", :password => "foobar", :password_confirmation => "foobar", :admin => true)
=> #
このように「:admin => true」と指定しているのに結果は「admin: false」となっていることからちゃんと保護されていることが分かります。
「attr_accessible」と似たものに「attr_protected」があり、こちらは逆に指定したフィールドだけを保護するメソッドです。
長期的にはフィールドが増加したときに「指定し忘れ」が起きてしまうことを考えると「attr_accessible」にしておいた方が安全でしょう。

以降は「validates」メソッドで必要な検証を定義しています。
「password」に対して「:confirmation => true」としています。
こうすることで「password_confirmation」というフィールドが自動で付加されて、「password」と一致していることをチェックしてくれます。

さて、user.rbを保存したらもう一度RSpecを実行してみましょう。
今度は全て成功するハズです。

2011年8月7日日曜日

Rails3を初歩から学ぶ#3 passwordについて

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



前回作成したUserモデルに「encrypted_password」と「salt」というものを追加していますが、どのような認証のシーケンスをたどるのか、あらかじめここで考えておきます。
Userクラスにはユーザーから渡されたパスワードを保持する「password」フィールドを定義します。これは前回生成した「encrypted__password」とは別モノです。
データベース上に格納するのはあくまで暗号化した情報である「encrypted_password」です。

Userモデルでは「password」を受け取るとこれを暗号化して「encrypted_password」としてデータベースに格納します。
このとき単純にハッシュをかけて暗号化するだけではセキュリティ上危険なので毎回異なる値をパスワードに付加してハッシュするようにします。この「毎回異なる値」が「salt」です。
なのでまとめると、「ユーザー入力」->「passwordフィールド」->「salt生成」
->「salt+passwordでハッシュ算出」->「encrypted_password」として格納、となります。

また一般的なWebサービスでの新規登録時にはパスワードを二回入力することが求められることが多いです。Railsでは「password」というフィールドに対して「:confirmation=>true」と指定することで「password_confirmation」というフィールドが自動で追加されます。
そして「password」と「password_confirmation」が一致しないと自動的にエラーを返すようになります。

この辺りの事情を頭に入れて、次回からUserモデルを実装していきます。

2011年8月5日金曜日

Rails3を初歩から学ぶ #2 モデルをつくる

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


前準備は終わったので今回はモデルをつくってみます。
まずはユーザー情報を管理するUserモデルをつくるのだ。
スキーマを下表に示します。
★idinteger
namestring
encrypted_passwordstring
saltstring
adminboolean
★created_atdatetime
★updated_atdatetime
「★」マークのデータはmigrationしたときにRailsによって自動的に付加される列です。
「name」はユーザー名、「encrypted_password」は暗号化したパスワード、「salt」は暗号化時にパスワードに付加する値、「admin」は管理者権限を表します。

ではつくってみましょう。
rails generate model User name:string encrypted_password:string
salt:string admin:boolean
※実際は一行
migrationファイルが出来ているのでエディタで開きます。

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :name
      t.string :encrypted_password
      t.boolean :admin, :default => false
      t.timestamps
    end
  end
  def self.down
    drop_table :users
  end
end
rake db:migrate
admin権限はデフォルトでfalseにしておきたいので赤字の部分を追加します。
おっと「salt」を忘れていました。追加しましょう。既存のモデルに追加する場合はmigrationファイルを生成します。
rails generate migration add_salt_to_users salt:string
一応生成されたファイルを確認してみましょう。db/XXXX_add_salt_to_users.rbをひらきます。
class AddSaltToUsers < ActiveRecord::Migration
  def self.up
    add_column :users, :salt, :string
  end
  def self.down
    remove_column :users, :salt
  end
end
「add_column」メソッドでusersテーブルにstring型のsalt列を追加していることが分かります。これを反映しましょう。
rake db:migrate
rake db:test:prepare
RSpecの検査でも有効になるよう「rake db:test:prepare」を実行しておきます。
さて、ここで意図した通りのテーブルが出来ているか確認してみましょう。
rails dbconsole
sqlite> .tables
-->schema_migrations users
sqlite> .schema users
-->CREATE TABLE "users" ("id" INTEGER 〜(省略)〜);
「rails dbconsole」を実行するとDBと接続したコンソールが起動します。
ターミナルに「sqlite>」と表示されるのでsqlite3のコマンドを入力します。
「.tables」と入力すると作成したテーブルの一覧が確認できます。
「users」が先ほど作成したテーブル、「schema_migrations」はRailsが「rake db:migrate」でどこまで反映したかを管理するためのテーブルです。
「.schema users」でusersテーブルの中身を確認すると、さきほど指定したスキーマの通りになっているかどうか確認できます。




2011年8月4日木曜日

Rails3を初歩から学ぶ#1 前準備編

勉強がてら、ごくごく簡単なWebサービスをRailsを使ってつくってみようと思います。
机上で学習しても身に付かないので、調べるー実践するーここに書く、というサイクルで学ぶのだ。
インストールや初期設定の説明はパス。サービスの作成からはじめるナウ。
ここではお気に入りの映画を管理するサービスを例に進めてみます。

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

rails new favmovie -T
favmovieがアプリケーション名です。ここではテキトーに「favmovie」としています。
デフォルトではテスティングフレームワークとしてTest::Unitが組み込まれますが、ここではRSpecを使いたいので「-T」オプションを指定して外しておきます。
次にGemfileを編集します。
source 'http://rubygems.org'
gem 'rails', '3.0.6'
gem 'sqlite3'
デフォルトではこのようになっているので下記のように編集します。

source 'http://rubygems.org'
gem 'rails', '3.0.6'
gem 'sqlite3-ruby', :require => 'sqlite3'
group :development do
  gem 'rspec-rails'
end
group :test do
  gem 'rspec'
  gem 'webrat'
end
Rubyからsqlite3を扱うために「sqlite3-ruby」として「sqlite3」も併せてインストールするように「:require」で指定します。
続いて開発環境(development)用にRails向けのRspecである「rspec-rails」を指定し、テスト環境(test)用に「rspec」と「webrat」を指定します(テスト実行のため)。
以上を指定した状態で下記のbundle installを実行してインストールします。
bundle install
完了したらRailsにRSpecをインストールします。
rails generate rspec:install
前準備はこれでおしまい。