はじめに
Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版の 8章 8.1 セッションの演習まとめ&解答例です。
個人の解答例なので、誤りがあればご指摘ください。
8.1.1 Sessionsコントローラ
本章での学び
Sessionsコントローラの作成
$ rails generate controller Sessions new
のコマンドで、sessionsコントローラ、sessionsコントローラのテスト、newアクションなどを自動生成できる。
yokoyan:~/workspace/sample_app (basic-login) $ rails generate controller Sessions new Running via Spring preloader in process 5829 Expected string default value for '--jbuilder'; got true (boolean) Expected string default value for '--helper'; got true (boolean) Expected string default value for '--assets'; got true (boolean) create app/controllers/sessions_controller.rb route get 'sessions/new' invoke erb create app/views/sessions create app/views/sessions/new.html.erb invoke test_unit create test/controllers/sessions_controller_test.rb invoke helper create app/helpers/sessions_helper.rb invoke test_unit invoke assets invoke coffee create app/assets/javascripts/sessions.coffee invoke scss create app/assets/stylesheets/sessions.scss
名前付きルーティングの設定
/config/routes.rb
にて、ログインに関するルーティングの設定を行う。
RESRfulなルーティングをフルセットで付与する場合は、resources :users
のように、resources
を付与するが、今回はフルセットは不要。
必要なHTTPリクエストのタイプに対するURLと、それに対応するコントローラのアクションを定義する。
get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy'
また、rails generate
で自動生成される以下のルーティングも削除する。
get 'sessions/new' get 'users/new'
テストの修正
自動生成されたテストケースは以下の通り。
test "should get new" do get sessions_new_url assert_response :success end
getメソッド部分を、新しく追加した名前付きルーティングlogin_path
をテストに修正する。
test "should get new" do get login_path assert_response :success end
現在設定しているルーティングの確認
rails routes
で、現在設定しているルーティングの確認が行える。
Prefix部分が、login_path
など名前付きルーティングの名称となる。
yokoyan:~/workspace/sample_app (basic-login) $ rails routes Prefix Verb URI Pattern Controller#Action root GET / static_pages#home help GET /help(.:format) static_pages#help about GET /about(.:format) static_pages#about contact GET /contact(.:format) static_pages#contact signup GET /signup(.:format) users#new POST /signup(.:format) users#create login GET /login(.:format) sessions#new POST /login(.:format) sessions#create logout DELETE /logout(.:format) sessions#destroy users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy
以下の部分が、routes.rb
でresources :users
を付与した場合に自動で設定されるフルセットのルーティング設定。
users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy
演習1
GET login_pathとPOST login_pathとの違いを説明できますか? 少し考えてみましょう。
login GET /login(.:format) sessions#new POST /login(.:format) sessions#create
GET login_pathの場合、ログインページへ遷移する際に使用する。 sign upボタンクリック時に、URL:/loginに対してGETすると、sessionsコントローラのnewアクションが動く。 GET時に付与したパラメータがある場合は、URLに表示される。
POST login_pathは、ログイン情報を送信する際に使用する。 フォームタグで入力したログイン情報を、URL:/loginに対してPOSTすると、sessionsコントローラのcreateアクションが動く。 POSTした情報はURLには表示されない。
演習2
ターミナルのパイプ機能を使ってrails routesの実行結果とgrepコマンドを繋ぐことで、Usersリソースに関するルーティングだけを表示させることができます。同様にして、Sessionsリソースに関する結果だけを表示させてみましょう。現在、いくつのSessionsリソースがあるでしょうか? ヒント: パイプやgrepの使い方が分からない場合は Learn Enough Command Line to Be Dangerousの Section on Grep (英語) を参考にしてみてください。
rails routes | grep キーワード
で表示できる。
Usersリソースに関するルーティングのみを表示。
rails console上では、users#
部分が赤字で強調されて表示される。
yokoyan:~/workspace/sample_app (basic-login) $ rails routes | grep users# signup GET /signup(.:format) users#new POST /signup(.:format) users#create users GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy
Sessionsリソースに関するルーティングのみを表示。 現時点では3つのリソースがある。
yokoyan:~/workspace/sample_app (basic-login) $ rails routes | grep sessions# login GET /login(.:format) sessions#new POST /login(.:format) sessions#create logout DELETE /logout(.:format) sessions#destroy
8.1.2 ログインフォーム
本章での学び
ビューの作成
ログインフォームの見た目は、ユーザ登録フォームとほぼ同じになる。
ユーザ登録フォームとログインフォームの違い
ユーザ登録フォームの場合
ユーザ登録フォームでは、form_for
ヘルパーに、Userモデルのインスタンス変数である@user
を引数に渡して、<%= form_for(@user) do |f| %>
で実現していた。
また、form_for(@user)
と記載することで、Railsが自動的に「フォームのアクションは、/usersというURLへのPOSTである」と判断していた。
ログインフォームの場合
セッションにはSessionモデルがないため、@session
のようなインスタンス変数に相当するものがない。そのため、リソースの名前とそれに対応するURLを以下のように具体的に指定する必要がある。
form_for(:session, url: login_path)
「フォームのアクションは、login_path(/login)というURLへのPOSTである。POSTするリソースは、:sessionである」
生成されたHTML
ユーザ登録フォーム
フォームでsubmitされるハッシュ情報は、params[:user]
となる。
<form class="new_user" id="new_user" action="/signup" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="✓" /><input type="hidden" name="authenticity_token" value="s4yYXtGa8OgmyvCWrN2Dwg97t0NI1WqO0K/V+D1yZP80QtDRfjbalkoxq+adYNgAxYWTOgVLRuCU+y23CQsCwA==" /> <label for="user_name">Name</label> <input class="form-control" type="text" name="user[name]" id="user_name" /> <label for="user_email">Email</label> <input class="form-control" type="email" name="user[email]" id="user_email" /> <label for="user_password">Password</label> <input class="form-control" type="password" name="user[password]" id="user_password" /> <label for="user_password_confirmation">Confirmation</label> <input class="form-control" type="password" name="user[password_confirmation]" id="user_password_confirmation" /> <input type="submit" name="commit" value="Create my account" class="btn btn-primary" data-disable-with="Create my account" /> </form>
ログインフォーム
フォームでsubmitされるハッシュ情報は、params[:session]
となる。
<form action="/login" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="✓" /><input type="hidden" name="authenticity_token" value="jwuSENMsYEUhX0ugB6hnKTNGJenkfcLV0a8FKWKogQp4v9+FFhbVjQVmNOQ0xf9pSE/OfsFH0TJFHTZDumFpSA==" /> <label for="session_email">Email</label> <input class="form-control" type="email" name="session[email]" id="session_email" /> <label for="session_password">Password</label> <input class="form-control" type="password" name="session[password]" id="session_password" /> <input type="submit" name="commit" value="log in" class="btn btn-primary" data-disable-with="log in" /> </form>
演習1
リスト 8.4で定義したフォームで送信すると、Sessionsコントローラのcreateアクションに到達します。Railsはこれをどうやって実現しているでしょうか? 考えてみてください。ヒント:表 8.1とリスト 8.5の1行目に注目してください。
開発環境のログインフォームにて、log inボタンをクリックすると、下記エラーが発生する。 Sessionコントローラのcreateアクションに到達している。
これは、ログインフォームで
form_for(:session, url: login_path)
と記載することで、URL:login_path(/login)に対して、POSTしているため。 結果、route.rbで定義したとおり、対応するsessionコントローラのcreateアクションに到達している。
yokoyan:~/workspace/sample_app (basic-login) $ rails routes | grep sessions# login GET /login(.:format) sessions#new POST /login(.:format) sessions#create logout DELETE /logout(.:format) sessions#destroy
8.1.3 ユーザーの検索と認証
本章での学び
セッション情報の取得
ユーザー情報は、sessionキーの下に、emailとpasswordがある。
session: !ruby/hash:ActiveSupport::HashWithIndifferentAccess email: user@example.com password: foobar commit: log in controller: sessions action: create
つまり、paramsハッシュがネストしたハッシュになっている。
params[:session]
paramsハッシュの中に、sessionハッシュがあり、さらにsessionハッシュの中にpasswordとemailが含まれている。
{ session: { password: "foobar", email: "user@example.com" } }
そのため、以下のようにするとemailの値が取得できる。
params[:session][:email]
同様に、パスワードの値を取得できる。
params[:session][:password]
`
ユーザの検索と認証
ユーザを検索するためには、User.find_by
メソッドを使う。
データベース内に小文字で格納されているため、downcase
メソッドで確実に一致するようにしている。
user = User.find_by(email: params[:session][:email].downcase)
ユーザの認証は、authenticate
メソッドを使う。
user && user.authenticate(params[:session][:password])
userとauthenticateの組み合わせは以下となる。
User | パスワード | 判定結果 |
---|---|---|
存在しない | 何でも良い | false(nil && [オブジェクト]) |
存在する | 一致しない | false (true && false) |
存在する | 一致する | true (true && true) |
演習1
Railsコンソールを使って、表 8.2のそれぞれの式が合っているか確かめてみましょう. まずはuser = nilの場合を、次にuser = User.firstとした場合を確かめてみてください。ヒント: 必ず論理値オブジェクトとなるように、4.2.3で紹介した!!のテクニックを使ってみましょう。例: !!(user && user.authenticate(’foobar’))
user = nilの場合。期待値はfalse。
yokoyan:~/workspace/sample_app (basic-login) $ rails console Running via Spring preloader in process 2949 Loading development environment (Rails 5.0.0.1) >> user = nil => nil >> !!(user && user.authenticate('foobar')) => false >> ?> user = User.first User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2017-05-07 21:50:35", updated_at: "2017-05-07 21:50:35", password_digest: "$2a$10$esrzyVS.n7KGJ27bcs7UF.78tD7e75pMkwy6gnnwhlQ..."> >> !!(user && user.authenticate('barfoo')) => false >> ?> !!(user && user.authenticate('foobar')) => true >>
user = User.firstで取得し、パスワード誤りの場合。 期待値はfalse。
?> user = User.first User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2017-05-07 21:50:35", updated_at: "2017-05-07 21:50:35", password_digest: "$2a$10$esrzyVS.n7KGJ27bcs7UF.78tD7e75pMkwy6gnnwhlQ..."> >> !!(user && user.authenticate('barfoo')) => false
user = User.firstで取得し、パスワード一致の場合。 期待値はtrue。
?> !!(user && user.authenticate('foobar')) => true >>
8.1.4 フラッシュメッセージを表示する
本章での学び
ログイン失敗時のメッセージ
ユーザ登録ページでは、Userモデル(特定のActive Recordオブジェクト)に関連付けられていたため、Userモデルのエラーメッセージがそのまま使用できた。 ログインページでは、セッションモデルがないため、フラッシュメッセージで代用する。
flash[:danger] = 'Invalid email/password combination'
[:danger]
はbootstrapのクラス名。
描画されたHTMLでは、以下のようにメッセージが赤で表示される。
<div class="alert alert-danger">Invalid email/password combination</div>
この方式の問題点
リクエストのフラッシュメッセ―ジが一度表示されると消えずに残ってしまう。
リダイレクトした時と異なり、render
メソッドで強制定期に再レンダリングしてもリクエストとしては見なされないため、
リクエストのメッセージは消えない。
8.1.5 フラッシュのテスト
本章での学び
結合テストコードの生成
rails generate integration_test users_login
を実行する。
yokoyan:~/workspace/sample_app (basic-login) $ rails generate integration_test users_login Running via Spring preloader in process 2308 Expected string default value for '--jbuilder'; got true (boolean) invoke test_unit create test/integration/users_login_test.rb
結合テストコードの単独実行
rails test
の引数にテストファイルを指定すると、そのテストファイルのみ実行できる。
yokoyan:~/workspace/sample_app (basic-login) $ rails test test/integration/users_login_test.rb Running via Spring preloader in process 2549 Started with run options --seed 15900 FAIL["test_login_with_invalid_information", UsersLoginTest, 5.106406192004215] test_login_with_invalid_information#UsersLoginTest (5.11s) Expected false to be truthy. test/integration/users_login_test.rb:15:in `block in <class:UsersLoginTest>' 1/1: [=================================================================================================================================================================================================] 100% Time: 00:00:05, Time: 00:00:05 Finished in 5.11290s 1 tests, 4 assertions, 1 failures, 0 errors, 0 skips
flash.now
flash.now
は、レンダリングが終わっているページで特別にフラッシュメッセージを表示する。
flash
と異なり、flash.now
のメッセージはその後のリクエストが発生すると消滅する。
flash.now
に書き換えると、テストコードがgreenになることを確認。
yokoyan:~/workspace/sample_app (basic-login) $ rails test test/integration/users_login_test.rb Running via Spring preloader in process 2598 Started with run options --seed 42458 1/1: [=================================================================================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00 Finished in 0.84829s 1 tests, 4 assertions, 0 failures, 0 errors, 0 skips
演習1
8.1.4の処理の流れが正しく動いているかどうか、ブラウザで確認してみてください。特に、flashがうまく機能しているかどうか、フラッシュメッセージの表示後に違うページに移動することを忘れないでください。
存在しないEmail、Passwordを入力する。
エラーメッセージが表示されることを確認。
ユーザ登録画面を表示して、フラッシュメッセージが表示されないことを確認。
おわりに
いよいよSessionが出てきました。 ログイン後のユーザ情報が保持できるようになるのは次章なので楽しみです! 記事も4本目になりました。マークダウンで記載するのはとても便利ですね。 Rails以外の記事も投稿していきたいと思います。