skip to content
Site header image satoooh.org

Ruby on Rails チュートリアルを初心者が進める記録 (Rails 5.1)

Ruby on Rails チュートリアルを初心者が進める記録 (Rails 5.1)

Last Updated:

(色々あって)ある日突然、 一刻も早く Ruby on Rails を使いこなせる状態にならなければいけなくなった ので、先輩エンジニアに泣きついて教えてもらった Ruby on Rails 入門の決定版、Rails チュートリアル を進めています。

Ruby on Rails チュートリアルとは

Ruby で書かれた Web 開発フレームワークである Ruby on Rails を使って、全14章に分けて開発をしていきます。Git, Bitbucket, Heroku などを用い、テスト駆動開発(= TDD)を実践しながら進めていくので非常に実践的なチュートリアルになっていて、ボリュームは満点ですがめちゃくちゃ勉強になります。

わからなかったらProgateとかやって基本知識を身につけてから挑戦しよう

Rails チュートリアルは解説が非常に丁寧とはいっても、初学者にはなかなかハードルが高いのも事実。コマンドラインの基本的ないじり方、プログラミング言語の基本的な文法や仕組みなどの知識に不安がある人は、 Progate などもう少しやさしいサービスで知識をつけてから挑戦するのが良いかと思います。

勉強開始時期と当時のスペック

  • 2019-07-14 に開始
  • Ruby on Rails って何?という状態
  • Python で競技プログラミングを少しやっていたのでコードを読むことに抵抗はそこまでないが、Ruby はほとんど知らない状態

第1章: ゼロからデプロイまで

1章では、文字通りかんたんなアプリをゼロから作ってデプロイ(アプリとして皆が使えるように公開する)まで一気に駆け抜けます。Hello World! みたいなのが表示されるだけだから怖くない。

学べること

  • Rails とは何か
  • AWS Cloud9 の環境構築
  • MVC モデルとは何か
  • Git を使ったバージョン管理
  • Heroku へのデプロイ
AWS Cloud9 をつかって開発していくよ
AWS Cloud9 をつかって開発していくよ
app/ モデル・ビュー・コントローラ・ヘルパーなどを含む、主要なアプリケーションコード
app/assets アプリケーションで使う CSS、JS、画像などのアセット
bin/ バイナリ実行可能ファイル
config/ アプリケーションの設定(configuration)
db/ データベース関連のファイル
doc/ マニュアルなど、アプリケーションのドキュメント
lib/ ライブラリモジュール
lib/assets ライブラリで使う CSS、JS、画像などのアセット
log/ アプリケーションのログファイル
public/ エラーページなど、一般に直接公開するデータ
bin/rails コード生成、コンソール起動、ローカルの Web サーバの立ち上げなどで使う Rails スクリプト
test/ アプリケーションのテスト
tmp/ 一時ファイル
vendor/ サードパーティのプラグイン、gem など
vendor/assets サードパーティのプラグインや gem で使う CSS、JS、画像などのアセット
README.md アプリケーションの簡単な説明
Rakefile rake コマンドで使えるタスク
Gemfile このアプリケーションに必要な gem の定義ファイル
Gemfile.lock アプリケーションで使われる gem のバージョンを確認するためのリスト
config.ru Rack ミドルウェア用の設定ファイル
.gitignore Git に取り込みたくないファイルを指定するためのパターン

第2章 Toyアプリケーション(所要時間: 2h)

この章では、Toy アプリケーションという簡単なアプリを scaffold を利用して簡単に作成し、Rails アプリケーションの概要や MVC モデルの挙動をざっくり理解します。

学べること

  • かんたんなアプリを scaffold を使って作成する
  • REST アーキテクチャ
  • データモデルの作成
  • rails console の使い方


HTTPの4つの基本操作:

GET Web上のデータを取得するときに使う。読み取り専用で、サーバー上のデータを変更しない
POST Webページ上のフォームに入力した値をブラウザからサーバーに送信するときに使う。新しいリソースの作成やデータの送信に使う
PATCH サーバー上のリソースの一部を更新するときに使う
DELETE サーバー上のリソースを削除するときに使う

第3章 ほぼ静的なページの作成(所要時間: 1.5h)

この章では、今後改修を重ねていくアプリである sample_app の基本的な部分を作っていきます。主に静的なページを作成し、自動化テストの雰囲気を掴みます。

