T_Y_CODE

プログラミング学習記録

学習記録 18日目

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

学習計画

学習内容

Ruby on Railsチュートリアル 9章

9.1 Remember me 機能
  • 永続cookieを使用してブラウザを再起動した後でもログイン状態を保持できる機能を追加していく。
  • トピックブランチを作成します。
$ git checkout -b advanced-login
9.1.1 記憶トークンと暗号化
$ rails g migration add_remember_digest_to_users remember_digest:string
  • マイグレートします。
$ rails db:migrate
  • ランダムな文字列を作成する必要があります。Ruby標準ライブラリのSecureRandomモジュールのurlsafe_base64メソッドがこの用途にあっています。
$ rails c
> SecureRandom.urlsafe_base64
# => "y0Ih5bXtUwf50Z87qcNbbw"
  • Userモデルのクラスメソッドにランダムなトークンを作成するnew_tokenメソッドを追記します。またremember_digestへデータベース登録するrememberメソッドを作成します。remember_digestにはnew_tokenで作成したランダムな文字列をUser.digestによりさらにハッシュ化させます。
# app/models/user.rb
class User < ApplicationRecord
  attr_accessor :remember_token
  #...

  def User.new_token
    SecureRandom.urlsafe_base64
  end

  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end
end
演習

1. コンソールを開き、データベースにある最初のユーザーを変数userに代入してください。その後、そのuserオブジェクトからrememberメソッドがうまく動くかどうか確認してみましょう。また、remember_tokenとremember_digestの違いも確認してみてください。

$ rails c --sandbox
> user = User.first
> user.remember
> user.remember_digest
# => "$2a$12$8zaItKWLQkIIToSEo/VhwOK6UcngE15xP0S15x95ykcnZAi7AHCky"
> user.remember_token
# => "g8xySprIPmPf4qDrdLJJ5Q"

2. リスト 9.3では、明示的にUserクラスを呼び出すことで、新しいトークンやダイジェスト用のクラスメソッドを定義しました。実際、User.new_tokenやUser.digestを使って呼び出せるようになったので、おそらく最も明確なクラスメソッドの定義方法であると言えるでしょう。しかし実は、より「Ruby的に正しい」クラスメソッドの定義方法が2通りあります。1つはややわかりにくく、もう1つは非常に混乱するでしょう。テストスイートを実行して、ややわかりにくいリスト 9.4の実装でも、非常に混乱しやすいリスト 9.5の実装でも、いずれも正しく動くことを確認してみてください。ヒント: selfは、通常の文脈ではUser「モデル」、つまりユーザーオブジェクトのインスタンスを指しますが、リスト 9.4やリスト 9.5の文脈では、selfはUser「クラス」を指すことにご注意ください。わかりにくさの原因の一部はこの点にあります。

  • 個人的にはselfが付いたらクラスメソッドと判断できselfの方が分かりやすいです。
# app/models/user.rb
class User < ApplicationRecord
  #...

  def self.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
                                                  BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  def self.new_token
    SecureRandom.urlsafe_base64
  end

  #...
end
9.1.2 ログイン状態の保持
  • 個別のcookiesはvalueとexpiresからなるハッシュで出来ています。
cookies[:remember_token] = { value: remember_token, expires: 20.years.from_now.utc }
  • 攻撃者がcookieを奪い取ることを避ける手段として署名付きcookieの使用があります。以下のようにすれば署名付きになります。
cookies.signed[:user_id] = user.id
||
- cookieの永続化(20年)をするにはparmanent(20年の期限を設定してくれるRailsのメソッド)メソッドを使用します。
>|ruby|
cookies.parmanent.signed[:user_id] = user.id
||
- 渡されたトークンがremember_digestと一致したらtrueを返すメソッドを追記する。
>|ruby|
# app/models/user.rb
class User < ApplicationRecord
  #...

  def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

  #...
end
  • ここらへんの理解が曖昧だったのでrailsコンソールで動作確認してみます。
