T_Y_CODE

プログラミング学習記録

学習記録 17日目

17日目の学習記録をまとめていきます。

学習計画

学習内容

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との違いを説明できますか? 少し考えてみましょう。

  • GET login_pathはlogin_pathのページをサーバから取得するリクエスト。POST login_pathは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>
  • 動作確認をします。

f:id:t_y_code:20201022120506p:plain

演習

1. リスト 8.4で定義したフォームで送信すると、Sessionsコントローラのcreateアクションに到達します。Railsはこれをどうやって実現しているでしょうか? 考えてみてください。ヒント:表 8.1とリスト 8.5の1行目に注目してください。

  • フォームを送信するとlogin_pathに対してPOSTリクエストを送ります。ルーティングでlogin_pathに対してPOSTリクエストを送った際はsessions#createアクションを実行するように記載しています。
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
  • 上記のコードには欠点があります。flashは描画されてから次のアクションが実行されるまで残り続けます。上記のケースだとcreateアクション実行→flash描画→newビューをレンダリングとなるため次のアクションを実行した際もflashが残り続けてしまいます。
  • ちなみに前章のUserの新規作成時のflashはcreateアクション実行→flash描画→リダイレクトしてshowアクションを実行のためページ移動すればflashが消えてくれてた。
  • 次のアクションを実行した際にflashを消したい場合はflash.nowを使用する。
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の値について調べてみてください。

  • Safariでは右クリック>要素の詳細を表示>ストレージ>Cookieから確認出来る。

f:id:t_y_code:20201022125232p:plain

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の文法やテクニックについてでした。曖昧になってる知識のみまとめます。
nilガード
  • 変数がnilであれば代入、nilでなければそのままにする。
number ||= 10
ぼっち演算子
  • レシーバがnilでなければそのメソッドを実行する。レシーバがnilの場合nilを返す。
user&.method
  • これは以下と同意
if user
  user.method
else
  nil
end
配列の各要素にメソッドを実行
  • 配列に繰り返し処理をする場合&:を使用すると変数の記載なしに実行出来る。
names = users.map { |user| user.name }
# 以下のように記述出来る。
names = users.map(&:name)

Ruby on Rails 5 速習実践ガイド 2章

  • いままで学習した内容の復習だったため読み通しだけ行いました。内容的にはRailsのインストールについて, scaffoldを使用したCRUDを備えたアプリケーションの作成, MVCについてでした。

本日の総括

  • Railsチュートリアルの内容が徐々に難しくなってきました。1周目では理解できなかった箇所を重点的に学習しました。