学ぶこと:

  • RED, GREEN, REFACTOR サイクル
  • テスト駆動開発(TDD)
  • 埋め込み Ruby(Embedded Ruby)
    • .erb は埋め込みRubyの拡張子で、次のような構文が使えて便利
      • <% ... %> 中に書いたコードを実行する
      • <%= ... %> 中に書いたコードを実行し、実行結果を templates の中に挿入する

第4章 Rails 風味の Ruby(所要時間: 2h)

この章では Ruby の基本的な文法を rails console を使いながら学んでいきます。

演習4.4.5

example_user.rb を次のように修正した

class User
  attr_accessor :first_name, :last_name, :email

  def initialize(attributes = {})
    @first_name  = attributes[:first_name]
    @last_name = attributes[:last_name]
    @email = attributes[:email]
  end
  
  def full_name
    @full_name = "#{@first_name} #{@last_name}"
  end
  
  def alphabetical_name
    @alphabetical_name = "#{@last_name}, #{@first_name}"
  end

  def formatted_email
    "#{@full_name} <#{@email}>"
  end
end

コンソール上で処理を確認してみる

>> require './example_user'
=> true
>> exam = User.new
=> #<User:0x0000000003136598 @first_name=nil, @last_name=nil, @email=nil>
>> exam.first_name = "Michael"
=> "Michael"
>> exam.last_name = "Hartl"
=> "Hartl"
>> exam.email = "[email protected]"
=> "[email protected]"
>> exam.full_name
=> "Michael Hartl"
>> exam.formatted_email
=> "Michael Hartl <[email protected]>"
>> exam.alphabetical_name
=> "Hartl, Michael"
>> exam.full_name.split == exam.alphabetical_name.split(', ').reverse
=> true

第5章 レイアウトを作成する(所要時間: 1.5h)

この章では、アプリケーションに Bootstrap フレームワークを埋め込み、カスタムスタイルを追加していきます。パーシャル、Rails のルーティング、Asset Pipeline、Sass についても勉強していきます。

学ぶこと

  • パーシャル機能を使うといい感じに整理できる
  • Sass のネスト構造と変数
  • Bootstrap フレームワークを使うと、いい感じのデザインをすばやく実装できる

演習5.1.3

layouts/_rails_default.html.erb として以下のファイルを作成

<%= csrf_meta_tags %>
<%= stylesheet_link_tag    'application', media: 'all',
                           'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application',
                            'data-turbolinks-track': 'reload' %>

第6章 ユーザーのモデルを作成する(所要時間: 2.5h)

この章からはユーザー登録・ログイン周りを扱っていきます。データベース(DB)の基礎を学んだり、Rubular を使って試しながら正規表現を利用してユーザー認証機能を実装したり。

学ぶこと

  • DB Browser for SQLite で DB の構造をみる
  • 正規表現
  • ユーザーの検証(存在性・長さ・フォーマット・一意性)
  • has_secure_password メソッドで、モデルに対してセキュアなパスワードを追加する

演習6.2.4

foo@bar..com  を許容してしまっている様子
[email protected] を許容してしまっている様子
foo@bar..com  も除外できた!
[email protected] も除外できた!

演習6.3.2

>> u = User.new(name: "Satoooh", email: "[email protected]")
=> #<User id: nil, name: "Satoooh", email: "[email protected]", created_at: nil, updated_at: nil, password_digest: nil>
>> u.valid?
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "[email protected]"], ["LIMIT", 1]]
=> false
>> u.errors.messages
=> {:password=>["can't be blank"]}

演習6.3.3

>> u = User.new(name: "hogekosan", email: "email.hoge.com", password: "hoge")
=> #<User id: nil, name: "hogekosan", email: "email.hoge.com", created_at: nil, updated_at: nil, password_digest: "$2a$10$dPKFHrcT0Vi3RL2Pb3y93OsbLacqovP2ZL6wMLzcM4K...">
>> u.valid?
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) LIMIT ?  [["email", "email.hoge.com"], ["LIMIT", 1]]
=> false
>> u.errors.messages
=> {:email=>["is invalid"], :password=>["is too short (minimum is 6 characters)"]}

演習6.3.4

>> user = User.find_by(email: "[email protected]")
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "[email protected]"], ["LIMIT", 1]]
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]", created_at: "2019-07-16 07:05:28", updated_at: "2019-07-16 07:05:28", password_digest: "$2a$10$hRCY59oDn.dV4ODfo9mHoOdgbGbRBvgMU9hbM74fI5L...">
>> user.name = "Satoooh"
=> "Satoooh"
>> user.save
   (0.1ms)  begin transaction
  User Exists (0.2ms)  SELECT  1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER(?) AND ("users"."id" != ?) LIMIT ?  [["email", "[email protected]"], ["id", 1], ["LIMIT", 1]]
   (0.1ms)  rollback transaction