$ rails c
> user = User.first
> user.remember
> user.remember_token
# => "k3w1u-gPF8EfaPrGZswP2Q"
> user.remember_digest
# => "$2a$12$45ackForGev3kW.7yM45bezbceF3Q7Teq0U69IKiyoqB2serI3tFm"
> BCrypt::Password.new(user.remember_digest).is_password?(user.remember_token)
# => true
# 本来の==であればfalseだがbcryptは==をオーバライドしており実際にはis_password?が使用されている。
> BCrypt::Password.new(user.remember_digest) == user.remember_token
# => true
  • ユーザーがログインした際にrememberメソッドを実行するようにする。まずはsessions_controller.rbに記載します。
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user&.authenticate(params[:session][:password])
      log_in user
      remember user
      redirect_to user
    else
      flash.now[:danger] = "Invalid email/password combination" 
      render 'new'
    end
  end

  def destroy
    log_out
    redirect_to root_url
  end
end
  • rememberメソッドはUserモデルでしか使用出来ないためヘルパーを書きます。永続署名付きセッションもここで作成する。署名付きのため攻撃への対策もしている。
# app/helpers/sessions_helper.rb
module SessionsHelper
  #...

  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user_id
    cookies.permanent[:remember_token] = user.remember_token
  end
end
  • sessionsヘルパーのcurrent_userメソッドは一時セッションにしか対応していないため記載し直す。
# app/helpers/sessions_helper.rb
module SessionsHelper
  #...

  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user&.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

  #...
end
  • これで永続ログインに対応しました。しかしログアウトしても永続セッションが残っているためログアウト処理が上手くいきません。よってテストを行っても失敗します。
$ rails t
23 tests, 64 assertions, 1 failures, 0 errors, 0 skips
演習

1. ブラウザのcookieを調べ、ログイン後のブラウザではremember_tokenと暗号化されたuser_idがあることを確認してみましょう。

  • 作成されています。しっかり暗号化されています。

f:id:t_y_code:20201023121232p:plain
2. コンソールを開き、リスト 9.6のauthenticated?メソッドがうまく動くかどうか確かめてみましょう。

$ rails c
> user = User.first
> user.remember
# => true
> user.authenticated?(user.remember_token)
# => true
9.1.3 ユーザーを忘れる
  • ユーザがログアウトした際にremember_digest, 永続セッションを削除するようにします。
# app/models/user.rb
class User < ApplicationRecord
  #...

  def forget
    update_attribute(:remember_digest, nil)
  end
end
# app/helpers/sessions_helper.rb
module SessionsHelper
  #...

  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end

  #...

  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end
end
  • これでログアウト時に永続セッションが削除されログアウト処理が上手く作動する。
$ rails t
23 tests, 66 assertions, 0 failures, 0 errors, 0 skips
演習

1. ログアウトした後に、ブラウザの対応するcookiesが削除されていることを確認してみましょう。

  • 削除されています。

f:id:t_y_code:20201023122222p:plain

9.1.4 2つの目立たないバグ
  • 現状2つの小さなバグが残っている。
    • 複数タブでログイン状態にしておきログアウト後別タブでログアウトしようとするとエラーが発生する。これはlog_outメソッド内のforget(current_user)のcurrent_userがnilになっているためである。対処法として、ユーザーのログアウト処理はログイン中の場合のみ実行させるようにする。
    • 複数ブラウザでログイン状態にし片方のブラウザでログアウトした後もう片方のブラウザでアクセスするとエラーになる。これはデータベース上のremember_digestがnilになりauthenticated?が上手く作動していないためである。この問題を解決するにはremember_digestが存在しない時はfalseを返す処理をauthenticated?に追加する必要がある。
  • まずはテストを書いていきます。
# 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
    # ここで別タブでのログアウトを行う
    delete logout_path
    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, 36 assertions, 0 failures, 1 errors, 0 skips
  • ログアウト処理をログイン中にしか行えないようにします。
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  #...

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end
$ rails test:integration
6 tests, 39 assertions, 0 failures, 0 errors, 0 skips
  • 2番目の問題についてのテストを書いていきます。
# test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  #...

  test "authenicated? shold return false for a user with nil digest" do
    assert_not @user.authenticated?('')
  end
end
  • rememberメソッドを呼び出す前にauthenicated?を使用しているのでremember_digestはnilになっています。トークンを空白にしていますが何を入力してもエラーになります。
$ rails test:integration
7 tests, 39 assertions, 0 failures, 1 errors, 0 skips
  • authenicated?に早期リターンを記載します。
# app/models/user.rb
class User < ApplicationRecord
  #...

  def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

  #...
