学習記録 17日目
17日目の学習記録をまとめていきます。
学習計画
- Railsチュートリアル 8章
- Ruby on Rails 5 速習実践ガイド 1, 2章
学習内容
Ruby on Railsチュートリアル 8章
8.1 セッション
- Railsでセッションを実装する方法の1つとしてcookiesを使用する方法があります。
- Railsではsessionメソッドを使用した一時セッションとcookiesメソッドを使用した長期間保有可能なセッションがある。本章ではsessionメソッドを使用する。
- トピックブランチを作成します。
$ git checkout -b basic-login
8.1.1 Sessionsコントローラ
- まずはSessionsコントローラを作成します。
$ rails g controller Sessions new
- ルーティングを追記します。必要なルーティングはセッションを出力するnew, セッションを作成・保存するcreate, セッションを破棄するdestroy。
# config/routes.rb Rails.application.routes.draw do get 'sessions/new' root "static_pages#home" get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete 'logout', to: 'sessions#destroy' resources :users end
- まずはnewアクションに対してテストを作成します。
# test/controllers/sessions_controller_test.rb require 'test_helper' class SessionsControllerTest < ActionDispatch::IntegrationTest test "should get new" do get login_path assert_response :success end end
- テストが成功することを確認します。
$ rails t 20 tests, 42 assertions, 0 failures, 0 errors, 0 skips
演習
1. GET login_pathとPOST login_pathとの違いを説明できますか? 少し考えてみましょう。
2. ターミナルのパイプ機能を使ってrails routesの実行結果とgrepコマンドを繋ぐことで、Usersリソースに関するルーティングだけを表示させることができます。同様にして、Sessionsリソースに関する結果だけを表示させてみましょう。現在、いくつのSessionsリソースがあるでしょうか?
$ rails routes|grep users ...
$ rails routes|grep sessions ...
8.1.2 ログインフォーム
- ログインフォームを作成していきます。
# app/views/sessions/new.html.erb <% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_with(url: login_path, scope: :session, local: true) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control'%> <%= f.label :password %> <%= f.password_field :password, class: 'form-control'%> <%= f.submit "Log in", class: 'btn btn-primary'%> <% end %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div>
- 動作確認をします。
演習
1. リスト 8.4で定義したフォームで送信すると、Sessionsコントローラのcreateアクションに到達します。Railsはこれをどうやって実現しているでしょうか? 考えてみてください。ヒント:表 8.1とリスト 8.5の1行目に注目してください。
8.1.3 ユーザの検索と認証
- createアクションにユーザの検索と認証に関するコードを記載します。
# app/controllers/session_controller.rb class SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) else render 'new' end end def destory end end
演習
1. Railsコンソールを使って、表 8.2のそれぞれの式が合っているか確かめてみましょう. まずはuser = nilの場合を、次にuser = User.firstとした場合を確かめてみてください。ヒント: 必ず論理値オブジェクトとなるように、4.2.2で紹介した!!のテクニックを使ってみましょう。例: !!(user && user.authenticate('foobar'))
$ rails c > user = nil > !!(user && user.authenticate("foobar")) # => false > user = User.first > !!(user && user.authenticate("foobaz")) # => true > !!(user && user.authenticate("foobar")) # => true
8.1.4 フラッシュメッセージを表示する
- エラーメッセージをフラッシュとして表示するようにします。
# app/controllers/session_controller.rb class SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) else flash[:danger] = "Invalid email/password combination" render 'new' end end def destory end end
8.1.5 フラッシュのテスト
- テストを書いていきます。
$ rails g integration_test users_login
# test/integration/users_login_test.rb require 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest test "login with invalid information" do get login_path assert_template 'sessions/new' post login_path, params: { session: { email: "", password: "" }} assert_template 'sessions/new' assert_not flash.empty? get root_path assert flash.empty? end end
- テストは上記の問題を抱えているため失敗します。
$ rails test:integration 4 tests, 19 assertions, 1 failures, 0 errors, 0 skips
- flash.nowへ変更して再テストします。
$ rails test:integration 4 tests, 19 assertions, 0 failures, 0 errors, 0 skips
- 成功しましたね。
演習
1. 8.1.4の処理の流れが正しく動いているかどうか、ブラウザで確認してみてください。特に、flashがうまく機能しているかどうか、フラッシュメッセージの表示後に違うページに移動することを忘れないでください。
- 省略
8.2 ログイン
- まずApplicationコントローラにSessionHelperモジュールをincludeします。これによりアプリケーションのどのコントローラからでもSessionHelper内のインターフェースが使用出来るようになります。
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base include SessionsHelper end
8.2.1 log_inメソッド
- SessionHelperモジュール内にuserのidをセッションに保存するメソッドを記載します。
# app/helpers/sessions_helper.rb module SessionsHelper def log_in(user) session[:user_id] = user.id end end
- 作成したヘルパーメソッドを使用しユーザのログインコードを完成させます。
# app/controllers/session_controller.rb class SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user redirect_to user else flash.now[:danger] = "Invalid email/password combination" render 'new' end end def destory end end
演習
1. 有効なユーザーで実際にログインし、ブラウザからcookiesの情報を調べてみてください。このとき、sessionの値はどうなっているでしょうか?
2. 先ほどの演習課題と同様に、Expiresの値について調べてみてください。
8.2.2 現在のユーザー
- 現在のユーザーを返してくれるヘルパーメソッドを作成します。
# app/helpers/sessions_helper.rb module SessionsHelper #... def current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end end
演習
1. Railsコンソールを使って、User.find_by(id: ...)で対応するユーザーが検索に引っかからなかったとき、nilを返すことを確認してみましょう。
$ rails c > User.find_by(id: 4) # => nil
2. 先ほどと同様に、今度は:user_idキーを持つsessionハッシュを作成してみましょう。リスト 8.17に記したステップに従って、||=演算子がうまく動くことも確認してみましょう。
> session = { } > session[:user_id] = nil > @current_user ||= User.find_by(id: session[:user_id]) > @current_user # => nil > session[:user_id]= User.first.id > @current_user ||= User.find_by(id: session[:user_id]) > @current_user # => #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2020-10-21 14:10:26", updated_at: "2020-10-21 14:10:26", password_digest: [FILTERED]> >
8.2.3 レイアウトリンクを変更する
- ログインした際に表示を変更したい。その際現在ログイン状態かを確認するヘルパーメソッドがほしいため記載していく。
# app/helpers/sessions_helper.rb module SessionsHelper #... def logged_in? !current_user.nil? end end
- ヘッダーの内容を変更する。
- jqueryを有効にする設定を行う。
演習
1. ブラウザのcookieインスペクタ機能を使って(8.2.1.1)、セッション用のcookieを削除してみてください。ヘッダー部分にあるリンクは非ログイン状態のものになっているでしょうか? 確認してみましょう。
- 省略
2. もう一度ログインしてみて、ヘッダーのレイアウトが変わったことを確認してみましょう。その後、ブラウザを再起動させ、再び非ログイン状態に戻ったことも確認してみてください。
- 省略
8.2.4 レイアウトの変更をテストする
- fixture向けのdigestメソッドを追加します。
# app/models/user.rb class User < ApplicationRecord #... def User.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end end
- test実行時に作成するユーザデータをfixtureに記載します。
# test/fixtures/users.yml michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %>
- テストを作成します。fixtureに記載したユーザはモデル名(:オブジェクト名)で取り出すことが出来る。
# test/integration/users_login_test.rb require 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end test "login with invalid information" do get login_path assert_template 'sessions/new' post login_path, params: { session: { email: @user.email, password: 'password' }} assert_redirected_to @user follow_redirect! assert_template 'users/show' assert_select "a[href=?]", login_path, count: 0 assert_select "a[href=?]", logout_path assert_select "a[href=?]", user_path(@user) end end
- テストが成功することを確認します。
$ rails test:integration 4 tests, 22 assertions, 0 failures, 0 errors, 0 skips
演習
1. リスト 8.15の8行目にあるif userから下をすべてコメントアウトすると、ユーザー名とパスワードを入力して認証しなくてもテストが通ってしまうことを確認しましょう(リスト 8.26)。通ってしまう理由は、リスト 8.9では「メールアドレスは正しいがパスワードが誤っている」ケースをテストしていないからです。このテストがないのは重大な手抜かりですので、テストスイートで正しいメールアドレスをUsersのログインテストに追加して、この手抜かりを修正してください(リスト 8.27)。テストが red (失敗)することを確認し、それから先ほどの8行目以降のコメントアウトを元に戻すと green (パス)することを確認してください(この演習の修正は重要なので、この先の 8.3のメインのコードにも修正を反映してあります)。
# app/controllers/sessions_controller.rb class SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user # && user.authenticate(params[:session][:password]) log_in user redirect_to user else flash.now[:danger] = "Invalid email/password combination" render 'new' end end def destory end end
$ rails test:integration 4 tests, 22 assertions, 0 failures, 0 errors, 0 skips
- パスワードを間違えてた場合のテストを追記する。
# test/integration/users_login_test.rb require 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest #... test "login with valid email/invalid password" do get login_path assert_template 'sessions/new' post login_path, params: { session: { email: @user.email, password: 'invalid' }} assert_template 'sessions/new' assert_not flash.empty? get root_path assert flash.empty? end end
- テストが失敗することを確認します。
$ rails test:integration 5 tests, 24 assertions, 1 failures, 0 errors, 0 skips
- コメントアウトを外しテストが成功することを確認します。
$ rails test:integration 5 tests, 26 assertions, 0 failures, 0 errors, 0 skips
2. “safe navigation演算子”(または“ぼっち演算子)と呼ばれる&.を用いて、リスト8.15の8行目の論理値(boolean値)のテストを、リスト 8.2812 のようにシンプルに変えてください。Rubyのぼっち演算子を使うと、obj && obj.methodのようなパターンをobj&.methodのように凝縮した形で書けます。変更後も、リスト 8.27のテストがパスすることを確認してください。
- ぼっち演算子へ変更してテストが成功することを確認します。
$ rails test:integration 5 tests, 26 assertions, 0 failures, 0 errors, 0 skips
8.2.5 ユーザー登録時にログイン
- ユーザ登録が完了したらログイン状態にする。
# app/controllers/user_controller.rb class UsersController < ApplicationController #... def create @user = User.new(user_params) if @user.save log_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end end #... end
- テストからapp内のヘルパーメソッドは使用出来ないためテストヘルパー内にlogged_in?メソッドを定義する。取り違え防止のためis_logged_in?メソッドに名前を変更する。
# test/test_helper.rb #... class ActiveSupport::TestCase # ... def is_logged_in? !session[:user_id].nil? end end
演習
1. リスト 8.29のlog_inの行をコメントアウトすると、テストスイートは red になるでしょうか? それとも green になるでしょうか? 確認してみましょう
- redになる。
2. 現在使っているテキストエディタの機能を使って、リスト 8.29をまとめてコメントアウトできないか調べてみましょう。
8.3 ログアウト
- SessionHelperにログアウト用のメソッドを書いていきます。
# app/helpers/session_helper.rb module SessionsHelper #... def log_out session.delete(:user_id) @current_user = nil end end
- destroyアクションにログアクト処理を実装します。
# app/controllers/sessions_controller.rb class SessionsController < ApplicationController #... def destory log_out redirect_to root_url end end
- ログアクト処理が上手く出来ているかテストを書きます。
# test/integration/users_login_test.rb require 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest #... test "login with valid information followed by logout" do get login_path post login_path, params: { session: { email: @user.email, password: 'password' } } assert is_logged_in? assert_redirected_to @user follow_redirect! assert_template 'users/show' assert_select "a[href=?]", login_path, count: 0 assert_select "a[href=?]", logout_path assert_select "a[href=?]", user_path(@user) delete logout_path assert_not is_logged_in? assert_redirected_to root_url follow_redirect! assert_select "a[href=?]", login_path assert_select "a[href=?]", logout_path, count: 0 assert_select "a[href=?]", user_path(@user), count: 0 end end
- テストが成功することを確認します。
$ rails test:integration 6 tests, 39 assertions, 0 failures, 0 errors, 0 skips
演習
1. ブラウザから[Log out]リンクをクリックし、どんな変化が起こるか確認してみましょう。また、リスト 8.35で定義した3つのステップを実行してみて、うまく動いているかどうか確認してみましょう。
2. cookiesの内容を調べてみて、ログアウト後にはsessionが正常に削除されていることを確認してみましょう。
- 省略
8.4 最後に
- リモートリポジトリにプッシュしてherokuにデプロイします。
$ rails t $ git add -A $ git commit -m "Implement basic login" $ git push origin basic-login $ git checkout master $ git merge basic-login $ git push origin master $ git push heroku
- 以上で8章は終了です。
Ruby on Rails 5 速習実践ガイド 1章
- いままで学習した内容の復習だったため読み通しだけ行いました。内容的にはRubyの文法やテクニックについてでした。曖昧になってる知識のみまとめます。
配列の各要素にメソッドを実行
- 配列に繰り返し処理をする場合&:を使用すると変数の記載なしに実行出来る。
names = users.map { |user| user.name } # 以下のように記述出来る。 names = users.map(&:name)