=> false

save できなかったのは、パスワードも更新することになるから?
てことは :name だけ更新する形なら可能なのかな?

>> user.update_attribute(:name, "El Duderino")
   (0.5ms)  begin transaction
  SQL (1.9ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "El Duderino"], ["updated_at", "2019-07-16 07:15:41.704158"], ["id", 1]]
   (5.9ms)  commit transaction
=> true
>> user.name
=> "El Duderino"

できた

第7章 ユーザー登録(所要時間: 2.5h)

デバッグを導入したり、 Gravatar で画像表示したり、フォームの処理を実装したり、flash メッセージを出したり、プロのデプロイをしたり盛り沢山です。なんか急に難しくなった。

学ぶこと

  • debug メソッドでデバッグ情報を表示する
  • よくわからない挙動が Rails アプリケーション内にあったら、 debugger を差し込んで調べてみよう
  • flash 変数の使い方
  • セキュアな通信・ハイパフォーマンスのための SSL, Puma の導入

演習7.1.1

/about にアクセスしたときのコントローラとアクション

  • controller: static_pages
  • action: about
>> user = User.first
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "El Duderino", email: "[email protected]", created_at: "2019-07-16 07:05:28", updated_at: "2019-07-16 07:15:41", password_digest: "$2a$10$hRCY59oDn.dV4ODfo9mHoOdgbGbRBvgMU9hbM74fI5L...">
>> puts user.attributes.to_yaml
---
id: 1
name: El Duderino
email: [email protected]
created_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &1 2019-07-16 07:05:28.752110000 Z
  zone: &2 !ruby/object:ActiveSupport::TimeZone
    name: Etc/UTC
  time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &3 2019-07-16 07:15:41.704158000 Z
  zone: *2
  time: *3
password_digest: "$2a$10$hRCY59oDn.dV4ODfo9mHoOdgbGbRBvgMU9hbM74fI5LB.8kBAL7XK"
=> nil
>> y user.attributes
---
id: 1
name: El Duderino
email: [email protected]
created_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &1 2019-07-16 07:05:28.752110000 Z
  zone: &2 !ruby/object:ActiveSupport::TimeZone
    name: Etc/UTC
  time: *1
updated_at: !ruby/object:ActiveSupport::TimeWithZone
  utc: &3 2019-07-16 07:15:41.704158000 Z
  zone: *2
  time: *3
password_digest: "$2a$10$hRCY59oDn.dV4ODfo9mHoOdgbGbRBvgMU9hbM74fI5LB.8kBAL7XK"
=> nil

演習7.4.2

>> "#{:success}"
=> "success"

>> flash = { success: "It worked!", danger: "It failed." }
=> {:success=>"It worked!", :danger=>"It failed."}
>> "#{flash[:success]}"
=> "It worked!"
>> "#{flash[:danger]}"
=> "It failed."

演習7.4.4.4

$ rails t
Running via Spring preloader in process 16365
Started with run options --seed 21445

 FAIL["test_invalid_signup_information", UsersSignupTest, 0.3632701909991738]
 test_invalid_signup_information#UsersSignupTest (0.36s)
        Expected at least 1 element matching "div#error_explanation", found 0..
        Expected 0 to be >= 1.
        test/integration/users_signup_test.rb:14:in `block in <class:UsersSignupTest>'

 FAIL["test_valid_signup_information", UsersSignupTest, 0.3775088170004892]
 test_valid_signup_information#UsersSignupTest (0.38s)
        "User.count" didn't change by 1.
        Expected: 1
          Actual: 0
        test/integration/users_signup_test.rb:21:in `block in <class:UsersSignupTest>'

  21/21: [===============================================================================================================================================] 100% Time: 00:00:00, Time: 00:00:00

Finished in 0.47808s
21 tests, 43 assertions, 2 failures, 0 errors, 0 skips

@user.save が実行されておらず、 User.count が増えていないからエラーが起きていると考えられる

プロのデプロイに無事成功した様子
プロのデプロイに無事成功した様子

第8章 基本的なログイン機構(所要時間: 1.5h)

この章では、ログインの基本的な仕組みを主に実装し、ユーザーがログイン・ログアウトできるようにします。