end
  • これでテストが成功するようになりました。
$ rails t
24 tests, 67 assertions, 0 failures, 0 errors, 0 skips
演習
  • 動作確認のため省略
9.2 [Remember me]チェックボックス
# 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 :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>remember me on this computer</span>
      <% end %>

      <%= 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:20201023124447p:plain

  • チェックボックスがチェックされた時永続セッションを作成します。
  • チェックボックスはparamsで取得出来ます。上記フォームの場合ならparams[:session][:remember_me]で取得可能。チェックが付いてれば1, 付いてなければ0が格納されます。nilではないので注意。
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  #...

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user&.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_to user
    else
      flash.now[:danger] = "Invalid email/password combination" 
      render 'new'
    end
  end

  #...
end
演習

1. ブラウザでcookies情報を調べ、[remember me]をチェックしたときに意図した結果になっているかどうかを確認してみましょう。

  • remember meをチェックした際のcookies情報です。意図した結果になっています。

f:id:t_y_code:20201023125339p:plain
2. コンソールを開き、三項演算子を使った実例を考えてみてください(コラム 9.2)。

$ rails c
>puts 1.nil? ? "nilだね" : "nilじゃないね"
nilじゃないね
9.3.1 [Remember me]ボックスをテストする
  • まずはテストでログイン処理を行うヘルパーを記載する。アプリケーション内のlog_inメソッドと混合しないようlog_in_asという名前にする。
# test/test_helper.rb
#...
class ActiveSupport::TestCase
  #...

  def log_in_as(user)
    session[:user_id] = user.id
  end
end
  • integrationテストではsessionを直接取り扱うことが出来ないためSessionsリソースに対してpostを送信することで代用します。integrationテスト中の実行なのでActionDispatch::IntegrationTestクラス内に記載する。
# test/test_helper.rb
#...
class ActiveSupport::TestCase
  #...
end

class ActionDispatch::IntegrationTest
  def log_in_as(user, password: 'password', remember_me: '1')
    post login_path, params: { session: { email: user.email,
                                          password: password,
                                          remember_me: remember_me } }
  end
end
  • テストを書いていきます。
# test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  #...

  test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_not_empty cookies[:remember_token]
  end

  test "login without remembering" do
    log_in_as(@user, remember_me: '1')
    assert_not_empty cookies[:remember_token]
    delete logout_path
    log_in_as(@user, remember_me: '0')
    assert_empty cookies[:remember_token]
  end
end
  • テストが成功することを確認します。
$ rails test:integration
9 tests, 46 assertions, 0 failures, 0 errors, 0 skips
演習

1. リスト 9.25の統合テストでは、仮想のremember_token属性にアクセスできないと説明しましたが、実は、assignsという特殊なテストメソッドを使うとアクセスできるようになります。コントローラで定義したインスタンス変数にテストの内部からアクセスするには、テスト内部でassignsメソッドを使います。このメソッドにはインスタンス変数に対応するシンボルを渡します。例えばcreateアクションで@userというインスタンス変数が定義されていれば、テスト内部ではassigns(:user)と書くことでインスタンス変数にアクセスできます。本チュートリアルのアプリケーションの場合、Sessionsコントローラのcreateアクションでは、userを(インスタンス変数ではない)通常のローカル変数として定義しましたが、これをインスタンス変数に変えてしまえば、cookiesにユーザーの記憶トークンが正しく含まれているかどうかをテストできるようになります。このアイデアに従ってリスト 9.27とリスト 9.28の不足分を埋め(ヒントとして?やFILL_INを目印に置いてあります)、[remember me]チェックボックスのテストを改良してみてください。17

  • assignメソッドでコントローラ内のインスタンス変数にアクセス出来るようにしたい。コントローラ内のuserをインスタンス変数にします。(現在はローカル変数になってしまっている)
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  #...

  def create
    @user = User.find_by(email: params[:session][:email].downcase)
    if @user&.authenticate(params[:session][:password])
      log_in @user
      params[:session][:remember_me] == '1' ? remember(@user) : forget(@user)
      redirect_to @user
    else
      flash.now[:danger] = "Invalid email/password combination" 
      render 'new'
    end
  end

  #...
end
  • テストを改良して成功することを確認します。
