はじめに
最近、プロジェクト管理業務が業務の大半を占めており、
プログラムを書く機会がなかなかありません。
このままだとプログラムがまったく書けない人になってしまう危機感(迫り来る35歳定年説)と、
新しいことに挑戦したいという思いから、
Ruby on Rails チュートリアル実例を使ってRailsを学ぼう 第4版を学習中です。
業務で使うのはもっぱらJavaなのですが、Rails楽しいですね。
これまでEvernoteに記録していましたが、ソースコードの貼付けに限界を感じたため、
Qiitaで自分が学習した結果をアウトプットしていきます。
個人の解答例なので、誤りがあればご指摘ください。
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が出てきました。
ログイン後のユーザ情報が保持できるようになるのは次章なので楽しみです!
Qiitaの記事も4本目になりました。マークダウンで記載するのはとても便利ですね。
Rails以外の記事も投稿していきたいと思います。