学ぶこと

  • セッションの実装
  • ログイン・ログアウト機能の実装

演習8.1.3

>> user = nil
=> nil
>> !!(user && user.authenticate('foobar'))
=> false
>> user = User.first
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "[email protected]", created_at: "2019-07-16 09:27:59", updated_at: "2019-07-16 09:27:59", password_digest: "$2a$10$ZVvp2d9hOoCkysaKb5vb6.nX3Wi7MWu.Q1zfZb1eLld...">
>> !!(user && user.authenticate('foobar'))
=> true

演習8.2.2

>> User.find_by(id: 100)
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 100], ["LIMIT", 1]]
=> nil
>> session = {}
=> {}
>> session[:user_id] = nil
=> nil
>> @current_user ||= User.find_by(id: session[:user_id])
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" IS NULL LIMIT ?  [["LIMIT", 1]]
=> nil
>> session[:user_id] = User.first.id
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> 1
>> @current_user ||= User.find_by(id: session[:user_id])
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "[email protected]", created_at: "2019-07-16 09:27:59", updated_at: "2019-07-16 09:27:59", password_digest: "$2a$10$ZVvp2d9hOoCkysaKb5vb6.nX3Wi7MWu.Q1zfZb1eLld...">
>> @current_user ||= User.find_by(id: session[:user_id])
=> #<User id: 1, name: "Rails Tutorial", email: "[email protected]", created_at: "2019-07-16 09:27:59", updated_at: "2019-07-16 09:27:59", password_digest: "$2a$10$ZVvp2d9hOoCkysaKb5vb6.nX3Wi7MWu.Q1zfZb1eLld...">

第9章 発展的なログイン機構(所要時間: 2.5h)

この章では永続クッキーを利用して remember me 機能を実装します。

学ぶこと

  • remember me 機能(ログイン状態の保持)の実装
  • 永続クッキー(permanent cookies)
  • 永続セッションは記憶トークンと記憶ダイジェストをユーザーごとに関連付けて実現する
  • 三項演算子

Cookieを盗み出す4つの方法と対策

方法 対策
管理の甘いネットワークを通過するネットワークバケットから直接 cookie を取り出す SSL をサイト全体に適用し、ネットワークデータを暗号化で保護する
データベースから記憶トークンを取り出す 記憶トークンのハッシュ値を保存する
クロスサイトスクリプティング(XSS)を使う Rails がビューのテンプレートで入力した内容をすべて自動的にエスケープしてくれる
ユーザーがログインしているデバイスを直接操作してアクセスを奪い取る 根本的に防衛することは出来ないが、デジタル署名を導入するなどして二次被害を最小限に留めることは可能

演習9.1.1.1

>> user = User.first
  User Load (0.1ms)  SELECT  "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "[email protected]", created_at: "2019-07-16 09:27:59", updated_at: "2019-07-16 09:27:59", password_digest: nil, remember_digest: nil>
>> user.remember
   (0.1ms)  SAVEPOINT active_record_1
  SQL (1.6ms)  UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ?  [["updated_at", "2019-07-17 04:57:19.875840"], ["remember_digest", "$2a$10$p8U/ODxRMxsHQL84SCEdr.bgAAeZVUS5fCfYbUzUIJbSMmQwQnq1K"], ["id", 1]]
   (0.1ms)  RELEASE SAVEPOINT active_record_1
=> true
>> user.remember_token
=> "RPvZ1MInKPX_E_y7xDkMzg"
>> user.remember_digest
=> "$2a$10$p8U/ODxRMxsHQL84SCEdr.bgAAeZVUS5fCfYbUzUIJbSMmQwQnq1K"

三項演算子について

論理値によって分岐する制御がたくさん出てくるが、これらは次のような三項演算子で書き換えることが出来る。

if boolean?
  何かをする
else
  別のことをする
end
論理値? ? 何かをする : 別のことをする

if boolean?
    var = foo
  else
    var = bar
  end
var = boolean? ? foo : bar

第10章 ユーザーの更新・表示・削除(所要時間: 3h)

この章では、User リソース用の REST アクションのうち、これまで実装していなかった edit , update , index , destroy のアクションを実装して REST アクションを完成させます。

このへんまで進んでくると、あっちを修正したらこっちが error になって、こっち修正したらあっちが error になってと大変です。実際の開発もこんな雰囲気なんでしょうね。。。