# test/integration/users_login_test.rb
require 'test_helper'

class UsersLoginTest < ActionDispatch::IntegrationTest
  #...

  test "login with remembering" do
    log_in_as(@user, remember_me: '1')
    assert_equal cookies[:remember_token], assigns(:user).remember_token
  end

  test "login without remembering" do
    log_in_as(@user, remember_me: '1')
    assert_equal cookies[:remember_token], assigns(:user).remember_token
    delete logout_path
    log_in_as(@user, remember_me: '0')
    assert_empty cookies[:remember_token]
  end
end
$ rails t
26 tests, 71 assertions, 0 failures, 0 errors, 0 skips
9.3.2 [Remember me]をテストする
  • current_userメソッド内のテストを全くしていないためわざとcurrent_userメソッド内でエラーを発生させます。
# app/helpers/sessions_helper.rb
module SessionsHelper
  #...

  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      raise
      user = User.find_by(id: user_id)
      if user&.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

  #...
end
  • テストを実行すると成功してしまいます。
$ rails t
26 tests, 71 assertions, 0 failures, 0 errors, 0 skips
  • SessionsHelperのテストを作成します。
# test/helpers/sessions_helper_test.rb
require 'test_helper'

class SessionsHelperTest < ActionView::TestCase

  def setup
    @user = users(:michael)
    remember(@user)
  end

  test "current_user returns right user when session is nil" do
    assert_equal @user, current_user
    assert is_logged_in?
  end

  test "current_user returns nil when remember digest is wrong" do
    @user.update_attribute(:remember_digest, User.digest(User.new_token))
    assert_nil current_user
  end
end
  • まずsetupメソッド内のrememberメソッドで@userを永続セッションさせます。
  • 1つ目のテストで@userとcurrent_userメソッドの戻り値が同じかを検証しています。これで先ほどraiseした箇所を調べています。is_logged_in?ヘルパーメソッドは一時セッションがあるかを確認しています。
  • 2つ目のテストでremember_digestにセッションとは一致しないトークンを入れています。そしてcurrent_userメソッドが一致しないことを検知しnilを返すか検証しています。
  • このテストはcurrent_userメソッド内のraiseにより失敗します。
$ rails t
28 tests, 71 assertions, 0 failures, 2 errors, 0 skips
  • raiseを削除するとテストは成功します。
$ rails t
28 tests, 74 assertions, 0 failures, 0 errors, 0 skips
演習

1. リスト 9.33にあるauthenticated?の式を削除すると、リスト 9.31の2つ目のテストで失敗することを確かめてみましょう(このテストが正しい対象をテストしていることを確認してみましょう)。

# app/helpers/sessions_helper.rb
module SessionsHelper
  #...

  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user# &.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

  #...
end
$ rails t
28 tests, 74 assertions, 1 failures, 0 errors, 0 skips
9.4 最後に
  • リモートリポジトリにプッシュしてherokuにデプロイします。
$ git add -A
$ git commit -m "Implement advanced login"
$ git push origin advanced-login
$ git checkout master
$ git merge advanced-login
$ git push origin master
$ heroku maintenance:on
$ git push heroku
$ heroku run rails db:migrate
$ heroku maintenance:off
  • 以上で9章は終了です。

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

3-1-3 アプリケーションのひな形を作成する
  • scaffoldを使用せずにCRUDを備えたアプリケーションを作成します。
  • データベースにはpostgresqlを使用します。データベースの指定は-dオプションで行います。
  • railsアプリケーションを新規作成します。
$ rails new taskleaf -d postgresql
  • 本アプリケーションもリモートリポジトリに上げていきます。
$ git init
$ git add -A
$ git commit -m "Initialize a Repository"
$ git remote add origin https://github.com/***/***.git
$ git push origin master
  • データベースを作成します。
$ cd taskleaf/
$ rails db:create
  • railsサーバの動作確認をします。
$ rails s
  • 動いてますね。

f:id:t_y_code:20201023150114p:plain

3-1-5 ビュー層を効率良く書くためにSlimを使えるようにする
  • RailsではデフォルトでERBを採用しているが本書ではSlimを使用していく。他にもテンプレートエンジンにはHalmがあるが個人的にSlimの文法の方が好みです。
  • slim-rails, html2slim gemを追加します。Gemfileに追記してbundle installします。