学ぶこと

  • フレンドリーフォワーディング
  • ページネーションの実装
  • 管理者権限の実装

演習10.1.1.1

gravatar の a タグに rel="noopener" を追記

<div class="gravatar_edit">
  <%= gravatar_for @user %>
  <a href="http://gravatar.com/emails" target="_blank" rel="noopener">change</a>
</div>
edit.html.erb

演習10.1.3

test "unsuccessfil edit"内の一番下に以下を追記。

assert_select 'div.alert', "The form contains 4 errors."
users_edit_test.rb

ところで、リスト10.32 の redirect_back_or user って redirect_back_or @user じゃないのかな?

第11章 アカウントの有効化(所要時間: 3h)

この章では、アカウントの有効化のためのメール送信に関連する実装を行います。長く続いた登録周りの実装ももうすぐすべて終わります。

学ぶこと

  • アカウント有効化のメール送信
  • 安全なアカウント有効化のための諸々の実装
  • メタプログラミング
  • SendGrid を用いたメール送信

演習11.1.2.3

app/models/user.rb で定義した downcase_email メソッドを以下のように変更する。

# メールアドレスをすべて小文字にする
def downcase_email
  self.email.downcase!
end
app/models/user.rb

演習11.2.1

>> CGI.escape('[email protected]')
=> "foo%40example.com"
>> CGI.escape("Don't panic!")
=> "Don%27t+panic%21"

リスト11.16 develop 環境のメール設定 について

development.rb をいじるときに自分の開発環境のホスト名を記入する必要がある。普通に AWS Cloud9 を利用している場合は次のように書けばOK。

Rails.application.configure do
  .
  .
  .
  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.delivery_method = :test
  host = 'us-east-2.console.aws.amazon.com'
  config.action_mailer.default_url_options = { host: host, protocol: 'https' }
  .
  .
  .
end
development.rb

演習11.3.3

# アカウントを有効にする
def activate
  update_columns(activated: true, activated_at: Time.zone.now)
end
user.rb
def index
  @users = User.where(activated: true).paginate(page: params[:page])
end
  
def show
  @user = User.find(params[:id])
  redirect_to root_url and return unless @user.activated?
end
users_controller.rb

最後にテスト作成。non-activated なユーザーについてのテストなので、まずは fixture に有効化されてないユーザーを作ってしまう。

non_activated:
  name: Non Activated
  email: [email protected]
  password_digest: <%= User.digest('password') %>
  activated: false
  activated_at: <%= Time.zone.now %>
users.yml
require 'test_helper'

class UsersControllerTest < ActionDispatch::IntegrationTest
  
  def setup
    @user = users(:michael)
    @other_user = users(:archer)
    @non_activated_user = users(:non_activated)
  end
  
  .
  .
  .

  test "should not allow the non-activated attribute" do
    log_in_as (@non_activated_user)
    assert_not @non_activated_user.activated?
    get users_path
    assert_select "a[href=?]", user_path(@non_activated_user), count: 0
    get user_path(@non_activated_user)
    assert_redirected_to root_url
  end
end
メール認証できた!
メール認証できた!

第12章 パスワードの設定(所要時間: 2h)

この章では、パスワードの再設定機能を実装します。11章のメール認証によって、ユーザーのメアドが本人のものであるという確証がもてるようになっているのでがんばります。

学ぶこと

  • パスワード再設定のメール送信

演習12.1.3

有効なメールアドレスをフォームから送信するとこうなる
有効なメールアドレスをフォームから送信するとこうなる

コンソールを確認すると reset_digest と reset_sent_at があることが確認できる。

>> user = User.find_by(email: "[email protected]")
  User Load (0.2ms)  SELECT  "users".* FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "[email protected]"], ["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "[email protected]", created_at: "2019-07-18 10:19:59", updated_at: "2019-07-18 19:25:28", password_digest: "$2a$10$sPAoPOxaoWGHOMDbgBa7/..QBg1ykmJWn3MmfoWrZC1...", remember_digest: nil, admin: true, activation_digest: "$2a$10$qACNkbRLJk5agboZegn.PeG2t/UvsStgF08NTrSssAZ...", activated: true, activated_at: "2019-07-18 10:19:58", reset_digest: "$2a$10$FCIa5J6CuCyHodOkojNz7ugU7yMpxku0WlN/.DVdcoJ...", reset_sent_at: "2019-07-18 19:25:28">
>> user.reset_digest
=> "$2a$10$FCIa5J6CuCyHodOkojNz7ugU7yMpxku0WlN/.DVdcoJsDDpWy45Im"
>> user.reset_sent_at
=> Thu, 18 Jul 2019 19:25:28 UTC +00:00

演習12.2.1

Rails サーバーのログに載っている生成されたメールの内容は次のようになっている。

----==_mimepart_5d30caa7feb7_188218e22e434e
Content-Type: text/plain;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

To reset your password click the link below:

https://us-east-2.console.aws.amazon.com/password_resets/p1vTQEI12jYizTl92x7ZjQ/edit?email=example%40railstutorial.org



This link will expire in two hours.

If you did not request your password to be reset, please ignore this email and
your password will stay as it is.


----==_mimepart_5d30caa7feb7_188218e22e434e
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <style>
      /* Email styles need to be inline */
    </style>
  </head>

  <body>
    <h1>Password reset</h1>

<p>To reset your password click the link below:</p>

<a href="https://us-east-2.console.aws.amazon.com/password_resets/p1vTQEI12jYizTl92x7ZjQ/edit?email=example%40railstutorial.org">Reset password</a>

<p>This link will expire in two hours.</p>

<p>
If you did not request your password to be reset, please ignore this email and
your password will stay as it is.
</p>

  </body>
</html>

----==_mimepart_5d30caa7feb7_188218e22e434e--
>> user.reset_digest
=> "$2a$10$HcqqTj1V7OylgtoKHdhXFeu1c6EUU5ed3dcHgXNNzAgPZ/hVLHa3i"
>> user.reset_sent_at
=> Thu, 18 Jul 2019 19:38:15 UTC +00:00

演習12.3.1

パスワード再設定のフォーム
パスワード再設定のフォーム
新しいパスワードを送信しようとすると現時点ではこうなる
新しいパスワードを送信しようとすると現時点ではこうなる

演習12.3.2

password と confirmation の文字列をわざと間違えると次の画面のように表示される。

>> user.password_digest
=> "$2a$10$sPAoPOxaoWGHOMDbgBa7/..QBg1ykmJWn3MmfoWrZC1eEIsbvcMAu"
>> user.reload
  User Load (0.3ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "[email protected]", created_at: "2019-07-18 10:19:59", updated_at: "2019-07-18 20:06:20", password_digest: "$2a$10$4odk9k7j4KV9jMBtCKTgC.C0auEoKY9FvGGUn9y/IEW...", remember_digest: nil, admin: true, activation_digest: "$2a$10$qACNkbRLJk5agboZegn.PeG2t/UvsStgF08NTrSssAZ...", activated: true, activated_at: "2019-07-18 10:19:58", reset_digest: "$2a$10$HcqqTj1V7OylgtoKHdhXFeu1c6EUU5ed3dcHgXNNzAg...", reset_sent_at: "2019-07-18 19:38:15">
>> user.password_digest
=> "$2a$10$4odk9k7j4KV9jMBtCKTgC.C0auEoKY9FvGGUn9y/IEWdmJ7ad5ojS"
パスワード再設定前後の password_digest の値の比較。変わっていることが分かる。

演習12.3.3

# パスワード再設定用の属性を設定する
def create_reset_digest
  self.reset_token = User.new_token
  update_columns(reset_digest: User.digest(reset_token), reset_sent_at: Time.zone.now)
end
user.rb
require 'test_helper'

class PasswordResetsTest < ActionDispatch::IntegrationTest
  
  .
  .
  .

  test "expired token" do
    get new_password_reset_path
    post password_resets_path,
        params: { password_reset: { email: @user.email } }
    
    @user = assigns(:user)
    @user.update_attribute(:reset_sent_at, 3.hours.ago)
    patch password_reset_path(@user.reset_token),
        params: { email: @user.email,
              user: { password:              "foobar",
                    password_confirmation:   "foobar" } }
    assert_response :redirect
    follow_redirect!
    assert_match "expired", response.body
  end
end
password_resets_test.rb

4.では3.で実装した「パスワードの再設定に成功したらダイジェストを nil にする」のテストを書けば OK です。

    # 有効なパスワードとパスワード確認
    patch password_reset_path(user.reset_token),
        params: { email: user.email,
                  user: { password:               "foobaz",
                          password_confirmation:  "foobaz" } }
    assert_nil user.reload.reset_digest
    assert is_logged_in?
    assert_not flash.empty?
    assert_redirected_to user
password_resets_test.rb
メール来た!
メール来た!