$ bundle install
  • 現在ビューファイルが3つ存在しているのでSlimへ変換しておきます。bundle execとはbundleでインストールされたgemをターミナルで使用するためのコマンドです。--deleteオプションで元ファイルを削除します。
$ bundle exec erb2slim app/views/layouts/ --delete
3-1-6 アプリケーションの見栄えを良くするためにBootstrapを導入する
  • Gemfileに追記してbundle installします。
$ bundle install
  • application.cssを削除してapplication.scssを作成してbootstrapをインポートします。
@import "bootstrap";
  • コンテナを作成します。ヘッダーのサイト名についても記載しておきます。
doctype html
html
  head
    title
      | Postleaf
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
  body
    .app-title.navbar.navbar-expand-md.navbar-light.bg-light
      .navbar-brand Taskleaf
    .container
      = yield
  • divタグを記載せずにdivタグが書けるslimめっちゃ便利。
3-1-7 Railsのエラーメッセージなどを日本語で出せるようにする
  • エラーメッセージを日本語化します。GitHub上に翻訳されたファイルがあるのでそれを利用します。ダウンロードします。本書ではwgetを使用しているが使用できなかったので同機能のcurlを使用してダウンロードする。
$ curl https://raw.githubusercontent.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml > config/locales/ja.yml
  • 設定ファイルを作成します。
$ cat > config/initializers/locale.rb
Rails.application.config.i18n.default_locale = :ja
  • リモートリポジトリへプッシュします。ブランチし忘れてmasterで作業してたのでstashに避難させてブランチを作成してからプッシュします。
$ git stash
$ git checkout -b chapter-3-1
$ git stash apply
$ git add -A
$ git commit -m "Finish chapter 3-1"
$ git push origin chapter-3-1
3-2-1 タスクモデルの属性を設計する
  • トピックブランチをchapter-3-2へ移動します。
$ git checkout -b chapter-3-2
  • Taskモデルにはname:string, description:textカラムを作成します。
3-2-2 タスクモデルのひな形を作成する
  • generate modelコマンドでTaskモデルを作成する。
$ rails g model Task name:string description:text
Running via Spring preloader in process 8842
      invoke  active_record
      create    db/migrate/20201023063535_create_tasks.rb
      create    app/models/task.rb
      invoke    test_unit
      create      test/models/task_test.rb
      create      test/fixtures/tasks.yml
3-2-3 マイグレーションでデータベースにテーブルを追加する
$ rails db:migrate
$ git add -A
$ git commit -m "Finish chapter 3-2"
$ git push origin chapter-3-2
3-3 コントローラとビュー
  • トピックブランチを作成します。
$ git checkout -b chapter-3-3
  • CRUDの機能を満足するアクションを備えたコントローラを作成します。
$ rails g controller tasks index show new edit
  • ルーティングのtasksの記述をresourcesへ変更しRESTfulなルーティングを一括で行う。またルートディレクトリのアクションをtasks#indexにする。
# config/routes.rb
Rails.application.routes.draw do
  root 'tasks#index'
  resources :tasks
end
  • 動作確認します。ちゃんと動作してますね。

f:id:t_y_code:20201023154643p:plain

3-3-1-1 一覧画面に新規登録リンクを追加する
  • indexビューに新規登録リンクを追加します。
# app/views/tasks/index.html.slim
h1 タスク一覧
= link_to '新規登録', new_task_path, class: 'btn btn-primary'

f:id:t_y_code:20201023155501p:plain

3-3-1-2 モデルの翻訳情報を追加する
  • モデルを作成したので翻訳情報を追記します。
# config/locales/ja.yml
---
ja:
  activerecord:
    errors:
      messages:
        record_invalid: 'バリデーションに失敗しました: %{errors}'
        restrict_dependent_destroy:
          has_one: "%{record}が存在しているので削除できません"
          has_many: "%{record}が存在しているので削除できません"
    models:
      task: タスク
    attributes:
      task:
      id:
      name: 名称
      description: 詳しい説明
      created_at: 登録日時
      updated_at: 更新日時
#...
3-3-1-4 アクションへデータを送る「リクエストパラメータ」
  • リクエストパラメータの送り方には2種類ある。
    • POSTで送る。formから送信する。
    • GETで送る。URLで?以降に送信したいパラメータを記述することで送信出来る。
  • どちらともparamsで受け取ることが出来る。
3-3-1-5 新規登録画面のビューを実装する
  • newアクションのビューを実装します。
# app/views/tasks/new.html.slim
h1 タスクの新規登録

.nav.justify-content-end
  = link_to '一覧', tasks_path, class: 'nav-link'

= form_with model: @task, local: true do |f|
  .form-group
    = f.label :name
    = f.text_field :name, class: 'form-control', id: 'task_name'
  .form-group
    = f.label :description
    = f.text_area :description, class: 'form-control', id: 'task_description'
  = f.submit nil, class: 'btn btn-primary'
  • submitの文字がnilだが動作確認すると保存するになっている。

f:id:t_y_code:20201023160400p:plain

  • これは翻訳情報のhelpers下に定義された文字を元に自動で挿入されている。
# config/locales/ja.yml
#...
  helpers:
    select:
      prompt: 選択してください
    submit:
      create: 登録する
      submit: 保存する
      update: 更新する
#...
3-3-1-6 登録アクションを実装する
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  #...

  def create
    task = Task.new(task_params)
    task.save!
    redirect_to tasks_url, notice: "タスク「#{task.name}」を登録しました。"
  end

  private
    def task_params
      params.requre(:task).permit(:name, :description)
    end
end
3-3-1-8 Flashメッセージ
  • リダイレクト後flashメッセージを表示します。以下のコードは
redirect_to tasks_url, notice: "タスク「#{task.name}」を登録しました。"
  • 以下と同意である。
flash[:notice] = "タスク「#{task.name}」を登録しました。"
redirect_to tasks_url
  • flashメッセージを表示するコードを記載する。
# app/views/layouts/application.html.slim
doctype html
html
  head
    title
      | Postleaf
    = csrf_meta_tags
    = csp_meta_tag
    = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
  body
    .app-title.navbar.navbar-expand-md.navbar-light.bg-light
      .navbar-brand Taskleaf
    .container
      - if flash.notice.present?
        .alert.alert-success= flash.notice
      = yield
  • 動作確認します。

f:id:t_y_code:20201023161906p:plain

3-3-2-1 一覧表示アクションでタスクデータを取得する
  • まずはindexビューで使用するインスタンス変数tasksを定義します。
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  def index
    @tasks = Task.all
  end
  #...
end
3-3-2-2 一覧画面ですべてのタスクデータを表示する
  • ビューでタスクを一覧表示する実装をします。
# app/views/tasks/index.html.slim
h1 タスク一覧
= link_to '新規登録', new_task_path, class: 'btn btn-primary'

.mb-3
table.table.table-hover
  thead.thead-default
    tr
      th= Task.human_attribute_name(:name)
      th= Task.human_attribute_name(:created_at)
    tbody
      - @tasks.each do |task|
        th= task.name
        th= task.created_at
3-3-3 詳細表示機能を実装する
  • task.nameにリンクを付けて詳細表示画面に飛べるようにします。
# app/views/tasks/index.html.slim
#...
      - @tasks.each do |task|
        th= link_to task.name, task
        th= task.created_at
3-3-3-1 指定されたタスクを詳細表示アクションで取得
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  #...
  def show
    @task = Task.find(params[:id])
  end
  #...
end
3-3-3-2 詳細画面にタスクの属性情報を表示する
  • showビューを実装します。
  • simple_formatメソッドは改行(\n)をbrと解釈してくれる。hメソッドは<などを<へ変換してくれる。
# app/views/tasks/new.html.slim
h1 タスクの詳細

.nav.justify-content-end
  = link_to '一覧', tasks_path, class: 'nav-link'
table.table.table-hover
  tbody
    tr
      th= Task.human_attribute_name(:id)
      td= @task.id
    tr
      th= Task.human_attribute_name(:name)
      td= @task.name
    tr
      th= Task.human_attribute_name(:description)
      td= simple_format(h(@task.description), {}, sanitize: false, wrapper_tag: "div")
    tr
      th= Task.human_attribute_name(:created_at)
      td= @task.created_at
    tr
      th= Task.human_attribute_name(:updated_at)
      td= @task.updated_at
  • 動作確認をします。一覧表示できました。

f:id:t_y_code:20201023163556p:plain

3-3-4 編集機能を実装する
  • editアクション, ビューを実装していきます。
  • まずはeditへのリンクをindexビューへ追記します。
# app/views/tasks/index.html.slim
h1 タスク一覧
= link_to '新規登録', new_task_path, class: 'btn btn-primary'

.mb-3
table.table.table-hover
  thead.thead-default
    tr
      th= Task.human_attribute_name(:name)
      th= Task.human_attribute_name(:created_at)
      th
    tbody
      - @tasks.each do |task|
        tr
          td= link_to task.name, task
          td= task.created_at
          td
            = link_to '編集', edit_task_path(task), class: 'btn btn-primary mr-3'
  • editアクションにインスタンス変数を設定します。
  • updateアクションにはupdate!メソッドでStrong Parameters経由でデータベースの更新を行っています。
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  #...

  def edit
    @task = Task.find(params[:id])
  end

  def update
    task = Task.find(params[:id])
    task.update!(task_params)
    redirect_to tasks_url, notice: "タスク「#{task.name}」を更新しました。"
  end

  #...
end
  • editビューを実装します。h1の内容以外newビューと同じ内容になっています。
# app/views/tasks/edit.html.slim
h1 タスクの編集

.nav.justify-content-end
  = link_to '一覧', tasks_path, class: 'nav-link'

= form_with model: @task, local: true do |f|
  .form-group
    = f.label :name
    = f.text_field :name, class: 'form-control', id: 'task_name'
  .form-group
    = f.label :description
    = f.text_area :description, rows: 5, class: 'form-control', id: 'task_description'
  = f.submit nil, class: 'btn btn-primary'
3-3-4-1 パーシャルを使った新規登録画面と編集画面の共通化
  • パーシャルを使用してフォーム部を共通化します。
$ touch app/views/tasks/_form.html.slim
# app/views/tasks/_form.html.slim
h1 タスクの編集

.nav.justify-content-end
  = link_to '一覧', tasks_path, class: 'nav-link'

= form_with model: task, local: true do |f|
  .form-group
    = f.label :name
    = f.text_field :name, class: 'form-control', id: 'task_name'
  .form-group
    = f.label :description
    = f.text_area :description, rows: 5, class: 'form-control', id: 'task_description'
  = f.submit nil, class: 'btn btn-primary'
# app/views/tasks/new.html.slim
h1 タスクの新規登録

= render partial: 'form', locals: { task: @task }
# app/views/tasks/edit.html.slim
h1 タスクの編集

= render partial: 'form', locals: { task: @task }
  • DRYになりました。localsでローカル変数を定義しています。
3-3-5 削除機能を実装する
  • まずはindexビューに削除ボタンを実装します。taskに対してDELETEリクエストしています。
# app/views/tasks/index.html.slim
#...
    tbody
      - @tasks.each do |task|
        tr
          td= link_to task.name, task
          td= task.created_at
          td
            = link_to '編集', edit_task_path(task), class: 'btn btn-primary mr-3'
            = link_to '削除', task, method: :delete, data: { confirm: "タスク「#{task.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger'
  • showビューにも同様のボタンを実装します。
# app/views/tasks/show.html.slim
#...
= link_to '編集', edit_task_path, class: 'btn btn-primary mr-3'
= link_to '削除', @task, method: :delete, data: { confirm: "タスク「#{@task.name}」を削除します。よろしいですか?" }, class: 'btn btn-danger'
  • Taskコントローラのdestroyアクションを実装します。
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  #...

  def destroy
    task = Task.find(params[:id])
    task.destroy
    redirect_to rasks_url, notice: "タスク「#{task.name}」を削除しました。"
  end

  #...
end
  • 動作確認します。しっかり削除できています。

f:id:t_y_code:20201023170459p:plain

  • 最後にリモートリポジトリにプッシュします。
$ git add -A
$ git commit -m "Finish Chapter 3-3"
$ git push origin chapter-3-3
$ git checkout master
$ git merge chapter-3-3
$ git push origin master
  • 以上で3章は終了です。

本日の総括

  • 1周目の9章はついていくので精一杯だった記憶がありました。今回2周目ですが細かい部分の理解をする余裕が生まれていました。少しずつ成長出来てるのかな?明日も頑張りたい。